forked from erp-dev/erp
feat: shipment_delivery
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user