forked from erp-dev/erp
feat: retry haobuye sync order
This commit is contained in:
@@ -3,6 +3,7 @@ from .models import (
|
||||
ExternalFinishedProduct,
|
||||
SalesItem,
|
||||
SalesItemChangeRecord,
|
||||
SalesItemRebuildRecord,
|
||||
Shipment,
|
||||
ShipmentDelivery,
|
||||
)
|
||||
@@ -128,6 +129,38 @@ class SalesItemChangeRecordAdmin(admin.ModelAdmin):
|
||||
]
|
||||
|
||||
|
||||
@admin.register(SalesItemRebuildRecord)
|
||||
class SalesItemRebuildRecordAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"id",
|
||||
"old_sales_item",
|
||||
"new_sales_item",
|
||||
"operator",
|
||||
"old_printing_job_id",
|
||||
"new_printing_job_id",
|
||||
"rebuilt_at",
|
||||
]
|
||||
list_filter = ["rebuilt_at", "created_at"]
|
||||
search_fields = [
|
||||
"old_sales_item__name",
|
||||
"new_sales_item__name",
|
||||
"operator__username",
|
||||
"operator__employee__name",
|
||||
]
|
||||
readonly_fields = [
|
||||
"old_sales_item",
|
||||
"new_sales_item",
|
||||
"operator",
|
||||
"rebuilt_at",
|
||||
"old_printing_job_id",
|
||||
"new_printing_job_id",
|
||||
"old_quantity",
|
||||
"new_quantity",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
|
||||
@admin.register(ShipmentDelivery)
|
||||
class ShipmentDeliveryAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
|
||||
74
shipment/migrations/0019_salesitemrebuildrecord.py
Normal file
74
shipment/migrations/0019_salesitemrebuildrecord.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("shipment", "0018_salesitem_soft_delete_fields"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SalesItemRebuildRecord",
|
||||
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="更新时间")),
|
||||
("rebuilt_at", models.DateTimeField(verbose_name="重建时间")),
|
||||
("old_printing_job_id", models.PositiveIntegerField(blank=True, null=True, verbose_name="旧生产任务ID")),
|
||||
("new_printing_job_id", models.PositiveIntegerField(verbose_name="新生产任务ID")),
|
||||
("old_quantity", models.DecimalField(decimal_places=2, max_digits=12, verbose_name="旧数量")),
|
||||
("new_quantity", models.DecimalField(decimal_places=2, max_digits=12, verbose_name="新数量")),
|
||||
(
|
||||
"new_sales_item",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="rebuild_target_records",
|
||||
to="shipment.salesitem",
|
||||
verbose_name="新销售品",
|
||||
),
|
||||
),
|
||||
(
|
||||
"old_sales_item",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="rebuild_source_records",
|
||||
to="shipment.salesitem",
|
||||
verbose_name="旧销售品",
|
||||
),
|
||||
),
|
||||
(
|
||||
"operator",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="sales_item_rebuild_records",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="操作者",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "销售品重建记录",
|
||||
"verbose_name_plural": "销售品重建记录",
|
||||
"db_table": "sales_item_rebuild_record",
|
||||
"ordering": ["-rebuilt_at", "-id"],
|
||||
},
|
||||
),
|
||||
]
|
||||
18
shipment/migrations/0020_salesitem_merge_remark.py
Normal file
18
shipment/migrations/0020_salesitem_merge_remark.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2026-04-06 10:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shipment', '0019_salesitemrebuildrecord'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='salesitem',
|
||||
name='merge_remark',
|
||||
field=models.JSONField(blank=True, help_text='合卷信息JSON,包含 merge_type, jobs, main_job, quantity, unit, job_count', null=True, verbose_name='合卷备注'),
|
||||
),
|
||||
]
|
||||
@@ -317,6 +317,13 @@ class SalesItem(ModelBase):
|
||||
related_name='deleted_sales_items',
|
||||
verbose_name='删除人',
|
||||
)
|
||||
|
||||
merge_remark = models.JSONField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name='合卷备注',
|
||||
help_text='合卷信息JSON,包含 merge_type, jobs, main_job, quantity, unit, job_count',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = 'sales_item'
|
||||
@@ -409,6 +416,76 @@ class SalesItemChangeRecord(ModelBase):
|
||||
return f"SalesItemChangeRecord #{self.id} - sales_item={self.sales_item_id}"
|
||||
|
||||
|
||||
class SalesItemRebuildRecord(ModelBase):
|
||||
"""
|
||||
销售品重建记录。
|
||||
|
||||
用于审计“软删除旧销售品并基于新 printing_job 重建销售品”的动作。
|
||||
"""
|
||||
|
||||
old_sales_item = models.ForeignKey(
|
||||
SalesItem,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="rebuild_source_records",
|
||||
verbose_name="旧销售品",
|
||||
)
|
||||
|
||||
new_sales_item = models.ForeignKey(
|
||||
SalesItem,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="rebuild_target_records",
|
||||
verbose_name="新销售品",
|
||||
)
|
||||
|
||||
operator = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="sales_item_rebuild_records",
|
||||
verbose_name="操作者",
|
||||
)
|
||||
|
||||
rebuilt_at = models.DateTimeField(
|
||||
verbose_name="重建时间",
|
||||
)
|
||||
|
||||
old_printing_job_id = models.PositiveIntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="旧生产任务ID",
|
||||
)
|
||||
|
||||
new_printing_job_id = models.PositiveIntegerField(
|
||||
verbose_name="新生产任务ID",
|
||||
)
|
||||
|
||||
old_quantity = models.DecimalField(
|
||||
max_digits=12,
|
||||
decimal_places=2,
|
||||
verbose_name="旧数量",
|
||||
)
|
||||
|
||||
new_quantity = models.DecimalField(
|
||||
max_digits=12,
|
||||
decimal_places=2,
|
||||
verbose_name="新数量",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "sales_item_rebuild_record"
|
||||
verbose_name = "销售品重建记录"
|
||||
verbose_name_plural = "销售品重建记录"
|
||||
ordering = ["-rebuilt_at", "-id"]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"SalesItemRebuildRecord #{self.id} - old={self.old_sales_item_id}, new={self.new_sales_item_id}"
|
||||
|
||||
|
||||
class ShipmentDelivery(ModelBase):
|
||||
"""
|
||||
送货单
|
||||
|
||||
@@ -16,6 +16,7 @@ from shipment.models import (
|
||||
ExternalFinishedProduct,
|
||||
SalesItem,
|
||||
SalesItemChangeRecord,
|
||||
SalesItemRebuildRecord,
|
||||
Shipment,
|
||||
ShipmentDelivery,
|
||||
ShipmentDeliveryStatus,
|
||||
@@ -50,6 +51,25 @@ def ensure_sales_item_can_be_soft_deleted(sales_item: SalesItem) -> None:
|
||||
raise ValueError("已关联出货单的销售品不允许删除")
|
||||
|
||||
|
||||
def _get_printing_job_for_sales_item(*, printing_job_id: int, merchant):
|
||||
from printing.models import PrintingJob
|
||||
|
||||
try:
|
||||
return PrintingJob.objects.select_related("printing_order").get(
|
||||
id=printing_job_id,
|
||||
merchant=merchant,
|
||||
)
|
||||
except PrintingJob.DoesNotExist:
|
||||
raise ValueError(f"生产任务 {printing_job_id} 不存在或不属于当前商户")
|
||||
|
||||
|
||||
def _normalize_sales_item_quantity(quantity: str | Decimal) -> Decimal:
|
||||
try:
|
||||
return Decimal(str(quantity))
|
||||
except (InvalidOperation, ValueError, TypeError) as e:
|
||||
raise ValueError(f"数量 {quantity} 格式无效: {e}")
|
||||
|
||||
|
||||
def _resolve_delivery_shipments(
|
||||
*,
|
||||
shipment_ids: list[int],
|
||||
@@ -302,6 +322,64 @@ def delete_sales_item(
|
||||
return sales_item
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def rebuild_sales_item(
|
||||
sales_item: SalesItem,
|
||||
*,
|
||||
new_printing_job_id: int,
|
||||
operator,
|
||||
quantity: str | Decimal | None = None,
|
||||
) -> SalesItem:
|
||||
"""
|
||||
软删除旧销售品,并基于新 printing_job 重建一个新销售品。
|
||||
"""
|
||||
ensure_sales_item_can_be_soft_deleted(sales_item)
|
||||
|
||||
merchant = sales_item.merchant
|
||||
printing_job = _get_printing_job_for_sales_item(
|
||||
printing_job_id=new_printing_job_id,
|
||||
merchant=merchant,
|
||||
)
|
||||
|
||||
new_quantity = (
|
||||
_normalize_sales_item_quantity(quantity)
|
||||
if quantity is not None
|
||||
else sales_item.quantity
|
||||
)
|
||||
|
||||
deleted_sales_item = delete_sales_item(sales_item, deleted_by=operator)
|
||||
|
||||
customer_id = sales_item.customer_id
|
||||
if customer_id is None and printing_job.printing_order and printing_job.printing_order.customer:
|
||||
customer_id = printing_job.printing_order.customer_id
|
||||
|
||||
new_sales_item = SalesItem.objects.create(
|
||||
shipment=None,
|
||||
merchant=merchant,
|
||||
name=sales_item.name,
|
||||
quantity=new_quantity,
|
||||
unit=sales_item.unit,
|
||||
created_by=sales_item.created_by,
|
||||
printing_job_id=printing_job.id,
|
||||
customer_id=customer_id,
|
||||
remark=sales_item.remark,
|
||||
position=sales_item.position,
|
||||
)
|
||||
|
||||
SalesItemRebuildRecord.objects.create(
|
||||
old_sales_item=deleted_sales_item,
|
||||
new_sales_item=new_sales_item,
|
||||
operator=operator,
|
||||
rebuilt_at=timezone.now(),
|
||||
old_printing_job_id=deleted_sales_item.printing_job_id,
|
||||
new_printing_job_id=printing_job.id,
|
||||
old_quantity=deleted_sales_item.quantity,
|
||||
new_quantity=new_sales_item.quantity,
|
||||
)
|
||||
|
||||
return new_sales_item
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def create_shipment_delivery(
|
||||
*,
|
||||
@@ -841,6 +919,7 @@ def create_sales_item(
|
||||
customer_id: int | None = None,
|
||||
remark: str = "",
|
||||
position: str = "",
|
||||
merge_remark: dict | None = None,
|
||||
) -> SalesItem:
|
||||
"""
|
||||
手动创建销售品
|
||||
@@ -861,8 +940,6 @@ def create_sales_item(
|
||||
Raises:
|
||||
ValueError: 如果生产任务不存在或不属于当前商户
|
||||
"""
|
||||
from printing.models import PrintingJob
|
||||
|
||||
# 获取当前用户的商户
|
||||
emp = getattr(created_by, "employee", None)
|
||||
merchant = getattr(emp, "merchant", None) if emp else None
|
||||
@@ -870,19 +947,13 @@ def create_sales_item(
|
||||
raise ValueError("用户未关联商户,无法创建销售品")
|
||||
|
||||
# 验证生产任务存在且属于当前商户
|
||||
try:
|
||||
printing_job = PrintingJob.objects.select_related("printing_order").get(
|
||||
id=printing_job_id,
|
||||
merchant=merchant,
|
||||
)
|
||||
except PrintingJob.DoesNotExist:
|
||||
raise ValueError(f"生产任务 {printing_job_id} 不存在或不属于当前商户")
|
||||
printing_job = _get_printing_job_for_sales_item(
|
||||
printing_job_id=printing_job_id,
|
||||
merchant=merchant,
|
||||
)
|
||||
|
||||
# 转换数量为Decimal
|
||||
try:
|
||||
quantity_decimal = Decimal(str(quantity))
|
||||
except (InvalidOperation, ValueError, TypeError) as e:
|
||||
raise ValueError(f"数量 {quantity} 格式无效: {e}")
|
||||
quantity_decimal = _normalize_sales_item_quantity(quantity)
|
||||
|
||||
# 如果未提供客户ID,尝试从主订单获取
|
||||
if (
|
||||
@@ -904,6 +975,35 @@ def create_sales_item(
|
||||
customer_id=customer_id,
|
||||
remark=remark,
|
||||
position=position,
|
||||
merge_remark=merge_remark,
|
||||
)
|
||||
|
||||
return sales_item
|
||||
|
||||
|
||||
def get_merge_remark_jobs_by_main_job(
|
||||
printing_job_id: int,
|
||||
merchant,
|
||||
) -> list[list[int]]:
|
||||
"""
|
||||
查找指定 printing_job_id 作为 main_job 的所有销售品,返回每条记录的 jobs 列表。
|
||||
|
||||
同一个 printing_job_id 可能出现在多条销售品的 main_job 中,
|
||||
因此返回值是一个二维列表,每个元素对应一条匹配记录的 jobs。
|
||||
|
||||
Args:
|
||||
printing_job_id: 要查询的生产任务ID
|
||||
merchant: 当前商户
|
||||
|
||||
Returns:
|
||||
匹配记录的 jobs 列表集合,无匹配时返回空列表。
|
||||
示例: [[9777, 9776], [9777, 9780]]
|
||||
"""
|
||||
merge_remarks = list(
|
||||
get_active_sales_items_queryset()
|
||||
.filter(merchant=merchant, merge_remark__main_job=printing_job_id)
|
||||
.values_list("merge_remark", flat=True)
|
||||
)
|
||||
if not merge_remarks:
|
||||
return []
|
||||
return [mr.get("jobs", []) for mr in merge_remarks if mr]
|
||||
|
||||
Reference in New Issue
Block a user