Files
Aislo/M02_MasterTemplete/M02_Hopyo_Relink.py
T
eomsangdonandClaude Opus 5.5 32de488401 feat(M02 붙여넣기 화면 · 다시 잇기): 격자 Ctrl+V · 열 짝 한 번 묻고 기억 · 이 공사 단가로 다시 잇기 (PLAN 705-2)
- 일위대가 격자(ui_template_hopyo_grid onPaste) 누르고 Ctrl+V → 서버 /hopyo-paste · 열 짝 = 처음 한 번 묻고 이 브라우저에 기억 · 엑셀 머리 글이면 묻지 않음
- 호표 머리 없음 = 열린 문서에 고정 줄(알림 누름 = 다시 잇기) · 머리 있음 = 호표마다 사전조합 + 결과 창 [다음 — 이 공사 단가로 다시 잇기]
- 다시 잇기 창(EST 신 · 구 단가 비교 꼴) — 붙인 단가 · 최신 판 단가 · 후보(705-3 이음 짝) · 금액 차이 · 줄마다 · 모두 고름 · [바꿈] · [되돌리기]
- 서버 M02_Hopyo_Relink — 후보 표 · 이음 칸만 고쳐 씀(숫자 안 받음) · 전후 1 단위 값 · 되돌림
- 실무자료 [모두 고정으로 들여오기] 결과에도 다음 걸음 단추
- M02 상세가 고정 줄을 읽을 때 떨구던 흠 — normalizeLabor 가 고정 칸 지킴 · 로직 표 · 격자에 고정 줄 이름 · 규격 · 단위 · 「고정」 꼬리표 · 이음 / 손 단가

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:57:39 +09:00

149 lines
6.8 KiB
Python

