Add Phase 5 Income-tax depreciation tool
This commit is contained in:
@@ -8,7 +8,7 @@ import os
|
||||
import sqlite3
|
||||
from typing import Iterator, Sequence
|
||||
|
||||
ACT_SCHEMA_VERSION = 4
|
||||
ACT_SCHEMA_VERSION = 5
|
||||
|
||||
|
||||
class AccountingActStoreError(RuntimeError):
|
||||
@@ -32,7 +32,8 @@ class AccountingActStore:
|
||||
Phase 1 provides accounting storage and read-only Tally discovery.
|
||||
Phase 2 adds durable client/registration -> Tally company mapping keyed by
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -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_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_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 <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,10 +9,13 @@
|
||||
<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 · Accounting</p>
|
||||
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Tally Accounting Masters</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>
|
||||
<h1 class="mt-1 text-2xl font-semibold text-slate-900">Tally Accounting</h1>
|
||||
<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 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>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
{% if initialized %}
|
||||
|
||||
@@ -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)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@router.get("/depreciation")
|
||||
def depreciation_it_tool(
|
||||
request: Request,
|
||||
client_id: int | None = None,
|
||||
tally_guid: str = "",
|
||||
fy_start: str = "",
|
||||
fy_end: str = "",
|
||||
calculated: int = 0,
|
||||
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.sync_masters": "Synchronize Tally Accounting Masters",
|
||||
"accounting.tally.sync_transactions": "Synchronize Tally Accounting Transactions",
|
||||
"accounting.tally.depreciation_it": "Calculate Income-tax Depreciation",
|
||||
"accounting.act.initialize": "Initialize Client Accounting ACT Storage",
|
||||
|
||||
"notice_cases.view": "View Notice and Case Management",
|
||||
@@ -183,3 +184,4 @@ def expand_permission_codes(code: str) -> list[str]:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import io
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
ERP_LOCAL_AGENT_VERSION = "1.5.0"
|
||||
ERP_LOCAL_AGENT_VERSION = "1.6.0"
|
||||
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
||||
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
||||
_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.
|
||||
|
||||
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"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
from typing import Sequence
|
||||
|
||||
|
||||
SCHEMA_VERSION = "4"
|
||||
SCHEMA_VERSION = "5"
|
||||
|
||||
MASTER_TABLES = {
|
||||
"groups": "tally_groups",
|
||||
@@ -31,8 +31,9 @@ class LocalAccountingStore:
|
||||
|
||||
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
|
||||
ledger, inventory, bill, cost-centre and bank allocations. Existing .act files
|
||||
are upgraded in place.
|
||||
ledger, inventory, bill, cost-centre and bank allocations. Phase 5 adds
|
||||
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):
|
||||
@@ -244,6 +245,35 @@ class LocalAccountingStore:
|
||||
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 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])
|
||||
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:
|
||||
path = self.db_path(client_id)
|
||||
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_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_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)}
|
||||
mapped = []
|
||||
for row in mappings:
|
||||
@@ -565,4 +749,5 @@ class LocalAccountingStore:
|
||||
"mappings": mapped,
|
||||
"latest_master_sync": latest_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
|
||||
ok = False
|
||||
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)
|
||||
elif action == "accounting_initialize":
|
||||
result = self._initialize(payload)
|
||||
@@ -35,6 +35,12 @@ class AgentCommandProcessor:
|
||||
result = self._sync_masters(payload)
|
||||
elif action == "accounting_sync_transactions":
|
||||
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:
|
||||
raise ValueError(f"Unsupported local-agent command: {action}")
|
||||
ok = True
|
||||
@@ -49,6 +55,7 @@ class AgentCommandProcessor:
|
||||
"tally_capability": True, "accounting_act_capability": True,
|
||||
"tally_mapping_capability": True, "tally_master_sync_capability": True,
|
||||
"tally_transaction_sync_capability": True,
|
||||
"it_depreciation_capability": True,
|
||||
}
|
||||
|
||||
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"))
|
||||
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()}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user