Files
eomsangdonandClaude Opus 5.5 a22af84b62 feat(785-5): 만들기 · 길 — 확인한 고른 것 → SC · SK · 장부 · 새 M02_Hopyo_Router · 판정 · 검산 · 옮기기
- 새 M02_Hopyo_Router — 옛 hopyo 길 모두 M02_Labor_Router 에서 옮김(옛 화면 785-7 까지 돎) · 새 길 hopyo-judge 목록 · 호표 · 블록 · hopyo/{키}/check · hopyo/{키}/move · main 등록 · 시동 warm · Fit.ensure 걷음 · 올림 때 엑셀 사전(Row.lexicon) · 블록 풀이 뒤 셈 · 끝내기 때 저장본 지움
- 새 M02_Hopyo_Judge — 1차 줄 판(Row) + 단산 줄 2차 풀이(Clause → Solve) · 엑셀 칸(이름 · 규격 · 단위 · 단가) · 블록 금액 · 절 몫 · 블록 풀이 파생 저장본(HP 폴더 _derived · 입력 지문 · 지문 다를 때만) · judged(folder, key) · 검산(입력값 = 엔진이 쓴 칸 모두 · 지역 · 자재지역 · 조종원직종 비움 · 맡은 절 단위 줄 · 몫과 품 대조 · 금액 = 일위대가 로직 줄과 같은 셈)
- 새 M02_Hopyo_Make — 고른 것 → SC 줄(같은 별칭 · 줄 본뜬 {호표, 차례, 행} · 수량 = 엑셀 × 몫 × 배 · 같은 기초 둘째 = 한 줄 SC 사본 · 별도계상 자재 · 이음 · 율 · 고정 · 참조) · SK 더한줄(바탕 없음 · SkUnit.adds · Sangeun.Maker.write) · 하위 먼저 · 입력 필요 · 단위 환산 못 정함 = 그 줄(블록)만 막힘 · 장부 고른 것 꼴 짝 · 이미 옮긴 것 = 덮음 없으면 409(남의 SC 덮지 않음) · 확인한 줄 몸(Lines) = Build 에서 옮김
- Build 나눔 — 모두 고정 길 · Builder · _order · _origins · build_all 그대로(B09 빌림) · 확인한 줄 몸 → Make.Lines
- SkUnit · Sangeun — SkPatch · Fit · Basis 부름 걷음(기계경비 기초 차례 SkUnit 으로 · 근거 꼬리표 = Clause.basis · 맞춤 셈 409 없앰 · 재료 고름 MO 됨) · Kids 가 _same_mark 가짐
- 시험 새 test_642_5_make(판정 · 검산 · 옮기기 · 예시 1 · 2 · 3 · 덮음 409 · 막는 길) · test_m02_hopyo_excel 마당에 새 Router 더함 · HOPYO_MAX_BYTES 자리(길 옮긴 까닭)
- 낡은 시험 고침: test_718_1 · test_729 「맞춤 셈 전 409」 뺌(맞춤 뒤 셈 안 기다림) · test_628 맞춤셈 캐시 넣기 뺌(근거 = 조항 색인) · test_729 근거 없음 꼴 · 고정 이음 유가 칸(785-3 LINKS 늘어난 까닭)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 23:13:53 +09:00

223 lines
12 KiB
Python

