"""727 M02 단가산출 기초 넣기(요구서_내역19 · 내역 20-1) — 임시 storage · 실제 M01 읽기만. - 727-1 고르기 창 기초 = 로직마다 한 줄(`logic/M02_Find_Heads.ts` · 옛 창도) · 규격 칸 = 「옵션 N」 - 727-2 첫 기초 = 바탕 · 둘째부터 = 더한줄 세부 펼침(덮개 · 뺀줄 · M01 개정 지문 · 꼬리표) · B09 굳힘 같은 값 - 727-3 넣은 뒤 고른 옵션 = SK `입력값` → 일위대가(펼침) · 굳힘도 그 옵션 """ from __future__ import annotations import json import re import subprocess from pathlib import Path import pytest from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Item_Prices as prices from M02_MasterTemplete import M02_Item_Source as source from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_Logic_Standard as standard from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Sangeun_Calc as calc from resources.tester.test_m02_labor_ref import MINE, world # noqa: F401 — 같은 임시 층 ROOT = Path(__file__).resolve().parents[2] M02 = ROOT / "M02_MasterTemplete" IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE) GF = "GF000099" # 측구터파기 토사 · 원/㎥ · 입력 지역 · 보정작업 def _node(files: list[str], body: str): code = "".join(IMPORTS.sub("", (ROOT / f).read_text("utf-8")) for f in files) got = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript", "-"], input=code + body, capture_output=True, text=True, encoding="utf-8", timeout=60, ) # fmt: skip assert got.returncode == 0, got.stderr return json.loads(got.stdout) def _heads(rows): body = ( f"\nconst rows = {json.dumps(rows, ensure_ascii=False, default=str)};" "\nconsole.log(JSON.stringify(logicHeads(rows, (n) => `옵션 ${n}`)));" ) return _node(["M02_MasterTemplete/logic/M02_Find_Heads.ts"], body) def test_727_1_기초마다_한_줄(): whole = m01.loaded()[1] rows = [] for key in (GF, "GC000269", "GC001109"): rows += standard._rows_of(whole, whole.logic(key), False) assert sum(r["키"] == GF for r in rows) == 3 # 옵션 축 보정작업 두 값 + 바탕 got = _heads(rows) assert [r["키"] for r in got] == [GF, "GC000269", "GC001109"] assert all(r["바꿈"] == {} for r in got) one = got[0] assert one["규격"] == "옵션 1" and one["금액"] == next(r for r in rows if r["키"] == GF)["금액"] assert got[2]["규격"] == "" # 옵션 없는 기초 listed = standard.listing() if listed["준비"]: # 파생 저장본이 있을 때 — 1,461 안팎(로직 수) heads = _heads([{k: r[k] for k in ("키", "규격", "바꿈")} for r in listed["줄"]]) assert len(heads) == len({r["키"] for r in listed["줄"]}) assert 1400 <= len(heads) <= 1600 def test_727_1_두_창_모두_기초_한_줄(): find = (M02 / "M02_MasterTemplete_Find_Pick.ts").read_text("utf-8") old = (M02 / "M02_MasterTemplete_Labor_Pick.ts").read_text("utf-8") assert "std = s?.줄.length ? logicHeads(s.줄) : null;" in find assert "std = got.줄.length ? logicHeads(got.줄) : null;" in old assert "logicOnly" not in find # ── 727-2 · 727-3 서버(임시 storage) ─────────────────────────────────────── # 금속기와 해체 3-2-1 · 원/㎡ · 지붕잇기공 0.018 · 보통인부 0.012 · 덧줄 공구손료 3 % PCT = "GC001109" SECOND = {"종류": "로직", "로직": PCT, "입력값": {}, "수량": 1} COVER = {"labor:LB000036#0": {"qty": 0.036}} DROP = ["pct:공구손료 및 경장비#0"] def _sk(folder, **doc): with store.at(folder): made = refs.create("sangeun", {"이름": "sub7임시", "단위": "㎥", "더한줄": [], **doc}) return store.read("sangeun", made["이름"])["문서"] def _run(folder, doc, given=None): with store.at(folder): return calc.calc_doc(doc, given) def _line(got, cid): return next(x for x in got["줄"] if x["cid"] == cid) def _m01(code, values=None): return float(labor.run_line(m01.loaded()[1], code, values or {}, None, None)["기준"]["계"]) def test_727_2_첫_기초_바탕_세부_줄(world): # noqa: F811 doc = _sk(world["personal"], 바탕={"로직": GF}) got = _run(world["personal"], doc) assert _line(got, "labor:LB000002#0")["수량"] == pytest.approx(0.023) assert _line(got, "logic:GC000267#0")["수량"] == pytest.approx(0.01546, abs=1e-5) assert got["꼬리표"] == "9-12-1" def test_727_2_기초_둘_세부_펼침_계(world): # noqa: F811 doc = _sk(world["personal"], 바탕={"로직": GF}, 더한줄=[SECOND]) bare = _run(world["personal"], _sk(world["personal"], 바탕={"로직": GF})) got = _run(world["personal"], doc) add = _line(got, "add:#0") assert add["꼬리표"] == "3-2-1" and add["바탕판"] and add["뺀세부"] == [] sub = {x["cid"]: x for x in add["펼침"]} assert list(sub) == [ "add:#0/labor:LB000036#0", "add:#0/labor:LB000002#0", "add:#0/pct:공구손료 및 경장비#0", ] assert sub["add:#0/labor:LB000036#0"]["수량"] == pytest.approx(0.018) # 머리 = 수량 × 기초 끝수 계(751-3 · 비목 1 원 · 줄 0.1 원 버림) — 펼침 합과 비목마다 수량 × 1 원 안 q = add["수량"] assert sum(x["금액"] for x in add["펼침"]) == pytest.approx(add["금액"], abs=0.3 + 3 * q) assert add["금액"] == pytest.approx(_m01(PCT), rel=1e-9) assert got["기준"]["계"] == pytest.approx(bare["기준"]["계"] + _m01(PCT), rel=1e-9) with store.at(world["personal"]): # 펼침 없는 셈(쓰는 자리) = 같은 계 plain = calc.run(doc, m01.loaded()[1]) assert float(plain["기준"]["계"]) == pytest.approx(got["기준"]["계"], rel=1e-9) def test_727_2_둘째_기초_덮개_뺀줄_저장(world): # noqa: F811 one = {**SECOND, "수량": 2, "덮개": COVER, "뺀줄": DROP} doc = _sk(world["personal"], 바탕={"로직": GF}, 더한줄=[one]) saved = doc["더한줄"][0] assert saved["덮개"]["labor:LB000036#0"] == { "qty": 0.036, "원값": {"qty": 0.018, "단가": 233250}, } assert saved["뺀줄"] == DROP and saved["바탕판"] got = _run(world["personal"], doc) add = _line(got, "add:#0") cut = labor.mcut.down # 751-3 — 기초 줄 0.1 원 · 비목 합 1 원 → × 수량 0.1 원 want = cut(2 * cut(cut(0.036 * 233250, 1) + cut(0.012 * 172068, 1), 0), 1) assert add["금액"] == pytest.approx(float(want), rel=1e-9) sub = {x["cid"]: x for x in add["펼침"]} assert sub["add:#0/labor:LB000036#0"]["덮개"] == ["qty"] assert sub["add:#0/labor:LB000036#0"]["수량"] == pytest.approx(0.072) assert add["뺀세부"] == [{"cid": "add:#0/" + DROP[0], "이름": "공구손료 및 경장비"}] lost = {**one, "덮개": {"labor:LB999999#0": {"qty": 1}}} got = _run(world["personal"], {**doc, "더한줄": [lost]}) assert any(x["cid"] == "add:#0/labor:LB999999#0" for x in got["짝잃음"]) def test_727_2_굳힘_같은_값(world, monkeypatch): # noqa: F811 one = {**SECOND, "덮개": COVER, "뺀줄": DROP} key = _sk( world["personal"], 바탕={"로직": GF}, 더한줄=[one], 입력값={"보정작업": "암석작업(연암・보통암・경암)"}, ) ref = {**MINE, "키": key["키"]} with store.at(world["personal"]): top = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 2}}]}) live = labor.calc(top, {})["줄"][0] rows = source.labor_doc_rows(top) bundle = source.price_bundle(rows, world["personal"]) frozen = rows["참조사본"][refs.ref_id(ref)] assert frozen["굳힌줄"]["호표"][-1]["값묶음"] # 고친 둘째 기초 = 셈한 값묶음 줄 got = prices.calc(rows, json.loads(json.dumps(bundle, ensure_ascii=False)), {}, {}, None) line = got["줄"][0] assert not line["까닭"] and not live["까닭"] for part in ("노무비", "재료비", "경비", "계"): assert line["기준"][part] == pytest.approx(live["기준"][part], rel=1e-9) def test_727_3_옵션_저장_일위대가_그_옵션(world): # noqa: F811 rock = {"지역": "전국평균", "보정작업": "암석작업(연암・보통암・경암)"} doc = _sk( world["personal"], 바탕={"로직": GF}, 입력값={"보정작업": "암석작업(연암・보통암・경암)"} ) assert doc["입력값"] == {"보정작업": "암석작업(연암・보통암・경암)"} # 다시 열면 그대로 with store.at(world["personal"]): up = refs.Expand(m01.loaded()[1])({**MINE, "키": doc["키"]}) assert float(up["기준"]["계"]) == pytest.approx(_m01(GF, rock), rel=1e-9) assert _m01(GF, rock) > _m01(GF, {"지역": "전국평균", "보정작업": "해당 없음"}) # 둘째 기초 입력(바탕에 없는 이름)도 SK 입력값에 남고 그 옵션으로 셈 two = _sk( # 단위 = 바탕 단위(728-2 — 성격 다르면 계수 칸) world["personal"], 바탕={"로직": PCT}, 단위="㎡", 더한줄=[{**SECOND, "로직": GF}], 입력값=rock, ) assert two["입력값"] == rock got = _run(world["personal"], two) assert _line(got, "add:#0")["금액"] == pytest.approx(_m01(GF, rock), rel=1e-9) def _model(ans, doc, script): body = ( f"\nconst ans = {json.dumps(ans, ensure_ascii=False)};" f"\nconst doc = {json.dumps(doc, ensure_ascii=False)};\n{script}" ) return _node( [ "M02_MasterTemplete/logic/M02_Born_Tag.ts", "M02_MasterTemplete/logic/M02_Sangeun_Undo.ts", "M02_MasterTemplete/logic/M02_Sangeun_Cover_Model.ts", ], body, ) def test_727_2_화면_꼴_세부_줄_고침(world): # noqa: F811 doc = _sk(world["personal"], 바탕={"로직": GF}, 더한줄=[{**SECOND, "수량": 2}]) ans = _run(world["personal"], doc) got = _model( ans, doc, """ const rows = rowsOf(ans, null, doc); const view = hoView(ans, null, null, doc); const sub = rows.find((r) => r.cid === "add:#0/labor:LB000036#0"); setLineCover(doc, sub, { qty: 0.072 }, false); const covered = JSON.parse(JSON.stringify(doc.더한줄[0])); dropLine(doc, rows.find((r) => r.cid === "add:#0/pct:공구손료 및 경장비#0")); const dropped = [...doc.더한줄[0].뺀줄]; restoreLine(doc, "add:#0/pct:공구손료 및 경장비#0"); const one = { ...doc, 더한줄: [] }; console.log(JSON.stringify({ cids: rows.map((r) => r.cid), boxes: rows.map((r) => r.묶음), sub, covered, dropped, back: doc.더한줄[0].뺀줄 ?? null, tags: view.호표.map((x) => x.꼬리표 ?? ""), extraTags: view.덧줄.map((x) => x.꼬리표 ?? ""), viewCids: view.cids, single: [...tagsOf(ans, one).values()], })); """, ) assert "add:#0" not in got["cids"] and "add:#0/labor:LB000036#0" in got["cids"] assert got["sub"]["sub"] == {"k": 0, "cid": "labor:LB000036#0", "scale": 2} assert got["sub"]["수량"] == pytest.approx(0.036) and got["sub"]["묶음"] == "인력" assert got["covered"]["덮개"]["labor:LB000036#0"]["qty"] == pytest.approx( 0.072 ) # 화면 품 그대로(M01 원문 몫 · 748-0 — 수량 2 로 안 나눔) assert got["covered"]["덮개"]["labor:LB000036#0"]["원값"] == {"qty": 0.018, "단가": 233250} assert got["dropped"] == DROP and got["back"] is None assert got["tags"] == ["9-12-1", "9-12-1", "3-2-1", "3-2-1"] # 바탕 둘 · 둘째 기초 인력 둘 assert got["extraTags"] == ["3-2-1"] and got["single"] == [] # 기초 하나면 꼬리표 없음