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

627 lines
16 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 typing import TYPE_CHECKING
from django.db import models
from django.contrib.auth import get_user_model
from flower.common import ModelBase
from basic_info import models as basic_models
if TYPE_CHECKING:
from printing.models import PrintingJob
User = get_user_model()
class UnitChoices(models.IntegerChoices):
"""销售品单位"""
METER = 1, ''
PIECE = 2, ''
YARD = 3, ''
UNIT = 4, ''
class ShipmentStatus(models.IntegerChoices):
"""出货单状态"""
DRAFT = 1, '草稿(未发布)'
PUBLISHED = 2, '已发布'
CANCELLED = 3, '已取消'
REJECTED = 4, '已驳回'
APPROVED = 5, '已审核'
class ShipmentDeliveryStatus(models.IntegerChoices):
"""送货单状态"""
PENDING = 1, '待送货'
IN_TRANSIT = 2, '送货中'
DELIVERED = 3, '已送达'
CANCELLED = 4, '已取消'
class ExternalFinishedProduct(ModelBase):
"""
外部成品表
用于兼容外部遗留系统的数据映射(可被多个出货单引用)。
"""
style_name = models.CharField(
max_length=200,
verbose_name='款式名称',
)
num_of_rolls = models.PositiveIntegerField(
verbose_name='卷数',
)
remark = models.CharField(
max_length=200,
null=True,
blank=True,
verbose_name='备注',
)
created_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
related_name='created_external_finished_products',
verbose_name='创建人',
)
shipment = models.ForeignKey(
'Shipment',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='external_finished_products',
verbose_name='出货单',
help_text='关联的出货单(可空,兼容历史数据/逐步迁移)',
)
class Meta:
db_table = 'external_finished_product'
verbose_name = '外部成品表'
verbose_name_plural = '外部成品表'
ordering = ['-created_at']
def __str__(self) -> str:
return f'{self.style_name} ({self.num_of_rolls}卷)'
class Shipment(ModelBase):
"""
出货单
记录一次出货操作,包含多个销售品。
"""
status = models.IntegerField(
choices=ShipmentStatus.choices,
default=ShipmentStatus.DRAFT,
verbose_name='状态',
)
status_modified_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='状态修改时间',
)
cancelled_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='cancelled_shipments',
verbose_name='取消人',
)
approved_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='approved_shipments',
verbose_name='审核人',
)
delivery = models.ForeignKey(
'ShipmentDelivery',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='shipments',
verbose_name='送货单',
)
external_id = models.CharField(
max_length=120,
null=True,
blank=True,
verbose_name='外部订单号',
help_text='用于兼容外部遗留系统的订单号(可空)',
)
merchant = models.ForeignKey(
basic_models.Merchant,
on_delete=models.PROTECT,
related_name='shipments',
verbose_name='所属商户',
)
customer = models.ForeignKey(
basic_models.Customer,
on_delete=models.PROTECT,
related_name='shipments',
verbose_name='客户'
)
shipment_date = models.DateField(
verbose_name='出货日期',
help_text='实际出货日期'
)
address = models.CharField(
max_length=255,
blank=True,
default='',
verbose_name='地址',
)
contact_name = models.CharField(
max_length=100,
blank=True,
default='',
verbose_name='联系人',
)
contact_phone = models.CharField(
max_length=50,
blank=True,
default='',
verbose_name='联系电话',
)
area = models.CharField(
max_length=30,
blank=True,
default='',
verbose_name='地区',
help_text='出货地区(可空字符串)',
)
remark = models.TextField(
blank=True,
default='',
verbose_name='备注'
)
created_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
related_name='created_shipments',
verbose_name='创建人'
)
geo_coordinates = models.JSONField(
null=True,
blank=True,
verbose_name='地理坐标',
help_text='由 Geo 系统返回的坐标信息,结构由调用方决定,后端不作校验',
)
rejected_sales_item_ids = models.JSONField(
default=list,
blank=True,
verbose_name='驳回前销售品ID快照',
help_text='仅用于审计记录出货单驳回前所绑定的销售品ID列表',
)
class Meta:
db_table = 'shipment'
verbose_name = '出货单'
verbose_name_plural = '出货单'
ordering = ['-created_at']
def __str__(self):
return f'出货单 #{self.id} - {self.customer.name}'
def cancel(self, operator: User | None) -> None:
"""
取消出货单(委托给 service 处理状态机与时间记录)
Args:
operator: 操作者(通常为 request.user可为空
"""
from shipment.services import modify_status
modify_status(
self,
target_status=ShipmentStatus.CANCELLED,
operator=operator,
)
class SalesItem(ModelBase):
"""
销售品
出货单中的具体商品明细。
销售品可以先创建(待分配状态),后续再关联到出货单。
"""
shipment = models.ForeignKey(
Shipment,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='items',
verbose_name='出货单',
help_text='为空表示待分配'
)
merchant = models.ForeignKey(
basic_models.Merchant,
on_delete=models.PROTECT,
related_name='sales_items',
verbose_name='所属商户',
)
name = models.CharField(
max_length=200,
verbose_name='名称'
)
quantity = models.DecimalField(
max_digits=12,
decimal_places=2,
verbose_name='数量'
)
unit = models.IntegerField(
choices=UnitChoices.choices,
default=UnitChoices.PIECE,
verbose_name='单位'
)
created_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
related_name='created_sales_items',
verbose_name='创建人'
)
# 关联的生产明细订单(可空)
# 使用 PositiveIntegerField 而非 ForeignKey避免模块间强依赖
printing_job_id = models.PositiveIntegerField(
null=True,
blank=True,
verbose_name='关联生产任务ID',
help_text='关联的 PrintingJob ID可空'
)
# 客户ID可空用于销售品级别的客户关联
customer_id = models.PositiveIntegerField(
null=True,
blank=True,
verbose_name='客户ID',
help_text='销售品级别的客户ID可空'
)
position = models.CharField(
max_length=200,
blank=True,
default='',
verbose_name='货位'
)
remark = models.CharField(
max_length=200,
blank=True,
default='',
verbose_name='备注'
)
delete_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='删除时间',
)
delete_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='deleted_sales_items',
verbose_name='删除人',
)
merge_remark = models.JSONField(
null=True,
blank=True,
verbose_name='合卷备注',
help_text='合卷信息JSON包含 merge_type, jobs, main_job, quantity, unit, job_count',
)
class Meta:
db_table = 'sales_item'
verbose_name = '销售品'
verbose_name_plural = '销售品'
ordering = ['id']
permissions = [
('soft_delete_salesitem', 'Can soft delete sales item'),
]
def __str__(self):
return f'{self.name} x {self.quantity} {self.get_unit_display()}'
def get_printing_job(self) -> PrintingJob | None:
"""
获取关联的 PrintingJob 实例
Returns:
PrintingJob 实例,如果不存在则返回 None
"""
if not self.printing_job_id:
return None
try:
from printing.models import PrintingJob
return PrintingJob.objects.get(id=self.printing_job_id)
except PrintingJob.DoesNotExist:
return None
def get_customer(self) -> basic_models.Customer | None:
"""
获取关联的 Customer 实例
Returns:
Customer 实例,如果不存在则返回 None
"""
if not self.customer_id:
return None
try:
return basic_models.Customer.objects.get(id=self.customer_id)
except basic_models.Customer.DoesNotExist:
return None
class SalesItemChangeRecord(ModelBase):
"""
销售品修改记录。
记录允许修改字段的变更前后值,以及操作者与操作时间。
"""
sales_item = models.ForeignKey(
SalesItem,
on_delete=models.CASCADE,
related_name="change_records",
verbose_name="销售品",
)
operator = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="sales_item_change_records",
verbose_name="操作者",
)
operated_at = models.DateTimeField(
verbose_name="操作时间",
)
before_values = models.JSONField(
default=dict,
blank=True,
verbose_name="修改前的值",
)
after_values = models.JSONField(
default=dict,
blank=True,
verbose_name="修改目标值",
)
class Meta:
db_table = "sales_item_change_record"
verbose_name = "销售品修改记录"
verbose_name_plural = "销售品修改记录"
ordering = ["-operated_at", "-id"]
def __str__(self) -> str:
return f"SalesItemChangeRecord #{self.id} - sales_item={self.sales_item_id}"
class SalesItemRebuildRecord(ModelBase):
"""
销售品重建记录。
用于审计“软删除旧销售品并基于新 printing_job 重建销售品”的动作。
"""
old_sales_item = models.ForeignKey(
SalesItem,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="rebuild_source_records",
verbose_name="旧销售品",
)
new_sales_item = models.ForeignKey(
SalesItem,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="rebuild_target_records",
verbose_name="新销售品",
)
operator = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="sales_item_rebuild_records",
verbose_name="操作者",
)
rebuilt_at = models.DateTimeField(
verbose_name="重建时间",
)
old_printing_job_id = models.PositiveIntegerField(
null=True,
blank=True,
verbose_name="旧生产任务ID",
)
new_printing_job_id = models.PositiveIntegerField(
verbose_name="新生产任务ID",
)
old_quantity = models.DecimalField(
max_digits=12,
decimal_places=2,
verbose_name="旧数量",
)
new_quantity = models.DecimalField(
max_digits=12,
decimal_places=2,
verbose_name="新数量",
)
class Meta:
db_table = "sales_item_rebuild_record"
verbose_name = "销售品重建记录"
verbose_name_plural = "销售品重建记录"
ordering = ["-rebuilt_at", "-id"]
def __str__(self) -> str:
return f"SalesItemRebuildRecord #{self.id} - old={self.old_sales_item_id}, new={self.new_sales_item_id}"
class ShipmentDelivery(ModelBase):
"""
送货单
记录一次具体送货行为,可关联多个出货单。
"""
status = models.IntegerField(
choices=ShipmentDeliveryStatus.choices,
default=ShipmentDeliveryStatus.PENDING,
db_index=True,
verbose_name='状态',
)
merchant = models.ForeignKey(
basic_models.Merchant,
on_delete=models.PROTECT,
related_name='shipment_deliveries',
verbose_name='所属商户',
)
driver_name = models.CharField(
max_length=100,
db_index=True,
verbose_name='司机名',
)
vehicle_trip = models.CharField(
max_length=100,
db_index=True,
verbose_name='车次',
)
contact_phone = models.CharField(
max_length=50,
blank=True,
default='',
verbose_name='联系电话',
)
vehicle_capacity = models.CharField(
max_length=100,
blank=True,
default='',
verbose_name='车辆容量',
)
remark = models.CharField(
max_length=200,
blank=True,
default='',
verbose_name='备注',
)
internal_remark = models.CharField(
max_length=200,
blank=True,
default='',
verbose_name='内部备注',
)
shipment_order_ids = models.JSONField(
null=True,
blank=True,
default=list,
verbose_name='出货单顺序ID列表',
help_text='前端自管的出货单展示顺序,后端不据此重排 shipments 关系',
)
started_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='开始送货时间',
)
delivered_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='送达时间',
)
cancelled_at = models.DateTimeField(
null=True,
blank=True,
verbose_name='取消时间',
)
created_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
related_name='created_shipment_deliveries',
verbose_name='创建人',
)
operator = models.ForeignKey(
basic_models.Employee,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='operated_shipment_deliveries',
verbose_name='操作人',
)
cancelled_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='cancelled_shipment_deliveries',
verbose_name='取消人',
)
class Meta:
db_table = 'shipment_delivery'
verbose_name = '送货单'
verbose_name_plural = '送货单'
ordering = ['-created_at', '-id']
permissions = [
('cancel_shipmentdelivery', 'Can cancel shipment delivery'),
]
def __str__(self) -> str:
return f'送货单 #{self.id} - {self.driver_name} / {self.vehicle_trip}'