资讯动态

Flask+SQLite进销存系统:支持审核流与库存状态机的生产级MVP

发布时间:2026/10/21 3:47:05 来源:尧图企业网站定制
简介这是一套面向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前端是纯 HTMLJinja2 模板无任何第三方 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 flask2.3.3 pip install flask-sqlalchemy3.0.5 pip install flask-migrate4.0.5 pip install python-dotenv1.0.0为什么只这 4 个flask2.3.3稳定 LTS 版本兼容性好路由和请求处理无已知内存泄漏flask-sqlalchemy3.0.5适配 SQLAlchemy 2.0 API模型定义更清晰.query写法已弃用强制用session.execute()或Model.query.filter()flask-migrate4.0.5配合alembic实现数据库结构演进比手写 SQL 更安全python-dotenv1.0.0读取.env文件把DEBUGTrue、DATABASE_URLsqlite:///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]控制是否清库生产环境必须设为Falsedb.create_all()不会覆盖已有表只建缺失表安全测试数据含真实业务字段Product.unit单位件/箱/千克、Product.cost_price采购成本、Product.selling_price销售定价非空字段全部赋值避免后续表单提交报错。初始化完成后启动服务# 设置环境变量Linux/macOS export FLASK_APPapp:create_app export FLASK_ENVdevelopment flask run --host0.0.0.0 --port5000注意Windows 用户用set FLASK_APPapp:create_app和set FLASK_ENVdevelopment或直接改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_idquantity 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_typeininventory_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 表新增一条记录statusdraft 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 无写入因为 statusdraft库存不更新此时库存仍为 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_iditem.product_id, change_typein, quantityitem.quantity, ref_idfpo-{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_idfpo-{po.id}日志关联采购单格式固定方便前端按ref_id查原始单据db.session.commit()在循环外统一提交保证原子性。刷新「库存查询」页A 商品库存显示100且「库存流水」列表中可见一条in 100记录ref_id为po-1。3.2 发起销售单库存不足时自动拦截不是报错而是友好提示进入「销售管理 → 新建销售单」填客户“某智能硬件公司”商品“A 型电阻”数量30点击「保存」页面无跳转底部弹出黄色提示✅ 销售单已保存为草稿未审核当前库存充足100 ≥ 30此时数据库sale_order新增一条statusdraft记录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_iditem.product_id, change_typeout, quantityitem.quantity, ref_idfso-{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 -30ref_id为so-1。至此采购→库存增加→销售→库存减少→毛利生成闭环完成。4. 避坑指南我在三台不同机器上踩过的 5 个真实坑附解决方案这份源码虽精炼但在不同环境部署时仍会遇到典型问题。以下是我实测过程中在 Ubuntu 22.04、macOS Sonoma、Windows 11 三台机器上复现并解决的 5 个高频问题。每条都按「现象 → 原因 → 解决」给出可立即执行的方案不讲虚的。4.1 现象启动时报ModuleNotFoundError: No module named app原因Python 导入路径错误。FLASK_APPapp:create_app要求app是可导入包但项目根目录下缺少__init__.py或当前工作目录不是项目根目录。解决确认项目结构为/your-project-root/app/__init__.pyapp目录内必须有空的__init__.py文件终端cd到/your-project-root即app目录的父目录再运行flask run若仍报错在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_keysON))并确保product表创建时包含FOREIGN KEY (category_id) REFERENCES category(id)源码中已含此步为保险。4.3 现象中文商品名在网页显示为乱码如A 型%E7%94%B5%E9%98%BB原因Jinja2 模板未声明 UTF-8 编码或浏览器未正确解析。解决在所有.html模板顶部head内强制添加meta charsetUTF-8 meta http-equivContent-Type contenttext/html; charsetUTF-8在app/__init__.py中配置 Flaskapp.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(namename).first(): raise ValueError(f商品名称 {name} 已存在请勿重复添加) super().__init__(**kwargs)并在商品添加表单的POST路由中捕获该异常返回友好提示。4.5 现象flask migrate报No changes in schema detected但明明改了模型原因flask-migrate依赖alembic的env.py中compare_typeTrue未开启导致字段类型变更如String(50)→String(100)不被识别。解决编辑migrations/env.py找到context.configure()调用在参数中添加context.configure( connectionconnection, target_metadatatarget_metadata, compare_typeTrue, # ← 关键开启类型比较 render_as_batchTrue # 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(days30): cutoff_date datetime.now() - timedelta(daysdays) # 找出近 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()避免 N1 查询性能优于not in [list]~Product.id.in_(...)是 SQLAlchemy 的标准否定语法清晰表达“未销售”结果按库存降序优先处理积压最严重的。5.2 查询毛利率 Top 5 商品按销售毛利总额排序from sqlalchemy import func def get_top_profit_products(limit5): 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(threshold10): return Product.query\ .filter(Product.stock_quantity Product.safety_stock threshold)\ .filter(Product.safety_stock 0)\ .order_by(Product.stock_quantity.asc())\ .all()使用场景每天早上运行邮件推送结果。threshold10是缓冲值避免刚低于安全库存就报警。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(day1) end_of_month (start_of_month timedelta(days32)).replace(day1) - timedelta(days1) # 采购额 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 fmonthly_report_{now.year}{now.month:02d}.xlsx df.to_excel(filename, indexFalse) print(f✅ 月度报告已生成{filename}) # 运行python export_monthly_report.py关键点scalar()安全取单值避免None报错pd.DataFrame([dict])保证单行输出Excel 打开即见表格文件名含年月避免覆盖。从那以后我每次上线新功能前都强制走一遍get_slow_moving_products()和export_monthly_report.py看数据是否符合预期。这比翻日志快十倍也比问业务方“对不对”更可靠。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