pilot_service.py 159 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376
  1. """中试服务管理 CRUD(细胞产品类型/规格/价格体系/项目价格、渠道信息、体检机构)"""
  2. from __future__ import annotations
  3. import json
  4. import math
  5. from datetime import datetime
  6. from typing import Any, Dict, List, Optional, Tuple
  7. from sqlalchemy import and_, func, or_
  8. from sqlalchemy.orm import Session
  9. from app.core.utils import generate_uuid
  10. from app.crud.collection_protocol import urls_to_attachment_list
  11. from app.models.models import (
  12. BizCellProductType,
  13. BizCellProductSpec,
  14. BizCellProductPrice,
  15. BizCellProductProjectSystem,
  16. BizCellProductProjectSystemItem,
  17. BizChannel,
  18. BizChannelAccount,
  19. BizChannelAgreement,
  20. BizChannelAgreementProductPrice,
  21. BizChannelInvoice,
  22. BizChannelMailAddress,
  23. BizChannelProductQuota,
  24. BizChannelServiceOrder,
  25. BizChannelServiceOrderProduct,
  26. BizChannelServiceOrderTxn,
  27. BizCheckupInstitution,
  28. BizCheckupInstitutionInvoice,
  29. BizCheckupInstitutionProject,
  30. BizCheckupInstitutionPackage,
  31. BizCheckupInstitutionPackageItem,
  32. SysUser,
  33. )
  34. def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
  35. val = (value or "").strip()
  36. if len(val) == 10:
  37. return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00"
  38. return val
  39. def _user_nick_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
  40. ids = list({x for x in user_ids if x})
  41. if not ids:
  42. return {}
  43. rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all()
  44. return {u.id: (u.nick_name or "").strip() for u in rows}
  45. # ────────────────────────────────────────────
  46. # Entity 1: BizCellProductType 细胞产品类型
  47. # ────────────────────────────────────────────
  48. def _row_to_dict_cell_product_type(row) -> Dict[str, Any]:
  49. return {
  50. "id": row.id,
  51. "productTypeCode": row.product_type_code or "",
  52. "productTypeName": row.product_type_name or "",
  53. "tenantNo": row.tenant_no or "",
  54. "orderNum": row.order_num if row.order_num is not None else 0,
  55. "remark": row.remark or "",
  56. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  57. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  58. }
  59. def list_cell_product_types(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
  60. def _filtered():
  61. q0 = db.query(BizCellProductType).filter(BizCellProductType.del_flag == 0)
  62. if tenant_no:
  63. q0 = q0.filter(BizCellProductType.tenant_no == tenant_no)
  64. kw = (keyword or "").strip()
  65. if kw:
  66. like = f"%{kw}%"
  67. q0 = q0.filter(
  68. or_(
  69. BizCellProductType.product_type_code.like(like),
  70. BizCellProductType.product_type_name.like(like),
  71. )
  72. )
  73. if user_info:
  74. from app.core.data_scope import apply_data_scope
  75. q0 = apply_data_scope(q0, BizCellProductType, user_info, db, tenant_no)
  76. return q0
  77. total = _filtered().order_by(None).with_entities(func.count(BizCellProductType.id)).scalar() or 0
  78. page = max(1, page)
  79. page_size = min(max(1, page_size), 100)
  80. rows = _filtered().order_by(BizCellProductType.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
  81. return [_row_to_dict_cell_product_type(x) for x in rows], int(total)
  82. def get_cell_product_type(db, type_id, tenant_no=None):
  83. q = db.query(BizCellProductType).filter(and_(BizCellProductType.id == type_id, BizCellProductType.del_flag == 0))
  84. if tenant_no:
  85. q = q.filter(BizCellProductType.tenant_no == tenant_no)
  86. return q.first()
  87. def cell_product_type_code_exists(db, code, tenant_no, exclude_id=None):
  88. q = db.query(BizCellProductType).filter(
  89. and_(BizCellProductType.product_type_code == code, BizCellProductType.del_flag == 0)
  90. )
  91. if tenant_no:
  92. q = q.filter(BizCellProductType.tenant_no == tenant_no)
  93. if exclude_id:
  94. q = q.filter(BizCellProductType.id != exclude_id)
  95. return db.query(q.exists()).scalar()
  96. def create_cell_product_type(db, *, product_type_code, product_type_name, tenant_no="zkxw", order_num=None, remark=None, user_id=None):
  97. if cell_product_type_code_exists(db, product_type_code, tenant_no):
  98. raise ValueError(f"产品类型编码 {product_type_code} 已存在")
  99. obj = BizCellProductType(
  100. id=generate_uuid(),
  101. product_type_code=product_type_code,
  102. product_type_name=product_type_name,
  103. tenant_no=tenant_no,
  104. order_num=order_num or 0,
  105. remark=remark or "",
  106. create_by=user_id,
  107. create_time=datetime.now(),
  108. update_by=user_id,
  109. update_time=datetime.now(),
  110. del_flag=0,
  111. )
  112. db.add(obj)
  113. db.commit()
  114. db.refresh(obj)
  115. return obj
  116. def update_cell_product_type(db, type_id, *, product_type_code=None, product_type_name=None, order_num=None, remark=None, user_id=None, tenant_no=None):
  117. obj = get_cell_product_type(db, type_id, tenant_no)
  118. if not obj:
  119. raise ValueError("记录不存在")
  120. if product_type_code is not None and product_type_code != obj.product_type_code:
  121. if cell_product_type_code_exists(db, product_type_code, tenant_no, exclude_id=type_id):
  122. raise ValueError(f"产品类型编码 {product_type_code} 已存在")
  123. obj.product_type_code = product_type_code
  124. if product_type_name is not None:
  125. obj.product_type_name = product_type_name
  126. if order_num is not None:
  127. obj.order_num = order_num
  128. if remark is not None:
  129. obj.remark = remark
  130. obj.update_by = user_id
  131. obj.update_time = datetime.now()
  132. db.add(obj)
  133. db.commit()
  134. db.refresh(obj)
  135. return obj
  136. def soft_delete_cell_product_type(db, type_id, tenant_no=None):
  137. obj = get_cell_product_type(db, type_id, tenant_no)
  138. if not obj:
  139. raise ValueError("记录不存在")
  140. obj.del_flag = 1
  141. obj.update_time = datetime.now()
  142. db.add(obj)
  143. db.commit()
  144. # ────────────────────────────────────────────
  145. # Entity 2: BizCellProductSpec 细胞产品规格
  146. # ────────────────────────────────────────────
  147. def _row_to_dict_cell_product_spec(row) -> Dict[str, Any]:
  148. return {
  149. "id": row.id,
  150. "specCode": row.spec_code or "",
  151. "specName": row.spec_name or "",
  152. "tenantNo": row.tenant_no or "",
  153. "orderNum": row.order_num if row.order_num is not None else 0,
  154. "remark": row.remark or "",
  155. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  156. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  157. }
  158. def list_cell_product_specs(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
  159. def _filtered():
  160. q0 = db.query(BizCellProductSpec).filter(BizCellProductSpec.del_flag == 0)
  161. if tenant_no:
  162. q0 = q0.filter(BizCellProductSpec.tenant_no == tenant_no)
  163. kw = (keyword or "").strip()
  164. if kw:
  165. like = f"%{kw}%"
  166. q0 = q0.filter(
  167. or_(
  168. BizCellProductSpec.spec_code.like(like),
  169. BizCellProductSpec.spec_name.like(like),
  170. )
  171. )
  172. if user_info:
  173. from app.core.data_scope import apply_data_scope
  174. q0 = apply_data_scope(q0, BizCellProductSpec, user_info, db, tenant_no)
  175. return q0
  176. total = _filtered().order_by(None).with_entities(func.count(BizCellProductSpec.id)).scalar() or 0
  177. page = max(1, page)
  178. page_size = min(max(1, page_size), 100)
  179. rows = _filtered().order_by(BizCellProductSpec.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
  180. return [_row_to_dict_cell_product_spec(x) for x in rows], int(total)
  181. def get_cell_product_spec(db, spec_id, tenant_no=None):
  182. q = db.query(BizCellProductSpec).filter(and_(BizCellProductSpec.id == spec_id, BizCellProductSpec.del_flag == 0))
  183. if tenant_no:
  184. q = q.filter(BizCellProductSpec.tenant_no == tenant_no)
  185. return q.first()
  186. def cell_product_spec_code_exists(db, code, tenant_no, exclude_id=None):
  187. q = db.query(BizCellProductSpec).filter(
  188. and_(BizCellProductSpec.spec_code == code, BizCellProductSpec.del_flag == 0)
  189. )
  190. if tenant_no:
  191. q = q.filter(BizCellProductSpec.tenant_no == tenant_no)
  192. if exclude_id:
  193. q = q.filter(BizCellProductSpec.id != exclude_id)
  194. return db.query(q.exists()).scalar()
  195. def create_cell_product_spec(db, *, spec_code, spec_name, tenant_no="zkxw", order_num=None, remark=None, user_id=None):
  196. if cell_product_spec_code_exists(db, spec_code, tenant_no):
  197. raise ValueError(f"规格编码 {spec_code} 已存在")
  198. obj = BizCellProductSpec(
  199. id=generate_uuid(),
  200. spec_code=spec_code,
  201. spec_name=spec_name,
  202. tenant_no=tenant_no,
  203. order_num=order_num or 0,
  204. remark=remark or "",
  205. create_by=user_id,
  206. create_time=datetime.now(),
  207. update_by=user_id,
  208. update_time=datetime.now(),
  209. del_flag=0,
  210. )
  211. db.add(obj)
  212. db.commit()
  213. db.refresh(obj)
  214. return obj
  215. def update_cell_product_spec(db, spec_id, *, spec_code=None, spec_name=None, order_num=None, remark=None, user_id=None, tenant_no=None):
  216. obj = get_cell_product_spec(db, spec_id, tenant_no)
  217. if not obj:
  218. raise ValueError("记录不存在")
  219. if spec_code is not None and spec_code != obj.spec_code:
  220. if cell_product_spec_code_exists(db, spec_code, tenant_no, exclude_id=spec_id):
  221. raise ValueError(f"规格编码 {spec_code} 已存在")
  222. obj.spec_code = spec_code
  223. if spec_name is not None:
  224. obj.spec_name = spec_name
  225. if order_num is not None:
  226. obj.order_num = order_num
  227. if remark is not None:
  228. obj.remark = remark
  229. obj.update_by = user_id
  230. obj.update_time = datetime.now()
  231. db.add(obj)
  232. db.commit()
  233. db.refresh(obj)
  234. return obj
  235. def soft_delete_cell_product_spec(db, spec_id, tenant_no=None):
  236. obj = get_cell_product_spec(db, spec_id, tenant_no)
  237. if not obj:
  238. raise ValueError("记录不存在")
  239. obj.del_flag = 1
  240. obj.update_time = datetime.now()
  241. db.add(obj)
  242. db.commit()
  243. # ────────────────────────────────────────────
  244. # Entity 3: BizCellProductPrice 细胞产品价格
  245. # ────────────────────────────────────────────
  246. def _row_to_dict_cell_product_price(row) -> Dict[str, Any]:
  247. return {
  248. "id": row.id,
  249. "projectName": row.project_name or "",
  250. "projectCode": row.project_code or "",
  251. "cellTypeCode": row.cell_type_code or "",
  252. "cellTypeName": row.cell_type_name or "",
  253. "specText": row.spec_text or "",
  254. "specId": row.spec_id or "",
  255. "cellProductTypeId": row.cell_product_type_id or "",
  256. "unitPrice": float(row.unit_price) if row.unit_price is not None else None,
  257. "tenantNo": row.tenant_no or "",
  258. "orderNum": row.order_num if row.order_num is not None else 0,
  259. "remark": row.remark or "",
  260. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  261. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  262. }
  263. def list_cell_product_prices(db, *, page=1, page_size=10, keyword=None, tenant_no=None, cell_product_type_id=None, user_info=None):
  264. def _filtered():
  265. q0 = db.query(BizCellProductPrice).filter(BizCellProductPrice.del_flag == 0)
  266. if tenant_no:
  267. q0 = q0.filter(BizCellProductPrice.tenant_no == tenant_no)
  268. if cell_product_type_id:
  269. q0 = q0.filter(BizCellProductPrice.cell_product_type_id == cell_product_type_id)
  270. kw = (keyword or "").strip()
  271. if kw:
  272. like = f"%{kw}%"
  273. q0 = q0.filter(
  274. or_(
  275. BizCellProductPrice.project_name.like(like),
  276. BizCellProductPrice.project_code.like(like),
  277. BizCellProductPrice.cell_type_name.like(like),
  278. )
  279. )
  280. if user_info:
  281. from app.core.data_scope import apply_data_scope
  282. q0 = apply_data_scope(q0, BizCellProductPrice, user_info, db, tenant_no)
  283. return q0
  284. total = _filtered().order_by(None).with_entities(func.count(BizCellProductPrice.id)).scalar() or 0
  285. page = max(1, page)
  286. page_size = min(max(1, page_size), 100)
  287. rows = _filtered().order_by(BizCellProductPrice.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
  288. return [_row_to_dict_cell_product_price(x) for x in rows], int(total)
  289. def get_cell_product_price(db, price_id, tenant_no=None):
  290. q = db.query(BizCellProductPrice).filter(and_(BizCellProductPrice.id == price_id, BizCellProductPrice.del_flag == 0))
  291. if tenant_no:
  292. q = q.filter(BizCellProductPrice.tenant_no == tenant_no)
  293. return q.first()
  294. def cell_product_price_code_exists(db, code, tenant_no, exclude_id=None):
  295. q = db.query(BizCellProductPrice).filter(
  296. and_(BizCellProductPrice.project_code == code, BizCellProductPrice.del_flag == 0)
  297. )
  298. if tenant_no:
  299. q = q.filter(BizCellProductPrice.tenant_no == tenant_no)
  300. if exclude_id:
  301. q = q.filter(BizCellProductPrice.id != exclude_id)
  302. return db.query(q.exists()).scalar()
  303. def create_cell_product_price(
  304. db,
  305. *,
  306. project_name,
  307. project_code,
  308. cell_type_code,
  309. cell_type_name,
  310. spec_text,
  311. spec_id,
  312. cell_product_type_id,
  313. unit_price,
  314. tenant_no="zkxw",
  315. order_num=None,
  316. remark=None,
  317. user_id=None,
  318. ):
  319. if cell_product_price_code_exists(db, project_code, tenant_no):
  320. raise ValueError(f"项目编码 {project_code} 已存在")
  321. obj = BizCellProductPrice(
  322. id=generate_uuid(),
  323. project_name=project_name,
  324. project_code=project_code,
  325. cell_type_code=cell_type_code,
  326. cell_type_name=cell_type_name,
  327. spec_text=spec_text or "",
  328. spec_id=spec_id or "",
  329. cell_product_type_id=cell_product_type_id or "",
  330. unit_price=unit_price,
  331. tenant_no=tenant_no,
  332. order_num=order_num or 0,
  333. remark=remark or "",
  334. create_by=user_id,
  335. create_time=datetime.now(),
  336. update_by=user_id,
  337. update_time=datetime.now(),
  338. del_flag=0,
  339. )
  340. db.add(obj)
  341. db.commit()
  342. db.refresh(obj)
  343. return obj
  344. def update_cell_product_price(
  345. db,
  346. price_id,
  347. *,
  348. project_name=None,
  349. project_code=None,
  350. cell_type_code=None,
  351. cell_type_name=None,
  352. spec_text=None,
  353. spec_id=None,
  354. cell_product_type_id=None,
  355. unit_price=None,
  356. order_num=None,
  357. remark=None,
  358. user_id=None,
  359. tenant_no=None,
  360. ):
  361. obj = get_cell_product_price(db, price_id, tenant_no)
  362. if not obj:
  363. raise ValueError("记录不存在")
  364. if project_code is not None and project_code != obj.project_code:
  365. if cell_product_price_code_exists(db, project_code, tenant_no, exclude_id=price_id):
  366. raise ValueError(f"项目编码 {project_code} 已存在")
  367. obj.project_code = project_code
  368. if project_name is not None:
  369. obj.project_name = project_name
  370. if cell_type_code is not None:
  371. obj.cell_type_code = cell_type_code
  372. if cell_type_name is not None:
  373. obj.cell_type_name = cell_type_name
  374. if spec_text is not None:
  375. obj.spec_text = spec_text
  376. if spec_id is not None:
  377. obj.spec_id = spec_id
  378. if cell_product_type_id is not None:
  379. obj.cell_product_type_id = cell_product_type_id
  380. if unit_price is not None:
  381. obj.unit_price = unit_price
  382. if order_num is not None:
  383. obj.order_num = order_num
  384. if remark is not None:
  385. obj.remark = remark
  386. obj.update_by = user_id
  387. obj.update_time = datetime.now()
  388. db.add(obj)
  389. db.commit()
  390. db.refresh(obj)
  391. return obj
  392. def soft_delete_cell_product_price(db, price_id, tenant_no=None):
  393. obj = get_cell_product_price(db, price_id, tenant_no)
  394. if not obj:
  395. raise ValueError("记录不存在")
  396. obj.del_flag = 1
  397. obj.update_time = datetime.now()
  398. db.add(obj)
  399. db.commit()
  400. # ────────────────────────────────────────────
  401. # Entity 4: BizCellProductProjectSystem 项目价格体系(主)
  402. # ────────────────────────────────────────────
  403. def _row_to_dict_project_system(row) -> Dict[str, Any]:
  404. return {
  405. "id": row.id,
  406. "systemName": row.system_name or "",
  407. "systemCode": row.system_code or "",
  408. "status": row.status or "1",
  409. "tenantNo": row.tenant_no or "",
  410. "orderNum": row.order_num if row.order_num is not None else 0,
  411. "remark": row.remark or "",
  412. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  413. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  414. }
  415. def list_project_systems(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
  416. def _filtered():
  417. q0 = db.query(BizCellProductProjectSystem).filter(BizCellProductProjectSystem.del_flag == 0)
  418. if tenant_no:
  419. q0 = q0.filter(BizCellProductProjectSystem.tenant_no == tenant_no)
  420. kw = (keyword or "").strip()
  421. if kw:
  422. like = f"%{kw}%"
  423. q0 = q0.filter(
  424. or_(
  425. BizCellProductProjectSystem.system_code.like(like),
  426. BizCellProductProjectSystem.system_name.like(like),
  427. )
  428. )
  429. if user_info:
  430. from app.core.data_scope import apply_data_scope
  431. q0 = apply_data_scope(q0, BizCellProductProjectSystem, user_info, db, tenant_no)
  432. return q0
  433. total = _filtered().order_by(None).with_entities(func.count(BizCellProductProjectSystem.id)).scalar() or 0
  434. page = max(1, page)
  435. page_size = min(max(1, page_size), 100)
  436. rows = _filtered().order_by(BizCellProductProjectSystem.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
  437. return [_row_to_dict_project_system(x) for x in rows], int(total)
  438. def get_project_system(db, system_id, tenant_no=None):
  439. q = db.query(BizCellProductProjectSystem).filter(
  440. and_(BizCellProductProjectSystem.id == system_id, BizCellProductProjectSystem.del_flag == 0)
  441. )
  442. if tenant_no:
  443. q = q.filter(BizCellProductProjectSystem.tenant_no == tenant_no)
  444. return q.first()
  445. def project_system_code_exists(db, code, tenant_no, exclude_id=None):
  446. q = db.query(BizCellProductProjectSystem).filter(
  447. and_(BizCellProductProjectSystem.system_code == code, BizCellProductProjectSystem.del_flag == 0)
  448. )
  449. if tenant_no:
  450. q = q.filter(BizCellProductProjectSystem.tenant_no == tenant_no)
  451. if exclude_id:
  452. q = q.filter(BizCellProductProjectSystem.id != exclude_id)
  453. return db.query(q.exists()).scalar()
  454. def create_project_system(db, *, system_name, system_code, status="1", tenant_no="zkxw", order_num=None, remark=None, user_id=None):
  455. if project_system_code_exists(db, system_code, tenant_no):
  456. raise ValueError(f"体系编码 {system_code} 已存在")
  457. obj = BizCellProductProjectSystem(
  458. id=generate_uuid(),
  459. system_name=system_name,
  460. system_code=system_code,
  461. status=status or "1",
  462. tenant_no=tenant_no,
  463. order_num=order_num or 0,
  464. remark=remark or "",
  465. create_by=user_id,
  466. create_time=datetime.now(),
  467. update_by=user_id,
  468. update_time=datetime.now(),
  469. del_flag=0,
  470. )
  471. db.add(obj)
  472. db.commit()
  473. db.refresh(obj)
  474. return obj
  475. def update_project_system(db, system_id, *, system_name=None, system_code=None, status=None, order_num=None, remark=None, user_id=None, tenant_no=None):
  476. obj = get_project_system(db, system_id, tenant_no)
  477. if not obj:
  478. raise ValueError("记录不存在")
  479. if system_code is not None and system_code != obj.system_code:
  480. if project_system_code_exists(db, system_code, tenant_no, exclude_id=system_id):
  481. raise ValueError(f"体系编码 {system_code} 已存在")
  482. obj.system_code = system_code
  483. if system_name is not None:
  484. obj.system_name = system_name
  485. if status is not None:
  486. obj.status = status
  487. if order_num is not None:
  488. obj.order_num = order_num
  489. if remark is not None:
  490. obj.remark = remark
  491. obj.update_by = user_id
  492. obj.update_time = datetime.now()
  493. db.add(obj)
  494. db.commit()
  495. db.refresh(obj)
  496. return obj
  497. def soft_delete_project_system(db, system_id, tenant_no=None):
  498. obj = get_project_system(db, system_id, tenant_no)
  499. if not obj:
  500. raise ValueError("记录不存在")
  501. obj.del_flag = 1
  502. obj.update_time = datetime.now()
  503. db.add(obj)
  504. db.commit()
  505. # ────────────────────────────────────────────
  506. # Entity 5: BizCellProductProjectSystemItem 项目价格体系明细
  507. # ────────────────────────────────────────────
  508. def _row_to_dict_project_system_item(row) -> Dict[str, Any]:
  509. return {
  510. "id": row.id,
  511. "projectSystemId": row.project_system_id or "",
  512. "sourcePriceId": row.source_price_id or "",
  513. "projectName": row.project_name or "",
  514. "projectCode": row.project_code or "",
  515. "cellTypeCode": row.cell_type_code or "",
  516. "cellTypeName": row.cell_type_name or "",
  517. "specText": row.spec_text or "",
  518. "specId": row.spec_id or "",
  519. "cellProductTypeId": row.cell_product_type_id or "",
  520. "unitPrice": float(row.unit_price) if row.unit_price is not None else None,
  521. "tenantNo": row.tenant_no or "",
  522. "orderNum": row.order_num if row.order_num is not None else 0,
  523. "remark": row.remark or "",
  524. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  525. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  526. }
  527. def list_project_system_items(db, project_system_id, tenant_no=None, user_info=None):
  528. q = db.query(BizCellProductProjectSystemItem).filter(
  529. and_(
  530. BizCellProductProjectSystemItem.project_system_id == project_system_id,
  531. BizCellProductProjectSystemItem.del_flag == 0,
  532. )
  533. )
  534. if tenant_no:
  535. q = q.filter(BizCellProductProjectSystemItem.tenant_no == tenant_no)
  536. if user_info:
  537. from app.core.data_scope import apply_data_scope
  538. q = apply_data_scope(q, BizCellProductProjectSystemItem, user_info, db, tenant_no)
  539. rows = q.order_by(
  540. BizCellProductProjectSystemItem.order_num.asc(),
  541. BizCellProductProjectSystemItem.create_time.asc(),
  542. ).all()
  543. return [_row_to_dict_project_system_item(x) for x in rows]
  544. def create_project_system_item(
  545. db,
  546. *,
  547. project_system_id,
  548. source_price_id="",
  549. project_name,
  550. project_code,
  551. cell_type_code,
  552. cell_type_name,
  553. spec_text="",
  554. spec_id="",
  555. cell_product_type_id="",
  556. unit_price,
  557. tenant_no="zkxw",
  558. order_num=None,
  559. remark=None,
  560. user_id=None,
  561. ):
  562. obj = BizCellProductProjectSystemItem(
  563. id=generate_uuid(),
  564. project_system_id=project_system_id,
  565. source_price_id=source_price_id,
  566. project_name=project_name,
  567. project_code=project_code,
  568. cell_type_code=cell_type_code,
  569. cell_type_name=cell_type_name,
  570. spec_text=spec_text or "",
  571. spec_id=spec_id or "",
  572. cell_product_type_id=cell_product_type_id or "",
  573. unit_price=unit_price,
  574. tenant_no=tenant_no,
  575. order_num=order_num or 0,
  576. remark=remark or "",
  577. create_by=user_id,
  578. create_time=datetime.now(),
  579. update_by=user_id,
  580. update_time=datetime.now(),
  581. del_flag=0,
  582. )
  583. db.add(obj)
  584. db.commit()
  585. db.refresh(obj)
  586. return obj
  587. def update_project_system_item(
  588. db,
  589. item_id,
  590. *,
  591. source_price_id=None,
  592. project_name=None,
  593. project_code=None,
  594. cell_type_code=None,
  595. cell_type_name=None,
  596. spec_text=None,
  597. spec_id=None,
  598. cell_product_type_id=None,
  599. unit_price=None,
  600. order_num=None,
  601. remark=None,
  602. user_id=None,
  603. tenant_no=None,
  604. ):
  605. q = db.query(BizCellProductProjectSystemItem).filter(
  606. and_(BizCellProductProjectSystemItem.id == item_id, BizCellProductProjectSystemItem.del_flag == 0)
  607. )
  608. if tenant_no:
  609. q = q.filter(BizCellProductProjectSystemItem.tenant_no == tenant_no)
  610. obj = q.first()
  611. if not obj:
  612. raise ValueError("记录不存在")
  613. if source_price_id is not None:
  614. obj.source_price_id = source_price_id
  615. if project_name is not None:
  616. obj.project_name = project_name
  617. if project_code is not None:
  618. obj.project_code = project_code
  619. if cell_type_code is not None:
  620. obj.cell_type_code = cell_type_code
  621. if cell_type_name is not None:
  622. obj.cell_type_name = cell_type_name
  623. if spec_text is not None:
  624. obj.spec_text = spec_text
  625. if spec_id is not None:
  626. obj.spec_id = spec_id
  627. if cell_product_type_id is not None:
  628. obj.cell_product_type_id = cell_product_type_id
  629. if unit_price is not None:
  630. obj.unit_price = unit_price
  631. if order_num is not None:
  632. obj.order_num = order_num
  633. if remark is not None:
  634. obj.remark = remark
  635. obj.update_by = user_id
  636. obj.update_time = datetime.now()
  637. db.add(obj)
  638. db.commit()
  639. db.refresh(obj)
  640. return obj
  641. def soft_delete_project_system_item(db, item_id, tenant_no=None):
  642. q = db.query(BizCellProductProjectSystemItem).filter(
  643. and_(BizCellProductProjectSystemItem.id == item_id, BizCellProductProjectSystemItem.del_flag == 0)
  644. )
  645. if tenant_no:
  646. q = q.filter(BizCellProductProjectSystemItem.tenant_no == tenant_no)
  647. obj = q.first()
  648. if not obj:
  649. raise ValueError("记录不存在")
  650. obj.del_flag = 1
  651. obj.update_time = datetime.now()
  652. db.add(obj)
  653. db.commit()
  654. # ────────────────────────────────────────────
  655. # Entity 6: BizChannel 渠道信息
  656. # ────────────────────────────────────────────
  657. def _row_to_dict_channel(
  658. row,
  659. db: Optional[Session] = None,
  660. tenant_no: Optional[str] = None,
  661. ) -> Dict[str, Any]:
  662. tn = tenant_no or row.tenant_no
  663. return {
  664. "id": row.id,
  665. "channelName": row.channel_name or "",
  666. "creditCode": row.credit_code or "",
  667. "channelType": row.channel_type or "",
  668. "contactName": row.contact_name or "",
  669. "contactPhone": row.contact_phone or "",
  670. "contactEmail": row.contact_email or "",
  671. "protocolAttachments": row.protocol_attachments or "",
  672. "attachmentFiles": urls_to_attachment_list(row.protocol_attachments, db, tn) if db else [],
  673. "tenantNo": row.tenant_no or "",
  674. "orderNum": row.order_num if row.order_num is not None else 0,
  675. "remark": row.remark or "",
  676. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  677. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  678. }
  679. def list_channels(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
  680. def _filtered():
  681. q0 = db.query(BizChannel).filter(BizChannel.del_flag == 0)
  682. if tenant_no:
  683. q0 = q0.filter(BizChannel.tenant_no == tenant_no)
  684. kw = (keyword or "").strip()
  685. if kw:
  686. like = f"%{kw}%"
  687. q0 = q0.filter(
  688. or_(
  689. BizChannel.channel_name.like(like),
  690. BizChannel.credit_code.like(like),
  691. BizChannel.contact_name.like(like),
  692. )
  693. )
  694. if user_info:
  695. from app.core.data_scope import apply_data_scope
  696. q0 = apply_data_scope(q0, BizChannel, user_info, db, tenant_no)
  697. return q0
  698. total = _filtered().order_by(None).with_entities(func.count(BizChannel.id)).scalar() or 0
  699. page = max(1, page)
  700. page_size = min(max(1, page_size), 100)
  701. rows = _filtered().order_by(BizChannel.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
  702. return [_row_to_dict_channel(x, db, tenant_no) for x in rows], int(total)
  703. def get_channel(db, channel_id, tenant_no=None):
  704. q = db.query(BizChannel).filter(and_(BizChannel.id == channel_id, BizChannel.del_flag == 0))
  705. if tenant_no:
  706. q = q.filter(BizChannel.tenant_no == tenant_no)
  707. return q.first()
  708. def create_channel(
  709. db,
  710. *,
  711. channel_name,
  712. credit_code=None,
  713. channel_type=None,
  714. contact_name=None,
  715. contact_phone=None,
  716. contact_email=None,
  717. protocol_attachments=None,
  718. tenant_no="zkxw",
  719. order_num=None,
  720. remark=None,
  721. user_id=None,
  722. ):
  723. obj = BizChannel(
  724. id=generate_uuid(),
  725. channel_name=channel_name,
  726. credit_code=credit_code or "",
  727. channel_type=channel_type or "",
  728. contact_name=contact_name or "",
  729. contact_phone=contact_phone or "",
  730. contact_email=contact_email or "",
  731. protocol_attachments=protocol_attachments or "",
  732. tenant_no=tenant_no,
  733. order_num=order_num or 0,
  734. remark=remark or "",
  735. create_by=user_id,
  736. create_time=datetime.now(),
  737. update_by=user_id,
  738. update_time=datetime.now(),
  739. del_flag=0,
  740. )
  741. db.add(obj)
  742. db.commit()
  743. db.refresh(obj)
  744. return obj
  745. def update_channel(
  746. db,
  747. channel_id,
  748. *,
  749. channel_name=None,
  750. credit_code=None,
  751. channel_type=None,
  752. contact_name=None,
  753. contact_phone=None,
  754. contact_email=None,
  755. protocol_attachments=None,
  756. order_num=None,
  757. remark=None,
  758. user_id=None,
  759. tenant_no=None,
  760. ):
  761. obj = get_channel(db, channel_id, tenant_no)
  762. if not obj:
  763. raise ValueError("记录不存在")
  764. if channel_name is not None:
  765. obj.channel_name = channel_name
  766. if credit_code is not None:
  767. obj.credit_code = credit_code
  768. if channel_type is not None:
  769. obj.channel_type = channel_type
  770. if contact_name is not None:
  771. obj.contact_name = contact_name
  772. if contact_phone is not None:
  773. obj.contact_phone = contact_phone
  774. if contact_email is not None:
  775. obj.contact_email = contact_email
  776. if protocol_attachments is not None:
  777. obj.protocol_attachments = protocol_attachments
  778. if order_num is not None:
  779. obj.order_num = order_num
  780. if remark is not None:
  781. obj.remark = remark
  782. obj.update_by = user_id
  783. obj.update_time = datetime.now()
  784. db.add(obj)
  785. db.commit()
  786. db.refresh(obj)
  787. return obj
  788. def soft_delete_channel(db, channel_id, tenant_no=None):
  789. obj = get_channel(db, channel_id, tenant_no)
  790. if not obj:
  791. raise ValueError("记录不存在")
  792. obj.del_flag = 1
  793. obj.update_time = datetime.now()
  794. db.add(obj)
  795. db.commit()
  796. # ────────────────────────────────────────────
  797. # Entity 7: BizChannelInvoice 渠道开票信息
  798. # ────────────────────────────────────────────
  799. def _row_to_dict_channel_invoice(row) -> Dict[str, Any]:
  800. return {
  801. "id": row.id,
  802. "channelId": row.channel_id or "",
  803. "companyName": row.company_name or "",
  804. "taxNo": row.tax_no or "",
  805. "companyAddress": row.company_address or "",
  806. "companyPhone": row.company_phone or "",
  807. "bankName": row.bank_name or "",
  808. "bankAccount": row.bank_account or "",
  809. "tenantNo": row.tenant_no or "",
  810. "orderNum": row.order_num if row.order_num is not None else 0,
  811. "remark": row.remark or "",
  812. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  813. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  814. }
  815. def list_channel_invoices(db, channel_id, tenant_no=None, user_info=None):
  816. q = db.query(BizChannelInvoice).filter(
  817. and_(
  818. BizChannelInvoice.channel_id == channel_id,
  819. BizChannelInvoice.del_flag == 0,
  820. )
  821. )
  822. if tenant_no:
  823. q = q.filter(BizChannelInvoice.tenant_no == tenant_no)
  824. if user_info:
  825. from app.core.data_scope import apply_data_scope
  826. q = apply_data_scope(q, BizChannelInvoice, user_info, db, tenant_no)
  827. rows = q.order_by(BizChannelInvoice.create_time.desc()).all()
  828. return [_row_to_dict_channel_invoice(x) for x in rows]
  829. def create_channel_invoice(
  830. db,
  831. *,
  832. channel_id,
  833. company_name,
  834. tax_no=None,
  835. company_address=None,
  836. company_phone=None,
  837. bank_name=None,
  838. bank_account=None,
  839. tenant_no="zkxw",
  840. order_num=None,
  841. remark=None,
  842. user_id=None,
  843. ):
  844. obj = BizChannelInvoice(
  845. id=generate_uuid(),
  846. channel_id=channel_id,
  847. company_name=company_name,
  848. tax_no=tax_no or "",
  849. company_address=company_address or "",
  850. company_phone=company_phone or "",
  851. bank_name=bank_name or "",
  852. bank_account=bank_account or "",
  853. tenant_no=tenant_no,
  854. order_num=order_num or 0,
  855. remark=remark or "",
  856. create_by=user_id,
  857. create_time=datetime.now(),
  858. update_by=user_id,
  859. update_time=datetime.now(),
  860. del_flag=0,
  861. )
  862. db.add(obj)
  863. db.commit()
  864. db.refresh(obj)
  865. return obj
  866. def update_channel_invoice(
  867. db,
  868. invoice_id,
  869. *,
  870. company_name=None,
  871. tax_no=None,
  872. company_address=None,
  873. company_phone=None,
  874. bank_name=None,
  875. bank_account=None,
  876. order_num=None,
  877. remark=None,
  878. user_id=None,
  879. tenant_no=None,
  880. ):
  881. q = db.query(BizChannelInvoice).filter(
  882. and_(BizChannelInvoice.id == invoice_id, BizChannelInvoice.del_flag == 0)
  883. )
  884. if tenant_no:
  885. q = q.filter(BizChannelInvoice.tenant_no == tenant_no)
  886. obj = q.first()
  887. if not obj:
  888. raise ValueError("记录不存在")
  889. if company_name is not None:
  890. obj.company_name = company_name
  891. if tax_no is not None:
  892. obj.tax_no = tax_no
  893. if company_address is not None:
  894. obj.company_address = company_address
  895. if company_phone is not None:
  896. obj.company_phone = company_phone
  897. if bank_name is not None:
  898. obj.bank_name = bank_name
  899. if bank_account is not None:
  900. obj.bank_account = bank_account
  901. if order_num is not None:
  902. obj.order_num = order_num
  903. if remark is not None:
  904. obj.remark = remark
  905. obj.update_by = user_id
  906. obj.update_time = datetime.now()
  907. db.add(obj)
  908. db.commit()
  909. db.refresh(obj)
  910. return obj
  911. def soft_delete_channel_invoice(db, invoice_id, tenant_no=None):
  912. q = db.query(BizChannelInvoice).filter(
  913. and_(BizChannelInvoice.id == invoice_id, BizChannelInvoice.del_flag == 0)
  914. )
  915. if tenant_no:
  916. q = q.filter(BizChannelInvoice.tenant_no == tenant_no)
  917. obj = q.first()
  918. if not obj:
  919. raise ValueError("记录不存在")
  920. obj.del_flag = 1
  921. obj.update_time = datetime.now()
  922. db.add(obj)
  923. db.commit()
  924. # ────────────────────────────────────────────
  925. # Entity 8: BizChannelMailAddress 渠道邮寄地址
  926. # ────────────────────────────────────────────
  927. def _row_to_dict_channel_mail_address(row) -> Dict[str, Any]:
  928. return {
  929. "id": row.id,
  930. "channelId": row.channel_id or "",
  931. "mailAddress": row.mail_address or "",
  932. "mailContact": row.mail_contact or "",
  933. "mailPhone": row.mail_phone or "",
  934. "tenantNo": row.tenant_no or "",
  935. "orderNum": row.order_num if row.order_num is not None else 0,
  936. "remark": row.remark or "",
  937. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  938. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  939. }
  940. def list_channel_mail_addresses(db, channel_id, tenant_no=None, user_info=None):
  941. q = db.query(BizChannelMailAddress).filter(
  942. and_(
  943. BizChannelMailAddress.channel_id == channel_id,
  944. BizChannelMailAddress.del_flag == 0,
  945. )
  946. )
  947. if tenant_no:
  948. q = q.filter(BizChannelMailAddress.tenant_no == tenant_no)
  949. if user_info:
  950. from app.core.data_scope import apply_data_scope
  951. q = apply_data_scope(q, BizChannelMailAddress, user_info, db, tenant_no)
  952. rows = q.order_by(BizChannelMailAddress.create_time.desc()).all()
  953. return [_row_to_dict_channel_mail_address(x) for x in rows]
  954. def create_channel_mail_address(
  955. db,
  956. *,
  957. channel_id,
  958. mail_address,
  959. mail_contact=None,
  960. mail_phone=None,
  961. tenant_no="zkxw",
  962. order_num=None,
  963. remark=None,
  964. user_id=None,
  965. ):
  966. obj = BizChannelMailAddress(
  967. id=generate_uuid(),
  968. channel_id=channel_id,
  969. mail_address=mail_address,
  970. mail_contact=mail_contact or "",
  971. mail_phone=mail_phone or "",
  972. tenant_no=tenant_no,
  973. order_num=order_num or 0,
  974. remark=remark or "",
  975. create_by=user_id,
  976. create_time=datetime.now(),
  977. update_by=user_id,
  978. update_time=datetime.now(),
  979. del_flag=0,
  980. )
  981. db.add(obj)
  982. db.commit()
  983. db.refresh(obj)
  984. return obj
  985. def update_channel_mail_address(
  986. db,
  987. address_id,
  988. *,
  989. mail_address=None,
  990. mail_contact=None,
  991. mail_phone=None,
  992. order_num=None,
  993. remark=None,
  994. user_id=None,
  995. tenant_no=None,
  996. ):
  997. q = db.query(BizChannelMailAddress).filter(
  998. and_(BizChannelMailAddress.id == address_id, BizChannelMailAddress.del_flag == 0)
  999. )
  1000. if tenant_no:
  1001. q = q.filter(BizChannelMailAddress.tenant_no == tenant_no)
  1002. obj = q.first()
  1003. if not obj:
  1004. raise ValueError("记录不存在")
  1005. if mail_address is not None:
  1006. obj.mail_address = mail_address
  1007. if mail_contact is not None:
  1008. obj.mail_contact = mail_contact
  1009. if mail_phone is not None:
  1010. obj.mail_phone = mail_phone
  1011. if order_num is not None:
  1012. obj.order_num = order_num
  1013. if remark is not None:
  1014. obj.remark = remark
  1015. obj.update_by = user_id
  1016. obj.update_time = datetime.now()
  1017. db.add(obj)
  1018. db.commit()
  1019. db.refresh(obj)
  1020. return obj
  1021. def soft_delete_channel_mail_address(db, address_id, tenant_no=None):
  1022. q = db.query(BizChannelMailAddress).filter(
  1023. and_(BizChannelMailAddress.id == address_id, BizChannelMailAddress.del_flag == 0)
  1024. )
  1025. if tenant_no:
  1026. q = q.filter(BizChannelMailAddress.tenant_no == tenant_no)
  1027. obj = q.first()
  1028. if not obj:
  1029. raise ValueError("记录不存在")
  1030. obj.del_flag = 1
  1031. obj.update_time = datetime.now()
  1032. db.add(obj)
  1033. db.commit()
  1034. # ────────────────────────────────────────────
  1035. # Entity 9: BizChannelAgreement 渠道协议
  1036. # ────────────────────────────────────────────
  1037. def _row_to_dict_channel_agreement(row) -> Dict[str, Any]:
  1038. def _fmt_date(v):
  1039. """CHAR(8) YYYYMMDD → YYYY-MM-DD"""
  1040. if not v or len(v) != 8:
  1041. return v or ""
  1042. return f"{v[:4]}-{v[4:6]}-{v[6:8]}"
  1043. return {
  1044. "id": row.id,
  1045. "channelId": row.channel_id or "",
  1046. "agreementType": row.agreement_type or "",
  1047. "businessType": row.business_type or "",
  1048. "settleMode": row.settle_mode or "",
  1049. "creditLimitAmount": float(row.credit_limit_amount) if row.credit_limit_amount is not None else None,
  1050. "effectiveDate": _fmt_date(row.effective_date),
  1051. "expireDate": _fmt_date(row.expire_date),
  1052. "agreementStatus": row.agreement_status or "",
  1053. "tenantNo": row.tenant_no or "",
  1054. "orderNum": row.order_num if row.order_num is not None else 0,
  1055. "remark": row.remark or "",
  1056. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  1057. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  1058. }
  1059. def list_channel_agreements(db, channel_id, tenant_no=None, user_info=None):
  1060. q = db.query(BizChannelAgreement).filter(
  1061. and_(
  1062. BizChannelAgreement.channel_id == channel_id,
  1063. BizChannelAgreement.del_flag == 0,
  1064. )
  1065. )
  1066. if tenant_no:
  1067. q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
  1068. if user_info:
  1069. from app.core.data_scope import apply_data_scope
  1070. q = apply_data_scope(q, BizChannelAgreement, user_info, db, tenant_no)
  1071. rows = q.order_by(BizChannelAgreement.create_time.desc()).all()
  1072. return [_row_to_dict_channel_agreement(x) for x in rows]
  1073. def create_channel_agreement(
  1074. db,
  1075. *,
  1076. channel_id,
  1077. agreement_type,
  1078. business_type=None,
  1079. settle_mode=None,
  1080. credit_limit_amount=None,
  1081. effective_date=None,
  1082. expire_date=None,
  1083. agreement_status="dsx",
  1084. tenant_no="zkxw",
  1085. order_num=None,
  1086. remark=None,
  1087. user_id=None,
  1088. ):
  1089. _ed = (effective_date or "").replace("-", "") if effective_date else ""
  1090. _xd = (expire_date or "").replace("-", "") if expire_date else ""
  1091. obj = BizChannelAgreement(
  1092. id=generate_uuid(),
  1093. channel_id=channel_id,
  1094. agreement_type=agreement_type,
  1095. business_type=business_type or "",
  1096. settle_mode=settle_mode or "",
  1097. credit_limit_amount=credit_limit_amount,
  1098. effective_date=_ed,
  1099. expire_date=_xd,
  1100. agreement_status=agreement_status or "dsx",
  1101. tenant_no=tenant_no,
  1102. order_num=order_num or 0,
  1103. remark=remark or "",
  1104. create_by=user_id,
  1105. create_time=datetime.now(),
  1106. update_by=user_id,
  1107. update_time=datetime.now(),
  1108. del_flag=0,
  1109. )
  1110. db.add(obj)
  1111. db.commit()
  1112. db.refresh(obj)
  1113. return obj
  1114. def update_channel_agreement(
  1115. db,
  1116. agreement_id,
  1117. *,
  1118. agreement_type=None,
  1119. business_type=None,
  1120. settle_mode=None,
  1121. credit_limit_amount=None,
  1122. effective_date=None,
  1123. expire_date=None,
  1124. order_num=None,
  1125. remark=None,
  1126. user_id=None,
  1127. tenant_no=None,
  1128. ):
  1129. q = db.query(BizChannelAgreement).filter(
  1130. and_(BizChannelAgreement.id == agreement_id, BizChannelAgreement.del_flag == 0)
  1131. )
  1132. if tenant_no:
  1133. q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
  1134. obj = q.first()
  1135. if not obj:
  1136. raise ValueError("记录不存在")
  1137. if agreement_type is not None:
  1138. obj.agreement_type = agreement_type
  1139. if business_type is not None:
  1140. obj.business_type = business_type
  1141. if settle_mode is not None:
  1142. obj.settle_mode = settle_mode
  1143. if credit_limit_amount is not None:
  1144. obj.credit_limit_amount = credit_limit_amount
  1145. if effective_date is not None:
  1146. obj.effective_date = effective_date.replace("-", "") if effective_date else ""
  1147. if expire_date is not None:
  1148. obj.expire_date = expire_date.replace("-", "") if expire_date else ""
  1149. if order_num is not None:
  1150. obj.order_num = order_num
  1151. if remark is not None:
  1152. obj.remark = remark
  1153. obj.update_by = user_id
  1154. obj.update_time = datetime.now()
  1155. db.add(obj)
  1156. db.commit()
  1157. db.refresh(obj)
  1158. return obj
  1159. def soft_delete_channel_agreement(db, agreement_id, tenant_no=None):
  1160. q = db.query(BizChannelAgreement).filter(
  1161. and_(BizChannelAgreement.id == agreement_id, BizChannelAgreement.del_flag == 0)
  1162. )
  1163. if tenant_no:
  1164. q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
  1165. obj = q.first()
  1166. if not obj:
  1167. raise ValueError("记录不存在")
  1168. obj.del_flag = 1
  1169. obj.update_time = datetime.now()
  1170. db.add(obj)
  1171. db.commit()
  1172. def activate_channel_agreement(db, agreement_id, tenant_no=None, user_id=None):
  1173. """生效协议:仅待生效(dsx)可变为生效中(sxz)"""
  1174. q = db.query(BizChannelAgreement).filter(
  1175. and_(BizChannelAgreement.id == agreement_id, BizChannelAgreement.del_flag == 0)
  1176. )
  1177. if tenant_no:
  1178. q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
  1179. obj = q.first()
  1180. if not obj:
  1181. raise ValueError("记录不存在")
  1182. if (obj.agreement_status or "").strip() != "dsx":
  1183. raise ValueError("仅待生效状态的协议可执行生效操作")
  1184. obj.agreement_status = "sxz"
  1185. obj.update_by = user_id
  1186. obj.update_time = datetime.now()
  1187. db.add(obj)
  1188. db.commit()
  1189. db.refresh(obj)
  1190. return obj
  1191. def void_channel_agreement(db, agreement_id, tenant_no=None, user_id=None):
  1192. """作废协议:设置 agreement_status='yzf'"""
  1193. q = db.query(BizChannelAgreement).filter(
  1194. and_(BizChannelAgreement.id == agreement_id, BizChannelAgreement.del_flag == 0)
  1195. )
  1196. if tenant_no:
  1197. q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
  1198. obj = q.first()
  1199. if not obj:
  1200. raise ValueError("记录不存在")
  1201. obj.agreement_status = "yzf"
  1202. obj.update_by = user_id
  1203. obj.update_time = datetime.now()
  1204. db.add(obj)
  1205. db.commit()
  1206. db.refresh(obj)
  1207. return obj
  1208. # ────────────────────────────────────────────
  1209. # Entity 10: BizChannelAgreementProductPrice 渠道协议产品价格
  1210. # ────────────────────────────────────────────
  1211. def _row_to_dict_channel_agreement_price(row) -> Dict[str, Any]:
  1212. return {
  1213. "id": row.id,
  1214. "channelAgreementId": row.channel_agreement_id or "",
  1215. "projectName": row.project_name or "",
  1216. "projectCode": row.project_code or "",
  1217. "cellTypeCode": row.cell_type_code or "",
  1218. "cellTypeName": row.cell_type_name or "",
  1219. "specText": row.spec_text or "",
  1220. "originalPrice": float(row.original_price) if row.original_price is not None else None,
  1221. "agreementPrice": float(row.agreement_price) if row.agreement_price is not None else None,
  1222. "tenantNo": row.tenant_no or "",
  1223. "orderNum": row.order_num if row.order_num is not None else 0,
  1224. "remark": row.remark or "",
  1225. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  1226. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  1227. }
  1228. def _channel_agreement_price_item_key(
  1229. project_code=None,
  1230. project_name=None,
  1231. cell_type_code=None,
  1232. cell_type_name=None,
  1233. spec_text=None,
  1234. ) -> str:
  1235. code = (project_code or project_name or "").strip().lower()
  1236. cell = (cell_type_code or cell_type_name or "").strip().lower()
  1237. spec = (spec_text or "").strip().lower()
  1238. return f"{code}|{cell}|{spec}"
  1239. def _channel_agreement_price_exists(
  1240. db,
  1241. channel_agreement_id,
  1242. *,
  1243. project_code=None,
  1244. project_name=None,
  1245. cell_type_code=None,
  1246. cell_type_name=None,
  1247. spec_text=None,
  1248. tenant_no=None,
  1249. exclude_price_id=None,
  1250. ):
  1251. target_key = _channel_agreement_price_item_key(
  1252. project_code=project_code,
  1253. project_name=project_name,
  1254. cell_type_code=cell_type_code,
  1255. cell_type_name=cell_type_name,
  1256. spec_text=spec_text,
  1257. )
  1258. q = db.query(BizChannelAgreementProductPrice).filter(
  1259. and_(
  1260. BizChannelAgreementProductPrice.channel_agreement_id == channel_agreement_id,
  1261. BizChannelAgreementProductPrice.del_flag == 0,
  1262. )
  1263. )
  1264. if tenant_no:
  1265. q = q.filter(BizChannelAgreementProductPrice.tenant_no == tenant_no)
  1266. if exclude_price_id:
  1267. q = q.filter(BizChannelAgreementProductPrice.id != exclude_price_id)
  1268. for row in q.all():
  1269. if _channel_agreement_price_item_key(
  1270. project_code=row.project_code,
  1271. project_name=row.project_name,
  1272. cell_type_code=row.cell_type_code,
  1273. cell_type_name=row.cell_type_name,
  1274. spec_text=row.spec_text,
  1275. ) == target_key:
  1276. return True
  1277. return False
  1278. def list_channel_agreement_prices(db, channel_agreement_id, tenant_no=None, user_info=None):
  1279. q = db.query(BizChannelAgreementProductPrice).filter(
  1280. and_(
  1281. BizChannelAgreementProductPrice.channel_agreement_id == channel_agreement_id,
  1282. BizChannelAgreementProductPrice.del_flag == 0,
  1283. )
  1284. )
  1285. if tenant_no:
  1286. q = q.filter(BizChannelAgreementProductPrice.tenant_no == tenant_no)
  1287. if user_info:
  1288. from app.core.data_scope import apply_data_scope
  1289. q = apply_data_scope(q, BizChannelAgreementProductPrice, user_info, db, tenant_no)
  1290. rows = q.order_by(BizChannelAgreementProductPrice.create_time.desc()).all()
  1291. return [_row_to_dict_channel_agreement_price(x) for x in rows]
  1292. def create_channel_agreement_price(
  1293. db,
  1294. *,
  1295. channel_agreement_id,
  1296. project_name=None,
  1297. project_code=None,
  1298. cell_type_code=None,
  1299. cell_type_name=None,
  1300. spec_text=None,
  1301. original_price=None,
  1302. agreement_price=None,
  1303. tenant_no="zkxw",
  1304. order_num=None,
  1305. remark=None,
  1306. user_id=None,
  1307. ):
  1308. if _channel_agreement_price_exists(
  1309. db,
  1310. channel_agreement_id,
  1311. project_code=project_code,
  1312. project_name=project_name,
  1313. cell_type_code=cell_type_code,
  1314. cell_type_name=cell_type_name,
  1315. spec_text=spec_text,
  1316. tenant_no=tenant_no,
  1317. ):
  1318. raise ValueError("该项目信息已存在,不可重复添加")
  1319. obj = BizChannelAgreementProductPrice(
  1320. id=generate_uuid(),
  1321. channel_agreement_id=channel_agreement_id,
  1322. project_name=project_name or "",
  1323. project_code=project_code or "",
  1324. cell_type_code=cell_type_code or "",
  1325. cell_type_name=cell_type_name or "",
  1326. spec_text=spec_text or "",
  1327. original_price=original_price,
  1328. agreement_price=agreement_price,
  1329. tenant_no=tenant_no,
  1330. order_num=order_num or 0,
  1331. remark=remark or "",
  1332. create_by=user_id,
  1333. create_time=datetime.now(),
  1334. update_by=user_id,
  1335. update_time=datetime.now(),
  1336. del_flag=0,
  1337. )
  1338. db.add(obj)
  1339. db.commit()
  1340. db.refresh(obj)
  1341. return obj
  1342. def update_channel_agreement_price(
  1343. db,
  1344. price_id,
  1345. *,
  1346. project_name=None,
  1347. project_code=None,
  1348. cell_type_code=None,
  1349. cell_type_name=None,
  1350. spec_text=None,
  1351. original_price=None,
  1352. agreement_price=None,
  1353. order_num=None,
  1354. remark=None,
  1355. user_id=None,
  1356. tenant_no=None,
  1357. ):
  1358. q = db.query(BizChannelAgreementProductPrice).filter(
  1359. and_(BizChannelAgreementProductPrice.id == price_id, BizChannelAgreementProductPrice.del_flag == 0)
  1360. )
  1361. if tenant_no:
  1362. q = q.filter(BizChannelAgreementProductPrice.tenant_no == tenant_no)
  1363. obj = q.first()
  1364. if not obj:
  1365. raise ValueError("记录不存在")
  1366. next_project_name = project_name if project_name is not None else obj.project_name
  1367. next_project_code = project_code if project_code is not None else obj.project_code
  1368. next_cell_type_code = cell_type_code if cell_type_code is not None else obj.cell_type_code
  1369. next_cell_type_name = cell_type_name if cell_type_name is not None else obj.cell_type_name
  1370. next_spec_text = spec_text if spec_text is not None else obj.spec_text
  1371. if _channel_agreement_price_exists(
  1372. db,
  1373. obj.channel_agreement_id,
  1374. project_code=next_project_code,
  1375. project_name=next_project_name,
  1376. cell_type_code=next_cell_type_code,
  1377. cell_type_name=next_cell_type_name,
  1378. spec_text=next_spec_text,
  1379. tenant_no=tenant_no,
  1380. exclude_price_id=price_id,
  1381. ):
  1382. raise ValueError("该项目信息已存在,不可重复添加")
  1383. if project_name is not None:
  1384. obj.project_name = project_name
  1385. if project_code is not None:
  1386. obj.project_code = project_code
  1387. if cell_type_code is not None:
  1388. obj.cell_type_code = cell_type_code
  1389. if cell_type_name is not None:
  1390. obj.cell_type_name = cell_type_name
  1391. if spec_text is not None:
  1392. obj.spec_text = spec_text
  1393. if original_price is not None:
  1394. obj.original_price = original_price
  1395. if agreement_price is not None:
  1396. obj.agreement_price = agreement_price
  1397. if order_num is not None:
  1398. obj.order_num = order_num
  1399. if remark is not None:
  1400. obj.remark = remark
  1401. obj.update_by = user_id
  1402. obj.update_time = datetime.now()
  1403. db.add(obj)
  1404. db.commit()
  1405. db.refresh(obj)
  1406. return obj
  1407. def soft_delete_channel_agreement_price(db, price_id, tenant_no=None):
  1408. q = db.query(BizChannelAgreementProductPrice).filter(
  1409. and_(BizChannelAgreementProductPrice.id == price_id, BizChannelAgreementProductPrice.del_flag == 0)
  1410. )
  1411. if tenant_no:
  1412. q = q.filter(BizChannelAgreementProductPrice.tenant_no == tenant_no)
  1413. obj = q.first()
  1414. if not obj:
  1415. raise ValueError("记录不存在")
  1416. obj.del_flag = 1
  1417. obj.update_time = datetime.now()
  1418. db.add(obj)
  1419. db.commit()
  1420. # ────────────────────────────────────────────
  1421. # Entity 11: BizCheckupInstitution 体检机构
  1422. # ────────────────────────────────────────────
  1423. def _row_to_dict_checkup_institution(row, projects: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
  1424. data = {
  1425. "id": row.id,
  1426. "institutionName": row.institution_name or "",
  1427. "institutionAddress": row.institution_address or "",
  1428. "contactName": row.contact_name or "",
  1429. "contactPhone": row.contact_phone or "",
  1430. "institutionLogo": row.institution_logo or "",
  1431. "status": row.status or "1",
  1432. "tenantNo": row.tenant_no or "",
  1433. "orderNum": row.order_num if row.order_num is not None else 0,
  1434. "remark": row.remark or "",
  1435. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  1436. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  1437. }
  1438. if projects is not None:
  1439. data["projects"] = projects
  1440. return data
  1441. def list_checkup_institutions(db, *, page=1, page_size=10, keyword=None, status=None, tenant_no=None, user_info=None):
  1442. def _filtered():
  1443. q0 = db.query(BizCheckupInstitution).filter(BizCheckupInstitution.del_flag == 0)
  1444. if tenant_no:
  1445. q0 = q0.filter(BizCheckupInstitution.tenant_no == tenant_no)
  1446. kw = (keyword or "").strip()
  1447. if kw:
  1448. like = f"%{kw}%"
  1449. q0 = q0.filter(
  1450. or_(
  1451. BizCheckupInstitution.institution_name.like(like),
  1452. BizCheckupInstitution.contact_name.like(like),
  1453. )
  1454. )
  1455. st = (status or "").strip()
  1456. if st in ("0", "1"):
  1457. q0 = q0.filter(BizCheckupInstitution.status == st)
  1458. if user_info:
  1459. from app.core.data_scope import apply_data_scope
  1460. q0 = apply_data_scope(q0, BizCheckupInstitution, user_info, db, tenant_no)
  1461. return q0
  1462. total = _filtered().order_by(None).with_entities(func.count(BizCheckupInstitution.id)).scalar() or 0
  1463. page = max(1, page)
  1464. page_size = min(max(1, page_size), 100)
  1465. rows = _filtered().order_by(BizCheckupInstitution.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
  1466. institution_ids = [x.id for x in rows]
  1467. projects_by_inst: Dict[str, List[Dict[str, Any]]] = {iid: [] for iid in institution_ids}
  1468. if institution_ids:
  1469. pq = db.query(BizCheckupInstitutionProject).filter(
  1470. BizCheckupInstitutionProject.checkup_institution_id.in_(institution_ids),
  1471. BizCheckupInstitutionProject.del_flag == 0,
  1472. )
  1473. if tenant_no:
  1474. pq = pq.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
  1475. if user_info:
  1476. from app.core.data_scope import apply_data_scope
  1477. pq = apply_data_scope(pq, BizCheckupInstitutionProject, user_info, db, tenant_no)
  1478. for pr in pq.order_by(BizCheckupInstitutionProject.create_time.desc()).all():
  1479. projects_by_inst.setdefault(pr.checkup_institution_id, []).append(
  1480. _row_to_dict_checkup_institution_project(pr)
  1481. )
  1482. return [
  1483. _row_to_dict_checkup_institution(x, projects_by_inst.get(x.id, [])) for x in rows
  1484. ], int(total)
  1485. def get_checkup_institution(db, institution_id, tenant_no=None):
  1486. q = db.query(BizCheckupInstitution).filter(
  1487. and_(BizCheckupInstitution.id == institution_id, BizCheckupInstitution.del_flag == 0)
  1488. )
  1489. if tenant_no:
  1490. q = q.filter(BizCheckupInstitution.tenant_no == tenant_no)
  1491. return q.first()
  1492. def create_checkup_institution(
  1493. db,
  1494. *,
  1495. institution_name,
  1496. institution_address=None,
  1497. contact_name=None,
  1498. contact_phone=None,
  1499. institution_logo=None,
  1500. status="1",
  1501. tenant_no="zkxw",
  1502. order_num=None,
  1503. remark=None,
  1504. user_id=None,
  1505. ):
  1506. obj = BizCheckupInstitution(
  1507. id=generate_uuid(),
  1508. institution_name=institution_name,
  1509. institution_address=institution_address or "",
  1510. contact_name=contact_name or "",
  1511. contact_phone=contact_phone or "",
  1512. institution_logo=(institution_logo or "").strip() or None,
  1513. status=status or "1",
  1514. tenant_no=tenant_no,
  1515. order_num=order_num or 0,
  1516. remark=remark or "",
  1517. create_by=user_id,
  1518. create_time=datetime.now(),
  1519. update_by=user_id,
  1520. update_time=datetime.now(),
  1521. del_flag=0,
  1522. )
  1523. db.add(obj)
  1524. db.commit()
  1525. db.refresh(obj)
  1526. return obj
  1527. def update_checkup_institution(
  1528. db,
  1529. institution_id,
  1530. *,
  1531. institution_name=None,
  1532. institution_address=None,
  1533. contact_name=None,
  1534. contact_phone=None,
  1535. institution_logo=None,
  1536. status=None,
  1537. order_num=None,
  1538. remark=None,
  1539. user_id=None,
  1540. tenant_no=None,
  1541. ):
  1542. obj = get_checkup_institution(db, institution_id, tenant_no)
  1543. if not obj:
  1544. raise ValueError("记录不存在")
  1545. if institution_name is not None:
  1546. obj.institution_name = institution_name
  1547. if institution_address is not None:
  1548. obj.institution_address = institution_address
  1549. if contact_name is not None:
  1550. obj.contact_name = contact_name
  1551. if contact_phone is not None:
  1552. obj.contact_phone = contact_phone
  1553. if institution_logo is not None:
  1554. obj.institution_logo = (institution_logo or "").strip() or None
  1555. if status is not None:
  1556. obj.status = status
  1557. if order_num is not None:
  1558. obj.order_num = order_num
  1559. if remark is not None:
  1560. obj.remark = remark
  1561. obj.update_by = user_id
  1562. obj.update_time = datetime.now()
  1563. db.add(obj)
  1564. db.commit()
  1565. db.refresh(obj)
  1566. return obj
  1567. def soft_delete_checkup_institution(db, institution_id, tenant_no=None):
  1568. obj = get_checkup_institution(db, institution_id, tenant_no)
  1569. if not obj:
  1570. raise ValueError("记录不存在")
  1571. obj.del_flag = 1
  1572. obj.update_time = datetime.now()
  1573. db.add(obj)
  1574. db.commit()
  1575. # ────────────────────────────────────────────
  1576. # Entity 12: BizCheckupInstitutionInvoice 体检机构开票信息
  1577. # ────────────────────────────────────────────
  1578. def _row_to_dict_checkup_institution_invoice(row) -> Dict[str, Any]:
  1579. return {
  1580. "id": row.id,
  1581. "checkupInstitutionId": row.checkup_institution_id or "",
  1582. "companyName": row.company_name or "",
  1583. "taxNo": row.tax_no or "",
  1584. "companyAddress": row.company_address or "",
  1585. "companyPhone": row.company_phone or "",
  1586. "bankName": row.bank_name or "",
  1587. "bankAccount": row.bank_account or "",
  1588. "tenantNo": row.tenant_no or "",
  1589. "orderNum": row.order_num if row.order_num is not None else 0,
  1590. "remark": row.remark or "",
  1591. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  1592. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  1593. }
  1594. def list_checkup_institution_invoices(db, checkup_institution_id, tenant_no=None, user_info=None):
  1595. q = db.query(BizCheckupInstitutionInvoice).filter(
  1596. and_(
  1597. BizCheckupInstitutionInvoice.checkup_institution_id == checkup_institution_id,
  1598. BizCheckupInstitutionInvoice.del_flag == 0,
  1599. )
  1600. )
  1601. if tenant_no:
  1602. q = q.filter(BizCheckupInstitutionInvoice.tenant_no == tenant_no)
  1603. if user_info:
  1604. from app.core.data_scope import apply_data_scope
  1605. q = apply_data_scope(q, BizCheckupInstitutionInvoice, user_info, db, tenant_no)
  1606. rows = q.order_by(BizCheckupInstitutionInvoice.create_time.desc()).all()
  1607. return [_row_to_dict_checkup_institution_invoice(x) for x in rows]
  1608. def create_checkup_institution_invoice(
  1609. db,
  1610. *,
  1611. checkup_institution_id,
  1612. company_name,
  1613. tax_no=None,
  1614. company_address=None,
  1615. company_phone=None,
  1616. bank_name=None,
  1617. bank_account=None,
  1618. tenant_no="zkxw",
  1619. order_num=None,
  1620. remark=None,
  1621. user_id=None,
  1622. ):
  1623. obj = BizCheckupInstitutionInvoice(
  1624. id=generate_uuid(),
  1625. checkup_institution_id=checkup_institution_id,
  1626. company_name=company_name,
  1627. tax_no=tax_no or "",
  1628. company_address=company_address or "",
  1629. company_phone=company_phone or "",
  1630. bank_name=bank_name or "",
  1631. bank_account=bank_account or "",
  1632. tenant_no=tenant_no,
  1633. order_num=order_num or 0,
  1634. remark=remark or "",
  1635. create_by=user_id,
  1636. create_time=datetime.now(),
  1637. update_by=user_id,
  1638. update_time=datetime.now(),
  1639. del_flag=0,
  1640. )
  1641. db.add(obj)
  1642. db.commit()
  1643. db.refresh(obj)
  1644. return obj
  1645. def update_checkup_institution_invoice(
  1646. db,
  1647. invoice_id,
  1648. *,
  1649. company_name=None,
  1650. tax_no=None,
  1651. company_address=None,
  1652. company_phone=None,
  1653. bank_name=None,
  1654. bank_account=None,
  1655. order_num=None,
  1656. remark=None,
  1657. user_id=None,
  1658. tenant_no=None,
  1659. ):
  1660. q = db.query(BizCheckupInstitutionInvoice).filter(
  1661. and_(BizCheckupInstitutionInvoice.id == invoice_id, BizCheckupInstitutionInvoice.del_flag == 0)
  1662. )
  1663. if tenant_no:
  1664. q = q.filter(BizCheckupInstitutionInvoice.tenant_no == tenant_no)
  1665. obj = q.first()
  1666. if not obj:
  1667. raise ValueError("记录不存在")
  1668. if company_name is not None:
  1669. obj.company_name = company_name
  1670. if tax_no is not None:
  1671. obj.tax_no = tax_no
  1672. if company_address is not None:
  1673. obj.company_address = company_address
  1674. if company_phone is not None:
  1675. obj.company_phone = company_phone
  1676. if bank_name is not None:
  1677. obj.bank_name = bank_name
  1678. if bank_account is not None:
  1679. obj.bank_account = bank_account
  1680. if order_num is not None:
  1681. obj.order_num = order_num
  1682. if remark is not None:
  1683. obj.remark = remark
  1684. obj.update_by = user_id
  1685. obj.update_time = datetime.now()
  1686. db.add(obj)
  1687. db.commit()
  1688. db.refresh(obj)
  1689. return obj
  1690. def soft_delete_checkup_institution_invoice(db, invoice_id, tenant_no=None):
  1691. q = db.query(BizCheckupInstitutionInvoice).filter(
  1692. and_(BizCheckupInstitutionInvoice.id == invoice_id, BizCheckupInstitutionInvoice.del_flag == 0)
  1693. )
  1694. if tenant_no:
  1695. q = q.filter(BizCheckupInstitutionInvoice.tenant_no == tenant_no)
  1696. obj = q.first()
  1697. if not obj:
  1698. raise ValueError("记录不存在")
  1699. obj.del_flag = 1
  1700. obj.update_time = datetime.now()
  1701. db.add(obj)
  1702. db.commit()
  1703. # ────────────────────────────────────────────
  1704. # Entity 13: BizCheckupInstitutionProject 体检机构项目及价格
  1705. # ────────────────────────────────────────────
  1706. def _row_to_dict_checkup_institution_project(row) -> Dict[str, Any]:
  1707. return {
  1708. "id": row.id,
  1709. "checkupInstitutionId": row.checkup_institution_id or "",
  1710. "projectName": row.project_name or "",
  1711. "projectFee": float(row.project_fee) if row.project_fee is not None else None,
  1712. "status": row.status or "1",
  1713. "tenantNo": row.tenant_no or "",
  1714. "orderNum": row.order_num if row.order_num is not None else 0,
  1715. "remark": row.remark or "",
  1716. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  1717. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  1718. }
  1719. def list_checkup_institution_projects(db, checkup_institution_id, tenant_no=None, user_info=None):
  1720. q = db.query(BizCheckupInstitutionProject).filter(
  1721. and_(
  1722. BizCheckupInstitutionProject.checkup_institution_id == checkup_institution_id,
  1723. BizCheckupInstitutionProject.del_flag == 0,
  1724. )
  1725. )
  1726. if tenant_no:
  1727. q = q.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
  1728. if user_info:
  1729. from app.core.data_scope import apply_data_scope
  1730. q = apply_data_scope(q, BizCheckupInstitutionProject, user_info, db, tenant_no)
  1731. rows = q.order_by(BizCheckupInstitutionProject.create_time.desc()).all()
  1732. return [_row_to_dict_checkup_institution_project(x) for x in rows]
  1733. def create_checkup_institution_project(
  1734. db,
  1735. *,
  1736. checkup_institution_id,
  1737. project_name,
  1738. project_fee=None,
  1739. status="1",
  1740. tenant_no="zkxw",
  1741. order_num=None,
  1742. remark=None,
  1743. user_id=None,
  1744. ):
  1745. obj = BizCheckupInstitutionProject(
  1746. id=generate_uuid(),
  1747. checkup_institution_id=checkup_institution_id,
  1748. project_name=project_name,
  1749. project_fee=project_fee,
  1750. status=status or "1",
  1751. tenant_no=tenant_no,
  1752. order_num=order_num or 0,
  1753. remark=remark or "",
  1754. create_by=user_id,
  1755. create_time=datetime.now(),
  1756. update_by=user_id,
  1757. update_time=datetime.now(),
  1758. del_flag=0,
  1759. )
  1760. db.add(obj)
  1761. db.commit()
  1762. db.refresh(obj)
  1763. return obj
  1764. def update_checkup_institution_project(
  1765. db,
  1766. project_id,
  1767. *,
  1768. project_name=None,
  1769. project_fee=None,
  1770. status=None,
  1771. order_num=None,
  1772. remark=None,
  1773. user_id=None,
  1774. tenant_no=None,
  1775. ):
  1776. q = db.query(BizCheckupInstitutionProject).filter(
  1777. and_(BizCheckupInstitutionProject.id == project_id, BizCheckupInstitutionProject.del_flag == 0)
  1778. )
  1779. if tenant_no:
  1780. q = q.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
  1781. obj = q.first()
  1782. if not obj:
  1783. raise ValueError("记录不存在")
  1784. if project_name is not None:
  1785. obj.project_name = project_name
  1786. if project_fee is not None:
  1787. obj.project_fee = project_fee
  1788. if status is not None:
  1789. obj.status = status
  1790. if order_num is not None:
  1791. obj.order_num = order_num
  1792. if remark is not None:
  1793. obj.remark = remark
  1794. obj.update_by = user_id
  1795. obj.update_time = datetime.now()
  1796. db.add(obj)
  1797. db.commit()
  1798. db.refresh(obj)
  1799. return obj
  1800. def soft_delete_checkup_institution_project(db, project_id, tenant_no=None):
  1801. q = db.query(BizCheckupInstitutionProject).filter(
  1802. and_(BizCheckupInstitutionProject.id == project_id, BizCheckupInstitutionProject.del_flag == 0)
  1803. )
  1804. if tenant_no:
  1805. q = q.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
  1806. obj = q.first()
  1807. if not obj:
  1808. raise ValueError("记录不存在")
  1809. obj.del_flag = 1
  1810. obj.update_time = datetime.now()
  1811. db.add(obj)
  1812. db.commit()
  1813. # ────────────────────────────────────────────
  1814. # Entity 14: BizCheckupInstitutionPackage 体检机构项目套餐
  1815. # ────────────────────────────────────────────
  1816. def _sum_package_item_fees(items: List[Dict[str, Any]]) -> float:
  1817. total = 0.0
  1818. for it in items:
  1819. fee = it.get("project_fee")
  1820. if fee is None and "projectFee" in it:
  1821. fee = it.get("projectFee")
  1822. total += float(fee) if fee is not None else 0.0
  1823. return total
  1824. def _row_to_dict_checkup_institution_package(
  1825. row,
  1826. *,
  1827. total_fee: Optional[float] = None,
  1828. item_count: Optional[int] = None,
  1829. ) -> Dict[str, Any]:
  1830. d = {
  1831. "id": row.id,
  1832. "checkupInstitutionId": row.checkup_institution_id or "",
  1833. "packageName": row.package_name or "",
  1834. "packageCode": row.package_code or "",
  1835. "status": row.status or "1",
  1836. "tenantNo": row.tenant_no or "",
  1837. "orderNum": row.order_num if row.order_num is not None else 0,
  1838. "remark": row.remark or "",
  1839. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  1840. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  1841. }
  1842. if total_fee is not None:
  1843. d["totalFee"] = total_fee
  1844. if item_count is not None:
  1845. d["itemCount"] = item_count
  1846. return d
  1847. def _row_to_dict_checkup_institution_package_item(
  1848. row,
  1849. *,
  1850. source_project_deleted: bool = False,
  1851. source_project_status: Optional[str] = None,
  1852. ) -> Dict[str, Any]:
  1853. return {
  1854. "id": row.id,
  1855. "packageId": row.package_id or "",
  1856. "checkupInstitutionProjectId": row.checkup_institution_project_id or "",
  1857. "projectName": row.project_name or "",
  1858. "projectFee": float(row.project_fee) if row.project_fee is not None else None,
  1859. "status": source_project_status if source_project_status is not None else "1",
  1860. "sourceProjectDeleted": source_project_deleted,
  1861. "tenantNo": row.tenant_no or "",
  1862. "orderNum": row.order_num if row.order_num is not None else 0,
  1863. "remark": row.remark or "",
  1864. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  1865. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  1866. }
  1867. def checkup_package_code_exists(db, institution_id, code, tenant_no, exclude_id=None):
  1868. q = db.query(BizCheckupInstitutionPackage).filter(
  1869. and_(
  1870. BizCheckupInstitutionPackage.checkup_institution_id == institution_id,
  1871. BizCheckupInstitutionPackage.package_code == code,
  1872. BizCheckupInstitutionPackage.del_flag == 0,
  1873. )
  1874. )
  1875. if tenant_no:
  1876. q = q.filter(BizCheckupInstitutionPackage.tenant_no == tenant_no)
  1877. if exclude_id:
  1878. q = q.filter(BizCheckupInstitutionPackage.id != exclude_id)
  1879. return db.query(q.exists()).scalar()
  1880. def _get_checkup_project_for_package(db, project_id, institution_id, tenant_no=None):
  1881. q = db.query(BizCheckupInstitutionProject).filter(
  1882. BizCheckupInstitutionProject.id == project_id,
  1883. BizCheckupInstitutionProject.checkup_institution_id == institution_id,
  1884. )
  1885. if tenant_no:
  1886. q = q.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
  1887. return q.first()
  1888. def _validate_package_items_for_save(
  1889. db,
  1890. institution_id,
  1891. items,
  1892. tenant_no=None,
  1893. existing_item_project_ids=None,
  1894. ):
  1895. if not items:
  1896. raise ValueError("套餐至少包含一个体检项目")
  1897. existing_ids = set(existing_item_project_ids or [])
  1898. seen = set()
  1899. for it in items:
  1900. pid = it.get("checkup_institution_project_id") or it.get("checkupInstitutionProjectId")
  1901. if not pid:
  1902. raise ValueError("套餐明细缺少机构项目")
  1903. if pid in seen:
  1904. raise ValueError("套餐内不能重复添加同一体检项目")
  1905. seen.add(pid)
  1906. proj = _get_checkup_project_for_package(db, pid, institution_id, tenant_no)
  1907. if not proj:
  1908. raise ValueError("存在无效的体检项目")
  1909. if pid in existing_ids:
  1910. continue
  1911. if proj.del_flag != 0:
  1912. raise ValueError("不能新添加已删除的体检项目")
  1913. if (proj.status or "1") != "1":
  1914. raise ValueError("套餐不能包含已停用的体检项目")
  1915. def _list_package_items_raw(db, package_id, tenant_no=None):
  1916. q = db.query(BizCheckupInstitutionPackageItem).filter(
  1917. and_(
  1918. BizCheckupInstitutionPackageItem.package_id == package_id,
  1919. BizCheckupInstitutionPackageItem.del_flag == 0,
  1920. )
  1921. )
  1922. if tenant_no:
  1923. q = q.filter(BizCheckupInstitutionPackageItem.tenant_no == tenant_no)
  1924. return q.order_by(BizCheckupInstitutionPackageItem.order_num.asc(), BizCheckupInstitutionPackageItem.create_time.asc()).all()
  1925. def _package_items_to_dicts(db, institution_id, item_rows, tenant_no=None):
  1926. result = []
  1927. for row in item_rows:
  1928. proj = _get_checkup_project_for_package(
  1929. db, row.checkup_institution_project_id, institution_id, tenant_no
  1930. )
  1931. deleted = proj is None or proj.del_flag != 0
  1932. proj_status = (proj.status or "1") if proj else "1"
  1933. result.append(
  1934. _row_to_dict_checkup_institution_package_item(
  1935. row,
  1936. source_project_deleted=deleted,
  1937. source_project_status=proj_status,
  1938. )
  1939. )
  1940. return result
  1941. def _insert_package_items(
  1942. db,
  1943. *,
  1944. package_id,
  1945. institution_id,
  1946. items,
  1947. tenant_no,
  1948. user_id=None,
  1949. ):
  1950. for idx, it in enumerate(items):
  1951. pid = it.get("checkup_institution_project_id") or it.get("checkupInstitutionProjectId")
  1952. proj = _get_checkup_project_for_package(db, pid, institution_id, tenant_no)
  1953. name = (it.get("project_name") or it.get("projectName") or "").strip()
  1954. if not name and proj:
  1955. name = proj.project_name or ""
  1956. fee = it.get("project_fee")
  1957. if fee is None:
  1958. fee = it.get("projectFee")
  1959. obj = BizCheckupInstitutionPackageItem(
  1960. id=generate_uuid(),
  1961. package_id=package_id,
  1962. checkup_institution_project_id=pid,
  1963. project_name=name,
  1964. project_fee=fee,
  1965. tenant_no=tenant_no,
  1966. order_num=it.get("order_num") if it.get("order_num") is not None else it.get("orderNum") or (idx + 1),
  1967. remark=it.get("remark") or "",
  1968. create_by=user_id,
  1969. create_time=datetime.now(),
  1970. update_by=user_id,
  1971. update_time=datetime.now(),
  1972. del_flag=0,
  1973. )
  1974. db.add(obj)
  1975. def list_checkup_institution_packages(db, checkup_institution_id, tenant_no=None, user_info=None):
  1976. q = db.query(BizCheckupInstitutionPackage).filter(
  1977. and_(
  1978. BizCheckupInstitutionPackage.checkup_institution_id == checkup_institution_id,
  1979. BizCheckupInstitutionPackage.del_flag == 0,
  1980. )
  1981. )
  1982. if tenant_no:
  1983. q = q.filter(BizCheckupInstitutionPackage.tenant_no == tenant_no)
  1984. if user_info:
  1985. from app.core.data_scope import apply_data_scope
  1986. q = apply_data_scope(q, BizCheckupInstitutionPackage, user_info, db, tenant_no)
  1987. rows = q.order_by(BizCheckupInstitutionPackage.create_time.desc()).all()
  1988. out = []
  1989. for row in rows:
  1990. item_rows = _list_package_items_raw(db, row.id, tenant_no)
  1991. fees = [float(x.project_fee) if x.project_fee is not None else 0.0 for x in item_rows]
  1992. out.append(
  1993. _row_to_dict_checkup_institution_package(
  1994. row,
  1995. total_fee=sum(fees),
  1996. item_count=len(item_rows),
  1997. )
  1998. )
  1999. return out
  2000. def get_checkup_institution_package(db, package_id, tenant_no=None):
  2001. q = db.query(BizCheckupInstitutionPackage).filter(
  2002. and_(BizCheckupInstitutionPackage.id == package_id, BizCheckupInstitutionPackage.del_flag == 0)
  2003. )
  2004. if tenant_no:
  2005. q = q.filter(BizCheckupInstitutionPackage.tenant_no == tenant_no)
  2006. return q.first()
  2007. def get_checkup_institution_package_detail(db, package_id, tenant_no=None):
  2008. pkg = get_checkup_institution_package(db, package_id, tenant_no)
  2009. if not pkg:
  2010. return None
  2011. item_rows = _list_package_items_raw(db, package_id, tenant_no)
  2012. items = _package_items_to_dicts(db, pkg.checkup_institution_id, item_rows, tenant_no)
  2013. fees = [float(x.project_fee) if x.project_fee is not None else 0.0 for x in item_rows]
  2014. d = _row_to_dict_checkup_institution_package(pkg, total_fee=sum(fees), item_count=len(item_rows))
  2015. d["items"] = items
  2016. return d
  2017. def create_checkup_institution_package(
  2018. db,
  2019. *,
  2020. checkup_institution_id,
  2021. package_name,
  2022. package_code,
  2023. status="1",
  2024. items,
  2025. tenant_no="zkxw",
  2026. order_num=None,
  2027. remark=None,
  2028. user_id=None,
  2029. ):
  2030. code = (package_code or "").strip()
  2031. if not code:
  2032. raise ValueError("套餐编码不能为空")
  2033. if checkup_package_code_exists(db, checkup_institution_id, code, tenant_no):
  2034. raise ValueError(f"套餐编码 {code} 已存在")
  2035. _validate_package_items_for_save(db, checkup_institution_id, items, tenant_no)
  2036. pkg = BizCheckupInstitutionPackage(
  2037. id=generate_uuid(),
  2038. checkup_institution_id=checkup_institution_id,
  2039. package_name=package_name,
  2040. package_code=code,
  2041. status=status or "1",
  2042. tenant_no=tenant_no,
  2043. order_num=order_num or 0,
  2044. remark=remark or "",
  2045. create_by=user_id,
  2046. create_time=datetime.now(),
  2047. update_by=user_id,
  2048. update_time=datetime.now(),
  2049. del_flag=0,
  2050. )
  2051. db.add(pkg)
  2052. db.flush()
  2053. _insert_package_items(
  2054. db,
  2055. package_id=pkg.id,
  2056. institution_id=checkup_institution_id,
  2057. items=items,
  2058. tenant_no=tenant_no,
  2059. user_id=user_id,
  2060. )
  2061. db.commit()
  2062. db.refresh(pkg)
  2063. return get_checkup_institution_package_detail(db, pkg.id, tenant_no)
  2064. def update_checkup_institution_package(
  2065. db,
  2066. package_id,
  2067. *,
  2068. package_name=None,
  2069. package_code=None,
  2070. status=None,
  2071. items=None,
  2072. order_num=None,
  2073. remark=None,
  2074. user_id=None,
  2075. tenant_no=None,
  2076. ):
  2077. pkg = get_checkup_institution_package(db, package_id, tenant_no)
  2078. if not pkg:
  2079. raise ValueError("记录不存在")
  2080. institution_id = pkg.checkup_institution_id
  2081. if package_code is not None:
  2082. code = package_code.strip()
  2083. if not code:
  2084. raise ValueError("套餐编码不能为空")
  2085. if code != pkg.package_code and checkup_package_code_exists(
  2086. db, institution_id, code, tenant_no, exclude_id=package_id
  2087. ):
  2088. raise ValueError(f"套餐编码 {code} 已存在")
  2089. pkg.package_code = code
  2090. if package_name is not None:
  2091. pkg.package_name = package_name
  2092. if status is not None:
  2093. pkg.status = status
  2094. if order_num is not None:
  2095. pkg.order_num = order_num
  2096. if remark is not None:
  2097. pkg.remark = remark
  2098. if items is not None:
  2099. old_rows = _list_package_items_raw(db, package_id, tenant_no)
  2100. existing_pids = {r.checkup_institution_project_id for r in old_rows}
  2101. _validate_package_items_for_save(
  2102. db,
  2103. institution_id,
  2104. items,
  2105. tenant_no,
  2106. existing_item_project_ids=existing_pids,
  2107. )
  2108. # 全量替换:物理删除旧明细,避免 uk(package_id, project_id) 与软删行冲突
  2109. q_items = db.query(BizCheckupInstitutionPackageItem).filter(
  2110. BizCheckupInstitutionPackageItem.package_id == package_id
  2111. )
  2112. if tenant_no:
  2113. q_items = q_items.filter(BizCheckupInstitutionPackageItem.tenant_no == tenant_no)
  2114. for row in q_items.all():
  2115. db.delete(row)
  2116. db.flush()
  2117. _insert_package_items(
  2118. db,
  2119. package_id=package_id,
  2120. institution_id=institution_id,
  2121. items=items,
  2122. tenant_no=tenant_no or pkg.tenant_no,
  2123. user_id=user_id,
  2124. )
  2125. pkg.update_by = user_id
  2126. pkg.update_time = datetime.now()
  2127. db.add(pkg)
  2128. db.commit()
  2129. return get_checkup_institution_package_detail(db, package_id, tenant_no)
  2130. def soft_delete_checkup_institution_package(db, package_id, tenant_no=None):
  2131. pkg = get_checkup_institution_package(db, package_id, tenant_no)
  2132. if not pkg:
  2133. raise ValueError("记录不存在")
  2134. now = datetime.now()
  2135. pkg.del_flag = 1
  2136. pkg.update_time = now
  2137. db.add(pkg)
  2138. for row in _list_package_items_raw(db, package_id, tenant_no):
  2139. row.del_flag = 1
  2140. row.update_time = now
  2141. db.add(row)
  2142. db.commit()
  2143. # ────────────────────────────────────────────
  2144. # Shared utility
  2145. # ────────────────────────────────────────────
  2146. def total_pages(total, page_size):
  2147. if total <= 0:
  2148. return 0
  2149. return int(math.ceil(total / float(page_size)))
  2150. # ────────────────────────────────────────────
  2151. # Entity 15: BizChannelAccount 渠道账户
  2152. # ────────────────────────────────────────────
  2153. def _row_to_dict_channel_account(row) -> Dict[str, Any]:
  2154. return {
  2155. "id": row.id,
  2156. "channelId": row.channel_id or "",
  2157. "balanceAmount": float(row.balance_amount) if row.balance_amount else 0,
  2158. "tenantNo": row.tenant_no or "",
  2159. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  2160. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  2161. }
  2162. def get_channel_account(db, channel_id, tenant_no=None):
  2163. """只读查询渠道账户,不加行锁"""
  2164. q = db.query(BizChannelAccount).filter(
  2165. and_(BizChannelAccount.channel_id == channel_id, BizChannelAccount.del_flag == 0)
  2166. )
  2167. if tenant_no:
  2168. q = q.filter(BizChannelAccount.tenant_no == tenant_no)
  2169. return q.first()
  2170. def _get_or_create_channel_account(db, channel_id, tenant_no, user_id):
  2171. """获取或创建渠道账户(带行锁 with_for_update)"""
  2172. q = db.query(BizChannelAccount).filter(
  2173. and_(BizChannelAccount.channel_id == channel_id, BizChannelAccount.del_flag == 0)
  2174. ).with_for_update()
  2175. if tenant_no:
  2176. q = q.filter(BizChannelAccount.tenant_no == tenant_no)
  2177. obj = q.first()
  2178. if obj:
  2179. return obj
  2180. obj = BizChannelAccount(
  2181. id=generate_uuid(),
  2182. channel_id=channel_id,
  2183. balance_amount=0,
  2184. tenant_no=tenant_no or "zkxw",
  2185. order_num=0,
  2186. remark="",
  2187. create_by=user_id,
  2188. create_dept_id="",
  2189. create_time=datetime.now(),
  2190. update_by=user_id,
  2191. update_time=datetime.now(),
  2192. del_flag=0,
  2193. )
  2194. db.add(obj)
  2195. db.flush()
  2196. return obj
  2197. # ────────────────────────────────────────────
  2198. # Entity 16: BizChannelProductQuota 渠道产品份数
  2199. # ────────────────────────────────────────────
  2200. def _row_to_dict_channel_product_quota(row) -> Dict[str, Any]:
  2201. return {
  2202. "id": row.id,
  2203. "channelId": row.channel_id or "",
  2204. "projectCode": row.project_code or "",
  2205. "projectName": row.project_name or "",
  2206. "cellTypeCode": row.cell_type_code or "",
  2207. "cellTypeName": row.cell_type_name or "",
  2208. "specText": row.spec_text or "",
  2209. "totalQty": float(row.total_qty) if row.total_qty else 0,
  2210. "usedQty": float(row.used_qty) if row.used_qty else 0,
  2211. "remainingQty": float(row.remaining_qty) if row.remaining_qty else 0,
  2212. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  2213. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  2214. }
  2215. def _upsert_channel_product_quota(
  2216. db,
  2217. channel_id,
  2218. project_code,
  2219. project_name,
  2220. cell_type_code,
  2221. cell_type_name,
  2222. spec_text,
  2223. add_qty,
  2224. tenant_no,
  2225. user_id,
  2226. ):
  2227. """增加份数:已存在则累加,不存在则新建"""
  2228. q = db.query(BizChannelProductQuota).filter(
  2229. and_(
  2230. BizChannelProductQuota.channel_id == channel_id,
  2231. BizChannelProductQuota.project_code == (project_code or ""),
  2232. BizChannelProductQuota.cell_type_code == (cell_type_code or ""),
  2233. BizChannelProductQuota.spec_text == (spec_text or ""),
  2234. BizChannelProductQuota.del_flag == 0,
  2235. )
  2236. )
  2237. if tenant_no:
  2238. q = q.filter(BizChannelProductQuota.tenant_no == tenant_no)
  2239. obj = q.first()
  2240. if obj:
  2241. obj.total_qty = (float(obj.total_qty) if obj.total_qty else 0) + add_qty
  2242. obj.remaining_qty = (float(obj.remaining_qty) if obj.remaining_qty else 0) + add_qty
  2243. obj.update_by = user_id
  2244. obj.update_time = datetime.now()
  2245. db.add(obj)
  2246. db.flush()
  2247. return obj
  2248. obj = BizChannelProductQuota(
  2249. id=generate_uuid(),
  2250. channel_id=channel_id,
  2251. project_code=project_code or "",
  2252. project_name=project_name or "",
  2253. cell_type_code=cell_type_code or "",
  2254. cell_type_name=cell_type_name or "",
  2255. spec_text=spec_text or "",
  2256. total_qty=add_qty,
  2257. used_qty=0,
  2258. remaining_qty=add_qty,
  2259. tenant_no=tenant_no or "zkxw",
  2260. order_num=0,
  2261. remark="",
  2262. create_by=user_id,
  2263. create_dept_id="",
  2264. create_time=datetime.now(),
  2265. update_by=user_id,
  2266. update_time=datetime.now(),
  2267. del_flag=0,
  2268. )
  2269. db.add(obj)
  2270. db.flush()
  2271. return obj
  2272. def _reduce_channel_product_quota(db, channel_id, project_code, cell_type_code, spec_text, reduce_qty, user_id):
  2273. """扣减份数:验证 remaining_qty >= reduce_qty"""
  2274. q = db.query(BizChannelProductQuota).filter(
  2275. and_(
  2276. BizChannelProductQuota.channel_id == channel_id,
  2277. BizChannelProductQuota.project_code == (project_code or ""),
  2278. BizChannelProductQuota.cell_type_code == (cell_type_code or ""),
  2279. BizChannelProductQuota.spec_text == (spec_text or ""),
  2280. BizChannelProductQuota.del_flag == 0,
  2281. )
  2282. )
  2283. obj = q.first()
  2284. if not obj:
  2285. raise ValueError("份数记录不存在")
  2286. remaining = float(obj.remaining_qty) if obj.remaining_qty else 0
  2287. if remaining < reduce_qty:
  2288. raise ValueError(f"剩余份数不足,当前剩余 {remaining},需扣减 {reduce_qty}")
  2289. obj.total_qty = (float(obj.total_qty) if obj.total_qty else 0) - reduce_qty
  2290. obj.remaining_qty = remaining - reduce_qty
  2291. obj.update_by = user_id
  2292. obj.update_time = datetime.now()
  2293. db.add(obj)
  2294. db.flush()
  2295. return obj
  2296. def list_channel_product_quotas(db, channel_id, tenant_no=None):
  2297. """查询渠道下所有产品份数"""
  2298. q = db.query(BizChannelProductQuota).filter(
  2299. and_(
  2300. BizChannelProductQuota.channel_id == channel_id,
  2301. BizChannelProductQuota.del_flag == 0,
  2302. )
  2303. )
  2304. if tenant_no:
  2305. q = q.filter(BizChannelProductQuota.tenant_no == tenant_no)
  2306. rows = q.order_by(BizChannelProductQuota.create_time.desc()).all()
  2307. return [_row_to_dict_channel_product_quota(x) for x in rows]
  2308. # ────────────────────────────────────────────
  2309. # Entity 17: BizChannelServiceOrder 渠道服务订单
  2310. # ────────────────────────────────────────────
  2311. def _calc_gmcp_purchase_total_from_db(db, order_id, tenant_no=None) -> float:
  2312. """从订单产品持仓汇总购买总额(协议单价 × 累计购买份数)。"""
  2313. q = db.query(BizChannelServiceOrderProduct).filter(
  2314. and_(
  2315. BizChannelServiceOrderProduct.service_order_id == order_id,
  2316. BizChannelServiceOrderProduct.del_flag == 0,
  2317. )
  2318. )
  2319. if tenant_no:
  2320. q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
  2321. total = 0.0
  2322. for prod in q.all():
  2323. price = float(prod.agreement_unit_price or 0)
  2324. qty = float(prod.total_buy_qty or 0)
  2325. total += price * qty
  2326. return total
  2327. def _batch_gmcp_purchase_totals(db, order_ids, tenant_no=None) -> Dict[str, float]:
  2328. if not order_ids:
  2329. return {}
  2330. amount_expr = BizChannelServiceOrderProduct.agreement_unit_price * BizChannelServiceOrderProduct.total_buy_qty
  2331. q = db.query(
  2332. BizChannelServiceOrderProduct.service_order_id,
  2333. func.coalesce(func.sum(amount_expr), 0),
  2334. ).filter(
  2335. BizChannelServiceOrderProduct.del_flag == 0,
  2336. BizChannelServiceOrderProduct.service_order_id.in_(order_ids),
  2337. )
  2338. if tenant_no:
  2339. q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
  2340. return {row[0]: float(row[1] or 0) for row in q.group_by(BizChannelServiceOrderProduct.service_order_id).all()}
  2341. def _calc_gmcp_current_holding_amount_from_db(db, order_id, tenant_no=None) -> float:
  2342. """从订单产品持仓汇总当前余额(协议单价 × 当前持有份数)。"""
  2343. q = db.query(BizChannelServiceOrderProduct).filter(
  2344. and_(
  2345. BizChannelServiceOrderProduct.service_order_id == order_id,
  2346. BizChannelServiceOrderProduct.del_flag == 0,
  2347. )
  2348. )
  2349. if tenant_no:
  2350. q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
  2351. total = 0.0
  2352. for prod in q.all():
  2353. price = float(prod.agreement_unit_price or 0)
  2354. qty = float(prod.current_qty or 0)
  2355. total += price * qty
  2356. return total
  2357. def _batch_gmcp_current_holding_amounts(db, order_ids, tenant_no=None) -> Dict[str, float]:
  2358. if not order_ids:
  2359. return {}
  2360. amount_expr = BizChannelServiceOrderProduct.agreement_unit_price * BizChannelServiceOrderProduct.current_qty
  2361. q = db.query(
  2362. BizChannelServiceOrderProduct.service_order_id,
  2363. func.coalesce(func.sum(amount_expr), 0),
  2364. ).filter(
  2365. BizChannelServiceOrderProduct.del_flag == 0,
  2366. BizChannelServiceOrderProduct.service_order_id.in_(order_ids),
  2367. )
  2368. if tenant_no:
  2369. q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
  2370. return {row[0]: float(row[1] or 0) for row in q.group_by(BizChannelServiceOrderProduct.service_order_id).all()}
  2371. def _resolve_service_order_purchase_total(row, db=None, tenant_no=None, purchase_total_override=None) -> float:
  2372. stored = float(row.purchase_total_amount) if row.purchase_total_amount else 0
  2373. if (row.order_type or "") != "gmcp":
  2374. return stored
  2375. if purchase_total_override is not None:
  2376. return float(purchase_total_override)
  2377. if db:
  2378. computed = _calc_gmcp_purchase_total_from_db(db, row.id, tenant_no or row.tenant_no)
  2379. if computed > 0 or not row.purchase_total_amount:
  2380. return computed
  2381. return stored
  2382. def _resolve_service_order_current_balance(
  2383. row, db=None, tenant_no=None, current_balance_override=None
  2384. ) -> float:
  2385. if (row.order_type or "") == "gmcp":
  2386. if current_balance_override is not None:
  2387. return float(current_balance_override)
  2388. if db:
  2389. return _calc_gmcp_current_holding_amount_from_db(db, row.id, tenant_no or row.tenant_no)
  2390. return 0.0
  2391. return float(row.current_balance) if row.current_balance else 0
  2392. def _row_to_dict_service_order(
  2393. row,
  2394. db=None,
  2395. tenant_no=None,
  2396. *,
  2397. purchase_total_override=None,
  2398. current_balance_override=None,
  2399. ) -> Dict[str, Any]:
  2400. tn = tenant_no or row.tenant_no
  2401. purchase_total = _resolve_service_order_purchase_total(
  2402. row, db, tn, purchase_total_override=purchase_total_override
  2403. )
  2404. current_balance = _resolve_service_order_current_balance(
  2405. row, db, tn, current_balance_override=current_balance_override
  2406. )
  2407. d = {
  2408. "id": row.id,
  2409. "channelId": row.channel_id or "",
  2410. "channelName": row.channel_name or "",
  2411. "orderName": row.order_name or "",
  2412. "deptName": row.dept_name or "",
  2413. "orderType": row.order_type or "",
  2414. "prepayAmount": float(row.prepay_amount) if row.prepay_amount else 0,
  2415. "purchaseTotalAmount": purchase_total,
  2416. "orderStatus": row.order_status or "",
  2417. "channelAgreementId": row.channel_agreement_id or "",
  2418. "totalRechargeAmount": float(row.total_recharge_amount) if row.total_recharge_amount else 0,
  2419. "totalRefundAmount": float(row.total_refund_amount) if row.total_refund_amount else 0,
  2420. "currentBalance": current_balance,
  2421. "balanceBefore": float(row.balance_before) if row.balance_before else 0,
  2422. "balanceAfter": float(row.balance_after) if row.balance_after else 0,
  2423. "financeSubmitBy": row.finance_submit_by or "",
  2424. "financeSubmitTime": row.finance_submit_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_submit_time else "",
  2425. "financeSubmitRemark": row.finance_submit_remark or "",
  2426. "financeSubmitAttachments": row.finance_submit_attachments or "",
  2427. "financeSubmitAttachmentFiles": urls_to_attachment_list(row.finance_submit_attachments, db, tn) if db and row.finance_submit_attachments else [],
  2428. "financeConfirmTime": row.finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_confirm_time else "",
  2429. "financeConfirmAttachments": row.finance_confirm_attachments or "",
  2430. "financeConfirmAttachmentFiles": urls_to_attachment_list(row.finance_confirm_attachments, db, tn) if db and row.finance_confirm_attachments else [],
  2431. "financeRemark": row.finance_remark or "",
  2432. "refundApplyBy": row.refund_apply_by or "",
  2433. "refundApplyTime": row.refund_apply_time.strftime("%Y-%m-%d %H:%M:%S") if row.refund_apply_time else "",
  2434. "refundAmount": float(row.refund_amount) if row.refund_amount else 0,
  2435. "refundRemark": row.refund_remark or "",
  2436. "refundAttachments": row.refund_attachments or "",
  2437. "refundAttachmentFiles": urls_to_attachment_list(row.refund_attachments, db, tn) if db and row.refund_attachments else [],
  2438. "refundFinanceConfirmTime": row.refund_finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.refund_finance_confirm_time else "",
  2439. "refundFinanceConfirmAttachments": row.refund_finance_confirm_attachments or "",
  2440. "refundFinanceConfirmAttachmentFiles": urls_to_attachment_list(row.refund_finance_confirm_attachments, db, tn) if db and row.refund_finance_confirm_attachments else [],
  2441. "refundFinanceRemark": row.refund_finance_remark or "",
  2442. "tenantNo": row.tenant_no or "",
  2443. "remark": row.remark or "",
  2444. "createBy": row.create_by or "",
  2445. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  2446. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  2447. }
  2448. return d
  2449. def _row_to_dict_service_order_product(row) -> Dict[str, Any]:
  2450. return {
  2451. "id": row.id,
  2452. "serviceOrderId": row.service_order_id or "",
  2453. "projectCode": row.project_code or "",
  2454. "projectName": row.project_name or "",
  2455. "cellType": row.cell_type or "",
  2456. "specText": row.spec_text or "",
  2457. "agreementUnitPrice": float(row.agreement_unit_price) if row.agreement_unit_price else 0,
  2458. "buyQty": float(row.buy_qty) if row.buy_qty else 0,
  2459. "buyAmount": float(row.buy_amount) if row.buy_amount else 0,
  2460. "totalBuyQty": float(row.total_buy_qty) if row.total_buy_qty else 0,
  2461. "totalUnsubscribeQty": float(row.total_unsubscribe_qty) if row.total_unsubscribe_qty else 0,
  2462. "currentQty": float(row.current_qty) if row.current_qty else 0,
  2463. "totalUnsubscribeAmount": float(row.total_unsubscribe_amount) if row.total_unsubscribe_amount else 0,
  2464. }
  2465. def _normalize_txn_product_item(p: Dict[str, Any]) -> Dict[str, Any]:
  2466. """流水 products_json 项统一为 camelCase(入库时为 snake_case)。"""
  2467. qty = p.get("qty")
  2468. if qty is None:
  2469. qty = p.get("buyQty") or p.get("buy_qty")
  2470. unit_price = p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0
  2471. qty_val = float(qty or 0)
  2472. amount = p.get("amount")
  2473. if amount is None and qty_val:
  2474. amount = float(unit_price or 0) * qty_val
  2475. return {
  2476. "projectCode": p.get("projectCode") or p.get("project_code") or "",
  2477. "projectName": p.get("projectName") or p.get("project_name") or "",
  2478. "cellType": p.get("cellType") or p.get("cell_type") or "",
  2479. "specText": p.get("specText") or p.get("spec_text") or "",
  2480. "agreementUnitPrice": float(unit_price or 0),
  2481. "qty": qty_val,
  2482. "amount": float(amount or 0),
  2483. }
  2484. def _enrich_txn_products_from_order(
  2485. db,
  2486. service_order_id: str,
  2487. products: List[Dict[str, Any]],
  2488. tenant_no=None,
  2489. ) -> List[Dict[str, Any]]:
  2490. """退订等流水可能仅存 projectCode,从订单持仓补全项目名与规格。"""
  2491. if not db or not service_order_id or not products:
  2492. return products
  2493. holdings = get_service_order_products(db, service_order_id, tenant_no)
  2494. if not holdings:
  2495. return products
  2496. by_code_spec = {
  2497. (h.get("projectCode") or "", h.get("specText") or ""): h for h in holdings
  2498. }
  2499. by_code: Dict[str, List[Dict[str, Any]]] = {}
  2500. for h in holdings:
  2501. code = h.get("projectCode") or ""
  2502. by_code.setdefault(code, []).append(h)
  2503. enriched: List[Dict[str, Any]] = []
  2504. for p in products:
  2505. item = dict(p)
  2506. code = item.get("projectCode") or ""
  2507. spec = item.get("specText") or ""
  2508. holding = by_code_spec.get((code, spec))
  2509. if not holding and code:
  2510. matches = by_code.get(code, [])
  2511. if len(matches) == 1:
  2512. holding = matches[0]
  2513. if holding:
  2514. if not (item.get("projectName") or "").strip():
  2515. item["projectName"] = holding.get("projectName") or ""
  2516. if not (item.get("specText") or "").strip():
  2517. item["specText"] = holding.get("specText") or ""
  2518. if not (item.get("cellType") or "").strip():
  2519. item["cellType"] = holding.get("cellType") or ""
  2520. enriched.append(item)
  2521. return enriched
  2522. def _row_to_dict_service_order_txn(row, db=None, tenant_no=None) -> Dict[str, Any]:
  2523. tn = tenant_no or row.tenant_no
  2524. d = {
  2525. "id": row.id,
  2526. "serviceOrderId": row.service_order_id or "",
  2527. "txnType": row.txn_type or "",
  2528. "amount": float(row.amount) if row.amount else 0,
  2529. "txnStatus": row.txn_status or "",
  2530. "productsJson": row.products_json or "",
  2531. "remark": row.remark or "",
  2532. "refundAttachments": row.refund_attachments or "",
  2533. "refundAttachmentFiles": urls_to_attachment_list(row.refund_attachments, db, tn) if db and row.refund_attachments else [],
  2534. "financeSubmitBy": row.finance_submit_by or "",
  2535. "financeSubmitTime": row.finance_submit_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_submit_time else "",
  2536. "financeSubmitRemark": row.finance_submit_remark or "",
  2537. "financeSubmitAttachments": row.finance_submit_attachments or "",
  2538. "financeSubmitAttachmentFiles": urls_to_attachment_list(row.finance_submit_attachments, db, tn) if db and row.finance_submit_attachments else [],
  2539. "financeConfirmTime": row.finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_confirm_time else "",
  2540. "financeConfirmBy": row.finance_confirm_by or "",
  2541. "financeConfirmAttachments": row.finance_confirm_attachments or "",
  2542. "financeConfirmAttachmentFiles": urls_to_attachment_list(row.finance_confirm_attachments, db, tn) if db and row.finance_confirm_attachments else [],
  2543. "financeRemark": row.finance_remark or "",
  2544. "voidBy": row.void_by or "",
  2545. "voidTime": row.void_time.strftime("%Y-%m-%d %H:%M:%S") if row.void_time else "",
  2546. "balanceBefore": float(row.balance_before) if row.balance_before else 0,
  2547. "balanceAfter": float(row.balance_after) if row.balance_after else 0,
  2548. "tenantNo": row.tenant_no or "",
  2549. "createBy": row.create_by or "",
  2550. "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
  2551. "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
  2552. }
  2553. if row.products_json:
  2554. try:
  2555. raw_products = json.loads(row.products_json)
  2556. products = [_normalize_txn_product_item(x) for x in raw_products]
  2557. if db and row.service_order_id:
  2558. products = _enrich_txn_products_from_order(
  2559. db, row.service_order_id, products, tenant_no
  2560. )
  2561. d["products"] = products
  2562. except Exception:
  2563. d["products"] = []
  2564. else:
  2565. d["products"] = []
  2566. return d
  2567. def _service_order_txn_to_response_dict(
  2568. db,
  2569. txn,
  2570. tenant_no=None,
  2571. *,
  2572. order=None,
  2573. name_map=None,
  2574. ) -> Dict[str, Any]:
  2575. """流水响应字典:含订单渠道信息与提交人/创建人昵称。"""
  2576. d = _row_to_dict_service_order_txn(txn, db, tenant_no)
  2577. if order is None and txn.service_order_id:
  2578. order = get_service_order(db, txn.service_order_id, tenant_no)
  2579. if order:
  2580. d["channelName"] = order.channel_name or ""
  2581. d["orderName"] = order.order_name or ""
  2582. d["deptName"] = order.dept_name or ""
  2583. d["orderType"] = order.order_type or ""
  2584. confirm_uid = txn.finance_confirm_by or (
  2585. txn.update_by
  2586. if (txn.txn_status or "") == "yqr" and txn.finance_confirm_time and not txn.finance_confirm_by
  2587. else None
  2588. )
  2589. void_uid = txn.void_by or (
  2590. txn.update_by
  2591. if (txn.txn_status or "") == "yzf"
  2592. and txn.finance_submit_time
  2593. and not txn.void_by
  2594. else None
  2595. )
  2596. if name_map is None:
  2597. user_ids = [x for x in (txn.create_by, txn.finance_submit_by, confirm_uid, void_uid) if x]
  2598. name_map = _user_nick_name_map(db, user_ids) if db and user_ids else {}
  2599. d["createByName"] = name_map.get(txn.create_by or "", "") or (txn.create_by or "")
  2600. d["financeSubmitByName"] = name_map.get(txn.finance_submit_by or "", "") or (
  2601. txn.finance_submit_by or ""
  2602. )
  2603. d["financeConfirmByName"] = name_map.get(confirm_uid or "", "") or (confirm_uid or "")
  2604. if not d.get("voidTime") and (txn.txn_status or "") == "yzf" and txn.update_time:
  2605. d["voidTime"] = txn.update_time.strftime("%Y-%m-%d %H:%M:%S")
  2606. d["voidByName"] = name_map.get(void_uid or "", "") or (void_uid or "")
  2607. if not (d.get("financeSubmitRemark") or "").strip() and (d.get("remark") or "").strip():
  2608. d["financeSubmitRemark"] = (d.get("remark") or "").strip()
  2609. _enrich_finance_operate_fields(d, txn)
  2610. return d
  2611. def _enrich_finance_operate_fields(d: Dict[str, Any], txn) -> None:
  2612. """财务页「已处理」展示:操作类型、操作人、操作时间。"""
  2613. status = txn.txn_status or ""
  2614. if status == "yqr" and txn.finance_confirm_time:
  2615. d["financeOperateType"] = "confirm"
  2616. d["financeOperateTypeLabel"] = "财务确认"
  2617. d["financeOperateByName"] = d.get("financeConfirmByName") or d.get("financeConfirmBy") or ""
  2618. d["financeOperateTime"] = d.get("financeConfirmTime") or ""
  2619. elif status == "yzf" and txn.finance_submit_time:
  2620. d["financeOperateType"] = "void"
  2621. d["financeOperateTypeLabel"] = "财务作废"
  2622. d["financeOperateByName"] = d.get("voidByName") or d.get("voidBy") or ""
  2623. d["financeOperateTime"] = d.get("voidTime") or ""
  2624. else:
  2625. d["financeOperateType"] = ""
  2626. d["financeOperateTypeLabel"] = ""
  2627. d["financeOperateByName"] = ""
  2628. d["financeOperateTime"] = ""
  2629. def list_channel_service_orders(
  2630. db,
  2631. *,
  2632. page=1,
  2633. page_size=10,
  2634. order_type=None,
  2635. order_status=None,
  2636. channel_id=None,
  2637. keyword=None,
  2638. create_time_start=None,
  2639. create_time_end=None,
  2640. tenant_no=None,
  2641. user_info=None,
  2642. ):
  2643. def _filtered():
  2644. q0 = db.query(BizChannelServiceOrder).filter(BizChannelServiceOrder.del_flag == 0)
  2645. if tenant_no:
  2646. q0 = q0.filter(BizChannelServiceOrder.tenant_no == tenant_no)
  2647. ot = (order_type or "").strip()
  2648. if ot:
  2649. q0 = q0.filter(BizChannelServiceOrder.order_type == ot)
  2650. # Support comma-separated order_status
  2651. os_raw = (order_status or "").strip()
  2652. if os_raw:
  2653. statuses = [s.strip() for s in os_raw.split(",") if s.strip()]
  2654. if statuses:
  2655. q0 = q0.filter(BizChannelServiceOrder.order_status.in_(statuses))
  2656. if channel_id:
  2657. q0 = q0.filter(BizChannelServiceOrder.channel_id == channel_id)
  2658. kw = (keyword or "").strip()
  2659. if kw:
  2660. like = f"%{kw}%"
  2661. q0 = q0.filter(
  2662. or_(
  2663. BizChannelServiceOrder.channel_name.like(like),
  2664. BizChannelServiceOrder.order_name.like(like),
  2665. BizChannelServiceOrder.dept_name.like(like),
  2666. )
  2667. )
  2668. if create_time_start and create_time_start.strip():
  2669. q0 = q0.filter(
  2670. BizChannelServiceOrder.create_time
  2671. >= _normalize_date_bound(create_time_start.strip())
  2672. )
  2673. if create_time_end and create_time_end.strip():
  2674. q0 = q0.filter(
  2675. BizChannelServiceOrder.create_time
  2676. <= _normalize_date_bound(create_time_end.strip(), end_of_day=True)
  2677. )
  2678. if user_info:
  2679. from app.core.data_scope import apply_data_scope
  2680. q0 = apply_data_scope(q0, BizChannelServiceOrder, user_info, db, tenant_no)
  2681. return q0
  2682. total = _filtered().order_by(None).with_entities(func.count(BizChannelServiceOrder.id)).scalar() or 0
  2683. page = max(1, page)
  2684. page_size = min(max(1, page_size), 100)
  2685. rows = (
  2686. _filtered()
  2687. .order_by(BizChannelServiceOrder.create_time.desc())
  2688. .offset((page - 1) * page_size)
  2689. .limit(page_size)
  2690. .all()
  2691. )
  2692. user_ids: List[str] = []
  2693. for x in rows:
  2694. if x.create_by:
  2695. user_ids.append(x.create_by)
  2696. if x.finance_submit_by:
  2697. user_ids.append(x.finance_submit_by)
  2698. name_map = _user_nick_name_map(db, user_ids) if db else {}
  2699. gmcp_ids = [x.id for x in rows if (x.order_type or "") == "gmcp"]
  2700. purchase_total_map = _batch_gmcp_purchase_totals(db, gmcp_ids, tenant_no) if db and gmcp_ids else {}
  2701. holding_amount_map = (
  2702. _batch_gmcp_current_holding_amounts(db, gmcp_ids, tenant_no) if db and gmcp_ids else {}
  2703. )
  2704. return [
  2705. {
  2706. **_row_to_dict_service_order(
  2707. x,
  2708. db,
  2709. tenant_no,
  2710. purchase_total_override=purchase_total_map.get(x.id),
  2711. current_balance_override=holding_amount_map.get(x.id),
  2712. ),
  2713. "createByName": name_map.get(x.create_by or "", "") or (x.create_by or ""),
  2714. "financeSubmitByName": name_map.get(x.finance_submit_by or "", "") or (x.finance_submit_by or ""),
  2715. }
  2716. for x in rows
  2717. ], int(total)
  2718. def get_service_order(db, order_id, tenant_no=None):
  2719. q = db.query(BizChannelServiceOrder).filter(
  2720. and_(BizChannelServiceOrder.id == order_id, BizChannelServiceOrder.del_flag == 0)
  2721. )
  2722. if tenant_no:
  2723. q = q.filter(BizChannelServiceOrder.tenant_no == tenant_no)
  2724. return q.first()
  2725. def get_service_order_products(db, service_order_id, tenant_no=None):
  2726. q = db.query(BizChannelServiceOrderProduct).filter(
  2727. and_(
  2728. BizChannelServiceOrderProduct.service_order_id == service_order_id,
  2729. BizChannelServiceOrderProduct.del_flag == 0,
  2730. )
  2731. )
  2732. if tenant_no:
  2733. q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
  2734. rows = q.order_by(BizChannelServiceOrderProduct.create_time.asc()).all()
  2735. return [_row_to_dict_service_order_product(x) for x in rows]
  2736. # ────────────────────────────────────────────
  2737. # Service Order: Create / Update / Delete
  2738. # ────────────────────────────────────────────
  2739. def _calc_gmcp_purchase_total(products):
  2740. total = 0.0
  2741. for p in products:
  2742. price = float(p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0)
  2743. qty = float(p.get("buyQty") or p.get("buy_qty") or 0)
  2744. total += price * qty
  2745. return total
  2746. def _gmcp_has_purchase_lock(db, order_id, tenant_no=None) -> bool:
  2747. """是否存在购买记录(未作废 purchase 流水,或历史建单写入的产品行)。"""
  2748. q = db.query(BizChannelServiceOrderTxn).filter(
  2749. and_(
  2750. BizChannelServiceOrderTxn.service_order_id == order_id,
  2751. BizChannelServiceOrderTxn.txn_type == "purchase",
  2752. BizChannelServiceOrderTxn.txn_status != "yzf",
  2753. BizChannelServiceOrderTxn.del_flag == 0,
  2754. )
  2755. )
  2756. if tenant_no:
  2757. q = q.filter(BizChannelServiceOrderTxn.tenant_no == tenant_no)
  2758. if q.first():
  2759. return True
  2760. pq = db.query(BizChannelServiceOrderProduct).filter(
  2761. and_(
  2762. BizChannelServiceOrderProduct.service_order_id == order_id,
  2763. BizChannelServiceOrderProduct.del_flag == 0,
  2764. )
  2765. )
  2766. if tenant_no:
  2767. pq = pq.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
  2768. for row in pq.all():
  2769. if float(row.buy_qty or 0) > 0 or float(row.total_buy_qty or 0) > 0:
  2770. return True
  2771. return False
  2772. def gmcp_agreement_editable(db, order, tenant_no=None) -> bool:
  2773. if order.order_type != "gmcp" or order.order_status == "yzf":
  2774. return False
  2775. return not _gmcp_has_purchase_lock(db, order.id, tenant_no or order.tenant_no)
  2776. def _resolve_agreement_price_cell_type(db, channel_agreement_id, project_code, spec_text, cell_type_fallback=""):
  2777. price_row = (
  2778. db.query(BizChannelAgreementProductPrice)
  2779. .filter(
  2780. and_(
  2781. BizChannelAgreementProductPrice.channel_agreement_id == channel_agreement_id,
  2782. BizChannelAgreementProductPrice.project_code == (project_code or ""),
  2783. BizChannelAgreementProductPrice.spec_text == (spec_text or ""),
  2784. BizChannelAgreementProductPrice.del_flag == 0,
  2785. )
  2786. )
  2787. ).first()
  2788. if price_row:
  2789. return price_row.cell_type_code or "", price_row.cell_type_name or cell_type_fallback
  2790. return "", cell_type_fallback
  2791. def _save_gmcp_product_rows(
  2792. db,
  2793. order_id,
  2794. products,
  2795. tenant_no,
  2796. user_id,
  2797. now=None,
  2798. ):
  2799. """仅保存购买订单产品明细,不调整份数库存"""
  2800. now = now or datetime.now()
  2801. for p in products:
  2802. project_code = p.get("projectCode") or p.get("project_code") or ""
  2803. project_name = p.get("projectName") or p.get("project_name") or ""
  2804. cell_type = p.get("cellType") or p.get("cell_type") or ""
  2805. spec_text = p.get("specText") or p.get("spec_text") or ""
  2806. agreement_unit_price = float(p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0)
  2807. buy_qty = float(p.get("buyQty") or p.get("buy_qty") or 0)
  2808. buy_amount = float(p.get("buyAmount") or p.get("buy_amount") or agreement_unit_price * buy_qty)
  2809. prod_row = BizChannelServiceOrderProduct(
  2810. id=generate_uuid(),
  2811. service_order_id=order_id,
  2812. project_code=project_code,
  2813. project_name=project_name,
  2814. cell_type=cell_type,
  2815. spec_text=spec_text,
  2816. agreement_unit_price=agreement_unit_price,
  2817. buy_qty=buy_qty,
  2818. buy_amount=buy_amount,
  2819. tenant_no=tenant_no or "zkxw",
  2820. order_num=0,
  2821. remark="",
  2822. create_by=user_id,
  2823. create_dept_id="",
  2824. create_time=now,
  2825. update_by=user_id,
  2826. update_time=now,
  2827. del_flag=0,
  2828. )
  2829. db.add(prod_row)
  2830. db.flush()
  2831. def _replace_gmcp_product_rows(db, order_id, products, tenant_no, user_id):
  2832. now = datetime.now()
  2833. db.query(BizChannelServiceOrderProduct).filter(
  2834. and_(
  2835. BizChannelServiceOrderProduct.service_order_id == order_id,
  2836. BizChannelServiceOrderProduct.del_flag == 0,
  2837. )
  2838. ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
  2839. _save_gmcp_product_rows(db, order_id, products, tenant_no, user_id, now=now)
  2840. def _apply_gmcp_order_quotas(db, order, tenant_no, user_id):
  2841. """财务确认收款后,按订单明细增加渠道产品份数"""
  2842. products = get_service_order_products(db, order.id, tenant_no)
  2843. for prod in products:
  2844. project_code = prod.get("projectCode") or ""
  2845. spec_text = prod.get("specText") or ""
  2846. cell_type_code, cell_type_name = _resolve_agreement_price_cell_type(
  2847. db,
  2848. order.channel_agreement_id,
  2849. project_code,
  2850. spec_text,
  2851. prod.get("cellType") or "",
  2852. )
  2853. _upsert_channel_product_quota(
  2854. db,
  2855. channel_id=order.channel_id,
  2856. project_code=project_code,
  2857. project_name=prod.get("projectName") or "",
  2858. cell_type_code=cell_type_code,
  2859. cell_type_name=cell_type_name,
  2860. spec_text=spec_text,
  2861. add_qty=float(prod.get("buyQty") or 0),
  2862. tenant_no=tenant_no or order.tenant_no,
  2863. user_id=user_id,
  2864. )
  2865. def _get_valid_channel_agreement(db, channel_agreement_id, channel_id, tenant_no=None):
  2866. """校验渠道协议存在、属于当前渠道且已生效"""
  2867. aid = (channel_agreement_id or "").strip()
  2868. if not aid:
  2869. raise ValueError("请选择渠道协议")
  2870. q = db.query(BizChannelAgreement).filter(
  2871. and_(
  2872. BizChannelAgreement.id == aid,
  2873. BizChannelAgreement.channel_id == channel_id,
  2874. BizChannelAgreement.del_flag == 0,
  2875. )
  2876. )
  2877. if tenant_no:
  2878. q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
  2879. agreement = q.first()
  2880. if not agreement:
  2881. raise ValueError("渠道协议不存在或不属于当前渠道")
  2882. if (agreement.agreement_status or "").strip() != "sxz":
  2883. raise ValueError("协议未生效,无法使用")
  2884. return agreement
  2885. def create_channel_service_order(
  2886. db,
  2887. *,
  2888. channel_id,
  2889. order_type,
  2890. channel_name=None,
  2891. order_name=None,
  2892. dept_name=None,
  2893. prepay_amount=None,
  2894. channel_agreement_id=None,
  2895. products=None,
  2896. tenant_no=None,
  2897. remark=None,
  2898. user_id=None,
  2899. ):
  2900. # Validate channel exists
  2901. channel = get_channel(db, channel_id, tenant_no)
  2902. if not channel:
  2903. raise ValueError("渠道不存在")
  2904. now = datetime.now()
  2905. balance_before = None
  2906. balance_after = None
  2907. purchase_total_amount = None
  2908. order_status = "dsx"
  2909. if order_type == "yfk":
  2910. # 预存款订单:创建后为待生效,充值金额通过「发起充值」流水录入
  2911. _get_valid_channel_agreement(db, channel_agreement_id, channel_id, tenant_no)
  2912. order_status = "dsx"
  2913. prepay_amount = None
  2914. elif order_type == "gmcp":
  2915. # 购买型:建单仅绑定渠道与协议;产品通过 purchase 流水录入并经财务确认
  2916. if not channel_agreement_id:
  2917. raise ValueError("请选择渠道协议")
  2918. _get_valid_channel_agreement(db, channel_agreement_id, channel_id, tenant_no)
  2919. purchase_total_amount = _calc_gmcp_purchase_total(products) if products else None
  2920. order_status = "dsx"
  2921. else:
  2922. raise ValueError(f"不支持的订单类型: {order_type}")
  2923. order_id = generate_uuid()
  2924. order = BizChannelServiceOrder(
  2925. id=order_id,
  2926. channel_id=channel_id,
  2927. channel_name=channel_name or channel.channel_name or "",
  2928. order_name=(order_name or "").strip() or None,
  2929. dept_name=dept_name or "",
  2930. order_type=order_type,
  2931. prepay_amount=None,
  2932. purchase_total_amount=purchase_total_amount,
  2933. order_status=order_status,
  2934. channel_agreement_id=channel_agreement_id,
  2935. balance_before=balance_before,
  2936. balance_after=balance_after,
  2937. tenant_no=tenant_no or "zkxw",
  2938. order_num=0,
  2939. remark=remark or "",
  2940. create_by=user_id,
  2941. create_dept_id="",
  2942. create_time=now,
  2943. update_by=user_id,
  2944. update_time=now,
  2945. del_flag=0,
  2946. )
  2947. db.add(order)
  2948. db.flush()
  2949. db.commit()
  2950. db.refresh(order)
  2951. return order
  2952. def update_channel_service_order(db, order_id, *, tenant_no=None, user_id=None, products=None, **kwargs):
  2953. """更新订单基本信息;gmcp 产品仅通过 purchase 流水维护"""
  2954. order = get_service_order(db, order_id, tenant_no)
  2955. if not order:
  2956. raise ValueError("订单不存在")
  2957. if order.order_status == "yzf":
  2958. raise ValueError("已作废订单不可编辑")
  2959. if kwargs.get("channel_id"):
  2960. raise ValueError("渠道不可修改")
  2961. if products is not None and order.order_type == "gmcp":
  2962. raise ValueError("请通过「购买产品」操作录入产品明细")
  2963. if order.order_type in ("yfk", "gmcp"):
  2964. if order.order_status not in ("dsx", "sxz"):
  2965. raise ValueError("当前状态不允许编辑")
  2966. else:
  2967. raise ValueError("不支持的订单类型")
  2968. if "dept_name" in kwargs and kwargs["dept_name"] is not None:
  2969. order.dept_name = kwargs["dept_name"]
  2970. if "order_name" in kwargs and kwargs["order_name"] is not None:
  2971. order.order_name = (kwargs["order_name"] or "").strip() or None
  2972. if "remark" in kwargs and kwargs["remark"] is not None:
  2973. order.remark = kwargs["remark"]
  2974. if order.order_type == "yfk":
  2975. if "channel_agreement_id" in kwargs and kwargs["channel_agreement_id"] is not None:
  2976. new_agreement_id = kwargs["channel_agreement_id"]
  2977. if new_agreement_id != (order.channel_agreement_id or ""):
  2978. if order.order_status != "dsx":
  2979. raise ValueError("已有业务流水,不可修改渠道协议")
  2980. _get_valid_channel_agreement(
  2981. db, new_agreement_id, order.channel_id, tenant_no
  2982. )
  2983. order.channel_agreement_id = new_agreement_id
  2984. elif order.order_type == "gmcp":
  2985. if "channel_agreement_id" in kwargs and kwargs["channel_agreement_id"] is not None:
  2986. new_agreement_id = kwargs["channel_agreement_id"]
  2987. if new_agreement_id != (order.channel_agreement_id or ""):
  2988. if _gmcp_has_purchase_lock(db, order_id, tenant_no):
  2989. raise ValueError("已有购买记录,不可修改渠道协议")
  2990. _get_valid_channel_agreement(
  2991. db, new_agreement_id, order.channel_id, tenant_no
  2992. )
  2993. order.channel_agreement_id = new_agreement_id
  2994. order.update_by = user_id
  2995. order.update_time = datetime.now()
  2996. db.add(order)
  2997. db.commit()
  2998. db.refresh(order)
  2999. return order
  3000. def soft_delete_service_order(db, order_id, tenant_no=None):
  3001. """仅允许待生效状态下删除订单"""
  3002. order = get_service_order(db, order_id, tenant_no)
  3003. if not order:
  3004. raise ValueError("订单不存在")
  3005. if order.order_status != "dsx":
  3006. raise ValueError("仅待生效订单可删除")
  3007. order.del_flag = 1
  3008. order.update_time = datetime.now()
  3009. db.add(order)
  3010. db.commit()
  3011. # ────────────────────────────────────────────
  3012. # Service Order: Status Transitions
  3013. # ────────────────────────────────────────────
  3014. def submit_finance_confirm(
  3015. db,
  3016. order_id,
  3017. *,
  3018. finance_submit_remark=None,
  3019. finance_submit_attachments=None,
  3020. tenant_no=None,
  3021. user_id=None,
  3022. ):
  3023. """发起财务确认:type=yfk/gmcp + status=dsx → dqr"""
  3024. order = get_service_order(db, order_id, tenant_no)
  3025. if not order:
  3026. raise ValueError("订单不存在")
  3027. if order.order_type not in ("yfk", "gmcp"):
  3028. raise ValueError("仅预存款或购买产品订单可发起财务确认")
  3029. if order.order_status != "dsx":
  3030. raise ValueError("仅待生效状态可发起财务确认")
  3031. now = datetime.now()
  3032. order.order_status = "dqr"
  3033. order.finance_submit_remark = finance_submit_remark or ""
  3034. order.finance_submit_attachments = finance_submit_attachments or ""
  3035. order.finance_submit_time = now
  3036. order.finance_submit_by = user_id
  3037. order.update_by = user_id
  3038. order.update_time = now
  3039. db.add(order)
  3040. db.commit()
  3041. db.refresh(order)
  3042. return order
  3043. def confirm_receipt(db, order_id, *, finance_remark=None, finance_confirm_attachments=None, tenant_no=None, user_id=None):
  3044. """确认到账:yfk dqr→yqr 增加余额;gmcp dqr→sxz 增加产品份数"""
  3045. order = get_service_order(db, order_id, tenant_no)
  3046. if not order:
  3047. raise ValueError("订单不存在")
  3048. if order.order_type not in ("yfk", "gmcp"):
  3049. raise ValueError("仅预存款或购买产品订单可确认到账")
  3050. if order.order_status != "dqr":
  3051. raise ValueError("仅待确认状态可确认到账")
  3052. now = datetime.now()
  3053. if order.order_type == "yfk":
  3054. account = _get_or_create_channel_account(db, order.channel_id, tenant_no or order.tenant_no, user_id)
  3055. balance_before = float(account.balance_amount) if account.balance_amount else 0
  3056. add_amount = float(order.prepay_amount) if order.prepay_amount else 0
  3057. account.balance_amount = balance_before + add_amount
  3058. balance_after = float(account.balance_amount)
  3059. account.update_by = user_id
  3060. account.update_time = now
  3061. db.add(account)
  3062. db.flush()
  3063. order.order_status = "yqr"
  3064. order.balance_before = balance_before
  3065. order.balance_after = balance_after
  3066. else:
  3067. _apply_gmcp_order_quotas(db, order, tenant_no, user_id)
  3068. order.order_status = "sxz"
  3069. order.finance_confirm_time = now
  3070. order.finance_confirm_attachments = finance_confirm_attachments or ""
  3071. order.finance_remark = finance_remark or ""
  3072. order.update_by = user_id
  3073. order.update_time = now
  3074. db.add(order)
  3075. db.commit()
  3076. db.refresh(order)
  3077. return order
  3078. def submit_refund(db, order_id, *, refund_amount=None, refund_remark=None, refund_attachments=None, tenant_no=None, user_id=None):
  3079. """申请退款:type=yfk + status=yqr → tfqrz"""
  3080. order = get_service_order(db, order_id, tenant_no)
  3081. if not order:
  3082. raise ValueError("订单不存在")
  3083. if order.order_type != "yfk":
  3084. raise ValueError("仅预存款订单可申请退款")
  3085. if order.order_status != "yqr":
  3086. raise ValueError("仅已确认状态可申请退款")
  3087. now = datetime.now()
  3088. # Default refund_amount to prepay_amount
  3089. if refund_amount is None:
  3090. refund_amount = float(order.prepay_amount) if order.prepay_amount else 0
  3091. else:
  3092. refund_amount = float(refund_amount)
  3093. prepay = float(order.prepay_amount) if order.prepay_amount else 0
  3094. if refund_amount > prepay:
  3095. raise ValueError(f"退款金额不能大于预存金额 {prepay}")
  3096. order.order_status = "tfqrz"
  3097. order.refund_apply_by = user_id
  3098. order.refund_apply_time = now
  3099. order.refund_amount = refund_amount
  3100. order.refund_remark = refund_remark or ""
  3101. order.refund_attachments = refund_attachments or ""
  3102. order.update_by = user_id
  3103. order.update_time = now
  3104. db.add(order)
  3105. db.commit()
  3106. db.refresh(order)
  3107. return order
  3108. def confirm_refund(db, order_id, *, refund_finance_remark=None, refund_finance_confirm_attachments=None, tenant_no=None, user_id=None):
  3109. """确认退款:status=tfqrz → ytf,扣减余额"""
  3110. order = get_service_order(db, order_id, tenant_no)
  3111. if not order:
  3112. raise ValueError("订单不存在")
  3113. if order.order_status != "tfqrz":
  3114. raise ValueError("仅退款待确认状态可确认退款")
  3115. now = datetime.now()
  3116. account = _get_or_create_channel_account(db, order.channel_id, tenant_no or order.tenant_no, user_id)
  3117. balance_before = float(account.balance_amount) if account.balance_amount else 0
  3118. deduct = float(order.refund_amount) if order.refund_amount else 0
  3119. account.balance_amount = balance_before - deduct
  3120. balance_after = float(account.balance_amount)
  3121. account.update_by = user_id
  3122. account.update_time = now
  3123. db.add(account)
  3124. db.flush()
  3125. order.order_status = "ytf"
  3126. order.refund_finance_confirm_time = now
  3127. order.refund_finance_confirm_attachments = refund_finance_confirm_attachments or ""
  3128. order.refund_finance_remark = refund_finance_remark or ""
  3129. order.balance_before = balance_before
  3130. order.balance_after = balance_after
  3131. order.update_by = user_id
  3132. order.update_time = now
  3133. db.add(order)
  3134. db.commit()
  3135. db.refresh(order)
  3136. return order
  3137. def void_service_order(db, order_id, *, tenant_no=None, user_id=None):
  3138. """作废订单:type=gmcp + status=sxz → yzf,扣减已生效的产品份数"""
  3139. order = get_service_order(db, order_id, tenant_no)
  3140. if not order:
  3141. raise ValueError("订单不存在")
  3142. if order.order_type != "gmcp":
  3143. raise ValueError("仅购买产品订单可作废")
  3144. if order.order_status != "sxz":
  3145. raise ValueError("仅生效中状态可作废")
  3146. now = datetime.now()
  3147. # Reduce product quotas
  3148. products = get_service_order_products(db, order_id, tenant_no)
  3149. for prod in products:
  3150. cell_type_code, _ = _resolve_agreement_price_cell_type(
  3151. db,
  3152. order.channel_agreement_id,
  3153. prod.get("projectCode") or "",
  3154. prod.get("specText") or "",
  3155. prod.get("cellType") or "",
  3156. )
  3157. _reduce_channel_product_quota(
  3158. db,
  3159. channel_id=order.channel_id,
  3160. project_code=prod.get("projectCode") or "",
  3161. cell_type_code=cell_type_code,
  3162. spec_text=prod.get("specText") or "",
  3163. reduce_qty=prod.get("buyQty") or 0,
  3164. user_id=user_id,
  3165. )
  3166. order.order_status = "yzf"
  3167. order.update_by = user_id
  3168. order.update_time = now
  3169. db.add(order)
  3170. db.commit()
  3171. db.refresh(order)
  3172. return order
  3173. # ────────────────────────────────────────────
  3174. # Service Order Txn: 操作流水 CRUD
  3175. # ────────────────────────────────────────────
  3176. def list_service_order_txns(
  3177. db,
  3178. *,
  3179. service_order_id=None,
  3180. channel_id=None,
  3181. txn_status=None,
  3182. txn_type=None,
  3183. keyword=None,
  3184. create_time_start=None,
  3185. create_time_end=None,
  3186. finance_done_only=False,
  3187. page=1,
  3188. page_size=20,
  3189. tenant_no=None,
  3190. ):
  3191. """查询操作流水列表。finance_done_only:财务已处理(已确认或财务侧作废)。"""
  3192. q = db.query(BizChannelServiceOrderTxn).filter(BizChannelServiceOrderTxn.del_flag == 0)
  3193. if tenant_no:
  3194. q = q.filter(BizChannelServiceOrderTxn.tenant_no == tenant_no)
  3195. if service_order_id:
  3196. q = q.filter(BizChannelServiceOrderTxn.service_order_id == service_order_id)
  3197. cid = (channel_id or "").strip()
  3198. kw = (keyword or "").strip()
  3199. if cid:
  3200. q = q.join(
  3201. BizChannelServiceOrder,
  3202. and_(
  3203. BizChannelServiceOrder.id == BizChannelServiceOrderTxn.service_order_id,
  3204. BizChannelServiceOrder.del_flag == 0,
  3205. ),
  3206. ).filter(BizChannelServiceOrder.channel_id == cid)
  3207. if kw:
  3208. like = f"%{kw}%"
  3209. q = q.filter(
  3210. or_(
  3211. BizChannelServiceOrder.channel_name.like(like),
  3212. BizChannelServiceOrder.order_name.like(like),
  3213. BizChannelServiceOrder.dept_name.like(like),
  3214. )
  3215. )
  3216. elif kw:
  3217. like = f"%{kw}%"
  3218. q = q.outerjoin(
  3219. BizChannelServiceOrder,
  3220. and_(
  3221. BizChannelServiceOrder.id == BizChannelServiceOrderTxn.service_order_id,
  3222. BizChannelServiceOrder.del_flag == 0,
  3223. ),
  3224. ).filter(
  3225. or_(
  3226. BizChannelServiceOrder.channel_name.like(like),
  3227. BizChannelServiceOrder.order_name.like(like),
  3228. BizChannelServiceOrder.dept_name.like(like),
  3229. )
  3230. )
  3231. if finance_done_only:
  3232. q = q.filter(
  3233. or_(
  3234. BizChannelServiceOrderTxn.txn_status == "yqr",
  3235. and_(
  3236. BizChannelServiceOrderTxn.txn_status == "yzf",
  3237. BizChannelServiceOrderTxn.finance_submit_time.isnot(None),
  3238. ),
  3239. )
  3240. )
  3241. else:
  3242. ss = (txn_status or "").strip()
  3243. if ss:
  3244. statuses = [s.strip() for s in ss.split(",") if s.strip()]
  3245. if statuses:
  3246. q = q.filter(BizChannelServiceOrderTxn.txn_status.in_(statuses))
  3247. tt = (txn_type or "").strip()
  3248. if tt:
  3249. types = [t.strip() for t in tt.split(",") if t.strip()]
  3250. if types:
  3251. q = q.filter(BizChannelServiceOrderTxn.txn_type.in_(types))
  3252. if create_time_start and create_time_start.strip():
  3253. q = q.filter(
  3254. BizChannelServiceOrderTxn.create_time
  3255. >= _normalize_date_bound(create_time_start.strip())
  3256. )
  3257. if create_time_end and create_time_end.strip():
  3258. q = q.filter(
  3259. BizChannelServiceOrderTxn.create_time
  3260. <= _normalize_date_bound(create_time_end.strip(), end_of_day=True)
  3261. )
  3262. total = q.order_by(None).with_entities(func.count(BizChannelServiceOrderTxn.id)).scalar() or 0
  3263. page = max(1, page)
  3264. page_size = min(max(1, page_size), 100)
  3265. if finance_done_only:
  3266. operate_time = func.coalesce(
  3267. BizChannelServiceOrderTxn.finance_confirm_time,
  3268. BizChannelServiceOrderTxn.void_time,
  3269. )
  3270. rows = (
  3271. q.order_by(operate_time.desc(), BizChannelServiceOrderTxn.create_time.desc())
  3272. .offset((page - 1) * page_size)
  3273. .limit(page_size)
  3274. .all()
  3275. )
  3276. else:
  3277. rows = (
  3278. q.order_by(BizChannelServiceOrderTxn.create_time.desc())
  3279. .offset((page - 1) * page_size)
  3280. .limit(page_size)
  3281. .all()
  3282. )
  3283. if not rows:
  3284. return [], int(total)
  3285. order_ids = list({r.service_order_id for r in rows if r.service_order_id})
  3286. orders: Dict[str, Any] = {}
  3287. if order_ids:
  3288. oq = db.query(BizChannelServiceOrder).filter(
  3289. BizChannelServiceOrder.id.in_(order_ids),
  3290. BizChannelServiceOrder.del_flag == 0,
  3291. )
  3292. if tenant_no:
  3293. oq = oq.filter(BizChannelServiceOrder.tenant_no == tenant_no)
  3294. for o in oq.all():
  3295. orders[o.id] = o
  3296. user_ids: List[str] = []
  3297. for r in rows:
  3298. if r.create_by:
  3299. user_ids.append(r.create_by)
  3300. if r.finance_submit_by:
  3301. user_ids.append(r.finance_submit_by)
  3302. if r.finance_confirm_by:
  3303. user_ids.append(r.finance_confirm_by)
  3304. elif (r.txn_status or "") == "yqr" and r.update_by:
  3305. user_ids.append(r.update_by)
  3306. if r.void_by:
  3307. user_ids.append(r.void_by)
  3308. elif (r.txn_status or "") == "yzf" and r.finance_submit_time and r.update_by:
  3309. user_ids.append(r.update_by)
  3310. name_map = _user_nick_name_map(db, user_ids) if db and user_ids else {}
  3311. items = [
  3312. _service_order_txn_to_response_dict(
  3313. db,
  3314. r,
  3315. tenant_no,
  3316. order=orders.get(r.service_order_id),
  3317. name_map=name_map,
  3318. )
  3319. for r in rows
  3320. ]
  3321. return items, int(total)
  3322. def get_service_order_txn(db, txn_id, *, tenant_no=None):
  3323. """获取单条流水"""
  3324. q = db.query(BizChannelServiceOrderTxn).filter(
  3325. and_(BizChannelServiceOrderTxn.id == txn_id, BizChannelServiceOrderTxn.del_flag == 0)
  3326. )
  3327. if tenant_no:
  3328. q = q.filter(BizChannelServiceOrderTxn.tenant_no == tenant_no)
  3329. return q.first()
  3330. def create_recharge_txn(
  3331. db,
  3332. order_id,
  3333. *,
  3334. amount,
  3335. remark=None,
  3336. finance_submit_attachments=None,
  3337. tenant_no=None,
  3338. user_id=None,
  3339. ):
  3340. """创建充值流水(yfk订单),状态 dsx;附件预存于 finance_submit_attachments 供后续财务确认"""
  3341. order = get_service_order(db, order_id, tenant_no)
  3342. if not order:
  3343. raise ValueError("订单不存在")
  3344. if order.order_type != "yfk":
  3345. raise ValueError("仅充值型订单可发起充值")
  3346. if order.order_status not in ("dsx", "sxz"):
  3347. raise ValueError("订单当前状态不允许充值")
  3348. amount = float(amount)
  3349. if amount <= 0:
  3350. raise ValueError("充值金额必须大于0")
  3351. now = datetime.now()
  3352. txn = BizChannelServiceOrderTxn(
  3353. id=generate_uuid(),
  3354. service_order_id=order_id,
  3355. txn_type="recharge",
  3356. amount=amount,
  3357. txn_status="dsx",
  3358. remark=remark or "",
  3359. finance_submit_attachments=(finance_submit_attachments or "").strip() or None,
  3360. tenant_no=tenant_no or order.tenant_no,
  3361. order_num=0,
  3362. create_by=user_id,
  3363. create_dept_id="",
  3364. create_time=now,
  3365. update_by=user_id,
  3366. update_time=now,
  3367. del_flag=0,
  3368. )
  3369. db.add(txn)
  3370. db.commit()
  3371. db.refresh(txn)
  3372. return txn
  3373. def create_refund_txn(db, order_id, *, amount, remark=None, attachments=None, tenant_no=None, user_id=None):
  3374. """创建退款流水(yfk订单),amount <= current_balance"""
  3375. order = get_service_order(db, order_id, tenant_no)
  3376. if not order:
  3377. raise ValueError("订单不存在")
  3378. if order.order_type != "yfk":
  3379. raise ValueError("仅充值型订单可申请退款")
  3380. if order.order_status != "sxz":
  3381. raise ValueError("仅生效中订单可申请退款")
  3382. amount = float(amount)
  3383. if amount <= 0:
  3384. raise ValueError("退款金额必须大于0")
  3385. current_balance = float(order.current_balance) if order.current_balance else 0
  3386. if amount > current_balance:
  3387. raise ValueError(f"退款金额不能大于当前余额 {current_balance}")
  3388. now = datetime.now()
  3389. txn = BizChannelServiceOrderTxn(
  3390. id=generate_uuid(),
  3391. service_order_id=order_id,
  3392. txn_type="refund",
  3393. amount=amount,
  3394. txn_status="dsx",
  3395. remark=remark or "",
  3396. refund_attachments=attachments or "",
  3397. tenant_no=tenant_no or order.tenant_no,
  3398. order_num=0,
  3399. create_by=user_id,
  3400. create_dept_id="",
  3401. create_time=now,
  3402. update_by=user_id,
  3403. update_time=now,
  3404. del_flag=0,
  3405. )
  3406. db.add(txn)
  3407. db.commit()
  3408. db.refresh(txn)
  3409. return txn
  3410. def create_purchase_txn(db, order_id, *, products, remark=None, tenant_no=None, user_id=None):
  3411. """创建购买产品流水(gmcp订单),同时创建/更新产品持仓"""
  3412. order = get_service_order(db, order_id, tenant_no)
  3413. if not order:
  3414. raise ValueError("订单不存在")
  3415. if order.order_type != "gmcp":
  3416. raise ValueError("仅购买型订单可购买产品")
  3417. if order.order_status not in ("dsx", "sxz"):
  3418. raise ValueError("订单当前状态不允许购买")
  3419. if not products:
  3420. raise ValueError("产品明细不能为空")
  3421. now = datetime.now()
  3422. tn = tenant_no or order.tenant_no
  3423. total_amount = 0.0
  3424. # Upsert product holding rows
  3425. for p in products:
  3426. project_code = p.get("projectCode") or p.get("project_code") or ""
  3427. project_name = p.get("projectName") or p.get("project_name") or ""
  3428. cell_type = p.get("cellType") or p.get("cell_type") or ""
  3429. spec_text = p.get("specText") or p.get("spec_text") or ""
  3430. agreement_unit_price = float(p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0)
  3431. qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
  3432. amt = float(p.get("amount") or agreement_unit_price * qty)
  3433. total_amount += amt
  3434. # Find or create product holding row
  3435. existing = db.query(BizChannelServiceOrderProduct).filter(
  3436. and_(
  3437. BizChannelServiceOrderProduct.service_order_id == order_id,
  3438. BizChannelServiceOrderProduct.project_code == project_code,
  3439. BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
  3440. BizChannelServiceOrderProduct.del_flag == 0,
  3441. )
  3442. ).first()
  3443. if existing:
  3444. existing.total_buy_qty = float(existing.total_buy_qty or 0) + qty
  3445. existing.current_qty = float(existing.current_qty or 0) + qty
  3446. existing.update_by = user_id
  3447. existing.update_time = now
  3448. db.add(existing)
  3449. else:
  3450. prod_row = BizChannelServiceOrderProduct(
  3451. id=generate_uuid(),
  3452. service_order_id=order_id,
  3453. project_code=project_code,
  3454. project_name=project_name,
  3455. cell_type=cell_type,
  3456. spec_text=spec_text,
  3457. agreement_unit_price=agreement_unit_price,
  3458. buy_qty=qty,
  3459. buy_amount=amt,
  3460. total_buy_qty=qty,
  3461. total_unsubscribe_qty=0,
  3462. current_qty=qty,
  3463. total_unsubscribe_amount=0,
  3464. tenant_no=tn,
  3465. order_num=0,
  3466. remark="",
  3467. create_by=user_id,
  3468. create_dept_id="",
  3469. create_time=now,
  3470. update_by=user_id,
  3471. update_time=now,
  3472. del_flag=0,
  3473. )
  3474. db.add(prod_row)
  3475. db.flush()
  3476. products_json = json.dumps(products, ensure_ascii=False)
  3477. txn = BizChannelServiceOrderTxn(
  3478. id=generate_uuid(),
  3479. service_order_id=order_id,
  3480. txn_type="purchase",
  3481. amount=total_amount,
  3482. txn_status="dsx",
  3483. products_json=products_json,
  3484. remark=remark or "",
  3485. tenant_no=tn,
  3486. order_num=0,
  3487. create_by=user_id,
  3488. create_dept_id="",
  3489. create_time=now,
  3490. update_by=user_id,
  3491. update_time=now,
  3492. del_flag=0,
  3493. )
  3494. order.purchase_total_amount = float(order.purchase_total_amount or 0) + total_amount
  3495. order.update_by = user_id
  3496. order.update_time = now
  3497. db.add(order)
  3498. db.add(txn)
  3499. db.commit()
  3500. db.refresh(txn)
  3501. return txn
  3502. def create_unsubscribe_txn(db, order_id, *, products, remark=None, tenant_no=None, user_id=None):
  3503. """创建退订产品流水(gmcp订单),校验退订份数 <= current_qty"""
  3504. order = get_service_order(db, order_id, tenant_no)
  3505. if not order:
  3506. raise ValueError("订单不存在")
  3507. if order.order_type != "gmcp":
  3508. raise ValueError("仅购买型订单可退订产品")
  3509. if order.order_status != "sxz":
  3510. raise ValueError("仅生效中订单可退订")
  3511. if not products:
  3512. raise ValueError("退订产品明细不能为空")
  3513. now = datetime.now()
  3514. tn = tenant_no or order.tenant_no
  3515. total_amount = 0.0
  3516. for p in products:
  3517. project_code = p.get("projectCode") or p.get("project_code") or ""
  3518. spec_text = p.get("specText") or p.get("spec_text") or ""
  3519. qty = float(p.get("qty") or 0)
  3520. if qty <= 0:
  3521. raise ValueError("退订数量必须大于0")
  3522. # Find existing product holding
  3523. existing = db.query(BizChannelServiceOrderProduct).filter(
  3524. and_(
  3525. BizChannelServiceOrderProduct.service_order_id == order_id,
  3526. BizChannelServiceOrderProduct.project_code == project_code,
  3527. BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
  3528. BizChannelServiceOrderProduct.del_flag == 0,
  3529. )
  3530. ).first()
  3531. if not existing:
  3532. raise ValueError(f"产品 {project_code} 不存在于持仓中")
  3533. current_qty = float(existing.current_qty or 0)
  3534. if qty > current_qty:
  3535. raise ValueError(f"退订数量 {qty} 超过当前持有数量 {current_qty}")
  3536. unit_price = float(existing.agreement_unit_price or 0)
  3537. amt = qty * unit_price
  3538. total_amount += amt
  3539. existing.total_unsubscribe_qty = float(existing.total_unsubscribe_qty or 0) + qty
  3540. existing.current_qty = current_qty - qty
  3541. existing.total_unsubscribe_amount = float(existing.total_unsubscribe_amount or 0) + amt
  3542. existing.update_by = user_id
  3543. existing.update_time = now
  3544. db.add(existing)
  3545. db.flush()
  3546. products_json = json.dumps(products, ensure_ascii=False)
  3547. txn = BizChannelServiceOrderTxn(
  3548. id=generate_uuid(),
  3549. service_order_id=order_id,
  3550. txn_type="unsubscribe",
  3551. amount=total_amount,
  3552. txn_status="dsx",
  3553. products_json=products_json,
  3554. remark=remark or "",
  3555. tenant_no=tn,
  3556. order_num=0,
  3557. create_by=user_id,
  3558. create_dept_id="",
  3559. create_time=now,
  3560. update_by=user_id,
  3561. update_time=now,
  3562. del_flag=0,
  3563. )
  3564. db.add(txn)
  3565. db.commit()
  3566. db.refresh(txn)
  3567. return txn
  3568. def _rollback_purchase_txn_holdings(db, order_id, products, user_id, now=None):
  3569. now = now or datetime.now()
  3570. for p in products:
  3571. project_code = p.get("projectCode") or p.get("project_code") or ""
  3572. spec_text = p.get("specText") or p.get("spec_text") or ""
  3573. qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
  3574. existing = db.query(BizChannelServiceOrderProduct).filter(
  3575. and_(
  3576. BizChannelServiceOrderProduct.service_order_id == order_id,
  3577. BizChannelServiceOrderProduct.project_code == project_code,
  3578. BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
  3579. BizChannelServiceOrderProduct.del_flag == 0,
  3580. )
  3581. ).first()
  3582. if existing:
  3583. existing.total_buy_qty = float(existing.total_buy_qty or 0) - qty
  3584. existing.current_qty = float(existing.current_qty or 0) - qty
  3585. existing.update_by = user_id
  3586. existing.update_time = now
  3587. db.add(existing)
  3588. def _apply_purchase_txn_holdings(db, order_id, products, tenant_no, user_id, now=None):
  3589. now = now or datetime.now()
  3590. tn = tenant_no
  3591. total_amount = 0.0
  3592. for p in products:
  3593. project_code = p.get("projectCode") or p.get("project_code") or ""
  3594. project_name = p.get("projectName") or p.get("project_name") or ""
  3595. cell_type = p.get("cellType") or p.get("cell_type") or ""
  3596. spec_text = p.get("specText") or p.get("spec_text") or ""
  3597. agreement_unit_price = float(p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0)
  3598. qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
  3599. if qty <= 0:
  3600. raise ValueError("购买份数必须大于0")
  3601. amt = float(p.get("amount") or agreement_unit_price * qty)
  3602. total_amount += amt
  3603. existing = db.query(BizChannelServiceOrderProduct).filter(
  3604. and_(
  3605. BizChannelServiceOrderProduct.service_order_id == order_id,
  3606. BizChannelServiceOrderProduct.project_code == project_code,
  3607. BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
  3608. BizChannelServiceOrderProduct.del_flag == 0,
  3609. )
  3610. ).first()
  3611. if existing:
  3612. existing.total_buy_qty = float(existing.total_buy_qty or 0) + qty
  3613. existing.current_qty = float(existing.current_qty or 0) + qty
  3614. existing.update_by = user_id
  3615. existing.update_time = now
  3616. db.add(existing)
  3617. else:
  3618. prod_row = BizChannelServiceOrderProduct(
  3619. id=generate_uuid(),
  3620. service_order_id=order_id,
  3621. project_code=project_code,
  3622. project_name=project_name,
  3623. cell_type=cell_type,
  3624. spec_text=spec_text,
  3625. agreement_unit_price=agreement_unit_price,
  3626. buy_qty=qty,
  3627. buy_amount=amt,
  3628. total_buy_qty=qty,
  3629. total_unsubscribe_qty=0,
  3630. current_qty=qty,
  3631. total_unsubscribe_amount=0,
  3632. tenant_no=tn,
  3633. order_num=0,
  3634. remark="",
  3635. create_by=user_id,
  3636. create_dept_id="",
  3637. create_time=now,
  3638. update_by=user_id,
  3639. update_time=now,
  3640. del_flag=0,
  3641. )
  3642. db.add(prod_row)
  3643. db.flush()
  3644. return total_amount
  3645. def _rollback_unsubscribe_txn_holdings(db, order_id, products, user_id, now=None):
  3646. now = now or datetime.now()
  3647. for p in products:
  3648. project_code = p.get("projectCode") or p.get("project_code") or ""
  3649. spec_text = p.get("specText") or p.get("spec_text") or ""
  3650. qty = float(p.get("qty") or 0)
  3651. existing = db.query(BizChannelServiceOrderProduct).filter(
  3652. and_(
  3653. BizChannelServiceOrderProduct.service_order_id == order_id,
  3654. BizChannelServiceOrderProduct.project_code == project_code,
  3655. BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
  3656. BizChannelServiceOrderProduct.del_flag == 0,
  3657. )
  3658. ).first()
  3659. if existing:
  3660. unit_price = float(existing.agreement_unit_price or 0)
  3661. existing.total_unsubscribe_qty = float(existing.total_unsubscribe_qty or 0) - qty
  3662. existing.current_qty = float(existing.current_qty or 0) + qty
  3663. existing.total_unsubscribe_amount = float(existing.total_unsubscribe_amount or 0) - (qty * unit_price)
  3664. existing.update_by = user_id
  3665. existing.update_time = now
  3666. db.add(existing)
  3667. def _apply_unsubscribe_txn_holdings(db, order_id, products, user_id, now=None):
  3668. now = now or datetime.now()
  3669. total_amount = 0.0
  3670. for p in products:
  3671. project_code = p.get("projectCode") or p.get("project_code") or ""
  3672. spec_text = p.get("specText") or p.get("spec_text") or ""
  3673. qty = float(p.get("qty") or 0)
  3674. if qty <= 0:
  3675. raise ValueError("退订数量必须大于0")
  3676. existing = db.query(BizChannelServiceOrderProduct).filter(
  3677. and_(
  3678. BizChannelServiceOrderProduct.service_order_id == order_id,
  3679. BizChannelServiceOrderProduct.project_code == project_code,
  3680. BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
  3681. BizChannelServiceOrderProduct.del_flag == 0,
  3682. )
  3683. ).first()
  3684. if not existing:
  3685. raise ValueError(f"产品 {project_code} 不存在于持仓中")
  3686. current_qty = float(existing.current_qty or 0)
  3687. if qty > current_qty:
  3688. raise ValueError(f"退订数量 {qty} 超过当前持有数量 {current_qty}")
  3689. unit_price = float(existing.agreement_unit_price or 0)
  3690. amt = qty * unit_price
  3691. total_amount += amt
  3692. existing.total_unsubscribe_qty = float(existing.total_unsubscribe_qty or 0) + qty
  3693. existing.current_qty = current_qty - qty
  3694. existing.total_unsubscribe_amount = float(existing.total_unsubscribe_amount or 0) + amt
  3695. existing.update_by = user_id
  3696. existing.update_time = now
  3697. db.add(existing)
  3698. db.flush()
  3699. return total_amount
  3700. def update_service_order_txn(
  3701. db,
  3702. txn_id,
  3703. *,
  3704. amount=None,
  3705. remark=None,
  3706. finance_submit_attachments=None,
  3707. refund_attachments=None,
  3708. products=None,
  3709. tenant_no=None,
  3710. user_id=None,
  3711. ):
  3712. """更新待生效流水:仅 dsx;充值/退款可改金额与附件,购买/退订可改产品明细与备注"""
  3713. txn = get_service_order_txn(db, txn_id, tenant_no=tenant_no)
  3714. if not txn:
  3715. raise ValueError("流水不存在")
  3716. if txn.txn_status != "dsx":
  3717. raise ValueError("仅待生效流水可编辑")
  3718. now = datetime.now()
  3719. txn_type = (txn.txn_type or "").strip()
  3720. if amount is not None:
  3721. amt = float(amount)
  3722. if amt <= 0:
  3723. raise ValueError("金额必须大于0")
  3724. if txn_type in ("recharge", "refund"):
  3725. if txn_type == "refund":
  3726. order = get_service_order(db, txn.service_order_id, tenant_no)
  3727. if not order:
  3728. raise ValueError("关联订单不存在")
  3729. current_balance = float(order.current_balance) if order.current_balance else 0
  3730. if amt > current_balance:
  3731. raise ValueError(f"退款金额不能大于当前余额 {current_balance}")
  3732. txn.amount = amt
  3733. else:
  3734. raise ValueError("购买/退订流水不可直接修改金额,请调整产品明细")
  3735. if products is not None:
  3736. if txn_type not in ("purchase", "unsubscribe"):
  3737. raise ValueError("仅购买/退订流水可修改产品明细")
  3738. if not products:
  3739. raise ValueError("产品明细不能为空")
  3740. order = get_service_order(db, txn.service_order_id, tenant_no)
  3741. if not order:
  3742. raise ValueError("关联订单不存在")
  3743. old_products = json.loads(txn.products_json) if txn.products_json else []
  3744. if txn_type == "purchase":
  3745. _rollback_purchase_txn_holdings(db, order.id, old_products, user_id, now)
  3746. total_amount = _apply_purchase_txn_holdings(
  3747. db, order.id, products, tenant_no or order.tenant_no, user_id, now
  3748. )
  3749. else:
  3750. _rollback_unsubscribe_txn_holdings(db, order.id, old_products, user_id, now)
  3751. total_amount = _apply_unsubscribe_txn_holdings(db, order.id, products, user_id, now)
  3752. txn.amount = total_amount
  3753. txn.products_json = json.dumps(products, ensure_ascii=False)
  3754. if remark is not None:
  3755. txn.remark = remark or ""
  3756. if finance_submit_attachments is not None:
  3757. txn.finance_submit_attachments = (finance_submit_attachments or "").strip() or None
  3758. if refund_attachments is not None:
  3759. if txn_type != "refund":
  3760. raise ValueError("仅退款流水可修改退款附件")
  3761. txn.refund_attachments = (refund_attachments or "").strip() or None
  3762. txn.update_by = user_id
  3763. txn.update_time = now
  3764. db.add(txn)
  3765. db.commit()
  3766. db.refresh(txn)
  3767. return txn
  3768. def submit_txn_finance(db, txn_id, *, finance_submit_remark=None, finance_submit_attachments=None, tenant_no=None, user_id=None):
  3769. """发起流水财务确认:dsx → dqr"""
  3770. txn = get_service_order_txn(db, txn_id, tenant_no=tenant_no)
  3771. if not txn:
  3772. raise ValueError("流水不存在")
  3773. if txn.txn_status != "dsx":
  3774. raise ValueError("仅待生效流水可发起确认")
  3775. now = datetime.now()
  3776. txn.txn_status = "dqr"
  3777. txn.finance_submit_by = user_id
  3778. txn.finance_submit_time = now
  3779. submit_remark = (finance_submit_remark or "").strip()
  3780. if not submit_remark:
  3781. submit_remark = (txn.remark or "").strip()
  3782. txn.finance_submit_remark = submit_remark
  3783. if finance_submit_attachments:
  3784. txn.finance_submit_attachments = finance_submit_attachments
  3785. elif not (txn.finance_submit_attachments or "").strip():
  3786. if (txn.txn_type or "").strip() == "refund" and (txn.refund_attachments or "").strip():
  3787. txn.finance_submit_attachments = txn.refund_attachments
  3788. else:
  3789. txn.finance_submit_attachments = ""
  3790. txn.update_by = user_id
  3791. txn.update_time = now
  3792. db.add(txn)
  3793. db.commit()
  3794. db.refresh(txn)
  3795. return txn
  3796. def confirm_txn(db, txn_id, *, finance_remark=None, finance_confirm_attachments=None, tenant_no=None, user_id=None):
  3797. """财务确认流水:dqr → yqr,更新主表汇总和产品持仓,更新渠道账户余额"""
  3798. txn = get_service_order_txn(db, txn_id, tenant_no=tenant_no)
  3799. if not txn:
  3800. raise ValueError("流水不存在")
  3801. if txn.txn_status != "dqr":
  3802. raise ValueError("仅待确认流水可进行财务确认")
  3803. order = get_service_order(db, txn.service_order_id, tenant_no)
  3804. if not order:
  3805. raise ValueError("关联订单不存在")
  3806. now = datetime.now()
  3807. if txn.txn_type == "recharge":
  3808. # Increase channel account balance
  3809. account = _get_or_create_channel_account(db, order.channel_id, tenant_no or order.tenant_no, user_id)
  3810. balance_before = float(account.balance_amount) if account.balance_amount else 0
  3811. add_amount = float(txn.amount) if txn.amount else 0
  3812. account.balance_amount = balance_before + add_amount
  3813. balance_after = float(account.balance_amount)
  3814. account.update_by = user_id
  3815. account.update_time = now
  3816. db.add(account)
  3817. db.flush()
  3818. # Update order summary
  3819. order.total_recharge_amount = float(order.total_recharge_amount or 0) + add_amount
  3820. order.current_balance = float(order.current_balance or 0) + add_amount
  3821. txn.balance_before = balance_before
  3822. txn.balance_after = balance_after
  3823. elif txn.txn_type == "refund":
  3824. # Deduct channel account balance
  3825. account = _get_or_create_channel_account(db, order.channel_id, tenant_no or order.tenant_no, user_id)
  3826. balance_before = float(account.balance_amount) if account.balance_amount else 0
  3827. deduct_amount = float(txn.amount) if txn.amount else 0
  3828. account.balance_amount = balance_before - deduct_amount
  3829. balance_after = float(account.balance_amount)
  3830. account.update_by = user_id
  3831. account.update_time = now
  3832. db.add(account)
  3833. db.flush()
  3834. # Update order summary
  3835. order.total_refund_amount = float(order.total_refund_amount or 0) + deduct_amount
  3836. order.current_balance = float(order.current_balance or 0) - deduct_amount
  3837. txn.balance_before = balance_before
  3838. txn.balance_after = balance_after
  3839. elif txn.txn_type == "purchase":
  3840. # Products already updated at create time, just update channel quotas
  3841. products = json.loads(txn.products_json) if txn.products_json else []
  3842. for p in products:
  3843. project_code = p.get("projectCode") or p.get("project_code") or ""
  3844. spec_text = p.get("specText") or p.get("spec_text") or ""
  3845. qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
  3846. cell_type_code, _ = _resolve_agreement_price_cell_type(
  3847. db, order.channel_agreement_id, project_code, spec_text, p.get("cellType") or p.get("cell_type") or ""
  3848. )
  3849. _upsert_channel_product_quota(
  3850. db,
  3851. channel_id=order.channel_id,
  3852. project_code=project_code,
  3853. project_name=p.get("projectName") or p.get("project_name") or "",
  3854. cell_type_code=cell_type_code,
  3855. cell_type_name=p.get("cellType") or p.get("cell_type") or "",
  3856. spec_text=spec_text,
  3857. add_qty=qty,
  3858. tenant_no=tenant_no or order.tenant_no,
  3859. user_id=user_id,
  3860. )
  3861. elif txn.txn_type == "unsubscribe":
  3862. # Products already updated at create time, just reduce channel quotas
  3863. products = json.loads(txn.products_json) if txn.products_json else []
  3864. for p in products:
  3865. project_code = p.get("projectCode") or p.get("project_code") or ""
  3866. spec_text = p.get("specText") or p.get("spec_text") or ""
  3867. qty = float(p.get("qty") or 0)
  3868. cell_type_code, _ = _resolve_agreement_price_cell_type(
  3869. db, order.channel_agreement_id, project_code, spec_text, p.get("cellType") or p.get("cell_type") or ""
  3870. )
  3871. _reduce_channel_product_quota(
  3872. db,
  3873. channel_id=order.channel_id,
  3874. project_code=project_code,
  3875. cell_type_code=cell_type_code,
  3876. spec_text=spec_text,
  3877. reduce_qty=qty,
  3878. user_id=user_id,
  3879. )
  3880. # Update txn status
  3881. txn.txn_status = "yqr"
  3882. txn.finance_confirm_time = now
  3883. txn.finance_confirm_by = user_id
  3884. txn.finance_confirm_attachments = finance_confirm_attachments or ""
  3885. txn.finance_remark = finance_remark or ""
  3886. txn.update_by = user_id
  3887. txn.update_time = now
  3888. # Transition order to sxz if currently dsx
  3889. if order.order_status == "dsx":
  3890. order.order_status = "sxz"
  3891. order.update_by = user_id
  3892. order.update_time = now
  3893. db.add(txn)
  3894. db.add(order)
  3895. db.commit()
  3896. db.refresh(txn)
  3897. return txn
  3898. def void_txn(db, txn_id, *, tenant_no=None, user_id=None):
  3899. """作废流水:dsx/dqr → yzf。如果是 purchase/unsubscribe 还需要回滚产品持仓"""
  3900. txn = get_service_order_txn(db, txn_id, tenant_no=tenant_no)
  3901. if not txn:
  3902. raise ValueError("流水不存在")
  3903. if txn.txn_status not in ("dsx", "dqr"):
  3904. raise ValueError("仅待生效或待财务确认流水可作废")
  3905. if (txn.txn_type or "").strip() == "settlement":
  3906. raise ValueError("输注计划结算流水不可作废")
  3907. now = datetime.now()
  3908. order = get_service_order(db, txn.service_order_id, tenant_no)
  3909. if txn.txn_type == "purchase" and order:
  3910. # Rollback product holdings
  3911. products = json.loads(txn.products_json) if txn.products_json else []
  3912. for p in products:
  3913. project_code = p.get("projectCode") or p.get("project_code") or ""
  3914. spec_text = p.get("specText") or p.get("spec_text") or ""
  3915. qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
  3916. existing = db.query(BizChannelServiceOrderProduct).filter(
  3917. and_(
  3918. BizChannelServiceOrderProduct.service_order_id == order.id,
  3919. BizChannelServiceOrderProduct.project_code == project_code,
  3920. BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
  3921. BizChannelServiceOrderProduct.del_flag == 0,
  3922. )
  3923. ).first()
  3924. if existing:
  3925. existing.total_buy_qty = float(existing.total_buy_qty or 0) - qty
  3926. existing.current_qty = float(existing.current_qty or 0) - qty
  3927. existing.update_by = user_id
  3928. existing.update_time = now
  3929. db.add(existing)
  3930. txn_amount = float(txn.amount or 0)
  3931. if txn_amount > 0:
  3932. order.purchase_total_amount = max(
  3933. 0.0,
  3934. float(order.purchase_total_amount or 0) - txn_amount,
  3935. )
  3936. order.update_by = user_id
  3937. order.update_time = now
  3938. db.add(order)
  3939. elif txn.txn_type == "unsubscribe" and order:
  3940. # Rollback unsubscribe: restore product holdings
  3941. products = json.loads(txn.products_json) if txn.products_json else []
  3942. for p in products:
  3943. project_code = p.get("projectCode") or p.get("project_code") or ""
  3944. spec_text = p.get("specText") or p.get("spec_text") or ""
  3945. qty = float(p.get("qty") or 0)
  3946. unit_price = 0
  3947. existing = db.query(BizChannelServiceOrderProduct).filter(
  3948. and_(
  3949. BizChannelServiceOrderProduct.service_order_id == order.id,
  3950. BizChannelServiceOrderProduct.project_code == project_code,
  3951. BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
  3952. BizChannelServiceOrderProduct.del_flag == 0,
  3953. )
  3954. ).first()
  3955. if existing:
  3956. unit_price = float(existing.agreement_unit_price or 0)
  3957. existing.total_unsubscribe_qty = float(existing.total_unsubscribe_qty or 0) - qty
  3958. existing.current_qty = float(existing.current_qty or 0) + qty
  3959. existing.total_unsubscribe_amount = float(existing.total_unsubscribe_amount or 0) - (qty * unit_price)
  3960. existing.update_by = user_id
  3961. existing.update_time = now
  3962. db.add(existing)
  3963. txn.txn_status = "yzf"
  3964. txn.void_by = user_id
  3965. txn.void_time = now
  3966. txn.update_by = user_id
  3967. txn.update_time = now
  3968. db.add(txn)
  3969. db.commit()
  3970. db.refresh(txn)
  3971. return txn