1
0
forked from erp-dev/erp
Files
erpnew/business/models.py

1026 lines
34 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from decimal import Decimal
from typing import List
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
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='状态',
)
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='批次号')
order_quantity = models.IntegerField(null=True, blank=True, 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 PreSalesOrder(ModelBase):
"""预销售单
预销售单用于在客户下达采购/要货指令时,提前通知仓库进行库存核对与调货。
目前阶段:仅定义 PreSalesOrder 与 PreSalesOrderItem 的一对多关系,
并关联 customer 与 created_by其他业务对象如 Warehouse / StockChangeRecord / StockFreeze 等)
暂不在此模型中建立关联。
"""
id = models.BigAutoField(primary_key=True)
merchant = models.ForeignKey(
basic_info_models.Merchant,
on_delete=models.PROTECT,
related_name='pre_sales_orders',
verbose_name='所属商户',
)
customer = models.ForeignKey(
basic_info_models.Customer,
on_delete=models.PROTECT,
related_name='pre_sales_orders',
verbose_name='客户',
)
created_by = models.ForeignKey(
basic_info_models.Employee,
on_delete=models.PROTECT,
related_name='created_pre_sales_orders',
null=True,
blank=True,
verbose_name='创建者',
)
warehouse = models.ForeignKey(
basic_info_models.WareHouse,
on_delete=models.PROTECT,
related_name='pre_sales_orders',
verbose_name='仓库',
)
kind = models.IntegerField(
choices=SalesOrderKindEnum.choices,
default=SalesOrderKindEnum.WHOLESALE,
verbose_name='预销售单类型',
)
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
def __str__(self):
return f'预销售单 {self.human_id or self.id}'
def clean(self):
super().clean()
errors = {}
if getattr(self, 'merchant_id', None) and getattr(self, 'customer_id', None):
if self.customer.merchant_id != self.merchant_id:
errors['customer'] = '客户所属商户与预销售单所属商户不一致'
if getattr(self, 'merchant_id', None) and getattr(self, 'warehouse_id', None):
if self.warehouse.merchant_id != self.merchant_id:
errors['warehouse'] = '仓库所属商户与预销售单所属商户不一致'
if getattr(self, 'created_by_id', None) and getattr(self, 'merchant_id', None):
if self.created_by.merchant_id != self.merchant_id:
errors['created_by'] = '创建者所属商户与预销售单所属商户不一致'
if errors:
raise ValidationError(errors)
@property
def customer_name(self) -> str:
if getattr(self, 'customer_id', None):
return self.customer.name
return ''
@property
def merchant_name(self) -> str:
if getattr(self, 'merchant_id', None):
return self.merchant.name
return ''
@property
def created_by_name(self) -> str:
if getattr(self, 'created_by_id', None):
return self.created_by.name
return ''
@property
def warehouse_name(self) -> str:
if getattr(self, 'warehouse_id', None):
return self.warehouse.name
return ''
@property
def human_id(self) -> str:
"""人类可读编号(虚拟编号,不落库)。
规则YS + YYYYMMDD{6位id}
"""
if self.id is None or self.created_at is None:
return ""
return f"YS{self.created_at.year}{self.created_at.month:02d}{self.created_at.day:02d}{self.id:06d}"
class Meta:
verbose_name = '预销售单'
verbose_name_plural = '预销售单'
class PreSalesOrderItem(ModelBase):
"""预销售单明细"""
id = models.BigAutoField(primary_key=True)
pre_sales_order = models.ForeignKey(
PreSalesOrder,
on_delete=models.CASCADE,
related_name='items',
verbose_name='预销售单',
)
# 暂不与 Product 建立外键,先用弱关联字段承载。
product_id = models.BigIntegerField(null=True, blank=True, verbose_name='产品ID')
product_name = models.CharField(max_length=100, blank=True, default='', verbose_name='产品名称')
color = models.CharField(max_length=50, null=True, blank=True, verbose_name='颜色')
quantity = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name='数量')
unit = models.CharField(max_length=50, blank=True, default='', verbose_name='单位')
spec = models.CharField(max_length=100, null=True, blank=True, verbose_name='规格')
quantity_of_rolls = models.TextField(
null=True,
blank=True,
verbose_name='各条数数量',
)
num_of_rolls = models.PositiveIntegerField(default=1, verbose_name='条数')
order_quantity = models.IntegerField(null=True, blank=True, verbose_name='下单数量')
remarks = models.TextField(blank=True, null=True, verbose_name='备注')
class Meta:
verbose_name = '预销售单明细'
verbose_name_plural = '预销售单明细'
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}'