Add financial year selector to Cash Payment Review
This commit is contained in:
@@ -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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user