Merge remote-tracking branches 'origin/sub_laptop_5', 'origin/sub_laptop_7' and 'origin/sub_laptop_8' into main_laptop_1
This commit is contained in:
@@ -16,6 +16,7 @@ from typing import Any
|
||||
|
||||
from B08_Quantity.B08_Quantity_Haul import bill_key, haul_groups
|
||||
from B08_Quantity.B08_Quantity_Material_Calc import _structure_row
|
||||
from M02_MasterTemplete.M02_Item_Source import item_name
|
||||
|
||||
WHY_EARTH = "조합 밖 토공 산근 — 연결 없음"
|
||||
WHY_READY = "준비공 대가 — 연결 없음"
|
||||
@@ -47,6 +48,20 @@ EARTH: list[tuple[str, str, str, tuple[str, str, str | None] | None]] = [
|
||||
#: 구조물 집계표 열 id 머리 → 공종(그 밖 = 2.2 배수공) · 커플링 = 자재라 뺌
|
||||
STRUCTURE_SECTION = {"pipe:revet|": "2.1", "pipe:chute|": "2.3", "pipe:wall|": "2.3"}
|
||||
SKIP_COLUMNS = ("pipe:cp|",)
|
||||
#: 층 이름 — 화면 낱말(번호 · 영문 층 이름은 화면에 안 냄)
|
||||
LAYER_WORD = {"system": "마스터", "company": "회사", "personal": "내 자료집", "project": "이 공사"}
|
||||
|
||||
|
||||
def combo_text(combo: str, names: dict[str, str]) -> str:
|
||||
"""조합 id 「{층}|{주인}|{키}」 → 「내 자료집 · 조합 이름」(이름을 못 읽으면 층 낱말만)."""
|
||||
layer, _, rest = combo.partition("|")
|
||||
owner, _, key = rest.partition("|")
|
||||
if combo not in names:
|
||||
try:
|
||||
names[combo] = item_name({"layer": layer, "owner": owner or None, "key": key})
|
||||
except (LookupError, KeyError, ValueError, OSError):
|
||||
names[combo] = ""
|
||||
return " · ".join(t for t in (LAYER_WORD.get(layer, ""), names[combo]) if t)
|
||||
|
||||
|
||||
def _same(row: dict[str, Any], want: tuple[str, str, str | None]) -> bool:
|
||||
@@ -80,13 +95,14 @@ def structure_items(summary: dict[str, Any] | None) -> list[dict[str, Any]]:
|
||||
columns = (summary or {}).get("columns") or []
|
||||
total = (summary or {}).get("total") or {}
|
||||
out = []
|
||||
names: dict[str, str] = {}
|
||||
for column in columns:
|
||||
cid = str(column.get("id") or "")
|
||||
if cid.startswith(SKIP_COLUMNS):
|
||||
continue
|
||||
section = next((s for head, s in STRUCTURE_SECTION.items() if cid.startswith(head)), "2.2")
|
||||
name, spec, unit = _structure_row(column)
|
||||
combos = [c for c in column.get("combos") or [] if c]
|
||||
combos = [combo_text(c, names) for c in column.get("combos") or [] if c]
|
||||
out.append(
|
||||
{
|
||||
"key": f"st:{cid}",
|
||||
|
||||
@@ -148,7 +148,7 @@ def suggest(rows: list[dict[str, Any]], memory: dict[str, dict[str, Any]]) -> li
|
||||
hit = _resolve(got, free) if got else None
|
||||
if hit:
|
||||
keys, tag = [hit], RECALL
|
||||
elif row["kind"] == "item":
|
||||
elif row["kind"] == "item" and not (memory.get(key_of(row)) or {}).get(FORGOT):
|
||||
keys, tag = [EARTH_KEY[n] for n in table(row["name"], row["spec"])], TABLE
|
||||
keys = [k for k in keys if k in free]
|
||||
else:
|
||||
|
||||
@@ -171,7 +171,13 @@ def ref_price(
|
||||
if row is None:
|
||||
return None
|
||||
no = f"대가{row['no']}" if tab == "dg" else f"산근 {row['no']}호표"
|
||||
return {"no": no, "tab": tab, "key": key, **{p: row.get(p) for p in (*PARTS, "sum")}}
|
||||
blank = tab == "dg" and row.get("blank") # 빈 줄 있는 대가(줄 0 개 포함) = 단가 모름
|
||||
return {
|
||||
"no": no,
|
||||
"tab": tab,
|
||||
"key": key,
|
||||
**{p: None if blank else row.get(p) for p in (*PARTS, "sum")},
|
||||
}
|
||||
return hopyo.get(key) if key else None
|
||||
|
||||
|
||||
|
||||
@@ -132,7 +132,8 @@ export function refPrice(ref: Row | null, books: Row, hopyo: Row, rowKey = ""):
|
||||
if (!row) return null;
|
||||
const no = tab === "dg" ? `대가${row.no}` : `산근 ${row.no}호표`;
|
||||
const out: Row = { no, tab, key };
|
||||
for (const p of [...PARTS, "sum"]) out[p] = row[p] ?? null;
|
||||
const blank = tab === "dg" && row.blank; // 빈 줄 있는 대가(줄 0 개 포함) = 단가 모름
|
||||
for (const p of [...PARTS, "sum"]) out[p] = blank ? null : (row[p] ?? null);
|
||||
return out;
|
||||
}
|
||||
return key ? (hopyo[key] ?? null) : null;
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
% 줄 = ROUNDDOWN(밑수 × % / 100, 1) · 관급 · 별도계상 자재 = 대가 안 0원 ·
|
||||
블록 비목 합 = ROUNDDOWN(Σ, 0) · 합계 = 세 비목 합.
|
||||
빈칸 = 단가 · 수량을 낸 원천이 없으면 None · 「연결 없음」 목록(gaps).
|
||||
빈 줄(166-6) = 블록 합 = 아는 줄 합 + `blank` n · `blank_names` — 내역 단가 · 부른 대가로는 안 넘김.
|
||||
빈 줄(166-6) = 블록 합 = 아는 줄 합 + `blank` n · `blank_names` — 내역 단가 · 부른 대가로는 안 넘김 ·
|
||||
줄 0 개 블록 = 빈 줄 하나(`NO_LINES`).
|
||||
고친 칸 덮개(702-2) = 사용자 값 `daega_cover` {블록 cover: {줄 cid: {qty · 노 · 재 · 경 단가}}} —
|
||||
있는 칸만 셈 값 대신 · 줄 `base`(품셈 값 = 셈 수량 · 단가) · `covered`(고친 칸) ·
|
||||
짝 잃은 덮개(그 블록 · 줄 없음) = `daega_orphans` [{cover, cid}] — 지우지 않고 알림만.
|
||||
@@ -36,6 +37,7 @@ from B09_Estimation.B09_Estimation_Calc_Base import SUPPLY, gap, num, rounddown,
|
||||
PARTS = ("labor", "material", "expense")
|
||||
COVER_KEYS = ("qty", *PARTS)
|
||||
TAB = "dg"
|
||||
NO_LINES = "구성 줄 없음" # 줄 0 개 일위대가의 빈 줄 이름
|
||||
COPY = "@copy"
|
||||
ADD = "@add"
|
||||
WHY = {
|
||||
@@ -158,6 +160,8 @@ def _block(
|
||||
known = [ln for ln in lines if ln["amount"] is not None]
|
||||
parts = {part: rounddown(sum(ln["amounts"][part] for ln in known), 0) for part in PARTS}
|
||||
blank_names = [ln.get("name") or "" for ln in lines if ln["amount"] is None]
|
||||
if not lines: # 줄 0 개 일위대가 = 금액 0 이 아니라 모름(빈 줄)
|
||||
blank_names = [NO_LINES]
|
||||
return {
|
||||
**{k: block.get(k) for k in ("id", "kind", "name", "spec", "unit", "cite", "cover")},
|
||||
**({"spec_raw": block["spec_raw"]} if block.get("spec_raw") else {}), # 157-4 풍선
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* 줄 금액 ROUNDDOWN(단가 × 수량, 1) · 블록 비목 합 ROUNDDOWN(Σ, 0) · 합계 = 세 비목 합
|
||||
* 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락에서 셈)
|
||||
* 빈칸 = 원천이 없으면 null · 「연결 없음」 목록(gaps)
|
||||
* 빈 줄(166-6) = 블록 합 = 아는 줄 합 + blank n · blank_names — 내역 단가 · 부른 대가로는 안 넘김
|
||||
* 빈 줄(166-6) = 블록 합 = 아는 줄 합 + blank n · blank_names — 내역 단가 · 부른 대가로는 안 넘김 ·
|
||||
* 줄 0 개 블록 = 빈 줄 하나(NO_LINES)
|
||||
* 고친 칸 덮개(702-2) = 사용자 값 daega_cover {블록 cover: {줄 cid: {qty · labor · material · expense}}} —
|
||||
* 있는 칸만 셈 값 대신 · 줄 base(품셈 값) · covered(고친 칸) · 짝 잃은 덮개 = daega_orphans(알림만)
|
||||
* 복사본(703-2) = 덮개 K:… 의 @copy {from · name · spec} — 원본 cover 블록을 본뜬 새 호표
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
const PARTS = ["labor", "material", "expense"] as const;
|
||||
const COVER_KEYS = ["qty", ...PARTS] as const;
|
||||
const TAB = "dg";
|
||||
const NO_LINES = "구성 줄 없음"; // 줄 0 개 일위대가의 빈 줄 이름(파이썬 짝)
|
||||
export const COPY = "@copy";
|
||||
/** 붙인 줄 덮개 열쇠(705-2) */
|
||||
export const ADD = "@add";
|
||||
@@ -153,7 +155,9 @@ function block(b: Row, books: Row, sums: Record<string, Row>, gaps: Gap[], own:
|
||||
known.reduce((acc, ln) => acc + ln.amounts[part], 0),
|
||||
0,
|
||||
);
|
||||
const blankNames = lines.filter((ln) => ln.amount === null).map((ln) => ln.name || "");
|
||||
const blankNames = lines.length
|
||||
? lines.filter((ln) => ln.amount === null).map((ln) => ln.name || "")
|
||||
: [NO_LINES]; // 줄 0 개 일위대가 = 금액 0 이 아니라 모름(빈 줄)
|
||||
return {
|
||||
id: b.id,
|
||||
key: b.id,
|
||||
|
||||
@@ -33,6 +33,7 @@ from typing import Any
|
||||
from B09_Estimation import B09_Estimation_Bill_Library as library
|
||||
from B09_Estimation import B09_Estimation_Sangeun as sg
|
||||
from B09_Estimation import B09_Estimation_Sangeun_Text as st
|
||||
from B09_Estimation.B09_Estimation_Bill_Items import LAYER_WORD
|
||||
from M02_MasterTemplete import M02_Item_Prices as item_prices
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
@@ -266,7 +267,8 @@ class Builder:
|
||||
{
|
||||
"kind": "사전조합",
|
||||
"name": doc.get("이름") or ref.get("키"),
|
||||
"spec": str(ref.get("키") or ""),
|
||||
"spec": LAYER_WORD.get(str(ref.get("층")), ""), # 번호(키)는 풍선 글로만
|
||||
"spec_raw": str(ref.get("키") or ""),
|
||||
"unit": doc.get("단위") or "",
|
||||
"cite": "",
|
||||
"origin": {"참조": refs.ref_id(ref)},
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "@ui/ui_template_context_menu";
|
||||
import type { Row } from "./B09_Estimation_Calc_Base";
|
||||
import type { TabContext, TabTable } from "./B09_Estimation_UI_Tabs";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { linkItems, type ApplyRef } from "./B09_Estimation_UI_Tabs_Bill_Link";
|
||||
import { linkMenu } from "./B09_Estimation_UI_Link";
|
||||
|
||||
@@ -208,6 +209,9 @@ export function editCell(
|
||||
put(ctx, w.bill);
|
||||
}
|
||||
|
||||
/** 단위 비교 꼴 — 띄어쓰기 · 대소문자 · 호환 글자(㎥ = m3) 무시 */
|
||||
const unitKey = (u: string): string => u.normalize("NFKC").replace(/\s+/g, "").toLowerCase();
|
||||
|
||||
/** 연결 적용 — 새 줄이 「새 줄」 · 빈 이름이면 고른 호표 명칭 · 규격 · 단위를 채움 */
|
||||
function applyRef(ctx: TabContext, rows: DataRow[], i: number): ApplyRef {
|
||||
return (ref, from) => {
|
||||
@@ -222,6 +226,10 @@ function applyRef(ctx: TabContext, rows: DataRow[], i: number): ApplyRef {
|
||||
e.unit = e.unit || from.unit || "";
|
||||
}
|
||||
put(ctx, w.bill);
|
||||
const mine = String(e.unit ?? row.unit ?? "");
|
||||
const theirs = String(from?.unit ?? "");
|
||||
if (ref && unitKey(mine) && unitKey(theirs) && unitKey(mine) !== unitKey(theirs))
|
||||
showToast(`단위 다름 ${mine} ↔ ${theirs} — 연결은 됨 · 수량 단위 확인`, "warning");
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ async function copyBlock(ctx: TabContext, raw: DataRow[], row: Row, block: Row):
|
||||
note: `「${block.name}」 을 이 공사 안 새 호표로 — 고친 칸까지 복사`,
|
||||
fields: [
|
||||
{ key: "name", label: "이름", value: `${block.name} 복사`, required: true },
|
||||
{ key: "spec", label: "규격", value: block.spec || "" },
|
||||
{ key: "spec", label: "규격", value: block.kind === "사전조합" ? "" : block.spec || "" },
|
||||
],
|
||||
okLabel: "만들기",
|
||||
closeLabel: "닫기",
|
||||
@@ -106,7 +106,7 @@ async function savePersonal(ctx: TabContext, block: Row): Promise<void> {
|
||||
const tail = left
|
||||
? ` · 안 담은 고친 칸 ${left}(품 ${skip.품} · 단가 ${skip.단가}) — 품은 고정 줄(PLAN 705) 뒤 담김`
|
||||
: "";
|
||||
showToast(`내 자료집에 저장 「${got.이름}」(${got.키})${tail}`, "success", 6000);
|
||||
showToast(`내 자료집에 저장 「${got.이름}」${tail}`, "success", 6000);
|
||||
return;
|
||||
}
|
||||
if (status !== 409 || !same) {
|
||||
@@ -116,7 +116,7 @@ async function savePersonal(ctx: TabContext, block: Row): Promise<void> {
|
||||
}
|
||||
const over = await askConfirm({
|
||||
title: "같은 이름",
|
||||
message: `내 자료집에 「${name.trim()}」(${same}) 이 있음 — 덮어쓰기 · 새 이름`,
|
||||
message: `내 자료집에 「${name.trim()}」 이 있음 — 덮어쓰기 · 새 이름`,
|
||||
okLabel: "덮어쓰기",
|
||||
cancelLabel: "새 이름",
|
||||
});
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
"""M02 기준 목록 · 계산서 끝수(PLAN 704-2 딸림) — B09 산근 끝수 규칙으로 셈(③ 서버 단독 ·
|
||||
`B09_Estimation_Sangeun` 과 규칙 짝 · 거울 시험 `test_704_settle`).
|
||||
|
||||
줄 금액 = ROUNDDOWN(단가 × 수량, 1) 비목마다 · 비목 총계 = ROUNDDOWN(Σ 줄, 0) · 계 = 셋 합 ·
|
||||
작업량 Q(중간 이름 Q · Q1 …) = ROUND(식, 2) 뒤 쓰임 · % 덧줄 밑수 = 그때까지 줄 합(줄 끝수 뒤).
|
||||
근거 = 지식DB `technical_info/01_임도/05_원가정보/단수처리_규칙.md`(행 금액 절사 · 관측 후보 규칙).
|
||||
M01 엔진(`master_formula.run`)의 구조 그대로 — 끝수 자리만 다름(엔진은 줄 끝수 없이 계에서 한 번).
|
||||
하위 로직(기계경비 포함)은 같은 규칙으로 다시 셈 · 흐름 · 결과 로직은 엔진 값 그대로.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import ROUND_DOWN, ROUND_HALF_UP, Decimal
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
mf = m01.cm.mf
|
||||
_Q = re.compile(r"Q\d*")
|
||||
|
||||
|
||||
def _down(value: Any, places: int) -> Any:
|
||||
return None if value is None else mf._round(Decimal(value), places, ROUND_DOWN)
|
||||
|
||||
|
||||
def _bundle(whole: Any, item: dict, env: dict, depth: int) -> dict:
|
||||
"""로직 호표 줄이 부르는 하위 로직 — 같은 규칙으로 다시 셈(엔진 `logic` 마디 그대로)."""
|
||||
node = mf.parse(item["요소"])
|
||||
if node[0] != "logic":
|
||||
return mf.evaluate(node, env, whole, depth)
|
||||
given = {k: mf.evaluate(v, env, whole, depth) for k, v in node[2]}
|
||||
got = settle(whole, mf.fill(node[1], env), given, depth + 1)
|
||||
return got["결과"] if "결과" in got else {k: got[k] for k in mf.BUNDLE}
|
||||
|
||||
|
||||
def settle(whole: Any, ref: str, given: dict, depth: int = 0) -> dict:
|
||||
"""`master_formula.run` 과 같은 꼴 `{줄, 노무비, 재료비, 경비, 계, 중간}` — 끝수만 B09 규칙."""
|
||||
row = whole.logic(ref)
|
||||
if isinstance(row.get("흐름"), dict) or "결과" in row:
|
||||
return mf.run(whole, ref, given, depth)
|
||||
env = mf._inputs(row, given)
|
||||
for step in row.get("중간", []):
|
||||
try:
|
||||
value = mf.evaluate(mf.parse(step["식"]), env, whole, depth)
|
||||
except mf.EmptyInput as e:
|
||||
env[step["이름"]] = mf.Blank(str(e))
|
||||
continue
|
||||
if _Q.fullmatch(step["이름"]) and isinstance(value, Decimal):
|
||||
value = value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
|
||||
env[step["이름"]] = value
|
||||
lines: list[dict] = []
|
||||
for n, item in enumerate(row.get("호표", [])):
|
||||
try:
|
||||
qty = mf.evaluate(mf.parse(item["수량"]), env, whole, depth)
|
||||
if item["종류"] == "로직":
|
||||
bundle = _bundle(whole, item, env, depth)
|
||||
price = bundle["계"]
|
||||
split = (
|
||||
{k: qty * bundle[k] for k in mf.COST_ITEMS}
|
||||
if "비목" not in item
|
||||
else {item["비목"]: qty * price}
|
||||
)
|
||||
else:
|
||||
ref_ = (
|
||||
{
|
||||
k: mf.fill(v, env) if isinstance(v, str) else v
|
||||
for k, v in item["요소"].items()
|
||||
}
|
||||
if isinstance(item["요소"], dict)
|
||||
else mf.fill(item["요소"], env)
|
||||
)
|
||||
if isinstance(ref_, str) and qty != 0:
|
||||
whole.get(ref_)
|
||||
price = mf.mp.priced(whole, item, ref_, env, n if depth == 0 else None)[0]
|
||||
if price is None and qty == 0:
|
||||
price = Decimal(0)
|
||||
split = {item["비목"]: None if price is None else qty * price}
|
||||
except mf.EmptyInput as e:
|
||||
lines.append(mf._blank_line(item, str(e)))
|
||||
continue
|
||||
lines.append(_line(item["이름"], split))
|
||||
sums = {k: sum((x["비목"].get(k) or 0 for x in lines), Decimal(0)) for k in mf.COST_ITEMS}
|
||||
env["줄"] = {x["이름"]: x["금액"] for x in lines if x["이름"] and x["금액"] is not None}
|
||||
for extra in row.get("덧줄", []):
|
||||
env.update(sums)
|
||||
try:
|
||||
value = mf.evaluate(mf.parse(extra["식"]), env, whole, depth)
|
||||
env[extra["이름"]] = value
|
||||
except mf.FormulaError:
|
||||
value = None
|
||||
sums[extra["비목"]] += _down(value, 1) or 0
|
||||
lines.append(_line(extra["이름"], {extra["비목"]: value}))
|
||||
if value is not None:
|
||||
env["줄"][extra["이름"]] = lines[-1]["금액"]
|
||||
sums = {k: mf.cut(row, _down(v, 0), "계") for k, v in sums.items()}
|
||||
return {"줄": lines, **sums, "계": sum(sums.values()), "중간": mf._shown(env)}
|
||||
|
||||
|
||||
def _line(name: str, split: dict) -> dict:
|
||||
cut = {k: _down(v, 1) for k, v in split.items()}
|
||||
whole_sum = None if all(v is None for v in cut.values()) else sum(v or 0 for v in cut.values())
|
||||
return {"이름": name, "비목": cut, "금액": whole_sum}
|
||||
|
||||
|
||||
def apply(groups: list[dict], engine: dict, done: dict) -> None:
|
||||
"""계산서 묶음의 줄 금액 · 소계를 `settle` 값으로 바꿈 — 줄 차례는 엔진과 같음."""
|
||||
seq: dict[str, list] = {k: [] for k in mf.COST_ITEMS}
|
||||
for eng, mine in zip(engine["줄"], done["줄"]):
|
||||
for part, _ in m01.mtx._shares(eng):
|
||||
seq[part].append(mine["비목"].get(part))
|
||||
for group in groups:
|
||||
for line, amount in zip(group["줄"], seq.get(group["비목"], [])):
|
||||
line["금액"] = amount
|
||||
group["소계"] = done[group["비목"]]
|
||||
@@ -5,12 +5,13 @@
|
||||
위 끝) · 그 밖 연속 값 입력은 견본 자동값(`master_auto.values`) 한 줄 · 지역 = 자동값(전국평균).
|
||||
펼침 = 바탕(자동값 · 규격 축은 첫 대표 값) 한 줄 + 규격 축마다 값 하나씩 바꾼 줄(곱하지 않음 ·
|
||||
바꾼 줄이 0 원이면 표에 없는 조합으로 보고 뺌).
|
||||
금액 = M01 엔진(`master_formula.run`) 「계」 — M01 시험 계산과 같은 셈 · 최신 판 노임(마스터 판).
|
||||
금액 = M01 엔진 식 + B09 산근 끝수(줄마다 버림 · `M02_Logic_Settle` · 근거 지식DB
|
||||
`technical_info/01_임도/05_원가정보/단수처리_규칙.md`) · 최신 판 노임(마스터 판).
|
||||
목록은 서버가 뒤에서 한 번 셈(프로세스 `WORKERS` 개 · 수 분) → 프로세스 메모리 + 파생 저장본
|
||||
(storage `_derived/m02_logic_standard/`) · 열쇠 = 마스터 내용 지문 + `SHAPE` · 같으면 안 셈 ·
|
||||
셈 도중 목록 = 빈 줄 `준비: false`(화면은 로직 줄 그대로 · 금액 빈칸).
|
||||
실무 낱말(`WORDS`) = 별칭 칸에 덧붙여 찾기(공용 `matchLogic`)가 「석축」 → 돌쌓기를 찾음.
|
||||
계산서 = `master_text.lines`(줄 「석공 | 268,908 * 0.1 = 26,891」 · 비목 소계 · 계 = 엔진 값).
|
||||
계산서 = `master_text.lines`(줄 「석공 | 268,908 * 0.1 = 26,891」 · 비목 소계 · 계) + 같은 끝수 값.
|
||||
M01 은 읽기만 · 권한은 부르는 쪽 몫(로그인).
|
||||
"""
|
||||
|
||||
@@ -27,9 +28,10 @@ from typing import Any
|
||||
from common_util.common_util_derived_store import derived_key, read_derived, write_derived
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs
|
||||
from M02_MasterTemplete import M02_Logic_Settle as settle
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
SHAPE = 2 # 줄 꼴이 바뀌면 올림 — 옛 저장본을 안 씀
|
||||
SHAPE = 3 # 줄 꼴이 바뀌면 올림 — 옛 저장본을 안 씀
|
||||
KIND = "m02_logic_standard"
|
||||
REGION = "지역" # 지역 = 자동값 한 벌(정함 8)
|
||||
QUIET = ("해당 없음", "없음") # 규격 글에서 뺄 값
|
||||
@@ -83,7 +85,10 @@ def kind_of(row: dict) -> str:
|
||||
|
||||
def words_of(row: dict) -> list[str]:
|
||||
name = _sq(row.get("이름"))
|
||||
return [w for w, targets in WORDS.items() if any(_sq(t) in name for t in targets)]
|
||||
out = [w for w, targets in WORDS.items() if any(_sq(t) in name for t in targets)]
|
||||
if "터파기" in name: # 내역서 낱말 「측구파기」 ↔ 로직 「측구터파기」
|
||||
out.append(str(row.get("이름")).replace("터파기", "파기"))
|
||||
return out
|
||||
|
||||
|
||||
def _range_label(table: dict, low: Any, high: Any, unit: str) -> str:
|
||||
@@ -168,7 +173,7 @@ def _money(whole: Any, logic: dict, given: dict) -> Decimal | None:
|
||||
if not str(logic.get("결과단위") or "").startswith("원"):
|
||||
return None
|
||||
try:
|
||||
got = m01.cm.mf.run(whole, str(logic["키"]), m01.cm.mf.fill_empty(logic, given))
|
||||
got = settle.settle(whole, str(logic["키"]), m01.cm.mf.fill_empty(logic, given))
|
||||
except (m01.cm.mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError):
|
||||
return None
|
||||
return got.get("계", got.get("결과"))
|
||||
@@ -325,7 +330,12 @@ def sheet(key: str, change: dict) -> dict:
|
||||
except m01.cm.mf.FormulaError as e:
|
||||
raise m01.StoreError(404, str(e)) from e
|
||||
given, ax = _given(whole, logic, change)
|
||||
got = m01.mtx.lines(files, key, m01.cm.mf.fill_empty(logic, given))
|
||||
filled = m01.cm.mf.fill_empty(logic, given)
|
||||
got = m01.mtx.lines(files, key, filled)
|
||||
done = settle.settle(whole, key, filled)
|
||||
if "결과" not in done: # 돈 로직 — 줄마다 버림(B09 산근 규칙)
|
||||
settle.apply(got.get("groups") or [], m01.cm.mf.run(whole, key, filled), done)
|
||||
got["계"] = done["계"]
|
||||
groups = [
|
||||
{
|
||||
"비목": g["비목"],
|
||||
|
||||
@@ -455,7 +455,7 @@ export function mountLaborDetail(
|
||||
if (!ref) return;
|
||||
void openRefEdit({
|
||||
ref,
|
||||
title: `${ref.키} ${lineOf(lineId({ 참조: ref }) ?? "")?.이름 ?? ""}`.trim(),
|
||||
title: lineOf(lineId({ 참조: ref }) ?? "")?.이름 ?? "",
|
||||
readOnly: !edit || ref.층 !== opt.layer,
|
||||
closed: () => alive && recalc(),
|
||||
});
|
||||
@@ -466,7 +466,7 @@ export function mountLaborDetail(
|
||||
const row = cur.담은로직[i];
|
||||
const line = lineOf(lineId(row) ?? "");
|
||||
openQtyFor(
|
||||
`${row.참조?.키 ?? row.로직} ${(row.참조 ? line?.이름 : logics.get(row.로직)?.이름) ?? ""}`.trim(),
|
||||
`${row.참조 ? "" : row.로직} ${(row.참조 ? line?.이름 : logics.get(row.로직)?.이름) ?? ""}`.trim(),
|
||||
row,
|
||||
line,
|
||||
line?.분모 ?? "",
|
||||
@@ -491,7 +491,7 @@ export function mountLaborDetail(
|
||||
pick: (ref: LaborRef, name: string, 별칭: string | null = null) => {
|
||||
const id = lineId({ 참조: ref });
|
||||
if (cur.담은로직.some((r) => lineId(r) === id))
|
||||
return showToast(tc("Labor_AlreadyRef").replace("{value}", `${ref.키} ${name}`), "error");
|
||||
return showToast(tc("Labor_AlreadyRef").replace("{value}", name), "error");
|
||||
const 수량 = lib ? { 손: 1 } : null; // 사전조합 줄 기본 1(51-6)
|
||||
cur.담은로직.push({ 로직: "", 참조: ref, 별칭, 메모: null, 입력값: {}, 수량 });
|
||||
changed();
|
||||
|
||||
@@ -34,6 +34,13 @@ const money = (m: LaborCost | null | undefined, times = 1): HopyoMoney =>
|
||||
}
|
||||
: {};
|
||||
|
||||
/** 층 낱말 — 번호(키)는 화면 글에 안 냄 */
|
||||
const layerWord = (layer: string): string =>
|
||||
layer === "personal" ? tc("Grid_Mine") : tc(layer === "company" ? "Lib_Company" : "Lib_System");
|
||||
|
||||
/** 일위대가번 — 번호꼴(SU-000011 · SU01010101)은 사람 글이 아니라 뺌 */
|
||||
const humanNo = (no?: string): string => (no && !/^S[A-Z]\d?-?\d{6,8}$/.test(no) ? no : "");
|
||||
|
||||
/** 문서 + calc 답 → 호표 하나 — calc 없으면(계산 중) 금액 빈 칸 · 빈 줄 0 */
|
||||
export function laborBlock(
|
||||
doc: LaborDoc,
|
||||
@@ -68,7 +75,9 @@ export function laborBlock(
|
||||
unit: line?.분모 ?? (row.참조 ? "" : (logic?.결과단위 ?? "")),
|
||||
prices: money(line?.기준, line?.배 || 1),
|
||||
amounts: money(line?.반영),
|
||||
note: line?.까닭 ?? (calc && qty === null ? tc("Grid_NoQty") : (row.참조?.키 ?? row.로직)),
|
||||
note:
|
||||
line?.까닭 ??
|
||||
(calc && qty === null ? tc("Grid_NoQty") : row.참조 ? layerWord(row.참조.층) : row.로직),
|
||||
tag: HOPYO_TAG.pum,
|
||||
};
|
||||
});
|
||||
@@ -96,7 +105,7 @@ export function laborBlock(
|
||||
const lines = [...logicLines, ...matLines];
|
||||
const block: HopyoBlock = {
|
||||
key: doc.열,
|
||||
name: doc.일위대가번 || doc.열,
|
||||
name: humanNo(doc.일위대가번) || (doc as { 이름?: string }).이름 || tc("Grid_Head"),
|
||||
unit: doc.단위 ?? "",
|
||||
total: money(calc?.반영합),
|
||||
lines,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
|
||||
export interface RefEditOptions {
|
||||
ref: LaborRef;
|
||||
/** 「SC-000001 돌붙임」 */
|
||||
/** 「돌붙임」 */
|
||||
title: string;
|
||||
readOnly: boolean;
|
||||
closed: () => void;
|
||||
|
||||
@@ -179,6 +179,8 @@ const TEXT = {
|
||||
Grid_Apart: ["별도계상 · 합계 밖", "Separate · not in total"],
|
||||
Grid_Linked: ["이 공사 단가에 이음", "Linked to project price"],
|
||||
Grid_Hand: ["손 단가(붙인 값)", "Hand price (pasted)"],
|
||||
Grid_Head: ["일위대가", "Unit price"],
|
||||
Grid_Mine: ["내 자료집", "My library"],
|
||||
} as const satisfies Record<string, readonly [string, string]>;
|
||||
|
||||
export type ComboTextKey = keyof typeof TEXT;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 회사에 올리기 요청 · 승인(PLAN 706-1) — 서버 `M02_Share_Request.py` 짝(장부 = 회사 층 파일 하나).
|
||||
* 일반 사용자 = 자기 개인 층 구조물 조합 · 사전조합 나무 오른쪽 클릭 「회사에 올리기 요청」 → 글(선택) ·
|
||||
* 같은 것 요청이 남아 있으면 「요청 중」(흐림) · 결과는 다음에 M02 열 때 위 띠(닫으면 봄)
|
||||
* 회사 관리자 = 회사 층 위 띠 「올리기 요청 n」 → 목록(요청자 · 항목 · 날짜 · 글) · [미리 보기] ·
|
||||
* 회사 관리자 = M02 위 띠 「올리기 요청 n」(어느 층이든) → 목록(요청자 · 항목 · 날짜 · 글) · [미리 보기] ·
|
||||
* [승인](폴더 고름 = 「회사로 복사…」와 같은 길 · 원본 그대로) · [거절](사유 한 줄)
|
||||
* 관리자(회사 층을 고칠 수 있는 사람)는 「회사로 복사…」가 있어 요청 메뉴를 안 둠.
|
||||
* ========================================================================== */
|
||||
@@ -50,7 +50,7 @@ export interface ShareOptions {
|
||||
user: SessionUser | null;
|
||||
/** 볼 회사(시스템 관리자는 고른 회사) */
|
||||
company: string | null;
|
||||
/** 지금 페이지 층 — 회사 층이면 관리자 띠 */
|
||||
/** 지금 페이지 층 — 개인 층이면 요청 메뉴 */
|
||||
layer: Layer;
|
||||
owner: string | null;
|
||||
showView: (label: string, node: HTMLElement) => void;
|
||||
@@ -77,10 +77,7 @@ export function createShare(opt: ShareOptions): ShareHandle {
|
||||
// 요청할 사람 = 회사가 있고 회사 층을 못 고침(고칠 수 있으면 「회사로 복사…」)
|
||||
const asker =
|
||||
!!ownOwner("personal", opt.user) && !accessFor("company", companyOwner, opt.user).canWrite;
|
||||
const boss =
|
||||
opt.layer === "company" &&
|
||||
!!companyOwner &&
|
||||
accessFor("company", companyOwner, opt.user).canWrite;
|
||||
const boss = !!companyOwner && accessFor("company", companyOwner, opt.user).canWrite;
|
||||
const q = opt.company ? `?company=${encodeURIComponent(opt.company)}` : "";
|
||||
let rows: ShareRow[] = [];
|
||||
let listHost: HTMLElement | null = null;
|
||||
|
||||
@@ -157,11 +157,15 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise<Mounte
|
||||
go: (next) => void go("personal", next, open),
|
||||
})
|
||||
: panel;
|
||||
// 704-4 [구조물 조합으로] 만든 항목 → 나무 · 옆 목록 다시(페이지가 내려가면 듣기 끝)
|
||||
// 704-4 [구조물 조합으로] 만든 항목 → 나무 · 옆 목록 · 열린 표(행렬)를 다시(페이지가 내려가면 듣기 끝)
|
||||
const afterItem = async (): Promise<void> => {
|
||||
await Promise.all([side.refresh(), tree.refresh()]);
|
||||
const now = main.current();
|
||||
const shown = now?.kind === "table" && now.name === STRUCTURE_TABLE;
|
||||
if (shown && (await main.settle()) !== false) await main.openTable(layer, null, true);
|
||||
};
|
||||
const onItem = (): void =>
|
||||
tree.root.isConnected
|
||||
? void Promise.all([side.refresh(), tree.refresh()])
|
||||
: document.removeEventListener(ITEM_BUILT, onItem);
|
||||
tree.root.isConnected ? void afterItem() : document.removeEventListener(ITEM_BUILT, onItem);
|
||||
document.addEventListener(ITEM_BUILT, onItem);
|
||||
const main = createMain(
|
||||
isAdmin,
|
||||
|
||||
@@ -44,3 +44,9 @@
|
||||
2. 보여주는 꼴은 익숙한게 좋을수 있음.
|
||||
3. 전체 신규계획과 요구서를 작성하여 진행.
|
||||
4. 조심해야될 부분은 우리 프로그램은 후발주자로서 다른 경쟁사의 프로그램 결과물을 불러오기가 되어야할것같음. 일단 현재도 일부는 되어 있지만 불편함. 매칭이 쉽지않음.
|
||||
|
||||
## 내역 9 (10-31 · 사용자 · 통합 점검 아티팩트 보고 뒤)
|
||||
|
||||
1. 원가계산서의 현행 비목의 요율은 다른 프로그램에서도 반영한거야?
|
||||
2. 저장속도를 개선되기만 하면 좋지.
|
||||
3. 브레인의 종료는 내가 정함. 이어서 진행
|
||||
|
||||
@@ -196,7 +196,7 @@ def test_새_산근_사본_뒤_번호_호표_탭_집계표(tmp_path):
|
||||
|
||||
def test_우클릭_메뉴_항목_추가_줄_삭제_연결_TS():
|
||||
# 184-3 — 모든 줄 메뉴 · 셈 출력 줄 = 계약 꼴(parent · auto · auto_{칸}) · 첫 손질 = v2 로 굳힘
|
||||
src = _ts(["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit"])
|
||||
src = _ts(["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Link", "UI_Tabs_Bill_Edit"])
|
||||
rows = [
|
||||
{"kind": "title", "key": "sec:1", "parent": None, "auto": True},
|
||||
{"kind": "title", "key": "sec:1.2", "parent": "sec:1", "auto": True},
|
||||
@@ -243,3 +243,26 @@ console.log(JSON.stringify(out));
|
||||
# 새 줄 지움 = 덮개에서 빠짐(gone 은 자동 줄만)
|
||||
assert [e["key"] for e in out["left"]["rows"]] == ["sec:1", "sec:1.2", "earth:4"]
|
||||
assert out["left"]["gone"] == []
|
||||
|
||||
|
||||
def test_줄_0개_일위대가_이으면_빈_줄():
|
||||
"""703-1 가져오기 흠 — 줄 0 개 대가(금액 0)를 이은 내역 줄 = 금액 0 이 아니라 빈 줄."""
|
||||
empty = {"no": 4, "key": "DG:e", "labor": 0, "material": 0, "expense": 0, "sum": 0,
|
||||
"blank": 1} # fmt: skip
|
||||
books = {**BOOKS, "daega": [*BOOKS["daega"], empty]}
|
||||
hand = {"key": "hand:e", "section": "1.1", "after": "earth:1", "name": "빈 대가", "spec": "",
|
||||
"unit": "㎡", "qty": 3, "ref": {"tab": "dg", "key": "DG:e"}} # fmt: skip
|
||||
user = {"bill": {"rows": [hand]}}
|
||||
sheets = compute_estimation(INPUTS, user, None, books)
|
||||
row = _rows(sheets)["hand:e"]
|
||||
assert row["sum"] is None and row["note"] == "대가4"
|
||||
ok = _rows(compute_estimation(INPUTS, {"bill": {"rows": [HAND[0]]}}, None, books))
|
||||
assert ok["hand:a"]["sum"] == 12300 # 빈 줄 없는 대가는 그대로
|
||||
calc = _ts(["Calc_Base", "Calc_Bill"])
|
||||
body = (
|
||||
f"\nconst i = {json.dumps(INPUTS, ensure_ascii=False)};"
|
||||
f"\nconst u = {json.dumps(user, ensure_ascii=False)};"
|
||||
f"\nconst b = {json.dumps(books, ensure_ascii=False)};"
|
||||
"\nconsole.log(JSON.stringify(computeEstimation(i, u, null, b)));"
|
||||
)
|
||||
assert json.loads(_node(calc + body)) == sheets # 거울
|
||||
|
||||
@@ -218,7 +218,9 @@ def test_우클릭_레벨_메뉴_TS():
|
||||
body = MENU_JS.replace("__ROWS__", json.dumps(rows, ensure_ascii=False)).replace(
|
||||
"__USER__", json.dumps(USER, ensure_ascii=False)
|
||||
)
|
||||
out = json.loads(_node(_ts(["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit"]) + body))
|
||||
out = json.loads(
|
||||
_node(_ts(["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Link", "UI_Tabs_Bill_Edit"]) + body)
|
||||
)
|
||||
level = {"묶음 줄로 바꾸기", "항목으로 바꾸기", "들여쓰기 →", "내어쓰기 ←"}
|
||||
pick = lambda key: [x for x in out[key] if x in level] # noqa: E731
|
||||
assert [x for x in out["g1"] if "추가" in x] == ["항목 추가"] # 246-1 더함 하나
|
||||
@@ -312,7 +314,7 @@ def test_연결_단추_칸_메뉴_고르기_창_TS():
|
||||
.replace("__BILL__", json.dumps(sheets["bill"], ensure_ascii=False))
|
||||
.replace("__DAEGA__", json.dumps(BOOKS["daega"], ensure_ascii=False))
|
||||
)
|
||||
src = _ts(["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill"])
|
||||
src = _ts(["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill"])
|
||||
out = json.loads(_node(src + body))
|
||||
assert out["columns"] == ["connect", "note"] # 비고 앞 「연결」 칸
|
||||
# 184-3 모든 항목 줄 · 새 줄 연결됨 = 호표 번호 · 자동 줄 연결됨 = 「연결됨」 · 묶음 = 없음
|
||||
@@ -357,7 +359,7 @@ console.log(JSON.stringify(out));
|
||||
|
||||
|
||||
def test_글_칸_위_우클릭도_줄_메뉴_TS():
|
||||
src = _ts(["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill"])
|
||||
src = _ts(["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill"])
|
||||
out = json.loads(_node(src + ROW_MENU_JS))
|
||||
assert out["onInput"] is True and out["onCell"] is True
|
||||
assert out["opened"] == [[5, 6, ["줄 7"]], [5, 6, ["줄 7"]]]
|
||||
|
||||
@@ -95,7 +95,7 @@ def _ts(names: list[str]) -> str:
|
||||
)
|
||||
|
||||
|
||||
SRC = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit"]
|
||||
SRC = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Link", "UI_Tabs_Bill_Edit"]
|
||||
|
||||
|
||||
def _menu(user, labels=(), runs=(), edits=(), rows=ROWS):
|
||||
|
||||
@@ -124,7 +124,7 @@ out.keyAct = [
|
||||
console.log(JSON.stringify(out));
|
||||
"""
|
||||
|
||||
SRC = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill", "UI_Tabs_Bill_Drag",
|
||||
SRC = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill", "UI_Tabs_Bill_Drag",
|
||||
"UI_Tabs_Bill_Level"] # fmt: skip
|
||||
IN, OUT, UP, DOWN = "들여쓰기 →", "내어쓰기 ←", "위로 ↑", "아래로 ↓"
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ if str(ROOT) not in sys.path:
|
||||
from B09_Estimation import B09_Estimation_Store as store # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Calc_Bill import compute_estimation # noqa: E402
|
||||
|
||||
SRC = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill", "UI_Tabs_Bill_Drag",
|
||||
SRC = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill", "UI_Tabs_Bill_Drag",
|
||||
"UI_Tabs_Bill_Level"] # fmt: skip
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ from B09_Estimation.B09_Estimation_Calc_Bill import compute_estimation # noqa:
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\r?\n", re.MULTILINE)
|
||||
TABLE = IMPORTS.sub("", (ROOT / "ui_template" / "ui_template_data_table.ts").read_text("utf-8"))
|
||||
SRC = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill", "UI_Tabs_Bill_Drag",
|
||||
SRC = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill", "UI_Tabs_Bill_Drag",
|
||||
"UI_Tabs_Bill_Level"] # fmt: skip
|
||||
IN, OUT = "들여쓰기 →", "내어쓰기 ←"
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from B09_Estimation.B09_Estimation_Calc_Bill import compute_estimation # noqa:
|
||||
|
||||
IMPORTS = re.compile(r"^import[^;]*;\r?\n", re.MULTILINE)
|
||||
TABLE = IMPORTS.sub("", (ROOT / "ui_template" / "ui_template_data_table.ts").read_text("utf-8"))
|
||||
SRC = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill"]
|
||||
SRC = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill"]
|
||||
SUMS = ["sum", "labor", "material", "expense"]
|
||||
FOLDER_SVG, LEAF_SVG = "M1.5 3.5h4", "M3.5 1.5h6"
|
||||
|
||||
@@ -303,7 +303,7 @@ def test_고른_줄_Enter_F2_입력_시작():
|
||||
"bar = levelBar(root, table);", "bar = levelBar(root, table, (i) => edits.push(i));"
|
||||
)
|
||||
body = (setup + KEYS_JS).replace("__ROWS__", json.dumps(FAKE_ROWS, ensure_ascii=False))
|
||||
names = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill",
|
||||
names = ["UI_Tabs", "UI_Tabs_Bill_Link", "UI_Link", "UI_Tabs_Bill_Edit", "UI_Tabs_Bill",
|
||||
"UI_Tabs_Bill_Drag", "UI_Tabs_Bill_Level"] # fmt: skip
|
||||
out = json.loads(_node(_ts(names) + body))
|
||||
at = [r["key"] for r in FAKE_ROWS].index("earth:5")
|
||||
|
||||
@@ -125,4 +125,4 @@ def test_B09_금액_길이_곱하기_대가(out):
|
||||
inputs = {"bill": {"items": structure_items(out)}}
|
||||
sheets = compute_estimation(inputs, {}, {"s-wall": price}.get)
|
||||
row = next(r for r in sheets["bill"]["rows"] if r["key"].endswith("#blk:w"))
|
||||
assert (row["qty"], row["sum"], row["note"]) == (12.0, 1495128, "대가1 · system||SS000201")
|
||||
assert (row["qty"], row["sum"], row["note"]) == (12.0, 1495128, "대가1 · 마스터")
|
||||
|
||||
@@ -58,7 +58,7 @@ def test_머리_구성_합계_줄_꼬리표_빈_줄():
|
||||
|
||||
|
||||
STUBS = r"""
|
||||
const tc = (k) => ({ Grid_NoQty: "수량 없음", Grid_Apart: "별도계상 · 합계 밖", Labor_Layer: "켜쌓기 {n}%" })[k] ?? k;
|
||||
const tc = (k) => ({ Grid_NoQty: "수량 없음", Grid_Apart: "별도계상 · 합계 밖", Labor_Layer: "켜쌓기 {n}%", Grid_Head: "일위대가", Grid_Mine: "내 자료집", Lib_Company: "회사", Lib_System: "마스터" })[k] ?? k;
|
||||
const valueLine = (inputs, values) => ({ text: Object.values(values ?? {}).join(" · "), empty: 0 });
|
||||
"""
|
||||
|
||||
@@ -108,7 +108,7 @@ def test_M02_문서_calc_를_격자_줄로():
|
||||
"\nconsole.log(JSON.stringify({ b, foot: hopyoGrid([b]).rows.at(-1), wait: hopyoGrid([wait]).rows.at(-1) }));"
|
||||
), STUBS) # fmt: skip
|
||||
b = out["b"]
|
||||
assert b["name"] == "SU-000006" and b["unit"] == "m" and b["total"]["sum"] == 112559.55
|
||||
assert b["name"] == "일위대가" and b["unit"] == "m" and b["total"]["sum"] == 112559.55
|
||||
one, two, mt1, mt2 = b["lines"]
|
||||
assert (
|
||||
one["name"] == "돌쌓기 찰쌓기"
|
||||
@@ -141,3 +141,19 @@ def test_세_곳_같은_부품():
|
||||
assert '"@ui/ui_template_hopyo_grid"' in m02 and '"@ui/ui_template_hopyo_grid"' in b09
|
||||
kit = (ROOT / "ui_template/ui_template_ui_kit_parts.ts").read_text("utf-8")
|
||||
assert 'key: "hopyo-grid"' in kit # 부품 모음 견본
|
||||
|
||||
|
||||
def test_번호는_글에_안_냄_사람_글은_그대로():
|
||||
files = [GRID, "M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts",
|
||||
"M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts",
|
||||
"M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts"] # fmt: skip
|
||||
ref = {"로직": "", "별칭": None, "입력값": {}, "수량": {"손": 1},
|
||||
"참조": {"층": "personal", "주인": "1/3", "키": "SC-000002"}} # fmt: skip
|
||||
named = {**DOC, "일위대가번": "제5호표", "담은로직": [ref]}
|
||||
out = _node(files, (
|
||||
f"\nconst a = laborBlock({json.dumps(named, ensure_ascii=False)}, null, new Map());"
|
||||
f"\nconst c = laborBlock({json.dumps({**named, '일위대가번': 'SP3-000015'}, ensure_ascii=False)}, null, new Map());"
|
||||
"\nconsole.log(JSON.stringify({ a, c }));"
|
||||
), STUBS) # fmt: skip
|
||||
assert out["a"]["name"] == "제5호표" and out["c"]["name"] == "일위대가"
|
||||
assert out["a"]["lines"][0]["note"] == "내 자료집"
|
||||
|
||||
@@ -34,13 +34,14 @@ def _given(key: str, change: dict) -> dict:
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", THREE)
|
||||
def test_줄_금액은_M01_시험_계산(key: str) -> None:
|
||||
def test_줄_금액은_M01_시험_계산에_끝수만_버림(key: str) -> None:
|
||||
rows = _rows(key)
|
||||
assert rows
|
||||
for row in rows:
|
||||
got = m01.calc(key, _given(key, row["바꿈"]))
|
||||
assert got["ok"], got
|
||||
assert row["금액"] == pytest.approx(float(got["sums"]["계"]), abs=0.01), row["규격"]
|
||||
gap = float(got["sums"]["계"]) - row["금액"] # 줄 · 비목 버림(B09 산근 규칙) — 최대 3 원
|
||||
assert -0.01 <= gap < 3.2, row["규격"]
|
||||
|
||||
|
||||
def test_대표_규격_펼침() -> None:
|
||||
@@ -126,3 +127,14 @@ def test_화면_기준_목록_계산서_담기() -> None:
|
||||
router = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py").read_text("utf-8")
|
||||
assert '@router.get("/logic-lib/standard", dependencies=[Depends(verify_session)])' in router
|
||||
assert '@router.post("/logic-lib/sheet", dependencies=[Depends(verify_session)])' in router
|
||||
|
||||
|
||||
def test_내역서_낱말_측구파기로_측구터파기를_찾음() -> None:
|
||||
logic = next(
|
||||
logic
|
||||
for _, data in m01._logic_files(m01.loaded()[0])
|
||||
for logic in data.get("줄") or []
|
||||
if logic.get("키") == "GF000099" # 9-12-1 측구터파기 토사
|
||||
)
|
||||
row = {**logic, "별칭": ", ".join(st.words_of(logic))}
|
||||
assert hit(row, "측구파기")
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""704-2 딸림 — 기준 목록 · 계산서 끝수 = B09 산근 규칙(줄마다 ROUNDDOWN 1 · 비목마다 ROUNDDOWN 0).
|
||||
|
||||
거울: 엔진 줄(수량 · 단가)에 B09 `_money` · `block_totals` 를 그대로 씌운 값 = `settle` 값.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B09_Estimation import B09_Estimation_Sangeun as sg # noqa: E402
|
||||
from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402
|
||||
from M02_MasterTemplete import M02_Logic_Settle as st # noqa: E402
|
||||
from M02_MasterTemplete import M02_Logic_Standard as ls # noqa: E402
|
||||
|
||||
mf = m01.cm.mf
|
||||
PART_OF = {"노무비": "labor", "재료비": "material", "경비": "expense"}
|
||||
|
||||
|
||||
def _plain(row: dict) -> bool:
|
||||
"""로직 줄 · 덧줄 · 작업량 Q 가 없는 돈 로직 — 엔진 줄만으로 B09 값을 낼 수 있음."""
|
||||
return (
|
||||
str(row.get("결과단위") or "").startswith("원")
|
||||
and not row.get("덧줄")
|
||||
and not row.get("끝수")
|
||||
and not any(i.get("종류") == "로직" for i in row.get("호표") or [])
|
||||
and not any(re.fullmatch(r"Q\d*", s["이름"]) for s in row.get("중간") or [])
|
||||
)
|
||||
|
||||
|
||||
def test_줄마다_버림_비목마다_버림_B09와_같음():
|
||||
files, whole = m01.loaded()
|
||||
checked = 0
|
||||
for _, data in m01._logic_files(files):
|
||||
for row in data.get("줄") or []:
|
||||
if not _plain(row) or checked >= 60:
|
||||
continue
|
||||
base, _ = ls.axes(whole, row)
|
||||
given = mf.fill_empty(row, base)
|
||||
got = mf.run(whole, row["키"], given)
|
||||
lines = []
|
||||
for line in got["줄"]:
|
||||
one = dict.fromkeys(PART_OF.values(), 0.0)
|
||||
for part, money in line["비목"].items():
|
||||
one[PART_OF[part]] = sg._money(1.0, float(money)) if money is not None else None
|
||||
lines.append(one)
|
||||
if any(v is None for one in lines for v in one.values()):
|
||||
continue
|
||||
want = sg.block_totals(lines)
|
||||
mine = st.settle(whole, row["키"], given)
|
||||
for name, part in PART_OF.items():
|
||||
assert float(mine[name]) == want[part], (row["키"], name)
|
||||
assert float(mine["계"]) == want["sum"], row["키"]
|
||||
checked += 1
|
||||
assert checked >= 30
|
||||
|
||||
|
||||
def test_계산서_줄_소계_계가_맞물림():
|
||||
files, whole = m01.loaded()
|
||||
key = "GF000089"
|
||||
got = ls.sheet(key, {})
|
||||
assert got["묶음"], key
|
||||
for group in got["묶음"]:
|
||||
lines = [Decimal(str(x["금액"] or 0)) for x in group["줄"]]
|
||||
assert all(v == v.quantize(Decimal("0.1")) for v in lines), group["비목"]
|
||||
assert group["소계"] == int(group["소계"])
|
||||
assert group["소계"] <= sum(lines) < group["소계"] + 1 + Decimal("0.05"), group["비목"]
|
||||
assert got["계"] == sum(g["소계"] for g in got["묶음"])
|
||||
assert got["계"] == ls._money(
|
||||
whole, whole.logic(key), ls._given(whole, whole.logic(key), {})[0]
|
||||
)
|
||||
@@ -301,3 +301,17 @@ def test_잊은_짝_느슨한_기억에도_안_나옴(tmp_path, monkeypatch):
|
||||
assert lk.recall(memory, third) is None # 잊은 짝 곁에서는 느슨하게 안 좁힘
|
||||
route.keep(user, {lk.key_of(row): live}) # 다시 이음 = 풀림
|
||||
assert lk.recall(route.recalled(user), row) == live
|
||||
|
||||
|
||||
def test_잊은_짝_짝_표_제안에도_안_나옴(geo):
|
||||
_user, rows = _load(geo)
|
||||
now = {s["key"]: s for s in lk.suggest(rows, {})}
|
||||
line = next(r for r in rows if (r["name"], r.get("spec")) == ("토사절취", "백호0.7㎥"))
|
||||
assert now[line["key"]]["꼬리표"] == lk.TABLE
|
||||
memory = {lk.key_of(line): {lk.FORGOT: True}}
|
||||
after = {s["key"]: s for s in lk.suggest(rows, memory)}
|
||||
assert line["key"] not in after # 짝 표 제안도 잊음
|
||||
assert set(now) - set(after) == {line["key"]} # 다른 줄은 그대로
|
||||
memory[lk.key_of(line)] = {"확정": "earth:4", "이름": "절토(토사)"} # 다시 이음 = 풀림
|
||||
assert lk.suggest(rows, memory)[0] is not None
|
||||
assert line["key"] in {s["key"] for s in lk.suggest(rows, memory)}
|
||||
|
||||
Reference in New Issue
Block a user