Add previous FY mirror selection and force fresh TDS review
This commit is contained in:
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from urllib.parse import quote
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
@@ -139,13 +140,22 @@ def tds_root(request: Request, client_id: int | None = None):
|
||||
|
||||
|
||||
@router.get("/compliance")
|
||||
def compliance(request:Request,client_id:int|None=None,tally_guid:str="",date_from:str="",date_to:str="",run:int=0):
|
||||
def compliance(request:Request,client_id:int|None=None,tally_guid:str="",date_from:str="",date_to:str="",run:int=0,run_token:str=""):
|
||||
db=CommonSessionLocal()
|
||||
try:
|
||||
user,response=_require_partner(request,db,"accounting.tally.view")
|
||||
if response:return response
|
||||
clients,selected,node,online,live,review,start,end,error,history=_workspace(request,db,user,client_id,tally_guid,date_from,date_to,run)
|
||||
return _render(request,db,user,"modules/accounting/templates/accounting/tds_compliance.html",clients=clients,selected_client=selected,node=node,node_online=online,live=live,review=review,date_from=start,date_to=end,tally_guid=tally_guid,error=error,rules=_rules(db,_tenant_id(request,user)),history=history)
|
||||
response=_render(
|
||||
request,db,user,"modules/accounting/templates/accounting/tds_compliance.html",
|
||||
clients=clients,selected_client=selected,node=node,node_online=online,live=live,
|
||||
review=review,date_from=start,date_to=end,tally_guid=tally_guid,error=error,
|
||||
rules=_rules(db,_tenant_id(request,user)),history=history,
|
||||
run_token=(run_token or uuid4().hex)
|
||||
)
|
||||
response.headers["Cache-Control"]="no-store, no-cache, must-revalidate, max-age=0"
|
||||
response.headers["Pragma"]="no-cache"
|
||||
return response
|
||||
finally: db.close()
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
{% set mirror = mirror_status.mirror if mirror_status and mirror_status.mirror else {} %}
|
||||
{% set mirror_effective_ready = mirror.ready_for_financial_year or preview %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
@@ -18,7 +19,7 @@
|
||||
|
||||
<section class="grid gap-4 md:grid-cols-3">
|
||||
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">ERP Local Agent</div><div class="mt-2 text-lg font-semibold {{ 'text-emerald-700' if agent_online else 'text-red-700' }}">{{ 'Connected' if agent_online else 'Offline' }}</div></div>
|
||||
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">SQLite Mirror</div><div class="mt-2 text-lg font-semibold {{ 'text-emerald-700' if mirror.ready_for_financial_year else 'text-amber-700' }}">{{ 'Ready' if mirror.ready_for_financial_year else 'Mirror required' }}</div>{% if mirror.ready_for_financial_year and mirror.company %}<div class="mt-1 text-xs text-slate-500">{{ mirror.company.company_name }} · {{ fy_start }} to {{ fy_end }}</div>{% elif mirror.financial_year_reason %}<div class="mt-1 text-xs text-amber-700">{{ mirror.financial_year_reason }}</div>{% endif %}</div>
|
||||
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">SQLite Mirror</div><div class="mt-2 text-lg font-semibold {{ 'text-emerald-700' if mirror_effective_ready else 'text-amber-700' }}">{{ 'Ready' if mirror_effective_ready else 'Mirror required' }}</div>{% if mirror_effective_ready and mirror.company %}<div class="mt-1 text-xs text-slate-500">{{ mirror.company.company_name }} · {{ fy_start }} to {{ fy_end }}</div>{% elif mirror.financial_year_reason %}<div class="mt-1 text-xs text-amber-700">{{ mirror.financial_year_reason }}</div>{% endif %}</div>
|
||||
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Write-back</div><div class="mt-2 text-lg font-semibold text-slate-700">Controlled</div><div class="mt-1 text-xs text-slate-500">Tally is contacted only after approval/posting.</div></div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -307,9 +307,16 @@
|
||||
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-700">Read-only export</span>
|
||||
</div>
|
||||
<div class="mt-4 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900"><strong>Before export:</strong> open the mapped company in TallyPrime and set Tally's active period (Alt+F2) to cover the full period you want exported. The ODBC mirror reads Tally's active period; the ERP does not change Tally's period automatically.</div>
|
||||
<form method="post" action="/tools/tally/full-export" class="mt-5 grid gap-4 lg:grid-cols-4 lg:items-end">
|
||||
<form method="post" action="/tools/tally/full-export" class="mt-5 grid gap-4 lg:grid-cols-5 lg:items-end" id="mirrorExportForm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
|
||||
<input type="hidden" name="client_id" value="{{ selected_client.id }}" />
|
||||
<label class="text-sm font-medium text-slate-700">Target Financial Year
|
||||
<select id="mirrorFinancialYear" name="financial_year" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
||||
{% for fy in mirror_fy_options %}
|
||||
<option value="{{ fy }}" {% if fy == selected_financial_year %}selected{% endif %}>FY {{ fy }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">Mapped Tally Company
|
||||
<select name="tally_guid" required class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm">
|
||||
<option value="">Select mapped company</option>
|
||||
@@ -320,10 +327,14 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="text-sm font-medium text-slate-700">From Date<input type="date" name="date_from" required value="{{ transaction_date_from }}" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm" /></label>
|
||||
<label class="text-sm font-medium text-slate-700">To Date<input type="date" name="date_to" required value="{{ transaction_date_to }}" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm" /></label>
|
||||
<label class="text-sm font-medium text-slate-700">From Date<input id="mirrorDateFrom" type="date" name="date_from" required value="{{ transaction_date_from }}" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm" /></label>
|
||||
<label class="text-sm font-medium text-slate-700">To Date<input id="mirrorDateTo" type="date" name="date_to" required value="{{ transaction_date_to }}" class="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm" /></label>
|
||||
<button id="fullExportButton" type="submit" class="rounded-xl bg-emerald-700 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-800">Mirror Tally to SQLite</button>
|
||||
</form>
|
||||
<div class="mt-3 flex flex-wrap items-center gap-3 text-sm">
|
||||
<button type="button" id="mirrorPreviousFY" class="rounded-lg border border-slate-300 bg-white px-3 py-1.5 font-semibold text-slate-700">Use Previous FY</button>
|
||||
<span class="text-slate-500">Select an FY here to create or refresh that year's separate local mirror in the client's FY storage hierarchy.</span>
|
||||
</div>
|
||||
<div id="fullExportProgress" class="mt-5 hidden rounded-xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div class="flex items-center justify-between gap-3"><div><div id="fullExportStage" class="font-semibold text-slate-900">Waiting…</div><div id="fullExportMessage" class="mt-1 text-xs text-slate-500"></div></div><div id="fullExportPct" class="text-lg font-bold text-slate-900">0%</div></div>
|
||||
<div class="mt-3 h-3 overflow-hidden rounded-full bg-slate-200"><div id="fullExportBar" class="h-full bg-emerald-600 transition-all duration-300" style="width:0%"></div></div>
|
||||
@@ -576,4 +587,28 @@
|
||||
pollFullExport();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const fy=document.getElementById("mirrorFinancialYear");
|
||||
const from=document.getElementById("mirrorDateFrom");
|
||||
const to=document.getElementById("mirrorDateTo");
|
||||
const previous=document.getElementById("mirrorPreviousFY");
|
||||
function applyFY(value){
|
||||
const m=/^(\d{4})-(\d{2})$/.exec(value||"");
|
||||
if(!m||!from||!to)return;
|
||||
const y=parseInt(m[1],10);
|
||||
from.value=String(y).padStart(4,"0")+"-04-01";
|
||||
to.value=String(y+1).padStart(4,"0")+"-03-31";
|
||||
}
|
||||
if(fy)fy.addEventListener("change",function(){applyFY(fy.value);});
|
||||
if(previous&&fy)previous.addEventListener("click",function(){
|
||||
if(fy.selectedIndex+1<fy.options.length){
|
||||
fy.selectedIndex+=1;
|
||||
applyFY(fy.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3"><div><p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">TDS & Withholding Tax</p><h1 class="mt-1 text-2xl font-semibold text-slate-900">TDS Compliance Review</h1><p class="mt-1 text-sm text-slate-500">Review expenses, purchases, journals and creditor postings from the client SQLite Accounting Mirror. Rules and thresholds come from the editable TDS Rule Master.</p></div><div class="flex gap-2"><a href="/tools/accounting/tds/rules" class="rounded-xl border px-3 py-2 text-sm font-semibold">Rule Master</a><a href="/tools/tally" class="rounded-xl border px-3 py-2 text-sm font-semibold">Tally</a></div></div>{% if error %}<div class="rounded-xl bg-red-50 p-3 text-sm text-red-800">{{ error }}</div>{% endif %}<form method="get" class="rounded-2xl bg-white p-5 shadow-soft"><div class="grid gap-3 lg:grid-cols-3"><label class="text-sm">Client<select name="client_id" required class="mt-1 w-full rounded-xl border px-3 py-2"><option value="">Select client</option>{% for c in clients %}<option value="{{ c.id }}" {% if selected_client and c.id==selected_client.id %}selected{% endif %}>{{ c.client_name }}</option>{% endfor %}</select></label><label class="text-sm">From<input type="date" name="date_from" value="{{ date_from }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">To<input type="date" name="date_to" value="{{ date_to }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label></div><input type="hidden" name="run" value="1"><button class="mt-4 rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Run TDS Review</button></form>{% if review %}<div class="flex justify-end gap-2"><a href="/tools/tally/analysis/{{ review.analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ date_from[:4] }}-{{ date_to[2:4] }}&format=xlsx" class="rounded-xl border border-emerald-300 bg-emerald-50 px-3 py-2 text-sm font-semibold text-emerald-800">Download Excel</a><a href="/tools/tally/analysis/{{ review.analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ date_from[:4] }}-{{ date_to[2:4] }}&format=pdf" class="rounded-xl border px-3 py-2 text-sm font-semibold">Download PDF</a></div><div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3"><div><p class="text-xs font-semibold uppercase tracking-[0.16em] text-brand-600">TDS & Withholding Tax</p><h1 class="mt-1 text-2xl font-semibold text-slate-900">TDS Compliance Review</h1><p class="mt-1 text-sm text-slate-500">Review expenses, purchases, journals and creditor postings from the client SQLite Accounting Mirror. Rules and thresholds come from the editable TDS Rule Master.</p></div><div class="flex gap-2"><a href="/tools/accounting/tds/rules" class="rounded-xl border px-3 py-2 text-sm font-semibold">Rule Master</a><a href="/tools/tally" class="rounded-xl border px-3 py-2 text-sm font-semibold">Tally</a></div></div>{% if error %}<div class="rounded-xl bg-red-50 p-3 text-sm text-red-800">{{ error }}</div>{% endif %}<form method="get" class="rounded-2xl bg-white p-5 shadow-soft"><div class="grid gap-3 lg:grid-cols-3"><label class="text-sm">Client<select name="client_id" required class="mt-1 w-full rounded-xl border px-3 py-2"><option value="">Select client</option>{% for c in clients %}<option value="{{ c.id }}" {% if selected_client and c.id==selected_client.id %}selected{% endif %}>{{ c.client_name }}</option>{% endfor %}</select></label><label class="text-sm">From<input type="date" name="date_from" value="{{ date_from }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label><label class="text-sm">To<input type="date" name="date_to" value="{{ date_to }}" required class="mt-1 w-full rounded-xl border px-3 py-2"></label></div><input type="hidden" name="run" value="1"><input type="hidden" name="run_token" value="{{ run_token }}"><button class="mt-4 rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white">Run TDS Review</button></form>{% if review %}
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm">
|
||||
<div><span class="font-semibold">Analysis Run #{{ review.analysis_run_id }}</span><span class="ml-2 text-slate-500">{{ review.company_name }} · {{ review.date_from }} to {{ review.date_to }}</span></div>
|
||||
<div class="text-xs text-slate-500">Fresh run {{ run_token[:8] }}</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2"><a href="/tools/tally/analysis/{{ review.analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ date_from[:4] }}-{{ date_to[2:4] }}&format=xlsx" class="rounded-xl border border-emerald-300 bg-emerald-50 px-3 py-2 text-sm font-semibold text-emerald-800">Download Excel</a><a href="/tools/tally/analysis/{{ review.analysis_run_id }}/download?client_id={{ selected_client.id }}&financial_year={{ date_from[:4] }}-{{ date_to[2:4] }}&format=pdf" class="rounded-xl border px-3 py-2 text-sm font-semibold">Download PDF</a></div><div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
||||
{% for k,v in review.summary.items() %}
|
||||
<div class="rounded-xl bg-white p-4 shadow-soft">
|
||||
<div class="text-xs uppercase text-slate-500">{{ k|replace('_',' ') }}</div>
|
||||
|
||||
@@ -67,6 +67,23 @@ def _financial_year_for_date(value: date | None = None) -> str:
|
||||
return f"{start_year}-{str(start_year + 1)[-2:]}"
|
||||
|
||||
|
||||
def _financial_year_bounds(financial_year: str) -> tuple[date, date]:
|
||||
match = re.fullmatch(r"(\d{4})-(\d{2})", str(financial_year or "").strip())
|
||||
if not match:
|
||||
raise ValueError("Invalid financial year.")
|
||||
start_year = int(match.group(1))
|
||||
return date(start_year, 4, 1), date(start_year + 1, 3, 31)
|
||||
|
||||
|
||||
def _financial_year_options(reference_fy: str | None = None, years_back: int = 7) -> list[str]:
|
||||
reference = str(reference_fy or _financial_year_for_date()).strip()
|
||||
start, _ = _financial_year_bounds(reference)
|
||||
return [
|
||||
f"{year}-{str(year + 1)[-2:]}"
|
||||
for year in range(start.year, start.year - max(1, int(years_back)), -1)
|
||||
]
|
||||
|
||||
|
||||
def _accounting_storage_payload(client, financial_year: str | None = None) -> dict:
|
||||
"""Use the same FY + client-folder policy as existing Engagement storage."""
|
||||
fy_value = str(financial_year or _financial_year_for_date()).strip()
|
||||
@@ -202,6 +219,7 @@ def tally_tool(
|
||||
synced: int = 0,
|
||||
transactions_synced: int = 0,
|
||||
export_started: int = 0,
|
||||
financial_year: str = "",
|
||||
error: str = "",
|
||||
):
|
||||
db = CommonSessionLocal()
|
||||
@@ -219,6 +237,13 @@ def tally_tool(
|
||||
live_result = None
|
||||
analysis_history = []
|
||||
command_error = error or ""
|
||||
selected_financial_year = str(financial_year or _financial_year_for_date()).strip()
|
||||
try:
|
||||
selected_fy_start, selected_fy_end = _financial_year_bounds(selected_financial_year)
|
||||
except Exception:
|
||||
selected_financial_year = _financial_year_for_date()
|
||||
selected_fy_start, selected_fy_end = _financial_year_bounds(selected_financial_year)
|
||||
mirror_fy_options = _financial_year_options(_financial_year_for_date(), 7)
|
||||
should_query_agent = bool(refresh or selected_client)
|
||||
|
||||
if refresh:
|
||||
@@ -230,7 +255,7 @@ def tally_tool(
|
||||
if should_query_agent and node and online:
|
||||
payload = {}
|
||||
if selected_client:
|
||||
payload = _accounting_storage_payload(selected_client)
|
||||
payload = _accounting_storage_payload(selected_client, selected_financial_year)
|
||||
try:
|
||||
response_data = request_agent_command(
|
||||
node.node_code,
|
||||
@@ -258,9 +283,12 @@ def tally_tool(
|
||||
command_error = str(exc)
|
||||
|
||||
today = date.today()
|
||||
fy_start_year = today.year if today.month >= 4 else today.year - 1
|
||||
transaction_date_from = date(fy_start_year, 4, 1).isoformat()
|
||||
transaction_date_to = today.isoformat()
|
||||
transaction_date_from = selected_fy_start.isoformat()
|
||||
transaction_date_to = (
|
||||
min(today, selected_fy_end).isoformat()
|
||||
if selected_financial_year == _financial_year_for_date(today)
|
||||
else selected_fy_end.isoformat()
|
||||
)
|
||||
|
||||
return _render(
|
||||
request,
|
||||
@@ -280,6 +308,8 @@ def tally_tool(
|
||||
transactions_synced=bool(transactions_synced),
|
||||
transaction_date_from=transaction_date_from,
|
||||
transaction_date_to=transaction_date_to,
|
||||
selected_financial_year=selected_financial_year,
|
||||
mirror_fy_options=mirror_fy_options,
|
||||
command_error=command_error,
|
||||
analysis_history=analysis_history,
|
||||
)
|
||||
@@ -726,6 +756,7 @@ def start_full_accounting_export(
|
||||
tally_guid: str = Form(...),
|
||||
date_from: str = Form(...),
|
||||
date_to: str = Form(...),
|
||||
financial_year: str = Form(""),
|
||||
csrf_token: str = Form(...),
|
||||
):
|
||||
validate_csrf(request, csrf_token)
|
||||
@@ -742,6 +773,13 @@ def start_full_accounting_export(
|
||||
end = date.fromisoformat(str(date_to or ""))
|
||||
if start > end:
|
||||
raise ValueError("From Date cannot be after To Date.")
|
||||
target_fy = str(financial_year or _financial_year_for_date(start)).strip()
|
||||
fy_start, fy_end = _financial_year_bounds(target_fy)
|
||||
if start < fy_start or end > fy_end:
|
||||
raise ValueError(
|
||||
f"Selected dates must fall within FY {target_fy} "
|
||||
f"({fy_start.isoformat()} to {fy_end.isoformat()})."
|
||||
)
|
||||
except Exception as exc:
|
||||
return RedirectResponse(url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}", status_code=303)
|
||||
node = get_active_storage_node_for_branch(db, scope.tenant_id, scope.branch_id)
|
||||
@@ -750,7 +788,7 @@ def start_full_accounting_export(
|
||||
result = request_agent_command(
|
||||
node.node_code, "accounting_full_export_start",
|
||||
{
|
||||
**_accounting_storage_payload(client, _financial_year_for_date(start)),
|
||||
**_accounting_storage_payload(client, target_fy),
|
||||
"tally_guid": str(tally_guid or "").strip(),
|
||||
"date_from": start.isoformat(), "date_to": end.isoformat(),
|
||||
"requested_by_user_id": int(user.id), "timeout_seconds": 1800,
|
||||
@@ -758,7 +796,7 @@ def start_full_accounting_export(
|
||||
)
|
||||
if not result.get("ok"):
|
||||
raise RuntimeError(str(result.get("error") or "Full Accounting Export could not be started."))
|
||||
return RedirectResponse(url=f"/tools/tally?client_id={client.id}&refresh=1&export_started=1", status_code=303)
|
||||
return RedirectResponse(url=f"/tools/tally?client_id={client.id}&financial_year={quote(target_fy)}&refresh=1&export_started=1", status_code=303)
|
||||
except Exception as exc:
|
||||
return RedirectResponse(url=f"/tools/tally?client_id={client_id}&error={quote(str(exc))}", status_code=303)
|
||||
finally:
|
||||
@@ -779,7 +817,8 @@ def full_accounting_export_status(request: Request, client_id: int):
|
||||
if not node or not _node_online(node):
|
||||
return JSONResponse({"ok": False, "error": "ERP Local Agent is offline."}, status_code=503)
|
||||
result = request_agent_command(
|
||||
node.node_code, "accounting_full_export_status", _accounting_storage_payload(client), timeout_seconds=10
|
||||
node.node_code, "accounting_full_export_status",
|
||||
{"client_id": int(client.id)}, timeout_seconds=10
|
||||
)
|
||||
if not result.get("ok"):
|
||||
return JSONResponse({"ok": False, "error": str(result.get("error") or "Progress lookup failed.")}, status_code=500)
|
||||
|
||||
Reference in New Issue
Block a user