1
0
forked from erp-dev/erp

feat: retry haobuye sync order

This commit is contained in:
2026-04-07 16:56:46 +08:00
parent ebd687ccdd
commit 76ea445880
15 changed files with 1098 additions and 118 deletions

View File

@@ -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 = [

View 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"],
},
),
]

View 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='合卷备注'),
),
]

View File

@@ -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):
"""
送货单

View File

@@ -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]