PAYATHON 2026

使用 Python 从 Tally 导出收付款的 Agst Ref

支付老李

明确结论

“Agst Ref”不是付款或收款凭证上的普通字段,而是凭证分录下的账单分配信息。它通常位于以下层级:

Voucher
└── AllLedgerEntries
    └── BillAllocations
        ├── Name       ← Agst Ref 的具体编号
        ├── BillType   ← "Agst Ref"
        └── Amount

因此,普通 ODBC 查询即使能取得 $Date、$PartyLedgerName 和 $Amount,通常也不能把 “Agst Ref” 作为同一层级的字段直接查询。可以采用以下两种方式:

  1. 在 Tally 中加载自定义 TDL,将 BillAllocations 展开成 ODBC 表。
  2. 通过 XML 导出完整凭证及其账单分配,再用 Python 解析 BILLALLOCATIONS.LIST。

如果无法在 Tally 中部署 TDL,建议使用 XML 方案。

XML 导出方案

下面的请求会导出指定日期范围内的 Voucher,并取得每条 Ledger Entry 下的 Bill Allocation。

<ENVELOPE>
    <HEADER>
        <VERSION>1</VERSION>
        <TALLYREQUEST>Export</TALLYREQUEST>
        <TYPE>Collection</TYPE>
        <ID>PaymentReceiptVouchers</ID>
    </HEADER>

    <BODY>
        <DESC>
            <STATICVARIABLES>
                <SVEXPORTFORMAT>$$SysName:XML</SVEXPORTFORMAT>
                <SVFROMDATE TYPE="Date">20190401</SVFROMDATE>
                <SVTODATE TYPE="Date">20200331</SVTODATE>
                <SVCURRENTCOMPANY>Your Company Name</SVCURRENTCOMPANY>
            </STATICVARIABLES>

            <TDL>
                <TDLMESSAGE>
                    <COLLECTION NAME="PaymentReceiptVouchers">
                        <TYPE>Voucher</TYPE>
                        <FILTER>IsPaymentOrReceipt</FILTER>
                        <FETCH>
                            Date,
                            VoucherTypeName,
                            VoucherNumber,
                            PartyLedgerName,
                            AllLedgerEntries.LedgerName,
                            AllLedgerEntries.Amount,
                            AllLedgerEntries.BillAllocations.Name,
                            AllLedgerEntries.BillAllocations.BillType,
                            AllLedgerEntries.BillAllocations.Amount
                        </FETCH>
                    </COLLECTION>

                    <SYSTEM TYPE="Formulae" NAME="IsPaymentOrReceipt">
                        ($VoucherTypeName = "Payment") OR
                        ($VoucherTypeName = "Receipt")
                    </SYSTEM>
                </TDLMESSAGE>
            </TDL>
        </DESC>
    </BODY>
</ENVELOPE>

将下面的公司名称替换为实际名称:

<SVCURRENTCOMPANY>Your Company Name</SVCURRENTCOMPANY>

日期使用 YYYYMMDD 格式。

如果公司使用的是自定义 Voucher Type,例如 “Bank Payment”,而不是标准的 “Payment”,还要相应修改过滤条件。可以先导出 VoucherTypeName,确认实际名称后再添加过滤。

Python 请求和解析示例

Tally 默认 HTTP 服务通常使用 9000 端口,实际端口以当前 Tally 配置为准。

from decimal import Decimal
from xml.etree import ElementTree as ET

import requests


TALLY_URL = "http://localhost:9000"

xml_request = """
<ENVELOPE>
    <HEADER>
        <VERSION>1</VERSION>
        <TALLYREQUEST>Export</TALLYREQUEST>
        <TYPE>Collection</TYPE>
        <ID>PaymentReceiptVouchers</ID>
    </HEADER>

    <BODY>
        <DESC>
            <STATICVARIABLES>
                <SVEXPORTFORMAT>$$SysName:XML</SVEXPORTFORMAT>
                <SVFROMDATE TYPE="Date">20190401</SVFROMDATE>
                <SVTODATE TYPE="Date">20200331</SVTODATE>
                <SVCURRENTCOMPANY>Your Company Name</SVCURRENTCOMPANY>
            </STATICVARIABLES>

            <TDL>
                <TDLMESSAGE>
                    <COLLECTION NAME="PaymentReceiptVouchers">
                        <TYPE>Voucher</TYPE>
                        <FILTER>IsPaymentOrReceipt</FILTER>
                        <FETCH>
                            Date,
                            VoucherTypeName,
                            VoucherNumber,
                            PartyLedgerName,
                            AllLedgerEntries.LedgerName,
                            AllLedgerEntries.Amount,
                            AllLedgerEntries.BillAllocations.Name,
                            AllLedgerEntries.BillAllocations.BillType,
                            AllLedgerEntries.BillAllocations.Amount
                        </FETCH>
                    </COLLECTION>

                    <SYSTEM TYPE="Formulae" NAME="IsPaymentOrReceipt">
                        ($VoucherTypeName = "Payment") OR
                        ($VoucherTypeName = "Receipt")
                    </SYSTEM>
                </TDLMESSAGE>
            </TDL>
        </DESC>
    </BODY>
</ENVELOPE>
"""

