|
@@ -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):
|
|
|
"""
|
|
|
生产出库申请
|