Merge remote-tracking branches 'origin/sub_laptop_2' and 'origin/sub_laptop_3' into sub_laptop_1
This commit is contained in:
@@ -106,6 +106,8 @@ interface Entry {
|
||||
/** 서버 요소조각의 첫 요소 — 자리 표시 든 줄이 입력으로 풀린 키 · 글 */
|
||||
unitRef?: string;
|
||||
unitText?: string;
|
||||
/** 자재 단위가 줄 단위와 성격이 다름 — 「줄 m · 자재 Ton」 */
|
||||
warn?: string;
|
||||
}
|
||||
|
||||
/** 재료 고르기 줄 — `요소` 가 글 대신 {구분·상세구분·규격·대표} 객체 */
|
||||
@@ -237,6 +239,7 @@ function entries(ctx: DetailContext): Entry[] {
|
||||
qtyFrag: [{ text: item.수량 }],
|
||||
price: price === undefined || price === null ? "" : formatPrice(price),
|
||||
amount: line ? line.금액 : null,
|
||||
warn: line?.단위경고,
|
||||
extra: false,
|
||||
open: (self) =>
|
||||
openMaterialModal({
|
||||
@@ -338,6 +341,14 @@ function lineRow(e: Entry): HTMLElement {
|
||||
? [el("div", { className: "m01-logic__tag", text: `${e.share} ${tl("Share")}` })]
|
||||
: []),
|
||||
...(e.spec ? [el("div", { className: "m01-logic__muted", text: e.spec })] : []),
|
||||
...(e.warn
|
||||
? [
|
||||
el("div", {
|
||||
className: "m01-logic__tag m01lab__warn",
|
||||
text: `${tx("Calc_UnitWarn")} · ${e.warn}`,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
cellOf(e.unit),
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import {
|
||||
fetchMaterials,
|
||||
searchElements,
|
||||
type ElementBrief,
|
||||
type NamedFormula,
|
||||
@@ -46,7 +45,18 @@ export interface PickCond {
|
||||
export const condText = (cond: PickCond): string =>
|
||||
["구분", "상세구분", "이름", "규격", "대표", "지역", "계약종별"]
|
||||
.map((k) => String(cond[k] ?? ""))
|
||||
.join("|");
|
||||
.join("|") + (cond["검색어"] ? `|${cond["검색어"]}` : "");
|
||||
|
||||
/** 재료 모달 검색 칸 글 — 검색어 첫 낱말의 괄호 앞(「합판(내수) 12t」 → 「합판」) */
|
||||
export const headWord = (text: unknown): string =>
|
||||
String(text ?? "")
|
||||
.trim()
|
||||
.split(/\s+/)[0]
|
||||
.replace(/[((].*$/, "");
|
||||
|
||||
/** 고르기 줄 후보 찾기 글 — 검색어 → 이름 → 상세구분 → 구분 차례 첫 낱말 */
|
||||
export const seedOf = (cond: PickCond): string =>
|
||||
[cond["검색어"], cond.이름, cond.상세구분, cond.구분].map(headWord).find(Boolean) ?? "";
|
||||
|
||||
export interface ModalTarget {
|
||||
title: string;
|
||||
@@ -227,19 +237,12 @@ const muted = (text: string): HTMLElement => el("p", { className: "m01-logic__mu
|
||||
/** 재료 고르기 줄 후보 목록 — 잡힌 품목(`pickedRef`)을 노란 줄로 · 후보가 100건을 넘으면 앞 100건만 옴 */
|
||||
function candidateView(
|
||||
cond: PickCond,
|
||||
unit: string,
|
||||
pickedRef: string,
|
||||
onPicked: (item: ElementBrief | null) => void,
|
||||
): HTMLElement {
|
||||
const box = el("div", { className: "m01lab__data", attrs: { "data-candidates": "1" } });
|
||||
box.append(muted("…"));
|
||||
void fetchMaterials({
|
||||
sub: String(cond["구분"] ?? ""),
|
||||
detail: String(cond["상세구분"] ?? ""),
|
||||
spec: String(cond["규격"] ?? ""),
|
||||
region: "",
|
||||
unit,
|
||||
})
|
||||
void searchElements("재료", seedOf(cond))
|
||||
.then((got) => {
|
||||
onPicked(got.items.find((it) => it.ref === pickedRef) ?? null);
|
||||
const rows = got.items.map((it) =>
|
||||
@@ -301,12 +304,17 @@ export function openMaterialModal(target: ModalTarget): void {
|
||||
const element = target.element ?? "";
|
||||
const base = element.split(".")[0];
|
||||
// 고르기 줄 = 서버가 조건 글을 열쇠로 준 줄(대표가 안 정해졌으면 없음)
|
||||
const priced = cond ? target.prices[condText(cond)] : undefined;
|
||||
const known = cond
|
||||
? target.prices[condText(cond)]
|
||||
? target.unitRef && priced?.ref !== target.unitRef
|
||||
? undefined // 서버가 잡은 품목이 다르면 그 품목으로
|
||||
: priced
|
||||
: element
|
||||
? (target.prices[element] ?? target.prices[base])
|
||||
: undefined;
|
||||
const priceRef = cond ? pickedOf(cond, target.values, known?.ref ?? "") : element;
|
||||
const priceRef = cond
|
||||
? target.unitRef || pickedOf(cond, target.values, known?.ref ?? "")
|
||||
: element;
|
||||
const logicKey = /^로직\((\w+)/.exec(element)?.[1] ?? "";
|
||||
const sum = el("p", { className: "m01lab__sum" });
|
||||
const setSum = (price: ElementBrief | null | undefined): void => {
|
||||
@@ -366,7 +374,7 @@ export function openMaterialModal(target: ModalTarget): void {
|
||||
rate,
|
||||
...(cond
|
||||
? [
|
||||
candidateView(cond, target.unit ?? "", priceRef, (item) => {
|
||||
candidateView(cond, priceRef, (item) => {
|
||||
if (known || !priceRef) return;
|
||||
const show = (brief: ElementBrief | null): void => {
|
||||
rate.replaceChildren(
|
||||
|
||||
@@ -90,6 +90,8 @@ export interface CalcLine {
|
||||
출처?: string;
|
||||
/** 자재 단위가 줄 단위와 성격이 달라 환산 못 하고 셈 — 「줄 m · 자재 Ton」 */
|
||||
단위경고?: string;
|
||||
/** 재료 줄이 잡은 품목 — 키 · 이름 · 규격 · 단위 · 값 칸 */
|
||||
품목?: { 키: string; 이름?: string; 규격?: string; 단위?: string; 값칸?: string };
|
||||
}
|
||||
export type CalcAnswer =
|
||||
| {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
machineControl,
|
||||
materialRows,
|
||||
pickBody,
|
||||
showItems,
|
||||
} from "./M01_MasterData_UI_Logic_CalcPick";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import { formatMoney } from "./M01_MasterData_UI_Logic_Money";
|
||||
@@ -183,6 +184,7 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
const answer = await runCalc(body);
|
||||
if (ctx.onText) ctx.onText(await runText(body));
|
||||
ctx.onLines(answer.ok ? (answer.lines ?? null) : null);
|
||||
if (answer.ok && answer.lines) showItems(host, ctx.values, answer.lines);
|
||||
out.replaceChildren(...answerView(answer, draft, (ctx.row.결과단위 ?? "").startsWith("원")));
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error");
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import { fetchRows, type Row } from "./M01_MasterData_Api_Fetch";
|
||||
import type { ElementBrief, LogicInput, LogicRow } from "./M01_MasterData_UI_Logic_Api";
|
||||
import type { CalcLine, ElementBrief, LogicInput, LogicRow } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { openPickTable } from "./M01_MasterData_UI_Logic_PickTable";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { condText, type PickCond } from "./M01_MasterData_UI_LogicLab_Modal";
|
||||
import { condText, headWord, type PickCond } from "./M01_MasterData_UI_LogicLab_Modal";
|
||||
|
||||
interface Chosen {
|
||||
ref: string;
|
||||
@@ -33,7 +33,6 @@ export function pickBody(values: Record<string, string>): { 고름?: Record<stri
|
||||
}
|
||||
|
||||
const nameOf = (row: Row): string => `${row["이름"] ?? ""} ${row["규격"] ?? ""}`.trim();
|
||||
const firstWord = (text: string): string => text.trim().split(/\s+/)[0] ?? "";
|
||||
|
||||
const MATERIAL_KEYS = /^MT\d{6}$/;
|
||||
const MACHINE_CODE = /^\d{4}-\d{4}$/;
|
||||
@@ -73,17 +72,18 @@ export function materialRows(
|
||||
? (item.요소 as unknown as PickCond)
|
||||
: null;
|
||||
const brief = (cond ? prices[condText(cond)] : prices[item.요소]) ?? null;
|
||||
const label = item.이름 ?? (brief?.이름 as string | undefined) ?? "";
|
||||
const named = item.이름 && !/^[A-Z]{2}\d{6}/.test(item.이름) ? item.이름 : "";
|
||||
const label = named || (brief?.이름 as string | undefined) || "";
|
||||
const held = mine.get(i);
|
||||
const nowRef = held?.ref ?? brief?.ref;
|
||||
const nowName =
|
||||
held?.label ?? (brief ? `${brief.이름 ?? ""} ${brief.규격 ?? ""}`.trim() : "") ?? "";
|
||||
const seeds = [cond?.["검색어"], cond?.이름, cond?.상세구분, cond?.구분, label]
|
||||
.map((w) => firstWord(String(w ?? "")))
|
||||
.map(headWord)
|
||||
.filter((w, k, all) => w && all.indexOf(w) === k);
|
||||
const current = el("span", {
|
||||
className: "m01-logic__muted",
|
||||
text: `${nowName || tx("Mat_NoPrice")} `,
|
||||
attrs: { "data-mat-idx": String(i), "data-ref": brief?.ref ?? "" },
|
||||
});
|
||||
const button = pickButton(tx("Pick_Item"), () =>
|
||||
openPickTable({
|
||||
@@ -91,7 +91,7 @@ export function materialRows(
|
||||
title: `${tx("Pick_MatTitle")} · ${label}`,
|
||||
seed: seeds[0] ?? "",
|
||||
alt: seeds.slice(1),
|
||||
isCurrent: (r) => String(r["키"]) === nowRef,
|
||||
isCurrent: (r) => String(r["키"]) === (mine.get(i)?.ref ?? current.dataset.ref),
|
||||
onPick: (r) => {
|
||||
mine.set(i, { ref: String(r["키"]), label: nameOf(r) });
|
||||
current.textContent = `${nameOf(r)} `;
|
||||
@@ -103,6 +103,22 @@ export function materialRows(
|
||||
});
|
||||
}
|
||||
|
||||
/** 계산 답의 `품목` 을 잡힌 품목 글로 — 고른 재료가 있는 줄은 그대로 */
|
||||
export function showItems(
|
||||
host: HTMLElement,
|
||||
values: Record<string, string>,
|
||||
lines: CalcLine[],
|
||||
): void {
|
||||
const mine = picksOf(values);
|
||||
host.querySelectorAll<HTMLElement>("[data-mat-idx]").forEach((span) => {
|
||||
const i = Number(span.dataset.matIdx);
|
||||
const item = lines[i]?.품목;
|
||||
if (!item || mine.has(i)) return;
|
||||
span.dataset.ref = item.키;
|
||||
span.textContent = `${`${item.이름 ?? ""} ${item.규격 ?? ""}`.trim() || item.키} `;
|
||||
});
|
||||
}
|
||||
|
||||
/** 기계 입력 칸 — 표 모달로 고르면 입력값(원문 분류번호 · 키 목록이면 키) */
|
||||
const machines = new Map<string, Row | null>();
|
||||
async function machineRow(code: string): Promise<Row | null> {
|
||||
|
||||
@@ -21,6 +21,7 @@ from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
import master_copy as mc
|
||||
import master_formula as mf
|
||||
import master_text_item as mti
|
||||
|
||||
PLACES = Decimal("0.0001") # 화면 `formatNumber` 와 같음(소수 넷째 자리까지)
|
||||
PRICE_TAIL = {"인력": "노임", "기계": "손료"} # 단가 자리의 이름 꼬리(그 밖은 「단가」)
|
||||
@@ -545,9 +546,9 @@ def lines(files: dict, key: str, given: dict) -> dict:
|
||||
if result is not None:
|
||||
shown = result["줄"]
|
||||
env["줄"] = {r["이름"]: r["금액"] for r in shown if r["이름"]}
|
||||
for item, line, label in zip(items, shown, labels):
|
||||
for n, (item, line, label) in enumerate(zip(items, shown, labels)):
|
||||
조각 = _formula_pieces(item["수량"], env, master, name_of, alias_by_name)
|
||||
요소 = _item_pieces(item, env, master)
|
||||
요소 = _item_pieces(item, env, master, line, n)
|
||||
for part, money in _shares(line):
|
||||
split = part if len(line["비목"]) > 1 else None
|
||||
one = {
|
||||
@@ -565,14 +566,14 @@ def lines(files: dict, key: str, given: dict) -> dict:
|
||||
끝수 = _cut_note(row)
|
||||
계 = result["계"]
|
||||
else:
|
||||
for item, label in zip(items, labels):
|
||||
for n, (item, label) in enumerate(zip(items, labels)):
|
||||
parts = (
|
||||
list(mf.COST_ITEMS)
|
||||
if item.get("종류") == "로직" and "비목" not in item
|
||||
else [item.get("비목")]
|
||||
)
|
||||
조각 = _formula_pieces(item["수량"], env, master, name_of, alias_by_name)
|
||||
요소 = _item_pieces(item, env, master)
|
||||
요소 = _item_pieces(item, env, master, None, n)
|
||||
for part in parts:
|
||||
if part not in mf.COST_ITEMS:
|
||||
continue
|
||||
@@ -628,13 +629,16 @@ def _arg_text(node, env: dict, master) -> str:
|
||||
return fmt(value)
|
||||
|
||||
|
||||
def _item_pieces(item: dict, env: dict, master) -> list[dict]:
|
||||
def _item_pieces(
|
||||
item: dict, env: dict, master, line: dict | None = None, index: int = 0
|
||||
) -> list[dict]:
|
||||
"""줄의 단가 자리(요소 · 로직 부르기) 조각 — PLAN 3-1 요소조각."""
|
||||
ref = item.get("요소")
|
||||
if item.get("종류") != "로직":
|
||||
name = str(item.get("이름") or "")
|
||||
if isinstance(ref, dict): # 재료 고르기 조건 — 품명만
|
||||
return [{"kind": "요소", "글": name}]
|
||||
if isinstance(ref, dict): # 재료 고르기 조건 — 잡힌 품목 이름 · 참조(고름 · 검색어 · 대표)
|
||||
found = mti.piece(item, line, index, env, master)
|
||||
return [found or {"kind": "요소", "글": name}]
|
||||
piece = _elem_piece(master, ref, env)
|
||||
return [{**piece, "글": name or piece["글"]}]
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""재료 줄 요소조각 — 잡힌 자재품목 이름 · 참조(PLAN 8-5). `master_text._item_pieces` 가 부름."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
import master_formula as mf
|
||||
import master_pick as mp
|
||||
|
||||
_ERRORS = (mf.FormulaError, KeyError, TypeError)
|
||||
|
||||
|
||||
def picked(item: dict, line: dict | None, index: int, env: dict, master) -> dict | None:
|
||||
"""재료 줄이 잡은 자재품목 줄 — 계산한 줄의 출처(`MT000018.거래가격`) 먼저 ·
|
||||
빈 줄이면 고름 · 검색어 · 대표로 다시 찾음. 못 찾으면 None."""
|
||||
got = str((line or {}).get("출처") or "")[:8]
|
||||
try:
|
||||
if got[:2] == "MT":
|
||||
return master.get(got)
|
||||
cond = {k: mf.fill(v, env) if isinstance(v, str) else v for k, v in item["요소"].items()}
|
||||
key = (mp.CHOSEN.get() or {}).get(str(index))
|
||||
if not key and cond.get("검색어"):
|
||||
key = mp.pick(master, cond, env, item.get("단위"))
|
||||
key = key or cond.get("대표")
|
||||
return master.get(str(key)) if key and str(key)[:2] == "MT" else None
|
||||
except _ERRORS:
|
||||
return None
|
||||
|
||||
|
||||
def piece(item: dict, line: dict | None, index: int, env: dict, master) -> dict | None:
|
||||
"""요소조각 — 글 = 품목 이름(지역 포함) · 참조 = 테이블 MT · 그 키. 못 찾으면 None."""
|
||||
row = picked(item, line, index, env, master)
|
||||
if row is None:
|
||||
return None
|
||||
key = str(row["키"])
|
||||
name = re.sub(r"\s+", " ", str(row.get("이름") or "")).strip()
|
||||
return {"kind": "요소", "글": name, "참조": {"테이블": key[:2], "키": key}}
|
||||
@@ -520,3 +520,27 @@ def test_표에_없는_입력_조합은_그_줄만_비우고_글은_그대로()
|
||||
assert len(blank) < len(lines) or got["계"] is not None
|
||||
calc = store.calc("GC001000", {**store.auto("GC001000")["값"], "시공구분": "기계시공"})
|
||||
assert got["계"] == calc["sums"]["계"]
|
||||
|
||||
|
||||
def _mat_piece(lines: list[dict], name: str) -> dict:
|
||||
return next(x for x in lines if x["이름"] == name)["요소조각"][0]
|
||||
|
||||
|
||||
def test_재료_줄_요소조각은_잡힌_품목_이름과_MT_참조() -> None:
|
||||
piece = _mat_piece(_text_of("GF000159"), "결속선 0.9㎜")
|
||||
assert piece["글"].startswith("어닐링철선(결속선)") and piece["글"].endswith("서울")
|
||||
assert piece["참조"] == {"테이블": "MT", "키": "MT000209"}
|
||||
auto = {k: v for k, v in store.auto("GF000159")["값"].items() if v is not None}
|
||||
assert store.text("GF000159", auto)["계"] == store.calc("GF000159", auto)["sums"]["계"]
|
||||
|
||||
|
||||
def test_고름으로_바꾼_재료_줄_요소조각은_고른_품목() -> None:
|
||||
import check_master as cm
|
||||
import master_pick as mp
|
||||
|
||||
auto = {k: v for k, v in store.auto("GF000159")["값"].items() if v is not None}
|
||||
items = cm.master().logic("GF000159")["호표"]
|
||||
at = next(i for i, h in enumerate(items) if h.get("이름") == "결속선 0.9㎜")
|
||||
with mp.choosing({at: "MT000174"}):
|
||||
got = _all_lines(store.text("GF000159", auto))
|
||||
assert _mat_piece(got, "결속선 0.9㎜")["참조"]["키"] == "MT000174"
|
||||
|
||||
Reference in New Issue
Block a user