refactor(841-1): B09 단가 책 · 바탕 셈 걷음 — B 걸음
- 단가 책 = 자재만(자재단가대비표 · 4 자재대 · 관급자재대) · 노무 · 기계 · 운전사 계수(CC000336 세 줄) · 연료 · 환율 줄 걷음 - 바탕 셈(py · ts 짝) 운전사 시간 노임 · 중기사용료(손료 · 운전사 · 연료 · 잡품 재현) · 경비목록표 · 노임 · 기계 덮개 걷음 - 노무비목록표 · 중기목록표 = 대가 · 산근 줄 엔진 단가(프로젝트 사본 M02 값) 나열 · 다시 안 셈(price_lists) - 경비목록표 · 중기사용료 · 환율및기초자료 탭 = 「—」 한 줄 + 까닭 한 줄(탭은 둠) - [단가 갱신] = 프로젝트 사본 자원 단가 줄(단가.json LB · MT · EQ · MO · FX)을 지금 M01 로 + 자재 단가 책(새 파일 B09_Estimation_Copy_Prices.py · 사용자가 누를 때만) · 미리 보기 = 덮개로 같은 셈(파일 안 씀) - 산근 · 대가 단가 고친 칸 걷음(품만) · 단가표 불러오기(707-3) 노임 · 기계 덮개 걷음(자재만) - 낡은 시험은 C 걸음에서 정리 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,26 +5,20 @@
|
||||
조작 중은 화면 짝이 바로 셈 · [저장] 때 서버가 이 파일로 정본을 다시 셈.
|
||||
|
||||
실무 엑셀(기번3 공통 xlsx) 탭 꼴 · 끝수 = 엑셀 자리 그대로:
|
||||
노무비목록표 · 경비목록표 = 굳힌 단가 그대로(사용자 고침 없음 · [단가 갱신] 때만 새로)
|
||||
환율및기초자료 = 환율 · 운전사 시간 노임 = ROUNDDOWN(일당 × 분자 ÷ 분모, 0)
|
||||
(분자 · 분모 = M01 CC000336 1/8 × 16/12 × 25/20 — 서버가 넣어 줌)
|
||||
노무비목록표 · 중기목록표 = 대가 · 산근 줄 엔진 단가(사본 값 · 서버가 `inputs.labor` ·
|
||||
`inputs.machines` 로 넣어 줌 · `B09_Estimation_Supply.price_lists`) 나열 — 다시 안 셈(841-1)
|
||||
경비목록표 · 중기사용료 · 환율및기초자료 = 걷음(841-1 · B09 가 기계 시간당 · 운전사 노임을
|
||||
다시 셈하던 곳 · 값 = M02 단가산출 그대로) — 빈 표
|
||||
자재단가대비표 = 출처 단가 중 최저(0 · 빈칸 뺌 · 견적 포함 = 엑셀 MIN) 자동 ·
|
||||
적용 차례 = 고른 출처(pick · 견적이 높아도) › 사용자 적용 단가 › 최저
|
||||
· 견적 = 사용자 손입력(quote · quote_page · 견적서 파일은 `B09_Estimation_Router_Quotes`)
|
||||
· 관급 · 별도계상 = 사용자 표시(없으면 입력 줄 기본 · B08 파형강관 = 관급)
|
||||
· 관급 · 별도계상 = 사용자 표시(없으면 입력 줄 기본 · 로직 자재 = M02 자재표시 · B08 파형강관 = 관급)
|
||||
→ 재료비목록표 단가 = 적용 단가
|
||||
재료비목록표 = 수량 × 적용 단가 = 총액 ROUND(·, 0)
|
||||
(엑셀 재료비수량금액집계표 G 열 자리 · 수량 · 단가 하나라도 빈칸이면 빈칸) · 수량 =
|
||||
입력 `material_qty`(183-1 · 서버 1차 셈 역전개 총수량 = 재료비수량금액집계표 수량 ·
|
||||
없는 키 빈칸) ·
|
||||
`material_qty` 가 없으면 입력 줄 수량(B08 총괄 합계 열)
|
||||
중기사용료 = 손료(경비 · 기준가 천원 × 손료계수 ÷ 10,000) + 운전사(노무 · 인 × 시간 노임)
|
||||
+ 연료(재료 · ℓ/hr × 적용 단가) + 잡품(재료 · 연료 금액 × % ÷ 100)
|
||||
줄 = ROUNDDOWN(단가 × 수량, 1) · 비목 합 = ROUNDDOWN(Σ, 0) → 중기목록표
|
||||
노임 · 기계 단가 덮개(707-3) = 사용자 값 `labor_cover` {노무 키: 일당} · `machine_cover`
|
||||
{기계 키: {labor, material, expense}}(셋 다 수일 때만) — 굳힌 값 대신 ·
|
||||
줄 `covered` · `book`(굳힌 값) ·
|
||||
덮은 기계 = 중기사용료 줄은 그대로 · 비목 합 · 합 = 덮개 · 그 기계 「연결 없음」 뺌
|
||||
빈칸 규칙 = 값을 낸 원천이 없으면 None(0 을 채우지 않음) · 그 칸을 쓰는 합도 None ·
|
||||
「연결 없음」 목록(`gaps`)에 탭 · 줄 키 · 까닭을 남김(목차 탭이 이동 링크로 씀).
|
||||
"""
|
||||
@@ -93,22 +87,10 @@ def gap(tab: str, key: str, name: str, why: str) -> dict[str, str]:
|
||||
return {"tab": tab, "key": key, "name": name, "why": why}
|
||||
|
||||
|
||||
def driver_hour(day: float | None, factor: dict[str, Any] | None) -> float | None:
|
||||
"""운전사 시간 노임 = ROUNDDOWN(일당 × 분자 ÷ 분모, 0)."""
|
||||
if day is None or not factor or not factor.get("den"):
|
||||
return None
|
||||
return rounddown(day * factor["num"] / factor["den"], 0)
|
||||
|
||||
|
||||
def labor_sheet(
|
||||
labor: list[dict[str, Any]], gaps: list, cover: dict[str, Any] | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
def labor_sheet(labor: list[dict[str, Any]], gaps: list) -> list[dict[str, Any]]:
|
||||
rows = []
|
||||
for no, row in enumerate(labor, 1):
|
||||
price = num(row.get("price"))
|
||||
mine = num((cover or {}).get(row["key"]))
|
||||
extra = {} if mine is None else {"covered": True, "book": price}
|
||||
price = price if mine is None else mine
|
||||
if price is None:
|
||||
gaps.append(gap("labor", row["key"], row["name"], "노임 단가 없음"))
|
||||
rows.append(
|
||||
@@ -117,22 +99,11 @@ def labor_sheet(
|
||||
"code": row.get("code"),
|
||||
"unit": row.get("unit") or "인",
|
||||
"price": price,
|
||||
**extra,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def machine_cover(cover: Any) -> dict[str, dict[str, float]]:
|
||||
"""기계 덮개 — 노 · 재 · 경 셋 다 수인 줄만."""
|
||||
out = {}
|
||||
for key, cell in (cover if isinstance(cover, dict) else {}).items():
|
||||
got = {p: num((cell or {}).get(p)) if isinstance(cell, dict) else None for p in _PARTS}
|
||||
if None not in got.values():
|
||||
out[key] = got
|
||||
return out
|
||||
|
||||
|
||||
def _head(row: dict[str, Any], no: int) -> dict[str, Any]:
|
||||
return {
|
||||
"no": no,
|
||||
@@ -143,17 +114,6 @@ def _head(row: dict[str, Any], no: int) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def expense_sheet(machines: list[dict[str, Any]], gaps: list) -> list[dict[str, Any]]:
|
||||
"""경비목록표 — 기계 손료 기준가(천원)."""
|
||||
rows = []
|
||||
for no, m in enumerate(machines, 1):
|
||||
price = num(m.get("price"))
|
||||
if price is None:
|
||||
gaps.append(gap("expense", m["key"], m["name"], "기계 기준가 없음"))
|
||||
rows.append({**_head(m, no), "unit": "천원", "price": price})
|
||||
return rows
|
||||
|
||||
|
||||
def lowest(prices: dict[str, Any]) -> tuple[float | None, str | None]:
|
||||
"""출처 단가 중 최저 — 0 · 빈칸 뺌 · 같으면 앞 칸."""
|
||||
best: tuple[float | None, str | None] = (None, None)
|
||||
@@ -219,155 +179,22 @@ def material_sheet(
|
||||
return rows
|
||||
|
||||
|
||||
def _line(name, spec, qty, unit, kind, price, ref, pct=False) -> dict[str, Any]:
|
||||
if price is None or qty is None:
|
||||
amount = None
|
||||
elif pct:
|
||||
amount = rounddown(price * qty / 100, 1)
|
||||
else:
|
||||
amount = rounddown(price * qty, 1)
|
||||
return {
|
||||
"name": name,
|
||||
"spec": spec,
|
||||
"qty": qty,
|
||||
"unit": unit,
|
||||
"kind": kind,
|
||||
"price": price,
|
||||
"amount": amount,
|
||||
"ref": ref,
|
||||
}
|
||||
|
||||
|
||||
def machine_block(
|
||||
m: dict[str, Any],
|
||||
drivers: dict[str, dict[str, Any]],
|
||||
fuels: dict[str, dict[str, Any]],
|
||||
gaps: list,
|
||||
) -> dict[str, Any]:
|
||||
"""중기사용료 한 블록 — 손료 · 운전사 · 연료 · 잡품 줄 + 비목 합."""
|
||||
lines = []
|
||||
loss = num(m.get("loss"))
|
||||
coef = loss / 10000 if loss is not None else None
|
||||
price = num(m.get("price"))
|
||||
lines.append(_line(m["name"], m.get("spec") or "", coef, "천원", "expense", price,
|
||||
{"tab": "expense", "key": m["key"]})) # fmt: skip
|
||||
crew = num(m.get("crew"))
|
||||
if crew:
|
||||
code = str(m.get("crew_code") or "")
|
||||
driver = drivers.get(code) or {}
|
||||
hour = driver.get("hour")
|
||||
if hour is None:
|
||||
gaps.append(gap("machine_blocks", m["key"], m["name"], "운전사 노임 없음"))
|
||||
lines.append(_line(driver.get("name") or "운전사", "", crew, "인", "labor", hour,
|
||||
{"tab": "basis", "key": code})) # fmt: skip
|
||||
lph = num(m.get("fuel_lph"))
|
||||
if lph:
|
||||
kind = str(m.get("fuel") or "")
|
||||
fuel = fuels.get(kind) or {}
|
||||
fprice = fuel.get("applied")
|
||||
if fprice is None:
|
||||
gaps.append(gap("machine_blocks", m["key"], m["name"], f"연료({kind}) 단가 없음"))
|
||||
fuel_line = _line(fuel.get("name") or kind, fuel.get("spec") or "", lph, "ℓ", "material",
|
||||
fprice, {"tab": "compare", "key": fuel.get("key") or ""}) # fmt: skip
|
||||
lines.append(fuel_line)
|
||||
misc = num(m.get("misc_pct"))
|
||||
if misc:
|
||||
lines.append(_line("잡품", "주연료의 %", misc, "%", "material", fuel_line["amount"],
|
||||
None, pct=True)) # fmt: skip
|
||||
if price is None or coef is None:
|
||||
gaps.append(gap("machine_blocks", m["key"], m["name"], "손료 기준가 · 계수 없음"))
|
||||
sums: dict[str, float | None] = {}
|
||||
for part in ("labor", "material", "expense"):
|
||||
s = total([ln["amount"] for ln in lines if ln["kind"] == part])
|
||||
sums[part] = None if s is None else rounddown(s, 0)
|
||||
return {
|
||||
"key": m["key"],
|
||||
"name": m["name"],
|
||||
"spec": m.get("spec") or "",
|
||||
"unit": "시간",
|
||||
"lines": lines,
|
||||
"labor": sums["labor"],
|
||||
"material": sums["material"],
|
||||
"expense": sums["expense"],
|
||||
"sum": total([sums["labor"], sums["material"], sums["expense"]]),
|
||||
}
|
||||
|
||||
|
||||
def basis_sheet(
|
||||
inputs: dict[str, Any], labor_rows: list[dict[str, Any]]
|
||||
) -> dict[str, list[dict[str, Any]]]:
|
||||
"""환율및기초자료 — 환율 · 운전사 시간 노임(기계가 부르는 직종만)."""
|
||||
factor = inputs.get("driver_factor")
|
||||
by_code = {str(row.get("code")): row for row in labor_rows if row.get("code") is not None}
|
||||
codes: list[str] = []
|
||||
for m in inputs.get("machines") or []:
|
||||
code = str(m.get("crew_code")) if m.get("crew_code") else None
|
||||
if code and num(m.get("crew")) and code not in codes:
|
||||
codes.append(code)
|
||||
drivers = []
|
||||
for code in codes:
|
||||
row = by_code.get(code) or {}
|
||||
day = row.get("price")
|
||||
drivers.append(
|
||||
{
|
||||
"key": code,
|
||||
"labor_key": row.get("key"),
|
||||
"name": row.get("name") or f"직종 {code}",
|
||||
"day": day,
|
||||
"factor": (factor or {}).get("text") or "",
|
||||
"hour": driver_hour(day, factor),
|
||||
}
|
||||
)
|
||||
fx = [
|
||||
{"key": f["key"], "name": f.get("name") or "", "unit": f.get("unit") or "",
|
||||
"value": num(f.get("value")), "date": f.get("date") or ""}
|
||||
for f in inputs.get("fx") or []
|
||||
] # fmt: skip
|
||||
return {"fx": fx, "drivers": drivers}
|
||||
|
||||
|
||||
def compute_base(inputs: dict[str, Any], user: dict[str, Any]) -> dict[str, Any]:
|
||||
"""144 탭 일곱 + 「연결 없음」 목록."""
|
||||
"""144 탭 + 「연결 없음」 목록 — 841-1 경비목록표 · 중기사용료 · 환율및기초자료 = 빈 표."""
|
||||
gaps: list[dict[str, str]] = []
|
||||
labor_rows = labor_sheet(inputs.get("labor") or [], gaps, user.get("labor_cover"))
|
||||
expense = expense_sheet(inputs.get("machines") or [], gaps)
|
||||
compare = compare_sheet(inputs.get("materials") or [], user.get("materials") or {}, gaps)
|
||||
basis = basis_sheet(inputs, labor_rows)
|
||||
drivers = {d["key"]: d for d in basis["drivers"]}
|
||||
by_key = {row["key"]: row for row in compare}
|
||||
fuels = {
|
||||
kind: by_key[key] for kind, key in (inputs.get("fuel_keys") or {}).items() if key in by_key
|
||||
}
|
||||
blocks = [machine_block(m, drivers, fuels, gaps) for m in inputs.get("machines") or []]
|
||||
covers = machine_cover(user.get("machine_cover"))
|
||||
for b in blocks:
|
||||
if b["key"] in covers: # 707-3 덮개 = 비목 합 · 합(줄은 그대로)
|
||||
b["book"] = {p: b[p] for p in (*_PARTS, "sum")}
|
||||
b.update(covers[b["key"]], covered=True)
|
||||
b["sum"] = total([b[p] for p in _PARTS])
|
||||
gaps = [g for g in gaps if not (g["tab"] == "machine_blocks" and g["key"] in covers)]
|
||||
machine = [
|
||||
{
|
||||
"no": no,
|
||||
"key": b["key"],
|
||||
"name": b["name"],
|
||||
"spec": b["spec"],
|
||||
"unit": b["unit"],
|
||||
"sum": b["sum"],
|
||||
"labor": b["labor"],
|
||||
"material": b["material"],
|
||||
"expense": b["expense"],
|
||||
**({"covered": True} if b.get("covered") else {}),
|
||||
}
|
||||
for no, b in enumerate(blocks, 1)
|
||||
]
|
||||
{**_head(m, no), "unit": "시간", **{p: num(m.get(p)) for p in _PARTS},
|
||||
"sum": total([num(m.get(p)) for p in _PARTS])}
|
||||
for no, m in enumerate(inputs.get("machines") or [], 1)
|
||||
] # fmt: skip
|
||||
return {
|
||||
"labor": labor_rows,
|
||||
"expense": expense,
|
||||
"basis": basis,
|
||||
"labor": labor_sheet(inputs.get("labor") or [], gaps),
|
||||
"expense": [],
|
||||
"basis": {"fx": [], "drivers": []},
|
||||
"compare": compare,
|
||||
"material": material_sheet(compare, inputs.get("material_qty")),
|
||||
"machine_blocks": blocks,
|
||||
"machine_blocks": [],
|
||||
"machine": machine,
|
||||
"gaps": gaps,
|
||||
}
|
||||
|
||||
@@ -6,15 +6,12 @@
|
||||
* 거울 시험 `resources/tester/test_144_b09_base_calc.py`.
|
||||
* 조작 중은 이 파일이 바로 셈 · [저장] 때 서버가 파이썬 짝으로 정본을 다시 셈.
|
||||
*
|
||||
* 노무비 · 경비목록표 = 굳힌 단가 · 환율및기초자료 = 운전사 시간 노임 ROUNDDOWN(일당 × 분자 ÷ 분모, 0)
|
||||
* 노무비목록표 · 중기목록표 = 대가 · 산근 줄 엔진 단가(사본 값 · 서버가 inputs.labor · machines 로 줌) 나열 ·
|
||||
* 경비목록표 · 중기사용료 · 환율및기초자료 = 걷음(841-1 · 값 = M02 단가산출 그대로) — 빈 표
|
||||
* 자재단가대비표 = 출처 중 최저 자동(견적 포함) · 적용 = 고른 출처 › 사용자 단가 › 최저 ·
|
||||
* 관급 · 별도 표시 → 재료비목록표
|
||||
* 재료비목록표 = 수량 × 적용 단가 = 총액 ROUND(·, 0) · 하나라도 빈칸이면 빈칸 · 수량 = 입력
|
||||
* material_qty(183-1 · 서버 역전개 총수량 · 없는 키 빈칸) ?? 입력 줄 수량(B08 총괄 합계 열)
|
||||
* 중기사용료 = 손료 + 운전사 + 연료 + 잡품 · 줄 ROUNDDOWN 1 · 비목 합 ROUNDDOWN 0 → 중기목록표
|
||||
* 노임 · 기계 단가 덮개(707-3) = 사용자 값 labor_cover {노무 키: 일당} · machine_cover
|
||||
* {기계 키: {labor, material, expense}}(셋 다 수일 때만) — 굳힌 값 대신 · covered · book(굳힌 값) ·
|
||||
* 덮은 기계 = 중기사용료 줄 그대로 · 비목 합 · 합 = 덮개 · 그 기계 「연결 없음」 뺌
|
||||
* 빈칸 = 원천이 없으면 null · 그 칸을 쓰는 합도 null · 「연결 없음」 목록(gaps)에 남김
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -44,7 +41,7 @@ export const SOURCES = [
|
||||
|
||||
/** 자재 줄 사용자 표시 */
|
||||
export const SUPPLY = ["관급", "별도"] as const;
|
||||
const COVER_PARTS = ["labor", "material", "expense"] as const;
|
||||
const PARTS = ["labor", "material", "expense"] as const;
|
||||
|
||||
function scaled(value: number, places: number, fn: (x: number) => number): number {
|
||||
const a = Math.abs(value);
|
||||
@@ -92,11 +89,6 @@ export const gap = (tab: string, key: string, name: string, why: string): Gap =>
|
||||
why,
|
||||
});
|
||||
|
||||
export function driverHour(day: Num, factor: Row | null | undefined): Num {
|
||||
if (day === null || !factor || !factor.den) return null;
|
||||
return rounddown((day * factor.num) / factor.den, 0);
|
||||
}
|
||||
|
||||
const head = (row: Row, no: number): Row => ({
|
||||
no,
|
||||
key: row.key,
|
||||
@@ -105,33 +97,11 @@ const head = (row: Row, no: number): Row => ({
|
||||
source: row.source || "",
|
||||
});
|
||||
|
||||
function laborSheet(labor: Row[], gaps: Gap[], cover?: Row): Row[] {
|
||||
function laborSheet(labor: Row[], gaps: Gap[]): Row[] {
|
||||
return labor.map((row, i) => {
|
||||
const book = num(row.price);
|
||||
const mine = num((cover || {})[row.key]);
|
||||
const price = mine === null ? book : mine;
|
||||
const price = num(row.price);
|
||||
if (price === null) gaps.push(gap("labor", row.key, row.name, "노임 단가 없음"));
|
||||
const extra = mine === null ? {} : { covered: true, book };
|
||||
return { ...head(row, i + 1), code: row.code ?? null, unit: row.unit || "인", price, ...extra };
|
||||
});
|
||||
}
|
||||
|
||||
/** 기계 덮개 — 노 · 재 · 경 셋 다 수인 줄만 */
|
||||
export function machineCover(cover: unknown): Row {
|
||||
const out: Row = {};
|
||||
for (const [key, cell] of Object.entries(cover && typeof cover === "object" ? cover : {})) {
|
||||
const got: Row = {};
|
||||
for (const p of COVER_PARTS) got[p] = cell && typeof cell === "object" ? num(cell[p]) : null;
|
||||
if (COVER_PARTS.every((p) => got[p] !== null)) out[key] = got;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function expenseSheet(machines: Row[], gaps: Gap[]): Row[] {
|
||||
return machines.map((m, i) => {
|
||||
const price = num(m.price);
|
||||
if (price === null) gaps.push(gap("expense", m.key, m.name, "기계 기준가 없음"));
|
||||
return { ...head(m, i + 1), unit: "천원", price };
|
||||
return { ...head(row, i + 1), code: row.code ?? null, unit: row.unit || "인", price };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -192,148 +162,21 @@ function materialSheet(compare: Row[], qtyOf?: Row | null): Row[] {
|
||||
});
|
||||
}
|
||||
|
||||
function line(
|
||||
name: string,
|
||||
spec: string,
|
||||
qty: Num,
|
||||
unit: string,
|
||||
kind: string,
|
||||
price: Num,
|
||||
ref: Row | null,
|
||||
pct = false,
|
||||
): Row {
|
||||
let amount: Num;
|
||||
if (price === null || qty === null) amount = null;
|
||||
else if (pct) amount = rounddown((price * qty) / 100, 1);
|
||||
else amount = rounddown(price * qty, 1);
|
||||
return { name, spec, qty, unit, kind, price, amount, ref };
|
||||
}
|
||||
|
||||
function machineBlock(m: Row, drivers: Row, fuels: Row, gaps: Gap[]): Row {
|
||||
const lines: Row[] = [];
|
||||
const loss = num(m.loss);
|
||||
const coef = loss !== null ? loss / 10000 : null;
|
||||
const price = num(m.price);
|
||||
lines.push(
|
||||
line(m.name, m.spec || "", coef, "천원", "expense", price, { tab: "expense", key: m.key }),
|
||||
);
|
||||
const crew = num(m.crew);
|
||||
if (crew) {
|
||||
const code = String(m.crew_code || "");
|
||||
const driver: Row = drivers[code] || {};
|
||||
const hour: Num = driver.hour ?? null;
|
||||
if (hour === null) gaps.push(gap("machine_blocks", m.key, m.name, "운전사 노임 없음"));
|
||||
lines.push(
|
||||
line(driver.name || "운전사", "", crew, "인", "labor", hour, { tab: "basis", key: code }),
|
||||
);
|
||||
}
|
||||
const lph = num(m.fuel_lph);
|
||||
if (lph) {
|
||||
const kind = String(m.fuel || "");
|
||||
const fuel: Row = fuels[kind] || {};
|
||||
const fprice: Num = fuel.applied ?? null;
|
||||
if (fprice === null) gaps.push(gap("machine_blocks", m.key, m.name, `연료(${kind}) 단가 없음`));
|
||||
const fuelLine = line(fuel.name || kind, fuel.spec || "", lph, "ℓ", "material", fprice, {
|
||||
tab: "compare",
|
||||
key: fuel.key || "",
|
||||
});
|
||||
lines.push(fuelLine);
|
||||
const misc = num(m.misc_pct);
|
||||
if (misc)
|
||||
lines.push(line("잡품", "주연료의 %", misc, "%", "material", fuelLine.amount, null, true));
|
||||
}
|
||||
if (price === null || coef === null)
|
||||
gaps.push(gap("machine_blocks", m.key, m.name, "손료 기준가 · 계수 없음"));
|
||||
const sums: Record<string, Num> = {};
|
||||
for (const part of ["labor", "material", "expense"]) {
|
||||
const s = total(lines.filter((ln) => ln.kind === part).map((ln) => ln.amount));
|
||||
sums[part] = s === null ? null : rounddown(s, 0);
|
||||
}
|
||||
return {
|
||||
key: m.key,
|
||||
name: m.name,
|
||||
spec: m.spec || "",
|
||||
unit: "시간",
|
||||
lines,
|
||||
labor: sums.labor,
|
||||
material: sums.material,
|
||||
expense: sums.expense,
|
||||
sum: total([sums.labor, sums.material, sums.expense]),
|
||||
};
|
||||
}
|
||||
|
||||
function basisSheet(inputs: Row, laborRows: Row[]): Row {
|
||||
const factor = inputs.driver_factor;
|
||||
const byCode: Row = {};
|
||||
for (const row of laborRows)
|
||||
if (row.code !== null && row.code !== undefined) byCode[String(row.code)] = row;
|
||||
const codes: string[] = [];
|
||||
for (const m of inputs.machines || []) {
|
||||
const code = m.crew_code ? String(m.crew_code) : null;
|
||||
if (code && num(m.crew) && !codes.includes(code)) codes.push(code);
|
||||
}
|
||||
const drivers = codes.map((code) => {
|
||||
const row: Row = byCode[code] || {};
|
||||
const day: Num = row.price ?? null;
|
||||
return {
|
||||
key: code,
|
||||
labor_key: row.key ?? null,
|
||||
name: row.name || `직종 ${code}`,
|
||||
day,
|
||||
factor: (factor || {}).text || "",
|
||||
hour: driverHour(day, factor),
|
||||
};
|
||||
});
|
||||
const fx = (inputs.fx || []).map((f: Row) => ({
|
||||
key: f.key,
|
||||
name: f.name || "",
|
||||
unit: f.unit || "",
|
||||
value: num(f.value),
|
||||
date: f.date || "",
|
||||
}));
|
||||
return { fx, drivers };
|
||||
}
|
||||
|
||||
/** 144 탭 일곱 + 「연결 없음」 목록 */
|
||||
/** 144 탭 + 「연결 없음」 목록 — 841-1 경비목록표 · 중기사용료 · 환율및기초자료 = 빈 표(짝 `compute_base`) */
|
||||
export function computeBase(inputs: Row, user: Row): Row {
|
||||
let gaps: Gap[] = [];
|
||||
const laborRows = laborSheet(inputs.labor || [], gaps, user.labor_cover);
|
||||
const expense = expenseSheet(inputs.machines || [], gaps);
|
||||
const gaps: Gap[] = [];
|
||||
const compare = compareSheet(inputs.materials || [], user.materials || {}, gaps);
|
||||
const basis = basisSheet(inputs, laborRows);
|
||||
const drivers: Row = Object.fromEntries(basis.drivers.map((d: Row) => [d.key, d]));
|
||||
const byKey: Row = Object.fromEntries(compare.map((row) => [row.key, row]));
|
||||
const fuels: Row = {};
|
||||
for (const [kind, key] of Object.entries(inputs.fuel_keys || {}))
|
||||
if ((key as string) in byKey) fuels[kind] = byKey[key as string];
|
||||
const blocks = (inputs.machines || []).map((m: Row) => machineBlock(m, drivers, fuels, gaps));
|
||||
const covers = machineCover(user.machine_cover);
|
||||
for (const b of blocks) {
|
||||
if (!(b.key in covers)) continue; // 707-3 덮개 = 비목 합 · 합(줄은 그대로)
|
||||
b.book = { labor: b.labor, material: b.material, expense: b.expense, sum: b.sum };
|
||||
Object.assign(b, covers[b.key], { covered: true });
|
||||
b.sum = total(COVER_PARTS.map((p) => b[p]));
|
||||
}
|
||||
gaps = gaps.filter((g) => !(g.tab === "machine_blocks" && g.key in covers));
|
||||
const machine = blocks.map((b: Row, i: number) => ({
|
||||
no: i + 1,
|
||||
key: b.key,
|
||||
name: b.name,
|
||||
spec: b.spec,
|
||||
unit: b.unit,
|
||||
sum: b.sum,
|
||||
labor: b.labor,
|
||||
material: b.material,
|
||||
expense: b.expense,
|
||||
...(b.covered ? { covered: true } : {}),
|
||||
}));
|
||||
const machine = (inputs.machines || []).map((m: Row, i: number) => {
|
||||
const parts: Row = Object.fromEntries(PARTS.map((p) => [p, num(m[p])]));
|
||||
return { ...head(m, i + 1), unit: "시간", ...parts, sum: total(PARTS.map((p) => parts[p])) };
|
||||
});
|
||||
return {
|
||||
labor: laborRows,
|
||||
expense,
|
||||
basis,
|
||||
labor: laborSheet(inputs.labor || [], gaps),
|
||||
expense: [],
|
||||
basis: { fx: [], drivers: [] },
|
||||
compare,
|
||||
material: materialSheet(compare, inputs.material_qty),
|
||||
machine_blocks: blocks,
|
||||
machine_blocks: [],
|
||||
machine,
|
||||
gaps,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""B09 [단가 갱신] = 프로젝트 사본 자원 단가 줄 갱신(PLAN 841-1 · 요구 854 · 866) — 서버 단독.
|
||||
|
||||
B09 금액 = 프로젝트 사본(구조물 사본 · 서고 사본)을 M02 엔진으로 셈한 값(B09 단가 책으로 다시 안 셈).
|
||||
그래서 사본 값을 지금 M01 로 올리는 일 = 사본 `단가.json` 자원 줄(LB · MT · EQ · MO · FX)만 지금 M01 줄로
|
||||
(로직 줄 · 일위대가 · 입력값 · 고른 자재 · 덮개 그대로 · M01 에서 지워진 키 = 마지막 값 그대로) —
|
||||
M02 [단가 갱신](`M02_Sangeun_Copy.refresh`)과 같은 뜻 · 사용자가 누를 때만(프로젝트 안은 자동 아님).
|
||||
미리 보기 = `OVER` 덮개(파일 안 씀 · 사본 읽는 곳이 `bundle` 로 읽음)로 같은 셈.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextvars import ContextVar
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from B09_Estimation import B09_Estimation_Bill_Library as library
|
||||
from common_util.common_util_json import atomic_write_json
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Item_Prices as item_prices
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
from M02_MasterTemplete.M02_Item_Snapshot import linked_snapshots, structures_dir
|
||||
from M02_MasterTemplete.M02_Item_Source import PRICE_FILE, read_json
|
||||
|
||||
#: 미리 보기 덮개 {사본 폴더 글: 새 `단가.json`} — 없으면 파일
|
||||
OVER: ContextVar[dict[str, dict] | None] = ContextVar("b09_copy_prices", default=None)
|
||||
|
||||
|
||||
def bundle(folder: Path) -> dict[str, Any]:
|
||||
"""사본 `단가.json` — 미리 보기 덮개가 있으면 그것."""
|
||||
over = OVER.get()
|
||||
if over and str(folder) in over:
|
||||
return over[str(folder)]
|
||||
return read_json(folder / PRICE_FILE) or {}
|
||||
|
||||
|
||||
def folders(root: str | Path) -> list[Path]:
|
||||
"""연결된 구조물 사본 + 서고 사본 폴더."""
|
||||
base = structures_dir(root)
|
||||
out = [base / one["폴더"] for one in linked_snapshots(root).values()]
|
||||
out += [folder for folder, _manifest in library.copies(root)]
|
||||
return list(dict.fromkeys(out))
|
||||
|
||||
|
||||
def _shown(row: dict[str, Any]) -> Any:
|
||||
"""미리 보기 값 — 노임 · 유가 = 값 · 기계 = 가격(천원) · 자재 = 출처 값 묶음."""
|
||||
value = row.get("값")
|
||||
if isinstance(value, dict):
|
||||
return value.get("가격")
|
||||
if value is not None:
|
||||
return value
|
||||
return {k: row.get(k) for k in ("조달청", "물가자료", "유통물가", "물가정보", "거래가격")}
|
||||
|
||||
|
||||
def refreshed(root: str | Path) -> tuple[dict[str, dict], list[dict[str, Any]]]:
|
||||
"""({바뀐 사본 폴더 글: 새 `단가.json`}, 바뀐 자원 [{키, 이름, 전, 후}]) — 파일 안 씀."""
|
||||
whole = skc.whole_()
|
||||
news: dict[str, dict] = {}
|
||||
changed: dict[str, dict[str, Any]] = {}
|
||||
for folder in folders(root):
|
||||
data = read_json(folder / PRICE_FILE)
|
||||
if not isinstance(data, dict):
|
||||
continue
|
||||
out, hit = {}, False
|
||||
for name, part in data.items():
|
||||
slot = m01.items_key(part) if isinstance(part, dict) else ""
|
||||
rows = part.get(slot) if slot else None
|
||||
if not isinstance(rows, list):
|
||||
out[name] = part
|
||||
continue
|
||||
fresh = []
|
||||
for row in rows:
|
||||
key = str((row or {}).get("키") or "")
|
||||
live = whole.index.get(key[:2], {}).get(key) if key[:2] in skc.RES else None
|
||||
new = item_prices.to_json(live) if live is not None else row
|
||||
if new != row:
|
||||
hit = True
|
||||
changed.setdefault(
|
||||
key,
|
||||
{"키": key, "이름": new.get("이름"), "전": _shown(row), "후": _shown(new)},
|
||||
)
|
||||
fresh.append(new)
|
||||
out[name] = {**part, slot: fresh}
|
||||
if hit:
|
||||
news[str(folder)] = out
|
||||
return news, list(changed.values())
|
||||
|
||||
|
||||
def write(news: dict[str, dict]) -> int:
|
||||
"""새 `단가.json` 을 사본 폴더에 씀 — 쓴 사본 수."""
|
||||
for folder, data in news.items():
|
||||
atomic_write_json(Path(folder) / PRICE_FILE, data)
|
||||
return len(news)
|
||||
@@ -2,65 +2,45 @@
|
||||
(덮개 제안 → 화면 캐시 → [저장]).
|
||||
|
||||
읽는 시트(꼴마다 첫 것) — 머리 = 「명칭」 · 「품명」 칸이 든 줄:
|
||||
노임 = STmate 「노무비목록표」 · EST 「노임단가」 → 단가(「적용단가」 › 「단가」)
|
||||
자재 = STmate 「재료비목록표」 › 「자재단가대비표」 · EST 「자재단가」 → 적용 단가(단위 % 줄 뺌)
|
||||
기계 = STmate 「중기목록표」(= 「중기사용료」 블록 합계) ·
|
||||
EST 「기계경비총괄표」(= 「기계경비」 합) → 시간 단가 노무비 · 재료비 · 경비
|
||||
이음 = 이 공사 단가 책 줄(바탕 셈 `compute_base` 노무비목록표 · 중기목록표 · 자재단가대비표)에
|
||||
이름 · 규격 · 단위로 — 705-3 이음 낱말 그대로(노임 = `M02_Hopyo_Shares.trade` 직종 이름 ·
|
||||
기계 = 이름 + 규격 같음 › `eq_key` 갈래 · 크기 · 자재 = 「별도계상」 글 뗀 이름 · 규격 · 단위 ›
|
||||
이름 · 단위) · 한 줄에만 맞을 때만 · 기계 갈래 · 크기는 표지 글(타이어 · 브레이커 · 할증 …)
|
||||
까지 · 한 책 줄에 엑셀 줄 여럿 = 「짝 여럿」(못 고름).
|
||||
덮개 = 노임 `labor_cover` · 기계 `machine_cover`(새 · `B09_Estimation_Calc_Base`) ·
|
||||
자재 = 지금 덮개 `materials[키].applied`(고른 출처 `pick` 은 뺌 — 적용 차례가 pick 먼저) ·
|
||||
(841-1 노임 · 기계 단가표 걷음 — B09 노무 · 기계 단가 = M02 사본 값 · 덮지 않음)
|
||||
이음 = 이 공사 자재단가대비표 줄에 이름 · 규격 · 단위로 — 705-3 이음 낱말 그대로(「별도계상」 글 뗀
|
||||
이름 · 규격 · 단위 › 이름 · 단위) · 한 줄에만 맞을 때만 · 한 책 줄에 엑셀 줄 여럿 = 「짝 여럿」.
|
||||
덮개 = 자재 = 지금 덮개 `materials[키].applied`(고른 출처 `pick` 은 뺌 — 적용 차례가 pick 먼저) ·
|
||||
책 값과 같으면 덮지 않음(옛 덮개 걷음).
|
||||
못 이은 줄 = 「짝 없음 n」(이름 목록).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
from collections import Counter
|
||||
from typing import Any
|
||||
|
||||
from B09_Estimation.B09_Estimation_Import_Rates import flat, money, read_sheets
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Hopyo_Shares as shares
|
||||
from M02_MasterTemplete.M02_Hopyo_Excel import ExcelError
|
||||
|
||||
KINDS = {
|
||||
"labor": ("노무비목록표", "노임단가"),
|
||||
"material": ("재료비목록표", "자재단가대비표", "자재단가"),
|
||||
"machine": ("중기목록표", "기계경비총괄표"),
|
||||
}
|
||||
PARTS = ("labor", "material", "expense")
|
||||
_MACHINE = {"labor": "노무비", "material": "재료비", "expense": "경비"}
|
||||
#: 841-1 노임 · 기계 단가 덮개 걷음(B09 노무 · 기계 단가 = M02 사본 값) — 자재만
|
||||
KINDS = {"material": ("재료비목록표", "자재단가대비표", "자재단가")}
|
||||
LIST_MAX = 40
|
||||
#: 같은 갈래 · 크기라도 다른 기계 — 표지 글이 같아야 이음
|
||||
_MARK = re.compile(r"타이어|브레|할증|암석|\(암\)|집게|진동|탑재|크레인|살수")
|
||||
|
||||
|
||||
def _n(text: Any) -> str:
|
||||
return flat(unicodedata.normalize("NFKC", str(text or ""))).lower()
|
||||
|
||||
|
||||
def table(rows: list[list[Any]], machine: bool) -> list[dict[str, Any]]:
|
||||
"""목록 시트 → [{name, spec, unit, price | labor · material · expense}]."""
|
||||
def table(rows: list[list[Any]]) -> list[dict[str, Any]]:
|
||||
"""목록 시트 → [{name, spec, unit, price}]."""
|
||||
for at, row in enumerate(rows[:12]):
|
||||
cols = [flat(v) for v in row]
|
||||
name = next((cols.index(h) for h in ("명칭", "품명") if h in cols), None)
|
||||
if name is None:
|
||||
continue
|
||||
spec, unit = (cols.index(h) if h in cols else None for h in ("규격", "단위"))
|
||||
if machine:
|
||||
price = {p: cols.index(h) for p, h in _MACHINE.items() if h in cols}
|
||||
if len(price) < 3:
|
||||
continue
|
||||
else:
|
||||
price = next((cols.index(h) for h in ("적용단가", "단가") if h in cols), None)
|
||||
if price is None:
|
||||
continue
|
||||
price = next((cols.index(h) for h in ("적용단가", "단가") if h in cols), None)
|
||||
if price is None:
|
||||
continue
|
||||
out = []
|
||||
for line in rows[at + 1 :]:
|
||||
|
||||
@@ -69,12 +49,8 @@ def table(rows: list[list[Any]], machine: bool) -> list[dict[str, Any]]:
|
||||
|
||||
item = {"name": str(cell(name) or "").strip(), "spec": str(cell(spec) or "").strip(),
|
||||
"unit": str(cell(unit) or "").strip()} # fmt: skip
|
||||
if machine:
|
||||
item.update({p: money(cell(c)) for p, c in price.items()})
|
||||
ok = None not in (item[p] for p in PARTS)
|
||||
else:
|
||||
item["price"] = money(cell(price))
|
||||
ok = item["price"] is not None and item["unit"] != "%"
|
||||
item["price"] = money(cell(price))
|
||||
ok = item["price"] is not None and item["unit"] != "%"
|
||||
if item["name"] and ok:
|
||||
out.append(item)
|
||||
return out
|
||||
@@ -82,31 +58,18 @@ def table(rows: list[list[Any]], machine: bool) -> list[dict[str, Any]]:
|
||||
|
||||
|
||||
def read_prices(name: str, data: bytes) -> dict[str, list[dict[str, Any]]]:
|
||||
"""파일 → {labor, material, machine: 줄 목록} · 시트 없음 = 빈 목록 · 셋 다 없으면 막힘."""
|
||||
"""파일 → {material: 줄 목록} · 시트 없음 = 막힘."""
|
||||
sheets = read_sheets(name, data, tuple(s for names in KINDS.values() for s in names))
|
||||
out = {}
|
||||
for kind, names in KINDS.items():
|
||||
sheet = next((s for s in names if s in sheets), None)
|
||||
out[kind] = table(sheets[sheet], kind == "machine") if sheet else []
|
||||
out[kind] = table(sheets[sheet]) if sheet else []
|
||||
out[f"{kind}_sheet"] = sheet
|
||||
if not any(out[k] for k in KINDS):
|
||||
raise ExcelError("노무비목록표 · 재료비목록표 · 중기목록표(STmate) · 노임단가 · 자재단가 · "
|
||||
"기계경비총괄표(EST) 시트가 없음") # fmt: skip
|
||||
raise ExcelError("재료비목록표 · 자재단가대비표(STmate) · 자재단가(EST) 시트가 없음")
|
||||
return out
|
||||
|
||||
|
||||
def _labor_keys(row: dict[str, Any]) -> list[str]:
|
||||
return [_n(shares.trade(row.get("name")) or row.get("name"))]
|
||||
|
||||
|
||||
def _machine_keys(row: dict[str, Any]) -> list[str]:
|
||||
"""[이름 + 규격 같음, 갈래 · 크기 + 표지 글(타이어 · 브레이커 · 할증 · 암 …)]."""
|
||||
text = f"{row.get('name') or ''} {row.get('spec') or ''}"
|
||||
kind = shares.eq_key(text)
|
||||
marks = "+".join(sorted(set(_MARK.findall(_n(text)))))
|
||||
return [_n(text), f"{kind}#{marks}" if kind else ""]
|
||||
|
||||
|
||||
def _material_keys(row: dict[str, Any]) -> list[str]:
|
||||
me, _ = hmat.clean({"이름": row.get("name"), "규격": row.get("spec")})
|
||||
unit = _n(row.get("unit"))
|
||||
@@ -123,18 +86,13 @@ def _match(book: list[dict[str, Any]], keys_of, item: dict[str, Any]) -> dict[st
|
||||
|
||||
|
||||
def _same(a: Any, b: Any) -> bool:
|
||||
if isinstance(a, dict):
|
||||
return all(_same(a[p], (b or {}).get(p)) for p in PARTS)
|
||||
return a is not None and b is not None and abs(a - b) < 0.5
|
||||
|
||||
|
||||
def propose(read: dict[str, Any], sheets: dict[str, Any], user: dict[str, Any]) -> dict[str, Any]:
|
||||
"""읽은 단가표 + 이 공사 단가 책(덮개 뺀 바탕 셈) → 덮개 patch · 이은 줄 · 짝 없음."""
|
||||
base = {"labor": sheets.get("labor") or [], "machine": sheets.get("machine") or [],
|
||||
"material": sheets.get("compare") or []} # fmt: skip
|
||||
keys = {"labor": _labor_keys, "machine": _machine_keys, "material": _material_keys}
|
||||
labor_cover = dict(user.get("labor_cover") or {})
|
||||
machine_cover = dict(user.get("machine_cover") or {})
|
||||
base = {"material": sheets.get("compare") or []}
|
||||
keys = {"material": _material_keys}
|
||||
materials = {k: dict(v) for k, v in (user.get("materials") or {}).items()}
|
||||
linked: list[dict[str, Any]] = []
|
||||
missing: dict[str, list[str]] = {k: [] for k in KINDS}
|
||||
@@ -147,30 +105,16 @@ def propose(read: dict[str, Any], sheets: dict[str, Any], user: dict[str, Any])
|
||||
if row is None or hits[row["key"]] > 1: # 한 책 줄에 엑셀 줄 여럿 = 못 고름
|
||||
missing[kind].append(label if row is None else f"{label}(짝 여럿)")
|
||||
continue
|
||||
if kind == "machine":
|
||||
got = {p: item[p] for p in PARTS}
|
||||
book = {p: row.get(p) for p in PARTS}
|
||||
else:
|
||||
got, book = item["price"], row.get("price" if kind == "labor" else "applied")
|
||||
got, book = item["price"], row.get("applied")
|
||||
linked.append({"kind": kind, "key": row["key"], "name": row.get("name") or "",
|
||||
"from": label, "excel": got, "book": book,
|
||||
"same": _same(got, book)}) # fmt: skip
|
||||
same = linked[-1]["same"] # 책 값 = 엑셀 → 옛 덮개 걷음(값 = 책)
|
||||
if kind in ("labor", "machine"):
|
||||
cover = labor_cover if kind == "labor" else machine_cover
|
||||
cover.pop(row["key"], None)
|
||||
if not same:
|
||||
cover[row["key"]] = got
|
||||
else:
|
||||
drop = ("pick", "applied") if same else ("pick",)
|
||||
cell = {k: v for k, v in materials.get(row["key"], {}).items() if k not in drop}
|
||||
materials[row["key"]] = cell if same else {**cell, "applied": got}
|
||||
drop = ("pick", "applied") if same else ("pick",)
|
||||
cell = {k: v for k, v in materials.get(row["key"], {}).items() if k not in drop}
|
||||
materials[row["key"]] = cell if same else {**cell, "applied": got}
|
||||
return {
|
||||
"patch": {
|
||||
"labor_cover": labor_cover,
|
||||
"machine_cover": machine_cover,
|
||||
"materials": materials,
|
||||
}, # fmt: skip
|
||||
"patch": {"materials": materials},
|
||||
"linked": linked,
|
||||
"missing": {k: v[:LIST_MAX] for k, v in missing.items()},
|
||||
"counts": {
|
||||
|
||||
@@ -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_Copy_Prices import bundle
|
||||
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
|
||||
@@ -50,7 +51,7 @@ from M02_MasterTemplete.M02_Item_Snapshot import (
|
||||
linked_snapshots,
|
||||
structures_dir,
|
||||
)
|
||||
from M02_MasterTemplete.M02_Item_Source import LABOR_FILE, PRICE_FILE, read_json
|
||||
from M02_MasterTemplete.M02_Item_Source import LABOR_FILE, read_json
|
||||
|
||||
mf = item_prices.mf
|
||||
|
||||
@@ -414,7 +415,7 @@ class Builder:
|
||||
labor = read_json(folder / LABOR_FILE)
|
||||
if not isinstance(labor, dict):
|
||||
return None
|
||||
prices = read_json(folder / PRICE_FILE) or {}
|
||||
prices = bundle(folder) # 841-1 미리 보기 덮개
|
||||
values = read_json(folder / VALUES_FILE) or {}
|
||||
picks = read_json(folder / PICKS_FILE) or {}
|
||||
whole = item_prices.load_prices(prices)
|
||||
@@ -472,7 +473,7 @@ class Builder:
|
||||
doc = copies.get(refs.ref_id(ref)) if isinstance(ref, dict) else None
|
||||
if not isinstance(doc, dict):
|
||||
return None
|
||||
whole = item_prices.load_prices(read_json(folder / PRICE_FILE) or {})
|
||||
whole = item_prices.load_prices(bundle(folder))
|
||||
if manifest["종류"] == "sangeun": # 내 자료집 단가산출(719-2)
|
||||
return self.sangeun(whole, ref, doc, copies, 1)
|
||||
return self.precombo(whole, ref, doc, copies, 1)
|
||||
|
||||
@@ -15,6 +15,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from B09_Estimation.B09_Estimation_Copy_Prices import bundle
|
||||
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
|
||||
@@ -24,7 +25,7 @@ from M02_MasterTemplete.M02_Item_Snapshot import (
|
||||
linked_snapshots,
|
||||
structures_dir,
|
||||
)
|
||||
from M02_MasterTemplete.M02_Item_Source import LABOR_FILE, PRICE_FILE, read_json
|
||||
from M02_MasterTemplete.M02_Item_Source import LABOR_FILE, read_json
|
||||
|
||||
#: 원가 단가 목록에 오르는 자원 표
|
||||
RESOURCE_TABLES = ("LB", "EQ", "MT", "MO")
|
||||
@@ -37,7 +38,7 @@ def used_keys(folder: Path) -> list[str]:
|
||||
labor = read_json(folder / LABOR_FILE)
|
||||
if not isinstance(labor, dict):
|
||||
return []
|
||||
snapshot = item_prices.load_prices(read_json(folder / PRICE_FILE) or {})
|
||||
snapshot = item_prices.load_prices(bundle(folder))
|
||||
values = read_json(folder / VALUES_FILE) or {}
|
||||
picks = read_json(folder / PICKS_FILE) or {}
|
||||
seen: list[str] = []
|
||||
@@ -110,8 +111,8 @@ def frozen_rows(project_root: str | Path) -> dict[str, dict[str, Any]]:
|
||||
|
||||
out: dict[str, dict[str, Any]] = {}
|
||||
for folder in _folders(project_root):
|
||||
bundle = read_json(folder / PRICE_FILE) or {}
|
||||
for name, data in bundle.items():
|
||||
got = bundle(folder) # 841-1 미리 보기 덮개
|
||||
for name, data in got.items():
|
||||
if name.startswith("_") or not isinstance(data, dict):
|
||||
continue
|
||||
for row in data.get("줄") or data.get("표") or []:
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
"""B09 원가 단가 한 벌 · 수요 모음(PLAN 144) — 서버 단독(M01 을 읽는 곳은 여기 하나).
|
||||
"""B09 자재 단가 책 · 수요 모음(PLAN 144 · 841-1) — 서버 단독(M01 을 읽는 곳은 여기 하나).
|
||||
|
||||
원가 단가 = 프로젝트 한 벌 — 원가산출에서 한 번 굳힘(`price_book.json`) ·
|
||||
841-1 — 노무 · 기계 · 운전사 · 연료 · 환율은 책에서 걷음(B09 금액 = 사본을 M02 엔진으로 셈한 값 ·
|
||||
`B09_Estimation_Copy_Prices`).
|
||||
책에 남는 것 = 자재단가대비표(4 자재대 · 관급자재대 단가 고르기) 자재 줄.
|
||||
|
||||
자재 단가 = 프로젝트 한 벌 — 원가산출에서 한 번 굳힘(`price_book.json`) ·
|
||||
값 = 사본 단가 기준 시점(795-4 · 구조물 지정 때 `단가.json` 자원 줄 · 없는 자원만 지금 M01 +
|
||||
`now_keys`) ·
|
||||
[단가 갱신] 때만 새로 · 대가 · 산근도 이 단가로 셈(한 직종 · 한 자재 = 한 단가).
|
||||
|
||||
`now_keys`) · [단가 갱신] 때만 새로.
|
||||
줄 = 수요 따라(사용자 승인 ① · 10-9) — 엑셀 목록을 기본으로 깔지 않음:
|
||||
연결된 사본이 부른 자원 키(`B09_Estimation_Linked` — 143-1 「연결된 사본」 원천 · 없으면 빈 표)
|
||||
+ 기계가 부르는 운전사(조종원직종 → 인력 원문번호) · 연료(주연료종류 → 유가 「종류:전국평균」)
|
||||
연결된 사본이 부른 자재 키(`B09_Estimation_Linked` — 143-1 「연결된 사본」 원천 · 없으면 빈 표)
|
||||
+ B08 총괄자재수량집계표 자재(M01 짝 없음 → 출처 빈칸 · 적용 단가는 사용자 입력)
|
||||
· 수량 = 합계 열 · 채집석 공제 줄 거름 · 키가 MT 코드(맞춤 없는 사본 자재)면
|
||||
사본 자원 줄에 수량만 얹음 · 파형강관 = 관급 기본
|
||||
운전사 시간 계수 = M01 CC000336(시간환산 · 상여계수 · 휴지계수 분자 · 분모 곱).
|
||||
연료 지역 = 전국평균(M01 기계경비 로직 지역 첫 값) — 다른 지역 값은 자재 적용 단가를 고쳐 씀.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -25,11 +24,6 @@ from B08_Quantity.B08_Quantity_Material_Calc import DEDUCT_KEY
|
||||
from common_util.common_util_time import utc_now_text
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
#: 운전사 시간 노임 계수 표(건설품셈 공통 8-1-3 · 건협기술-2230)
|
||||
DRIVER_TABLE = "CC000336"
|
||||
DRIVER_PARTS = ("시간환산", "상여계수", "휴지계수")
|
||||
#: 연료 지역 — M01 기계경비 로직 지역 고르기 첫 값
|
||||
FUEL_REGION = "전국평균"
|
||||
#: 자재품목 값 열 → 대비표 출처 칸 · 대비표 「관급」 = M01 「조달청」 칸(830-2)
|
||||
MT_SOURCES = ("관급", "물가자료", "유통물가", "물가정보", "거래가격")
|
||||
MT_COLUMN = {"관급": "조달청"}
|
||||
@@ -65,36 +59,6 @@ def _row(master, key: str) -> dict[str, Any] | None:
|
||||
return master.index.get(key[:2], {}).get(key)
|
||||
|
||||
|
||||
def labor_input(row: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"key": row["키"],
|
||||
"code": str(row.get("원문번호") or ""),
|
||||
"name": row.get("이름") or "",
|
||||
"spec": row.get("규격") or "",
|
||||
"unit": "인",
|
||||
"price": _f(row.get("값")),
|
||||
"source": row.get("출처") or "",
|
||||
}
|
||||
|
||||
|
||||
def machine_input(row: dict[str, Any]) -> dict[str, Any]:
|
||||
v = row.get("값") or {}
|
||||
return {
|
||||
"key": row["키"],
|
||||
"code": str(row.get("원문번호") or ""),
|
||||
"name": row.get("이름") or "",
|
||||
"spec": row.get("규격") or "",
|
||||
"price": _f(v.get("가격")),
|
||||
"loss": _f(v.get("손료계수")),
|
||||
"fuel": v.get("주연료종류") or None,
|
||||
"fuel_lph": _f(v.get("주연료")),
|
||||
"misc_pct": _f(v.get("잡재료율")),
|
||||
"crew": _f(v.get("조종원")),
|
||||
"crew_code": str(v["조종원직종"]) if v.get("조종원직종") else None,
|
||||
"source": (row.get("출처") or {}).get("가격") or row.get("원문") or "",
|
||||
}
|
||||
|
||||
|
||||
def material_input(row: dict[str, Any]) -> dict[str, Any]:
|
||||
"""자재품목(MT) · 유가(MO) 줄 → 대비표 입력 줄."""
|
||||
if row["키"].startswith("MO"):
|
||||
@@ -121,82 +85,22 @@ def material_input(row: dict[str, Any]) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def driver_factor(master) -> dict[str, Any] | None:
|
||||
table = _row(master, DRIVER_TABLE)
|
||||
if not table:
|
||||
return None
|
||||
parts = {r.get("구분"): r for r in table.get("줄") or []}
|
||||
if any(p not in parts for p in DRIVER_PARTS):
|
||||
return None
|
||||
top = bottom = 1
|
||||
text = []
|
||||
for p in DRIVER_PARTS:
|
||||
top *= int(parts[p]["분자"])
|
||||
bottom *= int(parts[p]["분모"])
|
||||
text.append(f"{parts[p]['분자']}/{parts[p]['분모']}")
|
||||
return {"num": top, "den": bottom, "text": " × ".join(text)}
|
||||
|
||||
|
||||
def fx_inputs(master) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{"key": key, "name": row.get("이름") or "", "unit": row.get("단위") or "",
|
||||
"value": _f(row.get("값")), "date": row.get("규격") or ""}
|
||||
for key, row in sorted(master.index.get("FX", {}).items())
|
||||
] # fmt: skip
|
||||
|
||||
|
||||
def _fuel_row(master, kind: str) -> dict[str, Any] | None:
|
||||
return master.numbers.get("MO", {}).get(f"{kind}:{FUEL_REGION}")
|
||||
|
||||
|
||||
def demand(master, keys: list[str]) -> dict[str, Any]:
|
||||
"""수요 키 → 줄 차례(노무 · 기계 · 자재) + 연료 짝 · 찾지 못한 키."""
|
||||
labor: list[str] = []
|
||||
machines: list[str] = []
|
||||
"""수요 키 → 자재 줄 차례 · 찾지 못한 키(841-1 노무 · 기계 = 사본 엔진 값 · 책 밖)."""
|
||||
materials: list[str] = []
|
||||
missing: list[str] = []
|
||||
fuel_keys: dict[str, str] = {}
|
||||
|
||||
def add(bucket: list[str], key: str) -> None:
|
||||
if key not in bucket:
|
||||
bucket.append(key)
|
||||
|
||||
for key in keys:
|
||||
if not key.startswith(("MT", "MO")):
|
||||
continue
|
||||
if _row(master, key) is None:
|
||||
missing.append(key)
|
||||
elif key.startswith("LB"):
|
||||
add(labor, key)
|
||||
elif key.startswith("EQ"):
|
||||
add(machines, key)
|
||||
elif key.startswith(("MT", "MO")):
|
||||
add(materials, key)
|
||||
for key in machines:
|
||||
v = _row(master, key).get("값") or {}
|
||||
code = v.get("조종원직종")
|
||||
driver = master.numbers.get("LB", {}).get(str(code)) if code and v.get("조종원") else None
|
||||
if driver:
|
||||
add(labor, driver["키"])
|
||||
kind = v.get("주연료종류")
|
||||
fuel = _fuel_row(master, kind) if kind and v.get("주연료") else None
|
||||
if fuel:
|
||||
add(materials, fuel["키"])
|
||||
fuel_keys[kind] = fuel["키"]
|
||||
return {
|
||||
"labor": labor,
|
||||
"machines": machines,
|
||||
"materials": materials,
|
||||
"fuel_keys": fuel_keys,
|
||||
"missing": missing,
|
||||
}
|
||||
elif key not in materials:
|
||||
materials.append(key)
|
||||
return {"materials": materials, "missing": missing}
|
||||
|
||||
|
||||
def _convert(master, key: str) -> dict[str, Any]:
|
||||
row = _row(master, key)
|
||||
if key.startswith("LB"):
|
||||
return labor_input(row)
|
||||
if key.startswith("EQ"):
|
||||
return machine_input(row)
|
||||
return material_input(row)
|
||||
return material_input(_row(master, key))
|
||||
|
||||
|
||||
def refresh_book(
|
||||
@@ -207,17 +111,12 @@ def refresh_book(
|
||||
`frozen` = 첫 굳힘(795-4) — 사본 단가 기준 시점 값 · 없는 자원 = 지금 값(`now_keys`)."""
|
||||
master = basis(frozen)
|
||||
want = demand(master, keys)
|
||||
rows = {
|
||||
key: _convert(master, key)
|
||||
for key in [*want["labor"], *want["machines"], *want["materials"]]
|
||||
}
|
||||
rows = {key: _convert(master, key) for key in want["materials"]}
|
||||
old = (book or {}).get("rows") or {}
|
||||
rows.update({key: old[key] for key in want["missing"] if key in old})
|
||||
return {
|
||||
"made": utc_now_text(),
|
||||
"rows": rows,
|
||||
"driver_factor": driver_factor(master),
|
||||
"fx": fx_inputs(master),
|
||||
"labor_edition": labor_editions(keys), # 703-3 미리 보기 「노임 판 전 → 후」
|
||||
"now_keys": now_keys(frozen, list(rows)),
|
||||
}
|
||||
@@ -228,7 +127,7 @@ def labor_editions(keys: list[str]) -> dict[str, str]:
|
||||
files, master = m01.loaded()
|
||||
groups = (files.get("인력.json") or {}).get("구분") or {}
|
||||
out: dict[str, str] = {}
|
||||
for key in demand(master, keys)["labor"]:
|
||||
for key in [k for k in keys if k.startswith("LB") and _row(master, k)]:
|
||||
kind = str((_row(master, key) or {}).get("구분") or "")
|
||||
if (groups.get(kind) or {}).get("판"):
|
||||
out[kind] = str(groups[kind]["판"])
|
||||
@@ -236,14 +135,12 @@ def labor_editions(keys: list[str]) -> dict[str, str]:
|
||||
|
||||
|
||||
def book_diff(old: dict[str, Any] | None, new: dict[str, Any]) -> dict[str, int | bool]:
|
||||
"""두 책 견줌(저장 꼴 JSON) — 새 줄 · 바뀐 줄 수 · 운전사 계수 · 환율 바뀜(203-1 · 703-3)."""
|
||||
"""두 책 견줌(저장 꼴 JSON) — 새 줄 · 바뀐 줄 수(203-1 · 703-3 · 자재 줄만)."""
|
||||
new = json.loads(json.dumps(new, ensure_ascii=False))
|
||||
rows = (old or {}).get("rows") or {}
|
||||
return {
|
||||
"new": sum(key not in rows for key in new["rows"]),
|
||||
"changed": sum(key in rows and rows[key] != row for key, row in new["rows"].items()),
|
||||
"factor": (old or {}).get("driver_factor") != new["driver_factor"],
|
||||
"fx": (old or {}).get("fx") != new["fx"],
|
||||
}
|
||||
|
||||
|
||||
@@ -256,8 +153,6 @@ def book_stale(book: dict[str, Any] | None, keys: list[str]) -> str | None:
|
||||
parts = [
|
||||
f"새 단가 줄 {diff['new']}" if diff["new"] else "",
|
||||
f"M01 에서 바뀐 단가 {diff['changed']}" if diff["changed"] else "",
|
||||
"운전사 계수 바뀜" if diff["factor"] else "",
|
||||
"환율 바뀜" if diff["fx"] else "",
|
||||
]
|
||||
return " · ".join(p for p in parts if p) or None
|
||||
|
||||
@@ -272,9 +167,7 @@ def extend_book(
|
||||
master = basis(frozen)
|
||||
want = demand(master, keys)
|
||||
rows = dict(book["rows"])
|
||||
added = [
|
||||
key for key in [*want["labor"], *want["machines"], *want["materials"]] if key not in rows
|
||||
]
|
||||
added = [key for key in want["materials"] if key not in rows]
|
||||
rows.update({key: _convert(master, key) for key in added})
|
||||
late = [*(book.get("now_keys") or []), *now_keys(frozen or {}, added)]
|
||||
return {**book, "rows": rows, "now_keys": list(dict.fromkeys(late))}
|
||||
@@ -326,7 +219,6 @@ def build_inputs(
|
||||
late.append(key)
|
||||
return {**_convert(master, key), "fresh": True}
|
||||
|
||||
factor = (book or {}).get("driver_factor") if book else driver_factor(master)
|
||||
materials = [pick(k) for k in want["materials"]]
|
||||
at = {row["key"]: i for i, row in enumerate(materials)}
|
||||
for row in b08_material_rows(b08_sheets):
|
||||
@@ -336,12 +228,7 @@ def build_inputs(
|
||||
else: # 같은 MT 코드 = 같은 자재 — 두 줄 안 만들고 수량만
|
||||
materials[same] = {**materials[same], "qty": row["qty"]}
|
||||
return {
|
||||
"labor": [pick(k) for k in want["labor"]],
|
||||
"machines": [pick(k) for k in want["machines"]],
|
||||
"materials": materials,
|
||||
"fuel_keys": want["fuel_keys"],
|
||||
"driver_factor": factor,
|
||||
"fx": (book or {}).get("fx") if book else fx_inputs(master),
|
||||
"missing": want["missing"],
|
||||
"now": list(dict.fromkeys(late)), # 위 줄들 pick 뒤 — 사전 차례대로 셈
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""B09 [단가 갱신] · [재생성](PLAN 795-4 · 옛 [업데이트] 703-3) — 사본은 만든 때 그대로 ·
|
||||
[단가 갱신](기본) = 단가 책만 지금 M01 값 · 미리 보기 `price_preview` = 바뀐 자원 · 총괄 전 → 후 ·
|
||||
"""B09 [단가 갱신] · [재생성](PLAN 795-4 · 옛 [업데이트] 703-3) — 사본 로직은 만든 때 그대로 ·
|
||||
[단가 갱신](기본 · 841-1) = 프로젝트 사본 자원 단가 줄(`B09_Estimation_Copy_Prices`) + 자재 단가 책만
|
||||
지금 M01 값 · 미리 보기 `price_preview` = 바뀐 자원 · 총괄 전 → 후 ·
|
||||
[재생성](사용자가 따로) = 아래 옛 [업데이트] — 단가 책 다시 굳힘 + 원본이 바뀐 구조물 사본 [재적용].
|
||||
|
||||
미리 보기(`preview`) = 바뀌는 단가 줄(새 줄 + 바뀐 줄 · 지금 수요) · 다시 가져올 구조물 사본
|
||||
@@ -34,12 +35,16 @@ def _value(row: dict[str, Any] | None) -> Any:
|
||||
|
||||
|
||||
def price_preview(
|
||||
old: dict[str, Any] | None, new: dict[str, Any], before: dict, after: dict, cells: int
|
||||
old: dict[str, Any] | None,
|
||||
new: dict[str, Any],
|
||||
before: dict,
|
||||
after: dict,
|
||||
cells: int,
|
||||
copies: list[dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""[단가 갱신] 미리 보기(795-4) — {할일, 바뀐자원 [{키, 이름, 전, 후}], 계수, 환율, 전, 후
|
||||
(총괄 grand · 빈 줄 있으면 None), 대가 {전, 후}(일위대가 블록 합), 고친칸, 노임판} ·
|
||||
사본(로직)은 안 바뀜."""
|
||||
diff = prices.book_diff(old, new)
|
||||
"""[단가 갱신] 미리 보기(795-4 · 841-1) — {할일, 바뀐자원 [{키, 이름, 전, 후}](사본 자원 줄 +
|
||||
자재 단가 책), 전, 후(총괄 grand · 빈 줄 있으면 None), 대가 {전, 후}(일위대가 블록 합), 고친칸,
|
||||
노임판} · 사본(로직)은 안 바뀜."""
|
||||
was = json.loads(json.dumps((old or {}).get("rows") or {}, ensure_ascii=False))
|
||||
now = json.loads(json.dumps(new["rows"], ensure_ascii=False))
|
||||
rows = [
|
||||
@@ -54,11 +59,10 @@ def price_preview(
|
||||
def daega(sheets: dict) -> float:
|
||||
return sum(float(b.get("sum") or 0) for b in sheets.get("daega_blocks") or [])
|
||||
|
||||
seen = {r["키"] for r in copies or []}
|
||||
return {
|
||||
"할일": "단가갱신",
|
||||
"바뀐자원": rows,
|
||||
"계수": diff["factor"],
|
||||
"환율": diff["fx"],
|
||||
"바뀐자원": [*(copies or []), *(r for r in rows if r["키"] not in seen)],
|
||||
"전": grand(before),
|
||||
"후": grand(after),
|
||||
"대가": {"전": daega(before), "후": daega(after)},
|
||||
@@ -138,8 +142,6 @@ def preview(
|
||||
"단가줄": diff["new"] + diff["changed"],
|
||||
"새줄": diff["new"],
|
||||
"바뀐줄": diff["changed"],
|
||||
"계수": diff["factor"],
|
||||
"환율": diff["fx"],
|
||||
"사본": copies + lib_names,
|
||||
"단가산출": sk_changes(root, copies, libs),
|
||||
"고친칸": cover_cells(cover),
|
||||
|
||||
@@ -73,6 +73,7 @@ from B08_Quantity.B08_Quantity_Router_Material import once as material_once
|
||||
from B08_Quantity.B08_Quantity_Router_Milestone import read_data as read_milestone
|
||||
from B08_Quantity.B08_Quantity_Router_Milestone import site_info
|
||||
from B09_Estimation import B09_Estimation_Bill_Library as library
|
||||
from B09_Estimation import B09_Estimation_Copy_Prices as copy_prices
|
||||
from B09_Estimation import B09_Estimation_Daega_Paste as daega_paste
|
||||
from B09_Estimation import B09_Estimation_Daega_Personal as personal
|
||||
from B09_Estimation import B09_Estimation_Linked as linked
|
||||
@@ -99,7 +100,7 @@ from B09_Estimation.B09_Estimation_Summary import (
|
||||
totals,
|
||||
used_materials,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_Supply import supply_defaults
|
||||
from B09_Estimation.B09_Estimation_Supply import price_lists, 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
|
||||
@@ -166,8 +167,10 @@ 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, supply) # 145-1 M02 엔진 값으로 풀이(841-1 · 166-2 손 줄)
|
||||
blocks = (inputs.get("daega") or {}).get("blocks") or []
|
||||
inputs["labor"], inputs["machines"] = price_lists(blocks, sg["sg"]) # 841-1 엔진 단가 나열
|
||||
base = compute_base(inputs, user)
|
||||
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(
|
||||
@@ -368,8 +371,8 @@ async def refresh_prices(
|
||||
body: dict[str, Any] | None = Body(None),
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
) -> dict[str, Any]:
|
||||
"""[단가 갱신](795-4) — 단가 책 다시 굳힘 · 사본 · 덮개 그대로 ·
|
||||
job 「재생성」 = 원본이 바뀐 구조물 사본 재적용(703-3) 먼저."""
|
||||
"""[단가 갱신](795-4 · 841-1) — 프로젝트 사본 자원 단가 줄 + 자재 단가 책을 지금 M01 값으로 ·
|
||||
사본 로직 · 덮개 그대로 · job 「재생성」 = 원본이 바뀐 구조물 사본 재적용(703-3) 먼저."""
|
||||
root = await _project_root(project_id)
|
||||
raw = (body or {}).get("user")
|
||||
draft = store.clean_user(raw) if isinstance(raw, dict) else None # 296 화면 캐시(저장 안 함)
|
||||
@@ -377,6 +380,8 @@ async def refresh_prices(
|
||||
again = {"다시": [], "막힘": []}
|
||||
if regen:
|
||||
again = await asyncio.to_thread(refresh.reapply_changed, root, importer(session))
|
||||
news, changed = await asyncio.to_thread(copy_prices.refreshed, root)
|
||||
await asyncio.to_thread(copy_prices.write, news)
|
||||
keys = await _demand(root, draft)
|
||||
old = await asyncio.to_thread(store.read_book, root)
|
||||
book = await asyncio.to_thread(prices.refresh_book, keys, old)
|
||||
@@ -387,7 +392,8 @@ async def refresh_prices(
|
||||
cells = refresh.cover_cells(kept.get("daega_cover")) + refresh.cover_cells(kept.get("sg_cover"))
|
||||
head = f"재생성 — 구조물 사본 {len(again['다시'])} 다시 가져옴 · " if regen else "단가 갱신 — "
|
||||
out["notes"] = [
|
||||
f"{head}단가 줄 {diff['new'] + diff['changed']} 바뀜 · 고친 칸 {cells} 그대로",
|
||||
f"{head}사본 {len(news)} · 자원 {len(changed)} · 자재 단가 줄 "
|
||||
f"{diff['new'] + diff['changed']} 바뀜 · 고친 칸 {cells} 그대로",
|
||||
*(f"재적용 못 함 {why}" for why in again["막힘"]),
|
||||
*(out.get("notes") or []),
|
||||
]
|
||||
@@ -409,10 +415,15 @@ async def refresh_preview(
|
||||
if (body or {}).get("job") == refresh.REGEN:
|
||||
return await asyncio.to_thread(refresh.preview, root, book, keys, user.get("daega_cover"))
|
||||
new = await asyncio.to_thread(prices.refresh_book, keys, book)
|
||||
news, changed = await asyncio.to_thread(copy_prices.refreshed, root)
|
||||
before = await payload(project_id, root, draft, True, keys)
|
||||
after = await payload(project_id, root, draft, True, keys, new)
|
||||
token = copy_prices.OVER.set(news) # 841-1 사본 새 단가 = 덮개로만(파일 안 씀)
|
||||
try:
|
||||
after = await payload(project_id, root, draft, True, keys, new)
|
||||
finally:
|
||||
copy_prices.OVER.reset(token)
|
||||
cells = refresh.cover_cells(user.get("daega_cover")) + refresh.cover_cells(user.get("sg_cover"))
|
||||
return refresh.price_preview(book, new, before["sheets"], after["sheets"], cells)
|
||||
return refresh.price_preview(book, new, before["sheets"], after["sheets"], cells, changed)
|
||||
|
||||
|
||||
@router.post("/{project_id}/estimation/calc")
|
||||
|
||||
@@ -19,9 +19,8 @@
|
||||
POST /api/projects/{id}/estimation/import/rates (multipart: file | file_key · user?)
|
||||
원가계산서 요율(707-2) → {file_key, name, kind, rows, rates, notes, check, patch{estimate}}
|
||||
POST /api/projects/{id}/estimation/import/prices (multipart: file | file_key · user?)
|
||||
노임 · 자재 · 기계 단가표(707-3) → 이 공사 단가 책에 이음 →
|
||||
{file_key, name, sheets, counts, linked, missing,
|
||||
patch{labor_cover, machine_cover, materials}}
|
||||
자재 단가표(707-3 · 841-1 노임 · 기계 걷음) → 이 공사 자재단가대비표에 이음 →
|
||||
{file_key, name, sheets, counts, linked, missing, patch{materials}}
|
||||
file_key = 세 길 같은 표지(`_keep` · `_saved`) — 같은 파일을 다시 올리지 않음 ·
|
||||
.estx(EST 원본 · 707-5 `B09_Estimation_Estx`) · .STC(STmate 원본 · 707-6 `B09_Estimation_Stc`) =
|
||||
이름 끝으로 갈림 — 내역 줄 · 단가표 같은 그릇 · 요율 = 400 「같은 공사 엑셀에서」 ·
|
||||
|
||||
@@ -43,6 +43,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 SUPPLY, gap, num, rounddown, total
|
||||
from B09_Estimation.B09_Estimation_Copy_Prices import bundle
|
||||
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
|
||||
@@ -55,7 +56,7 @@ from M02_MasterTemplete.M02_Item_Snapshot import (
|
||||
linked_snapshots,
|
||||
structures_dir,
|
||||
)
|
||||
from M02_MasterTemplete.M02_Item_Source import LABOR_FILE, PRICE_FILE, read_json
|
||||
from M02_MasterTemplete.M02_Item_Source import LABOR_FILE, read_json
|
||||
|
||||
mf = item_prices.mf
|
||||
import master_sheet_line as msl # noqa: E402 — 계산서 줄 덧칸(714-1) · scripts 길은 `m01` 이 둠
|
||||
@@ -109,7 +110,7 @@ def rows_of(folder: Path, whole: Any = None) -> list[dict[str, Any]]:
|
||||
if not isinstance(labor, dict):
|
||||
return []
|
||||
if whole is None:
|
||||
whole = item_prices.load_prices(read_json(folder / PRICE_FILE) or {})
|
||||
whole = item_prices.load_prices(bundle(folder))
|
||||
values = read_json(folder / VALUES_FILE) or {}
|
||||
picks = read_json(folder / PICKS_FILE) or {}
|
||||
copies = labor.get("참조사본") or {}
|
||||
@@ -630,7 +631,7 @@ def collect(
|
||||
return copy.deepcopy(memo["sg"])
|
||||
solver = Solver(supply)
|
||||
for owner, folder in entries:
|
||||
whole = item_prices.load_prices(read_json(folder / PRICE_FILE) or {})
|
||||
whole = item_prices.load_prices(bundle(folder))
|
||||
for src in rows_of(folder, whole):
|
||||
solver.add(whole, src, owner)
|
||||
hand_keys: dict[str, str] = {}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
거울 시험 `resources/tester/test_719_3_sangeun_cover.py`.
|
||||
조작 중 = 화면이 TS 짝으로 바로 셈 · [저장] = 서버가 다시 풀이(`Sangeun.build`) 뒤 이 파일로 정본.
|
||||
|
||||
덮개 = 사용자 값 `sg_cover` {산근 키: {줄 열쇠 「n:이름」: {qty?, labor?, material?, expense?}}} —
|
||||
qty = 1 단위 품 · 비목 = 단가 · 있는 칸만 셈 값 대신 · 줄이 없거나 이름이 다르면 짝 잃음
|
||||
덮개 = 사용자 값 `sg_cover` {산근 키: {줄 열쇠 「n:이름」: {qty?}}} —
|
||||
qty = 1 단위 품(841-1 단가 칸 걷음 · 옛 단가 칸은 안 읽음) · 있는 칸만 셈 값 대신 · 줄이 없거나 이름이 다르면 짝 잃음
|
||||
(`sg_orphans` · 알림만 · 덮개는 남음) · 산근 키 = 로직 + 입력값 → [업데이트] 뒤에도 같은 블록.
|
||||
고칠 칸(`fields`) = 노무 · 자재 · 중기 · 빈 줄 = 품 + 쓰는 비목 단가 · 하위 산근 = 품 ·
|
||||
고칠 칸(`fields`) = 노무 · 자재 · 중기 · 빈 줄 · 하위 산근 = 품 ·
|
||||
% 덧줄(`of` 있는 것) = 율.
|
||||
다시 셈 = 덮개가 닿은 블록과 그 블록을 부르는 블록만(안 닿은 블록 = 서버 값 그대로) —
|
||||
줄 금액 ROUNDDOWN(단가 × 품, 1) · 하위 산근 줄 단가 = 다시 센 그 블록 비목 합 ·
|
||||
@@ -29,7 +29,7 @@ from typing import Any
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import gap, num, rounddown, total
|
||||
|
||||
PARTS = ("labor", "material", "expense")
|
||||
COVER_KEYS = ("qty", *PARTS)
|
||||
COVER_KEYS = ("qty",) # 841-1 품만
|
||||
DEPTH = 8 # 하위 산근 깊이 상한(서버 풀이 4 보다 넉넉히)
|
||||
|
||||
|
||||
@@ -39,21 +39,11 @@ def line_id(n: int, line: dict[str, Any]) -> str:
|
||||
|
||||
|
||||
def fields(line: dict[str, Any]) -> list[str]:
|
||||
"""고칠 수 있는 칸 — 품(qty) + 단가 비목."""
|
||||
"""고칠 수 있는 칸 — 품(qty)만(841-1 단가 칸 걷음 · 단가 = M02 사본 값) · % 덧줄 = 율."""
|
||||
kind = line.get("kind")
|
||||
if kind == "sangeun":
|
||||
return ["qty"]
|
||||
if kind == "extra":
|
||||
return ["qty"] if line.get("of") else []
|
||||
if kind == "machine":
|
||||
return ["qty", *PARTS]
|
||||
if kind == "labor":
|
||||
return ["qty", "labor"]
|
||||
if kind == "material":
|
||||
return ["qty", *[p for p in PARTS if p in (line.get("price") or {})]]
|
||||
if kind == "blank":
|
||||
return ["qty", *[p for p in PARTS if line.get(p) is None]]
|
||||
return []
|
||||
return ["qty"] if kind in ("sangeun", "machine", "labor", "material", "blank") else []
|
||||
|
||||
|
||||
def _money(price: float | None, qty: float | None) -> float | None:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*
|
||||
* 덮개 = 사용자 값 `sg_cover` {산근 키: {줄 열쇠 「n:이름」: {qty?, labor?, material?, expense?}}} —
|
||||
* 있는 칸만 셈 값 대신 · 줄이 없거나 이름이 다르면 짝 잃음(`sg_orphans` · 알림만)
|
||||
* 고칠 칸(`fields`) = 노무 · 자재 · 중기 · 빈 줄 = 품 + 쓰는 비목 단가 · 하위 산근 = 품 · % 덧줄(`of`) = 율
|
||||
* 고칠 칸(`fields`) = 품만(841-1 단가 칸 걷음) · % 덧줄(`of`) = 율
|
||||
* 다시 셈 = 덮개가 닿은 블록과 그 블록을 부르는 블록만 — 줄 금액 ROUNDDOWN(단가 × 품, 1) ·
|
||||
* 하위 산근 줄 단가 = 다시 센 그 블록 비목 합 · % 덧줄 = 서버 값 × 밑수 비 × 율 비(처음이 0 · 빈칸이면
|
||||
* 밑수 × 율 ÷ 100) · 총계 = 비목마다 ROUNDDOWN(Σ, 0) · 목록표 · 빈칸 목록 같이
|
||||
@@ -20,7 +20,7 @@
|
||||
import { gap, num, rounddown, total, type Num, type Row } from "./B09_Estimation_Calc_Base";
|
||||
|
||||
const PARTS = ["labor", "material", "expense"] as const;
|
||||
export const SG_COVER_KEYS = ["qty", ...PARTS] as const;
|
||||
export const SG_COVER_KEYS = ["qty"] as const; // 841-1 품만
|
||||
const DEPTH = 8;
|
||||
|
||||
/** 덮개 줄 열쇠 — 블록 안 차례 + 이름 */
|
||||
@@ -29,13 +29,9 @@ export const lineId = (n: number, line: Row): string => `${n}:${line.name || ""}
|
||||
/** 고칠 수 있는 칸 — 품(qty) + 단가 비목 */
|
||||
export function fields(line: Row): string[] {
|
||||
const kind = line.kind;
|
||||
if (kind === "sangeun") return ["qty"];
|
||||
// 841-1 단가 칸 걷음 — 품(qty)만 · % 덧줄 = 율(짝 `fields`)
|
||||
if (kind === "extra") return line.of ? ["qty"] : [];
|
||||
if (kind === "machine") return ["qty", ...PARTS];
|
||||
if (kind === "labor") return ["qty", "labor"];
|
||||
if (kind === "material") return ["qty", ...PARTS.filter((p) => p in (line.price || {}))];
|
||||
if (kind === "blank") return ["qty", ...PARTS.filter((p) => (line[p] ?? null) === null)];
|
||||
return [];
|
||||
return ["sangeun", "machine", "labor", "material", "blank"].includes(kind) ? ["qty"] : [];
|
||||
}
|
||||
|
||||
const money = (price: Num | undefined, qty: Num | undefined): Num =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""B09 원가 저장 자리(PLAN 144-1) — 프로젝트 안 `B09_Estimation/v1/`.
|
||||
|
||||
price_book.json 굳힌 단가 한 벌 {"made": 시각, "rows": {키: 입력 줄},
|
||||
"driver_factor": …, "fx": […]}
|
||||
price_book.json 굳힌 자재 단가 한 벌 {"made": 시각, "rows": {키: 입력 줄}}(841-1 자재만 ·
|
||||
옛 책의 노무 · 기계 줄 · 계수 · 환율은 안 읽음)
|
||||
— [단가 갱신] 때만 M01 로 새로 ·
|
||||
[저장] 때 책에 없는 새 줄만 더함(있는 줄 안 바꿈)
|
||||
user_values.json 사용자 값 {"materials": {자재 키: {applied?, supply?, quote?, quote_page?,
|
||||
@@ -12,13 +12,10 @@
|
||||
— 고친 칸만(키가 있으면 그 값 · null = 빈칸 · 없으면 기본)
|
||||
"daega_cover": {블록 cover: {줄 cid: {qty?, labor?, material?, expense?}}}
|
||||
(702-2 일위대가 고친 칸 덮개 — 수만 · 셈 = `B09_Estimation_Calc_Labor`)
|
||||
"sg_cover": {산근 키: {줄 「n:이름」: {qty?, labor?, material?, expense?}}}
|
||||
"sg_cover": {산근 키: {줄 「n:이름」: {qty?}}}(841-1 단가 칸 걷음 · 품만)
|
||||
(719-3 산근 고친 칸 — 셈 = `B09_Estimation_Sangeun_Cover`)
|
||||
+ 복사본 `K:…` 의 `@copy` {from · name · spec}(703-2 · 글만)
|
||||
+ 붙인 줄 `@add` [블록 줄 fixed · pct](705-2 · `clean_added`)
|
||||
"labor_cover": {노무 키: 일당} · "machine_cover": {기계 키: {labor,
|
||||
material, expense}}(707-3 노임 · 기계 단가 덮개 · 공사 몫 · [업데이트] 가
|
||||
책만 새로 굳혀 덮개는 남음 · 셈 = `B09_Estimation_Calc_Base`)
|
||||
|
||||
quotes/<자재 키>/ 견적서 파일(166-4 · 여러 개) — 폴더가 목록(키의 못 쓰는 글자는 %XX) ·
|
||||
읽을 때 materials[키].files = 이름들 · 사용자 값으로는 저장 안 함
|
||||
@@ -36,7 +33,7 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import unquote
|
||||
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import SOURCES, SUPPLY, machine_cover
|
||||
from B09_Estimation.B09_Estimation_Calc_Base import SOURCES, SUPPLY
|
||||
from B09_Estimation.B09_Estimation_Calc_Bill import SECTIONS
|
||||
from B09_Estimation.B09_Estimation_Calc_Labor import ADD, COVER_KEYS, PARTS
|
||||
|
||||
@@ -357,11 +354,6 @@ def clean_user(raw: Any) -> dict[str, Any]:
|
||||
out["daega_cover"] = cover
|
||||
if cover := clean_cover(raw.get("sg_cover")): # 719-3 산근 고친 칸(같은 꼴 · 줄 「n:이름」)
|
||||
out["sg_cover"] = cover
|
||||
given = raw.get("labor_cover") if isinstance(raw.get("labor_cover"), dict) else {}
|
||||
if labor := {str(k)[:200]: _num(v) for k, v in given.items() if _num(v) is not None}:
|
||||
out["labor_cover"] = labor
|
||||
if machine := machine_cover(raw.get("machine_cover")):
|
||||
out["machine_cover"] = {str(k)[:200]: v for k, v in machine.items()}
|
||||
if clean_schedule and (schedule := clean_schedule(raw.get("schedule"))):
|
||||
out["schedule"] = schedule
|
||||
return out
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""B09 자재 관급 · 별도 표시(PLAN 841-1) — M02 자재표시를 대비표 기본 표시로 · 서버 단독 · 순수 함수.
|
||||
"""B09 자재 관급 · 별도 표시 · 사본 단가 목록(PLAN 841-1) — 서버 단독 · 순수 함수.
|
||||
|
||||
자재 표시 = M02 자재표시를 대비표 기본 표시로 · 노무비 · 중기목록 = 엔진 단가 나열(`price_lists`).
|
||||
|
||||
M02 일위대가 · 단가산출 셈은 재료 줄을 자재표시(자재구분 관급 · 별도계상 — 빈 칸 = 별도계상)로
|
||||
합에서 뺌(`M02_Labor_Doc.apart`) → B09 도 같은 표시로 대가 · 산근 안 0 원 · 4 자재대 · 관급자재대로
|
||||
@@ -12,6 +14,8 @@ from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
|
||||
|
||||
PARTS = ("labor", "material", "expense")
|
||||
|
||||
|
||||
def supply_mark(one: Any) -> str | None:
|
||||
"""M02 자재표시(841-1) → 대비표 기본 표시 — 관급 · 별도(별도계상 · M02 기본) · None(직접계산)."""
|
||||
@@ -48,3 +52,23 @@ def supply_defaults(inputs: dict[str, Any], user: dict[str, Any]) -> Any:
|
||||
return None if got == "사급" else got or default.get(key, marks.get(key, "별도"))
|
||||
|
||||
return supply
|
||||
|
||||
|
||||
def price_lists(blocks: list[dict[str, Any]], sg_blocks: list[dict[str, Any]]) -> tuple[list, list]:
|
||||
"""노무비목록표 · 중기목록표 입력(841-1) — 대가 입력 줄 `own` · 산근 줄 `price` = M02 엔진 단가
|
||||
(사본 값) 나열 · 다시 안 셈 · 수량 있는 줄만. ponytail: 키 하나 = 처음 본 단가(사본마다 시점이 달라도)."""
|
||||
labor: dict[str, dict] = {}
|
||||
machine: dict[str, dict] = {}
|
||||
for block in [*blocks, *sg_blocks]:
|
||||
for ln in block.get("lines") or []:
|
||||
kind = ln.get("kind")
|
||||
key = ln.get("key") or (ln.get("ref") or {}).get("key")
|
||||
if kind not in ("labor", "machine") or not key or not ln.get("qty"):
|
||||
continue
|
||||
price = ln.get("own") or ln.get("price") or {}
|
||||
head = {"key": key, "name": ln.get("name") or "", "spec": ln.get("spec") or ""}
|
||||
if kind == "labor" and price.get("labor") is not None:
|
||||
labor.setdefault(key, {**head, "unit": "인", "price": price["labor"]})
|
||||
elif kind == "machine" and None not in [price.get(p) for p in PARTS]:
|
||||
machine.setdefault(key, {**head, **{p: price[p] for p in PARTS}})
|
||||
return list(labor.values()), list(machine.values())
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* B09_Estimation_UI_Check.ts
|
||||
* B09 좌측 「데이터 점검」 묶음(PLAN 203-1) — [단가 갱신] · 연결 점검(옛 「연결 점검」 탭 내용).
|
||||
*
|
||||
* [단가 갱신](795-4 · 옛 [업데이트]) = 자원 단가만 지금 M01 값 · 서버가 낡았다고 보면(`payload.stale` 까닭 한 줄)
|
||||
* [단가 갱신](795-4 · 옛 [업데이트] · 841-1) = 프로젝트 사본 자원 단가 · 자재 단가만 지금 M01 값 · 서버가 낡았다고 보면(`payload.stale` 까닭 한 줄)
|
||||
* 단추에 점 배지(`is-pending`) · 굳힌 시각 · 까닭 = 공용 호버
|
||||
* 미리 보기 고리 = 누름을 먼저 잡아 POST …/estimation/prices/preview(화면 캐시) →
|
||||
* 「총괄 금액 전 → 후 · 바뀐 자원」 창 → [적용] = 원래 단추 일(단가만 · 사본 · 덮개 그대로) ·
|
||||
@@ -68,7 +68,7 @@ export function refreshTip(payload: Row | null): string {
|
||||
const made = payload?.book?.made;
|
||||
const when = made ? `단가 굳힘 ${dateTimeText(String(made))}` : "단가 아직 안 굳힘";
|
||||
const stale = payload?.stale;
|
||||
return `${when} · 누르면 미리 보기 → [적용] = 자원 단가만 지금 M01 값 · 사본(로직) · 고친 칸 그대로 · 로직까지 새로 = 미리 보기의 [재생성]${stale ? ` · 단가 갱신 가능 — ${stale}` : ""}`;
|
||||
return `${when} · 누르면 미리 보기 → [적용] = 프로젝트 사본 자원 단가 · 자재 단가만 지금 M01 값 · 사본(로직) · 고친 칸 그대로 · 로직까지 새로 = 미리 보기의 [재생성]${stale ? ` · 단가 갱신 가능 — ${stale}` : ""}`;
|
||||
}
|
||||
|
||||
/** [재생성] 미리 보기 글 줄(703-3 옛 [업데이트]) */
|
||||
@@ -77,8 +77,7 @@ export function previewLines(got: Row): string[] {
|
||||
const sks: Row[] = got.단가산출 || [];
|
||||
const edition = got.노임판 || {};
|
||||
return [
|
||||
`바뀌는 단가 줄 ${got.단가줄 ?? 0}(새 줄 ${got.새줄 ?? 0} · 값 바뀜 ${got.바뀐줄 ?? 0})` +
|
||||
`${got.계수 ? " · 운전사 계수 바뀜" : ""}${got.환율 ? " · 환율 바뀜" : ""}`,
|
||||
`바뀌는 자재 단가 줄 ${got.단가줄 ?? 0}(새 줄 ${got.새줄 ?? 0} · 값 바뀜 ${got.바뀐줄 ?? 0})`,
|
||||
`다시 가져올 사본 ${copies.length}${copies.length ? ` — ${copies.map((c) => c.이름).join(" · ")}` : ""}`,
|
||||
...(sks.length
|
||||
? [`M02 단가산출 바뀜 ${sks.length} — ${sks.map((s) => `${s.이름}(${s.판})`).join(" · ")}`]
|
||||
@@ -105,7 +104,7 @@ export function priceLines(got: Row): string[] {
|
||||
const shown = 30;
|
||||
return [
|
||||
`총괄 금액 ${won(got.전)} → ${won(got.후)} 원 · 대가 합 ${won(got.대가?.전)} → ${won(got.대가?.후)} 원`,
|
||||
`바뀐 자원 ${rows.length}${got.계수 ? " · 운전사 계수 바뀜" : ""}${got.환율 ? " · 환율 바뀜" : ""}`,
|
||||
`바뀐 자원 ${rows.length}(사본 자원 단가 · 자재 단가)`,
|
||||
...rows
|
||||
.slice(0, shown)
|
||||
.map((r) => `· ${r.이름 || r.키} ${priceText(r.전)} → ${priceText(r.후)}`),
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
*
|
||||
* 요율 · 기초 단가표 불러오기(PLAN 707-2 · 707-3) — 저장 안 함.
|
||||
* 단추 = 보조 탭 줄 도구 자리(`…_UI_TabBar` setTools) — 총괄 · 원가계산서 탭 [엑셀에서 요율 불러오기] ·
|
||||
* 노무비목록표 · 중기목록표 · 자재단가대비표 탭 [엑셀에서 단가표 불러오기]
|
||||
* 자재단가대비표 탭 [엑셀에서 단가표 불러오기](841-1 노임 · 기계 걷음)
|
||||
* 길 = `B09_Estimation_Router_Import.py` import/rates · import/prices(file | file_key · user = 화면 캐시) →
|
||||
* 확인 창(짝 표 · 식 다름 · 짝 없음 n) → [적용] = 답 `patch` 를 캐시에 얹음 · [저장] 때 서버 정본
|
||||
* 올린 파일 = 내역 줄 · 요율 · 단가표 세 길이 같은 표지(file_key · `last`) — 같은 파일 다시 안 올림
|
||||
@@ -65,7 +65,7 @@ async function post(projectId: string, path: string, body: BodyInit, json = fals
|
||||
|
||||
/** 받은 캐시 = 지금 캐시 + 내역 덮개 · 원가계산서 값(조달수수료율 · 요율) · 단가표 덮개(707-7) */
|
||||
function merged(user: Row, made: Row): Row {
|
||||
const keep = ["labor_cover", "machine_cover", "materials"].filter((k) => k in made);
|
||||
const keep = ["materials"].filter((k) => k in made); // 841-1 노임 · 기계 덮개 걷음
|
||||
return {
|
||||
...user,
|
||||
bill: made.bill,
|
||||
@@ -227,7 +227,7 @@ export function createBillImport(
|
||||
later ? `원가계산서 요율 — ${later}` : "원가계산서 요율 — 율 덮개",
|
||||
{ checked: !later, disabled: !!later },
|
||||
);
|
||||
const prices = createCheckbox("단가표 — 노임 · 자재 · 기계 단가를 이 공사 단가 책에 덮음", {
|
||||
const prices = createCheckbox("단가표 — 자재 단가를 이 공사 자재단가대비표에 덮음", {
|
||||
checked: true,
|
||||
});
|
||||
const take = createCheckbox("넘겨받기 — 불러온 수량을 씀(수량 산출 값은 곁에)", {
|
||||
@@ -330,9 +330,7 @@ type Kind = "rates" | "prices";
|
||||
const TABS: Partial<Record<TabId, Kind>> = {
|
||||
total: "rates",
|
||||
cost: "rates",
|
||||
labor: "prices",
|
||||
machine: "prices",
|
||||
compare: "prices",
|
||||
compare: "prices", // 841-1 노임 · 기계 단가 덮개 걷음 — 자재단가대비표만
|
||||
};
|
||||
const LABEL: Record<Kind, string> = {
|
||||
rates: "엑셀에서 요율 불러오기",
|
||||
|
||||
@@ -12,23 +12,22 @@
|
||||
* · 「견적서」 칸(견적 묶음 아래 · 183-2) 누름 = 파일 모달(`B09_Estimation_UI_Tabs_Base_Quote`)(166-4)
|
||||
* 견적서 글자 = 견적 단가 수기 줄만(185-5 · 나머지 빈칸 · 빈칸 누름 = 안 엶)
|
||||
* · 출처 묶음 = 단가 칸 하나 · 출처 뜻(M01 면수 · 유가 기준일)은 단가 칸 호버(185-4 `sourceInfo`)
|
||||
* 이동 = 재료비목록표 줄 → 대비표 · 중기목록표 줄 → 중기사용료 블록 · 블록 줄 → 경비 · 기초 · 대비표
|
||||
* 이동 = 재료비목록표 줄 → 대비표
|
||||
* 841-1 노무비목록표 · 중기목록표 = 프로젝트 사본(M02) 엔진 단가 나열 · 경비목록표 · 중기사용료 ·
|
||||
* 환율및기초자료 = 「—」 한 줄 + 까닭 한 줄(B09 가 기계 시간당 · 운전사 노임을 따로 셈하던 탭)
|
||||
* ========================================================================== */
|
||||
|
||||
import type { MapContextMenuItem } from "@ui/ui_template_context_menu";
|
||||
import type { DataRow } from "@ui/ui_template_data_table";
|
||||
import { SOURCES, type Row } from "./B09_Estimation_Calc_Base";
|
||||
import { formatPrice } from "../common_util/common_util_money";
|
||||
import { dateText } from "../common_util/common_util_time";
|
||||
import { openQuotes } from "./B09_Estimation_UI_Tabs_Base_Quote";
|
||||
import {
|
||||
blockLevel,
|
||||
money,
|
||||
parseNumber,
|
||||
qty,
|
||||
text,
|
||||
type TabBuild,
|
||||
type TabId,
|
||||
type TabTable,
|
||||
} from "./B09_Estimation_UI_Tabs";
|
||||
|
||||
@@ -51,69 +50,34 @@ function priceList(rows: Row[], unitPrice: string, hint: string): TabTable {
|
||||
};
|
||||
}
|
||||
|
||||
export const buildLabor: TabBuild = ({ sheets, payload }) =>
|
||||
/** 841-1 B09 가 따로 셈하던 탭 — 「—」 한 줄 + 까닭 한 줄(값 = M02 단가산출 그대로) */
|
||||
const GONE =
|
||||
"M02 단가산출 값을 그대로 씀 — B09 가 따로 셈하지 않음(기계 시간당 · 운전사 노임 포함)";
|
||||
const gone = (columns: TabTable["columns"]): TabTable => ({
|
||||
columns,
|
||||
rows: [{ key: "-", name: "—" }],
|
||||
hint: GONE,
|
||||
});
|
||||
|
||||
export const buildLabor: TabBuild = ({ sheets }) =>
|
||||
priceList(
|
||||
sheets.labor || [],
|
||||
"단가(원/인)",
|
||||
`이 공사 단가 책(${madeText(payload)}) 기준 · 새 노임은 [단가 갱신]`,
|
||||
"대가 · 산근이 쓰는 노임 = 프로젝트 사본(M02) 값 나열 · 새 값은 [단가 갱신]",
|
||||
);
|
||||
|
||||
export const buildExpense: TabBuild = ({ sheets, payload }) =>
|
||||
priceList(
|
||||
sheets.expense || [],
|
||||
"기준가(천원)",
|
||||
`이 공사 단가 책(${madeText(payload)}) 기준 · 새 노임은 [단가 갱신]`,
|
||||
);
|
||||
export const buildExpense: TabBuild = () => gone(priceList([], "기준가(천원)", "").columns);
|
||||
|
||||
function madeText(payload: Row): string {
|
||||
return payload.book?.made ? `${dateText(String(payload.book.made))} 정함` : "아직 안 정함";
|
||||
}
|
||||
|
||||
export const buildBasis: TabBuild = ({ sheets }): TabTable => {
|
||||
const basis: Row = sheets.basis || { fx: [], drivers: [] };
|
||||
const rows: DataRow[] = [
|
||||
...basis.fx.map((f: Row) => ({
|
||||
part: "1. 환율",
|
||||
key: f.key,
|
||||
name: f.name,
|
||||
spec: f.date,
|
||||
unit: f.unit,
|
||||
day: null,
|
||||
factor: "",
|
||||
value: f.value,
|
||||
})),
|
||||
...basis.drivers.map((d: Row) => ({
|
||||
part: "2. 인건비",
|
||||
key: d.key,
|
||||
name: d.name,
|
||||
spec: `직종 ${d.key}`,
|
||||
unit: "원/시간",
|
||||
day: d.day,
|
||||
factor: d.factor,
|
||||
value: d.hour,
|
||||
labor_key: d.labor_key,
|
||||
})),
|
||||
];
|
||||
return {
|
||||
columns: [
|
||||
text("part", "구분", 90),
|
||||
text("name", "명칭", 160),
|
||||
text("spec", "규격 · 기준일", 120),
|
||||
text("unit", "단위", 80),
|
||||
money("day", "일당(원)"),
|
||||
text("factor", "시간 환산", 160),
|
||||
money("value", "값", undefined, 2),
|
||||
],
|
||||
rows,
|
||||
frozen: 2,
|
||||
mergeSame: ["part"],
|
||||
hint: "인건비 줄을 누르면 노무비목록표로 이동",
|
||||
link: (i) => {
|
||||
const row = rows[i];
|
||||
return row?.labor_key ? { tab: "labor", key: String(row.labor_key) } : null;
|
||||
},
|
||||
};
|
||||
};
|
||||
export const buildBasis: TabBuild = (): TabTable =>
|
||||
gone([
|
||||
text("part", "구분", 90),
|
||||
text("name", "명칭", 160),
|
||||
text("spec", "규격 · 기준일", 120),
|
||||
text("unit", "단위", 80),
|
||||
money("day", "일당(원)"),
|
||||
text("factor", "시간 환산", 160),
|
||||
money("value", "값", undefined, 2),
|
||||
]);
|
||||
|
||||
// 841-1 기본 표시(M02 자재표시)가 있어도 끌 수 있게 — 별도 다음 = 사급(표시 끔)
|
||||
const SUPPLY_NEXT: Record<string, string | null> = { "": "관급", 관급: "별도", 별도: "사급" };
|
||||
@@ -326,79 +290,18 @@ export const buildMachine: TabBuild = ({ sheets }): TabTable => {
|
||||
],
|
||||
rows,
|
||||
frozen: 2,
|
||||
hint: LINK_HINT,
|
||||
link: (i) => (rows[i] ? { tab: "machine_blocks", key: String(rows[i].key) } : null),
|
||||
hint: "대가 · 산근이 쓰는 기계 시간당 = 프로젝트 사본(M02 단가산출) 값 나열 · 새 값은 [단가 갱신]",
|
||||
};
|
||||
};
|
||||
|
||||
const KIND_TAB: Record<string, TabId> = {
|
||||
expense: "expense",
|
||||
labor: "basis",
|
||||
material: "compare",
|
||||
};
|
||||
|
||||
export const buildMachineBlocks: TabBuild = ({ sheets }): TabTable => {
|
||||
const rows: DataRow[] = [];
|
||||
const links: Array<{ tab: TabId; key: string } | null> = [];
|
||||
(sheets.machine_blocks || []).forEach((b: Row, n: number) => {
|
||||
rows.push({
|
||||
key: b.key,
|
||||
name: `제 ${n + 1} 호표 ${b.name}`,
|
||||
no: n + 1,
|
||||
title: b.name, // 171-3 묶음 카드 머리
|
||||
spec: b.spec,
|
||||
unit: b.unit,
|
||||
});
|
||||
links.push(null);
|
||||
for (const ln of b.lines) {
|
||||
const at = (part: string): number | null => (ln.kind === part ? ln.price : null);
|
||||
const amt = (part: string): number | null => (ln.kind === part ? ln.amount : null);
|
||||
rows.push({
|
||||
key: `${b.key}|${ln.name}`,
|
||||
name: ln.name,
|
||||
spec: ln.spec,
|
||||
qty: ln.qty,
|
||||
unit: ln.unit,
|
||||
sum_price: ln.price,
|
||||
sum: ln.amount,
|
||||
labor_price: at("labor"),
|
||||
labor: amt("labor"),
|
||||
material_price: at("material"),
|
||||
material: amt("material"),
|
||||
expense_price: at("expense"),
|
||||
expense: amt("expense"),
|
||||
});
|
||||
links.push(ln.ref?.key ? { tab: KIND_TAB[ln.kind] ?? "compare", key: ln.ref.key } : null);
|
||||
}
|
||||
rows.push({
|
||||
key: `${b.key}|합계`,
|
||||
name: "합 계",
|
||||
sum: b.sum,
|
||||
labor: b.labor,
|
||||
material: b.material,
|
||||
expense: b.expense,
|
||||
});
|
||||
links.push(null);
|
||||
});
|
||||
const pair = (key: string, label: string) => [
|
||||
money(`${key}_price`, "단가", [label], 3),
|
||||
money(key, "금액", [label], 1),
|
||||
];
|
||||
return {
|
||||
columns: [
|
||||
text("name", "명칭", 220),
|
||||
text("spec", "규격", 120),
|
||||
qty("qty", "수량"),
|
||||
text("unit", "단위", 56),
|
||||
...pair("sum", "합계"),
|
||||
...pair("labor", "노무비"),
|
||||
...pair("material", "재료비"),
|
||||
...pair("expense", "경비"),
|
||||
],
|
||||
rows,
|
||||
frozen: 1,
|
||||
hint: LINK_HINT,
|
||||
link: (i) => links[i] ?? null,
|
||||
level: blockLevel("name", (i) => !String(rows[i]?.key).includes("|")),
|
||||
};
|
||||
};
|
||||
export const buildMachineBlocks: TabBuild = (): TabTable =>
|
||||
gone([
|
||||
text("name", "명칭", 220),
|
||||
text("spec", "규격", 120),
|
||||
qty("qty", "수량"),
|
||||
text("unit", "단위", 56),
|
||||
money("sum", "금액", ["합계"], 1),
|
||||
money("labor", "금액", ["노무비"], 1),
|
||||
money("material", "금액", ["재료비"], 1),
|
||||
money("expense", "금액", ["경비"], 1),
|
||||
]);
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*
|
||||
* 값 = 일위대가표와 같은 셈(`sheets.daega_blocks` · `B09_Estimation_Calc_Labor`) — 펼친 값 = 일위대가표 값
|
||||
* 줄 = 호표 줄(품명 · 규격 · 품 · 수량 · 단위 · 노 · 재 · 경 단가 · 금액 · 품셈 값) · 부른 대가 = 그 아래 「ㄴ」(3단까지)
|
||||
* 고친 칸 덮개(702-2) = 품 · 수량 · 단가 칸 두 번 눌러 고침 → 사용자 값 `daega_cover`
|
||||
* {블록 cover: {줄 cid: {qty · labor · material · expense}}} · 캐시에 바로 · 셈 짝이 바로 다시 셈 ·
|
||||
* 고친 칸 덮개(702-2) = 품 칸 두 번 눌러 고침 → 사용자 값 `daega_cover`(841-1 단가 칸 걷음)
|
||||
* {블록 cover: {줄 cid: {qty}}} · 캐시에 바로 · 셈 짝이 바로 다시 셈 ·
|
||||
* [저장] 때 서버가 덮개 입력값으로 정본을 다시 셈 · 노임 새 판 · [재적용] 뒤에도 남음(702-4)
|
||||
* 고친 칸 = 칸 색(`is-edited`) · 「품셈 값」 칸 = 셈 값 · 우클릭 [품셈 값으로] · 빈칸 입력 = 되돌림
|
||||
* M02 단가산출 블록 줄(`sk` · 719-2) = 되돌림 [단가산출 값으로](셈 값 = 단가산출 값) · 곁 글 = 품셈 값(`poom`)
|
||||
@@ -37,13 +37,8 @@ import {
|
||||
} from "../M02_MasterTemplete/M02_MasterTemplete_Hopyo_Relink";
|
||||
import { relinkRows, type PasteCols } from "../M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api";
|
||||
|
||||
/** 고칠 칸 → 덮개 칸 */
|
||||
const FIELD: Record<string, string> = {
|
||||
qty: "qty",
|
||||
labor_price: "labor",
|
||||
material_price: "material",
|
||||
expense_price: "expense",
|
||||
};
|
||||
/** 고칠 칸 → 덮개 칸 — 841-1 품만(단가 = M02 사본 값 · 고치지 않음) */
|
||||
const FIELD: Record<string, string> = { qty: "qty" };
|
||||
const LABEL: Record<string, string> = { qty: "품", labor: "노", material: "재", expense: "경" };
|
||||
/** 부른 대가 펼침 깊이 */
|
||||
const DEPTH = 3;
|
||||
|
||||
Reference in New Issue
Block a user