from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import AllowAny from rest_framework import status from rest_framework import viewsets from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import OrderingFilter from django.utils import timezone from django.db.models import Q, F, Case, When from datetime import datetime from collections import defaultdict from utils.page import MyPageNumberPagination from .models import * from .serializers import * from .filter import * import logging from django.db import transaction from bound.models import BoundListModel, BoundBatchModel, OutBatchModel,BoundDetailModel,OutBoundDetailModel,BatchLogModel logger = logging.getLogger('wms.boundBill') class WMSResponse: """ 入库申请专用响应格式 使用示例: return WMSResponse.success(data=serializer.data, total=2, success=2) return WMSResponse.error(message='验证失败', total=2, fail_count=1, fail_materials=[{...}]) """ @staticmethod def success(data, total, success, status=status.HTTP_201_CREATED): """成功响应格式""" logger.info('成功响应 | 数据: %s', data) return Response({ "status": True, "errorCode": status, "message": "success", "data": { "failCount": 0, "totalCount": total, "successCount": success, "fail_materials": [] } }, status=status) @staticmethod def error(message, total, fail_count, fail_materials=None, status=status.HTTP_400_BAD_REQUEST, exception=None): """错误响应格式""" if fail_materials is None: fail_materials = [] # 记录详细错误日志 if exception: logger.error(f"入库申请错误: {message}", exc_info=exception) return Response({ "status": False, "errorCode": status, "message": message, "data": { "failCount": fail_count, "totalCount": total, "successCount": total - fail_count, "fail_materials": fail_materials } }, status=status) class InboundApplyCreate(APIView): """ 生产入库申请 """ authentication_classes = [] permission_classes = [AllowAny] def post(self, request): logger.info('生产入库申请请求 | 原始数据: %s', request.data) try: total_count = len(request.data.get('materials', [])) if total_count == 0 : return WMSResponse.error( message="物料清单不能为空", total=0, fail_count=0 ) if total_count != request.data.get('totalCount', 0): return WMSResponse.error( message="物料数量不匹配", total=total_count, fail_count=total_count ) serializer = boundPostSerializer(data=request.data) if not serializer.is_valid(): return self._handle_validation_error(serializer.errors, total_count) unique_result,error_details = self.find_unique_billid_and_number(serializer) if not unique_result: return WMSResponse.error( message="单据编号或原始单据ID重复", total=total_count, fail_count=total_count, fail_materials=[{ "entryIds": None, "production_batch": None, "errors": error_details }] ) # 保存或更新入库单 bound_bill = self.save_or_update_inbound_bill(serializer) # 保存或更新物料明细 self.save_or_update_material_detail(bound_bill, serializer) return WMSResponse.success( data=serializer.data, total=total_count, success=total_count ) except Exception as e: logger.exception("服务器内部错误") return WMSResponse.error( message="系统处理异常", total=total_count, fail_count=total_count, status=status.HTTP_500_INTERNAL_SERVER_ERROR, exception=e ) def _handle_validation_error(self, errors, total_count): """增强错误解析""" fail_materials = [] # 提取嵌套错误信息 material_errors = errors.get('materials', []) for error in material_errors: # 解析DRF的错误结构 if isinstance(error, dict) and 'metadata' in error: fail_materials.append({ "entryIds": error['metadata'].get('entryIds'), "production_batch": error['metadata'].get('production_batch'), "errors": { "missing_fields": error['metadata']['missing_fields'], "message": error['detail'] } }) return WMSResponse.error( message="物料数据不完整", total=total_count, fail_count=len(fail_materials), fail_materials=fail_materials ) def _format_material_errors(self, error_dict): """格式化单个物料的错误信息""" return { field: details[0] if isinstance(details, list) else details for field, details in error_dict.items() } def find_unique_billid_and_number(self, serializer): """增强版唯一性验证""" bill_id = serializer.validated_data['billId'] number = serializer.validated_data['number'] # 使用Q对象进行联合查询 duplicates_id = InboundBill.objects.filter( Q(billId=bill_id) ).only('billId') # 使用Q对象进行联合查询 duplicates_nu = InboundBill.objects.filter( Q(number=number) ).only( 'number') error_details = {} # 检查单据编号重复 if any(obj.billId != bill_id for obj in duplicates_nu): error_details['number'] = ["number入库单编码已存在,但是系统中与之前入库单单据ID不一致,请检查"] # 检查业务编号重复 if any(obj.number != number for obj in duplicates_id): error_details['billId'] = ["billId入库单单据ID已存在,但是系统中与之前入库申请单编码不一致,请检查"] if error_details: return False,error_details return True,None def save_or_update_inbound_bill(self, serializer): """保存或更新入库单""" # 保存或更新入库单 try: bound_bill = InboundBill.objects.get(billId=serializer.validated_data['billId']) bound_bill.number = serializer.validated_data['number'] bound_bill.type = serializer.validated_data['type'] bound_bill.date = serializer.validated_data['date'] bound_bill.department = serializer.validated_data['department'] bound_bill.warehouse = serializer.validated_data['warehouse'] bound_bill.creater = serializer.validated_data['creater'] bound_bill.note = (serializer.validated_data['note'] if 'note' in serializer.validated_data else '') bound_bill.totalCount = serializer.validated_data['totalCount'] bound_bill.update_time = timezone.now() bound_bill.save() except InboundBill.DoesNotExist: bound_bill = InboundBill.objects.create( billId=serializer.validated_data['billId'], number=serializer.validated_data['number'], type=serializer.validated_data['type'], date=serializer.validated_data['date'], department=serializer.validated_data['department'], warehouse=serializer.validated_data['warehouse'], creater=serializer.validated_data['creater'], note=(serializer.validated_data['note'] if 'note' in serializer.validated_data else ''), totalCount=serializer.validated_data['totalCount'], create_time=timezone.now(), update_time=timezone.now() ) return bound_bill def save_or_update_material_detail(self, bound_bill, serializer): """保存或更新物料明细""" # 保存或更新物料明细 for item in serializer.validated_data['materials']: try: material_detail = MaterialDetail.objects.get(bound_billId=bound_bill, entryIds=item['entryIds']) material_detail.production_batch = item['production_batch'] material_detail.goods_code = item['goods_code'] material_detail.goods_name = item['goods_name'] material_detail.goods_std = item['goods_std'] material_detail.plan_qty = item['plan_qty'] material_detail.goods_total_weight = item['plan_qty'] material_detail.goods_unit = item['goods_unit'] material_detail.note = (item['note'] if 'note' in item else '') material_detail.update_time = timezone.now() material_detail.save() except MaterialDetail.DoesNotExist: material_detail = MaterialDetail.objects.create( bound_billId=bound_bill, entryIds=item['entryIds'], production_batch=item['production_batch'], goods_code=item['goods_code'], goods_name=item['goods_name'], goods_std=item['goods_std'], goods_weight=1, plan_qty=item['plan_qty'], goods_total_weight=item['plan_qty'], goods_unit=item['goods_unit'], note=(item['note'] if 'note' in item else ''), create_time=timezone.now(), update_time=timezone.now() ) return material_detail class GenerateInbound(APIView): """ 生产入库单生成接口 功能特性: 1. 防重复创建校验(状态校验+关联单据校验) 2. 事务级数据一致性保障 3. 批量操作优化 4. 完整日志追踪 """ def post(self, request): try: bill_id = request.data.get('billId') if not bill_id: return Response({"error": "缺少必要参数: billId"}, status=status.HTTP_400_BAD_REQUEST) # 开启原子事务 with transaction.atomic(): # 锁定原始单据并校验 bill_obj, bound_list = self.validate_and_lock(bill_id) # 创建出入库主单 bound_list = self.create_bound_list(bill_obj) logger.info(f"创建出入库主单成功 | bound_code: {bound_list.bound_code}") # 处理物料明细(批量操作优化) self.process_materials(bill_obj, bound_list) # 更新原始单据状态 bill_obj.bound_status = 1 bill_obj.save(update_fields=['bound_status']) logger.info(f"入库单生成成功 | billId: {bill_id} -> boundCode: {bound_list.bound_code}") return Response({ "code": 200, "count": 1, "next": "null", "previous": "null", "results":{ "bound_code": bound_list.bound_code, "batch_count": bill_obj.totalCount } }, status=status.HTTP_200_OK) except InboundBill.DoesNotExist: logger.error(f"原始单据不存在 | billId: {bill_id}") return Response({ "code": 404, "error": "原始单据不存在" }, status=status.HTTP_404_NOT_FOUND) except Exception as e: logger.exception(f"入库单生成异常 | billId: {bill_id}") return Response({ "code": 400, "error": str(e) }, status=status.HTTP_200_OK) def validate_and_lock(self, bill_id): """验证并锁定相关资源""" # 锁定原始单据 bill_obj = InboundBill.objects.select_for_update().get( billId=bill_id, is_delete=False ) logger.info(f"锁定原始单据成功 | billId: {bill_id}") logger.info(f"原始单据状态: {bill_obj.bound_status}") # 状态校验 if bill_obj.bound_status == 1: logger.warning(f"单据已生成过入库单 | status: {bill_obj.bound_status}") raise Exception("该单据已生成过入库单") # 关联单据校验(双重校验机制) existing_bound = BoundListModel.objects.filter( Q(bound_desc__contains=f"生产入库单{bill_obj.number}") | Q(relate_bill=bill_obj) ).first() if existing_bound: logger.warning(f"发现重复关联单据 | existingCode: {existing_bound.bound_code}") raise Exception(f"已存在关联入库单[{existing_bound.bound_code}]") return bill_obj, None def process_materials(self, bill_obj, bound_list): """批量处理物料明细""" materials = MaterialDetail.objects.filter( bound_billId=bill_obj, is_delete=False ).select_related('bound_billId') if not materials: raise Exception("入库单没有有效物料明细") # 批量创建对象列表 batch_list = [] detail_list = [] log_list = [] goods_counter = defaultdict(int) order_day=str(timezone.now().strftime('-%Y%m')) order_month=str(timezone.now().strftime('%Y%m')) for idx, material in enumerate(materials, 1): # 生成批次 data = {} qs_set = BoundBatchModel.objects.filter( goods_code=material.goods_code, bound_month=order_month, is_delete=False) goods_code = material.goods_code goods_counter[goods_code] += 1 len_qs_set = len(qs_set) + goods_counter[goods_code] print("len_qs_set", len_qs_set) data['bound_batch_order'] = int(order_day.split('-')[-1])*100 + len_qs_set data['bound_number'] = material.goods_code + order_day + str(len_qs_set).zfill(2) batch = BoundBatchModel( bound_number=data['bound_number'], bound_month=bound_list.bound_month, bound_batch_order=data['bound_batch_order'], warehouse_code='W01', warehouse_name='立体仓', goods_code=material.goods_code, goods_desc=material.goods_name, goods_std=material.goods_std, goods_unit=material.goods_unit, goods_qty=material.plan_qty, goods_weight=float(material.goods_weight), goods_total_weight=float(material.goods_total_weight), creater=bound_list.creater, openid=bound_list.openid, relate_material=material ) batch_list.append(batch) # 生成明细 detail_list.append(BoundDetailModel( bound_list=bound_list, bound_batch=batch, detail_code=f"{batch.bound_number}-DET", creater=bound_list.creater, openid=bound_list.openid )) # 生成日志 log_list.append(BatchLogModel( batch_id=batch, log_type=0, log_date=timezone.now(), goods_code=batch.goods_code, goods_desc=batch.goods_desc, goods_qty=batch.goods_qty, log_content=f"生产入库批次创建,来源单据:{bill_obj.number}", creater=batch.creater, openid=batch.openid )) # 批量写入数据库 BoundBatchModel.objects.bulk_create(batch_list) BoundDetailModel.objects.bulk_create(detail_list) BatchLogModel.objects.bulk_create(log_list) def create_bound_list(self, bill_obj): """创建出入库主单(带来源标识)""" if BoundListModel.objects.filter(relate_bill=bill_obj).exists(): return BoundListModel.objects.get(relate_bill=bill_obj) return BoundListModel.objects.create( bound_month=timezone.now().strftime("%Y%m"), bound_date=timezone.now().strftime("%Y-%m-%d"), bound_code=bill_obj.number, bound_code_type=bill_obj.type, bound_bs_type='B01', bound_type='in', bound_desc=f"生产入库单{bill_obj.number}", bound_department=(bill_obj.department if bill_obj.department else 'D99'), base_type=0, bound_status='101', creater=bill_obj.creater, openid='ERP', relate_bill=bill_obj ) def generate_sequence_code(self, prefix): """生成带顺序的编号(示例实现)""" timestamp = datetime.now().strftime("%Y%m%d%H%M") last_code = BoundListModel.objects.filter( bound_code__startswith=prefix ).order_by('-id').values_list('bound_code', flat=True).first() if last_code: seq = int(last_code[-3:]) + 1 else: seq = 1 return f"{prefix}{timestamp}-{seq:03d}" def handle_exception(self, exc): """统一异常处理""" if isinstance(exc, InboundBill.DoesNotExist): return Response({"error": "原始单据不存在或已被删除"}, status=status.HTTP_404_NOT_FOUND) elif "重复" in str(exc): return Response({"error": str(exc)}, status=status.HTTP_409_CONFLICT) return super().handle_exception(exc) class OutboundApplyCreate(APIView): """ 生产出库申请 """ authentication_classes = [] permission_classes = [AllowAny] def post(self, request): logger.info('生产出库申请请求 | 原始数据: %s', request.data) try: total_count = len(request.data.get('materials', [])) if total_count == 0 : return WMSResponse.error( message="物料清单不能为空", total=0, fail_count=0 ) if total_count != request.data.get('totalCount', 0): return WMSResponse.error( message="物料数量不匹配", total=total_count, fail_count=total_count ) serializer = outboundPostSerializer(data=request.data) if not serializer.is_valid(): print("出错",serializer.errors) return self._handle_validation_error(serializer.errors, total_count) unique_result,error_details = self.find_unique_billid_and_number(serializer) if not unique_result: return WMSResponse.error( message="单据编号或原始单据ID重复", total=total_count, fail_count=total_count, fail_materials=[{ "entryIds": None, "production_batch": None, "errors": error_details }] ) # 保存或更新入库单 bound_bill = self.save_or_update_inbound_bill(serializer) # 保存或更新物料明细 self.save_or_update_material_detail(bound_bill, serializer) return WMSResponse.success( data=serializer.data, total=total_count, success=total_count ) except Exception as e: logger.exception("服务器内部错误") return WMSResponse.error( message="系统处理异常", total=total_count, fail_count=total_count, status=status.HTTP_500_INTERNAL_SERVER_ERROR, exception=e ) def _handle_validation_error(self, errors, total_count): """增强错误解析""" fail_materials = [] # 提取嵌套错误信息 material_errors = errors.get('materials', []) for error in material_errors: # 解析DRF的错误结构 if isinstance(error, dict) and 'metadata' in error: fail_materials.append({ "entryIds": error['metadata'].get('entryIds'), "production_batch": error['metadata'].get('production_batch'), "errors": { "missing_fields": error['metadata']['missing_fields'], "message": error['detail'] } }) return WMSResponse.error( message="物料数据不完整", total=total_count, fail_count=len(fail_materials), fail_materials=fail_materials) def _format_material_errors(self, error_dict): """格式化单个物料的错误信息""" return { field: details[0] if isinstance(details, list) else details for field, details in error_dict.items() } def find_unique_billid_and_number(self, serializer): """增强版唯一性验证""" bill_id = serializer.validated_data['billId'] number = serializer.validated_data['number'] # 使用Q对象进行联合查询 duplicates_id = OutboundBill.objects.filter( Q(billId=bill_id) ).only('billId') # 使用Q对象进行联合查询 duplicates_nu = OutboundBill.objects.filter( Q(number=number) ).only( 'number') error_details = {} # 检查单据编号重复 if any(obj.billId != bill_id for obj in duplicates_nu): error_details['number'] = ["number出库单编码已存在,但是系统中与之前出库单单据ID不一致,请检查"] # 检查业务编号重复 if any(obj.number != number for obj in duplicates_id): error_details['billId'] = ["billId出库库单单据ID已存在,但是系统中与之前出库申请单编码不一致,请检查"] if error_details: return False,error_details return True,None def save_or_update_inbound_bill(self, serializer): """保存或更新出库单""" try: bound_bill = OutboundBill.objects.get(billId=serializer.validated_data['billId']) bound_bill.number = serializer.validated_data['number'] bound_bill.type = serializer.validated_data['type'] bound_bill.date = serializer.validated_data['date'] bound_bill.department = serializer.validated_data['department'] bound_bill.warehouse = serializer.validated_data['warehouse'] bound_bill.creater = serializer.validated_data['creater'] bound_bill.note = (serializer.validated_data['note'] if 'note' in serializer.validated_data else '') bound_bill.totalCount = serializer.validated_data['totalCount'] bound_bill.update_time = timezone.now() bound_bill.save() except OutboundBill.DoesNotExist: bound_bill = OutboundBill.objects.create( billId=serializer.validated_data['billId'], number=serializer.validated_data['number'], type=serializer.validated_data['type'], date=serializer.validated_data['date'], department=serializer.validated_data['department'], warehouse=serializer.validated_data['warehouse'], creater=serializer.validated_data['creater'], note=(serializer.validated_data['note'] if 'note' in serializer.validated_data else ''), totalCount=serializer.validated_data['totalCount'], create_time=timezone.now(), update_time=timezone.now() ) return bound_bill def save_or_update_material_detail(self, bound_bill, serializer): """保存或更新物料明细""" for item in serializer.validated_data['materials']: try: material_detail = OutMaterialDetail.objects.get(bound_billId=bound_bill, entryIds=item['entryIds']) material_detail.production_batch = item['production_batch'] material_detail.goods_code = item['goods_code'] material_detail.goods_name = item['goods_name'] material_detail.goods_out_qty = item['goods_out_qty'] material_detail.goods_total_weight = item['goods_out_qty'] material_detail.goods_unit = item['goods_unit'] material_detail.note = (item['note'] if 'note' in item else '') material_detail.update_time = timezone.now() material_detail.save() except OutMaterialDetail.DoesNotExist: material_detail = OutMaterialDetail.objects.create( bound_billId=bound_bill, entryIds=item['entryIds'], production_batch=item['production_batch'], goods_code=item['goods_code'], goods_name=item['goods_name'], goods_weight=1, goods_out_qty=item['goods_out_qty'], goods_total_weight=item['goods_out_qty'], goods_unit=item['goods_unit'], note=(item['note'] if 'note' in item else ''), create_time=timezone.now(), update_time=timezone.now() ) return material_detail class BatchUpdate(APIView): """ 批次信息更新 """ authentication_classes = [] # 禁用所有认证类 permission_classes = [AllowAny] # 允许任意访问 def post(self, request): logger.info('批次信息更新') serializer = BatchUpdateSerializer(data=request.data) return Response(serializer.data, status=status.HTTP_200_OK) class ProductInfo(APIView): """ 商品信息查询 """ authentication_classes = [] # 禁用所有认证类 permission_classes = [AllowAny] # 允许任意访问 def post(self, request): logger.info('商品信息查询') serializer = ProductInfoSerializer(data=request.data) return Response(serializer.data, status=status.HTTP_200_OK) class InboundBills(viewsets.ModelViewSet): """ retrieve: Response a data list(get) list: Response a data list(all) """ # authentication_classes = [] # 禁用所有认证类 # permission_classes = [AllowAny] # 允许任意访问 pagination_class = MyPageNumberPagination filter_backends = [DjangoFilterBackend, OrderingFilter, ] ordering_fields = ["update_time", "create_time"] filter_class = InboundBillFilter def get_project(self): # 获取项目ID,如果不存在则返回None try: id = self.kwargs.get('pk') return id except: return None def get_queryset(self): # 根据请求用户过滤查询集 id = self.get_project() if self.request.user: if id is None: return InboundBill.objects.filter(is_delete=False) else: return InboundBill.objects.filter(billId=id, is_delete=False) else: return InboundBill.objects.none() def get_serializer_class(self): # 根据操作类型选择合适的序列化器 if self.action in ['list', 'retrieve', ]: return InboundApplySerializer else: return self.http_method_not_allowed(request=self.request) class OutboundBills(viewsets.ModelViewSet): """ retrieve: Response a data list(get) list: Response a data list(all) """ # authentication_classes = [] # 禁用所有认证类 # permission_classes = [AllowAny] # 允许任意访问 pagination_class = MyPageNumberPagination filter_backends = [DjangoFilterBackend, OrderingFilter, ] ordering_fields = ["update_time", "create_time"] filter_class = OutboundBillFilter def get_project(self): # 获取项目ID,如果不存在则返回None try: id = self.kwargs.get('pk') return id except: return None def get_queryset(self): # 根据请求用户过滤查询集 id = self.get_project() if self.request.user: if id is None: return OutboundBill.objects.filter(is_delete=False) else: return OutboundBill.objects.filter(billId=id, is_delete=False) else: return OutboundBill.objects.none() def get_serializer_class(self): # 根据操作类型选择合适的序列化器 if self.action in ['list', 'retrieve', ]: return InboundApplySerializer else: return self.http_method_not_allowed(request=self.request) class Materials(viewsets.ModelViewSet): """ retrieve: Response a data list(get) list: Response a data list(all) """ # authentication_classes = [] # 禁用所有认证类 # permission_classes = [AllowAny] # 允许任意访问 pagination_class = MyPageNumberPagination filter_backends = [DjangoFilterBackend, OrderingFilter, ] ordering_fields = ['id', "create_time", "update_time", ] filter_class = MaterialDetailFilter def get_project(self): try: id = self.kwargs.get('pk') return id except: return None def get_queryset(self): id = self.get_project() if self.request.user: if id is None: return MaterialDetail.objects.filter(is_delete=False) else: return MaterialDetail.objects.filter(id=id) else: return MaterialDetail.objects.none() def get_serializer_class(self): if self.action in ['retrieve', 'list']: return MaterialDetailSerializer else: return self.http_method_not_allowed(request=self.request) class OutMaterials(viewsets.ModelViewSet): """ retrieve: Response a data list(get) list: Response a data list(all) """ # authentication_classes = [] # 禁用所有认证类 # permission_classes = [AllowAny] # 允许任意访问 pagination_class = MyPageNumberPagination filter_backends = [DjangoFilterBackend, OrderingFilter, ] ordering_fields = ['id', "create_time", "update_time", ] filter_class = OutMaterialDetailFilter def get_project(self): try: id = self.kwargs.get('pk') return id except: return None def get_queryset(self): id = self.get_project() if self.request.user: if id is None: return OutMaterialDetail.objects.filter(is_delete=False) else: return OutMaterialDetail.objects.filter(id=id) else: return OutMaterialDetail.objects.none() def get_serializer_class(self): if self.action in ['retrieve', 'list']: return OutMaterialDetailSerializer else: return self.http_method_not_allowed(request=self.request)