Merge remote-tracking branch 'origin/sub_laptop_6' into sub_laptop_5
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
대가 줄 = 부른 블록 합계(노무 · 재료 · 경비) · 산근 줄 = 145-1 산근 단가(`sangeun` {키: 값})
|
||||
비고 = 대가 줄 「대가n」 · 산근 줄 「산근 n호표」(산근 목록 번호)
|
||||
% 줄 = 밑수(그 블록 노무 줄 금액 합) × % / 100
|
||||
고정 줄(705-1) = 줄 단가(`own`) · 이음(`link`) 있으면 그 비목만 단가 책 값(노임 = 노무 · 자재 =
|
||||
재료(관급 · 별도 0원) · 기계 = 중기 세 비목) · 책에 없으면 줄 단가 그대로 · 빈칸 안 됨
|
||||
끝수 = 엑셀 자리 그대로 — 줄 금액 = ROUNDDOWN(단가 × 수량, 1) ·
|
||||
% 줄 = ROUNDDOWN(밑수 × % / 100, 1) · 관급 · 별도계상 자재 = 대가 안 0원 ·
|
||||
블록 비목 합 = ROUNDDOWN(Σ, 0) · 합계 = 세 비목 합.
|
||||
@@ -58,11 +60,31 @@ def _prices(line: dict[str, Any], books: dict[str, Any], sums: dict) -> dict[str
|
||||
) or {}
|
||||
for part in PARTS:
|
||||
out[part] = None if row.get("blank") else num(row.get(part)) # 빈 줄 대가 = 빈칸
|
||||
elif kind == "fixed":
|
||||
out = _fixed(line, books)
|
||||
else: # blank — 못 펼친 줄
|
||||
out = dict.fromkeys(PARTS, None)
|
||||
return out
|
||||
|
||||
|
||||
def _fixed(line: dict[str, Any], books: dict[str, Any]) -> dict[str, float | None]:
|
||||
"""고정 줄 단가(705-1) — 줄 단가 위에 이은 비목만 단가 책 값."""
|
||||
own, link = line.get("own") or {}, line.get("link") or {}
|
||||
out = {part: num(own.get(part)) or 0.0 for part in PARTS}
|
||||
labor = books["labor"].get(link.get("labor")) if link.get("labor") else None
|
||||
if labor and num(labor.get("price")) is not None:
|
||||
out["labor"] = num(labor["price"])
|
||||
row = books["material"].get(link.get("material")) if link.get("material") else None
|
||||
if row and (row.get("supply") in SUPPLY or num(row.get("price")) is not None):
|
||||
out["material"] = 0.0 if row.get("supply") in SUPPLY else num(row["price"])
|
||||
machine = books["machine"].get(link.get("machine")) if link.get("machine") else None
|
||||
if machine and not machine.get("blank"):
|
||||
for part in PARTS:
|
||||
if num(machine.get(part)) is not None:
|
||||
out[part] = num(machine[part])
|
||||
return out
|
||||
|
||||
|
||||
def _base(spec: str, lines: list[dict[str, Any]]) -> float | None:
|
||||
"""% 줄 밑수 — `part:비목` 그 비목 줄 금액 합 · `lines:labor` 노무 줄 금액 합 ·
|
||||
`line:이름` 그 줄."""
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
*
|
||||
* 노무 줄 = 노무비목록표 · 재료 줄 = 재료비목록표 · 중기 줄 = 중기목록표 · 대가 줄 = 부른 블록 합계 ·
|
||||
* 산근 줄 = 145-1 산근 단가 · % 줄 = 밑수(노무 줄 금액 합) × % / 100
|
||||
* 고정 줄(705-1) = 줄 단가(own) · 이음(link) 있으면 그 비목만 단가 책 값(노임 = 노무 · 자재 = 재료
|
||||
* (관급 · 별도 0원) · 기계 = 중기 세 비목) · 책에 없으면 줄 단가 그대로 · 빈칸 안 됨
|
||||
* 비고 = 대가 줄 「대가n」 · 산근 줄 「산근 n호표」(산근 목록 번호)
|
||||
* 줄 금액 ROUNDDOWN(단가 × 수량, 1) · 블록 비목 합 ROUNDDOWN(Σ, 0) · 합계 = 세 비목 합
|
||||
* 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락에서 셈)
|
||||
@@ -59,7 +61,25 @@ function linePrices(line: Row, books: Row, sums: Record<string, Row>): Parts {
|
||||
const src: Row = ({ machine: books.machine, daega: sums, sangeun: books.sangeun } as Row)[kind];
|
||||
const row: Row = src[key] || {};
|
||||
for (const part of PARTS) out[part] = row.blank ? null : num(row[part]); // 빈 줄 대가 = 빈칸
|
||||
} else return blanks(); // blank — 못 펼친 줄
|
||||
} else if (kind === "fixed") return fixedPrices(line, books);
|
||||
else return blanks(); // blank — 못 펼친 줄
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 고정 줄 단가(705-1) — 줄 단가 위에 이은 비목만 단가 책 값(짝 `_fixed`) */
|
||||
function fixedPrices(line: Row, books: Row): Parts {
|
||||
const own: Row = line.own || {};
|
||||
const link: Row = line.link || {};
|
||||
const out = zeros();
|
||||
for (const part of PARTS) out[part] = num(own[part]) ?? 0;
|
||||
const labor: Row | undefined = link.labor ? books.labor[link.labor] : undefined;
|
||||
if (labor && num(labor.price) !== null) out.labor = num(labor.price);
|
||||
const row: Row | undefined = link.material ? books.material[link.material] : undefined;
|
||||
const supplied = !!row && (SUPPLY as readonly string[]).includes(row.supply);
|
||||
if (row && (supplied || num(row.price) !== null)) out.material = supplied ? 0 : num(row.price);
|
||||
const machine: Row | undefined = link.machine ? books.machine[link.machine] : undefined;
|
||||
if (machine && !machine.blank)
|
||||
for (const part of PARTS) if (num(machine[part]) !== null) out[part] = num(machine[part]);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
— 조합이 수량만큼 부름 · 기계 = 기계경비 로직 줄 · 다른 돈 로직 줄 = 하위 산근
|
||||
산근 단가산출 줄(일위대가화 아닌 로직 줄) = 산근 호표 참조(키 = 145-1 `sangeun_key`)
|
||||
(155-3 · 사용자 뜻 = 일위대가화 줄 = 대가 · 단가산출 줄 = 산근 · 145 정함 뒤집음)
|
||||
고정 고정 줄(705-1) = `fixed` 줄 하나 · `own` = 줄 단가 · `link` = 이음 키(셈 짝이 단가 책 값)
|
||||
|
||||
같은 블록 = 같은 꼴 → 한 호표(id = 내용 해시 · 같은 원본 + 같은 셈 단가 = 하나) ·
|
||||
덮개 열쇠(702-2 · 내용 해시 밖 · 수량 · 단가가 바뀌어도 그대로) = 블록 `cover` + 줄 `cid` —
|
||||
@@ -34,6 +35,7 @@ from B09_Estimation import B09_Estimation_Sangeun as sg
|
||||
from B09_Estimation import B09_Estimation_Sangeun_Text as st
|
||||
from M02_MasterTemplete import M02_Item_Prices as item_prices
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete.M02_Item_Snapshot import (
|
||||
MANIFEST_NAME,
|
||||
@@ -194,6 +196,8 @@ class Builder:
|
||||
q = _f(qty)
|
||||
t = _f(times) or 1.0
|
||||
each = None if q is None else q / t
|
||||
if fixed.is_fixed(one):
|
||||
return fixed_line(one[fixed.FIXED], q)
|
||||
ref = one.get("참조")
|
||||
if isinstance(ref, dict):
|
||||
doc = copies.get(refs.ref_id(ref))
|
||||
@@ -324,6 +328,22 @@ class Builder:
|
||||
return self.precombo(whole, ref, doc, copies, 1)
|
||||
|
||||
|
||||
def fixed_line(fx: dict, qty: Any) -> dict[str, Any]:
|
||||
"""고정 줄(705-1) → 블록 줄 `fixed` — 수량 그대로(배 1) · 이음 없는 비목 = 줄 단가."""
|
||||
link = fx.get("이음") or {}
|
||||
return {
|
||||
"kind": "fixed",
|
||||
**{k: fx.get(v) or "" for k, v in (("name", "이름"), ("spec", "규격"), ("unit", "단위"))},
|
||||
"qty": _f(qty),
|
||||
"own": {PART[k]: _f(v) for k, v in (fx.get("단가") or {}).items() if k in PART},
|
||||
"link": {
|
||||
"labor": link.get("노임"),
|
||||
"material": link.get("자재"),
|
||||
"machine": link.get("기계"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _material(whole: Any, mat: dict, qty: Any) -> dict[str, Any]:
|
||||
"""자재 줄 — 조합 사용자 자재 · 사전조합 자재(175-2) 한 꼴 · 이름 · 규격 · 단위 = 사본 단가."""
|
||||
row = (whole.get(mat.get("코드")) if mat.get("코드") else None) or {}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
사본마다 `일위대가.json` 담은 로직을 그 사본 단가 묶음(`단가.json`) · 입력값 · 고른자재로 돌리며
|
||||
실제로 부른 자원 줄(인력 LB · 기계 EQ · 자재 MT · 유가 MO)만 모음 — 단가 묶음 통째
|
||||
(고르기 후보 전부)는 안 씀 · 값은 쓰지 않음(단가는 원가 한 벌 `price_book.json` 이 맡음) ·
|
||||
사본 일위대가 자재 · 굳힌 사전조합 자재 코드도(183-1 · 로직을 안 돌아 `get` 에 안 걸림).
|
||||
사본 일위대가 자재 · 굳힌 사전조합 자재 코드 · 고정 줄 이음 키도(183-1 · 705-1 · 로직을 안 돌아 `get` 에 안 걸림).
|
||||
고르기가 훑은 후보 줄도 걸림 → 원가 길이 두 번 셈해 쓰는 줄만 남김(`Summary.used_materials`).
|
||||
구조물 조합 사본 없으면 그 구조물은 없음 · 일위대가 없는 사본은 자원 없음.
|
||||
"""
|
||||
@@ -16,6 +16,7 @@ from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Item_Prices as item_prices
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete.M02_Item_Snapshot import (
|
||||
PICKS_FILE,
|
||||
VALUES_FILE,
|
||||
@@ -59,8 +60,10 @@ def used_keys(folder: Path) -> list[str]:
|
||||
labor_doc.run_line(
|
||||
snapshot, one["로직"], one.get("입력값"), one.get("켜쌓기율"), one.get("고름")
|
||||
)
|
||||
for mat in [*(doc.get("자재") or []), *(m for d in inner for m in d.get("자재") or [])]:
|
||||
code = str(mat.get("코드") or "") if isinstance(mat, dict) else ""
|
||||
mats = [*(doc.get("자재") or []), *(m for d in inner for m in d.get("자재") or [])]
|
||||
codes = [str(mat.get("코드") or "") if isinstance(mat, dict) else "" for mat in mats]
|
||||
codes += [k for d in [doc, *inner] for k in fixed.link_keys(d)] # 705-1 고정 줄 이음
|
||||
for code in codes:
|
||||
if code[:2] in RESOURCE_TABLES and code not in seen:
|
||||
seen.append(code)
|
||||
return seen
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
4 자재대 · 관급자재대 자재 줄 = 그 자재 그대로(호표 없음)
|
||||
연결 없는 항목(수량 · 호표 없음 · 호표 구성 없음 — 조합 밖 토공 · 운반 산근 등) = 전개 안 함 ·
|
||||
「연결 없음 n」(`unlinked`)
|
||||
고정 줄(705-1) = 이은 노임 · 자재 · 기계 키 자원으로 셈 · 이음 없는 줄 = 집계에서 빠짐 ·
|
||||
「고정 줄 n」(`fixed` · 빠진 줄 수)
|
||||
끝수 = 엑셀 자리 그대로 — 총수량 ROUND 2 · 금액 = ROUND(수량 × 단가, 0)(여기만 반올림) ·
|
||||
중기 = 노무 · 재료 · 경비 각 ROUND(시간 × 시간당, 0) · 합계 = 셋 합.
|
||||
단가 = 목록표(노무비 · 재료비 · 경비 · 중기목록표) — 없으면 금액 빈칸(수량은 둠).
|
||||
@@ -39,7 +41,7 @@ def expand(
|
||||
kind = line.get("kind")
|
||||
if kind == "hopyo":
|
||||
expand(lines_of, str(line.get("key")), qty * q, out, depth + 1)
|
||||
elif kind in KINDS:
|
||||
elif kind in (*KINDS, "fixed"):
|
||||
bucket = out.setdefault(kind, {})
|
||||
bucket[str(line.get("key"))] = bucket.get(str(line.get("key")), 0.0) + qty * q
|
||||
|
||||
@@ -123,6 +125,7 @@ def compute_summary(sheets: dict[str, Any], lines_of: dict[str, Any]) -> dict[st
|
||||
used, unlinked = totals(sheets, lines_of)
|
||||
return {
|
||||
"unlinked": unlinked,
|
||||
"fixed": len(used.get("fixed", {})),
|
||||
"sum_material": _resource_sheet(used.get("material", {}), _list(sheets["material"]), ""),
|
||||
"sum_labor": _resource_sheet(used.get("labor", {}), _list(sheets["labor"]), "인"),
|
||||
"sum_expense": _resource_sheet(used.get("expense", {}), _list(sheets["expense"]), ""),
|
||||
@@ -147,6 +150,9 @@ def block_lines(blocks: list[dict[str, Any]] | None) -> dict[str, Any]:
|
||||
for block in blocks or []:
|
||||
lines = []
|
||||
for line in block.get("lines") or []:
|
||||
if line.get("kind") == "fixed" and num(line.get("qty")) is not None:
|
||||
lines += _fixed_lines(block, line)
|
||||
continue
|
||||
kind = LINE_KIND.get(line.get("kind"))
|
||||
key = (line.get("ref") or {}).get("key")
|
||||
if kind and key and num(line.get("qty")) is not None:
|
||||
@@ -155,6 +161,16 @@ def block_lines(blocks: list[dict[str, Any]] | None) -> dict[str, Any]:
|
||||
return out
|
||||
|
||||
|
||||
def _fixed_lines(block: dict[str, Any], line: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""고정 줄(705-1) → 이은 자원 줄(기계 = 중기 하나) · 이음 없음 = `fixed` 줄(빠짐 셈)."""
|
||||
link = line.get("link") or {}
|
||||
pairs = [("machine", link.get("machine"))] if link.get("machine") else []
|
||||
pairs = pairs or [(k, link.get(k)) for k in ("labor", "material") if link.get(k)]
|
||||
if not pairs:
|
||||
pairs = [("fixed", f"{block['key']}|{line.get('cid') or line.get('name')}")]
|
||||
return [{"kind": k, "key": str(v), "qty": line["qty"]} for k, v in pairs]
|
||||
|
||||
|
||||
def used_materials(sheets: dict[str, Any], inputs: dict[str, Any], user: dict[str, Any]) -> set:
|
||||
"""재료비목록표에 남길 자재 키(183-1) — 대가 · 산근 · 중기 블록 재료 줄(수량 빈칸 · 0 도 =
|
||||
집계표 역전개와 같은 줄) · B08 총괄 줄(수량 있는 입력) · 관급 · 별도 표시 줄.
|
||||
@@ -167,6 +183,12 @@ def used_materials(sheets: dict[str, Any], inputs: dict[str, Any], user: dict[st
|
||||
for line in block.get("lines") or []
|
||||
if line.get("kind") == "material" and (line.get("ref") or {}).get("key")
|
||||
}
|
||||
keep |= { # 705-1 고정 줄이 이은 자재
|
||||
str(line["link"]["material"])
|
||||
for block in blocks
|
||||
for line in block.get("lines") or []
|
||||
if line.get("kind") == "fixed" and (line.get("link") or {}).get("material")
|
||||
}
|
||||
marks = user.get("materials") or {}
|
||||
for m in inputs.get("materials") or []:
|
||||
if "qty" in m or m.get("supply") or (marks.get(m["key"]) or {}).get("supply"):
|
||||
|
||||
@@ -100,7 +100,7 @@ export const buildDaegaBlocks: TabBuild = ({ sheets }): TabTable | string => {
|
||||
prices: { sum: ln.price, ...ln.prices },
|
||||
amounts: { sum: ln.amount, ...ln.amounts },
|
||||
note: ln.ref_no || ln.why || "",
|
||||
tag: HOPYO_TAG.pum,
|
||||
tag: ln.kind === "fixed" ? HOPYO_TAG.fix : HOPYO_TAG.pum, // 705-1 고정 줄
|
||||
})),
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*
|
||||
* 값 = 서버 단독 역전개(`B09_Estimation_Summary` · GET 응답 `summary`) — 조작 중엔 안 바뀜 · [저장] 뒤 정본
|
||||
* 끝수 = 총수량 ROUND 2 · 금액 ROUND(수량 × 단가, 0) · 줄 누름 = 단가를 낸 목록표 줄
|
||||
* 고정 줄(705-1) 이음 없는 줄 = 집계에서 빠짐 — 안내 「고정 줄 n」(서버 `summary.fixed`)
|
||||
* 경비 집계표 빈 까닭(166-5) = 산근 · 대가 어느 길도 경비 자원 줄을 안 냄(기계 → 중기 · 구역화물 → 자재)
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -22,9 +23,14 @@ import {
|
||||
const HINT = "줄을 누르면 목록표로 이동";
|
||||
|
||||
/** 안내 + 「빈 줄 n」(설계내역서와 같은 수 · 701-2 — 금액 모르는 줄은 집계에서 빠짐) */
|
||||
const hint = (sheets: Row, extra = ""): string => {
|
||||
const hint = (sheets: Row, payload: Row): string => {
|
||||
const n = blankRows(sheets).length;
|
||||
return `${HINT}${extra}${n ? ` · 빈 줄 ${n} — 집계에서 빠짐(좌측 데이터 점검)` : ""}`;
|
||||
const fixed = Number((payload.summary || {}).fixed) || 0;
|
||||
return [
|
||||
HINT,
|
||||
...(n ? [`빈 줄 ${n} — 집계에서 빠짐(좌측 데이터 점검)`] : []),
|
||||
...(fixed ? [`고정 줄 ${fixed} — 이은 단가 없어 집계에서 빠짐`] : []),
|
||||
].join(" · ");
|
||||
};
|
||||
|
||||
/** 줄이 없을 때 표 위 맨 앞 안내 */
|
||||
@@ -48,7 +54,7 @@ function resourceTab(id: string, list: TabId, unitPrice: string, none = ""): Tab
|
||||
rows,
|
||||
total: rows.length ? { name: "계", amount: sum } : null,
|
||||
frozen: 2,
|
||||
hint: !rows.length && none ? `${none} · ${hint(sheets)}` : hint(sheets),
|
||||
hint: !rows.length && none ? `${none} · ${hint(sheets, payload)}` : hint(sheets, payload),
|
||||
link: (i) => (rows[i] ? { tab: list, key: String(rows[i].key) } : null),
|
||||
};
|
||||
};
|
||||
@@ -86,7 +92,7 @@ export const buildSumMachine: TabBuild = ({ payload, sheets }): TabTable => {
|
||||
}
|
||||
: null,
|
||||
frozen: 2,
|
||||
hint: hint(sheets),
|
||||
hint: hint(sheets, payload),
|
||||
link: (i) => (rows[i] ? { tab: "machine", key: String(rows[i].key) } : null),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -159,6 +159,7 @@ def price_bundle(labor: dict[str, Any] | None, root: Path) -> dict[str, Any]:
|
||||
"""사본 단가 묶음(`단가.json`) — 담은 로직 · 굳힌 참조 로직 · 자재 · 고른 자재 · 사전조합 자재 ·
|
||||
개인 자재(`root` = 원본 층 · B09 설계내역서 서고 사본도 이 길 · 204-2)."""
|
||||
from M02_MasterTemplete import M02_Item_Prices as prices
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
logic_keys = [str(one.get("키")) for one in (labor or {}).get("로직", [])]
|
||||
@@ -169,6 +170,7 @@ def price_bundle(labor: dict[str, Any] | None, root: Path) -> dict[str, Any]:
|
||||
str(key) for one in doc.get("담은로직") or [] for key in (one.get("고름") or {}).values()
|
||||
]
|
||||
materials += refs.frozen_materials((labor or {}).get("참조사본") or {}) # 175-2 사전조합 자재
|
||||
materials += fixed.link_keys(doc) # 705-1 고정 줄이 잇는 노임 · 자재 · 기계
|
||||
wanted = logic_keys + [code for code in materials if code]
|
||||
bundle = prices.to_json(prices.collect(logic_keys, materials)) if wanted else {}
|
||||
if mine := own.bundle(
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
빈 칸 = 읽을 때 `MARK_DEFAULT`(`mark_of` · 저장은 비움 · 옛 「일반」 = 버림 → 사급 · 162-1).
|
||||
별도계상(222-1) = 그 자재 금액은 기준 · 합에서 빠지고 로직 `줄` 에서 지움(`drop_apart` · `settle` ·
|
||||
짝 `Labor_Calc.ts`) · `재료` · `자재`(자재목록 · B08 수량)엔 남음.
|
||||
고정 줄(705-1) = `{고정: {이름 · 규격 · 단위 · 단가 · 이음 · 출처}}` — 숫자 그대로 · 셈 `M02_Labor_Fixed`.
|
||||
% 덧줄(149-4) = 식 「X * Y / 100」 덧줄에 덧칸 `율`(Y) · `규격`(「노무비의 %」 등 · `pct_extras`) — 표기만 ·
|
||||
단위 · 수량 · 금액 그대로(「식 · 1」) · 화면이 율 · % 로 그림 · M01 시험계산 표도 이 함수.
|
||||
"""
|
||||
@@ -38,6 +39,7 @@ from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
@@ -101,6 +103,15 @@ def clean(doc: Any) -> dict[str, Any]:
|
||||
where = f"단가산출 로직 {at}째"
|
||||
if not isinstance(one, dict):
|
||||
raise _bad(f"{where} · 묶음 아님")
|
||||
if fixed.is_fixed(one) and not one.get("로직"):
|
||||
fx = fixed.clean(one[fixed.FIXED], where, whole)
|
||||
if fixed.fixed_id(fx) in seen:
|
||||
raise _bad(f"{where} · 같은 고정 줄을 두 번 담음 「{fx['이름']}」")
|
||||
seen.add(fixed.fixed_id(fx))
|
||||
memo, alias = one.get("메모"), _alias(one.get("별칭"), where)
|
||||
line = {"고정": fx, "별칭": alias, "메모": str(memo) if memo else None}
|
||||
lines.append({**line, "수량": _qty(one.get("수량"), where)})
|
||||
continue
|
||||
if one.get("참조") is not None and not one.get("로직"):
|
||||
here = here or refs.here()
|
||||
ref = refs.check_ref(one["참조"], where, here)
|
||||
@@ -407,7 +418,9 @@ def _is_ref(one: Any) -> bool:
|
||||
|
||||
|
||||
def line_id(one: dict) -> Any:
|
||||
"""줄 가름 — 로직 키 · 참조 줄은 `ref_id`(짝 `Labor_Calc.ts` `lineId`)."""
|
||||
"""줄 가름 — 로직 키 · 참조 줄은 `ref_id` · 고정 줄은 `fixed_id`(짝 `Labor_Calc.ts` `lineId`)."""
|
||||
if fixed.is_fixed(one):
|
||||
return fixed.fixed_id(one[fixed.FIXED])
|
||||
return refs.ref_id(one["참조"]) if _is_ref(one) else one.get("로직")
|
||||
|
||||
|
||||
@@ -493,6 +506,9 @@ def calc(
|
||||
if _is_ref(one):
|
||||
lines.append(ref_line(one["참조"], expand(one["참조"])))
|
||||
continue
|
||||
if fixed.is_fixed(one):
|
||||
lines.append(fixed.line(whole, one[fixed.FIXED]))
|
||||
continue
|
||||
code = one["로직"]
|
||||
pill = by_logic.get(code) or {}
|
||||
line = {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
"""M02 일위대가 고정 줄(PLAN 705-1 · 요구서 1장 · 4장 1 · 2 · 3) — 품 · 수량 · 단가 숫자를 그대로 적어 둔 줄.
|
||||
|
||||
줄 = `{고정: {이름, 규격, 단위, 단가: {노무비, 재료비, 경비}, 이음: {노임, 자재, 기계}, 출처?},
|
||||
별칭, 메모, 수량}` — 담은로직 안 로직 · 참조 줄과 섞임(줄 단위 꼬리표 · 기본 = 품셈).
|
||||
단가 = 그 줄 단위 1 개 값 · 빈 비목 = 0 · 합계만 있는 줄 = 고른 비목 하나(기본 재료비 · 붙이는 쪽 몫).
|
||||
이음 = 이 공사 노임(LB) · 자재(MT) · 기계(EQ) 키 · 못 이음 = null(「손 단가」) — 이은 비목만 단가 책 값
|
||||
(노임 새 판 때 따라감) · 품(수량)은 그대로.
|
||||
M02 셈(층 · 사본) = 노임 「값」 · 자재 기준단가로 이음 · 기계 = 줄 단가(기계 시간 단가 = B09 단가 책 몫).
|
||||
출처 = `{파일, 호표, 줄, 판}`(글 · 수만) · 줄 가름 = 「고정|이름|규격|단위」(짝 `Labor_Calc.ts` `lineId`).
|
||||
B09 대가 갈래 `fixed` = `B09_Estimation_Labor_Source.fixed_line` · 셈 `B09_Estimation_Calc_Labor`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
mf = m01.cm.mf
|
||||
mm = mf.mp.mm
|
||||
COSTS = mf.COST_ITEMS # 노무비 · 재료비 · 경비
|
||||
FIXED = "고정"
|
||||
LINKS = {"노임": "LB", "자재": "MT", "기계": "EQ"}
|
||||
SOURCE_KEYS = ("파일", "호표", "줄", "판")
|
||||
|
||||
|
||||
def _bad(text: str) -> Exception:
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
return store.StoreError(400, text)
|
||||
|
||||
|
||||
def _num(raw: Any) -> Decimal | None:
|
||||
if raw is None or isinstance(raw, bool):
|
||||
return None
|
||||
try:
|
||||
got = Decimal(str(raw).replace(",", "").strip())
|
||||
except InvalidOperation:
|
||||
return None
|
||||
return got if got.is_finite() else None
|
||||
|
||||
|
||||
def _plain(value: Decimal) -> int | float:
|
||||
return int(value) if value == value.to_integral_value() else float(value)
|
||||
|
||||
|
||||
def is_fixed(one: Any) -> bool:
|
||||
return isinstance(one, dict) and isinstance(one.get(FIXED), dict)
|
||||
|
||||
|
||||
def fixed_id(fx: dict) -> str:
|
||||
return f"고정|{fx.get('이름') or ''}|{fx.get('규격') or ''}|{fx.get('단위') or ''}"
|
||||
|
||||
|
||||
def clean(fx: Any, where: str, whole: Any) -> dict[str, Any]:
|
||||
"""고정 칸 정리 — 이름 없음 · 단가가 수 아님 · 이음 키 머리 틀림 · M01 에 없는 이음 = 400."""
|
||||
if not isinstance(fx, dict):
|
||||
raise _bad(f"{where} · 고정 줄이 묶음 아님")
|
||||
name = str(fx.get("이름") or "").strip()
|
||||
if not name:
|
||||
raise _bad(f"{where} · 고정 줄 이름 없음")
|
||||
raw = fx.get("단가") or {}
|
||||
if not isinstance(raw, dict):
|
||||
raise _bad(f"{where} · 고정 줄 단가는 {{노무비, 재료비, 경비}}")
|
||||
price = {}
|
||||
for k in COSTS:
|
||||
got = _num(raw.get(k))
|
||||
if got is None and raw.get(k) not in (None, ""):
|
||||
raise _bad(f"{where} · 고정 줄 {k} 단가가 수 아님")
|
||||
price[k] = _plain(got) if got is not None else 0
|
||||
given = fx.get("이음") or {}
|
||||
link = {}
|
||||
for k, head in LINKS.items():
|
||||
code = str(given.get(k) or "") if isinstance(given, dict) else ""
|
||||
if code and (code[:2] != head or not _has(whole, code)):
|
||||
raise _bad(f"{where} · 고정 줄 {k} 이음 「{code}」 M01 에 없음")
|
||||
link[k] = code or None
|
||||
src = fx.get("출처") if isinstance(fx.get("출처"), dict) else {}
|
||||
kept = {k: v for k in SOURCE_KEYS if isinstance(v := src.get(k), (str, int, float))}
|
||||
out = {"이름": name, "규격": str(fx.get("규격") or ""), "단위": str(fx.get("단위") or "")}
|
||||
return {**out, "단가": price, "이음": link, **({"출처": kept} if kept else {})}
|
||||
|
||||
|
||||
def _has(whole: Any, code: str) -> bool:
|
||||
try:
|
||||
return bool(whole.get(code))
|
||||
except mf.FormulaError:
|
||||
return False
|
||||
|
||||
|
||||
def link_keys(doc: Any) -> list[str]:
|
||||
"""문서 고정 줄이 잇는 키 — 사본 `단가.json` · B09 단가 책 수요(705-1)."""
|
||||
out: list[str] = []
|
||||
for one in (doc or {}).get("담은로직") or []:
|
||||
if is_fixed(one):
|
||||
out += [v for v in (one[FIXED].get("이음") or {}).values() if v and v not in out]
|
||||
return out
|
||||
|
||||
|
||||
def line(whole: Any, fx: dict) -> dict[str, Any]:
|
||||
"""고정 줄 계산 답 — 기준 = 줄 단가 · 이은 노임 · 자재는 `whole` 값(없으면 줄 단가 그대로) ·
|
||||
분모 = 줄 단위 · 배 1 · `이은` = 단가 책 값을 쓴 비목."""
|
||||
base = {k: _num((fx.get("단가") or {}).get(k)) or Decimal(0) for k in COSTS}
|
||||
link, used = fx.get("이음") or {}, []
|
||||
if code := link.get("노임"):
|
||||
try:
|
||||
got = _num((whole.get(code) or {}).get("값"))
|
||||
except mf.FormulaError:
|
||||
got = None
|
||||
if got is not None:
|
||||
base["노무비"], used = got, [*used, "노무비"]
|
||||
if code := link.get("자재"):
|
||||
try:
|
||||
got = _num(mm.element(whole, code, {}, fx.get("단위") or None)[0])
|
||||
except mf.FormulaError:
|
||||
got = None
|
||||
if got is not None:
|
||||
base["재료비"], used = got, [*used, "재료비"]
|
||||
# ponytail: 기계 이음은 M02 셈에서 줄 단가 그대로 — 기계 시간 단가 셈은 B09 중기목록표 몫
|
||||
money = {**base, "계": sum(base.values())}
|
||||
head = {FIXED: fx, "이름": fx.get("이름"), "알약": None, "분모": fx.get("단위"), "배": 1}
|
||||
return {**head, "기준": money, "이은": used, "까닭": None}
|
||||
@@ -5,7 +5,8 @@
|
||||
* 기준(결과단위 그대로)은 서버 calc 길이 셈 · 여기는 받은 기준에
|
||||
* 수량(손이 칸보다 앞섬 · 칸 = 알약 키 값) · 반영 = 기준 × 수량 ÷ 배 · 자재 = 재료비만.
|
||||
* 별도계상 자재(222-1 · 빈 칸 = 기본 별도계상) = 반영은 보이되 합에 안 더함(짝 `apart`).
|
||||
* 참조 줄(단가승격 · 사전조합 · 50-3)은 `lineId` = 「참조|층|주인|키」(짝 `M02_Labor_Ref.ref_id`). */
|
||||
* 참조 줄(단가승격 · 사전조합 · 50-3)은 `lineId` = 「참조|층|주인|키」(짝 `M02_Labor_Ref.ref_id`) ·
|
||||
* 고정 줄(705-1)은 「고정|이름|규격|단위」(짝 `M02_Labor_Fixed.fixed_id`) · 기준 = 서버가 낸 줄 단가. */
|
||||
|
||||
import type { CalcLine as LogicLine } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
|
||||
import type { LogicMat } from "./M02_Labor_LogicMats_Model";
|
||||
@@ -23,10 +24,23 @@ export interface LaborRef {
|
||||
키: string;
|
||||
}
|
||||
|
||||
/** 고정 줄(705-1) — 숫자 그대로 · 이음 = 이 공사 노임 · 자재 · 기계 키(못 이음 = null) */
|
||||
export interface LaborFixed {
|
||||
이름: string;
|
||||
규격?: string;
|
||||
단위?: string;
|
||||
단가: Record<"노무비" | "재료비" | "경비", number>;
|
||||
이음?: Record<"노임" | "자재" | "기계", string | null>;
|
||||
출처?: Record<string, string | number>;
|
||||
}
|
||||
|
||||
export interface CalcLine {
|
||||
/** 참조 줄은 서버 답에 없음(빈 글로 다룸) */
|
||||
로직: string;
|
||||
참조?: LaborRef;
|
||||
고정?: LaborFixed;
|
||||
/** 고정 줄 — 단가 책 값을 쓴 비목 */
|
||||
이은?: string[];
|
||||
알약?: string | null;
|
||||
분모?: string | null;
|
||||
배: number;
|
||||
@@ -79,7 +93,7 @@ export interface LaborCalc {
|
||||
|
||||
/** 수량만 보는 문서 조각 — 담은 로직 · 자재의 손 값 */
|
||||
export interface LaborQtyDoc {
|
||||
담은로직?: { 로직?: string; 참조?: LaborRef; 수량?: Qty }[];
|
||||
담은로직?: { 로직?: string; 참조?: LaborRef; 고정?: LaborFixed; 수량?: Qty }[];
|
||||
자재?: { 코드: string; 수량?: Qty; 별도계상?: string }[];
|
||||
}
|
||||
|
||||
@@ -89,8 +103,15 @@ function num(raw: unknown): number | null {
|
||||
return String(raw).trim() !== "" && Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
/** 줄 가름 — 로직 키 · 참조 줄은 「참조|층|주인|키」(짝 `M02_Labor_Doc.line_id`) */
|
||||
export function lineId(one: { 로직?: string; 참조?: LaborRef }): string | undefined {
|
||||
/** 줄 가름 — 로직 키 · 참조 줄은 「참조|층|주인|키」 · 고정 줄은 「고정|이름|규격|단위」
|
||||
* (짝 `M02_Labor_Doc.line_id`) */
|
||||
export function lineId(one: {
|
||||
로직?: string;
|
||||
참조?: LaborRef;
|
||||
고정?: LaborFixed;
|
||||
}): string | undefined {
|
||||
const fx = one.고정;
|
||||
if (fx) return `고정|${fx.이름 ?? ""}|${fx.규격 ?? ""}|${fx.단위 ?? ""}`;
|
||||
if (one.참조) return `참조|${one.참조.층}|${one.참조.주인 ?? ""}|${one.참조.키}`;
|
||||
return one.로직;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ def test_GET_응답에_집계표(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(router_mod, "material_sheets", fake_materials)
|
||||
body = asyncio.run(router_mod.payload("p", tmp_path))
|
||||
assert set(body["summary"]) == {
|
||||
"sum_material", "sum_labor", "sum_expense", "sum_machine", "unlinked",
|
||||
"sum_material", "sum_labor", "sum_expense", "sum_machine", "unlinked", "fixed", # 705-1 고정 줄 빠짐 수
|
||||
} # fmt: skip
|
||||
assert body["summary"]["sum_material"] == [] # 「별도」 · 「관급」 표시 전엔 내역 자재 줄 없음
|
||||
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
"""705-1 고정 줄 갈래 — 품 · 수량 · 단가 숫자를 그대로 적어 둔 일위대가 줄(요구서 1장 · 4장 1 · 2 · 3).
|
||||
|
||||
- M02 일위대가 줄 `{고정: {이름 · 규격 · 단위 · 단가 · 이음 · 출처}}` — 정리 · 셈 · 거울(`settle` 파이썬 ↔ TS)
|
||||
- B09 대가 줄 `fixed` — 이음 있으면 이 공사 단가 책 값 · 없으면 줄 단가 · 덮개 · 거울(Calc_Labor 파이썬 ↔ TS)
|
||||
- 노임 새 판 = 이은 비목만 따라감 · 품(수량) 그대로 · 원가계산서 빈 줄 0 · 집계표 이음 · 빠짐 수
|
||||
견본 = `test_145_2_b09_labor_calc`(엑셀 바탕 표 · 사본 꼴) · `test_m02_labor_calc_mirror`(TS 헬퍼).
|
||||
"""
|
||||
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor, daega_price # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Linked import linked_resource_keys # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Summary import compute_summary, hopyo_lines # noqa: E402
|
||||
from M02_MasterTemplete import M02_Item_Prices as item_prices # noqa: E402
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor # noqa: E402
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed # noqa: E402
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402
|
||||
from M02_MasterTemplete import M02_Item_Snapshot as snapshots # noqa: E402
|
||||
|
||||
|
||||
def _load(name):
|
||||
spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(f"{name}.py"))
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
b145 = _load("test_145_2_b09_labor_calc")
|
||||
m02 = _load("test_m02_labor_calc_mirror")
|
||||
|
||||
LB = "LB000033" # 메쌓기 인력 줄 직종(견본 사본 · M01 에 있음)
|
||||
|
||||
|
||||
def _fx(name, price, link=None, unit="인"):
|
||||
return {"이름": name, "규격": "", "단위": unit, "단가": price, "이음": link or {}}
|
||||
|
||||
|
||||
# ── M02 일위대가 줄 ────────────────────────────────────────────────────────
|
||||
def test_M02_정리_꼴과_막힘():
|
||||
doc = labor.clean(
|
||||
{
|
||||
"담은로직": [
|
||||
{"로직": m02.TEN, "수량": None},
|
||||
{
|
||||
"고정": {
|
||||
**_fx("보통인부", {"노무비": "1,000"}, {"노임": LB}),
|
||||
"출처": {"파일": "a.xlsx", "호표": 3, "몰래": 1},
|
||||
},
|
||||
"수량": {"손": 0.5},
|
||||
},
|
||||
]
|
||||
}
|
||||
)
|
||||
line = doc["담은로직"][1]
|
||||
assert line["고정"] == {
|
||||
"이름": "보통인부", "규격": "", "단위": "인",
|
||||
"단가": {"노무비": 1000, "재료비": 0, "경비": 0},
|
||||
"이음": {"노임": LB, "자재": None, "기계": None},
|
||||
"출처": {"파일": "a.xlsx", "호표": 3},
|
||||
} # fmt: skip
|
||||
assert line["수량"] == {"손": 0.5} and labor.line_id(line) == "고정|보통인부||인"
|
||||
bad = [
|
||||
[{"고정": _fx("", {})}], # 이름 없음
|
||||
[{"고정": _fx("a", {"노무비": "글"})}], # 단가가 수 아님
|
||||
[{"고정": _fx("a", {}, {"노임": "MT000008"})}], # 이음 머리 틀림
|
||||
[{"고정": _fx("a", {}, {"자재": "MT999999"})}], # M01 에 없음
|
||||
[{"고정": _fx("a", {})}, {"고정": _fx("a", {"재료비": 1})}], # 같은 줄 두 번
|
||||
]
|
||||
for lines in bad:
|
||||
with pytest.raises(store.StoreError) as e:
|
||||
labor.clean({"담은로직": lines})
|
||||
assert e.value.status == 400
|
||||
|
||||
|
||||
def _mixed_doc():
|
||||
"""섞인 호표 — 로직 줄 셋 + 고정 줄(이은 노임 · 이은 자재 · 손 단가) + 자재."""
|
||||
return labor.clean(
|
||||
{
|
||||
"열": "rv",
|
||||
"담은로직": [
|
||||
{"로직": m02.TEN, "수량": None},
|
||||
{"고정": _fx("석공", {"노무비": 1}, {"노임": LB}), "수량": {"손": 0.3}},
|
||||
{"로직": m02.THOUSAND, "수량": {"손": 4}},
|
||||
{
|
||||
"고정": _fx("자갈", {"재료비": 1}, {"자재": m02.MAT}, unit=""),
|
||||
"수량": {"손": 2},
|
||||
},
|
||||
{"고정": _fx("잡재료", {"재료비": 1234.5}, unit="식"), "수량": None},
|
||||
{"로직": m02.PLAIN, "수량": None},
|
||||
],
|
||||
"자재": [{"코드": m02.MAT, "수량": None}],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"values",
|
||||
[{}, {"사석 수상고르기|㎡": 2.5, f"자재|{m02.MAT}": "3"}, {"나무식재|본": "1,200"}],
|
||||
)
|
||||
def test_M02_섞인_호표_파이썬과_TS_가_같은_답(values):
|
||||
doc = _mixed_doc()
|
||||
answer = labor.calc(doc, {})
|
||||
py = labor.settle(answer, doc, values)
|
||||
m02._same(py, m02._ts(answer, doc, values))
|
||||
assert py == labor.calc(doc, values)
|
||||
stone, gravel, misc = (ln for ln in py["줄"] if "고정" in ln)
|
||||
whole = labor.m01.loaded()[1]
|
||||
assert stone["기준"]["노무비"] == whole.get(LB)["값"] and stone["이은"] == ["노무비"]
|
||||
assert stone["반영"]["노무비"] == pytest.approx(stone["기준"]["노무비"] * 0.3)
|
||||
assert gravel["이은"] == ["재료비"] and gravel["기준"]["재료비"] != 1
|
||||
assert misc["기준"] == {"노무비": 0, "재료비": 1234.5, "경비": 0, "계": 1234.5}
|
||||
assert (
|
||||
misc["이은"] == [] and misc["수량"] is None and misc["반영"] is None
|
||||
) # 손 단가 · 수량 없음
|
||||
|
||||
|
||||
def test_M02_사본_셈_이음_단가_묶음(tmp_path):
|
||||
"""사본 `단가.json` 에 이은 키가 실림(price_bundle) · 사본 셈 = 층 셈."""
|
||||
from M02_MasterTemplete.M02_Item_Source import price_bundle
|
||||
|
||||
doc = _mixed_doc()
|
||||
bundle = price_bundle({"로직": [], "줄": doc}, tmp_path)
|
||||
got = item_prices.load_prices(bundle)
|
||||
assert got.get(LB)["값"] == labor.m01.loaded()[1].get(LB)["값"]
|
||||
snap = item_prices.calc({"줄": doc}, bundle, {}, {})
|
||||
fixed_lines = [ln for ln in snap["줄"] if "고정" in ln]
|
||||
assert [ln["이은"] for ln in fixed_lines] == [["노무비"], ["재료비"], []]
|
||||
|
||||
|
||||
# ── B09 대가 셈(바탕 표 = 엑셀 견본) ──────────────────────────────────────────
|
||||
def _fixed_blocks():
|
||||
"""조합 블록 — 노무 줄 + 고정 줄 넷(이은 노임 · 이은 관급 자재 · 이은 중기 · 손 단가)."""
|
||||
own = {"labor": 100.0, "material": 200.0, "expense": 300.0}
|
||||
lines = [
|
||||
{"kind": "labor", "key": "L00017", "name": "보통인부", "qty": 0.1},
|
||||
{"kind": "fixed", "name": "석공", "qty": 0.2, "own": own,
|
||||
"link": {"labor": "L00005", "material": None, "machine": None}},
|
||||
{"kind": "fixed", "name": "파형강관", "qty": 1, "own": own,
|
||||
"link": {"labor": None, "material": "M00222", "machine": None}},
|
||||
{"kind": "fixed", "name": "굴착기", "qty": 0.5, "own": own,
|
||||
"link": {"labor": None, "material": None, "machine": "X00044"}},
|
||||
{"kind": "fixed", "name": "잡재료", "qty": 2, "own": {"material": 1234.5},
|
||||
"link": {"labor": "L99999", "material": None, "machine": None}}, # 책에 없음 = 손 단가
|
||||
] # fmt: skip
|
||||
for n, ln in enumerate(lines):
|
||||
ln["cid"] = f"{ln['kind']}:{ln.get('key') or ln['name']}#0"
|
||||
ln["n"] = n
|
||||
block = {"id": "DGF", "cover": "S:f", "kind": "조합", "name": "섞인", "spec": "", "unit": "m",
|
||||
"cite": "", "origin": {}, "lines": lines} # fmt: skip
|
||||
return {"blocks": [block], "owners": {"pipe@100.0": "DGF"}}
|
||||
|
||||
|
||||
def _line(out, name):
|
||||
return next(ln for ln in out["daega_blocks"][0]["lines"] if ln["name"] == name)
|
||||
|
||||
|
||||
def test_B09_고정_줄_단가_이음과_손_단가():
|
||||
out = compute_labor(_fixed_blocks(), b145.SHEETS, {})
|
||||
assert _line(out, "석공")["prices"] == {"labor": 258935, "material": 200, "expense": 300}
|
||||
assert _line(out, "파형강관")["prices"]["material"] == 0 # 관급 = 대가 안 0원
|
||||
assert _line(out, "굴착기")["prices"] == {"labor": 47231, "material": 7784, "expense": 9994}
|
||||
misc = _line(out, "잡재료")
|
||||
assert misc["prices"] == {"labor": 0, "material": 1234.5, "expense": 0}
|
||||
assert misc["amounts"]["material"] == 2469.0
|
||||
block = out["daega_blocks"][0]
|
||||
assert block["blank"] == 0 and out["gaps"] == [] # 빈칸 규칙에 안 걸림
|
||||
assert daega_price(out, "pipe@100.0")["sum"] == block["sum"] is not None
|
||||
|
||||
|
||||
def test_B09_노임_새_판_이은_단가만_따라감():
|
||||
old = compute_labor(_fixed_blocks(), b145.SHEETS, {})
|
||||
sheets = copy.deepcopy(b145.SHEETS)
|
||||
for row in sheets["labor"]:
|
||||
row["price"] = round(row["price"] * 1.05)
|
||||
new = compute_labor(_fixed_blocks(), sheets, {})
|
||||
assert _line(new, "석공")["prices"]["labor"] == round(258935 * 1.05)
|
||||
assert _line(new, "석공")["qty"] == _line(old, "석공")["qty"] == 0.2 # 품 그대로
|
||||
for name in ("파형강관", "굴착기", "잡재료"):
|
||||
assert _line(new, name)["prices"] == _line(old, name)["prices"]
|
||||
|
||||
|
||||
def test_B09_덮개도_고정_줄에_먹음():
|
||||
cover = {"S:f": {"fixed:잡재료#0": {"qty": 3, "material": 1000}}}
|
||||
out = compute_labor(_fixed_blocks(), b145.SHEETS, {}, cover)
|
||||
misc = _line(out, "잡재료")
|
||||
assert misc["qty"] == 3 and misc["prices"]["material"] == 1000
|
||||
assert misc["base"] == {"qty": 2, "labor": 0, "material": 1234.5, "expense": 0}
|
||||
assert out["daega_orphans"] == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cover", [None, {"S:f": {"fixed:석공#0": {"labor": 1}}}])
|
||||
def test_B09_거울_파이썬_TS(cover):
|
||||
daega = _fixed_blocks()
|
||||
py = json.loads(json.dumps(compute_labor(daega, b145.SHEETS, {}, cover), ensure_ascii=False))
|
||||
ts = b145._ts(daega, b145.SHEETS, {}, cover)
|
||||
assert b145._same(py, ts["out"])
|
||||
assert b145._same(daega_price(py, "pipe@100.0"), ts["price"])
|
||||
|
||||
|
||||
# ── 사본 → 대가 블록 → 내역 · 원가 · 집계표 한 바퀴 ─────────────────────────────
|
||||
def _snap(root, owner, key, lines):
|
||||
folder = snapshots.structures_dir(root) / snapshots.folder_name(owner)
|
||||
folder.mkdir(parents=True)
|
||||
|
||||
def write(name, data):
|
||||
(folder / name).write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
doc = labor.clean({"단위": "m", "담은로직": lines})
|
||||
codes = [one["로직"] for one in lines if "로직" in one]
|
||||
write(snapshots.MANIFEST_NAME, {"원본": {**b145._item(key), "경로": ["기슭막이", key]}})
|
||||
write("단가.json", item_prices.to_json(item_prices.collect(codes, fixed.link_keys(doc))))
|
||||
write("일위대가.json", {"줄": doc})
|
||||
write("입력값.json", b145._values(b145.STONE))
|
||||
|
||||
|
||||
def test_한_바퀴_사본_고정_줄_원가_빈칸_0_집계표(tmp_path):
|
||||
from B09_Estimation.B09_Estimation_Router import compute_sheets
|
||||
|
||||
root = b145._project(tmp_path / "loop", [(100, "SS000901")])
|
||||
lines = [
|
||||
{"로직": "GF000219", "승격": True, "수량": {"손": 1}}, # 품셈 줄(메쌓기)
|
||||
{"고정": _fx("석공", {"노무비": 1}, {"노임": LB}), "수량": {"손": 0.5}}, # 이은 노임
|
||||
{"고정": _fx("잡재료", {"재료비": 1000}, unit="식"), "수량": {"손": 1}}, # 손 단가
|
||||
]
|
||||
_snap(root, "pipe@100.0", "SS000901", lines)
|
||||
keys = linked_resource_keys(root)
|
||||
assert LB in keys # 단가 책 수요에 이음 키
|
||||
inputs = prices.build_inputs(None, keys)
|
||||
inputs["daega"] = daega_inputs(root)
|
||||
inputs["hopyo"] = {}
|
||||
inputs["bill"] = {
|
||||
"items": [
|
||||
{"key": "st:revet", "section": "2.1", "name": "돌기슭막이", "spec": "", "unit": "m",
|
||||
"qty": 10, "ref": {"tab": "dg_list", "key": "revet"}, "note": "",
|
||||
"owners": {"pipe@100.0": 10}},
|
||||
]
|
||||
} # fmt: skip
|
||||
sheets = compute_sheets(root, inputs, {})
|
||||
combo = next(b for b in sheets["daega_blocks"] if b["kind"] == "조합")
|
||||
kinds = [ln["kind"] for ln in combo["lines"]]
|
||||
assert kinds == ["daega", "fixed", "fixed"] and combo["blank"] == 0
|
||||
stone = combo["lines"][1]
|
||||
book = {r["key"]: r for r in sheets["labor"]}
|
||||
assert stone["prices"]["labor"] == book[LB]["price"] is not None
|
||||
assert sheets["bill"]["totals"]["direct"]["blank"] == 0 # 원가계산서 빈 줄 0
|
||||
assert sheets["bill"]["sums"]["material"] > 0
|
||||
summary = compute_summary(sheets, hopyo_lines(sheets))
|
||||
labor_qty = {r["key"]: r["qty"] for r in summary["sum_labor"]}
|
||||
assert labor_qty[LB] >= 10 * 0.5 # 이은 고정 줄 = 노임 자원으로 셈
|
||||
assert summary["fixed"] == 1 # 손 단가 줄 = 집계에서 빠짐 1
|
||||
|
||||
|
||||
def test_사본_재적용_업데이트_고정_줄_그대로(tmp_path):
|
||||
"""[업데이트](703-3) = 단가 책만 새로 — 고정 줄 품 · 손 단가 그대로 · 이은 단가만 새 판."""
|
||||
root = b145._project(tmp_path / "upd", [(100, "SS000901")])
|
||||
lines = [
|
||||
{"고정": _fx("석공", {"노무비": 1}, {"노임": LB}), "수량": {"손": 0.5}},
|
||||
{"고정": _fx("잡재료", {"재료비": 1000}, unit="식"), "수량": {"손": 1}},
|
||||
]
|
||||
_snap(root, "pipe@100.0", "SS000901", lines)
|
||||
daega = daega_inputs(root)
|
||||
book = prices.build_inputs(None, linked_resource_keys(root))
|
||||
rows = [{**r, "price": 100} if r["key"] == LB else r for r in book["labor"]]
|
||||
first = compute_labor(daega, {**b145.SHEETS, "labor": rows}, {})
|
||||
again = daega_inputs(root) # 사본 다시 읽음(재적용 뒤 꼴 = 같은 원본)
|
||||
second = compute_labor(again, {**b145.SHEETS, "labor": [{**r, "price": 200} for r in rows]}, {})
|
||||
a, b = first["daega_blocks"][0]["lines"], second["daega_blocks"][0]["lines"]
|
||||
assert [ln["qty"] for ln in a] == [ln["qty"] for ln in b] == [0.5, 1]
|
||||
assert (a[0]["prices"]["labor"], b[0]["prices"]["labor"]) == (100, 200)
|
||||
assert a[1]["prices"] == b[1]["prices"] == {"labor": 0, "material": 1000, "expense": 0}
|
||||
Reference in New Issue
Block a user