Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1

This commit is contained in:
2026-11-01 21:51:57 +09:00
11 changed files with 261 additions and 87 deletions
+24 -3
View File
@@ -25,6 +25,8 @@
통째 대신 · 로직 자재 겹침 · 전국만. `본뜬.품셈` = 품셈으로 넣은 엑셀 줄 차례([품셈으로 바꾸기] 첫 체크) ·
`줄수 {품셈, 고정, 호표, 빠짐, 이음, 이음후보}` · 만든 값 ↔ 엑셀 호표 단가(`비교` · 같음 = 비목마다 1 원 안).
엔진 · 일위대가 계산은 그대로(`M02_Labor_Ref.Expand`).
수량(723-1 · 내역 17-1) = 저장 때 걷음(`refs.no_qty`) · 엑셀 수량은 `본뜬.실무수량 {줄: {차례: 수}, 자재: {코드: 수}}`
(출처 기록 · 셈 안 씀 · 엑셀 대조 `compare` 만 — 하위 호표 것까지 `excel_given`).
"""
from __future__ import annotations
@@ -90,7 +92,12 @@ def confirmed(folder: str | Path, source: refs.Here | None = None) -> dict[str,
def _save(doc: dict[str, Any]) -> tuple[dict[str, Any], bool]:
"""(참조, 다시씀) — 같은 본뜬(키 · 원본)이 있으면 그 키에 덮음 · 없으면 새 키."""
"""(참조, 다시씀) — 같은 본뜬(키 · 원본)이 있으면 그 키에 덮음 · 없으면 새 키 · 수량 걷음(723-1)."""
hands = {
"줄": {str(i): x["수량"]["손"] for i, x in enumerate(doc["담은로직"]) if x.get("수량")},
"자재": {m["코드"]: m["수량"]["손"] for m in doc.get("자재") or [] if m.get("수량")},
}
doc = {**refs.no_qty(doc), "본뜬": {**doc["본뜬"], "실무수량": hands}}
at = refs.here()
want = doc["본뜬"]
have = next(
@@ -411,10 +418,24 @@ def _excel_round(got: dict[str, Any], how: str, by_line: bool) -> Decimal | None
return sum(v.to_integral_value(how) for v in parts.values())
def excel_given(ref: dict[str, Any], depth: int = 1) -> dict[str, Any]:
"""`본뜬.실무수량` → 안 줄 알약 값(723-1 · 하위 사전조합까지) — 엑셀 대조 전용."""
doc = refs.read_ref(ref) if depth <= refs.DEPTH else None
if not isinstance(doc, dict):
return {}
rid, hands = refs.ref_id(ref), (doc.get("본뜬") or {}).get("실무수량") or {}
out = {f"{rid}#{i}": v for i, v in (hands.get("줄") or {}).items()}
out |= {f"{rid}#자재|{code}": v for code, v in (hands.get("자재") or {}).items()}
for one in refs.refs_in(doc):
out |= excel_given(one, depth + 1)
return out
def compare(ref: dict[str, Any], hopyo: dict[str, Any]) -> dict[str, Any]:
"""만든 사전조합 1 단위 값 ↔ 엑셀 호표 단가(참고 · 판정 아님) — 우리 = 엑셀 끝수 규칙(내림 ·
반올림 중 엑셀과 같은 쪽 · `_excel_round`)."""
got = refs.Expand(m01.loaded()[1])({k: ref[k] for k in ("층", "주인", "키")})
반올림 중 엑셀과 같은 쪽 · `_excel_round`) · 수량 = 엑셀 수량(`excel_given` · 723-1)."""
head = {k: ref[k] for k in ("층", "주인", "키")}
got = refs.Expand(m01.loaded()[1], given=excel_given(head))(head)
base = got.get("기준") or {}
excel = (hopyo.get("금액") or {}).get("계")
cuts = [_excel_round(got, *one) for one in CUTS] if base else []
+5 -6
View File
@@ -12,8 +12,8 @@
기준단가가 된 칸 이름(`기준칸`)도 실음(43-3).
켜쌓기율(85-2) = 표 주문 「골쌓기 기준」 로직만 · % · 없음 = 100 · 그 로직 세부항목 줄 전부 수량 × 율
(`layered` · `run_line` 한 곳 → 문서 · 프로젝트 사본 · 서고 같은 함수).
참조 줄(단가승격 · 사전조합 · 50-3) = 기준이 가리킨 문서 1 단위 값(펼침 `M02_Labor_Ref.Expand`) ·
분모 = 그 문서 단위 · 배 1 · 알약 키 = `ref_id` · 같은 참조 두 번 금지 · 위 층만.
참조 줄(단가승격 · 사전조합 · 50-3) = 기준이 가리킨 문서 1 단위 값(펼침 `M02_Labor_Ref.Expand` · 안 줄
수량 = 같은 given 의 안 줄 알약 · 723-1) · 분모 = 그 문서 단위 · 배 1 · 알약 키 = `ref_id` · 같은 참조 두 번 금지.
짝(CLAUDE.md 5장 ①): 수량 · 반영 · 합 `settle` ↔ `M02_MasterTemplete_Labor_Calc.ts` —
거울 시험 `resources/tester/test_m02_labor_calc_mirror.py`.
규격 열(704-4) = 문서 `규격?` 칸 · 열마다 줄 수량(`M02_Labor_Spec` · settle `col`) · 계산값 `규격합`.
@@ -354,7 +354,7 @@ def pills(doc: dict) -> list[dict[str, Any]]:
"""면적산출 알약 — 로직 물량 알약(`combo_pills` 그대로) + 참조 알약(`refs.pill`) +
자재 알약 「자재|MT키」."""
logic = combo_store.combo_pills({"담은로직": doc.get("담은로직") or []})["물량"]
logic += [refs.pill(one["참조"]) for one in doc.get("담은로직") or [] if _is_ref(one)]
logic += [p for one in doc.get("담은로직") or [] if _is_ref(one) for p in refs.pills_of(one["참조"])] # fmt: skip
mats = [
{
"키": material_key(m["코드"]),
@@ -507,8 +507,8 @@ def calc(
프로젝트 사본 셈(142-1)도 이 한 벌 — `whole` = 사본 `단가.json`(`M02_Item_Prices.load_prices`) ·
`read` = 참조 → 굳힌 `참조사본` · `pills` = 사본 `알약`(가져온 때 것). 없으면 지금 M01 · 층."""
whole = m01.loaded()[1] if whole is None else whole
expand = refs.Expand(whole) if read is None else refs.Expand(whole, read)
whole, have = m01.loaded()[1] if whole is None else whole, _area_of(doc.get("열")) if given is None else given # fmt: skip
expand = refs.Expand(whole, read or refs.read_ref, have) # 사전조합 안 줄 수량 = 알약 값(723-1)
if pills is None:
pills = combo_store.combo_pills({"담은로직": doc.get("담은로직") or []})["물량"]
by_logic = {code: pill for pill in pills for code in pill.get("로직") or []}
@@ -541,7 +541,6 @@ def calc(
}
for one in doc.get("자재") or []
]
have = _area_of(doc.get("열")) if given is None else given
return settle(_json({"줄": lines, "자재": mats}), doc, have)
+80 -11
View File
@@ -2,6 +2,9 @@
참조 → `{이름, 단위, 기준(1 단위), 까닭, 안?}` — 단가승격(옛 SP) · 사전조합(SC) · 단가산출(SK ·
`M02_Sangeun_Calc.unit`). 이름은 `M02_Labor_Ref.Expand` 로 그대로 부름.
사전조합 안 줄 수량(723-1 · 내역 17-1) = 쓰는 쪽 `given` 의 알약 「참조|층|주인|키#차례」(자재 =
「…#자재|코드」 · `inner_pills`) · 문서 손은 굳힌 사본(`hands` · 프로젝트 · 불러오기 대조)과 비율 줄(율)만 ·
없으면 「수량 없음」(1 아님 · 그 사전조합 기준 없음).
"""
from __future__ import annotations
@@ -16,14 +19,35 @@ from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Sangeun_Calc as calc
Ref = dict[str, Any]
NO_QTY = "수량 없음"
MAT = "자재|"
class Expand:
"""참조 → `{이름, 단위, 기준(1 단위), 까닭, 안?}` — 한 번 셈 안에서 키별 캐시 ·
깊이 상한 · 순환은 까닭(통째로 멈추지 않음). `read` = 참조 → 문서(프로젝트는 굳힌 사본)."""
def __init__(self, whole: Any, read: Callable[[Ref], dict | None] = refs.read_ref) -> None:
def __init__(
self,
whole: Any,
read: Callable[[Ref], dict | None] = refs.read_ref,
given: dict[str, Any] | None = None,
hands: bool | None = None,
) -> None:
self.whole, self.read, self.memo = whole, read, {}
self.given = given or {}
self.hands = (
read is not refs.read_ref if hands is None else hands
) # 층 원본 손 = 옛 값(안 씀)
def _qty(self, one: dict, key: str) -> Decimal | None:
"""안 줄 수량 — 손(굳힌 사본 · 비율 줄) · 알약 값(given) · 없음."""
from M02_MasterTemplete import M02_Labor_Doc as labor
hand = labor._number((one.get("수량") or {}).get("손"))
if hand is not None and (self.hands or refs.is_rate(one)):
return hand
return labor._number(self.given.get(key))
def __call__(self, ref: Ref, trail: tuple[str, ...] = ()) -> dict[str, Any]:
rid = refs.ref_id(ref)
@@ -110,8 +134,8 @@ class Expand:
for pill in pills or combo_store.combo_pills({"담은로직": lines})["물량"]
for code in pill["로직"]
}
inner, rows, why = [], [], None
for one in lines:
inner, rows, why, rid = [], [], None, trail[-1] if trail else ""
for at, one in enumerate(lines):
ref = one.get("참조")
if isinstance(ref, dict): # 줄 덮개(715-3) 있는 SK 참조 = 그 줄만 다시 셈
head = {"참조": ref}
@@ -122,18 +146,21 @@ class Expand:
head, sub = {"고정": one["고정"]}, fixed.line(self.whole, one["고정"])
else:
continue
qty = labor._number((one.get("수량") or {}).get("손"))
qty = Decimal(1) if qty is None else qty
qty = self._qty(one, f"{rid}#{at}")
keep = ("이름", "단위", "기준", "까닭")
inner.append({**head, "수량": qty, **{k: sub.get(k) for k in keep}})
if sub.get("기준") is None:
why = why or f"「{sub.get('이름')}」 {sub.get('까닭')}"
continue
rows.append({k: v * qty for k, v in sub["기준"].items()})
for mat in doc.get("자재") or []: # 사용자 자재(175-2) = 기준단가 × 손 · 빈칸 = 안 셈
qty = labor._number((mat.get("수량") or {}).get("손"))
if qty is None or labor.apart(mat): # 별도계상 = 안 셈 · 안 줄에도 없음(222-1)
if qty is None: # 723-1 — 이은 값 없음 = 금액 빠짐(1 로 안 셈)
inner[-1]["까닭"] = NO_QTY
why = why or f"「{sub.get('이름')}」 {NO_QTY}"
continue
rows.append({k: v * qty for k, v in sub["기준"].items()})
for mat in doc.get("자재") or []: # 사용자 자재(175-2) = 기준단가 × 수량(이은 값)
if labor.apart(mat): # 별도계상 = 안 셈 · 안 줄에도 없음(222-1)
continue
qty = self._qty(mat, f"{rid}#{MAT}{mat['코드']}")
got = labor.material_price(self.whole, mat["코드"], mat.get("단위"))
price = labor._number(got["기준단가"])
zero = dict.fromkeys(labor.COSTS, Decimal(0))
@@ -149,8 +176,9 @@ class Expand:
"까닭": got["까닭"],
}
)
if base is None:
why = why or f"「{name}」 {got['까닭']}"
if qty is None or base is None:
inner[-1]["까닭"] = got["까닭"] or NO_QTY # 단가 까닭 먼저
why = why or f"「{name}」 {inner[-1]['까닭']}"
continue
rows.append({k: v * qty for k, v in base.items()})
under = labor._sum(rows)
@@ -172,3 +200,44 @@ def head_of(doc: dict) -> str | None:
"""상세 머리 「1 ㎡ 당」(715-3 · 일위대가 = 기준단위 1 당) — 단위 빈 옛 문서는 None."""
unit = str(doc.get("단위") or "").strip()
return f"1 {unit} 당" if unit else None
def inner_pills(
ref: Ref, read: Callable[[Ref], dict | None] = refs.read_ref, depth: int = 1
) -> list[dict[str, Any]]:
"""사전조합 안 줄 알약(723-1) — 키 「참조|…#차례」 · 「참조|…#자재|코드」 · 이름 = 「일위대가 · 줄」 ·
단위 = 그 줄 분모 · 비율 줄 뺌 · 안의 사전조합도 한 번씩(깊이 상한) · 사전조합 아니면 빈 것."""
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
doc = read(ref) if refs.kind_of(ref["키"]) == "precombo" and depth <= refs.DEPTH else None
if not isinstance(doc, dict):
return []
rid, top = refs.ref_id(ref), doc.get("이름") or ref["키"]
lines = [one for one in doc.get("담은로직") or [] if isinstance(one, dict)]
by_logic = {
code: pill
for pill in combo_store.combo_pills({"담은로직": lines})["물량"]
for code in pill["로직"]
}
out: list[dict[str, Any]] = []
for at, one in enumerate(lines):
if refs.is_rate(one):
continue
sub = one.get("참조")
if isinstance(sub, dict):
got = read(sub) or {}
name, unit = got.get("이름") or sub["키"], got.get("단위")
out += inner_pills(sub, read, depth + 1)
elif fixed.is_fixed(one):
name, unit = one["고정"].get("이름"), one["고정"].get("단위")
else:
pill = by_logic.get(one.get("로직")) or {}
name, unit = pill.get("이름") or one.get("로직"), pill.get("단위")
name = one.get("별칭") or name
out.append({"키": f"{rid}#{at}", "이름": f"{top} · {name}", "단위": unit or "", "로직": [], "참조": ref}) # fmt: skip
for mat in doc.get("자재") or []:
name = mat.get("이름") or mat.get("코드")
key = f"{rid}#{MAT}{mat.get('코드')}"
out.append({"키": key, "이름": f"{top} · {name}", "단위": mat.get("단위") or "", "로직": [], "참조": ref}) # fmt: skip
seen: set[str] = set()
return [p for p in out if not (p["키"] in seen or seen.add(p["키"]))]
+25 -5
View File
@@ -7,9 +7,9 @@
precombo `{층}/precombo/SC-######.json` 사전조합
`{종류:"사전조합", 키, 이름, 단위, 본뜬, 담은로직:[{참조, 수량:{손}, 메모}
| {로직, 입력값, 메모, 수량:{손}, 고름?, 자재표시?}], 자재:[일위대가 자재 꼴]}` —
승격 · 사전조합 · 단가산출 로직(51-11) · 수량은 손(기본 1) ·
자재 수량은 손 · 빈칸 = 안 셈(175) ·
값 = Σ 안 줄 1 단위 값 × 수량(로직 줄 = 기준 ÷ 배) + Σ 자재 기준단가 × 손
승격 · 사전조합 · 단가산출 로직(51-11) · 안 줄 · 자재 수량 = 저장 안 함(늘 null ·
723-1 · 내역 17-1 · `no_qty` · 비율 줄 율만 남김) · 쓰는 쪽 알약으로 이음(`Expand` given) ·
값 = Σ 안 줄 1 단위 값 × 이은 수량(로직 줄 = 기준 ÷ 배) + Σ 자재 기준단가 × 이은 수량
sangeun `{층}/sangeun/SK-######.json` 단가산출(715-2) — 꼴 `M02_Sangeun_Doc` · 셈
`M02_Sangeun_Calc` · 참조 자리 = `바탕.참조` · `더한줄[].참조`(`refs_in`)
참조 = `{층, 주인, 키}`(마스터 주인 null · 회사 `C` · 개인 `C/U`) — 종류는 키 머리(SP · SC · SK) ·
@@ -170,7 +170,19 @@ def clean_doc(kind: str, doc: Any) -> dict[str, Any]:
out["자재"] = _combo_mats(doc, out["담은로직"])
out.update({k: str(doc.get(k) or "").strip() for k in ("구분", "상세구분")})
out["본뜬"] = doc["본뜬"] if isinstance(doc.get("본뜬"), dict) else None
return out
return no_qty(out)
def is_rate(one: Any) -> bool:
"""비율 고정 줄(705-3) — 수량 = 율(%) · 기준단위 당 수량 아님."""
return isinstance(one, dict) and bool((one.get("고정") or {}).get("비율"))
def no_qty(doc: dict[str, Any]) -> dict[str, Any]:
"""사전조합 안 줄 · 자재 수량 걷음(723-1 · 내역 17-1 = M02 에 저장 안 함) · 비율 줄 율만 남김."""
lines = [one if is_rate(one) else {**one, "수량": None} for one in doc.get("담은로직") or []]
mats = [{**one, "수량": None} for one in doc.get("자재") or []]
return {**doc, "담은로직": lines, "자재": mats}
_OLD_CLASS = re.compile(r"품셈") # 134-5 — 옛 품셈 분류(건설품셈 공통 · 산림품셈 · 07장 …)
@@ -234,7 +246,7 @@ def _combo_lines(doc: dict, key: str) -> list[dict[str, Any]]:
head.update(로직=one["로직"], 입력값=one["입력값"])
head.update({k: one[k] for k in ("켜쌓기율", "고름", "자재표시") if k in one})
head.update({"별칭": one["별칭"]} if one["별칭"] else {}) # 실무 품명(324-3)
lines.append({**head, "수량": one["수량"] or {"손": 1}, "메모": one["메모"]})
lines.append({**head, "수량": one["수량"], "메모": one["메모"]})
at_ = here()
me = {"층": at_[0], "주인": at_[1], "키": key}
for one in lines:
@@ -458,9 +470,17 @@ def with_pills(doc: dict[str, Any]) -> dict[str, Any]:
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
lines = [one for one in doc.get("담은로직") or [] if isinstance(one, dict)]
doc = no_qty({**doc, "담은로직": lines}) # 723-1 — 옛 손도 굳히지 않음(쓰는 쪽 알약으로)
return {**doc, "알약": combo_store.combo_pills({"담은로직": lines})["물량"]}
def pills_of(ref: Ref) -> list[dict[str, Any]]:
"""참조 줄 알약 + 사전조합 안 줄 알약(723-1 · `inner_pills`)."""
from M02_MasterTemplete.M02_Labor_Expand import inner_pills
return [pill(ref), *inner_pills(ref)]
def frozen_materials(copies: dict[str, Any]) -> list[str]:
"""굳힌 사전조합의 자재 · 고른 로직 자재 코드(175-2) — 프로젝트 단가 묶음에 넣음."""
out = []