协议样本、生产产出和盘点明细按行更新并保留原 id;补齐存储证明质检预填、离职人员显示名,以及样本采集通知模板字段。 Co-authored-by: Cursor <cursoragent@cursor.com>
552 lines
20 KiB
Python
552 lines
20 KiB
Python
"""储存证质检信息:从已有业务数据补空字段。读不到的键不写入。
|
||
|
||
阴阳性只取检测参数里的阴/阳,不根据任务状态或结论推断;参数未填则留空。
|
||
细菌检测默认「阴性」。围产期检测结果评论、脐带检测结果评判默认「合格」。
|
||
同一采集协议下其他样本的母血可补当前样本缺的母亲血液项;本样本已有结果优先。不共享脐血。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from decimal import Decimal, InvalidOperation
|
||
from typing import Any, Dict, Iterable, List, Optional
|
||
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.models.models import (
|
||
BizCellProduct,
|
||
BizProductionTask,
|
||
BizProductionTaskOutput,
|
||
BizProductionWorkOrder,
|
||
BizProtocolCollectionInfo,
|
||
BizProtocolSample,
|
||
BizQcTask,
|
||
BizQcTaskResult,
|
||
BizQcWorkOrder,
|
||
BizQcWorkOrderInspectionBatch,
|
||
BizSampleProductionReception,
|
||
BizSampleReception,
|
||
)
|
||
|
||
_FIXED_COMMON = {"storage_years": "20年"}
|
||
_FIXED_UC_MSC = {
|
||
"available_cell_type": "间充质干细胞",
|
||
"available_cell_morphology": "短梭/贴壁型",
|
||
"available_immunophenotype": "符合间充质干细胞表型特性",
|
||
"freeze_method": "程序降温盒降温",
|
||
"storage_temp": "-196℃",
|
||
}
|
||
|
||
|
||
def _compact(name: Optional[str]) -> str:
|
||
text = (name or "").strip().lower().replace("+", "+")
|
||
for ch in " \t\r\n**//()()【】\"'“”+":
|
||
text = text.replace(ch, "")
|
||
return text
|
||
|
||
|
||
def match_cd34_param(param_name: Optional[str]) -> Optional[str]:
|
||
"""CD34+细胞流式参数名 → 储存证字段。CD45dim 优先于 CD34 总数。"""
|
||
compact = _compact(param_name)
|
||
if not compact:
|
||
return None
|
||
if "cd45dim" in compact:
|
||
return "cd34_cd45dim_1e5"
|
||
if "tnc" in compact or "有核细胞" in compact:
|
||
return "tnc_1e8"
|
||
if "cd34" in compact and "cd45" not in compact:
|
||
return "cd34_count_1e6"
|
||
return None
|
||
|
||
|
||
def is_cd34_flow_item(item_name: Optional[str]) -> bool:
|
||
compact = _compact(item_name)
|
||
return "cd34" in compact and "流式" in compact
|
||
|
||
|
||
def match_process_qc_field(item_name: Optional[str]) -> Optional[str]:
|
||
name = item_name or ""
|
||
if "内毒素" in name:
|
||
return "endotoxin_test"
|
||
if "真菌" in name:
|
||
return "fungus_test"
|
||
if "支原体" in name:
|
||
return "mycoplasma_test"
|
||
return None
|
||
|
||
|
||
def qual_yin_yang(values: Iterable[str]) -> str:
|
||
"""只接受明确的阴/阳。通过、未检出、阴阳性同时出现都留空。"""
|
||
found = set()
|
||
for raw in values:
|
||
text = (raw or "").strip()
|
||
if text in ("阴", "阴性"):
|
||
found.add("阴")
|
||
elif text in ("阳", "阳性"):
|
||
found.add("阳")
|
||
if len(found) == 1:
|
||
return next(iter(found))
|
||
return ""
|
||
|
||
|
||
def normalize_negative_positive(value: Optional[str]) -> str:
|
||
"""储存证阴阳性下拉用「阴性 / 阳性」。"""
|
||
text = (value or "").strip()
|
||
if text in ("阳", "阳性"):
|
||
return "阳性"
|
||
if text in ("阴", "阴性"):
|
||
return "阴性"
|
||
return ""
|
||
|
||
|
||
def _fold_name(name: Optional[str]) -> str:
|
||
return (name or "").replace(" ", "").replace(" ", "").upper()
|
||
|
||
|
||
def batch_kind_from_name(batch_name: Optional[str]) -> str:
|
||
"""检测批次名含母血、脐血时区分储存证来源。母血优先,避免名称里同时出现时串项。"""
|
||
folded = _fold_name(batch_name)
|
||
if "母血" in folded:
|
||
return "母血"
|
||
if "脐血" in folded:
|
||
return "脐血"
|
||
return ""
|
||
|
||
|
||
def _first_negative_positive(values: Iterable[str]) -> str:
|
||
found = []
|
||
for raw in values:
|
||
qual = normalize_negative_positive(raw)
|
||
if qual and qual not in found:
|
||
found.append(qual)
|
||
if "阳性" in found:
|
||
return "阳性"
|
||
if "阴性" in found:
|
||
return "阴性"
|
||
return ""
|
||
|
||
|
||
def _item_has(item_name: Optional[str], *parts: str) -> bool:
|
||
folded = _fold_name(item_name)
|
||
return all(part.upper() in folded for part in parts)
|
||
|
||
|
||
def _param_surface_antigen(param_name: Optional[str]) -> bool:
|
||
folded = _fold_name(param_name)
|
||
return "表面抗原" in folded and "抗体" not in folded
|
||
|
||
|
||
# (字段, 批次, 项目是否命中, 参数是否命中;参数为空则取该项目里能识别的阴/阳)
|
||
_HSC_SEROLOGY_RULES = (
|
||
("hbsag", "脐血", lambda name: _item_has(name, "乙肝五项"), _param_surface_antigen),
|
||
("anti_hcv", "脐血", lambda name: _item_has(name, "抗丙型肝炎病毒抗体"), None),
|
||
("anti_tp", "脐血", lambda name: _item_has(name, "梅毒", "抗体"), None),
|
||
("anti_hiv", "脐血", lambda name: _item_has(name, "抗人类免疫缺陷病毒抗体"), None),
|
||
("cmv_igm", "脐血", lambda name: _item_has(name, "巨细胞") and _item_has(name, "IGM"), None),
|
||
("mother_hbsag", "母血", lambda name: _item_has(name, "乙肝五项"), _param_surface_antigen),
|
||
(
|
||
"mother_hbv_dna",
|
||
"母血",
|
||
lambda name: _item_has(name, "乙型肝炎病毒DNA") or _item_has(name, "乙型肝炎病毒核酸"),
|
||
None,
|
||
),
|
||
("mother_anti_hcv", "母血", lambda name: _item_has(name, "抗丙型肝炎病毒抗体"), None),
|
||
(
|
||
"mother_hcv_rna",
|
||
"母血",
|
||
lambda name: _item_has(name, "丙型肝炎病毒RNA") or _item_has(name, "丙型肝炎病毒核酸"),
|
||
None,
|
||
),
|
||
("mother_anti_tp", "母血", lambda name: _item_has(name, "梅毒", "抗体"), None),
|
||
("mother_cmv_igm", "母血", lambda name: _item_has(name, "巨细胞") and _item_has(name, "IGM"), None),
|
||
("mother_anti_hiv", "母血", lambda name: _item_has(name, "抗人类免疫缺陷病毒抗体"), None),
|
||
(
|
||
"mother_hiv1_rna",
|
||
"母血",
|
||
lambda name: _item_has(name, "人类免疫缺陷病毒", "核酸") or _item_has(name, "HIV-1RNA") or _item_has(name, "HIV1RNA"),
|
||
None,
|
||
),
|
||
)
|
||
|
||
_MOTHER_SEROLOGY_RULES = tuple(rule for rule in _HSC_SEROLOGY_RULES if str(rule[0]).startswith("mother_"))
|
||
|
||
|
||
def _serology_from_tasks(
|
||
tasks: List[Dict[str, Any]],
|
||
rules=_HSC_SEROLOGY_RULES,
|
||
) -> Dict[str, str]:
|
||
out: Dict[str, str] = {}
|
||
for field, kind, item_ok, param_ok in rules:
|
||
matched = [
|
||
task
|
||
for task in tasks
|
||
if task.get("batch_kind") == kind and item_ok(task.get("item_name") or "")
|
||
]
|
||
if not matched:
|
||
continue
|
||
chosen = max(matched, key=lambda task: task["stamp"])
|
||
values = []
|
||
for param_name, param_value in chosen.get("params") or []:
|
||
if param_ok is not None and not param_ok(param_name):
|
||
continue
|
||
values.append(param_value)
|
||
qual = _first_negative_positive(values)
|
||
if qual:
|
||
out[field] = qual
|
||
return out
|
||
|
||
|
||
def format_number(value: Any) -> str:
|
||
if value is None or value == "":
|
||
return ""
|
||
try:
|
||
number = Decimal(str(value))
|
||
except (InvalidOperation, ValueError):
|
||
return ""
|
||
text = format(number, "f")
|
||
if "." in text:
|
||
text = text.rstrip("0").rstrip(".")
|
||
return text
|
||
|
||
|
||
def sum_cryo_volume(weight_g: Any, volume_ml: Any) -> str:
|
||
"""富集后血量与冻存液用量都有才相加。"""
|
||
if weight_g is None or volume_ml is None or weight_g == "" or volume_ml == "":
|
||
return ""
|
||
try:
|
||
total = Decimal(str(weight_g)) + Decimal(str(volume_ml))
|
||
except (InvalidOperation, ValueError):
|
||
return ""
|
||
return format_number(total)
|
||
|
||
|
||
def format_datetime(value: Any) -> str:
|
||
if isinstance(value, datetime):
|
||
return value.strftime("%Y-%m-%d %H:%M")
|
||
return ""
|
||
|
||
|
||
def format_date(value: Any) -> str:
|
||
if isinstance(value, datetime):
|
||
return value.strftime("%Y-%m-%d")
|
||
return ""
|
||
|
||
|
||
def merge_prefill(saved: Optional[Dict[str, Any]], suggested: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||
"""已有非空内容保留,只补空格。"""
|
||
out: Dict[str, Any] = dict(saved or {})
|
||
for key, value in (suggested or {}).items():
|
||
if value is None or str(value).strip() == "":
|
||
continue
|
||
current = out.get(key)
|
||
if current is None or str(current).strip() == "":
|
||
out[key] = value
|
||
return out
|
||
|
||
|
||
def _latest_task_params(tasks: List[Dict[str, Any]], predicate) -> List[str]:
|
||
matched = [task for task in tasks if predicate(task)]
|
||
if not matched:
|
||
return []
|
||
chosen = max(matched, key=lambda task: task["stamp"])
|
||
return list(chosen["values"])
|
||
|
||
|
||
def _cd34_from_tasks(tasks: List[Dict[str, Any]]) -> Dict[str, str]:
|
||
flow_tasks = [task for task in tasks if is_cd34_flow_item(task["item_name"])]
|
||
if not flow_tasks:
|
||
return {}
|
||
chosen = max(flow_tasks, key=lambda task: task["stamp"])
|
||
out: Dict[str, str] = {}
|
||
for param_name, param_value in chosen["params"]:
|
||
field = match_cd34_param(param_name)
|
||
text = (param_value or "").strip()
|
||
if field and text and field not in out:
|
||
out[field] = text
|
||
return out
|
||
|
||
|
||
def _process_qual_from_tasks(tasks: List[Dict[str, Any]]) -> Dict[str, str]:
|
||
"""生产过程质检:内毒素、真菌、支原体。只认参数里的阴/阳,任务结论「通过」不算。"""
|
||
out: Dict[str, str] = {}
|
||
for field in ("endotoxin_test", "fungus_test", "mycoplasma_test"):
|
||
values = _latest_task_params(
|
||
tasks, lambda task, field=field: match_process_qc_field(task["item_name"]) == field
|
||
)
|
||
qual = _first_negative_positive(values)
|
||
if qual:
|
||
out[field] = qual
|
||
return out
|
||
|
||
|
||
def _fill_shared_mother(suggested: Dict[str, str], tasks: Optional[List[Dict[str, Any]]]) -> None:
|
||
"""只补母亲血液空项。共享任务里的脐血不参与。"""
|
||
shared = _serology_from_tasks(tasks or [], _MOTHER_SEROLOGY_RULES)
|
||
for key, value in shared.items():
|
||
suggested.setdefault(key, value)
|
||
|
||
|
||
def build_suggested(
|
||
*,
|
||
sample_type_key: str,
|
||
collection_time: Any = None,
|
||
reception_time: Any = None,
|
||
receive_temperature: Any = None,
|
||
enriched_blood_weight_g: Any = None,
|
||
cryopreservation_volume_ml: Any = None,
|
||
storage_total_tubes: Any = None,
|
||
available_cell_count_1e7: Any = None,
|
||
gate_tasks: Optional[List[Dict[str, Any]]] = None,
|
||
process_tasks: Optional[List[Dict[str, Any]]] = None,
|
||
shared_mother_tasks: Optional[List[Dict[str, Any]]] = None,
|
||
latest_freeze_date: Any = None,
|
||
) -> Dict[str, str]:
|
||
suggested: Dict[str, str] = {}
|
||
collected = format_datetime(collection_time)
|
||
if collected:
|
||
suggested["collection_time"] = collected
|
||
received = format_datetime(reception_time)
|
||
if received:
|
||
suggested["receive_time"] = received
|
||
freeze = format_date(latest_freeze_date)
|
||
if sample_type_key == "perinatal_hsc":
|
||
volume = sum_cryo_volume(enriched_blood_weight_g, cryopreservation_volume_ml)
|
||
if volume:
|
||
suggested["cryo_volume_ml"] = volume
|
||
suggested.update(_cd34_from_tasks(gate_tasks or []))
|
||
suggested.update(_serology_from_tasks(gate_tasks or []))
|
||
suggested.setdefault("bacteria_test", "阴性")
|
||
suggested.setdefault("result_comment", "合格")
|
||
if freeze:
|
||
suggested["storage_date"] = freeze
|
||
suggested.update(_FIXED_COMMON)
|
||
elif sample_type_key == "uc_msc":
|
||
temp = ("" if receive_temperature is None else str(receive_temperature)).strip()
|
||
if temp:
|
||
suggested["transport_temp_c"] = temp
|
||
tubes = format_number(storage_total_tubes)
|
||
if tubes:
|
||
suggested["storage_tube_count"] = tubes
|
||
count = format_number(available_cell_count_1e7)
|
||
if count:
|
||
suggested["available_cell_count_1e7"] = count
|
||
suggested.update(_process_qual_from_tasks(process_tasks or []))
|
||
suggested.update(_serology_from_tasks(gate_tasks or [], _MOTHER_SEROLOGY_RULES))
|
||
suggested.setdefault("bacteria_test", "阴性")
|
||
suggested.setdefault("result_judgment", "合格")
|
||
if freeze:
|
||
suggested["storage_date_p2"] = freeze
|
||
suggested.update(_FIXED_COMMON)
|
||
suggested.update(_FIXED_UC_MSC)
|
||
if sample_type_key in ("perinatal_hsc", "uc_msc"):
|
||
_fill_shared_mother(suggested, shared_mother_tasks)
|
||
return {key: value for key, value in suggested.items() if str(value).strip()}
|
||
|
||
|
||
def _stamp(completed: Any, updated: Any) -> datetime:
|
||
if isinstance(completed, datetime):
|
||
return completed
|
||
if isinstance(updated, datetime):
|
||
return updated
|
||
return datetime.min
|
||
|
||
|
||
def _load_qc_tasks(db: Session, tenant_no: str, sample_code: str, trigger_source: str) -> List[Dict[str, Any]]:
|
||
rows = (
|
||
db.query(
|
||
BizQcTask.id,
|
||
BizQcTask.item_name,
|
||
BizQcTask.completed_time,
|
||
BizQcTask.update_time,
|
||
BizQcWorkOrderInspectionBatch.batch_name,
|
||
BizQcTaskResult.param_name,
|
||
BizQcTaskResult.param_value,
|
||
)
|
||
.join(BizQcWorkOrder, BizQcWorkOrder.id == BizQcTask.work_order_id)
|
||
.outerjoin(
|
||
BizQcWorkOrderInspectionBatch,
|
||
BizQcWorkOrderInspectionBatch.id == BizQcTask.batch_id,
|
||
)
|
||
.outerjoin(
|
||
BizQcTaskResult,
|
||
(BizQcTaskResult.task_id == BizQcTask.id) & (BizQcTaskResult.del_flag == 0),
|
||
)
|
||
.filter(
|
||
BizQcWorkOrder.tenant_no == tenant_no,
|
||
BizQcWorkOrder.sample_code == sample_code,
|
||
BizQcWorkOrder.del_flag == 0,
|
||
BizQcWorkOrder.trigger_source == trigger_source,
|
||
BizQcTask.tenant_no == tenant_no,
|
||
BizQcTask.del_flag == 0,
|
||
BizQcTask.task_status == "ywc",
|
||
)
|
||
.all()
|
||
)
|
||
grouped: Dict[str, Dict[str, Any]] = {}
|
||
for row in rows:
|
||
task = grouped.setdefault(
|
||
row.id,
|
||
{
|
||
"item_name": row.item_name or "",
|
||
"batch_kind": batch_kind_from_name(row.batch_name),
|
||
"stamp": _stamp(row.completed_time, row.update_time),
|
||
"params": [],
|
||
"values": [],
|
||
},
|
||
)
|
||
if (row.param_name or "").strip() or (row.param_value or "").strip():
|
||
task["params"].append((row.param_name or "", row.param_value or ""))
|
||
if (row.param_value or "").strip():
|
||
task["values"].append(row.param_value or "")
|
||
return list(grouped.values())
|
||
|
||
|
||
def _sibling_sample_codes(db: Session, tenant_no: str, sample_code: str) -> List[str]:
|
||
"""同一采集协议下的其他样本。母血通常只做在其中一份上。"""
|
||
owner = (
|
||
db.query(BizProtocolSample.collection_protocol_id)
|
||
.filter(
|
||
BizProtocolSample.tenant_no == tenant_no,
|
||
BizProtocolSample.sample_code == sample_code,
|
||
BizProtocolSample.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
protocol_id = (owner.collection_protocol_id or "").strip() if owner else ""
|
||
if not protocol_id:
|
||
return []
|
||
rows = (
|
||
db.query(BizProtocolSample.sample_code)
|
||
.filter(
|
||
BizProtocolSample.tenant_no == tenant_no,
|
||
BizProtocolSample.collection_protocol_id == protocol_id,
|
||
BizProtocolSample.sample_code != sample_code,
|
||
BizProtocolSample.del_flag == 0,
|
||
)
|
||
.all()
|
||
)
|
||
codes = []
|
||
for row in rows:
|
||
code = (row.sample_code or "").strip()
|
||
if code and code not in codes:
|
||
codes.append(code)
|
||
return codes
|
||
|
||
|
||
def _load_shared_mother_tasks(db: Session, tenant_no: str, sample_code: str) -> List[Dict[str, Any]]:
|
||
tasks: List[Dict[str, Any]] = []
|
||
for code in _sibling_sample_codes(db, tenant_no, sample_code):
|
||
tasks.extend(
|
||
task
|
||
for task in _load_qc_tasks(db, tenant_no, code, "reception")
|
||
if task.get("batch_kind") == "母血"
|
||
)
|
||
return tasks
|
||
|
||
|
||
def _latest_freeze_date(db: Session, tenant_no: str, sample_code: str) -> Optional[datetime]:
|
||
output_dates = (
|
||
db.query(BizCellProduct.freeze_date)
|
||
.join(BizProductionTaskOutput, BizProductionTaskOutput.cell_product_id == BizCellProduct.id)
|
||
.join(BizProductionTask, BizProductionTask.id == BizProductionTaskOutput.task_id)
|
||
.join(BizProductionWorkOrder, BizProductionWorkOrder.id == BizProductionTask.work_order_id)
|
||
.filter(
|
||
BizProductionWorkOrder.tenant_no == tenant_no,
|
||
BizProductionWorkOrder.sample_code == sample_code,
|
||
BizProductionWorkOrder.del_flag == 0,
|
||
BizProductionWorkOrder.work_order_status != "yzf",
|
||
BizProductionTask.tenant_no == tenant_no,
|
||
BizProductionTask.del_flag == 0,
|
||
BizProductionTaskOutput.tenant_no == tenant_no,
|
||
BizProductionTaskOutput.del_flag == 0,
|
||
BizCellProduct.tenant_no == tenant_no,
|
||
BizCellProduct.del_flag == 0,
|
||
BizCellProduct.product_status != "voided",
|
||
BizCellProduct.freeze_date.isnot(None),
|
||
)
|
||
.all()
|
||
)
|
||
linked_dates = (
|
||
db.query(BizCellProduct.freeze_date)
|
||
.join(BizProductionTask, BizProductionTask.id == BizCellProduct.production_task_id)
|
||
.join(BizProductionWorkOrder, BizProductionWorkOrder.id == BizProductionTask.work_order_id)
|
||
.filter(
|
||
BizProductionWorkOrder.tenant_no == tenant_no,
|
||
BizProductionWorkOrder.sample_code == sample_code,
|
||
BizProductionWorkOrder.del_flag == 0,
|
||
BizProductionWorkOrder.work_order_status != "yzf",
|
||
BizProductionTask.tenant_no == tenant_no,
|
||
BizProductionTask.del_flag == 0,
|
||
BizCellProduct.tenant_no == tenant_no,
|
||
BizCellProduct.del_flag == 0,
|
||
BizCellProduct.product_status != "voided",
|
||
BizCellProduct.freeze_date.isnot(None),
|
||
)
|
||
.all()
|
||
)
|
||
dates = [row.freeze_date for row in output_dates + linked_dates if isinstance(row.freeze_date, datetime)]
|
||
if not dates:
|
||
return None
|
||
return max(dates)
|
||
|
||
|
||
def collect_prefill(db: Session, tenant_no: str, sample_code: str, sample_type_key: Optional[str]) -> Dict[str, str]:
|
||
if sample_type_key not in ("perinatal_hsc", "uc_msc"):
|
||
return {}
|
||
code = (sample_code or "").strip()
|
||
if not code:
|
||
return {}
|
||
collection = (
|
||
db.query(BizProtocolCollectionInfo.sample_collect_time)
|
||
.join(
|
||
BizProtocolSample,
|
||
BizProtocolSample.collection_protocol_id == BizProtocolCollectionInfo.collection_protocol_id,
|
||
)
|
||
.filter(
|
||
BizProtocolSample.tenant_no == tenant_no,
|
||
BizProtocolSample.sample_code == code,
|
||
BizProtocolSample.del_flag == 0,
|
||
BizProtocolCollectionInfo.tenant_no == tenant_no,
|
||
BizProtocolCollectionInfo.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
collection_time = collection.sample_collect_time if collection else None
|
||
|
||
reception = (
|
||
db.query(BizSampleReception)
|
||
.filter(
|
||
BizSampleReception.tenant_no == tenant_no,
|
||
BizSampleReception.sample_code == code,
|
||
BizSampleReception.del_flag == 0,
|
||
BizSampleReception.reception_source == "cell_storage",
|
||
BizSampleReception.accept_conclusion == "js",
|
||
)
|
||
.order_by(BizSampleReception.reception_time.desc())
|
||
.first()
|
||
)
|
||
production = None
|
||
if reception is not None:
|
||
production = (
|
||
db.query(BizSampleProductionReception)
|
||
.filter(
|
||
BizSampleProductionReception.tenant_no == tenant_no,
|
||
BizSampleProductionReception.reception_id == reception.id,
|
||
BizSampleProductionReception.del_flag == 0,
|
||
)
|
||
.first()
|
||
)
|
||
return build_suggested(
|
||
sample_type_key=sample_type_key,
|
||
collection_time=collection_time,
|
||
reception_time=None if reception is None else reception.reception_time,
|
||
receive_temperature=None if reception is None else reception.receive_temperature,
|
||
enriched_blood_weight_g=None if production is None else production.enriched_blood_weight_g,
|
||
cryopreservation_volume_ml=None if production is None else production.cryopreservation_volume_ml,
|
||
storage_total_tubes=None if production is None else production.storage_total_tubes,
|
||
available_cell_count_1e7=None if production is None else production.available_cell_count_1e7,
|
||
gate_tasks=_load_qc_tasks(db, tenant_no, code, "reception"),
|
||
process_tasks=_load_qc_tasks(db, tenant_no, code, "production"),
|
||
shared_mother_tasks=_load_shared_mother_tasks(db, tenant_no, code),
|
||
latest_freeze_date=_latest_freeze_date(db, tenant_no, code),
|
||
)
|