Merge commit '43e945568' into sub_laptop_1

# Conflicts:
#	M02_MasterTemplete/M02_MasterTemplete_Hopyo_MatPick.ts
#	M02_MasterTemplete/M02_San_MatPick.py
This commit is contained in:
2026-11-13 20:49:48 +09:00
10 changed files with 426 additions and 49 deletions
+34 -3
View File
@@ -186,8 +186,10 @@ _LIST_COST = ("M", "L", "S", "T")
def hopyo(saved: dict) -> dict[str, Any]:
"""원파일 → 실무자료 읽기 꼴 — 단산(산출근거 머리)만 · 일위대가 안 만듦(요구 743-2 · HeadMainDat 안 씀)."""
out: dict[str, Any] = {"꼴": "원파일", "시트": saved["꼴"], "호표": [], "검산": {"어긋남": []}}
"""원파일 → 실무자료 읽기 꼴 — 단산(산출근거 머리) · 호표 = STmate 구성표 일위 줄(828-1 · 엑셀 일위대가표
길과 같은 꼴) · EST 일위대가 안 만듦(요구 743-2 · HeadMainDat 안 씀)."""
out: dict[str, Any] = {"꼴": "원파일", "시트": saved["꼴"], "호표": _ilwi(saved)}
out["검산"] = {"어긋남": []}
out["단산"] = [
{"번": n, "이름": b.get("이름"), "단위": b.get("단위"), "금액": _cost(b.get("저장"))}
for n, b in numbers(saved).items()
@@ -195,6 +197,23 @@ def hopyo(saved: dict) -> dict[str, Any]:
return out
def _ilwi(saved: dict) -> list[dict]:
"""STmate 일위 구성 줄 → 호표(행 = 격자 「일위대가표」 행 · 줄 재현 · 덮음 = `M02_Hopyo_UnitLine`)."""
if saved.get("꼴") != "STmate" or not saved.get("일위"):
return []
from M02_MasterTemplete import M02_Hopyo_UnitLine as ul
from M02_MasterTemplete import M02_San_Ilwi as il
out, at = il.hopyo(saved["일위"]), 2
for h in out:
h["행"] = [at, at + len(h["줄"])]
for i, line in enumerate(h["줄"], 1):
line["행"] = at + i
at += len(h["줄"]) + 2
ul.check_hopyo(h)
return out
def grid(saved: dict) -> dict[str, Any]:
"""원파일 → 단가산출표 한 시트 격자(`M02_Hopyo_Grid.read` 꼴) — 산출근거 머리(코드 · 이름 · 규격 ·
단위 · 저장 금액) 한 줄씩 · 행 = 1 머리 + 블록 차례."""
@@ -214,7 +233,7 @@ def grid(saved: dict) -> dict[str, Any]:
sheet = {"id": "s1", "이름": LIST, "칸": cells, "틀고정": {"행": 1}}
sheet["열"] = {c: {"폭": w} for c, w in zip(cols, (10, 40, 18, 6, 12, 12, 12, 12))}
book = {"종류": "통합문서", "판": 1, "열": "", "서식": [{}], "시트": [sheet], "활성": "s1"}
return {
out = {
"책": book,
"호표": [],
"단산": rows,
@@ -223,6 +242,18 @@ def grid(saved: dict) -> dict[str, Any]:
"목록": {"호표": [], "단산": [{"번": r["번"], "행": r["행"][0]} for r in rows]},
"줄": [],
}
if hp := _ilwi(saved): # 828-1 STmate 구성표 일위대가 = 「일위대가표」 시트(앞)
from M02_MasterTemplete import M02_San_Ilwi as il
book["시트"].insert(0, il.sheet(hp, "s2"))
book["활성"] = "s2"
out["호표"] = [{"번": h["번"], "이름": h["이름"], "행": h["행"]} for h in hp]
out["보기"][il.SHEET] = "s2"
out["원본"][il.SHEET] = il.SHEET
out["줄"] = [
{"번": h["번"], "차례": ln["차례"], "행": ln["행"]} for h in hp for ln in h["줄"]
]
return out
def _dec(v: Any) -> int | float | None:
@@ -41,9 +41,9 @@ interface MatKind {
단서: string[];
별도계상: boolean;
블록: number[];
/** 일위대가 구성 줄이 있는 호표(828-1 · 코드 숫자) */
호표?: number[];
줄: number;
/** 828-2 이 종이 든 일위대가 구성 줄의 호표 코드(STmate 엑셀 숨은 칸 · 원파일 구성표) */
일위?: string[];
고름?: MatCand;
}
@@ -135,13 +135,12 @@ export function createMatPick(opt: { scope: HopyoScope; key: string; target: Hop
function kindRow(k: MatKind): HTMLElement {
const what = `${label(k)}${k.단위 ? ` (${k.단위})` : ""}` || "—";
const hops = k.일위?.length ? ` · 일위대가 ${k.일위.length} 호표` : "";
const sub = [k.방법, `${k.블록.length} 블록${hops} · ${k.줄} 줄`, ...k.단서]
const hp = k.호표?.length ? ` · 일위대가 ${k.호표.length}` : "";
const sub = [k.방법, `${k.블록.length} 블록${hp} · ${k.줄} 줄`, ...k.단서]
.filter(Boolean)
.join(" · ");
const badge = createBadge(k.별도계상 ? `별도계상 · ${k.판정}` : k.판정, TONE[k.판정] ?? "warning", { size: 18 }); // prettier-ignore
const title = `단산 ${k.블록.join(" · ")}${hops ? ` / 일위대가 ${k.일위?.join(" · ")}` : ""}`;
const row = createListRow({ label: what, sub, trailing: badge, size: 36, title }); // prettier-ignore
const row = createListRow({ label: what, sub, trailing: badge, size: 36, title: `단산 ${k.블록.join(" · ")}${hp && ` · 호표 ${k.호표?.join(" · ")}`}` }); // prettier-ignore
const box = el("div", { attrs: { "data-mat-kind": k.종 }, children: [row, controls(k)] });
if (edits.has(k.종)) box.dataset.matEdited = "";
return box;
+3 -1
View File
@@ -415,7 +415,9 @@ class _Stm:
if not one_ and len(sole) == 1: # 비목 꼬리 없어도 값 든 비목 하나면 단가 줄(셈 같음)
one_, to = sole[0], None
res |= {"비목": one_, "옮김": to} if one_ else {}
parts = machine_parts(res) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
parts = (
machine_parts(res, pr.get("같은구성")) if self.keys else []
) # 812-9 · 816-2 같은 파일 구성(828-1)
if parts:
res["찾기"], link, many = parts[0], parts[0]["키"], None
res |= {"조합첫": True} if len(parts) > 1 else {}
+61 -7
View File
@@ -5,7 +5,7 @@
일위 구성 줄 꼴(원파일 · 엑셀 같은 꼴 — 읽기기 답 `일위` 블록 `줄`):
{차례, 코드(자원 · PR · RR · C1 · T1 · 빈), 갈래(M L S X B D W · 율 · 닫기 · 구분 · 글), 이름, 규격, 단위,
수량(1 차), 단가 {T L M S}?(셈에 쓴 자원 단가 · 중기 조종원 = 시간 환산), 금액 {T L M S}?(줄 금액),
율? {밑, 대상(M L S T), 곱(RR)}, 밑줄?(율 밑수에 듦), 관급?, 별도계상?(자원 규격 「별도계상」)}
율? {밑, 대상(M L S T), 곱(RR)}, 밑줄?(율 밑수에 듦), 관급?, 별도계상?(자재 이름 · 규격 「별도계상」 · 「별산」)}
블록 = {코드, 이름, 규격, 단위, 저장, 줄, 줄합 {T L M S}(다시 셈 · 끝전 뒤), 표지}
율 줄 이름 = 지은 글(「노무비의 3%」 · 원문 이름 칸은 못 읽음).
HP 호표(`hopyo`) = 엑셀 일위대가표 읽기(`M02_Hopyo_Stmate.read`)와 같은 꼴 — 글 · 구분 · 닫기 줄 뺌.
@@ -123,7 +123,9 @@ def lines(rows: list[dict], res: dict[str, dict], opt: dict) -> tuple[list[dict]
one["밑줄"] = True
if row.get("KWNGP") == "1":
one["관급"] = True
if "별도계상" in one["규격"]:
if kind == "M" and re.search(
r"별도계상|별산", one["이름"] + one["규격"]
): # 엑셀 비고 「별산…」 짝
one["별도계상"] = True
out.append(one)
before = dict(s)
@@ -144,7 +146,9 @@ def lines(rows: list[dict], res: dict[str, dict], opt: dict) -> tuple[list[dict]
continue
if kind not in KIND:
continue
saved = head.get("저장") or {}
saved = (
head.get("원단가") or head.get("저장") or {}
) # 낙찰률 줄 = 2 칸(smt_engine costSlot)
unit = one["단위"].strip()
own = "L" if code[0] == "L" else "S" if code[0] == "S" else "M"
pct = re.fullmatch(r"([MLST])%", unit)
@@ -161,8 +165,8 @@ def lines(rows: list[dict], res: dict[str, dict], opt: dict) -> tuple[list[dict]
p = {k: _f(saved.get(k)) for k in "MLS"}
if code in opt["환산"]:
p = {"M": 0.0, "L": _wage(p["L"], opt["환산식"]), "S": 0.0}
if unit == "$":
p = {k: v * opt["달러"] / 1000 for k, v in p.items()}
if unit == "$": # 달러 → 천원(엑셀 「중기사용료」 값 · 원 미만 버림)
p = {k: _tr(v * opt["달러"] / 1000) for k, v in p.items()}
elif unit in ("€", "¥", "¥"):
p = dict.fromkeys("MLS", 0.0)
one["단가"] = {"T": p["M"] + p["L"] + p["S"], **p}
@@ -187,7 +191,7 @@ def make_of(name: str, spec: str, rows: list[dict]) -> dict[str, Any]:
make: dict[str, Any] = {"이름": name, "규격": spec, "손료": []}
for one in rows:
g, q, unit = one["갈래"], one["수량"], one["단위"].strip().upper()
if g == "S" and unit == "천원" and q and (one.get("단가") or {}).get("S"):
if g == "S" and unit in ("천원", "$") and q and (one.get("단가") or {}).get("S"):
make["손료"].append([q * 10_000, one["단가"]["S"] * 1000, False])
elif g == "L" and q > 0 and not make.get("조종원"):
make["조종원"], make["조종원직종"] = q, one["이름"].strip()
@@ -222,7 +226,7 @@ def compose(
if parent[:1] not in ("B", "X") or not head:
continue
got, tot = lines(group, res, options(parent, ini, sbm.get(parent)))
saved = head.get("저장") or {}
saved = head.get("원단가") or head.get("저장") or {}
ok = not _f(saved.get("T")) or same(tot, saved)
if parent[0] == "B":
blocks[parent] = {"줄": got, "줄합": tot, "표지": [] if ok else [MISMATCH]}
@@ -272,3 +276,53 @@ def hopyo(blocks: list[dict]) -> list[dict[str, Any]]:
head |= {k: str(b.get(k) or "") for k in ("이름", "규격", "단위")}
out.append({**head, "금액": _hp(b.get("저장")), "줄": rows})
return out
SHEET = "일위대가표"
_COLS = "ABCDEFGHI"
_HEAD = ("품명", "규격", "수량", "단위", "재료비", "노무비", "경비", "합계", "코드")
def sheet(hps: list[dict], sid: str) -> dict[str, Any]:
"""HP 호표(행 있음) → 격자 「일위대가표」 시트(머리 줄 = 「제N호표」 · 금액 = 저장 · 줄 금액 = 다시 셈)."""
cells: dict[str, dict] = {f"{c}1": {"값": h} for c, h in zip(_COLS, _HEAD)}
def put(at: int, vals: list) -> None:
for c, v in zip(_COLS, vals):
if v not in (None, ""):
cells[f"{c}{at}"] = {"값": v}
for h in hps:
money = [h["금액"].get(k) for k in ("재료비", "노무비", "경비", "계")]
put(
h["행"][0],
[f"제{h['번']}호표 {h['이름']}", h["규격"], None, h["단위"], *money, h["코드"]],
)
for ln in h["줄"]:
money = [ln["금액"].get(k) for k in ("재료비", "노무비", "경비", "계")]
put(ln["행"], [ln["이름"], ln["규격"], ln["수량"], ln["단위"], *money, ln["코드"]])
widths = (36, 22, 10, 6, 12, 12, 12, 12, 10)
return {
"id": sid,
"이름": SHEET,
"칸": cells,
"틀고정": {"행": 1},
"열": {c: {"폭": w} for c, w in zip(_COLS, widths)},
}
def materials(saved: dict):
"""일위 구성 자재 줄(M) → (블록, 줄, 자원 {이름 · 규격 · 단위 · 코드 · 재료찾기?}) — 재료 찾기 = 산근 줄과 같은
길(827-2 `M02_San_Link.m01` · 파일 단서 · 머리 = 부모 일위 이름 규격) · 유가 · 환율 줄 = 재료찾기 없음."""
from M02_MasterTemplete import M02_San_Link as link
from M02_MasterTemplete import M02_San_MatFind as mf
for b in saved.get("일위") or []:
head = f"{b.get('이름') or ''} {b.get('규격') or ''}"
for ln in b.get("줄") or []:
if ln.get("갈래") != "M":
continue
res = {k: ln.get(k) or "" for k in ("이름", "규격", "단위", "코드")} | {"종류": "자재"}
with mf.clues_on(saved.get("단서"), head):
link.m01("재료", res, "")
yield b, ln, res
+22 -20
View File
@@ -180,9 +180,8 @@ def entry(item: dict, sym: str, excel: Any) -> dict | None:
# ── 판(그 파일 재료 종) ──────────────────────────────────────────────────
def census(folder: str | Path, key: str) -> list[dict]:
"""HP 블록마다 재료 줄 판정 → 종 [{종, 이름, 규격, 단위, 판정, 방법, 키, 후보, 단서, 별도계상, 블록, 줄,
일위?}] · 일위 = 그 종이 든 일위대가 구성 줄의 호표 코드(828-2 · 고름은 산근 블록 만들기에만 얹음)."""
호표?(828-1 일위 구성 줄 · 코드 숫자)}]."""
from M02_MasterTemplete import M02_Doc_FromNative as fn
from M02_MasterTemplete import M02_San_MatFind as mf
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh
hopyo_store.read(folder, key) # 없는 HP = 404
@@ -208,34 +207,37 @@ def census(folder: str | Path, key: str) -> list[dict]:
found = res.get("재료찾기")
if not found:
continue
g = _group(groups, res)
k = kind_of(res)
g = groups.setdefault(
k,
{"종": k, **{x: _text(res.get(x)) for x in ("이름", "규격", "단위")},
**{x: found.get(x) for x in ("판정", "방법", "키", "후보", "단서")},
"별도계상": _apart(res), "블록": [], "줄": 0},
) # fmt: skip
g["줄"] += 1
if no not in g["블록"]:
g["블록"].append(no)
for blk, res in (
mf.ilwi_found(src) if src else []
): # 828-2 일위대가 구성 줄 재료(일위 = 호표 코드)
if not res.get("재료찾기"):
from M02_MasterTemplete import M02_San_Ilwi as il
for b, _, res in il.materials(src) if src else (): # 828-1 일위 구성 줄(호표 = 코드 숫자)
if not (found := res.get("재료찾기")):
continue
g = _group(groups, res)
k = kind_of(res)
g = groups.setdefault(
k,
{"종": k, **{x: _text(res.get(x)) for x in ("이름", "규격", "단위")},
**{x: found.get(x) for x in ("판정", "방법", "키", "후보", "단서")},
"별도계상": _apart(res), "블록": [], "줄": 0},
) # fmt: skip
g["줄"] += 1
if blk["코드"] not in (hops := g.setdefault("일위", [])):
hops.append(blk["코드"])
hp = g.setdefault("호표", [])
if (n := int(str(b["코드"])[1:])) not in hp:
hp.append(n)
out_ = sorted(groups.values(), key=lambda g: (KINDS.index(g["판정"]) if g["판정"] in KINDS else 9, -len(g["블록"]), g["이름"])) # fmt: skip
atomic_write_json(path, {**saved, CENSUS: out_})
return out_
def _group(groups: dict[str, dict], res: dict) -> dict:
found, k = res["재료찾기"], kind_of(res)
return groups.setdefault(
k,
{"종": k, **{x: _text(res.get(x)) for x in ("이름", "규격", "단위")},
**{x: found.get(x) for x in ("판정", "방법", "키", "후보", "단서")},
"별도계상": _apart(res), "블록": [], "줄": 0},
) # fmt: skip
def fingerprint(folder: str | Path, key: str) -> tuple[str, str]:
hp = hopyo_store.read(folder, key)
return str(hp.get("지문") or ""), str(hp.get("파일") or "")
+1 -1
View File
@@ -151,7 +151,7 @@ def _stm_sheet(rows: list[list]) -> dict[str, dict]:
cur["손료"].append([_num(v[2]) * 10_000, _num(v[4]) * 1000, False])
elif len(v) > 2 and str(v[2]) == "인" and (_num(v[1]) or 0) > 0 and not cur.get("조종원"):
cur["조종원"], cur["조종원직종"] = _num(v[1]), str(v[0]).strip()
elif first == "잡품" and len(v) > 2 and cur.get("잡재료율") is None:
elif first in ("잡품", "잡재료", "잡재료비") and len(v) > 2 and cur.get("잡재료율") is None:
cur["잡재료율"] = _num(v[2])
elif len(v) > 3 and str(v[3]).upper() in ("L", "ℓ"):
_fuel(cur, v[0], v[2])
+8 -4
View File
@@ -8,7 +8,8 @@
<HII 꼬리표 · 길이 · 블록 번호>) · BOQ11.DBF(내역).
BDQTY.DBF(일위대가 · 중기 구성) = 828-1 `M02_San_Bdqty`(믿을 칸 · 스스로 검사 · 어긋나면 물러남 = 일위 줄 없음 +
표지) → `M02_San_Ilwi`(일위 구성 줄 · 다시 셈 · 중기 구성 `기계구성` · 같은 파일 구성 대조 `같은구성`).
SYSINFO = 난독화 → 표지. 금액 칸 = 차수 1 · 낙찰률 줄(NAGCD > 0) = 2 칸(원단가 · 828-1 ㉱ · 2~4 차 수량은 못 읽음).
SYSINFO = 난독화 → 표지. 금액 칸 = 차수 1(산근 · 자원 = 엑셀 단가산출근거와 같음) · 낙찰률 줄(NAGCD > 0) 2 칸 =
`원단가`(828-1 ㉱ — 구성 셈 · 일위 머리 = 엑셀 일위대가표와 같음 · 2~4 차 수량은 못 읽음).
"""
from __future__ import annotations
@@ -160,7 +161,7 @@ def read(data: bytes) -> dict[str, Any]:
code = r.get("CODE") or ""
if not code:
continue
saved = _amt(r, AMT2 if _f(r.get("NAGCD")) > 0 else AMT)
saved = _amt(r, AMT)
res[code] = {
"종류": KIND.get(code[:1]),
"이름": r.get("NAME"),
@@ -168,6 +169,8 @@ def read(data: bytes) -> dict[str, Any]:
"단위": r.get("UNIT"),
"저장": saved,
}
if _f(r.get("NAGCD")) > 0: # 낙찰률 줄 원단가(828-1 ㉱ — 구성 셈 · 일위 머리)
res[code]["원단가"] = _amt(r, AMT2)
prices[code] = {**saved, "meta": {k: r.get(f) for k, f in META.items()}}
out: dict[str, Any] = {"꼴": "STmate", "산근": [], "일위": [], "자원": res, "단가": prices}
ini = z.read(names["START8X.INI"]).decode("cp949", "replace") if "START8X.INI" in names else ""
@@ -179,7 +182,8 @@ def read(data: bytes) -> dict[str, Any]:
continue
tags: list[str] = []
head = {k: res[code][k] for k in ("이름", "규격", "단위", "저장")}
if text is None and code in comp:
if text is None and code in comp: # 일위 머리 = 원단가(엑셀 일위대가표와 같음)
head["저장"] = res[code].get("원단가") or head["저장"]
out["일위"].append({"코드": code, **head, **comp[code]})
continue
if text is None:
@@ -210,7 +214,7 @@ def read(data: bytes) -> dict[str, Any]:
*([read_ok] if why is None else [UNREAD[0], FALLBACK + why]),
UNREAD[1],
*out.pop("구성표지", []),
"금액 = 차수 1 칸(TAMT1 · LAMT1 · MAMT1 · SAMT1) · 낙찰률 줄 = 2 칸(원단가)",
"금액 = 차수 1 칸(TAMT1 · LAMT1 · MAMT1 · SAMT1) · 일위 머리 · 구성 셈의 낙찰률 줄 = 2 칸(원단가)",
"블록 끝전 갈래 = 옵션 SAN_TYP 후보(뜻 미확정)",
*(f"모르는 코드 머리 {c}" for c in odd),
]