Files
eomsangdonandClaude Sonnet 5.5 e23fec8c9c feat(786-1): 별도계상 줄 품목 고르기 없앰 — 후보 고정 줄 하나 · SC 합 밖 고정 줄
- Row.judge: 별도계상 줄은 M01 · 사전 · 장부 후보 안 찾고 고정 줄 하나(품목 「프로젝트에서」 표지) · 갈래 수 그대로
- Make: 별도계상 고정 줄 = 단가 0 · 메모 「별도계상 · 품목은 프로젝트에서」(합 밖)
- Picks.ts: apartPick 표지 셈
- 시험 642_3 · 642_5 · 642_6 Picks 몫을 새 꼴로 고침(옛 단언: 레미콘 자재 목록 별도계상 칸)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 07:17:50 +09:00

410 lines
21 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""M02 엑셀 불러오기 만들기(PLAN 785-5 · 요구 642 · 설계 642-A 2-7 ⑥) — 확인한 것 → 우리 SC · SK · 장부.
옮기기 몸 = `{호표 {번: {차례: [고른 것]}}, 블록 {번: [고른 것]}}` · 고른 것 = 785-3 꼴
`{갈래 기초|단가산출|일위대가|인력|재료|유가|기계|율|고정, 키, 입력값, 몫, 배?, 비목?, 기준?, 별도계상?, 층?,
주인?, 항목?}` · 서버가 다시 셈(브라우저 금액 · 품 안 받음 · 배 = `Row.factor` · 사용자 배 우선 ·
입력값 = 엔진이 쓴 칸 모두 `Judge.inputs`) · 「입력 필요」 · 배 못 정함 · 없는 키 = 그 줄(블록)만 `막힘`.
SC(`M02_Hopyo_Build.Builder` 가 줄 차례 · 확인 안 한 줄 = 모두 고정 길 그대로) — 한 엑셀 줄 = 고른 것마다
우리 줄(같은 별칭 = 엑셀 품명 · 줄 `본뜬 {호표, 차례, 행}`) · 수량 {손 = 엑셀 수량 × 몫 × 배} ·
기초 = 로직 줄(같은 로직 둘째 = 한 줄 SC 사본) · 재료 · 유가 = 자재 목록(별도계상) · 인력 · 기계 · 유가 이음 ·
별도계상 줄(`고정` + 별도계상 · 786-1) = 단가 0 고정 줄(합 밖 · 메모 「별도계상 · 품목은 프로젝트에서」) ·
율 = 고정 줄 `비율 {기준, 비목}` · 고정 = 엑셀 단가 고정 줄 · 단가산출 · 일위대가 = 참조
(`단산N` = 같은 옮기기 블록 SK · `제N호표` = 같은 HP 호표 SC — 하위 먼저 · 확인 안 된 하위 = 모두 고정).
SK(블록) = 바탕 없음 + 더한줄 — 기초 `{종류 로직, 입력값, 수량 = 몫}` · 참조 · 단위 줄(`항목`)에 이은
인력 · 기계 · 재료 · 유가 · 율 = `SkUnit.adds` · 고정 = 엑셀 금액 · 쓰기 = `Sangeun.Maker.write`.
같은 내용 재사용(`Kids.save` · `Sangeun.Maker.write`) · 장부 = 고른 것 꼴 짝(`Ledger.check_pair`) ·
엑셀 대조 = `Compare.compare`(참고 · 비목마다 1 원).
`Lines` = 확인한 줄 몸(옛 대상 꼴 `put` 은 옛 길 몫 · 785-7 지움).
"""
from __future__ import annotations
import re
from decimal import Decimal
from pathlib import Path
from typing import Any
from M02_MasterTemplete import M02_Hopyo_Build as hbuild
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
from M02_MasterTemplete import M02_Hopyo_Judge as judge
from M02_MasterTemplete import M02_Hopyo_Kids as hkids
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
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 hopyo_store
from M02_MasterTemplete import M02_Hopyo_Units as units
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Library_Same as lsame
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete.M02_Hopyo_Compare import compare
_SUB = {"단가산출": re.compile(r"^단산(\d+)$"), "일위대가": re.compile(r"^제(\d+)호표$")}
LINK = {"인력": "노임", "기계": "기계", "유가": "유가"}
OLD_PICK = {"인력": "노임", "기계": "기계", "재료": "자재", "유가": "자재"}
NEED = "입력 필요"
APART_MEMO = "별도계상 · 품목은 프로젝트에서"
COVER = "이미 옮긴 것이 있음 — 덮으려면 [덮어 옮기기](덮음)"
class Stuck(Exception):
"""그 줄(블록)만 못 옮김 — `{까닭, 칸?}`."""
def __init__(self, why: str, cells: list[str] | None = None) -> None:
super().__init__(why)
self.why, self.cells = why, cells or []
def _num(value: Decimal) -> int | float:
return hbuild._num(value)
# ── 확인한 줄 몸(SC) ─────────────────────────────────────────────────────
class Lines:
"""한 호표 줄 모음 — `Builder.build` 가 줄 차례대로 부름 · 확인 안 한 줄은 Builder 가 고정 길로."""
def __init__(self, b: Any, rows: list[dict], where: dict, stamp: str, linker: Any) -> None:
self.b, self.rows, self.where, self.stamp, self.linker = b, rows, where, stamp, linker
self.lines: list[dict] = []
self.mats: dict[str, dict] = {}
self.missing: list[dict] = []
self.used: set[str] = set()
self.mat_lines: dict[str, dict] = {}
self.nation: dict[tuple, dict] = {}
self.taken: set[str] = set()
self.kids: set[int] = set()
self.left: list[dict] = []
def put(self, line: dict, one: dict, alias: str | None, qty: Decimal | None = None,
mark: dict | None = None) -> None: # fmt: skip
"""대상 하나 → 줄 · 자재 — 옛 꼴 `{로직|자재|호표|전국|이음, 입력값, 배, 몫}` · `qty` = 새 길 수량
(엑셀 × 몫 × 배) · `mark` = 새 길 줄 `본뜬`."""
keep = len(self.lines)
if "이음" in one:
others = [x for x in self.rows if x is not line]
fx = hfix.line_of(line, others, self.where, self.taken, self.linker, one["이음"])
self.lines.append(fx)
else:
qty = (
qty
if qty is not None
else Decimal(str(line.get("수량") or 0)) * Decimal(str(one["배"]))
)
self._put(line, one, alias, qty, mark is not None)
for row in self.lines[keep:]:
row.update({"본뜬": mark} if mark else {})
def _put(self, line: dict, one: dict, alias: str | None, qty: Decimal, new: bool) -> None:
b = self.b
memo = hbuild._memo(line, one)
if "전국" in one:
at = (one["이름"], one["규격"], one["단위"])
have = self.nation.get(at)
hand = qty + Decimal(str(have["수량"]["손"])) if have else qty
head = {"이름": at[0], "규격": at[1], "단위": at[2], "수량": {"손": _num(hand)}}
self.nation[at] = {**head, **(hmat.marks(line, at[0]) if not have else {
k: have[k] for k in labor.MARKS if k in have
})} # fmt: skip
self.missing.append(hbuild._missing(line, hbuild.NATIONAL))
return
if "자재" in one:
code = one["자재"]
have = self.mats.get(code)
hand = qty + Decimal(str(have["수량"]["손"])) if have else qty
name = b.mat_names.get(code, "")
marks = (
have and {k: have[k] for k in labor.MARKS if k in have} or hmat.marks(line, name)
)
if one.get("별도계상") is not None: # 세 칸 값(빈 칸 = 기본 「별도계상」)
marks = {**marks, "별도계상": "별도계상" if one["별도계상"] else "직접계산"}
self.mats[code] = {"코드": code, "수량": {"손": _num(hand)}, **marks}
if not have:
self.mat_lines[code] = line
return
if "호표" in one or "참조" in one:
if "호표" in one:
self.kids.add(one["호표"])
key = b.done.get(hbuild.mark(b.key, one["호표"]), "SC-미리")
ref = {"층": refs.here()[0], "주인": refs.here()[1], "키": key}
else:
ref = one["참조"]
self.lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
return
row = {"로직": one["로직"], "입력값": one["입력값"], "메모": memo}
if not new:
kept, why = sangeun.kept(b, line, one, qty, alias, memo)
if why:
self.left.append(sangeun.left(line, why))
if kept is not None:
self.lines.append(kept)
return
if one["로직"] not in self.used:
self.used.add(one["로직"])
self.lines.append({**row, "별칭": alias, "수량": {"손": _num(qty)}})
return
at = int(line.get("차례", 0))
hopyo, file = b.hopyo, b.doc.get("파일") or ""
wrap = { # 같은 로직 둘째 — 한 줄 사전조합으로 감쌈
"이름": f"{hopyo['이름']} — {line.get('이름')}",
"단위": line.get("단위") or None,
"구분": hbuild.ORIGIN,
"상세구분": file,
"담은로직": [{**row, "별칭": alias, "수량": {"손": 1}}],
"자재": [],
"본뜬": b._origin(f"{hbuild.mark(b.key, b.no, at)} {one['로직']}", self.stamp),
}
ref = {"키": "SC-미리"} if b.dry else hkids.save(wrap, b.same)[0] # 미리 보기 = 쓰기 없음
self.lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
def apart(self, line: dict, alias: str | None, mark: dict) -> None:
"""별도계상 줄(786-1) = 합 밖 고정 줄 — 단가 0(셈에 안 듦) · 품명 · 규격 · 단위 · 수량 그대로 ·
품목은 프로젝트에 복사할 때 정함(메모 표지)."""
zero = {**line, "단가": {}, "금액": {}}
self.put(zero, {"이음": dict.fromkeys(hfix.fixed.LINKS)}, alias, None, mark)
self.lines[-1]["메모"] = f"{APART_MEMO} · {self.lines[-1].get('메모') or ''}".strip(" ·")
def picks(self, at: int, line: dict, picks: list[dict]) -> None:
"""고른 것(검사 · 다시 셈 끝난 것) → 그 엑셀 줄의 우리 줄 · 자재."""
alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None
mark = {"호표": self.b.no, "차례": at, "행": line.get("행")}
for p in picks:
kind, code = p["갈래"], p.get("키")
one = {"배": p["배"], "몫": p.get("몫")}
qty = p["수량"]
if kind == "기초":
self.put(line, {**one, "로직": code, "입력값": p["입력값"]}, alias, qty, mark)
elif kind in ("재료", "유가") and not p.get("이음"):
self.put(
line, {**one, "자재": code, "별도계상": p.get("별도계상")}, alias, qty, mark
)
elif kind in ("단가산출", "일위대가"):
self.put(line, {**one, "참조": p["참조"]}, alias, qty, mark)
elif kind == "율":
self.put(line, {"이음": {"비율": p["비목"]}}, alias, None, mark)
self.lines[-1]["고정"]["비율"] = {"기준": p["기준"], "비목": p["비목"]}
elif kind == "고정" and p.get("별도계상"):
self.apart(line, alias, mark)
elif kind == "고정":
self.put(line, {"이음": dict.fromkeys(hfix.fixed.LINKS)}, alias, None, mark)
else:
self.put(line, {"이음": {LINK[kind]: code}}, alias, None, mark)
def ledger_pick(p: dict[str, Any]) -> dict[str, Any]:
"""장부 짝(고른 것 꼴 · `Ledger.check_pair`) — 참조 = 만든 SK · SC 키."""
keep = ("갈래", "키", "입력값", "배", "몫", "비목", "기준", "별도계상")
out = {k: p[k] for k in keep if p.get(k) is not None}
return out | ({"키": p["참조"]["키"]} if "참조" in p else {})
# ── 고른 것 검사 · 다시 셈 ──────────────────────────────────────────────
class Move:
"""한 번 옮기기 — 파일 · M01 · 만든 SC · SK 를 한 번 읽음(쓰기 = `store.root()`)."""
def __init__(self, folder: Path, source: Path, key: str, who: Any) -> None:
self.folder, self.source, self.key, self.who = Path(folder), Path(source), key, who
self.hp = hopyo_store.read(source, key)
self.units = {int(h["번"]): h for h in self.hp.get("호표") or []}
self.m = judge.master()
self.known = ledger._known()
self.sk = sangeun.Maker(source, key, who)
self.same = lsame.Same()
self.out: dict[str, Any] = {"만듦": [], "단가산출": [], "막힘": [], "하위": []}
self.out["장부"] = {"씀": 0}
def _ref(self, p: dict) -> dict[str, Any]:
"""단가산출 · 일위대가 키 → 참조 칸(같은 HP 블록 · 호표 = 만든 것) · 없으면 막힘."""
code, kind = str(p.get("키") or ""), p["갈래"]
hit = _SUB[kind].match(code)
here = refs.here()
if hit:
n = int(hit.group(1))
made = (
self.sk.keys.get(sangeun.mark(self.key, n))
if kind == "단가산출"
else hbuild.confirmed(self.folder, refs.where(self.source)).get(
hbuild.mark(self.key, n)
)
)
if not made:
raise Stuck(f"{code} 먼저 옮김")
return {"층": here[0], "주인": here[1], "키": made}
head = "SK-" if kind == "단가산출" else "SC-"
ref = {"층": p.get("층") or here[0], "주인": p.get("주인"), "키": code}
if p.get("층") is None:
ref["주인"] = here[1]
if not code.startswith(head) or refs.read_ref(ref) is None:
raise Stuck(f"없는 {kind} 「{code}」")
return ref
def pick(self, p: dict, line: dict | None, where: str) -> dict[str, Any]:
"""고른 것 하나 재검사 · 다시 셈 → `{갈래, 키, 입력값, 배, 몫, 수량, 참조?, …}`."""
kind = p["갈래"]
got = {k: p[k] for k in ("갈래", "키", "몫", "비목", "기준", "별도계상") if k in p}
if kind == "기초":
full, need = judge.inputs(self.m, str(p.get("키") or ""), p.get("입력값"))
if need:
raise Stuck(f"{NEED} {len(need)}", need)
got["입력값"] = full
if kind in _SUB:
got["참조"] = self._ref(p)
else:
plain = {k: v for k, v in got.items() if k in ledger.PICK_KEYS}
ledger.check_pair(plain, where, self.known)
if kind in ("재료", "유가") and not got.get("키"):
raise Stuck(f"{kind} 키 없음")
if line is not None:
qty = judge.quantity(self.m, line, p)
if qty["배"] is None:
raise Stuck("단위 환산 입력")
got["배"], got["수량"] = qty["배"], Decimal(str(qty["우리"] or 0))
return got
# ── SC ──
def hopyo(self, no: int, rows: dict[int, list[dict]]) -> None:
unit = self.units[no]
lines = unit.get("줄") or []
checked = {}
for at, picks in sorted(rows.items()):
if not 0 <= at < len(lines):
raise store.StoreError(400, f"없는 줄 「제{no}호표 {at}」")
where = f"제{no}호표 {at + 1}째 줄"
if not picks:
continue
try:
checked[at] = {"고른": [self.pick(p, lines[at], where) for p in picks]}
except Stuck as e:
self.out["막힘"].append({"호표": no, "차례": at, "까닭": e.why, "칸": e.cells})
b = hbuild.Builder(self.folder, self.source, self.key, no, self.who)
b.strict, b.same = False, self.same
made = b.build(checked, [])
ref = made["참조"]
head = {"번": no, "키": ref["키"], "이름": unit.get("이름")}
head |= {k: made[k] for k in ("다시씀", "재사용")}
self.out["만듦"].append({**head, "비교": compare(ref, unit)})
self.out["장부"]["씀"] += made["장부"]["씀"]
def kid(self, no: int) -> None:
"""확인 안 된 하위 호표 — 모두 고정(705-3 길)으로 먼저."""
b = hbuild.Builder(self.folder, self.source, self.key, no, self.who)
b.strict, b.same = False, self.same
if b.done.get(hbuild.mark(self.key, no)):
return
made = b.build({}, [])
self.out["하위"].append({"번": no, "키": made["참조"]["키"], "새로": True})
# ── SK ──
def block(self, no: int, picks: list[dict]) -> None:
blk = self.sk.blocks[no]
items = blk.get("항목") or []
us = self.sk.units()
made: list[dict] = []
rows: list[dict] = []
for p in picks:
got = self.pick(p, None, f"단산{no}")
name = str(p.get("이름") or "").strip() or str(blk.get("이름") or f"단산{no}")
share = got.get("몫") or 1
at = p.get("항목")
item = items[at] if isinstance(at, int) and 0 <= at < len(items) else None
if got["갈래"] == "기초":
made.append({"종류": "로직", "로직": got["키"], "입력값": got["입력값"],
"이름": name, "수량": share}) # fmt: skip
continue
if got["갈래"] in _SUB:
qty = float(item.get("품") or 1) if item else share
made.append({"참조": got["참조"], "이름": name, "수량": qty})
continue
if item is None:
raise Stuck(f"{got['갈래']} — 블록 단위 줄(항목) 고름")
if got["갈래"] == "고정":
parts = hfix._parts(item.get("금액"))
fx = {"이름": name, "단가": {k: float(v) for k, v in parts.items()}, "이음": {}}
made.append({"고정": fx, "이름": name, "수량": 1})
continue
old = {"기준": got.get("기준"), "비목": got.get("비목")}
if got["갈래"] in OLD_PICK:
old = {OLD_PICK[got["갈래"]]: got["키"], "별도계상": got.get("별도계상")}
unit = skunit._picked(item, old, us)["단위"]
rows.append({"차례": at, "행": item.get("행"), "상태": units.LINKED, "단위": unit})
extra, _ = skunit.adds(blk, rows, us, self.sk.ref_of)
doc = {"키": "SK-000000", "이름": "옮김", "단위": blk.get("단위") or "식", "바탕": None}
try:
adds = sd.clean({**doc, "더한줄": made + extra}, None, self.sk.whole)["더한줄"]
except (ValueError, store.StoreError) as e:
raise Stuck(f"더한줄 틀림 — {e}") from e
plan = {"갈래": "고름", "바탕": None, "입력값": {}, "덮개": {}, "자재표시": {}, "뺀줄": []}
plan |= {"더한줄": adds, "못옮김": [], "놓음": []}
mark_ = sangeun.mark(self.key, no)
key = self.sk.write(no, plan, self.sk.keys.get(mark_))
self.sk.keys[mark_] = key
self.out["단가산출"].append({"번": no, "키": key, "이름": blk.get("이름")})
def blocks(self, sure: dict[int, list[dict]]) -> None:
"""하위 단산(블록 단위 줄 하위) 먼저."""
done: set[int] = set()
def visit(no: int, path: tuple = ()) -> None:
if no in done or no in path:
return
for one in self.sk.blocks[no].get("항목") or []:
sub = one.get("하위") or {}
if sub.get("종류") == "단산" and int(sub.get("번") or 0) in sure:
visit(int(sub["번"]), (*path, no))
done.add(no)
try:
self.block(no, sure[no])
except Stuck as e:
self.out["막힘"].append({"블록": no, "까닭": e.why, "칸": e.cells})
for no in sorted(sure):
visit(no)
def move(
folder: Path,
source: Path,
key: str,
units_: dict[int, dict[int, list[dict]]],
blocks: dict[int, list[dict]],
who: Any = None,
cover: bool = False,
) -> dict[str, Any]:
"""[옮기기] → `{만듦, 단가산출, 막힘, 하위, 장부}` — 블록 SK 먼저 · 호표 하위 먼저 ·
이미 옮긴 호표 · 블록(같은 HP 출처 SC · SK)은 `cover`(덮음) 때만 덮음 · 아니면 409 아무것도 안 씀."""
mv = Move(folder, source, key, who)
bad = [f"제{n}호표" for n in units_ if n not in mv.units]
bad += [f"단산{n}" for n in blocks if n not in mv.sk.blocks]
if bad:
raise store.StoreError(404, f"없는 {bad[0]}")
done = hbuild.confirmed(mv.folder, refs.where(mv.source))
had = {
"호표": [
{"번": n, "키": done[k]} for n in sorted(units_) if (k := hbuild.mark(key, n)) in done
]
}
had["블록"] = [
{"번": n, "키": mv.sk.keys[k]}
for n in sorted(blocks)
if (k := sangeun.mark(key, n)) in mv.sk.keys
]
if not cover and (had["호표"] or had["블록"]):
raise store.StoreError(409, {"까닭": COVER, **had})
mv.blocks(blocks)
wanted = {
int(hit.group(1))
for rows in units_.values()
for picks in rows.values()
for p in picks
if p.get("갈래") == "일위대가" and (hit := _SUB["일위대가"].match(str(p.get("키") or "")))
}
order = hbuild._order(mv.units)
for no in order: # 고른 것이 부르는 확인 안 된 하위 = 모두 고정으로 먼저
if no in wanted and no not in units_:
mv.kid(no)
for no in order:
if no in units_:
mv.hopyo(no, units_[no])
return mv.out