Move existing accounting writebacks to SQLite .NET batches

This commit is contained in:
A R R R Associates
2026-09-06 22:45:18 +05:30
parent bb9d4f7f47
commit a4a749f014
12 changed files with 1129 additions and 78 deletions
+106 -1
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
from datetime import date, datetime
from decimal import Decimal, InvalidOperation
from urllib.parse import quote
import hashlib
import uuid
from fastapi import APIRouter, Request
@@ -635,7 +636,8 @@ def cash_payment_allocation(
saved=False,
command_error=error or "",
allocation_plan=None,
allocation_input={"total_amount": "", "party_name": "", "expense_ledger": "", "source_payments": ""},
allocation_input={"total_amount": "", "party_name": "", "expense_ledger": "", "source_payments": "", "cash_ledger": "Cash"},
posting_result=None,
page_mode="allocation",
)
finally:
@@ -762,8 +764,111 @@ async def cash_payment_allocation_plan(request: Request):
"party_name": party_name,
"expense_ledger": expense_ledger,
"source_payments": source_payments,
"cash_ledger": str(form.get("cash_ledger") or "Cash"),
},
posting_result=None,
page_mode="allocation",
)
finally:
db.close()
@router.post("/allocation/post")
async def cash_payment_allocation_post(request: Request):
form = await request.form()
validate_csrf(request, str(form.get("csrf_token") or ""))
db = CommonSessionLocal()
try:
user, response = _require_partner(request, db, "accounting.tally.writeback")
if response:
return response
clients, scope = _visible_clients(db, request, user)
client_id = int(form.get("client_id") or 0)
selected_client = next((row for row in clients if int(row.id) == client_id), None)
if not selected_client:
return _denied()
setting = _setting(db, _tenant_id(request, user))
total_amount_text = str(form.get("total_amount") or "")
party_name = str(form.get("party_name") or "").strip()
expense_ledger = str(form.get("expense_ledger") or "").strip()
cash_ledger = str(form.get("cash_ledger") or "Cash").strip()
source_payments = str(form.get("source_payments") or "")
total_amount = _parse_money(total_amount_text, "Total cash payment")
plan = _allocation_plan(source_payments, total_amount, float(setting.cash_limit), int(setting.max_allocation_days))
if plan.get("status") != "ready":
raise ValueError("The source-date allocation must pass validation before posting to Tally.")
if not expense_ledger:
raise ValueError("Expense / purchase ledger is required for Tally posting.")
if not cash_ledger:
raise ValueError("Cash ledger is required for Tally posting.")
if str(form.get("confirm_posting") or "") != "yes" or str(form.get("confirm_text") or "").strip().upper() != "POST":
raise ValueError("Confirm the reviewed allocation and type POST exactly before Tally write-back.")
first_date = date.fromisoformat(str(plan["entries"][0]["date"]))
source_material = "|".join([str(client_id), expense_ledger, cash_ledger, party_name, source_payments, f"{total_amount:.2f}"])
source_key = "CASHALLOC-" + hashlib.sha256(source_material.encode("utf-8")).hexdigest()[:20].upper()
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
if not node or not _node_online(node):
raise ValueError("ERP Local Agent is offline for the active branch.")
result = request_agent_command(
node.node_code,
"accounting_post_cash_allocation",
{
**_accounting_storage_payload(selected_client, _financial_year_for_date(first_date)),
"client_id": int(selected_client.id),
"source_key": source_key,
"party_name": party_name,
"debit_ledger": expense_ledger,
"cash_ledger": cash_ledger,
"entries": list(plan.get("entries") or []),
"posted_by_user_id": int(user.id),
"narration": f"Cash payment allocation · {party_name or expense_ledger}",
},
timeout_seconds=180,
)
if not result.get("ok"):
raise ValueError(str(result.get("error") or "Cash allocation batch posting failed."))
body = result.get("result") or {}
return _render(
request, db, user, title="Cash Payment Compliance Review",
clients=clients, selected_client=selected_client, storage_node=node,
agent_online=_node_online(node), live_result=None, selected_tally_guid="",
date_from="", date_to="", setting=setting, review=None, cache_job=None,
active_job_id="", saved=False, command_error="", allocation_plan=plan,
allocation_input={
"total_amount": total_amount_text, "party_name": party_name,
"expense_ledger": expense_ledger, "source_payments": source_payments,
"cash_ledger": cash_ledger,
},
posting_result=body, page_mode="allocation",
)
except Exception as exc:
clients, scope = _visible_clients(db, request, user) if 'user' in locals() else ([], None)
selected_client = next((row for row in clients if int(row.id) == int(form.get("client_id") or 0)), None) if clients else None
setting = _setting(db, _tenant_id(request, user)) if 'user' in locals() else None
plan = None
try:
total_amount_text = str(form.get("total_amount") or "")
plan = _allocation_plan(str(form.get("source_payments") or ""), _parse_money(total_amount_text, "Total cash payment"), float(setting.cash_limit), int(setting.max_allocation_days)) if setting else None
except Exception:
pass
if 'user' not in locals():
raise
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id) if scope else None
return _render(
request, db, user, title="Cash Payment Compliance Review", clients=clients,
selected_client=selected_client, storage_node=node, agent_online=_node_online(node),
live_result=None, selected_tally_guid="", date_from="", date_to="", setting=setting,
review=None, cache_job=None, active_job_id="", saved=False, command_error=str(exc),
allocation_plan=plan,
allocation_input={
"total_amount": str(form.get("total_amount") or ""),
"party_name": str(form.get("party_name") or ""),
"expense_ledger": str(form.get("expense_ledger") or ""),
"source_payments": str(form.get("source_payments") or ""),
"cash_ledger": str(form.get("cash_ledger") or "Cash"),
},
posting_result=None, page_mode="allocation",
)
finally:
db.close()
+4 -1
View File
@@ -763,13 +763,16 @@ async def apply_corrections(
if not node or not _node_online(node):
raise ValueError("ERP Local Agent is offline for the active branch.")
selected_financial_year = str(form.get("financial_year") or "").strip()
response = request_agent_command(
node.node_code,
"accounting_opening_balance_apply",
{
**_accounting_storage_payload(client),
**_accounting_storage_payload(client, selected_financial_year or None),
"current_company_name": run.current_company_name,
"current_company_guid": run.current_company_guid,
"source_key": f"OPENING-RUN-{run.id}",
"requested_by_user_id": int(user.id),
"ledgers": ledger_rows,
"stock_items": stock_rows,
},
+1 -1
View File
@@ -204,7 +204,7 @@ async def post_liability(request:Request,instruction_id:int):
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
if not node or not _node_online(node): raise ValueError("Local Agent is offline.")
voucher_date=str(form.get("voucher_date") or (row.source_voucher_date.isoformat() if row.source_voucher_date else date.today().isoformat()))
result=request_agent_command(node.node_code,"accounting_post_tds_liability",{**_accounting_storage_payload(client,_financial_year_for_date(date.fromisoformat(voucher_date))),"tally_guid":str(form.get("tally_guid") or row.company_guid or ""),"voucher_date":voucher_date,"party_ledger":row.party_ledger,"tds_ledger":str(form.get("tds_ledger") or row.tds_ledger),"amount":row.liability_amount,"reference":row.erp_reference,"narration":row.narration or f"TDS liability {row.erp_reference}"},timeout_seconds=60)
result=request_agent_command(node.node_code,"accounting_post_tds_liability",{**_accounting_storage_payload(client,_financial_year_for_date(date.fromisoformat(voucher_date))),"tally_guid":str(form.get("tally_guid") or row.company_guid or ""),"voucher_date":voucher_date,"party_ledger":row.party_ledger,"tds_ledger":str(form.get("tds_ledger") or row.tds_ledger),"amount":row.liability_amount,"reference":row.erp_reference,"narration":row.narration or f"TDS liability {row.erp_reference}","posted_by_user_id":int(user.id)},timeout_seconds=120)
if not result.get("ok"): raise ValueError(str(result.get("error") or "Tally posting failed."))
rr=result.get("result") or {}; row.status="posted"; row.tds_ledger=str(form.get("tds_ledger") or row.tds_ledger); row.posted_by_user_id=user.id; row.posted_at_utc=datetime.utcnow(); row.tally_voucher_id=str(rr.get("last_voucher_id") or rr.get("voucher_id") or "") or None; db.commit()
return RedirectResponse(f"/tools/accounting/tds/liability?client_id={row.client_id}&saved=1",status_code=303)
@@ -141,6 +141,32 @@
<div class="mt-3 text-sm text-slate-700">Expected total: ₹{{ '%.2f'|format(allocation_plan.expected_total or 0) }} · Source total: ₹{{ '%.2f'|format(allocation_plan.supplied_total or 0) }} · Date span: {{ allocation_plan.span_days or 0 }} day(s) / {{ allocation_plan.max_allocation_days or setting.max_allocation_days }} allowed.</div>
{% if allocation_plan.entries %}<table class="mt-3 min-w-full text-sm"><thead><tr><th class="p-2 text-left">Actual Date</th><th class="p-2 text-left">Proposed Entry Amount</th></tr></thead><tbody>{% for row in allocation_plan.entries %}<tr class="border-t border-slate-200"><td class="p-2">{{ row.date }}</td><td class="p-2">₹{{ '%.2f'|format(row.amount) }}</td></tr>{% endfor %}</tbody></table>{% endif %}
<p class="mt-3 text-xs text-slate-600">{{ allocation_plan.note or 'No accounting dates are generated automatically.' }}</p>
{% if allocation_plan.status == 'ready' %}
<form method="post" action="/tools/accounting/cash-payments/allocation/post" class="mt-4 rounded-xl border border-slate-200 bg-white p-4">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="client_id" value="{{ selected_client.id }}">
<input type="hidden" name="total_amount" value="{{ allocation_input.total_amount }}">
<input type="hidden" name="party_name" value="{{ allocation_input.party_name }}">
<input type="hidden" name="expense_ledger" value="{{ allocation_input.expense_ledger }}">
<input type="hidden" name="source_payments" value="{{ allocation_input.source_payments }}">
<div class="grid gap-4 md:grid-cols-2">
<div><label class="text-xs font-semibold uppercase text-slate-500">Debit ledger</label><input value="{{ allocation_input.expense_ledger }}" disabled class="mt-1 w-full rounded-xl border-slate-300 bg-slate-50 text-sm"><p class="mt-1 text-xs text-slate-500">The reviewed expense / purchase ledger is debited.</p></div>
<div><label class="text-xs font-semibold uppercase text-slate-500">Cash ledger</label><input name="cash_ledger" value="{{ allocation_input.cash_ledger or 'Cash' }}" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required></div>
</div>
<div class="mt-4 flex flex-wrap items-center gap-3">
<label class="flex items-center gap-2 text-sm"><input type="checkbox" name="confirm_posting" value="yes" required> I reviewed the actual source dates and amounts.</label>
<input name="confirm_text" placeholder="Type POST" class="rounded-lg border border-slate-300 px-3 py-2 text-sm" required>
<button class="rounded-lg bg-brand-600 px-4 py-2 text-sm font-semibold text-white hover:bg-brand-700">Post Split Entries to Tally</button>
</div>
<p class="mt-2 text-xs text-slate-500">The entries are first stored in this client's local .act write queue, then transferred to Tally in .NET batches and read back for verification.</p>
</form>
{% endif %}
</div>
{% endif %}
{% if posting_result %}
<div class="mt-5 rounded-xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-900">
<div class="font-semibold">Cash payment split batch #{{ posting_result.batch_id }} posted and verified.</div>
<div class="mt-1">{{ posting_result.batch.batch.verified_count }} voucher(s) verified in TallyPrime.</div>
</div>
{% endif %}
</section>
@@ -7,20 +7,20 @@
<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">Depreciation (Income Tax)</h1>
<p class="mt-1 text-sm text-slate-500">Calculate Income-tax depreciation from the client SQLite Accounting Mirror. Approved drafts retain the existing controlled Tally write-back.</p>
<p class="mt-1 text-sm text-slate-500">Calculate Income-tax depreciation from the client SQLite Accounting Mirror. Approved drafts are stored locally and posted through the shared .NET batch writer with Tally read-back verification.</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. It still requires explicit Partner approval before Tally write-back.</div>{% endif %}
{% if approved %}<div class="rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm font-medium text-amber-900">Depreciation draft approved. No Tally voucher has been created yet.</div>{% endif %}
{% if posted %}<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">Approved depreciation Journal posted to TallyPrime successfully.</div>{% endif %}
{% if posted %}<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">Approved depreciation Journal posted through the local SQLite queue and verified in TallyPrime.</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>
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">SQLite Mirror</div><div class="mt-2 text-lg font-semibold {{ 'text-emerald-700' if mirror_effective_ready else 'text-amber-700' }}">{{ 'Ready' if mirror_effective_ready else 'Mirror required' }}</div>{% if mirror_effective_ready and mirror.company %}<div class="mt-1 text-xs text-slate-500">{{ mirror.company.company_name }} · {{ fy_start }} to {{ fy_end }}</div>{% elif mirror.financial_year_reason %}<div class="mt-1 text-xs text-amber-700">{{ mirror.financial_year_reason }}</div>{% endif %}</div>
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Write-back</div><div class="mt-2 text-lg font-semibold text-slate-700">Controlled</div><div class="mt-1 text-xs text-slate-500">Tally is contacted only after approval/posting.</div></div>
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Write-back</div><div class="mt-2 text-lg font-semibold text-slate-700">Controlled</div><div class="mt-1 text-xs text-slate-500">Approved instructions are persisted in the local .act before .NET batch posting and read-back verification.</div></div>
</section>
<section class="rounded-2xl bg-white p-5 shadow-soft">
@@ -244,7 +244,7 @@
<p class="mt-1 text-sm text-red-800">Existing safeguards are unchanged: current openings are rechecked immediately before write and every altered master is re-read from Tally afterward.</p>
<div class="mt-3 flex flex-wrap gap-3">
<input name="confirm_text" required placeholder="APPLY OPENING BALANCES" class="min-w-[280px] rounded-lg border border-red-300 bg-white px-3 py-2 text-sm">
<button class="rounded-lg bg-red-700 px-4 py-2 text-sm font-semibold text-white">Apply Selected Changes</button>
<button class="rounded-lg bg-red-700 px-4 py-2 text-sm font-semibold text-white">Apply Selected Changes via .NET Batch</button>
</div>
</section>
{% endif %}
@@ -281,6 +281,7 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="client_id" value="{{ selected_client.id }}">
<input type="hidden" name="run_id" value="{{ selected_run.id }}">
<input type="hidden" name="financial_year" value="{{ current_fy }}">
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead><tr class="bg-slate-50 text-left text-xs uppercase text-slate-500"><th class="px-3 py-2">Apply</th><th class="px-3 py-2">Previous Ledger</th><th class="px-3 py-2 text-right">PY Closing</th><th class="px-3 py-2">Current Ledger</th><th class="px-3 py-2 text-right">CY Opening</th><th class="px-3 py-2 text-right">Difference</th><th class="px-3 py-2">Status</th><th class="px-3 py-2">Correction</th></tr></thead>
@@ -304,7 +305,7 @@
<div class="border-t border-slate-200 p-4">
<div class="flex flex-wrap gap-3">
<input name="confirm_text" required placeholder="APPLY OPENING BALANCES" class="min-w-[280px] rounded-lg border border-red-300 px-3 py-2 text-sm">
<button class="rounded-lg bg-red-700 px-4 py-2 text-sm font-semibold text-white">Apply Selected Changes</button>
<button class="rounded-lg bg-red-700 px-4 py-2 text-sm font-semibold text-white">Apply Selected Changes via .NET Batch</button>
</div>
</div>
{% endif %}
@@ -352,6 +353,7 @@
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="client_id" value="{{ selected_client.id }}">
<input type="hidden" name="run_id" value="{{ selected_run.id }}">
<input type="hidden" name="financial_year" value="{{ current_fy }}">
<div class="overflow-x-auto">
<table class="min-w-full text-xs">
<thead><tr class="bg-slate-50 text-left uppercase text-slate-500"><th class="px-3 py-2">Apply</th><th class="px-3 py-2">Previous Stock Item</th><th class="px-3 py-2">PY Closing</th><th class="px-3 py-2">Current Stock Item</th><th class="px-3 py-2">CY Opening</th><th class="px-3 py-2">Difference</th><th class="px-3 py-2">Status</th><th class="px-3 py-2">Correction</th></tr></thead>
@@ -375,7 +377,7 @@
<div class="border-t border-slate-200 p-4">
<div class="flex flex-wrap gap-3">
<input name="confirm_text" required placeholder="APPLY OPENING BALANCES" class="min-w-[280px] rounded-lg border border-red-300 px-3 py-2 text-sm">
<button class="rounded-lg bg-red-700 px-4 py-2 text-sm font-semibold text-white">Apply Selected Changes</button>
<button class="rounded-lg bg-red-700 px-4 py-2 text-sm font-semibold text-white">Apply Selected Changes via .NET Batch</button>
</div>
</div>
{% endif %}