185 lines
5.4 KiB
Python
185 lines
5.4 KiB
Python
"""商品中心模型:Category / Brand / Unit / Product。
|
||
|
||
预留点:
|
||
- Product.ext_data:自定义属性 JSON(行业差异)
|
||
- Product.source_channel:manual/api/web/miniprogram/ai_agent
|
||
- Product.is_deleted:软删除
|
||
"""
|
||
|
||
from decimal import Decimal
|
||
|
||
from django.db import models
|
||
from apps.core.base_models import TenantScopedModel
|
||
|
||
|
||
class Category(TenantScopedModel):
|
||
"""商品分类(树形结构)。"""
|
||
|
||
code = models.CharField(max_length=64, help_text="分类代码")
|
||
name = models.CharField(max_length=128)
|
||
parent = models.ForeignKey(
|
||
"self",
|
||
null=True,
|
||
blank=True,
|
||
on_delete=models.SET_NULL,
|
||
related_name="children",
|
||
)
|
||
sort_order = models.IntegerField(default=0)
|
||
|
||
class Meta:
|
||
db_table = "catalog_category"
|
||
unique_together = [("tenant", "code")]
|
||
verbose_name_plural = "商品分类"
|
||
ordering = ["sort_order", "code"]
|
||
|
||
def __str__(self):
|
||
return f"{self.code} {self.name}"
|
||
|
||
|
||
class Brand(TenantScopedModel):
|
||
"""品牌。"""
|
||
|
||
code = models.CharField(max_length=64)
|
||
name = models.CharField(max_length=128)
|
||
logo_url = models.URLField(blank=True, default="")
|
||
|
||
class Meta:
|
||
db_table = "catalog_brand"
|
||
unique_together = [("tenant", "code")]
|
||
verbose_name_plural = "品牌"
|
||
ordering = ["code"]
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
|
||
class Unit(TenantScopedModel):
|
||
"""计量单位(箱/瓶/件/kg)。"""
|
||
|
||
code = models.CharField(max_length=16, help_text="单位代码,如 box/bottle/kg")
|
||
name = models.CharField(max_length=32)
|
||
is_base = models.BooleanField(default=True, help_text="是否基本单位")
|
||
|
||
class Meta:
|
||
db_table = "catalog_unit"
|
||
unique_together = [("tenant", "code")]
|
||
verbose_name_plural = "计量单位"
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
|
||
class Product(TenantScopedModel):
|
||
"""商品档案。"""
|
||
|
||
STATUS_ACTIVE = "active"
|
||
STATUS_INACTIVE = "inactive"
|
||
STATUS_CHOICES = [
|
||
(STATUS_ACTIVE, "启用"),
|
||
(STATUS_INACTIVE, "停用"),
|
||
]
|
||
|
||
code = models.CharField(max_length=64, help_text="商品编码(租户内唯一)")
|
||
name = models.CharField(max_length=255)
|
||
spec = models.CharField(max_length=255, blank=True, default="", help_text="规格")
|
||
barcode = models.CharField(max_length=64, blank=True, default="")
|
||
|
||
category = models.ForeignKey(
|
||
Category,
|
||
null=True,
|
||
blank=True,
|
||
on_delete=models.SET_NULL,
|
||
related_name="products",
|
||
)
|
||
brand = models.ForeignKey(
|
||
Brand,
|
||
null=True,
|
||
blank=True,
|
||
on_delete=models.SET_NULL,
|
||
related_name="products",
|
||
)
|
||
base_unit = models.ForeignKey(
|
||
Unit,
|
||
null=True,
|
||
blank=True,
|
||
on_delete=models.SET_NULL,
|
||
related_name="products",
|
||
)
|
||
|
||
# 价格字段
|
||
cost_price = models.DecimalField(
|
||
max_digits=18, decimal_places=4, default=0, help_text="参考成本价"
|
||
)
|
||
sale_price = models.DecimalField(
|
||
max_digits=18, decimal_places=4, default=0, help_text="默认销售价"
|
||
)
|
||
min_sale_price = models.DecimalField(
|
||
max_digits=18,
|
||
decimal_places=4,
|
||
default=0,
|
||
help_text="最低售价(0=不限制;低于需审批放行)",
|
||
)
|
||
|
||
# 批次/效期(食品、医药等批次管理行业)
|
||
is_batch_managed = models.BooleanField(
|
||
default=False, help_text="是否启用批次管理(出库按近效期优先 FEFO)"
|
||
)
|
||
shelf_life_days = models.IntegerField(
|
||
null=True, blank=True, help_text="保质期天数(用于入库时推算默认到期日)"
|
||
)
|
||
|
||
# 税率(批次 D4):默认 13%(一般纳税人常见档),可按商品覆盖
|
||
tax_rate = models.DecimalField(
|
||
max_digits=6, decimal_places=4, default=Decimal("0.13"),
|
||
help_text="增值税率(0.13 = 13%);0 = 免税/不启用",
|
||
)
|
||
|
||
status = models.CharField(
|
||
max_length=16, choices=STATUS_CHOICES, default=STATUS_ACTIVE
|
||
)
|
||
|
||
class Meta:
|
||
db_table = "catalog_product"
|
||
unique_together = [("tenant", "code")]
|
||
verbose_name_plural = "商品档案"
|
||
indexes = [
|
||
models.Index(fields=["tenant", "code"]),
|
||
models.Index(fields=["tenant", "name"]),
|
||
models.Index(fields=["tenant", "barcode"]),
|
||
models.Index(fields=["tenant", "category", "-created_at"]),
|
||
]
|
||
ordering = ["code"]
|
||
|
||
def __str__(self):
|
||
return f"{self.code} {self.name}"
|
||
|
||
|
||
class UnitConversion(TenantScopedModel):
|
||
"""商品多单位换算:1 个该单位 = rate × 基本单位。
|
||
|
||
例:商品基本单位"瓶",换算"箱" rate=24 → 1 箱 = 24 瓶。
|
||
"""
|
||
|
||
product = models.ForeignKey(
|
||
Product,
|
||
on_delete=models.CASCADE,
|
||
related_name="unit_conversions",
|
||
)
|
||
unit = models.ForeignKey(
|
||
Unit,
|
||
on_delete=models.PROTECT,
|
||
related_name="conversions",
|
||
)
|
||
rate = models.DecimalField(
|
||
max_digits=18, decimal_places=6, help_text="1 个该单位 = rate × 基本单位"
|
||
)
|
||
|
||
class Meta:
|
||
db_table = "catalog_unit_conversion"
|
||
unique_together = [("tenant", "product", "unit")]
|
||
verbose_name_plural = "商品单位换算"
|
||
ordering = ["product", "unit"]
|
||
|
||
def __str__(self):
|
||
return f"{self.product.code}:{self.unit.code}×{self.rate}"
|