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 urllib.parse import quote
import uuid
import re
from fastapi import APIRouter, Request
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
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):
base = {
"request": request,
@@ -170,6 +195,7 @@ def cash_payment_review(
tally_guid: str = "",
date_from: str = "",
date_to: str = "",
financial_year: str = "",
job_id: str = "",
cash_ledger_name: str = "",
analyze: int = 0,
@@ -196,16 +222,35 @@ def cash_payment_review(
today = date.today()
# 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.
# Financial Year is now an explicit review selector. This avoids any
# ambiguity between the ERP workspace FY and the local mirror 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)
requested_fy = str(financial_year or "").strip()
try:
review_fy = requested_fy if requested_fy else (
_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()
end_text = parsed_to.isoformat()
financial_year_options = _financial_year_options(review_fy, today)
review = None
mirror_status = None
@@ -294,6 +339,7 @@ def cash_payment_review(
allocation_input={"total_amount": "", "party_name": "", "expense_ledger": "", "source_payments": ""},
page_mode="compliance",
review_financial_year=review_fy,
financial_year_options=financial_year_options,
)
finally:
db.close()
@@ -737,7 +783,13 @@ async def save_cash_payment_settings(request: Request):
except Exception:
db.rollback()
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)
finally:
db.close()
@@ -33,7 +33,7 @@
{% endif %}
</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>
<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()">
@@ -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 %}
</select>
</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><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>
<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">
<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>
@@ -79,7 +87,7 @@
<div class="rounded-2xl bg-white p-5 shadow-soft">
<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">
<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">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>
@@ -148,4 +156,25 @@
</section>
{% endif %}
</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 %}