Add financial year selector to Cash Payment Review

This commit is contained in:
A R R R Associates
2026-09-09 14:54:16 +05:30
parent e3f012d302
commit 7f372330a5
2 changed files with 93 additions and 12 deletions
+60 -8
View File
@@ -4,6 +4,7 @@ from datetime import date, datetime
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from urllib.parse import quote from urllib.parse import quote
import uuid import uuid
import re
from fastapi import APIRouter, Request from fastapi import APIRouter, Request
from fastapi.responses import RedirectResponse, JSONResponse from fastapi.responses import RedirectResponse, JSONResponse
@@ -148,6 +149,30 @@ def _review_financial_year(date_from: str, date_to: str, fallback_today: date) -
return fy, parsed_from, parsed_to return fy, parsed_from, parsed_to
def _financial_year_bounds(financial_year: str) -> tuple[date, date]:
text = str(financial_year or "").strip()
match = re.fullmatch(r"(\d{4})-(\d{2})", text)
if not match:
raise ValueError("Invalid financial year.")
start_year = int(match.group(1))
expected_suffix = str((start_year + 1) % 100).zfill(2)
if match.group(2) != expected_suffix:
raise ValueError("Invalid financial year.")
return date(start_year, 4, 1), date(start_year + 1, 3, 31)
def _financial_year_options(selected_fy: str, today: date) -> list[str]:
current = _financial_year_for_date(today)
current_start = int(current.split("-", 1)[0])
selected_start = int(str(selected_fy).split("-", 1)[0]) if selected_fy else current_start
newest = max(current_start, selected_start)
oldest = min(current_start - 7, selected_start)
return [
f"{year}-{str((year + 1) % 100).zfill(2)}"
for year in range(newest, oldest - 1, -1)
]
def _render(request: Request, db, user, **context): def _render(request: Request, db, user, **context):
base = { base = {
"request": request, "request": request,
@@ -170,6 +195,7 @@ def cash_payment_review(
tally_guid: str = "", tally_guid: str = "",
date_from: str = "", date_from: str = "",
date_to: str = "", date_to: str = "",
financial_year: str = "",
job_id: str = "", job_id: str = "",
cash_ledger_name: str = "", cash_ledger_name: str = "",
analyze: int = 0, analyze: int = 0,
@@ -196,16 +222,35 @@ def cash_payment_review(
today = date.today() today = date.today()
# Default period still follows the current date when the user first opens # Financial Year is now an explicit review selector. This avoids any
# the page, but once a date range is supplied the mirror is resolved # ambiguity between the ERP workspace FY and the local mirror FY.
# strictly from that review period rather than from the workspace FY.
default_fy = _financial_year_for_date(today) default_fy = _financial_year_for_date(today)
default_start_year = int(default_fy.split("-", 1)[0]) requested_fy = str(financial_year or "").strip()
start_text = date_from or date(default_start_year, 4, 1).isoformat() try:
end_text = date_to or today.isoformat() review_fy = requested_fy if requested_fy else (
review_fy, parsed_from, parsed_to = _review_financial_year(start_text, end_text, today) _financial_year_for_date(_parse_review_date(date_from, today))
if date_from else default_fy
)
fy_start, fy_end = _financial_year_bounds(review_fy)
except Exception:
review_fy = default_fy
fy_start, fy_end = _financial_year_bounds(review_fy)
parsed_from = _parse_review_date(date_from, fy_start) if date_from else fy_start
parsed_to = _parse_review_date(date_to, fy_end) if date_to else fy_end
# Keep manual sub-period review possible, but never allow a reversed
# range such as 01/04/2025 -> 31/03/2025.
if parsed_from < fy_start or parsed_from > fy_end:
parsed_from = fy_start
if parsed_to < fy_start or parsed_to > fy_end:
parsed_to = fy_end
if parsed_to < parsed_from:
parsed_from, parsed_to = fy_start, fy_end
start_text = parsed_from.isoformat() start_text = parsed_from.isoformat()
end_text = parsed_to.isoformat() end_text = parsed_to.isoformat()
financial_year_options = _financial_year_options(review_fy, today)
review = None review = None
mirror_status = None mirror_status = None
@@ -294,6 +339,7 @@ def cash_payment_review(
allocation_input={"total_amount": "", "party_name": "", "expense_ledger": "", "source_payments": ""}, allocation_input={"total_amount": "", "party_name": "", "expense_ledger": "", "source_payments": ""},
page_mode="compliance", page_mode="compliance",
review_financial_year=review_fy, review_financial_year=review_fy,
financial_year_options=financial_year_options,
) )
finally: finally:
db.close() db.close()
@@ -737,7 +783,13 @@ async def save_cash_payment_settings(request: Request):
except Exception: except Exception:
db.rollback() db.rollback()
client_id = str(form.get("client_id") or "").strip() client_id = str(form.get("client_id") or "").strip()
suffix = f"&client_id={client_id}" if client_id else "" financial_year = str(form.get("financial_year") or "").strip()
parts = []
if client_id:
parts.append("client_id=" + quote(client_id))
if financial_year:
parts.append("financial_year=" + quote(financial_year))
suffix = ("&" + "&".join(parts)) if parts else ""
return RedirectResponse(url=f"/tools/accounting/cash-payments?saved=1{suffix}", status_code=303) return RedirectResponse(url=f"/tools/accounting/cash-payments?saved=1{suffix}", status_code=303)
finally: finally:
db.close() db.close()
@@ -33,7 +33,7 @@
{% endif %} {% endif %}
</div> </div>
<form method="get" action="/tools/accounting/cash-payments" class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-4"> <form id="cashPaymentReviewForm" method="get" action="/tools/accounting/cash-payments" class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-5">
<div> <div>
<label class="text-xs font-semibold uppercase text-slate-500">Client</label> <label class="text-xs font-semibold uppercase text-slate-500">Client</label>
<select name="client_id" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required onchange="this.form.submit()"> <select name="client_id" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required onchange="this.form.submit()">
@@ -41,8 +41,16 @@
{% for client in clients %}<option value="{{ client.id }}" {% if selected_client and selected_client.id == client.id %}selected{% endif %}>{{ client.client_name }}</option>{% endfor %} {% 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> </select>
</div> </div>
<div><label class="text-xs font-semibold uppercase text-slate-500">From</label><input type="date" name="date_from" value="{{ date_from }}" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required></div> <div>
<div><label class="text-xs font-semibold uppercase text-slate-500">To</label><input type="date" name="date_to" value="{{ date_to }}" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required></div> <label class="text-xs font-semibold uppercase text-slate-500">Financial Year</label>
<select id="cashPaymentFinancialYear" name="financial_year" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required>
{% for fy in financial_year_options %}
<option value="{{ fy }}" {% if fy == review_financial_year %}selected{% endif %}>FY {{ fy }}</option>
{% endfor %}
</select>
</div>
<div><label class="text-xs font-semibold uppercase text-slate-500">From</label><input id="cashPaymentDateFrom" type="date" name="date_from" value="{{ date_from }}" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required></div>
<div><label class="text-xs font-semibold uppercase text-slate-500">To</label><input id="cashPaymentDateTo" type="date" name="date_to" value="{{ date_to }}" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required></div>
<div class="flex items-end"> <div class="flex items-end">
<button type="submit" name="analyze" value="1" class="w-full rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-50" {% if not selected_client or not mirror.ready %}disabled{% endif %}>Review Cash Payments</button> <button type="submit" name="analyze" value="1" class="w-full rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-50" {% if not selected_client or not mirror.ready %}disabled{% endif %}>Review Cash Payments</button>
</div> </div>
@@ -79,7 +87,7 @@
<div class="rounded-2xl bg-white p-5 shadow-soft"> <div class="rounded-2xl bg-white p-5 shadow-soft">
<h2 class="text-lg font-semibold text-slate-900">Rule Settings</h2> <h2 class="text-lg font-semibold text-slate-900">Rule Settings</h2>
<form method="post" action="/tools/accounting/cash-payments/settings" class="mt-4 space-y-3"> <form method="post" action="/tools/accounting/cash-payments/settings" class="mt-4 space-y-3">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id if selected_client else '' }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}"><input type="hidden" name="client_id" value="{{ selected_client.id if selected_client else '' }}"><input type="hidden" name="financial_year" value="{{ review_financial_year }}">
<div><label class="text-xs font-semibold uppercase text-slate-500">Cash payment limit</label><input type="number" min="1" step="0.01" name="cash_limit" value="{{ '%.2f'|format(setting.cash_limit) }}" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required></div> <div><label class="text-xs font-semibold uppercase text-slate-500">Cash payment limit</label><input type="number" min="1" step="0.01" name="cash_limit" value="{{ '%.2f'|format(setting.cash_limit) }}" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required></div>
<div><label class="text-xs font-semibold uppercase text-slate-500">Effective from</label><input type="date" name="effective_from" value="{{ setting.effective_from.isoformat() }}" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required></div> <div><label class="text-xs font-semibold uppercase text-slate-500">Effective from</label><input type="date" name="effective_from" value="{{ setting.effective_from.isoformat() }}" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required></div>
<div><label class="text-xs font-semibold uppercase text-slate-500">Split review window (days)</label><input type="number" min="1" max="31" name="split_window_days" value="{{ setting.split_window_days }}" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required></div> <div><label class="text-xs font-semibold uppercase text-slate-500">Split review window (days)</label><input type="number" min="1" max="31" name="split_window_days" value="{{ setting.split_window_days }}" class="mt-1 w-full rounded-xl border-slate-300 text-sm" required></div>
@@ -148,4 +156,25 @@
</section> </section>
{% endif %} {% endif %}
</div> </div>
{% if page_mode != 'allocation' %}
<script>
(function () {
const fy = document.getElementById('cashPaymentFinancialYear');
const from = document.getElementById('cashPaymentDateFrom');
const to = document.getElementById('cashPaymentDateTo');
const form = document.getElementById('cashPaymentReviewForm');
if (!fy || !from || !to || !form) return;
fy.addEventListener('change', function () {
const m = /^([0-9]{4})-([0-9]{2})$/.exec(fy.value || '');
if (!m) return;
const y = Number(m[1]);
from.value = String(y).padStart(4, '0') + '-04-01';
to.value = String(y + 1).padStart(4, '0') + '-03-31';
form.submit();
});
})();
</script>
{% endif %}
{% endblock %} {% endblock %}