Merge remote-tracking branches 'origin/sub_laptop_1', 'origin/sub_laptop_2' and 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -87,6 +87,8 @@ interface Entry {
|
||||
qty: string;
|
||||
qtyTag: string;
|
||||
qtyFrag: Fragment[];
|
||||
/** 한 줄이 비목 몫으로 갈렸을 때 이 묶음의 몫 이름(예 「노무비」) */
|
||||
share?: string;
|
||||
price: string;
|
||||
amount: number | null;
|
||||
/** 덧줄이면 true */
|
||||
@@ -188,7 +190,12 @@ function infoBox(ctx: DetailContext): HTMLElement {
|
||||
const spread = (e: Entry, line?: CalcLine): Entry[] => {
|
||||
const parts = Object.entries(line?.비목 ?? {}).filter(([, v]) => v !== 0);
|
||||
if (!parts.length) return [e];
|
||||
return parts.map(([k, v]) => ({ ...e, group: OF_COST[k] ?? "other", amount: v }));
|
||||
return parts.map(([k, v]) => ({
|
||||
...e,
|
||||
group: OF_COST[k] ?? "other",
|
||||
amount: v,
|
||||
...(parts.length > 1 ? { share: k } : {}),
|
||||
}));
|
||||
};
|
||||
|
||||
function entries(ctx: DetailContext): Entry[] {
|
||||
@@ -275,6 +282,9 @@ function lineRow(e: Entry): HTMLElement {
|
||||
el("td", {
|
||||
children: [
|
||||
el("div", { text: e.name }),
|
||||
...(e.share
|
||||
? [el("div", { className: "m01-logic__tag", text: `${e.share} ${tl("Share")}` })]
|
||||
: []),
|
||||
...(e.spec ? [el("div", { className: "m01-logic__muted", text: e.spec })] : []),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -30,6 +30,7 @@ const TEXT = {
|
||||
G_other: ["그 밖 (인력·자재·경비 어디에도 안 맞는 줄)", "Other (fits none of the three)"],
|
||||
Tag_Table: ["표", "Table"],
|
||||
Subtotal: ["소계", "Subtotal"],
|
||||
Share: ["몫", "Share"],
|
||||
Total: ["계", "Total"],
|
||||
Extra: ["덧줄", "Extra"],
|
||||
Col_Name: ["이름", "Name"],
|
||||
|
||||
@@ -139,7 +139,12 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
|
||||
ctx.onText?.(answer);
|
||||
});
|
||||
}
|
||||
if (ctx.savedKey !== null && !autoAsked.has(ctx.values)) {
|
||||
if (!fields.length && !autoAsked.has(ctx.values)) {
|
||||
autoAsked.add(ctx.values); // 입력이 없으면 채울 것이 없음 — 열 때 한 번 바로 계산
|
||||
queueMicrotask(() => {
|
||||
if (previewSeq === mySeqAtOpen) host.querySelector("button")?.click();
|
||||
});
|
||||
} else if (ctx.savedKey !== null && !autoAsked.has(ctx.values)) {
|
||||
autoAsked.add(ctx.values);
|
||||
void fetchAuto(ctx.savedKey).then((auto) => {
|
||||
const names = (ctx.row.입력 ?? [])
|
||||
|
||||
@@ -352,11 +352,29 @@ def _shares(line: dict) -> list[tuple[str, Decimal]]:
|
||||
return [(part, money) for part, money in parts if money != 0] or parts[:1]
|
||||
|
||||
|
||||
def _loose(node, env: dict, master) -> bool:
|
||||
"""수량식 맨 위가 덧셈·뺄셈이면(곱 뒤에 오면 괄호가 필요함) · 만약은 걸린 갈래 기준."""
|
||||
if node[0] == "bin":
|
||||
return _PREC[node[1]] < _PREC["*"]
|
||||
if node[0] == "call" and node[1] == "만약":
|
||||
try:
|
||||
at = 1 if mf.evaluate(node[2][0], env, master, 0) else 2
|
||||
except _CALC_ERRORS:
|
||||
return False # 이름 꼴은 「만약(…)」 부르기 그대로라 괄호 불필요
|
||||
return _loose(node[2][at], env, master)
|
||||
return False
|
||||
|
||||
|
||||
def _times(text: str, item: dict, env: dict, master) -> str:
|
||||
"""「단가 * 수량식」 의 수량식 — 덧셈·뺄셈 묶음이면 괄호."""
|
||||
return f"({text})" if _loose(mf.parse(item["수량"]), env, master) else text
|
||||
|
||||
|
||||
def _line_text(item: dict, line: dict, env: dict, master, part: str | None) -> str:
|
||||
"""「단가 * 수량식 = 금액」 — 비목이 나뉜 로직 줄은 그 비목 몫만 · 빈 줄(금액 null)은 빈 글."""
|
||||
if line["금액"] is None:
|
||||
return ""
|
||||
qty = render(mf.parse(item["수량"]), env, master)
|
||||
qty = _times(render(mf.parse(item["수량"]), env, master), item, env, master)
|
||||
money = line["비목"][part] if part else line["금액"]
|
||||
price = line["단가"] if part is None or line["수량"] == 0 else money / line["수량"]
|
||||
return f"{fmt_money(price)} * {qty} = {fmt_money(money)}"
|
||||
@@ -368,7 +386,8 @@ def _line_names(item: dict, line: dict, env: dict, master, part: str | None, nam
|
||||
tail = PRICE_TAIL.get(str(item.get("종류") or ""), "단가")
|
||||
price = f"{name}{part or ''}{tail}"
|
||||
money = f"{name}{part}" if part else f"{name}비"
|
||||
return f"{price} * {_names(item['수량'], env, master, name_of)} = {money}"
|
||||
qty = _times(_names(item["수량"], env, master, name_of), item, env, master)
|
||||
return f"{price} * {qty} = {money}"
|
||||
|
||||
|
||||
def _extra_text(extra: dict, line: dict, env: dict, master) -> str:
|
||||
|
||||
@@ -331,3 +331,30 @@ def test_빈_줄은_빈_글이고_소계는_빈_줄_빼고_더한다(client: Tes
|
||||
blank = [one for one in _all_lines(got) if one["금액"] is None]
|
||||
assert blank and all(one["글"] == "" and one["까닭"] for one in blank)
|
||||
assert Decimal(str(got["계"])) == Decimal(str(calc["sums"]["계"]))
|
||||
|
||||
|
||||
def test_곱_뒤_덧셈_묶음은_괄호로_이름_식_값_식_모두() -> None:
|
||||
"""GC000467 촌락지대가 — 노임 * (아홉 항 합) · 첫 항에만 곱하는 것처럼 읽히지 않게."""
|
||||
files = store.cm.load(folder=REAL)
|
||||
row = store.cm.master(REAL).logic("GC000467")
|
||||
given = {n: store._dec(v) for n, v in store.auto("GC000467")["값"].items()}
|
||||
got = mt.lines(files, "GC000467", given)
|
||||
one = _all_lines(got)[0]
|
||||
assert one["글"].startswith("295,138 * (1 + 1 + 0.5") and " = 3,984,363" in one["글"]
|
||||
left = one["이름글"].split(" = ")[0]
|
||||
assert left.startswith("중급기술자건설노임 * (") and left.endswith(".수량)")
|
||||
assert Decimal(str(one["금액"])) == Decimal("295138") * Decimal("13.5")
|
||||
blank = mt.lines(files, "GC000467", {})
|
||||
assert _all_lines(blank)[0]["이름글"].split(" = ")[0].count("(") == 1 # 값 없이도 그대로
|
||||
assert row # 로직이 있음
|
||||
|
||||
|
||||
def test_이름글_번호_별칭은_별칭표로_되돌려진다() -> None:
|
||||
"""이름글 속 별칭(가1~가9)은 별칭표 이름 그대로여야 식으로 되돌려짐(찾은 줄 이름 X)."""
|
||||
files = store.cm.load(folder=REAL)
|
||||
given = {n: store._dec(v) for n, v in store.auto("GC000467")["값"].items()}
|
||||
got = mt.lines(files, "GC000467", given)
|
||||
left = _all_lines(got)[0]["이름글"].split(" = ")[0].split(" * ", 1)[1].strip("()")
|
||||
raw_of = {m["이름"]: m["원문"] for m in got["별칭"]}
|
||||
back = mt.mc._unmask(left, raw_of)
|
||||
assert back.count("찾기(") == 9 and "촌락지대가" not in back
|
||||
|
||||
Reference in New Issue
Block a user