forked from erp-dev/erp
feat: shipment_delivery
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
from django.contrib import admin
|
||||
from .models import ExternalFinishedProduct, Shipment, SalesItem
|
||||
from .models import ExternalFinishedProduct, Shipment, SalesItem, ShipmentDelivery
|
||||
|
||||
|
||||
class SalesItemInline(admin.TabularInline):
|
||||
@@ -18,10 +18,29 @@ class ExternalFinishedProductInline(admin.TabularInline):
|
||||
|
||||
@admin.register(Shipment)
|
||||
class ShipmentAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'merchant', 'customer', 'shipment_date', 'status', 'external_id', 'items_count', 'created_by', 'created_at']
|
||||
list_display = [
|
||||
'id',
|
||||
'merchant',
|
||||
'customer',
|
||||
'shipment_date',
|
||||
'status',
|
||||
'delivery',
|
||||
'external_id',
|
||||
'items_count',
|
||||
'created_by',
|
||||
'created_at',
|
||||
]
|
||||
list_filter = ['merchant', 'status', 'shipment_date', 'created_at']
|
||||
search_fields = ['customer__name', 'remark', 'external_id']
|
||||
search_fields = [
|
||||
'customer__name',
|
||||
'remark',
|
||||
'external_id',
|
||||
'address',
|
||||
'contact_name',
|
||||
'contact_phone',
|
||||
]
|
||||
readonly_fields = ['created_at', 'updated_at', 'created_by']
|
||||
raw_id_fields = ['delivery']
|
||||
date_hierarchy = 'shipment_date'
|
||||
inlines = [SalesItemInline, ExternalFinishedProductInline]
|
||||
|
||||
@@ -75,3 +94,77 @@ class ExternalFinishedProductAdmin(admin.ModelAdmin):
|
||||
if not change:
|
||||
obj.created_by = request.user
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
@admin.register(ShipmentDelivery)
|
||||
class ShipmentDeliveryAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'id',
|
||||
'merchant',
|
||||
'driver_name',
|
||||
'vehicle_trip',
|
||||
'contact_phone',
|
||||
'vehicle_capacity',
|
||||
'status',
|
||||
'shipments_count',
|
||||
'operator',
|
||||
'cancelled_by',
|
||||
'created_by',
|
||||
'started_at',
|
||||
'delivered_at',
|
||||
'cancelled_at',
|
||||
'created_at',
|
||||
]
|
||||
list_filter = ['merchant', 'status', 'created_at', 'started_at', 'delivered_at', 'cancelled_at']
|
||||
search_fields = [
|
||||
'driver_name',
|
||||
'vehicle_trip',
|
||||
'contact_phone',
|
||||
'vehicle_capacity',
|
||||
'remark',
|
||||
'internal_remark',
|
||||
'operator__name',
|
||||
'cancelled_by__username',
|
||||
'created_by__username',
|
||||
]
|
||||
readonly_fields = ['created_at', 'updated_at', 'created_by', 'operator', 'cancelled_by', 'cancelled_at', 'shipments_summary']
|
||||
fields = [
|
||||
'merchant',
|
||||
'driver_name',
|
||||
'vehicle_trip',
|
||||
'contact_phone',
|
||||
'vehicle_capacity',
|
||||
'remark',
|
||||
'internal_remark',
|
||||
'status',
|
||||
'started_at',
|
||||
'delivered_at',
|
||||
'cancelled_at',
|
||||
'operator',
|
||||
'cancelled_by',
|
||||
'created_by',
|
||||
'shipments_summary',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
]
|
||||
|
||||
def shipments_count(self, obj):
|
||||
return obj.shipments.count()
|
||||
|
||||
shipments_count.short_description = '出货单数量'
|
||||
|
||||
def shipments_summary(self, obj):
|
||||
ids = list(obj.shipments.order_by('id').values_list('id', flat=True))
|
||||
if not ids:
|
||||
return '无'
|
||||
return ', '.join(str(item_id) for item_id in ids)
|
||||
|
||||
shipments_summary.short_description = '关联出货单'
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change:
|
||||
obj.created_by = request.user
|
||||
employee = getattr(request.user, 'employee', None)
|
||||
if employee:
|
||||
obj.operator = employee
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
("shipment", "0012_replace_cancelled_at_with_status_modified_at_and_add_approved_by"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ShipmentDelivery",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("created_at", models.DateTimeField(auto_now_add=True, verbose_name="创建时间")),
|
||||
("updated_at", models.DateTimeField(auto_now=True, verbose_name="更新时间")),
|
||||
("status", models.IntegerField(choices=[(1, "待送货"), (2, "送货中"), (3, "已送达")], db_index=True, default=1, verbose_name="状态")),
|
||||
("driver_name", models.CharField(db_index=True, max_length=100, verbose_name="司机名")),
|
||||
("vehicle_trip", models.CharField(db_index=True, max_length=100, verbose_name="车次")),
|
||||
("started_at", models.DateTimeField(blank=True, null=True, verbose_name="开始送货时间")),
|
||||
("delivered_at", models.DateTimeField(blank=True, null=True, verbose_name="送达时间")),
|
||||
("created_by", models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="created_shipment_deliveries", to=settings.AUTH_USER_MODEL, verbose_name="创建人")),
|
||||
("merchant", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name="shipment_deliveries", to="basic_info.merchant", verbose_name="所属商户")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "送货单",
|
||||
"verbose_name_plural": "送货单",
|
||||
"db_table": "shipment_delivery",
|
||||
"ordering": ["-created_at", "-id"],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="shipment",
|
||||
name="address",
|
||||
field=models.CharField(blank=True, default="", max_length=255, verbose_name="地址"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="shipment",
|
||||
name="contact_name",
|
||||
field=models.CharField(blank=True, default="", max_length=100, verbose_name="联系人"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="shipment",
|
||||
name="contact_phone",
|
||||
field=models.CharField(blank=True, default="", max_length=50, verbose_name="联系电话"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="shipment",
|
||||
name="delivery",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="shipments",
|
||||
to="shipment.shipmentdelivery",
|
||||
verbose_name="送货单",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="shipmentdelivery",
|
||||
name="internal_remark",
|
||||
field=models.CharField(blank=True, default="", max_length=200, verbose_name="内部备注"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="shipmentdelivery",
|
||||
name="remark",
|
||||
field=models.CharField(blank=True, default="", max_length=200, verbose_name="备注"),
|
||||
),
|
||||
]
|
||||
25
shipment/migrations/0014_shipmentdelivery_operator.py
Normal file
25
shipment/migrations/0014_shipmentdelivery_operator.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("basic_info", "0025_customer_uniq_customer_merchant_name"),
|
||||
("shipment", "0013_shipmentdelivery_shipment_delivery"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="shipmentdelivery",
|
||||
name="operator",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="operated_shipment_deliveries",
|
||||
to="basic_info.employee",
|
||||
verbose_name="操作人",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,50 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("shipment", "0014_shipmentdelivery_operator"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="shipmentdelivery",
|
||||
name="cancelled_at",
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name="取消时间"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="shipmentdelivery",
|
||||
name="cancelled_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="cancelled_shipment_deliveries",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="取消人",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="shipmentdelivery",
|
||||
name="status",
|
||||
field=models.IntegerField(
|
||||
choices=[(1, "待送货"), (2, "送货中"), (3, "已送达"), (4, "已取消")],
|
||||
db_index=True,
|
||||
default=1,
|
||||
verbose_name="状态",
|
||||
),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="shipmentdelivery",
|
||||
options={
|
||||
"ordering": ["-created_at", "-id"],
|
||||
"permissions": [("cancel_shipmentdelivery", "Can cancel shipment delivery")],
|
||||
"verbose_name": "送货单",
|
||||
"verbose_name_plural": "送货单",
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("shipment", "0015_shipmentdelivery_cancel_fields_and_permission"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="shipmentdelivery",
|
||||
name="contact_phone",
|
||||
field=models.CharField(blank=True, default="", max_length=50, verbose_name="联系电话"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="shipmentdelivery",
|
||||
name="vehicle_capacity",
|
||||
field=models.CharField(blank=True, default="", max_length=100, verbose_name="车辆容量"),
|
||||
),
|
||||
]
|
||||
@@ -29,6 +29,14 @@ class ShipmentStatus(models.IntegerChoices):
|
||||
APPROVED = 5, '已审核'
|
||||
|
||||
|
||||
class ShipmentDeliveryStatus(models.IntegerChoices):
|
||||
"""送货单状态"""
|
||||
PENDING = 1, '待送货'
|
||||
IN_TRANSIT = 2, '送货中'
|
||||
DELIVERED = 3, '已送达'
|
||||
CANCELLED = 4, '已取消'
|
||||
|
||||
|
||||
class ExternalFinishedProduct(ModelBase):
|
||||
"""
|
||||
外部成品表
|
||||
@@ -111,6 +119,15 @@ class Shipment(ModelBase):
|
||||
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,
|
||||
@@ -138,6 +155,27 @@ class Shipment(ModelBase):
|
||||
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,
|
||||
@@ -302,3 +340,121 @@ class SalesItem(ModelBase):
|
||||
return basic_models.Customer.objects.get(id=self.customer_id)
|
||||
except basic_models.Customer.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
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='内部备注',
|
||||
)
|
||||
|
||||
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}'
|
||||
|
||||
@@ -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