fix(M02): 흐름 셈 옛 셈과 같은 뜻 — 재료 값이 로직 입력(자재지역 · 계약종별) 읽음 · 빈 단가는 알림 · 표 범위 경계 칸 · 검색어 조건 찾기(PLAN 36-2 대조)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
This commit is contained in:
@@ -19,6 +19,7 @@ from M01_MasterData import M01_MasterData_Store as store
|
||||
cm = store.cm
|
||||
_UKEY = re.compile(r"^UE-(\d{4,})$")
|
||||
RANGE_LOW, RANGE_HIGH = "·아래", "·위"
|
||||
RANGE_RULE = "범위규칙" # 표 블록 끝 칸 — 범위 경계(옛 find 와 같음 · 없으면 초과·이하)
|
||||
FIELDS = (*cm.mf.COST_ITEMS, "계")
|
||||
|
||||
|
||||
@@ -93,10 +94,12 @@ def _pick(key: Any) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
def _material(cond: dict, unit: str) -> dict:
|
||||
"""검색어 묶음 재료 단가 — 기존 로직 재료 줄과 같은 길(검색어 · 자동값 차례 · 단위 환산)."""
|
||||
def _material(cond: dict, unit: str, inputs: dict | None = None) -> dict:
|
||||
"""검색어 묶음 재료 단가 — 기존 로직 재료 줄과 같은 길(검색어 · 자동값 차례 · 단위 환산)
|
||||
· inputs = 로직 입력(자재지역 · 계약종별 · 검색어 {자리}) — 옛 셈이 읽는 그대로."""
|
||||
inputs = inputs or {}
|
||||
line = {"종류": "재료", "요소": cond, "이름": cond.get("검색어") or "재료", "수량": "1"}
|
||||
got = _calc({}, _row({**line, "단위": unit, "비목": "재료비"}, ()))
|
||||
got = _calc(inputs, _row({**line, "단위": unit, "비목": "재료비"}, inputs))
|
||||
if not got["ok"]:
|
||||
return got
|
||||
one = got["lines"][0]
|
||||
@@ -108,7 +111,7 @@ def _material(cond: dict, unit: str) -> dict:
|
||||
if empty
|
||||
else "맞는 재료 없음"
|
||||
)
|
||||
return {"ok": False, "reason": f"「{word}」 {why}"}
|
||||
return {"ok": False, "blank": True, "reason": f"「{word}」 {why} — {one.get('까닭') or ''}"}
|
||||
picked = one.get("품목") or {}
|
||||
return {"ok": True, "value": one["단가"], "name": picked.get("이름"), "unit": unit or None}
|
||||
|
||||
@@ -125,7 +128,7 @@ def _brief(key: str, want: str) -> tuple[dict | None, str | None]:
|
||||
return row, None
|
||||
|
||||
|
||||
def _price(group: str, key: str, unit: str = "") -> dict:
|
||||
def _price(group: str, key: str, unit: str = "", inputs: dict | None = None) -> dict:
|
||||
"""인력 노임 · 재료 단가 — 준용 · 낮은 값 · 출처는 엔진 그대로."""
|
||||
row, why = _brief(key, group)
|
||||
if row is None:
|
||||
@@ -133,11 +136,13 @@ def _price(group: str, key: str, unit: str = "") -> dict:
|
||||
kind, cost = {"인력": ("인력", "노무비"), "재료": ("재료", "재료비")}[group]
|
||||
line = {"종류": kind, "요소": key, "이름": row.get("이름") or key, "수량": "1"}
|
||||
line = {**line, "단위": unit or row.get("단위") or "", "비목": cost}
|
||||
got = _calc({}, _row(line, ()))
|
||||
inputs = inputs or {}
|
||||
got = _calc(inputs, _row(line, inputs))
|
||||
if not got["ok"]:
|
||||
return got
|
||||
one = got["lines"][0]
|
||||
out = {"ok": one["단가"] is not None, "value": one["단가"], "name": row.get("이름")}
|
||||
out |= {"blank": True} if one["단가"] is None else {} # 옛 셈은 그 줄만 비우고 계속
|
||||
return {**out, "unit": row.get("단위"), **({"reason": one["까닭"]} if one.get("까닭") else {})}
|
||||
|
||||
|
||||
@@ -209,6 +214,11 @@ def _table_of(table: dict | None, key: str, at: Any) -> dict:
|
||||
else:
|
||||
line.append(None if v == "" else v if not isinstance(v, dict) else str(v))
|
||||
rows.append(line)
|
||||
if (
|
||||
"범위" in conds.values()
|
||||
): # 범위 경계 규칙 — 끝 칸 「범위규칙」 줄마다 같은 글(lookup 이 읽음)
|
||||
rule = str(table.get(RANGE_RULE) or "초과·이하")
|
||||
head, rows = [*head, RANGE_RULE], [[*r, rule] for r in rows]
|
||||
return {"ok": True, "rows": [head, *rows], "name": table.get("이름"), "unit": table.get("기준")}
|
||||
|
||||
|
||||
@@ -304,9 +314,9 @@ def one_value(item: dict, local: list[dict] | None = None) -> dict:
|
||||
elif group in ("소요량", "계수"):
|
||||
got = _table(str(table.get("key") or ref or ""), table.get("row"))
|
||||
elif group == "재료" and _pick(ref) is not None:
|
||||
got = _material(_pick(ref), str(item.get("unit") or ""))
|
||||
got = _material(_pick(ref), str(item.get("unit") or ""), item.get("inputs"))
|
||||
elif group in ("인력", "재료"):
|
||||
got = _price(group, str(ref or ""), str(item.get("unit") or ""))
|
||||
got = _price(group, str(ref or ""), str(item.get("unit") or ""), item.get("inputs"))
|
||||
elif group == "기계" and str(ref or "").startswith("로직("):
|
||||
inputs = item.get("inputs") or {}
|
||||
line = {"종류": "로직", "요소": ref, "이름": "기계", "단위": "시간", "수량": "1"}
|
||||
@@ -387,7 +397,28 @@ def materials(q: str, sub: str, detail: str, page: int, size: int) -> dict:
|
||||
return {"total": len(hits), "page": page, "size": size, "items": [_brief_row(r) for r in part]}
|
||||
|
||||
|
||||
FIELDS_RULE, OPS_RULE = ("규격", "이름", "키"), ("포함", "같음") # Model CfMaterialRule
|
||||
FIELDS_RULE, OPS_RULE = ("규격", "이름", "키", "검색어"), ("포함", "같음") # Model CfMaterialRule
|
||||
_HOLE = re.compile(r"\{[^{}]+\}")
|
||||
|
||||
|
||||
def _filled(template: str, value: Any) -> str:
|
||||
"""검색어 틀의 {자리} — 알약 값이 이미 채운 글이면 그대로 · 아니면 자리마다 그 값."""
|
||||
text = str(value)
|
||||
pattern = "".join(
|
||||
".+" if _HOLE.fullmatch(p) else re.escape(p) for p in re.split(r"(\{[^{}]+\})", template)
|
||||
)
|
||||
return text if re.fullmatch(pattern, text) else _HOLE.sub(lambda _: text, template)
|
||||
|
||||
|
||||
def _search(slot: dict, value: Any) -> dict:
|
||||
"""조건 찾기 field 검색어 — 틀(요소 ref 묶음)의 {자리} 를 알약 값으로 채워 옛 찾기 그대로."""
|
||||
cond = _pick(slot.get("ref"))
|
||||
if cond is None:
|
||||
return {"ok": False, "reason": "검색어 틀이 없음 — 요소에 검색어 묶음을 넣어 주세요"}
|
||||
if value in (None, ""):
|
||||
return {"ok": False, "reason": "찾을 값이 비어 있음 — 이어진 알약을 확인해 주세요"}
|
||||
fill = {k: _filled(v, value) for k, v in cond.items() if isinstance(v, str) and _HOLE.search(v)}
|
||||
return _material({**cond, **fill}, str(slot.get("unit") or ""), slot.get("inputs"))
|
||||
|
||||
|
||||
def slot_price(slot: dict, value: Any = None, pick: str | None = None) -> dict:
|
||||
@@ -397,14 +428,14 @@ def slot_price(slot: dict, value: Any = None, pick: str | None = None) -> dict:
|
||||
scope?: {구분?, 상세구분?, 찾는말?}, field?: 규격 | 이름, op?: 포함 | 같음}
|
||||
조건 찾기(value = 이어진 알약 값): 하나면 그 단가 · 0 이면 맞는 재료 없음 · 여럿이면 후보 다섯.
|
||||
"""
|
||||
mode = slot.get("mode") or "fixed"
|
||||
mode, inputs = slot.get("mode") or "fixed", slot.get("inputs")
|
||||
if mode == "fixed":
|
||||
return _price("재료", str(slot.get("ref") or ""))
|
||||
return _price("재료", str(slot.get("ref") or ""), "", inputs)
|
||||
if mode == "choice":
|
||||
ref = pick or slot.get("default")
|
||||
if not ref:
|
||||
return {"ok": False, "reason": "고른 품목도 기본 품목도 없음 — 품목을 골라 주세요"}
|
||||
return _price("재료", str(ref))
|
||||
return _price("재료", str(ref), "", inputs)
|
||||
if mode != "rule":
|
||||
return {"ok": False, "reason": f"모르는 재료 방식 「{mode}」"}
|
||||
field, op, scope = (
|
||||
@@ -414,6 +445,8 @@ def slot_price(slot: dict, value: Any = None, pick: str | None = None) -> dict:
|
||||
)
|
||||
if field not in FIELDS_RULE or op not in OPS_RULE:
|
||||
return {"ok": False, "reason": f"모르는 찾기 규칙 「{field} {op}」"}
|
||||
if field == "검색어":
|
||||
return _search(slot, value)
|
||||
if value in (None, ""):
|
||||
return {"ok": False, "reason": "찾을 값이 비어 있음 — 이어진 알약을 확인해 주세요"}
|
||||
want = _squash(value)
|
||||
@@ -436,7 +469,7 @@ def slot_price(slot: dict, value: Any = None, pick: str | None = None) -> dict:
|
||||
"reason": f"맞는 재료 {len(hits)}개 — 범위나 규칙을 좁혀 주세요({names} …)",
|
||||
"candidates": [_brief_row(r) for r in hits[:5]],
|
||||
}
|
||||
got = _price("재료", str(hits[0].get("키")))
|
||||
got = _price("재료", str(hits[0].get("키")), "", inputs)
|
||||
return {**got, "ref": hits[0].get("키"), "name": hits[0].get("이름")}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user