Merge remote-tracking branches 'origin/main_laptop_1' and 'origin/sub_laptop_6' into sub_laptop_8
This commit is contained in:
@@ -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 []:
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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["키"]))]
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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["원래"]["품명"] == "임도표지석"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user