views.py 14 KB

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