# -*- coding: utf-8 -*- """공사(내역서) 삭제·단계 등록(차수) 끼워 넣기 API — 공사목록표(wPrjList) 「내역서 삭제」(Act_Del_PrjExecute · ST_U_DelPrj Delete_Prj). 근거: REA 09 resource-TWPRJLIST 메뉴 「내역서 삭제」 · old/30_원자료 module_exports(ST_U_DelPrj: Delete_Prj) · 02번(PRJLST 한 행 = 공사 · PCODE '1n' ↔ BOQ1n.DBF · PRJ_MEMO = 그 공사 원가 설정) 규칙: 고른 PCODE 의 PRJLST 행과 BOQ1n.DBF 를 뺌 · 다른 공사 행·표·메모는 바이트 그대로 · 마지막 한 공사는 지우지 않음 (원본 확인 문구·선택지는 근거 없음 — 화면은 확인 창 한 번) 단계 등록(NewProject → MkBoq · REA 31 Main_K_ORD_registration_transition_matrix · 최종보고서 §4.1): 원본 PCLASS × 등록(6 기성 · 7 설계변경) × 플래그(0 차수별 · 1 당초/변경) → 새 K_ORD(1 또는 K+1) · 상한 없음 새 PCLASS = 등록 번호(4 계약 · 6 기성 · 7 설계변경 갈래와 같은 수 — 추정) K+1 = 다음 차수 칸 열기(stc_project change) · 1 = 원본 마지막 차수 칸을 1·2 칸(당초·변경)으로 옮겨 새로 시작 """ from __future__ import annotations import pathlib from dbf_rw import DbfTable from fastapi import APIRouter, HTTPException from pydantic import BaseModel from stc_project import SLOT_COL, copy_register, safe_name, write_output from stc_read import Source from stc_save import SaveLog, recompute_boq router = APIRouter() def _root() -> pathlib.Path: import server # 실행 중 바뀔 수 있는 작업 폴더(시험은 임시 폴더로 바꿈) return server.WORK_DBF_ROOT def delete_project(files: dict, pcode: str) -> tuple[dict, list]: """(새 files, 바뀐 목록) — PRJLST 행 · BOQ{pcode}.DBF 뺌.""" pcode = str(pcode).strip() if "PRJLST.DBF" not in files: raise ValueError("PRJLST.DBF 없음") prj = DbfTable(files["PRJLST.DBF"]) idx = [i for i in range(len(prj)) if prj.get(i, "PCODE") == pcode] if not idx: raise ValueError(f"PRJLST 에 PCODE {pcode} 공사 없음") if len(prj) - len(idx) < 1: raise ValueError("마지막 공사는 지울 수 없음") log = [ {"table": "PRJLST.DBF", "recno": i, "field": "_delete", "old": pcode, "new": ""} for i in idx ] prj.recs = [r for i, r in enumerate(prj.recs) if i not in set(idx)] prj.dirty = True out = dict(files) out["PRJLST.DBF"] = prj.to_bytes() tbl = f"BOQ{pcode}.DBF" if out.pop(tbl, None) is not None: log.append({"table": tbl, "recno": -1, "field": "_drop", "old": tbl, "new": ""}) return out, log class DelPrjReq(BaseModel): source_path: str pcode: str target_folder_name: str = "" @router.post("/api/ui/project-delete") def api_project_delete(req: DelPrjReq): try: s = Source(req.source_path) files, log = delete_project(s.files, req.pcode) except (FileNotFoundError, ValueError) as e: raise HTTPException(400, str(e)) target = req.target_folder_name or s.path.stem names = {k: v for k, v in s.names.items() if k in files} res = write_output(_root(), safe_name(target), files, names, s.entries) tbl = f"BOQ{str(req.pcode).strip()}.DBF" left = pathlib.Path(res["saved_dir"]) / s.names.get(tbl, tbl) # 같은 작업본 폴더에 남은 옛 표 if tbl not in files and left.exists(): left.unlink() return {**res, "deleted": req.pcode, "changes": log} # 원본 PCLASS × 등록 × 플래그 → 새 K_ORD("1" | "K+1") — 표에 없는 원본(설계 0 등)은 다음 차수(K+1) K_ORD_RULE = { (4, 6, None): "1", (6, 6, None): "K+1", (7, 6, None): "1", (4, 7, None): "1", (6, 7, None): "1", (7, 7, 0): "K+1", (7, 7, 1): "1", } def new_k_ord(pclass: int, op: int, flag: int, k: int) -> int: rule = K_ORD_RULE.get((pclass, op, flag)) or K_ORD_RULE.get((pclass, op, None)) or "K+1" return k + 1 if rule == "K+1" else 1 def _move_slot(tbl: DbfTable, src: str, dst: tuple[str, ...]) -> None: pairs = [] for c in tbl.order: m = SLOT_COL.match(c) if m and m.group(2) == src: pairs += [(c, f"{m.group(1)}{d}") for d in dst if f"{m.group(1)}{d}" in tbl.fields] for i in range(len(tbl)): for a, b in pairs: tbl.set_raw(i, b, tbl.raw(i, a)) def stage_register(files: dict, src_pcode: str, op: int, flag: int, pname: str = ""): """(새 files, 새 PCODE, 새 K_ORD) — 단계 등록.""" prj = DbfTable(files["PRJLST.DBF"]) si = next(i for i in range(len(prj)) if prj.get(i, "PCODE") == str(src_pcode)) num = lambda f: int(float(prj.get(si, f) or 0)) if f in prj.fields else 0 # noqa: E731 k, pclass = num("K_ORD"), num("PCLASS") nk = new_k_ord(pclass, int(op), int(flag), k) out, pcode, _log = copy_register( files, str(src_pcode), "change" if nk == k + 1 else "copy", pname ) prj2 = DbfTable(out["PRJLST.DBF"]) ni = next(i for i in range(len(prj2)) if prj2.get(i, "PCODE") == pcode) prj2.set(ni, "K_ORD", nk) if "PCLASS" in prj2.fields: prj2.set(ni, "PCLASS", int(op)) if nk == 1 and k + 1 != 1: # 원본 마지막 차수(k+1) 칸 → 새 공사 당초·변경(1·2) 칸 name = f"BOQ{pcode}.DBF" bt = DbfTable(out[name]) _move_slot(bt, str(k + 1), ("1", "2")) direct = recompute_boq(bt, name, {"1", "2"}, SaveLog()) for s, vals in direct.items(): for c, v in zip(("MCOST", "LCOST", "SCOST", "TCOST"), vals): if f"{c}{s}" in prj2.fields: prj2.set(ni, f"{c}{s}", v) out[name] = bt.to_bytes() out["PRJLST.DBF"] = prj2.to_bytes() return out, pcode, nk class StageRegReq(BaseModel): source_path: str src_pcode: str = "11" op: int = 7 # 6 기성 · 7 설계변경 flag: int = 0 # 0 차수별 · 1 당초/변경 pname: str = "" target_folder_name: str = "" @router.post("/api/ui/stage-register") def api_stage_register(req: StageRegReq): try: s = Source(req.source_path) files, pcode, nk = stage_register(s.files, req.src_pcode, req.op, req.flag, req.pname) except (FileNotFoundError, ValueError, StopIteration) as e: raise HTTPException(400, str(e) or "공사 없음") target = req.target_folder_name or f"{s.path.stem}_등록{pcode}" names = dict(s.names, **{f"BOQ{pcode}.DBF": f"BOQ{pcode}.DBF"}) res = write_output(_root(), safe_name(target), files, names, s.entries) return {**res, "new_pcode": pcode, "k_ord": nk}