620 lines
28 KiB
Python
620 lines
28 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import date, datetime, timezone
|
|
from urllib.parse import quote
|
|
|
|
from fastapi import APIRouter, Form, Request
|
|
from fastapi.responses import RedirectResponse
|
|
from sqlalchemy import select
|
|
|
|
from app.core.db.common import CommonSessionLocal
|
|
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
|
|
from app.core.security.session_auth import get_current_user
|
|
from app.core.templating import templates
|
|
from app.modules.clients.models import Client
|
|
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
|
from app.modules.core.rbac.permission_guard import require_permission
|
|
from app.modules.documents.services import build_document_scope, get_active_storage_node_for_branch
|
|
from app.modules.accounting.agent_bridge import request_agent_command
|
|
from app.modules.registrations.models import ClientRegistration, RegistrationType
|
|
|
|
|
|
router = APIRouter(prefix="/tools/tally", tags=["accounting-tally-ui"])
|
|
|
|
|
|
def _denied():
|
|
from app.core.http_responses import ui_access_denied
|
|
return ui_access_denied()
|
|
|
|
|
|
def _require_partner(request: Request, db, permission: str):
|
|
user = get_current_user(request, db)
|
|
if not user:
|
|
return None, RedirectResponse(url="/login", status_code=303)
|
|
roles = set(get_user_roles(db, user.id))
|
|
if "Partner" not in roles:
|
|
return None, _denied()
|
|
try:
|
|
require_permission(db, user, permission)
|
|
except Exception:
|
|
return None, _denied()
|
|
return user, None
|
|
|
|
|
|
def _visible_clients(db, request: Request, user):
|
|
scope = build_document_scope(request, db, user)
|
|
stmt = select(Client).where(Client.tenant_id == scope.tenant_id, Client.partner_id == user.id)
|
|
if scope.branch_id is not None:
|
|
stmt = stmt.where(Client.branch_id == scope.branch_id)
|
|
return db.execute(stmt.order_by(Client.client_name.asc(), Client.id.asc())).scalars().all(), scope
|
|
|
|
|
|
def _find_visible_client(db, request: Request, user, client_id: int):
|
|
clients, scope = _visible_clients(db, request, user)
|
|
client = next((row for row in clients if int(row.id) == int(client_id)), None)
|
|
return client, clients, scope
|
|
|
|
|
|
def _client_registrations(db, client, tenant_id: int):
|
|
rows = db.execute(
|
|
select(ClientRegistration, RegistrationType)
|
|
.join(RegistrationType, RegistrationType.id == ClientRegistration.registration_type_id)
|
|
.where(
|
|
ClientRegistration.tenant_id == tenant_id,
|
|
ClientRegistration.client_id == client.id,
|
|
)
|
|
.order_by(RegistrationType.sort_order.asc(), ClientRegistration.id.asc())
|
|
).all()
|
|
return rows
|
|
|
|
|
|
def _registration_payload(registration, registration_type) -> dict:
|
|
return {
|
|
"id": int(registration.id),
|
|
"registration_type_id": int(registration.registration_type_id),
|
|
"registration_type_code": str(getattr(registration_type, "code", "") or "").strip(),
|
|
"registration_type_name": str(getattr(registration_type, "name", "") or "").strip(),
|
|
"registration_number": str(getattr(registration, "registration_number", "") or "").strip(),
|
|
"legal_name": str(getattr(registration, "legal_name", "") or "").strip(),
|
|
"trade_name": str(getattr(registration, "trade_name", "") or "").strip(),
|
|
"business_unit_id": getattr(registration, "business_unit_id", None),
|
|
"client_branch_id": getattr(registration, "client_branch_id", None),
|
|
}
|
|
|
|
|
|
def _node_online(node) -> bool:
|
|
if not node or not node.last_seen_at_utc:
|
|
return False
|
|
seen = node.last_seen_at_utc
|
|
if seen.tzinfo is None:
|
|
seen = seen.replace(tzinfo=timezone.utc)
|
|
return (datetime.now(timezone.utc) - seen).total_seconds() <= 180
|
|
|
|
|
|
def _render(request: Request, db, user, **context):
|
|
base = {
|
|
"request": request,
|
|
"current_user": user,
|
|
"current_user_roles": get_user_roles(db, user.id),
|
|
"current_user_permissions": get_user_permissions(db, user.id),
|
|
"csrf_token": get_or_create_csrf_token(request),
|
|
}
|
|
base.update(context)
|
|
return templates.TemplateResponse(
|
|
"modules/accounting/templates/accounting/tally.html",
|
|
base,
|
|
)
|
|
|
|
|
|
@router.get("")
|
|
def tally_tool(
|
|
request: Request,
|
|
client_id: int | None = None,
|
|
refresh: int = 0,
|
|
initialized: int = 0,
|
|
mapped: int = 0,
|
|
unmapped: int = 0,
|
|
synced: int = 0,
|
|
transactions_synced: int = 0,
|
|
error: str = "",
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.tally.view")
|
|
if response:
|
|
return response
|
|
|
|
clients, scope = _visible_clients(db, request, user)
|
|
selected_client = next((row for row in clients if client_id and int(row.id) == int(client_id)), None)
|
|
registrations = _client_registrations(db, selected_client, scope.tenant_id) if selected_client else []
|
|
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
|
online = _node_online(node)
|
|
|
|
live_result = None
|
|
command_error = error or ""
|
|
should_query_agent = bool(refresh or selected_client)
|
|
|
|
if refresh:
|
|
try:
|
|
require_permission(db, user, "accounting.tally.connect")
|
|
except Exception:
|
|
return _denied()
|
|
|
|
if should_query_agent and node and online:
|
|
payload = {}
|
|
if selected_client:
|
|
payload = {
|
|
"client_id": int(selected_client.id),
|
|
"client_name": selected_client.client_name,
|
|
}
|
|
try:
|
|
response_data = request_agent_command(
|
|
node.node_code,
|
|
"phase4_status",
|
|
payload,
|
|
timeout_seconds=20,
|
|
)
|
|
if response_data.get("ok"):
|
|
live_result = response_data.get("result") or {}
|
|
else:
|
|
command_error = str(response_data.get("error") or "Local agent command failed.")
|
|
except Exception as exc:
|
|
command_error = str(exc)
|
|
|
|
today = date.today()
|
|
fy_start_year = today.year if today.month >= 4 else today.year - 1
|
|
transaction_date_from = date(fy_start_year, 4, 1).isoformat()
|
|
transaction_date_to = today.isoformat()
|
|
|
|
return _render(
|
|
request,
|
|
db,
|
|
user,
|
|
title="Tally Accounting",
|
|
clients=clients,
|
|
selected_client=selected_client,
|
|
registrations=registrations,
|
|
storage_node=node,
|
|
agent_online=online,
|
|
live_result=live_result,
|
|
initialized=bool(initialized),
|
|
mapped=bool(mapped),
|
|
unmapped=bool(unmapped),
|
|
synced=bool(synced),
|
|
transactions_synced=bool(transactions_synced),
|
|
transaction_date_from=transaction_date_from,
|
|
transaction_date_to=transaction_date_to,
|
|
command_error=command_error,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/initialize")
|
|
def initialize_accounting_storage(
|
|
request: Request,
|
|
client_id: int = Form(...),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.act.initialize")
|
|
if response:
|
|
return response
|
|
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
return _denied()
|
|
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
|
if not node or not _node_online(node):
|
|
return RedirectResponse(
|
|
url=f"/tools/tally?client_id={client.id}&error={quote('ERP Local Agent is offline for the active branch.')}",
|
|
status_code=303,
|
|
)
|
|
try:
|
|
result = request_agent_command(
|
|
node.node_code,
|
|
"accounting_initialize",
|
|
{
|
|
"client_id": int(client.id),
|
|
"client_name": client.client_name,
|
|
"tenant_id": int(scope.tenant_id),
|
|
"requested_by_user_id": int(user.id),
|
|
},
|
|
timeout_seconds=20,
|
|
)
|
|
if not result.get("ok"):
|
|
raise RuntimeError(str(result.get("error") or "Accounting storage initialization failed."))
|
|
except Exception as exc:
|
|
return RedirectResponse(
|
|
url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}",
|
|
status_code=303,
|
|
)
|
|
return RedirectResponse(
|
|
url=f"/tools/tally?client_id={client.id}&refresh=1&initialized=1",
|
|
status_code=303,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/map-company")
|
|
def map_tally_company(
|
|
request: Request,
|
|
client_id: int = Form(...),
|
|
tally_guid: str = Form(...),
|
|
registration_id: str = Form(""),
|
|
allow_gstin_mismatch: bool = Form(False),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.tally.map_company")
|
|
if response:
|
|
return response
|
|
|
|
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
return _denied()
|
|
|
|
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
|
if not node or not _node_online(node):
|
|
return RedirectResponse(
|
|
url=f"/tools/tally?client_id={client.id}&error={quote('ERP Local Agent is offline for the active branch.')}",
|
|
status_code=303,
|
|
)
|
|
|
|
registration_payload = None
|
|
if str(registration_id or "").strip():
|
|
try:
|
|
rid = int(registration_id)
|
|
except Exception:
|
|
return RedirectResponse(
|
|
url=f"/tools/tally?client_id={client.id}&error={quote('Invalid registration selection.')}",
|
|
status_code=303,
|
|
)
|
|
|
|
row = db.execute(
|
|
select(ClientRegistration, RegistrationType)
|
|
.join(RegistrationType, RegistrationType.id == ClientRegistration.registration_type_id)
|
|
.where(
|
|
ClientRegistration.id == rid,
|
|
ClientRegistration.client_id == client.id,
|
|
ClientRegistration.tenant_id == scope.tenant_id,
|
|
)
|
|
).first()
|
|
if not row:
|
|
return _denied()
|
|
registration_payload = _registration_payload(row[0], row[1])
|
|
|
|
try:
|
|
result = request_agent_command(
|
|
node.node_code,
|
|
"accounting_map_company",
|
|
{
|
|
"client_id": int(client.id),
|
|
"client_name": client.client_name,
|
|
"tenant_id": int(scope.tenant_id),
|
|
"registration": registration_payload,
|
|
"tally_guid": str(tally_guid or "").strip(),
|
|
"allow_gstin_mismatch": bool(allow_gstin_mismatch),
|
|
"mapped_by_user_id": int(user.id),
|
|
},
|
|
timeout_seconds=20,
|
|
)
|
|
if not result.get("ok"):
|
|
raise RuntimeError(str(result.get("error") or "Tally company mapping failed."))
|
|
except Exception as exc:
|
|
return RedirectResponse(
|
|
url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}",
|
|
status_code=303,
|
|
)
|
|
|
|
return RedirectResponse(
|
|
url=f"/tools/tally?client_id={client.id}&refresh=1&mapped=1",
|
|
status_code=303,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/unmap-company")
|
|
def unmap_tally_company(
|
|
request: Request,
|
|
client_id: int = Form(...),
|
|
mapping_id: int = Form(...),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.tally.map_company")
|
|
if response:
|
|
return response
|
|
|
|
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
return _denied()
|
|
|
|
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
|
if not node or not _node_online(node):
|
|
return RedirectResponse(
|
|
url=f"/tools/tally?client_id={client.id}&error={quote('ERP Local Agent is offline for the active branch.')}",
|
|
status_code=303,
|
|
)
|
|
|
|
try:
|
|
result = request_agent_command(
|
|
node.node_code,
|
|
"accounting_unmap_company",
|
|
{
|
|
"client_id": int(client.id),
|
|
"mapping_id": int(mapping_id),
|
|
"unmapped_by_user_id": int(user.id),
|
|
},
|
|
timeout_seconds=20,
|
|
)
|
|
if not result.get("ok"):
|
|
raise RuntimeError(str(result.get("error") or "Tally company unmapping failed."))
|
|
except Exception as exc:
|
|
return RedirectResponse(
|
|
url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}",
|
|
status_code=303,
|
|
)
|
|
|
|
return RedirectResponse(
|
|
url=f"/tools/tally?client_id={client.id}&refresh=1&unmapped=1",
|
|
status_code=303,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/sync-masters")
|
|
def sync_tally_masters(
|
|
request: Request,
|
|
client_id: int = Form(...),
|
|
tally_guid: str = Form(...),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.tally.sync_masters")
|
|
if response:
|
|
return response
|
|
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
return _denied()
|
|
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
|
if not node or not _node_online(node):
|
|
return RedirectResponse(
|
|
url=f"/tools/tally?client_id={client.id}&error={quote('ERP Local Agent is offline for the active branch.')}",
|
|
status_code=303,
|
|
)
|
|
try:
|
|
result = request_agent_command(
|
|
node.node_code,
|
|
"accounting_sync_masters",
|
|
{
|
|
"client_id": int(client.id),
|
|
"tally_guid": str(tally_guid or "").strip(),
|
|
"requested_by_user_id": int(user.id),
|
|
},
|
|
timeout_seconds=120,
|
|
)
|
|
if not result.get("ok"):
|
|
raise RuntimeError(str(result.get("error") or "Tally master synchronization failed."))
|
|
except Exception as exc:
|
|
return RedirectResponse(
|
|
url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}",
|
|
status_code=303,
|
|
)
|
|
return RedirectResponse(
|
|
url=f"/tools/tally?client_id={client.id}&refresh=1&synced=1",
|
|
status_code=303,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/sync-transactions")
|
|
def sync_tally_transactions(
|
|
request: Request,
|
|
client_id: int = Form(...),
|
|
tally_guid: str = Form(...),
|
|
date_from: str = Form(...),
|
|
date_to: str = Form(...),
|
|
csrf_token: str = Form(...),
|
|
):
|
|
validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.tally.sync_transactions")
|
|
if response:
|
|
return response
|
|
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client:
|
|
return _denied()
|
|
try:
|
|
start = date.fromisoformat(str(date_from or ""))
|
|
end = date.fromisoformat(str(date_to or ""))
|
|
if start > end:
|
|
raise ValueError("From Date cannot be after To Date.")
|
|
if (end - start).days > 370:
|
|
raise ValueError("Transaction sync is limited to 371 days per run. Sync additional periods separately.")
|
|
except Exception as exc:
|
|
return RedirectResponse(url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}", status_code=303)
|
|
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
|
if not node or not _node_online(node):
|
|
return RedirectResponse(url=f"/tools/tally?client_id={client.id}&error={quote('ERP Local Agent is offline for the active branch.')}", status_code=303)
|
|
try:
|
|
result = request_agent_command(
|
|
node.node_code,
|
|
"accounting_sync_transactions",
|
|
{
|
|
"client_id": int(client.id),
|
|
"tally_guid": str(tally_guid or "").strip(),
|
|
"date_from": start.isoformat(),
|
|
"date_to": end.isoformat(),
|
|
"requested_by_user_id": int(user.id),
|
|
},
|
|
timeout_seconds=300,
|
|
)
|
|
if not result.get("ok"):
|
|
raise RuntimeError(str(result.get("error") or "Tally transaction synchronization failed."))
|
|
except Exception as exc:
|
|
return RedirectResponse(url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}", status_code=303)
|
|
return RedirectResponse(url=f"/tools/tally?client_id={client.id}&refresh=1&transactions_synced=1", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
@router.get("/depreciation")
|
|
def depreciation_it_tool(
|
|
request: Request,
|
|
client_id: int | None = None,
|
|
tally_guid: str = "",
|
|
fy_start: str = "",
|
|
fy_end: str = "",
|
|
calculated: int = 0,
|
|
approved: int = 0,
|
|
posted: int = 0,
|
|
run_id: int | None = None,
|
|
error: str = "",
|
|
):
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.tally.depreciation_it")
|
|
if response: return response
|
|
clients, scope = _visible_clients(db, request, user)
|
|
selected_client = next((row for row in clients if client_id and int(row.id) == int(client_id)), None)
|
|
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
|
online = _node_online(node)
|
|
today = date.today(); start_year = today.year if today.month >= 4 else today.year - 1
|
|
start_text = fy_start or date(start_year,4,1).isoformat(); end_text = fy_end or date(start_year+1,3,31).isoformat()
|
|
live_result = None; preview = None; depreciation_run = None; command_error = error or ""
|
|
if selected_client and node and online:
|
|
try:
|
|
status_response = request_agent_command(node.node_code,"phase6_status",{"client_id":int(selected_client.id)},timeout_seconds=20)
|
|
if status_response.get("ok"): live_result=status_response.get("result") or {}
|
|
else: command_error=str(status_response.get("error") or "Local Agent status failed.")
|
|
chosen_guid=str(tally_guid or "").strip()
|
|
if chosen_guid:
|
|
preview_response=request_agent_command(node.node_code,"accounting_depreciation_preview",{"client_id":int(selected_client.id),"tally_guid":chosen_guid,"fy_start":start_text,"fy_end":end_text},timeout_seconds=60)
|
|
if preview_response.get("ok"): preview=(preview_response.get("result") or {}).get("preview")
|
|
else: command_error=str(preview_response.get("error") or "Depreciation preview failed.")
|
|
if run_id:
|
|
run_response=request_agent_command(node.node_code,"accounting_get_it_depreciation_run",{"client_id":int(selected_client.id),"run_id":int(run_id)},timeout_seconds=30)
|
|
if run_response.get("ok"): depreciation_run=(run_response.get("result") or {}).get("depreciation")
|
|
except Exception as exc: command_error=str(exc)
|
|
base={"request":request,"current_user":user,"current_user_roles":get_user_roles(db,user.id),"current_user_permissions":get_user_permissions(db,user.id),"csrf_token":get_or_create_csrf_token(request)}
|
|
base.update({"title":"Income-tax Depreciation","clients":clients,"selected_client":selected_client,"storage_node":node,"agent_online":online,"live_result":live_result,"preview":preview,"selected_tally_guid":str(tally_guid or ""),"fy_start":start_text,"fy_end":end_text,"calculated":bool(calculated),"approved":bool(approved),"posted":bool(posted),"depreciation_run":depreciation_run,"command_error":command_error})
|
|
return templates.TemplateResponse("modules/accounting/templates/accounting/depreciation_it.html",base)
|
|
finally: db.close()
|
|
|
|
|
|
@router.post("/depreciation/calculate")
|
|
async def calculate_it_depreciation(request: Request):
|
|
form = await request.form()
|
|
csrf_token = str(form.get("csrf_token") or ""); validate_csrf(request, csrf_token)
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.tally.depreciation_it")
|
|
if response: return response
|
|
try: client_id=int(form.get("client_id")); start=date.fromisoformat(str(form.get("fy_start") or "")); end=date.fromisoformat(str(form.get("fy_end") or ""))
|
|
except Exception as exc: return RedirectResponse(url=f"/tools/tally/depreciation?error={quote('Invalid client or financial-year dates.')}",status_code=303)
|
|
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client: return _denied()
|
|
tally_guid=str(form.get("tally_guid") or "").strip()
|
|
assignments=[]
|
|
for key,value in form.multi_items():
|
|
if not str(key).startswith("rate__"): continue
|
|
ledger_key=str(key)[6:]
|
|
try: rate=float(str(value or "0").strip() or 0)
|
|
except Exception: rate=-1
|
|
assignments.append({"ledger_key":ledger_key,"rate_percent":rate})
|
|
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
|
|
if not node or not _node_online(node): return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&tally_guid={quote(tally_guid)}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&error={quote('ERP Local Agent is offline for the active branch.')}",status_code=303)
|
|
try:
|
|
result=request_agent_command(node.node_code,"accounting_calculate_it_depreciation",{"client_id":int(client.id),"tally_guid":tally_guid,"fy_start":start.isoformat(),"fy_end":end.isoformat(),"assignments":assignments,"depreciation_expense_ledger":str(form.get("depreciation_expense_ledger") or ""),"depreciation_reserve_ledger":str(form.get("depreciation_reserve_ledger") or ""),"requested_by_user_id":int(user.id)},timeout_seconds=120)
|
|
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Income-tax depreciation calculation failed."))
|
|
dep=(result.get("result") or {}).get("depreciation") or {}; rid=int(dep.get("run_id"))
|
|
except Exception as exc:
|
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&tally_guid={quote(tally_guid)}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&error={quote(str(exc))}",status_code=303)
|
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&tally_guid={quote(tally_guid)}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&calculated=1&run_id={rid}",status_code=303)
|
|
finally: db.close()
|
|
|
|
|
|
|
|
@router.post("/depreciation/approve")
|
|
async def approve_it_depreciation(request: Request):
|
|
form = await request.form()
|
|
validate_csrf(request, str(form.get("csrf_token") or ""))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.tally.writeback")
|
|
if response: return response
|
|
try:
|
|
client_id = int(form.get("client_id")); run_id = int(form.get("run_id"))
|
|
except Exception:
|
|
return RedirectResponse(url="/tools/tally/depreciation?error=" + quote("Invalid depreciation draft."), status_code=303)
|
|
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client: return _denied()
|
|
if str(form.get("confirm_approval") or "") != "yes":
|
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&run_id={run_id}&error={quote('Confirm that you reviewed the depreciation draft before approval.')}", status_code=303)
|
|
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
|
if not node or not _node_online(node):
|
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&run_id={run_id}&error={quote('ERP Local Agent is offline for the active branch.')}", status_code=303)
|
|
try:
|
|
result = request_agent_command(
|
|
node.node_code, "accounting_approve_it_depreciation",
|
|
{"client_id": client_id, "run_id": run_id, "approved_by_user_id": int(user.id), "approval_note": str(form.get("approval_note") or "")},
|
|
timeout_seconds=30,
|
|
)
|
|
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Approval failed."))
|
|
dep = (result.get("result") or {}).get("depreciation") or {}
|
|
guid = quote(str(dep.get("tally_guid") or ""))
|
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&tally_guid={guid}&fy_start={dep.get('fy_start','')}&fy_end={dep.get('fy_end','')}&run_id={run_id}&approved=1", status_code=303)
|
|
except Exception as exc:
|
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&run_id={run_id}&error={quote(str(exc))}", status_code=303)
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
@router.post("/depreciation/post-to-tally")
|
|
async def post_it_depreciation_to_tally(request: Request):
|
|
form = await request.form()
|
|
validate_csrf(request, str(form.get("csrf_token") or ""))
|
|
db = CommonSessionLocal()
|
|
try:
|
|
user, response = _require_partner(request, db, "accounting.tally.writeback")
|
|
if response: return response
|
|
try:
|
|
client_id = int(form.get("client_id")); run_id = int(form.get("run_id"))
|
|
except Exception:
|
|
return RedirectResponse(url="/tools/tally/depreciation?error=" + quote("Invalid depreciation draft."), status_code=303)
|
|
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
|
if not client: return _denied()
|
|
if str(form.get("post_confirmation") or "").strip().upper() != "POST":
|
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&run_id={run_id}&error={quote('Type POST exactly to confirm Tally write-back.')}", status_code=303)
|
|
if str(form.get("confirm_posting") or "") != "yes":
|
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&run_id={run_id}&error={quote('Confirm that you want to create the Journal voucher in TallyPrime.')}", status_code=303)
|
|
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
|
if not node or not _node_online(node):
|
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&run_id={run_id}&error={quote('ERP Local Agent is offline for the active branch.')}", status_code=303)
|
|
try:
|
|
result = request_agent_command(
|
|
node.node_code, "accounting_post_it_depreciation",
|
|
{"client_id": client_id, "run_id": run_id, "posted_by_user_id": int(user.id)},
|
|
timeout_seconds=120,
|
|
)
|
|
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Tally write-back failed."))
|
|
dep = (result.get("result") or {}).get("depreciation") or {}
|
|
guid = quote(str(dep.get("tally_guid") or ""))
|
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&tally_guid={guid}&fy_start={dep.get('fy_start','')}&fy_end={dep.get('fy_end','')}&run_id={run_id}&posted=1", status_code=303)
|
|
except Exception as exc:
|
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&run_id={run_id}&error={quote(str(exc))}", status_code=303)
|
|
finally:
|
|
db.close()
|