ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django进销存系统核心:库存表设计、事务与报表实现

Django进销存系统核心:库存表设计、事务与报表实现 简介这套基于Python Django的商品销售进销存系统是针对高校计算机相关专业毕业设计与课程开发的完整源码案例。系统采用Django2.2Python3.6MySQL5.6环境管理员可在后台维护商品、供应商、客户、进货及销售等核心业务数据前台则提供信息查询功能适合用于进销存业务原型、毕设答辩或初期项目演示。压缩包共收录2000个文件以JS、HTML、CSS为主覆盖前端页面与交互逻辑同时包含Python源码、JSON配置、XML配置及数据库说明文档整体体积仅5.45MB结构清晰、便于按模块查阅。该项目已在CSDN上获得345人学习浏览代码经过完整测试作者自述答辩平均分达96分且提供运行指导和远程教学支持。下载后可作为功能完整的毕设参考也可在此基础上扩展二次开发快速搭建自己的进销存管理系统。1. Django进销存zip包先看清楚它到底装了什么拿到这样一个 zip 包多数人第一反应是解压、跑python manage.py runserver看到登录页就以为完事了。实际上这类“Python 基于 Django 商品销售进销存系统”的项目包本质是完整的课设/毕设级工程里面至少装着四类东西Django 后端源码、模板与静态资源、SQLite 数据库文件或者建库脚本以及一份题目和要求说明文档。它的功能边界通常是商品档案、采购入库、销售出库、库存查询和简单的经营报表权限管理直接复用 Django admin。对这个包感兴趣的人大致有三类做数据库课程设计的学生、想从 Flask 转向 Django 完整工程的初级后端、以及需要把库存算法看懂再改造成内部工具的运维或实施工程师。这套系统的难点不在增删改查而在库存扣减、成本核算和报表聚合这些业务逻辑上。把这一层想明白了后面看源码和二次开发才有实际意义。2. 库存表设计Django进销存系统的核心难点进销存系统第一个要回答的问题是商品、入库、出库、库存到底该建几张表、字段怎么设计。很多从 Flask 或 PHP 转过来的开发者习惯性地把“库存数量”作为一个字段存在商品表里每次卖一件就stock - 1。这在单机演示里没问题但进销存真正的业务要求是每笔进出都有据可查哪天对不上账能回溯到具体单据。所以常见做法是建四张核心表商品表存档案信息入库单和出库单各建一张流水表库存不直接落库而是通过聚合算出来。2.1 四张核心表商品、入库、出库、库存台账以一份常规的goods/models.py为例最基础的模型如下from django.db import models class Category(models.Model): name models.CharField(max_length50, verbose_name分类名) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE, verbose_name父分类) class Product(models.Model): sku models.CharField(max_length64, uniqueTrue, verbose_name商品编码) name models.CharField(max_length128, verbose_name商品名称) category models.ForeignKey(Category, on_deletemodels.PROTECT) unit models.CharField(max_length10, default件, verbose_name单位) sale_price models.DecimalField(max_digits12, decimal_places2, verbose_name销售价) cost_price models.DecimalField(max_digits12, decimal_places2, verbose_name成本价) stock models.IntegerField(default0, verbose_name当前库存) created_at models.DateTimeField(auto_now_addTrue) class StockIn(models.Model): product models.ForeignKey(Product, on_deletemodels.CASCADE, verbose_name商品) quantity models.IntegerField(verbose_name入库数量) unit_cost models.DecimalField(max_digits12, decimal_places2, verbose_name进货单价) supplier models.CharField(max_length128, blankTrue, verbose_name供应商) created_at models.DateTimeField(auto_now_addTrue) class StockOut(models.Model): product models.ForeignKey(Product, on_deletemodels.CASCADE, verbose_name商品) quantity models.IntegerField(verbose_name出库数量) customer models.CharField(max_length128, blankTrue, verbose_name客户) created_at models.DateTimeField(auto_now_addTrue)这段代码里有三个值得注意的参数sku字段加了uniqueTrue因为商品编码是业务上的唯一标识不能靠自增 id 对外沟通cost_price和sale_price都用了DecimalField而不是FloatField这是为了避免浮点误差在累计统计时被放大StockIn和StockOut的product外键用on_deletemodels.CASCADE意思是商品删了流水也跟着删通常商品档案不建议物理删除真要删的话级联是合理的。Product.stock这个字段在模型里是先留着当冗余用日常列表页显示库存时不用每次都做聚合查询但它的值必须通过下面的业务逻辑来维护不能手工直接改。2.1.1 数据库字段的类型选择库存数量用IntegerField没问题但成本价和销售价一定要用DecimalField(max_digits12, decimal_places2)。max_digits表示总位数decimal_places表示小数位数。12 位搭配 2 位小数最大值到 99 亿适合绝大多数中小型贸易场景。如果预算单价可能超过这个范围改成max_digits14, decimal_places4也可以。on_deletemodels.PROTECT用在分类外键上表示有商品挂在某个分类下时禁止删除该分类这是防止误操作把商品档案搞成孤儿数据。2.2 库存不直接落库用聚合算出来的理由进销存系统最怕的一件事是“对不上账”。假设一个商品进货 10 件卖了 3 件又退回来 1 件库存应该显示 8 件。如果直接用stock字段加减退货逻辑一旦漏写或者顺序不对账面库存就和实际库存不一致之后所有报表都会跟着错。业界通用做法是当前库存 所有入库数量之和 - 所有出库数量之和实时用 Django ORM 聚合算。如下from django.db.models import Sum from goods.models import Product, StockIn, StockOut stock_in StockIn.objects.filter(product_id1).aggregate(totalSum(quantity))[total] or 0 stock_out StockOut.objects.filter(product_id1).aggregate(totalSum(quantity))[total] or 0 current_stock stock_in - stock_out这段查询里aggregate会返回一个字典而不是 QuerySet用[total]取值or 0处理没有任何流水时返回None的情况。这样做的代价是每次查询都要做两次全表聚合数据量超过几十万条后会变慢。所以更合理的折中是流水表是唯一的权威数据源Product.stock作为冗余展示字段存在每次入出库操作完成后在同一个事务里更新它。2.3 移动加权平均成本给进货价一个计算规则进销存的第二个核心难点是商品成本怎么算。同样一种商品第一次进货单价 10 元第二次进货单价 12 元你卖出去的时候这单成本算多少三种常见计价方式里Django 课设项目最常见的是“移动加权平均法”因为实现代码最简短库存金额也不会出现负数。from django.db import transaction from django.db.models import Sum transaction.atomic def stock_in(product_id, quantity, unit_cost): product Product.objects.select_for_update().get(pkproduct_id) in_sum StockIn.objects.filter(productproduct).aggregate( total_qtySum(quantity), total_costSum(unit_cost]) old_qty in_sum[total_qty] or 0 old_cost in_sum[total_cost] or 0 new_qty old_qty quantity product.cost_price (old_cost unit_cost * quantity) / new_qty product.stock new_qty - StockOut.objects.filter(productproduct).aggregate( tSum(quantity))[t] or 0 product.save(update_fields[cost_price, stock]) StockIn.objects.create(productproduct, quantityquantity, unit_costunit_cost)这段逻辑的重点是先把新入库流水算进总成本和总数量得出新的加权平均单价再回写到商品表此时stock同步更新。select_for_update()会对这一行加锁避免两个并发请求同时读到相同的旧库存这个细节在传统课设里几乎不会有人讲但生产环境这是必须的。2.4 用事务和行锁守住库存不为负销售出库业务里最常见的 bug 是超卖。两个请求同时进来都读到库存 5 件各出 3 件结果库存变成 -1。Django 里解决这个问题的标准姿势是事务加行锁from django.db import transaction from django.db.models import F from goods.models import Product, StockOut transaction.atomic def stock_out(product_id, quantity, customer): product Product.objects.select_for_update().get(pkproduct_id) if product.stock quantity: raise ValueError(f库存不足当前仅剩 {product.stock} 件) StockOut.objects.create(productproduct, quantityquantity, customercustomer) product.stock F(stock) - quantity product.save(update_fields[stock])这里用F(stock) - quantity而不是先product.stock - quantity再 save是因为F表达式让 SQL 在数据库端完成原子减法进一步缩小了并发窗口。加上外层transaction.atomic后流水创建和库存更新要么一起成功要么一起回滚不会出现卖货单建了但库存没扣的情况。这是整个进销存系统里最值得反复看的一段代码。3. 本地跑通解压、建环境、迁移数据库的四步法拿到 zip 包以后不能直接runserver就算完因为你不知道这个包是用 Django 2.x 还是 4.x 写的本地 Python 版本是否匹配依赖装没装。正确的启动过程应该是解压、创建虚拟环境、安装依赖、改配置、迁移数据库、建管理员、启动。特地把配置核实这一步单独拿出来是因为大多数运行失败都出在settings.py上。3.1 先读目录结构认准五个关键文件解压后先用tree或者ls -R看一遍目录。一个规范的 Django 进销存项目包通常长这样sales_system/ ├── manage.py ├── requirements.txt ├── db.sqlite3 ├── goods/ │ ├── admin.py │ ├── models.py │ ├── views.py │ └── urls.py ├── sales/ │ ├── models.py │ └── views.py ├── templates/ │ ├── base.html │ └── goods/ └── static/ ├── css/ └── js/这五个文件最关键requirements.txt锁定了依赖版本manage.py是 Django 所有命令的入口db.sqlite3是自带的数据库文件里面可能有测试数据settings.py记录数据库、时区、静态文件配置urls.py是整个系统的路由总表。先看requirements.txt而不是先跑代码能避免 pip 装错版本浪费十分钟。3.2 创建虚拟环境并安装依赖列表不推荐直接把依赖装到系统 Python 里尤其是这台机器上还要写其他项目的时候。用 venv 隔离是最常见的做法cd sales_system python -m venv venv source venv/bin/activate # Windows 下是 venv\Scripts\activate pip install -r requirements.txt如果pip install拉包速度很慢可以临时切到国内镜像源pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后用pip list检查关键包Django、djangorestframework如果包里有 API、pandas如果有报表导出成 Excel 的功能。这里要注意包里的 Django 版本和你本地的 Python 版本是否兼容Python 3.12 装老项目里的 Django 2.2 通常会直接报错。3.3 修改 settings.py数据库、时区、DEBUG打开sales_system/settings.py重点检查三个配置块DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } } TIME_ZONE Asia/Shanghai USE_TZ True DEBUG False ALLOWED_HOSTS [127.0.0.1, localhost]USE_TZ True时数据库里存的是 UTC 时间页面展示时 Django 会自动转成TIME_ZONE指定的时区这个组合不要随意改成USE_TZ False否则做日报按月分组统计时会出现 8 小时偏移。ALLOWED_HOSTS在DEBUGFalse时必须显式列出允许访问的主机名漏配会导致DisallowedHost报错。如果只有 sqlite3 一个库这里就是全部数据库配置后续想切 MySQL 才需要额外装mysqlclient并在ENGINE里改。3.4 执行迁移并创建超级管理员启动服务数据库配置改好以后执行下面的流程python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000makemigrations会根据模型生成迁移文件migrate会把迁移应用到数据库。如果项目包里自带db.sqlite3你可以选择删掉这个文件再重新 migrate得到一套干净的初始库避免旧数据残留干扰测试。createsuperuser需要交互式输入用户名、邮箱和密码。runserver 0.0.0.0:8000表示监听所有网卡地址这样局域网内其他机器也能通过你的 IP 访问方便给老师或同事演示。4. 源码走读Django进销存的请求链路与报表查询新手看 Django 项目最容易一头扎进views.py里一行行读结果被各种 import 绕晕。正确顺序应当是从urls.py出发浏览器输入 URL 之后Django 先按路由表找到对应的视图函数视图再操作模型最后把数据扔进模板渲染成 HTML。把这条链路打通整个项目就解开了一半。4.1 urls.py 和 views.py路由到视图的映射# sales/urls.py from django.urls import path from . import views urlpatterns [ path(, views.dashboard, namedashboard), path(products/, views.product_list, nameproduct_list), path(stockin/, views.stock_in_create, namestock_in_create), path(stockout/, views.stock_out_create, namestock_out_create), ]这段路由表说明了系统的基础能力dashboard是首页仪表盘products是商品列表stockin和stockout是新入库、新出库的表单页。视图函数的名字和 URL 一一对应看懂了这张表再去点进views.py里的同名函数基本上就能知道每个页面在做的事情。4.2 用 Django 通用视图组织列表、表单与详情页进销存的商品列表页和入库表单页代码量并不大。Django 内置的通用视图类可以省掉大量重复的样板代码课设项目里很常见。from django.views.generic import ListView, CreateView from goods.models import Product, StockIn class ProductListView(ListView): model Product template_name goods/product_list.html context_object_name products paginate_by 10 class StockInCreateView(CreateView): model StockIn fields [product, quantity, unit_cost, supplier] success_url /stockin/ListView会自动查Product.objects.all()把结果放进paginate_by指定的分页器里再传给模板。CreateView根据fields自动生成 HTML 表单提交后自动执行StockIn.objects.create()。这里有一个需要自己补全的关键点CreateView只负责插入流水记录不会去更新Product.stock和成本价所以此时一定要重写form_valid方法在里面调用上一章写的stock_in()业务函数才能保证库存和流水同步。4.3 报表页annotate 和聚合统计出销售额与利润正规的进销存系统都会带一个“经营报表”或“销售统计”页面用来算本月卖了多少钱、哪些商品卖得最好。Django 里统计这部分主要靠annotate按维度分组再聚合from django.db.models import Sum, F, DecimalField products Product.objects.annotate( sales_qtySum(stockout__quantity), sales_amountSum( F(stockout__quantity) * F(sale_price), output_fieldDecimalField(max_digits14, decimal_places2)), profitSum( F(stockout__quantity) * (F(sale_price) - F(cost_price)), output_fieldDecimalField(max_digits14, decimal_places2)), ).order_by(-sales_amount)这段查询是报表页的核心。stockout__quantity的写法是 Django 外键关联查询的语法意思是“从出库流水表里找当前商品的所有出库记录”。F(sale_price)表示取当前行商品表里的售价字段让加减乘除都在数据库层完成效率比 Python 循环高得多。output_field参数告诉 ORM 这个计算结果是个 Decimal 类型避免聚合出整数结果。这样一条语句就把商品销量、销售额、毛利润都算出来了模板端直接用{{ item.sales_amount }}输出。模板里的关键部分通常长这样{% for p in products %} tr td{{ p.sku }}/td td{{ p.name }}/td td{{ p.sales_qty|default:0 }}/td td{{ p.sales_amount|floatformat:2 }}/td td{{ p.profit|floatformat:2 }}/td /tr {% empty %} trtd colspan5暂无销售数据/td/tr {% endfor %}floatformat:2是模板过滤器强制保留两位小数。default:0处理没有销量时显示空白的问题。报表页到这里已经可以正常输出但如果需要按日期做环比还要在StockOut上加一个created_at__date的annotate分组这是后话。5. 二次开发与避坑改报表、加统计、查日志跑通只是第一步把系统改成自己能用才是拿到这个 zip 包的意义所在。这一章给出几个高频的开发动作和对应的坑。5.1 给商品列表增加毛利润列如果商品表里同时有sale_price和cost_price可以在原列表视图上直接加一个annotate字段不改模板结构就能输出毛利class ProductListView(ListView): model Product context_object_name products def get_queryset(self): return super().get_queryset().annotate( profitF(sale_price) - F(cost_price))然后在模板里增加一列td{{ p.profit }}/td即可。这里有个细节如果不重写get_queryset而是直接Product.objects.all().annotate(...)分页功能会因为 QuerySet 被整体替换而失效所以要保留super().get_queryset()再链式调用annotate。5.2 三个必看的调试位置运行过程中遇到问题按顺序检查这三处检查位置命令或文件典型问题数据库迁移状态python manage.py showmigrations未打勾的迁移会导致表缺失服务端日志runserver终端输出500 错会直接打印 Traceback异常表结构python manage.py shell后执行Product.objects.all()字段不匹配报 OperationalError如果迁移状态显示正常但表里查不到数据用python manage.py sqlmigrate goods 0001查看这条迁移实际执行的 SQL 语句手动在数据库客户端里执行一遍能快速确定是迁移文件问题还是数据库引擎问题。5.3 五分钟排错清单高频出问题的点集中在这几类逐个核对基本能解决大多数报错。settings.py里ALLOWED_HOSTS没加本机 IP浏览器访问时报Bad Request处理方式是加上当前机器的局域网 IP。mysqlclient在 Windows 上安装失败换成pymysql并在项目__init__.py里写import pymysql; pymysql.install_as_MySQLdb()。模板页面 CSS 全部消失多半是没执行python manage.py collectstatic生产部署时必须先执行这条命令再启动服务。静态文件配置里STATIC_ROOT必须指向一个真实存在的目录否则collectstatic会报错。查询结果出现Decimal(12.50)而不是12.5直接在前端模板里用floatformat处理。在shell里手动测试入库业务后发现商品列表页库存没有变化检查是否绕过了stock_in()而是直接StockIn.objects.create()只有走业务函数才能联动更新库存字段。完成以上核对之后进销存系统在本地基本就稳定可用了后面再按需加导出 Excel、多仓库管理、条形码扫码这些扩展功能都建立在同一套模型和事务逻辑之上。本文还有配套的精品资源点击获取
返回列表