diff --git a/B09_Estimation/B09_Estimation_Calc_Base.py b/B09_Estimation/B09_Estimation_Calc_Base.py index 127960c1b..14748c60d 100644 --- a/B09_Estimation/B09_Estimation_Calc_Base.py +++ b/B09_Estimation/B09_Estimation_Calc_Base.py @@ -21,6 +21,10 @@ 중기사용료 = 손료(경비 · 기준가 천원 × 손료계수 ÷ 10,000) + 운전사(노무 · 인 × 시간 노임) + 연료(재료 · ℓ/hr × 적용 단가) + 잡품(재료 · 연료 금액 × % ÷ 100) 줄 = ROUNDDOWN(단가 × 수량, 1) · 비목 합 = ROUNDDOWN(Σ, 0) → 중기목록표 +노임 · 기계 단가 덮개(707-3) = 사용자 값 `labor_cover` {노무 키: 일당} · `machine_cover` + {기계 키: {labor, material, expense}}(셋 다 수일 때만) — 굳힌 값 대신 · + 줄 `covered` · `book`(굳힌 값) · + 덮은 기계 = 중기사용료 줄은 그대로 · 비목 합 · 합 = 덮개 · 그 기계 「연결 없음」 뺌 빈칸 규칙 = 값을 낸 원천이 없으면 None(0 을 채우지 않음) · 그 칸을 쓰는 합도 None · 「연결 없음」 목록(`gaps`)에 탭 · 줄 키 · 까닭을 남김(목차 탭이 이동 링크로 씀). """ @@ -39,6 +43,7 @@ SOURCES = ("관급", "물가정보", "물가자료", "유통물가", "거래가 #: 자재 줄 사용자 표시 SUPPLY = ("관급", "별도") +_PARTS = ("labor", "material", "expense") def _scaled(value: float, places: int, fn) -> float: @@ -95,10 +100,15 @@ def driver_hour(day: float | None, factor: dict[str, Any] | None) -> float | Non return rounddown(day * factor["num"] / factor["den"], 0) -def labor_sheet(labor: list[dict[str, Any]], gaps: list) -> list[dict[str, Any]]: +def labor_sheet( + labor: list[dict[str, Any]], gaps: list, cover: dict[str, Any] | None = None +) -> list[dict[str, Any]]: rows = [] for no, row in enumerate(labor, 1): price = num(row.get("price")) + mine = num((cover or {}).get(row["key"])) + extra = {} if mine is None else {"covered": True, "book": price} + price = price if mine is None else mine if price is None: gaps.append(gap("labor", row["key"], row["name"], "노임 단가 없음")) rows.append( @@ -107,11 +117,22 @@ def labor_sheet(labor: list[dict[str, Any]], gaps: list) -> list[dict[str, Any]] "code": row.get("code"), "unit": row.get("unit") or "인", "price": price, + **extra, } ) return rows +def machine_cover(cover: Any) -> dict[str, dict[str, float]]: + """기계 덮개 — 노 · 재 · 경 셋 다 수인 줄만.""" + out = {} + for key, cell in (cover if isinstance(cover, dict) else {}).items(): + got = {p: num((cell or {}).get(p)) if isinstance(cell, dict) else None for p in _PARTS} + if None not in got.values(): + out[key] = got + return out + + def _head(row: dict[str, Any], no: int) -> dict[str, Any]: return { "no": no, @@ -306,7 +327,7 @@ def basis_sheet( def compute_base(inputs: dict[str, Any], user: dict[str, Any]) -> dict[str, Any]: """144 탭 일곱 + 「연결 없음」 목록.""" gaps: list[dict[str, str]] = [] - labor_rows = labor_sheet(inputs.get("labor") or [], gaps) + labor_rows = labor_sheet(inputs.get("labor") or [], gaps, user.get("labor_cover")) expense = expense_sheet(inputs.get("machines") or [], gaps) compare = compare_sheet(inputs.get("materials") or [], user.get("materials") or {}, gaps) basis = basis_sheet(inputs, labor_rows) @@ -316,6 +337,13 @@ def compute_base(inputs: dict[str, Any], user: dict[str, Any]) -> dict[str, Any] kind: by_key[key] for kind, key in (inputs.get("fuel_keys") or {}).items() if key in by_key } blocks = [machine_block(m, drivers, fuels, gaps) for m in inputs.get("machines") or []] + covers = machine_cover(user.get("machine_cover")) + for b in blocks: + if b["key"] in covers: # 707-3 덮개 = 비목 합 · 합(줄은 그대로) + b["book"] = {p: b[p] for p in (*_PARTS, "sum")} + b.update(covers[b["key"]], covered=True) + b["sum"] = total([b[p] for p in _PARTS]) + gaps = [g for g in gaps if not (g["tab"] == "machine_blocks" and g["key"] in covers)] machine = [ { "no": no, @@ -327,6 +355,7 @@ def compute_base(inputs: dict[str, Any], user: dict[str, Any]) -> dict[str, Any] "labor": b["labor"], "material": b["material"], "expense": b["expense"], + **({"covered": True} if b.get("covered") else {}), } for no, b in enumerate(blocks, 1) ] diff --git a/B09_Estimation/B09_Estimation_Calc_Base.ts b/B09_Estimation/B09_Estimation_Calc_Base.ts index 7c133ea91..d2e77ad7d 100644 --- a/B09_Estimation/B09_Estimation_Calc_Base.ts +++ b/B09_Estimation/B09_Estimation_Calc_Base.ts @@ -12,6 +12,9 @@ * 재료비목록표 = 수량 × 적용 단가 = 총액 ROUND(·, 0) · 하나라도 빈칸이면 빈칸 · 수량 = 입력 * material_qty(183-1 · 서버 역전개 총수량 · 없는 키 빈칸) ?? 입력 줄 수량(B08 총괄 합계 열) * 중기사용료 = 손료 + 운전사 + 연료 + 잡품 · 줄 ROUNDDOWN 1 · 비목 합 ROUNDDOWN 0 → 중기목록표 + * 노임 · 기계 단가 덮개(707-3) = 사용자 값 labor_cover {노무 키: 일당} · machine_cover + * {기계 키: {labor, material, expense}}(셋 다 수일 때만) — 굳힌 값 대신 · covered · book(굳힌 값) · + * 덮은 기계 = 중기사용료 줄 그대로 · 비목 합 · 합 = 덮개 · 그 기계 「연결 없음」 뺌 * 빈칸 = 원천이 없으면 null · 그 칸을 쓰는 합도 null · 「연결 없음」 목록(gaps)에 남김 * ========================================================================== */ @@ -41,6 +44,7 @@ export const SOURCES = [ /** 자재 줄 사용자 표시 */ export const SUPPLY = ["관급", "별도"] as const; +const PARTS = ["labor", "material", "expense"] as const; function scaled(value: number, places: number, fn: (x: number) => number): number { const a = Math.abs(value); @@ -101,14 +105,28 @@ const head = (row: Row, no: number): Row => ({ source: row.source || "", }); -function laborSheet(labor: Row[], gaps: Gap[]): Row[] { +function laborSheet(labor: Row[], gaps: Gap[], cover?: Row): Row[] { return labor.map((row, i) => { - const price = num(row.price); + const book = num(row.price); + const mine = num((cover || {})[row.key]); + const price = mine === null ? book : mine; if (price === null) gaps.push(gap("labor", row.key, row.name, "노임 단가 없음")); - return { ...head(row, i + 1), code: row.code ?? null, unit: row.unit || "인", price }; + const extra = mine === null ? {} : { covered: true, book }; + return { ...head(row, i + 1), code: row.code ?? null, unit: row.unit || "인", price, ...extra }; }); } +/** 기계 덮개 — 노 · 재 · 경 셋 다 수인 줄만 */ +export function machineCover(cover: unknown): Row { + const out: Row = {}; + for (const [key, cell] of Object.entries(cover && typeof cover === "object" ? cover : {})) { + const got: Row = {}; + for (const p of PARTS) got[p] = cell && typeof cell === "object" ? num(cell[p]) : null; + if (PARTS.every((p) => got[p] !== null)) out[key] = got; + } + return out; +} + function expenseSheet(machines: Row[], gaps: Gap[]): Row[] { return machines.map((m, i) => { const price = num(m.price); @@ -276,8 +294,8 @@ function basisSheet(inputs: Row, laborRows: Row[]): Row { /** 144 탭 일곱 + 「연결 없음」 목록 */ export function computeBase(inputs: Row, user: Row): Row { - const gaps: Gap[] = []; - const laborRows = laborSheet(inputs.labor || [], gaps); + let gaps: Gap[] = []; + const laborRows = laborSheet(inputs.labor || [], gaps, user.labor_cover); const expense = expenseSheet(inputs.machines || [], gaps); const compare = compareSheet(inputs.materials || [], user.materials || {}, gaps); const basis = basisSheet(inputs, laborRows); @@ -287,6 +305,14 @@ export function computeBase(inputs: Row, user: Row): Row { for (const [kind, key] of Object.entries(inputs.fuel_keys || {})) if ((key as string) in byKey) fuels[kind] = byKey[key as string]; const blocks = (inputs.machines || []).map((m: Row) => machineBlock(m, drivers, fuels, gaps)); + const covers = machineCover(user.machine_cover); + for (const b of blocks) { + if (!(b.key in covers)) continue; // 707-3 덮개 = 비목 합 · 합(줄은 그대로) + b.book = { labor: b.labor, material: b.material, expense: b.expense, sum: b.sum }; + Object.assign(b, covers[b.key], { covered: true }); + b.sum = total(PARTS.map((p) => b[p])); + } + gaps = gaps.filter((g) => !(g.tab === "machine_blocks" && g.key in covers)); const machine = blocks.map((b: Row, i: number) => ({ no: i + 1, key: b.key, @@ -297,6 +323,7 @@ export function computeBase(inputs: Row, user: Row): Row { labor: b.labor, material: b.material, expense: b.expense, + ...(b.covered ? { covered: true } : {}), })); return { labor: laborRows, diff --git a/B09_Estimation/B09_Estimation_Store.py b/B09_Estimation/B09_Estimation_Store.py index e228e2de4..bbb0206cd 100644 --- a/B09_Estimation/B09_Estimation_Store.py +++ b/B09_Estimation/B09_Estimation_Store.py @@ -13,6 +13,9 @@ "daega_cover": {블록 cover: {줄 cid: {qty?, labor?, material?, expense?}}} (702-2 일위대가 고친 칸 덮개 — 수만 · 셈 = `B09_Estimation_Calc_Labor`) + 복사본 `K:…` 의 `@copy` {from · name · spec}(703-2 · 글만) + "labor_cover": {노무 키: 일당} · "machine_cover": {기계 키: {labor, + material, expense}}(707-3 노임 · 기계 단가 덮개 · 공사 몫 · [업데이트] 가 + 책만 새로 굳혀 덮개는 남음 · 셈 = `B09_Estimation_Calc_Base`) quotes/<자재 키>/ 견적서 파일(166-4 · 여러 개) — 폴더가 목록(키의 못 쓰는 글자는 %XX) · 읽을 때 materials[키].files = 이름들 · 사용자 값으로는 저장 안 함 @@ -30,7 +33,7 @@ from pathlib import Path from typing import Any from urllib.parse import unquote -from B09_Estimation.B09_Estimation_Calc_Base import SOURCES, SUPPLY +from B09_Estimation.B09_Estimation_Calc_Base import SOURCES, SUPPLY, machine_cover from B09_Estimation.B09_Estimation_Calc_Bill import SECTIONS from B09_Estimation.B09_Estimation_Calc_Labor import COVER_KEYS @@ -281,6 +284,11 @@ def clean_user(raw: Any) -> dict[str, Any]: out = {"materials": materials, "estimate": estimate, **({"bill": bill} if bill else {})} if cover := clean_cover(raw.get("daega_cover")): out["daega_cover"] = cover + given = raw.get("labor_cover") if isinstance(raw.get("labor_cover"), dict) else {} + if labor := {str(k)[:200]: _num(v) for k, v in given.items() if _num(v) is not None}: + out["labor_cover"] = labor + if machine := machine_cover(raw.get("machine_cover")): + out["machine_cover"] = {str(k)[:200]: v for k, v in machine.items()} if clean_schedule and (schedule := clean_schedule(raw.get("schedule"))): out["schedule"] = schedule return out diff --git a/resources/tester/test_144_b09_base_calc.py b/resources/tester/test_144_b09_base_calc.py index 83df24dc8..293706474 100644 --- a/resources/tester/test_144_b09_base_calc.py +++ b/resources/tester/test_144_b09_base_calc.py @@ -165,6 +165,29 @@ def test_144_4_중기사용료_엑셀_검산(): assert why == {"연료(중유) 단가 없음", "기계 기준가 없음", "손료 기준가 · 계수 없음"} +#: 707-3 노임 · 기계 단가 덮개 — 건설기계운전사 일당 · 불도우저 시간 단가(셋 다) · 빠진 칸 = 버림 +COVER = { + "labor_cover": {"LB000048": 283297, "LB000002": 172068, "없는 키": 1}, + "machine_cover": {"X09999": {"labor": 1, "material": 2, "expense": 3}, + "X00003": {"labor": 1, "material": None, "expense": 3}}, +} # fmt: skip + + +def test_707_3_노임_기계_덮개(): + s = compute_base(INPUTS, COVER) + labor = {r["key"]: r for r in s["labor"]} + assert (labor["LB000048"]["price"], labor["LB000048"]["book"]) == (283297, 267360) + assert labor["LB000002"]["covered"] and "covered" not in labor["LB000049"] + assert "LB000002" not in {g["key"] for g in s["gaps"] if g["tab"] == "labor"} # 덮개 = 빈칸 아님 + hours = {d["key"]: d["hour"] for d in s["basis"]["drivers"]} + assert hours["1048"] == 59020 # 거창 기계경비 운전사 59,020.2 → 59,020 + got = {r["key"]: (r["sum"], r["labor"], r["material"], r["expense"]) for r in s["machine"]} + assert got["X09999"] == (6, 1, 2, 3) and got["X00003"][1] == 59020 # 반쪽 덮개 = 버림 + block = next(b for b in s["machine_blocks"] if b["key"] == "X09999") + assert block["covered"] and block["book"]["sum"] is None + assert "X09999" not in {g["key"] for g in s["gaps"] if g["tab"] == "machine_blocks"} + + def test_거울_TS_가_같은_값(): - for user in ({}, USER): + for user in ({}, USER, COVER): assert _run_ts(INPUTS, user) == compute_base(INPUTS, user)