feat: 内部事务成本按比例分摊到所有项目 + UI优化
- 内部事务成本池:非管理层用户的 时薪×投入时长,按产出秒数比例分摊 - 管理层用户提交内部事务不重复计算(已通过管理成本分摊) - 人力成本排除内部事务提交,避免重复 - 项目管理页面隐藏"内部事务"项目 - 阶段列宽度调整适配"内部事务"四字显示 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
becfd74efd
commit
a5d3739eef
@ -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,
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user