"""「이 공사 단가로 다시 잇기」(PLAN 705-2 · 요구서 4장 2 · EST 「신 · 구 단가 비교」 꼴) — 고정 줄마다 붙인 단가 ↔
단가 책(M02 = M01 최신 판 노임 「값」 · 자재 기준단가) · 이음 후보 · 고른 줄만 이음 바꿈 · 되돌리기.
후보 = 705-3 이음과 같은 짝(`M02_Hopyo_Fixed.Linker`) — 노임 = 이름이 우리 직종(단위 인) ·
자재 = 이름 · 규격 같은 품목 + 단위 배 1 · 단가가 달라도 후보(바꿀지는 사람이 고름) · 기계 = 없음(B09 중기 몫).
줄 = 비율 줄 뺀 고정 줄 · 줄 가름 = `fixed_id` · 붙인 이름 끝 「 (2)」(줄 가름)은 떼고 찾음.
바꿈 = `{키, 줄, 노임?, 자재?}` — 키 = 이 층 사전조합(SC) · 값 null = 이음 풂(붙인 단가로) ·
서버가 문서를 읽어 이음 칸만 고쳐 씀(숫자 안 받음) · 전후 1 단위 값(`Labor_Ref.Expand`) · `되돌림` = 옛 이음.
열린 문서(저장 전) = `doc` 로 줄 · 후보만 — 바꿈은 화면이 문서에 넣고 [저장].
"""
from __future__ import annotations
import re
from typing import Any
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
from M02_MasterTemplete import M02_Hopyo_Shares as shares
from M02_MasterTemplete import M02_Labor_Fixed as fixed
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
KINDS = ("노임", "자재")
CHANGES_MAX = 2000
_SUFFIX = re.compile(r"\s*\(\d+\)$")
def _num(raw: Any) -> float | None:
got = fixed._num(raw)
return float(got) if got is not None else None
class Finder:
"""후보 찾기 — 단가 책(M01 최신 판) 한 번 읽음."""
def __init__(self) -> None:
self.linker = hfix.Linker()
self.whole = self.linker.whole
def _name(self, code: str) -> dict[str, str]:
try:
row = self.whole.get(code) or {}
except fixed.mf.FormulaError:
row = {}
return {k: str(row.get(k) or "") for k in ("이름", "규격", "단위")}
def price(self, kind: str, code: str | None, unit: str | None) -> float | None:
if not code:
return None
try:
if kind == "노임":
return _num((self.whole.get(code) or {}).get("값"))
return _num(fixed.mm.element(self.whole, code, {}, unit or None)[0])
except fixed.mf.FormulaError:
return None
def candidates(self, fx: dict[str, Any]) -> list[dict[str, Any]]:
name = _SUFFIX.sub("", str(fx.get("이름") or ""))
line = {"이름": name, "규격": fx.get("규격") or "", "단위": fx.get("단위") or ""}
out = []
trade = shares.trade(name) if line["단위"].strip() == "인" else None
for code, value in self.linker.labor.get(trade or "", []):
out.append({"비목": "노임", "키": code, **self._name(code), "단가": _num(value)})
price = fx.get("단가") or {}
if not trade and (_num(price.get("재료비")) or not any(_num(v) for v in price.values())):
for code, value in self.linker._mats({**line, "갈래": "자재"}):
out.append({"비목": "자재", "키": code, **self._name(code), "단가": _num(value)})
return out
def row(self, key: str | None, one: dict[str, Any]) -> dict[str, Any]:
fx = one[fixed.FIXED]
link = fx.get("이음") or {}
qty = _num((one.get("수량") or {}).get("손"))
now = {
k: self.price(k, link.get(k), fx.get("단위"))
for k in KINDS
if link.get(k) # 이은 비목 지금 단가
}
return {
"키": key,
"줄": fixed.fixed_id(fx),
**{k: fx.get(k) or "" for k in ("이름", "규격", "단위")},
"수량": qty,
"붙인": {k: _num((fx.get("단가") or {}).get(k)) or 0 for k in fixed.COSTS},
"이음": {k: link.get(k) for k in KINDS},
"지금": now,
"후보": self.candidates(fx),
}
def _lines(doc: dict[str, Any]) -> list[dict[str, Any]]:
return [
one
for one in doc.get("담은로직") or []
if fixed.is_fixed(one) and not one[fixed.FIXED].get("비율")
]
def rows(keys: list[str] | None = None, doc: dict[str, Any] | None = None) -> dict[str, Any]:
"""고정 줄 표 — `keys` = 이 층 사전조합들 · `doc` = 열린 문서 → `{줄 [...], 문서 [{키, 이름}]}`."""
finder, out, docs = Finder(), [], []
if doc is not None:
out = [finder.row(None, one) for one in _lines(doc)]
for key in keys or []:
refs.check_key("precombo", key)
got = store.read("precombo", key)["문서"]
docs.append({"키": key, "이름": got.get("이름") or key})
out += [finder.row(key, one) for one in _lines(got)]
return {"줄": out, "문서": docs}
def _total(key: str) -> float | None:
at = refs.here()
got = refs.Expand(m01.loaded()[1])({"층": at[0], "주인": at[1], "키": key})
return _num((got.get("기준") or {}).get("계"))
def apply(changes: list[dict[str, Any]]) -> dict[str, Any]:
"""바꿈 → 이 층 사전조합 이음 칸만 고쳐 씀 · M01 에 없는 키 400(`Labor_Fixed.clean`) →
`{바뀜, 문서 [{키, 이름, 전, 후}], 되돌림 [{키, 줄, 노임, 자재}]}`."""
if len(changes) > CHANGES_MAX:
raise store.StoreError(400, f"바꿈은 {CHANGES_MAX} 줄 안")
by_key: dict[str, list[dict[str, Any]]] = {}
for one in changes:
if not isinstance(one, dict) or not isinstance(one.get("줄"), str):
raise store.StoreError(400, "바꿈 줄은 {키, 줄, 노임?, 자재?}")
refs.check_key("precombo", str(one.get("키") or ""))
by_key.setdefault(str(one["키"]), []).append(one)
whole, done, back, docs = m01.loaded()[1], 0, [], []
for key, wants in by_key.items():
got = store.read("precombo", key)
doc, before = got["문서"], _total(key)
lines = {fixed.fixed_id(x[fixed.FIXED]): x for x in _lines(doc)}
for want in wants:
one = lines.get(want["줄"])
if one is None:
raise store.StoreError(404, f"없는 고정 줄 「{want['줄']}」 ({key})")
link = dict(one[fixed.FIXED].get("이음") or {})
back.append({"키": key, "줄": want["줄"], **{k: link.get(k) for k in KINDS}})
for k in KINDS:
if k in want:
link[k] = want[k] or None
one[fixed.FIXED] = fixed.clean({**one[fixed.FIXED], "이음": link}, want["줄"], whole)
done += 1
store.write("precombo", key, got["판"], doc)
docs.append({"키": key, "이름": doc.get("이름") or key, "전": before, "후": _total(key)})
return {"바뀜": done, "문서": docs, "되돌림": back}