views.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. from rest_framework.views import APIView
  2. from rest_framework.response import Response
  3. from rest_framework.permissions import AllowAny
  4. from rest_framework import status
  5. from rest_framework import viewsets
  6. from django_filters.rest_framework import DjangoFilterBackend
  7. from rest_framework.filters import OrderingFilter
  8. from django.utils import timezone
  9. from django.db.models import Q, F, Case, When
  10. from utils.page import MyPageNumberPagination
  11. from .models import *
  12. from .serializers import *
  13. from .filter import *
  14. import logging
  15. logger = logging.getLogger('wms.boundBill')
  16. class WMSResponse:
  17. """
  18. 入库申请专用响应格式
  19. 使用示例:
  20. return WMSResponse.success(data=serializer.data, total=2, success=2)
  21. return WMSResponse.error(message='验证失败', total=2, fail_count=1,
  22. fail_materials=[{...}])
  23. """
  24. @staticmethod
  25. def success(data, total, success, status=status.HTTP_201_CREATED):
  26. """成功响应格式"""
  27. logger.info('成功响应 | 数据: %s', data)
  28. return Response({
  29. "status": True,
  30. "errorCode": status,
  31. "message": "success",
  32. "data": {
  33. "failCount": 0,
  34. "totalCount": total,
  35. "successCount": success,
  36. "fail_materials": []
  37. }
  38. }, status=status)
  39. @staticmethod
  40. def error(message, total, fail_count, fail_materials=None,
  41. status=status.HTTP_400_BAD_REQUEST, exception=None):
  42. """错误响应格式"""
  43. if fail_materials is None:
  44. fail_materials = []
  45. # 记录详细错误日志
  46. if exception:
  47. logger.error(f"入库申请错误: {message}", exc_info=exception)
  48. return Response({
  49. "status": False,
  50. "errorCode": status,
  51. "message": message,
  52. "data": {
  53. "failCount": fail_count,
  54. "totalCount": total,
  55. "successCount": total - fail_count,
  56. "fail_materials": fail_materials
  57. }
  58. }, status=status)
  59. # Create your views here.
  60. # urlpatterns = [
  61. # path('createInboundApply', views.InboundApplyCreate.as_view()),
  62. # path('createOutboundApply', views.OutboundApplyCreate.as_view()),
  63. # path('updateBatchInfo', views.BatchUpdate.as_view()),
  64. # path('productInfo', views.ProductInfo.as_view()),
  65. # ]
  66. class InboundApplyCreate(APIView):
  67. """
  68. 生产入库申请
  69. """
  70. authentication_classes = []
  71. permission_classes = [AllowAny]
  72. def post(self, request):
  73. logger.info('生产入库申请请求 | 原始数据: %s', request.data)
  74. try:
  75. total_count = len(request.data.get('materials', []))
  76. if total_count == 0 :
  77. return WMSResponse.error(
  78. message="物料清单不能为空",
  79. total=0,
  80. fail_count=0
  81. )
  82. if total_count != request.data.get('totalCount', 0):
  83. return WMSResponse.error(
  84. message="物料数量不匹配",
  85. total=total_count,
  86. fail_count=total_count
  87. )
  88. serializer = boundPostSerializer(data=request.data)
  89. if not serializer.is_valid():
  90. return self._handle_validation_error(serializer.errors, total_count)
  91. unique_result,error_details = self.find_unique_billid_and_number(serializer)
  92. if not unique_result:
  93. return WMSResponse.error(
  94. message="单据编号或原始单据ID重复",
  95. total=total_count,
  96. fail_count=total_count,
  97. fail_materials=[{
  98. "entryIds": None,
  99. "production_batch": None,
  100. "errors": error_details
  101. }]
  102. )
  103. # 保存或更新入库单
  104. bound_bill = self.save_or_update_inbound_bill(serializer)
  105. # 保存或更新物料明细
  106. self.save_or_update_material_detail(bound_bill, serializer)
  107. return WMSResponse.success(
  108. data=serializer.data,
  109. total=total_count,
  110. success=total_count
  111. )
  112. except Exception as e:
  113. logger.exception("服务器内部错误")
  114. return WMSResponse.error(
  115. message="系统处理异常",
  116. total=total_count,
  117. fail_count=total_count,
  118. status=status.HTTP_500_INTERNAL_SERVER_ERROR,
  119. exception=e
  120. )
  121. def _handle_validation_error(self, errors, total_count):
  122. """增强错误解析"""
  123. fail_materials = []
  124. # 提取嵌套错误信息
  125. material_errors = errors.get('materials', [])
  126. for error in material_errors:
  127. # 解析DRF的错误结构
  128. if isinstance(error, dict) and 'metadata' in error:
  129. fail_materials.append({
  130. "entryIds": error['metadata'].get('entryIds'),
  131. "production_batch": error['metadata'].get('production_batch'),
  132. "errors": {
  133. "missing_fields": error['metadata']['missing_fields'],
  134. "message": error['detail']
  135. }
  136. })
  137. return WMSResponse.error(
  138. message="物料数据不完整",
  139. total=total_count,
  140. fail_count=len(fail_materials),
  141. fail_materials=fail_materials
  142. )
  143. def _format_material_errors(self, error_dict):
  144. """格式化单个物料的错误信息"""
  145. return {
  146. field: details[0] if isinstance(details, list) else details
  147. for field, details in error_dict.items()
  148. }
  149. def find_unique_billid_and_number(self, serializer):
  150. """增强版唯一性验证"""
  151. bill_id = serializer.validated_data['billId']
  152. number = serializer.validated_data['number']
  153. # 使用Q对象进行联合查询
  154. duplicates_id = InboundBill.objects.filter(
  155. Q(billId=bill_id)
  156. ).only('billId')
  157. # 使用Q对象进行联合查询
  158. duplicates_nu = InboundBill.objects.filter(
  159. Q(number=number)
  160. ).only( 'number')
  161. error_details = {}
  162. # 检查单据编号重复
  163. if any(obj.billId != bill_id for obj in duplicates_nu):
  164. error_details['number'] = ["number入库单编码已存在,但是系统中与之前入库单单据ID不一致,请检查"]
  165. # 检查业务编号重复
  166. if any(obj.number != number for obj in duplicates_id):
  167. error_details['billId'] = ["billId入库单单据ID已存在,但是系统中与之前入库申请单编码不一致,请检查"]
  168. if error_details:
  169. return False,error_details
  170. return True,None
  171. def save_or_update_inbound_bill(self, serializer):
  172. """保存或更新入库单"""
  173. # 保存或更新入库单
  174. try:
  175. bound_bill = InboundBill.objects.get(billId=serializer.validated_data['billId'])
  176. bound_bill.number = serializer.validated_data['number']
  177. bound_bill.type = serializer.validated_data['type']
  178. bound_bill.date = serializer.validated_data['date']
  179. bound_bill.department = serializer.validated_data['department']
  180. bound_bill.warehouse = serializer.validated_data['warehouse']
  181. bound_bill.creater = serializer.validated_data['creater']
  182. bound_bill.note = (serializer.validated_data['note'] if 'note' in serializer.validated_data else '')
  183. bound_bill.totalCount = serializer.validated_data['totalCount']
  184. bound_bill.update_time = timezone.now()
  185. bound_bill.save()
  186. except InboundBill.DoesNotExist:
  187. bound_bill = InboundBill.objects.create(
  188. billId=serializer.validated_data['billId'],
  189. number=serializer.validated_data['number'],
  190. type=serializer.validated_data['type'],
  191. date=serializer.validated_data['date'],
  192. department=serializer.validated_data['department'],
  193. warehouse=serializer.validated_data['warehouse'],
  194. creater=serializer.validated_data['creater'],
  195. note=(serializer.validated_data['note'] if 'note' in serializer.validated_data else ''),
  196. totalCount=serializer.validated_data['totalCount'],
  197. create_time=timezone.now(),
  198. update_time=timezone.now()
  199. )
  200. return bound_bill
  201. def save_or_update_material_detail(self, bound_bill, serializer):
  202. """保存或更新物料明细"""
  203. # 保存或更新物料明细
  204. for item in serializer.validated_data['materials']:
  205. try:
  206. material_detail = MaterialDetail.objects.get(bound_billId=bound_bill, entryIds=item['entryIds'])
  207. material_detail.production_batch = item['production_batch']
  208. material_detail.goods_code = item['goods_code']
  209. material_detail.goods_name = item['goods_name']
  210. material_detail.goods_std = item['goods_std']
  211. material_detail.plan_qty = item['plan_qty']
  212. material_detail.goods_total_weight = item['plan_qty']
  213. material_detail.goods_unit = item['goods_unit']
  214. material_detail.note = (item['note'] if 'note' in item else '')
  215. material_detail.update_time = timezone.now()
  216. material_detail.save()
  217. except MaterialDetail.DoesNotExist:
  218. material_detail = MaterialDetail.objects.create(
  219. bound_billId=bound_bill,
  220. entryIds=item['entryIds'],
  221. production_batch=item['production_batch'],
  222. goods_code=item['goods_code'],
  223. goods_name=item['goods_name'],
  224. goods_std=item['goods_std'],
  225. goods_weight=1,
  226. plan_qty=item['plan_qty'],
  227. goods_total_weight=item['plan_qty'],
  228. goods_unit=item['goods_unit'],
  229. note=(item['note'] if 'note' in item else ''),
  230. create_time=timezone.now(),
  231. update_time=timezone.now()
  232. )
  233. return material_detail
  234. class OutboundApplyCreate(APIView):
  235. """
  236. 生产出库申请
  237. """
  238. authentication_classes = [] # 禁用所有认证类
  239. permission_classes = [AllowAny] # 允许任意访问
  240. def post(self, request):
  241. return Response(status=status.HTTP_200_OK)
  242. class BatchUpdate(APIView):
  243. """
  244. 批次信息更新
  245. """
  246. authentication_classes = [] # 禁用所有认证类
  247. permission_classes = [AllowAny] # 允许任意访问
  248. def post(self, request):
  249. logger.info('批次信息更新')
  250. serializer = BatchUpdateSerializer(data=request.data)
  251. return Response(serializer.data, status=status.HTTP_200_OK)
  252. class ProductInfo(APIView):
  253. """
  254. 商品信息查询
  255. """
  256. authentication_classes = [] # 禁用所有认证类
  257. permission_classes = [AllowAny] # 允许任意访问
  258. def post(self, request):
  259. logger.info('商品信息查询')
  260. serializer = ProductInfoSerializer(data=request.data)
  261. return Response(serializer.data, status=status.HTTP_200_OK)
  262. class InboundBills(viewsets.ModelViewSet):
  263. """
  264. retrieve:
  265. Response a data list(get)
  266. list:
  267. Response a data list(all)
  268. """
  269. # authentication_classes = [] # 禁用所有认证类
  270. # permission_classes = [AllowAny] # 允许任意访问
  271. pagination_class = MyPageNumberPagination
  272. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  273. ordering_fields = ["update_time", "create_time"]
  274. filter_class = InboundBillFilter
  275. def get_project(self):
  276. # 获取项目ID,如果不存在则返回None
  277. try:
  278. id = self.kwargs.get('pk')
  279. return id
  280. except:
  281. return None
  282. def get_queryset(self):
  283. # 根据请求用户过滤查询集
  284. id = self.get_project()
  285. if self.request.user:
  286. if id is None:
  287. return InboundBill.objects.filter(is_delete=False)
  288. else:
  289. return InboundBill.objects.filter(billId=id, is_delete=False)
  290. else:
  291. return InboundBill.objects.none()
  292. def get_serializer_class(self):
  293. # 根据操作类型选择合适的序列化器
  294. if self.action in ['list', 'retrieve', ]:
  295. return InboundApplySerializer
  296. else:
  297. return self.http_method_not_allowed(request=self.request)
  298. class Materials(viewsets.ModelViewSet):
  299. """
  300. retrieve:
  301. Response a data list(get)
  302. list:
  303. Response a data list(all)
  304. """
  305. # authentication_classes = [] # 禁用所有认证类
  306. # permission_classes = [AllowAny] # 允许任意访问
  307. pagination_class = MyPageNumberPagination
  308. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  309. ordering_fields = ['id', "create_time", "update_time", ]
  310. filter_class = MaterialDetailFilter
  311. def get_project(self):
  312. try:
  313. id = self.kwargs.get('pk')
  314. return id
  315. except:
  316. return None
  317. def get_queryset(self):
  318. id = self.get_project()
  319. if self.request.user:
  320. if id is None:
  321. return MaterialDetail.objects.filter(is_delete=False)
  322. else:
  323. return MaterialDetail.objects.filter(id=id)
  324. else:
  325. return MaterialDetail.objects.none()
  326. def get_serializer_class(self):
  327. if self.action in ['retrieve', 'list']:
  328. return MaterialDetailSerializer
  329. else:
  330. return self.http_method_not_allowed(request=self.request)