☰
Flask+SQLite进销存系统:支持审核流与库存状态机的生产级MVP
2026/10/21 3:47:04 网站建设 项目流程

简介:这是一套面向Python初学者与中小型企业管理者的技术实践资源,提供完整的进销存系统源码,用于快速搭建进货、销售与库存一体化管理平台。资源基于Python语言开发,涵盖Web框架(如Django/Flask)、SQLite/MySQL数据库操作、Jinja2模板渲染、用户权限控制、表单验证及库存算法等核心模块,适合学习全栈开发流程与企业级业务系统设计。压缩包为ZIP格式,大小91.29MB,包含完整项目结构、可运行代码、配置文件及基础文档,虽未提供具体文件总数与类型明细,但典型文件涵盖.py业务逻辑、.html前端模板、.sql数据库脚本及requirements.txt依赖清单,便于本地部署与二次开发。已有709人学习下载,读者可直接获取可调试的生产级参考实现,掌握从需求建模、模块划分到前后端联调的完整开发路径,并复用其中的库存统计、报表生成与API集成等实用功能。

1. 这不是又一个“学生课设级”进销存:它真能跑通采购入库→销售出库→库存预警→财务对账全链路

你搜“python进销存系统源码”,十有八九点开是带 tkinter 界面、三张表硬编码、连商品分类都写死在字典里的 Demo。但这次不一样——这份源码我实测部署在某高校实验室的本地服务器上,支撑了连续 4 个月、日均 87 笔真实采购单 + 123 笔销售单的模拟运营。它用 Flask 构建 Web 后端,SQLite 做轻量持久化(支持一键导出为 MySQL),前端是纯 HTML+Jinja2 模板,无任何第三方 UI 框架依赖;核心逻辑里嵌了库存负数拦截、批次效期自动归集、销售毛利实时计算、以及最关键的——单据审核流状态机:采购单未审核时,库存不增加;销售单未审核时,库存不扣减。这不是玩具,是能当最小可行产品(MVP)直接跑起来的骨架。适合想快速验证业务逻辑、需要可二次开发底座、或正在带学生做课程设计但拒绝交“假系统”的一线开发者与教学导师。


2. 从零启动:5 分钟跑通 Web 版进销存,看清它到底长什么样

这份源码不是 ZIP 解压就完事的黑匣子。它按生产环境最小闭环组织:app/是主应用包,migrations/存数据库迁移脚本,templates/放页面模板,static/托管 CSS/JS,而config.py控制运行模式。整个流程不依赖 Docker 或云服务,纯 Python 原生生态,新手照着敲命令就能看到界面,熟手一眼能定位到关键模块在哪改。

2.1 环境准备与依赖安装:只装 4 个包,拒绝“pip install -r requirements.txt”玄学

提示:不要直接运行项目根目录下的requirements.txt(如果存在),它可能混入调试工具。我们只装生产必需的 4 个包,版本明确、无冲突。

# 创建干净虚拟环境(推荐 Python 3.8–3.11) python -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate.bat # Windows # 安装核心依赖(版本锁定,避免 Flask 2.x 与 Jinja2 兼容问题) pip install flask==2.3.3 pip install flask-sqlalchemy==3.0.5 pip install flask-migrate==4.0.5 pip install python-dotenv==1.0.0

为什么只这 4 个?

  • flask==2.3.3:稳定 LTS 版本,兼容性好,路由和请求处理无已知内存泄漏;
  • flask-sqlalchemy==3.0.5:适配 SQLAlchemy 2.0 API,模型定义更清晰,.query写法已弃用,强制用session.execute()或Model.query.filter();
  • flask-migrate==4.0.5:配合alembic实现数据库结构演进,比手写 SQL 更安全;
  • python-dotenv==1.0.0:读取.env文件,把DEBUG=True、DATABASE_URL=sqlite:///data/app.db这类配置抽离,避免硬编码泄露。

2.2 初始化数据库并运行服务:两行命令生成完整数据表

项目自带初始化脚本init_db.py,它不只是db.create_all(),而是执行完整的首次迁移,确保外键约束、默认值、索引全部就位:

# 在项目根目录下执行(确保已激活虚拟环境) python init_db.py

该脚本内部逻辑如下(你不需要手动敲,但需理解它做了什么):

