diff --git a/app/modules/accounting/cash_payment_ui.py b/app/modules/accounting/cash_payment_ui.py index a1fd43d..ce21749 100644 --- a/app/modules/accounting/cash_payment_ui.py +++ b/app/modules/accounting/cash_payment_ui.py @@ -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() diff --git a/app/modules/accounting/templates/accounting/cash_payment_review.html b/app/modules/accounting/templates/accounting/cash_payment_review.html index c2cf287..d58d65e 100644 --- a/app/modules/accounting/templates/accounting/cash_payment_review.html +++ b/app/modules/accounting/templates/accounting/cash_payment_review.html @@ -57,6 +57,7 @@
Source: {{ mirror.path or mirror_status.mirror_db_path or '' }}
{% else %} -| Actual Date | Proposed Entry Amount |
|---|---|
| {{ row.date }} | ₹{{ '%.2f'|format(row.amount) }} |
{{ allocation_plan.note or 'No accounting dates are generated automatically.' }}
- {% if allocation_plan.status == 'ready' %} - - {% endif %} -