| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220 |
- """移动市场协议服务:双来源统一展示,写入保持来源内原子性。"""
- from __future__ import annotations
- from datetime import datetime, timedelta
- from decimal import Decimal
- from typing import List, Optional, Tuple
- from fastapi import HTTPException
- from sqlalchemy import func, or_
- from sqlalchemy.orm import Session
- from app.core.utils import parse_datetime
- from app.core.protocol_status_display import (
- build_void_type_label_map,
- resolve_void_type_label,
- )
- from app.crud import collection_protocol as cell_crud
- from app.crud import protocol_sample_payment as psp_crud
- from app.crud import research_donation as donation_crud
- from app.crud import sample_freight as sample_freight_crud
- from app.models.models import (
- BizCellSampleType,
- BizCollectionHospital,
- BizCollectionProtocol,
- BizCollectionRegion,
- BizCollectionRegionStaff,
- BizCustomer,
- BizItemUnit,
- BizProtocolCollectionInfo,
- BizProtocolPartyCustomer,
- BizProtocolPayment,
- BizProtocolPaymentDetail,
- BizProtocolPaymentSchedule,
- BizProtocolPaymentTransfer,
- BizProtocolSample,
- BizProtocolSamplePayment,
- BizProtocolSampleTransport,
- BizSampleCode,
- SysUser,
- )
- from app.models.research_donation import (
- BizResearchDonation,
- BizResearchDonationCollection,
- BizResearchDonationParty,
- BizResearchDonationSample,
- BizResearchDonationSampleTransport,
- )
- from app.schemas.market_mobile import MarketProtocolBody
- from app.services.market_field_scope import (
- MarketFieldContext,
- apply_cell_protocol_scope,
- apply_donation_scope,
- assert_cell_protocol_access,
- assert_donation_access,
- )
- from app.services import market_freight
- from app.services.market_support import (
- _dict_options,
- allowed_actions,
- validate_attachment_refs,
- )
- SOURCE_CELL = 'cell_storage'
- SOURCE_DONATION = 'research_donation'
- def _fixed_money(value):
- if value is None:
- return None
- return format(Decimal(str(value)).quantize(Decimal('0.01')), 'f')
- SOURCES = (SOURCE_CELL, SOURCE_DONATION)
- _CUSTOMER_LINK_FIELDS = (
- 'customer_id',
- 'father_customer_id',
- 'newborn_customer_id',
- )
- def _current_dept_id(user_info: dict) -> str:
- if not isinstance(user_info, dict):
- return ''
- return str(
- user_info.get('dept_id')
- or user_info.get('deptId')
- or user_info.get('region_id')
- or ''
- ).strip()
- def _dump(value) -> dict:
- if value is None:
- return {}
- if hasattr(value, 'model_dump'):
- return value.model_dump(by_alias=False, exclude_unset=True)
- return dict(value)
- def _assert_customer_link_policy(
- db: Session,
- customer: dict,
- *,
- ctx: MarketFieldContext,
- user_info: dict,
- ) -> None:
- """允许绑定/改绑本租户数据权限内的客户主档;空值表示清空;禁止无效 id。"""
- from app.core.data_scope import apply_data_scope
- for key in _CUSTOMER_LINK_FIELDS:
- if key not in customer:
- continue
- supplied = str(customer.get(key) or '').strip()
- if not supplied:
- continue
- query = db.query(BizCustomer.id).filter(
- BizCustomer.id == supplied,
- BizCustomer.tenant_no == ctx.tenant_no,
- BizCustomer.del_flag == 0,
- )
- query = apply_data_scope(query, BizCustomer, user_info, db, ctx.tenant_no)
- if query.first() is None:
- raise ValueError('客户不存在或不在权限范围内')
- def _fmt_dt(value) -> str:
- if value is None:
- return ''
- if isinstance(value, str):
- return value
- return value.strftime('%Y-%m-%d %H:%M:%S')
- def _manager_assignment_allowed(
- db: Session,
- ctx: MarketFieldContext,
- *,
- region_id: Optional[str],
- region_manager_user_id: Optional[str],
- ) -> bool:
- region = str(region_id or '').strip()
- if not region:
- return False
- return (
- db.query(BizCollectionRegionStaff.id)
- .filter(
- BizCollectionRegionStaff.tenant_no == ctx.tenant_no,
- BizCollectionRegionStaff.del_flag == 0,
- BizCollectionRegionStaff.user_id == ctx.user_id,
- BizCollectionRegionStaff.is_leader == 1,
- BizCollectionRegionStaff.region_id == region,
- )
- .first()
- is not None
- )
- def _ensure_manager_assignment(
- db: Session,
- ctx: MarketFieldContext,
- *,
- region_id: Optional[str],
- region_manager_user_id: Optional[str],
- ) -> None:
- if not _manager_assignment_allowed(
- db,
- ctx,
- region_id=region_id,
- region_manager_user_id=region_manager_user_id,
- ):
- raise ValueError('协议归属不在当前地区经理范围内')
- def _active_user_ids(db: Session, ctx: MarketFieldContext, ids: set) -> set:
- if not ids:
- return set()
- return {
- str(row[0])
- for row in db.query(SysUser.id)
- .filter(
- SysUser.id.in_(ids),
- SysUser.tenant_no == ctx.tenant_no,
- SysUser.del_flag == 0,
- SysUser.status == '1',
- )
- .all()
- }
- def _validate_references(
- db: Session,
- ctx: MarketFieldContext,
- *,
- customer: dict,
- collection: dict,
- samples: List[dict],
- extra_user_ids: Optional[set] = None,
- ) -> None:
- region_id = str(collection.get('region_id') or '').strip()
- hospital_id = str(collection.get('hospital_id') or '').strip()
- if region_id:
- exists = (
- db.query(BizCollectionRegion.id)
- .filter(
- BizCollectionRegion.id == region_id,
- BizCollectionRegion.tenant_no == ctx.tenant_no,
- BizCollectionRegion.del_flag == 0,
- )
- .first()
- )
- if not exists:
- raise ValueError('采集地区不存在或不属于当前租户')
- if hospital_id:
- exists = (
- db.query(BizCollectionHospital.id)
- .filter(
- BizCollectionHospital.id == hospital_id,
- BizCollectionHospital.region_id == region_id,
- BizCollectionHospital.tenant_no == ctx.tenant_no,
- BizCollectionHospital.del_flag == 0,
- )
- .first()
- )
- if not exists:
- raise ValueError('医院不存在、不属于当前租户或与采集地区不匹配')
- user_ids = {
- str(value).strip()
- for value in (
- customer.get('collector_user_id'),
- collection.get('sales_user_id'),
- collection.get('region_manager_user_id'),
- )
- if str(value or '').strip()
- }
- user_ids.update(
- str(value).strip()
- for value in (extra_user_ids or set())
- if str(value or '').strip()
- )
- if _active_user_ids(db, ctx, user_ids) != user_ids:
- raise ValueError('引用用户不存在、已停用或不属于当前租户')
- customer_ids = {
- str(customer.get(key) or '').strip()
- for key in ('customer_id', 'father_customer_id', 'newborn_customer_id')
- if str(customer.get(key) or '').strip()
- }
- if customer_ids:
- existing = {
- str(row[0])
- for row in db.query(BizCustomer.id)
- .filter(
- BizCustomer.id.in_(customer_ids),
- BizCustomer.tenant_no == ctx.tenant_no,
- BizCustomer.del_flag == 0,
- )
- .all()
- }
- if existing != customer_ids:
- raise ValueError('客户不存在或不属于当前租户')
- type_ids = {
- str(item.get('sample_type_id') or '').strip()
- for item in samples
- if str(item.get('sample_type_id') or '').strip()
- }
- if type_ids:
- existing = {
- str(row[0])
- for row in db.query(BizCellSampleType.id)
- .filter(
- BizCellSampleType.id.in_(type_ids),
- BizCellSampleType.tenant_no == ctx.tenant_no,
- BizCellSampleType.del_flag == 0,
- )
- .all()
- }
- if existing != type_ids:
- raise ValueError('样本类型不存在或不属于当前租户')
- unit_ids = {
- str(item.get('unit_id') or '').strip()
- for item in samples
- if str(item.get('unit_id') or '').strip()
- }
- if unit_ids:
- existing = {
- str(row[0])
- for row in db.query(BizItemUnit.id)
- .filter(
- BizItemUnit.id.in_(unit_ids),
- BizItemUnit.tenant_no == ctx.tenant_no,
- BizItemUnit.del_flag == 0,
- )
- .all()
- }
- if existing != unit_ids:
- raise ValueError('计量单位不存在或不属于当前租户')
- sample_code_ids = {
- str(item.get('sample_code_id') or '').strip()
- for item in samples
- if str(item.get('sample_code_id') or '').strip()
- }
- sample_code_map = {}
- if sample_code_ids:
- sample_code_map = {
- str(row.id): str(row.sample_code or '').strip()
- for row in db.query(BizSampleCode)
- .filter(
- BizSampleCode.id.in_(sample_code_ids),
- BizSampleCode.tenant_no == ctx.tenant_no,
- BizSampleCode.del_flag == 0,
- )
- .all()
- }
- if set(sample_code_map) != sample_code_ids:
- raise ValueError('样本编码不存在、已删除或不属于当前租户')
- for item in samples:
- sample_code_id = str(item.get('sample_code_id') or '').strip()
- if not sample_code_id:
- continue
- expected_code = sample_code_map[sample_code_id]
- supplied_code = str(item.get('sample_code') or '').strip()
- if supplied_code and supplied_code != expected_code:
- raise ValueError('样本编码ID与样本编码不一致')
- item['sample_code'] = expected_code
- def _ensure_donation_sample_codes(
- db: Session, samples: List[dict], *, tenant_no: str
- ) -> None:
- """科研捐赠:空样本编码时按 Web 规则自动生成。"""
- reserved: List[str] = []
- for item in samples:
- existing = str(item.get('sample_code') or '').strip()
- if existing:
- reserved.append(existing)
- continue
- code = donation_crud.next_donation_sample_code(
- db, tenant_no=tenant_no, extra_codes=reserved
- )
- item['sample_code'] = code
- reserved.append(code)
- def _normalize_write(
- db: Session,
- *,
- body: MarketProtocolBody,
- ctx: MarketFieldContext,
- current_collection=None,
- current_samples: Optional[List] = None,
- ) -> Tuple[dict, dict, List[dict], dict]:
- customer = _dump(body.customer)
- collection = _dump(body.collection)
- samples = [_dump(item) for item in (body.samples or [])]
- _validate_unique_sample_identity(samples)
- if body.samples is not None and current_samples is not None:
- samples = _merge_existing_sample_fields(current_samples, samples)
- elif any(str(item.get('id') or '').strip() for item in samples):
- raise ValueError('样本不存在或不属于当前协议')
- if body.source == SOURCE_DONATION:
- _ensure_donation_sample_codes(db, samples, tenant_no=ctx.tenant_no)
- # freight_draft 可能仍是嵌套 model;统一成 dict 供运费挂钩
- for item in samples:
- draft = item.get('freight_draft')
- if draft is not None and hasattr(draft, 'model_dump'):
- item['freight_draft'] = draft.model_dump(
- by_alias=False, exclude_unset=True
- )
- attachments = _dump(body.attachments)
- effective_collection = {
- key: getattr(current_collection, key, None)
- for key in (
- 'region_id',
- 'hospital_id',
- 'sales_user_id',
- 'region_manager_user_id',
- )
- }
- effective_collection.update(collection)
- if ctx.scope_kind == 'specialist':
- customer['collector_user_id'] = ctx.user_id
- collection['sales_user_id'] = ctx.user_id
- effective_collection['sales_user_id'] = ctx.user_id
- else:
- unchanged_direct_assignment = bool(
- current_collection is not None
- and str(getattr(current_collection, 'region_id', None) or '').strip()
- == str(effective_collection.get('region_id') or '').strip()
- and str(
- getattr(current_collection, 'region_manager_user_id', None) or ''
- ).strip()
- == ctx.user_id
- and str(effective_collection.get('region_manager_user_id') or '').strip()
- == ctx.user_id
- )
- if not unchanged_direct_assignment:
- _ensure_manager_assignment(
- db,
- ctx,
- region_id=effective_collection.get('region_id'),
- region_manager_user_id=effective_collection.get(
- 'region_manager_user_id'
- ),
- )
- for key in ('sample_collect_time', 'maternal_blood_collect_time'):
- if key in collection and isinstance(collection[key], str):
- collection[key] = parse_datetime(collection[key])
- if 'newborn_delivery_time' in customer and isinstance(
- customer['newborn_delivery_time'], str
- ):
- customer['newborn_delivery_time'] = parse_datetime(
- customer['newborn_delivery_time']
- )
- _validate_references(
- db,
- ctx,
- customer=customer,
- collection=effective_collection,
- samples=samples,
- extra_user_ids={body.collector_user_id}
- if ctx.scope_kind == 'manager' and body.collector_user_id
- else set(),
- )
- return customer, collection, samples, attachments
- _SAMPLE_PRESERVED_FIELDS = (
- 'sample_type_id',
- 'sample_code_id',
- 'sample_code',
- 'collect_amount',
- 'unit_id',
- 'price',
- 'original_price',
- 'storage_years',
- 'sample_status',
- 'storage_in_date',
- 'detect_start_date',
- 'void_date',
- 'storage_expire_date',
- 'is_special',
- 'special_remark',
- 'remark',
- )
- def _validate_unique_sample_identity(samples: List[dict]) -> None:
- for field_name, api_name in (
- ('id', 'sampleId'),
- ('sample_code', 'sampleCode'),
- ('sample_code_id', 'sampleCodeId'),
- ):
- values = [
- str(item.get(field_name) or '').strip()
- for item in samples
- if str(item.get(field_name) or '').strip()
- ]
- if len(values) != len(set(values)):
- raise ValueError(f'{api_name} 不能重复')
- def _unique_sample_map(current_samples: List, field_name: str) -> dict:
- grouped = {}
- for item in current_samples:
- value = str(getattr(item, field_name, None) or '').strip()
- if value:
- grouped.setdefault(value, []).append(item)
- return {
- value: rows[0]
- for value, rows in grouped.items()
- if len(rows) == 1
- }
- def _merge_existing_sample_fields(current_samples: List, samples: List[dict]) -> List[dict]:
- """按稳定身份沿用未展示字段;未匹配项一律按新样本处理。"""
- current_by_id = {
- str(getattr(item, 'id', None) or '').strip(): item
- for item in current_samples
- if str(getattr(item, 'id', None) or '').strip()
- }
- current_by_code_id = _unique_sample_map(current_samples, 'sample_code_id')
- current_by_code = _unique_sample_map(current_samples, 'sample_code')
- merged = []
- for incoming in samples:
- sample_id = str(incoming.get('id') or '').strip()
- code = str(incoming.get('sample_code') or '').strip()
- code_id = str(incoming.get('sample_code_id') or '').strip()
- if sample_id:
- current = current_by_id.get(sample_id)
- if current is None:
- raise ValueError('样本不存在或不属于当前协议')
- else:
- current = current_by_code_id.get(code_id) or current_by_code.get(code)
- base = (
- {
- key: getattr(current, key, None)
- for key in _SAMPLE_PRESERVED_FIELDS
- }
- if current is not None
- else {}
- )
- base.update(incoming)
- base.pop('id', None)
- merged.append(base)
- return merged
- def _validate_mobile_attachments(
- db: Session,
- attachments: dict,
- *,
- ctx: MarketFieldContext,
- creating: bool,
- existing: Optional[dict] = None,
- business_id: Optional[str] = None,
- ) -> dict:
- current = existing or {}
- normalized = dict(attachments)
- for key, refs in attachments.items():
- if refs is None:
- continue
- normalized[key] = validate_attachment_refs(
- db,
- refs,
- module='protocol',
- ctx=ctx,
- creating=creating,
- existing_refs=current.get(key),
- business_id=business_id,
- )
- return normalized
- def _validate_v1_payment_detail_attachments(
- db: Session,
- details: Optional[List],
- *,
- row: BizCollectionProtocol,
- ctx: MarketFieldContext,
- ) -> Optional[List[dict]]:
- """Validate and bind legacy detail receipts through the protocol boundary."""
- if details is None:
- return None
- payment = (
- db.query(BizProtocolPayment)
- .filter(
- BizProtocolPayment.collection_protocol_id == row.id,
- BizProtocolPayment.tenant_no == ctx.tenant_no,
- BizProtocolPayment.del_flag == 0,
- )
- .populate_existing()
- .with_for_update()
- .first()
- )
- existing_rows = (
- db.query(BizProtocolPaymentDetail)
- .filter(
- BizProtocolPaymentDetail.payment_id == payment.id,
- BizProtocolPaymentDetail.tenant_no == ctx.tenant_no,
- BizProtocolPaymentDetail.del_flag == 0,
- )
- .populate_existing()
- .with_for_update()
- .all()
- if payment is not None
- else []
- )
- existing_by_id = {item.id: item for item in existing_rows}
- normalized: List[dict] = []
- for item in details:
- raw = item.model_dump(by_alias=False, exclude_unset=True, mode='json')
- if 'transfer_attachments' not in raw:
- normalized.append(raw)
- continue
- current = existing_by_id.get(str(raw.get('id') or '').strip())
- current_refs = current.transfer_attachments if current is not None else None
- if current is not None and current.pay_status != 'dfj':
- if str(raw.get('transfer_attachments') or '').strip() != str(
- current_refs or ''
- ).strip():
- raise ValueError('锁定缴费明细的附件不可修改')
- raw['transfer_attachments'] = current_refs or ''
- else:
- raw['transfer_attachments'] = validate_attachment_refs(
- db,
- raw.get('transfer_attachments'),
- module='protocol',
- ctx=ctx,
- creating=current is None,
- existing_refs=current_refs,
- business_id=row.id,
- )
- normalized.append(raw)
- return normalized
- def _party_name_cell(db: Session, ids: List[str], tenant_no: str) -> dict:
- if not ids:
- return {}
- rows = (
- db.query(BizProtocolPartyCustomer)
- .filter(
- BizProtocolPartyCustomer.collection_protocol_id.in_(ids),
- BizProtocolPartyCustomer.tenant_no == tenant_no,
- BizProtocolPartyCustomer.del_flag == 0,
- )
- .all()
- )
- return {row.collection_protocol_id: row.mother_name or '' for row in rows}
- def _party_name_donation(db: Session, ids: List[str], tenant_no: str) -> dict:
- if not ids:
- return {}
- rows = (
- db.query(BizResearchDonationParty)
- .filter(
- BizResearchDonationParty.research_donation_id.in_(ids),
- BizResearchDonationParty.tenant_no == tenant_no,
- BizResearchDonationParty.del_flag == 0,
- )
- .all()
- )
- return {row.research_donation_id: row.mother_name or '' for row in rows}
- def _enrich_protocol_cards(
- db: Session, items: List[dict], tenant_no: str
- ) -> None:
- """批量补齐与 Web 列表一致的移动卡片字段,避免逐行查询。"""
- cell_ids = [item['id'] for item in items if item['source'] == SOURCE_CELL]
- donation_ids = [
- item['id'] for item in items if item['source'] == SOURCE_DONATION
- ]
- collection_map = {}
- sample_map = {}
- party_map = {}
- if cell_ids:
- for row in db.query(BizProtocolCollectionInfo).filter(
- BizProtocolCollectionInfo.collection_protocol_id.in_(cell_ids),
- BizProtocolCollectionInfo.tenant_no == tenant_no,
- BizProtocolCollectionInfo.del_flag == 0,
- ).order_by(BizProtocolCollectionInfo.create_time.desc()).all():
- collection_map.setdefault((SOURCE_CELL, row.collection_protocol_id), row)
- for row in db.query(BizProtocolSample).filter(
- BizProtocolSample.collection_protocol_id.in_(cell_ids),
- BizProtocolSample.tenant_no == tenant_no,
- BizProtocolSample.del_flag == 0,
- ).order_by(BizProtocolSample.create_time.asc()).all():
- sample_map.setdefault((SOURCE_CELL, row.collection_protocol_id), row)
- for row in db.query(BizProtocolPartyCustomer).filter(
- BizProtocolPartyCustomer.collection_protocol_id.in_(cell_ids),
- BizProtocolPartyCustomer.tenant_no == tenant_no,
- BizProtocolPartyCustomer.del_flag == 0,
- ).order_by(BizProtocolPartyCustomer.create_time.desc()).all():
- party_map.setdefault((SOURCE_CELL, row.collection_protocol_id), row)
- if donation_ids:
- for row in db.query(BizResearchDonationCollection).filter(
- BizResearchDonationCollection.research_donation_id.in_(donation_ids),
- BizResearchDonationCollection.tenant_no == tenant_no,
- BizResearchDonationCollection.del_flag == 0,
- ).order_by(BizResearchDonationCollection.create_time.desc()).all():
- collection_map.setdefault((SOURCE_DONATION, row.research_donation_id), row)
- for row in db.query(BizResearchDonationSample).filter(
- BizResearchDonationSample.research_donation_id.in_(donation_ids),
- BizResearchDonationSample.tenant_no == tenant_no,
- BizResearchDonationSample.del_flag == 0,
- ).order_by(BizResearchDonationSample.create_time.asc()).all():
- sample_map.setdefault((SOURCE_DONATION, row.research_donation_id), row)
- for row in db.query(BizResearchDonationParty).filter(
- BizResearchDonationParty.research_donation_id.in_(donation_ids),
- BizResearchDonationParty.tenant_no == tenant_no,
- BizResearchDonationParty.del_flag == 0,
- ).order_by(BizResearchDonationParty.create_time.desc()).all():
- party_map.setdefault((SOURCE_DONATION, row.research_donation_id), row)
- region_ids = {
- row.region_id for row in collection_map.values() if row.region_id
- }
- hospital_ids = {
- row.hospital_id for row in collection_map.values() if row.hospital_id
- }
- user_ids = {
- value
- for row in collection_map.values()
- for value in (row.sales_user_id, row.region_manager_user_id)
- if value
- }
- user_ids.update(
- item.get('collectorUserId')
- for item in items
- if item.get('collectorUserId')
- )
- type_ids = {
- row.sample_type_id for row in sample_map.values() if row.sample_type_id
- }
- region_names = {
- row.id: row.region_name or ''
- for row in db.query(BizCollectionRegion).filter(
- BizCollectionRegion.id.in_(region_ids or ['']),
- BizCollectionRegion.tenant_no == tenant_no,
- BizCollectionRegion.del_flag == 0,
- )
- }
- hospital_names = {
- row.id: row.hospital_name or ''
- for row in db.query(BizCollectionHospital).filter(
- BizCollectionHospital.id.in_(hospital_ids or ['']),
- BizCollectionHospital.tenant_no == tenant_no,
- BizCollectionHospital.del_flag == 0,
- )
- }
- user_names = {
- row.id: row.nick_name or ''
- for row in db.query(SysUser).filter(
- SysUser.id.in_(user_ids or ['']),
- SysUser.tenant_no == tenant_no,
- )
- }
- type_names = {
- row.id: row.type_name or ''
- for row in db.query(BizCellSampleType).filter(
- BizCellSampleType.id.in_(type_ids or ['']),
- BizCellSampleType.del_flag == 0,
- or_(
- BizCellSampleType.tenant_no == tenant_no,
- BizCellSampleType.tenant_no == '',
- ),
- )
- }
- sample_ids = [row.id for row in sample_map.values()]
- transport_by_sample = {}
- if sample_ids:
- for row in db.query(BizProtocolSampleTransport).filter(
- BizProtocolSampleTransport.sample_id.in_(sample_ids),
- BizProtocolSampleTransport.tenant_no == tenant_no,
- BizProtocolSampleTransport.del_flag == 0,
- ).all():
- transport_by_sample.setdefault(row.sample_id, row)
- for row in db.query(BizResearchDonationSampleTransport).filter(
- BizResearchDonationSampleTransport.sample_id.in_(sample_ids),
- BizResearchDonationSampleTransport.tenant_no == tenant_no,
- BizResearchDonationSampleTransport.del_flag == 0,
- ).all():
- transport_by_sample.setdefault(row.sample_id, row)
- lab_by_key = {}
- from app.crud import sample_reception as reception_crud
- for source in SOURCES:
- codes = [
- row.sample_code
- for (item_source, _), row in sample_map.items()
- if item_source == source and row.sample_code
- ]
- for code, lab_code in reception_crud.get_lab_storage_code_map_by_sample_codes(
- db, tenant_no, codes, reception_source=source
- ).items():
- lab_by_key[(source, code)] = lab_code
- payment_summaries = {}
- if cell_ids:
- from app.services.qc_work_order_fee_summary import (
- build_protocol_payment_summaries,
- )
- payment_summaries = build_protocol_payment_summaries(
- db, cell_ids, tenant_no
- )
- transport_labels = {'kdys': '快递运输', 'ryxd': '人员携带'}
- void_type_labels = build_void_type_label_map(
- db,
- tenant_no,
- [str(item.get('voidType') or '') for item in items],
- )
- for item in items:
- key = (item['source'], item['id'])
- collection = collection_map.get(key)
- sample = sample_map.get(key)
- party = party_map.get(key)
- payment_summary = payment_summaries.get(item['id']) or {}
- transport = transport_by_sample.get(getattr(sample, 'id', ''))
- sample_code = getattr(sample, 'sample_code', '') or ''
- region_manager_name = user_names.get(
- getattr(collection, 'region_manager_user_id', ''), ''
- )
- item.update(
- motherName=item.get('customerName', ''),
- motherPhone=getattr(party, 'mother_phone', '') or '',
- motherIdCard=getattr(party, 'mother_id_card', '') or '',
- fatherName=getattr(party, 'father_name', '') or '',
- newbornSex=getattr(party, 'newborn_sex', '') or '',
- deliveryMode=getattr(party, 'delivery_mode', '') or '',
- newbornWeightGram=getattr(party, 'newborn_weight_gram', None),
- birthOrder=getattr(party, 'birth_order', None),
- newbornDeliveryTime=_fmt_dt(
- getattr(party, 'newborn_delivery_time', None)
- ),
- protocolStatus=item.get('status', ''),
- donationStatus=item.get('status', ''),
- regionName=region_names.get(getattr(collection, 'region_id', ''), ''),
- hospitalName=hospital_names.get(
- getattr(collection, 'hospital_id', ''), ''
- ),
- sampleCollectTime=_fmt_dt(
- getattr(collection, 'sample_collect_time', None)
- ),
- maternalBloodCollectTime=_fmt_dt(
- getattr(collection, 'maternal_blood_collect_time', None)
- ),
- salesUserName=user_names.get(
- getattr(collection, 'sales_user_id', ''), ''
- ),
- collectorUserName=user_names.get(item.get('collectorUserId', ''), ''),
- managerName=region_manager_name,
- regionManagerUserName=region_manager_name,
- sampleCode=sample_code,
- sampleTypeName=type_names.get(
- getattr(sample, 'sample_type_id', ''), ''
- ),
- coStored=bool(getattr(sample, 'is_special', 0)),
- labStorageCode=lab_by_key.get((item['source'], sample_code), ''),
- payMode=payment_summary.get('payMode', ''),
- firstPayTime=payment_summary.get('firstPayTransferTime', ''),
- transportMode=getattr(transport, 'transport_type', '') or '',
- transportModeLabel=transport_labels.get(
- getattr(transport, 'transport_type', '') or '', ''
- ),
- voidTypeLabel=void_type_labels.get(
- str(item.get('voidType') or ''), ''
- ),
- )
- def list_protocols(
- db: Session,
- *,
- ctx: MarketFieldContext,
- user_info: dict,
- page: int = 1,
- page_size: int = 10,
- keyword: Optional[str] = None,
- source: Optional[str] = None,
- protocol_status: Optional[str] = None,
- create_time_start: Optional[str] = None,
- create_time_end: Optional[str] = None,
- sample_collect_time_start: Optional[str] = None,
- sample_collect_time_end: Optional[str] = None,
- ) -> Tuple[List[dict], int]:
- if source and source not in SOURCES:
- raise ValueError('不支持的协议来源')
- search = str(keyword or '').strip()
- def date_value(raw: Optional[str], *, end: bool = False) -> Optional[datetime]:
- text = str(raw or '').strip()
- if not text:
- return None
- try:
- parsed = datetime.strptime(text, '%Y-%m-%d')
- except ValueError as exc:
- raise ValueError('日期格式无效,应为 YYYY-MM-DD') from exc
- return parsed + timedelta(days=1) if end else parsed
- created_from = date_value(create_time_start)
- created_until = date_value(create_time_end, end=True)
- collected_from = date_value(sample_collect_time_start)
- collected_until = date_value(sample_collect_time_end, end=True)
- status = str(protocol_status or '').strip()
- rows = []
- fetch_limit = max(1, page) * min(max(1, page_size), 100)
- total = 0
- if source in (None, SOURCE_CELL):
- query = apply_cell_protocol_scope(
- db.query(BizCollectionProtocol), db, ctx, user_info=user_info
- )
- if status:
- query = query.filter(BizCollectionProtocol.protocol_status == status)
- if created_from:
- query = query.filter(BizCollectionProtocol.create_time >= created_from)
- if created_until:
- query = query.filter(BizCollectionProtocol.create_time < created_until)
- if collected_from or collected_until:
- collection_conditions = [
- BizProtocolCollectionInfo.collection_protocol_id
- == BizCollectionProtocol.id,
- BizProtocolCollectionInfo.tenant_no == ctx.tenant_no,
- BizProtocolCollectionInfo.del_flag == 0,
- ]
- if collected_from:
- collection_conditions.append(
- BizProtocolCollectionInfo.sample_collect_time >= collected_from
- )
- if collected_until:
- collection_conditions.append(
- BizProtocolCollectionInfo.sample_collect_time < collected_until
- )
- query = query.filter(
- db.query(BizProtocolCollectionInfo.id)
- .filter(*collection_conditions)
- .exists()
- .correlate(BizCollectionProtocol)
- )
- if search:
- like = f'%{search}%'
- party_ids = db.query(
- BizProtocolPartyCustomer.collection_protocol_id
- ).filter(
- BizProtocolPartyCustomer.tenant_no == ctx.tenant_no,
- BizProtocolPartyCustomer.del_flag == 0,
- or_(
- BizProtocolPartyCustomer.mother_name.like(like),
- BizProtocolPartyCustomer.mother_id_card.like(like),
- BizProtocolPartyCustomer.mother_phone.like(like),
- ),
- )
- sample_ids = db.query(BizProtocolSample.collection_protocol_id).filter(
- BizProtocolSample.tenant_no == ctx.tenant_no,
- BizProtocolSample.del_flag == 0,
- BizProtocolSample.sample_code.like(like),
- )
- query = query.filter(
- or_(
- BizCollectionProtocol.protocol_no.like(like),
- BizCollectionProtocol.id.in_(party_ids),
- BizCollectionProtocol.id.in_(sample_ids),
- )
- )
- total += int(
- query.order_by(None)
- .with_entities(func.count(BizCollectionProtocol.id))
- .scalar()
- or 0
- )
- cell_rows = (
- query.order_by(
- BizCollectionProtocol.create_time.desc(),
- BizCollectionProtocol.id.desc(),
- )
- .limit(fetch_limit)
- .all()
- )
- names = _party_name_cell(
- db, [row.id for row in cell_rows], ctx.tenant_no
- )
- rows.extend(
- {
- 'id': row.id,
- 'source': SOURCE_CELL,
- 'code': row.protocol_no or '',
- 'protocolNo': row.protocol_no or '',
- 'status': row.protocol_status or '',
- 'collectDate': row.register_date or '',
- 'registerDate': row.register_date or '',
- 'protocolTotalAmount': str(row.protocol_total_amount or ''),
- 'paymentModelVersion': int(
- getattr(row, 'payment_model_version', None) or 1
- ),
- 'voidType': row.void_type or '',
- 'collectorUserId': row.collector_user_id or '',
- 'customerName': names.get(row.id, ''),
- 'createTime': _fmt_dt(row.create_time),
- '_created': row.create_time or datetime.min,
- }
- for row in cell_rows
- )
- if source in (None, SOURCE_DONATION):
- if status or collected_from or collected_until:
- donation_enabled = False
- else:
- donation_enabled = True
- if not donation_enabled:
- query = None
- else:
- query = apply_donation_scope(
- db.query(BizResearchDonation), db, ctx, user_info=user_info
- )
- if query is None:
- donation_rows = []
- else:
- if created_from:
- query = query.filter(BizResearchDonation.create_time >= created_from)
- if created_until:
- query = query.filter(BizResearchDonation.create_time < created_until)
- if query is not None and search:
- like = f'%{search}%'
- party_ids = db.query(
- BizResearchDonationParty.research_donation_id
- ).filter(
- BizResearchDonationParty.tenant_no == ctx.tenant_no,
- BizResearchDonationParty.del_flag == 0,
- or_(
- BizResearchDonationParty.mother_name.like(like),
- BizResearchDonationParty.mother_id_card.like(like),
- BizResearchDonationParty.mother_phone.like(like),
- ),
- )
- sample_ids = db.query(
- BizResearchDonationSample.research_donation_id
- ).filter(
- BizResearchDonationSample.tenant_no == ctx.tenant_no,
- BizResearchDonationSample.del_flag == 0,
- BizResearchDonationSample.sample_code.like(like),
- )
- query = query.filter(
- or_(
- BizResearchDonation.donation_no.like(like),
- BizResearchDonation.id.in_(party_ids),
- BizResearchDonation.id.in_(sample_ids),
- )
- )
- if query is not None:
- total += int(
- query.order_by(None)
- .with_entities(func.count(BizResearchDonation.id))
- .scalar()
- or 0
- )
- donation_rows = (
- query.order_by(
- BizResearchDonation.create_time.desc(),
- BizResearchDonation.id.desc(),
- )
- .limit(fetch_limit)
- .all()
- )
- names = _party_name_donation(
- db, [row.id for row in donation_rows], ctx.tenant_no
- )
- rows.extend(
- {
- 'id': row.id,
- 'source': SOURCE_DONATION,
- 'code': row.donation_no or '',
- 'protocolNo': row.donation_no or '',
- 'donationNo': row.donation_no or '',
- 'status': row.donation_status or '',
- 'collectDate': row.donation_date or '',
- 'donationDate': row.donation_date or '',
- 'customerName': names.get(row.id, ''),
- 'collectorUserId': row.collector_user_id or '',
- 'createTime': _fmt_dt(row.create_time),
- 'monthlyVerifyNo': row.monthly_verify_no or '',
- 'voidType': row.void_type or '',
- 'voidDesc': row.void_desc or '',
- 'consentFormUrls': row.consent_form_urls or '',
- 'infectiousDiseaseReportUrls': row.infectious_disease_report_urls or '',
- 'attachmentUrls': row.attachment_urls or '',
- 'remark': row.remark or '',
- '_created': row.create_time or datetime.min,
- }
- for row in donation_rows
- )
- rows.sort(
- key=lambda item: (item['_created'], item['id']), reverse=True
- )
- safe_page = max(1, page)
- safe_size = min(max(1, page_size), 100)
- page_rows = rows[(safe_page - 1) * safe_size : safe_page * safe_size]
- for item in page_rows:
- item.pop('_created', None)
- _enrich_protocol_cards(db, page_rows, ctx.tenant_no)
- return page_rows, total
- def _attach_sample_eta(
- sample_dicts: List[dict],
- transport_by_sample_id: dict,
- ) -> List[dict]:
- for item in sample_dicts:
- transport = transport_by_sample_id.get(str(item.get('id') or ''))
- eta = ''
- if transport is not None:
- eta = cell_crud._fmt_dt(getattr(transport, 'eta_time', None))
- item['etaTime'] = eta
- item['transport'] = {'etaTime': eta} if eta else None
- return sample_dicts
- def _attach_sample_freight(
- db: Session,
- sample_dicts: List[dict],
- *,
- tenant_no: str,
- sample_source: str,
- ) -> List[dict]:
- from app.crud import sample_freight as sample_freight_svc
- ids = [str(item.get('id') or '').strip() for item in sample_dicts if item.get('id')]
- freight_map = sample_freight_svc.freight_map_for_samples(
- db,
- tenant_no=tenant_no,
- sample_source=sample_source,
- sample_ids=ids,
- money_as_string=True,
- )
- for item in sample_dicts:
- sid = str(item.get('id') or '').strip()
- freight = freight_map.get(sid)
- if freight:
- item['freight'] = freight
- item['freightId'] = freight.get('id') or ''
- else:
- item['freight'] = None
- item['freightId'] = ''
- return sample_dicts
- def _assert_sample_freight_targets(
- db: Session,
- *,
- protocol_id: Optional[str],
- samples: List[dict],
- ctx: MarketFieldContext,
- user_info: dict,
- ) -> None:
- """协议保存只允许保留原批次,或关联当前市场范围可访问的批次。"""
- existing_samples = (
- cell_crud.list_samples(db, protocol_id, ctx.tenant_no)
- if protocol_id
- else []
- )
- freight_by_sample = sample_freight_crud.freight_map_for_samples(
- db,
- tenant_no=ctx.tenant_no,
- sample_source=SOURCE_CELL,
- sample_ids=[row.id for row in existing_samples],
- )
- current_by_code = {
- str(row.sample_code or '').strip(): str(
- (freight_by_sample.get(row.id) or {}).get('id') or ''
- )
- for row in existing_samples
- if str(row.sample_code or '').strip()
- }
- checked_targets = set()
- for sample in samples:
- code = str(sample.get('sample_code') or sample.get('sampleCode') or '').strip()
- target_id = str(
- sample.get('freight_id') or sample.get('freightId') or ''
- ).strip()
- draft = sample.get('freight_draft') or sample.get('freightDraft')
- current_id = current_by_code.get(code, '')
- if current_id and (draft or (target_id and target_id != current_id)):
- raise ValueError('样本已关联运费批次,请先通过运费功能取消关联')
- if not target_id or target_id == current_id or target_id in checked_targets:
- continue
- market_freight.assert_link_target_access(
- db,
- target_id,
- ctx=ctx,
- user_info=user_info,
- )
- checked_targets.add(target_id)
- def _market_sample_payment_payload(
- db: Session,
- *,
- row: BizCollectionProtocol,
- body: MarketProtocolBody,
- ctx: MarketFieldContext,
- saved_samples: List[BizProtocolSample],
- ) -> List[dict]:
- """忽略客户端嵌套转账和样本 ID,只同步当前协议的缴费方案与期次。"""
- raw_payments = [
- item.model_dump(by_alias=True, exclude_none=False)
- for item in (body.sample_payments or [])
- ]
- sample_by_code = {
- str(sample.sample_code or '').strip(): sample
- for sample in saved_samples
- if str(sample.sample_code or '').strip()
- }
- current_payments = (
- db.query(BizProtocolSamplePayment)
- .filter(
- BizProtocolSamplePayment.collection_protocol_id == row.id,
- BizProtocolSamplePayment.tenant_no == ctx.tenant_no,
- BizProtocolSamplePayment.del_flag == 0,
- )
- .all()
- )
- payment_by_id = {payment.id: payment for payment in current_payments}
- payment_by_code = {
- str(payment.sample_code or '').strip(): payment
- for payment in current_payments
- }
- payment_ids = [payment.id for payment in current_payments]
- schedules = (
- db.query(BizProtocolPaymentSchedule)
- .filter(
- BizProtocolPaymentSchedule.sample_payment_id.in_(payment_ids or ['']),
- BizProtocolPaymentSchedule.tenant_no == ctx.tenant_no,
- BizProtocolPaymentSchedule.del_flag == 0,
- )
- .all()
- )
- schedule_by_payment_no = {
- (schedule.sample_payment_id, int(schedule.schedule_no or 0)): schedule
- for schedule in schedules
- }
- schedule_ids = [schedule.id for schedule in schedules]
- transfers = (
- db.query(BizProtocolPaymentTransfer)
- .filter(
- BizProtocolPaymentTransfer.schedule_id.in_(schedule_ids or ['']),
- BizProtocolPaymentTransfer.tenant_no == ctx.tenant_no,
- BizProtocolPaymentTransfer.del_flag == 0,
- )
- .all()
- )
- transfers_by_schedule = {}
- for transfer in transfers:
- transfers_by_schedule.setdefault(transfer.schedule_id, []).append(transfer)
- seen_codes = set()
- result = []
- for raw in raw_payments:
- code = str(raw.get('sampleCode') or '').strip()
- if not code or code not in sample_by_code:
- raise ValueError('样本缴费方案不属于当前协议样本')
- if code in seen_codes:
- raise ValueError('同一样本不能重复配置缴费方案')
- seen_codes.add(code)
- supplied_id = str(raw.get('id') or '').strip()
- current = payment_by_id.get(supplied_id) if supplied_id else payment_by_code.get(code)
- if supplied_id and (
- current is None or str(current.sample_code or '').strip() != code
- ):
- raise ValueError('样本缴费方案不存在或不属于当前协议样本')
- if current and payment_by_code.get(code) not in (None, current):
- raise ValueError('样本缴费方案与当前协议样本不一致')
- if current:
- raw['id'] = current.id
- else:
- raw.pop('id', None)
- raw['protocolSampleId'] = sample_by_code[code].id
- incoming_schedules = raw.get('schedules') or []
- incoming_nos = {
- int(schedule.get('scheduleNo') or index + 1)
- for index, schedule in enumerate(incoming_schedules)
- }
- current_nos = {
- number
- for payment_id, number in schedule_by_payment_no
- if current and payment_id == current.id
- }
- current_transfers = [
- transfer
- for schedule in schedules
- if current and schedule.sample_payment_id == current.id
- for transfer in transfers_by_schedule.get(schedule.id, [])
- ]
- if current and incoming_nos != current_nos and current_transfers:
- raise ValueError('存在转账记录时不可调整缴费期次结构')
- sanitized_schedules = []
- for index, schedule_raw in enumerate(incoming_schedules):
- schedule = dict(schedule_raw)
- number = int(schedule.get('scheduleNo') or index + 1)
- existing_schedule = (
- schedule_by_payment_no.get((current.id, number))
- if current
- else None
- )
- schedule.pop('id', None)
- schedule['scheduleStatus'] = (
- existing_schedule.schedule_status
- if existing_schedule
- else 'open'
- )
- schedule['transfers'] = (
- [
- {'id': transfer.id}
- for transfer in transfers_by_schedule.get(existing_schedule.id, [])
- ]
- if existing_schedule
- else []
- )
- sanitized_schedules.append(schedule)
- raw['schedules'] = sanitized_schedules
- result.append(raw)
- return result
- def _persist_sample_payments_if_any(
- db: Session,
- *,
- row: BizCollectionProtocol,
- body: MarketProtocolBody,
- ctx: MarketFieldContext,
- saved_samples: List[BizProtocolSample],
- ) -> None:
- """按数据库版本互斥保存 v1 整单缴费或 v2 按样本缴费方案。"""
- if body.source != SOURCE_CELL:
- if (
- body.sample_payments is not None
- or body.payment is not None
- or body.payment_details is not None
- ):
- raise ValueError('科研捐赠不支持缴费方案')
- return
- version = int(getattr(row, 'payment_model_version', None) or 1)
- if version < 2:
- if body.sample_payments is not None:
- raise ValueError('旧版 v1 协议不支持 samplePayments 样本缴费方案')
- if body.payment is None and body.payment_details is None:
- return
- if body.payment is None and body.payment_details is not None:
- existing_payment = cell_crud.get_payment_by_protocol_id(
- db, row.id, ctx.tenant_no
- )
- if existing_payment is None:
- raise ValueError('提交缴费明细前必须先提交缴费主表方案')
- payment_details = _validate_v1_payment_detail_attachments(
- db,
- body.payment_details,
- row=row,
- ctx=ctx,
- )
- cell_crud.save_payment_with_details(
- db,
- row.id,
- ctx.tenant_no,
- ctx.user_id,
- payment=body.payment.model_dump(
- by_alias=False, exclude_unset=True, mode='json'
- ) if body.payment is not None else None,
- payment_details=payment_details,
- commit=False,
- )
- return
- if body.payment is not None or body.payment_details is not None:
- raise ValueError('新版 v2 协议不支持 payment/paymentDetails 整单缴费方案')
- if body.sample_payments is None:
- return
- raw = _market_sample_payment_payload(
- db,
- row=row,
- body=body,
- ctx=ctx,
- saved_samples=saved_samples,
- )
- psp_crud.save_sample_payments_for_protocol(
- db,
- collection_protocol_id=row.id,
- tenant_no=ctx.tenant_no,
- user_id=ctx.user_id,
- sample_payments=raw,
- )
- def _cell_detail(db: Session, row: BizCollectionProtocol, tenant_no: str) -> dict:
- party = cell_crud.get_party_customer(db, row.id, tenant_no)
- collection = cell_crud.get_collection_info(db, row.id, tenant_no)
- samples = cell_crud.list_samples(db, row.id, tenant_no)
- payment = cell_crud.get_payment_by_protocol_id(db, row.id, tenant_no)
- transports = cell_crud.list_sample_transports(db, row.id, tenant_no)
- transport_by_sample_id = {str(t.sample_id): t for t in transports}
- # qc_task 先于 qc_work_order 初始化,避免两者历史互相导入时拿到半初始化模块。
- from app.crud import qc_task as _qc_task_svc # noqa: F401
- web_bundle = cell_crud.get_protocol_detail_bundle(db, row.id, tenant_no) or {}
- web_samples = web_bundle.get('samples') or []
- web_sample_by_id = {
- str(item.get('id') or ''): item for item in web_samples if item.get('id')
- }
- web_sample_by_code = {
- str(item.get('sampleCode') or ''): item
- for item in web_samples
- if item.get('sampleCode')
- }
- sample_dicts = _attach_sample_freight(
- db,
- _attach_sample_eta(
- [cell_crud._sample_to_dict(item) for item in samples],
- transport_by_sample_id,
- ),
- tenant_no=tenant_no,
- sample_source=SOURCE_CELL,
- )
- lifecycle_keys = (
- 'sampleReception',
- 'productionReception',
- 'qcInfo',
- 'processQcInfoList',
- 'productionInfo',
- 'storageReleaseInfo',
- )
- for item in sample_dicts:
- web_sample = web_sample_by_id.get(str(item.get('id') or '')) or web_sample_by_code.get(
- str(item.get('sampleCode') or '')
- )
- if not web_sample:
- continue
- for key in lifecycle_keys:
- item[key] = web_sample.get(key)
- version = int(getattr(row, 'payment_model_version', None) or 1)
- sample_payments = []
- if version >= 2:
- sample_payments = psp_crud.get_sample_payments_bundle(
- db,
- collection_protocol_id=row.id,
- tenant_no=tenant_no,
- money_as_string=True,
- )
- source_presign_id = getattr(row, 'source_presign_order_id', None)
- storage_cert = cell_crud.get_storage_cert(db, row.id, tenant_no)
- storage_cert_dict = (
- cell_crud._storage_cert_to_dict(storage_cert) if storage_cert else None
- )
- if storage_cert_dict:
- hid = str(storage_cert_dict.get('handlerUserId') or '').strip()
- if hid:
- hu = (
- db.query(SysUser)
- .filter(
- SysUser.id == hid,
- SysUser.tenant_no == tenant_no,
- SysUser.del_flag == 0,
- )
- .first()
- )
- storage_cert_dict['handlerUserName'] = (
- (hu.nick_name or '') if hu else ''
- )
- else:
- storage_cert_dict['handlerUserName'] = ''
- delivery_dt = getattr(party, 'newborn_delivery_time', None) if party else None
- fullmoon = cell_crud.get_fullmoon_todo(db, row.id, tenant_no)
- fullmoon_dict = cell_crud._fullmoon_todo_detail_dict(
- fullmoon, newborn_delivery_time=delivery_dt
- )
- hid2 = str(fullmoon_dict.get('handlerUserId') or '').strip()
- if hid2:
- hu2 = (
- db.query(SysUser)
- .filter(
- SysUser.id == hid2,
- SysUser.tenant_no == tenant_no,
- SysUser.del_flag == 0,
- )
- .first()
- )
- fullmoon_dict['handlerUserName'] = (hu2.nick_name or '') if hu2 else ''
- else:
- fullmoon_dict['handlerUserName'] = ''
- audits, _ = cell_crud.list_audits(
- db,
- page=1,
- page_size=100,
- collection_protocol_id=row.id,
- tenant_no=tenant_no,
- )
- basic_dict = cell_crud._protocol_to_dict(row)
- basic_dict['protocolTotalAmount'] = _fixed_money(row.protocol_total_amount)
- payment_dict = cell_crud._payment_to_dict(payment) if payment else None
- if payment_dict is not None:
- payment_dict['contractAmount'] = _fixed_money(payment.contract_amount)
- payment_details = list(web_bundle.get('paymentDetails') or [])
- if payment:
- detail_models = cell_crud.list_payment_details(db, payment.id, tenant_no)
- model_by_id = {str(item.id): item for item in detail_models}
- for item in payment_details:
- model = model_by_id.get(str(item.get('id') or ''))
- if model:
- for key, attr in (
- ('dueAmount', 'due_amount'),
- ('transferAmount', 'transfer_amount'),
- ('refundAmount', 'refund_amount'),
- ):
- item[key] = _fixed_money(getattr(model, attr, None))
- sample_model_by_id = {str(item.id): item for item in samples}
- for item in sample_dicts:
- model = sample_model_by_id.get(str(item.get('id') or ''))
- if model:
- item['price'] = _fixed_money(model.price)
- item['originalPrice'] = _fixed_money(model.original_price)
- detail = {
- 'source': SOURCE_CELL,
- 'basic': basic_dict,
- 'collection': cell_crud._collection_info_to_dict(collection)
- if collection
- else None,
- 'customer': cell_crud._party_customer_to_dict(party) if party else None,
- 'samples': sample_dicts,
- 'payment': payment_dict,
- 'paymentModelVersion': version,
- 'samplePayments': sample_payments,
- 'paymentDetails': payment_details,
- 'depositTransfers': psp_crud.list_deposit_transfers_for_protocol(
- db,
- source_presign_order_id=source_presign_id,
- tenant_no=tenant_no,
- money_as_string=True,
- ),
- 'presignDepositHint': psp_crud.get_presign_deposit_hint(
- db,
- source_presign_order_id=source_presign_id,
- tenant_no=tenant_no,
- money_as_string=True,
- ),
- 'storageCert': storage_cert_dict,
- 'fullmoonTodo': fullmoon_dict,
- 'audits': audits,
- 'statusChanges': cell_crud.list_protocol_status_changes(
- db,
- collection_protocol_id=row.id,
- tenant_no=tenant_no,
- ),
- 'attachments': {
- 'protocolAttachments': row.protocol_attachments or '',
- 'consentFormUrls': row.consent_form_urls or '',
- 'healthSurveyFormUrls': row.health_survey_form_urls or '',
- },
- }
- _enrich_transfer_type_labels(db, detail, tenant_no)
- _enrich_protocol_detail_labels(db, detail, tenant_no)
- return detail
- def _donation_detail(
- db: Session, row: BizResearchDonation, tenant_no: str
- ) -> dict:
- # 与 Web 查看弹窗共用同一份详情 DTO,避免移动端再次拼装时遗漏生命周期字段。
- from app.crud import qc_task as _qc_task_svc # noqa: F401
- web_bundle = donation_crud.get_donation_detail(db, row.id, tenant_no) or {}
- basic = dict(web_bundle.get('donation') or donation_crud._donation_to_dict(row))
- basic['convertTime'] = basic.get('convertTime') or basic.get('toStorageTime') or ''
- basic['convertDesc'] = basic.get('convertDesc') or basic.get('toStorageDesc') or ''
- audits = donation_crud.list_audits(
- db,
- tenant_no=tenant_no,
- page=1,
- page_size=100,
- research_donation_id=row.id,
- )
- detail = {
- 'source': SOURCE_DONATION,
- 'basic': basic,
- 'collection': web_bundle.get('collection'),
- 'customer': web_bundle.get('party'),
- 'samples': list(web_bundle.get('samples') or []),
- 'audits': list(audits.get('list') or []),
- 'statusChanges': donation_crud.list_donation_status_changes(
- db,
- research_donation_id=row.id,
- tenant_no=tenant_no,
- ),
- 'attachments': {
- 'consentFormUrls': basic.get('consentFormUrls') or '',
- 'attachmentUrls': basic.get('attachmentUrls') or '',
- 'infectiousDiseaseReportUrls': basic.get('infectiousDiseaseReportUrls') or '',
- },
- }
- _enrich_protocol_detail_labels(db, detail, tenant_no)
- return detail
- def _enrich_transfer_type_labels(
- db: Session, detail: dict, tenant_no: str
- ) -> None:
- """批量解析协议内所有转账方式;未知字典值保留原编码。"""
- label_map = {
- str(item.get('value') or ''): str(item.get('label') or '')
- for item in _dict_options(db, tenant_no, 'zzfs')
- }
- def apply(row: dict) -> None:
- code = str(row.get('transferType') or '').strip()
- if code:
- row['transferTypeLabel'] = (
- label_map.get(code)
- or str(row.get('transferTypeLabel') or '').strip()
- or code
- )
- for row in detail.get('depositTransfers') or []:
- apply(row)
- for row in detail.get('paymentDetails') or []:
- apply(row)
- for payment in detail.get('samplePayments') or []:
- for schedule in payment.get('schedules') or []:
- for row in schedule.get('transfers') or []:
- apply(row)
- def _enrich_protocol_detail_labels(
- db: Session, detail: dict, tenant_no: str
- ) -> None:
- """补齐 Web 详情使用的主数据名称,移动端不直接展示内部 ID。"""
- basic = detail.get('basic') or {}
- collection = detail.get('collection') or {}
- samples = detail.get('samples') or []
- basic['voidTypeLabel'] = resolve_void_type_label(
- db,
- tenant_no,
- str(basic.get('voidType') or ''),
- )
- region_id = str(collection.get('regionId') or '').strip()
- hospital_id = str(collection.get('hospitalId') or '').strip()
- if region_id:
- region = (
- db.query(BizCollectionRegion)
- .filter(
- BizCollectionRegion.id == region_id,
- BizCollectionRegion.tenant_no == tenant_no,
- )
- .first()
- )
- collection['regionName'] = region.region_name if region else ''
- if hospital_id:
- hospital = (
- db.query(BizCollectionHospital)
- .filter(
- BizCollectionHospital.id == hospital_id,
- BizCollectionHospital.tenant_no == tenant_no,
- )
- .first()
- )
- collection['hospitalName'] = hospital.hospital_name if hospital else ''
- user_ids = {
- str(value or '').strip()
- for value in (
- basic.get('collectorUserId'),
- collection.get('salesUserId'),
- collection.get('regionManagerUserId'),
- )
- if str(value or '').strip()
- }
- user_names = {
- row.id: row.nick_name or ''
- for row in db.query(SysUser).filter(
- SysUser.id.in_(user_ids or ['']),
- SysUser.tenant_no == tenant_no,
- )
- }
- basic['collectorUserName'] = user_names.get(
- str(basic.get('collectorUserId') or ''), ''
- )
- collection['salesUserName'] = user_names.get(
- str(collection.get('salesUserId') or ''), ''
- )
- collection['regionManagerUserName'] = user_names.get(
- str(collection.get('regionManagerUserId') or ''), ''
- )
- type_ids = {
- str(item.get('sampleTypeId') or '').strip()
- for item in samples
- if str(item.get('sampleTypeId') or '').strip()
- }
- type_names = {
- row.id: row.type_name or ''
- for row in db.query(BizCellSampleType).filter(
- BizCellSampleType.id.in_(type_ids or ['']),
- or_(
- BizCellSampleType.tenant_no == tenant_no,
- BizCellSampleType.tenant_no == '',
- ),
- )
- }
- unit_ids = {
- str(item.get('unitId') or '').strip()
- for item in samples
- if str(item.get('unitId') or '').strip()
- }
- unit_names = {
- row.id: row.unit_name or ''
- for row in db.query(BizItemUnit).filter(
- BizItemUnit.id.in_(unit_ids or ['']),
- BizItemUnit.tenant_no == tenant_no,
- )
- }
- for item in samples:
- item['sampleTypeName'] = type_names.get(
- str(item.get('sampleTypeId') or ''),
- str(item.get('sampleTypeName') or ''),
- )
- item['unitName'] = unit_names.get(
- str(item.get('unitId') or ''),
- str(item.get('unitName') or ''),
- )
- def get_protocol(
- db: Session,
- *,
- source: Optional[str],
- protocol_id: str,
- ctx: MarketFieldContext,
- user_info: dict,
- ) -> dict:
- if source == SOURCE_CELL:
- row = assert_cell_protocol_access(
- db, protocol_id, ctx, user_info=user_info
- )
- return _cell_detail(db, row, ctx.tenant_no)
- if source == SOURCE_DONATION:
- row = assert_donation_access(db, protocol_id, ctx, user_info=user_info)
- return _donation_detail(db, row, ctx.tenant_no)
- if source is None:
- cell = (
- apply_cell_protocol_scope(
- db.query(BizCollectionProtocol), db, ctx, user_info=user_info
- )
- .filter(BizCollectionProtocol.id == protocol_id)
- .first()
- )
- if cell is not None:
- return _cell_detail(db, cell, ctx.tenant_no)
- donation = (
- apply_donation_scope(
- db.query(BizResearchDonation), db, ctx, user_info=user_info
- )
- .filter(BizResearchDonation.id == protocol_id)
- .first()
- )
- if donation is not None:
- return _donation_detail(db, donation, ctx.tenant_no)
- raise HTTPException(status_code=404, detail='数据不存在或无权访问')
- raise ValueError('不支持的协议来源')
- def _lock_cell_protocol_access(
- db: Session,
- protocol_id: str,
- ctx: MarketFieldContext,
- *,
- user_info: dict,
- ) -> BizCollectionProtocol:
- row = (
- apply_cell_protocol_scope(
- db.query(BizCollectionProtocol), db, ctx, user_info=user_info
- )
- .filter(BizCollectionProtocol.id == protocol_id)
- .populate_existing()
- .with_for_update()
- .first()
- )
- if row is None:
- raise HTTPException(status_code=404, detail='数据不存在或无权访问')
- return row
- def _lock_donation_access(
- db: Session,
- protocol_id: str,
- ctx: MarketFieldContext,
- *,
- user_info: dict,
- ) -> BizResearchDonation:
- row = (
- apply_donation_scope(
- db.query(BizResearchDonation), db, ctx, user_info=user_info
- )
- .filter(BizResearchDonation.id == protocol_id)
- .populate_existing()
- .with_for_update()
- .first()
- )
- if row is None:
- raise HTTPException(status_code=404, detail='数据不存在或无权访问')
- return row
- def audit_protocol(
- db: Session,
- *,
- source: str,
- protocol_id: str,
- audit_status: str,
- audit_remark: Optional[str],
- ctx: MarketFieldContext,
- user_info: dict,
- ) -> dict:
- if source == SOURCE_CELL:
- row = assert_cell_protocol_access(db, protocol_id, ctx, user_info=user_info)
- if 'audit' not in allowed_actions('protocol', status=row.protocol_status or ''):
- raise ValueError('当前状态不可审核')
- cell_crud.create_audit(
- db,
- collection_protocol_id=protocol_id,
- audit_status=audit_status,
- audit_remark=audit_remark,
- tenant_no=ctx.tenant_no,
- user_id=ctx.user_id,
- )
- return {'id': protocol_id, 'source': source, 'protocolNo': row.protocol_no or ''}
- if source == SOURCE_DONATION:
- row = assert_donation_access(db, protocol_id, ctx, user_info=user_info)
- if 'audit' not in allowed_actions('protocol', status=row.donation_status or ''):
- raise ValueError('当前状态不可审核')
- donation_crud.submit_audit(
- db,
- protocol_id,
- audit_status=audit_status,
- audit_remark=audit_remark,
- tenant_no=ctx.tenant_no,
- user_id=ctx.user_id,
- )
- return {'id': protocol_id, 'source': source, 'donationNo': row.donation_no or ''}
- raise ValueError('不支持的协议来源')
- def delete_protocol(
- db: Session,
- *,
- source: str,
- protocol_id: str,
- ctx: MarketFieldContext,
- user_info: dict,
- ) -> None:
- if source == SOURCE_CELL:
- _lock_cell_protocol_access(
- db, protocol_id, ctx, user_info=user_info
- )
- cell_crud.soft_delete_collection_protocol(
- db, protocol_id, ctx.tenant_no, user_id=ctx.user_id
- )
- return
- if source == SOURCE_DONATION:
- _lock_donation_access(
- db, protocol_id, ctx, user_info=user_info
- )
- donation_crud.delete_donation(
- db, protocol_id, ctx.tenant_no, user_id=ctx.user_id
- )
- return
- raise ValueError('不支持的协议来源')
- def create_protocol(
- db: Session,
- *,
- body: MarketProtocolBody,
- ctx: MarketFieldContext,
- user_info: dict,
- ) -> dict:
- if body.source == SOURCE_DONATION and (
- body.sample_payments is not None
- or body.payment is not None
- or body.payment_details is not None
- ):
- raise ValueError('科研捐赠不支持缴费方案')
- customer, collection, samples, attachments = _normalize_write(
- db, body=body, ctx=ctx
- )
- _assert_customer_link_policy(db, customer, ctx=ctx, user_info=user_info)
- dept_id = _current_dept_id(user_info)
- try:
- if body.source == SOURCE_CELL:
- _assert_sample_freight_targets(
- db,
- protocol_id=None,
- samples=samples,
- ctx=ctx,
- user_info=user_info,
- )
- row = cell_crud.create_collection_protocol(
- db,
- protocol_no=body.protocol_no or '',
- # 对齐 Web 新增弹窗:默认生效中,移动端不接收客户端改状态
- protocol_status='sxz',
- protocol_attachments=attachments.get('protocol_attachments'),
- consent_form_urls=attachments.get('consent_form_urls'),
- health_survey_form_urls=attachments.get(
- 'health_survey_form_urls'
- ),
- register_date=body.register_date,
- collector_user_id=ctx.user_id
- if ctx.scope_kind == 'specialist'
- else body.collector_user_id,
- protocol_total_amount=body.protocol_total_amount,
- remark=body.remark,
- tenant_no=ctx.tenant_no,
- user_id=ctx.user_id,
- create_dept_id=dept_id,
- commit=False,
- )
- attachments = _validate_mobile_attachments(
- db,
- attachments,
- ctx=ctx,
- creating=True,
- business_id=row.id,
- )
- row.protocol_attachments = attachments.get('protocol_attachments')
- row.consent_form_urls = attachments.get('consent_form_urls')
- row.health_survey_form_urls = attachments.get(
- 'health_survey_form_urls'
- )
- db.add(row)
- cell_crud.upsert_party_customer(
- db,
- row.id,
- ctx.tenant_no,
- ctx.user_id,
- commit=False,
- create_dept_id=dept_id,
- **customer,
- )
- cell_crud.upsert_collection_info(
- db,
- row.id,
- ctx.tenant_no,
- ctx.user_id,
- commit=False,
- create_dept_id=dept_id,
- **collection,
- )
- saved_samples = cell_crud.save_samples(
- db,
- row.id,
- ctx.tenant_no,
- ctx.user_id,
- samples,
- create_dept_id=dept_id,
- commit=False,
- )
- _persist_sample_payments_if_any(
- db,
- row=row,
- body=body,
- ctx=ctx,
- saved_samples=saved_samples,
- )
- assert_cell_protocol_access(db, row.id, ctx, user_info=user_info)
- db.commit()
- db.refresh(row)
- return _cell_detail(db, row, ctx.tenant_no)
- if body.sample_payments:
- raise ValueError('科研捐赠不支持样本缴费方案')
- data = {
- 'donation_no': body.donation_no or '',
- # 对齐 Web 新增弹窗:默认生效中,移动端不接收客户端改状态
- 'donation_status': 'sxz',
- 'donation_date': body.donation_date,
- 'collector_user_id': ctx.user_id
- if ctx.scope_kind == 'specialist'
- else body.collector_user_id,
- 'remark': body.remark,
- 'party': customer,
- 'collection': collection,
- 'samples': samples,
- 'consent_form_urls': attachments.get('consent_form_urls'),
- 'attachment_urls': attachments.get('attachment_urls'),
- 'infectious_disease_report_urls': attachments.get(
- 'infectious_disease_report_urls'
- ),
- }
- row = donation_crud.create_donation(
- db,
- data=data,
- tenant_no=ctx.tenant_no,
- user_id=ctx.user_id,
- create_dept_id=dept_id,
- commit=False,
- sync_customers=False,
- )
- attachments = _validate_mobile_attachments(
- db,
- attachments,
- ctx=ctx,
- creating=True,
- business_id=row.id,
- )
- row.consent_form_urls = attachments.get('consent_form_urls')
- row.attachment_urls = attachments.get('attachment_urls')
- row.infectious_disease_report_urls = attachments.get(
- 'infectious_disease_report_urls'
- )
- db.add(row)
- assert_donation_access(db, row.id, ctx, user_info=user_info)
- db.commit()
- db.refresh(row)
- return _donation_detail(db, row, ctx.tenant_no)
- except Exception:
- db.rollback()
- raise
- def update_protocol(
- db: Session,
- *,
- source: str,
- protocol_id: str,
- body: MarketProtocolBody,
- ctx: MarketFieldContext,
- user_info: dict,
- ) -> dict:
- if body.source != source:
- raise ValueError('请求来源与路径来源不一致')
- if source == SOURCE_DONATION and (
- body.sample_payments is not None
- or body.payment is not None
- or body.payment_details is not None
- ):
- raise ValueError('科研捐赠不支持缴费方案')
- dept_id = _current_dept_id(user_info)
- try:
- if source == SOURCE_CELL:
- row = _lock_cell_protocol_access(
- db, protocol_id, ctx, user_info=user_info
- )
- current_collection = cell_crud.get_collection_info(
- db, row.id, ctx.tenant_no
- )
- current_party = cell_crud.get_party_customer(
- db, row.id, ctx.tenant_no
- )
- current_samples = cell_crud.list_samples(
- db, row.id, ctx.tenant_no
- )
- customer, collection, samples, attachments = _normalize_write(
- db,
- body=body,
- ctx=ctx,
- current_collection=current_collection,
- current_samples=current_samples,
- )
- if body.samples is not None:
- _assert_sample_freight_targets(
- db,
- protocol_id=row.id,
- samples=samples,
- ctx=ctx,
- user_info=user_info,
- )
- attachments = _validate_mobile_attachments(
- db,
- attachments,
- ctx=ctx,
- creating=False,
- existing={
- 'protocol_attachments': row.protocol_attachments,
- 'consent_form_urls': row.consent_form_urls,
- 'health_survey_form_urls': row.health_survey_form_urls,
- },
- business_id=row.id,
- )
- _assert_customer_link_policy(
- db, customer, ctx=ctx, user_info=user_info
- )
- cell_crud.update_collection_protocol(
- db,
- row.id,
- tenant_no=ctx.tenant_no,
- user_id=ctx.user_id,
- protocol_no=body.protocol_no,
- protocol_attachments=attachments.get('protocol_attachments'),
- consent_form_urls=attachments.get('consent_form_urls'),
- health_survey_form_urls=attachments.get(
- 'health_survey_form_urls'
- ),
- register_date=body.register_date,
- collector_user_id=ctx.user_id
- if ctx.scope_kind == 'specialist'
- else body.collector_user_id,
- protocol_total_amount=body.protocol_total_amount,
- remark=body.remark,
- commit=False,
- )
- if body.customer is not None:
- if current_party is not None:
- customer = {
- **cell_crud._normalize_fields(
- cell_crud._party_customer_to_dict(current_party)
- ),
- **customer,
- }
- for metadata_key in (
- 'id',
- 'collection_protocol_id',
- 'tenant_no',
- 'order_num',
- 'create_time',
- 'update_time',
- ):
- customer.pop(metadata_key, None)
- newborn_delivery_time = customer.get(
- 'newborn_delivery_time'
- )
- if isinstance(newborn_delivery_time, str):
- if newborn_delivery_time.strip():
- customer['newborn_delivery_time'] = parse_datetime(
- newborn_delivery_time
- )
- else:
- customer.pop('newborn_delivery_time', None)
- cell_crud.upsert_party_customer(
- db,
- row.id,
- ctx.tenant_no,
- ctx.user_id,
- commit=False,
- create_dept_id=dept_id,
- **customer,
- )
- if body.collection is not None:
- cell_crud.upsert_collection_info(
- db,
- row.id,
- ctx.tenant_no,
- ctx.user_id,
- commit=False,
- create_dept_id=dept_id,
- **collection,
- )
- saved_samples = current_samples
- if body.samples is not None:
- saved_samples = cell_crud.save_samples(
- db,
- row.id,
- ctx.tenant_no,
- ctx.user_id,
- samples,
- create_dept_id=dept_id,
- commit=False,
- )
- _persist_sample_payments_if_any(
- db,
- row=row,
- body=body,
- ctx=ctx,
- saved_samples=saved_samples,
- )
- row = assert_cell_protocol_access(
- db, row.id, ctx, user_info=user_info
- )
- db.commit()
- db.refresh(row)
- return _cell_detail(db, row, ctx.tenant_no)
- row = _lock_donation_access(
- db, protocol_id, ctx, user_info=user_info
- )
- current_collection = donation_crud.get_collection(
- db, row.id, ctx.tenant_no
- )
- current_party = donation_crud.get_party(db, row.id, ctx.tenant_no)
- current_samples = donation_crud.list_samples(
- db, row.id, ctx.tenant_no
- )
- customer, collection, samples, attachments = _normalize_write(
- db,
- body=body,
- ctx=ctx,
- current_collection=current_collection,
- current_samples=current_samples,
- )
- attachments = _validate_mobile_attachments(
- db,
- attachments,
- ctx=ctx,
- creating=False,
- existing={
- 'consent_form_urls': row.consent_form_urls,
- 'attachment_urls': row.attachment_urls,
- 'infectious_disease_report_urls': row.infectious_disease_report_urls,
- },
- business_id=row.id,
- )
- _assert_customer_link_policy(
- db, customer, ctx=ctx, user_info=user_info
- )
- if body.customer is not None and current_party is not None:
- customer = {
- **donation_crud._normalize_fields(
- donation_crud._party_to_dict(current_party)
- ),
- **customer,
- }
- if body.collection is not None and current_collection is not None:
- collection = {
- **donation_crud._normalize_fields(
- donation_crud._collection_to_dict(current_collection)
- ),
- **collection,
- }
- data = {
- 'donation_no': body.donation_no,
- 'donation_date': body.donation_date,
- 'collector_user_id': ctx.user_id
- if ctx.scope_kind == 'specialist'
- else body.collector_user_id,
- 'remark': body.remark,
- 'consent_form_urls': attachments.get('consent_form_urls'),
- 'attachment_urls': attachments.get('attachment_urls'),
- 'infectious_disease_report_urls': attachments.get(
- 'infectious_disease_report_urls'
- ),
- }
- if body.customer is not None:
- data['party'] = customer
- if body.collection is not None:
- data['collection'] = collection
- if body.samples is not None:
- data['samples'] = samples
- data = {key: value for key, value in data.items() if value is not None}
- row = donation_crud.update_donation(
- db,
- row.id,
- data=data,
- tenant_no=ctx.tenant_no,
- user_id=ctx.user_id,
- create_dept_id=dept_id,
- commit=False,
- sync_customers=False,
- )
- row = assert_donation_access(db, row.id, ctx, user_info=user_info)
- db.commit()
- db.refresh(row)
- return _donation_detail(db, row, ctx.tenant_no)
- except Exception:
- db.rollback()
- raise
|