diff --git a/M02_MasterTemplete/M02_San_Ilwi.py b/M02_MasterTemplete/M02_San_Ilwi.py index d7081a6c2..599216b31 100644 --- a/M02_MasterTemplete/M02_San_Ilwi.py +++ b/M02_MasterTemplete/M02_San_Ilwi.py @@ -5,7 +5,7 @@ 일위 구성 줄 꼴(원파일 · 엑셀 같은 꼴 — 읽기기 답 `일위` 블록 `줄`): {차례, 코드(자원 · PR · RR · C1 · T1 · 빈), 갈래(M L S X B D W · 율 · 닫기 · 구분 · 글), 이름, 규격, 단위, 수량(1 차), 단가 {T L M S}?(셈에 쓴 자원 단가 · 중기 조종원 = 시간 환산), 금액 {T L M S}?(줄 금액), - 율? {밑, 대상(M L S T), 곱(RR)}, 밑줄?(율 밑수에 듦), 관급?, 별도계상?(자재 이름 · 규격 「별도계상」 · 「별산」)} + 율? {밑, 대상(M L S T), 곱(RR)}, 밑줄?(율 밑수에 듦), 관급?, 별도계상?(자재 · 경비 · 일식 이름 · 규격 「별도계상」 · 「별산」)} 블록 = {코드, 이름, 규격, 단위, 저장, 줄, 줄합 {T L M S}(다시 셈 · 끝전 뒤), 표지} 율 줄 이름 = 지은 글(「노무비의 3%」 · 원문 이름 칸은 못 읽음). HP 호표(`hopyo`) = 엑셀 일위대가표 읽기(`M02_Hopyo_Stmate.read`)와 같은 꼴 — 글 · 구분 · 닫기 줄 뺌. @@ -123,9 +123,8 @@ def lines(rows: list[dict], res: dict[str, dict], opt: dict) -> tuple[list[dict] one["밑줄"] = True if row.get("KWNGP") == "1": one["관급"] = True - if kind == "M" and re.search( - r"별도계상|별산", one["이름"] + one["규격"] - ): # 엑셀 비고 「별산…」 짝 + apart = re.search(r"별도계상|별산", one["이름"] + one["규격"]) + if kind in ("M", "S", "W") and apart: # 엑셀 비고 「별산…」 짝 one["별도계상"] = True out.append(one) before = dict(s) @@ -260,6 +259,10 @@ def hopyo(blocks: list[dict]) -> list[dict[str, Any]]: line = {"차례": len(rows), **{k: one[k] for k in ("이름", "규격", "수량", "단위")}} line |= {"단가": _hp(one.get("단가")), "금액": _hp(one.get("금액")), "코드": code} line["비고"] = "별산" if one.get("별도계상") else "" + if rate := one.get( + "율" + ): # 숨은 칸 율 밑 · 대상(828-2 `M02_Hopyo_Fixed._ratio` 와 같은 글) + line["율"] = {k: WORD[rate[k]] for k in ("밑", "대상")} for k in ("계", "노무비", "재료비", "경비"): line["단가"].setdefault(k, 0) line["금액"].setdefault(k, 0) @@ -320,8 +323,8 @@ def materials(saved: dict): for b in saved.get("일위") or []: head = f"{b.get('이름') or ''} {b.get('규격') or ''}" for ln in b.get("줄") or []: - if ln.get("갈래") != "M": - continue + if ln.get("갈래") != "M" or str(ln.get("단위") or "").strip().endswith("%"): + continue # 율 자원(「잡재료비 재료비의 %」) = 재료 아님 res = {k: ln.get(k) or "" for k in ("이름", "규격", "단위", "코드")} | {"종류": "자재"} with mf.clues_on(saved.get("단서"), head): link.m01("재료", res, "") diff --git a/resources/tester/test_828_1_stc_composition.py b/resources/tester/test_828_1_stc_composition.py index 6d5937e74..4065fedce 100644 --- a/resources/tester/test_828_1_stc_composition.py +++ b/resources/tester/test_828_1_stc_composition.py @@ -211,7 +211,39 @@ def test_line_shape() -> None: kinds = {ln["갈래"] for t in stc.FILES for x in parsed(t)["일위"] for ln in x["줄"]} assert kinds <= {"M", "L", "S", "X", "B", "D", "W", "율", "닫기", "구분", "글"} apart = [ln for x in parsed("동부청영월")["일위"] for ln in x["줄"] if ln.get("별도계상")] - assert len(apart) == 17 # 엑셀 비고 「별산…」 17 줄과 같음(분석 813 2 장) + assert len(apart) == 18 # 엑셀 비고 「별산…」 줄과 같음(자재 17 + 경비 S11039 · 828-2) + + +def test_same_lines_as_excel_reader() -> None: + """828-2 엑셀 읽기기(`M02_San_Xlsx_Ilwi` 숨은 칸) 일위 줄 = 원파일 줄(코드 · 갈래 · 수량 · 율 · 표시 · 136 · 706).""" + from M02_MasterTemplete import M02_San_Book as book + from M02_MasterTemplete import M02_San_Xlsx as sx + + def key(x: dict) -> tuple: + r, code = x.get("율") or {}, x["코드"] + qty = None if x["갈래"] == "율" else round(float(x["수량"] or 0), 6) + flags = tuple(bool(x.get(k)) for k in ("밑줄", "관급", "별도계상")) + return code[:1] if code[:1] in "MLSXBDW" else code, x["갈래"], qty, r.get("밑"), r.get("대상"), flags # fmt: skip + + hops = lines = 0 + for xt, st in PAIR.items(): + path = golden.FILES[xt] + if not path.exists(): + pytest.skip(f"실무 엑셀 없음 {xt}") + xl = sx.read_sheets(book.sheets(path.name, path.read_bytes())) + nat = parsed(st)["일위"] + by_code = {b["코드"]: b for b in nat} + by_name = {(_flat(b["이름"]), _flat(b["규격"])): b for b in nat} + for b in xl["일위"]: + head = (_flat(b["이름"]), _flat(b["규격"])) + y = by_code.get(b["코드"]) + if ( + y is None or (_flat(y["이름"]), _flat(y["규격"])) != head + ): # 울진대흥 = 코드 새로 매김 + y = by_name[head] + assert [key(x) for x in b["줄"]] == [key(x) for x in y["줄"]], (xt, b["코드"]) + hops, lines = hops + 1, lines + len(b["줄"]) + assert (hops, lines) == (136, 706) # ── ④ 중기 구성 ──────────────────────────────────────────────────────────