feat(lifecycle): apply monthly shared allocation from project start month
This commit is contained in:
@@ -1020,6 +1020,125 @@ def resolve_lifecycle_allocation(project_type: str, allocation_item: dict | None
|
|||||||
return 1, 1, 1.0
|
return 1, 1, 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def _to_year_month(value: str | None) -> str:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if len(text) < 7 or text[4] != "-":
|
||||||
|
return ""
|
||||||
|
month = text[5:7]
|
||||||
|
if not month.isdigit():
|
||||||
|
return ""
|
||||||
|
mm = int(month)
|
||||||
|
if mm < 1 or mm > 12:
|
||||||
|
return ""
|
||||||
|
return text[:7]
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_year_months(start_ym: str, end_ym: str):
|
||||||
|
if not start_ym or not end_ym:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
sy, sm = int(start_ym[:4]), int(start_ym[5:7])
|
||||||
|
ey, em = int(end_ym[:4]), int(end_ym[5:7])
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
if (ey, em) < (sy, sm):
|
||||||
|
return
|
||||||
|
y, m = sy, sm
|
||||||
|
while (y, m) <= (ey, em):
|
||||||
|
yield f"{y:04d}-{m:02d}"
|
||||||
|
m += 1
|
||||||
|
if m > 12:
|
||||||
|
y += 1
|
||||||
|
m = 1
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_monthly_shared_distribution(conn: sqlite3.Connection, base_project_code: str) -> dict:
|
||||||
|
project_rows = conn.execute(
|
||||||
|
"""
|
||||||
|
select
|
||||||
|
pm.project_code,
|
||||||
|
pm.start_date,
|
||||||
|
pm.end_date,
|
||||||
|
min(case when coalesce(t.transaction_date, '') <> '' then t.transaction_date end) as min_tx_date,
|
||||||
|
max(case when coalesce(t.transaction_date, '') <> '' then t.transaction_date end) as max_tx_date
|
||||||
|
from project_master pm
|
||||||
|
left join ptc_transactions t on t.project_code = pm.project_code
|
||||||
|
where coalesce(pm.project_type, '') = '시공'
|
||||||
|
group by pm.project_code, pm.start_date, pm.end_date
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
project_ranges: dict[str, tuple[str, str]] = {}
|
||||||
|
for row in project_rows:
|
||||||
|
code = (row["project_code"] or "").strip()
|
||||||
|
if not code:
|
||||||
|
continue
|
||||||
|
start_ym = _to_year_month(row["start_date"]) or _to_year_month(row["min_tx_date"])
|
||||||
|
end_ym = _to_year_month(row["end_date"]) or _to_year_month(row["max_tx_date"]) or start_ym
|
||||||
|
if not start_ym:
|
||||||
|
continue
|
||||||
|
project_ranges[code] = (start_ym, end_ym)
|
||||||
|
|
||||||
|
base_range = project_ranges.get(base_project_code)
|
||||||
|
if not base_range:
|
||||||
|
return {"labor_shared": 0.0, "common_shared": 0.0}
|
||||||
|
|
||||||
|
pool_rows = conn.execute(
|
||||||
|
"""
|
||||||
|
select
|
||||||
|
substr(coalesce(transaction_date, ''), 1, 7) as ym,
|
||||||
|
account_code_final as account_code,
|
||||||
|
coalesce(sum(supply_amount), 0) as expense_supply
|
||||||
|
from ptc_transactions
|
||||||
|
where in_out = '출금'
|
||||||
|
and coalesce(transaction_date, '') <> ''
|
||||||
|
group by substr(coalesce(transaction_date, ''), 1, 7), account_code_final
|
||||||
|
having ym <> ''
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
labor_pool_by_month: dict[str, float] = defaultdict(float)
|
||||||
|
common_pool_by_month: dict[str, float] = defaultdict(float)
|
||||||
|
for row in pool_rows:
|
||||||
|
ym = (row["ym"] or "").strip()
|
||||||
|
code = (row["account_code"] or "").strip()
|
||||||
|
amount = float(row["expense_supply"] or 0)
|
||||||
|
if not ym or amount == 0:
|
||||||
|
continue
|
||||||
|
meta = ACCOUNT_MASTER.get(code) or {}
|
||||||
|
if meta.get("project_type") != "관리":
|
||||||
|
continue
|
||||||
|
if meta.get("category") == "인건비":
|
||||||
|
labor_pool_by_month[ym] += amount
|
||||||
|
else:
|
||||||
|
common_pool_by_month[ym] += 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}
|
||||||
|
|
||||||
|
month_active_counts: dict[str, int] = defaultdict(int)
|
||||||
|
for start_ym, end_ym in project_ranges.values():
|
||||||
|
for ym in _iter_year_months(start_ym, end_ym):
|
||||||
|
month_active_counts[ym] += 1
|
||||||
|
|
||||||
|
base_start_ym, base_end_ym = base_range
|
||||||
|
labor_shared = 0.0
|
||||||
|
common_shared = 0.0
|
||||||
|
for ym in candidate_months:
|
||||||
|
if ym < base_start_ym:
|
||||||
|
continue
|
||||||
|
if base_end_ym and ym > base_end_ym:
|
||||||
|
continue
|
||||||
|
active_count = int(month_active_counts.get(ym) or 0)
|
||||||
|
if active_count <= 0:
|
||||||
|
continue
|
||||||
|
labor_shared += float(labor_pool_by_month.get(ym) or 0) / active_count
|
||||||
|
common_shared += float(common_pool_by_month.get(ym) or 0) / active_count
|
||||||
|
|
||||||
|
return {"labor_shared": labor_shared, "common_shared": common_shared}
|
||||||
|
|
||||||
|
|
||||||
def build_company_allocated_project_rows(
|
def build_company_allocated_project_rows(
|
||||||
conn: sqlite3.Connection, project_rows: list[sqlite3.Row], source_project_type: str
|
conn: sqlite3.Connection, project_rows: list[sqlite3.Row], source_project_type: str
|
||||||
) -> tuple[list[dict], dict]:
|
) -> tuple[list[dict], dict]:
|
||||||
@@ -1250,6 +1369,85 @@ def build_project_lifecycle_cost(
|
|||||||
account_entry["shared_expense_supply"] += shared_expense_supply
|
account_entry["shared_expense_supply"] += shared_expense_supply
|
||||||
account_entry["expense_supply"] += base_expense_supply
|
account_entry["expense_supply"] += base_expense_supply
|
||||||
|
|
||||||
|
monthly_shared = calculate_monthly_shared_distribution(conn, base_project_code)
|
||||||
|
labor_shared = float(monthly_shared.get("labor_shared") or 0.0)
|
||||||
|
common_shared = float(monthly_shared.get("common_shared") or 0.0)
|
||||||
|
base_project_info = project_lookup.get(base_project_code) or {}
|
||||||
|
|
||||||
|
if labor_shared:
|
||||||
|
breakdown_components["인건비"]["shared"] += labor_shared
|
||||||
|
breakdown_components["인건비"]["total"] += labor_shared
|
||||||
|
project_entry = breakdown_project_maps["인건비"].setdefault(
|
||||||
|
base_project_code,
|
||||||
|
{
|
||||||
|
"project_code": base_project_code,
|
||||||
|
"project_name": base_project_info.get("project_name") or "",
|
||||||
|
"project_type": base_project_info.get("project_type") or "시공",
|
||||||
|
"construction_family": base_project_info.get("construction_family") or "",
|
||||||
|
"construction_method": base_project_info.get("construction_method") or "",
|
||||||
|
"allocation_numerator": 1,
|
||||||
|
"allocation_denominator": 1,
|
||||||
|
"allocation_ratio": 1.0,
|
||||||
|
"direct_expense_supply": 0.0,
|
||||||
|
"shared_expense_supply": 0.0,
|
||||||
|
"expense_supply": 0.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
if common_shared:
|
||||||
|
breakdown_components["관리비"]["shared"] += common_shared
|
||||||
|
breakdown_components["관리비"]["total"] += common_shared
|
||||||
|
project_entry = breakdown_project_maps["관리비"].setdefault(
|
||||||
|
base_project_code,
|
||||||
|
{
|
||||||
|
"project_code": base_project_code,
|
||||||
|
"project_name": base_project_info.get("project_name") or "",
|
||||||
|
"project_type": base_project_info.get("project_type") or "시공",
|
||||||
|
"construction_family": base_project_info.get("construction_family") or "",
|
||||||
|
"construction_method": base_project_info.get("construction_method") or "",
|
||||||
|
"allocation_numerator": 1,
|
||||||
|
"allocation_denominator": 1,
|
||||||
|
"allocation_ratio": 1.0,
|
||||||
|
"direct_expense_supply": 0.0,
|
||||||
|
"shared_expense_supply": 0.0,
|
||||||
|
"expense_supply": 0.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
total_expense = (
|
||||||
|
breakdown_components["시공비"]["total"]
|
||||||
|
+ breakdown_components["인건비"]["total"]
|
||||||
|
+ breakdown_components["관리비"]["total"]
|
||||||
|
)
|
||||||
|
|
||||||
breakdown = [
|
breakdown = [
|
||||||
{
|
{
|
||||||
"label": "시공비",
|
"label": "시공비",
|
||||||
|
|||||||
Reference in New Issue
Block a user