diff --git a/backend/calculations.py b/backend/calculations.py index ffd015c..d25b5f8 100644 --- a/backend/calculations.py +++ b/backend/calculations.py @@ -54,10 +54,11 @@ def calc_labor_cost_for_project(project_id: int, db: Session) -> float: total_labor += override.override_amount continue - # 这个人这天所有项目的提交 + # 这个人这天所有项目的提交(排除内部事务,其成本单独分摊) day_subs = db.query(Submission).filter( Submission.user_id == uid, Submission.submit_date == d, + Submission.project_phase != PhaseGroup.INTERNAL, ).all() # 计算这天各项目的秒数和条数 @@ -217,6 +218,71 @@ def calc_management_cost_for_project(project_id: int, db: Session) -> float: return 0.0 +# ──────────────────────────── 内部事务成本分摊 ──────────────────────────── + +def calc_internal_affairs_cost_for_project(project_id: int, db: Session) -> float: + """ + 计算某项目分摊的内部事务成本(培训/招聘面试等) + 规则: + - 汇总"内部事务"项目下非管理层用户的 hours_spent × 时薪 + - 管理层用户(exempt_submission=1)的成本已通过管理成本分摊,不重复计算 + - 总池按各项目产出秒数比例分摊到所有进行中项目 + """ + from models import Project, ProjectStatus, Role + + # 找到内部事务项目 + internal_proj = db.query(Project).filter(Project.name == "内部事务").first() + if not internal_proj: + return 0.0 + + # 内部事务项目不分摊给自己 + if project_id == internal_proj.id: + return 0.0 + + # 管理层用户 ID(exempt_submission=1),其成本已在管理成本中计算 + exempt_role_ids = set( + r.id for r in db.query(Role).filter(Role.exempt_submission == 1).all() + ) + + # 汇总内部事务的总成本:非管理层用户的 hours_spent × 时薪 + internal_subs = db.query(Submission).filter( + Submission.project_id == internal_proj.id, + ).all() + if not internal_subs: + return 0.0 + + total_pool = 0.0 + user_cache = {} + for s in internal_subs: + if s.user_id not in user_cache: + user_cache[s.user_id] = db.query(User).filter(User.id == s.user_id).first() + user = user_cache[s.user_id] + if not user or user.daily_cost <= 0: + continue + # 跳过管理层用户(已通过管理成本分摊) + if user.role_id in exempt_role_ids: + continue + hourly_cost = user.daily_cost / 8 + total_pool += (s.hours_spent or 0) * hourly_cost + + if total_pool <= 0: + return 0.0 + + # 按产出秒数比例分摊(排除内部事务项目本身) + all_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.total_seconds > 0, + Submission.project_id != internal_proj.id, + ).scalar() or 0 + proj_secs = db.query(sa_func.sum(Submission.total_seconds)).filter( + Submission.project_id == project_id, + Submission.total_seconds > 0, + ).scalar() or 0 + + if all_secs > 0: + return round(total_pool * proj_secs / all_secs, 2) + return 0.0 + + # ──────────────────────────── 工作日计算工具 ──────────────────────────── def _working_days_between(start_date, end_date) -> int: @@ -497,7 +563,8 @@ def calc_project_settlement(project_id: int, db: Session) -> dict: outsource = calc_outsource_cost_for_project(project_id, db) overhead = calc_overhead_cost_for_project(project_id, db) management = calc_management_cost_for_project(project_id, db) - total_cost = labor + ai_tool + outsource + overhead + management + internal_affairs = calc_internal_affairs_cost_for_project(project_id, db) + total_cost = labor + ai_tool + outsource + overhead + management + internal_affairs waste = calc_waste_for_project(project_id, db) efficiency = calc_team_efficiency(project_id, db) @@ -510,6 +577,7 @@ def calc_project_settlement(project_id: int, db: Session) -> dict: "outsource_cost": outsource, "overhead_cost": overhead, "management_cost": management, + "internal_affairs_cost": internal_affairs, "total_cost": round(total_cost, 2), **waste, "team_efficiency": efficiency, diff --git a/frontend/src/views/Projects.vue b/frontend/src/views/Projects.vue index 3a5bc5e..47a9f3f 100644 --- a/frontend/src/views/Projects.vue +++ b/frontend/src/views/Projects.vue @@ -191,7 +191,10 @@ function stageLabel(row) { async function load() { loading.value = true - try { projects.value = await projectApi.list(filter) } finally { loading.value = false } + try { + const all = await projectApi.list(filter) + projects.value = all.filter(p => p.name !== '内部事务') + } finally { loading.value = false } } async function handleCreate() { diff --git a/frontend/src/views/Submissions.vue b/frontend/src/views/Submissions.vue index 2a74440..8569e31 100644 --- a/frontend/src/views/Submissions.vue +++ b/frontend/src/views/Submissions.vue @@ -36,7 +36,7 @@ - +