Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1

# Conflicts:
#	resources/tester/test_779_2_hopyo_marks.py
This commit is contained in:
2026-11-04 14:17:12 +09:00
9 changed files with 599 additions and 317 deletions
+7 -5
View File
@@ -41,7 +41,9 @@ def _make(client, folder, data, name):
assert res.status_code == 200, res.text
key = res.json()["키"]
fit.run_job(folder, key)
got = client.post(f"{URL.format('personal')}/{key}/sangeun")
url = f"{URL.format('personal')}/{key}/sangeun"
rows = client.post(url).json()["판정"] # 778-5 — 확인한 블록만(옮길 수 있음 모두 확인)
got = client.post(url, json={"확인": [{"번": r["번"]} for r in rows if not r["고를줄"]]})
assert got.status_code == 200, got.text
return key, got.json()
@@ -53,10 +55,8 @@ def test_불러오기_같은내용_SK_씀_바이트만_다른_사본_새_SK_0(cl
docs = _sks(folder)
fingers = [same.finger("sangeun", d) for d in docs.values()]
assert len(fingers) == len(set(fingers)) # 같은 내용 SK 한 벌
assert one["같은것씀"] and len(docs) + len(one["같은것씀"]) == len(one["만듦"])
assert one["만듦"] and len(docs) + len(one["같은것씀"]) == len(one["만듦"])
assert one["위층같음"] == []
shared = docs[one["같은것씀"][0]["키"]]
assert [x["키"].split(" ")[0] for x in shared["본뜬"]["또"]] == [hp1]
again = client.post(f"{URL.format('personal')}/{hp1}/sangeun").json()
assert again["만듦"] == [] and again["그대로"] == len(one["만듦"])
# 위 층(회사)에 같은 것 = 알림만
@@ -71,7 +71,9 @@ def test_불러오기_같은내용_SK_씀_바이트만_다른_사본_새_SK_0(cl
{k: x[k] for k in ("층", "주인", "키")} for x in two["위층같음"]
]
also = {x["키"].split(" ")[0] for d in _sks(folder).values() for x in d["본뜬"].get("또", [])}
assert also == {hp1, hp2}
assert also == (
{hp1, hp2} if one["같은것씀"] else {hp2}
) # 778-5 단위 SK = 파일 안 겹침 없을 수 있음
again = client.post(f"{URL.format('personal')}/{hp2}/sangeun").json()
assert again["만듦"] == [] and again["그대로"] == len(one["만듦"])
+10 -8
View File
@@ -72,7 +72,8 @@ def test_옛_HP_목록에서만_숨김(client):
assert old.is_file() and _keys(client) == ["HP-000008"]
def test_결과_단가산출_산근_블록_기초_SK_고름대기_겹침_없음(client):
def test_결과_단가산출_확인한_블록만_단위_SK_겹침_없음(client):
"""778-5 — 확인 없음 = 판정만 · 확인한 블록(옮길 수 있음)만 단위 SK · 같은 내용 한 벌 · 다시 = 그대로."""
folder = layers.personal_dir(7, 42)
assert _post(client).json()["키"] == "HP-000001"
url = URL.format("personal") + "/HP-000001/sangeun"
@@ -81,16 +82,17 @@ def test_결과_단가산출_산근_블록_기초_SK_고름대기_겹침_없음(
got = client.post(url)
assert got.status_code == 200, got.text
got = got.json()
wait = len(got["고름대기"])
assert got["블록"] > 0 and len(got["만듦"]) + wait == got["블록"] and got["그대로"] == 0
assert got["블록"] == len(got["판정"]) > 0 and got["만듦"] == [] and got["그대로"] == 0
good = [r["번"] for r in got["판정"] if not r["고를줄"]]
made = client.post(url, json={"확인": [{"번": n} for n in good]}).json()
assert sorted(x["번"] for x in made["만듦"]) == good and made["못만듦"] == []
docs = [json.loads(p.read_bytes()) for p in sorted((folder / "sangeun").glob("SK-*.json"))]
assert len(got["만듦"]) == got["기초"] + got["SK"]
assert len(docs) + len(got["같은것씀"]) == len(got["만듦"]) # 같은 내용 SK 는 한 벌(733-1)
assert len(docs) + len(made["같은것씀"]) == len(made["만듦"]) # 같은 내용 SK 는 한 벌(733-1)
one = docs[0]
assert one["바탕"]["로직"] and one["본뜬"]["키"].startswith("HP-000001 단산") # 바탕 없는 SK 0
assert one["구분"] == "실무자료" and one["단위"] and one["본뜬"]["갈래"] in ("기초", "SK")
assert one["바탕"] is None and one["본뜬"]["키"].startswith("HP-000001 단산") # 단위 SK
assert one["구분"] == "실무자료" and one["단위"] and one["본뜬"]["갈래"] == "단위"
again = client.post(url).json() # 다시 = 그대로(겹침 없음)
assert again["만듦"] == [] and again["그대로"] == len(got["만듦"])
assert again["만듦"] == [] and again["그대로"] == len(made["만듦"])
res = client.delete(URL.format("personal") + "/HP-000001").json()
assert res["만든"] == len(docs) # 같은 내용 SK 한 벌 — 사본 걷은 문서 수
assert len(list((folder / "sangeun").glob("SK-*.json"))) == len(docs) # 남음
+89 -168
View File
@@ -1,11 +1,12 @@
"""실무문서 단가산출 → 기초 · SK · 고름 대기 · 일위대가 이음 · 남은 줄 막음(PLAN 729-1 · 729-2 · 요구서 내역19).
"""실무문서 단가산출 → 줄 판정 · 확인한 블록만 단위 SK · 일위대가 이음 · 남은 줄 막음(PLAN 729-1 · 729-2 ·
778-5 D-3 · 요구 623).
북부청 임도(실물 엑셀)를 임시 층에 올려 맞춤셈 → [단가산출 만들기] →
기초잡석채집 · 운반 · 다짐(엑셀만 덤프트럭) · 철근가공조립 Type-Ⅰ · Type-Ⅱ · 구조물되메우기 = SK ·
SK 품 = 엑셀 품(차이는 엑셀 값 그대로) · 같은 단가산출 = 기초(덮개 없음) + 별칭 제안(짝 = 기초까지만) ·
고름 대기 = SK 안 만듦 → 고른 기초로 SK · 안 이은 줄 남으면 만들기 409(미리 보기 = 같은 줄) ·
사용자가 고르면 만듦 · 노임 = 이름 이음 · 기계 = EQ 이음 · master_data 원본 그대로. 층 = 임시 폴더.
729-4 = 단위 성격 다름(계수 · 고름 대기) · 못옮김 잇기 · 남은 못옮김 = 일위대가 막음.
북부청 임도(실물 엑셀)를 임시 층에 올려 맞춤셈 → [단가산출](확인 없음) = 판정만 · SK 0 →
기초잡석채집 · 운반 · 다짐 · 철근가공조립 Type-Ⅰ · Type-Ⅱ · 구조물되메우기 확인 = 바탕 없는 단위 SK(줄 = 엑셀 품 그대로 ·
인력 · 기계경비 로직 줄 · 율 줄 = 줄 목록 · 별도계상 철근 = 0 원) · 근거 꼬리표 = 맞춤셈 1위 ·
확인 안 한 단가산출 줄 = 일위대가 남은 줄(확인 전) · 확인한 블록 = SK 참조로 펼쳐 담음 ·
고를 줄 남은 블록(도장) = 못만듦 → 줄마다 고름 · 뺌 = 만듦 · 노임 = 이름 이음 · 기계 = EQ 이음 · master_data 원본 그대로.
층 = 임시 폴더.
"""
import json
@@ -13,38 +14,24 @@ 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_Sangeun as sangeun
from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit
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 = { # 블록 번 → (이름 앞, 엑셀 규격 글)
SAMPLES = { # 블록 번 → 이름 앞
33: "기초잡석채집,운반,다짐",
47: "철근가공조립(현장)", # Type-Ⅰ(소량 시공) — 철근공 4.2 · 보통인부 1.4
72: "철근가공조립(현장)", # Type-ll — 철근공 2.67 · 보통인부 1.02
47: "철근가공조립(현장)", # Type-Ⅰ(소량 시공) — 철근공 0.67 + 3.53 · 보통인부 0.22 + 1.18
72: "철근가공조립(현장)", # Type-ll
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")):
@@ -54,177 +41,111 @@ def _sk_docs(folder) -> dict[int, dict]:
return out
def test_실무_단가산출_기초_SK_고름대기_일위대가_막음(client):
def _caller(folder, key: str, no: int) -> int:
"""그 블록을 부르는 첫 호표 번."""
return sangeun.calls(hs.read(folder, key))[1][no]["_호표"]
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 품 = 엑셀 품
got = client.post(url, json={}).json()
assert got["만듦"] == [] and not _sk_docs(folder) # 확인 없음 = 판정만
rows = {r["번"]: r for r in got["판정"]}
assert all(rows[n]["판정"] == skunit.GOOD for n in SAMPLES)
sure = {"확인": [{"번": n} for n in SAMPLES]}
made = client.post(url, json=sure).json()
assert sorted(x["번"] for x in made["만듦"]) == sorted(SAMPLES) and made["못만듦"] == []
docs = _sk_docs(folder)
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
assert doc["이름"].startswith(head) and doc["바탕"] is None, no
assert doc["본뜬"]["갈래"] == skunit.UNIT and doc["본뜬"]["근거"] == rows[no]["근거"]
mine = [x["수량"] for x in doc["더한줄"] if x.get("종류") == "인력"] # 엑셀 품 그대로
excel = [r["매개값"]["품"] for r in rows[no]["줄"] if r["묶음"] == "L"]
assert mine == excel, no
assert docs[12]["더한줄"] == [
{
"종류": "인력",
"요소": "LB000002",
"비목": "노무비",
"이름": "보통인부",
"단위": "인",
"수량": 0.1,
"처음수량": 0.1,
}
]
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, "줄": []}
assert any("덤프트럭" in ours.eq_of(_eq(x))[0] for x in dump) # 기계 = 기계경비 로직 줄
rates = [x for x in docs[47]["더한줄"] if (x.get("고정") or {}).get("비율")]
assert [x["고정"]["비율"]["기준"] for x in rates] == [
["add:#1", "add:#2"],
["add:#5", "add:#6"],
]
assert (
docs[47]["더한줄"][0]["이름"] == "철근" and docs[47]["더한줄"][0]["고정"]
) # 별도계상 0 원
again = client.post(url, json={}).json() # 다시 = 그대로
assert again["만듦"] == [] and again["그대로"] == len(SAMPLES)
# 일위대가 — 확인 안 한 단가산출 줄 = 남은 줄(확인 전) · 확인한 블록 = SK 펼쳐 담음
no = _caller(folder, key, 12)
body = {"층": "personal", "키": key, "번": no, "줄": []}
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 any(x.get("단산") == 12 for x in 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)
marks = {(x.get("본뜬") or x.get("참조") or {}).get("키") for x in doc["담은로직"]}
assert docs[12]["키"] in marks # 단가산출 줄 = SK(바탕 없는 SK = 참조 그대로 · 740-1)
assert client.post(LEFT, json={**body, "줄": picks}).json()["수"] == 0
wait = _caller(folder, key, 2) # 확인 안 한 블록을 부르는 호표
rest = client.post(LEFT, json={"층": "personal", "키": key, "번": wait, "줄": []}).json()
assert any(x.get("까닭") == sangeun.WAIT_WHY for x in rest["남은줄"])
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 의 못옮김 = 응답 남은못옮김 · 잇기 뒤 사라짐."""
def test_고를_줄_남으면_못만듦_줄마다_고름_뺌(client):
"""778-5 — 도장(엑셀 자재 넷 = 글자 규격 · 후보) · 확인만 = 못만듦 · 줄마다 고름 · 뺌 = 만듦 ·
틀린 고름 400 · 고른 줄 꼬리표 「내가 고름」."""
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]}
rows = {r["번"]: r for r in client.post(url, json={}).json()["판정"]}
paint = rows[95]
picks = [r for r in paint["줄"] if r["상태"] != "이어짐"]
assert paint["판정"] == "고를 줄 4" and [r["묶음"] for r in picks] == ["M"] * 4
only = client.post(url, json={"확인": [{"번": 95}]}).json()
assert only["만듦"] == [] and only["못만듦"] == [
{"번": 95, "이름": paint["이름"], "까닭": "고를 줄 4"}
]
def mine(rows: list) -> list:
return [x for x in rows if x["차례"] == 2]
def sure(chosen: dict) -> object:
return client.post(url, json={"확인": [{"번": 95, "고름": chosen}]})
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["담은로직"])
first = picks[0]
assert sure({str(first["차례"]): {"기계": "EQ000001"}}).status_code == 400
assert sure({str(first["차례"]): {"자재": "MT999999"}}).status_code == 400
code = first["후보"][0]["자재"]
chosen = {str(r["차례"]): {"뺌": "현장 지급"} for r in picks[1:]}
done = sure({str(first["차례"]): {"자재": code}, **chosen}).json()
assert [x["번"] for x in done["만듦"]] == [95]
doc = _sk_docs(folder)[95]
mat = [x for x in doc["더한줄"] if x.get("종류") == "재료"]
assert [(x["요소"], x["수량"]) for x in mat] == [(code, 0.322)]
mine = next(r for r in done["판정"] if r["번"] == 95)["줄"]
assert all("내가 고름" in r["꼬리표"] for r in mine if r["차례"] in {p["차례"] for p in picks})
assert [r["상태"] for r in mine if r["차례"] in {p["차례"] for p in picks[1:]}] == ["뺌"] * 3
assert master_hash() == before
+172
View File
@@ -0,0 +1,172 @@
"""778-5 줄 잇기 한 벌 · 블록 = 줄 판정 모음 · 단위 SK(D-2 · D-3 · 요구 622 · 623 · 624).
- 줄 잇기(`M02_Hopyo_Units`) — 작업반장 = 건설업 · 숫자 규격 치수 차례 · 구분자 무관 · 글자 규격 = 고를 것 ·
손료율 = 자재 줄 방법 · 우리에 없는 율 항목(설치비 · 제경비) = 율 줄 · 여덟 파일 묶음별 수 굳힘
- 단위 SK(`M02_Hopyo_SkUnit` · `M02_Hopyo_Sangeun`) — 확인 안 한 블록 안 만듦 · 확인 블록 = 바탕 없는 SK +
근거 꼬리표 · 고를 줄 남으면 못만듦 · 고르면 만듦 · 다시 올려도 같은 SK 재사용 · 일위대가 = 확인 전 못 이음
층 = 임시 폴더 · M01 · 마스터 읽기만.
"""
from __future__ import annotations
import json
import pytest
from M02_MasterTemplete import M02_Hopyo_Fit as fit
from M02_MasterTemplete import M02_Hopyo_Material as hmat
from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun
from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit
from M02_MasterTemplete import M02_Hopyo_Store as hs
from M02_MasterTemplete import M02_Hopyo_Units as units
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete import M02_Template_Layers as layers
from resources.tester import helper_m02_hopyo_golden as golden
from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401
BUILD = "/api/m02/layers/personal/hopyo-build"
# 여덟 파일 불리는 블록 줄 연결 표(778-5 D-2 굳힘 · 바뀌면 까닭 커밋 글)
TABLE = {
"L": {"이어짐": 366, "고를 것": 0, "못 이음": 0},
"X": {"이어짐": 180, "고를 것": 10, "못 이음": 0},
"M": {"이어짐": 71, "고를 것": 55, "못 이음": 4},
"R": {"이어짐": 150, "고를 것": 2, "못 이음": 0},
"S": {"이어짐": 3, "고를 것": 0, "못 이음": 0},
"E": {"이어짐": 1, "고를 것": 0, "못 이음": 0},
}
ALL_LINKED = 156
@pytest.fixture(scope="module")
def us():
return units.Units()
def test_작업반장_건설업_기본(us):
got = us.line({"묶음": "L", "이름": "작업반장", "직종": "작업반장", "품": 0.1})
assert got["상태"] == units.LINKED and got["단위"] == {"노임": "LB000001"} # 건설업
assert got["꼬리표"] == ["기본 — 건설업 작업반장"] and got["후보"][0]["키"] == "LB000261"
one = us.line({"묶음": "L", "이름": "보통인부", "직종": "보통인부", "품": 0.5})
assert one["단위"] == {"노임": "LB000002"} and not one["꼬리표"]
def test_숫자_규격_차례_구분자_무관_글자_규격_후보(us):
assert hmat.dims("600×1200") == hmat.dims("1200x600 mm") == hmat.dims("1200*600㎜")
assert hmat.dims("1종2류(철재)") is None and hmat.lettered("1종2류(철재)")
assert not hmat.lettered("600*1200mm") and not hmat.lettered("#20, 0.9mm")
for spec in ("600×1200", "1200x600", "1200 X 600mm"):
got = us.line({"묶음": "M", "이름": "유로폼패널", "규격": spec, "단위": "매", "품": 1})
assert got["상태"] == units.LINKED and got["단위"] == {"자재": "MT000467"}, spec
named = us.line({"묶음": "M", "이름": "유로폼(패널)(600×1200)", "단위": None, "품": 1})
assert named["단위"] == {"자재": "MT000467"} # 「유로폼(패널)」 = 유로폼패널 · 단위 없는 EST 줄
paint = {"묶음": "M", "이름": "광명단조합페인트", "규격": "1종2류(철재)", "단위": "L", "품": 1}
got = us.line(paint)
assert got["상태"] == units.PICK and got["글"] == units.LETTERS and got["후보"]
def test_손료율_자재_줄_율_항목_율_줄(us):
wear = us.line({"묶음": "M", "이름": "유로폼패널", "규격": "600×1200", "품": 0.267,
"손료율": 0.267}) # fmt: skip
assert wear["방법"] == units.WEAR and wear["상태"] == units.LINKED
setup = {"묶음": "R", "이름": "설치비(재료비의 5%)", "품": 0.05, "기준꼴": "자재"}
assert us.line(setup)["단위"] == {"기준": "재료", "비목": "재료비"} # 우리에 없는 율 항목
rest = {"묶음": "R", "이름": "제경비(노무비의 60%)", "품": 0.6, "기준꼴": "노무비"}
assert us.line(rest)["단위"] == {"기준": "노무비", "비목": "경비"}
lost = us.line({"묶음": "R", "이름": "기구손료(노무비의 9%)", "기준": "노무비", "품": 0.09})
assert lost["상태"] == units.PICK and lost["후보"] == [{"기준": "노무비", "비목": "경비"}]
# 단위 SK 셈 — 설치비 = 재료 줄 × 5 % · 제경비 = 노무비 × 60 %
block = {"항목": [
{"묶음": "L", "이름": "보통인부", "직종": "보통인부", "품": 0.5, "행": 1},
{"묶음": "M", "이름": "유로폼패널", "규격": "600×1200", "단위": "매", "품": 2, "행": 2},
{**setup, "행": 3}, {**rest, "행": 4},
]} # fmt: skip
got = skunit.judge(block, us, lambda sub: None, None, "㎡")
assert got["판정"] == skunit.GOOD and [r["상태"] for r in got["줄"]] == [units.LINKED] * 4
labor_, mat = (got["줄"][0]["우리금액"], got["줄"][1]["우리금액"])
assert got["줄"][2]["우리금액"] == pytest.approx(mat * 0.05, abs=0.1)
assert got["줄"][3]["우리금액"] == pytest.approx(labor_ * 0.6, abs=0.1)
doc = {"키": "SK-000001", "이름": "t", "단위": "㎡", "바탕": None, "더한줄": got["더한줄"]}
whole = us.whole
ran = labor._json(calc.run(sd.clean(doc, None, whole), whole))
assert ran["1단위"]["계"] == pytest.approx(got["우리금액"])
def test_여덟_파일_줄_연결_표(us):
rows, full, blocks = [], 0, 0
for tag in golden.FILES:
hp = golden.read(tag)
_, first = sangeun.calls(hp)
for b in hp["단산"]:
if int(b["번"]) not in first:
continue
got = [(o["묶음"], us.line(o)["상태"]) for o in b.get("항목") or [] if not o.get("밖")]
rows += got
blocks += 1
full += all(s == units.LINKED for _, s in got)
assert units.summary(rows) == TABLE and (full, blocks) == (ALL_LINKED, 206)
def _sks(folder) -> dict[str, dict]:
return {p.stem: json.loads(p.read_bytes()) for p in (folder / "sangeun").glob("SK-*.json")}
def test_확인한_블록만_단위_SK_고름_재사용(client):
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)
got = client.post(url, json={}).json()
assert got["만듦"] == [] and not _sks(folder) # 확인 안 한 블록 = 안 만듦
assert len(got["판정"]) == got["블록"] == 49 # 불리는 블록
rows = {r["번"]: r for r in got["판정"]}
good = [n for n, r in rows.items() if r["판정"] == skunit.GOOD and r["근거"]]
pick = next(n for n, r in rows.items() if r["고를줄"])
assert rows[pick]["판정"] == f"고를 줄 {rows[pick]['고를줄']}"
one = rows[good[0]]
assert all("우리금액" in x and "차" in x for x in one["줄"] if x["상태"] == units.LINKED)
# 확인 = 그 블록만 · 고를 줄 남은 블록 = 못만듦
made = client.post(url, json={"확인": [{"번": good[0]}, {"번": pick}]}).json()
assert [x["번"] for x in made["만듦"]] == [good[0]] and made["못만듦"][0]["번"] == pick
docs = _sks(folder)
doc = docs[made["만듦"][0]["키"]]
assert doc["바탕"] is None and doc["본뜬"]["갈래"] == skunit.UNIT and doc["더한줄"]
assert doc["본뜬"]["근거"]["로직"] == one["근거"]["로직"] and doc["본뜬"]["못옮김"] == []
# 고르면 만듦(못 이은 줄마다 첫 후보 · 후보 없으면 뺌)
picks = {}
for x in rows[pick]["줄"]:
if x["상태"] in (units.PICK, units.NONE):
c = (x["후보"] or [{}])[0]
k = c.get("키") or c.get("자재")
field = {"L": "노임", "X": "기계", "M": "자재"}.get(x["묶음"])
picks[str(x["차례"])] = {field: k} if k and field else c or {"뺌": "현장"}
again = client.post(url, json={"확인": [{"번": pick, "고름": picks}]}).json()
assert [x["번"] for x in again["만듦"]] == [pick] and again["그대로"] == 1
mine = next(r for r in again["판정"] if r["번"] == pick)
assert all("내가 고름" in x["꼬리표"] for x in mine["줄"] if str(x["차례"]) in picks)
bad = {"확인": [{"번": pick, "고름": {"0": {"노임": "XX"}}}]}
assert client.post(url, json=bad).status_code == 400
assert client.post(url, json={"확인": [{"번": 9999}]}).status_code == 404
# 일위대가 만들기 — 확인 안 한 단가산출 줄 = 못 이음(확인 전)
calls = {}
for h in hs.read(folder, key)["호표"]:
for x in h.get("줄") or []:
ref = x.get("참조") or {}
if ref.get("종류") == "단산":
calls.setdefault(int(ref["번"]), h["번"])
wait = next(n for n in sorted(calls) if n not in (good[0], pick))
res = client.post(BUILD, json={"층": "personal", "키": key, "번": calls[wait], "줄": []})
left = [x for x in res.json()["detail"]["남은줄"] if x.get("까닭") == sangeun.WAIT_WHY]
assert res.status_code == 409 and left
# 다시 올림(끝내기 뒤) = 같은 내용 SK 재사용
n = len(_sks(folder))
client.delete(f"{URL.format('personal')}/{key}")
key2 = _post(client).json()["키"]
fit.run_job(folder, key2)
url2 = f"{URL.format('personal')}/{key2}/sangeun"
re_ = client.post(url2, json={"확인": [{"번": good[0]}]}).json()
assert re_["같은것씀"] == [{"번": good[0], "키": made["만듦"][0]["키"]}]
assert len(_sks(folder)) == n
assert master_hash() == before
+4 -2
View File
@@ -66,8 +66,10 @@ def test_marks_읽기만_판정_캐시_같음_SK(client):
assert all(r["SK"] is None and r["갈래"] is None and r["못옮김"] == [] for r in rows)
# 778-5 D-6 — 엑셀 셈 어긋남 덧칸(불리는 블록) · 2 · 33 = 엑셀 합이 줄 뺌
assert {r["번"] for r in rows if r["어긋남"]} == {2, 33, 64, 95}
# [단가산출 만들기] 뒤 = SK · 갈래 · 못옮김 실림
made = client.post(f"{base}/{KEY}/sangeun")
# [단가산출 만들기] 뒤 = SK · 갈래 · 못옮김 실림(778-5 — 확인한 블록만)
rows = client.post(f"{base}/{KEY}/sangeun").json()["판정"]
sure = [{"번": r["번"]} for r in rows if not r["고를줄"]]
made = client.post(f"{base}/{KEY}/sangeun", json={"확인": sure})
assert made.status_code == 200, made.text
after = {r["번"]: r for r in client.get(f"{base}-marks/{KEY}").json()["블록"]}
for one in made.json()["만듦"]:
+12
View File
@@ -67,6 +67,17 @@ def _build(client, key: str, no: int, lines: list, decide: bool = False, **extra
return res.json()
def _sure(client, key: str, hopyo: dict, body: list) -> None:
"""778-5 — 체크 안 한 단가산출 줄의 블록을 [단가산출]에서 확인(자동 없음 · 옮길 수 있는 것만)."""
picked = {x["차례"] for x in body}
want = {int(x["참조"]["번"]) for x in hopyo["줄"]
if (x.get("참조") or {}).get("종류") == "단산" and x["차례"] not in picked} # fmt: skip
url = f"{URL.format('personal')}/{key}/sangeun"
rows = client.post(url).json()["판정"]
sure = [{"번": r["번"]} for r in rows if r["번"] in want and not r["고를줄"]]
assert client.post(url, json={"확인": sure}).status_code == 200
def _doc(ref: dict) -> dict:
path = layers.personal_dir(*FOLDER) / "precombo" / f"{ref['키']}.json"
return json.loads(path.read_text(encoding="utf-8"))
@@ -95,6 +106,7 @@ def test_끝까지_확인한_줄만_별칭(client):
two = [{k: p[k] for k in ("로직", "입력값", "배")} for p in rebar["맞춤"]["대상"]]
assert {p["로직"] for p in two} == {"GC000206", "GC000207"} # 다른 점 → 사용자가 체크
body = body + [{"차례": rebar["차례"], "대상": two}]
_sure(client, "HP-000001", hopyo, body)
made = _build(client, "HP-000001", no, body)
national = [x for x in body if any(t.get("전국") for t in x.get("대상", []))] # 361-1
assert national and all(x["대상"] == [{"전국": True, "배": 1}] for x in national)