forked from erp-dev/erp
885 lines
30 KiB
Python
885 lines
30 KiB
Python
from __future__ import annotations
|
||
|
||
from decimal import Decimal
|
||
from typing import List
|
||
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}'
|
||
|
||
@property
|
||
def human_id(self) -> str:
|
||
"""
|
||
人类可读编号(虚拟编号,不落库)。
|
||
|
||
规则与 SalesOrder.human_id 保持一致:CG + YYYYMMDD{6位id}
|
||
"""
|
||
if self.id is None or self.created_at is None:
|
||
return ""
|
||
return f"CG{self.created_at.year}{self.created_at.month:02d}{self.created_at.day:02d}{self.id:06d}"
|
||
|
||
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)
|
||
|
||
def split_quantity_of_rolls(self) -> List[int]:
|
||
if self.quantity_of_rolls:
|
||
return [int(value) for value in self.quantity_of_rolls.split(',') if value.strip()]
|
||
return []
|
||
|
||
|
||
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='状态',
|
||
)
|
||
order_quantity = models.IntegerField(null=True, blank=True, verbose_name='下单数量')
|
||
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
|
||
|
||
def __str__(self):
|
||
order_id = self.human_id or str(self.id)
|
||
return f'销售订单 {order_id} - {self.customer.name}'
|
||
|
||
@property
|
||
def human_id(self) -> str:
|
||
"""
|
||
人类可读编号(虚拟编号,不落库)。
|
||
|
||
规则:XS + YYYYMMDD{6位id}
|
||
"""
|
||
if self.id is None or self.created_at is None:
|
||
return ""
|
||
return f"XS{self.created_at.year}{self.created_at.month:02d}{self.created_at.day:02d}{self.id:06d}"
|
||
|
||
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='产品',
|
||
)
|
||
printing_job = models.ForeignKey(
|
||
'printing.PrintingJob',
|
||
on_delete=models.PROTECT,
|
||
related_name='sales_order_items',
|
||
null=True,
|
||
blank=True,
|
||
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.TextField(
|
||
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 split_quantity_of_rolls(self) -> List[int]:
|
||
if self.quantity_of_rolls:
|
||
return [int(value) for value in self.quantity_of_rolls.split(',') if value.strip()]
|
||
return []
|
||
|
||
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 PurchaseReturnStatusEnum(models.IntegerChoices):
|
||
PENDING = 1, '审批中'
|
||
APPROVED = 2, '审批通过'
|
||
CANCELLED = 3, '作废'
|
||
|
||
|
||
class PurchaseReturnOrder(OrderItemsAggregationMixin, OrderDirectionMixin, OrderCounterpartyMixin, ModelBase):
|
||
"""采购退货单(库存出库 + 供应商应付减少)"""
|
||
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey(
|
||
basic_info_models.Merchant,
|
||
on_delete=models.PROTECT,
|
||
related_name='purchase_return_orders',
|
||
verbose_name='所属商户',
|
||
)
|
||
supplier = models.ForeignKey(
|
||
basic_info_models.Supplier,
|
||
on_delete=models.PROTECT,
|
||
related_name='purchase_return_orders',
|
||
verbose_name='供应商',
|
||
)
|
||
purchase_order = models.ForeignKey(
|
||
'PurchaseOrder',
|
||
on_delete=models.SET_NULL,
|
||
related_name='return_orders',
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='原采购单',
|
||
)
|
||
return_date = models.DateField(verbose_name='退货日期')
|
||
operator = models.ForeignKey(
|
||
basic_info_models.Employee,
|
||
on_delete=models.PROTECT,
|
||
related_name='purchase_return_orders',
|
||
verbose_name='经办人',
|
||
)
|
||
warehouse = models.ForeignKey(
|
||
basic_info_models.WareHouse,
|
||
on_delete=models.PROTECT,
|
||
related_name='purchase_return_orders',
|
||
verbose_name='仓库',
|
||
)
|
||
status = models.IntegerField(
|
||
choices=PurchaseReturnStatusEnum.choices,
|
||
default=PurchaseReturnStatusEnum.PENDING,
|
||
verbose_name='状态',
|
||
)
|
||
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
|
||
|
||
class Meta:
|
||
verbose_name = '采购退货单'
|
||
verbose_name_plural = '采购退货单'
|
||
|
||
def __str__(self):
|
||
return f'采购退货 {self.id} - {self.supplier.name}'
|
||
|
||
@property
|
||
def human_id(self) -> str:
|
||
"""
|
||
人类可读编号(虚拟编号,不落库)。
|
||
|
||
规则与 SalesOrder.human_id 保持一致:CT + YYYYMMDD{6位id}
|
||
"""
|
||
if self.id is None or self.created_at is None:
|
||
return ""
|
||
return f"CT{self.created_at.year}{self.created_at.month:02d}{self.created_at.day:02d}{self.id:06d}"
|
||
|
||
def get_direction(self) -> int:
|
||
return -1
|
||
|
||
def get_counterparty_field_name(self) -> str:
|
||
return 'supplier'
|
||
|
||
|
||
class PurchaseReturnOrderItem(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
purchase_return_order = models.ForeignKey(
|
||
PurchaseReturnOrder,
|
||
on_delete=models.CASCADE,
|
||
related_name='items',
|
||
verbose_name='采购退货单',
|
||
)
|
||
product = models.ForeignKey(
|
||
basic_info_models.Product,
|
||
on_delete=models.PROTECT,
|
||
related_name='purchase_return_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 split_quantity_of_rolls(self) -> List[int]:
|
||
if self.quantity_of_rolls:
|
||
return [int(value) for value in self.quantity_of_rolls.split(',') if value.strip()]
|
||
return []
|
||
|
||
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 SalesReturnStatusEnum(models.IntegerChoices):
|
||
PENDING = 1, '审批中'
|
||
APPROVED = 2, '审批通过'
|
||
CANCELLED = 3, '作废'
|
||
|
||
|
||
class SalesReturnOrder(OrderItemsAggregationMixin, OrderDirectionMixin, OrderCounterpartyMixin, ModelBase):
|
||
"""销售退货单(库存入库 + 客户应收减少)"""
|
||
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey(
|
||
basic_info_models.Merchant,
|
||
on_delete=models.PROTECT,
|
||
related_name='sales_return_orders',
|
||
verbose_name='所属商户',
|
||
)
|
||
customer = models.ForeignKey(
|
||
basic_info_models.Customer,
|
||
on_delete=models.PROTECT,
|
||
related_name='sales_return_orders',
|
||
verbose_name='客户',
|
||
)
|
||
sales_order = models.ForeignKey(
|
||
'SalesOrder',
|
||
on_delete=models.SET_NULL,
|
||
related_name='return_orders',
|
||
null=True,
|
||
blank=True,
|
||
verbose_name='原销售单',
|
||
)
|
||
return_date = models.DateField(verbose_name='退货日期')
|
||
operator = models.ForeignKey(
|
||
basic_info_models.Employee,
|
||
on_delete=models.PROTECT,
|
||
related_name='sales_return_orders',
|
||
verbose_name='经办人',
|
||
)
|
||
warehouse = models.ForeignKey(
|
||
basic_info_models.WareHouse,
|
||
on_delete=models.PROTECT,
|
||
related_name='sales_return_orders',
|
||
verbose_name='仓库',
|
||
)
|
||
status = models.IntegerField(
|
||
choices=SalesReturnStatusEnum.choices,
|
||
default=SalesReturnStatusEnum.PENDING,
|
||
verbose_name='状态',
|
||
)
|
||
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
|
||
|
||
class Meta:
|
||
verbose_name = '销售退货单'
|
||
verbose_name_plural = '销售退货单'
|
||
|
||
def __str__(self):
|
||
return f'销售退货 {self.id} - {self.customer.name}'
|
||
|
||
@property
|
||
def human_id(self) -> str:
|
||
"""
|
||
人类可读编号(虚拟编号,不落库)。
|
||
|
||
规则与 SalesOrder.human_id 保持一致:XT + YYYYMMDD{6位id}
|
||
"""
|
||
if self.id is None or self.created_at is None:
|
||
return ""
|
||
return f"XT{self.created_at.year}{self.created_at.month:02d}{self.created_at.day:02d}{self.id:06d}"
|
||
|
||
def get_direction(self) -> int:
|
||
return 1
|
||
|
||
def get_counterparty_field_name(self) -> str:
|
||
return 'customer'
|
||
|
||
|
||
class SalesReturnOrderItem(ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
sales_return_order = models.ForeignKey(
|
||
SalesReturnOrder,
|
||
on_delete=models.CASCADE,
|
||
related_name='items',
|
||
verbose_name='销售退货单',
|
||
)
|
||
product = models.ForeignKey(
|
||
basic_info_models.Product,
|
||
on_delete=models.PROTECT,
|
||
related_name='sales_return_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)
|
||
|
||
def split_quantity_of_rolls(self) -> List[int]:
|
||
if self.quantity_of_rolls:
|
||
return [int(value) for value in self.quantity_of_rolls.split(',') if value.strip()]
|
||
return []
|
||
|
||
|
||
class PaymentOrderStatusEnum(models.IntegerChoices):
|
||
PENDING = 1, '审批中'
|
||
APPROVED = 2, '审批通过'
|
||
CANCELLED = 3, '作废'
|
||
|
||
|
||
class PaymentOrder(OrderDirectionMixin, OrderCounterpartyMixin, ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey(
|
||
basic_info_models.Merchant,
|
||
on_delete=models.PROTECT,
|
||
related_name='payment_orders',
|
||
verbose_name='所属商户',
|
||
)
|
||
supplier = models.ForeignKey(
|
||
basic_info_models.Supplier,
|
||
on_delete=models.PROTECT,
|
||
related_name='payment_orders',
|
||
verbose_name='供应商',
|
||
)
|
||
payment_date = models.DateField(verbose_name='付款日期')
|
||
amount = models.DecimalField(max_digits=15, decimal_places=2, verbose_name='金额')
|
||
discount_amount = models.DecimalField(
|
||
max_digits=15,
|
||
decimal_places=2,
|
||
default=Decimal('0'),
|
||
verbose_name='折扣金额',
|
||
)
|
||
bank_account = models.ForeignKey(
|
||
basic_info_models.BankAccount,
|
||
on_delete=models.PROTECT,
|
||
related_name='payment_orders',
|
||
verbose_name='银行账户',
|
||
blank=True,
|
||
null=True,
|
||
)
|
||
markup = models.CharField(max_length=255, blank=True, null=True, verbose_name='附言')
|
||
operator = models.ForeignKey(
|
||
basic_info_models.Employee,
|
||
on_delete=models.PROTECT,
|
||
related_name='payment_orders',
|
||
verbose_name='经办人',
|
||
)
|
||
status = models.IntegerField(
|
||
choices=PaymentOrderStatusEnum.choices,
|
||
default=PaymentOrderStatusEnum.PENDING,
|
||
verbose_name='状态',
|
||
)
|
||
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
|
||
|
||
class Meta:
|
||
verbose_name = '付款单'
|
||
verbose_name_plural = '付款单'
|
||
|
||
def __str__(self):
|
||
return f'付款单 {self.id} - {self.supplier.name}'
|
||
|
||
@property
|
||
def settlement_amount(self) -> Decimal:
|
||
discount = self.discount_amount or Decimal('0')
|
||
return self.amount + discount
|
||
|
||
def get_total_amount(self) -> Decimal:
|
||
return self.settlement_amount
|
||
|
||
def get_direction(self) -> int:
|
||
return -1
|
||
|
||
def get_counterparty_field_name(self) -> str:
|
||
return 'supplier'
|
||
|
||
|
||
class ReceiptOrderStatusEnum(models.IntegerChoices):
|
||
PENDING = 1, '审批中'
|
||
APPROVED = 2, '审批通过'
|
||
CANCELLED = 3, '作废'
|
||
|
||
|
||
class ReceiptOrder(OrderDirectionMixin, OrderCounterpartyMixin, ModelBase):
|
||
id = models.BigAutoField(primary_key=True)
|
||
merchant = models.ForeignKey(
|
||
basic_info_models.Merchant,
|
||
on_delete=models.PROTECT,
|
||
related_name='receipt_orders',
|
||
verbose_name='所属商户',
|
||
)
|
||
customer = models.ForeignKey(
|
||
basic_info_models.Customer,
|
||
on_delete=models.PROTECT,
|
||
related_name='receipt_orders',
|
||
verbose_name='客户',
|
||
)
|
||
receipt_date = models.DateField(verbose_name='收款日期')
|
||
amount = models.DecimalField(max_digits=15, decimal_places=2, verbose_name='金额')
|
||
discount_amount = models.DecimalField(
|
||
max_digits=15,
|
||
decimal_places=2,
|
||
default=Decimal('0'),
|
||
verbose_name='折扣金额',
|
||
)
|
||
bank_account = models.ForeignKey(
|
||
basic_info_models.BankAccount,
|
||
on_delete=models.PROTECT,
|
||
related_name='receipt_orders',
|
||
verbose_name='银行账户',
|
||
blank=True,
|
||
null=True,
|
||
)
|
||
markup = models.CharField(max_length=255, blank=True, null=True, verbose_name='附言')
|
||
operator = models.ForeignKey(
|
||
basic_info_models.Employee,
|
||
on_delete=models.PROTECT,
|
||
related_name='receipt_orders',
|
||
verbose_name='经办人',
|
||
)
|
||
status = models.IntegerField(
|
||
choices=ReceiptOrderStatusEnum.choices,
|
||
default=ReceiptOrderStatusEnum.PENDING,
|
||
verbose_name='状态',
|
||
)
|
||
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
|
||
|
||
class Meta:
|
||
verbose_name = '收款单'
|
||
verbose_name_plural = '收款单'
|
||
|
||
def __str__(self):
|
||
return f'收款单 {self.id} - {self.customer.name}'
|
||
|
||
@property
|
||
def settlement_amount(self) -> Decimal:
|
||
discount = self.discount_amount or Decimal('0')
|
||
return self.amount + discount
|
||
|
||
def get_total_amount(self) -> Decimal:
|
||
return self.settlement_amount
|
||
|
||
def get_direction(self) -> int:
|
||
return -1
|
||
|
||
def get_counterparty_field_name(self) -> str:
|
||
return 'customer'
|
||
|
||
|
||
class SupplierBalance(ModelBase):
|
||
merchant = models.ForeignKey(
|
||
basic_info_models.Merchant,
|
||
on_delete=models.PROTECT,
|
||
related_name='supplier_balances',
|
||
verbose_name='所属商户',
|
||
)
|
||
supplier = models.ForeignKey(
|
||
basic_info_models.Supplier,
|
||
on_delete=models.PROTECT,
|
||
related_name='balances',
|
||
verbose_name='供应商',
|
||
)
|
||
balance = models.DecimalField(max_digits=15, decimal_places=2, default=Decimal('0'), verbose_name='应付余额')
|
||
|
||
class Meta:
|
||
verbose_name = '供应商余额'
|
||
verbose_name_plural = '供应商余额'
|
||
unique_together = ('merchant', 'supplier')
|
||
|
||
|
||
class CustomerBalance(ModelBase):
|
||
merchant = models.ForeignKey(
|
||
basic_info_models.Merchant,
|
||
on_delete=models.PROTECT,
|
||
related_name='customer_balances',
|
||
verbose_name='所属商户',
|
||
)
|
||
customer = models.ForeignKey(
|
||
basic_info_models.Customer,
|
||
on_delete=models.PROTECT,
|
||
related_name='balances',
|
||
verbose_name='客户',
|
||
)
|
||
balance = models.DecimalField(max_digits=15, decimal_places=2, default=Decimal('0'), verbose_name='应收余额')
|
||
|
||
class Meta:
|
||
verbose_name = '客户余额'
|
||
verbose_name_plural = '客户余额'
|
||
unique_together = ('merchant', 'customer')
|
||
|
||
|
||
class BalanceChangeTargetEnum(models.IntegerChoices):
|
||
SUPPLIER = 1, '供应商'
|
||
CUSTOMER = 2, '客户'
|
||
|
||
|
||
class BalanceChangeSourceEnum(models.IntegerChoices):
|
||
PURCHASE_ORDER = 1, '采购单'
|
||
SALES_ORDER = 2, '销售单'
|
||
PAYMENT_ORDER = 3, '付款单'
|
||
RECEIPT_ORDER = 4, '收款单'
|
||
PURCHASE_RETURN_ORDER = 5, '采购退货单'
|
||
SALES_RETURN_ORDER = 6, '销售退货单'
|
||
|
||
|
||
class BalanceChangeDirectionEnum(models.IntegerChoices):
|
||
INCREASE = 1, '增加'
|
||
DECREASE = 2, '减少'
|
||
|
||
|
||
class BalanceChangeRecord(ModelBase):
|
||
merchant = models.ForeignKey(
|
||
basic_info_models.Merchant,
|
||
on_delete=models.PROTECT,
|
||
related_name='balance_change_records',
|
||
verbose_name='所属商户',
|
||
)
|
||
supplier = models.ForeignKey(
|
||
basic_info_models.Supplier,
|
||
on_delete=models.PROTECT,
|
||
related_name='balance_change_records',
|
||
verbose_name='供应商',
|
||
null=True,
|
||
blank=True,
|
||
)
|
||
customer = models.ForeignKey(
|
||
basic_info_models.Customer,
|
||
on_delete=models.PROTECT,
|
||
related_name='balance_change_records',
|
||
verbose_name='客户',
|
||
null=True,
|
||
blank=True,
|
||
)
|
||
target_type = models.IntegerField(
|
||
choices=BalanceChangeTargetEnum.choices,
|
||
verbose_name='业务主体类型',
|
||
)
|
||
source_type = models.IntegerField(
|
||
choices=BalanceChangeSourceEnum.choices,
|
||
verbose_name='来源业务类型',
|
||
)
|
||
source_id = models.BigIntegerField(verbose_name='来源业务ID')
|
||
delta = models.DecimalField(max_digits=15, decimal_places=2, verbose_name='变动金额')
|
||
direction = models.IntegerField(
|
||
choices=BalanceChangeDirectionEnum.choices,
|
||
verbose_name='方向',
|
||
)
|
||
balance_before = models.DecimalField(max_digits=15, decimal_places=2, verbose_name='变动前余额')
|
||
balance_after = models.DecimalField(max_digits=15, decimal_places=2, verbose_name='变动后余额')
|
||
request_id = models.CharField(max_length=64, null=True, blank=True, verbose_name='幂等请求ID')
|
||
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
|
||
extra_meta = models.JSONField(default=dict, blank=True, verbose_name='扩展信息')
|
||
offset_to = models.BigIntegerField(null=True, blank=True, verbose_name='冲抵目标ID')
|
||
offset_at = models.DateTimeField(null=True, blank=True, verbose_name='冲抵时间')
|
||
offset_id = models.BigIntegerField(null=True, blank=True, verbose_name='冲抵来源ID')
|
||
cancelled = models.BooleanField(default=False, verbose_name='已被冲抵')
|
||
cancelled_at = models.DateTimeField(null=True, blank=True, verbose_name='被冲抵时间')
|
||
|
||
class Meta:
|
||
verbose_name = '余额变动记录'
|
||
verbose_name_plural = '余额变动记录'
|
||
indexes = [
|
||
models.Index(fields=['merchant', 'source_type', 'source_id'], name='balance_change_source_idx'),
|
||
]
|
||
constraints = [
|
||
models.CheckConstraint(
|
||
check=(
|
||
(models.Q(supplier__isnull=False, customer__isnull=True))
|
||
| (models.Q(supplier__isnull=True, customer__isnull=False))
|
||
),
|
||
name='balance_change_single_counterparty',
|
||
),
|
||
]
|
||
|
||
def __str__(self):
|
||
counterparty = self.supplier or self.customer
|
||
return f'余额变动 {self.id} - {counterparty}'
|