@@ -0,0 +1,244 @@
""" 707-1 경쟁사 내역 줄 불러오기 — 읽기(`Bill_Excel`) · 짜기(`Bill_Import`) · 고정 단가 칸 셈 거울.
- 실무 xlsx(STmate 기번3 · 영월 · 봉화 · EST 거창) = 빠짐 0 · 불러온 뒤 직접비 합 = 엑셀 합 ·
어긋난 줄 0 · 조달수수료 = 율(줄로 안 실음) · DRM = 「읽을 수 없음」
- 수량 갈래 (가) take = 붙인 자동 줄 수량 엑셀 · auto_qty = B08 / (나) price = B08 · src_qty = 엑셀
- 두 번 셈 막음 = 이름 같은 자동 줄에 붙임(한 번만 셈) · drop = 남은 자동 줄 지움 ·
같은 파일 다시 = 갈아 끼움 · 조달수수료율 = 빈 때만
- 고정 단가 칸 = 연결이 단가를 못 낼 때만 · 다듬기 · TS 짝 같은 값
"""
import json
import re
import subprocess
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_Bill_Import as imp # noqa: E402
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
from B09_Estimation . B09_Estimation_Bill_Excel import BillExcelError , read_bill # noqa: E402
from B09_Estimation . B09_Estimation_Calc_Bill import compute_estimation # noqa: E402
K = ROOT / " resources " / " knowledge " / " original " / " 실무문서 "
FILES = {
" 기번3 " : K / " 2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).xlsx " , # noqa: E501
" 영월 " : K / " 05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx " ,
" 봉화 " : K / " 04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx " ,
" 거창 " : K / " 01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx " , # noqa: E501
} # fmt: skip
#: (꼴, 항목 수, 엑셀 직접비 합)
WANT = {
" 기번3 " : ( " STmate " , 136 , 710359136 ) ,
" 영월 " : ( " STmate " , 108 , 767148700 ) ,
" 봉화 " : ( " STmate " , 77 , 781283415 ) ,
" 거창 " : ( " EST " , 60 , 384916227 ) ,
}
EMPTY = { " materials " : [ ] , " bill " : { " items " : [ ] } , " hopyo " : { } }
ITEMS = [
{ " key " : " earth:4 " , " section " : " 1.2 " , " name " : " 절토(토사) " , " unit " : " ㎥ " , " qty " : 10 ,
" ref " : { " tab " : " sg_list " , " key " : " D01341 " } } ,
{ " key " : " earth:9 " , " section " : " 1.6 " , " name " : " 노면 정리X " , " unit " : " ㎡ " , " qty " : 3 ,
" ref " : { " tab " : " sg_list " , " key " : " D01341 " } } ,
{ " key " : " earth:6 " , " section " : " 1.2 " , " name " : " 암발파 " , " unit " : " ㎥ " , " qty " : None ,
" ref " : { " tab " : " sg_list " , " key " : " D01341 " } } ,
{ " key " : " haul:3 " , " section " : " 3 " , " name " : " 모래 " , " unit " : " ㎥ " , " qty " : 2 ,
" ref " : { " tab " : " sg_list " , " key " : " D01341 " } } ,
] # fmt: skip
INPUTS = { " materials " : [ ] , " bill " : { " items " : ITEMS } , " hopyo " : { " D01341 " : {
" labor " : 1 , " material " : 0 , " expense " : 0 } } } # fmt: skip
IMPORTS = re . compile ( r " ^import[^;]*; \ n " , re . MULTILINE )
@pytest.fixture ( scope = " module " )
def parsed ( ) :
return { k : read_bill ( p . name , p . read_bytes ( ) ) for k , p in FILES . items ( ) }
def _rows ( inputs , user ) :
return compute_estimation ( inputs , user , None , { } ) [ " bill " ]
def _load ( got , inputs = EMPTY , user = None , * * kw ) :
user = user or { }
made , report = imp . apply ( got , user , _rows ( inputs , user ) [ " rows " ] , " abcd1234 " , * * kw )
made = store . clean_user ( made )
return made , report , _rows ( inputs , made )
@pytest.mark.parametrize ( " name " , list ( FILES ) )
def test_실무_내역_빠짐_0_합계_엑셀과_같음 ( parsed , name ) :
got = parsed [ name ]
kind , items , total = WANT [ name ]
assert ( got [ " 꼴 " ] , sum ( 1 for x in got [ " 줄 " ] if not x [ " 묶음 " ] ) ) == ( kind , items )
assert got [ " 합계 " ] [ " 직접 " ] == total and got [ " 조달수수료율 " ] == 0.54
made , report , bill = _load ( got )
assert report [ " 실음 " ] == len ( got [ " 줄 " ] ) and report [ " 붙임 " ] == 0
assert bill [ " totals " ] [ " direct " ] [ " sum " ] == total and bill [ " totals " ] [ " direct " ] [ " blank " ] == 0
assert imp . differences ( got , report , bill [ " rows " ] ) == [ ]
assert made [ " estimate " ] [ " procurement_fee_pct " ] == 0.54
assert max ( r [ " level " ] for r in bill [ " rows " ] ) < = 5
keys = { r [ " key " ] for r in bill [ " rows " ] }
assert all ( k in keys for k in report [ " 줄 " ] . values ( ) ) # 실은 줄 모두 보임
# 관급 = 제외(관급자재대 아래 · 직접비 밖)
assert bill [ " totals " ] [ " owner " ] [ " net_material " ] > 0
def test_EST_비고_참조_STmate_코드 ( parsed ) :
est = [ x [ " 호표 " ] for x in parsed [ " 거창 " ] [ " 줄 " ] if not x [ " 묶음 " ] ]
assert { " 종류 " : " 산근 " , " 번 " : 46 } in est and { " 종류 " : " 대가 " , " 번 " : 8 } in est
st = next ( x for x in parsed [ " 기번3 " ] [ " 줄 " ] if x [ " 이름 " ] == " 규준틀설치 " )
assert st [ " 호표 " ] == { " 종류 " : " 대가 " , " 번 " : 5 , " 코드 " : " B00194 " }
def test_DRM_읽을_수_없음 ( ) :
with pytest . raises ( BillExcelError , match = " 읽을 수 없음 " ) :
read_bill ( " a.xlsx " , b " \x9b DRMONE " + b " \x00 " * 300 )
with pytest . raises ( BillExcelError , match = " 엑셀 " ) :
read_bill ( " a.xlsx " , b " PK \x03 \x04 broken " )
def test_가_넘겨받기_붙인_자동_줄_수량_엑셀 ( parsed ) :
made , report , bill = _load ( parsed [ " 기번3 " ] , INPUTS , mode = " take " )
rows = { r [ " key " ] : r for r in bill [ " rows " ] }
assert report [ " 붙임 " ] == 2 # 절토(토사) 첫 줄만(1공구) · 암발파 · 운반 「모래」 = 안 붙음
cut = rows [ " earth:4 " ]
assert ( cut [ " qty " ] , cut [ " auto_qty " ] , cut [ " src_qty " ] ) == ( 5675 , 10 , 5675 )
assert cut [ " fixed " ] and cut [ " sum " ] == 11003825 # 엑셀 단가 · 연결 덮개 null
assert cut [ " parent " ] . startswith ( " hand:xabcd1234- " ) # 불러온 자리로 옮김
assert sum ( 1 for r in bill [ " rows " ] if r [ " name " ] == " 절토(토사) " ) == 3 # 두 번 셈 없음
# 붙인 줄 금액 = 엑셀 · 안 붙은 자동 줄(노면 정리X 3 · 운반 모래 2) 그대로 = 직접비 = 엑셀 + 5
assert bill [ " totals " ] [ " direct " ] [ " sum " ] == 710359136 + 5
assert " haul:3 " in rows and rows [ " haul:3 " ] [ " parent " ] == " sec:3 "
tops = [ r [ " key " ] for r in bill [ " rows " ] if r [ " parent " ] is None ]
assert tops . index ( " hand:xabcd1234-00006 " ) < tops . index ( " sec:G " ) # 1공구 = 관급자재대 앞
def test_나_단가만_B08_이김_비교_칸 ( parsed ) :
made , report , bill = _load ( parsed [ " 기번3 " ] , INPUTS , mode = " price " )
cut = { r [ " key " ] : r for r in bill [ " rows " ] } [ " earth:4 " ]
assert ( cut [ " qty " ] , cut [ " src_qty " ] , " auto_qty " in cut ) == ( 10 , 5675 , False )
assert cut [ " sum " ] == 1939 * 10 # 엑셀 단가 × B08 수량
blast = { r [ " key " ] : r for r in bill [ " rows " ] } [ " earth:6 " ]
assert ( blast [ " qty " ] , blast [ " sum " ] ) == ( 3889 , 43700693 ) # B08 수량 없음 = 엑셀 수량
diff = imp . differences ( parsed [ " 기번3 " ] , report , bill [ " rows " ] )
assert [ d [ " 까닭 " ] for d in diff ] == [ " 수량 = B08(단가만) " ]
def test_붙이지_않음_지우기_다시_부르기 ( parsed ) :
_ , report , bill = _load ( parsed [ " 기번3 " ] , INPUTS , attach = False , drop = True )
assert ( report [ " 붙임 " ] , report [ " 지움 " ] ) == ( 0 , 4 ) # 운반 줄도 지움
assert bill [ " totals " ] [ " direct " ] [ " sum " ] == 710359136
made , _ , _ = _load ( parsed [ " 기번3 " ] , INPUTS )
made [ " bill " ] [ " rows " ] . append ( { " key " : " hand:mine " , " parent " : " hand:xabcd1234-00007 " ,
" name " : " 내 줄 " , " qty " : 1 , " ref " : None } ) # fmt: skip
again , report , bill = _load ( parsed [ " 기번3 " ] , INPUTS , user = made )
keys = [ e [ " key " ] for e in again [ " bill " ] [ " rows " ] ]
assert len ( keys ) == len ( set ( keys ) ) and report [ " 붙임 " ] == 2
assert bill [ " totals " ] [ " direct " ] [ " sum " ] == 710359136 + 5 # 갈아 끼움 · 두 번 안 셈
mine = next ( e for e in again [ " bill " ] [ " rows " ] if e [ " key " ] == " hand:mine " )
assert " parent " not in mine # 내 줄 = 남음(맨 위)
def test_조달수수료율_빈_때만 ( parsed ) :
made , _ , _ = _load ( parsed [ " 거창 " ] , user = { " estimate " : { " procurement_fee_pct " : 1.0 } } )
assert made [ " estimate " ] [ " procurement_fee_pct " ] == 1.0
def test_고정_단가_칸_다듬기_연결이_이김 ( ) :
raw = { " bill " : { " v " : 2 , " rows " : [
{ " key " : " hand:a " , " name " : " a " , " qty " : 2 , " ref " : { " tab " : " sg " , " key " : " SG:1 " } ,
" price " : { " labor " : 5 , " material " : " x " } , " src_qty " : 3 , " note " : " 대가 5호표 " } ,
{ " key " : " earth:4 " , " price " : { " expense " : 7 } , " src_qty " : " y " } ,
] } } # fmt: skip
user = store . clean_user ( raw )
hand , auto = user [ " bill " ] [ " rows " ]
assert hand [ " price " ] == { " labor " : 5 , " material " : 0 , " expense " : 0 } and hand [ " src_qty " ] == 3
assert hand [ " note " ] == " 대가 5호표 " and auto == { " key " : " earth:4 " , " price " : {
" labor " : 0 , " material " : 0 , " expense " : 7 } } # fmt: skip
books = { " sg_list " : [ { " no " : 1 , " key " : " SG:1 " , " labor " : 100 , " material " : 0 , " expense " : 0 ,
" sum " : 100 } ] } # fmt: skip
rows = { r [ " key " ] : r for r in compute_estimation ( INPUTS , user , None , books ) [ " bill " ] [ " rows " ] }
assert ( rows [ " hand:a " ] [ " sum " ] , rows [ " hand:a " ] . get ( " fixed " ) ) == ( 200 , None ) # 연결 이김
assert rows [ " earth:4 " ] [ " sum " ] == 10 and " fixed " not in rows [ " earth:4 " ] # 자동 연결 이김
rows = { r [ " key " ] : r for r in compute_estimation ( INPUTS , user , None , { } ) [ " bill " ] [ " rows " ] }
assert ( rows [ " hand:a " ] [ " sum " ] , rows [ " hand:a " ] [ " fixed " ] ) == ( 10 , True )
assert rows [ " hand:a " ] [ " note " ] == " 대가 5호표 "
def test_거울_TS_가_같은_값 ( parsed ) :
calc = " " . join (
IMPORTS . sub ( " " , ( ROOT / " B09_Estimation " / f " B09_Estimation_ { n } .ts " ) . read_text ( " utf-8 " ) )
for n in ( " Calc_Base " , " Calc_Bill " )
)
users = [
_load ( parsed [ " 거창 " ] , INPUTS , mode = " take " ) [ 0 ] ,
_load ( parsed [ " 영월 " ] , INPUTS , mode = " price " ) [ 0 ] ,
{ " bill " : { " v " : 2 , " rows " : [ { " key " : " earth:4 " , " ref " : None , " price " : { " labor " : 3 } } ,
{ " key " : " hand:a " , " qty " : 2 , " price " : { " material " : 4 } } ] } } ,
] # fmt: skip
body = (
f " \n const i = { json . dumps ( INPUTS , ensure_ascii = False ) } ; "
f " \n const us = { json . dumps ( users , ensure_ascii = False ) } ; "
" \n console.log(JSON.stringify(us.map((u) => computeEstimation(i, u, null, {} )))); "
)
result = subprocess . run (
[ " node " , " --experimental-strip-types " , " --no-warnings " ,
" --input-type=module-typescript " ] ,
input = calc + body , capture_output = True , text = True , encoding = " utf-8 " , timeout = 60 ,
) # fmt: skip
assert result . returncode == 0 , result . stderr
assert json . loads ( result . stdout ) == [ compute_estimation ( INPUTS , u , None , { } ) for u in users ]
def test_이음_같은_책_일위대가_이음_단가_어긋남_알림 ( parsed ) :
got = parsed [ " 기번3 " ]
link = { " tab " : " dg " , " key " : " DG:5 " , " lib " : " precombo@personal@1-3@SC-000001 " }
made , report , _ = _load ( got , links = { 5 : link } )
lines = [ e for e in made [ " bill " ] [ " rows " ] if e . get ( " ref " ) ]
assert report [ " 이음 " ] == 3 and len ( lines ) == 3 # 대가 5호표(종단규준틀) 공구마다
assert lines [ 0 ] [ " ref " ] == link and lines [ 0 ] [ " price " ] [ " labor " ] == 21936 # 고정 단가 곁에 남김
same = { " daega " : [ { " no " : 5 , " key " : " DG:5 " , " labor " : 21936 , " material " : 3885 , " expense " : 0 ,
" sum " : 25821 } ] } # fmt: skip
rows = compute_estimation ( EMPTY , made , None , same ) [ " bill " ] [ " rows " ]
assert imp . differences ( got , report , rows ) == [ ] # 실무자료 사본 = 엑셀 단가 그대로
other = { " daega " : [ { * * same [ " daega " ] [ 0 ] , " labor " : 30000 , " sum " : 33885 } ] }
rows = compute_estimation ( EMPTY , made , None , other ) [ " bill " ] [ " rows " ]
diff = imp . differences ( got , report , rows )
assert len ( diff ) == 3 and { d [ " 까닭 " ] for d in diff } == { " 이음 단가 = 우리 셈 " }
# 사본이 없어짐(연결이 단가를 못 냄) = 고정 단가
rows = compute_estimation ( EMPTY , made , None , { } ) [ " bill " ] [ " rows " ]
assert imp . differences ( got , report , rows ) == [ ]
def test_이음_찾기_같은_파일_이름_본뜬만 ( tmp_path , monkeypatch ) :
from B09_Estimation import B09_Estimation_Router_Import as route
folder = tmp_path / " precombo "
folder . mkdir ( )
docs = {
" SC-000001 " : { " 층 " : " 실무자료 " , " 키 " : " HP-000001 제5호표 " , " 파일 " : " a.xlsx " } ,
" SC-000002 " : { " 층 " : " 실무자료 " , " 키 " : " HP-000001 제5호표 2째 줄 " , " 파일 " : " a.xlsx " } ,
" SC-000003 " : { " 층 " : " 실무자료 " , " 키 " : " HP-000002 제8호표 " , " 파일 " : " b.xlsx " } ,
" SC-000004 " : { " 층 " : " 실무자료 " , " 키 " : " HP-000001 제9호표 " , " 파일 " : " a.xlsx " } ,
}
for key , origin in docs . items ( ) :
( folder / f " { key } .json " ) . write_text ( json . dumps ( { " 본뜬 " : origin } ) , encoding = " utf-8 " )
class Got :
layer , owner = " personal " , " 1/3 "
def grant ( session , layer ) :
if layer != " personal " :
raise route . HTTPException ( status_code = 409 )
return Got
monkeypatch . setattr ( route . access , " grant " , grant )
monkeypatch . setattr ( route . refs , " folder_of " , lambda layer , owner : tmp_path )
found = route . _found ( { } , " a.xlsx " , { 5 , 8 } )
assert found == { 5 : ( " personal " , " 1/3 " , " SC-000001 " ) } # 감싼 줄 · 다른 파일 · 안 쓰는 번 뺌