"""M02 실무 단가산출 블록 = 줄 판정 모음 · 단위 SK(PLAN 778-5 · D-3 · 요구 623 · 624).
모두 줄로만 — 바탕 기초 안 씀(기초 = 근거 꼬리표만) · 자동 없음(사용자가 확인한 블록만 만듦).
줄 판정 = `M02_Hopyo_Units` 잇기(상태 · 방법 · 단위) + 매개값(품 · 시간 · 수량 · 율) · 사용자 고름
`{차례: 단위 | {뺌: 까닭}}`(그 줄만 · 꼬리표 「내가 고름」) · 단가 = M01 지금 값으로 다시 셈(`우리금액`) ·
엑셀 금액과의 `차` = 참고(엑셀 = 과거 값 · 판정에 안 씀) · 셈 밖 줄 = 상태 「밖」(SK 에 안 들임).
블록 판정 = 줄 모음 — 모두 이어짐 = 「옮길 수 있음」 · 아니면 「고를 줄 n」 · 줄 0 = 「줄 없음」(고를줄 1 · 빈 SK 막음).
단위 SK 더한줄(바탕 없음 · 778-4 꼴) = 인력 `{요소 LB, 수량 품}` · 기계경비 로직 `{로직 GC, 입력값 {기계},
수량 시간 × 할증}` · 재료 `{요소 MT, 수량(손료율 든), 별도계상?}`(못 찾은 별도계상 = 0 원 고정 줄) ·
율 `{고정 {비율 {기준, 비목}}, 수량 = 율 %}`(줄 목록 = 그 행 줄 `add:#k`) · 하위 `{참조 SK · SC}` · 값 고정 줄.
근거 꼬리표 = 조항 색인(`Sangeun.Maker.basis` · 참고) · 고른 것(785-5 `M02_Hopyo_Make`)도 `adds` 를 부름 ·
기계경비 기초 차례(`_mach_logics`) = 옛 SkPatch 에서 옮김. 서버 단독 · 쓰기 = `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_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 = "옮길 수 있음"
EMPTY = "줄 없음"
OUT, DROP = "밖", "뺌"
UNIT = "단위" # 본뜬 갈래
MINE = "내가 고름"
WORD = {"L": "품", "X": "시간", "M": "수량", "R": "율", "S": "수량"}
PICKS = {"L": ("노임", "LB"), "X": ("기계", "EQ"), "M": ("자재", ("MT", "MO"))}
MACH_IN = "기계" # 기계경비 기초 기계 입력 칸
MACHINE = {"굴착기": "GC000267"} # 굴착기(무한궤도) 시간당 기계경비 · 그 밖 장비 = GC000268
MACHINE_ANY = "GC000268"
MACHINE_SET = {"GC000267", "GC000268", "GC000269", "GC000275"}
# (주연료, 조종원) 없는 기종 → 그 기계경비 기초(M01 로직이 부르는 것과 같음 · 778-2)
SOLO = {(False, False): "GC000273", (True, False): "GC000272", (False, True): "GC000276"}
class PickError(ValueError):
"""고름 틀림 — 부르는 쪽이 400."""
def _mach_logics(whole: Any, kinds: set, eq: dict | None = None) -> list[str]:
"""기계경비 기초 차례 — 갈래 첫 기초(굴착기 = 무한궤도) › 그 밖 · `eq` 가 있으면
연료 · 조종원 없는 기종 = 그 기초 하나 · 아니면 그 기계(또는 같은 이름 기계)를 고르기에 둔 기초가 앞."""
first = next((c for k, c in MACHINE.items() if k in kinds), MACHINE_ANY)
order = [first, *sorted(MACHINE_SET - {first})]
if not eq:
return order
has = eq.get("값") if isinstance(eq.get("값"), dict) else {}
if solo := SOLO.get((bool(has.get("주연료")), bool(has.get("조종원")))):
return [solo]
eqs = whole.numbers.get("EQ", {})
def takes(code: str) -> bool:
ins = whole.logic(code).get("입력") or []
opts = next((s.get("고르기") or [] for s in ins if s.get("이름") == MACH_IN), [])
same = any((eqs.get(o) or {}).get("이름") == eq.get("이름") for o in opts)
return eq.get("원문번호") in opts or same
hit = [c for c in order if takes(c)]
return hit + [c for c in order if c not in hit]
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):
shown = head if isinstance(head, str) else head[0]
raise PickError(f"{units.HOW.get(kind, kind)} 줄 · {field or '뺌'} 키({shown}…)로 고름")
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["로직"] = _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) · 줄 0 = 1(확인해도 못 만듦)."""
if not rows:
return EMPTY, 1
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 = {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 {})