diff --git a/resources/master_data/scripts/master_auto.py b/resources/master_data/scripts/master_auto.py index 217b63a4..32911604 100644 --- a/resources/master_data/scripts/master_auto.py +++ b/resources/master_data/scripts/master_auto.py @@ -89,5 +89,8 @@ def values(master: mf.Master, row: dict) -> dict: out[name] = spec["범위"][0] else: got = _lowest(master, finds.get(name, [])) - out[name] = got if got is not None else Decimal(1 if name in divs else 0) + if got is None and "원" in str(spec.get("단위") or ""): + out[name] = None # 가격 입력 — 0 원처럼 보이지 않게 비워 둠 + else: + out[name] = got if got is not None else Decimal(1 if name in divs else 0) return out diff --git a/resources/master_data/scripts/master_formula.py b/resources/master_data/scripts/master_formula.py index 7a88a453..b31581e1 100644 --- a/resources/master_data/scripts/master_formula.py +++ b/resources/master_data/scripts/master_formula.py @@ -32,6 +32,10 @@ class FormulaError(ValueError): pass +class EmptyInput(FormulaError): + """값을 안 넣은(null) 입력을 씀 — 그 줄만 비우고 로직은 계속(`run`).""" + + # ── 읽기 ────────────────────────────────────────────────────────────── class _Parser: def __init__(self, text: str): @@ -360,6 +364,8 @@ def evaluate(node, env: dict, master: Master, depth: int = 0): if kind == "name": if node[1] not in env: raise FormulaError(f"모르는 이름 「{node[1]}」") + if env[node[1]] is None: + raise EmptyInput(f"입력 「{node[1]}」 비어 있음") return env[node[1]] if kind == "ref": value = element(master, fill(node[1], env), env)[0] @@ -464,7 +470,7 @@ def _inputs(row: dict, given: dict) -> dict: raise FormulaError(f"입력 「{name}」={value} 고르기 밖") if "범위" in spec: low, high = spec["범위"] - if not low <= value <= high: + if value is not None and not low <= value <= high: raise FormulaError(f"입력 「{name}」={value} 범위 [{low}, {high}] 밖") env[name] = value unknown = set(given) - set(env) @@ -485,8 +491,23 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict: sums = dict.fromkeys(COST_ITEMS, Decimal(0)) lines = [] for item in row.get("호표", []): - qty = evaluate(parse(item["수량"]), env, master, depth) source = why = None + try: + qty = evaluate(parse(item["수량"]), env, master, depth) + except EmptyInput as e: # 비어 있는 입력을 쓰는 줄만 비움 + qty, price, why = None, None, str(e) + lines.append( + { + "이름": item.get("이름", ""), + "단위": item.get("단위", ""), + "수량": None, + "단가": None, + "금액": None, + "비목": {item["비목"]: None} if "비목" in item else {}, + "까닭": why, + } + ) + continue if item["종류"] == "로직": bundle = evaluate(parse(item["요소"]), env, master, depth) price = bundle["계"] diff --git a/resources/tester/test_m01_auto.py b/resources/tester/test_m01_auto.py index 4979af8b..7f2c1432 100644 --- a/resources/tester/test_m01_auto.py +++ b/resources/tester/test_m01_auto.py @@ -94,3 +94,20 @@ def test_자동값으로_로직_전부_멈춤_0() -> None: except (store.cm.mf.FormulaError, ArithmeticError, KeyError, TypeError) as e: stuck.append(f"{row['키']} {e}") assert stuck == [] + + +def test_원_단위_입력은_자동값_null(client) -> None: + got = _auto(client, "GF000056") # 체인톱가격(원) · 체인오일단가(원/ℓ) + assert got["체인톱가격"] is None and got["체인오일단가"] is None + assert got["소작업로"] == 0 # 원 아닌 수는 그대로 0 + + +def test_빈_입력을_쓰는_줄만_비움(client) -> None: + got = _calc(client, "GF000056") + blank = {one["이름"]: one for one in got["lines"] if one.get("까닭")} + assert set(blank) == {"체인오일", "체인톱 손료"} + for one in blank.values(): + assert one["단가"] is None and one["금액"] is None and "비어 있음" in one["까닭"] + rest = sum(Decimal(str(one["금액"])) for one in got["lines"] if one["금액"] is not None) + assert Decimal(str(got["sums"]["계"])) == rest > 0 # 빈 줄은 빼고 더함 + assert got["sums"]["경비"] == 0