diff --git a/app/modules/accounting/templates/accounting/depreciation_it.html b/app/modules/accounting/templates/accounting/depreciation_it.html index 351cfaf..25edf15 100644 --- a/app/modules/accounting/templates/accounting/depreciation_it.html +++ b/app/modules/accounting/templates/accounting/depreciation_it.html @@ -18,7 +18,7 @@
ERP Local Agent
{{ 'Connected' if agent_online else 'Offline' }}
-
SQLite Mirror
{{ 'Ready' if mirror.ready_for_financial_year else 'Mirror required' }}
{{ mirror.company.company_name if mirror.ready_for_financial_year and mirror.company else '' }}
+
SQLite Mirror
{{ 'Ready' if mirror.ready_for_financial_year else 'Mirror required' }}
{% if mirror.ready_for_financial_year and mirror.company %}
{{ mirror.company.company_name }} · {{ fy_start }} to {{ fy_end }}
{% elif mirror.financial_year_reason %}
{{ mirror.financial_year_reason }}
{% endif %}
Write-back
Controlled
Tally is contacted only after approval/posting.
@@ -44,7 +44,7 @@
-
+

Any existing ledger may be selected, including the asset ledger itself.

These ledger selections are stored with the draft. Phase 6 can post the Journal only after the saved draft is explicitly approved.

