当前位置:首页>python>用Python写了一个合同账务系统(附源码)

用Python写了一个合同账务系统(附源码)

  • 2026-08-24 13:39:41
用Python写了一个合同账务系统(附源码)
合同金额 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。

但账务系统有几条线不能退:金额不能用浮点数,回款不能重复,累计金额不能超过合同金额,修改记录不能悄无声息。

页面做丑一点还能继续用,账一旦算错,后面所有报表都只是一本正经地错下去。

最新文章

随机文章