项目初始化:Django项目创建、应用模块划分与数据库模型设计原则

好,咱们正式开始动手了。这一章,我带你从零搭起ERP进销存系统的骨架。说白了,就是先把Django项目建起来,把四个核心模块(采购、销售、库存、财务)划分清楚,再聊聊数据库模型该怎么设计。

我记得刚带团队做第一个ERP项目时,上来就急着写业务代码,结果模块之间耦合得跟麻花似的,后来重构了两次才理顺。所以,这一步走稳了,后面能省不少心。

3.1 创建Django项目

先确认你的环境。我习惯用Python 3.10+,Django 4.2 LTS版本。为什么选LTS?你想想看,企业项目一跑就是好几年,选长期支持版心里踏实。

# 创建虚拟环境
python -m venv erp_env
source erp_env/bin/activate  # Windows下用 erp_env\Scripts\activate

# 安装Django
pip install django==4.2

# 创建项目
django-admin startproject erp_backend
cd erp_backend

# 创建四个核心应用
python manage.py startapp purchase
python manage.py startapp sales
python manage.py startapp inventory
python manage.py startapp finance

嗯,这里要注意:应用名我全用了英文单数形式。有些团队喜欢用复数,我个人觉得单数更简洁,跟模型类名保持一致就好。

3.2 应用模块划分

四个模块怎么分工?我直接给你画个图:

模块 核心职责 典型模型
purchase 采购订单、供应商管理、入库单 PurchaseOrder, Supplier, PurchaseItem
sales 销售订单、客户管理、出库单 SalesOrder, Customer, SalesItem
inventory 商品档案、库存台账、库存变动 Product, Warehouse, StockMovement
finance 应收应付、收款付款、会计凭证 Invoice, Payment, AccountingEntry

你可能会问:为什么把商品放在inventory而不是单独建个product模块?我在项目中踩过这个坑。商品信息跟库存变动是强绑定的,分开后每次查库存都要跨模块join,性能差不说,逻辑还容易乱。

核心原则:每个模块只管自己的事。采购模块只管「跟供应商下订单」,别去管库存怎么扣减。库存变动通过信号(signal)或事件驱动来触发,模块之间通过接口通信,不直接调用对方的模型。

3.3 数据库模型设计原则

做ERP系统,数据库设计是地基。地基歪了,楼盖得再高也得塌。我总结了四条铁律,你记一下:

原则一:每个模型都要有审计字段

企业系统里,谁在什么时候创建了这条记录、最后谁修改的,这是刚需。我习惯写一个抽象基类:

# common/models.py
from django.db import models
from django.utils import timezone

class BaseModel(models.Model):
    created_at = models.DateTimeField(default=timezone.now, verbose_name="创建时间")
    updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
    created_by = models.CharField(max_length=64, blank=True, verbose_name="创建人")
    updated_by = models.CharField(max_length=64, blank=True, verbose_name="更新人")
    is_deleted = models.BooleanField(default=False, verbose_name="逻辑删除")

    class Meta:
        abstract = True

所有业务模型都继承这个BaseModel。注意那个is_deleted字段——企业数据不能物理删除,只能打标记。我曾经见过一个项目直接删数据,月底对账对不上,查了三天才发现是误删了。

原则二:用编码字段,别用自增ID做业务标识

采购单号、销售单号、商品编码,这些业务标识要单独设计。自增ID只做内部主键,对外展示用编码:

class PurchaseOrder(BaseModel):
    # 内部主键 Django 自动生成
    order_code = models.CharField(max_length=32, unique=True, verbose_name="采购单号")
    # ... 其他字段

    def save(self, *args, **kwargs):
        if not self.order_code:
            # 生成规则:PO + 日期 + 流水号
            today = timezone.now().strftime("%Y%m%d")
            last_order = PurchaseOrder.objects.filter(
                order_code__startswith=f"PO{today}"
            ).order_by('-order_code').first()
            if last_order:
                seq = int(last_order.order_code[-4:]) + 1
            else:
                seq = 1
            self.order_code = f"PO{today}{seq:04d}"
        super().save(*args, **kwargs)

为什么这么做?你想想看,客户打电话说「帮我查一下PO202403150001这个单子」,你总不能说「好的,请告诉我数据库自增ID是几」吧?

原则三:金额字段用Decimal,别用Float

这个坑我踩得特别深。有一次财务说系统算的税额跟发票差了一分钱,查了半天,是float精度问题。从那以后,所有金额字段我都用Decimal:

from django.db import models

class SalesItem(models.Model):
    quantity = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="数量")
    unit_price = models.DecimalField(max_digits=12, decimal_places=4, verbose_name="单价")
    tax_rate = models.DecimalField(max_digits=5, decimal_places=2, verbose_name="税率(%)")
    
    @property
    def subtotal(self):
        # 金额计算用Decimal,保证精度
        return self.quantity * self.unit_price
    
    @property
    def tax_amount(self):
        return self.subtotal * self.tax_rate / Decimal('100')

警告:Decimal的decimal_places要留够。单价我习惯用4位小数,因为有些商品按克计价,单价可能是0.1234元。总金额用2位就够了,但中间计算过程必须保留足够精度。

原则四:用外键关联,但别滥用

采购单关联供应商、采购明细关联商品,这些用外键没问题。但有些场景,比如历史快照,就不能用外键。举个例子:

class PurchaseItem(BaseModel):
    order = models.ForeignKey(
        PurchaseOrder, on_delete=models.CASCADE,
        related_name='items', verbose_name="所属订单"
    )
    product = models.ForeignKey(
        Product, on_delete=models.PROTECT,  # 注意:用PROTECT,别用CASCADE
        verbose_name="商品"
    )
    # 快照字段:记录下单时的商品信息
    product_name = models.CharField(max_length=128, verbose_name="商品名称(快照)")
    product_code = models.CharField(max_length=32, verbose_name="商品编码(快照)")
    product_spec = models.CharField(max_length=64, blank=True, verbose_name="规格(快照)")

为什么要有快照字段?因为商品信息会变。今天下单时商品叫「A型号」,明天改名叫「A+型号」了,但历史订单里还得显示原来的名字。用外键关联实时数据,历史记录就全乱了。

3.4 项目结构一览

最后,给你看看我习惯的项目目录结构:

erp_backend/
├── erp_backend/          # 项目配置
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── common/               # 公共模块
│   ├── models.py         # BaseModel
│   ├── utils.py          # 工具函数
│   └── signals.py        # 信号定义
├── purchase/             # 采购模块
│   ├── models.py
│   ├── views.py
│   └── serializers.py
├── sales/                # 销售模块
│   ├── models.py
│   ├── views.py
│   └── serializers.py
├── inventory/            # 库存模块
│   ├── models.py
│   ├── views.py
│   └── serializers.py
├── finance/              # 财务模块
│   ├── models.py
│   ├── views.py
│   └── serializers.py
└── manage.py

小提示:common模块不是Django应用,不需要注册到INSTALLED_APPS里。它就是个公共代码仓库,所有模块都能引用。另外,我习惯把信号(signals)单独放一个文件,别写在models.py里,不然导入顺序容易出问题。

好了,项目骨架搭起来了。下一章咱们开始往里面填肉——先做商品管理和库存台账,这是进销存系统的核心。到时候我会带你一步步写模型、做序列化、搭视图,把理论落地成代码。

记住:好的开始是成功的一半。项目初始化这一步,值得你花时间认真做。