Merge remote-tracking branch 'origin/sub_laptop_6' into main_laptop_1
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
몸이 비면 = 모두 고정(705-3 · [모두 고정으로 들여오기]).
|
||||
서버 재검사(브라우저 값을 받아 적지 않음 · 금액 · 품 칸 없음) — 차례 = 그 호표 줄 · 대상 하나 =
|
||||
돈 로직 · M01 자재품목 · 확인된 호표 중 하나 · 입력 이름 = 그 로직 입력 · 배 0 넘음 · 몫 (0, 1] ·
|
||||
호표 = 만들 층에 확인된 사전조합(없으면 409 「제N호표 먼저 확인」).
|
||||
호표 = 만들 층에 확인된 사전조합(확인 안 된 하위 = 저장 때 하위 먼저 함께 만듦 · `M02_Hopyo_Kids.first` · 777-1).
|
||||
호표 하나 = 사전조합 하나 — 이름 = 품명 + 규격 · 단위 = 엑셀 단위 · 구분 「실무자료」 ·
|
||||
상세구분 = 파일 · 본뜬 = {층 「실무자료」, 키 「HP-###### 제N호표」, 파일, 원본 [층, 주인],
|
||||
확인 {누가, 때}} · 만드는 층 = 부르는 층(일위대가 문서 층).
|
||||
@@ -18,7 +18,8 @@
|
||||
「전국」 자재(361-1 · 키 · 단가 없음 · 서버가 줄에서 다시 셈) = 일위대가 자재 칸이 M01 키만
|
||||
받아 `본뜬.전국자재 [{이름, 규격, 단위, 수량, 세 칸}]` 에 적고 빠진 줄에 까닭(금액 없음 ·
|
||||
지역은 프로젝트에서) · 장부엔 안 남김.
|
||||
같은 본뜬 사전조합이 있으면 확인한 내용으로 덮음(`다시씀`) · 확인한 줄은 별칭 장부에 남음
|
||||
같은 본뜬 사전조합이 있으면 확인한 내용으로 덮음(`다시씀`) · 없으면 같은 내용 SC 재사용(`재사용` · `M02_Hopyo_Kids.save`) ·
|
||||
`하위` = 부르는 하위 호표 `[{번, 키, 새로}]`(부모 줄 = 하위 사본) · 확인한 줄은 별칭 장부에 남음
|
||||
(`M02_Hopyo_Ledger` · 호표 참조 줄 뺌).
|
||||
빠짐 0(705-3) — 확인 안 한 줄 · 노임 · % · 「우리에 없음」 줄 = 고정 줄(`M02_Hopyo_Fixed` · 엑셀 숫자
|
||||
그대로) · 확인 안 한 호표 줄 = 확인된 하위 사전조합 참조(단위 같음 · 아니면 고정) · 빠진 줄 = 「안 씀」 ·
|
||||
@@ -31,13 +32,12 @@
|
||||
`{비율: 기준}` = 고정 줄 + 그 이음 · 남은 줄(`Fixed.linked` 아님 · 「우리에 없음」 고른 줄 뺌) 있으면 409
|
||||
`{까닭, 남은줄}`(저장 안 함 · `save` 거짓 = 막지 않고 `남은줄` 만) · 미리 보기 = `unlinked`(쓰기 없음).
|
||||
수량(742-1 · 요구 584-1) = 엑셀 수량을 줄 · 자재 저장 수량(`{손}`)으로 · 같은 값 `본뜬.실무수량 {줄: {차례: 수},
|
||||
자재: {코드: 수}}`(출처 기록 · 엑셀 대조 `compare` — 하위 호표 것까지 `excel_given`).
|
||||
자재: {코드: 수}}`(출처 기록 · 엑셀 대조 `M02_Hopyo_Compare` — 하위 호표 것까지 `excel_given`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from decimal import ROUND_FLOOR, ROUND_HALF_UP, Decimal
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -45,6 +45,7 @@ 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_Kids as hkids
|
||||
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
|
||||
@@ -52,7 +53,12 @@ from M02_MasterTemplete import M02_Hopyo_SkLink as sklink
|
||||
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_Library_Same as lsame
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete.M02_Hopyo_Compare import ( # noqa: F401 — 부르는 쪽 이름 그대로
|
||||
compare,
|
||||
excel_given,
|
||||
)
|
||||
|
||||
ORIGIN = "실무자료"
|
||||
LINES_MAX = 500
|
||||
@@ -69,23 +75,8 @@ def mark(key: str, no: int, at: int | None = None) -> str:
|
||||
return head if at is None else f"{head} {at + 1}째 줄"
|
||||
|
||||
|
||||
def _origins(folder: Path):
|
||||
"""층 사전조합 가운데 실무자료 본뜬 → (키, 본뜬, 판)."""
|
||||
for path in sorted((Path(folder) / "precombo").glob("SC-*.json")):
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
doc = json.loads(raw)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
got = doc.get("본뜬") if isinstance(doc, dict) else None
|
||||
if isinstance(got, dict) and got.get("층") == ORIGIN:
|
||||
yield path.stem, got, raw
|
||||
|
||||
|
||||
def _same_source(got: dict[str, Any], source: refs.Here | None) -> bool:
|
||||
"""본뜬 원본 층이 같음 — 원본 없는 옛 것 · `source` None 은 가리지 않음."""
|
||||
have = got.get("원본")
|
||||
return source is None or not have or tuple(have) == tuple(source)
|
||||
_origins = hkids.entries # B09 가 빌려 씀(또 칸까지)
|
||||
_same_source = hkids.same_source
|
||||
|
||||
|
||||
def confirmed(folder: str | Path, source: refs.Here | None = None) -> dict[str, str]:
|
||||
@@ -97,31 +88,6 @@ def confirmed(folder: str | Path, source: refs.Here | None = None) -> dict[str,
|
||||
}
|
||||
|
||||
|
||||
def _save(doc: dict[str, Any]) -> tuple[dict[str, Any], bool]:
|
||||
"""(참조, 다시씀) — 같은 본뜬(키 · 원본)이 있으면 그 키에 덮음 · 없으면 새 키 · 엑셀 수량 = 저장(742-1)."""
|
||||
hands = {
|
||||
"줄": {str(i): x["수량"]["손"] for i, x in enumerate(doc["담은로직"]) if x.get("수량")},
|
||||
"자재": {m["코드"]: m["수량"]["손"] for m in doc.get("자재") or [] if m.get("수량")},
|
||||
}
|
||||
doc = {**doc, "본뜬": {**doc["본뜬"], "실무수량": hands}}
|
||||
at = refs.here()
|
||||
want = doc["본뜬"]
|
||||
have = next(
|
||||
(
|
||||
(key, store.version_of(raw))
|
||||
for key, got, raw in _origins(store.root())
|
||||
if got.get("키") == want["키"] and _same_source(got, tuple(want["원본"]))
|
||||
),
|
||||
None,
|
||||
)
|
||||
if have:
|
||||
store.write("precombo", have[0], have[1], doc)
|
||||
key = have[0]
|
||||
else:
|
||||
key = refs.create("precombo", doc)["이름"]
|
||||
return {"층": at[0], "주인": at[1], "키": key}, bool(have)
|
||||
|
||||
|
||||
def _num(value: Decimal) -> int | float:
|
||||
value = value.normalize()
|
||||
return int(value) if value == value.to_integral_value() else float(value)
|
||||
@@ -163,10 +129,12 @@ class Builder:
|
||||
self.done = confirmed(folder, self.source)
|
||||
self.known = ledger._known()
|
||||
self.mat_names = {x["자재"]: x["이름"] for x in hopyo_match.targets()["자재"]}
|
||||
self.units = {int(h["번"]) for h in self.doc.get("호표") or []}
|
||||
self.hopyo = next((h for h in self.doc.get("호표") or [] if int(h["번"]) == self.no), None)
|
||||
if self.hopyo is None:
|
||||
raise _bad(f"없는 호표 「제{no}호표」", 404)
|
||||
self.src, self.dry, self._sk, self._maker = Path(source), False, None, None
|
||||
self.same, self.reused = lsame.Same(), False # 같은 내용 SC 찾기(한 요청 메모)
|
||||
self.strict = True # 남은 줄 있으면 409(붙여넣기 `build_all` = 거짓)
|
||||
self.doc_id = ledger.doc_id(source, key) # 739-1 이 불러오기에서 이은 짝
|
||||
|
||||
@@ -185,8 +153,10 @@ class Builder:
|
||||
child = int(raw["호표"])
|
||||
if child == self.no:
|
||||
raise _bad(f"호표가 자기를 담음 「{where}」")
|
||||
if mark(self.key, child) not in self.done:
|
||||
raise _bad(f"제{child}호표 먼저 확인", 409)
|
||||
if mark(self.key, child) not in self.done and not self.dry:
|
||||
raise _bad(
|
||||
f"제{child}호표 먼저 확인", 409
|
||||
) # 저장 길은 하위 먼저 만든 뒤(`hkids.first`)
|
||||
times, part = ledger.times_of(raw, where)
|
||||
return {"호표": child, "배": times, "몫": part}
|
||||
|
||||
@@ -311,7 +281,7 @@ class Builder:
|
||||
ref = {
|
||||
"층": refs.here()[0],
|
||||
"주인": refs.here()[1],
|
||||
"키": self.done[mark(self.key, one["호표"])],
|
||||
"키": self.done.get(mark(self.key, one["호표"]), "SC-미리"),
|
||||
}
|
||||
lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
|
||||
return
|
||||
@@ -336,7 +306,9 @@ class Builder:
|
||||
"자재": [],
|
||||
"본뜬": self._origin(f"{mark(self.key, self.no, at)} {one['로직']}", stamp),
|
||||
}
|
||||
ref = {"키": "SC-미리"} if self.dry else _save(wrap)[0] # 미리 보기 = 쓰기 없음
|
||||
ref = (
|
||||
{"키": "SC-미리"} if self.dry else hkids.save(wrap, self.same)[0]
|
||||
) # 미리 보기 = 쓰기 없음
|
||||
lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}})
|
||||
|
||||
book: dict[str, dict[str, Any]] = {}
|
||||
@@ -395,13 +367,15 @@ class Builder:
|
||||
count = {"품셈": len(poomsem), "고정": fixed_n, "호표": kid_n}
|
||||
count |= {"빠짐": sum(1 for x in missing if x["까닭"] != TWICE), "이음": links}
|
||||
count["단가산출"] = sum(1 for x in lines if _is_sk(x))
|
||||
sub = [{"번": n, "키": self.done.get(mark(self.key, n))} for n in sorted(kids)]
|
||||
if not save:
|
||||
ledger.write(self.folder, book, self.who, file or None, self.doc_id)
|
||||
head = {"문서": doc, "빠진줄": missing, "남은줄": left}
|
||||
head = {"문서": doc, "빠진줄": missing, "남은줄": left, "하위": sub}
|
||||
return {**head, "줄수": {**count, "이음후보": linker.near}}
|
||||
ref, again = _save(doc)
|
||||
ref, again, self.reused = hkids.save(doc, self.same)
|
||||
wrote = ledger.write(self.folder, book, self.who, file or None, self.doc_id)
|
||||
out = {"참조": ref, "다시씀": again, "빠진줄": missing, "장부": {"씀": wrote}}
|
||||
out = {"참조": ref, "다시씀": again, "재사용": self.reused, "빠진줄": missing, "하위": sub}
|
||||
out["장부"] = {"씀": wrote}
|
||||
return {**out, "줄수": {**count, "이음후보": linker.near}}
|
||||
|
||||
def unlinked(self, checked: dict[int, dict[str, Any]], whole: list[dict[str, Any]]) -> list:
|
||||
@@ -418,7 +392,9 @@ class Builder:
|
||||
if finger.kind_of(line) != "호표" and ref.get("종류") != "일위":
|
||||
return None
|
||||
no = int(ref.get("번") or 0)
|
||||
made = self.done.get(mark(self.key, no))
|
||||
made = self.done.get(mark(self.key, no)) or (
|
||||
"SC-미리" if self.dry and no in self.units else None
|
||||
)
|
||||
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:
|
||||
@@ -457,59 +433,6 @@ 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 excel_given(ref: dict[str, Any], depth: int = 1) -> dict[str, Any]:
|
||||
"""`본뜬.실무수량` → 안 줄 알약 값(723-1 · 하위 사전조합까지) — 엑셀 대조 전용."""
|
||||
doc = refs.read_ref(ref) if depth <= refs.DEPTH else None
|
||||
if not isinstance(doc, dict):
|
||||
return {}
|
||||
rid, hands = refs.ref_id(ref), (doc.get("본뜬") or {}).get("실무수량") or {}
|
||||
out = {f"{rid}#{i}": v for i, v in (hands.get("줄") or {}).items()}
|
||||
out |= {f"{rid}#자재|{code}": v for code, v in (hands.get("자재") or {}).items()}
|
||||
for one in refs.refs_in(doc):
|
||||
out |= excel_given(one, depth + 1)
|
||||
return out
|
||||
|
||||
|
||||
def compare(ref: dict[str, Any], hopyo: dict[str, Any]) -> dict[str, Any]:
|
||||
"""만든 사전조합 1 단위 값 ↔ 엑셀 호표 단가(참고 · 판정 아님) — 우리 = 엑셀 끝수 규칙(내림 ·
|
||||
반올림 중 엑셀과 같은 쪽 · `_excel_round`) · 수량 = 엑셀 수량(`excel_given` · 723-1)."""
|
||||
head = {k: ref[k] for k in ("층", "주인", "키")}
|
||||
got = refs.Expand(m01.loaded()[1], given=excel_given(head))(head)
|
||||
base = got.get("기준") or {}
|
||||
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
|
||||
same = ours is not None and abs(ours - (excel or 0)) < 1 # 705-3 — 엑셀 금액 없음 = 0
|
||||
return {"엑셀": excel, "우리": ours, "차이": gap, "같음": same, "까닭": got.get("까닭")}
|
||||
|
||||
|
||||
def unlinked(
|
||||
folder: Path,
|
||||
source: Path,
|
||||
@@ -520,6 +443,7 @@ def unlinked(
|
||||
) -> dict[str, Any]:
|
||||
"""안 이은 줄 미리 보기(729-3) — 몸 = 만들기 몸 → `{남은줄, 수}`(저장 없음)."""
|
||||
maker = Builder(folder, source, key, no, None)
|
||||
maker.dry = True # 하위 확인 전 호표 대상 = 저장 때 함께 만듦(409 아님)
|
||||
checked, pairs = maker.check(list(lines or []), whole)
|
||||
left = maker.unlinked(checked, pairs)
|
||||
return {"남은줄": left, "수": len(left)}
|
||||
@@ -535,10 +459,16 @@ def build(
|
||||
who: Any = None,
|
||||
) -> dict[str, Any]:
|
||||
"""실무 호표(`source` 층 파일) + 확인한 줄 → 지금 층(`folder`) 사전조합 ·
|
||||
별칭 장부 — `{참조, 이름, 다시씀, 빠진줄, 비교, 장부}`(부르는 쪽이 `store.at(folder)`)."""
|
||||
별칭 장부 — `{참조, 이름, 다시씀, 재사용, 빠진줄, 하위, 비교, 장부}`(부르는 쪽이 `store.at(folder)`) ·
|
||||
확인 안 된 하위 호표 = 먼저 서버 체크 제안대로 함께 만듦(777-1 · 실패 = 409 「하위 제N호표 실패」)."""
|
||||
kids = (
|
||||
hkids.first(folder, source, key, no, who, lines) if whole is None else []
|
||||
) # 통째 = 하위 줄 안 씀
|
||||
new = {x["번"] for x in kids}
|
||||
maker = Builder(folder, source, key, no, who)
|
||||
checked, pairs = maker.check(list(lines or []), whole)
|
||||
made = maker.build(checked, pairs)
|
||||
made["하위"] = [{**x, "새로": x["번"] in new} for x in made["하위"]]
|
||||
return {**made, "이름": maker.hopyo.get("이름"), "비교": compare(made["참조"], maker.hopyo)}
|
||||
|
||||
|
||||
@@ -581,12 +511,16 @@ def build_all(
|
||||
"실패": [],
|
||||
"만듦": [],
|
||||
}
|
||||
shared, failed = lsame.Same(), set()
|
||||
for no in _order(units):
|
||||
out["줄"] += len(units[no].get("줄") or [])
|
||||
bad = next((k for k in hkids.kids_of(units[no]) if k in failed), None)
|
||||
try:
|
||||
maker = Builder(folder, source, key, no, who)
|
||||
maker.strict = strict
|
||||
maker.strict, maker.same = strict, shared
|
||||
have = maker.done.get(mark(key, no))
|
||||
if bad is not None and not have: # 777-1 — 하위 실패 연쇄 까닭 따로
|
||||
raise store.StoreError(409, {"까닭": f"하위 제{bad}호표 실패", "하위": bad})
|
||||
if have:
|
||||
out["그대로"] += 1
|
||||
ref = {"층": refs.here()[0], "주인": refs.here()[1], "키": have}
|
||||
@@ -600,6 +534,7 @@ def build_all(
|
||||
except store.StoreError as e:
|
||||
why = e.detail if isinstance(e.detail, dict) else {"까닭": str(e.detail)}
|
||||
out["실패"].append({"번": no, **why})
|
||||
failed.add(no)
|
||||
continue
|
||||
if got["같음"]:
|
||||
out["같음"] += 1
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""M02 실무 호표 만든 일위대가 ↔ 엑셀 호표 단가 대조(PLAN 705-3 · 723-1 · 742-1 · 777-1 `M02_Hopyo_Build` 에서 떼어 냄).
|
||||
|
||||
참고 · 판정 아님 — 우리 = 엑셀 끝수 꼴(EST 비목 합 내림 · STmate 줄 반올림 · 계 = 줄 계 합 내림) 중 엑셀과 같은 쪽 ·
|
||||
수량 = 엑셀 수량(`본뜬.실무수량` · 하위 사전조합까지 `excel_given`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import ROUND_FLOOR, ROUND_HALF_UP, Decimal
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
|
||||
# 엑셀 끝수 꼴(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 excel_given(ref: dict[str, Any], depth: int = 1) -> dict[str, Any]:
|
||||
"""`본뜬.실무수량` → 안 줄 알약 값(723-1 · 하위 사전조합까지) — 엑셀 대조 전용."""
|
||||
doc = refs.read_ref(ref) if depth <= refs.DEPTH else None
|
||||
if not isinstance(doc, dict):
|
||||
return {}
|
||||
rid, hands = refs.ref_id(ref), (doc.get("본뜬") or {}).get("실무수량") or {}
|
||||
out = {f"{rid}#{i}": v for i, v in (hands.get("줄") or {}).items()}
|
||||
out |= {f"{rid}#자재|{code}": v for code, v in (hands.get("자재") or {}).items()}
|
||||
for one in refs.refs_in(doc):
|
||||
out |= excel_given(one, depth + 1)
|
||||
return out
|
||||
|
||||
|
||||
def compare(ref: dict[str, Any], hopyo: dict[str, Any]) -> dict[str, Any]:
|
||||
"""만든 사전조합 1 단위 값 ↔ 엑셀 호표 단가(참고 · 판정 아님) — 우리 = 엑셀 끝수 규칙(내림 ·
|
||||
반올림 중 엑셀과 같은 쪽 · `_excel_round`) · 수량 = 엑셀 수량(`excel_given` · 723-1)."""
|
||||
head = {k: ref[k] for k in ("층", "주인", "키")}
|
||||
got = refs.Expand(m01.loaded()[1], given=excel_given(head))(head)
|
||||
base = got.get("기준") or {}
|
||||
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
|
||||
same = ours is not None and abs(ours - (excel or 0)) < 1 # 705-3 — 엑셀 금액 없음 = 0
|
||||
return {"엑셀": excel, "우리": ours, "차이": gap, "같음": same, "까닭": got.get("까닭")}
|
||||
@@ -5,7 +5,7 @@
|
||||
HP · 격자 · 맞춤셈 캐시 지움 · 그 HP 로 만든 이 층 일위대가(SC) · 단가산출(SK) 엑셀 사본 걷음
|
||||
(`본뜬` → 출처 `{층 「실무자료」, 파일, 때, 누가}` + 전국자재 · 복사원본 ·
|
||||
SC 줄 메모 「실무 …」 걷음 · `본뜬.실무수량` = 모든 값이 그 줄 저장 `수량.손` 과 같을 때만
|
||||
걷음(742-1 · 다르면 남김 = 금액 그대로)) · SK `본뜬.또` = 그 HP 몫만 뺌 ·
|
||||
걷음(742-1 · 다르면 남김 = 금액 그대로)) · SK · SC `본뜬.또` = 그 HP 몫만 뺌 ·
|
||||
문서 몸(줄 · 덮개 · 더한줄 · 별칭 · 구분)과 별칭 장부는 그대로 ·
|
||||
끝낸 기록 없음(같은 엑셀 다시 올림 됨 · 키 번은 `불러옴.json` 번으로 다시 안 씀).
|
||||
옛 파일 정리 `tidy`(한 번 · 층 폴더마다 · 미리 보기 = 표만) — 옛 HP(지문 없음) · 하루 지난 HP 끝냄 ·
|
||||
@@ -113,7 +113,11 @@ def forget(folder: str | Path, gone: Callable[[str], bool], apply: bool = True)
|
||||
got = doc["본뜬"]
|
||||
if not any(hit(x) for x in [got, *(got.get("또") or [])] if isinstance(x, dict)):
|
||||
continue
|
||||
new = strip_sc(doc) if kind == "precombo" else strip_sk(doc, hit)
|
||||
new = strip_sk(doc, hit) if kind == "sangeun" or not hit(got) else strip_sc(doc)
|
||||
if kind == "precombo" and (
|
||||
also := [x for x in got.get("또") or [] if isinstance(x, dict) and not hit(x)]
|
||||
):
|
||||
new["본뜬"]["또"] = also # 777-1 같은 내용 SC 를 나눠 쓴 다른 불러오기 몫은 그대로
|
||||
if new != doc:
|
||||
out.append(path.stem)
|
||||
if apply:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[구조물 조합으로] = 확인 창 몸(체크한 줄 · 통째 · `M02_Hopyo_Build.check` 그대로)으로 줄을 만들어(`build(save=False)` ·
|
||||
사전조합 SC 문서 안 씀 · 옛 [사전조합 생성] 길은 그대로) 이 층 구조물집계표에 새 항목(SU · `M02_Item_Personal.create_item`
|
||||
· 경로 「실무자료」) + 그 일위대가 문서에 줄을 담음(수량 = 1 단위 몫 `{손}`).
|
||||
확인 안 된 하위 호표 = 먼저 일위대가로 함께 만듦(777-1 `M02_Hopyo_Kids.first` · 줄 = 하위 사본 · 못 만들면 고정 줄).
|
||||
규격 묶음 = 같은 파일에서 이름 · 단위 같고 줄 구성(줄 이름 · 단위 차례) 같은 · 규격만 다른 호표(5-A 「구성 같은 묶음」) →
|
||||
한 항목의 규격 열(704-4) — 열 이름 = 규격 글에서 달라지는 칸(「H=1.5」 → 이름 H · 열 1.5) · 아니면 이름 「규격」 ·
|
||||
열마다 같은 몸으로 만든 줄 수량(줄 차례 같음 · 다르면 400) · 기본 열 = 연 호표.
|
||||
@@ -15,6 +16,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hb
|
||||
from M02_MasterTemplete import M02_Hopyo_Kids as hkids
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Item_Personal as personal
|
||||
from M02_MasterTemplete import M02_Labor_Spec as spec
|
||||
@@ -82,6 +84,10 @@ def siblings(source: Path, key: str, no: int) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _doc(folder: Path, source: Path, key: str, no: int, lines: list, whole: Any, who: Any) -> dict:
|
||||
try: # 777-1 — 하위 호표 먼저(엑셀 단가 한 줄로 납작 안 함) · 못 만들면 지금처럼 고정 줄
|
||||
hkids.first(folder, source, key, no, who, lines) if whole is None else None
|
||||
except store.StoreError:
|
||||
pass
|
||||
maker = hb.Builder(folder, source, key, no, who)
|
||||
checked, pairs = maker.check(list(lines or []), whole)
|
||||
return {**maker.build(checked, pairs, save=False), "호표": maker.hopyo}
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
"""M02 실무 호표가 다른 호표를 부를 때 = 일위대가 둘(PLAN 777-1 · 요구 621-4 · 요구 581 「SK · SC 서로 독립」).
|
||||
|
||||
하위 함께 만들기 `first` — 부모 저장(`M02_Hopyo_Build.build` · [구조물 조합으로])에서 확인 안 된 하위 호표(호표 줄 ·
|
||||
호표 대상 · 깊이 끝까지)를 하위 먼저 그 하위의 서버 체크 제안 몸(`proposal`)으로 만듦(409 「먼저 확인」 대신) ·
|
||||
부모 줄 = 하위 사본(740-1 그대로) · 하위 실패 = 409 `{까닭 「하위 제N호표 실패 — …」, 하위 N, 하위남은줄}`.
|
||||
화면 캐시 고름은 화면이 하위를 먼저 보냄(`M02_MasterTemplete_Hopyo_Kids.ts`).
|
||||
`proposal` 짝 = 화면 `bodyFor` · `wholeFor`(`M02_MasterTemplete_Hopyo_Kids.ts` · 캐시 빈 것) — 체크 제안 줄 · 대상 꼴 같음.
|
||||
같은 내용 일위대가 재사용 `save` — 같은 본뜬(키 · 원본)이면 그 SC(혼자 쓰면 덮음 · 나눠 쓰면 내용 같을 때만) ·
|
||||
아니면 이 층 같은 이름 · 같은 내용 SC(`M02_Library_Same.find_same` + 수량까지) 의 `본뜬.또` 에 출처를 더해 씀
|
||||
(SK 꼴 `M02_Hopyo_Sangeun.Maker.write`) · 없으면 새로 → 다시 불러와도 SC 가 안 늚.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Finger as finger
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
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 entries(folder: str | Path):
|
||||
"""층 사전조합 실무자료 출처 → (키, 본뜬 · 또 한 칸, 판) — 또 = 같은 내용으로 이 SC 를 쓴 뒤 출처."""
|
||||
for path in sorted((Path(folder) / KIND).glob("SC-*.json")):
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
doc = json.loads(raw)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
got = doc.get("본뜬") if isinstance(doc, dict) else None
|
||||
if isinstance(got, dict) and got.get("층") == ORIGIN:
|
||||
for one in [got, *(x for x in got.get("또") or [] if isinstance(x, dict))]:
|
||||
yield path.stem, one, raw
|
||||
|
||||
|
||||
def same_source(got: dict[str, Any], source: refs.Here | None) -> bool:
|
||||
"""본뜬 원본 층이 같음 — 원본 없는 옛 것 · `source` None 은 가리지 않음."""
|
||||
have = got.get("원본")
|
||||
return source is None or not have or tuple(have) == tuple(source)
|
||||
|
||||
|
||||
def _bare(value: Any) -> Any:
|
||||
"""사본 블록 `주인`(몸 지문 · 메모까지 셈) 뺌 — 셈 내용은 `몸` 이 다 가짐."""
|
||||
if isinstance(value, list):
|
||||
return [_bare(x) for x in value]
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
return {k: _bare(x) for k, x in value.items() if not (k == "주인" and "몸" in value)}
|
||||
|
||||
|
||||
def _rows(doc: dict[str, Any], part: str) -> list[str]:
|
||||
return sorted(json.dumps(lsame.norm(_bare(x)), ensure_ascii=False, sort_keys=True) for x in doc.get(part) or []) # fmt: skip
|
||||
|
||||
|
||||
def body(doc: dict[str, Any]) -> list:
|
||||
"""셈 내용 그대로(수량까지 · 하위 = 펼친 사본 몸) — `find_same` 지문은 SC 수량을 안 봄."""
|
||||
flat = lflat.flat_doc(KIND, doc)
|
||||
head = [lsame.unit(flat.get("단위")), lsame.norm(flat.get("규격"))]
|
||||
return [*head, _rows(flat, "담은로직"), _rows(flat, "자재")]
|
||||
|
||||
|
||||
def _entry(doc: dict[str, Any]) -> dict[str, Any]:
|
||||
return {**doc["본뜬"], "이름": doc["이름"]}
|
||||
|
||||
|
||||
def _split(old: dict[str, Any], mine: dict[str, Any]) -> tuple[dict, list]:
|
||||
got = old.get("본뜬") or {}
|
||||
own = {k: v for k, v in got.items() if k != "또"}
|
||||
rest = [x for x in got.get("또") or [] if isinstance(x, dict) and not _same_mark(x, mine)]
|
||||
return own, rest
|
||||
|
||||
|
||||
def _put(key: str, raw: bytes, old: dict, own: dict, also: list, doc: dict | None = None) -> None:
|
||||
new = {**(doc or old), "본뜬": {**own, "또": also} if also else own}
|
||||
if new != old:
|
||||
store.write(KIND, key, store.version_of(raw), new)
|
||||
|
||||
|
||||
def _empty(root: Path, doc: dict[str, Any]) -> list[str]:
|
||||
"""빈 호표(줄 · 자재 없음 = 지문 없음) — 이름 · 단위 같은 실무자료 SC."""
|
||||
if doc.get("담은로직") or doc.get("자재"):
|
||||
return []
|
||||
out = []
|
||||
for path in sorted((root / KIND).glob("SC-*.json")):
|
||||
try:
|
||||
got = json.loads(path.read_bytes())
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
same = (got.get("이름"), lsame.unit(got.get("단위"))) == (doc["이름"], lsame.unit(doc.get("단위"))) # fmt: skip
|
||||
if same and (got.get("본뜬") or {}).get("층") == ORIGIN:
|
||||
out.append(path.stem)
|
||||
return out
|
||||
|
||||
|
||||
def save(doc: dict[str, Any], same: lsame.Same | None = None) -> tuple[dict[str, Any], bool, bool]:
|
||||
"""(참조, 다시씀, 재사용) — 엑셀 수량 = 저장(742-1) · `same` = 한 요청 안 지문 메모."""
|
||||
hands = {
|
||||
"줄": {str(i): x["수량"]["손"] for i, x in enumerate(doc["담은로직"]) if x.get("수량")},
|
||||
"자재": {m["코드"]: m["수량"]["손"] for m in doc.get("자재") or [] if m.get("수량")},
|
||||
}
|
||||
doc = {**doc, "본뜬": {**doc["본뜬"], "실무수량": hands}}
|
||||
same, root, mine = same or lsame.Same(), store.root(), doc["본뜬"]
|
||||
here = refs.here()
|
||||
ref = lambda key: {"층": here[0], "주인": here[1], "키": key} # noqa: E731
|
||||
have = next(
|
||||
(
|
||||
(key, raw)
|
||||
for key, got, raw in entries(root)
|
||||
if got.get("키") == mine["키"] and same_source(got, tuple(mine["원본"]))
|
||||
),
|
||||
None,
|
||||
)
|
||||
if have:
|
||||
key, raw = have
|
||||
old = json.loads(raw)
|
||||
own, rest = _split(old, mine)
|
||||
owner, alike = _same_mark(own, mine), body(old) == body(doc)
|
||||
if owner and (not rest or alike): # 혼자 · 내용 같음 = 제자리 덮음(나눈 출처 그대로)
|
||||
_put(key, raw, old, mine, rest, doc)
|
||||
same.add(root, KIND, key, doc)
|
||||
return ref(key), True, False
|
||||
if alike: # 남의 SC 를 같은 내용으로 씀 — 제 출처 칸만
|
||||
_put(key, raw, old, own, [*rest, _entry(doc)])
|
||||
return ref(key), True, False
|
||||
head = {k: v for k, v in rest[0].items() if k != "이름"} if owner else own
|
||||
_put(key, raw, old, head, rest[1:] if owner else rest) # 내용 달라짐 = 떼어 냄
|
||||
for key in same.find_same(root, KIND, doc) or _empty(root, doc):
|
||||
raw = (root / KIND / f"{key}.json").read_bytes()
|
||||
old = json.loads(raw)
|
||||
if old.get("이름") == doc["이름"] and body(old) == body(doc): # 이름까지 = 같은 호표
|
||||
own, rest = _split(old, mine)
|
||||
_put(key, raw, old, own, [*rest, _entry(doc)])
|
||||
return ref(key), False, True
|
||||
key = refs.create(KIND, doc)["이름"]
|
||||
same.add(root, KIND, key, doc)
|
||||
return ref(key), False, False
|
||||
|
||||
|
||||
# ── 하위 함께 만들기 ─────────────────────────────────────────────────
|
||||
def kids_of(hopyo: dict[str, Any]) -> list[int]:
|
||||
"""호표 줄이 부르는 호표 번(줄 차례)."""
|
||||
out = []
|
||||
for line in hopyo.get("줄") or []:
|
||||
ref = line.get("참조") or {}
|
||||
if finger.kind_of(line) == "호표" or ref.get("종류") == "일위":
|
||||
out.append(int(ref.get("번") or 0))
|
||||
return out
|
||||
|
||||
|
||||
def order(units: dict[int, dict[str, Any]], no: int, also: Any = ()) -> list[int]:
|
||||
"""`no` 아래 하위 호표(+ `also` 와 그 아래) — 하위 먼저 · `no` 뺌 · 고리는 끊음."""
|
||||
out: list[int] = []
|
||||
seen = {int(no)}
|
||||
|
||||
def visit(at: int) -> None:
|
||||
for kid in kids_of(units.get(at) or {}):
|
||||
if kid not in seen and kid in units:
|
||||
seen.add(kid)
|
||||
visit(kid)
|
||||
out.append(kid)
|
||||
|
||||
visit(int(no))
|
||||
for kid in also:
|
||||
if kid not in seen and kid in units:
|
||||
seen.add(kid)
|
||||
visit(kid)
|
||||
out.append(kid)
|
||||
return out
|
||||
|
||||
|
||||
def _target(p: dict[str, Any]) -> dict[str, Any]:
|
||||
"""대상 → 만들기 몸 대상(화면 `toTarget` 짝)."""
|
||||
times = 1 if p.get("배") is None else p["배"]
|
||||
if p.get("호표") is not None:
|
||||
return {"호표": p["호표"], "배": times}
|
||||
link = next((k for k in ("노임", "기계", "비율") if p.get(k)), None)
|
||||
if link:
|
||||
return {link: p[link], "배": 1}
|
||||
head = (
|
||||
{"로직": p["로직"], "입력값": p.get("입력값") or {}}
|
||||
if p.get("로직")
|
||||
else {"전국": True}
|
||||
if p.get("전국")
|
||||
else {"자재": p.get("자재")}
|
||||
)
|
||||
return {**head, "배": times, **({"몫": p["몫"]} if p.get("몫") is not None else {})}
|
||||
|
||||
|
||||
def proposal(hopyo: dict[str, Any]) -> tuple[list[dict[str, Any]], dict[str, Any] | None]:
|
||||
"""서버 체크 제안 몸 (줄, 통째) — 보기(`M02_Hopyo_Line.hopyo_view`) 체크 제안 줄만(화면 `bodyFor` 짝)."""
|
||||
whole = hopyo.get("통째")
|
||||
if whole and whole.get("체크"):
|
||||
return [], {"대상": [_target(p) for p in whole.get("대상") or []]}
|
||||
lines = []
|
||||
for line in hopyo.get("줄") or []:
|
||||
got = line.get("맞춤") or {}
|
||||
parts = got.get("대상") if isinstance(got.get("대상"), list) else []
|
||||
if got.get("상태") == "%":
|
||||
ok = bool(parts and parts[0].get("비율"))
|
||||
else:
|
||||
ok = bool(parts) and all((p.get("배") or 0) > 0 for p in parts)
|
||||
if got.get("체크") and ok:
|
||||
lines.append({"차례": line["차례"], "대상": [_target(p) for p in parts]})
|
||||
return lines, None
|
||||
|
||||
|
||||
def first(
|
||||
folder: Path, source: Path, key: str, no: int, who: Any, lines: Any = ()
|
||||
) -> list[dict[str, Any]]:
|
||||
"""확인 안 된 하위 호표(호표 줄 + 몸 `lines` 의 호표 대상)를 하위 먼저 서버 체크 제안대로 만듦 →
|
||||
`[{번, 키}]`(새로 만든 것) ·
|
||||
실패 = 409 `{까닭 「하위 제N호표 실패 — …」, 하위, 하위남은줄}`(만든 하위는 남음)."""
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hb
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
|
||||
units = {int(h["번"]): h for h in hopyo_store.read(source, key).get("호표") or []}
|
||||
also = [
|
||||
t["호표"] for x in lines or [] for t in x.get("대상") or [] if t.get("호표") is not None
|
||||
]
|
||||
todo = order(units, no, also)
|
||||
done = hb.confirmed(folder, refs.where(source)) if todo else {}
|
||||
made: list[dict[str, Any]] = []
|
||||
for kid in todo:
|
||||
if hb.mark(key, kid) in done:
|
||||
continue
|
||||
try:
|
||||
lines, whole = proposal(hm.view(source, key, kid, to=folder)["호표"])
|
||||
maker = hb.Builder(folder, source, key, kid, who)
|
||||
got = maker.build(*maker.check(lines, whole))
|
||||
except store.StoreError as e:
|
||||
why = e.detail if isinstance(e.detail, dict) else {"까닭": str(e.detail)}
|
||||
left = why.get("남은줄") or []
|
||||
detail = {"까닭": f"하위 제{kid}호표 실패 — {why.get('까닭')}", "하위": kid}
|
||||
raise store.StoreError(409, {**detail, "하위남은줄": left}) from e
|
||||
done[hb.mark(key, kid)] = got["참조"]["키"]
|
||||
made.append({"번": kid, "키": got["참조"]["키"]})
|
||||
return made
|
||||
@@ -256,7 +256,7 @@ def match_line(line: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, A
|
||||
target = {"호표": no, "이름": h.get("이름", ""), "단위": h.get("단위"), "입력값": {}}
|
||||
target.update(못읽음=[], 출처={}, 환산=conv, 몫=None, 배=conv["배"])
|
||||
ok = bool(made) and conv["배"] is not None and conv["갈래"] != "추정"
|
||||
text = f"확인된 일위대가 {made}" if made else f"제{no}호표 먼저 확인"
|
||||
text = f"하위 일위대가 {made} 사본" if made else f"제{no}호표 — 저장 때 하위로 함께 만듦"
|
||||
return {**out, "상태": "호표", "체크": ok, "대상": [target], "글": text}
|
||||
if kind == "노임":
|
||||
return {**out, "상태": "노임", "글": "노임 · 그릇 없음"}
|
||||
|
||||
@@ -29,7 +29,7 @@ WHY = {
|
||||
"별도계상": "자재 후보 여럿 · 없음",
|
||||
"노임": "같은 이름 노임 없음",
|
||||
"기계": "같은 기계(EQ) 없음 · 여럿",
|
||||
"호표": "하위 호표 확인 전 · 단위 다름",
|
||||
"호표": "하위 호표 단위 다름 · 같은 하위 둘째",
|
||||
"%": "밑수가 다른 줄 금액 합 아님",
|
||||
}
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
@@ -27,12 +25,14 @@ from pydantic import BaseModel, ConfigDict, Field
|
||||
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
|
||||
@@ -212,17 +212,7 @@ def new_material(
|
||||
HOPYO_MAX_BYTES = 20 * 1024 * 1024
|
||||
|
||||
|
||||
def _later(module: str, name: str):
|
||||
"""맞춤 엔진(가 창) 함수 — 아직 없으면 None(올리기 · 시동은 그대로)."""
|
||||
try:
|
||||
got = importlib.import_module(f"M02_MasterTemplete.{module}")
|
||||
except ImportError:
|
||||
return None
|
||||
return getattr(got, name, None)
|
||||
|
||||
|
||||
if (_warm := _later("M02_Hopyo_Ours", "warm")) is not None:
|
||||
_warm() # 시동 — 우리 지문 캐시(디스크) 읽어 둠
|
||||
hopyo_ours.warm() # 시동 — 우리 지문 캐시(디스크) 읽어 둠
|
||||
|
||||
|
||||
def _who(session: Session) -> dict[str, Any]:
|
||||
@@ -243,8 +233,7 @@ def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]:
|
||||
except Exception: # 격자는 보기용 덤 — 못 만들어도 올리기는 됨(보기 창 404)
|
||||
grid = None
|
||||
row = hopyo_store.save(store.root(), name, parsed, who, grid, mark)
|
||||
if (ensure := _later("M02_Hopyo_Fit", "ensure")) is not None:
|
||||
ensure(store.root(), row["키"]) # 맞춤 뒤 셈(일감 줄 · 화면 안 기다림)
|
||||
hopyo_fit.ensure(store.root(), row["키"]) # 맞춤 뒤 셈(일감 줄 · 화면 안 기다림)
|
||||
return row
|
||||
|
||||
|
||||
@@ -255,9 +244,8 @@ def hopyo_list(
|
||||
"""실무자료 목록 `{줄: [{키, 파일, 올린때, 올린이, 시트, 호표수, 어긋남}]}`."""
|
||||
folder = _root(session, layer, owner)
|
||||
rows = hopyo_store.rows(folder)
|
||||
if (ensure := _later("M02_Hopyo_Fit", "ensure")) is not None:
|
||||
for row in rows: # 지금 판 맞춤셈 없는 파일 = 목록 열 때 뒤 셈 예약(358-3 ③)
|
||||
ensure(folder, row["키"])
|
||||
for row in rows: # 지금 판 맞춤셈 없는 파일 = 목록 열 때 뒤 셈 예약(358-3 ③)
|
||||
hopyo_fit.ensure(folder, row["키"])
|
||||
return {"줄": rows}
|
||||
|
||||
|
||||
@@ -329,11 +317,9 @@ def hopyo_one(
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""호표 하나 — 줄마다 맞춤(상태 · 체크 제안 · 대상 · 후보 5) · `to` = 만들 층(없으면 파일 층 ·
|
||||
호표 참조 줄 체크 · 만든 · 맞춤 엔진이 `to` 를 받을 때만)."""
|
||||
호표 참조 줄 체크 · 만든)."""
|
||||
folder = _root(session, layer, owner)
|
||||
if to is None or "to" not in inspect.signature(hopyo_match.view).parameters:
|
||||
return _call(folder, hopyo_match.view, folder, key, no)
|
||||
target = _root(session, to, to_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))
|
||||
|
||||
|
||||
@@ -396,13 +382,12 @@ class BuildWhole(_Strict):
|
||||
|
||||
class BuildBody(_Strict):
|
||||
"""실무 호표 → 이 층 사전조합 — 파일 층 `층` · `주인` · `키` + 호표 `번` + 확인한 `줄` ·
|
||||
직접 줄 호표 = `통째` · 같은 본뜬은 늘 덮음(`새로` = 옛 화면 호환 · 안 씀)."""
|
||||
직접 줄 호표 = `통째` · 같은 본뜬은 늘 덮음."""
|
||||
|
||||
층: Layer
|
||||
주인: str | None = None
|
||||
키: str
|
||||
번: int
|
||||
새로: bool = False
|
||||
줄: list[BuildLine] = Field(default_factory=list, max_length=500)
|
||||
통째: BuildWhole | None = None
|
||||
|
||||
@@ -415,7 +400,7 @@ def hopyo_make(
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""[확인하고 사전조합 만들기] — 확인한 줄만(이 층 쓰기 · 파일 층 읽기) · 별칭 장부 씀 →
|
||||
`{참조, 이름, 다시씀, 빠진줄, 비교, 장부}` · 몸 없음 400 · 하위 호표 확인 전 409."""
|
||||
`{참조, 이름, 다시씀, 재사용, 빠진줄, 하위, 비교, 장부}` · 하위 호표 확인 전 = 함께 만듦(실패 409)."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
source = _root(session, body.층, body.주인)
|
||||
lines = [one.model_dump(exclude_none=True) for one in body.줄]
|
||||
|
||||
@@ -29,7 +29,6 @@ import { openModal } from "@ui/ui_template_modal";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source";
|
||||
import {
|
||||
buildHopyo,
|
||||
guessHopyo,
|
||||
readHopyoOne,
|
||||
type BuildLine,
|
||||
@@ -41,6 +40,7 @@ import {
|
||||
type HopyoPart,
|
||||
type HopyoScope,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { buildWithKids as buildHopyo } from "./M02_MasterTemplete_Hopyo_Kids"; // 777-1 하위 함께
|
||||
import { builtWord, openHopyoChoose, targetText } from "./M02_MasterTemplete_Hopyo_Choose";
|
||||
import {
|
||||
checkTables,
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Kids.ts
|
||||
* 일위대가 실무자료 만들기 몸 · 하위 호표 함께(PLAN 777-1 · 요구 621-4) — 호표가 다른 호표를 부르면 일위대가 둘
|
||||
* (하위 일위대가 따로 + 부모 줄 = 하위 사본).
|
||||
* · bodyFor · wholeFor = 확인 창 캐시 위 체크 제안(짝 = 서버 `M02_Hopyo_Kids.proposal` · 캐시 빈 것)
|
||||
* · unmadeKids = 고른 호표 줄이 부르는 확인 안 된 하위 호표(깊이 끝까지 · 하위 먼저) — [저장]이 그 캐시대로 함께 만듦
|
||||
* · buildWithKids = 확인 창 [생성] — 확인 안 된 하위를 그 캐시대로 먼저 → 부모(실패 = 「하위 제N호표 실패」)
|
||||
* · 캐시 없는 하위도 서버가 부모 저장 때 제안대로 함께 만듦(`M02_Hopyo_Kids.first`) — 화면은 캐시 고름을 살림
|
||||
* · kidsText = 결과 글 「· 하위 제4호표 SC-11(사본 담음)」 · leftAt = 하위 실패의 남은 줄 = 그 하위 [고르기]
|
||||
* ========================================================================== */
|
||||
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import { StaleError } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import {
|
||||
buildHopyo,
|
||||
readHopyoOne,
|
||||
type HopyoBuilt,
|
||||
type HopyoOne,
|
||||
type HopyoScope,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { bodyOf, draftKey, loadDraft, partsOf, toTarget } from "./M02_MasterTemplete_Hopyo_Check";
|
||||
|
||||
type Body = Parameters<typeof buildHopyo>[2];
|
||||
type Whole = Parameters<typeof buildHopyo>[3];
|
||||
type From = Parameters<typeof buildHopyo>[1];
|
||||
|
||||
/** 호표 하나의 몸 — 확인 창 캐시 위 체크 제안 · 통째면 줄 없음 */
|
||||
export function bodyFor(scope: HopyoScope, key: string, one: HopyoOne): Body {
|
||||
const h = one.호표;
|
||||
const d = loadDraft(draftKey(scope, key, h.번));
|
||||
return h.통째 && (d.통째 ?? h.통째.체크) ? [] : bodyOf(h.줄, d);
|
||||
}
|
||||
export function wholeFor(scope: HopyoScope, key: string, one: HopyoOne): Whole {
|
||||
const h = one.호표;
|
||||
const d = loadDraft(draftKey(scope, key, h.번));
|
||||
return h.통째 && (d.통째 ?? h.통째.체크) ? { 대상: h.통째.대상.map(toTarget) } : null;
|
||||
}
|
||||
|
||||
/** (줄, 통째) 몸 한 벌 */
|
||||
export const bodyPair = (scope: HopyoScope, key: string, one: HopyoOne): [Body, Whole] => [
|
||||
bodyFor(scope, key, one),
|
||||
wholeFor(scope, key, one),
|
||||
];
|
||||
|
||||
/** 호표 줄이 부르는 호표 번(체크 안 한 호표 줄 · 캐시에서 고른 호표 대상까지) */
|
||||
const kidNos = (scope: HopyoScope, key: string, one: HopyoOne): number[] => {
|
||||
const d = loadDraft(draftKey(scope, key, one.호표.번));
|
||||
return one.호표.줄.flatMap((l) =>
|
||||
partsOf(l, d).flatMap((p) => (p.호표 !== undefined ? [p.호표] : [])),
|
||||
);
|
||||
};
|
||||
|
||||
/** [저장] 할 호표 — 함께 만드는 하위 · 체크 줄(통째) 있음 · 하위를 부름(호표 줄 = 하위 사본) */
|
||||
export function readyFor(scope: HopyoScope, key: string, one: HopyoOne, picked: number[]): boolean {
|
||||
const [lines, whole] = bodyPair(scope, key, one);
|
||||
return (
|
||||
!picked.includes(one.호표.번) || !!lines.length || !!whole || !!kidNos(scope, key, one).length
|
||||
);
|
||||
}
|
||||
|
||||
/** 확인 안 된 하위 호표 보기 — 하위 먼저 · `ones` 와 이미 만든 하위(그 아래도) 뺌 */
|
||||
export async function unmadeKids(
|
||||
scope: HopyoScope,
|
||||
key: string,
|
||||
target: HopyoScope,
|
||||
ones: HopyoOne[],
|
||||
): Promise<HopyoOne[]> {
|
||||
const out: HopyoOne[] = [];
|
||||
const seen = new Set(ones.map((one) => one.호표.번));
|
||||
async function visit(one: HopyoOne): Promise<void> {
|
||||
for (const no of kidNos(scope, key, one)) {
|
||||
if (seen.has(no)) continue;
|
||||
seen.add(no);
|
||||
const kid = await readHopyoOne(scope, key, no, target);
|
||||
if (kid.호표.만든) continue;
|
||||
await visit(kid);
|
||||
out.push(kid);
|
||||
}
|
||||
}
|
||||
for (const one of ones) await visit(one);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 하위 실패 — 까닭 「하위 제N호표 실패 — …」 · 남은 줄은 그 하위 몫(`leftAt`) */
|
||||
function kidFail(error: unknown, no: number): StaleError {
|
||||
const left = (error as { detail?: { 남은줄?: unknown } } | null)?.detail?.남은줄;
|
||||
const why = error instanceof Error ? error.message : String(error);
|
||||
const text = `${L("M02_HopyoKidFail").replace("{n}", String(no))} — ${why}`;
|
||||
return new StaleError(text, { 까닭: text, 하위: no, 하위남은줄: left ?? [] });
|
||||
}
|
||||
|
||||
/** 확인 창 [생성] — 확인 안 된 하위를 그 캐시대로 먼저 만들고 부모 */
|
||||
export async function buildWithKids(
|
||||
target: HopyoScope,
|
||||
from: From,
|
||||
줄: Body,
|
||||
통째?: Whole,
|
||||
): Promise<HopyoBuilt> {
|
||||
const scope = { layer: from.층, owner: from.주인 };
|
||||
const me = await readHopyoOne(scope, from.키, from.번, target);
|
||||
for (const kid of await unmadeKids(scope, from.키, target, [me])) {
|
||||
const no = kid.호표.번;
|
||||
const body = [bodyFor(scope, from.키, kid), wholeFor(scope, from.키, kid)] as const;
|
||||
await buildHopyo(target, { ...from, 번: no }, ...body).catch((error: unknown) => {
|
||||
throw kidFail(error, no);
|
||||
});
|
||||
}
|
||||
return buildHopyo(target, from, 줄, 통째);
|
||||
}
|
||||
|
||||
/** 하위 실패면 (그 하위 남은 줄 몸, 하위 번) · 아니면 그대로 — 결과 표 [고르기]가 그 하위를 엶 */
|
||||
export function leftAt(error: unknown, no: number): [unknown, number] {
|
||||
const d = (error as { detail?: { 하위?: number; 하위남은줄?: unknown } } | null)?.detail;
|
||||
return d?.하위 !== undefined ? [{ detail: { 남은줄: d.하위남은줄 } }, d.하위] : [error, no];
|
||||
}
|
||||
|
||||
/** 결과 글 꼬리 — 같은 내용 재사용 · 하위 일위대가(사본 담음 · 함께 만듦) */
|
||||
export function kidsText(got: HopyoBuilt): string {
|
||||
const more = got as HopyoBuilt & {
|
||||
재사용?: boolean;
|
||||
하위?: { 번: number; 키: string | null; 새로?: boolean }[];
|
||||
};
|
||||
const kids = (more.하위 ?? []).map((k) => {
|
||||
const head = L("M02_HopyoKid")
|
||||
.replace("{n}", String(k.번))
|
||||
.replace("{key}", k.키 ?? "");
|
||||
return ` · ${head}${k.새로 ? ` · ${L("M02_HopyoKidNew")}` : ""}`;
|
||||
});
|
||||
return (more.재사용 ? ` · ${L("M02_HopyoReused")}` : "") + kids.join("");
|
||||
}
|
||||
@@ -6,8 +6,8 @@
|
||||
* 오른쪽 = 호표 목록(체크 · 제N호표 [수정] · 찾기 · 제안 n/m — 다 연결이면 녹색 · 뒤 셈 중이면 3 초마다 다시) + [저장].
|
||||
* · 목록 누름 → 시트에서 그 호표 영역(B열 ~ 끝 열) 고름 · 칸 누름 → 그 행 호표를 목록에서 강조 · 영역 고름
|
||||
* · [수정] = 그 호표 확인 창(`openHopyoCheck` · 줄 누름과 따로)
|
||||
* · [저장] = 체크한 호표마다 체크 줄(확인 창 캐시 · 없으면 서버 제안)로 하위 호표부터 차례로 `/hopyo-build` —
|
||||
* 물음 한 번(확인 창 안 연 호표 수) · 체크 줄 없는 호표 건너뜀 · 하위 확인 전(409)은 안내 ·
|
||||
* · [저장] = 체크한 호표 + 확인 안 된 하위 호표(777-1 · `Hopyo_Kids`)마다 체크 줄(확인 창 캐시 · 없으면 서버 제안)로
|
||||
* 하위 호표부터 차례로 `/hopyo-build` — 물음 한 번 · 체크 줄 없는 고른 호표 건너뜀 · 하위 실패 = 그 하위 [고르기] ·
|
||||
* 결과 표(사전조합 키 · 빠진 줄 · 값 차이) · `onImport` 가 있으면 호표마다 알림([로직 추가] 참조 줄)
|
||||
* · [모두 고정으로 들여오기](705-3) = 물음 한 번 → 서버 `/hopyo-build-all`(호표 모두 · 하위 먼저 · 엑셀 숫자 그대로 ·
|
||||
* 이미 만든 호표 그대로) → 알림 「호표 n · 줄 m · 품셈 줄 a · 고정 줄 b · 빠짐 0 · 엑셀 합계 = 우리 합계 n/n」 + 다음 걸음
|
||||
@@ -27,7 +27,6 @@ import { openModal } from "@ui/ui_template_modal";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import type { Sheet } from "../A00_Common/spreadsheet/spreadsheet_types";
|
||||
import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import { StaleError } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import {
|
||||
buildHopyo,
|
||||
buildHopyoAll,
|
||||
@@ -45,7 +44,6 @@ import {
|
||||
type HopyoView,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import {
|
||||
bodyOf,
|
||||
draftKey,
|
||||
hasDraft,
|
||||
HOPYO_BUILT,
|
||||
@@ -53,8 +51,8 @@ import {
|
||||
openHopyoCheck,
|
||||
partsOf,
|
||||
POLL_MS,
|
||||
toTarget,
|
||||
} from "./M02_MasterTemplete_Hopyo_Check";
|
||||
import { bodyPair, kidsText, leftAt, readyFor, unmadeKids } from "./M02_MasterTemplete_Hopyo_Kids";
|
||||
import { builtWord } from "./M02_MasterTemplete_Hopyo_Choose";
|
||||
import { openRelink, relinkKeys } from "./M02_MasterTemplete_Hopyo_Relink";
|
||||
import { leftButton } from "./M02_MasterTemplete_Hopyo_Left";
|
||||
@@ -83,7 +81,7 @@ export interface HopyoSheetOptions {
|
||||
}
|
||||
|
||||
const ILWI = "일위대가";
|
||||
const DANSAN = "단가산출";
|
||||
const DANSAN = ["단가산출", "단가산출근거"]; // 형식별 블록 시트(EST · STmate)
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
const won = (v: number | null | undefined): string =>
|
||||
@@ -392,7 +390,8 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
|
||||
poll(file);
|
||||
if (grid instanceof Error) return void left.replaceChildren(note(grid.message));
|
||||
await mountSheet(grid);
|
||||
if (opt.단산행 !== undefined) sheet?.select(rowsText(opt.단산행), DANSAN);
|
||||
const rows = opt.단산행 === undefined ? null : rowsText(opt.단산행);
|
||||
if (rows) DANSAN.some((name) => sheet?.select(rows, name));
|
||||
else if (current !== null) focus(current);
|
||||
} catch (error) {
|
||||
if (!closed) {
|
||||
@@ -413,17 +412,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
|
||||
],
|
||||
});
|
||||
|
||||
/** 호표 하나의 몸 — 확인 창 캐시 위 체크 제안 · 통째면 줄 없음 */
|
||||
function bodyFor(one: HopyoOne): Parameters<typeof buildHopyo>[2] {
|
||||
const h = one.호표;
|
||||
const d = loadDraft(draftKey(opt.scope, opt.key, h.번));
|
||||
return h.통째 && (d.통째 ?? h.통째.체크) ? [] : bodyOf(h.줄, d);
|
||||
}
|
||||
function wholeFor(one: HopyoOne): Parameters<typeof buildHopyo>[3] {
|
||||
const h = one.호표;
|
||||
const d = loadDraft(draftKey(opt.scope, opt.key, h.번));
|
||||
return h.통째 && (d.통째 ?? h.통째.체크) ? { 대상: h.통째.대상.map(toTarget) } : null;
|
||||
}
|
||||
const body = (one: HopyoOne) => bodyPair(opt.scope, opt.key, one);
|
||||
const headCell = (no: number, name: string): HTMLElement =>
|
||||
el("td", { text: `제${no}호표 ${name}`.trim() });
|
||||
|
||||
@@ -448,11 +437,12 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
|
||||
skipped.push(el("tr", { children: [headCell(no, ""), el("td", { text: why(error) })] }));
|
||||
}
|
||||
}
|
||||
ones.push(...(await unmadeKids(opt.scope, opt.key, target, ones))); // 777-1 하위 함께
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
paintGo();
|
||||
}
|
||||
const ready = ones.filter((one) => bodyFor(one).length || wholeFor(one));
|
||||
const ready = ones.filter((one) => readyFor(opt.scope, opt.key, one, nos)); // 하위 함께
|
||||
for (const one of ones)
|
||||
if (!ready.includes(one))
|
||||
skipped.push(
|
||||
@@ -488,7 +478,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
|
||||
const cell = headCell(no, h.이름);
|
||||
try {
|
||||
const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: opt.key, 번: no };
|
||||
const got = await buildHopyo(target, from, bodyFor(one), wholeFor(one));
|
||||
const got = await buildHopyo(target, from, ...body(one));
|
||||
ok += 1;
|
||||
opt.onImport?.(got, h);
|
||||
rows.push(
|
||||
@@ -496,18 +486,16 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
|
||||
attrs: { "data-built": got.참조.키 },
|
||||
children: [
|
||||
cell,
|
||||
el("td", { text: `${got.참조.키} · ${builtWord(got)}` }),
|
||||
el("td", { text: `${got.참조.키} · ${builtWord(got)}${kidsText(got)}` }),
|
||||
el("td", { text: String(got.빠진줄.length), className: "m02-hopyo__num" }),
|
||||
el("td", { text: won(got.비교.차이), className: "m02-hopyo__num" }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
const left = leftButton(error, { scope: opt.scope, target, key: opt.key, 파일: opt.파일, 번: no }); // prettier-ignore
|
||||
const text =
|
||||
left || !(error instanceof StaleError)
|
||||
? `실패 — ${why(error)}`
|
||||
: `안내 — ${why(error)} · 하위 호표를 함께 고르거나 먼저 저장`;
|
||||
const [err, kid] = leftAt(error, no); // 하위 실패 = 그 하위 남은 줄
|
||||
const left = leftButton(err, { scope: opt.scope, target, key: opt.key, 파일: opt.파일, 번: kid }); // prettier-ignore
|
||||
const text = `실패 — ${why(error)}`;
|
||||
rows.push(el("tr", { children: [cell, el("td", { text }), ...(left ? [el("td", { children: [left] })] : [])] })); // prettier-ignore
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,11 @@ def test_이어서_끝내기_지움_겹침_막음(client):
|
||||
assert _keys(client) == ["HP-000001"]
|
||||
made = client.post(ALL, json={"층": "personal", "키": "HP-000001"}).json()
|
||||
n = len(made["만듦"]) # 729-2 — 남은 줄 있는 호표 = 실패(남은줄)
|
||||
assert n and n + len(made["실패"]) == 68 and all(x["남은줄"] for x in made["실패"])
|
||||
assert (
|
||||
n
|
||||
and n + len(made["실패"]) == 68
|
||||
and all(x.get("남은줄") or x.get("하위") for x in made["실패"])
|
||||
)
|
||||
hs.remember(folder, {"장부 줄": {"갈래": "안 씀"}}) # 별칭 장부 = 끝내기 뒤 남음
|
||||
cache = fit.cache_path(folder, "HP-000001")
|
||||
cache.write_text("{}", encoding="utf-8")
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
"""777-1 호표가 다른 호표를 부를 때 = 일위대가 둘(요구 621-4 · `M02_Hopyo_Kids`).
|
||||
|
||||
- 하위 확인 전 부모 저장 = 409 없이 하위 · 부모 둘(하위 = 서버 체크 제안대로 · 부모 줄 = 하위 사본 · 답 `하위`)
|
||||
- 하위가 남은 줄로 못 만들면 = 409 「하위 제N호표 실패」 · `하위` · `하위남은줄`(부모 · 하위 안 씀)
|
||||
- 같은 엑셀 두 번([끝내기] 뒤 다시 올림) = 같은 내용 SC 재사용(`본뜬.또`) · SC 안 늚 · 끝내면 그 몫만 뺌
|
||||
- 깊이 3 사슬 = 손자 → 자식 → 부모 차례로 · 부모 안 사본 안에 사본
|
||||
- 서버 체크 제안 몸(`proposal`) = 화면 `bodyFor` 꼴(체크 줄만 · % = 밑수 고른 것만 · 통째)
|
||||
층은 임시 폴더(공용 storage 안 씀).
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hb
|
||||
from M02_MasterTemplete import M02_Hopyo_Fit as fit
|
||||
from M02_MasterTemplete import M02_Hopyo_Forget as forget
|
||||
from M02_MasterTemplete import M02_Hopyo_Kids as hk
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
# 올리기 길 시험의 `client` 마당을 같이 씀
|
||||
# ruff: noqa: F811
|
||||
from resources.tester.test_705_3_hopyo_fixed import DOCS, FILES
|
||||
from resources.tester.test_m02_hopyo_excel import _post, client, master_hash # noqa: F401
|
||||
|
||||
BUILD = "/api/m02/layers/personal/hopyo-build"
|
||||
LEFT = "/api/m02/layers/personal/hopyo-unlinked"
|
||||
GEO = DOCS / FILES["거창"]
|
||||
|
||||
|
||||
def _mine():
|
||||
return layers.personal_dir(7, 42)
|
||||
|
||||
|
||||
def _scs():
|
||||
return sorted(p.stem for p in (_mine() / "precombo").glob("SC-*.json"))
|
||||
|
||||
|
||||
def _doc(key):
|
||||
return json.loads((_mine() / "precombo" / f"{key}.json").read_bytes())
|
||||
|
||||
|
||||
def _decided(client, body):
|
||||
left = client.post(LEFT, json=body).json()["남은줄"]
|
||||
return {**body, "줄": [*body["줄"], *({"차례": x["차례"], "뺌": "우리에 없음"} for x in left)]}
|
||||
|
||||
|
||||
def _geo(client, key="HP-000001"):
|
||||
assert _post(client, data=GEO.read_bytes(), name=GEO.name).status_code == 200
|
||||
fit.run_job(_mine(), key) # 숫자 판정 · 단가산출 맞춤셈(서버 체크 제안이 씀)
|
||||
|
||||
|
||||
def test_하위_확인_전_부모_저장_둘_생김(client):
|
||||
before = master_hash()
|
||||
_geo(client)
|
||||
body = _decided(client, {"층": "personal", "키": "HP-000001", "번": 3, "줄": []})
|
||||
res = client.post(BUILD, json=body)
|
||||
assert res.status_code == 200, res.text # 409 「제4호표 먼저 확인」 없음
|
||||
got = res.json()
|
||||
kid = hb.confirmed(_mine())["HP-000001 제4호표"]
|
||||
assert got["하위"] == [{"번": 4, "키": kid, "새로": True}]
|
||||
assert hb.confirmed(_mine())["HP-000001 제3호표"] == got["참조"]["키"] != kid
|
||||
copy = next(x for x in _doc(got["참조"]["키"])["담은로직"] if "참조" in x)
|
||||
assert copy["참조"]["층"] == "사본" and copy["참조"]["키"] == kid # 부모 줄 = 하위 사본(740-1)
|
||||
again = client.post(BUILD, json=body).json() # 다시 저장 = 하위 그대로 · 부모 덮음
|
||||
assert again["하위"] == [{"번": 4, "키": kid, "새로": False}] and again["다시씀"] is True
|
||||
assert master_hash() == before
|
||||
|
||||
|
||||
def test_하위_실패_까닭_따로_아무것도_안_씀(client):
|
||||
assert _post(client).status_code == 200 # 북부청 임도 제45호표 → 제46호표(남은 줄 셋)
|
||||
body = {"층": "personal", "키": "HP-000001", "번": 45, "줄": []}
|
||||
res = client.post(BUILD, json=body)
|
||||
assert res.status_code == 409, res.text
|
||||
why = res.json()["detail"]
|
||||
assert why["까닭"].startswith("하위 제46호표 실패 — ") and why["하위"] == 46
|
||||
assert why["하위남은줄"] and "남은줄" not in why # 부모 [고르기]로 잘못 안 엶 — 하위 몫
|
||||
assert hb.confirmed(_mine()) == {}
|
||||
left = client.post(LEFT, json=body).json()["남은줄"] # 미리 보기 = 하위 호표 줄은 함께 만듦
|
||||
assert all(x["갈래"] != "호표" for x in left)
|
||||
|
||||
|
||||
def test_같은_엑셀_두_번_SC_안_늚(client):
|
||||
assert _post(client).status_code == 200
|
||||
folder = _mine()
|
||||
with store.at(folder):
|
||||
first = hb.build_all(folder, folder, "HP-000001")
|
||||
made = _scs()
|
||||
assert first["실패"] == [] and len(made) <= first["호표"] + 3
|
||||
keys = {x["번"]: x["키"] for x in first["만듦"]}
|
||||
forget.finish(folder, "HP-000001")
|
||||
assert hb.confirmed(folder) == {} # 끝낸 뒤 = 출처만(키 뺌)
|
||||
up = _post(client).json()
|
||||
assert up["키"] == "HP-000002" and not up.get("이어서")
|
||||
with store.at(folder):
|
||||
second = hb.build_all(folder, folder, "HP-000002")
|
||||
assert _scs() == made # 같은 내용 = 재사용 · 안 늚
|
||||
assert {x["번"]: x["키"] for x in second["만듦"]} == keys
|
||||
assert (second["같음"], second["다름"]) == (first["같음"], first["다름"])
|
||||
shared = [k for k in made if any(x["키"].startswith("HP-000002 ") for x in _doc(k)["본뜬"].get("또", []))] # fmt: skip
|
||||
assert shared and set(hb.confirmed(folder)) == {hb.mark("HP-000002", n) for n in keys}
|
||||
forget.finish(folder, "HP-000002") # 그 몫만 뺌
|
||||
assert _scs() == made and not any(_doc(k)["본뜬"].get("또") for k in made)
|
||||
|
||||
|
||||
def test_깊이_3_사슬(client, monkeypatch):
|
||||
_geo(client)
|
||||
folder = _mine()
|
||||
units = {int(h["번"]): h for h in hs.read(folder, "HP-000001")["호표"]}
|
||||
deep = {
|
||||
no: hk.order(units, no)
|
||||
for no in units
|
||||
if any(hk.kids_of(units[k]) for k in hk.kids_of(units[no]) if k in units)
|
||||
}
|
||||
top = min(deep)
|
||||
plain = hk.proposal
|
||||
|
||||
def decided(hopyo): # 하위 남은 줄 = 「우리에 없음」(화면 [고르기] 몫을 시험이 대신)
|
||||
lines, _ = plain({**hopyo, "통째": None}) # 줄로(통째면 하위 줄이 빠짐 — 사슬을 보려고)
|
||||
body = {"층": "personal", "키": "HP-000001", "번": hopyo["번"], "줄": lines}
|
||||
return _decided(client, body)["줄"], None
|
||||
|
||||
monkeypatch.setattr(hk, "proposal", decided)
|
||||
body = _decided(client, {"층": "personal", "키": "HP-000001", "번": top, "줄": []})
|
||||
res = client.post(BUILD, json=body)
|
||||
assert res.status_code == 200, res.text
|
||||
done = hb.confirmed(folder)
|
||||
assert all(hb.mark("HP-000001", k) in done for k in deep[top]) # 손자까지 다 만듦
|
||||
new = [x["번"] for x in res.json()["하위"] if x["새로"]]
|
||||
assert new and set(new) <= set(deep[top])
|
||||
|
||||
def depth(doc):
|
||||
inner = [
|
||||
x["참조"]["몸"] for x in doc.get("담은로직") or [] if (x.get("참조") or {}).get("몸")
|
||||
]
|
||||
return 1 + max((depth(x) for x in inner), default=0)
|
||||
|
||||
assert depth(_doc(done[hb.mark("HP-000001", top)])) == 3 # 부모 안 사본 안에 사본
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("view", "want"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"줄": [
|
||||
{
|
||||
"차례": 0,
|
||||
"맞춤": {
|
||||
"체크": True,
|
||||
"상태": "로직",
|
||||
"대상": [{"로직": "GF1", "입력값": {"a": 1}, "배": 2, "몫": 0.5}],
|
||||
},
|
||||
}, # fmt: skip
|
||||
{
|
||||
"차례": 1,
|
||||
"맞춤": {"체크": False, "상태": "로직", "대상": [{"로직": "GF2", "배": 1}]},
|
||||
}, # fmt: skip
|
||||
{"차례": 2, "맞춤": {"체크": True, "상태": "%", "대상": [{"비율": None}]}},
|
||||
{
|
||||
"차례": 3,
|
||||
"맞춤": {"체크": True, "상태": "호표", "대상": [{"호표": 4, "배": None}]},
|
||||
}, # fmt: skip
|
||||
{
|
||||
"차례": 4,
|
||||
"맞춤": {"체크": True, "상태": "자재", "대상": [{"자재": "MT1", "배": 0}]},
|
||||
}, # fmt: skip
|
||||
{
|
||||
"차례": 5,
|
||||
"맞춤": {"체크": True, "상태": "자재", "대상": [{"전국": True, "배": 3}]},
|
||||
}, # fmt: skip
|
||||
]
|
||||
},
|
||||
(
|
||||
[
|
||||
{"차례": 0, "대상": [{"로직": "GF1", "입력값": {"a": 1}, "배": 2, "몫": 0.5}]},
|
||||
{"차례": 5, "대상": [{"전국": True, "배": 3}]},
|
||||
],
|
||||
None,
|
||||
),
|
||||
),
|
||||
(
|
||||
{"줄": [], "통째": {"체크": True, "대상": [{"로직": "GF1", "배": 1}]}},
|
||||
([], {"대상": [{"로직": "GF1", "입력값": {}, "배": 1}]}),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_서버_체크_제안_몸_화면_짝(view, want):
|
||||
assert hk.proposal(view) == want
|
||||
@@ -1,6 +1,6 @@
|
||||
"""일위대가 실무자료 → 사전조합 만들기(PLAN 324-3 · 338-3 · `M02_Hopyo_Build`) — 확인한 줄만 ·
|
||||
몸 없음 400 · 빠진 줄 까닭 · 로직 여럿 같은 별칭 · 같은 로직 감쌈 · 배 · 몫 · 자재 · 통째 ·
|
||||
재검사 400(없는 로직 · 모르는 입력 · 배 ≤ 0 · 금액 칸) · 하위 호표 409 → 확인 뒤 참조 · 장부 씀 ·
|
||||
재검사 400(없는 로직 · 모르는 입력 · 배 ≤ 0 · 금액 칸) · 하위 실패 409(777-1) → 확인 뒤 참조 · 장부 씀 ·
|
||||
`본뜬.확인` · 덮음 · 옛 즉시 확정 길 404 · 장부 길(보기 · 고치기 · 권한) · picks 확인 ·
|
||||
master_data 원본 그대로. 층은 임시 폴더.
|
||||
"""
|
||||
@@ -244,7 +244,10 @@ def test_몸_없음_재검사_400(client):
|
||||
def test_하위_호표_먼저_확인_퍼센트_빠짐_통째(client):
|
||||
assert _post(client).status_code == 200
|
||||
ref = [{"차례": 0, "대상": [{"호표": 46, "배": 1}]}]
|
||||
assert "제46호표 먼저 확인" in _build(client, 45, ref, want=409)["detail"]
|
||||
why = _build(client, 45, ref, want=409)[
|
||||
"detail"
|
||||
] # 777-1 — 하위 먼저 함께(제안대로) · 남은 줄로 실패
|
||||
assert why["까닭"].startswith("하위 제46호표 실패") and why["하위"] == 46 and why["하위남은줄"]
|
||||
child = _build(client, 46, [{"차례": 0, "뺌": "안 씀"}])
|
||||
assert [x["까닭"] for x in child["빠진줄"]] == ["뺌 — 안 씀"] # % 줄 = 비율 고정 줄(705-3)
|
||||
rate = _doc(layers.personal_dir(7, 42), child["참조"]["키"])["담은로직"][-1]["고정"]
|
||||
|
||||
@@ -140,7 +140,11 @@ def test_새_상태_체크_제안(client):
|
||||
assert dig["대상"][0]["로직"] == "GC000083" and len(dig["후보"]) == 5
|
||||
assert "특별인부 엑셀 0.3542 · 우리 0.3704" in dig["꼬리표"]
|
||||
ref = _hopyo(client, 45)["고화매트재료비"]
|
||||
assert ref["상태"] == "호표" and ref["체크"] is False and ref["글"] == "제46호표 먼저 확인"
|
||||
assert (
|
||||
ref["상태"] == "호표"
|
||||
and ref["체크"] is False
|
||||
and ref["글"] == "제46호표 — 저장 때 하위로 함께 만듦"
|
||||
) # 777-1
|
||||
assert ref["대상"][0]["호표"] == 46 and ref["대상"][0]["단위"] == "㎡"
|
||||
pct = _hopyo(client, 46)["잡재료비"]
|
||||
assert pct["상태"] == "%" and pct["체크"] is False
|
||||
|
||||
@@ -319,7 +319,9 @@ def test_옛_즉시_확정과_바로_만들기_없음():
|
||||
save = sheet.split("async function save(")[1].split("async function makeAll(")[0]
|
||||
assert 'showConfirmDialog(ask, "저장")' in save and "hasDraft(" in save
|
||||
assert "건너뜀 — 체크 줄 없음" in save and "await makeAll(ready, skipped);" in save
|
||||
assert "buildOrder(" in sheet and "error instanceof StaleError" in sheet # 하위 먼저 · 409 안내
|
||||
assert (
|
||||
"buildOrder(" in sheet and "unmadeKids(" in sheet and "leftAt(" in sheet
|
||||
) # 하위 먼저 · 함께(777-1)
|
||||
edit = sheet.split("const editButton =")[1].split("function paintList(")[0]
|
||||
assert '"수정"' in edit and "e.stopPropagation();" in edit and "openHopyoCheck({" in edit
|
||||
row = sheet.split("function paintList(")[1]
|
||||
|
||||
@@ -297,6 +297,10 @@ export const ui_locales_m2 = {
|
||||
M02_LeftMakeSk: ["SK 만들기", "Make SK"],
|
||||
M02_LeftSkNone: ["만든 단가산출 없음 — 고름 대기 그대로", "Nothing made — still waiting"],
|
||||
M02_LeftChoose: ["고르기", "Pick"],
|
||||
M02_HopyoKid: ["하위 제{n}호표 {key}(사본 담음)", "Sub sheet {n} {key} (copy inside)"],
|
||||
M02_HopyoKidNew: ["함께 만듦", "made together"],
|
||||
M02_HopyoKidFail: ["하위 제{n}호표 실패", "Sub sheet {n} failed"],
|
||||
M02_HopyoReused: ["같은 내용 일위대가 다시 씀", "Same unit-price reused"],
|
||||
M02_LeftFind: ["찾기", "Find"],
|
||||
M02_LeftMachine: ["기계", "Machine"],
|
||||
M02_LeftNoneTip: [
|
||||
|
||||
Reference in New Issue
Block a user