Add Phase 5 Income-tax depreciation tool

This commit is contained in:
A R R R Associates
2026-08-19 21:56:26 +05:30
parent 102520bb25
commit 66d286fd55
11 changed files with 407 additions and 14 deletions
+2
View File
@@ -338,6 +338,7 @@ ROLE_PERMISSION_MAP = {
"accounting.tally.map_company", "accounting.tally.map_company",
"accounting.tally.sync_masters", "accounting.tally.sync_masters",
"accounting.tally.sync_transactions", "accounting.tally.sync_transactions",
"accounting.tally.depreciation_it",
"accounting.act.initialize", "accounting.act.initialize",
"clients.view.own_only", "clients.view.own_only",
"employees.dashboard.view", "employees.dashboard.view",
@@ -973,3 +974,4 @@ def on_startup(app: FastAPI) -> None:
+24 -3
View File
@@ -8,7 +8,7 @@ import os
import sqlite3 import sqlite3
from typing import Iterator, Sequence from typing import Iterator, Sequence
ACT_SCHEMA_VERSION = 4 ACT_SCHEMA_VERSION = 5
class AccountingActStoreError(RuntimeError): class AccountingActStoreError(RuntimeError):
@@ -32,7 +32,8 @@ class AccountingActStore:
Phase 1 provides accounting storage and read-only Tally discovery. Phase 1 provides accounting storage and read-only Tally discovery.
Phase 2 adds durable client/registration -> Tally company mapping keyed by Phase 2 adds durable client/registration -> Tally company mapping keyed by
Tally GUID. Phase 3 adds read-only Tally accounting master snapshot tables. Tally GUID. Phase 3 adds read-only Tally accounting master snapshot tables.
Phase 4 adds read-only voucher/transaction tables. Tally write-back remains Phase 4 adds read-only voucher/transaction tables. Phase 5 adds draft-only
Income-tax depreciation rules and calculation tables. Tally write-back remains
out of scope. out of scope.
""" """
@@ -199,7 +200,27 @@ class AccountingActStore:
); );
CREATE INDEX IF NOT EXISTS ix_tally_vouchers_company_date ON tally_vouchers(tally_guid, voucher_date); CREATE INDEX IF NOT EXISTS ix_tally_vouchers_company_date ON tally_vouchers(tally_guid, voucher_date);
CREATE INDEX IF NOT EXISTS ix_tally_voucher_ledger_entries_voucher ON tally_voucher_ledger_entries(voucher_id, line_no); CREATE INDEX IF NOT EXISTS ix_tally_voucher_ledger_entries_voucher ON tally_voucher_ledger_entries(voucher_id, line_no);
CREATE INDEX IF NOT EXISTS ix_tally_voucher_inventory_entries_voucher ON tally_voucher_inventory_entries(voucher_id, line_no); CREATE INDEX IF NOT EXISTS ix_tally_bank_allocations_ledger_entry ON tally_bank_allocations(ledger_entry_id, line_no);
CREATE TABLE IF NOT EXISTS it_depreciation_rate_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, ledger_key TEXT NOT NULL, ledger_guid TEXT NOT NULL DEFAULT '',
ledger_name TEXT NOT NULL, rate_percent REAL NOT NULL DEFAULT 0, updated_by_user_id INTEGER, updated_at_utc TEXT NOT NULL,
UNIQUE(tally_guid, ledger_key)
);
CREATE TABLE IF NOT EXISTS it_depreciation_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, company_name TEXT NOT NULL, mapping_id INTEGER,
fy_start TEXT NOT NULL, fy_end TEXT NOT NULL, depreciation_expense_ledger TEXT NOT NULL DEFAULT '', depreciation_reserve_ledger TEXT NOT NULL DEFAULT '',
requested_by_user_id INTEGER, status TEXT NOT NULL DEFAULT 'draft', created_at_utc TEXT NOT NULL, total_depreciation REAL NOT NULL DEFAULT 0, details_json TEXT NOT NULL DEFAULT '{}'
);
CREATE TABLE IF NOT EXISTS it_depreciation_run_lines (
id INTEGER PRIMARY KEY AUTOINCREMENT, run_id INTEGER NOT NULL, line_no INTEGER NOT NULL, ledger_key TEXT NOT NULL, ledger_guid TEXT NOT NULL DEFAULT '',
ledger_name TEXT NOT NULL, parent_group TEXT NOT NULL DEFAULT '', opening_wdv REAL NOT NULL DEFAULT 0, additions_full_rate REAL NOT NULL DEFAULT 0,
additions_half_rate REAL NOT NULL DEFAULT 0, reductions REAL NOT NULL DEFAULT 0, rate_percent REAL NOT NULL DEFAULT 0, depreciation_amount REAL NOT NULL DEFAULT 0,
closing_wdv REAL NOT NULL DEFAULT 0, payload_json TEXT NOT NULL DEFAULT '{}', FOREIGN KEY(run_id) REFERENCES it_depreciation_runs(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS ix_it_dep_rate_rules_company ON it_depreciation_rate_rules(tally_guid, ledger_name);
CREATE INDEX IF NOT EXISTS ix_it_dep_runs_company_fy ON it_depreciation_runs(tally_guid, fy_start, fy_end, id);
CREATE INDEX IF NOT EXISTS ix_it_dep_lines_run ON it_depreciation_run_lines(run_id, line_no);
CREATE INDEX IF NOT EXISTS ix_tally_groups_company ON tally_groups(tally_guid, name); CREATE INDEX IF NOT EXISTS ix_tally_groups_company ON tally_groups(tally_guid, name);
CREATE INDEX IF NOT EXISTS ix_tally_ledgers_company ON tally_ledgers(tally_guid, name); CREATE INDEX IF NOT EXISTS ix_tally_ledgers_company ON tally_ledgers(tally_guid, name);
@@ -0,0 +1,69 @@
{% extends "ui/templates/base/layout.html" %}
{% block content %}
{% set agent = live_result.agent if live_result else None %}
{% 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="flex flex-wrap items-start justify-between gap-4">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">Tools · Tally · Phase 5</p>
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Depreciation (Income Tax)</h1>
<p class="mt-1 text-sm text-slate-500">Review synchronized Fixed Assets, assign rates ledger-wise and generate a draft depreciation calculation. No entry is posted to Tally.</p>
</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>
</div>
{% if calculated %}<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">Income-tax depreciation draft calculated and saved in the client accounting database. Nothing was posted to Tally.</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">
<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">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">Phase 5</div><div class="mt-2 text-lg font-semibold {{ 'text-emerald-700' if agent and agent.it_depreciation_capability else 'text-slate-700' }}">{{ 'Available' if agent and agent.it_depreciation_capability else 'Update Agent' }}</div><div class="mt-1 text-xs text-slate-500">Draft calculation only</div></div>
</section>
<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">
<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>
<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>
</section>
{% if preview %}
{% 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">
{% if coverage.complete %}Phase 4 transaction data completely covers {{ preview.fy_start }} to {{ preview.fy_end }}.{% else %}Phase 4 transaction data does not fully cover this financial year. Synchronize the missing period before calculation.{% endif %}
</div>
<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 }}">
<section class="rounded-2xl bg-white p-5 shadow-soft">
<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 Reserve / Credit Ledger</label><select name="depreciation_reserve_ledger" class="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2"><option value="">Depreciation Reserve</option>{% for name in preview.all_ledgers %}<option value="{{ name }}">{{ name }}</option>{% endfor %}</select></div>
</div>
<p class="mt-3 text-xs text-slate-500">These ledger selections are stored with the draft for the proposed journal. Phase 5 does not create or post any Tally voucher.</p>
</section>
<section class="overflow-hidden rounded-2xl bg-white shadow-soft">
<div class="border-b border-slate-200 px-5 py-4"><h2 class="font-semibold text-slate-900">Fixed Asset Ledgers</h2><p class="mt-1 text-sm text-slate-500">Rate is selected by the reviewer. Transaction date is used only as a review proxy for the 180-day put-to-use test; verify where actual put-to-use differs.</p></div>
<div class="overflow-x-auto"><table class="min-w-full text-sm"><thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500"><tr><th class="px-4 py-3">Ledger</th><th class="px-4 py-3 text-right">Opening WDV</th><th class="px-4 py-3 text-right">Additions ≥180 days</th><th class="px-4 py-3 text-right">Additions &lt;180 days</th><th class="px-4 py-3 text-right">Reductions</th><th class="px-4 py-3">Rate</th></tr></thead><tbody class="divide-y divide-slate-100">
{% for row in preview.fixed_asset_ledgers %}<tr><td class="px-4 py-3"><div class="font-medium text-slate-900">{{ row.ledger_name }}</div><div class="text-xs text-slate-500">{{ row.parent_group }}</div></td><td class="px-4 py-3 text-right">{{ '%.2f'|format(row.opening_wdv) }}</td><td class="px-4 py-3 text-right">{{ '%.2f'|format(row.additions_full_rate) }}</td><td class="px-4 py-3 text-right">{{ '%.2f'|format(row.additions_half_rate) }}</td><td class="px-4 py-3 text-right">{{ '%.2f'|format(row.reductions) }}</td><td class="px-4 py-3"><select name="rate__{{ row.ledger_key }}" class="rounded-lg border border-slate-300 px-2 py-1.5"><option value="0" {% if row.rate_percent == 0 %}selected{% endif %}>0%</option>{% for rate in [5,10,15,20,25,30,40,45,60,80,100] %}<option value="{{ rate }}" {% if row.rate_percent == rate %}selected{% endif %}>{{ rate }}%</option>{% endfor %}</select></td></tr>{% else %}<tr><td colspan="6" class="px-4 py-8 text-center text-slate-500">No ledgers under Fixed Assets were found in the synchronized master snapshot.</td></tr>{% endfor %}
</tbody></table></div>
<div class="flex justify-end border-t border-slate-200 px-5 py-4"><button type="submit" {% if not coverage.complete or not preview.fixed_asset_ledgers %}disabled{% endif %} class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-50">Calculate & Save Draft</button></div>
</section>
</form>
{% endif %}
{% if depreciation_run %}
<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">Proposed only: Debit {{ depreciation_run.depreciation_expense_ledger or 'Depreciation' }} and credit {{ depreciation_run.depreciation_reserve_ledger or 'Depreciation Reserve' }} for ₹ {{ '%.2f'|format(depreciation_run.total_depreciation) }}. Tally write-back remains disabled.</div>
<div class="mt-4 overflow-x-auto"><table class="min-w-full text-sm"><thead class="bg-slate-50 text-left text-xs uppercase text-slate-500"><tr><th class="px-3 py-2">Ledger</th><th class="px-3 py-2 text-right">Rate</th><th class="px-3 py-2 text-right">Depreciation</th><th class="px-3 py-2 text-right">Closing WDV</th></tr></thead><tbody class="divide-y divide-slate-100">{% for row in depreciation_run.lines %}<tr><td class="px-3 py-2">{{ row.ledger_name }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.rate_percent) }}%</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.depreciation_amount) }}</td><td class="px-3 py-2 text-right">{{ '%.2f'|format(row.closing_wdv) }}</td></tr>{% endfor %}</tbody></table></div>
</section>
{% endif %}
<div class="rounded-xl border border-blue-200 bg-blue-50 px-4 py-3 text-sm text-blue-800">Phase 5 is review/calculation only. Phase 6 will add controlled, approved Tally write-back.</div>
</div>
{% endblock %}
@@ -9,11 +9,14 @@
<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 · Accounting</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">Tally Accounting Masters</h1> <h1 class="mt-1 text-2xl font-semibold text-slate-900">Tally Accounting</h1>
<p class="mt-1 text-sm text-slate-500">Phase 3 permanently maps an ERP client or registration to a loaded TallyPrime company using the Tally GUID.</p> <p class="mt-1 text-sm text-slate-500">Company mapping, master sync and transaction sync remain available. Phase 5 adds review-only accounting tools without Tally write-back.</p>
</div> </div>
<div class="flex flex-wrap gap-2">
{% if selected_client %}<a href="/tools/tally/depreciation?client_id={{ selected_client.id }}" class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50">Depreciation (IT)</a>{% endif %}
<a href="/tools/tally{% if selected_client %}?client_id={{ selected_client.id }}&refresh=1{% else %}?refresh=1{% endif %}" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white shadow-soft hover:bg-brand-700">Refresh Tally Companies</a> <a href="/tools/tally{% if selected_client %}?client_id={{ selected_client.id }}&refresh=1{% else %}?refresh=1{% endif %}" class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white shadow-soft hover:bg-brand-700">Refresh Tally Companies</a>
</div> </div>
</div>
{% if initialized %} {% if initialized %}
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">Client accounting storage was initialized successfully.</div> <div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">Client accounting storage was initialized successfully.</div>
+74
View File
@@ -469,3 +469,77 @@ def sync_tally_transactions(
return RedirectResponse(url=f"/tools/tally?client_id={client.id}&refresh=1&transactions_synced=1", status_code=303) return RedirectResponse(url=f"/tools/tally?client_id={client.id}&refresh=1&transactions_synced=1", status_code=303)
finally: finally:
db.close() db.close()
@router.get("/depreciation")
def depreciation_it_tool(
request: Request,
client_id: int | None = None,
tally_guid: str = "",
fy_start: str = "",
fy_end: str = "",
calculated: int = 0,
run_id: int | None = None,
error: str = "",
):
db = CommonSessionLocal()
try:
user, response = _require_partner(request, db, "accounting.tally.depreciation_it")
if response: return response
clients, scope = _visible_clients(db, request, user)
selected_client = next((row for row in clients if client_id and int(row.id) == int(client_id)), None)
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
online = _node_online(node)
today = date.today(); start_year = today.year if today.month >= 4 else today.year - 1
start_text = fy_start or date(start_year,4,1).isoformat(); end_text = fy_end or date(start_year+1,3,31).isoformat()
live_result = None; preview = None; depreciation_run = None; command_error = error or ""
if selected_client and node and online:
try:
status_response = request_agent_command(node.node_code,"phase5_status",{"client_id":int(selected_client.id)},timeout_seconds=20)
if status_response.get("ok"): live_result=status_response.get("result") or {}
else: command_error=str(status_response.get("error") or "Local Agent status failed.")
chosen_guid=str(tally_guid or "").strip()
if chosen_guid:
preview_response=request_agent_command(node.node_code,"accounting_depreciation_preview",{"client_id":int(selected_client.id),"tally_guid":chosen_guid,"fy_start":start_text,"fy_end":end_text},timeout_seconds=60)
if preview_response.get("ok"): preview=(preview_response.get("result") or {}).get("preview")
else: command_error=str(preview_response.get("error") or "Depreciation preview failed.")
if run_id:
run_response=request_agent_command(node.node_code,"accounting_get_it_depreciation_run",{"client_id":int(selected_client.id),"run_id":int(run_id)},timeout_seconds=30)
if run_response.get("ok"): depreciation_run=(run_response.get("result") or {}).get("depreciation")
except Exception as exc: command_error=str(exc)
base={"request":request,"current_user":user,"current_user_roles":get_user_roles(db,user.id),"current_user_permissions":get_user_permissions(db,user.id),"csrf_token":get_or_create_csrf_token(request)}
base.update({"title":"Income-tax Depreciation","clients":clients,"selected_client":selected_client,"storage_node":node,"agent_online":online,"live_result":live_result,"preview":preview,"selected_tally_guid":str(tally_guid or ""),"fy_start":start_text,"fy_end":end_text,"calculated":bool(calculated),"depreciation_run":depreciation_run,"command_error":command_error})
return templates.TemplateResponse("modules/accounting/templates/accounting/depreciation_it.html",base)
finally: db.close()
@router.post("/depreciation/calculate")
async def calculate_it_depreciation(request: Request):
form = await request.form()
csrf_token = str(form.get("csrf_token") or ""); validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user, response = _require_partner(request, db, "accounting.tally.depreciation_it")
if response: return response
try: client_id=int(form.get("client_id")); start=date.fromisoformat(str(form.get("fy_start") or "")); end=date.fromisoformat(str(form.get("fy_end") or ""))
except Exception as exc: return RedirectResponse(url=f"/tools/tally/depreciation?error={quote('Invalid client or financial-year dates.')}",status_code=303)
client, _clients, scope = _find_visible_client(db, request, user, client_id)
if not client: return _denied()
tally_guid=str(form.get("tally_guid") or "").strip()
assignments=[]
for key,value in form.multi_items():
if not str(key).startswith("rate__"): continue
ledger_key=str(key)[6:]
try: rate=float(str(value or "0").strip() or 0)
except Exception: rate=-1
assignments.append({"ledger_key":ledger_key,"rate_percent":rate})
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
if not node or not _node_online(node): return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&tally_guid={quote(tally_guid)}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&error={quote('ERP Local Agent is offline for the active branch.')}",status_code=303)
try:
result=request_agent_command(node.node_code,"accounting_calculate_it_depreciation",{"client_id":int(client.id),"tally_guid":tally_guid,"fy_start":start.isoformat(),"fy_end":end.isoformat(),"assignments":assignments,"depreciation_expense_ledger":str(form.get("depreciation_expense_ledger") or ""),"depreciation_reserve_ledger":str(form.get("depreciation_reserve_ledger") or ""),"requested_by_user_id":int(user.id)},timeout_seconds=120)
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Income-tax depreciation calculation failed."))
dep=(result.get("result") or {}).get("depreciation") or {}; rid=int(dep.get("run_id"))
except Exception as exc:
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&tally_guid={quote(tally_guid)}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&error={quote(str(exc))}",status_code=303)
return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&tally_guid={quote(tally_guid)}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&calculated=1&run_id={rid}",status_code=303)
finally: db.close()
@@ -152,6 +152,7 @@ PERMISSIONS = {
"accounting.tally.map_company": "Map ERP Client or Registration to Tally Company", "accounting.tally.map_company": "Map ERP Client or Registration to Tally Company",
"accounting.tally.sync_masters": "Synchronize Tally Accounting Masters", "accounting.tally.sync_masters": "Synchronize Tally Accounting Masters",
"accounting.tally.sync_transactions": "Synchronize Tally Accounting Transactions", "accounting.tally.sync_transactions": "Synchronize Tally Accounting Transactions",
"accounting.tally.depreciation_it": "Calculate Income-tax Depreciation",
"accounting.act.initialize": "Initialize Client Accounting ACT Storage", "accounting.act.initialize": "Initialize Client Accounting ACT Storage",
"notice_cases.view": "View Notice and Case Management", "notice_cases.view": "View Notice and Case Management",
@@ -183,3 +184,4 @@ def expand_permission_codes(code: str) -> list[str]:
+1 -1
View File
@@ -4,7 +4,7 @@ import io
from pathlib import Path from pathlib import Path
import zipfile import zipfile
ERP_LOCAL_AGENT_VERSION = "1.5.0" ERP_LOCAL_AGENT_VERSION = "1.6.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)
@@ -21,4 +21,6 @@ Phase 2: client/registration to Tally company mapping is supported using Tally G
Phase 3: read-only Tally accounting master sync (Groups, Ledgers, Voucher Types, Stock Groups/Categories/Items, Units and Cost Centres/Categories) into client .act storage. Phase 3: read-only Tally accounting master sync (Groups, Ledgers, Voucher Types, Stock Groups/Categories/Items, Units and Cost Centres/Categories) into client .act storage.
Version 1.5.0: Phase 4 read-only Tally transaction synchronization (vouchers, ledger/inventory entries, bill/cost-centre/bank allocations) with raw voucher XML preservation. Version 1.6.0: Phase 4 read-only Tally transaction synchronization (vouchers, ledger/inventory entries, bill/cost-centre/bank allocations) with raw voucher XML preservation.
Phase 5 adds draft-only Income-tax depreciation calculations from synchronized .act data. No Tally write-back.
@@ -1,2 +1,2 @@
__version__ = "1.5.0" __version__ = "1.6.0"
AGENT_NAME = "ERP Local Agent" AGENT_NAME = "ERP Local Agent"
@@ -1,13 +1,13 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timezone from datetime import date, datetime, timedelta, timezone
import json import json
from pathlib import Path from pathlib import Path
import sqlite3 import sqlite3
from typing import Sequence from typing import Sequence
SCHEMA_VERSION = "4" SCHEMA_VERSION = "5"
MASTER_TABLES = { MASTER_TABLES = {
"groups": "tally_groups", "groups": "tally_groups",
@@ -31,8 +31,9 @@ class LocalAccountingStore:
Phase 3 preserves Phase 1/2 metadata and mappings and adds company-scoped Phase 3 preserves Phase 1/2 metadata and mappings and adds company-scoped
read-only Tally master snapshots. Phase 4 adds date-scoped vouchers and their read-only Tally master snapshots. Phase 4 adds date-scoped vouchers and their
ledger, inventory, bill, cost-centre and bank allocations. Existing .act files ledger, inventory, bill, cost-centre and bank allocations. Phase 5 adds
are upgraded in place. reviewable Income-tax depreciation rate rules and draft calculations only.
Existing .act files are upgraded in place and no Tally write-back occurs.
""" """
def __init__(self, storage_root: Path): def __init__(self, storage_root: Path):
@@ -244,6 +245,35 @@ class LocalAccountingStore:
FOREIGN KEY(ledger_entry_id) REFERENCES tally_voucher_ledger_entries(id) ON DELETE CASCADE FOREIGN KEY(ledger_entry_id) REFERENCES tally_voucher_ledger_entries(id) ON DELETE CASCADE
); );
CREATE INDEX IF NOT EXISTS ix_tally_bank_allocations_ledger_entry ON tally_bank_allocations(ledger_entry_id, line_no); CREATE INDEX IF NOT EXISTS ix_tally_bank_allocations_ledger_entry ON tally_bank_allocations(ledger_entry_id, line_no);
CREATE TABLE IF NOT EXISTS it_depreciation_rate_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tally_guid TEXT NOT NULL, ledger_key TEXT NOT NULL, ledger_guid TEXT NOT NULL DEFAULT '',
ledger_name TEXT NOT NULL, rate_percent REAL NOT NULL DEFAULT 0,
updated_by_user_id INTEGER, updated_at_utc TEXT NOT NULL,
UNIQUE(tally_guid, ledger_key)
);
CREATE INDEX IF NOT EXISTS ix_it_dep_rate_rules_company ON it_depreciation_rate_rules(tally_guid, ledger_name);
CREATE TABLE IF NOT EXISTS it_depreciation_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT, tally_guid TEXT NOT NULL, company_name TEXT NOT NULL,
mapping_id INTEGER, fy_start TEXT NOT NULL, fy_end TEXT NOT NULL,
depreciation_expense_ledger TEXT NOT NULL DEFAULT '', depreciation_reserve_ledger TEXT NOT NULL DEFAULT '',
requested_by_user_id INTEGER, status TEXT NOT NULL DEFAULT 'draft', created_at_utc TEXT NOT NULL,
total_depreciation REAL NOT NULL DEFAULT 0, details_json TEXT NOT NULL DEFAULT '{}'
);
CREATE INDEX IF NOT EXISTS ix_it_dep_runs_company_fy ON it_depreciation_runs(tally_guid, fy_start, fy_end, id);
CREATE TABLE IF NOT EXISTS it_depreciation_run_lines (
id INTEGER PRIMARY KEY AUTOINCREMENT, run_id INTEGER NOT NULL, line_no INTEGER NOT NULL,
ledger_key TEXT NOT NULL, ledger_guid TEXT NOT NULL DEFAULT '', ledger_name TEXT NOT NULL, parent_group TEXT NOT NULL DEFAULT '',
opening_wdv REAL NOT NULL DEFAULT 0, additions_full_rate REAL NOT NULL DEFAULT 0,
additions_half_rate REAL NOT NULL DEFAULT 0, reductions REAL NOT NULL DEFAULT 0,
rate_percent REAL NOT NULL DEFAULT 0, depreciation_amount REAL NOT NULL DEFAULT 0,
closing_wdv REAL NOT NULL DEFAULT 0, payload_json TEXT NOT NULL DEFAULT '{}',
FOREIGN KEY(run_id) REFERENCES it_depreciation_runs(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS ix_it_dep_lines_run ON it_depreciation_run_lines(run_id, line_no);
""" """
) )
@@ -517,6 +547,159 @@ class LocalAccountingStore:
result[key] = int(db.execute(f"SELECT COUNT(*) FROM {table} WHERE tally_guid=?", (tally_guid,)).fetchone()[0]) result[key] = int(db.execute(f"SELECT COUNT(*) FROM {table} WHERE tally_guid=?", (tally_guid,)).fetchone()[0])
return result return result
@staticmethod
def _ledger_key(row: dict) -> str:
guid = str(row.get("master_guid") or row.get("guid") or "").strip()
name = str(row.get("name") or row.get("ledger_name") or "").strip()
return ("G:" + guid) if guid else ("N:" + name.casefold())
@staticmethod
def _is_debit_entry(row: sqlite3.Row | dict) -> bool:
flag = str(row["is_deemed_positive"] if isinstance(row, sqlite3.Row) else row.get("is_deemed_positive") or "").strip().lower()
if flag in {"yes", "true", "1"}:
return True
if flag in {"no", "false", "0"}:
return False
amount = float(row["amount"] if isinstance(row, sqlite3.Row) else row.get("amount") or 0)
return amount < 0
@staticmethod
def _half_rate_addition(voucher_date: str, fy_end: date) -> bool:
try:
acquired = date.fromisoformat(str(voucher_date or ""))
except Exception:
return False
# Transaction date is a review proxy for put-to-use date. Half-rate is
# flagged only when fewer than 180 calendar days remain including that date.
return ((fy_end - acquired).days + 1) < 180
def _fixed_asset_group_names(self, db: sqlite3.Connection, tally_guid: str) -> set[str]:
rows = db.execute("SELECT name, parent FROM tally_groups WHERE tally_guid=?", (tally_guid,)).fetchall()
all_groups = {str(r["name"] or "").strip(): str(r["parent"] or "").strip() for r in rows}
fixed = {name for name in all_groups if name.casefold() == "fixed assets"}
changed = True
while changed:
changed = False
fixed_fold = {x.casefold() for x in fixed}
for name, parent in all_groups.items():
if name not in fixed and parent.casefold() in fixed_fold:
fixed.add(name); changed = True
return fixed
def _transaction_coverage(self, db: sqlite3.Connection, tally_guid: str, fy_start: date, fy_end: date) -> dict:
rows = db.execute(
"SELECT details_json FROM tally_sync_runs WHERE sync_type='transactions' AND tally_guid=? AND status='completed' ORDER BY id",
(tally_guid,),
).fetchall()
ranges = []
for row in rows:
try:
d = json.loads(row["details_json"] or "{}")
a = date.fromisoformat(str(d.get("date_from") or "")); b = date.fromisoformat(str(d.get("date_to") or ""))
if b >= fy_start and a <= fy_end:
ranges.append((max(a, fy_start), min(b, fy_end)))
except Exception:
continue
ranges.sort()
merged = []
for a,b in ranges:
if not merged or a > merged[-1][1] + timedelta(days=1): merged.append([a,b])
elif b > merged[-1][1]: merged[-1][1] = b
complete = bool(merged and merged[0][0] <= fy_start and merged[-1][1] >= fy_end and all(merged[i][1] + timedelta(days=1) >= merged[i+1][0] for i in range(len(merged)-1)))
return {"complete": complete, "ranges": [{"from":a.isoformat(),"to":b.isoformat()} for a,b in merged]}
def depreciation_preview(self, client_id: int, *, tally_guid: str, fy_start: str, fy_end: str) -> dict:
if not self.exists(client_id):
raise ValueError("Accounting storage is not initialized for this client.")
mapping = self.get_active_mapping_by_guid(client_id, tally_guid)
start = date.fromisoformat(str(fy_start)); end = date.fromisoformat(str(fy_end))
if start > end: raise ValueError("Financial-year start cannot be after end date.")
with self.connect(client_id) as db:
fixed_groups = self._fixed_asset_group_names(db, tally_guid)
if not fixed_groups:
raise ValueError("Fixed Assets group was not found in synchronized Tally masters. Synchronize Phase 3 masters first.")
ledgers = db.execute(
"SELECT master_guid, name, parent, opening_balance, closing_balance, payload_json FROM tally_ledgers WHERE tally_guid=? ORDER BY name COLLATE NOCASE",
(tally_guid,),
).fetchall()
rule_rows = db.execute("SELECT ledger_key, rate_percent FROM it_depreciation_rate_rules WHERE tally_guid=?", (tally_guid,)).fetchall()
rules = {str(r["ledger_key"]): float(r["rate_percent"] or 0) for r in rule_rows}
coverage = self._transaction_coverage(db, tally_guid, start, end)
fixed_fold = {x.casefold() for x in fixed_groups}
result=[]
for row in ledgers:
if str(row["parent"] or "").strip().casefold() not in fixed_fold: continue
item=dict(row); key=self._ledger_key(item); name=str(row["name"] or "").strip()
entries=db.execute(
"""SELECT v.voucher_date, e.amount, e.is_deemed_positive
FROM tally_voucher_ledger_entries e JOIN tally_vouchers v ON v.id=e.voucher_id
WHERE v.tally_guid=? AND e.ledger_name=? AND v.voucher_date>=? AND v.voucher_date<=?
AND lower(COALESCE(v.is_cancelled,'')) NOT IN ('yes','true','1')
AND lower(COALESCE(v.is_optional,'')) NOT IN ('yes','true','1')""",
(tally_guid,name,start.isoformat(),end.isoformat()),
).fetchall()
full=half=reductions=0.0
for e in entries:
amt=abs(float(e["amount"] or 0))
if self._is_debit_entry(e):
if self._half_rate_addition(str(e["voucher_date"] or ""), end): half += amt
else: full += amt
else: reductions += amt
result.append({
"ledger_key":key,"ledger_guid":str(row["master_guid"] or ""),"ledger_name":name,"parent_group":str(row["parent"] or ""),
"opening_wdv":abs(float(row["opening_balance"] or 0)),"tally_closing_balance":abs(float(row["closing_balance"] or 0)),
"additions_full_rate":round(full,2),"additions_half_rate":round(half,2),"reductions":round(reductions,2),
"rate_percent":rules.get(key,0.0),
})
all_ledgers=[str(r["name"] or "").strip() for r in ledgers if str(r["name"] or "").strip()]
latest=db.execute("SELECT id, fy_start, fy_end, total_depreciation, depreciation_expense_ledger, depreciation_reserve_ledger, created_at_utc FROM it_depreciation_runs WHERE tally_guid=? ORDER BY id DESC LIMIT 1",(tally_guid,)).fetchone()
return {"mapping":mapping,"fy_start":start.isoformat(),"fy_end":end.isoformat(),"transaction_coverage":coverage,"fixed_asset_groups":sorted(fixed_groups,key=str.casefold),"fixed_asset_ledgers":result,"all_ledgers":all_ledgers,"latest_run":dict(latest) if latest else None}
def calculate_it_depreciation(self, client_id: int, *, tally_guid: str, fy_start: str, fy_end: str, assignments: list[dict], depreciation_expense_ledger: str = "", depreciation_reserve_ledger: str = "", requested_by_user_id: int | None = None) -> dict:
preview=self.depreciation_preview(client_id,tally_guid=tally_guid,fy_start=fy_start,fy_end=fy_end)
if not preview["transaction_coverage"].get("complete"):
raise ValueError("Phase 4 transaction data does not completely cover the selected financial year. Synchronize the full financial year before calculating depreciation.")
by_key={str(x.get("ledger_key")):x for x in preview["fixed_asset_ledgers"]}
assignment_map={str(x.get("ledger_key") or ""): float(x.get("rate_percent") or 0) for x in assignments}
unknown=[k for k in assignment_map if k not in by_key]
if unknown: raise ValueError("One or more submitted fixed-asset ledgers are no longer available. Refresh the depreciation page.")
for rate in assignment_map.values():
if rate < 0 or rate > 100: raise ValueError("Depreciation rate must be between 0% and 100%.")
now=_utc_now_iso(); lines=[]; total=0.0
for index,row in enumerate(preview["fixed_asset_ledgers"],1):
rate=float(assignment_map.get(row["ledger_key"], row.get("rate_percent") or 0))
opening=float(row["opening_wdv"] or 0); full=float(row["additions_full_rate"] or 0); half=float(row["additions_half_rate"] or 0); reductions=float(row["reductions"] or 0)
closing_before=max(0.0, opening+full+half-reductions)
full_base=max(0.0, opening+full-reductions)
dep=min(closing_before, (full_base*rate/100.0)+(half*rate/200.0))
dep=round(dep,2); closing=round(max(0.0,closing_before-dep),2); total+=dep
line={**row,"rate_percent":rate,"depreciation_amount":dep,"closing_wdv":closing,"closing_before_depreciation":round(closing_before,2)}
lines.append(line)
mapping=preview["mapping"]
with self.connect(client_id) as db:
for row in lines:
db.execute("""INSERT INTO it_depreciation_rate_rules(tally_guid,ledger_key,ledger_guid,ledger_name,rate_percent,updated_by_user_id,updated_at_utc)
VALUES(?,?,?,?,?,?,?) ON CONFLICT(tally_guid,ledger_key) DO UPDATE SET ledger_guid=excluded.ledger_guid,ledger_name=excluded.ledger_name,rate_percent=excluded.rate_percent,updated_by_user_id=excluded.updated_by_user_id,updated_at_utc=excluded.updated_at_utc""",
(tally_guid,row["ledger_key"],row["ledger_guid"],row["ledger_name"],row["rate_percent"],requested_by_user_id,now))
details={"calculation_basis":"Ledger-level draft using synchronized opening balance and voucher movements; transaction date is used as the review proxy for put-to-use date for the 180-day test.","transaction_coverage":preview["transaction_coverage"],"no_tally_writeback":True}
cur=db.execute("""INSERT INTO it_depreciation_runs(tally_guid,company_name,mapping_id,fy_start,fy_end,depreciation_expense_ledger,depreciation_reserve_ledger,requested_by_user_id,status,created_at_utc,total_depreciation,details_json)
VALUES(?,?,?,?,?,?,?,?, 'draft',?,?,?)""",
(tally_guid,str(mapping.get("company_name") or ""),mapping.get("id"),str(fy_start),str(fy_end),str(depreciation_expense_ledger or ""),str(depreciation_reserve_ledger or ""),requested_by_user_id,now,round(total,2),json.dumps(details,ensure_ascii=False,separators=(",",":"))))
run_id=int(cur.lastrowid)
for i,row in enumerate(lines,1):
db.execute("""INSERT INTO it_depreciation_run_lines(run_id,line_no,ledger_key,ledger_guid,ledger_name,parent_group,opening_wdv,additions_full_rate,additions_half_rate,reductions,rate_percent,depreciation_amount,closing_wdv,payload_json)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(run_id,i,row["ledger_key"],row["ledger_guid"],row["ledger_name"],row["parent_group"],row["opening_wdv"],row["additions_full_rate"],row["additions_half_rate"],row["reductions"],row["rate_percent"],row["depreciation_amount"],row["closing_wdv"],json.dumps(row,ensure_ascii=False,separators=(",",":"))))
return {"run_id":run_id,"status":"draft","company_name":str(mapping.get("company_name") or ""),"fy_start":str(fy_start),"fy_end":str(fy_end),"total_depreciation":round(total,2),"depreciation_expense_ledger":str(depreciation_expense_ledger or ""),"depreciation_reserve_ledger":str(depreciation_reserve_ledger or ""),"lines":lines,"proposed_entry":{"debit_ledger":str(depreciation_expense_ledger or "Depreciation"),"credit_ledger":str(depreciation_reserve_ledger or "Depreciation Reserve"),"amount":round(total,2)},"no_tally_writeback":True}
def get_it_depreciation_run(self, client_id: int, run_id: int) -> dict:
with self.connect(client_id) as db:
run=db.execute("SELECT * FROM it_depreciation_runs WHERE id=?",(int(run_id),)).fetchone()
if not run: raise ValueError("Depreciation draft was not found.")
lines=db.execute("SELECT * FROM it_depreciation_run_lines WHERE run_id=? ORDER BY line_no",(int(run_id),)).fetchall()
result=dict(run); result["lines"]=[dict(x) for x in lines]; result["no_tally_writeback"]=True
return result
def snapshot(self, client_id: int) -> dict: def snapshot(self, client_id: int) -> dict:
path = self.db_path(client_id) path = self.db_path(client_id)
if not path.is_file(): if not path.is_file():
@@ -536,6 +719,7 @@ class LocalAccountingStore:
latest = db.execute("SELECT checked_at_utc, connected, tally_url, company_count, error_message FROM tally_connection_history ORDER BY id DESC LIMIT 1").fetchone() latest = db.execute("SELECT checked_at_utc, connected, tally_url, company_count, error_message FROM tally_connection_history ORDER BY id DESC LIMIT 1").fetchone()
latest_sync = db.execute("SELECT id, sync_type, tally_guid, company_name, mapping_id, requested_by_user_id, status, started_at_utc, completed_at_utc, rows_processed, error_message, details_json FROM tally_sync_runs WHERE sync_type='masters' ORDER BY id DESC LIMIT 1").fetchone() latest_sync = db.execute("SELECT id, sync_type, tally_guid, company_name, mapping_id, requested_by_user_id, status, started_at_utc, completed_at_utc, rows_processed, error_message, details_json FROM tally_sync_runs WHERE sync_type='masters' ORDER BY id DESC LIMIT 1").fetchone()
latest_transaction_sync = db.execute("SELECT id, sync_type, tally_guid, company_name, mapping_id, requested_by_user_id, status, started_at_utc, completed_at_utc, rows_processed, error_message, details_json FROM tally_sync_runs WHERE sync_type='transactions' ORDER BY id DESC LIMIT 1").fetchone() latest_transaction_sync = db.execute("SELECT id, sync_type, tally_guid, company_name, mapping_id, requested_by_user_id, status, started_at_utc, completed_at_utc, rows_processed, error_message, details_json FROM tally_sync_runs WHERE sync_type='transactions' ORDER BY id DESC LIMIT 1").fetchone()
latest_depreciation = db.execute("SELECT id, tally_guid, company_name, fy_start, fy_end, total_depreciation, depreciation_expense_ledger, depreciation_reserve_ledger, status, created_at_utc FROM it_depreciation_runs ORDER BY id DESC LIMIT 1").fetchone()
loaded_guids = {str(row["guid"] or "") for row in companies if int(row["is_currently_loaded"] or 0)} loaded_guids = {str(row["guid"] or "") for row in companies if int(row["is_currently_loaded"] or 0)}
mapped = [] mapped = []
for row in mappings: for row in mappings:
@@ -565,4 +749,5 @@ class LocalAccountingStore:
"mappings": mapped, "mappings": mapped,
"latest_master_sync": latest_sync_dict, "latest_master_sync": latest_sync_dict,
"latest_transaction_sync": latest_transaction_sync_dict, "latest_transaction_sync": latest_transaction_sync_dict,
"latest_it_depreciation": dict(latest_depreciation) if latest_depreciation else None,
} }
@@ -23,7 +23,7 @@ class AgentCommandProcessor:
error: str | None = None error: str | None = None
ok = False ok = False
try: try:
if action in {"tally_status", "phase1_status", "phase2_status", "phase3_status", "phase4_status"}: if action in {"tally_status", "phase1_status", "phase2_status", "phase3_status", "phase4_status", "phase5_status"}:
result = self._status(payload) result = self._status(payload)
elif action == "accounting_initialize": elif action == "accounting_initialize":
result = self._initialize(payload) result = self._initialize(payload)
@@ -35,6 +35,12 @@ class AgentCommandProcessor:
result = self._sync_masters(payload) result = self._sync_masters(payload)
elif action == "accounting_sync_transactions": elif action == "accounting_sync_transactions":
result = self._sync_transactions(payload) result = self._sync_transactions(payload)
elif action == "accounting_depreciation_preview":
result = self._depreciation_preview(payload)
elif action == "accounting_calculate_it_depreciation":
result = self._calculate_it_depreciation(payload)
elif action == "accounting_get_it_depreciation_run":
result = self._get_it_depreciation_run(payload)
else: else:
raise ValueError(f"Unsupported local-agent command: {action}") raise ValueError(f"Unsupported local-agent command: {action}")
ok = True ok = True
@@ -49,6 +55,7 @@ class AgentCommandProcessor:
"tally_capability": True, "accounting_act_capability": True, "tally_capability": True, "accounting_act_capability": True,
"tally_mapping_capability": True, "tally_master_sync_capability": True, "tally_mapping_capability": True, "tally_master_sync_capability": True,
"tally_transaction_sync_capability": True, "tally_transaction_sync_capability": True,
"it_depreciation_capability": True,
} }
def _status(self, payload: dict[str, Any]) -> dict[str, Any]: def _status(self, payload: dict[str, Any]) -> dict[str, Any]:
@@ -172,3 +179,31 @@ class AgentCommandProcessor:
) )
self.logger.info("Tally transaction sync completed client_id=%s company=%s period=%s..%s rows=%s", client_id, company_name, date_from, date_to, sync.get("rows_processed")) self.logger.info("Tally transaction sync completed client_id=%s company=%s period=%s..%s rows=%s", client_id, company_name, date_from, date_to, sync.get("rows_processed"))
return {"synced": True, "sync": sync, "accounting": self.store.snapshot(client_id), "tally": tally_status, "agent": self._agent_info()} return {"synced": True, "sync": sync, "accounting": self.store.snapshot(client_id), "tally": tally_status, "agent": self._agent_info()}
def _depreciation_preview(self, payload: dict[str, Any]) -> dict[str, Any]:
client_id = int(payload.get("client_id"))
tally_guid = str(payload.get("tally_guid") or "").strip()
fy_start = str(payload.get("fy_start") or "").strip()
fy_end = str(payload.get("fy_end") or "").strip()
if not tally_guid: raise ValueError("Select a mapped Tally company for depreciation.")
return {"preview": self.store.depreciation_preview(client_id, tally_guid=tally_guid, fy_start=fy_start, fy_end=fy_end), "agent": self._agent_info()}
def _calculate_it_depreciation(self, payload: dict[str, Any]) -> dict[str, Any]:
client_id = int(payload.get("client_id"))
result = self.store.calculate_it_depreciation(
client_id,
tally_guid=str(payload.get("tally_guid") or "").strip(),
fy_start=str(payload.get("fy_start") or "").strip(),
fy_end=str(payload.get("fy_end") or "").strip(),
assignments=list(payload.get("assignments") or []),
depreciation_expense_ledger=str(payload.get("depreciation_expense_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,
)
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"))
return {"calculated": True, "depreciation": result, "accounting": self.store.snapshot(client_id), "agent": self._agent_info()}
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"))
return {"depreciation": self.store.get_it_depreciation_run(client_id, run_id), "agent": self._agent_info()}