Fix Cash Payment FY-specific Accounting Mirror resolution

This commit is contained in:
A R R R Associates
2026-09-09 13:57:01 +05:30
parent 4ef6b1fc5d
commit e3f012d302
2 changed files with 42 additions and 140 deletions
+36 -110
View File
@@ -3,7 +3,6 @@ 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
@@ -126,6 +125,29 @@ def _allocation_plan(source_text: str, expected_total: float, cash_limit: float,
}
def _parse_review_date(value: str, fallback: date) -> date:
text = str(value or "").strip()
if not text:
return fallback
try:
return date.fromisoformat(text)
except Exception:
return fallback
def _review_financial_year(date_from: str, date_to: str, fallback_today: date) -> tuple[str, date, date]:
"""Resolve Cash Payment review FY from the requested review period.
The review must follow the period selected by the user, not the ERP workspace FY.
"""
parsed_from = _parse_review_date(date_from, fallback_today)
parsed_to = _parse_review_date(date_to, parsed_from)
if parsed_to < parsed_from:
parsed_to = parsed_from
fy = _financial_year_for_date(parsed_from)
return fy, parsed_from, parsed_to
def _render(request: Request, db, user, **context):
base = {
"request": request,
@@ -173,10 +195,17 @@ def cash_payment_review(
online = _node_online(node)
today = date.today()
fy = _financial_year_for_date(today)
fy_start_year = int(fy.split("-", 1)[0])
start_text = date_from or date(fy_start_year, 4, 1).isoformat()
# Default period still follows the current date when the user first opens
# the page, but once a date range is supplied the mirror is resolved
# strictly from that review period rather than from the workspace FY.
default_fy = _financial_year_for_date(today)
default_start_year = int(default_fy.split("-", 1)[0])
start_text = date_from or date(default_start_year, 4, 1).isoformat()
end_text = date_to or today.isoformat()
review_fy, parsed_from, parsed_to = _review_financial_year(start_text, end_text, today)
start_text = parsed_from.isoformat()
end_text = parsed_to.isoformat()
review = None
mirror_status = None
@@ -191,7 +220,7 @@ def cash_payment_review(
else:
storage_payload = _accounting_storage_payload(
selected_client,
_financial_year_for_date(date.fromisoformat(start_text)),
review_fy,
)
try:
status_response = request_agent_command(
@@ -264,6 +293,7 @@ def cash_payment_review(
allocation_plan=None,
allocation_input={"total_amount": "", "party_name": "", "expense_ledger": "", "source_payments": ""},
page_mode="compliance",
review_financial_year=review_fy,
)
finally:
db.close()
@@ -636,8 +666,7 @@ def cash_payment_allocation(
saved=False,
command_error=error or "",
allocation_plan=None,
allocation_input={"total_amount": "", "party_name": "", "expense_ledger": "", "source_payments": "", "cash_ledger": "Cash"},
posting_result=None,
allocation_input={"total_amount": "", "party_name": "", "expense_ledger": "", "source_payments": ""},
page_mode="allocation",
)
finally:
@@ -764,111 +793,8 @@ 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()