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 datetime import date, datetime
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import APIRouter, Request
|
from fastapi import APIRouter, Request
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
@@ -139,13 +140,22 @@ def tds_root(request: Request, client_id: int | None = None):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/compliance")
|
@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()
|
db=CommonSessionLocal()
|
||||||
try:
|
try:
|
||||||
user,response=_require_partner(request,db,"accounting.tally.view")
|
user,response=_require_partner(request,db,"accounting.tally.view")
|
||||||
if response:return response
|
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)
|
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()
|
finally: db.close()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends "ui/templates/base/layout.html" %}
|
{% extends "ui/templates/base/layout.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% set mirror = mirror_status.mirror if mirror_status and mirror_status.mirror else {} %}
|
{% 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="space-y-6">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
|
|
||||||
<section class="grid gap-4 md:grid-cols-3">
|
<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">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>
|
<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>
|
</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>
|
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-700">Read-only export</span>
|
||||||
</div>
|
</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>
|
<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="csrf_token" value="{{ csrf_token }}" />
|
||||||
<input type="hidden" name="client_id" value="{{ selected_client.id }}" />
|
<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
|
<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">
|
<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>
|
<option value="">Select mapped company</option>
|
||||||
@@ -320,10 +327,14 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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">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 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">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>
|
<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>
|
</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 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="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>
|
<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();
|
pollFullExport();
|
||||||
})();
|
})();
|
||||||
</script>
|
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
{% extends "ui/templates/base/layout.html" %}
|
{% extends "ui/templates/base/layout.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="space-y-6">
|
<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() %}
|
{% for k,v in review.summary.items() %}
|
||||||
<div class="rounded-xl bg-white p-4 shadow-soft">
|
<div class="rounded-xl bg-white p-4 shadow-soft">
|
||||||
<div class="text-xs uppercase text-slate-500">{{ k|replace('_',' ') }}</div>
|
<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:]}"
|
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:
|
def _accounting_storage_payload(client, financial_year: str | None = None) -> dict:
|
||||||
"""Use the same FY + client-folder policy as existing Engagement storage."""
|
"""Use the same FY + client-folder policy as existing Engagement storage."""
|
||||||
fy_value = str(financial_year or _financial_year_for_date()).strip()
|
fy_value = str(financial_year or _financial_year_for_date()).strip()
|
||||||
@@ -202,6 +219,7 @@ def tally_tool(
|
|||||||
synced: int = 0,
|
synced: int = 0,
|
||||||
transactions_synced: int = 0,
|
transactions_synced: int = 0,
|
||||||
export_started: int = 0,
|
export_started: int = 0,
|
||||||
|
financial_year: str = "",
|
||||||
error: str = "",
|
error: str = "",
|
||||||
):
|
):
|
||||||
db = CommonSessionLocal()
|
db = CommonSessionLocal()
|
||||||
@@ -219,6 +237,13 @@ def tally_tool(
|
|||||||
live_result = None
|
live_result = None
|
||||||
analysis_history = []
|
analysis_history = []
|
||||||
command_error = error or ""
|
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)
|
should_query_agent = bool(refresh or selected_client)
|
||||||
|
|
||||||
if refresh:
|
if refresh:
|
||||||
@@ -230,7 +255,7 @@ def tally_tool(
|
|||||||
if should_query_agent and node and online:
|
if should_query_agent and node and online:
|
||||||
payload = {}
|
payload = {}
|
||||||
if selected_client:
|
if selected_client:
|
||||||
payload = _accounting_storage_payload(selected_client)
|
payload = _accounting_storage_payload(selected_client, selected_financial_year)
|
||||||
try:
|
try:
|
||||||
response_data = request_agent_command(
|
response_data = request_agent_command(
|
||||||
node.node_code,
|
node.node_code,
|
||||||
@@ -258,9 +283,12 @@ def tally_tool(
|
|||||||
command_error = str(exc)
|
command_error = str(exc)
|
||||||
|
|
||||||
today = date.today()
|
today = date.today()
|
||||||
fy_start_year = today.year if today.month >= 4 else today.year - 1
|
transaction_date_from = selected_fy_start.isoformat()
|
||||||
transaction_date_from = date(fy_start_year, 4, 1).isoformat()
|
transaction_date_to = (
|
||||||
transaction_date_to = today.isoformat()
|
min(today, selected_fy_end).isoformat()
|
||||||
|
if selected_financial_year == _financial_year_for_date(today)
|
||||||
|
else selected_fy_end.isoformat()
|
||||||
|
)
|
||||||
|
|
||||||
return _render(
|
return _render(
|
||||||
request,
|
request,
|
||||||
@@ -280,6 +308,8 @@ def tally_tool(
|
|||||||
transactions_synced=bool(transactions_synced),
|
transactions_synced=bool(transactions_synced),
|
||||||
transaction_date_from=transaction_date_from,
|
transaction_date_from=transaction_date_from,
|
||||||
transaction_date_to=transaction_date_to,
|
transaction_date_to=transaction_date_to,
|
||||||
|
selected_financial_year=selected_financial_year,
|
||||||
|
mirror_fy_options=mirror_fy_options,
|
||||||
command_error=command_error,
|
command_error=command_error,
|
||||||
analysis_history=analysis_history,
|
analysis_history=analysis_history,
|
||||||
)
|
)
|
||||||
@@ -726,6 +756,7 @@ def start_full_accounting_export(
|
|||||||
tally_guid: str = Form(...),
|
tally_guid: str = Form(...),
|
||||||
date_from: str = Form(...),
|
date_from: str = Form(...),
|
||||||
date_to: str = Form(...),
|
date_to: str = Form(...),
|
||||||
|
financial_year: str = Form(""),
|
||||||
csrf_token: str = Form(...),
|
csrf_token: str = Form(...),
|
||||||
):
|
):
|
||||||
validate_csrf(request, csrf_token)
|
validate_csrf(request, csrf_token)
|
||||||
@@ -742,6 +773,13 @@ def start_full_accounting_export(
|
|||||||
end = date.fromisoformat(str(date_to or ""))
|
end = date.fromisoformat(str(date_to or ""))
|
||||||
if start > end:
|
if start > end:
|
||||||
raise ValueError("From Date cannot be after To Date.")
|
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:
|
except Exception as exc:
|
||||||
return RedirectResponse(url=f"/tools/tally?client_id={client.id}&error={quote(str(exc))}", status_code=303)
|
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)
|
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(
|
result = request_agent_command(
|
||||||
node.node_code, "accounting_full_export_start",
|
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(),
|
"tally_guid": str(tally_guid or "").strip(),
|
||||||
"date_from": start.isoformat(), "date_to": end.isoformat(),
|
"date_from": start.isoformat(), "date_to": end.isoformat(),
|
||||||
"requested_by_user_id": int(user.id), "timeout_seconds": 1800,
|
"requested_by_user_id": int(user.id), "timeout_seconds": 1800,
|
||||||
@@ -758,7 +796,7 @@ def start_full_accounting_export(
|
|||||||
)
|
)
|
||||||
if not result.get("ok"):
|
if not result.get("ok"):
|
||||||
raise RuntimeError(str(result.get("error") or "Full Accounting Export could not be started."))
|
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:
|
except Exception as exc:
|
||||||
return RedirectResponse(url=f"/tools/tally?client_id={client_id}&error={quote(str(exc))}", status_code=303)
|
return RedirectResponse(url=f"/tools/tally?client_id={client_id}&error={quote(str(exc))}", status_code=303)
|
||||||
finally:
|
finally:
|
||||||
@@ -779,7 +817,8 @@ def full_accounting_export_status(request: Request, client_id: int):
|
|||||||
if not node or not _node_online(node):
|
if not node or not _node_online(node):
|
||||||
return JSONResponse({"ok": False, "error": "ERP Local Agent is offline."}, status_code=503)
|
return JSONResponse({"ok": False, "error": "ERP Local Agent is offline."}, status_code=503)
|
||||||
result = request_agent_command(
|
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"):
|
if not result.get("ok"):
|
||||||
return JSONResponse({"ok": False, "error": str(result.get("error") or "Progress lookup failed.")}, status_code=500)
|
return JSONResponse({"ok": False, "error": str(result.get("error") or "Progress lookup failed.")}, status_code=500)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import io
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
ERP_LOCAL_AGENT_VERSION = "1.25.6"
|
ERP_LOCAL_AGENT_VERSION = "1.25.7"
|
||||||
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
||||||
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
||||||
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
|
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
__version__ = "1.25.6"
|
__version__ = "1.25.7"
|
||||||
AGENT_NAME = "ERP Local Agent"
|
AGENT_NAME = "ERP Local Agent"
|
||||||
|
|||||||
@@ -3174,16 +3174,17 @@ class AgentCommandProcessor:
|
|||||||
}
|
}
|
||||||
if value in exact:
|
if value in exact:
|
||||||
return True
|
return True
|
||||||
owner_prefix = (
|
owner_word = any(
|
||||||
value.startswith("partner ") or value.startswith("partners ")
|
token in value.split()
|
||||||
or value.startswith("proprietor ") or value.startswith("proprietors ")
|
for token in ("partner","partners","proprietor","proprietors","owner","owners")
|
||||||
or value.startswith("owner ") or value.startswith("owners ")
|
|
||||||
)
|
)
|
||||||
current_account_like = (
|
current_account_like = (
|
||||||
" current account" in value or " current accounts" in value
|
"current account" in value
|
||||||
or " current a c" in value or value.endswith(" current ac")
|
or "current accounts" in value
|
||||||
|
or "current a c" in value
|
||||||
|
or value.endswith("current ac")
|
||||||
)
|
)
|
||||||
if owner_prefix and current_account_like:
|
if owner_word and current_account_like:
|
||||||
return True
|
return True
|
||||||
return (
|
return (
|
||||||
value.startswith("capital account ")
|
value.startswith("capital account ")
|
||||||
@@ -3211,6 +3212,14 @@ class AgentCommandProcessor:
|
|||||||
parent_group = str(ledger_parent_by_name.get(lname.casefold()) or "").strip()
|
parent_group = str(ledger_parent_by_name.get(lname.casefold()) or "").strip()
|
||||||
if _capital_account_parent(parent_group):
|
if _capital_account_parent(parent_group):
|
||||||
continue
|
continue
|
||||||
|
normalized_capital_context = re.sub(
|
||||||
|
r"[^a-z0-9]+", " ", f"{parent_group} {lname}".casefold()
|
||||||
|
).strip()
|
||||||
|
if (
|
||||||
|
any(x in normalized_capital_context.split() for x in ("partner","partners","proprietor","proprietors","owner","owners"))
|
||||||
|
and ("current account" in normalized_capital_context or "current a c" in normalized_capital_context)
|
||||||
|
):
|
||||||
|
continue
|
||||||
text = lname.casefold()
|
text = lname.casefold()
|
||||||
matches=[]
|
matches=[]
|
||||||
for r in rules:
|
for r in rules:
|
||||||
|
|||||||
Reference in New Issue
Block a user