合同金额 20 万,财务登记了两笔回款,业务那边却说还差 8 万。我把 Excel 拉下来一看,问题不复杂:一笔回款填错了合同编号,另一笔被复制了两遍。表格里的公式倒是绿油油一片,没有任何报错。
这种账,我第一眼就不太信 Excel。
合同少的时候,做几列“合同金额、已回款、未回款”确实够用。合同一多,再碰上分期付款、作废回款、合同变更,靠表格维护迟早对不上。于是我用 Python 写了一个小型合同账务系统,功能不算花哨,但该卡住的地方都卡住了:
数据库我用了 SQLite。这个系统本来就是给小团队内部用,没必要一上来先搭 MySQL、Redis,再补一套部署脚本。工具系统最怕架子搭得很大,最后没人愿意维护。
先把两张表建出来:
import sqlite3
DB_FILE = "contract_book.db"
definit_db():
with sqlite3.connect(DB_FILE) as conn:
conn.executescript("""
CREATE TABLE IF NOT EXISTS contract (
id INTEGER PRIMARY KEY AUTOINCREMENT,
contract_no TEXT NOT NULL UNIQUE,
customer_name TEXT NOT NULL,
total_amount TEXT NOT NULL,
signed_date TEXT NOT NULL,
status TEXT NOT NULL DEFAULT '待回款'
);
CREATE TABLE IF NOT EXISTS receipt (
id INTEGER PRIMARY KEY AUTOINCREMENT,
receipt_no TEXT NOT NULL UNIQUE,
contract_no TEXT NOT NULL,
amount TEXT NOT NULL,
received_date TEXT NOT NULL,
remark TEXT DEFAULT '',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(contract_no)
REFERENCES contract(contract_no)
);
""")
金额字段我没有用 float。
财务系统里看到 19999.9999997 这种数,基本就可以准备返工了。Python 处理金额老老实实用 Decimal,数据库里按字符串保存,计算时再转换。
合同录入没什么复杂的,真正容易出问题的是回款。
一笔回款进来,我会依次检查:回款单号是否重复、合同是否存在、金额是否合法、累计回款是否超过合同金额。任何一个条件不对,事务直接回滚。
from decimal import Decimal, InvalidOperation
defadd_receipt(receipt_no, contract_no, amount, received_date, remark=""):
try:
current_amount = Decimal(str(amount))
except InvalidOperation:
raise ValueError("回款金额格式不正确")
if current_amount <= 0:
raise ValueError("回款金额必须大于0")
with sqlite3.connect(DB_FILE) as conn:
conn.row_factory = sqlite3.Row
conn.execute("BEGIN IMMEDIATE")
contract = conn.execute(
"""
SELECT contract_no, total_amount
FROM contract
WHERE contract_no = ?
""",
(contract_no,)
).fetchone()
if contract isNone:
raise ValueError(f"合同不存在:{contract_no}")
paid_row = conn.execute(
"""
SELECT amount
FROM receipt
WHERE contract_no = ?
""",
(contract_no,)
).fetchall()
paid_amount = sum(
(Decimal(row["amount"]) for row in paid_row),
Decimal("0")
)
total_amount = Decimal(contract["total_amount"])
if paid_amount + current_amount > total_amount:
raise ValueError(
f"本次入账后将超出合同金额,最多还能登记"
f"{total_amount - paid_amount}元"
)
conn.execute(
"""
INSERT INTO receipt(
receipt_no,
contract_no,
amount,
received_date,
remark
)
VALUES (?, ?, ?, ?, ?)
""",
(
receipt_no,
contract_no,
str(current_amount),
received_date,
remark
)
)
new_paid = paid_amount + current_amount
new_status = "已结清"if new_paid == total_amount else"回款中"
conn.execute(
"""
UPDATE contract
SET status = ?
WHERE contract_no = ?
""",
(new_status, contract_no)
)
这里有两个地方不能省。
一个是 receipt_no 必须加唯一约束。不要只在 Python 里先查一遍再插入,并发一上来,两次请求可能同时查到“不存在”,然后各插一条。最终兜底还得靠数据库。
另一个是 BEGIN IMMEDIATE。登记回款时,这笔合同的累计金额必须在同一个事务里读取和修改。否则两个财务同时操作,各自看到的“剩余可回款金额”都可能是旧值。
查询账务就简单多了,把合同金额和回款合计放在一条 SQL 里算:
defget_contract_account(contract_no):
with sqlite3.connect(DB_FILE) as conn:
conn.row_factory = sqlite3.Row
row = conn.execute(
"""
SELECT
c.contract_no,
c.customer_name,
c.total_amount,
c.status,
COALESCE(SUM(r.amount + 0), 0) AS paid_amount
FROM contract c
LEFT JOIN receipt r
ON r.contract_no = c.contract_no
WHERE c.contract_no = ?
GROUP BY c.id
""",
(contract_no,)
).fetchone()
if row isNone:
raise ValueError("合同不存在")
total = Decimal(row["total_amount"])
paid = Decimal(str(row["paid_amount"]))
return {
"合同编号": row["contract_no"],
"客户名称": row["customer_name"],
"合同金额": str(total),
"已回款": str(paid),
"未回款": str(total - paid),
"状态": row["status"]
}
这套系统没有做得很重。界面用 Flask 套几个表单就够了,合同和回款也可以导出成 Excel。
但账务系统有几条线不能退:金额不能用浮点数,回款不能重复,累计金额不能超过合同金额,修改记录不能悄无声息。
页面做丑一点还能继续用,账一旦算错,后面所有报表都只是一本正经地错下去。