Group presign counts by project and team, include totals and unassigned rows, and reuse count drill-down. Remove the standalone footer while retaining presignTotal compatibility and the four formal business types. Related to issue #13. Independent source review completed; no tests or builds run under user self-test arrangement.
417 lines
23 KiB
Python
417 lines
23 KiB
Python
"""项目资料、权限内统计和关联归属事务。"""
|
|
from datetime import date, datetime, timedelta
|
|
from fastapi import HTTPException
|
|
from sqlalchemy import or_, and_, func
|
|
from app.core.data_scope import apply_data_scope
|
|
from app.core.utils import generate_uuid
|
|
from app.crud.child_row_sync import sync_child_rows
|
|
from app.models.project_management import (Project, ProjectTeam, ProjectTeamMember,
|
|
ProjectTeamRegion, ProjectTeamHospital, ProjectAssignmentAudit)
|
|
from app.models.models import (SysUser, BizCollectionRegion, BizCollectionHospital,
|
|
BizPresignOrder, BizCollectionProtocol, BizPilotApplication)
|
|
from app.models.research_donation import BizResearchDonation, BizProtocolDonationConvert
|
|
from app.services.web_auth_roles import iter_user_menu_perm_codes
|
|
|
|
BUSINESS = {
|
|
'presign': (BizPresignOrder, 'protocol_no', 'order_status'),
|
|
'cell_storage': (BizCollectionProtocol, 'protocol_no', 'protocol_status'),
|
|
'research_donation': (BizResearchDonation, 'donation_no', 'donation_status'),
|
|
'pilot_service': (BizPilotApplication, 'application_no', 'application_status'),
|
|
'basic_experiment': (BizPilotApplication, 'application_no', 'application_status'),
|
|
}
|
|
COUNT_TYPES = tuple(k for k in BUSINESS if k != 'presign')
|
|
ALL_PROJECTS_PERMISSION = 'project:project:all'
|
|
|
|
|
|
def scoped(db, model, tenant):
|
|
return db.query(model).filter(model.tenant_no == tenant, model.del_flag == 0)
|
|
|
|
|
|
def get_row(db, model, tenant, row_id, lock=False):
|
|
query = scoped(db, model, tenant).filter(model.id == row_id)
|
|
row = (query.populate_existing().with_for_update() if lock else query).first()
|
|
if not row:
|
|
raise HTTPException(404, '记录不存在或无权访问')
|
|
return row
|
|
|
|
|
|
def has_permission(db, tenant, user, code):
|
|
return user.get('role') == 'super_admin' or code in iter_user_menu_perm_codes(db, user.get('user_id', ''), tenant_no=tenant)
|
|
|
|
|
|
def check_permission(db, tenant, user, *codes):
|
|
if not any(has_permission(db, tenant, user, code) for code in codes):
|
|
raise HTTPException(403, '无对应菜单权限')
|
|
|
|
|
|
def project_brief(db, tenant_no, project_id):
|
|
result = dict(projectId=project_id, projectName=None, teamId=None, teamName=None)
|
|
if project_id:
|
|
row = scoped(db, Project, tenant_no).filter(Project.id == project_id).first()
|
|
if row:
|
|
team = scoped(db, ProjectTeam, tenant_no).filter(ProjectTeam.id == row.team_id).first()
|
|
result.update(projectName=row.project_name, teamId=row.team_id, teamName=team.team_name if team else None)
|
|
return result
|
|
|
|
|
|
def validate_assignment(db, tenant_no, user, project_id, biz_type, *, current_project_id=None, required=False):
|
|
project_id = project_id or None
|
|
if current_project_id:
|
|
if project_id != current_project_id:
|
|
raise HTTPException(400, '请通过项目归属维护调整')
|
|
return current_project_id
|
|
if not project_id:
|
|
if required:
|
|
raise HTTPException(400, '请选择项目')
|
|
return None
|
|
row = get_row(db, Project, tenant_no, project_id, lock=True)
|
|
team = get_row(db, ProjectTeam, tenant_no, row.team_id, lock=True)
|
|
if row.status != 'active' or not team.enabled:
|
|
raise HTTPException(400, '项目或项目组已停用或尚未启用')
|
|
effective_type = 'cell_storage' if biz_type == 'presign' else biz_type
|
|
if effective_type not in row.biz_types.split(','):
|
|
raise HTTPException(400, '项目不支持该业务类型')
|
|
uid = user.get('user_id')
|
|
member = uid == team.leader_user_id or scoped(db, ProjectTeamMember, tenant_no).filter(ProjectTeamMember.team_id == team.id, ProjectTeamMember.user_id == uid).with_for_update().first()
|
|
if not member and not has_permission(db, tenant_no, user, ALL_PROJECTS_PERMISSION):
|
|
raise HTTPException(403, '仅可选择所在项目组的项目,跨组选择需全部项目权限')
|
|
return project_id
|
|
|
|
|
|
def business_query(db, tenant, user, kind):
|
|
if kind not in BUSINESS:
|
|
raise HTTPException(400, '业务类型不支持')
|
|
model = BUSINESS[kind][0]
|
|
query = scoped(db, model, tenant)
|
|
if model is BizPilotApplication:
|
|
query = query.filter(model.biz_type == kind)
|
|
return apply_data_scope(query, model, user, db, tenant)
|
|
|
|
|
|
def filtered_query(db, tenant, user, kind, filters):
|
|
model, number, status = BUSINESS[kind]
|
|
q = business_query(db, tenant, user, kind)
|
|
if filters.get('projectId'):
|
|
q = q.filter(model.project_id == filters['projectId'])
|
|
if filters.get('teamId'):
|
|
q = q.filter(model.project_id.in_(scoped(db, Project, tenant).filter(Project.team_id == filters['teamId']).with_entities(Project.id)))
|
|
if filters.get('unassigned'):
|
|
q = q.filter(model.project_id.is_(None))
|
|
if filters.get('keyword'):
|
|
q = q.filter(getattr(model, number).contains(filters['keyword']))
|
|
for key, op in [('startDate', 'start'), ('endDate', 'end')]:
|
|
if filters.get(key):
|
|
try:
|
|
day = date.fromisoformat(str(filters[key]))
|
|
except ValueError:
|
|
raise HTTPException(400, '日期格式应为 YYYY-MM-DD')
|
|
q = q.filter(model.create_time >= datetime.combine(day, datetime.min.time())) if op == 'start' else q.filter(model.create_time < datetime.combine(day + timedelta(days=1), datetime.min.time()))
|
|
return q
|
|
|
|
|
|
def record_dict(db, tenant, kind, row):
|
|
return dict(id=row.id, bizType=kind, bizNo=getattr(row, BUSINESS[kind][1]), status=getattr(row, BUSINESS[kind][2]), createTime=row.create_time, **project_brief(db, tenant, row.project_id))
|
|
|
|
|
|
def records(db, tenant, user, biz_type, page=1, pageSize=20, **filters):
|
|
if biz_type not in BUSINESS:
|
|
raise HTTPException(400, '业务类型不支持')
|
|
model, _, status = BUSINESS[biz_type]
|
|
q = filtered_query(db, tenant, user, biz_type, filters)
|
|
status_column = getattr(model, status)
|
|
status_counts = {str(value or ''): count for value, count in
|
|
q.with_entities(status_column, func.count(model.id)).group_by(status_column).all()}
|
|
if filters.get('status'):
|
|
q = q.filter(getattr(model, status) == filters['status'])
|
|
total = q.count()
|
|
rows = q.order_by(model.create_time.desc(), model.id).offset((page-1)*pageSize).limit(pageSize).all()
|
|
return dict(list=[record_dict(db, tenant, biz_type, r) for r in rows], total=total, statusCounts=status_counts)
|
|
|
|
|
|
def statistics(db, tenant, user, groupBy='project', **filters):
|
|
if groupBy not in ('project', 'team'):
|
|
raise HTTPException(400, '分组方式不支持')
|
|
statistics_types = ('presign', *COUNT_TYPES)
|
|
totals = dict.fromkeys(statistics_types, 0)
|
|
unassigned = dict.fromkeys(statistics_types, 0)
|
|
counts = {}
|
|
for kind in statistics_types:
|
|
model = BUSINESS[kind][0]
|
|
counts[kind] = filtered_query(db, tenant, user, kind, filters).with_entities(
|
|
model.project_id, func.count(model.id)).group_by(model.project_id).all()
|
|
project_ids = {pid for values in counts.values() for pid, _ in values if pid}
|
|
metadata = {}
|
|
if project_ids:
|
|
rows = scoped(db, Project, tenant).outerjoin(ProjectTeam, and_(
|
|
ProjectTeam.id == Project.team_id, ProjectTeam.tenant_no == tenant,
|
|
ProjectTeam.del_flag == 0)).filter(Project.id.in_(project_ids)).with_entities(
|
|
Project.id, Project.project_name, Project.team_id, ProjectTeam.team_name).all()
|
|
metadata = {r.id: r for r in rows}
|
|
items = {}
|
|
for kind, values in counts.items():
|
|
for project_id, count in values:
|
|
totals[kind] += count
|
|
if not project_id:
|
|
unassigned[kind] += count
|
|
continue
|
|
info = metadata.get(project_id)
|
|
team_id = info.team_id if info else None
|
|
team_name = info.team_name if info else None
|
|
key = team_id if groupBy == 'team' else project_id
|
|
item = items.setdefault(key, dict(id=key,
|
|
name=team_name if groupBy == 'team' else (info.project_name if info else None),
|
|
teamName=team_name, **dict.fromkeys(statistics_types, 0)))
|
|
item[kind] += count
|
|
return dict(items=list(items.values()), totals=totals, unassignedTotals=unassigned,
|
|
presignTotal=totals['presign'])
|
|
|
|
|
|
def linked_records(db, tenant, user, kind, row_id, lock=False, current_read=False):
|
|
"""构建双向来源链;无权关联记录导致整次操作拒绝。"""
|
|
def graph_query(model):
|
|
query = scoped(db, model, tenant).populate_existing()
|
|
return query.with_for_update() if current_read else query
|
|
|
|
seen, pending, rows = set(), [(kind, row_id)], []
|
|
while pending:
|
|
current, rid = pending.pop()
|
|
if (current, rid) in seen:
|
|
continue
|
|
seen.add((current, rid))
|
|
model = BUSINESS[current][0]
|
|
row = get_row(db, model, tenant, rid, lock=current_read)
|
|
if not business_query(db, tenant, user, current).filter(model.id == rid).first():
|
|
raise HTTPException(403, '关联记录不存在或无权访问')
|
|
rows.append((current, row))
|
|
if current == 'presign':
|
|
pending += [('cell_storage', r.id) for r in graph_query(BizCollectionProtocol).filter(BizCollectionProtocol.source_presign_order_id == rid).all()]
|
|
if row.converted_protocol_id:
|
|
pending.append(('cell_storage', row.converted_protocol_id))
|
|
if current == 'cell_storage':
|
|
if row.source_presign_order_id:
|
|
pending.append(('presign', row.source_presign_order_id))
|
|
pending += [('presign', r.id) for r in graph_query(BizPresignOrder).filter(BizPresignOrder.converted_protocol_id == rid).all()]
|
|
pending += [('research_donation', r.id) for r in graph_query(BizResearchDonation).filter(BizResearchDonation.source_protocol_id == rid).all()]
|
|
if current == 'research_donation' and row.source_protocol_id:
|
|
pending.append(('cell_storage', row.source_protocol_id))
|
|
if current in ('cell_storage', 'research_donation'):
|
|
col = BizProtocolDonationConvert.protocol_id if current == 'cell_storage' else BizProtocolDonationConvert.research_donation_id
|
|
for link in graph_query(BizProtocolDonationConvert).filter(col == rid).all():
|
|
pending.extend([('cell_storage', link.protocol_id), ('research_donation', link.research_donation_id)])
|
|
rows.sort(key=lambda pair: (pair[0], pair[1].id))
|
|
if lock:
|
|
rows = [(k, get_row(db, BUSINESS[k][0], tenant, r.id, lock=True)) for k, r in rows]
|
|
return rows
|
|
|
|
|
|
def preview_assignment(db, tenant, user, kind, row_id, project_id):
|
|
check_permission(db, tenant, user, 'project:assignment:edit')
|
|
rows = linked_records(db, tenant, user, kind, row_id)
|
|
for k, row in rows:
|
|
validate_assignment(db, tenant, user, project_id, k, required=True)
|
|
return {'records': [record_dict(db, tenant, k, r) for k, r in rows]}
|
|
|
|
|
|
def assign(db, tenant, user, kind, row_id, project_id, reason, expected):
|
|
check_permission(db, tenant, user, 'project:assignment:edit')
|
|
if not reason or not reason.strip():
|
|
raise HTTPException(400, '请填写调整原因')
|
|
rows = linked_records(db, tenant, user, kind, row_id, lock=True)
|
|
actual = {(k, r.id, r.project_id) for k, r in rows}
|
|
refreshed = linked_records(db, tenant, user, kind, row_id, current_read=True)
|
|
snapshot = {(r['bizType'], r['id'], r.get('projectId')) for r in expected}
|
|
if actual != snapshot or actual != {(k, r.id, r.project_id) for k, r in refreshed} or len(expected) != len(actual):
|
|
raise HTTPException(409, '关联记录或归属已变更,请重新预览')
|
|
for k, row in rows:
|
|
validate_assignment(db, tenant, user, project_id, k, required=True)
|
|
new_name = project_brief(db, tenant, project_id)['projectName']
|
|
operator = scoped(db, SysUser, tenant).filter(SysUser.id == user.get('user_id')).first()
|
|
for k, row in rows:
|
|
db.add(ProjectAssignmentAudit(id=generate_uuid(), tenant_no=tenant, biz_type=k, biz_id=row.id, old_project_id=row.project_id, new_project_id=project_id, old_project_name=project_brief(db, tenant, row.project_id)['projectName'], new_project_name=new_name, reason=reason.strip(), operator_name=operator.nick_name if operator else user.get('user_id'), create_by=user.get('user_id'), create_dept_id=user.get('dept_id') or ''))
|
|
row.project_id = project_id
|
|
row.update_by = user.get('user_id')
|
|
row.update_time = datetime.now()
|
|
db.commit()
|
|
return {'updated': len(rows)}
|
|
|
|
|
|
def assignment_history(db, tenant, user, kind, row_id):
|
|
model = BUSINESS.get(kind, (None,))[0]
|
|
if model is None or not business_query(db, tenant, user, kind).filter(model.id == row_id).first():
|
|
raise HTTPException(404, '记录不存在或无权访问')
|
|
rows = scoped(db, ProjectAssignmentAudit, tenant).filter(ProjectAssignmentAudit.biz_type == kind, ProjectAssignmentAudit.biz_id == row_id).order_by(ProjectAssignmentAudit.create_time.desc()).all()
|
|
return {'list': [dict(id=r.id, oldProjectName=r.old_project_name, newProjectName=r.new_project_name, reason=r.reason, operatorName=r.operator_name, createTime=r.create_time) for r in rows]}
|
|
|
|
|
|
CHILDREN = (
|
|
('members', ProjectTeamMember, 'userId', 'user_id', SysUser, 'nick_name'),
|
|
('regions', ProjectTeamRegion, 'regionId', 'region_id', BizCollectionRegion, 'region_name'),
|
|
('hospitals', ProjectTeamHospital, 'hospitalId', 'hospital_id', BizCollectionHospital, 'hospital_name'),
|
|
)
|
|
|
|
|
|
def serialize_entity(db, tenant, row):
|
|
leader = scoped(db, SysUser, tenant).filter(SysUser.id == row.leader_user_id).first()
|
|
result = dict(id=row.id, leaderUserId=row.leader_user_id, leaderName=leader.nick_name if leader else '', remark=row.remark, createTime=row.create_time)
|
|
if isinstance(row, ProjectTeam):
|
|
result.update(teamName=row.team_name, enabled=bool(row.enabled))
|
|
for key, model, public, attr, ref_model, name in CHILDREN:
|
|
children = scoped(db, model, tenant).filter(model.team_id == row.id).order_by(model.order_num).all()
|
|
result[key] = []
|
|
for child in children:
|
|
ref = scoped(db, ref_model, tenant).filter(ref_model.id == getattr(child, attr)).first()
|
|
result[key].append(dict(id=child.id, **{public: getattr(child, attr)}, name=getattr(ref, name) if ref else ''))
|
|
else:
|
|
result.update(projectNo=row.project_no, projectName=row.project_name, teamId=row.team_id, teamName=project_brief(db, tenant, row.id)['teamName'], bizTypes=row.biz_types.split(','), startDate=row.start_date, endDate=row.end_date, status=row.status)
|
|
return result
|
|
|
|
|
|
def list_entities(db, tenant, model, page=1, pageSize=20, **filters):
|
|
q = scoped(db, model, tenant)
|
|
name = model.team_name if model is ProjectTeam else model.project_name
|
|
if filters.get('keyword'):
|
|
q = q.filter(name.contains(filters['keyword']))
|
|
for public, attr in [('leaderUserId', 'leader_user_id'), ('teamId', 'team_id'), ('status', 'status'), ('enabled', 'enabled')]:
|
|
if filters.get(public) not in (None, '') and hasattr(model, attr):
|
|
q = q.filter(getattr(model, attr) == filters[public])
|
|
return dict(total=q.count(), list=[serialize_entity(db, tenant, r) for r in q.order_by(model.create_time.desc(), model.id).offset((page-1)*pageSize).limit(pageSize).all()])
|
|
|
|
|
|
def references(db, tenant):
|
|
return dict(users=[dict(id=r.id, name=r.nick_name) for r in scoped(db, SysUser, tenant).filter(SysUser.status == '1').all()], regions=[dict(id=r.id, name=r.region_name) for r in scoped(db, BizCollectionRegion, tenant).all()], hospitals=[dict(id=r.id, name=r.hospital_name, regionId=r.region_id) for r in scoped(db, BizCollectionHospital, tenant).all()])
|
|
|
|
|
|
def has_business(db, tenant, project_id):
|
|
# Deleted business is still a historical reference and prevents structural changes.
|
|
return any(db.query(model.id).filter(model.tenant_no == tenant, model.project_id == project_id).with_for_update().first() for model in {v[0] for v in BUSINESS.values()})
|
|
|
|
|
|
def save_entity(db, tenant, user, model, body, row_id=None):
|
|
row = get_row(db, model, tenant, row_id, lock=True) if row_id else model(id=generate_uuid(), tenant_no=tenant, create_by=user.get('user_id'), create_dept_id=user.get('dept_id') or '')
|
|
name_key = 'teamName' if model is ProjectTeam else 'projectName'
|
|
if not str(body.get(name_key) or '').strip():
|
|
raise HTTPException(400, '名称不能为空')
|
|
leader = get_row(db, SysUser, tenant, body.get('leaderUserId'))
|
|
if leader.status != '1':
|
|
raise HTTPException(400, '负责人已停用')
|
|
row.leader_user_id = leader.id
|
|
row.remark = body.get('remark')
|
|
row.update_by = user.get('user_id')
|
|
row.update_time = datetime.now()
|
|
if model is ProjectTeam:
|
|
row.team_name = body[name_key].strip()
|
|
row.enabled = int(bool(body.get('enabled', True)))
|
|
db.add(row)
|
|
for key, child_model, public, attr, ref_model, _ in CHILDREN:
|
|
children = body.get(key, [])
|
|
seen = set()
|
|
for child in children:
|
|
ref_id = child.get(public)
|
|
if ref_id in seen:
|
|
raise HTTPException(400, '关联资料不能重复')
|
|
seen.add(ref_id)
|
|
get_row(db, ref_model, tenant, ref_id)
|
|
if child.get('id'):
|
|
existing = db.query(child_model).filter(child_model.id == child['id']).first()
|
|
if existing and (existing.tenant_no != tenant or existing.team_id != row.id or existing.del_flag):
|
|
raise HTTPException(400, '子行不属于当前项目组')
|
|
try:
|
|
sync_child_rows(db, child_model, parent_attr='team_id', parent_id=row.id, tenant_no=tenant, user_id=user.get('user_id'), items=children,
|
|
build_row=lambda rid, cm=child_model: cm(id=rid, tenant_no=tenant, team_id=row.id, create_by=user.get('user_id'), create_dept_id=user.get('dept_id') or ''),
|
|
apply_fields=lambda child, item, index, now, a=attr, p=public: (setattr(child, a, item[p]), setattr(child, 'order_num', index)),
|
|
natural_key=lambda item, p=public: item[p], row_natural_key=lambda child, a=attr: getattr(child, a))
|
|
except ValueError as exc:
|
|
raise HTTPException(400, str(exc))
|
|
else:
|
|
team = get_row(db, ProjectTeam, tenant, body.get('teamId'), lock=True)
|
|
if row_id and row.team_id != team.id and has_business(db, tenant, row.id):
|
|
raise HTTPException(400, '项目已有业务,不可更换项目组')
|
|
kinds = body.get('bizTypes') or []
|
|
if not kinds or any(k not in COUNT_TYPES for k in kinds):
|
|
raise HTTPException(400, '请选择有效业务类型')
|
|
status = body.get('status', 'pending')
|
|
if status not in ('pending', 'active', 'completed', 'disabled'):
|
|
raise HTTPException(400, '项目状态无效')
|
|
row.project_name, row.team_id, row.biz_types, row.status = body[name_key].strip(), team.id, ','.join(dict.fromkeys(kinds)), status
|
|
if not row_id:
|
|
row.project_no = 'XM' + datetime.now().strftime('%Y%m%d') + generate_uuid().replace('-', '')[:16].upper()
|
|
try:
|
|
row.start_date = date.fromisoformat(body['startDate']) if body.get('startDate') else None
|
|
row.end_date = date.fromisoformat(body['endDate']) if body.get('endDate') else None
|
|
except ValueError:
|
|
raise HTTPException(400, '日期格式应为 YYYY-MM-DD')
|
|
if row.start_date and row.end_date and row.start_date > row.end_date:
|
|
raise HTTPException(400, '结束日期不能早于开始日期')
|
|
db.add(row)
|
|
db.commit()
|
|
return serialize_entity(db, tenant, row)
|
|
|
|
|
|
def delete_entity(db, tenant, user, model, row_id):
|
|
row = get_row(db, model, tenant, row_id, lock=True)
|
|
if model is Project and has_business(db, tenant, row.id):
|
|
raise HTTPException(400, '项目已被业务引用,请停用')
|
|
if model is ProjectTeam and db.query(Project.id).filter(Project.tenant_no == tenant, Project.team_id == row.id).with_for_update().first():
|
|
raise HTTPException(400, '项目组已被项目引用,请停用')
|
|
row.del_flag, row.update_by = 1, user.get('user_id')
|
|
if model is ProjectTeam:
|
|
for _, child_model, *_ in CHILDREN:
|
|
for child in scoped(db, child_model, tenant).filter(child_model.team_id == row.id).all():
|
|
child.del_flag, child.update_by = 1, user.get('user_id')
|
|
db.commit()
|
|
return {'id': row.id}
|
|
|
|
|
|
def apply_project_choice_scope(query, db, tenant, user):
|
|
"""Apply team membership or the explicit tenant-wide project-choice permission.
|
|
|
|
The caller joins the tenant-scoped, non-deleted ProjectTeam table. Business
|
|
data scope and project availability remain separate caller constraints.
|
|
"""
|
|
if has_permission(db, tenant, user, ALL_PROJECTS_PERMISSION):
|
|
return query
|
|
memberships = scoped(db, ProjectTeamMember, tenant).filter(
|
|
ProjectTeamMember.user_id == user.get('user_id')).with_entities(ProjectTeamMember.team_id)
|
|
return query.filter(or_(ProjectTeam.leader_user_id == user.get('user_id'),
|
|
Project.team_id.in_(memberships)))
|
|
|
|
|
|
def options(db, tenant, user, biz_type, keyword=None):
|
|
if biz_type not in BUSINESS:
|
|
raise HTTPException(400, '业务类型不支持')
|
|
effective_type = 'cell_storage' if biz_type == 'presign' else biz_type
|
|
q = scoped(db, Project, tenant).join(ProjectTeam, and_(
|
|
ProjectTeam.id == Project.team_id, ProjectTeam.tenant_no == tenant,
|
|
ProjectTeam.del_flag == 0, ProjectTeam.enabled == 1)).filter(Project.status == 'active')
|
|
q = apply_project_choice_scope(q, db, tenant, user)
|
|
if keyword:
|
|
q = q.filter(or_(Project.project_name.contains(keyword), Project.project_no.contains(keyword)))
|
|
rows = q.with_entities(Project.id, Project.project_name, Project.project_no,
|
|
Project.team_id, Project.biz_types, ProjectTeam.team_name).order_by(
|
|
Project.create_time.desc(), Project.id).all()
|
|
result = [dict(id=r.id, projectName=r.project_name, projectNo=r.project_no,
|
|
teamId=r.team_id, teamName=r.team_name, disabled=False)
|
|
for r in rows if effective_type in r.biz_types.split(',')]
|
|
from app.core.config import settings
|
|
return {'list': result, 'required': settings.PROJECT_ASSIGNMENT_REQUIRED}
|
|
|
|
|
|
def filter_options(db, tenant, user, biz_type, keyword=None, *, business_scope=None):
|
|
"""Intersect project-choice scope with visible history; include stopped projects."""
|
|
if biz_type not in BUSINESS:
|
|
raise HTTPException(400, '业务类型不支持')
|
|
model = BUSINESS[biz_type][0]
|
|
visible = business_scope if business_scope is not None else business_query(db, tenant, user, biz_type)
|
|
ids = visible.with_entities(model.project_id).filter(model.project_id.isnot(None)).distinct()
|
|
q = scoped(db, Project, tenant).join(ProjectTeam, and_(
|
|
ProjectTeam.id == Project.team_id, ProjectTeam.tenant_no == tenant,
|
|
ProjectTeam.del_flag == 0)).filter(Project.id.in_(ids))
|
|
q = apply_project_choice_scope(q, db, tenant, user)
|
|
if keyword:
|
|
q = q.filter(or_(Project.project_name.contains(keyword), Project.project_no.contains(keyword)))
|
|
rows = q.with_entities(Project.id, Project.project_name, Project.project_no,
|
|
Project.team_id, ProjectTeam.team_name).order_by(Project.create_time.desc(), Project.id).all()
|
|
return {'list': [dict(id=r.id, projectName=r.project_name, projectNo=r.project_no,
|
|
teamId=r.team_id, teamName=r.team_name, disabled=False) for r in rows],
|
|
'required': False}
|