Adjust lifecycle allocation UI and account-level shared cost breakdown

This commit is contained in:
2026-05-04 13:55:41 +09:00
parent 21ad66c8b4
commit 9891ea0a32
2 changed files with 380 additions and 40 deletions

View File

@@ -1109,7 +1109,18 @@ def calculate_monthly_shared_distribution(
base_range = project_ranges.get(base_project_code)
if not base_range:
return {"labor_shared": 0.0, "common_shared": 0.0}
return {
"labor_shared": 0.0,
"common_shared": 0.0,
"labor_source_total": 0.0,
"common_source_total": 0.0,
"labor_project_basis_total": 0.0,
"common_project_basis_total": 0.0,
"labor_overall_basis_total": 0.0,
"common_overall_basis_total": 0.0,
"labor_allocation_details": [],
"common_allocation_details": [],
}
pool_rows = conn.execute(
"""
@@ -1127,6 +1138,8 @@ def calculate_monthly_shared_distribution(
labor_pool_by_month: dict[str, float] = defaultdict(float)
common_pool_by_month: dict[str, float] = defaultdict(float)
labor_pool_accounts_by_month: dict[str, dict[str, float]] = defaultdict(lambda: defaultdict(float))
common_pool_accounts_by_month: dict[str, dict[str, float]] = defaultdict(lambda: defaultdict(float))
for row in pool_rows:
ym = (row["ym"] or "").strip()
code = (row["account_code"] or "").strip()
@@ -1138,12 +1151,25 @@ def calculate_monthly_shared_distribution(
continue
if meta.get("category") == "인건비":
labor_pool_by_month[ym] += amount
labor_pool_accounts_by_month[ym][code] += amount
else:
common_pool_by_month[ym] += amount
common_pool_accounts_by_month[ym][code] += amount
candidate_months = sorted(set([*labor_pool_by_month.keys(), *common_pool_by_month.keys()]))
if not candidate_months:
return {"labor_shared": 0.0, "common_shared": 0.0}
return {
"labor_shared": 0.0,
"common_shared": 0.0,
"labor_source_total": 0.0,
"common_source_total": 0.0,
"labor_project_basis_total": 0.0,
"common_project_basis_total": 0.0,
"labor_overall_basis_total": 0.0,
"common_overall_basis_total": 0.0,
"labor_allocation_details": [],
"common_allocation_details": [],
}
month_active_projects: dict[str, set[str]] = defaultdict(set)
for project_code, (start_ym, end_ym) in project_ranges.items():
@@ -1176,6 +1202,16 @@ def calculate_monthly_shared_distribution(
base_start_ym, base_end_ym = base_range
labor_shared = 0.0
common_shared = 0.0
labor_source_total = 0.0
common_source_total = 0.0
labor_project_basis_total = 0.0
common_project_basis_total = 0.0
labor_overall_basis_total = 0.0
common_overall_basis_total = 0.0
labor_allocation_details: list[dict] = []
common_allocation_details: list[dict] = []
labor_account_allocated: dict[str, float] = defaultdict(float)
common_account_allocated: dict[str, float] = defaultdict(float)
for ym in candidate_months:
if ym < base_start_ym:
continue
@@ -1196,13 +1232,70 @@ def calculate_monthly_shared_distribution(
if total_value > 0:
ratio = max(0.0, min(1.0, base_value / total_value))
display_project_basis_amount = base_value
display_total_basis_amount = total_value
used_equal_split = False
else:
ratio = 1.0 / len(active_projects)
display_project_basis_amount = 1.0
display_total_basis_amount = float(len(active_projects))
used_equal_split = True
labor_shared += float(labor_pool_by_month.get(ym) or 0) * ratio
common_shared += float(common_pool_by_month.get(ym) or 0) * ratio
labor_pool = float(labor_pool_by_month.get(ym) or 0)
common_pool = float(common_pool_by_month.get(ym) or 0)
labor_shared += labor_pool * ratio
common_shared += common_pool * ratio
labor_source_total += labor_pool
common_source_total += common_pool
labor_project_basis_total += base_value
common_project_basis_total += base_value
labor_overall_basis_total += total_value
common_overall_basis_total += total_value
if labor_pool:
labor_allocation_details.append(
{
"year_month": ym,
"source_amount": labor_pool,
"project_basis_amount": base_value,
"total_basis_amount": total_value,
"display_project_basis_amount": display_project_basis_amount,
"display_total_basis_amount": display_total_basis_amount,
"used_equal_split": used_equal_split,
"allocated_amount": labor_pool * ratio,
}
)
for account_code, account_amount in (labor_pool_accounts_by_month.get(ym) or {}).items():
labor_account_allocated[account_code] += float(account_amount or 0.0) * ratio
if common_pool:
common_allocation_details.append(
{
"year_month": ym,
"source_amount": common_pool,
"project_basis_amount": base_value,
"total_basis_amount": total_value,
"display_project_basis_amount": display_project_basis_amount,
"display_total_basis_amount": display_total_basis_amount,
"used_equal_split": used_equal_split,
"allocated_amount": common_pool * ratio,
}
)
for account_code, account_amount in (common_pool_accounts_by_month.get(ym) or {}).items():
common_account_allocated[account_code] += float(account_amount or 0.0) * ratio
return {"labor_shared": labor_shared, "common_shared": common_shared}
return {
"labor_shared": labor_shared,
"common_shared": common_shared,
"labor_source_total": labor_source_total,
"common_source_total": common_source_total,
"labor_project_basis_total": labor_project_basis_total,
"common_project_basis_total": common_project_basis_total,
"labor_overall_basis_total": labor_overall_basis_total,
"common_overall_basis_total": common_overall_basis_total,
"labor_allocation_details": labor_allocation_details,
"common_allocation_details": common_allocation_details,
"labor_account_allocated": dict(labor_account_allocated),
"common_account_allocated": dict(common_account_allocated),
}
def build_company_allocated_project_rows(
@@ -1462,18 +1555,59 @@ def build_project_lifecycle_cost(
)
project_entry["shared_expense_supply"] += labor_shared
project_entry["expense_supply"] += labor_shared
account_entry = breakdown_account_maps["인건비"].setdefault(
"SHARED_LABOR",
{
"account_code": "SHARED_LABOR",
"account_name": "월별 공통배분(인건비)",
"direct_expense_supply": 0.0,
"shared_expense_supply": 0.0,
"expense_supply": 0.0,
},
)
account_entry["shared_expense_supply"] += labor_shared
account_entry["expense_supply"] += labor_shared
labor_account_allocated = monthly_shared.get("labor_account_allocated") or {}
if labor_account_allocated:
for shared_code, shared_amount in labor_account_allocated.items():
shared_code_str = (shared_code or "").strip()
if not shared_code_str:
continue
shared_amount_value = float(shared_amount or 0.0)
shared_meta = ACCOUNT_MASTER.get(shared_code_str) or {}
account_entry = breakdown_account_maps["인건비"].setdefault(
shared_code_str,
{
"account_code": shared_code_str,
"account_name": shared_meta.get("name") or shared_code_str,
"direct_expense_supply": 0.0,
"shared_expense_supply": 0.0,
"expense_supply": 0.0,
"allocation_source_amount": 0.0,
"allocation_project_basis_amount": 0.0,
"allocation_total_basis_amount": 0.0,
"allocation_mode": common_allocation_mode,
"allocation_details": [],
},
)
account_entry["shared_expense_supply"] += shared_amount_value
account_entry["expense_supply"] += shared_amount_value
account_entry["allocation_source_amount"] = float(monthly_shared.get("labor_source_total") or 0.0)
account_entry["allocation_project_basis_amount"] = float(monthly_shared.get("labor_project_basis_total") or 0.0)
account_entry["allocation_total_basis_amount"] = float(monthly_shared.get("labor_overall_basis_total") or 0.0)
account_entry["allocation_mode"] = common_allocation_mode
account_entry["allocation_details"] = list(monthly_shared.get("labor_allocation_details") or [])
else:
account_entry = breakdown_account_maps["인건비"].setdefault(
"SHARED_LABOR",
{
"account_code": "SHARED_LABOR",
"account_name": "월별 공통배분(인건비)",
"direct_expense_supply": 0.0,
"shared_expense_supply": 0.0,
"expense_supply": 0.0,
"allocation_source_amount": 0.0,
"allocation_project_basis_amount": 0.0,
"allocation_total_basis_amount": 0.0,
"allocation_mode": common_allocation_mode,
"allocation_details": [],
},
)
account_entry["shared_expense_supply"] += labor_shared
account_entry["expense_supply"] += labor_shared
account_entry["allocation_source_amount"] = float(monthly_shared.get("labor_source_total") or 0.0)
account_entry["allocation_project_basis_amount"] = float(monthly_shared.get("labor_project_basis_total") or 0.0)
account_entry["allocation_total_basis_amount"] = float(monthly_shared.get("labor_overall_basis_total") or 0.0)
account_entry["allocation_mode"] = common_allocation_mode
account_entry["allocation_details"] = list(monthly_shared.get("labor_allocation_details") or [])
if common_shared:
breakdown_components["관리비"]["shared"] += common_shared
@@ -1496,18 +1630,59 @@ def build_project_lifecycle_cost(
)
project_entry["shared_expense_supply"] += common_shared
project_entry["expense_supply"] += common_shared
account_entry = breakdown_account_maps["관리비"].setdefault(
"SHARED_COMMON",
{
"account_code": "SHARED_COMMON",
"account_name": "월별 공통배분(관리비)",
"direct_expense_supply": 0.0,
"shared_expense_supply": 0.0,
"expense_supply": 0.0,
},
)
account_entry["shared_expense_supply"] += common_shared
account_entry["expense_supply"] += common_shared
common_account_allocated = monthly_shared.get("common_account_allocated") or {}
if common_account_allocated:
for shared_code, shared_amount in common_account_allocated.items():
shared_code_str = (shared_code or "").strip()
if not shared_code_str:
continue
shared_amount_value = float(shared_amount or 0.0)
shared_meta = ACCOUNT_MASTER.get(shared_code_str) or {}
account_entry = breakdown_account_maps["관리비"].setdefault(
shared_code_str,
{
"account_code": shared_code_str,
"account_name": shared_meta.get("name") or shared_code_str,
"direct_expense_supply": 0.0,
"shared_expense_supply": 0.0,
"expense_supply": 0.0,
"allocation_source_amount": 0.0,
"allocation_project_basis_amount": 0.0,
"allocation_total_basis_amount": 0.0,
"allocation_mode": common_allocation_mode,
"allocation_details": [],
},
)
account_entry["shared_expense_supply"] += shared_amount_value
account_entry["expense_supply"] += shared_amount_value
account_entry["allocation_source_amount"] = float(monthly_shared.get("common_source_total") or 0.0)
account_entry["allocation_project_basis_amount"] = float(monthly_shared.get("common_project_basis_total") or 0.0)
account_entry["allocation_total_basis_amount"] = float(monthly_shared.get("common_overall_basis_total") or 0.0)
account_entry["allocation_mode"] = common_allocation_mode
account_entry["allocation_details"] = list(monthly_shared.get("common_allocation_details") or [])
else:
account_entry = breakdown_account_maps["관리비"].setdefault(
"SHARED_COMMON",
{
"account_code": "SHARED_COMMON",
"account_name": "월별 공통배분(관리비)",
"direct_expense_supply": 0.0,
"shared_expense_supply": 0.0,
"expense_supply": 0.0,
"allocation_source_amount": 0.0,
"allocation_project_basis_amount": 0.0,
"allocation_total_basis_amount": 0.0,
"allocation_mode": common_allocation_mode,
"allocation_details": [],
},
)
account_entry["shared_expense_supply"] += common_shared
account_entry["expense_supply"] += common_shared
account_entry["allocation_source_amount"] = float(monthly_shared.get("common_source_total") or 0.0)
account_entry["allocation_project_basis_amount"] = float(monthly_shared.get("common_project_basis_total") or 0.0)
account_entry["allocation_total_basis_amount"] = float(monthly_shared.get("common_overall_basis_total") or 0.0)
account_entry["allocation_mode"] = common_allocation_mode
account_entry["allocation_details"] = list(monthly_shared.get("common_allocation_details") or [])
total_expense = (
breakdown_components["시공비"]["total"]
@@ -1693,8 +1868,32 @@ def build_lifecycle_account_detail(
"max_date": max((row.get("transaction_date") or "" for row in filtered_transactions), default=""),
}
allocation_mode = ""
allocation_source_amount = 0.0
allocation_project_basis_amount = 0.0
allocation_total_basis_amount = 0.0
allocation_details: list[dict] = []
if account_code in {"SHARED_LABOR", "SHARED_COMMON"}:
allocation_mode = fetch_lifecycle_common_allocation_mode(conn, base_project_code)
monthly_shared = calculate_monthly_shared_distribution(conn, base_project_code, allocation_mode)
if account_code == "SHARED_LABOR":
allocation_source_amount = float(monthly_shared.get("labor_source_total") or 0.0)
allocation_project_basis_amount = float(monthly_shared.get("labor_project_basis_total") or 0.0)
allocation_total_basis_amount = float(monthly_shared.get("labor_overall_basis_total") or 0.0)
allocation_details = list(monthly_shared.get("labor_allocation_details") or [])
else:
allocation_source_amount = float(monthly_shared.get("common_source_total") or 0.0)
allocation_project_basis_amount = float(monthly_shared.get("common_project_basis_total") or 0.0)
allocation_total_basis_amount = float(monthly_shared.get("common_overall_basis_total") or 0.0)
allocation_details = list(monthly_shared.get("common_allocation_details") or [])
return {
"summary": summary,
"allocation_mode": allocation_mode,
"allocation_source_amount": allocation_source_amount,
"allocation_project_basis_amount": allocation_project_basis_amount,
"allocation_total_basis_amount": allocation_total_basis_amount,
"allocation_details": allocation_details,
"projects": sorted(
project_map.values(),
key=lambda item: (-float(item.get("expense_supply_sum") or 0), item.get("project_code") or ""),