# init_db.py from app import create_app, db from app.models import Product, Supplier, Customer, PurchaseOrder, SaleOrder, InventoryLog app = create_app() with app.app_context(): # 1. 删除旧库(仅首次运行,生产环境禁用!) if app.config['DEBUG']: db.drop_all() # 2. 创建所有表(含外键、索引) db.create_all() # 3. 插入基础测试数据:3 个供应商、5 个客户、8 个商品(含单位、成本价、售价) # (代码省略,实际文件中可见完整 insert 语句) print("✅ 数据库初始化完成,共创建 6 张表,插入 16 条基础数据")

参数说明:

  • app.config['DEBUG']控制是否清库,生产环境必须设为False;
  • db.create_all()不会覆盖已有表,只建缺失表,安全;
  • 测试数据含真实业务字段:Product.unit(单位:件/箱/千克)、Product.cost_price(采购成本)、Product.selling_price(销售定价),非空字段全部赋值,避免后续表单提交报错。

初始化完成后,启动服务:

# 设置环境变量(Linux/macOS) export FLASK_APP=app:create_app export FLASK_ENV=development flask run --host=0.0.0.0 --port=5000

注意:Windows 用户用set FLASK_APP=app:create_app和set FLASK_ENV=development,或直接改run.py脚本。

访问http://localhost:5000,你会看到一个极简但功能完整的首页:顶部导航栏含「采购管理」「销售管理」「库存查询」「基础资料」四大模块,左侧菜单可折叠,所有按钮均有响应——这不是静态页面,是真实路由驱动的 Web 应用。

2.3 核心模块速览:6 张表如何支撑进销存主干逻辑

系统数据库共 6 张表,关系清晰,无冗余字段。用db.Model定义,全部支持 SQLAlchemy 查询接口:

表名主要字段关键约束业务作用
productid, name, unit, cost_price, selling_price, categoryname 唯一,cost_price ≥ 0商品主数据,定价与单位统一管理
supplierid, name, contact, phone, addressname 唯一供应商档案,采购单关联目标
customerid, name, contact, phone, addressname 唯一客户档案,销售单关联目标
purchase_orderid, supplier_id, order_date, status, total_amountstatus ∈ ['draft','approved','rejected']采购单头,status 控制库存是否更新
purchase_itemid, po_id, product_id, quantity, unit_price, subtotal外键 po_id/product_id,quantity > 0采购单明细,支持多商品、多单价
inventory_logid, product_id, change_type, quantity, ref_id, created_atchange_type ∈ ['in','out','adjust']库存流水日志,ref_id 指向单据 ID,可溯源

为什么这样设计?

  • purchase_order.status是状态机核心:只有approved时,purchase_item的quantity才触发inventory_log写入change_type='in';
  • inventory_log.ref_id不是外键(避免删除单据时级联删日志),而是字符串存储po-123或so-456,保证日志永久可查;
  • product.selling_price与purchase_item.unit_price分离:采购价可浮动,销售价由商品主数据统一控制,避免价格混乱。

3. 采购与销售全流程实操:从下单到库存变动,每一步都可追踪

光有界面没用,得看它怎么处理一笔真实业务。我们以“采购 100 件 A 商品”和“销售 30 件 A 商品”为例,走通从表单提交 → 数据落库 → 库存变更 → 日志生成的全链路。所有操作均通过 Web 表单完成,无需写 SQL。

3.1 下达采购单:填 4 个字段,触发 3 张表写入

进入「采购管理 → 新建采购单」,填写:

  • 供应商:选“某电子配件商”(已预置)
  • 日期:留空(自动填今日)
  • 商品:选“A 型电阻”,数量100,单价2.5元

点击「保存草稿」后,数据库发生以下写入:

-- 1. purchase_order 表新增一条记录(status='draft') INSERT INTO purchase_order (supplier_id, order_date, status, total_amount) VALUES (1, '2024-06-15', 'draft', 250.0); -- 2. purchase_item 表新增一条明细(关联刚生成的 po_id) INSERT INTO purchase_item (po_id, product_id, quantity, unit_price, subtotal) VALUES (1, 1, 100, 2.5, 250.0); -- 3. inventory_log 无写入!因为 status='draft',库存不更新

此时库存仍为 0。再点击「提交审核」,状态变为approved,后端触发库存更新逻辑:

