- 새 M02_Hopyo_SkPatch: 판정 맞음 = 기초(바탕 = 1위 기초 · 덮개 없음) · 다른 점 = 바탕 기초 + 인력 덮개(qty · 배율) · 엑셀만 직종 · 장비(GC000267 · GC000268 기계 옵션 찾기) 더한줄 · 우리만 줄 뺀줄 · 블록 안 자재 덮개 · 옮기지 못한 차이 = 본뜬.못옮김 · 후보 = 고름 대기
- M02_Hopyo_Sangeun: 바탕 없는 SK + 고정 한 줄 걷음 · 블록마다 기초 · SK · 고름 대기 · 고른 기초(바탕 {번: 로직}) · 별칭 장부 짝 = 기초까지만 · 맞춤 셈 전 409
- 여덟 파일 206 블록 = 기초 82 · SK 40 · 고름 대기 84 · SK 품 어긋남 0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
295 lines
15 KiB
Python
295 lines
15 KiB
Python
"""M02 실무 단가산출(산근 블록) → 단가산출(SK) 꼴(PLAN 729-1 · 요구서 내역19 1장 7 · 3장 6 ·
|
|
분석 19-B D-1).
|
|
|
|
블록 맞춤셈 한 칸(`M02_Hopyo_Verdict.block` 꼴 · 캐시 `M02_Hopyo_Fit.result`) → 갈래 셋:
|
|
기초 = 판정 맞음(또는 견줘 차이 없음) — 바탕 = 1위 기초 · 입력값 = 옵션 거꾸로 찾은 값 · 덮개 없음
|
|
SK = 다른 점 — 바탕 = 1위 기초(조합 둘째부터 = 더한줄 로직 · 수량 = 몫) + 차이 = 엑셀 품 그대로
|
|
인력 = 직종마다 덮개 qty(줄 하나) · 배율(여럿) · 엑셀만 직종 = 더한줄 인력 · 우리만 = 뺀줄
|
|
장비 = 시간 같은 같은 갈래 줄은 그대로 · 같은 갈래 남은 줄 = 덮개 qty · 엑셀만 = 더한줄
|
|
기계경비 로직(굴착기 GC000267 · 그 밖 GC000268 · 기계 = 옵션 거꾸로 찾기) · 남은 우리 = 뺀줄
|
|
자재 = 같은 이름 우리 줄 수량 덮개 · 옮기지 못한 차이 = `못옮김`(버리지 않고 본뜬에 적음)
|
|
고름 대기 = 후보 · 숫자 없음 — SK 안 만듦 · 후보 다섯(사용자가 고른 기초 `pick` = 견줘 SK · 기초).
|
|
cid = `kind:키#n`(첫 기초 바탕 줄만 · `M02_Sangeun_Doc.line_ids`) · 더한줄 · 덮개 qty = 바탕 행 몫
|
|
(1 단위 품 × 바탕 결과단위 앞 수 · 셈이 ÷ 배) · 더한줄 로직 수량 = 몫 × 바탕 배 ÷ 그 기초 배.
|
|
품 견줌 = 우리 셈(`M02_Sangeun_Calc.run` 줄 수량 ÷ 배) · 같음 = `Option.near`(0.5 % + 엑셀 자릿수 반올림).
|
|
우리만 자재 줄은 그대로(판정 견줌도 안 봄 · 엑셀은 자재를 일위대가 줄로 따로 적는 일이 많음).
|
|
서버 단독 · 읽기만(쓰기 = `M02_Hopyo_Sangeun`).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from decimal import Decimal
|
|
from typing import Any
|
|
|
|
from M02_MasterTemplete import M02_Hopyo_Combine as combine
|
|
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
|
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.M02_Hopyo_Words import mtypes, specnums
|
|
|
|
BASE, SK, WAIT = "기초", "SK", "고름 대기"
|
|
KEYS = ("덮개", "자재표시", "더한줄", "뺀줄", "못옮김")
|
|
TINY = 1e-9
|
|
_ADD = re.compile(r"^add:#(\d+)$")
|
|
|
|
|
|
def _near(a: float, b: float) -> bool:
|
|
return abs(a - b) <= TINY or option.near(a, b, fit.TOL[0], option.ru(a))
|
|
|
|
|
|
def _num(x: float) -> int | float:
|
|
got = Decimal(repr(float(x))).normalize()
|
|
return int(got) if got == got.to_integral_value() else float(got)
|
|
|
|
|
|
# ── 우리 셈 → 1 단위 품 ──────────────────────────────────────────────
|
|
def _types(sub: str, item: dict, args: str | None, given: dict, auto: dict) -> tuple[set, set]:
|
|
"""기계경비 줄 갈래 · 규격 — `M02_Hopyo_Ours._engine` 과 같은 읽기(중간값 없이)."""
|
|
ts = set(ours.FIXED_MACH.get(sub, set())) | mtypes(item.get("이름"))
|
|
sp = set(specnums(item.get("이름")))
|
|
for v in ours._args(args).values():
|
|
lit = v.strip("'\"")
|
|
if not ours.EQ_CODE.match(lit):
|
|
lit = str(given.get(lit) or auto.get(lit) or "")
|
|
if ours.EQ_CODE.match(lit):
|
|
a, b = ours.eq_of(lit)
|
|
ts, sp = ts | a, sp | b
|
|
return ts, sp
|
|
|
|
|
|
def parts(doc: dict, whole: Any) -> list[dict[str, Any]]:
|
|
"""SK 문서 → 줄마다 `{cid, 갈래 인력|장비|자재|기초|그밖, 이름 · 종류 · 규격, 품(1 단위)}` ·
|
|
더한줄 기초 = 그 기초 1 단위 품 × 줄 몫(`L` · `X`)."""
|
|
got = calc.resolve(doc, whole)
|
|
items = got["줄"]["호표"]
|
|
ran = calc.run(doc, whole)
|
|
times = Decimal(str(ran["배"]))
|
|
labor = ours._labor_names()
|
|
auto = {}
|
|
if (doc.get("바탕") or {}).get("로직"):
|
|
auto = option.m01.mau.values(whole, whole.logic(doc["바탕"]["로직"]))
|
|
qty = {x["cid"]: x.get("수량") for x in ran.get("줄") or [] if x.get("cid")}
|
|
out = []
|
|
for item, cid in zip(items, got["정체"]): # 별도계상 재료 = 셈 줄에서 빠짐 → 품 None
|
|
raw = qty.get(cid)
|
|
one: dict[str, Any] = {"cid": cid, "갈래": "그밖"}
|
|
one["품"] = None if raw is None else float(Decimal(str(raw)) / times)
|
|
kind, found = item.get("종류"), ours._SUB.match(str(item.get("요소") or ""))
|
|
if kind == "인력":
|
|
one.update(갈래="인력", 이름=labor.get(str(item.get("요소")), str(item.get("이름"))))
|
|
elif kind == "재료":
|
|
one.update(갈래="자재", 이름=str(item.get("이름") or ""))
|
|
elif kind == "로직" and found:
|
|
sub = found.group(1)
|
|
if ours.is_mach_logic(whole.logic(sub)):
|
|
ts, sp = _types(sub, item, found.group(2), got["입력값"], auto)
|
|
one.update(갈래="장비", 종류=ts, 규격=sp)
|
|
elif hit := _ADD.match(cid):
|
|
given = (doc.get("더한줄") or [])[int(hit[1])].get("입력값") or {}
|
|
v = ours.run(sub, given) or {"L": {}, "X": []}
|
|
k = (one["품"] or 0.0) * ours.times_of(whole.logic(sub))
|
|
one.update(갈래="기초", 로직=sub, L={t: q * k for t, q in v["L"].items()})
|
|
one["X"] = [(set(a), set(b), q * k) for a, b, q in v["X"]]
|
|
out.append(one)
|
|
return out
|
|
|
|
|
|
def measure(doc: dict, whole: Any) -> dict[str, Any]:
|
|
"""SK 1 단위 품 — `{인력 {직종: 품}, 장비 [{갈래, 시간}], 자재 {이름: 수량}}`(시험 · 견줌)."""
|
|
labor: dict[str, float] = {}
|
|
machines, mats = [], {}
|
|
for one in parts(doc, whole):
|
|
if one["갈래"] == "인력" and one["품"]:
|
|
labor[one["이름"]] = labor.get(one["이름"], 0.0) + one["품"]
|
|
elif one["갈래"] == "장비" and one["품"]:
|
|
machines.append({"갈래": sorted(one["종류"]), "시간": one["품"]})
|
|
elif one["갈래"] == "자재" and one["품"]:
|
|
mats[one["이름"]] = mats.get(one["이름"], 0.0) + one["품"]
|
|
elif one["갈래"] == "기초":
|
|
for t, q in one["L"].items():
|
|
labor[t] = labor.get(t, 0.0) + q
|
|
machines += [{"갈래": sorted(a), "시간": q} for a, _, q in one["X"] if q]
|
|
return {"인력": labor, "장비": machines, "자재": mats}
|
|
|
|
|
|
# ── 차이 → 덮개 · 더한줄 · 뺀줄 ──────────────────────────────────────
|
|
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.lines = parts(doc, whole)
|
|
self.lost: list[str] = []
|
|
self.lb = {name: key for key, name in ours._labor_names().items()}
|
|
|
|
def cover(self, cid: str, **one: float) -> None:
|
|
self.doc["덮개"][cid] = {k: _num(v) for k, v in one.items()}
|
|
|
|
def drop(self, cid: str) -> None:
|
|
if cid not in self.doc["뺀줄"]:
|
|
self.doc["뺀줄"].append(cid)
|
|
|
|
def add(self, one: dict[str, Any]) -> None:
|
|
self.doc["더한줄"].append(one)
|
|
|
|
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]
|
|
extra: dict[str, float] = {}
|
|
for x in self.lines:
|
|
for t, q in (x.get("L") or {}).items():
|
|
extra[t] = extra.get(t, 0.0) + q
|
|
for t in sorted(set(excel) | {x["이름"] for x in mine if x["품"]} | set(extra)):
|
|
a, rows = excel.get(t, 0.0), [x for x in mine if x["이름"] == t]
|
|
b, f = sum(x["품"] for x in rows), extra.get(t, 0.0)
|
|
if a and _near(a, b + f):
|
|
continue
|
|
want = a - f
|
|
if want <= TINY:
|
|
for x in rows:
|
|
self.drop(x["cid"])
|
|
if want < -TINY:
|
|
self.lost.append(f"{t} 엑셀 {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} — 우리 인력에 없음")
|
|
elif len(rows) == 1 or not b:
|
|
self.cover(rows[0]["cid"], qty=want * self.tb)
|
|
for x in rows[1:]:
|
|
self.drop(x["cid"])
|
|
else:
|
|
for x in rows:
|
|
self.cover(x["cid"], 배율=want / b)
|
|
|
|
def machines(self, excel: list[dict[str, Any]], line: dict[str, Any]) -> None:
|
|
mine = [x for x in self.lines if x["갈래"] == "장비" and x["품"]]
|
|
fixed = [(a, q) for x in self.lines for a, _, q in x.get("X") or [] if q]
|
|
used, rest = set(), []
|
|
for m in excel:
|
|
kinds, h = set(m["갈래"]), m["시간"]
|
|
hit = next((x for x in mine if x["cid"] not in used and x["종류"] & kinds
|
|
and _near(h, x["품"])), None) # fmt: skip
|
|
if hit is not None:
|
|
used.add(hit["cid"])
|
|
continue
|
|
at = next((i for i, (a, q) in enumerate(fixed) if a & kinds and _near(h, q)), None)
|
|
if at is not None:
|
|
fixed.pop(at)
|
|
continue
|
|
rest.append(m)
|
|
for m in rest:
|
|
kinds, sp = set(m["갈래"]), set(m.get("규격") or [])
|
|
same = [x for x in mine if x["cid"] not in used and x["종류"] & kinds]
|
|
same.sort(key=lambda x: not (sp and sp & x["규격"]))
|
|
if same:
|
|
used.add(same[0]["cid"])
|
|
self.cover(same[0]["cid"], qty=m["시간"] * self.tb)
|
|
else:
|
|
self._add_machine(m, line)
|
|
for x in mine:
|
|
if x["cid"] not in used:
|
|
self.drop(x["cid"])
|
|
self.lost += [f"우리만 장비 {'·'.join(sorted(a))} — 더한 기초 안" for a, _ in fixed]
|
|
|
|
def _add_machine(self, m: dict[str, Any], line: dict[str, Any]) -> None:
|
|
"""엑셀만 장비 → 기계경비 로직 줄(기계 = 그 갈래 · 규격 옵션 거꾸로 찾기)."""
|
|
fp = finger.empty(line)
|
|
fp["장비"] = [{**m, "시간": 1.0, "밑시간": 1.0, "율": []}]
|
|
kinds = set(m["갈래"])
|
|
first = next((c for k, c in combine.MACHINE.items() if k in kinds), combine.MACHINE_ANY)
|
|
for code in [first, *sorted(combine.MACHINE_SET - {first})]:
|
|
got = option.find(code, fp, [])
|
|
if got["수준"] <= 1:
|
|
name = " ".join([*m["갈래"], *(f"{x:g}" for x in m.get("규격") or [])])
|
|
self.add({"종류": "로직", "로직": code, "입력값": got["입력"], "이름": name,
|
|
"단위": "시간", "수량": _num(m["시간"] * self.tb)}) # fmt: skip
|
|
return
|
|
self.lost.append(f"엑셀만 장비 {'·'.join(m['갈래'])} {m['시간']:.6g} 시간 — 기계 못 찾음")
|
|
|
|
def materials(self, excel: list[dict[str, Any]]) -> None:
|
|
mine = [x for x in self.lines if x["갈래"] == "자재" and not x["cid"].startswith("add:")]
|
|
for m in excel:
|
|
words = option.mat_words(m["이름"])
|
|
rows = [x for x in mine if option._named(words, x["이름"])]
|
|
if rows and all(x["품"] is None for x in rows): # 우리 = 별도계상 · 엑셀 = 블록 안
|
|
self.doc["자재표시"][rows[0]["cid"]] = {"별도계상": "직접계산"}
|
|
self.cover(rows[0]["cid"], qty=m["품"] * self.tb)
|
|
continue
|
|
rows = [x for x in rows if x["품"] is not None]
|
|
if rows and _near(m["품"], sum(x["품"] for x in rows)):
|
|
continue
|
|
if rows:
|
|
self.cover(rows[0]["cid"], qty=m["품"] * self.tb)
|
|
for x in rows[1:]:
|
|
self.drop(x["cid"])
|
|
else:
|
|
self.lost.append(f"엑셀만 자재 {m['이름']} {m['품']:.6g}")
|
|
|
|
|
|
def _extras(entry: dict, code: str, whole: Any, tb: float) -> list[dict[str, Any]]:
|
|
"""조합 둘째부터(체크된 로직) = 더한줄 로직 — 수량 = 몫 × 바탕 배 ÷ 그 기초 배."""
|
|
out = []
|
|
for c in entry.get("조합") or []:
|
|
if not c.get("체크") or c["로직"] == code:
|
|
continue
|
|
given = ((entry.get("옵션") or {}).get(c["로직"]) or {}).get("입력") or {}
|
|
times = ours.times_of(whole.logic(c["로직"]))
|
|
qty = float(c.get("몫") or 1) * tb / times
|
|
out.append({"종류": "로직", "로직": c["로직"], "입력값": given, "수량": _num(qty)})
|
|
return out
|
|
|
|
|
|
def _wait(entry: dict) -> 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}
|
|
|
|
|
|
def plan(
|
|
entry: dict | None,
|
|
block: dict | None,
|
|
line: dict[str, Any],
|
|
whole: Any,
|
|
pick: dict | None = None,
|
|
) -> dict[str, Any]:
|
|
"""블록 한 칸 → `{갈래 기초|SK|고름 대기, 바탕, 입력값, 덮개, 자재표시, 더한줄, 뺀줄, 못옮김, 엑셀품,
|
|
후보?}` ·
|
|
`pick` = 사용자가 고른 기초 `{로직, 입력값?}`(옵션 없으면 거꾸로 찾음 · 늘 견줌)."""
|
|
entry = entry or {}
|
|
state = (entry.get("판정") or {}).get("상태")
|
|
efp = finger.excel(block, line)
|
|
if pick:
|
|
code = str(pick["로직"])
|
|
given = pick.get("입력값")
|
|
if given is None:
|
|
given = option.find(code, efp, (block or {}).get("글") or [])["입력"]
|
|
elif state in ("맞음", "다른 점") and entry.get("차례"):
|
|
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)
|
|
from M02_MasterTemplete import M02_Hopyo_Verdict as verdict # 판정이 Fit 을 부름 — 늦게 읽음
|
|
|
|
excel = verdict.excel_qty(efp)
|
|
head = {"바탕": doc["바탕"], "입력값": doc["입력값"], "엑셀품": excel}
|
|
if state == "맞음" and not pick and not doc["더한줄"]:
|
|
return {**head, "갈래": BASE, **dict.fromkeys(KEYS[:2], {}), **dict.fromkeys(KEYS[2:], [])}
|
|
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("품")])
|
|
out = {k: doc[k] for k in KEYS[:4]}
|
|
changed = any(out.values())
|
|
return {**head, **out, "갈래": SK if changed else BASE, "못옮김": work.lost}
|