简介:这是一套基于Python开发的进销存系统源码,面向希望掌握企业级业务系统开发的中初级Python开发者与计算机专业学生,可用于课程设计、毕业设计或二次开发参考。系统围绕进货、销售、库存三大核心流程展开,涉及Web框架、数据库交互、模板渲染、路由处理、表单验证、会话管理、权限认证、日志记录、库存管理算法、数据分析报表及API集成等模块,帮助读者理解业务系统从数据建模到界面呈现的完整链路。压缩包为zip格式,整体约91.29MB,文件类型明细暂未提供,但可预期包含源码、模板、静态资源与配置脚本等。目前已有709人学习下载,适合作为进销存项目实战的参考素材,便于对照源码梳理模块划分、数据库设计与业务逻辑,并在此基础上按自身需求进行定制与扩展。
1. 从一张Excel台账说起:这套python进销存系统源码到底能干什么
我见过太多小团队用一张Excel管进销存:采购填一列、销售填一列、库存靠手算,月底对账对到怀疑人生。这套python进销存系统源码,解决的就是这个场景——把商品、供应商、客户、采购入库、销售出库、库存流水、往来账款这些环节,用一套能跑起来的Web系统串起来。它适合两类人:一类是刚学完Python想找个完整项目练手的开发者,另一类是小型商贸、零售、批发场景里想自己搭一套轻量管理工具的技术负责人。技术栈上,后端用Flask或Django(源码里通常是Flask+SQLAlchemy的组合),前端用Bootstrap+Jinja2模板,数据库默认SQLite,改一行配置就能切MySQL。整套代码不依赖重型中间件,一台普通云主机甚至本地电脑就能跑,属于那种“下载下来、装完依赖、改改配置就能看到界面”的实战型项目。
2. 技术选型与目录结构:先看清这套源码的骨架
2.1 为什么是Flask+SQLAlchemy这套组合
拿到源码先别急着跑,先看它为什么这么选。进销存系统的核心是“表多、关系杂、查询频繁”,商品、仓库、库存、单据、明细、往来单位之间全是外键关联。Flask本身轻,但配上SQLAlchemy之后,ORM层能把这种多表关系处理得很干净。比如一张销售出库单,主表存单号、客户、日期、总金额,明细表存每一行商品和数量,库存表再根据明细做增减——这种一对多的结构,用SQLAlchemy的relationship定义出来,查询和写入都不用手写复杂SQL。
常见做法是:models.py里定义所有数据模型,views/或routes/目录下按业务模块拆分蓝图(Blueprint),templates/放Jinja2模板,static/放CSS和JS,config.py集中管理数据库连接和密钥。这套结构的好处是,你想加一个“退货单”功能,只需要新增一个模型、一个蓝图、几个模板,不用动其他模块。
2.2 目录结构逐层拆解
一份典型的python进销存源码,目录大致长这样:
inventory_system/ ├── app.py # 应用入口,创建app实例、注册蓝图 ├── config.py # 数据库URI、SECRET_KEY、分页条数等 ├── models.py # 所有数据模型:商品、供应商、客户、单据、库存 ├── extensions.py # db = SQLAlchemy() 等扩展实例 ├── requirements.txt # 依赖清单 ├── blueprints/ │ ├── auth.py # 登录、注销、权限校验 │ ├── product.py # 商品增删改查 │ ├── purchase.py # 采购入库 │ ├── sale.py # 销售出库 │ ├── stock.py # 库存查询与流水 │ └── report.py # 统计报表 ├── templates/ │ ├── base.html # 公共布局,含导航栏和消息闪现 │ ├── product/ │ ├── purchase/ │ └── sale/ ├── static/ │ ├── css/ │ └── js/ └── migrations/ # 如果用了Flask-Migrate这个结构里,models.py是理解整个系统的钥匙。商品表(Product)通常有编码、名称、规格、单位、进价、售价、库存下限;库存表(Stock)记录每个商品在每个仓库的数量;单据主表(PurchaseOrder/SaleOrder)和明细表(OrderItem)构成一对多。看懂这几张表的关联,后面改功能就是顺藤摸瓜。
2.3 数据库表关系与核心字段
以销售出库为例,核心表关系是:SaleOrder(主表)→SaleOrderItem(明细)→Product(商品),同时SaleOrder关联Customer(客户)。库存的扣减发生在保存出库单时,遍历明细,对每个商品执行stock.quantity -= item.quantity。这里有个关键字段设计:库存表通常用product_id + warehouse_id做联合唯一约束,避免同一商品在不同仓库的库存混在一起。
提示:如果源码里库存扣减写在视图函数里而不是模型方法里,二次开发时建议抽成
StockService,否则并发下单容易出现超卖。
3. 跑起来才算数:环境搭建与首次启动的完整步骤
3.1 依赖安装与虚拟环境
不管源码多简单,第一步永远是隔离环境。我一般会这么做:
# 创建虚拟环境,避免污染全局Python python -m venv venv # 激活:Linux/Mac source venv/bin/activate # 激活:Windows venv\Scripts\activate # 安装依赖,requirements.txt里通常有Flask、Flask-SQLAlchemy、Flask-Login、Flask-WTF pip install -r requirements.txt如果requirements.txt里没有锁定版本,建议手动加上版本号,比如Flask==3.0.0、Flask-SQLAlchemy==3.1.1,否则不同时间安装可能因为依赖升级导致启动报错。这是踩过坑的经验:某次Flask-SQLAlchemy从2.x升到3.x,db.Model的用法没变,但Query接口有调整,老代码直接报AttributeError。
3.2 数据库初始化与配置修改
默认配置一般在config.py里:
import os class Config: # SQLite默认路径,改成MySQL只需替换这一行 SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 'sqlite:///' + os.path.join(os.path.abspath(os.path.dirname(__file__)), 'inventory.db') SQLALCHEMY_TRACK_MODIFICATIONS = False SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-key-change-in-production' ITEMS_PER_PAGE = 20 # 列表分页条数参数说明:SQLALCHEMY_DATABASE_URI是数据库连接串,SQLite用sqlite:///路径,MySQL用mysql+pymysql://用户:密码@主机:端口/库名;SECRET_KEY用于会话加密,生产环境必须改;ITEMS_PER_PAGE控制列表页每页显示多少条,商品多的时候调大或调小都行。
初始化数据库:
# 如果源码提供了init_db.py或manage.py python init_db.py # 或者用Flask-Migrate flask db init flask db migrate -m "init tables" flask db upgrade3.3 启动与首次登录
# 启动开发服务器 flask run --host=0.0.0.0 --port=5000 # 或者直接 python app.py浏览器打开http://127.0.0.1:5000,通常会跳转到登录页。源码里一般会预置一个管理员账号,常见的是admin/admin或admin/123456,具体看init_db.py里的种子数据。登录后先别急着录单,按这个顺序验证:新建一个仓库→新建一个商品→新建一个供应商→做一笔采购入库→查看库存是否增加→新建客户→做一笔销售出库→查看库存是否减少。这一圈走通,说明核心链路没问题。
注意:如果启动时报
ModuleNotFoundError,先检查虚拟环境是否激活;如果报数据库表不存在,说明初始化步骤没执行;如果登录后页面空白,看控制台有没有模板渲染错误,通常是templates路径配置问题。
4. 核心模块拆解:采购入库、销售出库与库存扣减的代码实现
4.1 数据模型定义:商品、库存与单据
先看模型层,这是整个系统的地基:
from extensions import db from datetime import datetime class Product(db.Model): __tablename__ = 'product' id = db.Column(db.Integer, primary_key=True) code = db.Column(db.String(32), unique=True, nullable=False) # 商品编码 name = db.Column(db.String(128), nullable=False) # 商品名称 spec = db.Column(db.String(64)) # 规格 unit = db.Column(db.String(16)) # 单位 purchase_price = db.Column(db.Numeric(10, 2), default=0) # 进价 sale_price = db.Column(db.Numeric(10, 2), default=0) # 售价 stock_lower = db.Column(db.Integer, default=0) # 库存下限预警 class Stock(db.Model): __tablename__ = 'stock' id = db.Column(db.Integer, primary_key=True) product_id = db.Column(db.Integer, db.ForeignKey('product.id'), nullable=False) warehouse_id = db.Column(db.Integer, db.ForeignKey('warehouse.id'), nullable=False) quantity = db.Column(db.Integer, default=0) # 当前库存数量 # 联合唯一约束,保证同一商品同一仓库只有一条记录 __table_args__ = (db.UniqueConstraint('product_id', 'warehouse_id'),) class SaleOrder(db.Model): __tablename__ = 'sale_order' id = db.Column(db.Integer, primary_key=True) order_no = db.Column(db.String(32), unique=True, nullable=False) # 单号 customer_id = db.Column(db.Integer, db.ForeignKey('customer.id')) order_date = db.Column(db.Date, default=datetime.utcnow) total_amount = db.Column(db.Numeric(12, 2), default=0) status = db.Column(db.String(16), default='draft') # draft/confirmed/cancelled items = db.relationship('SaleOrderItem', backref='order', lazy=True) class SaleOrderItem(db.Model): __tablename__ = 'sale_order_item' id = db.Column(db.Integer, primary_key=True) order_id = db.Column(db.Integer, db.ForeignKey('sale_order.id'), nullable=False) product_id = db.Column(db.Integer, db.ForeignKey('product.id'), nullable=False) quantity = db.Column(db.Integer, nullable=False) price = db.Column(db.Numeric(10, 2), nullable=False) amount = db.Column(db.Numeric(12, 2), nullable=False)逻辑说明:Product存商品基础信息;Stock用联合唯一约束保证库存记录不重复;SaleOrder和SaleOrderItem是一对多,items关系让取明细时直接order.items就行。参数上,金额字段用Numeric而不是Float,避免浮点精度问题——这是财务相关系统的铁律。
4.2 销售出库的库存扣减逻辑
出库保存时,核心是遍历明细、扣库存、算总额:
from flask import Blueprint, request, jsonify from extensions import db from models import SaleOrder, SaleOrderItem, Stock, Product sale_bp = Blueprint('sale', __name__, url_prefix='/sale') @sale_bp.route('/create', methods=['POST']) def create_sale_order(): data = request.get_json() # 1. 创建主单 order = SaleOrder( order_no=data['order_no'], customer_id=data['customer_id'], order_date=data['order_date'], status='confirmed' ) db.session.add(order) db.session.flush() # 拿到order.id,但不提交事务 total = 0 # 2. 遍历明细,扣库存 for item in data['items']: product = Product.query.get(item['product_id']) if not product: db.session.rollback() return jsonify({'msg': f'商品{item["product_id"]}不存在'}), 400 # 查库存记录,没有则创建 stock = Stock.query.filter_by( product_id=item['product_id'], warehouse_id=data['warehouse_id'] ).first() if not stock: stock = Stock(product_id=item['product_id'], warehouse_id=data['warehouse_id'], quantity=0) db.session.add(stock) # 库存不足直接回滚 if stock.quantity < item['quantity']: db.session.rollback() return jsonify({'msg': f'{product.name}库存不足,当前{stock.quantity}'}), 400 stock.quantity -= item['quantity'] amount = item['quantity'] * item['price'] total += amount # 3. 写明细 db.session.add(SaleOrderItem( order_id=order.id, product_id=item['product_id'], quantity=item['quantity'], price=item['price'], amount=amount )) order.total_amount = total db.session.commit() # 全部成功才提交 return jsonify({'msg': '出库成功', 'order_id': order.id})逻辑说明:先用db.session.flush()拿到主单ID但不提交,然后逐条检查库存,任何一条不足就rollback(),保证要么全成功要么全失败。参数上,warehouse_id从请求里传,支持多仓库;price用前端传的售价,也可以改成从Product.sale_price取。这段代码的关键是事务边界——所有操作在一个事务里,避免扣了库存但单据没保存的脏数据。
4.3 采购入库与库存增加
采购入库逻辑对称,只是库存做加法:
@purchase_bp.route('/create', methods=['POST']) def create_purchase_order(): data = request.get_json() order = PurchaseOrder( order_no=data['order_no'], supplier_id=data['supplier_id'], order_date=data['order_date'], status='confirmed' ) db.session.add(order) db.session.flush() total = 0 for item in data['items']: stock = Stock.query.filter_by( product_id=item['product_id'], warehouse_id=data['warehouse_id'] ).first() if not stock: stock = Stock(product_id=item['product_id'], warehouse_id=data['warehouse_id'], quantity=0) db.session.add(stock) stock.quantity += item['quantity'] # 入库是加法 amount = item['quantity'] * item['price'] total += amount db.session.add(PurchaseOrderItem( order_id=order.id, product_id=item['product_id'], quantity=item['quantity'], price=item['price'], amount=amount )) order.total_amount = total db.session.commit() return jsonify({'msg': '入库成功', 'order_id': order.id})采购和销售共用Stock表,区别只在加减方向。二次开发时如果想加“退货”,销售退货就是库存加回来,采购退货就是库存减回去,逻辑复用度很高。
4.4 库存流水与报表查询
光有当前库存不够,还得能查“为什么是这个数”。库存流水表(StockLog)记录每次变动:
class StockLog(db.Model): __tablename__ = 'stock_log' id = db.Column(db.Integer, primary_key=True) product_id = db.Column(db.Integer, db.ForeignKey('product.id')) warehouse_id = db.Column(db.Integer, db.ForeignKey('warehouse.id')) change_type = db.Column(db.String(16)) # purchase_in / sale_out / adjust change_qty = db.Column(db.Integer) # 正数增加,负数减少 before_qty = db.Column(db.Integer) # 变动前数量 after_qty = db.Column(db.Integer) # 变动后数量 ref_order_no = db.Column(db.String(32)) # 关联单号 created_at = db.Column(db.DateTime, default=datetime.utcnow)每次扣减或增加库存时,同步写一条StockLog。报表模块就可以按商品、按时间段聚合,算出“某商品本月入库多少、出库多少、期末库存多少”。这个设计在排查库存对不上时特别有用——直接看流水,哪一笔异常一目了然。
5. 避坑与常见问题:跑这套源码时最容易翻车的几个点
5.1 库存扣成负数:并发下的超卖
现象:两个销售单同时出库同一商品,库存只剩5,两单各卖3,结果库存变成-1。原因:先查库存再扣减,中间没有锁。解决:在查询库存时加with_for_update(),或者用数据库层面的UPDATE stock SET quantity = quantity - ? WHERE quantity >= ?原子操作。Flask-SQLAlchemy里可以写Stock.query.filter_by(...).with_for_update().first(),但这要求数据库支持行锁,SQLite下效果有限,生产环境建议换MySQL或PostgreSQL。
5.2 金额字段用Float导致对账差几分钱
现象:销售单总额和明细加总差0.01。原因:Float类型在Python和数据库里都有精度丢失。解决:所有金额字段用Numeric(10, 2)或Decimal,Python里计算也用Decimal而不是float。如果源码里已经用了Float,改模型后需要重新迁移数据库,老数据要手动清洗。
5.3 删除商品导致历史单据明细丢失
现象:删了一个商品,之前的销售单明细查不到了。原因:SaleOrderItem.product_id外键没有设置ondelete行为,或者代码里直接db.session.delete(product)。解决:商品这种基础数据不要物理删除,加一个is_active字段做逻辑删除;外键设置ondelete='RESTRICT',有单据引用时禁止删除。这是进销存系统的通用原则——历史单据必须可追溯。
5.4 分页查询在数据量大时变慢
现象:商品表几千条以后,列表页打开要好几秒。原因:query.all()一次性加载全部,或者count()和offset/limit在大表上效率低。解决:用paginate()方法,确保order_by字段有索引;如果数据量真的很大,考虑用keyset pagination(基于上次最后一条的ID)替代offset。另外,列表页只查需要的字段,别SELECT *。
5.5 生产环境用Flask开发服务器
现象:部署后并发一高就卡死或报错。原因:flask run启动的是开发服务器,单线程、性能差、不适合生产。解决:用gunicorn或uWSGI部署,比如gunicorn -w 4 -b 0.0.0.0:5000 app:app,前面再挂Nginx处理静态文件。同时把DEBUG设为False,SECRET_KEY换成随机长字符串。
6. 二次开发怎么改:从加字段到接API的实操思路
拿到这套源码,最常见的改造需求是“加字段”。比如商品要加“品牌”和“条形码”,步骤是:在Product模型里加brand = db.Column(db.String(64))和barcode = db.Column(db.String(32), unique=True);执行flask db migrate和flask db upgrade生成迁移;在商品新增/编辑模板里加两个输入框;在视图函数里接收这两个参数并赋值。整个过程不用动其他模块,这就是按蓝图拆分的好处。
第二个常见需求是“加报表”。比如要一个“商品销售排行”,在report.py里加一个路由,用SQLAlchemy的func.sum聚合:
from sqlalchemy import func @report_bp.route('/top_products') def top_products(): # 按商品分组,统计销售数量和金额,取前10 results = db.session.query( Product.name, func.sum(SaleOrderItem.quantity).label('total_qty'), func.sum(SaleOrderItem.amount).label('total_amount') ).join(SaleOrderItem, Product.id == SaleOrderItem.product_id)\ .join(SaleOrder, SaleOrder.id == SaleOrderItem.order_id)\ .filter(SaleOrder.status == 'confirmed')\ .group_by(Product.id)\ .order_by(func.sum(SaleOrderItem.quantity).desc())\ .limit(10).all() return render_template('report/top_products.html', results=results)参数说明:filter里限定已确认的单据,避免草稿单混入;group_by(Product.id)而不是Product.name,防止同名商品被合并;limit(10)取前10。这个查询在数据量大时,确保SaleOrderItem.product_id和SaleOrder.status上有索引。
第三个改造方向是“接外部API”。比如把库存查询暴露成JSON接口给小程序或ERP用。在stock.py里加一个返回JSON的路由,用Flask-Login的@login_required或自定义的API Key校验。注意接口返回的库存数要实时查Stock表,不要缓存,否则多端操作时数据不一致。
我自己的习惯是:每次改完模型,先跑一遍flask db migrate看生成的迁移脚本对不对,再upgrade;改完视图,用Postman或curl把增删改查各打一遍;上线前把DEBUG关掉、SECRET_KEY换掉、数据库切到MySQL。这套流程走下来,基本不会出现“本地好好的、线上就崩”的情况。从那以后我每次拿到新源码,都强制先跑通核心链路再动手改,省得在错误的基础上越改越乱。希望帮到你。
本文还有配套的精品资源,点击获取