Files
dpb/backend/app/api/v1/module_bre/selection_result/service.py
T
34047007@qq.com b95053c52c init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
2026-08-06 00:17:49 +08:00

552 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from typing import Any
from fastapi import UploadFile
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.base_schema import AuthSchema, PageResultSchema, ImportResultSchema
from app.core.exceptions import CustomException
from app.core.logger import logger
from app.utils.common_util import search_to_dict
from app.utils.excel_util import ExcelUtil
from app.utils.dict_util import DictLabelResolver, dict_value_to_label
from app.utils.number_gen import NumberGenService
from .crud import BreedingSelectionResultCRUD
from .schema import (
SelectionResultBatchCreateResultSchema,
SelectionResultBatchCreateSchema,
SelectionResultCreateSchema,
SelectionResultOutSchema,
SelectionResultQueryParam,
SelectionResultUpdateSchema,
)
from app.api.v1.module_bre.cross_combination.crud import BreedingCrossCombinationCRUD
from app.api.v1.module_bre.tree.crud import BreedingTreeCRUD
from app.api.v1.module_bre.clone.crud import BreedingCloneCRUD
from app.api.v1.module_bre.selection_rule.crud import BreedingSelectionRuleCRUD
from app.api.v1.module_bre.personnel.crud import BreedingPersonnelCRUD
from app.api.v1.module_bre.trial_study.crud import BreedingTrialStudyCRUD
from app.api.v1.module_bre.germplasm.crud import BreedingGermplasmCRUD
from app.api.v1.module_bre.pedigree.crud import BreedingPedigreeCRUD
from app.core.base_crud import assert_dict_values, assert_no_children, assert_parents_exist, assert_status_forward
from app.api.v1.module_bre.cross_combination.model import CrossCombinationModel
from app.api.v1.module_bre.tree.model import TreeModel
from app.api.v1.module_bre.clone.model import CloneModel
from app.api.v1.module_bre.selection_rule.model import SelectionRuleModel
from app.api.v1.module_bre.personnel.model import PersonnelModel
from app.api.v1.module_bre.trial_study.model import TrialStudyModel
# A1 晋级触发条件(§8.10):选育结论为正向选择且晋级阶段为无性系阶段时建 clone。
_PROMOTE_TO_STAGES = {"sp", "ap", "line", "regional_trial", "released"}
_POSITIVE_SELECT = {"selected", "primary", "key"}
# 晋级到品系及以后时,单株晋升为种质(建 bre_germplasm 并回填 tree.germplasm_id)。
_GERMPLASM_STAGES = {"line", "regional_trial", "released"}
_STAGE_ORDER = {
"germplasm": 0, "parent": 1, "seedling": 2,
"sp": 3, "ap": 4, "line": 5, "regional_trial": 6, "released": 7,
}
# identify_status(选育结论)落回单株状态(tree_status)的映射:两字典共享键,
# 正向结论写回 tree.status,使已入选/已淘汰的单株不再出现在存活候选池,避免重复筛选。
_TREE_STATUS_BY_SELECT = {
"selected": "selected",
"primary": "primary",
"key": "key",
"preserved": "preserved",
"eliminated": "eliminated",
}
def _is_blank(v: Any) -> bool:
return v is None or (isinstance(v, str) and v.strip() == "")
def _none_if_blank(v: Any) -> Any:
if _is_blank(v):
return None
return str(v).strip() if isinstance(v, str) else v
def _to_float(v: Any) -> float | None:
if _is_blank(v):
return None
try:
return float(v)
except (TypeError, ValueError):
return None
def _to_int(v: Any) -> int | None:
if _is_blank(v):
return None
try:
return int(float(v))
except (TypeError, ValueError):
return None
class SelectionResultService:
"""选育结果 模块服务层"""
def __init__(self, auth: AuthSchema, db: AsyncSession) -> None:
self.auth = auth
self.db = db
async def _attach_fk_labels(self, items: list[SelectionResultOutSchema]) -> None:
if not items:
return
crud = BreedingSelectionResultCRUD(self.auth, self.db)
combination_id_ids = {getattr(it, "combination_id") for it in items if getattr(it, "combination_id")}
if combination_id_ids:
refs = await BreedingCrossCombinationCRUD(self.auth, self.db).get_list(search={"id": ("in", list(combination_id_ids))})
ref_map = {r.id: getattr(r, "combination_code") for r in refs}
for it in items:
it.combination_name = ref_map.get(getattr(it, "combination_id"))
tree_id_ids = {getattr(it, "tree_id") for it in items if getattr(it, "tree_id")}
if tree_id_ids:
refs = await BreedingTreeCRUD(self.auth, self.db).get_list(search={"id": ("in", list(tree_id_ids))})
ref_map = {r.id: getattr(r, "tree_no") for r in refs}
for it in items:
it.tree_name = ref_map.get(getattr(it, "tree_id"))
clone_id_ids = {getattr(it, "clone_id") for it in items if getattr(it, "clone_id")}
if clone_id_ids:
refs = await BreedingCloneCRUD(self.auth, self.db).get_list(search={"id": ("in", list(clone_id_ids))})
ref_map = {r.id: getattr(r, "clone_code") for r in refs}
for it in items:
it.clone_name = ref_map.get(getattr(it, "clone_id"))
rule_id_ids = {getattr(it, "rule_id") for it in items if getattr(it, "rule_id")}
if rule_id_ids:
refs = await BreedingSelectionRuleCRUD(self.auth, self.db).get_list(search={"id": ("in", list(rule_id_ids))})
ref_map = {r.id: getattr(r, "rule_name") for r in refs}
for it in items:
it.rule_name = ref_map.get(getattr(it, "rule_id"))
approved_by_ids = {getattr(it, "approved_by") for it in items if getattr(it, "approved_by")}
if approved_by_ids:
refs = await BreedingPersonnelCRUD(self.auth, self.db).get_list(search={"id": ("in", list(approved_by_ids))})
ref_map = {r.id: getattr(r, "name") for r in refs}
for it in items:
it.approved_by_name = ref_map.get(getattr(it, "approved_by"))
trial_study_id_ids = {getattr(it, "trial_study_id") for it in items if getattr(it, "trial_study_id")}
if trial_study_id_ids:
refs = await BreedingTrialStudyCRUD(self.auth, self.db).get_list(search={"id": ("in", list(trial_study_id_ids))})
ref_map = {r.id: getattr(r, "study_name") for r in refs}
for it in items:
it.trial_study_name = ref_map.get(getattr(it, "trial_study_id"))
async def detail(self, id: int) -> SelectionResultOutSchema:
obj = await BreedingSelectionResultCRUD(self.auth, self.db).get(id=id)
if not obj:
raise CustomException(msg="该选育结果不存在")
out = SelectionResultOutSchema.model_validate(obj)
await self._attach_fk_labels([out])
return out
async def get_list(
self,
search: SelectionResultQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> list[SelectionResultOutSchema]:
obj_list = await BreedingSelectionResultCRUD(self.auth, self.db).get_list(
search=search_to_dict(search), order_by=order_by
)
outs = [SelectionResultOutSchema.model_validate(obj) for obj in obj_list]
await self._attach_fk_labels(outs)
return outs
async def page(
self,
page_no: int,
page_size: int,
search: SelectionResultQueryParam | None = None,
order_by: list[dict[str, str]] | None = None,
) -> PageResultSchema[SelectionResultOutSchema]:
offset = (page_no - 1) * page_size
result = await BreedingSelectionResultCRUD(self.auth, self.db).page(
offset=offset,
limit=page_size,
order_by=order_by or [{"id": "asc"}],
search=search_to_dict(search, {}),
out_schema=SelectionResultOutSchema,
)
await self._attach_fk_labels(result.items)
return result
async def _fetch_tree_no(self, tree_id: int) -> str:
tree = await BreedingTreeCRUD(self.auth, self.db).get(id=tree_id)
return (tree.tree_no or "") if tree else ""
@staticmethod
def _year_of(v: Any) -> int | None:
"""从 'YYYY-MM-DD' / 'YYYY' 等字符串提取年份。"""
if _is_blank(v):
return None
head = str(v).strip()[:4]
return int(head) if head.isdigit() else None
async def _promote_tree_to_clone(self, tree_id: int, to_stage: str) -> int | None:
"""A1 入选晋升才建 clone(§8.10)。命中晋级条件时:
单株尚无无性系 → 按 {组合编号}-{序号:04d} 新建 bre_clone(复选沿用既有,不重复建);
推进 tree.stage=to_stage(不回退);晋级到品系及以后且未关联种质 →
以 clone_code 为品种名新建 bre_germplasm 并回填 tree.germplasm_id。
返回应关联的无性系 id(新建或既有;无则 None)。
"""
if to_stage not in _PROMOTE_TO_STAGES:
return None
tree = await BreedingTreeCRUD(self.auth, self.db).get(id=tree_id)
if not tree:
return None
if tree.stage in _STAGE_ORDER and to_stage in _STAGE_ORDER \
and _STAGE_ORDER[tree.stage] > _STAGE_ORDER[to_stage]:
return None # 不回退阶段
clone_id = tree.clone_id
combo = None
if clone_id is None:
combo = await BreedingCrossCombinationCRUD(self.auth, self.db).get(id=tree.combination_id)
combo_code = combo.combination_code if combo else ""
async def _seed() -> int | None:
existing = await BreedingCloneCRUD(self.auth, self.db).get_list(search={"combination_id": tree.combination_id})
seq = 0
for c in existing:
tail = (c.clone_code or "").rsplit("-", 1)[-1]
if tail.isdigit():
seq = max(seq, int(tail))
return seq + 1
seq = await NumberGenService(self.db).next_seq(f"clone:{tree.combination_id}", seed_fn=_seed)
clone_code = f"{combo_code}-{seq:04d}" if combo_code else f"CL{seq:04d}"
clone = await BreedingCloneCRUD(self.auth, self.db).create(data={
"clone_code": clone_code,
"combination_id": tree.combination_id,
"female_parent_id": tree.dam_id if tree.dam_id is not None else (combo.female_parent_id if combo else None),
"male_parent_id": tree.sire_id if tree.sire_id is not None else (combo.male_parent_id if combo else None),
"planting_year": self._year_of(tree.planted_date),
"generation": tree.generation,
"status": "1",
})
clone_id = clone.id
tree_update = {"clone_id": clone.id}
else:
_c = await BreedingCloneCRUD(self.auth, self.db).get(id=clone_id)
clone_code = _c.clone_code if _c else f"CL{clone_id}"
tree_update = {}
if tree.stage != to_stage:
tree_update["stage"] = to_stage
if tree_update:
await BreedingTreeCRUD(self.auth, self.db).update(id=tree.id, data=tree_update)
if to_stage in _GERMPLASM_STAGES and tree.germplasm_id is None:
germ = await BreedingGermplasmCRUD(self.auth, self.db).create(data={
"cultivar_name": clone_code,
"generation": tree.generation,
"stage": to_stage,
})
await BreedingTreeCRUD(self.auth, self.db).update(id=tree.id, data={"germplasm_id": germ.id})
# 世代闭环:晋升种质须同步落 bre_pedigreechild_code=品种名,dam/sire=种质级亲本)。
# 否则统计引擎 _build_pedigree 按 child_code 匹配不到 → 该种质被当 founder,
# 跨世代系谱链(A-BLUP/亲缘/近交规避/遗传增益滚雪球)断裂。
if combo is None:
combo = await BreedingCrossCombinationCRUD(self.auth, self.db).get(id=tree.combination_id)
dam_id = tree.dam_id if tree.dam_id is not None else (combo.female_parent_id if combo else None)
sire_id = tree.sire_id if tree.sire_id is not None else (combo.male_parent_id if combo else None)
if dam_id is not None or sire_id is not None:
existing_ped = await BreedingPedigreeCRUD(self.auth, self.db).get_list(
search={"child_code": clone_code})
if not existing_ped:
await BreedingPedigreeCRUD(self.auth, self.db).create(data={
"combination_id": tree.combination_id,
"child_code": clone_code,
"dam_id": dam_id,
"sire_id": sire_id,
"generation": tree.generation,
"remark": "单株晋级自动落系谱(世代闭环)",
})
return clone_id
async def _assert_stage_forward(self, tree_id: int, new_value: str | None) -> None:
"""选育结论状态机:不早于该单株此前已到达的最晚阶段。"""
if not new_value:
return
existing = await BreedingSelectionResultCRUD(self.auth, self.db).get_list(search={"tree_id": tree_id})
await assert_status_forward(
self.db, "identify_status", [e.is_selected for e in existing], new_value, "选育结论"
)
async def _sync_tree_status(self, tree_id: int, is_selected: str | None) -> None:
"""选育结论落回单株状态(tree_status):只进不退,已入选/已淘汰的单株
不再出现在存活候选池,避免重复筛选。"""
if not is_selected or is_selected not in _TREE_STATUS_BY_SELECT:
return
tree = await BreedingTreeCRUD(self.auth, self.db).get(id=tree_id)
if not tree:
return
new_status = _TREE_STATUS_BY_SELECT[is_selected]
await assert_status_forward(self.db, "tree_status", [tree.status], new_status, "单株状态")
if tree.status != new_status:
await BreedingTreeCRUD(self.auth, self.db).update(id=tree.id, data={"status": new_status})
async def create(self, data: SelectionResultCreateSchema) -> SelectionResultOutSchema:
await assert_parents_exist(
self.db,
[
(CrossCombinationModel, data.combination_id, '杂交组合'),
(TreeModel, data.tree_id, '单株'),
(CloneModel, data.clone_id, '晋级无性系'),
(SelectionRuleModel, data.rule_id, '选育规则'),
(PersonnelModel, data.approved_by, '审核人'),
(TrialStudyModel, data.trial_study_id, '试验研究点'),
],
)
await assert_dict_values(
self.db,
[
('identify_status', data.is_selected, '选育结论'),
('breeding_stage', data.from_stage, '起始阶段'),
('breeding_stage', data.to_stage, '晋级阶段'),
],
)
await self._assert_stage_forward(data.tree_id, data.is_selected)
fields = data.model_dump(exclude_none=True)
fields["tree_no"] = await self._fetch_tree_no(data.tree_id)
obj = await BreedingSelectionResultCRUD(self.auth, self.db).create(data=fields)
if data.is_selected in _POSITIVE_SELECT:
clone_id = await self._promote_tree_to_clone(data.tree_id, data.to_stage)
if clone_id is not None and obj.clone_id is None:
obj.clone_id = clone_id
await self._sync_tree_status(data.tree_id, data.is_selected)
out = SelectionResultOutSchema.model_validate(obj)
await self._attach_fk_labels([out])
return out
async def create_batch(self, data: SelectionResultBatchCreateSchema) -> SelectionResultBatchCreateResultSchema:
"""批量评选:一批单株共享同一评审结论,单事务内逐棵复用 create()
(含校验 / A1 晋升 / 状态回写),一棵失败不影响其他。"""
if not data.tree_id:
raise CustomException(msg="批量评选失败,请至少选择一棵单株")
combo = await BreedingCrossCombinationCRUD(self.auth, self.db).get(id=data.combination_id)
if not combo:
raise CustomException(msg="批量评选失败,该杂交组合不存在")
common = data.model_dump(exclude={"combination_id", "tree_id"}, exclude_none=True)
success_ids: list[int] = []
fail_details: list[dict] = []
for tid in data.tree_id:
try:
create_data = SelectionResultCreateSchema(
combination_id=data.combination_id,
tree_id=tid,
**common,
)
obj = await self.create(create_data)
if obj.id is not None:
success_ids.append(obj.id)
except Exception as e:
tree_no = await self._fetch_tree_no(tid)
fail_details.append({"tree_id": tid, "tree_no": tree_no or f"#{tid}", "msg": f"{e!s}"})
return SelectionResultBatchCreateResultSchema(
success_count=len(success_ids),
fail_count=len(fail_details),
success_ids=success_ids,
fail_details=fail_details,
)
async def update(self, id: int, data: SelectionResultUpdateSchema) -> SelectionResultOutSchema:
obj = await BreedingSelectionResultCRUD(self.auth, self.db).get(id=id)
if not obj:
raise CustomException(msg="更新失败,该选育结果不存在")
await assert_parents_exist(
self.db,
[
(CrossCombinationModel, data.combination_id, '杂交组合'),
(TreeModel, data.tree_id, '单株'),
(CloneModel, data.clone_id, '晋级无性系'),
(SelectionRuleModel, data.rule_id, '选育规则'),
(PersonnelModel, data.approved_by, '审核人'),
(TrialStudyModel, data.trial_study_id, '试验研究点'),
],
)
await assert_dict_values(
self.db,
[
('identify_status', data.is_selected, '选育结论'),
('breeding_stage', data.from_stage, '起始阶段'),
('breeding_stage', data.to_stage, '晋级阶段'),
],
)
tree_id = data.tree_id if data.tree_id is not None else obj.tree_id
await self._assert_stage_forward(tree_id, data.is_selected)
fields = data.model_dump(exclude_unset=True, exclude_none=True)
if "tree_id" in fields:
fields["tree_no"] = await self._fetch_tree_no(fields["tree_id"])
obj = await BreedingSelectionResultCRUD(self.auth, self.db).update(id=id, data=fields)
is_selected = data.is_selected if data.is_selected is not None else obj.is_selected
to_stage = data.to_stage if data.to_stage is not None else obj.to_stage
if is_selected in _POSITIVE_SELECT:
clone_id = await self._promote_tree_to_clone(tree_id, to_stage)
if clone_id is not None and obj.clone_id is None:
obj.clone_id = clone_id
await self._sync_tree_status(tree_id, is_selected)
out = SelectionResultOutSchema.model_validate(obj)
await self._attach_fk_labels([out])
return out
async def delete(self, ids: list[int]) -> None:
if not ids:
raise CustomException(msg="删除失败,删除对象不能为空")
objs = await BreedingSelectionResultCRUD(self.auth, self.db).get_list(search={"id": ("in", ids)})
obj_map = {o.id: o for o in objs}
for id_ in ids:
if id_ not in obj_map:
raise CustomException(msg="删除失败,该选育结果不存在")
await BreedingSelectionResultCRUD(self.auth, self.db).delete(ids=ids)
async def list_options(self) -> list[dict[str, Any]]:
"""供前端下拉选择使用:返回 [{value, label}]。"""
obj_list = await BreedingSelectionResultCRUD(self.auth, self.db).get_list(order_by=[{"id": "asc"}])
return [{"value": o.id, "label": o.is_selected} for o in obj_list]
@staticmethod
def batch_export(obj_list: list[dict[str, Any]]) -> bytes:
mapping_dict = {
"combination_name": "杂交组合",
"tree_name": "单株",
"selection_year": "入选年份",
"is_selected": "选育结论",
"clone_name": "晋级无性系",
"rule_name": "选育规则",
"from_stage": "起始阶段",
"to_stage": "晋级阶段",
"approved_by_name": "审核人",
"trial_study_name": "试验研究点",
"reason": "入选理由",
"remark": "备注",
"created_time": "创建时间",
"created_by": "创建者",
}
data = [dict(item) for item in obj_list]
for item in data:
creator = item.get("created_by")
item["created_by"] = creator.get("name", "未知") if isinstance(creator, dict) else "未知"
item["is_selected"] = dict_value_to_label("identify_status", item.get("is_selected"))
item["from_stage"] = dict_value_to_label("breeding_stage", item.get("from_stage"))
item["to_stage"] = dict_value_to_label("breeding_stage", item.get("to_stage"))
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
async def batch_import(self, file: UploadFile, update_support: bool = False) -> ImportResultSchema:
header_dict = {
"杂交组合": "combination_id",
"单株": "tree_id",
"入选年份": "selection_year",
"选育结论": "is_selected",
"晋级无性系": "clone_id",
"选育规则": "rule_id",
"起始阶段": "from_stage",
"晋级阶段": "to_stage",
"审核人": "approved_by",
"试验研究点": "trial_study_id",
"入选理由": "reason",
"备注": "remark",
}
try:
contents = await file.read()
rows = ExcelUtil.read_excel_to_dicts(contents)
await file.close()
if not rows:
raise CustomException(msg="导入文件为空")
missing_headers = [h for h in header_dict if h not in rows[0]]
if missing_headers:
raise CustomException(msg=f"导入文件缺少必要的列: {', '.join(missing_headers)}")
combination_id_refs = await BreedingCrossCombinationCRUD(self.auth, self.db).get_list(order_by=[{"id": "asc"}])
combination_id_map = {getattr(r, "combination_code"): r.id for r in combination_id_refs}
tree_id_refs = await BreedingTreeCRUD(self.auth, self.db).get_list(order_by=[{"id": "asc"}])
tree_id_map = {getattr(r, "tree_no"): r.id for r in tree_id_refs}
clone_id_refs = await BreedingCloneCRUD(self.auth, self.db).get_list(order_by=[{"id": "asc"}])
clone_id_map = {getattr(r, "clone_code"): r.id for r in clone_id_refs}
rule_id_refs = await BreedingSelectionRuleCRUD(self.auth, self.db).get_list(order_by=[{"id": "asc"}])
rule_id_map = {getattr(r, "rule_name"): r.id for r in rule_id_refs}
approved_by_refs = await BreedingPersonnelCRUD(self.auth, self.db).get_list(order_by=[{"id": "asc"}])
approved_by_map = {getattr(r, "name"): r.id for r in approved_by_refs}
trial_study_id_refs = await BreedingTrialStudyCRUD(self.auth, self.db).get_list(order_by=[{"id": "asc"}])
trial_study_id_map = {getattr(r, "study_name"): r.id for r in trial_study_id_refs}
mapped_rows = []
for row in rows:
mapped_rows.append({en: row.get(ch) for ch, en in header_dict.items()})
required_fields = ["combination_id", "tree_id"]
errors = []
for field in required_fields:
missing_indices = [i + 1 for i, r in enumerate(mapped_rows) if _is_blank(r.get(field))]
if missing_indices:
field_name = next(k for k, v in header_dict.items() if v == field)
rows_str = "、".join(str(i) for i in missing_indices)
errors.append(f"{field_name}不能为空,第{rows_str}行")
if errors:
raise CustomException(msg=f"导入失败,以下行缺少必要字段:\n{'; '.join(errors)}")
error_msgs: list[str] = []
success_count = 0
resolver = DictLabelResolver(self.auth, self.db, ["identify_status", "breeding_stage"])
for i, row in enumerate(mapped_rows, start=1):
try:
combination_id_val = combination_id_map.get(str(row.get("combination_id")).strip()) if not _is_blank(row.get("combination_id")) else None
tree_id_val = tree_id_map.get(str(row.get("tree_id")).strip()) if not _is_blank(row.get("tree_id")) else None
clone_id_val = clone_id_map.get(str(row.get("clone_id")).strip()) if not _is_blank(row.get("clone_id")) else None
rule_id_val = rule_id_map.get(str(row.get("rule_id")).strip()) if not _is_blank(row.get("rule_id")) else None
approved_by_val = approved_by_map.get(str(row.get("approved_by")).strip()) if not _is_blank(row.get("approved_by")) else None
trial_study_id_val = trial_study_id_map.get(str(row.get("trial_study_id")).strip()) if not _is_blank(row.get("trial_study_id")) else None
fields = {
"combination_id": combination_id_val,
"tree_id": tree_id_val,
"selection_year": _to_int(row.get("selection_year")),
"is_selected": await resolver.resolve("identify_status", row.get("is_selected")),
"clone_id": clone_id_val,
"rule_id": rule_id_val,
"from_stage": await resolver.resolve("breeding_stage", row.get("from_stage")),
"to_stage": await resolver.resolve("breeding_stage", row.get("to_stage")),
"approved_by": approved_by_val,
"trial_study_id": trial_study_id_val,
"reason": _none_if_blank(row.get("reason")),
"remark": _none_if_blank(row.get("remark")),
}
create_data = SelectionResultCreateSchema(**fields)
await self.create(create_data)
success_count += 1
except Exception as e:
error_msgs.append(f"第{i}行: {e!s}")
continue
return ImportResultSchema(
valid_count=success_count,
invalid_count=len(error_msgs),
message_list=error_msgs,
)
except Exception as e:
logger.error(f"批量导入选育结果失败: {e!s}")
raise CustomException(msg=f"导入失败: {e!s}")
@staticmethod
def import_template_download() -> bytes:
header_list = [
"杂交组合",
"单株",
"入选年份",
"选育结论",
"晋级无性系",
"选育规则",
"起始阶段",
"晋级阶段",
"审核人",
"试验研究点",
"入选理由",
"备注",
]
selector_header_list = ["选育结论", "起始阶段", "晋级阶段"]
option_list = [
{"选育结论": ["未知", "入选", "初选", "重点", "保存", "淘汰"]},
{"起始阶段": ["germplasm", "parent", "seedling", "sp", "ap", "line", "regional_trial", "released"]},
{"晋级阶段": ["germplasm", "parent", "seedling", "sp", "ap", "line", "regional_trial", "released"]},
]
return ExcelUtil.get_excel_template(
header_list=header_list,
selector_header_list=selector_header_list,
option_list=option_list,
)