1
0
forked from erp-dev/erp

feat: shipment_delivery

This commit is contained in:
2026-04-03 16:55:22 +08:00
parent 371cd796e7
commit 76b30cf4f4
16 changed files with 2513 additions and 9 deletions

View File

@@ -11,7 +11,63 @@ from django.db.models import Count, Exists, IntegerField, OuterRef, QuerySet, Su
from django.db.models.functions import Coalesce
from django.utils import timezone
from shipment.models import ExternalFinishedProduct, SalesItem, Shipment, ShipmentStatus
from shipment.models import (
ExternalFinishedProduct,
SalesItem,
Shipment,
ShipmentDelivery,
ShipmentDeliveryStatus,
ShipmentStatus,
)
def _resolve_user_merchant(user):
emp = getattr(user, "employee", None)
return getattr(emp, "merchant", None) if emp else None
def _resolve_user_employee(user):
return getattr(user, "employee", None)
def _resolve_delivery_shipments(
*,
shipment_ids: list[int],
merchant,
current_delivery: ShipmentDelivery | None = None,
) -> list[Shipment]:
normalized_ids = list(dict.fromkeys(shipment_ids or []))
if not normalized_ids:
return []
shipments = list(
Shipment.objects.filter(id__in=normalized_ids, merchant=merchant)
.select_related("customer", "delivery")
.order_by("id")
)
found_ids = {shipment.id for shipment in shipments}
missing_ids = sorted(set(normalized_ids) - found_ids)
if missing_ids:
raise ValueError(f"以下出货单不存在或不属于当前商户: {missing_ids}")
occupied_ids = sorted(
shipment.id
for shipment in shipments
if shipment.delivery_id is not None
and (current_delivery is None or shipment.delivery_id != current_delivery.id)
)
if occupied_ids:
raise ValueError(f"以下出货单已关联到其他送货单: {occupied_ids}")
not_approved_ids = sorted(
shipment.id
for shipment in shipments
if shipment.status != ShipmentStatus.APPROVED
)
if not_approved_ids:
raise ValueError(f"以下出货单未处于已审核状态,不能绑定到送货单: {not_approved_ids}")
return shipments
def get_customers_with_unshipped_sales_items(*, merchant) -> QuerySet:
@@ -137,6 +193,220 @@ def get_sales_items_by_customer(
return queryset.select_related("shipment").order_by("id")
@transaction.atomic
def create_shipment_delivery(
*,
driver_name: str,
vehicle_trip: str,
contact_phone: str = "",
vehicle_capacity: str = "",
shipment_ids: list[int],
created_by,
remark: str = "",
internal_remark: str = "",
) -> ShipmentDelivery:
merchant = _resolve_user_merchant(created_by)
operator = _resolve_user_employee(created_by)
if not merchant:
raise ValueError("用户未关联商户,无法创建送货单")
driver_name = (driver_name or "").strip()
vehicle_trip = (vehicle_trip or "").strip()
if not driver_name:
raise ValueError("driver_name 不能为空")
if not vehicle_trip:
raise ValueError("vehicle_trip 不能为空")
shipments = _resolve_delivery_shipments(
shipment_ids=shipment_ids,
merchant=merchant,
)
delivery = ShipmentDelivery.objects.create(
merchant=merchant,
driver_name=driver_name,
vehicle_trip=vehicle_trip,
contact_phone=(contact_phone or "").strip(),
vehicle_capacity=(vehicle_capacity or "").strip(),
remark=(remark or "").strip(),
internal_remark=(internal_remark or "").strip(),
created_by=created_by,
operator=operator,
)
if shipments:
Shipment.objects.filter(id__in=[shipment.id for shipment in shipments]).update(
delivery=delivery
)
return delivery
@transaction.atomic
def update_shipment_delivery(
delivery: ShipmentDelivery,
*,
driver_name: str | None = None,
vehicle_trip: str | None = None,
contact_phone: str | None = None,
vehicle_capacity: str | None = None,
shipment_ids: list[int] | None = None,
remark: str | None = None,
internal_remark: str | None = None,
operator=None,
) -> ShipmentDelivery:
if driver_name is not None:
normalized_driver_name = driver_name.strip()
if not normalized_driver_name:
raise ValueError("driver_name 不能为空")
delivery.driver_name = normalized_driver_name
if vehicle_trip is not None:
normalized_vehicle_trip = vehicle_trip.strip()
if not normalized_vehicle_trip:
raise ValueError("vehicle_trip 不能为空")
delivery.vehicle_trip = normalized_vehicle_trip
if contact_phone is not None:
delivery.contact_phone = (contact_phone or "").strip()
if vehicle_capacity is not None:
delivery.vehicle_capacity = (vehicle_capacity or "").strip()
if remark is not None:
delivery.remark = (remark or "").strip()
if internal_remark is not None:
delivery.internal_remark = (internal_remark or "").strip()
if shipment_ids is not None:
shipments = _resolve_delivery_shipments(
shipment_ids=shipment_ids,
merchant=delivery.merchant,
current_delivery=delivery,
)
Shipment.objects.filter(delivery=delivery).exclude(
id__in=[shipment.id for shipment in shipments]
).update(delivery=None)
if shipments:
Shipment.objects.filter(id__in=[shipment.id for shipment in shipments]).update(
delivery=delivery
)
else:
Shipment.objects.filter(delivery=delivery).update(delivery=None)
if operator is not None:
delivery.operator = _resolve_user_employee(operator)
delivery.save()
return delivery
@transaction.atomic
def bind_shipments_to_delivery(
delivery: ShipmentDelivery,
*,
shipment_ids: list[int],
operator=None,
) -> ShipmentDelivery:
shipments = _resolve_delivery_shipments(
shipment_ids=shipment_ids,
merchant=delivery.merchant,
current_delivery=delivery,
)
if not shipments:
return delivery
Shipment.objects.filter(id__in=[shipment.id for shipment in shipments]).update(
delivery=delivery
)
if operator is not None:
delivery.operator = _resolve_user_employee(operator)
delivery.save(update_fields=["operator", "updated_at"])
return delivery
@transaction.atomic
def modify_shipment_delivery_status(
delivery: ShipmentDelivery,
*,
target_status: int,
operator=None,
) -> ShipmentDelivery:
try:
target_status = int(target_status)
except (TypeError, ValueError):
raise ValueError("无效的送货单状态")
current_status = delivery.status
if current_status == target_status:
return delivery
allowed_transitions = {
ShipmentDeliveryStatus.PENDING: {ShipmentDeliveryStatus.IN_TRANSIT},
ShipmentDeliveryStatus.IN_TRANSIT: {ShipmentDeliveryStatus.DELIVERED},
ShipmentDeliveryStatus.DELIVERED: set(),
ShipmentDeliveryStatus.CANCELLED: set(),
}
if target_status not in ShipmentDeliveryStatus.values:
raise ValueError("无效的送货单状态")
if target_status == ShipmentDeliveryStatus.CANCELLED:
raise ValueError("取消送货单请使用独立的取消接口")
if target_status not in allowed_transitions.get(current_status, set()):
raise ValueError(
f"不允许将送货单状态从 {delivery.get_status_display()} 修改为 "
f"{ShipmentDeliveryStatus(target_status).label}"
)
now = timezone.now()
delivery.status = target_status
update_fields = ["status", "updated_at"]
if operator is not None:
delivery.operator = _resolve_user_employee(operator)
update_fields.append("operator")
if target_status == ShipmentDeliveryStatus.IN_TRANSIT:
delivery.started_at = now
update_fields.append("started_at")
elif target_status == ShipmentDeliveryStatus.DELIVERED:
delivery.delivered_at = now
update_fields.append("delivered_at")
delivery.save(update_fields=update_fields)
return delivery
@transaction.atomic
def cancel_shipment_delivery(
delivery: ShipmentDelivery,
*,
cancelled_by=None,
operator=None,
) -> ShipmentDelivery:
if delivery.status == ShipmentDeliveryStatus.CANCELLED:
return delivery
delivery.status = ShipmentDeliveryStatus.CANCELLED
delivery.cancelled_at = timezone.now()
delivery.cancelled_by = cancelled_by
if operator is not None:
delivery.operator = _resolve_user_employee(operator)
delivery.save(
update_fields=[
"status",
"cancelled_at",
"cancelled_by",
"operator",
"updated_at",
]
)
return delivery
@transaction.atomic
def delete_shipment_delivery(delivery: ShipmentDelivery) -> None:
Shipment.objects.filter(delivery=delivery).update(delivery=None)
delivery.delete()
@transaction.atomic
def modify_status(
shipment: Shipment,
@@ -213,6 +483,9 @@ def update_shipment(
*,
customer_id: int | None = None,
shipment_date=None,
address: str | None = None,
contact_name: str | None = None,
contact_phone: str | None = None,
area: str | None = None,
remark: str | None = None,
external_id: str | None = None,
@@ -239,6 +512,12 @@ def update_shipment(
if shipment_date is not None:
shipment.shipment_date = shipment_date
if address is not None:
shipment.address = address or ""
if contact_name is not None:
shipment.contact_name = contact_name or ""
if contact_phone is not None:
shipment.contact_phone = contact_phone or ""
if area is not None:
shipment.area = (area or "").strip()
if remark is not None:
@@ -256,6 +535,9 @@ def create_shipment(
shipment_date,
sales_item_ids: List[int],
created_by,
address: str = "",
contact_name: str = "",
contact_phone: str = "",
remark: str = "",
area: str = "",
) -> Shipment:
@@ -349,6 +631,9 @@ def create_shipment(
merchant=merchant,
customer=customer,
shipment_date=shipment_date,
address=address or "",
contact_name=contact_name or "",
contact_phone=contact_phone or "",
area=(area or "").strip(),
remark=remark,
created_by=created_by,
@@ -372,6 +657,9 @@ def create_external_shipment(
external_id: str,
external_finished_products: List[dict],
created_by,
address: str = "",
contact_name: str = "",
contact_phone: str = "",
remark: str = "",
area: str = "",
) -> Shipment:
@@ -409,6 +697,9 @@ def create_external_shipment(
merchant=merchant,
customer=customer,
shipment_date=shipment_date,
address=address or "",
contact_name=contact_name or "",
contact_phone=contact_phone or "",
area=(area or "").strip(),
remark=remark,
created_by=created_by,