Merge remote-tracking branches 'origin/main_laptop_1' and 'origin/sub_laptop_6' into sub_laptop_8

This commit is contained in:
2026-11-01 21:55:23 +09:00
13 changed files with 265 additions and 89 deletions
+21 -10
View File
@@ -58,11 +58,14 @@ def _qty(value: Any) -> float | None:
return _num(float(value) if isinstance(value, Decimal) else value)
def _ref_mats(whole: Any, read: Any, ref: dict, trail: tuple[str, ...] = ()) -> list[tuple]:
def _ref_mats(
whole: Any, read: Any, ref: dict, trail: tuple[str, ...] = (), given: dict | None = None
) -> list[tuple]:
"""참조 문서 1 단위 재료 `[(재료 줄, 수량)]` — 층 셈 펼침(`M02_Labor_Ref.Expand`)과 같은 길:
승격 = 원 로직 재료 ÷ 배 · 사전조합 = 안 줄 재료 × 손 수량(빈칸 1) · 로직 줄은 알약 배로 나눔 ·
사전조합 자재(175-2) = 손 수량(빈칸 = 안 셈 · 품목 키 = 코드 · `자재` 표시 = 연료 거름 안 함) ·
순환 · 깊이 넘음 · 없는 문서 = 빈 것."""
승격 = 원 로직 재료 ÷ 배 · 사전조합 = 안 줄 재료 × 안 줄 수량(굳힌 손 · 아니면 면적 알약
「참조|…#차례」 값 `given` · 723-1 · 없으면 안 셈) · 로직 줄은 알약 배로 나눔 ·
사전조합 자재(175-2) = 손 · 알약 「…#자재|코드」(없음 = 안 셈 · 품목 키 = 코드 · `자재` 표시 =
연료 거름 안 함) · 순환 · 깊이 넘음 · 없는 문서 = 빈 것."""
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Ref as refs
@@ -90,17 +93,23 @@ def _ref_mats(whole: Any, read: Any, ref: dict, trail: tuple[str, ...] = ()) ->
pills = kept if isinstance(kept, list) else combo_store.combo_pills({"담은로직": lines})["물량"]
by_logic = {code: pill for pill in pills for code in pill.get("로직") or []}
out: list[tuple] = []
for one in lines:
given = given or {}
def qty_of(one: dict, key: str) -> float | None: # 723-1 — 굳힌 손 · 아니면 이은 칸 값
hand = _qty((one.get("수량") or {}).get("손"))
return _qty(given.get(f"{rid}#{key}")) if hand is None else hand
for at, one in enumerate(lines):
if isinstance(one.get("참조"), dict):
sub = _ref_mats(whole, read, one["참조"], (*trail, rid))
sub = _ref_mats(whole, read, one["참조"], (*trail, rid), given)
elif one.get("로직"):
sub = run(one, (by_logic.get(one["로직"]) or {}).get("배"))
else:
continue
hand = _qty((one.get("수량") or {}).get("손"))
out += [(m, q * (1.0 if hand is None else hand)) for m, q in sub]
if (hand := qty_of(one, str(at))) is not None:
out += [(m, q * hand) for m, q in sub]
for mat in doc.get("자재") or []:
if hand := _qty((mat.get("수량") or {}).get("손")):
if hand := qty_of(mat, f"자재|{mat.get('코드')}"):
line = {k: mat.get(k) for k in ("이름", "단위")}
out.append(({**line, "품목": {"키": mat.get("코드")}, "자재": True}, hand))
return out
@@ -111,6 +120,7 @@ def _folder_units(root: str | Path, folder: str, meta: dict[str, dict]) -> dict[
(키 = 고른 품목 MT 코드 · 없으면 이름|단위 · 재료 수량 × 줄 수량 ÷ 배) + 참조 줄 안 재료
(160-3) · 줄 수량 빈칸 · 0 · 파형강관 · 기계 연료는 안 담음 · 셈 못 하면 빈 것."""
from M02_MasterTemplete import M02_Item_Copy as copies
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete.M02_Item_Prices import load_prices
from M02_MasterTemplete.M02_Item_Source import LABOR_FILE, PRICE_FILE, read_json
@@ -149,6 +159,7 @@ def _folder_units(root: str | Path, folder: str, meta: dict[str, dict]) -> dict[
frozen = (labor.get("참조사본") if isinstance(labor, dict) else None) or {}
whole = None # 사본 단가 — 참조 줄이 있을 때만 읽음
given = labor_doc.area_values(read_json(copies.folder_of(root, folder) / copies.KINDS["면적"]))
def read(ref: dict) -> Any:
return frozen.get(refs.ref_id(ref))
@@ -159,7 +170,7 @@ def _folder_units(root: str | Path, folder: str, meta: dict[str, dict]) -> dict[
continue
if isinstance(line.get("참조"), dict): # 참조 줄 = 배 1
whole = load_prices(bundle) if whole is None else whole
for mat, per in _ref_mats(whole, read, line["참조"]):
for mat, per in _ref_mats(whole, read, line["참조"], given=given):
logic_mat(mat, per * qty)
continue
for mat in line.get("재료") or []:
+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 = []
@@ -143,6 +143,8 @@ export function buildLibrarySection(opt: LibraryOptions): LibraryHandle {
add.classList.add("m02-tree__add");
add.title = L("M02_TreeAddTop");
add.setAttribute("aria-label", L("M02_TreeAddTop"));
// 구조물 조합 줄과 같은 자리(줄 오른쪽 끝 · `m02-tree__tools`)
const addTools = el("span", { className: "ui-toolbar m02-tree__tools", children: [add] });
const leaves = new Map<string, HTMLElement>();
const toggled = new Map<string, boolean>(); // 손으로 펴고 접은 폴더 — 다시 그려도 그대로
const targets = new Map<string, Target>();
@@ -470,7 +472,7 @@ export function buildLibrarySection(opt: LibraryOptions): LibraryHandle {
const mine = ++seq;
const at = opt.scope();
add.hidden = !at.canWrite;
barHost.replaceChildren(opt.bar(add));
barHost.replaceChildren(opt.bar(addTools));
if (!keep) treeHost.replaceChildren(createEmpty({ text: tc("Loading"), small: true }));
const [got, tree] = await Promise.all([
// 못 받으면 빈 나무(목록표에서 까닭) · 폴더를 못 받으면 문서는 모두 뿌리
@@ -96,7 +96,8 @@ def test_참조_줄_재료_연료_뺌_고른_품목(tmp_path, monkeypatch):
price = {"묶음": {"줄": [{"키": "MT9", "이름": "레디믹스트 레미콘", "규격": "25-18-80"}]}}
frozen = {
ref_id(SC): {
"담은로직": [{"로직": "L1", "수량": {"손": 2}}, {"참조": SP}],
# 굳힌 옛 사본 = 안 줄 손 다 있음(723-1 앞 · 빈 수량 = 이은 칸 값 · 없으면 안 셈)
"담은로직": [{"로직": "L1", "수량": {"손": 2}}, {"참조": SP, "수량": {"손": 1}}],
"알약": [{"로직": ["L1"], "배": 10}],
},
ref_id(SP): {"담은로직": [{"로직": "L2"}], "배": 4},
+43 -20
View File
@@ -2,7 +2,7 @@
175-1 화면 · 저장 — 서고 모드 자재목록 · 로직 자재 세 칸 막힘 풂 · 저장 `_combo_lines` 가
고름 · 자재표시 남김 · 문서 `자재`(일위대가 자재 검사 · 로직 자재와 같은 품목 400).
175-2 셈 — 담은 자재(손 · 빈칸 = 안 셈)가 사전조합 1 단위 값(`Expand._combo` · 안 줄) · B08 참조 줄
175-2 셈 — 담은 자재(수량 = 이은 알약 값 · 723-1 · 손은 저장 안 함)가 사전조합 1 단위 값(`Expand._combo` · 안 줄) · B08 참조 줄
재료(`_ref_mats`) · B09 겹 대가 자재 줄 · 프로젝트 사본 단가 묶음(`frozen_materials`)에 들어감.
"""
@@ -16,7 +16,7 @@ from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 임시 층 fixture
from resources.tester.test_m02_labor_ref import _area, world # noqa: F401 — 임시 층 fixture
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "..", "..", "M02_MasterTemplete")
@@ -51,7 +51,7 @@ def test_저장_왕복_고름_자재표시_자재_남음(world): # noqa: F811
_, got = _save(world["system"], {"담은로직": [line], "자재": mats})
one = got["담은로직"][0]
assert one["고름"] == {"4": NET_ITEM} and one["자재표시"] == {"3": {"자재구분": "관급"}}
assert [(m["코드"], m["수량"]) for m in got["자재"]] == [(MAT, {"손": 2}), (WIRE, None)]
assert [(m["코드"], m["수량"]) for m in got["자재"]] == [(MAT, None), (WIRE, None)] # 723-1
assert got["자재"][0]["운반비"] == "미포함" and got["자재"][0]["이름"] != "틀린 이름"
# 자재 없음 = 빈 목록(옛 문서 꼴 그대로 읽힘)
_, empty = _save(world["system"], {"담은로직": [{"로직": NET}]})
@@ -73,8 +73,23 @@ def _price(code):
return Decimal(str(labor.material_price(whole, code, None)["기준단가"]))
def _given(key):
"""안 줄 알약 값(723-1) — STONE 1 · NET 0 · MAT 2 · WIRE 0."""
rid = refs.ref_id({**SYS, "키": key})
return {f"{rid}#0": 1, f"{rid}#1": 0, f"{rid}#자재|{MAT}": 2, f"{rid}#자재|{WIRE}": 0}
def _legacy(world, key): # noqa: F811
"""옛 손 수량 문서(723-1 앞 · 굳힌 사본 꼴) — B08 · B09 셈 길 시험용 · 파일은 안 바꿈."""
doc = json.loads((world["system"] / "precombo" / f"{key}.json").read_bytes())
hands = ({"손": 1}, {"손": 0})
lines = [{**one, "수량": q} for one, q in zip(doc["담은로직"], hands)]
mats = [{**m, "수량": {"손": 2} if m["코드"] == MAT else None} for m in doc["자재"]]
return {**doc, "담은로직": lines, "자재": mats}
def _world_combo(world): # noqa: F811
"""STONE 1 + MAT 손 2 + WIRE 빈칸(안 셈) · NET 고름 4 = NET_ITEM — (키, 손셈 1 단위 값)."""
"""STONE 1 + MAT 2 + WIRE 0 · NET 고름 4 = NET_ITEM — (키, 1 단위 값) · 수량은 `_given`."""
lines = [
{"로직": STONE, "수량": {"손": 1}},
{"로직": NET, "수량": {"손": 0}, "고름": {"4": NET_ITEM}},
@@ -95,34 +110,42 @@ def test_사전조합_값에_자재_금액_빈칸은_안_셈_안_줄에_자재(w
doc = labor.clean(
{"열": "rv", "담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}]}
)
one = labor.calc(doc, {})["줄"][0]
bare = labor.calc(doc, {})["줄"][0]
one = labor.calc(doc, _given(key))["줄"][0]
assert bare["기준"] is None and "수량 없음" in bare["까닭"] # 723-1 — 안 이으면 값 없음
assert one["기준"]["계"] == pytest.approx(float(want))
assert one["반영"]["계"] == pytest.approx(float(want * 3))
mats = [x for x in one["안"] if x.get("자재")]
assert [(x["자재"], x["수량"]) for x in mats] == [(MAT, 2)] # WIRE 빈칸 = 안 셈
assert [(x["자재"], x["수량"]) for x in mats] == [(MAT, 2)] # WIRE = 기본 별도계상(안 셈)
assert mats[0]["기준"]["재료비"] == pytest.approx(float(_price(MAT)))
# 목록 기준금액(51-4)도 같은 펼침
# 목록 기준금액(51-4) — 이은 값 없음 = 빈 값(723-1)
rows = [{"종류": "precombo", "이름": key}]
refs.summaries(world["system"], rows)
assert rows[0]["기준금액"] == pytest.approx(float(want))
assert rows[0]["기준금액"] is None
def test_B08_참조_줄_재료에_사전조합_자재_손_곱(world): # noqa: F811
from B08_Quantity.B08_Quantity_Router_Material import _ref_mats
key, _ = _world_combo(world)
with store.at(world["system"]):
outer = refs.create(
"precombo",
{
"이름": "겹",
"단위": "㎡",
"담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}],
},
)["이름"]
got = _ref_mats(m01.loaded()[1], refs.read_ref, {**SYS, "키": outer})
outer = {
"이름": "겹",
"단위": "㎡",
"담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}],
}
docs = {"SC-겹": outer, key: _legacy(world, key)} # 옛 손 문서(723-1 앞 꼴 · B08 길만 잼)
got = _ref_mats(m01.loaded()[1], lambda r: docs.get(r["키"]), {**SYS, "키": "SC-겹"})
mats = [(m["품목"]["키"], q) for m, q in got if m.get("자재")]
assert mats == [(MAT, pytest.approx(6.0))] # 2 × 겹 손 3 · WIRE 빈칸 없음
# 723-1 — 수량 없는 새 문서 = 면적 알약 값(given)으로 · 안 이으면 안 셈
live = {"SC-겹": outer, key: refs.read_ref({**SYS, "키": key})}
bare = _ref_mats(m01.loaded()[1], lambda r: live.get(r["키"]), {**SYS, "키": "SC-겹"})
assert [m for m, _ in bare if m.get("자재")] == []
got = _ref_mats(
m01.loaded()[1], lambda r: live.get(r["키"]), {**SYS, "키": "SC-겹"}, given=_given(key)
)
mats = [(m["품목"]["키"], q) for m, q in got if m.get("자재")]
assert mats == [(MAT, pytest.approx(6.0))]
def test_B09_겹_대가에_자재_줄(world): # noqa: F811
@@ -131,7 +154,7 @@ def test_B09_겹_대가에_자재_줄(world): # noqa: F811
key, _ = _world_combo(world)
ref = {**SYS, "키": key}
build = Builder()
bid = build.precombo(m01.loaded()[1], ref, refs.read_ref(ref), {})
bid = build.precombo(m01.loaded()[1], ref, _legacy(world, key), {}) # 옛 손 문서(723-1 앞)
mats = [x for x in build.blocks[bid]["lines"] if x["kind"] == "material"]
assert [(x["key"], x["qty"]) for x in mats] == [(MAT, 2.0), (WIRE, None)]
assert mats[0]["name"] and mats[0]["unit"]
@@ -150,5 +173,5 @@ def test_사본_단가_묶음에_사전조합_자재_고름_사본_값_같음(wo
assert "refs.frozen_materials(" in open(source.__file__, encoding="utf-8").read()
bundle = prices.to_json(prices.collect(refs.frozen_logics(rows["참조사본"]), mats))
(world["system"] / "precombo" / f"{key}.json").unlink() # 원본이 사라져도 굳힌 사본
got = prices.calc(rows, bundle, {}, {}, None)["줄"][0]
got = prices.calc(rows, bundle, {}, {}, _area(_given(key)))["줄"][0] # 사본 면적 칸 = 안 줄 값
assert got["기준"]["계"] == pytest.approx(float(want), rel=1e-9)
+5 -1
View File
@@ -32,8 +32,12 @@ def _sc(folder, lines, unit="㎡"):
def _value(folder, key):
"""안 줄 수량 = 모두 1(723-1 · 저장 안 함 → 쓰는 쪽 알약 값)."""
ref = {**MINE, "키": key}
with store.at(folder):
return refs.Expand(_whole())({**MINE, "키": key})
lines = refs.read_ref(ref)["담은로직"]
given = {f"{refs.ref_id(ref)}#{i}": 1 for i in range(len(lines))}
return refs.Expand(_whole(), given=given)(ref)
def test_logic_line_cover_in_precombo(world):
@@ -64,7 +64,7 @@ def test_화면_층_단추_최상위_폴더_이름_바꾸기():
# 720-2 — 층 줄 「+」 = 서고 칸 맨 위 메뉴 · 메뉴 줄 = 같은 addFolder([])
assert page.count(" add,\n") == 3 # 구조물 조합 · 일위대가 · 단가산출
assert "onClick: (event) => openTreeMenu(event, menuAt([]))" in lib
assert "add.hidden = !at.canWrite;" in lib and "opt.bar(add)" in lib
assert "add.hidden = !at.canWrite;" in lib and "opt.bar(addTools)" in lib
assert lib.count('L("M02_TreeAddTopFolder"), () => void addFolder([])') == 1 # 폴더 줄
assert '{ label: L("M02_TreeAddTopFolder"), run: () => void addFolder([])' in lib # 문서 줄
tree = _text("M02_MasterTemplete_TreePanel.ts")
+20 -11
View File
@@ -61,22 +61,24 @@ def test_확인한_줄만_별칭_수량_빠진_줄_장부(client):
assert origin["확인"]["누가"] == {"id": 42, "이름": "갑"} and origin["확인"]["때"]
stone, *made, soil = doc["담은로직"] # 705-3 — 확인 안 한 줄 · 「우리에 없음」 = 고정 줄
assert stone["고정"]["단가"] == {"노무비": 0, "재료비": 1400000, "경비": 0}
assert stone["고정"]["출처"]["줄"] == 0 and stone["수량"] == {"손": 1}
assert soil["고정"]["이름"] == "잔토처리(인력100%)" and soil["수량"] == {"손": 0.09}
hands = origin["실무수량"]["줄"] # 723-1 — 수량 = 저장 안 함 · 엑셀 수량은 출처 기록만
assert stone["고정"]["출처"]["줄"] == 0 and stone["수량"] is None and hands["0"] == 1
assert soil["고정"]["이름"] == "잔토처리(인력100%)" and soil["수량"] is None
assert hands[str(len(doc["담은로직"]) - 1)] == 0.09
assert origin["품셈"] == [1, 2] and got["줄수"]["고정"] == 2 and got["빠진줄"] == []
assert made == [
{
"로직": "GF000101",
"입력값": {"지역": "서울"},
"별칭": "구조물터파기(인력100%)",
"수량": {"손": 0.14},
"수량": None, # 723-1 — 엑셀 0.14 = 본뜬.실무수량
"메모": "실무 0.14㎥ × 1 · 토사",
},
{
"로직": "GF000113",
"입력값": {},
"별칭": "구조물되메우기(인력100%)",
"수량": {"손": 0.1}, # 엑셀 0.05 × 배 2
"수량": None, # 엑셀 0.05 × 배 2 = 본뜬.실무수량(723-1)
"메모": "실무 0.05㎥ × 2",
},
]
@@ -108,14 +110,20 @@ def test_로직_여럿_같은_별칭_감쌈_자재_몫_덮음(client):
first, second, fx, wrapped = doc["담은로직"]
assert fx["고정"]["출처"]["줄"] == 2 # 확인 안 한 줄 = 고정(705-3)
assert first["별칭"] == second["별칭"] == "구조물터파기(인력100%)" # 한 줄 로직 둘 = 같은 별칭
assert second["수량"] == {"손": 0.014} and second["메모"] == "실무 0.14㎥ × 0.1(몫 0.1) · 토사"
assert wrapped["별칭"] == "잔토처리(인력100%)" and wrapped["수량"] == {"손": 0.09}
hands = doc["본뜬"]["실무수량"]["줄"] # 723-1 — 저장 수량 없음 · 엑셀 수량 = 출처 기록
assert second["수량"] is None and hands["1"] == 0.014
assert second["메모"] == "실무 0.14㎥ × 0.1(몫 0.1) · 토사"
assert (
wrapped["별칭"] == "잔토처리(인력100%)" and wrapped["수량"] is None and hands["3"] == 0.09
)
inner = _doc(mine, wrapped["참조"]["키"])
assert inner["본뜬"]["키"] == "HP-000001 제3호표 4째 줄 GF000101"
assert inner["담은로직"][0]["로직"] == "GF000101" and inner["담은로직"][0]["수량"] == {"손": 1}
assert inner["담은로직"][0]["로직"] == "GF000101" and inner["담은로직"][0]["수량"] is None
assert inner["본뜬"]["실무수량"]["줄"] == {"0": 1}
assert [(m["코드"], m["수량"], m["별도계상"]) for m in doc["자재"]] == [
("MT004258", {"손": 1}, "직접계산")
("MT004258", None, "직접계산")
]
assert doc["본뜬"]["실무수량"]["자재"] == {"MT004258": 1}
stone = next(v for k, v in _book(mine).items() if k.startswith("임도표지석(받침"))
assert stone["짝"][0]["자재"] == "MT004258"
files = sorted(p.name for p in (mine / "precombo").glob("*.json"))
@@ -232,14 +240,15 @@ def test_하위_호표_먼저_확인_퍼센트_빠짐_통째(client):
assert rate["비율"] == {"기준": "재료비", "비목": "재료비"}
got = _build(client, 45, ref)
mine = layers.personal_dir(7, 42)
line = _doc(mine, got["참조"]["키"])["담은로직"][0]
made = _doc(mine, got["참조"]["키"])
line = made["담은로직"][0]
assert line["참조"] == child["참조"] and line["별칭"] == "고화매트재료비"
assert line["수량"] == {"손": 2.3}
assert line["수량"] is None and made["본뜬"]["실무수량"]["줄"]["0"] == 2.3 # 723-1
assert "고화매트재료비||m2" not in _book(mine) # 호표 참조 줄 = 장부 안 씀
whole = _build(client, 3, 통째={"대상": [{"로직": "GF000101", "배": 1}]})
doc = _doc(mine, whole["참조"]["키"])
assert [x["로직"] for x in doc["담은로직"]] == ["GF000101"]
assert doc["담은로직"][0]["수량"] == {"손": 1}
assert doc["담은로직"][0]["수량"] is None and doc["본뜬"]["실무수량"]["줄"] == {"0": 1}
assert {x["까닭"] for x in whole["빠진줄"]} == {"호표 통째로 대신"}
head = next(v for k, v in _book(mine).items() if k.startswith("호표|임도표지석|"))
assert head["짝"][0]["로직"] == "GF000101" and head["원래"]["품명"] == "임도표지석"
+4 -2
View File
@@ -128,8 +128,10 @@ def test_섞인_줄_몫_배(client):
assert screen["lines"][0]["checkable"] and not screen["lines"][0]["checked"]
one = {k: part[k] for k in ("로직", "입력값", "배", "몫")}
made = _build(client, "HP-000001", no, [{"차례": line["차례"], "대상": [one]}])
row = next(x for x in _doc(made["참조"])["담은로직"] if x.get("로직") == "GF000211")
assert row["수량"]["손"] == round(line["수량"] * 0.1, 6)
doc = _doc(made["참조"])
at = next(i for i, x in enumerate(doc["담은로직"]) if x.get("로직") == "GF000211")
assert doc["담은로직"][at]["수량"] is None # 723-1 — 수량 저장 안 함 · 엑셀 수량 = 출처 기록
assert doc["본뜬"]["실무수량"]["줄"][str(at)] == round(line["수량"] * 0.1, 6)
def test_막는_길(client):
+32 -17
View File
@@ -73,6 +73,13 @@ def _combo(folder, lines, name="조합"):
return refs.create("precombo", {"이름": name, "단위": "㎡", "담은로직": lines})["이름"]
def _area(given):
"""면적산출 사본 꼴 — 알약 키마다 칸 하나(723-1 · 안 줄 수량 = 이은 칸 값)."""
cells = {f"A{i}": {"값": v} for i, v in enumerate(given.values(), 1)}
links = [{"칸": f"A{i}", "키": k} for i, k in enumerate(given, 1)]
return {"시트": [{"id": "s", "칸": cells, "연결": links}]}
def _read(folder, kind, key):
return json.loads((folder / kind / f"{key}.json").read_bytes())
@@ -221,7 +228,9 @@ def test_calc_는_참조를_펼침_승격은_기준_나누기_배_사전조합
ref = {**SYS, "키": sc}
with store.at(world["system"]):
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 5}}]})
got = labor.calc(doc, {})
bare = labor.calc(doc, {})["줄"][0] # 723-1 — 안 줄 손은 저장 안 함 · 안 이으면 수량 없음
got = labor.calc(doc, {f"{refs.ref_id(ref)}#0": 2}) # 안 줄 알약 = 구조물 조합 칸 값
assert bare["기준"] is None and "수량 없음" in bare["까닭"] and bare["안"][0]["수량"] is None
one = got["줄"][0]
want = Decimal(str(_base(TEN)["계"])) / 10 * 2
assert one["기준"]["계"] == pytest.approx(float(want))
@@ -233,7 +242,9 @@ def test_calc_는_참조를_펼침_승격은_기준_나누기_배_사전조합
assert one["반영"]["계"] == pytest.approx(float(want * 5))
assert one["안"][0]["수량"] == 2 and one["안"][0]["참조"]["키"] == sp
pills = labor.pills(doc)
assert pills == [{"키": refs.ref_id(ref), "이름": "둘", "단위": "㎡", "로직": [], "참조": ref}]
assert pills[0] == {"키": refs.ref_id(ref), "이름": "둘", "단위": "㎡", "로직": [], "참조": ref}
assert [p["키"] for p in pills[1:]] == [f"{refs.ref_id(ref)}#0"] # 안 줄 알약(723-1)
assert pills[1]["이름"].startswith("둘 · ")
def test_승격_줄_상세는_원_로직_키_표_사전조합은_안_줄만(world):
@@ -386,9 +397,10 @@ def test_프로젝트는_참조사본으로_굳힘_원본을_지워도_같은_
sp = _promote(world["system"])
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 2}}])
ref = {**SYS, "키": sc}
given = {f"{refs.ref_id(ref)}#0": 2} # 723-1 — 안 줄 수량 = 이은 칸 값
with store.at(world["system"]):
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 1}}]})
live = labor.calc(doc, {})["줄"][0]["기준"]["계"]
live = labor.calc(doc, given)["줄"][0]["기준"]["계"]
rows = source.labor_doc_rows(doc)
assert set(rows["참조사본"]) == {refs.ref_id(ref), refs.ref_id({**SYS, "키": sp})}
assert refs.frozen_logics(rows["참조사본"]) == [TEN]
@@ -396,7 +408,7 @@ def test_프로젝트는_참조사본으로_굳힘_원본을_지워도_같은_
for key in (sc, sp): # 원본이 사라져도 프로젝트는 사본만 봄
kind = refs.kind_of(key)
(world["system"] / kind / f"{key}.json").unlink()
got = prices.calc(rows, bundle, {}, {}, None)
got = prices.calc(rows, bundle, {}, {}, _area(given))
line = got["줄"][0]
assert not line["까닭"] and line["참조"] == ref # 층 셈 꼴(142-1)
assert line["기준"]["계"] == pytest.approx(live, rel=1e-9)
@@ -438,8 +450,8 @@ def test_층_목록_서고_줄은_이름_단위_기준금액을_실음(world):
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 3}}], "셋")
rows = [{"종류": "precombo", "이름": sc}, {"종류": "labor", "이름": "rv"}]
refs.summaries(world["system"], rows)
one = float(Decimal(str(_base(TEN)["계"])) / 10)
assert (rows[0]["제목"], rows[0]["기준금액"]) == ("셋", pytest.approx(one * 3))
# 723-1 — 안 줄 수량 저장 안 함 → 목록 기준금액 없음(쓰는 쪽 알약으로만 값)
assert (rows[0]["제목"], rows[0]["기준금액"]) == ("셋", None)
assert "기준금액" not in rows[1]
@@ -456,16 +468,17 @@ def test_사전조합_로직_줄_섞임_합은_손셈_지우기_409_굳힘(world
]
sc = _combo(world["system"], lines, "섞음")
saved = _read(world["system"], "precombo", sc)["담은로직"]
assert saved[1:] == [
{"로직": STONE, "입력값": {}, "수량": {"손": 3}, "메모": "돌"},
{"로직": TEN, "입력값": {}, "수량": {"손": 1}, "메모": None},
assert saved[1:] == [ # 723-1 — 안 줄 수량 저장 안 함
{"로직": STONE, "입력값": {}, "수량": None, "메모": "돌"},
{"로직": TEN, "입력값": {}, "수량": None, "메모": None},
]
ten, stone = (Decimal(str(_base(c)["계"])) for c in (TEN, STONE))
want = ten / 10 * 2 + stone * 3 + ten / 10 * 1 # 손셈 — 승격 · TEN 은 원/10㎡(배 10)
want = ten / 10 * 2 + stone * 3 + ten / 10 * 1 # 이은 수량 — 승격 · TEN 은 원/10㎡(배 10)
ref = {**SYS, "키": sc}
given = {f"{refs.ref_id(ref)}#{i}": q for i, q in enumerate((2, 3, 1))}
with store.at(world["system"]):
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 5}}]})
one = labor.calc(doc, {})["줄"][0]
one = labor.calc(doc, given)["줄"][0]
assert one["기준"]["계"] == pytest.approx(float(want))
assert one["반영"]["계"] == pytest.approx(float(want * 5))
assert [x.get("로직") or x["참조"]["키"] for x in one["안"]] == [sp, STONE, TEN]
@@ -482,16 +495,16 @@ def test_사전조합_로직_줄_섞임_합은_손셈_지우기_409_굳힘(world
bundle = prices.to_json(prices.collect([TEN, STONE]))
for key in (sc, sp): # 원본이 사라져도 굳힌 사본으로 같은 값
(world["system"] / refs.kind_of(key) / f"{key}.json").unlink()
got = prices.calc(rows, bundle, {}, {}, None)["줄"][0]
got = prices.calc(rows, bundle, {}, {}, _area(given))["줄"][0]
assert got["기준"]["계"] == pytest.approx(float(want), rel=1e-9)
def test_사전조합이_자기_자신을_담으면_422_수량_기본_1(world):
def test_사전조합이_자기_자신을_담으면_422_수량_없음(world):
sp = _promote(world["system"])
a = _combo(world["system"], [{"참조": {**SYS, "키": sp}}], "가")
with store.at(world["system"]):
have = store.read("precombo", a)
assert have["문서"]["담은로직"][0]["수량"] == {"손": 1}
assert have["문서"]["담은로직"][0]["수량"] is None # 723-1 — 기본 1 걷음
with pytest.raises(store.StoreError) as e:
store.write(
"precombo",
@@ -535,14 +548,16 @@ def test_사전조합_굳힘은_안_로직_알약도_사본_값_142_2(world, mon
with store.at(world["system"]):
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 1}}]})
rows = source.labor_doc_rows(doc)
given = {f"{refs.ref_id(ref)}#0": 1}
kept = rows["참조사본"][refs.ref_id(ref)]["알약"]
assert [(p["로직"], p.get("배")) for p in kept] == [([TEN], 10)]
assert "알약" not in _read(world["system"], "precombo", sc) # 층 문서는 그대로
bundle = prices.to_json(prices.collect([TEN]))
before = prices.calc(rows, bundle, {}, {}, None)["줄"][0]["기준"]["계"]
before = prices.calc(rows, bundle, {}, {}, _area(given))["줄"][0]["기준"]["계"]
changed = {"물량": [{"키": "x|㎡", "이름": "x", "단위": "㎡", "로직": [TEN]}]} # 배 1 로 바뀜
monkeypatch.setattr(combo_store, "combo_pills", lambda row: changed)
assert prices.calc(rows, bundle, {}, {}, None)["줄"][0]["기준"]["계"] == pytest.approx(before)
again = prices.calc(rows, bundle, {}, {}, _area(given))["줄"][0]["기준"]["계"]
assert again == pytest.approx(before)
with store.at(world["system"]):
live = labor.calc(doc, {})["줄"][0]["기준"]["계"]
live = labor.calc(doc, given)["줄"][0]["기준"]["계"]
assert live == pytest.approx(before * 10) # 층 셈은 지금 M01(배 1)