Adjust lifecycle allocation UI and account-level shared cost breakdown
This commit is contained in:
@@ -1925,6 +1925,7 @@
|
|||||||
const [lifecycleAllocationModal, setLifecycleAllocationModal] = useState(null);
|
const [lifecycleAllocationModal, setLifecycleAllocationModal] = useState(null);
|
||||||
const [lifecycleAllocationSaving, setLifecycleAllocationSaving] = useState(false);
|
const [lifecycleAllocationSaving, setLifecycleAllocationSaving] = useState(false);
|
||||||
const [lifecycleCommonAllocationSaving, setLifecycleCommonAllocationSaving] = useState(false);
|
const [lifecycleCommonAllocationSaving, setLifecycleCommonAllocationSaving] = useState(false);
|
||||||
|
const [lifecycleCommonAllocationDraft, setLifecycleCommonAllocationDraft] = useState("");
|
||||||
const [projectEditModalOpen, setProjectEditModalOpen] = useState(false);
|
const [projectEditModalOpen, setProjectEditModalOpen] = useState(false);
|
||||||
const [relatedProjectSearch, setRelatedProjectSearch] = useState("");
|
const [relatedProjectSearch, setRelatedProjectSearch] = useState("");
|
||||||
const [projectTxnDateFrom, setProjectTxnDateFrom] = useState("");
|
const [projectTxnDateFrom, setProjectTxnDateFrom] = useState("");
|
||||||
@@ -1963,6 +1964,13 @@
|
|||||||
});
|
});
|
||||||
const deferredProjectKeyword = useDeferredValue(projectKeyword);
|
const deferredProjectKeyword = useDeferredValue(projectKeyword);
|
||||||
const deferredVendorKeyword = useDeferredValue(vendorKeyword);
|
const deferredVendorKeyword = useDeferredValue(vendorKeyword);
|
||||||
|
const effectiveCommonAllocationMode = lifecycleCommonAllocationDraft
|
||||||
|
|| detail?.lifecycle_cost?.summary?.common_allocation_mode
|
||||||
|
|| "expense_ratio";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLifecycleCommonAllocationDraft("");
|
||||||
|
}, [selectedProjectCode]);
|
||||||
const debouncedProjectKeyword = useDebouncedValue(deferredProjectKeyword, 250);
|
const debouncedProjectKeyword = useDebouncedValue(deferredProjectKeyword, 250);
|
||||||
const debouncedVendorKeyword = useDebouncedValue(deferredVendorKeyword, 250);
|
const debouncedVendorKeyword = useDebouncedValue(deferredVendorKeyword, 250);
|
||||||
const deferredRelatedProjectSearch = useDeferredValue(relatedProjectSearch);
|
const deferredRelatedProjectSearch = useDeferredValue(relatedProjectSearch);
|
||||||
@@ -3609,6 +3617,8 @@
|
|||||||
async function saveLifecycleCommonAllocationMode(nextMode) {
|
async function saveLifecycleCommonAllocationMode(nextMode) {
|
||||||
if (!selectedProjectCode) return;
|
if (!selectedProjectCode) return;
|
||||||
if (!["expense_ratio", "income_ratio"].includes(nextMode || "")) return;
|
if (!["expense_ratio", "income_ratio"].includes(nextMode || "")) return;
|
||||||
|
if (effectiveCommonAllocationMode === nextMode) return;
|
||||||
|
setLifecycleCommonAllocationDraft(nextMode);
|
||||||
setLifecycleCommonAllocationSaving(true);
|
setLifecycleCommonAllocationSaving(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -3628,6 +3638,7 @@
|
|||||||
setDetail(nextDetail);
|
setDetail(nextDetail);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
setLifecycleCommonAllocationDraft("");
|
||||||
setError("공통배분 기준 저장에 실패했습니다.");
|
setError("공통배분 기준 저장에 실패했습니다.");
|
||||||
} finally {
|
} finally {
|
||||||
setLifecycleCommonAllocationSaving(false);
|
setLifecycleCommonAllocationSaving(false);
|
||||||
@@ -5211,17 +5222,36 @@
|
|||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 18, fontWeight: 700 }}>프로젝트 생애주기 원가</div>
|
<div style={{ fontSize: 18, fontWeight: 700 }}>프로젝트 생애주기 원가</div>
|
||||||
<div className="subtle" style={{ marginTop: 6 }}>현재 시공 프로젝트를 포함해 연결된 전체 비용을 시공비, 인건비, 관리비로 나눠 봅니다.</div>
|
<div className="subtle" style={{ marginTop: 6 }}>현재 시공 프로젝트를 포함해 연결된 전체 비용을 시공비, 인건비, 관리비로 나눠 봅니다.</div>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 10 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 14, marginTop: 10, minWidth: 0 }}>
|
||||||
<span className="subtle">공통배분 기준</span>
|
<span className="subtle" style={{ whiteSpace: "nowrap", flex: "0 0 auto" }}>공통배분 기준</span>
|
||||||
<select
|
<div style={{ display: "flex", gap: 6 }}>
|
||||||
value={detail?.lifecycle_cost?.summary?.common_allocation_mode || "expense_ratio"}
|
<button
|
||||||
onChange={(e) => saveLifecycleCommonAllocationMode(e.target.value)}
|
type="button"
|
||||||
|
className={`mode-chip ${effectiveCommonAllocationMode === "expense_ratio" ? "active" : ""}`}
|
||||||
|
onClick={() => saveLifecycleCommonAllocationMode("expense_ratio")}
|
||||||
disabled={lifecycleCommonAllocationSaving}
|
disabled={lifecycleCommonAllocationSaving}
|
||||||
style={{ minWidth: 220 }}
|
style={{ minWidth: 116, height: 34, fontSize: 13, borderRadius: 10, padding: "0 10px" }}
|
||||||
>
|
>
|
||||||
<option value="expense_ratio">프로젝트 지출 / 전체지출 비율</option>
|
지출기준
|
||||||
<option value="income_ratio">프로젝트 입금 / 전체입금 비율</option>
|
</button>
|
||||||
</select>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`mode-chip ${effectiveCommonAllocationMode === "income_ratio" ? "active" : ""}`}
|
||||||
|
onClick={() => saveLifecycleCommonAllocationMode("income_ratio")}
|
||||||
|
disabled={lifecycleCommonAllocationSaving}
|
||||||
|
style={{ minWidth: 116, height: 34, fontSize: 13, borderRadius: 10, padding: "0 10px" }}
|
||||||
|
>
|
||||||
|
수입기준
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="subtle" style={{ marginLeft: 12, lineHeight: 1.45, minWidth: 0 }}>
|
||||||
|
<div style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||||
|
본사관리비 배부원천 x (프로젝트 기준값 / 전체 기준값)
|
||||||
|
</div>
|
||||||
|
<div style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||||
|
기준값: {effectiveCommonAllocationMode === "income_ratio" ? "프로젝트 입금 / 전체입금" : "프로젝트 지출 / 전체지출"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<span className="subtle">{lifecycleCommonAllocationSaving ? "저장 중..." : ""}</span>
|
<span className="subtle">{lifecycleCommonAllocationSaving ? "저장 중..." : ""}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -5393,6 +5423,12 @@
|
|||||||
bucket_label: lifecycleBreakdownModal.label,
|
bucket_label: lifecycleBreakdownModal.label,
|
||||||
account_code: item.account_code || "",
|
account_code: item.account_code || "",
|
||||||
account_name: item.account_name || "",
|
account_name: item.account_name || "",
|
||||||
|
allocation_source_amount: Number(item.allocation_source_amount || 0),
|
||||||
|
allocation_project_basis_amount: Number(item.allocation_project_basis_amount || 0),
|
||||||
|
allocation_total_basis_amount: Number(item.allocation_total_basis_amount || 0),
|
||||||
|
allocation_mode: item.allocation_mode || "",
|
||||||
|
allocation_result_amount: Number(item.expense_supply || 0),
|
||||||
|
allocation_details: Array.isArray(item.allocation_details) ? item.allocation_details.map((row) => ({ ...row })) : [],
|
||||||
detail: null,
|
detail: null,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -5413,6 +5449,37 @@
|
|||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 700 }}>{item.account_name || "미지정 계정"}</div>
|
<div style={{ fontWeight: 700 }}>{item.account_name || "미지정 계정"}</div>
|
||||||
<div className="subtle" style={{ marginTop: 4 }}>{item.account_code || "-"}</div>
|
<div className="subtle" style={{ marginTop: 4 }}>{item.account_code || "-"}</div>
|
||||||
|
{((item.account_code || "").startsWith("SHARED_")) && (
|
||||||
|
<div className="subtle" style={{ marginTop: 4 }}>
|
||||||
|
본사관리비 배부원천 x (프로젝트 기준값 / 전체 기준값)
|
||||||
|
<br />
|
||||||
|
기준값: {item.allocation_mode === "income_ratio" ? "프로젝트 입금 / 전체입금" : "프로젝트 지출 / 전체지출"}
|
||||||
|
<br />
|
||||||
|
{(Array.isArray(item.allocation_details) && item.allocation_details.length
|
||||||
|
? item.allocation_details
|
||||||
|
: [{
|
||||||
|
source_amount: item.allocation_source_amount || 0,
|
||||||
|
project_basis_amount: item.allocation_project_basis_amount || 0,
|
||||||
|
total_basis_amount: item.allocation_total_basis_amount || 0,
|
||||||
|
allocated_amount: item.expense_supply || 0,
|
||||||
|
}]
|
||||||
|
).map((row, idx) => {
|
||||||
|
const sourceAmount = Number(row.source_amount || 0);
|
||||||
|
const allocatedAmount = Number(row.allocated_amount || 0);
|
||||||
|
const projectBasisAmount = Number(
|
||||||
|
row.display_project_basis_amount ?? row.project_basis_amount ?? 0
|
||||||
|
);
|
||||||
|
const totalBasisAmount = Number(
|
||||||
|
row.display_total_basis_amount ?? row.total_basis_amount ?? 0
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<span key={`alloc-line-${item.account_code}-${idx}`} style={{ display: "block" }}>
|
||||||
|
{((row.year_month || "").slice(0, 4) || "-")}년 · {fmt(sourceAmount)}원 x ({fmt(projectBasisAmount)}원 / {fmt(totalBasisAmount)}원) = {fmt(allocatedAmount)}원
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ textAlign: "right", fontWeight: 700 }}>{fmt(item.expense_supply || 0)}원</div>
|
<div style={{ textAlign: "right", fontWeight: 700 }}>{fmt(item.expense_supply || 0)}원</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -6755,6 +6822,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "grid", gap: 18, marginTop: 18 }}>
|
<div style={{ display: "grid", gap: 18, marginTop: 18 }}>
|
||||||
|
{((lifecycleAccountDetailModal.account_code || "").startsWith("SHARED_") ||
|
||||||
|
Number(lifecycleAccountDetailModal.allocation_source_amount || 0) > 0 ||
|
||||||
|
Number(lifecycleAccountDetailModal.detail?.allocation_source_amount || 0) > 0) && (
|
||||||
|
<section className="mini-card">
|
||||||
|
<div style={{ fontSize: 16, fontWeight: 700 }}>배부 계산식</div>
|
||||||
|
<div className="subtle" style={{ marginTop: 6 }}>
|
||||||
|
본사관리비 배부원천 x (프로젝트 기준값 / 전체 기준값)
|
||||||
|
</div>
|
||||||
|
<div className="subtle" style={{ marginTop: 4 }}>
|
||||||
|
기준값: {(lifecycleAccountDetailModal.detail?.allocation_mode || lifecycleAccountDetailModal.allocation_mode) === "income_ratio"
|
||||||
|
? "프로젝트 입금 / 전체입금"
|
||||||
|
: "프로젝트 지출 / 전체지출"}
|
||||||
|
</div>
|
||||||
|
<div className="subtle" style={{ marginTop: 8, lineHeight: 1.55 }}>
|
||||||
|
{(() => {
|
||||||
|
const rows = (Array.isArray(lifecycleAccountDetailModal.detail?.allocation_details) && lifecycleAccountDetailModal.detail.allocation_details.length)
|
||||||
|
? lifecycleAccountDetailModal.detail.allocation_details
|
||||||
|
: (Array.isArray(lifecycleAccountDetailModal.allocation_details) ? lifecycleAccountDetailModal.allocation_details : []);
|
||||||
|
const fallback = [{
|
||||||
|
year_month: "",
|
||||||
|
source_amount: lifecycleAccountDetailModal.detail?.allocation_source_amount ?? lifecycleAccountDetailModal.allocation_source_amount ?? 0,
|
||||||
|
project_basis_amount: lifecycleAccountDetailModal.detail?.allocation_project_basis_amount ?? lifecycleAccountDetailModal.allocation_project_basis_amount ?? 0,
|
||||||
|
total_basis_amount: lifecycleAccountDetailModal.detail?.allocation_total_basis_amount ?? lifecycleAccountDetailModal.allocation_total_basis_amount ?? 0,
|
||||||
|
allocated_amount: lifecycleAccountDetailModal.allocation_result_amount ?? 0,
|
||||||
|
}];
|
||||||
|
const list = rows.length ? rows : fallback;
|
||||||
|
return list.map((row, idx) => {
|
||||||
|
const sourceAmount = Number(row.source_amount || 0);
|
||||||
|
const allocatedAmount = Number(row.allocated_amount || 0);
|
||||||
|
const projectBasisAmount = Number(row.display_project_basis_amount ?? row.project_basis_amount ?? 0);
|
||||||
|
const totalBasisAmount = Number(row.display_total_basis_amount ?? row.total_basis_amount ?? 0);
|
||||||
|
return (
|
||||||
|
<div key={`alloc-restore-${idx}`}>
|
||||||
|
{((row.year_month || "").slice(0, 4) || "-")}년 · {fmt(sourceAmount)}원 x ({fmt(projectBasisAmount)}원 / {fmt(totalBasisAmount)}원) = {fmt(allocatedAmount)}원
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<section className="mini-card">
|
<section className="mini-card">
|
||||||
<div style={{ fontSize: 16, fontWeight: 700 }}>프로젝트별 금액</div>
|
<div style={{ fontSize: 16, fontWeight: 700 }}>프로젝트별 금액</div>
|
||||||
<div className="subtle" style={{ marginTop: 6 }}>연결된 프로젝트들 중 이 계정에 포함된 지출입니다.</div>
|
<div className="subtle" style={{ marginTop: 6 }}>연결된 프로젝트들 중 이 계정에 포함된 지출입니다.</div>
|
||||||
@@ -6831,6 +6940,38 @@
|
|||||||
<div className="empty-state">표시할 거래내역이 없습니다.</div>
|
<div className="empty-state">표시할 거래내역이 없습니다.</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="mini-card">
|
||||||
|
<div style={{ fontSize: 16, fontWeight: 700 }}>계정별 금액</div>
|
||||||
|
<div className="subtle" style={{ marginTop: 6 }}>현재 선택한 계정 기준 금액입니다.</div>
|
||||||
|
<div style={{ display: "grid", gap: 10, marginTop: 12 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
textAlign: "left",
|
||||||
|
border: "1px solid var(--line)",
|
||||||
|
borderRadius: 14,
|
||||||
|
background: "white",
|
||||||
|
padding: "12px 14px",
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "minmax(220px, 1fr) minmax(140px, 0.4fr)",
|
||||||
|
gap: 12,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 700 }}>{lifecycleAccountDetailModal.account_name || "(계정명없음)"}</div>
|
||||||
|
<div className="subtle" style={{ marginTop: 4 }}>{lifecycleAccountDetailModal.account_code || "-"}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: "right", fontWeight: 700 }}>
|
||||||
|
{fmt(
|
||||||
|
(lifecycleAccountDetailModal.account_code || "").startsWith("SHARED_")
|
||||||
|
? Number(lifecycleAccountDetailModal.allocation_result_amount || 0)
|
||||||
|
: Number(lifecycleAccountDetailModal.detail?.summary?.expense_supply_sum || 0)
|
||||||
|
)}원
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1109,7 +1109,18 @@ def calculate_monthly_shared_distribution(
|
|||||||
|
|
||||||
base_range = project_ranges.get(base_project_code)
|
base_range = project_ranges.get(base_project_code)
|
||||||
if not base_range:
|
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(
|
pool_rows = conn.execute(
|
||||||
"""
|
"""
|
||||||
@@ -1127,6 +1138,8 @@ def calculate_monthly_shared_distribution(
|
|||||||
|
|
||||||
labor_pool_by_month: dict[str, float] = defaultdict(float)
|
labor_pool_by_month: dict[str, float] = defaultdict(float)
|
||||||
common_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:
|
for row in pool_rows:
|
||||||
ym = (row["ym"] or "").strip()
|
ym = (row["ym"] or "").strip()
|
||||||
code = (row["account_code"] or "").strip()
|
code = (row["account_code"] or "").strip()
|
||||||
@@ -1138,12 +1151,25 @@ def calculate_monthly_shared_distribution(
|
|||||||
continue
|
continue
|
||||||
if meta.get("category") == "인건비":
|
if meta.get("category") == "인건비":
|
||||||
labor_pool_by_month[ym] += amount
|
labor_pool_by_month[ym] += amount
|
||||||
|
labor_pool_accounts_by_month[ym][code] += amount
|
||||||
else:
|
else:
|
||||||
common_pool_by_month[ym] += amount
|
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()]))
|
candidate_months = sorted(set([*labor_pool_by_month.keys(), *common_pool_by_month.keys()]))
|
||||||
if not candidate_months:
|
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)
|
month_active_projects: dict[str, set[str]] = defaultdict(set)
|
||||||
for project_code, (start_ym, end_ym) in project_ranges.items():
|
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
|
base_start_ym, base_end_ym = base_range
|
||||||
labor_shared = 0.0
|
labor_shared = 0.0
|
||||||
common_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:
|
for ym in candidate_months:
|
||||||
if ym < base_start_ym:
|
if ym < base_start_ym:
|
||||||
continue
|
continue
|
||||||
@@ -1196,13 +1232,70 @@ def calculate_monthly_shared_distribution(
|
|||||||
|
|
||||||
if total_value > 0:
|
if total_value > 0:
|
||||||
ratio = max(0.0, min(1.0, base_value / total_value))
|
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:
|
else:
|
||||||
ratio = 1.0 / len(active_projects)
|
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
|
labor_pool = float(labor_pool_by_month.get(ym) or 0)
|
||||||
common_shared += float(common_pool_by_month.get(ym) or 0) * ratio
|
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(
|
def build_company_allocated_project_rows(
|
||||||
@@ -1462,6 +1555,37 @@ def build_project_lifecycle_cost(
|
|||||||
)
|
)
|
||||||
project_entry["shared_expense_supply"] += labor_shared
|
project_entry["shared_expense_supply"] += labor_shared
|
||||||
project_entry["expense_supply"] += labor_shared
|
project_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(
|
account_entry = breakdown_account_maps["인건비"].setdefault(
|
||||||
"SHARED_LABOR",
|
"SHARED_LABOR",
|
||||||
{
|
{
|
||||||
@@ -1470,10 +1594,20 @@ def build_project_lifecycle_cost(
|
|||||||
"direct_expense_supply": 0.0,
|
"direct_expense_supply": 0.0,
|
||||||
"shared_expense_supply": 0.0,
|
"shared_expense_supply": 0.0,
|
||||||
"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["shared_expense_supply"] += labor_shared
|
||||||
account_entry["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:
|
if common_shared:
|
||||||
breakdown_components["관리비"]["shared"] += common_shared
|
breakdown_components["관리비"]["shared"] += common_shared
|
||||||
@@ -1496,6 +1630,37 @@ def build_project_lifecycle_cost(
|
|||||||
)
|
)
|
||||||
project_entry["shared_expense_supply"] += common_shared
|
project_entry["shared_expense_supply"] += common_shared
|
||||||
project_entry["expense_supply"] += common_shared
|
project_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(
|
account_entry = breakdown_account_maps["관리비"].setdefault(
|
||||||
"SHARED_COMMON",
|
"SHARED_COMMON",
|
||||||
{
|
{
|
||||||
@@ -1504,10 +1669,20 @@ def build_project_lifecycle_cost(
|
|||||||
"direct_expense_supply": 0.0,
|
"direct_expense_supply": 0.0,
|
||||||
"shared_expense_supply": 0.0,
|
"shared_expense_supply": 0.0,
|
||||||
"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["shared_expense_supply"] += common_shared
|
||||||
account_entry["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 = (
|
total_expense = (
|
||||||
breakdown_components["시공비"]["total"]
|
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=""),
|
"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 {
|
return {
|
||||||
"summary": summary,
|
"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(
|
"projects": sorted(
|
||||||
project_map.values(),
|
project_map.values(),
|
||||||
key=lambda item: (-float(item.get("expense_supply_sum") or 0), item.get("project_code") or ""),
|
key=lambda item: (-float(item.get("expense_supply_sum") or 0), item.get("project_code") or ""),
|
||||||
|
|||||||
Reference in New Issue
Block a user