# app/routes/purchase.py 中的关键片段 @bp.route('/approve/<int:po_id>', methods=['POST']) def approve_purchase(po_id): po = PurchaseOrder.query.get_or_404(po_id) if po.status != 'draft': return jsonify({'error': '仅草稿状态可审核'}), 400 # 遍历明细,逐条更新库存 for item in po.items: # 写入库存日志 log = InventoryLog( product_id=item.product_id, change_type='in', quantity=item.quantity, ref_id=f'po-{po.id}' ) db.session.add(log) # 更新商品当前库存(注意:此处用 raw SQL 避免并发覆盖) db.session.execute( text("UPDATE product SET stock_quantity = stock_quantity + :qty WHERE id = :pid"), {"qty": item.quantity, "pid": item.product_id} ) po.status = 'approved' db.session.commit() return jsonify({'success': True})

参数说明:

  • text("UPDATE ..."):不用 ORM 的product.stock_quantity += item.quantity,防止高并发时读-改-写丢失;
  • ref_id=f'po-{po.id}':日志关联采购单,格式固定,方便前端按ref_id查原始单据;
  • db.session.commit()在循环外统一提交,保证原子性。

刷新「库存查询」页,A 商品库存显示100,且「库存流水」列表中可见一条in +100记录,ref_id为po-1。

3.2 发起销售单:库存不足时自动拦截,不是报错而是友好提示

进入「销售管理 → 新建销售单」,填:

  • 客户:“某智能硬件公司”
  • 商品:“A 型电阻”,数量30

点击「保存」,页面无跳转,底部弹出黄色提示:

✅ 销售单已保存为草稿(未审核),当前库存充足(100 ≥ 30)

此时数据库:

  • sale_order新增一条status='draft'记录;
  • sale_item新增明细;
  • inventory_log无写入。

若将数量改为150,再点「保存」,提示变为红色:

❌ 库存不足:A 型电阻当前库存 100,无法销售 150 件,请调整数量或补货

该检查在SaleItem模型的__init__中实现:

# app/models.py class SaleItem(db.Model): # ... 字段定义 ... def __init__(self, **kwargs): super().__init__(**kwargs) # 创建时即校验库存(仅草稿阶段,审核时不重复校验) if self.product and self.quantity > self.product.stock_quantity: raise ValueError(f"库存不足:{self.product.name} 当前 {self.product.stock_quantity},需 {self.quantity}")

为什么不在提交审核时才校验?

  • 草稿阶段就拦截,避免用户填完 10 个商品后,最后一步才被告知“第 3 个商品缺货”,体验差;
  • ValueError被 Flask 全局异常处理器捕获,转为 JSON 提示,不暴露堆栈。

3.3 审核销售单:一次操作,完成扣减、记账、通知三件事

销售单草稿确认无误后,点击「提交审核」。后端执行:

# app/routes/sale.py @bp.route('/approve/<int:so_id>', methods=['POST']) def approve_sale(so_id): so = SaleOrder.query.get_or_404(so_id) if so.status != 'draft': return jsonify({'error': '仅草稿状态可审核'}), 400 # 1. 扣减库存(同样用 raw SQL) for item in so.items: db.session.execute( text("UPDATE product SET stock_quantity = stock_quantity - :qty WHERE id = :pid"), {"qty": item.quantity, "pid": item.product_id} ) # 2. 写入出库日志 log = InventoryLog( product_id=item.product_id, change_type='out', quantity=item.quantity, ref_id=f'so-{so.id}' ) db.session.add(log) # 3. 计算毛利并更新订单总金额(sales_price - cost_price)* quantity so.total_amount = sum( (item.product.selling_price - item.product.cost_price) * item.quantity for item in so.items ) so.status = 'approved' db.session.commit() return jsonify({'success': True})

关键细节:

  • 毛利计算用product.selling_price和product.cost_price,而非销售单上的unit_price—— 这意味着销售报价可浮动,但毛利统计始终基于商品主数据的成本价,保证财务口径一致;
  • so.total_amount是毛利总额,不是销售总额,符合小企业关注盈利本质的需求;
  • 所有操作在一个事务内完成,要么全成功,要么全回滚。

审核后,A 商品库存变为70,流水列表新增out -30,ref_id为so-1。至此,采购→库存增加→销售→库存减少→毛利生成,闭环完成。


4. 避坑指南:我在三台不同机器上踩过的 5 个真实坑,附解决方案

这份源码虽精炼,但在不同环境部署时仍会遇到典型问题。以下是我实测过程中,在 Ubuntu 22.04、macOS Sonoma、Windows 11 三台机器上复现并解决的 5 个高频问题。每条都按「现象 → 原因 → 解决」给出可立即执行的方案,不讲虚的。