@@ -60,7 +60,7 @@ -
+
SelectLedgerOpening WDVAdditions ≥180 daysAdditions <180 daysReductionsRate
{% for row in preview.fixed_asset_ledgers %} @@ -69,9 +69,11 @@ + + - {% else %}{% endfor %} + {% else %}{% endfor %}
SelectLedgerOpening WDVAdditions ≥180 daysAdditions <180 daysOther ReductionsDepreciation Already RecordedRatePosting Ledger
{{ '%.2f'|format(row.additions_full_rate) }} {{ '%.2f'|format(row.additions_half_rate) }} {{ '%.2f'|format(row.reductions) }}{% if row.depreciation_already_recorded %}
{{ '%.2f'|format(row.recorded_depreciation) }}
{{ row.recorded_depreciation_voucher_count }} voucher(s)
{% else %}—{% endif %}
No active Fixed Asset ledgers were found. Zero-opening ledgers without additions are intentionally omitted.
No active Fixed Asset ledgers were found. Zero-opening ledgers without additions are intentionally omitted.
@@ -94,7 +96,7 @@ {% if depreciation_run.tally_posting_error %}
{{ depreciation_run.tally_posting_error }}
{% endif %} -
{% for row in depreciation_run.lines %}{% endfor %}
LedgerRateDepreciationClosing WDV
{{ row.ledger_name }}{{ '%.2f'|format(row.rate_percent) }}%{{ '%.2f'|format(row.depreciation_amount) }}{{ '%.2f'|format(row.closing_wdv) }}
+
{% for row in depreciation_run.lines %}{% endfor %}
LedgerRateCalculatedAlready RecordedUnrecordedClosing WDV
{{ row.ledger_name }}{{ '%.2f'|format(row.rate_percent) }}%{{ '%.2f'|format(row.depreciation_amount) }}{{ '%.2f'|format(row.recorded_depreciation or 0) }}{{ '%.2f'|format(row.remaining_unrecorded_depreciation if row.remaining_unrecorded_depreciation is defined else row.depreciation_amount) }}{{ '%.2f'|format(row.closing_wdv) }}
{% if depreciation_run.status == 'draft' %}
@@ -155,4 +157,19 @@
{% for row in analysis_history %}{% endfor %}
RunFYPeriodCompleted
#{{ row.id }}{{ row.financial_year }}{{ row.period_from }} → {{ row.period_to }}{{ row.completed_at_utc }}Excel · PDF
{% endif %} + + + {% endblock %} diff --git a/app/modules/accounting/ui.py b/app/modules/accounting/ui.py index 955ff5c..0f7fa4c 100644 --- a/app/modules/accounting/ui.py +++ b/app/modules/accounting/ui.py @@ -810,9 +810,10 @@ def depreciation_it_tool( 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() + active_start_year = today.year if today.month >= 4 else today.year - 1 + explicit_period = bool(str(fy_start or "").strip() and str(fy_end or "").strip()) + start_text = fy_start or date(active_start_year, 4, 1).isoformat() + end_text = fy_end or date(active_start_year + 1, 3, 31).isoformat() preview = None depreciation_run = None mirror_status = None @@ -820,17 +821,30 @@ def depreciation_it_tool( analysis_history = [] if selected_client and node and online: - storage_payload = _accounting_storage_payload( - selected_client, _financial_year_for_date(date.fromisoformat(start_text)) - ) + selected_fy=_financial_year_for_date(date.fromisoformat(start_text)) + storage_payload=_accounting_storage_payload(selected_client,selected_fy) try: - status_response = request_agent_command( - node.node_code, "accounting_mirror_status", storage_payload, timeout_seconds=20 + status_response=request_agent_command( + node.node_code,"accounting_mirror_status",storage_payload,timeout_seconds=20 ) if status_response.get("ok"): - mirror_status = status_response.get("result") or {} + mirror_status=status_response.get("result") or {} else: - command_error = str(status_response.get("error") or "Accounting Mirror status failed.") + command_error=str(status_response.get("error") or "Accounting Mirror status failed.") + + if not explicit_period and not ((mirror_status or {}).get("mirror") or {}).get("ready_for_financial_year"): + prior_start_year=active_start_year-1 + prior_start=date(prior_start_year,4,1) + prior_end=date(prior_start_year+1,3,31) + prior_fy=_financial_year_for_date(prior_start) + prior_payload=_accounting_storage_payload(selected_client,prior_fy) + prior_response=request_agent_command( + node.node_code,"accounting_mirror_status",prior_payload,timeout_seconds=20 + ) + prior_status=(prior_response.get("result") or {}) if prior_response.get("ok") else {} + if ((prior_status.get("mirror") or {}).get("ready_for_financial_year")): + start_text=prior_start.isoformat(); end_text=prior_end.isoformat() + storage_payload=prior_payload; mirror_status=prior_status if mirror_status and (mirror_status.get("mirror") or {}).get("ready_for_financial_year"): preview_response = request_agent_command( diff --git a/app/modules/documents/agent_package.py b/app/modules/documents/agent_package.py index e82d6f1..a06b5e9 100644 --- a/app/modules/documents/agent_package.py +++ b/app/modules/documents/agent_package.py @@ -4,7 +4,7 @@ import io from pathlib import Path import zipfile -ERP_LOCAL_AGENT_VERSION = "1.25.5" +ERP_LOCAL_AGENT_VERSION = "1.25.6" 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) diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py b/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py index ea8d995..7508429 100644 --- a/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py +++ b/app/modules/documents/local_agent_runtime/erp_local_agent/__init__.py @@ -1,2 +1,2 @@ -__version__ = "1.25.5" +__version__ = "1.25.6" AGENT_NAME = "ERP Local Agent" diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/accounting_store.py b/app/modules/documents/local_agent_runtime/erp_local_agent/accounting_store.py index 43afbc6..6bef562 100644 --- a/app/modules/documents/local_agent_runtime/erp_local_agent/accounting_store.py +++ b/app/modules/documents/local_agent_runtime/erp_local_agent/accounting_store.py @@ -968,6 +968,61 @@ class LocalAccountingStore: "provisional": fy_end > today, } + @staticmethod + def _looks_like_depreciation_text(value: str) -> bool: + text = re.sub(r"[^a-z0-9]+", " ", str(value or "").casefold()).strip() + if not text: + return False + return any(token in text for token in ( + "depreciation","depn","depreciation expense","depreciation reserve", + "accumulated depreciation","provision for depreciation", + )) + + def _recorded_depreciation_for_asset(self, db: sqlite3.Connection, *, tally_guid: str, + asset_ledger_name: str, start_date: str, end_date: str) -> dict: + rows = db.execute( + """SELECT v.id AS voucher_id, + COALESCE(NULLIF(v.effective_date,''),v.voucher_date) AS movement_date, + v.voucher_type_name,v.voucher_number,v.reference,v.narration, + 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 lower(trim(COALESCE(e.ledger_name,'')))=lower(trim(?)) + AND COALESCE(NULLIF(v.effective_date,''),v.voucher_date)>=? + AND COALESCE(NULLIF(v.effective_date,''),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') + ORDER BY movement_date,v.id""", + (tally_guid,asset_ledger_name,start_date,end_date), + ).fetchall() + total=0.0; vouchers=[]; seen=set() + for row in rows: + if self._is_debit_entry(row): + continue + counter_rows=db.execute( + """SELECT ledger_name FROM tally_voucher_ledger_entries + WHERE voucher_id=? AND lower(trim(COALESCE(ledger_name,'')))<>lower(trim(?))""", + (int(row["voucher_id"]),asset_ledger_name), + ).fetchall() + counters=[str(x["ledger_name"] or "").strip() for x in counter_rows] + text=" ".join([str(row["voucher_type_name"] or ""),str(row["reference"] or ""), + str(row["narration"] or ""),*counters]) + if not self._looks_like_depreciation_text(text): + continue + total += abs(float(row["amount"] or 0)) + vid=int(row["voucher_id"]) + if vid not in seen: + seen.add(vid) + vouchers.append({ + "voucher_id":vid,"date":str(row["movement_date"] or ""), + "voucher_type":str(row["voucher_type_name"] or ""), + "voucher_number":str(row["voucher_number"] or ""), + "reference":str(row["reference"] or ""), + "counter_ledgers":counters, + }) + return {"amount":round(total,2),"voucher_count":len(vouchers),"vouchers":vouchers,"already_recorded":bool(total>0)} + 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.") @@ -1003,13 +1058,20 @@ class LocalAccountingStore: AND lower(COALESCE(v.is_optional,'')) NOT IN ('yes','true','1')""", (tally_guid,name,start.isoformat(),movement_end.isoformat()), ).fetchall() - full=half=reductions=0.0 + recorded_dep=self._recorded_depreciation_for_asset( + db,tally_guid=tally_guid,asset_ledger_name=name, + start_date=start.isoformat(),end_date=movement_end.isoformat() + ) + recorded_dep_amount=float(recorded_dep.get("amount") or 0) + full=half=0.0; raw_credit_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["movement_date"] or ""), end): half += amt else: full += amt - else: reductions += amt + else: + raw_credit_reductions += amt + reductions=max(0.0,raw_credit_reductions-recorded_dep_amount) opening_wdv = abs(float(row["opening_balance"] or 0)) tally_closing = abs(float(row["closing_balance"] or 0)) additions_total = round(full + half, 2) @@ -1024,6 +1086,10 @@ class LocalAccountingStore: "ledger_key":key,"ledger_guid":str(row["master_guid"] or ""),"ledger_name":name,"parent_group":str(row["parent"] or ""), "opening_wdv":opening_wdv,"tally_closing_balance":tally_closing, "additions_full_rate":round(full,2),"additions_half_rate":round(half,2),"reductions":round(reductions,2), + "recorded_depreciation":round(recorded_dep_amount,2), + "depreciation_already_recorded":bool(recorded_dep.get("already_recorded")), + "recorded_depreciation_voucher_count":int(recorded_dep.get("voucher_count") or 0), + "recorded_depreciation_vouchers":list(recorded_dep.get("vouchers") or []), "rate_percent":rules.get(key,0.0), "movement_entries":len(entries), }) @@ -1050,7 +1116,12 @@ class LocalAccountingStore: 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)} + recorded_dep=float(row.get("recorded_depreciation") or 0) + line={**row,"rate_percent":rate,"depreciation_amount":dep, + "recorded_depreciation":round(recorded_dep,2), + "depreciation_already_recorded":bool(recorded_dep>0), + "remaining_unrecorded_depreciation":round(max(0.0,dep-recorded_dep),2), + "closing_wdv":closing,"closing_before_depreciation":round(closing_before,2)} lines.append(line) mapping=preview["mapping"] with self.connect(client_id) as db: @@ -1067,7 +1138,19 @@ class LocalAccountingStore: 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} + total_recorded=round(sum(float(x.get("recorded_depreciation") or 0) for x in lines),2) + total_remaining=round(sum(float(x.get("remaining_unrecorded_depreciation") or 0) for x in lines),2) + 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), + "total_recorded_depreciation":total_recorded, + "total_remaining_unrecorded_depreciation":total_remaining, + "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: diff --git a/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py b/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py index b3efacd..0df974c 100644 --- a/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py +++ b/app/modules/documents/local_agent_runtime/erp_local_agent/commands.py @@ -3164,22 +3164,27 @@ class AgentCommandProcessor: if not value: return False exact = { - "capital account", - "capital accounts", - "partners capital", - "partner capital", - "partners capital accounts", - "partner capital accounts", - "proprietors capital", - "proprietor capital", - "owners capital", - "owner capital", - "share capital", - "reserves surplus", - "reserves and surplus", + "capital account","capital accounts","partners capital","partner capital", + "partners capital accounts","partner capital accounts","proprietors capital", + "proprietor capital","owners capital","owner capital","share capital", + "reserves surplus","reserves and surplus","partners current account", + "partner current account","partners current accounts","partner current accounts", + "proprietors current account","proprietor current account", + "owners current account","owner current account", } if value in exact: return True + owner_prefix = ( + value.startswith("partner ") or value.startswith("partners ") + or value.startswith("proprietor ") or value.startswith("proprietors ") + or value.startswith("owner ") or value.startswith("owners ") + ) + current_account_like = ( + " current account" in value or " current accounts" in value + or " current a c" in value or value.endswith(" current ac") + ) + if owner_prefix and current_account_like: + return True return ( value.startswith("capital account ") or value.endswith(" capital account")