diff --git a/B08_Quantity/B08_Quantity_Router_Material.py b/B08_Quantity/B08_Quantity_Router_Material.py index 4c7334dbd..d8e6626cb 100644 --- a/B08_Quantity/B08_Quantity_Router_Material.py +++ b/B08_Quantity/B08_Quantity_Router_Material.py @@ -57,6 +57,7 @@ def _qty(value: Any) -> float | None: def _ref_mats(whole: Any, read: Any, ref: dict, trail: tuple[str, ...] = ()) -> list[tuple]: """참조 문서 1 단위 재료 `[(재료 줄, 수량)]` — 층 셈 펼침(`M02_Labor_Ref.Expand`)과 같은 길: 승격 = 원 로직 재료 ÷ 배 · 사전조합 = 안 줄 재료 × 손 수량(빈칸 1) · 로직 줄은 알약 배로 나눔 · + 사전조합 자재(175-2) = 손 수량(빈칸 = 안 셈 · 품목 키 = 코드 · `자재` 표시 = 연료 거름 안 함) · 순환 · 깊이 넘음 · 없는 문서 = 빈 것.""" from M01_MasterData import M01_MasterData_Store_Combo as combo_store from M02_MasterTemplete import M02_Labor_Doc as labor @@ -94,6 +95,10 @@ def _ref_mats(whole: Any, read: Any, ref: dict, trail: tuple[str, ...] = ()) -> continue hand = _qty((one.get("수량") or {}).get("손")) out += [(m, q * (1.0 if hand is None else hand)) for m, q in sub] + for mat in doc.get("자재") or []: + if hand := _qty((mat.get("수량") or {}).get("손")): + line = {k: mat.get(k) for k in ("이름", "단위")} + out.append(({**line, "품목": {"키": mat.get("코드")}, "자재": True}, hand)) return out @@ -129,7 +134,7 @@ def _folder_units(root: str | Path, folder: str, meta: dict[str, dict]) -> dict[ def logic_mat(mat: dict, qty: float) -> None: name, unit = str(mat.get("이름") or ""), str(mat.get("단위") or "") - if _fuel(name, unit): + if _fuel(name, unit) and not mat.get("자재"): # 사전조합 사용자 자재는 거르지 않음 return goods = mat.get("품목") code = str(goods.get("키") or "") if isinstance(goods, dict) else "" diff --git a/B09_Estimation/B09_Estimation_Labor_Source.py b/B09_Estimation/B09_Estimation_Labor_Source.py index b6d73a401..cb49e0f09 100644 --- a/B09_Estimation/B09_Estimation_Labor_Source.py +++ b/B09_Estimation/B09_Estimation_Labor_Source.py @@ -4,7 +4,8 @@ (`M02_Item_Prices.calc` = 142-1 층 셈 꼴)로 돌려 줄 수량을 받고, 블록 꼴로 펼침: 조합 구조물 조합 사본 하나 = 블록(엑셀 「조합」 꼴) · 줄 = 담은 로직 줄 · 자재 줄 - 사전조합 참조 SC = 겹 대가(굳힌 `참조사본`) · 줄 = 그 안 로직 · 참조 줄(수량 = 손) + 사전조합 참조 SC = 겹 대가(굳힌 `참조사본`) · 줄 = 그 안 로직 · 참조 줄(수량 = 손) · + 자재 줄(175-2 · 수량 = 손 · 빈칸 = 수량 없음) 품셈 일위대가화(승격) 줄 · 옛 참조 SP = 로직마다 따로(노무 · 재료 · 중기 · 산근 · % 줄) — 조합이 수량만큼 부름 · 기계 = 기계경비 로직 줄 · 다른 돈 로직 줄 = 하위 산근 산근 단가산출 줄(일위대가화 아닌 로직 줄) = 산근 호표 참조(키 = 145-1 `sangeun_key`) @@ -206,7 +207,7 @@ class Builder: "spec_raw": block.get("spec_raw"), "unit": block["unit"], "qty": each} # fmt: skip def precombo(self, whole: Any, ref: dict, doc: dict, copies: dict, depth: int = 0) -> str: - """사전조합(SC) = 겹 대가 — 줄 수량 = 손(기본 1) · 로직 줄 = 품셈 호표.""" + """사전조합(SC) = 겹 대가 — 줄 수량 = 손(기본 1) · 로직 줄 = 품셈 호표 · 자재 줄(175-2).""" from M01_MasterData import M01_MasterData_Store_Combo as combo_store inner = [one for one in doc.get("담은로직") or [] if isinstance(one, dict)] @@ -226,6 +227,10 @@ class Builder: continue qty = 1 if hand is None else hand lines.append(self.line_of(whole, one, qty, times, name, copies, depth)) + lines += [ + _material(whole, mat, (mat.get("수량") or {}).get("손")) + for mat in doc.get("자재") or [] + ] return self.put( { "kind": "사전조합", @@ -262,11 +267,7 @@ class Builder: if line.get("qty") is None and line["kind"] != "blank": line = {**line, "why": "수량 없음 — 면적산출 알약 칸 · 손 수량을 넣음"} lines.append(line) - for mat in result.get("자재") or []: - row = (whole.get(mat.get("코드")) if mat.get("코드") else None) or {} - lines.append({"kind": "material", "key": mat.get("코드"), "name": row.get("이름") or "", - "spec": row.get("규격") or "", "unit": row.get("단위") or "", - "qty": _f(mat.get("수량"))}) # fmt: skip + lines += [_material(whole, mat, mat.get("수량")) for mat in result.get("자재") or []] manifest = read_json(folder / MANIFEST_NAME) or {} origin = manifest.get("원본") or {} return self.put( @@ -282,6 +283,13 @@ class Builder: ) +def _material(whole: Any, mat: dict, qty: Any) -> dict[str, Any]: + """자재 줄 — 조합 사용자 자재 · 사전조합 자재(175-2) 한 꼴 · 이름 · 규격 · 단위 = 사본 단가.""" + row = (whole.get(mat.get("코드")) if mat.get("코드") else None) or {} + names = {k: row.get(v) or "" for k, v in (("name", "이름"), ("spec", "규격"), ("unit", "단위"))} + return {"kind": "material", "key": mat.get("코드"), **names, "qty": _f(qty)} + + def _engine_bases(engine: list[dict], items: list[dict]) -> dict[str, float]: """엔진 호표 줄 → 덧줄 밑수 후보 — `part:비목` 합 · `lines:labor` 인력 줄 합 · `line:이름`.""" out: dict[str, float] = {f"part:{p}": 0.0 for p in PART.values()} diff --git a/M02_MasterTemplete/M02_Item_Source.py b/M02_MasterTemplete/M02_Item_Source.py index 30d53ed22..717791ba8 100644 --- a/M02_MasterTemplete/M02_Item_Source.py +++ b/M02_MasterTemplete/M02_Item_Source.py @@ -162,10 +162,11 @@ def build_source(item: dict[str, Any]) -> dict[str, Any]: materials += [ # 고른 로직 자재(103-1) 품목 줄도 — 복사본 계산이 그 단가를 씀 str(key) for one in doc.get("담은로직") or [] for key in (one.get("고름") or {}).values() ] + materials += refs.frozen_materials((labor or {}).get("참조사본") or {}) # 175-2 사전조합 자재 wanted = logic_keys + [code for code in materials if code] files[PRICE_FILE] = prices.to_json(prices.collect(logic_keys, materials)) if wanted else {} if mine := own.bundle( - own.codes(doc), root - ): # 개인 자재(148-3) — 원본 목록이 바뀌어도 사본 그대로 + [c for c in materials if own.is_own(c)], root + ): # 개인 자재(148-3 · 사전조합 것도 175-2) — 원본 목록이 바뀌어도 사본 그대로 files[PRICE_FILE][own.BUNDLE] = mine return files diff --git a/M02_MasterTemplete/M02_Labor_Doc.py b/M02_MasterTemplete/M02_Labor_Doc.py index 0692a0245..890c7155a 100644 --- a/M02_MasterTemplete/M02_Labor_Doc.py +++ b/M02_MasterTemplete/M02_Labor_Doc.py @@ -607,8 +607,13 @@ def server(doc: Any) -> dict[str, Any]: 사용자 자재가 로직 자재 품목과 같으면 400(103-1 · 같은 품목 두 번 셈).""" got = clean(doc) out = calc(got) + no_twice(got, out) + return {**got, "계산값": out} + + +def no_twice(got: dict, out: dict) -> None: + """사용자 자재가 로직 자재 품목과 같으면 400 — 일위대가 · 사전조합(175-1) 저장 한 벌.""" inside = {x["품목"]["키"] for one in out["줄"] for x in one.get("재료") or [] if x.get("품목")} twice = sorted({m["코드"] for m in got["자재"]} & inside) if twice: raise _bad(f"자재 「{', '.join(twice)}」 는 로직 자재에 있음 — 같은 품목 두 번 담음") - return {**got, "계산값": out} diff --git a/M02_MasterTemplete/M02_Labor_Ref.py b/M02_MasterTemplete/M02_Labor_Ref.py index 126a968a0..bc06b9ac0 100644 --- a/M02_MasterTemplete/M02_Labor_Ref.py +++ b/M02_MasterTemplete/M02_Labor_Ref.py @@ -6,8 +6,10 @@ 층 문서를 읽을 때 옛 참조 줄 = 로직 줄 + `승격`(`unpromote`) · 프로젝트 사본은 그대로 셈 precombo `{층}/precombo/SC-######.json` 사전조합 `{종류:"사전조합", 키, 이름, 단위, 본뜬, 담은로직:[{참조, 수량:{손}, 메모} - | {로직, 입력값, 메모, 수량:{손}}]}` — 승격 · 사전조합 · 단가산출 로직(51-11) · - 수량은 손(기본 1) · 값 = Σ 안 줄 1 단위 값 × 수량(로직 줄 = 기준 ÷ 배) + | {로직, 입력값, 메모, 수량:{손}, 고름?, 자재표시?}], 자재:[일위대가 자재 꼴]}` — + 승격 · 사전조합 · 단가산출 로직(51-11) · 수량은 손(기본 1) · + 자재 수량은 손 · 빈칸 = 안 셈(175) · + 값 = Σ 안 줄 1 단위 값 × 수량(로직 줄 = 기준 ÷ 배) + Σ 자재 기준단가 × 손 참조 = `{층, 주인, 키}`(마스터 주인 null · 회사 `C` · 개인 `C/U`) — 종류는 키 머리(SP · SC) · 위 층만 가리킴(개인 → 그 개인 · 그 회사 · 마스터 / 회사 → 그 회사 · 마스터 / 마스터 → 마스터) · 같은 참조 두 번 · 순환(422)은 저장 때 막음 · 계산 때도 깊이 상한(`DEPTH`)에서 멈추고 까닭. @@ -142,6 +144,7 @@ def clean_doc(kind: str, doc: Any) -> dict[str, Any]: if not name: raise _bad("사전조합 이름이 빔") out.update(이름=name, 단위=unit or None, 담은로직=_combo_lines(doc, key)) + out["자재"] = _combo_mats(doc, out["담은로직"]) out.update({k: str(doc.get(k) or "").strip() for k in ("구분", "상세구분")}) out["본뜬"] = doc["본뜬"] if isinstance(doc.get("본뜬"), dict) else None return out @@ -206,7 +209,7 @@ def _combo_lines(doc: dict, key: str) -> list[dict[str, Any]]: head = {"참조": one["참조"]} if "참조" in one else {"로직": one["로직"]} if "로직" in one: head["입력값"] = one["입력값"] - head.update({k: one[k] for k in ("켜쌓기율",) if k in one}) + head.update({k: one[k] for k in ("켜쌓기율", "고름", "자재표시") if k in one}) lines.append({**head, "수량": one["수량"] or {"손": 1}, "메모": one["메모"]}) at_ = here() me = {"층": at_[0], "주인": at_[1], "키": key} @@ -219,6 +222,18 @@ def _combo_lines(doc: dict, key: str) -> list[dict[str, Any]]: return lines +def _combo_mats(doc: dict, lines: list[dict[str, Any]]) -> list[dict[str, Any]]: + """사전조합 자재(175-1) — 일위대가 자재와 같은 검사(`labor.clean` · 개인 자재 · 세 칸) · + 로직 자재와 같은 품목 400(`labor.no_twice`).""" + from M02_MasterTemplete import M02_Labor_Doc as labor + + mats = labor.clean({"자재": doc.get("자재") or []})["자재"] + if mats: + got = {"담은로직": lines, "자재": mats} + labor.no_twice(got, labor.calc(got, {})) + return mats + + def _no_loop(ref: Ref, trail: list[Ref]) -> None: """저장 때 순환 · 깊이 — 아래로 따라가 처음 문서를 다시 만나면 422 「A → B → A」.""" if len(trail) > DEPTH: @@ -446,6 +461,29 @@ class Expand: why = why or f"「{sub.get('이름')}」 {sub.get('까닭')}" continue rows.append({k: v * qty for k, v in sub["기준"].items()}) + for mat in doc.get("자재") or []: # 사용자 자재(175-2) = 기준단가 × 손 · 빈칸 = 안 셈 + qty = labor._number((mat.get("수량") or {}).get("손")) + if qty is None: + continue + got = labor.material_price(self.whole, mat["코드"], mat.get("단위")) + price = labor._number(got["기준단가"]) + zero = dict.fromkeys(labor.COSTS, Decimal(0)) + base = None if price is None else {**zero, "재료비": price, "계": price} + name = mat.get("이름") or mat["코드"] + inner.append( + { + "자재": mat["코드"], + "수량": qty, + "이름": name, + "단위": mat.get("단위"), + "기준": base, + "까닭": got["까닭"], + } + ) + if base is None: + why = why or f"「{name}」 {got['까닭']}" + continue + rows.append({k: v * qty for k, v in base.items()}) return { "이름": doc.get("이름"), "단위": doc.get("단위"), @@ -498,6 +536,16 @@ def with_pills(doc: dict[str, Any]) -> dict[str, Any]: return {**doc, "알약": combo_store.combo_pills({"담은로직": lines})["물량"]} +def frozen_materials(copies: dict[str, Any]) -> list[str]: + """굳힌 사전조합의 자재 · 고른 로직 자재 코드(175-2) — 프로젝트 단가 묶음에 넣음.""" + out = [] + for doc in copies.values(): + out += [str(m.get("코드")) for m in doc.get("자재") or [] if isinstance(m, dict)] + lines = [one for one in doc.get("담은로직") or [] if isinstance(one, dict)] + out += [str(key) for one in lines for key in (one.get("고름") or {}).values()] + return out + + def frozen_logics(copies: dict[str, Any]) -> list[str]: """굳힌 단가승격 · 사전조합이 담은 로직 키 — 프로젝트 단가 묶음에 넣음.""" return [ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts index 2024f610a..529f6bba4 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts @@ -40,12 +40,13 @@ export interface CalcLine { /** 로직 자재(103-1) — 맨 위 층 재료 줄 · 금액은 `기준` 안(여기서 안 더함) */ 재료?: LogicMat[]; /** 참조 줄 — 가리킨 문서 이름 · 단가승격의 원 로직 키 · 사전조합 안 줄(한 층 · 50-3 ④ · - * 참조 줄 = `참조` · 로직 줄(51-11) = `로직`) */ + * 참조 줄 = `참조` · 로직 줄(51-11) = `로직` · 자재 줄(175-2) = `자재` 코드) */ 이름?: string | null; 원로직?: string | null; 안?: { 참조?: LaborRef; 로직?: string; + 자재?: string; 수량: number; 이름?: string | null; 단위?: string | null; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index a37a05411..71a5e79ce 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -10,7 +10,7 @@ * · 자재목록 = 로직 자재 줄(103-1 · `Labor_LogicMats` · [바꾸기] → `담은로직[].고름` · 입력이 바뀌면 걸린 고름 풂) + * 사용자 자재 줄([삭제]) · 로직 자재와 같은 품목은 사용자 자재로 못 담음 * · 오른쪽 「원문」 접는 패널(47-4 · 상세만 · 공용 `ui_template_side_panels`) = 고른 줄 로직의 출처 원문 - * · 서고 모드(50-3 · `mode`) = 사전조합 — 이름 칸 · 단가산출 로직 표 · 원문만(자재 · 면적 · 다이어그램 없음) · + * · 서고 모드(50-3 · `mode`) = 사전조합 — 이름 칸 · 단가산출 로직 표 · 자재목록(175-1) · 원문(면적 · 다이어그램 없음) · * 정보 칸 구분 · 상세구분(51-12 · 고름) · 일위대가 상세와 같은 [로직 추가] · 표 · 동작(51-11) · 수량 손 입력(기본 1) * · 로직 줄 구분 드롭다운(134-1 · 단가산출 | 단가승격 · 서고 모드엔 없음) * ========================================================================== */ @@ -228,13 +228,13 @@ export function mountLaborDetail( cur.담은로직, calc ?? last, !calc && !fail, - // 세 칸(148-4) = 구조물 일위대가만(사전조합 줄은 `_combo_lines` 가 안 남김) + // 세 칸(148-4) — 사전조합도 같음(175-1 · `_combo_lines` 가 남김) edit ? { open: openRow, swap: swapMat, reset: resetMat, - mark: lib ? undefined : markLogicMat, + mark: markLogicMat, } : { open: openRow }, ), @@ -641,7 +641,7 @@ export function mountLaborDetail( ]), // 사전조합 = 같은 [로직 추가](51-11) section(tc("Logics_Title"), opt.card ? null : addLogic, logicHost), - ...(lib ? [] : [section(tc("Labor_Mats"), opt.card ? null : addMat, matHost)]), + section(tc("Labor_Mats"), opt.card ? null : addMat, matHost), ...(opt.card || lib ? [] : [dgBox]), ], }), @@ -668,12 +668,11 @@ export function mountLaborDetail( }); return { - // 서고 모드 = 문서 종류 그대로 · 열 · 자재 없음 + // 서고 모드 = 문서 종류 그대로 · 열 없음 getDoc: () => { if (!lib) return cur; const out: Record = { ...cur, 종류: "사전조합" }; delete out.열; - delete out.자재; return out as unknown as LaborDoc; }, saveArea: async () => { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts index 0380f2469..4b67f4686 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Rows.ts @@ -267,14 +267,14 @@ export interface LogicTableData { } /** 고른 줄 하위 행(51-2 · 같은 표 · 같은 칸) — 단가산출 · 단가승격 = 그 로직 재료 · 노무 · 경비 줄 · - * 일위조합 = 세부 항목 한 층(51-13 · 참조 줄 · 로직 줄 · 안을 다시 펼치지 않음 · 금액 = 안 줄 기준 × 안 줄 수량) · + * 일위조합 = 세부 항목 한 층(51-13 · 참조 줄 · 로직 줄 · 자재 줄 175-2 · 안을 다시 펼치지 않음 · 금액 = 안 줄 기준 × 안 줄 수량) · * 금액 칸 = 위 기준(위 줄 기준과 같은 바탕) / 아래 반영(× 위 줄 수량 ÷ 배) · * 하위 합 = 위 줄 금액 · 줄이 없으면 빈 한 줄(「…」 · 까닭 · 「—」) */ function subRows(line: LaborCalcLine | undefined, wait: boolean, tools: boolean): HTMLElement[] { const times = line?.수량 === null || line?.수량 === undefined ? null : line.수량 / (line.배 || 1); const items = line?.안 ? line.안.map((x) => { - const key = x.참조?.키 ?? x.로직 ?? ""; + const key = x.참조?.키 ?? x.로직 ?? x.자재 ?? ""; const label = x.이름 && x.이름 !== key ? `${key} ${x.이름}` : key; // 로직 줄 이름 없으면 키만 return { name: `${label}${x.까닭 ? ` · ${x.까닭}` : ""}`, diff --git a/resources/tester/test_175_precombo_mats.py b/resources/tester/test_175_precombo_mats.py new file mode 100644 index 000000000..9b98c4cb1 --- /dev/null +++ b/resources/tester/test_175_precombo_mats.py @@ -0,0 +1,149 @@ +"""M02 일위대가 사전조합 자재목록(PLAN 175) — 구조물 일위대가와 같은 자재목록. + +175-1 화면 · 저장 — 서고 모드 자재목록 · 로직 자재 세 칸 막힘 풂 · 저장 `_combo_lines` 가 +고름 · 자재표시 남김 · 문서 `자재`(일위대가 자재 검사 · 로직 자재와 같은 품목 400). +175-2 셈 — 담은 자재(손 · 빈칸 = 안 셈)가 사전조합 1 단위 값(`Expand._combo` · 안 줄) · B08 참조 줄 +재료(`_ref_mats`) · B09 겹 대가 자재 줄 · 프로젝트 사본 단가 묶음(`frozen_materials`)에 들어감. +""" + +import json +import os +from decimal import Decimal + +import pytest + +from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Labor_Doc as labor +from M02_MasterTemplete import M02_Labor_Ref as refs +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from resources.tester.test_m02_labor_ref import world # noqa: F401 — 임시 층 fixture + +HERE = os.path.dirname(os.path.abspath(__file__)) +SRC = os.path.join(HERE, "..", "..", "M02_MasterTemplete") +NET = "GC000337" # 낙석방지망 — 재료 줄 3 · 4 · 5(철망 · 결속선 · 에폭시) +NET_ITEM = "MT006697" +WIRE = "MT000194" +MAT = "MT000008" # 자재품목 · 물가정보 83000 +STONE = "GF000219" # 원/㎡ +SYS = {"층": "system", "주인": None} + + +def _save(folder, doc): + with store.at(folder): + key = refs.create("precombo", {"이름": "자재 조합", **doc})["이름"] + return key, json.loads((folder / "precombo" / f"{key}.json").read_bytes()) + + +def test_화면_서고_모드도_자재목록_세_칸_자재_저장(world): # noqa: F811 + detail = open(os.path.join(SRC, "M02_MasterTemplete_Labor_Detail.ts"), encoding="utf-8").read() + assert 'section(tc("Labor_Mats"), opt.card ? null : addMat, matHost),' in detail + assert 'lib ? [] : [section(tc("Labor_Mats")' not in detail + assert "mark: markLogicMat," in detail and "lib ? undefined" not in detail + assert "delete out.자재" not in detail and "delete out.열" in detail + + +def test_저장_왕복_고름_자재표시_자재_남음(world): # noqa: F811 + line = {"로직": NET, "고름": {"4": NET_ITEM}, "자재표시": {"3": {"자재구분": "관급"}}} + mats = [ + {"코드": MAT, "수량": {"손": 2}, "운반비": "미포함", "이름": "틀린 이름"}, + {"코드": WIRE, "수량": None}, + ] + _, got = _save(world["system"], {"담은로직": [line], "자재": mats}) + one = got["담은로직"][0] + assert one["고름"] == {"4": NET_ITEM} and one["자재표시"] == {"3": {"자재구분": "관급"}} + assert [(m["코드"], m["수량"]) for m in got["자재"]] == [(MAT, {"손": 2}), (WIRE, None)] + assert got["자재"][0]["운반비"] == "미포함" and got["자재"][0]["이름"] != "틀린 이름" + # 자재 없음 = 빈 목록(옛 문서 꼴 그대로 읽힘) + _, empty = _save(world["system"], {"담은로직": [{"로직": NET}]}) + assert empty["자재"] == [] + + +def test_로직_자재와_같은_품목_400_없는_자재_400(world): # noqa: F811 + with pytest.raises(store.StoreError) as e: + _save(world["system"], {"담은로직": [{"로직": NET}], "자재": [{"코드": WIRE}]}) + assert e.value.status == 400 and WIRE in str(e.value.detail) + with pytest.raises(store.StoreError) as e: + _save(world["system"], {"담은로직": [], "자재": [{"코드": "MT999999"}]}) + assert e.value.status == 400 + + +# ── 175-2 셈 ────────────────────────────────────────────────────────── +def _price(code): + whole = m01.loaded()[1] + return Decimal(str(labor.material_price(whole, code, None)["기준단가"])) + + +def _world_combo(world): # noqa: F811 + """STONE 1 + MAT 손 2 + WIRE 빈칸(안 셈) · NET 고름 4 = NET_ITEM — (키, 손셈 1 단위 값).""" + lines = [ + {"로직": STONE, "수량": {"손": 1}}, + {"로직": NET, "수량": {"손": 0}, "고름": {"4": NET_ITEM}}, + ] + mats = [{"코드": MAT, "수량": {"손": 2}}, {"코드": WIRE, "수량": None}] + with store.at(world["system"]): + key = refs.create("precombo", {"이름": "자재 조합", "담은로직": lines, "자재": mats})[ + "이름" + ] + stone = Decimal(str(labor.run_line(m01.loaded()[1], STONE, {})["기준"]["계"])) + return key, stone + _price(MAT) * 2 + + +def test_사전조합_값에_자재_금액_빈칸은_안_셈_안_줄에_자재(world): # noqa: F811 + key, want = _world_combo(world) + with store.at(world["system"]): + doc = labor.clean( + {"열": "rv", "담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}]} + ) + one = labor.calc(doc, {})["줄"][0] + assert one["기준"]["계"] == pytest.approx(float(want)) + assert one["반영"]["계"] == pytest.approx(float(want * 3)) + mats = [x for x in one["안"] if x.get("자재")] + assert [(x["자재"], x["수량"]) for x in mats] == [(MAT, 2)] # WIRE 빈칸 = 안 셈 + assert mats[0]["기준"]["재료비"] == pytest.approx(float(_price(MAT))) + # 목록 기준금액(51-4)도 같은 펼침 + rows = [{"종류": "precombo", "이름": key}] + refs.summaries(world["system"], rows) + assert rows[0]["기준금액"] == pytest.approx(float(want)) + + +def test_B08_참조_줄_재료에_사전조합_자재_손_곱(world): # noqa: F811 + from B08_Quantity.B08_Quantity_Router_Material import _ref_mats + + key, _ = _world_combo(world) + with store.at(world["system"]): + outer = refs.create( + "precombo", + {"이름": "겹", "담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}]}, + )["이름"] + got = _ref_mats(m01.loaded()[1], refs.read_ref, {**SYS, "키": outer}) + mats = [(m["품목"]["키"], q) for m, q in got if m.get("자재")] + assert mats == [(MAT, pytest.approx(6.0))] # 2 × 겹 손 3 · WIRE 빈칸 없음 + + +def test_B09_겹_대가에_자재_줄(world): # noqa: F811 + from B09_Estimation.B09_Estimation_Labor_Source import Builder + + key, _ = _world_combo(world) + ref = {**SYS, "키": key} + build = Builder() + bid = build.precombo(m01.loaded()[1], ref, refs.read_ref(ref), {}) + mats = [x for x in build.blocks[bid]["lines"] if x["kind"] == "material"] + assert [(x["key"], x["qty"]) for x in mats] == [(MAT, 2.0), (WIRE, None)] + assert mats[0]["name"] and mats[0]["unit"] + + +def test_사본_단가_묶음에_사전조합_자재_고름_사본_값_같음(world): # noqa: F811 + from M02_MasterTemplete import M02_Item_Prices as prices + from M02_MasterTemplete import M02_Item_Source as source + + key, want = _world_combo(world) + doc = {"열": "rv", "담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 1}}], "자재": []} + with store.at(world["system"]): + rows = source.labor_doc_rows(labor.clean(doc)) + mats = refs.frozen_materials(rows["참조사본"]) + assert {MAT, WIRE, NET_ITEM} <= set(mats) + assert "refs.frozen_materials(" in open(source.__file__, encoding="utf-8").read() + bundle = prices.to_json(prices.collect(refs.frozen_logics(rows["참조사본"]), mats)) + (world["system"] / "precombo" / f"{key}.json").unlink() # 원본이 사라져도 굳힌 사본 + got = prices.calc(rows, bundle, {}, {}, None)["줄"][0] + assert got["기준"]["계"] == pytest.approx(float(want), rel=1e-9) diff --git a/resources/tester/test_m02_labor_mat_marks.py b/resources/tester/test_m02_labor_mat_marks.py index e10707d17..e788663a8 100644 --- a/resources/tester/test_m02_labor_mat_marks.py +++ b/resources/tester/test_m02_labor_mat_marks.py @@ -125,7 +125,7 @@ def test_화면_칸_부품_사용자_줄_로직_줄_같은_칸(): assert "...markCells(mat, on?.mark" in rows and "...markHeads()," in rows assert "...markCells(" in _src("M02_MasterTemplete_Labor_LogicMats.ts") detail = _src("M02_MasterTemplete_Labor_Detail.ts") - assert "mark: lib ? undefined : markLogicMat" in detail + assert "mark: markLogicMat," in detail and "lib ? undefined" not in detail # 사전조합도(175-1) assert "mark: markMat" in detail # 서버 허용 값 = 화면 허용 값 api = _src("M02_MasterTemplete_Labor_Api.ts")