- 새 M02_Library_Flat: SK 에 SK = 더한줄 로직 한 줄에 원본 몸 통째(덮개 · 뺀줄 · 고름 · 더한줄 · % 덧줄까지 같은 금액) · SC 에 SK = 로직 줄 + 패치 · SK 에 SC = 줄마다 더한줄(수량 = 저장 수량.손 › 실무수량 › 1) · 바탕 SK · 패치 SK 참조 = 덮개 겹침 · 줄 본뜬 {층, 주인, 키, 이름, 때}
- 저장 · 읽기 · 층 복사 때 서버가 펼침(못 펼친 줄 = 옛 참조 그대로) · SC 안 SC(하위 호표)는 저장 · 읽기에서 참조 그대로(엑셀 하위 호표 끝수 · B09 하위 대가) · 찾아 넣기 길만 펴 넣음
- 길: POST {kind}-flat(넣을 줄 · 못 펼침 400) · POST {kind}-reset(미리 보기 걷힐 수 · 씀)
- 초기화 = 덮개 · 뺀줄 · 더한줄 단가 걷음(안쪽 넣은 몸까지) · 항목 · 수량 · 입력값 · 고름 남김
- 옮기기 = migrate(apply) · 전후 1 단위 금액 같을 때만 씀 · 공유 storage 는 브레인
- 둘째 기초 세부 셈 = 넣은 SK 몸이면 이름 없는 SK 로(Sangeun_Calc.sub_run)
- B09: SC 사본 덮개 줄 = 굳힌 줄(굳힌 {키, 줄 …}) → 사본 셈 · 대가 블록이 덮개를 읽음(옛 산근 길은 덮개 무시 · 실측 6,451 대 M02 210,866)
- 733 지문 = 옛 참조 줄 펼친 꼴로 대조
- 낡은 시험 고침(까닭 = 단가산출이 낀 참조가 이제 펼쳐 담김): 715 store(바탕 · 사전조합 SK · 층 복사) · 717-2a 목록 바탕 · 717-4 SC 안 SK · 719-1 원본판(원본 고쳐도 그대로) · 719-2 속 일위대가(빠진 줄 없음 · 쓰지 않는 _calc_given 지움) · 725-1 SK 안 SC · 729 SK 줄 본뜬 · 733-3 쓰는 곳 = 구조물 조합(SK 안 SK 참조 없음) · 572 지문 대조 대상
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
254 lines
14 KiB
Python
254 lines
14 KiB
Python
"""실무문서 단가산출 → 기초 · SK · 고름 대기 · 일위대가 이음 · 남은 줄 막음(PLAN 729-1 · 729-2 · 요구서 내역19).
|
||
|
||
북부청 임도(실물 엑셀)를 임시 층에 올려 맞춤셈 → [단가산출 만들기] →
|
||
기초잡석채집 · 운반 · 다짐(엑셀만 덤프트럭) · 철근가공조립 Type-Ⅰ · Type-Ⅱ · 구조물되메우기 = SK ·
|
||
SK 품 = 엑셀 품(차이는 엑셀 값 그대로) · 같은 단가산출 = 기초(덮개 없음) + 별칭 제안(짝 = 기초까지만) ·
|
||
고름 대기 = SK 안 만듦 → 고른 기초로 SK · 안 이은 줄 남으면 만들기 409(미리 보기 = 같은 줄) ·
|
||
사용자가 고르면 만듦 · 노임 = 이름 이음 · 기계 = EQ 이음 · master_data 원본 그대로. 층 = 임시 폴더.
|
||
729-4 = 단위 성격 다름(계수 · 고름 대기) · 못옮김 잇기 · 남은 못옮김 = 일위대가 막음.
|
||
"""
|
||
|
||
import json
|
||
|
||
from M01_MasterData import M01_MasterData_Store as m01
|
||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
|
||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||
from M02_MasterTemplete import M02_Hopyo_Option as option
|
||
from M02_MasterTemplete import M02_Hopyo_Ours as ours
|
||
from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch
|
||
from M02_MasterTemplete import M02_Hopyo_Store as hs
|
||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||
from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401
|
||
|
||
BUILD = "/api/m02/layers/personal/hopyo-build"
|
||
LEFT = "/api/m02/layers/personal/hopyo-unlinked"
|
||
SAMPLES = { # 블록 번 → (이름 앞, 엑셀 규격 글)
|
||
33: "기초잡석채집,운반,다짐",
|
||
47: "철근가공조립(현장)", # Type-Ⅰ(소량 시공) — 철근공 4.2 · 보통인부 1.4
|
||
72: "철근가공조립(현장)", # Type-ll — 철근공 2.67 · 보통인부 1.02
|
||
12: "구조물되메우기(인력100%)",
|
||
}
|
||
|
||
|
||
def _same(a: float, b: float) -> bool:
|
||
return abs(a - b) <= 1e-9 or option.near(a, b, fit.TOL[0], option.ru(a))
|
||
|
||
|
||
def _by_type(rows: list[dict]) -> dict[str, float]:
|
||
out: dict[str, float] = {}
|
||
for x in rows:
|
||
k = "+".join(sorted(x["갈래"]))
|
||
out[k] = out.get(k, 0.0) + x["시간"]
|
||
return out
|
||
|
||
|
||
def _sk_docs(folder) -> dict[int, dict]:
|
||
out = {}
|
||
for path in sorted((folder / "sangeun").glob("SK-*.json")):
|
||
doc = json.loads(path.read_bytes())
|
||
for one in [doc["본뜬"], *doc["본뜬"].get("또", [])]: # 또 = 같은 내용 SK 를 쓴 블록(733-1)
|
||
out[int(one["키"].rsplit("단산", 1)[1])] = doc
|
||
return out
|
||
|
||
|
||
def test_실무_단가산출_기초_SK_고름대기_일위대가_막음(client):
|
||
before = master_hash()
|
||
folder = layers.personal_dir(7, 42)
|
||
key = _post(client).json()["키"]
|
||
url = f"{URL.format('personal')}/{key}/sangeun"
|
||
assert client.post(url).status_code == 409 # 맞춤 셈 전
|
||
fit.run_job(folder, key)
|
||
got = client.post(url, json={})
|
||
assert got.status_code == 200, got.text
|
||
got = got.json()
|
||
assert (
|
||
got["기초"] + got["SK"] + len(got["고름대기"])
|
||
== got["블록"]
|
||
== len(got["만듦"]) + len(got["고름대기"])
|
||
)
|
||
assert got["SK"] > 0 and got["기초"] > 0 and got["고름대기"]
|
||
docs, whole = _sk_docs(folder), m01.loaded()[1]
|
||
assert all(d["바탕"] and d["바탕"]["로직"] for d in docs.values()) # 바탕 없는 SK 0
|
||
assert not set(docs) & {x["번"] for x in got["고름대기"]}
|
||
# SK 품 = 엑셀 품
|
||
for no, head in SAMPLES.items():
|
||
doc = docs[no]
|
||
assert doc["이름"].startswith(head) and doc["본뜬"]["갈래"] == "SK", (no, doc["본뜬"])
|
||
excel = doc["본뜬"]["엑셀품"]
|
||
with store.at(folder):
|
||
ours_q = skpatch.measure(doc, whole)
|
||
assert set(ours_q["인력"]) == set(excel["인력"]), no
|
||
assert all(_same(q, ours_q["인력"][t]) for t, q in excel["인력"].items()), no
|
||
theirs, mine = _by_type(excel["장비"]), _by_type(ours_q["장비"])
|
||
assert set(theirs) == set(mine), no
|
||
assert all(_same(q, mine[t]) for t, q in theirs.items()), no
|
||
dump = [x for x in docs[33]["더한줄"] if x.get("로직") == "GC000268"]
|
||
assert any("덤프트럭" in ours.eq_of(_eq(x))[0] for x in dump) # 엑셀만 장비 = 더한줄
|
||
assert docs[12]["덮개"]["labor:LB000002#0"]["qty"] == 0.1 # 엑셀 값 그대로(우리 0.01)
|
||
assert docs[47]["더한줄"][0]["로직"] == "GC000206" # 조합 둘째 = 더한줄 기초
|
||
# 같은 단가산출 = 기초 + 별칭 제안(짝 = 기초까지만)
|
||
base = next(d for d in docs.values() if d["본뜬"]["갈래"] == "기초")
|
||
assert base["덮개"] == {} and base["더한줄"] == [] and base["뺀줄"] == []
|
||
book = ledger.read(folder)
|
||
hp = hs.read(folder, key)
|
||
lines = {
|
||
int(x["참조"]["번"]): x
|
||
for h in reversed(hp["호표"])
|
||
for x in reversed(h["줄"])
|
||
if (x.get("참조") or {}).get("종류") == "단산"
|
||
}
|
||
no = int(base["본뜬"]["키"].rsplit("단산", 1)[1])
|
||
pair = book[ledger.key(lines[no])]["짝"]
|
||
assert pair == [{"로직": base["바탕"]["로직"], "입력값": base["입력값"], "배": 1, "몫": None}]
|
||
assert book[ledger.key(lines[12])]["짝"][0]["로직"] == docs[12]["바탕"]["로직"]
|
||
assert got["별칭"] > 0
|
||
# 다시 = 그대로 · 고름 대기 = 고른 기초로 SK
|
||
again = client.post(url, json={}).json()
|
||
assert again["만듦"] == [] and again["그대로"] == len(docs)
|
||
wait = got["고름대기"][0]
|
||
code = (wait["후보"] or [{"로직": "GF000113"}])[0]["로직"]
|
||
picked = client.post(url, json={"바탕": {str(wait["번"]): {"로직": code}}}).json()
|
||
assert [x["번"] for x in picked["만듦"]] == [wait["번"]]
|
||
assert _sk_docs(folder)[wait["번"]]["바탕"]["로직"] == code
|
||
assert client.post(url, json={"바탕": {"9999": {"로직": code}}}).status_code == 404
|
||
assert client.post(url, json={"바탕": {str(wait["번"]): {"로직": "XX"}}}).status_code == 400
|
||
# 안 이은 줄 남으면 만들기 막힘 · 미리 보기 = 같은 줄 · 고르면 만듦
|
||
body = {"층": "personal", "키": key, "번": 1, "줄": []}
|
||
left = client.post(LEFT, json=body).json()
|
||
res = client.post(BUILD, json=body)
|
||
assert res.status_code == 409 and left["수"] > 0
|
||
assert res.json()["detail"]["남은줄"] == left["남은줄"]
|
||
assert not list((folder / "precombo").glob("SC-*.json")) # 막힘 = 저장 0
|
||
picks = [{"차례": x["차례"], "뺌": "우리에 없음"} for x in left["남은줄"]]
|
||
ok = client.post(BUILD, json={**body, "줄": picks})
|
||
assert ok.status_code == 200, ok.text
|
||
doc = json.loads((folder / "precombo" / f"{ok.json()['참조']['키']}.json").read_bytes())
|
||
refs_sk = [x for x in doc["담은로직"] if str((x.get("본뜬") or {}).get("키")).startswith("SK")]
|
||
assert refs_sk # 단가산출 줄 = SK 펼쳐 담음(740-1 · 로직 줄 + 패치 · 본뜬 = 그 SK)
|
||
assert client.post(LEFT, json={**body, "줄": picks}).json()["수"] == 0
|
||
assert master_hash() == before
|
||
|
||
|
||
def test_단위_성격_다름_계수_못옮김_잇기_남으면_일위대가_막음(client):
|
||
"""729-4 — 728-2 단위 환산 뒤 500 0 · 성격 다른 단위 = 계수 거꾸로(자신 있을 때) · 아니면 고름 대기
|
||
(후보 계수 칸) · 못옮김 잇기 = SK 더한줄 · 뺌 = 놓음 · 남으면 그 SK 를 쓰는 일위대가 막음."""
|
||
before = master_hash()
|
||
folder, whole = layers.personal_dir(7, 42), m01.loaded()[1]
|
||
key = _post(client).json()["키"]
|
||
url = f"{URL.format('personal')}/{key}/sangeun"
|
||
fit.run_job(folder, key)
|
||
got = client.post(url, json={})
|
||
assert got.status_code == 200, got.text
|
||
got = got.json()
|
||
wait = {x["번"]: x for x in got["고름대기"]}
|
||
assert wait[30]["까닭"].startswith(skpatch.UNIT_WHY) # 큰돌채집운반 ㎡ ↔ 100개
|
||
assert wait[30]["후보"][0]["계수"] == {"가": "100개", "나": "㎡", "갈래": "사용자"}
|
||
docs = _sk_docs(folder)
|
||
coef = [d for d in docs.values() if d.get("환산")]
|
||
assert coef # 계수 거꾸로 = 셈 됨 · 엑셀 품 그대로
|
||
for d in coef:
|
||
with store.at(folder):
|
||
mine = skpatch.measure(d, whole)
|
||
assert all(_same(q, mine["인력"][t]) for t, q in d["본뜬"]["엑셀품"]["인력"].items())
|
||
lost = {x["번"]: x["못옮김"] for x in got["만듦"] if x["못옮김"]}
|
||
a, b = lost[95] # 도장 — 엑셀만 자재 둘
|
||
assert a["갈래"] == "자재" and a["id"] != b["id"] and a["수량"] == 0.322
|
||
hp = hs.read(folder, key)
|
||
no = next(h["번"] for h in hp["호표"] for x in h["줄"]
|
||
if (x.get("참조") or {}).get("종류") == "단산" and int(x["참조"]["번"]) == 95) # fmt: skip
|
||
body = {"층": "personal", "키": key, "번": no, "줄": []}
|
||
|
||
def left() -> list:
|
||
return [x for x in client.post(LEFT, json=body).json()["남은줄"] if x.get("단산") == 95]
|
||
|
||
assert [len(x["못옮김"]) for x in left()] == [2]
|
||
assert client.post(BUILD, json=body).status_code == 409
|
||
|
||
def link(*rows: dict):
|
||
return client.post(url, json={"바탕": {"95": {"잇기": list(rows)}}})
|
||
|
||
assert link({"id": "자재:없음", "자재": "MT000010"}).status_code == 400
|
||
assert link({"id": a["id"], "기계": "EQ000001"}).status_code == 400
|
||
assert link({"id": a["id"], "자재": "MT999999"}).status_code == 400
|
||
one = link({"id": a["id"], "자재": "MT000010"}).json()
|
||
assert [x["못옮김"] for x in one["만듦"]] == [[b]]
|
||
doc = _sk_docs(folder)[95]
|
||
tb = float(calc.ratio_of(doc, whole.logic(doc["바탕"]["로직"]), whole))
|
||
add = doc["더한줄"][-1]
|
||
assert add["종류"] == "재료" and add["요소"] == "MT000010" and _same(add["수량"], 0.322 * tb)
|
||
assert [len(x["못옮김"]) for x in left()] == [1]
|
||
two = link({"id": a["id"], "자재": "MT000010"}, {"id": b["id"], "뺌": "현장 지급"}).json()
|
||
assert two["만듦"][0]["못옮김"] == []
|
||
assert _sk_docs(folder)[95]["본뜬"]["놓음"][0]["id"] == b["id"]
|
||
assert left() == []
|
||
assert master_hash() == before
|
||
|
||
|
||
def test_별칭_체크_줄_못옮김_SK_남은줄_그대로_SK_남은못옮김(client):
|
||
"""729-5 — 확인한 로직 줄이 못옮김 남은 차이 SK 의 바탕이면 남은 줄(차이 잃지 않음) · 만들기 409 ·
|
||
그대로 SK 의 못옮김 = 응답 남은못옮김 · 잇기 뒤 사라짐."""
|
||
before = master_hash()
|
||
folder = layers.personal_dir(7, 42)
|
||
key = _post(client).json()["키"]
|
||
url = f"{URL.format('personal')}/{key}/sangeun"
|
||
fit.run_job(folder, key)
|
||
made = {x["번"]: x for x in client.post(url, json={}).json()["만듦"]}
|
||
sk = made[60] # 제27호표 2째 줄 와이어메시바닥깔기 — SK(바탕 GC000665) · 엑셀만 와이어메쉬
|
||
assert sk["갈래"] == "SK" and len(sk["못옮김"]) == 1
|
||
check = {"차례": 2, "대상": [{"로직": "GC000665", "배": 1}]} # 별칭 자동 체크와 같은 꼴
|
||
body = {"층": "personal", "키": key, "번": 27, "줄": [check]}
|
||
|
||
def mine(rows: list) -> list:
|
||
return [x for x in rows if x["차례"] == 2]
|
||
|
||
left = client.post(LEFT, json=body).json()["남은줄"]
|
||
assert [(x["단산"], x["SK"], x["못옮김"]) for x in mine(left)] == [(60, sk["키"], sk["못옮김"])]
|
||
res = client.post(BUILD, json=body)
|
||
assert res.status_code == 409 and mine(res.json()["detail"]["남은줄"]) == mine(left)
|
||
# 그대로 SK 의 못옮김 = 응답 남은못옮김(만듦[].못옮김 과 같은 꼴)
|
||
again = client.post(url, json={}).json()
|
||
rest = {x["번"]: x for x in again["남은못옮김"]}
|
||
assert again["만듦"] == [] and set(rest) == {n for n, x in made.items() if x["못옮김"]}
|
||
assert rest[60] == {"번": 60, "이름": sk["이름"], "키": sk["키"], "못옮김": sk["못옮김"]}
|
||
cut = {"id": sk["못옮김"][0]["id"], "자재": "MT000010"} # 이음 = SK 더한줄(차이 남음)
|
||
linked = client.post(url, json={"바탕": {"60": {"잇기": [cut]}}}).json()
|
||
assert [(x["번"], x["키"], x["못옮김"]) for x in linked["만듦"]] == [(60, sk["키"], [])]
|
||
assert 60 not in {x["번"] for x in client.post(url, json={}).json()["남은못옮김"]}
|
||
left = client.post(LEFT, json=body).json()["남은줄"]
|
||
assert mine(left) == []
|
||
picks = [{"차례": x["차례"], "뺌": "우리에 없음"} for x in left]
|
||
ok = client.post(BUILD, json={**body, "줄": [check, *picks]})
|
||
assert ok.status_code == 200, ok.text
|
||
doc = json.loads((folder / "precombo" / f"{ok.json()['참조']['키']}.json").read_bytes())
|
||
# 차이 SK = 펼쳐 담음(740-1 · 본뜬 = 그 SK)
|
||
assert any((x.get("본뜬") or {}).get("키") == sk["키"] for x in doc["담은로직"])
|
||
assert master_hash() == before
|
||
|
||
|
||
def _eq(one: dict) -> str:
|
||
return str((one.get("입력값") or {}).get("기계") or "")
|
||
|
||
|
||
def test_노임_이름_이음_기계_EQ_이음_사용자_이음(client):
|
||
with store.at(layers.personal_dir(7, 42)):
|
||
linker = hfix.Linker(by_name=True)
|
||
labor = {"갈래": "노임", "이름": "보통인부", "단위": "인"}
|
||
price = {"노무비": hfix.Decimal(1), "재료비": hfix.Decimal(0), "경비": hfix.Decimal(0)}
|
||
assert linker.link(labor, price)["노임"] == "LB000002" # 단가 달라도 이름으로
|
||
machine = {"갈래": "기계", "이름": "덤프트럭", "규격": "15ton", "단위": "hr"}
|
||
code = linker.link(machine, price)["기계"]
|
||
assert code and "덤프트럭" in ours.eq_of(_eq_code(code))[0]
|
||
line = {"갈래": "자재", "이름": "없는자재", "규격": "", "단위": "개", "수량": 2,
|
||
"단가": {"재료비": 10}, "차례": 0} # fmt: skip
|
||
one = hfix.line_of(line, [], {"파일": "x", "호표": 1}, set(), linker, {"노임": "LB000002"})
|
||
assert one["고정"]["이음"] == {"노임": "LB000002", "자재": None, "기계": None}
|
||
assert hfix.linked(one) and not hfix.linked(hfix.line_of(line, [], {}, set(), linker))
|
||
|
||
|
||
def _eq_code(key: str) -> str:
|
||
"""EQ 키(EQ000001) → 기계 번호(0101-0007)."""
|
||
return next(k for k, v in m01.loaded()[1].numbers["EQ"].items() if v.get("키") == key)
|