feat(778-5): 블록 = 줄 판정 모음 · 확인한 블록만 단위 SK(바탕 없음 · 근거 꼬리표) · 확인 목록 받는 길(D-3 · 요구 623 · 624)

- 새 M02_Hopyo_SkUnit: 줄 판정(Units 잇기 + 매개값 · 사용자 고름 {차례: 노임|기계|자재|기준|뺌} · 꼬리표 「내가 고름」) · 단가 = M01 지금 값으로 다시 셈(우리금액) · 엑셀 금액 차 = 참고 · 블록 판정 「옮길 수 있음」 | 「고를 줄 n」 · 단위 SK 더한줄 = 인력 · 기계경비 로직 줄 · 재료(손료율 든 수량 · 별도계상) · 율 줄(778-4 꼴 · 줄 목록 = 그 행 add:#k) · 하위 참조 · 값 고정 줄 · 저장 꼴로 다듬어 같은 내용 SK 지문 같음
- Sangeun: 기초 · SK · 고름 대기 갈림(SkPatch.plan) 걷고 단위 SK 하나 · 확인 [{번, 고름?}] 에 든 블록만 만듦 · 하위 단산 먼저 · 고를 줄 남으면 못만듦 · 일위대가 만들기가 부르는 Maker.plan(번) = 「확인 전」 고름 대기(자동 없음) · 별칭 제안 걷음(바탕 기초 없음)
- Router: sangeun 몸 바탕 → 확인(줄 수 그대로)
- 시험 새 뜻: test_729(기초 · SK · 고름 대기 · 계수 · 못옮김 잇기 · 729-5 차이 SK 바탕 길 = 바탕 없는 단위 SK 로 사라짐 → 확인 블록 단위 SK · 줄 고름 · 뺌으로 갈음) · test_718_1 · test_572 · test_779_2 · test_m02_hopyo_flow(확인 목록을 보내야 SK 생김 · 같은 파일 안 같은 내용 단위 SK 없을 수 있음 · 바탕 없는 SK = SC 에 참조 그대로)
- 새 test_778_5: 작업반장 = 건설업 · 규격 차례 무관 · 글자 규격 = 후보 · 손료율 · 설치비 · 제경비 율 줄 · 여덟 파일 줄 연결 표 · 확인 안 한 블록 안 만듦 · 고름 · 400 · 404 · 다시 올림 같은 SK 재사용 · 일위대가 확인 전 못 이음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 14:16:12 +09:00
co-authored by Claude Opus 5.5
parent 215cc1531d
commit 4cdb41f80b
9 changed files with 599 additions and 317 deletions
+110 -129
View File
@@ -1,24 +1,16 @@
"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 729-1 · 요구서 내역19 1장 7 · 3장 6) —
산근(단산) 블록마다 매칭 → 비교 → 기초 · SK · 고름 대기.
"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 729-1 · 778-5 D-3 · 요구 621 · 623) — 산근(단산) 블록마다
줄 판정 모음(`M02_Hopyo_SkUnit`) → 사용자가 확인한 블록만 단위 SK.
블록마다 그 블록을 부르는 첫 엑셀 줄(「단산N참조」)의 맞춤셈(`M02_Hopyo_Fit` 캐시 · 없으면 그 블록만
셈) → `M02_Hopyo_SkPatch.plan`:
기초 = SK 바탕 = 1위 기초 · 입력값 · 덮개 없음 · SK = 바탕 기초 + 덮개 · 더한줄 · 뺀줄(엑셀 품 그대로) ·
고름 대기 = SK 안 만듦(바탕 없는 SK 로 몰래 채우지 않음) · 후보 다섯(성격 다른 단위 = `계수` 칸 뜻) →
사용자가 고른 기초 `바탕 {번: {로직?, 입력값?, 환산?, 잇기?}}` 로 다시 부르면 그 기초에 견줘 SK(이미 있으면
그 키에 덮음) · 잇기 `[{id, 노임|기계|자재: 키} | {id, 뺌}]` = 못옮김 → SK 더한줄 · 남은 못옮김 = 응답
`만듦[].못옮김` · 본뜬 그대로(그 SK 를 쓰는 일위대가 만들기 = 남은 줄 409 · `M02_Hopyo_SkLink`) ·
그대로 SK 의 못옮김 = 응답 `남은못옮김 [{번, 이름, 키, 못옮김}]` · 잇기만 보냈는데 1위 기초가 고름 대기면
그 SK 바탕(로직 · 입력값 · 환산)으로 다시 셈(729-5).
모두 줄로만(바탕 기초 안 씀 · 맞춤셈 1위 기초 = 근거 꼬리표만) · 자동 없음 — `확인 [{번, 고름?: {차례: 고름}}]`
(블록마다 [확인] · [옮기기])에 든 블록만 만듦 · 확인 안 한 블록 = 판정만(안 만듦) · 줄 다 이어지지 않은 확인 블록 =
`못만듦` · 하위 단산 줄 = 그 블록 SK 참조(확인 목록에 있으면 먼저 만듦 · 없으면 못만듦).
SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 ·
본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래, 엑셀품, 못옮김} · 같은 본뜬(키 · 원본)이
이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층.
바탕 없음 · 본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래 「단위」, 근거 {로직, 이름}, 못옮김 []} ·
같은 본뜬(키 · 원본)이 이 층에 있으면 그 키에 덮음 · 만드는 층 = 부르는 층.
같은 내용(733-1 · `M02_Library_Same` 지문) SK 가 이 층에 있으면 새로 안 만들고 그것을 씀 — 이름 = 먼저 만든 것 ·
뒤 출처 = 그 SK `본뜬.또 [그 블록 본뜬 + 이름]`(`made` · `origin_of` 가 읽음 · 갈래 · 못옮김은 출처마다) ·
다른 출처가 쓰는 SK 는 내용을 안 바꿈(고른 기초 · 잇기로 바뀌면 떼고 다시 찾음) · 응답 `같은것씀 [{번, 키}]` ·
위 층(회사 · 마스터) 같은 것 = 응답 `위층같음 [{번, 층, 주인, 키}]`(알림만 · 연결 안 함).
별칭 제안 = 기초 · SK 만든 블록의 엑셀 줄 → 이 층 별칭 장부 짝 = 바탕 기초까지만(차이 SK 는 이 층 문서 ·
요구서 3장 6) · 장부에 이미 짝 · 갈래가 있는 줄은 그대로 · 승인 길 = M01 별칭 장부(716).
뒤 출처 = 그 SK `본뜬.또 [그 블록 본뜬 + 이름]`(`made` · `origin_of` 가 읽음) · 다른 출처가 쓰는 SK 는 내용을
안 바꿈(바뀌면 떼고 다시 찾음) · 응답 `같은것씀 [{번, 키}]` · 위 층 같은 것 = `위층같음 [{번, 층, 주인, 키}]`(알림만).
일위대가 만들기(`M02_Hopyo_SkLink`)가 부르는 `Maker.plan(번)`(확인 없음) = 고름 대기 「확인 전」 — 못 이음.
"""
from __future__ import annotations
@@ -32,14 +24,16 @@ from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Hopyo_Fit as fit
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch
from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit
from M02_MasterTemplete import M02_Hopyo_Units as units
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Library_Same as same
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Doc as sd
ORIGIN = "실무자료"
KIND = "sangeun"
WAIT_WHY = "확인 전 — [단가산출]에서 블록 [확인] 뒤 [옮기기]"
def mark(key: str, no: int) -> str:
@@ -99,50 +93,26 @@ def _bad(text: str, status: int = 400) -> store.StoreError:
return store.StoreError(status, text)
def _picks(raw: Any) -> dict[int, dict[str, Any]]:
"""사용자 고름 `{번: {로직?, 입력값?, 환산?, 잇기?}}` 재검사 — 돈 로직만 · 입력 이름 = 그 로직 입력 ·
환산 = SK 꼴(`M02_Sangeun_Doc`) · 잇기 = `[{id, …}]`(id 짝 · 키는 `M02_Hopyo_SkPatch` 가 봄)."""
def _sure(raw: Any) -> dict[int, dict[int, Any]]:
"""확인 목록 `[{번, 고름?: {차례: 고름}}]` → `{번: {차례: 고름}}`(고름 꼴은 `M02_Hopyo_SkUnit` 이 봄)."""
if not raw:
return {}
if not isinstance(raw, dict) or len(raw) > 500:
raise _bad("바탕은 {블록 번: {로직, 입력값}}")
logics = ledger._known()[0]
out = {}
for no, one in raw.items():
code = str((one or {}).get("로직") or "") if isinstance(one, dict) else "?"
if code and code not in logics:
raise _bad(f"단산{no} · 돈 로직이 아님 「{code}」")
if not code and not (one.get("환산") or one.get("잇기")):
raise _bad(f"단산{no} · 로직 · 환산 · 잇기 중 하나")
pick: dict[str, Any] = {}
given = one.get("입력값") if code else None
if code:
pick["로직"] = code
if given is not None:
bad = set(given) - ledger.input_names(code) if isinstance(given, dict) else {1}
if bad:
raise _bad(f"단산{no} · 모르는 입력 {sorted(map(str, bad))}")
pick["입력값"] = given
if one.get("환산"):
try:
pick["환산"] = sd._swap_how(one["환산"])
except sd.DocError as e:
raise _bad(f"단산{no} · {e}") from e
links = one.get("잇기")
if links:
if not isinstance(links, list) or not all(
isinstance(x, dict) and x.get("id") for x in links
):
raise _bad(f"단산{no} · 잇기는 [{{id, 노임|기계|자재: 키}}]")
pick["잇기"] = links
out[int(no)] = pick
if not isinstance(raw, list) or len(raw) > 500:
raise _bad("확인은 [{번, 고름?: {차례: 고름}}]")
out: dict[int, dict[int, Any]] = {}
for one in raw:
try:
picks = one.get("고름") or {}
out[int(one["번"])] = {int(k): v for k, v in picks.items()}
except (TypeError, ValueError, KeyError, AttributeError) as e:
raise _bad("확인은 [{번, 고름?: {차례: 고름}}]") from e
return out
def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) -> dict[str, Any]:
name = str(block.get("이름") or line.get("이름") or "").strip() or f"단산{no}"
unit = str(line.get("단위") or block.get("단위") or "").strip() or "식"
keep = {k: plan[k] for k in ("갈래", "엑셀품", "못옮김")}
keep = {k: plan[k] for k in ("갈래", "엑셀품", "못옮김", "근거") if k in plan}
keep |= {"놓음": plan["놓음"]} if plan.get("놓음") else {}
return {
"종류": "단가산출",
@@ -160,32 +130,6 @@ def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) ->
}
def _old_base(folder: Path, key: str, mark_: str, source: refs.Here) -> dict[str, Any]:
"""이미 있는 SK 의 바탕 고름 `{로직, 입력값, 환산?}` · `본뜬` — 읽기 실패 = 빈 것."""
try:
doc = json.loads((Path(folder) / KIND / f"{key}.json").read_bytes())
except (OSError, ValueError):
return {}
base = {"로직": (doc.get("바탕") or {}).get("로직"), "입력값": doc.get("입력값") or {}}
return {
**base,
**({"환산": doc["환산"]} if doc.get("환산") else {}),
"본뜬": origin_of(doc, mark_, source),
}
def _alias(line: dict, plan: dict) -> dict[str, Any]:
origin = {"품명": line.get("이름"), "규격": line.get("규격"), "단위": line.get("단위")}
clauses = (line.get("근거") or {}).get("출처") or []
pair = {"로직": plan["바탕"]["로직"], "입력값": plan["입력값"], "배": 1, "몫": None}
return {
"원래": {k: str(v or "") for k, v in origin.items()},
"갈래": "짝",
"짝": [pair],
"엑셀조항": clauses if isinstance(clauses, list) else [],
}
class Maker:
"""한 번 만들기 — 파일 · 맞춤셈 · 이 층 SK 를 한 번 읽음(`source` = 파일 층 · 쓰기 = `store.root()`)."""
@@ -199,16 +143,77 @@ class Maker:
self.whole = m01.loaded()[1]
self.sure = {"원본": list(self.here), "확인": {"누가": who, "때": utc_now_text()}}
self.same, self.reused, self.above = same.Same(), [], []
self.keys = made(source, key, self.here) # {본뜬 키: SK} — 만든 것 더해 감
self.judged: dict[int, dict] = {}
self._units: units.Units | None = None
def plan(self, no: int, pick: dict | None = None) -> dict[str, Any]:
def units(self) -> units.Units:
if self._units is None:
books = ledger.books(self.source)
self._units = units.Units(books, ledger.doc_id(self.source, self.key))
return self._units
def basis(self, no: int) -> dict[str, Any] | None:
"""근거 꼬리표 = 맞춤셈 1위 기초(참고)."""
entry = self.cache.get(fit.unit_key(self.first[no])) or {}
top = (entry.get("차례") or [None])[0]
if not top:
return None
name = (self.whole.logic(top) or {}).get("이름")
return {"로직": top, "이름": name, "판정": (entry.get("판정") or {}).get("상태")}
def ref_of(self, sub: dict[str, Any]) -> dict[str, Any] | None:
"""하위 줄 → 참조 칸 — 단산 = 이 HP 그 블록 SK · 일위 = 확인된 그 호표 SC · 없으면 None."""
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build # Build → SkLink → 여기
if sub.get("종류") == "단산":
got = self.keys.get(mark(self.key, int(sub["번"])))
else:
done = hopyo_build.confirmed(store.root(), self.here)
got = done.get(hopyo_build.mark(self.key, int(sub["번"])))
layer, owner = refs.here()
return {"층": layer, "주인": owner, "키": got} if got else None
def judge(self, no: int, picks: dict | None = None) -> dict[str, Any]:
line, block = self.first[no], self.blocks[no]
entry = self.cache.get(fit.unit_key(line))
if entry is None and not (pick or {}).get("로직"):
entry = fit.compute(block, line)
unit = str(line.get("단위") or block.get("단위") or "").strip()
try:
return skpatch.plan(entry, block, line, self.whole, pick)
except skpatch.LinkError as e:
got = skunit.judge(block, self.units(), self.ref_of, picks, unit)
except skunit.PickError as e:
raise _bad(f"단산{no} · {e}") from e
self.judged[no] = got
return got
def plan(self, no: int, picks: dict | None = None) -> dict[str, Any]:
"""확인한 블록 → 단위 SK 꼴 · 확인 없음(`picks` None) · 고를 줄 남음 = 고름 대기."""
if picks is None:
return {"갈래": skpatch.WAIT, "까닭": WAIT_WHY, "후보": []}
got = self.judge(no, picks)
if got["고를줄"]:
return {"갈래": skpatch.WAIT, "까닭": got["판정"], "후보": []}
return skunit.plan(got, self.basis(no))
def make(self, no: int, sure: dict[int, dict], out: dict, path: tuple = ()) -> None:
"""확인한 블록 하나 — 확인 목록의 하위 단산 먼저 · 못 만들면 `못만듦`."""
mark_ = mark(self.key, no)
if no in path or any(x["번"] == no for x in out["만듦"] + out["못만듦"]):
return
for one in self.blocks[no].get("항목") or []:
sub = one.get("하위") or {}
m = int(sub.get("번") or 0)
if sub.get("종류") == "단산" and m in sure and mark(self.key, m) not in self.keys:
self.make(m, sure, out, (*path, no))
plan = self.plan(no, sure[no])
got = self.judged[no]
lost = [r for r in got["줄"] if r["상태"] == units.LINKED and r["차례"] not in got["담음"]]
name = self.blocks[no].get("이름") or self.first[no].get("이름")
if plan["갈래"] == skpatch.WAIT or lost:
why = plan.get("까닭") or f"하위 「{lost[0]['이름']}」 — 그 블록 먼저 확인"
out["못만듦"].append({"번": no, "이름": name, "까닭": why})
return
key = self.write(no, plan, self.keys.get(mark_))
self.keys[mark_] = key
out["만듦"].append({"번": no, "이름": name, "키": key, "갈래": skunit.UNIT, "못옮김": []})
def write(self, no: int, plan: dict, have: str | None) -> str:
"""그 블록 SK 를 씀 → 키 — 다른 출처(또)가 쓰는 SK 의 내용은 안 바꿈(바뀌면 떼고 다시 찾음)."""
@@ -261,53 +266,29 @@ class Maker:
def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[str, Any]:
"""[단가산출 만들기] — 산근 블록마다 기초 · SK · 고름 대기 → `{블록, 만듦 [{번, 이름, 키, 갈래, 못옮김,
환산?}], 그대로, 남은못옮김 [{번, 이름, 키, 못옮김}], 기초, SK, 고름대기 [{번, 이름, 단위, 까닭, 후보}],
별칭, 같은것씀 [{번, 키}], 위층같음 [{번, 층, 주인, 키}]}` · `picks` = 고름."""
chosen = _picks(picks)
"""[단가산출] — 블록마다 줄 판정 · 확인한 블록만 단위 SK → `{블록, 판정 [{번, 이름, 단위, 판정, 고를줄,
우리금액, 엑셀금액, 근거, 만든, 줄}], 만듦 [{번, 이름, 키, 갈래, 못옮김}], 못만듦 [{번, 이름, 까닭}], 그대로,
같은것씀, 위층같음}` · `picks` = 확인 목록."""
sure = _sure(picks)
maker = Maker(folder, key, who)
bad = sorted(set(chosen) - set(maker.first))
bad = sorted(set(sure) - set(maker.first))
if bad:
raise _bad(f"없는 블록 · 부르는 줄 없음 「단산{bad[0]}」", 404)
if not maker.done and set(maker.first) - set(chosen):
if not maker.done:
fit.ensure(folder, key)
raise _bad("맞춤 셈 중 — 끝나면 다시 누름", 409)
have = made(folder, key, maker.here)
out: dict[str, Any] = {"블록": len(maker.first), "만듦": [], "그대로": 0, "고름대기": []}
out["남은못옮김"] = []
out.update({skpatch.BASE: 0, skpatch.SK: 0})
books, alias = ledger.books(folder), {}
out: dict[str, Any] = {"블록": len(maker.first), "만듦": [], "못만듦": []}
for no in sorted(sure):
maker.make(no, sure, out)
rows = []
for no in sorted(maker.first):
old = have.get(mark(key, no))
line = maker.first[no]
name = maker.blocks[no].get("이름") or line.get("이름")
if old and no not in chosen:
out["그대로"] += 1
lost = _old_base(folder, old, mark(key, no), maker.here)["본뜬"].get("못옮김")
if lost:
out["남은못옮김"].append({"번": no, "이름": name, "키": old, "못옮김": lost})
continue
plan = maker.plan(no, chosen.get(no))
if plan["갈래"] == skpatch.WAIT and old and not chosen[no].get("로직"):
base = _old_base(
folder, old, mark(key, no), maker.here
) # 고른 기초로 만든 SK 의 잇기 = 그 바탕 그대로
if base.get("로직"):
pick = {k: v for k, v in base.items() if k != "본뜬"}
plan = maker.plan(no, {**pick, **chosen[no]})
if plan["갈래"] == skpatch.WAIT:
head = {"번": no, "이름": maker.blocks[no].get("이름") or line.get("이름")}
out["고름대기"].append({**head, "단위": line.get("단위"), **plan})
continue
got = maker.write(no, plan, old)
out[plan["갈래"]] += 1
made_ = {"번": no, "이름": name, "키": got, "갈래": plan["갈래"], "못옮김": plan["못옮김"]}
out["만듦"].append({**made_, **({"환산": plan["환산"]} if plan.get("환산") else {})})
found = ledger.recall(line, folder, books)
if no in chosen or found is None or found.get("규격다름"):
alias[ledger.key(line)] = _alias(line, plan)
out["별칭"] = ledger.write(
folder, alias, who, maker.hp.get("파일") or None, ledger.doc_id(folder, key)
)
line, block = maker.first[no], maker.blocks[no]
got = maker.judged.get(no) or maker.judge(no)
head = {"번": no, "이름": block.get("이름") or line.get("이름"), "단위": line.get("단위")}
keep = ("판정", "고를줄", "우리금액", "엑셀금액", "줄")
head |= {k: got[k] for k in keep} | {"근거": maker.basis(no)}
rows.append({**head, "만든": maker.keys.get(mark(key, no))})
out["판정"] = rows
out["그대로"] = sum(1 for r in rows if r["만든"] and r["번"] not in sure)
out["같은것씀"], out["위층같음"] = maker.reused, maker.above
return out
+190
View File
@@ -0,0 +1,190 @@
"""M02 실무 단가산출 블록 = 줄 판정 모음 · 단위 SK(PLAN 778-5 · D-3 · 요구 623 · 624).
모두 줄로만 — 바탕 기초 안 씀(기초 = 근거 꼬리표만) · 자동 없음(사용자가 확인한 블록만 만듦).
줄 판정 = `M02_Hopyo_Units` 잇기(상태 · 방법 · 단위) + 매개값(품 · 시간 · 수량 · 율) · 사용자 고름
`{차례: 단위 | {뺌: 까닭}}`(그 줄만 · 꼬리표 「내가 고름」) · 단가 = M01 지금 값으로 다시 셈(`우리금액`) ·
엑셀 금액과의 `차` = 참고(엑셀 = 과거 값 · 판정에 안 씀) · 셈 밖 줄 = 상태 「밖」(SK 에 안 들임).
블록 판정 = 줄 모음 — 모두 이어짐 = 「옮길 수 있음」 · 아니면 「고를 줄 n」.
단위 SK 더한줄(바탕 없음 · 778-4 꼴) = 인력 `{요소 LB, 수량 품}` · 기계경비 로직 `{로직 GC, 입력값 {기계},
수량 시간 × 할증}` · 재료 `{요소 MT, 수량(손료율 든), 별도계상?}`(못 찾은 별도계상 = 0 원 고정 줄) ·
율 `{고정 {비율 {기준, 비목}}, 수량 = 율 %}`(줄 목록 = 그 행 줄 `add:#k`) · 하위 `{참조 SK · SC}` · 값 고정 줄.
근거 꼬리표 = 맞춤셈 1위 기초(본뜬 칸 · 참고). 서버 단독 · 쓰기 = `M02_Hopyo_Sangeun`.
"""
from __future__ import annotations
from typing import Any
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch
from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line
from M02_MasterTemplete import M02_Hopyo_Units as units
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete.M02_Hopyo_Words import mtypes
GOOD = "옮길 수 있음"
OUT, DROP = "밖", "뺌"
UNIT = "단위" # 본뜬 갈래
MINE = "내가 고름"
WORD = {"L": "품", "X": "시간", "M": "수량", "R": "율", "S": "수량"}
PICKS = {"L": ("노임", "LB"), "X": ("기계", "EQ"), "M": ("자재", "MT")}
class PickError(ValueError):
"""고름 틀림 — 부르는 쪽이 400."""
def _picked(one: dict[str, Any], pick: Any, us: units.Units) -> dict[str, Any]:
"""사용자 고름 → 이어짐 단위 — 노임 LB · 기계 EQ(→ 기계경비 로직) · 자재 MT · 율 기준 · 뺌."""
if not isinstance(pick, dict):
raise PickError("고름은 {노임|기계|자재: 키} · {기준, 비목?} · {뺌}")
if pick.get("뺌"):
return {"상태": DROP, "단위": None, "글": str(pick["뺌"]), "꼬리표": [MINE]}
kind = one["묶음"]
if kind == "R" and pick.get("기준") is not None:
unit = {"기준": pick["기준"], "비목": pick.get("비목") or units.rate_part(one.get("이름"))}
hfix.fixed.clean({"이름": "율", "비율": unit}, "고름", us.whole)
return {"상태": units.LINKED, "단위": unit, "꼬리표": [MINE]}
field, head = PICKS.get(kind, ("", "?"))
code = str(pick.get(field) or "")
if not field or not code.startswith(head):
raise PickError(f"{units.HOW.get(kind, kind)} 줄 · {field or '뺌'} 키({head}…)로 고름")
try:
row = us.whole.get(code)
except Exception as e: # noqa: BLE001 — 없는 키
raise PickError(f"없는 키 「{code}」") from e
unit: dict[str, Any] = {field: code}
if kind == "X":
kinds = mtypes(f"{row.get('상세구분')} {row.get('이름')}")
unit["로직"] = skpatch._mach_logics(us.whole, kinds, row)[0]
unit |= {"할증": one["할증"]} if one.get("할증") else {}
if kind == "M" and (one.get("어떻게") == "별도" or pick.get("별도계상")):
unit["별도계상"] = True
return {"상태": units.LINKED, "단위": unit, "꼬리표": [MINE]}
def lines(block: dict[str, Any], us: units.Units, picks: dict | None = None) -> list[dict]:
"""블록 단위 줄 → 줄 판정 `[{차례, 행, 묶음, 이름, 규격, 매개값, 엑셀금액, 재현, 상태, 방법, 단위, 후보,
꼬리표, 글}]`(재현 = 엑셀 단가 × 매개값 ≈ 엑셀 금액 · D-6 띠) · `picks` = `{차례: 고름}`."""
out = []
for at, one in enumerate(block.get("항목") or []):
q = one.get("품")
row = {"차례": at, "행": one.get("행"), "묶음": one["묶음"], "이름": one.get("이름")}
row |= {"규격": one.get("규격") or "", "엑셀금액": one.get("금액"), "재현": one.get("재현")}
row["매개값"] = {WORD[one["묶음"]]: q} if one["묶음"] in WORD and q is not None else {}
if one.get("밖"):
out.append({**row, "상태": OUT, "방법": "", "단위": None, "후보": [], "꼬리표": []})
continue
got = us.line(one)
pick = (picks or {}).get(at)
if pick is not None:
got = {**got, "후보": [], "글": "", **_picked(one, pick, us)}
out.append({**row, **got})
return out
def verdict(rows: list[dict]) -> tuple[str, int]:
"""블록 판정 — (「옮길 수 있음」 | 「고를 줄 n」, n)."""
n = sum(1 for r in rows if r["상태"] not in (units.LINKED, OUT, DROP))
return (GOOD if not n else f"고를 줄 {n}"), n
def adds(block: dict, rows: list[dict], us: units.Units, ref_of: Any) -> tuple[list, dict]:
"""이어진 줄 → 단위 SK 더한줄 · `{차례: add 번}` — `ref_of(하위)` = 참조 칸 | None(빠짐)."""
out: list[dict] = []
at_of: dict[int, int] = {}
by_row = {r.get("행"): r["차례"] for r in rows}
items = block.get("항목") or []
for r in rows:
if r["상태"] != units.LINKED:
continue
one, unit = items[r["차례"]], r["단위"] or {}
name = " ".join(str(one.get("이름") or "").split()) or "이름 없음"
q = float(one.get("품") or 0)
kind = one["묶음"]
if kind == "L":
add = {"종류": "인력", "요소": unit["노임"], "이름": name, "단위": "인", "수량": q}
elif kind == "X":
eq = us.whole.get(unit["기계"]) or {}
given = {skpatch.MACH_IN: eq.get("원문번호")}
q *= float(unit.get("할증") or 1)
add = {
"종류": "로직",
"로직": unit["로직"],
"입력값": given,
"이름": name,
"단위": "시간",
}
add["수량"] = q
elif kind == "M" and unit.get("자재"):
add = {"종류": "재료", "요소": unit["자재"], "이름": name, "수량": q}
add |= {"별도계상": True} if unit.get("별도계상") else {}
elif kind == "R":
basis = unit["기준"]
if isinstance(basis, dict): # 줄 목록 = 그 행 줄
basis = [f"add:#{at_of[k]}" for h in basis.get("행") or []
if (k := by_row.get(h)) in at_of] # fmt: skip
fx = {
"이름": name,
"단가": {},
"이음": {},
"비율": {"기준": basis, "비목": unit["비목"]},
}
add = {"고정": fx, "이름": name, "단위": "%", "수량": round(q * 100, 9)}
elif kind == "S":
ref = ref_of(unit["하위"])
if ref is None:
continue
add = {"참조": ref, "이름": name, "수량": q or 1}
else: # 값 · 못 찾은 별도계상 = 고정 금액 줄
money = unit.get("값", 0)
fx = {
"이름": name,
"단가": {k: float(v) for k, v in hfix._parts({"계": money}).items()},
}
add = {"고정": {**fx, "이음": {}}, "이름": name, "수량": q if kind == "M" else 1}
at_of[r["차례"]] = len(out)
out.append(add)
return out, at_of
def priced(rows: list[dict], made: list[dict], at_of: dict, unit: str, whole: Any) -> Any:
"""M01 지금 값으로 다시 셈 → (저장 꼴 더한줄, 블록 1 단위 계 | None) · 줄 `우리금액` · `차`(참고) ·
셈 못 함 = 금액 칸 비움(판정은 그대로)."""
doc = {"키": "SK-000000", "이름": "판정", "단위": unit or "식", "바탕": None, "더한줄": made}
try:
doc = sd.clean(doc, None, whole) # 저장 꼴(같은 내용 SK 지문이 저장본과 같게)
got = calc.run(doc, whole)
except Exception: # noqa: BLE001
return made, None
money = {x.get("cid"): x.get("금액") for x in got.get("줄") or []}
for r in rows:
k = at_of.get(r["차례"])
mine = money.get(f"add:#{k}") if k is not None else None
r["우리금액"] = float(mine) if mine is not None else None
theirs = r.get("엑셀금액")
r["차"] = round(r["우리금액"] - float(theirs), 4) if mine is not None and theirs else None
total = (got.get("1단위") or {}).get("계")
return doc["더한줄"], float(total) if total is not None else None
def judge(
block: dict, us: units.Units, ref_of: Any, picks: dict | None = None, unit: str = ""
) -> dict[str, Any]:
"""블록 하나 → `{판정, 고를줄, 우리금액, 엑셀금액, 줄 [...], 더한줄, 담음}`(더한줄 = 단위 SK 몸 ·
담음 = 더한줄에 든 줄 차례 · 이어졌는데 빠진 줄 = 하위 SK · SC 아직 없음)."""
rows = lines(block, us, picks)
word, n = verdict(rows)
made, at_of = adds(block, rows, us, ref_of)
made, total = priced(rows, made, at_of, unit, us.whole)
head = {"판정": word, "고를줄": n, "우리금액": total}
head["엑셀금액"] = unit_line.money(block.get("금액"))
return {**head, "줄": rows, "더한줄": made, "담음": sorted(at_of)}
def plan(got: dict[str, Any], basis: dict | None) -> dict[str, Any]:
"""판정 → `Sangeun.Maker.write` 꼴 — 바탕 없음 · 덮개 · 뺀줄 없음 · 근거 꼬리표."""
keep = {"갈래": UNIT, "바탕": None, "입력값": {}, "덮개": {}, "자재표시": {}, "뺀줄": []}
out = {**keep, "더한줄": got["더한줄"], "못옮김": [], "놓음": []}
return out | ({"근거": basis} if basis else {})
+5 -5
View File
@@ -489,9 +489,9 @@ def hopyo_delete(
class SangeunBody(_Strict):
"""`바탕` = 고름 대기 블록에 사용자가 고른 기초 `{블록 번: {로직, 입력값?}}`(729-1)."""
"""`확인` = 사용자가 확인한 블록만 단위 SK `[{번, 고름?: {차례: 고름}}]`(778-5 · 자동 없음)."""
바탕: dict[str, dict[str, Any]] = Field(default_factory=dict)
확인: list[dict[str, Any]] = Field(default_factory=list, max_length=500)
@router.post("/layers/{layer}/hopyo/{key}/sangeun")
@@ -502,10 +502,10 @@ def hopyo_make_sangeun(
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""불러오기 결과 「단가산출」(718-1 · 729-1) — 산근 블록마다 매칭 → 비교 → 기초 · SK · 고름 대기
(파일 층 = 이 층) → `{블록, 만듦, 그대로, 기초, SK, 고름대기, 별칭}` · 맞춤 셈 전 409."""
"""불러오기 결과 「단가산출」(718-1 · 778-5) — 블록마다 줄 판정 · 확인한 블록만 단위 SK
(파일 층 = 이 층) → `{블록, 판정, 만듦, 못만듦, 그대로, 같은것씀, 위층같음}` · 맞춤 셈 전 409."""
folder = _root(session, layer, owner, write=True)
picks = body.바탕 if body else {}
picks = body.확인 if body else []
return _call(folder, hopyo_sangeun.build, folder, key, _who(session), picks)
+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
@@ -64,8 +64,10 @@ def test_marks_읽기만_판정_캐시_같음_SK(client):
assert judged and all(r["판정"] in ("맞음", "다른 점", "후보", "없음") for r in judged)
assert {r["판정"] for r in judged} <= {e["판정"]["상태"] for e in cache.values()}
assert all(r["SK"] is None and r["갈래"] is None and r["못옮김"] == [] for r in rows)
# [단가산출 만들기] 뒤 = 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)