Merge remote-tracking branch 'origin/dev' into sub_laptop_2
This commit is contained in:
@@ -133,7 +133,7 @@ def engine(master, ref: str, got: dict, depth: int = 0) -> dict:
|
||||
# 중간 = 덧줄이 본 값(버린 줄 합 · 덧줄 값 · `줄`) — 시험 계산 중간 칸
|
||||
return {**got, "줄": lines, **costs, "계": sum(costs.values()), "중간": env}
|
||||
costs = settle(logic, total)
|
||||
more = {"단계": ms.steps(logic, lines)}
|
||||
more = {"단계": ms.steps(logic, lines), "셈법": ms.rule(logic)}
|
||||
return {**got, "줄": lines, **costs, "계": sum(costs.values()), "중간": env, **more}
|
||||
|
||||
|
||||
@@ -150,17 +150,41 @@ def settle(logic: dict, total: dict) -> dict:
|
||||
return {k: down(v, 0) for k, v in costs.items()}
|
||||
|
||||
|
||||
def plain(got: dict) -> dict:
|
||||
def kept(got: dict) -> dict:
|
||||
"""`run_line` 이 엔진 답에서 그대로 넘기는 덧칸(792-3) — 단계 · 셈법."""
|
||||
return {k: got[k] for k in ("단계", "셈법") if k in got}
|
||||
|
||||
|
||||
def plain(got: dict, gone=()) -> dict:
|
||||
"""로직 줄 답(`run_line` 꼴 `{기준, 줄, 재료}`)을 줄만 다시 버림 —
|
||||
켜쌓기율 곱 · 별도계상 뺀 뒤 · 기준 = 남은 줄 비목 합 ROUNDDOWN 0."""
|
||||
켜쌓기율 곱 · 별도계상 뺀 뒤(`gone` = 뺄 줄 차례) · 기준 = 남은 줄 비목 합 ROUNDDOWN 0 ·
|
||||
답 `셈법`(791-1 새 의미 로직) = 줄 자리 · 합밖 · 단계 · 블록 끝전 · 조정 그대로(`settle`)."""
|
||||
if not got.get("기준"):
|
||||
return got
|
||||
rule = got.get("셈법")
|
||||
places = [x.get("자리") for x in ms.items(rule)] if rule else []
|
||||
skip = set(gone) | (ms.skipped(rule) if rule else set())
|
||||
|
||||
def at(n):
|
||||
return places[n] if isinstance(n, int) and 0 <= n < len(places) else None
|
||||
|
||||
lines, total = [], dict.fromkeys(COSTS, Decimal(0))
|
||||
for line in got.get("줄") or []:
|
||||
cut, money = _parts(dict(line.get("비목") or {}))
|
||||
for n, line in enumerate(got.get("줄") or []):
|
||||
cut, money = _parts(dict(line.get("비목") or {}), at(n))
|
||||
for k, v in cut.items():
|
||||
total[k] += Decimal(str(v or 0))
|
||||
total[k] += 0 if n in skip else Decimal(str(v or 0))
|
||||
lines.append({**line, "비목": cut, "금액": money})
|
||||
mats = [{**m, "금액": down(m.get("금액"), 1)} for m in got.get("재료") or []]
|
||||
base = {k: down(v, 0) for k, v in total.items()}
|
||||
return {**got, "줄": lines, "재료": mats, "기준": {**base, "계": sum(base.values())}}
|
||||
mats = [{**m, "금액": _line(m.get("금액"), at(m.get("차례")))} for m in got.get("재료") or []]
|
||||
kept = [x for n, x in enumerate(lines) if n not in gone]
|
||||
if not rule:
|
||||
base = {k: down(v, 0) for k, v in total.items()}
|
||||
return {**got, "줄": kept, "재료": mats, "기준": {**base, "계": sum(base.values())}}
|
||||
base = settle(rule, total)
|
||||
steps = ms.steps(rule, [{} if n in gone else x for n, x in enumerate(lines)])
|
||||
return {
|
||||
**got,
|
||||
"줄": kept,
|
||||
"재료": mats,
|
||||
"기준": {**base, "계": sum(base.values())},
|
||||
"단계": steps,
|
||||
}
|
||||
|
||||
@@ -33,6 +33,17 @@ def uses(row: dict) -> bool:
|
||||
return any(k in row for k in ROW_KEYS) or any(k in x for x in items(row) for k in LINE_KEYS)
|
||||
|
||||
|
||||
def rule(row: dict) -> dict:
|
||||
"""셈법(792-3) — 새 의미 칸만 뗀 작은 로직 꼴(이 모듈 · `master_cut.settle` 이 읽음) ·
|
||||
엔진 답 `셈법` 으로 실어 켜쌓기율 · 별도계상 뒤 다시 버림(`master_cut.plain`)이 씀."""
|
||||
keep = ("이름", *LINE_KEYS)
|
||||
lines = {
|
||||
k: [{x: one[x] for x in keep if x in one} for one in row.get(k, [])]
|
||||
for k in ("호표", "덧줄")
|
||||
}
|
||||
return {**lines, **{k: row[k] for k in (*ROW_KEYS, "끝수") if k in row}}
|
||||
|
||||
|
||||
def shown(step: dict, value):
|
||||
"""중간 `표시` = 값을 n 자리 반올림해 뒤 줄에 넘김(STmate `X = 식 =`)."""
|
||||
if step.get("표시") is None or not isinstance(value, Decimal):
|
||||
|
||||
@@ -22,6 +22,7 @@ if str(ROOT) not in sys.path:
|
||||
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
|
||||
@@ -163,13 +164,13 @@ def test_작업량_Q는_소수_둘():
|
||||
|
||||
whole = m01.loaded()[1]
|
||||
for code, row in sorted((whole.index.get("GF") or {}).items()):
|
||||
if not any(sg._Q.fullmatch(s["이름"]) for s in row.get("중간", [])):
|
||||
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 sg._Q.fullmatch(s["이름"]))
|
||||
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
|
||||
|
||||
@@ -25,6 +25,7 @@ SRC = os.path.join(HERE, "..", "..", "M02_MasterTemplete")
|
||||
MIX = "GF000156" # 기계비빔타설 — 호표 3 시멘트 · 4 모래 · 5 자갈
|
||||
GIVEN = {"재료배합": "(A)", "골재최대치수": 40}
|
||||
MAT = "MT000008"
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
DIRECT = {"별도계상": "직접계산"}
|
||||
|
||||
|
||||
@@ -36,6 +37,15 @@ def _rows(got):
|
||||
return {x["이름"]: {**x, "수량": float(x["수량"])} for x in got["줄"]}
|
||||
|
||||
|
||||
def _kept(raw, names=None):
|
||||
"""별도계상 뺀 기준(751-3 끝수) = 남은 줄 비목 합마다 원 버림 · 계 = 셋 합 ·
|
||||
`names` = 뺄 재료 이름(없으면 재료 전부)."""
|
||||
gone = {m["차례"] for m in raw["재료"] if names is None or m["이름"] in names}
|
||||
lines = [x for n, x in enumerate(raw["줄"]) if n not in gone]
|
||||
out = {k: int(sum(Decimal(str(x["비목"].get(k) or 0)) for x in lines)) for k in COSTS}
|
||||
return {**out, "계": sum(out.values())}
|
||||
|
||||
|
||||
# ── ① 단위 ──────────────────────────────────────────────────────────
|
||||
def test_단위중량_칸에_t_값이면_1000배_0이면_표_위끝():
|
||||
wrong = labor.run_line(_whole(), MIX, {**GIVEN, "모래단위중량": 2, "자갈단위중량": 2.1})
|
||||
@@ -60,16 +70,14 @@ def _calc(shown=None, mats=()):
|
||||
|
||||
def test_로직_자재_기본_별도계상은_기준_줄에서_빠지고_재료엔_남음():
|
||||
raw = labor.run_line(_whole(), MIX, GIVEN)
|
||||
cut = sum(Decimal(str(m["금액"])) for m in raw["재료"])
|
||||
one = _calc()["줄"][0]
|
||||
names = {x["이름"] for x in one["줄"]}
|
||||
assert not names & {"시멘트", "모래(건조상태)", "자갈 또는 부순돌"}
|
||||
assert {"콘크리트공", "보통인부"} <= names
|
||||
assert len(one["재료"]) == 3 # 자재목록(세 칸 고침)엔 남음
|
||||
want = Decimal(str(raw["기준"]["재료비"])) - cut
|
||||
assert one["기준"]["재료비"] == pytest.approx(float(want))
|
||||
assert one["기준"]["계"] == pytest.approx(float(Decimal(str(raw["기준"]["계"])) - cut))
|
||||
assert one["기준"]["노무비"] == pytest.approx(float(raw["기준"]["노무비"]))
|
||||
want = _kept(raw)
|
||||
assert {k: one["기준"][k] for k in (*COSTS, "계")} == want
|
||||
assert one["기준"]["노무비"] == raw["기준"]["노무비"]
|
||||
|
||||
|
||||
def test_직접계산은_그대로_섞이면_그_자재만_빠짐():
|
||||
@@ -80,9 +88,8 @@ def test_직접계산은_그대로_섞이면_그_자재만_빠짐():
|
||||
part = _calc({"3": DIRECT})["줄"][0] # 시멘트만 셈
|
||||
names = {x["이름"] for x in part["줄"]}
|
||||
assert "시멘트" in names and "모래(건조상태)" not in names
|
||||
cement = next(m for m in raw["재료"] if m["이름"] == "시멘트")
|
||||
others = sum(Decimal(str(m["금액"])) for m in raw["재료"]) - Decimal(str(cement["금액"]))
|
||||
assert part["기준"]["계"] == pytest.approx(float(Decimal(str(raw["기준"]["계"])) - others))
|
||||
others = {m["이름"] for m in raw["재료"]} - {"시멘트"}
|
||||
assert part["기준"]["계"] == _kept(raw, others)["계"]
|
||||
|
||||
|
||||
def test_사용자_자재_별도계상은_합에서_빠지고_반영은_보임():
|
||||
@@ -106,7 +113,6 @@ def test_사용자_자재_별도계상_파이썬과_TS_같은_답(mark):
|
||||
|
||||
def test_사전조합_안_줄도_별도계상_뺌():
|
||||
raw = labor.run_line(_whole(), MIX, GIVEN)
|
||||
cut = sum(Decimal(str(m["금액"])) for m in raw["재료"])
|
||||
doc = {
|
||||
"이름": "비빔 조합",
|
||||
"단위": "㎥",
|
||||
@@ -120,8 +126,8 @@ def test_사전조합_안_줄도_별도계상_뺌():
|
||||
assert [x.get("자재") for x in got["안"]] == [None]
|
||||
pill = got["안"][0]
|
||||
times = Decimal(1) # 기계비빔타설 알약 배 1
|
||||
assert pill["기준"]["계"] * times == pytest.approx(Decimal(str(raw["기준"]["계"])) - cut)
|
||||
assert got["기준"]["계"] == pytest.approx(Decimal(str(raw["기준"]["계"])) - cut)
|
||||
assert pill["기준"]["계"] * times == _kept(raw)["계"]
|
||||
assert got["기준"]["계"] == _kept(raw)["계"]
|
||||
|
||||
|
||||
def test_화면_별도계상_바꾸면_다시_셈():
|
||||
|
||||
@@ -324,7 +324,7 @@ def test_검사():
|
||||
|
||||
|
||||
# ── ③ 실무 손 옮김 견본 넷 ─────────────────────────────────────────────
|
||||
def test_봉화_D00102_합판거푸집_STmate_번지_율():
|
||||
def 봉화():
|
||||
"""봉화 단가산출근거 2909~2943 행 — `&M00081& * 1.03 * 0.327 :=A01` · `&PA01M:M& * 11'%'` ·
|
||||
`&PA02L:S& * 1'%'` · `(=)` · `(==)` · 총계 45,652 · 34,119 · 11,192 · 341."""
|
||||
row = _logic(
|
||||
@@ -358,16 +358,21 @@ def test_봉화_D00102_합판거푸집_STmate_번지_율():
|
||||
{"LB990101": ("형틀목공", 274978), "LB990102": ("보통인부", 165545)},
|
||||
{"MT990101": ("합판(내수)", "㎡", 11468), "MT990102": ("각재", "㎥", 500661)},
|
||||
)
|
||||
return master, row, {"노무비": 34119, "재료비": 11192, "경비": 341, "계": 45652}
|
||||
|
||||
|
||||
def test_봉화_D00102_합판거푸집_STmate_번지_율():
|
||||
master, row, want = 봉화()
|
||||
assert mf.check_logic(master, row)[0] == []
|
||||
got = _calc(master, "GX990101")
|
||||
assert _money(got) == {"노무비": 34119, "재료비": 11192, "경비": 341, "계": 45652}
|
||||
assert _money(got) == want
|
||||
assert [x["금액"] for x in got["줄"]] == [
|
||||
D(x) for x in ("3862.5", "6221.2", "27497.8", "6621.8", "1109.2", "341.1")
|
||||
]
|
||||
assert [x["계"] for x in got["단계"]] == [D("11192.9"), D("34460.7"), D("45653.6")]
|
||||
|
||||
|
||||
def test_울진소광_D01489_낙석방지책_STmate_L퍼센트():
|
||||
def 소광():
|
||||
"""울진소광 32 호표(D01489) — `'공구손료 :'&S01045& * 2'%'`(S01045 단위 L% = 처음부터
|
||||
노무비 누계) · `L = Q/10 =` 표시 2 · 계 줄 420,734 · 182,223 · 230,359 · 8,152
|
||||
(계약단가 87.745 % 는 B09 몫)."""
|
||||
@@ -412,15 +417,20 @@ def test_울진소광_D01489_낙석방지책_STmate_L퍼센트():
|
||||
},
|
||||
{"MT990103": ("낙석방지책", "경간", 227000)},
|
||||
)
|
||||
return master, row, {"노무비": 182223, "재료비": 230359, "경비": 8152, "계": 420734}
|
||||
|
||||
|
||||
def test_울진소광_D01489_낙석방지책_STmate_L퍼센트():
|
||||
master, row, want = 소광()
|
||||
assert mf.check_logic(master, row)[0] == []
|
||||
got = _calc(master, "GX990102")
|
||||
assert _money(got) == {"노무비": 182223, "재료비": 230359, "경비": 8152, "계": 420734}
|
||||
assert _money(got) == want
|
||||
assert _line(got, "공구손료")["금액"] == D("3644.4")
|
||||
assert _step(got, "지주 계")["계"] == D("49824.2")
|
||||
assert _step(got, "합계")["계"] == D("420735.3")
|
||||
|
||||
|
||||
def test_동부청영월_D00003_터파기_STmate_큰나머지_끝전():
|
||||
def 영월():
|
||||
"""동부청영월 단가산출근거 54~103 행(D00003) — `f = 1/1.3 =` · `E=…=` ·
|
||||
`Q = 3600*q*K*f*E/Cm =`(표시 2) · `&X00205L/M/S& / Q * 0.9` · 계 원 버림 + 큰 나머지
|
||||
5,471 · 4,764 · 310 · 397."""
|
||||
@@ -446,16 +456,21 @@ def test_동부청영월_D00003_터파기_STmate_큰나머지_끝전():
|
||||
끝전="큰나머지",
|
||||
)
|
||||
master = _master([row, _HOUR], {"LB990106": ("보통인부", 165545)})
|
||||
return master, row, {"노무비": 4764, "재료비": 310, "경비": 397, "계": 5471}
|
||||
|
||||
|
||||
def test_동부청영월_D00003_터파기_STmate_큰나머지_끝전():
|
||||
master, row, want = 영월()
|
||||
assert mf.check_logic(master, row)[0] == []
|
||||
got = _calc(master, "GX990103")
|
||||
assert got["중간"]["Q"] == D("52.39")
|
||||
assert _money(got) == {"노무비": 4764, "재료비": 310, "경비": 397, "계": 5471}
|
||||
assert _money(got) == want
|
||||
assert _step(got, "계")["계"] == D("5471.0")
|
||||
row["끝전"] = "비목버림" # 비목 원 버림이면 1 원 모자람
|
||||
assert _calc(master, "GX990103")["계"] == 5470
|
||||
|
||||
|
||||
def test_북부청임도_18_문양거푸집_EST_줄_자리():
|
||||
def 임도():
|
||||
"""북부청임도 단가산출 18 번(문양거푸집) — `5,786.1 × 2% = 116`(그 줄만 반올림 0) ·
|
||||
`인부소계 : 24,467.3` · 노무비 24,467 · 재료비 5,902 · 계 30,369."""
|
||||
row = _logic(
|
||||
@@ -480,11 +495,16 @@ def test_북부청임도_18_문양거푸집_EST_줄_자리():
|
||||
{"LB990107": ("형틀목공", 275790), "LB990108": ("보통인부", 172068)},
|
||||
{"MT990104": ("문양거푸집(ABS코팅폼)", "㎡", 4166)},
|
||||
)
|
||||
return master, row, {"노무비": 24467, "재료비": 5902, "경비": 0, "계": 30369}
|
||||
|
||||
|
||||
def test_북부청임도_18_문양거푸집_EST_줄_자리():
|
||||
master, row, want = 임도()
|
||||
assert mf.check_logic(master, row)[0] == []
|
||||
got = _calc(master, "GX990104")
|
||||
assert _line(got, "잡재료비")["금액"] == 116
|
||||
assert _step(got, "인부소계")["계"] == D("24467.3")
|
||||
assert _money(got) == {"노무비": 24467, "재료비": 5902, "경비": 0, "계": 30369}
|
||||
assert _money(got) == want
|
||||
del row["덧줄"][0]["자리"] # 기본 0.1 버림이면 엑셀과 1 원 다름
|
||||
assert _calc(master, "GX990104")["계"] == 30368
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
"""792-3 엔진 새 의미 네 길 지킴 시험 — 같은 로직 JSON 이 ① 엔진 직접(`master_cut.engine`) ·
|
||||
② M02 단가산출 SK 셈(`M02_Sangeun_Calc.run`) · ③ 일위대가 SC 한 줄(수량 1 · `M02_Labor_Doc.calc`) ·
|
||||
④ B09 산근 풀이(`B09_Estimation_Sangeun.Solver` · 단가 = 같은 값의 B09 목록표)에서 같은 금액.
|
||||
|
||||
견본 = 791-1 실무 손 옮김 넷(봉화 D00102 · 울진소광 D01489 · 동부청영월 D00003 · 북부청임도 18 =
|
||||
엑셀 금액) + 합밖 · 제외 · 조정 단위 로직. 켜쌓기율 · 별도계상 뒤 다시 버림(`master_cut.plain`)도
|
||||
엔진 셈법(줄 자리 · 끝전 · 조정)을 지킴. 옛 로직(새 칸 없음) 전후 같음 = `test_791_1` M01 지문.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import importlib
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
import resources.tester.test_791_1_engine_semantics as t
|
||||
from B09_Estimation import B09_Estimation_Sangeun as sg
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as sc
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
|
||||
mcut = importlib.import_module("master_cut")
|
||||
D = Decimal
|
||||
COSTS = ("노무비", "재료비", "경비", "계")
|
||||
PART = {"labor": "노무비", "material": "재료비", "expense": "경비", "sum": "계"}
|
||||
EQ = "EQ990001"
|
||||
|
||||
|
||||
def _units():
|
||||
"""합밖 · 제외 단계 · 조정 · 큰나머지 · 줄 자리를 한 로직에(791-1 단위 견본 바탕)."""
|
||||
master = t._base(
|
||||
덧줄=[
|
||||
{"이름": "공구손료", "식": "단계.'인부 소계'.노무비 * 3 / 100", "비목": "경비"},
|
||||
{"이름": "잡", "식": "번지.A01.재료비 * 1.5 / 100", "비목": "재료비"},
|
||||
{"이름": "덧", "식": "0.6", "비목": "노무비", "자리": {"방법": "올림", "자리": 0}},
|
||||
],
|
||||
단계=[
|
||||
{"이름": "자재 소계", "단": 1, "줄": ["갑", "을"]},
|
||||
{"이름": "인부 소계", "단": 1, "줄": ["보통", "특별"]},
|
||||
{"이름": "경비 소계", "단": 1, "줄": ["공구손료"], "제외": True},
|
||||
],
|
||||
끝전="큰나머지",
|
||||
조정={"배율": {"재료비": D("0.9")}},
|
||||
)
|
||||
row = master.logic("GX990001")
|
||||
row["호표"][1]["합밖"] = True
|
||||
return master, row, None
|
||||
|
||||
|
||||
CASES = {"봉화": t.봉화, "소광": t.소광, "영월": t.영월, "임도": t.임도, "단위": _units}
|
||||
|
||||
|
||||
def _key(row) -> str:
|
||||
return row["키"]
|
||||
|
||||
|
||||
def _engine(master, row) -> dict:
|
||||
got = t._calc(master, _key(row))
|
||||
return {k: got[k] for k in COSTS}
|
||||
|
||||
|
||||
def _sk(master, row) -> dict:
|
||||
doc = {"키": "SK-990001", "이름": "시험", "단위": "식", "바탕": {"로직": _key(row)}}
|
||||
got = sc.run(sd.clean(doc, None, master), master)
|
||||
return {k: got["기준"][k] for k in COSTS}
|
||||
|
||||
|
||||
def _sc(master, row) -> dict:
|
||||
mats = {
|
||||
str(n): {"별도계상": "직접계산"} for n, x in enumerate(row["호표"]) if x["종류"] == "재료"
|
||||
}
|
||||
line = {"로직": _key(row), "입력값": {}, "수량": {"손": 1}, "자재표시": mats}
|
||||
got = labor.calc({"담은로직": [line], "자재": []}, {}, whole=master, pills=[])
|
||||
return {k: got["반영합"][k] for k in COSTS}
|
||||
|
||||
|
||||
def _b09_master(master, row):
|
||||
"""B09 = 기계 줄 단가를 중기목록표에서 — 기계경비 로직이 부르는 EQ 키를 잡게
|
||||
시간당 로직에 한 줄."""
|
||||
hour = copy.deepcopy(master.index["GX"].get("GX990009") or {})
|
||||
if hour:
|
||||
hour["호표"] = [t._mat(EQ, "1", "기계", 비목="경비")]
|
||||
master.index["GX"]["GX990009"] = hour
|
||||
master.index["EQ"] = {EQ: {"키": EQ, "이름": "시험 기계", "값": D(0)}}
|
||||
return master
|
||||
|
||||
|
||||
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)
|
||||
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 # 산근 글도 같은 셈
|
||||
assert block["foot"].endswith(f"= {block['sum']:,.0f}"), block["foot"]
|
||||
return {PART[p]: D(str(block[p])) for p in PART}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(CASES))
|
||||
def test_네_길_같은_금액(name):
|
||||
build = CASES[name]
|
||||
master, row, want = build()
|
||||
one = _engine(master, row)
|
||||
if want is not None:
|
||||
assert one == want # 엑셀 금액
|
||||
assert _sk(*build()[:2]) == one
|
||||
assert _sc(*build()[:2]) == one
|
||||
assert _b09(build, row) == one
|
||||
|
||||
|
||||
def test_단위_로직_새_의미가_금액을_바꿈():
|
||||
master, row, _ = _units()
|
||||
got = _engine(master, row)
|
||||
plain = copy.deepcopy(row)
|
||||
for k in ("끝전", "조정", "단계"):
|
||||
plain.pop(k)
|
||||
for x in (*plain["호표"], *plain["덧줄"]):
|
||||
for k in ("합밖", "자리", "번지"):
|
||||
x.pop(k, None)
|
||||
assert got != _engine(t._master([plain], t.LAB, t.MAT), plain) # 시험이 새 길을 거침
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["영월", "단위"])
|
||||
def test_다시_버림도_셈법_지킴(name):
|
||||
"""켜쌓기율 · 별도계상 뒤 다시 버림(`plain`) — 셈법이 있으면
|
||||
줄 자리 · 합밖 · 끝전 · 조정 그대로."""
|
||||
master, row, _ = CASES[name]()
|
||||
out = labor.run_line(master, _key(row), {})
|
||||
assert "셈법" in out and out["단계"]
|
||||
again = mcut.plain(out)
|
||||
assert again["기준"] == out["기준"]
|
||||
assert [x["계"] for x in again["단계"]] == [x["계"] for x in out["단계"]]
|
||||
old = mcut.plain({k: v for k, v in out.items() if k != "셈법"})
|
||||
if name == "영월":
|
||||
assert (
|
||||
old["기준"]["계"] == 5470 and again["기준"]["계"] == 5471
|
||||
) # 옛 다시 버림 = 1 원 모자람
|
||||
@@ -0,0 +1,183 @@
|
||||
"""793-3a 지킴 — 근거 제안 엔진(`M02_San_Basis*`) · 실무 여덟 파일 산근 459 블록.
|
||||
|
||||
판정 수 ≥ 분석 648-D2 합친 길(기초 짚음 260 · 옵션 전부 52 · 품 검산 맞음 141 · 없음 94) ·
|
||||
손 확인 견본(합판거푸집 넷 · 철근 Type-Ⅱ 가공 + 조립 · 레미콘 소량감률 50 · 터파기 #51 ·
|
||||
야면석소운반 GC001140 석재류 · 불량 · 큰돌쌓기 ÷10 근접) · 지어 넣은 값 0 · 보정 자료 = 여덟 파일 ·
|
||||
저장본 길(임시 폴더). 엑셀 = `helper_m02_hopyo_golden.FILES`(읽기만 · 없으면 skip) ·
|
||||
STmate 숨은 G 식 = 올림 길과 같은 `M02_San_Basis_Store.hidden`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
from collections import Counter
|
||||
from functools import lru_cache
|
||||
|
||||
import helper_m02_hopyo_golden as golden
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Judge as judge
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_San_Basis as sb
|
||||
from M02_MasterTemplete import M02_San_Basis_Const as bc
|
||||
from M02_MasterTemplete import M02_San_Basis_Index as bi
|
||||
from M02_MasterTemplete import M02_San_Basis_Store as store
|
||||
from M02_MasterTemplete import M02_San_Book as san_book
|
||||
|
||||
WAYS = {
|
||||
"B1",
|
||||
"B2",
|
||||
"B3",
|
||||
"B4",
|
||||
"B5",
|
||||
"B6",
|
||||
"V",
|
||||
"검산",
|
||||
"검산(같은 품 여럿)",
|
||||
"근접",
|
||||
"근접(같은 품 여럿)",
|
||||
}
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def books() -> list[tuple]:
|
||||
"""[(파일, HP 꼴 책 + 숨은식)] — 없는 파일이면 skip."""
|
||||
out = []
|
||||
for tag, path in golden.FILES.items():
|
||||
book = golden.read(tag) if path.exists() else None
|
||||
if book is None:
|
||||
pytest.skip(f"실무 엑셀 없음 {tag}")
|
||||
sheets = san_book.sheets(path.name, path.read_bytes()) if book["꼴"] == "STmate" else {}
|
||||
out.append((tag, {**book, "숨은식": store.hidden(book, sheets)}))
|
||||
return out
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def results() -> tuple[dict, float]:
|
||||
sb.ctx()
|
||||
t0 = time.time()
|
||||
got = {}
|
||||
for tag, book in books():
|
||||
for blk in book["단산"]:
|
||||
got[f"{tag}#{blk['번']}"] = store.one(book, int(blk["번"]))
|
||||
return got, time.time() - t0
|
||||
|
||||
|
||||
def test_counts_at_least_d2():
|
||||
got, sec = results()
|
||||
kind = Counter(r["꼴"] for r in got.values())
|
||||
tier = Counter(r["단"] for r in got.values())
|
||||
same = sum(
|
||||
1
|
||||
for r in got.values()
|
||||
if r["꼴"].startswith("확정")
|
||||
and "다름" not in (st := [sb.checked(s) for s in sb.chosen_units(r)])
|
||||
and "같음" in st
|
||||
)
|
||||
print(f"\n블록 {len(got)} · 꼴 {dict(kind)} · 단 {dict(tier)} · 품 같음 {same} · {sec:.1f} 초")
|
||||
assert len(got) == 459
|
||||
assert sum(v for k, v in kind.items() if k.startswith("확정")) >= 260
|
||||
assert kind["확정·전부"] >= 52
|
||||
assert same >= 141
|
||||
assert tier["없음"] <= 94 and kind["없음"] <= 94
|
||||
|
||||
|
||||
def _first(bid: str) -> dict:
|
||||
return results()[0][bid]["근거후보"]
|
||||
|
||||
|
||||
def test_plywood_four_same():
|
||||
for bid in ("북부청임도#24", "북부청사방#66", "거창#41", "동부청영월#1841"):
|
||||
(c,) = _first(bid)
|
||||
assert (c["기초"], c["단"], c["품검산"]) == ("GC000211", "확정", "같음"), bid
|
||||
assert {c["옵션"][k] for k in ("사용횟수", "유형")} == {"4회", "보통"}, bid
|
||||
assert {"높이감률", "구멍땜"} <= set(c["입력필요"]), bid # 글에 없는 감률 = 입력 필요
|
||||
|
||||
|
||||
def test_rebar_type2_cut_and_join():
|
||||
got = {c["기초"]: c for c in _first("북부청임도#72")}
|
||||
assert set(got) == {"GC000206", "GC000207"}
|
||||
assert got["GC000206"]["옵션"]["Type"] == got["GC000207"]["옵션"]["Type"] == "Type-Ⅱ"
|
||||
assert all(c["단"] == "확정" for c in got.values())
|
||||
|
||||
|
||||
def test_remicon_small_50():
|
||||
(c,) = _first("북부청임도#21")
|
||||
assert c["기초"] == "GC000198" and c["단"] == "확정"
|
||||
assert c["옵션"]["소량감률"] == 50 and c["옵션길"]["소량감률"] == "B5"
|
||||
|
||||
|
||||
def test_excavation_51():
|
||||
(c,) = _first("북부청임도#51")
|
||||
assert (c["기초"], c["단"], c["옵션"]["Type"], c["옵션"]["깊이"]) == (
|
||||
"GC000089",
|
||||
"확정",
|
||||
"TypeⅢ",
|
||||
"5m 이하",
|
||||
)
|
||||
|
||||
|
||||
def test_rubble_carry_gc001140():
|
||||
r = results()[0]["동부청영월#1466"]
|
||||
c = r["근거후보"][0]
|
||||
assert r["단"] == "사람" and c["기초"] == "GC001140"
|
||||
assert c["옵션"] == {"재료갈래": "석재류", "운반로": "불량"}
|
||||
assert set(c["옵션길"].values()) == {"V"}
|
||||
|
||||
|
||||
def test_big_stone_near_unit_basis():
|
||||
r = results()[0]["북부청임도#32"]
|
||||
(c,) = r["근거후보"]
|
||||
assert (r["단"], c["기초"], c["단"], c["품검산"]) == ("근접", "GF000226", "근접", "다름")
|
||||
assert {t for t, _ in c["까닭"]} == {"단위 기준 차이"} and "÷10" in c["까닭"][0][1]
|
||||
|
||||
|
||||
def test_no_made_up_values():
|
||||
"""옵션마다 출처 꼬리표 · 입력 필요 · 0 간주 칸은 옵션에 없음 ·
|
||||
없음 단은 근거후보 없이 보강 까닭만."""
|
||||
for bid, r in results()[0].items():
|
||||
for c in r["근거후보"]:
|
||||
assert set(c["옵션"]) <= set(c.get("옵션길") or {}), bid
|
||||
assert set((c.get("옵션길") or {}).values()) <= WAYS, bid
|
||||
assert not set(c["옵션"]) & set(c["입력필요"]), bid
|
||||
assert all(v is not None for v in c["옵션"].values()), bid
|
||||
assert c["단"] in ("확정", "근접", "사람")
|
||||
if r["단"] == "없음":
|
||||
assert r["근거후보"] == [] and r["보강"]["까닭"], bid
|
||||
|
||||
|
||||
def test_corpus_is_eight_files():
|
||||
"""보정 자료 = 여덟 파일 상수(일반 뺌) — 뽑기 규칙을 바꾸면 다시 지어 넣음."""
|
||||
preps = [
|
||||
bc.prep(blk, book["꼴"], (book["숨은식"] or {}).get(str(blk["번"])))
|
||||
for _, book in books()
|
||||
for blk in book["단산"]
|
||||
]
|
||||
shipped = json.loads(bi.CORPUS.read_text(encoding="utf-8"))
|
||||
assert sorted(map(json.dumps, bc.corpus(preps))) == sorted(map(json.dumps, shipped))
|
||||
|
||||
|
||||
def test_store_round_trip(tmp_path):
|
||||
"""임시 폴더 HP → 뒤 셈 저장본 · 목록 · 블록 하나 · 끝내기."""
|
||||
_, book = books()[0]
|
||||
small = {**book, "단산": [b for b in book["단산"] if b["번"] in (21, 51)]}
|
||||
row = hopyo_store.save(tmp_path, "시험.xls", small, {"id": 0, "이름": "시험"})
|
||||
key = row["키"]
|
||||
assert store.solved(tmp_path, key) is None
|
||||
got = store.solve_all(tmp_path, key)
|
||||
assert set(got) == {"21", "51"} and store.solved(tmp_path, key) == json.loads(json.dumps(got))
|
||||
assert store.listing(tmp_path, key)["셈중"] is False
|
||||
assert store.block(tmp_path, key, 51)["근거후보"][0]["기초"] == "GC000089"
|
||||
with pytest.raises(Exception):
|
||||
store.block(tmp_path, key, 999)
|
||||
store.forget(tmp_path, key)
|
||||
assert store.solved(tmp_path, key) is None
|
||||
judge.forget(tmp_path, key)
|
||||
|
||||
|
||||
def test_hidden_g_lines():
|
||||
book = {"꼴": "STmate", "단산": [{"번": "3", "행": [2, 4]}]}
|
||||
rows = [[None] * 7, [None] * 6 + ["_x0007_L=20"], [None] * 6 + ["_x0007_"], [None] * 6 + ["x"]]
|
||||
assert store.hidden(book, {store.SHEET: rows}) == {"3": ["L=20"]}
|
||||
assert store.hidden({"꼴": "엑셀꼴", "단산": []}, {}) == {}
|
||||
@@ -47,6 +47,73 @@ def test_annual_fixed_amount_with_zero_quantity_and_duplicate_names():
|
||||
assert len(result["invalid"]["errors"]) == 1
|
||||
|
||||
|
||||
def test_annual_procurement_fee_is_allocated_and_kept_in_government_costs():
|
||||
result = run(
|
||||
"math",
|
||||
"""(()=>{
|
||||
const p=EstNative.empty();p.options.SourceRefresh=false;
|
||||
p.id='123456789012345678901234';p.options.GangubExceptJodalRatio=true;
|
||||
p.tables.EstMainDat=[
|
||||
{PkID:1,Name:'labor',GN:1,NU:1000},
|
||||
{LinkIndex:54},
|
||||
{PkID:2,Name:'관급자재',Level:1},
|
||||
{PkID:3,Name:'material',GN:1,GU:301},
|
||||
{PkID:4,Name:'조달수수료',LinkIndex:57,GN:10,Unite:'%',OCheck:4,SCheck:3}];
|
||||
p.tables.Wonga=[
|
||||
{Gubun:'F',Name:'직접노무비',BaseField:'NC'},
|
||||
{Gubun:'G',Name:'관급자재대'},
|
||||
{Gubun:'S',Name:'산업안전보건관리비',VSusic:'G*0.1'},
|
||||
{Gubun:'T',Name:'총공사비',LinkIndex:55,VSusic:'F+G+S'}];
|
||||
p.annual=[{name:'1차',formula:'[기준]*0.5'},{name:'2차',formula:'[기준]-[1차]'}];
|
||||
const q=EstNative.calculate(p),before=JSON.stringify(q),output=EstBusiness.annual(q);
|
||||
return {q,output,preserved:before===JSON.stringify(q),
|
||||
again:output.results.map(item=>EstNative.calculate(item.project))};
|
||||
})()""",
|
||||
)
|
||||
assert result["preserved"]
|
||||
assert result["output"]["errors"] == []
|
||||
assert result["q"]["calculation"]["total"] == 1361
|
||||
projects = [item["project"] for item in result["output"]["results"]]
|
||||
for project in projects + result["again"]:
|
||||
assert "id" not in project
|
||||
fee = project["tables"]["EstMainDat"][4]
|
||||
assert fee["LinkIndex"] == 99
|
||||
assert fee["CalcIndex"] == 1
|
||||
assert fee["GN"] == 10
|
||||
assert fee["TC"] == fee["JC"] == 15
|
||||
assert fee["TU"] == fee["JU"] == 0
|
||||
government = project["tables"]["Wonga"][1]
|
||||
assert government["NC"] == 15
|
||||
assert project["tables"]["Wonga"][2]["Cost"] == 15
|
||||
assert [project["calculation"]["total"] for project in projects] == [680, 681]
|
||||
assert [project["calculation"]["total"] for project in result["again"]] == [680, 681]
|
||||
|
||||
|
||||
def test_annual_excluded_fee_does_not_reduce_the_government_safety_base():
|
||||
result = run(
|
||||
"math",
|
||||
"""(()=>{
|
||||
const p=EstNative.empty();p.options.SourceRefresh=false;
|
||||
p.options.GangubExceptJodalRatio=true;
|
||||
p.tables.EstMainDat=[{LinkIndex:54},{Level:1,Name:'관급자재'},
|
||||
{GN:1,GU:301},{LinkIndex:57,Name:'조달수수료',GN:10,CalcIndex:2}];
|
||||
p.tables.Wonga=[{Gubun:'G',Name:'관급자재대'},
|
||||
{Gubun:'S',Name:'산업안전보건관리비',VSusic:'G*0.1'},
|
||||
{Gubun:'T',Name:'총공사비',VSusic:'G+S',LinkIndex:55}];
|
||||
p.annual=[{name:'first',formula:'[기준]*0.5'}];
|
||||
const q=EstNative.calculate(p);
|
||||
return {q,output:EstBusiness.annual(q)};
|
||||
})()""",
|
||||
)
|
||||
assert result["q"]["tables"]["Wonga"][0]["NC"] == 0
|
||||
project = result["output"]["results"][0]["project"]
|
||||
assert project["tables"]["EstMainDat"][3]["CalcIndex"] == 3
|
||||
assert project["tables"]["EstMainDat"][3]["TC"] == 15
|
||||
assert project["tables"]["Wonga"][0]["Cost"] == 150
|
||||
assert project["tables"]["Wonga"][0]["NC"] == 0
|
||||
assert project["tables"]["Wonga"][1]["Cost"] == 15
|
||||
|
||||
|
||||
def test_resource_expansion_preserves_different_labor_and_material_bid_rates():
|
||||
result = run(
|
||||
"math",
|
||||
|
||||
@@ -135,3 +135,125 @@ def test_subtotal_includes_price_only_rows_and_updates_truncated_final_amount():
|
||||
149380,
|
||||
158440,
|
||||
]
|
||||
|
||||
|
||||
def test_operator_followed_by_spaces_does_not_start_a_new_assignment():
|
||||
result = run(
|
||||
"math",
|
||||
r"""[1700,2000].map(weight=>EstText.calculate([
|
||||
{Index:1,Code:'forest',LineData:'forest'},
|
||||
{LineData:` N = 54.93 q = 250 W = ${weight}`},
|
||||
{LineData:' Q = N × q ÷ W = 54.93 × 250 ÷ 1,700 = \x128.08\x12 ㎥/hr'},
|
||||
{LineData:' V = Q × 2 = \x1216.16\x12'}
|
||||
]))""",
|
||||
)
|
||||
assert all(item["errors"] == [] for item in result)
|
||||
assert [item["rows"][2]["TC"] for item in result] == [8.08, 6.87]
|
||||
assert [item["rows"][3]["TC"] for item in result] == [16.16, 13.74]
|
||||
|
||||
|
||||
def test_curly_wrapped_results_and_explanatory_calculations_keep_their_text():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'forest',LineData:'forest'},
|
||||
{LineData:' {0.91m×0.91m×0.6m = \x120.49686\x12㎥}'},
|
||||
{LineData:' {0.49686 ×2.6Ton = \x121.29\x12}'},
|
||||
{LineData:' ※ ㎡당 중량 1.6개 ×1,325÷1,000 = \x122.12\x12Ton/㎡'}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert [row["TC"] for row in result["rows"][1:]] == [0.49686, 1.29, 2.12]
|
||||
assert result["rows"][1]["LineData"].strip().startswith("{")
|
||||
assert result["rows"][1]["LineData"].endswith("}")
|
||||
|
||||
|
||||
def test_won_unit_glyphs_are_units_instead_of_operators():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'money',LineData:'money'},
|
||||
{LineData:' 0.0347㎥ / 2.5 × 0 \\/㎥ = \x120\x12'},
|
||||
{LineData:' 0.0347㎥ / 2.5 × 2500 \\/㎥ = \x1234.7\x12'},
|
||||
{LineData:' 2㎥ × 2500₩/㎥ = \x125000\x12'}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert [row["TC"] for row in result["rows"][1:]] == [0, 34.7, 5000]
|
||||
|
||||
|
||||
def test_range_conditions_are_not_assigned_arbitrary_values():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'range',LineData:'range'},
|
||||
{LineData:' 앙카핀 (D=13, L=450~600mm) 1.0㎡ : 별도계상'},
|
||||
{LineData:' H = 0~4m : (기계80%, 인력20%)'},
|
||||
{LineData:' Q = 2 × 3 = \x126\x12'},
|
||||
{LineData:' V = H × 2 = \x128\x12'}
|
||||
])""",
|
||||
)
|
||||
assert len(result["errors"]) == 1
|
||||
assert "참조 항목 없음: H" in result["errors"][0]
|
||||
assert result["rows"][3]["TC"] == 6
|
||||
|
||||
|
||||
def test_quantity_subtotal_keeps_six_digits_before_pricing():
|
||||
result = run(
|
||||
"math",
|
||||
r"""[4.4,5].map(length=>EstText.calculate([
|
||||
{Index:1,Code:'sign',LineData:'sign'},
|
||||
{Level:2,LineData:'각재 수량'},
|
||||
{Diget:7,DigetIndex:1,LineData:` 0.09 ×0.09 ×${length}m ×2개 = \x120.07128\x12`},
|
||||
{Diget:7,DigetIndex:1,LineData:' 0.06 ×0.06 ×2.28m ×5개 = \x120.04104\x12'},
|
||||
{Diget:7,DigetIndex:1,LineData:' 0.06 ×0.06 ×2.28m ×7개 = \x120.057456\x12'},
|
||||
{Diget:7,DigetIndex:1,LinkIndex:61,LineData:' 소 계 : Q1 = \x0c0.169776\x0c ㎥/개소'},
|
||||
{SCheck:-2,LinkIndex:5,LineData:' 재료비 : \x03577,842\x03 × Q1 = 577,842 × 0.169776 = \x1298,103.7\x12'}
|
||||
]))""",
|
||||
)
|
||||
assert all(item["errors"] == [] for item in result)
|
||||
assert [item["rows"][5]["TC"] for item in result] == [0.169776, 0.179496]
|
||||
assert [item["rows"][6]["TC"] for item in result] == [98103.7, 103720.3]
|
||||
|
||||
|
||||
def test_declaration_can_follow_an_hour_unit_with_a_single_space():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'transport',LineData:'transport'},
|
||||
{LineData:' L = 1.0Km{?} V1 = 5.5Km/hr V2 = 9.3Km/hr t=35분'},
|
||||
{LineData:' Q = t × 2 = \x1270\x12'}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert result["rows"][2]["TC"] == 70
|
||||
|
||||
|
||||
def test_fullwidth_unit_slash_and_ground_level_annotations():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'annotations',LineData:'annotations'},
|
||||
{LineData:' Q = 23.32 ㎥/hr'},
|
||||
{LineData:' D1 = {-}6.6m [평균지반고]'},
|
||||
{LineData:' D2 = {-}18.6m [기초터파기 평균 저면 EL.]'},
|
||||
{LineData:' V = Q + D1 - D2 = \x1235.32\x12',Precision:2}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert result["rows"][4]["TC"] == 35.32
|
||||
|
||||
|
||||
def test_spaced_metre_unit_is_not_a_second_variable_declaration():
|
||||
result = run(
|
||||
"math",
|
||||
r"""EstText.calculate([
|
||||
{Index:1,Code:'spaced_unit',LineData:'spaced_unit'},
|
||||
{LineData:' L7 = 4.5m'},
|
||||
{LineData:' Q2 = L7 / 0.8 M = 4.5 / 0.8 = \x125.63\x12 개'},
|
||||
{SCheck:-2,LineData:' 재료비 : \x033,000\x03 × Q2 = 3,000 × 5.63 = \x1216,890\x12'}
|
||||
])""",
|
||||
)
|
||||
assert result["errors"] == []
|
||||
assert result["rows"][2]["TC"] == 5.63
|
||||
assert result["rows"][3]["TC"] == 16890
|
||||
|
||||
Reference in New Issue
Block a user