Merge remote-tracking branch 'origin/sub_laptop_6' into sub_laptop_8
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
[확인하고 사전조합 만들기] 몸 = 사용자가 체크한 줄만 —
|
||||
`줄 [{차례, 대상 [{로직|자재|호표, 입력값, 배, 몫}] | 뺌}]` · 직접 줄 호표 = `통째 {대상}`.
|
||||
몸이 비면 400(확인 없이 들어가는 길 없음).
|
||||
몸이 비면 = 모두 고정(705-3 · [모두 고정으로 들여오기]).
|
||||
서버 재검사(브라우저 값을 받아 적지 않음 · 금액 · 품 칸 없음) — 차례 = 그 호표 줄 · 대상 하나 =
|
||||
돈 로직 · M01 자재품목 · 확인된 호표 중 하나 · 입력 이름 = 그 로직 입력 · 배 0 넘음 · 몫 (0, 1] ·
|
||||
호표 = 만들 층에 확인된 사전조합(없으면 409 「제N호표 먼저 확인」).
|
||||
@@ -20,19 +20,24 @@
|
||||
지역은 프로젝트에서) · 장부엔 안 남김.
|
||||
같은 본뜬 사전조합이 있으면 확인한 내용으로 덮음(`다시씀`) · 확인한 줄은 별칭 장부에 남음
|
||||
(`M02_Hopyo_Ledger` · 호표 참조 줄 뺌).
|
||||
빠진 줄(확인 안 함 · 뺌 · %) = 엑셀 금액과 함께 보고 · 만든 값 ↔ 엑셀 호표 단가(참고).
|
||||
빠짐 0(705-3) — 확인 안 한 줄 · 노임 · % · 「우리에 없음」 줄 = 고정 줄(`M02_Hopyo_Fixed` · 엑셀 숫자
|
||||
그대로) · 확인 안 한 호표 줄 = 확인된 하위 사전조합 참조(단위 같음 · 아니면 고정) · 빠진 줄 = 「안 씀」 ·
|
||||
통째 대신 · 로직 자재 겹침 · 전국만. `본뜬.품셈` = 품셈으로 넣은 엑셀 줄 차례([품셈으로 바꾸기] 첫 체크) ·
|
||||
`줄수 {품셈, 고정, 호표, 빠짐, 이음, 이음후보}` · 만든 값 ↔ 엑셀 호표 단가(`비교` · 같음 = 비목마다 1 원 안).
|
||||
엔진 · 일위대가 계산은 그대로(`M02_Labor_Ref.Expand`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from decimal import Decimal
|
||||
from decimal import ROUND_FLOOR, ROUND_HALF_UP, Decimal
|
||||
from pathlib import Path
|
||||
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_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
@@ -177,8 +182,8 @@ class Builder:
|
||||
"""몸 재검사 → ({차례: 확인 줄}, 통째 대상) — 틀리면 400 · 하위 호표 409."""
|
||||
if whole is not None and lines:
|
||||
raise _bad("통째 · 줄 중 하나만")
|
||||
if not lines and not (whole or {}).get("대상"):
|
||||
raise _bad("확인한 줄이 없음")
|
||||
if whole is not None and not whole.get("대상"):
|
||||
raise _bad("통째 대상이 없음")
|
||||
if len(lines) > LINES_MAX:
|
||||
raise _bad(f"줄은 {LINES_MAX} 개 안")
|
||||
have = self.hopyo.get("줄") or []
|
||||
@@ -221,6 +226,9 @@ class Builder:
|
||||
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(), [], set(), 0
|
||||
where = {"파일": file, "호표": self.no}
|
||||
rows = hopyo.get("줄") or []
|
||||
|
||||
def put(line: dict[str, Any], one: dict[str, Any], alias: str | None) -> None:
|
||||
qty = Decimal(str(line.get("수량") or 0)) * Decimal(str(one["배"]))
|
||||
@@ -252,6 +260,7 @@ class Builder:
|
||||
mat_lines[one["자재"]] = line
|
||||
return
|
||||
if "호표" in one:
|
||||
kids.add(one["호표"])
|
||||
ref = {
|
||||
"층": refs.here()[0],
|
||||
"주인": refs.here()[1],
|
||||
@@ -282,18 +291,23 @@ class Builder:
|
||||
for one in whole:
|
||||
put({"수량": 1, "단위": hopyo.get("단위"), "이름": hopyo.get("이름")}, one, None)
|
||||
book[ledger.hopyo_key(hopyo)] = self._row(hopyo, "짝", whole)
|
||||
for at, line in enumerate(hopyo.get("줄") or []):
|
||||
for at, line in enumerate(rows):
|
||||
got = checked.get(at)
|
||||
if line.get("갈래") == "%":
|
||||
missing.append(_missing(line, PERCENT))
|
||||
if whole:
|
||||
missing.append(_missing(line, WHOLE))
|
||||
continue
|
||||
if whole or got is None:
|
||||
missing.append(_missing(line, WHOLE if whole else UNCHECKED))
|
||||
continue
|
||||
if got.get("뺌"):
|
||||
missing.append(_missing(line, f"뺌 — {got['뺌']}"))
|
||||
if got and got.get("뺌"):
|
||||
book[ledger.key(line)] = self._row(line, got["뺌"], [])
|
||||
if got and got.get("뺌") == "안 씀":
|
||||
missing.append(_missing(line, f"뺌 — {got['뺌']}"))
|
||||
continue
|
||||
if not got or got.get("뺌"): # 빠짐 0(705-3) — 하위 호표 참조 · 아니면 고정 줄
|
||||
kid = self._kid(line, kids)
|
||||
kid_n += bool(kid)
|
||||
others = [x for x in rows if x is not line]
|
||||
lines.append(kid or hfix.line_of(line, others, where, taken, linker))
|
||||
continue
|
||||
poomsem.append(at)
|
||||
alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None
|
||||
for one in got["대상"]:
|
||||
put(line, one, alias)
|
||||
@@ -314,9 +328,32 @@ class Builder:
|
||||
}
|
||||
if nation:
|
||||
doc["본뜬"]["전국자재"] = list(nation.values())
|
||||
doc["본뜬"]["품셈"] = poomsem
|
||||
ref, again = _save(doc)
|
||||
wrote = ledger.write(self.folder, book, self.who, file or None)
|
||||
return {"참조": ref, "다시씀": again, "빠진줄": missing, "장부": {"씀": wrote}}
|
||||
fixed_n = sum(1 for x in lines if "고정" in x)
|
||||
links = sum(1 for x in lines if any(((x.get("고정") or {}).get("이음") or {}).values()))
|
||||
count = {"품셈": len(poomsem), "고정": fixed_n, "호표": kid_n}
|
||||
count |= {"빠짐": sum(1 for x in missing if x["까닭"] != TWICE), "이음": links}
|
||||
out = {"참조": ref, "다시씀": again, "빠진줄": missing, "장부": {"씀": wrote}}
|
||||
return {**out, "줄수": {**count, "이음후보": linker.near}}
|
||||
|
||||
def _kid(self, line: dict[str, Any], kids: set[int]) -> dict[str, Any] | None:
|
||||
"""확인 안 한 호표 줄 → 확인된 하위 사전조합 참조(단위 같음 · 같은 하위 한 번) · 아니면 None."""
|
||||
ref = line.get("참조") or {}
|
||||
if finger.kind_of(line) != "호표" and ref.get("종류") != "일위":
|
||||
return None
|
||||
no = int(ref.get("번") or 0)
|
||||
made = self.done.get(mark(self.key, no))
|
||||
unit = next((h.get("단위") for h in self.doc["호표"] if int(h["번"]) == no), None)
|
||||
times = hopyo_match.factor(line.get("단위"), unit, line.get("규격"))["배"]
|
||||
if not made or no in kids or no == self.no or times != 1:
|
||||
return None
|
||||
kids.add(no)
|
||||
qty = Decimal(str(line.get("수량") or 0))
|
||||
head = {"층": refs.here()[0], "주인": refs.here()[1], "키": made}
|
||||
return {"참조": head, "별칭": str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None,
|
||||
"메모": f"실무 {qty}{line.get('단위') or ''} · 제{no}호표", "수량": {"손": _num(qty)}} # fmt: skip
|
||||
|
||||
def _inside(self, lines: list[dict[str, Any]], mats: dict[str, dict[str, Any]]) -> list[str]:
|
||||
"""로직 자재와 같은 품목(Labor_Doc `no_twice` 400 막음) — 사용자 자재에서 빼고
|
||||
@@ -346,14 +383,43 @@ class Builder:
|
||||
}
|
||||
|
||||
|
||||
# 엑셀 끝수 꼴(705-3) — (줄 금액 0.1 · 합 원 자름, 계 = 줄 계 합에서) · EST = 비목 합 내림 ·
|
||||
# STmate = 줄 반올림 · 계 = 줄 계 합 내림(비목 머리 값과 따로)
|
||||
CUTS = ((ROUND_FLOOR, False), (ROUND_HALF_UP, True), (ROUND_HALF_UP, False))
|
||||
|
||||
|
||||
def _excel_round(got: dict[str, Any], how: str, by_line: bool) -> Decimal | None:
|
||||
"""엑셀 끝수(705-3) — 줄 금액 비목마다 0.1(`how`) · 하위 호표 단가 = 원 · 비율 줄 = 기준 × 율 ÷ 100 ·
|
||||
합 = 비목마다 원(`how`) 더함 · `by_line` = 줄 계 합 원 내림 · 못 셈 줄 있으면 None."""
|
||||
parts = dict.fromkeys(labor.COSTS, Decimal(0))
|
||||
for one in got.get("안") or []:
|
||||
if one.get("기준") is None:
|
||||
return None
|
||||
cut = 100 if (one.get("고정") or {}).get("비율") else 1
|
||||
for k in labor.COSTS:
|
||||
unit = Decimal(one["기준"][k])
|
||||
unit = unit.to_integral_value(how) if "참조" in one else unit
|
||||
parts[k] += (unit * one["수량"] / cut).quantize(Decimal("0.1"), how)
|
||||
if by_line:
|
||||
return sum(parts.values()).to_integral_value(ROUND_FLOOR)
|
||||
return sum(v.to_integral_value(how) for v in parts.values())
|
||||
|
||||
|
||||
def compare(ref: dict[str, Any], hopyo: dict[str, Any]) -> dict[str, Any]:
|
||||
"""만든 사전조합 1 단위 값 ↔ 엑셀 호표 단가(참고 · 판정 아님)."""
|
||||
"""만든 사전조합 1 단위 값 ↔ 엑셀 호표 단가(참고 · 판정 아님) — 우리 = 엑셀 끝수 규칙(내림 ·
|
||||
반올림 중 엑셀과 같은 쪽 · `_excel_round`)."""
|
||||
got = refs.Expand(m01.loaded()[1])({k: ref[k] for k in ("층", "주인", "키")})
|
||||
base = got.get("기준") or {}
|
||||
ours = float(base["계"]) if "계" in base else None
|
||||
excel = (hopyo.get("금액") or {}).get("계")
|
||||
cuts = [_excel_round(got, *one) for one in CUTS] if base else []
|
||||
cuts = [c for c in cuts if c is not None]
|
||||
cut = next(
|
||||
(c for c in cuts if abs(c - Decimal(str(excel or 0))) < 1), cuts[0] if cuts else None
|
||||
)
|
||||
ours = float(cut if cut is not None else base["계"]) if "계" in base else None
|
||||
gap = round(ours - excel, 1) if ours is not None and excel is not None else None
|
||||
return {"엑셀": excel, "우리": ours, "차이": gap, "까닭": got.get("까닭")}
|
||||
same = ours is not None and abs(ours - (excel or 0)) < 1 # 705-3 — 엑셀 금액 없음 = 0
|
||||
return {"엑셀": excel, "우리": ours, "차이": gap, "같음": same, "까닭": got.get("까닭")}
|
||||
|
||||
|
||||
def build(
|
||||
@@ -371,3 +437,61 @@ def build(
|
||||
checked, pairs = maker.check(list(lines or []), whole)
|
||||
made = maker.build(checked, pairs)
|
||||
return {**made, "이름": maker.hopyo.get("이름"), "비교": compare(made["참조"], maker.hopyo)}
|
||||
|
||||
|
||||
def _order(units: dict[int, dict[str, Any]]) -> list[int]:
|
||||
"""하위 호표 먼저(호표 줄 참조) · 고리는 끊음."""
|
||||
out: list[int] = []
|
||||
seen: set[int] = set()
|
||||
|
||||
def visit(no: int) -> None:
|
||||
if no in seen or no not in units:
|
||||
return
|
||||
seen.add(no)
|
||||
for line in units[no].get("줄") or []:
|
||||
ref = line.get("참조") or {}
|
||||
if finger.kind_of(line) == "호표" or ref.get("종류") == "일위":
|
||||
visit(int(ref.get("번") or 0))
|
||||
out.append(no)
|
||||
|
||||
for no in sorted(units):
|
||||
visit(no)
|
||||
return out
|
||||
|
||||
|
||||
def build_all(folder: Path, source: Path, key: str, who: Any = None) -> dict[str, Any]:
|
||||
"""[모두 고정으로 들여오기](705-3) — 시트 호표 모두 · 하위 먼저 · 줄 = 고정(하위 = 참조) ·
|
||||
이미 확인된 호표는 덮지 않음(`그대로`) → `{호표, 줄, 품셈, 고정, 호표참조, 빠짐, 이음, 이음후보,
|
||||
같음, 그대로, 다름 [{번, 이름, 엑셀, 우리, 차이}], 실패 [{번, 까닭}]}`."""
|
||||
units = {int(h["번"]): h for h in hopyo_store.read(source, key).get("호표") or []}
|
||||
sums = dict.fromkeys(("품셈", "고정", "호표", "빠짐", "이음", "이음후보"), 0)
|
||||
out: dict[str, Any] = {
|
||||
"호표": len(units),
|
||||
"줄": 0,
|
||||
"같음": 0,
|
||||
"그대로": 0,
|
||||
"다름": [],
|
||||
"실패": [],
|
||||
}
|
||||
for no in _order(units):
|
||||
out["줄"] += len(units[no].get("줄") or [])
|
||||
try:
|
||||
maker = Builder(folder, source, key, no, who)
|
||||
have = maker.done.get(mark(key, no))
|
||||
if have:
|
||||
out["그대로"] += 1
|
||||
ref = {"층": refs.here()[0], "주인": refs.here()[1], "키": have}
|
||||
else:
|
||||
made = maker.build({}, [])
|
||||
ref = made["참조"]
|
||||
for k in sums:
|
||||
sums[k] += made["줄수"][k]
|
||||
got = compare(ref, maker.hopyo)
|
||||
except store.StoreError as e:
|
||||
out["실패"].append({"번": no, "까닭": str(e.detail)})
|
||||
continue
|
||||
if got["같음"]:
|
||||
out["같음"] += 1
|
||||
else:
|
||||
out["다름"].append({"번": no, "이름": units[no].get("이름"), **got})
|
||||
return {**out, **{("호표참조" if k == "호표" else k): v for k, v in sums.items()}}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"""M02 실무자료 줄 → 고정 줄(PLAN 705-3 · 요구서 1장 · 4장 2 · 3 · 7) — 엑셀 숫자 그대로 · 빠짐 0.
|
||||
|
||||
맞춤 확인 안 한 줄 · 노임 · % · 「우리에 없음」 줄 = 빠진 줄 대신 고정 줄(705-1 그릇 `M02_Labor_Fixed`).
|
||||
숫자는 서버가 실무자료 파일에서 읽음(브라우저 값 안 받음).
|
||||
단가 = 엑셀 단가 노무비 · 재료비 · 경비(빈 칸 0) · 합계만 있는 줄 = 재료비(4장 3) ·
|
||||
수량 없음 = 수량 1 · 단가 = 엑셀 금액(「금액만」).
|
||||
이음 = 이름 · 규격 · 단위 · 단가가 다 같은 노임(LB) · 자재(MT)만 저절로 → 엑셀 합계 = 우리 합계 ·
|
||||
단가가 다른 짝 = `후보` 수만(바꾸기는 705-2 「이 공사 단가로 다시 잇기」).
|
||||
% 줄 = 비율 줄 `비율 {기준, 비목}` — 엑셀 밑수(단가 계) = 다른 줄 금액[기준] 합일 때만 ·
|
||||
아니면(주재료비 등) 수량 1 · 「식」 · 단가 = 엑셀 금액.
|
||||
같은 이름 · 규격 · 단위 둘째부터 = 이름 끝 「 (2)」(줄 가름 · 705-1 400 피함).
|
||||
출처 = `{파일, 호표, 줄}`(줄 = 엑셀 줄 차례 0부터 · [품셈으로 바꾸기] 짝).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Hopyo_Shares as shares
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
_BASE = re.compile(r"(?<!주)(노무비|재료비|경비|인건비)\s*의")
|
||||
GAP = Decimal(1) # 밑수 · 단가 같음 판정 끝수(원)
|
||||
|
||||
|
||||
_d, _plain = fixed._num, fixed._plain
|
||||
|
||||
|
||||
def _parts(money: Any) -> dict[str, Decimal]:
|
||||
"""엑셀 단가 · 금액 → 세 비목(빈 칸 0) · 합계만 있으면 재료비(4장 3)."""
|
||||
money = money if isinstance(money, dict) else {}
|
||||
out = {k: _d(money.get(k)) or Decimal(0) for k in COSTS}
|
||||
total = _d(money.get("계"))
|
||||
if total and not any(_d(money.get(k)) is not None for k in COSTS):
|
||||
out["재료비"] = total
|
||||
return out
|
||||
|
||||
|
||||
def _ratio(line: dict[str, Any], others: list[dict[str, Any]]) -> dict[str, str] | None:
|
||||
"""% 줄 → `{기준, 비목}` — 밑수 = 다른 줄 엑셀 금액[기준] 합(끝수 1 원 안) · 금액 비목 하나."""
|
||||
hit = _BASE.search(f"{line.get('규격') or ''} {line.get('이름') or ''}")
|
||||
under = _d((line.get("단가") or {}).get("계"))
|
||||
paid = [k for k, v in _parts(line.get("금액")).items() if v]
|
||||
if not hit or under is None or len(paid) != 1:
|
||||
return None
|
||||
base = "노무비" if hit.group(1) == "인건비" else hit.group(1)
|
||||
total = sum((_parts(x.get("금액"))[base] for x in others), Decimal(0))
|
||||
return {"기준": base, "비목": paid[0]} if abs(total - under) <= GAP else None
|
||||
|
||||
|
||||
class Linker:
|
||||
"""이음 — 노임(LB 이름 같음) · 자재(이름 · 규격 같은 한 품목 · 단위 같음)가 단가까지 같을 때만."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.whole = m01.loaded()[1]
|
||||
self.got = hm.targets()
|
||||
self.labor: dict[str, list[tuple[str, Decimal | None]]] = {}
|
||||
for row in self.whole.numbers.get("LB", {}).values():
|
||||
pair = (str(row.get("키")), _d(row.get("값")))
|
||||
self.labor.setdefault(str(row.get("이름") or ""), []).append(pair)
|
||||
self.near = 0 # 단가 다른 이음 후보 수
|
||||
|
||||
def link(self, line: dict[str, Any], price: dict[str, Decimal]) -> dict[str, str | None]:
|
||||
out: dict[str, str | None] = {"노임": None, "자재": None, "기계": None}
|
||||
kind = finger.kind_of(line)
|
||||
if kind == "노임":
|
||||
pairs = self.labor.get(shares.trade(line.get("이름")) or "", [])
|
||||
out["노임"] = self._same(pairs, price["노무비"])
|
||||
elif kind in hm.MATERIAL_KINDS and price["재료비"]:
|
||||
out["자재"] = self._same(self._mats(line), price["재료비"])
|
||||
# ponytail: 기계 이음 없음 — 기계 시간 단가는 B09 중기목록표 셈 · 같은 값 판정 못 함
|
||||
return out
|
||||
|
||||
def _same(self, pairs: list[tuple[str, Decimal | None]], want: Decimal) -> str | None:
|
||||
same = [code for code, value in pairs if value is not None and abs(value - want) < GAP]
|
||||
if len(same) == 1:
|
||||
return same[0]
|
||||
self.near += bool(pairs)
|
||||
return None
|
||||
|
||||
def _mats(self, line: dict[str, Any]) -> list[tuple[str, Decimal | None]]:
|
||||
me, _ = hmat.clean(line)
|
||||
found = hm._exact(me, "자재", self.got)
|
||||
if len(found) != 1:
|
||||
found = [r["one"] for r in hmat.sure(me)[0]]
|
||||
out = []
|
||||
for one in found:
|
||||
code = one.get("자재")
|
||||
if not code or hm.factor(line.get("단위"), one.get("단위"))["배"] != 1:
|
||||
continue
|
||||
try:
|
||||
price = fixed.mm.element(self.whole, code, {}, line.get("단위") or None)[0]
|
||||
except fixed.mf.FormulaError:
|
||||
price = None
|
||||
out.append((code, _d(price)))
|
||||
return out
|
||||
|
||||
|
||||
def line_of(
|
||||
line: dict[str, Any],
|
||||
others: list[dict[str, Any]],
|
||||
where: dict[str, Any],
|
||||
taken: set[str],
|
||||
linker: Linker,
|
||||
) -> dict[str, Any]:
|
||||
"""엑셀 줄 하나 → 일위대가 고정 줄 `{고정, 별칭, 메모, 수량}` — `taken` = 이 호표 줄 가름(채워 감)."""
|
||||
qty = _d(line.get("수량"))
|
||||
spec, unit = str(line.get("규격") or "").strip(), str(line.get("단위") or "").strip()
|
||||
rate = _ratio(line, others) if finger.kind_of(line) == "%" else None
|
||||
memo = f"실무 {qty if qty is not None else ''}{unit} 그대로"
|
||||
if rate:
|
||||
price = dict.fromkeys(COSTS, Decimal(0))
|
||||
elif qty is None or finger.kind_of(line) == "%": # 금액만 · 밑수 다른 % 줄
|
||||
price, qty, memo = _parts(line.get("금액")), Decimal(1), f"{memo} · 금액만"
|
||||
unit = "식" if finger.kind_of(line) == "%" else unit
|
||||
else:
|
||||
price = _parts(line.get("단가"))
|
||||
name = str(line.get("이름") or "").strip() or "이름 없음"
|
||||
head, n = name, 1
|
||||
while fixed.fixed_id({"이름": name, "규격": spec, "단위": unit}) in taken:
|
||||
n += 1
|
||||
name = f"{head} ({n})"
|
||||
fx = {
|
||||
"이름": name,
|
||||
"규격": spec,
|
||||
"단위": unit,
|
||||
"단가": {k: _plain(v) for k, v in price.items()},
|
||||
}
|
||||
fx["이음"] = dict.fromkeys(fixed.LINKS) if rate else linker.link(line, price)
|
||||
fx["출처"] = {**where, "줄": int(line.get("차례", 0))}
|
||||
if rate:
|
||||
fx["비율"] = rate
|
||||
taken.add(fixed.fixed_id(fx))
|
||||
alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None
|
||||
return {"고정": fx, "별칭": alias, "메모": memo, "수량": {"손": _plain(qty)}}
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -382,10 +383,20 @@ def hopyo_view(hopyo: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str,
|
||||
if full:
|
||||
made = ctx.done.get(hopyo_build.mark(ctx.key, int(hopyo["번"])))
|
||||
out.update(통째=whole(hopyo, ctx), 셈=ctx.calc)
|
||||
out["만든"] = {"키": made, "확인": True} if made else None
|
||||
out["만든"] = {"키": made, "확인": True, **_made(ctx.to, made)} if made else None
|
||||
return out
|
||||
|
||||
|
||||
def _made(folder: Path, key: str) -> dict[str, Any]:
|
||||
"""만든 사전조합의 줄 갈래(705-3) — `품셈` = 품셈으로 넣은 엑셀 줄 차례(옛 것 = None) · `고정` 줄 수."""
|
||||
try:
|
||||
doc = json.loads((Path(folder) / "precombo" / f"{key}.json").read_bytes())
|
||||
except (OSError, ValueError):
|
||||
return {"품셈": None, "고정": 0}
|
||||
fixed = sum(1 for x in doc.get("담은로직") or [] if isinstance(x, dict) and "고정" in x)
|
||||
return {"품셈": (doc.get("본뜬") or {}).get("품셈"), "고정": fixed}
|
||||
|
||||
|
||||
def guess(
|
||||
folder: Path, key: str, no: int, at: int, target: Any, to: Path | None = None
|
||||
) -> dict[str, Any]:
|
||||
|
||||
@@ -456,6 +456,7 @@ def settle(result: dict, doc: dict, given: dict[str, Any], col: Any = None) -> d
|
||||
continue
|
||||
bases.append({**zero, "재료비": price, "계": price} if price is not None else None)
|
||||
reflects.append({**zero, "재료비": cost, "계": cost} if cost is not None else None)
|
||||
fixed.ratios(lines, reflects, bases) # 비율 고정 줄(705-3) — 다른 줄 반영합 × 율
|
||||
return _json({"줄": lines, "자재": mats, "기준합": _sum(bases), "반영합": _sum(reflects)})
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
(노임 새 판 때 따라감) · 품(수량)은 그대로.
|
||||
M02 셈(층 · 사본) = 노임 「값」 · 자재 기준단가로 이음 · 기계 = 줄 단가(기계 시간 단가 = B09 단가 책 몫).
|
||||
출처 = `{파일, 호표, 줄, 판}`(글 · 수만) · 줄 가름 = 「고정|이름|규격|단위」(짝 `Labor_Calc.ts` `lineId`).
|
||||
비율 줄(705-3 · 엑셀 % 줄) = `비율 {기준, 비목}` · 수량 = 율(%) — 반영[비목] = 다른 줄 반영합[기준] × 율 ÷ 100
|
||||
(`ratios` · 짝 `Labor_Calc.ts` `ratioLines` · B09 = `pct` 줄).
|
||||
B09 대가 갈래 `fixed` = `B09_Estimation_Labor_Source.fixed_line` · 셈 `B09_Estimation_Calc_Labor`.
|
||||
"""
|
||||
|
||||
@@ -76,10 +78,16 @@ def clean(fx: Any, where: str, whole: Any) -> dict[str, Any]:
|
||||
if code and (code[:2] != head or not _has(whole, code)):
|
||||
raise _bad(f"{where} · 고정 줄 {k} 이음 「{code}」 M01 에 없음")
|
||||
link[k] = code or None
|
||||
rate = fx.get("비율")
|
||||
if rate is not None and not (
|
||||
isinstance(rate, dict) and rate.get("기준") in COSTS and rate.get("비목") in COSTS
|
||||
):
|
||||
raise _bad(f"{where} · 고정 줄 비율은 {{기준, 비목}} — 노무비 · 재료비 · 경비")
|
||||
src = fx.get("출처") if isinstance(fx.get("출처"), dict) else {}
|
||||
kept = {k: v for k in SOURCE_KEYS if isinstance(v := src.get(k), (str, int, float))}
|
||||
out = {"이름": name, "규격": str(fx.get("규격") or ""), "단위": str(fx.get("단위") or "")}
|
||||
return {**out, "단가": price, "이음": link, **({"출처": kept} if kept else {})}
|
||||
out = {**out, "단가": price, "이음": link, **({"출처": kept} if kept else {})}
|
||||
return {**out, "비율": {k: rate[k] for k in ("기준", "비목")}} if rate else out
|
||||
|
||||
|
||||
def _has(whole: Any, code: str) -> bool:
|
||||
@@ -102,6 +110,9 @@ def line(whole: Any, fx: dict) -> dict[str, Any]:
|
||||
"""고정 줄 계산 답 — 기준 = 줄 단가 · 이은 노임 · 자재는 `whole` 값(없으면 줄 단가 그대로) ·
|
||||
분모 = 줄 단위 · 배 1 · `이은` = 단가 책 값을 쓴 비목."""
|
||||
base = {k: _num((fx.get("단가") or {}).get(k)) or Decimal(0) for k in COSTS}
|
||||
if fx.get("비율"): # 기준 · 반영 = `ratios`(settle 끝)
|
||||
return {FIXED: fx, "이름": fx.get("이름"), "알약": None, "분모": "%", "배": 100,
|
||||
"기준": {**base, "계": Decimal(0)}, "이은": [], "까닭": None} # fmt: skip
|
||||
link, used = fx.get("이음") or {}, []
|
||||
if code := link.get("노임"):
|
||||
try:
|
||||
@@ -121,3 +132,27 @@ def line(whole: Any, fx: dict) -> dict[str, Any]:
|
||||
money = {**base, "계": sum(base.values())}
|
||||
head = {FIXED: fx, "이름": fx.get("이름"), "알약": None, "분모": fx.get("단위"), "배": 1}
|
||||
return {**head, "기준": money, "이은": used, "까닭": None}
|
||||
|
||||
|
||||
def ratio_row(rate: dict, qty: Decimal | None, under: dict) -> tuple[dict, dict | None]:
|
||||
"""비율 줄 (기준, 반영) — 기준[비목] = 밑[기준] · 반영 = 기준 × 율 ÷ 100(수량 없음 = None)."""
|
||||
base = dict.fromkeys((*COSTS, "계"), Decimal(0))
|
||||
base[rate["비목"]] = base["계"] = under[rate["기준"]]
|
||||
return base, None if qty is None else {k: v * qty / 100 for k, v in base.items()}
|
||||
|
||||
|
||||
def ratios(lines: list[dict], reflects: list[dict | None], bases: list[dict | None]) -> None:
|
||||
"""비율 줄(705-3) — `settle` 끝에 · 밑 = 비율 줄 뺀 반영합(줄 · 자재) · 배 100 · 기준합엔 안 셈 ·
|
||||
`lines` · `reflects` · `bases` 앞 줄 = 같은 차례(짝 `Labor_Calc.ts` `ratioLines`)."""
|
||||
at = {i for i, x in enumerate(lines) if (x.get(FIXED) or {}).get("비율")}
|
||||
under = {
|
||||
k: sum((r[k] for i, r in enumerate(reflects) if r and i not in at), Decimal(0))
|
||||
for k in COSTS
|
||||
}
|
||||
for i in sorted(at):
|
||||
base, done = ratio_row(lines[i][FIXED]["비율"], lines[i].get("수량"), under)
|
||||
lines[i], reflects[i], bases[i] = (
|
||||
{**lines[i], "기준": base, "배": 100, "반영": done},
|
||||
done,
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -26,6 +26,7 @@ from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
@@ -206,7 +207,7 @@ def _combo_lines(doc: dict, key: str) -> list[dict[str, Any]]:
|
||||
|
||||
lines = []
|
||||
for one in labor.clean({"담은로직": doc.get("담은로직") or []})["담은로직"]:
|
||||
head = {"참조": one["참조"]} if "참조" in one else {"로직": one["로직"]}
|
||||
head = {k: one[k] for k in ("참조", "고정") if k in one} or {"로직": one["로직"]}
|
||||
if "로직" in one:
|
||||
head["입력값"] = one["입력값"]
|
||||
head.update({k: one[k] for k in ("켜쌓기율", "고름", "자재표시") if k in one})
|
||||
@@ -461,6 +462,8 @@ class Expand:
|
||||
head, sub = {"참조": ref}, self(ref, trail)
|
||||
elif one.get("로직"): # 로직 줄(51-11) = 1 단위 값(기준 ÷ 배 · 승격과 같음)
|
||||
head, sub = {"로직": one["로직"]}, self._logic(one, by_logic.get(one["로직"]))
|
||||
elif fixed.is_fixed(one): # 고정 줄(705-3) = 줄 단가 · 비율 줄 = 끝에(`ratio_row`)
|
||||
head, sub = {"고정": one["고정"]}, fixed.line(self.whole, one["고정"])
|
||||
else:
|
||||
continue
|
||||
qty = labor._number((one.get("수량") or {}).get("손"))
|
||||
@@ -494,6 +497,11 @@ class Expand:
|
||||
why = why or f"「{name}」 {got['까닭']}"
|
||||
continue
|
||||
rows.append({k: v * qty for k, v in base.items()})
|
||||
under = labor._sum(rows)
|
||||
for one in inner: # 비율 고정 줄(705-3) — 밑 = 다른 줄 합
|
||||
if (one.get("고정") or {}).get("비율"):
|
||||
one["기준"], row = fixed.ratio_row(one["고정"]["비율"], one["수량"], under)
|
||||
rows.append(row)
|
||||
return {
|
||||
"이름": doc.get("이름"),
|
||||
"단위": doc.get("단위"),
|
||||
|
||||
@@ -382,6 +382,29 @@ def hopyo_make(
|
||||
return _call(folder, hopyo_build.build, *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))
|
||||
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)
|
||||
|
||||
@@ -181,7 +181,13 @@ export interface HopyoOne extends HopyoFile {
|
||||
통째?: { 대상: HopyoPart[]; 글?: string; 체크: boolean } | null;
|
||||
셈?: HopyoCalc;
|
||||
/** 만들 층에 이 호표로 만든 사전조합 */
|
||||
만든?: { 키: string; 확인: boolean } | null;
|
||||
만든?: {
|
||||
키: string;
|
||||
확인: boolean;
|
||||
/** 705-3 — 품셈으로 넣은 엑셀 줄 차례(옛 것 = null) · 고정 줄 수 */
|
||||
품셈?: number[] | null;
|
||||
고정?: number;
|
||||
} | null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -226,8 +232,49 @@ export interface HopyoBuilt {
|
||||
이름: string;
|
||||
다시씀: boolean;
|
||||
빠진줄: HopyoMissing[];
|
||||
비교: { 엑셀: number | null; 우리: number | null; 차이: number | null; 까닭: string | null };
|
||||
비교: {
|
||||
엑셀: number | null;
|
||||
우리: number | null;
|
||||
차이: number | null;
|
||||
/** 705-3 — 엑셀 끝수 규칙으로 같음 */
|
||||
같음?: boolean;
|
||||
까닭: string | null;
|
||||
};
|
||||
장부?: { 씀: number };
|
||||
/** 705-3 — 엑셀 줄 갈래 수 */
|
||||
줄수?: HopyoLineCount;
|
||||
}
|
||||
|
||||
/** 705-3 줄 갈래 수 — 품셈 · 고정 · 호표(하위 참조) · 빠짐 · 이음(저절로) · 이음후보(단가 다름) */
|
||||
export interface HopyoLineCount {
|
||||
품셈: number;
|
||||
고정: number;
|
||||
호표: number;
|
||||
빠짐: number;
|
||||
이음: number;
|
||||
이음후보: number;
|
||||
}
|
||||
|
||||
/** [모두 고정으로 들여오기] 결과(705-3) */
|
||||
export interface HopyoBuiltAll {
|
||||
호표: number;
|
||||
줄: number;
|
||||
품셈: number;
|
||||
고정: number;
|
||||
호표참조: number;
|
||||
빠짐: number;
|
||||
이음: number;
|
||||
이음후보: number;
|
||||
같음: number;
|
||||
그대로: number;
|
||||
다름: {
|
||||
번: number;
|
||||
이름: string;
|
||||
엑셀: number | null;
|
||||
우리: number | null;
|
||||
차이: number | null;
|
||||
}[];
|
||||
실패: { 번: number; 까닭: string }[];
|
||||
}
|
||||
|
||||
/** [로직 추가] 「실무 호표」 줄 하나 — 확인 = 문서 층에 확인된 사전조합 */
|
||||
@@ -340,6 +387,13 @@ export const buildHopyo = (
|
||||
body: JSON.stringify(통째 ? { ...from, 통째 } : { ...from, 줄 }),
|
||||
});
|
||||
|
||||
/** [모두 고정으로 들여오기](705-3) — 시트 호표 모두 · 숫자는 서버가 파일에서 */
|
||||
export const buildHopyoAll = (
|
||||
target: HopyoScope,
|
||||
from: { 층: Layer; 주인: string | null; 키: string },
|
||||
): Promise<HopyoBuiltAll> =>
|
||||
call(at(target, "/hopyo-build-all"), { method: "POST", body: JSON.stringify(from) });
|
||||
|
||||
export const listHopyoPicks = async (scope: HopyoScope): Promise<HopyoPickRow[]> =>
|
||||
(await call<{ 줄: HopyoPickRow[] }>(at(scope, "/hopyo-picks"))).줄;
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
* · 대상 · 옵션을 바꾸면 서버 `guess`(저장 안 함 · 줄마다 최신 응답만)가 우리 품을 다시 셈
|
||||
* · 캐시 = sessionStorage `m02-hopyo-check:{층}:{주인}:{키}:{번}`(조작 즉시 · 만들기 성공하면 지움) · 자동 저장 없음
|
||||
* · 화면 잠금은 [사전조합 생성] 때만 · 쓰기 단추 = 만들 층 쓰기 권한만.
|
||||
* · 705-3 — 체크 안 한 줄 = 고정 줄(엑셀 숫자 그대로) · 만든 사전조합이 있으면 첫 체크 = 그 품셈 줄(`만든.품셈`) ·
|
||||
* [품셈으로 바꾸기 (n)] = 맞음 · 별칭 줄 체크 + 생성 · [모두 고정으로] = 체크 모두 끔 + 생성(되돌리기) ·
|
||||
* 생성 물음에 전후 금액(품셈 = 우리 미리 셈 · 고정 = 엑셀) · 품 다른 줄 수.
|
||||
* 계산은 서버(`M02_Hopyo_Match` · `M02_Hopyo_Build` · `/labor/{열}/calc`) — 화면은 보이기 · 체크 · 고름만.
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -240,6 +243,8 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
|
||||
const one = await readHopyoOne(opt.scope, opt.key, opt.no, opt.target);
|
||||
if (my !== seq) return;
|
||||
got = one;
|
||||
const made = one.호표.만든?.품셈; // 705-3 — 만든 사전조합의 품셈 줄 = 첫 체크
|
||||
if (made && !hasDraft(k)) for (const l of one.호표.줄) d.체크[l.차례] = made.includes(l.차례);
|
||||
draw();
|
||||
} catch (error) {
|
||||
if (my === seq && !quiet) root.replaceChildren(loadingError(why(error), () => void load()));
|
||||
@@ -391,21 +396,38 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
|
||||
단산행: 행,
|
||||
});
|
||||
|
||||
/** 1 단위 금액(705-3) — 품셈 줄 = 우리 미리 셈 · 고정 줄 = 엑셀 · 셈 중 = null */
|
||||
function sumOf(lines: HopyoLine[], poom: (l: HopyoLine) => boolean): number | null {
|
||||
let total = 0;
|
||||
for (const l of lines) {
|
||||
const p = poom(l) ? price(l) : null;
|
||||
if (p && !p.calc && !p.fail) return null;
|
||||
total += p?.calc?.반영합?.계 ?? l.금액.계 ?? 0;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
async function make(h: HopyoOne["호표"]): Promise<void> {
|
||||
const whole = !!h.통째 && (d.통째 ?? h.통째.체크);
|
||||
const lines = whole ? [] : bodyOf(h.줄, d);
|
||||
const out = leftOut(h.줄, d);
|
||||
const put = lines.filter((x) => x.대상).length;
|
||||
const outs = lines.length - put;
|
||||
const poom = (l: HopyoLine): boolean => isChecked(l, d) && checkable(l, d) && !d.뺌[l.차례];
|
||||
const now = (l: HopyoLine): boolean => (h.만든?.품셈 ?? []).includes(l.차례);
|
||||
const outs = h.줄.filter((l) => isChecked(l, d) && d.뺌[l.차례] === "안 씀").length;
|
||||
const put = h.줄.filter(poom);
|
||||
const diff = put.filter((l) => {
|
||||
const q = d.품[l.차례] ?? l.맞춤.품;
|
||||
return !!q && Object.values(q.같음).some((v) => !v);
|
||||
}).length;
|
||||
const after = sumOf(h.줄, poom);
|
||||
const known = !!h.만든?.품셈; // 옛 사전조합(품셈 줄 기록 없음) = 지금 금액 모름
|
||||
const before = known ? sumOf(h.줄, now) : null;
|
||||
const money =
|
||||
`1 단위 ${known ? `지금 ${before === null ? "셈 중" : won(Math.round(before))} → ` : ""}` +
|
||||
`바뀐 뒤 ${after === null ? "셈 중" : won(Math.round(after))} · 엑셀 ${won(h.금액.계)}`;
|
||||
const ask = whole
|
||||
? `호표 통째로 만듦 — ${h.통째!.대상.map(targetText).join(" · ")}`
|
||||
: `넣을 줄 ${put} · 뺌 ${outs} · 빠질 줄 ${Object.values(out).reduce((a, b) => a + b, 0)}` +
|
||||
(Object.keys(out).length
|
||||
? `(${Object.entries(out)
|
||||
.map(([a, b]) => `${a} ${b}`)
|
||||
.join(" · ")})`
|
||||
: "") +
|
||||
" — 사전조합 생성";
|
||||
: `품셈 줄 ${put.length}${diff ? `(품 다름 ${diff})` : ""} · 고정 줄 ${h.줄.length - put.length - outs}` +
|
||||
` · 뺌 ${outs} — ${money}(미리 셈 · 끝수 차 있음) — 사전조합 생성`;
|
||||
if (!(await showConfirmDialog(ask, "생성"))) return;
|
||||
const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: opt.key, 번: opt.no };
|
||||
showLoadingOverlay();
|
||||
@@ -498,10 +520,32 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
|
||||
});
|
||||
const whole = !!h.통째 && (d.통째 ?? h.통째.체크);
|
||||
const go = createButton({ label: "사전조합 생성", variant: "filled", size: "sm" });
|
||||
go.disabled = !whole && checked < 1;
|
||||
go.setAttribute("data-hopyo-make", "");
|
||||
go.addEventListener("click", () => void make(h));
|
||||
tools.push(all, go);
|
||||
tools.push(all);
|
||||
if (h.만든 && !whole) {
|
||||
// 705-3 — 고정 ↔ 품셈 바꾸기(줄 하나 = 체크 하나 + [사전조합 생성])
|
||||
const poom = createButton({ label: `품셈으로 바꾸기 (${sure.length})`, size: "sm" });
|
||||
poom.setAttribute("data-hopyo-to-poom", "");
|
||||
poom.disabled = !sure.length;
|
||||
poom.addEventListener("click", () => {
|
||||
for (const l of sure) d.체크[l.차례] = true;
|
||||
keep();
|
||||
draw();
|
||||
void make(h);
|
||||
});
|
||||
const back = createButton({ label: "모두 고정으로", size: "sm" });
|
||||
back.setAttribute("data-hopyo-to-fixed", "");
|
||||
back.disabled = !h.만든.품셈?.length;
|
||||
back.addEventListener("click", () => {
|
||||
for (const l of h.줄) d.체크[l.차례] = false;
|
||||
keep();
|
||||
draw();
|
||||
void make(h);
|
||||
});
|
||||
tools.push(poom, back);
|
||||
}
|
||||
tools.push(go);
|
||||
}
|
||||
const head = el("div", {
|
||||
className: "ui-toolbar m02-hopyo__bar",
|
||||
@@ -515,7 +559,16 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement {
|
||||
...(calc && !calc.끝
|
||||
? [note(`숫자 판정 셈 중 ${calc.한}/${calc.모두}${calc.까닭 ? ` · ${calc.까닭}` : ""}`)]
|
||||
: []),
|
||||
...(h.만든 ? [note(`만든 사전조합 ${h.만든.키}${h.만든.확인 ? " · 확인함" : ""}`)] : []),
|
||||
...(h.만든
|
||||
? [
|
||||
note(
|
||||
`만든 사전조합 ${h.만든.키}${h.만든.확인 ? " · 확인함" : ""}` +
|
||||
(h.만든.품셈
|
||||
? ` · 품셈 줄 ${h.만든.품셈.length} · 고정 줄 ${h.만든.고정 ?? 0}`
|
||||
: ""),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
createButton({
|
||||
label: "엑셀 보기",
|
||||
size: "sm",
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
* · [저장] = 체크한 호표마다 체크 줄(확인 창 캐시 · 없으면 서버 제안)로 하위 호표부터 차례로 `/hopyo-build` —
|
||||
* 물음 한 번(확인 창 안 연 호표 수) · 체크 줄 없는 호표 건너뜀 · 하위 확인 전(409)은 안내 ·
|
||||
* 결과 표(사전조합 키 · 빠진 줄 · 값 차이) · `onImport` 가 있으면 호표마다 알림([로직 추가] 참조 줄)
|
||||
* · [모두 고정으로 들여오기](705-3) = 물음 한 번 → 서버 `/hopyo-build-all`(호표 모두 · 하위 먼저 · 엑셀 숫자 그대로 ·
|
||||
* 이미 만든 호표 그대로) → 알림 「호표 n · 줄 m · 품셈 줄 a · 고정 줄 b · 빠짐 0 · 엑셀 합계 = 우리 합계 n/n」 + 다음 걸음
|
||||
* · 격자 = 서버 `/hopyo-grid/{키}` · 옛 파일(격자 없음)은 시트 자리에 까닭 글 · 목록 · 저장은 됨
|
||||
* ⚠ 부르는 쪽은 `import()` 로 엶 · 스프레드시트도 열 때 불러옴(다른 화면 번들에 안 섞이게).
|
||||
* ========================================================================== */
|
||||
@@ -27,12 +29,14 @@ import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import { StaleError } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import {
|
||||
buildHopyo,
|
||||
buildHopyoAll,
|
||||
deleteHopyo,
|
||||
readHopyo,
|
||||
readHopyoGrid,
|
||||
readHopyoOne,
|
||||
type HopyoBrief,
|
||||
type HopyoBuilt,
|
||||
type HopyoBuiltAll,
|
||||
type HopyoGrid,
|
||||
type HopyoOne,
|
||||
type HopyoScope,
|
||||
@@ -167,6 +171,9 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
|
||||
const go = createButton({ label: "저장", variant: "filled", size: "sm" });
|
||||
go.disabled = true;
|
||||
go.setAttribute("data-hopyo-save", "");
|
||||
const fixAll = createButton({ label: "모두 고정으로 들여오기", size: "sm" });
|
||||
fixAll.setAttribute("data-hopyo-all-fixed", "");
|
||||
fixAll.disabled = true; // 엑셀을 다 읽은 뒤 켬
|
||||
const result = el("div", { className: "m02-hopyo-sheet__result" });
|
||||
const right = el("div", {
|
||||
className: "m02-hopyo-sheet__side",
|
||||
@@ -174,7 +181,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
|
||||
search,
|
||||
el("div", {
|
||||
className: "ui-toolbar m02-hopyo__bar",
|
||||
children: [count, ...(opt.canWrite ? [go] : [])],
|
||||
children: [count, ...(opt.canWrite ? [fixAll, go] : [])],
|
||||
}),
|
||||
list,
|
||||
result,
|
||||
@@ -372,6 +379,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
|
||||
]);
|
||||
if (closed) return;
|
||||
briefs = file.호표;
|
||||
fixAll.disabled = !briefs.length;
|
||||
spans = grid instanceof Error ? [] : grid.호표;
|
||||
paintHead(file);
|
||||
paintList();
|
||||
@@ -508,5 +516,60 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
|
||||
}
|
||||
go.addEventListener("click", () => void save());
|
||||
|
||||
/** 들여오기 알림 글(705-3) */
|
||||
const allText = (got: HopyoBuiltAll): string =>
|
||||
`호표 ${got.호표} · 줄 ${got.줄} · 품셈 줄 ${got.품셈} · 고정 줄 ${got.고정} · ` +
|
||||
`하위 호표 참조 ${got.호표참조} · 빠짐 ${got.빠짐} · 엑셀 합계 = 우리 합계 ${got.같음}/${got.호표}` +
|
||||
(got.그대로 ? ` · 이미 만든 호표 ${got.그대로} 그대로` : "");
|
||||
|
||||
/** [모두 고정으로 들여오기](705-3) — 물음 한 번 · 서버가 호표 모두를 하위부터(숫자 = 서버가 파일에서) */
|
||||
async function importAll(): Promise<void> {
|
||||
const ask =
|
||||
`호표 ${briefs.length}개를 엑셀 숫자 그대로(고정 줄) 사전조합으로 들여옴 — ` +
|
||||
"이미 만든 호표는 그대로 · 품셈 맞춤은 뒤에 호표 [수정]에서 [품셈으로 바꾸기]";
|
||||
if (!(await showConfirmDialog(ask, "들여오기"))) return;
|
||||
fixAll.disabled = true;
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: opt.key };
|
||||
const got = await buildHopyoAll(target, from);
|
||||
if (got.호표 > got.그대로) document.dispatchEvent(new Event(HOPYO_BUILT));
|
||||
const next =
|
||||
"다음 — 호표 [수정]에서 [품셈으로 바꾸기]" +
|
||||
(got.이음후보
|
||||
? ` · 이음 후보 ${got.이음후보} 줄(이름은 같고 단가가 다름)은 엑셀 단가 그대로 — 「이 공사 단가로 다시 잇기」에서 바꿈`
|
||||
: "");
|
||||
const rows = [
|
||||
...got.다름.map((x) =>
|
||||
el("tr", {
|
||||
children: [
|
||||
el("td", { text: `제${x.번}호표 ${x.이름}` }),
|
||||
el("td", {
|
||||
text: `엑셀 ${won(x.엑셀)} · 우리 ${won(x.우리)}`,
|
||||
className: "m02-hopyo__num",
|
||||
}),
|
||||
el("td", { text: won(x.차이), className: "m02-hopyo__num" }),
|
||||
],
|
||||
}),
|
||||
),
|
||||
...got.실패.map((x) =>
|
||||
el("tr", { children: [el("td", { text: `제${x.번}호표` }), el("td", { text: x.까닭 })] }),
|
||||
),
|
||||
];
|
||||
result.replaceChildren(
|
||||
note(allText(got)),
|
||||
note(next),
|
||||
...(rows.length ? [table(["호표", "합계 다름 · 실패", "차이"], rows)] : []),
|
||||
);
|
||||
showToast(allText(got), got.빠짐 || got.실패.length ? "warning" : "success");
|
||||
} catch (error) {
|
||||
showToast(`실패 — ${why(error)}`, "error");
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
fixAll.disabled = false;
|
||||
}
|
||||
}
|
||||
fixAll.addEventListener("click", () => void importAll());
|
||||
|
||||
void load();
|
||||
}
|
||||
|
||||
@@ -33,7 +33,10 @@ export interface LaborFixed {
|
||||
단가: Record<"노무비" | "재료비" | "경비", number>;
|
||||
이음?: Record<"노임" | "자재" | "기계", string | null>;
|
||||
출처?: Record<string, string | number>;
|
||||
/** 비율 줄(705-3 · 엑셀 % 줄) — 수량 = 율 · 반영[비목] = 다른 줄 반영합[기준] × 율 ÷ 100 */
|
||||
비율?: { 기준: CostKey; 비목: CostKey };
|
||||
}
|
||||
type CostKey = "노무비" | "재료비" | "경비";
|
||||
|
||||
export interface CalcLine {
|
||||
/** 참조 줄은 서버 답에 없음(빈 글로 다룸) */
|
||||
@@ -220,5 +223,26 @@ export function settle(
|
||||
if (cost !== null && !aside.has(one.코드)) add(done, { ...zero(), 재료비: cost, 계: cost });
|
||||
return { ...one, 수량: qty, 수량출처: source, 반영: cost };
|
||||
});
|
||||
return { 줄, 자재, 기준합: base, 반영합: done };
|
||||
return { 줄: ratioLines(줄, done, base), 자재, 기준합: base, 반영합: done };
|
||||
}
|
||||
|
||||
/** 비율 고정 줄(705-3 · 짝 `M02_Labor_Fixed.ratios`) — 밑 = 비율 줄 뺀 반영합 · 기준[비목] = 밑[기준] ·
|
||||
* 배 100 · 반영 = 기준 × 율 ÷ 100 · `done` 에 더함 · 기준합(`base`)엔 안 셈 */
|
||||
function ratioLines(lines: CalcLine[], done: Money, base: Money): CalcLine[] {
|
||||
for (const l of lines.filter((x) => x.고정?.비율)) {
|
||||
for (const k of MONEY_KEYS) {
|
||||
done[k] -= l.반영?.[k] ?? 0;
|
||||
base[k] -= l.기준?.[k] ?? 0;
|
||||
}
|
||||
}
|
||||
const under = { ...done };
|
||||
return lines.map((line) => {
|
||||
const rate = line.고정?.비율;
|
||||
if (!rate) return line;
|
||||
const own = zero();
|
||||
own[rate.비목] = own.계 = under[rate.기준];
|
||||
const got = reflect(own, line.수량 ?? null, 100);
|
||||
if (got) for (const k of MONEY_KEYS) done[k] += got[k];
|
||||
return { ...line, 기준: own, 배: 100, 반영: got };
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user