response = requests.post(
    TALLY_URL,
    data=xml_request.encode("utf-8"),
    headers={"Content-Type": "text/xml; charset=utf-8"},
    timeout=60,
)
response.raise_for_status()

root = ET.fromstring(response.content)


def node_text(node, path, default=""):
    element = node.find(path)
    if element is None or element.text is None:
        return default
    return element.text.strip()


rows = []

for voucher in root.findall(".//VOUCHER"):
    voucher_date = node_text(voucher, "DATE")
    voucher_type = node_text(voucher, "VOUCHERTYPENAME")
    voucher_number = node_text(voucher, "VOUCHERNUMBER")
    default_party = node_text(voucher, "PARTYLEDGERNAME")

    for ledger_entry in voucher.findall("./ALLLEDGERENTRIES.LIST"):
        ledger_name = node_text(ledger_entry, "LEDGERNAME", default_party)

        for allocation in ledger_entry.findall("./BILLALLOCATIONS.LIST"):
            bill_type = node_text(allocation, "BILLTYPE")

            if bill_type.casefold() != "agst ref".casefold():
                continue

            reference = node_text(allocation, "NAME")
            amount_text = node_text(allocation, "AMOUNT", "0")

            try:
                amount = Decimal(amount_text.replace(",", ""))
            except Exception:
                amount = Decimal("0")

            rows.append(
                {
                    "Date": voucher_date,
                    "Agst Ref": reference,
                    "Party Name": ledger_name,
                    "Amount": amount,
                    "Voucher Type": voucher_type,
                    "Voucher Number": voucher_number,
                }
            )

for row in rows:
    print(
        row["Date"],
        row["Agst Ref"],
        row["Party Name"],
        row["Amount"],
    )

解析时需要检查:

<BILLTYPE>Agst Ref</BILLTYPE>

真正的参考编号位于同一节点的 NAME 中:

<NAME>19-20/1256</NAME>

不要把 BILLTYPE 本身误当成参考编号。

通过 ODBC 导出的做法

如果必须继续使用 ODBC,需要先加载自定义 TDL,将嵌套的 BillAllocations 展开成可查询集合。

TDL 示例如下:

[Collection: PaymentReceiptAgainstRefs]
    Type        : Voucher
    Walk        : AllLedgerEntries, BillAllocations
    Compute     : VoucherDate    : Date   : $$Owner:$$Owner:$Date
    Compute     : VoucherType    : String : $$Owner:$$Owner:$VoucherTypeName
    Compute     : VoucherNumber  : String : $$Owner:$$Owner:$VoucherNumber
    Compute     : PartyName      : String : $$Owner:$LedgerName
    Compute     : AgainstRef     : String : $Name
    Compute     : ReferenceType  : String : $BillType
    Compute     : ReferenceAmt   : Amount : $Amount
    Filter      : IsAgainstRef
    Is ODBC Table : Yes

[System: Formula]
    IsAgainstRef :
        ($BillType = "Agst Ref") AND
        (
            ($$Owner:$$Owner:$VoucherTypeName = "Payment") OR
            ($$Owner:$$Owner:$VoucherTypeName = "Receipt")
        )

加载 TDL 后,重启或重新加载 Tally,然后通过 ODBC 尝试执行以下查询:

SELECT
    $VoucherDate,
    $AgainstRef,
    $PartyName,
    $ReferenceAmt
FROM PaymentReceiptAgainstRefs

不同 Tally 或 TallyPrime 版本对自定义 ODBC Collection、Walk 和所有者字段访问的支持可能不同。如果集合已经加载,但在 ODBC 中看不到表,可以逐项检查:

  • Tally 的 ODBC 服务是否启用;
  • TDL 是否成功加载;
  • 当前版本是否支持 Is ODBC Table : Yes;
  • ODBC 驱动中是否能看到该集合名称;
  • 当前打开的公司和日期范围是否正确。

TDL 方案需要在目标 Tally 版本上实际验证。相比之下,XML 导出更容易查看原始层级,排查问题也更方便。

多个 Agst Ref 的处理

一张 Payment 或 Receipt Voucher 可能有多个 Ledger Entry,每个 Ledger Entry 下也可能有多个 Bill Allocation,其中还可能混有 New Ref、Agst Ref、On Account 等分配类型。

所以,一张凭证不一定只生成一行。报表通常应按“每个 Agst Ref 分配一行”的粒度输出,例如:

Date        Agst Ref     Party Name    Amount
20191125    19-20/1256   ABC           4145
20191125    19-20/1270   ABC           2000

如果把凭证总金额重复填入每个参考编号,汇总结果会重复计算。金额应优先取对应 BILLALLOCATIONS.LIST 下的 AMOUNT,而不是 Voucher 或 Ledger Entry 的总金额。

金额正负号

Tally 中金额的正负号取决于借贷方向。付款、收款以及不同 Ledger Entry 返回的符号可能不同,因此不要在解析时直接假定所有付款都是负数,或所有收款都是正数。

如果报表只需显示绝对值,可以先确认业务口径,再进行处理:

display_amount = abs(row["Amount"])

如果后续还要核对借贷平衡,应保留 XML 返回的原始符号,并单独生成显示金额。

备注:内容仅供参考。