Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_3

This commit is contained in:
2026-11-15 19:12:17 +09:00
4 changed files with 59 additions and 24 deletions
+20 -21
View File
@@ -148,9 +148,10 @@ def _est_sheet(rows: list[list]) -> dict[str, dict]:
def _stm_sheet(rows: list[list]) -> dict[str, dict]:
"""STmate 엑셀 「중기사용료」 — 「제 N 호표」 뒤 머리 [이름, 규격, 시간, 코드] · 손료 [이름, 규격, 계수(천원당),
「천원」, 가격 천원 …] · 조종원 [직종, 인, 「인」 …] · 연료 [이름, 등급, ℓ, 「L」 …] · 잡품 [잡품, 주연료의%, % …] ·
「합 계」 끝 → {코드 · 이름|규격: 구성}(손료계수 = 계수 × 10⁴ · 가격 = 천원 × 1000 원)."""
"""STmate 엑셀 「중기사용료」 — 칸 차례 [명칭, 규격, 수량, 단위, 단가 …](첫 글 칸부터 · 빈 규격도 칸 자리 지킴 ·
843-2 — 규격 빈 머리 · 손료 줄을 못 읽던 것) · 「제 N 호표」 뒤 머리(단위 「시간」 · 끝 칸 코드 X…) ·
손료(단위 「천원」 · 수량 = 계수(천원당) · 단가 = 가격 천원) · 조종원(「인」) · 연료(「L」) · 전력(「kWh」) ·
잡품(명칭 잡품 · 수량 = %) · 「합 계」 끝 → {코드 · 이름|규격: 구성}(손료계수 = 계수 × 10⁴ · 가격 = 천원 × 1000 원)."""
out: dict[str, dict] = {}
cur = None
for row in rows:
@@ -164,26 +165,24 @@ def _stm_sheet(rows: list[list]) -> dict[str, dict]:
if first == "합계":
cur = None
continue
if (
not cur
and len(v) >= 4
and str(v[2]) in ("시간", "hr")
and re.fullmatch(r"X\d+", str(v[-1]))
):
cur.update(_blank(str(v[0]), str(v[1])))
out[str(v[-1])] = out[key_of(v[0], v[1])] = cur
at = next(i for i, c in enumerate(row) if c not in (None, ""))
name, spec, qty, unit, price = (*row[at : at + 5], *[None] * 5)[:5]
spec, qty, price, unit = spec or "", _num(qty), _num(price), str(unit or "").strip()
if not cur and unit in ("시간", "hr") and re.fullmatch(r"X\d+", str(v[-1])):
cur.update(_blank(str(name), str(spec)))
out[str(v[-1])] = out[key_of(name, spec)] = cur
elif not cur:
continue
elif len(v) > 4 and str(v[3]) == "천원" and _num(v[2]) and _num(v[4]):
cur["손료"].append([_num(v[2]) * 10_000, _num(v[4]) * 1000, False])
elif len(v) > 2 and str(v[2]) == "인" and (_num(v[1]) or 0) > 0:
_crew(cur, v[0], v[1])
elif first in MISC_WORDS and len(v) > 2:
_misc(cur, v[2])
elif len(v) > 3 and str(v[3]).upper() in ("L", "ℓ"):
_fuel(cur, v[0], v[2])
elif len(v) > 3 and str(v[3]).upper() == "KWH":
cur["전력"] = _num(v[2])
elif unit == "천원" and qty and price:
cur["손료"].append([qty * 10_000, price * 1000, False])
elif unit == "인" and (qty or 0) > 0:
_crew(cur, name, qty)
elif first in MISC_WORDS and qty is not None:
_misc(cur, qty)
elif unit.upper() in ("L", "ℓ"):
_fuel(cur, name, qty)
elif unit.upper() == "KWH":
cur["전력"] = qty
return {k: m for k, m in out.items() if m.get("손료")}
+4 -1
View File
@@ -15,6 +15,7 @@ from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from common_util.common_util_auth import verify_session
from M01_MasterData import M01_MasterData_Router as m01_router
from M01_MasterData import M01_MasterData_Store as store
from M02_MasterTemplete import M02_Logic_Lib_Read as lib
@@ -78,7 +79,9 @@ def test_요소_표_밖_파일은_막음(user: TestClient) -> None:
def test_저장_안_한_줄_셈_쓰기_없음(user: TestClient) -> None:
before = _prints()
saved = user.post(f"{BASE}/calc", json={"key": KEY, "inputs": {}}).json()
row = store.logic(KEY)["logic"]
row = m01_router._num(
store.logic(KEY)["logic"]
) # 화면이 받는 꼴(M01 /logic = _num · 794-2b 「개정」 수 칸)
same = user.post(f"{BASE}/calc", json={"key": KEY, "inputs": {}, "row": row}).json()
line = row["호표"][0]
row2 = {**row, "호표": [{**line, "수량": f"({line['수량']}) * 2"}, *row["호표"][1:]]}
@@ -262,7 +262,7 @@ def test_machines_equal_excel() -> None:
heads = [rows[i + 1] for i, r in enumerate(rows) if re.fullmatch(r"제\d+호표", _flat(r[0]))]
assert all(mm.key_of(h[0], h[1]) in mine for h in heads), xt # 엑셀 읽기기가 못 읽는 머리도
m += len(heads)
assert (n, m) == (100, 118) # 엑셀 읽기기 100(울진대흥 머리 코드 없음 9 만) · 엑셀 호표 118
assert (n, m) == (118, 118) # 843-2 규격 빈 머리 · 손료 줄도 칸 자리로 읽음 = 엑셀 호표 118 모두
def test_my_machine_seed_and_peers() -> None:
@@ -5,6 +5,7 @@
② 몸 = `master_machine_crew` 꼴 인력 · 잡품 줄 · 못 이음 = 비고 「확인 필요」 — main 구성 전부 버린 줄 0
③ 층에 만든 내 기계 시간당 노무비 = 줄마다 우리 노임 × 인 × 5/24 의 합(둘째 줄부터도 셈에 들어감)
④ 한 줄 구성은 전과 같은 몸(옛 칸 그대로 · 저장 때 목록으로 바뀜)
⑤ STmate 엑셀 「중기사용료」 규격 빈 칸(머리 · 손료 줄) = 칸 자리로 읽음(843-2) — 울진대흥 호표 25 모두 · .STC 구성과 같음
"""
from __future__ import annotations
@@ -83,7 +84,10 @@ def test_no_line_dropped() -> None:
b = mm.body(m, 0, _line(m["이름"], m["규격"]), "main.estx")
if b is None or b["방식"] != "바로":
continue
v, notes = b["값"], [x for x in b["비고"].split(" · ") if mm.CHECK in x and x != "값 확인 필요"]
v, notes = (
b["값"],
[x for x in b["비고"].split(" · ") if mm.CHECK in x and x != "값 확인 필요"],
)
src = sum(len(m.get(k) or []) for k in ("인력줄", "연료줄", "재료줄"))
src += len([r for r in m.get("잡품줄") or [] if r]) # 0 % = 셈할 것 없음
crew = len(v["인력"]) if "인력" in v else int(bool(v.get("조종원")))
@@ -120,5 +124,34 @@ def test_single_line_same_as_before() -> None:
assert "인력" not in v and v["조종원"] == 1.0 and v["잡재료율"] == 11.0
# ⑤ STmate 엑셀 빈 규격 ─────────────────────────────────────────────
def test_stmate_excel_blank_spec() -> None:
import test_797_1a_import_flags as practice
from M02_MasterTemplete import M02_Doc_FromNative as fn
from M02_MasterTemplete import M02_San_Book as book
p = practice.FILES.get("울진대흥")
stc = next(iter(practice.golden.DOCS.rglob("2024년 간선임도사업(기번3).STC")), None)
if not p or not p.exists() or stc is None:
pytest.skip("실무 견본 없음")
got = mm.xlsx_makes(book.sheets(p.name, p.read_bytes()))
codes = sorted(k for k in got if k.startswith("X"))
assert len(codes) == 25 # 호표 25 모두(전 = 9 · 규격 빈 머리 16 못 읽음)
assert got["X00003"]["손료"] == [[2085.0, 62550000.0, False]] # 「굴삭기(0.2m3)」 규격 빈 머리
assert [x[1] for x in got["X00014"]["손료"]] == [
110926000.0,
16315000.0,
] # 둘째 손료 규격 빈 줄
native = fn.read(stc.name, stc.read_bytes())["기계구성"]
keep = ("손료", "조종원", "조종원직종", "주연료종류", "주연료", "잡재료율")
for k in codes:
a, b = got[k], native[k]
assert [[round(x, 3) for x in r[:2]] for r in a["손료"]] == [
[round(x, 3) for x in r[:2]] for r in b["손료"]
], k
assert all(a.get(f) == b.get(f) for f in keep[1:]), k
if __name__ == "__main__":
pytest.main([__file__, "-q"])