feat(821-4): 불러오기 못 이은 기계 = 내 기계 자동 생성 · 「내 기계 생성 n」 표시
- 새 M02_San_MyMachine — 그 파일 기계 구성(EST 원파일 MachMainDat · EST 엑셀 「기계경비」 · STmate 엑셀 「중기사용료」) → 우리 틀 값(가격 천원 · 달러 = 환율 FX000001 + 비고 · 손료계수 · 주연료 → 우리 유가 · 잡품 % · 조종원 인 + 우리 직종 · 전력) · 경쟁사 조종원 노임 환산 안 가져옴 · 구성 없음 = 시간당 값 직접 · 조합 기계 = 조각마다 - 만드는 때 = 층 저장 길(FromHopyo.make) 만 · 같은 이름 · 규격 내 기계 있으면 그것 · 다시 불러와도 중복 없음 · 미리 보기(emit) = 원본 「내기계예정」 만 · 개인 층 아님 = 안 만듦 - 불러오기 기계 찾기 = M01 만(Doc_FromNative.emit · Machine_Find.m01_only) — 내 기계 잇기는 한 곳(다시 불러와도 같은 SK) - 가져온 식 색인 = M01 + 그 층 내 기계(Imported._whole) · 직접 꼴 = [우리 단가로] 내 기계 시간당 · 「못 이음」 에서 뺌 · 수식 문서 변수 설명 「내 기계 키」 - EST 원파일 둘 꼴 칸 기계구성 · STmate 엑셀 중기사용료 시트 둠 - 가져온 식 패널 — 「내 기계 생성 n」 · 「내 기계 이음 n」 · 줄마다 「내 기계 · 이름 규격」 꼬리표 · 「만들어질 내 기계 n」 - 금액(엑셀 단가 그대로) 줄 값 안 건드림 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,9 @@ def pack(got: dict, name: str) -> dict[str, Any]:
|
||||
"""파서 답 → 둘 꼴(셈 · 변환에 쓰는 칸만 · JSON)."""
|
||||
out: dict[str, Any] = {"꼴": got["꼴"], "파일": name, "표지": got.get("표지") or []}
|
||||
out |= {"엑셀": True} if got.get("엑셀") else {} # 812-1 엑셀 읽기기(`M02_San_Xlsx`) — 같은 꼴
|
||||
out |= (
|
||||
{"기계구성": got["기계구성"]} if got.get("기계구성") else {}
|
||||
) # 821-4 EST 원파일 기계 구성
|
||||
head = ("코드", "번", "이름", "규격", "단위", "저장")
|
||||
if got.get(
|
||||
"엑셀"
|
||||
@@ -104,7 +107,15 @@ def numbers(saved: dict) -> dict[int, dict]:
|
||||
|
||||
|
||||
def emit(saved: dict, no: int, keys: bool = True) -> dict[str, Any]:
|
||||
"""원파일 산근 블록 하나 → `{로직, 원본}`(원본 `원파일` True · 코드 · 확정 수) · 없는 번 = KeyError."""
|
||||
"""원파일 산근 블록 하나 → `{로직, 원본}`(원본 `원파일` True · 코드 · 확정 수) · 없는 번 = KeyError ·
|
||||
기계 찾기 = M01 만(821-4 — 못 이은 기계 → 내 기계는 `M02_San_MyMachine.attach`)."""
|
||||
from M02_MasterTemplete.M02_Machine_Find import m01_only
|
||||
|
||||
with m01_only():
|
||||
return _emit(saved, no, keys)
|
||||
|
||||
|
||||
def _emit(saved: dict, no: int, keys: bool) -> dict[str, Any]:
|
||||
blk = numbers(saved)[int(no)]
|
||||
if saved["꼴"] == "EST":
|
||||
names = {b["코드"]: n for n, b in numbers(saved).items()}
|
||||
|
||||
@@ -325,6 +325,8 @@ def _attach(doc: dict, row: dict, ids: list[str], src: dict[str, dict]) -> None:
|
||||
if not item.get("참조") and var["연결"].get("값") != excel:
|
||||
continue # SK 덮개 단가 = 사용자 값
|
||||
link, note = _link_of(item)
|
||||
if mine := (item.get("자원") or {}).get("내기계"): # 821-4 꼬리표 「내 기계」
|
||||
note = f"내 기계 {mine}" + ("" if link else " · 시간당 값 직접 — 엑셀 단가 그대로")
|
||||
if note:
|
||||
var["설명"] = note
|
||||
if link is None:
|
||||
|
||||
@@ -19,6 +19,9 @@ import math
|
||||
import re
|
||||
import time
|
||||
import unicodedata
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from contextvars import ContextVar
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
@@ -150,10 +153,26 @@ def _table(whole: Any) -> tuple[list[Row], frozenset[str], dict[str, Row]]:
|
||||
_SEEN: list = [0.0, None, None] # (때, 표, 층) — M01 · 내 기계 판 확인은 층마다 1 초에 한 번
|
||||
|
||||
|
||||
_M01_ONLY: ContextVar[bool] = ContextVar("m01_only", default=False)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def m01_only() -> Iterator[None]:
|
||||
"""이 안의 찾기 = M01 기계만(821-4 불러오기 — 내 기계 잇기는 `M02_San_MyMachine.attach` 한 곳)."""
|
||||
token = _M01_ONLY.set(True)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_M01_ONLY.reset(token)
|
||||
|
||||
|
||||
def table() -> tuple[list[Row], frozenset[str], dict[str, Row]]:
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_MyMachine as mym
|
||||
|
||||
if _M01_ONLY.get():
|
||||
return _table(m01.loaded()[1])
|
||||
now, here = time.monotonic(), store.root()
|
||||
if _SEEN[1] is None or now - _SEEN[0] > 1.0 or _SEEN[2] != here:
|
||||
_SEEN[:] = [now, _table(mym.whole()), here]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* 바꾸기 전후 금액 · [원본 글 보기]
|
||||
* · 줄 2 = 이음 없는 자원 = 표지 + [이음 고르기]
|
||||
* · 줄 2-2 = 소요량 · 계수 이음(797-2 · 후보 집계 + [이음 보기] → `Hopyo_QtyLinks` 모달)
|
||||
* · 줄 2-3 = 못 이은 기계 → 내 기계(821-4) — 「내 기계 생성 n」 · 줄마다 「내 기계」 꼬리표 · 개인 층 밖 = 「만들어질 내 기계 n」
|
||||
* · 경쟁사 로직 그대로 — 기초로 바꾸기 · 근거 기초 제안 없음(809-1 · 요구 703)
|
||||
* 부품 = 단추 · 배지 · 글 · 입력 · 띠 · 표 · 모달(날 단추 · 입력 없음) · 서버 길은 부르는 쪽.
|
||||
* ========================================================================== */
|
||||
@@ -47,6 +48,12 @@ const TEXT = {
|
||||
Gap: ["바꾸기 전 {a} → 후 {b} ({d})", "Before {a} → after {b} ({d})"],
|
||||
QtyLinks: ["소요량 · 계수 이음 {n}", "Quantity / coefficient links {n}"],
|
||||
QtyOpen: ["이음 보기", "Show links"],
|
||||
MyMade: ["내 기계 생성 {n}", "My machines created {n}"],
|
||||
MyUsed: ["내 기계 이음 {n}", "Linked to my machines {n}"],
|
||||
MyPlan: ["만들어질 내 기계 {n}", "My machines to create {n}"],
|
||||
MyTag: ["내 기계", "My machine"],
|
||||
MyNew: ["이번에 만듦 · 값 확인 필요", "Created now · check values"],
|
||||
MyOld: ["있던 내 기계에 이음", "Linked to an existing one"],
|
||||
Close: ["닫기", "Close"],
|
||||
} as const satisfies Record<string, readonly [string, string]>;
|
||||
export const tx = (k: keyof typeof TEXT): string =>
|
||||
@@ -116,6 +123,34 @@ function checkBadge(doc: ImportedDoc): HTMLElement {
|
||||
return b;
|
||||
}
|
||||
|
||||
/** 못 이은 기계 → 내 기계(821-4) — 생성 수 · 이은 수 · 줄마다 꼬리표 · 예정 수 */
|
||||
function myMachines(src: NonNullable<ImportedDoc["가져온"]>["원본"]): HTMLElement | null {
|
||||
const got = src?.내기계 ?? [];
|
||||
const plan = src?.내기계예정 ?? [];
|
||||
if (!got.length && !plan.length) return null;
|
||||
const made = got.filter((m) => m.새로).length;
|
||||
const items: HTMLElement[] = [];
|
||||
if (made) items.push(createBadge(tx("MyMade").replace("{n}", String(made)), "info"));
|
||||
if (got.length > made) items.push(createBadge(tx("MyUsed").replace("{n}", String(got.length - made)), "neutral")); // prettier-ignore
|
||||
for (const m of got) {
|
||||
const b = createBadge(
|
||||
`${tx("MyTag")} · ${[m.이름, m.규격].filter(Boolean).join(" ")}`,
|
||||
"accent",
|
||||
);
|
||||
b.title = `${m.키} · ${m.새로 ? tx("MyNew") : tx("MyOld")}`;
|
||||
b.dataset.importedMine = m.키;
|
||||
items.push(b);
|
||||
}
|
||||
if (plan.length) {
|
||||
const b = createBadge(tx("MyPlan").replace("{n}", String(plan.length)), "warning");
|
||||
b.title = plan.map((m) => [m.이름, m.규격].filter(Boolean).join(" ")).join("\n");
|
||||
items.push(b);
|
||||
}
|
||||
const bar = createToolbar({ items });
|
||||
bar.dataset.importedMineBar = String(made);
|
||||
return bar;
|
||||
}
|
||||
|
||||
/** 머리 판 */
|
||||
export function importedPanel(s: PanelState, a: PanelActs): HTMLElement {
|
||||
const meta = s.doc.가져온 ?? {};
|
||||
@@ -163,6 +198,8 @@ export function importedPanel(s: PanelState, a: PanelActs): HTMLElement {
|
||||
];
|
||||
kids.push(createToolbar({ items: [...tags, ...items] }));
|
||||
}
|
||||
const mine = myMachines(src);
|
||||
if (here && mine) kids.push(mine);
|
||||
if (here && s.qty?.자리) {
|
||||
const open = createButton({ label: tx("QtyOpen"), size: 24, variant: "ghost", onClick: a.qty });
|
||||
open.dataset.importedQty = String(s.qty.자리);
|
||||
|
||||
@@ -21,6 +21,7 @@ STM_SHEETS = (
|
||||
"재료비목록표",
|
||||
"경비목록표",
|
||||
"중기목록표",
|
||||
"중기사용료", # 821-4 기계 구성(못 이은 기계 → 내 기계 값)
|
||||
)
|
||||
EST_SHEETS = (
|
||||
"단가산출총괄표",
|
||||
|
||||
@@ -444,5 +444,8 @@ def read(data: bytes) -> dict[str, Any]:
|
||||
blk["줄합"] = head_sum(lines)
|
||||
out[key].append({**blk, "줄": lines, "표지": tags})
|
||||
out["자원"] = {f"{LINK[k[0]]}:{k[1]}": v for k, v in res.items()}
|
||||
from M02_MasterTemplete.M02_San_MyMachine import est_makes
|
||||
|
||||
out["기계구성"] = est_makes(t) # 821-4 못 이은 기계 → 내 기계 값
|
||||
out["표지"] = sorted(warn) + ["옵션(datestoption.opt) · 원가 서식(wongadat) 안 읽음"]
|
||||
return out
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
"""불러오기 못 이은 기계 → 내 기계 자동 생성(PLAN 821-4 · 요구서_796 4 장 · 방향 장부 Q1 · S) — 서버 단독.
|
||||
|
||||
구성 읽기(그 파일 값 → 우리 틀 값 · 경쟁사 조종원 노임 환산 ×16/12 ×25/20 은 안 가져옴):
|
||||
EST 원파일 MachMainDat(`est_makes` · 올릴 때 둘 꼴 칸 `기계구성`) · EST 엑셀 「기계경비」 ·
|
||||
STmate 엑셀 「중기사용료」(`xlsx_makes` · 둔 산근 시트에서 그때 읽음) → {열쇠: 구성}
|
||||
구성 = {이름, 규격, 손료 [[계수 10⁻⁷, 가격 원, 달러?]], 주연료종류, 주연료 ℓ, 잡재료율 %, 조종원 인,
|
||||
조종원직종(글), 전력 kWh} · 열쇠 = 자원 코드 · 「이름|규격」(표기 정규화 `M02_Machine_Find.norm`).
|
||||
고름(`plan`) = 가져온 로직 호표 기계 줄 가운데 이음 없음 · 찾기 판정 「못 찾음」(별칭 · 표기 · 같은 파일 대조 뒤 ·
|
||||
후보 여럿 = 사람이 고름이라 안 만듦) · 조합 기계 = 조각마다(첫 조각 = 첫 손료 + 연료 · 조종원 ·
|
||||
다음 조각 = 그 차례 손료만 · 구성 없는 조합 = 안 만듦).
|
||||
몸 = 구성 있음 → 방식 바로(가격 천원 · 달러 = × 환율 FX000001 + 비고 · 손료계수 · 주연료 → 우리 유가 이름 ·
|
||||
잡품 % · 조종원 → 우리 직종 원문번호 · 전력) · 못 이은 연료 · 직종 = 칸 비움 + 비고 「확인 필요」 ·
|
||||
구성 없음 → 직접(그 파일 시간당 노 · 재 · 경).
|
||||
잇기(`attach` · 층 저장 길 `M02_Sangeun_FromHopyo.make` 만) = 그 층 내 기계 같은 이름 · 규격이면 그 줄 · 없으면
|
||||
새로(`M02_MyMachine.create`) · 바로 = 기계 줄 이음 키 · 로직 줄 = 시간당 부름 글 / 직접 = 이음 없이 자원
|
||||
`내기계` 키(셈 = 그 파일 값 · [우리 단가로] = 내 기계 시간당 · `M02_Sangeun_Imported.row_of`) ·
|
||||
원본 `내기계` [{키, 이름, 규격, 새로}] · 개인 층 아님 = 안 만듦(원본 `내기계예정` [{이름, 규격}] 만).
|
||||
금액(엑셀 단가 그대로)은 줄 값을 안 건드려 그대로.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete.M02_Machine_Find import NONE, norm
|
||||
|
||||
NOTE = "불러오기 자동 생성 — {file} · 값 확인 필요"
|
||||
CHECK = "확인 필요"
|
||||
COST = {"L": "노무비", "M": "재료비", "S": "경비"}
|
||||
|
||||
|
||||
def key_of(name: Any, spec: Any) -> str:
|
||||
return f"{norm(name)}|{norm(spec)}"
|
||||
|
||||
|
||||
def _num(v: Any) -> float | None:
|
||||
if isinstance(v, bool) or v is None:
|
||||
return None
|
||||
if isinstance(v, (int, float)):
|
||||
return float(v)
|
||||
s = str(v).strip().replace(",", "")
|
||||
return float(s) if re.fullmatch(r"-?\d+(?:\.\d*)?|-?\.\d+", s) else None
|
||||
|
||||
|
||||
def _blank(name: str = "", spec: str = "") -> dict[str, Any]:
|
||||
return {"이름": name, "규격": spec, "손료": []}
|
||||
|
||||
|
||||
def _fuel(make: dict, kind: Any, litre: Any) -> None:
|
||||
"""주연료 종류 · ℓ(처음 것) — 종류 「전력」 = 전력 kWh 칸."""
|
||||
if re.sub(r"\s", "", str(kind or "")) == "전력" and (_num(litre) or 0) > 0:
|
||||
make["전력"] = _num(litre)
|
||||
elif make.get("주연료") is None and (_num(litre) or 0) > 0:
|
||||
make["주연료종류"], make["주연료"] = str(kind or "").strip(), _num(litre)
|
||||
|
||||
|
||||
# ── 구성 읽기 ─────────────────────────────────────────────────────────
|
||||
def est_makes(t: dict[str, list[dict]]) -> dict[str, dict]:
|
||||
"""EST 원파일 MachMainDat → {자원 코드: 구성} — TitleCode 머리 다음 줄부터 빈 줄까지 ·
|
||||
Index -3 손료(GN 계수 · TU 가격 · 단위 「$」 = 달러) · -2 「주연료」(Size 종류) · 연료 이름 줄(선박용경유 등 ℓ) ·
|
||||
잡품 · 잡유 % · 전력 KWH · -1 조종원(직종 · GN 인)."""
|
||||
heads = {r.get("Code"): r for r in t.get("MachTitleDat", [])}
|
||||
out: dict[str, dict] = {}
|
||||
cur = None
|
||||
for r in t.get("MachMainDat", []):
|
||||
if r.get("TitleCode"):
|
||||
head = heads.get(r["TitleCode"]) or r
|
||||
cur = out.setdefault(
|
||||
r["TitleCode"], _blank(head.get("Name") or "", head.get("Size") or "")
|
||||
)
|
||||
elif not r.get("Name"):
|
||||
cur = None
|
||||
elif cur is not None:
|
||||
i, gn, word = r.get("Index"), r.get("GN"), re.sub(r"\s", "", str(r["Name"]))
|
||||
unit = str(r.get("Unite") or "").strip().upper()
|
||||
if i == -3 and gn:
|
||||
cur["손료"].append([gn, r.get("TU"), unit == "$"])
|
||||
elif i == -2 and word == "주연료":
|
||||
_fuel(cur, r.get("Size"), gn)
|
||||
elif i == -2 and word in ("잡품", "잡유") and cur.get("잡재료율") is None:
|
||||
cur["잡재료율"] = gn
|
||||
elif i == -2 and word == "전력":
|
||||
cur["전력"] = gn
|
||||
elif i == -2 and unit == "ℓ":
|
||||
_fuel(cur, r["Name"], gn)
|
||||
elif i == -1 and (gn or 0) > 0 and not cur.get("조종원"):
|
||||
cur["조종원"], cur["조종원직종"] = gn, str(r["Name"]).strip()
|
||||
return {k: v for k, v in out.items() if v["손료"]}
|
||||
|
||||
|
||||
def _est_sheet(rows: list[list]) -> dict[str, dict]:
|
||||
"""EST 엑셀 「기계경비」 — 「N. 이름 · 규격」 머리 아래 빈 줄까지: 「손료 가격 ₩×(달러 $×) 계수 × 10(-7)」 ·
|
||||
「주연료 종류 ℓ」 · 「잡품 주연료의 % 」 · 「직종 인수 인」 · 「전력 … kWh」."""
|
||||
out: dict[str, dict] = {}
|
||||
cur = None
|
||||
for row in rows:
|
||||
v = [c for c in row if c not in (None, "")]
|
||||
head = re.match(r"\d+\.\s*(.+)", str(v[0])) if v else None
|
||||
if head:
|
||||
spec = v[1] if len(v) > 1 and isinstance(v[1], str) else ""
|
||||
cur = out.setdefault(key_of(head.group(1), spec), _blank(head.group(1).strip(), spec))
|
||||
elif not v:
|
||||
cur = None
|
||||
elif cur is not None:
|
||||
word = re.sub(r"\s", "", str(v[0]))
|
||||
mark = next((x for x in v if x in ("₩×", "$×")), None)
|
||||
if "손료" in word and mark and (at := v.index(mark) + 1) < len(v):
|
||||
cur["손료"].append([_num(v[at]), _num(v[1]), mark == "$×"])
|
||||
elif word == "주연료" and len(v) > 2:
|
||||
_fuel(cur, v[1], v[2])
|
||||
elif word in ("잡품", "잡유") and len(v) > 2 and cur.get("잡재료율") is None:
|
||||
cur["잡재료율"] = _num(v[2])
|
||||
elif word == "전력" and len(v) > 2:
|
||||
cur["전력"] = _num(v[1]) if _num(v[1]) is not None else _num(v[2])
|
||||
elif len(v) > 2 and v[2] == "인" and (_num(v[1]) or 0) > 0 and not cur.get("조종원"):
|
||||
cur["조종원"], cur["조종원직종"] = _num(v[1]), str(v[0]).strip()
|
||||
elif len(v) > 3 and str(v[3]).upper() in ("ℓ", "L") and (_num(v[2]) or 0) > 0:
|
||||
_fuel(cur, v[0], v[2])
|
||||
return {k: m for k, m in out.items() if m["손료"] and None not in m["손료"][0][:2]}
|
||||
|
||||
|
||||
def _stm_sheet(rows: list[list]) -> dict[str, dict]:
|
||||
"""STmate 엑셀 「중기사용료」 — 「제 N 호표」 뒤 머리 [이름, 규격, 시간, 코드] · 손료 [이름, 규격, 계수(천원당),
|
||||
「천원」, 가격 천원 …] · 조종원 [직종, 인, 「인」 …] · 연료 [이름, 등급, ℓ, 「L」 …] · 잡품 [잡품, 주연료의%, % …] ·
|
||||
「합 계」 끝 → {코드 · 이름|규격: 구성}(손료계수 = 계수 × 10⁴ · 가격 = 천원 × 1000 원)."""
|
||||
out: dict[str, dict] = {}
|
||||
cur = None
|
||||
for row in rows:
|
||||
v = [c for c in row if c not in (None, "")]
|
||||
first = re.sub(r"\s", "", str(v[0])) if v else ""
|
||||
if re.fullmatch(r"제\d+호표", first):
|
||||
cur = {}
|
||||
continue
|
||||
if cur is None or not v:
|
||||
continue
|
||||
if first == "합계":
|
||||
cur = None
|
||||
continue
|
||||
if (
|
||||
not cur
|
||||
and len(v) >= 4
|
||||
and str(v[2]) in ("시간", "hr")
|
||||
and re.fullmatch(r"X\d+", str(v[-1]))
|
||||
):
|
||||
cur.update(_blank(str(v[0]), str(v[1])))
|
||||
out[str(v[-1])] = out[key_of(v[0], v[1])] = cur
|
||||
elif not cur:
|
||||
continue
|
||||
elif len(v) > 4 and str(v[3]) == "천원" and _num(v[2]) and _num(v[4]):
|
||||
cur["손료"].append([_num(v[2]) * 10_000, _num(v[4]) * 1000, False])
|
||||
elif len(v) > 2 and str(v[2]) == "인" and (_num(v[1]) or 0) > 0 and not cur.get("조종원"):
|
||||
cur["조종원"], cur["조종원직종"] = _num(v[1]), str(v[0]).strip()
|
||||
elif first == "잡품" and len(v) > 2 and cur.get("잡재료율") is None:
|
||||
cur["잡재료율"] = _num(v[2])
|
||||
elif len(v) > 3 and str(v[3]).upper() in ("L", "ℓ"):
|
||||
_fuel(cur, v[0], v[2])
|
||||
elif len(v) > 3 and str(v[3]).upper() == "KWH":
|
||||
cur["전력"] = _num(v[2])
|
||||
return {k: m for k, m in out.items() if m.get("손료")}
|
||||
|
||||
|
||||
def xlsx_makes(sheets: dict[str, list]) -> dict[str, dict]:
|
||||
"""엑셀 산근 시트 → 구성(EST 「기계경비」 · STmate 「중기사용료」 · 없음 = {})."""
|
||||
if sheets.get("기계경비"):
|
||||
return _est_sheet(sheets["기계경비"])
|
||||
if sheets.get("중기사용료"):
|
||||
return _stm_sheet(sheets["중기사용료"])
|
||||
return {}
|
||||
|
||||
|
||||
# ── 고름 · 몸 ─────────────────────────────────────────────────────────
|
||||
def _todo(logic: dict) -> list[tuple[dict, int]]:
|
||||
"""(줄, 조각 차례) — 기계 줄 · 이음 없음 · 찾기 판정 「못 찾음」."""
|
||||
out, n = [], 0
|
||||
for line in logic.get("호표") or []:
|
||||
res = line.get("자원") or {}
|
||||
if res.get("갈래") != "기계":
|
||||
continue
|
||||
n = n + 1 if res.get("조합") else 0
|
||||
if line.get("이음") or res.get("조합식") or res.get("내기계"):
|
||||
continue
|
||||
if (res.get("찾기") or {}).get("판정") == NONE:
|
||||
out.append((line, n))
|
||||
return out
|
||||
|
||||
|
||||
def _make_of(res: dict, makes: dict) -> dict | None:
|
||||
return makes.get(str(res.get("코드") or "")) or makes.get(
|
||||
key_of(res.get("이름"), res.get("규격"))
|
||||
)
|
||||
|
||||
|
||||
def _kinds(make: dict, notes: list[str]) -> dict[str, Any]:
|
||||
"""연료 · 조종원 → 우리 유가 이름 · 직종 원문번호(못 이음 = 칸 비움 + 비고)."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
index, out = m01.loaded()[1], {}
|
||||
fuel, litre = str(make.get("주연료종류") or ""), make.get("주연료") or 0
|
||||
if litre:
|
||||
ours = next(
|
||||
(n for n in ("경유", "휘발유", "실내등유") if n == re.sub(r"\s", "", fuel)), None
|
||||
)
|
||||
ours = ours or next((n for n in ("경유", "휘발유", "실내등유") if fuel.startswith(n)), None)
|
||||
if ours and f"{ours}:전국평균" in (index.numbers.get("MO") or {}):
|
||||
out |= {"주연료": litre, "주연료종류": ours}
|
||||
out |= {"잡재료율": make["잡재료율"]} if make.get("잡재료율") else {}
|
||||
else:
|
||||
notes.append(f"주연료 「{fuel}」 {litre:g}ℓ — 우리 유가에 못 이음({CHECK})")
|
||||
if make.get("조종원"):
|
||||
job = norm(make.get("조종원직종"))
|
||||
hit = [k for k, r in (index.numbers.get("LB") or {}).items() if norm(r.get("이름")) == job]
|
||||
if len(hit) == 1:
|
||||
out |= {"조종원": make["조종원"], "조종원직종": hit[0]}
|
||||
else:
|
||||
notes.append(
|
||||
f"조종원 「{make.get('조종원직종')}」 {make['조종원']:g}인 — 우리 직종에 못 이음({CHECK})"
|
||||
)
|
||||
if make.get("전력"):
|
||||
out["전력"] = make["전력"]
|
||||
return out
|
||||
|
||||
|
||||
def body(make: dict | None, n: int, line: dict, file: str) -> dict | None:
|
||||
"""내 기계 몸(`M02_MyMachine.create`) — 구성 있음 = 바로 · 없음 = 직접(조합 조각 = 안 만듦)."""
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
res = line.get("자원") or {}
|
||||
found = res.get("찾기") or {}
|
||||
name = str(found.get("이름") or res.get("이름") or line.get("이름") or "").strip()
|
||||
spec = str((found.get("규격") if found.get("이름") else res.get("규격")) or "").strip()
|
||||
head = {"이름": name, "규격": spec}
|
||||
notes = [NOTE.format(file=file)]
|
||||
if make is None:
|
||||
if n or res.get("조합첫"):
|
||||
return None
|
||||
got = res.get("단가") or {}
|
||||
pack = line.get("값묶음") or {}
|
||||
hour = {v: _num(got.get(c)) or _num(pack.get(v)) or 0 for c, v in COST.items()}
|
||||
return {**head, "방식": "직접", "시간당": hour, "비고": " · ".join(notes)}
|
||||
if n >= len(make["손료"]):
|
||||
return None
|
||||
coef, price, usd = make["손료"][n]
|
||||
if usd:
|
||||
rate = float(m01.loaded()[1].index["FX"]["FX000001"]["값"])
|
||||
notes.append(f"가격 ${price:g} × 환율 {rate:,} 원/USD")
|
||||
price = price * rate
|
||||
vals: dict[str, Any] = {"가격": round(float(price) / 1000, 4), "손료계수": coef}
|
||||
if n == 0:
|
||||
vals |= _kinds(make, notes)
|
||||
return {**head, "방식": "바로", "값": vals, "비고": " · ".join(notes)}
|
||||
|
||||
|
||||
def plan(logic: dict, makes: dict, file: str) -> list[dict]:
|
||||
"""만들(또는 이을) 내 기계 [{줄, 열쇠, 몸}] — 글 · 층 안 건드림."""
|
||||
out = []
|
||||
for line, n in _todo(logic):
|
||||
got = body(_make_of(line.get("자원") or {}, makes), n, line, file)
|
||||
if got:
|
||||
out.append({"줄": line, "열쇠": key_of(got["이름"], got["규격"]), "몸": got})
|
||||
return out
|
||||
|
||||
|
||||
def preview(out: dict, makes: dict) -> None:
|
||||
"""미리 보기 · 분석 길 — 원본 `내기계예정` [{이름, 규격}](안 만듦)."""
|
||||
todo = plan(out["로직"], makes, str(out["원본"].get("파일") or ""))
|
||||
seen = {t["열쇠"]: {"이름": t["몸"]["이름"], "규격": t["몸"]["규격"]} for t in todo}
|
||||
if seen:
|
||||
out["원본"]["내기계예정"] = list(seen.values())
|
||||
|
||||
|
||||
# ── 잇기(층 쓰기) ─────────────────────────────────────────────────────
|
||||
def link(line: dict, row: dict) -> None:
|
||||
"""가져온 줄 → 내 기계 — 바로 = 기계 줄 키 · 로직 줄 시간당 부름 글 / 직접 = 자원 `내기계` 만."""
|
||||
from M02_MasterTemplete.M02_San_Link import hourly_text
|
||||
|
||||
line["자원"]["내기계"] = row["키"]
|
||||
if row.get("방식") == "직접":
|
||||
return
|
||||
line["이음"] = hourly_text(row["키"]) if line.get("종류") == "로직" else row["키"]
|
||||
|
||||
|
||||
def attach(out: dict, makes: dict) -> list[dict]:
|
||||
"""층 저장 길 — 못 이은 기계 줄을 그 층 내 기계(같은 이름 · 규격) · 새 내 기계에 이음 · 원본 `내기계`."""
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_MyMachine as mym
|
||||
|
||||
src = out["원본"]
|
||||
todo = plan(out["로직"], makes, str(src.get("파일") or ""))
|
||||
if not todo:
|
||||
return []
|
||||
try:
|
||||
mym._user()
|
||||
except store.StoreError: # 개인 층 아님 — 안 만듦(예정만)
|
||||
preview(out, makes)
|
||||
return []
|
||||
done: dict[str, dict] = {}
|
||||
for one in todo:
|
||||
if one["열쇠"] not in done:
|
||||
have = next(
|
||||
(r for r in mym.rows() if key_of(r.get("이름"), r.get("규격")) == one["열쇠"]), None
|
||||
)
|
||||
row = have or mym.create(one["몸"])["줄"]
|
||||
done[one["열쇠"]] = {"줄": row, "새로": have is None}
|
||||
link(one["줄"], done[one["열쇠"]]["줄"])
|
||||
made = [
|
||||
{**{k: d["줄"][k] for k in ("키", "이름", "규격", "방식")}, "새로": d["새로"]}
|
||||
for d in done.values()
|
||||
]
|
||||
src["내기계"] = made
|
||||
src.pop("내기계예정", None)
|
||||
return made
|
||||
@@ -25,6 +25,7 @@ from M02_MasterTemplete import M02_Doc_FromNative as fn
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_San_Emit as em
|
||||
from M02_MasterTemplete import M02_San_MyMachine as mm
|
||||
|
||||
DIRNAME = "산근"
|
||||
RELOAD = "가져온 식 자료 없음 — 엑셀을 다시 올림"
|
||||
@@ -109,7 +110,15 @@ def has(folder: str | Path, key: str) -> bool:
|
||||
|
||||
|
||||
def emit(folder: str | Path, key: str, no: int) -> dict[str, Any]:
|
||||
"""HP 단산 블록 하나 → `{로직, 원본}`(원파일 우선 · 아니면 엑셀 읽기기 같은 길 · 이음 = M01 키)."""
|
||||
"""HP 단산 블록 하나 → `{로직, 원본}`(원파일 우선 · 아니면 엑셀 읽기기 같은 길 · 이음 = M01 키) ·
|
||||
못 이은 기계 = 원본 `내기계예정`(821-4 · 안 만듦)."""
|
||||
out, makes = _emit(folder, key, no)
|
||||
mm.preview(out, makes)
|
||||
return out
|
||||
|
||||
|
||||
def _emit(folder: str | Path, key: str, no: int) -> tuple[dict[str, Any], dict]:
|
||||
"""(블록 `{로직, 원본}`, 그 파일 기계 구성 — 원파일 둘 꼴 `기계구성` · 엑셀 = 둔 시트 · 821-4)."""
|
||||
hopyo_store.read(folder, key) # 없는 HP = 404
|
||||
path = _path(folder, key)
|
||||
try:
|
||||
@@ -118,13 +127,13 @@ def emit(folder: str | Path, key: str, no: int) -> dict[str, Any]:
|
||||
raise store.StoreError(409, RELOAD) from e
|
||||
native = saved.get("원파일")
|
||||
if isinstance(native, dict) and int(no) in fn.numbers(native): # 원파일 우선(810-2)
|
||||
return fn.emit(native, int(no))
|
||||
return fn.emit(native, int(no)), native.get("기계구성") or {}
|
||||
if not saved.get("시트"):
|
||||
raise store.StoreError(404, f"없는 단산{no}")
|
||||
excel = _excel_of(path, saved)
|
||||
if int(no) not in fn.numbers(excel):
|
||||
raise store.StoreError(404, f"없는 단산{no}")
|
||||
return fn.emit(excel, int(no))
|
||||
return fn.emit(excel, int(no)), mm.xlsx_makes(saved["시트"])
|
||||
|
||||
|
||||
def _inner(
|
||||
@@ -173,7 +182,7 @@ def make(source: Path, key: str, no: int, name: str = "", _trail: tuple = ()) ->
|
||||
안쪽 산근 블록은 형제 SK 를 먼저 만들어 참조(797-1)."""
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
|
||||
out = emit(source, key, no)
|
||||
out, makes = _emit(source, key, no)
|
||||
out["원본"]["가져온때"] = utc_now_text() # 원문 패널 「어디서 왔나」(807-1)
|
||||
_link_inner(source, key, no, out, _trail)
|
||||
try: # 소요량 · 계수 후보 표지(797-2 · 사람 확인) — 못 셈 = 표지 없이 만듦
|
||||
@@ -182,13 +191,15 @@ def make(source: Path, key: str, no: int, name: str = "", _trail: tuple = ()) ->
|
||||
out["원본"]["표지"] += qty.flags(out["로직"])
|
||||
except Exception: # noqa: BLE001 — 역색인 짓기 실패가 가져온 식 만들기를 막지 않음
|
||||
pass
|
||||
made = mm.attach(out, makes) # 821-4 못 이은 기계 = 내 기계(그 층 같은 것 · 새로)
|
||||
mine = {"내기계": made} if made else {}
|
||||
title = str(name or "").strip() or str(out["로직"].get("이름") or f"단산{no}")
|
||||
try:
|
||||
got = imp.create(out["로직"], out["원본"], title)
|
||||
return {**got, "다시씀": False}
|
||||
return {**got, "다시씀": False, **mine}
|
||||
except store.StoreError as e:
|
||||
same = (e.detail or {}).get("같은것") if isinstance(e.detail, dict) else None
|
||||
if e.status != 409 or not same:
|
||||
raise
|
||||
got = imp.create(out["로직"], out["원본"], title, use=same[0]["키"])
|
||||
return {**got, "다시씀": True}
|
||||
return {**got, "다시씀": True, **mine}
|
||||
|
||||
@@ -63,9 +63,10 @@ def _mf() -> Any:
|
||||
|
||||
|
||||
def _whole() -> Any:
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
"""M01 + 지금 층 내 기계(821-4 · 못 이은 기계를 이은 개인 키 `EQP…`)."""
|
||||
from M02_MasterTemplete import M02_MyMachine as mym
|
||||
|
||||
return m01.loaded()[1]
|
||||
return mym.whole()
|
||||
|
||||
|
||||
def is_imported(doc: Any) -> bool:
|
||||
@@ -80,7 +81,7 @@ def hourly_of(key: str) -> dict:
|
||||
"""이음 기계(EQ) 시간당 노 · 재 · 경 — M01 판이 바뀌면 다시(캐시 = M01 묶음 하나에 매임)."""
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as machine
|
||||
|
||||
whole = _whole()
|
||||
whole = _whole() # 내 기계 고침 = 색인이 바뀜 → 다시
|
||||
if _HOURLY["whole"] is not whole:
|
||||
_HOURLY.update(whole=whole, rows={})
|
||||
if key not in _HOURLY["rows"]:
|
||||
@@ -112,6 +113,8 @@ def row_of(doc: Any) -> dict | None:
|
||||
"참조"
|
||||
): # 안쪽 산근 = 형제 SK 1 단위 값(셈이 채움 · 엑셀 값묶음은 얼린 사본으로 둠)
|
||||
item |= {"요소": None, "값묶음": None, "기준": "우리" if ours else "엑셀"}
|
||||
if ours and not link and (mine := (one.get("자원") or {}).get("내기계")):
|
||||
_direct(item, one, mine) # 821-4 시간당 값 직접 내기계 = 그 줄 값
|
||||
if ours and link:
|
||||
item.pop("단가", None)
|
||||
item.pop("값묶음", None)
|
||||
@@ -124,6 +127,18 @@ def row_of(doc: Any) -> dict | None:
|
||||
return {**row, "키": doc.get("키"), "호표": items}
|
||||
|
||||
|
||||
def _direct(item: dict, one: dict, key: str) -> None:
|
||||
"""[우리 단가로] 이음 없이 이은 내 기계(시간당 값 직접 · 821-4) — 단가 = 그 비목 · 값묶음 = 셋 ·
|
||||
지운 내 기계 = 엑셀 값 그대로."""
|
||||
got = hourly_of(key)
|
||||
if not got.get("ok"):
|
||||
return
|
||||
if "단가" in item and one.get("비목") in COSTS:
|
||||
item["단가"] = sd.plain(Decimal(str(got[one["비목"]])))
|
||||
elif "값묶음" in item:
|
||||
item["값묶음"] = {k: sd.plain(Decimal(str(got[k]))) for k in COSTS}
|
||||
|
||||
|
||||
def base_row(doc: dict) -> dict[str, Any]:
|
||||
"""바탕 없는 SK 바탕 줄(`sd.empty_row`) — 가져온 식 = 그 줄 · 아니면 입력 · 줄 없음."""
|
||||
return row_of(doc) or {
|
||||
@@ -314,7 +329,7 @@ def unlinked(doc: dict) -> list[dict]:
|
||||
{"자리": ids[n], "이름": one.get("이름") or "", "까닭": link.why(one)}
|
||||
| ({"견적": True} if (one.get("자원") or {}).get("갈래") == "고정" else {})
|
||||
for n, one in enumerate(doc[KEY]["로직"].get("호표") or [])
|
||||
if not one.get("이음") and not one.get("참조")
|
||||
if not one.get("이음") and not one.get("참조") and not (one.get("자원") or {}).get("내기계")
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ export interface ImportedMeta {
|
||||
갈래?: string;
|
||||
엑셀금액?: Money4;
|
||||
글줄?: { 행?: number | null; 글?: string; 자리?: string; 값?: number | null }[];
|
||||
/** 못 이은 기계 → 내 기계(821-4 · 새로 = 이번에 만듦 · 아니면 그 층 같은 것) */
|
||||
내기계?: { 키: string; 이름: string; 규격?: string; 방식?: string; 새로?: boolean }[];
|
||||
/** 개인 층 밖에서 연 것 — 만들어질 내 기계(안 만듦) */
|
||||
내기계예정?: { 이름: string; 규격?: string }[];
|
||||
};
|
||||
검산?: {
|
||||
같음: boolean;
|
||||
|
||||
Reference in New Issue
Block a user