from __future__ import annotations from decimal import Decimal from django.db import models from flower.common import ModelBase from basic_info import models as basic_info_models class PurchaseOrderKindEnum(models.IntegerChoices): """采购单类型""" WHOLESALE = 1, '大货' SAMPLE = 2, '样板' class PurchaseOrderStatusEnum(models.IntegerChoices): """采购单状态""" PENDING = 1, '审批中' APPROVED = 2, '审批通过' CANCELLED = 3, '作废' class OrderItemsAggregationMixin: """ 提供订单金额/数量聚合的通用实现。 默认读取 `items` 关联管理器,可在子类通过 `order_items_accessor` 覆盖以适配不同的关联名称。 """ order_items_accessor = 'items' def _iter_order_items(self): return getattr(self, self.order_items_accessor).all() def get_total_amount(self) -> Decimal: total = Decimal('0') for item in self._iter_order_items(): total += Decimal(item.total_amount()) return total def get_total_quantity(self) -> Decimal: total = Decimal('0') for item in self._iter_order_items(): total += Decimal(item.quantity) return total def get_total_diff_quantity(self) -> Decimal: total = Decimal('0') for item in self._iter_order_items(): total += Decimal(item.diff_quantity()) return total class OrderDirectionMixin: """提供带方向金额的通用实现,子类只需要覆盖方向。""" def get_direction(self) -> int: """ 返回订单方向: 入库 / 正向金额: 1 出库 / 负向金额: -1 """ def get_signed_total_amount(self) -> Decimal: return self.get_total_amount() * Decimal(self.get_direction()) class OrderCounterpartyMixin: """ 抽象出“订单关联业务主体”的能力,子类只需指出字段名称, 即可通过统一接口获取 Supplier / Customer 等对象。 """ counterparty_field_name = '' def get_counterparty_field_name(self) -> str: """返回用于存放业务主体的字段名,如 supplier / customer。""" def get_counterparty(self) -> basic_info_models.Supplier | basic_info_models.Customer: field_name = self.get_counterparty_field_name() or self.counterparty_field_name if not field_name: raise AttributeError('未定义 counterparty 字段名称') try: counterparty = getattr(self, field_name) except AttributeError as exc: raise AttributeError(f'找不到字段 {field_name}') from exc if counterparty is None: raise ValueError(f'{field_name} 未设置,无法获取业务主体') return counterparty class PurchaseOrder(OrderItemsAggregationMixin, OrderDirectionMixin, OrderCounterpartyMixin, 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='供应商', ) purchase_date = models.DateField(verbose_name='采购日期') kind = models.IntegerField( choices=PurchaseOrderKindEnum.choices, default=PurchaseOrderKindEnum.WHOLESALE, verbose_name='采购单类型', ) operator = models.ForeignKey( basic_info_models.Employee, on_delete=models.PROTECT, related_name='purchase_orders', verbose_name='经办人', ) warehouse = models.ForeignKey( basic_info_models.WareHouse, on_delete=models.PROTECT, related_name='purchase_orders', verbose_name='仓库', ) status = models.IntegerField( choices=PurchaseOrderStatusEnum.choices, default=PurchaseOrderStatusEnum.PENDING, 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 = '采购单' def get_direction(self) -> int: return 1 def get_counterparty_field_name(self) -> str: return 'supplier' class PurchaseOrderItem(ModelBase): """采购单明细模型""" id = models.BigAutoField(primary_key=True) purchase_order = models.ForeignKey( PurchaseOrder, on_delete=models.CASCADE, related_name='items', verbose_name='采购单', ) product = models.ForeignKey( basic_info_models.Product, on_delete=models.PROTECT, related_name='purchase_order_items', verbose_name='产品', ) price = models.DecimalField(max_digits=15, decimal_places=2, verbose_name='单价') color = models.CharField(max_length=50, null=True, blank=True, verbose_name='颜色') quantity = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='数量') unit = models.CharField(max_length=50, verbose_name='单位') spec = models.CharField(max_length=100, null=True, blank=True, verbose_name='规格') empty_diff_percent = models.DecimalField( max_digits=15, decimal_places=2, verbose_name='空差百分比', ) # if purchase_order.warehouse.mode != WarehouseModeEnum.UNRESTRICTED, then quantity_of_rolls is required quantity_of_rolls = models.CharField( max_length=255, null=True, blank=True, verbose_name='各条数数量', ) num_of_rolls = models.PositiveIntegerField(default=1, verbose_name='条数') batch_number = models.CharField(null=True, blank=True, max_length=100, verbose_name='批次号') remarks = models.TextField(blank=True, null=True, verbose_name='备注') class Meta: verbose_name = '采购单明细' verbose_name_plural = '采购单明细' def real_quantity(self): return round(self.quantity * (1 - self.empty_diff_percent / 100), 2) def diff_quantity(self): return round(self.quantity * (self.empty_diff_percent / 100), 2) def total_amount(self): return round(self.price * self.real_quantity(), 2) class SalesOrderKindEnum(models.IntegerChoices): """销售单类型""" WHOLESALE = 1, '大货' SAMPLE = 2, '样板' class SalesOrderStatusEnum(models.IntegerChoices): """销售单状态""" PENDING = 1, '审批中' APPROVED = 2, '审批通过' CANCELLED = 3, '作废' class SalesOrder(OrderItemsAggregationMixin, OrderDirectionMixin, OrderCounterpartyMixin, ModelBase): """销售单模型""" id = models.BigAutoField(primary_key=True) merchant = models.ForeignKey( basic_info_models.Merchant, on_delete=models.PROTECT, related_name='sales_orders', verbose_name='所属商户', ) customer = models.ForeignKey( basic_info_models.Customer, on_delete=models.PROTECT, related_name='sales_orders', verbose_name='客户', ) sales_date = models.DateField(verbose_name='销售日期') kind = models.IntegerField( choices=SalesOrderKindEnum.choices, default=SalesOrderKindEnum.WHOLESALE, verbose_name='销售单类型', ) operator = models.ForeignKey( basic_info_models.Employee, on_delete=models.PROTECT, related_name='sales_orders', verbose_name='经办人', ) warehouse = models.ForeignKey( basic_info_models.WareHouse, on_delete=models.PROTECT, related_name='sales_orders', verbose_name='仓库', ) status = models.IntegerField( choices=SalesOrderStatusEnum.choices, default=SalesOrderStatusEnum.PENDING, verbose_name='状态', ) remarks = models.TextField(blank=True, null=True, verbose_name='备注') def __str__(self): return f'销售订单 {self.id} - {self.customer.name}' class Meta: verbose_name = '销售单' verbose_name_plural = '销售单' def get_direction(self) -> int: return -1 def get_counterparty_field_name(self) -> str: return 'customer' class SalesOrderItem(ModelBase): """销售单明细模型""" id = models.BigAutoField(primary_key=True) sales_order = models.ForeignKey( SalesOrder, on_delete=models.CASCADE, related_name='items', verbose_name='销售单', ) product = models.ForeignKey( basic_info_models.Product, on_delete=models.PROTECT, related_name='sales_order_items', verbose_name='产品', ) price = models.DecimalField(max_digits=15, decimal_places=2, verbose_name='单价') color = models.CharField(max_length=50, null=True, blank=True, verbose_name='颜色') quantity = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='数量') unit = models.CharField(max_length=50, verbose_name='单位') spec = models.CharField(max_length=100, null=True, blank=True, verbose_name='规格') empty_diff_percent = models.DecimalField( max_digits=15, decimal_places=2, verbose_name='空差百分比', ) quantity_of_rolls = models.CharField( max_length=255, null=True, blank=True, verbose_name='各条数数量', ) num_of_rolls = models.PositiveIntegerField(default=1, verbose_name='条数') consume_detail_ids = models.CharField( max_length=255, null=True, blank=True, verbose_name='消耗入库明细ID列表', ) batch_number = models.CharField(null=True, blank=True, max_length=100, verbose_name='批次号') remarks = models.TextField(blank=True, null=True, verbose_name='备注') class Meta: verbose_name = '销售单明细' verbose_name_plural = '销售单明细' def real_quantity(self): return round(self.quantity * (1 - self.empty_diff_percent / 100), 2) def diff_quantity(self): return round(self.quantity * (self.empty_diff_percent / 100), 2) def total_amount(self): return round(self.price * self.real_quantity(), 2)