feat(B09 불러오기): 경쟁사 공사 통째 불러오기 — 창 한 번 · 일위대가 이 공사 서고 사본 (707-7)
- 불러오기 창 = 불러올 것 체크(일위대가 · 단가산출 / 내역 줄 / 원가계산서 요율 / 단가표 · 기본 다 켬 · .estx · .STC 일위대가 · 요율 흐림 + 까닭) - [불러오기] 한 번 = 서버 차례 — 일위대가(705-3 고정 줄 · 하위 먼저 · 임시 폴더에서 짓고 서고 사본 precombo@project · 층 쓰기 0) → 내역 줄 → 요율 → 단가표 → 셈 - 내역 단가 ≠ 일위대가표 단가 줄 = 잇지 않고 내역 단가 그대로(경쟁사 금액 재현 · 목록) - 끝 알림 한 장 — 호표 · 내역 줄 · 직접비 · 총공사비 엑셀 = 우리 · 어긋난 줄 · 다음 걸음 · 줄 어긋남 없으면 알림 한 줄 - 내 자료집에 저장(703-2) = 불러온 하위 호표도 지금 층에 같이 씀(한 번만) - 시험 test_707_7 10 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,16 @@
|
||||
(file_key = 표지 · 같은 파일 다시 씀) → {file_key, 파일, 꼴, 시트, 묶음, 항목, 합계,
|
||||
조달수수료율, 합계줄, 율줄, 같은이름, 남는자동, 이음} ·
|
||||
user = 화면 캐시(JSON 글 · 없으면 저장 값) · DRM = 400 「읽을 수 없음」
|
||||
POST /api/projects/{id}/estimation/import/bill/apply {file_key, mode, attach, drop, user?}
|
||||
짜기(`B09_Estimation_Bill_Import.apply`) →
|
||||
{user(새 캐시), 알림, 어긋남, 엑셀합계, payload(그 캐시로 셈)} ·
|
||||
이음 = 읽을 수 있는 층(개인 → 회사 → 시스템)에서 같은 파일 이름으로 들여온 실무자료 사전조합
|
||||
(본뜬 「HP-… 제N호표」) → 서고 사본(층 쓰기 0 · 프로젝트 사본만) · 대가 N 줄 연결.
|
||||
POST /api/projects/{id}/estimation/import/bill/apply
|
||||
{file_key, mode, attach, drop, hopyo, rates, prices, user?}
|
||||
통째 불러오기(707-7) 차례 — 일위대가 · 단가산출(hopyo · 엑셀만 ·
|
||||
`B09_Estimation_Bill_Hopyo` = 이 공사 서고 사본 · 층 쓰기 0) →
|
||||
내역 줄 짜기(`B09_Estimation_Bill_Import.apply`) →
|
||||
요율(rates · 707-2) → 단가표(prices · 707-3) → 그 캐시로 셈 →
|
||||
{user(새 캐시), 알림, 어긋남, 엑셀합계, 단계 {일위대가, 요율, 단가표}(수 · 못 읽음 = 까닭),
|
||||
합계 {엑셀직접, 우리직접, 엑셀총, 우리총}, payload} ·
|
||||
hopyo 거짓 = 이음은 읽을 수 있는 층(개인 → 회사 → 시스템)에서 같은 파일 이름으로 들여온
|
||||
실무자료 사전조합(본뜬 「HP-… 제N호표」) → 서고 사본 · 대가 N 줄 연결.
|
||||
POST /api/projects/{id}/estimation/import/rates (multipart: file | file_key · user?)
|
||||
원가계산서 요율(707-2) → {file_key, name, kind, rows, rates, notes, check, patch{estimate}}
|
||||
POST /api/projects/{id}/estimation/import/prices (multipart: file | file_key · user?)
|
||||
@@ -39,6 +44,7 @@ from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from B08_Quantity.B08_Quantity_Router_Hydro import _project_root
|
||||
from B09_Estimation import B09_Estimation_Bill_Hopyo as bill_hopyo
|
||||
from B09_Estimation import B09_Estimation_Bill_Import as importer
|
||||
from B09_Estimation import B09_Estimation_Bill_Library as library
|
||||
from B09_Estimation import B09_Estimation_Estx as estx
|
||||
@@ -147,6 +153,11 @@ def _links(root: Path, found: dict[int, tuple], who: Any) -> dict[int, dict[str,
|
||||
folders[no] = library.import_copy(root, "precombo", layer, owner, key, who)["폴더"]
|
||||
except (LookupError, ValueError): # 원본 없음 · 모양 틀림 = 고정 단가로
|
||||
continue
|
||||
return _linked(root, folders)
|
||||
|
||||
|
||||
def _linked(root: Path, folders: dict[int, str]) -> dict[int, dict[str, Any]]:
|
||||
"""대가 번 → 서고 사본 폴더 → 줄 연결 {tab dg, key 블록 id, lib 폴더}."""
|
||||
libs = (daega_inputs(root).get("libs") or {}) if folders else {}
|
||||
return {
|
||||
no: {"tab": "dg", "key": libs[name], "lib": name}
|
||||
@@ -155,6 +166,26 @@ def _links(root: Path, found: dict[int, tuple], who: Any) -> dict[int, dict[str,
|
||||
}
|
||||
|
||||
|
||||
def _own_links(
|
||||
root: Path, name: str, data: bytes, parsed: dict[str, Any], tag: str, who: Any
|
||||
) -> tuple[dict[int, dict[str, Any]], dict[str, Any]]:
|
||||
"""707-7 — 파일 일위대가 · 단가산출 → 이 공사 서고 사본(층 쓰기 0) →
|
||||
(줄 연결, 단계 수 · 까닭)."""
|
||||
if _raw(name):
|
||||
return {}, {"까닭": f"{Path(name).suffix} 구성 줄은 못 읽음 — 같은 공사 엑셀에서"}
|
||||
try:
|
||||
book = bill_hopyo.read(name, data)
|
||||
except ExcelError as error:
|
||||
return {}, {"까닭": str(error)}
|
||||
built = bill_hopyo.build(book, name, tag, who)
|
||||
numbers, other = bill_hopyo.agree(parsed, book)
|
||||
folders = {
|
||||
no: got for no in sorted(numbers) if (got := bill_hopyo.copy(root, built, tag, no, who))
|
||||
}
|
||||
links = _linked(root, folders)
|
||||
return links, {**built["수"], "사본": len(links), "실패": built["실패"], "단가다름": other}
|
||||
|
||||
|
||||
def _numbers(parsed: dict[str, Any]) -> set[int]:
|
||||
return {
|
||||
int(h["번"])
|
||||
@@ -204,6 +235,9 @@ class ApplyBody(BaseModel):
|
||||
mode: str = "take"
|
||||
attach: bool = True
|
||||
drop: bool = False
|
||||
hopyo: bool = False
|
||||
rates: bool = False
|
||||
prices: bool = False
|
||||
user: dict[str, Any] | None = None
|
||||
|
||||
|
||||
@@ -217,22 +251,52 @@ async def apply_bill(
|
||||
raise HTTPException(status_code=400, detail="수량 갈래는 take · price 중 하나")
|
||||
root = await _project_root(project_id)
|
||||
path = _saved(root, body.file_key)
|
||||
parsed = await asyncio.to_thread(_parse, path.name, path.read_bytes())
|
||||
data = path.read_bytes()
|
||||
parsed = await asyncio.to_thread(_parse, path.name, data)
|
||||
draft = _draft(body.user) or await asyncio.to_thread(store.read_user, root)
|
||||
rows = (await payload(project_id, root, draft, True))["sheets"]["bill"]["rows"]
|
||||
found = await asyncio.to_thread(_found, session, path.name, _numbers(parsed))
|
||||
links = await asyncio.to_thread(_links, root, found, who_of(session))
|
||||
steps: dict[str, Any] = {}
|
||||
if body.hopyo:
|
||||
args = (root, path.name, data, parsed, body.file_key, who_of(session))
|
||||
links, steps["일위대가"] = await asyncio.to_thread(_own_links, *args)
|
||||
else:
|
||||
found = await asyncio.to_thread(_found, session, path.name, _numbers(parsed))
|
||||
links = await asyncio.to_thread(_links, root, found, who_of(session))
|
||||
made, report = importer.apply(
|
||||
parsed, draft, rows, body.file_key, body.mode, body.attach, body.drop, links
|
||||
)
|
||||
made = store.clean_user(made)
|
||||
excel_grand = None
|
||||
if body.rates:
|
||||
try:
|
||||
got = await asyncio.to_thread(_rate_patch, path.name, data, made["estimate"])
|
||||
made = store.clean_user({**made, **got["patch"]})
|
||||
excel_grand = got["check"]["excel_grand"]
|
||||
steps["요율"] = {"줄": len(got["rates"]), "다름": got["check"]["differ"]}
|
||||
except HTTPException as error:
|
||||
steps["요율"] = {"까닭": error.detail}
|
||||
if body.prices:
|
||||
try:
|
||||
got = await _price_patch(project_id, root, path.name, data, made)
|
||||
made = store.clean_user({**made, **got["patch"]})
|
||||
steps["단가표"] = {k: got["counts"].get(k) for k in import_prices.KINDS}
|
||||
except HTTPException as error:
|
||||
steps["단가표"] = {"까닭": error.detail}
|
||||
out = await payload(project_id, root, made, True)
|
||||
lines = out["sheets"]["bill"]["rows"]
|
||||
total = out["sheets"].get("total") or {}
|
||||
return {
|
||||
"user": made,
|
||||
"알림": {k: v for k, v in report.items() if k != "줄"},
|
||||
"어긋남": importer.differences(parsed, report, lines),
|
||||
"엑셀합계": parsed["합계"],
|
||||
"단계": steps,
|
||||
"합계": {
|
||||
"엑셀직접": parsed["합계"].get("직접"),
|
||||
"우리직접": (total.get("sums") or {}).get("direct"),
|
||||
"엑셀총": excel_grand,
|
||||
"우리총": (total.get("subtotals") or {}).get("grand"),
|
||||
},
|
||||
"payload": out,
|
||||
}
|
||||
|
||||
@@ -264,21 +328,28 @@ async def import_rate_sheet(
|
||||
key, name, data = await _file(file, file_key, root)
|
||||
draft = _draft(user)
|
||||
if _raw(name):
|
||||
raise HTTPException(
|
||||
400, f"원가계산서 요율은 {Path(name).suffix} 에서 못 읽음 — 같은 공사 엑셀에서 불러옴"
|
||||
)
|
||||
raise HTTPException(400, _RAW_RATES.format(Path(name).suffix))
|
||||
mine = (draft if draft is not None else await asyncio.to_thread(store.read_user, root))[
|
||||
"estimate"
|
||||
]
|
||||
return {"file_key": key, "name": name, **await asyncio.to_thread(_rate_patch, name, data, mine)}
|
||||
|
||||
|
||||
_RAW_RATES = "원가계산서 요율은 {} 에서 못 읽음 — 같은 공사 엑셀에서 불러옴"
|
||||
|
||||
|
||||
def _rate_patch(name: str, data: bytes, mine: dict[str, Any]) -> dict[str, Any]:
|
||||
"""요율(707-2) → {kind, sheet, rows, rates, notes, check, patch{estimate}} · 못 읽음 = 400."""
|
||||
if _raw(name):
|
||||
raise HTTPException(400, _RAW_RATES.format(Path(name).suffix))
|
||||
try:
|
||||
read = await asyncio.to_thread(import_rates.read_rates, name, data)
|
||||
read = import_rates.read_rates(name, data)
|
||||
except ExcelError as error:
|
||||
raise HTTPException(400, str(error)) from error
|
||||
check = await asyncio.to_thread(import_rates.check, read, mine, rate_table())
|
||||
check = import_rates.check(read, mine, rate_table())
|
||||
estimate = {**mine, **read["estimate"], "rates": {**(mine.get("rates") or {}), **read["rates"]}}
|
||||
keep = ("kind", "sheet", "rows", "rates", "notes")
|
||||
return {"file_key": key, "name": name, **{k: read[k] for k in keep}, "check": check,
|
||||
"patch": {"estimate": estimate}} # fmt: skip
|
||||
return {**{k: read[k] for k in keep}, "check": check, "patch": {"estimate": estimate}}
|
||||
|
||||
|
||||
@router.post(ROOT + "/prices")
|
||||
@@ -288,12 +359,19 @@ async def import_price_sheets(
|
||||
file_key: str | None = Form(None),
|
||||
user: str | None = Form(None),
|
||||
) -> dict[str, Any]:
|
||||
from B09_Estimation import B09_Estimation_Router as main # 서로 부름 — 부를 때만
|
||||
|
||||
root = await _project_root(project_id)
|
||||
key, name, data = await _file(file, file_key, root)
|
||||
draft = _draft(user)
|
||||
mine = draft if draft is not None else await asyncio.to_thread(store.read_user, root)
|
||||
return {"file_key": key, "name": name, **await _price_patch(project_id, root, name, data, mine)}
|
||||
|
||||
|
||||
async def _price_patch(
|
||||
project_id: UUID, root: Path, name: str, data: bytes, mine: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""단가표(707-3) → {sheets, counts, linked, missing, patch} · 못 읽음 = 400."""
|
||||
from B09_Estimation import B09_Estimation_Router as main # 서로 부름 — 부를 때만
|
||||
|
||||
try:
|
||||
reader = raw.read_prices if (raw := _raw(name)) else import_prices.read_prices
|
||||
read = await asyncio.to_thread(reader, name, data)
|
||||
@@ -305,5 +383,4 @@ async def import_price_sheets(
|
||||
inputs = await asyncio.to_thread(prices.build_inputs, book, keys, b08["materials"])
|
||||
base = compute_base(inputs, {}) # 덮개 뺀 이 공사 단가 책
|
||||
out = import_prices.propose(read, base, mine)
|
||||
sheets = {k: read[f"{k}_sheet"] for k in import_prices.KINDS}
|
||||
return {"file_key": key, "name": name, "sheets": sheets, **out}
|
||||
return {"sheets": {k: read[f"{k}_sheet"] for k in import_prices.KINDS}, **out}
|
||||
|
||||
Reference in New Issue
Block a user