2257 lines
79 KiB
Python
2257 lines
79 KiB
Python
"""移动市场协议服务:双来源统一展示,写入保持来源内原子性。"""
|
|
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,
|
|
current_collection=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('引用用户不存在、已停用或不属于当前租户')
|
|
|
|
_validate_region_staff_assignments(
|
|
db,
|
|
ctx,
|
|
collection=collection,
|
|
current_collection=current_collection,
|
|
)
|
|
|
|
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 _validate_region_staff_assignments(
|
|
db: Session,
|
|
ctx: MarketFieldContext,
|
|
*,
|
|
collection: dict,
|
|
current_collection=None,
|
|
) -> None:
|
|
"""新增/变更归属必须来自目标地区人员;未变更历史值保持兼容。"""
|
|
region_id = str(collection.get('region_id') or '').strip()
|
|
current_region_id = str(
|
|
getattr(current_collection, 'region_id', None) or ''
|
|
).strip()
|
|
for field_name, api_name in (
|
|
('sales_user_id', 'salesUserId'),
|
|
('region_manager_user_id', 'regionManagerUserId'),
|
|
):
|
|
user_id = str(collection.get(field_name) or '').strip()
|
|
current_user_id = str(
|
|
getattr(current_collection, field_name, None) or ''
|
|
).strip()
|
|
unchanged = (
|
|
current_collection is not None
|
|
and region_id == current_region_id
|
|
and user_id == current_user_id
|
|
)
|
|
if unchanged or not user_id:
|
|
continue
|
|
binding = (
|
|
db.query(BizCollectionRegionStaff.id)
|
|
.filter(
|
|
BizCollectionRegionStaff.tenant_no == ctx.tenant_no,
|
|
BizCollectionRegionStaff.region_id == region_id,
|
|
BizCollectionRegionStaff.user_id == user_id,
|
|
BizCollectionRegionStaff.del_flag == 0,
|
|
)
|
|
.first()
|
|
)
|
|
if binding is None:
|
|
raise ValueError(f'{api_name} 必须是目标采集地区人员')
|
|
|
|
|
|
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(),
|
|
current_collection=current_collection,
|
|
)
|
|
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,
|
|
'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 []),
|
|
'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 '',
|
|
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 '',
|
|
'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
|