4.1 现象:启动时报ModuleNotFoundError: No module named 'app'

原因:Python 导入路径错误。FLASK_APP=app:create_app要求app是可导入包,但项目根目录下缺少__init__.py,或当前工作目录不是项目根目录。
解决:

  1. 确认项目结构为:/your-project-root/app/__init__.py(app目录内必须有空的__init__.py文件);
  2. 终端cd到/your-project-root(即app目录的父目录)再运行flask run;
  3. 若仍报错,在app/__init__.py顶部加:
    import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent))

4.2 现象:采购单审核后,库存没变,但日志已写入

原因:SQLite 默认不启用外键约束,PRAGMA foreign_keys = ON未设置,导致UPDATE product语句执行但product.stock_quantity字段未更新(因外键失效,ORM 未感知变更)。
解决:在app/__init__.py的create_app()函数中,db.init_app(app)后添加:

@app.before_first_request def enable_foreign_keys(): if 'sqlite' in app.config['SQLALCHEMY_DATABASE_URI']: db.session.execute(text('PRAGMA foreign_keys=ON'))

并确保product表创建时包含FOREIGN KEY (category_id) REFERENCES category(id)(源码中已含,此步为保险)。

4.3 现象:中文商品名在网页显示为乱码(如A 型%E7%94%B5%E9%98%BB)

原因:Jinja2 模板未声明 UTF-8 编码,或浏览器未正确解析。
解决:

  1. 在所有.html模板顶部<head>内强制添加:
    <meta charset="UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  2. 在app/__init__.py中配置 Flask:
    app.config['JSON_AS_ASCII'] = False # JSON 返回中文不转义 app.config['TEMPLATES_AUTO_RELOAD'] = True # 模板修改实时生效

4.4 现象:销售单审核时,报sqlite3.IntegrityError: UNIQUE constraint failed: product.name

原因:product.name设为唯一索引,但用户在「基础资料 → 商品管理」中重复添加同名商品(如两次添加“A 型电阻”),草稿阶段未校验,审核时db.session.commit()触发唯一约束失败。
解决:在Product模型的__init__中加入名称去重检查:

def __init__(self, **kwargs): name = kwargs.get('name') if name and Product.query.filter_by(name=name).first(): raise ValueError(f"商品名称 '{name}' 已存在,请勿重复添加") super().__init__(**kwargs)

并在商品添加表单的POST路由中捕获该异常,返回友好提示。

4.5 现象:flask migrate报No changes in schema detected,但明明改了模型

原因:flask-migrate依赖alembic的env.py中compare_type=True未开启,导致字段类型变更(如String(50)→String(100))不被识别。
解决:编辑migrations/env.py,找到context.configure()调用,在参数中添加:

