statementsetu / tally_export.py
perceptron01's picture
Upload 16 files
10ec275 verified
Raw
History Blame Contribute Delete
3.31 kB
"""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