views.py 100 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438
  1. from pathlib import Path
  2. from rest_framework.views import APIView
  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 django.core.cache import cache
  11. import requests
  12. from collections import defaultdict
  13. from utils.page import MyPageNumberPagination
  14. from .models import *
  15. from .serializers import *
  16. from .filter import *
  17. from .parsers import TextJSONParser
  18. from .parsers import TextJSONRenderer
  19. import logging
  20. import time
  21. from django.db import transaction
  22. from bound.models import BoundListModel, BoundBatchModel, OutBatchModel,BoundDetailModel,OutBoundDetailModel,BatchOperateLogModel
  23. from warehouse.models import ProductListModel
  24. from rest_framework.response import Response
  25. from operation_log.views import log_success_operation, log_failure_operation
  26. import json
  27. logger = logging.getLogger('wms.boundBill')
  28. class WMSResponse:
  29. """
  30. 入库申请专用响应格式
  31. 使用示例:
  32. return WMSResponse.success(data=serializer.data, total=2, success=2)
  33. return WMSResponse.error(message='验证失败', total=2, fail_count=1,
  34. fail_materials=[{...}])
  35. """
  36. @staticmethod
  37. def success(data, total, success, status=status.HTTP_200_OK):
  38. """成功响应格式"""
  39. logger.info('成功响应 | 数据: %s', data)
  40. return Response({
  41. "status": True,
  42. "errorCode": status,
  43. "message": "success",
  44. "data": {
  45. "failCount": 0,
  46. "totalCount": total,
  47. "successCount": success,
  48. "fail_materials": []
  49. }
  50. }, status=status)
  51. @staticmethod
  52. def error(message, total, fail_count, fail_materials=None,
  53. status=status.HTTP_200_OK, exception=None):
  54. """错误响应格式"""
  55. if fail_materials is None:
  56. fail_materials = []
  57. # 记录详细错误日志
  58. if exception:
  59. logger.error(f"入库申请错误: {message}", exc_info=exception)
  60. return Response({
  61. "status": False,
  62. "errorCode": 400,
  63. "message": message,
  64. "data": {
  65. "failCount": fail_count,
  66. "totalCount": total,
  67. "successCount": total - fail_count,
  68. "fail_materials": fail_materials
  69. }
  70. }, status=status)
  71. """入库申请"""
  72. class InboundApplyCreate(APIView):
  73. """
  74. 生产入库申请
  75. """
  76. authentication_classes = []
  77. permission_classes = [AllowAny]
  78. def post(self, request):
  79. logger.info('生产入库申请请求 | 原始数据: %s', request.data)
  80. try:
  81. total_count = len(request.data.get('materials', []))
  82. if total_count == 0 :
  83. return WMSResponse.error(
  84. message="物料清单不能为空",
  85. total=0,
  86. fail_count=0
  87. )
  88. if total_count != request.data.get('totalCount', 0):
  89. return WMSResponse.error(
  90. message="物料数量不匹配",
  91. total=total_count,
  92. fail_count=total_count
  93. )
  94. serializer = boundPostSerializer(data=request.data)
  95. if not serializer.is_valid():
  96. return self._handle_validation_error(serializer.errors, total_count)
  97. unique_result,error_details = self.find_unique_billid_and_number(serializer)
  98. if not unique_result:
  99. return WMSResponse.error(
  100. message="单据编号或原始单据ID重复",
  101. total=total_count,
  102. fail_count=total_count,
  103. fail_materials=[{
  104. "entryIds": None,
  105. "production_batch": None,
  106. "errors": error_details
  107. }]
  108. )
  109. # 保存或更新入库单
  110. bound_bill = self.save_or_update_inbound_bill(serializer)
  111. materildetail_list = MaterialDetail.objects.filter(bound_billId=bound_bill).all()
  112. for item in materildetail_list:
  113. item.is_delete = True
  114. item.save()
  115. # 保存或更新物料明细
  116. self.save_or_update_material_detail(bound_bill, serializer)
  117. return WMSResponse.success(
  118. data=serializer.data,
  119. total=total_count,
  120. success=total_count
  121. )
  122. except Exception as e:
  123. logger.exception("服务器内部错误")
  124. return WMSResponse.error(
  125. message="系统处理异常",
  126. total=total_count,
  127. fail_count=total_count,
  128. status=status.HTTP_500_INTERNAL_SERVER_ERROR,
  129. exception=e
  130. )
  131. def _handle_validation_error(self, errors, total_count):
  132. """增强错误解析"""
  133. fail_materials = []
  134. # 提取嵌套错误信息
  135. material_errors = errors.get('materials', [])
  136. for error in material_errors:
  137. # 解析DRF的错误结构
  138. if isinstance(error, dict) and 'metadata' in error:
  139. fail_materials.append({
  140. "entryIds": error['metadata'].get('entryIds'),
  141. "production_batch": error['metadata'].get('production_batch'),
  142. "errors": {
  143. "missing_fields": error['metadata']['missing_fields'],
  144. "message": error['detail']
  145. }
  146. })
  147. return WMSResponse.error(
  148. message="物料数据不完整",
  149. total=total_count,
  150. fail_count=len(fail_materials),
  151. fail_materials=fail_materials
  152. )
  153. def _format_material_errors(self, error_dict):
  154. """格式化单个物料的错误信息"""
  155. return {
  156. field: details[0] if isinstance(details, list) else details
  157. for field, details in error_dict.items()
  158. }
  159. def find_unique_billid_and_number(self, serializer):
  160. """增强版唯一性验证"""
  161. bill_id = serializer.validated_data['billId']
  162. number = serializer.validated_data['number']
  163. # 使用Q对象进行联合查询
  164. duplicates_id = InboundBill.objects.filter(
  165. Q(billId=bill_id)
  166. ).only('billId')
  167. # 使用Q对象进行联合查询
  168. duplicates_nu = InboundBill.objects.filter(
  169. Q(number=number)
  170. ).only( 'number')
  171. error_details = {}
  172. # 检查单据编号重复
  173. if any(obj.billId != bill_id for obj in duplicates_nu):
  174. error_details['number'] = ["number入库单编码已存在,但是系统中与之前入库单单据ID不一致,请检查"]
  175. # 检查业务编号重复
  176. if any(obj.number != number for obj in duplicates_id):
  177. error_details['billId'] = ["billId入库单单据ID已存在,但是系统中与之前入库申请单编码不一致,请检查"]
  178. if error_details:
  179. return False,error_details
  180. return True,None
  181. def audit_save_status(self, bound_bill_type,base_type):
  182. # 根据不同入库单类型和不同出入库类型,返回审核状态和保存状态
  183. # bound_bill_type:表示不同出入库类型 :1(生产入库申请-生产汇报单)2(采购入库申请-收料通知单) 3(其他入库)4(调拨入库) 1(销售出库申请-发货通知单) 2(生产领料)3(其他出库)
  184. # 1,1 生产入库-> 0,9 (0为未审核,9为无需保存)
  185. # 2,1 采购入库-> 0,0 (0为未审核,0为未保存)
  186. # 3,1 其他入库-> 0,9 (0为未审核,9为无需保存)
  187. # 4,1 调拨入库-> 0,9 (0为未审核,9为无需保存)
  188. # 1,2 销售出库-> 9,0 (9为无需审核,0为未保存)
  189. # 2,2 生产领料-> 0,9 (0为未审核,9为无需保存)
  190. # 3,2 其他出库-> 0,9 (0为未审核,9为无需保存)
  191. # base_type:表示不同入库单类型 1 为入库 2为出库
  192. if base_type == 1: # 入库
  193. if bound_bill_type == 1: # 生产入库
  194. return 0, 9
  195. elif bound_bill_type == 2: # 采购入库
  196. return 0, 0
  197. elif bound_bill_type == 3: # 其他入库
  198. return 0, 9
  199. elif bound_bill_type == 4: # 调拨入库
  200. return 0, 9
  201. else: # 出库
  202. if bound_bill_type == 1: # 销售出库
  203. return 9, 0
  204. elif bound_bill_type == 2: # 生产领料
  205. return 0, 9
  206. elif bound_bill_type == 3: # 其他出库
  207. return 0, 9
  208. return 0, 0
  209. def save_or_update_inbound_bill(self, serializer):
  210. """保存或更新入库单"""
  211. # 保存或更新入库单
  212. try:
  213. bound_bill = InboundBill.objects.get(billId=serializer.validated_data['billId'])
  214. bound_bill.number = serializer.validated_data['number']
  215. bound_bill.type = serializer.validated_data['type']
  216. bound_bill.date = serializer.validated_data['date']
  217. bound_bill.department = (serializer.validated_data['department'] if 'department' in serializer.validated_data else '')
  218. bound_bill.warehouse = serializer.validated_data['warehouse']
  219. bound_bill.creater = serializer.validated_data['creater']
  220. bound_bill.note = (serializer.validated_data['note'] if 'note' in serializer.validated_data else '')
  221. bound_bill.totalCount = serializer.validated_data['totalCount']
  222. if bound_bill.type != 1:
  223. bound_bill.erp_audit_id = bound_bill.number
  224. bound_bill.erp_save_id = bound_bill.billId
  225. bound_bill.audit_status, bound_bill.save_status = self.audit_save_status(bound_bill.type,1)
  226. bound_bill.update_time = timezone.now()
  227. bound_bill.is_delete = False
  228. bound_bill.save()
  229. InboundBillOperateLog.objects.create(
  230. billId=bound_bill,
  231. log_type ='update',
  232. log_content = f"更新入库单{bound_bill.number}信息",
  233. )
  234. except InboundBill.DoesNotExist:
  235. audit_status, save_status = self.audit_save_status(serializer.validated_data['type'], 1)
  236. bound_bill = InboundBill.objects.create(
  237. billId=serializer.validated_data['billId'],
  238. number=serializer.validated_data['number'],
  239. type=serializer.validated_data['type'],
  240. date=serializer.validated_data['date'],
  241. department=(serializer.validated_data['department'] if 'department' in serializer.validated_data else ''),
  242. warehouse=serializer.validated_data['warehouse'],
  243. creater=serializer.validated_data['creater'],
  244. note=(serializer.validated_data['note'] if 'note' in serializer.validated_data else ''),
  245. totalCount=serializer.validated_data['totalCount'],
  246. erp_save_id = serializer.validated_data['billId'],
  247. audit_status = audit_status,
  248. save_status = save_status,
  249. create_time=timezone.now(),
  250. update_time=timezone.now()
  251. )
  252. if bound_bill.type != 1:
  253. bound_bill.erp_audit_id = bound_bill.number
  254. bound_bill.save()
  255. InboundBillOperateLog.objects.create(
  256. billId=bound_bill,
  257. log_type ='create',
  258. log_content = f"创建入库单{bound_bill.number}信息",
  259. )
  260. return bound_bill
  261. def save_or_update_material_detail(self, bound_bill, serializer):
  262. """保存或更新物料明细"""
  263. # 保存或更新物料明细
  264. for item in serializer.validated_data['materials']:
  265. try:
  266. material_detail = MaterialDetail.objects.get(bound_billId=bound_bill, entryIds=item['entryIds'])
  267. material_detail.production_batch = item['production_batch']
  268. material_detail.goods_code = item['goods_code']
  269. material_detail.goods_name = item['goods_name']
  270. material_detail.goods_std = (item['goods_std'] if 'goods_std' in item else '')
  271. material_detail.plan_qty = item['plan_qty']
  272. material_detail.goods_total_weight = item['plan_qty']
  273. material_detail.goods_unit = item['goods_unit']
  274. material_detail.note = (item['note'] if 'note' in item else '')
  275. material_detail.update_time = timezone.now()
  276. material_detail.is_delete = False
  277. material_detail.save()
  278. except MaterialDetail.DoesNotExist:
  279. material_detail = MaterialDetail.objects.create(
  280. bound_billId=bound_bill,
  281. entryIds=item['entryIds'],
  282. production_batch=item['production_batch'],
  283. goods_code=item['goods_code'],
  284. goods_name=item['goods_name'],
  285. goods_std=(item['goods_std'] if 'goods_std' in item else ''),
  286. goods_weight=1,
  287. plan_qty=item['plan_qty'],
  288. goods_total_weight=item['plan_qty'],
  289. goods_unit=item['goods_unit'],
  290. note=(item['note'] if 'note' in item else ''),
  291. create_time=timezone.now(),
  292. update_time=timezone.now()
  293. )
  294. return material_detail
  295. """入库单生成"""
  296. class GenerateInbound(APIView):
  297. """
  298. 生产入库单生成接口
  299. 功能特性:
  300. 1. 防重复创建校验(状态校验+关联单据校验)
  301. 2. 事务级数据一致性保障
  302. 3. 批量操作优化
  303. 4. 完整日志追踪
  304. """
  305. def post(self, request):
  306. try:
  307. bill_id = request.data.get('billId')
  308. if not bill_id:
  309. return Response({"error": "缺少必要参数: billId"},
  310. status=status.HTTP_400_BAD_REQUEST)
  311. # 开启原子事务
  312. with transaction.atomic():
  313. # 锁定原始单据并校验
  314. bill_obj, bound_list = self.validate_and_lock(bill_id)
  315. # 创建出入库主单
  316. bound_list = self.create_bound_list(bill_obj)
  317. logger.info(f"创建出入库主单成功 | bound_code: {bound_list.bound_code}")
  318. # 处理物料明细(批量操作优化)
  319. self.process_materials(bill_obj, bound_list)
  320. # 更新原始单据状态
  321. bill_obj.bound_status = 1
  322. bill_obj.save(update_fields=['bound_status'])
  323. logger.info(f"入库单生成成功 | billId: {bill_id} -> boundCode: {bound_list.bound_code}")
  324. return Response({
  325. "code": 200,
  326. "count": 1,
  327. "next": "null",
  328. "previous": "null",
  329. "results":{
  330. "bound_code": bound_list.bound_code,
  331. "batch_count": bill_obj.totalCount
  332. }
  333. }, status=status.HTTP_200_OK)
  334. except InboundBill.DoesNotExist:
  335. logger.error(f"原始单据不存在 | billId: {bill_id}")
  336. return Response({
  337. "code": 404,
  338. "error": "原始单据不存在"
  339. }, status=status.HTTP_404_NOT_FOUND)
  340. except Exception as e:
  341. logger.exception(f"入库单生成异常 | billId: {bill_id}")
  342. return Response({
  343. "code": 400,
  344. "error": str(e)
  345. }, status=status.HTTP_200_OK)
  346. def validate_and_lock(self, bill_id):
  347. """验证并锁定相关资源"""
  348. # 锁定原始单据
  349. bill_obj = InboundBill.objects.get(
  350. billId=bill_id,
  351. is_delete=False
  352. )
  353. logger.info(f"锁定原始单据成功 | billId: {bill_id}")
  354. logger.info(f"原始单据状态: {bill_obj.bound_status}")
  355. # 状态校验
  356. if bill_obj.bound_status == 1:
  357. logger.warning(f"单据已生成过入库单 | status: {bill_obj.bound_status}")
  358. raise Exception("该单据已生成过入库单")
  359. # 关联单据校验(双重校验机制)
  360. existing_bound = BoundListModel.objects.filter(
  361. Q(bound_desc__contains=f"生产入库单{bill_obj.number}") |
  362. Q(relate_bill=bill_obj),
  363. is_delete=False
  364. ).first()
  365. if existing_bound:
  366. logger.warning(f"发现重复关联单据 | existingCode: {existing_bound.bound_code}")
  367. raise Exception(f"已存在关联入库单[{existing_bound.bound_code}]")
  368. return bill_obj, None
  369. def process_materials(self, bill_obj, bound_list):
  370. """批量处理物料明细"""
  371. materials = MaterialDetail.objects.filter(
  372. bound_billId=bill_obj,
  373. is_delete=False
  374. ).select_related('bound_billId')
  375. if not materials:
  376. raise Exception("入库单没有有效物料明细")
  377. # 批量创建对象列表
  378. batch_list = []
  379. detail_list = []
  380. log_list = []
  381. order_month = str(timezone.now().strftime('%Y%m')) # 修正变量名
  382. for material in materials:
  383. # 规范批次号处理(支持7位和9位格式)
  384. try:
  385. # 拆分生产批次(物料代码-批次号)
  386. parts = material.production_batch.split('-')
  387. if len(parts) < 2:
  388. raise ValueError(f"无效的生产批次格式: {material.production_batch}")
  389. material_goods_code = parts[0]
  390. batch_str = parts[1]
  391. # 标准化批次号处理
  392. if len(batch_str) == 7: # 2508001 格式
  393. # 添加"20"前缀转为9位格式: 202508001
  394. standardized_batch = "20" + batch_str
  395. elif len(batch_str) == 9: # 202508001 格式
  396. standardized_batch = batch_str
  397. else:
  398. raise ValueError(f"无效的批次号长度: {batch_str}")
  399. # 创建规范化的bound_number(物料代码-标准批次号)
  400. bound_number = f"{material_goods_code}-{standardized_batch}"
  401. material.material_goods_code = material_goods_code
  402. material.material_batch_order = standardized_batch
  403. material.save(update_fields=['material_goods_code','material_batch_order'])
  404. except (IndexError, ValueError) as e:
  405. raise Exception(f"物料批次处理错误: {e}")
  406. # 创建批次记录
  407. batch = BoundBatchModel(
  408. bound_number=bound_number, # 使用规范化的批次号
  409. sourced_number=material.production_batch, # 保留原始批次号
  410. bound_month=bound_list.bound_month,
  411. bound_batch_order=standardized_batch, # 存储标准化批次号
  412. warehouse_code='W01',
  413. warehouse_name='立体仓',
  414. goods_code=material_goods_code ,
  415. goods_desc=material.goods_name,
  416. goods_std=material.goods_std or '', # 处理空值
  417. goods_unit=material.goods_unit,
  418. goods_qty=material.plan_qty,
  419. goods_weight=float(material.goods_weight),
  420. goods_total_weight=float(material.goods_total_weight),
  421. creater=bound_list.creater,
  422. openid=bound_list.openid,
  423. relate_material=material # 关联原始物料
  424. )
  425. batch_list.append(batch)
  426. # 创建明细记录
  427. detail_list.append(BoundDetailModel(
  428. bound_list=bound_list,
  429. bound_batch=batch,
  430. detail_code=f"{bound_number}-DET", # 使用规范化的批次号
  431. creater=bound_list.creater,
  432. openid=bound_list.openid
  433. ))
  434. # 创建日志记录
  435. log_list.append(BatchOperateLogModel(
  436. batch_id=batch,
  437. log_type=0,
  438. log_date=timezone.now(),
  439. goods_code=material_goods_code ,
  440. goods_desc=batch.goods_desc,
  441. goods_qty=batch.goods_qty,
  442. log_content=f"生产入库批次创建,来源单据:{bill_obj.number}",
  443. creater=batch.creater,
  444. openid=batch.openid
  445. ))
  446. # 批量写入数据库
  447. try:
  448. with transaction.atomic():
  449. BoundBatchModel.objects.bulk_create(batch_list)
  450. BoundDetailModel.objects.bulk_create(detail_list)
  451. BatchOperateLogModel.objects.bulk_create(log_list)
  452. except Exception as e:
  453. raise Exception(f"批次数据保存失败: {str(e)}")
  454. def create_bound_list(self, bill_obj):
  455. """创建出入库主单(带来源标识)"""
  456. if BoundListModel.objects.filter(relate_bill=bill_obj, is_delete=False).exists():
  457. return BoundListModel.objects.get(relate_bill=bill_obj,is_delete=False)
  458. if bill_obj.type == 1:
  459. bound_desc = f"生产入库单{bill_obj.number}"
  460. elif bill_obj.type == 2:
  461. bound_desc = f"采购入库单{bill_obj.number}"
  462. elif bill_obj.type == 4:
  463. bound_desc = f"调拨入库单{bill_obj.number}"
  464. else:
  465. bound_desc = f"其他入库单{bill_obj.number}"
  466. InboundBillOperateLog.objects.create(
  467. billId=bill_obj,
  468. log_type ='wms_audit',
  469. log_content = f"WMS创建入库单{bill_obj.number}信息,开始执行入库任务",
  470. )
  471. # 更新原始单据状态
  472. bill_obj.status = 2
  473. bill_obj.save()
  474. return BoundListModel.objects.create(
  475. bound_month=timezone.now().strftime("%Y%m"),
  476. bound_date=timezone.now().strftime("%Y-%m-%d"),
  477. bound_code=bill_obj.number,
  478. bound_code_type=bill_obj.type,
  479. bound_bs_type='B01',
  480. bound_type='in',
  481. bound_desc=bound_desc,
  482. bound_department=(bill_obj.department if bill_obj.department else 'D99'),
  483. base_type=0,
  484. bound_status='101',
  485. creater=bill_obj.creater,
  486. openid='ERP',
  487. relate_bill=bill_obj
  488. )
  489. def handle_exception(self, exc):
  490. """统一异常处理"""
  491. if isinstance(exc, InboundBill.DoesNotExist):
  492. return Response({"error": "原始单据不存在或已被删除"}, status=status.HTTP_404_NOT_FOUND)
  493. elif "重复" in str(exc):
  494. return Response({"error": str(exc)}, status=status.HTTP_409_CONFLICT)
  495. return super().handle_exception(exc)
  496. class FindExistingBatch(APIView):
  497. """
  498. 预查询是否存在可关联的离线批次
  499. 匹配策略:
  500. 1. 标准化批次号完全匹配
  501. 2. 相同物料代码
  502. 3. 批次未关联其他物料
  503. 4. 批次状态有效
  504. """
  505. def post(self, request):
  506. """
  507. 预查询是否存在可关联的离线批次
  508. 匹配策略:
  509. 1. 标准化批次号完全匹配
  510. 2. 相同物料代码
  511. 3. 批次未关联其他物料
  512. 4. 批次状态有效
  513. """
  514. # 标准化批次号处理
  515. try:
  516. bill_id = request.data.get('billId')
  517. InboundBill_obj = InboundBill.objects.get(billId=bill_id, is_delete=False)
  518. if not InboundBill_obj:
  519. raise InboundBill.DoesNotExist(f"原始单据不存在或已被删除")
  520. MaterialDetail_obj_all = MaterialDetail.objects.filter(bound_billId=InboundBill_obj, is_delete=False).all()
  521. if not MaterialDetail_obj_all:
  522. raise Exception("入库单没有有效物料明细")
  523. # 拆分生产批次(物料代码-批次号)
  524. material_map = {}
  525. for material in MaterialDetail_obj_all:
  526. parts = material.production_batch.split('-')
  527. if len(parts) < 2:
  528. raise ValueError(f"无效的生产批次格式: {material.production_batch}")
  529. material_goods_code = parts[0]
  530. batch_str = parts[1]
  531. # 标准化批次号处理
  532. if len(batch_str) == 7: # 2508001 格式
  533. # 添加"20"前缀转为9位格式: 202508001
  534. standardized_batch = "20" + batch_str
  535. elif len(batch_str) == 9: # 202508001 格式
  536. standardized_batch = batch_str
  537. else:
  538. raise ValueError(f"无效的批次号长度: {batch_str}")
  539. # 创建规范化的bound_number(物料代码-标准批次号)
  540. bound_number = f"{material_goods_code}-{standardized_batch}"
  541. material.material_goods_code = material_goods_code
  542. material.material_batch_order = standardized_batch
  543. material.save(update_fields=['material_goods_code','material_batch_order'])
  544. if BoundBatchModel.objects.filter(bound_number=bound_number, is_delete=False).exists():
  545. material_map[bound_number] = True
  546. else:
  547. material_map[bound_number] = False
  548. return_data= material_map
  549. return Response({
  550. "code": 200,
  551. "data": return_data
  552. }, status=status.HTTP_200_OK)
  553. except (IndexError, ValueError) as e:
  554. raise Exception(f"物料批次处理错误: {e}")
  555. def handle_exception(self, exc):
  556. """统一异常处理"""
  557. if isinstance(exc, InboundBill.DoesNotExist):
  558. return Response({"error": "原始单据不存在或已被删除"}, status=status.HTTP_404_NOT_FOUND)
  559. elif "重复" in str(exc):
  560. return Response({"error": str(exc)}, status=status.HTTP_409_CONFLICT)
  561. return super().handle_exception(exc)
  562. """出库单生成"""
  563. class GenerateOutbound(APIView):
  564. """
  565. 生产出库单生成接口
  566. 功能特性:
  567. 1. 防重复创建校验(状态校验+关联单据校验)
  568. 2. 事务级数据一致性保障
  569. 3. 批量操作优化
  570. 4. 完整日志追踪
  571. """
  572. def post(self, request):
  573. try:
  574. bill_id = request.data.get('billId')
  575. if not bill_id:
  576. return Response({"error": "缺少必要参数: billId"},
  577. status=status.HTTP_400_BAD_REQUEST)
  578. # 开启原子事务
  579. with transaction.atomic():
  580. # 锁定原始单据并校验
  581. bill_obj, bound_list = self.validate_and_lock(bill_id)
  582. # 创建出库主单
  583. bound_list = self.create_bound_list(bill_obj)
  584. logger.info(f"创建出入库主单成功 | bound_code: {bound_list.bound_code}")
  585. # 处理物料明细(批量操作优化)
  586. self.process_materials(bill_obj, bound_list)
  587. # 更新原始单据状态
  588. bill_obj.bound_status = 1
  589. bill_obj.save(update_fields=['bound_status'])
  590. logger.info(f"出库单生成成功 | billId: {bill_id} -> boundCode: {bound_list.bound_code}")
  591. return Response({
  592. "code": 200,
  593. "count": 1,
  594. "next": "null",
  595. "previous": "null",
  596. "results":{
  597. "bound_code": bound_list.bound_code,
  598. "batch_count": bill_obj.totalCount
  599. }
  600. }, status=status.HTTP_200_OK)
  601. except InboundBill.DoesNotExist:
  602. logger.error(f"原始单据不存在 | billId: {bill_id}")
  603. return Response({
  604. "code": 404,
  605. "error": "原始单据不存在"
  606. }, status=status.HTTP_404_NOT_FOUND)
  607. except Exception as e:
  608. logger.exception(f"出库单生成异常 | billId: {bill_id}")
  609. return Response({
  610. "code": 400,
  611. "error": str(e)
  612. }, status=status.HTTP_200_OK)
  613. def validate_and_lock(self, bill_id):
  614. """验证并锁定相关资源"""
  615. # 锁定原始单据
  616. bill_obj = OutboundBill.objects.get(
  617. billId=bill_id,
  618. is_delete=False
  619. )
  620. logger.info(f"锁定原始单据成功 | billId: {bill_id}")
  621. logger.info(f"原始单据状态: {bill_obj.bound_status}")
  622. # 状态校验
  623. if bill_obj.bound_status == 1:
  624. logger.warning(f"单据已生成过出库单 | status: {bill_obj.bound_status}")
  625. raise Exception("该单据已生成过出库单")
  626. # 关联单据校验(双重校验机制)
  627. existing_bound = BoundListModel.objects.filter(
  628. Q(bound_desc__contains=f"生产出库单{bill_obj.number}") |
  629. Q(relate_out_bill=bill_obj),
  630. is_delete=False
  631. ).first()
  632. if existing_bound:
  633. logger.warning(f"发现重复关联单据 | existingCode: {existing_bound.bound_code}")
  634. raise Exception(f"已存在关联出库单[{existing_bound.bound_code}]")
  635. return bill_obj, None
  636. def process_materials(self, bill_obj, bound_list):
  637. """批量处理物料明细 - 出库版本"""
  638. materials = OutMaterialDetail.objects.filter(
  639. bound_billId=bill_obj,
  640. is_delete=False
  641. ).select_related('bound_billId', 'Material_entryIds')
  642. if not materials:
  643. raise Exception("出库单没有有效物料明细")
  644. # 批量创建对象列表
  645. batch_list = []
  646. detail_list = []
  647. log_list = []
  648. for material in materials:
  649. # 直接使用关联的入库明细,避免额外查询
  650. try:
  651. # 拆分生产批次(物料代码-批次号)
  652. parts = material.production_batch.split('-')
  653. if len(parts) < 2:
  654. raise ValueError(f"无效的生产批次格式: {material.production_batch}")
  655. material_goods_code = parts[0]
  656. batch_str = parts[1]
  657. # 标准化批次号处理
  658. if len(batch_str) == 7: # 2508001 格式
  659. # 添加"20"前缀转为9位格式: 202508001
  660. standardized_batch = "20" + batch_str
  661. elif len(batch_str) == 9: # 202508001 格式
  662. standardized_batch = batch_str
  663. else:
  664. raise ValueError(f"无效的批次号长度: {batch_str}")
  665. # 创建规范化的bound_number(物料代码-标准批次号)
  666. bound_number = f"{material_goods_code}-{standardized_batch}"
  667. material.material_goods_code = material_goods_code
  668. material.material_batch_order = standardized_batch
  669. material.save(update_fields=['material_goods_code','material_batch_order'])
  670. except (IndexError, ValueError) as e:
  671. raise Exception(f"物料批次处理错误: {e}")
  672. # 获取关联的入库批次
  673. try:
  674. batch_obj = BoundBatchModel.objects.get(goods_code = material.material_goods_code, bound_batch_order = material.material_batch_order)
  675. except BoundBatchModel.DoesNotExist:
  676. raise Exception(f"入库物料ID:{material.material_goods_code}-{ material.material_batch_order}构成的批次未找到关联批次记录")
  677. except BoundBatchModel.MultipleObjectsReturned:
  678. raise Exception(f"入库物料ID:{material.material_goods_code}-{ material.material_batch_order}构成的批次关联多个批次记录")
  679. # 计算重量 - 使用入库单件的重量进行计算
  680. unit_weight = batch_obj.goods_weight or 1 # 避免除零错误
  681. out_qty = material.goods_out_qty
  682. from decimal import Decimal
  683. total_weight = Decimal(unit_weight) * Decimal(out_qty)
  684. # 创建出库批次记录
  685. batch = OutBatchModel(
  686. out_number=batch_obj.bound_number, # 使用标准化的入库批次号
  687. batch_number=batch_obj,
  688. out_date=timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
  689. out_type=bill_obj.type,
  690. out_note=bill_obj.note,
  691. warehouse_code='W01',
  692. warehouse_name='立体仓',
  693. goods_code=batch_obj.goods_code,
  694. goods_desc=batch_obj.goods_desc,
  695. goods_std=batch_obj.goods_std or '', # 处理空值
  696. goods_unit=batch_obj.goods_unit,
  697. goods_qty=batch_obj.goods_qty, # 批次总量
  698. goods_out_qty=out_qty, # 本次出库量
  699. status=0,
  700. container_number=0, # 初始化为空字符串
  701. goods_weight=unit_weight, # 使用实际重量
  702. goods_total_weight=total_weight, # 计算总重量
  703. creater=bill_obj.creater,
  704. openid='ERP', # 使用单据中的openid
  705. relate_material=material,
  706. bound_list = bound_list,
  707. )
  708. batch_list.append(batch)
  709. # 生成出库明细记录
  710. detail_list.append(OutBoundDetailModel(
  711. bound_list=bound_list,
  712. bound_batch=batch,
  713. bound_batch_number=batch_obj,
  714. detail_code=f"{batch_obj.bound_number}-ODET-{material.id}",
  715. creater=bound_list.creater,
  716. openid=bound_list.openid
  717. ))
  718. # 生成批次操作日志
  719. log_list.append(BatchOperateLogModel(
  720. batch_id=batch_obj,
  721. log_type=1,
  722. log_date=timezone.now(),
  723. goods_code=batch_obj.goods_code,
  724. goods_desc=batch_obj.goods_desc,
  725. goods_qty=-out_qty, # 出库为负值,表示减少库存
  726. log_content=f"生产出库批次创建,来源单据:{bill_obj.number},出库数量:{out_qty}",
  727. creater=batch.creater,
  728. openid=batch.openid
  729. ))
  730. # 批量写入数据库(使用事务保证一致性)
  731. try:
  732. with transaction.atomic():
  733. OutBatchModel.objects.bulk_create(batch_list)
  734. OutBoundDetailModel.objects.bulk_create(detail_list)
  735. BatchOperateLogModel.objects.bulk_create(log_list)
  736. except Exception as e:
  737. raise Exception(f"出库批次数据保存失败: {str(e)}")
  738. def create_bound_list(self, bill_obj):
  739. """创建出入库主单(带来源标识)"""
  740. if BoundListModel.objects.filter(relate_out_bill=bill_obj,is_delete=False).exists():
  741. return BoundListModel.objects.get(relate_out_bill=bill_obj)
  742. if bill_obj.type == 1:
  743. bound_desc = f"销售出库单{bill_obj.number}"
  744. elif bill_obj.type == 2:
  745. bound_desc = f"生产领料单{bill_obj.number}"
  746. elif bill_obj.type == 4:
  747. bound_desc = f"调拨出库单{bill_obj.number}"
  748. else:
  749. bound_desc = f"其他出库单{bill_obj.number}"
  750. OutboundBillOperateLog.objects.create(
  751. billId=bill_obj,
  752. log_type ='wms_audit',
  753. log_content = f"WMS创建出库单{bill_obj.number}信息,开始执行出库任务",
  754. )
  755. # 更新原始单据状态
  756. bill_obj.status = 2
  757. bill_obj.save()
  758. return BoundListModel.objects.create(
  759. bound_month=timezone.now().strftime("%Y%m"),
  760. bound_date=timezone.now().strftime("%Y-%m-%d"),
  761. bound_code=bill_obj.number,
  762. bound_code_type=bill_obj.type,
  763. bound_bs_type='B01',
  764. bound_type='out',
  765. bound_desc=bound_desc,
  766. bound_department=(bill_obj.department if bill_obj.department else 'D99'),
  767. base_type=1,
  768. bound_status='201',
  769. creater=bill_obj.creater,
  770. openid='ERP',
  771. relate_out_bill=bill_obj
  772. )
  773. def handle_exception(self, exc):
  774. """统一异常处理"""
  775. if isinstance(exc, OutboundBill.DoesNotExist):
  776. return Response({"error": "原始单据不存在或已被删除"}, status=status.HTTP_404_NOT_FOUND)
  777. elif "重复" in str(exc):
  778. return Response({"error": str(exc)}, status=status.HTTP_409_CONFLICT)
  779. return super().handle_exception(exc)
  780. """出库申请"""
  781. class OutboundApplyCreate(APIView):
  782. """
  783. 生产出库申请
  784. """
  785. authentication_classes = []
  786. permission_classes = [AllowAny]
  787. def post(self, request):
  788. logger.info('出库申请请求 | 原始数据: %s', request.data)
  789. try:
  790. total_count = len(request.data.get('materials', []))
  791. if total_count == 0 :
  792. return WMSResponse.error(
  793. message="物料清单不能为空",
  794. total=0,
  795. fail_count=0
  796. )
  797. if total_count != request.data.get('totalCount', 0):
  798. return WMSResponse.error(
  799. message="物料数量不匹配",
  800. total=total_count,
  801. fail_count=total_count
  802. )
  803. serializer = outboundPostSerializer(data=request.data)
  804. if not serializer.is_valid():
  805. print("出错",serializer.errors)
  806. return self._handle_validation_error(serializer.errors, total_count)
  807. unique_result,error_details = self.find_unique_billid_and_number(serializer)
  808. if not unique_result:
  809. return WMSResponse.error(
  810. message="单据编号或原始单据ID重复",
  811. total=total_count,
  812. fail_count=total_count,
  813. fail_materials=[{
  814. "entryIds": None,
  815. "production_batch": None,
  816. "errors": error_details
  817. }]
  818. )
  819. # 保存或更新出库单
  820. bound_bill = self.save_or_update_outbound_bill(serializer)
  821. # 保存或更新物料明细
  822. self.save_or_update_material_detail(bound_bill, serializer)
  823. return WMSResponse.success(
  824. data=serializer.data,
  825. total=total_count,
  826. success=total_count
  827. )
  828. except Exception as e:
  829. logger.exception("服务器内部错误")
  830. return WMSResponse.error(
  831. message="系统处理异常",
  832. total=total_count,
  833. fail_count=total_count,
  834. status=status.HTTP_500_INTERNAL_SERVER_ERROR,
  835. exception=e
  836. )
  837. def _handle_validation_error(self, errors, total_count):
  838. """增强错误解析"""
  839. fail_materials = []
  840. # 提取嵌套错误信息
  841. material_errors = errors.get('materials', [])
  842. for error in material_errors:
  843. # 解析DRF的错误结构
  844. if isinstance(error, dict) and 'metadata' in error:
  845. fail_materials.append({
  846. "entryIds": error['metadata'].get('entryIds'),
  847. "production_batch": error['metadata'].get('production_batch'),
  848. "errors": {
  849. "missing_fields": error['metadata']['missing_fields'],
  850. "message": error['detail']
  851. }
  852. })
  853. return WMSResponse.error(
  854. message="物料数据不完整",
  855. total=total_count,
  856. fail_count=len(fail_materials),
  857. fail_materials=fail_materials)
  858. def _format_material_errors(self, error_dict):
  859. """格式化单个物料的错误信息"""
  860. return {
  861. field: details[0] if isinstance(details, list) else details
  862. for field, details in error_dict.items()
  863. }
  864. def find_unique_billid_and_number(self, serializer):
  865. """增强版唯一性验证"""
  866. bill_id = serializer.validated_data['billId']
  867. number = serializer.validated_data['number']
  868. # 使用Q对象进行联合查询
  869. duplicates_id = OutboundBill.objects.filter(
  870. Q(billId=bill_id)
  871. ).only('billId')
  872. # 使用Q对象进行联合查询
  873. duplicates_nu = OutboundBill.objects.filter(
  874. Q(number=number)
  875. ).only( 'number')
  876. error_details = {}
  877. # 检查单据编号重复
  878. if any(obj.billId != bill_id for obj in duplicates_nu):
  879. error_details['number'] = ["number出库单编码已存在,但是系统中与之前出库单单据ID不一致,请检查"]
  880. # 检查业务编号重复
  881. if any(obj.number != number for obj in duplicates_id):
  882. error_details['billId'] = ["billId出库库单单据ID已存在,但是系统中与之前出库申请单编码不一致,请检查"]
  883. if error_details:
  884. return False,error_details
  885. return True,None
  886. def audit_save_status(self, bound_bill_type,base_type):
  887. # 根据不同入库单类型和不同出入库类型,返回审核状态和保存状态
  888. # bound_bill_type:表示不同出入库类型 :1(生产入库申请-生产汇报单)2(采购入库申请-收料通知单) 3(其他入库)4(调拨入库) 1(销售出库申请-发货通知单) 2(生产领料)3(其他出库)
  889. # 1,1 生产入库-> 0,9 (0为未审核,9为无需保存)
  890. # 2,1 采购入库-> 0,0 (0为未审核,0为未保存)
  891. # 3,1 其他入库-> 0,9 (0为未审核,9为无需保存)
  892. # 4,1 调拨入库-> 0,9 (0为未审核,9为无需保存)
  893. # 1,2 销售出库-> 9,0 (9为无需审核,0为未保存)
  894. # 2,2 生产领料-> 0,9 (0为未审核,9为无需保存)
  895. # 3,2 其他出库-> 0,9 (0为未审核,9为无需保存)
  896. # base_type:表示不同入库单类型 1 为入库 2为出库
  897. if base_type == 1: # 入库
  898. if bound_bill_type == 1: # 生产入库
  899. return 0, 9
  900. elif bound_bill_type == 2: # 采购入库
  901. return 0, 0
  902. elif bound_bill_type == 3: # 其他入库
  903. return 0, 9
  904. elif bound_bill_type == 4: # 调拨入库
  905. return 0, 9
  906. else: # 出库
  907. if bound_bill_type == 1: # 销售出库
  908. return 9, 0
  909. elif bound_bill_type == 2: # 生产领料
  910. return 0, 9
  911. elif bound_bill_type == 3: # 其他出库
  912. return 0, 9
  913. return 0, 0
  914. def save_or_update_outbound_bill(self, serializer):
  915. """保存或更新出库单"""
  916. try:
  917. bound_bill = OutboundBill.objects.get(billId=serializer.validated_data['billId'])
  918. bound_bill.number = serializer.validated_data['number']
  919. bound_bill.type = serializer.validated_data['type']
  920. bound_bill.date = serializer.validated_data['date']
  921. bound_bill.department = (serializer.validated_data['department'] if 'department' in serializer.validated_data else '')
  922. bound_bill.warehouse = serializer.validated_data['warehouse']
  923. bound_bill.creater = serializer.validated_data['creater']
  924. bound_bill.note = (serializer.validated_data['note'] if 'note' in serializer.validated_data else '')
  925. bound_bill.totalCount = serializer.validated_data['totalCount']
  926. bound_bill.erp_audit_id = bound_bill.number
  927. bound_bill.erp_save_id = bound_bill.billId
  928. bound_bill.audit_status ,bound_bill.save_status = self.audit_save_status(bound_bill.type,2)
  929. bound_bill.update_time = timezone.now()
  930. bound_bill.is_delete = False
  931. bound_bill.save()
  932. OutboundBillOperateLog.objects.create(
  933. billId=bound_bill,
  934. log_type ='update',
  935. log_content = f"ERP更新出库单{bound_bill.number}信息",
  936. )
  937. except OutboundBill.DoesNotExist:
  938. audit_status, save_status = self.audit_save_status(serializer.validated_data['type'], 2)
  939. bound_bill = OutboundBill.objects.create(
  940. billId=serializer.validated_data['billId'],
  941. number=serializer.validated_data['number'],
  942. type=serializer.validated_data['type'],
  943. date=serializer.validated_data['date'],
  944. department=(serializer.validated_data['department'] if 'department' in serializer.validated_data else ''),
  945. warehouse=serializer.validated_data['warehouse'],
  946. creater=serializer.validated_data['creater'],
  947. note=(serializer.validated_data['note'] if 'note' in serializer.validated_data else ''),
  948. totalCount=serializer.validated_data['totalCount'],
  949. erp_audit_id = serializer.validated_data['number'],
  950. erp_save_id = serializer.validated_data['billId'],
  951. audit_status = audit_status,
  952. save_status = save_status,
  953. create_time=timezone.now(),
  954. update_time=timezone.now()
  955. )
  956. OutboundBillOperateLog.objects.create(
  957. billId=bound_bill,
  958. log_type ='create',
  959. log_content = f"ERP创建出库单{bound_bill.number}信息",
  960. )
  961. return bound_bill
  962. def save_or_update_material_detail(self, bound_bill, serializer):
  963. """保存或更新物料明细"""
  964. for item in serializer.validated_data['materials']:
  965. try:
  966. material_detail = OutMaterialDetail.objects.get(bound_billId=bound_bill, entryIds=item['entryIds'])
  967. Material_entryIds = MaterialDetail.objects.filter(
  968. goods_code = item['goods_code'],
  969. production_batch = item['production_batch']
  970. ).first()
  971. if not Material_entryIds:
  972. logger.info("出库单号%s,更新——物料明细不存在",bound_bill.number)
  973. material_detail.Material_entryIds = Material_entryIds
  974. material_detail.production_batch = item['production_batch']
  975. material_detail.goods_code = item['goods_code']
  976. material_detail.goods_name = item['goods_name']
  977. material_detail.goods_out_qty = item['goods_out_qty']
  978. material_detail.goods_total_weight = item['goods_out_qty']
  979. material_detail.goods_unit = item['goods_unit']
  980. material_detail.note = (item['note'] if 'note' in item else '')
  981. material_detail.update_time = timezone.now()
  982. material_detail.is_delete = False
  983. material_detail.save()
  984. except OutMaterialDetail.DoesNotExist:
  985. Material_entryIds = MaterialDetail.objects.filter(
  986. goods_code = item['goods_code'],
  987. production_batch = item['production_batch']
  988. ).first()
  989. if not Material_entryIds:
  990. logger.info("出库单号%s,创建——物料明细不存在",bound_bill.number)
  991. material_detail = OutMaterialDetail.objects.create(
  992. bound_billId=bound_bill,
  993. entryIds=item['entryIds'],
  994. Material_entryIds=Material_entryIds,
  995. production_batch=item['production_batch'],
  996. goods_code=item['goods_code'],
  997. goods_name=item['goods_name'],
  998. goods_weight=1,
  999. goods_out_qty=item['goods_out_qty'],
  1000. goods_total_weight=item['goods_out_qty'],
  1001. goods_unit=item['goods_unit'],
  1002. note=(item['note'] if 'note' in item else ''),
  1003. create_time=timezone.now(),
  1004. update_time=timezone.now()
  1005. )
  1006. return material_detail
  1007. """产品信息"""
  1008. class ProductInfo(APIView):
  1009. """
  1010. 批次信息更新
  1011. """
  1012. authentication_classes = [] # 禁用所有认证类
  1013. permission_classes = [AllowAny] # 允许任意访问
  1014. # parser_classes = [TextJSONParser] # 强制使用 text/json
  1015. # renderer_classes = [TextJSONRenderer] # 强制使用 text/json
  1016. def post(self, request):
  1017. data = request.data
  1018. logger.info('批次信息更新 | 原始数据: %s', data)
  1019. total_count = data.get('totalCount', 0)
  1020. materials = data.get('materials', [])
  1021. success_count = 0
  1022. fail_count = 0
  1023. fail_materials = []
  1024. try:
  1025. with transaction.atomic(): # 开启事务确保原子性
  1026. # 预查询已存在的产品ID
  1027. existing_ids = set(ProductListModel.objects.filter(
  1028. id__in=[m.get('id') for m in materials if m.get('id')]
  1029. ).values_list('id', flat=True))
  1030. for material in materials:
  1031. material_id = material.get('id')
  1032. try:
  1033. if material_id and material_id in existing_ids:
  1034. instance = ProductListModel.objects.get(id=material_id)
  1035. created = False
  1036. else:
  1037. instance = ProductListModel()
  1038. created = True
  1039. # 字段映射与校验
  1040. instance.id = material_id
  1041. instance.product_code = material.get('product_code', instance.product_code)
  1042. instance.product_name = material.get('product_name', instance.product_name)
  1043. instance.product_std = material.get('product_std', instance.product_std or 'on std')
  1044. instance.product_unit = material.get('product_unit', instance.product_unit or 'KG')
  1045. # 必填字段校验[7](@ref)
  1046. required_fields = ['product_code', 'product_name']
  1047. if any(not getattr(instance, field) for field in required_fields):
  1048. raise ValueError(f"Missing required fields: {required_fields}")
  1049. instance.is_delete = False # 强制重置删除标记[1](@ref)
  1050. instance.full_clean() # 触发模型验证[3](@ref)
  1051. instance.save()
  1052. success_count += 1
  1053. except Exception as e:
  1054. fail_count += 1
  1055. error_msg = f"{type(e).__name__}: {str(e)}"
  1056. logger.warning(f"Material processing failed: {material} | Error: {error_msg}")
  1057. fail_materials.append({
  1058. **material,
  1059. "error": error_msg
  1060. })
  1061. # 检查总数一致性[7](@ref)
  1062. if success_count != total_count:
  1063. return WMSResponse.error(
  1064. message="物料数量不匹配",
  1065. total=total_count,
  1066. fail_count=total_count,
  1067. fail_materials=fail_materials
  1068. )
  1069. except Exception as e:
  1070. logger.error(f"Batch update transaction failed: {str(e)}", exc_info=True)
  1071. return WMSResponse.error(
  1072. message=f"批量处理失败: {str(e)}",
  1073. total=total_count,
  1074. fail_count=fail_count,
  1075. fail_materials=fail_materials,
  1076. exception=e
  1077. )
  1078. return WMSResponse.success(
  1079. data=[], # 根据需求可返回处理后的数据
  1080. total=total_count,
  1081. success=success_count
  1082. )
  1083. """批次更新"""
  1084. class BatchUpdate(APIView):
  1085. """
  1086. 商品信息查询
  1087. """
  1088. authentication_classes = [] # 禁用所有认证类
  1089. permission_classes = [AllowAny] # 允许任意访问
  1090. def post(self, request):
  1091. data = request.data
  1092. total_count = data.get('totalCount', 0)
  1093. material_audit_id = data.get('billnos')
  1094. materials = data.get('materials', [])
  1095. success_count = 0
  1096. fail_count = 0
  1097. fail_materials = []
  1098. try:
  1099. with transaction.atomic(): # 开启事务确保原子性
  1100. if total_count != len(materials):
  1101. return WMSResponse.error(
  1102. message="物料数量不匹配",
  1103. total=total_count,
  1104. fail_count=total_count
  1105. )
  1106. for material in materials:
  1107. material_billId = material.get('billId')
  1108. material_entryId = material.get('entryIds')
  1109. material_status = material.get('status')
  1110. if material_status == 'passing':
  1111. try:
  1112. bill_obj = InboundBill.objects.get(billId=material_billId)
  1113. if not bill_obj:
  1114. logger.info("入库单不存在")
  1115. fail_materials.append({
  1116. **material,
  1117. "error": "入库单不存在"
  1118. })
  1119. fail_count += 1
  1120. continue
  1121. bill_obj.erp_audit_id = material_audit_id
  1122. instance = MaterialDetail.objects.get(bound_billId_id=material_billId,entryIds=material_entryId)
  1123. if not instance:
  1124. logger.info("物料明细不存在")
  1125. fail_materials.append({
  1126. **material,
  1127. "error": "物料明细不存在"
  1128. })
  1129. fail_count += 1
  1130. continue
  1131. if instance.status == 0:
  1132. InboundBillOperateLog.objects.create(
  1133. billId=bill_obj,
  1134. log_type ='update_batch',
  1135. log_content = f"物料明细{instance.goods_name},质检通过",
  1136. )
  1137. instance.status = 1
  1138. logger.info("[1]入库单号%s,物料明细%s,更新状态,审核通过%s",bill_obj.number,material_entryId,material_audit_id)
  1139. logger.info("[2]入库单号%s,物料明细%s,更新状态,审核通过%s",bill_obj.number,material_entryId,bill_obj.erp_audit_id)
  1140. instance.save()
  1141. materials_instance = Materials()
  1142. materials_instance._check_status_operation(instance)
  1143. bill_obj.save()
  1144. success_count += 1
  1145. except Exception as e:
  1146. fail_count += 1
  1147. error_msg = f"{type(e).__name__}: {str(e)}"
  1148. logger.warning(f"Material processing failed: {material} | Error: {error_msg}")
  1149. fail_materials.append({
  1150. **material,
  1151. "error": error_msg
  1152. })
  1153. # 检查总数一致性[7](@ref)
  1154. if success_count != total_count:
  1155. return WMSResponse.error(
  1156. message="单据编码与详细编码不匹配",
  1157. total=total_count,
  1158. fail_count=total_count,
  1159. fail_materials=fail_materials
  1160. )
  1161. except Exception as e:
  1162. logger.error(f"Batch update transaction failed: {str(e)}", exc_info=True)
  1163. return WMSResponse.error(
  1164. message=f"批量处理失败: {str(e)}",
  1165. total=total_count,
  1166. fail_count=fail_count,
  1167. fail_materials=fail_materials,
  1168. exception=e
  1169. )
  1170. return WMSResponse.success(
  1171. data=[],
  1172. total=total_count,
  1173. success=success_count
  1174. )
  1175. """token"""
  1176. class AccessToken(APIView):
  1177. """
  1178. 获取ERP的access_token
  1179. 方法:post到
  1180. https://okyy.test.kdgalaxy.com/kapi/oauth2/getToken
  1181. 参数:
  1182. {
  1183. "client_id" : "WMS",
  1184. "client_secret" : "1Ca~2Tu-3Fx$3Rg@",
  1185. "username" : "xs",
  1186. "accountId" : "2154719510106474496",
  1187. "nonce" : "2025-04-27 11:36:08",
  1188. "timestamp" : "2025-04-27 11:36:08",
  1189. "language" : "zh_CN"
  1190. }
  1191. 返回:
  1192. {
  1193. "data": {
  1194. "access_token": "CanAzMDM=",
  1195. "token_type": "Bearer",
  1196. "refresh_token": "4297d40e-b2ac-48b4-98a2-b50665e6faaf",
  1197. "scope": "API",
  1198. "expires_in": 7199990,
  1199. "id_token": "d09UWXhOakV4TnpjMkE9EVGVV",
  1200. "id_token_expires_in": 7199990,
  1201. "language": "zh_CN"
  1202. },
  1203. "errorCode": "0",
  1204. "message": "",
  1205. "status": true
  1206. }
  1207. """
  1208. authentication_classes = [] # 禁用所有认证类
  1209. permission_classes = [AllowAny] # 允许任意访问
  1210. @classmethod
  1211. def get_token(cls):
  1212. try:
  1213. """获取access_token"""
  1214. # url = "https://okyy.test.kdgalaxy.com/kapi/oauth2/getToken" # 测试环境
  1215. url = "https://okyy.kdgalaxy.com/kapi/oauth2/getToken"
  1216. data = {
  1217. "client_id" : "WMS",
  1218. "client_secret" : "1Ca~2Tu-3Fx$3Rg@",
  1219. "username" : "cengjie",
  1220. "accountId" : "2154719510039370752",
  1221. "nonce" : timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
  1222. "timestamp" : timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
  1223. "language" : "zh_CN"
  1224. }
  1225. print("请求参数",data)
  1226. response = requests.post(url, json=data,timeout=10)
  1227. if response.status_code == 200:
  1228. result = response.json()
  1229. if result.get('status'):
  1230. logger.info(f"获取access_token成功 | access_token: {result.get('data',{}).get('access_token')}")
  1231. return result.get('data',{}).get('access_token')
  1232. return None
  1233. except Exception as e:
  1234. print("获取access_token异常",e)
  1235. logger.exception("获取access_token异常")
  1236. return None
  1237. @classmethod
  1238. def get_current_token(cls):
  1239. """获取当前有效Token(带缓存机制)"""
  1240. cache_key = 'erp_access_token'
  1241. cached_token = cache.get(cache_key)
  1242. if not cached_token:
  1243. new_token = cls.get_token()
  1244. if new_token:
  1245. # 缓存时间略短于实际有效期
  1246. cache.set(cache_key, new_token, timeout=1000)
  1247. return new_token
  1248. return cached_token
  1249. """基本同步业务类"""
  1250. class ERPSyncBase:
  1251. """ERP同步基类(作为发送方)"""
  1252. max_retries = 30 # 最大重试次数
  1253. retry_delay = 30
  1254. # 重试间隔秒数
  1255. def __init__(self, wms_bill,entryIds):
  1256. self.wms_bill = wms_bill # WMS单据对象
  1257. self.entryIds = entryIds # 物料明细ID数组
  1258. self.erp_id_field = None # 需要更新的ERP ID字段名
  1259. def build_erp_payload(self):
  1260. """构造ERP请求数据(需子类实现)"""
  1261. raise NotImplementedError
  1262. def get_erp_endpoint(self):
  1263. """获取ERP接口地址(需子类实现)"""
  1264. raise NotImplementedError
  1265. def process_erp_response(self, response):
  1266. """处理ERP响应(需子类实现)返回erp_id"""
  1267. raise NotImplementedError
  1268. def execute_sync(self):
  1269. """执行同步操作"""
  1270. # 测试环境 @ todo
  1271. headers = {
  1272. 'accessToken': f'{AccessToken.get_current_token()}',
  1273. # 'x-acgw-identity': 'djF8MTk2M2QzMWEzMjUwMTZlMzA3MDF8NDg5ODM4MzM4NjE3OHwYjYJyvo-DbkhOliEpFtiFOsCgKKo6braaiQGE9qdNx3w='
  1274. 'x-acgw-identity': 'djF8MTk3OGFjNmZhNmUwMTdlZjJjMDF8NDkwMzk3OTk4NjUyMHzeN7OIDKxY967-XetDtAlOwC-vhGdImLK9R3FVsm0Y-Hw='
  1275. }
  1276. for attempt in range(self.max_retries):
  1277. try:
  1278. print("请求头:",headers)
  1279. print("请求体:",self.build_erp_payload())
  1280. print("请求地址:",self.get_erp_endpoint())
  1281. response = requests.post(
  1282. self.get_erp_endpoint(),
  1283. json=self.build_erp_payload(),
  1284. headers=headers,
  1285. timeout=60
  1286. )
  1287. response.raise_for_status()
  1288. return self.process_erp_response(response.json())
  1289. except requests.exceptions.HTTPError as http_err:
  1290. if response.status_code == 519:
  1291. print("特定HTTP错误 519:", http_err)
  1292. logger.error(f"ERP接口HTTP错误 519 第{attempt+1}次重试 | 单据:{self.wms_bill.number} | 错误: {http_err}")
  1293. else:
  1294. print("HTTP错误:", http_err)
  1295. logger.error(f"ERP接口HTTP错误 第{attempt+1}次重试 | 单据:{self.wms_bill.number} | 错误: {http_err}")
  1296. time.sleep(self.retry_delay)
  1297. except requests.exceptions.RequestException as e:
  1298. print("ERP接口请求异常:",e)
  1299. print(f"ERP接口请求失败 第{attempt+1}次重试 | 单据:{self.wms_bill.number}")
  1300. logger.error(f"ERP接口请求失败 第{attempt+1}次重试 | 单据:{self.wms_bill.number}")
  1301. time.sleep(self.retry_delay)
  1302. logger.error(f"ERP同步最终失败 | 单据:{self.wms_bill.number}")
  1303. return False
  1304. # ==================== 业务接口 ====================
  1305. """生产入库审核"""
  1306. class ProductionInboundAuditSync(ERPSyncBase):
  1307. erp_id_field = 'erp_audit_id'
  1308. # 请求地址
  1309. def get_erp_endpoint(self):
  1310. # return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_mdc_mftmanuinbill/audit"
  1311. return "https://okyy.kdgalaxy.com/kapi/v2/l772/im/im_mdc_mftmanuinbill/audit"
  1312. # 请求参数
  1313. # {
  1314. # "data":{
  1315. # "billnos":[
  1316. # "AgTSC",
  1317. # "fgBAH"
  1318. # ]
  1319. # }
  1320. # }
  1321. def build_erp_payload(self):
  1322. return {
  1323. "data": {
  1324. "billnos": [
  1325. self.wms_bill.erp_audit_id,
  1326. ]
  1327. }
  1328. }
  1329. # 处理响应
  1330. def process_erp_response(self, response):
  1331. logger.info("ERP审核响应:",response)
  1332. if response['status']:
  1333. self.wms_bill.audit_status = 1
  1334. self.wms_bill.save()
  1335. return response['status']
  1336. """采购收料入库审核"""
  1337. class PurchaseInboundAuditSync(ERPSyncBase):
  1338. erp_id_field = 'erp_audit_id'
  1339. # 请求地址
  1340. def get_erp_endpoint(self):
  1341. # return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_purreceivebill/audit"
  1342. return "https://okyy.kdgalaxy.com/kapi/v2/l772/im/im_purreceivebill/audit"
  1343. # 请求参数
  1344. # {
  1345. # "data":{
  1346. # "billnos":[
  1347. # "AgTSC",
  1348. # "fgBAH"
  1349. # ]
  1350. # }
  1351. # }
  1352. def build_erp_payload(self):
  1353. return {
  1354. "data": {
  1355. "billnos": [
  1356. self.wms_bill.erp_audit_id,
  1357. ]
  1358. }
  1359. }
  1360. # 处理响应
  1361. def process_erp_response(self, response):
  1362. logger.info("ERP审核响应:",response)
  1363. if response['status']:
  1364. self.wms_bill.audit_status = 1
  1365. self.wms_bill.save()
  1366. return response['status']
  1367. """其他入库审核"""
  1368. class OtherInboundAuditSync(ERPSyncBase):
  1369. erp_id_field = 'erp_audit_id'
  1370. # 请求地址
  1371. def get_erp_endpoint(self):
  1372. # return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_otherinbill/audit"
  1373. return "https://okyy.kdgalaxy.com/kapi/v2/l772/im/im_otherinbill/audit"
  1374. # 请求参数
  1375. # {
  1376. # "data":{
  1377. # "billnos":[
  1378. # "AgTSC",
  1379. # "fgBAH"
  1380. # ]
  1381. # }
  1382. # }
  1383. def build_erp_payload(self):
  1384. return {
  1385. "data": {
  1386. "billnos": [
  1387. self.wms_bill.erp_audit_id,
  1388. ]
  1389. }
  1390. }
  1391. # 处理响应
  1392. def process_erp_response(self, response):
  1393. logger.info("ERP审核响应:",response['message'])
  1394. if response['status']:
  1395. self.wms_bill.audit_status = 1
  1396. self.wms_bill.save()
  1397. return response['status']
  1398. """调拨入库审核"""
  1399. class TransferInboundAuditSync(ERPSyncBase):
  1400. erp_id_field = 'erp_audit_id'
  1401. # 请求地址
  1402. def get_erp_endpoint(self):
  1403. # return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_transdirbill/audit"
  1404. return "https://okyy.kdgalaxy.com/kapi/v2/l772/im/im_transdirbill/audit"
  1405. # 请求参数
  1406. # {
  1407. # "data":{
  1408. # "billnos":[
  1409. # "AgTSC",
  1410. # "fgBAH"
  1411. # ]
  1412. # }
  1413. # }
  1414. def build_erp_payload(self):
  1415. return {
  1416. "data": {
  1417. "billnos": [
  1418. self.wms_bill.erp_audit_id,
  1419. ]
  1420. }
  1421. }
  1422. # 处理响应
  1423. def process_erp_response(self, response):
  1424. logger.info("ERP审核响应:",response)
  1425. if response['status']:
  1426. self.wms_bill.audit_status = 1
  1427. self.wms_bill.save()
  1428. return response['status']
  1429. """其他出库审核"""
  1430. class OtherOutboundAuditSync(ERPSyncBase):
  1431. erp_id_field = 'erp_audit_id'
  1432. # 请求地址
  1433. def get_erp_endpoint(self):
  1434. return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_otheroutbill/audit"
  1435. # 请求参数
  1436. # {
  1437. # "data":{
  1438. # "billnos":[
  1439. # "AgTSC",
  1440. # "fgBAH"
  1441. # ]
  1442. # }
  1443. # }
  1444. def build_erp_payload(self):
  1445. return {
  1446. "data": {
  1447. "billnos": [
  1448. self.wms_bill.erp_audit_id,
  1449. ]
  1450. }
  1451. }
  1452. # 处理响应
  1453. def process_erp_response(self, response):
  1454. logger.info("ERP审核响应:",response)
  1455. if response['status']:
  1456. self.wms_bill.audit_status = 1
  1457. self.wms_bill.save()
  1458. return response['status']
  1459. """生产领料出库审核"""
  1460. class ProductionOutboundAuditSync(ERPSyncBase):
  1461. erp_id_field = 'erp_audit_id'
  1462. # 请求地址
  1463. def get_erp_endpoint(self):
  1464. # return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_mdc_mftproorder/audit"
  1465. return "https://okyy.kdgalaxy.com/kapi/v2/l772/im/im_mdc_mftproorder/audit"
  1466. # 请求参数
  1467. # {
  1468. # "data":{
  1469. # "billnos":[
  1470. # "AgTSC",
  1471. # "fgBAH"
  1472. # ]
  1473. # }
  1474. # }
  1475. def build_erp_payload(self):
  1476. return {
  1477. "data": {
  1478. "billnos": [
  1479. self.wms_bill.erp_audit_id,
  1480. ]
  1481. }
  1482. }
  1483. # 处理响应
  1484. def process_erp_response(self, response):
  1485. logger.info("ERP审核响应:",response['message'],response['status'])
  1486. if response['status']:
  1487. self.wms_bill.audit_status = 1
  1488. self.wms_bill.save()
  1489. return response['status']
  1490. """采购入库保存"""
  1491. class PurchaseInboundSaveSync(ERPSyncBase):
  1492. erp_id_field = 'erp_save_id'
  1493. def get_erp_endpoint(self):
  1494. # return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_purinbill/save"
  1495. return "https://okyy.kdgalaxy.com/kapi/v2/l772/im/im_purinbill/save"
  1496. def build_erp_payload(self):
  1497. # {
  1498. # "purinbill":{
  1499. # "billId":"1745732021174",
  1500. # "entryIds":[
  1501. # "1745732021087"
  1502. # ]
  1503. # }
  1504. # }
  1505. return {
  1506. "purinbill": {
  1507. "billId":self.wms_bill.erp_save_id,
  1508. "entryIds":
  1509. self.entryIds,
  1510. }
  1511. }
  1512. def process_erp_response(self, response):
  1513. logger.info("ERP审核响应:",response['message'])
  1514. if response['status']:
  1515. InboundBillOperateLog.objects.create(
  1516. billId=self.wms_bill,
  1517. log_type ='erp_save',
  1518. log_content = f"保存入库单{self.wms_bill.number}信息",
  1519. )
  1520. self.wms_bill.save_status = 1
  1521. self.wms_bill.save()
  1522. return response['status']
  1523. """销售出库保存"""
  1524. class SaleOutboundSaveSync(ERPSyncBase):
  1525. erp_id_field = 'erp_save_id'
  1526. def get_erp_endpoint(self):
  1527. # return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_saloutbill/save"
  1528. return "https://okyy.kdgalaxy.com/kapi/v2/l772/im/im_saloutbill/save"
  1529. def build_erp_payload(self):
  1530. # {
  1531. # "saloutbill":{
  1532. # "billId":"1745732823064",
  1533. # "entryIds":[
  1534. # "1745732823073"
  1535. # ]
  1536. # }
  1537. # }
  1538. return {
  1539. "saloutbill": {
  1540. "billId":self.wms_bill.erp_save_id,
  1541. "entryIds":
  1542. self.entryIds,
  1543. }
  1544. }
  1545. def process_erp_response(self, response):
  1546. logger.info("ERP审核响应:",response)
  1547. if response['status']:
  1548. OutboundBillOperateLog.objects.create(
  1549. billId=self.wms_bill,
  1550. log_type ='erp_save',
  1551. log_content = f"保存出库单{self.wms_bill.number}信息",
  1552. )
  1553. self.wms_bill.save_status = 1
  1554. self.wms_bill.save()
  1555. return response['status']
  1556. """审核与保存接口"""
  1557. class bound_apply(APIView):
  1558. """
  1559. 入库单审核与保存
  1560. bill_base :0 入库单 1 出库单
  1561. bill_type :1 生产入库 2 采购入库 3 其他入库 4 调拨入库 2 生产出库 1 销售出库 3 其他出库
  1562. """
  1563. def post(self, request, action_type):
  1564. """统一处理 POST 请求,根据 action_type 分发逻辑"""
  1565. if action_type == 'audit':
  1566. # 调用 audit 逻辑
  1567. return self._audit(request)
  1568. elif action_type == 'save':
  1569. # 调用 save 逻辑
  1570. return self._save(request)
  1571. else:
  1572. return Response({'message': '无效操作'}, status=status.HTTP_400_BAD_REQUEST)
  1573. def _audit(self, request):
  1574. """统一审核逻辑"""
  1575. # 参数校验
  1576. if error_response := self._validate_audit_params(request.data):
  1577. return error_response
  1578. try:
  1579. # 获取审核配置
  1580. config = self._get_audit_config(request.data['billbase'], request.data['billtype'])
  1581. if not config:
  1582. return Response({'message': '无效的业务类型'}, status=status.HTTP_400_BAD_REQUEST)
  1583. # 处理审核业务
  1584. return self._process_audit(
  1585. bill_id=request.data['billid'],
  1586. config=config
  1587. )
  1588. except Exception as e:
  1589. logger.error(f"审核异常: {str(e)}", exc_info=True)
  1590. return Response({'message': '服务器处理失败'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  1591. def _validate_audit_params(self, data):
  1592. """参数验证"""
  1593. required_params = {'billid', 'billtype', 'billbase'}
  1594. if missing := [p for p in required_params if p not in data]:
  1595. return Response(
  1596. {'message': f'缺少必要参数: {", ".join(missing)}'},
  1597. status=status.HTTP_400_BAD_REQUEST
  1598. )
  1599. return None
  1600. def _get_audit_config(self, bill_base, bill_type):
  1601. """获取审核配置"""
  1602. config_map = {
  1603. # 入库单配置 (bill_base=0)
  1604. (0, 1): {'model': InboundBill, 'sync_class': ProductionInboundAuditSync, 'log_model': InboundBillOperateLog},
  1605. (0, 2): {'model': InboundBill, 'sync_class': PurchaseInboundAuditSync, 'log_model': InboundBillOperateLog},
  1606. (0, 3): {'model': InboundBill, 'sync_class': OtherInboundAuditSync, 'log_model': InboundBillOperateLog},
  1607. (0, 4): {'model': InboundBill, 'sync_class': TransferInboundAuditSync, 'log_model': InboundBillOperateLog},
  1608. # 出库单配置 (bill_base=1)
  1609. (1, 2): {'model': OutboundBill, 'sync_class': ProductionOutboundAuditSync, 'log_model': OutboundBillOperateLog},
  1610. (1, 3): {'model': OutboundBill, 'sync_class': OtherOutboundAuditSync, 'log_model': OutboundBillOperateLog}
  1611. }
  1612. return config_map.get((bill_base, bill_type))
  1613. def _process_audit(self, bill_id, config):
  1614. """执行审核流程"""
  1615. # 获取单据对象
  1616. bill_obj = config['model'].objects.filter(billId=bill_id).first()
  1617. if not bill_obj:
  1618. return Response({'message': '单据不存在'}, status=status.HTTP_404_NOT_FOUND)
  1619. # 执行同步操作
  1620. sync_result = self._execute_sync(
  1621. sync_class=config['sync_class'],
  1622. bill_obj=bill_obj,
  1623. material_ids=[]
  1624. )
  1625. if not sync_result:
  1626. return Response({'message': 'ERP审核失败'}, status=status.HTTP_404_NOT_FOUND)
  1627. # 记录操作日志
  1628. self._create_audit_log(
  1629. log_model=config['log_model'],
  1630. bill_obj=bill_obj
  1631. )
  1632. return Response({'message': '审核成功'}, status=status.HTTP_200_OK)
  1633. def _execute_sync(self, sync_class, bill_obj, material_ids):
  1634. """执行ERP同步"""
  1635. try:
  1636. return sync_class(bill_obj, material_ids).execute_sync()
  1637. except Exception as e:
  1638. logger.error(f"ERP同步失败: {str(e)}")
  1639. return False
  1640. def _create_audit_log(self, log_model, bill_obj):
  1641. """创建审核日志"""
  1642. log_model.objects.create(
  1643. billId=bill_obj,
  1644. log_type='erp_audit',
  1645. log_content='ERP审核成功'
  1646. )
  1647. def _save(self,request):
  1648. """保存"""
  1649. bill_id = request.data.get('billid', None)
  1650. bill_type = request.data.get('billtype', None)
  1651. bill_base = request.data.get('billbase', None)
  1652. if bill_id is None or bill_type is None or bill_base is None:
  1653. return Response({'message': '参数错误'}, status=status.HTTP_400_BAD_REQUEST)
  1654. try:
  1655. if bill_base == 0:
  1656. if bill_type == 2:
  1657. # 采购入库保存
  1658. bill_obj = InboundBill.objects.filter(billId=bill_id).first()
  1659. entryIds = list(MaterialDetail.objects.filter(bound_billId=bill_id).values_list('entryIds', flat=True))
  1660. if bill_obj:
  1661. logger.info(f"[单据ID]:{bill_obj.billId}")
  1662. sync_obj = PurchaseInboundSaveSync(bill_obj,entryIds)
  1663. sync_result = sync_obj.execute_sync()
  1664. return Response({'message': sync_result}, status=status.HTTP_200_OK)
  1665. else:
  1666. return Response({'message': '单据不存在'}, status=status.HTTP_400_BAD_REQUEST)
  1667. elif bill_base == 1:
  1668. if bill_type == 1:
  1669. # 销售出库保存
  1670. bill_obj = OutboundBill.objects.filter(billId=bill_id).first()
  1671. entryIds = list(OutMaterialDetail.objects.filter(bound_billId=bill_id).values_list('entryIds', flat=True))
  1672. if bill_obj:
  1673. sync_obj = SaleOutboundSaveSync(bill_obj,entryIds)
  1674. sync_result = sync_obj.execute_sync()
  1675. return Response({'message': sync_result}, status=status.HTTP_200_OK)
  1676. else:
  1677. return Response({'message': '单据不存在'}, status=status.HTTP_400_BAD_REQUEST)
  1678. else:
  1679. return Response({'message': '无需保存'}, status=status.HTTP_200_OK)
  1680. except Exception as e:
  1681. return Response({'message': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  1682. """前端视图类·ERP入库"""
  1683. class InboundBills(viewsets.ModelViewSet):
  1684. """
  1685. retrieve:
  1686. Response a data list(get)
  1687. list:
  1688. Response a data list(all)
  1689. """
  1690. # authentication_classes = [] # 禁用所有认证类
  1691. # permission_classes = [AllowAny] # 允许任意访问
  1692. pagination_class = MyPageNumberPagination
  1693. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  1694. ordering_fields = ["update_time", "create_time"]
  1695. filter_class = InboundBillFilter
  1696. def get_project(self):
  1697. # 获取项目ID,如果不存在则返回None
  1698. try:
  1699. id = self.kwargs.get('pk')
  1700. return id
  1701. except:
  1702. return None
  1703. def get_queryset(self):
  1704. # 根据请求用户过滤查询集
  1705. id = self.get_project()
  1706. if self.request.user:
  1707. if id is None:
  1708. return InboundBill.objects.filter(is_delete=False)
  1709. else:
  1710. return InboundBill.objects.filter(billId=id, is_delete=False)
  1711. else:
  1712. return InboundBill.objects.none()
  1713. def get_serializer_class(self):
  1714. # 根据操作类型选择合适的序列化器
  1715. if self.action in ['list', 'retrieve']:
  1716. return InboundApplySerializer
  1717. else:
  1718. return self.http_method_not_allowed(request=self.request)
  1719. def get_bound_number(self,request):
  1720. billid = request.data.get('billid', None)
  1721. bill_obj = InboundBill.objects.filter(billId=billid).first()
  1722. return_data = {}
  1723. if bill_obj:
  1724. return_data['bill_number'] = bill_obj.number
  1725. return_data['bill_audit'] = bill_obj.erp_audit_id
  1726. return_data['bill_save'] = bill_obj.erp_save_id
  1727. return_data['bill_status'] = bill_obj.bound_status
  1728. return_data['bill_type'] = bill_obj.type
  1729. return Response(return_data,status=status.HTTP_200_OK)
  1730. def destroy(self, request, pk):
  1731. qs = self.get_object()
  1732. bill_number = qs.number
  1733. bill_id = qs.billId
  1734. try:
  1735. qs.is_delete = True
  1736. MaterialDetail.objects.filter(bound_billId=qs).update(is_delete=True)
  1737. qs.save()
  1738. # 记录成功日志
  1739. try:
  1740. log_success_operation(
  1741. request=request,
  1742. operation_content=f"删除入库单 ID:{pk},单据编号: {bill_number}",
  1743. operation_level="delete",
  1744. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  1745. object_id=pk,
  1746. module_name="ERP入库管理"
  1747. )
  1748. except Exception as e:
  1749. pass
  1750. return Response({'message': '删除成功'}, status=status.HTTP_200_OK)
  1751. except Exception as e:
  1752. # 记录失败日志
  1753. try:
  1754. log_failure_operation(
  1755. request=request,
  1756. operation_content=f"删除入库单失败 ID:{pk},单据编号: {bill_number},错误: {str(e)}",
  1757. operation_level="delete",
  1758. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  1759. object_id=pk,
  1760. module_name="ERP入库管理"
  1761. )
  1762. except Exception as log_error:
  1763. pass
  1764. raise
  1765. def update_inbound(self, request, *args, **kwargs):
  1766. id = self.get_project()
  1767. if id is None:
  1768. return Response({'message': '没找到数据', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
  1769. else:
  1770. bill_obj = InboundBill.objects.filter(billId=id).first()
  1771. if bill_obj:
  1772. try:
  1773. serializer = InboundApplyPOSTSerializer(bill_obj, data=request.data)
  1774. if serializer.is_valid():
  1775. serializer.save()
  1776. # 记录成功日志
  1777. try:
  1778. log_success_operation(
  1779. request=request,
  1780. operation_content=f"更新入库单 ID:{id},单据编号: {bill_obj.number}",
  1781. operation_level="update",
  1782. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  1783. object_id=bill_obj.id if hasattr(bill_obj, 'id') else None,
  1784. module_name="ERP入库管理"
  1785. )
  1786. except Exception as e:
  1787. pass
  1788. return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
  1789. else:
  1790. # 记录失败日志(验证失败)
  1791. try:
  1792. log_failure_operation(
  1793. request=request,
  1794. operation_content=f"更新入库单失败 ID:{id},单据编号: {bill_obj.number},错误: 参数验证失败",
  1795. operation_level="update",
  1796. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  1797. object_id=bill_obj.id if hasattr(bill_obj, 'id') else None,
  1798. module_name="ERP入库管理"
  1799. )
  1800. except Exception as e:
  1801. pass
  1802. return Response({'message': '上传参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
  1803. except Exception as e:
  1804. # 记录异常日志
  1805. try:
  1806. log_failure_operation(
  1807. request=request,
  1808. operation_content=f"更新入库单异常 ID:{id},错误: {str(e)}",
  1809. operation_level="update",
  1810. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  1811. module_name="ERP入库管理"
  1812. )
  1813. except Exception as log_error:
  1814. pass
  1815. raise
  1816. else:
  1817. return Response({'message': '数据不存在', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
  1818. """前端视图类·ERP出库"""
  1819. class OutboundBills(viewsets.ModelViewSet):
  1820. """
  1821. retrieve:
  1822. Response a data list(get)
  1823. list:
  1824. Response a data list(all)
  1825. """
  1826. # authentication_classes = [] # 禁用所有认证类
  1827. # permission_classes = [AllowAny] # 允许任意访问
  1828. pagination_class = MyPageNumberPagination
  1829. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  1830. ordering_fields = ["update_time", "create_time"]
  1831. filter_class = OutboundBillFilter
  1832. def get_project(self):
  1833. # 获取项目ID,如果不存在则返回None
  1834. try:
  1835. id = self.kwargs.get('pk')
  1836. return id
  1837. except:
  1838. return None
  1839. def get_queryset(self):
  1840. # 根据请求用户过滤查询集
  1841. id = self.get_project()
  1842. if self.request.user:
  1843. if id is None:
  1844. return OutboundBill.objects.filter(is_delete=False)
  1845. else:
  1846. return OutboundBill.objects.filter(billId=id, is_delete=False)
  1847. else:
  1848. return OutboundBill.objects.none()
  1849. def get_serializer_class(self):
  1850. # 根据操作类型选择合适的序列化器
  1851. if self.action in ['list', 'retrieve', ]:
  1852. return OutboundApplySerializer
  1853. else:
  1854. return self.http_method_not_allowed(request=self.request)
  1855. def get_bound_number(self,request):
  1856. billid = request.data.get('billid', None)
  1857. bill_obj = OutboundBill.objects.filter(billId=billid).first()
  1858. return_data = {}
  1859. if bill_obj:
  1860. return_data['bill_number'] = bill_obj.number
  1861. return_data['bill_audit'] = bill_obj.erp_audit_id
  1862. return_data['bill_save'] = bill_obj.erp_save_id
  1863. return_data['bill_status'] = bill_obj.bound_status
  1864. return_data['bill_type'] = bill_obj.type
  1865. return Response(return_data,status=status.HTTP_200_OK)
  1866. def destroy(self, request, pk):
  1867. qs = self.get_object()
  1868. bill_number = qs.number
  1869. bill_id = qs.billId
  1870. try:
  1871. qs.is_delete = True
  1872. OutMaterialDetail.objects.filter(bound_billId=qs).update(is_delete=True)
  1873. qs.save()
  1874. # 记录成功日志
  1875. try:
  1876. log_success_operation(
  1877. request=request,
  1878. operation_content=f"删除出库单 ID:{pk},单据编号: {bill_number}",
  1879. operation_level="delete",
  1880. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  1881. object_id=pk,
  1882. module_name="ERP出库管理"
  1883. )
  1884. except Exception as e:
  1885. pass
  1886. return Response({'message': '删除成功'}, status=status.HTTP_200_OK)
  1887. except Exception as e:
  1888. # 记录失败日志
  1889. try:
  1890. log_failure_operation(
  1891. request=request,
  1892. operation_content=f"删除出库单失败 ID:{pk},单据编号: {bill_number},错误: {str(e)}",
  1893. operation_level="delete",
  1894. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  1895. object_id=pk,
  1896. module_name="ERP出库管理"
  1897. )
  1898. except Exception as log_error:
  1899. pass
  1900. raise
  1901. def update_outbound(self, request, *args, **kwargs):
  1902. id = self.get_project()
  1903. if id is None:
  1904. return Response({'message': '没找到数据', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
  1905. else:
  1906. bill_obj = OutboundBill.objects.filter(billId=id).first()
  1907. if bill_obj:
  1908. try:
  1909. serializer = OutboundApplyPOSTSerializer(bill_obj, data=request.data)
  1910. if serializer.is_valid():
  1911. serializer.save()
  1912. # 记录成功日志
  1913. try:
  1914. log_success_operation(
  1915. request=request,
  1916. operation_content=f"更新出库单 ID:{id},单据编号: {bill_obj.number}",
  1917. operation_level="update",
  1918. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  1919. object_id=bill_obj.id if hasattr(bill_obj, 'id') else None,
  1920. module_name="ERP出库管理"
  1921. )
  1922. except Exception as e:
  1923. pass
  1924. return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
  1925. else:
  1926. # 记录失败日志(验证失败)
  1927. try:
  1928. log_failure_operation(
  1929. request=request,
  1930. operation_content=f"更新出库单失败 ID:{id},单据编号: {bill_obj.number},错误: 参数验证失败",
  1931. operation_level="update",
  1932. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  1933. object_id=bill_obj.id if hasattr(bill_obj, 'id') else None,
  1934. module_name="ERP出库管理"
  1935. )
  1936. except Exception as e:
  1937. pass
  1938. return Response({'message': '上传参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
  1939. except Exception as e:
  1940. # 记录异常日志
  1941. try:
  1942. log_failure_operation(
  1943. request=request,
  1944. operation_content=f"更新出库单异常 ID:{id},错误: {str(e)}",
  1945. operation_level="update",
  1946. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  1947. module_name="ERP出库管理"
  1948. )
  1949. except Exception as log_error:
  1950. pass
  1951. raise
  1952. else:
  1953. return Response({'message': '数据不存在', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
  1954. class Materials(viewsets.ModelViewSet):
  1955. """
  1956. retrieve:
  1957. Response a data list(get)
  1958. list:
  1959. Response a data list(all)
  1960. """
  1961. # authentication_classes = [] # 禁用所有认证类
  1962. # permission_classes = [AllowAny] # 允许任意访问
  1963. pagination_class = MyPageNumberPagination
  1964. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  1965. ordering_fields = ['id', "create_time", "update_time", ]
  1966. filter_class = MaterialDetailFilter
  1967. def get_project(self):
  1968. try:
  1969. id = self.kwargs.get('pk')
  1970. return id
  1971. except:
  1972. return None
  1973. def get_queryset(self):
  1974. id = self.get_project()
  1975. if self.request.user:
  1976. if id is None:
  1977. return MaterialDetail.objects.filter(is_delete=False)
  1978. else:
  1979. return MaterialDetail.objects.filter(id=id)
  1980. else:
  1981. return MaterialDetail.objects.none()
  1982. def get_serializer_class(self):
  1983. if self.action in ['retrieve', 'list','update']:
  1984. return MaterialDetailSerializer
  1985. else:
  1986. return self.http_method_not_allowed(request=self.request)
  1987. def update_material(self, request, *args, **kwargs):
  1988. id = request.data.get('id', None)
  1989. if id is None:
  1990. return Response({'message': '参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
  1991. else:
  1992. material_obj = MaterialDetail.objects.filter(id=id).first()
  1993. if material_obj:
  1994. try:
  1995. serializer = MaterialDetailPOSTSerializer(material_obj, data=request.data)
  1996. if serializer.is_valid():
  1997. serializer.save()
  1998. # 记录成功日志
  1999. try:
  2000. log_success_operation(
  2001. request=request,
  2002. operation_content=f"更新物料明细 ID:{id},物料编码: {material_obj.goods_code}",
  2003. operation_level="update",
  2004. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  2005. object_id=id,
  2006. module_name="ERP物料管理"
  2007. )
  2008. except Exception as e:
  2009. pass
  2010. return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
  2011. else:
  2012. # 记录失败日志(验证失败)
  2013. try:
  2014. log_failure_operation(
  2015. request=request,
  2016. operation_content=f"更新物料明细失败 ID:{id},错误: 参数验证失败",
  2017. operation_level="update",
  2018. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  2019. object_id=id,
  2020. module_name="ERP物料管理"
  2021. )
  2022. except Exception as e:
  2023. pass
  2024. return Response({'message': '参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
  2025. except Exception as e:
  2026. # 记录异常日志
  2027. try:
  2028. log_failure_operation(
  2029. request=request,
  2030. operation_content=f"更新物料明细异常 ID:{id},错误: {str(e)}",
  2031. operation_level="update",
  2032. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  2033. object_id=id,
  2034. module_name="ERP物料管理"
  2035. )
  2036. except Exception as log_error:
  2037. pass
  2038. raise
  2039. else:
  2040. return Response({'message': '数据不存在', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
  2041. def _check_status_operation(self, material_obj):
  2042. return_data = {}
  2043. return_data['status'] = material_obj.status
  2044. if material_obj.status == 0:
  2045. return_data['message'] = '未质检,请等待'
  2046. elif material_obj.status == 1:
  2047. if material_obj.material_goods_code == '' or material_obj.material_batch_order == '':
  2048. return_data['message'] = 'erp已质检,但是批次信息未填写,请手动检查,并将批次状态设置为已质检'
  2049. material_goods_code_divded= material_obj.production_batch.split('-')
  2050. if len(material_goods_code_divded) == 2:
  2051. material_obj.material_goods_code = material_goods_code_divded[0]
  2052. material_obj.material_batch_order = material_goods_code_divded[1]
  2053. if len(material_obj.material_batch_order) == 7:
  2054. material_obj.material_batch_order = '20'+material_obj.material_batch_order[0:6]
  2055. material_obj.save()
  2056. return_data['message'] = 'erp已质检,但是批次信息未填写,已自动填写,请手动检查,并将批次状态设置为已质检'
  2057. from bound.models import BoundBatchModel
  2058. batch_obj = BoundBatchModel.objects.filter(goods_code=material_obj.material_goods_code, bound_batch_order=material_obj.material_batch_order).first()
  2059. if not batch_obj:
  2060. return_data['message'] = 'erp已质检,但是批次信息未填写,已自动填写,请手动检查,并将批次状态设置为已质检,但是WMS未找到该批次,请手动检查,并将批次状态设置为已质检'
  2061. else :
  2062. batch_obj.check_status = 1
  2063. batch_obj.save()
  2064. return_data['message'] = 'erp已质检,但是批次信息未填写,已自动填写,请手动检查,并将批次状态设置为已质检,已自动将批次状态设置为已质检'
  2065. else:
  2066. from bound.models import BoundBatchModel
  2067. batch_obj = BoundBatchModel.objects.filter(goods_code=material_obj.material_goods_code, bound_batch_order=material_obj.material_batch_order).first()
  2068. if not batch_obj:
  2069. return_data['message'] = 'erp已质检,但是WMS未找到该批次,请手动检查,并将批次状态设置为已质检'
  2070. else :
  2071. batch_obj.check_status = 1
  2072. batch_obj.save()
  2073. return_data['message'] = '已质检,请确认'
  2074. return return_data
  2075. def check_status(self,request):
  2076. id = request.data.get('id', None)
  2077. if id is None:
  2078. return Response({'message': '参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
  2079. else:
  2080. material_obj = MaterialDetail.objects.filter(id=id).first()
  2081. if material_obj:
  2082. return_data = self._check_status_operation(material_obj)
  2083. return Response({'message': '操作成功', 'data': return_data, 'code': 200}, status=status.HTTP_200_OK)
  2084. else:
  2085. return Response({'message': '数据不存在', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
  2086. class OutMaterials(viewsets.ModelViewSet):
  2087. """
  2088. retrieve:
  2089. Response a data list(get)
  2090. list:
  2091. Response a data list(all)
  2092. """
  2093. # authentication_classes = [] # 禁用所有认证类
  2094. # permission_classes = [AllowAny] # 允许任意访问
  2095. pagination_class = MyPageNumberPagination
  2096. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  2097. ordering_fields = ['id', "create_time", "update_time", ]
  2098. filter_class = OutMaterialDetailFilter
  2099. def get_project(self):
  2100. try:
  2101. id = self.kwargs.get('pk')
  2102. return id
  2103. except:
  2104. return None
  2105. def get_queryset(self):
  2106. id = self.get_project()
  2107. if self.request.user:
  2108. if id is None:
  2109. return OutMaterialDetail.objects.filter(is_delete=False)
  2110. else:
  2111. return OutMaterialDetail.objects.filter(id=id)
  2112. else:
  2113. return OutMaterialDetail.objects.none()
  2114. def get_serializer_class(self):
  2115. if self.action in ['retrieve', 'list']:
  2116. return OutMaterialDetailSerializer
  2117. else:
  2118. return self.http_method_not_allowed(request=self.request)
  2119. class InboundBillsLog(viewsets.ModelViewSet):
  2120. """
  2121. retrieve:
  2122. Response a data list(get)
  2123. list:
  2124. Response a data list(all)
  2125. """
  2126. pagination_class = MyPageNumberPagination
  2127. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  2128. ordering_fields = ["update_time", "create_time"]
  2129. def get_queryset(self):
  2130. billid = self.request.query_params.get('billid', None)
  2131. # body = json.loads(self.request.body)
  2132. # billid = body.get('billid', None)
  2133. if billid is None:
  2134. return InboundBillOperateLog.objects.none()
  2135. else:
  2136. return InboundBillOperateLog.objects.filter(billId_id=billid)
  2137. def get_serializer_class(self):
  2138. return InboundbillOperateLogSerializer
  2139. class OutboundBillsLog(viewsets.ModelViewSet):
  2140. """
  2141. retrieve:
  2142. Response a data list(get)
  2143. list:
  2144. Response a data list(all)
  2145. """
  2146. pagination_class = MyPageNumberPagination
  2147. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  2148. ordering_fields = ["update_time", "create_time"]
  2149. def get_queryset(self):
  2150. billid = self.request.query_params.get('billid', None)
  2151. # body = json.loads(self.request.body)
  2152. # billid = body.get('billid', None)
  2153. if billid is None:
  2154. return OutboundBillOperateLog.objects.none()
  2155. else:
  2156. return OutboundBillOperateLog.objects.filter(billId_id=billid)
  2157. def get_serializer_class(self):
  2158. return OutboundbillOperateLogSerializer