feat(b09): 재료비목록표 수량 · 총액 열 — B08 총괄 합계 × 적용 단가 ROUND 0 · 공제 줄 거름 · MT 합침(166-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015sDZSjFvoiDjdGpgj3muc3
This commit is contained in:
@@ -10,6 +10,8 @@
|
|||||||
(분자 · 분모 = M01 CC000336 1/8 × 16/12 × 25/20 — 서버가 넣어 줌)
|
(분자 · 분모 = M01 CC000336 1/8 × 16/12 × 25/20 — 서버가 넣어 줌)
|
||||||
자재단가대비표 = 출처 단가 중 최저(0 · 빈칸 뺌) 자동 · 사용자 적용 단가가 있으면 그 값
|
자재단가대비표 = 출처 단가 중 최저(0 · 빈칸 뺌) 자동 · 사용자 적용 단가가 있으면 그 값
|
||||||
· 관급 · 별도계상 = 사용자 표시 → 재료비목록표 단가 = 적용 단가
|
· 관급 · 별도계상 = 사용자 표시 → 재료비목록표 단가 = 적용 단가
|
||||||
|
재료비목록표 = 수량(B08 총괄자재수량집계표 합계 열) × 적용 단가 = 총액 ROUND(·, 0)
|
||||||
|
(엑셀 재료비수량금액집계표 G 열 자리 · 수량 · 단가 하나라도 빈칸이면 빈칸)
|
||||||
중기사용료 = 손료(경비 · 기준가 천원 × 손료계수 ÷ 10,000) + 운전사(노무 · 인 × 시간 노임)
|
중기사용료 = 손료(경비 · 기준가 천원 × 손료계수 ÷ 10,000) + 운전사(노무 · 인 × 시간 노임)
|
||||||
+ 연료(재료 · ℓ/hr × 적용 단가) + 잡품(재료 · 연료 금액 × % ÷ 100)
|
+ 연료(재료 · ℓ/hr × 적용 단가) + 잡품(재료 · 연료 금액 × % ÷ 100)
|
||||||
줄 = ROUNDDOWN(단가 × 수량, 1) · 비목 합 = ROUNDDOWN(Σ, 0) → 중기목록표
|
줄 = ROUNDDOWN(단가 × 수량, 1) · 비목 합 = ROUNDDOWN(Σ, 0) → 중기목록표
|
||||||
@@ -163,15 +165,21 @@ def compare_sheet(
|
|||||||
"applied": applied,
|
"applied": applied,
|
||||||
"applied_from": applied_from if applied is not None else None,
|
"applied_from": applied_from if applied is not None else None,
|
||||||
"supply": supply,
|
"supply": supply,
|
||||||
|
"qty": num(m.get("qty")),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def material_sheet(compare: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
def material_sheet(compare: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
"""재료비목록표 — 단가 = 대비표 적용 단가."""
|
"""재료비목록표 — 단가 = 대비표 적용 단가 · 총액 = ROUND(수량 × 단가, 0)."""
|
||||||
keep = ("no", "key", "name", "spec", "unit", "supply", "origin")
|
keep = ("no", "key", "name", "spec", "unit", "supply", "origin", "qty")
|
||||||
return [{**{k: row[k] for k in keep}, "price": row["applied"]} for row in compare]
|
rows = []
|
||||||
|
for row in compare:
|
||||||
|
price, q = row["applied"], row["qty"]
|
||||||
|
amount = None if price is None or q is None else round_half(q * price, 0)
|
||||||
|
rows.append({**{k: row[k] for k in keep}, "price": price, "amount": amount})
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def _line(name, spec, qty, unit, kind, price, ref, pct=False) -> dict[str, Any]:
|
def _line(name, spec, qty, unit, kind, price, ref, pct=False) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
*
|
*
|
||||||
* 노무비 · 경비목록표 = 굳힌 단가 · 환율및기초자료 = 운전사 시간 노임 ROUNDDOWN(일당 × 분자 ÷ 분모, 0)
|
* 노무비 · 경비목록표 = 굳힌 단가 · 환율및기초자료 = 운전사 시간 노임 ROUNDDOWN(일당 × 분자 ÷ 분모, 0)
|
||||||
* 자재단가대비표 = 출처 중 최저 자동 · 사용자 적용 단가 앞섬 · 관급 · 별도 표시 → 재료비목록표
|
* 자재단가대비표 = 출처 중 최저 자동 · 사용자 적용 단가 앞섬 · 관급 · 별도 표시 → 재료비목록표
|
||||||
|
* 재료비목록표 = 수량(B08 총괄 합계 열) × 적용 단가 = 총액 ROUND(·, 0) · 하나라도 빈칸이면 빈칸
|
||||||
* 중기사용료 = 손료 + 운전사 + 연료 + 잡품 · 줄 ROUNDDOWN 1 · 비목 합 ROUNDDOWN 0 → 중기목록표
|
* 중기사용료 = 손료 + 운전사 + 연료 + 잡품 · 줄 ROUNDDOWN 1 · 비목 합 ROUNDDOWN 0 → 중기목록표
|
||||||
* 빈칸 = 원천이 없으면 null · 그 칸을 쓰는 합도 null · 「연결 없음」 목록(gaps)에 남김
|
* 빈칸 = 원천이 없으면 null · 그 칸을 쓰는 합도 null · 「연결 없음」 목록(gaps)에 남김
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
@@ -138,16 +139,22 @@ function compareSheet(materials: Row[], user: Row, gaps: Gap[]): Row[] {
|
|||||||
applied,
|
applied,
|
||||||
applied_from: applied !== null ? appliedFrom : null,
|
applied_from: applied !== null ? appliedFrom : null,
|
||||||
supply,
|
supply,
|
||||||
|
qty: num(m.qty),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function materialSheet(compare: Row[]): Row[] {
|
function materialSheet(compare: Row[]): Row[] {
|
||||||
const keep = ["no", "key", "name", "spec", "unit", "supply", "origin"];
|
const keep = ["no", "key", "name", "spec", "unit", "supply", "origin", "qty"];
|
||||||
return compare.map((row) => ({
|
return compare.map((row) => {
|
||||||
...Object.fromEntries(keep.map((k) => [k, row[k]])),
|
const price: Num = row.applied;
|
||||||
price: row.applied,
|
const q: Num = row.qty;
|
||||||
}));
|
return {
|
||||||
|
...Object.fromEntries(keep.map((k) => [k, row[k]])),
|
||||||
|
price,
|
||||||
|
amount: price === null || q === null ? null : roundHalf(q * price, 0),
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function line(
|
function line(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
연결된 사본이 부른 자원 키(`B09_Estimation_Linked` — 143-1 「연결된 사본」 원천 · 없으면 빈 표)
|
연결된 사본이 부른 자원 키(`B09_Estimation_Linked` — 143-1 「연결된 사본」 원천 · 없으면 빈 표)
|
||||||
+ 기계가 부르는 운전사(조종원직종 → 인력 원문번호) · 연료(주연료종류 → 유가 「종류:전국평균」)
|
+ 기계가 부르는 운전사(조종원직종 → 인력 원문번호) · 연료(주연료종류 → 유가 「종류:전국평균」)
|
||||||
+ B08 총괄자재수량집계표 자재(M01 짝 없음 → 출처 빈칸 · 적용 단가는 사용자 입력)
|
+ B08 총괄자재수량집계표 자재(M01 짝 없음 → 출처 빈칸 · 적용 단가는 사용자 입력)
|
||||||
|
· 수량 = 합계 열 · 채집석 공제 줄 거름 · 키가 MT 코드(맞춤 없는 사본 자재)면 사본 자원 줄에 수량만 얹음
|
||||||
운전사 시간 계수 = M01 CC000336(시간환산 · 상여계수 · 휴지계수 분자 · 분모 곱).
|
운전사 시간 계수 = M01 CC000336(시간환산 · 상여계수 · 휴지계수 분자 · 분모 곱).
|
||||||
연료 지역 = 전국평균(M01 기계경비 로직 지역 첫 값) — 다른 지역 값은 자재 적용 단가를 고쳐 씀.
|
연료 지역 = 전국평균(M01 기계경비 로직 지역 첫 값) — 다른 지역 값은 자재 적용 단가를 고쳐 씀.
|
||||||
"""
|
"""
|
||||||
@@ -17,6 +18,7 @@ from datetime import datetime
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from B08_Quantity.B08_Quantity_Material_Calc import DEDUCT_KEY
|
||||||
from M01_MasterData import M01_MasterData_Store as m01
|
from M01_MasterData import M01_MasterData_Store as m01
|
||||||
|
|
||||||
#: 운전사 시간 노임 계수 표(건설품셈 공통 8-1-3 · 건협기술-2230)
|
#: 운전사 시간 노임 계수 표(건설품셈 공통 8-1-3 · 건협기술-2230)
|
||||||
@@ -212,7 +214,7 @@ def b08_material_rows(b08_sheets: dict[str, Any] | None) -> list[dict[str, Any]]
|
|||||||
rows = ((b08_sheets or {}).get("material") or {}).get("rows") or []
|
rows = ((b08_sheets or {}).get("material") or {}).get("rows") or []
|
||||||
out = []
|
out = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if row.get("amount") is None:
|
if row.get("amount") is None or row["key"] == DEDUCT_KEY:
|
||||||
continue
|
continue
|
||||||
out.append(
|
out.append(
|
||||||
{
|
{
|
||||||
@@ -244,10 +246,18 @@ def build_inputs(
|
|||||||
return {**_convert(master, key), "fresh": True}
|
return {**_convert(master, key), "fresh": True}
|
||||||
|
|
||||||
factor = (book or {}).get("driver_factor") if book else driver_factor(master)
|
factor = (book or {}).get("driver_factor") if book else driver_factor(master)
|
||||||
|
materials = [pick(k) for k in want["materials"]]
|
||||||
|
at = {row["key"]: i for i, row in enumerate(materials)}
|
||||||
|
for row in b08_material_rows(b08_sheets):
|
||||||
|
same = at.get(row["key"].removeprefix("b08:"))
|
||||||
|
if same is None:
|
||||||
|
materials.append(row)
|
||||||
|
else: # 같은 MT 코드 = 같은 자재 — 두 줄 안 만들고 수량만
|
||||||
|
materials[same] = {**materials[same], "qty": row["qty"]}
|
||||||
return {
|
return {
|
||||||
"labor": [pick(k) for k in want["labor"]],
|
"labor": [pick(k) for k in want["labor"]],
|
||||||
"machines": [pick(k) for k in want["machines"]],
|
"machines": [pick(k) for k in want["machines"]],
|
||||||
"materials": [pick(k) for k in want["materials"]] + b08_material_rows(b08_sheets),
|
"materials": materials,
|
||||||
"fuel_keys": want["fuel_keys"],
|
"fuel_keys": want["fuel_keys"],
|
||||||
"driver_factor": factor,
|
"driver_factor": factor,
|
||||||
"fx": (book or {}).get("fx") if book else fx_inputs(master),
|
"fx": (book or {}).get("fx") if book else fx_inputs(master),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* B09_Estimation_UI_Tabs_Base.ts
|
* B09_Estimation_UI_Tabs_Base.ts
|
||||||
* B09 바탕 단가 탭 일곱(PLAN 144-2~4) — 노무비 · 경비목록표 · 환율및기초자료 · 자재단가대비표 ·
|
* B09 바탕 단가 탭 일곱(PLAN 144-2~4) — 노무비 · 경비목록표 · 환율및기초자료 · 자재단가대비표 ·
|
||||||
* 재료비목록표 · 중기사용료 · 중기목록표(실무 엑셀 꼴 · 값 = `B09_Estimation_Calc_Base`).
|
* 재료비목록표 · 중기사용료 · 중기목록표(실무 엑셀 꼴 · 값 = `B09_Estimation_Calc_Base`).
|
||||||
|
* 재료비목록표 = 엑셀 꼴 + 수량(B08 합계) · 총액 열(166-3)
|
||||||
*
|
*
|
||||||
* 고칠 칸 = 자재단가대비표 적용 단가(빈칸으로 지우면 최저 자동) · 관급 / 별도 칸 누름 = 표시 바꿈
|
* 고칠 칸 = 자재단가대비표 적용 단가(빈칸으로 지우면 최저 자동) · 관급 / 별도 칸 누름 = 표시 바꿈
|
||||||
* 이동 = 재료비목록표 줄 → 대비표 · 중기목록표 줄 → 중기사용료 블록 · 블록 줄 → 경비 · 기초 · 대비표
|
* 이동 = 재료비목록표 줄 → 대비표 · 중기목록표 줄 → 중기사용료 블록 · 블록 줄 → 경비 · 기초 · 대비표
|
||||||
@@ -179,13 +180,18 @@ export const buildMaterial: TabBuild = ({ sheets }): TabTable => {
|
|||||||
text("name", "명칭", 200),
|
text("name", "명칭", 200),
|
||||||
text("spec", "규격", 160),
|
text("spec", "규격", 160),
|
||||||
text("unit", "단위", 56),
|
text("unit", "단위", 56),
|
||||||
|
qty("qty", "수량", 3),
|
||||||
money("price", "단가"),
|
money("price", "단가"),
|
||||||
|
money("amount", "총액"),
|
||||||
text("supply", "관급 · 별도", 80),
|
text("supply", "관급 · 별도", 80),
|
||||||
text("origin", "비고", 90),
|
text("origin", "비고", 90),
|
||||||
],
|
],
|
||||||
rows,
|
rows,
|
||||||
|
total: rows.length
|
||||||
|
? { name: "계", amount: rows.reduce((sum, r) => sum + (Number(r.amount) || 0), 0) }
|
||||||
|
: null,
|
||||||
frozen: 2,
|
frozen: 2,
|
||||||
hint: `단가 = 자재단가대비표 적용 단가 · ${LINK_HINT}`,
|
hint: `수량 = B08 총괄자재수량집계표 합계 열(할증 포함 · 사본 자원 줄은 빈칸) · 단가 = 자재단가대비표 적용 단가 · 총액 = ROUND(수량 × 단가, 0) · 계 = 빈칸 뺀 합 · ${LINK_HINT}`,
|
||||||
link: (i) => (rows[i] ? { tab: "compare", key: String(rows[i].key) } : null),
|
link: (i) => (rows[i] ? { tab: "compare", key: String(rows[i].key) } : null),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""166-3 재료비목록표 = B08 총괄 합계 수량 × 적용 단가 = 총액 ROUND 0 · 공제 줄 거름 · MT 합침 · 거울."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
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_Base import compute_base # noqa: E402
|
||||||
|
from resources.tester.test_144_b09_base_calc import _run_ts # noqa: E402
|
||||||
|
|
||||||
|
MATERIALS = [
|
||||||
|
{"key": "MT1", "name": "부직포", "prices": {"물가자료": 1234.5}, "qty": 10.25},
|
||||||
|
{"key": "MT2", "name": "수량 없는 사본 줄", "prices": {"물가자료": 500}},
|
||||||
|
{"key": "b08:cement", "name": "시멘트", "prices": {}, "qty": 3.333},
|
||||||
|
{"key": "b08:sand", "name": "모래", "prices": {}, "qty": 7},
|
||||||
|
]
|
||||||
|
INPUTS = {"labor": [], "machines": [], "materials": MATERIALS}
|
||||||
|
USER = {"materials": {"b08:cement": {"applied": 7000.5}}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_총액_ROUND_0_빈칸():
|
||||||
|
rows = {r["key"]: r for r in compute_base(INPUTS, USER)["material"]}
|
||||||
|
assert (rows["MT1"]["qty"], rows["MT1"]["price"], rows["MT1"]["amount"]) == (
|
||||||
|
10.25,
|
||||||
|
1234.5,
|
||||||
|
12654,
|
||||||
|
) # 12,653.6 → 반 올림
|
||||||
|
assert rows["b08:cement"]["amount"] == 23333 # 3.333 × 7000.5 = 23,332.67
|
||||||
|
assert rows["MT2"]["qty"] is None and rows["MT2"]["amount"] is None # 수량 빈칸
|
||||||
|
assert rows["b08:sand"]["price"] is None and rows["b08:sand"]["amount"] is None # 단가 빈칸
|
||||||
|
|
||||||
|
|
||||||
|
def test_거울_TS():
|
||||||
|
for user in ({}, USER):
|
||||||
|
assert _run_ts(INPUTS, user) == compute_base(INPUTS, user)
|
||||||
|
|
||||||
|
|
||||||
|
def test_공제_거름_MT_합침(monkeypatch):
|
||||||
|
sheets = {
|
||||||
|
"material": {
|
||||||
|
"rows": [
|
||||||
|
{"key": "cement", "group": "시멘트", "unit": "포", "amount": 12.5},
|
||||||
|
{"key": "picked_deduct", "group": "채집석 공제", "unit": "㎥", "amount": 4},
|
||||||
|
{"key": "MT1", "group": "부직포", "unit": "㎡", "amount": 30.1},
|
||||||
|
{"key": "MT9", "group": "사본에 없는 MT", "unit": "개", "amount": 2},
|
||||||
|
{"key": "empty", "group": "수량 없음", "amount": None},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys = [r["key"] for r in prices.b08_material_rows(sheets)]
|
||||||
|
assert keys == ["b08:cement", "b08:MT1", "b08:MT9"]
|
||||||
|
|
||||||
|
monkeypatch.setattr(prices, "_master", lambda: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
prices,
|
||||||
|
"demand",
|
||||||
|
lambda master, keys: {"labor": [], "machines": [], "materials": ["MT1", "MO1"],
|
||||||
|
"fuel_keys": {}, "missing": []},
|
||||||
|
) # fmt: skip
|
||||||
|
book = {"rows": {"MT1": {"key": "MT1", "name": "부직포"}, "MO1": {"key": "MO1"}}}
|
||||||
|
got = prices.build_inputs(book, [], sheets)["materials"]
|
||||||
|
assert [r["key"] for r in got] == ["MT1", "MO1", "b08:cement", "b08:MT9"]
|
||||||
|
assert got[0]["qty"] == 30.1 and "qty" not in book["rows"]["MT1"] # 책은 안 바꿈
|
||||||
|
assert got[2]["qty"] == 12.5
|
||||||
Reference in New Issue
Block a user