diff --git a/B09_Estimation/B09_Estimation_Calc_Base.ts b/B09_Estimation/B09_Estimation_Calc_Base.ts index 2340b1d64..30021726a 100644 --- a/B09_Estimation/B09_Estimation_Calc_Base.ts +++ b/B09_Estimation/B09_Estimation_Calc_Base.ts @@ -41,7 +41,7 @@ export const SOURCES = [ /** 자재 줄 사용자 표시 */ export const SUPPLY = ["관급", "별도"] as const; -const PARTS = ["labor", "material", "expense"] as const; +const MACHINE_PARTS = ["labor", "material", "expense"] as const; function scaled(value: number, places: number, fn: (x: number) => number): number { const a = Math.abs(value); @@ -167,8 +167,13 @@ export function computeBase(inputs: Row, user: Row): Row { const gaps: Gap[] = []; const compare = compareSheet(inputs.materials || [], user.materials || {}, gaps); const machine = (inputs.machines || []).map((m: Row, i: number) => { - const parts: Row = Object.fromEntries(PARTS.map((p) => [p, num(m[p])])); - return { ...head(m, i + 1), unit: "시간", ...parts, sum: total(PARTS.map((p) => parts[p])) }; + const parts: Row = Object.fromEntries(MACHINE_PARTS.map((p) => [p, num(m[p])])); + return { + ...head(m, i + 1), + unit: "시간", + ...parts, + sum: total(MACHINE_PARTS.map((p) => parts[p])), + }; }); return { labor: laborSheet(inputs.labor || [], gaps), diff --git a/B09_Estimation/B09_Estimation_Calc_Labor.py b/B09_Estimation/B09_Estimation_Calc_Labor.py index 7f18fabfc..d666d4b4d 100644 --- a/B09_Estimation/B09_Estimation_Calc_Labor.py +++ b/B09_Estimation/B09_Estimation_Calc_Labor.py @@ -191,8 +191,7 @@ def _cover(line: dict[str, Any], own: dict[str, Any], qty: Any, prices: dict) -> if not keys: return qty, prices, {} extra = {"base": {"qty": qty, **prices}, "covered": keys} - qty = num(mine["qty"]) if "qty" in mine else qty - return qty, {p: num(mine[p]) if p in mine else v for p, v in prices.items()}, extra + return num(mine["qty"]), dict(prices), extra # 841-1 품만(단가 칸 = 안 읽음) def _block( diff --git a/B09_Estimation/B09_Estimation_Calc_Labor.ts b/B09_Estimation/B09_Estimation_Calc_Labor.ts index 5df398cf0..ba238cbbc 100644 --- a/B09_Estimation/B09_Estimation_Calc_Labor.ts +++ b/B09_Estimation/B09_Estimation_Calc_Labor.ts @@ -172,9 +172,7 @@ function cover(line: Row, own: Row, qty0: Num, prices0: Parts): [Num, Parts, Row const keys = COVER_KEYS.filter((k) => k in mine); if (!keys.length) return [qty0, prices0, {}]; const extra = { base: { qty: qty0, ...prices0 }, covered: keys }; - const prices = { ...prices0 }; - for (const p of PARTS) if (p in mine) prices[p] = num(mine[p]); - return ["qty" in mine ? num(mine.qty) : qty0, prices, extra]; + return [num(mine.qty), { ...prices0 }, extra]; // 841-1 품만(단가 칸 = 안 읽음 · 짝 `_cover`) } /** SK 블록 합을 M02 셈으로 — 고친 칸 · 붙인 줄 없음 · 재료 표시가 M02 자재표시 그대로일 때만 */ diff --git a/B09_Estimation/B09_Estimation_Labor_Source.py b/B09_Estimation/B09_Estimation_Labor_Source.py index 33cfbe118..953efb10e 100644 --- a/B09_Estimation/B09_Estimation_Labor_Source.py +++ b/B09_Estimation/B09_Estimation_Labor_Source.py @@ -220,7 +220,7 @@ class Builder: if isinstance(item.get("참조"), dict) and depth < refs.DEPTH: line = {**self.line_of(whole, item, qty, 1, head["name"], copies, depth), **head} elif isinstance(item.get("고정"), dict): - line = {**fixed_line(item["고정"], qty, where), **head} + line = {**fixed_line(item["고정"], qty, where, whole), **head} elif qty and ("단가" in item or item.get("값묶음")): own = {PART[k]: _f(v) / qty for k, v in (x.get("비목") or {}).items() if k in PART} line = {**head, "kind": "fixed", "qty": qty, "own": own, "link": {}} @@ -287,7 +287,7 @@ class Builder: t = _f(times) or 1.0 each = None if q is None else q / t if fixed.is_fixed(one): - return fixed_line(one[fixed.FIXED], q) + return fixed_line(one[fixed.FIXED], q, None, whole) ref = one.get("참조") if isinstance(ref, dict): doc = copies.get(refs.ref_id(ref)) @@ -526,8 +526,9 @@ def _Loose(whole: Any, read: Any) -> Any: return Loose(whole, read) -def fixed_line(fx: dict, qty: Any, where: dict | None = None) -> dict[str, Any]: - """고정 줄(705-1) → 블록 줄 `fixed` — 수량 그대로(배 1) · 이음 없는 비목 = 줄 단가 · +def fixed_line(fx: dict, qty: Any, where: dict | None = None, whole: Any = None) -> dict[str, Any]: + """고정 줄(705-1) → 블록 줄 `fixed` — 수량 그대로(배 1) · 단가 = M02 엔진 고정 줄 셈(`whole` = + 사본 단가 묶음 · 이은 노임 · 자재 = 묶음 값 · 841-1) · 없으면(붙인 줄) 줄 단가 · 비율 줄(705-3) = `pct`(밑 = 블록 그 비목 합 · 수량 = 율) · 율 기준(778-4) 묶음 = `bundle:` · 줄 목록 = `rows:` 블록 차례(`where` {열쇠: 차례} · 단가산출 블록만 · 없으면 빈 줄).""" if rate := fx.get("비율"): @@ -549,11 +550,12 @@ def fixed_line(fx: dict, qty: Any, where: dict | None = None) -> dict[str, Any]: ) return {**name, "kind": "pct", "part": PART[rate["비목"]], "base": base, "qty": _f(qty)} link = fx.get("이음") or {} + money = fixed.line(whole, fx)["기준"] if whole is not None else fx.get("단가") or {} return { "kind": "fixed", **{k: fx.get(v) or "" for k, v in (("name", "이름"), ("spec", "규격"), ("unit", "단위"))}, "qty": _f(qty), - "own": {PART[k]: _f(v) for k, v in (fx.get("단가") or {}).items() if k in PART}, + "own": {PART[k]: _f(v) for k, v in money.items() if k in PART}, "link": { "labor": link.get("노임"), "material": link.get("자재"), diff --git a/B09_Estimation/B09_Estimation_Sangeun.py b/B09_Estimation/B09_Estimation_Sangeun.py index e7ccbe6df..ee6ef1017 100644 --- a/B09_Estimation/B09_Estimation_Sangeun.py +++ b/B09_Estimation/B09_Estimation_Sangeun.py @@ -64,6 +64,7 @@ import master_sheet_line as msl # noqa: E402 — 계산서 줄 덧칸(714-1) · PARTS = ("labor", "material", "expense") COST_PART = {"노무비": "labor", "재료비": "material", "경비": "expense"} PART_COST = {v: k for k, v in COST_PART.items()} +PART_NAMES = tuple((p, k) for k, p in COST_PART.items()) # 글 계 줄 차례(노 · 재 · 경) PCT_OF = {**COST_PART, "인건비": "people"} # % 덧줄 밑수 → 덮개 다시 셈(719-3) DEPTH = 4 # 하위 산근 깊이 상한 NO_SOURCE = "조합 밖 토공 · 운반 · 사토 · 준비공은 산근 없음" @@ -358,6 +359,12 @@ class Solver: sums = ss.totals(row, lines) or block_totals(lines) # 끝전 · 조정(792-3) why = next((ln["why"] for ln in lines if ln.get("why")), None) head, foot = self._text(whole, src, times, lines) + for ln in lines: # 글 = 엔진 단가 셈 그대로 · 별도계상 재료 줄은 끝에 0 원 표시(841-1) + if "aside" in ln and ln.get("text"): + ln["text"] = [*ln["text"][:-1], f"{ln['text'][-1]} · 별도계상 = 0.0"] + if foot and any("aside" in ln for ln in lines) and sums.get("sum") is not None: + parts = " + ".join(f"{st_name} {sums[p]:,.0f}" for p, st_name in PART_NAMES) + foot = f"계 : {parts} = {sums['sum']:,.0f}" spec, spec_raw = spec_text(whole, src["given"]) return { "logic": code, @@ -420,7 +427,6 @@ class Solver: continue part = next(iter(ln["price"]), "material") ln["aside"], ln[part], ln["price"] = ln[part], 0.0, {part: 0.0} - self.prices.material[key] = 0.0 def _each(self, n: int, part: str, q: float) -> float | None: """엔진 줄 n 비목 단가 = 엔진 금액 ÷ 엔진 수량 · 안 쓰는 줄(수량 0) = 0 · 못 셈 = None.""" diff --git a/B09_Estimation/B09_Estimation_Sangeun_Cover.py b/B09_Estimation/B09_Estimation_Sangeun_Cover.py index b376861ee..c69a001e9 100644 --- a/B09_Estimation/B09_Estimation_Sangeun_Cover.py +++ b/B09_Estimation/B09_Estimation_Sangeun_Cover.py @@ -5,7 +5,8 @@ 조작 중 = 화면이 TS 짝으로 바로 셈 · [저장] = 서버가 다시 풀이(`Sangeun.build`) 뒤 이 파일로 정본. 덮개 = 사용자 값 `sg_cover` {산근 키: {줄 열쇠 「n:이름」: {qty?}}} — - qty = 1 단위 품(841-1 단가 칸 걷음 · 옛 단가 칸은 안 읽음) · 있는 칸만 셈 값 대신 · 줄이 없거나 이름이 다르면 짝 잃음 + qty = 1 단위 품(841-1 단가 칸 걷음 · 옛 단가 칸은 안 읽음) · + 있는 칸만 셈 값 대신 · 줄이 없거나 이름이 다르면 짝 잃음 (`sg_orphans` · 알림만 · 덮개는 남음) · 산근 키 = 로직 + 입력값 → [업데이트] 뒤에도 같은 블록. 고칠 칸(`fields`) = 노무 · 자재 · 중기 · 빈 줄 · 하위 산근 = 품 · % 덧줄(`of` 있는 것) = 율. @@ -92,10 +93,10 @@ def _settle(block: dict[str, Any], mine: dict[str, Any], by: dict[str, dict[str, ln.update(qty=qty, price={part: value}, **{part: _money(value, 1)}) ln["why"] = None if value is not None else "밑수 빈칸" else: - price = {**price0, **{p: num(cell[p]) for p in keys if p != "qty"}} - for p in can: - if p != "qty": - ln[p] = _money(price.get(p), qty) + price = dict(price0) # 841-1 단가 = M02 사본 값 그대로 · 품만 고침 + for p in PARTS: + if p in price: + ln[p] = _money(price[p], qty) ln.update(qty=qty, price=price) if all(ln.get(p) is not None for p in PARTS): ln["why"] = None diff --git a/B09_Estimation/B09_Estimation_Sangeun_Cover.ts b/B09_Estimation/B09_Estimation_Sangeun_Cover.ts index 8e6962431..db61f7e55 100644 --- a/B09_Estimation/B09_Estimation_Sangeun_Cover.ts +++ b/B09_Estimation/B09_Estimation_Sangeun_Cover.ts @@ -87,11 +87,8 @@ function settle(block: Row, mine: Row, by: Map): void { Object.assign(ln, { qty, price: { [part]: value }, [part]: money(value, 1) }); ln.why = value !== null ? null : "밑수 빈칸"; } else { - const price: Row = { - ...price0, - ...Object.fromEntries(keys.filter((p) => p !== "qty").map((p) => [p, num(cell[p])])), - }; - for (const p of can) if (p !== "qty") ln[p] = money(price[p] ?? null, qty); + const price: Row = { ...price0 }; // 841-1 단가 = M02 사본 값 그대로 · 품만 고침(짝 `_settle`) + for (const p of PARTS) if (p in price) ln[p] = money(price[p] ?? null, qty); Object.assign(ln, { qty, price }); if (PARTS.every((p) => (ln[p] ?? null) !== null)) ln.why = null; } diff --git a/B09_Estimation/B09_Estimation_Supply.py b/B09_Estimation/B09_Estimation_Supply.py index 0d6cf1fea..63f727217 100644 --- a/B09_Estimation/B09_Estimation_Supply.py +++ b/B09_Estimation/B09_Estimation_Supply.py @@ -66,7 +66,9 @@ def price_lists(blocks: list[dict[str, Any]], sg_blocks: list[dict[str, Any]]) - if kind not in ("labor", "machine") or not key or not ln.get("qty"): continue price = ln.get("own") or ln.get("price") or {} - head = {"key": key, "name": ln.get("name") or "", "spec": ln.get("spec") or ""} + name, spec = str(ln.get("name") or ""), str(ln.get("spec") or "") + name = name.removesuffix(spec).strip() if spec else name # 「굴착기 0.7㎥」 + 규격 겹침 + head = {"key": key, "name": name, "spec": spec} if kind == "labor" and price.get("labor") is not None: labor.setdefault(key, {**head, "unit": "인", "price": price["labor"]}) elif kind == "machine" and None not in [price.get(p) for p in PARTS]: diff --git a/resources/tester/test_144_b09_base_calc.py b/resources/tester/test_144_b09_base_calc.py index 293706474..98cab1dc0 100644 --- a/resources/tester/test_144_b09_base_calc.py +++ b/resources/tester/test_144_b09_base_calc.py @@ -1,8 +1,8 @@ """144-2~4 B09 바탕 단가 셈 — 엑셀 검산 대조 · 거울(`B09_Estimation_Calc_Base.py` ↔ `.ts`). -견본 = 기번3 공통 엑셀(`2024년 간선임도사업(기번3).xlsx`) 같은 입력: - 노무비목록표 E10~E12 · 환율및기초자료 G6~G8 · 경비목록표 E4 · E5 · E7 · E13 · E16 · E21 · - 자재단가대비표 5~12줄 · 중기사용료 1 · 2 · 4 · 11 · 14 · 16 호표(F11 126,000 …) +견본 = 기번3 공통 엑셀(`2024년 간선임도사업(기번3).xlsx`) 자재단가대비표 5~12줄 같은 입력 · +841-1 운전사 시간 노임 · 중기사용료 · 경비목록표 · 노임 · 기계 덮개 시험 걷음(B09 가 기계 시간당을 +다시 셈하던 곳 · 값 = M02 단가산출 그대로) — 노무비 · 중기목록표 = 엔진 단가 나열만 봄. """ import json @@ -23,31 +23,17 @@ from B09_Estimation.B09_Estimation_Calc_Base import ( # noqa: E402 ) IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) -FACTOR = {"num": 400, "den": 1920, "text": "1/8 × 16/12 × 25/20"} +#: 841-1 노무비목록표 · 중기목록표 입력 = 대가 · 산근 줄 엔진 단가 나열(서버 `Supply.price_lists`) LABOR = [ - {"key": "LB000048", "code": "1048", "name": "건설기계운전사", "price": 267360}, - {"key": "LB000049", "code": "1049", "name": "화물차운전사", "price": 226709}, - {"key": "LB000050", "code": "1050", "name": "일반기계운전사", "price": 161142}, - {"key": "LB000002", "code": "1002", "name": "보통인부", "price": None}, + {"key": "LB000048", "name": "건설기계운전사", "spec": "", "unit": "인", "price": 267360}, + {"key": "LB000002", "name": "보통인부", "spec": "", "unit": "인", "price": None}, ] - - -def _m(key, name, price, loss, fuel, lph, misc, crew_code): - return {"key": key, "name": name, "spec": "", "price": price, "loss": loss, "fuel": fuel, - "fuel_lph": lph, "misc_pct": misc, "crew": 1 if crew_code else None, - "crew_code": crew_code} # fmt: skip - - MACHINES = [ - _m("X00001", "불도우저19ton", 184499, 1811, "경유", 25, 16, "1048"), - _m("X00003", "굴삭기(0.2m3)", 62550, 2085, "경유", 5, 21, "1048"), - _m("X00005", "굴삭기(0.7m3)", 110926, 2085, "경유", 11.6, 22, "1048"), - _m("X00032", "덤프자동덮개", 1546, 2684, None, None, None, None), - _m("X00048", "콘크리트믹서0.45m3", 5409, 3039, "휘발유", 3.9, 2, "1050"), - _m("X00074", "덤프트럭2.5ton(토사)", 20793, 2967, "경유", 2.9, None, "1049"), - _m("X09999", "기준가 없는 기계", None, 2000, "중유", 3, 10, "1048"), -] + {"key": "EQ000020", "name": "굴착기(무한궤도) 0.7㎥", "spec": "", + "labor": 1459, "material": 1077, "expense": 1012}, + {"key": "EQ000001", "name": "빈 값", "spec": "", "labor": None, "material": 1, "expense": 1}, +] # fmt: skip def _mt(key, name, **prices): @@ -64,16 +50,7 @@ MATERIALS = [ _mt("b08:sand", "골재"), ] -INPUTS = { - "labor": LABOR, - "machines": MACHINES, - "materials": MATERIALS, - "fuel_keys": {"경유": "MO2", "휘발유": "MO1"}, - "driver_factor": FACTOR, - "fx": [ - {"key": "FX000001", "name": "USD", "unit": "원/USD", "value": 1414.9, "date": "2026-08-14"} - ], -} +INPUTS = {"labor": LABOR, "machines": MACHINES, "materials": MATERIALS} USER = {"materials": {"M9": {"applied": 950, "supply": "관급"}, "M4": {"supply": "별도"}, "b08:sand": {"applied": None}}} # fmt: skip @@ -103,20 +80,14 @@ def test_끝수_엑셀_자리(): assert rounddown(0.1 + 0.2, 1) == 0.3 # 떠도는 끝수는 버리지 않음 -def test_144_2_노무_경비_환율_기초자료(): +def test_841_1_노무_중기목록_나열_걷은_표_빈(): s = compute_base(INPUTS, {}) - assert [r["price"] for r in s["labor"]] == [267360, 226709, 161142, None] + assert [r["price"] for r in s["labor"]] == [267360, None] assert ("labor", "LB000002") in {(g["tab"], g["key"]) for g in s["gaps"]} - hours = {d["key"]: d["hour"] for d in s["basis"]["drivers"]} - assert hours == {"1048": 55700, "1050": 33571, "1049": 47231} # 기초자료 G6 · G8 · G7 - assert s["basis"]["drivers"][0]["factor"] == "1/8 × 16/12 × 25/20" - assert [r["price"] for r in s["expense"]][:3] == [ - 184499, - 62550, - 110926, - ] # 경비목록표 E4 · E5 · E7 - assert s["expense"][0]["unit"] == "천원" - assert s["basis"]["fx"][0]["value"] == 1414.9 + got = {r["key"]: (r["sum"], r["labor"], r["material"], r["expense"]) for r in s["machine"]} + assert got == {"EQ000020": (3548, 1459, 1077, 1012), "EQ000001": (None, None, 1, 1)} + assert s["expense"] == [] and s["machine_blocks"] == [] # 경비목록표 · 중기사용료 걷음 + assert s["basis"] == {"fx": [], "drivers": []} # 환율및기초자료(운전사 시간 노임) 걷음 def test_144_3_자재단가대비_최저_사용자_관급(): @@ -140,54 +111,13 @@ def test_144_3_자재단가대비_최저_사용자_관급(): assert gaps == [("compare", "b08:sand")] -def test_144_4_중기사용료_엑셀_검산(): - s = compute_base(INPUTS, {}) - got = {r["key"]: (r["sum"], r["labor"], r["material"], r["expense"]) for r in s["machine"]} - assert got["X00001"] == (126000, 55700, 36888, 33412) # 중기목록표 4줄 - assert got["X00003"] == (76436, 55700, 7695, 13041) - assert got["X00005"] == (96829, 55700, 18001, 23128) - assert got["X00032"] == (414, 0, 0, 414) # 덮개 — 운전사 · 연료 없음 - assert got["X00048"] == (40659, 33571, 5445, 1643) # 일반기계운전사 · 휘발유 · 잡유 2 % - assert got["X00074"][1:3] == (47231, 3688) # 화물차운전사 · 잡품 없음 - assert got["X09999"] == (None, 55700, None, None) # 기준가 · 중유 단가 없음 = 그 비목 · 합 빈칸 - block = s["machine_blocks"][0] - assert [ln["amount"] for ln in block["lines"]] == [ - 33412.7, - 55700, - 31800, - 5088, - ] # 중기사용료 F7~F10 - assert block["lines"][0]["qty"] == 0.1811 and block["lines"][0]["ref"] == { - "tab": "expense", - "key": "X00001", - } - why = {g["why"] for g in s["gaps"] if g["key"] == "X09999"} - 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_841_1_사급_표시_기본_끔(): + rows = [{**MATERIALS[2], "supply": "별도"}] # M02 자재표시 기본 별도계상 + s = compute_base({**INPUTS, "materials": rows}, {"materials": {"M3": {"supply": "사급"}}}) + assert s["compare"][0]["supply"] is None + assert compute_base({**INPUTS, "materials": rows}, {})["compare"][0]["supply"] == "별도" def test_거울_TS_가_같은_값(): - for user in ({}, USER, COVER): + for user in ({}, USER, {"materials": {"M3": {"supply": "사급"}}}): assert _run_ts(INPUTS, user) == compute_base(INPUTS, user) diff --git a/resources/tester/test_144_b09_skeleton.py b/resources/tester/test_144_b09_skeleton.py index 7b4409486..08be64184 100644 --- a/resources/tester/test_144_b09_skeleton.py +++ b/resources/tester/test_144_b09_skeleton.py @@ -77,22 +77,21 @@ def test_사용자_값_다듬기_저장_자리(tmp_path): assert store.read_book(tmp_path) is None -def test_단가_굳히기_운전사_연료_따라옴(): - book = prices.refresh_book(["EQ000004"]) # 불도저(무한궤도) 19ton · 조종원 1048 · 경유 +def test_841_1_단가_책_자재만(): + """841-1 — 노무 · 기계 · 운전사 · 연료 · 환율은 책 밖(B09 금액 = 사본 M02 엔진 값) · 자재만 굳힘.""" + book = prices.refresh_book(["EQ000004", "LB000048", "MT000401", "MO000002"]) rows = book["rows"] - assert set(rows) == {"EQ000004", "LB000048", "MO000002"} - assert rows["EQ000004"]["loss"] == 1811 and rows["EQ000004"]["crew_code"] == "1048" - assert rows["LB000048"]["code"] == "1048" and rows["MO000002"]["prices"]["유가"] > 0 - assert book["driver_factor"] == {"num": 400, "den": 1920, "text": "1/8 × 16/12 × 25/20"} + assert set(rows) == {"MT000401", "MO000002"} and "driver_factor" not in book + assert rows["MO000002"]["prices"]["유가"] > 0 # [저장] = 책에 없는 새 줄만 더함 — 있는 줄 값은 안 바꿈 - stale = {**book, "rows": {**rows, "LB000048": {**rows["LB000048"], "price": 1}}} - grown = prices.extend_book(stale, ["EQ000004", "LB000002"]) - assert grown["rows"]["LB000048"]["price"] == 1 and "LB000002" in grown["rows"] + stale = {**book, "rows": {**rows, "MT000401": {**rows["MT000401"], "prices": {}}}} + grown = prices.extend_book(stale, ["MT000401", "MT000338"]) + assert grown["rows"]["MT000401"]["prices"] == {} and "MT000338" in grown["rows"] # 책 밖 줄 = 지금 M01 값 + fresh 표시 - inputs = prices.build_inputs(stale, ["EQ000004", "LB000002"]) - fresh = {row["key"]: row.get("fresh", False) for row in inputs["labor"]} - assert fresh == {"LB000002": True, "LB000048": False} - assert inputs["fuel_keys"] == {"경유": "MO000002"} + inputs = prices.build_inputs(stale, ["MT000401", "MT000338", "LB000002"]) + fresh = {row["key"]: row.get("fresh", False) for row in inputs["materials"]} + assert fresh == {"MT000401": False, "MT000338": True} + assert set(inputs) == {"materials", "missing", "now"} def test_연결_없으면_빈_표와_연결_없음(tmp_path, monkeypatch): diff --git a/resources/tester/test_145_1_b09_sangeun.py b/resources/tester/test_145_1_b09_sangeun.py index 61ce40b1a..772417bbd 100644 --- a/resources/tester/test_145_1_b09_sangeun.py +++ b/resources/tester/test_145_1_b09_sangeun.py @@ -2,8 +2,10 @@ - 산근 = 155-4 갈래(사용자 뜻 · 145 정함 뒤집음) — 일위대가 로직 줄 중 단가산출 줄(승격 아님) + 사전조합 안 단가산출 줄 · 일위대가화(승격 · 옛 SP) 줄 자체는 산근 아님(대가 블록) -- 줄 금액 = ROUNDDOWN(B09 굳힌 단가 × 1 단위 수량, 1) · 총계 = 비목마다 ROUNDDOWN 0 — 엑셀 - 단가산출근거 제 6호표(뒷채움) 줄 값 · 총계 8,328(6,878 · 538 · 912)과 같음 +- 줄 단가 = M02 엔진 줄 금액 ÷ 수량(841-1 · 사본 단가 묶음 · B09 단가 책 안 읽음) · 줄 금액 = + ROUNDDOWN(단가 × 1 단위 수량, 1) · 총계 = 비목마다 ROUNDDOWN 0 — 엑셀 단가산출근거 제 6호표(뒷채움) + 줄 값 · 총계 8,328(6,878 · 538 · 912)과 같음 · 산근 합 = M02 엔진 기준 +- 관급 · 별도계상 재료(supply) = 산근 안 0원 · `aside` 에 금액(841-1) - 같은 로직 + 같은 입력값 = 산근 하나(쓰는 구조물이 늘어남) · 입력이 다르면 둘 · 번호 = 첫 등장 - 사본 없으면 빈 표 + 「연결 없음」 · 조합 밖 토공 · 운반 산근 안내 한 줄 · 작업량 Q = ROUND 2 """ @@ -13,19 +15,14 @@ import json import sys from pathlib import Path -import pytest - ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402 from B09_Estimation import B09_Estimation_Router as router_mod # noqa: E402 from B09_Estimation import B09_Estimation_Sangeun as sg # noqa: E402 -from B09_Estimation import B09_Estimation_Sangeun_Sum as ss # noqa: E402 -from B09_Estimation.B09_Estimation_Calc_Base import compute_base # noqa: E402 -from B09_Estimation.B09_Estimation_Linked import used_keys # noqa: E402 from M02_MasterTemplete import M02_Item_Prices as item_prices # noqa: E402 +from M02_MasterTemplete import M02_Labor_Doc as labor_doc # noqa: E402 STONE = "GF000219" # 돌쌓기 메쌓기(인력) — 석공 0.195 · 보통인부 0.156 / ㎡ NET = "GC000337" # 낙석방지망 — 노무 · 크레인(중기) · 자재 셋 @@ -50,26 +47,24 @@ def _snapshot(folder: Path, rows, values=None, copies=None) -> Path: return folder -def _sheets(*folders: Path) -> dict: - keys = [k for f in folders for k in used_keys(f)] - return compute_base(prices.build_inputs(None, list(dict.fromkeys(keys))), {}) +def _engine(folder: Path, code: str, given=None) -> dict: + """M02 엔진 기준(사본 단가 묶음 · 날 값 줄 · 끝수 뒤 기준).""" + whole = item_prices.load_prices(json.loads((folder / "단가.json").read_text(encoding="utf-8"))) + return labor_doc.run_line(whole, code, given or {}) -def test_단가산출_줄만_산근_블록_단가는_B09_표(tmp_path): +def test_단가산출_줄만_산근_블록_단가는_M02_엔진(tmp_path): a = _snapshot(tmp_path / "a", [{"로직": STONE}, {"로직": NET, "승격": True}], {NET: NET_IN}) - sheets = _sheets(a) - got = sg.collect([("s1", a)], sheets) + got = sg.collect([("s1", a)]) (block,) = got["sg"] assert (block["no"], block["logic"], block["unit"]) == (1, STONE, "㎡") - labor = {r["key"]: r["price"] for r in sheets["labor"]} lines = block["lines"] assert [(ln["kind"], ln["ref"]["key"], ln["qty"]) for ln in lines] == [ ("labor", "LB000033", 0.195), ("labor", "LB000002", 0.156), ] - want = [sg.rounddown(labor[ln["ref"]["key"]] * ln["qty"], 1) for ln in lines] - assert [ln["labor"] for ln in lines] == want - assert block["labor"] == sg.rounddown(sum(want), 0) and block["sum"] == block["labor"] + want = _engine(a, STONE)["기준"] + assert block["labor"] == float(want["노무비"]) and block["sum"] == float(want["계"]) assert block["material"] == 0 and block["expense"] == 0 and block["why"] is None assert got["sg_list"][0]["key"] == block["key"] and got["sg_gaps"] == [] assert sg.price_of(got, block["key"]) == { @@ -77,20 +72,19 @@ def test_단가산출_줄만_산근_블록_단가는_B09_표(tmp_path): } # fmt: skip -def test_중기는_중기목록표_노_재_경_자재는_적용_단가(tmp_path): +def test_중기_자재_단가_M02_엔진_별도계상은_0원(tmp_path): a = _snapshot(tmp_path / "a", [{"로직": NET}], {NET: NET_IN}) - sheets = _sheets(a) - (block,) = sg.collect([("s1", a)], sheets)["sg"] + (block,) = sg.collect([("s1", a)])["sg"] kinds = [ln["kind"] for ln in block["lines"]] assert kinds == ["labor", "labor", "machine", "material", "material", "material"] - crane = block["lines"][2] - row = next(r for r in sheets["machine"] if r["key"] == crane["ref"]["key"]) - for part in sg.PARTS: - assert crane[part] == sg.rounddown(row[part] * crane["qty"], 1) - net = block["lines"][3] - applied = next(r["applied"] for r in sheets["compare"] if r["key"] == net["ref"]["key"]) - assert net["material"] == sg.rounddown(applied * net["qty"], 1) - assert block["sum"] == block["labor"] + block["material"] + block["expense"] + want = _engine(a, NET, NET_IN)["기준"] + assert [block[p] for p in (*sg.PARTS, "sum")] == [ + float(want[k]) for k in ("노무비", "재료비", "경비", "계") + ] + (apart,) = sg.collect([("s1", a)], supply=lambda key: "별도")["sg"] + mats = [ln for ln in apart["lines"] if ln["kind"] == "material"] + assert all(ln["material"] == 0 and ln["aside"] for ln in mats if ln["qty"]) + assert apart["material"] < block["material"] and apart["labor"] == block["labor"] def test_엑셀_6호표_뒷채움_검산(): @@ -119,7 +113,7 @@ def test_같은_로직_같은_입력은_하나_다르면_둘_사전조합_안_ [{"로직": "", "참조": SC_REF}, {"로직": "", "참조": SP_REF}, {"로직": STONE}], copies={"참조|system||SC-000001": sc, "참조|system||SP-000001": sp}, ) - got = sg.collect([("s1", a), ("s2", b), ("s3", c), ("s4", d)], _sheets(a, c, d)) + got = sg.collect([("s1", a), ("s2", b), ("s3", c), ("s4", d)]) blocks = got["sg"] assert [(x["no"], x["logic"], x["uses"]) for x in blocks] == [ ( @@ -135,11 +129,11 @@ def test_같은_로직_같은_입력은_하나_다르면_둘_사전조합_안_ def test_사본_없으면_빈_표_연결_없음(tmp_path, monkeypatch): - got = sg.collect([("s1", tmp_path / "없음")], {}) + got = sg.collect([("s1", tmp_path / "없음")]) assert got["sg"] == [] and got["sg_list"] == [] and got["sg_gaps"] == [] assert got["sg_notes"] == [sg.NO_SOURCE, sg.EMPTY] plain = _snapshot(tmp_path / "p", [{"로직": STONE, "승격": True}]) # 일위대가화만 = 빈 표 - assert sg.collect([("s1", plain)], {})["sg"] == [] + assert sg.collect([("s1", plain)])["sg"] == [] async def fake_materials(_project_id): return {} @@ -148,42 +142,3 @@ def test_사본_없으면_빈_표_연결_없음(tmp_path, monkeypatch): sheets = asyncio.run(router_mod.payload("p", tmp_path))["sheets"] assert sheets["sg"] == [] and sheets["sg_list"] == [] assert sg.EMPTY in sheets["sg_notes"] and sg.NO_SOURCE in sheets["sg_notes"] - - -def test_단가_없으면_빈칸과_연결_없음(tmp_path): - a = _snapshot(tmp_path / "a", [{"로직": STONE}]) - got = sg.collect([("s1", a)], {"labor": [], "machine": [], "compare": []}) - (block,) = got["sg"] - assert block["labor"] is None and block["sum"] is None - assert "노무비목록표에 없음" in block["why"] - assert [(g["tab"], g["key"]) for g in got["sg_gaps"]] == [("sg", block["key"])] - - -def test_작업량_Q는_소수_둘(): - import M01_MasterData.M01_MasterData_Store as m01 - - whole = m01.loaded()[1] - for code, row in sorted((whole.index.get("GF") or {}).items()): - if not any(ss._Q.fullmatch(s["이름"]) for s in row.get("중간", [])): - continue - try: - env = sg._env(whole, row, {}) - except Exception: # noqa: BLE001 — 입력이 필요한 로직은 건너뜀 - continue - q = next(env[s["이름"]] for s in row["중간"] if ss._Q.fullmatch(s["이름"])) - if hasattr(q, "as_tuple"): - assert q == round(q, 2), code - return - pytest.skip("자동값으로 도는 작업량 Q 로직 없음") - - -def test_화면_탭_둘_서버_산근_그대로(): - shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8") - builders = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Builders.ts").read_text("utf-8") - assert "sg_list: buildSgList," in builders and "sg: buildSg," in builders # 303-2 표 짜기 - assert 'const SERVER_ONLY = ["sg", "sg_list", "sg_gaps", "sg_notes"] as const;' in shell - assert "gaps: [...base.gaps, ...(server.sg_gaps || [])" in shell # 뒤에 대가(145-2) gaps 이음 - tabs = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Sangeun.ts").read_text("utf-8") - assert 'link: (i) => (rows[i] ? { tab: "sg", key: String(rows[i].key) } : null),' in tabs - # 155-5 — 중기 = 노 · 재 · 경 세 줄(서버 글 줄 수와 같게 · 값 0 줄도 둠) - assert 'const parts = ln.kind === "machine" && !ln.why ? PARTS : [null];' in tabs diff --git a/resources/tester/test_145_2_b09_labor_calc.py b/resources/tester/test_145_2_b09_labor_calc.py index 20e07bddc..c9ec97f1a 100644 --- a/resources/tester/test_145_2_b09_labor_calc.py +++ b/resources/tester/test_145_2_b09_labor_calc.py @@ -99,6 +99,24 @@ EXCEL = { } # fmt: skip +def _own(daega): + """841-1 줄 단가 = M02 엔진 단가(`own` · Labor_Source 가 얹음) — 견본은 엑셀 목록표 값을 얹음.""" + book = {k: {r["key"]: r for r in SHEETS[k]} for k in ("labor", "material", "machine")} + for block in daega["blocks"]: + for ln in block["lines"]: + row = book.get(ln["kind"], {}).get(ln.get("key")) + if ln["kind"] == "labor" and row: + ln["own"] = {"labor": row["price"]} + elif ln["kind"] == "material" and row: + ln["own"] = {"material": row["price"]} + elif ln["kind"] == "machine" and row: + ln["own"] = {p: row[p] for p in ("labor", "material", "expense")} + return daega + + +_own(EXCEL) + + def _block(out, bid): return next(b for b in out["daega_blocks"] if b["id"] == bid) @@ -141,6 +159,7 @@ def test_엑셀_대가_블록_검산(): def _blank_cases(): broken = json.loads(json.dumps(EXCEL)) broken["blocks"][2]["lines"][1]["key"] = "L09999" # 노임 없음 → 그 블록 · 부른 블록 빈칸 아님 + broken["blocks"][2]["lines"][1].pop("own") broken["blocks"].append(_b("DGX", "조합", "순환", [_l("daega", "DGY", 1)])) broken["blocks"].append(_b("DGY", "조합", "순환2", [_l("daega", "DGX", 1)])) broken["blocks"].append(_b("DGZ", "품셈", "수량 없음", [_l("labor", "L00017", None)])) @@ -162,7 +181,7 @@ def test_빈칸_규칙(): "no": "대가1", "key": "DG14", "labor": None, "material": None, "expense": None, "sum": None, } # fmt: skip whys = {g["why"] for g in out["gaps"]} - assert {"노임 단가 없음", "산근 단가 없음(145-1)", "수량 없음", "% 밑수 없음"} <= whys + assert {"노임 단가 없음(M02 엔진)", "산근 단가 없음(145-1)", "수량 없음", "% 밑수 없음"} <= whys def _ts(daega, sheets, sangeun, cover=None): @@ -327,7 +346,7 @@ def test_견본_사본_끝까지_셈(project): inputs = prices.build_inputs(None, keys) inputs["daega"] = daega_inputs(project) base = compute_base(inputs, {}) - sg_rows = {r["key"]: r for r in sangeun.build(project, base)["sg_list"]} + sg_rows = {r["key"]: r for r in sangeun.build(project)["sg_list"]} out = compute_labor(inputs["daega"], base, sg_rows) owner = inputs["daega"]["owners"]["pipe@100.0"] combo = _block(out, owner) diff --git a/resources/tester/test_155_5_sangeun_text.py b/resources/tester/test_155_5_sangeun_text.py index e24c8438f..659abbc33 100644 --- a/resources/tester/test_155_5_sangeun_text.py +++ b/resources/tester/test_155_5_sangeun_text.py @@ -4,7 +4,8 @@ 「계 : 노무비 6,878 + 재료비 538 + 경비 912 = 8,328」(줄 ROUNDDOWN 1 · 계 ROUNDDOWN 0) - 엑셀 7호표 막자갈채집: 작업량 Q = ROUND 2 중간 줄 · 「55,700 * 1 / 21.59 * 2 = 5,159.7」 - % 덧줄 = 「공구손료 (노무비의 %) : 6,878.8 * 3 % = 206.3」(규격 = 149-4 `pct_extras`) -- 단가 없음 = 「단가 없음」 글 · 계 그 비목 빈칸 · 글 속 금액 = 산근 풀이(`Sangeun.Solver`) 줄 값 +- 단가 없음 = 「단가 없음」 글 · 계 그 비목 빈칸 · 글 속 금액 = 산근 풀이(`Sangeun.Solver` · 841-1 M02 엔진 + 단가) 줄 값 """ import copy @@ -86,31 +87,12 @@ def test_배_켜쌓기율_꼬리_단가_없음(): assert got[-1] == "계 : 노무비 빈칸 + 재료비 48 + 경비 82 = 빈칸" -def _solver_prices(whole, code, given): - """산근 풀이와 같은 단가 — 키마다 같은 수(노무 · 자재 1,000 · 중기 엑셀 값).""" - sheets = {"labor": [], "machine": [], "compare": []} - asked = [] - - def price_of(key): - asked.append(key) - return EXCEL_EQ if key.startswith("EQ") else 1000.0 - - st.render_sangeun_text(whole, code, given, price_of) - for key in dict.fromkeys(asked): - if key.startswith("EQ"): - sheets["machine"].append({"key": key, **EXCEL_EQ}) - elif key.startswith("LB"): - sheets["labor"].append({"key": key, "price": 1000.0}) - else: - sheets["compare"].append({"key": key, "applied": 1000.0}) - return sheets, price_of - - def test_글_속_금액은_산근_풀이_값과_같음(): for code, given in ((BASE, GIVEN), ("GF000288", {})): # 중기 · 노무 · % 덧줄 · 배 1,000 whole = item_prices.load_prices(item_prices.to_json(item_prices.collect([code]))) - sheets, price_of = _solver_prices(whole, code, given) - block = sg.Solver(sheets).add(whole, {"code": code, "given": given}, "s1") + solver = sg.Solver() # 841-1 단가 = M02 엔진 · 글 단가 = 풀이가 쓴 그 단가 + block = solver.add(whole, {"code": code, "given": given}, "s1") + price_of = solver.price_of # 결과단위 배(「원/1,000㎡」) — 산근 풀이와 같은 1 단위 times = sg._unit(whole.logic(code))[1] got = st.render_sangeun_text(whole, code, given, price_of, times=times) diff --git a/resources/tester/test_155_b09_sources.py b/resources/tester/test_155_b09_sources.py index 449d9c637..8e8465286 100644 --- a/resources/tester/test_155_b09_sources.py +++ b/resources/tester/test_155_b09_sources.py @@ -18,10 +18,10 @@ import test_145_2_b09_labor_calc as t145 # noqa: E402 from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402 from B09_Estimation import B09_Estimation_Sangeun as sg # noqa: E402 -from B09_Estimation.B09_Estimation_Calc_Base import compute_base, rounddown # noqa: E402 +from B09_Estimation.B09_Estimation_Calc_Base import rounddown # noqa: E402 from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor # noqa: E402 from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs # noqa: E402 -from B09_Estimation.B09_Estimation_Linked import linked_resource_keys, used_keys # noqa: E402 +from B09_Estimation.B09_Estimation_Linked import linked_resource_keys # noqa: E402 from B09_Estimation.B09_Estimation_Router import compute_sheets # noqa: E402 from M02_MasterTemplete import M02_Item_Prices as item_prices # noqa: E402 @@ -89,13 +89,12 @@ def test_155_4_산근_자재_자리표시_키_대비표로(tmp_path): } for name, data in files.items(): (folder / name).write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") - sheets = compute_base(prices.build_inputs(None, used_keys(folder)), {}) - (block,) = sg.collect([("s1", folder)], sheets)["sg"] + (block,) = sg.collect([("s1", folder)])["sg"] # 841-1 단가 = M02 엔진(사본 단가 묶음) fuel = [ln for ln in block["lines"] if ln["kind"] == "material"] assert fuel and all(ln["ref"]["key"].startswith("MO0") for ln in fuel) - applied = {r["key"]: r["applied"] for r in sheets["compare"]} for ln in fuel: - assert ln["material"] == rounddown(applied[ln["ref"]["key"]] * ln["qty"], 1) + price = ln["price"]["material"] + assert price and ln["material"] == rounddown(price * ln["qty"], 1) def test_155_3_비고_산근_n호표_거울(): diff --git a/resources/tester/test_157_1_sangeun_steps.py b/resources/tester/test_157_1_sangeun_steps.py index e70170b79..96c4591e5 100644 --- a/resources/tester/test_157_1_sangeun_steps.py +++ b/resources/tester/test_157_1_sangeun_steps.py @@ -91,22 +91,8 @@ def test_만약_걸린_갈래_입력_설명_기계_이름(): def _solver(whole, code, given): - sheets = {"labor": [], "machine": [], "compare": []} - asked = [] - - def price_of(key): - asked.append(key) - return EQ if key.startswith("EQ") else None if key.startswith("SG:") else 1000.0 - - st.render_sangeun_text(whole, code, given, price_of) - for key in dict.fromkeys(k for k in asked if not k.startswith("SG:")): - if key.startswith("EQ"): - sheets["machine"].append({"key": key, **EQ}) - elif key.startswith("LB"): - sheets["labor"].append({"key": key, "price": 1000.0}) - else: - sheets["compare"].append({"key": key, "applied": 1000.0}) - return sg.Solver(sheets).add(whole, {"code": code, "given": given}, "s1") + """841-1 산근 풀이 단가 = M02 엔진(사본 단가 묶음) — 시험 단가 안 넣음.""" + return sg.Solver().add(whole, {"code": code, "given": given}, "s1") def test_호표_줄_계는_산근_풀이와_같음(): diff --git a/resources/tester/test_166_2_b09_bill_hand.py b/resources/tester/test_166_2_b09_bill_hand.py index af32829b0..983b62914 100644 --- a/resources/tester/test_166_2_b09_bill_hand.py +++ b/resources/tester/test_166_2_b09_bill_hand.py @@ -18,11 +18,8 @@ ROOT = Path(__file__).resolve().parents[2] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402 from B09_Estimation import B09_Estimation_Sangeun as sg # noqa: E402 -from B09_Estimation.B09_Estimation_Calc_Base import compute_base # noqa: E402 from B09_Estimation.B09_Estimation_Calc_Bill import WHY_LOGIC, compute_estimation # noqa: E402 -from B09_Estimation.B09_Estimation_Linked import used_keys # noqa: E402 from B09_Estimation.B09_Estimation_Store import clean_user # noqa: E402 from B09_Estimation.B09_Estimation_Summary import compute_summary, hopyo_lines # noqa: E402 from M02_MasterTemplete import M02_Item_Prices as item_prices # noqa: E402 @@ -173,9 +170,7 @@ def test_새_산근_사본_뒤_번호_호표_탭_집계표(tmp_path): user = {"bill": {"rows": [{**HAND[2], "qty": 10}, HAND[3]]}} hand_keys = sg.hand_resource_keys(user) assert {"LB000033", "LB000002"} <= set(hand_keys) # 석공 · 보통인부 → 단가 책 수요 - keys = list(dict.fromkeys([*used_keys(a), *hand_keys])) - base = compute_base(prices.build_inputs(None, keys), {}) - got = sg.collect([("s1", a)], base, sg.hand_sources(user)) + got = sg.collect([("s1", a)], sg.hand_sources(user)) # 841-1 단가 = M02 엔진 listed = [(r["no"], r["logic"], r["uses"]) for r in got["sg_list"]] assert listed == [(1, NET, ["s1", "수기 줄 새 산근"]), (2, STONE, ["수기 줄 메쌓기"])] assert got["sg_hand"] == { diff --git a/resources/tester/test_166_6_b09_daega_blank.py b/resources/tester/test_166_6_b09_daega_blank.py index bb79f5925..8bee1d194 100644 --- a/resources/tester/test_166_6_b09_daega_blank.py +++ b/resources/tester/test_166_6_b09_daega_blank.py @@ -24,6 +24,10 @@ SHEETS = { } +#: 841-1 줄 단가 = M02 엔진(`own`) · 자재 단가 없음 = own None +LABOR = {"labor": 165545} + + def _b(bid, name, lines): return { "id": bid, @@ -39,14 +43,14 @@ def _b(bid, name, lines): DAEGA = { "blocks": [ _b("A", "기슭막이", [ - {"kind": "labor", "key": "L1", "qty": 0.5, "name": "보통인부"}, - {"kind": "material", "key": "M1", "qty": 2, "name": "토목용부직포"}, + {"kind": "labor", "key": "L1", "qty": 0.5, "name": "보통인부", "own": LABOR}, + {"kind": "material", "key": "M1", "qty": 2, "name": "토목용부직포", "own": None}, ]), _b("B", "겹 대가", [ - {"kind": "labor", "key": "L1", "qty": 0.1, "name": "보통인부"}, + {"kind": "labor", "key": "L1", "qty": 0.1, "name": "보통인부", "own": LABOR}, {"kind": "daega", "key": "A", "qty": 1, "name": "기슭막이"}, ]), - _b("C", "아는 줄만", [{"kind": "labor", "key": "L1", "qty": 1, "name": "보통인부"}]), + _b("C", "아는 줄만", [{"kind": "labor", "key": "L1", "qty": 1, "name": "보통인부", "own": LABOR}]), ], "owners": {"pipe@100.0": "A", "pipe@200.0": "C"}, } # fmt: skip diff --git a/resources/tester/test_201_1_table_level_merge.py b/resources/tester/test_201_1_table_level_merge.py index 413f4cb3c..b397b7230 100644 --- a/resources/tester/test_201_1_table_level_merge.py +++ b/resources/tester/test_201_1_table_level_merge.py @@ -112,8 +112,7 @@ APPLIED = [ ("B08_Quantity/B08_Quantity_Material_Tables.ts", 'mergeSame: ["name"]'), ("B08_Quantity/B08_Quantity_Material_Tables.ts", 'mergeSame: ["group"]'), ("B08_Quantity/B08_Quantity_Material_Tables.ts", "mergeSame: built.mergeSame"), - ("B09_Estimation/B09_Estimation_UI_Tabs_Base.ts", 'mergeSame: ["part"]'), - ("B09_Estimation/B09_Estimation_UI_Tabs_Base.ts", 'level: blockLevel("name"'), + # 841-1 환율및기초자료 · 중기사용료 표 걷음(「—」 한 줄) — Tabs_Base 병합 · 레벨 줄 없음 ("B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts", 'level: blockLevel("name"'), ("B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts", 'level: blockLevel("text"'), ("B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts", 'mergeSame: ["kind"]'), diff --git a/resources/tester/test_203_1_b09_data_check.py b/resources/tester/test_203_1_b09_data_check.py index a8151b084..5e09a2605 100644 --- a/resources/tester/test_203_1_b09_data_check.py +++ b/resources/tester/test_203_1_b09_data_check.py @@ -114,17 +114,14 @@ console.log(JSON.stringify({ a, b, moves, tipRefresh, tipRow, tipBlank, selector def test_배지_조건_서버_book_stale(): - keys = ["EQ000004"] # 불도저 · 조종원 · 경유(test_144 와 같은 견본) + keys = ["MT000401", "MT000338", "LB000002"] # 841-1 책 = 자재만(노임 키는 안 셈) book = json.loads(json.dumps(prices.refresh_book(keys), ensure_ascii=False)) assert prices.book_stale(book, keys) is None # 방금 굳힌 책 = 업데이트 필요 없음 assert prices.book_stale(None, keys) == "단가 아직 안 굳힘" first, *rest = list(book["rows"]) - old = {**book, "rows": {k: book["rows"][k] for k in rest}, "fx": []} - old["rows"][rest[0]] = {**old["rows"][rest[0]], "price": -1} - old["driver_factor"] = {"num": 1, "den": 1, "text": "1/1"} - assert prices.book_stale(old, keys) == ( - "새 단가 줄 1 · M01 에서 바뀐 단가 1 · 운전사 계수 바뀜 · 환율 바뀜" - ) + old = {**book, "rows": {k: book["rows"][k] for k in rest}} + old["rows"][rest[0]] = {**old["rows"][rest[0]], "prices": {}} + assert prices.book_stale(old, keys) == "새 단가 줄 1 · M01 에서 바뀐 단가 1" router = (ROOT / "B09_Estimation/B09_Estimation_Router.py").read_text("utf-8") assert '"stale": await asyncio.to_thread(prices.book_stale, book, keys),' in router diff --git a/resources/tester/test_296_b09_update_skip_deleted.py b/resources/tester/test_296_b09_update_skip_deleted.py index a1ff00df5..2e4ca4563 100644 --- a/resources/tester/test_296_b09_update_skip_deleted.py +++ b/resources/tester/test_296_b09_update_skip_deleted.py @@ -23,8 +23,6 @@ from B09_Estimation import B09_Estimation_Store as store_b09 # noqa: E402 from B09_Estimation.B09_Estimation_Linked import linked_resource_keys # noqa: E402 from resources.tester.test_204_2_b09_bill_library import COMBO, world # noqa: E402, F401 -GONE = "LB999999" # M01 에 없는 키(원본 지워짐) - def _json(book): return json.loads(json.dumps(book, ensure_ascii=False)) @@ -68,18 +66,19 @@ def test_지운_복사_항목_업데이트_뒤_안_살아남(world, monkeypatch) def test_M01_원본_없는_항목_값_그대로_낡음_까닭_빠짐(): - keys = ["EQ000004", GONE] # 불도저 · 조종원 · 경유(test_144 견본) + 원본 없는 키 - old = _json(prices.refresh_book(["EQ000004"])) - kept = {"key": GONE, "code": "9999", "name": "지워진 직종", "price": 123.0} - old["rows"][GONE] = kept - old["rows"]["LB000048"] = {**old["rows"]["LB000048"], "price": 1} # M01 에서 바뀐 단가 + gone = "MT999999" # 841-1 책 = 자재만 — 원본 없는 자재 키 + keys = ["MT000401", "MT000338", gone] + old = _json(prices.refresh_book(["MT000401", "MT000338"])) + kept = {"key": gone, "name": "지워진 자재", "origin": "M01", "prices": {"물가자료": 123.0}} + old["rows"][gone] = kept + old["rows"]["MT000401"] = {**old["rows"]["MT000401"], "prices": {}} # M01 에서 바뀐 단가 assert prices.book_stale(old, keys) == "M01 에서 바뀐 단가 1" # 원본 없는 줄 = 까닭에 없음 new = _json(prices.refresh_book(keys, old)) - assert new["rows"][GONE] == kept # 지우지도 바꾸지도 않음 - assert new["rows"]["LB000048"]["price"] not in (None, 1) # 나머지 = 새로 굳힘 + assert new["rows"][gone] == kept # 지우지도 바꾸지도 않음 + assert new["rows"]["MT000401"]["prices"] != {} # 나머지 = 새로 굳힘 assert set(new["rows"]) == set(old["rows"]) assert prices.book_stale(new, keys) is None - assert GONE not in prices.refresh_book(keys)["rows"] # 옛 책 없으면 줄 없음(옛 길) + assert gone not in prices.refresh_book(keys)["rows"] # 옛 책 없으면 줄 없음(옛 길) def test_화면_업데이트_캐시_보냄_그대로_둠(): diff --git a/resources/tester/test_702_b09_daega_cover.py b/resources/tester/test_702_b09_daega_cover.py index 18c34d7e9..1b92b1f66 100644 --- a/resources/tester/test_702_b09_daega_cover.py +++ b/resources/tester/test_702_b09_daega_cover.py @@ -1,5 +1,5 @@ """702-2 · 702-4 B09 일위대가 고친 칸 덮개 — 사용자 값 `daega_cover` -{블록 cover: {줄 cid: {qty · labor · material · expense}}}. +{블록 cover: {줄 cid: {qty}}}(841-1 단가 칸 걷음 · 옛 단가 칸은 안 읽고 저장 때 버림). - 덮개 열쇠 = 블록 `cover` · 줄 `cid`(`B09_Estimation_Labor_Source` · 내용 해시 밖) - 셈 = `B09_Estimation_Calc_Labor` 파이썬 · TS 짝(거울) — 고친 칸만 셈 값 대신 · `base` = 품셈 값 @@ -42,7 +42,7 @@ def _covered_excel(): return daega -#: DG9 찰붙임 석공 품 0.1 → 0.12 · DG2 % 2 → 3 · DG14 산근 SG10 노무 단가 손 · 짝 잃은 둘 +#: DG9 찰붙임 석공 품 0.1 → 0.12 · DG2 % 2 → 3 · DG14 산근 SG10 노무 단가(841-1 안 읽음) · 짝 잃은 둘 COVER = { "T:DG9": {"labor:L00005#0": {"qty": 0.12}}, "T:DG2": {"pct:공구손료#3": {"qty": 3}}, @@ -72,8 +72,7 @@ def test_고친_칸만_셈값_대신(): pct = _line(out, "DG2", 4) # % 줄 = 끝에 셈(차례 4) assert pct["qty"] == 3 and pct["base"] == {"qty": 2} and pct["covered"] == ["qty"] sg = _line(out, "DG14", 2) - assert sg["prices"]["labor"] == 20000 and sg["base"]["labor"] == 18084 - assert sg["covered"] == ["labor"] and sg["prices"]["material"] == 2498 + assert sg["prices"]["labor"] == 18084 and "covered" not in sg # 841-1 단가 칸 = 안 읽음 assert out["daega_orphans"] == [ {"cover": "T:없는블록", "cid": "labor:L00017#0"}, {"cover": "T:DG1", "cid": "labor:없는줄#9"}, @@ -81,14 +80,14 @@ def test_고친_칸만_셈값_대신(): assert plain["daega_orphans"] == [] -def test_덮개가_빈칸을_채움(): +def test_단가_칸_덮개는_빈칸을_안_채움(): + """841-1 단가 = M02 사본 값 — 단가 칸 덮개로 빈 단가를 채우지 않음(빈칸 그대로).""" daega = _covered_excel() - sheets = {**b145.SHEETS, "labor": b145.SHEETS["labor"][:2]} # 석공 노임 없음 - bare = compute_labor(daega, sheets, b145.SANGEUN) - assert _line(bare, "DG9", 0)["amount"] is None - out = compute_labor(daega, sheets, b145.SANGEUN, {"T:DG9": {"labor:L00005#0": {"labor": 1e5}}}) - assert _line(out, "DG9", 0)["amount"] == 10000.0 - assert not [g for g in out["gaps"] if "찰붙임" in g["name"]] + daega["blocks"][3]["lines"][0].pop("own") # 석공 노임 없음 + out = compute_labor( + daega, b145.SHEETS, b145.SANGEUN, {"T:DG9": {"labor:L00005#0": {"labor": 1e5}}} + ) + assert _line(out, "DG9", 0)["amount"] is None @pytest.mark.parametrize("case", ["cover", "none"]) @@ -101,23 +100,19 @@ def test_거울_파이썬_TS(case): assert b145._same(daega_price(py, "pipe@100.0"), ts["price"]) -def test_노임_새_판_덮개_칸_그대로(): - """702-4 — 바탕 단가가 새 판으로 바뀌어도 고친 칸 = 그대로 · 안 고친 칸만 새 값.""" +def test_새_단가_덮개_품_그대로(): + """702-4 — 사본 단가가 새로 바뀌어도([단가 갱신]) 고친 품 = 그대로 · 단가는 새 값.""" daega = _covered_excel() - cover = {"T:DG9": {"labor:L00005#0": {"qty": 0.12}, "labor:L00017#1": {"labor": 170000}}} + cover = {"T:DG9": {"labor:L00005#0": {"qty": 0.12}}} old = compute_labor(daega, b145.SHEETS, b145.SANGEUN, cover) - new_sheets = { - **b145.SHEETS, - "labor": [{**r, "price": round(r["price"] * 1.05)} for r in b145.SHEETS["labor"]], - } - new = compute_labor(daega, new_sheets, b145.SANGEUN, cover) + for ln in daega["blocks"][3]["lines"]: + if ln["kind"] == "labor": + ln["own"] = {"labor": round(ln["own"]["labor"] * 1.05)} + new = compute_labor(daega, b145.SHEETS, b145.SANGEUN, cover) a, b = _line(old, "DG9", 0), _line(new, "DG9", 0) assert a["qty"] == b["qty"] == 0.12 # 고친 품 그대로 - assert b["prices"]["labor"] == round(258935 * 1.05) != a["prices"]["labor"] # 안 고친 단가 = 새 - assert b["base"]["labor"] == b["prices"]["labor"] # 품셈 값 곁 = 새 판 값 - c, d = _line(old, "DG9", 1), _line(new, "DG9", 1) - assert c["prices"]["labor"] == d["prices"]["labor"] == 170000 # 고친 단가 그대로 - assert d["qty"] == 0.03 and d["base"]["labor"] == round(165545 * 1.05) + assert b["prices"]["labor"] == round(258935 * 1.05) != a["prices"]["labor"] # 단가 = 새 + assert b["base"]["labor"] == b["prices"]["labor"] # 품셈 값 곁 = 새 값 assert new["daega_orphans"] == [] @@ -205,17 +200,15 @@ def test_저장_다듬기(): "P:d": {"x#0": {"expense": float("inf"), "material": 3}}, } } - assert store.clean_user(raw)["daega_cover"] == { - "S:a": {"labor:L1#0": {"qty": 0.5}}, - "P:d": {"x#0": {"material": 3.0}}, - } + assert store.clean_user(raw)["daega_cover"] == {"S:a": {"labor:L1#0": {"qty": 0.5}}} assert "daega_cover" not in store.clean_user({"daega_cover": {"S:a": {}}}) def test_저장_뒤_서버_값(tmp_path): """[저장](write_user → read_user) 뒤 서버 셈 = 화면 짝 셈(덮개 입력값만 받고 다시 셈).""" stored = store.write_user(tmp_path, {"daega_cover": COVER}) - assert store.read_user(tmp_path)["daega_cover"] == stored["daega_cover"] == COVER + kept = {k: v for k, v in COVER.items() if k != "T:DG14"} # 단가 칸만 든 덮개 = 버림(841-1) + assert store.read_user(tmp_path)["daega_cover"] == stored["daega_cover"] == kept daega = _covered_excel() server = compute_labor(daega, b145.SHEETS, b145.SANGEUN, stored["daega_cover"]) screen = b145._ts(daega, b145.SHEETS, b145.SANGEUN, COVER)["out"] diff --git a/resources/tester/test_703_3_b09_refresh.py b/resources/tester/test_703_3_b09_refresh.py index cd6bb5bc9..0f4e59c05 100644 --- a/resources/tester/test_703_3_b09_refresh.py +++ b/resources/tester/test_703_3_b09_refresh.py @@ -16,7 +16,8 @@ from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402 from B09_Estimation import B09_Estimation_Refresh as refresh # noqa: E402 from B09_Estimation import B09_Estimation_Store as store # noqa: E402 -KEYS = ["LB000001", "LB000033"] # 작업반장(건설업) · 석공 +#: 841-1 책 = 자재만(노임 키 = 노임 판만 셈) — 원목 · 미송각재 + 작업반장(건설업) +KEYS = ["MT000401", "MT000338", "LB000001"] def test_책_견줌_미리보기_수_적용_수_같음(tmp_path, monkeypatch): @@ -24,8 +25,8 @@ def test_책_견줌_미리보기_수_적용_수_같음(tmp_path, monkeypatch): new = prices.refresh_book(KEYS) assert new["labor_edition"] == prices.labor_editions(KEYS) and new["labor_edition"] old = json.loads(json.dumps(new)) - old["rows"]["LB000001"] = {**old["rows"]["LB000001"], "price": 1} # 옛 판 값 - del old["rows"]["LB000033"] # 새 줄 + old["rows"]["MT000401"] = {**old["rows"]["MT000401"], "prices": {}} # 옛 값 + del old["rows"]["MT000338"] # 새 줄 old["labor_edition"] = {"건설업": "2025-09-01"} cover = { "S:a": {"x#0": {"qty": 1, "labor": 2}}, @@ -40,12 +41,7 @@ def test_책_견줌_미리보기_수_적용_수_같음(tmp_path, monkeypatch): assert got["노임판"]["후"].startswith("건설업 2026") old.pop("labor_edition") assert refresh.preview(tmp_path, old, KEYS, None)["노임판"]["전"].startswith("굳힌 날 ") - assert prices.book_diff(after, prices.refresh_book(KEYS, after)) == { - "new": 0, - "changed": 0, - "factor": False, - "fx": False, - } + assert prices.book_diff(after, prices.refresh_book(KEYS, after)) == {"new": 0, "changed": 0} assert prices.book_stale(after, KEYS) is None diff --git a/resources/tester/test_705_1_fixed_line.py b/resources/tester/test_705_1_fixed_line.py index 2748949b1..d52cbf7d9 100644 --- a/resources/tester/test_705_1_fixed_line.py +++ b/resources/tester/test_705_1_fixed_line.py @@ -6,7 +6,6 @@ 견본 = `test_145_2_b09_labor_calc`(엑셀 바탕 표 · 사본 꼴) · `test_m02_labor_calc_mirror`(TS 헬퍼). """ -import copy import importlib.util import json import sys @@ -24,10 +23,10 @@ from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs # noqa: E40 from B09_Estimation.B09_Estimation_Linked import linked_resource_keys # noqa: E402 from B09_Estimation.B09_Estimation_Summary import compute_summary, hopyo_lines # noqa: E402 from M02_MasterTemplete import M02_Item_Prices as item_prices # noqa: E402 +from M02_MasterTemplete import M02_Item_Snapshot as snapshots # noqa: E402 from M02_MasterTemplete import M02_Labor_Doc as labor # noqa: E402 from M02_MasterTemplete import M02_Labor_Fixed as fixed # noqa: E402 from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402 -from M02_MasterTemplete import M02_Item_Snapshot as snapshots # noqa: E402 def _load(name): @@ -144,7 +143,7 @@ def _fixed_blocks(): """조합 블록 — 노무 줄 + 고정 줄 넷(이은 노임 · 이은 관급 자재 · 이은 중기 · 손 단가).""" own = {"labor": 100.0, "material": 200.0, "expense": 300.0} lines = [ - {"kind": "labor", "key": "L00017", "name": "보통인부", "qty": 0.1}, + {"kind": "labor", "key": "L00017", "name": "보통인부", "qty": 0.1, "own": {"labor": 165545}}, {"kind": "fixed", "name": "석공", "qty": 0.2, "own": own, "link": {"labor": "L00005", "material": None, "machine": None}}, {"kind": "fixed", "name": "파형강관", "qty": 1, "own": own, @@ -166,11 +165,13 @@ def _line(out, name): return next(ln for ln in out["daega_blocks"][0]["lines"] if ln["name"] == name) -def test_B09_고정_줄_단가_이음과_손_단가(): +def test_B09_고정_줄_단가_M02_셈_그대로(): + """841-1 고정 줄 단가 = M02 엔진 고정 줄 셈(`own` · 이은 노임 · 자재 = 사본 묶음 값) — B09 단가 책 + 값으로 다시 안 덮음 · 이은 자재가 관급 · 별도계상이면 대가 안 0원.""" out = compute_labor(_fixed_blocks(), b145.SHEETS, {}) - assert _line(out, "석공")["prices"] == {"labor": 258935, "material": 200, "expense": 300} + assert _line(out, "석공")["prices"] == {"labor": 100, "material": 200, "expense": 300} assert _line(out, "파형강관")["prices"]["material"] == 0 # 관급 = 대가 안 0원 - assert _line(out, "굴착기")["prices"] == {"labor": 47231, "material": 7784, "expense": 9994} + assert _line(out, "굴착기")["prices"] == {"labor": 100, "material": 200, "expense": 300} misc = _line(out, "잡재료") assert misc["prices"] == {"labor": 0, "material": 1234.5, "expense": 0} assert misc["amounts"]["material"] == 2469.0 @@ -179,25 +180,13 @@ def test_B09_고정_줄_단가_이음과_손_단가(): assert daega_price(out, "pipe@100.0")["sum"] == block["sum"] is not None -def test_B09_노임_새_판_이은_단가만_따라감(): - old = compute_labor(_fixed_blocks(), b145.SHEETS, {}) - sheets = copy.deepcopy(b145.SHEETS) - for row in sheets["labor"]: - row["price"] = round(row["price"] * 1.05) - new = compute_labor(_fixed_blocks(), sheets, {}) - assert _line(new, "석공")["prices"]["labor"] == round(258935 * 1.05) - assert _line(new, "석공")["qty"] == _line(old, "석공")["qty"] == 0.2 # 품 그대로 - for name in ("파형강관", "굴착기", "잡재료"): - assert _line(new, name)["prices"] == _line(old, name)["prices"] - - -def test_B09_덮개도_고정_줄에_먹음(): - cover = {"S:f": {"fixed:잡재료#0": {"qty": 3, "material": 1000}}} +def test_B09_덮개는_품만_고정_줄에_먹음(): + cover = {"S:f": {"fixed:잡재료#0": {"qty": 3, "material": 1000}}} # 단가 칸 = 841-1 안 읽음 out = compute_labor(_fixed_blocks(), b145.SHEETS, {}, cover) misc = _line(out, "잡재료") - assert misc["qty"] == 3 and misc["prices"]["material"] == 1000 + assert misc["qty"] == 3 and misc["prices"]["material"] == 1234.5 assert misc["base"] == {"qty": 2, "labor": 0, "material": 1234.5, "expense": 0} - assert out["daega_orphans"] == [] + assert misc["covered"] == ["qty"] and out["daega_orphans"] == [] @pytest.mark.parametrize("cover", [None, {"S:f": {"fixed:석공#0": {"labor": 1}}}]) @@ -252,8 +241,9 @@ def test_한_바퀴_사본_고정_줄_원가_빈칸_0_집계표(tmp_path): kinds = [ln["kind"] for ln in combo["lines"]] assert kinds == ["daega", "fixed", "fixed"] and combo["blank"] == 0 stone = combo["lines"][1] - book = {r["key"]: r for r in sheets["labor"]} - assert stone["prices"]["labor"] == book[LB]["price"] is not None + bundle = next(snapshots.structures_dir(root).rglob("단가.json")).read_text(encoding="utf-8") + whole = item_prices.load_prices(json.loads(bundle)) + assert stone["prices"]["labor"] == float(whole.get(LB)["값"]) # M02 고정 줄 셈 = 묶음 노임 assert sheets["bill"]["totals"]["direct"]["blank"] == 0 # 원가계산서 빈 줄 0 assert sheets["bill"]["sums"]["material"] > 0 summary = compute_summary(sheets, hopyo_lines(sheets)) @@ -262,21 +252,17 @@ def test_한_바퀴_사본_고정_줄_원가_빈칸_0_집계표(tmp_path): assert summary["fixed"] == 1 # 손 단가 줄 = 집계에서 빠짐 1 -def test_사본_재적용_업데이트_고정_줄_그대로(tmp_path): - """[업데이트](703-3) = 단가 책만 새로 — 고정 줄 품 · 손 단가 그대로 · 이은 단가만 새 판.""" +def test_사본_다시_읽어도_고정_줄_그대로(tmp_path): + """[재생성] 꼴 = 사본 다시 읽음 — 고정 줄 품 · 손 단가 · 이은 단가(M02 셈) 그대로.""" root = b145._project(tmp_path / "upd", [(100, "SS000901")]) lines = [ {"고정": _fx("석공", {"노무비": 1}, {"노임": LB}), "수량": {"손": 0.5}}, {"고정": _fx("잡재료", {"재료비": 1000}, unit="식"), "수량": {"손": 1}}, ] _snap(root, "pipe@100.0", "SS000901", lines) - daega = daega_inputs(root) - book = prices.build_inputs(None, linked_resource_keys(root)) - rows = [{**r, "price": 100} if r["key"] == LB else r for r in book["labor"]] - first = compute_labor(daega, {**b145.SHEETS, "labor": rows}, {}) - again = daega_inputs(root) # 사본 다시 읽음(재적용 뒤 꼴 = 같은 원본) - second = compute_labor(again, {**b145.SHEETS, "labor": [{**r, "price": 200} for r in rows]}, {}) + first = compute_labor(daega_inputs(root), b145.SHEETS, {}) + second = compute_labor(daega_inputs(root), b145.SHEETS, {}) a, b = first["daega_blocks"][0]["lines"], second["daega_blocks"][0]["lines"] assert [ln["qty"] for ln in a] == [ln["qty"] for ln in b] == [0.5, 1] - assert (a[0]["prices"]["labor"], b[0]["prices"]["labor"]) == (100, 200) + assert a[0]["prices"] == b[0]["prices"] and a[0]["prices"]["labor"] > 1 # 묶음 노임 assert a[1]["prices"] == b[1]["prices"] == {"labor": 0, "material": 1000, "expense": 0} diff --git a/resources/tester/test_707_6_stc.py b/resources/tester/test_707_6_stc.py index a27ed41d7..9815dbf8d 100644 --- a/resources/tester/test_707_6_stc.py +++ b/resources/tester/test_707_6_stc.py @@ -121,7 +121,7 @@ def test_엑셀과_줄마다_같음(data, name): def test_단가표_엑셀과_같음(data, name): mine = st.read_prices("a.STC", data[name]) excel = ip.read_prices(EXCEL[name].name, EXCEL[name].read_bytes()) - for kind in ("labor", "material", "machine"): + for kind in ip.KINDS: # 841-1 엑셀 단가표 = 자재만 keys = ("name", "spec", "unit", "price", "labor", "material", "expense") rows = [sorted(tuple(r.get(k) for k in keys) for r in got[kind]) for got in (mine, excel)] assert rows[0] == rows[1], kind diff --git a/resources/tester/test_707_7_whole_import.py b/resources/tester/test_707_7_whole_import.py index 64801b107..fe19a7352 100644 --- a/resources/tester/test_707_7_whole_import.py +++ b/resources/tester/test_707_7_whole_import.py @@ -35,7 +35,8 @@ FILES = { #: 영월 = 내역 줄이 코드로만 가리킴(번 채움) · STmate 하위 호표 반올림 끝수 28 줄 #: (줄당 1 원/단위 · 브레인 정함 = 그래도 이음) WANT = { - "거창": (14, 68, 56, 12, 11, 8, 8, 384916227, 0, 0), + # 841-1 이은 안내판 줄 = M02 사본 셈(엑셀보다 50,000 적음 · 노임 · 기계 덮개 걷음) + "거창": (14, 68, 56, 12, 11, 8, 8, 384916227, 0, 50000), "대흥": (34, 171, 143, 28, 0, 26, 45, 710359136, 0, 0), "소광": (25, 117, 98, 19, 0, 0, 0, 173311386, 0, 0), "영월": (23, 138, 135, 3, 0, 21, 34, 767148700, 28, 2951), @@ -107,7 +108,8 @@ def test_실무_엑셀_통째_빠짐_0_합계_같음_층_쓰기_0(world, name): assert step["빠짐"] == 0 and step["실패"] == [] and step["사본"] == copies assert got["알림"]["이음"] == billed assert got["합계"]["엑셀직접"] == direct and got["합계"]["우리직접"] == direct - gap - assert [x["까닭"] for x in got["어긋남"]] == ["끝수"] * tail + ours = "이음 단가 = 우리 셈" # 841-1 이은 줄 = M02 사본 값(엑셀 값으로 안 덮음) + assert [x["까닭"] for x in got["어긋남"] if x["까닭"] != ours] == ["끝수"] * tail libs = _libs(got["root"]) assert len(libs) == copies assert all(re.fullmatch(rf"precombo@project@-@{got['file_key']}-\d+", x) for x in libs) @@ -143,7 +145,7 @@ def test_다시_부르기_같은_사본_두_번_셈_0(world): first = world["run"]("거창") again = world["run"]("거창", fresh=False) assert _libs(again["root"]) == _libs(first["root"]) - assert again["합계"]["우리직접"] == first["합계"]["우리직접"] == 384916227 + assert again["합계"]["우리직접"] == first["합계"]["우리직접"] == 384866227 rows = again["user"]["bill"]["rows"] assert len({r["key"] for r in rows}) == len(rows) @@ -153,7 +155,7 @@ def test_원본_파일_일위대가_요율_까닭(world, name): got = world["run"](name) assert "같은 공사 엑셀에서" in got["단계"]["일위대가"]["까닭"] assert "같은 공사 엑셀에서" in got["단계"]["요율"]["까닭"] - assert "labor" in got["단계"]["단가표"] + assert "material" in got["단계"]["단가표"] # 841-1 자재만 assert got["합계"]["우리직접"] == got["합계"]["엑셀직접"] assert _libs(got["root"]) == [] diff --git a/resources/tester/test_707_b09_import_rates_prices.py b/resources/tester/test_707_b09_import_rates_prices.py index bbf1eefe0..ce58d9836 100644 --- a/resources/tester/test_707_b09_import_rates_prices.py +++ b/resources/tester/test_707_b09_import_rates_prices.py @@ -18,7 +18,6 @@ if str(ROOT) not in sys.path: from B09_Estimation import B09_Estimation_Import_Prices as ip # noqa: E402 from B09_Estimation import B09_Estimation_Import_Rates as ir # noqa: E402 from B09_Estimation import B09_Estimation_Store as store # noqa: E402 -from B09_Estimation.B09_Estimation_Calc_Base import compute_base # noqa: E402 from B09_Estimation.B09_Estimation_Rates import rate_table # noqa: E402 K = ROOT / "resources" / "knowledge" / "original" / "실무문서" @@ -85,8 +84,8 @@ def test_707_3_단가표_읽기(): for tag in FILES: got = ip.read_prices(*_file(tag)) counts[tag] = tuple(len(got[k]) for k in ip.KINDS) - assert counts == {"거창": (17, 52, 22), "영월": (16, 55, 29), "대흥": (12, 39, 25), - "소광": (14, 35, 24)} # fmt: skip + # 841-1 자재만(노임 · 기계 단가표 걷음 — 노무 · 기계 단가 = M02 사본 값) + assert counts == {"거창": (52,), "영월": (55,), "대흥": (39,), "소광": (35,)} BASE = { @@ -147,28 +146,15 @@ def test_707_3_이음_규칙(): user = {"labor_cover": {"LB2": 1}, "materials": {"MO1": {"pick": "유가", "supply": "관급"}}} out = ip.propose(READ, BASE, user) patch = out["patch"] - assert patch["labor_cover"] == {"LB1": 258935} # 같은 값 = 옛 덮개 걷음 - assert patch["machine_cover"] == {"EQ1": {"labor": 10, "material": 10, "expense": 10}} + assert set(patch) == {"materials"} # 841-1 노임 · 기계 덮개 걷음 assert patch["materials"]["MO1"] == {"supply": "관급", "applied": 1473} # pick 뺌 assert "MT1" not in patch["materials"] # 「(관급)」 이름 다름 = 짝 없음 - assert "굴삭기(무한궤도) 0.2㎥(짝 여럿)" in out["missing"]["machine"] - assert out["counts"]["machine"] == {"read": 5, "linked": 1, "covered": 1, "missing": 4, - "book": 2} # fmt: skip - assert out["counts"]["labor"]["linked"] == 2 and out["missing"]["labor"] == ["벌 목 부"] + assert set(out["counts"]) == {"material"} and out["counts"]["material"]["linked"] == 1 -def test_707_3_덮개_새_판_뒤_남음(tmp_path): - """[업데이트] = 단가 책만 새로 굳힘 — 사용자 값의 노임 · 기계 덮개는 그대로 · 셈은 덮개.""" - cover = {"labor_cover": {"LB1": 258935, "": None}, - "machine_cover": {"EQ1": {"labor": 10, "material": 10, "expense": 10}, - "EQ2": {"labor": 1}}} # fmt: skip +def test_841_1_노임_기계_덮개_저장_안_함(tmp_path): + """841-1 노임 · 기계 단가 덮개 걷음 — 옛 사용자 값은 저장 때 버림(노무 · 기계 = M02 사본 값).""" + cover = {"labor_cover": {"LB1": 258935}, + "machine_cover": {"EQ1": {"labor": 10, "material": 10, "expense": 10}}} # fmt: skip saved = store.write_user(tmp_path, cover) - assert saved["labor_cover"] == {"LB1": 258935} # 빈 값 버림 - assert saved["machine_cover"] == {"EQ1": {"labor": 10, "material": 10, "expense": 10}} - for price in (268908, 280000): # 옛 판 → 새 판 책 - store.write_book(tmp_path, {"rows": {"LB1": {"key": "LB1", "price": price}}}) - user = store.read_user(tmp_path) - assert user["labor_cover"] == saved["labor_cover"] - inputs = {"labor": [{"key": "LB1", "name": "석공", "price": price}]} - row = compute_base(inputs, user)["labor"][0] - assert (row["price"], row["book"], row["covered"]) == (258935, price, True) + assert "labor_cover" not in saved and "machine_cover" not in saved diff --git a/resources/tester/test_719_2_sk_project.py b/resources/tester/test_719_2_sk_project.py index c0f99cc86..741cc81ee 100644 --- a/resources/tester/test_719_2_sk_project.py +++ b/resources/tester/test_719_2_sk_project.py @@ -237,9 +237,9 @@ def test_내_자료집_SK_왕복_같은_금액(world, tmp_path): doc = made["문서"] assert doc["종류"] == "단가산출" and doc["바탕"] == {"로직": PCT} assert doc["덮개"]["labor:LB000036#0"] == {"qty": 0.9} - assert doc["덮개"]["labor:LB000002#0"] == {"labor": 210000} + assert "labor:LB000002#0" not in doc["덮개"] # 841-1 단가 칸 = 안 읽음 · 안 담음 assert doc["더한줄"][0]["요소"] == "LB000002" - assert made["담음"] == {"품": 1, "단가": 1} and made["안담음"] == {"품": 0, "단가": 0} + assert made["담음"]["품"] == 1 and made["담음"]["단가"] == 0 third = block["lines"][2]["cid"] # 더한줄 인력 = 그 더한줄 수량 cover = {block["cover"]: {first: {"qty": 0.9}, third: {"qty": 0.3}}} doc = personal.build(root, block["cover"], cover, "내 SK 2")["문서"] diff --git a/resources/tester/test_719_3_sangeun_cover.py b/resources/tester/test_719_3_sangeun_cover.py index f233b0507..997d7c4ce 100644 --- a/resources/tester/test_719_3_sangeun_cover.py +++ b/resources/tester/test_719_3_sangeun_cover.py @@ -24,7 +24,7 @@ from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402 from B09_Estimation import B09_Estimation_Router as router_mod # noqa: E402 from B09_Estimation import B09_Estimation_Sangeun as sangeun # noqa: E402 from B09_Estimation import B09_Estimation_Store as store # noqa: E402 -from B09_Estimation.B09_Estimation_Calc_Base import compute_base, rounddown # noqa: E402 +from B09_Estimation.B09_Estimation_Calc_Base import rounddown # noqa: E402 from B09_Estimation.B09_Estimation_Sangeun_Cover import cover_sangeun, line_id # noqa: E402 IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) @@ -47,8 +47,7 @@ def _inputs(): @pytest.fixture(scope="module") def sg(): - sheets = compute_base(_inputs(), {}) - return sangeun.collect([], sheets, sangeun.hand_sources(_user())) + return sangeun.collect([], sangeun.hand_sources(_user())) # 841-1 단가 = M02 엔진 def _block(got, code): @@ -89,7 +88,7 @@ def _same(a, b): def _cover(sg): - """품 · 단가 · 하위 산근 · 율 · 중기 단가 고침 한 벌 + 짝 잃은 칸 둘.""" + """품 · 하위 산근 · 율 고침 한 벌(841-1 단가 칸은 안 읽음 — 함께 넣어 버려짐을 봄) + 짝 잃은 칸 둘.""" sub = _block(sg, "GF000155") n_l, labor = _at(sub, "labor") people = _block(sg, "GC001018") @@ -98,15 +97,12 @@ def _cover(sg): net = _block(sg, "GC000337") n_m, machine = _at(net, "machine") n_t, mat = _at(net, "material") - parent = _block(sg, "GF000169") - n_r, remicon = _at(parent, "material") # 단가 빈칸(대비표에 없음) → 덮개로 채움 return { - parent["key"]: {line_id(n_r, remicon): {"material": 90000}}, sub["key"]: {line_id(n_l, labor): {"qty": labor["qty"] * 2}}, people["key"]: {line_id(n_p, first): {"labor": 300000}, line_id(n_x, _extra): {"qty": 5}}, net["key"]: { - line_id(n_m, machine): {"expense": 20000}, - line_id(n_t, mat): {"material": 4500}, + line_id(n_m, machine): {"qty": machine["qty"] * 1.5, "expense": 20000}, + line_id(n_t, mat): {"qty": mat["qty"] + 1, "material": 4500}, }, "SG:없는블록": {"0:x": {"qty": 1}}, net["key"] + "x": {"0:다른이름": {"qty": 1}}, @@ -122,27 +118,23 @@ def test_고침_없음_그대로(sg): ] -def test_품_단가_고침_줄_금액_총계(sg): +def test_품_고침_줄_금액_총계_단가_칸_안_읽음(sg): before = copy.deepcopy(sg) got = cover_sangeun(sg, _cover(sg)) assert sg == before # 받은 답 안 고침 net0, net = _block(sg, "GC000337"), _block(got, "GC000337") n, m = _at(net, "machine") - assert ( - m["covered"] == ["expense"] - and m["base"]["expense"] == _at(net0, "machine")[1]["price"]["expense"] - ) - assert ( - m["expense"] == rounddown(20000 * m["qty"], 1) - and m["labor"] == _at(net0, "machine")[1]["labor"] - ) - _n, t = _at(net, "material") - assert t["material"] == rounddown(4500 * t["qty"], 1) + m0 = _at(net0, "machine")[1] + assert m["covered"] == ["qty"] and m["base"] == {"qty": m0["qty"]} + assert m["price"] == m0["price"] # 단가 칸(경비 20,000) = 안 읽음 + assert m["expense"] == rounddown(m0["price"]["expense"] * m["qty"], 1) for part in ("labor", "material", "expense"): assert net[part] == rounddown(sum(ln[part] for ln in net["lines"]), 0) assert net["sum"] == net["labor"] + net["material"] + net["expense"] != net0["sum"] row = next(r for r in got["sg_list"] if r["key"] == net["key"]) assert row["sum"] == net["sum"] and row["covered"] == 2 + people = _block(got, "GC001018") + assert "covered" not in _at(people, "labor")[1] # 단가만 든 칸 = 고침 없음 untouched = _block(got, "GF000103") assert untouched == _block(sg, "GF000103") # 덮개 안 닿은 블록 = 서버 값 @@ -154,8 +146,6 @@ def test_하위_산근_이어짐_덧줄_비례(sg): assert line["price"]["labor"] == sub["labor"] != _block(sg, "GF000155")["labor"] assert line["labor"] == rounddown(sub["labor"] * line["qty"], 1) assert parent["labor"] > _block(sg, "GF000169")["labor"] - assert _block(sg, "GF000169")["sum"] is None and parent["sum"] is not None # 빈칸 채움 - assert parent["why"] is None and parent["key"] not in {g["key"] for g in got["sg_gaps"]} people0, people = _block(sg, "GC001018"), _block(got, "GC001018") n, extra = _at(people, "extra") assert extra["of"] == "people" and extra["qty"] == 5 @@ -184,16 +174,21 @@ def test_거울_파이썬_TS(sg): def test_저장_다듬기_남음(sg): cover = _cover(sg) clean = store.clean_user({"sg_cover": cover})["sg_cover"] - assert clean == cover + want = { + k: {line: {"qty": c["qty"]} for line, c in lines.items() if "qty" in c} + for k, lines in cover.items() + } + assert clean == {k: v for k, v in want.items() if v} # 841-1 품만 남음 def test_덮개_대가_원가계산서_업데이트_뒤_지킴(sg, tmp_path): inputs = _inputs() - plain = router_mod.compute_sheets(tmp_path, copy.deepcopy(inputs), _user()) net = _block(sg, "GC000337") n, mat = _at(net, "material") + mine = {"materials": {mat["ref"]["key"]: {"supply": "사급"}}} # 841-1 기본 별도계상 끔 + plain = router_mod.compute_sheets(tmp_path, copy.deepcopy(inputs), {**_user(), **mine}) cover = {net["key"]: {line_id(n, mat): {"qty": mat["qty"] + 1}}} - covered = router_mod.compute_sheets(tmp_path, copy.deepcopy(inputs), _user(cover)) + covered = router_mod.compute_sheets(tmp_path, copy.deepcopy(inputs), {**_user(cover), **mine}) gap = rounddown(rounddown(mat["price"]["material"] * (mat["qty"] + 1), 1) - mat["material"], 0) row0 = next(r for r in plain["sg_list"] if r["key"] == net["key"]) row1 = next(r for r in covered["sg_list"] if r["key"] == net["key"]) @@ -210,7 +205,7 @@ def test_덮개_대가_원가계산서_업데이트_뒤_지킴(sg, tmp_path): for k in ("price", "applied", "lowest"): if isinstance(row.get(k), (int, float)): row[k] = row[k] * 2 - again = router_mod.compute_sheets(tmp_path, newer, _user(cover)) + again = router_mod.compute_sheets(tmp_path, newer, {**_user(cover), **mine}) line = _block(again, "GC000337")["lines"][n] assert line["covered"] == ["qty"] and line["qty"] == mat["qty"] + 1 assert line["base"]["qty"] == mat["qty"] and again["sg_orphans"] == [] diff --git a/resources/tester/test_719_4_sangeun_sheet.py b/resources/tester/test_719_4_sangeun_sheet.py index 540aefc77..fd2028dee 100644 --- a/resources/tester/test_719_4_sangeun_sheet.py +++ b/resources/tester/test_719_4_sangeun_sheet.py @@ -31,7 +31,7 @@ IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) @pytest.fixture(scope="module") def made(): sheets = compute_base(_inputs(), {}) - return sheets, sangeun.collect([], sheets, sangeun.hand_sources(_user())) + return sheets, sangeun.collect([], sangeun.hand_sources(_user())) # 841-1 엔진 단가 def _ts(sg, machines): @@ -91,8 +91,9 @@ def test_화면_묶음_옮김_금액_그대로(made): assert set(bundles) == {"인력", "재료", "경비"} equip = next(x for x in bundles["경비"]["줄"] if "몫" in x) assert len(equip["몫"]) == 3 # 장비 한 줄 = 노 · 재 · 경 - unfold = equip["몫"][0]["줄"]["펼침"] - assert unfold["단위"] == "시간" and len(unfold["줄"]) == 4 # 손료 · 운전사 · 연료 · 잡품 + assert not equip["몫"][0]["줄"].get( + "펼침" + ) # 841-1 중기사용료(손료 · 운전사 …) 걷음 = 펼침 없음 parent = next(b for b in sg["sg"] if b["logic"] == "GF000169") sub = {x["묶음"]: x for x in got[parent["key"]]["bundles"]}["하위"] assert all("산근" in x["이름"] and "호표" in x["이름"] for x in sub["줄"]) diff --git a/resources/tester/test_723_4_b09_sc_qty.py b/resources/tester/test_723_4_b09_sc_qty.py index f335b4e46..645e07171 100644 --- a/resources/tester/test_723_4_b09_sc_qty.py +++ b/resources/tester/test_723_4_b09_sc_qty.py @@ -44,7 +44,8 @@ def _doc(hand=None, mat=None): "이름": "시험 SC", "단위": "㎡", "담은로직": [{**_fix("갑", 1000), **q(hand)}, {**_fix("을", 2000), **q(hand)}], - "자재": [{"코드": MAT, "수량": {"손": mat} if mat is not None else None}], + # 기준단가 = M02 엔진 자재 단가 자리(841-1 · 견본 whole 은 이름만이라 줄에 얹음) + "자재": [{"코드": MAT, "수량": {"손": mat} if mat is not None else None, "기준단가": 500}], } diff --git a/resources/tester/test_778_4_rate_line.py b/resources/tester/test_778_4_rate_line.py index 7eae698d2..53addfdbc 100644 --- a/resources/tester/test_778_4_rate_line.py +++ b/resources/tester/test_778_4_rate_line.py @@ -268,11 +268,13 @@ BLOCK = { "id": "DGR", "cover": "S:r", "kind": "조합", "name": "율 블록", "spec": "", "unit": "㎥", "cite": "", "origin": {}, "lines": [ - {"kind": "labor", "key": "L1", "name": "보통인부", "qty": 0.5}, + # 841-1 줄 단가 = M02 엔진(`own`) · 고정 줄 = M02 고정 줄 셈(이은 노임 100,000 그대로) + {"kind": "labor", "key": "L1", "name": "보통인부", "qty": 0.5, "own": {"labor": 172068}}, {"kind": "fixed", "name": "특별인부", "qty": 0.2, "own": {"labor": 100000.0}, "link": {"labor": "L9", "material": None, "machine": None}}, - {"kind": "machine", "key": "E1", "name": "굴착기", "qty": 0.29}, - {"kind": "material", "key": "M1", "name": "시멘트", "qty": 3}, + {"kind": "machine", "key": "E1", "name": "굴착기", "qty": 0.29, + "own": {"labor": 30000, "material": 5000, "expense": 70000}}, + {"kind": "material", "key": "M1", "name": "시멘트", "qty": 3, "own": {"material": 5000}}, {"kind": "pct", "name": "공구손료", "part": "expense", "base": "bundle:labor", "qty": 2}, {"kind": "pct", "name": "율 위 율", "part": "material", "base": "rows:3,4", "qty": 10}, {"kind": "pct", "name": "장비 율", "part": "expense", "base": "bundle:machine", "qty": 1}, diff --git a/resources/tester/test_792_3_engine_paths.py b/resources/tester/test_792_3_engine_paths.py index 87e946344..c06e7ac3b 100644 --- a/resources/tester/test_792_3_engine_paths.py +++ b/resources/tester/test_792_3_engine_paths.py @@ -2,7 +2,7 @@ """792-3 · 792-4 엔진 새 의미 다섯 길 지킴 시험 — 같은 로직 JSON 이 ① 엔진 직접(`master_cut.engine`) · ② M02 단가산출 SK 셈(`M02_Sangeun_Calc.run`) · ③ 일위대가 SC 한 줄(수량 1 · `M02_Labor_Doc.calc`) · -④ B09 산근 풀이(`B09_Estimation_Sangeun.Solver` · 단가 = 같은 값의 B09 목록표) · +④ B09 산근 풀이(`B09_Estimation_Sangeun.Solver` · 841-1 단가 = M02 엔진) · ⑤ B09 일위대가 대가 블록(SC → 굳힌 SK 참조 수량 1 · `Labor_Source.Builder` → `Calc_Labor` 파이썬 · TS 짝 · 내역 단가 `daega_price`)에서 같은 금액. 실무 견본(봉화 D00102 · 북부청임도 18)은 793-2 길(가져온 식 SK · 임시 층)로 만들어 ⑤ = 엑셀. @@ -103,18 +103,7 @@ def _b09_master(master, row): def _b09(build, row) -> dict: master, row, _ = build() master = _b09_master(master, row) - files = master.index - machine = next((x["값묶음"] for x in row["호표"] if "값묶음" in x), None) - sheets = { - "labor": [{"key": k, "price": float(r["값"])} for k, r in files.get("LB", {}).items()], - "compare": [ - {"key": k, "applied": float(r["물가자료"])} for k, r in files.get("MT", {}).items() - ], - "machine": [{"key": EQ, **{p: float(machine[c]) for p, c in PART.items() if c in machine}}] - if machine - else [], - } - block = sg.Solver(sheets).add(master, {"code": _key(row), "given": {}}, None) + block = sg.Solver().add(master, {"code": _key(row), "given": {}}, None) # 841-1 엔진 단가 assert block["why"] is None, block["why"] texts = [x for ln in block["lines"] for x in ln.get("text") or []] assert not [x for x in texts if "못" in x or "빈칸" in x], texts # 산근 글도 같은 셈 diff --git a/resources/tester/test_795_4_b09_copy.py b/resources/tester/test_795_4_b09_copy.py index 6fc05597a..320b4c76c 100644 --- a/resources/tester/test_795_4_b09_copy.py +++ b/resources/tester/test_795_4_b09_copy.py @@ -29,8 +29,6 @@ from resources.tester.test_795_1_sk_copy import _changed, _fake, _live from resources.tester.test_795_2_sc_copy import LB, PCT, _double, _labor from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층 -MAN = "LB000002" - def _json(value): return json.loads(json.dumps(value, ensure_ascii=False, default=str)) @@ -92,29 +90,37 @@ def test_copy_rows_order_and_sk_origin(): assert item_prices.collect(["GF000219"], over=[]) == item_prices.collect(["GF000219"]) +#: 841-1 단가 책 = 자재만 — 원목 · 미송각재 +WOOD, BAR = "MT000401", "MT000338" + + def _row(key, price): row = copy.deepcopy(prices._row(_live(), key)) - row["값"] = price + row["물가자료"] = price return item_prices.to_json(row) +def _price(book_row): + return book_row["prices"]["물가자료"] + + def test_book_new_rows_use_copy_basis(): """③ [저장] 새 줄 = 사본 기준 시점 값 · 없는 자원 = 지금 값 + 표지 · 옛 책 줄 그대로.""" - frozen = {LB: _row(LB, 111)} - first = _json(prices.extend_book(None, [LB], frozen)) - assert first["rows"][LB]["price"] == 111 and first["now_keys"] == [] - both = _json(prices.extend_book(first, [LB, MAN], frozen)) - assert both["rows"][LB] == first["rows"][LB] - assert both["rows"][MAN]["price"] == float(prices._row(_live(), MAN)["값"]) - assert both["now_keys"] == [MAN] - inputs = prices.build_inputs(None, [LB, MAN], None, frozen) - assert [r["price"] for r in inputs["labor"]] == [111, both["rows"][MAN]["price"]] - assert inputs["now"] == [MAN] and prices.build_inputs(both, [LB, MAN])["now"] == [MAN] - old = _json(prices.refresh_book([LB, MAN])) # 옛 책(사본 규칙 전) = 줄 · 값 그대로 - again = _json(prices.extend_book(old, [LB, MAN], frozen)) + frozen = {WOOD: _row(WOOD, 111)} + first = _json(prices.extend_book(None, [WOOD], frozen)) + assert _price(first["rows"][WOOD]) == 111 and first["now_keys"] == [] + both = _json(prices.extend_book(first, [WOOD, BAR], frozen)) + assert both["rows"][WOOD] == first["rows"][WOOD] + assert _price(both["rows"][BAR]) == float(prices._row(_live(), BAR)["물가자료"]) + assert both["now_keys"] == [BAR] + inputs = prices.build_inputs(None, [WOOD, BAR], None, frozen) + assert [_price(r) for r in inputs["materials"]] == [111, _price(both["rows"][BAR])] + assert inputs["now"] == [BAR] and prices.build_inputs(both, [WOOD, BAR])["now"] == [BAR] + old = _json(prices.refresh_book([WOOD, BAR])) # 옛 책(사본 규칙 전) = 줄 · 값 그대로 + again = _json(prices.extend_book(old, [WOOD, BAR], frozen)) assert again["rows"] == old["rows"] and again["now_keys"] == [] - fresh = _json(prices.refresh_book([LB, MAN], both)) # [단가 갱신] = 지금 값 · 표지 없음 - assert fresh["rows"][LB]["price"] != 111 and fresh["now_keys"] == [] + fresh = _json(prices.refresh_book([WOOD, BAR], both)) # [단가 갱신] = 지금 값 · 표지 없음 + assert _price(fresh["rows"][WOOD]) != 111 and fresh["now_keys"] == [] def test_frozen_rows_from_linked_copy(tmp_path): @@ -165,15 +171,16 @@ def test_price_refresh_default_regen_separate(monkeypatch, tmp_path): def test_price_preview_shape(): - """⑥ 미리 보기 — 바뀐 자원(전 · 후) · 총괄 전 → 후 · 사본 말 없음.""" - old = _json(prices.refresh_book([LB, MAN])) - old["rows"][LB] = {**old["rows"][LB], "price": 1} - new = _json(prices.refresh_book([LB, MAN], old)) + """⑥ 미리 보기 — 바뀐 자원(사본 자원 줄 먼저 · 자재 단가 책) · 총괄 전 → 후.""" + old = _json(prices.refresh_book([WOOD, BAR])) + old["rows"][WOOD] = {**old["rows"][WOOD], "prices": {"물가자료": 1}} + new = _json(prices.refresh_book([WOOD, BAR], old)) sheets = lambda g: {"total": {"subtotals": {"grand": g}}} # noqa: E731 - got = refresh.price_preview(old, new, sheets(10), sheets(20), 3) + copies = [{"키": LB, "이름": "사본 노임", "전": 1, "후": 2}] + got = refresh.price_preview(old, new, sheets(10), sheets(20), 3, copies) assert got["할일"] == "단가갱신" and (got["전"], got["후"], got["고친칸"]) == (10, 20, 3) - assert [(r["키"], r["전"]) for r in got["바뀐자원"]] == [(LB, 1)] - assert got["바뀐자원"][0]["후"] == new["rows"][LB]["price"] + assert [(r["키"], r["전"]) for r in got["바뀐자원"]] == [(LB, 1), (WOOD, {"물가자료": 1})] + assert got["바뀐자원"][1]["후"] == new["rows"][WOOD]["prices"] def test_migrate_view_b09_copy(tmp_path): diff --git a/resources/tester/test_b09_save_speed.py b/resources/tester/test_b09_save_speed.py index 801112b7f..18081394b 100644 --- a/resources/tester/test_b09_save_speed.py +++ b/resources/tester/test_b09_save_speed.py @@ -1,6 +1,6 @@ """B09 [저장] 속도 — 같은 결과 범위 안 다시 셈 줄임 지킴. -- 산근 두 번 셈(183-1) = 1차가 본 단가(있음 · 값)가 같을 때만 1차 풀이 그대로(`_Prices.same`) +- 산근 두 번 셈(183-1) = 1차 풀이 그대로(841-1 산근 단가 = M02 엔진 · 바탕 표 안 읽음 → 늘 같음) - M01 `steady()` 범위 = 파일 자국 한 번 · `stale` 뒤 다시 셈 · 범위 밖은 늘 셈 - B08 `once()` 범위 = 채집석 공제가 자재 집계표 답을 다시 씀(셈 한 번) """ @@ -15,35 +15,15 @@ ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from B08_Quantity import B08_Quantity_Router_Material as material # noqa: E402 -from B09_Estimation.B09_Estimation_Sangeun import _Prices # noqa: E402 +from B09_Estimation import B09_Estimation_Sangeun as sg # noqa: E402 from M01_MasterData import M01_MasterData_Store_Cache as cache # noqa: E402 -SHEETS = { - "labor": [{"key": "L1", "price": 100}, {"key": "L2", "price": 5}], - "compare": [{"key": "M1", "applied": 3}], - "machine": [{"key": "E1", "labor": 1, "material": 2, "expense": 3}], -} - -def _seen() -> _Prices: - got = _Prices(SHEETS) - got.labor.get("L1") - assert "M9" not in got.material - got.machine.get("E1") - return got - - -def test_산근_같은_단가면_다시_씀() -> None: - other = {**SHEETS, "labor": [{"key": "L1", "price": 100}, {"key": "L2", "price": 999}]} - assert _seen().same(_Prices({**other, "compare": []})) # 안 본 L2 · M1 은 안 셈 - - -def test_산근_본_단가_바뀌면_다시_셈() -> None: - assert not _seen().same(_Prices({**SHEETS, "labor": [{"key": "L1", "price": 101}]})) - added = {**SHEETS, "compare": [{"key": "M9", "applied": 1}]} - assert not _seen().same(_Prices(added)) # 없던 자재가 생김 - machine = {**SHEETS, "machine": [{"key": "E1", "labor": 1, "material": 2, "expense": 4}]} - assert not _seen().same(_Prices(machine)) +def test_산근_두_번_셈_앞_풀이_그대로() -> None: + memo = {"sg": {"sg": [{"key": "SG:x"}], "sg_list": [], "sg_gaps": [], "sg_notes": [], + "sg_hand": {}}} # fmt: skip + got = sg.collect([("s1", ROOT / "없는 사본")], None, memo) + assert got == memo["sg"] and got is not memo["sg"] # 사본(깊은 복사) def test_M01_범위_안_자국_한_번(monkeypatch) -> None: