Unify accounting analysis on Client FY mirror registry
This commit is contained in:
@@ -21,9 +21,10 @@ from app.modules.accounting.accounting_mirror_service import (
|
||||
class AccountingAnalyticsDataSource:
|
||||
"""Common read-only data source for accounting/audit analytical procedures.
|
||||
|
||||
This class is additive. Existing tools are not silently redirected in v1.23.0.
|
||||
New or migrated tools can use the mirror immediately while current Opening
|
||||
Balance, Cash Payment, Depreciation and posting workflows continue unchanged.
|
||||
Read-only accounting/audit analysis is mirror-first. UI analysis workflows
|
||||
resolve their Client/FY through AccountingMirrorRegistry before constructing
|
||||
this source or invoking Local-Agent mirror commands. Live Tally remains a
|
||||
bootstrap/write channel only; analytical reads use the registered .act mirror.
|
||||
"""
|
||||
|
||||
def __init__(self, *, node_code: str, accounting_payload: dict[str, Any]):
|
||||
|
||||
@@ -936,7 +936,7 @@ def register_accounting_mirror_version(
|
||||
is_active=True,
|
||||
status="active",
|
||||
accounting_relative_dir=str(accounting_relative_dir or "").strip(),
|
||||
mirror_file_name=Path(str(job.get("accounting_db_path") or f"client_{int(client_id):08d}.act")).name,
|
||||
mirror_file_name=Path(str(job.get("mirror_db_path") or mirror.get("path") or job.get("accounting_db_path") or f"client_{int(client_id):08d}_mirror.act")).name,
|
||||
created_by_user_id=requested_by_user_id,
|
||||
supersedes_mirror_id=(int(current.id) if current else None),
|
||||
)
|
||||
@@ -969,8 +969,8 @@ def register_accounting_mirror_version(
|
||||
|
||||
row.storage_node_id = int(storage_node_id) if storage_node_id else None
|
||||
row.accounting_relative_dir = str(accounting_relative_dir or row.accounting_relative_dir or "").strip()
|
||||
row.mirror_file_name = Path(str(job.get("accounting_db_path") or row.mirror_file_name or f"client_{int(client_id):08d}.act")).name
|
||||
mirror_local_path = str(job.get("accounting_db_path") or mirror.get("path") or "").strip()
|
||||
row.mirror_file_name = Path(str(job.get("mirror_db_path") or mirror.get("path") or job.get("accounting_db_path") or row.mirror_file_name or f"client_{int(client_id):08d}_mirror.act")).name
|
||||
mirror_local_path = str(job.get("mirror_db_path") or mirror.get("path") or job.get("accounting_db_path") or "").strip()
|
||||
if mirror_local_path:
|
||||
row.mirror_local_path = mirror_local_path
|
||||
tally_relative = str(job.get("tally_data_relative_path") or mirror.get("tally_data_relative_path") or "").strip()
|
||||
@@ -1040,7 +1040,7 @@ def upsert_registered_mirror(
|
||||
current.company_name = str(((mirror or {}).get("company") or {}).get("company_name") or current.company_name or "").strip()
|
||||
current.company_guid = str(((mirror or {}).get("company") or {}).get("company_guid") or current.company_guid or "").strip()
|
||||
current.file_size_bytes = int((mirror or {}).get("size_bytes") or current.file_size_bytes or 0)
|
||||
discovered_path = str((job or {}).get("accounting_db_path") or (mirror or {}).get("path") or "").strip()
|
||||
discovered_path = str((job or {}).get("mirror_db_path") or (mirror or {}).get("path") or (job or {}).get("accounting_db_path") or "").strip()
|
||||
if discovered_path:
|
||||
current.mirror_local_path = discovered_path
|
||||
current.updated_at_utc = _mirror_utcnow()
|
||||
|
||||
@@ -18,6 +18,7 @@ from app.modules.accounting.agent_bridge import request_agent_command
|
||||
from app.modules.accounting.cash_payment_models import CashPaymentRuleSetting
|
||||
from app.modules.accounting.ui import (
|
||||
_accounting_storage_payload,
|
||||
_analysis_accounting_storage_payload,
|
||||
_denied,
|
||||
_find_visible_client,
|
||||
_financial_year_for_date,
|
||||
@@ -263,9 +264,11 @@ def cash_payment_review(
|
||||
"SQLite Accounting Mirror, so the storage agent must be online."
|
||||
)
|
||||
else:
|
||||
storage_payload = _accounting_storage_payload(
|
||||
storage_payload = _analysis_accounting_storage_payload(
|
||||
selected_client,
|
||||
review_fy,
|
||||
db=db,
|
||||
tenant_id=scope.tenant_id,
|
||||
)
|
||||
try:
|
||||
status_response = request_agent_command(
|
||||
@@ -387,7 +390,7 @@ async def cash_payment_ledgers(request: Request):
|
||||
try:
|
||||
fy = _financial_year_for_date(date.fromisoformat(date_from))
|
||||
payload = {
|
||||
**_accounting_storage_payload(selected_client, fy),
|
||||
**_analysis_accounting_storage_payload(selected_client, fy, db=db, tenant_id=scope.tenant_id),
|
||||
"tally_guid": tally_guid,
|
||||
"company_name": company_name,
|
||||
"ledger_scope": ledger_scope,
|
||||
@@ -470,7 +473,7 @@ def cash_payment_ledgers_progress(
|
||||
})
|
||||
fy = _financial_year_for_date(date.fromisoformat(date_from))
|
||||
payload = {
|
||||
**_accounting_storage_payload(selected_client, fy),
|
||||
**_analysis_accounting_storage_payload(selected_client, fy, db=db, tenant_id=scope.tenant_id),
|
||||
"job_id": str(job_id),
|
||||
"tally_guid": str(tally_guid or ""),
|
||||
"company_name": str(company_name or ""),
|
||||
@@ -576,7 +579,7 @@ async def start_cash_payment_cache(request: Request):
|
||||
node.node_code,
|
||||
"accounting_cash_payment_cache_start",
|
||||
{
|
||||
**_accounting_storage_payload(selected_client, _financial_year_for_date(parsed_from)),
|
||||
**_analysis_accounting_storage_payload(selected_client, _financial_year_for_date(parsed_from), db=db, tenant_id=scope.tenant_id),
|
||||
"tally_guid": tally_guid,
|
||||
"company_name": company_name,
|
||||
"date_from": date_from,
|
||||
@@ -632,7 +635,7 @@ async def cancel_cash_payment_cache(request: Request):
|
||||
node.node_code,
|
||||
"accounting_cash_payment_cache_cancel",
|
||||
{
|
||||
**_accounting_storage_payload(selected_client, _financial_year_for_date(parsed_from)),
|
||||
**_analysis_accounting_storage_payload(selected_client, _financial_year_for_date(parsed_from), db=db, tenant_id=scope.tenant_id),
|
||||
"job_id": job_id,
|
||||
},
|
||||
timeout_seconds=15,
|
||||
@@ -665,7 +668,7 @@ def cash_payment_cache_progress(request: Request, client_id: int, job_id: str, d
|
||||
result = request_agent_command(
|
||||
node.node_code,
|
||||
"accounting_cash_payment_cache_status",
|
||||
{**_accounting_storage_payload(selected_client, fy), "job_id": str(job_id)},
|
||||
{**_analysis_accounting_storage_payload(selected_client, fy, db=db, tenant_id=scope.tenant_id), "job_id": str(job_id)},
|
||||
timeout_seconds=20,
|
||||
)
|
||||
except Exception as exc:
|
||||
|
||||
@@ -21,6 +21,7 @@ from app.modules.accounting.accounting_mirror_service import (
|
||||
from app.modules.accounting.agent_bridge import request_agent_command
|
||||
from app.modules.accounting.ui import (
|
||||
_accounting_storage_payload,
|
||||
_analysis_accounting_storage_payload,
|
||||
_active_financial_year,
|
||||
_financial_year_bounds,
|
||||
_find_visible_client,
|
||||
@@ -111,9 +112,9 @@ def _load_analysis(request: Request, db, user, client_id: int, financial_year: s
|
||||
return None, clients, scope, f"No registered Accounting Mirror is available for follow-up FY {follow_up_fy}."
|
||||
follow_up_start = follow_start.isoformat()
|
||||
follow_up_end = follow_end.isoformat()
|
||||
follow_up_payload = _accounting_storage_payload(client, follow_up_fy)
|
||||
follow_up_payload = _analysis_accounting_storage_payload(client, follow_up_fy, db=db, tenant_id=scope.tenant_id)
|
||||
|
||||
payload = _accounting_storage_payload(client, financial_year)
|
||||
payload = _analysis_accounting_storage_payload(client, financial_year, db=db, tenant_id=scope.tenant_id)
|
||||
try:
|
||||
result = sundry_creditors_aging(
|
||||
node_code=str(node.node_code),
|
||||
|
||||
@@ -21,7 +21,7 @@ from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
|
||||
from app.core.settings import get_settings
|
||||
from app.core.templating import templates
|
||||
from app.modules.accounting.agent_bridge import request_agent_command
|
||||
from app.modules.accounting.ui import _accounting_storage_payload, _find_visible_client, _require_partner, _visible_clients, _node_online
|
||||
from app.modules.accounting.ui import _accounting_storage_payload, _analysis_accounting_storage_payload, _find_visible_client, _require_partner, _visible_clients, _node_online
|
||||
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
||||
from app.modules.credential_vault.crypto import decrypt_value
|
||||
from app.modules.credential_vault.models import CredentialVaultEntry
|
||||
@@ -256,13 +256,18 @@ def _vault_entries(db, user, request, tenant_id: int, client_id: int, registrati
|
||||
return sorted(eligible, key=rank)
|
||||
|
||||
|
||||
def _storage_payload(client, fy: str, gstin: str) -> tuple[str, str]:
|
||||
def _storage_payload(client, fy: str, gstin: str, *, db=None, tenant_id: int | None = None, require_registered: bool = False) -> tuple[dict, str]:
|
||||
fy_folder = sanitize_segment(f"FY{fy}", "FY")
|
||||
letter, client_folder = client_folder_parts(client, int(client.id))
|
||||
root = Path(fy_folder) / "Clients" / letter / client_folder
|
||||
accounting = _accounting_storage_payload(client, fy)
|
||||
accounting_dir = str(accounting.get("accounting_relative_dir") or (root / "Accounting").as_posix()).strip()
|
||||
return accounting_dir, (root / "GST" / sanitize_segment(f"GSTIN_{gstin}", "GSTIN")).as_posix()
|
||||
if require_registered:
|
||||
accounting = _analysis_accounting_storage_payload(
|
||||
client, fy, db=db, tenant_id=tenant_id
|
||||
)
|
||||
else:
|
||||
accounting = _accounting_storage_payload(client, fy, db=db, tenant_id=tenant_id)
|
||||
accounting.setdefault("accounting_relative_dir", (root / "Accounting").as_posix())
|
||||
return accounting, (root / "GST" / sanitize_segment(f"GSTIN_{gstin}", "GSTIN")).as_posix()
|
||||
|
||||
|
||||
def _redirect(client_id: int, **params):
|
||||
@@ -490,7 +495,8 @@ def start_download(
|
||||
# Ensure FY is consistent in single-month mode.
|
||||
if download_mode == "single":
|
||||
financial_year=_fy_for_period(periods[0])
|
||||
accounting_dir,gst_dir=_storage_payload(client,financial_year,gstin)
|
||||
accounting_payload,gst_dir=_storage_payload(client,financial_year,gstin,db=db,tenant_id=scope.tenant_id)
|
||||
accounting_dir=str(accounting_payload.get("accounting_relative_dir") or "")
|
||||
jti=uuid.uuid4().hex
|
||||
token=_encode_operator_token({
|
||||
"jti":jti,"user_id":int(user.id),"tenant_id":int(scope.tenant_id),"branch_id":int(scope.branch_id),
|
||||
@@ -610,7 +616,8 @@ async def import_downloaded_returns(
|
||||
allowed_periods = set(_selected_periods(download_mode, financial_year, period))
|
||||
if download_mode == "single":
|
||||
financial_year = _fy_for_period(next(iter(allowed_periods)))
|
||||
accounting_dir, gst_dir = _storage_payload(client, financial_year, gstin)
|
||||
accounting_payload, gst_dir = _storage_payload(client, financial_year, gstin, db=db, tenant_id=scope.tenant_id)
|
||||
accounting_dir = str(accounting_payload.get("accounting_relative_dir") or "")
|
||||
groups = {
|
||||
"GSTR1": gstr1_files or [], "GSTR2B": gstr2b_files or [],
|
||||
"GSTR3B": gstr3b_files or [], "GSTR2A": gstr2a_files or [],
|
||||
@@ -719,13 +726,14 @@ def analyze(request: Request, client_id: int=Form(...), registration_id: int=For
|
||||
reg,_=pair; gstin=_norm_gstin(reg.registration_number)
|
||||
periods=_periods_for_fy(financial_year) if analyze_mode=="full_fy" else [re.sub(r"\D","",period or "")]
|
||||
if any(len(p)!=6 for p in periods): raise ValueError("Enter a valid MMYYYY period.")
|
||||
accounting_dir,gst_dir=_storage_payload(client,financial_year,gstin)
|
||||
accounting_payload,gst_dir=_storage_payload(client,financial_year,gstin,db=db,tenant_id=scope.tenant_id,require_registered=True)
|
||||
accounting_dir=str(accounting_payload.get("accounting_relative_dir") or "")
|
||||
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
|
||||
if not node or not _node_online(node): return _redirect(client_id,registration_id=registration_id,period=period,financial_year=financial_year,error="Local Storage Agent is offline.")
|
||||
results=[]
|
||||
for p in periods:
|
||||
date_from,date_to=_period_bounds(p)
|
||||
res=request_agent_command(node.node_code,"gst_reconciliation_analyze",{"client_id":client.id,"gstin":gstin,"financial_year":financial_year,"period":p,"gst_relative_dir":gst_dir,"accounting_relative_dir":accounting_dir,"date_from":date_from,"date_to":date_to},timeout_seconds=30)
|
||||
res=request_agent_command(node.node_code,"gst_reconciliation_analyze",{**accounting_payload,"client_id":client.id,"gstin":gstin,"financial_year":financial_year,"period":p,"gst_relative_dir":gst_dir,"accounting_relative_dir":accounting_dir,"date_from":date_from,"date_to":date_to},timeout_seconds=30)
|
||||
if res.get("ok"):
|
||||
results.append((res.get("result") or {}).get("analysis") or {})
|
||||
if not results: raise RuntimeError("No stored GST periods could be reconciled.")
|
||||
|
||||
@@ -25,6 +25,7 @@ from app.modules.accounting.opening_balance_service import (
|
||||
)
|
||||
from app.modules.accounting.ui import (
|
||||
_accounting_storage_payload,
|
||||
_analysis_accounting_storage_payload,
|
||||
_financial_year_for_date,
|
||||
_find_visible_client,
|
||||
_node_online,
|
||||
@@ -187,7 +188,7 @@ def page(
|
||||
response = request_agent_command(
|
||||
node.node_code,
|
||||
"accounting_mirror_status",
|
||||
_accounting_storage_payload(selected, fy_value),
|
||||
_analysis_accounting_storage_payload(selected, fy_value, db=db, tenant_id=scope.tenant_id),
|
||||
timeout_seconds=20,
|
||||
)
|
||||
status = (response.get("result") or {}) if response.get("ok") else {}
|
||||
@@ -570,8 +571,8 @@ def compare(
|
||||
if not node or not _node_online(node):
|
||||
raise ValueError("ERP Local Agent is offline for the active branch.")
|
||||
|
||||
previous_payload = _accounting_storage_payload(client, previous_fy)
|
||||
current_payload = _accounting_storage_payload(client, current_fy)
|
||||
previous_payload = _analysis_accounting_storage_payload(client, previous_fy, db=db, tenant_id=scope.tenant_id)
|
||||
current_payload = _analysis_accounting_storage_payload(client, current_fy, db=db, tenant_id=scope.tenant_id)
|
||||
response = request_agent_command(
|
||||
node.node_code,
|
||||
"accounting_opening_balance_mirror_snapshot",
|
||||
@@ -768,7 +769,7 @@ async def apply_corrections(
|
||||
node.node_code,
|
||||
"accounting_opening_balance_apply",
|
||||
{
|
||||
**_accounting_storage_payload(client, selected_financial_year or None),
|
||||
**_analysis_accounting_storage_payload(client, selected_financial_year or None, db=db, tenant_id=scope.tenant_id),
|
||||
"current_company_name": run.current_company_name,
|
||||
"current_company_guid": run.current_company_guid,
|
||||
"source_key": f"OPENING-RUN-{run.id}",
|
||||
|
||||
@@ -14,7 +14,7 @@ from app.core.security.csrf import get_or_create_csrf_token, validate_csrf
|
||||
from app.core.templating import templates
|
||||
from app.modules.accounting.agent_bridge import request_agent_command
|
||||
from app.modules.accounting.tds_models import AccountingTDSPostingInstruction, AccountingTDSRule
|
||||
from app.modules.accounting.ui import _accounting_storage_payload, _financial_year_for_date, _node_online, _require_partner, _visible_clients
|
||||
from app.modules.accounting.ui import _accounting_storage_payload, _analysis_accounting_storage_payload, _financial_year_for_date, _node_online, _require_partner, _visible_clients
|
||||
from app.modules.core.rbac.deps import get_user_permissions, get_user_roles
|
||||
from app.modules.documents.services import get_active_storage_node_for_branch
|
||||
|
||||
@@ -70,7 +70,7 @@ def _workspace(request, db, user, client_id, tally_guid, date_from, date_to, run
|
||||
start=date_from or date(sy,4,1).isoformat(); end=date_to or today.isoformat()
|
||||
live=None; review=None; error=""; history=[]
|
||||
if selected and node and online:
|
||||
storage = _accounting_storage_payload(selected,_financial_year_for_date(date.fromisoformat(start)))
|
||||
storage = _analysis_accounting_storage_payload(selected,_financial_year_for_date(date.fromisoformat(start)), db=db, tenant_id=scope.tenant_id)
|
||||
st=request_agent_command(node.node_code,"accounting_mirror_status",storage,timeout_seconds=20)
|
||||
if st.get("ok"): live=st.get("result") or {}
|
||||
else: error=str(st.get("error") or "Accounting Mirror status failed.")
|
||||
@@ -204,7 +204,7 @@ async def post_liability(request:Request,instruction_id:int):
|
||||
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
|
||||
if not node or not _node_online(node): raise ValueError("Local Agent is offline.")
|
||||
voucher_date=str(form.get("voucher_date") or (row.source_voucher_date.isoformat() if row.source_voucher_date else date.today().isoformat()))
|
||||
result=request_agent_command(node.node_code,"accounting_post_tds_liability",{**_accounting_storage_payload(client,_financial_year_for_date(date.fromisoformat(voucher_date))),"tally_guid":str(form.get("tally_guid") or row.company_guid or ""),"voucher_date":voucher_date,"party_ledger":row.party_ledger,"tds_ledger":str(form.get("tds_ledger") or row.tds_ledger),"amount":row.liability_amount,"reference":row.erp_reference,"narration":row.narration or f"TDS liability {row.erp_reference}","posted_by_user_id":int(user.id)},timeout_seconds=120)
|
||||
result=request_agent_command(node.node_code,"accounting_post_tds_liability",{**_analysis_accounting_storage_payload(client,_financial_year_for_date(date.fromisoformat(voucher_date)), db=db, tenant_id=_tenant_id(request,user)),"tally_guid":str(form.get("tally_guid") or row.company_guid or ""),"voucher_date":voucher_date,"party_ledger":row.party_ledger,"tds_ledger":str(form.get("tds_ledger") or row.tds_ledger),"amount":row.liability_amount,"reference":row.erp_reference,"narration":row.narration or f"TDS liability {row.erp_reference}","posted_by_user_id":int(user.id)},timeout_seconds=120)
|
||||
if not result.get("ok"): raise ValueError(str(result.get("error") or "Tally posting failed."))
|
||||
rr=result.get("result") or {}; row.status="posted"; row.tds_ledger=str(form.get("tds_ledger") or row.tds_ledger); row.posted_by_user_id=user.id; row.posted_at_utc=datetime.utcnow(); row.tally_voucher_id=str(rr.get("last_voucher_id") or rr.get("voucher_id") or "") or None; db.commit()
|
||||
return RedirectResponse(f"/tools/accounting/tds/liability?client_id={row.client_id}&saved=1",status_code=303)
|
||||
@@ -247,7 +247,7 @@ async def reconciliation_run(request:Request):
|
||||
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
|
||||
if not node or not _node_online(node): raise ValueError("Local Agent is offline.")
|
||||
dfrom=str(form.get("date_from") or ""); dto=str(form.get("date_to") or ""); guid=str(form.get("tally_guid") or "")
|
||||
rr=request_agent_command(node.node_code,"accounting_tds_compliance",{**_accounting_storage_payload(selected,_financial_year_for_date(date.fromisoformat(dfrom))),"date_from":dfrom,"date_to":dto,"rules":_rule_payload(_rules(db,_tenant_id(request,user)))},timeout_seconds=120)
|
||||
rr=request_agent_command(node.node_code,"accounting_tds_compliance",{**_analysis_accounting_storage_payload(selected,_financial_year_for_date(date.fromisoformat(dfrom)), db=db, tenant_id=_tenant_id(request,user)),"date_from":dfrom,"date_to":dto,"rules":_rule_payload(_rules(db,_tenant_id(request,user)))},timeout_seconds=120)
|
||||
if not rr.get("ok"): raise ValueError(str(rr.get("error") or "Tally review failed."))
|
||||
review=(rr.get("result") or {}).get("tds_review") or {}; tally_total=round(sum(float(x.get("actual_tds") or 0) for x in review.get("transactions",[])),2); expected_total=round(sum(float(x.get("expected_tds") or 0) for x in review.get("transactions",[])),2)
|
||||
returns=_parse_external(str(form.get("return_data") or "")); challans=_parse_external(str(form.get("challan_data") or "")); traces=_parse_external(str(form.get("traces_data") or ""))
|
||||
|
||||
@@ -190,6 +190,39 @@ def _accounting_storage_payload(client, financial_year: str | None = None, *, db
|
||||
own_db.close()
|
||||
|
||||
|
||||
def _analysis_accounting_storage_payload(
|
||||
client,
|
||||
financial_year: str | None = None,
|
||||
*,
|
||||
db=None,
|
||||
tenant_id: int | None = None,
|
||||
) -> dict:
|
||||
"""Return the authoritative registered Client/FY mirror payload for analysis jobs.
|
||||
|
||||
Analytical procedures must never silently fall back to a deterministic folder.
|
||||
The fallback remains available to bootstrap/sync/write workflows through
|
||||
``_accounting_storage_payload``. Requiring a registry row here guarantees that
|
||||
every analysis is tied to the same Client/FY/version selected by
|
||||
``AccountingMirrorRegistry``.
|
||||
"""
|
||||
payload = _accounting_storage_payload(
|
||||
client,
|
||||
financial_year,
|
||||
db=db,
|
||||
tenant_id=tenant_id,
|
||||
)
|
||||
if not bool(payload.get("mirror_registered")):
|
||||
fy_value = str(payload.get("financial_year") or financial_year or "").strip()
|
||||
raise ValueError(
|
||||
f"No registered Accounting Mirror is available for FY {fy_value}. "
|
||||
"Create or register the Client/FY mirror from Tally Accounting first."
|
||||
)
|
||||
if not str(payload.get("accounting_relative_dir") or "").strip():
|
||||
raise ValueError("The registered Accounting Mirror has no storage path.")
|
||||
payload["analysis_source"] = "accounting_mirror_registry"
|
||||
return payload
|
||||
|
||||
|
||||
def _client_registrations(db, client, tenant_id: int):
|
||||
rows = db.execute(
|
||||
select(ClientRegistration, RegistrationType)
|
||||
@@ -535,7 +568,7 @@ def download_analysis_report(
|
||||
node.node_code,
|
||||
"accounting_analysis_get",
|
||||
{
|
||||
**_accounting_storage_payload(client, financial_year),
|
||||
**_analysis_accounting_storage_payload(client, financial_year, db=db, tenant_id=scope.tenant_id),
|
||||
"run_id": int(analysis_run_id),
|
||||
},
|
||||
timeout_seconds=30,
|
||||
@@ -1109,7 +1142,7 @@ def depreciation_it_tool(
|
||||
|
||||
if selected_client and node and online:
|
||||
selected_fy=_financial_year_for_date(date.fromisoformat(start_text))
|
||||
storage_payload=_accounting_storage_payload(selected_client,selected_fy)
|
||||
storage_payload=_analysis_accounting_storage_payload(selected_client,selected_fy,db=db,tenant_id=scope.tenant_id)
|
||||
try:
|
||||
status_response=request_agent_command(
|
||||
node.node_code,"accounting_mirror_status",storage_payload,timeout_seconds=20
|
||||
@@ -1124,7 +1157,7 @@ def depreciation_it_tool(
|
||||
prior_start=date(prior_start_year,4,1)
|
||||
prior_end=date(prior_start_year+1,3,31)
|
||||
prior_fy=_financial_year_for_date(prior_start)
|
||||
prior_payload=_accounting_storage_payload(selected_client,prior_fy)
|
||||
prior_payload=_analysis_accounting_storage_payload(selected_client,prior_fy,db=db,tenant_id=scope.tenant_id)
|
||||
prior_response=request_agent_command(
|
||||
node.node_code,"accounting_mirror_status",prior_payload,timeout_seconds=20
|
||||
)
|
||||
@@ -1222,7 +1255,7 @@ async def calculate_it_depreciation(request: Request):
|
||||
node=get_active_storage_node_for_branch(db,scope.tenant_id,scope.branch_id)
|
||||
if not node or not _node_online(node): return RedirectResponse(url=f"/tools/tally/depreciation?client_id={client.id}&fy_start={start.isoformat()}&fy_end={end.isoformat()}&error={quote('ERP Local Agent is offline for the active branch.')}",status_code=303)
|
||||
try:
|
||||
result=request_agent_command(node.node_code,"accounting_calculate_it_depreciation",{**_accounting_storage_payload(client, _financial_year_for_date(start)),"fy_start":start.isoformat(),"fy_end":end.isoformat(),"assignments":assignments,"depreciation_expense_ledger":str(form.get("depreciation_expense_ledger") or ""),"depreciation_reserve_ledger":str(form.get("depreciation_reserve_ledger") or ""),"financial_year":_financial_year_for_date(start),"requested_by_user_id":int(user.id)},timeout_seconds=120)
|
||||
result=request_agent_command(node.node_code,"accounting_calculate_it_depreciation",{**_analysis_accounting_storage_payload(client, _financial_year_for_date(start), db=db, tenant_id=scope.tenant_id),"fy_start":start.isoformat(),"fy_end":end.isoformat(),"assignments":assignments,"depreciation_expense_ledger":str(form.get("depreciation_expense_ledger") or ""),"depreciation_reserve_ledger":str(form.get("depreciation_reserve_ledger") or ""),"financial_year":_financial_year_for_date(start),"requested_by_user_id":int(user.id)},timeout_seconds=120)
|
||||
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Income-tax depreciation calculation failed."))
|
||||
dep=(result.get("result") or {}).get("depreciation") or {}; rid=int(dep.get("run_id"))
|
||||
except Exception as exc:
|
||||
@@ -1255,7 +1288,7 @@ async def approve_it_depreciation(request: Request):
|
||||
try:
|
||||
result = request_agent_command(
|
||||
node.node_code, "accounting_approve_it_depreciation",
|
||||
{**_accounting_storage_payload(client, _financial_year_for_date(approval_fy_start)), "run_id": run_id, "approved_by_user_id": int(user.id), "approval_note": str(form.get("approval_note") or "")},
|
||||
{**_analysis_accounting_storage_payload(client, _financial_year_for_date(approval_fy_start), db=db, tenant_id=scope.tenant_id), "run_id": run_id, "approved_by_user_id": int(user.id), "approval_note": str(form.get("approval_note") or "")},
|
||||
timeout_seconds=30,
|
||||
)
|
||||
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Approval failed."))
|
||||
@@ -1293,7 +1326,7 @@ async def post_it_depreciation_to_tally(request: Request):
|
||||
try:
|
||||
result = request_agent_command(
|
||||
node.node_code, "accounting_post_it_depreciation",
|
||||
{**_accounting_storage_payload(client, _financial_year_for_date(posting_fy_start)), "run_id": run_id, "posted_by_user_id": int(user.id)},
|
||||
{**_analysis_accounting_storage_payload(client, _financial_year_for_date(posting_fy_start), db=db, tenant_id=scope.tenant_id), "run_id": run_id, "posted_by_user_id": int(user.id)},
|
||||
timeout_seconds=120,
|
||||
)
|
||||
if not result.get("ok"): raise RuntimeError(str(result.get("error") or "Tally write-back failed."))
|
||||
|
||||
@@ -4,7 +4,7 @@ import io
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
ERP_LOCAL_AGENT_VERSION = "1.26.19"
|
||||
ERP_LOCAL_AGENT_VERSION = "1.26.20"
|
||||
ERP_LOCAL_AGENT_NAME = "ERP Local Agent"
|
||||
RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime"
|
||||
_DETERMINISTIC_ZIP_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
__version__ = "1.26.19"
|
||||
__version__ = "1.26.20"
|
||||
AGENT_NAME = "ERP Local Agent"
|
||||
|
||||
@@ -679,7 +679,10 @@ class AgentCommandProcessor:
|
||||
raise ValueError("Client GST and Accounting storage paths are required.")
|
||||
root = Path(self.config.storage_root)
|
||||
base_dir = root / Path(gst_relative_dir) / period
|
||||
mirror_db = root / Path(accounting_relative_dir) / f"client_{client_id:08d}_mirror.act"
|
||||
mirror_file_name = Path(str(payload.get("mirror_file_name") or f"client_{client_id:08d}_mirror.act")).name
|
||||
if not mirror_file_name.lower().endswith(".act"):
|
||||
raise ValueError("Registered Accounting Mirror filename is invalid.")
|
||||
mirror_db = root / Path(accounting_relative_dir) / mirror_file_name
|
||||
if not base_dir.exists():
|
||||
raise ValueError("GST data has not yet been downloaded for this client/period.")
|
||||
if not mirror_db.exists():
|
||||
@@ -4219,10 +4222,17 @@ class AgentCommandProcessor:
|
||||
client_id = int(payload.get("client_id") or 0)
|
||||
if client_id <= 0:
|
||||
raise ValueError("client_id is required.")
|
||||
filename = f"client_{client_id:08d}.act"
|
||||
mirror_filename = f"client_{client_id:08d}_mirror.act"
|
||||
accounting_filename = f"client_{client_id:08d}.act"
|
||||
items: list[dict[str, Any]] = []
|
||||
pattern = f"FY*/Clients/*/*/Accounting/{filename}"
|
||||
for path in sorted(self.store.storage_root.glob(pattern)):
|
||||
pattern = f"FY*/Clients/*/*/Accounting/{mirror_filename}"
|
||||
paths = sorted(self.store.storage_root.glob(pattern))
|
||||
# Legacy fallback: older workstations may have only the client accounting
|
||||
# database until the next full mirror refresh. Discovery still reports it,
|
||||
# but new/full exports register the actual *_mirror.act analytical source.
|
||||
if not paths:
|
||||
paths = sorted(self.store.storage_root.glob(f"FY*/Clients/*/*/Accounting/{accounting_filename}"))
|
||||
for path in paths:
|
||||
if not path.is_file():
|
||||
continue
|
||||
try:
|
||||
@@ -4254,7 +4264,8 @@ class AgentCommandProcessor:
|
||||
"client_id": client_id,
|
||||
"financial_year": fy,
|
||||
"accounting_relative_dir": relative,
|
||||
"accounting_db_path": str(path),
|
||||
"mirror_db_path": str(path),
|
||||
"accounting_db_path": str(path.parent / accounting_filename),
|
||||
"company_name": str(company.get("company_name") or "").strip(),
|
||||
"tally_guid": str(company.get("company_guid") or "").strip(),
|
||||
"mirror": {
|
||||
|
||||
Reference in New Issue
Block a user