feat(736-3): 단가산출 · 일위대가 [항목 추가] 기계(M01 EQ) 고름 — 고정 줄(이음 기계 · 단가 = 한 시간 노 · 재 · 경)
- 찾아 넣기 창 기계 줄 누름 = 서버 `/logic-lib/machine-hourly` 로 한 시간 값을 받아 고정 줄 단가로 - 일위대가 = 이음 기계 고정 줄 · 단가산출 = 더한줄 고정 줄(수량 1) — 같은 금액 - 서버 `M02_Machine_Hourly`(기계경비 로직 짝 차례 첫째 · 읽기만) · 시험 추가 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ from __future__ import annotations
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from M01_MasterData import M01_MasterData_Router as m01
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as machine
|
||||
|
||||
router = APIRouter(prefix="/api/m02/logic-lib", tags=["M02 Logic Lib Read"])
|
||||
|
||||
@@ -71,3 +72,9 @@ def post_calc(body: m01.CalcBody) -> dict:
|
||||
@router.post("/text")
|
||||
def post_text(body: m01.CalcBody) -> dict:
|
||||
return m01.post_text(body)
|
||||
|
||||
|
||||
@router.get("/machine-hourly")
|
||||
def get_machine_hourly(key: str) -> dict:
|
||||
"""기계(EQ) 한 시간 값 노 · 재 · 경(736-3 · [항목 추가] 기계 고정 줄 단가)."""
|
||||
return machine.hourly(key)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
"""M02 기계(M01 EQ) 한 시간 값 — [항목 추가] 창에서 기계를 고르면 고정 줄 단가(PLAN 736-3).
|
||||
|
||||
기계 줄 = 일위대가 · 단가산출 더한줄의 고정 줄(이음 기계 EQ · 수량 1) — 단가 = 이 값(노 · 재 · 경).
|
||||
셈 = M01 「건설기계」 장 기계경비 로직 가운데 그 기종을 받는 짝 차례 첫째
|
||||
(`로직(GC, 기계='원문번호')` · 옵션은 기본값). 짝 차례 · 셈은
|
||||
`DataFlow/M02_DF_Values._machine_hourly` 와 같은 규칙(그 폴더는 시험 길이라 따로 둠).
|
||||
마스터 파일은 읽기만 · 로그인 사용자 모두.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
cm = store.cm
|
||||
_PLAIN = {"기계", "지역", "보정작업", "조종원직종"} # 이 밖의 입력 = 부착 장비 전용
|
||||
|
||||
|
||||
def _family() -> list[dict]:
|
||||
"""기계 시간당 경비 로직 — 「건설기계」 장 파일 안 · 입력 「기계」 고르기가 있는 돈 로직."""
|
||||
return [
|
||||
row
|
||||
for name, data in store.loaded()[0].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 _codes(row: dict) -> list:
|
||||
return next(i["고르기"] for i in row["입력"] if i.get("이름") == "기계")
|
||||
|
||||
|
||||
def _rank(row: dict) -> tuple:
|
||||
"""부착 전용 · 손료만 · 보정 · 기종 수 · 키 — 작을수록 먼저."""
|
||||
names = [i.get("이름") for i in row.get("입력") or []]
|
||||
attach = any(n not in _PLAIN for n in names)
|
||||
loss = "지역" not in names
|
||||
return (attach, loss, loss and "보정작업" in names, len(_codes(row)), str(row.get("키")))
|
||||
|
||||
|
||||
def hourly(key: str) -> dict[str, Any]:
|
||||
"""`{ok, 이름, 규격, 노무비, 재료비, 경비, 계}` · 못 셈 = `{ok: False, reason}`."""
|
||||
if not key.startswith("EQ"):
|
||||
return {"ok": False, "reason": f"「{key}」 는 기계 아님"}
|
||||
try:
|
||||
row = store.loaded()[1].get(key)
|
||||
except cm.mf.FormulaError as e:
|
||||
return {"ok": False, "reason": str(e)}
|
||||
code = str(row.get("원문번호") or "")
|
||||
fit = [r for r in _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=_rank)
|
||||
args = "".join(
|
||||
f", {i['이름']}='{(i.get('고르기') or [''])[0]}'"
|
||||
for i in logic.get("입력") or []
|
||||
if i.get("이름") != "기계"
|
||||
)
|
||||
call = f"로직({logic['키']}, 기계='{code}'{args})"
|
||||
line = {"종류": "로직", "요소": call, "이름": "기계", "단위": "시간", "수량": "1"}
|
||||
holder = {
|
||||
"키": "", "원문번호": "", "이름": "기계 시간", "결과단위": "원/식", "출처": "자체",
|
||||
"입력": [], "중간": [], "호표": [line], "덧줄": [], "끝수": None,
|
||||
} # fmt: skip
|
||||
got = store.calc("", {}, holder, None)
|
||||
if not got.get("ok") or "sums" not in got:
|
||||
return {"ok": False, "reason": got.get("reason") or "셈 못 함"}
|
||||
sums = {k: float(got["sums"][k]) for k in (*cm.mf.COST_ITEMS, "계")}
|
||||
return {"ok": True, "이름": row.get("이름"), "규격": row.get("규격"), **sums}
|
||||
@@ -49,6 +49,8 @@ export interface M01Element {
|
||||
규격: string;
|
||||
단위: string;
|
||||
값: number | null;
|
||||
/** 기계(736-3) — 한 시간 노 · 재 · 경(서버 `/logic-lib/machine-hourly` · 고르는 순간 받음) */
|
||||
단가?: Record<"노무비" | "재료비" | "경비", number>;
|
||||
}
|
||||
|
||||
/** 고를 수 있는 종류 — logic = M01 단가산출 · 기초 로직 · labor = 인력 · material = 재료 · machine = 기계 · sk = M02 단가산출 · pre = 일위대가 */
|
||||
@@ -80,6 +82,16 @@ export const logicHeads = (rows: StandardRow[]): StandardRow[] =>
|
||||
|
||||
/** 일위대가 고정 줄(705-1 이음 꼴) — 인력 = 노임 LB · 재료 = 자재 MT · 단가 = 지금 값(셈은 단가 책 값) */
|
||||
export function fixedOf(e: M01Element): LaborFixed {
|
||||
if (e.종류 === "기계") {
|
||||
// 736-3 기계 = 이음 기계 EQ · 단가 = 한 시간 노 · 재 · 경(셈은 줄 단가 그대로)
|
||||
return {
|
||||
이름: e.이름,
|
||||
규격: e.규격,
|
||||
단위: e.단위,
|
||||
단가: e.단가 ?? { 노무비: 0, 재료비: 0, 경비: 0 },
|
||||
이음: { 노임: null, 자재: null, 기계: e.코드 },
|
||||
};
|
||||
}
|
||||
const labor = e.종류 === "인력";
|
||||
return {
|
||||
이름: e.이름,
|
||||
@@ -171,6 +183,8 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
rowClass: (i) => (shown[i]?.dim ? "is-dim" : null),
|
||||
onRowClick: (i) => shown[i]?.go(),
|
||||
});
|
||||
const fail = (error: unknown): void =>
|
||||
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error");
|
||||
const take = (f: () => void) => (): void => {
|
||||
close();
|
||||
f();
|
||||
@@ -199,6 +213,20 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
},
|
||||
];
|
||||
});
|
||||
/** 기계 고름 — 한 시간 값을 받아 단가로(못 셈 = 까닭 알림 · 창 그대로) */
|
||||
const hourly = async (e: M01Element): Promise<M01Element | null> => {
|
||||
try {
|
||||
const got = await call<{ ok: boolean; reason?: string } & Record<string, number>>(
|
||||
`/logic-lib/machine-hourly?key=${encodeURIComponent(e.코드)}`,
|
||||
);
|
||||
if (!got.ok) throw new Error(got.reason ?? tc("Load_Failed"));
|
||||
const { 노무비, 재료비, 경비, 계 } = got;
|
||||
return { ...e, 값: 계, 단가: { 노무비, 재료비, 경비 } };
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const elementLines = (kind: M01Element["종류"], items: PickItem[]): Line[] =>
|
||||
items.map((m) => {
|
||||
const e: M01Element = {
|
||||
@@ -223,7 +251,11 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
구분: join(kind, m.구분, put ? "" : L("M02_FindElementLater")),
|
||||
출처: `M01 ${m.ref}`,
|
||||
dim: !put,
|
||||
go: put ? take(() => put(e)) : () => showToast(L("M02_FindElementLater"), "warning"),
|
||||
go: !put
|
||||
? () => showToast(L("M02_FindElementLater"), "warning")
|
||||
: kind === "기계"
|
||||
? () => void hourly(e).then((x) => x && take(() => put(x))())
|
||||
: take(() => put(e)),
|
||||
};
|
||||
});
|
||||
const docLines = (rows: SangeunRow[], kind: string): Line[] =>
|
||||
@@ -346,8 +378,6 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
search.input.focus();
|
||||
draw();
|
||||
|
||||
const fail = (error: unknown): void =>
|
||||
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error");
|
||||
void Promise.all([fetchLibLogics(), fetchStandard().catch(() => null)])
|
||||
.then(([l, s]) => {
|
||||
logics = l;
|
||||
|
||||
@@ -109,6 +109,7 @@ export function openLogicPicker(
|
||||
logic: done,
|
||||
ref: lib.pick,
|
||||
element: fixed && ((e) => fixed(fixedOf(e), e.이름)),
|
||||
machine: fixed && ((e) => fixed(fixedOf(e), e.이름)), // 736-3 기계 = 이음 기계 고정 줄
|
||||
material: lib.material,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { call, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { openFindPicker } from "./M02_MasterTemplete_Find_Pick";
|
||||
import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { fixedOf, openFindPicker } from "./M02_MasterTemplete_Find_Pick";
|
||||
import type { LaborFixed, LaborRef } from "./M02_MasterTemplete_Labor_Calc";
|
||||
|
||||
export const SANGEUN_KIND = "sangeun";
|
||||
/** 층 차례 — 위에서 아래 */
|
||||
@@ -73,7 +73,8 @@ export async function listLayerDocs(
|
||||
export type SangeunLine =
|
||||
| { 종류: "로직"; 로직: string; 입력값: Record<string, never>; 수량: number }
|
||||
| { 참조: LaborRef; 수량: number }
|
||||
| { 종류: "인력" | "재료"; 요소: string; 이름: string | null; 단위: string | null; 수량: number };
|
||||
| { 종류: "인력" | "재료"; 요소: string; 이름: string | null; 단위: string | null; 수량: number }
|
||||
| { 고정: LaborFixed; 이름: string; 단위: string | null; 수량: number };
|
||||
|
||||
/** 단가산출 [항목 추가] — `scope` = 그 SK 문서 층(고를 수 있는 서고 = 위 층만) */
|
||||
export function openSangeunLinePicker(
|
||||
@@ -87,5 +88,7 @@ export function openSangeunLinePicker(
|
||||
element: (e) =>
|
||||
e.종류 !== "기계" &&
|
||||
done({ 종류: e.종류, 요소: e.코드, 이름: e.이름, 단위: e.단위, 수량: 1 }),
|
||||
// 736-3 기계 = 고정 줄(이음 기계 EQ · 단가 = 한 시간 노 · 재 · 경) · 수량 1
|
||||
machine: (e) => done({ 고정: fixedOf(e), 이름: e.이름, 단위: e.단위, 수량: 1 }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ def test_721_2_부르는_곳_셋만_새_창():
|
||||
def test_721_2_고정_줄_꼴():
|
||||
find = _text(M02 / "M02_MasterTemplete_Find_Pick.ts")
|
||||
assert "이음: { 노임: labor ? e.코드 : null, 자재: labor ? null : e.코드, 기계: null }" in find
|
||||
assert "go: put ? take(() => put(e))" in find
|
||||
assert "take(() => put(e))" in find and "!put" in find
|
||||
assert "dim: !put," in find # 받는 쪽 없으면 회색 막힘
|
||||
|
||||
|
||||
@@ -151,3 +151,42 @@ def test_736_2_빠진_길_둘_신규_자재_기계_탭():
|
||||
assert '"machine"' in lost and "pickMachine" not in lost
|
||||
unlinked = _text(M02 / "M02_MasterTemplete_Labor_Unlinked.ts")
|
||||
assert 'machine: (e) => done("기계", e.코드)' in unlinked and "pickMachine" not in unlinked
|
||||
|
||||
|
||||
def test_736_3_기계_고정_줄_금액():
|
||||
"""736-3 — [항목 추가] 기계(EQ) = 고정 줄(이음 기계 · 단가 = 한 시간 노 · 재 · 경) — 단가산출 · 일위대가가 같은 금액."""
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as machine
|
||||
|
||||
EQ = "EQ000019" # 굴착기(무한궤도) 0.6㎥
|
||||
got = machine.hourly(EQ)
|
||||
assert got["ok"] and got["계"] > 0
|
||||
assert got["계"] == pytest.approx(got["노무비"] + got["재료비"] + got["경비"])
|
||||
assert not machine.hourly(LB)["ok"] and not machine.hourly("EQ999999")["ok"]
|
||||
fx = {
|
||||
"이름": got["이름"],
|
||||
"규격": got["규격"],
|
||||
"단위": "시간",
|
||||
"단가": {k: got[k] for k in ("노무비", "재료비", "경비")},
|
||||
"이음": {"노임": None, "자재": None, "기계": EQ},
|
||||
}
|
||||
sk = calc.calc_doc(
|
||||
{
|
||||
"키": "SK-000001",
|
||||
"이름": "시험",
|
||||
"단위": "시간",
|
||||
"바탕": None,
|
||||
"더한줄": [{"고정": fx, "이름": got["이름"], "단위": "시간", "수량": 1}],
|
||||
}
|
||||
)["1단위"]
|
||||
doc = labor.clean({"담은로직": [{"로직": "", "고정": fx, "수량": {"손": 1}}]})
|
||||
assert sk["계"] == pytest.approx(got["계"])
|
||||
assert labor.calc(doc, {})["반영합"]["계"] == pytest.approx(got["계"])
|
||||
find = _text(M02 / "M02_MasterTemplete_Find_Pick.ts")
|
||||
assert (
|
||||
"/logic-lib/machine-hourly?key=" in find
|
||||
and "이음: { 노임: null, 자재: null, 기계: e.코드 }" in find
|
||||
)
|
||||
assert "machine: fixed &&" in _text(M02 / "M02_MasterTemplete_Labor_Pick.ts")
|
||||
assert "machine: (e) => done({ 고정: fixedOf(e)" in _text(
|
||||
M02 / "M02_MasterTemplete_Sangeun_Pick.ts"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user