from django.db import models from django.core.exceptions import ValidationError from flower.common import ModelBase from basic_info import models as basic_info_models from functools import reduce import decimal # ==================== 枚举类定义 ==================== class StockFreezeStatusEnum(models.IntegerChoices): """库存冻结状态类型""" FROZEN = 1, '冻结中' CANCELLED = 2, '已取消' COMPLETED = 3, '已成单' class StockChangeSourceEnum(models.IntegerChoices): """库存变动来源类型""" PURCHASE = 1, '采购' SALES_RETURN = 2, '销退' TRANSPORT_IN = 3, '调入' RECHECK_ADD = 4, '盘盈' SALES = 5, '销售' PURCHASE_RETURN = 6, '采购退货' TRANSPORT_OUT = 7, '调出' RECHECK_REMOVE = 8, '盘亏' class StockChangeTypeEnum(models.IntegerChoices): """库存变动类型""" ADD = 1, '入库' REMOVE = 2, '出库' # ==================== 模型定义 ==================== class PurchaseOrder(ModelBase): """采购单模型""" id = models.BigAutoField(primary_key=True) merchant = models.ForeignKey( basic_info_models.Merchant, on_delete=models.PROTECT, related_name='purchase_orders', verbose_name='所属商户' ) supplier = models.ForeignKey(basic_info_models.Supplier, on_delete=models.PROTECT, related_name='purchase_orders', verbose_name='供应商') order_date = models.DateField(verbose_name='订单日期') total_amount = models.DecimalField(max_digits=15, decimal_places=2, verbose_name='总金额') remarks = models.TextField(blank=True, null=True, verbose_name='备注') def __str__(self): return f'采购订单 {self.id} - {self.supplier.name}' class Meta: verbose_name = '采购单' verbose_name_plural = '采购单' class StockChangeRecord(ModelBase): """库存变动记录模型""" id = models.BigAutoField(primary_key=True) merchant = models.ForeignKey( basic_info_models.Merchant, on_delete=models.PROTECT, related_name='stock_change_records', verbose_name='所属商户' ) type = models.IntegerField(choices=StockChangeTypeEnum.choices, verbose_name='变动类型') warehouse = models.ForeignKey( basic_info_models.WareHouse, on_delete=models.PROTECT, related_name='stock_change_records', verbose_name='仓库', ) source_type = models.IntegerField( choices=StockChangeSourceEnum.choices, verbose_name='变动来源', ) created_by = models.ForeignKey( 'auth.User', on_delete=models.PROTECT, null=True, blank=True, verbose_name='操作人', ) source_id = models.BigIntegerField(null=True, blank=True, verbose_name='来源单据ID') is_finished = models.BooleanField(default=False, verbose_name='完成出入库') finished_at = models.DateTimeField(null=True, blank=True, verbose_name='完成时间') remarks = models.TextField(blank=True, null=True, verbose_name='备注') def __str__(self): return f'出入库记录 {self.id} - 仓库:{self.warehouse.name}' class Meta: verbose_name = '出入库记录' verbose_name_plural = '出入库记录' def clean(self): if self.type == StockChangeTypeEnum.ADD: if self.source_type > 4: raise ValidationError('入库记录的来源类型无效') elif self.type == StockChangeTypeEnum.REMOVE: if self.source_type < 5: raise ValidationError('出库记录的来源类型无效') def save(self, *args, **kwargs): self.full_clean() super().save(*args, **kwargs) @property def is_incoming(self) -> bool: """判断是否为入库记录""" return self.type == StockChangeTypeEnum.ADD @property def is_outgoing(self) -> bool: """判断是否为出库记录""" return self.type == StockChangeTypeEnum.REMOVE @property def num_of_rolls(self) -> int: """获取匹数""" self.details.count() return self.details.count() @property def delta_num_of_rolls(self) -> int: """计算库存变动的匹数增减""" # 入库为正,出库为负 if self.type == StockChangeTypeEnum.ADD: return self.num_of_rolls elif self.type == StockChangeTypeEnum.REMOVE: return self.num_of_rolls * -1 else: raise ValueError("未知的库存变动类型") class StockChangeDetail(ModelBase): """库存变动明细模型""" id = models.BigAutoField(primary_key=True) merchant = models.ForeignKey( basic_info_models.Merchant, on_delete=models.PROTECT, related_name='stock_change_details', verbose_name='所属商户' ) product = models.ForeignKey( basic_info_models.Product, on_delete=models.PROTECT, related_name='stock_change_details', verbose_name='产品', ) unit = models.IntegerField( verbose_name='单位', choices=basic_info_models.ProductUnitEnum.choices, ) stock_change_record = models.ForeignKey( StockChangeRecord, on_delete=models.CASCADE, related_name='details', verbose_name='库存变动记录', ) quantity = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='数量') unit = models.IntegerField( verbose_name='单位', choices=basic_info_models.ProductUnitEnum.choices, ) def __str__(self): return f'库存变动明细 {self.id} - 记录ID: {self.stock_change_record.id}' class Meta: verbose_name = '库存变动明细' verbose_name_plural = '库存变动明细' class Inventory(ModelBase): """库存模型""" id = models.BigAutoField(primary_key=True) merchant = models.ForeignKey( basic_info_models.Merchant, on_delete=models.PROTECT, related_name='inventories', verbose_name='所属商户' ) product = models.ForeignKey( basic_info_models.Product, on_delete=models.PROTECT, related_name='inventories', verbose_name='产品', ) warehouse = models.ForeignKey( basic_info_models.WareHouse, on_delete=models.PROTECT, related_name='inventories', verbose_name='仓库', ) quantity = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='库存数量') num_of_rolls = models.IntegerField(default=1, verbose_name='匹数') minimum_quantity = models.IntegerField(null=True, blank=True, verbose_name='最低库存量') spec = models.CharField(max_length=100, blank=True, null=True, verbose_name='规格') description = models.TextField(blank=True, null=True, verbose_name='备注描述') def __str__(self): return f'库存 {self.product.name} - 仓库: {self.warehouse.name}' class Meta: verbose_name = '库存' verbose_name_plural = '库存' unique_together = ('product', 'warehouse') class StockSnapshot(ModelBase): """库存变动快照模型""" id = models.BigAutoField(primary_key=True) merchant = models.ForeignKey( basic_info_models.Merchant, on_delete=models.PROTECT, related_name='stock_snapshots', verbose_name='所属商户' ) product = models.ForeignKey( basic_info_models.Product, on_delete=models.PROTECT, related_name='stock_snapshots', verbose_name='产品', ) warehouse = models.ForeignKey( basic_info_models.WareHouse, on_delete=models.PROTECT, related_name='stock_snapshots', verbose_name='仓库', ) delta = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='库存变动数量') quantity_before = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='变动前库存数量') quantity_after = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='变动后库存数量') stock_change_record = models.ForeignKey( StockChangeRecord, on_delete=models.PROTECT, related_name='stock_snapshots', verbose_name='关联的库存变动记录', ) unit = models.IntegerField( verbose_name='单位', choices=basic_info_models.ProductUnitEnum.choices, ) num_of_rolls = models.IntegerField(default=1, verbose_name='匹数') # offset_to表示该快照被冲抵到的目标快照ID # 当offset_to不为空时,cancelled不可为True 及 offset_id只能为空 # 因为该记录是冲抵记录无法再取消,也不可能冲抵的同时被冲抵 offset_to = models.BigIntegerField( null=True, blank=True, verbose_name='冲抵目标ID', ) offset_at = models.DateTimeField( null=True, blank=True, verbose_name='冲抵时间', ) # 当cancelled为True时,offset_id不可为空,表示该快照已被冲抵 cancelled = models.BooleanField(default=False, verbose_name='已取消') cancelled_at = models.DateTimeField( null=True, blank=True, verbose_name='取消时间', ) offset_id = models.BigIntegerField( null=True, blank=True, verbose_name='冲抵记录ID', ) def __str__(self): return f'库存快照 {self.id} - 产品: {self.product.name}' class Meta: verbose_name = '库存快照' verbose_name_plural = '库存快照' class StockFreeze(ModelBase): """库存冻结模型""" id = models.BigAutoField(primary_key=True) merchant = models.ForeignKey( basic_info_models.Merchant, on_delete=models.PROTECT, related_name='stock_freezes', verbose_name='所属商户' ) product = models.ForeignKey( basic_info_models.Product, on_delete=models.PROTECT, related_name='stock_freezes', verbose_name='产品', ) warehouse = models.ForeignKey( basic_info_models.WareHouse, on_delete=models.PROTECT, related_name='stock_freezes', verbose_name='仓库', ) stock_detail = models.ForeignKey( StockChangeDetail, on_delete=models.PROTECT, related_name='stock_freezes', verbose_name='关联的库存变动明细', ) quantity = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='冻结数量') unit = models.IntegerField( verbose_name='单位', choices=basic_info_models.ProductUnitEnum.choices, ) status = models.IntegerField( choices=StockFreezeStatusEnum.choices, default=StockFreezeStatusEnum.FROZEN, verbose_name='冻结状态', ) frozen_by = models.ForeignKey( 'auth.User', on_delete=models.PROTECT, verbose_name='冻结操作人', ) frozen_with = models.BigIntegerField(null=True, blank=True, verbose_name='冻结关联ID') cancelled_by = models.ForeignKey( 'auth.User', on_delete=models.PROTECT, null=True, blank=True, related_name='cancelled_stock_freezes', verbose_name='取消操作人', ) completed_by = models.ForeignKey( 'auth.User', on_delete=models.PROTECT, null=True, blank=True, related_name='completed_stock_freezes', verbose_name='成单操作人', ) completed_at = models.DateTimeField(null=True, blank=True, verbose_name='成单时间') completed_with = models.BigIntegerField(null=True, blank=True, verbose_name='成单关联ID') cancelled_at = models.DateTimeField(null=True, blank=True, verbose_name='取消时间') reason = models.TextField(blank=True, null=True, verbose_name='冻结原因') def __str__(self): return f'库存冻结 {self.id} - 产品: {self.product.name}' class Meta: verbose_name = '库存冻结' verbose_name_plural = '库存冻结'