views.py 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543
  1. from rest_framework.views import APIView
  2. from rest_framework.parsers import JSONParser
  3. from rest_framework.decorators import api_view, parser_classes
  4. from rest_framework.response import Response
  5. from rest_framework.permissions import AllowAny
  6. from rest_framework import status
  7. from rest_framework import viewsets
  8. from django_filters.rest_framework import DjangoFilterBackend
  9. from rest_framework.filters import OrderingFilter
  10. from django.utils import timezone
  11. from django.db.models import Q, F, Case, When
  12. from django.core.cache import cache
  13. from datetime import datetime
  14. import requests
  15. from collections import defaultdict
  16. from utils.page import MyPageNumberPagination
  17. from .models import *
  18. from .serializers import *
  19. from .filter import *
  20. from .parsers import TextJSONParser
  21. from .parsers import TextJSONRenderer
  22. import logging
  23. import time
  24. from django.db import transaction
  25. from bound.models import BoundListModel, BoundBatchModel, OutBatchModel,BoundDetailModel,OutBoundDetailModel,BatchLogModel
  26. from warehouse.models import ProductListModel
  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. # 保存或更新物料明细
  112. self.save_or_update_material_detail(bound_bill, serializer)
  113. return WMSResponse.success(
  114. data=serializer.data,
  115. total=total_count,
  116. success=total_count
  117. )
  118. except Exception as e:
  119. logger.exception("服务器内部错误")
  120. return WMSResponse.error(
  121. message="系统处理异常",
  122. total=total_count,
  123. fail_count=total_count,
  124. status=status.HTTP_500_INTERNAL_SERVER_ERROR,
  125. exception=e
  126. )
  127. def _handle_validation_error(self, errors, total_count):
  128. """增强错误解析"""
  129. fail_materials = []
  130. # 提取嵌套错误信息
  131. material_errors = errors.get('materials', [])
  132. for error in material_errors:
  133. # 解析DRF的错误结构
  134. if isinstance(error, dict) and 'metadata' in error:
  135. fail_materials.append({
  136. "entryIds": error['metadata'].get('entryIds'),
  137. "production_batch": error['metadata'].get('production_batch'),
  138. "errors": {
  139. "missing_fields": error['metadata']['missing_fields'],
  140. "message": error['detail']
  141. }
  142. })
  143. return WMSResponse.error(
  144. message="物料数据不完整",
  145. total=total_count,
  146. fail_count=len(fail_materials),
  147. fail_materials=fail_materials
  148. )
  149. def _format_material_errors(self, error_dict):
  150. """格式化单个物料的错误信息"""
  151. return {
  152. field: details[0] if isinstance(details, list) else details
  153. for field, details in error_dict.items()
  154. }
  155. def find_unique_billid_and_number(self, serializer):
  156. """增强版唯一性验证"""
  157. bill_id = serializer.validated_data['billId']
  158. number = serializer.validated_data['number']
  159. # 使用Q对象进行联合查询
  160. duplicates_id = InboundBill.objects.filter(
  161. Q(billId=bill_id)
  162. ).only('billId')
  163. # 使用Q对象进行联合查询
  164. duplicates_nu = InboundBill.objects.filter(
  165. Q(number=number)
  166. ).only( 'number')
  167. error_details = {}
  168. # 检查单据编号重复
  169. if any(obj.billId != bill_id for obj in duplicates_nu):
  170. error_details['number'] = ["number入库单编码已存在,但是系统中与之前入库单单据ID不一致,请检查"]
  171. # 检查业务编号重复
  172. if any(obj.number != number for obj in duplicates_id):
  173. error_details['billId'] = ["billId入库单单据ID已存在,但是系统中与之前入库申请单编码不一致,请检查"]
  174. if error_details:
  175. return False,error_details
  176. return True,None
  177. def save_or_update_inbound_bill(self, serializer):
  178. """保存或更新入库单"""
  179. # 保存或更新入库单
  180. try:
  181. bound_bill = InboundBill.objects.get(billId=serializer.validated_data['billId'])
  182. bound_bill.number = serializer.validated_data['number']
  183. bound_bill.type = serializer.validated_data['type']
  184. bound_bill.date = serializer.validated_data['date']
  185. bound_bill.department = (serializer.validated_data['department'] if 'department' in serializer.validated_data else '')
  186. bound_bill.warehouse = serializer.validated_data['warehouse']
  187. bound_bill.creater = serializer.validated_data['creater']
  188. bound_bill.note = (serializer.validated_data['note'] if 'note' in serializer.validated_data else '')
  189. bound_bill.totalCount = serializer.validated_data['totalCount']
  190. bound_bill.update_time = timezone.now()
  191. bound_bill.save()
  192. except InboundBill.DoesNotExist:
  193. bound_bill = InboundBill.objects.create(
  194. billId=serializer.validated_data['billId'],
  195. number=serializer.validated_data['number'],
  196. type=serializer.validated_data['type'],
  197. date=serializer.validated_data['date'],
  198. department=(serializer.validated_data['department'] if 'department' in serializer.validated_data else ''),
  199. warehouse=serializer.validated_data['warehouse'],
  200. creater=serializer.validated_data['creater'],
  201. note=(serializer.validated_data['note'] if 'note' in serializer.validated_data else ''),
  202. totalCount=serializer.validated_data['totalCount'],
  203. create_time=timezone.now(),
  204. update_time=timezone.now()
  205. )
  206. return bound_bill
  207. def save_or_update_material_detail(self, bound_bill, serializer):
  208. """保存或更新物料明细"""
  209. # 保存或更新物料明细
  210. for item in serializer.validated_data['materials']:
  211. try:
  212. material_detail = MaterialDetail.objects.get(bound_billId=bound_bill, entryIds=item['entryIds'])
  213. material_detail.production_batch = item['production_batch']
  214. material_detail.goods_code = item['goods_code']
  215. material_detail.goods_name = item['goods_name']
  216. material_detail.goods_std = item['goods_std']
  217. material_detail.plan_qty = item['plan_qty']
  218. material_detail.goods_total_weight = item['plan_qty']
  219. material_detail.goods_unit = item['goods_unit']
  220. material_detail.note = (item['note'] if 'note' in item else '')
  221. material_detail.update_time = timezone.now()
  222. material_detail.save()
  223. except MaterialDetail.DoesNotExist:
  224. material_detail = MaterialDetail.objects.create(
  225. bound_billId=bound_bill,
  226. entryIds=item['entryIds'],
  227. production_batch=item['production_batch'],
  228. goods_code=item['goods_code'],
  229. goods_name=item['goods_name'],
  230. goods_std=item['goods_std'],
  231. goods_weight=1,
  232. plan_qty=item['plan_qty'],
  233. goods_total_weight=item['plan_qty'],
  234. goods_unit=item['goods_unit'],
  235. note=(item['note'] if 'note' in item else ''),
  236. create_time=timezone.now(),
  237. update_time=timezone.now()
  238. )
  239. return material_detail
  240. """入库单生成"""
  241. class GenerateInbound(APIView):
  242. """
  243. 生产入库单生成接口
  244. 功能特性:
  245. 1. 防重复创建校验(状态校验+关联单据校验)
  246. 2. 事务级数据一致性保障
  247. 3. 批量操作优化
  248. 4. 完整日志追踪
  249. """
  250. def post(self, request):
  251. try:
  252. bill_id = request.data.get('billId')
  253. if not bill_id:
  254. return Response({"error": "缺少必要参数: billId"},
  255. status=status.HTTP_400_BAD_REQUEST)
  256. # 开启原子事务
  257. with transaction.atomic():
  258. # 锁定原始单据并校验
  259. bill_obj, bound_list = self.validate_and_lock(bill_id)
  260. # 创建出入库主单
  261. bound_list = self.create_bound_list(bill_obj)
  262. logger.info(f"创建出入库主单成功 | bound_code: {bound_list.bound_code}")
  263. # 处理物料明细(批量操作优化)
  264. self.process_materials(bill_obj, bound_list)
  265. # 更新原始单据状态
  266. bill_obj.bound_status = 1
  267. bill_obj.save(update_fields=['bound_status'])
  268. logger.info(f"入库单生成成功 | billId: {bill_id} -> boundCode: {bound_list.bound_code}")
  269. return Response({
  270. "code": 200,
  271. "count": 1,
  272. "next": "null",
  273. "previous": "null",
  274. "results":{
  275. "bound_code": bound_list.bound_code,
  276. "batch_count": bill_obj.totalCount
  277. }
  278. }, status=status.HTTP_200_OK)
  279. except InboundBill.DoesNotExist:
  280. logger.error(f"原始单据不存在 | billId: {bill_id}")
  281. return Response({
  282. "code": 404,
  283. "error": "原始单据不存在"
  284. }, status=status.HTTP_404_NOT_FOUND)
  285. except Exception as e:
  286. logger.exception(f"入库单生成异常 | billId: {bill_id}")
  287. return Response({
  288. "code": 400,
  289. "error": str(e)
  290. }, status=status.HTTP_200_OK)
  291. def validate_and_lock(self, bill_id):
  292. """验证并锁定相关资源"""
  293. # 锁定原始单据
  294. bill_obj = InboundBill.objects.select_for_update().get(
  295. billId=bill_id,
  296. is_delete=False
  297. )
  298. logger.info(f"锁定原始单据成功 | billId: {bill_id}")
  299. logger.info(f"原始单据状态: {bill_obj.bound_status}")
  300. # 状态校验
  301. if bill_obj.bound_status == 1:
  302. logger.warning(f"单据已生成过入库单 | status: {bill_obj.bound_status}")
  303. raise Exception("该单据已生成过入库单")
  304. # 关联单据校验(双重校验机制)
  305. existing_bound = BoundListModel.objects.filter(
  306. Q(bound_desc__contains=f"生产入库单{bill_obj.number}") |
  307. Q(relate_bill=bill_obj)
  308. ).first()
  309. if existing_bound:
  310. logger.warning(f"发现重复关联单据 | existingCode: {existing_bound.bound_code}")
  311. raise Exception(f"已存在关联入库单[{existing_bound.bound_code}]")
  312. return bill_obj, None
  313. def process_materials(self, bill_obj, bound_list):
  314. """批量处理物料明细"""
  315. materials = MaterialDetail.objects.filter(
  316. bound_billId=bill_obj,
  317. is_delete=False
  318. ).select_related('bound_billId')
  319. if not materials:
  320. raise Exception("入库单没有有效物料明细")
  321. # 批量创建对象列表
  322. batch_list = []
  323. detail_list = []
  324. log_list = []
  325. goods_counter = defaultdict(int)
  326. order_day=str(timezone.now().strftime('-%Y%m'))
  327. order_month=str(timezone.now().strftime('%Y%m'))
  328. for idx, material in enumerate(materials, 1):
  329. # 生成批次
  330. data = {}
  331. qs_set = BoundBatchModel.objects.filter( goods_code=material.goods_code, bound_month=order_month, is_delete=False)
  332. goods_code = material.goods_code
  333. goods_counter[goods_code] += 1
  334. len_qs_set = len(qs_set) + goods_counter[goods_code]
  335. print("len_qs_set", len_qs_set)
  336. data['bound_batch_order'] = int(order_day.split('-')[-1])*100 + len_qs_set
  337. data['bound_number'] = material.goods_code + order_day + str(len_qs_set).zfill(2)
  338. batch = BoundBatchModel(
  339. bound_number=data['bound_number'],
  340. sourced_number = material.production_batch,
  341. bound_month=bound_list.bound_month,
  342. bound_batch_order=data['bound_batch_order'],
  343. warehouse_code='W01',
  344. warehouse_name='立体仓',
  345. goods_code=material.goods_code,
  346. goods_desc=material.goods_name,
  347. goods_std=material.goods_std,
  348. goods_unit=material.goods_unit,
  349. goods_qty=material.plan_qty,
  350. goods_weight=float(material.goods_weight),
  351. goods_total_weight=float(material.goods_total_weight),
  352. creater=bound_list.creater,
  353. openid=bound_list.openid,
  354. relate_material=material
  355. )
  356. batch_list.append(batch)
  357. # 生成明细
  358. detail_list.append(BoundDetailModel(
  359. bound_list=bound_list,
  360. bound_batch=batch,
  361. detail_code=f"{batch.bound_number}-DET",
  362. creater=bound_list.creater,
  363. openid=bound_list.openid
  364. ))
  365. # 生成日志
  366. log_list.append(BatchLogModel(
  367. batch_id=batch,
  368. log_type=0,
  369. log_date=timezone.now(),
  370. goods_code=batch.goods_code,
  371. goods_desc=batch.goods_desc,
  372. goods_qty=batch.goods_qty,
  373. log_content=f"生产入库批次创建,来源单据:{bill_obj.number}",
  374. creater=batch.creater,
  375. openid=batch.openid
  376. ))
  377. # 批量写入数据库
  378. BoundBatchModel.objects.bulk_create(batch_list)
  379. BoundDetailModel.objects.bulk_create(detail_list)
  380. BatchLogModel.objects.bulk_create(log_list)
  381. def create_bound_list(self, bill_obj):
  382. """创建出入库主单(带来源标识)"""
  383. if BoundListModel.objects.filter(relate_bill=bill_obj).exists():
  384. return BoundListModel.objects.get(relate_bill=bill_obj)
  385. return BoundListModel.objects.create(
  386. bound_month=timezone.now().strftime("%Y%m"),
  387. bound_date=timezone.now().strftime("%Y-%m-%d"),
  388. bound_code=bill_obj.number,
  389. bound_code_type=bill_obj.type,
  390. bound_bs_type='B01',
  391. bound_type='in',
  392. bound_desc=f"生产入库单{bill_obj.number}",
  393. bound_department=(bill_obj.department if bill_obj.department else 'D99'),
  394. base_type=0,
  395. bound_status='101',
  396. creater=bill_obj.creater,
  397. openid='ERP',
  398. relate_bill=bill_obj
  399. )
  400. def handle_exception(self, exc):
  401. """统一异常处理"""
  402. if isinstance(exc, InboundBill.DoesNotExist):
  403. return Response({"error": "原始单据不存在或已被删除"}, status=status.HTTP_404_NOT_FOUND)
  404. elif "重复" in str(exc):
  405. return Response({"error": str(exc)}, status=status.HTTP_409_CONFLICT)
  406. return super().handle_exception(exc)
  407. """出库单生成"""
  408. class GenerateOutbound(APIView):
  409. """
  410. 生产出库单生成接口
  411. 功能特性:
  412. 1. 防重复创建校验(状态校验+关联单据校验)
  413. 2. 事务级数据一致性保障
  414. 3. 批量操作优化
  415. 4. 完整日志追踪
  416. """
  417. def post(self, request):
  418. try:
  419. bill_id = request.data.get('billId')
  420. if not bill_id:
  421. return Response({"error": "缺少必要参数: billId"},
  422. status=status.HTTP_400_BAD_REQUEST)
  423. # 开启原子事务
  424. with transaction.atomic():
  425. # 锁定原始单据并校验
  426. bill_obj, bound_list = self.validate_and_lock(bill_id)
  427. # 创建出库主单
  428. bound_list = self.create_bound_list(bill_obj)
  429. logger.info(f"创建出入库主单成功 | bound_code: {bound_list.bound_code}")
  430. # 处理物料明细(批量操作优化)
  431. self.process_materials(bill_obj, bound_list)
  432. # 更新原始单据状态
  433. bill_obj.bound_status = 1
  434. bill_obj.save(update_fields=['bound_status'])
  435. logger.info(f"出库单生成成功 | billId: {bill_id} -> boundCode: {bound_list.bound_code}")
  436. return Response({
  437. "code": 200,
  438. "count": 1,
  439. "next": "null",
  440. "previous": "null",
  441. "results":{
  442. "bound_code": bound_list.bound_code,
  443. "batch_count": bill_obj.totalCount
  444. }
  445. }, status=status.HTTP_200_OK)
  446. except InboundBill.DoesNotExist:
  447. logger.error(f"原始单据不存在 | billId: {bill_id}")
  448. return Response({
  449. "code": 404,
  450. "error": "原始单据不存在"
  451. }, status=status.HTTP_404_NOT_FOUND)
  452. except Exception as e:
  453. logger.exception(f"出库单生成异常 | billId: {bill_id}")
  454. return Response({
  455. "code": 400,
  456. "error": str(e)
  457. }, status=status.HTTP_200_OK)
  458. def validate_and_lock(self, bill_id):
  459. """验证并锁定相关资源"""
  460. # 锁定原始单据
  461. bill_obj = OutboundBill.objects.select_for_update().get(
  462. billId=bill_id,
  463. is_delete=False
  464. )
  465. logger.info(f"锁定原始单据成功 | billId: {bill_id}")
  466. logger.info(f"原始单据状态: {bill_obj.bound_status}")
  467. # 状态校验
  468. if bill_obj.bound_status == 1:
  469. logger.warning(f"单据已生成过出库单 | status: {bill_obj.bound_status}")
  470. raise Exception("该单据已生成过出库单")
  471. # 关联单据校验(双重校验机制)
  472. existing_bound = BoundListModel.objects.filter(
  473. Q(bound_desc__contains=f"生产出库单{bill_obj.number}") |
  474. Q(relate_out_bill=bill_obj)
  475. ).first()
  476. if existing_bound:
  477. logger.warning(f"发现重复关联单据 | existingCode: {existing_bound.bound_code}")
  478. raise Exception(f"已存在关联出库单[{existing_bound.bound_code}]")
  479. return bill_obj, None
  480. def process_materials(self, bill_obj, bound_list):
  481. """批量处理物料明细"""
  482. materials = OutMaterialDetail.objects.filter(
  483. bound_billId=bill_obj,
  484. is_delete=False
  485. ).select_related('bound_billId')
  486. if not materials:
  487. raise Exception("出库单没有有效物料明细")
  488. # 批量创建对象列表
  489. batch_list = []
  490. detail_list = []
  491. log_list = []
  492. for idx, material in enumerate(materials, 1):
  493. # 生成批次
  494. MaterialDetail_obj = MaterialDetail.objects.get(entryIds=material.Material_entryIds.entryIds)
  495. batch_obj = BoundBatchModel.objects.get(relate_material=MaterialDetail_obj)
  496. batch = OutBatchModel(
  497. out_number = MaterialDetail_obj.production_batch,
  498. batch_number = batch_obj,
  499. out_date = timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
  500. out_type = bill_obj.type,
  501. out_note = bill_obj.note,
  502. warehouse_code='W01',
  503. warehouse_name='立体仓',
  504. goods_code=MaterialDetail_obj.goods_code,
  505. goods_desc=MaterialDetail_obj.goods_name,
  506. goods_std=MaterialDetail_obj.goods_std,
  507. goods_unit=MaterialDetail_obj.goods_unit,
  508. goods_qty=batch_obj.goods_qty,
  509. goods_out_qty=material.goods_out_qty,
  510. status = 0,
  511. container_number = 0,
  512. goods_weight = 1,
  513. goods_total_weight = material.goods_out_qty,
  514. creater=bill_obj.creater,
  515. openid='ERP',
  516. relate_material=material
  517. )
  518. batch_list.append(batch)
  519. # 生成明细
  520. detail_list.append(OutBoundDetailModel(
  521. bound_list=bound_list,
  522. bound_batch=batch,
  523. bound_batch_number = batch_obj,
  524. detail_code=f"{batch.out_number}-ODET",
  525. creater=bound_list.creater,
  526. openid=bound_list.openid
  527. ))
  528. # 生成日志
  529. log_list.append(BatchLogModel(
  530. batch_id=batch_obj,
  531. log_type=1,
  532. log_date=timezone.now(),
  533. goods_code=batch_obj.goods_code,
  534. goods_desc=batch_obj.goods_desc,
  535. goods_qty=batch.goods_out_qty,
  536. log_content=f"生产出库批次创建,来源单据:{bill_obj.number},出库件数:{batch.goods_out_qty}",
  537. creater=batch.creater,
  538. openid=batch.openid
  539. ))
  540. # 批量写入数据库
  541. OutBatchModel.objects.bulk_create(batch_list)
  542. OutBoundDetailModel.objects.bulk_create(detail_list)
  543. BatchLogModel.objects.bulk_create(log_list)
  544. def create_bound_list(self, bill_obj):
  545. """创建出入库主单(带来源标识)"""
  546. if BoundListModel.objects.filter(relate_out_bill=bill_obj).exists():
  547. return BoundListModel.objects.get(relate_out_bill=bill_obj)
  548. return BoundListModel.objects.create(
  549. bound_month=timezone.now().strftime("%Y%m"),
  550. bound_date=timezone.now().strftime("%Y-%m-%d"),
  551. bound_code=bill_obj.number,
  552. bound_code_type=bill_obj.type,
  553. bound_bs_type='B01',
  554. bound_type='out',
  555. bound_desc=f"生产出库单{bill_obj.number}",
  556. bound_department=(bill_obj.department if bill_obj.department else 'D99'),
  557. base_type=1,
  558. bound_status='201',
  559. creater=bill_obj.creater,
  560. openid='ERP',
  561. relate_out_bill=bill_obj
  562. )
  563. def handle_exception(self, exc):
  564. """统一异常处理"""
  565. if isinstance(exc, OutboundBill.DoesNotExist):
  566. return Response({"error": "原始单据不存在或已被删除"}, status=status.HTTP_404_NOT_FOUND)
  567. elif "重复" in str(exc):
  568. return Response({"error": str(exc)}, status=status.HTTP_409_CONFLICT)
  569. return super().handle_exception(exc)
  570. """出库申请"""
  571. class OutboundApplyCreate(APIView):
  572. """
  573. 生产出库申请
  574. """
  575. authentication_classes = []
  576. permission_classes = [AllowAny]
  577. def post(self, request):
  578. logger.info('生产出库申请请求 | 原始数据: %s', request.data)
  579. try:
  580. total_count = len(request.data.get('materials', []))
  581. if total_count == 0 :
  582. return WMSResponse.error(
  583. message="物料清单不能为空",
  584. total=0,
  585. fail_count=0
  586. )
  587. if total_count != request.data.get('totalCount', 0):
  588. return WMSResponse.error(
  589. message="物料数量不匹配",
  590. total=total_count,
  591. fail_count=total_count
  592. )
  593. serializer = outboundPostSerializer(data=request.data)
  594. if not serializer.is_valid():
  595. print("出错",serializer.errors)
  596. return self._handle_validation_error(serializer.errors, total_count)
  597. unique_result,error_details = self.find_unique_billid_and_number(serializer)
  598. if not unique_result:
  599. return WMSResponse.error(
  600. message="单据编号或原始单据ID重复",
  601. total=total_count,
  602. fail_count=total_count,
  603. fail_materials=[{
  604. "entryIds": None,
  605. "production_batch": None,
  606. "errors": error_details
  607. }]
  608. )
  609. # 保存或更新入库单
  610. bound_bill = self.save_or_update_inbound_bill(serializer)
  611. # 保存或更新物料明细
  612. self.save_or_update_material_detail(bound_bill, serializer)
  613. return WMSResponse.success(
  614. data=serializer.data,
  615. total=total_count,
  616. success=total_count
  617. )
  618. except Exception as e:
  619. logger.exception("服务器内部错误")
  620. return WMSResponse.error(
  621. message="系统处理异常",
  622. total=total_count,
  623. fail_count=total_count,
  624. status=status.HTTP_500_INTERNAL_SERVER_ERROR,
  625. exception=e
  626. )
  627. def _handle_validation_error(self, errors, total_count):
  628. """增强错误解析"""
  629. fail_materials = []
  630. # 提取嵌套错误信息
  631. material_errors = errors.get('materials', [])
  632. for error in material_errors:
  633. # 解析DRF的错误结构
  634. if isinstance(error, dict) and 'metadata' in error:
  635. fail_materials.append({
  636. "entryIds": error['metadata'].get('entryIds'),
  637. "production_batch": error['metadata'].get('production_batch'),
  638. "errors": {
  639. "missing_fields": error['metadata']['missing_fields'],
  640. "message": error['detail']
  641. }
  642. })
  643. return WMSResponse.error(
  644. message="物料数据不完整",
  645. total=total_count,
  646. fail_count=len(fail_materials),
  647. fail_materials=fail_materials)
  648. def _format_material_errors(self, error_dict):
  649. """格式化单个物料的错误信息"""
  650. return {
  651. field: details[0] if isinstance(details, list) else details
  652. for field, details in error_dict.items()
  653. }
  654. def find_unique_billid_and_number(self, serializer):
  655. """增强版唯一性验证"""
  656. bill_id = serializer.validated_data['billId']
  657. number = serializer.validated_data['number']
  658. # 使用Q对象进行联合查询
  659. duplicates_id = OutboundBill.objects.filter(
  660. Q(billId=bill_id)
  661. ).only('billId')
  662. # 使用Q对象进行联合查询
  663. duplicates_nu = OutboundBill.objects.filter(
  664. Q(number=number)
  665. ).only( 'number')
  666. error_details = {}
  667. # 检查单据编号重复
  668. if any(obj.billId != bill_id for obj in duplicates_nu):
  669. error_details['number'] = ["number出库单编码已存在,但是系统中与之前出库单单据ID不一致,请检查"]
  670. # 检查业务编号重复
  671. if any(obj.number != number for obj in duplicates_id):
  672. error_details['billId'] = ["billId出库库单单据ID已存在,但是系统中与之前出库申请单编码不一致,请检查"]
  673. if error_details:
  674. return False,error_details
  675. return True,None
  676. def save_or_update_inbound_bill(self, serializer):
  677. """保存或更新出库单"""
  678. try:
  679. bound_bill = OutboundBill.objects.get(billId=serializer.validated_data['billId'])
  680. bound_bill.number = serializer.validated_data['number']
  681. bound_bill.type = serializer.validated_data['type']
  682. bound_bill.date = serializer.validated_data['date']
  683. bound_bill.department = (serializer.validated_data['department'] if 'department' in serializer.validated_data else '')
  684. bound_bill.warehouse = serializer.validated_data['warehouse']
  685. bound_bill.creater = serializer.validated_data['creater']
  686. bound_bill.note = (serializer.validated_data['note'] if 'note' in serializer.validated_data else '')
  687. bound_bill.totalCount = serializer.validated_data['totalCount']
  688. bound_bill.update_time = timezone.now()
  689. bound_bill.save()
  690. except OutboundBill.DoesNotExist:
  691. bound_bill = OutboundBill.objects.create(
  692. billId=serializer.validated_data['billId'],
  693. number=serializer.validated_data['number'],
  694. type=serializer.validated_data['type'],
  695. date=serializer.validated_data['date'],
  696. department=(serializer.validated_data['department'] if 'department' in serializer.validated_data else ''),
  697. warehouse=serializer.validated_data['warehouse'],
  698. creater=serializer.validated_data['creater'],
  699. note=(serializer.validated_data['note'] if 'note' in serializer.validated_data else ''),
  700. totalCount=serializer.validated_data['totalCount'],
  701. create_time=timezone.now(),
  702. update_time=timezone.now()
  703. )
  704. return bound_bill
  705. def save_or_update_material_detail(self, bound_bill, serializer):
  706. """保存或更新物料明细"""
  707. for item in serializer.validated_data['materials']:
  708. try:
  709. material_detail = OutMaterialDetail.objects.get(bound_billId=bound_bill, entryIds=item['entryIds'])
  710. Material_entryIds = MaterialDetail.objects.filter(
  711. goods_code = item['goods_code'],
  712. production_batch = item['production_batch']
  713. ).first()
  714. if not Material_entryIds:
  715. logger.info("出库单号%s,更新——物料明细不存在",bound_bill.number)
  716. material_detail.Material_entryIds = Material_entryIds
  717. material_detail.production_batch = item['production_batch']
  718. material_detail.goods_code = item['goods_code']
  719. material_detail.goods_name = item['goods_name']
  720. material_detail.goods_out_qty = item['goods_out_qty']
  721. material_detail.goods_total_weight = item['goods_out_qty']
  722. material_detail.goods_unit = item['goods_unit']
  723. material_detail.note = (item['note'] if 'note' in item else '')
  724. material_detail.update_time = timezone.now()
  725. material_detail.save()
  726. except OutMaterialDetail.DoesNotExist:
  727. Material_entryIds = MaterialDetail.objects.filter(
  728. goods_code = item['goods_code'],
  729. production_batch = item['production_batch']
  730. ).first()
  731. if not Material_entryIds:
  732. logger.info("出库单号%s,创建——物料明细不存在",bound_bill.number)
  733. material_detail = OutMaterialDetail.objects.create(
  734. bound_billId=bound_bill,
  735. entryIds=item['entryIds'],
  736. Material_entryIds=Material_entryIds,
  737. production_batch=item['production_batch'],
  738. goods_code=item['goods_code'],
  739. goods_name=item['goods_name'],
  740. goods_weight=1,
  741. goods_out_qty=item['goods_out_qty'],
  742. goods_total_weight=item['goods_out_qty'],
  743. goods_unit=item['goods_unit'],
  744. note=(item['note'] if 'note' in item else ''),
  745. create_time=timezone.now(),
  746. update_time=timezone.now()
  747. )
  748. return material_detail
  749. """产品信息"""
  750. class ProductInfo(APIView):
  751. """
  752. 批次信息更新
  753. """
  754. authentication_classes = [] # 禁用所有认证类
  755. permission_classes = [AllowAny] # 允许任意访问
  756. # parser_classes = [TextJSONParser] # 强制使用 text/json
  757. # renderer_classes = [TextJSONRenderer] # 强制使用 text/json
  758. def post(self, request):
  759. data = request.data
  760. logger.info('批次信息更新 | 原始数据: %s', data)
  761. total_count = data.get('totalCount', 0)
  762. materials = data.get('materials', [])
  763. success_count = 0
  764. fail_count = 0
  765. fail_materials = []
  766. try:
  767. with transaction.atomic(): # 开启事务确保原子性
  768. # 预查询已存在的产品ID
  769. existing_ids = set(ProductListModel.objects.filter(
  770. id__in=[m.get('id') for m in materials if m.get('id')]
  771. ).values_list('id', flat=True))
  772. for material in materials:
  773. material_id = material.get('id')
  774. try:
  775. if material_id and material_id in existing_ids:
  776. instance = ProductListModel.objects.get(id=material_id)
  777. created = False
  778. else:
  779. instance = ProductListModel()
  780. created = True
  781. # 字段映射与校验
  782. instance.id = material_id
  783. instance.product_code = material.get('product_code', instance.product_code)
  784. instance.product_name = material.get('product_name', instance.product_name)
  785. instance.product_std = material.get('product_std', instance.product_std)
  786. instance.product_unit = material.get('product_unit', instance.product_unit or 'KG')
  787. # 必填字段校验[7](@ref)
  788. required_fields = ['product_code', 'product_name']
  789. if any(not getattr(instance, field) for field in required_fields):
  790. raise ValueError(f"Missing required fields: {required_fields}")
  791. instance.is_delete = False # 强制重置删除标记[1](@ref)
  792. instance.full_clean() # 触发模型验证[3](@ref)
  793. instance.save()
  794. success_count += 1
  795. except Exception as e:
  796. fail_count += 1
  797. error_msg = f"{type(e).__name__}: {str(e)}"
  798. logger.warning(f"Material processing failed: {material} | Error: {error_msg}")
  799. fail_materials.append({
  800. **material,
  801. "error": error_msg
  802. })
  803. # 检查总数一致性[7](@ref)
  804. if len(materials) != total_count:
  805. return WMSResponse.error(
  806. message="物料数量不匹配",
  807. total=total_count,
  808. fail_count=total_count,
  809. fail_materials=fail_materials
  810. )
  811. except Exception as e:
  812. logger.error(f"Batch update transaction failed: {str(e)}", exc_info=True)
  813. return WMSResponse.error(
  814. message=f"批量处理失败: {str(e)}",
  815. total=total_count,
  816. fail_count=fail_count,
  817. fail_materials=fail_materials,
  818. exception=e
  819. )
  820. return WMSResponse.success(
  821. data=[], # 根据需求可返回处理后的数据
  822. total=total_count,
  823. success=success_count
  824. )
  825. """批次更新"""
  826. class BatchUpdate(APIView):
  827. """
  828. 商品信息查询
  829. """
  830. authentication_classes = [] # 禁用所有认证类
  831. permission_classes = [AllowAny] # 允许任意访问
  832. def post(self, request):
  833. data = request.data
  834. total_count = data.get('totalCount', 0)
  835. materials = data.get('materials', [])
  836. success_count = 0
  837. fail_count = 0
  838. fail_materials = []
  839. try:
  840. with transaction.atomic(): # 开启事务确保原子性
  841. if total_count != len(materials):
  842. return WMSResponse.error(
  843. message="物料数量不匹配",
  844. total=total_count,
  845. fail_count=total_count
  846. )
  847. for material in materials:
  848. material_billId = material.get('billId')
  849. material_entryId = material.get('entryIds')
  850. material_status = material.get('status')
  851. if material_status == 'passing':
  852. try:
  853. instance = MaterialDetail.objects.get(bound_billId_id=material_billId,entryIds=material_entryId)
  854. if not instance:
  855. logger.info("物料明细不存在")
  856. fail_materials.append({
  857. **material,
  858. "error": "物料明细不存在"
  859. })
  860. fail_count += 1
  861. continue
  862. instance.status = 1
  863. instance.save()
  864. success_count += 1
  865. except Exception as e:
  866. fail_count += 1
  867. error_msg = f"{type(e).__name__}: {str(e)}"
  868. logger.warning(f"Material processing failed: {material} | Error: {error_msg}")
  869. fail_materials.append({
  870. **material,
  871. "error": error_msg
  872. })
  873. # 检查总数一致性[7](@ref)
  874. if success_count != total_count:
  875. return WMSResponse.error(
  876. message="单据编码与详细编码不匹配",
  877. total=total_count,
  878. fail_count=total_count,
  879. fail_materials=fail_materials
  880. )
  881. except Exception as e:
  882. logger.error(f"Batch update transaction failed: {str(e)}", exc_info=True)
  883. return WMSResponse.error(
  884. message=f"批量处理失败: {str(e)}",
  885. total=total_count,
  886. fail_count=fail_count,
  887. fail_materials=fail_materials,
  888. exception=e
  889. )
  890. return WMSResponse.success(
  891. data=[],
  892. total=total_count,
  893. success=success_count
  894. )
  895. """token"""
  896. class AccessToken(APIView):
  897. """
  898. 获取ERP的access_token
  899. 方法:post到
  900. https://okyy.test.kdgalaxy.com/kapi/oauth2/getToken
  901. 参数:
  902. {
  903. "client_id" : "WMS",
  904. "client_secret" : "1Ca~2Tu-3Fx$3Rg@",
  905. "username" : "xs",
  906. "accountId" : "2154719510106474496",
  907. "nonce" : "2025-04-27 11:36:08",
  908. "timestamp" : "2025-04-27 11:36:08",
  909. "language" : "zh_CN"
  910. }
  911. 返回:
  912. {
  913. "data": {
  914. "access_token": "CanAzMDM=",
  915. "token_type": "Bearer",
  916. "refresh_token": "4297d40e-b2ac-48b4-98a2-b50665e6faaf",
  917. "scope": "API",
  918. "expires_in": 7199990,
  919. "id_token": "d09UWXhOakV4TnpjMkE9EVGVV",
  920. "id_token_expires_in": 7199990,
  921. "language": "zh_CN"
  922. },
  923. "errorCode": "0",
  924. "message": "",
  925. "status": true
  926. }
  927. """
  928. authentication_classes = [] # 禁用所有认证类
  929. permission_classes = [AllowAny] # 允许任意访问
  930. @classmethod
  931. def get_token(cls):
  932. try:
  933. """获取access_token"""
  934. url = "https://okyy.test.kdgalaxy.com/kapi/oauth2/getToken"
  935. data = {
  936. "client_id" : "WMS",
  937. "client_secret" : "1Ca~2Tu-3Fx$3Rg@",
  938. "username" : "xs",
  939. "accountId" : "2154719510106474496",
  940. "nonce" : timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
  941. "timestamp" : timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
  942. "language" : "zh_CN"
  943. }
  944. print("请求参数",data)
  945. response = requests.post(url, json=data,timeout=10)
  946. if response.status_code == 200:
  947. result = response.json()
  948. if result.get('status'):
  949. logger.info(f"获取access_token成功 | access_token: {result.get('data',{}).get('access_token')}")
  950. return result.get('data',{}).get('access_token')
  951. return None
  952. except Exception as e:
  953. print("获取access_token异常",e)
  954. logger.exception("获取access_token异常")
  955. return None
  956. @classmethod
  957. def get_current_token(cls):
  958. """获取当前有效Token(带缓存机制)"""
  959. cache_key = 'erp_access_token'
  960. cached_token = cache.get(cache_key)
  961. if not cached_token:
  962. new_token = cls.get_token()
  963. if new_token:
  964. # 缓存时间略短于实际有效期
  965. cache.set(cache_key, new_token, timeout=1000)
  966. return new_token
  967. return cached_token
  968. """基本同步业务类"""
  969. class ERPSyncBase:
  970. """ERP同步基类(作为发送方)"""
  971. max_retries = 30 # 最大重试次数
  972. retry_delay = 3
  973. # 重试间隔秒数
  974. def __init__(self, wms_bill):
  975. self.wms_bill = wms_bill # WMS单据对象
  976. self.erp_id_field = None # 需要更新的ERP ID字段名
  977. def build_erp_payload(self):
  978. """构造ERP请求数据(需子类实现)"""
  979. raise NotImplementedError
  980. def get_erp_endpoint(self):
  981. """获取ERP接口地址(需子类实现)"""
  982. raise NotImplementedError
  983. def process_erp_response(self, response):
  984. """处理ERP响应(需子类实现)返回erp_id"""
  985. raise NotImplementedError
  986. def execute_sync(self):
  987. """执行同步操作"""
  988. headers = {
  989. 'accessToken': f'{AccessToken.get_current_token()}'
  990. }
  991. for attempt in range(self.max_retries):
  992. try:
  993. print("请求头:",headers)
  994. print("请求体:",self.build_erp_payload())
  995. print("请求地址:",self.get_erp_endpoint())
  996. response = requests.post(
  997. self.get_erp_endpoint(),
  998. json=self.build_erp_payload(),
  999. headers=headers,
  1000. timeout=10
  1001. )
  1002. response.raise_for_status()
  1003. erp_id = self.process_erp_response(response.json())
  1004. return True
  1005. except requests.exceptions.HTTPError as http_err:
  1006. if response.status_code == 519:
  1007. print("特定HTTP错误 519:", http_err)
  1008. logger.error(f"ERP接口HTTP错误 519 第{attempt+1}次重试 | 单据:{self.wms_bill.number} | 错误: {http_err}")
  1009. else:
  1010. print("HTTP错误:", http_err)
  1011. logger.error(f"ERP接口HTTP错误 第{attempt+1}次重试 | 单据:{self.wms_bill.number} | 错误: {http_err}")
  1012. time.sleep(self.retry_delay)
  1013. except requests.exceptions.RequestException as e:
  1014. print("ERP接口请求异常:",e)
  1015. print(f"ERP接口请求失败 第{attempt+1}次重试 | 单据:{self.wms_bill.number}")
  1016. logger.error(f"ERP接口请求失败 第{attempt+1}次重试 | 单据:{self.wms_bill.number}")
  1017. time.sleep(self.retry_delay)
  1018. logger.error(f"ERP同步最终失败 | 单据:{self.wms_bill.number}")
  1019. return False
  1020. # ==================== 业务接口 ====================
  1021. """生产入库审核"""
  1022. class ProductionInboundAuditSync(ERPSyncBase):
  1023. erp_id_field = 'erp_audit_id'
  1024. # 请求地址
  1025. def get_erp_endpoint(self):
  1026. return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_productinbill/audit"
  1027. # 请求参数
  1028. # {
  1029. # "data":{
  1030. # "billnos":[
  1031. # "AgTSC",
  1032. # "fgBAH"
  1033. # ]
  1034. # }
  1035. # }
  1036. def build_erp_payload(self):
  1037. return {
  1038. "data": {
  1039. "billnos": [
  1040. self.wms_bill.number,
  1041. ]
  1042. }
  1043. }
  1044. # 处理响应
  1045. def process_erp_response(self, response):
  1046. print("ERP审核响应:",response)
  1047. if response['code'] != '200':
  1048. raise ValueError(f"ERP审核失败: {response['msg']}")
  1049. return response['data']['erp_audit_id']
  1050. """采购收料入库审核"""
  1051. class PurchaseInboundAuditSync(ERPSyncBase):
  1052. erp_id_field = 'erp_audit_id'
  1053. # 请求地址
  1054. def get_erp_endpoint(self):
  1055. return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_purreceivebill/audit"
  1056. # 请求参数
  1057. # {
  1058. # "data":{
  1059. # "billnos":[
  1060. # "AgTSC",
  1061. # "fgBAH"
  1062. # ]
  1063. # }
  1064. # }
  1065. def build_erp_payload(self):
  1066. return {
  1067. "data": {
  1068. "billnos": [
  1069. self.wms_bill.number,
  1070. ]
  1071. }
  1072. }
  1073. # 处理响应
  1074. def process_erp_response(self, response):
  1075. print("ERP审核响应:",response)
  1076. if response['code'] != '200':
  1077. raise ValueError(f"ERP审核失败: {response['msg']}")
  1078. return response['data']['erp_audit_id']
  1079. """其他入库审核"""
  1080. class OtherInboundAuditSync(ERPSyncBase):
  1081. erp_id_field = 'erp_audit_id'
  1082. # 请求地址
  1083. def get_erp_endpoint(self):
  1084. return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_otherinbill/audit"
  1085. # 请求参数
  1086. # {
  1087. # "data":{
  1088. # "billnos":[
  1089. # "AgTSC",
  1090. # "fgBAH"
  1091. # ]
  1092. # }
  1093. # }
  1094. def build_erp_payload(self):
  1095. return {
  1096. "data": {
  1097. "billnos": [
  1098. self.wms_bill.number,
  1099. ]
  1100. }
  1101. }
  1102. # 处理响应
  1103. def process_erp_response(self, response):
  1104. print("ERP审核响应:",response)
  1105. if response['code'] != '200':
  1106. raise ValueError(f"ERP审核失败: {response['msg']}")
  1107. return response['data']['erp_audit_id']
  1108. """其他出库审核同步"""
  1109. class OtherOutboundAuditSync(ERPSyncBase):
  1110. erp_id_field = 'erp_audit_id'
  1111. # 请求地址
  1112. def get_erp_endpoint(self):
  1113. return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_otheroutbill/audit"
  1114. # 请求参数
  1115. # {
  1116. # "data":{
  1117. # "billnos":[
  1118. # "AgTSC",
  1119. # "fgBAH"
  1120. # ]
  1121. # }
  1122. # }
  1123. def build_erp_payload(self):
  1124. return {
  1125. "data": {
  1126. "billnos": [
  1127. self.wms_bill.number,
  1128. ]
  1129. }
  1130. }
  1131. # 处理响应
  1132. def process_erp_response(self, response):
  1133. print("ERP审核响应:",response)
  1134. if response['code'] != '200':
  1135. raise ValueError(f"ERP审核失败: {response['msg']}")
  1136. return response['data']['erp_audit_id']
  1137. """生产领料出库审核"""
  1138. class ProductionOutboundAuditSync(ERPSyncBase):
  1139. erp_id_field = 'erp_audit_id'
  1140. # 请求地址
  1141. def get_erp_endpoint(self):
  1142. return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_mdc_mftproorder/audit"
  1143. # 请求参数
  1144. # {
  1145. # "data":{
  1146. # "billnos":[
  1147. # "AgTSC",
  1148. # "fgBAH"
  1149. # ]
  1150. # }
  1151. # }
  1152. def build_erp_payload(self):
  1153. return {
  1154. "data": {
  1155. "billnos": [
  1156. self.wms_bill.number,
  1157. ]
  1158. }
  1159. }
  1160. # 处理响应
  1161. def process_erp_response(self, response):
  1162. print("ERP审核响应:",response)
  1163. if response['code'] != '200':
  1164. raise ValueError(f"ERP审核失败: {response['msg']}")
  1165. return response['data']['erp_audit_id']
  1166. """采购入库保存"""
  1167. class PurchaseInboundSaveSync(ERPSyncBase):
  1168. erp_id_field = 'erp_save_id'
  1169. def get_erp_endpoint(self):
  1170. return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_purinbill/save"
  1171. def build_erp_payload(self):
  1172. # {
  1173. # "purinbill":{
  1174. # "billId":"1745732021174",
  1175. # "entryIds":[
  1176. # "1745732021087"
  1177. # ]
  1178. # }
  1179. # }
  1180. return {
  1181. "purinbill": {
  1182. "billId":self.wms_bill.number,
  1183. "entryIds": [
  1184. self.wms_bill.number,
  1185. ]
  1186. }
  1187. }
  1188. def process_erp_response(self, response):
  1189. print("ERP审核响应:",response)
  1190. return response['data']['purchase_order_id']
  1191. """销售出库保存"""
  1192. class SaleOutboundSaveSync(ERPSyncBase):
  1193. erp_id_field = 'erp_save_id'
  1194. def get_erp_endpoint(self):
  1195. return "https://okyy.test.kdgalaxy.com/kapi/v2/l772/im/im_saloutbill/save"
  1196. def build_erp_payload(self):
  1197. # {
  1198. # "purinbill":{
  1199. # "billId":"1745732021174",
  1200. # "entryIds":[
  1201. # "1745732021087"
  1202. # ]
  1203. # }
  1204. # }
  1205. return {
  1206. "purinbill": {
  1207. "billId":self.wms_bill.number,
  1208. "entryIds": [
  1209. self.wms_bill.number,
  1210. ]
  1211. }
  1212. }
  1213. def process_erp_response(self, response):
  1214. print("ERP审核响应:",response)
  1215. return response['data']['purchase_order_id']
  1216. """前端视图类·ERP入库"""
  1217. class InboundBills(viewsets.ModelViewSet):
  1218. """
  1219. retrieve:
  1220. Response a data list(get)
  1221. list:
  1222. Response a data list(all)
  1223. """
  1224. # authentication_classes = [] # 禁用所有认证类
  1225. # permission_classes = [AllowAny] # 允许任意访问
  1226. pagination_class = MyPageNumberPagination
  1227. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  1228. ordering_fields = ["update_time", "create_time"]
  1229. filter_class = InboundBillFilter
  1230. def get_project(self):
  1231. # 获取项目ID,如果不存在则返回None
  1232. try:
  1233. id = self.kwargs.get('pk')
  1234. return id
  1235. except:
  1236. return None
  1237. def get_queryset(self):
  1238. # 根据请求用户过滤查询集
  1239. id = self.get_project()
  1240. if self.request.user:
  1241. if id is None:
  1242. return InboundBill.objects.filter(is_delete=False)
  1243. else:
  1244. return InboundBill.objects.filter(billId=id, is_delete=False)
  1245. else:
  1246. return InboundBill.objects.none()
  1247. def get_serializer_class(self):
  1248. # 根据操作类型选择合适的序列化器
  1249. if self.action in ['list', 'retrieve', ]:
  1250. return InboundApplySerializer
  1251. else:
  1252. return self.http_method_not_allowed(request=self.request)
  1253. """前端视图类·ERP出库"""
  1254. class OutboundBills(viewsets.ModelViewSet):
  1255. """
  1256. retrieve:
  1257. Response a data list(get)
  1258. list:
  1259. Response a data list(all)
  1260. """
  1261. # authentication_classes = [] # 禁用所有认证类
  1262. # permission_classes = [AllowAny] # 允许任意访问
  1263. pagination_class = MyPageNumberPagination
  1264. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  1265. ordering_fields = ["update_time", "create_time"]
  1266. filter_class = OutboundBillFilter
  1267. def get_project(self):
  1268. # 获取项目ID,如果不存在则返回None
  1269. try:
  1270. id = self.kwargs.get('pk')
  1271. return id
  1272. except:
  1273. return None
  1274. def get_queryset(self):
  1275. # 根据请求用户过滤查询集
  1276. id = self.get_project()
  1277. if self.request.user:
  1278. if id is None:
  1279. return OutboundBill.objects.filter(is_delete=False)
  1280. else:
  1281. return OutboundBill.objects.filter(billId=id, is_delete=False)
  1282. else:
  1283. return OutboundBill.objects.none()
  1284. def get_serializer_class(self):
  1285. # 根据操作类型选择合适的序列化器
  1286. if self.action in ['list', 'retrieve', ]:
  1287. return OutboundApplySerializer
  1288. else:
  1289. return self.http_method_not_allowed(request=self.request)
  1290. class Materials(viewsets.ModelViewSet):
  1291. """
  1292. retrieve:
  1293. Response a data list(get)
  1294. list:
  1295. Response a data list(all)
  1296. """
  1297. # authentication_classes = [] # 禁用所有认证类
  1298. # permission_classes = [AllowAny] # 允许任意访问
  1299. pagination_class = MyPageNumberPagination
  1300. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  1301. ordering_fields = ['id', "create_time", "update_time", ]
  1302. filter_class = MaterialDetailFilter
  1303. def get_project(self):
  1304. try:
  1305. id = self.kwargs.get('pk')
  1306. return id
  1307. except:
  1308. return None
  1309. def get_queryset(self):
  1310. id = self.get_project()
  1311. if self.request.user:
  1312. if id is None:
  1313. return MaterialDetail.objects.filter(is_delete=False)
  1314. else:
  1315. return MaterialDetail.objects.filter(id=id)
  1316. else:
  1317. return MaterialDetail.objects.none()
  1318. def get_serializer_class(self):
  1319. if self.action in ['retrieve', 'list']:
  1320. return MaterialDetailSerializer
  1321. else:
  1322. return self.http_method_not_allowed(request=self.request)
  1323. class OutMaterials(viewsets.ModelViewSet):
  1324. """
  1325. retrieve:
  1326. Response a data list(get)
  1327. list:
  1328. Response a data list(all)
  1329. """
  1330. # authentication_classes = [] # 禁用所有认证类
  1331. # permission_classes = [AllowAny] # 允许任意访问
  1332. pagination_class = MyPageNumberPagination
  1333. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  1334. ordering_fields = ['id', "create_time", "update_time", ]
  1335. filter_class = OutMaterialDetailFilter
  1336. def get_project(self):
  1337. try:
  1338. id = self.kwargs.get('pk')
  1339. return id
  1340. except:
  1341. return None
  1342. def get_queryset(self):
  1343. id = self.get_project()
  1344. if self.request.user:
  1345. if id is None:
  1346. return OutMaterialDetail.objects.filter(is_delete=False)
  1347. else:
  1348. return OutMaterialDetail.objects.filter(id=id)
  1349. else:
  1350. return OutMaterialDetail.objects.none()
  1351. def get_serializer_class(self):
  1352. if self.action in ['retrieve', 'list']:
  1353. return OutMaterialDetailSerializer
  1354. else:
  1355. return self.http_method_not_allowed(request=self.request)