test(841-1): B09 = M02 값 — 산근 · 대가 · 고정 줄 셈 다듬기 · 낡은 시험 정리 — C 걸음
코드 - 고정 줄 단가 = M02_Labor_Fixed.line(사본 묶음 · 이은 노임 · 자재 = 묶음 값) — B09 단가 책으로 다시 안 덮음 - 대가 · 산근 고친 칸 = 품만 실제로(단가 칸 덮기 줄 남던 흠 · 품 고친 뒤 줄 금액 다시 셈 흠 py · ts 짝) - 산근 글 = 엔진 단가 셈 그대로 · 별도계상 재료 줄 끝 「· 별도계상 = 0.0」 · 계 줄 = 블록 합 - 노무비 · 중기목록 이름에서 규격 겹침 뗌 · TS 바탕 셈 이름 PARTS → MACHINE_PARTS(짝 시험이 TS 를 이어 붙일 때 겹침) 시험(고친 까닭 = 단가 원천이 B09 책 → M02 엔진 · 노임 · 기계 덮개 · 단가 고친 칸 · 중기사용료 걷음) - 144 바탕 · 뼈대: 운전사 · 중기사용료 · 경비 · 덮개 시험 걷음 → 노무 · 중기 나열 · 사급 표시 · 자재만 책 - 145-1 · 145-2 · 155 · 157-1 · 166-2 · 166-6 · 719-3 · 719-4 · 723-4 · 778-4 · 792-3: 줄 단가 = own(엔진) · 산근 = Solver() · collect 새 부름 꼴 - 702 · 705-1 · 719-2: 단가 칸 덮개 = 안 읽음 · 고정 줄 = M02 셈(노임 새 판 따라가기 시험 걷음) - 203-1 · 296 · 703-3 · 795-4: 단가 책 = 자재 줄로 - 707 · 707-6 · 707-7: 단가표 불러오기 = 자재만 · 거창 이은 안내판 줄 = M02 사본 셈(엑셀 −50,000) - 201-1: 환율 · 중기사용료 표 걷음 · b09 저장 속도: 산근 앞 풀이 그대로(단가 표 같음 견줌 걷음) - 대상 113 파일 764 통과 · 25 실패 = 원래 실패(formatMoney · formatPrice 없음 · 단위 글 ㎜ · sec · 719-1 · 796-2e · 717-4 · 721 · 311 · 243 · 246) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@ export const SOURCES = [
|
||||
|
||||
/** 자재 줄 사용자 표시 */
|
||||
export const SUPPLY = ["관급", "별도"] as const;
|
||||
const PARTS = ["labor", "material", "expense"] as const;
|
||||
const MACHINE_PARTS = ["labor", "material", "expense"] as const;
|
||||
|
||||
function scaled(value: number, places: number, fn: (x: number) => number): number {
|
||||
const a = Math.abs(value);
|
||||
@@ -167,8 +167,13 @@ export function computeBase(inputs: Row, user: Row): Row {
|
||||
const gaps: Gap[] = [];
|
||||
const compare = compareSheet(inputs.materials || [], user.materials || {}, gaps);
|
||||
const machine = (inputs.machines || []).map((m: Row, i: number) => {
|
||||
const parts: Row = Object.fromEntries(PARTS.map((p) => [p, num(m[p])]));
|
||||
return { ...head(m, i + 1), unit: "시간", ...parts, sum: total(PARTS.map((p) => parts[p])) };
|
||||
const parts: Row = Object.fromEntries(MACHINE_PARTS.map((p) => [p, num(m[p])]));
|
||||
return {
|
||||
...head(m, i + 1),
|
||||
unit: "시간",
|
||||
...parts,
|
||||
sum: total(MACHINE_PARTS.map((p) => parts[p])),
|
||||
};
|
||||
});
|
||||
return {
|
||||
labor: laborSheet(inputs.labor || [], gaps),
|
||||
|
||||
@@ -191,8 +191,7 @@ def _cover(line: dict[str, Any], own: dict[str, Any], qty: Any, prices: dict) ->
|
||||
if not keys:
|
||||
return qty, prices, {}
|
||||
extra = {"base": {"qty": qty, **prices}, "covered": keys}
|
||||
qty = num(mine["qty"]) if "qty" in mine else qty
|
||||
return qty, {p: num(mine[p]) if p in mine else v for p, v in prices.items()}, extra
|
||||
return num(mine["qty"]), dict(prices), extra # 841-1 품만(단가 칸 = 안 읽음)
|
||||
|
||||
|
||||
def _block(
|
||||
|
||||
@@ -172,9 +172,7 @@ function cover(line: Row, own: Row, qty0: Num, prices0: Parts): [Num, Parts, Row
|
||||
const keys = COVER_KEYS.filter((k) => k in mine);
|
||||
if (!keys.length) return [qty0, prices0, {}];
|
||||
const extra = { base: { qty: qty0, ...prices0 }, covered: keys };
|
||||
const prices = { ...prices0 };
|
||||
for (const p of PARTS) if (p in mine) prices[p] = num(mine[p]);
|
||||
return ["qty" in mine ? num(mine.qty) : qty0, prices, extra];
|
||||
return [num(mine.qty), { ...prices0 }, extra]; // 841-1 품만(단가 칸 = 안 읽음 · 짝 `_cover`)
|
||||
}
|
||||
|
||||
/** SK 블록 합을 M02 셈으로 — 고친 칸 · 붙인 줄 없음 · 재료 표시가 M02 자재표시 그대로일 때만 */
|
||||
|
||||
@@ -220,7 +220,7 @@ class Builder:
|
||||
if isinstance(item.get("참조"), dict) and depth < refs.DEPTH:
|
||||
line = {**self.line_of(whole, item, qty, 1, head["name"], copies, depth), **head}
|
||||
elif isinstance(item.get("고정"), dict):
|
||||
line = {**fixed_line(item["고정"], qty, where), **head}
|
||||
line = {**fixed_line(item["고정"], qty, where, whole), **head}
|
||||
elif qty and ("단가" in item or item.get("값묶음")):
|
||||
own = {PART[k]: _f(v) / qty for k, v in (x.get("비목") or {}).items() if k in PART}
|
||||
line = {**head, "kind": "fixed", "qty": qty, "own": own, "link": {}}
|
||||
@@ -287,7 +287,7 @@ class Builder:
|
||||
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)
|
||||
return fixed_line(one[fixed.FIXED], q, None, whole)
|
||||
ref = one.get("참조")
|
||||
if isinstance(ref, dict):
|
||||
doc = copies.get(refs.ref_id(ref))
|
||||
@@ -526,8 +526,9 @@ def _Loose(whole: Any, read: Any) -> Any:
|
||||
return Loose(whole, read)
|
||||
|
||||
|
||||
def fixed_line(fx: dict, qty: Any, where: dict | None = None) -> dict[str, Any]:
|
||||
"""고정 줄(705-1) → 블록 줄 `fixed` — 수량 그대로(배 1) · 이음 없는 비목 = 줄 단가 ·
|
||||
def fixed_line(fx: dict, qty: Any, where: dict | None = None, whole: Any = None) -> dict[str, Any]:
|
||||
"""고정 줄(705-1) → 블록 줄 `fixed` — 수량 그대로(배 1) · 단가 = M02 엔진 고정 줄 셈(`whole` =
|
||||
사본 단가 묶음 · 이은 노임 · 자재 = 묶음 값 · 841-1) · 없으면(붙인 줄) 줄 단가 ·
|
||||
비율 줄(705-3) = `pct`(밑 = 블록 그 비목 합 · 수량 = 율) · 율 기준(778-4) 묶음 = `bundle:` ·
|
||||
줄 목록 = `rows:` 블록 차례(`where` {열쇠: 차례} · 단가산출 블록만 · 없으면 빈 줄)."""
|
||||
if rate := fx.get("비율"):
|
||||
@@ -549,11 +550,12 @@ def fixed_line(fx: dict, qty: Any, where: dict | None = None) -> dict[str, Any]:
|
||||
)
|
||||
return {**name, "kind": "pct", "part": PART[rate["비목"]], "base": base, "qty": _f(qty)}
|
||||
link = fx.get("이음") or {}
|
||||
money = fixed.line(whole, fx)["기준"] if whole is not None else 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},
|
||||
"own": {PART[k]: _f(v) for k, v in money.items() if k in PART},
|
||||
"link": {
|
||||
"labor": link.get("노임"),
|
||||
"material": link.get("자재"),
|
||||
|
||||
@@ -64,6 +64,7 @@ import master_sheet_line as msl # noqa: E402 — 계산서 줄 덧칸(714-1) ·
|
||||
PARTS = ("labor", "material", "expense")
|
||||
COST_PART = {"노무비": "labor", "재료비": "material", "경비": "expense"}
|
||||
PART_COST = {v: k for k, v in COST_PART.items()}
|
||||
PART_NAMES = tuple((p, k) for k, p in COST_PART.items()) # 글 계 줄 차례(노 · 재 · 경)
|
||||
PCT_OF = {**COST_PART, "인건비": "people"} # % 덧줄 밑수 → 덮개 다시 셈(719-3)
|
||||
DEPTH = 4 # 하위 산근 깊이 상한
|
||||
NO_SOURCE = "조합 밖 토공 · 운반 · 사토 · 준비공은 산근 없음"
|
||||
@@ -358,6 +359,12 @@ class Solver:
|
||||
sums = ss.totals(row, lines) or block_totals(lines) # 끝전 · 조정(792-3)
|
||||
why = next((ln["why"] for ln in lines if ln.get("why")), None)
|
||||
head, foot = self._text(whole, src, times, lines)
|
||||
for ln in lines: # 글 = 엔진 단가 셈 그대로 · 별도계상 재료 줄은 끝에 0 원 표시(841-1)
|
||||
if "aside" in ln and ln.get("text"):
|
||||
ln["text"] = [*ln["text"][:-1], f"{ln['text'][-1]} · 별도계상 = 0.0"]
|
||||
if foot and any("aside" in ln for ln in lines) and sums.get("sum") is not None:
|
||||
parts = " + ".join(f"{st_name} {sums[p]:,.0f}" for p, st_name in PART_NAMES)
|
||||
foot = f"계 : {parts} = {sums['sum']:,.0f}"
|
||||
spec, spec_raw = spec_text(whole, src["given"])
|
||||
return {
|
||||
"logic": code,
|
||||
@@ -420,7 +427,6 @@ class Solver:
|
||||
continue
|
||||
part = next(iter(ln["price"]), "material")
|
||||
ln["aside"], ln[part], ln["price"] = ln[part], 0.0, {part: 0.0}
|
||||
self.prices.material[key] = 0.0
|
||||
|
||||
def _each(self, n: int, part: str, q: float) -> float | None:
|
||||
"""엔진 줄 n 비목 단가 = 엔진 금액 ÷ 엔진 수량 · 안 쓰는 줄(수량 0) = 0 · 못 셈 = None."""
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
조작 중 = 화면이 TS 짝으로 바로 셈 · [저장] = 서버가 다시 풀이(`Sangeun.build`) 뒤 이 파일로 정본.
|
||||
|
||||
덮개 = 사용자 값 `sg_cover` {산근 키: {줄 열쇠 「n:이름」: {qty?}}} —
|
||||
qty = 1 단위 품(841-1 단가 칸 걷음 · 옛 단가 칸은 안 읽음) · 있는 칸만 셈 값 대신 · 줄이 없거나 이름이 다르면 짝 잃음
|
||||
qty = 1 단위 품(841-1 단가 칸 걷음 · 옛 단가 칸은 안 읽음) ·
|
||||
있는 칸만 셈 값 대신 · 줄이 없거나 이름이 다르면 짝 잃음
|
||||
(`sg_orphans` · 알림만 · 덮개는 남음) · 산근 키 = 로직 + 입력값 → [업데이트] 뒤에도 같은 블록.
|
||||
고칠 칸(`fields`) = 노무 · 자재 · 중기 · 빈 줄 · 하위 산근 = 품 ·
|
||||
% 덧줄(`of` 있는 것) = 율.
|
||||
@@ -92,10 +93,10 @@ def _settle(block: dict[str, Any], mine: dict[str, Any], by: dict[str, dict[str,
|
||||
ln.update(qty=qty, price={part: value}, **{part: _money(value, 1)})
|
||||
ln["why"] = None if value is not None else "밑수 빈칸"
|
||||
else:
|
||||
price = {**price0, **{p: num(cell[p]) for p in keys if p != "qty"}}
|
||||
for p in can:
|
||||
if p != "qty":
|
||||
ln[p] = _money(price.get(p), qty)
|
||||
price = dict(price0) # 841-1 단가 = M02 사본 값 그대로 · 품만 고침
|
||||
for p in PARTS:
|
||||
if p in price:
|
||||
ln[p] = _money(price[p], qty)
|
||||
ln.update(qty=qty, price=price)
|
||||
if all(ln.get(p) is not None for p in PARTS):
|
||||
ln["why"] = None
|
||||
|
||||
@@ -87,11 +87,8 @@ function settle(block: Row, mine: Row, by: Map<string, Row>): void {
|
||||
Object.assign(ln, { qty, price: { [part]: value }, [part]: money(value, 1) });
|
||||
ln.why = value !== null ? null : "밑수 빈칸";
|
||||
} else {
|
||||
const price: Row = {
|
||||
...price0,
|
||||
...Object.fromEntries(keys.filter((p) => p !== "qty").map((p) => [p, num(cell[p])])),
|
||||
};
|
||||
for (const p of can) if (p !== "qty") ln[p] = money(price[p] ?? null, qty);
|
||||
const price: Row = { ...price0 }; // 841-1 단가 = M02 사본 값 그대로 · 품만 고침(짝 `_settle`)
|
||||
for (const p of PARTS) if (p in price) ln[p] = money(price[p] ?? null, qty);
|
||||
Object.assign(ln, { qty, price });
|
||||
if (PARTS.every((p) => (ln[p] ?? null) !== null)) ln.why = null;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,9 @@ def price_lists(blocks: list[dict[str, Any]], sg_blocks: list[dict[str, Any]]) -
|
||||
if kind not in ("labor", "machine") or not key or not ln.get("qty"):
|
||||
continue
|
||||
price = ln.get("own") or ln.get("price") or {}
|
||||
head = {"key": key, "name": ln.get("name") or "", "spec": ln.get("spec") or ""}
|
||||
name, spec = str(ln.get("name") or ""), str(ln.get("spec") or "")
|
||||
name = name.removesuffix(spec).strip() if spec else name # 「굴착기 0.7㎥」 + 규격 겹침
|
||||
head = {"key": key, "name": name, "spec": spec}
|
||||
if kind == "labor" and price.get("labor") is not None:
|
||||
labor.setdefault(key, {**head, "unit": "인", "price": price["labor"]})
|
||||
elif kind == "machine" and None not in [price.get(p) for p in PARTS]:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""144-2~4 B09 바탕 단가 셈 — 엑셀 검산 대조 · 거울(`B09_Estimation_Calc_Base.py` ↔ `.ts`).
|
||||
|
||||
견본 = 기번3 공통 엑셀(`2024년 간선임도사업(기번3).xlsx`) 같은 입력:
|
||||
노무비목록표 E10~E12 · 환율및기초자료 G6~G8 · 경비목록표 E4 · E5 · E7 · E13 · E16 · E21 ·
|
||||
자재단가대비표 5~12줄 · 중기사용료 1 · 2 · 4 · 11 · 14 · 16 호표(F11 126,000 …)
|
||||
견본 = 기번3 공통 엑셀(`2024년 간선임도사업(기번3).xlsx`) 자재단가대비표 5~12줄 같은 입력 ·
|
||||
841-1 운전사 시간 노임 · 중기사용료 · 경비목록표 · 노임 · 기계 덮개 시험 걷음(B09 가 기계 시간당을
|
||||
다시 셈하던 곳 · 값 = M02 단가산출 그대로) — 노무비 · 중기목록표 = 엔진 단가 나열만 봄.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -23,31 +23,17 @@ from B09_Estimation.B09_Estimation_Calc_Base import ( # noqa: E402
|
||||
)
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
FACTOR = {"num": 400, "den": 1920, "text": "1/8 × 16/12 × 25/20"}
|
||||
|
||||
#: 841-1 노무비목록표 · 중기목록표 입력 = 대가 · 산근 줄 엔진 단가 나열(서버 `Supply.price_lists`)
|
||||
LABOR = [
|
||||
{"key": "LB000048", "code": "1048", "name": "건설기계운전사", "price": 267360},
|
||||
{"key": "LB000049", "code": "1049", "name": "화물차운전사", "price": 226709},
|
||||
{"key": "LB000050", "code": "1050", "name": "일반기계운전사", "price": 161142},
|
||||
{"key": "LB000002", "code": "1002", "name": "보통인부", "price": None},
|
||||
{"key": "LB000048", "name": "건설기계운전사", "spec": "", "unit": "인", "price": 267360},
|
||||
{"key": "LB000002", "name": "보통인부", "spec": "", "unit": "인", "price": None},
|
||||
]
|
||||
|
||||
|
||||
def _m(key, name, price, loss, fuel, lph, misc, crew_code):
|
||||
return {"key": key, "name": name, "spec": "", "price": price, "loss": loss, "fuel": fuel,
|
||||
"fuel_lph": lph, "misc_pct": misc, "crew": 1 if crew_code else None,
|
||||
"crew_code": crew_code} # fmt: skip
|
||||
|
||||
|
||||
MACHINES = [
|
||||
_m("X00001", "불도우저19ton", 184499, 1811, "경유", 25, 16, "1048"),
|
||||
_m("X00003", "굴삭기(0.2m3)", 62550, 2085, "경유", 5, 21, "1048"),
|
||||
_m("X00005", "굴삭기(0.7m3)", 110926, 2085, "경유", 11.6, 22, "1048"),
|
||||
_m("X00032", "덤프자동덮개", 1546, 2684, None, None, None, None),
|
||||
_m("X00048", "콘크리트믹서0.45m3", 5409, 3039, "휘발유", 3.9, 2, "1050"),
|
||||
_m("X00074", "덤프트럭2.5ton(토사)", 20793, 2967, "경유", 2.9, None, "1049"),
|
||||
_m("X09999", "기준가 없는 기계", None, 2000, "중유", 3, 10, "1048"),
|
||||
]
|
||||
{"key": "EQ000020", "name": "굴착기(무한궤도) 0.7㎥", "spec": "",
|
||||
"labor": 1459, "material": 1077, "expense": 1012},
|
||||
{"key": "EQ000001", "name": "빈 값", "spec": "", "labor": None, "material": 1, "expense": 1},
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
def _mt(key, name, **prices):
|
||||
@@ -64,16 +50,7 @@ MATERIALS = [
|
||||
_mt("b08:sand", "골재"),
|
||||
]
|
||||
|
||||
INPUTS = {
|
||||
"labor": LABOR,
|
||||
"machines": MACHINES,
|
||||
"materials": MATERIALS,
|
||||
"fuel_keys": {"경유": "MO2", "휘발유": "MO1"},
|
||||
"driver_factor": FACTOR,
|
||||
"fx": [
|
||||
{"key": "FX000001", "name": "USD", "unit": "원/USD", "value": 1414.9, "date": "2026-08-14"}
|
||||
],
|
||||
}
|
||||
INPUTS = {"labor": LABOR, "machines": MACHINES, "materials": MATERIALS}
|
||||
USER = {"materials": {"M9": {"applied": 950, "supply": "관급"}, "M4": {"supply": "별도"},
|
||||
"b08:sand": {"applied": None}}} # fmt: skip
|
||||
|
||||
@@ -103,20 +80,14 @@ def test_끝수_엑셀_자리():
|
||||
assert rounddown(0.1 + 0.2, 1) == 0.3 # 떠도는 끝수는 버리지 않음
|
||||
|
||||
|
||||
def test_144_2_노무_경비_환율_기초자료():
|
||||
def test_841_1_노무_중기목록_나열_걷은_표_빈():
|
||||
s = compute_base(INPUTS, {})
|
||||
assert [r["price"] for r in s["labor"]] == [267360, 226709, 161142, None]
|
||||
assert [r["price"] for r in s["labor"]] == [267360, None]
|
||||
assert ("labor", "LB000002") in {(g["tab"], g["key"]) for g in s["gaps"]}
|
||||
hours = {d["key"]: d["hour"] for d in s["basis"]["drivers"]}
|
||||
assert hours == {"1048": 55700, "1050": 33571, "1049": 47231} # 기초자료 G6 · G8 · G7
|
||||
assert s["basis"]["drivers"][0]["factor"] == "1/8 × 16/12 × 25/20"
|
||||
assert [r["price"] for r in s["expense"]][:3] == [
|
||||
184499,
|
||||
62550,
|
||||
110926,
|
||||
] # 경비목록표 E4 · E5 · E7
|
||||
assert s["expense"][0]["unit"] == "천원"
|
||||
assert s["basis"]["fx"][0]["value"] == 1414.9
|
||||
got = {r["key"]: (r["sum"], r["labor"], r["material"], r["expense"]) for r in s["machine"]}
|
||||
assert got == {"EQ000020": (3548, 1459, 1077, 1012), "EQ000001": (None, None, 1, 1)}
|
||||
assert s["expense"] == [] and s["machine_blocks"] == [] # 경비목록표 · 중기사용료 걷음
|
||||
assert s["basis"] == {"fx": [], "drivers": []} # 환율및기초자료(운전사 시간 노임) 걷음
|
||||
|
||||
|
||||
def test_144_3_자재단가대비_최저_사용자_관급():
|
||||
@@ -140,54 +111,13 @@ def test_144_3_자재단가대비_최저_사용자_관급():
|
||||
assert gaps == [("compare", "b08:sand")]
|
||||
|
||||
|
||||
def test_144_4_중기사용료_엑셀_검산():
|
||||
s = compute_base(INPUTS, {})
|
||||
got = {r["key"]: (r["sum"], r["labor"], r["material"], r["expense"]) for r in s["machine"]}
|
||||
assert got["X00001"] == (126000, 55700, 36888, 33412) # 중기목록표 4줄
|
||||
assert got["X00003"] == (76436, 55700, 7695, 13041)
|
||||
assert got["X00005"] == (96829, 55700, 18001, 23128)
|
||||
assert got["X00032"] == (414, 0, 0, 414) # 덮개 — 운전사 · 연료 없음
|
||||
assert got["X00048"] == (40659, 33571, 5445, 1643) # 일반기계운전사 · 휘발유 · 잡유 2 %
|
||||
assert got["X00074"][1:3] == (47231, 3688) # 화물차운전사 · 잡품 없음
|
||||
assert got["X09999"] == (None, 55700, None, None) # 기준가 · 중유 단가 없음 = 그 비목 · 합 빈칸
|
||||
block = s["machine_blocks"][0]
|
||||
assert [ln["amount"] for ln in block["lines"]] == [
|
||||
33412.7,
|
||||
55700,
|
||||
31800,
|
||||
5088,
|
||||
] # 중기사용료 F7~F10
|
||||
assert block["lines"][0]["qty"] == 0.1811 and block["lines"][0]["ref"] == {
|
||||
"tab": "expense",
|
||||
"key": "X00001",
|
||||
}
|
||||
why = {g["why"] for g in s["gaps"] if g["key"] == "X09999"}
|
||||
assert why == {"연료(중유) 단가 없음", "기계 기준가 없음", "손료 기준가 · 계수 없음"}
|
||||
|
||||
|
||||
#: 707-3 노임 · 기계 단가 덮개 — 건설기계운전사 일당 · 불도우저 시간 단가(셋 다) · 빠진 칸 = 버림
|
||||
COVER = {
|
||||
"labor_cover": {"LB000048": 283297, "LB000002": 172068, "없는 키": 1},
|
||||
"machine_cover": {"X09999": {"labor": 1, "material": 2, "expense": 3},
|
||||
"X00003": {"labor": 1, "material": None, "expense": 3}},
|
||||
} # fmt: skip
|
||||
|
||||
|
||||
def test_707_3_노임_기계_덮개():
|
||||
s = compute_base(INPUTS, COVER)
|
||||
labor = {r["key"]: r for r in s["labor"]}
|
||||
assert (labor["LB000048"]["price"], labor["LB000048"]["book"]) == (283297, 267360)
|
||||
assert labor["LB000002"]["covered"] and "covered" not in labor["LB000049"]
|
||||
assert "LB000002" not in {g["key"] for g in s["gaps"] if g["tab"] == "labor"} # 덮개 = 빈칸 아님
|
||||
hours = {d["key"]: d["hour"] for d in s["basis"]["drivers"]}
|
||||
assert hours["1048"] == 59020 # 거창 기계경비 운전사 59,020.2 → 59,020
|
||||
got = {r["key"]: (r["sum"], r["labor"], r["material"], r["expense"]) for r in s["machine"]}
|
||||
assert got["X09999"] == (6, 1, 2, 3) and got["X00003"][1] == 59020 # 반쪽 덮개 = 버림
|
||||
block = next(b for b in s["machine_blocks"] if b["key"] == "X09999")
|
||||
assert block["covered"] and block["book"]["sum"] is None
|
||||
assert "X09999" not in {g["key"] for g in s["gaps"] if g["tab"] == "machine_blocks"}
|
||||
def test_841_1_사급_표시_기본_끔():
|
||||
rows = [{**MATERIALS[2], "supply": "별도"}] # M02 자재표시 기본 별도계상
|
||||
s = compute_base({**INPUTS, "materials": rows}, {"materials": {"M3": {"supply": "사급"}}})
|
||||
assert s["compare"][0]["supply"] is None
|
||||
assert compute_base({**INPUTS, "materials": rows}, {})["compare"][0]["supply"] == "별도"
|
||||
|
||||
|
||||
def test_거울_TS_가_같은_값():
|
||||
for user in ({}, USER, COVER):
|
||||
for user in ({}, USER, {"materials": {"M3": {"supply": "사급"}}}):
|
||||
assert _run_ts(INPUTS, user) == compute_base(INPUTS, user)
|
||||
|
||||
@@ -77,22 +77,21 @@ def test_사용자_값_다듬기_저장_자리(tmp_path):
|
||||
assert store.read_book(tmp_path) is None
|
||||
|
||||
|
||||
def test_단가_굳히기_운전사_연료_따라옴():
|
||||
book = prices.refresh_book(["EQ000004"]) # 불도저(무한궤도) 19ton · 조종원 1048 · 경유
|
||||
def test_841_1_단가_책_자재만():
|
||||
"""841-1 — 노무 · 기계 · 운전사 · 연료 · 환율은 책 밖(B09 금액 = 사본 M02 엔진 값) · 자재만 굳힘."""
|
||||
book = prices.refresh_book(["EQ000004", "LB000048", "MT000401", "MO000002"])
|
||||
rows = book["rows"]
|
||||
assert set(rows) == {"EQ000004", "LB000048", "MO000002"}
|
||||
assert rows["EQ000004"]["loss"] == 1811 and rows["EQ000004"]["crew_code"] == "1048"
|
||||
assert rows["LB000048"]["code"] == "1048" and rows["MO000002"]["prices"]["유가"] > 0
|
||||
assert book["driver_factor"] == {"num": 400, "den": 1920, "text": "1/8 × 16/12 × 25/20"}
|
||||
assert set(rows) == {"MT000401", "MO000002"} and "driver_factor" not in book
|
||||
assert rows["MO000002"]["prices"]["유가"] > 0
|
||||
# [저장] = 책에 없는 새 줄만 더함 — 있는 줄 값은 안 바꿈
|
||||
stale = {**book, "rows": {**rows, "LB000048": {**rows["LB000048"], "price": 1}}}
|
||||
grown = prices.extend_book(stale, ["EQ000004", "LB000002"])
|
||||
assert grown["rows"]["LB000048"]["price"] == 1 and "LB000002" in grown["rows"]
|
||||
stale = {**book, "rows": {**rows, "MT000401": {**rows["MT000401"], "prices": {}}}}
|
||||
grown = prices.extend_book(stale, ["MT000401", "MT000338"])
|
||||
assert grown["rows"]["MT000401"]["prices"] == {} and "MT000338" in grown["rows"]
|
||||
# 책 밖 줄 = 지금 M01 값 + fresh 표시
|
||||
inputs = prices.build_inputs(stale, ["EQ000004", "LB000002"])
|
||||
fresh = {row["key"]: row.get("fresh", False) for row in inputs["labor"]}
|
||||
assert fresh == {"LB000002": True, "LB000048": False}
|
||||
assert inputs["fuel_keys"] == {"경유": "MO000002"}
|
||||
inputs = prices.build_inputs(stale, ["MT000401", "MT000338", "LB000002"])
|
||||
fresh = {row["key"]: row.get("fresh", False) for row in inputs["materials"]}
|
||||
assert fresh == {"MT000401": False, "MT000338": True}
|
||||
assert set(inputs) == {"materials", "missing", "now"}
|
||||
|
||||
|
||||
def test_연결_없으면_빈_표와_연결_없음(tmp_path, monkeypatch):
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
- 산근 = 155-4 갈래(사용자 뜻 · 145 정함 뒤집음) — 일위대가 로직 줄 중 단가산출 줄(승격 아님) +
|
||||
사전조합 안 단가산출 줄 · 일위대가화(승격 · 옛 SP) 줄 자체는 산근 아님(대가 블록)
|
||||
- 줄 금액 = ROUNDDOWN(B09 굳힌 단가 × 1 단위 수량, 1) · 총계 = 비목마다 ROUNDDOWN 0 — 엑셀
|
||||
단가산출근거 제 6호표(뒷채움) 줄 값 · 총계 8,328(6,878 · 538 · 912)과 같음
|
||||
- 줄 단가 = M02 엔진 줄 금액 ÷ 수량(841-1 · 사본 단가 묶음 · B09 단가 책 안 읽음) · 줄 금액 =
|
||||
ROUNDDOWN(단가 × 1 단위 수량, 1) · 총계 = 비목마다 ROUNDDOWN 0 — 엑셀 단가산출근거 제 6호표(뒷채움)
|
||||
줄 값 · 총계 8,328(6,878 · 538 · 912)과 같음 · 산근 합 = M02 엔진 기준
|
||||
- 관급 · 별도계상 재료(supply) = 산근 안 0원 · `aside` 에 금액(841-1)
|
||||
- 같은 로직 + 같은 입력값 = 산근 하나(쓰는 구조물이 늘어남) · 입력이 다르면 둘 · 번호 = 첫 등장
|
||||
- 사본 없으면 빈 표 + 「연결 없음」 · 조합 밖 토공 · 운반 산근 안내 한 줄 · 작업량 Q = ROUND 2
|
||||
"""
|
||||
@@ -13,19 +15,14 @@ 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 import B09_Estimation_Router as router_mod # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Sangeun as sg # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Sangeun_Sum as ss # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import compute_base # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Linked import used_keys # noqa: E402
|
||||
from M02_MasterTemplete import M02_Item_Prices as item_prices # noqa: E402
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor_doc # noqa: E402
|
||||
|
||||
STONE = "GF000219" # 돌쌓기 메쌓기(인력) — 석공 0.195 · 보통인부 0.156 / ㎡
|
||||
NET = "GC000337" # 낙석방지망 — 노무 · 크레인(중기) · 자재 셋
|
||||
@@ -50,26 +47,24 @@ def _snapshot(folder: Path, rows, values=None, copies=None) -> Path:
|
||||
return folder
|
||||
|
||||
|
||||
def _sheets(*folders: Path) -> dict:
|
||||
keys = [k for f in folders for k in used_keys(f)]
|
||||
return compute_base(prices.build_inputs(None, list(dict.fromkeys(keys))), {})
|
||||
def _engine(folder: Path, code: str, given=None) -> dict:
|
||||
"""M02 엔진 기준(사본 단가 묶음 · 날 값 줄 · 끝수 뒤 기준)."""
|
||||
whole = item_prices.load_prices(json.loads((folder / "단가.json").read_text(encoding="utf-8")))
|
||||
return labor_doc.run_line(whole, code, given or {})
|
||||
|
||||
|
||||
def test_단가산출_줄만_산근_블록_단가는_B09_표(tmp_path):
|
||||
def test_단가산출_줄만_산근_블록_단가는_M02_엔진(tmp_path):
|
||||
a = _snapshot(tmp_path / "a", [{"로직": STONE}, {"로직": NET, "승격": True}], {NET: NET_IN})
|
||||
sheets = _sheets(a)
|
||||
got = sg.collect([("s1", a)], sheets)
|
||||
got = sg.collect([("s1", a)])
|
||||
(block,) = got["sg"]
|
||||
assert (block["no"], block["logic"], block["unit"]) == (1, STONE, "㎡")
|
||||
labor = {r["key"]: r["price"] for r in sheets["labor"]}
|
||||
lines = block["lines"]
|
||||
assert [(ln["kind"], ln["ref"]["key"], ln["qty"]) for ln in lines] == [
|
||||
("labor", "LB000033", 0.195),
|
||||
("labor", "LB000002", 0.156),
|
||||
]
|
||||
want = [sg.rounddown(labor[ln["ref"]["key"]] * ln["qty"], 1) for ln in lines]
|
||||
assert [ln["labor"] for ln in lines] == want
|
||||
assert block["labor"] == sg.rounddown(sum(want), 0) and block["sum"] == block["labor"]
|
||||
want = _engine(a, STONE)["기준"]
|
||||
assert block["labor"] == float(want["노무비"]) and block["sum"] == float(want["계"])
|
||||
assert block["material"] == 0 and block["expense"] == 0 and block["why"] is None
|
||||
assert got["sg_list"][0]["key"] == block["key"] and got["sg_gaps"] == []
|
||||
assert sg.price_of(got, block["key"]) == {
|
||||
@@ -77,20 +72,19 @@ def test_단가산출_줄만_산근_블록_단가는_B09_표(tmp_path):
|
||||
} # fmt: skip
|
||||
|
||||
|
||||
def test_중기는_중기목록표_노_재_경_자재는_적용_단가(tmp_path):
|
||||
def test_중기_자재_단가_M02_엔진_별도계상은_0원(tmp_path):
|
||||
a = _snapshot(tmp_path / "a", [{"로직": NET}], {NET: NET_IN})
|
||||
sheets = _sheets(a)
|
||||
(block,) = sg.collect([("s1", a)], sheets)["sg"]
|
||||
(block,) = sg.collect([("s1", a)])["sg"]
|
||||
kinds = [ln["kind"] for ln in block["lines"]]
|
||||
assert kinds == ["labor", "labor", "machine", "material", "material", "material"]
|
||||
crane = block["lines"][2]
|
||||
row = next(r for r in sheets["machine"] if r["key"] == crane["ref"]["key"])
|
||||
for part in sg.PARTS:
|
||||
assert crane[part] == sg.rounddown(row[part] * crane["qty"], 1)
|
||||
net = block["lines"][3]
|
||||
applied = next(r["applied"] for r in sheets["compare"] if r["key"] == net["ref"]["key"])
|
||||
assert net["material"] == sg.rounddown(applied * net["qty"], 1)
|
||||
assert block["sum"] == block["labor"] + block["material"] + block["expense"]
|
||||
want = _engine(a, NET, NET_IN)["기준"]
|
||||
assert [block[p] for p in (*sg.PARTS, "sum")] == [
|
||||
float(want[k]) for k in ("노무비", "재료비", "경비", "계")
|
||||
]
|
||||
(apart,) = sg.collect([("s1", a)], supply=lambda key: "별도")["sg"]
|
||||
mats = [ln for ln in apart["lines"] if ln["kind"] == "material"]
|
||||
assert all(ln["material"] == 0 and ln["aside"] for ln in mats if ln["qty"])
|
||||
assert apart["material"] < block["material"] and apart["labor"] == block["labor"]
|
||||
|
||||
|
||||
def test_엑셀_6호표_뒷채움_검산():
|
||||
@@ -119,7 +113,7 @@ def test_같은_로직_같은_입력은_하나_다르면_둘_사전조합_안_
|
||||
[{"로직": "", "참조": SC_REF}, {"로직": "", "참조": SP_REF}, {"로직": STONE}],
|
||||
copies={"참조|system||SC-000001": sc, "참조|system||SP-000001": sp},
|
||||
)
|
||||
got = sg.collect([("s1", a), ("s2", b), ("s3", c), ("s4", d)], _sheets(a, c, d))
|
||||
got = sg.collect([("s1", a), ("s2", b), ("s3", c), ("s4", d)])
|
||||
blocks = got["sg"]
|
||||
assert [(x["no"], x["logic"], x["uses"]) for x in blocks] == [
|
||||
(
|
||||
@@ -135,11 +129,11 @@ def test_같은_로직_같은_입력은_하나_다르면_둘_사전조합_안_
|
||||
|
||||
|
||||
def test_사본_없으면_빈_표_연결_없음(tmp_path, monkeypatch):
|
||||
got = sg.collect([("s1", tmp_path / "없음")], {})
|
||||
got = sg.collect([("s1", tmp_path / "없음")])
|
||||
assert got["sg"] == [] and got["sg_list"] == [] and got["sg_gaps"] == []
|
||||
assert got["sg_notes"] == [sg.NO_SOURCE, sg.EMPTY]
|
||||
plain = _snapshot(tmp_path / "p", [{"로직": STONE, "승격": True}]) # 일위대가화만 = 빈 표
|
||||
assert sg.collect([("s1", plain)], {})["sg"] == []
|
||||
assert sg.collect([("s1", plain)])["sg"] == []
|
||||
|
||||
async def fake_materials(_project_id):
|
||||
return {}
|
||||
@@ -148,42 +142,3 @@ def test_사본_없으면_빈_표_연결_없음(tmp_path, monkeypatch):
|
||||
sheets = asyncio.run(router_mod.payload("p", tmp_path))["sheets"]
|
||||
assert sheets["sg"] == [] and sheets["sg_list"] == []
|
||||
assert sg.EMPTY in sheets["sg_notes"] and sg.NO_SOURCE in sheets["sg_notes"]
|
||||
|
||||
|
||||
def test_단가_없으면_빈칸과_연결_없음(tmp_path):
|
||||
a = _snapshot(tmp_path / "a", [{"로직": STONE}])
|
||||
got = sg.collect([("s1", a)], {"labor": [], "machine": [], "compare": []})
|
||||
(block,) = got["sg"]
|
||||
assert block["labor"] is None and block["sum"] is None
|
||||
assert "노무비목록표에 없음" in block["why"]
|
||||
assert [(g["tab"], g["key"]) for g in got["sg_gaps"]] == [("sg", block["key"])]
|
||||
|
||||
|
||||
def test_작업량_Q는_소수_둘():
|
||||
import M01_MasterData.M01_MasterData_Store as m01
|
||||
|
||||
whole = m01.loaded()[1]
|
||||
for code, row in sorted((whole.index.get("GF") or {}).items()):
|
||||
if not any(ss._Q.fullmatch(s["이름"]) for s in row.get("중간", [])):
|
||||
continue
|
||||
try:
|
||||
env = sg._env(whole, row, {})
|
||||
except Exception: # noqa: BLE001 — 입력이 필요한 로직은 건너뜀
|
||||
continue
|
||||
q = next(env[s["이름"]] for s in row["중간"] if ss._Q.fullmatch(s["이름"]))
|
||||
if hasattr(q, "as_tuple"):
|
||||
assert q == round(q, 2), code
|
||||
return
|
||||
pytest.skip("자동값으로 도는 작업량 Q 로직 없음")
|
||||
|
||||
|
||||
def test_화면_탭_둘_서버_산근_그대로():
|
||||
shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
|
||||
builders = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Builders.ts").read_text("utf-8")
|
||||
assert "sg_list: buildSgList," in builders and "sg: buildSg," in builders # 303-2 표 짜기
|
||||
assert 'const SERVER_ONLY = ["sg", "sg_list", "sg_gaps", "sg_notes"] as const;' in shell
|
||||
assert "gaps: [...base.gaps, ...(server.sg_gaps || [])" in shell # 뒤에 대가(145-2) gaps 이음
|
||||
tabs = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Sangeun.ts").read_text("utf-8")
|
||||
assert 'link: (i) => (rows[i] ? { tab: "sg", key: String(rows[i].key) } : null),' in tabs
|
||||
# 155-5 — 중기 = 노 · 재 · 경 세 줄(서버 글 줄 수와 같게 · 값 0 줄도 둠)
|
||||
assert 'const parts = ln.kind === "machine" && !ln.why ? PARTS : [null];' in tabs
|
||||
|
||||
@@ -99,6 +99,24 @@ EXCEL = {
|
||||
} # fmt: skip
|
||||
|
||||
|
||||
def _own(daega):
|
||||
"""841-1 줄 단가 = M02 엔진 단가(`own` · Labor_Source 가 얹음) — 견본은 엑셀 목록표 값을 얹음."""
|
||||
book = {k: {r["key"]: r for r in SHEETS[k]} for k in ("labor", "material", "machine")}
|
||||
for block in daega["blocks"]:
|
||||
for ln in block["lines"]:
|
||||
row = book.get(ln["kind"], {}).get(ln.get("key"))
|
||||
if ln["kind"] == "labor" and row:
|
||||
ln["own"] = {"labor": row["price"]}
|
||||
elif ln["kind"] == "material" and row:
|
||||
ln["own"] = {"material": row["price"]}
|
||||
elif ln["kind"] == "machine" and row:
|
||||
ln["own"] = {p: row[p] for p in ("labor", "material", "expense")}
|
||||
return daega
|
||||
|
||||
|
||||
_own(EXCEL)
|
||||
|
||||
|
||||
def _block(out, bid):
|
||||
return next(b for b in out["daega_blocks"] if b["id"] == bid)
|
||||
|
||||
@@ -141,6 +159,7 @@ def test_엑셀_대가_블록_검산():
|
||||
def _blank_cases():
|
||||
broken = json.loads(json.dumps(EXCEL))
|
||||
broken["blocks"][2]["lines"][1]["key"] = "L09999" # 노임 없음 → 그 블록 · 부른 블록 빈칸 아님
|
||||
broken["blocks"][2]["lines"][1].pop("own")
|
||||
broken["blocks"].append(_b("DGX", "조합", "순환", [_l("daega", "DGY", 1)]))
|
||||
broken["blocks"].append(_b("DGY", "조합", "순환2", [_l("daega", "DGX", 1)]))
|
||||
broken["blocks"].append(_b("DGZ", "품셈", "수량 없음", [_l("labor", "L00017", None)]))
|
||||
@@ -162,7 +181,7 @@ def test_빈칸_규칙():
|
||||
"no": "대가1", "key": "DG14", "labor": None, "material": None, "expense": None, "sum": None,
|
||||
} # fmt: skip
|
||||
whys = {g["why"] for g in out["gaps"]}
|
||||
assert {"노임 단가 없음", "산근 단가 없음(145-1)", "수량 없음", "% 밑수 없음"} <= whys
|
||||
assert {"노임 단가 없음(M02 엔진)", "산근 단가 없음(145-1)", "수량 없음", "% 밑수 없음"} <= whys
|
||||
|
||||
|
||||
def _ts(daega, sheets, sangeun, cover=None):
|
||||
@@ -327,7 +346,7 @@ def test_견본_사본_끝까지_셈(project):
|
||||
inputs = prices.build_inputs(None, keys)
|
||||
inputs["daega"] = daega_inputs(project)
|
||||
base = compute_base(inputs, {})
|
||||
sg_rows = {r["key"]: r for r in sangeun.build(project, base)["sg_list"]}
|
||||
sg_rows = {r["key"]: r for r in sangeun.build(project)["sg_list"]}
|
||||
out = compute_labor(inputs["daega"], base, sg_rows)
|
||||
owner = inputs["daega"]["owners"]["pipe@100.0"]
|
||||
combo = _block(out, owner)
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
「계 : 노무비 6,878 + 재료비 538 + 경비 912 = 8,328」(줄 ROUNDDOWN 1 · 계 ROUNDDOWN 0)
|
||||
- 엑셀 7호표 막자갈채집: 작업량 Q = ROUND 2 중간 줄 · 「55,700 * 1 / 21.59 * 2 = 5,159.7」
|
||||
- % 덧줄 = 「공구손료 (노무비의 %) : 6,878.8 * 3 % = 206.3」(규격 = 149-4 `pct_extras`)
|
||||
- 단가 없음 = 「단가 없음」 글 · 계 그 비목 빈칸 · 글 속 금액 = 산근 풀이(`Sangeun.Solver`) 줄 값
|
||||
- 단가 없음 = 「단가 없음」 글 · 계 그 비목 빈칸 · 글 속 금액 = 산근 풀이(`Sangeun.Solver` · 841-1 M02 엔진
|
||||
단가) 줄 값
|
||||
"""
|
||||
|
||||
import copy
|
||||
@@ -86,31 +87,12 @@ def test_배_켜쌓기율_꼬리_단가_없음():
|
||||
assert got[-1] == "계 : 노무비 빈칸 + 재료비 48 + 경비 82 = 빈칸"
|
||||
|
||||
|
||||
def _solver_prices(whole, code, given):
|
||||
"""산근 풀이와 같은 단가 — 키마다 같은 수(노무 · 자재 1,000 · 중기 엑셀 값)."""
|
||||
sheets = {"labor": [], "machine": [], "compare": []}
|
||||
asked = []
|
||||
|
||||
def price_of(key):
|
||||
asked.append(key)
|
||||
return EXCEL_EQ if key.startswith("EQ") else 1000.0
|
||||
|
||||
st.render_sangeun_text(whole, code, given, price_of)
|
||||
for key in dict.fromkeys(asked):
|
||||
if key.startswith("EQ"):
|
||||
sheets["machine"].append({"key": key, **EXCEL_EQ})
|
||||
elif key.startswith("LB"):
|
||||
sheets["labor"].append({"key": key, "price": 1000.0})
|
||||
else:
|
||||
sheets["compare"].append({"key": key, "applied": 1000.0})
|
||||
return sheets, price_of
|
||||
|
||||
|
||||
def test_글_속_금액은_산근_풀이_값과_같음():
|
||||
for code, given in ((BASE, GIVEN), ("GF000288", {})): # 중기 · 노무 · % 덧줄 · 배 1,000
|
||||
whole = item_prices.load_prices(item_prices.to_json(item_prices.collect([code])))
|
||||
sheets, price_of = _solver_prices(whole, code, given)
|
||||
block = sg.Solver(sheets).add(whole, {"code": code, "given": given}, "s1")
|
||||
solver = sg.Solver() # 841-1 단가 = M02 엔진 · 글 단가 = 풀이가 쓴 그 단가
|
||||
block = solver.add(whole, {"code": code, "given": given}, "s1")
|
||||
price_of = solver.price_of
|
||||
# 결과단위 배(「원/1,000㎡」) — 산근 풀이와 같은 1 단위
|
||||
times = sg._unit(whole.logic(code))[1]
|
||||
got = st.render_sangeun_text(whole, code, given, price_of, times=times)
|
||||
|
||||
@@ -18,10 +18,10 @@ import test_145_2_b09_labor_calc as t145 # noqa: E402
|
||||
|
||||
from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Sangeun as sg # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import compute_base, rounddown # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import rounddown # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Linked import linked_resource_keys, used_keys # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Linked import linked_resource_keys # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Router import compute_sheets # noqa: E402
|
||||
from M02_MasterTemplete import M02_Item_Prices as item_prices # noqa: E402
|
||||
|
||||
@@ -89,13 +89,12 @@ def test_155_4_산근_자재_자리표시_키_대비표로(tmp_path):
|
||||
}
|
||||
for name, data in files.items():
|
||||
(folder / name).write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||||
sheets = compute_base(prices.build_inputs(None, used_keys(folder)), {})
|
||||
(block,) = sg.collect([("s1", folder)], sheets)["sg"]
|
||||
(block,) = sg.collect([("s1", folder)])["sg"] # 841-1 단가 = M02 엔진(사본 단가 묶음)
|
||||
fuel = [ln for ln in block["lines"] if ln["kind"] == "material"]
|
||||
assert fuel and all(ln["ref"]["key"].startswith("MO0") for ln in fuel)
|
||||
applied = {r["key"]: r["applied"] for r in sheets["compare"]}
|
||||
for ln in fuel:
|
||||
assert ln["material"] == rounddown(applied[ln["ref"]["key"]] * ln["qty"], 1)
|
||||
price = ln["price"]["material"]
|
||||
assert price and ln["material"] == rounddown(price * ln["qty"], 1)
|
||||
|
||||
|
||||
def test_155_3_비고_산근_n호표_거울():
|
||||
|
||||
@@ -91,22 +91,8 @@ def test_만약_걸린_갈래_입력_설명_기계_이름():
|
||||
|
||||
|
||||
def _solver(whole, code, given):
|
||||
sheets = {"labor": [], "machine": [], "compare": []}
|
||||
asked = []
|
||||
|
||||
def price_of(key):
|
||||
asked.append(key)
|
||||
return EQ if key.startswith("EQ") else None if key.startswith("SG:") else 1000.0
|
||||
|
||||
st.render_sangeun_text(whole, code, given, price_of)
|
||||
for key in dict.fromkeys(k for k in asked if not k.startswith("SG:")):
|
||||
if key.startswith("EQ"):
|
||||
sheets["machine"].append({"key": key, **EQ})
|
||||
elif key.startswith("LB"):
|
||||
sheets["labor"].append({"key": key, "price": 1000.0})
|
||||
else:
|
||||
sheets["compare"].append({"key": key, "applied": 1000.0})
|
||||
return sg.Solver(sheets).add(whole, {"code": code, "given": given}, "s1")
|
||||
"""841-1 산근 풀이 단가 = M02 엔진(사본 단가 묶음) — 시험 단가 안 넣음."""
|
||||
return sg.Solver().add(whole, {"code": code, "given": given}, "s1")
|
||||
|
||||
|
||||
def test_호표_줄_계는_산근_풀이와_같음():
|
||||
|
||||
@@ -18,11 +18,8 @@ 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 import B09_Estimation_Sangeun as sg # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import compute_base # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Calc_Bill import WHY_LOGIC, compute_estimation # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Linked import used_keys # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Store import clean_user # 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
|
||||
@@ -173,9 +170,7 @@ def test_새_산근_사본_뒤_번호_호표_탭_집계표(tmp_path):
|
||||
user = {"bill": {"rows": [{**HAND[2], "qty": 10}, HAND[3]]}}
|
||||
hand_keys = sg.hand_resource_keys(user)
|
||||
assert {"LB000033", "LB000002"} <= set(hand_keys) # 석공 · 보통인부 → 단가 책 수요
|
||||
keys = list(dict.fromkeys([*used_keys(a), *hand_keys]))
|
||||
base = compute_base(prices.build_inputs(None, keys), {})
|
||||
got = sg.collect([("s1", a)], base, sg.hand_sources(user))
|
||||
got = sg.collect([("s1", a)], sg.hand_sources(user)) # 841-1 단가 = M02 엔진
|
||||
listed = [(r["no"], r["logic"], r["uses"]) for r in got["sg_list"]]
|
||||
assert listed == [(1, NET, ["s1", "수기 줄 새 산근"]), (2, STONE, ["수기 줄 메쌓기"])]
|
||||
assert got["sg_hand"] == {
|
||||
|
||||
@@ -24,6 +24,10 @@ SHEETS = {
|
||||
}
|
||||
|
||||
|
||||
#: 841-1 줄 단가 = M02 엔진(`own`) · 자재 단가 없음 = own None
|
||||
LABOR = {"labor": 165545}
|
||||
|
||||
|
||||
def _b(bid, name, lines):
|
||||
return {
|
||||
"id": bid,
|
||||
@@ -39,14 +43,14 @@ def _b(bid, name, lines):
|
||||
DAEGA = {
|
||||
"blocks": [
|
||||
_b("A", "기슭막이", [
|
||||
{"kind": "labor", "key": "L1", "qty": 0.5, "name": "보통인부"},
|
||||
{"kind": "material", "key": "M1", "qty": 2, "name": "토목용부직포"},
|
||||
{"kind": "labor", "key": "L1", "qty": 0.5, "name": "보통인부", "own": LABOR},
|
||||
{"kind": "material", "key": "M1", "qty": 2, "name": "토목용부직포", "own": None},
|
||||
]),
|
||||
_b("B", "겹 대가", [
|
||||
{"kind": "labor", "key": "L1", "qty": 0.1, "name": "보통인부"},
|
||||
{"kind": "labor", "key": "L1", "qty": 0.1, "name": "보통인부", "own": LABOR},
|
||||
{"kind": "daega", "key": "A", "qty": 1, "name": "기슭막이"},
|
||||
]),
|
||||
_b("C", "아는 줄만", [{"kind": "labor", "key": "L1", "qty": 1, "name": "보통인부"}]),
|
||||
_b("C", "아는 줄만", [{"kind": "labor", "key": "L1", "qty": 1, "name": "보통인부", "own": LABOR}]),
|
||||
],
|
||||
"owners": {"pipe@100.0": "A", "pipe@200.0": "C"},
|
||||
} # fmt: skip
|
||||
|
||||
@@ -112,8 +112,7 @@ APPLIED = [
|
||||
("B08_Quantity/B08_Quantity_Material_Tables.ts", 'mergeSame: ["name"]'),
|
||||
("B08_Quantity/B08_Quantity_Material_Tables.ts", 'mergeSame: ["group"]'),
|
||||
("B08_Quantity/B08_Quantity_Material_Tables.ts", "mergeSame: built.mergeSame"),
|
||||
("B09_Estimation/B09_Estimation_UI_Tabs_Base.ts", 'mergeSame: ["part"]'),
|
||||
("B09_Estimation/B09_Estimation_UI_Tabs_Base.ts", 'level: blockLevel("name"'),
|
||||
# 841-1 환율및기초자료 · 중기사용료 표 걷음(「—」 한 줄) — Tabs_Base 병합 · 레벨 줄 없음
|
||||
("B09_Estimation/B09_Estimation_UI_Tabs_Labor.ts", 'level: blockLevel("name"'),
|
||||
("B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts", 'level: blockLevel("text"'),
|
||||
("B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts", 'mergeSame: ["kind"]'),
|
||||
|
||||
@@ -114,17 +114,14 @@ console.log(JSON.stringify({ a, b, moves, tipRefresh, tipRow, tipBlank, selector
|
||||
|
||||
|
||||
def test_배지_조건_서버_book_stale():
|
||||
keys = ["EQ000004"] # 불도저 · 조종원 · 경유(test_144 와 같은 견본)
|
||||
keys = ["MT000401", "MT000338", "LB000002"] # 841-1 책 = 자재만(노임 키는 안 셈)
|
||||
book = json.loads(json.dumps(prices.refresh_book(keys), ensure_ascii=False))
|
||||
assert prices.book_stale(book, keys) is None # 방금 굳힌 책 = 업데이트 필요 없음
|
||||
assert prices.book_stale(None, keys) == "단가 아직 안 굳힘"
|
||||
first, *rest = list(book["rows"])
|
||||
old = {**book, "rows": {k: book["rows"][k] for k in rest}, "fx": []}
|
||||
old["rows"][rest[0]] = {**old["rows"][rest[0]], "price": -1}
|
||||
old["driver_factor"] = {"num": 1, "den": 1, "text": "1/1"}
|
||||
assert prices.book_stale(old, keys) == (
|
||||
"새 단가 줄 1 · M01 에서 바뀐 단가 1 · 운전사 계수 바뀜 · 환율 바뀜"
|
||||
)
|
||||
old = {**book, "rows": {k: book["rows"][k] for k in rest}}
|
||||
old["rows"][rest[0]] = {**old["rows"][rest[0]], "prices": {}}
|
||||
assert prices.book_stale(old, keys) == "새 단가 줄 1 · M01 에서 바뀐 단가 1"
|
||||
router = (ROOT / "B09_Estimation/B09_Estimation_Router.py").read_text("utf-8")
|
||||
assert '"stale": await asyncio.to_thread(prices.book_stale, book, keys),' in router
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@ from B09_Estimation import B09_Estimation_Store as store_b09 # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Linked import linked_resource_keys # noqa: E402
|
||||
from resources.tester.test_204_2_b09_bill_library import COMBO, world # noqa: E402, F401
|
||||
|
||||
GONE = "LB999999" # M01 에 없는 키(원본 지워짐)
|
||||
|
||||
|
||||
def _json(book):
|
||||
return json.loads(json.dumps(book, ensure_ascii=False))
|
||||
@@ -68,18 +66,19 @@ def test_지운_복사_항목_업데이트_뒤_안_살아남(world, monkeypatch)
|
||||
|
||||
|
||||
def test_M01_원본_없는_항목_값_그대로_낡음_까닭_빠짐():
|
||||
keys = ["EQ000004", GONE] # 불도저 · 조종원 · 경유(test_144 견본) + 원본 없는 키
|
||||
old = _json(prices.refresh_book(["EQ000004"]))
|
||||
kept = {"key": GONE, "code": "9999", "name": "지워진 직종", "price": 123.0}
|
||||
old["rows"][GONE] = kept
|
||||
old["rows"]["LB000048"] = {**old["rows"]["LB000048"], "price": 1} # M01 에서 바뀐 단가
|
||||
gone = "MT999999" # 841-1 책 = 자재만 — 원본 없는 자재 키
|
||||
keys = ["MT000401", "MT000338", gone]
|
||||
old = _json(prices.refresh_book(["MT000401", "MT000338"]))
|
||||
kept = {"key": gone, "name": "지워진 자재", "origin": "M01", "prices": {"물가자료": 123.0}}
|
||||
old["rows"][gone] = kept
|
||||
old["rows"]["MT000401"] = {**old["rows"]["MT000401"], "prices": {}} # M01 에서 바뀐 단가
|
||||
assert prices.book_stale(old, keys) == "M01 에서 바뀐 단가 1" # 원본 없는 줄 = 까닭에 없음
|
||||
new = _json(prices.refresh_book(keys, old))
|
||||
assert new["rows"][GONE] == kept # 지우지도 바꾸지도 않음
|
||||
assert new["rows"]["LB000048"]["price"] not in (None, 1) # 나머지 = 새로 굳힘
|
||||
assert new["rows"][gone] == kept # 지우지도 바꾸지도 않음
|
||||
assert new["rows"]["MT000401"]["prices"] != {} # 나머지 = 새로 굳힘
|
||||
assert set(new["rows"]) == set(old["rows"])
|
||||
assert prices.book_stale(new, keys) is None
|
||||
assert GONE not in prices.refresh_book(keys)["rows"] # 옛 책 없으면 줄 없음(옛 길)
|
||||
assert gone not in prices.refresh_book(keys)["rows"] # 옛 책 없으면 줄 없음(옛 길)
|
||||
|
||||
|
||||
def test_화면_업데이트_캐시_보냄_그대로_둠():
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""702-2 · 702-4 B09 일위대가 고친 칸 덮개 — 사용자 값 `daega_cover`
|
||||
{블록 cover: {줄 cid: {qty · labor · material · expense}}}.
|
||||
{블록 cover: {줄 cid: {qty}}}(841-1 단가 칸 걷음 · 옛 단가 칸은 안 읽고 저장 때 버림).
|
||||
|
||||
- 덮개 열쇠 = 블록 `cover` · 줄 `cid`(`B09_Estimation_Labor_Source` · 내용 해시 밖)
|
||||
- 셈 = `B09_Estimation_Calc_Labor` 파이썬 · TS 짝(거울) — 고친 칸만 셈 값 대신 · `base` = 품셈 값
|
||||
@@ -42,7 +42,7 @@ def _covered_excel():
|
||||
return daega
|
||||
|
||||
|
||||
#: DG9 찰붙임 석공 품 0.1 → 0.12 · DG2 % 2 → 3 · DG14 산근 SG10 노무 단가 손 · 짝 잃은 둘
|
||||
#: DG9 찰붙임 석공 품 0.1 → 0.12 · DG2 % 2 → 3 · DG14 산근 SG10 노무 단가(841-1 안 읽음) · 짝 잃은 둘
|
||||
COVER = {
|
||||
"T:DG9": {"labor:L00005#0": {"qty": 0.12}},
|
||||
"T:DG2": {"pct:공구손료#3": {"qty": 3}},
|
||||
@@ -72,8 +72,7 @@ def test_고친_칸만_셈값_대신():
|
||||
pct = _line(out, "DG2", 4) # % 줄 = 끝에 셈(차례 4)
|
||||
assert pct["qty"] == 3 and pct["base"] == {"qty": 2} and pct["covered"] == ["qty"]
|
||||
sg = _line(out, "DG14", 2)
|
||||
assert sg["prices"]["labor"] == 20000 and sg["base"]["labor"] == 18084
|
||||
assert sg["covered"] == ["labor"] and sg["prices"]["material"] == 2498
|
||||
assert sg["prices"]["labor"] == 18084 and "covered" not in sg # 841-1 단가 칸 = 안 읽음
|
||||
assert out["daega_orphans"] == [
|
||||
{"cover": "T:없는블록", "cid": "labor:L00017#0"},
|
||||
{"cover": "T:DG1", "cid": "labor:없는줄#9"},
|
||||
@@ -81,14 +80,14 @@ def test_고친_칸만_셈값_대신():
|
||||
assert plain["daega_orphans"] == []
|
||||
|
||||
|
||||
def test_덮개가_빈칸을_채움():
|
||||
def test_단가_칸_덮개는_빈칸을_안_채움():
|
||||
"""841-1 단가 = M02 사본 값 — 단가 칸 덮개로 빈 단가를 채우지 않음(빈칸 그대로)."""
|
||||
daega = _covered_excel()
|
||||
sheets = {**b145.SHEETS, "labor": b145.SHEETS["labor"][:2]} # 석공 노임 없음
|
||||
bare = compute_labor(daega, sheets, b145.SANGEUN)
|
||||
assert _line(bare, "DG9", 0)["amount"] is None
|
||||
out = compute_labor(daega, sheets, b145.SANGEUN, {"T:DG9": {"labor:L00005#0": {"labor": 1e5}}})
|
||||
assert _line(out, "DG9", 0)["amount"] == 10000.0
|
||||
assert not [g for g in out["gaps"] if "찰붙임" in g["name"]]
|
||||
daega["blocks"][3]["lines"][0].pop("own") # 석공 노임 없음
|
||||
out = compute_labor(
|
||||
daega, b145.SHEETS, b145.SANGEUN, {"T:DG9": {"labor:L00005#0": {"labor": 1e5}}}
|
||||
)
|
||||
assert _line(out, "DG9", 0)["amount"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", ["cover", "none"])
|
||||
@@ -101,23 +100,19 @@ def test_거울_파이썬_TS(case):
|
||||
assert b145._same(daega_price(py, "pipe@100.0"), ts["price"])
|
||||
|
||||
|
||||
def test_노임_새_판_덮개_칸_그대로():
|
||||
"""702-4 — 바탕 단가가 새 판으로 바뀌어도 고친 칸 = 그대로 · 안 고친 칸만 새 값."""
|
||||
def test_새_단가_덮개_품_그대로():
|
||||
"""702-4 — 사본 단가가 새로 바뀌어도([단가 갱신]) 고친 품 = 그대로 · 단가는 새 값."""
|
||||
daega = _covered_excel()
|
||||
cover = {"T:DG9": {"labor:L00005#0": {"qty": 0.12}, "labor:L00017#1": {"labor": 170000}}}
|
||||
cover = {"T:DG9": {"labor:L00005#0": {"qty": 0.12}}}
|
||||
old = compute_labor(daega, b145.SHEETS, b145.SANGEUN, cover)
|
||||
new_sheets = {
|
||||
**b145.SHEETS,
|
||||
"labor": [{**r, "price": round(r["price"] * 1.05)} for r in b145.SHEETS["labor"]],
|
||||
}
|
||||
new = compute_labor(daega, new_sheets, b145.SANGEUN, cover)
|
||||
for ln in daega["blocks"][3]["lines"]:
|
||||
if ln["kind"] == "labor":
|
||||
ln["own"] = {"labor": round(ln["own"]["labor"] * 1.05)}
|
||||
new = compute_labor(daega, b145.SHEETS, b145.SANGEUN, cover)
|
||||
a, b = _line(old, "DG9", 0), _line(new, "DG9", 0)
|
||||
assert a["qty"] == b["qty"] == 0.12 # 고친 품 그대로
|
||||
assert b["prices"]["labor"] == round(258935 * 1.05) != a["prices"]["labor"] # 안 고친 단가 = 새
|
||||
assert b["base"]["labor"] == b["prices"]["labor"] # 품셈 값 곁 = 새 판 값
|
||||
c, d = _line(old, "DG9", 1), _line(new, "DG9", 1)
|
||||
assert c["prices"]["labor"] == d["prices"]["labor"] == 170000 # 고친 단가 그대로
|
||||
assert d["qty"] == 0.03 and d["base"]["labor"] == round(165545 * 1.05)
|
||||
assert b["prices"]["labor"] == round(258935 * 1.05) != a["prices"]["labor"] # 단가 = 새
|
||||
assert b["base"]["labor"] == b["prices"]["labor"] # 품셈 값 곁 = 새 값
|
||||
assert new["daega_orphans"] == []
|
||||
|
||||
|
||||
@@ -205,17 +200,15 @@ def test_저장_다듬기():
|
||||
"P:d": {"x#0": {"expense": float("inf"), "material": 3}},
|
||||
}
|
||||
}
|
||||
assert store.clean_user(raw)["daega_cover"] == {
|
||||
"S:a": {"labor:L1#0": {"qty": 0.5}},
|
||||
"P:d": {"x#0": {"material": 3.0}},
|
||||
}
|
||||
assert store.clean_user(raw)["daega_cover"] == {"S:a": {"labor:L1#0": {"qty": 0.5}}}
|
||||
assert "daega_cover" not in store.clean_user({"daega_cover": {"S:a": {}}})
|
||||
|
||||
|
||||
def test_저장_뒤_서버_값(tmp_path):
|
||||
"""[저장](write_user → read_user) 뒤 서버 셈 = 화면 짝 셈(덮개 입력값만 받고 다시 셈)."""
|
||||
stored = store.write_user(tmp_path, {"daega_cover": COVER})
|
||||
assert store.read_user(tmp_path)["daega_cover"] == stored["daega_cover"] == COVER
|
||||
kept = {k: v for k, v in COVER.items() if k != "T:DG14"} # 단가 칸만 든 덮개 = 버림(841-1)
|
||||
assert store.read_user(tmp_path)["daega_cover"] == stored["daega_cover"] == kept
|
||||
daega = _covered_excel()
|
||||
server = compute_labor(daega, b145.SHEETS, b145.SANGEUN, stored["daega_cover"])
|
||||
screen = b145._ts(daega, b145.SHEETS, b145.SANGEUN, COVER)["out"]
|
||||
|
||||
@@ -16,7 +16,8 @@ from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Refresh as refresh # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
|
||||
|
||||
KEYS = ["LB000001", "LB000033"] # 작업반장(건설업) · 석공
|
||||
#: 841-1 책 = 자재만(노임 키 = 노임 판만 셈) — 원목 · 미송각재 + 작업반장(건설업)
|
||||
KEYS = ["MT000401", "MT000338", "LB000001"]
|
||||
|
||||
|
||||
def test_책_견줌_미리보기_수_적용_수_같음(tmp_path, monkeypatch):
|
||||
@@ -24,8 +25,8 @@ def test_책_견줌_미리보기_수_적용_수_같음(tmp_path, monkeypatch):
|
||||
new = prices.refresh_book(KEYS)
|
||||
assert new["labor_edition"] == prices.labor_editions(KEYS) and new["labor_edition"]
|
||||
old = json.loads(json.dumps(new))
|
||||
old["rows"]["LB000001"] = {**old["rows"]["LB000001"], "price": 1} # 옛 판 값
|
||||
del old["rows"]["LB000033"] # 새 줄
|
||||
old["rows"]["MT000401"] = {**old["rows"]["MT000401"], "prices": {}} # 옛 값
|
||||
del old["rows"]["MT000338"] # 새 줄
|
||||
old["labor_edition"] = {"건설업": "2025-09-01"}
|
||||
cover = {
|
||||
"S:a": {"x#0": {"qty": 1, "labor": 2}},
|
||||
@@ -40,12 +41,7 @@ def test_책_견줌_미리보기_수_적용_수_같음(tmp_path, monkeypatch):
|
||||
assert got["노임판"]["후"].startswith("건설업 2026")
|
||||
old.pop("labor_edition")
|
||||
assert refresh.preview(tmp_path, old, KEYS, None)["노임판"]["전"].startswith("굳힌 날 ")
|
||||
assert prices.book_diff(after, prices.refresh_book(KEYS, after)) == {
|
||||
"new": 0,
|
||||
"changed": 0,
|
||||
"factor": False,
|
||||
"fx": False,
|
||||
}
|
||||
assert prices.book_diff(after, prices.refresh_book(KEYS, after)) == {"new": 0, "changed": 0}
|
||||
assert prices.book_stale(after, KEYS) is None
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
견본 = `test_145_2_b09_labor_calc`(엑셀 바탕 표 · 사본 꼴) · `test_m02_labor_calc_mirror`(TS 헬퍼).
|
||||
"""
|
||||
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
@@ -24,10 +23,10 @@ from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs # noqa: E40
|
||||
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_Item_Snapshot as snapshots # 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):
|
||||
@@ -144,7 +143,7 @@ def _fixed_blocks():
|
||||
"""조합 블록 — 노무 줄 + 고정 줄 넷(이은 노임 · 이은 관급 자재 · 이은 중기 · 손 단가)."""
|
||||
own = {"labor": 100.0, "material": 200.0, "expense": 300.0}
|
||||
lines = [
|
||||
{"kind": "labor", "key": "L00017", "name": "보통인부", "qty": 0.1},
|
||||
{"kind": "labor", "key": "L00017", "name": "보통인부", "qty": 0.1, "own": {"labor": 165545}},
|
||||
{"kind": "fixed", "name": "석공", "qty": 0.2, "own": own,
|
||||
"link": {"labor": "L00005", "material": None, "machine": None}},
|
||||
{"kind": "fixed", "name": "파형강관", "qty": 1, "own": own,
|
||||
@@ -166,11 +165,13 @@ def _line(out, name):
|
||||
return next(ln for ln in out["daega_blocks"][0]["lines"] if ln["name"] == name)
|
||||
|
||||
|
||||
def test_B09_고정_줄_단가_이음과_손_단가():
|
||||
def test_B09_고정_줄_단가_M02_셈_그대로():
|
||||
"""841-1 고정 줄 단가 = M02 엔진 고정 줄 셈(`own` · 이은 노임 · 자재 = 사본 묶음 값) — B09 단가 책
|
||||
값으로 다시 안 덮음 · 이은 자재가 관급 · 별도계상이면 대가 안 0원."""
|
||||
out = compute_labor(_fixed_blocks(), b145.SHEETS, {})
|
||||
assert _line(out, "석공")["prices"] == {"labor": 258935, "material": 200, "expense": 300}
|
||||
assert _line(out, "석공")["prices"] == {"labor": 100, "material": 200, "expense": 300}
|
||||
assert _line(out, "파형강관")["prices"]["material"] == 0 # 관급 = 대가 안 0원
|
||||
assert _line(out, "굴착기")["prices"] == {"labor": 47231, "material": 7784, "expense": 9994}
|
||||
assert _line(out, "굴착기")["prices"] == {"labor": 100, "material": 200, "expense": 300}
|
||||
misc = _line(out, "잡재료")
|
||||
assert misc["prices"] == {"labor": 0, "material": 1234.5, "expense": 0}
|
||||
assert misc["amounts"]["material"] == 2469.0
|
||||
@@ -179,25 +180,13 @@ def test_B09_고정_줄_단가_이음과_손_단가():
|
||||
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}}}
|
||||
def test_B09_덮개는_품만_고정_줄에_먹음():
|
||||
cover = {"S:f": {"fixed:잡재료#0": {"qty": 3, "material": 1000}}} # 단가 칸 = 841-1 안 읽음
|
||||
out = compute_labor(_fixed_blocks(), b145.SHEETS, {}, cover)
|
||||
misc = _line(out, "잡재료")
|
||||
assert misc["qty"] == 3 and misc["prices"]["material"] == 1000
|
||||
assert misc["qty"] == 3 and misc["prices"]["material"] == 1234.5
|
||||
assert misc["base"] == {"qty": 2, "labor": 0, "material": 1234.5, "expense": 0}
|
||||
assert out["daega_orphans"] == []
|
||||
assert misc["covered"] == ["qty"] and out["daega_orphans"] == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cover", [None, {"S:f": {"fixed:석공#0": {"labor": 1}}}])
|
||||
@@ -252,8 +241,9 @@ def test_한_바퀴_사본_고정_줄_원가_빈칸_0_집계표(tmp_path):
|
||||
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
|
||||
bundle = next(snapshots.structures_dir(root).rglob("단가.json")).read_text(encoding="utf-8")
|
||||
whole = item_prices.load_prices(json.loads(bundle))
|
||||
assert stone["prices"]["labor"] == float(whole.get(LB)["값"]) # M02 고정 줄 셈 = 묶음 노임
|
||||
assert sheets["bill"]["totals"]["direct"]["blank"] == 0 # 원가계산서 빈 줄 0
|
||||
assert sheets["bill"]["sums"]["material"] > 0
|
||||
summary = compute_summary(sheets, hopyo_lines(sheets))
|
||||
@@ -262,21 +252,17 @@ def test_한_바퀴_사본_고정_줄_원가_빈칸_0_집계표(tmp_path):
|
||||
assert summary["fixed"] == 1 # 손 단가 줄 = 집계에서 빠짐 1
|
||||
|
||||
|
||||
def test_사본_재적용_업데이트_고정_줄_그대로(tmp_path):
|
||||
"""[업데이트](703-3) = 단가 책만 새로 — 고정 줄 품 · 손 단가 그대로 · 이은 단가만 새 판."""
|
||||
def test_사본_다시_읽어도_고정_줄_그대로(tmp_path):
|
||||
"""[재생성] 꼴 = 사본 다시 읽음 — 고정 줄 품 · 손 단가 · 이은 단가(M02 셈) 그대로."""
|
||||
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]}, {})
|
||||
first = compute_labor(daega_inputs(root), b145.SHEETS, {})
|
||||
second = compute_labor(daega_inputs(root), b145.SHEETS, {})
|
||||
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[0]["prices"] == b[0]["prices"] and a[0]["prices"]["labor"] > 1 # 묶음 노임
|
||||
assert a[1]["prices"] == b[1]["prices"] == {"labor": 0, "material": 1000, "expense": 0}
|
||||
|
||||
@@ -121,7 +121,7 @@ def test_엑셀과_줄마다_같음(data, name):
|
||||
def test_단가표_엑셀과_같음(data, name):
|
||||
mine = st.read_prices("a.STC", data[name])
|
||||
excel = ip.read_prices(EXCEL[name].name, EXCEL[name].read_bytes())
|
||||
for kind in ("labor", "material", "machine"):
|
||||
for kind in ip.KINDS: # 841-1 엑셀 단가표 = 자재만
|
||||
keys = ("name", "spec", "unit", "price", "labor", "material", "expense")
|
||||
rows = [sorted(tuple(r.get(k) for k in keys) for r in got[kind]) for got in (mine, excel)]
|
||||
assert rows[0] == rows[1], kind
|
||||
|
||||
@@ -35,7 +35,8 @@ FILES = {
|
||||
#: 영월 = 내역 줄이 코드로만 가리킴(번 채움) · STmate 하위 호표 반올림 끝수 28 줄
|
||||
#: (줄당 1 원/단위 · 브레인 정함 = 그래도 이음)
|
||||
WANT = {
|
||||
"거창": (14, 68, 56, 12, 11, 8, 8, 384916227, 0, 0),
|
||||
# 841-1 이은 안내판 줄 = M02 사본 셈(엑셀보다 50,000 적음 · 노임 · 기계 덮개 걷음)
|
||||
"거창": (14, 68, 56, 12, 11, 8, 8, 384916227, 0, 50000),
|
||||
"대흥": (34, 171, 143, 28, 0, 26, 45, 710359136, 0, 0),
|
||||
"소광": (25, 117, 98, 19, 0, 0, 0, 173311386, 0, 0),
|
||||
"영월": (23, 138, 135, 3, 0, 21, 34, 767148700, 28, 2951),
|
||||
@@ -107,7 +108,8 @@ def test_실무_엑셀_통째_빠짐_0_합계_같음_층_쓰기_0(world, name):
|
||||
assert step["빠짐"] == 0 and step["실패"] == [] and step["사본"] == copies
|
||||
assert got["알림"]["이음"] == billed
|
||||
assert got["합계"]["엑셀직접"] == direct and got["합계"]["우리직접"] == direct - gap
|
||||
assert [x["까닭"] for x in got["어긋남"]] == ["끝수"] * tail
|
||||
ours = "이음 단가 = 우리 셈" # 841-1 이은 줄 = M02 사본 값(엑셀 값으로 안 덮음)
|
||||
assert [x["까닭"] for x in got["어긋남"] if x["까닭"] != ours] == ["끝수"] * tail
|
||||
libs = _libs(got["root"])
|
||||
assert len(libs) == copies
|
||||
assert all(re.fullmatch(rf"precombo@project@-@{got['file_key']}-\d+", x) for x in libs)
|
||||
@@ -143,7 +145,7 @@ def test_다시_부르기_같은_사본_두_번_셈_0(world):
|
||||
first = world["run"]("거창")
|
||||
again = world["run"]("거창", fresh=False)
|
||||
assert _libs(again["root"]) == _libs(first["root"])
|
||||
assert again["합계"]["우리직접"] == first["합계"]["우리직접"] == 384916227
|
||||
assert again["합계"]["우리직접"] == first["합계"]["우리직접"] == 384866227
|
||||
rows = again["user"]["bill"]["rows"]
|
||||
assert len({r["key"] for r in rows}) == len(rows)
|
||||
|
||||
@@ -153,7 +155,7 @@ def test_원본_파일_일위대가_요율_까닭(world, name):
|
||||
got = world["run"](name)
|
||||
assert "같은 공사 엑셀에서" in got["단계"]["일위대가"]["까닭"]
|
||||
assert "같은 공사 엑셀에서" in got["단계"]["요율"]["까닭"]
|
||||
assert "labor" in got["단계"]["단가표"]
|
||||
assert "material" in got["단계"]["단가표"] # 841-1 자재만
|
||||
assert got["합계"]["우리직접"] == got["합계"]["엑셀직접"]
|
||||
assert _libs(got["root"]) == []
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ if str(ROOT) not in sys.path:
|
||||
from B09_Estimation import B09_Estimation_Import_Prices as ip # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Import_Rates as ir # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import compute_base # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Rates import rate_table # noqa: E402
|
||||
|
||||
K = ROOT / "resources" / "knowledge" / "original" / "실무문서"
|
||||
@@ -85,8 +84,8 @@ def test_707_3_단가표_읽기():
|
||||
for tag in FILES:
|
||||
got = ip.read_prices(*_file(tag))
|
||||
counts[tag] = tuple(len(got[k]) for k in ip.KINDS)
|
||||
assert counts == {"거창": (17, 52, 22), "영월": (16, 55, 29), "대흥": (12, 39, 25),
|
||||
"소광": (14, 35, 24)} # fmt: skip
|
||||
# 841-1 자재만(노임 · 기계 단가표 걷음 — 노무 · 기계 단가 = M02 사본 값)
|
||||
assert counts == {"거창": (52,), "영월": (55,), "대흥": (39,), "소광": (35,)}
|
||||
|
||||
|
||||
BASE = {
|
||||
@@ -147,28 +146,15 @@ def test_707_3_이음_규칙():
|
||||
user = {"labor_cover": {"LB2": 1}, "materials": {"MO1": {"pick": "유가", "supply": "관급"}}}
|
||||
out = ip.propose(READ, BASE, user)
|
||||
patch = out["patch"]
|
||||
assert patch["labor_cover"] == {"LB1": 258935} # 같은 값 = 옛 덮개 걷음
|
||||
assert patch["machine_cover"] == {"EQ1": {"labor": 10, "material": 10, "expense": 10}}
|
||||
assert set(patch) == {"materials"} # 841-1 노임 · 기계 덮개 걷음
|
||||
assert patch["materials"]["MO1"] == {"supply": "관급", "applied": 1473} # pick 뺌
|
||||
assert "MT1" not in patch["materials"] # 「(관급)」 이름 다름 = 짝 없음
|
||||
assert "굴삭기(무한궤도) 0.2㎥(짝 여럿)" in out["missing"]["machine"]
|
||||
assert out["counts"]["machine"] == {"read": 5, "linked": 1, "covered": 1, "missing": 4,
|
||||
"book": 2} # fmt: skip
|
||||
assert out["counts"]["labor"]["linked"] == 2 and out["missing"]["labor"] == ["벌 목 부"]
|
||||
assert set(out["counts"]) == {"material"} and out["counts"]["material"]["linked"] == 1
|
||||
|
||||
|
||||
def test_707_3_덮개_새_판_뒤_남음(tmp_path):
|
||||
"""[업데이트] = 단가 책만 새로 굳힘 — 사용자 값의 노임 · 기계 덮개는 그대로 · 셈은 덮개."""
|
||||
cover = {"labor_cover": {"LB1": 258935, "": None},
|
||||
"machine_cover": {"EQ1": {"labor": 10, "material": 10, "expense": 10},
|
||||
"EQ2": {"labor": 1}}} # fmt: skip
|
||||
def test_841_1_노임_기계_덮개_저장_안_함(tmp_path):
|
||||
"""841-1 노임 · 기계 단가 덮개 걷음 — 옛 사용자 값은 저장 때 버림(노무 · 기계 = M02 사본 값)."""
|
||||
cover = {"labor_cover": {"LB1": 258935},
|
||||
"machine_cover": {"EQ1": {"labor": 10, "material": 10, "expense": 10}}} # fmt: skip
|
||||
saved = store.write_user(tmp_path, cover)
|
||||
assert saved["labor_cover"] == {"LB1": 258935} # 빈 값 버림
|
||||
assert saved["machine_cover"] == {"EQ1": {"labor": 10, "material": 10, "expense": 10}}
|
||||
for price in (268908, 280000): # 옛 판 → 새 판 책
|
||||
store.write_book(tmp_path, {"rows": {"LB1": {"key": "LB1", "price": price}}})
|
||||
user = store.read_user(tmp_path)
|
||||
assert user["labor_cover"] == saved["labor_cover"]
|
||||
inputs = {"labor": [{"key": "LB1", "name": "석공", "price": price}]}
|
||||
row = compute_base(inputs, user)["labor"][0]
|
||||
assert (row["price"], row["book"], row["covered"]) == (258935, price, True)
|
||||
assert "labor_cover" not in saved and "machine_cover" not in saved
|
||||
|
||||
@@ -237,9 +237,9 @@ def test_내_자료집_SK_왕복_같은_금액(world, tmp_path):
|
||||
doc = made["문서"]
|
||||
assert doc["종류"] == "단가산출" and doc["바탕"] == {"로직": PCT}
|
||||
assert doc["덮개"]["labor:LB000036#0"] == {"qty": 0.9}
|
||||
assert doc["덮개"]["labor:LB000002#0"] == {"labor": 210000}
|
||||
assert "labor:LB000002#0" not in doc["덮개"] # 841-1 단가 칸 = 안 읽음 · 안 담음
|
||||
assert doc["더한줄"][0]["요소"] == "LB000002"
|
||||
assert made["담음"] == {"품": 1, "단가": 1} and made["안담음"] == {"품": 0, "단가": 0}
|
||||
assert made["담음"]["품"] == 1 and made["담음"]["단가"] == 0
|
||||
third = block["lines"][2]["cid"] # 더한줄 인력 = 그 더한줄 수량
|
||||
cover = {block["cover"]: {first: {"qty": 0.9}, third: {"qty": 0.3}}}
|
||||
doc = personal.build(root, block["cover"], cover, "내 SK 2")["문서"]
|
||||
|
||||
@@ -24,7 +24,7 @@ from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Router as router_mod # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Sangeun as sangeun # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import compute_base, rounddown # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import rounddown # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Sangeun_Cover import cover_sangeun, line_id # noqa: E402
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
@@ -47,8 +47,7 @@ def _inputs():
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def sg():
|
||||
sheets = compute_base(_inputs(), {})
|
||||
return sangeun.collect([], sheets, sangeun.hand_sources(_user()))
|
||||
return sangeun.collect([], sangeun.hand_sources(_user())) # 841-1 단가 = M02 엔진
|
||||
|
||||
|
||||
def _block(got, code):
|
||||
@@ -89,7 +88,7 @@ def _same(a, b):
|
||||
|
||||
|
||||
def _cover(sg):
|
||||
"""품 · 단가 · 하위 산근 · 율 · 중기 단가 고침 한 벌 + 짝 잃은 칸 둘."""
|
||||
"""품 · 하위 산근 · 율 고침 한 벌(841-1 단가 칸은 안 읽음 — 함께 넣어 버려짐을 봄) + 짝 잃은 칸 둘."""
|
||||
sub = _block(sg, "GF000155")
|
||||
n_l, labor = _at(sub, "labor")
|
||||
people = _block(sg, "GC001018")
|
||||
@@ -98,15 +97,12 @@ def _cover(sg):
|
||||
net = _block(sg, "GC000337")
|
||||
n_m, machine = _at(net, "machine")
|
||||
n_t, mat = _at(net, "material")
|
||||
parent = _block(sg, "GF000169")
|
||||
n_r, remicon = _at(parent, "material") # 단가 빈칸(대비표에 없음) → 덮개로 채움
|
||||
return {
|
||||
parent["key"]: {line_id(n_r, remicon): {"material": 90000}},
|
||||
sub["key"]: {line_id(n_l, labor): {"qty": labor["qty"] * 2}},
|
||||
people["key"]: {line_id(n_p, first): {"labor": 300000}, line_id(n_x, _extra): {"qty": 5}},
|
||||
net["key"]: {
|
||||
line_id(n_m, machine): {"expense": 20000},
|
||||
line_id(n_t, mat): {"material": 4500},
|
||||
line_id(n_m, machine): {"qty": machine["qty"] * 1.5, "expense": 20000},
|
||||
line_id(n_t, mat): {"qty": mat["qty"] + 1, "material": 4500},
|
||||
},
|
||||
"SG:없는블록": {"0:x": {"qty": 1}},
|
||||
net["key"] + "x": {"0:다른이름": {"qty": 1}},
|
||||
@@ -122,27 +118,23 @@ def test_고침_없음_그대로(sg):
|
||||
]
|
||||
|
||||
|
||||
def test_품_단가_고침_줄_금액_총계(sg):
|
||||
def test_품_고침_줄_금액_총계_단가_칸_안_읽음(sg):
|
||||
before = copy.deepcopy(sg)
|
||||
got = cover_sangeun(sg, _cover(sg))
|
||||
assert sg == before # 받은 답 안 고침
|
||||
net0, net = _block(sg, "GC000337"), _block(got, "GC000337")
|
||||
n, m = _at(net, "machine")
|
||||
assert (
|
||||
m["covered"] == ["expense"]
|
||||
and m["base"]["expense"] == _at(net0, "machine")[1]["price"]["expense"]
|
||||
)
|
||||
assert (
|
||||
m["expense"] == rounddown(20000 * m["qty"], 1)
|
||||
and m["labor"] == _at(net0, "machine")[1]["labor"]
|
||||
)
|
||||
_n, t = _at(net, "material")
|
||||
assert t["material"] == rounddown(4500 * t["qty"], 1)
|
||||
m0 = _at(net0, "machine")[1]
|
||||
assert m["covered"] == ["qty"] and m["base"] == {"qty": m0["qty"]}
|
||||
assert m["price"] == m0["price"] # 단가 칸(경비 20,000) = 안 읽음
|
||||
assert m["expense"] == rounddown(m0["price"]["expense"] * m["qty"], 1)
|
||||
for part in ("labor", "material", "expense"):
|
||||
assert net[part] == rounddown(sum(ln[part] for ln in net["lines"]), 0)
|
||||
assert net["sum"] == net["labor"] + net["material"] + net["expense"] != net0["sum"]
|
||||
row = next(r for r in got["sg_list"] if r["key"] == net["key"])
|
||||
assert row["sum"] == net["sum"] and row["covered"] == 2
|
||||
people = _block(got, "GC001018")
|
||||
assert "covered" not in _at(people, "labor")[1] # 단가만 든 칸 = 고침 없음
|
||||
untouched = _block(got, "GF000103")
|
||||
assert untouched == _block(sg, "GF000103") # 덮개 안 닿은 블록 = 서버 값
|
||||
|
||||
@@ -154,8 +146,6 @@ def test_하위_산근_이어짐_덧줄_비례(sg):
|
||||
assert line["price"]["labor"] == sub["labor"] != _block(sg, "GF000155")["labor"]
|
||||
assert line["labor"] == rounddown(sub["labor"] * line["qty"], 1)
|
||||
assert parent["labor"] > _block(sg, "GF000169")["labor"]
|
||||
assert _block(sg, "GF000169")["sum"] is None and parent["sum"] is not None # 빈칸 채움
|
||||
assert parent["why"] is None and parent["key"] not in {g["key"] for g in got["sg_gaps"]}
|
||||
people0, people = _block(sg, "GC001018"), _block(got, "GC001018")
|
||||
n, extra = _at(people, "extra")
|
||||
assert extra["of"] == "people" and extra["qty"] == 5
|
||||
@@ -184,16 +174,21 @@ def test_거울_파이썬_TS(sg):
|
||||
def test_저장_다듬기_남음(sg):
|
||||
cover = _cover(sg)
|
||||
clean = store.clean_user({"sg_cover": cover})["sg_cover"]
|
||||
assert clean == cover
|
||||
want = {
|
||||
k: {line: {"qty": c["qty"]} for line, c in lines.items() if "qty" in c}
|
||||
for k, lines in cover.items()
|
||||
}
|
||||
assert clean == {k: v for k, v in want.items() if v} # 841-1 품만 남음
|
||||
|
||||
|
||||
def test_덮개_대가_원가계산서_업데이트_뒤_지킴(sg, tmp_path):
|
||||
inputs = _inputs()
|
||||
plain = router_mod.compute_sheets(tmp_path, copy.deepcopy(inputs), _user())
|
||||
net = _block(sg, "GC000337")
|
||||
n, mat = _at(net, "material")
|
||||
mine = {"materials": {mat["ref"]["key"]: {"supply": "사급"}}} # 841-1 기본 별도계상 끔
|
||||
plain = router_mod.compute_sheets(tmp_path, copy.deepcopy(inputs), {**_user(), **mine})
|
||||
cover = {net["key"]: {line_id(n, mat): {"qty": mat["qty"] + 1}}}
|
||||
covered = router_mod.compute_sheets(tmp_path, copy.deepcopy(inputs), _user(cover))
|
||||
covered = router_mod.compute_sheets(tmp_path, copy.deepcopy(inputs), {**_user(cover), **mine})
|
||||
gap = rounddown(rounddown(mat["price"]["material"] * (mat["qty"] + 1), 1) - mat["material"], 0)
|
||||
row0 = next(r for r in plain["sg_list"] if r["key"] == net["key"])
|
||||
row1 = next(r for r in covered["sg_list"] if r["key"] == net["key"])
|
||||
@@ -210,7 +205,7 @@ def test_덮개_대가_원가계산서_업데이트_뒤_지킴(sg, tmp_path):
|
||||
for k in ("price", "applied", "lowest"):
|
||||
if isinstance(row.get(k), (int, float)):
|
||||
row[k] = row[k] * 2
|
||||
again = router_mod.compute_sheets(tmp_path, newer, _user(cover))
|
||||
again = router_mod.compute_sheets(tmp_path, newer, {**_user(cover), **mine})
|
||||
line = _block(again, "GC000337")["lines"][n]
|
||||
assert line["covered"] == ["qty"] and line["qty"] == mat["qty"] + 1
|
||||
assert line["base"]["qty"] == mat["qty"] and again["sg_orphans"] == []
|
||||
|
||||
@@ -31,7 +31,7 @@ IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
|
||||
@pytest.fixture(scope="module")
|
||||
def made():
|
||||
sheets = compute_base(_inputs(), {})
|
||||
return sheets, sangeun.collect([], sheets, sangeun.hand_sources(_user()))
|
||||
return sheets, sangeun.collect([], sangeun.hand_sources(_user())) # 841-1 엔진 단가
|
||||
|
||||
|
||||
def _ts(sg, machines):
|
||||
@@ -91,8 +91,9 @@ def test_화면_묶음_옮김_금액_그대로(made):
|
||||
assert set(bundles) == {"인력", "재료", "경비"}
|
||||
equip = next(x for x in bundles["경비"]["줄"] if "몫" in x)
|
||||
assert len(equip["몫"]) == 3 # 장비 한 줄 = 노 · 재 · 경
|
||||
unfold = equip["몫"][0]["줄"]["펼침"]
|
||||
assert unfold["단위"] == "시간" and len(unfold["줄"]) == 4 # 손료 · 운전사 · 연료 · 잡품
|
||||
assert not equip["몫"][0]["줄"].get(
|
||||
"펼침"
|
||||
) # 841-1 중기사용료(손료 · 운전사 …) 걷음 = 펼침 없음
|
||||
parent = next(b for b in sg["sg"] if b["logic"] == "GF000169")
|
||||
sub = {x["묶음"]: x for x in got[parent["key"]]["bundles"]}["하위"]
|
||||
assert all("산근" in x["이름"] and "호표" in x["이름"] for x in sub["줄"])
|
||||
|
||||
@@ -44,7 +44,8 @@ def _doc(hand=None, mat=None):
|
||||
"이름": "시험 SC",
|
||||
"단위": "㎡",
|
||||
"담은로직": [{**_fix("갑", 1000), **q(hand)}, {**_fix("을", 2000), **q(hand)}],
|
||||
"자재": [{"코드": MAT, "수량": {"손": mat} if mat is not None else None}],
|
||||
# 기준단가 = M02 엔진 자재 단가 자리(841-1 · 견본 whole 은 이름만이라 줄에 얹음)
|
||||
"자재": [{"코드": MAT, "수량": {"손": mat} if mat is not None else None, "기준단가": 500}],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -268,11 +268,13 @@ BLOCK = {
|
||||
"id": "DGR", "cover": "S:r", "kind": "조합", "name": "율 블록", "spec": "", "unit": "㎥",
|
||||
"cite": "", "origin": {},
|
||||
"lines": [
|
||||
{"kind": "labor", "key": "L1", "name": "보통인부", "qty": 0.5},
|
||||
# 841-1 줄 단가 = M02 엔진(`own`) · 고정 줄 = M02 고정 줄 셈(이은 노임 100,000 그대로)
|
||||
{"kind": "labor", "key": "L1", "name": "보통인부", "qty": 0.5, "own": {"labor": 172068}},
|
||||
{"kind": "fixed", "name": "특별인부", "qty": 0.2, "own": {"labor": 100000.0},
|
||||
"link": {"labor": "L9", "material": None, "machine": None}},
|
||||
{"kind": "machine", "key": "E1", "name": "굴착기", "qty": 0.29},
|
||||
{"kind": "material", "key": "M1", "name": "시멘트", "qty": 3},
|
||||
{"kind": "machine", "key": "E1", "name": "굴착기", "qty": 0.29,
|
||||
"own": {"labor": 30000, "material": 5000, "expense": 70000}},
|
||||
{"kind": "material", "key": "M1", "name": "시멘트", "qty": 3, "own": {"material": 5000}},
|
||||
{"kind": "pct", "name": "공구손료", "part": "expense", "base": "bundle:labor", "qty": 2},
|
||||
{"kind": "pct", "name": "율 위 율", "part": "material", "base": "rows:3,4", "qty": 10},
|
||||
{"kind": "pct", "name": "장비 율", "part": "expense", "base": "bundle:machine", "qty": 1},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""792-3 · 792-4 엔진 새 의미 다섯 길 지킴 시험 — 같은 로직 JSON 이
|
||||
① 엔진 직접(`master_cut.engine`) ·
|
||||
② M02 단가산출 SK 셈(`M02_Sangeun_Calc.run`) · ③ 일위대가 SC 한 줄(수량 1 · `M02_Labor_Doc.calc`) ·
|
||||
④ B09 산근 풀이(`B09_Estimation_Sangeun.Solver` · 단가 = 같은 값의 B09 목록표) ·
|
||||
④ B09 산근 풀이(`B09_Estimation_Sangeun.Solver` · 841-1 단가 = M02 엔진) ·
|
||||
⑤ B09 일위대가 대가 블록(SC → 굳힌 SK 참조 수량 1 · `Labor_Source.Builder` → `Calc_Labor` 파이썬 ·
|
||||
TS 짝 · 내역 단가 `daega_price`)에서 같은 금액.
|
||||
실무 견본(봉화 D00102 · 북부청임도 18)은 793-2 길(가져온 식 SK · 임시 층)로 만들어 ⑤ = 엑셀.
|
||||
@@ -103,18 +103,7 @@ def _b09_master(master, row):
|
||||
def _b09(build, row) -> dict:
|
||||
master, row, _ = build()
|
||||
master = _b09_master(master, row)
|
||||
files = master.index
|
||||
machine = next((x["값묶음"] for x in row["호표"] if "값묶음" in x), None)
|
||||
sheets = {
|
||||
"labor": [{"key": k, "price": float(r["값"])} for k, r in files.get("LB", {}).items()],
|
||||
"compare": [
|
||||
{"key": k, "applied": float(r["물가자료"])} for k, r in files.get("MT", {}).items()
|
||||
],
|
||||
"machine": [{"key": EQ, **{p: float(machine[c]) for p, c in PART.items() if c in machine}}]
|
||||
if machine
|
||||
else [],
|
||||
}
|
||||
block = sg.Solver(sheets).add(master, {"code": _key(row), "given": {}}, None)
|
||||
block = sg.Solver().add(master, {"code": _key(row), "given": {}}, None) # 841-1 엔진 단가
|
||||
assert block["why"] is None, block["why"]
|
||||
texts = [x for ln in block["lines"] for x in ln.get("text") or []]
|
||||
assert not [x for x in texts if "못" in x or "빈칸" in x], texts # 산근 글도 같은 셈
|
||||
|
||||
@@ -29,8 +29,6 @@ from resources.tester.test_795_1_sk_copy import _changed, _fake, _live
|
||||
from resources.tester.test_795_2_sc_copy import LB, PCT, _double, _labor
|
||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||
|
||||
MAN = "LB000002"
|
||||
|
||||
|
||||
def _json(value):
|
||||
return json.loads(json.dumps(value, ensure_ascii=False, default=str))
|
||||
@@ -92,29 +90,37 @@ def test_copy_rows_order_and_sk_origin():
|
||||
assert item_prices.collect(["GF000219"], over=[]) == item_prices.collect(["GF000219"])
|
||||
|
||||
|
||||
#: 841-1 단가 책 = 자재만 — 원목 · 미송각재
|
||||
WOOD, BAR = "MT000401", "MT000338"
|
||||
|
||||
|
||||
def _row(key, price):
|
||||
row = copy.deepcopy(prices._row(_live(), key))
|
||||
row["값"] = price
|
||||
row["물가자료"] = price
|
||||
return item_prices.to_json(row)
|
||||
|
||||
|
||||
def _price(book_row):
|
||||
return book_row["prices"]["물가자료"]
|
||||
|
||||
|
||||
def test_book_new_rows_use_copy_basis():
|
||||
"""③ [저장] 새 줄 = 사본 기준 시점 값 · 없는 자원 = 지금 값 + 표지 · 옛 책 줄 그대로."""
|
||||
frozen = {LB: _row(LB, 111)}
|
||||
first = _json(prices.extend_book(None, [LB], frozen))
|
||||
assert first["rows"][LB]["price"] == 111 and first["now_keys"] == []
|
||||
both = _json(prices.extend_book(first, [LB, MAN], frozen))
|
||||
assert both["rows"][LB] == first["rows"][LB]
|
||||
assert both["rows"][MAN]["price"] == float(prices._row(_live(), MAN)["값"])
|
||||
assert both["now_keys"] == [MAN]
|
||||
inputs = prices.build_inputs(None, [LB, MAN], None, frozen)
|
||||
assert [r["price"] for r in inputs["labor"]] == [111, both["rows"][MAN]["price"]]
|
||||
assert inputs["now"] == [MAN] and prices.build_inputs(both, [LB, MAN])["now"] == [MAN]
|
||||
old = _json(prices.refresh_book([LB, MAN])) # 옛 책(사본 규칙 전) = 줄 · 값 그대로
|
||||
again = _json(prices.extend_book(old, [LB, MAN], frozen))
|
||||
frozen = {WOOD: _row(WOOD, 111)}
|
||||
first = _json(prices.extend_book(None, [WOOD], frozen))
|
||||
assert _price(first["rows"][WOOD]) == 111 and first["now_keys"] == []
|
||||
both = _json(prices.extend_book(first, [WOOD, BAR], frozen))
|
||||
assert both["rows"][WOOD] == first["rows"][WOOD]
|
||||
assert _price(both["rows"][BAR]) == float(prices._row(_live(), BAR)["물가자료"])
|
||||
assert both["now_keys"] == [BAR]
|
||||
inputs = prices.build_inputs(None, [WOOD, BAR], None, frozen)
|
||||
assert [_price(r) for r in inputs["materials"]] == [111, _price(both["rows"][BAR])]
|
||||
assert inputs["now"] == [BAR] and prices.build_inputs(both, [WOOD, BAR])["now"] == [BAR]
|
||||
old = _json(prices.refresh_book([WOOD, BAR])) # 옛 책(사본 규칙 전) = 줄 · 값 그대로
|
||||
again = _json(prices.extend_book(old, [WOOD, BAR], frozen))
|
||||
assert again["rows"] == old["rows"] and again["now_keys"] == []
|
||||
fresh = _json(prices.refresh_book([LB, MAN], both)) # [단가 갱신] = 지금 값 · 표지 없음
|
||||
assert fresh["rows"][LB]["price"] != 111 and fresh["now_keys"] == []
|
||||
fresh = _json(prices.refresh_book([WOOD, BAR], both)) # [단가 갱신] = 지금 값 · 표지 없음
|
||||
assert _price(fresh["rows"][WOOD]) != 111 and fresh["now_keys"] == []
|
||||
|
||||
|
||||
def test_frozen_rows_from_linked_copy(tmp_path):
|
||||
@@ -165,15 +171,16 @@ def test_price_refresh_default_regen_separate(monkeypatch, tmp_path):
|
||||
|
||||
|
||||
def test_price_preview_shape():
|
||||
"""⑥ 미리 보기 — 바뀐 자원(전 · 후) · 총괄 전 → 후 · 사본 말 없음."""
|
||||
old = _json(prices.refresh_book([LB, MAN]))
|
||||
old["rows"][LB] = {**old["rows"][LB], "price": 1}
|
||||
new = _json(prices.refresh_book([LB, MAN], old))
|
||||
"""⑥ 미리 보기 — 바뀐 자원(사본 자원 줄 먼저 · 자재 단가 책) · 총괄 전 → 후."""
|
||||
old = _json(prices.refresh_book([WOOD, BAR]))
|
||||
old["rows"][WOOD] = {**old["rows"][WOOD], "prices": {"물가자료": 1}}
|
||||
new = _json(prices.refresh_book([WOOD, BAR], old))
|
||||
sheets = lambda g: {"total": {"subtotals": {"grand": g}}} # noqa: E731
|
||||
got = refresh.price_preview(old, new, sheets(10), sheets(20), 3)
|
||||
copies = [{"키": LB, "이름": "사본 노임", "전": 1, "후": 2}]
|
||||
got = refresh.price_preview(old, new, sheets(10), sheets(20), 3, copies)
|
||||
assert got["할일"] == "단가갱신" and (got["전"], got["후"], got["고친칸"]) == (10, 20, 3)
|
||||
assert [(r["키"], r["전"]) for r in got["바뀐자원"]] == [(LB, 1)]
|
||||
assert got["바뀐자원"][0]["후"] == new["rows"][LB]["price"]
|
||||
assert [(r["키"], r["전"]) for r in got["바뀐자원"]] == [(LB, 1), (WOOD, {"물가자료": 1})]
|
||||
assert got["바뀐자원"][1]["후"] == new["rows"][WOOD]["prices"]
|
||||
|
||||
|
||||
def test_migrate_view_b09_copy(tmp_path):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""B09 [저장] 속도 — 같은 결과 범위 안 다시 셈 줄임 지킴.
|
||||
|
||||
- 산근 두 번 셈(183-1) = 1차가 본 단가(있음 · 값)가 같을 때만 1차 풀이 그대로(`_Prices.same`)
|
||||
- 산근 두 번 셈(183-1) = 1차 풀이 그대로(841-1 산근 단가 = M02 엔진 · 바탕 표 안 읽음 → 늘 같음)
|
||||
- M01 `steady()` 범위 = 파일 자국 한 번 · `stale` 뒤 다시 셈 · 범위 밖은 늘 셈
|
||||
- B08 `once()` 범위 = 채집석 공제가 자재 집계표 답을 다시 씀(셈 한 번)
|
||||
"""
|
||||
@@ -15,35 +15,15 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity import B08_Quantity_Router_Material as material # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Sangeun import _Prices # noqa: E402
|
||||
from B09_Estimation import B09_Estimation_Sangeun as sg # noqa: E402
|
||||
from M01_MasterData import M01_MasterData_Store_Cache as cache # noqa: E402
|
||||
|
||||
SHEETS = {
|
||||
"labor": [{"key": "L1", "price": 100}, {"key": "L2", "price": 5}],
|
||||
"compare": [{"key": "M1", "applied": 3}],
|
||||
"machine": [{"key": "E1", "labor": 1, "material": 2, "expense": 3}],
|
||||
}
|
||||
|
||||
|
||||
def _seen() -> _Prices:
|
||||
got = _Prices(SHEETS)
|
||||
got.labor.get("L1")
|
||||
assert "M9" not in got.material
|
||||
got.machine.get("E1")
|
||||
return got
|
||||
|
||||
|
||||
def test_산근_같은_단가면_다시_씀() -> None:
|
||||
other = {**SHEETS, "labor": [{"key": "L1", "price": 100}, {"key": "L2", "price": 999}]}
|
||||
assert _seen().same(_Prices({**other, "compare": []})) # 안 본 L2 · M1 은 안 셈
|
||||
|
||||
|
||||
def test_산근_본_단가_바뀌면_다시_셈() -> None:
|
||||
assert not _seen().same(_Prices({**SHEETS, "labor": [{"key": "L1", "price": 101}]}))
|
||||
added = {**SHEETS, "compare": [{"key": "M9", "applied": 1}]}
|
||||
assert not _seen().same(_Prices(added)) # 없던 자재가 생김
|
||||
machine = {**SHEETS, "machine": [{"key": "E1", "labor": 1, "material": 2, "expense": 4}]}
|
||||
assert not _seen().same(_Prices(machine))
|
||||
def test_산근_두_번_셈_앞_풀이_그대로() -> None:
|
||||
memo = {"sg": {"sg": [{"key": "SG:x"}], "sg_list": [], "sg_gaps": [], "sg_notes": [],
|
||||
"sg_hand": {}}} # fmt: skip
|
||||
got = sg.collect([("s1", ROOT / "없는 사본")], None, memo)
|
||||
assert got == memo["sg"] and got is not memo["sg"] # 사본(깊은 복사)
|
||||
|
||||
|
||||
def test_M01_범위_안_자국_한_번(monkeypatch) -> None:
|
||||
|
||||
Reference in New Issue
Block a user