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