forked from erp-dev/erp
feat: salesitem can change/soft delete
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
from django.contrib import admin
|
||||
from .models import ExternalFinishedProduct, Shipment, SalesItem, ShipmentDelivery
|
||||
from .models import (
|
||||
ExternalFinishedProduct,
|
||||
SalesItem,
|
||||
SalesItemChangeRecord,
|
||||
Shipment,
|
||||
ShipmentDelivery,
|
||||
)
|
||||
|
||||
|
||||
class SalesItemInline(admin.TabularInline):
|
||||
@@ -64,10 +70,10 @@ class ShipmentAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(SalesItem)
|
||||
class SalesItemAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'name', 'quantity', 'unit', 'position', 'shipment_status', 'printing_job_id', 'customer_id', 'created_by', 'created_at']
|
||||
list_filter = ['unit', 'created_at', ('shipment', admin.EmptyFieldListFilter)]
|
||||
list_display = ['id', 'name', 'quantity', 'unit', 'position', 'shipment_status', 'printing_job_id', 'customer_id', 'created_by', 'delete_at', 'delete_by', 'created_at']
|
||||
list_filter = ['unit', 'created_at', 'delete_at', ('shipment', admin.EmptyFieldListFilter)]
|
||||
search_fields = ['name', 'shipment__customer__name', 'position', 'remark']
|
||||
readonly_fields = ['created_at', 'updated_at', 'created_by']
|
||||
readonly_fields = ['created_at', 'updated_at', 'created_by', 'delete_at', 'delete_by']
|
||||
raw_id_fields = ['shipment']
|
||||
|
||||
def shipment_status(self, obj):
|
||||
@@ -96,6 +102,32 @@ class ExternalFinishedProductAdmin(admin.ModelAdmin):
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
@admin.register(SalesItemChangeRecord)
|
||||
class SalesItemChangeRecordAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"id",
|
||||
"sales_item",
|
||||
"operator",
|
||||
"operated_at",
|
||||
"created_at",
|
||||
]
|
||||
list_filter = ["operated_at", "created_at"]
|
||||
search_fields = [
|
||||
"sales_item__name",
|
||||
"operator__username",
|
||||
"operator__employee__name",
|
||||
]
|
||||
readonly_fields = [
|
||||
"sales_item",
|
||||
"operator",
|
||||
"operated_at",
|
||||
"before_values",
|
||||
"after_values",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
@admin.register(ShipmentDelivery)
|
||||
class ShipmentDeliveryAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
|
||||
59
shipment/migrations/0017_salesitemchangerecord.py
Normal file
59
shipment/migrations/0017_salesitemchangerecord.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("shipment", "0016_shipmentdelivery_contact_phone_and_vehicle_capacity"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SalesItemChangeRecord",
|
||||
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="更新时间")),
|
||||
("operated_at", models.DateTimeField(verbose_name="操作时间")),
|
||||
("before_values", models.JSONField(blank=True, default=dict, verbose_name="修改前的值")),
|
||||
("after_values", models.JSONField(blank=True, default=dict, verbose_name="修改目标值")),
|
||||
(
|
||||
"operator",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="sales_item_change_records",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="操作者",
|
||||
),
|
||||
),
|
||||
(
|
||||
"sales_item",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="change_records",
|
||||
to="shipment.salesitem",
|
||||
verbose_name="销售品",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "销售品修改记录",
|
||||
"verbose_name_plural": "销售品修改记录",
|
||||
"db_table": "sales_item_change_record",
|
||||
"ordering": ["-operated_at", "-id"],
|
||||
},
|
||||
),
|
||||
]
|
||||
40
shipment/migrations/0018_salesitem_soft_delete_fields.py
Normal file
40
shipment/migrations/0018_salesitem_soft_delete_fields.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("shipment", "0017_salesitemchangerecord"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="salesitem",
|
||||
name="delete_at",
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name="删除时间"),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="salesitem",
|
||||
name="delete_by",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="deleted_sales_items",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="删除人",
|
||||
),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="salesitem",
|
||||
options={
|
||||
"ordering": ["id"],
|
||||
"permissions": [("soft_delete_salesitem", "Can soft delete sales item")],
|
||||
"verbose_name": "销售品",
|
||||
"verbose_name_plural": "销售品",
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -302,12 +302,30 @@ class SalesItem(ModelBase):
|
||||
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='删除人',
|
||||
)
|
||||
|
||||
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()}'
|
||||
@@ -342,6 +360,55 @@ class SalesItem(ModelBase):
|
||||
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 ShipmentDelivery(ModelBase):
|
||||
"""
|
||||
送货单
|
||||
|
||||
@@ -4,6 +4,7 @@ Shipment 模块业务逻辑层
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import List
|
||||
|
||||
from django.db import transaction
|
||||
@@ -14,6 +15,7 @@ from django.utils import timezone
|
||||
from shipment.models import (
|
||||
ExternalFinishedProduct,
|
||||
SalesItem,
|
||||
SalesItemChangeRecord,
|
||||
Shipment,
|
||||
ShipmentDelivery,
|
||||
ShipmentDeliveryStatus,
|
||||
@@ -82,6 +84,7 @@ def get_customers_with_unshipped_sales_items(*, merchant) -> QuerySet:
|
||||
base_sales_items = SalesItem.objects.filter(
|
||||
merchant=merchant,
|
||||
shipment__isnull=True,
|
||||
delete_at__isnull=True,
|
||||
customer_id=OuterRef("pk"),
|
||||
)
|
||||
count_subquery = (
|
||||
@@ -153,6 +156,7 @@ def get_sales_items_by_printing_order(
|
||||
|
||||
# 2. 查询 SalesItem,过滤 printing_job_id 在这些 job_ids 中
|
||||
queryset = SalesItem.objects.filter(printing_job_id__in=list(job_ids))
|
||||
queryset = queryset.filter(delete_at__isnull=True)
|
||||
|
||||
# 3. 根据参数决定是否过滤已出货的销售品
|
||||
if not include_already_has_shipment:
|
||||
@@ -176,6 +180,7 @@ def get_sales_items_by_customer(
|
||||
queryset = SalesItem.objects.filter(
|
||||
merchant=merchant,
|
||||
customer_id=customer_id,
|
||||
delete_at__isnull=True,
|
||||
)
|
||||
|
||||
if external_order_id:
|
||||
@@ -193,6 +198,91 @@ def get_sales_items_by_customer(
|
||||
return queryset.select_related("shipment").order_by("id")
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def update_sales_item(
|
||||
sales_item: SalesItem,
|
||||
*,
|
||||
quantity: str | Decimal | None = None,
|
||||
remark: str | None = None,
|
||||
position: str | None = None,
|
||||
operator=None,
|
||||
) -> SalesItem:
|
||||
"""
|
||||
更新销售品的非关系字段。
|
||||
|
||||
当前仅允许修改:
|
||||
- quantity
|
||||
- remark
|
||||
- position
|
||||
"""
|
||||
changes: dict[str, tuple[object, object]] = {}
|
||||
|
||||
if quantity is not None:
|
||||
try:
|
||||
normalized_quantity = Decimal(str(quantity))
|
||||
except (InvalidOperation, ValueError, TypeError) as exc:
|
||||
raise ValueError(f"数量 {quantity} 格式无效: {exc}")
|
||||
if normalized_quantity != sales_item.quantity:
|
||||
changes["quantity"] = (sales_item.quantity, normalized_quantity)
|
||||
sales_item.quantity = normalized_quantity
|
||||
|
||||
if remark is not None:
|
||||
normalized_remark = remark or ""
|
||||
if normalized_remark != sales_item.remark:
|
||||
changes["remark"] = (sales_item.remark, normalized_remark)
|
||||
sales_item.remark = normalized_remark
|
||||
|
||||
if position is not None:
|
||||
normalized_position = position or ""
|
||||
if normalized_position != sales_item.position:
|
||||
changes["position"] = (sales_item.position, normalized_position)
|
||||
sales_item.position = normalized_position
|
||||
|
||||
if not changes:
|
||||
return sales_item
|
||||
|
||||
sales_item.save(update_fields=[*changes.keys(), "updated_at"])
|
||||
|
||||
def _serialize_value(value):
|
||||
if isinstance(value, Decimal):
|
||||
return str(value)
|
||||
return value
|
||||
|
||||
SalesItemChangeRecord.objects.create(
|
||||
sales_item=sales_item,
|
||||
operator=operator,
|
||||
operated_at=timezone.now(),
|
||||
before_values={
|
||||
field: _serialize_value(old_value)
|
||||
for field, (old_value, _) in changes.items()
|
||||
},
|
||||
after_values={
|
||||
field: _serialize_value(new_value)
|
||||
for field, (_, new_value) in changes.items()
|
||||
},
|
||||
)
|
||||
|
||||
return sales_item
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def delete_sales_item(
|
||||
sales_item: SalesItem,
|
||||
*,
|
||||
deleted_by=None,
|
||||
) -> SalesItem:
|
||||
"""
|
||||
软删除销售品。
|
||||
"""
|
||||
if sales_item.delete_at is not None:
|
||||
return sales_item
|
||||
|
||||
sales_item.delete_at = timezone.now()
|
||||
sales_item.delete_by = deleted_by
|
||||
sales_item.save(update_fields=["delete_at", "delete_by", "updated_at"])
|
||||
return sales_item
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def create_shipment_delivery(
|
||||
*,
|
||||
@@ -579,7 +669,7 @@ def create_shipment(
|
||||
from printing.models import PrintingJob
|
||||
|
||||
# 查询销售品
|
||||
sales_items = SalesItem.objects.filter(id__in=sales_item_ids)
|
||||
sales_items = SalesItem.objects.filter(id__in=sales_item_ids, delete_at__isnull=True)
|
||||
found_ids = set(sales_items.values_list("id", flat=True))
|
||||
missing_ids = set(sales_item_ids) - found_ids
|
||||
|
||||
@@ -642,7 +732,7 @@ def create_shipment(
|
||||
# 关联销售品
|
||||
if sales_item_ids:
|
||||
updated = SalesItem.objects.filter(
|
||||
id__in=sales_item_ids, merchant=merchant
|
||||
id__in=sales_item_ids, merchant=merchant, delete_at__isnull=True
|
||||
).update(shipment=shipment)
|
||||
if updated != len(sales_item_ids):
|
||||
raise ValueError("存在不属于当前商户的销售品,无法关联到出货单")
|
||||
@@ -752,7 +842,6 @@ def create_sales_item(
|
||||
Raises:
|
||||
ValueError: 如果生产任务不存在或不属于当前商户
|
||||
"""
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from printing.models import PrintingJob
|
||||
|
||||
# 获取当前用户的商户
|
||||
|
||||
Reference in New Issue
Block a user