1
0
forked from erp-dev/erp
Files
erpnew/business/models.py
2025-11-30 17:06:48 +08:00

319 lines
10 KiB
Python

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)