尧图精选

Django开源ERP系统源码:架构设计与核心模块解析

🕒 发布时间:2026/9/9 1:12:14 📁 来源:尧图网络
简介这是一份基于Python Django框架开发的高效开源ERP系统源码适合企业信息化开发人员、Python/Django进阶学习者以及需要快速搭建内部管理系统的技术团队。源码整合了销售管理、采购管理、库存管理、组织管理等核心模块并支持项目费用归集、工作流审批、采购单与报价单批量导入可帮助企业提升运营效率。压缩包共132个文件大小仅2.36MB包括74个Python源文件、15个HTML模板、18个Excel表格以及多份使用手册文档目录结构清晰便于按模块阅读和二次开发。已有1689人学习浏览特别适合用于研究ERP系统的模块设计、权限模型与业务流程实现也可作为Django企业级开发实战参考。 做企业信息化这些年被问得最多的一个问题就是Python Django到底能不能做ERP很多人一听到“ERP系统源码”脑子里马上联想到Java单体或老牌C#项目觉得Python只适合做网站后台。但我实际做过几个中型制造企业的进销存和财务一体化项目之后可以明确告诉你Django不仅能做而且一旦理解了ERP的本质是“单据流库存账权限矩阵”它甚至比很多传统技术栈更适合快速交付开源之后还特别容易让甲方自己维护。这篇文章围绕“高效开源Python Django ERP系统源码”这个主题展开把架构选型、数据模型、核心业务模块的实现思路、性能优化、部署运维和踩坑记录完整梳理一遍。适合三类人看想用Python进入企业级应用开发的程序员、企业内部负责信息化选型的技术负责人、以及想研究ERP源码逻辑但苦于无从下手的初学者。1. 为什么敢用Django写ERP选型边界与架构前提1.1 Django做ERP的最大阻力其实是思维惯性大多数人说“Django做不了ERP”理由无非是ORM太笨重、Admin后台太玩具、复杂事务撑不住。这些说法一半对一半不对。Django的ORM确实不适合做海量数据分析Admin也确实不能直接当业务界面用但ERP的核心场景根本不是高并发而是“低并发、重逻辑、强一致”。换句话说一次采购入库可能牵动库存余额、采购订单状态、应付暂估、成本均价四个地方这种业务对框架的性能要求远低于对事务一致性和代码可维护性的要求。Django真正擅长的恰好是这个领域最需要的自带ORM迁移、自带认证授权、自带Admin、自带信号机制、自带事务控制。这些在Java里要用一堆Spring子项目拼出来的东西Django默认就给了。更实际的一点是Python开发人员招聘成本低、上手快企业拿到开源源码之后哪怕最初写代码的人走了新人也能在两周内读懂核心模型。1.2 哪些ERP场景不适合Django说完了优势也得说边界。我自己的判断是以下三类场景尽量不要用Django硬扛十万级QPS的电商前台这不叫ERP这叫互联网应用应该交给Go或Java体系。复杂的MES设备控制需要高频采集PLC数据、毫秒级响应Python不是不行但Django这套同步阻塞模型不合适。纯数据分析型BI系统ERP里的报表查询可以用Django做但大规模离线分析应该交给数仓Django只负责取数展示。ERP系统最合适的形态是“后管系统”。它需要跟车间设备交互但交互频率不高用MQTT或Modbus协议采数写到库里Django负责业务流转和展示这个模型最稳。1.3 “高效”和“开源”到底指什么很多人把“高效”理解成“性能好”其实在企业信息化语境下高效指的是“能否快速适配业务流程”。开源ERP源码的真正价值是让企业不用从零开始又能完全控制核心逻辑。商业ERP最大的痛点就是你改不动报表服务器配置出了问题都要找原厂一个字段的显示都要提工单。开源Django ERP则可以针对行业流程直接二次开发这才是标题里“高效”和“开源”两个词的落点。我自己在项目里明确跟客户说不要指望部署一套源码就能直接用ERP一定是要做行业参数化定制的。开源的价值在于你有改造的权利和可能性而不是省去实施成本。带着这个预期去选型你对框架、源码结构的理解和后续推进都会顺畅很多。2. 这套开源ERP源码的工程结构与核心数据模型2.1 按业务域拆分的Django工程结构但凡ERP项目最忌讳的就是把所有models放在一个文件里几十张表堆一起谁维护谁知道。我做过多次重构之后固定下来的结构是“一个业务域一个app”erp_project/ ├── config/ # 项目配置settings、urls、celery ├── common/ # 全局通用字段、基础抽象模型、工具函数 ├── apps/ │ ├── base/ # 基础数据物料、计量单位、仓库、往来单位 │ ├── purchase/ # 采购管理采购订单、到货、退货 │ ├── sales/ # 销售管理销售订单、发货、退货 │ ├── inventory/ # 库存管理出入库单、库存台账、盘点 │ ├── finance/ # 财务核算应收应付、成本结转、凭证 │ └── system/ # 系统设置用户、角色、菜单、按钮权限 └── templates/每个app内部还可以进一步分三层models放数据定义services放业务逻辑views/api放接口。ERP业务逻辑复杂建议不要写进视图函数里后期测试和复用都麻烦。按这个结构切分新来的人看代码路径就能猜出业务归属排查问题效率高很多。2.2 物料、单位、批次这几个基础模型的细节ERP的一切业务都是围绕“物料”展开的所以Product模型的设计直接决定后面所有单据的复杂度。我自己形成了一套固定的字段组合class Product(models.Model): code models.CharField(max_length64, uniqueTrue, verbose_name物料编码) name models.CharField(max_length128, verbose_name物料名称) spec models.CharField(max_length255, blankTrue, verbose_name规格型号) category models.ForeignKey(ProductCategory, on_deletemodels.PROTECT, verbose_name物料分类) base_uom models.ForeignKey(UOM, on_deletemodels.PROTECT, related_namebase_products, verbose_name基本单位) is_batch_managed models.BooleanField(defaultFalse, verbose_name启用批次管理) is_serial_managed models.BooleanField(defaultFalse, verbose_name启用序列号管理) default_warehouse models.ForeignKey(Warehouse, nullTrue, blankTrue, on_deletemodels.SET_NULL, verbose_name默认仓库) cost_method models.CharField(max_length16, choices[(moving_average, 移动加权平均), (fifo, 先进先出)], defaultmoving_average, verbose_name成本核算方法) status models.CharField(max_length16, choices[(active, 启用), (inactive, 停用)], defaultactive, verbose_name状态)三条容易忽略的约束code必须企业内唯一不能只靠id因为外部系统对接和财务审计都认编码。启用批次管理的物料库存表必须细分到批次而不仅仅是物料和仓库。on_delete全部用PROTECT业务数据不允许被级联删除最多停用。计量单位这一块也比看起来麻烦要处理一个基本单位多个换算单位的模型比如吨和千克的换算率是1000一箱和一件的换算率可能是12。每次单据录入时数量要保存两个字段单据数量和基本单位数量否则月底对账一定会乱。2.3 单据主从模型与状态机设计ERP里的单据基本都是主从结构采购订单头存供应商、单据日期、状态订单行存物料、数量、单价。如果一个人用Django开发很容易用ArrayField或者JSONField去存明细这是最大的坑。明细必须拆成子表因为后面每一行明细都会独立生成到货记录、库存流水和应付数据。class PurchaseOrder(models.Model): order_no models.CharField(max_length32, uniqueTrue, verbose_name单据编号) supplier models.ForeignKey(Supplier, on_deletemodels.PROTECT, verbose_name供应商) order_date models.DateField(verbose_name单据日期) status models.CharField(max_length16, choicesORDER_STATUS, defaultdraft, verbose_name状态) remark models.TextField(blankTrue, verbose_name备注) created_by models.ForeignKey(User, on_deletemodels.PROTECT, verbose_name制单人) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class PurchaseOrderLine(models.Model): order models.ForeignKey(PurchaseOrder, on_deletemodels.CASCADE, related_namelines, verbose_name采购订单) product models.ForeignKey(Product, on_deletemodels.PROTECT, verbose_name物料) qty models.DecimalField(max_digits18, decimal_places3, verbose_name数量) base_qty models.DecimalField(max_digits18, decimal_places3, verbose_name基本单位数量) price models.DecimalField(max_digits18, decimal_places6, verbose_name含税单价) received_qty models.DecimalField(max_digits18, decimal_places3, default0, verbose_name累计到货数量)状态字段不要裸用一个CharField到处改。我习惯用django-fsm状态机库把“创建、提交、审核、关闭”这些流转定义清楚谁能在哪个状态执行哪个动作由权限和状态机双重控制。这样从源码层面就能保证业务规范而不是靠程序员自觉。2.4 多公司权限控制数据权限比菜单权限更难ERP的权限分三层菜单权限、按钮权限、数据权限。Django自带的Permission只能解决前两层数据权限必须自己实现。最常见的是“业务员只能看自己客户的订单”更复杂的还有“仓库主管只能看本仓库存账”。我常用的做法是给核心单据加一个org字段结合用户所属部门和数据范围等级在视图层的queryset里统一过滤class DataScopeChoices(models.TextChoices): ALL all, 全部数据 DEPT dept, 本部门数据 SELF self, 仅本人数据 def filter_by_scope(queryset, user, owner_fieldcreated_by): scope user.role.data_scope if scope DataScopeChoices.SELF: return queryset.filter(**{owner_field: user}) if scope DataScopeChoices.DEPT: dept_ids user.department.get_descendants(include_selfTrue).values_list(id, flatTrue) return queryset.filter(department_id__indept_ids) return queryset数据权限的过滤逻辑务必集中在service层不要散落在各个视图里否则审计的时候根本说不清谁能看谁不能看。3. 库存、订单、报表三大核心模块的源码实现思路3.1 库存台账为什么不能靠汇总库存表新手做库存最容易犯的错误是库存表里存一个qty字段每次出入库直接加减。这个设计一上线就会被财务打回来因为任何一次数据错误都无法追溯。正确做法是“流水驱动”所有库存变动都写入库存流水表当前库存可以由流水汇总得到也可以单独维护一个库存余额表但余额表必须只能从流水表过账生成不允许手工改动。我设计的最小库存流水模型长这样class StockLedger(models.Model): move models.ForeignKey(StockMove, on_deletemodels.PROTECT, verbose_name关联单据) product models.ForeignKey(Product, on_deletemodels.PROTECT, verbose_name物料) warehouse models.ForeignKey(Warehouse, on_deletemodels.PROTECT, verbose_name仓库) batch_no models.CharField(max_length64, nullTrue, blankTrue, verbose_name批次号) direction models.SmallIntegerField(choices[(1, 入库), (-1, 出库)], verbose_name方向) qty models.DecimalField(max_digits18, decimal_places3, verbose_name变动数量) unit_cost models.DecimalField(max_digits18, decimal_places6, nullTrue, verbose_name成本单价) balance_qty models.DecimalField(max_digits18, decimal_places3, verbose_name结存数量) biz_time models.DateTimeField(verbose_name业务时间) created_at models.DateTimeField(auto_now_addTrue, verbose_name记账时间)关键点是每一笔流水都冗余保存了当时的结存数量。虽然这样有点冗余但好处是月底对账时可以直接看到任意时点的库存快照不需要从期初一直推算到期末。业务上这个设计思路值得坚持别为了省一点存储给自己埋坑。3.2 销售订单到出库单的流转闭环ERP系统里最典型的流程是接单-发货-开票-收款源码实现的关键在于“订单行累计发货数量”和“库存可用量”之间的联动。一个常见做法是在订单行上增加一个delivered_qty字段每生成一张出库单并过账就把发货数回写到对应的订单行同时校验发货数不能大于订单未发数。def post_stock_move(move): with transaction.atomic(): move.status posted move.save() for line in move.lines.all(): order_line line.source_order_line if order_line: order_line.delivered_qty line.qty order_line.save() create_stock_ledger(line, move)这段代码的核心是事务原子性回写订单和生成流水必须放在同一个事务里否则可能出现出库单有了、订单状态没更新的情况。库存过账之后出库单不允许直接删除只能做红冲单这是财务审计的硬性要求。3.3 报表统计的ORM聚合写法ERP报表里最常见的需求是“按月份、按物料、按仓库汇总出入库数量”。很多新手的做法是循环遍历每一条流水在Python里做累加数据量一大页面直接卡死。Django的ORM其实提供了原生的分组聚合方法from django.db.models import Sum summary ( StockLedger.objects .filter(biz_time__year2025) .values(warehouse__name, product__category__name) .annotate(total_inSum(qty, filterQ(direction1)), total_outSum(qty, filterQ(direction-1))) .order_by(warehouse__name, product__category__name) )数据库执行的就是一条GROUP BY语句千万级流水跑一次也就几百毫秒。报表慢不是Django的问题是SQL写法的问题。真正到了千万级以上的报表再把聚合结果定时刷新到汇总表或者用物化视图也是一个很安全的优化路线。4. 从慢查询到并发扣减性能与数据一致性的调优案例4.1 ORM关联查询的两把武器Django开发ERP最常见的性能问题就是N1查询。比如查询出库单列表然后逐行访问出库单对应的客户名称和物料名称一次10列表单页就会产生几十条SQL。我习惯在查询所有列表接口之前先问自己一个问题这个页面会展示哪些外键字段然后在queryset上加上select_related或prefetch_related。moves ( StockMove.objects .select_related(warehouse, created_by) .prefetch_related(lines__product) .filter(move_date__range(start_date, end_date)) )select_related解决外键的单行关联查询内部走SQL JOINprefetch_related解决ManyToMany和反向外键内部走第二次查询再在内存中关联。两者结合就可以避免绝大多数N1问题。4.2 并发扣库存锁怎么加才不翻车库存扣减最常见的bug是超发也就是并发情况下扣成了负数。解决思路有两种悲观锁和乐观锁。ERP系统的库存扣减因为并发量不高但对正确性要求苛刻我建议直接用悲观锁。def lock_stock(product_id, warehouse_id, batch_noNone): qs StockBalance.objects.select_for_update().filter( product_idproduct_id, warehouse_idwarehouse_id ) if batch_no: qs qs.filter(batch_nobatch_no) return qs.first()事务里先锁行再判断可用量然后扣减最后提交事务这样同一行库存记录在同一时刻只能被一个事务修改不会互相覆盖。select_for_update写起来很简单但要注意两点必须放在事务里否则锁不生效不能对空结果做select_for_update加锁如果你的业务允许发单时还没有库存余额记录需要先用get_or_create把记录建出来再锁。4.3 Celery解决批量业务ERP项目实施的时候最常遇到的数据导入场景是Excel导入物料清单和期初库存。几千到几万行的导入如果用同步方式做请求会超时。我的做法是Celery异步任务导入模板校验前端先把文件上传然后后台任务逐行解析校验把错误信息逐行回写到导入结果表用户在前端查看导入报告。Celery本身不复杂但生产环境要正确配置broker和worker我在用的组合是Redis做broker、Django后台管理定时任务。后台报表生成也建议走异步比如月底库存汇总表生成后存成文件避免用户等待。4.4 缓存哪些数据才能提速ERP系统里有两类数据非常适合缓存物料名称和单位换算率这类基础档案、以及用户的菜单权限树。业务单据本身不推荐缓存因为一致性要求太高。基础档案的缓存失效可以通过Django的信号机制在模型save时自动清除这样代码侵入很小也能保证更新了档案之后缓存不会残留旧数据。5. 从开发机到生产环境麒麟系统与常规部署路径5.1 数据库选型优先PostgreSQLSQLite只适合开发调试MySQL也能用但如果你要跑真正的ERP业务我建议首选PostgreSQL理由很简单PostgreSQL在复杂查询、Json字段、数值精度和并发控制上更可靠将来做分区表、物化视图、全文检索也顺手。ERP涉及钱数据库选型不值得冒险。数据库配置里有一项必须提前改事务隔离级别。Django默认的隔离级别是READ COMMITTED这对绝大多数ERP场景够用。真正需要SERIALIZABLE的场景极少而且还容易出现死锁重试建议先从应用层解决并发问题不要轻易调全局隔离级别。5.2 麒麟系统上部署Django的几个适配细节最近几年国产化环境越来越多不少项目要求在麒麟V10上部署。走一遍下来有几个注意点麒麟系统自带Python版本比较旧建议用pyenv编译安装Python 3.10编译前先保证openssl-devel、bzip2-devel、libffi-devel这些依赖已经装上否则pip install某些包时会报错。cryptography、psycopg2这些带二进制扩展的包在国产化环境下有时没有适配预编译wheel需要源码编译提前安装好gcc、python3-devel。数据库驱动推荐用psycopg2-binary但在麒麟上如果二进制版本不兼容改用psycopg2源码编译并确认OpenSSL版本满足要求。还有一个常见坑是系统时区。生产环境务必在settings.py里把TIME_ZONE和USE_TZ配置好建议数据库统一用UTC存储展示层再转本地时间。这样服务器部署到哪个时区都不会出现日期错位。5.3 Nginx Gunicorn的具体配置生产环境Django不能靠runserver跑我用的是Nginx Gunicorn组合Nginx负责静态文件和反向代理Gunicorn负责动态请求。gunicorn config.wsgi:application \ --bind 127.0.0.1:8001 \ --workers 4 \ --threads 2 \ --timeout 120 \ --access-logfile /var/log/gunicorn/access.log \ --error-logfile /var/log/gunicorn/error.logworkers数量的经验公式是2*CPU核数1但ERP应用有不少内存占用不要盲目调高。timeout设置建议120秒以上因为Excel导入和报表生成即使走了Celery偶尔也会有一些长请求。Nginx核心配置location /static/ { alias /opt/erp/static/; } location /media/ { alias /opt/erp/media/; } location / { proxy_pass http://127.0.0.1:8001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }用了Nginx之后settings.py里要加一个配置USE_X_FORWARDED_HOST True SECURE_PROXY_SSL_HEADER (HTTP_X_FORWARDED_PROTO, https)否则Django生成的重定向和绝对URL会一直带着http登录跳转和安全校验都会出问题。5.4 数据备份策略不能等到出事再想我见过不止一个项目开发阶段压根没想过备份上线一个月后误操作把整个产品表清空才意识到没有备份可用。数据库备份一定要一开始就自动化。我的惯例是每天凌晨全量备份保留30天再用定时任务把备份上传到异地存储备份脚本里加上pg_dump的格式化参数和压缩选项。如果数据量大到全量备份时间太长再考虑WAL归档做增量备份。对于中小型ERP日备异地存储已经足够应对绝大多数场景了。6. 复盘几个踩过的坑N1、死锁、时区和权限6.1 开发时看不见的性能问题上线后被放大第一次做ERP的时候列表页在开发环境十几条数据根本看不出问题上线后客户录入了三个月数据出库单列表打开要三秒。排查后发现每个出库单行都访问了产品分类名称典型的N1。后来统一用select_related和prefetch_related列表页降到200毫秒。从那以后我养成一个习惯任何一个列表接口都要主动检查SQL执行条数用django-debug-toolbar的SQL面板看一眼超过20条就要开始警惕。6.2 并发扣库存导致负库存这个坑出现在一个客户的双十一促销场景线上下单量大两个仓库同时发同一批订单库存只有50件却出了80件。当时库存扣减逻辑就是简单的读-判断-写没加锁。后来改成select_for_update扣减前锁住库存行问题就解决了。这里的教训是用户告诉你的并发量不大不意味着生产环境不会出现峰谷库存扣减永远默认按并发场景设计。6.3 时区设置引发的一天数据对不上有一次客户反馈某天销售报表少了两个小时的数据排查下来发现数据库中时间存的是UTC时区但客户查看的是东八区时间跨天统计用biz_time__date分组时凌晨的数据被划到了前一天。解决办法是统计时用ORM的TruncDate并指定时区或者干脆在查询代码里把时间先转成本地时区再分组。这个坑不容易发现因为开发环境数据量小刚好避开跨天的边界数据上线后遇到第一个月底对账就暴露了。6.4 权限模型迁移动不动报错Django的auth_permission是迁移自动生成的但如果你在自定义模型上加了Meta.permissions修改字段之后忘记生成新迁移生产环境部署时就会报权限不存在。另外还有一点很容易忽略ModelAdmin的actions和自定义permission并不会自动关联你自定义了一个“审核”权限但用户即使被分配了这个权限Django Admin里也不会自动显示对应按钮需要自己在模板或视图里判断权限。权限这块建议提前设计好统一封装不要全凭视图函数里临时加装饰器。回到开头那个问题Django能不能做ERP经过这几个项目的实践我的答案是可以而且开源源码配合合理的二次开发节奏比很多商业套件的落地效果更贴合企业实际业务。最后分享一个小技巧如果你刚接触这类项目不要一上来就去啃权限和报表模块先把“一个订单如何变成库存流水和应收款”这条链路走通理解了这条主链路整个ERP系统的脉络基本就摸清了。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →