"""실무문서 단가산출 → 기초 · SK · 고름 대기 · 일위대가 이음 · 남은 줄 막음(PLAN 729-1 · 729-2 · 요구서 내역19). 북부청 임도(실물 엑셀)를 임시 층에 올려 맞춤셈 → [단가산출 만들기] → 기초잡석채집 · 운반 · 다짐(엑셀만 덤프트럭) · 철근가공조립 Type-Ⅰ · Type-Ⅱ · 구조물되메우기 = SK · SK 품 = 엑셀 품(차이는 엑셀 값 그대로) · 같은 단가산출 = 기초(덮개 없음) + 별칭 제안(짝 = 기초까지만) · 고름 대기 = SK 안 만듦 → 고른 기초로 SK · 안 이은 줄 남으면 만들기 409(미리 보기 = 같은 줄) · 사용자가 고르면 만듦 · 노임 = 이름 이음 · 기계 = EQ 이음 · master_data 원본 그대로. 층 = 임시 폴더. 729-4 = 단위 성격 다름(계수 · 고름 대기) · 못옮김 잇기 · 남은 못옮김 = 일위대가 막음. """ import json from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Hopyo_Fit as fit from M02_MasterTemplete import M02_Hopyo_Fixed as hfix from M02_MasterTemplete import M02_Hopyo_Ledger as ledger from M02_MasterTemplete import M02_Hopyo_Option as option from M02_MasterTemplete import M02_Hopyo_Ours as ours from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch from M02_MasterTemplete import M02_Hopyo_Store as hs from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Sangeun_Calc as calc from M02_MasterTemplete import M02_Template_Layers as layers from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401 BUILD = "/api/m02/layers/personal/hopyo-build" LEFT = "/api/m02/layers/personal/hopyo-unlinked" SAMPLES = { # 블록 번 → (이름 앞, 엑셀 규격 글) 33: "기초잡석채집,운반,다짐", 47: "철근가공조립(현장)", # Type-Ⅰ(소량 시공) — 철근공 4.2 · 보통인부 1.4 72: "철근가공조립(현장)", # Type-ll — 철근공 2.67 · 보통인부 1.02 12: "구조물되메우기(인력100%)", } def _same(a: float, b: float) -> bool: return abs(a - b) <= 1e-9 or option.near(a, b, fit.TOL[0], option.ru(a)) def _by_type(rows: list[dict]) -> dict[str, float]: out: dict[str, float] = {} for x in rows: k = "+".join(sorted(x["갈래"])) out[k] = out.get(k, 0.0) + x["시간"] return out def _sk_docs(folder) -> dict[int, dict]: out = {} for path in sorted((folder / "sangeun").glob("SK-*.json")): doc = json.loads(path.read_bytes()) for one in [doc["본뜬"], *doc["본뜬"].get("또", [])]: # 또 = 같은 내용 SK 를 쓴 블록(733-1) out[int(one["키"].rsplit("단산", 1)[1])] = doc return out def test_실무_단가산출_기초_SK_고름대기_일위대가_막음(client): before = master_hash() folder = layers.personal_dir(7, 42) key = _post(client).json()["키"] url = f"{URL.format('personal')}/{key}/sangeun" assert client.post(url).status_code == 409 # 맞춤 셈 전 fit.run_job(folder, key) got = client.post(url, json={}) assert got.status_code == 200, got.text got = got.json() assert ( got["기초"] + got["SK"] + len(got["고름대기"]) == got["블록"] == len(got["만듦"]) + len(got["고름대기"]) ) assert got["SK"] > 0 and got["기초"] > 0 and got["고름대기"] docs, whole = _sk_docs(folder), m01.loaded()[1] assert all(d["바탕"] and d["바탕"]["로직"] for d in docs.values()) # 바탕 없는 SK 0 assert not set(docs) & {x["번"] for x in got["고름대기"]} # SK 품 = 엑셀 품 for no, head in SAMPLES.items(): doc = docs[no] assert doc["이름"].startswith(head) and doc["본뜬"]["갈래"] == "SK", (no, doc["본뜬"]) excel = doc["본뜬"]["엑셀품"] with store.at(folder): ours_q = skpatch.measure(doc, whole) assert set(ours_q["인력"]) == set(excel["인력"]), no assert all(_same(q, ours_q["인력"][t]) for t, q in excel["인력"].items()), no theirs, mine = _by_type(excel["장비"]), _by_type(ours_q["장비"]) assert set(theirs) == set(mine), no assert all(_same(q, mine[t]) for t, q in theirs.items()), no dump = [x for x in docs[33]["더한줄"] if x.get("로직") == "GC000268"] assert any("덤프트럭" in ours.eq_of(_eq(x))[0] for x in dump) # 엑셀만 장비 = 더한줄 assert docs[12]["덮개"]["labor:LB000002#0"]["qty"] == 0.1 # 엑셀 값 그대로(우리 0.01) assert docs[47]["더한줄"][0]["로직"] == "GC000206" # 조합 둘째 = 더한줄 기초 # 같은 단가산출 = 기초 + 별칭 제안(짝 = 기초까지만) base = next(d for d in docs.values() if d["본뜬"]["갈래"] == "기초") assert base["덮개"] == {} and base["더한줄"] == [] and base["뺀줄"] == [] book = ledger.read(folder) hp = hs.read(folder, key) lines = { int(x["참조"]["번"]): x for h in reversed(hp["호표"]) for x in reversed(h["줄"]) if (x.get("참조") or {}).get("종류") == "단산" } no = int(base["본뜬"]["키"].rsplit("단산", 1)[1]) pair = book[ledger.key(lines[no])]["짝"] assert pair == [{"로직": base["바탕"]["로직"], "입력값": base["입력값"], "배": 1, "몫": None}] assert book[ledger.key(lines[12])]["짝"][0]["로직"] == docs[12]["바탕"]["로직"] assert got["별칭"] > 0 # 다시 = 그대로 · 고름 대기 = 고른 기초로 SK again = client.post(url, json={}).json() assert again["만듦"] == [] and again["그대로"] == len(docs) wait = got["고름대기"][0] code = (wait["후보"] or [{"로직": "GF000113"}])[0]["로직"] picked = client.post(url, json={"바탕": {str(wait["번"]): {"로직": code}}}).json() assert [x["번"] for x in picked["만듦"]] == [wait["번"]] assert _sk_docs(folder)[wait["번"]]["바탕"]["로직"] == code assert client.post(url, json={"바탕": {"9999": {"로직": code}}}).status_code == 404 assert client.post(url, json={"바탕": {str(wait["번"]): {"로직": "XX"}}}).status_code == 400 # 안 이은 줄 남으면 만들기 막힘 · 미리 보기 = 같은 줄 · 고르면 만듦 body = {"층": "personal", "키": key, "번": 1, "줄": []} left = client.post(LEFT, json=body).json() res = client.post(BUILD, json=body) assert res.status_code == 409 and left["수"] > 0 assert res.json()["detail"]["남은줄"] == left["남은줄"] assert not list((folder / "precombo").glob("SC-*.json")) # 막힘 = 저장 0 picks = [{"차례": x["차례"], "뺌": "우리에 없음"} for x in left["남은줄"]] ok = client.post(BUILD, json={**body, "줄": picks}) assert ok.status_code == 200, ok.text doc = json.loads((folder / "precombo" / f"{ok.json()['참조']['키']}.json").read_bytes()) refs_sk = [x for x in doc["담은로직"] if str((x.get("본뜬") or {}).get("키")).startswith("SK")] assert refs_sk # 단가산출 줄 = SK 펼쳐 담음(740-1 · 로직 줄 + 패치 · 본뜬 = 그 SK) assert client.post(LEFT, json={**body, "줄": picks}).json()["수"] == 0 assert master_hash() == before def test_단위_성격_다름_계수_못옮김_잇기_남으면_일위대가_막음(client): """729-4 — 728-2 단위 환산 뒤 500 0 · 성격 다른 단위 = 계수 거꾸로(자신 있을 때) · 아니면 고름 대기 (후보 계수 칸) · 못옮김 잇기 = SK 더한줄 · 뺌 = 놓음 · 남으면 그 SK 를 쓰는 일위대가 막음.""" before = master_hash() folder, whole = layers.personal_dir(7, 42), m01.loaded()[1] key = _post(client).json()["키"] url = f"{URL.format('personal')}/{key}/sangeun" fit.run_job(folder, key) got = client.post(url, json={}) assert got.status_code == 200, got.text got = got.json() wait = {x["번"]: x for x in got["고름대기"]} assert wait[30]["까닭"].startswith(skpatch.UNIT_WHY) # 큰돌채집운반 ㎡ ↔ 100개 assert wait[30]["후보"][0]["계수"] == {"가": "100개", "나": "㎡", "갈래": "사용자"} docs = _sk_docs(folder) coef = [d for d in docs.values() if d.get("환산")] assert coef # 계수 거꾸로 = 셈 됨 · 엑셀 품 그대로 for d in coef: with store.at(folder): mine = skpatch.measure(d, whole) assert all(_same(q, mine["인력"][t]) for t, q in d["본뜬"]["엑셀품"]["인력"].items()) lost = {x["번"]: x["못옮김"] for x in got["만듦"] if x["못옮김"]} a, b = lost[95] # 도장 — 엑셀만 자재 둘 assert a["갈래"] == "자재" and a["id"] != b["id"] and a["수량"] == 0.322 hp = hs.read(folder, key) no = next(h["번"] for h in hp["호표"] for x in h["줄"] if (x.get("참조") or {}).get("종류") == "단산" and int(x["참조"]["번"]) == 95) # fmt: skip body = {"층": "personal", "키": key, "번": no, "줄": []} def left() -> list: return [x for x in client.post(LEFT, json=body).json()["남은줄"] if x.get("단산") == 95] assert [len(x["못옮김"]) for x in left()] == [2] assert client.post(BUILD, json=body).status_code == 409 def link(*rows: dict): return client.post(url, json={"바탕": {"95": {"잇기": list(rows)}}}) assert link({"id": "자재:없음", "자재": "MT000010"}).status_code == 400 assert link({"id": a["id"], "기계": "EQ000001"}).status_code == 400 assert link({"id": a["id"], "자재": "MT999999"}).status_code == 400 one = link({"id": a["id"], "자재": "MT000010"}).json() assert [x["못옮김"] for x in one["만듦"]] == [[b]] doc = _sk_docs(folder)[95] tb = float(calc.ratio_of(doc, whole.logic(doc["바탕"]["로직"]), whole)) add = doc["더한줄"][-1] assert add["종류"] == "재료" and add["요소"] == "MT000010" and _same(add["수량"], 0.322 * tb) assert [len(x["못옮김"]) for x in left()] == [1] two = link({"id": a["id"], "자재": "MT000010"}, {"id": b["id"], "뺌": "현장 지급"}).json() assert two["만듦"][0]["못옮김"] == [] assert _sk_docs(folder)[95]["본뜬"]["놓음"][0]["id"] == b["id"] assert left() == [] assert master_hash() == before def test_별칭_체크_줄_못옮김_SK_남은줄_그대로_SK_남은못옮김(client): """729-5 — 확인한 로직 줄이 못옮김 남은 차이 SK 의 바탕이면 남은 줄(차이 잃지 않음) · 만들기 409 · 그대로 SK 의 못옮김 = 응답 남은못옮김 · 잇기 뒤 사라짐.""" before = master_hash() folder = layers.personal_dir(7, 42) key = _post(client).json()["키"] url = f"{URL.format('personal')}/{key}/sangeun" fit.run_job(folder, key) made = {x["번"]: x for x in client.post(url, json={}).json()["만듦"]} sk = made[60] # 제27호표 2째 줄 와이어메시바닥깔기 — SK(바탕 GC000665) · 엑셀만 와이어메쉬 assert sk["갈래"] == "SK" and len(sk["못옮김"]) == 1 check = {"차례": 2, "대상": [{"로직": "GC000665", "배": 1}]} # 별칭 자동 체크와 같은 꼴 body = {"층": "personal", "키": key, "번": 27, "줄": [check]} def mine(rows: list) -> list: return [x for x in rows if x["차례"] == 2] left = client.post(LEFT, json=body).json()["남은줄"] assert [(x["단산"], x["SK"], x["못옮김"]) for x in mine(left)] == [(60, sk["키"], sk["못옮김"])] res = client.post(BUILD, json=body) assert res.status_code == 409 and mine(res.json()["detail"]["남은줄"]) == mine(left) # 그대로 SK 의 못옮김 = 응답 남은못옮김(만듦[].못옮김 과 같은 꼴) again = client.post(url, json={}).json() rest = {x["번"]: x for x in again["남은못옮김"]} assert again["만듦"] == [] and set(rest) == {n for n, x in made.items() if x["못옮김"]} assert rest[60] == {"번": 60, "이름": sk["이름"], "키": sk["키"], "못옮김": sk["못옮김"]} cut = {"id": sk["못옮김"][0]["id"], "자재": "MT000010"} # 이음 = SK 더한줄(차이 남음) linked = client.post(url, json={"바탕": {"60": {"잇기": [cut]}}}).json() assert [(x["번"], x["키"], x["못옮김"]) for x in linked["만듦"]] == [(60, sk["키"], [])] assert 60 not in {x["번"] for x in client.post(url, json={}).json()["남은못옮김"]} left = client.post(LEFT, json=body).json()["남은줄"] assert mine(left) == [] picks = [{"차례": x["차례"], "뺌": "우리에 없음"} for x in left] ok = client.post(BUILD, json={**body, "줄": [check, *picks]}) assert ok.status_code == 200, ok.text doc = json.loads((folder / "precombo" / f"{ok.json()['참조']['키']}.json").read_bytes()) # 차이 SK = 펼쳐 담음(740-1 · 본뜬 = 그 SK) assert any((x.get("본뜬") or {}).get("키") == sk["키"] for x in doc["담은로직"]) assert master_hash() == before def _eq(one: dict) -> str: return str((one.get("입력값") or {}).get("기계") or "") def test_노임_이름_이음_기계_EQ_이음_사용자_이음(client): with store.at(layers.personal_dir(7, 42)): linker = hfix.Linker(by_name=True) labor = {"갈래": "노임", "이름": "보통인부", "단위": "인"} price = {"노무비": hfix.Decimal(1), "재료비": hfix.Decimal(0), "경비": hfix.Decimal(0)} assert linker.link(labor, price)["노임"] == "LB000002" # 단가 달라도 이름으로 machine = {"갈래": "기계", "이름": "덤프트럭", "규격": "15ton", "단위": "hr"} code = linker.link(machine, price)["기계"] assert code and "덤프트럭" in ours.eq_of(_eq_code(code))[0] line = {"갈래": "자재", "이름": "없는자재", "규격": "", "단위": "개", "수량": 2, "단가": {"재료비": 10}, "차례": 0} # fmt: skip one = hfix.line_of(line, [], {"파일": "x", "호표": 1}, set(), linker, {"노임": "LB000002"}) assert one["고정"]["이음"] == {"노임": "LB000002", "자재": None, "기계": None} assert hfix.linked(one) and not hfix.linked(hfix.line_of(line, [], {}, set(), linker)) def _eq_code(key: str) -> str: """EQ 키(EQ000001) → 기계 번호(0101-0007).""" return next(k for k, v in m01.loaded()[1].numbers["EQ"].items() if v.get("키") == key)