Spaces:
Sleeping
Sleeping
| """Step F: Tally-importable XML export. | |
| Generates one <VOUCHER> 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 <TALLYMESSAGE> 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""" <TALLYMESSAGE xmlns:UDF="TallyUDF"> | |
| <VOUCHER VCHTYPE="{vtype}" ACTION="Create"> | |
| <DATE>{date}</DATE> | |
| <VOUCHERTYPENAME>{vtype}</VOUCHERTYPENAME> | |
| <NARRATION>{narration}</NARRATION> | |
| <ALLLEDGERENTRIES.LIST> | |
| <LEDGERNAME>{cat_ledger}</LEDGERNAME> | |
| <ISDEEMEDPOSITIVE>{other_pos}</ISDEEMEDPOSITIVE> | |
| <AMOUNT>{other_amt:.2f}</AMOUNT> | |
| </ALLLEDGERENTRIES.LIST> | |
| <ALLLEDGERENTRIES.LIST> | |
| <LEDGERNAME>{bank}</LEDGERNAME> | |
| <ISDEEMEDPOSITIVE>{bank_pos}</ISDEEMEDPOSITIVE> | |
| <AMOUNT>{bank_amt:.2f}</AMOUNT> | |
| </ALLLEDGERENTRIES.LIST> | |
| </VOUCHER> | |
| </TALLYMESSAGE>""" | |
| 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"""<ENVELOPE> | |
| <HEADER><TALLYREQUEST>Import Data</TALLYREQUEST></HEADER> | |
| <BODY><IMPORTDATA> | |
| <REQUESTDESC> | |
| <REPORTNAME>Vouchers</REPORTNAME> | |
| <STATICVARIABLES><SVCURRENTCOMPANY>{escape(company)}</SVCURRENTCOMPANY></STATICVARIABLES> | |
| </REQUESTDESC> | |
| <REQUESTDATA> | |
| {vouchers} | |
| </REQUESTDATA> | |
| </IMPORTDATA></BODY> | |
| </ENVELOPE>""" | |
| 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 | |