1
0
forked from erp-dev/erp

feat: batch update for plate order

This commit is contained in:
2025-12-18 17:36:43 +08:00
parent 98d797221a
commit 1a6ce32441
33 changed files with 32087 additions and 8 deletions

View File

@@ -10,6 +10,7 @@ from .printing import (
PrintingJobBatchAdvanceSubmitView,
PlateOrderByProcessNodeView,
PlateOrderByProcessView,
PlateOrderBatchUpdateView,
)
from .stateflow import BusinessObjectCloneView
@@ -21,6 +22,7 @@ __all__ = [
'PrintingJobBatchAdvanceSubmitView',
'PlateOrderByProcessNodeView',
'PlateOrderByProcessView',
'PlateOrderBatchUpdateView',
'BusinessObjectCloneView',
]

View File

@@ -2,6 +2,7 @@ import datetime
from django.utils import timezone
from django.db import transaction
from django.db import models as django_models
from django.db.models import Q, Count, CharField, Prefetch
from django.db.models.functions import Cast, Coalesce
from rest_framework import serializers, status, permissions
@@ -41,6 +42,7 @@ class PrintingJobV2Serializer(serializers.ModelSerializer):
model = printing_models.PrintingJob
fields = [
'id',
'original_id',
'printing_order',
'product',
'work_state',
@@ -385,6 +387,7 @@ class PlateOrderByProcessNodeSerializer(serializers.ModelSerializer):
model = printing_models.PlateOrder
fields = [
'id',
'original_id',
'design_code',
'customer',
'customer_name',
@@ -627,6 +630,7 @@ class PlateOrderByProcessSerializer(serializers.ModelSerializer):
model = printing_models.PlateOrder
fields = [
'id',
'original_id',
'design_code',
'customer',
'customer_name',
@@ -886,3 +890,193 @@ class PlateOrderByProcessView(APIView):
'previous': paginator.get_previous_link() if page is not None else None,
'results': srz.data,
})
# 允许批量更新的字段白名单:只改这里即可增减
PLATE_ORDER_BATCH_UPDATE_ALLOWED_FIELDS = [
"original_id",
"design_code",
"plate_type",
"plate_date",
"plate_method",
"image_name",
"plate_notes",
"reprint_reason",
"urgency_level",
"is_invalid",
"customer",
"area",
"default_address",
"salesperson",
"merchandiser",
"designer",
"style_name",
"fabric",
"fabric_source",
"width",
"production_method",
"is_mark_frame",
"drawing_rating",
"color_matching_rating",
"sample_rating",
"difficulty_rating",
"sample_meter",
"required_sample_meters",
"required_completion_date",
"completion_date",
"approval_result",
"is_ordered",
"customer_feedback",
]
class PlateOrderBatchUpdateDataSerializer(serializers.ModelSerializer):
"""
PlateOrder 批量更新允许字段(白名单)。
说明:
- 不允许更新 process/business_object/created_by 等会触发流程副作用或越权的字段
- plate_image 属于结构化字段(且 v1 有额外转换逻辑),暂不纳入批量更新,避免前端误用
"""
class Meta:
model = printing_models.PlateOrder
fields = PLATE_ORDER_BATCH_UPDATE_ALLOWED_FIELDS
class PlateOrderBatchUpdateRequestSerializer(serializers.Serializer):
plate_order_ids = serializers.ListField(
child=serializers.IntegerField(min_value=1),
allow_empty=False,
help_text="需要批量更新的 PlateOrder id 列表(同一组 data 会应用到所有 id",
)
data = serializers.DictField(
child=serializers.JSONField(),
allow_empty=False,
help_text='需要更新的字段集合,例如 {"urgency_level": "加急", "designer": 123}',
)
dry_run = serializers.BooleanField(
required=False,
default=False,
help_text="仅校验与预览,不实际写库",
)
def validate_plate_order_ids(self, value):
# 去重保持稳定性(前端可能重复传)
deduped = list(dict.fromkeys(value))
if not deduped:
raise serializers.ValidationError("plate_order_ids 不能为空")
return deduped
def validate_data(self, value):
data_srz = PlateOrderBatchUpdateDataSerializer(data=value, partial=True)
data_srz.is_valid(raise_exception=True)
if not data_srz.validated_data:
raise serializers.ValidationError({"detail": "data 不能为空"})
return data_srz.validated_data
class PlateOrderBatchUpdateView(APIView):
"""
PlateOrder 批量更新(同一份 data 应用到多个 plate_order
POST /api/v2/plate-orders/batch-update/
Body:
{
"plate_order_ids": [1, 2, 3],
"data": {"urgency_level": "加急", "designer": 10},
"dry_run": false
}
规则:
- 全成功/全失败(任意 id 不存在直接 400不做部分更新
- 需要 printing.change_plateorder 权限
- 如包含 is_invalid
- is_invalid=true 需要 printing.can_invalidate_plateorder
- is_invalid=false 需要 printing.can_activate_plateorder
"""
permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]
def post(self, request):
# 注意Django 会缓存 has_perm 结果user._perm_cache
# 在测试中 APIClient.force_authenticate 会复用同一个 user 实例,
# 可能导致“中途赋权后第二次请求仍判定无权限”的假阴性;这里主动清理缓存更稳妥。
for cache_attr in ("_perm_cache", "_user_perm_cache", "_group_perm_cache"):
if hasattr(request.user, cache_attr):
delattr(request.user, cache_attr)
if not request.user.has_perm("printing.change_plateorder"):
return Response({"detail": "您没有权限批量更新开版订单"}, status=status.HTTP_403_FORBIDDEN)
# 先对 data 做“字段白名单”校验(保证错误输出为顶层 detail便于前端/测试消费)
raw_data = request.data.get("data") if isinstance(request.data, dict) else None
if isinstance(raw_data, dict):
allowed = set(PLATE_ORDER_BATCH_UPDATE_ALLOWED_FIELDS)
unknown = sorted(set(raw_data.keys()) - allowed)
if unknown:
return Response(
{"detail": f"不支持批量更新字段: {', '.join(unknown)}", "allowed_fields": sorted(allowed)},
status=status.HTTP_400_BAD_REQUEST,
)
srz = PlateOrderBatchUpdateRequestSerializer(data=request.data)
srz.is_valid(raise_exception=True)
payload = srz.validated_data
plate_order_ids: list[int] = payload["plate_order_ids"]
data: dict = payload["data"]
dry_run: bool = payload.get("dry_run", False)
# is_invalid 权限语义:沿用 v1 的作废/恢复权限
if "is_invalid" in data:
if data["is_invalid"] is True and not request.user.has_perm("printing.can_invalidate_plateorder"):
return Response({"detail": "您没有权限作废开版订单"}, status=status.HTTP_403_FORBIDDEN)
if data["is_invalid"] is False and not request.user.has_perm("printing.can_activate_plateorder"):
return Response({"detail": "您没有权限恢复开版订单"}, status=status.HTTP_403_FORBIDDEN)
qs = printing_models.PlateOrder.objects.filter(id__in=plate_order_ids)
found_ids = list(qs.values_list("id", flat=True))
found_set = set(found_ids)
missing_ids = [str(i) for i in plate_order_ids if i not in found_set]
if missing_ids:
return Response({"detail": f"以下 PlateOrder 不存在: {', '.join(missing_ids)}"}, status=status.HTTP_400_BAD_REQUEST)
# 返回/展示时保持与入参一致的顺序
found_ids = [i for i in plate_order_ids if i in found_set]
# 将 validated_data 转换为 queryset.update 可用的 kwargs处理 FK -> *_id
update_kwargs: dict = {}
for k, v in data.items():
try:
field = printing_models.PlateOrder._meta.get_field(k)
except Exception:
update_kwargs[k] = v
continue
if isinstance(field, django_models.ForeignKey):
update_kwargs[f"{k}_id"] = v.pk if v is not None else None
else:
update_kwargs[k] = v
update_kwargs["updated_at"] = timezone.now()
if dry_run:
return Response(
{
"dry_run": True,
"plate_order_ids": found_ids,
"matched_count": len(found_ids),
"data": request.data.get("data") or {},
}
)
with transaction.atomic():
updated_count = qs.update(**update_kwargs)
return Response(
{
"detail": "批量更新成功",
"updated_count": updated_count,
"plate_order_ids": found_ids,
}
)