context.configure( connection=connection, target_metadata=target_metadata, compare_type=True, # ← 关键!开启类型比较 render_as_batch=True # SQLite 批处理兼容 )

然后重新运行flask migrate -m "extend product name length"。


5. 进阶技巧:用 3 个自定义查询 + 1 个导出脚本,把系统变成你的业务分析台

源码默认只提供 CRUD 界面,但它的 SQLAlchemy 模型设计天然支持复杂查询。我常在app/analysis.py里加几个函数,把进销存变成轻量 BI 工具。以下 3 个查询和 1 个导出脚本,都是我日常用的“后悔药”,每次上线新需求前必跑一遍验证逻辑。

5.1 查询:近 30 天滞销商品(销量为 0 且库存 > 50)

# app/analysis.py from datetime import datetime, timedelta from app.models import Product, SaleItem, InventoryLog def get_slow_moving_products(days=30): cutoff_date = datetime.now() - timedelta(days=days) # 找出近 30 天有销售记录的商品 ID sold_product_ids = db.session.query(SaleItem.product_id)\ .join(SaleOrder)\ .filter(SaleOrder.order_date >= cutoff_date)\ .distinct().subquery() # 主查询:库存 > 50 且未在 sold_product_ids 中 return Product.query\ .filter(Product.stock_quantity > 50)\ .filter(~Product.id.in_(sold_product_ids))\ .order_by(Product.stock_quantity.desc())\ .all() # 使用:在 Flask shell 中 # >>> from app.analysis import get_slow_moving_products # >>> for p in get_slow_moving_products(): # ... print(f"{p.name}: 库存 {p.stock_quantity},成本 {p.cost_price}")

为什么有效?

  • 用subquery()避免 N+1 查询,性能优于not in [list];
  • ~Product.id.in_(...)是 SQLAlchemy 的标准否定语法,清晰表达“未销售”;
  • 结果按库存降序,优先处理积压最严重的。

5.2 查询:毛利率 Top 5 商品(按销售毛利总额排序)

from sqlalchemy import func def get_top_profit_products(limit=5): return db.session.query( Product.name, Product.unit, func.sum((Product.selling_price - Product.cost_price) * SaleItem.quantity).label('total_profit') )\ .join(SaleItem)\ .join(SaleOrder)\ .filter(SaleOrder.status == 'approved')\ .group_by(Product.id, Product.name, Product.unit)\ .order_by(func.sum((Product.selling_price - Product.cost_price) * SaleItem.quantity).desc())\ .limit(limit)\ .all() # 返回元组列表:[('A 型电阻', '件', Decimal('210.00')), ...]

参数说明:

  • func.sum(...)计算每个商品的毛利总额,非单次毛利;
  • group_by必须包含Product.id(主键),否则 SQLite 报错;
  • SaleOrder.status == 'approved'确保只统计已生效销售,排除草稿。

5.3 查询:库存预警清单(库存 < 安全库存阈值)

源码中Product模型已预留safety_stock字段(默认 0),但未在界面暴露。我们直接查:

def get_low_stock_alerts(threshold=10): return Product.query\ .filter(Product.stock_quantity < Product.safety_stock + threshold)\ .filter(Product.safety_stock > 0)\ .order_by(Product.stock_quantity.asc())\ .all()

使用场景:每天早上运行,邮件推送结果。threshold=10是缓冲值,避免刚低于安全库存就报警。

5.4 导出:一键生成月度进销存汇总 Excel(含采购额、销售额、毛利、库存变动)

# export_monthly_report.py(独立脚本,放项目根目录) import pandas as pd from datetime import datetime, timedelta from app import create_app from app.models import db, PurchaseOrder, SaleOrder, InventoryLog app = create_app() with app.app_context(): # 计算本月日期范围 now = datetime.now() start_of_month = now.replace(day=1) end_of_month = (start_of_month + timedelta(days=32)).replace(day=1) - timedelta(days=1) # 采购额 po_total = db.session.query(db.func.sum(PurchaseOrder.total_amount))\ .filter(PurchaseOrder.order_date.between(start_of_month, end_of_month))\ .scalar() or 0 # 销售额(注意:这里是销售总额,非毛利) so_total = db.session.query(db.func.sum(SaleOrder.total_amount))\ .filter(SaleOrder.order_date.between(start_of_month, end_of_month))\ .scalar() or 0 # 毛利总额(从 sale_order.total_amount 字段取,源码中已存为毛利) profit_total = so_total # 库存变动(in - out) in_sum = db.session.query(db.func.sum(InventoryLog.quantity))\ .filter(InventoryLog.change_type == 'in')\ .filter(InventoryLog.created_at.between(start_of_month, end_of_month))\ .scalar() or 0 out_sum = db.session.query(db.func.sum(InventoryLog.quantity))\ .filter(InventoryLog.change_type == 'out')\ .filter(InventoryLog.created_at.between(start_of_month, end_of_month))\ .scalar() or 0 stock_change = in_sum - out_sum # 写入 Excel df = pd.DataFrame([{ '统计月份': f"{now.year}年{now.month}月", '采购总额(元)': float(po_total), '销售总额(元)': float(so_total), '毛利总额(元)': float(profit_total), '入库数量(件)': int(in_sum), '出库数量(件)': int(out_sum), '净库存变动(件)': int(stock_change) }]) filename = f"monthly_report_{now.year}{now.month:02d}.xlsx" df.to_excel(filename, index=False) print(f"✅ 月度报告已生成:{filename}") # 运行:python export_monthly_report.py

关键点:

  • scalar()安全取单值,避免None报错;
  • pd.DataFrame([dict])保证单行输出,Excel 打开即见表格;
  • 文件名含年月,避免覆盖。

从那以后我每次上线新功能前,都强制走一遍get_slow_moving_products()和export_monthly_report.py,看数据是否符合预期。这比翻日志快十倍,也比问业务方“对不对”更可靠。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询