feat(manage): refine lifecycle flow UI and direct/shared cost breakdown

This commit is contained in:
2026-04-23 14:31:41 +09:00
parent 90042a003a
commit 868661426f
3 changed files with 242 additions and 105 deletions

View File

@@ -879,6 +879,13 @@ def build_related_projects(conn: sqlite3.Connection, project_code: str, project_
{excluded_clause}
group by tx.project_code
),
pile_progress_summary as (
select
project_code,
sum(coalesce(pile_count, 0)) as entry_pile_total
from project_pile_progress_entries
group by project_code
),
master_rows as (
select
pm.project_code as project_code,
@@ -906,10 +913,17 @@ def build_related_projects(conn: sqlite3.Connection, project_code: str, project_
coalesce(ps.expense_supply, 0) as expense_supply,
coalesce(ps.txn_count, 0) as txn_count,
coalesce(ps.min_date, '') as min_date,
coalesce(ps.max_date, '') as max_date
coalesce(ps.max_date, '') as max_date,
case
when coalesce(pp.contract_pile_count, 0) > 0 then
(coalesce(psum.entry_pile_total, pp.constructed_pile_count, 0) / pp.contract_pile_count) * 100
else coalesce(pp.progress_rate, 0)
end as progress_rate
from code_set cs
left join project_summary ps on ps.project_code = cs.project_code
left join master_rows mr on mr.project_code = cs.project_code
left join project_progress pp on pp.project_code = cs.project_code
left join pile_progress_summary psum on psum.project_code = cs.project_code
order by cs.project_code
""",
[project_code, *excluded_values],
@@ -949,6 +963,7 @@ def build_related_projects(conn: sqlite3.Connection, project_code: str, project_
"txn_count": int(row_dict.get("txn_count") or 0),
"min_date": row_dict.get("min_date") or "",
"max_date": row_dict.get("max_date") or "",
"progress_rate": float(row_dict.get("progress_rate") or 0),
"is_current": code == project_code,
}
items.append(item)
@@ -1135,7 +1150,11 @@ def build_project_lifecycle_cost(
total_income = sum(float(item.get("income_supply") or 0) for item in rows_with_allocation)
total_expense = sum(float(item.get("adjusted_expense_supply") or 0) for item in rows_with_allocation)
project_codes = [item.get("project_code") for item in rows_with_allocation if item.get("project_code")]
breakdown_totals = {"시공비": 0.0, "인건비": 0.0, "관리비": 0.0}
breakdown_components = {
"시공비": {"direct": 0.0, "shared": 0.0, "total": 0.0},
"인건비": {"direct": 0.0, "shared": 0.0, "total": 0.0},
"관리비": {"direct": 0.0, "shared": 0.0, "total": 0.0},
}
breakdown_project_maps: dict[str, dict[str, dict]] = {
"시공비": {},
"인건비": {},
@@ -1185,7 +1204,11 @@ def build_project_lifecycle_cost(
allocation_ratio = float(project_info.get("allocation_ratio") or 1.0)
expense_supply = float(row["expense_supply"] or 0) * allocation_ratio
breakdown_totals[bucket] += expense_supply
# Until common-cost allocation is introduced, lifecycle-linked costs
# are treated as direct costs for the target project.
source_component = "direct"
breakdown_components[bucket][source_component] += expense_supply
breakdown_components[bucket]["total"] += expense_supply
project_entry = breakdown_project_maps[bucket].setdefault(
project_code,
@@ -1198,9 +1221,12 @@ def build_project_lifecycle_cost(
"allocation_numerator": numerator,
"allocation_denominator": denominator,
"allocation_ratio": allocation_ratio,
"direct_expense_supply": 0.0,
"shared_expense_supply": 0.0,
"expense_supply": 0.0,
},
)
project_entry[f"{source_component}_expense_supply"] += expense_supply
project_entry["expense_supply"] += expense_supply
account_entry = breakdown_account_maps[bucket].setdefault(
@@ -1208,15 +1234,20 @@ def build_project_lifecycle_cost(
{
"account_code": account_code or "",
"account_name": (meta or {}).get("name") or (row["account_code"] or ""),
"direct_expense_supply": 0.0,
"shared_expense_supply": 0.0,
"expense_supply": 0.0,
},
)
account_entry[f"{source_component}_expense_supply"] += expense_supply
account_entry["expense_supply"] += expense_supply
breakdown = [
{
"label": "시공비",
"expense_supply": breakdown_totals["시공비"],
"expense_supply": breakdown_components["시공비"]["total"],
"direct_expense_supply": breakdown_components["시공비"]["direct"],
"shared_expense_supply": breakdown_components["시공비"]["shared"],
"projects": sorted(
breakdown_project_maps["시공비"].values(),
key=lambda item: (-float(item.get("expense_supply") or 0), item.get("project_code") or ""),
@@ -1228,13 +1259,23 @@ def build_project_lifecycle_cost(
},
{
"label": "인건비",
"expense_supply": 0.0,
"projects": [],
"accounts": [],
"expense_supply": breakdown_components["인건비"]["total"],
"direct_expense_supply": breakdown_components["인건비"]["direct"],
"shared_expense_supply": breakdown_components["인건비"]["shared"],
"projects": sorted(
breakdown_project_maps["인건비"].values(),
key=lambda item: (-float(item.get("expense_supply") or 0), item.get("project_code") or ""),
),
"accounts": sorted(
breakdown_account_maps["인건비"].values(),
key=lambda item: (-float(item.get("expense_supply") or 0), item.get("account_code") or ""),
),
},
{
"label": "관리비",
"expense_supply": breakdown_totals["관리비"],
"expense_supply": breakdown_components["관리비"]["total"],
"direct_expense_supply": breakdown_components["관리비"]["direct"],
"shared_expense_supply": breakdown_components["관리비"]["shared"],
"projects": sorted(
breakdown_project_maps["관리비"].values(),
key=lambda item: (-float(item.get("expense_supply") or 0), item.get("project_code") or ""),
@@ -1263,10 +1304,10 @@ def classify_lifecycle_bucket(account_code: str, project_code: str, project_type
meta = meta or ACCOUNT_MASTER.get(account_code)
if meta:
if meta.get("category") == "인건비":
return "시공"
if meta.get("project_type") == "시공":
return "시공"
return "관리"
return "인건"
if meta.get("project_type") == "관리":
return "관리"
return "시공"
if "-시공-" in project_code or project_type == "시공":
return "시공비"
return "관리비"