feat: 内部事务成本按比例分摊到所有项目 + UI优化
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 1m19s
Build and Deploy Web / build-and-deploy (push) Successful in 1m9s

- 内部事务成本池:非管理层用户的 时薪×投入时长,按产出秒数比例分摊
- 管理层用户提交内部事务不重复计算(已通过管理成本分摊)
- 人力成本排除内部事务提交,避免重复
- 项目管理页面隐藏"内部事务"项目
- 阶段列宽度调整适配"内部事务"四字显示

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-03-10 11:11:15 +08:00
parent becfd74efd
commit a5d3739eef
3 changed files with 75 additions and 4 deletions

View File

@ -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
# 管理层用户 IDexempt_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,

View File

@ -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() {

View File

@ -36,7 +36,7 @@
<el-table-column prop="submit_date" label="日期" width="110" sortable />
<el-table-column prop="user_name" label="提交人" width="80" />
<el-table-column prop="project_name" label="项目" width="240" show-overflow-tooltip />
<el-table-column prop="project_phase" label="阶段" width="70" />
<el-table-column prop="project_phase" label="阶段" width="85" />
<el-table-column label="工作类型" width="80">
<template #default="{row}">
<el-tag :type="row.work_type === '测试' ? 'warning' : row.work_type === '方案' ? 'info' : row.work_type === '修改' ? 'danger' : row.work_type === 'QC' ? 'success' : ''" size="small">{{ row.work_type }}</el-tag>