forked from erp-dev/erp
363 lines
14 KiB
Python
363 lines
14 KiB
Python
import datetime
|
||
|
||
from django.utils import timezone
|
||
from django.db import transaction
|
||
from rest_framework import serializers, status, permissions
|
||
from rest_framework.response import Response
|
||
from rest_framework.views import APIView
|
||
|
||
from basic_info import models as basic_models
|
||
from printing import models as printing_models
|
||
from api_man.serializers import ProductSerializer
|
||
|
||
|
||
class IsPrintingFactory(permissions.BasePermission):
|
||
"""仅允许印染工厂用户访问(与 api_v1 逻辑保持一致)"""
|
||
|
||
message = '您没有访问印染订单的权限'
|
||
|
||
def has_permission(self, request, view):
|
||
if not request.user or not request.user.is_authenticated:
|
||
return False
|
||
if hasattr(request.user, 'employee'):
|
||
return request.user.employee.merchant.type == basic_models.MerchantTypeEnum.FACTORY
|
||
return False
|
||
|
||
|
||
class PrintingJobV2Serializer(serializers.ModelSerializer):
|
||
"""v2 独立的印染任务序列化器,包含开单数量"""
|
||
|
||
billed_quantity = serializers.DecimalField(max_digits=18, decimal_places=2, read_only=True)
|
||
business_object_id = serializers.SerializerMethodField()
|
||
width = serializers.SerializerMethodField()
|
||
fabric = serializers.SerializerMethodField()
|
||
product = ProductSerializer(read_only=True)
|
||
|
||
class Meta:
|
||
model = printing_models.PrintingJob
|
||
fields = [
|
||
'id',
|
||
'printing_order',
|
||
'product',
|
||
'work_state',
|
||
'quantity',
|
||
'width',
|
||
'fabric',
|
||
'unit',
|
||
'size',
|
||
'pieces',
|
||
'description',
|
||
'business_object_id',
|
||
'created_at',
|
||
'updated_at',
|
||
'billed_quantity',
|
||
]
|
||
read_only_fields = ['id', 'created_at', 'updated_at', 'billed_quantity']
|
||
|
||
def get_business_object_id(self, obj):
|
||
return obj.business_object_id
|
||
|
||
def get_width(self, obj: printing_models.PrintingJob) -> float:
|
||
return obj.printing_order.width
|
||
|
||
def get_fabric(self, obj: printing_models.PrintingJob) -> str:
|
||
return obj.printing_order.fabric
|
||
|
||
|
||
class PrintingJobByCustomerView(APIView):
|
||
"""
|
||
按客户与日期范围查询印染任务。
|
||
|
||
必填 query 参数:
|
||
- customer_id: 客户 ID
|
||
- date_from: 开始日期 (YYYY-MM-DD)
|
||
- date_to: 结束日期 (YYYY-MM-DD),闭区间,包含 23:59:59
|
||
可选过滤:
|
||
- printing_order: 按印染主订单 ID
|
||
- product_id / product_name / product_human_id / product_width_size / product_color
|
||
"""
|
||
|
||
serializer_class = PrintingJobV2Serializer
|
||
permission_classes = [permissions.AllowAny]
|
||
|
||
def get(self, request):
|
||
qp = request.query_params
|
||
customer_id = qp.get('customer_id')
|
||
date_from = qp.get('date_from')
|
||
date_to = qp.get('date_to')
|
||
printing_order_id = qp.get('printing_order')
|
||
product_id = qp.get('product_id')
|
||
product_name = qp.get('product_name')
|
||
product_human_id = qp.get('product_human_id')
|
||
product_width_size = qp.get('product_width_size')
|
||
product_color = qp.get('product_color')
|
||
|
||
if not customer_id:
|
||
return Response({'detail': 'customer_id 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
|
||
if not date_from or not date_to:
|
||
return Response({'detail': 'date_from 与 date_to 为必填参数'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
customer_id_int = int(customer_id)
|
||
except (TypeError, ValueError):
|
||
return Response({'detail': 'customer_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
start_date = datetime.datetime.strptime(date_from, '%Y-%m-%d').date()
|
||
end_date = datetime.datetime.strptime(date_to, '%Y-%m-%d').date()
|
||
except ValueError:
|
||
return Response({'detail': '日期格式需为 YYYY-MM-DD'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 闭区间:包含当日 00:00:00 和 23:59:59.999999
|
||
start_dt = datetime.datetime.combine(start_date, datetime.time.min)
|
||
end_dt = datetime.datetime.combine(end_date, datetime.time.max)
|
||
|
||
if timezone.is_naive(start_dt):
|
||
start_dt = timezone.make_aware(start_dt, timezone.get_default_timezone())
|
||
if timezone.is_naive(end_dt):
|
||
end_dt = timezone.make_aware(end_dt, timezone.get_default_timezone())
|
||
|
||
queryset = printing_models.PrintingJob.objects.select_related('printing_order', 'product').filter(
|
||
printing_order__customer_id=customer_id_int,
|
||
created_at__gte=start_dt,
|
||
created_at__lte=end_dt,
|
||
)
|
||
|
||
# 可选过滤:printing_order
|
||
if printing_order_id:
|
||
try:
|
||
queryset = queryset.filter(printing_order_id=int(printing_order_id))
|
||
except (TypeError, ValueError):
|
||
return Response({'detail': 'printing_order 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 可选过滤:product
|
||
if product_id:
|
||
try:
|
||
queryset = queryset.filter(product_id=int(product_id))
|
||
except (TypeError, ValueError):
|
||
return Response({'detail': 'product_id 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
|
||
if product_name:
|
||
queryset = queryset.filter(product__name__icontains=product_name)
|
||
if product_human_id:
|
||
queryset = queryset.filter(product__human_id__icontains=product_human_id)
|
||
if product_width_size:
|
||
try:
|
||
width_decimal = float(product_width_size)
|
||
except (TypeError, ValueError):
|
||
return Response({'detail': 'product_width_size 必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
|
||
queryset = queryset.filter(product__width_size=width_decimal)
|
||
if product_color:
|
||
queryset = queryset.filter(product__color__icontains=product_color)
|
||
|
||
queryset = queryset.order_by('-created_at')
|
||
|
||
serializer = self.serializer_class(queryset, many=True)
|
||
return Response(serializer.data)
|
||
|
||
|
||
class PrintingJobBatchAdvancePreviewRequestSerializer(serializers.Serializer):
|
||
"""批量推进:预览/校验请求"""
|
||
|
||
printing_job_ids = serializers.ListField(
|
||
child=serializers.IntegerField(min_value=1),
|
||
allow_empty=False,
|
||
help_text='需要批量推进的 printing_job id 列表',
|
||
)
|
||
|
||
def validate_printing_job_ids(self, value):
|
||
# 去重保持稳定性(前端可能重复传)
|
||
deduped = list(dict.fromkeys(value))
|
||
if not deduped:
|
||
raise serializers.ValidationError('printing_job_ids 不能为空')
|
||
return deduped
|
||
|
||
|
||
class PrintingJobBatchAdvanceSubmitRequestSerializer(PrintingJobBatchAdvancePreviewRequestSerializer):
|
||
"""批量推进:提交请求"""
|
||
|
||
parameters = serializers.DictField(
|
||
child=serializers.JSONField(),
|
||
required=False,
|
||
default=dict,
|
||
help_text='与单条推进接口一致的工艺参数(将作为 **kwargs 传给 stateflow)',
|
||
)
|
||
|
||
|
||
def _validate_jobs_for_batch_advance(printing_job_ids: list[int]):
|
||
"""
|
||
批量推进的核心一致性校验(preview 与 submit 共用)
|
||
|
||
规则:
|
||
1) 所有 id 都存在
|
||
2) 全部属于同一个 printing_order
|
||
3) 全部存在 business_object(流程实例)
|
||
4) 全部具有相同的 next_pending_state(下一待执行节点),否则不允许批量
|
||
|
||
注意:这里不做“竞态”处理(preview 后 submit 前状态变化),submit 时会再次调用该函数重新校验。
|
||
后续如需增强,可在 preview 返回 snapshot token,在 submit 校验 token 以提升用户体验。
|
||
"""
|
||
# 查询并校验存在性
|
||
qs = (
|
||
printing_models.PrintingJob.objects
|
||
.select_related('printing_order', 'business_object', 'business_object__process')
|
||
.filter(id__in=printing_job_ids)
|
||
)
|
||
jobs = list(qs)
|
||
found_ids = {j.id for j in jobs}
|
||
missing_ids = [str(i) for i in printing_job_ids if i not in found_ids]
|
||
if missing_ids:
|
||
raise serializers.ValidationError({'detail': f'以下 printing_job 不存在: {", ".join(missing_ids)}'})
|
||
|
||
# 同一订单
|
||
order_ids = {j.printing_order_id for j in jobs}
|
||
if len(order_ids) != 1:
|
||
raise serializers.ValidationError({'detail': '所选明细不属于同一个 printing_order,无法批量推进'})
|
||
printing_order_id = next(iter(order_ids))
|
||
printing_order = jobs[0].printing_order
|
||
|
||
# 必须有关联流程实例
|
||
no_bo = [str(j.id) for j in jobs if not j.business_object_id]
|
||
if no_bo:
|
||
raise serializers.ValidationError({'detail': f'以下 printing_job 未关联流程实例(business_object),无法推进: {", ".join(no_bo)}'})
|
||
|
||
# 计算并校验 next_pending_state 一致
|
||
from stateflow import services as stateflow_services
|
||
|
||
next_infos = []
|
||
for j in jobs:
|
||
info = stateflow_services.get_next_pending_state(j.business_object, include_parameters=True)
|
||
if info is None:
|
||
next_infos.append((j.id, None))
|
||
else:
|
||
next_infos.append((j.id, info))
|
||
|
||
# 不能包含“无待执行节点”(流程已完成或无节点)
|
||
cannot_advance = [str(job_id) for job_id, info in next_infos if info is None]
|
||
if cannot_advance:
|
||
raise serializers.ValidationError({'detail': f'以下 printing_job 没有待执行节点(流程已完成或无节点),无法批量推进: {", ".join(cannot_advance)}'})
|
||
|
||
# 比对 state_id
|
||
first_info = next_infos[0][1]
|
||
target_state = first_info['state']
|
||
target_order = first_info['order']
|
||
target_state_id = target_state.id
|
||
|
||
diff_jobs = []
|
||
for job_id, info in next_infos:
|
||
if info['state'].id != target_state_id:
|
||
diff_jobs.append(str(job_id))
|
||
if diff_jobs:
|
||
raise serializers.ValidationError({'detail': f'所选明细当前待执行节点不一致,无法批量推进(不同节点的 jobs: {", ".join(diff_jobs)})'})
|
||
|
||
# 参数定义取目标节点(所有一致)
|
||
target_parameters = first_info.get('parameters', []) or []
|
||
|
||
return {
|
||
'printing_order': printing_order,
|
||
'printing_order_id': printing_order_id,
|
||
'jobs': jobs,
|
||
'target_state': target_state,
|
||
'target_order': target_order,
|
||
'target_parameters': target_parameters,
|
||
}
|
||
|
||
|
||
class PrintingJobBatchAdvancePreviewView(APIView):
|
||
"""
|
||
批量推进:预览
|
||
|
||
作用:
|
||
- 校验 printing_job_ids 是否可批量推进(同订单/同待执行节点)
|
||
- 返回“下一步待执行节点”的信息及其工艺参数定义,供前端生成批量表单
|
||
"""
|
||
|
||
permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]
|
||
|
||
def post(self, request):
|
||
srz = PrintingJobBatchAdvancePreviewRequestSerializer(data=request.data)
|
||
srz.is_valid(raise_exception=True)
|
||
|
||
data = _validate_jobs_for_batch_advance(srz.validated_data['printing_job_ids'])
|
||
|
||
from stateflow.serializers import StateParameterSerializer
|
||
|
||
params_srz = StateParameterSerializer(
|
||
data['target_parameters'],
|
||
many=True,
|
||
context={'request': request},
|
||
)
|
||
|
||
return Response({
|
||
'printing_order_id': data['printing_order_id'],
|
||
'printing_job_ids': [j.id for j in data['jobs']],
|
||
'next_state': {
|
||
'id': data['target_state'].id,
|
||
'name': data['target_state'].name,
|
||
'description': data['target_state'].description,
|
||
'order': data['target_order'],
|
||
'parameters': params_srz.data,
|
||
}
|
||
})
|
||
|
||
|
||
class PrintingJobBatchAdvanceSubmitView(APIView):
|
||
"""
|
||
批量推进:提交
|
||
|
||
规则:全成功/全失败
|
||
- 任意一个 job 推进失败:整体回滚(不产生任何 stateflow 日志,也不产生批量推进记录)
|
||
"""
|
||
|
||
permission_classes = [permissions.IsAuthenticated, IsPrintingFactory]
|
||
|
||
def post(self, request):
|
||
srz = PrintingJobBatchAdvanceSubmitRequestSerializer(data=request.data)
|
||
srz.is_valid(raise_exception=True)
|
||
|
||
payload = srz.validated_data
|
||
parameters = payload.get('parameters') or {}
|
||
|
||
data = _validate_jobs_for_batch_advance(payload['printing_job_ids'])
|
||
|
||
from stateflow import services as stateflow_services
|
||
|
||
# 全成功/全失败:用事务包住整个批量推进
|
||
with transaction.atomic():
|
||
record = printing_models.PrintingJobBatchAdvanceRecord.objects.create(
|
||
printing_order=data['printing_order'],
|
||
state=data['target_state'],
|
||
created_by=request.user,
|
||
parameters=parameters,
|
||
)
|
||
record.printing_jobs.set(data['jobs'])
|
||
|
||
# 逐个复用单条推进逻辑
|
||
last_message = None
|
||
for job in data['jobs']:
|
||
ok, msg, _state_log = stateflow_services.advance_to_next_state(
|
||
job.business_object,
|
||
request.user,
|
||
**parameters
|
||
)
|
||
if not ok:
|
||
# 抛异常触发事务回滚,保证“全部失败”
|
||
raise serializers.ValidationError({'detail': msg})
|
||
last_message = msg
|
||
|
||
# 返回最新的 job 列表(可用于前端刷新)
|
||
refreshed_jobs = (
|
||
printing_models.PrintingJob.objects
|
||
.select_related('printing_order', 'product')
|
||
.filter(id__in=[j.id for j in data['jobs']])
|
||
.order_by('id')
|
||
)
|
||
job_srz = PrintingJobV2Serializer(refreshed_jobs, many=True)
|
||
|
||
return Response({
|
||
'detail': last_message or '批量推进成功',
|
||
'batch_id': record.id,
|
||
'printing_order_id': data['printing_order_id'],
|
||
'printing_job_ids': [j.id for j in data['jobs']],
|
||
'jobs': job_srz.data,
|
||
})
|