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
|
total_labor += override.override_amount
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 这个人这天所有项目的提交
|
# 这个人这天所有项目的提交(排除内部事务,其成本单独分摊)
|
||||||
day_subs = db.query(Submission).filter(
|
day_subs = db.query(Submission).filter(
|
||||||
Submission.user_id == uid,
|
Submission.user_id == uid,
|
||||||
Submission.submit_date == d,
|
Submission.submit_date == d,
|
||||||
|
Submission.project_phase != PhaseGroup.INTERNAL,
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
# 计算这天各项目的秒数和条数
|
# 计算这天各项目的秒数和条数
|
||||||
@ -217,6 +218,71 @@ def calc_management_cost_for_project(project_id: int, db: Session) -> float:
|
|||||||
return 0.0
|
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:
|
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)
|
outsource = calc_outsource_cost_for_project(project_id, db)
|
||||||
overhead = calc_overhead_cost_for_project(project_id, db)
|
overhead = calc_overhead_cost_for_project(project_id, db)
|
||||||
management = calc_management_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)
|
waste = calc_waste_for_project(project_id, db)
|
||||||
efficiency = calc_team_efficiency(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,
|
"outsource_cost": outsource,
|
||||||
"overhead_cost": overhead,
|
"overhead_cost": overhead,
|
||||||
"management_cost": management,
|
"management_cost": management,
|
||||||
|
"internal_affairs_cost": internal_affairs,
|
||||||
"total_cost": round(total_cost, 2),
|
"total_cost": round(total_cost, 2),
|
||||||
**waste,
|
**waste,
|
||||||
"team_efficiency": efficiency,
|
"team_efficiency": efficiency,
|
||||||
|
|||||||
@ -191,7 +191,10 @@ function stageLabel(row) {
|
|||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
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() {
|
async function handleCreate() {
|
||||||
|
|||||||
@ -36,7 +36,7 @@
|
|||||||
<el-table-column prop="submit_date" label="日期" width="110" sortable />
|
<el-table-column prop="submit_date" label="日期" width="110" sortable />
|
||||||
<el-table-column prop="user_name" label="提交人" width="80" />
|
<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_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">
|
<el-table-column label="工作类型" width="80">
|
||||||
<template #default="{row}">
|
<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>
|
<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