"""Step F: Tally-importable XML export. Generates one per transaction for Gateway of Tally -> Import Data -> Vouchers. Sign conventions in Tally XML are fiddly: * Debit txn -> Payment voucher: expense ledger gets a negative AMOUNT (deemed-positive Yes), bank ledger gets positive AMOUNT (deemed-positive No). * Credit txn -> Receipt voucher: mirrored. * Contra -> Contra voucher. Per voucher the two AMOUNTs sum to zero (double entry), which the tests assert. """ from xml.sax.saxutils import escape from constants import CONTRA, voucher_type_for def _tally_date(iso): """'2026-04-01' -> '20260401' (YYYYMMDD). Falls back to digits-only.""" if not iso: return "" parts = iso.split("-") if len(parts) == 3: return f"{parts[0]}{parts[1]}{parts[2]}" return "".join(c for c in iso if c.isdigit()) def _voucher(txn, bank_ledger): """Build one block for a transaction.""" debit = txn.get("debit") or 0 credit = txn.get("credit") or 0 category = txn.get("category") or "Suspense / Unclassified" vtype = txn.get("voucher_type") or voucher_type_for(debit, credit, category) date = _tally_date(txn.get("date")) narration = escape(str(txn.get("narration") or "")) amount = debit if debit else credit cat_ledger = escape(category) bank = escape(bank_ledger) if vtype == "Receipt": # Money in: bank deemed-positive (debit side), income ledger credit side. bank_pos, bank_amt = "Yes", -amount other_pos, other_amt = "No", amount else: # Payment / Contra: money out. Expense ledger deemed-positive (negative # amount), bank ledger opposite. other_pos, other_amt = "Yes", -amount bank_pos, bank_amt = "No", amount return f""" {date} {vtype} {narration} {cat_ledger} {other_pos} {other_amt:.2f} {bank} {bank_pos} {bank_amt:.2f} """ def generate_tally_xml(transactions, bank_ledger="Bank Account", company="##SVCURRENTCOMPANY"): """Return a Tally import XML string for the given transactions.""" vouchers = "\n".join(_voucher(t, bank_ledger) for t in transactions) return f"""
Import Data
Vouchers {escape(company)} {vouchers}
""" def write_tally_xml(transactions, path, bank_ledger="Bank Account", company="##SVCURRENTCOMPANY"): xml = generate_tally_xml(transactions, bank_ledger, company) with open(path, "w", encoding="utf-8") as f: f.write(xml) return path