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

This commit is contained in:
2026-11-02 21:49:03 +09:00
20 changed files with 321 additions and 199 deletions
+7 -4
View File
@@ -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):
+1 -1
View File
@@ -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 += [
@@ -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]]:
+10 -7
View File
@@ -346,17 +346,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) · 없으면 셈
@@ -368,7 +371,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 "")
@@ -378,7 +381,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(
+4 -4
View File
@@ -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(
+11 -7
View File
@@ -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)
@@ -157,7 +161,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
+17 -10
View File
@@ -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) ·
@@ -214,7 +214,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:
@@ -222,11 +222,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장 …)
@@ -553,7 +559,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})["물량"]}
+1 -1
View File
@@ -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` 한 벌 = 한 요청 안 메모).
@@ -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`(면적 잇기 · 수량산출).
* ========================================================================== */
@@ -51,10 +52,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}”?"],
@@ -63,9 +67,10 @@ const TEXT = {
} as const satisfies Record<string, readonly [string, string]>;
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,
@@ -73,14 +78,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<string, ItemLogic>();
/** 임시 수량(휘발) — 줄 id · 「자재|코드」 → 수 */
const temp = new Map<string, number>();
let calc: LaborCalc | null = null;
let last: LaborCalc | null = null;
let fail: string | null = null;
@@ -97,19 +97,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",
@@ -130,28 +117,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<number>();
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<string, string> = { 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);
@@ -165,13 +154,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);
@@ -189,7 +178,7 @@ export function mountLaborTable(
// 칸 초점이 표를 밀면(scroll) 메뉴가 바로 닫힘 — 밀림이 끝난 다음 그림에 엶
requestAnimationFrame(() => requestAnimationFrame(() => openTreeMenu(ev, menuAt(n))));
});
tipBorn(grid.root, at, (n) => (body.담은로직[n] ?? body.자재[n - body.담은로직.length])?.본뜬); // 740-2
tipBorn(grid.root, at, (n) => (cur.담은로직[n] ?? cur.자재[n - cur.담은로직.length])?.본뜬); // 740-2
const bar = unlinkedBar(cur, edit && ((qtyOnly) => changed(qtyOnly))); // 729-3 안 이은 줄 띠
gridHost.replaceChildren(...(bar ? [bar] : []), grid.root);
showSource();
@@ -205,10 +194,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) {
@@ -218,34 +206,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());
}
@@ -343,13 +324,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();
}
@@ -366,14 +342,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 찾아 넣기 창에서 재료를 고름 — 「자재 목록」 줄 · 로직 안 자재 · 이미 있는 자재는 막음 */
@@ -387,7 +370,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();
},
};
@@ -398,7 +381,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)),
});
@@ -469,7 +452,7 @@ export function mountLaborTable(
: []),
],
}),
createText(t("TempHint"), "hint"),
createText(t("QtyHint"), "hint"),
gridHost,
],
}),
+9 -6
View File
@@ -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)
)
@@ -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"] == {} # 구조물 사본 없어도 호표
# 단가 묶음 = 담은 로직 · 사전조합 자재 · 고른 자재(사본 셈이 마스터를 안 읽음)
@@ -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():
+5 -3
View File
@@ -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
+3 -3
View File
@@ -80,9 +80,9 @@ def test_list_amount_and_labor_line_holds_upper_sk(world): # noqa: F811
# 740-1 — 회사 SK = 로직 줄 + 패치로 펼쳐 담음 · 출처 = 본뜬
assert {k: saved[k] for k in ("층", "주인", "키")} == {**COMPANY, "키": sk}
assert line["로직"] and "참조" not in line
# 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 으로 바로 참조
+4 -2
View File
@@ -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)
+7 -48
View File
@@ -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<string, number>();" in table # 휘발 — 문서 밖
assert "calc = calc && settle(calc, withTemp(), {});" in table # 짝 셈 미리 보기
assert (
'const out: Record<string, unknown> = { ...cur, 종류: "사전조합" };' in table
) # temp 안 실음
assert "calc = settle(calc, cur, {});" in table # 짝 셈 바로(742-1 수량 = 문서)
assert 'const out: Record<string, unknown> = { ...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 고정 줄 이음
+140
View File
@@ -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 # 읽기 층 못 고침
+15 -12
View File
@@ -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("임도표지석(받침"))
@@ -253,12 +256,12 @@ def test_하위_호표_먼저_확인_퍼센트_빠짐_통째(client):
# 740-1 하위 호표 = 사본 블록(원본 연결 없음 · 키 = 출처)
assert line["참조"]["층"] == "사본" and line["참조"]["키"] == child["참조"]["키"]
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["원래"]["품명"] == "임도표지석"
+3 -2
View File
@@ -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):
+10 -7
View File
@@ -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["알약"]) == (
"㎡",
@@ -451,8 +453,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]
@@ -469,8 +472,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))