diff --git a/B08_Quantity/B08_Quantity_Router_Material.py b/B08_Quantity/B08_Quantity_Router_Material.py index 483076076..a611db080 100644 --- a/B08_Quantity/B08_Quantity_Router_Material.py +++ b/B08_Quantity/B08_Quantity_Router_Material.py @@ -63,7 +63,7 @@ def _ref_mats( ) -> list[tuple]: """참조 문서 1 단위 재료 `[(재료 줄, 수량)]` — 층 셈 펼침(`M02_Labor_Ref.Expand`)과 같은 길: 승격 = 원 로직 재료 ÷ 배 · 사전조합 = 안 줄 재료 × 안 줄 수량(굳힌 손 · 아니면 면적 알약 - 「참조|…#차례」 값 `given` · 723-1 · 없으면 안 셈) · 로직 줄은 알약 배로 나눔 · + 「참조|…#차례」 값 `given` · 그다음 굳힌 저장 수량(742-1) · 없으면 안 셈) · 로직 줄은 알약 배로 나눔 · 사전조합 자재(175-2) = 손 · 알약 「…#자재|코드」(없음 = 안 셈 · 품목 키 = 코드 · `자재` 표시 = 연료 거름 안 함) · 순환 · 깊이 넘음 · 없는 문서 = 빈 것.""" from M01_MasterData import M01_MasterData_Store_Combo as combo_store @@ -95,9 +95,12 @@ def _ref_mats( out: list[tuple] = [] 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 + def qty_of(one: dict, key: str) -> float | None: # 굳힌 손 → 이은 칸 값 → 저장 수량(742-1) + qty = one.get("수량") if isinstance(one.get("수량"), dict) else {} + hand = _qty(qty.get("손")) + if hand is None: + hand = _qty(given.get(f"{rid}#{key}")) + return _qty(qty.get(refs.SAVED)) if hand is None else hand for at, one in enumerate(lines): if isinstance(one.get("참조"), dict): diff --git a/B09_Estimation/B09_Estimation_Bill_Hopyo.py b/B09_Estimation/B09_Estimation_Bill_Hopyo.py index e3f32b602..d911ed45b 100644 --- a/B09_Estimation/B09_Estimation_Bill_Hopyo.py +++ b/B09_Estimation/B09_Estimation_Bill_Hopyo.py @@ -141,7 +141,7 @@ def _copies(docs: dict[int, dict[str, Any]], tag: str, no: int) -> dict[str, dic rid = refs.ref_id(_ref(tag, at)) if rid in out or at not in docs: continue - # 이 공사 엑셀 수량 = 굳힌 손(723-4 · `with_pills` 의 723-1 수량 걷기는 M02 저장용) + # 이 공사 엑셀 수량 = 굳힌 손(723-4 · `with_pills` 의 저장 수량 옮김(742-1)은 M02 저장용) doc = docs[at] out[rid] = {**refs.with_pills(doc), **{k: doc.get(k) or [] for k in ("담은로직", "자재")}} todo += [ diff --git a/B09_Estimation/B09_Estimation_Daega_Personal.py b/B09_Estimation/B09_Estimation_Daega_Personal.py index 73438ccf7..9495f7dad 100644 --- a/B09_Estimation/B09_Estimation_Daega_Personal.py +++ b/B09_Estimation/B09_Estimation_Daega_Personal.py @@ -2,7 +2,7 @@ 담는 것 = 담은로직(넣을 값 · 고른 자재 얹음) · 자재 · 줄 수량(= 수량산출 수기 `손`) — 맨 위 줄 수량 덮개 = 손(원래 손 × 덮개 ÷ 블록 줄 수량 · 대가 줄 ÷ 배를 되돌림) · - SC 쓸 때 손 걷힘(723-1 `refs.no_qty` · 쓰는 곳 알약 · 이 공사 입력으로 이음) + SC 저장 수량(742-1 · 쓰는 곳 알약 · 이 공사 입력이 이어지면 그 값이 앞섬) 안 담는 것 = 품 덮개(부른 블록 안 줄 · 품셈 블록 줄 — 로직 통째 배수로 바꾸면 다른 줄까지 바뀜) · 단가 덮개(사전조합 꼴에 단가 칸 없음) → 수만 알림(품은 PLAN 705 고정 줄이 생기면 담김) 원천 = 블록 cover — 조합 `S:`/`L:` = 사본 폴더(일위대가 · 넣을 값 · 고른 자재 · 셈 수량) · @@ -79,13 +79,21 @@ def _frozen(root: Path, rid: str) -> dict[str, Any] | None: return None +def _hand(one: dict[str, Any], none: Any) -> Any: + """굳힌 줄 수량 → 층 저장 꼴 `{손}` — 굳힌 손 · 저장 수량(742-1 `refs.SAVED`) · 없으면 `none`.""" + qty = one.get("수량") if isinstance(one.get("수량"), dict) else {} + hand = qty.get("손", qty.get(refs.SAVED)) + return none if hand is None else {"손": hand} + + def _precombo(doc: dict[str, Any]) -> tuple[list[dict], list[dict]]: lines = [ - {**one, "수량": {"손": (one.get("수량") or {}).get("손", 1)}} + {**one, "수량": _hand(one, {"손": 1})} for one in doc.get("담은로직") or [] if isinstance(one, dict) ] - return lines, [m for m in doc.get("자재") or [] if isinstance(m, dict)] + mats = [{**m, "수량": _hand(m, None)} for m in doc.get("자재") or [] if isinstance(m, dict)] + return lines, mats def source(root: Path, block: dict[str, Any], base: str) -> tuple[list[dict], list[dict]]: diff --git a/B09_Estimation/B09_Estimation_Labor_Source.py b/B09_Estimation/B09_Estimation_Labor_Source.py index 621602eee..389544545 100644 --- a/B09_Estimation/B09_Estimation_Labor_Source.py +++ b/B09_Estimation/B09_Estimation_Labor_Source.py @@ -336,17 +336,20 @@ class Builder: depth: int = 0, use: tuple[dict, str] = ({}, ""), ) -> str: - """사전조합(SC) = 겹 대가 — 로직 줄 = 품셈 호표 · 자재 줄(175-2) · 줄 수량(723-4) = - 굳힌 손 → use 알약 값 → 없음(`ask`) · use = (쓰는 구조물 사본 면적 알약 값, 덮개 자리).""" + """사전조합(SC) = 겹 대가 — 로직 줄 = 품셈 호표 · 자재 줄(175-2) · 줄 수량(723-4 · 742-1) = + 굳힌 손 → use 알약 값 → 저장 수량(`refs.SAVED`) → 없음(`ask`) · use = (쓰는 구조물 사본 면적 + 알약 값, 덮개 자리).""" from M01_MasterData import M01_MasterData_Store_Combo as combo_store rid, (given, where) = refs.ref_id(ref), use pilled: list[bool] = [] # 줄마다 알약 값을 썼나 - def qty_of(key: str, hand: Any) -> float | None: # 짝 = `M02_Labor_Expand._qty` 차례 - got = _f(hand) + def qty_of(key: str, qty: Any) -> float | None: # 짝 = `M02_Labor_Expand._qty` 차례 + qty = qty if isinstance(qty, dict) else {} + got = _f(qty.get("손")) + pill = None if got is not None else _f(given.get(key)) pilled.append(got is None and key in given) - return _f(given.get(key)) if got is None else got + return got if got is not None else pill if pill is not None else _f(qty.get(refs.SAVED)) inner = [one for one in doc.get("담은로직") or [] if isinstance(one, dict)] kept = doc.get("알약") # 굳힌 알약(142-2 · 지금 M01 안 읽음 · 719-2) · 없으면 셈 @@ -358,7 +361,7 @@ class Builder: } lines = [] for n, one in enumerate(inner): - qty = qty_of(f"{rid}#{n}", (one.get("수량") or {}).get("손")) + qty = qty_of(f"{rid}#{n}", one.get("수량")) pill = pills.get(one.get("로직")) or {} times = pill.get("배") if "로직" in one else 1 name = pill.get("이름") or str(one.get("로직") or "") @@ -368,7 +371,7 @@ class Builder: lines.append(self.line_of(whole, one, qty, times, name, copies, depth, use)) for mat in doc.get("자재") or []: key = f"{rid}#{labor_doc.material_key(mat.get('코드'))}" - lines.append(_material(whole, mat, qty_of(key, (mat.get("수량") or {}).get("손")))) + lines.append(_material(whole, mat, qty_of(key, mat.get("수량")))) lines = [ln if ln["kind"] == "blank" or ln.get("qty") is not None else {**ln, "ask": ASK} for ln in lines] # fmt: skip return self.put( diff --git a/M02_MasterTemplete/M02_Hopyo_Build.py b/M02_MasterTemplete/M02_Hopyo_Build.py index 1713e0323..0dc19505c 100644 --- a/M02_MasterTemplete/M02_Hopyo_Build.py +++ b/M02_MasterTemplete/M02_Hopyo_Build.py @@ -30,8 +30,8 @@ 참조(차이 보존) · 노임 = 이름 · 기계 = EQ(`M02_Hopyo_Fixed.Linker`) · 사용자 이음 대상 `{노임|기계: 키}` · `{비율: 기준}` = 고정 줄 + 그 이음 · 남은 줄(`Fixed.linked` 아님 · 「우리에 없음」 고른 줄 뺌) 있으면 409 `{까닭, 남은줄}`(저장 안 함 · `save` 거짓 = 막지 않고 `남은줄` 만) · 미리 보기 = `unlinked`(쓰기 없음). -수량(723-1 · 내역 17-1) = 저장 때 걷음(`refs.no_qty`) · 엑셀 수량은 `본뜬.실무수량 {줄: {차례: 수}, 자재: {코드: 수}}` -(출처 기록 · 셈 안 씀 · 엑셀 대조 `compare` 만 — 하위 호표 것까지 `excel_given`). +수량(742-1 · 요구 584-1) = 엑셀 수량을 줄 · 자재 저장 수량(`{손}`)으로 · 같은 값 `본뜬.실무수량 {줄: {차례: 수}, +자재: {코드: 수}}`(출처 기록 · 엑셀 대조 `compare` — 하위 호표 것까지 `excel_given`). """ from __future__ import annotations @@ -98,12 +98,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).""" + """(참조, 다시씀) — 같은 본뜬(키 · 원본)이 있으면 그 키에 덮음 · 없으면 새 키 · 엑셀 수량 = 저장(742-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}} + doc = {**doc, "본뜬": {**doc["본뜬"], "실무수량": hands}} at = refs.here() want = doc["본뜬"] have = next( diff --git a/M02_MasterTemplete/M02_Labor_Expand.py b/M02_MasterTemplete/M02_Labor_Expand.py index 0f18a8798..0b58f0e75 100644 --- a/M02_MasterTemplete/M02_Labor_Expand.py +++ b/M02_MasterTemplete/M02_Labor_Expand.py @@ -2,9 +2,9 @@ 참조 → `{이름, 단위, 기준(1 단위), 까닭, 안?}` — 단가승격(옛 SP) · 사전조합(SC) · 단가산출(SK · `M02_Sangeun_Calc.unit`). 이름은 `M02_Labor_Ref.Expand` 로 그대로 부름. -사전조합 안 줄 수량(723-1 · 내역 17-1) = 쓰는 쪽 `given` 의 알약 「참조|층|주인|키#차례」(자재 = -「…#자재|코드」 · `inner_pills`) · 문서 손은 굳힌 사본(`hands` · 프로젝트 · 불러오기 대조)과 비율 줄(율)만 · -없으면 「수량 없음」(1 아님 · 그 사전조합 기준 없음). +사전조합 안 줄 수량(742-1) 차례 — 층 문서 = 알약 값(쓰는 쪽 `given` 「참조|층|주인|키#차례」 · 자재 = +「…#자재|코드」 · `inner_pills`) → 저장 손 · 굳힌 사본(`hands` · 프로젝트) = 굳힌 손(옛 사본 · 707-7) → +알약 값 → 저장(`refs.SAVED`) · 비율 줄 = 율 · 모두 없으면 「수량 없음」(1 아님 · 그 사전조합 기준 없음). """ from __future__ import annotations @@ -41,13 +41,17 @@ class Expand: ) # 층 원본 손 = 옛 값(안 씀) def _qty(self, one: dict, key: str) -> Decimal | None: - """안 줄 수량 — 손(굳힌 사본 · 비율 줄) · 알약 값(given) · 없음.""" + """안 줄 수량 — 굳힌 손(사본) · 비율 줄 율 → 알약 값(given) → 저장 수량(층 손 · 사본 `저장`).""" from M02_MasterTemplete import M02_Labor_Doc as labor - hand = labor._number((one.get("수량") or {}).get("손")) + qty = one.get("수량") if isinstance(one.get("수량"), dict) else {} + hand = labor._number(qty.get("손")) if hand is not None and (self.hands or refs.is_rate(one)): return hand - return labor._number(self.given.get(key)) + got = labor._number(self.given.get(key)) + if got is not None: + return got + return labor._number(qty.get(refs.SAVED)) if self.hands else hand def __call__(self, ref: Ref, trail: tuple[str, ...] = ()) -> dict[str, Any]: rid = refs.ref_id(ref) @@ -152,7 +156,7 @@ class Expand: if sub.get("기준") is None: why = why or f"「{sub.get('이름')}」 {sub.get('까닭')}" continue - if qty is None: # 723-1 — 이은 값 없음 = 금액 빠짐(1 로 안 셈) + if qty is None: # 742-1 — 저장 · 이은 값 없음 = 금액 빠짐(1 로 안 셈) inner[-1]["까닭"] = NO_QTY why = why or f"「{sub.get('이름')}」 {NO_QTY}" continue diff --git a/M02_MasterTemplete/M02_Labor_Ref.py b/M02_MasterTemplete/M02_Labor_Ref.py index 3490e859b..9afa8b5b0 100644 --- a/M02_MasterTemplete/M02_Labor_Ref.py +++ b/M02_MasterTemplete/M02_Labor_Ref.py @@ -7,9 +7,9 @@ precombo `{층}/precombo/SC-######.json` 사전조합 `{종류:"사전조합", 키, 이름, 단위, 본뜬, 담은로직:[{참조, 수량:{손}, 메모} | {로직, 입력값, 메모, 수량:{손}, 고름?, 자재표시?}], 자재:[일위대가 자재 꼴]}` — - 승격 · 사전조합 · 단가산출 로직(51-11) · 안 줄 · 자재 수량 = 저장 안 함(늘 null · - 723-1 · 내역 17-1 · `no_qty` · 비율 줄 율만 남김) · 쓰는 쪽 알약으로 이음(`Expand` given) · - 값 = Σ 안 줄 1 단위 값 × 이은 수량(로직 줄 = 기준 ÷ 배) + Σ 자재 기준단가 × 이은 수량 + 승격 · 사전조합 · 단가산출 로직(51-11) · 안 줄 · 자재 수량 = 저장(742-1 · 사용자 입력 · + null = 빈 칸 · 금액 빠짐) · 쓰는 쪽 알약이 이어지면 그 값이 앞섬(`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,7 @@ 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 no_qty(out) + return out def is_rate(one: Any) -> bool: @@ -178,11 +178,17 @@ def is_rate(one: Any) -> bool: 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} +SAVED = "저장" # 굳힌 사본의 저장 수량 칸(742-1) — 굳힌 손(옛 사본 · 707-7)과 가름 + + +def saved_qty(one: dict[str, Any]) -> dict[str, Any]: + """굳힐 때 줄 · 자재 저장 수량 `{손}` → `{저장}`(742-1 · 공사 차례 = 굳힌 손 → 알약 → 저장) · + 비율 줄 율 · 빈 수량 그대로.""" + qty = one.get("수량") + hand = qty.get("손") if isinstance(qty, dict) else None + if is_rate(one) or hand is None: + return one + return {**one, "수량": {SAVED: hand}} _OLD_CLASS = re.compile(r"품셈") # 134-5 — 옛 품셈 분류(건설품셈 공통 · 산림품셈 · 07장 …) @@ -497,7 +503,8 @@ 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 — 옛 손도 굳히지 않음(쓰는 쪽 알약으로) + mats = [saved_qty(one) for one in doc.get("자재") or [] if isinstance(one, dict)] + doc = {**doc, "담은로직": [saved_qty(one) for one in lines], "자재": mats} return {**doc, "알약": combo_store.combo_pills({"담은로직": lines})["물량"]} diff --git a/M02_MasterTemplete/M02_Library_Same.py b/M02_MasterTemplete/M02_Library_Same.py index 991ed02b4..ba449ee4a 100644 --- a/M02_MasterTemplete/M02_Library_Same.py +++ b/M02_MasterTemplete/M02_Library_Same.py @@ -5,7 +5,7 @@ 날짜(수정일) · 고정 줄 출처 수 = Decimal 정규화(0.20 = 0.2 = 「0.2」) · 글 = 겹 빈칸 정리 · 빈 값 칸 뺌 · 단위 = 소문자 + m3 → ㎥ 차례 무시 = 더한줄 · 담은로직 · 자재 · 뺀줄 · 열린입력 정렬 · 더한줄 요소 줄 이름 뺌(요소 키가 정체) - SC 줄 · 자재 수량 = null(723-1 `refs.no_qty` · 비율 줄 율만) · SK 더한줄 수량은 넣음 + SC 줄 · 자재 수량(742-1 저장 수량) · SK 더한줄 수량 넣음 참조 = 가리킨 문서의 지문(깊이 `refs.DEPTH` · 층 넘어 사본도 같음) 빈 문서(SK 바탕 · 더한줄 없음 · SC 줄 · 자재 없음) = 지문 None(검사 안 함). 저장 칸 없음 — 그때그때 셈(`Same` 한 벌 = 한 요청 안 메모). diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts index 7bbbb4138..6b035a2d8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts @@ -3,12 +3,13 @@ * M02 일위대가(서고 · 사전조합) 상세 = 일위대가표만(PLAN 723-2 · 내역 16 · 17 · 요구서_내역16 2장 3~5). * · 차례 = 정보(이름 · 구분 · 단위 · 본뜬) → 일위대가표(엑셀 꼴 격자 · `Labor_Grid.laborBlock`) · 오른쪽 원문 패널 * · 옵션 값 = 규격 칸 · 「빈 값 n」 = 비고 칸 붉게(입력 빈 로직 줄) - * · 수량 칸 = 확인용 임시 입력(723-1 · 휘발 · 문서 · 저장 · dirty 안 건드림) → 그 줄 금액 · 합계 미리 보기 - * (짝 셈 `Labor_Calc.settle` · 서버 안 부름) · 문서 안 줄 수량은 늘 null(쓰는 쪽 알약으로 이음) + * · 수량 칸 = 일위대가 1 단위에 드는 양(742-1 · 요구 584-1 · 문서에 저장 · 새 줄 1 · 빈 칸 = 「수량 입력」 · + * 금액 빠짐) → 그 줄 금액 · 합계 바로(짝 셈 `Labor_Calc.settle`) · 구조물 조합 · 공사 알약이 이어지면 그 값이 앞섬 + * · 합계 열 = 「기준단위 단가」(줄 1 단위) · 「수량 반영 금액」(× 수량) * · 머리 단추 = [항목 추가](736-1 · 자재는 찾기 창에서 고름 → 자재 목록) · 줄 우클릭 · 줄 끝 ⋯ = 위로 · 아래로 · 별칭 · 삭제 * · 줄 누름 = 줄 모달(`Labor_RowModal` — 옵션 · 메모 · 켜쌓기율 · 로직 자재 바꾸기 · 세 칸 · 단가산출) · * 사전조합 참조 줄 = 서고 원본 모달(`Labor_RefEdit`) · 사용자 자재 줄 = 자재 모달(세 칸) - * · 금액 = 서버 calc(최신 응답만 · 저장 안 함) · [저장]은 UI_Main(서버가 수량 걷고 다시 셈) + * · 금액 = 서버 calc(최신 응답만 · 저장 안 함) · [저장]은 UI_Main(서버가 다시 셈) * · 안 이은 고정 줄(729-3) = 격자 위 띠 · [연결](`Labor_Unlinked` — 이음만 · 저장은 [저장]) * 구조물 조합 일위대가(카드 · 열 상세)는 그대로 `Labor_Detail`(면적 잇기 · 수량산출). * ========================================================================== */ @@ -49,10 +50,13 @@ import "@ui/ui_template_general_layout.css"; import "./M02_MasterTemplete_Labor.css"; const TEXT = { - TempHint: [ - "수량 칸 = 확인용 임시 입력(저장 안 함) — 실제 수량은 구조물 조합 · 프로젝트에서 이음", - "Quantity = temporary check only (not saved) — real quantities link in structures · projects", + QtyHint: [ + "수량 = 이 일위대가 1 단위에 드는 양(저장) — 구조물 조합 · 공사에서 알약 · 계산 값이 이어지면 그 값이 앞섬", + "Quantity = amount per 1 unit of this item (saved) — linked pills · project values take precedence", ], + NeedQty: ["수량 입력", "Enter quantity"], + BasePrice: ["기준단위 단가", "Unit price"], + QtyAmount: ["수량 반영 금액", "Amount × qty"], Alias: ["별칭", "Alias"], AliasAsk: ["별칭(비우면 지움)", "Alias (empty = clear)"], RemoveAsk: ["「{value}」 줄을 지울까?", "Delete line “{value}”?"], @@ -61,9 +65,10 @@ const TEXT = { } as const satisfies Record; const t = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0]; -/** 비율 고정 줄(705-3) — 수량 = 율(문서에 남음 · 임시 입력 아님) */ +/** 비율 고정 줄(705-3) — 수량 = 율 */ const isRate = (row: LaborRow): boolean => !!row.고정?.비율; -const matKey = (code: string): string => `자재|${code}`; +/** 새 줄 수량(742-1) — 1 · 사용자가 고침 */ +const ONE = { 손: 1 } as const; export function mountLaborTable( host: HTMLElement, @@ -71,14 +76,9 @@ export function mountLaborTable( opt: LaborDetailOptions, ): LaborDetailHandle { const cur = normalizeLabor(doc, opt.column); - // 723-1 — 옛 문서 손 수량은 안 씀(서버도 저장 때 걷음) · 비율 줄 율만 - for (const r of cur.담은로직) if (!isRate(r)) r.수량 = null; - for (const m of cur.자재) m.수량 = null; const saved = JSON.stringify(cur); const edit = !opt.readOnly; const logics = new Map(); - /** 임시 수량(휘발) — 줄 id · 「자재|코드」 → 수 */ - const temp = new Map(); let calc: LaborCalc | null = null; let last: LaborCalc | null = null; let fail: string | null = null; @@ -95,19 +95,6 @@ export function mountLaborTable( return () => watchers.delete(fn); }; - /** 셈 몸 = 문서 + 임시 수량(저장 안 함) */ - const withTemp = (): LaborDoc => ({ - ...cur, - 담은로직: cur.담은로직.map((r) => { - const v = temp.get(lineId(r) ?? ""); - return v === undefined ? r : { ...r, 수량: { 손: v } }; - }), - 자재: cur.자재.map((m) => { - const v = temp.get(matKey(m.코드)); - return v === undefined ? m : { ...m, 수량: { 손: v } }; - }), - }); - const gridHost = el("div", { attrs: { "data-labor-grid": "", "data-labor-table": "" } }); const source = createSourceView({ path: "/m02/logic-lib/source", @@ -128,28 +115,30 @@ export function mountLaborTable( if (sourceHost.firstChild !== source.root) sourceHost.replaceChildren(source.root); } - /** 일위대가표 — 줄 누름 · 우클릭 · 수량 칸(임시) · 빈 값 붉게 */ + /** 일위대가표 — 줄 누름 · 우클릭 · 수량 칸(저장) · 빈 값 · 빈 수량 붉게 */ const draw = (): void => { - const body = withTemp(); - const block = laborBlock(body, calc ?? last, logics); + const block = laborBlock(cur, calc ?? last, logics); const empties = new Set(); block.lines.forEach((ln, n) => { - const row = body.담은로직[n]; - if (!row) return void (ln.note = ln.note === tc("Grid_NoQty") ? "" : ln.note); // 자재 줄 - const empty = row.로직 ? valueLine(logics.get(row.로직)?.입력 ?? [], row.입력값).empty : 0; - if (empty) empties.add(n); + const row = cur.담은로직[n]; + if (ln.note === tc("Grid_NoQty")) ln.note = t("NeedQty"); // 742-1 빈 수량 = 사용자가 넣음 + const empty = row?.로직 ? valueLine(logics.get(row.로직)?.입력 ?? [], row.입력값).empty : 0; if (empty) ln.note = t("Empty").replace("{n}", String(empty)); - else if (ln.note === tc("Grid_NoQty")) ln.note = ""; // 수량 없음 = 서고에선 보통(723-1) + if (empty || ln.note === t("NeedQty")) empties.add(n); }); const have = calc ?? last; - // 빈 줄 = 단가 모름(까닭) · 빈 값만 — 임시 수량 없는 줄은 안 셈 - block.blank = have ? block.lines.filter((l) => l.prices.sum == null).length + empties.size : 0; + // 빈 줄 = 금액 모름(단가 까닭 · 빈 수량) · 빈 값 + block.blank = have + ? block.lines.filter((l, n) => l.amounts.sum == null || empties.has(n)).length + : 0; if (fail && block.lines[0]) block.lines[0].note = fail; const g = hopyoGrid([block]); const at = (i: number): number | null => g.at.get(i)?.[1] ?? null; + const label: Record = { sum_price: t("BasePrice"), sum: t("QtyAmount") }; const grid = createHopyoGrid( [block], { + columns: g.columns.map((c) => (label[c.key] ? { ...c, label: label[c.key] } : c)), onRowClick: (i) => { const n = at(i); if (n !== null) openAt(n); @@ -163,13 +152,13 @@ export function mountLaborTable( : undefined, editable: (i, key) => { const n = at(i); - if (key !== "qty" || n === null) return false; + if (!edit || key !== "qty" || n === null) return false; const row = cur.담은로직[n]; - return !row || !isRate(row); // 임시 입력이라 읽기만 층도 됨 + return !row || !isRate(row); }, onEdit: (i, _key, text) => { const n = at(i); - if (n !== null) setTemp(n, text); + if (n !== null) setQty(n, text); }, cellClass: (i, key) => { const n = at(i); @@ -202,10 +191,9 @@ export function mountLaborTable( fail = null; draw(); try { - const body = withTemp(); const got = opt.calc - ? await opt.calc(body) - : await calcLabor(opt.layer, opt.column, body, undefined, opt.owner); + ? await opt.calc(cur) + : await calcLabor(opt.layer, opt.column, cur, undefined, opt.owner); if (!alive || mine !== seq) return; calc = last = got; } catch (error) { @@ -215,34 +203,27 @@ export function mountLaborTable( draw(); }, 150); }; - /** `qtyOnly` = 수량만(임시) — 짝 셈으로 바로 */ + /** `qtyOnly` = 수량만 — 짝 셈으로 바로 */ const changed = (qtyOnly = false): void => { opt.onChanged(dirty()); if (qtyOnly && calc) { - calc = settle(calc, withTemp(), {}); + calc = settle(calc, cur, {}); draw(); } else recalc(); }; - /** 임시 수량(휘발) — 빈 글 = 지움 · 수 아님 = 알림 */ - function setTemp(n: number, text: string): void { - const row = cur.담은로직[n]; - const key = row ? (lineId(row) ?? "") : matKey(cur.자재[n - cur.담은로직.length]?.코드 ?? ""); + /** 수량 칸(742-1 · 저장) — 빈 글 = 빈 수량(「수량 입력」) · 수 아님 · 음수 = 알림 */ + function setQty(n: number, text: string): void { + const row = cur.담은로직[n] ?? cur.자재[n - cur.담은로직.length]; const raw = text.replace(/,/g, "").trim(); - if (!raw) temp.delete(key); - else if (Number.isFinite(Number(raw))) temp.set(key, Number(raw)); - else return showToast(t("NotNumber"), "error"); - calc = calc && settle(calc, withTemp(), {}); - draw(); + const v = Number(raw); + if (!row) return; + if (raw && !(Number.isFinite(v) && v >= 0)) return showToast(t("NotNumber"), "error"); + row.수량 = raw ? { 손: v } : null; + changed(true); } - /** 붙여넣기(705-2) 뒤 — 엑셀 수량은 임시 칸으로(문서엔 안 남김) */ + /** 붙여넣기(705-2) 뒤 — 엑셀 수량 = 저장 수량 */ function afterPaste(): void { - for (const r of cur.담은로직) { - const v = r.수량?.손; - if (isRate(r) || v == null) continue; - temp.set(lineId(r) ?? "", v); - r.수량 = null; - } void loadLogics().then(() => changed()); } @@ -340,13 +321,8 @@ export function mountLaborTable( } function remove(n: number): void { const rows = cur.담은로직.length; - if (n < rows) { - temp.delete(lineId(cur.담은로직[n]) ?? ""); - cur.담은로직.splice(n, 1); - } else { - temp.delete(matKey(cur.자재[n - rows]?.코드 ?? "")); - cur.자재.splice(n - rows, 1); - } + if (n < rows) cur.담은로직.splice(n, 1); + else cur.자재.splice(n - rows, 1); changed(); } @@ -363,14 +339,21 @@ export function mountLaborTable( const id = lineId({ 참조: ref }); if (cur.담은로직.some((r) => lineId(r) === id)) return showToast(tc("Labor_AlreadyRef").replace("{value}", name), "error"); - cur.담은로직.push({ 로직: "", 참조: ref, 별칭, 메모: null, 입력값: {}, 수량: null }); + cur.담은로직.push({ 로직: "", 참조: ref, 별칭, 메모: null, 입력값: {}, 수량: { ...ONE } }); changed(); }, fixed: (fx: LaborFixed, name: string) => { const id = lineId({ 고정: fx }); if (cur.담은로직.some((r) => lineId(r) === id)) return showToast(tc("Labor_AlreadyRef").replace("{value}", name), "error"); - cur.담은로직.push({ 로직: "", 고정: fx, 별칭: null, 메모: null, 입력값: {}, 수량: null }); + cur.담은로직.push({ + 로직: "", + 고정: fx, + 별칭: null, + 메모: null, + 입력값: {}, + 수량: { ...ONE }, + }); changed(); }, /** 736-1 찾아 넣기 창에서 재료를 고름 — 「자재 목록」 줄 · 로직 안 자재 · 이미 있는 자재는 막음 */ @@ -384,7 +367,7 @@ export function mountLaborTable( ? "Labor_InLogicMat" : cur.자재.some((x) => x.코드 === m.코드) && "Labor_AlreadyMat"; if (why) return showToast(tc(why).replace("{value}", `${m.코드} ${m.이름 ?? ""}`), "error"); - cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: null }); + cur.자재.push({ 코드: m.코드, 이름: m.이름, 규격: m.규격, 단위: m.단위, 수량: { ...ONE } }); changed(); }, }; @@ -395,7 +378,7 @@ export function mountLaborTable( openLogicPicker((x) => { if (cur.담은로직.some((r) => r.로직 === x.키)) return showToast(tc("Labor_Already").replace("{value}", `${x.키} ${x.이름}`), "error"); - cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량: null }); + cur.담은로직.push({ 로직: x.키, 별칭: null, 메모: null, 입력값: {}, 수량: { ...ONE } }); void loadLogics().then(() => changed()); }, withHopyo(library)), }); @@ -466,7 +449,7 @@ export function mountLaborTable( : []), ], }), - createText(t("TempHint"), "hint"), + createText(t("QtyHint"), "hint"), gridHost, ], }), diff --git a/resources/tester/test_175_precombo_mats.py b/resources/tester/test_175_precombo_mats.py index 2868c77c4..d12d156f8 100644 --- a/resources/tester/test_175_precombo_mats.py +++ b/resources/tester/test_175_precombo_mats.py @@ -57,7 +57,10 @@ 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, None), (WIRE, None)] # 723-1 + assert [(m["코드"], m["수량"]) for m in got["자재"]] == [ + (MAT, {"손": 2}), + (WIRE, None), + ] # 742-1 assert got["자재"][0]["운반비"] == "미포함" and got["자재"][0]["이름"] != "틀린 이름" # 자재 없음 = 빈 목록(옛 문서 꼴 그대로 읽힘) _, empty = _save(world["system"], {"담은로직": [{"로직": NET}]}) @@ -118,16 +121,16 @@ def test_사전조합_값에_자재_금액_빈칸은_안_셈_안_줄에_자재(w ) bare = labor.calc(doc, {})["줄"][0] one = labor.calc(doc, _given(key))["줄"][0] - assert bare["기준"] is None and "수량 없음" in bare["까닭"] # 723-1 — 안 이으면 값 없음 + assert bare["기준"]["계"] == pytest.approx(float(want)) # 742-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 mats[0]["기준"]["재료비"] == pytest.approx(float(_price(MAT))) - # 목록 기준금액(51-4) — 이은 값 없음 = 빈 값(723-1) + # 목록 기준금액(51-4) — 저장 수량으로(742-1) rows = [{"종류": "precombo", "이름": key}] refs.summaries(world["system"], rows) - assert rows[0]["기준금액"] is None + assert rows[0]["기준금액"] == pytest.approx(float(want)) def test_B08_참조_줄_재료에_사전조합_자재_손_곱(world): # noqa: F811 @@ -143,10 +146,10 @@ def test_B08_참조_줄_재료에_사전조합_자재_손_곱(world): # noqa: F 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)으로 · 안 이으면 안 셈 + # 742-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("자재")] == [] + assert [(m["품목"]["키"], q) for m, q in bare if m.get("자재")] == [(MAT, pytest.approx(6.0))] got = _ref_mats( m01.loaded()[1], lambda r: live.get(r["키"]), {**SYS, "키": "SC-겹"}, given=_given(key) ) diff --git a/resources/tester/test_204_2_b09_bill_library.py b/resources/tester/test_204_2_b09_bill_library.py index d7854a95d..dc7e7baf3 100644 --- a/resources/tester/test_204_2_b09_bill_library.py +++ b/resources/tester/test_204_2_b09_bill_library.py @@ -27,7 +27,7 @@ if str(ROOT) not in sys.path: from B09_Estimation import B09_Estimation_Bill_Library as library # noqa: E402 from B09_Estimation import B09_Estimation_Router as router # noqa: E402 from B09_Estimation import B09_Estimation_Store as store_b09 # noqa: E402 -from B09_Estimation.B09_Estimation_Labor_Source import ASK, daega_inputs # noqa: E402 +from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs # noqa: E402 from B09_Estimation.B09_Estimation_Linked import linked_resource_keys # noqa: E402 from config import config_system # noqa: E402 from M02_MasterTemplete import M02_Labor_Ref as refs # noqa: E402 @@ -86,9 +86,9 @@ def test_사전조합_고름_사본_들여옴_새_호표_다시_고르면_재사 got, block = _block(root, folder) assert (block["kind"], block["name"], block["unit"]) == ("사전조합", "서고 조합", "㎡") kinds = [(ln["kind"], ln.get("qty")) for ln in block["lines"]] - # 723-1 · 723-4 = M02 SC 안 줄 수량 저장 안 함 → 내역에서 바로 쓴 SC = 수량 없음(이 공사에서 입력) - assert kinds == [("sangeun", None), ("sangeun", None), ("material", None)] - assert all(ln["ask"] == ASK for ln in block["lines"]) + # 742-1 = M02 SC 안 줄 저장 수량 → 내역에서 바로 쓴 SC = 굳힌 저장 수량(이 공사 입력이 앞섬) + assert kinds == [("sangeun", 2), ("sangeun", 1), ("material", 3)] + assert not any(ln.get("ask") for ln in block["lines"]) assert block["cover"] == f"C:{block['origin']['참조']}" # 공사 안 한 덮개 그대로 assert got["owners"] == {} # 구조물 사본 없어도 호표 # 단가 묶음 = 담은 로직 · 사전조합 자재 · 고른 자재(사본 셈이 마스터를 안 읽음) diff --git a/resources/tester/test_702_3_b09_estimation_live.py b/resources/tester/test_702_3_b09_estimation_live.py index 99d29ad70..48edc6896 100644 --- a/resources/tester/test_702_3_b09_estimation_live.py +++ b/resources/tester/test_702_3_b09_estimation_live.py @@ -44,11 +44,12 @@ def test_서고_사본_산근_목록에_듦_대가_단가_채움(world): by_key = {r["key"]: r for r in sg["sg_list"]} labor = compute_labor(daega, {**base, "gaps": []}, by_key) row = next(r for r in labor["daega"] if r["key"] == block["id"]) - assert not row["sum"] and row["blank"] == 3 # 723-4 서고 SC 안 줄 수량 없음 = 이 공사에서 입력 + saved = row["sum"] # 742-1 서고 SC 안 줄 저장 수량(2 · 1 · 3) = 굳힌 사본 그대로 + assert saved and not row.get("blank") cover = {block["cover"]: {ln["cid"]: {"qty": q} for ln, q in zip(block["lines"], (2, 1, 3))}} labor = compute_labor(daega, {**base, "gaps": []}, by_key, cover) row = next(r for r in labor["daega"] if r["key"] == block["id"]) - assert row["sum"] and not row.get("blank") + assert row["sum"] == saved and not row.get("blank") # 이 공사 입력(같은 수) = 같은 값 def test_화면_연결_뒤_셈_저장_단추_옮김_물음_TS(): diff --git a/resources/tester/test_703_b09_daega_copy.py b/resources/tester/test_703_b09_daega_copy.py index c29381245..b44739261 100644 --- a/resources/tester/test_703_b09_daega_copy.py +++ b/resources/tester/test_703_b09_daega_copy.py @@ -186,8 +186,8 @@ def test_자료집_같은_이름_물음(world): saved = json.loads( (world["folder"] / "precombo" / f"{first['이름']}.json").read_text(encoding="utf-8") ) - # 723-1 = M02 SC 안 줄 수량 저장 안 함(덮어쓴 손 9 도 걷힘 · 쓰는 곳 알약 · 이 공사 입력으로 이음) - assert saved["이름"] == "내 돌붙임" and saved["담은로직"][1]["수량"] is None + # 742-1 = M02 SC 안 줄 수량 저장(덮어쓴 손 9 · 쓰는 곳 알약 · 이 공사 입력이 이어지면 그 값이 앞섬) + assert saved["이름"] == "내 돌붙임" and saved["담은로직"][1]["수량"] == {"손": 9} assert saved["담은로직"][0]["입력값"] == b145.STONE assert len(list((world["folder"] / "precombo").glob("SC-*.json"))) == 2 @@ -221,5 +221,7 @@ def test_자료집_길_같은_이름_409_덮어쓰기(world, monkeypatch): over = client.post(url, json={**body, "overwrite": first.json()["키"]}) assert over.status_code == 200 and over.json()["키"] == first.json()["키"] saved = world["folder"] / "precombo" / f"{first.json()['키']}.json" - assert json.loads(saved.read_text(encoding="utf-8"))["담은로직"][0]["수량"] is None # 723-1 + assert json.loads(saved.read_text(encoding="utf-8"))["담은로직"][0]["수량"] == { + "손": 4 + } # 742-1 assert client.post(url, json={**body, "cover": "S:없음"}).status_code == 404 diff --git a/resources/tester/test_717_4_sangeun_pick.py b/resources/tester/test_717_4_sangeun_pick.py index 37cde66d8..bdc8cf249 100644 --- a/resources/tester/test_717_4_sangeun_pick.py +++ b/resources/tester/test_717_4_sangeun_pick.py @@ -78,9 +78,9 @@ def test_list_amount_and_labor_line_holds_upper_sk(world): # noqa: F811 fed = labor.calc(outer, {f"{refs.ref_id({**MINE, '키': sc})}#0": 3}) direct = labor.calc({"담은로직": [{"참조": {**COMPANY, "키": sk}, "수량": {"손": 3}}]}, {}) assert saved == {**COMPANY, "키": sk} # 참조 = 회사 SK 그대로 - # 723-1 · 내역 17-1 — 일위대가 안 줄 수량은 저장 안 함(null) · 쓰는 자리(바깥 문서)에서 들어옴 - assert inner == [None] - assert got["반영합"]["계"] == 0 # 안 줄 수량 없음 = 금액 빠짐 + # 742-1 — 일위대가 안 줄 수량 = 저장(사용자 입력) · 쓰는 자리 알약이 이어지면 그 값 + assert inner == [{"손": 3}] + assert got["반영합"]["계"] == pytest.approx(one * 3, abs=1) # 저장 수량 3 assert direct["반영합"]["계"] == pytest.approx( one * 3, abs=1 ) # 바깥이 SK 를 손 3 으로 바로 참조 diff --git a/resources/tester/test_719_1_sk_freeze.py b/resources/tester/test_719_1_sk_freeze.py index d0d1217b8..26cef1aff 100644 --- a/resources/tester/test_719_1_sk_freeze.py +++ b/resources/tester/test_719_1_sk_freeze.py @@ -93,7 +93,7 @@ def test_B09_서고_사본_값_같음_원본판(world, tmp_path): "precombo", {"이름": "조합", "단위": "㎡", "담은로직": [{"참조": ref["ref"], "수량": {"손": 2}}]}, )["이름"] - # 723-1 = SC 안 줄 수량(손 2) 저장 안 함 → 쓰는 쪽 알약 값으로 이음(층 · 사본 같은 값) + # 742-1 = SC 안 줄 수량(손 2) 저장 → 굳힌 사본 = 저장 2 · 쓰는 쪽 알약 값(2)이 앞섬(층 · 사본 같은 값) pill = f"{refs.ref_id({**MINE, '키': sc})}#0" with store.at(world["personal"]): top = labor.clean( @@ -108,7 +108,9 @@ def test_B09_서고_사본_값_같음_원본판(world, tmp_path): folder = library.library_dir(root) / made["폴더"] read = {p.name: json.loads(p.read_text(encoding="utf-8")) for p in folder.glob("*.json")} bare = prices.calc(read[source.LABOR_FILE], read[source.PRICE_FILE], {}, {}, None)["줄"][0] - assert bare["기준"] is None and "수량 없음" in bare["까닭"] # 이은 값 없음 = 1 로 안 셈 + assert bare["기준"]["계"] == pytest.approx( + live["기준"]["계"], rel=1e-9 + ) # 이은 값 없음 = 저장 2 got = prices.calc(read[source.LABOR_FILE], read[source.PRICE_FILE], {}, {}, area)["줄"][0] assert not got["까닭"] and not live["까닭"] assert got["기준"]["계"] == pytest.approx(live["기준"]["계"], rel=1e-9) diff --git a/resources/tester/test_719_2_sk_project.py b/resources/tester/test_719_2_sk_project.py index 56da848e5..1d7c8ee02 100644 --- a/resources/tester/test_719_2_sk_project.py +++ b/resources/tester/test_719_2_sk_project.py @@ -21,7 +21,7 @@ from B09_Estimation import B09_Estimation_Refresh as refresh from B09_Estimation import B09_Estimation_Sangeun as sangeun from B09_Estimation.B09_Estimation_Calc_Base import compute_base from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor -from B09_Estimation.B09_Estimation_Labor_Source import ASK, daega_inputs +from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs from B09_Estimation.B09_Estimation_Linked import linked_resource_keys from M02_MasterTemplete import M02_Item_Copy as item_copy from M02_MasterTemplete import M02_Item_Snapshot as snapshots @@ -122,9 +122,8 @@ def test_SK_단가_덮개_속_일위대가_펼침(world, tmp_path): if not _engine_ready(world): pytest.skip("715-1b 엔진 입력 덧칸(단가 · 값묶음) 뒤 — 지금은 M02 SK 셈도 못 함") key = _mine(world, full=True) - # 723-1 속 일위대가 안 줄 수량 없음 → M02 SK = 그 줄만 빠짐(725-2) · B09 = 그 줄만 빈 줄 - gone = _calc(world["personal"], key)["빠진줄"] - assert len(gone) == 1 and "수량 없음" in gone[0]["까닭"] + # 742-1 속 일위대가 안 줄 저장 수량(손 1) → M02 SK · B09 모두 빈 줄 없음 + assert _calc(world["personal"], key)["빠진줄"] == [] root, folder = _lib(world, tmp_path, key) daega, done, _ = _b09(root) block = done[daega["libs"][folder]] @@ -133,8 +132,8 @@ def test_SK_단가_덮개_속_일위대가_펼침(world, tmp_path): sc_line = next(ln for ln in lines if ln["kind"] == "daega") sc = done[sc_line["key"]] assert sc["kind"] == "사전조합" and sc_line["qty"] == 2 - assert sc["blank"] == 1 and sc["lines"][0]["why"] == ASK and block["blank"] >= 1 - # 이 공사에서 입력(702 덮개 qty 1) = 옛 손 1 과 같은 값 → M02 셈(알약 값 1) 과 같은 금액 + assert sc["blank"] == 0 and sc["lines"][0]["qty"] == 1 and block["blank"] == 0 + # 이 공사에서 입력(702 덮개 qty 1) = 저장 1 과 같은 값 → M02 셈(알약 값 1) 과 같은 금액 cover = {sc["cover"]: {sc["lines"][0]["cid"]: {"qty": 1}}} block = _b09(root, cover)[1][daega["libs"][folder]] assert block["blank"] == 0 diff --git a/resources/tester/test_723_labor_table.py b/resources/tester/test_723_labor_table.py index 7dd002705..814903e4a 100644 --- a/resources/tester/test_723_labor_table.py +++ b/resources/tester/test_723_labor_table.py @@ -1,15 +1,12 @@ -"""PLAN 723 — M02 일위대가(사전조합) 수량 = 저장 안 함 · 일위대가표만 · 줄 모달(내역 16 · 17). +"""PLAN 723 — M02 일위대가(사전조합) 일위대가표만 · 줄 모달(내역 16 · 17). -723-1 서버 — 저장 때 안 줄 · 자재 수량 null(비율 줄 율만) · 펼침 = 쓰는 쪽 알약 값 · 안 이으면 「수량 없음」 · -구조물 조합 알약에 안 줄 알약 · 불러오기 엑셀 수량 = `본뜬.실무수량`(셈 안 씀). -723-2 · 723-3 화면 — 서고 상세 = `Labor_Table`(수량 칸 휘발 임시 입력 · 우클릭) · 줄 모달 = 717-2b 상세 줄 모드. +723-1 서버 — 구조물 조합 알약에 안 줄 알약 +(안 줄 수량 저장은 742-1 이 되살림 · `test_742_1_sc_qty_saved`). +723-2 · 723-3 화면 — 서고 상세 = `Labor_Table`(우클릭) · 줄 모달 = 717-2b 상세 줄 모드. """ import os -import pytest - -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 @@ -25,41 +22,6 @@ def _src(name: str) -> str: return open(os.path.join(SRC, name), encoding="utf-8").read() -def test_저장은_안_줄_수량을_걷고_비율_줄_율만_남김(world): # noqa: F811 - rate = {"고정": {**RATE, "비율": {"기준": "노무비", "비목": "경비"}}, "수량": {"손": 3}} - lines = [{"로직": STONE, "수량": {"손": 2.5}}, rate] - with store.at(world["system"]): - key = refs.create("precombo", {"이름": "수량 없음", "단위": "m", "담은로직": lines})["이름"] - doc = store.read("precombo", key)["문서"] - assert doc["담은로직"][0]["수량"] is None - assert doc["담은로직"][1]["수량"] == {"손": 3} # 비율 = 율(기준단위 당 수량 아님) - frozen = refs.with_pills(doc) # 프로젝트 굳힘도 수량 없이 - assert frozen["담은로직"][0]["수량"] is None and frozen["알약"] - - -def test_펼침은_이은_알약만_쓰고_층_원본_옛_손은_안_씀(world): # noqa: F811 - with store.at(world["system"]): - key = refs.create("precombo", {"이름": "옛", "단위": "m", "담은로직": [{"로직": STONE}]})[ - "이름" - ] - ref = {**SYS, "키": key} - path = world["system"] / "precombo" / f"{key}.json" - import json - - old = json.loads(path.read_bytes()) - old["담은로직"][0]["수량"] = {"손": 9} # 옛 문서(723-1 앞) — 파일은 그대로 둠 - path.write_text(json.dumps(old, ensure_ascii=False), encoding="utf-8") - whole = m01.loaded()[1] - with store.at(world["system"]): - bare = refs.Expand(whole)(ref) - got = refs.Expand(whole, given={f"{refs.ref_id(ref)}#0": 2})(ref) - frozen = refs.Expand(whole, read=lambda r: old)(ref) # 굳힌 사본 = 손 그대로 - stone = labor.run_line(whole, STONE, {})["기준"]["계"] - assert bare["기준"] is None and "수량 없음" in bare["까닭"] - assert got["기준"]["계"] == pytest.approx(stone * 2) - assert frozen["기준"]["계"] == pytest.approx(stone * 9) - - def test_구조물_조합_알약에_안_줄_알약(world): # noqa: F811 with store.at(world["system"]): key = refs.create( @@ -72,17 +34,14 @@ def test_구조물_조합_알약에_안_줄_알약(world): # noqa: F811 assert pills[1]["이름"] == "옹벽 · 돌" and pills[1]["단위"] == "㎡" -def test_화면_서고_상세는_일위대가표만_임시_수량_휘발(): +def test_화면_서고_상세는_일위대가표만(): table, detail = ( _src("M02_MasterTemplete_Labor_Table.ts"), _src("M02_MasterTemplete_Labor_Detail.ts"), ) assert 'if (opt.mode === "precombo") return mountLaborTable(host, doc, opt);' in detail - assert "const temp = new Map();" in table # 휘발 — 문서 밖 - assert "calc = calc && settle(calc, withTemp(), {});" in table # 짝 셈 미리 보기 - assert ( - 'const out: Record = { ...cur, 종류: "사전조합" };' in table - ) # temp 안 실음 + assert "calc = settle(calc, cur, {});" in table # 짝 셈 바로(742-1 수량 = 문서) + assert 'const out: Record = { ...cur, 종류: "사전조합" };' in table for label in ('tc("Up")', 'tc("Down")', 't("Alias")', 'tc("Remove")'): assert label in table # 우클릭 메뉴 assert "fixed: (fx: LaborFixed, name: string)" in table # 721-2 고정 줄 이음 diff --git a/resources/tester/test_742_1_sc_qty_saved.py b/resources/tester/test_742_1_sc_qty_saved.py new file mode 100644 index 000000000..48350ace1 --- /dev/null +++ b/resources/tester/test_742_1_sc_qty_saved.py @@ -0,0 +1,140 @@ +"""PLAN 742-1 — M02 일위대가(SC) 안 줄 수량 = 사용자 입력 저장(요구 584-1 · 723-1 「null」 걷음). + +- 저장 = 줄 · 자재 `수량 {손}` 그대로(빈 수량 = null · 비율 줄 율) · 다시 열기 같음 +- 층 셈 차례 = 알약 값(쓰는 쪽 given) → 저장 수량 → 없으면 「수량 없음」 +- 공사 굳힘(`with_pills`) = 저장 수량 `{저장}` 칸으로 옮김 + → 공사 차례 = 굳힌 손(옛 사본 · 707-7) → 알약 → 저장 +- 단가산출(SK) 안 일위대가 = 그 저장 수량으로 금액(「수량 없음」 없어짐) +- 화면 일위대가표 = 수량 칸 저장 · 새 줄 1 · 빈 칸 「수량 입력」 · + 합계 열 「기준단위 단가」 · 「수량 반영 금액」 +""" + +import os +from decimal import Decimal + +import pytest + +from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor +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_723_4_b09_sc_qty import MAT, RID, SHEETS, _build, _doc +from resources.tester.test_m02_labor_ref import world # noqa: F401 — 임시 층 fixture + +SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "M02_MasterTemplete") +SYS = {"층": "system", "주인": None} +STONE = "GF000219" # 원/㎡ +RATE = {"이름": "공구손료", "단위": "%", "단가": {"노무비": 0, "재료비": 0, "경비": 0}} + + +def _stone() -> float: + return labor.run_line(m01.loaded()[1], STONE, {})["기준"]["계"] + + +def _sc(world, lines): # noqa: F811 + with store.at(world["system"]): + return refs.create("precombo", {"이름": "저장", "단위": "m", "담은로직": lines})["이름"] + + +def test_저장_다시_열기_수량_그대로_빈_수량_null_비율_율(world): # noqa: F811 + rate = {"고정": {**RATE, "비율": {"기준": "노무비", "비목": "경비"}}, "수량": {"손": 3}} + key = _sc(world, [{"로직": STONE, "수량": {"손": 2.5}}, {"로직": "GF000297"}, rate]) + with store.at(world["system"]): + doc = store.read("precombo", key)["문서"] + assert [x["수량"] for x in doc["담은로직"]] == [{"손": 2.5}, None, {"손": 3}] + frozen = refs.with_pills(doc) # 공사 굳힘 = 저장 칸으로(비율 율 · 빈 수량 그대로) + assert [x["수량"] for x in frozen["담은로직"]] == [{"저장": 2.5}, None, {"손": 3}] + assert frozen["알약"] + + +def test_층_차례_알약_먼저_없으면_저장_없으면_수량_없음(world): # noqa: F811 + key = _sc(world, [{"로직": STONE, "수량": {"손": 2}}]) + blank = _sc(world, [{"로직": STONE}]) + ref, whole = {**SYS, "키": key}, m01.loaded()[1] + with store.at(world["system"]): + saved = refs.Expand(whole)(ref) + fed = refs.Expand(whole, given={f"{refs.ref_id(ref)}#0": 5})(ref) + none = refs.Expand(whole)({**SYS, "키": blank}) + assert saved["기준"]["계"] == pytest.approx(_stone() * 2) and saved["안"][0]["수량"] == 2 + assert fed["기준"]["계"] == pytest.approx(_stone() * 5) # 알약 있음 = 알약 + assert none["기준"] is None and "수량 없음" in none["까닭"] # 빈 수량 = 금액 빠짐 + + +def test_굳힌_사본_차례_굳힌_손_알약_저장(world): # noqa: F811 + key = _sc(world, [{"로직": STONE, "수량": {"손": 2}}]) + ref, whole = {**SYS, "키": key}, m01.loaded()[1] + with store.at(world["system"]): + doc = refs.read_ref(ref) + frozen = refs.with_pills(doc) + old = {**frozen, "담은로직": [{**frozen["담은로직"][0], "수량": {"손": 9}}]} # 옛 굳힌 손 + pill = {f"{refs.ref_id(ref)}#0": 4} + with store.at(world["system"]): + bare = refs.Expand(whole, read=lambda r: frozen)(ref) + fed = refs.Expand(whole, read=lambda r: frozen, given=pill)(ref) + kept = refs.Expand(whole, read=lambda r: old, given=pill)(ref) + assert bare["기준"]["계"] == pytest.approx(_stone() * 2) # 저장 + assert fed["기준"]["계"] == pytest.approx(_stone() * 4) # 알약 > 저장 + assert kept["기준"]["계"] == pytest.approx(_stone() * 9) # 굳힌 손 > 알약 + + +def _saved_doc(): + """723-4 SC 꼴(고정 노무 1000 · 2000 + 자재 MAT 500) · 저장 수량 2 · 2 · 자재 3 + = 굳힌 `{저장}`.""" + doc = _doc(hand=2, mat=3) + return { + **doc, + "담은로직": [refs.saved_qty(x) for x in doc["담은로직"]], + "자재": [refs.saved_qty(x) for x in doc["자재"]], + } + + +def test_B09_저장_수량_금액_알약_있으면_알약(): + daega, block = _build(_saved_doc(), ({}, "L:lib")) + assert [ln["qty"] for ln in block["lines"]] == [2.0, 2.0, 3.0] + assert not any(ln.get("ask") for ln in block["lines"]) and block["cover"] == f"C:{RID}" + got = compute_labor(daega, SHEETS)["daega_blocks"][0] + assert got["sum"] == 2000.0 + 4000.0 + 1500.0 and got["blank"] == 0 + given = {f"{RID}#0": 1.5, f"{RID}#자재|{MAT}": 4} # 첫 줄 · 자재만 알약 · 둘째 줄 = 저장 + daega, block = _build(_saved_doc(), (given, "S:pipe@100.0")) + assert [ln["qty"] for ln in block["lines"]] == [1.5, 2.0, 4.0] + assert block["cover"] == f"C:{RID}|S:pipe@100.0" + got = compute_labor(daega, SHEETS)["daega_blocks"][0] + assert got["sum"] == 1500.0 + 4000.0 + 2000.0 + + +def test_B09_옛_사본_null_은_그대로_빈_줄(): + daega, block = _build(_doc(), ({}, "L:lib")) # 723-1 때 굳힌 사본 = 전후 같음 + assert [ln["qty"] for ln in block["lines"]] == [None, None, None] + assert compute_labor(daega, SHEETS)["daega_blocks"][0]["blank"] == 3 + + +def test_단가산출_안_일위대가_저장_수량_금액(world): # noqa: F811 + from M02_MasterTemplete import M02_Sangeun_Calc as calc + + key = _sc(world, [{"로직": STONE, "수량": {"손": 2}}]) + sk = { + "키": "SK-000001", + "이름": "겹", + "단위": "식", + "더한줄": [{"참조": {**SYS, "키": key}, "수량": 3}], + } + with store.at(world["system"]): + got = calc.calc_doc(calc.clean_doc(sk, refs.here())) + assert not got["빠진줄"] + assert got["1단위"]["계"] == pytest.approx(float(Decimal(str(_stone())) * 2 * 3), rel=1e-6) + + +def _src(name: str) -> str: + return open(os.path.join(SRC, name), encoding="utf-8").read() + + +def test_화면_수량_칸_저장_새_줄_1_두_칸(): + table = _src("M02_MasterTemplete_Labor_Table.ts") + assert "const temp = new Map" not in table and "withTemp" not in table # 휘발 칸 걷음 + assert "row.수량 = raw ? { 손: v } : null;" in table and "changed(true);" in table + assert table.count("수량: { ...ONE }") == 4 # 기초 · 참조 · 고정 · 자재 새 줄 = 1 + assert 'BasePrice: ["기준단위 단가"' in table and 'QtyAmount: ["수량 반영 금액"' in table + assert 'sum_price: t("BasePrice"), sum: t("QtyAmount")' in table + assert 'NeedQty: ["수량 입력"' in table + assert 'if (!edit || key !== "qty" || n === null) return false;' in table # 읽기 층 못 고침 diff --git a/resources/tester/test_m02_hopyo_build.py b/resources/tester/test_m02_hopyo_build.py index d3f4f6293..d2103a934 100644 --- a/resources/tester/test_m02_hopyo_build.py +++ b/resources/tester/test_m02_hopyo_build.py @@ -66,9 +66,10 @@ def test_확인한_줄만_별칭_수량_빠진_줄_장부(client): assert origin["확인"]["누가"] == {"id": 42, "이름": "갑"} and origin["확인"]["때"] stone, *made, soil = doc["담은로직"] # 705-3 — 확인 안 한 줄 · 「우리에 없음」 = 고정 줄 assert stone["고정"]["단가"] == {"노무비": 0, "재료비": 1400000, "경비": 0} - hands = origin["실무수량"]["줄"] # 723-1 — 수량 = 저장 안 함 · 엑셀 수량은 출처 기록만 - assert stone["고정"]["출처"]["줄"] == 0 and stone["수량"] is None and hands["0"] == 1 - assert soil["고정"]["이름"] == "잔토처리(인력100%)" and soil["수량"] is None + hands = origin["실무수량"]["줄"] # 742-1 — 엑셀 수량 = 저장 수량 · 출처 기록도 같은 값 + assert stone["고정"]["출처"]["줄"] == 0 and stone["수량"] == {"손": hands["0"]} == {"손": 1} + assert soil["고정"]["이름"] == "잔토처리(인력100%)" + assert soil["수량"] == {"손": hands[str(len(doc["담은로직"]) - 1)]} assert hands[str(len(doc["담은로직"]) - 1)] == 0.09 assert origin["품셈"] == [1, 2] and got["줄수"]["고정"] == 2 and got["빠진줄"] == [] assert made == [ @@ -76,14 +77,14 @@ def test_확인한_줄만_별칭_수량_빠진_줄_장부(client): "로직": "GF000101", "입력값": {"지역": "서울"}, "별칭": "구조물터파기(인력100%)", - "수량": None, # 723-1 — 엑셀 0.14 = 본뜬.실무수량 + "수량": {"손": 0.14}, # 742-1 — 엑셀 0.14 = 저장 수량 "메모": "실무 0.14㎥ × 1 · 토사", }, { "로직": "GF000113", "입력값": {}, "별칭": "구조물되메우기(인력100%)", - "수량": None, # 엑셀 0.05 × 배 2 = 본뜬.실무수량(723-1) + "수량": {"손": 0.1}, # 엑셀 0.05 × 배 2 = 저장 수량(742-1) "메모": "실무 0.05㎥ × 2", }, ] @@ -117,18 +118,20 @@ def test_로직_여럿_같은_별칭_감쌈_자재_몫_덮음(client): first, second, fx, wrapped = doc["담은로직"] assert fx["고정"]["출처"]["줄"] == 2 # 확인 안 한 줄 = 고정(705-3) assert first["별칭"] == second["별칭"] == "구조물터파기(인력100%)" # 한 줄 로직 둘 = 같은 별칭 - hands = doc["본뜬"]["실무수량"]["줄"] # 723-1 — 저장 수량 없음 · 엑셀 수량 = 출처 기록 - assert second["수량"] is None and hands["1"] == 0.014 + hands = doc["본뜬"]["실무수량"]["줄"] # 742-1 — 엑셀 수량 = 저장 수량 · 출처 기록도 같은 값 + assert second["수량"] == {"손": 0.014} 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 + wrapped["별칭"] == "잔토처리(인력100%)" + and wrapped["수량"] == {"손": 0.09} + and hands["3"] == 0.09 ) inner = _doc(mine, wrapped["참조"]["키"]) assert inner["본뜬"]["키"] == "HP-000001 제3호표 4째 줄 GF000101" - assert inner["담은로직"][0]["로직"] == "GF000101" and inner["담은로직"][0]["수량"] is None + assert inner["담은로직"][0]["로직"] == "GF000101" and inner["담은로직"][0]["수량"] == {"손": 1} assert inner["본뜬"]["실무수량"]["줄"] == {"0": 1} assert [(m["코드"], m["수량"], m["별도계상"]) for m in doc["자재"]] == [ - ("MT004258", None, "직접계산") + ("MT004258", {"손": 1}, "직접계산") # 742-1 저장 수량 ] assert doc["본뜬"]["실무수량"]["자재"] == {"MT004258": 1} stone = next(v for k, v in _book(mine).items() if k.startswith("임도표지석(받침")) @@ -251,12 +254,12 @@ def test_하위_호표_먼저_확인_퍼센트_빠짐_통째(client): made = _doc(mine, got["참조"]["키"]) line = made["담은로직"][0] assert line["참조"] == child["참조"] and line["별칭"] == "고화매트재료비" - assert line["수량"] is None and made["본뜬"]["실무수량"]["줄"]["0"] == 2.3 # 723-1 + assert line["수량"] == {"손": 2.3} and made["본뜬"]["실무수량"]["줄"]["0"] == 2.3 # 742-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]["수량"] is None and doc["본뜬"]["실무수량"]["줄"] == {"0": 1} + assert doc["담은로직"][0]["수량"] == {"손": 1} 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["원래"]["품명"] == "임도표지석" diff --git a/resources/tester/test_m02_hopyo_flow.py b/resources/tester/test_m02_hopyo_flow.py index d0062d5d6..8c1d4452c 100644 --- a/resources/tester/test_m02_hopyo_flow.py +++ b/resources/tester/test_m02_hopyo_flow.py @@ -134,8 +134,9 @@ def test_섞인_줄_몫_배(client): made = _build(client, "HP-000001", no, [{"차례": line["차례"], "대상": [one]}], decide=True) 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) + want = round(line["수량"] * 0.1, 6) # 742-1 — 엑셀 수량(몫) = 저장 수량 · 출처 기록도 같은 값 + assert doc["담은로직"][at]["수량"] == {"손": want} + assert doc["본뜬"]["실무수량"]["줄"][str(at)] == want def test_막는_길(client): diff --git a/resources/tester/test_m02_labor_ref.py b/resources/tester/test_m02_labor_ref.py index b3534e2ef..6907dd7d6 100644 --- a/resources/tester/test_m02_labor_ref.py +++ b/resources/tester/test_m02_labor_ref.py @@ -228,11 +228,13 @@ def test_calc_는_참조를_펼침_승격은_기준_나누기_배_사전조합 ref = {**SYS, "키": sc} with store.at(world["system"]): doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 5}}]}) - bare = labor.calc(doc, {})["줄"][0] # 723-1 — 안 줄 손은 저장 안 함 · 안 이으면 수량 없음 + bare = labor.calc(doc, {})["줄"][0] # 742-1 — 안 줄 저장 수량(손 2) 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] + fed = labor.calc(doc, {f"{refs.ref_id(ref)}#0": 4})["줄"][0] # 알약이 저장보다 앞섬 want = Decimal(str(_base(TEN)["계"])) / 10 * 2 + assert bare["기준"]["계"] == pytest.approx(float(want)) and bare["안"][0]["수량"] == 2 + assert fed["기준"]["계"] == pytest.approx(float(want * 2)) and fed["안"][0]["수량"] == 4 + one = got["줄"][0] assert one["기준"]["계"] == pytest.approx(float(want)) assert (one["분모"], one["배"], one["알약"]) == ( "㎡", @@ -450,8 +452,9 @@ def test_층_목록_서고_줄은_이름_단위_기준금액을_실음(world): sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 3}}], "셋") rows = [{"종류": "precombo", "이름": sc}, {"종류": "labor", "이름": "rv"}] refs.summaries(world["system"], rows) - # 723-1 — 안 줄 수량 저장 안 함 → 목록 기준금액 없음(쓰는 쪽 알약으로만 값) - assert (rows[0]["제목"], rows[0]["기준금액"]) == ("셋", None) + # 742-1 — 목록 기준금액 = 안 줄 저장 수량(손 3)으로 + want = Decimal(str(_base(TEN)["계"])) / 10 * 3 + assert rows[0]["제목"] == "셋" and rows[0]["기준금액"] == pytest.approx(float(want)) assert "기준금액" not in rows[1] @@ -468,8 +471,8 @@ def test_사전조합_로직_줄_섞임_합은_손셈_지우기_409_굳힘(world ] sc = _combo(world["system"], lines, "섞음") saved = _read(world["system"], "precombo", sc)["담은로직"] - assert saved[1:] == [ # 723-1 — 안 줄 수량 저장 안 함 - {"로직": STONE, "입력값": {}, "수량": None, "메모": "돌"}, + assert saved[1:] == [ # 742-1 — 안 줄 수량 저장(빈 수량 = null) + {"로직": STONE, "입력값": {}, "수량": {"손": 3}, "메모": "돌"}, {"로직": TEN, "입력값": {}, "수량": None, "메모": None}, ] ten, stone = (Decimal(str(_base(c)["계"])) for c in (TEN, STONE))