- 새 M02_Hopyo_UnitLine — 단위 줄 꼴 · 자체 검산(줄 재현 0.5 % · 줄합 = 블록 금액 덮음) · 값 줄 → 하위 가름 · 율 줄 기준꼴 · 기준줄 · 할증 풀이 · 일위대가 줄 투영(묶음 · 재현 · % 기준) · 두 어댑터 끝에 settle - ReadEst.items — 줄 금액 · 율 · 값 줄 · 합계 · 소계 절 = 밖 · 끝 점 「28,500. ×」 · 이름 속 = · / 자재 · 「(지주)」 · 「\/㎡」 금액 꼬리 · 「직종 : P」 선언 줄 뺌 · 자재 손료율 표지 · 기계 비목마다 매개값 계수 · 절 = 제목 - Stmate — 같은 기계 코드 두 묶음 따로(울진대흥 1129 · 2119 · 울진소광 1448 단가 두 배 흠) · 비목마다 H 다르면 계수(봉화 덤프 (Cm-t1)/Cm · 영월 물탱크 ×0.722) · PR · RR 율 행(기준 표지 PA01M 꼴) · 이름 없는 율 행 = 위 라벨 줄 이름 · 기준글 · G 열 「* N'%'」 자원 = 율 줄 · 자재 손료율 · D 행 = 하위 · N 열 C = 밖 - Store.BOOK 3 — 읽기 꼴 바뀜(옛 판 HP = 다시 올림) - 여덟 파일 불리는 블록: 줄 재현 811/836 → 836/842 · 덮음 201 → 204/206(남은 둘 = 엑셀 합이 진동로울러 노무 뺌) · 일위대가 줄 1,344/1,350 - 새 시험 test_778_3_hopyo_unit_lines Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
7.3 KiB
Python
175 lines
7.3 KiB
Python
"""778-3(D-1) 단위 줄 — 단가산출 블록 · 일위대가 줄을 같은 계산 단위 꼴로(`M02_Hopyo_UnitLine`).
|
||
|
||
꼴(EST 글 · STmate 행) · 같은 기계 코드 두 묶음 따로 · 비목마다 매개값 `계수` · 율 행 · 셈 밖 ·
|
||
여덟 파일(불리는 블록) 줄 수 · 재현 · 덮음 · 할증 풀이 · 요구 624(이름 없는 율 줄 · 두 행 노무 ·
|
||
손료율 표지). 읽기만 · 서버 없음.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import warnings
|
||
from collections import Counter
|
||
from functools import lru_cache
|
||
|
||
import pytest
|
||
|
||
from M02_MasterTemplete import M02_Hopyo_Excel as hx
|
||
from M02_MasterTemplete import M02_Hopyo_ReadEst as est
|
||
from M02_MasterTemplete import M02_Hopyo_Sangeun as sg
|
||
from M02_MasterTemplete import M02_Hopyo_Stmate as st
|
||
from M02_MasterTemplete import M02_Hopyo_UnitLine as ul
|
||
from resources.tester.helper_m02_hopyo_golden import FILES
|
||
|
||
warnings.filterwarnings("ignore", category=UserWarning, module="openpyxl")
|
||
KEYS = {"묶음", "이름", "규격", "품", "단가", "금액", "어떻게", "행", "절", "윗절"}
|
||
|
||
|
||
def test_EST_글_단위_줄():
|
||
text = [
|
||
" 1. 굴착기(무한궤도)(0.7㎥)",
|
||
" 노무비 : 59,020 ÷ 26.95 × 0.9 = 1,970.9",
|
||
" 재료비 : 19,473 ÷ 26.95 = 722.5",
|
||
" 2. 인 부",
|
||
" 보통인부 : 172,068 × 0.011인 = 1,892.7 (지주)",
|
||
" 합판(내수),대전 (12×1220=2440) 28,500. × 0.1 = 2,850.",
|
||
" 각재(외송) 500,000 × 0.038 ㎥ × 38. % = 7,220 ₩ / ㎡",
|
||
" 특별인부 : P",
|
||
" 3. 공구손료(인력품의 3%)",
|
||
" 1,892.7 × 0.03 = 56.7",
|
||
" 레미콘 : 별도계상",
|
||
" 4. 합 계",
|
||
" 보통인부 : 172,068 × 0.011인 = 1,892.7",
|
||
]
|
||
got = est.items(text, list(range(1, len(text) + 1)))
|
||
book = {"단산": [{"번": 1, "금액": {"계": 14712.8}, "항목": got}], "호표": []}
|
||
ul.settle(book)
|
||
kinds = [(u["묶음"], bool(u.get("밖"))) for u in got]
|
||
assert kinds == [
|
||
("X", False),
|
||
("L", False),
|
||
("M", False),
|
||
("M", False),
|
||
("R", False),
|
||
("M", False),
|
||
("L", True),
|
||
] # fmt: skip — 「특별인부 : P」 선언 줄 뺌
|
||
assert all(KEYS <= set(u) for u in got)
|
||
mach, labor, mat, loss, rate = got[:5]
|
||
assert mach["품"] == pytest.approx(0.9 / 26.95) and mach["계수"] == {
|
||
"재료비": pytest.approx(1 / 0.9)
|
||
}
|
||
assert mach["금액"] == pytest.approx(2693.4) and mach["재현"]
|
||
assert labor["직종"] == "보통인부" and labor["금액"] == 1892.7 and labor["재현"]
|
||
assert mat["이름"].startswith("합판") and mat["품"] == 0.1 and mat["재현"] # 끝 점 · 이름 속 =
|
||
assert loss["손료율"] == 0.38 and loss["금액"] == 7220 and loss["재현"] # 손료율 = 자재 줄 표지
|
||
assert rate["품"] == 0.03 and rate["기준값"] == 1892.7 and rate["기준"] == "인력품"
|
||
assert (
|
||
rate["재현"] and rate["절"] == "공구손료(인력품의 3%)" and rate["이름"].startswith("공구")
|
||
)
|
||
assert rate["기준꼴"] == "인력" and rate["기준줄"] == [5]
|
||
assert book["단산"][0]["줄합"] == pytest.approx(14712.8) and book["단산"][0]["덮음"]
|
||
|
||
|
||
def _row(**cells) -> list:
|
||
row = [None] * 24
|
||
for col, v in cells.items():
|
||
row[int(col[1:])] = v
|
||
return row
|
||
|
||
|
||
def test_STmate_같은_코드_두_묶음_따로_계수_율_밖():
|
||
machine = "굴삭기(0.7m3) / 0.7㎥,(암석)"
|
||
rows = [
|
||
_row(c0="제 1호표", c2=100, c6="`BDCOD|D00001"),
|
||
_row(c1=" (1) 채집"),
|
||
_row(c1=" ① 굴삭기", c2=10, c3=10, c7=0.1, c8=100, c9=100, c12="X00009_1", c23=machine),
|
||
_row(c1=" 재료비", c2=5, c4=5, c7=0.05, c8=100, c10=100, c12="X00009_1", c23=machine),
|
||
_row(c1=" (2) 상차"),
|
||
_row(c1=" 노무비", c2=20, c3=20, c7=0.2, c8=100, c9=100, c12="X00009_1", c23=machine),
|
||
_row(
|
||
c1=" 기구손료 : 30 * 2 %",
|
||
c2=0.6,
|
||
c4=0.6,
|
||
c6="&PA01M:M& * 2'%' =",
|
||
c7=2,
|
||
c8=30,
|
||
c12="PR_1",
|
||
),
|
||
_row(c1=" 기구손료 : (가공노력품의 9%)"),
|
||
_row(
|
||
c1=" 30 * 0.09 = 2.7",
|
||
c2=2.7,
|
||
c4=2.7,
|
||
c6="&RA01:M& * 0.09 =",
|
||
c7=0.09,
|
||
c8=30,
|
||
c12="RR_1",
|
||
),
|
||
_row(
|
||
c1=" 보통인부 : 1 * 0.5",
|
||
c2=50,
|
||
c3=50,
|
||
c7=0.5,
|
||
c8=100,
|
||
c9=100,
|
||
c12="L00002_1",
|
||
c13="C1_1",
|
||
c23="보통인부 / ",
|
||
), # fmt: skip
|
||
_row(c0="총계"),
|
||
]
|
||
blocks = st._blocks(rows, {})
|
||
units = blocks["D00001"]["항목"]
|
||
xs = [u for u in units if u["묶음"] == "X"]
|
||
assert [x["품"] for x in xs] == [0.1, 0.2] # 두 묶음 따로(단가 두 배 아님)
|
||
assert xs[0]["단가"]["계"] == 200 and xs[1]["단가"]["계"] == 100
|
||
assert xs[0]["계수"] == {"재료비": 0.5} and xs[0]["금액"] == 15
|
||
rate, bare = [u for u in units if u["묶음"] == "R"]
|
||
assert rate["품"] == 0.02 and rate["기준"] == "PA01M" and rate["기준값"] == 30
|
||
assert bare["이름"] == "기구손료" and bare["기준글"] == "가공노력품" and bare["품"] == 0.09
|
||
assert next(u for u in units if u["묶음"] == "L").get("밖") is True
|
||
assert [r["코드"] for r in blocks["D00001"]["자원"]] == ["X00009", "X00009", "L00002"]
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def _books() -> dict:
|
||
return {tag: hx.read_book(p.name, p.read_bytes()) for tag, p in FILES.items()}
|
||
|
||
|
||
def test_여덟_파일_불리는_블록_줄_재현_덮음():
|
||
kinds, outside, redo, cover = Counter(), Counter(), Counter(), Counter()
|
||
lines = Counter()
|
||
for book in _books().values():
|
||
_, first = sg.calls(book)
|
||
called = {int(k) for k in first}
|
||
for block in book["단산"]:
|
||
if int(block["번"]) not in called:
|
||
continue
|
||
cover[block["덮음"]] += 1
|
||
for u in block["항목"]:
|
||
if u.get("밖"):
|
||
outside[u["묶음"]] += 1
|
||
continue
|
||
kinds[u["묶음"]] += 1
|
||
redo[u["재현"]] += 1
|
||
for hopyo in book["호표"]:
|
||
lines.update(bool(x["재현"]) for x in hopyo["줄"])
|
||
assert dict(kinds) == {"L": 366, "X": 190, "M": 130, "R": 152, "S": 3, "E": 1}
|
||
assert dict(outside) == {"L": 2}
|
||
assert redo[True] >= 836 and sum(redo.values()) == 842 # 전 811/836
|
||
assert cover[True] == 204 and sum(cover.values()) == 206 # 남은 둘 = 엑셀 자체 흠(합이 줄 뺌)
|
||
assert lines[True] == 1344 and sum(lines.values()) == 1350
|
||
|
||
|
||
def test_할증_풀이_같은_코드_따로():
|
||
books = _books()
|
||
block = next(b for b in books["울진소광"]["단산"] if b["번"] == 1448)
|
||
xs = [u for u in block["항목"] if u["묶음"] == "X"]
|
||
assert [x["할증"] for x in xs] == [1.2, 1.2, 1.25] and all(x["재현"] for x in xs)
|
||
block = next(b for b in books["울진대흥"]["단산"] if b["번"] == 1129)
|
||
dig = [u for u in block["항목"] if u.get("코드") == "X00009"]
|
||
assert len(dig) == 2 and all(u["단가"]["계"] == 100378 and u["재현"] for u in dig)
|
||
block = next(b for b in books["봉화"]["단산"] if b["번"] == 3)
|
||
dump = next(u for u in block["항목"] if u["이름"].startswith("덤프"))
|
||
assert dump["계수"]["재료비"] == pytest.approx(0.1527, abs=1e-4) and dump["재현"]
|