|  | @@ -7,12 +7,16 @@ 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')
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -63,15 +67,6 @@ class WMSResponse:
 | 
	
		
			
				|  |  |                  "fail_materials": fail_materials
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |          }, status=status)
 | 
	
		
			
				|  |  | -# Create your views here.
 | 
	
		
			
				|  |  | -# urlpatterns = [
 | 
	
		
			
				|  |  | -#     path('createInboundApply', views.InboundApplyCreate.as_view()),
 | 
	
		
			
				|  |  | -#     path('createOutboundApply', views.OutboundApplyCreate.as_view()),
 | 
	
		
			
				|  |  | -#     path('updateBatchInfo', views.BatchUpdate.as_view()),
 | 
	
		
			
				|  |  | -#     path('productInfo', views.ProductInfo.as_view()),
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -# ]
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  class InboundApplyCreate(APIView):
 | 
	
		
			
				|  |  |      """
 | 
	
	
		
			
				|  | @@ -267,6 +262,197 @@ class InboundApplyCreate(APIView):
 | 
	
		
			
				|  |  |                  )
 | 
	
		
			
				|  |  |          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({
 | 
	
		
			
				|  |  | +                    "bound_code": bound_list.bound_code,
 | 
	
		
			
				|  |  | +                    "batch_count": bill_obj.totalCount
 | 
	
		
			
				|  |  | +                }, status=status.HTTP_201_CREATED)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        except InboundBill.DoesNotExist:
 | 
	
		
			
				|  |  | +            logger.error(f"原始单据不存在 | billId: {bill_id}")
 | 
	
		
			
				|  |  | +            return Response({"error": "原始单据不存在"}, status=status.HTTP_404_NOT_FOUND)
 | 
	
		
			
				|  |  | +        except Exception as e:
 | 
	
		
			
				|  |  | +            logger.exception(f"入库单生成异常 | billId: {bill_id}")
 | 
	
		
			
				|  |  | +            return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    def validate_and_lock(self, bill_id):
 | 
	
		
			
				|  |  | +        """验证并锁定相关资源"""
 | 
	
		
			
				|  |  | +        # 锁定原始单据
 | 
	
		
			
				|  |  | +        bill_obj = InboundBill.objects.select_for_update().get(
 | 
	
		
			
				|  |  | +            billId=bill_id,
 | 
	
		
			
				|  |  | +            is_delete=False
 | 
	
		
			
				|  |  | +        )
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        # 状态校验
 | 
	
		
			
				|  |  | +        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):
 | 
	
		
			
				|  |  |      """
 | 
	
		
			
				|  |  |      生产出库申请
 |