diff --git a/M02_MasterTemplete/CostFlow/M02_CF_Router.py b/M02_MasterTemplete/CostFlow/M02_CF_Router.py index cc7aa882f..a611eca45 100644 --- a/M02_MasterTemplete/CostFlow/M02_CF_Router.py +++ b/M02_MasterTemplete/CostFlow/M02_CF_Router.py @@ -342,10 +342,70 @@ def _user(uid: str, local: list[dict]) -> dict: return {"ok": True, "value": one["value"], "name": one.get("name"), "unit": one.get("unit")} +MACHINE_SEP = "#" # 입력 판(sub3) 기계 요소 ref 「EQ키#칸」 +HOURLY = "시간당 경비" # 기계 칸 이름 — 기계 시간당 경비 로직으로 셈(기종 → 로직 짝) +_MACHINE_PLAIN = {"기계", "지역", "보정작업", "조종원직종"} # 이 밖의 입력 = 부착 장비 전용 + + +def _machine_family() -> list[dict]: + """기계 시간당 경비 로직 — 「건설기계」 장 파일 안 · 입력 「기계」 고르기가 있는 돈 로직.""" + files = store.loaded()[0] + return [ + row + for name, data in files.items() + if data.get("그룹") == "로직" and "건설기계" in name + for row in data.get("줄") or [] + if str(row.get("결과단위") or "").startswith("원") + and any(i.get("이름") == "기계" and i.get("고르기") for i in row.get("입력") or []) + ] + + +def _machine_rank(row: dict) -> tuple: + """짝 차례(옛 EasyCalc `pickMachine` 와 같음) — 부착 전용 · 손료만 · 보정 · 기종 수 · 키.""" + names = [i.get("이름") for i in row.get("입력") or []] + codes = next(i.get("고르기") for i in row["입력"] if i.get("이름") == "기계") + attach = any(n not in _MACHINE_PLAIN for n in names) + loss = "지역" not in names + return (attach, loss, loss and "보정작업" in names, len(codes), str(row.get("키"))) + + +def _machine_hourly(key: str, inputs: dict) -> dict: + """기계 한 시간 값 — 기종을 받는 기계경비 로직 가운데 짝 차례 첫째 · 빈 입력은 첫 고르기.""" + row, why = _brief(key, "기계") + if row is None: + return {"ok": False, "reason": why} + code = str(row.get("원문번호") or "") + fit = [r for r in _machine_family() if code in [str(c) for c in _codes(r)]] + if not fit: + return {"ok": False, "reason": f"「{row.get('이름')}」 은 시간당 경비를 셀 자료가 없음"} + logic = min(fit, key=_machine_rank) + given = {} + for spec in logic.get("입력") or []: + name = spec.get("이름") + if name != "기계": + given[name] = inputs.get(name, (spec.get("고르기") or [None])[0]) + args = "".join(f", {n}='{v}'" for n, v in given.items()) + ref = f"로직({logic['키']}, 기계='{code}'{args})" + line = {"종류": "로직", "요소": ref, "이름": "기계", "단위": "시간", "수량": "1"} + got = _money(store.calc("", {}, _row(line, ()), None), "계") + return {**got, "name": row.get("이름"), "unit": "원/시간"} if got["ok"] else got + + +def _codes(row: dict) -> list: + return next( + (i.get("고르기") or [] for i in row.get("입력") or [] if i.get("이름") == "기계"), [] + ) + + def one_value(item: dict, local: list[dict] | None = None) -> dict: - """물음 한 줄 `CfQuery {id, group, ref? · table? · unit? · field? · inputs?}` → `CfAnswer`.""" + """물음 한 줄 `CfQuery {id, group, ref? · table? · unit? · field? · inputs?}` → `CfAnswer`. + + 기계 ref 「EQ키#칸」(입력 판 요소 ref)도 받음 · 칸 「시간당 경비」 = 기계경비 로직으로 셈. + """ group, ref = item.get("group"), item.get("ref") - table = item.get("table") or {} + table, field = item.get("table") or {}, item.get("field") + if group == "기계" and isinstance(ref, str) and MACHINE_SEP in ref: + ref, _, field = ref.partition(MACHINE_SEP) try: if isinstance(ref, str) and _UKEY.match(ref): got = _user(ref, local or []) @@ -358,9 +418,11 @@ def one_value(item: dict, local: list[dict] | None = None) -> dict: elif group == "기계" and str(ref or "").startswith("로직("): inputs = item.get("inputs") or {} line = {"종류": "로직", "요소": ref, "이름": "기계", "단위": "시간", "수량": "1"} - got = _money(store.calc("", inputs, _row(line, inputs), None), item.get("field")) + got = _money(store.calc("", inputs, _row(line, inputs), None), field) + elif group == "기계" and field == HOURLY: + got = _machine_hourly(str(ref or ""), item.get("inputs") or {}) elif group in ("기계", "유가전력", "환율", "요율"): - got = _plain_value(group, str(ref or ""), item.get("field")) + got = _plain_value(group, str(ref or ""), field) else: got = {"ok": False, "reason": f"모르는 그룹 「{group}」"} except store.StoreError as e: diff --git a/resources/tester/test_m02_cf_router.py b/resources/tester/test_m02_cf_router.py index bbeeddc59..6b8e4bf83 100644 --- a/resources/tester/test_m02_cf_router.py +++ b/resources/tester/test_m02_cf_router.py @@ -305,3 +305,49 @@ def test_시스템_관리자가_아니면_403(folder: Path) -> None: assert client.get("/api/m02/cf/user-elements").status_code == 403 app.dependency_overrides[verify_session] = lambda: {"role": "SYSTEM_ADMIN", "company_id": None} assert [x["key"] for x in client.get("/api/m02/cf/list").json()["logics"]] == ["견본 돌쌓기"] + + +@pytest.mark.skipif(NO_NODE, reason="node 가 없음") +def test_기계_키_칸_꼴과_시간당_경비(client: TestClient) -> None: + """입력 판(sub3) 기계 ref 「EQ키#칸」 · 물음 {ref, field} · 「시간당 경비」 = 기계경비 로직.""" + logic = min( + (r for r in cf._machine_family() if "지역" in [i["이름"] for i in r["입력"]]), + key=cf._machine_rank, + ) + code = str(cf._codes(logic)[0]) + eq = _first("기계.json", lambda r: str(r.get("원문번호")) == code) + family = [r for r in cf._machine_family() if code in [str(c) for c in cf._codes(r)]] + chosen = min(family, key=cf._machine_rank) + given = { + i["이름"]: (code if i["이름"] == "기계" else (i.get("고르기") or [None])[0]) + for i in chosen["입력"] + } + want = _post(client, "/api/m01/calc", {"key": chosen["키"], "inputs": given})["sums"]["계"] + + items = [ + {"id": "p", "group": "기계", "ref": f"{eq['키']}#가격"}, + {"id": "p2", "group": "기계", "ref": eq["키"], "field": "가격"}, + {"id": "h", "group": "기계", "ref": f"{eq['키']}#시간당 경비"}, + {"id": "h2", "group": "기계", "ref": eq["키"], "field": "시간당 경비"}, + {"id": "x", "group": "기계", "ref": f"{eq['키']}#없는칸"}, + ] + got = {v["id"]: v for v in _post(client, "/api/m02/cf/values", {"items": items})["values"]} + assert got["p"]["value"] == got["p2"]["value"] == eq["값"]["가격"] + assert got["h"]["value"] == got["h2"]["value"] == want and got["h"]["unit"] == "원/시간" + assert got["x"]["ok"] is False and "없는칸" not in got["x"].get("value", "") + + # 저장 — 입력 상자 요소 ref 그대로(「EQ키#칸」) + box = _box("m1", "input", "기계", input={"mode": "group", "group": "기계", "variables": [], + "elements": [{"ref": f"{eq['키']}#시간당 경비", "name": "기계 · 시간당 경비", + "spec": "", "unit": "원/시간"}]}) # fmt: skip + f1 = _box("f1", "function", "곱", func=_func( + {"id": "s1", "이름": "곱", "칸": {"B1": {"식": "A1*2"}}}, + [{"port": "단가", "cell": "A1"}], + {"cell": "B1", "name": "경비", "unit": "원"})) # fmt: skip + o1 = _box("o1", "output", "경비", output={"cost": "경비", "unit": "원"}) + body = {"key": None, "name": "기계 견본", "resultUnit": "원/시간", "boxes": [box, f1, o1], + "links": [_link("a", "m1", "기계 · 시간당 경비", "f1", "단가"), + _link("b", "f1", "경비", "o1", "값")], "userElements": []} # fmt: skip + saved = _post(client, "/api/m02/cf/save", {"logic": body}) + assert _d(saved["values"]["cost"]["경비"]) == 2 * _d(want) + _post(client, "/api/m02/cf/delete", {"key": saved["key"]})