Refine process cost navigation and unify section layouts
This commit is contained in:
@@ -4473,6 +4473,54 @@ def save_project_quick_links(session_id: str | None, codes: list[str]) -> None:
|
||||
)
|
||||
|
||||
|
||||
def get_process_cost_quick_links() -> list[str]:
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT support_dept_code
|
||||
FROM project_quick_links
|
||||
WHERE page_key = 'process_cost'
|
||||
ORDER BY sort_order, updated_at DESC, support_dept_code
|
||||
"""
|
||||
)
|
||||
).mappings().all()
|
||||
return [normalize_text(row["support_dept_code"]) for row in rows if normalize_text(row["support_dept_code"])]
|
||||
|
||||
|
||||
def save_process_cost_quick_links(codes: list[str]) -> None:
|
||||
normalized_codes: list[str] = []
|
||||
for code in codes:
|
||||
normalized_code = normalize_text(code)
|
||||
if normalized_code and normalized_code not in normalized_codes:
|
||||
normalized_codes.append(normalized_code)
|
||||
with engine.begin() as conn:
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
DELETE FROM project_quick_links
|
||||
WHERE page_key = 'process_cost'
|
||||
"""
|
||||
)
|
||||
)
|
||||
for sort_order, support_dept_code in enumerate(normalized_codes):
|
||||
conn.execute(
|
||||
text(
|
||||
"""
|
||||
INSERT INTO project_quick_links (
|
||||
page_key, support_dept_code, sort_order, updated_at
|
||||
) VALUES (
|
||||
'process_cost', :support_dept_code, :sort_order, CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
),
|
||||
{
|
||||
"support_dept_code": support_dept_code,
|
||||
"sort_order": sort_order,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_project_uncontracted_classification_map() -> dict[str, str]:
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(
|
||||
@@ -5332,34 +5380,53 @@ def get_process_cost_related_codes(support_dept_code: str | None) -> list[str]:
|
||||
|
||||
|
||||
def get_process_cost_available_years(source: str) -> list[int]:
|
||||
normalized_source = normalize_text(source).lower()
|
||||
if normalized_source == "wehago":
|
||||
init_wehago_compare_db(engine)
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT DISTINCT fiscal_year
|
||||
FROM wehago_voucher_rows
|
||||
WHERE fiscal_year IS NOT NULL
|
||||
ORDER BY fiscal_year
|
||||
"""
|
||||
)
|
||||
).fetchall()
|
||||
return [int(row[0]) for row in rows if row and row[0] is not None]
|
||||
contract_meta = _get_project_contract_meta()
|
||||
detected_years = {
|
||||
year
|
||||
for code in contract_meta.keys()
|
||||
for year in [_extract_year_from_project_code(code)]
|
||||
if year is not None
|
||||
}
|
||||
max_year = max(detected_years) if detected_years else datetime.now().year
|
||||
min_year = 1994
|
||||
if max_year < min_year:
|
||||
max_year = min_year
|
||||
return list(range(min_year, max_year + 1))
|
||||
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT DISTINCT year
|
||||
FROM transactions
|
||||
WHERE year IS NOT NULL
|
||||
ORDER BY year
|
||||
"""
|
||||
)
|
||||
).fetchall()
|
||||
return [int(row[0]) for row in rows if row and row[0] is not None]
|
||||
|
||||
def _extract_year_from_project_code(value: Any) -> int | None:
|
||||
code = normalize_text(value).upper()
|
||||
if len(code) < 3:
|
||||
return None
|
||||
digits = "".join(ch for ch in code if ch.isdigit())
|
||||
if len(digits) < 2:
|
||||
return None
|
||||
year_2d = digits[:2]
|
||||
if not year_2d.isdigit():
|
||||
return None
|
||||
year_value = int(year_2d)
|
||||
if year_value >= 94:
|
||||
return 1900 + year_value
|
||||
return 2000 + year_value
|
||||
|
||||
|
||||
def _sort_process_cost_project_options(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
def sort_key(item: dict[str, Any]) -> str:
|
||||
return normalize_text(item.get("support_dept_code")).upper()
|
||||
|
||||
return sorted(items, key=sort_key)
|
||||
|
||||
|
||||
def _get_process_cost_project_kind(code: Any) -> tuple[str, str]:
|
||||
normalized = normalize_text(code).upper()
|
||||
prefix = normalized[:1]
|
||||
if prefix == "X":
|
||||
return "X", "사전사업"
|
||||
if prefix == "Y":
|
||||
return "Y", "설계"
|
||||
if prefix == "Z":
|
||||
return "Z", "감리"
|
||||
return "", "기타"
|
||||
|
||||
|
||||
def _get_project_contract_meta() -> dict[str, dict[str, Any]]:
|
||||
@@ -5385,7 +5452,9 @@ def _get_project_contract_meta() -> dict[str, dict[str, Any]]:
|
||||
COALESCE(c.support_dept_name, p.support_dept_name, b.support_dept_name, '') AS support_dept_name,
|
||||
COALESCE(c.hanmac_contract_amount, 0) AS hanmac_contract_amount,
|
||||
COALESCE(c.client_name, '') AS client_name,
|
||||
COALESCE(p.expected_as_cost, b.expected_as_cost, 0) AS expected_as_cost
|
||||
COALESCE(p.expected_as_cost, b.expected_as_cost, 0) AS expected_as_cost,
|
||||
COALESCE(p.expected_sga_budget, b.expected_sga_budget, 0) AS expected_sga_budget,
|
||||
COALESCE(p.project_start_date, b.project_start_date, '') AS project_start_date
|
||||
FROM code_universe AS u
|
||||
LEFT JOIN project_contract_info AS c
|
||||
ON c.support_dept_code = u.support_dept_code
|
||||
@@ -5405,12 +5474,117 @@ def _get_project_contract_meta() -> dict[str, dict[str, Any]]:
|
||||
return result
|
||||
|
||||
|
||||
def _get_project_actual_sga_summary(codes: list[str]) -> dict[str, dict[str, Any]]:
|
||||
normalized_codes = [normalize_text(code) for code in codes if normalize_text(code)]
|
||||
if not normalized_codes:
|
||||
return {}
|
||||
in_clause, params = build_in_clause("actual_sga_code", normalized_codes)
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT
|
||||
support_dept_code,
|
||||
COALESCE(label, '') AS label,
|
||||
COALESCE(reference, '') AS reference,
|
||||
COALESCE(note, '') AS note,
|
||||
COALESCE(grade, '') AS grade,
|
||||
COALESCE(minutes, '') AS minutes,
|
||||
COALESCE(amount, 0) AS amount
|
||||
FROM project_actual_input_entries
|
||||
WHERE support_dept_code IN ({in_clause})
|
||||
AND COALESCE(group_name, '') = 'sga'
|
||||
ORDER BY position, id
|
||||
"""
|
||||
),
|
||||
params,
|
||||
).mappings().all()
|
||||
|
||||
grouped: dict[str, list[dict[str, Any]]] = {}
|
||||
for row in rows:
|
||||
code = normalize_text(row.get("support_dept_code"))
|
||||
if not code:
|
||||
continue
|
||||
grouped.setdefault(code, []).append(dict(row))
|
||||
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
for code, code_rows in grouped.items():
|
||||
total_amount = sum(normalize_amount(item.get("amount")) for item in code_rows)
|
||||
has_detail_trace = any(
|
||||
normalize_text(item.get("reference"))
|
||||
or normalize_text(item.get("note"))
|
||||
or normalize_text(item.get("grade"))
|
||||
or normalize_text(item.get("minutes"))
|
||||
for item in code_rows
|
||||
)
|
||||
distinct_labels = {
|
||||
normalize_text(item.get("label"))
|
||||
for item in code_rows
|
||||
if normalize_text(item.get("label"))
|
||||
}
|
||||
result[code] = {
|
||||
"rows": code_rows,
|
||||
"amount": total_amount,
|
||||
"has_detail_trace": has_detail_trace,
|
||||
"distinct_labels": distinct_labels,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _get_project_exec_budget_summary(codes: list[str]) -> dict[str, dict[str, float]]:
|
||||
normalized_codes = [normalize_text(code) for code in codes if normalize_text(code)]
|
||||
if not normalized_codes:
|
||||
return {}
|
||||
in_clause, params = build_in_clause("exec_budget_code", normalized_codes)
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(
|
||||
text(
|
||||
f"""
|
||||
SELECT
|
||||
support_dept_code,
|
||||
COALESCE(group_name, '') AS group_name,
|
||||
SUM(COALESCE(amount, 0)) AS amount
|
||||
FROM project_exec_budget_entries
|
||||
WHERE support_dept_code IN ({in_clause})
|
||||
GROUP BY support_dept_code, group_name
|
||||
"""
|
||||
),
|
||||
params,
|
||||
).mappings().all()
|
||||
|
||||
result: dict[str, dict[str, float]] = {}
|
||||
for row in rows:
|
||||
code = normalize_text(row.get("support_dept_code"))
|
||||
group_name = normalize_text(row.get("group_name"))
|
||||
if not code:
|
||||
continue
|
||||
current = result.setdefault(code, {"labor": 0.0, "outsource": 0.0, "cost_plan": 0.0})
|
||||
current[group_name or "labor"] = normalize_amount(row.get("amount"))
|
||||
return result
|
||||
|
||||
|
||||
def _is_real_project_actual_sga(
|
||||
actual_sga_summary: dict[str, Any] | None,
|
||||
expected_sga_budget: float,
|
||||
) -> bool:
|
||||
if not actual_sga_summary:
|
||||
return False
|
||||
amount = normalize_amount(actual_sga_summary.get("amount"))
|
||||
if amount <= 0:
|
||||
return False
|
||||
if actual_sga_summary.get("has_detail_trace"):
|
||||
return True
|
||||
distinct_labels = set(actual_sga_summary.get("distinct_labels") or [])
|
||||
if len(distinct_labels) > 1:
|
||||
return True
|
||||
if distinct_labels and distinct_labels != {"판관비"}:
|
||||
return True
|
||||
if expected_sga_budget > 0 and abs(amount - expected_sga_budget) <= 0.5:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _get_hanmac_process_cost_tx_by_code(selected_year: int | None) -> dict[str, dict[str, Any]]:
|
||||
year_clause = ""
|
||||
params: dict[str, Any] = {}
|
||||
if selected_year:
|
||||
year_clause = "AND year = :selected_year"
|
||||
params["selected_year"] = selected_year
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(
|
||||
text(
|
||||
@@ -5427,11 +5601,10 @@ def _get_hanmac_process_cost_tx_by_code(selected_year: int | None) -> dict[str,
|
||||
AND support_dept_code NOT IN ('ZZZZZZ')
|
||||
AND COALESCE(support_dept_name, '') <> ''
|
||||
AND support_dept_name NOT IN ('공통', '경영지원부', '기술개발센터', '임원실', '기술개발부', '총괄기획실')
|
||||
{year_clause}
|
||||
GROUP BY support_dept_code
|
||||
"""
|
||||
),
|
||||
params,
|
||||
{},
|
||||
).mappings().all()
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
for row in rows:
|
||||
@@ -5448,18 +5621,18 @@ def _get_hanmac_process_cost_tx_by_code(selected_year: int | None) -> dict[str,
|
||||
return result
|
||||
|
||||
|
||||
def get_process_cost_project_options(source: str, selected_year: int | None, include_related: bool = False) -> list[dict[str, Any]]:
|
||||
def get_process_cost_project_options(
|
||||
source: str,
|
||||
start_year: int | None,
|
||||
end_year: int | None,
|
||||
include_related: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
normalized_source = normalize_text(source).lower()
|
||||
contract_meta = _get_project_contract_meta()
|
||||
billing_summary_map = get_project_billing_summary_map() if normalized_source != "wehago" else {}
|
||||
|
||||
if normalized_source == "wehago":
|
||||
init_wehago_compare_db(engine)
|
||||
year_clause = ""
|
||||
params: dict[str, Any] = {}
|
||||
if selected_year:
|
||||
year_clause = "AND fiscal_year = :selected_year"
|
||||
params["selected_year"] = selected_year
|
||||
with engine.begin() as conn:
|
||||
rows = conn.execute(
|
||||
text(
|
||||
@@ -5480,7 +5653,6 @@ def get_process_cost_project_options(source: str, selected_year: int | None, inc
|
||||
FROM wehago_voucher_rows
|
||||
WHERE COALESCE(support_dept_code, '') <> ''
|
||||
AND support_dept_code NOT IN ('ZZZZZZ')
|
||||
{year_clause}
|
||||
)
|
||||
SELECT
|
||||
support_dept_code,
|
||||
@@ -5494,10 +5666,10 @@ def get_process_cost_project_options(source: str, selected_year: int | None, inc
|
||||
ORDER BY expense_amount DESC, support_dept_code
|
||||
"""
|
||||
),
|
||||
params,
|
||||
{},
|
||||
).mappings().all()
|
||||
else:
|
||||
tx_by_code = _get_hanmac_process_cost_tx_by_code(selected_year)
|
||||
tx_by_code = _get_hanmac_process_cost_tx_by_code(None)
|
||||
universe_codes = sorted(set(contract_meta) | set(billing_summary_map) | set(tx_by_code))
|
||||
rows = []
|
||||
for code in universe_codes:
|
||||
@@ -5525,6 +5697,14 @@ def get_process_cost_project_options(source: str, selected_year: int | None, inc
|
||||
continue
|
||||
contract_row = contract_meta.get(code, {})
|
||||
billing_row = billing_summary_map.get(code, {})
|
||||
project_start_date = normalize_text(contract_row.get("project_start_date"))
|
||||
project_start_year = _extract_year_from_project_code(code)
|
||||
if start_year and project_start_year and project_start_year < start_year:
|
||||
continue
|
||||
if end_year and project_start_year and project_start_year > end_year:
|
||||
continue
|
||||
if (start_year or end_year) and not project_start_year:
|
||||
continue
|
||||
contract_amount = normalize_amount(contract_row.get("hanmac_contract_amount"))
|
||||
revenue_amount = normalize_amount(row.get("revenue_amount"))
|
||||
expense_amount = normalize_amount(row.get("expense_amount"))
|
||||
@@ -5541,10 +5721,12 @@ def get_process_cost_project_options(source: str, selected_year: int | None, inc
|
||||
"profit_rate": _safe_ratio(profit_amount, revenue_amount),
|
||||
"voucher_count": int(row.get("voucher_count") or 0),
|
||||
"last_posting_date": normalize_text(row.get("last_posting_date")),
|
||||
"project_start_date": project_start_date,
|
||||
"project_kind_code": _get_process_cost_project_kind(code)[0],
|
||||
"project_kind_label": _get_process_cost_project_kind(code)[1],
|
||||
}
|
||||
)
|
||||
result.sort(key=lambda item: (-normalize_amount(item.get("expense_amount")), -normalize_amount(item.get("revenue_amount")), item.get("support_dept_code", "")))
|
||||
return result
|
||||
return _sort_process_cost_project_options(result)
|
||||
|
||||
|
||||
def get_process_cost_project_detail(
|
||||
@@ -5568,14 +5750,12 @@ def get_process_cost_project_detail(
|
||||
contract_meta = contract_meta_map.get(code, {})
|
||||
related_clusters = _build_process_cost_related_clusters() if include_related else {}
|
||||
cluster_codes = related_clusters.get(code, [code]) if include_related else [code]
|
||||
actual_sga_summary_map = _get_project_actual_sga_summary(cluster_codes)
|
||||
exec_budget_summary_map = _get_project_exec_budget_summary(cluster_codes)
|
||||
if normalized_source == "wehago":
|
||||
init_wehago_compare_db(engine)
|
||||
in_clause, code_params = build_in_clause("process_cost_wehago_code", cluster_codes)
|
||||
year_clause = ""
|
||||
params: dict[str, Any] = dict(code_params)
|
||||
if selected_year:
|
||||
year_clause = "AND fiscal_year = :selected_year"
|
||||
params["selected_year"] = selected_year
|
||||
amount_expr = (
|
||||
"CASE "
|
||||
"WHEN ABS(COALESCE(compare_amount, 0)) > 0 THEN ABS(COALESCE(compare_amount, 0)) "
|
||||
@@ -5599,7 +5779,6 @@ def get_process_cost_project_detail(
|
||||
MAX(COALESCE(proof_date, '')) AS last_posting_date
|
||||
FROM wehago_voucher_rows
|
||||
WHERE support_dept_code IN ({in_clause})
|
||||
{year_clause}
|
||||
"""
|
||||
),
|
||||
params,
|
||||
@@ -5616,7 +5795,6 @@ def get_process_cost_project_detail(
|
||||
FROM wehago_voucher_rows
|
||||
WHERE support_dept_code IN ({in_clause})
|
||||
AND (account_code LIKE '5%' OR account_code LIKE '6%')
|
||||
{year_clause}
|
||||
GROUP BY account_code, account_name
|
||||
ORDER BY amount DESC, account_code
|
||||
LIMIT 14
|
||||
@@ -5633,7 +5811,6 @@ def get_process_cost_project_detail(
|
||||
SUM(CASE WHEN account_code LIKE '5%' OR account_code LIKE '6%' THEN {amount_expr} ELSE 0 END) AS expense_amount
|
||||
FROM wehago_voucher_rows
|
||||
WHERE support_dept_code IN ({in_clause})
|
||||
{year_clause}
|
||||
AND LENGTH(COALESCE(proof_date, '')) >= 7
|
||||
GROUP BY month_label
|
||||
ORDER BY month_label DESC
|
||||
@@ -5644,11 +5821,7 @@ def get_process_cost_project_detail(
|
||||
).mappings().all()
|
||||
else:
|
||||
in_clause, code_params = build_in_clause("process_cost_code", cluster_codes)
|
||||
year_clause = ""
|
||||
params = dict(code_params)
|
||||
if selected_year:
|
||||
year_clause = "AND year = :selected_year"
|
||||
params["selected_year"] = selected_year
|
||||
with engine.begin() as conn:
|
||||
summary = conn.execute(
|
||||
text(
|
||||
@@ -5665,7 +5838,6 @@ def get_process_cost_project_detail(
|
||||
MAX(COALESCE(posting_date, '')) AS last_posting_date
|
||||
FROM transactions
|
||||
WHERE support_dept_code IN ({in_clause})
|
||||
{year_clause}
|
||||
"""
|
||||
),
|
||||
params,
|
||||
@@ -5682,7 +5854,6 @@ def get_process_cost_project_detail(
|
||||
FROM transactions
|
||||
WHERE support_dept_code IN ({in_clause})
|
||||
AND (account_code LIKE '5%' OR account_code LIKE '6%')
|
||||
{year_clause}
|
||||
GROUP BY account_code, account_name
|
||||
ORDER BY amount DESC, account_code
|
||||
LIMIT 14
|
||||
@@ -5699,7 +5870,6 @@ def get_process_cost_project_detail(
|
||||
SUM(CASE WHEN account_code LIKE '5%' OR account_code LIKE '6%' THEN amount ELSE 0 END) AS expense_amount
|
||||
FROM transactions
|
||||
WHERE support_dept_code IN ({in_clause})
|
||||
{year_clause}
|
||||
AND year IS NOT NULL
|
||||
AND month IS NOT NULL
|
||||
GROUP BY year, month
|
||||
@@ -5714,24 +5884,46 @@ def get_process_cost_project_detail(
|
||||
if include_related:
|
||||
contract_amount = sum(normalize_amount(contract_meta_map.get(member, {}).get("hanmac_contract_amount")) for member in cluster_codes)
|
||||
as_cost_amount = sum(normalize_amount(contract_meta_map.get(member, {}).get("expected_as_cost")) for member in cluster_codes)
|
||||
expected_sga_budget = sum(normalize_amount(contract_meta_map.get(member, {}).get("expected_sga_budget")) for member in cluster_codes)
|
||||
planned_labor_amount = sum(normalize_amount((exec_budget_summary_map.get(member) or {}).get("labor")) for member in cluster_codes)
|
||||
planned_outsource_amount = sum(normalize_amount((exec_budget_summary_map.get(member) or {}).get("outsource")) for member in cluster_codes)
|
||||
planned_cost_plan_amount = sum(normalize_amount((exec_budget_summary_map.get(member) or {}).get("cost_plan")) for member in cluster_codes)
|
||||
else:
|
||||
contract_amount = normalize_amount(contract_meta.get("hanmac_contract_amount"))
|
||||
as_cost_amount = normalize_amount(contract_meta.get("expected_as_cost"))
|
||||
expected_sga_budget = normalize_amount(contract_meta.get("expected_sga_budget"))
|
||||
planned_labor_amount = normalize_amount((exec_budget_summary_map.get(code) or {}).get("labor"))
|
||||
planned_outsource_amount = normalize_amount((exec_budget_summary_map.get(code) or {}).get("outsource"))
|
||||
planned_cost_plan_amount = normalize_amount((exec_budget_summary_map.get(code) or {}).get("cost_plan"))
|
||||
revenue_amount = normalize_amount(summary_row.get("revenue_amount"))
|
||||
expense_amount = normalize_amount(summary_row.get("expense_amount"))
|
||||
ledger_expense_amount = normalize_amount(summary_row.get("expense_amount"))
|
||||
labor_amount = normalize_amount(summary_row.get("labor_amount"))
|
||||
outsourcing_amount = normalize_amount(summary_row.get("outsourcing_amount"))
|
||||
sga_amount = normalize_amount(summary_row.get("sga_amount"))
|
||||
design_cost_amount = max(expense_amount - labor_amount - outsourcing_amount - sga_amount, 0.0)
|
||||
ledger_sga_amount = normalize_amount(summary_row.get("sga_amount"))
|
||||
real_project_actual_sga_amount = 0.0
|
||||
for member in cluster_codes:
|
||||
member_expected_sga_budget = normalize_amount(contract_meta_map.get(member, {}).get("expected_sga_budget"))
|
||||
actual_summary = actual_sga_summary_map.get(member)
|
||||
if _is_real_project_actual_sga(actual_summary, member_expected_sga_budget):
|
||||
real_project_actual_sga_amount += normalize_amount((actual_summary or {}).get("amount"))
|
||||
sga_amount = ledger_sga_amount + real_project_actual_sga_amount
|
||||
expense_amount = ledger_expense_amount + real_project_actual_sga_amount
|
||||
design_cost_amount = max(ledger_expense_amount - labor_amount - outsourcing_amount - ledger_sga_amount, 0.0)
|
||||
profit_amount = revenue_amount - expense_amount
|
||||
|
||||
target_base = contract_amount * 0.85 if contract_amount > 0 else max(revenue_amount * 0.85, expense_amount)
|
||||
target_base = (
|
||||
planned_labor_amount
|
||||
+ planned_outsource_amount
|
||||
+ planned_cost_plan_amount
|
||||
+ as_cost_amount
|
||||
+ expected_sga_budget
|
||||
)
|
||||
phase_rows = [
|
||||
{"phase": "직접인건비", "target_amount": target_base * 0.30, "actual_amount": labor_amount},
|
||||
{"phase": "외주비", "target_amount": target_base * 0.32, "actual_amount": outsourcing_amount},
|
||||
{"phase": "제경비", "target_amount": target_base * 0.23, "actual_amount": design_cost_amount},
|
||||
{"phase": "직접인건비", "target_amount": planned_labor_amount, "actual_amount": labor_amount},
|
||||
{"phase": "외주비", "target_amount": planned_outsource_amount, "actual_amount": outsourcing_amount},
|
||||
{"phase": "제경비", "target_amount": planned_cost_plan_amount, "actual_amount": design_cost_amount},
|
||||
{"phase": "A/S비", "target_amount": as_cost_amount, "actual_amount": as_cost_amount},
|
||||
{"phase": "판관비", "target_amount": target_base * 0.15, "actual_amount": sga_amount},
|
||||
{"phase": "판관비", "target_amount": expected_sga_budget, "actual_amount": sga_amount},
|
||||
]
|
||||
for row in phase_rows:
|
||||
row["gap_amount"] = row["target_amount"] - row["actual_amount"]
|
||||
@@ -5750,6 +5942,19 @@ def get_process_cost_project_detail(
|
||||
"last_posting_date": normalize_text(row.get("last_posting_date")),
|
||||
}
|
||||
)
|
||||
if real_project_actual_sga_amount > 0:
|
||||
normalized_accounts.append(
|
||||
{
|
||||
"account_code": "PROJECT-SGA",
|
||||
"account_name": "판관비(프로젝트 정보)",
|
||||
"amount": real_project_actual_sga_amount,
|
||||
"row_count": sum(len((actual_sga_summary_map.get(member) or {}).get("rows") or []) for member in cluster_codes),
|
||||
"share_rate": _safe_ratio(real_project_actual_sga_amount, expense_amount),
|
||||
"last_posting_date": "",
|
||||
}
|
||||
)
|
||||
normalized_accounts.sort(key=lambda item: (-normalize_amount(item.get("amount")), normalize_text(item.get("account_code"))))
|
||||
normalized_accounts = normalized_accounts[:14]
|
||||
|
||||
normalized_monthly = []
|
||||
for row in monthly_rows:
|
||||
@@ -5781,6 +5986,9 @@ def get_process_cost_project_detail(
|
||||
"voucher_count": int(summary_row.get("voucher_count") or 0),
|
||||
"row_count": int(summary_row.get("row_count") or 0),
|
||||
"included_codes": cluster_codes if include_related else [code],
|
||||
"expected_sga_budget": expected_sga_budget,
|
||||
"ledger_sga_amount": ledger_sga_amount,
|
||||
"project_actual_sga_amount": real_project_actual_sga_amount,
|
||||
},
|
||||
"phase_rows": phase_rows,
|
||||
"account_rows": normalized_accounts,
|
||||
@@ -5798,7 +6006,8 @@ def get_process_cost_project_detail(
|
||||
def render_process_cost_page(
|
||||
request: Request,
|
||||
source: str | None = None,
|
||||
year: int | None = None,
|
||||
start_year: int | None = None,
|
||||
end_year: int | None = None,
|
||||
code: str | None = None,
|
||||
include_related: bool = False,
|
||||
message: str = "",
|
||||
@@ -5810,17 +6019,39 @@ def render_process_cost_page(
|
||||
normalized_source = "hanmac"
|
||||
|
||||
years = get_process_cost_available_years(normalized_source)
|
||||
selected_year = year if year in years else None
|
||||
project_options = get_process_cost_project_options(normalized_source, selected_year, include_related=include_related)
|
||||
selected_start_year = start_year if start_year in years else None
|
||||
selected_end_year = end_year if end_year in years else None
|
||||
if not selected_start_year and not selected_end_year and years:
|
||||
selected_start_year = years[0]
|
||||
selected_end_year = years[-1]
|
||||
elif selected_start_year and selected_end_year and selected_start_year > selected_end_year:
|
||||
selected_end_year = selected_start_year
|
||||
elif selected_end_year and not selected_start_year:
|
||||
selected_start_year = years[0] if years else None
|
||||
if selected_start_year and selected_start_year > selected_end_year:
|
||||
selected_start_year = selected_end_year
|
||||
elif selected_start_year and not selected_end_year:
|
||||
selected_end_year = years[-1] if years else None
|
||||
if selected_end_year and selected_end_year < selected_start_year:
|
||||
selected_end_year = selected_start_year
|
||||
|
||||
project_options = get_process_cost_project_options(
|
||||
normalized_source,
|
||||
selected_start_year,
|
||||
selected_end_year,
|
||||
include_related=include_related,
|
||||
)
|
||||
selected_code = normalize_text(code)
|
||||
if selected_code and not any(item["support_dept_code"] == selected_code for item in project_options):
|
||||
selected_code = ""
|
||||
if not selected_code and project_options:
|
||||
selected_code = project_options[0]["support_dept_code"]
|
||||
selected_project = next(
|
||||
(item for item in project_options if normalize_text(item.get("support_dept_code")) == selected_code),
|
||||
None,
|
||||
)
|
||||
|
||||
detail = get_process_cost_project_detail(
|
||||
normalized_source,
|
||||
selected_year,
|
||||
None,
|
||||
selected_code,
|
||||
include_related=include_related,
|
||||
)
|
||||
@@ -5828,12 +6059,16 @@ def render_process_cost_page(
|
||||
**base_context(request, message),
|
||||
"process_cost_source": normalized_source,
|
||||
"process_cost_years": years,
|
||||
"process_cost_selected_year": selected_year,
|
||||
"process_cost_years_desc": sorted(years, reverse=True),
|
||||
"process_cost_selected_start_year": selected_start_year,
|
||||
"process_cost_selected_end_year": selected_end_year,
|
||||
"process_cost_selected_code": selected_code,
|
||||
"process_cost_selected_project": selected_project,
|
||||
"process_cost_include_related": include_related,
|
||||
"process_cost_projects": project_options,
|
||||
"process_cost_detail": detail,
|
||||
"process_cost_related_codes": get_process_cost_related_codes(selected_code),
|
||||
"process_cost_quick_link_codes": get_process_cost_quick_links(),
|
||||
}
|
||||
return templates.TemplateResponse(request, "process_cost.html", context)
|
||||
|
||||
@@ -6832,6 +7067,31 @@ async def project_quick_links_save(request: Request):
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/process-cost/quick-links")
|
||||
async def process_cost_quick_links_load():
|
||||
try:
|
||||
return JSONResponse(content={"codes": get_process_cost_quick_links()})
|
||||
except Exception as exc:
|
||||
logger.exception("프로세스 원가 바로가기 조회 에러: %s", exc)
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.post("/process-cost/quick-links")
|
||||
async def process_cost_quick_links_save(request: Request):
|
||||
try:
|
||||
payload = await request.json()
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("잘못된 바로가기 형식입니다.")
|
||||
codes = payload.get("codes") or []
|
||||
if not isinstance(codes, list):
|
||||
raise ValueError("바로가기 목록 형식이 잘못되었습니다.")
|
||||
save_process_cost_quick_links([normalize_text(code) for code in codes if isinstance(code, str)])
|
||||
return JSONResponse(content={"status": "ok", "codes": get_process_cost_quick_links()})
|
||||
except Exception as exc:
|
||||
logger.exception("프로세스 원가 바로가기 저장 에러: %s", exc)
|
||||
return JSONResponse(content={"error": str(exc)}, status_code=500)
|
||||
|
||||
|
||||
@app.post("/projects/uncontracted-category")
|
||||
async def project_uncontracted_category_save(request: Request):
|
||||
try:
|
||||
@@ -7214,7 +7474,8 @@ async def annual_summary(request: Request):
|
||||
async def process_cost(
|
||||
request: Request,
|
||||
source: str | None = None,
|
||||
year: str | None = None,
|
||||
start_year: str | None = None,
|
||||
end_year: str | None = None,
|
||||
code: str | None = None,
|
||||
include_related: str | None = None,
|
||||
):
|
||||
@@ -7222,7 +7483,8 @@ async def process_cost(
|
||||
return render_process_cost_page(
|
||||
request,
|
||||
source=source,
|
||||
year=parse_optional_year(year),
|
||||
start_year=parse_optional_year(start_year),
|
||||
end_year=parse_optional_year(end_year),
|
||||
code=code,
|
||||
include_related=normalize_text(include_related) in {"1", "true", "y", "yes", "on"},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user