Fix accounting mirror extraction for selected financial year

This commit is contained in:
A R R R Associates
2026-09-09 21:49:44 +05:30
parent 1a7563c9da
commit 2872b67671
4 changed files with 269 additions and 5 deletions
@@ -1,2 +1 @@
__version__ = "1.26.12"
AGENT_NAME = "ERP Local Agent"
__version__ = "1.26.13"
@@ -574,6 +574,213 @@ class AccountingMirrorEngine:
report(98, "Mirror ready", "Accounting Mirror database is ready for ERP synchronization.", rows=dict(rows), mirror=result.get("mirror") or {})
return result
def replace_transaction_period_from_vouchers(
self,
*,
client_id: int,
company_name: str,
company_guid: str,
date_from: str,
date_to: str,
vouchers: list[dict[str, Any]],
) -> dict[str, Any]:
"""Replace only the mirror transaction tables with an explicitly dated export.
Tally ODBC Type:Voucher collections follow Tally's active period. The ERP
full-export screen, however, carries an explicit financial-year period. If
those periods differ, the existing read-only XML voucher exporter is used
as a correctness fallback and this method writes those exact-period rows
into the already-built ODBC mirror. Master tables remain untouched.
"""
paths = self.paths(client_id)
db_path = paths.current_mirror
if not db_path.is_file():
raise RuntimeError("Accounting Mirror database is not available for transaction-period correction.")
start = datetime.fromisoformat(str(date_from)).date()
end = datetime.fromisoformat(str(date_to)).date()
if start > end:
raise ValueError("From Date cannot be after To Date.")
db = sqlite3.connect(db_path, timeout=60)
db.row_factory = sqlite3.Row
now = _utc_now()
try:
company = db.execute(
"SELECT * FROM company_master ORDER BY synced_at DESC LIMIT 1"
).fetchone()
if not company:
raise RuntimeError("Accounting Mirror company master is missing.")
instance = _text(company["company_instance_id"])
stored_guid = _text(company["company_guid"])
effective_guid = _text(company_guid) or stored_guid
effective_name = _text(company_name) or _text(company["company_name"])
stock_rows = db.execute(
"SELECT stock_item_guid,product_name,base_uom,current_hsn,current_gst_rate "
"FROM stock_item_master"
).fetchall()
stock_by_name = {
_text(r["product_name"]).casefold(): dict(r) for r in stock_rows if _text(r["product_name"])
}
db.execute("BEGIN IMMEDIATE")
db.execute("DELETE FROM inventory_movement")
db.execute("DELETE FROM voucher_ledger_entry")
db.execute("DELETE FROM voucher")
voucher_count = 0
ledger_count = 0
inventory_count = 0
seen_dates: list[str] = []
max_alter = 0
for seq, voucher in enumerate(vouchers or [], start=1):
vdate = _text(voucher.get("date") or voucher.get("effective_date"))
try:
parsed = datetime.fromisoformat(vdate).date()
except Exception:
continue
if parsed < start or parsed > end:
continue
vtype = _text(voucher.get("voucher_type_name"))
vnum = _text(voucher.get("voucher_number"))
party = _text(voucher.get("party_ledger_name"))
ref = _text(voucher.get("reference"))
narration = _text(voucher.get("narration"))
master_id = int(_number(voucher.get("master_id")) or 0)
alter_id = int(_number(voucher.get("alter_id")) or 0)
max_alter = max(max_alter, alter_id)
guid = _text(voucher.get("guid"))
if not guid:
identity = "|".join([
effective_guid, vdate, vtype, vnum, str(master_id), str(alter_id), ref, party, str(seq)
])
guid = "ERPXML-" + hashlib.sha256(identity.encode("utf-8")).hexdigest()[:32]
entries = list(voucher.get("ledger_entries") or [])
party_amount = 0.0
largest = 0.0
for entry in entries:
amt = float(entry.get("amount") or 0.0)
largest = max(largest, abs(amt))
if party and _text(entry.get("ledger_name")).casefold() == party.casefold():
party_amount = abs(amt)
voucher_amount = party_amount or largest
db.execute(
"""INSERT OR REPLACE INTO voucher(
company_instance_id,company_guid,voucher_guid,alter_id,master_id,voucher_date,
voucher_type,voucher_number,party_ledger,narration,reference,voucher_amount,
is_cancelled,is_optional,synced_at
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
instance, effective_guid, guid, alter_id, master_id, vdate, vtype, vnum, party,
narration, ref, voucher_amount,
1 if _text(voucher.get("is_cancelled")).casefold() in {"yes","true","1"} else 0,
1 if _text(voucher.get("is_optional")).casefold() in {"yes","true","1"} else 0,
now,
),
)
voucher_count += 1
seen_dates.append(vdate)
for line_no, entry in enumerate(entries, start=1):
raw = float(entry.get("amount") or 0.0)
deemed = _text(entry.get("is_deemed_positive"))
if raw < 0:
drcr = "DR"
elif raw > 0:
drcr = "CR"
else:
drcr = "DR" if deemed.casefold() in {"yes","true","1"} else "CR"
db.execute(
"""INSERT INTO voucher_ledger_entry(
company_instance_id,company_guid,voucher_guid,line_no,alter_id,master_id,
voucher_date,voucher_type,voucher_number,party_ledger,ledger_name,dr_cr,
amount,raw_amount,raw_is_deemed_positive
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
instance, effective_guid, guid, line_no, alter_id, master_id, vdate, vtype,
vnum, party, _text(entry.get("ledger_name")), drcr, abs(raw), raw, deemed,
),
)
ledger_count += 1
for line_no, entry in enumerate(list(voucher.get("inventory_entries") or []), start=1):
stock_name = _text(entry.get("stock_item_name"))
stock = stock_by_name.get(stock_name.casefold(), {})
raw_value = float(entry.get("amount") or 0.0)
deemed = _text(entry.get("is_deemed_positive"))
qty_text = _text(entry.get("billed_qty"))
actual_text = _text(entry.get("actual_qty"))
rate_text = _text(entry.get("rate"))
qty = abs(_number(qty_text)) if qty_text else None
actual_qty = abs(_number(actual_text)) if actual_text else None
rate = _number(rate_text) if rate_text else None
vt = vtype.casefold()
if "sales" in vt:
direction, sign = "OUTWARD", -1.0
elif "purchase" in vt:
direction, sign = "INWARD", 1.0
else:
sign = -1.0 if deemed.casefold() in {"yes","true","1"} else 1.0
direction = "OUTWARD" if sign < 0 else "INWARD"
db.execute(
"""INSERT INTO inventory_movement(
company_instance_id,company_guid,voucher_guid,line_no,alter_id,master_id,
voucher_date,voucher_type,voucher_number,party_ledger,stock_item_name,
stock_item_guid,hsn,gst_rate,cgst_rate,sgst_rate,igst_rate,cess_rate,
taxability,billed_qty_text,billed_qty,actual_qty_text,actual_qty,uom,
rate_text,rate,value,direction,signed_qty,signed_value,raw_is_deemed_positive
) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
instance, effective_guid, guid, line_no, alter_id, master_id, vdate, vtype,
vnum, party, stock_name, _text(stock.get("stock_item_guid")),
_text(stock.get("current_hsn")), stock.get("current_gst_rate"),
None, None, None, None, "", qty_text, qty, actual_text, actual_qty,
_text(stock.get("base_uom")), rate_text, rate, abs(raw_value), direction,
(qty * sign) if qty is not None else None, abs(raw_value) * sign, deemed,
),
)
inventory_count += 1
period_from = min(seen_dates) if seen_dates else str(date_from)
period_to = max(seen_dates) if seen_dates else str(date_to)
try:
db.execute(
"""UPDATE sync_state
SET period_from=?,period_to=?,max_voucher_alter_id=?,voucher_count=?,
voucher_ledger_count=?,inventory_count=?,last_sync_at=?,sync_mode=?
WHERE company_instance_id=?""",
(
period_from, period_to, max_alter, voucher_count, ledger_count, inventory_count,
now, "ODBC_MASTERS_XML_EXACT_PERIOD_TRANSACTIONS", instance,
),
)
except sqlite3.Error:
pass
db.commit()
except Exception:
db.rollback()
raise
finally:
db.close()
# Keep the GUID/name cache aligned with the corrected current mirror.
cache = self._cache_file(paths, effective_name, effective_guid)
shutil.copy2(paths.current_mirror, cache)
return {
"date_from": str(date_from),
"date_to": str(date_to),
"voucher_count": voucher_count,
"voucher_ledger_count": ledger_count,
"inventory_count": inventory_count,
"mirror_db_path": str(paths.current_mirror),
"mirror": self._summary(paths.current_mirror),
}
def company_db(self, client_id: int, company_name: str, company_guid: str = "") -> Path:
paths = self.paths(client_id)
requested_name = _text(company_name)
@@ -4076,7 +4076,64 @@ class AgentCommandProcessor:
)
mirror_company_name = str(mirror_result.get("company_name") or company_name).strip() or company_name
validation_warnings = list(mirror_result.get("validation_warnings") or [])
publish(status="running", percent=98, stage="Updating client accounting database", message="Refreshing the existing client .act transaction snapshot from the completed ODBC mirror. Master analysis remains available directly from the mirror without clearing any existing auxiliary masters.", mirror_db_path=str(mirror_result.get("mirror_db_path") or ""), warnings=validation_warnings)
# Tally ODBC Type:Voucher collections follow the period currently active
# in TallyPrime. The ERP full-export screen carries an explicit period,
# which is authoritative for a financial-year mirror. If ODBC returned
# a different period (or no rows for the requested dates), correct only
# the transaction tables through the existing read-only XML exporter.
# Masters remain the proven ODBC mirror data.
mirror_summary = (mirror_result.get("mirror") or {})
voucher_period = mirror_summary.get("voucher_period") or {}
odbc_from = str(voucher_period.get("from_date") or "").strip()
odbc_to = str(voucher_period.get("to_date") or "").strip()
requested_transactions = self.tally.mirror.transactions(
client_id, mirror_company_name, date_from, date_to, tally_guid
)
requested_vouchers = list(requested_transactions.get("vouchers") or [])
period_mismatch = False
try:
if odbc_from and odbc_to:
od_from = _dt_date.fromisoformat(odbc_from)
od_to = _dt_date.fromisoformat(odbc_to)
req_from = _dt_date.fromisoformat(date_from)
req_to = _dt_date.fromisoformat(date_to)
period_mismatch = od_from < req_from or od_to > req_to
except Exception:
period_mismatch = True
odbc_voucher_count = int((mirror_summary.get("counts") or {}).get("voucher") or 0)
needs_exact_period = period_mismatch or (odbc_voucher_count > 0 and not requested_vouchers)
exact_period_result = None
if needs_exact_period:
publish(
status="running", percent=97, stage="Correcting selected financial year",
message=(
"Tally ODBC returned voucher period "
+ ((odbc_from + " to " + odbc_to) if odbc_from and odbc_to else "outside the requested dates")
+ ". Re-reading transactions for exactly " + date_from + " to " + date_to
+ " using the existing read-only Tally exporter."
),
mirror_db_path=str(mirror_result.get("mirror_db_path") or ""),
warnings=validation_warnings,
)
exact_vouchers = self.tally.live.export_vouchers(mirror_company_name, date_from, date_to)
exact_period_result = self.tally.mirror.replace_transaction_period_from_vouchers(
client_id=client_id,
company_name=mirror_company_name,
company_guid=tally_guid,
date_from=date_from,
date_to=date_to,
vouchers=exact_vouchers,
)
mirror_result = {**mirror_result, **self.tally.mirror.status(client_id)}
mirror_summary = (mirror_result.get("mirror") or {})
validation_warnings.append(
"ODBC transaction period did not match the ERP-selected financial year; "
"transaction tables were corrected using the exact requested dates."
)
publish(status="running", percent=98, stage="Updating client accounting database", message="Refreshing the existing client .act transaction snapshot from the completed Accounting Mirror. Master analysis remains available directly from the mirror without clearing any existing auxiliary masters.", mirror_db_path=str(mirror_result.get("mirror_db_path") or ""), warnings=validation_warnings)
transactions = self.tally.mirror.transactions(client_id, mirror_company_name, date_from, date_to, tally_guid)
transaction_sync = self.store.replace_transaction_snapshot(
client_id, mapping={**mapping, "company_name": mirror_company_name}, transactions=transactions,
@@ -4091,6 +4148,7 @@ class AgentCommandProcessor:
finished_at_utc=datetime.now(timezone.utc).isoformat(), error="", warnings=validation_warnings,
rows=counts, mirror=mirror_summary,
import_diagnostics=mirror_result.get("import_diagnostics") or {},
exact_period_correction=exact_period_result or {},
mirror_db_path=str(mirror_result.get("mirror_db_path") or ""),
accounting_db_path=str(self.store.db_path(client_id)),
transaction_sync=transaction_sync,