views.py 203 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569
  1. from rest_framework.viewsets import ViewSet
  2. from rest_framework import viewsets
  3. from utils.page import MyPageNumberPagination
  4. from django.db.models import Prefetch
  5. from rest_framework.filters import OrderingFilter
  6. from django_filters.rest_framework import DjangoFilterBackend
  7. from rest_framework.response import Response
  8. from django.db.models import F, Case, When
  9. from django.db.models import OuterRef, Subquery
  10. from django.utils import timezone
  11. import requests
  12. from django.db import transaction
  13. import logging
  14. from rest_framework import status
  15. from .models import ContainerListModel,ContainerDetailModel,ContainerOperationModel,ContainerWCSModel,TaskModel,out_batch_detail,ContainerDetailLogModel,batchLogModel,WCSTaskLogModel
  16. from bound.models import BoundDetailModel,BoundListModel,OutBoundDetailModel
  17. from bin.views import LocationAllocation,base_location
  18. from bin.models import LocationModel,LocationContainerLink,LocationGroupModel
  19. from bound.models import BoundBatchModel,OutBatchModel,BatchOperateLogModel
  20. from django.conf import settings
  21. from rest_framework.views import APIView
  22. from rest_framework.response import Response as DRFResponse
  23. import os
  24. import re
  25. from .serializers import ContainerDetailGetSerializer,ContainerDetailPostSerializer,ContainerDetailSimpleGetSerializer,ContainerDetailPutSerializer
  26. from .serializers import ContainerListGetSerializer,ContainerListPostSerializer
  27. from .serializers import ContainerOperationGetSerializer,ContainerOperationPostSerializer
  28. from .serializers import TaskGetSerializer,TaskPostSerializer
  29. from .serializers import WCSTaskGetSerializer,WCSTaskLogSerializer
  30. from .serializers import OutBoundFullDetailSerializer,OutBoundDetailSerializer
  31. from .serializers import ContainerDetailLogSerializer
  32. from .serializers import batchLogModelSerializer
  33. from .filter import ContainerDetailFilter,ContainerListFilter,ContainerOperationFilter,TaskFilter,WCSTaskFilter,ContainerDetailLogFilter,batchLogFilter,WCSTaskLogFilter
  34. from rest_framework.permissions import AllowAny
  35. import threading
  36. from django.db import close_old_connections
  37. from bin.services import AllocationService
  38. from collections import defaultdict
  39. from django.db.models import Sum
  40. from staff.models import ListModel as StaffListModel
  41. from operation_log.views import log_success_operation, log_failure_operation, log_operation
  42. logger = logging.getLogger(__name__)
  43. loggertask = logging.getLogger('wms.WCSTask')
  44. from .models import DispatchConfig
  45. from .serializers import DispatchConfigSerializer
  46. DEFAULT_LOCATION_GROUP_ID = 0
  47. DEFAULT_ORDER_NUMBER = 0
  48. LOCATION_CODE_REGEX = re.compile(r'([A-Z0-9]+-L\d+C\d{3}-\d{2})', re.IGNORECASE)
  49. GROUP_CODE_REGEX = re.compile(r'([A-Z0-9]+-L\d+C\d{3})', re.IGNORECASE)
  50. COORDINATE_SUFFIX_REGEX = re.compile(r'(\d+)-(\d+)-(\d+)$')
  51. def select_reference_location(task_type, current_location, target_location):
  52. """
  53. 根据任务类型确定需要解析的库位:入库/移库看目标,其余看当前
  54. """
  55. normalized = str(task_type or '').lower()
  56. if normalized in ('inbound', 'move', 'putaway'):
  57. return target_location or current_location
  58. return current_location or target_location
  59. def _find_location_instance(location_str):
  60. if not location_str:
  61. return None
  62. normalized = str(location_str).strip()
  63. if not normalized:
  64. return None
  65. candidates = {normalized, normalized.upper()}
  66. regex_match = LOCATION_CODE_REGEX.search(normalized)
  67. if regex_match:
  68. candidates.add(regex_match.group(1).upper())
  69. for candidate in candidates:
  70. location = LocationModel.objects.filter(
  71. location_code=candidate
  72. ).only('id', 'location_group', 'c_number').first()
  73. if location:
  74. return location
  75. coordinate_match = COORDINATE_SUFFIX_REGEX.search(normalized)
  76. if coordinate_match:
  77. row, col, layer = coordinate_match.groups()
  78. try:
  79. return LocationModel.objects.filter(
  80. row=int(row),
  81. col=int(col),
  82. layer=int(layer)
  83. ).only('id', 'location_group', 'c_number').first()
  84. except ValueError:
  85. return None
  86. return None
  87. def _extract_group_code(location_str):
  88. if not location_str:
  89. return None
  90. match = GROUP_CODE_REGEX.search(location_str)
  91. if match:
  92. return match.group(1).upper()
  93. parts = str(location_str).split('-')
  94. if len(parts) > 1 and parts[-1].isdigit():
  95. candidate = '-'.join(parts[:-1])
  96. if GROUP_CODE_REGEX.match(candidate):
  97. return candidate.upper()
  98. return None
  99. def resolve_location_group_metadata(location_value):
  100. defaults = {
  101. 'location_group_id': DEFAULT_LOCATION_GROUP_ID,
  102. 'order_number': DEFAULT_ORDER_NUMBER,
  103. }
  104. if location_value is None:
  105. return defaults.copy()
  106. location_str = str(location_value).strip()
  107. if not location_str:
  108. return defaults.copy()
  109. location_instance = _find_location_instance(location_str)
  110. if location_instance:
  111. group_code = location_instance.location_group
  112. group = LocationGroupModel.objects.filter(
  113. group_code=group_code
  114. ).only('id').first()
  115. return {
  116. 'location_group_id': group.id if group else defaults['location_group_id'],
  117. 'order_number': (
  118. location_instance.c_number
  119. ) or defaults['order_number'],
  120. }
  121. group_code = _extract_group_code(location_str)
  122. if group_code:
  123. group = LocationGroupModel.objects.filter(
  124. group_code=group_code
  125. ).only('id').first()
  126. if group:
  127. return {
  128. 'location_group_id': group.id,
  129. 'order_number': defaults['order_number'],
  130. }
  131. return defaults.copy()
  132. def get_task_location_metadata(task_type, current_location, target_location):
  133. reference_location = select_reference_location(task_type, current_location, target_location)
  134. return resolve_location_group_metadata(reference_location)
  135. # 托盘分类视图
  136. # 借助LocationContainerLink,其中
  137. # 库位-托盘关联表(记录实时存放关系)
  138. # class LocationContainerLink(models.Model):
  139. # location = models.ForeignKey(
  140. # LocationModel,
  141. # on_delete=models.CASCADE,
  142. # related_name='container_links',
  143. # verbose_name='库位'
  144. # )
  145. # container = models.ForeignKey(
  146. # ContainerListModel,
  147. # on_delete=models.CASCADE,
  148. # related_name='location_links',
  149. # verbose_name='关联托盘'
  150. # )
  151. # task_wcs = models.ForeignKey(ContainerWCSModel, on_delete=models.CASCADE, null=True, blank=True)
  152. # task_detail = models.ForeignKey(TaskModel, on_delete=models.CASCADE, null=True, blank=True)
  153. # put_time = models.DateTimeField(auto_now_add=True, verbose_name='上架时间')
  154. # operator = models.CharField(max_length=50, verbose_name='操作人')
  155. # is_active = models.BooleanField(default=True, verbose_name='是否有效')
  156. # 借助LocationContainerLink中的is_active字段,可以查询托盘是否在库位中,
  157. # 若is_active为True,则托盘在库位中,否则托盘不在库位中。同时,再增加字段来显示在库托盘中的存储的物料信息。
  158. # class containerclassViewSet(viewsets.ModelViewSet):
  159. # 托盘流水汇总批次流水
  160. class batchLogModelViewSet(viewsets.ModelViewSet):
  161. """
  162. retrieve:
  163. Response a data list(get)
  164. list:
  165. Response a data list(all)
  166. create:
  167. Create a data line(post)
  168. delete:
  169. Delete a data line(delete)
  170. """
  171. pagination_class = MyPageNumberPagination
  172. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  173. ordering_fields = ['id', "create_time", "update_time", ]
  174. filter_class = batchLogFilter
  175. def get_project(self):
  176. try:
  177. id = self.kwargs.get('pk')
  178. return id
  179. except:
  180. return None
  181. def get_queryset(self):
  182. id = self.get_project()
  183. if self.request.user:
  184. if id is None:
  185. return batchLogModel.objects.filter()
  186. else:
  187. return batchLogModel.objects.filter(id=id)
  188. else:
  189. return batchLogModel.objects.none()
  190. def get_serializer_class(self):
  191. if self.action in ['list', 'destroy','retrieve']:
  192. return batchLogModelSerializer
  193. else:
  194. return self.http_method_not_allowed(request=self.request)
  195. def create(self, request, *args, **kwargs):
  196. data = self.request.data
  197. return Response(data, status=200)
  198. def update(self, request, pk):
  199. qs = self.get_object()
  200. data = self.request.data
  201. serializer = self.get_serializer(qs, data=data)
  202. serializer.is_valid(raise_exception=True)
  203. serializer.save()
  204. headers = self.get_success_headers(serializer.data)
  205. return Response(serializer.data, status=200, headers=headers)
  206. def get_container_operation_log(self,request):
  207. batchlog_id = self.request.query_params.get('batchlog_id')
  208. batch_obj = batchLogModel.objects.get(id=batchlog_id)
  209. container_operation_log = batch_obj.detail_logs.all()
  210. serializer = ContainerDetailLogSerializer(container_operation_log, many=True)
  211. return Response(serializer.data, status=200)
  212. # 进出库log查看
  213. class ContainerDetailLogModelViewSet(viewsets.ModelViewSet):
  214. """
  215. retrieve:
  216. Response a data list(get)
  217. list:
  218. Response a data list(all)
  219. create:
  220. Create a data line(post)
  221. delete:
  222. Delete a data line(delete)
  223. """
  224. # authentication_classes = [] # 禁用所有认证类
  225. # permission_classes = [AllowAny] # 允许任意访问
  226. pagination_class = MyPageNumberPagination
  227. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  228. ordering_fields = ['id', "create_time", "update_time", ]
  229. filter_class = ContainerDetailLogFilter
  230. def get_project(self):
  231. try:
  232. id = self.kwargs.get('pk')
  233. return id
  234. except:
  235. return None
  236. def get_queryset(self):
  237. id = self.get_project()
  238. if self.request.user:
  239. if id is None:
  240. return ContainerDetailLogModel.objects.filter()
  241. else:
  242. return ContainerDetailLogModel.objects.filter(id=id)
  243. else:
  244. return ContainerDetailLogModel.objects.none()
  245. def get_serializer_class(self):
  246. if self.action in ['list', 'destroy','retrieve']:
  247. return ContainerDetailLogSerializer
  248. else:
  249. return self.http_method_not_allowed(request=self.request)
  250. def create(self, request, *args, **kwargs):
  251. data = self.request.data
  252. return Response(data, status=200)
  253. def update(self, request, pk):
  254. qs = self.get_object()
  255. data = self.request.data
  256. serializer = self.get_serializer(qs, data=data)
  257. serializer.is_valid(raise_exception=True)
  258. serializer.save()
  259. headers = self.get_success_headers(serializer.data)
  260. return Response(serializer.data, status=200, headers=headers)
  261. # 托盘列表视图
  262. class ContainerListViewSet(viewsets.ModelViewSet):
  263. """
  264. retrieve:
  265. Response a data list(get)
  266. list:
  267. Response a data list(all)
  268. create:
  269. Create a data line(post)
  270. delete:
  271. Delete a data line(delete)
  272. """
  273. # authentication_classes = [] # 禁用所有认证类
  274. # permission_classes = [AllowAny] # 允许任意访问
  275. pagination_class = MyPageNumberPagination
  276. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  277. ordering_fields = ['id', "create_time", "update_time", ]
  278. filter_class = ContainerListFilter
  279. def get_project(self):
  280. try:
  281. id = self.kwargs.get('pk')
  282. return id
  283. except:
  284. return None
  285. def get_queryset(self):
  286. id = self.get_project()
  287. if self.request.user:
  288. if id is None:
  289. return ContainerListModel.objects.filter()
  290. else:
  291. return ContainerListModel.objects.filter(id=id)
  292. else:
  293. return ContainerListModel.objects.none()
  294. def get_serializer_class(self):
  295. if self.action in ['list', 'destroy','retrieve']:
  296. return ContainerListGetSerializer
  297. elif self.action in ['create', 'update']:
  298. return ContainerListPostSerializer
  299. else:
  300. return self.http_method_not_allowed(request=self.request)
  301. def create(self, request, *args, **kwargs):
  302. # 创建托盘:托盘码五位数字(唯一),当前库位,目标库位,状态,最后操作时间
  303. container_all = ContainerListModel.objects.all().order_by('container_code')
  304. if container_all.count() == 0:
  305. container_code = 12345
  306. else:
  307. container_code = container_all.last().container_code + 1
  308. container_obj = ContainerListModel.objects.create(
  309. container_code=container_code,
  310. current_location='N/A',
  311. target_location='N/A',
  312. status=0,
  313. last_operation=timezone.now()
  314. )
  315. serializer = ContainerListGetSerializer(container_obj)
  316. headers = self.get_success_headers(serializer.data)
  317. try:
  318. log_success_operation(
  319. request=self.request,
  320. operation_content=f"创建托盘列表:{container_code}",
  321. operation_level="new",
  322. operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
  323. object_id=container_obj.id,
  324. module_name="托盘管理"
  325. )
  326. except Exception as e:
  327. pass
  328. return Response(serializer.data, status=201, headers=headers)
  329. def update(self, request, pk):
  330. qs = self.get_object()
  331. data = self.request.data
  332. serializer = self.get_serializer(qs, data=data)
  333. serializer.is_valid(raise_exception=True)
  334. serializer.save()
  335. headers = self.get_success_headers(serializer.data)
  336. try:
  337. log_success_operation(
  338. request=self.request,
  339. operation_content=f"更新托盘列表 ID:{pk}",
  340. operation_level="update",
  341. operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
  342. object_id=pk,
  343. module_name="托盘管理"
  344. )
  345. except Exception as e:
  346. pass
  347. return Response(serializer.data, status=200, headers=headers)
  348. def check_container_postion(self, request, *args, **kwargs):
  349. # 获取查询集
  350. container_list = ContainerListModel.objects.exclude(current_location=F('target_location'))
  351. # 手动应用分页
  352. page = self.paginate_queryset(container_list)
  353. if page is not None:
  354. serializer = ContainerListGetSerializer(page, many=True)
  355. return self.get_paginated_response(serializer.data)
  356. # 如果没有分页,返回完整结果(不推荐)
  357. serializer = ContainerListGetSerializer(container_list, many=True)
  358. return Response(serializer.data, status=200)
  359. def update_container_categories(self, request, *args, **kwargs):
  360. from .utils import update_container_categories_task
  361. update_container_categories_task()
  362. return Response({'message': '托盘分类更新任务已触发'}, status=200)
  363. # wcs任务视图
  364. class WCSTaskViewSet(viewsets.ModelViewSet):
  365. """
  366. retrieve:
  367. Response a data list(get)
  368. list:
  369. Response a data list(all)
  370. create:
  371. Create a data line(post)
  372. delete:
  373. Delete a data line(delete)
  374. """
  375. # authentication_classes = [] # 禁用所有认证类
  376. # permission_classes = [AllowAny] # 允许任意访问
  377. pagination_class = MyPageNumberPagination
  378. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  379. ordering_fields = ['-id', "-create_time", "update_time", ]
  380. filter_class = WCSTaskFilter
  381. def get_project(self):
  382. try:
  383. id = self.kwargs.get('pk')
  384. return id
  385. except:
  386. return None
  387. def get_queryset(self):
  388. id = self.get_project()
  389. if self.request.user:
  390. base_qs = ContainerWCSModel.objects.select_related(
  391. 'batch',
  392. 'batch_out',
  393. 'batch_out__batch_number',
  394. 'batch_out__bound_list',
  395. 'bound_list'
  396. )
  397. if id is None:
  398. return base_qs
  399. else:
  400. return base_qs.filter(id=id)
  401. else:
  402. return ContainerWCSModel.objects.none()
  403. def get_serializer_class(self):
  404. if self.action in ['list', 'destroy','retrieve']:
  405. return WCSTaskGetSerializer
  406. else:
  407. return self.http_method_not_allowed(request=self.request)
  408. def send_task_to_wcs(self, request, *args, **kwargs):
  409. data = self.request.data
  410. task_id = data.get('taskid')
  411. logger.info(f"请求任务:{task_id}")
  412. data_return = {}
  413. try:
  414. task_obj = ContainerWCSModel.objects.get(id=task_id)
  415. if not task_obj:
  416. data_return = {
  417. 'code': '400',
  418. 'message': '任务不存在',
  419. 'data': data
  420. }
  421. log_failure_operation(
  422. request=self.request,
  423. operation_content=f"任务不存在:{task_id}",
  424. operation_level="other",
  425. operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
  426. module_name="WCS任务"
  427. )
  428. return Response(data_return, status=status.HTTP_400_BAD_REQUEST)
  429. if task_obj.working == 1:
  430. OutboundService.send_task_to_wcs(task_obj)
  431. data_return = {
  432. 'code': '200',
  433. 'message': '任务已在执行中,再次下发',
  434. 'data': data
  435. }
  436. else:
  437. data_return = {
  438. 'code': '200',
  439. 'message': '任务已执行完成',
  440. 'data': data
  441. }
  442. except ContainerWCSModel.DoesNotExist:
  443. data_return = {
  444. 'code': '404',
  445. 'message': '任务不存在',
  446. 'data': data
  447. }
  448. log_failure_operation(
  449. request=self.request,
  450. operation_content=f"任务下发失败:{task_id}",
  451. operation_level="other",
  452. operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
  453. module_name="WCS任务"
  454. )
  455. log_success_operation(
  456. request=self.request,
  457. operation_content=f"任务下发成功:{task_id}",
  458. operation_level="other",
  459. operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
  460. module_name="WCS任务"
  461. )
  462. return Response(data_return, status=status.HTTP_200_OK)
  463. # 入库任务视图
  464. class TaskViewSet(viewsets.ModelViewSet):
  465. """
  466. retrieve:
  467. Response a data list(get)
  468. list:
  469. Response a data list(all)
  470. create:
  471. Create a data line(post)
  472. delete:
  473. Delete a data line(delete)
  474. """
  475. pagination_class = MyPageNumberPagination
  476. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  477. ordering_fields = ['id', "create_time", "update_time", ]
  478. filter_class = TaskFilter
  479. def get_project(self):
  480. try:
  481. id = self.kwargs.get('pk')
  482. return id
  483. except:
  484. return None
  485. def get_queryset(self):
  486. id = self.get_project()
  487. if self.request.user:
  488. if id is None:
  489. return TaskModel.objects.filter()
  490. else:
  491. return TaskModel.objects.filter(id=id)
  492. else:
  493. return TaskModel.objects.none()
  494. def get_serializer_class(self):
  495. if self.action in ['list', 'destroy','retrieve']:
  496. return TaskGetSerializer
  497. elif self.action in ['create', 'update']:
  498. return TaskPostSerializer
  499. else:
  500. return self.http_method_not_allowed(request=self.request)
  501. def create(self, request, *args, **kwargs):
  502. data = self.request.data
  503. return Response(data, status=200)
  504. def update(self, request, pk):
  505. qs = self.get_object()
  506. data = self.request.data
  507. serializer = self.get_serializer(qs, data=data)
  508. serializer.is_valid(raise_exception=True)
  509. serializer.save()
  510. headers = self.get_success_headers(serializer.data)
  511. return Response(serializer.data, status=200, headers=headers)
  512. # 任务回滚
  513. class TaskRollbackMixin:
  514. def rollback_task(self, request, task_id, *args, **kwargs):
  515. """
  516. 撤销入库任务并回滚相关状态
  517. """
  518. try:
  519. # 获取任务实例并锁定数据库记录
  520. task = ContainerWCSModel.objects.get(taskid=task_id)
  521. container_code = task.container
  522. target_location = task.target_location
  523. batch = task.batch
  524. # 初始化库位分配器
  525. allocator = LocationAllocation()
  526. # ==================== 库位状态回滚 ====================
  527. # 解析目标库位信息(格式:仓库代码-行-列-层)
  528. try:
  529. warehouse_code, row, col, layer = target_location.split('-')
  530. location = LocationModel.objects.get(
  531. warehouse_code=warehouse_code,
  532. row=int(row),
  533. col=int(col),
  534. layer=int(layer)
  535. )
  536. # 回滚库位状态到可用状态
  537. allocator.update_location_status(location.location_code, 'available')
  538. # 更新库位组状态(需要根据实际逻辑实现)
  539. allocator.update_location_group_status(location.location_code)
  540. # 解除库位与托盘的关联
  541. allocator.update_location_container_link(location.location_code, None)
  542. # 清除库位组的批次关联
  543. allocator.update_location_group_batch(location, None)
  544. except (ValueError, LocationModel.DoesNotExist) as e:
  545. logger.error(f"库位解析失败: {str(e)}")
  546. raise Exception("关联库位信息无效")
  547. # ==================== 批次状态回滚 ====================
  548. if batch:
  549. # 将批次状态恢复为未处理状态(假设原状态为1)
  550. allocator.update_batch_status(batch.bound_number, '1')
  551. # ==================== 托盘状态回滚 ====================
  552. container_obj = ContainerListModel.objects.get(container_code=container_code)
  553. # 恢复托盘详细状态为初始状态(假设原状态为1)
  554. allocator.update_container_detail_status(container_code, 1)
  555. # 恢复托盘的目标位置为当前所在位置
  556. container_obj.target_location = task.current_location
  557. container_obj.save()
  558. # ==================== 删除任务记录 ====================
  559. task.delete()
  560. # ==================== 其他关联清理 ====================
  561. # 如果有其他关联数据(如inport_update_task的操作),在此处添加清理逻辑
  562. return Response(
  563. {'code': '200', 'message': '任务回滚成功', 'data': None},
  564. status=status.HTTP_200_OK
  565. )
  566. except ContainerWCSModel.DoesNotExist:
  567. logger.warning(f"任务不存在: {task_id}")
  568. return Response(
  569. {'code': '404', 'message': '任务不存在', 'data': None},
  570. status=status.HTTP_404_NOT_FOUND
  571. )
  572. except Exception as e:
  573. logger.error(f"任务回滚失败: {str(e)}", exc_info=True)
  574. return Response(
  575. {'code': '500', 'message': '服务器内部错误', 'data': None},
  576. status=status.HTTP_500_INTERNAL_SERVER_ERROR
  577. )
  578. # 入库任务下发
  579. class ContainerWCSViewSet(viewsets.ModelViewSet):
  580. """
  581. retrieve:
  582. Response a data list(get)
  583. list:
  584. Response a data list(all)
  585. create:
  586. Create a data line(post)
  587. delete:
  588. Delete a data line(delete)
  589. """
  590. authentication_classes = [] # 禁用所有认证类
  591. permission_classes = [AllowAny] # 允许任意访问
  592. def generate_move_task(self, request, *args, **kwargs):
  593. data = self.request.data
  594. container = data.get('container_code')
  595. start_location = data.get('start_location')
  596. target_location = data.get('target_location')
  597. logger.info(f"移库请求托盘:{container},起始位置:{start_location},目标位置:{target_location}")
  598. data_return = {}
  599. try:
  600. container_obj = ContainerListModel.objects.filter(container_code=container).first()
  601. if not container_obj:
  602. data_return = {
  603. 'code': '400',
  604. 'message': '托盘编码不存在',
  605. 'data': data
  606. }
  607. # 记录失败日志
  608. try:
  609. log_failure_operation(
  610. request=request,
  611. operation_content=f"移库任务创建失败:托盘编码不存在 - {container}",
  612. operation_level="other",
  613. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  614. module_name="WCS移库管理"
  615. )
  616. except Exception as log_error:
  617. pass
  618. return Response(data_return, status=status.HTTP_400_BAD_REQUEST)
  619. # 检查是否已在目标位置
  620. if target_location == str(container_obj.target_location) and target_location!= '203' and target_location!= '103':
  621. logger.info(f"托盘 {container} 已在目标位置")
  622. data_return = {
  623. 'code': '200',
  624. 'message': '当前位置已是目标位置',
  625. 'data': data
  626. }
  627. else:
  628. # 生成任务
  629. # 查询移库任务,排除已完成和已取消的任务
  630. current_task = ContainerWCSModel.objects.filter(
  631. container=container,
  632. tasktype='inbound',
  633. working=1
  634. ).exclude(status=300).exclude(status=400).first() # 排除已完成和已取消的任务
  635. if current_task:
  636. data_return = {
  637. 'code': '200',
  638. 'message': '任务已存在,重新下发',
  639. 'data': current_task.to_dict()
  640. }
  641. else:
  642. # todo: 这里的入库操作记录里面的记录的数量不对
  643. location_min_value,allocation_target_location, batch_info = AllocationService._move_allocation(start_location, target_location,container)
  644. batch_id = batch_info['number']
  645. if batch_info['class'] == 2:
  646. self.generate_task_no_batch(container, start_location, allocation_target_location,batch_id,location_min_value.c_number)
  647. self.generate_container_operate_no_batch(container_obj, batch_id, allocation_target_location)
  648. elif batch_info['class'] == 3:
  649. self.generate_task_no_batch(container, start_location, allocation_target_location,batch_id,location_min_value.c_number)
  650. self.generate_move_container_operate(container_obj, allocation_target_location)
  651. else:
  652. self.generate_task(container, start_location, allocation_target_location,batch_id,location_min_value.c_number) # 生成任务
  653. self.generate_move_container_operate(container_obj, allocation_target_location)
  654. current_task = ContainerWCSModel.objects.get(
  655. container=container,
  656. tasktype='inbound',
  657. working=1,
  658. )
  659. OutboundService.send_task_to_wcs(current_task)
  660. data_return = {
  661. 'code': '200',
  662. 'message': '任务下发成功',
  663. 'data': current_task.to_dict()
  664. }
  665. container_obj.target_location = allocation_target_location
  666. container_obj.save()
  667. if batch_info['class'] == 1 or batch_info['class'] == 3:
  668. self.inport_update_task(current_task.id, container_obj.id)
  669. http_status = status.HTTP_200_OK if data_return['code'] == '200' else status.HTTP_400_BAD_REQUEST
  670. loggertask.info(f"任务号:{current_task.tasknumber-20000000000},移库请求托盘:{container},起始位置:{start_location},目标位置:{target_location},返回结果:{data_return}")
  671. # 记录操作日志
  672. try:
  673. if data_return['code'] == '200':
  674. task_info = data_return.get('data', {})
  675. task_id = task_info.get('tasknumber', '未知') if isinstance(task_info, dict) else '未知'
  676. log_success_operation(
  677. request=request,
  678. operation_content=f"移库任务创建成功:托盘 {container},起始位置:{start_location},目标位置:{target_location},任务号:{task_id}",
  679. operation_level="other",
  680. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  681. object_id=container_obj.id if container_obj else None,
  682. module_name="WCS移库管理"
  683. )
  684. else:
  685. log_failure_operation(
  686. request=request,
  687. operation_content=f"移库任务创建失败:托盘 {container},错误:{data_return.get('message', '未知错误')}",
  688. operation_level="other",
  689. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  690. module_name="WCS移库管理"
  691. )
  692. except Exception as log_error:
  693. pass
  694. return Response(data_return, status=http_status)
  695. except Exception as e:
  696. logger.error(f"处理请求时发生错误: {str(e)}", exc_info=True)
  697. # 记录异常日志
  698. try:
  699. log_failure_operation(
  700. request=request,
  701. operation_content=f"移库任务处理异常:托盘 {container},错误:{str(e)}",
  702. operation_level="other",
  703. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  704. module_name="WCS移库管理"
  705. )
  706. except Exception as log_error:
  707. pass
  708. return Response(
  709. {'code': '500', 'message': '服务器内部错误', 'data': None},
  710. status=status.HTTP_500_INTERNAL_SERVER_ERROR
  711. )
  712. def generate_out_task(self, request, *args, **kwargs):
  713. data = self.request.data
  714. container = data.get('container_code')
  715. start_location = data.get('current_location')
  716. target_location = data.get('target_location')
  717. logger.info(f"出库请求托盘:{container},起始位置:{start_location},目标位置:{target_location}")
  718. data_return = {}
  719. try:
  720. container_obj = ContainerListModel.objects.filter(container_code=container).first()
  721. if not container_obj:
  722. data_return = {
  723. 'code': '400',
  724. 'message': '托盘编码不存在',
  725. 'data': data
  726. }
  727. # 记录失败日志
  728. try:
  729. log_failure_operation(
  730. request=request,
  731. operation_content=f"出库任务创建失败:托盘编码不存在 - {container}",
  732. operation_level="other",
  733. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  734. module_name="WCS出库管理"
  735. )
  736. except Exception as log_error:
  737. pass
  738. return Response(data_return, status=status.HTTP_400_BAD_REQUEST)
  739. # 检查是否已在目标位置
  740. if target_location == str(container_obj.target_location) :
  741. logger.info(f"托盘 {container} 已在目标位置")
  742. data_return = {
  743. 'code': '200',
  744. 'message': '当前位置已是目标位置',
  745. 'data': data
  746. }
  747. else:
  748. # 生成任务
  749. # 查询出库任务,排除已完成和已取消的任务
  750. current_task = ContainerWCSModel.objects.filter(
  751. container=container,
  752. tasktype='outbound',
  753. working=1
  754. ).exclude(status=300).exclude(status=400).first() # 排除已完成和已取消的任务
  755. if current_task:
  756. data_return = {
  757. 'code': '200',
  758. 'message': '任务已存在,重新下发',
  759. 'data': current_task.to_dict()
  760. }
  761. else:
  762. # todo: 这里的入库操作记录里面的记录的数量不对
  763. allocation_target_location, batch_info = AllocationService._out_allocation(start_location, target_location,container)
  764. batch_id = batch_info['number']
  765. if batch_info['class'] == 2:
  766. self.generate_task_no_batch(container, start_location, allocation_target_location,batch_id,1,'outbound')
  767. self.generate_container_operate_no_batch(container_obj, batch_id, allocation_target_location,"outbound")
  768. elif batch_info['class'] == 3:
  769. self.generate_task_no_batch(container, start_location, allocation_target_location,batch_id,1,'outbound')
  770. self.generate_move_container_operate(container_obj, allocation_target_location,"outbound")
  771. else:
  772. self.generate_task(container, start_location, allocation_target_location,batch_id,1,'outbound') # 生成任务
  773. self.generate_move_container_operate(container_obj, allocation_target_location,"outbound")
  774. current_task = ContainerWCSModel.objects.get(
  775. container=container,
  776. tasktype='outbound',
  777. working=1,
  778. )
  779. OutboundService.send_task_to_wcs(current_task)
  780. data_return = {
  781. 'code': '200',
  782. 'message': '任务下发成功',
  783. 'data': current_task.to_dict()
  784. }
  785. container_obj.target_location = allocation_target_location
  786. container_obj.save()
  787. if batch_info['class'] == 1 or batch_info['class'] == 3:
  788. self.inport_update_task(current_task.id, container_obj.id)
  789. http_status = status.HTTP_200_OK if data_return['code'] == '200' else status.HTTP_400_BAD_REQUEST
  790. loggertask.info(f"任务号:{current_task.tasknumber-20000000000},出库请求托盘:{container}返回结果:{data_return}")
  791. # 记录操作日志
  792. try:
  793. if data_return['code'] == '200':
  794. task_info = data_return.get('data', {})
  795. task_id = task_info.get('tasknumber', '未知') if isinstance(task_info, dict) else '未知'
  796. log_success_operation(
  797. request=request,
  798. operation_content=f"出库任务创建成功:托盘 {container},起始位置:{start_location},目标位置:{target_location},任务号:{task_id}",
  799. operation_level="other",
  800. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  801. object_id=container_obj.id if container_obj else None,
  802. module_name="WCS出库管理"
  803. )
  804. else:
  805. log_failure_operation(
  806. request=request,
  807. operation_content=f"出库任务创建失败:托盘 {container},错误:{data_return.get('message', '未知错误')}",
  808. operation_level="other",
  809. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  810. module_name="WCS出库管理"
  811. )
  812. except Exception as log_error:
  813. pass
  814. return Response(data_return, status=http_status)
  815. except Exception as e:
  816. logger.error(f"处理请求时发生错误: {str(e)}", exc_info=True)
  817. # 记录异常日志
  818. try:
  819. log_failure_operation(
  820. request=request,
  821. operation_content=f"出库任务处理异常:托盘 {container},错误:{str(e)}",
  822. operation_level="other",
  823. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  824. module_name="WCS出库管理"
  825. )
  826. except Exception as log_error:
  827. pass
  828. return Response(
  829. {'code': '500', 'message': '服务器内部错误', 'data': None},
  830. status=status.HTTP_500_INTERNAL_SERVER_ERROR
  831. )
  832. def get_container_wcs(self, request, *args, **kwargs):
  833. data = self.request.data
  834. container = data.get('container_number')
  835. current_location = data.get('current_location')
  836. logger.info(f"入库请求托盘:{container},请求位置:{current_location}")
  837. data_return = {}
  838. try:
  839. container_obj = ContainerListModel.objects.filter(container_code=container).first()
  840. if not container_obj:
  841. data_return = {
  842. 'code': '400',
  843. 'message': '托盘编码不存在',
  844. 'data': data
  845. }
  846. # 记录失败日志
  847. try:
  848. log_failure_operation(
  849. request=request,
  850. operation_content=f"入库请求失败:托盘编码不存在 - {container}",
  851. operation_level="other",
  852. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  853. module_name="WCS入库管理"
  854. )
  855. except Exception as log_error:
  856. pass
  857. return Response(data_return, status=status.HTTP_400_BAD_REQUEST)
  858. # 更新托盘数据(部分更新)
  859. serializer = ContainerListPostSerializer(
  860. container_obj,
  861. data=data,
  862. partial=True # 允许部分字段更新
  863. )
  864. serializer.is_valid(raise_exception=True)
  865. serializer.save()
  866. # 检查是否已在目标位置
  867. if current_location == str(container_obj.target_location) and current_location!= '203' and current_location!= '103':
  868. logger.info(f"托盘 {container} 已在目标位置")
  869. data_return = {
  870. 'code': '200',
  871. 'message': '当前位置已是目标位置',
  872. 'data': data
  873. }
  874. else:
  875. # 查询入库任务,排除已完成和已取消的任务
  876. current_task = ContainerWCSModel.objects.filter(
  877. container=container,
  878. tasktype='inbound',
  879. working=1
  880. ).exclude(status=300).exclude(status=400).first() # 排除已完成和已取消的任务
  881. if current_task:
  882. data_return = {
  883. 'code': '200',
  884. 'message': '任务已存在,重新下发',
  885. 'data': current_task.to_dict()
  886. }
  887. else:
  888. # todo: 这里的入库操作记录里面的记录的数量不对
  889. location_min_value,allocation_target_location, batch_info = AllocationService.allocate(container, current_location)
  890. batch_id = batch_info['number']
  891. if batch_info['class'] == 2:
  892. self.generate_task_no_batch(container, current_location, allocation_target_location,batch_id,location_min_value.c_number)
  893. self.generate_container_operate_no_batch(container_obj, batch_id, allocation_target_location)
  894. elif batch_info['class'] == 3:
  895. self.generate_task_no_batch(container, current_location, allocation_target_location,batch_id,location_min_value.c_number)
  896. self.generate_container_operate(container_obj, allocation_target_location)
  897. else:
  898. self.generate_task(container, current_location, allocation_target_location,batch_id,location_min_value.c_number) # 生成任务
  899. self.generate_container_operate(container_obj, allocation_target_location)
  900. current_task = ContainerWCSModel.objects.get(
  901. container=container,
  902. tasktype='inbound',
  903. working=1,
  904. )
  905. data_return = {
  906. 'code': '200',
  907. 'message': '任务下发成功',
  908. 'data': current_task.to_dict()
  909. }
  910. container_obj.target_location = allocation_target_location
  911. container_obj.save()
  912. if batch_info['class'] == 1 or batch_info['class'] == 3:
  913. self.inport_update_task(current_task.id, container_obj.id)
  914. http_status = status.HTTP_200_OK if data_return['code'] == '200' else status.HTTP_400_BAD_REQUEST
  915. # 尝试从返回数据中获取任务号用于日志
  916. try:
  917. task_info = data_return.get('data', {})
  918. if isinstance(task_info, dict) and 'taskNumber' in task_info:
  919. task_number = task_info['taskNumber'] - 20000000000 if isinstance(task_info['taskNumber'], int) else '未知'
  920. loggertask.info(f"任务号:{task_number},入库请求托盘:{container},起始位置:{current_location},返回结果:{data_return}")
  921. except Exception:
  922. loggertask.info(f"入库请求托盘:{container},起始位置:{current_location},返回结果:{data_return}")
  923. # 记录操作日志
  924. try:
  925. if data_return['code'] == '200':
  926. task_info = data_return.get('data', {})
  927. task_id = task_info.get('taskNumber', '未知') if isinstance(task_info, dict) else '未知'
  928. log_success_operation(
  929. request=request,
  930. operation_content=f"入库任务创建成功:托盘 {container},起始位置:{current_location},任务号:{task_id}",
  931. operation_level="other",
  932. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  933. object_id=container_obj.id if container_obj else None,
  934. module_name="WCS入库管理"
  935. )
  936. else:
  937. log_failure_operation(
  938. request=request,
  939. operation_content=f"入库任务创建失败:托盘 {container},错误:{data_return.get('message', '未知错误')}",
  940. operation_level="other",
  941. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  942. module_name="WCS入库管理"
  943. )
  944. except Exception as log_error:
  945. pass
  946. return Response(data_return, status=http_status)
  947. except Exception as e:
  948. logger.error(f"处理请求时发生错误: {str(e)}", exc_info=True)
  949. # 记录异常日志
  950. try:
  951. log_failure_operation(
  952. request=request,
  953. operation_content=f"入库请求处理异常:托盘 {container},错误:{str(e)}",
  954. operation_level="other",
  955. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  956. module_name="WCS入库管理"
  957. )
  958. except Exception as log_error:
  959. pass
  960. return Response(
  961. {'code': '500', 'message': '服务器内部错误', 'data': None},
  962. status=status.HTTP_500_INTERNAL_SERVER_ERROR
  963. )
  964. # def generate_container_operate(self, container_obj, bound_number,allocation_target_location):
  965. def generate_container_operate(self, container_obj, allocation_target_location):
  966. # 获取托盘中所有有效的批次明细(排除已删除和状态3的)
  967. container_detaillist = ContainerDetailModel.objects.filter(
  968. container=container_obj,
  969. is_delete=False
  970. ).exclude(status=3)
  971. # 优化查询 - 预取批次对象
  972. container_detaillist = container_detaillist.select_related('batch')
  973. # 创建批次数量字典
  974. batch_totals = {}
  975. for detail in container_detaillist:
  976. batch_id = detail.batch_id
  977. if batch_id not in batch_totals:
  978. batch_totals[batch_id] = {
  979. 'obj': detail.batch, # 批次对象
  980. 'total_qty': 0
  981. }
  982. batch_totals[batch_id]['total_qty'] += detail.goods_qty
  983. # 当前月份(单次计算多次使用)
  984. current_month = int(timezone.now().strftime("%Y%m"))
  985. current_time = timezone.now()
  986. current_location = container_obj.current_location
  987. # 为每个批次创建操作记录
  988. for batch_id, data in batch_totals.items():
  989. batch_obj = data['obj']
  990. goods_qty = data['total_qty']
  991. ContainerOperationModel.objects.create(
  992. month=current_month,
  993. container=container_obj,
  994. goods_code=batch_obj.goods_code,
  995. goods_desc=batch_obj.goods_desc,
  996. operation_type="inbound",
  997. batch_id=batch_obj.id,
  998. goods_qty=goods_qty,
  999. goods_weight=goods_qty,
  1000. from_location=current_location,
  1001. to_location=allocation_target_location,
  1002. timestamp=current_time,
  1003. operator="WMS",
  1004. memo=f"WCS入库: 批次: {batch_obj.bound_number}, 数量: {goods_qty}" # 使用实际托盘中的数量
  1005. )
  1006. def generate_move_container_operate(self, container_obj, allocation_target_location,operate_type="adjust"):
  1007. # 获取托盘中所有有效的批次明细
  1008. container_detaillist = ContainerDetailModel.objects.filter(
  1009. container=container_obj,
  1010. is_delete=False
  1011. ).exclude(status=3)
  1012. # 优化查询 - 预取批次对象
  1013. container_detaillist = container_detaillist.select_related('batch')
  1014. # 创建批次数量字典
  1015. batch_totals = {}
  1016. for detail in container_detaillist:
  1017. batch_id = detail.batch_id
  1018. if batch_id not in batch_totals:
  1019. batch_totals[batch_id] = {
  1020. 'obj': detail.batch, # 批次对象
  1021. 'total_qty': 0
  1022. }
  1023. batch_totals[batch_id]['total_qty'] += detail.goods_qty
  1024. # 获取当前时间和位置信息
  1025. current_month = int(timezone.now().strftime("%Y%m"))
  1026. current_time = timezone.now()
  1027. current_location = container_obj.current_location
  1028. # 为每个批次创建操作记录
  1029. for batch_id, data in batch_totals.items():
  1030. batch_obj = data['obj']
  1031. goods_qty = data['total_qty']
  1032. ContainerOperationModel.objects.create(
  1033. month=current_month,
  1034. container=container_obj,
  1035. goods_code=batch_obj.goods_code,
  1036. goods_desc=batch_obj.goods_desc,
  1037. operation_type=operate_type, # 操作类型改为移动
  1038. batch_id=batch_obj.id,
  1039. goods_qty=goods_qty,
  1040. goods_weight=goods_qty,
  1041. from_location=current_location,
  1042. to_location=allocation_target_location,
  1043. timestamp=current_time,
  1044. operator="WMS",
  1045. memo=f"托盘移动: 批次: {batch_obj.bound_number}, 数量: {goods_qty}"
  1046. )
  1047. def generate_move_container_operate_no_batch(self, container_obj, bound_number,allocation_target_location):
  1048. ContainerOperationModel.objects.create(
  1049. month = int(timezone.now().strftime("%Y%m")),
  1050. container = container_obj,
  1051. goods_code = 'container',
  1052. goods_desc = '托盘组',
  1053. operation_type ="adjust",
  1054. goods_qty = 1,
  1055. goods_weight = 0,
  1056. from_location = container_obj.current_location,
  1057. to_location= allocation_target_location,
  1058. timestamp=timezone.now(),
  1059. memo=f"托盘组移库:从{container_obj.current_location}移库到{allocation_target_location}"
  1060. )
  1061. def generate_container_operate_no_batch(self, container_obj, bound_number,allocation_target_location,operate_type="inbound"):
  1062. ContainerOperationModel.objects.create(
  1063. month = int(timezone.now().strftime("%Y%m")),
  1064. container = container_obj,
  1065. goods_code = 'container',
  1066. goods_desc = '托盘组',
  1067. operation_type =operate_type,
  1068. goods_qty = 1,
  1069. goods_weight = 0,
  1070. from_location = container_obj.current_location,
  1071. to_location= allocation_target_location,
  1072. timestamp=timezone.now(),
  1073. memo=f"WCSs手动出库库: 批次: {bound_number}, 数量: 1"
  1074. )
  1075. def generate_task(self, container, current_location, target_location,batch_id,location_c_number,tasktype='inbound'):
  1076. batch = BoundBatchModel.objects.filter(bound_number=batch_id).first()
  1077. batch_detail = BoundDetailModel.objects.filter(bound_batch=batch).first()
  1078. if not batch:
  1079. logger.error(f"批次号 {batch_id} 不存在")
  1080. return False
  1081. data_tosave = {
  1082. 'container': container,
  1083. 'batch': batch,
  1084. 'batch_number': batch_id,
  1085. 'batch_out': None,
  1086. 'bound_list': batch_detail.bound_list,
  1087. 'sequence': 1,
  1088. 'order_number' :location_c_number,
  1089. 'priority': 1,
  1090. 'current_location': current_location,
  1091. 'month': timezone.now().strftime('%Y%m'),
  1092. 'target_location': target_location,
  1093. 'tasktype': tasktype,
  1094. 'status': 103,
  1095. 'is_delete': False
  1096. }
  1097. data_tosave.update(get_task_location_metadata(tasktype, current_location, target_location))
  1098. # 生成唯一递增的 taskid
  1099. last_task = ContainerWCSModel.objects.filter(
  1100. month=data_tosave['month'],
  1101. ).order_by('-tasknumber').first()
  1102. if last_task:
  1103. number_id = last_task.tasknumber + 1
  1104. new_id = f"{number_id:05d}"
  1105. else:
  1106. new_id = "00001"
  1107. number_id = f"{data_tosave['month']}{new_id}"
  1108. data_tosave['taskid'] = f"inbound-{data_tosave['month']}-{new_id}"
  1109. logger.info(f"生成入库任务: {data_tosave['taskid']}")
  1110. # 每月生成唯一递增的 taskNumber
  1111. data_tosave['tasknumber'] = number_id
  1112. ContainerWCSModel.objects.create(**data_tosave)
  1113. def generate_task_no_batch(self, container, current_location, target_location,batch_id,location_c_number,tasktype='inbound'):
  1114. data_tosave = {
  1115. 'container': container,
  1116. 'batch': None,
  1117. 'batch_number': batch_id,
  1118. 'batch_out': None,
  1119. 'bound_list': None,
  1120. 'sequence': 1,
  1121. 'order_number' :location_c_number,
  1122. 'priority': 1,
  1123. 'current_location': current_location,
  1124. 'month': timezone.now().strftime('%Y%m'),
  1125. 'target_location': target_location,
  1126. 'tasktype': tasktype,
  1127. 'status': 103,
  1128. 'is_delete': False
  1129. }
  1130. data_tosave.update(get_task_location_metadata(tasktype, current_location, target_location))
  1131. # 生成唯一递增的 taskid
  1132. last_task = ContainerWCSModel.objects.filter(
  1133. month=data_tosave['month'],
  1134. ).order_by('-tasknumber').first()
  1135. if last_task:
  1136. number_id = last_task.tasknumber + 1
  1137. new_id = f"{number_id:05d}"
  1138. else:
  1139. new_id = "00001"
  1140. number_id = f"{data_tosave['month']}{new_id}"
  1141. data_tosave['taskid'] = f"inbound-{data_tosave['month']}-{new_id}"
  1142. logger.info(f"生成入库任务: {data_tosave['taskid']}")
  1143. # 每月生成唯一递增的 taskNumber
  1144. data_tosave['tasknumber'] = number_id
  1145. ContainerWCSModel.objects.create(**data_tosave)
  1146. def update_container_wcs(self, request, *args, **kwargs):
  1147. data = self.request.data
  1148. loggertask.info(f"WCS返回任务: 任务号:{data.get('taskNumber')},请求托盘:{data.get('container_number')}, 请求位置:{data.get('current_location')},")
  1149. try:
  1150. # 前置校验
  1151. container_obj, error_response = self.validate_container(data)
  1152. if error_response:
  1153. # 记录验证失败日志
  1154. try:
  1155. log_failure_operation(
  1156. request=request,
  1157. operation_content=f"WCS任务更新失败:托盘验证失败 - {data.get('container_number', '未知')}",
  1158. operation_level="update",
  1159. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  1160. module_name="WCS任务管理"
  1161. )
  1162. except Exception as log_error:
  1163. pass
  1164. return error_response
  1165. # 更新托盘数据
  1166. if not self.update_container_data(container_obj, data):
  1167. return Response(
  1168. {'code': '400', 'message': '数据更新失败', 'data': data},
  1169. status=status.HTTP_400_BAD_REQUEST
  1170. )
  1171. # 处理位置逻辑
  1172. task = ContainerWCSModel.objects.filter(
  1173. container=container_obj.container_code,
  1174. tasktype='inbound'
  1175. ).first()
  1176. if self.is_already_at_target(container_obj, data.get('current_location')):
  1177. loggertask.info(f"WMS返回数据:任务号:{task.tasknumber-20000000000},托盘 {container_obj.container_code} 已在目标位置")
  1178. # 记录任务完成日志
  1179. try:
  1180. task_number = data.get('taskNumber', '未知')
  1181. log_success_operation(
  1182. request=request,
  1183. operation_content=f"WCS任务完成:托盘 {container_obj.container_code} 已到达目标位置,任务号:{task_number}",
  1184. operation_level="update",
  1185. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  1186. object_id=container_obj.id,
  1187. module_name="WCS任务管理"
  1188. )
  1189. except Exception as log_error:
  1190. pass
  1191. return self.handle_target_reached(container_obj, data)
  1192. elif task:
  1193. data_return = {
  1194. 'code': '200',
  1195. 'message': '任务已存在,重新下发',
  1196. 'data': task.to_dict()
  1197. }
  1198. loggertask.info(f"WMS返回数据:任务号:{task.tasknumber-20000000000},入库请求托盘:{container_obj.container_code},起始位置:{data.get('current_location')},目标位置:{container_obj.target_location},返回结果:{data_return}")
  1199. return Response(data_return, status=status.HTTP_200_OK)
  1200. else:
  1201. return self.handle_new_allocation(container_obj, data)
  1202. except Exception as e:
  1203. logger.error(f"处理请求时发生错误: {str(e)}", exc_info=True)
  1204. # 记录异常日志
  1205. try:
  1206. log_failure_operation(
  1207. request=request,
  1208. operation_content=f"WCS任务更新异常:托盘 {data.get('container_number', '未知')},任务号:{data.get('taskNumber', '未知')},错误:{str(e)}",
  1209. operation_level="update",
  1210. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  1211. module_name="WCS任务管理"
  1212. )
  1213. except Exception as log_error:
  1214. pass
  1215. return Response({'code': '500', 'message': '服务器内部错误', 'data': None},
  1216. status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  1217. def cancel_task(self, request, *args, **kwargs):
  1218. """取消任务并回滚相关数据 - 支持所有任务类型(入库、移库、出库、检查)"""
  1219. data = self.request.data
  1220. task_number = data.get('task_number')
  1221. container_code = data.get('container_code')
  1222. task_type = data.get('task_type', 'inbound')
  1223. if not task_number:
  1224. return Response({
  1225. 'code': '400',
  1226. 'message': '缺少任务号参数',
  1227. 'success': False
  1228. }, status=status.HTTP_400_BAD_REQUEST)
  1229. if not container_code:
  1230. return Response({
  1231. 'code': '400',
  1232. 'message': '缺少托盘编码参数',
  1233. 'success': False
  1234. }, status=status.HTTP_400_BAD_REQUEST)
  1235. try:
  1236. # 转换任务号格式(前端传入的是减去20000000000后的值)
  1237. full_task_number = task_number + 20000000000
  1238. # 查找任务
  1239. task = ContainerWCSModel.objects.filter(
  1240. tasknumber=full_task_number,
  1241. container=container_code,
  1242. tasktype=task_type,
  1243. working=1
  1244. ).first()
  1245. if not task:
  1246. return Response({
  1247. 'code': '404',
  1248. 'message': '任务不存在或已完成',
  1249. 'success': False
  1250. }, status=status.HTTP_404_NOT_FOUND)
  1251. # 获取托盘对象
  1252. container_obj = ContainerListModel.objects.filter(
  1253. container_code=container_code
  1254. ).first()
  1255. if not container_obj:
  1256. return Response({
  1257. 'code': '404',
  1258. 'message': '托盘不存在',
  1259. 'success': False
  1260. }, status=status.HTTP_404_NOT_FOUND)
  1261. # 使用事务确保原子性
  1262. rollback_details = {} # 收集回滚详情
  1263. with transaction.atomic():
  1264. # 1. 更新任务状态为取消
  1265. task.status = 400 # 使用400表示已取消
  1266. task.message = '任务已取消'
  1267. task.working = 0
  1268. task.save()
  1269. allocator = LocationAllocation()
  1270. # 2. 根据任务类型执行不同的回滚逻辑
  1271. if task_type == 'inbound':
  1272. # 入库任务:回滚库位绑定
  1273. rollback_details = self._rollback_inbound_task(task, container_obj, allocator)
  1274. elif task_type == 'move':
  1275. # 移库任务:回滚库位状态(如果已更新)
  1276. rollback_details = self._rollback_move_task(task, container_obj, allocator)
  1277. elif task_type == 'outbound':
  1278. # 出库任务:回滚出库数量和出库明细
  1279. rollback_details = self._rollback_outbound_task(task, container_obj, allocator)
  1280. elif task_type == 'check':
  1281. # 检查任务:回滚库位状态(如果已更新)
  1282. rollback_details = self._rollback_check_task(task, container_obj, allocator)
  1283. # 3. 更新托盘目标位置为当前位置
  1284. old_target_location = container_obj.target_location
  1285. container_obj.target_location = container_obj.current_location
  1286. container_obj.save()
  1287. # 记录托盘位置变化
  1288. if old_target_location != container_obj.target_location:
  1289. if 'container_changes' not in rollback_details:
  1290. rollback_details['container_changes'] = []
  1291. rollback_details['container_changes'].append({
  1292. 'field': 'target_location',
  1293. 'old_value': old_target_location,
  1294. 'new_value': container_obj.target_location,
  1295. 'description': f'托盘目标位置已更新为当前位置'
  1296. })
  1297. # 4. 记录取消任务日志
  1298. try:
  1299. log_success_operation(
  1300. request=request,
  1301. operation_content=f"取消任务,任务号:{task_number},托盘:{container_code},任务类型:{task_type}",
  1302. operation_level="update",
  1303. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  1304. object_id=task.id,
  1305. module_name="WCS任务管理"
  1306. )
  1307. except Exception as log_error:
  1308. logger.error(f"记录取消任务日志失败: {str(log_error)}")
  1309. return Response({
  1310. 'code': '200',
  1311. 'message': '任务已取消,相关数据已回滚',
  1312. 'success': True,
  1313. 'data': {
  1314. 'task_number': task_number,
  1315. 'container_code': container_code,
  1316. 'task_type': task_type,
  1317. 'rollback_details': rollback_details
  1318. }
  1319. }, status=status.HTTP_200_OK)
  1320. except Exception as e:
  1321. logger.error(f"取消任务失败: {str(e)}", exc_info=True)
  1322. try:
  1323. log_failure_operation(
  1324. request=request,
  1325. operation_content=f"取消任务失败:任务号 {task_number},托盘 {container_code},错误:{str(e)}",
  1326. operation_level="update",
  1327. operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
  1328. module_name="WCS任务管理"
  1329. )
  1330. except Exception as log_error:
  1331. logger.error(f"记录取消任务失败日志失败: {str(log_error)}")
  1332. return Response({
  1333. 'code': '500',
  1334. 'message': f'取消任务失败: {str(e)}',
  1335. 'success': False
  1336. }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  1337. def _rollback_inbound_task(self, task, container_obj, allocator):
  1338. """回滚入库任务相关数据,返回详细的回滚信息"""
  1339. rollback_details = {
  1340. 'location_changes': [],
  1341. 'link_changes': []
  1342. }
  1343. if not task.target_location:
  1344. return rollback_details
  1345. try:
  1346. # 获取目标库位编码
  1347. location_code = self.get_location_code(task.target_location)
  1348. # 解除库位-托盘关联
  1349. link = LocationContainerLink.objects.filter(
  1350. location__location_code=location_code,
  1351. container=container_obj,
  1352. is_active=True
  1353. ).first()
  1354. if link:
  1355. link.is_active = False
  1356. link.save()
  1357. rollback_details['link_changes'].append({
  1358. 'location_code': location_code,
  1359. 'action': '解除关联',
  1360. 'description': f'已解除库位 {location_code} 与托盘 {container_obj.container_code} 的绑定关系'
  1361. })
  1362. # 更新库位状态为可用
  1363. location = LocationModel.objects.filter(
  1364. location_code=location_code
  1365. ).first()
  1366. if location and location.status in ['occupied', 'reserved']:
  1367. old_status = location.status
  1368. location.status = 'available'
  1369. location.save()
  1370. # 更新库位组状态
  1371. allocator.update_location_group_status(location_code)
  1372. rollback_details['location_changes'].append({
  1373. 'location_code': location_code,
  1374. 'old_status': old_status,
  1375. 'new_status': 'available',
  1376. 'description': f'库位 {location_code} 状态已从 {old_status} 恢复为 available'
  1377. })
  1378. logger.info(f"入库任务 {task.taskid} 取消成功,已回滚库位绑定关系")
  1379. return rollback_details
  1380. except Exception as rollback_error:
  1381. logger.error(f"回滚入库任务失败: {str(rollback_error)}")
  1382. raise
  1383. def _rollback_move_task(self, task, container_obj, allocator):
  1384. """回滚移库任务相关数据,返回详细的回滚信息"""
  1385. rollback_details = {
  1386. 'location_changes': [],
  1387. 'link_changes': []
  1388. }
  1389. try:
  1390. # 移库任务如果已经更新了目标库位,需要回滚
  1391. if task.target_location and task.target_location not in ['103', '203']:
  1392. try:
  1393. location_code = self.get_location_code(task.target_location)
  1394. location = LocationModel.objects.filter(
  1395. location_code=location_code
  1396. ).first()
  1397. if location and location.status in ['occupied', 'reserved']:
  1398. old_status = location.status
  1399. # 检查是否有活跃的关联
  1400. active_link = LocationContainerLink.objects.filter(
  1401. location=location,
  1402. container=container_obj,
  1403. is_active=True
  1404. ).first()
  1405. if active_link:
  1406. active_link.is_active = False
  1407. active_link.save()
  1408. rollback_details['link_changes'].append({
  1409. 'location_code': location_code,
  1410. 'action': '解除关联',
  1411. 'description': f'已解除目标库位 {location_code} 与托盘 {container_obj.container_code} 的绑定关系'
  1412. })
  1413. location.status = 'available'
  1414. location.save()
  1415. allocator.update_location_group_status(location_code)
  1416. rollback_details['location_changes'].append({
  1417. 'location_code': location_code,
  1418. 'old_status': old_status,
  1419. 'new_status': 'available',
  1420. 'description': f'目标库位 {location_code} 状态已从 {old_status} 恢复为 available'
  1421. })
  1422. logger.info(f"移库任务 {task.taskid} 取消成功,已回滚目标库位状态")
  1423. except Exception as e:
  1424. logger.warning(f"回滚移库任务目标库位失败: {str(e)}")
  1425. # 回滚起始库位状态(如果已更新为reserved)
  1426. if task.current_location and task.current_location not in ['103', '203']:
  1427. try:
  1428. current_location_code = self.get_location_code(task.current_location)
  1429. current_location = LocationModel.objects.filter(
  1430. location_code=current_location_code
  1431. ).first()
  1432. if current_location and current_location.status == 'reserved':
  1433. old_status = current_location.status
  1434. current_location.status = 'available'
  1435. current_location.save()
  1436. allocator.update_location_group_status(current_location_code)
  1437. rollback_details['location_changes'].append({
  1438. 'location_code': current_location_code,
  1439. 'old_status': old_status,
  1440. 'new_status': 'available',
  1441. 'description': f'起始库位 {current_location_code} 状态已从 {old_status} 恢复为 available'
  1442. })
  1443. except Exception as e:
  1444. logger.warning(f"回滚移库任务起始库位失败: {str(e)}")
  1445. except Exception as rollback_error:
  1446. logger.error(f"回滚移库任务失败: {str(rollback_error)}")
  1447. raise
  1448. return rollback_details
  1449. def _rollback_outbound_task(self, task, container_obj, allocator):
  1450. """回滚出库任务相关数据 - 包括出库数量和出库明细,返回详细的回滚信息"""
  1451. rollback_details = {
  1452. 'batch_changes': [],
  1453. 'out_detail_changes': [],
  1454. 'container_status_changes': [],
  1455. 'total_rollback_qty': 0
  1456. }
  1457. try:
  1458. from decimal import Decimal
  1459. # 1. 回滚出库明细(out_batch_detail)
  1460. out_details = out_batch_detail.objects.filter(
  1461. container=container_obj,
  1462. working=1,
  1463. is_delete=False
  1464. )
  1465. total_rollback_qty = Decimal('0')
  1466. for out_detail in out_details:
  1467. # 回滚托盘明细的出库数量
  1468. container_detail = out_detail.container_detail
  1469. if container_detail:
  1470. # 回滚到上次的出库数量
  1471. rollback_qty = out_detail.out_goods_qty
  1472. old_out_qty = container_detail.goods_out_qty
  1473. container_detail.goods_out_qty = out_detail.last_out_goods_qty
  1474. container_detail.save()
  1475. total_rollback_qty += rollback_qty
  1476. batch_id = container_detail.batch.id if container_detail.batch else '未知'
  1477. batch_number = container_detail.batch.bound_number if container_detail.batch else '未知'
  1478. rollback_details['batch_changes'].append({
  1479. 'batch_id': batch_id,
  1480. 'batch_number': batch_number,
  1481. 'rollback_qty': float(rollback_qty),
  1482. 'old_out_qty': float(old_out_qty),
  1483. 'new_out_qty': float(container_detail.goods_out_qty),
  1484. 'description': f'批次 {batch_number} 出库数量已从 {old_out_qty} 回滚到 {container_detail.goods_out_qty},回滚数量: {rollback_qty}'
  1485. })
  1486. # 创建批次操作日志
  1487. try:
  1488. BatchOperateLogModel.objects.create(
  1489. batch_id=container_detail.batch,
  1490. log_type=1, # 出库日志类型
  1491. log_date=timezone.now(),
  1492. goods_code=container_detail.batch.goods_code if container_detail.batch else '',
  1493. goods_desc=container_detail.batch.goods_desc if container_detail.batch else '',
  1494. goods_qty=-rollback_qty, # 负数表示回滚
  1495. log_content=f"取消出库任务回滚:托盘 {container_obj.container_code} 批次 {batch_id} 回滚数量 {rollback_qty}",
  1496. creater="WMS",
  1497. openid="WMS"
  1498. )
  1499. except Exception as log_error:
  1500. logger.warning(f"创建批次操作日志失败: {str(log_error)}")
  1501. # 标记出库明细为已取消
  1502. out_detail.working = 0
  1503. out_detail.is_delete = True
  1504. out_detail.save()
  1505. rollback_details['out_detail_changes'].append({
  1506. 'out_detail_id': out_detail.id,
  1507. 'action': '已取消',
  1508. 'description': f'出库明细 ID {out_detail.id} 已标记为取消'
  1509. })
  1510. rollback_details['total_rollback_qty'] = float(total_rollback_qty)
  1511. # 2. 回滚库位状态(如果已更新)
  1512. if task.current_location and task.current_location not in ['103', '203']:
  1513. try:
  1514. location_code = self.get_location_code(task.current_location)
  1515. location = LocationModel.objects.filter(
  1516. location_code=location_code
  1517. ).first()
  1518. if location:
  1519. # 检查是否有活跃的关联
  1520. active_link = LocationContainerLink.objects.filter(
  1521. location=location,
  1522. container=container_obj,
  1523. is_active=True
  1524. ).first()
  1525. if not active_link:
  1526. # 如果没有关联,说明可能已经解除了,需要重新绑定
  1527. # 或者库位状态需要恢复
  1528. if location.status == 'available':
  1529. # 如果库位是可用状态,可能需要恢复为占用(如果托盘还在库位)
  1530. pass
  1531. except Exception as e:
  1532. logger.warning(f"回滚出库任务库位状态失败: {str(e)}")
  1533. # 3. 恢复托盘状态(如果已更新为已出库)
  1534. if container_obj.status == 3: # 3表示已出库
  1535. # 根据业务逻辑决定是否恢复状态
  1536. # 如果托盘还有在库的明细,应该恢复为在库状态
  1537. has_in_stock_details = ContainerDetailModel.objects.filter(
  1538. container=container_obj,
  1539. status=2, # 2表示在库
  1540. is_delete=False
  1541. ).exists()
  1542. if has_in_stock_details:
  1543. old_status = container_obj.status
  1544. container_obj.status = 2 # 2表示在库
  1545. container_obj.save()
  1546. rollback_details['container_status_changes'].append({
  1547. 'field': 'status',
  1548. 'old_value': old_status,
  1549. 'new_value': 2,
  1550. 'description': f'托盘状态已从 已出库({old_status}) 恢复为 在库(2)'
  1551. })
  1552. logger.info(f"出库任务 {task.taskid} 取消成功,已回滚出库数量: {total_rollback_qty}")
  1553. except Exception as rollback_error:
  1554. logger.error(f"回滚出库任务失败: {str(rollback_error)}")
  1555. raise
  1556. return rollback_details
  1557. def _rollback_check_task(self, task, container_obj, allocator):
  1558. """回滚检查任务相关数据,返回详细的回滚信息"""
  1559. rollback_details = {
  1560. 'location_changes': []
  1561. }
  1562. try:
  1563. # 检查任务通常不涉及库位状态变更,但如果有,需要回滚
  1564. if task.target_location and task.target_location not in ['103', '203']:
  1565. try:
  1566. location_code = self.get_location_code(task.target_location)
  1567. location = LocationModel.objects.filter(
  1568. location_code=location_code
  1569. ).first()
  1570. if location and location.status == 'reserved':
  1571. old_status = location.status
  1572. location.status = 'available'
  1573. location.save()
  1574. allocator.update_location_group_status(location_code)
  1575. rollback_details['location_changes'].append({
  1576. 'location_code': location_code,
  1577. 'old_status': old_status,
  1578. 'new_status': 'available',
  1579. 'description': f'库位 {location_code} 状态已从 {old_status} 恢复为 available'
  1580. })
  1581. except Exception as e:
  1582. logger.warning(f"回滚检查任务库位状态失败: {str(e)}")
  1583. logger.info(f"检查任务 {task.taskid} 取消成功")
  1584. except Exception as rollback_error:
  1585. logger.error(f"回滚检查任务失败: {str(rollback_error)}")
  1586. raise
  1587. return rollback_details
  1588. # ---------- 辅助函数 ----------
  1589. def validate_container(self, data):
  1590. """验证托盘是否存在"""
  1591. container = data.get('container_number')
  1592. container_obj = ContainerListModel.objects.filter(container_code=container).first()
  1593. if not container_obj:
  1594. return None, Response({
  1595. 'code': '400',
  1596. 'message': '托盘编码不存在',
  1597. 'data': data
  1598. }, status=status.HTTP_400_BAD_REQUEST)
  1599. return container_obj, None
  1600. def update_container_data(self, container_obj, data):
  1601. """更新托盘数据"""
  1602. serializer = ContainerListPostSerializer(
  1603. container_obj,
  1604. data=data,
  1605. partial=True
  1606. )
  1607. if serializer.is_valid():
  1608. serializer.save()
  1609. return True
  1610. return False
  1611. def is_already_at_target(self, container_obj, current_location):
  1612. """检查是否已在目标位置"""
  1613. # print (current_location)
  1614. # print (str(container_obj.target_location))
  1615. return current_location == str(container_obj.target_location)
  1616. def handle_target_reached(self, container_obj, data):
  1617. """处理已到达目标位置的逻辑"""
  1618. logger.info(f"托盘 {container_obj.container_code} 已在目标位置")
  1619. task = self.get_task_by_tasknumber(data)
  1620. self.update_pressure_values(task, container_obj)
  1621. # if task.working == 1:
  1622. # alloca = LocationAllocation()
  1623. # alloca.update_batch_goods_in_location_qty(container_obj.container_code, 1)
  1624. task = self.process_task_completion(data)
  1625. if not task:
  1626. return Response({'code': '400', 'message': '任务不存在', 'data': data},
  1627. status=status.HTTP_400_BAD_REQUEST)
  1628. if task and task.tasktype == 'inbound':
  1629. self.update_storage_system(container_obj)
  1630. if task and task.tasktype == 'outbound' and task.status == 300:
  1631. success = self.handle_outbound_completion(container_obj, task)
  1632. if not success:
  1633. return Response({'code': '500', 'message': '出库状态更新失败', 'data': None},
  1634. status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  1635. # WCS完成一条任务后,只下发一条新任务,优先同楼层
  1636. # 从刚完成的任务中提取楼层信息
  1637. preferred_layer = None
  1638. try:
  1639. parts = str(task.current_location).split('-')
  1640. preferred_layer = parts[3] if len(parts) >= 4 else None
  1641. except Exception:
  1642. pass
  1643. OutboundService.process_next_task(single_task=True, preferred_layer=preferred_layer)
  1644. if task and task.tasktype == 'check' and task.status == 300:
  1645. success = self.handle_outbound_completion(container_obj, task)
  1646. if not success:
  1647. return Response({'code': '500', 'message': '出库状态更新失败', 'data': None},
  1648. status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  1649. # WCS完成一条任务后,只下发一条新任务,优先同楼层
  1650. # 从刚完成的任务中提取楼层信息
  1651. preferred_layer = None
  1652. try:
  1653. parts = str(task.current_location).split('-')
  1654. preferred_layer = parts[3] if len(parts) >= 4 else None
  1655. except Exception:
  1656. pass
  1657. OutboundService.process_next_task(single_task=True, preferred_layer=preferred_layer)
  1658. return Response({
  1659. 'code': '200',
  1660. 'message': '当前位置已是目标位置',
  1661. 'data': data
  1662. }, status=status.HTTP_200_OK)
  1663. def get_task_by_tasknumber(self, data):
  1664. taskNumber = data.get('taskNumber') + 20000000000
  1665. task = ContainerWCSModel.objects.filter(tasknumber=taskNumber).first()
  1666. if task:
  1667. return task
  1668. else:
  1669. return None
  1670. def process_task_completion(self, data):
  1671. """处理任务完成状态"""
  1672. taskNumber = data.get('taskNumber') + 20000000000
  1673. task = ContainerWCSModel.objects.filter(tasknumber=taskNumber).first()
  1674. if task:
  1675. task.status = 300
  1676. task.message = '任务已完成'
  1677. task.working = 0
  1678. task.save()
  1679. try:
  1680. original_task_number = data.get('taskNumber')
  1681. if original_task_number is not None:
  1682. WCSTaskLogModel.objects.filter(
  1683. task_number=original_task_number
  1684. ).update(is_completed=True)
  1685. except Exception as log_error:
  1686. logger.warning(f"更新任务日志完成状态失败: {log_error}")
  1687. return task
  1688. def update_pressure_values(self, task, container_obj):
  1689. """更新压力值计算"""
  1690. if task :
  1691. base_location_obj = base_location.objects.get(id=1)
  1692. layer = int(container_obj.target_location.split('-')[-1])
  1693. pressure_field = f"layer{layer}_pressure"
  1694. logger.info(f"更新压力值,压力字段:{pressure_field}")
  1695. current_pressure = getattr(base_location_obj, pressure_field, 0)
  1696. updated_pressure = max(current_pressure - task.working, 0)
  1697. setattr(base_location_obj, pressure_field, updated_pressure)
  1698. base_location_obj.save()
  1699. def update_storage_system(self, container_obj):
  1700. """更新仓储系统状态"""
  1701. allocator = LocationAllocation()
  1702. location_code = self.get_location_code(container_obj.target_location)
  1703. # 链式更新操作
  1704. update_operations = [
  1705. (allocator.update_location_status, location_code, 'occupied'),
  1706. (allocator.update_location_container_link, location_code, container_obj.container_code),
  1707. (allocator.update_container_detail_status, container_obj.container_code, 2)
  1708. ]
  1709. for func, *args in update_operations:
  1710. if not func(*args):
  1711. logger.error(f"操作失败: {func.__name__}")
  1712. return False
  1713. return True
  1714. def get_location_code(self, target_location):
  1715. """从目标位置解析获取位置编码"""
  1716. parts = target_location.split('-')
  1717. coordinate = f"{int(parts[1])}-{int(parts[2])}-{int(parts[3])}"
  1718. return LocationModel.objects.filter(coordinate=coordinate).first().location_code
  1719. def handle_new_allocation(self, container_obj, data):
  1720. """处理新库位分配逻辑"""
  1721. allocator = LocationAllocation()
  1722. container_code = container_obj.container_code
  1723. # 获取并验证库位分配
  1724. location = allocator.get_location_by_status(container_code, data.get('current_location'))
  1725. if not location or not self.perform_initial_allocation(allocator, location, container_code):
  1726. return Response({'code': '400', 'message': '库位分配失败', 'data': data},
  1727. status=status.HTTP_400_BAD_REQUEST)
  1728. # 生成目标位置并更新托盘
  1729. target_location = self.generate_target_location(location)
  1730. container_obj.target_location = target_location
  1731. container_obj.save()
  1732. # 创建任务并返回响应
  1733. task = self.create_inbound_task(container_code, data, target_location, location)
  1734. return Response({
  1735. 'code': '200',
  1736. 'message': '任务下发成功',
  1737. 'data': task.to_dict()
  1738. }, status=status.HTTP_200_OK)
  1739. def perform_initial_allocation(self, allocator, location, container_code):
  1740. """执行初始库位分配操作"""
  1741. operations = [
  1742. (allocator.update_location_status, location.location_code, 'reserved'),
  1743. (allocator.update_location_group_status, location.location_code),
  1744. (allocator.update_batch_status, container_code, '2'),
  1745. (allocator.update_location_group_batch, location, container_code),
  1746. (allocator.update_location_container_link, location.location_code, container_code),
  1747. (allocator.update_container_detail_status, container_code, 2)
  1748. ]
  1749. for func, *args in operations:
  1750. if not func(*args):
  1751. logger.error(f"分配操作失败: {func.__name__}")
  1752. return False
  1753. return True
  1754. def generate_target_location(self, location):
  1755. """生成目标位置字符串"""
  1756. return (
  1757. f"{location.warehouse_code}-"
  1758. f"{int(location.row):02d}-"
  1759. f"{int(location.col):02d}-"
  1760. f"{int(location.layer):02d}"
  1761. )
  1762. def create_inbound_task(self, container_code, data, target_location, location):
  1763. """创建入库任务"""
  1764. batch_id = LocationAllocation().get_batch(container_code)
  1765. self.generate_task(
  1766. container_code,
  1767. data.get('current_location'),
  1768. target_location,
  1769. batch_id,
  1770. location.c_number
  1771. )
  1772. task = ContainerWCSModel.objects.get(container=container_code, tasktype='inbound')
  1773. self.inport_update_task(task.id, container_code)
  1774. return task
  1775. def inport_update_task(self, wcs_id,container_id):
  1776. try:
  1777. task_obj = ContainerWCSModel.objects.filter(id=wcs_id).first()
  1778. if task_obj:
  1779. container_detail_obj = ContainerDetailModel.objects.filter(container=container_id,is_delete=False).all()
  1780. if container_detail_obj:
  1781. for detail in container_detail_obj:
  1782. # 保存到数据库
  1783. batch = BoundDetailModel.objects.filter(bound_batch_id=detail.batch.id).first()
  1784. TaskModel.objects.create(
  1785. task_wcs = task_obj,
  1786. container_detail = detail,
  1787. batch_detail = batch
  1788. )
  1789. logger.info(f"入库任务 {wcs_id} 已更新")
  1790. else:
  1791. logger.info(f"入库任务 {container_id} 批次不存在")
  1792. else:
  1793. logger.info(f"入库任务 {wcs_id} 不存在")
  1794. except Exception as e:
  1795. logger.error(f"处理入库任务时发生错误: {str(e)}", exc_info=True)
  1796. return Response(
  1797. {'code': '500', 'message': '服务器内部错误', 'data': None},
  1798. status=status.HTTP_500_INTERNAL_SERVER_ERROR
  1799. )
  1800. def release_location(self,request):
  1801. """释放库位"""
  1802. """添加权限管理"""
  1803. token = self.request.META.get('HTTP_TOKEN')
  1804. appid = self.request.META.get('HTTP_APPID')
  1805. if not token:
  1806. return Response({'code': '401', 'message': '请登录', 'data': None},
  1807. status=status.HTTP_200_OK)
  1808. user = StaffListModel.objects.filter(openid=token,appid=appid).first()
  1809. if not user:
  1810. return Response({'code': '401', 'message': '请登录', 'data': None},
  1811. status=status.HTTP_200_OK)
  1812. if user.staff_type not in ['Supervisor', 'Manager','Admin','管理员','经理','主管']:
  1813. return Response({'code': '401', 'message': '无权限', 'data': None},
  1814. status=status.HTTP_200_OK)
  1815. try:
  1816. location_release = request.data.get('location_release')
  1817. location_row = location_release.split('-')[1]
  1818. location_col = location_release.split('-')[2]
  1819. location_layer = location_release.split('-')[3]
  1820. location= LocationModel.objects.filter(row=location_row, col=location_col, layer=location_layer).first()
  1821. location_code = location.location_code
  1822. allocator = LocationAllocation()
  1823. with transaction.atomic():
  1824. if not allocator.release_location(location_code):
  1825. raise Exception("解除库位关联失败")
  1826. if not allocator.update_location_status(location_code, 'available'):
  1827. raise Exception("库位状态更新失败")
  1828. if not allocator.update_location_group_status(location_code):
  1829. raise Exception("库位组状态更新失败")
  1830. # 记录成功日志
  1831. try:
  1832. log_success_operation(
  1833. request=request,
  1834. operation_content=f"释放库位成功:库位编码 {location_code},库位位置 {location_release}",
  1835. operation_level="update",
  1836. operator=user.staff_name if user else None,
  1837. object_id=location.id if location else None,
  1838. module_name="WCS库位管理"
  1839. )
  1840. except Exception as log_error:
  1841. pass
  1842. except Exception as e:
  1843. logger.error(f"解除库位关联失败: {str(e)}")
  1844. # 记录失败日志
  1845. try:
  1846. location_info = request.data.get('location_release', '未知') if hasattr(request, 'data') else '未知'
  1847. log_failure_operation(
  1848. request=request,
  1849. operation_content=f"释放库位失败:库位位置 {location_info},错误:{str(e)}",
  1850. operation_level="update",
  1851. operator=user.staff_name if user else None,
  1852. module_name="WCS库位管理"
  1853. )
  1854. except Exception as log_error:
  1855. pass
  1856. return Response({'code': '500', 'message': e, 'data': None},
  1857. status=status.HTTP_200_OK)
  1858. return Response({'code': '200', 'message': '解除库位关联成功', 'data': None},
  1859. status=status.HTTP_200_OK)
  1860. def handle_outbound_completion(self, container_obj, task):
  1861. """处理出库完成后的库位释放和状态更新"""
  1862. try:
  1863. allocator = LocationAllocation()
  1864. location_task = task.current_location
  1865. if location_task == '103' or location_task == '203':
  1866. return True
  1867. else:
  1868. location_row = location_task.split('-')[1]
  1869. location_col = location_task.split('-')[2]
  1870. location_layer = location_task.split('-')[3]
  1871. location= LocationModel.objects.filter(row=location_row, col=location_col, layer=location_layer).first()
  1872. location_code = location.location_code
  1873. # 事务确保原子性
  1874. with transaction.atomic():
  1875. # 解除库位与托盘的关联
  1876. if not allocator.release_location(location_code):
  1877. raise Exception("解除库位关联失败")
  1878. # 更新库位状态为可用
  1879. if not allocator.update_location_status(location_code, 'available'):
  1880. raise Exception("库位状态更新失败")
  1881. # 更新库位组的统计信息
  1882. self.handle_group_location_status(location_code, location.location_group)
  1883. # 更新托盘状态为已出库(假设状态3表示已出库)
  1884. container_obj.status = 3
  1885. container_obj.save()
  1886. return True
  1887. except Exception as e:
  1888. logger.error(f"出库完成处理失败: {str(e)}")
  1889. return False
  1890. def handle_group_location_status(self,location_code,location_group):
  1891. """
  1892. 处理库位组和库位的关联关系
  1893. :param location_code: 库位编码
  1894. :param location_group: 库位组编码
  1895. :return:
  1896. """
  1897. # 1. 获取库位空闲状态的库位数目
  1898. location_obj_number = LocationModel.objects.filter(
  1899. location_group=location_group,
  1900. status='available'
  1901. ).all().count()
  1902. # 2. 获取库位组对象
  1903. logger.info(f"库位组 {location_group} 下的库位数目:{location_obj_number}")
  1904. # 1. 获取库位和库位组的关联关系
  1905. location_group_obj = LocationGroupModel.objects.filter(
  1906. group_code=location_group
  1907. ).first()
  1908. if not location_group_obj:
  1909. logger.info(f"库位组 {location_group} 不存在")
  1910. return None
  1911. else:
  1912. if location_obj_number == 0:
  1913. # 库位组库位已满,更新库位组状态为full
  1914. location_group_obj.status = 'full'
  1915. location_group_obj.save()
  1916. elif location_obj_number < location_group_obj.max_capacity:
  1917. location_group_obj.status = 'occupied'
  1918. location_group_obj.save()
  1919. else:
  1920. location_group_obj.status = 'available'
  1921. location_group_obj.current_batch = ''
  1922. location_group_obj.current_goods_code = ''
  1923. location_group_obj.save()
  1924. # PDA组盘入库 将扫描到的托盘编码和批次信息保存到数据库
  1925. # 1. 先查询托盘对象,如果不存在,则创建托盘对象
  1926. # 2. 循环处理每个批次,查询批次对象,
  1927. # 3. 更新批次数据(根据业务规则)
  1928. # 4. 保存到数据库
  1929. # 5. 保存操作记录到数据库
  1930. class ContainerDetailViewSet(viewsets.ModelViewSet):
  1931. """
  1932. retrieve:
  1933. Response a data list(get)
  1934. list:
  1935. Response a data list(all)
  1936. create:
  1937. Create a data line(post)
  1938. delete:
  1939. Delete a data line(delete)
  1940. """
  1941. # authentication_classes = [] # 禁用所有认证类
  1942. # permission_classes = [AllowAny] # 允许任意访问
  1943. pagination_class = MyPageNumberPagination
  1944. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  1945. ordering_fields = ['id', "create_time", "update_time", ]
  1946. filter_class = ContainerDetailFilter
  1947. def get_project(self):
  1948. try:
  1949. id = self.kwargs.get('pk')
  1950. return id
  1951. except:
  1952. return None
  1953. def get_queryset(self):
  1954. id = self.get_project()
  1955. if self.request.user:
  1956. if id is None:
  1957. return ContainerDetailModel.objects.filter( is_delete=False)
  1958. else:
  1959. return ContainerDetailModel.objects.filter( id=id, is_delete=False)
  1960. else:
  1961. return ContainerDetailModel.objects.none()
  1962. def get_serializer_class(self):
  1963. if self.action in ['list', 'destroy','retrieve']:
  1964. return ContainerDetailGetSerializer
  1965. elif self.action in ['create']:
  1966. return ContainerDetailPostSerializer
  1967. elif self.action in ['update']:
  1968. return ContainerDetailPutSerializer
  1969. else:
  1970. return self.http_method_not_allowed(request=self.request)
  1971. def create(self, request, *args, **kwargs):
  1972. data = self.request.data
  1973. from .container_operate import ContainerService
  1974. ContainerService.create_container_operation(data,logger=logger)
  1975. # 将处理后的数据返回(或根据业务需求保存到数据库)
  1976. res_data={
  1977. "code": "200",
  1978. "msg": "Success Create",
  1979. "data": data
  1980. }
  1981. try:
  1982. log_success_operation(
  1983. request=self.request,
  1984. operation_content=f"创建托盘操作:{data.get('container_code', '未知')}",
  1985. operation_level="new",
  1986. operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
  1987. module_name="托盘管理"
  1988. )
  1989. except Exception as e:
  1990. pass
  1991. return Response(res_data, status=200)
  1992. def update(self, request, pk):
  1993. qs = self.get_object()
  1994. data = self.request.data
  1995. serializer = self.get_serializer(qs, data=data)
  1996. serializer.is_valid(raise_exception=True)
  1997. serializer.save()
  1998. headers = self.get_success_headers(serializer.data)
  1999. try:
  2000. log_success_operation(
  2001. request=self.request,
  2002. operation_content=f"更新托盘详情 ID:{pk}",
  2003. operation_level="update",
  2004. operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
  2005. object_id=pk,
  2006. module_name="托盘管理"
  2007. )
  2008. except Exception as e:
  2009. pass
  2010. return Response(serializer.data, status=200, headers=headers)
  2011. def destroy(self, request, pk):
  2012. qs = self.get_object()
  2013. qs.is_delete = True
  2014. qs.save()
  2015. try:
  2016. log_success_operation(
  2017. request=self.request,
  2018. operation_content=f"删除托盘详情 ID:{pk}",
  2019. operation_level="delete",
  2020. operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
  2021. object_id=pk,
  2022. module_name="托盘管理"
  2023. )
  2024. except Exception as e:
  2025. pass
  2026. return Response({'code': 200,'message': '删除成功', 'data': None}, status=200)
  2027. def containerdetail_list(self, request):
  2028. """
  2029. 获取托盘详情列表
  2030. """
  2031. try:
  2032. container_id = request.query_params.get('container')
  2033. if not container_id:
  2034. return Response(
  2035. {'code': 400, 'message': '缺少托盘ID参数', 'data': None},
  2036. status=status.HTTP_400_BAD_REQUEST
  2037. )
  2038. # 获取托盘对象
  2039. try:
  2040. container = ContainerListModel.objects.get(id=container_id)
  2041. except ContainerListModel.DoesNotExist:
  2042. return Response(
  2043. {'code': 404, 'message': '指定托盘不存在', 'data': None},
  2044. status=status.HTTP_404_NOT_FOUND
  2045. )
  2046. # 查询关联批次明细(排除状态0和3)
  2047. details = ContainerDetailModel.objects.filter(
  2048. container=container,is_delete=False
  2049. ).exclude(
  2050. status__in=[0, 3]
  2051. ).select_related('batch')
  2052. details_serializer = ContainerDetailSimpleGetSerializer(details, many=True)
  2053. return Response(
  2054. {'code': 200, 'message': 'Success', 'data': details_serializer.data},
  2055. status=status.HTTP_200_OK
  2056. )
  2057. except Exception as e:
  2058. return Response(
  2059. {'code': 500, 'message': f'服务器错误: {str(e)}', 'data': None},
  2060. status=status.HTTP_500_INTERNAL_SERVER_ERROR
  2061. )
  2062. def locationdetail_list(self, request):
  2063. """
  2064. 获取库位所处托盘的信息(按批次号 + 数量分组)
  2065. 新增批次总量统计功能
  2066. """
  2067. try:
  2068. container_id = request.query_params.get('container')
  2069. if not container_id:
  2070. return Response(
  2071. {'code': 400, 'message': '缺少托盘ID参数', 'data': None},
  2072. status=status.HTTP_400_BAD_REQUEST
  2073. )
  2074. # 获取托盘对象
  2075. try:
  2076. container = ContainerListModel.objects.get(id=container_id)
  2077. except ContainerListModel.DoesNotExist:
  2078. return Response(
  2079. {'code': 404, 'message': '指定托盘不存在', 'data': None},
  2080. status=status.HTTP_404_NOT_FOUND
  2081. )
  2082. # 查询关联批次明细(排除状态0和3)
  2083. details = ContainerDetailModel.objects.filter(
  2084. container=container,is_delete=False
  2085. ).exclude(
  2086. status__in=[0, 3]
  2087. ).select_related('batch')
  2088. if not details.exists():
  2089. return Response(
  2090. {'code': 404, 'message': '未找到有效批次数据', 'data': None},
  2091. status=status.HTTP_404_NOT_FOUND
  2092. )
  2093. # 按批次号 + 数量分组统计
  2094. batch_dict = {}
  2095. batch_qty_dict = defaultdict(int) # 使用默认字典自动初始化
  2096. for detail in details:
  2097. if not detail.batch:
  2098. continue
  2099. bound_number = detail.batch.bound_number
  2100. goods_qty = detail.goods_qty - detail.goods_out_qty # 剔除出库数量
  2101. # 组合键:批次号 + 当前数量
  2102. batch_key = (bound_number, goods_qty)
  2103. batch_qty_dict[bound_number] += goods_qty # 自动处理键初始化
  2104. # 分组统计
  2105. if batch_key not in batch_dict:
  2106. batch_obj = BoundBatchModel.objects.filter( bound_number=bound_number).first()
  2107. batch_dict[batch_key] = {
  2108. "goods_code": detail.goods_code,
  2109. "goods_desc": detail.goods_desc,
  2110. "goods_qty": goods_qty,
  2111. "goods_class": detail.goods_class,
  2112. "goods_package": batch_obj.goods_package,
  2113. "batch_total_qty": batch_obj.goods_qty,
  2114. "batch_total_in_qty": batch_obj.goods_in_qty - batch_obj.goods_out_qty,
  2115. "status": detail.status,
  2116. "group_qty": 1,
  2117. "create_time": detail.create_time,
  2118. }
  2119. else:
  2120. batch_dict[batch_key]["group_qty"] += 1
  2121. # 重构数据结构
  2122. results = []
  2123. for (bound_number, qty), data in batch_dict.items():
  2124. results.append({
  2125. **data,
  2126. "bound_number": bound_number,
  2127. "current_qty": qty,
  2128. "total_batch_qty": batch_qty_dict[bound_number] # 添加批次总量
  2129. })
  2130. batch_totals =[]
  2131. for bound_number, qty in batch_qty_dict.items():
  2132. batch_totals.append({
  2133. "bound_number": bound_number,
  2134. "total_batch_qty": qty
  2135. })
  2136. return Response(
  2137. {
  2138. "code": 200,
  2139. "message": "Success",
  2140. "data": {
  2141. "container_code": container.container_code,
  2142. "count": len(results),
  2143. "results": results,
  2144. "batch_totals": batch_totals # 可选:单独返回批次总量
  2145. }
  2146. },
  2147. status=status.HTTP_200_OK
  2148. )
  2149. except Exception as e:
  2150. return Response(
  2151. {'code': 500, 'message': f'服务器错误: {str(e)}', 'data': None},
  2152. status=status.HTTP_500_INTERNAL_SERVER_ERROR
  2153. )
  2154. def pdadetail_list(self, request):
  2155. """
  2156. 获取PDA组盘入库的托盘详情列表
  2157. """
  2158. try:
  2159. container_code = request.query_params.get('container_code')
  2160. if not container_code:
  2161. return Response(
  2162. {'code': 400, 'message': '缺少托盘编码参数', 'data': None},
  2163. status=status.HTTP_200_OK
  2164. )
  2165. # 获取托盘对象
  2166. try:
  2167. container = ContainerListModel.objects.get(container_code=container_code)
  2168. except ContainerListModel.DoesNotExist:
  2169. return Response(
  2170. {'code': 404, 'message': '指定托盘不存在', 'data': None},
  2171. status=status.HTTP_200_OK
  2172. )
  2173. # 查询关联批次明细(排除状态0和3)
  2174. details = ContainerDetailModel.objects.filter(
  2175. container=container,is_delete=False
  2176. ).exclude(
  2177. status__in=[0, 3]
  2178. ).select_related('batch')
  2179. if not details.exists():
  2180. return Response(
  2181. {'code': 404, 'message': '未找到有效批次数据', 'data': None},
  2182. status=status.HTTP_200_OK
  2183. )
  2184. # 按批次号 + 数量分组统计
  2185. batch_dict = {}
  2186. batch_qty_dict = defaultdict(int) # 使用默认字典自动初始化
  2187. for detail in details:
  2188. if not detail.batch:
  2189. continue
  2190. bound_number = detail.batch.bound_number
  2191. goods_qty = detail.goods_qty - detail.goods_out_qty # 剔除出库数量
  2192. # 组合键:批次号 + 当前数量
  2193. batch_key = (bound_number, goods_qty)
  2194. batch_qty_dict[bound_number] += goods_qty # 自动处理键初始化
  2195. # 分组统计
  2196. if batch_key not in batch_dict:
  2197. batch_obj = BoundBatchModel.objects.filter( bound_number=bound_number).first()
  2198. batch_dict[batch_key] = {
  2199. "goods_code": detail.goods_code,
  2200. "goods_desc": detail.goods_desc,
  2201. "goods_qty": goods_qty,
  2202. "goods_class": detail.goods_class,
  2203. "goods_package": batch_obj.goods_package,
  2204. "batch_total_qty": batch_obj.goods_qty,
  2205. "batch_total_in_qty": batch_obj.goods_in_qty - batch_obj.goods_out_qty,
  2206. "status": detail.status,
  2207. "group_qty": 1,
  2208. "create_time": detail.create_time,
  2209. }
  2210. else:
  2211. batch_dict[batch_key]["group_qty"] += 1
  2212. # 重构数据结构
  2213. results = []
  2214. for (bound_number, qty), data in batch_dict.items():
  2215. results.append({
  2216. **data,
  2217. "bound_number": bound_number,
  2218. "current_qty": qty,
  2219. "total_batch_qty": batch_qty_dict[bound_number] # 添加批次总量
  2220. })
  2221. batch_totals =[]
  2222. for bound_number, qty in batch_qty_dict.items():
  2223. batch_totals.append({
  2224. "bound_number": bound_number,
  2225. "total_batch_qty": qty
  2226. })
  2227. return Response(
  2228. {
  2229. "code": 200,
  2230. "message": "Success",
  2231. "data": {
  2232. "count": len(results),
  2233. "results": results,
  2234. "batch_totals": batch_totals # 可选:单独返回批次总量
  2235. }
  2236. },
  2237. status=status.HTTP_200_OK
  2238. )
  2239. except Exception as e:
  2240. return Response(
  2241. {'code': 500, 'message': f'服务器错误: {str(e)}', 'data': None},
  2242. status=status.HTTP_200_OK
  2243. )
  2244. # 托盘操作历史记录
  2245. class ContainerOperateViewSet(viewsets.ModelViewSet):
  2246. """
  2247. retrieve:
  2248. Response a data list(get)
  2249. list:
  2250. Response a data list(all)
  2251. create:
  2252. Create a data line(post)
  2253. delete:
  2254. Delete a data line(delete)
  2255. """
  2256. # authentication_classes = [] # 禁用所有认证类
  2257. # permission_classes = [AllowAny] # 允许任意访问
  2258. pagination_class = MyPageNumberPagination
  2259. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  2260. ordering_fields = ['id', "timestamp" ]
  2261. filter_class = ContainerOperationFilter
  2262. def get_project(self):
  2263. try:
  2264. id = self.kwargs.get('pk')
  2265. return id
  2266. except:
  2267. return None
  2268. def get_queryset(self):
  2269. id = self.get_project()
  2270. if self.request.user:
  2271. if id is None:
  2272. return ContainerOperationModel.objects.filter( is_delete=False)
  2273. else:
  2274. return ContainerOperationModel.objects.filter( id=id, is_delete=False)
  2275. else:
  2276. return ContainerOperationModel.objects.none()
  2277. def get_serializer_class(self):
  2278. if self.action in ['list', 'destroy','retrieve']:
  2279. return ContainerOperationGetSerializer
  2280. elif self.action in ['create', 'update']:
  2281. return ContainerOperationPostSerializer
  2282. else:
  2283. return self.http_method_not_allowed(request=self.request)
  2284. def create(self, request, *args, **kwargs):
  2285. data = self.request.data
  2286. try:
  2287. serializer = self.get_serializer(data=data)
  2288. serializer.is_valid(raise_exception=True)
  2289. serializer.save()
  2290. headers = self.get_success_headers(serializer.data)
  2291. # 记录成功日志
  2292. try:
  2293. log_success_operation(
  2294. request=self.request,
  2295. operation_content=f"创建托盘操作记录:{data.get('container_code', '未知')}",
  2296. operation_level="new",
  2297. operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
  2298. object_id=serializer.data.get('id'),
  2299. module_name="托盘管理"
  2300. )
  2301. except Exception as e:
  2302. pass
  2303. return Response(serializer.data, status=200, headers=headers)
  2304. except Exception as e:
  2305. # 记录失败日志
  2306. try:
  2307. log_failure_operation(
  2308. request=self.request,
  2309. operation_content=f"创建托盘操作记录失败:{str(e)}",
  2310. operation_level="new",
  2311. operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
  2312. module_name="托盘管理"
  2313. )
  2314. except Exception as log_error:
  2315. pass
  2316. raise
  2317. def update(self, request, pk):
  2318. qs = self.get_object()
  2319. data = self.request.data
  2320. try:
  2321. serializer = self.get_serializer(qs, data=data)
  2322. serializer.is_valid(raise_exception=True)
  2323. serializer.save()
  2324. headers = self.get_success_headers(serializer.data)
  2325. # 记录成功日志
  2326. try:
  2327. log_success_operation(
  2328. request=self.request,
  2329. operation_content=f"更新托盘操作记录 ID:{pk}",
  2330. operation_level="update",
  2331. operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
  2332. object_id=pk,
  2333. module_name="托盘管理"
  2334. )
  2335. except Exception as e:
  2336. pass
  2337. return Response(serializer.data, status=200, headers=headers)
  2338. except Exception as e:
  2339. # 记录失败日志
  2340. try:
  2341. log_failure_operation(
  2342. request=self.request,
  2343. operation_content=f"更新托盘操作记录失败 ID:{pk},错误: {str(e)}",
  2344. operation_level="update",
  2345. operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
  2346. object_id=pk,
  2347. module_name="托盘管理"
  2348. )
  2349. except Exception as log_error:
  2350. pass
  2351. raise
  2352. # 出库任务生成
  2353. class OutboundService:
  2354. @staticmethod
  2355. def generate_task_id():
  2356. """生成唯一任务ID(格式: outbound-年月-顺序号)"""
  2357. month = timezone.now().strftime("%Y%m")
  2358. last_task = ContainerWCSModel.objects.filter(
  2359. tasktype='outbound',
  2360. month=int(month)
  2361. ).order_by('-sequence').first()
  2362. sequence = last_task.sequence + 1 if last_task else 1
  2363. return f"outbound-{month}-{sequence:05d}"
  2364. @staticmethod
  2365. def send_task_to_wcs(task):
  2366. """异步发送任务到WCS(非阻塞版本)"""
  2367. reference_location = select_reference_location(
  2368. task.tasktype,
  2369. task.current_location,
  2370. task.target_location
  2371. )
  2372. if task.location_group_id is None or task.order_number in (None, 0):
  2373. location_meta = resolve_location_group_metadata(reference_location)
  2374. update_fields = []
  2375. if task.location_group_id is None and location_meta['location_group_id'] is not None:
  2376. task.location_group_id = location_meta['location_group_id']
  2377. update_fields.append('location_group_id')
  2378. if task.order_number in (None, 0) and location_meta['order_number'] not in (None, 0):
  2379. task.order_number = location_meta['order_number']
  2380. update_fields.append('order_number')
  2381. if update_fields:
  2382. task.save(update_fields=update_fields)
  2383. # 提取任务关键数据用于线程(避免直接传递ORM对象)
  2384. task_data = {
  2385. 'task_id': task.pk, # 使用主键而不是对象
  2386. 'send_data': {
  2387. "code":'200',
  2388. "message": task.message,
  2389. "data":{
  2390. "taskid": task.taskid,
  2391. "container": task.container,
  2392. "current_location": task.current_location,
  2393. "target_location": task.target_location,
  2394. "tasktype": task.tasktype,
  2395. "month": task.month,
  2396. "message": task.message,
  2397. "status": task.status,
  2398. "taskNumber": task.tasknumber-20000000000,
  2399. "order_number":task.order_number,
  2400. "sequence":task.sequence,
  2401. "location_group_id": task.location_group_id or DEFAULT_LOCATION_GROUP_ID,
  2402. }
  2403. }
  2404. }
  2405. loggertask.info(f"任务号:{task.tasknumber-20000000000}任务发送请求:{task.container},起始位置:{task.current_location},目标位置:{task.target_location},返回结果:{task_data}")
  2406. # 异步记录日志到数据库(不阻塞发送)
  2407. log_thread = threading.Thread(
  2408. target=OutboundService._async_log_handler,
  2409. kwargs={
  2410. 'task_number': task.tasknumber - 20000000000,
  2411. 'container': str(task.container),
  2412. 'current_location': task.current_location,
  2413. 'target_location': task.target_location,
  2414. 'task_type': task.tasktype,
  2415. 'order_number': task.order_number,
  2416. 'sequence': task.sequence,
  2417. 'response_data': task_data,
  2418. 'log_type': task.tasktype or 'outbound',
  2419. 'location_group_id': task.location_group_id or DEFAULT_LOCATION_GROUP_ID,
  2420. },
  2421. daemon=True
  2422. )
  2423. log_thread.start()
  2424. # 创建并启动线程
  2425. thread = threading.Thread(
  2426. target=OutboundService._async_send_handler,
  2427. kwargs=task_data,
  2428. daemon=True # 守护线程(主程序退出时自动终止)
  2429. )
  2430. thread.start()
  2431. return True # 立即返回表示已开始处理
  2432. @staticmethod
  2433. def _extract_layer(location):
  2434. """解析库位字符串中的楼层信息"""
  2435. try:
  2436. parts = str(location).split('-')
  2437. return parts[3] if len(parts) >= 4 else None
  2438. except Exception:
  2439. return None
  2440. @staticmethod
  2441. def _get_running_layers():
  2442. """获取当前正在执行中的出库任务所在楼层"""
  2443. running_locations = ContainerWCSModel.objects.filter(
  2444. tasktype='outbound',
  2445. status__gt=100,
  2446. status__lt=300,
  2447. working=1,
  2448. is_delete=False
  2449. ).values_list('current_location', flat=True)
  2450. active_layers = set()
  2451. for location in running_locations:
  2452. layer = OutboundService._extract_layer(location)
  2453. if layer:
  2454. active_layers.add(layer)
  2455. return active_layers
  2456. @staticmethod
  2457. def _async_log_handler(task_number, container, current_location, target_location, task_type, order_number, sequence, response_data, log_type='outbound', location_group_id=None):
  2458. """异步记录 WCS 任务发送日志到数据库(不阻塞发送)"""
  2459. try:
  2460. close_old_connections()
  2461. # 解析库位组与优先级
  2462. group_id = location_group_id
  2463. order_number_value = order_number
  2464. left_priority = None
  2465. right_priority = None
  2466. floor = None
  2467. group_obj = None
  2468. if group_id not in (None, 0):
  2469. group_obj = LocationGroupModel.objects.filter(
  2470. id=group_id
  2471. ).only('id', 'group_code', 'left_priority', 'right_priority').first()
  2472. if group_obj:
  2473. left_priority = group_obj.left_priority
  2474. right_priority = group_obj.right_priority
  2475. if group_obj is None or order_number_value in (None, 0):
  2476. try:
  2477. parts = current_location.split('-')
  2478. if len(parts) >= 4:
  2479. row = int(parts[1])
  2480. col = int(parts[2])
  2481. layer = int(parts[3])
  2482. floor = parts[3]
  2483. loc = LocationModel.objects.filter(row=row, col=col, layer=layer).only(
  2484. 'location_group', 'c_number'
  2485. ).first()
  2486. if loc:
  2487. group = group_obj or LocationGroupModel.objects.filter(
  2488. group_code=loc.location_group
  2489. ).first()
  2490. if group:
  2491. group_obj = group
  2492. group_id = group.id
  2493. left_priority = group.left_priority
  2494. right_priority = group.right_priority
  2495. if order_number_value in (None, 0):
  2496. order_number_value = loc.c_number or DEFAULT_ORDER_NUMBER
  2497. except Exception as e:
  2498. logger.error(f"解析库位组信息失败: {e}")
  2499. if group_id in (None, 0) or order_number_value in (None, 0):
  2500. metadata = resolve_location_group_metadata(current_location)
  2501. if group_id in (None, 0):
  2502. group_id = metadata['location_group_id']
  2503. if order_number_value in (None, 0):
  2504. order_number_value = metadata.get('order_number', DEFAULT_ORDER_NUMBER)
  2505. # 创建日志记录
  2506. WCSTaskLogModel.objects.create(
  2507. task_number=task_number,
  2508. container=container,
  2509. current_location=current_location,
  2510. target_location=target_location,
  2511. location_group_id=group_id or DEFAULT_LOCATION_GROUP_ID,
  2512. left_priority=left_priority,
  2513. right_priority=right_priority,
  2514. task_type=task_type,
  2515. order_number=order_number_value or DEFAULT_ORDER_NUMBER,
  2516. sequence=sequence,
  2517. response_data=response_data,
  2518. floor=floor,
  2519. log_type=log_type or task_type or 'outbound',
  2520. )
  2521. except Exception as e:
  2522. logger.error(f"记录 WCS 任务日志失败: {e}", exc_info=True)
  2523. @staticmethod
  2524. def _async_send_handler(task_id, send_data):
  2525. """异步处理的实际工作函数"""
  2526. try:
  2527. # 每个线程需要独立的数据库连接
  2528. close_old_connections()
  2529. # 重新获取任务对象(确保使用最新数据)
  2530. task = ContainerWCSModel.objects.get(pk=task_id)
  2531. # 发送第一个请求(不处理结果)
  2532. # requests.post(
  2533. # "http://127.0.0.1:8008/container/batch/",
  2534. # json=send_data,
  2535. # timeout=10
  2536. # )
  2537. #
  2538. # 发送关键请求
  2539. response = requests.post(
  2540. "http://192.168.18.200:1616/wcs/WebApi/getOutTask",
  2541. json=send_data,
  2542. timeout=10
  2543. )
  2544. # 处理响应
  2545. if response.status_code == 200:
  2546. task.status = 200
  2547. task.save()
  2548. logger.info(f"任务 {task.taskid} 已发送")
  2549. else:
  2550. logger.error(f"WCS返回错误: {response.text}")
  2551. except Exception as e:
  2552. logger.error(f"发送失败: {str(e)}")
  2553. finally:
  2554. close_old_connections() # 清理数据库连接
  2555. @staticmethod
  2556. def create_initial_tasks(container_list,bound_list_id):
  2557. """生成初始任务队列,返回楼层信息用于初始化发送"""
  2558. with transaction.atomic():
  2559. # 查询出库任务,排除已取消的任务(status=400表示已取消)
  2560. # status__lt=300 已经排除了 status=400,但为了明确性,仍然添加 exclude
  2561. current_WCS = ContainerWCSModel.objects.filter(
  2562. tasktype='outbound',
  2563. bound_list_id=bound_list_id,
  2564. is_delete=False,
  2565. status__lt=300
  2566. ).exclude(status=400).first() # 明确排除已取消的任务
  2567. if current_WCS:
  2568. logger.error(f"当前{bound_list_id}已有出库任务")
  2569. return {
  2570. "success": False,
  2571. "msg": f"出库申请 {bound_list_id} 仍有待完成任务",
  2572. }
  2573. tasks = []
  2574. task_layers = set()
  2575. start_sequence = ContainerWCSModel.objects.filter(tasktype='outbound').count() + 1
  2576. tasknumber = ContainerWCSModel.objects.filter().count()
  2577. tasknumber_index = 1
  2578. for index, container in enumerate(container_list, start=start_sequence):
  2579. container_obj = ContainerListModel.objects.filter(id =container['container_number']).first()
  2580. if not container_obj:
  2581. logger.error(f"托盘记录 {container['container_number']} 不存在")
  2582. return {
  2583. "success": False,
  2584. "msg": "托盘信息缺失,无法创建任务",
  2585. }
  2586. if container_obj.current_location != container_obj.target_location:
  2587. logger.error(f"托盘 {container_obj.container_code} 未到达目的地,不生成任务")
  2588. return {
  2589. "success": False,
  2590. "msg": f"托盘 {container_obj.container_code} 未处于可出库状态",
  2591. }
  2592. # 检查前序作业
  2593. existing_task = ContainerWCSModel.objects.filter(
  2594. container=container_obj.container_code,
  2595. working=1,
  2596. status__lt=300,
  2597. is_delete=False
  2598. ).exists()
  2599. if existing_task:
  2600. logger.error(f"托盘 {container_obj.container_code} 仍有未完成任务")
  2601. return {
  2602. "success": False,
  2603. "msg": f"托盘 {container_obj.container_code} 仍有未完成任务",
  2604. }
  2605. OutBoundDetail_obj = OutBoundDetailModel.objects.filter(bound_list=bound_list_id,bound_batch_number_id=container['batch_id']).first()
  2606. if not OutBoundDetail_obj:
  2607. logger.error(f"批次 {container['batch_id']} 不存在")
  2608. return {
  2609. "success": False,
  2610. "msg": f"批次 {container['batch_id']} 不存在",
  2611. }
  2612. month = int(timezone.now().strftime("%Y%m"))
  2613. location_meta = get_task_location_metadata(
  2614. "outbound",
  2615. container_obj.current_location,
  2616. "103"
  2617. )
  2618. task = ContainerWCSModel(
  2619. taskid=OutboundService.generate_task_id(),
  2620. batch = OutBoundDetail_obj.bound_batch_number,
  2621. batch_out = OutBoundDetail_obj.bound_batch,
  2622. bound_list = OutBoundDetail_obj.bound_list,
  2623. sequence=index,
  2624. order_number = container.get('c_number') or location_meta['order_number'] or DEFAULT_ORDER_NUMBER,
  2625. priority=100,
  2626. tasknumber = month*100000+tasknumber_index+tasknumber,
  2627. container=container_obj.container_code,
  2628. current_location=container_obj.current_location,
  2629. target_location="103",
  2630. tasktype="outbound",
  2631. month=int(timezone.now().strftime("%Y%m")),
  2632. message="等待出库",
  2633. status=100,
  2634. location_group_id=location_meta['location_group_id'],
  2635. )
  2636. layer = None
  2637. try:
  2638. parts = str(task.current_location).split('-')
  2639. if len(parts) >= 4:
  2640. layer = parts[3]
  2641. except Exception:
  2642. layer = None
  2643. if layer:
  2644. task_layers.add(layer)
  2645. tasknumber_index += 1
  2646. tasks.append(task)
  2647. container_obj = ContainerListModel.objects.filter(container_code=task.container).first()
  2648. container_obj.target_location = task.target_location
  2649. container_obj.save()
  2650. ContainerWCSModel.objects.bulk_create(tasks)
  2651. logger.info(f"已创建 {len(tasks)} 个初始任务")
  2652. return {
  2653. "success": True,
  2654. "layers": sorted(task_layers),
  2655. "task_count": len(tasks),
  2656. }
  2657. @staticmethod
  2658. def create_initial_check_tasks(container_list,batch_id):
  2659. """生成初始任务队列"""
  2660. with transaction.atomic():
  2661. # 查询检查任务,排除已取消的任务(status=400表示已取消)
  2662. current_WCS = ContainerWCSModel.objects.filter(
  2663. tasktype='check',
  2664. batch_id=batch_id,
  2665. is_delete=False,
  2666. working=1
  2667. ).exclude(status=400).first() # 排除已取消的任务
  2668. if current_WCS:
  2669. logger.error(f"当前{batch_id}已有检查任务")
  2670. return False
  2671. tasks = []
  2672. start_sequence = ContainerWCSModel.objects.filter(tasktype='check').count() + 1
  2673. tasknumber = ContainerWCSModel.objects.filter().count()
  2674. tasknumber_index = 1
  2675. for index, container in enumerate(container_list, start=start_sequence):
  2676. container_obj = ContainerListModel.objects.filter(id =container['container_number']).first()
  2677. if container_obj.current_location != container_obj.target_location:
  2678. logger.error(f"托盘 {container_obj.container_code} 未到达目的地,不生成任务")
  2679. return False
  2680. batch_obj = BoundBatchModel.objects.filter(id =batch_id).first()
  2681. month = int(timezone.now().strftime("%Y%m"))
  2682. location_meta = get_task_location_metadata(
  2683. "check",
  2684. container_obj.current_location,
  2685. "103"
  2686. )
  2687. task = ContainerWCSModel(
  2688. taskid=OutboundService.generate_task_id(),
  2689. batch = batch_obj,
  2690. batch_out = None,
  2691. bound_list = None,
  2692. sequence=index,
  2693. order_number = container.get('c_number') or location_meta['order_number'] or DEFAULT_ORDER_NUMBER,
  2694. priority=100,
  2695. tasknumber = month*100000+tasknumber_index+tasknumber,
  2696. container=container_obj.container_code,
  2697. current_location=container_obj.current_location,
  2698. target_location="103",
  2699. tasktype="check",
  2700. month=int(timezone.now().strftime("%Y%m")),
  2701. message="等待出库",
  2702. status=100,
  2703. location_group_id=location_meta['location_group_id'],
  2704. )
  2705. tasknumber_index += 1
  2706. tasks.append(task)
  2707. container_obj = ContainerListModel.objects.filter(container_code=task.container).first()
  2708. container_obj.target_location = task.target_location
  2709. container_obj.save()
  2710. ContainerWCSModel.objects.bulk_create(tasks)
  2711. logger.info(f"已创建 {len(tasks)} 个初始任务")
  2712. @staticmethod
  2713. def insert_new_tasks(new_tasks):
  2714. """动态插入新任务并重新排序"""
  2715. with transaction.atomic():
  2716. pending_tasks = list(ContainerWCSModel.objects.filter(status=100))
  2717. # 插入新任务
  2718. for new_task_data in new_tasks:
  2719. target_location = new_task_data.get('target_location', 'OUT01')
  2720. location_meta = get_task_location_metadata(
  2721. "outbound",
  2722. new_task_data['current_location'],
  2723. target_location
  2724. )
  2725. order_number = new_task_data.get('order_number')
  2726. if order_number in (None, 0):
  2727. order_number = location_meta['order_number']
  2728. if order_number in (None, 0):
  2729. order_number = DEFAULT_ORDER_NUMBER
  2730. new_task = ContainerWCSModel(
  2731. taskid=OutboundService.generate_task_id(),
  2732. priority=new_task_data.get('priority', 100),
  2733. order_number=order_number,
  2734. container=new_task_data['container'],
  2735. current_location=new_task_data['current_location'],
  2736. target_location=target_location,
  2737. tasktype="outbound",
  2738. month=int(timezone.now().strftime("%Y%m")),
  2739. message="等待出库",
  2740. status=100,
  2741. location_group_id=location_meta['location_group_id'],
  2742. )
  2743. # 找到插入位置
  2744. insert_pos = 0
  2745. for i, task in enumerate(pending_tasks):
  2746. if new_task.priority < task.priority:
  2747. insert_pos = i
  2748. break
  2749. else:
  2750. insert_pos = len(pending_tasks)
  2751. pending_tasks.insert(insert_pos, new_task)
  2752. # 重新分配顺序号
  2753. for i, task in enumerate(pending_tasks, start=1):
  2754. task.sequence = i
  2755. if task.pk is None:
  2756. task.save()
  2757. else:
  2758. task.save(update_fields=['sequence'])
  2759. logger.info(f"已插入 {len(new_tasks)} 个新任务")
  2760. @staticmethod
  2761. def process_next_task(single_task=False, preferred_layer=None, initial_layers=None):
  2762. """处理下一个任务 - 支持前端可配置的跨楼层并发与同层排序
  2763. Args:
  2764. single_task: 如果为True,只下发一条任务(用于WCS完成回调场景)
  2765. 如果为False,批量下发多条任务(用于初始下发场景)
  2766. preferred_layer: 优先选择的楼层(用于single_task=True时,优先同楼层任务)
  2767. """
  2768. # 获取待处理任务,优先按批次排序(同一批次连续出),同一批次内按sequence排序
  2769. # 使用Case处理batch_out为None的情况,确保有批次的任务优先
  2770. from django.db.models import F, Case, When, IntegerField
  2771. from django.conf import settings
  2772. from .models import DispatchConfig
  2773. def get_pending_tasks():
  2774. """获取待处理任务查询集"""
  2775. return ContainerWCSModel.objects.filter(
  2776. status=100,
  2777. working=1,
  2778. is_delete=False
  2779. ).annotate(
  2780. # 为排序添加批次ID字段,None值排最后
  2781. batch_out_id_for_sort=Case(
  2782. When(batch_out__isnull=False, then=F('batch_out_id')),
  2783. default=999999999, # None值使用大数字,排到最后
  2784. output_field=IntegerField()
  2785. )
  2786. ).order_by('batch_out_id_for_sort', 'sequence')
  2787. pending_tasks = get_pending_tasks()
  2788. if not pending_tasks.exists():
  2789. logger.info("没有待处理任务")
  2790. return
  2791. # 读取调度配置(默认2条跨楼层并发)
  2792. cfg = DispatchConfig.get_active_config()
  2793. cross_floor_limit = max(1, int(cfg.cross_floor_concurrent_limit or 2))
  2794. desired_layers = set(initial_layers or [])
  2795. active_layers = OutboundService._get_running_layers()
  2796. # 处理任务列表
  2797. # 如果single_task=True,只下发1条;否则批量下发(最多cross_floor_limit条)
  2798. processed_count = 0
  2799. if desired_layers:
  2800. max_tasks = max(len(desired_layers), 1)
  2801. else:
  2802. max_tasks = 1 if single_task else cross_floor_limit
  2803. skip_count = 0
  2804. max_skip = max(20, len(desired_layers) * 5)
  2805. dispatched_ids = set()
  2806. used_layers = set()
  2807. blocked_layers = set()
  2808. while processed_count < max_tasks and skip_count < max_skip:
  2809. # 重新获取待处理任务(因为可能有任务被跳过)
  2810. pending_tasks = get_pending_tasks().exclude(pk__in=dispatched_ids)
  2811. if not pending_tasks.exists():
  2812. break
  2813. if desired_layers:
  2814. effective_layers = desired_layers - blocked_layers
  2815. if not effective_layers:
  2816. logger.info("初始楼层均存在执行中的任务,暂不新增同楼层下发")
  2817. break
  2818. if used_layers.issuperset(effective_layers):
  2819. logger.info("已完成初始多楼层任务的分发")
  2820. break
  2821. # 如果single_task=True且提供了preferred_layer,优先选择同楼层的任务
  2822. next_task = None
  2823. if desired_layers:
  2824. effective_layers = desired_layers - blocked_layers
  2825. remaining_layers = effective_layers - used_layers
  2826. target_layers = remaining_layers if remaining_layers else effective_layers
  2827. prioritized = []
  2828. fallback = []
  2829. for task in pending_tasks:
  2830. task_layer = OutboundService._extract_layer(task.current_location)
  2831. if task_layer in target_layers:
  2832. prioritized.append(task)
  2833. else:
  2834. fallback.append(task)
  2835. if prioritized:
  2836. next_task = prioritized[0]
  2837. elif remaining_layers:
  2838. skip_count += 1
  2839. continue
  2840. elif fallback:
  2841. next_task = fallback[0]
  2842. elif single_task and preferred_layer:
  2843. # 先尝试找同楼层的任务(在同楼层任务中,仍然按批次和sequence排序)
  2844. same_layer_tasks = []
  2845. other_layer_tasks = []
  2846. for task in pending_tasks:
  2847. task_layer = OutboundService._extract_layer(task.current_location)
  2848. if task_layer == preferred_layer:
  2849. same_layer_tasks.append(task)
  2850. else:
  2851. other_layer_tasks.append(task)
  2852. # 优先从同楼层任务中选择
  2853. if same_layer_tasks:
  2854. next_task = same_layer_tasks[0]
  2855. logger.info(f"优先选择同楼层任务,楼层: {preferred_layer}, 任务: {next_task.taskid}")
  2856. # 如果没找到同楼层的任务,使用第一个任务(按批次和sequence排序)
  2857. elif other_layer_tasks:
  2858. next_task = other_layer_tasks[0]
  2859. logger.info(f"未找到同楼层任务,使用其他楼层任务,任务: {next_task.taskid}")
  2860. else:
  2861. next_task = pending_tasks.first()
  2862. else:
  2863. # 根据同层排序策略,仍旧使用 batch_then_sequence(已在order_by体现)
  2864. next_task = pending_tasks.first()
  2865. if not next_task:
  2866. break
  2867. dispatched_ids.add(next_task.pk)
  2868. location = next_task.current_location
  2869. # 解析楼层(假设格式 Wxx-row-col-layer)
  2870. task_layer = OutboundService._extract_layer(location)
  2871. if location == '103' or location == '203':
  2872. logger.info(f"需要跳过该任务: {next_task.taskid}, 位置: {location}")
  2873. next_task.status = 200
  2874. next_task.working = 0
  2875. next_task.save()
  2876. skip_count += 1
  2877. # 跳过这个任务后,继续处理下一个
  2878. continue
  2879. if task_layer and task_layer in active_layers:
  2880. logger.info(f"楼层 {task_layer} 已有执行中的任务,跳过下发: {next_task.taskid}")
  2881. blocked_layers.add(task_layer)
  2882. skip_count += 1
  2883. continue
  2884. # 跨楼层并发控制:同一轮不允许重复楼层(仅批量下发时生效)
  2885. if not single_task and not desired_layers and task_layer and task_layer in used_layers:
  2886. skip_count += 1
  2887. continue
  2888. try:
  2889. allocator = LocationAllocation()
  2890. allocation_success = perform_initial_allocation(
  2891. allocator,
  2892. next_task.current_location
  2893. )
  2894. if not allocation_success:
  2895. logger.warning(f"任务分配失败,跳过: {next_task.taskid}")
  2896. skip_count += 1
  2897. continue
  2898. OutboundService.send_task_to_wcs(next_task)
  2899. # 标记任务为已下发,避免重复下发
  2900. next_task.status = 150
  2901. next_task.save(update_fields=['status'])
  2902. processed_count += 1
  2903. if task_layer:
  2904. used_layers.add(task_layer)
  2905. active_layers.add(task_layer)
  2906. logger.info(f"成功下发任务: {next_task.taskid}, 批次: {next_task.batch_out_id if next_task.batch_out else '无批次'}")
  2907. except Exception as e:
  2908. logger.error(f"任务处理失败: {next_task.taskid}, 错误: {str(e)}")
  2909. # 处理失败后,继续尝试下一个任务
  2910. skip_count += 1
  2911. if processed_count > 0:
  2912. logger.info(f"本次共下发 {processed_count} 条任务")
  2913. elif skip_count >= max_skip:
  2914. logger.warning(f"跳过了 {skip_count} 个任务,未找到可处理的任务")
  2915. @staticmethod
  2916. def process_current_task(task_id):
  2917. """发送指定任务"""
  2918. try:
  2919. task = ContainerWCSModel.objects.get(taskid=task_id)
  2920. allocator = LocationAllocation()
  2921. perform_initial_allocation(allocator, task.current_location)
  2922. OutboundService.send_task_to_wcs(task)
  2923. except Exception as e:
  2924. logger.error(f"任务处理失败: {str(e)}")
  2925. class DispatchConfigView(APIView):
  2926. """
  2927. 获取/更新任务下发调度配置
  2928. GET: 返回当前启用的配置
  2929. PUT: 更新配置(cross_floor_concurrent_limit, intra_floor_order, enabled)
  2930. """
  2931. def get(self, request):
  2932. cfg = DispatchConfig.get_active_config()
  2933. return DRFResponse(DispatchConfigSerializer(cfg).data, status=200)
  2934. def put(self, request):
  2935. cfg = DispatchConfig.get_active_config()
  2936. serializer = DispatchConfigSerializer(cfg, data=request.data, partial=True)
  2937. serializer.is_valid(raise_exception=True)
  2938. serializer.save()
  2939. return DRFResponse(serializer.data, status=200)
  2940. class WCSTaskLogViewSet(viewsets.ModelViewSet):
  2941. """
  2942. retrieve:
  2943. Response a data list(get)
  2944. list:
  2945. Response a data list(all)
  2946. """
  2947. pagination_class = MyPageNumberPagination
  2948. filter_backends = [DjangoFilterBackend, OrderingFilter, ]
  2949. ordering_fields = ['-id', "-send_time", "send_time", ]
  2950. filter_class = WCSTaskLogFilter
  2951. def get_queryset(self):
  2952. if self.request.user:
  2953. return WCSTaskLogModel.objects.all()
  2954. else:
  2955. return WCSTaskLogModel.objects.none()
  2956. def get_serializer_class(self):
  2957. if self.action in ['list', 'retrieve']:
  2958. return WCSTaskLogSerializer
  2959. else:
  2960. return self.http_method_not_allowed(request=self.request)
  2961. def perform_initial_allocation(allocator, location):
  2962. """执行初始库位分配操作"""
  2963. location_row = location.split('-')[1]
  2964. location_col = location.split('-')[2]
  2965. location_layer = location.split('-')[3]
  2966. location_obj = LocationModel.objects.filter(row=location_row, col=location_col, layer=location_layer).first()
  2967. if not location_obj:
  2968. logger.error(f"未找到库位: {location}")
  2969. return False
  2970. location_code = location_obj.location_code
  2971. operations = [
  2972. (allocator.update_location_status, location_code, 'reserved'),
  2973. (allocator.update_location_group_status, location_code)
  2974. ]
  2975. for func, *args in operations:
  2976. if not func(*args):
  2977. logger.error(f"分配操作失败: {func.__name__}")
  2978. return False
  2979. return True
  2980. # 出库任务下发
  2981. class OutTaskViewSet(ViewSet):
  2982. """
  2983. # fun:get_out_task:下发出库任务
  2984. # fun:get_batch_count_by_boundlist:获取出库申请下的批次数量
  2985. # fun:generate_location_by_demand:根据出库需求生成出库任务
  2986. """
  2987. # authentication_classes = [] # 禁用所有认证类
  2988. # permission_classes = [AllowAny] # 允许任意访问
  2989. def post(self, request):
  2990. try:
  2991. data = self.request.data
  2992. logger.info(f"收到 出库 推送数据: {data}")
  2993. # 从请求中获取 bound_list_id
  2994. bound_list_id = data.get('bound_list_id')
  2995. # 记录操作日志
  2996. try:
  2997. log_operation(
  2998. request=request,
  2999. operation_content=f"接收出库任务请求,出库申请ID: {bound_list_id}",
  3000. operation_level="other",
  3001. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  3002. module_name="WCS出库管理"
  3003. )
  3004. except Exception as log_error:
  3005. pass
  3006. # 查询出库任务,排除已取消的任务(status=400表示已取消)
  3007. current_WCS = ContainerWCSModel.objects.filter(
  3008. tasktype='outbound',
  3009. bound_list_id=bound_list_id,
  3010. is_delete=False
  3011. ).exclude(status=400).first() # 排除已取消的任务
  3012. if current_WCS:
  3013. logger.info(f"当前{bound_list_id}已有出库任务{current_WCS.taskid}")
  3014. if current_WCS.working == 1:
  3015. OutboundService.process_current_task(current_WCS.taskid)
  3016. # 记录成功日志
  3017. try:
  3018. log_success_operation(
  3019. request=request,
  3020. operation_content=f"重新下发出库任务成功,出库申请ID: {bound_list_id},任务ID: {current_WCS.taskid}",
  3021. operation_level="other",
  3022. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  3023. module_name="WCS出库管理"
  3024. )
  3025. except Exception as log_error:
  3026. pass
  3027. return Response({"code": 200, "msg": f"下发任务{ current_WCS.taskid }到WCS成功"}, status=200)
  3028. else :
  3029. # 记录任务处理中日志
  3030. try:
  3031. log_operation(
  3032. request=request,
  3033. operation_content=f"出库任务正在处理中,出库申请ID: {bound_list_id},任务ID: {current_WCS.taskid}",
  3034. operation_level="other",
  3035. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  3036. module_name="WCS出库管理"
  3037. )
  3038. except Exception as log_error:
  3039. pass
  3040. return Response({"code": 200, "msg": f"当前任务{current_WCS.taskid}正在处理中"}, status=200)
  3041. # 获取关联的出库批次
  3042. out_batches = OutBatchModel.objects.filter(
  3043. bound_list_id=bound_list_id,
  3044. is_delete=False
  3045. ).select_related('batch_number')
  3046. if not out_batches.exists():
  3047. # 记录失败日志
  3048. try:
  3049. log_failure_operation(
  3050. request=request,
  3051. operation_content=f"出库任务创建失败,出库申请ID: {bound_list_id},未找到相关出库批次",
  3052. operation_level="other",
  3053. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  3054. module_name="WCS出库管理"
  3055. )
  3056. except Exception as log_error:
  3057. pass
  3058. return Response({"code": 404, "msg": "未找到相关出库批次"}, status=404)
  3059. # 构建批次需求字典
  3060. batch_demand = {}
  3061. for ob in out_batches:
  3062. if ob.batch_number_id not in batch_demand:
  3063. batch_demand[ob.batch_number_id] = {
  3064. 'required': ob.goods_out_qty,
  3065. 'allocated': ob.goods_qty,
  3066. 'remaining': ob.goods_qty - ob.goods_out_qty
  3067. }
  3068. else:
  3069. batch_demand[ob.batch_number_id]['required'] += ob.goods_out_qty
  3070. batch_demand[ob.batch_number_id]['allocated'] += ob.goods_qty
  3071. batch_demand[ob.batch_number_id]['remaining'] += (ob.goods_out_qty - ob.goods_qty)
  3072. # 生成出库任务
  3073. generate_result = self.generate_location_by_demand(
  3074. batch_demand=batch_demand,
  3075. bound_list_id=bound_list_id
  3076. )
  3077. if generate_result['code'] != '200':
  3078. # 记录失败日志
  3079. try:
  3080. log_failure_operation(
  3081. request=request,
  3082. operation_content=f"出库任务生成失败,出库申请ID: {bound_list_id},错误: {generate_result.get('msg', '未知错误')}",
  3083. operation_level="other",
  3084. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  3085. module_name="WCS出库管理"
  3086. )
  3087. except Exception as log_error:
  3088. pass
  3089. return Response(generate_result, status=500)
  3090. # 创建并处理出库任务
  3091. container_list = generate_result['data']
  3092. # 2. 生成初始任务
  3093. creation_result = OutboundService.create_initial_tasks(container_list,bound_list_id)
  3094. if not creation_result.get("success"):
  3095. return Response(
  3096. {"code": 400, "msg": creation_result.get("msg", "创建任务失败")},
  3097. status=400
  3098. )
  3099. # 3. 根据楼层信息初始化下发
  3100. initial_layers = creation_result.get("layers", [])
  3101. if creation_result.get("task_count", 0) > 0:
  3102. if len(initial_layers) > 1:
  3103. OutboundService.process_next_task(initial_layers=initial_layers)
  3104. else:
  3105. OutboundService.process_next_task()
  3106. # 记录成功日志
  3107. try:
  3108. container_count = len(container_list) if container_list else 0
  3109. log_success_operation(
  3110. request=request,
  3111. operation_content=f"出库任务创建成功,出库申请ID: {bound_list_id},托盘数量: {container_count}",
  3112. operation_level="other",
  3113. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  3114. module_name="WCS出库管理"
  3115. )
  3116. except Exception as log_error:
  3117. pass
  3118. return Response({"code": 200, "msg": "下发任务成功"}, status=200)
  3119. except Exception as e:
  3120. logger.error(f"任务生成失败: {str(e)}")
  3121. # 记录异常日志
  3122. try:
  3123. log_failure_operation(
  3124. request=request,
  3125. operation_content=f"出库任务生成异常,出库申请ID: {bound_list_id if 'bound_list_id' in locals() else '未知'},错误: {str(e)}",
  3126. operation_level="other",
  3127. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  3128. module_name="WCS出库管理"
  3129. )
  3130. except Exception as log_error:
  3131. pass
  3132. return Response({"code": 200, "msg": str(e)}, status=200)
  3133. def post_check(self, request):
  3134. try:
  3135. data = self.request.data
  3136. logger.info(f"收到 出库抽检 推送数据: {data}")
  3137. # 从请求中获取 batch_id
  3138. batch_id = data.get('batch_id')
  3139. container_demand = int(data.get('container_demand'))
  3140. # 记录操作日志
  3141. try:
  3142. log_operation(
  3143. request=request,
  3144. operation_content=f"接收出库抽检任务请求,批次ID: {batch_id},抽检托盘数量: {container_demand}",
  3145. operation_level="other",
  3146. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  3147. module_name="WCS出库抽检管理"
  3148. )
  3149. except Exception as log_error:
  3150. pass
  3151. if not batch_id or not container_demand:
  3152. # 记录失败日志
  3153. try:
  3154. log_failure_operation(
  3155. request=request,
  3156. operation_content=f"出库抽检任务创建失败,缺少抽检数目或批次号",
  3157. operation_level="other",
  3158. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  3159. module_name="WCS出库抽检管理"
  3160. )
  3161. except Exception as log_error:
  3162. pass
  3163. return Response({"code": "400", "msg": "缺少抽检数目或批次号"}, status=200)
  3164. # 查询检查任务,排除已取消的任务(status=400表示已取消)
  3165. current_WCS = ContainerWCSModel.objects.filter(
  3166. batch=batch_id,
  3167. tasktype='check',
  3168. is_delete=False,
  3169. working=1
  3170. ).exclude(status=400).first() # 排除已取消的任务
  3171. if current_WCS:
  3172. logger.info(f"当前{batch_id}已有出库抽检任务{current_WCS.taskid}")
  3173. if current_WCS.working == 1:
  3174. OutboundService.process_current_task(current_WCS.taskid)
  3175. # 记录成功日志
  3176. try:
  3177. log_success_operation(
  3178. request=request,
  3179. operation_content=f"重新下发出库抽检任务成功,批次ID: {batch_id},任务ID: {current_WCS.taskid}",
  3180. operation_level="other",
  3181. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  3182. module_name="WCS出库抽检管理"
  3183. )
  3184. except Exception as log_error:
  3185. pass
  3186. return Response({"code": 200, "msg": f"下发任务{ current_WCS.taskid }到WCS成功"}, status=200)
  3187. else :
  3188. # 记录任务处理中日志
  3189. try:
  3190. log_operation(
  3191. request=request,
  3192. operation_content=f"出库抽检任务正在处理中,批次ID: {batch_id},任务ID: {current_WCS.taskid}",
  3193. operation_level="other",
  3194. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  3195. module_name="WCS出库抽检管理"
  3196. )
  3197. except Exception as log_error:
  3198. pass
  3199. return Response({"code": 200, "msg": f"当前任务{current_WCS.taskid}正在处理中"}, status=200)
  3200. # 获取批次号
  3201. generate_result = self.generate_location_by_check(batch_id,container_demand)
  3202. if generate_result['code'] != '200':
  3203. # 记录失败日志
  3204. try:
  3205. log_failure_operation(
  3206. request=request,
  3207. operation_content=f"出库抽检任务生成失败,批次ID: {batch_id},错误: {generate_result.get('msg', '未知错误')}",
  3208. operation_level="other",
  3209. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  3210. module_name="WCS出库抽检管理"
  3211. )
  3212. except Exception as log_error:
  3213. pass
  3214. return Response(generate_result, status=200)
  3215. # 创建并处理出库任务
  3216. container_list = generate_result['data']
  3217. # 3. 立即发送第一个任务
  3218. OutboundService.create_initial_check_tasks(container_list,batch_id)
  3219. OutboundService.process_next_task()
  3220. # 记录成功日志
  3221. try:
  3222. container_count = len(container_list) if container_list else 0
  3223. log_success_operation(
  3224. request=request,
  3225. operation_content=f"出库抽检任务创建成功,批次ID: {batch_id},托盘数量: {container_count}",
  3226. operation_level="other",
  3227. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  3228. module_name="WCS出库抽检管理"
  3229. )
  3230. except Exception as log_error:
  3231. pass
  3232. return Response({"code": 200, "msg": "下发任务成功"}, status=200)
  3233. except Exception as e:
  3234. logger.error(f"任务生成失败: {str(e)}")
  3235. # 记录异常日志
  3236. try:
  3237. log_failure_operation(
  3238. request=request,
  3239. operation_content=f"出库抽检任务生成异常,批次ID: {batch_id if 'batch_id' in locals() else '未知'},错误: {str(e)}",
  3240. operation_level="other",
  3241. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "WCS系统",
  3242. module_name="WCS出库抽检管理"
  3243. )
  3244. except Exception as log_error:
  3245. pass
  3246. return Response({"code": 200, "msg": str(e)}, status=200)
  3247. def get_batch_count_by_boundlist(self,bound_list_id):
  3248. try:
  3249. bound_list_obj_all = OutBoundDetailModel.objects.filter(bound_list=bound_list_id).all()
  3250. if bound_list_obj_all:
  3251. batch_count_dict = {}
  3252. # 统计批次数量(创建哈希表,去重)
  3253. for batch in bound_list_obj_all:
  3254. if batch.bound_batch_number_id not in batch_count_dict:
  3255. batch_count_dict[batch.bound_batch_number_id] = batch.bound_batch.goods_out_qty
  3256. else:
  3257. batch_count_dict[batch.bound_batch_number_id] += batch.bound_batch.goods_out_qty
  3258. return batch_count_dict
  3259. else:
  3260. logger.error(f"查询批次数量失败: {bound_list_id} 不存在")
  3261. return {}
  3262. except Exception as e:
  3263. logger.error(f"查询批次数量失败: {str(e)}")
  3264. return {}
  3265. def get_location_by_status_and_batch(self,status,bound_id):
  3266. try:
  3267. container_obj = ContainerDetailModel.objects.filter(batch=bound_id,status=status,is_delete=False).all()
  3268. if container_obj:
  3269. container_dict = {}
  3270. # 统计托盘数量(创建哈希表,去重)
  3271. for obj in container_obj:
  3272. if obj.container_id not in container_dict:
  3273. container_dict[obj.container_id] = obj.goods_qty
  3274. else:
  3275. container_dict[obj.container_id] += obj.goods_qty
  3276. return container_dict
  3277. else:
  3278. logger.error(f"查询{status}状态的批次数量失败: {bound_id} 不存在")
  3279. return {}
  3280. except Exception as e:
  3281. logger.error(f"查询{status}状态的批次数量失败: {str(e)}")
  3282. return {}
  3283. def get_order_by_batch(self,container_list,bound_id):
  3284. try:
  3285. container_dict = {}
  3286. for container in container_list:
  3287. location_container = LocationContainerLink.objects.filter(container_id=container,is_active=True).first()
  3288. if location_container:
  3289. location_c_number = location_container.location.c_number
  3290. if container not in container_dict:
  3291. container_dict[container] = {
  3292. "container_number":container,
  3293. "location_c_number":location_c_number,
  3294. "location_id ":location_container.location.id,
  3295. "location_type":location_container.location.location_type,
  3296. "batch_id":bound_id,
  3297. }
  3298. if len(container_dict.keys()) == len(container_list):
  3299. return container_dict
  3300. else:
  3301. logger.error(f"查询批次数量失败: {container_list} 不存在")
  3302. return {}
  3303. except Exception as e:
  3304. logger.error(f"查询批次数量失败: {str(e)}")
  3305. return {}
  3306. except Exception as e:
  3307. logger.error(f"查询{status}状态的批次数量失败: {str(e)}")
  3308. return {}
  3309. def get_container_allocation(self, batch_id):
  3310. """兼容所有数据库的去重方案"""
  3311. # 获取唯一托盘ID列表
  3312. container_ids = (
  3313. ContainerDetailModel.objects
  3314. .filter(batch_id=batch_id, status=2,is_delete=False)
  3315. .values_list('container_id', flat=True)
  3316. .distinct()
  3317. )
  3318. # 获取每个托盘的最新明细(按id倒序)
  3319. return (
  3320. ContainerDetailModel.objects
  3321. .filter(container_id__in=container_ids,batch_id=batch_id, status=2,is_delete=False)
  3322. .select_related('container')
  3323. .prefetch_related(
  3324. Prefetch('container__location_links',
  3325. queryset=LocationContainerLink.objects.select_related('location'),
  3326. to_attr='active_location')
  3327. )
  3328. .order_by('container_id', '-id')
  3329. )
  3330. def generate_location_by_check(self,batch_id,container_demand):
  3331. '''
  3332. 根据抽检托盘数目,把相应的库位给到出库任务
  3333. '''
  3334. try:
  3335. return_data = []
  3336. # 获取已去重的托盘列表
  3337. container_qs = self.get_container_allocation(batch_id)
  3338. # 构建托盘信息字典(自动去重)
  3339. container_map = {}
  3340. for cd in container_qs:
  3341. if cd.container_id in container_map:
  3342. continue
  3343. # 获取有效库位信息
  3344. active_location = next(
  3345. (link.location for link in cd.container.active_location
  3346. if link.is_active),
  3347. None
  3348. )
  3349. container_map[cd.container_id] = {
  3350. 'detail': cd,
  3351. 'container': cd.container,
  3352. 'location': active_location
  3353. }
  3354. # 转换为排序列表
  3355. container_list = list(container_map.values())
  3356. sorted_containers = sorted(
  3357. container_list,
  3358. key=lambda x: (
  3359. self._get_goods_class_priority(x['detail'].goods_class),
  3360. -(x['location'].c_number if x['location'] else 0),
  3361. # -(x['location'].layer if x['location'] else 0),
  3362. # x['location'].row if x['location'] else 0,
  3363. # x['location'].col if x['location'] else 0
  3364. )
  3365. )
  3366. for item in sorted_containers:
  3367. if container_demand <= 0:
  3368. break
  3369. # 获取可分配数量
  3370. # 记录分配信息
  3371. allocate_container = {
  3372. "container_number": item['container'].id,
  3373. "batch_id": batch_id,
  3374. "location_code": item['location'].location_code if item['location'] else 'N/A',
  3375. "allocate_qty": 0,
  3376. "c_number": item['location'].c_number if item['location'] else 0
  3377. }
  3378. return_data.append(allocate_container)
  3379. container_demand -= 1
  3380. # 降重 return_data,以container_number为key
  3381. return_data = list({v['container_number']: v for v in return_data}.values())
  3382. # 排序
  3383. return_data = sorted(return_data, key=lambda x: -x['c_number'])
  3384. return {"code": "200", "msg": "Success", "data": return_data}
  3385. except Exception as e:
  3386. logger.error(f"出库任务生成失败: {str(e)}", exc_info=True)
  3387. return {"code": "500", "msg": str(e)}
  3388. def generate_location_by_demand(self, batch_demand, bound_list_id):
  3389. try:
  3390. return_data = []
  3391. for batch_id, demand in batch_demand.items():
  3392. # 获取已去重的托盘列表
  3393. container_qs = self.get_container_allocation(batch_id)
  3394. # 构建托盘信息字典(自动去重)
  3395. container_map = {}
  3396. for cd in container_qs:
  3397. if cd.container_id in container_map:
  3398. container_map[cd.container_id]['goods_qty'] += cd.goods_qty - cd.goods_out_qty
  3399. continue
  3400. # 获取有效库位信息
  3401. active_location = next(
  3402. (link.location for link in cd.container.active_location
  3403. if link.is_active),
  3404. None
  3405. )
  3406. container_map[cd.container_id] = {
  3407. 'detail': cd,
  3408. 'goods_qty': cd.goods_qty - cd.goods_out_qty,
  3409. 'container': cd.container,
  3410. 'location': active_location
  3411. }
  3412. # 转换为排序列表
  3413. container_list = list(container_map.values())
  3414. # 多维度排序(优化性能版)
  3415. sorted_containers = sorted(
  3416. container_list,
  3417. key=lambda x: (
  3418. self._get_goods_class_priority(x['detail'].goods_class),
  3419. -(x['location'].c_number if x['location'] else 0),
  3420. # -(x['location'].layer if x['location'] else 0),
  3421. # x['location'].row if x['location'] else 0,
  3422. # x['location'].col if x['location'] else 0
  3423. )
  3424. )
  3425. # 分配逻辑
  3426. required = demand['required']
  3427. for item in sorted_containers:
  3428. if required <= 0:
  3429. break
  3430. # 获取可分配数量
  3431. allocatable = item['goods_qty']
  3432. allocate_qty = min(required, allocatable)
  3433. # 记录分配信息
  3434. allocate_container = {
  3435. "container_number": item['container'].id,
  3436. "batch_id": batch_id,
  3437. "location_code": item['location'].location_code if item['location'] else 'N/A',
  3438. "allocate_qty": allocate_qty,
  3439. "c_number": item['location'].c_number if item['location'] else 0
  3440. }
  3441. return_data.append(allocate_container)
  3442. required -= allocate_qty
  3443. # 更新数据库状态(需要事务处理)
  3444. self._update_allocation_status(allocate_container, allocate_qty,bound_list_id)
  3445. # 降重 return_data,以container_number为key
  3446. return_data = list({v['container_number']: v for v in return_data}.values())
  3447. # 排序
  3448. return_data = sorted(return_data, key=lambda x: -x['c_number'])
  3449. return {"code": "200", "msg": "Success", "data": return_data}
  3450. except Exception as e:
  3451. logger.error(f"出库任务生成失败: {str(e)}", exc_info=True)
  3452. return {"code": "500", "msg": str(e)}
  3453. def _get_goods_class_priority(self, goods_class):
  3454. """货物类型优先级权重"""
  3455. return {
  3456. 3: 0, # 散盘最高
  3457. 1: 1, # 成品次之
  3458. 2: 2 # 空盘最低
  3459. }.get(goods_class, 99)
  3460. def _update_allocation_status(self, allocate_container, allocate_qty,bound_list_id):
  3461. """事务化更新分配状态"""
  3462. try:
  3463. # 更新托盘明细
  3464. container_detail_all = ContainerDetailModel.objects.filter(
  3465. container_id=allocate_container['container_number'],
  3466. batch_id=allocate_container['batch_id'],
  3467. is_delete=False
  3468. ).all()
  3469. left_qty = 0
  3470. for cd in container_detail_all:
  3471. if left_qty - allocate_qty >= 0:
  3472. break
  3473. add_qty = min(allocate_qty-left_qty, cd.goods_qty - cd.goods_out_qty)
  3474. if add_qty == 0:
  3475. continue
  3476. left_qty += add_qty
  3477. last_out_qty = cd.goods_out_qty
  3478. cd.goods_out_qty += add_qty
  3479. # print(f"{left_qty/25} 更新托盘 {cd.container.container_code} 批次 {cd.batch_id} 出库数量: {cd.goods_out_qty}")
  3480. cd.save()
  3481. out_batch_detail.objects.create(
  3482. out_bound_id=bound_list_id,
  3483. container_id=cd.container_id,
  3484. container_detail_id=cd.id,
  3485. out_goods_qty=add_qty,
  3486. last_out_goods_qty = last_out_qty,
  3487. working = 1,
  3488. is_delete = False
  3489. )
  3490. return True
  3491. except Exception as e:
  3492. logger.error(f"状态更新失败: {str(e)}")
  3493. return False
  3494. def create_or_update_container_operation(self,container_obj,batch_id,bound_id,to_location,goods_qty,goods_weight):
  3495. try:
  3496. container_operation_obj = ContainerOperationModel.objects.filter(container=container_obj,batch_id=batch_id,bound_id=bound_id,operation_type="outbound").first()
  3497. if container_operation_obj:
  3498. logger.info(f"[0]查询出库任务: {container_operation_obj.operation_type} ")
  3499. logger.info(f"更新出库任务: {container_obj.container_code} 批次 {batch_id} 出库需求: {bound_id} 数量: {goods_qty} 重量: {goods_weight}")
  3500. container_operation_obj.to_location = to_location
  3501. container_operation_obj.goods_qty = goods_qty
  3502. container_operation_obj.goods_weight = goods_weight
  3503. container_operation_obj.save()
  3504. # 记录更新日志(内部方法,不记录详细日志,避免过多记录)
  3505. else:
  3506. logger.info(f"创建出库任务: {container_obj.container_code} 批次 {batch_id} 出库需求: {bound_id} 数量: {goods_qty} 重量: {goods_weight}")
  3507. batch = BoundBatchModel.objects.filter(id=batch_id).first()
  3508. if not batch:
  3509. return {"code": "500", "msg": f"批次 {batch_id} 不存在"}
  3510. ContainerOperationModel.objects.create(
  3511. month = int(timezone.now().strftime("%Y%m")),
  3512. container = container_obj,
  3513. goods_code = batch.goods_code,
  3514. goods_desc = batch.goods_desc,
  3515. operation_type ="outbound",
  3516. batch_id = batch_id,
  3517. bound_id = bound_id,
  3518. goods_qty = goods_qty,
  3519. goods_weight = goods_weight,
  3520. from_location = container_obj.current_location,
  3521. to_location= to_location,
  3522. timestamp=timezone.now(),
  3523. operator="WMS",
  3524. memo=f"出库需求: {bound_id}, 批次: {batch_id}, 数量: {goods_qty}"
  3525. )
  3526. # 记录创建日志(内部方法,不记录详细日志,避免过多记录)
  3527. return {"code": "200", "msg": "Success"}
  3528. except Exception as e:
  3529. return {"code": "500", "msg": str(e)}
  3530. def update_container_detail_out_qty(self,container_obj,batch_id):
  3531. try:
  3532. logger.info(f"[1]更新托盘出库数量: {container_obj.container_code} 批次 {batch_id} ")
  3533. container_operation_obj = ContainerOperationModel.objects.filter(container=container_obj,batch_id=batch_id,operation_type="outbound").all()
  3534. if not container_operation_obj:
  3535. logger.error(f"[1]批次 {batch_id} 托盘 {container_obj.container_code} 无出库任务")
  3536. return {"code": "500", "msg": f"批次 {batch_id} 托盘 {container_obj.container_code} 无出库任务"}
  3537. container_detail_obj = ContainerDetailModel.objects.filter(container=container_obj,batch_id=batch_id,status=2,is_delete=False).first()
  3538. if not container_detail_obj:
  3539. logger.error(f"[1]批次 {batch_id} 托盘 {container_obj.container_code} 无批次信息")
  3540. return {"code": "500", "msg": f"批次 {batch_id} 托盘 {container_obj.container_code} 无批次信息"}
  3541. out_qty = 0
  3542. for obj in container_operation_obj:
  3543. out_qty += obj.goods_qty
  3544. if out_qty >= container_detail_obj.goods_qty:
  3545. out_qty = container_detail_obj.goods_qty
  3546. container_detail_obj.status = 3
  3547. break
  3548. if out_qty == 0:
  3549. logger.error(f"[1]批次 {batch_id} 托盘 {container_obj.container_code} 无出库数量")
  3550. return {"code": "500", "msg": f"批次 {batch_id} 托盘 {container_obj.container_code} 无出库数量"}
  3551. container_detail_obj.goods_out_qty = out_qty
  3552. container_detail_obj.save()
  3553. return {"code": "200", "msg": "Success"}
  3554. except Exception as e:
  3555. return {"code": "500", "msg": str(e)}
  3556. # 出库任务监测
  3557. class BatchViewSet(viewsets.ModelViewSet):
  3558. authentication_classes = [] # 禁用所有认证类
  3559. permission_classes = [AllowAny] # 允许任意访问
  3560. def wcs_post(self, request, *args, **kwargs):
  3561. data = self.request.data
  3562. logger.info(f"收到 WMS 推送数据: {data}")
  3563. return Response({"code": "200", "msg": "Success"}, status=200)
  3564. # views.py
  3565. class OutDetailViewSet(viewsets.ModelViewSet):
  3566. pagination_class = MyPageNumberPagination
  3567. serializer_class = OutBoundDetailSerializer
  3568. def get_project(self):
  3569. try:
  3570. id = self.kwargs.get('pk')
  3571. return id
  3572. except:
  3573. return None
  3574. def get_queryset(self):
  3575. """根据不同的action调整查询集"""
  3576. if self.action == 'list':
  3577. # 获取每个out_bound的最新一条记录
  3578. # 子查询,用于获取每个out_bound对应的最新out_batch_detail记录
  3579. subquery = out_batch_detail.objects.filter(
  3580. out_bound=OuterRef('out_bound')
  3581. ).order_by('-id')
  3582. # 返回最新的out_batch_detail记录,通过子查询的结果进行过滤
  3583. return out_batch_detail.objects.filter(
  3584. id=Subquery(subquery.values('id')[:1])
  3585. )
  3586. return out_batch_detail.objects.all()
  3587. def retrieve(self, request, *args, **kwargs):
  3588. """重写retrieve方法返回关联集合"""
  3589. qs = self.get_project()
  3590. queryset = self.filter_queryset(
  3591. out_batch_detail.objects.filter(out_bound = qs)
  3592. )
  3593. # 分页处理
  3594. # page = self.paginate_queryset(queryset)
  3595. # if queryset is not None:
  3596. # serializer = self.get_serializer(queryset, many=True)
  3597. # return self.get_paginated_response(serializer.data)
  3598. serializer = self.get_serializer(queryset, many=True)
  3599. return Response(serializer.data)
  3600. def get_serializer_class(self):
  3601. """根据action切换序列化器"""
  3602. if self.action == 'retrieve':
  3603. return OutBoundFullDetailSerializer
  3604. return super().get_serializer_class()
  3605. def get_out_batch_detail(self, request):
  3606. """获取某个托盘的出库明细"""
  3607. try:
  3608. container_code = request.query_params.get('container_code')
  3609. container_obj = ContainerListModel.objects.filter(container_code=container_code).first()
  3610. if not container_obj:
  3611. return Response({"code": "500", "message": f"托盘 {container_code} 不存在"}, status=status.HTTP_200_OK)
  3612. out_batch_detail_all = out_batch_detail.objects.filter(container=container_obj,working=1,is_delete=False).all()
  3613. if not out_batch_detail_all:
  3614. return Response({"code": "500", "message": f"托盘 {container_code} 无出库明细"}, status=status.HTTP_200_OK)
  3615. serializer = OutBoundFullDetailSerializer(out_batch_detail_all, many=True)
  3616. return Response({"code": "200", "message": "Success", "data": serializer.data}, status=status.HTTP_200_OK)
  3617. except Exception as e:
  3618. return Response({"code": "500", "message": str(e)}, status=status.HTTP_200_OK)
  3619. def confirm_out_batch_detail(self, request):
  3620. """确认出库 - 支持按批次和数量部分确认"""
  3621. try:
  3622. container_code = request.data.get('container_code')
  3623. confirm_items = request.data.get('confirm_items', []) # 格式: [{"detail_id": 1, "confirm_qty": 10}, ...]
  3624. # 记录操作日志
  3625. try:
  3626. log_operation(
  3627. request=request,
  3628. operation_content=f"接收确认出库请求,托盘编码: {container_code},确认项数量: {len(confirm_items) if confirm_items else '全部'}",
  3629. operation_level="other",
  3630. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3631. module_name="出库确认管理"
  3632. )
  3633. except Exception as log_error:
  3634. pass
  3635. if not container_code:
  3636. # 记录失败日志
  3637. try:
  3638. log_failure_operation(
  3639. request=request,
  3640. operation_content=f"确认出库失败,缺少托盘编码参数",
  3641. operation_level="other",
  3642. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3643. module_name="出库确认管理"
  3644. )
  3645. except Exception as log_error:
  3646. pass
  3647. return Response({"code": "500", "message": "缺少托盘编码参数"}, status=status.HTTP_200_OK)
  3648. container_obj = ContainerListModel.objects.filter(container_code=container_code).first()
  3649. if not container_obj:
  3650. # 记录失败日志
  3651. try:
  3652. log_failure_operation(
  3653. request=request,
  3654. operation_content=f"确认出库失败,托盘 {container_code} 不存在",
  3655. operation_level="other",
  3656. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3657. module_name="出库确认管理"
  3658. )
  3659. except Exception as log_error:
  3660. pass
  3661. return Response({"code": "500", "message": f"托盘 {container_code} 不存在"}, status=status.HTTP_200_OK)
  3662. # 如果没有指定确认项,则确认所有出库明细(保持向后兼容)
  3663. if not confirm_items:
  3664. out_batch_detail_all = out_batch_detail.objects.filter(container=container_obj,working=1,is_delete=False).order_by('-id').all()
  3665. if not out_batch_detail_all:
  3666. # 记录失败日志
  3667. try:
  3668. log_failure_operation(
  3669. request=request,
  3670. operation_content=f"确认出库失败,托盘 {container_code} 无出库明细",
  3671. operation_level="other",
  3672. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3673. module_name="出库确认管理"
  3674. )
  3675. except Exception as log_error:
  3676. pass
  3677. return Response({"code": "500", "message": f"托盘 {container_code} 无出库明细"}, status=status.HTTP_200_OK)
  3678. total_qty = 0
  3679. batch_list = []
  3680. for obj in out_batch_detail_all:
  3681. obj.working = 0
  3682. obj.save()
  3683. total_qty += obj.out_goods_qty
  3684. batch_list.append(f"批次{obj.container_detail.batch.id}(数量:{obj.out_goods_qty})")
  3685. BatchOperateLogModel.objects.create(
  3686. batch_id = obj.container_detail.batch,
  3687. log_type = 1,
  3688. log_date = timezone.now(),
  3689. goods_code = obj.container_detail.batch.goods_code,
  3690. goods_desc = obj.container_detail.batch.goods_desc,
  3691. goods_qty = obj.out_goods_qty,
  3692. log_content = f"出库托盘 {container_code} 批次 {obj.container_detail.batch.id} 数量 {obj.out_goods_qty}",
  3693. creater = "WMS",
  3694. openid = "WMS"
  3695. )
  3696. # 记录成功日志
  3697. try:
  3698. log_success_operation(
  3699. request=request,
  3700. operation_content=f"确认出库成功,托盘编码: {container_code},确认明细数: {len(out_batch_detail_all)},总数量: {total_qty},批次: {', '.join(batch_list)}",
  3701. operation_level="other",
  3702. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3703. module_name="出库确认管理"
  3704. )
  3705. except Exception as log_error:
  3706. pass
  3707. return Response({"code": "200", "message": "出库成功"}, status=status.HTTP_200_OK)
  3708. # 按批次和数量部分确认
  3709. from decimal import Decimal
  3710. for item in confirm_items:
  3711. out_detail_id = item.get('detail_id') # 这是 out_batch_detail 的 ID
  3712. confirm_qty = Decimal(str(item.get('confirm_qty', 0)))
  3713. if not out_detail_id or confirm_qty <= 0:
  3714. continue
  3715. # 确保 out_detail_id 是整数类型
  3716. try:
  3717. out_detail_id = int(out_detail_id)
  3718. except (ValueError, TypeError):
  3719. logger.error(f"无效的 out_detail_id: {out_detail_id}")
  3720. continue
  3721. # 添加调试日志
  3722. logger.info(f"查询出库明细: container={container_obj.container_code}, out_batch_detail_id={out_detail_id}")
  3723. # 直接通过 out_batch_detail 的 ID 查找记录
  3724. out_detail = out_batch_detail.objects.filter(
  3725. id=out_detail_id,
  3726. container_id=container_obj.id,
  3727. working=1,
  3728. is_delete=False
  3729. ).first()
  3730. if not out_detail:
  3731. # 尝试不限制 working 状态查找
  3732. out_detail_any = out_batch_detail.objects.filter(
  3733. id=out_detail_id,
  3734. container_id=container_obj.id,
  3735. is_delete=False
  3736. ).first()
  3737. if out_detail_any:
  3738. if out_detail_any.working == 0:
  3739. logger.warning(f"出库明细 {out_detail_id} 的 working 状态为 0,可能已经被处理过了")
  3740. else:
  3741. logger.warning(f"出库明细 {out_detail_id} 的 working 状态为 {out_detail_any.working},不是 1")
  3742. else:
  3743. logger.error(f"未找到出库明细: out_batch_detail_id={out_detail_id}, container={container_obj.container_code}")
  3744. continue
  3745. # 计算本次要确认的数量(不能超过该出库明细的数量)
  3746. out_qty_to_confirm = min(confirm_qty, out_detail.out_goods_qty)
  3747. # 如果确认数量等于或大于出库数量,则标记为已完成
  3748. if out_qty_to_confirm >= out_detail.out_goods_qty:
  3749. out_detail.working = 0
  3750. out_detail.save()
  3751. else:
  3752. # 部分确认,创建新的确认记录并减少剩余数量
  3753. # 创建一个新的 out_batch_detail 记录用于剩余数量
  3754. remaining_qty = out_detail.out_goods_qty - out_qty_to_confirm
  3755. out_detail.out_goods_qty = out_qty_to_confirm
  3756. out_detail.working = 0
  3757. out_detail.save()
  3758. # 创建剩余数量的记录(如果需要)
  3759. if remaining_qty > 0:
  3760. out_batch_detail.objects.create(
  3761. out_bound_id=out_detail.out_bound_id,
  3762. container_id=out_detail.container_id,
  3763. container_detail_id=out_detail.container_detail_id,
  3764. out_goods_qty=remaining_qty,
  3765. last_out_goods_qty=out_detail.last_out_goods_qty,
  3766. working=1,
  3767. is_delete=False
  3768. )
  3769. # 创建操作日志
  3770. BatchOperateLogModel.objects.create(
  3771. batch_id = out_detail.container_detail.batch,
  3772. log_type = 1,
  3773. log_date = timezone.now(),
  3774. goods_code = out_detail.container_detail.batch.goods_code,
  3775. goods_desc = out_detail.container_detail.batch.goods_desc,
  3776. goods_qty = out_qty_to_confirm,
  3777. log_content = f"出库托盘 {container_code} 批次 {out_detail.container_detail.batch.id} 数量 {out_qty_to_confirm}",
  3778. creater = "WMS",
  3779. openid = "WMS"
  3780. )
  3781. logger.info(f"确认出库明细 {out_detail_id}: 确认数量={out_qty_to_confirm}, 剩余数量={out_detail.out_goods_qty}")
  3782. # 记录成功日志
  3783. try:
  3784. confirmed_count = len(confirm_items)
  3785. total_confirmed_qty = sum(Decimal(str(item.get('confirm_qty', 0))) for item in confirm_items)
  3786. log_success_operation(
  3787. request=request,
  3788. operation_content=f"确认出库成功,托盘编码: {container_code},确认明细数: {confirmed_count},总确认数量: {total_confirmed_qty}",
  3789. operation_level="other",
  3790. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3791. module_name="出库确认管理"
  3792. )
  3793. except Exception as log_error:
  3794. pass
  3795. return Response({"code": "200", "message": "出库成功"}, status=status.HTTP_200_OK)
  3796. except Exception as e:
  3797. logger.error(f"确认出库失败: {str(e)}", exc_info=True)
  3798. # 记录异常日志
  3799. try:
  3800. log_failure_operation(
  3801. request=request,
  3802. operation_content=f"确认出库异常,托盘编码: {container_code if 'container_code' in locals() else '未知'},错误: {str(e)}",
  3803. operation_level="other",
  3804. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3805. module_name="出库确认管理"
  3806. )
  3807. except Exception as log_error:
  3808. pass
  3809. return Response({"code": "500", "message": str(e)}, status=status.HTTP_200_OK)
  3810. def cancel_out_batch_detail(self, request):
  3811. """取消出库 - 支持按批次和数量部分取消"""
  3812. try:
  3813. container_code = request.data.get('container_code')
  3814. cancel_items = request.data.get('cancel_items', []) # 格式: [{"detail_id": 1, "cancel_qty": 10}, ...]
  3815. # 记录操作日志
  3816. try:
  3817. log_operation(
  3818. request=request,
  3819. operation_content=f"接收取消出库请求,托盘编码: {container_code},取消项数量: {len(cancel_items) if cancel_items else '全部'}",
  3820. operation_level="other",
  3821. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3822. module_name="出库取消管理"
  3823. )
  3824. except Exception as log_error:
  3825. pass
  3826. if not container_code:
  3827. # 记录失败日志
  3828. try:
  3829. log_failure_operation(
  3830. request=request,
  3831. operation_content=f"取消出库失败,缺少托盘编码参数",
  3832. operation_level="other",
  3833. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3834. module_name="出库取消管理"
  3835. )
  3836. except Exception as log_error:
  3837. pass
  3838. return Response({"code": "500", "message": "缺少托盘编码参数"}, status=status.HTTP_200_OK)
  3839. container_obj = ContainerListModel.objects.filter(container_code=container_code).first()
  3840. if not container_obj:
  3841. # 记录失败日志
  3842. try:
  3843. log_failure_operation(
  3844. request=request,
  3845. operation_content=f"取消出库失败,托盘 {container_code} 不存在",
  3846. operation_level="other",
  3847. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3848. module_name="出库取消管理"
  3849. )
  3850. except Exception as log_error:
  3851. pass
  3852. return Response({"code": "500", "message": f"托盘 {container_code} 不存在"}, status=status.HTTP_200_OK)
  3853. # 如果没有指定取消项,则取消所有出库明细(保持向后兼容)
  3854. if not cancel_items:
  3855. out_batch_detail_all = out_batch_detail.objects.filter(container=container_obj,working=1,is_delete=False).order_by('-id').all()
  3856. if not out_batch_detail_all:
  3857. # 记录失败日志
  3858. try:
  3859. log_failure_operation(
  3860. request=request,
  3861. operation_content=f"取消出库失败,托盘 {container_code} 无出库明细",
  3862. operation_level="other",
  3863. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3864. module_name="出库取消管理"
  3865. )
  3866. except Exception as log_error:
  3867. pass
  3868. return Response({"code": "500", "message": f"托盘 {container_code} 无出库明细"}, status=status.HTTP_200_OK)
  3869. total_cancel_qty = 0
  3870. batch_list = []
  3871. for obj in out_batch_detail_all:
  3872. total_cancel_qty += obj.out_goods_qty
  3873. batch_list.append(f"批次{obj.container_detail.batch.id}(数量:{obj.out_goods_qty})")
  3874. obj.container_detail.goods_out_qty = obj.last_out_goods_qty
  3875. obj.container_detail.save()
  3876. obj.is_delete = True
  3877. obj.working = 0
  3878. obj.save()
  3879. # 记录成功日志
  3880. try:
  3881. log_success_operation(
  3882. request=request,
  3883. operation_content=f"取消出库成功,托盘编码: {container_code},取消明细数: {len(out_batch_detail_all)},总取消数量: {total_cancel_qty},批次: {', '.join(batch_list)}",
  3884. operation_level="other",
  3885. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3886. module_name="出库取消管理"
  3887. )
  3888. except Exception as log_error:
  3889. pass
  3890. return Response({"code": "200", "message": "出库取消成功"}, status=status.HTTP_200_OK)
  3891. # 按批次和数量部分取消
  3892. from decimal import Decimal
  3893. for item in cancel_items:
  3894. out_detail_id = item.get('detail_id') # 这是 out_batch_detail 的 ID
  3895. cancel_qty = Decimal(str(item.get('cancel_qty', 0)))
  3896. if not out_detail_id or cancel_qty <= 0:
  3897. continue
  3898. # 确保 out_detail_id 是整数类型
  3899. try:
  3900. out_detail_id = int(out_detail_id)
  3901. except (ValueError, TypeError):
  3902. logger.error(f"无效的 out_detail_id: {out_detail_id}")
  3903. continue
  3904. # 添加调试日志
  3905. logger.info(f"查询出库明细: container={container_obj.container_code}, out_batch_detail_id={out_detail_id}")
  3906. # 直接通过 out_batch_detail 的 ID 查找记录
  3907. out_detail = out_batch_detail.objects.filter(
  3908. id=out_detail_id,
  3909. container_id=container_obj.id,
  3910. working=1,
  3911. is_delete=False
  3912. ).first()
  3913. if not out_detail:
  3914. # 尝试不限制 working 状态查找
  3915. out_detail_any = out_batch_detail.objects.filter(
  3916. id=out_detail_id,
  3917. container_id=container_obj.id,
  3918. is_delete=False
  3919. ).first()
  3920. if out_detail_any:
  3921. if out_detail_any.working == 0:
  3922. logger.warning(f"出库明细 {out_detail_id} 的 working 状态为 0,可能已经被处理过了")
  3923. else:
  3924. logger.warning(f"出库明细 {out_detail_id} 的 working 状态为 {out_detail_any.working},不是 1")
  3925. else:
  3926. logger.error(f"未找到出库明细: out_batch_detail_id={out_detail_id}, container={container_obj.container_code}")
  3927. continue
  3928. # 计算本次要取消的数量(不能超过该出库明细的数量)
  3929. out_qty_to_cancel = min(cancel_qty, out_detail.out_goods_qty)
  3930. # 更新容器明细的出库数量
  3931. out_detail.container_detail.goods_out_qty -= out_qty_to_cancel
  3932. out_detail.container_detail.goods_out_qty = max(
  3933. out_detail.container_detail.goods_out_qty,
  3934. out_detail.last_out_goods_qty
  3935. )
  3936. out_detail.container_detail.save()
  3937. # 如果全部取消,则删除或标记该出库明细
  3938. if out_detail.out_goods_qty <= out_qty_to_cancel:
  3939. out_detail.is_delete = True
  3940. out_detail.working = 0
  3941. out_detail.out_goods_qty = Decimal('0') # 清零
  3942. else:
  3943. # 部分取消,减少出库数量
  3944. out_detail.out_goods_qty -= out_qty_to_cancel
  3945. out_detail.save()
  3946. logger.info(f"取消出库明细 {out_detail_id}: 取消数量={out_qty_to_cancel}, 剩余数量={out_detail.out_goods_qty}")
  3947. # 记录成功日志
  3948. try:
  3949. canceled_count = len(cancel_items)
  3950. total_canceled_qty = sum(Decimal(str(item.get('cancel_qty', 0))) for item in cancel_items)
  3951. log_success_operation(
  3952. request=request,
  3953. operation_content=f"取消出库成功,托盘编码: {container_code},取消明细数: {canceled_count},总取消数量: {total_canceled_qty}",
  3954. operation_level="other",
  3955. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3956. module_name="出库取消管理"
  3957. )
  3958. except Exception as log_error:
  3959. pass
  3960. return Response({"code": "200", "message": "出库取消成功"}, status=status.HTTP_200_OK)
  3961. except Exception as e:
  3962. logger.error(f"取消出库失败: {str(e)}", exc_info=True)
  3963. # 记录异常日志
  3964. try:
  3965. log_failure_operation(
  3966. request=request,
  3967. operation_content=f"取消出库异常,托盘编码: {container_code if 'container_code' in locals() else '未知'},错误: {str(e)}",
  3968. operation_level="other",
  3969. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3970. module_name="出库取消管理"
  3971. )
  3972. except Exception as log_error:
  3973. pass
  3974. return Response({"code": "500", "message": str(e)}, status=status.HTTP_200_OK)
  3975. def get_contianer_detail(self, request):
  3976. try:
  3977. container_code = request.query_params.get('container_code')
  3978. # 记录操作日志(查询操作)
  3979. try:
  3980. log_operation(
  3981. request=request,
  3982. operation_content=f"查询托盘明细,托盘编码: {container_code}",
  3983. operation_level="view",
  3984. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3985. module_name="出库明细管理"
  3986. )
  3987. except Exception as log_error:
  3988. pass
  3989. container_obj = ContainerListModel.objects.filter(container_code=container_code).first()
  3990. if not container_obj:
  3991. # 记录失败日志
  3992. try:
  3993. log_failure_operation(
  3994. request=request,
  3995. operation_content=f"查询托盘明细失败,托盘 {container_code} 不存在",
  3996. operation_level="view",
  3997. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  3998. module_name="出库明细管理"
  3999. )
  4000. except Exception as log_error:
  4001. pass
  4002. return Response({"code": "500", "message": f"托盘 {container_code} 不存在"}, status=status.HTTP_200_OK)
  4003. container_detail_all = ContainerDetailModel.objects.filter(container=container_obj,is_delete=False).all().exclude(status__in=[3])
  4004. return_data=[]
  4005. for obj in container_detail_all:
  4006. return_data.append({
  4007. "id": obj.id,
  4008. "batch": obj.batch.bound_number,
  4009. "goods_code": obj.goods_code,
  4010. "goods_desc": obj.goods_desc,
  4011. "goods_qty" :obj.goods_qty,
  4012. "out_goods_qty": obj.goods_out_qty
  4013. })
  4014. # 记录成功日志(查询成功)
  4015. try:
  4016. log_success_operation(
  4017. request=request,
  4018. operation_content=f"查询托盘明细成功,托盘编码: {container_code},明细数量: {len(return_data)}",
  4019. operation_level="view",
  4020. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  4021. module_name="出库明细管理"
  4022. )
  4023. except Exception as log_error:
  4024. pass
  4025. return Response({"code": "200", "message": "Success", "data": return_data}, status=status.HTTP_200_OK)
  4026. except Exception as e:
  4027. # 记录异常日志
  4028. try:
  4029. log_failure_operation(
  4030. request=request,
  4031. operation_content=f"查询托盘明细异常,托盘编码: {container_code if 'container_code' in locals() else '未知'},错误: {str(e)}",
  4032. operation_level="view",
  4033. operator=request.auth.name if hasattr(request, 'auth') and request.auth else "系统",
  4034. module_name="出库明细管理"
  4035. )
  4036. except Exception as log_error:
  4037. pass
  4038. return Response({"code": "500", "message": str(e)}, status=status.HTTP_200_OK)
  4039. def change_container_out_qty(self, request):
  4040. try:
  4041. container_code = request.data.get('container_code')
  4042. container_obj = ContainerListModel.objects.filter(container_code=container_code).first()
  4043. if not container_obj:
  4044. return Response({"code": "500", "message": f"托盘 {container_code} 不存在"}, status=status.HTTP_200_OK)
  4045. logger.info(f"change_container_out_qty: {request.data}")
  4046. for container_detail_id, out_qty in request.data.get('detail_list').items():
  4047. container_detail_obj = ContainerDetailModel.objects.filter(id=container_detail_id,is_delete=False).first()
  4048. if not container_detail_obj:
  4049. continue
  4050. from decimal import Decimal
  4051. out_qty = Decimal(out_qty)
  4052. container_detail_obj.goods_out_qty += out_qty
  4053. container_detail_obj.save()
  4054. return Response({"code": "200", "message": "Success"}, status=status.HTTP_200_OK)
  4055. except Exception as e:
  4056. return Response({"code": "500", "message": str(e)}, status=status.HTTP_200_OK)