| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290 |
- """移动市场运费批次:按现场范围过滤明细并执行原子写入。"""
- from __future__ import annotations
- from datetime import datetime, timedelta
- from decimal import Decimal, InvalidOperation
- from typing import Any, Dict, List, Optional, Tuple
- from fastapi import HTTPException
- from sqlalchemy import and_, exists, func, literal, or_, select
- from sqlalchemy.exc import IntegrityError
- from sqlalchemy.orm import Session, aliased
- from app.core.data_scope import apply_data_scope
- from app.core.utils import generate_uuid
- from app.models.models import (
- BizCellSampleType,
- BizCollectionProtocol,
- BizCollectionRegionStaff,
- BizProtocolCollectionInfo,
- BizProtocolPartyCustomer,
- BizProtocolSample,
- BizSampleFreight,
- BizSampleFreightItem,
- SysDept,
- SysDictData,
- SysPost,
- SysPostRole,
- SysRole,
- SysRoleDataScopeDetail,
- SysUser,
- SysUserPost,
- SysUserRole,
- )
- from app.models.research_donation import (
- BizResearchDonation,
- BizResearchDonationCollection,
- BizResearchDonationParty,
- BizResearchDonationSample,
- )
- from app.services.market_field_scope import (
- MarketFieldContext,
- apply_cell_protocol_scope,
- apply_donation_scope,
- )
- from app.services.sample_transport_mode import DICT_TYPE, resolve_transport_mode_value
- from app.services.sf_express_settlement import (
- assert_freight_mutable,
- freight_amount_label,
- resolve_header_for_save,
- )
- from app.services.web_auth_roles import (
- _map_role_name_to_web,
- resolve_web_role_for_user,
- )
- CELL = 'cell_storage'
- DONATION = 'research_donation'
- SOURCES = (CELL, DONATION)
- def _current_tenant_user_info(
- db: Session, ctx: MarketFieldContext, user_info: dict
- ) -> dict:
- """用当前租户有效角色覆盖不可信的令牌 role 快照。"""
- if not isinstance(user_info, dict):
- raise PermissionError('用户身份与现场范围上下文不一致')
- user_id = str(user_info.get('user_id') or '').strip()
- tenant_no = str(user_info.get('tenant_no') or '').strip()
- if user_id != ctx.user_id or tenant_no != ctx.tenant_no:
- raise PermissionError('用户身份与现场范围上下文不一致')
- current = dict(user_info)
- current['role'] = resolve_web_role_for_user(
- db, ctx.user_id, tenant_no=ctx.tenant_no
- )
- return current
- def _latest_cell_party_id(ctx: MarketFieldContext):
- latest = aliased(BizProtocolPartyCustomer)
- return (
- select(latest.id)
- .where(
- latest.collection_protocol_id == BizCollectionProtocol.id,
- latest.tenant_no == ctx.tenant_no,
- latest.del_flag == 0,
- )
- .order_by(latest.create_time.desc(), latest.id.desc())
- .limit(1)
- .correlate(BizCollectionProtocol)
- .scalar_subquery()
- )
- def _latest_donation_party_id(ctx: MarketFieldContext):
- latest = aliased(BizResearchDonationParty)
- return (
- select(latest.id)
- .where(
- latest.research_donation_id == BizResearchDonation.id,
- latest.tenant_no == ctx.tenant_no,
- latest.del_flag == 0,
- )
- .order_by(latest.create_time.desc(), latest.id.desc())
- .limit(1)
- .correlate(BizResearchDonation)
- .scalar_subquery()
- )
- def _amount(value: Any) -> Decimal:
- try:
- result = Decimal(str(value))
- except (InvalidOperation, ValueError, TypeError) as exc:
- raise ValueError('运费格式无效') from exc
- if not result.is_finite() or result < 0:
- raise ValueError('运费格式无效')
- return result.quantize(Decimal('0.01'))
- def _fmt_dt(value: Optional[datetime]) -> str:
- return value.strftime('%Y-%m-%d %H:%M:%S') if value else ''
- def _normalize_items(items: List[Dict[str, Any]]) -> List[Tuple[str, str]]:
- if not items:
- raise ValueError('至少选择一个样本')
- result: List[Tuple[str, str]] = []
- seen = set()
- for item in items:
- source = str(item.get('sampleSource') or item.get('sample_source') or '').strip()
- sample_id = str(item.get('sampleId') or item.get('sample_id') or '').strip()
- if source not in SOURCES or not sample_id:
- raise ValueError('关联样本须包含有效的 sampleSource 与 sampleId')
- key = (source, sample_id)
- if key in seen:
- raise ValueError('关联样本不可重复')
- seen.add(key)
- result.append(key)
- return result
- def _cell_item_query(db: Session, ctx: MarketFieldContext, user_info: dict):
- query = (
- db.query(
- BizSampleFreightItem,
- BizProtocolSample,
- BizCollectionProtocol,
- BizProtocolPartyCustomer,
- BizCellSampleType,
- )
- .join(
- BizProtocolSample,
- and_(
- BizProtocolSample.id == BizSampleFreightItem.sample_id,
- BizProtocolSample.tenant_no == ctx.tenant_no,
- BizProtocolSample.del_flag == 0,
- ),
- )
- .join(
- BizCollectionProtocol,
- and_(
- BizCollectionProtocol.id == BizProtocolSample.collection_protocol_id,
- BizCollectionProtocol.tenant_no == ctx.tenant_no,
- BizCollectionProtocol.del_flag == 0,
- ),
- )
- .outerjoin(
- BizProtocolPartyCustomer,
- and_(
- BizProtocolPartyCustomer.id == _latest_cell_party_id(ctx),
- BizProtocolPartyCustomer.tenant_no == ctx.tenant_no,
- BizProtocolPartyCustomer.del_flag == 0,
- ),
- )
- .outerjoin(
- BizCellSampleType,
- and_(
- BizCellSampleType.id == BizProtocolSample.sample_type_id,
- BizCellSampleType.tenant_no == ctx.tenant_no,
- BizCellSampleType.del_flag == 0,
- ),
- )
- .filter(
- BizSampleFreightItem.tenant_no == ctx.tenant_no,
- BizSampleFreightItem.del_flag == 0,
- BizSampleFreightItem.sample_source == CELL,
- )
- )
- return apply_cell_protocol_scope(query, db, ctx, user_info=user_info)
- def _donation_item_query(db: Session, ctx: MarketFieldContext, user_info: dict):
- query = (
- db.query(
- BizSampleFreightItem,
- BizResearchDonationSample,
- BizResearchDonation,
- BizResearchDonationParty,
- BizCellSampleType,
- )
- .join(
- BizResearchDonationSample,
- and_(
- BizResearchDonationSample.id == BizSampleFreightItem.sample_id,
- BizResearchDonationSample.tenant_no == ctx.tenant_no,
- BizResearchDonationSample.del_flag == 0,
- ),
- )
- .join(
- BizResearchDonation,
- and_(
- BizResearchDonation.id == BizResearchDonationSample.research_donation_id,
- BizResearchDonation.tenant_no == ctx.tenant_no,
- BizResearchDonation.del_flag == 0,
- ),
- )
- .outerjoin(
- BizResearchDonationParty,
- and_(
- BizResearchDonationParty.id == _latest_donation_party_id(ctx),
- BizResearchDonationParty.tenant_no == ctx.tenant_no,
- BizResearchDonationParty.del_flag == 0,
- ),
- )
- .outerjoin(
- BizCellSampleType,
- and_(
- BizCellSampleType.id == BizResearchDonationSample.sample_type_id,
- BizCellSampleType.tenant_no == ctx.tenant_no,
- BizCellSampleType.del_flag == 0,
- ),
- )
- .filter(
- BizSampleFreightItem.tenant_no == ctx.tenant_no,
- BizSampleFreightItem.del_flag == 0,
- BizSampleFreightItem.sample_source == DONATION,
- )
- )
- return apply_donation_scope(query, db, ctx, user_info=user_info)
- def _visible_item_query(db: Session, source: str, ctx: MarketFieldContext, user_info: dict):
- return _cell_item_query(db, ctx, user_info) if source == CELL else _donation_item_query(db, ctx, user_info)
- def _visible_rows(db: Session, freight_ids: List[str], ctx: MarketFieldContext, user_info: dict):
- rows = []
- if not freight_ids:
- return rows
- for source in SOURCES:
- rows.extend(
- _visible_item_query(db, source, ctx, user_info)
- .filter(BizSampleFreightItem.freight_id.in_(freight_ids))
- .order_by(BizSampleFreightItem.create_time.asc(), BizSampleFreightItem.id.asc())
- .all()
- )
- rows.sort(key=lambda row: (row[0].create_time or datetime.min, row[0].id))
- # 业务库历史上可能存在多条有效客户方信息;明细身份始终以关联行 ID 为准。
- unique = {}
- for row in rows:
- unique.setdefault(row[0].id, row)
- return list(unique.values())
- def _item_dict(row) -> Dict[str, Any]:
- item, sample, parent, party, sample_type = row
- source = item.sample_source
- return {
- 'id': item.id,
- 'freightId': item.freight_id,
- 'sampleSource': source,
- 'sampleId': sample.id,
- 'sampleCode': sample.sample_code or '',
- 'protocolId': parent.id,
- 'protocolNo': (parent.protocol_no if source == CELL else parent.donation_no) or '',
- 'sampleTypeName': sample_type.type_name if sample_type else '',
- 'motherName': party.mother_name if party else '',
- 'newbornName': party.newborn_name if party else '',
- }
- def _label_map(db: Session, tenant_no: str, values: List[str]) -> Dict[str, str]:
- wanted = list({value for value in values if value})
- if not wanted:
- return {}
- rows = db.query(SysDictData.dict_value, SysDictData.dict_label).filter(
- SysDictData.tenant_no == tenant_no,
- SysDictData.dict_type == DICT_TYPE,
- SysDictData.dict_value.in_(wanted),
- SysDictData.del_flag == 0,
- ).all()
- return {value: label or value for value, label in rows}
- def _creator_name_map(
- db: Session, tenant_no: str, user_ids: List[str]
- ) -> Dict[str, str]:
- wanted = list({str(user_id or '').strip() for user_id in user_ids if user_id})
- if not wanted:
- return {}
- rows = (
- db.query(SysUser.id, SysUser.nick_name)
- .filter(
- SysUser.id.in_(wanted),
- SysUser.tenant_no == tenant_no,
- )
- .all()
- )
- return {user_id: nick_name or '' for user_id, nick_name in rows}
- def _batch_dict(
- obj: BizSampleFreight, count: int, label: str, creator_name: str = ''
- ) -> Dict[str, Any]:
- amount = obj.freight_amount
- settle_time = getattr(obj, 'settle_time', None)
- return {
- 'id': obj.id,
- 'freightNo': obj.freight_no or '',
- 'transportMode': obj.transport_mode or '',
- 'transportModeLabel': label or obj.transport_mode or '',
- 'freightAmount': None
- if amount is None
- else str(Decimal(amount).quantize(Decimal('0.01'))),
- 'freightAmountLabel': freight_amount_label(obj.transport_mode, amount),
- 'waybillNo': getattr(obj, 'waybill_no', None) or '',
- 'settleStatus': getattr(obj, 'settle_status', None) or '',
- 'settleTime': _fmt_dt(settle_time) if settle_time else '',
- 'visibleItemCount': int(count),
- 'remark': obj.remark or '',
- 'createBy': obj.create_by or '',
- 'createByName': creator_name,
- 'createTime': _fmt_dt(obj.create_time),
- 'updateTime': _fmt_dt(obj.update_time),
- }
- def _freight_accessible(
- db: Session, freight_id: str, *, ctx: MarketFieldContext, user_info: dict
- ) -> Optional[BizSampleFreight]:
- """按 Web 运费单数据权限解析批次;不再要求现场可见明细。"""
- q = db.query(BizSampleFreight).filter(
- BizSampleFreight.id == freight_id,
- BizSampleFreight.tenant_no == ctx.tenant_no,
- BizSampleFreight.del_flag == 0,
- )
- return apply_data_scope(q, BizSampleFreight, user_info, db, ctx.tenant_no).first()
- def list_freights(
- db: Session, *, ctx: MarketFieldContext, user_info: dict, page: int = 1,
- page_size: int = 10, keyword: Optional[str] = None,
- transport_mode: Optional[str] = None, sample_source: Optional[str] = None,
- include_items: bool = False,
- create_time_start: Optional[str] = None,
- create_time_end: Optional[str] = None,
- ) -> Tuple[List[Dict[str, Any]], int]:
- """列表与 Web 一致:租户 + 通用数据权限;明细仍按现场范围脱敏。"""
- user_info = _current_tenant_user_info(db, ctx, user_info)
- page, page_size = max(1, int(page)), max(1, min(100, int(page_size)))
- source = (sample_source or '').strip()
- if source and source not in SOURCES:
- raise ValueError('sampleSource 无效')
- q = db.query(BizSampleFreight).filter(
- BizSampleFreight.tenant_no == ctx.tenant_no,
- BizSampleFreight.del_flag == 0,
- )
- q = apply_data_scope(q, BizSampleFreight, user_info, db, ctx.tenant_no)
- 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)
- if created_from:
- q = q.filter(BizSampleFreight.create_time >= created_from)
- if created_until:
- q = q.filter(BizSampleFreight.create_time < created_until)
- if source:
- item_exists = exists().where(
- and_(
- BizSampleFreightItem.freight_id == BizSampleFreight.id,
- BizSampleFreightItem.tenant_no == ctx.tenant_no,
- BizSampleFreightItem.del_flag == 0,
- BizSampleFreightItem.sample_source == source,
- )
- )
- q = q.filter(item_exists)
- if transport_mode:
- q = q.filter(BizSampleFreight.transport_mode == transport_mode.strip())
- text = (keyword or '').strip()
- if text:
- like = f'%{text}%'
- label_values = [
- value
- for value, in db.query(SysDictData.dict_value)
- .filter(
- SysDictData.dict_type == DICT_TYPE,
- SysDictData.tenant_no == ctx.tenant_no,
- SysDictData.del_flag == 0,
- SysDictData.dict_label.like(like),
- )
- .all()
- ]
- sample_code_exists = exists().where(
- and_(
- BizSampleFreightItem.freight_id == BizSampleFreight.id,
- BizSampleFreightItem.tenant_no == ctx.tenant_no,
- BizSampleFreightItem.del_flag == 0,
- BizSampleFreightItem.sample_code.like(like),
- )
- )
- conds = [
- BizSampleFreight.freight_no.like(like),
- BizSampleFreight.transport_mode.like(like),
- sample_code_exists,
- ]
- if label_values:
- conds.append(BizSampleFreight.transport_mode.in_(label_values))
- q = q.filter(or_(*conds))
- total = q.count()
- batches = q.order_by(BizSampleFreight.create_time.desc(), BizSampleFreight.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
- ids = [row.id for row in batches]
- visible_rows = _visible_rows(db, ids, ctx, user_info)
- active_counts = {
- freight_id: int(count)
- for freight_id, count in db.query(
- BizSampleFreightItem.freight_id,
- func.count(BizSampleFreightItem.id),
- ).filter(
- BizSampleFreightItem.freight_id.in_(ids or ['']),
- BizSampleFreightItem.tenant_no == ctx.tenant_no,
- BizSampleFreightItem.del_flag == 0,
- ).group_by(BizSampleFreightItem.freight_id).all()
- }
- by_batch: Dict[str, list] = {}
- for row in visible_rows:
- by_batch.setdefault(row[0].freight_id, []).append(row)
- labels = _label_map(db, ctx.tenant_no, [row.transport_mode for row in batches])
- creator_names = _creator_name_map(
- db, ctx.tenant_no, [row.create_by for row in batches]
- )
- result = []
- for batch in batches:
- rows = by_batch.get(batch.id, [])
- data = _batch_dict(
- batch,
- len(rows),
- labels.get(batch.transport_mode or '', ''),
- creator_names.get(batch.create_by or '', ''),
- )
- has_outside_scope_item = active_counts.get(batch.id, 0) > len(rows)
- data['allowedActions'] = (
- ['link', 'unlink']
- if has_outside_scope_item
- else ['edit', 'delete', 'link', 'unlink']
- )
- if include_items:
- data['items'] = [_item_dict(row) for row in rows]
- result.append(data)
- return result, total
- def get_freight_detail(db: Session, freight_id: str, *, ctx: MarketFieldContext, user_info: dict) -> Dict[str, Any]:
- user_info = _current_tenant_user_info(db, ctx, user_info)
- obj = _freight_accessible(db, freight_id, ctx=ctx, user_info=user_info)
- if not obj:
- raise HTTPException(status_code=404, detail='数据不存在或无权访问')
- rows = _visible_rows(db, [freight_id], ctx, user_info)
- labels = _label_map(db, ctx.tenant_no, [obj.transport_mode])
- creator_names = _creator_name_map(db, ctx.tenant_no, [obj.create_by])
- data = _batch_dict(
- obj,
- len(rows),
- labels.get(obj.transport_mode or '', ''),
- creator_names.get(obj.create_by or '', ''),
- )
- data['items'] = [_item_dict(row) for row in rows]
- visible_item_ids = {row[0].id for row in rows}
- has_outside_scope_item = (
- db.query(BizSampleFreightItem.id)
- .filter(
- BizSampleFreightItem.freight_id == freight_id,
- BizSampleFreightItem.tenant_no == ctx.tenant_no,
- BizSampleFreightItem.del_flag == 0,
- *([BizSampleFreightItem.id.notin_(visible_item_ids)] if visible_item_ids else []),
- )
- .first()
- is not None
- )
- data['allowedActions'] = (
- ['link', 'unlink']
- if has_outside_scope_item or not rows
- else ['edit', 'delete', 'link', 'unlink']
- )
- return data
- def _option_query(db: Session, source: str, ctx: MarketFieldContext, user_info: dict, keyword: str):
- if source == CELL:
- q = db.query(
- literal(CELL).label('source'), BizProtocolSample.id.label('sample_id'),
- BizProtocolSample.sample_code.label('sample_code'), BizCollectionProtocol.id.label('protocol_id'),
- BizCollectionProtocol.protocol_no.label('protocol_no'), BizProtocolPartyCustomer.mother_name.label('customer_name'),
- BizProtocolPartyCustomer.newborn_name.label('newborn_name'),
- BizCellSampleType.type_name.label('sample_type_name'),
- BizProtocolSample.create_time.label('create_time'),
- ).join(BizCollectionProtocol, and_(BizCollectionProtocol.id == BizProtocolSample.collection_protocol_id, BizCollectionProtocol.tenant_no == ctx.tenant_no, BizCollectionProtocol.del_flag == 0)).outerjoin(BizProtocolPartyCustomer, and_(BizProtocolPartyCustomer.id == _latest_cell_party_id(ctx), BizProtocolPartyCustomer.tenant_no == ctx.tenant_no, BizProtocolPartyCustomer.del_flag == 0)).outerjoin(BizCellSampleType, and_(BizCellSampleType.id == BizProtocolSample.sample_type_id, BizCellSampleType.tenant_no == ctx.tenant_no, BizCellSampleType.del_flag == 0)).filter(BizProtocolSample.tenant_no == ctx.tenant_no, BizProtocolSample.del_flag == 0)
- q = apply_cell_protocol_scope(q, db, ctx, user_info=user_info)
- if keyword:
- like = f'%{keyword}%'
- q = q.filter(or_(BizProtocolSample.sample_code.like(like), BizCollectionProtocol.protocol_no.like(like), BizProtocolPartyCustomer.mother_name.like(like)))
- return q.distinct()
- q = db.query(
- literal(DONATION).label('source'), BizResearchDonationSample.id.label('sample_id'),
- BizResearchDonationSample.sample_code.label('sample_code'), BizResearchDonation.id.label('protocol_id'),
- BizResearchDonation.donation_no.label('protocol_no'), BizResearchDonationParty.mother_name.label('customer_name'),
- BizResearchDonationParty.newborn_name.label('newborn_name'),
- BizCellSampleType.type_name.label('sample_type_name'),
- BizResearchDonationSample.create_time.label('create_time'),
- ).join(BizResearchDonation, and_(BizResearchDonation.id == BizResearchDonationSample.research_donation_id, BizResearchDonation.tenant_no == ctx.tenant_no, BizResearchDonation.del_flag == 0)).outerjoin(BizResearchDonationParty, and_(BizResearchDonationParty.id == _latest_donation_party_id(ctx), BizResearchDonationParty.tenant_no == ctx.tenant_no, BizResearchDonationParty.del_flag == 0)).outerjoin(BizCellSampleType, and_(BizCellSampleType.id == BizResearchDonationSample.sample_type_id, BizCellSampleType.tenant_no == ctx.tenant_no, BizCellSampleType.del_flag == 0)).filter(BizResearchDonationSample.tenant_no == ctx.tenant_no, BizResearchDonationSample.del_flag == 0)
- q = apply_donation_scope(q, db, ctx, user_info=user_info)
- if keyword:
- like = f'%{keyword}%'
- q = q.filter(or_(BizResearchDonationSample.sample_code.like(like), BizResearchDonation.donation_no.like(like), BizResearchDonationParty.mother_name.like(like)))
- return q.distinct()
- def list_sample_options(db: Session, *, ctx: MarketFieldContext, user_info: dict, page: int = 1, page_size: int = 20, keyword: Optional[str] = None, sample_source: Optional[str] = None):
- user_info = _current_tenant_user_info(db, ctx, user_info)
- page, page_size = max(1, int(page)), max(1, min(100, int(page_size)))
- source = (sample_source or '').strip()
- if source and source not in SOURCES:
- raise ValueError('sampleSource 无效')
- sources = (source,) if source else SOURCES
- queries = [_option_query(db, src, ctx, user_info, (keyword or '').strip()) for src in sources]
- combined = queries[0] if len(queries) == 1 else queries[0].union_all(*queries[1:])
- sub = combined.subquery()
- total = db.query(func.count()).select_from(sub).scalar() or 0
- rows = db.query(sub).order_by(sub.c.create_time.desc(), sub.c.source.desc(), sub.c.sample_id.desc()).offset((page - 1) * page_size).limit(page_size).all()
- keys = [(row.source, row.sample_id) for row in rows]
- links = {}
- if keys:
- clauses = [and_(BizSampleFreightItem.sample_source == src, BizSampleFreightItem.sample_id == sid) for src, sid in keys]
- for item, freight_no in db.query(
- BizSampleFreightItem, BizSampleFreight.freight_no
- ).join(
- BizSampleFreight,
- and_(
- BizSampleFreight.id == BizSampleFreightItem.freight_id,
- BizSampleFreight.tenant_no == ctx.tenant_no,
- BizSampleFreight.del_flag == 0,
- ),
- ).filter(
- BizSampleFreightItem.tenant_no == ctx.tenant_no,
- BizSampleFreightItem.del_flag == 0,
- or_(*clauses),
- ).all():
- links[(item.sample_source, item.sample_id)] = (
- item.freight_id, freight_no or ''
- )
- return [{
- 'sampleSource': row.source, 'sampleId': row.sample_id,
- 'sampleCode': row.sample_code or '', 'protocolId': row.protocol_id,
- 'protocolNo': row.protocol_no or '', 'customerName': row.customer_name or '',
- 'newbornName': row.newborn_name or '',
- 'sampleTypeName': row.sample_type_name or '',
- 'createTime': _fmt_dt(row.create_time),
- 'freightId': (links.get((row.source, row.sample_id)) or ('', ''))[0],
- 'freightNo': (links.get((row.source, row.sample_id)) or ('', ''))[1],
- } for row in rows], int(total)
- def _sample_parent_lock_query(
- db: Session,
- source: str,
- sample_ids: List[str],
- ctx: MarketFieldContext,
- ):
- ids = sorted(set(sample_ids))
- if source == CELL:
- query = (
- db.query(BizProtocolSample, BizCollectionProtocol)
- .join(
- BizCollectionProtocol,
- and_(
- BizCollectionProtocol.id
- == BizProtocolSample.collection_protocol_id,
- BizCollectionProtocol.tenant_no == ctx.tenant_no,
- BizCollectionProtocol.del_flag == 0,
- ),
- )
- .filter(
- BizProtocolSample.id.in_(ids),
- BizProtocolSample.tenant_no == ctx.tenant_no,
- BizProtocolSample.del_flag == 0,
- )
- )
- return query.order_by(
- BizProtocolSample.id.asc(), BizCollectionProtocol.id.asc()
- ).populate_existing().with_for_update()
- query = (
- db.query(BizResearchDonationSample, BizResearchDonation)
- .join(
- BizResearchDonation,
- and_(
- BizResearchDonation.id
- == BizResearchDonationSample.research_donation_id,
- BizResearchDonation.tenant_no == ctx.tenant_no,
- BizResearchDonation.del_flag == 0,
- ),
- )
- .filter(
- BizResearchDonationSample.id.in_(ids),
- BizResearchDonationSample.tenant_no == ctx.tenant_no,
- BizResearchDonationSample.del_flag == 0,
- )
- )
- return query.order_by(
- BizResearchDonationSample.id.asc(), BizResearchDonation.id.asc()
- ).populate_existing().with_for_update()
- def _collection_lock_query(
- db: Session,
- source: str,
- parent_ids: List[str],
- ctx: MarketFieldContext,
- ):
- ids = sorted(set(parent_ids))
- if source == CELL:
- return (
- db.query(BizProtocolCollectionInfo)
- .filter(
- BizProtocolCollectionInfo.collection_protocol_id.in_(ids),
- BizProtocolCollectionInfo.tenant_no == ctx.tenant_no,
- BizProtocolCollectionInfo.del_flag == 0,
- )
- .order_by(
- BizProtocolCollectionInfo.collection_protocol_id.asc(),
- BizProtocolCollectionInfo.id.asc(),
- )
- .populate_existing()
- .with_for_update()
- )
- return (
- db.query(BizResearchDonationCollection)
- .filter(
- BizResearchDonationCollection.research_donation_id.in_(ids),
- BizResearchDonationCollection.tenant_no == ctx.tenant_no,
- BizResearchDonationCollection.del_flag == 0,
- )
- .order_by(
- BizResearchDonationCollection.research_donation_id.asc(),
- BizResearchDonationCollection.id.asc(),
- )
- .populate_existing()
- .with_for_update()
- )
- def _leader_lock_query(db: Session, ctx: MarketFieldContext):
- """锁定经理当前有效的地区负责人关系,供字段范围直接计算。"""
- return (
- db.query(BizCollectionRegionStaff)
- .filter(
- BizCollectionRegionStaff.tenant_no == ctx.tenant_no,
- BizCollectionRegionStaff.del_flag == 0,
- BizCollectionRegionStaff.user_id == ctx.user_id,
- BizCollectionRegionStaff.is_leader == 1,
- )
- .order_by(
- BizCollectionRegionStaff.region_id.asc(),
- BizCollectionRegionStaff.id.asc(),
- )
- .populate_existing()
- .with_for_update()
- )
- def _current_field_visible_parent_ids(
- source: str,
- collection_rows: List[Any],
- leader_rows: List[BizCollectionRegionStaff],
- ctx: MarketFieldContext,
- ) -> set[str]:
- """只使用已加锁的采集分配/负责人当前值计算现场字段范围。"""
- parent_attr = (
- 'collection_protocol_id'
- if source == CELL
- else 'research_donation_id'
- )
- leader_regions = {row.region_id for row in leader_rows}
- visible: set[str] = set()
- for row in collection_rows:
- if ctx.scope_kind == 'specialist':
- allowed = row.sales_user_id == ctx.user_id
- else:
- allowed = (
- row.region_id in leader_regions
- or row.region_manager_user_id == ctx.user_id
- )
- if allowed:
- visible.add(getattr(row, parent_attr))
- return visible
- def _current_data_scope_parent_ids(
- db: Session,
- parents: List[Any],
- ctx: MarketFieldContext,
- user_info: dict,
- ) -> set[str]:
- """锁定数据权限依赖并按锁后当前值计算多角色并集范围。"""
- if not isinstance(user_info, dict) or str(
- user_info.get('user_id') or ''
- ).strip() != ctx.user_id:
- raise PermissionError('用户身份与现场范围上下文不一致')
- parent_ids = {parent.id for parent in parents}
- current_users = (
- db.query(SysUser)
- .filter(
- SysUser.id == ctx.user_id,
- SysUser.tenant_no == ctx.tenant_no,
- SysUser.del_flag == 0,
- )
- .order_by(SysUser.id.asc())
- .populate_existing()
- .with_for_update()
- .all()
- )
- if not current_users:
- return set()
- current_user = current_users[0]
- direct_links = (
- db.query(SysUserRole)
- .filter(
- SysUserRole.user_id == ctx.user_id,
- SysUserRole.tenant_no == ctx.tenant_no,
- SysUserRole.del_flag == 0,
- )
- .order_by(SysUserRole.role_id.asc(), SysUserRole.id.asc())
- .populate_existing()
- .with_for_update()
- .all()
- )
- user_posts = (
- db.query(SysUserPost)
- .filter(
- SysUserPost.user_id == ctx.user_id,
- SysUserPost.tenant_no == ctx.tenant_no,
- SysUserPost.del_flag == 0,
- )
- .order_by(SysUserPost.post_id.asc(), SysUserPost.id.asc())
- .populate_existing()
- .with_for_update()
- .all()
- )
- post_ids = sorted({row.post_id for row in user_posts})
- posts = []
- if post_ids:
- posts = (
- db.query(SysPost)
- .filter(
- SysPost.id.in_(post_ids),
- SysPost.tenant_no == ctx.tenant_no,
- SysPost.del_flag == 0,
- SysPost.status == '1',
- )
- .order_by(SysPost.id.asc())
- .populate_existing()
- .with_for_update()
- .all()
- )
- enabled_post_ids = sorted({row.id for row in posts})
- post_role_links = []
- if enabled_post_ids:
- post_role_links = (
- db.query(SysPostRole)
- .filter(
- SysPostRole.post_id.in_(enabled_post_ids),
- SysPostRole.tenant_no == ctx.tenant_no,
- SysPostRole.del_flag == 0,
- )
- .order_by(
- SysPostRole.post_id.asc(),
- SysPostRole.role_id.asc(),
- SysPostRole.id.asc(),
- )
- .populate_existing()
- .with_for_update()
- .all()
- )
- role_ids = sorted(
- {row.role_id for row in direct_links}
- | {row.role_id for row in post_role_links}
- )
- roles = []
- if role_ids:
- roles = (
- db.query(SysRole)
- .filter(
- SysRole.id.in_(role_ids),
- SysRole.tenant_no == ctx.tenant_no,
- SysRole.del_flag == 0,
- SysRole.status == '1',
- )
- .order_by(SysRole.id.asc())
- .populate_existing()
- .with_for_update()
- .all()
- )
- if not roles:
- return set()
- if any(
- _map_role_name_to_web(role.role_name) == 'super_admin'
- for role in roles
- ):
- return parent_ids
- if any((role.data_scope or '1') == '1' for role in roles):
- return parent_ids
- active_role_ids = sorted({role.id for role in roles})
- details = (
- db.query(SysRoleDataScopeDetail)
- .filter(
- SysRoleDataScopeDetail.role_id.in_(active_role_ids),
- SysRoleDataScopeDetail.tenant_no == ctx.tenant_no,
- SysRoleDataScopeDetail.del_flag == 0,
- )
- .order_by(
- SysRoleDataScopeDetail.role_id.asc(),
- SysRoleDataScopeDetail.scope_type.asc(),
- SysRoleDataScopeDetail.scope_value.asc(),
- SysRoleDataScopeDetail.id.asc(),
- )
- .populate_existing()
- .with_for_update()
- .all()
- )
- details_by_role: Dict[str, List[SysRoleDataScopeDetail]] = {}
- for detail in details:
- details_by_role.setdefault(detail.role_id, []).append(detail)
- dept_ids: set[str] = set()
- user_ids: set[str] = set()
- current_dept_id = (current_user.dept_id or '').strip()
- needs_descendants = any(
- (role.data_scope or '1') == '3' for role in roles
- )
- dept_rows = []
- if needs_descendants and current_dept_id:
- dept_rows = (
- db.query(SysDept)
- .filter(
- SysDept.tenant_no == ctx.tenant_no,
- SysDept.del_flag == 0,
- )
- .order_by(SysDept.id.asc())
- .populate_existing()
- .with_for_update()
- .all()
- )
- descendants = {current_dept_id} if current_dept_id else set()
- if dept_rows and current_dept_id:
- pending = [current_dept_id]
- while pending:
- parent_id = pending.pop(0)
- for dept in dept_rows:
- if dept.parent_id == parent_id and dept.id not in descendants:
- descendants.add(dept.id)
- pending.append(dept.id)
- for role in roles:
- scope = role.data_scope or '1'
- if scope == '2' and current_dept_id:
- dept_ids.add(current_dept_id)
- elif scope == '3':
- dept_ids.update(descendants)
- elif scope == '4':
- user_ids.add(ctx.user_id)
- elif scope == '5':
- for detail in details_by_role.get(role.id, []):
- value = (detail.scope_value or '').strip()
- if not value:
- continue
- if detail.scope_type == 'dept':
- dept_ids.add(value)
- elif detail.scope_type == 'user':
- user_ids.add(value)
- return {
- parent.id
- for parent in parents
- if (
- (parent.create_dept_id in dept_ids if dept_ids else False)
- or (parent.create_by in user_ids if user_ids else False)
- )
- }
- def _active_item_lock_query(
- db: Session,
- ctx: MarketFieldContext,
- *,
- freight_id: Optional[str] = None,
- keys: Optional[List[Tuple[str, str]]] = None,
- ):
- conditions = []
- if freight_id:
- conditions.append(BizSampleFreightItem.freight_id == freight_id)
- for source, sample_id in sorted(set(keys or [])):
- conditions.append(
- and_(
- BizSampleFreightItem.sample_source == source,
- BizSampleFreightItem.sample_id == sample_id,
- )
- )
- query = db.query(BizSampleFreightItem).filter(
- BizSampleFreightItem.tenant_no == ctx.tenant_no,
- BizSampleFreightItem.del_flag == 0,
- )
- if conditions:
- query = query.filter(or_(*conditions))
- else:
- query = query.filter(False)
- return query.order_by(
- BizSampleFreightItem.sample_source.asc(),
- BizSampleFreightItem.sample_id.asc(),
- BizSampleFreightItem.id.asc(),
- ).populate_existing().with_for_update()
- def _lock_batch_header(
- db: Session, freight_id: str, ctx: MarketFieldContext
- ) -> BizSampleFreight:
- obj = (
- db.query(BizSampleFreight)
- .filter(
- BizSampleFreight.id == freight_id,
- BizSampleFreight.tenant_no == ctx.tenant_no,
- BizSampleFreight.del_flag == 0,
- )
- .populate_existing()
- .with_for_update()
- .first()
- )
- if not obj:
- raise HTTPException(status_code=404, detail='数据不存在或无权访问')
- return obj
- def _discover_batch_keys(
- db: Session, freight_id: str, ctx: MarketFieldContext
- ) -> List[Tuple[str, str]]:
- """批次头已锁后发现参与者;最终关联锁会再次验证该快照。"""
- rows = (
- db.query(
- BizSampleFreightItem.sample_source,
- BizSampleFreightItem.sample_id,
- )
- .filter(
- BizSampleFreightItem.freight_id == freight_id,
- BizSampleFreightItem.tenant_no == ctx.tenant_no,
- BizSampleFreightItem.del_flag == 0,
- )
- .order_by(
- BizSampleFreightItem.sample_source.asc(),
- BizSampleFreightItem.sample_id.asc(),
- BizSampleFreightItem.id.asc(),
- )
- .all()
- )
- return [(source, sample_id) for source, sample_id in rows]
- def _lock_sample_graph(
- db: Session,
- keys: List[Tuple[str, str]],
- ctx: MarketFieldContext,
- user_info: dict,
- ) -> Dict[Tuple[str, str], Tuple[Any, Any]]:
- """按来源和 ID 锁样本/父单/采集分配,再用锁定当前读重算范围。"""
- normalized = sorted(set(keys))
- locked_rows: Dict[str, List[Tuple[Any, Any]]] = {}
- locked_collections: Dict[str, List[Any]] = {}
- for source in SOURCES:
- ids = [sample_id for item_source, sample_id in normalized if item_source == source]
- if not ids:
- continue
- rows = _sample_parent_lock_query(db, source, ids, ctx).all()
- locked_rows[source] = rows
- parent_ids = [parent.id for _, parent in rows]
- locked_collections[source] = _collection_lock_query(
- db, source, parent_ids, ctx
- ).all()
- leader_rows = (
- _leader_lock_query(db, ctx).all()
- if ctx.scope_kind == 'manager'
- else []
- )
- all_parents = [
- parent
- for rows in locked_rows.values()
- for _, parent in rows
- ]
- data_scope_ids = _current_data_scope_parent_ids(
- db, all_parents, ctx, user_info
- )
- visible: Dict[Tuple[str, str], Tuple[Any, Any]] = {}
- for source, rows in locked_rows.items():
- field_scope_ids = _current_field_visible_parent_ids(
- source, locked_collections[source], leader_rows, ctx
- )
- for sample, parent in rows:
- if parent.id in field_scope_ids and parent.id in data_scope_ids:
- visible[(source, sample.id)] = (sample, parent)
- return visible
- def _lock_active_items(
- db: Session,
- ctx: MarketFieldContext,
- *,
- freight_id: Optional[str] = None,
- keys: Optional[List[Tuple[str, str]]] = None,
- expected_batch_keys: Optional[List[Tuple[str, str]]] = None,
- ) -> List[BizSampleFreightItem]:
- rows = _active_item_lock_query(
- db, ctx, freight_id=freight_id, keys=keys
- ).all()
- if freight_id is not None and expected_batch_keys is not None:
- current = sorted(
- (row.sample_source, row.sample_id)
- for row in rows
- if row.freight_id == freight_id
- )
- if current != sorted(expected_batch_keys):
- raise PermissionError('批次明细已发生变化,请重试')
- return rows
- def _next_freight_no(db: Session, tenant_no: str, now: datetime, offset: int = 0) -> str:
- prefix = f"YF{now.strftime('%Y%m%d')}"
- latest = db.query(BizSampleFreight.freight_no).filter(BizSampleFreight.tenant_no == tenant_no, BizSampleFreight.freight_no.like(f'{prefix}%')).order_by(BizSampleFreight.freight_no.desc()).with_for_update().first()
- seq = int(str(latest[0])[-3:]) + 1 if latest and str(latest[0])[-3:].isdigit() else 1
- seq += offset
- if seq > 999:
- raise ValueError('当日运费批次号已达上限(999)')
- return f'{prefix}{seq:03d}'
- def create_freight(db: Session, *, ctx: MarketFieldContext, user_info: dict, transport_mode: str, freight_amount: Any, items: List[Dict[str, Any]], waybill_no: Any = None):
- keys = _normalize_items(items)
- try:
- samples = _lock_sample_graph(db, keys, ctx, user_info)
- if len(samples) != len(set(keys)):
- raise HTTPException(status_code=404, detail='样本不存在或无权操作')
- conflicts = _lock_active_items(db, ctx, keys=keys)
- if conflicts:
- raise ValueError('样本已关联其他运费批次')
- mode = resolve_transport_mode_value(db, tenant_no=ctx.tenant_no, raw=transport_mode, user_id=ctx.user_id)
- plan = resolve_header_for_save(
- transport_mode=mode,
- existing_waybill=None,
- existing_amount=None,
- existing_settle_status=None,
- waybill_supplied=True,
- waybill_no=waybill_no,
- amount_supplied=True,
- freight_amount=freight_amount,
- )
- now = datetime.now()
- obj = None
- for attempt in range(3):
- candidate = BizSampleFreight(id=generate_uuid(), freight_no=_next_freight_no(db, ctx.tenant_no, now, attempt), transport_mode=plan.transport_mode, freight_amount=plan.freight_amount, waybill_no=plan.waybill_no, settle_status=plan.settle_status, settle_time=None, tenant_no=ctx.tenant_no, order_num=0, remark=None, create_by=ctx.user_id, create_dept_id=str(user_info.get('dept_id') or user_info.get('region_id') or ''), create_time=now, update_by=ctx.user_id, update_time=now, del_flag=0)
- try:
- with db.begin_nested():
- db.add(candidate)
- db.flush()
- obj = candidate
- break
- except IntegrityError:
- if attempt == 2:
- raise ValueError('运费批次号生成冲突,请重试')
- assert obj is not None
- for source, sample_id in keys:
- sample, parent = samples[(source, sample_id)]
- db.add(BizSampleFreightItem(id=generate_uuid(), freight_id=obj.id, sample_source=source, sample_id=sample.id, sample_code=sample.sample_code, protocol_id=parent.id, tenant_no=ctx.tenant_no, order_num=0, remark=None, create_by=ctx.user_id, create_dept_id=str(user_info.get('dept_id') or user_info.get('region_id') or ''), create_time=now, update_by=ctx.user_id, update_time=now, del_flag=0))
- db.flush()
- db.commit()
- return get_freight_detail(db, obj.id, ctx=ctx, user_info=user_info)
- except IntegrityError as exc:
- db.rollback()
- raise ValueError('样本已关联运费批次,不允许移动') from exc
- except Exception:
- db.rollback()
- raise
- def update_freight(db: Session, freight_id: str, *, ctx: MarketFieldContext, user_info: dict, transport_mode: Optional[str] = None, freight_amount: Any = None, waybill_no: Any = None, waybill_supplied: bool = False, amount_supplied: bool = False):
- try:
- obj = _lock_batch_header(db, freight_id, ctx)
- expected_keys = _discover_batch_keys(db, freight_id, ctx)
- visible = _lock_sample_graph(db, expected_keys, ctx, user_info)
- active = _lock_active_items(
- db,
- ctx,
- freight_id=freight_id,
- expected_batch_keys=expected_keys,
- )
- if not active or not visible:
- raise HTTPException(status_code=404, detail='数据不存在或无权访问')
- if any(
- (row.sample_source, row.sample_id) not in visible for row in active
- ):
- raise PermissionError('仅当批次全部有效样本均在当前范围内时可修改批次信息')
- mode = obj.transport_mode or ''
- if transport_mode is not None and str(transport_mode).strip() != '':
- mode = resolve_transport_mode_value(db, tenant_no=ctx.tenant_no, raw=str(transport_mode), user_id=ctx.user_id)
- plan = resolve_header_for_save(
- transport_mode=mode,
- existing_waybill=obj.waybill_no,
- existing_amount=obj.freight_amount,
- existing_settle_status=obj.settle_status,
- waybill_supplied=waybill_supplied,
- waybill_no=waybill_no,
- amount_supplied=amount_supplied,
- freight_amount=freight_amount,
- )
- obj.transport_mode = plan.transport_mode
- obj.waybill_no = plan.waybill_no
- obj.freight_amount = plan.freight_amount
- obj.settle_status = plan.settle_status
- if plan.clear_settle_time:
- obj.settle_time = None
- obj.update_by, obj.update_time = ctx.user_id, datetime.now()
- db.flush()
- db.commit()
- return get_freight_detail(db, freight_id, ctx=ctx, user_info=user_info)
- except Exception:
- db.rollback()
- raise
- def assert_link_target_access(
- db: Session,
- freight_id: str,
- *,
- ctx: MarketFieldContext,
- user_info: dict,
- ) -> None:
- """确认目标批次在 Web 同款数据权限内可关联(不要求已有现场可见明细)。"""
- user_info = _current_tenant_user_info(db, ctx, user_info)
- if not _freight_accessible(db, freight_id, ctx=ctx, user_info=user_info):
- raise HTTPException(status_code=404, detail='数据不存在或无权访问')
- def link_samples(db: Session, freight_id: str, *, ctx: MarketFieldContext, user_info: dict, items: List[Dict[str, Any]]):
- keys = _normalize_items(items)
- try:
- user_info = _current_tenant_user_info(db, ctx, user_info)
- if not _freight_accessible(db, freight_id, ctx=ctx, user_info=user_info):
- raise HTTPException(status_code=404, detail='数据不存在或无权访问')
- obj = _lock_batch_header(db, freight_id, ctx)
- assert_freight_mutable(obj.settle_status, 'items')
- expected_keys = _discover_batch_keys(db, freight_id, ctx)
- samples = _lock_sample_graph(
- db, sorted(set(expected_keys + keys)), ctx, user_info
- )
- locked_items = _lock_active_items(
- db,
- ctx,
- freight_id=freight_id,
- keys=keys,
- expected_batch_keys=expected_keys,
- )
- if any(key not in samples for key in keys):
- raise HTTPException(status_code=404, detail='样本不存在或无权操作')
- requested = set(keys)
- conflicts = [
- row
- for row in locked_items
- if (row.sample_source, row.sample_id) in requested
- ]
- if conflicts:
- raise ValueError('样本已关联运费批次,不允许移动')
- now = datetime.now()
- for source, sample_id in keys:
- sample, parent = samples[(source, sample_id)]
- db.add(BizSampleFreightItem(id=generate_uuid(), freight_id=obj.id, sample_source=source, sample_id=sample.id, sample_code=sample.sample_code, protocol_id=parent.id, tenant_no=ctx.tenant_no, order_num=0, remark=None, create_by=ctx.user_id, create_dept_id=str(user_info.get('dept_id') or user_info.get('region_id') or ''), create_time=now, update_by=ctx.user_id, update_time=now, del_flag=0))
- db.flush()
- db.commit()
- return get_freight_detail(db, freight_id, ctx=ctx, user_info=user_info)
- except IntegrityError as exc:
- db.rollback()
- raise ValueError('样本已关联运费批次,不允许移动') from exc
- except Exception:
- db.rollback()
- raise
- def unlink_samples(db: Session, freight_id: str, *, ctx: MarketFieldContext, user_info: dict, items: List[Dict[str, Any]]):
- keys = _normalize_items(items)
- try:
- obj = _lock_batch_header(db, freight_id, ctx)
- assert_freight_mutable(obj.settle_status, 'items')
- expected_keys = _discover_batch_keys(db, freight_id, ctx)
- visible = _lock_sample_graph(db, expected_keys, ctx, user_info)
- active = _lock_active_items(
- db,
- ctx,
- freight_id=freight_id,
- expected_batch_keys=expected_keys,
- )
- if not any(
- (row.sample_source, row.sample_id) in visible for row in active
- ):
- raise HTTPException(status_code=404, detail='数据不存在或无权访问')
- requested = set(keys)
- matched = [
- row
- for row in active
- if (row.sample_source, row.sample_id) in visible
- and (row.sample_source, row.sample_id) in requested
- ]
- if len(matched) != len(keys):
- raise ValueError('关联明细不存在或无权操作')
- now = datetime.now()
- for row in matched:
- row.del_flag, row.update_by, row.update_time = 1, ctx.user_id, now
- db.flush()
- matched_ids = {row.id for row in matched}
- remaining = [row for row in active if row.id not in matched_ids]
- remaining_visible = [
- row
- for row in remaining
- if (row.sample_source, row.sample_id) in visible
- ]
- if not remaining_visible:
- if not remaining:
- obj.del_flag, obj.update_by, obj.update_time = 1, ctx.user_id, now
- db.commit()
- return {'id': freight_id, 'visibleItemCount': 0, 'disappeared': True}
- db.commit()
- return get_freight_detail(db, freight_id, ctx=ctx, user_info=user_info)
- except Exception:
- db.rollback()
- raise
- def delete_freight(db: Session, freight_id: str, *, ctx: MarketFieldContext, user_info: dict):
- """级联软删运费单;仅当全部有效关联样本均在当前范围内时允许。"""
- try:
- obj = _lock_batch_header(db, freight_id, ctx)
- assert_freight_mutable(obj.settle_status, 'delete')
- expected_keys = _discover_batch_keys(db, freight_id, ctx)
- visible = _lock_sample_graph(db, expected_keys, ctx, user_info)
- active = _lock_active_items(
- db,
- ctx,
- freight_id=freight_id,
- expected_batch_keys=expected_keys,
- )
- if not active or not visible:
- raise HTTPException(status_code=404, detail='数据不存在或无权访问')
- if any(
- (row.sample_source, row.sample_id) not in visible for row in active
- ):
- raise PermissionError('仅当批次全部有效样本均在当前范围内时可删除运费单')
- now = datetime.now()
- for row in active:
- row.del_flag, row.update_by, row.update_time = 1, ctx.user_id, now
- obj.del_flag, obj.update_by, obj.update_time = 1, ctx.user_id, now
- db.flush()
- db.commit()
- return {'id': freight_id, 'visibleItemCount': 0, 'disappeared': True}
- except Exception:
- db.rollback()
- raise
|