- 변경 차수 상한 4차(K_ORD>4 거부) · 4차 = 5번째 칸(MCOST5·XX5NM·QTY5) - 할증 계수 float32 — 70%·115% 1원 적음 - 손료 E0~E3 = 조달청 XML 구분값뿐 · 계수 곱 없음 - jdl_format.py — 13종 자르는 자리 · 단가 목록 8종 열 이름 · 줄 만들기·읽기·검사 - 지식DB 34번 §17 · REA_MCP_보강 근거 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
212 lines
7.3 KiB
Python
212 lines
7.3 KiB
Python
"""STMATE 재현 앱 — 원본 정적 규칙(지식DB REA 34) 지킴 시험 · 제잡비 차수 콤보·요율 조회·표준시장단가·노임 환산·할증율.
|
||
|
||
기대값은 원본 바이트에서 읽은 규칙을 이 파일에서 따로 손셈한 것(실무 표본이 없는 갈래).
|
||
차수 콤보 = ST_KanJub 0x46c779(PCLASS 갈래표 0x46c790) · 처음 계열 0x46c679 · 기본값 0x47a6ec·0x47b633
|
||
건설기계 = 0x47fa24 · 퇴직공제 0x47ae79 · 30일 미만 0x47b376 · 표준시장단가 Main 0x45a468 · OPTIMIZ ST_Func_C 0x48094c
|
||
노임 환산 갈래 Main 0x43894c(RXX 1·10~14) · 할증율 ST_Boq 0x507cc8
|
||
"""
|
||
|
||
import json
|
||
import math
|
||
import subprocess
|
||
from pathlib import Path
|
||
|
||
HELPER = Path(__file__).with_name("helper_stmate_kanjub_rea.cjs")
|
||
|
||
|
||
def run(req):
|
||
return json.loads(
|
||
subprocess.check_output(
|
||
["node", str(HELPER)], input=json.dumps(req), text=True, encoding="utf-8"
|
||
)
|
||
)
|
||
|
||
|
||
def test_chasu_combo_by_pclass():
|
||
"""cBx_Chasu 목록 = PCLASS 갈래 · 7 = 계약(당초) + K_ORD 개 변경 · 계열 = 순번+1."""
|
||
cases = [
|
||
[0, 0, 0],
|
||
[1, 0, 0],
|
||
[2, 0, 0],
|
||
[3, 0, 0],
|
||
[4, 0, 1],
|
||
[4, 0, 0],
|
||
[6, 0, 0],
|
||
[7, 3, 0],
|
||
[8, 0, 0],
|
||
[9, 0, 0],
|
||
]
|
||
out = run({"mode": "native", "cases": cases})
|
||
series = [[s for _, s in o["list"]] for o in out]
|
||
assert series == [
|
||
[1],
|
||
[1, 2],
|
||
[1, 2],
|
||
[1, 2],
|
||
[2, 1],
|
||
[1],
|
||
[1, 3, 4],
|
||
[1, 2, 3, 4],
|
||
[1, 2, 3, 4],
|
||
[1],
|
||
]
|
||
assert [lb.strip() for lb, _ in out[7]["list"]] == [
|
||
"계약(당초) 내역",
|
||
"1차 변경내역",
|
||
"2차 변경내역",
|
||
"3차 변경내역",
|
||
]
|
||
assert [lb.strip() for lb, _ in out[8]["list"]][2] == "변경내역-시비"
|
||
over = run({"mode": "native", "cases": [[7, 4, 0], [7, 5, 0]]})
|
||
assert [s for _, s in over[0]["list"]] == [1, 2, 3, 4, 5] and over[1][
|
||
"list"
|
||
] == [] # 4차 = 계열 5 · 5차 거부
|
||
|
||
|
||
def test_first_series_and_change_defaults():
|
||
"""처음 계열(0x46c690) · 변경 계열 기본값 = 안전 2 · 보험료 1(상시근로자 밑수면 0)."""
|
||
out = run(
|
||
{
|
||
"mode": "native",
|
||
"cases": [[7, 2, 0], [7, 0, 0], [7, 6, 0], [8, 0, 0], [3, 0, 0], [0, 0, 0]],
|
||
}
|
||
)
|
||
assert [o["first"] for o in out] == [3, 2, 2, 4, 2, 1]
|
||
seven = out[0]
|
||
assert [d["I_ANJUNAUTO"] for d in seven["dft"]] == [1, 2, 2]
|
||
assert [d["I_BF_CH_BO"] for d in seven["dft"]] == [0, 1, 1]
|
||
assert seven["dftSangsi"]["I_BF_CH_BO"] == 0
|
||
assert seven["gate"] == [False, True]
|
||
assert out[4]["gate"] == [False, False] # PCLASS 3(통합형) — I_BF_CH_BO 를 0 으로 지움
|
||
assert set(seven["items"]) == {"KUNBO", "YUNBO", "SLVBO", "TBUGUM", "ANJUN"}
|
||
|
||
|
||
def _matrix():
|
||
"""50×35 — 칸 값 = 행×100 + 열(1부터) · 0 칸 없음."""
|
||
return [[r * 100 + c for c in range(1, 36)] for r in range(50)]
|
||
|
||
|
||
def test_rate_eqip_tbugum_short_period():
|
||
base = {"kongType": 0, "kikan": 1, "scale": 1e9, "anjunType": 1, "anjunScale": 1e9}
|
||
cases = [
|
||
base | {"eqip": 0, "tbugum": 0},
|
||
base | {"eqip": 5, "tbugum": 2},
|
||
base | {"eqip": 1, "tbugum": 3},
|
||
base | {"eqip": 0, "kongType": 3, "tbugum": 1},
|
||
base | {"eqip": 0, "kongType": 2},
|
||
base | {"eqip": 2, "kongType": 5},
|
||
base | {"eqip": -1},
|
||
base | {"kikan": -1},
|
||
]
|
||
out = run({"mode": "rate", "matrix": _matrix(), "cases": cases})
|
||
cell = lambda r, c: r * 100 + c # noqa: E731
|
||
eq = [o["rates"].get("EQIP") for o in out]
|
||
# 구분 0 = 공사종류(0 RATE_01 · 3 RATE_03 · 2 RATE_02) · 1~5 = RATE_06~10 · 국가유산(5)·−1 = 0
|
||
assert eq[:7] == [cell(49, 1), cell(49, 10), cell(49, 6), cell(49, 3), cell(49, 2), 0, 0]
|
||
tb = [o["rates"].get("TBUGUM") for o in out[:4]]
|
||
assert tb == [cell(46, 16), cell(46, 18), None, cell(46, 17)]
|
||
assert out[2]["keys"]["R_TBUGUM"] == 7 # 기타(3) = 그대로
|
||
short = out[7]
|
||
assert [short["rates"][c] for c in ("KUNBO", "YUNBO", "SLVBO")] == [0, 0, 0]
|
||
assert short["keys"]["R_KUNBO"] == 0
|
||
assert (
|
||
short["rates"]["KANNO"] == out[0]["rates"]["KANNO"] - 100
|
||
) # 30일 미만 = 6개월 이하 칸(기간 0)
|
||
|
||
|
||
def _optimiz(T, parts):
|
||
fl = [math.trunc(v) for v in parts]
|
||
fr = [v - f for v, f in zip(parts, fl)]
|
||
left = round(math.trunc(T) - sum(fl))
|
||
while left > 0:
|
||
k = max(range(len(fr)), key=lambda i: (fr[i], -i))
|
||
if fr[k] <= 0:
|
||
break
|
||
fl[k] += 1
|
||
fr[k] = -1
|
||
left -= 1
|
||
return fl
|
||
|
||
|
||
def _split(T, R, pre, ty, op):
|
||
if R == 0 and pre:
|
||
L = pre
|
||
else:
|
||
L0, X = T * R / 100, T * (100 - R) / 100
|
||
L = (
|
||
_optimiz(T, [L0, 0, 0, X])[0]
|
||
if op == 1
|
||
else _optimiz(T, [X, 0, 0, L0])[3]
|
||
if op == 2
|
||
else math.trunc(L0 + 1e-9)
|
||
)
|
||
rem = T - L
|
||
return {
|
||
"L": L,
|
||
"M": rem if ty == 1 else 0,
|
||
"S": rem if ty == 2 else 0,
|
||
"N": 0 if ty in (1, 2) else rem,
|
||
}
|
||
|
||
|
||
def test_std_market_split_modes():
|
||
"""ACTTYP 0·1·2 × ACTOPT 0·1·2 — 노무 끝전(버림 · 노무 우선 · 타금액 우선) · 나머지 자리."""
|
||
cases = [
|
||
[T, R, 0, ty, op]
|
||
for T, R in ((1001, 37.5), (1000, 45.05), (999, 50), (12345, 63.3))
|
||
for ty in (0, 1, 2)
|
||
for op in (0, 1, 2)
|
||
]
|
||
cases.append([5000, 0, 1234, 1, 1]) # 율 0 · 노무 미리값 = 그대로
|
||
out = run({"mode": "split", "cases": cases})
|
||
for c, o in zip(cases, out):
|
||
want = _split(*c)
|
||
assert {k: o[k] for k in "LMSN"} == want, c
|
||
assert o["T"] == c[0]
|
||
# 0.5 동률 — 노무 우선이면 노무가 1원 · 타금액 우선이면 다른 금액이 1원
|
||
assert out[cases.index([999, 50, 0, 1, 1])]["L"] == 500
|
||
assert out[cases.index([999, 50, 0, 1, 2])]["L"] == 499
|
||
|
||
|
||
def test_labor_branch_by_rxx():
|
||
"""RXX 1·10~14 = 식 하나로 모든 L 행 · 0·2~9 = 직종 목록 CONLD→CONTD→CON1D~ 첫 일치."""
|
||
ini = {
|
||
"RXXNM": "A",
|
||
"RXTNM": "T",
|
||
"RX1NM": "R1",
|
||
"RX4NM": "R4",
|
||
"CONLD": "L00001",
|
||
"CON1D": "L00002L00001",
|
||
}
|
||
out = run(
|
||
{
|
||
"mode": "labor",
|
||
"ini": ini,
|
||
"cases": [
|
||
[0, ["L00001", "L00002", "L00003"]],
|
||
[1, ["L00009"]],
|
||
[10, ["L00001"]],
|
||
[14, ["L00003", "M00001"]],
|
||
[3, ["L00002"]],
|
||
],
|
||
}
|
||
)
|
||
assert out == [["A", "R1", None], ["A"], ["T"], ["R4", None], ["R1"]]
|
||
|
||
|
||
def test_hal_rate_rows():
|
||
"""할증율 여러 행 — 보호(UC=T) 행 건너뜀(비목 무관) · B·D·L·M·S·W·X 밖 코드는 율 0."""
|
||
rows = [
|
||
{"BDCODE1": c, "UC": uc, "_recno": i, "SORTCODE": str(i + 1), "ENDCODE": "0"}
|
||
for i, (c, uc) in enumerate(
|
||
[("M00001", ""), ("B00002", "T"), ("A00003", ""), ("W00004", "")]
|
||
)
|
||
]
|
||
out = run({"mode": "hal", "rows": rows, "rates": {"M": 110, "L": 120, "S": 130}, "how": "each"})
|
||
got = {r[0]: r[1:] for r in out["rows"]}
|
||
assert out["r"]["kept"] == 1
|
||
assert got["M00001"] == ["110", "120", "130"]
|
||
assert got["A00003"] == ["0", "0", "0"]
|
||
assert got["W00004"] == ["110", "120", "130"]
|
||
assert got["B00002"] == [None, None, None]
|