forked from erp-dev/erp
feat: shipment patch
This commit is contained in:
26
shipment/migrations/0011_alter_shipment_status_choices.py
Normal file
26
shipment/migrations/0011_alter_shipment_status_choices.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("shipment", "0010_salesitem_merchant_shipment_area_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="shipment",
|
||||
name="status",
|
||||
field=models.IntegerField(
|
||||
choices=[
|
||||
(1, "草稿(未发布)"),
|
||||
(2, "已发布"),
|
||||
(3, "已取消"),
|
||||
(4, "已驳回"),
|
||||
(5, "已审核"),
|
||||
],
|
||||
default=1,
|
||||
verbose_name="状态",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("shipment", "0011_alter_shipment_status_choices"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="shipment",
|
||||
name="cancelled_at",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="shipment",
|
||||
name="approved_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="approved_shipments",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="审核人",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="shipment",
|
||||
name="status_modified_at",
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name="状态修改时间"),
|
||||
),
|
||||
]
|
||||
@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
from flower.common import ModelBase
|
||||
from basic_info import models as basic_models
|
||||
|
||||
@@ -23,9 +22,11 @@ class UnitChoices(models.IntegerChoices):
|
||||
|
||||
class ShipmentStatus(models.IntegerChoices):
|
||||
"""出货单状态"""
|
||||
PENDING_DELIVERY = 1, '待送货'
|
||||
DELIVERED = 2, '已交付'
|
||||
DRAFT = 1, '草稿(未发布)'
|
||||
PUBLISHED = 2, '已发布'
|
||||
CANCELLED = 3, '已取消'
|
||||
REJECTED = 4, '已驳回'
|
||||
APPROVED = 5, '已审核'
|
||||
|
||||
|
||||
class ExternalFinishedProduct(ModelBase):
|
||||
@@ -82,14 +83,14 @@ class Shipment(ModelBase):
|
||||
"""
|
||||
status = models.IntegerField(
|
||||
choices=ShipmentStatus.choices,
|
||||
default=ShipmentStatus.PENDING_DELIVERY,
|
||||
default=ShipmentStatus.DRAFT,
|
||||
verbose_name='状态',
|
||||
)
|
||||
|
||||
cancelled_at = models.DateTimeField(
|
||||
status_modified_at = models.DateTimeField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='取消时间',
|
||||
verbose_name='状态修改时间',
|
||||
)
|
||||
|
||||
cancelled_by = models.ForeignKey(
|
||||
@@ -101,6 +102,15 @@ class Shipment(ModelBase):
|
||||
verbose_name='取消人',
|
||||
)
|
||||
|
||||
approved_by = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='approved_shipments',
|
||||
verbose_name='审核人',
|
||||
)
|
||||
|
||||
external_id = models.CharField(
|
||||
max_length=120,
|
||||
null=True,
|
||||
@@ -161,15 +171,18 @@ class Shipment(ModelBase):
|
||||
|
||||
def cancel(self, operator: User | None) -> None:
|
||||
"""
|
||||
取消出货单(记录取消时间与操作者)
|
||||
取消出货单(委托给 service 处理状态机与时间记录)
|
||||
|
||||
Args:
|
||||
operator: 操作者(通常为 request.user,可为空)
|
||||
"""
|
||||
self.status = ShipmentStatus.CANCELLED
|
||||
self.cancelled_at = timezone.now()
|
||||
self.cancelled_by = operator
|
||||
self.save(update_fields=['status', 'cancelled_at', 'cancelled_by', 'updated_at'])
|
||||
from shipment.services import modify_status
|
||||
|
||||
modify_status(
|
||||
self,
|
||||
target_status=ShipmentStatus.CANCELLED,
|
||||
operator=operator,
|
||||
)
|
||||
|
||||
|
||||
class SalesItem(ModelBase):
|
||||
|
||||
@@ -9,8 +9,9 @@ from typing import List
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, Exists, IntegerField, OuterRef, QuerySet, Subquery
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
|
||||
from shipment.models import ExternalFinishedProduct, SalesItem, Shipment
|
||||
from shipment.models import ExternalFinishedProduct, SalesItem, Shipment, ShipmentStatus
|
||||
|
||||
|
||||
def get_customers_with_unshipped_sales_items(*, merchant) -> QuerySet:
|
||||
@@ -48,24 +49,50 @@ def get_customers_with_unshipped_sales_items(*, merchant) -> QuerySet:
|
||||
|
||||
|
||||
def get_sales_items_by_printing_order(
|
||||
printing_order_id: int,
|
||||
printing_order_id: int | str,
|
||||
include_already_has_shipment: bool = False,
|
||||
merchant=None,
|
||||
) -> QuerySet[SalesItem]:
|
||||
"""
|
||||
通过生产订单ID查询对应的销售品
|
||||
通过生产订单 ID 或 external_order_id 查询对应的销售品。
|
||||
|
||||
Args:
|
||||
printing_order_id: 生产订单ID
|
||||
printing_order_id: 生产订单内部 ID,或 external_order_id
|
||||
include_already_has_shipment: 是否包含已关联出货单的销售品,默认为 False
|
||||
merchant: 可选的商户约束;传入后会限定只在该商户下解析生产订单
|
||||
|
||||
Returns:
|
||||
SalesItem 查询集
|
||||
"""
|
||||
from printing.models import PrintingJob
|
||||
from printing.models import PrintingJob, PrintingOrder
|
||||
|
||||
# 1. 获取该生产订单下所有 PrintingJob 的 ID
|
||||
raw_value = str(printing_order_id).strip()
|
||||
order_queryset = PrintingOrder.objects.all()
|
||||
if merchant is not None:
|
||||
order_queryset = order_queryset.filter(merchant=merchant)
|
||||
|
||||
resolved_order_ids: list[int] = []
|
||||
|
||||
# 优先按内部 ID 解析,保持和当前 API 路径语义一致。
|
||||
if raw_value.isdigit():
|
||||
resolved_order_ids = list(
|
||||
order_queryset.filter(id=int(raw_value)).values_list("id", flat=True)[:1]
|
||||
)
|
||||
|
||||
# 内部 ID 未命中时,再按 external_order_id 查询。
|
||||
if not resolved_order_ids and raw_value:
|
||||
resolved_order_ids = list(
|
||||
order_queryset.filter(external_order_id=raw_value).values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
)
|
||||
|
||||
if not resolved_order_ids:
|
||||
return SalesItem.objects.none().select_related("shipment").order_by("id")
|
||||
|
||||
# 1. 获取目标生产订单下所有 PrintingJob 的 ID
|
||||
job_ids = PrintingJob.objects.filter(
|
||||
printing_order_id=printing_order_id
|
||||
printing_order_id__in=resolved_order_ids
|
||||
).values_list("id", flat=True)
|
||||
|
||||
# 2. 查询 SalesItem,过滤 printing_job_id 在这些 job_ids 中
|
||||
@@ -110,6 +137,76 @@ def get_sales_items_by_customer(
|
||||
return queryset.select_related("shipment").order_by("id")
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def modify_status(
|
||||
shipment: Shipment,
|
||||
*,
|
||||
target_status: int,
|
||||
operator=None,
|
||||
approved_by=None,
|
||||
) -> Shipment:
|
||||
"""
|
||||
修改出货单状态,并记录状态修改时间。
|
||||
|
||||
规则:
|
||||
- 草稿 -> 只能已发布
|
||||
- 已发布 -> 已审核 / 已驳回 / 已取消
|
||||
- 已驳回 -> 已审核 / 已取消
|
||||
- 已审核 -> 已取消
|
||||
- 已取消 -> 不可再变
|
||||
- 重复设置同一状态保持幂等,直接返回
|
||||
"""
|
||||
current_status = shipment.status
|
||||
if current_status == target_status:
|
||||
return shipment
|
||||
|
||||
allowed_transitions = {
|
||||
ShipmentStatus.DRAFT: {ShipmentStatus.PUBLISHED},
|
||||
ShipmentStatus.PUBLISHED: {
|
||||
ShipmentStatus.APPROVED,
|
||||
ShipmentStatus.REJECTED,
|
||||
ShipmentStatus.CANCELLED,
|
||||
},
|
||||
ShipmentStatus.REJECTED: {
|
||||
ShipmentStatus.APPROVED,
|
||||
ShipmentStatus.CANCELLED,
|
||||
},
|
||||
ShipmentStatus.APPROVED: {
|
||||
ShipmentStatus.CANCELLED,
|
||||
},
|
||||
ShipmentStatus.CANCELLED: set(),
|
||||
}
|
||||
|
||||
if target_status not in allowed_transitions.get(current_status, set()):
|
||||
raise ValueError(
|
||||
f"不允许将出货单状态从 {shipment.get_status_display()} 修改为 "
|
||||
f"{ShipmentStatus(target_status).label}"
|
||||
)
|
||||
|
||||
if target_status == ShipmentStatus.APPROVED and approved_by is None:
|
||||
raise ValueError("目标状态为已审核时,approved_by 不能为空")
|
||||
|
||||
shipment.status = target_status
|
||||
shipment.status_modified_at = timezone.now()
|
||||
|
||||
if target_status == ShipmentStatus.CANCELLED:
|
||||
shipment.cancelled_by = operator
|
||||
|
||||
if target_status == ShipmentStatus.APPROVED:
|
||||
shipment.approved_by = approved_by
|
||||
|
||||
shipment.save(
|
||||
update_fields=[
|
||||
"status",
|
||||
"status_modified_at",
|
||||
"cancelled_by",
|
||||
"approved_by",
|
||||
"updated_at",
|
||||
]
|
||||
)
|
||||
return shipment
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def create_shipment(
|
||||
customer_id: int,
|
||||
@@ -154,6 +251,8 @@ def create_shipment(
|
||||
|
||||
# 验证销售品
|
||||
if sales_item_ids:
|
||||
from printing.models import PrintingJob
|
||||
|
||||
# 查询销售品
|
||||
sales_items = SalesItem.objects.filter(id__in=sales_item_ids)
|
||||
found_ids = set(sales_items.values_list("id", flat=True))
|
||||
@@ -168,6 +267,40 @@ def create_shipment(
|
||||
shipped_ids = list(already_shipped.values_list("id", flat=True))
|
||||
raise ValueError(f"以下销售品已关联到其他出货单: {shipped_ids}")
|
||||
|
||||
sales_items_list = list(sales_items)
|
||||
missing_printing_job_ids = [
|
||||
item.id for item in sales_items_list if not item.printing_job_id
|
||||
]
|
||||
if missing_printing_job_ids:
|
||||
raise ValueError(
|
||||
f"以下销售品缺少关联生产任务,无法创建出货单: {missing_printing_job_ids}"
|
||||
)
|
||||
|
||||
printing_job_ids = {item.printing_job_id for item in sales_items_list}
|
||||
printing_job_map = {
|
||||
job.id: job.printing_order_id
|
||||
for job in PrintingJob.objects.filter(id__in=printing_job_ids).only(
|
||||
"id", "printing_order_id"
|
||||
)
|
||||
}
|
||||
|
||||
invalid_printing_job_ids = sorted(printing_job_ids - set(printing_job_map.keys()))
|
||||
if invalid_printing_job_ids:
|
||||
affected_item_ids = sorted(
|
||||
item.id
|
||||
for item in sales_items_list
|
||||
if item.printing_job_id in invalid_printing_job_ids
|
||||
)
|
||||
raise ValueError(
|
||||
f"以下销售品关联的生产任务不存在,无法创建出货单: {affected_item_ids}"
|
||||
)
|
||||
|
||||
printing_order_ids = {
|
||||
printing_job_map[item.printing_job_id] for item in sales_items_list
|
||||
}
|
||||
if len(printing_order_ids) > 1:
|
||||
raise ValueError("出货单中的销售品必须来自同一个生产订单")
|
||||
|
||||
# 创建出货单
|
||||
shipment = Shipment.objects.create(
|
||||
merchant=merchant,
|
||||
|
||||
Reference in New Issue
Block a user