Python+SQLite实现轻量级进销存,自动生成利润表

发布时间:2026/9/7 3:07:12
Python+SQLite实现轻量级进销存,自动生成利润表 “月底算利润”大概是所有小店老板和门店运营最头疼的事。卖货时笑嘻嘻一算账就发懵库存数量对不上进货价翻了好几次期间还有退货、折价、临时费用最后手工填进 Excel 的利润表自己都不敢信。本文分享一套用 Python 实现的轻量级小店进销存方案核心目标是让利润表自动生成只要管好进货、销售、费用三件事系统就能按时间段直接输出营业收入、成本、毛利和净利润。适合正在做门店管理系统、想练手 Python 数据库开发、或者单纯想把小卖部账目理清的读者代码全部可复制跟着做就能跑起来。1. 为什么小店的利润表需要程序化1.1 手工算利润表的真实痛点很多小店还在用最传统的方式记账进货记一个本子销售记一个本子月底再拿着计算器把收入、成本、费用逐项加一遍。这种方法在小规模时勉强能用但一旦商品种类超过几十种问题就非常明显进货批次多同一件商品不同批次进价不一样月底不知道该按哪个价格算成本。销售记录分散在收银台、微信转账、现金单据里统计收入容易漏。退货、报损、临期打折等特殊情况手工调整起来非常容易算错。利润表里的费用项往往靠拍脑袋归类缺少统一数据来源。月底汇总耗时数据不透明老板想看看“这个月到底赚没赚钱”至少要等一两天。这些痛点本质上是“业务数据没有形成结构化记录”所以任何计算都只能靠人工二次加工。只要把进货、销售、费用记录管理起来利润表就是一条 SQL 或者一段代码能算出来的结果。1.2 程序化方案能带来什么用程序来管理进销存并不是要做一个复杂的 ERP 系统而是先解决最核心的一条链路进货入库、销售出库、费用登记然后自动汇总利润。这样做的好处很直接数据录入一次报表自动生成月底不用再逐项手工加。库存可以随时查到哪个商品缺货、哪个商品滞销一目了然。利润计算口径统一不会因为换人记账就换一套算法。历史数据可追溯每个月都能对比收入、成本、费用变化。本文要实现的就是一个“能跑起来、能算利润、可继续扩展”的基础版本。技术选型用 Python 内置的 sqlite3 模块不依赖额外第三方库适合作为小店管理系统的基础骨架。2. 利润表背后的进销存计算逻辑2.1 进销存三个核心环节进销存三个字分别对应三块业务进采购入库。供应商把货送到店里系统记录进货单和进货明细库存数量增加同时记录该批次商品的成本价。销销售出库。顾客买走商品系统记录销售单和销售明细库存数量减少同时记录成交价。存库存管理。当前仓库里每个商品有多少数量价值是多少是进与销相减后的结果。利润表关心的不只是“存”更关心“进”与“销”的金额差。也就是说利润的核心是卖出去的商品销售收入是多少对应的成本是多少两者相减得到毛利再扣掉房租、水电、人工等费用才是净利润。2.2 利润表的关键公式一份最基础的小店利润表至少包含以下几个字段字段含义计算方式营业收入当期销售商品的总金额销售数量 × 销售单价逐笔累加营业成本当期售出商品的进货成本销售数量 × 进货成本逐笔累加毛利润销售环节的利润营业收入 - 营业成本费用支出房租、水电、人工等期间费用费用逐笔累加净利润最终到手的钱毛利润 - 费用支出这套公式看着简单实际实现时最容易出问题的点是“营业成本”。如果每个月手动算成本很容易被算成“当月进货总额”但正确的口径应当只统计“当月售出商品对应的进货成本”而不是当月进了多少货。举个例子月初进了 10000 元的货月底仓库还剩 3000 元商品当中只卖出了 7000 元的货那么营业成本应该是 7000 元而不是 10000 元。这个口径如果没有统一利润表很容易失真。2.3 成本核算方法的选择同样是卖一件商品进货批次不同、单价不同最终算出来的成本可能完全不同。常见的成本核算方法有三种先进先出法FIFO先购入的批次先销售成本按最早批次价格计算。移动加权平均法每进一次货重新计算平均成本销售时按最新平均成本出库。个别计价法每一件商品都标记具体成本卖哪件算哪件。对于小店场景最实用的是移动加权平均法。它不需要维护复杂的批次关系只需要在每次进货时更新商品的平均成本价销售时用当前平均成本作为出库成本。本文示例也会采用这种思路在进货时更新商品成本价在销售时把当前成本价写入销售明细后续商品进价变动不会影响历史订单的利润统计。3. 技术方案与数据库设计3.1 技术选型考虑到小店进销存系统需要在普通电脑甚至树莓派上运行同时希望代码少、易维护本文选择 Python 3.8 搭配 SQLite 数据库。SQLite 是 Python 内置的轻量级数据库无需安装服务端数据保存在一个本地文件中备份只需要复制文件即可非常适合单机版的小店管理系统。项目结构非常简洁shop_inventory/ ├── shop.db # SQLite 数据库文件运行后自动生成 └── shop_inventory.py # 主程序文件全部代码放在一个 Python 文件中等基础版本跑通后再按模块拆分也不迟。3.2 数据表结构说明整个系统需要 6 张表表名职责products商品档案维护商品名称、规格、单位、成本价、销售价、库存purchase_orders进货单主表记录供应商、进货日期、单号purchase_items进货明细表记录每张进货单对应的商品、数量、进价sale_orders销售单主表记录顾客、销售日期、单号sale_items销售明细表记录每张销售单对应的商品、数量、售价、成本价expenses费用表记录房租、水电、人工等支出进货单与进货明细、销售单与销售明细是一对多关系。每张进货单可以包含多种商品每张销售单也可以包含多种商品。这种设计更贴近真实业务而不是简单地把所有记录堆在一张表里。3.3 字段设计要点products 商品表字段设计CREATE TABLE products ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, spec TEXT DEFAULT , unit TEXT DEFAULT 件, cost_price REAL DEFAULT 0, sale_price REAL DEFAULT 0, stock INTEGER DEFAULT 0 );其中 cost_price 保存当前平均成本价每次进货后需要更新stock 保存当前库存数量sale_price 是建议零售价开销售单时可手工传入售价也可以自动带出。purchase_items 进货明细表字段设计CREATE TABLE purchase_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER, product_id INTEGER, quantity INTEGER, cost_price REAL );每次进货都要把当次 cost_price 记录在明细表里这样后续即使商品平均成本变化也能查到历史进货价。销售明细表 sale_items 同样需要记录 cost_price这个字段代表“销售那一刻商品的平均成本”保证后续不会因为商品成本变动而影响历史订单利润。4. 完整代码实现4.1 初始化数据库先写一个 init_db 函数负责建表。将以下代码保存为 shop_inventory.py# 文件路径shop_inventory.py import sqlite3 DB_PATH shop.db def get_conn(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row return conn def init_db(): conn get_conn() c conn.cursor() c.execute( CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, spec TEXT DEFAULT , unit TEXT DEFAULT 件, cost_price REAL DEFAULT 0, sale_price REAL DEFAULT 0, stock INTEGER DEFAULT 0 ) ) c.execute( CREATE TABLE IF NOT EXISTS purchase_orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_no TEXT, supplier TEXT, order_date TEXT ) ) c.execute( CREATE TABLE IF NOT EXISTS purchase_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER, product_id INTEGER, quantity INTEGER, cost_price REAL ) ) c.execute( CREATE TABLE IF NOT EXISTS sale_orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_no TEXT, customer TEXT, sale_date TEXT ) ) c.execute( CREATE TABLE IF NOT EXISTS sale_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER, product_id INTEGER, quantity INTEGER, sale_price REAL, cost_price REAL ) ) c.execute( CREATE TABLE IF NOT EXISTS expenses ( id INTEGER PRIMARY KEY AUTOINCREMENT, expense_date TEXT, category TEXT, amount REAL, remark TEXT ) ) conn.commit() conn.close() print(数据库初始化完成)这里要说明的是 get_conn 函数统一负责创建数据库连接同时设置 row_factory 为 sqlite3.Row这样查询结果可以像字典一样通过字段名取值代码可读性更高。4.2 商品管理与进货入库商品管理的第一步是添加商品档案def add_product(name, spec, unit, cost_price, sale_price, stock0): conn get_conn() c conn.cursor() c.execute( INSERT INTO products (name, spec, unit, cost_price, sale_price, stock) VALUES (?,?,?,?,?,?), (name, spec, unit, cost_price, sale_price, stock) ) conn.commit() conn.close() print(f商品已添加{name})接下来是进货入库逻辑。创建进货单时需要同时写入进货主表和进货明细表并更新商品的成本价和库存def create_purchase_order(order_no, supplier, order_date, items): conn get_conn() c conn.cursor() c.execute( INSERT INTO purchase_orders (order_no, supplier, order_date) VALUES (?,?,?), (order_no, supplier, order_date) ) order_id c.lastrowid total 0 for item in items: product_id item[product_id] quantity item[quantity] cost_price item[cost_price] c.execute( INSERT INTO purchase_items (order_id, product_id, quantity, cost_price) VALUES (?,?,?,?), (order_id, product_id, quantity, cost_price) ) c.execute( UPDATE products SET cost_price ?, stock stock ? WHERE id ?, (cost_price, quantity, product_id) ) total quantity * cost_price conn.commit() conn.close() print(f进货单 {order_no} 已保存进货金额{total})这段代码中有两点比较关键。一是 items 参数是一个列表每个元素是一个字典包含 product_id、quantity、cost_price 三个字段这样一张进货单可以录入多种商品二是货源入库时同步更新商品表的 cost_price相当于把这次进货价作为商品新的平均成本。为了演示方便进货单默认不做事务回滚处理。在实际生产代码中建议用 try/except 包裹整段写入逻辑出现异常时执行 conn.rollback()避免进货单主表和明细表数据不一致。4.3 销售开单与库存扣减销售开单的逻辑与进货类似但多一个库存校验步骤。如果库存不足整张销售单都不应该提交def create_sale_order(order_no, customer, sale_date, items): conn get_conn() c conn.cursor() c.execute( INSERT INTO sale_orders (order_no, customer, sale_date) VALUES (?,?,?), (order_no, customer, sale_date) ) order_id c.lastrowid total 0 for item in items: product_id item[product_id] quantity item[quantity] sale_price item[sale_price] row c.execute( SELECT stock, cost_price FROM products WHERE id ?, (product_id,) ).fetchone() if row is None or row[stock] quantity: conn.rollback() print(f商品ID {product_id} 库存不足销售单未保存) return False c.execute( INSERT INTO sale_items (order_id, product_id, quantity, sale_price, cost_price) VALUES (?,?,?,?,?), (order_id, product_id, quantity, sale_price, row[cost_price]) ) c.execute( UPDATE products SET stock stock - ? WHERE id ?, (quantity, product_id) ) total quantity * sale_price conn.commit() conn.close() print(f销售单 {order_no} 已保存销售金额{total}) return True这里最容易被忽略的就是 sale_items 表中的 cost_price 字段。它保存的并不是当前商品档案里的最新成本价而是开销售单那一刻商品的平均成本价。这样的好处是销售完成后即使后面又进货导致成本价变化也不会影响之前销售单的成本统计利润表的历史数据才算得准。4.4 费用记录与利润统计费用记录比较简单只需要记录日期、费用分类、金额和备注def add_expense(expense_date, category, amount, remark): conn get_conn() c conn.cursor() c.execute( INSERT INTO expenses (expense_date, category, amount, remark) VALUES (?,?,?,?), (expense_date, category, amount, remark) ) conn.commit() conn.close() print(f费用已记录{category} {amount} 元)利润统计是整个系统最核心的部分。它需要按日期区间汇总销售明细中的售价和成本def compute_profit(start_dateNone, end_dateNone): conn get_conn() c conn.cursor() if start_date and end_date: sale_orders c.execute( SELECT id FROM sale_orders WHERE sale_date BETWEEN ? AND ?, (start_date, end_date) ).fetchall() else: sale_orders c.execute(SELECT id FROM sale_orders).fetchall() order_ids [row[id] for row in sale_orders] total_sales 0 total_cost 0 if order_ids: placeholders ,.join(? for _ in order_ids) sql f SELECT quantity, sale_price, cost_price FROM sale_items WHERE order_id IN ({placeholders}) rows c.execute(sql, order_ids).fetchall() for row in rows: total_sales row[quantity] * row[sale_price] total_cost row[quantity] * row[cost_price] if start_date and end_date: expense_row c.execute( SELECT COALESCE(SUM(amount), 0) AS total FROM expenses WHERE expense_date BETWEEN ? AND ?, (start_date, end_date) ).fetchone() else: expense_row c.execute( SELECT COALESCE(SUM(amount), 0) AS total FROM expenses ).fetchone() total_expense expense_row[total] gross_profit total_sales - total_cost net_profit gross_profit - total_expense conn.close() return { total_sales: total_sales, total_cost: total_cost, gross_profit: gross_profit, total_expense: total_expense, net_profit: net_profit }这个函数的关键点在于营业成本来源于销售明细中的 cost_price而不是来源于商品档案里的当前成本价。这样统计某段时间利润时成本与销售单一一对应不会出现“先把货卖了后补进货单导致历史利润变化”的问题。4.5 生成利润表有了 compute_profit 之后再写一个 generate_profit_report 函数负责格式化输出def generate_profit_report(start_dateNone, end_dateNone): data compute_profit(start_date, end_date) print( 利润表 ) print(f统计区间{start_date or 全部} ~ {end_date or 全部}) print(f营业收入{data[total_sales]:.2f}) print(f营业成本{data[total_cost]:.2f}) print(f毛利润{data[gross_profit]:.2f}) print(f费用支出{data[total_expense]:.2f}) print(f净利润{data[net_profit]:.2f})到这里一个可以使用的进销存利润统计核心逻辑已经完成。接下来用示例数据验证。5. 运行演示与结果分析5.1 录入示例数据为了验证程序逻辑我们模拟一个最简单的场景先添加两种商品然后进货、销售、记录费用最后生成 2025 年 1 月的利润表。if __name__ __main__: init_db() add_product(农夫山泉 550ml, , 瓶, 1.2, 2.0, stock100) add_product(可口可乐 330ml, , 罐, 2.0, 3.5) create_purchase_order( PO20250110001, 本地供应商A, 2025-01-10, [ {product_id: 1, quantity: 200, cost_price: 1.2}, {product_id: 2, quantity: 100, cost_price: 2.0}, ] ) create_sale_order( SO20250111001, 散客, 2025-01-11, [ {product_id: 1, quantity: 50, sale_price: 2.0}, {product_id: 2, quantity: 20, sale_price: 3.5}, ] ) add_expense(2025-01-12, 水电费, 50, 1月水电费) generate_profit_report(2025-01-01, 2025-01-31)运行方式python shop_inventory.py5.2 查看运行结果正常执行后控制台输出如下数据库初始化完成 商品已添加农夫山泉 550ml 商品已添加可口可乐 330ml 进货单 PO20250110001 已保存进货金额440.0 销售单 SO20250111001 已保存销售金额170.0 费用已记录水电费 50 元 利润表 统计区间2025-01-01 ~ 2025-01-31 营业收入170.00 营业成本100.00 毛利润70.00 费用支出50.00 净利润20.00营业收入 170 元由两笔销售组成50 瓶农夫山泉 × 2.0 元 100 元 20 罐可口可乐 × 3.5 元 70 元营业成本 100 元对应销售明细中的成本价50 瓶农夫山泉 × 1.2 元 60 元 20 罐可口可乐 × 2.0 元 40 元毛利 70 元再减去水电费 50 元净利润 20 元。5.3 结果背后的业务含义从结果可以看出利润表并不是“月底把收入加起来减去进货总额”而是严格按“当期卖出商品的收入与成本”来统计。进货 440 元的货本月只卖出了成本 100 元的部分还有 340 元成本对应的库存仍在仓库里。只有当这些库存后续卖出时它的成本才会进入销售发生月份的利润表。这套逻辑对门店经营的意义很大老板随时可以知道这个月真实卖货赚了多少而不是被“进了很多货”干扰判断。如果只按进货金额算成本很容易出现“月底账上剩一堆货但利润表却显示亏了很多”的假象。6. 常见问题与排查思路问题现象常见原因解决思路销售时提示库存不足商品库存没初始化或者进货数据没录入先执行进货入库再开销售单检查商品表 stock 字段利润表中的营业收入为 0销售单日期不在统计区间内或者销售明细为空查询 sale_orders 表记录核对日期参数营业成本明显偏低销售时商品 cost_price 为 0或商品档案没有成本价检查进货时是否更新了 products.cost_price核对销售明细 cost_price费用统计不完整有些费用没有录入或者日期区间不匹配检查 expenses 表确认每笔费用都有正确的 expense_date同一商品后期进价变化历史利润被改动了销售明细未保存当时的成本价确认 sale_items 表中的 cost_price 是在开销售单时写入而不是统计时取商品表当前成本数据库文件损坏或误删没有定期备份复制 shop.db 文件即可完成备份可设置每日定时任务自动备份实际排查时建议先从数据库层面验证数据完整性不要直接看利润结果。可以手动查询销售明细和费用表-- 查询指定日期的销售情况 SELECT p.name, si.quantity, si.sale_price, si.cost_price, si.quantity * si.sale_price AS 收入, si.quantity * si.cost_price AS 成本 FROM sale_items si JOIN products p ON p.id si.product_id JOIN sale_orders so ON so.id si.order_id WHERE so.sale_date BETWEEN 2025-01-01 AND 2025-01-31;用这种方式核对单笔明细能更快定位是数据录入问题还是统计逻辑问题。7. 最佳实践与工程建议7.1 数据录入规范进销存系统最怕数据脏。即使程序算得再准录入的时候数量和单价填错了最终利润表也会失真。建议在录入环节做基础校验数量必须大于 0售价和成本价必须大于等于 0日期格式统一为 yyyy-mm-dd。同时给商品名称建立命名规则例如“品牌 规格 单位”避免同一商品出现多个相似名称导致重复建档。进货单和销售单的编号建议有规则比如“PO20250110001”表示进货单、日期为 2025-01-10、当天第一单。这样人工在数据库里排查问题时能快速识别单据类型和时间。7.2 成本核算与库存一致性本文示例采用“移动加权平均”简化版本每次进货更新商品表 cost_price销售时把当前 cost_price 写入销售明细。这种方式实现简单适合大多数小店场景。但要注意如果经常存在跨期进货和销售最好把成本计算函数独立封装并在月底做一次库存盘点。更严谨的做法是维护一张“库存变动流水表”每次进货、销售、退货、报损都记录一条流水包含变动类型、商品ID、变动数量、成本单价、关联单据号。这样月底可以基于流水表重新推算每个商品的移动加权平均成本并且能追溯任何一天的库存价值。本文为了控制篇幅没有加入流水表但不影响理解整体利润计算逻辑。7.3 安全备份与权限SQLite 数据库就是一个单文件备份最容易实现。建议每天营业结束后把 shop.db 复制到另一个目录或网盘。更规范一点可以写一个备份脚本保留最近 30 天的备份文件#!/bin/bash # 文件路径backup.sh BACKUP_DIR/home/user/shop_backup mkdir -p $BACKUP_DIR cp shop.db $BACKUP_DIR/shop_$(date %Y%m%d).db find $BACKUP_DIR -name shop_*.db -mtime 30 -delete如果系统开放给员工录入数据建议至少区分管理员和普通录入员两种角色。管理员能修改商品成本价、删除订单普通录入员只能新增进货单和销售单。虽然本文示例没有做权限模块但在实际部署时这是很重要的一环。7.4 扩展方向这个基础版本跑通之后还可以继续扩展以下功能商品分类与条码管理扫描枪直接录入商品。供应商和客户档案管理带往来对账功能。退货单和报损单库存回补并冲减当期收入或成本。图表展示按月生成收入和利润趋势图。导出 Excel 报表方便财务复核。每一步扩展都不需要推翻现有表结构只需要在现有基础上增加表和业务函数即可。8. 总结这套小店进销存方案的核心价值是把利润计算从“月底人工统计”变成“日常录入、随时生成”。从数据库表设计到完整代码实现再到运行演示整条链路是闭环的。现在再看到月底利润表只需要运行 generate_profit_report(2025-01-01, 2025-01-31)收入、成本、毛利、费用、净利润全部都会自动算好。如果你正在开发自己的门店管理系统建议先按文章思路把进货、销售、费用三条数据流理顺再逐步补充库存流水、退货和权限等功能。下一步可以尝试把界面换成 Web 管理系统或者给这个小工具加上 Spring Boot 后端接口让手机端也能录入数据。总之先把最核心的利润表自动化跑通后面的扩展就都顺了。