Unify accounting analysis on local SQLite mirror
This commit is contained in:
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
from datetime import datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, Form, Request
|
from fastapi import APIRouter, Form, Request
|
||||||
from fastapi.responses import RedirectResponse, StreamingResponse
|
from fastapi.responses import RedirectResponse, StreamingResponse
|
||||||
@@ -24,6 +24,7 @@ from app.modules.accounting.opening_balance_service import (
|
|||||||
)
|
)
|
||||||
from app.modules.accounting.ui import (
|
from app.modules.accounting.ui import (
|
||||||
_accounting_storage_payload,
|
_accounting_storage_payload,
|
||||||
|
_financial_year_for_date,
|
||||||
_find_visible_client,
|
_find_visible_client,
|
||||||
_node_online,
|
_node_online,
|
||||||
_require_partner,
|
_require_partner,
|
||||||
@@ -118,6 +119,7 @@ def page(
|
|||||||
show_zero_new: int = 0,
|
show_zero_new: int = 0,
|
||||||
message: str = "",
|
message: str = "",
|
||||||
error: str = "",
|
error: str = "",
|
||||||
|
analysis_run_id: int = 0,
|
||||||
):
|
):
|
||||||
db = CommonSessionLocal()
|
db = CommonSessionLocal()
|
||||||
try:
|
try:
|
||||||
@@ -136,20 +138,35 @@ def page(
|
|||||||
tally = None
|
tally = None
|
||||||
companies = []
|
companies = []
|
||||||
command_error = error or ""
|
command_error = error or ""
|
||||||
|
today = date.today()
|
||||||
|
current_fy = _financial_year_for_date(today)
|
||||||
|
current_start = int(current_fy.split("-", 1)[0])
|
||||||
|
previous_fy = f"{current_start-1}-{str(current_start)[-2:]}"
|
||||||
|
previous_mirror_status = None
|
||||||
|
current_mirror_status = None
|
||||||
|
|
||||||
if selected and node and online:
|
if selected and node and online:
|
||||||
try:
|
try:
|
||||||
response = request_agent_command(
|
prev_response = request_agent_command(
|
||||||
node.node_code,
|
node.node_code,
|
||||||
"phase6_status",
|
"accounting_mirror_status",
|
||||||
_accounting_storage_payload(selected),
|
_accounting_storage_payload(selected, previous_fy),
|
||||||
timeout_seconds=20,
|
timeout_seconds=20,
|
||||||
)
|
)
|
||||||
if response.get("ok"):
|
curr_response = request_agent_command(
|
||||||
tally = (response.get("result") or {}).get("tally")
|
node.node_code,
|
||||||
companies = _company_rows(tally)
|
"accounting_mirror_status",
|
||||||
else:
|
_accounting_storage_payload(selected, current_fy),
|
||||||
command_error = str(response.get("error") or "Local Agent status failed.")
|
timeout_seconds=20,
|
||||||
|
)
|
||||||
|
if prev_response.get("ok"):
|
||||||
|
previous_mirror_status = prev_response.get("result") or {}
|
||||||
|
if curr_response.get("ok"):
|
||||||
|
current_mirror_status = curr_response.get("result") or {}
|
||||||
|
if not prev_response.get("ok"):
|
||||||
|
command_error = str(prev_response.get("error") or "Previous-year Accounting Mirror status failed.")
|
||||||
|
elif not curr_response.get("ok"):
|
||||||
|
command_error = str(curr_response.get("error") or "Current-year Accounting Mirror status failed.")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
command_error = str(exc)
|
command_error = str(exc)
|
||||||
|
|
||||||
@@ -467,8 +484,8 @@ def download_report(request: Request, run_id: int, client_id: int | None = None)
|
|||||||
def compare(
|
def compare(
|
||||||
request: Request,
|
request: Request,
|
||||||
client_id: int = Form(...),
|
client_id: int = Form(...),
|
||||||
previous_company_name: str = Form(...),
|
previous_fy: str = Form(...),
|
||||||
current_company_name: str = Form(...),
|
current_fy: str = Form(...),
|
||||||
csrf_token: str = Form(...),
|
csrf_token: str = Form(...),
|
||||||
):
|
):
|
||||||
validate_csrf(request, csrf_token)
|
validate_csrf(request, csrf_token)
|
||||||
@@ -477,32 +494,30 @@ def compare(
|
|||||||
user, denied = _require_partner(request, db, "accounting.tally.sync_masters")
|
user, denied = _require_partner(request, db, "accounting.tally.sync_masters")
|
||||||
if denied:
|
if denied:
|
||||||
return denied
|
return denied
|
||||||
|
|
||||||
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
||||||
if not client:
|
if not client:
|
||||||
return _go(error="Client is not visible.")
|
return _go(error="Client is not visible.")
|
||||||
if previous_company_name.strip().casefold() == current_company_name.strip().casefold():
|
if previous_fy.strip() == current_fy.strip():
|
||||||
raise ValueError(
|
raise ValueError("Previous-year and current-year mirrors must be different.")
|
||||||
"Previous-year and current-year companies must be different in Phase 21A. "
|
|
||||||
"This protects against using the current company's latest closing balance as the prior-year closing."
|
|
||||||
)
|
|
||||||
|
|
||||||
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
||||||
if not node or not _node_online(node):
|
if not node or not _node_online(node):
|
||||||
raise ValueError("ERP Local Agent is offline for the active branch.")
|
raise ValueError("ERP Local Agent is offline for the active branch.")
|
||||||
|
|
||||||
|
previous_payload = _accounting_storage_payload(client, previous_fy)
|
||||||
|
current_payload = _accounting_storage_payload(client, current_fy)
|
||||||
response = request_agent_command(
|
response = request_agent_command(
|
||||||
node.node_code,
|
node.node_code,
|
||||||
"accounting_opening_balance_snapshot",
|
"accounting_opening_balance_mirror_snapshot",
|
||||||
{
|
{
|
||||||
**_accounting_storage_payload(client),
|
**current_payload,
|
||||||
"previous_company_name": previous_company_name,
|
"previous_accounting_relative_dir": previous_payload["accounting_relative_dir"],
|
||||||
"current_company_name": current_company_name,
|
"current_accounting_relative_dir": current_payload["accounting_relative_dir"],
|
||||||
},
|
},
|
||||||
timeout_seconds=180,
|
timeout_seconds=180,
|
||||||
)
|
)
|
||||||
if not response.get("ok"):
|
if not response.get("ok"):
|
||||||
raise RuntimeError(str(response.get("error") or "Opening balance snapshot failed."))
|
raise RuntimeError(str(response.get("error") or "Opening balance mirror comparison failed."))
|
||||||
|
|
||||||
result = response.get("result") or {}
|
result = response.get("result") or {}
|
||||||
run = create_comparison_run(
|
run = create_comparison_run(
|
||||||
@@ -515,10 +530,63 @@ def compare(
|
|||||||
current_masters=result.get("current_masters") or {},
|
current_masters=result.get("current_masters") or {},
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ledger_rows = ledger_items(db, run_id=run.id)
|
||||||
|
stock_rows = stock_items(db, run_id=run.id)
|
||||||
|
try:
|
||||||
|
saved = request_agent_command(
|
||||||
|
node.node_code,
|
||||||
|
"accounting_analysis_save",
|
||||||
|
{
|
||||||
|
**current_payload,
|
||||||
|
"analysis_type": "OPENING_BALANCE_REVIEW",
|
||||||
|
"financial_year": current_fy,
|
||||||
|
"period_from": previous_fy,
|
||||||
|
"period_to": current_fy,
|
||||||
|
"company_guid": run.current_company_guid,
|
||||||
|
"company_name": run.current_company_name,
|
||||||
|
"requested_by_user_id": int(user.id),
|
||||||
|
"source_mirror_path": str(result.get("current_mirror_path") or ""),
|
||||||
|
"parameters": {"previous_fy": previous_fy, "current_fy": current_fy, "erp_run_id": run.id},
|
||||||
|
"summary": json.loads(run.summary_json or "{}"),
|
||||||
|
"result": {
|
||||||
|
"erp_run_id": run.id,
|
||||||
|
"previous_company": result.get("previous_company") or {},
|
||||||
|
"current_company": result.get("current_company") or {},
|
||||||
|
"ledgers": [
|
||||||
|
{
|
||||||
|
"previous_name": x.previous_name,
|
||||||
|
"current_name": x.current_name,
|
||||||
|
"previous_closing_balance": x.previous_closing_balance,
|
||||||
|
"current_opening_balance": x.current_opening_balance,
|
||||||
|
"difference": x.difference,
|
||||||
|
"status": x.status,
|
||||||
|
} for x in ledger_rows
|
||||||
|
],
|
||||||
|
"stock_items": [
|
||||||
|
{
|
||||||
|
"previous_name": x.previous_name,
|
||||||
|
"current_name": x.current_name,
|
||||||
|
"previous_closing_qty": x.previous_closing_qty,
|
||||||
|
"current_opening_qty": x.current_opening_qty,
|
||||||
|
"previous_closing_value": x.previous_closing_value,
|
||||||
|
"current_opening_value": x.current_opening_value,
|
||||||
|
"status": x.status,
|
||||||
|
} for x in stock_rows
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timeout_seconds=30,
|
||||||
|
)
|
||||||
|
analysis_run_id = int(((saved.get("result") or {}).get("analysis_run_id") or 0)) if saved.get("ok") else 0
|
||||||
|
except Exception:
|
||||||
|
analysis_run_id = 0
|
||||||
|
|
||||||
return _go(
|
return _go(
|
||||||
client.id,
|
client.id,
|
||||||
run_id=run.id,
|
run_id=run.id,
|
||||||
tab="summary",
|
tab="summary",
|
||||||
|
analysis_run_id=analysis_run_id,
|
||||||
message=(
|
message=(
|
||||||
f"Opening balance comparison #{run.id} completed: "
|
f"Opening balance comparison #{run.id} completed: "
|
||||||
f"{run.ledger_count} ledger row(s), {run.stock_item_count} stock row(s)."
|
f"{run.ledger_count} ledger row(s), {run.stock_item_count} stock row(s)."
|
||||||
|
|||||||
@@ -67,16 +67,36 @@ def _workspace(request, db, user, client_id, tally_guid, date_from, date_to, run
|
|||||||
online = _node_online(node)
|
online = _node_online(node)
|
||||||
today=date.today(); fy=_financial_year_for_date(today); sy=int(fy.split('-',1)[0])
|
today=date.today(); fy=_financial_year_for_date(today); sy=int(fy.split('-',1)[0])
|
||||||
start=date_from or date(sy,4,1).isoformat(); end=date_to or today.isoformat()
|
start=date_from or date(sy,4,1).isoformat(); end=date_to or today.isoformat()
|
||||||
live=None; review=None; error=""
|
live=None; review=None; error=""; history=[]
|
||||||
if selected and node and online:
|
if selected and node and online:
|
||||||
st=request_agent_command(node.node_code,"phase4_status",_accounting_storage_payload(selected,_financial_year_for_date(date.fromisoformat(start))),timeout_seconds=20)
|
storage = _accounting_storage_payload(selected,_financial_year_for_date(date.fromisoformat(start)))
|
||||||
|
st=request_agent_command(node.node_code,"accounting_mirror_status",storage,timeout_seconds=20)
|
||||||
if st.get("ok"): live=st.get("result") or {}
|
if st.get("ok"): live=st.get("result") or {}
|
||||||
else: error=str(st.get("error") or "Local Agent status failed.")
|
else: error=str(st.get("error") or "Accounting Mirror status failed.")
|
||||||
if run and tally_guid:
|
if run and not error:
|
||||||
rr=request_agent_command(node.node_code,"accounting_tds_compliance",{**_accounting_storage_payload(selected,_financial_year_for_date(date.fromisoformat(start))),"tally_guid":tally_guid,"date_from":start,"date_to":end,"rules":_rule_payload(_rules(db,_tenant_id(request,user)))},timeout_seconds=120)
|
rr=request_agent_command(
|
||||||
|
node.node_code,
|
||||||
|
"accounting_tds_compliance",
|
||||||
|
{
|
||||||
|
**storage,
|
||||||
|
"date_from":start,
|
||||||
|
"date_to":end,
|
||||||
|
"rules":_rule_payload(_rules(db,_tenant_id(request,user))),
|
||||||
|
"requested_by_user_id":int(user.id),
|
||||||
|
},
|
||||||
|
timeout_seconds=120,
|
||||||
|
)
|
||||||
if rr.get("ok"): review=(rr.get("result") or {}).get("tds_review") or {}
|
if rr.get("ok"): review=(rr.get("result") or {}).get("tds_review") or {}
|
||||||
else: error=str(rr.get("error") or "TDS review failed.")
|
else: error=str(rr.get("error") or "TDS review failed.")
|
||||||
return clients, selected, node, online, live, review, start, end, error
|
try:
|
||||||
|
hist=request_agent_command(
|
||||||
|
node.node_code,"accounting_analysis_history",
|
||||||
|
{**storage,"analysis_type":"TDS_COMPLIANCE","limit":20},timeout_seconds=20
|
||||||
|
)
|
||||||
|
if hist.get("ok"): history=(hist.get("result") or {}).get("runs") or []
|
||||||
|
except Exception:
|
||||||
|
history=[]
|
||||||
|
return clients, selected, node, online, live, review, start, end, error, history
|
||||||
|
|
||||||
|
|
||||||
@router.get("/rules")
|
@router.get("/rules")
|
||||||
@@ -116,8 +136,8 @@ def compliance(request:Request,client_id:int|None=None,tally_guid:str="",date_fr
|
|||||||
try:
|
try:
|
||||||
user,response=_require_partner(request,db,"accounting.tally.view")
|
user,response=_require_partner(request,db,"accounting.tally.view")
|
||||||
if response:return response
|
if response:return response
|
||||||
clients,selected,node,online,live,review,start,end,error=_workspace(request,db,user,client_id,tally_guid,date_from,date_to,run)
|
clients,selected,node,online,live,review,start,end,error,history=_workspace(request,db,user,client_id,tally_guid,date_from,date_to,run)
|
||||||
return _render(request,db,user,"modules/accounting/templates/accounting/tds_compliance.html",clients=clients,selected_client=selected,node=node,node_online=online,live=live,review=review,date_from=start,date_to=end,tally_guid=tally_guid,error=error,rules=_rules(db,_tenant_id(request,user)))
|
return _render(request,db,user,"modules/accounting/templates/accounting/tds_compliance.html",clients=clients,selected_client=selected,node=node,node_online=online,live=live,review=review,date_from=start,date_to=end,tally_guid=tally_guid,error=error,rules=_rules(db,_tenant_id(request,user)),history=history)
|
||||||
finally: db.close()
|
finally: db.close()
|
||||||
|
|
||||||
|
|
||||||
@@ -127,10 +147,10 @@ def liability(request:Request,client_id:int|None=None,tally_guid:str="",date_fro
|
|||||||
try:
|
try:
|
||||||
user,response=_require_partner(request,db,"accounting.tally.view")
|
user,response=_require_partner(request,db,"accounting.tally.view")
|
||||||
if response:return response
|
if response:return response
|
||||||
clients,selected,node,online,live,review,start,end,run_error=_workspace(request,db,user,client_id,tally_guid,date_from,date_to,run)
|
clients,selected,node,online,live,review,start,end,run_error,history=_workspace(request,db,user,client_id,tally_guid,date_from,date_to,run)
|
||||||
instructions=[]
|
instructions=[]
|
||||||
if selected: instructions=list(db.execute(select(AccountingTDSPostingInstruction).where(AccountingTDSPostingInstruction.tenant_id==_tenant_id(request,user),AccountingTDSPostingInstruction.client_id==selected.id).order_by(AccountingTDSPostingInstruction.id.desc()).limit(100)).scalars())
|
if selected: instructions=list(db.execute(select(AccountingTDSPostingInstruction).where(AccountingTDSPostingInstruction.tenant_id==_tenant_id(request,user),AccountingTDSPostingInstruction.client_id==selected.id).order_by(AccountingTDSPostingInstruction.id.desc()).limit(100)).scalars())
|
||||||
return _render(request,db,user,"modules/accounting/templates/accounting/tds_liability.html",clients=clients,selected_client=selected,node=node,node_online=online,live=live,review=review,date_from=start,date_to=end,tally_guid=tally_guid,error=error or run_error,saved=saved,instructions=instructions)
|
return _render(request,db,user,"modules/accounting/templates/accounting/tds_liability.html",clients=clients,selected_client=selected,node=node,node_online=online,live=live,review=review,date_from=start,date_to=end,tally_guid=tally_guid,error=error or run_error,saved=saved,instructions=instructions,history=history)
|
||||||
finally:db.close()
|
finally:db.close()
|
||||||
|
|
||||||
|
|
||||||
@@ -209,7 +229,7 @@ async def reconciliation_run(request:Request):
|
|||||||
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
|
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
|
||||||
if not node or not _node_online(node): raise ValueError("Local Agent is offline.")
|
if not node or not _node_online(node): raise ValueError("Local Agent is offline.")
|
||||||
dfrom=str(form.get("date_from") or ""); dto=str(form.get("date_to") or ""); guid=str(form.get("tally_guid") or "")
|
dfrom=str(form.get("date_from") or ""); dto=str(form.get("date_to") or ""); guid=str(form.get("tally_guid") or "")
|
||||||
rr=request_agent_command(node.node_code,"accounting_tds_compliance",{**_accounting_storage_payload(selected,_financial_year_for_date(date.fromisoformat(dfrom))),"tally_guid":guid,"date_from":dfrom,"date_to":dto,"rules":_rule_payload(_rules(db,_tenant_id(request,user)))},timeout_seconds=120)
|
rr=request_agent_command(node.node_code,"accounting_tds_compliance",{**_accounting_storage_payload(selected,_financial_year_for_date(date.fromisoformat(dfrom))),"date_from":dfrom,"date_to":dto,"rules":_rule_payload(_rules(db,_tenant_id(request,user)))},timeout_seconds=120)
|
||||||
if not rr.get("ok"): raise ValueError(str(rr.get("error") or "Tally review failed."))
|
if not rr.get("ok"): raise ValueError(str(rr.get("error") or "Tally review failed."))
|
||||||
review=(rr.get("result") or {}).get("tds_review") or {}; tally_total=round(sum(float(x.get("actual_tds") or 0) for x in review.get("transactions",[])),2); expected_total=round(sum(float(x.get("expected_tds") or 0) for x in review.get("transactions",[])),2)
|
review=(rr.get("result") or {}).get("tds_review") or {}; tally_total=round(sum(float(x.get("actual_tds") or 0) for x in review.get("transactions",[])),2); expected_total=round(sum(float(x.get("expected_tds") or 0) for x in review.get("transactions",[])),2)
|
||||||
returns=_parse_external(str(form.get("return_data") or "")); challans=_parse_external(str(form.get("challan_data") or "")); traces=_parse_external(str(form.get("traces_data") or ""))
|
returns=_parse_external(str(form.get("return_data") or "")); challans=_parse_external(str(form.get("challan_data") or "")); traces=_parse_external(str(form.get("traces_data") or ""))
|
||||||
|
|||||||
@@ -91,10 +91,9 @@
|
|||||||
|
|
||||||
{% if review %}
|
{% if review %}
|
||||||
<section class="space-y-4">
|
<section class="space-y-4">
|
||||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">
|
<div class="flex flex-wrap justify-end gap-2">
|
||||||
Analysis completed entirely from the local SQLite Accounting Mirror. TallyPrime was not contacted.
|
<a href="/tools/tally/analysis/{{ review.analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ date_from[:4] }}-{{ date_to[2:4] }}&format=xlsx" class="rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-2 text-sm font-semibold text-emerald-800">Download Excel</a>
|
||||||
{% if review.cash_ledgers %} Cash ledger(s): <strong>{{ review.cash_ledgers|join(', ') }}</strong>.{% endif %}
|
<a href="/tools/tally/analysis/{{ review.analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ date_from[:4] }}-{{ date_to[2:4] }}&format=pdf" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700">Download PDF</a>
|
||||||
{% if review.mirror_period %} Mirror period: {{ review.mirror_period.from_date or '—' }} → {{ review.mirror_period.to_date or '—' }}.{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
||||||
{% for label, value in [('Vouchers reviewed', review.summary.vouchers_reviewed), ('Cash payments', review.summary.cash_payment_vouchers), ('Single-voucher exceptions', review.summary.single_voucher_exceptions), ('Same-day exceptions', review.summary.same_day_exceptions), ('Possible split patterns', review.summary.possible_split_patterns)] %}
|
{% for label, value in [('Vouchers reviewed', review.summary.vouchers_reviewed), ('Cash payments', review.summary.cash_payment_vouchers), ('Single-voucher exceptions', review.summary.single_voucher_exceptions), ('Same-day exceptions', review.summary.same_day_exceptions), ('Possible split patterns', review.summary.possible_split_patterns)] %}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
{% extends "ui/templates/base/layout.html" %}
|
{% extends "ui/templates/base/layout.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% set agent = live_result.agent if live_result else None %}
|
{% set mirror = mirror_status.mirror if mirror_status and mirror_status.mirror else {} %}
|
||||||
{% set tally = live_result.tally if live_result else None %}
|
|
||||||
{% set accounting = live_result.accounting if live_result else None %}
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">Tools · Tally · Phase 6</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">Tools · Accounting</p>
|
||||||
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Depreciation (Income Tax)</h1>
|
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Depreciation (Income Tax)</h1>
|
||||||
<p class="mt-1 text-sm text-slate-500">Calculate Income-tax depreciation, approve the reviewed draft and—only after explicit confirmation—post the approved Journal to the mapped company currently open in TallyPrime.</p>
|
<p class="mt-1 text-sm text-slate-500">Calculate Income-tax depreciation from the client SQLite Accounting Mirror. Approved drafts retain the existing controlled Tally write-back.</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="/tools/tally{% if selected_client %}?client_id={{ selected_client.id }}&refresh=1{% endif %}" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Back to Tally</a>
|
<a href="/tools/tally{% if selected_client %}?client_id={{ selected_client.id }}&refresh=1{% endif %}" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Back to Tally</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,15 +17,14 @@
|
|||||||
{% if command_error %}<div class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">{{ command_error }}</div>{% endif %}
|
{% if command_error %}<div class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">{{ command_error }}</div>{% endif %}
|
||||||
|
|
||||||
<section class="grid gap-4 md:grid-cols-3">
|
<section class="grid gap-4 md:grid-cols-3">
|
||||||
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">ERP Local Agent</div><div class="mt-2 text-lg font-semibold {{ 'text-emerald-700' if agent_online else 'text-red-700' }}">{{ 'Connected' if agent_online else 'Offline' }}</div><div class="mt-1 text-xs text-slate-500">Agent {{ agent.version if agent and agent.version else '-' }}</div></div>
|
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">ERP Local Agent</div><div class="mt-2 text-lg font-semibold {{ 'text-emerald-700' if agent_online else 'text-red-700' }}">{{ 'Connected' if agent_online else 'Offline' }}</div></div>
|
||||||
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">TallyPrime</div><div class="mt-2 text-lg font-semibold {{ 'text-emerald-700' if tally and tally.connected else 'text-slate-700' }}">{{ 'Connected' if tally and tally.connected else 'Not connected' }}</div><div class="mt-1 text-xs text-slate-500">{{ tally.company_count if tally else 0 }} currently loaded</div></div>
|
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">SQLite Mirror</div><div class="mt-2 text-lg font-semibold {{ 'text-emerald-700' if mirror.ready else 'text-amber-700' }}">{{ 'Ready' if mirror.ready else 'Mirror required' }}</div><div class="mt-1 text-xs text-slate-500">{{ mirror.company.company_name if mirror.ready and mirror.company else '' }}</div></div>
|
||||||
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Phase 6</div><div class="mt-2 text-lg font-semibold {{ 'text-emerald-700' if agent and agent.tally_writeback_capability else 'text-slate-700' }}">{{ 'Available' if agent and agent.tally_writeback_capability else 'Update Agent' }}</div><div class="mt-1 text-xs text-slate-500">Controlled approved write-back</div></div>
|
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Write-back</div><div class="mt-2 text-lg font-semibold text-slate-700">Controlled</div><div class="mt-1 text-xs text-slate-500">Tally is contacted only after approval/posting.</div></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||||
<form method="get" action="/tools/tally/depreciation" class="grid gap-4 lg:grid-cols-4 lg:items-end">
|
<form method="get" action="/tools/tally/depreciation" class="grid gap-4 lg:grid-cols-3 lg:items-end">
|
||||||
<div><label class="text-sm font-medium text-slate-700">ERP Client</label><select name="client_id" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" onchange="this.form.submit()"><option value="">Select client</option>{% for client in clients %}<option value="{{ client.id }}" {% if selected_client and selected_client.id == client.id %}selected{% endif %}>{{ client.client_name }}</option>{% endfor %}</select></div>
|
<div><label class="text-sm font-medium text-slate-700">ERP Client</label><select name="client_id" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" onchange="this.form.submit()"><option value="">Select client</option>{% for client in clients %}<option value="{{ client.id }}" {% if selected_client and selected_client.id == client.id %}selected{% endif %}>{{ client.client_name }}</option>{% endfor %}</select></div>
|
||||||
<div><label class="text-sm font-medium text-slate-700">Currently Open in TallyPrime</label><select name="tally_guid" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2" {% if not selected_client %}disabled{% endif %}><option value="">Select mapped open company</option>{% if tally %}{% for company in tally.companies or [] %}<option value="{{ company.guid }}" {% if selected_tally_guid == company.guid %}selected{% endif %}>{{ company.name }}{% if company.gstin %} · {{ company.gstin }}{% endif %}</option>{% endfor %}{% endif %}</select></div>
|
|
||||||
<div><label class="text-sm font-medium text-slate-700">Financial Year</label><div class="mt-1 grid grid-cols-2 gap-2"><input type="date" name="fy_start" value="{{ fy_start }}" class="w-full rounded-xl border border-slate-300 px-3 py-2"><input type="date" name="fy_end" value="{{ fy_end }}" class="w-full rounded-xl border border-slate-300 px-3 py-2"></div></div>
|
<div><label class="text-sm font-medium text-slate-700">Financial Year</label><div class="mt-1 grid grid-cols-2 gap-2"><input type="date" name="fy_start" value="{{ fy_start }}" class="w-full rounded-xl border border-slate-300 px-3 py-2"><input type="date" name="fy_end" value="{{ fy_end }}" class="w-full rounded-xl border border-slate-300 px-3 py-2"></div></div>
|
||||||
<button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Load Fixed Assets</button>
|
<button type="submit" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Load Fixed Assets</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -37,14 +34,14 @@
|
|||||||
{% set coverage = preview.transaction_coverage %}
|
{% set coverage = preview.transaction_coverage %}
|
||||||
<div class="rounded-xl border {{ 'border-emerald-200 bg-emerald-50 text-emerald-800' if coverage.complete else 'border-amber-200 bg-amber-50 text-amber-900' }} px-4 py-3 text-sm">
|
<div class="rounded-xl border {{ 'border-emerald-200 bg-emerald-50 text-emerald-800' if coverage.complete else 'border-amber-200 bg-amber-50 text-amber-900' }} px-4 py-3 text-sm">
|
||||||
{% if coverage.complete %}
|
{% if coverage.complete %}
|
||||||
Phase 4 transaction data covers the required period {{ coverage.required_from }} to {{ coverage.required_to }}{% if coverage.provisional %}. This is a provisional current-FY calculation; future dates are not required yet{% endif %}.
|
SQLite mirror data covers the required period {{ coverage.required_from }} to {{ coverage.required_to }}{% if coverage.provisional %}. This is a provisional current-FY calculation; future dates are not required yet{% endif %}.
|
||||||
{% else %}
|
{% else %}
|
||||||
Phase 4 transaction data does not yet cover {{ coverage.required_from }} to {{ coverage.required_to }}. You may save a draft, but approval/write-back will remain blocked until the missing period is synchronized.
|
SQLite mirror data does not yet cover {{ coverage.required_from }} to {{ coverage.required_to }}. You may save a draft, but approval/write-back will remain blocked until the required FY is mirrored.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" action="/tools/tally/depreciation/calculate" class="space-y-5">
|
<form method="post" action="/tools/tally/depreciation/calculate" class="space-y-5">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}"><input type="hidden" name="tally_guid" value="{{ selected_tally_guid }}"><input type="hidden" name="fy_start" value="{{ fy_start }}"><input type="hidden" name="fy_end" value="{{ fy_end }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id }}"><input type="hidden" name="fy_start" value="{{ fy_start }}"><input type="hidden" name="fy_end" value="{{ fy_end }}">
|
||||||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
<div><label class="text-sm font-medium text-slate-700">Depreciation Expense Ledger</label><select name="depreciation_expense_ledger" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"><option value="">Depreciation</option>{% for name in preview.all_ledgers %}<option value="{{ name }}">{{ name }}</option>{% endfor %}</select></div>
|
<div><label class="text-sm font-medium text-slate-700">Depreciation Expense Ledger</label><select name="depreciation_expense_ledger" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"><option value="">Depreciation</option>{% for name in preview.all_ledgers %}<option value="{{ name }}">{{ name }}</option>{% endfor %}</select></div>
|
||||||
@@ -82,6 +79,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if depreciation_run %}
|
{% if depreciation_run %}
|
||||||
|
{% if depreciation_run.analysis_run_id %}
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<a href="/tools/tally/analysis/{{ depreciation_run.analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ fy_start[:4] }}-{{ fy_end[2:4] }}&format=xlsx" class="rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-2 text-sm font-semibold text-emerald-800">Download Excel</a>
|
||||||
|
<a href="/tools/tally/analysis/{{ depreciation_run.analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ fy_start[:4] }}-{{ fy_end[2:4] }}&format=pdf" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold">Download PDF</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<section class="rounded-2xl bg-white p-5 shadow-soft"><div class="flex flex-wrap justify-between gap-4"><div><h2 class="font-semibold text-slate-900">Draft Calculation #{{ depreciation_run.id }}</h2><p class="mt-1 text-sm text-slate-500">{{ depreciation_run.company_name }} · {{ depreciation_run.fy_start }} to {{ depreciation_run.fy_end }}</p></div><div class="text-right"><div class="text-xs uppercase tracking-wide text-slate-500">Total Depreciation</div><div class="text-2xl font-semibold text-slate-900">₹ {{ '%.2f'|format(depreciation_run.total_depreciation) }}</div></div></div>
|
<section class="rounded-2xl bg-white p-5 shadow-soft"><div class="flex flex-wrap justify-between gap-4"><div><h2 class="font-semibold text-slate-900">Draft Calculation #{{ depreciation_run.id }}</h2><p class="mt-1 text-sm text-slate-500">{{ depreciation_run.company_name }} · {{ depreciation_run.fy_start }} to {{ depreciation_run.fy_end }}</p></div><div class="text-right"><div class="text-xs uppercase tracking-wide text-slate-500">Total Depreciation</div><div class="text-2xl font-semibold text-slate-900">₹ {{ '%.2f'|format(depreciation_run.total_depreciation) }}</div></div></div>
|
||||||
<div class="mt-4 rounded-xl border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-800">Journal: Debit <strong>{{ depreciation_run.depreciation_expense_ledger or 'Depreciation' }}</strong> and credit <strong>{{ depreciation_run.depreciation_reserve_ledger or 'Depreciation Reserve' }}</strong> for ₹ {{ '%.2f'|format(depreciation_run.total_depreciation) }}. Voucher date will be {{ depreciation_run.fy_end }} and reference ARRR-ITDEP-{{ depreciation_run.id }}.</div>
|
<div class="mt-4 rounded-xl border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-800">Journal: Debit <strong>{{ depreciation_run.depreciation_expense_ledger or 'Depreciation' }}</strong> and credit <strong>{{ depreciation_run.depreciation_reserve_ledger or 'Depreciation Reserve' }}</strong> for ₹ {{ '%.2f'|format(depreciation_run.total_depreciation) }}. Voucher date will be {{ depreciation_run.fy_end }} and reference ARRR-ITDEP-{{ depreciation_run.id }}.</div>
|
||||||
<div class="mt-4 grid gap-3 md:grid-cols-3">
|
<div class="mt-4 grid gap-3 md:grid-cols-3">
|
||||||
@@ -148,4 +151,10 @@
|
|||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{% if analysis_history %}
|
||||||
|
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||||
|
<h2 class="font-semibold text-slate-900">Depreciation Analysis History</h2>
|
||||||
|
<div class="mt-3 overflow-x-auto"><table class="min-w-full text-sm"><thead><tr class="text-left text-xs uppercase text-slate-500"><th class="p-2">Run</th><th class="p-2">FY</th><th class="p-2">Period</th><th class="p-2">Completed</th><th class="p-2"></th></tr></thead><tbody>{% for row in analysis_history %}<tr class="border-t"><td class="p-2">#{{ row.id }}</td><td class="p-2">{{ row.financial_year }}</td><td class="p-2">{{ row.period_from }} → {{ row.period_to }}</td><td class="p-2">{{ row.completed_at_utc }}</td><td class="p-2 text-right"><a class="font-semibold text-brand-700" href="/tools/tally/analysis/{{ row.id }}/download?client_id={{ selected_client.id }}&financial_year={{ row.financial_year }}&format=xlsx">Excel</a> · <a class="font-semibold text-brand-700" href="/tools/tally/analysis/{{ row.id }}/download?client_id={{ selected_client.id }}&financial_year={{ row.financial_year }}&format=pdf">PDF</a></td></tr>{% endfor %}</tbody></table></div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -22,33 +22,29 @@
|
|||||||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||||
<form method="get" class="grid gap-4 md:grid-cols-[1.5fr_auto]">
|
<form method="get" class="grid gap-4 md:grid-cols-[1.5fr_auto]">
|
||||||
<label class="text-sm font-semibold">ERP Client
|
<label class="text-sm font-semibold">ERP Client
|
||||||
<select name="client_id" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2">
|
<select name="client_id" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2" onchange="this.form.submit()">
|
||||||
<option value="">Select client</option>
|
<option value="">Select client</option>
|
||||||
{% for client in clients %}
|
{% for client in clients %}
|
||||||
<option value="{{ client.id }}" {% if selected_client and selected_client.id==client.id %}selected{% endif %}>{{ client.client_code }} · {{ client.client_name }}</option>
|
<option value="{{ client.id }}" {% if selected_client and selected_client.id==client.id %}selected{% endif %}>{{ client.client_code }} · {{ client.client_name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<div class="flex items-end"><button class="rounded-xl bg-brand-600 px-5 py-2 text-sm font-semibold text-white">Load Tally Companies</button></div>
|
<div class="flex items-end"><a href="/tools/tally{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-slate-300 bg-white px-5 py-2 text-sm font-semibold">Manage Mirrors</a></div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if selected_client %}
|
{% if selected_client %}
|
||||||
<form method="post" action="/tools/accounting/opening-balances/compare" class="mt-5 grid gap-4 rounded-xl border border-slate-200 bg-slate-50 p-4 md:grid-cols-[1fr_1fr_auto]">
|
{% set prev = previous_mirror_status.mirror if previous_mirror_status and previous_mirror_status.mirror else {} %}
|
||||||
|
{% set curr = current_mirror_status.mirror if current_mirror_status and current_mirror_status.mirror else {} %}
|
||||||
|
<div class="mt-5 grid gap-3 md:grid-cols-2">
|
||||||
|
<div class="rounded-xl border p-4 {{ 'border-emerald-200 bg-emerald-50' if prev.ready else 'border-amber-200 bg-amber-50' }}"><div class="text-xs font-semibold uppercase text-slate-500">Previous FY Mirror · {{ previous_fy }}</div><div class="mt-1 font-semibold">{{ 'Ready' if prev.ready else 'Mirror required' }}</div>{% if prev.ready %}<div class="mt-1 text-xs text-slate-500">{{ prev.company.company_name }} · {{ prev.counts.voucher }} vouchers</div>{% endif %}</div>
|
||||||
|
<div class="rounded-xl border p-4 {{ 'border-emerald-200 bg-emerald-50' if curr.ready else 'border-amber-200 bg-amber-50' }}"><div class="text-xs font-semibold uppercase text-slate-500">Current FY Mirror · {{ current_fy }}</div><div class="mt-1 font-semibold">{{ 'Ready' if curr.ready else 'Mirror required' }}</div>{% if curr.ready %}<div class="mt-1 text-xs text-slate-500">{{ curr.company.company_name }} · {{ curr.counts.voucher }} vouchers</div>{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="/tools/accounting/opening-balances/compare" class="mt-4 flex flex-wrap items-end gap-3">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||||
<input type="hidden" name="client_id" value="{{ selected_client.id }}">
|
<input type="hidden" name="client_id" value="{{ selected_client.id }}">
|
||||||
<label class="text-xs font-semibold">Previous-Year Tally Company
|
<input type="hidden" name="previous_fy" value="{{ previous_fy }}">
|
||||||
<select name="previous_company_name" required class="mt-1 w-full rounded-lg border border-slate-300 bg-white px-3 py-2">
|
<input type="hidden" name="current_fy" value="{{ current_fy }}">
|
||||||
<option value="">Select previous-year company</option>
|
<button class="rounded-xl bg-slate-900 px-5 py-2 text-sm font-semibold text-white" {% if not node_online or not prev.ready or not curr.ready %}disabled{% endif %}>Compare Opening Balances</button>
|
||||||
{% for company in companies %}<option value="{{ company.name }}">{{ company.name }}</option>{% endfor %}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="text-xs font-semibold">Current-Year Tally Company
|
|
||||||
<select name="current_company_name" required class="mt-1 w-full rounded-lg border border-slate-300 bg-white px-3 py-2">
|
|
||||||
<option value="">Select current-year company</option>
|
|
||||||
{% for company in companies %}<option value="{{ company.name }}">{{ company.name }}</option>{% endfor %}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<div class="flex items-end"><button class="rounded-xl bg-slate-900 px-5 py-2 text-sm font-semibold text-white" {% if not node_online %}disabled{% endif %}>Compare Opening Balances</button></div>
|
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
@@ -77,8 +73,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if selected_run %}
|
{% if selected_run %}
|
||||||
<div class="mb-4 flex justify-end">
|
<div class="mb-4 flex justify-end gap-2">
|
||||||
<a href="/tools/accounting/opening-balances/download/{{ selected_run.id }}?client_id={{ selected_client.id }}" class="rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-2 text-sm font-semibold text-emerald-800">Download Excel Report</a>
|
<a href="/tools/accounting/opening-balances/download/{{ selected_run.id }}?client_id={{ selected_client.id }}" class="rounded-xl border border-emerald-300 bg-emerald-50 px-4 py-2 text-sm font-semibold text-emerald-800">Download Excel Report</a>
|
||||||
|
{% if analysis_run_id %}<a href="/tools/tally/analysis/{{ analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ current_fy }}&format=pdf" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold">Download PDF</a>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||||
|
|||||||
@@ -48,6 +48,10 @@
|
|||||||
<a href="/tools/tally/depreciation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-semibold">Depreciation (IT)</a>
|
<a href="/tools/tally/depreciation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm font-semibold">Depreciation (IT)</a>
|
||||||
<a href="/tools/accounting/cash-payments{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-orange-300 bg-white px-3 py-2 text-sm font-semibold text-orange-800">Cash Payment Compliance</a>
|
<a href="/tools/accounting/cash-payments{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-orange-300 bg-white px-3 py-2 text-sm font-semibold text-orange-800">Cash Payment Compliance</a>
|
||||||
<a href="/tools/accounting/cash-payments/allocation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-amber-300 bg-white px-3 py-2 text-sm font-semibold text-amber-800">Cash Payment Entry Allocation</a>
|
<a href="/tools/accounting/cash-payments/allocation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-amber-300 bg-white px-3 py-2 text-sm font-semibold text-amber-800">Cash Payment Entry Allocation</a>
|
||||||
|
<a href="/tools/accounting/tds{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-violet-300 bg-white px-3 py-2 text-sm font-semibold text-violet-800">TDS Compliance Review</a>
|
||||||
|
<a href="/tools/accounting/tds/rules" class="rounded-xl border border-violet-200 bg-white px-3 py-2 text-sm font-semibold text-violet-700">TDS Rules</a>
|
||||||
|
<a href="/tools/accounting/tds/liability{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-violet-200 bg-white px-3 py-2 text-sm font-semibold text-violet-700">TDS Liability</a>
|
||||||
|
<a href="/tools/accounting/tds/reconciliation{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-violet-200 bg-white px-3 py-2 text-sm font-semibold text-violet-700">TDS Reconciliation</a>
|
||||||
<a href="/tools/accounting/ai{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-indigo-300 bg-indigo-50 px-3 py-2 text-sm font-semibold text-indigo-800">Accounting AI</a>
|
<a href="/tools/accounting/ai{% if selected_client %}?client_id={{ selected_client.id }}{% endif %}" class="rounded-xl border border-indigo-300 bg-indigo-50 px-3 py-2 text-sm font-semibold text-indigo-800">Accounting AI</a>
|
||||||
<a href="/tools/accounting/internal-model" class="rounded-xl border border-emerald-300 bg-emerald-50 px-3 py-2 text-sm font-semibold text-emerald-800">Internal Model</a>
|
<a href="/tools/accounting/internal-model" class="rounded-xl border border-emerald-300 bg-emerald-50 px-3 py-2 text-sm font-semibold text-emerald-800">Internal Model</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,6 +173,12 @@
|
|||||||
{% set active_mappings = (accounting.mappings or []) | selectattr('is_active') | list %}
|
{% set active_mappings = (accounting.mappings or []) | selectattr('is_active') | list %}
|
||||||
{% if active_mappings %}
|
{% if active_mappings %}
|
||||||
{% set current_mapping = active_mappings[0] %}
|
{% set current_mapping = active_mappings[0] %}
|
||||||
|
{% if selected_client and analysis_history %}
|
||||||
|
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||||
|
<div class="flex items-center justify-between gap-3"><div><h2 class="font-semibold text-slate-900">Recent Accounting Analysis</h2><p class="mt-1 text-sm text-slate-500">Stored in the client local SQLite accounting database.</p></div></div>
|
||||||
|
<div class="mt-3 overflow-x-auto"><table class="min-w-full text-sm"><thead><tr class="text-left text-xs uppercase text-slate-500"><th class="p-2">Run</th><th class="p-2">Analysis</th><th class="p-2">FY / Period</th><th class="p-2">Completed</th><th class="p-2"></th></tr></thead><tbody>{% for row in analysis_history %}<tr class="border-t"><td class="p-2">#{{ row.id }}</td><td class="p-2 font-medium">{{ row.analysis_type|replace('_',' ')|title }}</td><td class="p-2">{{ row.financial_year }} · {{ row.period_from }} → {{ row.period_to }}</td><td class="p-2">{{ row.completed_at_utc }}</td><td class="p-2 text-right"><a class="font-semibold text-brand-700" href="/tools/tally/analysis/{{ row.id }}/download?client_id={{ selected_client.id }}&financial_year={{ row.financial_year }}&format=xlsx">Excel</a> · <a class="font-semibold text-brand-700" href="/tools/tally/analysis/{{ row.id }}/download?client_id={{ selected_client.id }}&financial_year={{ row.financial_year }}&format=pdf">PDF</a></td></tr>{% endfor %}</tbody></table></div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
<details class="rounded-2xl border border-slate-200 bg-white shadow-soft">
|
<details class="rounded-2xl border border-slate-200 bg-white shadow-soft">
|
||||||
<summary class="cursor-pointer list-none px-5 py-4">
|
<summary class="cursor-pointer list-none px-5 py-4">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% extends "ui/templates/base/layout.html" %}
|
{% extends "ui/templates/base/layout.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-3"><div><p class="text-xs font-semibold uppercase tracking-[0.16em] text-violet-600">TDS & Withholding Tax</p><h1 class="mt-1 text-2xl font-semibold text-slate-900">TDS Compliance Review</h1><p class="mt-1 text-sm text-slate-500">Read-only Tally scan across expenses, purchases, capital/service purchases, journals and creditor postings. Rules and thresholds come from the editable TDS Rule Master.</p></div><div class="flex gap-2"><a href="/tools/accounting/tds/rules" class="rounded-xl border px-3 py-2 text-sm font-semibold">Rule Master</a><a href="/tools/tally" class="rounded-xl border px-3 py-2 text-sm font-semibold">Tally</a></div></div>{% if error %}<div class="rounded-xl bg-red-50 p-3 text-sm text-red-800">{{ error }}</div>{% endif %}<form method="get" class="rounded-2xl bg-white p-5 shadow-soft"><div class="grid gap-3 lg:grid-cols-4"><label class="text-sm">Client<select name="client_id" required class="mt-1 w-full rounded-xl border px-3 py-2"><option value="">Select client</option>{% for c in clients %}<option value="{{ c.id }}" {% if selected_client and c.id==selected_client.id %}selected{% endif %}>{{ c.client_name }}</option>{% endfor %}</select></label><label class="text-sm">Tally company GUID<input name="tally_guid" value="{{ tally_guid or '' }}" required class="mt-1 w-full rounded-xl border px-3 py-2" placeholder="Select/copy GUID from mapped/open company"></label><label class="text-sm">From<input type="date" name="date_from" value="{{ date_from }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">To<input type="date" name="date_to" value="{{ date_to }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label></div><input type="hidden" name="run" value="1"><button class="mt-4 rounded-xl bg-violet-700 px-4 py-2 text-sm font-semibold text-white">Run TDS Review</button></form>{% if review %}<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">{% for k,v in review.summary.items() %}<div class="rounded-xl bg-white p-4 shadow-soft"><div class="text-xs uppercase text-slate-500">{{ k|replace('_',' ') }}</div><div class="mt-1 text-xl font-semibold">{{ v }}</div></div>{% endfor %}</div><div class="overflow-x-auto rounded-2xl bg-white shadow-soft"><table class="min-w-full text-sm"><thead class="bg-slate-50"><tr><th class="p-3 text-left">Date</th><th class="p-3 text-left">Party</th><th class="p-3 text-left">Ledger / Rule</th><th class="p-3 text-right">Base</th><th class="p-3 text-right">Expected</th><th class="p-3 text-right">Actual</th><th class="p-3 text-right">Liability</th><th class="p-3 text-left">Status</th></tr></thead><tbody>{% for x in review.transactions %}<tr class="border-t"><td class="p-3">{{ x.date }}</td><td class="p-3">{{ x.party }}</td><td class="p-3">{{ x.nature_ledger }}<div class="text-xs text-slate-500">{{ x.rule_name }} · {{ x.legacy_section }}</div></td><td class="p-3 text-right">{{ '%.2f'|format(x.taxable_base) }}</td><td class="p-3 text-right">{{ '%.2f'|format(x.expected_tds) }}</td><td class="p-3 text-right">{{ '%.2f'|format(x.actual_tds) }}</td><td class="p-3 text-right font-semibold">{{ '%.2f'|format(x.liability) }}</td><td class="p-3">{{ x.status }}</td></tr>{% endfor %}</tbody></table></div>{% endif %}</div>
|
<div class="flex flex-wrap items-start justify-between gap-3"><div><p class="text-xs font-semibold uppercase tracking-[0.16em] text-violet-600">TDS & Withholding Tax</p><h1 class="mt-1 text-2xl font-semibold text-slate-900">TDS Compliance Review</h1><p class="mt-1 text-sm text-slate-500">Review expenses, purchases, journals and creditor postings from the client SQLite Accounting Mirror. Rules and thresholds come from the editable TDS Rule Master.</p></div><div class="flex gap-2"><a href="/tools/accounting/tds/rules" class="rounded-xl border px-3 py-2 text-sm font-semibold">Rule Master</a><a href="/tools/tally" class="rounded-xl border px-3 py-2 text-sm font-semibold">Tally</a></div></div>{% if error %}<div class="rounded-xl bg-red-50 p-3 text-sm text-red-800">{{ error }}</div>{% endif %}<form method="get" class="rounded-2xl bg-white p-5 shadow-soft"><div class="grid gap-3 lg:grid-cols-3"><label class="text-sm">Client<select name="client_id" required class="mt-1 w-full rounded-xl border px-3 py-2"><option value="">Select client</option>{% for c in clients %}<option value="{{ c.id }}" {% if selected_client and c.id==selected_client.id %}selected{% endif %}>{{ c.client_name }}</option>{% endfor %}</select></label><label class="text-sm">From<input type="date" name="date_from" value="{{ date_from }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">To<input type="date" name="date_to" value="{{ date_to }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label></div><input type="hidden" name="run" value="1"><button class="mt-4 rounded-xl bg-violet-700 px-4 py-2 text-sm font-semibold text-white">Run TDS Review</button></form>{% if review %}<div class="flex justify-end gap-2"><a href="/tools/tally/analysis/{{ review.analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ date_from[:4] }}-{{ date_to[2:4] }}&format=xlsx" class="rounded-xl border border-emerald-300 bg-emerald-50 px-3 py-2 text-sm font-semibold text-emerald-800">Download Excel</a><a href="/tools/tally/analysis/{{ review.analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ date_from[:4] }}-{{ date_to[2:4] }}&format=pdf" class="rounded-xl border px-3 py-2 text-sm font-semibold">Download PDF</a></div><div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">{% for k,v in review.summary.items() %}<div class="rounded-xl bg-white p-4 shadow-soft"><div class="text-xs uppercase text-slate-500">{{ k|replace('_',' ') }}</div><div class="mt-1 text-xl font-semibold">{{ v }}</div></div>{% endfor %}</div><div class="overflow-x-auto rounded-2xl bg-white shadow-soft"><table class="min-w-full text-sm"><thead class="bg-slate-50"><tr><th class="p-3 text-left">Date</th><th class="p-3 text-left">Party</th><th class="p-3 text-left">Ledger / Rule</th><th class="p-3 text-right">Base</th><th class="p-3 text-right">Expected</th><th class="p-3 text-right">Actual</th><th class="p-3 text-right">Liability</th><th class="p-3 text-left">Status</th></tr></thead><tbody>{% for x in review.transactions %}<tr class="border-t"><td class="p-3">{{ x.date }}</td><td class="p-3">{{ x.party }}</td><td class="p-3">{{ x.nature_ledger }}<div class="text-xs text-slate-500">{{ x.rule_name }} · {{ x.legacy_section }}</div></td><td class="p-3 text-right">{{ '%.2f'|format(x.taxable_base) }}</td><td class="p-3 text-right">{{ '%.2f'|format(x.expected_tds) }}</td><td class="p-3 text-right">{{ '%.2f'|format(x.actual_tds) }}</td><td class="p-3 text-right font-semibold">{{ '%.2f'|format(x.liability) }}</td><td class="p-3">{{ x.status }}</td></tr>{% endfor %}</tbody></table></div>{% endif %}{% if history %}<div class="rounded-2xl bg-white p-5 shadow-soft"><h2 class="font-semibold">TDS Analysis History</h2><div class="mt-3 overflow-x-auto"><table class="min-w-full text-sm"><tbody>{% for row in history %}<tr class="border-t"><td class="p-2">#{{ row.id }}</td><td class="p-2">{{ row.financial_year }}</td><td class="p-2">{{ row.period_from }} → {{ row.period_to }}</td><td class="p-2 text-right"><a class="font-semibold text-violet-700" href="/tools/tally/analysis/{{ row.id }}/download?client_id={{ selected_client.id }}&financial_year={{ row.financial_year }}&format=xlsx">Excel</a> · <a class="font-semibold text-violet-700" href="/tools/tally/analysis/{{ row.id }}/download?client_id={{ selected_client.id }}&financial_year={{ row.financial_year }}&format=pdf">PDF</a></td></tr>{% endfor %}</tbody></table></div></div>{% endif %}</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+264
-27
@@ -1,13 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import date, datetime, timezone
|
from datetime import date, datetime, timezone
|
||||||
|
from io import BytesIO
|
||||||
|
import json
|
||||||
|
import textwrap
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
from fastapi import APIRouter, Form, Request
|
from fastapi import APIRouter, Form, Request
|
||||||
from fastapi.responses import RedirectResponse, JSONResponse
|
from fastapi.responses import RedirectResponse, JSONResponse, StreamingResponse
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
from openpyxl import Workbook
|
||||||
|
|
||||||
from app.core.db.common import CommonSessionLocal
|
from app.core.db.common import CommonSessionLocal
|
||||||
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
|
from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
|
||||||
@@ -213,6 +217,7 @@ def tally_tool(
|
|||||||
online = _node_online(node)
|
online = _node_online(node)
|
||||||
|
|
||||||
live_result = None
|
live_result = None
|
||||||
|
analysis_history = []
|
||||||
command_error = error or ""
|
command_error = error or ""
|
||||||
should_query_agent = bool(refresh or selected_client)
|
should_query_agent = bool(refresh or selected_client)
|
||||||
|
|
||||||
@@ -236,6 +241,17 @@ def tally_tool(
|
|||||||
if response_data.get("ok"):
|
if response_data.get("ok"):
|
||||||
live_result = response_data.get("result") or {}
|
live_result = response_data.get("result") or {}
|
||||||
_enrich_gstin_display(live_result, registrations)
|
_enrich_gstin_display(live_result, registrations)
|
||||||
|
try:
|
||||||
|
hist = request_agent_command(
|
||||||
|
node.node_code,
|
||||||
|
"accounting_analysis_history",
|
||||||
|
{**payload, "limit": 20},
|
||||||
|
timeout_seconds=20,
|
||||||
|
)
|
||||||
|
if hist.get("ok"):
|
||||||
|
analysis_history = (hist.get("result") or {}).get("runs") or []
|
||||||
|
except Exception:
|
||||||
|
analysis_history = []
|
||||||
else:
|
else:
|
||||||
command_error = str(response_data.get("error") or "Local agent command failed.")
|
command_error = str(response_data.get("error") or "Local agent command failed.")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -265,6 +281,161 @@ def tally_tool(
|
|||||||
transaction_date_from=transaction_date_from,
|
transaction_date_from=transaction_date_from,
|
||||||
transaction_date_to=transaction_date_to,
|
transaction_date_to=transaction_date_to,
|
||||||
command_error=command_error,
|
command_error=command_error,
|
||||||
|
analysis_history=analysis_history,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _analysis_flat_lines(value, prefix=""):
|
||||||
|
lines = []
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for key, item in value.items():
|
||||||
|
label = f"{prefix}.{key}" if prefix else str(key)
|
||||||
|
lines.extend(_analysis_flat_lines(item, label))
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for idx, item in enumerate(value, 1):
|
||||||
|
label = f"{prefix}[{idx}]"
|
||||||
|
lines.extend(_analysis_flat_lines(item, label))
|
||||||
|
else:
|
||||||
|
lines.append((prefix, "" if value is None else str(value)))
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def _simple_pdf_bytes(title: str, lines: list[str]) -> bytes:
|
||||||
|
def esc(text):
|
||||||
|
return str(text).replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
|
||||||
|
page_chunks = [lines[i:i+48] for i in range(0, max(1, len(lines)), 48)] or [[]]
|
||||||
|
objects = []
|
||||||
|
# Object 1 catalog, 2 pages, 3 font. Page/content objects follow.
|
||||||
|
page_refs = []
|
||||||
|
obj_no = 4
|
||||||
|
page_objects = []
|
||||||
|
for chunk in page_chunks:
|
||||||
|
page_no = obj_no
|
||||||
|
content_no = obj_no + 1
|
||||||
|
page_refs.append(f"{page_no} 0 R")
|
||||||
|
content = ["BT", "/F1 10 Tf", "48 790 Td", f"({esc(title)}) Tj", "0 -20 Td"]
|
||||||
|
for line in chunk:
|
||||||
|
content.append(f"({esc(line[:150])}) Tj")
|
||||||
|
content.append("0 -14 Td")
|
||||||
|
content.append("ET")
|
||||||
|
stream = "\n".join(content).encode("latin-1", "replace")
|
||||||
|
page_objects.append((page_no, f"<< /Type /Page /Parent 2 0 R /Resources << /Font << /F1 3 0 R >> >> /MediaBox [0 0 595 842] /Contents {content_no} 0 R >>".encode()))
|
||||||
|
page_objects.append((content_no, b"<< /Length %d >>\nstream\n" % len(stream) + stream + b"\nendstream"))
|
||||||
|
obj_no += 2
|
||||||
|
objects.append((1, b"<< /Type /Catalog /Pages 2 0 R >>"))
|
||||||
|
objects.append((2, f"<< /Type /Pages /Kids [{' '.join(page_refs)}] /Count {len(page_refs)} >>".encode()))
|
||||||
|
objects.append((3, b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>"))
|
||||||
|
objects.extend(page_objects)
|
||||||
|
objects.sort()
|
||||||
|
out = bytearray(b"%PDF-1.4\n")
|
||||||
|
offsets = {0: 0}
|
||||||
|
for number, body in objects:
|
||||||
|
offsets[number] = len(out)
|
||||||
|
out.extend(f"{number} 0 obj\n".encode())
|
||||||
|
out.extend(body)
|
||||||
|
out.extend(b"\nendobj\n")
|
||||||
|
xref = len(out)
|
||||||
|
max_obj = max(offsets)
|
||||||
|
out.extend(f"xref\n0 {max_obj+1}\n".encode())
|
||||||
|
out.extend(b"0000000000 65535 f \n")
|
||||||
|
for i in range(1, max_obj + 1):
|
||||||
|
out.extend(f"{offsets.get(i,0):010d} 00000 n \n".encode())
|
||||||
|
out.extend(f"trailer\n<< /Size {max_obj+1} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF".encode())
|
||||||
|
return bytes(out)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/analysis/{analysis_run_id}/download")
|
||||||
|
def download_analysis_report(
|
||||||
|
request: Request,
|
||||||
|
analysis_run_id: int,
|
||||||
|
client_id: int,
|
||||||
|
financial_year: str,
|
||||||
|
format: str = "xlsx",
|
||||||
|
):
|
||||||
|
db = CommonSessionLocal()
|
||||||
|
try:
|
||||||
|
user, response = _require_partner(request, db, "accounting.tally.view")
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
result = request_agent_command(
|
||||||
|
node.node_code,
|
||||||
|
"accounting_analysis_get",
|
||||||
|
{
|
||||||
|
**_accounting_storage_payload(client, financial_year),
|
||||||
|
"run_id": int(analysis_run_id),
|
||||||
|
},
|
||||||
|
timeout_seconds=30,
|
||||||
|
)
|
||||||
|
if not result.get("ok"):
|
||||||
|
raise RuntimeError(str(result.get("error") or "Analysis report was not found."))
|
||||||
|
run = (result.get("result") or {}).get("run") or {}
|
||||||
|
safe_type = re.sub(r"[^A-Za-z0-9_-]+", "_", str(run.get("analysis_type") or "analysis")).strip("_")
|
||||||
|
filename_base = f"{safe_type}_{analysis_run_id}"
|
||||||
|
|
||||||
|
if str(format).lower() == "pdf":
|
||||||
|
lines = [
|
||||||
|
f"Client: {client.client_name}",
|
||||||
|
f"Analysis: {run.get('analysis_type','')}",
|
||||||
|
f"Financial year: {run.get('financial_year','')}",
|
||||||
|
f"Period: {run.get('period_from','')} to {run.get('period_to','')}",
|
||||||
|
f"Company: {run.get('company_name','')}",
|
||||||
|
f"Completed: {run.get('completed_at_utc','')}",
|
||||||
|
"",
|
||||||
|
"Summary",
|
||||||
|
]
|
||||||
|
for key, value in (run.get("summary") or {}).items():
|
||||||
|
lines.append(f"{str(key).replace('_',' ').title()}: {value}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("Details")
|
||||||
|
for key, value in _analysis_flat_lines(run.get("result") or {}):
|
||||||
|
lines.append(f"{key}: {value}")
|
||||||
|
payload = _simple_pdf_bytes(f"{safe_type.replace('_',' ')} Report", lines)
|
||||||
|
return StreamingResponse(
|
||||||
|
BytesIO(payload),
|
||||||
|
media_type="application/pdf",
|
||||||
|
headers={"Content-Disposition": f'attachment; filename="{filename_base}.pdf"'},
|
||||||
|
)
|
||||||
|
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "Summary"
|
||||||
|
ws.append(["Field", "Value"])
|
||||||
|
for row in (
|
||||||
|
("Client", client.client_name),
|
||||||
|
("Analysis Type", run.get("analysis_type", "")),
|
||||||
|
("Financial Year", run.get("financial_year", "")),
|
||||||
|
("Period From", run.get("period_from", "")),
|
||||||
|
("Period To", run.get("period_to", "")),
|
||||||
|
("Company", run.get("company_name", "")),
|
||||||
|
("Completed At UTC", run.get("completed_at_utc", "")),
|
||||||
|
):
|
||||||
|
ws.append(list(row))
|
||||||
|
ws.append([])
|
||||||
|
ws.append(["Summary", ""])
|
||||||
|
for key, value in (run.get("summary") or {}).items():
|
||||||
|
ws.append([str(key).replace("_", " ").title(), value])
|
||||||
|
details = wb.create_sheet("Details")
|
||||||
|
details.append(["Path", "Value"])
|
||||||
|
for key, value in _analysis_flat_lines(run.get("result") or {}):
|
||||||
|
details.append([key, value])
|
||||||
|
stream = BytesIO()
|
||||||
|
wb.save(stream)
|
||||||
|
stream.seek(0)
|
||||||
|
return StreamingResponse(
|
||||||
|
stream,
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
headers={"Content-Disposition": f'attachment; filename="{filename_base}.xlsx"'},
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
@@ -621,7 +792,6 @@ def full_accounting_export_status(request: Request, client_id: int):
|
|||||||
def depreciation_it_tool(
|
def depreciation_it_tool(
|
||||||
request: Request,
|
request: Request,
|
||||||
client_id: int | None = None,
|
client_id: int | None = None,
|
||||||
tally_guid: str = "",
|
|
||||||
fy_start: str = "",
|
fy_start: str = "",
|
||||||
fy_end: str = "",
|
fy_end: str = "",
|
||||||
calculated: int = 0,
|
calculated: int = 0,
|
||||||
@@ -633,32 +803,100 @@ def depreciation_it_tool(
|
|||||||
db = CommonSessionLocal()
|
db = CommonSessionLocal()
|
||||||
try:
|
try:
|
||||||
user, response = _require_partner(request, db, "accounting.tally.depreciation_it")
|
user, response = _require_partner(request, db, "accounting.tally.depreciation_it")
|
||||||
if response: return response
|
if response:
|
||||||
|
return response
|
||||||
clients, scope = _visible_clients(db, request, user)
|
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)
|
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)
|
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
||||||
online = _node_online(node)
|
online = _node_online(node)
|
||||||
today = date.today(); start_year = today.year if today.month >= 4 else today.year - 1
|
today = date.today()
|
||||||
start_text = fy_start or date(start_year,4,1).isoformat(); end_text = fy_end or date(start_year+1,3,31).isoformat()
|
start_year = today.year if today.month >= 4 else today.year - 1
|
||||||
live_result = None; preview = None; depreciation_run = None; command_error = error or ""
|
start_text = fy_start or date(start_year, 4, 1).isoformat()
|
||||||
|
end_text = fy_end or date(start_year + 1, 3, 31).isoformat()
|
||||||
|
preview = None
|
||||||
|
depreciation_run = None
|
||||||
|
mirror_status = None
|
||||||
|
command_error = error or ""
|
||||||
|
analysis_history = []
|
||||||
|
|
||||||
if selected_client and node and online:
|
if selected_client and node and online:
|
||||||
|
storage_payload = _accounting_storage_payload(
|
||||||
|
selected_client, _financial_year_for_date(date.fromisoformat(start_text))
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
status_response = request_agent_command(node.node_code,"phase6_status",_accounting_storage_payload(selected_client, _financial_year_for_date(date.fromisoformat(start_text))),timeout_seconds=20)
|
status_response = request_agent_command(
|
||||||
if status_response.get("ok"): live_result=status_response.get("result") or {}
|
node.node_code, "accounting_mirror_status", storage_payload, timeout_seconds=20
|
||||||
else: command_error=str(status_response.get("error") or "Local Agent status failed.")
|
)
|
||||||
chosen_guid=str(tally_guid or "").strip()
|
if status_response.get("ok"):
|
||||||
if chosen_guid:
|
mirror_status = status_response.get("result") or {}
|
||||||
preview_response=request_agent_command(node.node_code,"accounting_depreciation_preview",{**_accounting_storage_payload(selected_client, _financial_year_for_date(date.fromisoformat(start_text))),"tally_guid":chosen_guid,"fy_start":start_text,"fy_end":end_text},timeout_seconds=60)
|
else:
|
||||||
if preview_response.get("ok"): preview=(preview_response.get("result") or {}).get("preview")
|
command_error = str(status_response.get("error") or "Accounting Mirror status failed.")
|
||||||
else: command_error=str(preview_response.get("error") or "Depreciation preview failed.")
|
|
||||||
|
if mirror_status and (mirror_status.get("mirror") or {}).get("ready"):
|
||||||
|
preview_response = request_agent_command(
|
||||||
|
node.node_code,
|
||||||
|
"accounting_depreciation_preview",
|
||||||
|
{
|
||||||
|
**storage_payload,
|
||||||
|
"fy_start": start_text,
|
||||||
|
"fy_end": end_text,
|
||||||
|
"requested_by_user_id": int(user.id),
|
||||||
|
},
|
||||||
|
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:
|
if run_id:
|
||||||
run_response=request_agent_command(node.node_code,"accounting_get_it_depreciation_run",{**_accounting_storage_payload(selected_client, _financial_year_for_date(date.fromisoformat(start_text))),"run_id":int(run_id)},timeout_seconds=30)
|
run_response = request_agent_command(
|
||||||
if run_response.get("ok"): depreciation_run=(run_response.get("result") or {}).get("depreciation")
|
node.node_code,
|
||||||
except Exception as exc: command_error=str(exc)
|
"accounting_get_it_depreciation_run",
|
||||||
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)}
|
{**storage_payload, "run_id": int(run_id)},
|
||||||
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})
|
timeout_seconds=30,
|
||||||
|
)
|
||||||
|
if run_response.get("ok"):
|
||||||
|
depreciation_run = (run_response.get("result") or {}).get("depreciation")
|
||||||
|
|
||||||
|
hist = request_agent_command(
|
||||||
|
node.node_code,
|
||||||
|
"accounting_analysis_history",
|
||||||
|
{**storage_payload, "analysis_type": "DEPRECIATION_IT", "limit": 20},
|
||||||
|
timeout_seconds=20,
|
||||||
|
)
|
||||||
|
if hist.get("ok"):
|
||||||
|
analysis_history = (hist.get("result") or {}).get("runs") or []
|
||||||
|
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,
|
||||||
|
"mirror_status": mirror_status,
|
||||||
|
"preview": preview,
|
||||||
|
"fy_start": start_text,
|
||||||
|
"fy_end": end_text,
|
||||||
|
"calculated": bool(calculated),
|
||||||
|
"approved": bool(approved),
|
||||||
|
"posted": bool(posted),
|
||||||
|
"depreciation_run": depreciation_run,
|
||||||
|
"analysis_history": analysis_history,
|
||||||
|
"command_error": command_error,
|
||||||
|
})
|
||||||
return templates.TemplateResponse("modules/accounting/templates/accounting/depreciation_it.html", base)
|
return templates.TemplateResponse("modules/accounting/templates/accounting/depreciation_it.html", base)
|
||||||
finally: db.close()
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/depreciation/calculate")
|
@router.post("/depreciation/calculate")
|
||||||
@@ -673,7 +911,6 @@ async def calculate_it_depreciation(request: Request):
|
|||||||
except Exception as exc: return RedirectResponse(url=f"/tools/tally/depreciation?error={quote('Invalid client or financial-year dates.')}",status_code=303)
|
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)
|
client, _clients, scope = _find_visible_client(db, request, user, client_id)
|
||||||
if not client: return _denied()
|
if not client: return _denied()
|
||||||
tally_guid=str(form.get("tally_guid") or "").strip()
|
|
||||||
assignments=[]
|
assignments=[]
|
||||||
for key,value in form.multi_items():
|
for key,value in form.multi_items():
|
||||||
if not str(key).startswith("rate__"): continue
|
if not str(key).startswith("rate__"): continue
|
||||||
@@ -682,14 +919,14 @@ async def calculate_it_depreciation(request: Request):
|
|||||||
except Exception: rate=-1
|
except Exception: rate=-1
|
||||||
assignments.append({"ledger_key":ledger_key,"rate_percent":rate})
|
assignments.append({"ledger_key":ledger_key,"rate_percent":rate})
|
||||||
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
|
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)
|
if not node or not _node_online(node): return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&error={quote('ERP Local Agent is offline for the active branch.')}",status_code=303)
|
||||||
try:
|
try:
|
||||||
result=request_agent_command(node.node_code,"accounting_calculate_it_depreciation",{**_accounting_storage_payload(client, _financial_year_for_date(start)),"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)
|
result=request_agent_command(node.node_code,"accounting_calculate_it_depreciation",{**_accounting_storage_payload(client, _financial_year_for_date(start)),"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 ""),"financial_year":_financial_year_for_date(start),"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."))
|
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"))
|
dep=(result.get("result") or {}).get("depreciation") or {}; rid=int(dep.get("run_id"))
|
||||||
except Exception as exc:
|
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}&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)
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&calculated=1&run_id={rid}",status_code=303)
|
||||||
finally: db.close()
|
finally: db.close()
|
||||||
|
|
||||||
|
|
||||||
@@ -723,7 +960,7 @@ async def approve_it_depreciation(request: Request):
|
|||||||
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Approval failed."))
|
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Approval failed."))
|
||||||
dep = (result.get("result") or {}).get("depreciation") or {}
|
dep = (result.get("result") or {}).get("depreciation") or {}
|
||||||
guid = quote(str(dep.get("tally_guid") 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)
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&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:
|
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)
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&run_id={run_id}&error={quote(str(exc))}", status_code=303)
|
||||||
finally:
|
finally:
|
||||||
@@ -761,7 +998,7 @@ async def post_it_depreciation_to_tally(request: Request):
|
|||||||
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Tally write-back failed."))
|
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Tally write-back failed."))
|
||||||
dep = (result.get("result") or {}).get("depreciation") or {}
|
dep = (result.get("result") or {}).get("depreciation") or {}
|
||||||
guid = quote(str(dep.get("tally_guid") 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)
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&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:
|
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)
|
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client_id}&run_id={run_id}&error={quote(str(exc))}", status_code=303)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import io
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
ERP_LOCAL_AGENT_VERSION = "1.24.6"
|
ERP_LOCAL_AGENT_VERSION = "1.25.0"
|
||||||
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
||||||
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
||||||
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
|
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "1.24.6"
|
__version__ = "1.25.0"
|
||||||
AGENT_NAME = "ERP Local Agent"
|
AGENT_NAME = "ERP Local Agent"
|
||||||
|
|||||||
@@ -178,6 +178,14 @@ class AgentCommandProcessor:
|
|||||||
result = self._accounting_full_export_status(payload)
|
result = self._accounting_full_export_status(payload)
|
||||||
elif action == "accounting_mirror_query":
|
elif action == "accounting_mirror_query":
|
||||||
result = self._accounting_mirror_query(payload)
|
result = self._accounting_mirror_query(payload)
|
||||||
|
elif action == "accounting_analysis_save":
|
||||||
|
result = self._accounting_analysis_save(payload)
|
||||||
|
elif action == "accounting_analysis_history":
|
||||||
|
result = self._accounting_analysis_history(payload)
|
||||||
|
elif action == "accounting_analysis_get":
|
||||||
|
result = self._accounting_analysis_get(payload)
|
||||||
|
elif action == "accounting_opening_balance_mirror_snapshot":
|
||||||
|
result = self._opening_balance_mirror_snapshot(payload)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported local-agent command: {action}")
|
raise ValueError(f"Unsupported local-agent command: {action}")
|
||||||
ok = True
|
ok = True
|
||||||
@@ -205,6 +213,8 @@ class AgentCommandProcessor:
|
|||||||
"full_accounting_export_capability": True,
|
"full_accounting_export_capability": True,
|
||||||
"accounting_mirror_progress_capability": True,
|
"accounting_mirror_progress_capability": True,
|
||||||
"mirror_first_accounting_reads": True,
|
"mirror_first_accounting_reads": True,
|
||||||
|
"accounting_analysis_history_capability": True,
|
||||||
|
"cross_fy_mirror_analysis_capability": True,
|
||||||
"historical_learning_read_capability": True,
|
"historical_learning_read_capability": True,
|
||||||
"purchase_posting_preflight_capability": True,
|
"purchase_posting_preflight_capability": True,
|
||||||
"purchase_voucher_write_capability": True,
|
"purchase_voucher_write_capability": True,
|
||||||
@@ -471,6 +481,274 @@ class AgentCommandProcessor:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_analysis_tables(self, client_id: int) -> None:
|
||||||
|
db = self.store.connect(client_id)
|
||||||
|
try:
|
||||||
|
db.executescript(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS accounting_analysis_runs (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
analysis_type TEXT NOT NULL,
|
||||||
|
financial_year TEXT NOT NULL DEFAULT '',
|
||||||
|
period_from TEXT NOT NULL DEFAULT '',
|
||||||
|
period_to TEXT NOT NULL DEFAULT '',
|
||||||
|
company_guid TEXT NOT NULL DEFAULT '',
|
||||||
|
company_name TEXT NOT NULL DEFAULT '',
|
||||||
|
status TEXT NOT NULL DEFAULT 'completed',
|
||||||
|
requested_by_user_id INTEGER,
|
||||||
|
source_mirror_path TEXT NOT NULL DEFAULT '',
|
||||||
|
source_mirror_synced_at TEXT NOT NULL DEFAULT '',
|
||||||
|
parameters_json TEXT NOT NULL DEFAULT '{}',
|
||||||
|
summary_json TEXT NOT NULL DEFAULT '{}',
|
||||||
|
result_json TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at_utc TEXT NOT NULL,
|
||||||
|
completed_at_utc TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_accounting_analysis_runs_type
|
||||||
|
ON accounting_analysis_runs(analysis_type, created_at_utc DESC);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def _save_analysis_run(
|
||||||
|
self,
|
||||||
|
client_id: int,
|
||||||
|
*,
|
||||||
|
analysis_type: str,
|
||||||
|
financial_year: str = "",
|
||||||
|
period_from: str = "",
|
||||||
|
period_to: str = "",
|
||||||
|
company_guid: str = "",
|
||||||
|
company_name: str = "",
|
||||||
|
requested_by_user_id: int | None = None,
|
||||||
|
source_mirror_path: str = "",
|
||||||
|
source_mirror_synced_at: str = "",
|
||||||
|
parameters: dict[str, Any] | None = None,
|
||||||
|
summary: dict[str, Any] | None = None,
|
||||||
|
result: dict[str, Any] | None = None,
|
||||||
|
) -> int:
|
||||||
|
self._ensure_analysis_tables(client_id)
|
||||||
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
|
db = self.store.connect(client_id)
|
||||||
|
try:
|
||||||
|
cur = db.execute(
|
||||||
|
"""INSERT INTO accounting_analysis_runs(
|
||||||
|
analysis_type,financial_year,period_from,period_to,company_guid,company_name,
|
||||||
|
status,requested_by_user_id,source_mirror_path,source_mirror_synced_at,
|
||||||
|
parameters_json,summary_json,result_json,created_at_utc,completed_at_utc
|
||||||
|
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||||
|
(
|
||||||
|
str(analysis_type or "").strip(),
|
||||||
|
str(financial_year or "").strip(),
|
||||||
|
str(period_from or "").strip(),
|
||||||
|
str(period_to or "").strip(),
|
||||||
|
str(company_guid or "").strip(),
|
||||||
|
str(company_name or "").strip(),
|
||||||
|
"completed",
|
||||||
|
int(requested_by_user_id) if requested_by_user_id not in (None, "") else None,
|
||||||
|
str(source_mirror_path or "").strip(),
|
||||||
|
str(source_mirror_synced_at or "").strip(),
|
||||||
|
json.dumps(parameters or {}, ensure_ascii=False, default=str),
|
||||||
|
json.dumps(summary or {}, ensure_ascii=False, default=str),
|
||||||
|
json.dumps(result or {}, ensure_ascii=False, default=str),
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
return int(cur.lastrowid)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def _accounting_analysis_save(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
client_id = int(payload.get("client_id") or 0)
|
||||||
|
if client_id <= 0:
|
||||||
|
raise ValueError("client_id is required.")
|
||||||
|
run_id = self._save_analysis_run(
|
||||||
|
client_id,
|
||||||
|
analysis_type=str(payload.get("analysis_type") or "ACCOUNTING_ANALYSIS"),
|
||||||
|
financial_year=str(payload.get("financial_year") or ""),
|
||||||
|
period_from=str(payload.get("period_from") or ""),
|
||||||
|
period_to=str(payload.get("period_to") or ""),
|
||||||
|
company_guid=str(payload.get("company_guid") or ""),
|
||||||
|
company_name=str(payload.get("company_name") or ""),
|
||||||
|
requested_by_user_id=payload.get("requested_by_user_id"),
|
||||||
|
source_mirror_path=str(payload.get("source_mirror_path") or ""),
|
||||||
|
source_mirror_synced_at=str(payload.get("source_mirror_synced_at") or ""),
|
||||||
|
parameters=dict(payload.get("parameters") or {}),
|
||||||
|
summary=dict(payload.get("summary") or {}),
|
||||||
|
result=dict(payload.get("result") or {}),
|
||||||
|
)
|
||||||
|
return {"analysis_run_id": run_id, "agent": self._agent_info()}
|
||||||
|
|
||||||
|
def _accounting_analysis_history(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
client_id = int(payload.get("client_id") or 0)
|
||||||
|
if client_id <= 0:
|
||||||
|
raise ValueError("client_id is required.")
|
||||||
|
self._ensure_analysis_tables(client_id)
|
||||||
|
limit = max(1, min(100, int(payload.get("limit") or 30)))
|
||||||
|
analysis_type = str(payload.get("analysis_type") or "").strip()
|
||||||
|
db = self.store.connect(client_id)
|
||||||
|
try:
|
||||||
|
if analysis_type:
|
||||||
|
rows = db.execute(
|
||||||
|
"""SELECT id,analysis_type,financial_year,period_from,period_to,company_name,
|
||||||
|
status,summary_json,created_at_utc,completed_at_utc
|
||||||
|
FROM accounting_analysis_runs
|
||||||
|
WHERE analysis_type=?
|
||||||
|
ORDER BY id DESC LIMIT ?""",
|
||||||
|
(analysis_type, limit),
|
||||||
|
).fetchall()
|
||||||
|
else:
|
||||||
|
rows = db.execute(
|
||||||
|
"""SELECT id,analysis_type,financial_year,period_from,period_to,company_name,
|
||||||
|
status,summary_json,created_at_utc,completed_at_utc
|
||||||
|
FROM accounting_analysis_runs
|
||||||
|
ORDER BY id DESC LIMIT ?""",
|
||||||
|
(limit,),
|
||||||
|
).fetchall()
|
||||||
|
result = []
|
||||||
|
for row in rows:
|
||||||
|
item = dict(row)
|
||||||
|
try:
|
||||||
|
item["summary"] = json.loads(item.pop("summary_json") or "{}")
|
||||||
|
except Exception:
|
||||||
|
item["summary"] = {}
|
||||||
|
result.append(item)
|
||||||
|
return {"runs": result, "agent": self._agent_info()}
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def _accounting_analysis_get(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
client_id = int(payload.get("client_id") or 0)
|
||||||
|
run_id = int(payload.get("run_id") or 0)
|
||||||
|
if client_id <= 0 or run_id <= 0:
|
||||||
|
raise ValueError("client_id and run_id are required.")
|
||||||
|
self._ensure_analysis_tables(client_id)
|
||||||
|
db = self.store.connect(client_id)
|
||||||
|
try:
|
||||||
|
row = db.execute(
|
||||||
|
"SELECT * FROM accounting_analysis_runs WHERE id=?",
|
||||||
|
(run_id,),
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
raise ValueError("Analysis run was not found in the client SQLite database.")
|
||||||
|
item = dict(row)
|
||||||
|
for field in ("parameters_json", "summary_json", "result_json"):
|
||||||
|
target = field[:-5]
|
||||||
|
try:
|
||||||
|
item[target] = json.loads(item.pop(field) or "{}")
|
||||||
|
except Exception:
|
||||||
|
item[target] = {}
|
||||||
|
return {"run": item, "agent": self._agent_info()}
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def _mirror_db_from_relative_dir(self, client_id: int, relative_dir: str):
|
||||||
|
from pathlib import Path as _Path
|
||||||
|
import os as _os
|
||||||
|
relative = _Path(str(relative_dir or "").replace("\\", "/"))
|
||||||
|
if relative.is_absolute() or ".." in relative.parts:
|
||||||
|
raise ValueError("Invalid Accounting Mirror storage path.")
|
||||||
|
base = self.store.storage_root.resolve()
|
||||||
|
folder = (base / relative).resolve()
|
||||||
|
if _os.path.commonpath([str(base), str(folder)]) != str(base):
|
||||||
|
raise ValueError("Accounting Mirror storage path escapes the Storage Node root.")
|
||||||
|
return folder / f"client_{int(client_id):08d}_mirror.act"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _mirror_master_snapshot_from_file(db_path, *, previous_year: bool = False):
|
||||||
|
import sqlite3 as _sqlite3
|
||||||
|
if not db_path.is_file():
|
||||||
|
raise ValueError(f"Accounting Mirror was not found: {db_path}")
|
||||||
|
db = _sqlite3.connect(db_path, timeout=60)
|
||||||
|
db.row_factory = _sqlite3.Row
|
||||||
|
try:
|
||||||
|
company = db.execute("SELECT * FROM company_master ORDER BY synced_at DESC LIMIT 1").fetchone()
|
||||||
|
ledgers = []
|
||||||
|
for row in db.execute("SELECT * FROM ledger_master ORDER BY ledger_name").fetchall():
|
||||||
|
ledgers.append({
|
||||||
|
"guid": row["ledger_guid"] or "",
|
||||||
|
"name": row["ledger_name"] or "",
|
||||||
|
"parent": row["parent_group"] or "",
|
||||||
|
"opening_balance": float(row["opening_balance"] or 0),
|
||||||
|
"closing_balance": float(row["closing_balance"] or 0),
|
||||||
|
"is_revenue": row["is_revenue"] or "",
|
||||||
|
"is_active": "Yes",
|
||||||
|
})
|
||||||
|
stocks = []
|
||||||
|
for row in db.execute("SELECT * FROM stock_item_master ORDER BY product_name").fetchall():
|
||||||
|
opening_qty = float(row["opening_qty"] or 0)
|
||||||
|
opening_value = float(row["opening_value"] or 0)
|
||||||
|
closing_qty = opening_qty
|
||||||
|
closing_value = opening_value
|
||||||
|
if previous_year:
|
||||||
|
movement = db.execute(
|
||||||
|
"""SELECT
|
||||||
|
COALESCE(SUM(CASE WHEN UPPER(direction)='INWARD' THEN ABS(quantity)
|
||||||
|
WHEN UPPER(direction)='OUTWARD' THEN -ABS(quantity) ELSE 0 END),0),
|
||||||
|
COALESCE(SUM(CASE WHEN UPPER(direction)='INWARD' THEN ABS(value)
|
||||||
|
WHEN UPPER(direction)='OUTWARD' THEN -ABS(value) ELSE 0 END),0)
|
||||||
|
FROM inventory_movement WHERE stock_item_guid=?""",
|
||||||
|
(row["stock_item_guid"],),
|
||||||
|
).fetchone()
|
||||||
|
closing_qty = opening_qty + float(movement[0] or 0)
|
||||||
|
closing_value = opening_value + float(movement[1] or 0)
|
||||||
|
stocks.append({
|
||||||
|
"guid": row["stock_item_guid"] or "",
|
||||||
|
"name": row["product_name"] or "",
|
||||||
|
"parent": row["parent_group"] or "",
|
||||||
|
"base_units": row["base_uom"] or "",
|
||||||
|
"opening_balance": opening_qty,
|
||||||
|
"opening_value": opening_value,
|
||||||
|
"closing_balance": closing_qty,
|
||||||
|
"closing_value": closing_value,
|
||||||
|
"hsn_code": row["current_hsn"] or "",
|
||||||
|
"is_active": "Yes",
|
||||||
|
})
|
||||||
|
company_dict = dict(company) if company else {}
|
||||||
|
return {
|
||||||
|
"company": {
|
||||||
|
"name": str(company_dict.get("company_name") or ""),
|
||||||
|
"guid": str(company_dict.get("company_guid") or ""),
|
||||||
|
"gstin": str(company_dict.get("gstin") or ""),
|
||||||
|
},
|
||||||
|
"masters": {"ledgers": ledgers, "stock_items": stocks},
|
||||||
|
"path": str(db_path),
|
||||||
|
"sync": company_dict,
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def _opening_balance_mirror_snapshot(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
client_id = int(payload.get("client_id") or 0)
|
||||||
|
previous_relative_dir = str(payload.get("previous_accounting_relative_dir") or "").strip()
|
||||||
|
current_relative_dir = str(payload.get("current_accounting_relative_dir") or payload.get("accounting_relative_dir") or "").strip()
|
||||||
|
if not previous_relative_dir or not current_relative_dir:
|
||||||
|
raise ValueError("Previous-year and current-year Accounting Mirror paths are required.")
|
||||||
|
previous_path = self._mirror_db_from_relative_dir(client_id, previous_relative_dir)
|
||||||
|
current_path = self._mirror_db_from_relative_dir(client_id, current_relative_dir)
|
||||||
|
previous = self._mirror_master_snapshot_from_file(previous_path, previous_year=True)
|
||||||
|
current = self._mirror_master_snapshot_from_file(current_path, previous_year=False)
|
||||||
|
if previous["company"].get("guid") and current["company"].get("guid") and previous["company"]["guid"] != current["company"]["guid"]:
|
||||||
|
self.logger.info(
|
||||||
|
"Opening Balance cross-FY mirror company GUID differs previous=%s current=%s; continuing because FY-split Tally companies are supported.",
|
||||||
|
previous["company"]["guid"], current["company"]["guid"],
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"previous_company": previous["company"],
|
||||||
|
"current_company": current["company"],
|
||||||
|
"previous_masters": previous["masters"],
|
||||||
|
"current_masters": current["masters"],
|
||||||
|
"previous_mirror_path": previous["path"],
|
||||||
|
"current_mirror_path": current["path"],
|
||||||
|
"read_only": True,
|
||||||
|
"agent": self._agent_info(),
|
||||||
|
}
|
||||||
|
|
||||||
def _opening_balance_company(self, company_name: str):
|
def _opening_balance_company(self, company_name: str):
|
||||||
wanted=str(company_name or "").strip()
|
wanted=str(company_name or "").strip()
|
||||||
if not wanted: raise ValueError("Tally company name is required.")
|
if not wanted: raise ValueError("Tally company name is required.")
|
||||||
@@ -2391,8 +2669,7 @@ class AgentCommandProcessor:
|
|||||||
exceptions.sort(key=lambda x: (x["date"], x["party"].casefold()))
|
exceptions.sort(key=lambda x: (x["date"], x["party"].casefold()))
|
||||||
split_patterns.sort(key=lambda x: (x["date_from"], x["party"].casefold()))
|
split_patterns.sort(key=lambda x: (x["date_from"], x["party"].casefold()))
|
||||||
|
|
||||||
return {
|
review = {
|
||||||
"cash_payment_review": {
|
|
||||||
"company_name": company_name,
|
"company_name": company_name,
|
||||||
"company_guid": company_guid,
|
"company_guid": company_guid,
|
||||||
"date_from": date_from,
|
"date_from": date_from,
|
||||||
@@ -2418,10 +2695,24 @@ class AgentCommandProcessor:
|
|||||||
"tally_retry_count": 0,
|
"tally_retry_count": 0,
|
||||||
"tally_pause_ms": 0,
|
"tally_pause_ms": 0,
|
||||||
"read_only": True,
|
"read_only": True,
|
||||||
},
|
|
||||||
"mirror": mirror,
|
|
||||||
"agent": self._agent_info(),
|
|
||||||
}
|
}
|
||||||
|
analysis_run_id = self._save_analysis_run(
|
||||||
|
client_id,
|
||||||
|
analysis_type="CASH_PAYMENT_COMPLIANCE",
|
||||||
|
financial_year=str(payload.get("financial_year") or ""),
|
||||||
|
period_from=date_from,
|
||||||
|
period_to=date_to,
|
||||||
|
company_guid=company_guid,
|
||||||
|
company_name=company_name,
|
||||||
|
requested_by_user_id=payload.get("requested_by_user_id"),
|
||||||
|
source_mirror_path=str(mirror_status.get("mirror_db_path") or mirror.get("path") or ""),
|
||||||
|
source_mirror_synced_at=str((mirror.get("sync") or {}).get("last_sync_at") or ""),
|
||||||
|
parameters={"cash_limit": cash_limit, "split_window_days": split_window_days, "near_limit_percent": near_limit_percent},
|
||||||
|
summary=review.get("summary") or {},
|
||||||
|
result=review,
|
||||||
|
)
|
||||||
|
review["analysis_run_id"] = analysis_run_id
|
||||||
|
return {"cash_payment_review": review, "mirror": mirror, "agent": self._agent_info()}
|
||||||
|
|
||||||
def _cash_payment_cache_analyze(self, payload: dict[str, Any]) -> dict[str, Any]:
|
def _cash_payment_cache_analyze(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@@ -2832,15 +3123,24 @@ class AgentCommandProcessor:
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import date as _date
|
from datetime import date as _date
|
||||||
|
|
||||||
company, company_name = self._resolve_open_company(payload)
|
client_id = int(payload.get("client_id") or 0)
|
||||||
|
if client_id <= 0:
|
||||||
|
raise ValueError("client_id is required.")
|
||||||
|
mirror_status = self.tally.mirror.status(client_id)
|
||||||
|
mirror = mirror_status.get("mirror") or {}
|
||||||
|
if not mirror.get("ready"):
|
||||||
|
raise ValueError("Accounting Mirror is not ready. Run 'Mirror Tally to SQLite' first.")
|
||||||
|
company_info = mirror.get("company") or {}
|
||||||
|
company_name = str(company_info.get("company_name") or "").strip()
|
||||||
|
company_guid = str(company_info.get("company_guid") or "").strip()
|
||||||
date_from = str(payload.get("date_from") or "").strip()
|
date_from = str(payload.get("date_from") or "").strip()
|
||||||
date_to = str(payload.get("date_to") or "").strip()
|
date_to = str(payload.get("date_to") or "").strip()
|
||||||
rules = list(payload.get("rules") or [])
|
rules = list(payload.get("rules") or [])
|
||||||
if not rules:
|
if not rules:
|
||||||
raise ValueError("No active TDS rules were supplied by ERP.")
|
raise ValueError("No active TDS rules were supplied by ERP.")
|
||||||
vouchers = self.tally.export_vouchers(company_name, date_from, date_to)
|
vouchers = (self.tally.mirror.transactions(client_id, company_name, date_from, date_to, company_guid).get("vouchers") or [])
|
||||||
# TDS ledgers are identified by both ledger name and parent/group text; this is deliberately broader than one fixed ledger name.
|
# TDS ledgers are identified from the local mirror ledger master only.
|
||||||
masters = self.tally.export_master_collection(company_name, "ledgers")
|
masters = (self.tally.mirror.master_snapshot(client_id, company_name, company_guid).get("ledgers") or [])
|
||||||
tds_ledger_names = set()
|
tds_ledger_names = set()
|
||||||
for led in masters:
|
for led in masters:
|
||||||
name = str(led.get("name") or "").strip()
|
name = str(led.get("name") or "").strip()
|
||||||
@@ -2894,7 +3194,24 @@ class AgentCommandProcessor:
|
|||||||
liability=round(max(0.0,expected-actual),2)
|
liability=round(max(0.0,expected-actual),2)
|
||||||
status="OK" if expected>0 and liability<=0.009 else ("TDS NOT DEDUCTED" if expected>0 and actual<=0.009 else ("SHORT DEDUCTION" if liability>0 else "BELOW / OUTSIDE THRESHOLD"))
|
status="OK" if expected>0 and liability<=0.009 else ("TDS NOT DEDUCTED" if expected>0 and actual<=0.009 else ("SHORT DEDUCTION" if liability>0 else "BELOW / OUTSIDE THRESHOLD"))
|
||||||
transactions.append({**{k:v for k,v in x.items() if k!='rule'},"rule_id":r.get("id"),"rule_code":r.get("rule_code"),"rule_name":r.get("name"),"legacy_section":r.get("legacy_section"),"statutory_reference":r.get("statutory_reference"),"cumulative_amount":cumulative,"expected_tds":expected,"actual_tds":actual,"liability":liability,"status":status})
|
transactions.append({**{k:v for k,v in x.items() if k!='rule'},"rule_id":r.get("id"),"rule_code":r.get("rule_code"),"rule_name":r.get("name"),"legacy_section":r.get("legacy_section"),"statutory_reference":r.get("statutory_reference"),"cumulative_amount":cumulative,"expected_tds":expected,"actual_tds":actual,"liability":liability,"status":status})
|
||||||
return {"tds_review":{"company_name":company_name,"company_guid":str(company.get("guid") or ""),"date_from":date_from,"date_to":date_to,"summary":{"vouchers_reviewed":len(vouchers),"candidate_transactions":len(transactions),"tds_not_deducted":sum(1 for x in transactions if x["status"]=="TDS NOT DEDUCTED"),"short_deduction":sum(1 for x in transactions if x["status"]=="SHORT DEDUCTION"),"purchase_transactions":sum(1 for x in transactions if x["rule_code"]=="PURCHASE_GOODS")},"transactions":transactions,"tds_ledgers":sorted(tds_ledger_names),"read_only":True},"agent":self._agent_info()}
|
review = {"company_name":company_name,"company_guid":company_guid,"date_from":date_from,"date_to":date_to,"summary":{"vouchers_reviewed":len(vouchers),"candidate_transactions":len(transactions),"tds_not_deducted":sum(1 for x in transactions if x["status"]=="TDS NOT DEDUCTED"),"short_deduction":sum(1 for x in transactions if x["status"]=="SHORT DEDUCTION"),"purchase_transactions":sum(1 for x in transactions if x["rule_code"]=="PURCHASE_GOODS")},"transactions":transactions,"tds_ledgers":sorted(tds_ledger_names),"read_only":True,"sqlite_mirror":True}
|
||||||
|
analysis_run_id = self._save_analysis_run(
|
||||||
|
client_id,
|
||||||
|
analysis_type="TDS_COMPLIANCE",
|
||||||
|
financial_year=str(payload.get("financial_year") or ""),
|
||||||
|
period_from=date_from,
|
||||||
|
period_to=date_to,
|
||||||
|
company_guid=company_guid,
|
||||||
|
company_name=company_name,
|
||||||
|
requested_by_user_id=payload.get("requested_by_user_id"),
|
||||||
|
source_mirror_path=str(mirror_status.get("mirror_db_path") or mirror.get("path") or ""),
|
||||||
|
source_mirror_synced_at=str((mirror.get("sync") or {}).get("last_sync_at") or ""),
|
||||||
|
parameters={"rule_count": len(rules)},
|
||||||
|
summary=review["summary"],
|
||||||
|
result=review,
|
||||||
|
)
|
||||||
|
review["analysis_run_id"] = analysis_run_id
|
||||||
|
return {"tds_review":review,"mirror":mirror,"agent":self._agent_info()}
|
||||||
|
|
||||||
def _post_tds_liability(self, payload: dict[str, Any]) -> dict[str, Any]:
|
def _post_tds_liability(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
company, company_name = self._resolve_open_company(payload)
|
company, company_name = self._resolve_open_company(payload)
|
||||||
@@ -3045,29 +3362,68 @@ class AgentCommandProcessor:
|
|||||||
"agent": self._agent_info(),
|
"agent": self._agent_info(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _depreciation_preview(self, payload: dict[str, Any]) -> dict[str, Any]:
|
def _depreciation_mirror_context(self, payload: dict[str, Any]):
|
||||||
client_id = int(payload.get("client_id"))
|
client_id = int(payload.get("client_id") or 0)
|
||||||
tally_guid = str(payload.get("tally_guid") or "").strip()
|
if client_id <= 0:
|
||||||
|
raise ValueError("client_id is required.")
|
||||||
|
mirror_status = self.tally.mirror.status(client_id)
|
||||||
|
mirror = mirror_status.get("mirror") or {}
|
||||||
|
if not mirror.get("ready"):
|
||||||
|
raise ValueError("Accounting Mirror is not ready for this financial year. Run 'Mirror Tally to SQLite' first.")
|
||||||
|
company = mirror.get("company") or {}
|
||||||
|
company_name = str(company.get("company_name") or "").strip()
|
||||||
|
company_guid = str(company.get("company_guid") or "").strip()
|
||||||
|
if not company_name:
|
||||||
|
raise ValueError("Accounting Mirror does not contain a company identity.")
|
||||||
|
mapping = {}
|
||||||
|
if company_guid:
|
||||||
|
try:
|
||||||
|
mapping = self.store.get_active_mapping_by_guid(client_id, company_guid)
|
||||||
|
except Exception:
|
||||||
|
mapping = {}
|
||||||
|
if not mapping:
|
||||||
|
mappings = self.store.list_active_mappings(client_id)
|
||||||
|
mapping = (mappings[0] if mappings else {
|
||||||
|
"tally_guid": company_guid,
|
||||||
|
"company_name": company_name,
|
||||||
|
"gstin": str(company.get("gstin") or ""),
|
||||||
|
})
|
||||||
|
return client_id, mirror_status, mirror, company_name, company_guid, mapping
|
||||||
|
|
||||||
|
def _depreciation_refresh_local_snapshot(self, payload: dict[str, Any]):
|
||||||
|
client_id, mirror_status, mirror, company_name, company_guid, mapping = self._depreciation_mirror_context(payload)
|
||||||
fy_start = str(payload.get("fy_start") or "").strip()
|
fy_start = str(payload.get("fy_start") or "").strip()
|
||||||
fy_end = str(payload.get("fy_end") or "").strip()
|
fy_end = str(payload.get("fy_end") or "").strip()
|
||||||
if not tally_guid: raise ValueError("Select a mapped Tally company for depreciation.")
|
masters = self.tally.mirror.master_snapshot(client_id, company_name, company_guid)
|
||||||
sync_payload = dict(payload)
|
transactions = self.tally.mirror.transactions(client_id, company_name, fy_start, fy_end, company_guid)
|
||||||
sync_payload["date_from"] = fy_start
|
self.store.replace_master_snapshot(
|
||||||
sync_payload["date_to"] = fy_end
|
client_id,
|
||||||
self._sync_masters(sync_payload)
|
mapping={**mapping, "company_name": company_name, "tally_guid": company_guid},
|
||||||
self._sync_transactions(sync_payload)
|
masters=masters,
|
||||||
return {"preview": self.store.depreciation_preview(client_id, tally_guid=tally_guid, fy_start=fy_start, fy_end=fy_end), "agent": self._agent_info()}
|
requested_by_user_id=int(payload.get("requested_by_user_id")) if payload.get("requested_by_user_id") not in (None, "") else None,
|
||||||
|
)
|
||||||
|
self.store.replace_transaction_snapshot(
|
||||||
|
client_id,
|
||||||
|
mapping={**mapping, "company_name": company_name, "tally_guid": company_guid},
|
||||||
|
transactions=transactions,
|
||||||
|
requested_by_user_id=int(payload.get("requested_by_user_id")) if payload.get("requested_by_user_id") not in (None, "") else None,
|
||||||
|
)
|
||||||
|
return client_id, mirror_status, mirror, company_name, company_guid
|
||||||
|
|
||||||
|
def _depreciation_preview(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
client_id, mirror_status, mirror, company_name, company_guid = self._depreciation_refresh_local_snapshot(payload)
|
||||||
|
fy_start = str(payload.get("fy_start") or "").strip()
|
||||||
|
fy_end = str(payload.get("fy_end") or "").strip()
|
||||||
|
preview = self.store.depreciation_preview(
|
||||||
|
client_id, tally_guid=company_guid, fy_start=fy_start, fy_end=fy_end
|
||||||
|
)
|
||||||
|
return {"preview": preview, "mirror": mirror, "agent": self._agent_info()}
|
||||||
|
|
||||||
def _calculate_it_depreciation(self, payload: dict[str, Any]) -> dict[str, Any]:
|
def _calculate_it_depreciation(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
client_id = int(payload.get("client_id"))
|
client_id, mirror_status, mirror, company_name, company_guid = self._depreciation_refresh_local_snapshot(payload)
|
||||||
sync_payload = dict(payload)
|
|
||||||
sync_payload["date_from"] = str(payload.get("fy_start") or "").strip()
|
|
||||||
sync_payload["date_to"] = str(payload.get("fy_end") or "").strip()
|
|
||||||
self._sync_masters(sync_payload)
|
|
||||||
self._sync_transactions(sync_payload)
|
|
||||||
result = self.store.calculate_it_depreciation(
|
result = self.store.calculate_it_depreciation(
|
||||||
client_id,
|
client_id,
|
||||||
tally_guid=str(payload.get("tally_guid") or "").strip(),
|
tally_guid=company_guid,
|
||||||
fy_start=str(payload.get("fy_start") or "").strip(),
|
fy_start=str(payload.get("fy_start") or "").strip(),
|
||||||
fy_end=str(payload.get("fy_end") or "").strip(),
|
fy_end=str(payload.get("fy_end") or "").strip(),
|
||||||
assignments=list(payload.get("assignments") or []),
|
assignments=list(payload.get("assignments") or []),
|
||||||
@@ -3075,8 +3431,31 @@ class AgentCommandProcessor:
|
|||||||
depreciation_reserve_ledger=str(payload.get("depreciation_reserve_ledger") or "").strip(),
|
depreciation_reserve_ledger=str(payload.get("depreciation_reserve_ledger") or "").strip(),
|
||||||
requested_by_user_id=int(payload.get("requested_by_user_id")) if payload.get("requested_by_user_id") not in (None, "") else None,
|
requested_by_user_id=int(payload.get("requested_by_user_id")) if payload.get("requested_by_user_id") not in (None, "") else None,
|
||||||
)
|
)
|
||||||
self.logger.info("IT depreciation draft calculated client_id=%s company=%s run_id=%s total=%s", client_id, result.get("company_name"), result.get("run_id"), result.get("total_depreciation"))
|
analysis_run_id = self._save_analysis_run(
|
||||||
return {"calculated": True, "depreciation": result, "accounting": self.store.snapshot(client_id), "agent": self._agent_info()}
|
client_id,
|
||||||
|
analysis_type="DEPRECIATION_IT",
|
||||||
|
financial_year=str(payload.get("financial_year") or ""),
|
||||||
|
period_from=str(payload.get("fy_start") or ""),
|
||||||
|
period_to=str(payload.get("fy_end") or ""),
|
||||||
|
company_guid=company_guid,
|
||||||
|
company_name=company_name,
|
||||||
|
requested_by_user_id=payload.get("requested_by_user_id"),
|
||||||
|
source_mirror_path=str(mirror_status.get("mirror_db_path") or mirror.get("path") or ""),
|
||||||
|
source_mirror_synced_at=str((mirror.get("sync") or {}).get("last_sync_at") or ""),
|
||||||
|
parameters={
|
||||||
|
"depreciation_expense_ledger": str(payload.get("depreciation_expense_ledger") or ""),
|
||||||
|
"depreciation_reserve_ledger": str(payload.get("depreciation_reserve_ledger") or ""),
|
||||||
|
},
|
||||||
|
summary={
|
||||||
|
"total_depreciation": result.get("total_depreciation"),
|
||||||
|
"line_count": len(result.get("lines") or []),
|
||||||
|
"status": result.get("status"),
|
||||||
|
},
|
||||||
|
result=result,
|
||||||
|
)
|
||||||
|
result["analysis_run_id"] = analysis_run_id
|
||||||
|
self.logger.info("IT depreciation draft calculated from SQLite mirror client_id=%s company=%s run_id=%s total=%s", client_id, result.get("company_name"), result.get("run_id"), result.get("total_depreciation"))
|
||||||
|
return {"calculated": True, "depreciation": result, "mirror": mirror, "accounting": self.store.snapshot(client_id), "agent": self._agent_info()}
|
||||||
|
|
||||||
def _get_it_depreciation_run(self, payload: dict[str, Any]) -> dict[str, Any]:
|
def _get_it_depreciation_run(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
client_id = int(payload.get("client_id")); run_id = int(payload.get("run_id"))
|
client_id = int(payload.get("client_id")); run_id = int(payload.get("run_id"))
|
||||||
|
|||||||
Reference in New Issue
Block a user