Files
Aislo/resources/tester/test_729_hopyo_sk_link.py
T
eomsangdonandClaude Opus 5.5 4c8a06559a feat(740-1): 단가산출 · 일위대가 서로 독립 — 넣기 펼침(M01 키 줄 + 덮개 사본 · 본뜬) · 초기화 길 · 옮기기 함수 · B09 덮개 줄 굳힘
- 새 M02_Library_Flat: SK 에 SK = 더한줄 로직 한 줄에 원본 몸 통째(덮개 · 뺀줄 · 고름 · 더한줄 · % 덧줄까지 같은 금액) · SC 에 SK = 로직 줄 + 패치 · SK 에 SC = 줄마다 더한줄(수량 = 저장 수량.손 › 실무수량 › 1) · 바탕 SK · 패치 SK 참조 = 덮개 겹침 · 줄 본뜬 {층, 주인, 키, 이름, 때}
- 저장 · 읽기 · 층 복사 때 서버가 펼침(못 펼친 줄 = 옛 참조 그대로) · SC 안 SC(하위 호표)는 저장 · 읽기에서 참조 그대로(엑셀 하위 호표 끝수 · B09 하위 대가) · 찾아 넣기 길만 펴 넣음
- 길: POST {kind}-flat(넣을 줄 · 못 펼침 400) · POST {kind}-reset(미리 보기 걷힐 수 · 씀)
- 초기화 = 덮개 · 뺀줄 · 더한줄 단가 걷음(안쪽 넣은 몸까지) · 항목 · 수량 · 입력값 · 고름 남김
- 옮기기 = migrate(apply) · 전후 1 단위 금액 같을 때만 씀 · 공유 storage 는 브레인
- 둘째 기초 세부 셈 = 넣은 SK 몸이면 이름 없는 SK 로(Sangeun_Calc.sub_run)
- B09: SC 사본 덮개 줄 = 굳힌 줄(굳힌 {키, 줄 …}) → 사본 셈 · 대가 블록이 덮개를 읽음(옛 산근 길은 덮개 무시 · 실측 6,451 대 M02 210,866)
- 733 지문 = 옛 참조 줄 펼친 꼴로 대조
- 낡은 시험 고침(까닭 = 단가산출이 낀 참조가 이제 펼쳐 담김): 715 store(바탕 · 사전조합 SK · 층 복사) · 717-2a 목록 바탕 · 717-4 SC 안 SK · 719-1 원본판(원본 고쳐도 그대로) · 719-2 속 일위대가(빠진 줄 없음 · 쓰지 않는 _calc_given 지움) · 725-1 SK 안 SC · 729 SK 줄 본뜬 · 733-3 쓰는 곳 = 구조물 조합(SK 안 SK 참조 없음) · 572 지문 대조 대상

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 19:47:33 +09:00

254 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""실무문서 단가산출 → 기초 · 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)