| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376 |
- """中试服务管理 CRUD(细胞产品类型/规格/价格体系/项目价格、渠道信息、体检机构)"""
- from __future__ import annotations
- import json
- import math
- from datetime import datetime
- from typing import Any, Dict, List, Optional, Tuple
- from sqlalchemy import and_, func, or_
- from sqlalchemy.orm import Session
- from app.core.utils import generate_uuid
- from app.crud.collection_protocol import urls_to_attachment_list
- from app.models.models import (
- BizCellProductType,
- BizCellProductSpec,
- BizCellProductPrice,
- BizCellProductProjectSystem,
- BizCellProductProjectSystemItem,
- BizChannel,
- BizChannelAccount,
- BizChannelAgreement,
- BizChannelAgreementProductPrice,
- BizChannelInvoice,
- BizChannelMailAddress,
- BizChannelProductQuota,
- BizChannelServiceOrder,
- BizChannelServiceOrderProduct,
- BizChannelServiceOrderTxn,
- BizCheckupInstitution,
- BizCheckupInstitutionInvoice,
- BizCheckupInstitutionProject,
- BizCheckupInstitutionPackage,
- BizCheckupInstitutionPackageItem,
- SysUser,
- )
- def _normalize_date_bound(value: str, *, end_of_day: bool = False) -> str:
- val = (value or "").strip()
- if len(val) == 10:
- return f"{val} 23:59:59" if end_of_day else f"{val} 00:00:00"
- return val
- def _user_nick_name_map(db: Session, user_ids: List[str]) -> Dict[str, str]:
- ids = list({x for x in user_ids if x})
- if not ids:
- return {}
- rows = db.query(SysUser.id, SysUser.nick_name).filter(SysUser.id.in_(ids)).all()
- return {u.id: (u.nick_name or "").strip() for u in rows}
- # ────────────────────────────────────────────
- # Entity 1: BizCellProductType 细胞产品类型
- # ────────────────────────────────────────────
- def _row_to_dict_cell_product_type(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "productTypeCode": row.product_type_code or "",
- "productTypeName": row.product_type_name or "",
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def list_cell_product_types(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
- def _filtered():
- q0 = db.query(BizCellProductType).filter(BizCellProductType.del_flag == 0)
- if tenant_no:
- q0 = q0.filter(BizCellProductType.tenant_no == tenant_no)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizCellProductType.product_type_code.like(like),
- BizCellProductType.product_type_name.like(like),
- )
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, BizCellProductType, user_info, db, tenant_no)
- return q0
- total = _filtered().order_by(None).with_entities(func.count(BizCellProductType.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- rows = _filtered().order_by(BizCellProductType.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
- return [_row_to_dict_cell_product_type(x) for x in rows], int(total)
- def get_cell_product_type(db, type_id, tenant_no=None):
- q = db.query(BizCellProductType).filter(and_(BizCellProductType.id == type_id, BizCellProductType.del_flag == 0))
- if tenant_no:
- q = q.filter(BizCellProductType.tenant_no == tenant_no)
- return q.first()
- def cell_product_type_code_exists(db, code, tenant_no, exclude_id=None):
- q = db.query(BizCellProductType).filter(
- and_(BizCellProductType.product_type_code == code, BizCellProductType.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizCellProductType.tenant_no == tenant_no)
- if exclude_id:
- q = q.filter(BizCellProductType.id != exclude_id)
- return db.query(q.exists()).scalar()
- def create_cell_product_type(db, *, product_type_code, product_type_name, tenant_no="zkxw", order_num=None, remark=None, user_id=None):
- if cell_product_type_code_exists(db, product_type_code, tenant_no):
- raise ValueError(f"产品类型编码 {product_type_code} 已存在")
- obj = BizCellProductType(
- id=generate_uuid(),
- product_type_code=product_type_code,
- product_type_name=product_type_name,
- tenant_no=tenant_no,
- order_num=order_num or 0,
- remark=remark or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- 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):
- obj = get_cell_product_type(db, type_id, tenant_no)
- if not obj:
- raise ValueError("记录不存在")
- if product_type_code is not None and product_type_code != obj.product_type_code:
- if cell_product_type_code_exists(db, product_type_code, tenant_no, exclude_id=type_id):
- raise ValueError(f"产品类型编码 {product_type_code} 已存在")
- obj.product_type_code = product_type_code
- if product_type_name is not None:
- obj.product_type_name = product_type_name
- if order_num is not None:
- obj.order_num = order_num
- if remark is not None:
- obj.remark = remark
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def soft_delete_cell_product_type(db, type_id, tenant_no=None):
- obj = get_cell_product_type(db, type_id, tenant_no)
- if not obj:
- raise ValueError("记录不存在")
- obj.del_flag = 1
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- # ────────────────────────────────────────────
- # Entity 2: BizCellProductSpec 细胞产品规格
- # ────────────────────────────────────────────
- def _row_to_dict_cell_product_spec(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "specCode": row.spec_code or "",
- "specName": row.spec_name or "",
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def list_cell_product_specs(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
- def _filtered():
- q0 = db.query(BizCellProductSpec).filter(BizCellProductSpec.del_flag == 0)
- if tenant_no:
- q0 = q0.filter(BizCellProductSpec.tenant_no == tenant_no)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizCellProductSpec.spec_code.like(like),
- BizCellProductSpec.spec_name.like(like),
- )
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, BizCellProductSpec, user_info, db, tenant_no)
- return q0
- total = _filtered().order_by(None).with_entities(func.count(BizCellProductSpec.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- rows = _filtered().order_by(BizCellProductSpec.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
- return [_row_to_dict_cell_product_spec(x) for x in rows], int(total)
- def get_cell_product_spec(db, spec_id, tenant_no=None):
- q = db.query(BizCellProductSpec).filter(and_(BizCellProductSpec.id == spec_id, BizCellProductSpec.del_flag == 0))
- if tenant_no:
- q = q.filter(BizCellProductSpec.tenant_no == tenant_no)
- return q.first()
- def cell_product_spec_code_exists(db, code, tenant_no, exclude_id=None):
- q = db.query(BizCellProductSpec).filter(
- and_(BizCellProductSpec.spec_code == code, BizCellProductSpec.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizCellProductSpec.tenant_no == tenant_no)
- if exclude_id:
- q = q.filter(BizCellProductSpec.id != exclude_id)
- return db.query(q.exists()).scalar()
- def create_cell_product_spec(db, *, spec_code, spec_name, tenant_no="zkxw", order_num=None, remark=None, user_id=None):
- if cell_product_spec_code_exists(db, spec_code, tenant_no):
- raise ValueError(f"规格编码 {spec_code} 已存在")
- obj = BizCellProductSpec(
- id=generate_uuid(),
- spec_code=spec_code,
- spec_name=spec_name,
- tenant_no=tenant_no,
- order_num=order_num or 0,
- remark=remark or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- 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):
- obj = get_cell_product_spec(db, spec_id, tenant_no)
- if not obj:
- raise ValueError("记录不存在")
- if spec_code is not None and spec_code != obj.spec_code:
- if cell_product_spec_code_exists(db, spec_code, tenant_no, exclude_id=spec_id):
- raise ValueError(f"规格编码 {spec_code} 已存在")
- obj.spec_code = spec_code
- if spec_name is not None:
- obj.spec_name = spec_name
- if order_num is not None:
- obj.order_num = order_num
- if remark is not None:
- obj.remark = remark
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def soft_delete_cell_product_spec(db, spec_id, tenant_no=None):
- obj = get_cell_product_spec(db, spec_id, tenant_no)
- if not obj:
- raise ValueError("记录不存在")
- obj.del_flag = 1
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- # ────────────────────────────────────────────
- # Entity 3: BizCellProductPrice 细胞产品价格
- # ────────────────────────────────────────────
- def _row_to_dict_cell_product_price(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "projectName": row.project_name or "",
- "projectCode": row.project_code or "",
- "cellTypeCode": row.cell_type_code or "",
- "cellTypeName": row.cell_type_name or "",
- "specText": row.spec_text or "",
- "specId": row.spec_id or "",
- "cellProductTypeId": row.cell_product_type_id or "",
- "unitPrice": float(row.unit_price) if row.unit_price is not None else None,
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def list_cell_product_prices(db, *, page=1, page_size=10, keyword=None, tenant_no=None, cell_product_type_id=None, user_info=None):
- def _filtered():
- q0 = db.query(BizCellProductPrice).filter(BizCellProductPrice.del_flag == 0)
- if tenant_no:
- q0 = q0.filter(BizCellProductPrice.tenant_no == tenant_no)
- if cell_product_type_id:
- q0 = q0.filter(BizCellProductPrice.cell_product_type_id == cell_product_type_id)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizCellProductPrice.project_name.like(like),
- BizCellProductPrice.project_code.like(like),
- BizCellProductPrice.cell_type_name.like(like),
- )
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, BizCellProductPrice, user_info, db, tenant_no)
- return q0
- total = _filtered().order_by(None).with_entities(func.count(BizCellProductPrice.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- rows = _filtered().order_by(BizCellProductPrice.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
- return [_row_to_dict_cell_product_price(x) for x in rows], int(total)
- def get_cell_product_price(db, price_id, tenant_no=None):
- q = db.query(BizCellProductPrice).filter(and_(BizCellProductPrice.id == price_id, BizCellProductPrice.del_flag == 0))
- if tenant_no:
- q = q.filter(BizCellProductPrice.tenant_no == tenant_no)
- return q.first()
- def cell_product_price_code_exists(db, code, tenant_no, exclude_id=None):
- q = db.query(BizCellProductPrice).filter(
- and_(BizCellProductPrice.project_code == code, BizCellProductPrice.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizCellProductPrice.tenant_no == tenant_no)
- if exclude_id:
- q = q.filter(BizCellProductPrice.id != exclude_id)
- return db.query(q.exists()).scalar()
- def create_cell_product_price(
- db,
- *,
- project_name,
- project_code,
- cell_type_code,
- cell_type_name,
- spec_text,
- spec_id,
- cell_product_type_id,
- unit_price,
- tenant_no="zkxw",
- order_num=None,
- remark=None,
- user_id=None,
- ):
- if cell_product_price_code_exists(db, project_code, tenant_no):
- raise ValueError(f"项目编码 {project_code} 已存在")
- obj = BizCellProductPrice(
- id=generate_uuid(),
- project_name=project_name,
- project_code=project_code,
- cell_type_code=cell_type_code,
- cell_type_name=cell_type_name,
- spec_text=spec_text or "",
- spec_id=spec_id or "",
- cell_product_type_id=cell_product_type_id or "",
- unit_price=unit_price,
- tenant_no=tenant_no,
- order_num=order_num or 0,
- remark=remark or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def update_cell_product_price(
- db,
- price_id,
- *,
- project_name=None,
- project_code=None,
- cell_type_code=None,
- cell_type_name=None,
- spec_text=None,
- spec_id=None,
- cell_product_type_id=None,
- unit_price=None,
- order_num=None,
- remark=None,
- user_id=None,
- tenant_no=None,
- ):
- obj = get_cell_product_price(db, price_id, tenant_no)
- if not obj:
- raise ValueError("记录不存在")
- if project_code is not None and project_code != obj.project_code:
- if cell_product_price_code_exists(db, project_code, tenant_no, exclude_id=price_id):
- raise ValueError(f"项目编码 {project_code} 已存在")
- obj.project_code = project_code
- if project_name is not None:
- obj.project_name = project_name
- if cell_type_code is not None:
- obj.cell_type_code = cell_type_code
- if cell_type_name is not None:
- obj.cell_type_name = cell_type_name
- if spec_text is not None:
- obj.spec_text = spec_text
- if spec_id is not None:
- obj.spec_id = spec_id
- if cell_product_type_id is not None:
- obj.cell_product_type_id = cell_product_type_id
- if unit_price is not None:
- obj.unit_price = unit_price
- if order_num is not None:
- obj.order_num = order_num
- if remark is not None:
- obj.remark = remark
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def soft_delete_cell_product_price(db, price_id, tenant_no=None):
- obj = get_cell_product_price(db, price_id, tenant_no)
- if not obj:
- raise ValueError("记录不存在")
- obj.del_flag = 1
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- # ────────────────────────────────────────────
- # Entity 4: BizCellProductProjectSystem 项目价格体系(主)
- # ────────────────────────────────────────────
- def _row_to_dict_project_system(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "systemName": row.system_name or "",
- "systemCode": row.system_code or "",
- "status": row.status or "1",
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def list_project_systems(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
- def _filtered():
- q0 = db.query(BizCellProductProjectSystem).filter(BizCellProductProjectSystem.del_flag == 0)
- if tenant_no:
- q0 = q0.filter(BizCellProductProjectSystem.tenant_no == tenant_no)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizCellProductProjectSystem.system_code.like(like),
- BizCellProductProjectSystem.system_name.like(like),
- )
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, BizCellProductProjectSystem, user_info, db, tenant_no)
- return q0
- total = _filtered().order_by(None).with_entities(func.count(BizCellProductProjectSystem.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- rows = _filtered().order_by(BizCellProductProjectSystem.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
- return [_row_to_dict_project_system(x) for x in rows], int(total)
- def get_project_system(db, system_id, tenant_no=None):
- q = db.query(BizCellProductProjectSystem).filter(
- and_(BizCellProductProjectSystem.id == system_id, BizCellProductProjectSystem.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizCellProductProjectSystem.tenant_no == tenant_no)
- return q.first()
- def project_system_code_exists(db, code, tenant_no, exclude_id=None):
- q = db.query(BizCellProductProjectSystem).filter(
- and_(BizCellProductProjectSystem.system_code == code, BizCellProductProjectSystem.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizCellProductProjectSystem.tenant_no == tenant_no)
- if exclude_id:
- q = q.filter(BizCellProductProjectSystem.id != exclude_id)
- return db.query(q.exists()).scalar()
- def create_project_system(db, *, system_name, system_code, status="1", tenant_no="zkxw", order_num=None, remark=None, user_id=None):
- if project_system_code_exists(db, system_code, tenant_no):
- raise ValueError(f"体系编码 {system_code} 已存在")
- obj = BizCellProductProjectSystem(
- id=generate_uuid(),
- system_name=system_name,
- system_code=system_code,
- status=status or "1",
- tenant_no=tenant_no,
- order_num=order_num or 0,
- remark=remark or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- 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):
- obj = get_project_system(db, system_id, tenant_no)
- if not obj:
- raise ValueError("记录不存在")
- if system_code is not None and system_code != obj.system_code:
- if project_system_code_exists(db, system_code, tenant_no, exclude_id=system_id):
- raise ValueError(f"体系编码 {system_code} 已存在")
- obj.system_code = system_code
- if system_name is not None:
- obj.system_name = system_name
- if status is not None:
- obj.status = status
- if order_num is not None:
- obj.order_num = order_num
- if remark is not None:
- obj.remark = remark
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def soft_delete_project_system(db, system_id, tenant_no=None):
- obj = get_project_system(db, system_id, tenant_no)
- if not obj:
- raise ValueError("记录不存在")
- obj.del_flag = 1
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- # ────────────────────────────────────────────
- # Entity 5: BizCellProductProjectSystemItem 项目价格体系明细
- # ────────────────────────────────────────────
- def _row_to_dict_project_system_item(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "projectSystemId": row.project_system_id or "",
- "sourcePriceId": row.source_price_id or "",
- "projectName": row.project_name or "",
- "projectCode": row.project_code or "",
- "cellTypeCode": row.cell_type_code or "",
- "cellTypeName": row.cell_type_name or "",
- "specText": row.spec_text or "",
- "specId": row.spec_id or "",
- "cellProductTypeId": row.cell_product_type_id or "",
- "unitPrice": float(row.unit_price) if row.unit_price is not None else None,
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def list_project_system_items(db, project_system_id, tenant_no=None, user_info=None):
- q = db.query(BizCellProductProjectSystemItem).filter(
- and_(
- BizCellProductProjectSystemItem.project_system_id == project_system_id,
- BizCellProductProjectSystemItem.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizCellProductProjectSystemItem.tenant_no == tenant_no)
- if user_info:
- from app.core.data_scope import apply_data_scope
- q = apply_data_scope(q, BizCellProductProjectSystemItem, user_info, db, tenant_no)
- rows = q.order_by(
- BizCellProductProjectSystemItem.order_num.asc(),
- BizCellProductProjectSystemItem.create_time.asc(),
- ).all()
- return [_row_to_dict_project_system_item(x) for x in rows]
- def create_project_system_item(
- db,
- *,
- project_system_id,
- source_price_id="",
- project_name,
- project_code,
- cell_type_code,
- cell_type_name,
- spec_text="",
- spec_id="",
- cell_product_type_id="",
- unit_price,
- tenant_no="zkxw",
- order_num=None,
- remark=None,
- user_id=None,
- ):
- obj = BizCellProductProjectSystemItem(
- id=generate_uuid(),
- project_system_id=project_system_id,
- source_price_id=source_price_id,
- project_name=project_name,
- project_code=project_code,
- cell_type_code=cell_type_code,
- cell_type_name=cell_type_name,
- spec_text=spec_text or "",
- spec_id=spec_id or "",
- cell_product_type_id=cell_product_type_id or "",
- unit_price=unit_price,
- tenant_no=tenant_no,
- order_num=order_num or 0,
- remark=remark or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def update_project_system_item(
- db,
- item_id,
- *,
- source_price_id=None,
- project_name=None,
- project_code=None,
- cell_type_code=None,
- cell_type_name=None,
- spec_text=None,
- spec_id=None,
- cell_product_type_id=None,
- unit_price=None,
- order_num=None,
- remark=None,
- user_id=None,
- tenant_no=None,
- ):
- q = db.query(BizCellProductProjectSystemItem).filter(
- and_(BizCellProductProjectSystemItem.id == item_id, BizCellProductProjectSystemItem.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizCellProductProjectSystemItem.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- if source_price_id is not None:
- obj.source_price_id = source_price_id
- if project_name is not None:
- obj.project_name = project_name
- if project_code is not None:
- obj.project_code = project_code
- if cell_type_code is not None:
- obj.cell_type_code = cell_type_code
- if cell_type_name is not None:
- obj.cell_type_name = cell_type_name
- if spec_text is not None:
- obj.spec_text = spec_text
- if spec_id is not None:
- obj.spec_id = spec_id
- if cell_product_type_id is not None:
- obj.cell_product_type_id = cell_product_type_id
- if unit_price is not None:
- obj.unit_price = unit_price
- if order_num is not None:
- obj.order_num = order_num
- if remark is not None:
- obj.remark = remark
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def soft_delete_project_system_item(db, item_id, tenant_no=None):
- q = db.query(BizCellProductProjectSystemItem).filter(
- and_(BizCellProductProjectSystemItem.id == item_id, BizCellProductProjectSystemItem.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizCellProductProjectSystemItem.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- obj.del_flag = 1
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- # ────────────────────────────────────────────
- # Entity 6: BizChannel 渠道信息
- # ────────────────────────────────────────────
- def _row_to_dict_channel(
- row,
- db: Optional[Session] = None,
- tenant_no: Optional[str] = None,
- ) -> Dict[str, Any]:
- tn = tenant_no or row.tenant_no
- return {
- "id": row.id,
- "channelName": row.channel_name or "",
- "creditCode": row.credit_code or "",
- "channelType": row.channel_type or "",
- "contactName": row.contact_name or "",
- "contactPhone": row.contact_phone or "",
- "contactEmail": row.contact_email or "",
- "protocolAttachments": row.protocol_attachments or "",
- "attachmentFiles": urls_to_attachment_list(row.protocol_attachments, db, tn) if db else [],
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def list_channels(db, *, page=1, page_size=10, keyword=None, tenant_no=None, user_info=None):
- def _filtered():
- q0 = db.query(BizChannel).filter(BizChannel.del_flag == 0)
- if tenant_no:
- q0 = q0.filter(BizChannel.tenant_no == tenant_no)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizChannel.channel_name.like(like),
- BizChannel.credit_code.like(like),
- BizChannel.contact_name.like(like),
- )
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, BizChannel, user_info, db, tenant_no)
- return q0
- total = _filtered().order_by(None).with_entities(func.count(BizChannel.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- rows = _filtered().order_by(BizChannel.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
- return [_row_to_dict_channel(x, db, tenant_no) for x in rows], int(total)
- def get_channel(db, channel_id, tenant_no=None):
- q = db.query(BizChannel).filter(and_(BizChannel.id == channel_id, BizChannel.del_flag == 0))
- if tenant_no:
- q = q.filter(BizChannel.tenant_no == tenant_no)
- return q.first()
- def create_channel(
- db,
- *,
- channel_name,
- credit_code=None,
- channel_type=None,
- contact_name=None,
- contact_phone=None,
- contact_email=None,
- protocol_attachments=None,
- tenant_no="zkxw",
- order_num=None,
- remark=None,
- user_id=None,
- ):
- obj = BizChannel(
- id=generate_uuid(),
- channel_name=channel_name,
- credit_code=credit_code or "",
- channel_type=channel_type or "",
- contact_name=contact_name or "",
- contact_phone=contact_phone or "",
- contact_email=contact_email or "",
- protocol_attachments=protocol_attachments or "",
- tenant_no=tenant_no,
- order_num=order_num or 0,
- remark=remark or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def update_channel(
- db,
- channel_id,
- *,
- channel_name=None,
- credit_code=None,
- channel_type=None,
- contact_name=None,
- contact_phone=None,
- contact_email=None,
- protocol_attachments=None,
- order_num=None,
- remark=None,
- user_id=None,
- tenant_no=None,
- ):
- obj = get_channel(db, channel_id, tenant_no)
- if not obj:
- raise ValueError("记录不存在")
- if channel_name is not None:
- obj.channel_name = channel_name
- if credit_code is not None:
- obj.credit_code = credit_code
- if channel_type is not None:
- obj.channel_type = channel_type
- if contact_name is not None:
- obj.contact_name = contact_name
- if contact_phone is not None:
- obj.contact_phone = contact_phone
- if contact_email is not None:
- obj.contact_email = contact_email
- if protocol_attachments is not None:
- obj.protocol_attachments = protocol_attachments
- if order_num is not None:
- obj.order_num = order_num
- if remark is not None:
- obj.remark = remark
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def soft_delete_channel(db, channel_id, tenant_no=None):
- obj = get_channel(db, channel_id, tenant_no)
- if not obj:
- raise ValueError("记录不存在")
- obj.del_flag = 1
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- # ────────────────────────────────────────────
- # Entity 7: BizChannelInvoice 渠道开票信息
- # ────────────────────────────────────────────
- def _row_to_dict_channel_invoice(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "channelId": row.channel_id or "",
- "companyName": row.company_name or "",
- "taxNo": row.tax_no or "",
- "companyAddress": row.company_address or "",
- "companyPhone": row.company_phone or "",
- "bankName": row.bank_name or "",
- "bankAccount": row.bank_account or "",
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def list_channel_invoices(db, channel_id, tenant_no=None, user_info=None):
- q = db.query(BizChannelInvoice).filter(
- and_(
- BizChannelInvoice.channel_id == channel_id,
- BizChannelInvoice.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizChannelInvoice.tenant_no == tenant_no)
- if user_info:
- from app.core.data_scope import apply_data_scope
- q = apply_data_scope(q, BizChannelInvoice, user_info, db, tenant_no)
- rows = q.order_by(BizChannelInvoice.create_time.desc()).all()
- return [_row_to_dict_channel_invoice(x) for x in rows]
- def create_channel_invoice(
- db,
- *,
- channel_id,
- company_name,
- tax_no=None,
- company_address=None,
- company_phone=None,
- bank_name=None,
- bank_account=None,
- tenant_no="zkxw",
- order_num=None,
- remark=None,
- user_id=None,
- ):
- obj = BizChannelInvoice(
- id=generate_uuid(),
- channel_id=channel_id,
- company_name=company_name,
- tax_no=tax_no or "",
- company_address=company_address or "",
- company_phone=company_phone or "",
- bank_name=bank_name or "",
- bank_account=bank_account or "",
- tenant_no=tenant_no,
- order_num=order_num or 0,
- remark=remark or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def update_channel_invoice(
- db,
- invoice_id,
- *,
- company_name=None,
- tax_no=None,
- company_address=None,
- company_phone=None,
- bank_name=None,
- bank_account=None,
- order_num=None,
- remark=None,
- user_id=None,
- tenant_no=None,
- ):
- q = db.query(BizChannelInvoice).filter(
- and_(BizChannelInvoice.id == invoice_id, BizChannelInvoice.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizChannelInvoice.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- if company_name is not None:
- obj.company_name = company_name
- if tax_no is not None:
- obj.tax_no = tax_no
- if company_address is not None:
- obj.company_address = company_address
- if company_phone is not None:
- obj.company_phone = company_phone
- if bank_name is not None:
- obj.bank_name = bank_name
- if bank_account is not None:
- obj.bank_account = bank_account
- if order_num is not None:
- obj.order_num = order_num
- if remark is not None:
- obj.remark = remark
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def soft_delete_channel_invoice(db, invoice_id, tenant_no=None):
- q = db.query(BizChannelInvoice).filter(
- and_(BizChannelInvoice.id == invoice_id, BizChannelInvoice.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizChannelInvoice.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- obj.del_flag = 1
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- # ────────────────────────────────────────────
- # Entity 8: BizChannelMailAddress 渠道邮寄地址
- # ────────────────────────────────────────────
- def _row_to_dict_channel_mail_address(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "channelId": row.channel_id or "",
- "mailAddress": row.mail_address or "",
- "mailContact": row.mail_contact or "",
- "mailPhone": row.mail_phone or "",
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def list_channel_mail_addresses(db, channel_id, tenant_no=None, user_info=None):
- q = db.query(BizChannelMailAddress).filter(
- and_(
- BizChannelMailAddress.channel_id == channel_id,
- BizChannelMailAddress.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizChannelMailAddress.tenant_no == tenant_no)
- if user_info:
- from app.core.data_scope import apply_data_scope
- q = apply_data_scope(q, BizChannelMailAddress, user_info, db, tenant_no)
- rows = q.order_by(BizChannelMailAddress.create_time.desc()).all()
- return [_row_to_dict_channel_mail_address(x) for x in rows]
- def create_channel_mail_address(
- db,
- *,
- channel_id,
- mail_address,
- mail_contact=None,
- mail_phone=None,
- tenant_no="zkxw",
- order_num=None,
- remark=None,
- user_id=None,
- ):
- obj = BizChannelMailAddress(
- id=generate_uuid(),
- channel_id=channel_id,
- mail_address=mail_address,
- mail_contact=mail_contact or "",
- mail_phone=mail_phone or "",
- tenant_no=tenant_no,
- order_num=order_num or 0,
- remark=remark or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def update_channel_mail_address(
- db,
- address_id,
- *,
- mail_address=None,
- mail_contact=None,
- mail_phone=None,
- order_num=None,
- remark=None,
- user_id=None,
- tenant_no=None,
- ):
- q = db.query(BizChannelMailAddress).filter(
- and_(BizChannelMailAddress.id == address_id, BizChannelMailAddress.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizChannelMailAddress.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- if mail_address is not None:
- obj.mail_address = mail_address
- if mail_contact is not None:
- obj.mail_contact = mail_contact
- if mail_phone is not None:
- obj.mail_phone = mail_phone
- if order_num is not None:
- obj.order_num = order_num
- if remark is not None:
- obj.remark = remark
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def soft_delete_channel_mail_address(db, address_id, tenant_no=None):
- q = db.query(BizChannelMailAddress).filter(
- and_(BizChannelMailAddress.id == address_id, BizChannelMailAddress.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizChannelMailAddress.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- obj.del_flag = 1
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- # ────────────────────────────────────────────
- # Entity 9: BizChannelAgreement 渠道协议
- # ────────────────────────────────────────────
- def _row_to_dict_channel_agreement(row) -> Dict[str, Any]:
- def _fmt_date(v):
- """CHAR(8) YYYYMMDD → YYYY-MM-DD"""
- if not v or len(v) != 8:
- return v or ""
- return f"{v[:4]}-{v[4:6]}-{v[6:8]}"
- return {
- "id": row.id,
- "channelId": row.channel_id or "",
- "agreementType": row.agreement_type or "",
- "businessType": row.business_type or "",
- "settleMode": row.settle_mode or "",
- "creditLimitAmount": float(row.credit_limit_amount) if row.credit_limit_amount is not None else None,
- "effectiveDate": _fmt_date(row.effective_date),
- "expireDate": _fmt_date(row.expire_date),
- "agreementStatus": row.agreement_status or "",
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def list_channel_agreements(db, channel_id, tenant_no=None, user_info=None):
- q = db.query(BizChannelAgreement).filter(
- and_(
- BizChannelAgreement.channel_id == channel_id,
- BizChannelAgreement.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
- if user_info:
- from app.core.data_scope import apply_data_scope
- q = apply_data_scope(q, BizChannelAgreement, user_info, db, tenant_no)
- rows = q.order_by(BizChannelAgreement.create_time.desc()).all()
- return [_row_to_dict_channel_agreement(x) for x in rows]
- def create_channel_agreement(
- db,
- *,
- channel_id,
- agreement_type,
- business_type=None,
- settle_mode=None,
- credit_limit_amount=None,
- effective_date=None,
- expire_date=None,
- agreement_status="dsx",
- tenant_no="zkxw",
- order_num=None,
- remark=None,
- user_id=None,
- ):
- _ed = (effective_date or "").replace("-", "") if effective_date else ""
- _xd = (expire_date or "").replace("-", "") if expire_date else ""
- obj = BizChannelAgreement(
- id=generate_uuid(),
- channel_id=channel_id,
- agreement_type=agreement_type,
- business_type=business_type or "",
- settle_mode=settle_mode or "",
- credit_limit_amount=credit_limit_amount,
- effective_date=_ed,
- expire_date=_xd,
- agreement_status=agreement_status or "dsx",
- tenant_no=tenant_no,
- order_num=order_num or 0,
- remark=remark or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def update_channel_agreement(
- db,
- agreement_id,
- *,
- agreement_type=None,
- business_type=None,
- settle_mode=None,
- credit_limit_amount=None,
- effective_date=None,
- expire_date=None,
- order_num=None,
- remark=None,
- user_id=None,
- tenant_no=None,
- ):
- q = db.query(BizChannelAgreement).filter(
- and_(BizChannelAgreement.id == agreement_id, BizChannelAgreement.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- if agreement_type is not None:
- obj.agreement_type = agreement_type
- if business_type is not None:
- obj.business_type = business_type
- if settle_mode is not None:
- obj.settle_mode = settle_mode
- if credit_limit_amount is not None:
- obj.credit_limit_amount = credit_limit_amount
- if effective_date is not None:
- obj.effective_date = effective_date.replace("-", "") if effective_date else ""
- if expire_date is not None:
- obj.expire_date = expire_date.replace("-", "") if expire_date else ""
- if order_num is not None:
- obj.order_num = order_num
- if remark is not None:
- obj.remark = remark
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def soft_delete_channel_agreement(db, agreement_id, tenant_no=None):
- q = db.query(BizChannelAgreement).filter(
- and_(BizChannelAgreement.id == agreement_id, BizChannelAgreement.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- obj.del_flag = 1
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- def activate_channel_agreement(db, agreement_id, tenant_no=None, user_id=None):
- """生效协议:仅待生效(dsx)可变为生效中(sxz)"""
- q = db.query(BizChannelAgreement).filter(
- and_(BizChannelAgreement.id == agreement_id, BizChannelAgreement.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- if (obj.agreement_status or "").strip() != "dsx":
- raise ValueError("仅待生效状态的协议可执行生效操作")
- obj.agreement_status = "sxz"
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def void_channel_agreement(db, agreement_id, tenant_no=None, user_id=None):
- """作废协议:设置 agreement_status='yzf'"""
- q = db.query(BizChannelAgreement).filter(
- and_(BizChannelAgreement.id == agreement_id, BizChannelAgreement.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- obj.agreement_status = "yzf"
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- # ────────────────────────────────────────────
- # Entity 10: BizChannelAgreementProductPrice 渠道协议产品价格
- # ────────────────────────────────────────────
- def _row_to_dict_channel_agreement_price(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "channelAgreementId": row.channel_agreement_id or "",
- "projectName": row.project_name or "",
- "projectCode": row.project_code or "",
- "cellTypeCode": row.cell_type_code or "",
- "cellTypeName": row.cell_type_name or "",
- "specText": row.spec_text or "",
- "originalPrice": float(row.original_price) if row.original_price is not None else None,
- "agreementPrice": float(row.agreement_price) if row.agreement_price is not None else None,
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def _channel_agreement_price_item_key(
- project_code=None,
- project_name=None,
- cell_type_code=None,
- cell_type_name=None,
- spec_text=None,
- ) -> str:
- code = (project_code or project_name or "").strip().lower()
- cell = (cell_type_code or cell_type_name or "").strip().lower()
- spec = (spec_text or "").strip().lower()
- return f"{code}|{cell}|{spec}"
- def _channel_agreement_price_exists(
- db,
- channel_agreement_id,
- *,
- project_code=None,
- project_name=None,
- cell_type_code=None,
- cell_type_name=None,
- spec_text=None,
- tenant_no=None,
- exclude_price_id=None,
- ):
- target_key = _channel_agreement_price_item_key(
- project_code=project_code,
- project_name=project_name,
- cell_type_code=cell_type_code,
- cell_type_name=cell_type_name,
- spec_text=spec_text,
- )
- q = db.query(BizChannelAgreementProductPrice).filter(
- and_(
- BizChannelAgreementProductPrice.channel_agreement_id == channel_agreement_id,
- BizChannelAgreementProductPrice.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizChannelAgreementProductPrice.tenant_no == tenant_no)
- if exclude_price_id:
- q = q.filter(BizChannelAgreementProductPrice.id != exclude_price_id)
- for row in q.all():
- if _channel_agreement_price_item_key(
- project_code=row.project_code,
- project_name=row.project_name,
- cell_type_code=row.cell_type_code,
- cell_type_name=row.cell_type_name,
- spec_text=row.spec_text,
- ) == target_key:
- return True
- return False
- def list_channel_agreement_prices(db, channel_agreement_id, tenant_no=None, user_info=None):
- q = db.query(BizChannelAgreementProductPrice).filter(
- and_(
- BizChannelAgreementProductPrice.channel_agreement_id == channel_agreement_id,
- BizChannelAgreementProductPrice.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizChannelAgreementProductPrice.tenant_no == tenant_no)
- if user_info:
- from app.core.data_scope import apply_data_scope
- q = apply_data_scope(q, BizChannelAgreementProductPrice, user_info, db, tenant_no)
- rows = q.order_by(BizChannelAgreementProductPrice.create_time.desc()).all()
- return [_row_to_dict_channel_agreement_price(x) for x in rows]
- def create_channel_agreement_price(
- db,
- *,
- channel_agreement_id,
- project_name=None,
- project_code=None,
- cell_type_code=None,
- cell_type_name=None,
- spec_text=None,
- original_price=None,
- agreement_price=None,
- tenant_no="zkxw",
- order_num=None,
- remark=None,
- user_id=None,
- ):
- if _channel_agreement_price_exists(
- db,
- channel_agreement_id,
- project_code=project_code,
- project_name=project_name,
- cell_type_code=cell_type_code,
- cell_type_name=cell_type_name,
- spec_text=spec_text,
- tenant_no=tenant_no,
- ):
- raise ValueError("该项目信息已存在,不可重复添加")
- obj = BizChannelAgreementProductPrice(
- id=generate_uuid(),
- channel_agreement_id=channel_agreement_id,
- project_name=project_name or "",
- project_code=project_code or "",
- cell_type_code=cell_type_code or "",
- cell_type_name=cell_type_name or "",
- spec_text=spec_text or "",
- original_price=original_price,
- agreement_price=agreement_price,
- tenant_no=tenant_no,
- order_num=order_num or 0,
- remark=remark or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def update_channel_agreement_price(
- db,
- price_id,
- *,
- project_name=None,
- project_code=None,
- cell_type_code=None,
- cell_type_name=None,
- spec_text=None,
- original_price=None,
- agreement_price=None,
- order_num=None,
- remark=None,
- user_id=None,
- tenant_no=None,
- ):
- q = db.query(BizChannelAgreementProductPrice).filter(
- and_(BizChannelAgreementProductPrice.id == price_id, BizChannelAgreementProductPrice.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizChannelAgreementProductPrice.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- next_project_name = project_name if project_name is not None else obj.project_name
- next_project_code = project_code if project_code is not None else obj.project_code
- next_cell_type_code = cell_type_code if cell_type_code is not None else obj.cell_type_code
- next_cell_type_name = cell_type_name if cell_type_name is not None else obj.cell_type_name
- next_spec_text = spec_text if spec_text is not None else obj.spec_text
- if _channel_agreement_price_exists(
- db,
- obj.channel_agreement_id,
- project_code=next_project_code,
- project_name=next_project_name,
- cell_type_code=next_cell_type_code,
- cell_type_name=next_cell_type_name,
- spec_text=next_spec_text,
- tenant_no=tenant_no,
- exclude_price_id=price_id,
- ):
- raise ValueError("该项目信息已存在,不可重复添加")
- if project_name is not None:
- obj.project_name = project_name
- if project_code is not None:
- obj.project_code = project_code
- if cell_type_code is not None:
- obj.cell_type_code = cell_type_code
- if cell_type_name is not None:
- obj.cell_type_name = cell_type_name
- if spec_text is not None:
- obj.spec_text = spec_text
- if original_price is not None:
- obj.original_price = original_price
- if agreement_price is not None:
- obj.agreement_price = agreement_price
- if order_num is not None:
- obj.order_num = order_num
- if remark is not None:
- obj.remark = remark
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def soft_delete_channel_agreement_price(db, price_id, tenant_no=None):
- q = db.query(BizChannelAgreementProductPrice).filter(
- and_(BizChannelAgreementProductPrice.id == price_id, BizChannelAgreementProductPrice.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizChannelAgreementProductPrice.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- obj.del_flag = 1
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- # ────────────────────────────────────────────
- # Entity 11: BizCheckupInstitution 体检机构
- # ────────────────────────────────────────────
- def _row_to_dict_checkup_institution(row, projects: Optional[List[Dict[str, Any]]] = None) -> Dict[str, Any]:
- data = {
- "id": row.id,
- "institutionName": row.institution_name or "",
- "institutionAddress": row.institution_address or "",
- "contactName": row.contact_name or "",
- "contactPhone": row.contact_phone or "",
- "institutionLogo": row.institution_logo or "",
- "status": row.status or "1",
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- if projects is not None:
- data["projects"] = projects
- return data
- def list_checkup_institutions(db, *, page=1, page_size=10, keyword=None, status=None, tenant_no=None, user_info=None):
- def _filtered():
- q0 = db.query(BizCheckupInstitution).filter(BizCheckupInstitution.del_flag == 0)
- if tenant_no:
- q0 = q0.filter(BizCheckupInstitution.tenant_no == tenant_no)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizCheckupInstitution.institution_name.like(like),
- BizCheckupInstitution.contact_name.like(like),
- )
- )
- st = (status or "").strip()
- if st in ("0", "1"):
- q0 = q0.filter(BizCheckupInstitution.status == st)
- if user_info:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, BizCheckupInstitution, user_info, db, tenant_no)
- return q0
- total = _filtered().order_by(None).with_entities(func.count(BizCheckupInstitution.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- rows = _filtered().order_by(BizCheckupInstitution.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
- institution_ids = [x.id for x in rows]
- projects_by_inst: Dict[str, List[Dict[str, Any]]] = {iid: [] for iid in institution_ids}
- if institution_ids:
- pq = db.query(BizCheckupInstitutionProject).filter(
- BizCheckupInstitutionProject.checkup_institution_id.in_(institution_ids),
- BizCheckupInstitutionProject.del_flag == 0,
- )
- if tenant_no:
- pq = pq.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
- if user_info:
- from app.core.data_scope import apply_data_scope
- pq = apply_data_scope(pq, BizCheckupInstitutionProject, user_info, db, tenant_no)
- for pr in pq.order_by(BizCheckupInstitutionProject.create_time.desc()).all():
- projects_by_inst.setdefault(pr.checkup_institution_id, []).append(
- _row_to_dict_checkup_institution_project(pr)
- )
- return [
- _row_to_dict_checkup_institution(x, projects_by_inst.get(x.id, [])) for x in rows
- ], int(total)
- def get_checkup_institution(db, institution_id, tenant_no=None):
- q = db.query(BizCheckupInstitution).filter(
- and_(BizCheckupInstitution.id == institution_id, BizCheckupInstitution.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizCheckupInstitution.tenant_no == tenant_no)
- return q.first()
- def create_checkup_institution(
- db,
- *,
- institution_name,
- institution_address=None,
- contact_name=None,
- contact_phone=None,
- institution_logo=None,
- status="1",
- tenant_no="zkxw",
- order_num=None,
- remark=None,
- user_id=None,
- ):
- obj = BizCheckupInstitution(
- id=generate_uuid(),
- institution_name=institution_name,
- institution_address=institution_address or "",
- contact_name=contact_name or "",
- contact_phone=contact_phone or "",
- institution_logo=(institution_logo or "").strip() or None,
- status=status or "1",
- tenant_no=tenant_no,
- order_num=order_num or 0,
- remark=remark or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def update_checkup_institution(
- db,
- institution_id,
- *,
- institution_name=None,
- institution_address=None,
- contact_name=None,
- contact_phone=None,
- institution_logo=None,
- status=None,
- order_num=None,
- remark=None,
- user_id=None,
- tenant_no=None,
- ):
- obj = get_checkup_institution(db, institution_id, tenant_no)
- if not obj:
- raise ValueError("记录不存在")
- if institution_name is not None:
- obj.institution_name = institution_name
- if institution_address is not None:
- obj.institution_address = institution_address
- if contact_name is not None:
- obj.contact_name = contact_name
- if contact_phone is not None:
- obj.contact_phone = contact_phone
- if institution_logo is not None:
- obj.institution_logo = (institution_logo or "").strip() or None
- if status is not None:
- obj.status = status
- if order_num is not None:
- obj.order_num = order_num
- if remark is not None:
- obj.remark = remark
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def soft_delete_checkup_institution(db, institution_id, tenant_no=None):
- obj = get_checkup_institution(db, institution_id, tenant_no)
- if not obj:
- raise ValueError("记录不存在")
- obj.del_flag = 1
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- # ────────────────────────────────────────────
- # Entity 12: BizCheckupInstitutionInvoice 体检机构开票信息
- # ────────────────────────────────────────────
- def _row_to_dict_checkup_institution_invoice(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "checkupInstitutionId": row.checkup_institution_id or "",
- "companyName": row.company_name or "",
- "taxNo": row.tax_no or "",
- "companyAddress": row.company_address or "",
- "companyPhone": row.company_phone or "",
- "bankName": row.bank_name or "",
- "bankAccount": row.bank_account or "",
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def list_checkup_institution_invoices(db, checkup_institution_id, tenant_no=None, user_info=None):
- q = db.query(BizCheckupInstitutionInvoice).filter(
- and_(
- BizCheckupInstitutionInvoice.checkup_institution_id == checkup_institution_id,
- BizCheckupInstitutionInvoice.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizCheckupInstitutionInvoice.tenant_no == tenant_no)
- if user_info:
- from app.core.data_scope import apply_data_scope
- q = apply_data_scope(q, BizCheckupInstitutionInvoice, user_info, db, tenant_no)
- rows = q.order_by(BizCheckupInstitutionInvoice.create_time.desc()).all()
- return [_row_to_dict_checkup_institution_invoice(x) for x in rows]
- def create_checkup_institution_invoice(
- db,
- *,
- checkup_institution_id,
- company_name,
- tax_no=None,
- company_address=None,
- company_phone=None,
- bank_name=None,
- bank_account=None,
- tenant_no="zkxw",
- order_num=None,
- remark=None,
- user_id=None,
- ):
- obj = BizCheckupInstitutionInvoice(
- id=generate_uuid(),
- checkup_institution_id=checkup_institution_id,
- company_name=company_name,
- tax_no=tax_no or "",
- company_address=company_address or "",
- company_phone=company_phone or "",
- bank_name=bank_name or "",
- bank_account=bank_account or "",
- tenant_no=tenant_no,
- order_num=order_num or 0,
- remark=remark or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def update_checkup_institution_invoice(
- db,
- invoice_id,
- *,
- company_name=None,
- tax_no=None,
- company_address=None,
- company_phone=None,
- bank_name=None,
- bank_account=None,
- order_num=None,
- remark=None,
- user_id=None,
- tenant_no=None,
- ):
- q = db.query(BizCheckupInstitutionInvoice).filter(
- and_(BizCheckupInstitutionInvoice.id == invoice_id, BizCheckupInstitutionInvoice.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizCheckupInstitutionInvoice.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- if company_name is not None:
- obj.company_name = company_name
- if tax_no is not None:
- obj.tax_no = tax_no
- if company_address is not None:
- obj.company_address = company_address
- if company_phone is not None:
- obj.company_phone = company_phone
- if bank_name is not None:
- obj.bank_name = bank_name
- if bank_account is not None:
- obj.bank_account = bank_account
- if order_num is not None:
- obj.order_num = order_num
- if remark is not None:
- obj.remark = remark
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def soft_delete_checkup_institution_invoice(db, invoice_id, tenant_no=None):
- q = db.query(BizCheckupInstitutionInvoice).filter(
- and_(BizCheckupInstitutionInvoice.id == invoice_id, BizCheckupInstitutionInvoice.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizCheckupInstitutionInvoice.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- obj.del_flag = 1
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- # ────────────────────────────────────────────
- # Entity 13: BizCheckupInstitutionProject 体检机构项目及价格
- # ────────────────────────────────────────────
- def _row_to_dict_checkup_institution_project(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "checkupInstitutionId": row.checkup_institution_id or "",
- "projectName": row.project_name or "",
- "projectFee": float(row.project_fee) if row.project_fee is not None else None,
- "status": row.status or "1",
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def list_checkup_institution_projects(db, checkup_institution_id, tenant_no=None, user_info=None):
- q = db.query(BizCheckupInstitutionProject).filter(
- and_(
- BizCheckupInstitutionProject.checkup_institution_id == checkup_institution_id,
- BizCheckupInstitutionProject.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
- if user_info:
- from app.core.data_scope import apply_data_scope
- q = apply_data_scope(q, BizCheckupInstitutionProject, user_info, db, tenant_no)
- rows = q.order_by(BizCheckupInstitutionProject.create_time.desc()).all()
- return [_row_to_dict_checkup_institution_project(x) for x in rows]
- def create_checkup_institution_project(
- db,
- *,
- checkup_institution_id,
- project_name,
- project_fee=None,
- status="1",
- tenant_no="zkxw",
- order_num=None,
- remark=None,
- user_id=None,
- ):
- obj = BizCheckupInstitutionProject(
- id=generate_uuid(),
- checkup_institution_id=checkup_institution_id,
- project_name=project_name,
- project_fee=project_fee,
- status=status or "1",
- tenant_no=tenant_no,
- order_num=order_num or 0,
- remark=remark or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def update_checkup_institution_project(
- db,
- project_id,
- *,
- project_name=None,
- project_fee=None,
- status=None,
- order_num=None,
- remark=None,
- user_id=None,
- tenant_no=None,
- ):
- q = db.query(BizCheckupInstitutionProject).filter(
- and_(BizCheckupInstitutionProject.id == project_id, BizCheckupInstitutionProject.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- if project_name is not None:
- obj.project_name = project_name
- if project_fee is not None:
- obj.project_fee = project_fee
- if status is not None:
- obj.status = status
- if order_num is not None:
- obj.order_num = order_num
- if remark is not None:
- obj.remark = remark
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- db.refresh(obj)
- return obj
- def soft_delete_checkup_institution_project(db, project_id, tenant_no=None):
- q = db.query(BizCheckupInstitutionProject).filter(
- and_(BizCheckupInstitutionProject.id == project_id, BizCheckupInstitutionProject.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
- obj = q.first()
- if not obj:
- raise ValueError("记录不存在")
- obj.del_flag = 1
- obj.update_time = datetime.now()
- db.add(obj)
- db.commit()
- # ────────────────────────────────────────────
- # Entity 14: BizCheckupInstitutionPackage 体检机构项目套餐
- # ────────────────────────────────────────────
- def _sum_package_item_fees(items: List[Dict[str, Any]]) -> float:
- total = 0.0
- for it in items:
- fee = it.get("project_fee")
- if fee is None and "projectFee" in it:
- fee = it.get("projectFee")
- total += float(fee) if fee is not None else 0.0
- return total
- def _row_to_dict_checkup_institution_package(
- row,
- *,
- total_fee: Optional[float] = None,
- item_count: Optional[int] = None,
- ) -> Dict[str, Any]:
- d = {
- "id": row.id,
- "checkupInstitutionId": row.checkup_institution_id or "",
- "packageName": row.package_name or "",
- "packageCode": row.package_code or "",
- "status": row.status or "1",
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- if total_fee is not None:
- d["totalFee"] = total_fee
- if item_count is not None:
- d["itemCount"] = item_count
- return d
- def _row_to_dict_checkup_institution_package_item(
- row,
- *,
- source_project_deleted: bool = False,
- source_project_status: Optional[str] = None,
- ) -> Dict[str, Any]:
- return {
- "id": row.id,
- "packageId": row.package_id or "",
- "checkupInstitutionProjectId": row.checkup_institution_project_id or "",
- "projectName": row.project_name or "",
- "projectFee": float(row.project_fee) if row.project_fee is not None else None,
- "status": source_project_status if source_project_status is not None else "1",
- "sourceProjectDeleted": source_project_deleted,
- "tenantNo": row.tenant_no or "",
- "orderNum": row.order_num if row.order_num is not None else 0,
- "remark": row.remark or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def checkup_package_code_exists(db, institution_id, code, tenant_no, exclude_id=None):
- q = db.query(BizCheckupInstitutionPackage).filter(
- and_(
- BizCheckupInstitutionPackage.checkup_institution_id == institution_id,
- BizCheckupInstitutionPackage.package_code == code,
- BizCheckupInstitutionPackage.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizCheckupInstitutionPackage.tenant_no == tenant_no)
- if exclude_id:
- q = q.filter(BizCheckupInstitutionPackage.id != exclude_id)
- return db.query(q.exists()).scalar()
- def _get_checkup_project_for_package(db, project_id, institution_id, tenant_no=None):
- q = db.query(BizCheckupInstitutionProject).filter(
- BizCheckupInstitutionProject.id == project_id,
- BizCheckupInstitutionProject.checkup_institution_id == institution_id,
- )
- if tenant_no:
- q = q.filter(BizCheckupInstitutionProject.tenant_no == tenant_no)
- return q.first()
- def _validate_package_items_for_save(
- db,
- institution_id,
- items,
- tenant_no=None,
- existing_item_project_ids=None,
- ):
- if not items:
- raise ValueError("套餐至少包含一个体检项目")
- existing_ids = set(existing_item_project_ids or [])
- seen = set()
- for it in items:
- pid = it.get("checkup_institution_project_id") or it.get("checkupInstitutionProjectId")
- if not pid:
- raise ValueError("套餐明细缺少机构项目")
- if pid in seen:
- raise ValueError("套餐内不能重复添加同一体检项目")
- seen.add(pid)
- proj = _get_checkup_project_for_package(db, pid, institution_id, tenant_no)
- if not proj:
- raise ValueError("存在无效的体检项目")
- if pid in existing_ids:
- continue
- if proj.del_flag != 0:
- raise ValueError("不能新添加已删除的体检项目")
- if (proj.status or "1") != "1":
- raise ValueError("套餐不能包含已停用的体检项目")
- def _list_package_items_raw(db, package_id, tenant_no=None):
- q = db.query(BizCheckupInstitutionPackageItem).filter(
- and_(
- BizCheckupInstitutionPackageItem.package_id == package_id,
- BizCheckupInstitutionPackageItem.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizCheckupInstitutionPackageItem.tenant_no == tenant_no)
- return q.order_by(BizCheckupInstitutionPackageItem.order_num.asc(), BizCheckupInstitutionPackageItem.create_time.asc()).all()
- def _package_items_to_dicts(db, institution_id, item_rows, tenant_no=None):
- result = []
- for row in item_rows:
- proj = _get_checkup_project_for_package(
- db, row.checkup_institution_project_id, institution_id, tenant_no
- )
- deleted = proj is None or proj.del_flag != 0
- proj_status = (proj.status or "1") if proj else "1"
- result.append(
- _row_to_dict_checkup_institution_package_item(
- row,
- source_project_deleted=deleted,
- source_project_status=proj_status,
- )
- )
- return result
- def _insert_package_items(
- db,
- *,
- package_id,
- institution_id,
- items,
- tenant_no,
- user_id=None,
- ):
- for idx, it in enumerate(items):
- pid = it.get("checkup_institution_project_id") or it.get("checkupInstitutionProjectId")
- proj = _get_checkup_project_for_package(db, pid, institution_id, tenant_no)
- name = (it.get("project_name") or it.get("projectName") or "").strip()
- if not name and proj:
- name = proj.project_name or ""
- fee = it.get("project_fee")
- if fee is None:
- fee = it.get("projectFee")
- obj = BizCheckupInstitutionPackageItem(
- id=generate_uuid(),
- package_id=package_id,
- checkup_institution_project_id=pid,
- project_name=name,
- project_fee=fee,
- tenant_no=tenant_no,
- order_num=it.get("order_num") if it.get("order_num") is not None else it.get("orderNum") or (idx + 1),
- remark=it.get("remark") or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- def list_checkup_institution_packages(db, checkup_institution_id, tenant_no=None, user_info=None):
- q = db.query(BizCheckupInstitutionPackage).filter(
- and_(
- BizCheckupInstitutionPackage.checkup_institution_id == checkup_institution_id,
- BizCheckupInstitutionPackage.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizCheckupInstitutionPackage.tenant_no == tenant_no)
- if user_info:
- from app.core.data_scope import apply_data_scope
- q = apply_data_scope(q, BizCheckupInstitutionPackage, user_info, db, tenant_no)
- rows = q.order_by(BizCheckupInstitutionPackage.create_time.desc()).all()
- out = []
- for row in rows:
- item_rows = _list_package_items_raw(db, row.id, tenant_no)
- fees = [float(x.project_fee) if x.project_fee is not None else 0.0 for x in item_rows]
- out.append(
- _row_to_dict_checkup_institution_package(
- row,
- total_fee=sum(fees),
- item_count=len(item_rows),
- )
- )
- return out
- def get_checkup_institution_package(db, package_id, tenant_no=None):
- q = db.query(BizCheckupInstitutionPackage).filter(
- and_(BizCheckupInstitutionPackage.id == package_id, BizCheckupInstitutionPackage.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizCheckupInstitutionPackage.tenant_no == tenant_no)
- return q.first()
- def get_checkup_institution_package_detail(db, package_id, tenant_no=None):
- pkg = get_checkup_institution_package(db, package_id, tenant_no)
- if not pkg:
- return None
- item_rows = _list_package_items_raw(db, package_id, tenant_no)
- items = _package_items_to_dicts(db, pkg.checkup_institution_id, item_rows, tenant_no)
- fees = [float(x.project_fee) if x.project_fee is not None else 0.0 for x in item_rows]
- d = _row_to_dict_checkup_institution_package(pkg, total_fee=sum(fees), item_count=len(item_rows))
- d["items"] = items
- return d
- def create_checkup_institution_package(
- db,
- *,
- checkup_institution_id,
- package_name,
- package_code,
- status="1",
- items,
- tenant_no="zkxw",
- order_num=None,
- remark=None,
- user_id=None,
- ):
- code = (package_code or "").strip()
- if not code:
- raise ValueError("套餐编码不能为空")
- if checkup_package_code_exists(db, checkup_institution_id, code, tenant_no):
- raise ValueError(f"套餐编码 {code} 已存在")
- _validate_package_items_for_save(db, checkup_institution_id, items, tenant_no)
- pkg = BizCheckupInstitutionPackage(
- id=generate_uuid(),
- checkup_institution_id=checkup_institution_id,
- package_name=package_name,
- package_code=code,
- status=status or "1",
- tenant_no=tenant_no,
- order_num=order_num or 0,
- remark=remark or "",
- create_by=user_id,
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(pkg)
- db.flush()
- _insert_package_items(
- db,
- package_id=pkg.id,
- institution_id=checkup_institution_id,
- items=items,
- tenant_no=tenant_no,
- user_id=user_id,
- )
- db.commit()
- db.refresh(pkg)
- return get_checkup_institution_package_detail(db, pkg.id, tenant_no)
- def update_checkup_institution_package(
- db,
- package_id,
- *,
- package_name=None,
- package_code=None,
- status=None,
- items=None,
- order_num=None,
- remark=None,
- user_id=None,
- tenant_no=None,
- ):
- pkg = get_checkup_institution_package(db, package_id, tenant_no)
- if not pkg:
- raise ValueError("记录不存在")
- institution_id = pkg.checkup_institution_id
- if package_code is not None:
- code = package_code.strip()
- if not code:
- raise ValueError("套餐编码不能为空")
- if code != pkg.package_code and checkup_package_code_exists(
- db, institution_id, code, tenant_no, exclude_id=package_id
- ):
- raise ValueError(f"套餐编码 {code} 已存在")
- pkg.package_code = code
- if package_name is not None:
- pkg.package_name = package_name
- if status is not None:
- pkg.status = status
- if order_num is not None:
- pkg.order_num = order_num
- if remark is not None:
- pkg.remark = remark
- if items is not None:
- old_rows = _list_package_items_raw(db, package_id, tenant_no)
- existing_pids = {r.checkup_institution_project_id for r in old_rows}
- _validate_package_items_for_save(
- db,
- institution_id,
- items,
- tenant_no,
- existing_item_project_ids=existing_pids,
- )
- # 全量替换:物理删除旧明细,避免 uk(package_id, project_id) 与软删行冲突
- q_items = db.query(BizCheckupInstitutionPackageItem).filter(
- BizCheckupInstitutionPackageItem.package_id == package_id
- )
- if tenant_no:
- q_items = q_items.filter(BizCheckupInstitutionPackageItem.tenant_no == tenant_no)
- for row in q_items.all():
- db.delete(row)
- db.flush()
- _insert_package_items(
- db,
- package_id=package_id,
- institution_id=institution_id,
- items=items,
- tenant_no=tenant_no or pkg.tenant_no,
- user_id=user_id,
- )
- pkg.update_by = user_id
- pkg.update_time = datetime.now()
- db.add(pkg)
- db.commit()
- return get_checkup_institution_package_detail(db, package_id, tenant_no)
- def soft_delete_checkup_institution_package(db, package_id, tenant_no=None):
- pkg = get_checkup_institution_package(db, package_id, tenant_no)
- if not pkg:
- raise ValueError("记录不存在")
- now = datetime.now()
- pkg.del_flag = 1
- pkg.update_time = now
- db.add(pkg)
- for row in _list_package_items_raw(db, package_id, tenant_no):
- row.del_flag = 1
- row.update_time = now
- db.add(row)
- db.commit()
- # ────────────────────────────────────────────
- # Shared utility
- # ────────────────────────────────────────────
- def total_pages(total, page_size):
- if total <= 0:
- return 0
- return int(math.ceil(total / float(page_size)))
- # ────────────────────────────────────────────
- # Entity 15: BizChannelAccount 渠道账户
- # ────────────────────────────────────────────
- def _row_to_dict_channel_account(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "channelId": row.channel_id or "",
- "balanceAmount": float(row.balance_amount) if row.balance_amount else 0,
- "tenantNo": row.tenant_no or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def get_channel_account(db, channel_id, tenant_no=None):
- """只读查询渠道账户,不加行锁"""
- q = db.query(BizChannelAccount).filter(
- and_(BizChannelAccount.channel_id == channel_id, BizChannelAccount.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizChannelAccount.tenant_no == tenant_no)
- return q.first()
- def _get_or_create_channel_account(db, channel_id, tenant_no, user_id):
- """获取或创建渠道账户(带行锁 with_for_update)"""
- q = db.query(BizChannelAccount).filter(
- and_(BizChannelAccount.channel_id == channel_id, BizChannelAccount.del_flag == 0)
- ).with_for_update()
- if tenant_no:
- q = q.filter(BizChannelAccount.tenant_no == tenant_no)
- obj = q.first()
- if obj:
- return obj
- obj = BizChannelAccount(
- id=generate_uuid(),
- channel_id=channel_id,
- balance_amount=0,
- tenant_no=tenant_no or "zkxw",
- order_num=0,
- remark="",
- create_by=user_id,
- create_dept_id="",
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.flush()
- return obj
- # ────────────────────────────────────────────
- # Entity 16: BizChannelProductQuota 渠道产品份数
- # ────────────────────────────────────────────
- def _row_to_dict_channel_product_quota(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "channelId": row.channel_id or "",
- "projectCode": row.project_code or "",
- "projectName": row.project_name or "",
- "cellTypeCode": row.cell_type_code or "",
- "cellTypeName": row.cell_type_name or "",
- "specText": row.spec_text or "",
- "totalQty": float(row.total_qty) if row.total_qty else 0,
- "usedQty": float(row.used_qty) if row.used_qty else 0,
- "remainingQty": float(row.remaining_qty) if row.remaining_qty else 0,
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- def _upsert_channel_product_quota(
- db,
- channel_id,
- project_code,
- project_name,
- cell_type_code,
- cell_type_name,
- spec_text,
- add_qty,
- tenant_no,
- user_id,
- ):
- """增加份数:已存在则累加,不存在则新建"""
- q = db.query(BizChannelProductQuota).filter(
- and_(
- BizChannelProductQuota.channel_id == channel_id,
- BizChannelProductQuota.project_code == (project_code or ""),
- BizChannelProductQuota.cell_type_code == (cell_type_code or ""),
- BizChannelProductQuota.spec_text == (spec_text or ""),
- BizChannelProductQuota.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizChannelProductQuota.tenant_no == tenant_no)
- obj = q.first()
- if obj:
- obj.total_qty = (float(obj.total_qty) if obj.total_qty else 0) + add_qty
- obj.remaining_qty = (float(obj.remaining_qty) if obj.remaining_qty else 0) + add_qty
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.flush()
- return obj
- obj = BizChannelProductQuota(
- id=generate_uuid(),
- channel_id=channel_id,
- project_code=project_code or "",
- project_name=project_name or "",
- cell_type_code=cell_type_code or "",
- cell_type_name=cell_type_name or "",
- spec_text=spec_text or "",
- total_qty=add_qty,
- used_qty=0,
- remaining_qty=add_qty,
- tenant_no=tenant_no or "zkxw",
- order_num=0,
- remark="",
- create_by=user_id,
- create_dept_id="",
- create_time=datetime.now(),
- update_by=user_id,
- update_time=datetime.now(),
- del_flag=0,
- )
- db.add(obj)
- db.flush()
- return obj
- def _reduce_channel_product_quota(db, channel_id, project_code, cell_type_code, spec_text, reduce_qty, user_id):
- """扣减份数:验证 remaining_qty >= reduce_qty"""
- q = db.query(BizChannelProductQuota).filter(
- and_(
- BizChannelProductQuota.channel_id == channel_id,
- BizChannelProductQuota.project_code == (project_code or ""),
- BizChannelProductQuota.cell_type_code == (cell_type_code or ""),
- BizChannelProductQuota.spec_text == (spec_text or ""),
- BizChannelProductQuota.del_flag == 0,
- )
- )
- obj = q.first()
- if not obj:
- raise ValueError("份数记录不存在")
- remaining = float(obj.remaining_qty) if obj.remaining_qty else 0
- if remaining < reduce_qty:
- raise ValueError(f"剩余份数不足,当前剩余 {remaining},需扣减 {reduce_qty}")
- obj.total_qty = (float(obj.total_qty) if obj.total_qty else 0) - reduce_qty
- obj.remaining_qty = remaining - reduce_qty
- obj.update_by = user_id
- obj.update_time = datetime.now()
- db.add(obj)
- db.flush()
- return obj
- def list_channel_product_quotas(db, channel_id, tenant_no=None):
- """查询渠道下所有产品份数"""
- q = db.query(BizChannelProductQuota).filter(
- and_(
- BizChannelProductQuota.channel_id == channel_id,
- BizChannelProductQuota.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizChannelProductQuota.tenant_no == tenant_no)
- rows = q.order_by(BizChannelProductQuota.create_time.desc()).all()
- return [_row_to_dict_channel_product_quota(x) for x in rows]
- # ────────────────────────────────────────────
- # Entity 17: BizChannelServiceOrder 渠道服务订单
- # ────────────────────────────────────────────
- def _calc_gmcp_purchase_total_from_db(db, order_id, tenant_no=None) -> float:
- """从订单产品持仓汇总购买总额(协议单价 × 累计购买份数)。"""
- q = db.query(BizChannelServiceOrderProduct).filter(
- and_(
- BizChannelServiceOrderProduct.service_order_id == order_id,
- BizChannelServiceOrderProduct.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
- total = 0.0
- for prod in q.all():
- price = float(prod.agreement_unit_price or 0)
- qty = float(prod.total_buy_qty or 0)
- total += price * qty
- return total
- def _batch_gmcp_purchase_totals(db, order_ids, tenant_no=None) -> Dict[str, float]:
- if not order_ids:
- return {}
- amount_expr = BizChannelServiceOrderProduct.agreement_unit_price * BizChannelServiceOrderProduct.total_buy_qty
- q = db.query(
- BizChannelServiceOrderProduct.service_order_id,
- func.coalesce(func.sum(amount_expr), 0),
- ).filter(
- BizChannelServiceOrderProduct.del_flag == 0,
- BizChannelServiceOrderProduct.service_order_id.in_(order_ids),
- )
- if tenant_no:
- q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
- return {row[0]: float(row[1] or 0) for row in q.group_by(BizChannelServiceOrderProduct.service_order_id).all()}
- def _calc_gmcp_current_holding_amount_from_db(db, order_id, tenant_no=None) -> float:
- """从订单产品持仓汇总当前余额(协议单价 × 当前持有份数)。"""
- q = db.query(BizChannelServiceOrderProduct).filter(
- and_(
- BizChannelServiceOrderProduct.service_order_id == order_id,
- BizChannelServiceOrderProduct.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
- total = 0.0
- for prod in q.all():
- price = float(prod.agreement_unit_price or 0)
- qty = float(prod.current_qty or 0)
- total += price * qty
- return total
- def _batch_gmcp_current_holding_amounts(db, order_ids, tenant_no=None) -> Dict[str, float]:
- if not order_ids:
- return {}
- amount_expr = BizChannelServiceOrderProduct.agreement_unit_price * BizChannelServiceOrderProduct.current_qty
- q = db.query(
- BizChannelServiceOrderProduct.service_order_id,
- func.coalesce(func.sum(amount_expr), 0),
- ).filter(
- BizChannelServiceOrderProduct.del_flag == 0,
- BizChannelServiceOrderProduct.service_order_id.in_(order_ids),
- )
- if tenant_no:
- q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
- return {row[0]: float(row[1] or 0) for row in q.group_by(BizChannelServiceOrderProduct.service_order_id).all()}
- def _resolve_service_order_purchase_total(row, db=None, tenant_no=None, purchase_total_override=None) -> float:
- stored = float(row.purchase_total_amount) if row.purchase_total_amount else 0
- if (row.order_type or "") != "gmcp":
- return stored
- if purchase_total_override is not None:
- return float(purchase_total_override)
- if db:
- computed = _calc_gmcp_purchase_total_from_db(db, row.id, tenant_no or row.tenant_no)
- if computed > 0 or not row.purchase_total_amount:
- return computed
- return stored
- def _resolve_service_order_current_balance(
- row, db=None, tenant_no=None, current_balance_override=None
- ) -> float:
- if (row.order_type or "") == "gmcp":
- if current_balance_override is not None:
- return float(current_balance_override)
- if db:
- return _calc_gmcp_current_holding_amount_from_db(db, row.id, tenant_no or row.tenant_no)
- return 0.0
- return float(row.current_balance) if row.current_balance else 0
- def _row_to_dict_service_order(
- row,
- db=None,
- tenant_no=None,
- *,
- purchase_total_override=None,
- current_balance_override=None,
- ) -> Dict[str, Any]:
- tn = tenant_no or row.tenant_no
- purchase_total = _resolve_service_order_purchase_total(
- row, db, tn, purchase_total_override=purchase_total_override
- )
- current_balance = _resolve_service_order_current_balance(
- row, db, tn, current_balance_override=current_balance_override
- )
- d = {
- "id": row.id,
- "channelId": row.channel_id or "",
- "channelName": row.channel_name or "",
- "orderName": row.order_name or "",
- "deptName": row.dept_name or "",
- "orderType": row.order_type or "",
- "prepayAmount": float(row.prepay_amount) if row.prepay_amount else 0,
- "purchaseTotalAmount": purchase_total,
- "orderStatus": row.order_status or "",
- "channelAgreementId": row.channel_agreement_id or "",
- "totalRechargeAmount": float(row.total_recharge_amount) if row.total_recharge_amount else 0,
- "totalRefundAmount": float(row.total_refund_amount) if row.total_refund_amount else 0,
- "currentBalance": current_balance,
- "balanceBefore": float(row.balance_before) if row.balance_before else 0,
- "balanceAfter": float(row.balance_after) if row.balance_after else 0,
- "financeSubmitBy": row.finance_submit_by or "",
- "financeSubmitTime": row.finance_submit_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_submit_time else "",
- "financeSubmitRemark": row.finance_submit_remark or "",
- "financeSubmitAttachments": row.finance_submit_attachments or "",
- "financeSubmitAttachmentFiles": urls_to_attachment_list(row.finance_submit_attachments, db, tn) if db and row.finance_submit_attachments else [],
- "financeConfirmTime": row.finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_confirm_time else "",
- "financeConfirmAttachments": row.finance_confirm_attachments or "",
- "financeConfirmAttachmentFiles": urls_to_attachment_list(row.finance_confirm_attachments, db, tn) if db and row.finance_confirm_attachments else [],
- "financeRemark": row.finance_remark or "",
- "refundApplyBy": row.refund_apply_by or "",
- "refundApplyTime": row.refund_apply_time.strftime("%Y-%m-%d %H:%M:%S") if row.refund_apply_time else "",
- "refundAmount": float(row.refund_amount) if row.refund_amount else 0,
- "refundRemark": row.refund_remark or "",
- "refundAttachments": row.refund_attachments or "",
- "refundAttachmentFiles": urls_to_attachment_list(row.refund_attachments, db, tn) if db and row.refund_attachments else [],
- "refundFinanceConfirmTime": row.refund_finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.refund_finance_confirm_time else "",
- "refundFinanceConfirmAttachments": row.refund_finance_confirm_attachments or "",
- "refundFinanceConfirmAttachmentFiles": urls_to_attachment_list(row.refund_finance_confirm_attachments, db, tn) if db and row.refund_finance_confirm_attachments else [],
- "refundFinanceRemark": row.refund_finance_remark or "",
- "tenantNo": row.tenant_no or "",
- "remark": row.remark or "",
- "createBy": row.create_by or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- return d
- def _row_to_dict_service_order_product(row) -> Dict[str, Any]:
- return {
- "id": row.id,
- "serviceOrderId": row.service_order_id or "",
- "projectCode": row.project_code or "",
- "projectName": row.project_name or "",
- "cellType": row.cell_type or "",
- "specText": row.spec_text or "",
- "agreementUnitPrice": float(row.agreement_unit_price) if row.agreement_unit_price else 0,
- "buyQty": float(row.buy_qty) if row.buy_qty else 0,
- "buyAmount": float(row.buy_amount) if row.buy_amount else 0,
- "totalBuyQty": float(row.total_buy_qty) if row.total_buy_qty else 0,
- "totalUnsubscribeQty": float(row.total_unsubscribe_qty) if row.total_unsubscribe_qty else 0,
- "currentQty": float(row.current_qty) if row.current_qty else 0,
- "totalUnsubscribeAmount": float(row.total_unsubscribe_amount) if row.total_unsubscribe_amount else 0,
- }
- def _normalize_txn_product_item(p: Dict[str, Any]) -> Dict[str, Any]:
- """流水 products_json 项统一为 camelCase(入库时为 snake_case)。"""
- qty = p.get("qty")
- if qty is None:
- qty = p.get("buyQty") or p.get("buy_qty")
- unit_price = p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0
- qty_val = float(qty or 0)
- amount = p.get("amount")
- if amount is None and qty_val:
- amount = float(unit_price or 0) * qty_val
- return {
- "projectCode": p.get("projectCode") or p.get("project_code") or "",
- "projectName": p.get("projectName") or p.get("project_name") or "",
- "cellType": p.get("cellType") or p.get("cell_type") or "",
- "specText": p.get("specText") or p.get("spec_text") or "",
- "agreementUnitPrice": float(unit_price or 0),
- "qty": qty_val,
- "amount": float(amount or 0),
- }
- def _enrich_txn_products_from_order(
- db,
- service_order_id: str,
- products: List[Dict[str, Any]],
- tenant_no=None,
- ) -> List[Dict[str, Any]]:
- """退订等流水可能仅存 projectCode,从订单持仓补全项目名与规格。"""
- if not db or not service_order_id or not products:
- return products
- holdings = get_service_order_products(db, service_order_id, tenant_no)
- if not holdings:
- return products
- by_code_spec = {
- (h.get("projectCode") or "", h.get("specText") or ""): h for h in holdings
- }
- by_code: Dict[str, List[Dict[str, Any]]] = {}
- for h in holdings:
- code = h.get("projectCode") or ""
- by_code.setdefault(code, []).append(h)
- enriched: List[Dict[str, Any]] = []
- for p in products:
- item = dict(p)
- code = item.get("projectCode") or ""
- spec = item.get("specText") or ""
- holding = by_code_spec.get((code, spec))
- if not holding and code:
- matches = by_code.get(code, [])
- if len(matches) == 1:
- holding = matches[0]
- if holding:
- if not (item.get("projectName") or "").strip():
- item["projectName"] = holding.get("projectName") or ""
- if not (item.get("specText") or "").strip():
- item["specText"] = holding.get("specText") or ""
- if not (item.get("cellType") or "").strip():
- item["cellType"] = holding.get("cellType") or ""
- enriched.append(item)
- return enriched
- def _row_to_dict_service_order_txn(row, db=None, tenant_no=None) -> Dict[str, Any]:
- tn = tenant_no or row.tenant_no
- d = {
- "id": row.id,
- "serviceOrderId": row.service_order_id or "",
- "txnType": row.txn_type or "",
- "amount": float(row.amount) if row.amount else 0,
- "txnStatus": row.txn_status or "",
- "productsJson": row.products_json or "",
- "remark": row.remark or "",
- "refundAttachments": row.refund_attachments or "",
- "refundAttachmentFiles": urls_to_attachment_list(row.refund_attachments, db, tn) if db and row.refund_attachments else [],
- "financeSubmitBy": row.finance_submit_by or "",
- "financeSubmitTime": row.finance_submit_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_submit_time else "",
- "financeSubmitRemark": row.finance_submit_remark or "",
- "financeSubmitAttachments": row.finance_submit_attachments or "",
- "financeSubmitAttachmentFiles": urls_to_attachment_list(row.finance_submit_attachments, db, tn) if db and row.finance_submit_attachments else [],
- "financeConfirmTime": row.finance_confirm_time.strftime("%Y-%m-%d %H:%M:%S") if row.finance_confirm_time else "",
- "financeConfirmBy": row.finance_confirm_by or "",
- "financeConfirmAttachments": row.finance_confirm_attachments or "",
- "financeConfirmAttachmentFiles": urls_to_attachment_list(row.finance_confirm_attachments, db, tn) if db and row.finance_confirm_attachments else [],
- "financeRemark": row.finance_remark or "",
- "voidBy": row.void_by or "",
- "voidTime": row.void_time.strftime("%Y-%m-%d %H:%M:%S") if row.void_time else "",
- "balanceBefore": float(row.balance_before) if row.balance_before else 0,
- "balanceAfter": float(row.balance_after) if row.balance_after else 0,
- "tenantNo": row.tenant_no or "",
- "createBy": row.create_by or "",
- "createTime": row.create_time.strftime("%Y-%m-%d %H:%M:%S") if row.create_time else "",
- "updateTime": row.update_time.strftime("%Y-%m-%d %H:%M:%S") if row.update_time else "",
- }
- if row.products_json:
- try:
- raw_products = json.loads(row.products_json)
- products = [_normalize_txn_product_item(x) for x in raw_products]
- if db and row.service_order_id:
- products = _enrich_txn_products_from_order(
- db, row.service_order_id, products, tenant_no
- )
- d["products"] = products
- except Exception:
- d["products"] = []
- else:
- d["products"] = []
- return d
- def _service_order_txn_to_response_dict(
- db,
- txn,
- tenant_no=None,
- *,
- order=None,
- name_map=None,
- ) -> Dict[str, Any]:
- """流水响应字典:含订单渠道信息与提交人/创建人昵称。"""
- d = _row_to_dict_service_order_txn(txn, db, tenant_no)
- if order is None and txn.service_order_id:
- order = get_service_order(db, txn.service_order_id, tenant_no)
- if order:
- d["channelName"] = order.channel_name or ""
- d["orderName"] = order.order_name or ""
- d["deptName"] = order.dept_name or ""
- d["orderType"] = order.order_type or ""
- confirm_uid = txn.finance_confirm_by or (
- txn.update_by
- if (txn.txn_status or "") == "yqr" and txn.finance_confirm_time and not txn.finance_confirm_by
- else None
- )
- void_uid = txn.void_by or (
- txn.update_by
- if (txn.txn_status or "") == "yzf"
- and txn.finance_submit_time
- and not txn.void_by
- else None
- )
- if name_map is None:
- user_ids = [x for x in (txn.create_by, txn.finance_submit_by, confirm_uid, void_uid) if x]
- name_map = _user_nick_name_map(db, user_ids) if db and user_ids else {}
- d["createByName"] = name_map.get(txn.create_by or "", "") or (txn.create_by or "")
- d["financeSubmitByName"] = name_map.get(txn.finance_submit_by or "", "") or (
- txn.finance_submit_by or ""
- )
- d["financeConfirmByName"] = name_map.get(confirm_uid or "", "") or (confirm_uid or "")
- if not d.get("voidTime") and (txn.txn_status or "") == "yzf" and txn.update_time:
- d["voidTime"] = txn.update_time.strftime("%Y-%m-%d %H:%M:%S")
- d["voidByName"] = name_map.get(void_uid or "", "") or (void_uid or "")
- if not (d.get("financeSubmitRemark") or "").strip() and (d.get("remark") or "").strip():
- d["financeSubmitRemark"] = (d.get("remark") or "").strip()
- _enrich_finance_operate_fields(d, txn)
- return d
- def _enrich_finance_operate_fields(d: Dict[str, Any], txn) -> None:
- """财务页「已处理」展示:操作类型、操作人、操作时间。"""
- status = txn.txn_status or ""
- if status == "yqr" and txn.finance_confirm_time:
- d["financeOperateType"] = "confirm"
- d["financeOperateTypeLabel"] = "财务确认"
- d["financeOperateByName"] = d.get("financeConfirmByName") or d.get("financeConfirmBy") or ""
- d["financeOperateTime"] = d.get("financeConfirmTime") or ""
- elif status == "yzf" and txn.finance_submit_time:
- d["financeOperateType"] = "void"
- d["financeOperateTypeLabel"] = "财务作废"
- d["financeOperateByName"] = d.get("voidByName") or d.get("voidBy") or ""
- d["financeOperateTime"] = d.get("voidTime") or ""
- else:
- d["financeOperateType"] = ""
- d["financeOperateTypeLabel"] = ""
- d["financeOperateByName"] = ""
- d["financeOperateTime"] = ""
- def list_channel_service_orders(
- db,
- *,
- page=1,
- page_size=10,
- order_type=None,
- order_status=None,
- channel_id=None,
- keyword=None,
- create_time_start=None,
- create_time_end=None,
- tenant_no=None,
- user_info=None,
- ):
- def _filtered():
- q0 = db.query(BizChannelServiceOrder).filter(BizChannelServiceOrder.del_flag == 0)
- if tenant_no:
- q0 = q0.filter(BizChannelServiceOrder.tenant_no == tenant_no)
- ot = (order_type or "").strip()
- if ot:
- q0 = q0.filter(BizChannelServiceOrder.order_type == ot)
- # Support comma-separated order_status
- os_raw = (order_status or "").strip()
- if os_raw:
- statuses = [s.strip() for s in os_raw.split(",") if s.strip()]
- if statuses:
- q0 = q0.filter(BizChannelServiceOrder.order_status.in_(statuses))
- if channel_id:
- q0 = q0.filter(BizChannelServiceOrder.channel_id == channel_id)
- kw = (keyword or "").strip()
- if kw:
- like = f"%{kw}%"
- q0 = q0.filter(
- or_(
- BizChannelServiceOrder.channel_name.like(like),
- BizChannelServiceOrder.order_name.like(like),
- BizChannelServiceOrder.dept_name.like(like),
- )
- )
- if create_time_start and create_time_start.strip():
- q0 = q0.filter(
- BizChannelServiceOrder.create_time
- >= _normalize_date_bound(create_time_start.strip())
- )
- if create_time_end and create_time_end.strip():
- q0 = q0.filter(
- BizChannelServiceOrder.create_time
- <= _normalize_date_bound(create_time_end.strip(), end_of_day=True)
- )
- if user_info:
- from app.core.data_scope import apply_data_scope
- q0 = apply_data_scope(q0, BizChannelServiceOrder, user_info, db, tenant_no)
- return q0
- total = _filtered().order_by(None).with_entities(func.count(BizChannelServiceOrder.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- rows = (
- _filtered()
- .order_by(BizChannelServiceOrder.create_time.desc())
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- user_ids: List[str] = []
- for x in rows:
- if x.create_by:
- user_ids.append(x.create_by)
- if x.finance_submit_by:
- user_ids.append(x.finance_submit_by)
- name_map = _user_nick_name_map(db, user_ids) if db else {}
- gmcp_ids = [x.id for x in rows if (x.order_type or "") == "gmcp"]
- purchase_total_map = _batch_gmcp_purchase_totals(db, gmcp_ids, tenant_no) if db and gmcp_ids else {}
- holding_amount_map = (
- _batch_gmcp_current_holding_amounts(db, gmcp_ids, tenant_no) if db and gmcp_ids else {}
- )
- return [
- {
- **_row_to_dict_service_order(
- x,
- db,
- tenant_no,
- purchase_total_override=purchase_total_map.get(x.id),
- current_balance_override=holding_amount_map.get(x.id),
- ),
- "createByName": name_map.get(x.create_by or "", "") or (x.create_by or ""),
- "financeSubmitByName": name_map.get(x.finance_submit_by or "", "") or (x.finance_submit_by or ""),
- }
- for x in rows
- ], int(total)
- def get_service_order(db, order_id, tenant_no=None):
- q = db.query(BizChannelServiceOrder).filter(
- and_(BizChannelServiceOrder.id == order_id, BizChannelServiceOrder.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizChannelServiceOrder.tenant_no == tenant_no)
- return q.first()
- def get_service_order_products(db, service_order_id, tenant_no=None):
- q = db.query(BizChannelServiceOrderProduct).filter(
- and_(
- BizChannelServiceOrderProduct.service_order_id == service_order_id,
- BizChannelServiceOrderProduct.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
- rows = q.order_by(BizChannelServiceOrderProduct.create_time.asc()).all()
- return [_row_to_dict_service_order_product(x) for x in rows]
- # ────────────────────────────────────────────
- # Service Order: Create / Update / Delete
- # ────────────────────────────────────────────
- def _calc_gmcp_purchase_total(products):
- total = 0.0
- for p in products:
- price = float(p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0)
- qty = float(p.get("buyQty") or p.get("buy_qty") or 0)
- total += price * qty
- return total
- def _gmcp_has_purchase_lock(db, order_id, tenant_no=None) -> bool:
- """是否存在购买记录(未作废 purchase 流水,或历史建单写入的产品行)。"""
- q = db.query(BizChannelServiceOrderTxn).filter(
- and_(
- BizChannelServiceOrderTxn.service_order_id == order_id,
- BizChannelServiceOrderTxn.txn_type == "purchase",
- BizChannelServiceOrderTxn.txn_status != "yzf",
- BizChannelServiceOrderTxn.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizChannelServiceOrderTxn.tenant_no == tenant_no)
- if q.first():
- return True
- pq = db.query(BizChannelServiceOrderProduct).filter(
- and_(
- BizChannelServiceOrderProduct.service_order_id == order_id,
- BizChannelServiceOrderProduct.del_flag == 0,
- )
- )
- if tenant_no:
- pq = pq.filter(BizChannelServiceOrderProduct.tenant_no == tenant_no)
- for row in pq.all():
- if float(row.buy_qty or 0) > 0 or float(row.total_buy_qty or 0) > 0:
- return True
- return False
- def gmcp_agreement_editable(db, order, tenant_no=None) -> bool:
- if order.order_type != "gmcp" or order.order_status == "yzf":
- return False
- return not _gmcp_has_purchase_lock(db, order.id, tenant_no or order.tenant_no)
- def _resolve_agreement_price_cell_type(db, channel_agreement_id, project_code, spec_text, cell_type_fallback=""):
- price_row = (
- db.query(BizChannelAgreementProductPrice)
- .filter(
- and_(
- BizChannelAgreementProductPrice.channel_agreement_id == channel_agreement_id,
- BizChannelAgreementProductPrice.project_code == (project_code or ""),
- BizChannelAgreementProductPrice.spec_text == (spec_text or ""),
- BizChannelAgreementProductPrice.del_flag == 0,
- )
- )
- ).first()
- if price_row:
- return price_row.cell_type_code or "", price_row.cell_type_name or cell_type_fallback
- return "", cell_type_fallback
- def _save_gmcp_product_rows(
- db,
- order_id,
- products,
- tenant_no,
- user_id,
- now=None,
- ):
- """仅保存购买订单产品明细,不调整份数库存"""
- now = now or datetime.now()
- for p in products:
- project_code = p.get("projectCode") or p.get("project_code") or ""
- project_name = p.get("projectName") or p.get("project_name") or ""
- cell_type = p.get("cellType") or p.get("cell_type") or ""
- spec_text = p.get("specText") or p.get("spec_text") or ""
- agreement_unit_price = float(p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0)
- buy_qty = float(p.get("buyQty") or p.get("buy_qty") or 0)
- buy_amount = float(p.get("buyAmount") or p.get("buy_amount") or agreement_unit_price * buy_qty)
- prod_row = BizChannelServiceOrderProduct(
- id=generate_uuid(),
- service_order_id=order_id,
- project_code=project_code,
- project_name=project_name,
- cell_type=cell_type,
- spec_text=spec_text,
- agreement_unit_price=agreement_unit_price,
- buy_qty=buy_qty,
- buy_amount=buy_amount,
- tenant_no=tenant_no or "zkxw",
- order_num=0,
- remark="",
- create_by=user_id,
- create_dept_id="",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(prod_row)
- db.flush()
- def _replace_gmcp_product_rows(db, order_id, products, tenant_no, user_id):
- now = datetime.now()
- db.query(BizChannelServiceOrderProduct).filter(
- and_(
- BizChannelServiceOrderProduct.service_order_id == order_id,
- BizChannelServiceOrderProduct.del_flag == 0,
- )
- ).update({"del_flag": 1, "update_time": now}, synchronize_session=False)
- _save_gmcp_product_rows(db, order_id, products, tenant_no, user_id, now=now)
- def _apply_gmcp_order_quotas(db, order, tenant_no, user_id):
- """财务确认收款后,按订单明细增加渠道产品份数"""
- products = get_service_order_products(db, order.id, tenant_no)
- for prod in products:
- project_code = prod.get("projectCode") or ""
- spec_text = prod.get("specText") or ""
- cell_type_code, cell_type_name = _resolve_agreement_price_cell_type(
- db,
- order.channel_agreement_id,
- project_code,
- spec_text,
- prod.get("cellType") or "",
- )
- _upsert_channel_product_quota(
- db,
- channel_id=order.channel_id,
- project_code=project_code,
- project_name=prod.get("projectName") or "",
- cell_type_code=cell_type_code,
- cell_type_name=cell_type_name,
- spec_text=spec_text,
- add_qty=float(prod.get("buyQty") or 0),
- tenant_no=tenant_no or order.tenant_no,
- user_id=user_id,
- )
- def _get_valid_channel_agreement(db, channel_agreement_id, channel_id, tenant_no=None):
- """校验渠道协议存在、属于当前渠道且已生效"""
- aid = (channel_agreement_id or "").strip()
- if not aid:
- raise ValueError("请选择渠道协议")
- q = db.query(BizChannelAgreement).filter(
- and_(
- BizChannelAgreement.id == aid,
- BizChannelAgreement.channel_id == channel_id,
- BizChannelAgreement.del_flag == 0,
- )
- )
- if tenant_no:
- q = q.filter(BizChannelAgreement.tenant_no == tenant_no)
- agreement = q.first()
- if not agreement:
- raise ValueError("渠道协议不存在或不属于当前渠道")
- if (agreement.agreement_status or "").strip() != "sxz":
- raise ValueError("协议未生效,无法使用")
- return agreement
- def create_channel_service_order(
- db,
- *,
- channel_id,
- order_type,
- channel_name=None,
- order_name=None,
- dept_name=None,
- prepay_amount=None,
- channel_agreement_id=None,
- products=None,
- tenant_no=None,
- remark=None,
- user_id=None,
- ):
- # Validate channel exists
- channel = get_channel(db, channel_id, tenant_no)
- if not channel:
- raise ValueError("渠道不存在")
- now = datetime.now()
- balance_before = None
- balance_after = None
- purchase_total_amount = None
- order_status = "dsx"
- if order_type == "yfk":
- # 预存款订单:创建后为待生效,充值金额通过「发起充值」流水录入
- _get_valid_channel_agreement(db, channel_agreement_id, channel_id, tenant_no)
- order_status = "dsx"
- prepay_amount = None
- elif order_type == "gmcp":
- # 购买型:建单仅绑定渠道与协议;产品通过 purchase 流水录入并经财务确认
- if not channel_agreement_id:
- raise ValueError("请选择渠道协议")
- _get_valid_channel_agreement(db, channel_agreement_id, channel_id, tenant_no)
- purchase_total_amount = _calc_gmcp_purchase_total(products) if products else None
- order_status = "dsx"
- else:
- raise ValueError(f"不支持的订单类型: {order_type}")
- order_id = generate_uuid()
- order = BizChannelServiceOrder(
- id=order_id,
- channel_id=channel_id,
- channel_name=channel_name or channel.channel_name or "",
- order_name=(order_name or "").strip() or None,
- dept_name=dept_name or "",
- order_type=order_type,
- prepay_amount=None,
- purchase_total_amount=purchase_total_amount,
- order_status=order_status,
- channel_agreement_id=channel_agreement_id,
- balance_before=balance_before,
- balance_after=balance_after,
- tenant_no=tenant_no or "zkxw",
- order_num=0,
- remark=remark or "",
- create_by=user_id,
- create_dept_id="",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(order)
- db.flush()
- db.commit()
- db.refresh(order)
- return order
- def update_channel_service_order(db, order_id, *, tenant_no=None, user_id=None, products=None, **kwargs):
- """更新订单基本信息;gmcp 产品仅通过 purchase 流水维护"""
- order = get_service_order(db, order_id, tenant_no)
- if not order:
- raise ValueError("订单不存在")
- if order.order_status == "yzf":
- raise ValueError("已作废订单不可编辑")
- if kwargs.get("channel_id"):
- raise ValueError("渠道不可修改")
- if products is not None and order.order_type == "gmcp":
- raise ValueError("请通过「购买产品」操作录入产品明细")
- if order.order_type in ("yfk", "gmcp"):
- if order.order_status not in ("dsx", "sxz"):
- raise ValueError("当前状态不允许编辑")
- else:
- raise ValueError("不支持的订单类型")
- if "dept_name" in kwargs and kwargs["dept_name"] is not None:
- order.dept_name = kwargs["dept_name"]
- if "order_name" in kwargs and kwargs["order_name"] is not None:
- order.order_name = (kwargs["order_name"] or "").strip() or None
- if "remark" in kwargs and kwargs["remark"] is not None:
- order.remark = kwargs["remark"]
- if order.order_type == "yfk":
- if "channel_agreement_id" in kwargs and kwargs["channel_agreement_id"] is not None:
- new_agreement_id = kwargs["channel_agreement_id"]
- if new_agreement_id != (order.channel_agreement_id or ""):
- if order.order_status != "dsx":
- raise ValueError("已有业务流水,不可修改渠道协议")
- _get_valid_channel_agreement(
- db, new_agreement_id, order.channel_id, tenant_no
- )
- order.channel_agreement_id = new_agreement_id
- elif order.order_type == "gmcp":
- if "channel_agreement_id" in kwargs and kwargs["channel_agreement_id"] is not None:
- new_agreement_id = kwargs["channel_agreement_id"]
- if new_agreement_id != (order.channel_agreement_id or ""):
- if _gmcp_has_purchase_lock(db, order_id, tenant_no):
- raise ValueError("已有购买记录,不可修改渠道协议")
- _get_valid_channel_agreement(
- db, new_agreement_id, order.channel_id, tenant_no
- )
- order.channel_agreement_id = new_agreement_id
- order.update_by = user_id
- order.update_time = datetime.now()
- db.add(order)
- db.commit()
- db.refresh(order)
- return order
- def soft_delete_service_order(db, order_id, tenant_no=None):
- """仅允许待生效状态下删除订单"""
- order = get_service_order(db, order_id, tenant_no)
- if not order:
- raise ValueError("订单不存在")
- if order.order_status != "dsx":
- raise ValueError("仅待生效订单可删除")
- order.del_flag = 1
- order.update_time = datetime.now()
- db.add(order)
- db.commit()
- # ────────────────────────────────────────────
- # Service Order: Status Transitions
- # ────────────────────────────────────────────
- def submit_finance_confirm(
- db,
- order_id,
- *,
- finance_submit_remark=None,
- finance_submit_attachments=None,
- tenant_no=None,
- user_id=None,
- ):
- """发起财务确认:type=yfk/gmcp + status=dsx → dqr"""
- order = get_service_order(db, order_id, tenant_no)
- if not order:
- raise ValueError("订单不存在")
- if order.order_type not in ("yfk", "gmcp"):
- raise ValueError("仅预存款或购买产品订单可发起财务确认")
- if order.order_status != "dsx":
- raise ValueError("仅待生效状态可发起财务确认")
- now = datetime.now()
- order.order_status = "dqr"
- order.finance_submit_remark = finance_submit_remark or ""
- order.finance_submit_attachments = finance_submit_attachments or ""
- order.finance_submit_time = now
- order.finance_submit_by = user_id
- order.update_by = user_id
- order.update_time = now
- db.add(order)
- db.commit()
- db.refresh(order)
- return order
- def confirm_receipt(db, order_id, *, finance_remark=None, finance_confirm_attachments=None, tenant_no=None, user_id=None):
- """确认到账:yfk dqr→yqr 增加余额;gmcp dqr→sxz 增加产品份数"""
- order = get_service_order(db, order_id, tenant_no)
- if not order:
- raise ValueError("订单不存在")
- if order.order_type not in ("yfk", "gmcp"):
- raise ValueError("仅预存款或购买产品订单可确认到账")
- if order.order_status != "dqr":
- raise ValueError("仅待确认状态可确认到账")
- now = datetime.now()
- if order.order_type == "yfk":
- account = _get_or_create_channel_account(db, order.channel_id, tenant_no or order.tenant_no, user_id)
- balance_before = float(account.balance_amount) if account.balance_amount else 0
- add_amount = float(order.prepay_amount) if order.prepay_amount else 0
- account.balance_amount = balance_before + add_amount
- balance_after = float(account.balance_amount)
- account.update_by = user_id
- account.update_time = now
- db.add(account)
- db.flush()
- order.order_status = "yqr"
- order.balance_before = balance_before
- order.balance_after = balance_after
- else:
- _apply_gmcp_order_quotas(db, order, tenant_no, user_id)
- order.order_status = "sxz"
- order.finance_confirm_time = now
- order.finance_confirm_attachments = finance_confirm_attachments or ""
- order.finance_remark = finance_remark or ""
- order.update_by = user_id
- order.update_time = now
- db.add(order)
- db.commit()
- db.refresh(order)
- return order
- def submit_refund(db, order_id, *, refund_amount=None, refund_remark=None, refund_attachments=None, tenant_no=None, user_id=None):
- """申请退款:type=yfk + status=yqr → tfqrz"""
- order = get_service_order(db, order_id, tenant_no)
- if not order:
- raise ValueError("订单不存在")
- if order.order_type != "yfk":
- raise ValueError("仅预存款订单可申请退款")
- if order.order_status != "yqr":
- raise ValueError("仅已确认状态可申请退款")
- now = datetime.now()
- # Default refund_amount to prepay_amount
- if refund_amount is None:
- refund_amount = float(order.prepay_amount) if order.prepay_amount else 0
- else:
- refund_amount = float(refund_amount)
- prepay = float(order.prepay_amount) if order.prepay_amount else 0
- if refund_amount > prepay:
- raise ValueError(f"退款金额不能大于预存金额 {prepay}")
- order.order_status = "tfqrz"
- order.refund_apply_by = user_id
- order.refund_apply_time = now
- order.refund_amount = refund_amount
- order.refund_remark = refund_remark or ""
- order.refund_attachments = refund_attachments or ""
- order.update_by = user_id
- order.update_time = now
- db.add(order)
- db.commit()
- db.refresh(order)
- return order
- def confirm_refund(db, order_id, *, refund_finance_remark=None, refund_finance_confirm_attachments=None, tenant_no=None, user_id=None):
- """确认退款:status=tfqrz → ytf,扣减余额"""
- order = get_service_order(db, order_id, tenant_no)
- if not order:
- raise ValueError("订单不存在")
- if order.order_status != "tfqrz":
- raise ValueError("仅退款待确认状态可确认退款")
- now = datetime.now()
- account = _get_or_create_channel_account(db, order.channel_id, tenant_no or order.tenant_no, user_id)
- balance_before = float(account.balance_amount) if account.balance_amount else 0
- deduct = float(order.refund_amount) if order.refund_amount else 0
- account.balance_amount = balance_before - deduct
- balance_after = float(account.balance_amount)
- account.update_by = user_id
- account.update_time = now
- db.add(account)
- db.flush()
- order.order_status = "ytf"
- order.refund_finance_confirm_time = now
- order.refund_finance_confirm_attachments = refund_finance_confirm_attachments or ""
- order.refund_finance_remark = refund_finance_remark or ""
- order.balance_before = balance_before
- order.balance_after = balance_after
- order.update_by = user_id
- order.update_time = now
- db.add(order)
- db.commit()
- db.refresh(order)
- return order
- def void_service_order(db, order_id, *, tenant_no=None, user_id=None):
- """作废订单:type=gmcp + status=sxz → yzf,扣减已生效的产品份数"""
- order = get_service_order(db, order_id, tenant_no)
- if not order:
- raise ValueError("订单不存在")
- if order.order_type != "gmcp":
- raise ValueError("仅购买产品订单可作废")
- if order.order_status != "sxz":
- raise ValueError("仅生效中状态可作废")
- now = datetime.now()
- # Reduce product quotas
- products = get_service_order_products(db, order_id, tenant_no)
- for prod in products:
- cell_type_code, _ = _resolve_agreement_price_cell_type(
- db,
- order.channel_agreement_id,
- prod.get("projectCode") or "",
- prod.get("specText") or "",
- prod.get("cellType") or "",
- )
- _reduce_channel_product_quota(
- db,
- channel_id=order.channel_id,
- project_code=prod.get("projectCode") or "",
- cell_type_code=cell_type_code,
- spec_text=prod.get("specText") or "",
- reduce_qty=prod.get("buyQty") or 0,
- user_id=user_id,
- )
- order.order_status = "yzf"
- order.update_by = user_id
- order.update_time = now
- db.add(order)
- db.commit()
- db.refresh(order)
- return order
- # ────────────────────────────────────────────
- # Service Order Txn: 操作流水 CRUD
- # ────────────────────────────────────────────
- def list_service_order_txns(
- db,
- *,
- service_order_id=None,
- channel_id=None,
- txn_status=None,
- txn_type=None,
- keyword=None,
- create_time_start=None,
- create_time_end=None,
- finance_done_only=False,
- page=1,
- page_size=20,
- tenant_no=None,
- ):
- """查询操作流水列表。finance_done_only:财务已处理(已确认或财务侧作废)。"""
- q = db.query(BizChannelServiceOrderTxn).filter(BizChannelServiceOrderTxn.del_flag == 0)
- if tenant_no:
- q = q.filter(BizChannelServiceOrderTxn.tenant_no == tenant_no)
- if service_order_id:
- q = q.filter(BizChannelServiceOrderTxn.service_order_id == service_order_id)
- cid = (channel_id or "").strip()
- kw = (keyword or "").strip()
- if cid:
- q = q.join(
- BizChannelServiceOrder,
- and_(
- BizChannelServiceOrder.id == BizChannelServiceOrderTxn.service_order_id,
- BizChannelServiceOrder.del_flag == 0,
- ),
- ).filter(BizChannelServiceOrder.channel_id == cid)
- if kw:
- like = f"%{kw}%"
- q = q.filter(
- or_(
- BizChannelServiceOrder.channel_name.like(like),
- BizChannelServiceOrder.order_name.like(like),
- BizChannelServiceOrder.dept_name.like(like),
- )
- )
- elif kw:
- like = f"%{kw}%"
- q = q.outerjoin(
- BizChannelServiceOrder,
- and_(
- BizChannelServiceOrder.id == BizChannelServiceOrderTxn.service_order_id,
- BizChannelServiceOrder.del_flag == 0,
- ),
- ).filter(
- or_(
- BizChannelServiceOrder.channel_name.like(like),
- BizChannelServiceOrder.order_name.like(like),
- BizChannelServiceOrder.dept_name.like(like),
- )
- )
- if finance_done_only:
- q = q.filter(
- or_(
- BizChannelServiceOrderTxn.txn_status == "yqr",
- and_(
- BizChannelServiceOrderTxn.txn_status == "yzf",
- BizChannelServiceOrderTxn.finance_submit_time.isnot(None),
- ),
- )
- )
- else:
- ss = (txn_status or "").strip()
- if ss:
- statuses = [s.strip() for s in ss.split(",") if s.strip()]
- if statuses:
- q = q.filter(BizChannelServiceOrderTxn.txn_status.in_(statuses))
- tt = (txn_type or "").strip()
- if tt:
- types = [t.strip() for t in tt.split(",") if t.strip()]
- if types:
- q = q.filter(BizChannelServiceOrderTxn.txn_type.in_(types))
- if create_time_start and create_time_start.strip():
- q = q.filter(
- BizChannelServiceOrderTxn.create_time
- >= _normalize_date_bound(create_time_start.strip())
- )
- if create_time_end and create_time_end.strip():
- q = q.filter(
- BizChannelServiceOrderTxn.create_time
- <= _normalize_date_bound(create_time_end.strip(), end_of_day=True)
- )
- total = q.order_by(None).with_entities(func.count(BizChannelServiceOrderTxn.id)).scalar() or 0
- page = max(1, page)
- page_size = min(max(1, page_size), 100)
- if finance_done_only:
- operate_time = func.coalesce(
- BizChannelServiceOrderTxn.finance_confirm_time,
- BizChannelServiceOrderTxn.void_time,
- )
- rows = (
- q.order_by(operate_time.desc(), BizChannelServiceOrderTxn.create_time.desc())
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- else:
- rows = (
- q.order_by(BizChannelServiceOrderTxn.create_time.desc())
- .offset((page - 1) * page_size)
- .limit(page_size)
- .all()
- )
- if not rows:
- return [], int(total)
- order_ids = list({r.service_order_id for r in rows if r.service_order_id})
- orders: Dict[str, Any] = {}
- if order_ids:
- oq = db.query(BizChannelServiceOrder).filter(
- BizChannelServiceOrder.id.in_(order_ids),
- BizChannelServiceOrder.del_flag == 0,
- )
- if tenant_no:
- oq = oq.filter(BizChannelServiceOrder.tenant_no == tenant_no)
- for o in oq.all():
- orders[o.id] = o
- user_ids: List[str] = []
- for r in rows:
- if r.create_by:
- user_ids.append(r.create_by)
- if r.finance_submit_by:
- user_ids.append(r.finance_submit_by)
- if r.finance_confirm_by:
- user_ids.append(r.finance_confirm_by)
- elif (r.txn_status or "") == "yqr" and r.update_by:
- user_ids.append(r.update_by)
- if r.void_by:
- user_ids.append(r.void_by)
- elif (r.txn_status or "") == "yzf" and r.finance_submit_time and r.update_by:
- user_ids.append(r.update_by)
- name_map = _user_nick_name_map(db, user_ids) if db and user_ids else {}
- items = [
- _service_order_txn_to_response_dict(
- db,
- r,
- tenant_no,
- order=orders.get(r.service_order_id),
- name_map=name_map,
- )
- for r in rows
- ]
- return items, int(total)
- def get_service_order_txn(db, txn_id, *, tenant_no=None):
- """获取单条流水"""
- q = db.query(BizChannelServiceOrderTxn).filter(
- and_(BizChannelServiceOrderTxn.id == txn_id, BizChannelServiceOrderTxn.del_flag == 0)
- )
- if tenant_no:
- q = q.filter(BizChannelServiceOrderTxn.tenant_no == tenant_no)
- return q.first()
- def create_recharge_txn(
- db,
- order_id,
- *,
- amount,
- remark=None,
- finance_submit_attachments=None,
- tenant_no=None,
- user_id=None,
- ):
- """创建充值流水(yfk订单),状态 dsx;附件预存于 finance_submit_attachments 供后续财务确认"""
- order = get_service_order(db, order_id, tenant_no)
- if not order:
- raise ValueError("订单不存在")
- if order.order_type != "yfk":
- raise ValueError("仅充值型订单可发起充值")
- if order.order_status not in ("dsx", "sxz"):
- raise ValueError("订单当前状态不允许充值")
- amount = float(amount)
- if amount <= 0:
- raise ValueError("充值金额必须大于0")
- now = datetime.now()
- txn = BizChannelServiceOrderTxn(
- id=generate_uuid(),
- service_order_id=order_id,
- txn_type="recharge",
- amount=amount,
- txn_status="dsx",
- remark=remark or "",
- finance_submit_attachments=(finance_submit_attachments or "").strip() or None,
- tenant_no=tenant_no or order.tenant_no,
- order_num=0,
- create_by=user_id,
- create_dept_id="",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(txn)
- db.commit()
- db.refresh(txn)
- return txn
- def create_refund_txn(db, order_id, *, amount, remark=None, attachments=None, tenant_no=None, user_id=None):
- """创建退款流水(yfk订单),amount <= current_balance"""
- order = get_service_order(db, order_id, tenant_no)
- if not order:
- raise ValueError("订单不存在")
- if order.order_type != "yfk":
- raise ValueError("仅充值型订单可申请退款")
- if order.order_status != "sxz":
- raise ValueError("仅生效中订单可申请退款")
- amount = float(amount)
- if amount <= 0:
- raise ValueError("退款金额必须大于0")
- current_balance = float(order.current_balance) if order.current_balance else 0
- if amount > current_balance:
- raise ValueError(f"退款金额不能大于当前余额 {current_balance}")
- now = datetime.now()
- txn = BizChannelServiceOrderTxn(
- id=generate_uuid(),
- service_order_id=order_id,
- txn_type="refund",
- amount=amount,
- txn_status="dsx",
- remark=remark or "",
- refund_attachments=attachments or "",
- tenant_no=tenant_no or order.tenant_no,
- order_num=0,
- create_by=user_id,
- create_dept_id="",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(txn)
- db.commit()
- db.refresh(txn)
- return txn
- def create_purchase_txn(db, order_id, *, products, remark=None, tenant_no=None, user_id=None):
- """创建购买产品流水(gmcp订单),同时创建/更新产品持仓"""
- order = get_service_order(db, order_id, tenant_no)
- if not order:
- raise ValueError("订单不存在")
- if order.order_type != "gmcp":
- raise ValueError("仅购买型订单可购买产品")
- if order.order_status not in ("dsx", "sxz"):
- raise ValueError("订单当前状态不允许购买")
- if not products:
- raise ValueError("产品明细不能为空")
- now = datetime.now()
- tn = tenant_no or order.tenant_no
- total_amount = 0.0
- # Upsert product holding rows
- for p in products:
- project_code = p.get("projectCode") or p.get("project_code") or ""
- project_name = p.get("projectName") or p.get("project_name") or ""
- cell_type = p.get("cellType") or p.get("cell_type") or ""
- spec_text = p.get("specText") or p.get("spec_text") or ""
- agreement_unit_price = float(p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0)
- qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
- amt = float(p.get("amount") or agreement_unit_price * qty)
- total_amount += amt
- # Find or create product holding row
- existing = db.query(BizChannelServiceOrderProduct).filter(
- and_(
- BizChannelServiceOrderProduct.service_order_id == order_id,
- BizChannelServiceOrderProduct.project_code == project_code,
- BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
- BizChannelServiceOrderProduct.del_flag == 0,
- )
- ).first()
- if existing:
- existing.total_buy_qty = float(existing.total_buy_qty or 0) + qty
- existing.current_qty = float(existing.current_qty or 0) + qty
- existing.update_by = user_id
- existing.update_time = now
- db.add(existing)
- else:
- prod_row = BizChannelServiceOrderProduct(
- id=generate_uuid(),
- service_order_id=order_id,
- project_code=project_code,
- project_name=project_name,
- cell_type=cell_type,
- spec_text=spec_text,
- agreement_unit_price=agreement_unit_price,
- buy_qty=qty,
- buy_amount=amt,
- total_buy_qty=qty,
- total_unsubscribe_qty=0,
- current_qty=qty,
- total_unsubscribe_amount=0,
- tenant_no=tn,
- order_num=0,
- remark="",
- create_by=user_id,
- create_dept_id="",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(prod_row)
- db.flush()
- products_json = json.dumps(products, ensure_ascii=False)
- txn = BizChannelServiceOrderTxn(
- id=generate_uuid(),
- service_order_id=order_id,
- txn_type="purchase",
- amount=total_amount,
- txn_status="dsx",
- products_json=products_json,
- remark=remark or "",
- tenant_no=tn,
- order_num=0,
- create_by=user_id,
- create_dept_id="",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- order.purchase_total_amount = float(order.purchase_total_amount or 0) + total_amount
- order.update_by = user_id
- order.update_time = now
- db.add(order)
- db.add(txn)
- db.commit()
- db.refresh(txn)
- return txn
- def create_unsubscribe_txn(db, order_id, *, products, remark=None, tenant_no=None, user_id=None):
- """创建退订产品流水(gmcp订单),校验退订份数 <= current_qty"""
- order = get_service_order(db, order_id, tenant_no)
- if not order:
- raise ValueError("订单不存在")
- if order.order_type != "gmcp":
- raise ValueError("仅购买型订单可退订产品")
- if order.order_status != "sxz":
- raise ValueError("仅生效中订单可退订")
- if not products:
- raise ValueError("退订产品明细不能为空")
- now = datetime.now()
- tn = tenant_no or order.tenant_no
- total_amount = 0.0
- for p in products:
- project_code = p.get("projectCode") or p.get("project_code") or ""
- spec_text = p.get("specText") or p.get("spec_text") or ""
- qty = float(p.get("qty") or 0)
- if qty <= 0:
- raise ValueError("退订数量必须大于0")
- # Find existing product holding
- existing = db.query(BizChannelServiceOrderProduct).filter(
- and_(
- BizChannelServiceOrderProduct.service_order_id == order_id,
- BizChannelServiceOrderProduct.project_code == project_code,
- BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
- BizChannelServiceOrderProduct.del_flag == 0,
- )
- ).first()
- if not existing:
- raise ValueError(f"产品 {project_code} 不存在于持仓中")
- current_qty = float(existing.current_qty or 0)
- if qty > current_qty:
- raise ValueError(f"退订数量 {qty} 超过当前持有数量 {current_qty}")
- unit_price = float(existing.agreement_unit_price or 0)
- amt = qty * unit_price
- total_amount += amt
- existing.total_unsubscribe_qty = float(existing.total_unsubscribe_qty or 0) + qty
- existing.current_qty = current_qty - qty
- existing.total_unsubscribe_amount = float(existing.total_unsubscribe_amount or 0) + amt
- existing.update_by = user_id
- existing.update_time = now
- db.add(existing)
- db.flush()
- products_json = json.dumps(products, ensure_ascii=False)
- txn = BizChannelServiceOrderTxn(
- id=generate_uuid(),
- service_order_id=order_id,
- txn_type="unsubscribe",
- amount=total_amount,
- txn_status="dsx",
- products_json=products_json,
- remark=remark or "",
- tenant_no=tn,
- order_num=0,
- create_by=user_id,
- create_dept_id="",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(txn)
- db.commit()
- db.refresh(txn)
- return txn
- def _rollback_purchase_txn_holdings(db, order_id, products, user_id, now=None):
- now = now or datetime.now()
- for p in products:
- project_code = p.get("projectCode") or p.get("project_code") or ""
- spec_text = p.get("specText") or p.get("spec_text") or ""
- qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
- existing = db.query(BizChannelServiceOrderProduct).filter(
- and_(
- BizChannelServiceOrderProduct.service_order_id == order_id,
- BizChannelServiceOrderProduct.project_code == project_code,
- BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
- BizChannelServiceOrderProduct.del_flag == 0,
- )
- ).first()
- if existing:
- existing.total_buy_qty = float(existing.total_buy_qty or 0) - qty
- existing.current_qty = float(existing.current_qty or 0) - qty
- existing.update_by = user_id
- existing.update_time = now
- db.add(existing)
- def _apply_purchase_txn_holdings(db, order_id, products, tenant_no, user_id, now=None):
- now = now or datetime.now()
- tn = tenant_no
- total_amount = 0.0
- for p in products:
- project_code = p.get("projectCode") or p.get("project_code") or ""
- project_name = p.get("projectName") or p.get("project_name") or ""
- cell_type = p.get("cellType") or p.get("cell_type") or ""
- spec_text = p.get("specText") or p.get("spec_text") or ""
- agreement_unit_price = float(p.get("agreementUnitPrice") or p.get("agreement_unit_price") or 0)
- qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
- if qty <= 0:
- raise ValueError("购买份数必须大于0")
- amt = float(p.get("amount") or agreement_unit_price * qty)
- total_amount += amt
- existing = db.query(BizChannelServiceOrderProduct).filter(
- and_(
- BizChannelServiceOrderProduct.service_order_id == order_id,
- BizChannelServiceOrderProduct.project_code == project_code,
- BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
- BizChannelServiceOrderProduct.del_flag == 0,
- )
- ).first()
- if existing:
- existing.total_buy_qty = float(existing.total_buy_qty or 0) + qty
- existing.current_qty = float(existing.current_qty or 0) + qty
- existing.update_by = user_id
- existing.update_time = now
- db.add(existing)
- else:
- prod_row = BizChannelServiceOrderProduct(
- id=generate_uuid(),
- service_order_id=order_id,
- project_code=project_code,
- project_name=project_name,
- cell_type=cell_type,
- spec_text=spec_text,
- agreement_unit_price=agreement_unit_price,
- buy_qty=qty,
- buy_amount=amt,
- total_buy_qty=qty,
- total_unsubscribe_qty=0,
- current_qty=qty,
- total_unsubscribe_amount=0,
- tenant_no=tn,
- order_num=0,
- remark="",
- create_by=user_id,
- create_dept_id="",
- create_time=now,
- update_by=user_id,
- update_time=now,
- del_flag=0,
- )
- db.add(prod_row)
- db.flush()
- return total_amount
- def _rollback_unsubscribe_txn_holdings(db, order_id, products, user_id, now=None):
- now = now or datetime.now()
- for p in products:
- project_code = p.get("projectCode") or p.get("project_code") or ""
- spec_text = p.get("specText") or p.get("spec_text") or ""
- qty = float(p.get("qty") or 0)
- existing = db.query(BizChannelServiceOrderProduct).filter(
- and_(
- BizChannelServiceOrderProduct.service_order_id == order_id,
- BizChannelServiceOrderProduct.project_code == project_code,
- BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
- BizChannelServiceOrderProduct.del_flag == 0,
- )
- ).first()
- if existing:
- unit_price = float(existing.agreement_unit_price or 0)
- existing.total_unsubscribe_qty = float(existing.total_unsubscribe_qty or 0) - qty
- existing.current_qty = float(existing.current_qty or 0) + qty
- existing.total_unsubscribe_amount = float(existing.total_unsubscribe_amount or 0) - (qty * unit_price)
- existing.update_by = user_id
- existing.update_time = now
- db.add(existing)
- def _apply_unsubscribe_txn_holdings(db, order_id, products, user_id, now=None):
- now = now or datetime.now()
- total_amount = 0.0
- for p in products:
- project_code = p.get("projectCode") or p.get("project_code") or ""
- spec_text = p.get("specText") or p.get("spec_text") or ""
- qty = float(p.get("qty") or 0)
- if qty <= 0:
- raise ValueError("退订数量必须大于0")
- existing = db.query(BizChannelServiceOrderProduct).filter(
- and_(
- BizChannelServiceOrderProduct.service_order_id == order_id,
- BizChannelServiceOrderProduct.project_code == project_code,
- BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
- BizChannelServiceOrderProduct.del_flag == 0,
- )
- ).first()
- if not existing:
- raise ValueError(f"产品 {project_code} 不存在于持仓中")
- current_qty = float(existing.current_qty or 0)
- if qty > current_qty:
- raise ValueError(f"退订数量 {qty} 超过当前持有数量 {current_qty}")
- unit_price = float(existing.agreement_unit_price or 0)
- amt = qty * unit_price
- total_amount += amt
- existing.total_unsubscribe_qty = float(existing.total_unsubscribe_qty or 0) + qty
- existing.current_qty = current_qty - qty
- existing.total_unsubscribe_amount = float(existing.total_unsubscribe_amount or 0) + amt
- existing.update_by = user_id
- existing.update_time = now
- db.add(existing)
- db.flush()
- return total_amount
- def update_service_order_txn(
- db,
- txn_id,
- *,
- amount=None,
- remark=None,
- finance_submit_attachments=None,
- refund_attachments=None,
- products=None,
- tenant_no=None,
- user_id=None,
- ):
- """更新待生效流水:仅 dsx;充值/退款可改金额与附件,购买/退订可改产品明细与备注"""
- txn = get_service_order_txn(db, txn_id, tenant_no=tenant_no)
- if not txn:
- raise ValueError("流水不存在")
- if txn.txn_status != "dsx":
- raise ValueError("仅待生效流水可编辑")
- now = datetime.now()
- txn_type = (txn.txn_type or "").strip()
- if amount is not None:
- amt = float(amount)
- if amt <= 0:
- raise ValueError("金额必须大于0")
- if txn_type in ("recharge", "refund"):
- if txn_type == "refund":
- order = get_service_order(db, txn.service_order_id, tenant_no)
- if not order:
- raise ValueError("关联订单不存在")
- current_balance = float(order.current_balance) if order.current_balance else 0
- if amt > current_balance:
- raise ValueError(f"退款金额不能大于当前余额 {current_balance}")
- txn.amount = amt
- else:
- raise ValueError("购买/退订流水不可直接修改金额,请调整产品明细")
- if products is not None:
- if txn_type not in ("purchase", "unsubscribe"):
- raise ValueError("仅购买/退订流水可修改产品明细")
- if not products:
- raise ValueError("产品明细不能为空")
- order = get_service_order(db, txn.service_order_id, tenant_no)
- if not order:
- raise ValueError("关联订单不存在")
- old_products = json.loads(txn.products_json) if txn.products_json else []
- if txn_type == "purchase":
- _rollback_purchase_txn_holdings(db, order.id, old_products, user_id, now)
- total_amount = _apply_purchase_txn_holdings(
- db, order.id, products, tenant_no or order.tenant_no, user_id, now
- )
- else:
- _rollback_unsubscribe_txn_holdings(db, order.id, old_products, user_id, now)
- total_amount = _apply_unsubscribe_txn_holdings(db, order.id, products, user_id, now)
- txn.amount = total_amount
- txn.products_json = json.dumps(products, ensure_ascii=False)
- if remark is not None:
- txn.remark = remark or ""
- if finance_submit_attachments is not None:
- txn.finance_submit_attachments = (finance_submit_attachments or "").strip() or None
- if refund_attachments is not None:
- if txn_type != "refund":
- raise ValueError("仅退款流水可修改退款附件")
- txn.refund_attachments = (refund_attachments or "").strip() or None
- txn.update_by = user_id
- txn.update_time = now
- db.add(txn)
- db.commit()
- db.refresh(txn)
- return txn
- def submit_txn_finance(db, txn_id, *, finance_submit_remark=None, finance_submit_attachments=None, tenant_no=None, user_id=None):
- """发起流水财务确认:dsx → dqr"""
- txn = get_service_order_txn(db, txn_id, tenant_no=tenant_no)
- if not txn:
- raise ValueError("流水不存在")
- if txn.txn_status != "dsx":
- raise ValueError("仅待生效流水可发起确认")
- now = datetime.now()
- txn.txn_status = "dqr"
- txn.finance_submit_by = user_id
- txn.finance_submit_time = now
- submit_remark = (finance_submit_remark or "").strip()
- if not submit_remark:
- submit_remark = (txn.remark or "").strip()
- txn.finance_submit_remark = submit_remark
- if finance_submit_attachments:
- txn.finance_submit_attachments = finance_submit_attachments
- elif not (txn.finance_submit_attachments or "").strip():
- if (txn.txn_type or "").strip() == "refund" and (txn.refund_attachments or "").strip():
- txn.finance_submit_attachments = txn.refund_attachments
- else:
- txn.finance_submit_attachments = ""
- txn.update_by = user_id
- txn.update_time = now
- db.add(txn)
- db.commit()
- db.refresh(txn)
- return txn
- def confirm_txn(db, txn_id, *, finance_remark=None, finance_confirm_attachments=None, tenant_no=None, user_id=None):
- """财务确认流水:dqr → yqr,更新主表汇总和产品持仓,更新渠道账户余额"""
- txn = get_service_order_txn(db, txn_id, tenant_no=tenant_no)
- if not txn:
- raise ValueError("流水不存在")
- if txn.txn_status != "dqr":
- raise ValueError("仅待确认流水可进行财务确认")
- order = get_service_order(db, txn.service_order_id, tenant_no)
- if not order:
- raise ValueError("关联订单不存在")
- now = datetime.now()
- if txn.txn_type == "recharge":
- # Increase channel account balance
- account = _get_or_create_channel_account(db, order.channel_id, tenant_no or order.tenant_no, user_id)
- balance_before = float(account.balance_amount) if account.balance_amount else 0
- add_amount = float(txn.amount) if txn.amount else 0
- account.balance_amount = balance_before + add_amount
- balance_after = float(account.balance_amount)
- account.update_by = user_id
- account.update_time = now
- db.add(account)
- db.flush()
- # Update order summary
- order.total_recharge_amount = float(order.total_recharge_amount or 0) + add_amount
- order.current_balance = float(order.current_balance or 0) + add_amount
- txn.balance_before = balance_before
- txn.balance_after = balance_after
- elif txn.txn_type == "refund":
- # Deduct channel account balance
- account = _get_or_create_channel_account(db, order.channel_id, tenant_no or order.tenant_no, user_id)
- balance_before = float(account.balance_amount) if account.balance_amount else 0
- deduct_amount = float(txn.amount) if txn.amount else 0
- account.balance_amount = balance_before - deduct_amount
- balance_after = float(account.balance_amount)
- account.update_by = user_id
- account.update_time = now
- db.add(account)
- db.flush()
- # Update order summary
- order.total_refund_amount = float(order.total_refund_amount or 0) + deduct_amount
- order.current_balance = float(order.current_balance or 0) - deduct_amount
- txn.balance_before = balance_before
- txn.balance_after = balance_after
- elif txn.txn_type == "purchase":
- # Products already updated at create time, just update channel quotas
- products = json.loads(txn.products_json) if txn.products_json else []
- for p in products:
- project_code = p.get("projectCode") or p.get("project_code") or ""
- spec_text = p.get("specText") or p.get("spec_text") or ""
- qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
- cell_type_code, _ = _resolve_agreement_price_cell_type(
- db, order.channel_agreement_id, project_code, spec_text, p.get("cellType") or p.get("cell_type") or ""
- )
- _upsert_channel_product_quota(
- db,
- channel_id=order.channel_id,
- project_code=project_code,
- project_name=p.get("projectName") or p.get("project_name") or "",
- cell_type_code=cell_type_code,
- cell_type_name=p.get("cellType") or p.get("cell_type") or "",
- spec_text=spec_text,
- add_qty=qty,
- tenant_no=tenant_no or order.tenant_no,
- user_id=user_id,
- )
- elif txn.txn_type == "unsubscribe":
- # Products already updated at create time, just reduce channel quotas
- products = json.loads(txn.products_json) if txn.products_json else []
- for p in products:
- project_code = p.get("projectCode") or p.get("project_code") or ""
- spec_text = p.get("specText") or p.get("spec_text") or ""
- qty = float(p.get("qty") or 0)
- cell_type_code, _ = _resolve_agreement_price_cell_type(
- db, order.channel_agreement_id, project_code, spec_text, p.get("cellType") or p.get("cell_type") or ""
- )
- _reduce_channel_product_quota(
- db,
- channel_id=order.channel_id,
- project_code=project_code,
- cell_type_code=cell_type_code,
- spec_text=spec_text,
- reduce_qty=qty,
- user_id=user_id,
- )
- # Update txn status
- txn.txn_status = "yqr"
- txn.finance_confirm_time = now
- txn.finance_confirm_by = user_id
- txn.finance_confirm_attachments = finance_confirm_attachments or ""
- txn.finance_remark = finance_remark or ""
- txn.update_by = user_id
- txn.update_time = now
- # Transition order to sxz if currently dsx
- if order.order_status == "dsx":
- order.order_status = "sxz"
- order.update_by = user_id
- order.update_time = now
- db.add(txn)
- db.add(order)
- db.commit()
- db.refresh(txn)
- return txn
- def void_txn(db, txn_id, *, tenant_no=None, user_id=None):
- """作废流水:dsx/dqr → yzf。如果是 purchase/unsubscribe 还需要回滚产品持仓"""
- txn = get_service_order_txn(db, txn_id, tenant_no=tenant_no)
- if not txn:
- raise ValueError("流水不存在")
- if txn.txn_status not in ("dsx", "dqr"):
- raise ValueError("仅待生效或待财务确认流水可作废")
- if (txn.txn_type or "").strip() == "settlement":
- raise ValueError("输注计划结算流水不可作废")
- now = datetime.now()
- order = get_service_order(db, txn.service_order_id, tenant_no)
- if txn.txn_type == "purchase" and order:
- # Rollback product holdings
- products = json.loads(txn.products_json) if txn.products_json else []
- for p in products:
- project_code = p.get("projectCode") or p.get("project_code") or ""
- spec_text = p.get("specText") or p.get("spec_text") or ""
- qty = float(p.get("qty") or p.get("buyQty") or p.get("buy_qty") or 0)
- existing = db.query(BizChannelServiceOrderProduct).filter(
- and_(
- BizChannelServiceOrderProduct.service_order_id == order.id,
- BizChannelServiceOrderProduct.project_code == project_code,
- BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
- BizChannelServiceOrderProduct.del_flag == 0,
- )
- ).first()
- if existing:
- existing.total_buy_qty = float(existing.total_buy_qty or 0) - qty
- existing.current_qty = float(existing.current_qty or 0) - qty
- existing.update_by = user_id
- existing.update_time = now
- db.add(existing)
- txn_amount = float(txn.amount or 0)
- if txn_amount > 0:
- order.purchase_total_amount = max(
- 0.0,
- float(order.purchase_total_amount or 0) - txn_amount,
- )
- order.update_by = user_id
- order.update_time = now
- db.add(order)
- elif txn.txn_type == "unsubscribe" and order:
- # Rollback unsubscribe: restore product holdings
- products = json.loads(txn.products_json) if txn.products_json else []
- for p in products:
- project_code = p.get("projectCode") or p.get("project_code") or ""
- spec_text = p.get("specText") or p.get("spec_text") or ""
- qty = float(p.get("qty") or 0)
- unit_price = 0
- existing = db.query(BizChannelServiceOrderProduct).filter(
- and_(
- BizChannelServiceOrderProduct.service_order_id == order.id,
- BizChannelServiceOrderProduct.project_code == project_code,
- BizChannelServiceOrderProduct.spec_text == (spec_text or ""),
- BizChannelServiceOrderProduct.del_flag == 0,
- )
- ).first()
- if existing:
- unit_price = float(existing.agreement_unit_price or 0)
- existing.total_unsubscribe_qty = float(existing.total_unsubscribe_qty or 0) - qty
- existing.current_qty = float(existing.current_qty or 0) + qty
- existing.total_unsubscribe_amount = float(existing.total_unsubscribe_amount or 0) - (qty * unit_price)
- existing.update_by = user_id
- existing.update_time = now
- db.add(existing)
- txn.txn_status = "yzf"
- txn.void_by = user_id
- txn.void_time = now
- txn.update_by = user_id
- txn.update_time = now
- db.add(txn)
- db.commit()
- db.refresh(txn)
- return txn
|