Merge remote-tracking branches 'origin/main_laptop_1' and 'origin/sub_laptop_5' into sub_laptop_1

This commit is contained in:
2026-11-15 15:52:02 +09:00
13 changed files with 131 additions and 23 deletions
@@ -183,6 +183,8 @@ def compare_sheet(
if applied is None:
gaps.append(gap("compare", m["key"], m["name"], "적용 단가 없음(출처 없음)"))
supply = next((v for v in (mine.get("supply"), m.get("supply")) if v in SUPPLY), None)
if mine.get("supply") == "사급": # 841-1 사용자가 기본 표시(M02 자재표시)를 끔
supply = None
rows.append(
{
**_head(m, no),
+3 -1
View File
@@ -158,7 +158,9 @@ function compareSheet(materials: Row[], user: Row, gaps: Gap[]): Row[] {
else [applied, appliedFrom] = [low, lowFrom];
if (applied === null) gaps.push(gap("compare", m.key, m.name, "적용 단가 없음(출처 없음)"));
const supply =
[mine.supply, m.supply].find((v) => (SUPPLY as readonly string[]).includes(v)) ?? null;
mine.supply === "사급" // 841-1 사용자가 기본 표시(M02 자재표시)를 끔
? null
: ([mine.supply, m.supply].find((v) => (SUPPLY as readonly string[]).includes(v)) ?? null);
return {
...head(m, i + 1),
unit: m.unit || "",
@@ -132,6 +132,7 @@ def _item_row(item: dict[str, Any], prices: dict[str, Any] | None) -> dict[str,
def _material_items(inputs: dict[str, Any], compare: list[dict[str, Any]]) -> list[dict]:
"""대비표에서 「별도」 · 「관급」 표시한 수량 있는 자재 → 4 자재대 · 관급자재대 항목."""
qty_of = {m["key"]: m.get("qty") for m in inputs.get("materials") or [] if "qty" in m}
qty_of.update(inputs.get("aside_qty") or {}) # 841-1 대가 · 산근 밖 재료 = 그 역전개 수량
out = []
for row in compare:
if row["supply"] is None or row["key"] not in qty_of:
@@ -95,6 +95,7 @@ function itemRow(item: Row, prices: Row | null): Row {
function materialItems(inputs: Row, compare: Row[]): Row[] {
const qtyOf: Row = {};
for (const m of inputs.materials || []) if ("qty" in m) qtyOf[m.key] = m.qty;
Object.assign(qtyOf, inputs.aside_qty || {}); // 841-1 대가 · 산근 밖 재료 = 그 역전개 수량
const out: Row[] = [];
for (const row of compare) {
if (row.supply === null || !(row.key in qtyOf)) continue;
+10 -5
View File
@@ -103,9 +103,6 @@ def _prices(line: dict[str, Any], books: dict[str, Any], sums: dict) -> dict[str
): # 841-1 = M02 엔진 단가(`own`) · B09 단가 책 안 읽음
own = line.get("own")
out = {p: num(own.get(p)) if p in own else 0.0 for p in PARTS} if own else _none()
row = (books["material"].get(key) or {}) if kind == "material" else {}
if row.get("supply") in SUPPLY: # 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락)
out["material"] = 0.0
elif kind in ("daega", "sangeun"):
row = {"daega": sums, "sangeun": books["sangeun"]}[kind].get(key) or {}
for part in PARTS:
@@ -117,6 +114,11 @@ def _prices(line: dict[str, Any], books: dict[str, Any], sums: dict) -> dict[str
return out
def _apart(line: dict[str, Any], books: dict[str, Any]) -> bool:
"""대가 밖 재료(841-1) — 대비표 표시(사용자 › M02 자재표시 기본) 관급 · 별도 = 자재대 · 관급 단락."""
return (books["material"].get(line.get("key")) or {}).get("supply") in SUPPLY
def _none() -> dict[str, float | None]:
return dict.fromkeys(PARTS, None)
@@ -137,8 +139,9 @@ def _base(spec: str, lines: list[dict[str, Any]], src: list) -> float | None:
`line:이름` 그 줄 · `bundle:labor|material|machine` 그 종류 줄(+ 그 이음 고정 줄) 합 ·
`rows:0,2@labor` 블록 그 차례 줄(`src` cid · `@비목` = 그 몫) 합(778-4 · 짝 `.ts` `pctBase`)."""
kind, _, name = spec.partition(":")
if kind == "part":
return total([ln["amounts"][name] for ln in lines if not ln.get("out")])
if kind == "part": # 대가 밖 재료(841-1 `aside`)도 밑수에 — M02 엔진 「재료비의 %」 꼴
aside = "aside" if name == "material" else ""
return total([ln.get(aside) or ln["amounts"][name] for ln in lines if not ln.get("out")])
if kind == "rows": # `3@labor` = 그 줄 그 비목 몫
out: list[float | None] = []
for one in dict.fromkeys(name.split(",")):
@@ -195,6 +198,8 @@ def _block(
part: None if p is None or qty is None else cut(p * qty, line.get("place"))
for part, p in prices.items()
}
if line["kind"] == "material" and _apart(line, books): # 관급 · 별도계상 = 대가 안 0원
extra["aside"], prices["material"], amounts["material"] = amounts["material"], 0.0, 0.0
ref = {"ref": {"key": line["key"]}} if line.get("key") else {} # 집계표(145-5) 이음
ask = {"why": line["ask"]} if qty is None and line.get("ask") else {} # 723-4
lines.append(
+13 -4
View File
@@ -109,9 +109,6 @@ function linePrices(line: Row, books: Row, sums: Record<string, Row>): Parts {
const own: Row | undefined = line.own;
out = blanks();
if (own) for (const part of PARTS) out[part] = part in own ? num(own[part]) : 0;
const row: Row = kind === "material" ? books.material[key] || {} : {};
// 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락)
if ((SUPPLY as readonly string[]).includes(row.supply)) out.material = 0;
} else if (kind === "daega" || kind === "sangeun") {
const src: Row = ({ daega: sums, sangeun: books.sangeun } as Row)[kind];
const row: Row = src[key] || {};
@@ -140,7 +137,12 @@ function pctBase(spec: string, lines: Row[], src: Row[]): Num {
const kind = spec.slice(0, at);
const name = spec.slice(at + 1);
const whole = (ln: Row): Num => total(PARTS.map((p) => ln.amounts[p]));
if (kind === "part") return total(lines.filter((ln) => !ln.out).map((ln) => ln.amounts[name]));
// 대가 밖 재료(841-1 `aside`)도 밑수에 — M02 엔진 「재료비의 %」 꼴
const aside = name === "material" ? "aside" : "";
if (kind === "part")
return total(
lines.filter((ln) => !ln.out).map((ln) => (aside && ln[aside]) || ln.amounts[name]),
);
if (kind === "rows") {
// `3@labor` = 그 줄 그 비목 몫
const out: Num[] = [];
@@ -185,6 +187,13 @@ function block(b: Row, books: Row, sums: Record<string, Row>, gaps: Gap[], own:
const p = prices[part];
amounts[part] = p === null || qty === null ? null : cut(p * qty, line.place);
}
// 관급 · 별도계상 = 대가 안 0원(841-1 대비표 표시 = 사용자 › M02 자재표시)
const supply = (books.material[line.key] || {}).supply;
if (line.kind === "material" && (SUPPLY as readonly string[]).includes(supply)) {
extra.aside = amounts.material;
prices.material = 0;
amounts.material = 0;
}
const ref = line.key ? { ref: { key: line.key } } : {}; // 집계표(145-5) 이음
const ask: Row = qty === null && line.ask ? { why: line.ask } : {}; // 723-4
lines.push({ ...line, ...ref, ...ask, qty, prices, amounts, ...extra });
+14 -3
View File
@@ -38,6 +38,7 @@ from B09_Estimation import B09_Estimation_Labor_Rule as rule
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 B09_Estimation.B09_Estimation_Supply import supply_mark
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
@@ -150,6 +151,10 @@ class Builder:
for line in lines:
if line["kind"] == "machine":
line["spec"] = (eq.get(line["key"]) or {}).get("규격") or ""
marks = one.get("자재표시") or {} # 841-1 M02 자재표시(호표 차례) → 대비표 기본 표시
for n, line in enumerate(lines):
if line["kind"] == "material":
line["mark"] = supply_mark(marks.get(str(n)))
law = rule.mark(logic, lines, engine) # 새 의미 로직 = 줄 자리 · 합밖 · 번지 · 끝전(792-4)
unit = str(logic.get("결과단위") or "").replace("원/", "")
spec = " · ".join(f"{k} {v}" for k, v in sorted(given.items()))
@@ -198,7 +203,8 @@ class Builder:
return None
except (sd.DocError, mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError):
return None
one = {"로직": key, "입력값": got["입력값"], "켜쌓기율": got["켜쌓기율"]}
one = {"로직": key, "입력값": got["입력값"], "켜쌓기율": got["켜쌓기율"],
"자재표시": got.get("자재표시")} # fmt: skip
where = {c: n for n, c in enumerate(got["정체"])} # 율 기준 줄 목록(778-4) → 블록 차례
block = self._poomsem(calc._Over(whole, key, row), {**one, "고름": got["고름"]})
if block is None:
@@ -436,7 +442,11 @@ class Builder:
if line.get("qty") is None and line["kind"] != "blank":
line = {**line, "why": "수량 없음 — 면적산출 알약 칸 · 손 수량을 넣음"}
lines.append(line)
lines += [_material(whole, mat, mat.get("수량")) for mat in result.get("자재") or []]
marks = {m.get("코드"): m for m in doc.get("자재") or [] if isinstance(m, dict)}
lines += [
_material(whole, {**(marks.get(mat.get("코드")) or {}), **mat}, mat.get("수량"))
for mat in result.get("자재") or []
]
manifest = read_json(folder / MANIFEST_NAME) or {}
origin = manifest.get("원본") or {}
return self.put(
@@ -576,7 +586,8 @@ def _material(whole: Any, mat: dict, qty: Any) -> dict[str, Any]:
if price is None and mat.get("코드"):
price = labor_doc.material_price(whole, mat["코드"], mat.get("단위"))["기준단가"]
own = None if _f(price) is None else {"material": _f(price)}
return {"kind": "material", "key": mat.get("코드"), **names, "qty": _f(qty), "own": own}
return {"kind": "material", "key": mat.get("코드"), **names, "qty": _f(qty), "own": own,
"mark": supply_mark(mat)} # fmt: skip
def _engine_bases(engine: list[dict], items: list[dict]) -> dict[str, float]:
+10 -2
View File
@@ -93,7 +93,13 @@ from B09_Estimation.B09_Estimation_Rates import rate_candidates, rate_table
from B09_Estimation.B09_Estimation_Router_Import import router as import_router
from B09_Estimation.B09_Estimation_Router_Link import router as link_router
from B09_Estimation.B09_Estimation_Router_Quotes import router as quotes_router
from B09_Estimation.B09_Estimation_Summary import compute_summary, hopyo_lines, used_materials
from B09_Estimation.B09_Estimation_Summary import (
compute_summary,
hopyo_lines,
totals,
used_materials,
)
from B09_Estimation.B09_Estimation_Supply import supply_defaults
from common_util.common_util_auth import verify_session
from common_util.common_util_project_settings import reset_section
from common_util.common_util_storage import resolve_stored_project_path
@@ -159,8 +165,9 @@ def compute_sheets(
화면 짝 = `B09_Estimation_UI_Shell.computeSheets`(산근만 서버 답 그대로) ·
memo = 두 번 셈의 앞 산근 풀이(`Sangeun.collect`)."""
supply = supply_defaults(inputs, user)
base = compute_base(inputs, user)
sg = sangeun.build(root, user, memo) # 145-1 — M02 엔진 값으로 풀이(841-1 · + 166-2 손 줄 로직)
sg = sangeun.build(root, user, memo, supply) # 145-1 M02 엔진 값으로 풀이(841-1 · 166-2 손 줄)
sg = sg_cover.cover_sangeun(sg, user.get("sg_cover")) # 719-3 이 공사 산근 고친 칸
by_key = {row["key"]: row for row in sg["sg_list"]}
labor = compute_labor(
@@ -189,6 +196,7 @@ def compute_twice(root: Path, inputs: dict[str, Any], user: dict[str, Any]) -> t
keep = used_materials(first, inputs, user)
inputs["materials"] = [m for m in inputs.get("materials") or [] if m["key"] in keep]
inputs["material_qty"] = {row["key"]: row["qty"] for row in summary["sum_material"]}
inputs["aside_qty"] = totals(first, hopyo_lines(first))[0].get("aside") or {} # 841-1 자재대
sheets = compute_sheets(root, inputs, user, memo)
return sheets, compute_summary(sheets, hopyo_lines(sheets))
+19 -4
View File
@@ -42,7 +42,7 @@ from typing import Any
from B08_Quantity.B08_Quantity_Haul_Cycle import fill_given
from B09_Estimation import B09_Estimation_Sangeun_Sum as ss
from B09_Estimation.B09_Estimation_Calc_Base import gap, num, rounddown, total
from B09_Estimation.B09_Estimation_Calc_Base import SUPPLY, gap, num, rounddown, total
from B09_Estimation.B09_Estimation_Store import ref_rows
from M01_MasterData import M01_MasterData_Store as m01
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
@@ -311,7 +311,8 @@ def block_totals(lines: list[dict[str, Any]]) -> dict[str, float | None]:
class Solver:
"""한 번 셈 — 산근 블록을 첫 등장 차례로 모음(하위 산근 포함)."""
def __init__(self) -> None:
def __init__(self, supply: Any = None) -> None:
self.supply = supply or (lambda key: None) # 자재 키 → 관급 · 별도 · None(841-1)
self.prices = _Prices()
self.eng: list[dict[str, Any]] = [] # 지금 푸는 로직의 엔진 줄(호표 차례 · 841-1)
self.blocks: dict[str, dict[str, Any]] = {}
@@ -349,6 +350,7 @@ class Solver:
lines.append(self._line(whole, item, n, env, times, rate, depth))
ss.places(row, lines) # 줄 자리(792-3)
self._extras(whole, row, env, lines)
self._aside(lines)
_annotate(whole, row, env, lines)
except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) as e:
return self._stop(code, name, str(e) or type(e).__name__)
@@ -409,6 +411,16 @@ class Solver:
at += n
return text[:start], text[-1]
def _aside(self, lines: list[dict[str, Any]]) -> None:
"""관급 · 별도계상 재료 = 산근 안 0원(841-1 · % 덧줄 밑수는 엔진 값 그대로 · 자재대 · 관급 단락)."""
for ln in lines:
key = (ln.get("ref") or {}).get("key")
if ln["kind"] != "material" or self.supply(key) not in SUPPLY:
continue
part = next(iter(ln["price"]), "material")
ln["aside"], ln[part], ln["price"] = ln[part], 0.0, {part: 0.0}
self.prices.material[key] = 0.0
def _each(self, n: int, part: str, q: float) -> float | None:
"""엔진 줄 n 비목 단가 = 엔진 금액 ÷ 엔진 수량 · 안 쓰는 줄(수량 0) = 0 · 못 셈 = None."""
x = self.eng[n] if n < len(self.eng) else {}
@@ -608,13 +620,15 @@ def collect(
entries: list[tuple[str, Path]],
hands: list[tuple[str, str, dict[str, Any]]] | None = None,
memo: dict[str, Any] | None = None,
supply: Any = None,
) -> dict[str, Any]:
"""`entries` = [(구조물 id, 사본 폴더)] → 블록 · 목록 · 「연결 없음」 ·
supply = 자재 키 → 관급 · 별도(841-1 · 대비표 표시) ·
`hands` = 손 줄 로직(166-2 · 사본 뒤 번호 · 마스터 로직) ·
memo = 같은 사본 · 손 줄의 앞 풀이(두 번 셈) — 앞 풀이가 본 단가가 같으면 그 풀이 사본."""
if memo and "sg" in memo: # 841-1 풀이 = 사본 · 엔진만(바탕 표 안 읽음) → 앞 풀이 그대로
return copy.deepcopy(memo["sg"])
solver = Solver()
solver = Solver(supply)
for owner, folder in entries:
whole = item_prices.load_prices(read_json(folder / PRICE_FILE) or {})
for src in rows_of(folder, whole):
@@ -647,6 +661,7 @@ def build(
project_root: str | Path,
user: dict[str, Any] | None = None,
memo: dict[str, Any] | None = None,
supply: Any = None,
) -> dict[str, Any]:
"""연결된 사본 전부(구조물 id 차례 → 설계내역서 서고 사본 · 702-3)의 산근 +
설계내역서 손 줄 로직(166-2 · 뒤 번호) — 차례 = `Labor_Source.daega_inputs` ·
@@ -662,4 +677,4 @@ def build(
entries += [
(f"서고 {(m.get('원본') or {}).get('name') or f.name}", f) for f, m in copies(project_root)
]
return collect(entries, hand_sources(user), memo)
return collect(entries, hand_sources(user), memo, supply)
+1 -1
View File
@@ -328,7 +328,7 @@ def clean_user(raw: Any) -> dict[str, Any]:
out: dict[str, Any] = {}
if "applied" in cell:
out["applied"] = _num(cell["applied"])
if cell.get("supply") in SUPPLY:
if cell.get("supply") in (*SUPPLY, "사급"): # 사급 = 기본 표시 끔(841-1)
out["supply"] = cell["supply"]
if "quote" in cell:
out["quote"] = _num(cell["quote"])
+5 -2
View File
@@ -41,7 +41,7 @@ def expand(
kind = line.get("kind")
if kind == "hopyo":
expand(lines_of, str(line.get("key")), qty * q, out, depth + 1)
elif kind in (*KINDS, "fixed"):
elif kind in (*KINDS, "fixed", "aside"):
bucket = out.setdefault(kind, {})
bucket[str(line.get("key"))] = bucket.get(str(line.get("key")), 0.0) + qty * q
@@ -154,6 +154,8 @@ def block_lines(blocks: list[dict[str, Any]] | None) -> dict[str, Any]:
lines += _fixed_lines(block, line)
continue
kind = LINE_KIND.get(line.get("kind"))
if kind == "material" and "aside" in line: # 841-1 대가 · 산근 밖 재료 = 자재대 몫
kind = "aside"
key = (line.get("ref") or {}).get("key")
if kind and key and num(line.get("qty")) is not None:
lines.append({"kind": kind, "key": str(key), "qty": line["qty"]})
@@ -191,7 +193,8 @@ def used_materials(sheets: dict[str, Any], inputs: dict[str, Any], user: dict[st
}
marks = user.get("materials") or {}
for m in inputs.get("materials") or []:
if "qty" in m or m.get("supply") or (marks.get(m["key"]) or {}).get("supply"):
own = m.get("supply") and m.get("origin") != "M01" # 841-1 M02 기본 표시는 남길 까닭 아님
if "qty" in m or own or (marks.get(m["key"]) or {}).get("supply"):
keep.add(m["key"])
return keep
+50
View File
@@ -0,0 +1,50 @@
"""B09 자재 관급 · 별도 표시(PLAN 841-1) — M02 자재표시를 대비표 기본 표시로 · 서버 단독 · 순수 함수.
M02 일위대가 · 단가산출 셈은 재료 줄을 자재표시(자재구분 관급 · 별도계상 — 빈 칸 = 별도계상)로
합에서 뺌(`M02_Labor_Doc.apart`) → B09 도 같은 표시로 대가 · 산근 안 0 원 · 4 자재대 · 관급자재대로
(`B09_Estimation_Calc_Labor._apart` · `Sangeun.Solver._aside` · `Calc_Bill` 자재대 수량 = `aside_qty`).
대비표 표시 = 사용자 › 이 기본 · 사용자 「사급」 = 기본을 끔.
"""
from __future__ import annotations
from typing import Any
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
def supply_mark(one: Any) -> str | None:
"""M02 자재표시(841-1) → 대비표 기본 표시 — 관급 · 별도(별도계상 · M02 기본) · None(직접계산)."""
if labor_doc.mark_of(one, "자재구분") == "관급":
return "관급"
return "별도" if labor_doc.apart(one) else None
def material_marks(blocks: list[dict[str, Any]]) -> dict[str, str | None]:
"""대가 블록 재료 줄 M02 표시를 자재 키마다 하나로(841-1) — 관급 하나라도 = 관급 · 별도 하나라도 =
별도 · 모두 직접계산 = None. ponytail: 키 하나 = 표시 하나(줄마다 다르면 별도 쪽 · 자재대에 한 번)."""
out: dict[str, str | None] = {}
for block in blocks:
for line in block.get("lines") or []:
if line.get("kind") != "material" or not line.get("key") or "mark" not in line:
continue
key, was = str(line["key"]), out.get(str(line["key"]))
out[key] = "관급" if "관급" in (was, line["mark"]) else was or line["mark"]
return out
def supply_defaults(inputs: dict[str, Any], user: dict[str, Any]) -> Any:
"""841-1 대비표 기본 표시 = M02 자재표시(대가 재료 줄 · 없으면 M02 기본 별도계상) — 로직 자재
(B08 줄 밖 · 유가 밖) 입력 줄에 `supply` 를 얹음 → 자재 키 → 관급 · 별도(사용자 표시 › 기본)."""
marks = material_marks((inputs.get("daega") or {}).get("blocks") or [])
for row in inputs.get("materials") or []:
if row.get("origin") == "M01" and "supply" not in row and marks.get(row["key"], "별도"):
row["supply"] = marks.get(row["key"], "별도")
mine = user.get("materials") or {}
default = {row["key"]: row.get("supply") for row in inputs.get("materials") or []}
def supply(key: Any) -> str | None:
got = (mine.get(key) or {}).get("supply")
return None if got == "사급" else got or default.get(key, marks.get(key, "별도"))
return supply
@@ -115,7 +115,8 @@ export const buildBasis: TabBuild = ({ sheets }): TabTable => {
};
};
const SUPPLY_NEXT: Record<string, string | null> = { "": "관급", 관급: "별도", 별도: null };
// 841-1 기본 표시(M02 자재표시)가 있어도 끌 수 있게 — 별도 다음 = 사급(표시 끔)
const SUPPLY_NEXT: Record<string, string | null> = { "": "관급", 관급: "별도", 별도: "사급" };
/** 적용 단가 고르기 항목(183-3) — 값 있는 출처(견적 포함 · 0 뺌) 「출처 단가 · 최저」 + 「최저(자동)」 ·
* `pick(s)` = 그 출처 · null = 자동 */