feat(785-5): 만들기 · 길 — 확인한 고른 것 → SC · SK · 장부 · 새 M02_Hopyo_Router · 판정 · 검산 · 옮기기
- 새 M02_Hopyo_Router — 옛 hopyo 길 모두 M02_Labor_Router 에서 옮김(옛 화면 785-7 까지 돎) · 새 길 hopyo-judge 목록 · 호표 · 블록 · hopyo/{키}/check · hopyo/{키}/move · main 등록 · 시동 warm · Fit.ensure 걷음 · 올림 때 엑셀 사전(Row.lexicon) · 블록 풀이 뒤 셈 · 끝내기 때 저장본 지움
- 새 M02_Hopyo_Judge — 1차 줄 판(Row) + 단산 줄 2차 풀이(Clause → Solve) · 엑셀 칸(이름 · 규격 · 단위 · 단가) · 블록 금액 · 절 몫 · 블록 풀이 파생 저장본(HP 폴더 _derived · 입력 지문 · 지문 다를 때만) · judged(folder, key) · 검산(입력값 = 엔진이 쓴 칸 모두 · 지역 · 자재지역 · 조종원직종 비움 · 맡은 절 단위 줄 · 몫과 품 대조 · 금액 = 일위대가 로직 줄과 같은 셈)
- 새 M02_Hopyo_Make — 고른 것 → SC 줄(같은 별칭 · 줄 본뜬 {호표, 차례, 행} · 수량 = 엑셀 × 몫 × 배 · 같은 기초 둘째 = 한 줄 SC 사본 · 별도계상 자재 · 이음 · 율 · 고정 · 참조) · SK 더한줄(바탕 없음 · SkUnit.adds · Sangeun.Maker.write) · 하위 먼저 · 입력 필요 · 단위 환산 못 정함 = 그 줄(블록)만 막힘 · 장부 고른 것 꼴 짝 · 이미 옮긴 것 = 덮음 없으면 409(남의 SC 덮지 않음) · 확인한 줄 몸(Lines) = Build 에서 옮김
- Build 나눔 — 모두 고정 길 · Builder · _order · _origins · build_all 그대로(B09 빌림) · 확인한 줄 몸 → Make.Lines
- SkUnit · Sangeun — SkPatch · Fit · Basis 부름 걷음(기계경비 기초 차례 SkUnit 으로 · 근거 꼬리표 = Clause.basis · 맞춤 셈 409 없앰 · 재료 고름 MO 됨) · Kids 가 _same_mark 가짐
- 시험 새 test_642_5_make(판정 · 검산 · 옮기기 · 예시 1 · 2 · 3 · 덮음 409 · 막는 길) · test_m02_hopyo_excel 마당에 새 Router 더함 · HOPYO_MAX_BYTES 자리(길 옮긴 까닭)
- 낡은 시험 고침: test_718_1 · test_729 「맞춤 셈 전 409」 뺌(맞춤 뒤 셈 안 기다림) · test_628 맞춤셈 캐시 넣기 뺌(근거 = 조항 색인) · test_729 근거 없음 꼴 · 고정 이음 유가 칸(785-3 LINKS 늘어난 까닭)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,8 @@
|
||||
`{까닭, 남은줄}`(저장 안 함 · `save` 거짓 = 막지 않고 `남은줄` 만) · 미리 보기 = `unlinked`(쓰기 없음).
|
||||
수량(742-1 · 요구 584-1) = 엑셀 수량을 줄 · 자재 저장 수량(`{손}`)으로 · 같은 값 `본뜬.실무수량 {줄: {차례: 수},
|
||||
자재: {코드: 수}}`(출처 기록 · 엑셀 대조 `M02_Hopyo_Compare` — 하위 호표 것까지 `excel_given`).
|
||||
785-5 — 확인한 줄 몸 = `M02_Hopyo_Make.Lines`(옛 대상 꼴 `put` · 새 고른 것 꼴 `picks` = 몸 `{차례: {고른}}`) ·
|
||||
여기는 모두 고정 길 · 줄 차례 · 쓰기(`Builder` · `_order` · `_origins` · `build_all` = B09 빌림).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -234,82 +236,17 @@ class Builder:
|
||||
self, checked: dict[int, dict[str, Any]], whole: list[dict[str, Any]], save: bool = True
|
||||
) -> dict:
|
||||
"""`save` 거짓 = 사전조합 안 씀 · `{문서, 빠진줄, 줄수}`(704-4 [구조물 조합으로])."""
|
||||
from M02_MasterTemplete import M02_Hopyo_Make as hmake # 확인한 줄 몸(785-5)
|
||||
|
||||
stamp = utc_now_text()
|
||||
hopyo, file = self.hopyo, self.doc.get("파일") or ""
|
||||
lines, mats, missing, used, mat_lines = [], {}, [], set(), {}
|
||||
nation: dict[tuple, dict[str, Any]] = {}
|
||||
taken, linker, poomsem, kids, kid_n = set(), hfix.Linker(self.strict and save), [], set(), 0
|
||||
left: list[dict[str, Any]] = []
|
||||
where = {"파일": file, "호표": self.no}
|
||||
rows = hopyo.get("줄") or []
|
||||
|
||||
def put(line: dict[str, Any], one: dict[str, Any], alias: str | None) -> None:
|
||||
if "이음" in one:
|
||||
others = [x for x in rows if x is not line]
|
||||
lines.append(hfix.line_of(line, others, where, taken, linker, one["이음"]))
|
||||
return
|
||||
qty = Decimal(str(line.get("수량") or 0)) * Decimal(str(one["배"]))
|
||||
memo = _memo(line, one)
|
||||
if "전국" in one:
|
||||
at = (one["이름"], one["규격"], one["단위"])
|
||||
have = nation.get(at)
|
||||
hand = qty + Decimal(str(have["수량"]["손"])) if have else qty
|
||||
head = {"이름": at[0], "규격": at[1], "단위": at[2], "수량": {"손": _num(hand)}}
|
||||
nation[at] = {**head, **(hmat.marks(line, at[0]) if not have else {
|
||||
k: have[k] for k in labor.MARKS if k in have
|
||||
})} # fmt: skip
|
||||
missing.append(_missing(line, NATIONAL))
|
||||
return
|
||||
if "자재" in one:
|
||||
have = mats.get(one["자재"])
|
||||
hand = qty + Decimal(str(have["수량"]["손"])) if have else qty
|
||||
name = self.mat_names.get(one["자재"], "")
|
||||
mats[one["자재"]] = {
|
||||
"코드": one["자재"],
|
||||
"수량": {"손": _num(hand)},
|
||||
**(
|
||||
have
|
||||
and {k: have[k] for k in labor.MARKS if k in have}
|
||||
or hmat.marks(line, name)
|
||||
),
|
||||
}
|
||||
if not have:
|
||||
mat_lines[one["자재"]] = line
|
||||
return
|
||||
if "호표" in one:
|
||||
kids.add(one["호표"])
|
||||
ref = {
|
||||
"층": refs.here()[0],
|
||||
"주인": refs.here()[1],
|
||||
"키": self.done.get(mark(self.key, one["호표"]), "SC-미리"),
|
||||
}
|
||||
lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
|
||||
return
|
||||
row = {"로직": one["로직"], "입력값": one["입력값"], "메모": memo}
|
||||
kept, why = sklink.kept(self, line, one, qty, alias, memo)
|
||||
if why:
|
||||
left.append(sklink.left(line, why))
|
||||
if kept is not None:
|
||||
lines.append(kept)
|
||||
return
|
||||
if one["로직"] not in used:
|
||||
used.add(one["로직"])
|
||||
lines.append({**row, "별칭": alias, "수량": {"손": _num(qty)}})
|
||||
return
|
||||
at = int(line.get("차례", 0))
|
||||
wrap = { # 같은 로직 둘째 — 한 줄 사전조합으로 감쌈
|
||||
"이름": f"{hopyo['이름']} — {line.get('이름')}",
|
||||
"단위": line.get("단위") or None,
|
||||
"구분": ORIGIN,
|
||||
"상세구분": file,
|
||||
"담은로직": [{**row, "별칭": alias, "수량": {"손": 1}}],
|
||||
"자재": [],
|
||||
"본뜬": self._origin(f"{mark(self.key, self.no, at)} {one['로직']}", stamp),
|
||||
}
|
||||
ref = (
|
||||
{"키": "SC-미리"} if self.dry else hkids.save(wrap, self.same)[0]
|
||||
) # 미리 보기 = 쓰기 없음
|
||||
lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
|
||||
where = {"파일": file, "호표": self.no}
|
||||
body = hmake.Lines(self, rows, where, stamp, hfix.Linker(self.strict and save))
|
||||
lines, mats, missing, left = body.lines, body.mats, body.missing, body.left
|
||||
taken, linker, kids, put = body.taken, body.linker, body.kids, body.put
|
||||
nation, mat_lines = body.nation, body.mat_lines
|
||||
poomsem, kid_n = [], 0
|
||||
|
||||
book: dict[str, dict[str, Any]] = {}
|
||||
if whole:
|
||||
@@ -318,6 +255,12 @@ class Builder:
|
||||
book[ledger.hopyo_key(hopyo)] = self._row(hopyo, "짝", whole)
|
||||
for at, line in enumerate(rows):
|
||||
got = checked.get(at)
|
||||
if got and "고른" in got: # 새 길 고른 것(785-5 · Make 가 검사 · 다시 셈)
|
||||
poomsem.append(at)
|
||||
body.picks(at, line, got["고른"])
|
||||
pairs = [hmake.ledger_pick(p) for p in got["고른"]]
|
||||
book[ledger.key(line)] = self._row(line, "짝", pairs)
|
||||
continue
|
||||
if whole:
|
||||
missing.append(_missing(line, WHOLE))
|
||||
continue
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
"""M02 엑셀 불러오기 판정 · 검산(PLAN 785-5 · 요구 642 · 설계 642-A 2-6 ⑤ · 2-9) — 서버 단독 · 읽기만.
|
||||
|
||||
판정 = 1차 줄 판(`M02_Hopyo_Row.rows`) + 단산 줄 2차 블록 풀이(`Clause.basis` 후보 → `Solve.parts` →
|
||||
`Solve.block`) · 목록(`listing`) · 호표 하나(`hopyo`) · 블록 하나(`block`).
|
||||
블록 풀이 = 파생 저장본(HP 폴더 `_derived/hopyo_judge_{키}` · 입력 지문 = HP + M01 파일 내용 + 풀이 판 ·
|
||||
지문 다를 때만 다시 셈 · GET 은 안 지움 · 올림 뒤 한 번 뒤에서 `ensure` · 셈 중 = `셈중` · 판정 None) ·
|
||||
블록 판정 읽기 = `judged`(Marks 칠) · [끝내기] = `forget`.
|
||||
줄 판 줄에 엑셀 칸(이름 · 규격 · 단위 · 단가) · 블록에 엑셀 금액 · 절에 몫(785-6 화면이 읽음).
|
||||
검산(`check`) = 고른 것마다 — 기초 = 입력값(`inputs` · 엔진이 쓴 칸 모두 · 지역 · 자재지역 · 조종원직종
|
||||
비움) · 입력필요 · 엑셀 단위 줄과 품 대조(`Solve.pair`) · 금액(엔진 한 번 · 입력 필요면 셈 안 함) /
|
||||
그 밖 = 「확인」 · 수량 = 엑셀 × 몫 × 배(`Row.quantity` · 사용자 배 우선).
|
||||
저장 없음 · 지어 넣은 값 없음(입력 필요 칸은 비워 둠 · 안 쓰는 칸 = 기초 기본값).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import threading
|
||||
from collections import Counter
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from common_util import common_util_derived_store as derived
|
||||
from M02_MasterTemplete import M02_Hopyo_Bind as hb
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hbuild
|
||||
from M02_MasterTemplete import M02_Hopyo_Clause as clause
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_Row as hrow
|
||||
from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun
|
||||
from M02_MasterTemplete import M02_Hopyo_Solve as solve
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
mf, ma = hb.mf, hb.ma
|
||||
EXCEL = ("이름", "규격", "단위", "단가")
|
||||
CHECKED = "확인"
|
||||
KIND = "hopyo_judge_" # 파생 저장본 종류 = 이것 + HP 키(HP 폴더 `_derived/`)
|
||||
VERSION = "785-5.1" # 풀이 꼴이 바뀌면 올림
|
||||
_LOCK, _RUNNING = threading.Lock(), set()
|
||||
|
||||
|
||||
def master() -> mf.Master:
|
||||
return hb.m01.cm.master()
|
||||
|
||||
|
||||
def _bad(text: str, status: int = 400) -> store.StoreError:
|
||||
return store.StoreError(status, text)
|
||||
|
||||
|
||||
def _blocks(book: dict[str, Any]) -> dict[int, dict[str, Any]]:
|
||||
return {int(b["번"]): b for b in book.get("단산") or [] if b.get("번") is not None}
|
||||
|
||||
|
||||
def _unit(book: dict[str, Any], no: int) -> dict[str, Any]:
|
||||
got = next((h for h in book.get("호표") or [] if int(h["번"]) == int(no)), None)
|
||||
if got is None:
|
||||
raise _bad(f"없는 호표 「제{no}호표」", 404)
|
||||
return got
|
||||
|
||||
|
||||
def _made(folder: Path, key: str, to: Path) -> tuple[dict[str, str], dict[str, str]]:
|
||||
"""(만든 SC {본뜬 키: SC}, 만든 SK {본뜬 키: SK}) — 만들 층 · 이 파일 층 것만."""
|
||||
source = refs.where(folder)
|
||||
return hbuild.confirmed(to, source), sangeun.made(to, key, source)
|
||||
|
||||
|
||||
# ── 기초 입력값 ───────────────────────────────────────────────────────
|
||||
def inputs(m: mf.Master, code: str, given: dict[str, Any] | None) -> tuple[dict, list[str]]:
|
||||
"""(저장 입력값, 입력필요) — 고른 값(그 기초 칸 · 빈 값 뺌) + 안 쓰는 칸 = 기초 기본값 ·
|
||||
쓰는데 빈 칸 = 입력필요(비워 둠) · 지역 · 자재지역 · 조종원직종 = 비움(프로젝트가 정함)."""
|
||||
row = m.logic(code)
|
||||
names = [s["이름"] for s in hb.specs_of(row)]
|
||||
have = {
|
||||
k: v
|
||||
for k, v in (given or {}).items()
|
||||
if k in names and k not in hb.SKIP and v not in (None, "")
|
||||
}
|
||||
need = [n for n in names if n in hb._needed(m, code, row, have) and n not in hb.SKIP]
|
||||
base = ma.defaults(m, row)
|
||||
idle = [n for n in names if n not in have and n not in need and n not in hb.SKIP]
|
||||
full = {n: hb.plain(base[n]) for n in idle if base.get(n) is not None}
|
||||
return {**full, **{k: hb.plain(v) for k, v in have.items()}}, need
|
||||
|
||||
|
||||
def money(m: mf.Master, code: str, given: dict[str, Any]) -> dict[str, Any]:
|
||||
"""기초 한 줄 셈(일위대가 로직 줄과 같은 길 `Labor_Doc.run_line` · 끝수 · 비운 지역 칸 = 자동값 ·
|
||||
참고 셈만) → {노무비, 재료비, 경비, 계} · 못 셈 = {계 None, 까닭}."""
|
||||
got = labor.run_line(m, code, given)
|
||||
if got.get("결과") is not None:
|
||||
return {"결과": hb.plain(got["결과"])}
|
||||
if got.get("기준") is None:
|
||||
return {"계": None, "까닭": got.get("까닭")}
|
||||
return {k: hb.plain(v) for k, v in got["기준"].items()}
|
||||
|
||||
|
||||
# ── 판정 ─────────────────────────────────────────────────────────────
|
||||
def _row(line: dict[str, Any], one: dict[str, Any]) -> dict[str, Any]:
|
||||
return {**one, **{k: line.get(k) for k in EXCEL}}
|
||||
|
||||
|
||||
def _solve(m: mf.Master, blk: dict, line: dict | None, same: list[dict]) -> dict[str, Any]:
|
||||
"""블록 풀이 — 조항 색인 후보 → 절 → 검산 · 판정 + 근거 · 엑셀 금액 · 절 몫."""
|
||||
basis = clause.basis(blk)
|
||||
parts = solve.parts(blk, basis["후보"])
|
||||
got = solve.block(m, blk, parts, {"줄": line, "같은줄": same})
|
||||
for sec, part in zip(got["절"], parts, strict=True):
|
||||
share = part.get("몫")
|
||||
sec["몫"] = share.get("값") if isinstance(share, dict) else share
|
||||
sec["번"] = part.get("번")
|
||||
got["근거"] = {k: basis[k] for k in ("표지", "글", "출처", "후보", "뺌")}
|
||||
got["금액"] = blk.get("금액")
|
||||
return got
|
||||
|
||||
|
||||
# ── 블록 풀이 파생 저장본(올림 뒤 한 번 · 뒤에서) ─────────────────────────
|
||||
def _stamp(key: str, book: dict[str, Any]) -> str:
|
||||
"""입력 지문 — HP(지문 · 판 · 올린때) + M01 첫 층 파일 내용 + 풀이 판."""
|
||||
folder = Path(hb.m01.FOLDER)
|
||||
files = {
|
||||
p.name: derived.file_digest(p)
|
||||
for p in sorted(folder.glob("*.json"))
|
||||
if not p.name.startswith("_")
|
||||
}
|
||||
head = (VERSION, key, book.get("지문"), book.get("판"), book.get("올린때"))
|
||||
return derived.derived_key(*head, files)
|
||||
|
||||
|
||||
def _caller(book: dict[str, Any], no: int) -> tuple[dict | None, list[dict]]:
|
||||
"""블록을 부르는 첫 줄 · 그 호표 다른 줄(B1 · B6) — 일위대가 밖 = (None, [])."""
|
||||
line = sangeun.calls(book)[1].get(no)
|
||||
if not line:
|
||||
return None, []
|
||||
unit = _unit(book, line["_호표"])
|
||||
same = [x for x in unit.get("줄") or [] if x.get("차례") != line.get("차례")]
|
||||
return {k: v for k, v in line.items() if k != "_호표"}, same
|
||||
|
||||
|
||||
def solved(folder: Path, key: str) -> dict[str, Any] | None:
|
||||
"""저장본 `{번 글: 풀이}` — 지문 다르거나 없으면 None(GET 은 안 지움)."""
|
||||
book = hopyo_store.read(folder, key)
|
||||
got = derived.read_derived(hopyo_store._dir(folder), KIND + key, _stamp(key, book))
|
||||
return got["value"] if got else None
|
||||
|
||||
|
||||
def solve_all(folder: Path, key: str) -> dict[str, Any]:
|
||||
"""블록 모두 풀어 저장본으로(부르는 첫 줄 기준)."""
|
||||
book = hopyo_store.read(folder, key)
|
||||
m, out = master(), {}
|
||||
for no, blk in sorted(_blocks(book).items()):
|
||||
out[str(no)] = _solve(m, blk, *_caller(book, no))
|
||||
root = hopyo_store._dir(folder)
|
||||
derived.write_derived(root, KIND + key, _stamp(key, book), out, keep=1)
|
||||
return out
|
||||
|
||||
|
||||
def _work(folder: Path, key: str) -> None:
|
||||
try:
|
||||
solve_all(folder, key)
|
||||
except Exception: # noqa: BLE001 — 뒤 셈 실패 = 다음 읽기에서 다시(파생 저장본)
|
||||
pass
|
||||
finally:
|
||||
with _LOCK:
|
||||
_RUNNING.discard(f"{folder}|{key}")
|
||||
|
||||
|
||||
def _start(folder: Path, key: str) -> None:
|
||||
tag = f"{folder}|{key}"
|
||||
with _LOCK:
|
||||
if tag in _RUNNING:
|
||||
return
|
||||
_RUNNING.add(tag)
|
||||
threading.Thread(target=_work, args=(Path(folder), key), daemon=True).start()
|
||||
|
||||
|
||||
def ensure(folder: Path, key: str) -> bool:
|
||||
"""저장본 있음 = 참 · 없으면 뒤 셈 시작(화면 안 막음 · 같은 파일 한 번) = 거짓."""
|
||||
if solved(folder, key) is not None:
|
||||
return True
|
||||
_start(folder, key)
|
||||
return False
|
||||
|
||||
|
||||
def judged(folder: Path, key: str) -> dict[int, dict[str, Any]] | None:
|
||||
"""블록 판정 `{번: {판정, 고를수}}` — 셈 중 = None(뒤 셈 시작 · Marks 칠이 부름)."""
|
||||
got = solved(folder, key)
|
||||
if got is None:
|
||||
_start(folder, key)
|
||||
return None
|
||||
return {int(no): {"판정": v["판정"], "고를수": v["고를수"]} for no, v in got.items()}
|
||||
|
||||
|
||||
def forget(folder: Path, key: str) -> None:
|
||||
"""[끝내기] — 그 HP 저장본 지움."""
|
||||
shutil.rmtree(derived.derived_dir(hopyo_store._dir(folder), KIND + key), ignore_errors=True)
|
||||
|
||||
|
||||
def listing(folder: Path, key: str, to: Path) -> dict[str, Any]:
|
||||
"""호표 목록(1차 판정 수 · 만든 SC) · 블록 목록(부름 · 표지 · 판정 · 고를수 · 만든 SK) ·
|
||||
`셈중` = 블록 풀이 뒤 셈 중(블록 판정 · 고를수 None)."""
|
||||
book = hopyo_store.read(folder, key)
|
||||
rows = hrow.rows(book, folder, ledger.doc_id(folder, key))
|
||||
sc, sk = _made(folder, key, to)
|
||||
units = []
|
||||
for h in book.get("호표") or []:
|
||||
no = int(h["번"])
|
||||
count = Counter(r["판정"] for r in rows.get(h["번"]) or [])
|
||||
head = {"번": no, **{k: h.get(k) for k in ("이름", "규격", "단위", "단가")}}
|
||||
units.append(
|
||||
{**head, "줄수": len(h.get("줄") or []), "판정": dict(count),
|
||||
"만든": sc.get(hbuild.mark(key, no))}
|
||||
) # fmt: skip
|
||||
call, done = sangeun.callers(book), judged(folder, key)
|
||||
blocks = []
|
||||
for no, b in sorted(_blocks(book).items()):
|
||||
got = (done or {}).get(no) or {"판정": None, "고를수": None}
|
||||
blocks.append(
|
||||
{"번": no, "이름": b.get("이름"), "단위": b.get("단위"), "금액": b.get("금액"),
|
||||
"부름": call.get(no, []), "표지": clause.basis(b)["표지"], **got,
|
||||
"만든": sk.get(sangeun.mark(key, no))}
|
||||
) # fmt: skip
|
||||
out = {"키": key, "파일": book.get("파일"), "셈중": done is None}
|
||||
return {**out, "호표": units, "블록": blocks}
|
||||
|
||||
|
||||
def hopyo(folder: Path, key: str, no: int, to: Path) -> dict[str, Any]:
|
||||
"""호표 하나 — 줄 판(+ 엑셀 칸) · 단산 줄 `풀이`(부르는 첫 줄이면 저장본)."""
|
||||
book = hopyo_store.read(folder, key)
|
||||
unit = _unit(book, no)
|
||||
rows = hrow.rows({**book, "호표": [unit]}, folder, ledger.doc_id(folder, key))[unit["번"]]
|
||||
lines, blocks, m = unit.get("줄") or [], _blocks(book), None
|
||||
cache, first = solved(folder, key) or {}, sangeun.calls(book)[1]
|
||||
out = []
|
||||
for r, line in zip(rows, lines, strict=True):
|
||||
r = _row(line, r)
|
||||
sub = r.get("번")
|
||||
if "단가산출" in r["갈래"] and sub is not None and int(sub) in blocks:
|
||||
head = first.get(int(sub)) or {}
|
||||
mine = head.get("_호표") == unit["번"] and head.get("차례") == line.get("차례")
|
||||
if mine and str(sub) in cache:
|
||||
r["풀이"] = cache[str(sub)]
|
||||
else:
|
||||
m = m or master()
|
||||
others = [x for x in lines if x is not line]
|
||||
r["풀이"] = _solve(m, blocks[int(sub)], line, others)
|
||||
out.append(r)
|
||||
sc, _ = _made(folder, key, to)
|
||||
head = {k: unit.get(k) for k in ("번", "이름", "규격", "단위", "단가")}
|
||||
return {**head, "만든": sc.get(hbuild.mark(key, no)), "줄": out}
|
||||
|
||||
|
||||
def block(folder: Path, key: str, no: int, to: Path) -> dict[str, Any]:
|
||||
"""단산 블록 하나 — 풀이(저장본 › 셈) + 이름 · 단위 · 부름 · 만든(부르는 첫 줄로 B1 · B6)."""
|
||||
book = hopyo_store.read(folder, key)
|
||||
blocks = _blocks(book)
|
||||
if no not in blocks:
|
||||
raise _bad(f"없는 블록 「단산{no}」", 404)
|
||||
line, same = _caller(book, no)
|
||||
got = (solved(folder, key) or {}).get(str(no)) or _solve(master(), blocks[no], line, same)
|
||||
_, sk = _made(folder, key, to)
|
||||
blk = blocks[no]
|
||||
head = {"이름": blk.get("이름"), "단위": blk.get("단위") or (line or {}).get("단위")}
|
||||
head |= {"부름": sangeun.callers(book).get(no, []), "만든": sk.get(sangeun.mark(key, no))}
|
||||
return {**got, **head}
|
||||
|
||||
|
||||
# ── 검산 ─────────────────────────────────────────────────────────────
|
||||
def _want(m: mf.Master, pick: dict[str, Any]) -> Any:
|
||||
"""고른 것 단위 — 기초 = 결과단위(「원/㎥」 → ㎥ · 「원/자재 단위」 → 자재) · 그 밖 = 고른 것 단위."""
|
||||
if pick.get("갈래") == "기초" and pick.get("키"):
|
||||
unit = str(m.logic(pick["키"]).get("결과단위") or "")
|
||||
unit = unit.split("/", 1)[1] if unit.startswith("원/") else unit
|
||||
return unit.removesuffix(" 단위").strip() # 「원/자재 단위」 = 줄 단위 그대로
|
||||
return pick.get("단위")
|
||||
|
||||
|
||||
def quantity(m: mf.Master, line: dict, pick: dict) -> dict[str, Any]:
|
||||
"""줄 수량 `{엑셀, 몫, 배, 갈래, 우리}` — 사용자 배(단위 환산 값)가 있으면 그것."""
|
||||
got = hrow.quantity(line, _want(m, pick), pick.get("몫"))
|
||||
times = pick.get("배")
|
||||
if times is not None and line.get("수량") is not None:
|
||||
ours = Decimal(str(line["수량"])) * Decimal(str(pick.get("몫") or 1))
|
||||
got |= {"배": times, "갈래": "사용자", "우리": hrow._plain(ours * Decimal(str(times)))}
|
||||
return got
|
||||
|
||||
|
||||
def one_pick(m: mf.Master, pick: dict[str, Any], items: list[dict], share: Any) -> dict:
|
||||
"""고른 것 하나 검산."""
|
||||
out = {"갈래": pick["갈래"], "키": pick.get("키")}
|
||||
if pick["갈래"] != "기초":
|
||||
return {**out, "판정": CHECKED}
|
||||
code = str(pick.get("키") or "")
|
||||
try:
|
||||
m.logic(code)
|
||||
except Exception as e: # noqa: BLE001 — 없는 키
|
||||
raise _bad(f"없는 기초 「{code}」") from e
|
||||
full, need = inputs(m, code, pick.get("입력값"))
|
||||
rows = solve.pair(hb.quantities(m, code, full), solve._usable(items), share) if items else []
|
||||
if need:
|
||||
verdict = solve.NEED
|
||||
elif any(r["판정"] == solve.DIFF for r in rows):
|
||||
verdict = solve.DIFF
|
||||
else:
|
||||
verdict = solve.SAME if any(r["판정"] == solve.SAME for r in rows) else CHECKED
|
||||
got = money(m, code, full) if not need else {"계": None, "까닭": f"입력 필요 {len(need)}"}
|
||||
return {**out, "입력값": full, "입력필요": need, "줄": rows, "판정": verdict, "금액": got}
|
||||
|
||||
|
||||
def check(
|
||||
folder: Path, key: str, hno: int | None, at: int | None, bno: int | None, picks: list[dict]
|
||||
) -> dict[str, Any]:
|
||||
"""`{고른 [...], 수량}` — 호표 줄(`hno` · `at`) · 블록(`bno` · 줄이 단산이면 그 블록) · 고른 기초는
|
||||
맡은 절(`Solve.parts`) 단위 줄 · 몫과 대조."""
|
||||
book = hopyo_store.read(folder, key)
|
||||
blocks, line = _blocks(book), None
|
||||
if hno is not None:
|
||||
lines = _unit(book, hno).get("줄") or []
|
||||
if at is None or not 0 <= at < len(lines):
|
||||
raise _bad(f"없는 줄 「제{hno}호표 {at}」")
|
||||
line = lines[at]
|
||||
ref = line.get("참조") or {}
|
||||
if bno is None and ref.get("종류") == "단산" and ref.get("번") is not None:
|
||||
bno = int(ref["번"])
|
||||
if bno is not None and bno not in blocks:
|
||||
raise _bad(f"없는 블록 「단산{bno}」", 404)
|
||||
blk = blocks[bno] if bno is not None else {}
|
||||
parts = solve.parts(blk, clause.basis(blk)["후보"]) if blk else []
|
||||
m = master()
|
||||
out = []
|
||||
for i, pick in enumerate(picks):
|
||||
part = next((x for x in parts if pick.get("키") in x["후보"]), None) or {}
|
||||
items = part.get("항목") # 그 기초가 맡은 절 단위 줄(없으면 블록 통째)
|
||||
items = (blk.get("항목") or []) if items is None else items
|
||||
share = part.get("몫")
|
||||
share = pick.get("몫") or (share.get("값") if isinstance(share, dict) else share) or 1
|
||||
out.append({"차례": i, **one_pick(m, pick, items, share)})
|
||||
qty = quantity(m, line, picks[0]) if line is not None and picks else None
|
||||
return {"고른": out, "수량": qty}
|
||||
@@ -22,13 +22,19 @@ from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Library_Flat as lflat
|
||||
from M02_MasterTemplete import M02_Library_Same as lsame
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete.M02_Hopyo_Sangeun import _same_mark
|
||||
|
||||
ORIGIN = "실무자료"
|
||||
KIND = "precombo"
|
||||
|
||||
|
||||
# ── 같은 내용 재사용 ─────────────────────────────────────────────────
|
||||
def _same_mark(a: dict, b: dict) -> bool:
|
||||
"""같은 출처 — 본뜬 키 같음 · 원본(파일 층)이 둘 다 있으면 같음(SC · SK 함께 · `Sangeun` 이 빌림)."""
|
||||
if a.get("키") != b.get("키"):
|
||||
return False
|
||||
return not a.get("원본") or not b.get("원본") or tuple(a["원본"]) == tuple(b["원본"])
|
||||
|
||||
|
||||
def entries(folder: str | Path):
|
||||
"""층 사전조합 실무자료 출처 → (키, 본뜬 · 또 한 칸, 판) — 또 = 같은 내용으로 이 SC 를 쓴 뒤 출처."""
|
||||
for path in sorted((Path(folder) / KIND).glob("SC-*.json")):
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
"""M02 엑셀 불러오기 만들기(PLAN 785-5 · 요구 642 · 설계 642-A 2-7 ⑥) — 확인한 것 → 우리 SC · SK · 장부.
|
||||
|
||||
옮기기 몸 = `{호표 {번: {차례: [고른 것]}}, 블록 {번: [고른 것]}}` · 고른 것 = 785-3 꼴
|
||||
`{갈래 기초|단가산출|일위대가|인력|재료|유가|기계|율|고정, 키, 입력값, 몫, 배?, 비목?, 기준?, 별도계상?, 층?,
|
||||
주인?, 항목?}` · 서버가 다시 셈(브라우저 금액 · 품 안 받음 · 배 = `Row.factor` · 사용자 배 우선 ·
|
||||
입력값 = 엔진이 쓴 칸 모두 `Judge.inputs`) · 「입력 필요」 · 배 못 정함 · 없는 키 = 그 줄(블록)만 `막힘`.
|
||||
SC(`M02_Hopyo_Build.Builder` 가 줄 차례 · 확인 안 한 줄 = 모두 고정 길 그대로) — 한 엑셀 줄 = 고른 것마다
|
||||
우리 줄(같은 별칭 = 엑셀 품명 · 줄 `본뜬 {호표, 차례, 행}`) · 수량 {손 = 엑셀 수량 × 몫 × 배} ·
|
||||
기초 = 로직 줄(같은 로직 둘째 = 한 줄 SC 사본) · 재료 · 유가 = 자재 목록(별도계상) · 인력 · 기계 · 유가 이음 ·
|
||||
율 = 고정 줄 `비율 {기준, 비목}` · 고정 = 엑셀 단가 고정 줄 · 단가산출 · 일위대가 = 참조
|
||||
(`단산N` = 같은 옮기기 블록 SK · `제N호표` = 같은 HP 호표 SC — 하위 먼저 · 확인 안 된 하위 = 모두 고정).
|
||||
SK(블록) = 바탕 없음 + 더한줄 — 기초 `{종류 로직, 입력값, 수량 = 몫}` · 참조 · 단위 줄(`항목`)에 이은
|
||||
인력 · 기계 · 재료 · 유가 · 율 = `SkUnit.adds` · 고정 = 엑셀 금액 · 쓰기 = `Sangeun.Maker.write`.
|
||||
같은 내용 재사용(`Kids.save` · `Sangeun.Maker.write`) · 장부 = 고른 것 꼴 짝(`Ledger.check_pair`) ·
|
||||
엑셀 대조 = `Compare.compare`(참고 · 비목마다 1 원).
|
||||
`Lines` = 확인한 줄 몸(옛 대상 꼴 `put` 은 옛 길 몫 · 785-7 지움).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hbuild
|
||||
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
|
||||
from M02_MasterTemplete import M02_Hopyo_Judge as judge
|
||||
from M02_MasterTemplete import M02_Hopyo_Kids as hkids
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun
|
||||
from M02_MasterTemplete import M02_Hopyo_SkLink as sklink
|
||||
from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Hopyo_Units as units
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Library_Same as lsame
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete.M02_Hopyo_Compare import compare
|
||||
|
||||
_SUB = {"단가산출": re.compile(r"^단산(\d+)$"), "일위대가": re.compile(r"^제(\d+)호표$")}
|
||||
LINK = {"인력": "노임", "기계": "기계", "유가": "유가"}
|
||||
OLD_PICK = {"인력": "노임", "기계": "기계", "재료": "자재", "유가": "자재"}
|
||||
NEED = "입력 필요"
|
||||
COVER = "이미 옮긴 것이 있음 — 덮으려면 [덮어 옮기기](덮음)"
|
||||
|
||||
|
||||
class Stuck(Exception):
|
||||
"""그 줄(블록)만 못 옮김 — `{까닭, 칸?}`."""
|
||||
|
||||
def __init__(self, why: str, cells: list[str] | None = None) -> None:
|
||||
super().__init__(why)
|
||||
self.why, self.cells = why, cells or []
|
||||
|
||||
|
||||
def _num(value: Decimal) -> int | float:
|
||||
return hbuild._num(value)
|
||||
|
||||
|
||||
# ── 확인한 줄 몸(SC) ─────────────────────────────────────────────────────
|
||||
class Lines:
|
||||
"""한 호표 줄 모음 — `Builder.build` 가 줄 차례대로 부름 · 확인 안 한 줄은 Builder 가 고정 길로."""
|
||||
|
||||
def __init__(self, b: Any, rows: list[dict], where: dict, stamp: str, linker: Any) -> None:
|
||||
self.b, self.rows, self.where, self.stamp, self.linker = b, rows, where, stamp, linker
|
||||
self.lines: list[dict] = []
|
||||
self.mats: dict[str, dict] = {}
|
||||
self.missing: list[dict] = []
|
||||
self.used: set[str] = set()
|
||||
self.mat_lines: dict[str, dict] = {}
|
||||
self.nation: dict[tuple, dict] = {}
|
||||
self.taken: set[str] = set()
|
||||
self.kids: set[int] = set()
|
||||
self.left: list[dict] = []
|
||||
|
||||
def put(self, line: dict, one: dict, alias: str | None, qty: Decimal | None = None,
|
||||
mark: dict | None = None) -> None: # fmt: skip
|
||||
"""대상 하나 → 줄 · 자재 — 옛 꼴 `{로직|자재|호표|전국|이음, 입력값, 배, 몫}` · `qty` = 새 길 수량
|
||||
(엑셀 × 몫 × 배) · `mark` = 새 길 줄 `본뜬`."""
|
||||
keep = len(self.lines)
|
||||
if "이음" in one:
|
||||
others = [x for x in self.rows if x is not line]
|
||||
fx = hfix.line_of(line, others, self.where, self.taken, self.linker, one["이음"])
|
||||
self.lines.append(fx)
|
||||
else:
|
||||
qty = (
|
||||
qty
|
||||
if qty is not None
|
||||
else Decimal(str(line.get("수량") or 0)) * Decimal(str(one["배"]))
|
||||
)
|
||||
self._put(line, one, alias, qty, mark is not None)
|
||||
for row in self.lines[keep:]:
|
||||
row.update({"본뜬": mark} if mark else {})
|
||||
|
||||
def _put(self, line: dict, one: dict, alias: str | None, qty: Decimal, new: bool) -> None:
|
||||
b = self.b
|
||||
memo = hbuild._memo(line, one)
|
||||
if "전국" in one:
|
||||
at = (one["이름"], one["규격"], one["단위"])
|
||||
have = self.nation.get(at)
|
||||
hand = qty + Decimal(str(have["수량"]["손"])) if have else qty
|
||||
head = {"이름": at[0], "규격": at[1], "단위": at[2], "수량": {"손": _num(hand)}}
|
||||
self.nation[at] = {**head, **(hmat.marks(line, at[0]) if not have else {
|
||||
k: have[k] for k in labor.MARKS if k in have
|
||||
})} # fmt: skip
|
||||
self.missing.append(hbuild._missing(line, hbuild.NATIONAL))
|
||||
return
|
||||
if "자재" in one:
|
||||
code = one["자재"]
|
||||
have = self.mats.get(code)
|
||||
hand = qty + Decimal(str(have["수량"]["손"])) if have else qty
|
||||
name = b.mat_names.get(code, "")
|
||||
marks = (
|
||||
have and {k: have[k] for k in labor.MARKS if k in have} or hmat.marks(line, name)
|
||||
)
|
||||
if one.get("별도계상") is not None: # 세 칸 값(빈 칸 = 기본 「별도계상」)
|
||||
marks = {**marks, "별도계상": "별도계상" if one["별도계상"] else "직접계산"}
|
||||
self.mats[code] = {"코드": code, "수량": {"손": _num(hand)}, **marks}
|
||||
if not have:
|
||||
self.mat_lines[code] = line
|
||||
return
|
||||
if "호표" in one or "참조" in one:
|
||||
if "호표" in one:
|
||||
self.kids.add(one["호표"])
|
||||
key = b.done.get(hbuild.mark(b.key, one["호표"]), "SC-미리")
|
||||
ref = {"층": refs.here()[0], "주인": refs.here()[1], "키": key}
|
||||
else:
|
||||
ref = one["참조"]
|
||||
self.lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
|
||||
return
|
||||
row = {"로직": one["로직"], "입력값": one["입력값"], "메모": memo}
|
||||
if not new:
|
||||
kept, why = sklink.kept(b, line, one, qty, alias, memo)
|
||||
if why:
|
||||
self.left.append(sklink.left(line, why))
|
||||
if kept is not None:
|
||||
self.lines.append(kept)
|
||||
return
|
||||
if one["로직"] not in self.used:
|
||||
self.used.add(one["로직"])
|
||||
self.lines.append({**row, "별칭": alias, "수량": {"손": _num(qty)}})
|
||||
return
|
||||
at = int(line.get("차례", 0))
|
||||
hopyo, file = b.hopyo, b.doc.get("파일") or ""
|
||||
wrap = { # 같은 로직 둘째 — 한 줄 사전조합으로 감쌈
|
||||
"이름": f"{hopyo['이름']} — {line.get('이름')}",
|
||||
"단위": line.get("단위") or None,
|
||||
"구분": hbuild.ORIGIN,
|
||||
"상세구분": file,
|
||||
"담은로직": [{**row, "별칭": alias, "수량": {"손": 1}}],
|
||||
"자재": [],
|
||||
"본뜬": b._origin(f"{hbuild.mark(b.key, b.no, at)} {one['로직']}", self.stamp),
|
||||
}
|
||||
ref = {"키": "SC-미리"} if b.dry else hkids.save(wrap, b.same)[0] # 미리 보기 = 쓰기 없음
|
||||
self.lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
|
||||
|
||||
def picks(self, at: int, line: dict, picks: list[dict]) -> None:
|
||||
"""고른 것(검사 · 다시 셈 끝난 것) → 그 엑셀 줄의 우리 줄 · 자재."""
|
||||
alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None
|
||||
mark = {"호표": self.b.no, "차례": at, "행": line.get("행")}
|
||||
for p in picks:
|
||||
kind, code = p["갈래"], p.get("키")
|
||||
one = {"배": p["배"], "몫": p.get("몫")}
|
||||
qty = p["수량"]
|
||||
if kind == "기초":
|
||||
self.put(line, {**one, "로직": code, "입력값": p["입력값"]}, alias, qty, mark)
|
||||
elif kind in ("재료", "유가") and not p.get("이음"):
|
||||
self.put(
|
||||
line, {**one, "자재": code, "별도계상": p.get("별도계상")}, alias, qty, mark
|
||||
)
|
||||
elif kind in ("단가산출", "일위대가"):
|
||||
self.put(line, {**one, "참조": p["참조"]}, alias, qty, mark)
|
||||
elif kind == "율":
|
||||
self.put(line, {"이음": {"비율": p["비목"]}}, alias, None, mark)
|
||||
self.lines[-1]["고정"]["비율"] = {"기준": p["기준"], "비목": p["비목"]}
|
||||
elif kind == "고정":
|
||||
self.put(line, {"이음": dict.fromkeys(hfix.fixed.LINKS)}, alias, None, mark)
|
||||
else:
|
||||
self.put(line, {"이음": {LINK[kind]: code}}, alias, None, mark)
|
||||
|
||||
|
||||
def ledger_pick(p: dict[str, Any]) -> dict[str, Any]:
|
||||
"""장부 짝(고른 것 꼴 · `Ledger.check_pair`) — 참조 = 만든 SK · SC 키."""
|
||||
keep = ("갈래", "키", "입력값", "배", "몫", "비목", "기준", "별도계상")
|
||||
out = {k: p[k] for k in keep if p.get(k) is not None}
|
||||
return out | ({"키": p["참조"]["키"]} if "참조" in p else {})
|
||||
|
||||
|
||||
# ── 고른 것 검사 · 다시 셈 ──────────────────────────────────────────────
|
||||
class Move:
|
||||
"""한 번 옮기기 — 파일 · M01 · 만든 SC · SK 를 한 번 읽음(쓰기 = `store.root()`)."""
|
||||
|
||||
def __init__(self, folder: Path, source: Path, key: str, who: Any) -> None:
|
||||
self.folder, self.source, self.key, self.who = Path(folder), Path(source), key, who
|
||||
self.hp = hopyo_store.read(source, key)
|
||||
self.units = {int(h["번"]): h for h in self.hp.get("호표") or []}
|
||||
self.m = judge.master()
|
||||
self.known = ledger._known()
|
||||
self.sk = sangeun.Maker(source, key, who)
|
||||
self.same = lsame.Same()
|
||||
self.out: dict[str, Any] = {"만듦": [], "단가산출": [], "막힘": [], "하위": []}
|
||||
self.out["장부"] = {"씀": 0}
|
||||
|
||||
def _ref(self, p: dict) -> dict[str, Any]:
|
||||
"""단가산출 · 일위대가 키 → 참조 칸(같은 HP 블록 · 호표 = 만든 것) · 없으면 막힘."""
|
||||
code, kind = str(p.get("키") or ""), p["갈래"]
|
||||
hit = _SUB[kind].match(code)
|
||||
here = refs.here()
|
||||
if hit:
|
||||
n = int(hit.group(1))
|
||||
made = (
|
||||
self.sk.keys.get(sangeun.mark(self.key, n))
|
||||
if kind == "단가산출"
|
||||
else hbuild.confirmed(self.folder, refs.where(self.source)).get(
|
||||
hbuild.mark(self.key, n)
|
||||
)
|
||||
)
|
||||
if not made:
|
||||
raise Stuck(f"{code} 먼저 옮김")
|
||||
return {"층": here[0], "주인": here[1], "키": made}
|
||||
head = "SK-" if kind == "단가산출" else "SC-"
|
||||
ref = {"층": p.get("층") or here[0], "주인": p.get("주인"), "키": code}
|
||||
if p.get("층") is None:
|
||||
ref["주인"] = here[1]
|
||||
if not code.startswith(head) or refs.read_ref(ref) is None:
|
||||
raise Stuck(f"없는 {kind} 「{code}」")
|
||||
return ref
|
||||
|
||||
def pick(self, p: dict, line: dict | None, where: str) -> dict[str, Any]:
|
||||
"""고른 것 하나 재검사 · 다시 셈 → `{갈래, 키, 입력값, 배, 몫, 수량, 참조?, …}`."""
|
||||
kind = p["갈래"]
|
||||
got = {k: p[k] for k in ("갈래", "키", "몫", "비목", "기준", "별도계상") if k in p}
|
||||
if kind == "기초":
|
||||
full, need = judge.inputs(self.m, str(p.get("키") or ""), p.get("입력값"))
|
||||
if need:
|
||||
raise Stuck(f"{NEED} {len(need)}", need)
|
||||
got["입력값"] = full
|
||||
if kind in _SUB:
|
||||
got["참조"] = self._ref(p)
|
||||
else:
|
||||
plain = {k: v for k, v in got.items() if k in ledger.PICK_KEYS}
|
||||
ledger.check_pair(plain, where, self.known)
|
||||
if kind in ("재료", "유가") and not got.get("키"):
|
||||
raise Stuck(f"{kind} 키 없음")
|
||||
if line is not None:
|
||||
qty = judge.quantity(self.m, line, p)
|
||||
if qty["배"] is None:
|
||||
raise Stuck("단위 환산 입력")
|
||||
got["배"], got["수량"] = qty["배"], Decimal(str(qty["우리"] or 0))
|
||||
return got
|
||||
|
||||
# ── SC ──
|
||||
def hopyo(self, no: int, rows: dict[int, list[dict]]) -> None:
|
||||
unit = self.units[no]
|
||||
lines = unit.get("줄") or []
|
||||
checked = {}
|
||||
for at, picks in sorted(rows.items()):
|
||||
if not 0 <= at < len(lines):
|
||||
raise store.StoreError(400, f"없는 줄 「제{no}호표 {at}」")
|
||||
where = f"제{no}호표 {at + 1}째 줄"
|
||||
if not picks:
|
||||
continue
|
||||
try:
|
||||
checked[at] = {"고른": [self.pick(p, lines[at], where) for p in picks]}
|
||||
except Stuck as e:
|
||||
self.out["막힘"].append({"호표": no, "차례": at, "까닭": e.why, "칸": e.cells})
|
||||
b = hbuild.Builder(self.folder, self.source, self.key, no, self.who)
|
||||
b.strict, b.same = False, self.same
|
||||
made = b.build(checked, [])
|
||||
ref = made["참조"]
|
||||
head = {"번": no, "키": ref["키"], "이름": unit.get("이름")}
|
||||
head |= {k: made[k] for k in ("다시씀", "재사용")}
|
||||
self.out["만듦"].append({**head, "비교": compare(ref, unit)})
|
||||
self.out["장부"]["씀"] += made["장부"]["씀"]
|
||||
|
||||
def kid(self, no: int) -> None:
|
||||
"""확인 안 된 하위 호표 — 모두 고정(705-3 길)으로 먼저."""
|
||||
b = hbuild.Builder(self.folder, self.source, self.key, no, self.who)
|
||||
b.strict, b.same = False, self.same
|
||||
if b.done.get(hbuild.mark(self.key, no)):
|
||||
return
|
||||
made = b.build({}, [])
|
||||
self.out["하위"].append({"번": no, "키": made["참조"]["키"], "새로": True})
|
||||
|
||||
# ── SK ──
|
||||
def block(self, no: int, picks: list[dict]) -> None:
|
||||
blk = self.sk.blocks[no]
|
||||
items = blk.get("항목") or []
|
||||
us = self.sk.units()
|
||||
made: list[dict] = []
|
||||
rows: list[dict] = []
|
||||
for p in picks:
|
||||
got = self.pick(p, None, f"단산{no}")
|
||||
name = str(p.get("이름") or "").strip() or str(blk.get("이름") or f"단산{no}")
|
||||
share = got.get("몫") or 1
|
||||
at = p.get("항목")
|
||||
item = items[at] if isinstance(at, int) and 0 <= at < len(items) else None
|
||||
if got["갈래"] == "기초":
|
||||
made.append({"종류": "로직", "로직": got["키"], "입력값": got["입력값"],
|
||||
"이름": name, "수량": share}) # fmt: skip
|
||||
continue
|
||||
if got["갈래"] in _SUB:
|
||||
qty = float(item.get("품") or 1) if item else share
|
||||
made.append({"참조": got["참조"], "이름": name, "수량": qty})
|
||||
continue
|
||||
if item is None:
|
||||
raise Stuck(f"{got['갈래']} — 블록 단위 줄(항목) 고름")
|
||||
if got["갈래"] == "고정":
|
||||
parts = hfix._parts(item.get("금액"))
|
||||
fx = {"이름": name, "단가": {k: float(v) for k, v in parts.items()}, "이음": {}}
|
||||
made.append({"고정": fx, "이름": name, "수량": 1})
|
||||
continue
|
||||
old = {"기준": got.get("기준"), "비목": got.get("비목")}
|
||||
if got["갈래"] in OLD_PICK:
|
||||
old = {OLD_PICK[got["갈래"]]: got["키"], "별도계상": got.get("별도계상")}
|
||||
unit = skunit._picked(item, old, us)["단위"]
|
||||
rows.append({"차례": at, "행": item.get("행"), "상태": units.LINKED, "단위": unit})
|
||||
extra, _ = skunit.adds(blk, rows, us, self.sk.ref_of)
|
||||
doc = {"키": "SK-000000", "이름": "옮김", "단위": blk.get("단위") or "식", "바탕": None}
|
||||
try:
|
||||
adds = sd.clean({**doc, "더한줄": made + extra}, None, self.sk.whole)["더한줄"]
|
||||
except (ValueError, store.StoreError) as e:
|
||||
raise Stuck(f"더한줄 틀림 — {e}") from e
|
||||
plan = {"갈래": "고름", "바탕": None, "입력값": {}, "덮개": {}, "자재표시": {}, "뺀줄": []}
|
||||
plan |= {"더한줄": adds, "못옮김": [], "놓음": []}
|
||||
mark_ = sangeun.mark(self.key, no)
|
||||
key = self.sk.write(no, plan, self.sk.keys.get(mark_))
|
||||
self.sk.keys[mark_] = key
|
||||
self.out["단가산출"].append({"번": no, "키": key, "이름": blk.get("이름")})
|
||||
|
||||
def blocks(self, sure: dict[int, list[dict]]) -> None:
|
||||
"""하위 단산(블록 단위 줄 하위) 먼저."""
|
||||
done: set[int] = set()
|
||||
|
||||
def visit(no: int, path: tuple = ()) -> None:
|
||||
if no in done or no in path:
|
||||
return
|
||||
for one in self.sk.blocks[no].get("항목") or []:
|
||||
sub = one.get("하위") or {}
|
||||
if sub.get("종류") == "단산" and int(sub.get("번") or 0) in sure:
|
||||
visit(int(sub["번"]), (*path, no))
|
||||
done.add(no)
|
||||
try:
|
||||
self.block(no, sure[no])
|
||||
except Stuck as e:
|
||||
self.out["막힘"].append({"블록": no, "까닭": e.why, "칸": e.cells})
|
||||
|
||||
for no in sorted(sure):
|
||||
visit(no)
|
||||
|
||||
|
||||
def move(
|
||||
folder: Path,
|
||||
source: Path,
|
||||
key: str,
|
||||
units_: dict[int, dict[int, list[dict]]],
|
||||
blocks: dict[int, list[dict]],
|
||||
who: Any = None,
|
||||
cover: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""[옮기기] → `{만듦, 단가산출, 막힘, 하위, 장부}` — 블록 SK 먼저 · 호표 하위 먼저 ·
|
||||
이미 옮긴 호표 · 블록(같은 HP 출처 SC · SK)은 `cover`(덮음) 때만 덮음 · 아니면 409 아무것도 안 씀."""
|
||||
mv = Move(folder, source, key, who)
|
||||
bad = [f"제{n}호표" for n in units_ if n not in mv.units]
|
||||
bad += [f"단산{n}" for n in blocks if n not in mv.sk.blocks]
|
||||
if bad:
|
||||
raise store.StoreError(404, f"없는 {bad[0]}")
|
||||
done = hbuild.confirmed(mv.folder, refs.where(mv.source))
|
||||
had = {
|
||||
"호표": [
|
||||
{"번": n, "키": done[k]} for n in sorted(units_) if (k := hbuild.mark(key, n)) in done
|
||||
]
|
||||
}
|
||||
had["블록"] = [
|
||||
{"번": n, "키": mv.sk.keys[k]}
|
||||
for n in sorted(blocks)
|
||||
if (k := sangeun.mark(key, n)) in mv.sk.keys
|
||||
]
|
||||
if not cover and (had["호표"] or had["블록"]):
|
||||
raise store.StoreError(409, {"까닭": COVER, **had})
|
||||
mv.blocks(blocks)
|
||||
wanted = {
|
||||
int(hit.group(1))
|
||||
for rows in units_.values()
|
||||
for picks in rows.values()
|
||||
for p in picks
|
||||
if p.get("갈래") == "일위대가" and (hit := _SUB["일위대가"].match(str(p.get("키") or "")))
|
||||
}
|
||||
order = hbuild._order(mv.units)
|
||||
for no in order: # 고른 것이 부르는 확인 안 된 하위 = 모두 고정으로 먼저
|
||||
if no in wanted and no not in units_:
|
||||
mv.kid(no)
|
||||
for no in order:
|
||||
if no in units_:
|
||||
mv.hopyo(no, units_[no])
|
||||
return mv.out
|
||||
@@ -0,0 +1,590 @@
|
||||
"""M02 일위대가 실무자료 길(PLAN 785-5 · 요구 642 · 설계 642-A 2-9) — `/api/m02/layers/{층}/hopyo…`.
|
||||
|
||||
새 길(글 먼저 · 결정적 매칭) — 판정 `hopyo-judge`(1차 `M02_Hopyo_Row` + 2차 `M02_Hopyo_Solve` ·
|
||||
`M02_Hopyo_Judge`) · 검산 `hopyo/{키}/check`(저장 없음) · 옮기기 `hopyo/{키}/move`(확인한 것 →
|
||||
SC · SK · 장부 · `M02_Hopyo_Make`) · 경로 층 = 파일 층(판정 · 검산 · `to` = 만들 층) / 만들 층(옮기기).
|
||||
올림 = Book + 격자 + 엑셀 사전(`Row.lexicon`) + 블록 풀이 뒤 셈(`Judge.ensure` · 파생 저장본) ·
|
||||
옛 맞춤 뒤 셈 · 시동 데움 없음 · [끝내기] = 저장본도 지움.
|
||||
옛 길(`M02_Labor_Router` 에서 옮김 · 옛 화면 = 785-6 끝까지 · 지움 = 785-7) · 그대로 쓰는 길(격자 ·
|
||||
지우기 · 장부 · 붙여넣기 · 다시 잇기 · 구조물 조합 · 칠 = B09 · M01 이 씀).
|
||||
층 권한 = `Router_Items._root` · ⚠ 등록은 `main.py`(로그인만).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel
|
||||
from M02_MasterTemplete import M02_Hopyo_Forget as hopyo_forget
|
||||
from M02_MasterTemplete import M02_Hopyo_Grid as hopyo_grid
|
||||
from M02_MasterTemplete import M02_Hopyo_Item as hopyo_item
|
||||
from M02_MasterTemplete import M02_Hopyo_Judge as hopyo_judge
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as hopyo_ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_Make as hopyo_maker
|
||||
from M02_MasterTemplete import M02_Hopyo_Marks as hopyo_marks
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
|
||||
from M02_MasterTemplete import M02_Hopyo_Paste as hopyo_paste
|
||||
from M02_MasterTemplete import M02_Hopyo_Relink as hopyo_relink
|
||||
from M02_MasterTemplete import M02_Hopyo_Row as hopyo_row
|
||||
from M02_MasterTemplete import M02_Hopyo_Sangeun as hopyo_sangeun
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Layer_Access as access
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root
|
||||
|
||||
router = APIRouter(prefix="/api/m02", tags=["M02 Hopyo"])
|
||||
|
||||
|
||||
# ── 새 길(785-5) — 판정 · 검산 · 옮기기 ─────────────────────────────────────
|
||||
def _to(session: Session, folder: Path, to: Layer | None, to_owner: str | None) -> Path:
|
||||
return _root(session, to, to_owner) if to is not None else folder
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo-judge/{key}")
|
||||
def hopyo_judge_list(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
owner: str | None = None,
|
||||
to: Layer | None = None,
|
||||
to_owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""호표 목록 + 1차 판정 수 · 블록 목록 `{키, 파일, 호표 [...], 블록 [...]}`(읽기만)."""
|
||||
folder = _root(session, layer, owner)
|
||||
target = _to(session, folder, to, to_owner)
|
||||
return _call(folder, lambda: hopyo_judge.listing(folder, key, target))
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo-judge/{key}/{no}")
|
||||
def hopyo_judge_one(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
no: int,
|
||||
owner: str | None = None,
|
||||
to: Layer | None = None,
|
||||
to_owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""호표 하나 — 줄 판(1차) · 단산 줄 `풀이`(2차 블록 풀이)."""
|
||||
folder = _root(session, layer, owner)
|
||||
target = _to(session, folder, to, to_owner)
|
||||
return _call(folder, lambda: hopyo_judge.hopyo(folder, key, no, target))
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo-judge/{key}/block/{no}")
|
||||
def hopyo_judge_block(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
no: int,
|
||||
owner: str | None = None,
|
||||
to: Layer | None = None,
|
||||
to_owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""단산 블록 하나 — 블록 풀이(`Solve.block`) + 이름 · 단위 · 부름 · 근거 · 만든."""
|
||||
folder = _root(session, layer, owner)
|
||||
target = _to(session, folder, to, to_owner)
|
||||
return _call(folder, lambda: hopyo_judge.block(folder, key, no, target))
|
||||
|
||||
|
||||
class Pick(BaseModel):
|
||||
"""고른 것(785-3 꼴) — 금액 · 품 칸 없음(서버가 셈) · 고정 단가 = 엑셀 줄 것."""
|
||||
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
갈래: Literal["기초", "단가산출", "일위대가", "인력", "재료", "유가", "기계", "율", "고정"]
|
||||
키: str | None = Field(None, max_length=40)
|
||||
이름: str | None = Field(None, max_length=200)
|
||||
단위: str | None = Field(None, max_length=40)
|
||||
입력값: dict[str, Any] = Field(default_factory=dict)
|
||||
몫: float | None = None
|
||||
배: float | None = None
|
||||
비목: str | None = None
|
||||
기준: str | list[str] | None = None
|
||||
별도계상: bool | None = None
|
||||
층: Layer | None = None
|
||||
주인: str | None = None
|
||||
항목: int | None = None # 블록 고른 것 — 블록 단위 줄 차례(수량 = 그 줄 품)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _no_money(cls, raw: Any) -> Any:
|
||||
"""금액 · 품 · 수량 칸 = 400(서버가 셈 · 브라우저 값 안 받아 적음)."""
|
||||
if isinstance(raw, dict) and {"금액", "품", "수량"} & set(raw):
|
||||
raise ValueError("금액 · 품 · 수량은 서버가 셈")
|
||||
return raw
|
||||
|
||||
|
||||
class CheckBody(BaseModel):
|
||||
"""`호표` + `차례` = 일위대가 줄 · `블록` = 단산 블록(항목 줄과 대조) · `고른` = 고른 것."""
|
||||
|
||||
호표: int | None = None
|
||||
차례: int | None = None
|
||||
블록: int | None = None
|
||||
고른: list[Pick] = Field(default_factory=list, max_length=20)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo/{key}/check")
|
||||
def hopyo_check(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
body: CheckBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""검산(저장 없음) — 고른 것마다 입력값(엔진이 쓴 칸 모두) · 입력필요 · 줄 대조 · 금액(서버 셈)."""
|
||||
folder = _root(session, layer, owner)
|
||||
picks = [p.model_dump(exclude_none=True) for p in body.고른]
|
||||
args = (folder, key, body.호표, body.차례, body.블록, picks)
|
||||
return _call(folder, hopyo_judge.check, *args)
|
||||
|
||||
|
||||
class MoveBody(BaseModel):
|
||||
"""확인한 것 — 파일 층 `층` · `주인` · `호표 {번: {차례: [고른 것]}}` · `블록 {번: [고른 것]}`."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
층: Layer
|
||||
주인: str | None = None
|
||||
호표: dict[int, dict[int, list[Pick]]] = Field(default_factory=dict, max_length=500)
|
||||
블록: dict[int, list[Pick]] = Field(default_factory=dict, max_length=500)
|
||||
덮음: bool = False # 이미 옮긴 호표 · 블록(같은 HP 출처) 덮기 — 아니면 409 {까닭, 호표, 블록}
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo/{key}/move")
|
||||
def hopyo_move(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
body: MoveBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""[옮기기] — 확인한 것 → 이 층 SC · SK · 장부(하위 먼저 · 서버가 다시 셈) →
|
||||
`{만듦, 단가산출, 막힘, 하위, 장부}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
source = _root(session, body.층, body.주인)
|
||||
dump = lambda ps: [p.model_dump(exclude_none=True) for p in ps] # noqa: E731
|
||||
units = {no: {at: dump(ps) for at, ps in rows.items()} for no, rows in body.호표.items()}
|
||||
blocks = {no: dump(ps) for no, ps in body.블록.items()}
|
||||
args = (folder, source, key, units, blocks, _who(session), body.덮음)
|
||||
return _call(folder, hopyo_maker.move, *args)
|
||||
|
||||
|
||||
# ── 옛 길(785-7 까지 · 옛 화면) · 그대로 쓰는 길 ─────────────────────────────
|
||||
HOPYO_MAX_BYTES = 20 * 1024 * 1024
|
||||
|
||||
|
||||
def _who(session: Session) -> dict[str, Any]:
|
||||
return {"id": session.get("user_id"), "이름": session.get("name")}
|
||||
|
||||
|
||||
def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]:
|
||||
mark = hopyo_store.fingerprint(data)
|
||||
hopyo_forget.sweep(store.root()) # 하루 지난 작업 중 HP 끝냄(743-1)
|
||||
if same := next((r for r in hopyo_store.rows(store.root()) if r.get("지문") == mark), None):
|
||||
hopyo_judge.ensure(store.root(), same["키"])
|
||||
return {**same, "이어서": True}
|
||||
try:
|
||||
parsed = hopyo_excel.read_book(name, data)
|
||||
except hopyo_excel.ExcelError as error:
|
||||
raise store.StoreError(400, str(error)) from error
|
||||
try:
|
||||
grid = hopyo_grid.read(name, data, parsed)
|
||||
except Exception: # 격자는 보기용 덤 — 못 만들어도 올리기는 됨(보기 창 404)
|
||||
grid = None
|
||||
sheets, _ = hopyo_excel._open(name, data, hopyo_row.SHEETS)
|
||||
parsed["사전"] = hopyo_row.lexicon(sheets) # 엑셀 사전(785-3 · 1차 줄 판정)
|
||||
row = hopyo_store.save(store.root(), name, parsed, who, grid, mark)
|
||||
hopyo_judge.ensure(store.root(), row["키"]) # 블록 풀이 뒤 셈(파생 저장본 · 화면 안 기다림)
|
||||
return row
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo")
|
||||
def hopyo_list(
|
||||
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""실무자료 목록 `{줄: [{키, 파일, 올린때, 올린이, 시트, 호표수, 어긋남}]}`."""
|
||||
return {"줄": hopyo_store.rows(_root(session, layer, owner))}
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo-picks")
|
||||
def hopyo_picks(
|
||||
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""[로직 추가] 「실무 호표」 줄 — 이 층 · 위 층 파일의 호표마다
|
||||
`{층, 주인, 키, 파일, 번, 이름, 규격, 단위, 맞춤, 확인}`(확인 = 이 층에 확인된 사전조합)."""
|
||||
folder = _root(session, layer, owner)
|
||||
return {"줄": _call(folder, _picks, folder)}
|
||||
|
||||
|
||||
def _picks(folder: Path) -> list[dict[str, Any]]:
|
||||
rows, here, done = hopyo_match.picks(folder), refs.where(folder), {}
|
||||
for row in rows:
|
||||
src = (row["층"], row.get("주인"))
|
||||
if src not in done:
|
||||
done[src] = hopyo_build.confirmed(folder, src)
|
||||
key = done[src].get(hopyo_build.mark(row["키"], row["번"]))
|
||||
row["확인"] = {"층": here[0], "주인": here[1], "키": key} if key else None
|
||||
return rows
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo")
|
||||
def hopyo_upload(
|
||||
layer: Layer,
|
||||
file: UploadFile = File(...),
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""엑셀(xls · xlsx) 올리기 — 「일위대가」 시트를 읽어 결과 · 격자 저장 · 20 MB ·
|
||||
엉터리 · 시트 없음 400."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
data = file.file.read(HOPYO_MAX_BYTES + 1)
|
||||
if len(data) > HOPYO_MAX_BYTES:
|
||||
raise HTTPException(status_code=400, detail="파일이 너무 큼(20 MB 넘음)")
|
||||
name = Path(file.filename or "실무자료").name[:120]
|
||||
return _call(folder, _upload, name, data, _who(session))
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo/{key}")
|
||||
def hopyo_read(
|
||||
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""실무자료 하나 — 검산 · 호표마다 `맞춤` [들어감, 줄 수] · 파일 `맞춤` 합."""
|
||||
folder = _root(session, layer, owner)
|
||||
return _call(folder, hopyo_match.view, folder, key)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo-grid/{key}")
|
||||
def hopyo_grid_read(
|
||||
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""엑셀꼴 격자(329-1) `{책: Workbook, 호표: [{번, 이름, 행}], 단산: [{번, 이름, 행}]}` ·
|
||||
옛 파일 404."""
|
||||
folder = _root(session, layer, owner)
|
||||
return _call(folder, hopyo_store.grid, folder, key)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo/{key}/{no}")
|
||||
def hopyo_one(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
no: int,
|
||||
owner: str | None = None,
|
||||
to: Layer | None = None,
|
||||
to_owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""호표 하나 — 줄마다 맞춤(상태 · 체크 제안 · 대상 · 후보 5) · `to` = 만들 층(없으면 파일 층 ·
|
||||
호표 참조 줄 체크 · 만든)."""
|
||||
folder = _root(session, layer, owner)
|
||||
target = _root(session, to, to_owner) if to is not None else None
|
||||
return _call(folder, lambda: hopyo_match.view(folder, key, no, to=target))
|
||||
|
||||
|
||||
class GuessBody(BaseModel):
|
||||
"""`차례` = 호표 안 줄(0부터) · `대상` = [{로직 | 자재, 입력값?, 몫?}](낱개도 받음)."""
|
||||
|
||||
차례: int
|
||||
대상: list[dict[str, Any]] | dict[str, Any]
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo/{key}/{no}/guess")
|
||||
def hopyo_guess(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
no: int,
|
||||
body: GuessBody,
|
||||
owner: str | None = None,
|
||||
to: Layer | None = None,
|
||||
to_owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""미리 셈(저장 안 함 · 설계 3-4) — 고른 대상 · 옵션으로 엔진 한 번 → `{대상, 품, 글}` ·
|
||||
`to` = 만들 층(없으면 파일 층)."""
|
||||
folder = _root(session, layer, owner)
|
||||
target = _root(session, to, to_owner) if to is not None else None
|
||||
args = (folder, key, no, body.차례, body.대상)
|
||||
return _call(folder, lambda: hopyo_match.guess(*args, to=target))
|
||||
|
||||
|
||||
class _Strict(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid") # 금액 · 품 칸 = 400(서버가 셈)
|
||||
|
||||
|
||||
class BuildTarget(_Strict):
|
||||
"""대상 하나 — 로직 · 자재 · 호표 중 하나 · 배 = 우리 수량 ÷ 엑셀 수량 · 몫 = 섞인 줄 몫."""
|
||||
|
||||
로직: str | None = None
|
||||
자재: str | None = None
|
||||
호표: int | None = None
|
||||
전국: bool | None = None # 「전국」 자재(361-1 · 키 없음 · 서버가 줄에서 다시 셈)
|
||||
노임: str | None = None # 729-2 사용자 이음 — LB 키(엑셀 숫자 + 이음)
|
||||
기계: str | None = None # EQ 키
|
||||
비율: Literal["노무비", "재료비", "경비"] | None = None # % 줄 밑수
|
||||
입력값: dict[str, Any] = Field(default_factory=dict)
|
||||
배: float = 1
|
||||
몫: float | None = None
|
||||
|
||||
|
||||
class BuildLine(_Strict):
|
||||
"""확인한 줄 — `차례` = 호표 안 줄(0부터) · `대상` 목록 또는 `뺌` 까닭."""
|
||||
|
||||
차례: int
|
||||
대상: list[BuildTarget] = Field(default_factory=list)
|
||||
뺌: Literal["우리에 없음", "안 씀"] | None = None
|
||||
|
||||
|
||||
class BuildWhole(_Strict):
|
||||
대상: list[BuildTarget]
|
||||
|
||||
|
||||
class BuildBody(_Strict):
|
||||
"""실무 호표 → 이 층 사전조합 — 파일 층 `층` · `주인` · `키` + 호표 `번` + 확인한 `줄` ·
|
||||
직접 줄 호표 = `통째` · 같은 본뜬은 늘 덮음."""
|
||||
|
||||
층: Layer
|
||||
주인: str | None = None
|
||||
키: str
|
||||
번: int
|
||||
줄: list[BuildLine] = Field(default_factory=list, max_length=500)
|
||||
통째: BuildWhole | None = None
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-build")
|
||||
def hopyo_make(
|
||||
layer: Layer,
|
||||
body: BuildBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""[확인하고 사전조합 만들기] — 확인한 줄만(이 층 쓰기 · 파일 층 읽기) · 별칭 장부 씀 →
|
||||
`{참조, 이름, 다시씀, 재사용, 빠진줄, 하위, 비교, 장부}` · 하위 호표 확인 전 = 함께 만듦(실패 409)."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
source = _root(session, body.층, body.주인)
|
||||
lines = [one.model_dump(exclude_none=True) for one in body.줄]
|
||||
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
|
||||
args = (folder, source, body.키, body.번, lines, whole, _who(session))
|
||||
return _call(folder, hopyo_build.build, *args)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-unlinked")
|
||||
def hopyo_unlinked(
|
||||
layer: Layer,
|
||||
body: BuildBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""안 이은 줄 미리 보기(729-2) — 만들기 몸 그대로 → `{남은줄 [{차례, 이름, 규격, 단위, 갈래, 까닭,
|
||||
후보}], 수}`(저장 없음)."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
source = _root(session, body.층, body.주인)
|
||||
lines = [one.model_dump(exclude_none=True) for one in body.줄]
|
||||
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
|
||||
args = (folder, source, body.키, body.번, lines, whole)
|
||||
return _call(folder, hopyo_build.unlinked, *args)
|
||||
|
||||
|
||||
class BuildAllBody(_Strict):
|
||||
"""시트 통째(705-3) — 파일 층 `층` · `주인` · `키` 만(숫자 · 줄 칸 없음 · 서버가 파일에서 읽음)."""
|
||||
|
||||
층: Layer
|
||||
주인: str | None = None
|
||||
키: str
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-build-all")
|
||||
def hopyo_make_all(
|
||||
layer: Layer,
|
||||
body: BuildAllBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""[모두 고정으로 들여오기] — 시트 호표 모두 고정 줄로 · 하위 먼저 · 확인된 호표 그대로 →
|
||||
`{호표, 줄, 품셈, 고정, 호표참조, 빠짐, 이음, 이음후보, 같음, 그대로, 다름, 실패}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
source = _root(session, body.층, body.주인)
|
||||
args = (folder, source, body.키, _who(session), True) # 729-2 — 남은 줄 있는 호표 = 실패
|
||||
return _call(folder, hopyo_build.build_all, *args)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo-ledger")
|
||||
def hopyo_ledger_read(
|
||||
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""별칭 장부 — 이 층 + 위 층 줄 `{열쇠, 원래, 갈래, 짝, 누가, 때, 처음파일, 층, 주인, 고침}`
|
||||
(고침 = 이 층 · 쓰기 권한)."""
|
||||
folder = _root(session, layer, owner)
|
||||
writable = access.grant(session, layer, owner).write
|
||||
return {"줄": _call(folder, hopyo_ledger.rows, folder, writable)}
|
||||
|
||||
|
||||
class LedgerBody(_Strict):
|
||||
"""`줄` = {열쇠: 장부 줄 | null(지움)} — 이 층만 · 서버가 짝을 다시 검사."""
|
||||
|
||||
줄: dict[str, Any]
|
||||
|
||||
|
||||
@router.put("/layers/{layer}/hopyo-ledger")
|
||||
def hopyo_ledger_write(
|
||||
layer: Layer,
|
||||
body: LedgerBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""별칭 장부 [저장] — 고친 줄 · 지운 줄(층 쓰기) → `{씀, 줄}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, hopyo_ledger.put, folder, body.줄, _who(session))
|
||||
|
||||
|
||||
@router.delete("/layers/{layer}/hopyo/{key}")
|
||||
def hopyo_delete(
|
||||
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""실무자료 지우기 = [끝내기](718-1 · 743-1 · 화면 떠남도) — HP · 격자 · 맞춤셈 캐시 지움 ·
|
||||
만든 일위대가 · 단가산출은 엑셀 사본만 걷고 남음 · 별칭 장부 그대로 → `{지움, 만든}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
got = _call(folder, hopyo_forget.finish, folder, key)
|
||||
hopyo_judge.forget(folder, key) # 블록 풀이 저장본(785-5)
|
||||
return got
|
||||
|
||||
|
||||
class SangeunBody(_Strict):
|
||||
"""`확인` = 사용자가 확인한 블록만 단위 SK `[{번, 고름?: {차례: 고름}}]`(778-5 · 자동 없음)."""
|
||||
|
||||
확인: list[dict[str, Any]] = Field(default_factory=list, max_length=500)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo/{key}/sangeun")
|
||||
def hopyo_make_sangeun(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
body: SangeunBody | None = None,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""불러오기 결과 「단가산출」(718-1 · 778-5) — 블록마다 줄 판정 · 확인한 블록만 단위 SK
|
||||
(파일 층 = 이 층) → `{블록, 판정, 만듦, 못만듦, 그대로, 같은것씀, 위층같음}` · 맞춤 셈 전 409."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
picks = body.확인 if body else []
|
||||
return _call(folder, hopyo_sangeun.build, folder, key, _who(session), picks)
|
||||
|
||||
|
||||
class PasteBody(_Strict):
|
||||
"""엑셀 붙여넣기(705-2) — `글` = 붙인 글 그대로 · `열` = 기억한 열 짝(없으면 머리 글 · 짐작 + 물음) ·
|
||||
`줄만` = 열린 문서에 줄로 · `있음` = 문서의 고정 줄 가름(이름 겹침 피함)."""
|
||||
|
||||
글: str = Field(max_length=hopyo_paste.TEXT_MAX)
|
||||
열: dict[str, int] | None = None
|
||||
줄만: bool = False
|
||||
있음: list[str] = Field(default_factory=list, max_length=500)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-paste")
|
||||
def hopyo_paste_run(
|
||||
layer: Layer,
|
||||
body: PasteBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""[Ctrl+V] — `갈래` 물음(열 짝) · 줄(고정 줄 · 저장은 문서 [저장]) · 만듦(실무자료 + 호표마다 사전조합 ·
|
||||
층 쓰기 · `build_all` 답 + 키)."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
args = (folder, body.글, body.열, _who(session), body.줄만, body.있음)
|
||||
return _call(folder, hopyo_paste.paste, *args)
|
||||
|
||||
|
||||
class RelinkRowsBody(BaseModel):
|
||||
"""다시 잇기 표(705-2) — `키` = 이 층 사전조합들 · `문서` = 열린 문서(저장 전 · 줄 · 후보만)."""
|
||||
|
||||
키: list[str] = Field(default_factory=list, max_length=500)
|
||||
문서: dict[str, Any] | None = None
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-relink/rows")
|
||||
def hopyo_relink_rows(
|
||||
layer: Layer,
|
||||
body: RelinkRowsBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""「이 공사 단가로 다시 잇기」 표 — 고정 줄마다 붙인 단가 · 이은 단가 · 후보(층 읽기)."""
|
||||
folder = _root(session, layer, owner)
|
||||
return _call(folder, hopyo_relink.rows, body.키, body.문서)
|
||||
|
||||
|
||||
class RelinkBody(_Strict):
|
||||
"""바꿈 `[{키, 줄, 노임?, 자재?}]` — 이음 키만(숫자 칸 없음 · 값 null = 풂)."""
|
||||
|
||||
바꿈: list[dict[str, Any]] = Field(max_length=hopyo_relink.CHANGES_MAX)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-relink")
|
||||
def hopyo_relink_apply(
|
||||
layer: Layer,
|
||||
body: RelinkBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""[바꿈] · [되돌리기] — 이 층 사전조합 이음 칸 고쳐 씀(층 쓰기) → `{바뀜, 문서, 되돌림}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, hopyo_relink.apply, body.바꿈)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo/{key}/{no}/siblings")
|
||||
def hopyo_siblings(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
no: int,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""규격 묶음 제안(704-4) — 이름 · 단위 · 줄 구성 같고 규격만 다른 호표 `{묶음 [{번, 규격, 열}], 이름}`."""
|
||||
folder = _root(session, layer, owner)
|
||||
return _call(folder, hopyo_item.siblings, folder, key, no)
|
||||
|
||||
|
||||
class ItemBody(BuildBody):
|
||||
"""[구조물 조합으로](704-4) — 확인 창 몸 그대로 + `묶음` = 규격 열로 묶을 호표 번 · `이름` = 항목 이름."""
|
||||
|
||||
묶음: list[int] = Field(default_factory=list, max_length=20)
|
||||
이름: str | None = Field(None, max_length=120)
|
||||
새로: bool = False # 같은 내용 항목 409 뒤 — 그래도 새로(733-2)
|
||||
씀: str | None = Field(None, max_length=80) # 같은 내용 409 뒤 — 그 항목 씀
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-item")
|
||||
def hopyo_to_item(
|
||||
layer: Layer,
|
||||
body: ItemBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""실무 호표 → 이 층 구조물 조합 항목(사전조합 안 만듦 · 이 층 쓰기 · 파일 층 읽기) →
|
||||
`{key, 열, 이름, 줄수, 열수, 빠진줄}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
source = _root(session, body.층, body.주인)
|
||||
lines = [one.model_dump(exclude_none=True) for one in body.줄]
|
||||
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
|
||||
args = (folder, source, body.키, body.번, lines, whole, body.묶음, body.이름, _who(session))
|
||||
args += (body.새로, body.씀)
|
||||
return _call(folder, hopyo_item.make_item, *args)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo-marks/{key}")
|
||||
def hopyo_marks_read(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
owner: str | None = None,
|
||||
to: Layer | None = None,
|
||||
to_owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""하이라이트 재료(779-2 · 읽기만) — `블록 [{번, 판정, SK, 갈래, 못옮김}]` · `to` = SK 를 볼 층."""
|
||||
folder = _root(session, layer, owner)
|
||||
target = _root(session, to, to_owner) if to else folder
|
||||
return _call(folder, lambda: {"블록": hopyo_marks.blocks(folder, key, target)})
|
||||
@@ -1,7 +1,7 @@
|
||||
"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 729-1 · 778-5 D-3 · 요구 621 · 623) — 산근(단산) 블록마다
|
||||
줄 판정 모음(`M02_Hopyo_SkUnit`) → 사용자가 확인한 블록만 단위 SK.
|
||||
|
||||
모두 줄로만(바탕 기초 안 씀 · 맞춤셈 1위 기초 = 근거 꼬리표만) · 자동 없음 — `확인 [{번, 고름?: {차례: 고름}}]`
|
||||
모두 줄로만(바탕 기초 안 씀 · 조항 색인 기초 = 근거 꼬리표만) · 자동 없음 — `확인 [{번, 고름?: {차례: 고름}}]`
|
||||
(블록마다 [확인] · [옮기기])에 든 블록만 만듦 · 확인 안 한 블록 = 판정만(안 만듦) · 줄 다 이어지지 않은 확인 블록 =
|
||||
`못만듦` · 하위 단산 줄 = 그 블록 SK 참조(확인 목록에 있으면 먼저 만듦 · 없으면 못만듦).
|
||||
SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 ·
|
||||
@@ -19,24 +19,26 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_time import utc_now_text
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Clause as clause
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch
|
||||
from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit
|
||||
from M02_MasterTemplete import M02_Hopyo_Units as units
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Hopyo_Units as units
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Library_Same as same
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete.M02_Hopyo_Kids import _same_mark
|
||||
|
||||
ORIGIN = "실무자료"
|
||||
KIND = "sangeun"
|
||||
WAIT_WHY = "확인 전 — [단가산출]에서 블록 [확인] 뒤 [옮기기]"
|
||||
WAIT = "고름 대기"
|
||||
skpatch = SimpleNamespace(WAIT=WAIT, SK="SK") # 옛 이름(SkLink 가 읽음 · 785-7 지움)
|
||||
|
||||
|
||||
def mark(key: str, no: int) -> str:
|
||||
@@ -64,13 +66,6 @@ def made(
|
||||
return out
|
||||
|
||||
|
||||
def _same_mark(a: dict, b: dict) -> bool:
|
||||
"""같은 출처 — 본뜬 키 같음 · 원본(파일 층)이 둘 다 있으면 같음."""
|
||||
if a.get("키") != b.get("키"):
|
||||
return False
|
||||
return not a.get("원본") or not b.get("원본") or tuple(a["원본"]) == tuple(b["원본"])
|
||||
|
||||
|
||||
def origin_of(doc: dict, mark_: str, source: refs.Here | None = None) -> dict[str, Any]:
|
||||
"""그 블록 출처의 본뜬(갈래 · 못옮김 · 놓음 …) — 또 가운데 `mark_` · 없으면 제 본뜬."""
|
||||
own = doc.get("본뜬") or {}
|
||||
@@ -147,14 +142,13 @@ def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) ->
|
||||
|
||||
|
||||
class Maker:
|
||||
"""한 번 만들기 — 파일 · 맞춤셈 · 이 층 SK 를 한 번 읽음(`source` = 파일 층 · 쓰기 = `store.root()`)."""
|
||||
"""한 번 만들기 — 파일 · 이 층 SK 를 한 번 읽음(`source` = 파일 층 · 쓰기 = `store.root()`)."""
|
||||
|
||||
def __init__(self, source: Path, key: str, who: Any = None) -> None:
|
||||
self.source, self.key, self.who = Path(source), key, who
|
||||
self.hp = hopyo_store.read(source, key)
|
||||
self.blocks, self.first = calls(self.hp)
|
||||
got = fit.result(source, key) or {}
|
||||
self.cache, self.done = got.get("블록") or {}, bool(got.get("끝"))
|
||||
self.done = True # 옛 이름(SkLink) — 맞춤셈 안 기다림(785-5)
|
||||
self.here = refs.where(source)
|
||||
self.whole = m01.loaded()[1]
|
||||
self.sure = {"원본": list(self.here), "확인": {"누가": who, "때": utc_now_text()}}
|
||||
@@ -170,19 +164,27 @@ class Maker:
|
||||
return self._units
|
||||
|
||||
def basis(self, no: int) -> dict[str, Any] | None:
|
||||
"""근거 꼬리표 = 맞춤셈 1위 기초(참고) · 출처 조항이 우리 기초 밖이고 1위가 맞음 아니면
|
||||
1위 대신 표지(781-2 · 「기초 없음 — 건설 공통 1-2-7 운반」 · 「옛 번호 — 근거 못 씀」)."""
|
||||
if no not in self.first: # 일위대가 밖 = 맞춤셈 없음
|
||||
"""근거 꼬리표(참고) — 조항 색인(`M02_Hopyo_Clause.basis`) · 표지(기초 없음 · 옛 번호 · 준용 ·
|
||||
번호 없음) 있으면 표지 · 후보 하나면 그 기초 · 여럿이면 블록 풀이(`M02_Hopyo_Judge` 저장본 ›
|
||||
그 블록만 셈) 첫 고름 · 일위대가 밖 블록 = 없음."""
|
||||
if no not in self.first:
|
||||
return None
|
||||
entry = self.cache.get(fit.unit_key(self.first[no])) or {}
|
||||
top = (entry.get("차례") or [None])[0]
|
||||
outside = hopyo_basis.no_basis(self.blocks[no].get("출처"))
|
||||
if outside and (entry.get("판정") or {}).get("상태") != "맞음":
|
||||
return {"로직": None, "이름": outside["이름"], "판정": outside["판정"]}
|
||||
got = clause.basis(self.blocks[no])
|
||||
if got["표지"]:
|
||||
return {"로직": None, "이름": got["글"], "판정": got["표지"]}
|
||||
cands = got["후보"]
|
||||
top = cands[0]["키"] if len(cands) == 1 else self._solved(no)
|
||||
if not top:
|
||||
return None
|
||||
name = (self.whole.logic(top) or {}).get("이름")
|
||||
return {"로직": top, "이름": name, "판정": (entry.get("판정") or {}).get("상태")}
|
||||
return {"로직": top, "이름": (self.whole.logic(top) or {}).get("이름"), "판정": None}
|
||||
|
||||
def _solved(self, no: int) -> str | None:
|
||||
from M02_MasterTemplete import M02_Hopyo_Judge as judge # Judge → 여기 — 늦게 읽음
|
||||
|
||||
cache = judge.solved(self.source, self.key) if getattr(self, "key", None) else None
|
||||
line = {k: v for k, v in self.first[no].items() if k != "_호표"}
|
||||
got = (cache or {}).get(str(no)) or judge._solve(judge.master(), self.blocks[no], line, [])
|
||||
return next((s["고름"] for s in got["절"] if s["고름"]), None)
|
||||
|
||||
def ref_of(self, sub: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""하위 줄 → 참조 칸 — 단산 = 이 HP 그 블록 SK · 일위 = 확인된 그 호표 SC · 없으면 None."""
|
||||
@@ -209,10 +211,10 @@ class Maker:
|
||||
def plan(self, no: int, picks: dict | None = None) -> dict[str, Any]:
|
||||
"""확인한 블록 → 단위 SK 꼴 · 확인 없음(`picks` None) · 고를 줄 남음 = 고름 대기."""
|
||||
if picks is None:
|
||||
return {"갈래": skpatch.WAIT, "까닭": WAIT_WHY, "후보": []}
|
||||
return {"갈래": WAIT, "까닭": WAIT_WHY, "후보": []}
|
||||
got = self.judge(no, picks)
|
||||
if got["고를줄"]:
|
||||
return {"갈래": skpatch.WAIT, "까닭": got["판정"], "후보": []}
|
||||
return {"갈래": WAIT, "까닭": got["판정"], "후보": []}
|
||||
return skunit.plan(got, self.basis(no))
|
||||
|
||||
def make(self, no: int, sure: dict[int, dict], out: dict, path: tuple = ()) -> None:
|
||||
@@ -229,7 +231,7 @@ class Maker:
|
||||
got = self.judged[no]
|
||||
lost = [r for r in got["줄"] if r["상태"] == units.LINKED and r["차례"] not in got["담음"]]
|
||||
name = self.blocks[no].get("이름") or (self.first.get(no) or {}).get("이름")
|
||||
if plan["갈래"] == skpatch.WAIT or lost:
|
||||
if plan["갈래"] == WAIT or lost:
|
||||
why = plan.get("까닭") or f"하위 「{lost[0]['이름']}」 — 그 블록 먼저 확인"
|
||||
out["못만듦"].append({"번": no, "이름": name, "까닭": why})
|
||||
return
|
||||
@@ -296,9 +298,6 @@ def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[st
|
||||
bad = sorted(set(sure) - set(maker.blocks))
|
||||
if bad:
|
||||
raise _bad(f"없는 블록 「단산{bad[0]}」", 404)
|
||||
if not maker.done:
|
||||
fit.ensure(folder, key)
|
||||
raise _bad("맞춤 셈 중 — 끝나면 다시 누름", 409)
|
||||
out: dict[str, Any] = {"블록": len(maker.blocks), "만듦": [], "못만듦": []}
|
||||
for no in sorted(sure):
|
||||
maker.make(no, sure, out)
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
단위 SK 더한줄(바탕 없음 · 778-4 꼴) = 인력 `{요소 LB, 수량 품}` · 기계경비 로직 `{로직 GC, 입력값 {기계},
|
||||
수량 시간 × 할증}` · 재료 `{요소 MT, 수량(손료율 든), 별도계상?}`(못 찾은 별도계상 = 0 원 고정 줄) ·
|
||||
율 `{고정 {비율 {기준, 비목}}, 수량 = 율 %}`(줄 목록 = 그 행 줄 `add:#k`) · 하위 `{참조 SK · SC}` · 값 고정 줄.
|
||||
근거 꼬리표 = 맞춤셈 1위 기초(본뜬 칸 · 참고). 서버 단독 · 쓰기 = `M02_Hopyo_Sangeun`.
|
||||
근거 꼬리표 = 조항 색인(`Sangeun.Maker.basis` · 참고) · 고른 것(785-5 `M02_Hopyo_Make`)도 `adds` 를 부름 ·
|
||||
기계경비 기초 차례(`_mach_logics`) = 옛 SkPatch 에서 옮김. 서버 단독 · 쓰기 = `M02_Hopyo_Sangeun`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -16,7 +17,6 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
|
||||
from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch
|
||||
from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line
|
||||
from M02_MasterTemplete import M02_Hopyo_Units as units
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
@@ -29,13 +29,41 @@ OUT, DROP = "밖", "뺌"
|
||||
UNIT = "단위" # 본뜬 갈래
|
||||
MINE = "내가 고름"
|
||||
WORD = {"L": "품", "X": "시간", "M": "수량", "R": "율", "S": "수량"}
|
||||
PICKS = {"L": ("노임", "LB"), "X": ("기계", "EQ"), "M": ("자재", "MT")}
|
||||
PICKS = {"L": ("노임", "LB"), "X": ("기계", "EQ"), "M": ("자재", ("MT", "MO"))}
|
||||
MACH_IN = "기계" # 기계경비 기초 기계 입력 칸
|
||||
MACHINE = {"굴착기": "GC000267"} # 굴착기(무한궤도) 시간당 기계경비 · 그 밖 장비 = GC000268
|
||||
MACHINE_ANY = "GC000268"
|
||||
MACHINE_SET = {"GC000267", "GC000268", "GC000269", "GC000275"}
|
||||
# (주연료, 조종원) 없는 기종 → 그 기계경비 기초(M01 로직이 부르는 것과 같음 · 778-2)
|
||||
SOLO = {(False, False): "GC000273", (True, False): "GC000272", (False, True): "GC000276"}
|
||||
|
||||
|
||||
class PickError(ValueError):
|
||||
"""고름 틀림 — 부르는 쪽이 400."""
|
||||
|
||||
|
||||
def _mach_logics(whole: Any, kinds: set, eq: dict | None = None) -> list[str]:
|
||||
"""기계경비 기초 차례 — 갈래 첫 기초(굴착기 = 무한궤도) › 그 밖 · `eq` 가 있으면
|
||||
연료 · 조종원 없는 기종 = 그 기초 하나 · 아니면 그 기계(또는 같은 이름 기계)를 고르기에 둔 기초가 앞."""
|
||||
first = next((c for k, c in MACHINE.items() if k in kinds), MACHINE_ANY)
|
||||
order = [first, *sorted(MACHINE_SET - {first})]
|
||||
if not eq:
|
||||
return order
|
||||
has = eq.get("값") if isinstance(eq.get("값"), dict) else {}
|
||||
if solo := SOLO.get((bool(has.get("주연료")), bool(has.get("조종원")))):
|
||||
return [solo]
|
||||
eqs = whole.numbers.get("EQ", {})
|
||||
|
||||
def takes(code: str) -> bool:
|
||||
ins = whole.logic(code).get("입력") or []
|
||||
opts = next((s.get("고르기") or [] for s in ins if s.get("이름") == MACH_IN), [])
|
||||
same = any((eqs.get(o) or {}).get("이름") == eq.get("이름") for o in opts)
|
||||
return eq.get("원문번호") in opts or same
|
||||
|
||||
hit = [c for c in order if takes(c)]
|
||||
return hit + [c for c in order if c not in hit]
|
||||
|
||||
|
||||
def _picked(one: dict[str, Any], pick: Any, us: units.Units) -> dict[str, Any]:
|
||||
"""사용자 고름 → 이어짐 단위 — 노임 LB · 기계 EQ(→ 기계경비 로직) · 자재 MT · 율 기준 · 뺌."""
|
||||
if not isinstance(pick, dict):
|
||||
@@ -50,7 +78,8 @@ def _picked(one: dict[str, Any], pick: Any, us: units.Units) -> dict[str, Any]:
|
||||
field, head = PICKS.get(kind, ("", "?"))
|
||||
code = str(pick.get(field) or "")
|
||||
if not field or not code.startswith(head):
|
||||
raise PickError(f"{units.HOW.get(kind, kind)} 줄 · {field or '뺌'} 키({head}…)로 고름")
|
||||
shown = head if isinstance(head, str) else head[0]
|
||||
raise PickError(f"{units.HOW.get(kind, kind)} 줄 · {field or '뺌'} 키({shown}…)로 고름")
|
||||
try:
|
||||
row = us.whole.get(code)
|
||||
except Exception as e: # noqa: BLE001 — 없는 키
|
||||
@@ -58,7 +87,7 @@ def _picked(one: dict[str, Any], pick: Any, us: units.Units) -> dict[str, Any]:
|
||||
unit: dict[str, Any] = {field: code}
|
||||
if kind == "X":
|
||||
kinds = mtypes(f"{row.get('상세구분')} {row.get('이름')}")
|
||||
unit["로직"] = skpatch._mach_logics(us.whole, kinds, row)[0]
|
||||
unit["로직"] = _mach_logics(us.whole, kinds, row)[0]
|
||||
unit |= {"할증": one["할증"]} if one.get("할증") else {}
|
||||
if kind == "M" and (one.get("어떻게") == "별도" or pick.get("별도계상")):
|
||||
unit["별도계상"] = True
|
||||
@@ -110,7 +139,7 @@ def adds(block: dict, rows: list[dict], us: units.Units, ref_of: Any) -> tuple[l
|
||||
add = {"종류": "인력", "요소": unit["노임"], "이름": name, "단위": "인", "수량": q}
|
||||
elif kind == "X":
|
||||
eq = us.whole.get(unit["기계"]) or {}
|
||||
given = {skpatch.MACH_IN: eq.get("원문번호")}
|
||||
given = {MACH_IN: eq.get("원문번호")}
|
||||
q *= float(unit.get("할증") or 1)
|
||||
add = {
|
||||
"종류": "로직",
|
||||
|
||||
@@ -5,38 +5,19 @@
|
||||
지우기는 양식 길 그대로) + 개인 자재 목록 · 추가(148-2 · 개인 층만 · 품번은 서버) +
|
||||
서고 나무 자유 폴더(158-1 · 716-4 `{종류}-tree` · `M02_Precombo_Folders` · 읽기 = 층 읽기 ·
|
||||
고침 = 층 쓰기) +
|
||||
일위대가 실무자료(324 · `M02_Hopyo_*` · 올리기 · 지우기 = 층 쓰기 · 목록 · 읽기 = 층 읽기) ·
|
||||
338 = 확인한 줄만 만들기(몸 없음 400 · 옛 즉시 확정 길 없앰) · 별칭 장부 보기 · 고치기 ·
|
||||
올린 뒤 맞춤 뒤 셈(`M02_Hopyo_Fit.ensure`) · 시동 때 우리 지문 데움(`M02_Hopyo_Ours.warm`) ·
|
||||
718-1 한시 불러오기 = 올림 지문(작업 중 같은 엑셀 = 그 파일 `이어서` · 끝낸 것 = 새로 · 743-1) ·
|
||||
지우기 = [끝내기](`M02_Hopyo_Forget.finish` · 올림 때 하루 지난 HP 도 끝냄).
|
||||
일위대가 실무자료 길 = `M02_Hopyo_Router`(785-5).
|
||||
층 권한 = `Router_Items._root`(`M02_Layer_Access` 한 벌).
|
||||
⚠ 등록은 `main.py`(로그인만).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as hopyo_fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Forget as hopyo_forget
|
||||
from M02_MasterTemplete import M02_Hopyo_Grid as hopyo_grid
|
||||
from M02_MasterTemplete import M02_Hopyo_Item as hopyo_item
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as hopyo_ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_Marks as hopyo_marks
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
|
||||
from M02_MasterTemplete import M02_Hopyo_Ours as hopyo_ours
|
||||
from M02_MasterTemplete import M02_Hopyo_Paste as hopyo_paste
|
||||
from M02_MasterTemplete import M02_Hopyo_Relink as hopyo_relink
|
||||
from M02_MasterTemplete import M02_Hopyo_Sangeun as hopyo_sangeun
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_Layer_Access as access
|
||||
@@ -206,420 +187,3 @@ def new_material(
|
||||
raise HTTPException(status_code=400, detail="신규 자재는 개인 층에서만")
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, own.add, folder, body)
|
||||
|
||||
|
||||
# ── 일위대가 실무자료(324 · 338) ───────────────────────────────────────
|
||||
HOPYO_MAX_BYTES = 20 * 1024 * 1024
|
||||
|
||||
|
||||
hopyo_ours.warm() # 시동 — 우리 지문 캐시(디스크) 읽어 둠
|
||||
|
||||
|
||||
def _who(session: Session) -> dict[str, Any]:
|
||||
return {"id": session.get("user_id"), "이름": session.get("name")}
|
||||
|
||||
|
||||
def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]:
|
||||
mark = hopyo_store.fingerprint(data)
|
||||
hopyo_forget.sweep(store.root()) # 하루 지난 작업 중 HP 끝냄(743-1)
|
||||
if same := next((r for r in hopyo_store.rows(store.root()) if r.get("지문") == mark), None):
|
||||
return {**same, "이어서": True}
|
||||
try:
|
||||
parsed = hopyo_excel.read_book(name, data)
|
||||
except hopyo_excel.ExcelError as error:
|
||||
raise store.StoreError(400, str(error)) from error
|
||||
try:
|
||||
grid = hopyo_grid.read(name, data, parsed)
|
||||
except Exception: # 격자는 보기용 덤 — 못 만들어도 올리기는 됨(보기 창 404)
|
||||
grid = None
|
||||
row = hopyo_store.save(store.root(), name, parsed, who, grid, mark)
|
||||
hopyo_fit.ensure(store.root(), row["키"]) # 맞춤 뒤 셈(일감 줄 · 화면 안 기다림)
|
||||
return row
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo")
|
||||
def hopyo_list(
|
||||
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""실무자료 목록 `{줄: [{키, 파일, 올린때, 올린이, 시트, 호표수, 어긋남}]}`."""
|
||||
folder = _root(session, layer, owner)
|
||||
rows = hopyo_store.rows(folder)
|
||||
for row in rows: # 지금 판 맞춤셈 없는 파일 = 목록 열 때 뒤 셈 예약(358-3 ③)
|
||||
hopyo_fit.ensure(folder, row["키"])
|
||||
return {"줄": rows}
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo-picks")
|
||||
def hopyo_picks(
|
||||
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""[로직 추가] 「실무 호표」 줄 — 이 층 · 위 층 파일의 호표마다
|
||||
`{층, 주인, 키, 파일, 번, 이름, 규격, 단위, 맞춤, 확인}`(확인 = 이 층에 확인된 사전조합)."""
|
||||
folder = _root(session, layer, owner)
|
||||
return {"줄": _call(folder, _picks, folder)}
|
||||
|
||||
|
||||
def _picks(folder: Path) -> list[dict[str, Any]]:
|
||||
rows, here, done = hopyo_match.picks(folder), refs.where(folder), {}
|
||||
for row in rows:
|
||||
src = (row["층"], row.get("주인"))
|
||||
if src not in done:
|
||||
done[src] = hopyo_build.confirmed(folder, src)
|
||||
key = done[src].get(hopyo_build.mark(row["키"], row["번"]))
|
||||
row["확인"] = {"층": here[0], "주인": here[1], "키": key} if key else None
|
||||
return rows
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo")
|
||||
def hopyo_upload(
|
||||
layer: Layer,
|
||||
file: UploadFile = File(...),
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""엑셀(xls · xlsx) 올리기 — 「일위대가」 시트를 읽어 결과 · 격자 저장 · 20 MB ·
|
||||
엉터리 · 시트 없음 400."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
data = file.file.read(HOPYO_MAX_BYTES + 1)
|
||||
if len(data) > HOPYO_MAX_BYTES:
|
||||
raise HTTPException(status_code=400, detail="파일이 너무 큼(20 MB 넘음)")
|
||||
name = Path(file.filename or "실무자료").name[:120]
|
||||
return _call(folder, _upload, name, data, _who(session))
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo/{key}")
|
||||
def hopyo_read(
|
||||
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""실무자료 하나 — 검산 · 호표마다 `맞춤` [들어감, 줄 수] · 파일 `맞춤` 합."""
|
||||
folder = _root(session, layer, owner)
|
||||
return _call(folder, hopyo_match.view, folder, key)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo-grid/{key}")
|
||||
def hopyo_grid_read(
|
||||
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""엑셀꼴 격자(329-1) `{책: Workbook, 호표: [{번, 이름, 행}], 단산: [{번, 이름, 행}]}` ·
|
||||
옛 파일 404."""
|
||||
folder = _root(session, layer, owner)
|
||||
return _call(folder, hopyo_store.grid, folder, key)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo/{key}/{no}")
|
||||
def hopyo_one(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
no: int,
|
||||
owner: str | None = None,
|
||||
to: Layer | None = None,
|
||||
to_owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""호표 하나 — 줄마다 맞춤(상태 · 체크 제안 · 대상 · 후보 5) · `to` = 만들 층(없으면 파일 층 ·
|
||||
호표 참조 줄 체크 · 만든)."""
|
||||
folder = _root(session, layer, owner)
|
||||
target = _root(session, to, to_owner) if to is not None else None
|
||||
return _call(folder, lambda: hopyo_match.view(folder, key, no, to=target))
|
||||
|
||||
|
||||
class GuessBody(BaseModel):
|
||||
"""`차례` = 호표 안 줄(0부터) · `대상` = [{로직 | 자재, 입력값?, 몫?}](낱개도 받음)."""
|
||||
|
||||
차례: int
|
||||
대상: list[dict[str, Any]] | dict[str, Any]
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo/{key}/{no}/guess")
|
||||
def hopyo_guess(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
no: int,
|
||||
body: GuessBody,
|
||||
owner: str | None = None,
|
||||
to: Layer | None = None,
|
||||
to_owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""미리 셈(저장 안 함 · 설계 3-4) — 고른 대상 · 옵션으로 엔진 한 번 → `{대상, 품, 글}` ·
|
||||
`to` = 만들 층(없으면 파일 층)."""
|
||||
folder = _root(session, layer, owner)
|
||||
target = _root(session, to, to_owner) if to is not None else None
|
||||
args = (folder, key, no, body.차례, body.대상)
|
||||
return _call(folder, lambda: hopyo_match.guess(*args, to=target))
|
||||
|
||||
|
||||
class _Strict(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid") # 금액 · 품 칸 = 400(서버가 셈)
|
||||
|
||||
|
||||
class BuildTarget(_Strict):
|
||||
"""대상 하나 — 로직 · 자재 · 호표 중 하나 · 배 = 우리 수량 ÷ 엑셀 수량 · 몫 = 섞인 줄 몫."""
|
||||
|
||||
로직: str | None = None
|
||||
자재: str | None = None
|
||||
호표: int | None = None
|
||||
전국: bool | None = None # 「전국」 자재(361-1 · 키 없음 · 서버가 줄에서 다시 셈)
|
||||
노임: str | None = None # 729-2 사용자 이음 — LB 키(엑셀 숫자 + 이음)
|
||||
기계: str | None = None # EQ 키
|
||||
비율: Literal["노무비", "재료비", "경비"] | None = None # % 줄 밑수
|
||||
입력값: dict[str, Any] = Field(default_factory=dict)
|
||||
배: float = 1
|
||||
몫: float | None = None
|
||||
|
||||
|
||||
class BuildLine(_Strict):
|
||||
"""확인한 줄 — `차례` = 호표 안 줄(0부터) · `대상` 목록 또는 `뺌` 까닭."""
|
||||
|
||||
차례: int
|
||||
대상: list[BuildTarget] = Field(default_factory=list)
|
||||
뺌: Literal["우리에 없음", "안 씀"] | None = None
|
||||
|
||||
|
||||
class BuildWhole(_Strict):
|
||||
대상: list[BuildTarget]
|
||||
|
||||
|
||||
class BuildBody(_Strict):
|
||||
"""실무 호표 → 이 층 사전조합 — 파일 층 `층` · `주인` · `키` + 호표 `번` + 확인한 `줄` ·
|
||||
직접 줄 호표 = `통째` · 같은 본뜬은 늘 덮음."""
|
||||
|
||||
층: Layer
|
||||
주인: str | None = None
|
||||
키: str
|
||||
번: int
|
||||
줄: list[BuildLine] = Field(default_factory=list, max_length=500)
|
||||
통째: BuildWhole | None = None
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-build")
|
||||
def hopyo_make(
|
||||
layer: Layer,
|
||||
body: BuildBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""[확인하고 사전조합 만들기] — 확인한 줄만(이 층 쓰기 · 파일 층 읽기) · 별칭 장부 씀 →
|
||||
`{참조, 이름, 다시씀, 재사용, 빠진줄, 하위, 비교, 장부}` · 하위 호표 확인 전 = 함께 만듦(실패 409)."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
source = _root(session, body.층, body.주인)
|
||||
lines = [one.model_dump(exclude_none=True) for one in body.줄]
|
||||
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
|
||||
args = (folder, source, body.키, body.번, lines, whole, _who(session))
|
||||
return _call(folder, hopyo_build.build, *args)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-unlinked")
|
||||
def hopyo_unlinked(
|
||||
layer: Layer,
|
||||
body: BuildBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""안 이은 줄 미리 보기(729-2) — 만들기 몸 그대로 → `{남은줄 [{차례, 이름, 규격, 단위, 갈래, 까닭,
|
||||
후보}], 수}`(저장 없음)."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
source = _root(session, body.층, body.주인)
|
||||
lines = [one.model_dump(exclude_none=True) for one in body.줄]
|
||||
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
|
||||
args = (folder, source, body.키, body.번, lines, whole)
|
||||
return _call(folder, hopyo_build.unlinked, *args)
|
||||
|
||||
|
||||
class BuildAllBody(_Strict):
|
||||
"""시트 통째(705-3) — 파일 층 `층` · `주인` · `키` 만(숫자 · 줄 칸 없음 · 서버가 파일에서 읽음)."""
|
||||
|
||||
층: Layer
|
||||
주인: str | None = None
|
||||
키: str
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-build-all")
|
||||
def hopyo_make_all(
|
||||
layer: Layer,
|
||||
body: BuildAllBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""[모두 고정으로 들여오기] — 시트 호표 모두 고정 줄로 · 하위 먼저 · 확인된 호표 그대로 →
|
||||
`{호표, 줄, 품셈, 고정, 호표참조, 빠짐, 이음, 이음후보, 같음, 그대로, 다름, 실패}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
source = _root(session, body.층, body.주인)
|
||||
args = (folder, source, body.키, _who(session), True) # 729-2 — 남은 줄 있는 호표 = 실패
|
||||
return _call(folder, hopyo_build.build_all, *args)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo-ledger")
|
||||
def hopyo_ledger_read(
|
||||
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""별칭 장부 — 이 층 + 위 층 줄 `{열쇠, 원래, 갈래, 짝, 누가, 때, 처음파일, 층, 주인, 고침}`
|
||||
(고침 = 이 층 · 쓰기 권한)."""
|
||||
folder = _root(session, layer, owner)
|
||||
writable = access.grant(session, layer, owner).write
|
||||
return {"줄": _call(folder, hopyo_ledger.rows, folder, writable)}
|
||||
|
||||
|
||||
class LedgerBody(_Strict):
|
||||
"""`줄` = {열쇠: 장부 줄 | null(지움)} — 이 층만 · 서버가 짝을 다시 검사."""
|
||||
|
||||
줄: dict[str, Any]
|
||||
|
||||
|
||||
@router.put("/layers/{layer}/hopyo-ledger")
|
||||
def hopyo_ledger_write(
|
||||
layer: Layer,
|
||||
body: LedgerBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""별칭 장부 [저장] — 고친 줄 · 지운 줄(층 쓰기) → `{씀, 줄}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, hopyo_ledger.put, folder, body.줄, _who(session))
|
||||
|
||||
|
||||
@router.delete("/layers/{layer}/hopyo/{key}")
|
||||
def hopyo_delete(
|
||||
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""실무자료 지우기 = [끝내기](718-1 · 743-1 · 화면 떠남도) — HP · 격자 · 맞춤셈 캐시 지움 ·
|
||||
만든 일위대가 · 단가산출은 엑셀 사본만 걷고 남음 · 별칭 장부 그대로 → `{지움, 만든}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, hopyo_forget.finish, folder, key)
|
||||
|
||||
|
||||
class SangeunBody(_Strict):
|
||||
"""`확인` = 사용자가 확인한 블록만 단위 SK `[{번, 고름?: {차례: 고름}}]`(778-5 · 자동 없음)."""
|
||||
|
||||
확인: list[dict[str, Any]] = Field(default_factory=list, max_length=500)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo/{key}/sangeun")
|
||||
def hopyo_make_sangeun(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
body: SangeunBody | None = None,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""불러오기 결과 「단가산출」(718-1 · 778-5) — 블록마다 줄 판정 · 확인한 블록만 단위 SK
|
||||
(파일 층 = 이 층) → `{블록, 판정, 만듦, 못만듦, 그대로, 같은것씀, 위층같음}` · 맞춤 셈 전 409."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
picks = body.확인 if body else []
|
||||
return _call(folder, hopyo_sangeun.build, folder, key, _who(session), picks)
|
||||
|
||||
|
||||
class PasteBody(_Strict):
|
||||
"""엑셀 붙여넣기(705-2) — `글` = 붙인 글 그대로 · `열` = 기억한 열 짝(없으면 머리 글 · 짐작 + 물음) ·
|
||||
`줄만` = 열린 문서에 줄로 · `있음` = 문서의 고정 줄 가름(이름 겹침 피함)."""
|
||||
|
||||
글: str = Field(max_length=hopyo_paste.TEXT_MAX)
|
||||
열: dict[str, int] | None = None
|
||||
줄만: bool = False
|
||||
있음: list[str] = Field(default_factory=list, max_length=500)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-paste")
|
||||
def hopyo_paste_run(
|
||||
layer: Layer,
|
||||
body: PasteBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""[Ctrl+V] — `갈래` 물음(열 짝) · 줄(고정 줄 · 저장은 문서 [저장]) · 만듦(실무자료 + 호표마다 사전조합 ·
|
||||
층 쓰기 · `build_all` 답 + 키)."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
args = (folder, body.글, body.열, _who(session), body.줄만, body.있음)
|
||||
return _call(folder, hopyo_paste.paste, *args)
|
||||
|
||||
|
||||
class RelinkRowsBody(BaseModel):
|
||||
"""다시 잇기 표(705-2) — `키` = 이 층 사전조합들 · `문서` = 열린 문서(저장 전 · 줄 · 후보만)."""
|
||||
|
||||
키: list[str] = Field(default_factory=list, max_length=500)
|
||||
문서: dict[str, Any] | None = None
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-relink/rows")
|
||||
def hopyo_relink_rows(
|
||||
layer: Layer,
|
||||
body: RelinkRowsBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""「이 공사 단가로 다시 잇기」 표 — 고정 줄마다 붙인 단가 · 이은 단가 · 후보(층 읽기)."""
|
||||
folder = _root(session, layer, owner)
|
||||
return _call(folder, hopyo_relink.rows, body.키, body.문서)
|
||||
|
||||
|
||||
class RelinkBody(_Strict):
|
||||
"""바꿈 `[{키, 줄, 노임?, 자재?}]` — 이음 키만(숫자 칸 없음 · 값 null = 풂)."""
|
||||
|
||||
바꿈: list[dict[str, Any]] = Field(max_length=hopyo_relink.CHANGES_MAX)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-relink")
|
||||
def hopyo_relink_apply(
|
||||
layer: Layer,
|
||||
body: RelinkBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""[바꿈] · [되돌리기] — 이 층 사전조합 이음 칸 고쳐 씀(층 쓰기) → `{바뀜, 문서, 되돌림}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, hopyo_relink.apply, body.바꿈)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo/{key}/{no}/siblings")
|
||||
def hopyo_siblings(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
no: int,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""규격 묶음 제안(704-4) — 이름 · 단위 · 줄 구성 같고 규격만 다른 호표 `{묶음 [{번, 규격, 열}], 이름}`."""
|
||||
folder = _root(session, layer, owner)
|
||||
return _call(folder, hopyo_item.siblings, folder, key, no)
|
||||
|
||||
|
||||
class ItemBody(BuildBody):
|
||||
"""[구조물 조합으로](704-4) — 확인 창 몸 그대로 + `묶음` = 규격 열로 묶을 호표 번 · `이름` = 항목 이름."""
|
||||
|
||||
묶음: list[int] = Field(default_factory=list, max_length=20)
|
||||
이름: str | None = Field(None, max_length=120)
|
||||
새로: bool = False # 같은 내용 항목 409 뒤 — 그래도 새로(733-2)
|
||||
씀: str | None = Field(None, max_length=80) # 같은 내용 409 뒤 — 그 항목 씀
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/hopyo-item")
|
||||
def hopyo_to_item(
|
||||
layer: Layer,
|
||||
body: ItemBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""실무 호표 → 이 층 구조물 조합 항목(사전조합 안 만듦 · 이 층 쓰기 · 파일 층 읽기) →
|
||||
`{key, 열, 이름, 줄수, 열수, 빠진줄}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
source = _root(session, body.층, body.주인)
|
||||
lines = [one.model_dump(exclude_none=True) for one in body.줄]
|
||||
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
|
||||
args = (folder, source, body.키, body.번, lines, whole, body.묶음, body.이름, _who(session))
|
||||
args += (body.새로, body.씀)
|
||||
return _call(folder, hopyo_item.make_item, *args)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo-marks/{key}")
|
||||
def hopyo_marks_read(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
owner: str | None = None,
|
||||
to: Layer | None = None,
|
||||
to_owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""하이라이트 재료(779-2 · 읽기만) — `블록 [{번, 판정, SK, 갈래, 못옮김}]` · `to` = SK 를 볼 층."""
|
||||
folder = _root(session, layer, owner)
|
||||
target = _root(session, to, to_owner) if to else folder
|
||||
return _call(folder, lambda: {"블록": hopyo_marks.blocks(folder, key, target)})
|
||||
|
||||
@@ -96,6 +96,7 @@ from E03_StabSection.E03_StabSection_Router_WhatIf import router as e03_stab_wha
|
||||
from E03_StabSection.E03_StabSection_Router_Prob import router as e03_stab_prob_router
|
||||
from E06_StabRock.E06_StabRock_Router import router as e06_stab_rock_router
|
||||
from M01_MasterData.M01_MasterData_Router import router as m01_master_data_router
|
||||
from M02_MasterTemplete.M02_Hopyo_Router import router as m02_hopyo_router
|
||||
from M02_MasterTemplete.M02_Labor_Router import router as m02_labor_router
|
||||
from M02_MasterTemplete.M02_MasterTemplete_Router import router as m02_master_template_router
|
||||
from M02_MasterTemplete.M02_MasterTemplete_Router_Combos import router as m02_combos_router
|
||||
@@ -563,6 +564,7 @@ app.include_router(m02_items_router, dependencies=protected)
|
||||
app.include_router(m02_combos_router, dependencies=protected)
|
||||
app.include_router(m02_drawing_router, dependencies=protected)
|
||||
app.include_router(m02_labor_router, dependencies=protected)
|
||||
app.include_router(m02_hopyo_router, dependencies=protected)
|
||||
# 단가산출 로직 상세 읽기 · 셈(PLAN 717-1) — 로그인만 · 쓰기 없음
|
||||
app.include_router(
|
||||
__import__("M02_MasterTemplete.M02_Logic_Lib_Read", fromlist=["router"]).router,
|
||||
|
||||
@@ -82,7 +82,6 @@ def test_예시1_근거_인력운반_지게():
|
||||
assert got["차례"][0] == "GC001140" # 지게(사람 줄) — 1141 기계설비 · 1142 벽돌 아님
|
||||
maker = sangeun.Maker.__new__(sangeun.Maker)
|
||||
maker.first, maker.blocks, maker.whole = {10: line}, {10: block}, m01.loaded()[1]
|
||||
maker.cache = {fit.unit_key(line): got}
|
||||
basis = maker.basis(10)
|
||||
assert basis["로직"] == "GC001140" and basis["이름"].startswith("인력운반(지게)")
|
||||
judged = skunit.judge(block, units.Units(), lambda sub: None, None, "㎡")
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""642-5(PLAN 785-5) 만들기 · 길 지킴 시험(설계 642-A 5-3 넷째 줄) — 임시 storage(`client` 마당).
|
||||
|
||||
- 판정 길(목록 · 호표 · 블록) · 검산 길(입력값 = 엔진이 쓴 칸 모두 · 지역 · 자재지역 · 조종원직종 비움)
|
||||
- 옮기기 → SC 줄(고른 것마다 · 같은 별칭 · 본뜬 {호표, 차례, 행} · 수량 = 엑셀 × 몫) · 같은 기초 둘째 = 사본 ·
|
||||
별도계상 자재 · 입력 필요 = 그 줄만 막힘(고정 줄로 남음) · SK 더한줄(바탕 없음) · 장부 · 다시 옮김 = SC 안 늚
|
||||
- 금액 · 품 칸 막음 · M01 원본 그대로
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Bind as hb
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hbuild
|
||||
from M02_MasterTemplete import M02_Hopyo_Judge as judge
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
# 올리기 길 시험의 `client` 마당을 같이 씀
|
||||
# ruff: noqa: F811
|
||||
from resources.tester.test_m02_hopyo_excel import _post, client, master_hash # noqa: F401
|
||||
|
||||
API = "/api/m02/layers/personal"
|
||||
KEY = "HP-000001"
|
||||
|
||||
|
||||
def _mine():
|
||||
return layers.personal_dir(7, 42)
|
||||
|
||||
|
||||
def _doc(kind, key):
|
||||
return json.loads((_mine() / kind / f"{key}.json").read_bytes())
|
||||
|
||||
|
||||
def _at(unit, no):
|
||||
"""호표 줄 판에서 단산 N 을 부르는 줄."""
|
||||
return next(r for r in unit["줄"] if r.get("번") == no and "단가산출" in r["갈래"])
|
||||
|
||||
|
||||
def _base(row, sec=0):
|
||||
"""그 줄 풀이 절의 고른 기초 · 입력값."""
|
||||
part = row["풀이"]["절"][sec]
|
||||
cand = next(c for c in part["후보"] if c["키"] == part["고름"])
|
||||
return {"갈래": "기초", "키": cand["키"], "입력값": cand["바인딩"]["입력값"]}
|
||||
|
||||
|
||||
def _specs(code):
|
||||
m = hb.m01.cm.master()
|
||||
return {s["이름"] for s in hb.specs_of(m.logic(code))} - set(hb.SKIP)
|
||||
|
||||
|
||||
def test_판정_검산_옮기기(client):
|
||||
before = master_hash()
|
||||
assert _post(client).status_code == 200
|
||||
got = client.get(f"{API}/hopyo-judge/{KEY}").json()
|
||||
assert got["셈중"] is True and got["블록"][9]["판정"] is None # 뒤 셈 전
|
||||
assert judge.judged(_mine(), KEY) is None
|
||||
judge.solve_all(_mine(), KEY) # 올림 뒤 뒤 셈(시험은 바로)
|
||||
got = client.get(f"{API}/hopyo-judge/{KEY}").json()
|
||||
assert got["셈중"] is False and len(got["호표"]) == 68 and len(got["블록"]) == 95
|
||||
assert got["블록"][9]["번"] == 10 and got["블록"][9]["판정"] == "기초로 풂"
|
||||
assert judge.judged(_mine(), KEY)[10] == {"판정": "기초로 풂", "고를수": 0}
|
||||
assert got["호표"][0]["판정"] == {"제안 있음": 2, "2차 필요": 4}
|
||||
|
||||
two = client.get(f"{API}/hopyo-judge/{KEY}/2").json()
|
||||
ten = _at(two, 10) # 예시 1 야면석소운반 단산10
|
||||
assert ten["이름"] and "단가" in ten and ten["풀이"]["판정"] == "기초로 풂"
|
||||
ex1 = _base(ten)
|
||||
assert ex1["키"] == "GC001140" and ex1["입력값"]["운반무게"] == 575
|
||||
blk = client.get(f"{API}/hopyo-judge/{KEY}/block/10").json()
|
||||
assert blk["부름"] and "몫" in blk["절"][0] and "금액" in blk
|
||||
|
||||
check = client.post(
|
||||
f"{API}/hopyo/{KEY}/check", json={"호표": 2, "차례": ten["차례"], "고른": [ex1]}
|
||||
).json()
|
||||
one = check["고른"][0]
|
||||
assert one["판정"] == "품 같음" and one["입력필요"] == []
|
||||
assert set(one["입력값"]) == _specs("GC001140")
|
||||
assert not set(one["입력값"]) & set(hb.SKIP) # 지역 · 자재지역 · 조종원직종 비움
|
||||
assert int(one["금액"]["계"]) == 94227 # 엑셀 94,227 (원 미만 버림)
|
||||
assert check["수량"]["우리"] == ten["수량"]["엑셀"]
|
||||
|
||||
first = client.get(f"{API}/hopyo-judge/{KEY}/1").json()
|
||||
rebar, form = _at(first, 47), _at(first, 24) # 예시 2 철근 · 예시 3 거푸집
|
||||
a, b = _base(rebar, 0), _base(rebar, 1)
|
||||
b["입력값"] = {**b["입력값"], "구분": "토목"} # 엇갈림 = 사용자 고름
|
||||
a["몫"] = 0.5
|
||||
mat = first["줄"][0] # 레미콘 별도계상
|
||||
cement = {**mat["후보"][0], "별도계상": True}
|
||||
stuck = {"갈래": "기초", "키": "GC000211", "입력값": {"유형": "보통"}}
|
||||
ex3 = _base(form) # 예시 3 GC000211 보통 · 4회 · 높이감률 · 구멍땜 = 입력 필요
|
||||
url = f"{API}/hopyo/{KEY}/check"
|
||||
need = client.post(url, json={"호표": 1, "차례": form["차례"], "고른": [ex3]}).json()
|
||||
assert (
|
||||
need["고른"][0]["입력필요"] == ["높이감률", "구멍땜"]
|
||||
and need["고른"][0]["금액"]["계"] is None
|
||||
)
|
||||
ex3["입력값"] |= {"높이감률": 0, "구멍땜": 0}
|
||||
got3 = client.post(url, json={"호표": 1, "차례": form["차례"], "고른": [ex3]}).json()["고른"][0]
|
||||
assert (
|
||||
got3["판정"] == "품 같음" and abs(got3["금액"]["노무비"] - 38291) <= 1
|
||||
) # 엑셀 노무 38,291
|
||||
other = next(r for r in two["줄"] if r["차례"] > ten["차례"]) # 같은 기초 둘째 줄
|
||||
twice = {**ex1, "입력값": {**ex1["입력값"], "운반거리": 12}}
|
||||
body = {
|
||||
"층": "personal",
|
||||
"호표": {
|
||||
"1": {
|
||||
str(rebar["차례"]): [a, b],
|
||||
str(form["차례"]): [stuck],
|
||||
"0": [cement],
|
||||
},
|
||||
"2": {str(ten["차례"]): [ex1], str(other["차례"]): [twice]},
|
||||
},
|
||||
"블록": {"10": [{**ex1, "몫": 1}]},
|
||||
}
|
||||
res = client.post(f"{API}/hopyo/{KEY}/move", json=body)
|
||||
assert res.status_code == 200, res.text
|
||||
out = res.json()
|
||||
assert [x["번"] for x in out["만듦"]] == [1, 2] and out["장부"]["씀"] >= 4
|
||||
assert out["막힘"] == [
|
||||
{
|
||||
"호표": 1,
|
||||
"차례": form["차례"],
|
||||
"까닭": out["막힘"][0]["까닭"],
|
||||
"칸": out["막힘"][0]["칸"],
|
||||
}
|
||||
]
|
||||
assert out["막힘"][0]["까닭"].startswith("입력 필요") and out["막힘"][0]["칸"]
|
||||
|
||||
sc2 = _doc("precombo", out["만듦"][1]["키"])
|
||||
lines = sc2["담은로직"]
|
||||
mine = next(x for x in lines if x.get("로직") == "GC001140")
|
||||
assert mine["별칭"] == ten["이름"][: len(mine["별칭"])]
|
||||
assert mine["본뜬"] == {"호표": 2, "차례": ten["차례"], "행": ten["행"]}
|
||||
assert mine["수량"]["손"] == ten["수량"]["엑셀"]
|
||||
assert set(mine["입력값"]) == _specs("GC001140")
|
||||
wrap = next(x for x in lines if x.get("별칭") == other["이름"][: len(x.get("별칭") or "")])
|
||||
inner = wrap["참조"]["몸"]["담은로직"][
|
||||
0
|
||||
] # 같은 기초 둘째 = 한 줄 SC 사본(본뜬 = 사본 출처 740-1)
|
||||
assert inner["로직"] == "GC001140" and inner["입력값"]["운반거리"] == 12
|
||||
assert len(lines) == len(two["줄"]) # 확인 안 한 줄 = 고정(빠짐 0)
|
||||
|
||||
sc1 = _doc("precombo", out["만듦"][0]["키"])
|
||||
pair = [x for x in sc1["담은로직"] if (x.get("본뜬") or {}).get("차례") == rebar["차례"]]
|
||||
assert [x["로직"] for x in pair] == ["GC000206", "GC000207"]
|
||||
assert pair[0]["별칭"] == pair[1]["별칭"] # 한 엑셀 줄 = 우리 두 줄 · 같은 별칭
|
||||
excel = rebar["수량"]["엑셀"]
|
||||
assert pair[0]["수량"]["손"] == excel * 0.5 and pair[1]["수량"]["손"] == excel
|
||||
assert pair[1]["입력값"]["구분"] == "토목"
|
||||
held = next(
|
||||
x
|
||||
for x in sc1["담은로직"]
|
||||
if (x.get("고정") or {}).get("출처", {}).get("줄") == form["차례"]
|
||||
)
|
||||
assert "본뜬" not in held # 막힌 줄 = 고정 줄(엑셀 숫자)
|
||||
cement_row = next(m for m in sc1["자재"] if m["코드"] == cement["키"])
|
||||
assert labor.mark_of(cement_row, "별도계상") == "별도계상"
|
||||
assert cement_row["수량"]["손"] == mat["수량"]["엑셀"]
|
||||
|
||||
sk = _doc("sangeun", out["단가산출"][0]["키"])
|
||||
assert sk["바탕"] is None and [x["로직"] for x in sk["더한줄"]] == ["GC001140"]
|
||||
assert sk["더한줄"][0]["입력값"]["운반무게"] == 575
|
||||
|
||||
book = ledger.read(_mine())
|
||||
assert any(
|
||||
p.get("갈래") == "기초" and p.get("키") == "GC000207"
|
||||
for row in book.values()
|
||||
for p in row.get("짝") or []
|
||||
)
|
||||
scs = sorted(p.stem for p in (_mine() / "precombo").glob("SC-*.json"))
|
||||
stop = client.post(f"{API}/hopyo/{KEY}/move", json=body) # 이미 옮긴 것 = 덮음 없이 409
|
||||
assert stop.status_code == 409 and [x["번"] for x in stop.json()["detail"]["호표"]] == [1, 2]
|
||||
assert sorted(p.stem for p in (_mine() / "precombo").glob("SC-*.json")) == scs
|
||||
again = client.post(f"{API}/hopyo/{KEY}/move", json={**body, "덮음": True})
|
||||
assert again.status_code == 200, again.text
|
||||
again = again.json()
|
||||
assert [x["키"] for x in again["만듦"]] == [x["키"] for x in out["만듦"]]
|
||||
assert sorted(p.stem for p in (_mine() / "precombo").glob("SC-*.json")) == scs
|
||||
assert hbuild.confirmed(_mine())[hbuild.mark(KEY, 2)] == out["만듦"][1]["키"]
|
||||
assert client.delete(f"{API}/hopyo/{KEY}").status_code == 200 # [끝내기] = 저장본도 지움
|
||||
assert not list((_mine() / "실무자료" / "_derived").glob(f"*{KEY}*"))
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
def test_막는_길(client):
|
||||
assert _post(client).status_code == 200
|
||||
pick = {"갈래": "기초", "키": "GC001140", "입력값": {}, "금액": 1}
|
||||
bad = client.post(
|
||||
f"{API}/hopyo/{KEY}/move", json={"층": "personal", "호표": {"2": {"0": [pick]}}}
|
||||
)
|
||||
assert bad.status_code == 422 # 금액 · 품 칸 = 서버가 셈
|
||||
none = client.post(f"{API}/hopyo/{KEY}/move", json={"층": "personal", "호표": {"99": {}}})
|
||||
assert none.status_code == 404
|
||||
wrong = {"갈래": "인력", "키": "LB999999"}
|
||||
res = client.post(
|
||||
f"{API}/hopyo/{KEY}/move", json={"층": "personal", "호표": {"2": {"0": [wrong]}}}
|
||||
)
|
||||
assert res.status_code == 400
|
||||
@@ -77,7 +77,6 @@ def test_결과_단가산출_확인한_블록만_단위_SK_겹침_없음(client)
|
||||
folder = layers.personal_dir(7, 42)
|
||||
assert _post(client).json()["키"] == "HP-000001"
|
||||
url = URL.format("personal") + "/HP-000001/sangeun"
|
||||
assert client.post(url).status_code == 409 # 729-1 — 맞춤 셈 전
|
||||
fit.run_job(folder, "HP-000001")
|
||||
got = client.post(url)
|
||||
assert got.status_code == 200, got.text
|
||||
|
||||
@@ -51,7 +51,6 @@ def test_실무_단가산출_확인한_블록만_단위_SK_일위대가_막음(c
|
||||
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={}).json()
|
||||
assert got["만듦"] == [] and not _sk_docs(folder) # 확인 없음 = 판정만
|
||||
@@ -64,7 +63,7 @@ def test_실무_단가산출_확인한_블록만_단위_SK_일위대가_막음(c
|
||||
for no, head in SAMPLES.items():
|
||||
doc = docs[no]
|
||||
assert doc["이름"].startswith(head) and doc["바탕"] is None, no
|
||||
assert doc["본뜬"]["갈래"] == skunit.UNIT and doc["본뜬"]["근거"] == rows[no]["근거"]
|
||||
assert doc["본뜬"]["갈래"] == skunit.UNIT and doc["본뜬"].get("근거") == rows[no]["근거"]
|
||||
mine = [x["수량"] for x in doc["더한줄"] if x.get("종류") == "인력"] # 엑셀 품 그대로
|
||||
excel = [r["매개값"]["품"] for r in rows[no]["줄"] if r["묶음"] == "L"]
|
||||
assert mine == excel, no
|
||||
@@ -165,7 +164,7 @@ def test_노임_이름_이음_기계_EQ_이음_사용자_이음(client):
|
||||
line = {"갈래": "자재", "이름": "없는자재", "규격": "", "단위": "개", "수량": 2,
|
||||
"단가": {"재료비": 10}, "차례": 0} # fmt: skip
|
||||
one = hfix.line_of(line, [], {"파일": "x", "호표": 1}, set(), linker, {"노임": "LB000002"})
|
||||
assert one["고정"]["이음"] == {"노임": "LB000002", "자재": None, "기계": None}
|
||||
assert one["고정"]["이음"] == {"노임": "LB000002", "자재": None, "기계": None, "유가": None}
|
||||
assert hfix.linked(one) and not hfix.linked(hfix.line_of(line, [], {}, set(), linker))
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from config import config_system
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hx
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Ours as ours
|
||||
from M02_MasterTemplete import M02_Hopyo_Router as hopyo_router
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hs
|
||||
from M02_MasterTemplete import M02_Labor_Router as labor_router
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
@@ -133,6 +134,9 @@ def calc_cache(tmp_path, monkeypatch) -> Path:
|
||||
shutil.copy(ours.path_of(), cache / ours.path_of().name)
|
||||
monkeypatch.setenv("AISLO_HOPYO_CACHE", str(cache))
|
||||
monkeypatch.setattr(fit, "ensure", fit.status)
|
||||
monkeypatch.setattr(
|
||||
hopyo_router.hopyo_judge, "_start", lambda *_: None
|
||||
) # 블록 풀이 뒤 셈 안 돌림
|
||||
return cache
|
||||
|
||||
|
||||
@@ -148,6 +152,7 @@ def client(tmp_path, monkeypatch):
|
||||
calc_cache(tmp_path, monkeypatch)
|
||||
app = FastAPI()
|
||||
app.include_router(labor_router.router)
|
||||
app.include_router(hopyo_router.router) # 785-5 hopyo 길 옮김
|
||||
who = dict(USER)
|
||||
app.dependency_overrides[verify_session] = lambda: who
|
||||
got = TestClient(app)
|
||||
@@ -194,7 +199,7 @@ def test_엉터리_시트없음_큰_파일_400(client, monkeypatch):
|
||||
assert bad.status_code == 400 and "읽지 못함" in bad.json()["detail"]
|
||||
none = _post(client, data=NO_SHEET.read_bytes(), name=NO_SHEET.name)
|
||||
assert none.status_code == 400 and none.json()["detail"] == "일위대가 시트 없음"
|
||||
monkeypatch.setattr(labor_router, "HOPYO_MAX_BYTES", 1000)
|
||||
monkeypatch.setattr(hopyo_router, "HOPYO_MAX_BYTES", 1000)
|
||||
big = _post(client)
|
||||
assert big.status_code == 400 and "20 MB" in big.json()["detail"]
|
||||
assert client.get(URL.format("personal")).json()["줄"] == []
|
||||
|
||||
Reference in New Issue
Block a user