fix(729-4): 단가산출 만들기 단위 성격 다름 500 걷음 · 못옮김 잇기 서버 길

- 728-2 단위 환산 뒤 [단가산출 만들기] 500 · 400(100개 ↔ ㎡ · ㎥ ↔ m2 · 본 ↔ M · 개소 ↔ m) 걷음 — 견줌 SK 배 = ratio_of(728 규칙) · 성격 다름 = 환산 계수 = 우리 품 ÷ 엑셀 품(인력 · 장비 둘 이상 같은 비일 때만) · 못 구함 = 고름 대기(까닭 「단위 성격 다름 · 계수 필요」 · 후보[].계수 칸)
- 맞음 블록도 단위 견줌(전엔 셈 없이 기초로 써서 뒤 셈에서 막힘)
- 못옮김 = [{id, 갈래 노임|기계|자재, 이름, 규격, 단위, 수량, 글, 후보}] · 응답 만듦[].못옮김 · 환산
- 몸 바탕[번] = {로직?, 입력값?, 환산?, 잇기?} · 잇기 [{id, 노임|기계|자재: 키} | {id, 뺌}] → SK 더한줄 인력 · 기계 · 재료(엑셀 수량 × 배) · 뺌 = 본뜬 놓음 · 틀린 id · 키 400
- 못옮김 남은 SK 를 쓰는 일위대가 = 남은줄(단산 · SK · 못옮김) · hopyo-build · build-all 409
- 못옮김 남은 블록 = SK(같지 않음)
- 시험 test_729 에 단위 · 잇기 · 409 시험 하나 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 11:33:31 +09:00
co-authored by Claude Opus 5.5
parent a70539c161
commit b63d000822
4 changed files with 246 additions and 39 deletions
+42 -13
View File
@@ -4,8 +4,10 @@
블록마다 그 블록을 부르는 첫 엑셀 줄(「단산N참조」)의 맞춤셈(`M02_Hopyo_Fit` 캐시 · 없으면 그 블록만
셈) → `M02_Hopyo_SkPatch.plan`:
기초 = SK 바탕 = 1위 기초 · 입력값 · 덮개 없음 · SK = 바탕 기초 + 덮개 · 더한줄 · 뺀줄(엑셀 품 그대로) ·
고름 대기 = SK 안 만듦(바탕 없는 SK 로 몰래 채우지 않음) · 후보 다섯 → 사용자가 고른 기초 `바탕 {번: {로직,
입력값?}}` 로 다시 부르면 그 기초에 견줘 SK(이미 있으면 그 키에 덮음).
고름 대기 = SK 안 만듦(바탕 없는 SK 로 몰래 채우지 않음) · 후보 다섯(성격 다른 단위 = `계수` 칸 뜻) →
사용자가 고른 기초 `바탕 {번: {로직?, 입력값?, 환산?, 잇기?}}` 로 다시 부르면 그 기초에 견줘 SK(이미 있으면
그 키에 덮음) · 잇기 `[{id, 노임|기계|자재: 키} | {id, 뺌}]` = 못옮김 → SK 더한줄 · 남은 못옮김 = 응답
`만듦[].못옮김` · 본뜬 그대로(그 SK 를 쓰는 일위대가 만들기 = 남은 줄 409 · `M02_Hopyo_SkLink`).
SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 ·
본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래, 엑셀품, 못옮김} · 같은 본뜬(키 · 원본)이
이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층.
@@ -27,6 +29,7 @@ from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Doc as sd
ORIGIN = "실무자료"
KIND = "sangeun"
@@ -73,7 +76,8 @@ def _bad(text: str, status: int = 400) -> store.StoreError:
def _picks(raw: Any) -> dict[int, dict[str, Any]]:
"""사용자가 고른 기초 `{번: {로직, 입력값?}}` 재검사 — 돈 로직만 · 입력 이름 = 그 로직 입력."""
"""사용자 고름 `{번: {로직?, 입력값?, 환산?, 잇기?}}` 재검사 — 돈 로직만 · 입력 이름 = 그 로직 입력 ·
환산 = SK 꼴(`M02_Sangeun_Doc`) · 잇기 = `[{id, …}]`(id 짝 · 키는 `M02_Hopyo_SkPatch` 가 봄)."""
if not raw:
return {}
if not isinstance(raw, dict) or len(raw) > 500:
@@ -81,14 +85,33 @@ def _picks(raw: Any) -> dict[int, dict[str, Any]]:
logics = ledger._known()[0]
out = {}
for no, one in raw.items():
if not isinstance(one, dict) or str(one.get("로직") or "") not in logics:
raise _bad(f"단산{no} · 돈 로직이 아님 「{(one or {}).get('로직')}」")
given = one.get("입력값")
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(one["로직"]) if isinstance(given, dict) else {1}
bad = set(given) - ledger.input_names(code) if isinstance(given, dict) else {1}
if bad:
raise _bad(f"단산{no} · 모르는 입력 {sorted(map(str, bad))}")
out[int(no)] = {"로직": one["로직"], **({"입력값": given} if given is not None else {})}
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
return out
@@ -96,6 +119,7 @@ def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) ->
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 |= {"놓음": plan["놓음"]} if plan.get("놓음") else {}
return {
"종류": "단가산출",
"이름": name,
@@ -105,6 +129,7 @@ def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) ->
"본뜬": {"층": ORIGIN, "키": mark(hp["키"], no), "파일": hp.get("파일"), **sure, **keep},
"바탕": plan["바탕"],
"입력값": plan["입력값"],
**({"환산": plan["환산"]} if plan.get("환산") else {}),
"열린입력": [],
"고름": {},
**{k: plan[k] for k in ("자재표시", "덮개", "더한줄", "뺀줄")},
@@ -139,9 +164,12 @@ class Maker:
def plan(self, no: int, pick: 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:
if entry is None and not (pick or {}).get("로직"):
entry = fit.compute(block, line)
return skpatch.plan(entry, block, line, self.whole, pick)
try:
return skpatch.plan(entry, block, line, self.whole, pick)
except skpatch.LinkError as e:
raise _bad(f"단산{no} · {e}") from e
def write(self, no: int, plan: dict, have: str | None) -> str:
doc = _doc(self.first[no], self.blocks[no], no, self.hp, plan, self.sure)
@@ -153,8 +181,8 @@ class Maker:
def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[str, Any]:
"""[단가산출 만들기] — 산근 블록마다 기초 · SK · 고름 대기 → `{블록, 만듦 [{번, 이름, 키, 갈래}],
그대로, 기초, SK, 고름대기 [{번, 이름, 단위, 까닭, 후보}], 별칭}` · `picks` = 고른 기초."""
"""[단가산출 만들기] — 산근 블록마다 기초 · SK · 고름 대기 → `{블록, 만듦 [{번, 이름, 키, 갈래, 못옮김,
환산?}], 그대로, 기초, SK, 고름대기 [{번, 이름, 단위, 까닭, 후보}], 별칭}` · `picks` = 고름."""
chosen = _picks(picks)
maker = Maker(folder, key, who)
bad = sorted(set(chosen) - set(maker.first))
@@ -181,7 +209,8 @@ def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[st
got = maker.write(no, plan, old)
out[plan["갈래"]] += 1
name = maker.blocks[no].get("이름") or line.get("이름")
out["만듦"].append({"번": no, "이름": name, "키": got, "갈래": plan["갈래"]})
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)
+12 -3
View File
@@ -4,7 +4,8 @@
확인 안 한 단가산출 줄 = 그 블록 SK 참조(이 층 · 같은 원본 · 없으면 `M02_Hopyo_Sangeun.Maker` 로 그 블록만
만듦 · 미리 보기 = 키만 · 맞춤 셈 전 · 고름 대기 · 단위 다름 = 못 이음) ·
확인한 로직 줄이 차이 SK 의 바탕 = 그 SK 참조(엑셀 차이 보존 · 별칭 짝 = 기초까지만) ·
남은 줄 = 엑셀 줄 · 갈래 · 까닭 · 후보(단가산출 고름 대기).
남은 줄 = 엑셀 줄 · 갈래 · 까닭 · 후보(단가산출 고름 대기) · 못옮김 남은 SK = 못 이음(`단산` 번 · `못옮김` ·
잇기 = [단가산출 만들기] `바탕[번].잇기` · 729-4).
"""
from __future__ import annotations
@@ -64,6 +65,7 @@ def sk_line(b: Any, line: dict[str, Any], create: bool) -> tuple[dict | None, di
return None, None
no = int(ref.get("번") or 0)
key = sks(b).get(sangeun.mark(b.key, no))
lost = None
if key is None and create:
if b._maker is None:
b._maker = sangeun.Maker(b.src, b.key, b.who)
@@ -74,11 +76,17 @@ def sk_line(b: Any, line: dict[str, Any], create: bool) -> tuple[dict | None, di
plan = b._maker.plan(no)
if plan["갈래"] == sangeun.skpatch.WAIT:
return None, {"까닭": plan["까닭"], "후보": plan["후보"]}
lost = plan["못옮김"]
key = f"SK-미리{no}" if b.dry else b._maker.write(no, plan, None)
sks(b)[sangeun.mark(b.key, no)] = key
if key is None:
return None, None
unit = sk_doc(b, key).get("단위") or line.get("단위")
doc = sk_doc(b, key)
lost = (doc.get("본뜬") or {}).get("못옮김") if lost is None else lost
if lost: # 연결 안 되면 못 만듦(내역 20-4)
why = f"단가산출 못옮김 {len(lost)} — [단가산출 만들기]에서 이음"
return None, {"까닭": why, "단산": no, "SK": key, "못옮김": lost}
unit = doc.get("단위") or line.get("단위")
if mm_unit(unit) != mm_unit(line.get("단위")):
return None, {"까닭": f"단가산출 단위 「{unit}」 다름"}
qty = Decimal(str(line.get("수량") or 0))
@@ -105,4 +113,5 @@ def left(line: dict[str, Any], why: dict | None) -> dict[str, Any]:
keep = ("차례", "이름", "규격", "단위", "수량")
out = {**{k: line.get(k) for k in keep}, "갈래": kind}
out["까닭"] = (why or {}).get("까닭") or WHY.get(kind, "이을 것 없음")
return {**out, "후보": (why or {}).get("후보") or []}
more = {k: v for k, v in (why or {}).items() if k not in ("까닭", "후보")}
return {**out, "후보": (why or {}).get("후보") or [], **more}
+135 -23
View File
@@ -8,6 +8,10 @@
장비 = 시간 같은 같은 갈래 줄은 그대로 · 같은 갈래 남은 줄 = 덮개 qty · 엑셀만 = 더한줄
기계경비 로직(굴착기 GC000267 · 그 밖 GC000268 · 기계 = 옵션 거꾸로 찾기) · 남은 우리 = 뺀줄
자재 = 같은 이름 우리 줄 수량 덮개 · 옮기지 못한 차이 = `못옮김`(버리지 않고 본뜬에 적음)
못옮김 = `[{id, 갈래 노임|기계|자재, 이름, 규격, 단위, 수량(엑셀 1 단위 품), 글, 후보}]` · 사용자 `잇기`
`[{id, 노임|기계|자재: 키}]` = SK 더한줄 인력 · 기계 · 재료(엑셀 수량 그대로) · `{id, 뺌: 까닭}` = 놓음.
SK 단위(728-2) = 엑셀 줄 단위 · 바탕 기준단위와 성격 다름 = 환산 계수 = 우리 품 ÷ 엑셀 품(인력 · 장비
둘 이상이 같은 비일 때만 · `환산` {계수}) · 못 구함 = 고름 대기(까닭 「단위 성격 다름 · 계수 필요」).
고름 대기 = 후보 · 숫자 없음 — SK 안 만듦 · 후보 다섯(사용자가 고른 기초 `pick` = 견줘 SK · 기초).
cid = `kind:키#n`(첫 기초 바탕 줄만 · `M02_Sangeun_Doc.line_ids`) · 더한줄 · 덮개 qty = 바탕 행 몫
(1 단위 품 × 바탕 결과단위 앞 수 · 셈이 ÷ 배) · 더한줄 로직 수량 = 몫 × 바탕 배 ÷ 그 기초 배.
@@ -28,12 +32,23 @@ from M02_MasterTemplete import M02_Hopyo_Fit as fit
from M02_MasterTemplete import M02_Hopyo_Option as option
from M02_MasterTemplete import M02_Hopyo_Ours as ours
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, specnums
BASE, SK, WAIT = "기초", "SK", "고름 대기"
KEYS = ("덮개", "자재표시", "더한줄", "뺀줄", "못옮김")
TINY = 1e-9
_ADD = re.compile(r"^add:#(\d+)$")
UNIT_WHY = "단위 성격 다름 · 계수 필요"
LINK = {
"노임": ("인력", "LB"),
"기계": ("기계", "EQ"),
"자재": ("재료", "MT"),
} # 잇기 칸 → 더한줄 · 키 머리
class LinkError(ValueError):
"""잇기 틀림(없는 id · 갈래 다름 · 없는 키) — 부르는 쪽이 400."""
def _near(a: float, b: float) -> bool:
@@ -119,9 +134,9 @@ def measure(doc: dict, whole: Any) -> dict[str, Any]:
class _Plan:
def __init__(self, doc: dict, whole: Any) -> None:
self.doc, self.whole = doc, whole
self.tb = float(calc.times_of(whole.logic(doc["바탕"]["로직"])))
self.tb = float(calc.ratio_of(doc, whole.logic(doc["바탕"]["로직"]), whole))
self.lines = parts(doc, whole)
self.lost: list[str] = []
self.lost: list[dict[str, Any]] = []
self.lb = {name: key for key, name in ours._labor_names().items()}
def cover(self, cid: str, **one: float) -> None:
@@ -134,6 +149,38 @@ class _Plan:
def add(self, one: dict[str, Any]) -> None:
self.doc["더한줄"].append(one)
def miss(self, kind: str, name: str, qty: float, text: str, **more: Any) -> None:
"""못옮김 한 줄 — id = 「갈래:이름」(같은 것 = #n) · 수량 = 엑셀 1 단위 품."""
base = f"{kind}:{name}"
n = sum(1 for x in self.lost if (x["갈래"], x["이름"]) == (kind, name))
one = {"id": base if not n else f"{base}#{n}", "갈래": kind, "이름": name, "규격": "",
"단위": "", "수량": _num(float(f"{qty:.10g}")), "글": text, "후보": []} # fmt: skip
self.lost.append({**one, **more})
def link(self, picks: list[dict[str, Any]]) -> None:
"""사용자 잇기 — 이은 줄 = 더한줄(엑셀 수량 × 배) · 뺌 = 놓음 · 나머지 못옮김 그대로."""
for raw in picks:
got = next((x for x in self.lost if x["id"] == str(raw.get("id"))), None)
if got is None:
raise LinkError(f"없는 못옮김 「{raw.get('id')}」")
if raw.get("뺌"):
self.lost.remove(got)
self.doc.setdefault("놓음", []).append({**got, "뺌": str(raw["뺌"])})
continue
kind, head = LINK.get(got["갈래"], ("", ""))
key = str(raw.get(got["갈래"]) or "")
if not kind or not key.startswith(head):
raise LinkError(f"「{got['id']}」 · {got['갈래']} 키({head}…)로 이음")
try:
elem = self.whole.get(key) or {}
except Exception as e: # noqa: BLE001 — 없는 키
raise LinkError(f"「{got['id']}」 · 없는 키 「{key}」") from e
self.lost.remove(got)
name = str(elem.get("이름") or got["이름"])
unit = got["단위"] or ("인" if kind == "인력" else str(elem.get("단위") or ""))
self.add({"종류": kind, "요소": key, "이름": name, "단위": unit,
"수량": _num(float(got["수량"]) * self.tb)}) # fmt: skip
def labor(self, excel: dict[str, float]) -> None:
mine = [x for x in self.lines if x["갈래"] == "인력" and not x["cid"].startswith("add:")]
mine = [{**x, "품": x["품"] or 0.0} for x in mine]
@@ -151,14 +198,14 @@ class _Plan:
for x in rows:
self.drop(x["cid"])
if want < -TINY:
self.lost.append(f"{t} 엑셀 {a:.6g} · 더한 기초만 {f:.6g}")
self.miss("노임", t, a, f"엑셀 {a:.6g} · 더한 기초만 {f:.6g}", 단위="인")
elif not rows:
if t in self.lb:
unit = "인"
self.add({"종류": "인력", "요소": self.lb[t], "이름": t, "단위": unit,
"수량": _num(want * self.tb)}) # fmt: skip
else:
self.lost.append(f"{t} 엑셀 {a:.6g} — 우리 인력에 없음")
self.miss("노임", t, want, f"엑셀 {a:.6g} — 우리 인력에 없음", 단위="인")
elif len(rows) == 1 or not b:
self.cover(rows[0]["cid"], qty=want * self.tb)
for x in rows[1:]:
@@ -195,7 +242,8 @@ class _Plan:
for x in mine:
if x["cid"] not in used:
self.drop(x["cid"])
self.lost += [f"우리만 장비 {'·'.join(sorted(a))} — 더한 기초 안" for a, _ in fixed]
for a, q in fixed:
self.miss("기계", "·".join(sorted(a)), q, "우리만 장비 — 더한 기초 안", 단위="시간")
def _add_machine(self, m: dict[str, Any], line: dict[str, Any]) -> None:
"""엑셀만 장비 → 기계경비 로직 줄(기계 = 그 갈래 · 규격 옵션 거꾸로 찾기)."""
@@ -210,7 +258,9 @@ class _Plan:
self.add({"종류": "로직", "로직": code, "입력값": got["입력"], "이름": name,
"단위": "시간", "수량": _num(m["시간"] * self.tb)}) # fmt: skip
return
self.lost.append(f"엑셀만 장비 {'·'.join(m['갈래'])} {m['시간']:.6g} 시간 — 기계 못 찾음")
spec = " ".join(f"{x:g}" for x in m.get("규격") or [])
name = "·".join(m["갈래"])
self.miss("기계", name, m["시간"], "엑셀만 장비 — 기계 못 찾음", 규격=spec, 단위="시간")
def materials(self, excel: list[dict[str, Any]]) -> None:
mine = [x for x in self.lines if x["갈래"] == "자재" and not x["cid"].startswith("add:")]
@@ -229,7 +279,7 @@ class _Plan:
for x in rows[1:]:
self.drop(x["cid"])
else:
self.lost.append(f"엑셀만 자재 {m['이름']} {m['품']:.6g}")
self.miss("자재", m["이름"], m["품"], "엑셀만 자재")
def _extras(entry: dict, code: str, whole: Any, tb: float) -> list[dict[str, Any]]:
@@ -245,11 +295,49 @@ def _extras(entry: dict, code: str, whole: Any, tb: float) -> list[dict[str, Any
return out
def _wait(entry: dict) -> dict[str, Any]:
def _need(code: str, unit: str, whole: Any) -> dict[str, Any] | None:
"""후보 기초 기준단위 ↔ 엑셀 단위가 성격 다름 = 계수 칸 `{가, 나, 갈래 사용자|단위중량}` · 아니면 None."""
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
try:
got = calc.base_unit(whole.logic(code))
except Exception: # noqa: BLE001 — 없는 로직
return None
if not got or str(unit or "").strip() in ("", "식"):
return None
swap = combo_store.mu.swap(got[0], unit)
return None if swap["배"] is not None else {"가": got[0], "나": unit, "갈래": swap["계수"]}
def _wait(entry: dict, whole: Any, unit: str, why: str | None = None) -> dict[str, Any]:
keep = ("로직", "이름", "단위", "판정", "글")
cands = [{k: c.get(k) for k in keep} for c in (entry.get("후보") or [])[: fit.TOP_OPT]]
why = (entry.get("판정") or {}).get("글") or "후보 없음"
return {"갈래": WAIT, "까닭": f"맞는 기초 없음 — {why}", "후보": cands}
for c in cands:
c["계수"] = _need(str(c.get("로직") or ""), unit, whole)
if why is None:
why = "맞는 기초 없음 — " + ((entry.get("판정") or {}).get("글") or "후보 없음")
return {"갈래": WAIT, "까닭": why, "후보": cands}
def _by_type(rows: list[dict[str, Any]]) -> 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 _coef(doc: dict, whole: Any, excel: dict[str, Any]) -> float | None:
"""성격 다른 단위 계수(바탕 앞 수 뗀 1 단위 = SK 몇 단위) = 우리 품 ÷ 엑셀 품 — 인력 직종 · 장비 갈래
둘 이상이 같은 비(맞춤 허용 폭)일 때만 · 아니면 None(고름 대기)."""
mine = measure({**doc, "단위": ""}, whole) # 「」 = 앞 수 뗀 바탕 단위(옛 셈)
pairs = [(mine["인력"].get(t, 0.0), q) for t, q in excel["인력"].items()]
ours_m = _by_type(mine["장비"])
pairs += [(ours_m.get(k, 0.0), q) for k, q in _by_type(excel["장비"]).items()]
rates = [a / b for a, b in pairs if a > TINY and b > TINY]
if len(rates) < 2 or max(rates) > min(rates) * (1 + fit.TOL[0]):
return None
return float(f"{sum(rates) / len(rates):.6g}")
def plan(
@@ -259,13 +347,14 @@ def plan(
whole: Any,
pick: dict | None = None,
) -> dict[str, Any]:
"""블록 한 칸 → `{갈래 기초|SK|고름 대기, 바탕, 입력값, 덮개, 자재표시, 더한줄, 뺀줄, 못옮김, 엑셀품,
후보?}` ·
`pick` = 사용자가 고른 기초 `{로직, 입력값?}`(옵션 없으면 거꾸로 찾음 · 늘 견줌)."""
entry = entry or {}
"""블록 한 칸 → `{갈래 기초|SK|고름 대기, 바탕, 입력값, 환산?, 덮개, 자재표시, 더한줄, 뺀줄, 못옮김,
놓음, 엑셀품, 후보?}` · `pick` = 사용자 `{로직?, 입력값?, 환산?, 잇기?}` — 로직 = 고른 기초(옵션 없으면
거꾸로 찾음 · 늘 견줌) · 환산 = SK `환산` 꼴 · 잇기 = 못옮김 잇기(`_Plan.link` · 틀림 `LinkError`)."""
entry, pick = entry or {}, pick or {}
state = (entry.get("판정") or {}).get("상태")
efp = finger.excel(block, line)
if pick:
unit = str(line.get("단위") or "").strip() or "식"
if pick.get("로직"):
code = str(pick["로직"])
given = pick.get("입력값")
if given is None:
@@ -274,21 +363,44 @@ def plan(
code = entry["차례"][0]
given = ((entry.get("옵션") or {}).get(code) or {}).get("입력") or {}
else:
return _wait(entry)
doc = {"키": "SK-000000", "이름": "견줌", "단위": line.get("단위") or "식",
"바탕": {"로직": code}, "입력값": dict(given), "덮개": {}, "뺀줄": [], "자재표시": {}} # fmt: skip
tb = float(calc.times_of(whole.logic(code)))
doc["더한줄"] = [] if pick else _extras(entry, code, whole, tb)
return _wait(entry, whole, unit)
doc = {"키": "SK-000000", "이름": "견줌", "단위": unit, "바탕": {"로직": code},
"입력값": dict(given), "덮개": {}, "뺀줄": [], "자재표시": {}} # fmt: skip
if pick.get("환산"):
doc["환산"] = dict(pick["환산"])
from M02_MasterTemplete import M02_Hopyo_Verdict as verdict # 판정이 Fit 을 부름 — 늦게 읽음
excel = verdict.excel_qty(efp)
row = whole.logic(code)
def extras(tb: float) -> list[dict[str, Any]]:
return [] if pick.get("로직") else _extras(entry, code, whole, tb)
try:
tb = float(calc.ratio_of(doc, row, whole))
except sd.DocError as e:
old = {**doc, "더한줄": extras(float(calc.times_of(row)))}
coef = None if pick.get("환산") else _coef(old, whole, excel)
if coef is None:
cands = entry.get("후보") or []
if code not in [c.get("로직") for c in cands]:
cands = [{"로직": code, "이름": row.get("이름")}, *cands]
why = f"{UNIT_WHY} — {str(e).split(' · ', 1)[-1]}"
return _wait({"후보": cands}, whole, unit, why)
doc["환산"] = {"계수": coef}
tb = float(calc.ratio_of(doc, row, whole))
doc["더한줄"] = extras(tb)
head = {"바탕": doc["바탕"], "입력값": doc["입력값"], "엑셀품": excel}
head |= {"환산": doc["환산"]} if doc.get("환산") else {}
if state == "맞음" and not pick and not doc["더한줄"]:
return {**head, "갈래": BASE, **dict.fromkeys(KEYS[:2], {}), **dict.fromkeys(KEYS[2:], [])}
base = {**dict.fromkeys(KEYS[:2], {}), **dict.fromkeys(KEYS[2:], []), "놓음": []}
return {**head, "갈래": BASE, **base}
work = _Plan(doc, whole)
work.labor({t: x["품"] for t, x in efp["인력"].items() if x.get("품")})
work.machines([m for m in efp["장비"] if m.get("시간")], line)
work.materials([m for m in efp["자재"] if m.get("품")])
work.link(pick.get("잇기") or [])
out = {k: doc[k] for k in KEYS[:4]}
changed = any(out.values())
return {**head, **out, "갈래": SK if changed else BASE, "못옮김": work.lost}
changed = any(out.values()) or bool(work.lost)
kept = {"못옮김": work.lost, "놓음": doc.get("놓음") or []}
return {**head, **out, "갈래": SK if changed else BASE, **kept}
@@ -5,6 +5,7 @@
SK 품 = 엑셀 품(차이는 엑셀 값 그대로) · 같은 단가산출 = 기초(덮개 없음) + 별칭 제안(짝 = 기초까지만) ·
고름 대기 = SK 안 만듦 → 고른 기초로 SK · 안 이은 줄 남으면 만들기 409(미리 보기 = 같은 줄) ·
사용자가 고르면 만듦 · 노임 = 이름 이음 · 기계 = EQ 이음 · master_data 원본 그대로. 층 = 임시 폴더.
729-4 = 단위 성격 다름(계수 · 고름 대기) · 못옮김 잇기 · 남은 못옮김 = 일위대가 막음.
"""
import json
@@ -18,6 +19,7 @@ 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
@@ -129,6 +131,61 @@ def test_실무_단가산출_기초_SK_고름대기_일위대가_막음(client):
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 _eq(one: dict) -> str:
return str((one.get("입력값") or {}).get("기계") or "")