fix(725-1·725-2): 단가산출 [항목 추가] 못 셈 줄 = 그 줄만 금액 없음 · 나머지 셈 계속
- 더한줄 로직 입력 = 줄 값 › 공통 옵션(바탕 입력값 · 화면 시험 입력) › 로직 기본값(M01 견본) 차례로 채움 — 셈 때 채워 문서엔 안 적음(공통 옵션 바꾸면 따라감) - 그래도 빈 입력 · 수량 없는 일위대가(723-1) · 못 셈 참조 = 0 값묶음 줄(차례 그대로) + 줄 까닭 · 답 `빠진줄` · 계 = 나머지 합 · 줄 남아 빼기 됨 - 쓰는 자리(`unit` · 층 목록 기준금액 · 일위대가 펼침) = 빠진 줄 있으면 통째 기준 없음(부분 값 안 넘김) - 화면 = 띠 「못 셈 줄 — 이름」 · 계산서 금액 — · M01 strict 셈 · Pick.ts 그대로 - 낡은 시험 고침: 715 store · 717-4 = 723-1 뒤 안 줄 수량을 알약(given)으로 · 719-2 = 수량 없음이 통째 까닭 아닌 빠진줄로 - 새 지킴 시험 test_725_1_sangeun_add_inputs(임시 storage) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -211,12 +211,22 @@ export function mountSangeunDetail(
|
||||
|
||||
function drawNotes(): void {
|
||||
const lost = ans?.짝잃음 ?? [];
|
||||
const gaps = ans?.빠진줄 ?? [];
|
||||
const revised = !!(doc.바탕판 && ans?.바탕판 && doc.바탕판 !== ans.바탕판);
|
||||
notes.replaceChildren(
|
||||
...(fail || ans?.까닭
|
||||
? [createBanner({ tone: "danger", text: fail || String(ans?.까닭) })]
|
||||
: []),
|
||||
...(revised ? [createBanner({ tone: "warning", text: L("M02_SangeunRevisedNote") })] : []),
|
||||
...(gaps.length
|
||||
? [
|
||||
createBanner({
|
||||
tone: "warning",
|
||||
text: `${L("M02_SangeunGap")} — ${gaps.map((x) => x.이름 || x.cid).join(" · ")}`,
|
||||
details: gaps.map((x) => `${x.이름 || x.cid} — ${x.까닭}`).join("\n"),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(lost.length
|
||||
? [
|
||||
createBanner({
|
||||
|
||||
@@ -13,6 +13,7 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
@@ -83,11 +84,20 @@ def _base_of(doc: dict, whole: Any, read: Any, trail: tuple[str, ...]) -> dict[s
|
||||
return {**up, "짝잃음": lost}
|
||||
|
||||
|
||||
def resolve(doc: dict, whole: Any, read: Any = refs.read_ref, trail: tuple = ()) -> dict[str, Any]:
|
||||
def resolve(
|
||||
doc: dict, whole: Any, read: Any = refs.read_ref, trail: tuple = (), given: dict | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""SK 문서 → 고친 줄 `{줄, 고름, 자재표시, 정체, 짝잃음, 입력값, 켜쌓기율, 배, 바탕판}` ·
|
||||
`trail` = 지나온 참조 이름표(순환 · 깊이 422)."""
|
||||
`trail` = 지나온 참조 이름표(순환 · 깊이 422) · `given` = 공통 옵션(더한줄 로직 입력)."""
|
||||
base = _base_of(doc, whole, read, trail)
|
||||
common = {**base["입력값"], **(doc.get("입력값") or {}), **(given or {})}
|
||||
doc, lack = _fill(doc, whole, common)
|
||||
got = sd.patched(base["줄"], doc, base["고름"], base["자재표시"])
|
||||
items = got["줄"]["호표"]
|
||||
for at, cid in enumerate(got["정체"][: len(items)]):
|
||||
hit = re.match(r"^add:#(\d+)$", cid)
|
||||
if hit and int(hit[1]) in lack:
|
||||
items[at] = _gap(items[at], lack[int(hit[1])])
|
||||
rate = doc.get("켜쌓기율", base["켜쌓기율"])
|
||||
return {
|
||||
**got,
|
||||
@@ -99,18 +109,70 @@ def resolve(doc: dict, whole: Any, read: Any = refs.read_ref, trail: tuple = ())
|
||||
}
|
||||
|
||||
|
||||
def _bundles(row: dict, whole: Any, expand: Any, trail: tuple) -> str | None:
|
||||
"""더한줄 참조 · 고정 줄 `값묶음` = 1 단위 노 · 재 · 경 — 못 셈이면 까닭."""
|
||||
for item in row["호표"]:
|
||||
# ── 못 셈 줄 = 그 줄만 빠짐(725-1 · 725-2) ─────────────────────────────────
|
||||
NO_QTY = "수량 없음" # `M02_Labor_Expand.NO_QTY` 와 같은 글(가져오기 둘레 피함)
|
||||
NO_QTY_WHY = "수량 없음 — 쓰는 자리에서 연결"
|
||||
|
||||
|
||||
def _defaults(whole: Any, code: str) -> dict:
|
||||
"""로직 기본값 = M01 견본 입력(`/m01/logic/auto` 와 같은 길 · 흐름 예시값 › 옛 자동값)."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M01_MasterData import M01_MasterData_Store_Flow as flow
|
||||
|
||||
try:
|
||||
got = flow.auto(code) or {"값": m01.mau.values(whole, whole.logic(code))}
|
||||
except Exception: # noqa: BLE001 — 없는 로직 · 자동값 못 셈 = 기본값 없음
|
||||
return {}
|
||||
return got.get("값") or {}
|
||||
|
||||
|
||||
def _fill(doc: dict, whole: Any, common: dict) -> tuple[dict, dict[int, str]]:
|
||||
"""더한줄 로직 입력 = 줄 값 › 공통 옵션 › 로직 기본값 — (채운 사본, {더한줄 차례: 까닭})."""
|
||||
adds, lack = [], {}
|
||||
for k, one in enumerate(doc.get("더한줄") or []):
|
||||
if one.get("종류") != "로직":
|
||||
adds.append(one)
|
||||
continue
|
||||
try:
|
||||
names = [str(s.get("이름")) for s in whole.logic(one["로직"]).get("입력") or []]
|
||||
except Exception: # noqa: BLE001 — 없는 로직 = 검사(`sd.clean`)가 먼저 막음
|
||||
names = []
|
||||
mine = dict(one.get("입력값") or {})
|
||||
blank = [n for n in names if mine.get(n) is None or not str(mine[n]).strip()]
|
||||
auto = _defaults(whole, one["로직"]) if blank else {}
|
||||
for n in blank:
|
||||
mine.pop(n, None)
|
||||
for v in (common.get(n), auto.get(n)):
|
||||
if v is not None and str(v).strip():
|
||||
mine[n] = v
|
||||
break
|
||||
miss = [n for n in names if n not in mine]
|
||||
if miss:
|
||||
lack[k] = f"입력 「{miss[0]}」 없음"
|
||||
adds.append({**one, "입력값": mine})
|
||||
return {**doc, "더한줄": adds}, lack
|
||||
|
||||
|
||||
def _gap(item: dict, why: str) -> dict:
|
||||
"""못 셈 줄 → 0 값묶음 줄(차례 그대로 · 계에 안 듦) + `빠짐` 까닭."""
|
||||
zero = dict.fromkeys(COSTS, Decimal(0))
|
||||
head = {k: item.get(k) or "" for k in ("이름", "단위")}
|
||||
return {**head, "종류": "로직", "요소": None, "수량": item["수량"], "값묶음": zero, "빠짐": why}
|
||||
|
||||
|
||||
def _bundles(row: dict, whole: Any, expand: Any, trail: tuple) -> None:
|
||||
"""더한줄 참조 · 고정 줄 `값묶음` = 1 단위 노 · 재 · 경 — 못 셈 줄 = `_gap`(나머지 셈 계속)."""
|
||||
for at, item in enumerate(row["호표"]):
|
||||
if item.get("값묶음") is not None or not ("참조" in item or "고정" in item):
|
||||
continue
|
||||
sub = expand(item["참조"], trail) if "참조" in item else fixed.line(whole, item["고정"])
|
||||
if sub.get("기준") is None:
|
||||
return f"「{sub.get('이름') or item.get('이름')}」 {sub.get('까닭')}"
|
||||
item["값묶음"] = {k: Decimal(str(sub["기준"][k])) for k in COSTS}
|
||||
item["이름"] = item.get("이름") or sub.get("이름") or ""
|
||||
item["단위"] = item.get("단위") or sub.get("단위") or ""
|
||||
return None
|
||||
if sub.get("기준") is None:
|
||||
why = str(sub.get("까닭") or "")
|
||||
row["호표"][at] = _gap(item, NO_QTY_WHY if NO_QTY in why else why)
|
||||
continue
|
||||
item["값묶음"] = {k: Decimal(str(sub["기준"][k])) for k in COSTS}
|
||||
|
||||
|
||||
def run(
|
||||
@@ -125,28 +187,33 @@ def run(
|
||||
고친 칸 `덮개`(칸 이름) · `원값`. 틀린 문서 · 순환 = `sd.DocError`."""
|
||||
from M02_MasterTemplete.M02_Labor_Expand import Expand
|
||||
|
||||
got = resolve(doc, whole, read, trail)
|
||||
got = resolve(doc, whole, read, trail, given)
|
||||
row = {**got["줄"], "호표": [dict(x) for x in got["줄"]["호표"]]}
|
||||
why = _bundles(row, whole, expand or Expand(whole, read), trail)
|
||||
_bundles(row, whole, expand or Expand(whole, read), trail)
|
||||
head = {k: got[k] for k in ("정체", "짝잃음", "바탕판")} | {"배": got["배"]}
|
||||
if why:
|
||||
return {**head, "기준": None, "1단위": None, "줄": [], "까닭": why}
|
||||
gaps = {at: x for at, x in enumerate(row["호표"]) if x.get("빠짐")}
|
||||
key = str(doc.get("키") or "SK-000000")
|
||||
over = _Over(whole, key, {**row, "키": key})
|
||||
values = {**got["입력값"], **(given or {})}
|
||||
out = labor.run_line(over, key, values, got["켜쌓기율"], got["고름"] or None)
|
||||
covers = doc.get("덮개") or {}
|
||||
lines = []
|
||||
for cid, line in zip(got["정체"], out.get("줄") or []):
|
||||
for at, (cid, line) in enumerate(zip(got["정체"], out.get("줄") or [])):
|
||||
mine = covers.get(cid) or {}
|
||||
mark = {"덮개": [k for k in sd.COVER_KEYS if k in mine]} if mine else {}
|
||||
if at in gaps: # 못 셈 줄 = 금액 — · 까닭(725)
|
||||
mark |= {"단가": None, "금액": None, "비목": {}, "까닭": gaps[at]["빠짐"]}
|
||||
lines.append(
|
||||
{**line, "cid": cid, **mark, **({"원값": mine["원값"]} if "원값" in mine else {})}
|
||||
)
|
||||
out = labor.drop_apart({**out, "줄": lines}, got["자재표시"])
|
||||
base = out.get("기준")
|
||||
unit = {k: Decimal(str(v)) / got["배"] for k, v in base.items()} if base else None
|
||||
return {**out, **head, "1단위": unit}
|
||||
gone = [
|
||||
{"cid": got["정체"][at], "이름": x.get("이름") or "", "까닭": x["빠짐"]}
|
||||
for at, x in gaps.items()
|
||||
]
|
||||
return {**out, **head, "1단위": unit, "빠진줄": gone}
|
||||
|
||||
|
||||
def unit(doc: dict, whole: Any, expand: Any, trail: tuple) -> dict[str, Any]:
|
||||
@@ -156,7 +223,9 @@ def unit(doc: dict, whole: Any, expand: Any, trail: tuple) -> dict[str, Any]:
|
||||
got = run(doc, whole, None, expand.read, expand, trail)
|
||||
except sd.DocError as e:
|
||||
return {**head, "기준": None, "까닭": str(e)}
|
||||
why = got.get("까닭") or (None if got.get("1단위") else "돈 아닌 로직")
|
||||
gone = got.get("빠진줄") or [] # 쓰는 자리 = 빠진 줄 있으면 통째 기준 없음(부분 값 안 넘김)
|
||||
why = got.get("까닭") or next((f"「{x['이름']}」 {x['까닭']}" for x in gone), None)
|
||||
why = why or (None if got.get("1단위") else "돈 아닌 로직")
|
||||
times = got["배"]
|
||||
lines = [
|
||||
{
|
||||
@@ -166,7 +235,13 @@ def unit(doc: dict, whole: Any, expand: Any, trail: tuple) -> dict[str, Any]:
|
||||
}
|
||||
for x in got.get("줄") or []
|
||||
]
|
||||
return {**head, "기준": got["1단위"], "까닭": why, "줄": lines, "짝잃음": got["짝잃음"]}
|
||||
return {
|
||||
**head,
|
||||
"기준": None if gone else got["1단위"],
|
||||
"까닭": why,
|
||||
"줄": lines,
|
||||
"짝잃음": got["짝잃음"],
|
||||
}
|
||||
|
||||
|
||||
def head_of(doc: dict) -> str:
|
||||
|
||||
@@ -63,6 +63,8 @@ export interface SkLine {
|
||||
묶음?: string | null;
|
||||
cid: string;
|
||||
율?: number;
|
||||
/** 못 셈 줄(725) — 금액 없음 · 계에 안 듦 */
|
||||
까닭?: string;
|
||||
}
|
||||
|
||||
export interface SkMat {
|
||||
@@ -79,6 +81,8 @@ export interface SkAnswer {
|
||||
줄: SkLine[];
|
||||
재료?: SkMat[];
|
||||
까닭?: string | null;
|
||||
/** 못 셈 줄(725 · 입력 없음 · 수량 없음) — 그 줄만 빠지고 나머지 셈 */
|
||||
빠진줄?: { cid: string; 이름: string; 까닭: string }[];
|
||||
정체: string[];
|
||||
짝잃음: { cid: string; 까닭: string; 바탕?: string }[];
|
||||
바탕판: string;
|
||||
@@ -283,7 +287,7 @@ export function sheetGroups(rows: CoverRow[]): SheetGroup[] {
|
||||
const entries = Object.entries(r.parts);
|
||||
for (const [k, v] of entries.length ? entries : [["경비", 0] as [string, number]]) {
|
||||
const share = entries.length > 1 && r.금액 ? (r.단가 ?? 0) * (v / r.금액) : r.단가;
|
||||
const amount = r.apart ? null : v;
|
||||
const amount = r.apart || (!entries.length && r.금액 == null) ? null : v;
|
||||
const g = take(k);
|
||||
g.소계 = (g.소계 ?? 0) + (amount ?? 0);
|
||||
g.줄.push({
|
||||
|
||||
Reference in New Issue
Block a user