Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
# Conflicts: # M01_MasterData/M01_MasterData_Router.py
This commit is contained in:
@@ -242,7 +242,7 @@ def call_of(whole: Any, item: dict, env: dict) -> dict[str, Any] | None:
|
||||
target = None
|
||||
if not target:
|
||||
return None
|
||||
if any(s.get("이름") == "기계" for s in target.get("입력", [])):
|
||||
if mf.mb.is_machine(target): # 기계경비 판별 한 곳(712-1)
|
||||
keys = _captured(whole, node, env)
|
||||
return {"machine": keys[0] if keys else None}
|
||||
given = {name: mf.evaluate(expr, env, whole, 1) for name, expr in node[2]}
|
||||
|
||||
@@ -276,7 +276,7 @@ def _logic_lines(whole, item, env, qty_text, q, price_of) -> list[tuple[str, dic
|
||||
except mf.FormulaError:
|
||||
target = None
|
||||
name = item.get("이름", "")
|
||||
if target and any(s.get("이름") == "기계" for s in target.get("입력", [])):
|
||||
if mf.mb.is_machine(target): # 기계경비 판별 한 곳(712-1)
|
||||
keys = sg._captured(whole, node, env)
|
||||
got = price_of(keys[0]) if keys else None
|
||||
if not got:
|
||||
|
||||
@@ -18,6 +18,7 @@ from M01_MasterData import M01_MasterData_Store_Flow as flow
|
||||
from M01_MasterData import M01_MasterData_Store_List as store_list
|
||||
from M01_MasterData import M01_MasterData_Store_Make as make
|
||||
from M01_MasterData import M01_MasterData_Store_Revise as revise
|
||||
from M01_MasterData import M01_MasterData_Store_Sheet as sheet
|
||||
|
||||
import master_basis as mbs # noqa: E402 (`store` 가 scripts 를 길에 둠)
|
||||
|
||||
@@ -275,7 +276,7 @@ def post_text(body: CalcBody) -> dict:
|
||||
if got is not None:
|
||||
return got
|
||||
with store.cm.mf.mp.choosing(body.고름):
|
||||
return _call(store.text, body.key, body.inputs, body.row, body.file)
|
||||
return _call(sheet.text, body.key, body.inputs, body.row, body.file) # 줄 덧칸(714-1)
|
||||
|
||||
|
||||
@router.post("/save")
|
||||
|
||||
@@ -17,6 +17,7 @@ from config import config_system
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
cm, mk, mcb = store.cm, store.mk, store.mcb
|
||||
import master_unit as mu # noqa: E402 — `store` 가 scripts 를 길에 둔 뒤(712-2)
|
||||
|
||||
FILE = mcb.FILE
|
||||
OWNERS = mcb.OWNERS
|
||||
@@ -150,9 +151,6 @@ def combo(key: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
_PER = re.compile(
|
||||
r"원\s*/\s*(\d[\d,]*\.?\d*|\.\d+)?\s*([^\s·(\d,.][^\s·(]*)"
|
||||
) # 「원/1,000본」 = 배 1000
|
||||
_TAIL = re.compile(r"\s*\([^()]*\)\s*$")
|
||||
|
||||
|
||||
@@ -177,7 +175,7 @@ def combo_pills(row: dict) -> dict:
|
||||
got = whole.logic(code)
|
||||
except cm.mf.FormulaError:
|
||||
continue
|
||||
per = _PER.findall(str(got.get("결과단위") or ""))
|
||||
per = mu.per(got.get("결과단위")) # 결과단위 배 읽기 한 벌(712-2)
|
||||
if not per:
|
||||
continue
|
||||
# ponytail: 분모가 여럿(「원/㎡ · 원/㎥(단위당 따름)」)이면 「㎡ · ㎥」 한 글 — 입력이 고름
|
||||
@@ -187,9 +185,9 @@ def combo_pills(row: dict) -> dict:
|
||||
(name, unit), {"키": f"{name}|{unit}", "이름": name, "단위": unit, "로직": []}
|
||||
)
|
||||
pill["로직"].append(code)
|
||||
times = per[0][0].replace(",", "")
|
||||
if len(per) == 1 and times and float(times) != 1:
|
||||
pill["배"] = float(times) if "." in times else int(times)
|
||||
times = per[0][0]
|
||||
if len(per) == 1 and times != 1:
|
||||
pill["배"] = int(times) if times == times.to_integral_value() else float(times)
|
||||
return {"키": row.get("키"), "이름": row.get("이름"), "물량": list(pills.values()), "조건": []}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""M01 `/text` 계산서 줄 덧칸(PLAN 714-1) — `store.text` 답에 줄마다 쓰는 입력 · 묶음 · 기준단위 ·
|
||||
환산 근거(`master_sheet_line.annotate` — M02 `/logic-lib/sheet` 와 한 벌) · 서버 단독(③) · 저장 안 함.
|
||||
`M01_MasterData_Store.py` 는 700 줄 근처라 여기서 감쌈."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
mf = store.cm.mf
|
||||
import master_sheet_line as msl # noqa: E402 — `store` 가 scripts 를 길에 둔 뒤
|
||||
|
||||
|
||||
def text(key: str, inputs: dict, row: dict | None = None, file: str | None = None) -> dict:
|
||||
got = store.text(key, inputs, row, file)
|
||||
if not isinstance(got, dict) or not got.get("ok"):
|
||||
return got
|
||||
given = {k: store._dec(v) for k, v in inputs.items()}
|
||||
try:
|
||||
if row is None:
|
||||
master = store.loaded()[1]
|
||||
else: # 저장 전 시험 — `store.text` 와 같은 메모리 사본
|
||||
files = store.cm.load(folder=store.FOLDER)
|
||||
key = store._swap(files, str(row.get("키") or key), row, file)
|
||||
master = mf.Master(files)
|
||||
return msl.annotate(master, key, mf.fill_empty(master.logic(key), given), got)
|
||||
except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError):
|
||||
return got
|
||||
@@ -32,9 +32,9 @@ from M01_MasterData import M01_MasterData_Store_Cache as m01_cache
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import conds, mtypes, specnums
|
||||
|
||||
mb, mu = m01.cm.mf.mb, m01.cm.mf.mp.mm.mu # 묶음(712-1) · 단위(712-2) 한 벌
|
||||
FP_VERSION = 1 # 지문 꼴이 바뀌면 올림 — 옛 캐시 버림
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
_PER = re.compile(r"원\s*/\s*(\d[\d,]*\.?\d*|\.\d+)?")
|
||||
_SUB = re.compile(r"^\s*로직\(\s*(\w+)\s*(?:,(.*))?\)\s*$")
|
||||
EQ_CODE = re.compile(r"^\d{4}-\d{4}$")
|
||||
_PCT = re.compile(r"\*\s*([\d.]+)\s*/\s*100")
|
||||
@@ -84,7 +84,7 @@ def whole() -> Any:
|
||||
|
||||
|
||||
def is_mach_logic(row: dict[str, Any]) -> bool:
|
||||
return "시간당" in str(row.get("이름") or "")
|
||||
return mb.is_machine(row) # 기계경비 판별 한 곳(712-1 · 결과단위 원/시간)
|
||||
|
||||
|
||||
def eq_of(code: Any) -> tuple[set[str], set[float]]:
|
||||
@@ -95,8 +95,8 @@ def eq_of(code: Any) -> tuple[set[str], set[float]]:
|
||||
|
||||
|
||||
def times_of(row: dict[str, Any]) -> float:
|
||||
per = _PER.findall(str(row.get("결과단위") or ""))
|
||||
return float(per[0].replace(",", "")) if per and per[0] else 1.0
|
||||
per = mu.per(row.get("결과단위")) # 결과단위 배 읽기 한 벌(712-2)
|
||||
return float(per[0][0]) if per else 1.0
|
||||
|
||||
|
||||
def _args(text: str | None) -> dict[str, str]:
|
||||
|
||||
@@ -31,6 +31,9 @@ from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs
|
||||
from M02_MasterTemplete import M02_Logic_Settle as settle
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
mf = m01.cm.mf
|
||||
import master_sheet_line as msl # noqa: E402 — 계산서 줄 덧칸(714-1) · scripts 길은 `m01` 이 둠
|
||||
|
||||
SHAPE = 3 # 줄 꼴이 바뀌면 올림 — 옛 저장본을 안 씀
|
||||
KIND = "m02_logic_standard"
|
||||
REGION = "지역" # 지역 = 자동값 한 벌(정함 8)
|
||||
@@ -331,7 +334,7 @@ def sheet(key: str, change: dict) -> dict:
|
||||
raise m01.StoreError(404, str(e)) from e
|
||||
given, ax = _given(whole, logic, change)
|
||||
filled = m01.cm.mf.fill_empty(logic, given)
|
||||
got = m01.mtx.lines(files, key, filled)
|
||||
got = msl.annotate(whole, key, filled, m01.mtx.lines(files, key, filled)) # 줄 덧칸(714-1)
|
||||
done = settle.settle(whole, key, filled)
|
||||
if "결과" not in done: # 돈 로직 — 줄마다 버림(B09 산근 규칙)
|
||||
settle.apply(got.get("groups") or [], m01.cm.mf.run(whole, key, filled), done)
|
||||
@@ -341,7 +344,7 @@ def sheet(key: str, change: dict) -> dict:
|
||||
"비목": g["비목"],
|
||||
"소계": g.get("소계"),
|
||||
"끝수": g.get("끝수") or "",
|
||||
"줄": [{k: line.get(k) for k in ("이름", "글", "금액")} for line in g["줄"]],
|
||||
"줄": [{k: line.get(k) for k in ("이름", "글", "금액", *msl.KEYS)} for line in g["줄"]],
|
||||
}
|
||||
for g in got.get("groups") or []
|
||||
]
|
||||
@@ -356,5 +359,6 @@ def sheet(key: str, change: dict) -> dict:
|
||||
"입력": given,
|
||||
"묶음": groups,
|
||||
"계": got.get("계"),
|
||||
"기준단위": got.get("기준단위"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -15,10 +15,9 @@ import re
|
||||
from decimal import Decimal
|
||||
from functools import lru_cache
|
||||
|
||||
import master_unit as mu
|
||||
from check_master_body import section_of
|
||||
|
||||
# ponytail: 단위 별칭은 여기 짧은 표 — 712-2 단위 모듈이 서면 그쪽 별칭으로 갈아 끼움
|
||||
_ALIAS = {"ea": "개", "톤": "ton", "t": "ton", "㎞": "km", "hr": "시간", "min": "분", "㎏": "kg"}
|
||||
_UNITS = sorted(
|
||||
(
|
||||
"㎡ ㎥ ㎢ ㎞ km ㎜ mm ㎝ cm m ha ton 톤 t kg ㎏ 개소 개 본 대 회 조 기 틀 "
|
||||
@@ -33,11 +32,10 @@ _UNIT_AT_END = re.compile(
|
||||
)
|
||||
_HANGUL = re.compile(r"[가-힣]")
|
||||
_HEAD = re.compile(r"^\(\s*(?:단위\s*:\s*)?([^()]*당[^()]*)\)$") # 원문 머리 줄 「(단위: ㎥당)」
|
||||
_RESULT = re.compile(r"원\s*/\s*(\d[\d,]*\.?\d*)?\s*([^\s·(\d,.][^\s·(]*)") # 「원/10㎡」
|
||||
|
||||
|
||||
def unit(text: str) -> str:
|
||||
return _ALIAS.get(text, text)
|
||||
return mu.shown(text) # 보이는 꼴 별칭 한 벌(712-2 `master_unit`)
|
||||
|
||||
|
||||
def _times(text: str | None) -> Decimal:
|
||||
@@ -97,7 +95,7 @@ def basis(table: dict) -> dict:
|
||||
|
||||
def result_per(result_unit) -> list[tuple[Decimal, str]]:
|
||||
"""로직 결과단위 분모 — 「원/10㎡」 → [(10, ㎡)] · 여럿이면 여럿."""
|
||||
return [(_times(n), unit(u)) for n, u in _RESULT.findall(str(result_unit or ""))]
|
||||
return [(n, unit(u)) for n, u in mu.per(result_unit)]
|
||||
|
||||
|
||||
def _formula_text(row: dict) -> str:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""인력 · 재료 · 경비 묶음 — **셈 때 파생**(정본에 칸 없음 · PLAN 712-1 · 요구서 원칙 5).
|
||||
|
||||
규칙 한 줄:
|
||||
- 호표 · 덧줄 = `비목` 따라(노무비 → 인력 · 재료비 → 재료 · 경비 → 경비).
|
||||
- 비목 없는 로직 줄 = 부르는 로직이 기계경비(`is_machine`)면 경비 + `장비` · 아니면 하위.
|
||||
두 줄 모두 엔진 줄 `비목` 에 노무비 · 재료비 · 경비 세 몫이 그대로 남음(한 줄 · 흩어 넣기 둘 다).
|
||||
|
||||
기계경비 판별 = `is_machine` 한 곳 — M01 엔진 · M02 엑셀 맞춤(`M02_Hopyo_Ours`) ·
|
||||
B09 산근(`B09_Estimation_Sangeun` · `_Text`)이 같이 씀.
|
||||
엔진 줄 덧칸 = `묶음`(출력 꼬리표만 · 입력 값 덧칸 `값묶음` 은 715-1b 몫) · 기계경비 줄 `장비` true.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
import master_formula as mf
|
||||
|
||||
BUNDLES = ("인력", "재료", "경비", "하위")
|
||||
BY_ITEM = {"노무비": "인력", "재료비": "재료", "경비": "경비"}
|
||||
BY_KIND = {"인력": "인력", "재료": "재료", "기계": "경비"} # 비목 빈 옛 줄 받침
|
||||
|
||||
|
||||
def is_machine(row: dict | None) -> bool:
|
||||
"""기계경비 로직 — 돈 로직이고 결과단위 「원/시간」(작업량 「㎥/시간」 · 이름 「시간당」 아님)."""
|
||||
if not row or "결과" in row or isinstance(row.get("흐름"), dict):
|
||||
return False
|
||||
return re.sub(r"\s", "", str(row.get("결과단위") or "")) == "원/시간"
|
||||
|
||||
|
||||
def called(master, item: dict, env: dict | None = None) -> dict | None:
|
||||
"""로직 줄이 부르는 로직 줄(정본) — 못 찾으면 None."""
|
||||
try:
|
||||
node = mf.parse(item.get("요소") or "")
|
||||
if not node or node[0] != "logic":
|
||||
return None
|
||||
key = mf.fill(node[1], env) if env is not None and "{" in node[1] else node[1]
|
||||
return master.logic(key)
|
||||
except (mf.FormulaError, mf.EmptyInput, KeyError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def bundle(master, item: dict, env: dict | None = None) -> tuple[str, bool]:
|
||||
"""줄 하나(호표 · 덧줄 정본 줄) → (묶음, 장비인가)."""
|
||||
if item.get("비목") in BY_ITEM:
|
||||
return BY_ITEM[item["비목"]], False
|
||||
if item.get("종류") == "로직":
|
||||
return ("경비", True) if is_machine(called(master, item, env)) else ("하위", False)
|
||||
return BY_KIND.get(str(item.get("종류")), "하위"), False
|
||||
|
||||
|
||||
def tag(master, row: dict, lines: list[dict], env: dict | None = None) -> list[dict]:
|
||||
"""엔진 `run` 줄에 `묶음`(+ 기계경비 줄 `장비`) — 줄 차례 = 호표 줄 하나씩 뒤 덧줄 하나씩."""
|
||||
for item, line in zip([*row.get("호표", []), *row.get("덧줄", [])], lines):
|
||||
name, machine = bundle(master, item, env)
|
||||
line["묶음"] = name
|
||||
if machine:
|
||||
line["장비"] = True
|
||||
return lines
|
||||
@@ -657,6 +657,7 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
|
||||
}
|
||||
)
|
||||
sums = {k: cut(row, v, "계") for k, v in sums.items()} # 비목 합마다 끊고 계는 그 합
|
||||
mb.tag(master, row, lines, env) # 줄마다 인력 · 재료 · 경비 · 하위 묶음(712-1)
|
||||
return {"줄": lines, **sums, "계": sum(sums.values()), "중간": _shown(env)}
|
||||
|
||||
|
||||
@@ -676,6 +677,7 @@ from master_material import ( # noqa: E402, F401
|
||||
pick,
|
||||
)
|
||||
import master_pick as mp # noqa: E402 — 검색어 · 고름 · 단위
|
||||
import master_bundle as mb # noqa: E402 — 인력 · 재료 · 경비 묶음
|
||||
from master_formula_check import ( # noqa: E402, F401 — 검사는 나눈 파일 · 이름은 여기서 그대로
|
||||
_check_cut,
|
||||
_check_node,
|
||||
|
||||
@@ -12,6 +12,8 @@ import re
|
||||
import unicodedata
|
||||
from decimal import Decimal
|
||||
|
||||
import master_unit as mu
|
||||
|
||||
LINKED = "MP" # 품셈재료 테이블ID
|
||||
MARKET = "MT" # 자재품목 테이블ID
|
||||
PRICED = "MO" # 유가전력 테이블ID — 값 한 칸
|
||||
@@ -27,7 +29,6 @@ PICK_KEYS = (
|
||||
) # 재료 고르기 조건 칸 (`_틀.md` 7장)
|
||||
REGION = "자재지역" # 로직 입력 — 「지역」 은 유가 고르기가 씀
|
||||
TARIFF = "전력 계약종별" # 로직 입력 — 품셈·계약예규에 계약종별 규정 없음
|
||||
_UNITS = {"ea": "개", "ton": "톤", "t": "톤", "l": "ℓ", "리터": "ℓ", "set": "조", "세트": "조"}
|
||||
|
||||
|
||||
def plain(s) -> str:
|
||||
@@ -41,8 +42,7 @@ def plain_spec(s) -> str:
|
||||
|
||||
|
||||
def plain_unit(s) -> str:
|
||||
s = plain(s).replace("ℓ", "l")
|
||||
return _UNITS.get(s, s)
|
||||
return mu.plain(s) # 별칭 · 맞대기 꼴 한 벌(712-2)
|
||||
|
||||
|
||||
def _offers(master: mf.Master, key: str) -> list[tuple[Decimal, str]]:
|
||||
|
||||
@@ -9,18 +9,10 @@ from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from contextvars import ContextVar
|
||||
from decimal import Decimal
|
||||
|
||||
import master_material as mm
|
||||
from master_unit import UNITS, convert, kind_of, same_kind # noqa: F401 — 단위 한 벌(712-2)
|
||||
|
||||
# 「같은 성격」 단위 — {성격: {정규 단위: 기준 단위 몇 배}} (키는 `plain_unit` 을 거친 꼴)
|
||||
UNITS = {
|
||||
"무게": {"g": 1, "kg": 1000, "톤": 1000000},
|
||||
"길이": {"mm": 1, "cm": 10, "m": 1000, "km": 1000000},
|
||||
"넓이": {"cm2": 1, "m2": 10000},
|
||||
"부피": {"cm3": 1, "ℓ": 1000, "m3": 1000000},
|
||||
}
|
||||
_SCALE = {u: (kind, n) for kind, group in UNITS.items() for u, n in group.items()}
|
||||
REGIONS = (
|
||||
"전국평균 전국 서울 경기 강원 충북 충남 전북 전남 경북 경남 부산 대구 인천 대전 울산 세종 제주 광주"
|
||||
" 전남광주"
|
||||
@@ -60,27 +52,6 @@ def machine(spec: dict, value) -> bool:
|
||||
return bool(picks) and str(value).strip() in known and all(str(o) in known for o in picks)
|
||||
|
||||
|
||||
# ── 단위 ───────────────────────────────────────────────────────────────
|
||||
def kind_of(unit) -> str | None:
|
||||
got = _SCALE.get(mm.plain_unit(unit))
|
||||
return got[0] if got else None
|
||||
|
||||
|
||||
def same_kind(a, b) -> bool:
|
||||
"""단위가 같거나 같은 성격(무게 · 길이 · 넓이 · 부피)."""
|
||||
pa, pb = mm.plain_unit(a), mm.plain_unit(b)
|
||||
return pa == pb or (kind_of(a) is not None and kind_of(a) == kind_of(b))
|
||||
|
||||
|
||||
def convert(price, have, want):
|
||||
"""`have` 단위 단가 → `want` 단위 단가 — 같은 성격이 아니면 그대로."""
|
||||
if price is None or not want or mm.plain_unit(have) == mm.plain_unit(want):
|
||||
return price
|
||||
if not same_kind(have, want) or kind_of(have) is None:
|
||||
return price
|
||||
return price * Decimal(_SCALE[mm.plain_unit(want)][1]) / Decimal(_SCALE[mm.plain_unit(have)][1])
|
||||
|
||||
|
||||
# ── 찾기 ───────────────────────────────────────────────────────────────
|
||||
def _words(term) -> tuple[list[str], list[str]]:
|
||||
"""(이름·규격에 든 낱말, 지역 낱말) — 지역 낱말은 고르는 차례에만 쓰임."""
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""계산서 줄 덧칸 — 「쓰는 입력」 · 묶음 · 기준단위 · 환산 근거(PLAN 714-1 · 요구서 4장 5 · 8).
|
||||
|
||||
`master_text.lines` 답(비목 묶음 줄)에 줄마다 덧칸을 얹음 — M01 `/text` 와 M02 `/logic-lib/sheet`
|
||||
두 길이 같은 함수(`annotate`)를 거쳐 같은 줄 꼴(`KEYS`)을 냄. 셈은 `master_formula.run` 한 벌.
|
||||
- `쓰는입력` = 그 줄 값(수량 식 · 요소 · 덧줄 식)에 쓰인 로직 입력 이름 — 중간값을 거슬러 감 ·
|
||||
덧줄이 부르는 비목 합 · 줄 금액은 거스르지 않음(율 식에 직접 쓴 입력만).
|
||||
- `묶음` · `장비` = 712-1 `master_bundle`(엔진 줄 덧칸 그대로).
|
||||
- `기준단위` = 그 줄 수량 식이 찾는 표의 기준(「10㎡」 · 712-3 `master_basis.basis`) · 답 머리
|
||||
`기준단위` = 로직 결과단위 분모(「10㎡」 · 여럿이면 「㎡ · ㎥」).
|
||||
- `환산근거` = 표 기준배 ↔ 결과단위 배(「10㎡당 → 1㎡당 ÷10」) · 재료 품목 단위 ↔ 줄 단위
|
||||
(712-2 `master_unit.note`) · 없으면 「」.
|
||||
줄 차례 맞춤 = `master_text.lines` 와 같은 차례(호표 줄마다 `_shares` 비목 · 뒤 덧줄).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
|
||||
import master_basis as mbs
|
||||
import master_bundle as mb
|
||||
import master_formula as mf
|
||||
import master_text as mtx
|
||||
import master_unit as mu
|
||||
|
||||
KEYS = ("쓰는입력", "묶음", "장비", "기준단위", "환산근거")
|
||||
_HOLE = re.compile(r"\{([^{}]+)\}")
|
||||
_ERRORS = (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError)
|
||||
|
||||
|
||||
def _names(expr) -> tuple[set[str], set[str]]:
|
||||
"""식 하나의 (이름 · 찾는 표 키) — `{입력}` 구멍 이름도 셈."""
|
||||
if isinstance(expr, dict): # 재료 고르기 조건 묶음 — 값 글의 구멍만
|
||||
return {h for v in expr.values() for h in _HOLE.findall(str(v))}, set()
|
||||
if expr in (None, ""):
|
||||
return set(), set()
|
||||
try:
|
||||
tree = mf.parse(expr)
|
||||
except mf.FormulaError:
|
||||
return set(_HOLE.findall(str(expr))), set()
|
||||
names, tables = set(), set()
|
||||
for node in mf.walk(tree):
|
||||
if node[0] == "name":
|
||||
names.add(node[1])
|
||||
elif node[0] in ("find", "logic", "ref"):
|
||||
names |= set(_HOLE.findall(node[1]))
|
||||
if node[0] == "find":
|
||||
tables.add(node[1])
|
||||
return names, tables
|
||||
|
||||
|
||||
def _closure(row: dict, exprs: list) -> tuple[list[str], list[str]]:
|
||||
"""식들이 쓰는 (입력 이름 · 표 키) — 중간값은 그 식으로 거슬러 감 · 입력 차례대로."""
|
||||
steps = {s["이름"]: s["식"] for s in row.get("중간", [])}
|
||||
inputs = [s["이름"] for s in row.get("입력", [])]
|
||||
seen, tables, todo = set(), [], list(exprs)
|
||||
while todo:
|
||||
names, found = _names(todo.pop())
|
||||
tables += [t for t in sorted(found) if t not in tables]
|
||||
for name in names - seen:
|
||||
seen.add(name)
|
||||
if name in steps:
|
||||
todo.append(steps[name])
|
||||
return [n for n in inputs if n in seen], tables
|
||||
|
||||
|
||||
def _times(x: Decimal) -> str:
|
||||
return format(x.normalize(), "f")
|
||||
|
||||
|
||||
def _per_text(per: list[tuple[Decimal, str]]) -> str:
|
||||
return " · ".join(dict.fromkeys(f"{_times(n) if n != 1 else ''}{u}" for n, u in per))
|
||||
|
||||
|
||||
def _basis(master, tables: list[str], env: dict) -> dict | None:
|
||||
for ref in tables:
|
||||
try:
|
||||
got = mbs.basis(master.get(mf.fill(ref, env) if "{" in ref else ref))
|
||||
except (*_ERRORS, mf.EmptyInput):
|
||||
continue
|
||||
if got["갈래"] == "단위":
|
||||
return got
|
||||
return None
|
||||
|
||||
|
||||
def _note(basis: dict | None, per: list, line: dict) -> str:
|
||||
notes = []
|
||||
if basis and len(per) == 1:
|
||||
times, one = per[0]
|
||||
if mu.plain(basis["기준단위"]) == mu.plain(mbs.unit(one)) and basis["기준배"] != times:
|
||||
ratio = basis["기준배"] / times
|
||||
how = f"÷{_times(ratio)}" if ratio > 1 else f"×{_times(1 / ratio)}"
|
||||
notes.append(
|
||||
f"{_times(basis['기준배'])}{basis['기준단위']}당 → {_times(times)}{one}당 {how}"
|
||||
)
|
||||
goods = line.get("품목") or {}
|
||||
if goods.get("단위") and line.get("단위"):
|
||||
notes.append(mu.note(goods["단위"], line["단위"]))
|
||||
return " · ".join(n for n in notes if n)
|
||||
|
||||
|
||||
def _fields(master, row, exprs, env, per, line: dict, bundle: tuple[str, bool] | None) -> dict:
|
||||
uses, tables = _closure(row, exprs)
|
||||
basis = _basis(master, tables, env)
|
||||
out = {
|
||||
"쓰는입력": uses,
|
||||
"기준단위": f"{_times(basis['기준배']) if basis['기준배'] != 1 else ''}{basis['기준단위']}"
|
||||
if basis
|
||||
else None,
|
||||
"환산근거": _note(basis, per, line),
|
||||
}
|
||||
if bundle:
|
||||
out["묶음"] = bundle[0]
|
||||
if bundle[1]:
|
||||
out["장비"] = True
|
||||
return out
|
||||
|
||||
|
||||
def annotate(master, key: str, given: dict, answer: dict) -> dict:
|
||||
"""`master_text.lines` 답에 줄마다 `KEYS` 덧칸 · 머리 `기준단위` — 못 셈이면 답 그대로."""
|
||||
if not answer.get("ok") or "groups" not in answer:
|
||||
return answer
|
||||
row = master.logic(key)
|
||||
per = mbs.result_per(row.get("결과단위"))
|
||||
answer["기준단위"] = _per_text(per) or None
|
||||
try:
|
||||
env = mtx._env_of(master, row, given)
|
||||
except _ERRORS:
|
||||
env = {}
|
||||
if "결과" in row:
|
||||
for line in answer["groups"][0]["줄"]:
|
||||
line.update(_fields(master, row, [row["결과"]["식"]], env, per, {}, None))
|
||||
return answer
|
||||
try:
|
||||
result = mf.run(master, key, given)
|
||||
except _ERRORS:
|
||||
result = None
|
||||
slots = {g["비목"]: iter(g["줄"]) for g in answer["groups"]}
|
||||
items = row.get("호표") or []
|
||||
for n, item in enumerate(items):
|
||||
line = result["줄"][n] if result else {}
|
||||
if result:
|
||||
parts = [p for p, _ in mtx._shares(line)]
|
||||
elif item.get("종류") == "로직" and "비목" not in item:
|
||||
parts = list(mf.COST_ITEMS)
|
||||
else:
|
||||
parts = [item.get("비목")]
|
||||
exprs = [item.get("수량"), item.get("요소")]
|
||||
got = _fields(master, row, exprs, env, per, line, mb.bundle(master, item, env))
|
||||
for part in parts:
|
||||
target = next(slots.get(part, iter(())), None)
|
||||
if target is not None:
|
||||
target.update(got)
|
||||
for extra in row.get("덧줄") or []:
|
||||
target = next(slots.get(extra["비목"], iter(())), None)
|
||||
if target is not None:
|
||||
target.update(
|
||||
_fields(master, row, [extra["식"]], env, per, {}, mb.bundle(master, extra))
|
||||
)
|
||||
return answer
|
||||
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""단위 한 벌 — 별칭 · 개수 묶음 · 결과단위 배 읽기 · 같은 성격 환산 · 환산 근거 글(PLAN 712-2).
|
||||
|
||||
쓰는 곳: 재료 단가 → 줄 단위(`master_pick`) · 자재 맞대기 별칭(`master_material.plain_unit`) ·
|
||||
조합 알약 배(`M01_MasterData_Store_Combo`) · 엑셀 맞춤 배(`M02_Hopyo_Ours` · `_Match`) ·
|
||||
표 기준배(`master_basis`) · 계산서 줄 환산 근거(714-1). 서버 단독(③) — TS 는 서버가 준 글만 씀.
|
||||
- 개수 묶음(개 · EA · 본 · 매) = 1 : 1 같은 성격 · 조 · 세트는 묶음 밖(한 조 = 여럿).
|
||||
- 부피 ↔ 무게 = 단위중량 표 CC000337 에서 고른 줄(`unit_weight`)을 받을 때만 · 기본 줄 없음
|
||||
(사용자 확인 거리) · 밀도(kg ↔ ℓ) · ㎾h 등은 환산 없이 경고 글만.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
from decimal import Decimal
|
||||
|
||||
# 맞대기 꼴(`plain`) 별칭 — NFKC · 소문자 · 빈칸 없앤 뒤
|
||||
ALIAS = {"ea": "개", "ton": "톤", "t": "톤", "l": "ℓ", "리터": "ℓ", "set": "조", "세트": "조"}
|
||||
# 보이는 꼴(`shown`) 별칭 — 표 기준 · 결과단위 분모(원문 기호 ㎡ · ㎥ 그대로)
|
||||
SHOWN = {"ea": "개", "톤": "ton", "t": "ton", "㎞": "km", "hr": "시간", "min": "분", "㎏": "kg"}
|
||||
# 같은 성격 — {성격: {맞대기 단위: 기준 단위 몇 배}}
|
||||
UNITS = {
|
||||
"무게": {"g": 1, "kg": 1000, "톤": 1000000},
|
||||
"길이": {"mm": 1, "cm": 10, "m": 1000, "km": 1000000},
|
||||
"넓이": {"cm2": 1, "m2": 10000},
|
||||
"부피": {"cm3": 1, "ℓ": 1000, "m3": 1000000},
|
||||
"개수": {"개": 1, "본": 1, "매": 1},
|
||||
}
|
||||
_SCALE = {u: (kind, n) for kind, group in UNITS.items() for u, n in group.items()}
|
||||
WEIGHT_TABLE = "CC000337" # 건설품셈 공통 1-2-4 재료의 단위중량(㎏/㎥)
|
||||
# 결과단위 분모 — 「원/10㎡」 → (10, ㎡) · 「원/1,000본」 → (1000, 본)
|
||||
_PER = re.compile(r"원\s*/\s*(\d[\d,]*\.?\d*|\.\d+)?\s*([^\s·(\d,.][^\s·(]*)")
|
||||
|
||||
|
||||
def plain(unit) -> str:
|
||||
"""맞대기 꼴 — 「㎥」 = 「m3」 · 「EA」 = 「개」 · 「Ton」 = 「톤」 · 「리터」 = 「ℓ」."""
|
||||
s = re.sub(r"\s+", "", unicodedata.normalize("NFKC", str(unit or "")).lower())
|
||||
s = s.replace("ℓ", "l")
|
||||
return ALIAS.get(s, s)
|
||||
|
||||
|
||||
def shown(unit) -> str:
|
||||
return SHOWN.get(unit, unit)
|
||||
|
||||
|
||||
def per(result_unit) -> list[tuple[Decimal, str]]:
|
||||
"""결과단위 분모마다 (배, 단위 원문 글) — 배 없으면 1 · 분모가 여럿이면 여럿."""
|
||||
return [
|
||||
(Decimal(n.replace(",", "")) if n else Decimal(1), u)
|
||||
for n, u in _PER.findall(str(result_unit or ""))
|
||||
]
|
||||
|
||||
|
||||
def kind_of(unit) -> str | None:
|
||||
got = _SCALE.get(plain(unit))
|
||||
return got[0] if got else None
|
||||
|
||||
|
||||
def same_kind(a, b) -> bool:
|
||||
"""단위가 같거나 같은 성격(무게 · 길이 · 넓이 · 부피 · 개수)."""
|
||||
return plain(a) == plain(b) or (kind_of(a) is not None and kind_of(a) == kind_of(b))
|
||||
|
||||
|
||||
def _weighs(a, b) -> bool:
|
||||
return {kind_of(a), kind_of(b)} == {"무게", "부피"}
|
||||
|
||||
|
||||
def factor(have, want, density=None) -> Decimal | None:
|
||||
"""`have` 1 단위가 `want` 몇 단위인가 — 단가는 ÷, 수량은 ×.
|
||||
부피 ↔ 무게는 단위중량(㎏/㎥) `density` 가 있을 때만 · 못 하면 None."""
|
||||
a, b = plain(have), plain(want)
|
||||
if a == b:
|
||||
return Decimal(1)
|
||||
if same_kind(a, b):
|
||||
return Decimal(_SCALE[a][1]) / Decimal(_SCALE[b][1])
|
||||
if density and _weighs(a, b):
|
||||
grams_per_cm3 = Decimal(str(density)) / Decimal(1000)
|
||||
base = Decimal(_SCALE[a][1]) * (
|
||||
grams_per_cm3 if kind_of(a) == "부피" else 1 / grams_per_cm3
|
||||
)
|
||||
return base / Decimal(_SCALE[b][1])
|
||||
return None
|
||||
|
||||
|
||||
def convert(price, have, want, density=None):
|
||||
"""`have` 단위 단가 → `want` 단위 단가 — 못 바꾸면 그대로(경고는 부르는 쪽)."""
|
||||
if price is None or not want or not have or plain(have) == plain(want):
|
||||
return price
|
||||
if same_kind(have, want): # × 뒤 ÷ — 옛 셈과 같은 Decimal 꼴(자릿수)
|
||||
return price * Decimal(_SCALE[plain(want)][1]) / Decimal(_SCALE[plain(have)][1])
|
||||
got = factor(want, have, density) # want 1 단위 = have 몇 단위
|
||||
return price if got is None or got == 1 else price * got
|
||||
|
||||
|
||||
def note(have, want, density=None) -> str:
|
||||
"""환산 근거 글 — 「kg → 톤 ×1000」 · 「본 = 개(개수 1:1)」 · 못 바꾸면 까닭."""
|
||||
if not have or not want or plain(have) == plain(want):
|
||||
return ""
|
||||
got = factor(want, have, density)
|
||||
if got is not None:
|
||||
if kind_of(have) == "개수" and got == 1:
|
||||
return f"{have} = {want}(개수 1:1)"
|
||||
how = f" · 단위중량 {density}㎏/㎥" if _weighs(have, want) else ""
|
||||
return f"{have} → {want} ×{_num(got)}{how}"
|
||||
if _weighs(have, want):
|
||||
return f"{have} ↔ {want} — 단위중량({WEIGHT_TABLE}) 골라야 환산 · 지금 그대로"
|
||||
return f"{have} ↔ {want} — 성격 다름 · 환산 없음(경고)"
|
||||
|
||||
|
||||
def _num(x: Decimal) -> str:
|
||||
return format(x.normalize(), "f") if x == x.to_integral_value() else format(x, "f")[:10]
|
||||
|
||||
|
||||
def unit_weight(master, kind: str, shape: str, end: str = "위") -> Decimal | None:
|
||||
"""단위중량 표에서 고른 줄의 ㎏/㎥ — 기본 줄 없음(고르지 않으면 None)."""
|
||||
for row in master.get(WEIGHT_TABLE).get("줄") or []:
|
||||
if row.get("종별") == kind and row.get("형상") == shape:
|
||||
got = row.get(end)
|
||||
return None if got is None else Decimal(str(got))
|
||||
return None
|
||||
@@ -0,0 +1,64 @@
|
||||
"""712-1 지킴 — 인력 · 재료 · 경비 묶음 셈 때 파생 · 기계경비 판별 한 곳(`master_bundle`)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "resources/master_data/scripts"))
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import check_master as cm # noqa: E402
|
||||
import master_auto as mau # noqa: E402
|
||||
import master_bundle as mb # noqa: E402
|
||||
import master_formula as mf # noqa: E402
|
||||
|
||||
FILES = cm.load()
|
||||
MASTER = mf.Master(FILES)
|
||||
LOGICS = {r["키"]: r for d in FILES.values() if d.get("그룹") == "로직" for r in d.get("줄") or []}
|
||||
|
||||
|
||||
def test_every_line_bundled():
|
||||
"""호표 · 덧줄 전수 — 하위(못 가름) ≤ 31 · 나머지는 인력 · 재료 · 경비 · 기계경비 줄 = 경비."""
|
||||
count = dict.fromkeys(mb.BUNDLES, 0)
|
||||
machine = 0
|
||||
for row in LOGICS.values():
|
||||
for item in [*row.get("호표", []), *row.get("덧줄", [])]:
|
||||
name, is_mach = mb.bundle(MASTER, item)
|
||||
count[name] += 1
|
||||
machine += is_mach
|
||||
if is_mach:
|
||||
assert name == "경비"
|
||||
assert sum(count.values()) > 5_000
|
||||
assert count["하위"] <= 31, count
|
||||
assert machine >= 1_000, machine
|
||||
|
||||
|
||||
def test_machine_rule_one_place():
|
||||
"""기계경비 = 결과단위 원/시간 돈 로직 — 작업량(㎥/시간) · 이름 「시간당」 은 아님."""
|
||||
machines = {k for k, r in LOGICS.items() if mb.is_machine(r)}
|
||||
assert {"GC000267", "GC000268", "GF000145"} <= machines
|
||||
assert not machines & {"GF000102", "GC000422", "GC000260"} # 작업량 · ㎡/시간 · 원/㎡
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from M02_MasterTemplete import M02_Hopyo_Ours as ours
|
||||
|
||||
assert all(ours.is_mach_logic(r) == mb.is_machine(r) for r in LOGICS.values())
|
||||
|
||||
|
||||
def test_run_lines_tagged():
|
||||
"""엔진 `run` 줄마다 `묶음` · 기계경비 줄은 `장비` + 비목 노 · 재 · 경 세 몫."""
|
||||
seen = set()
|
||||
for key in ("GF000169", "GF000103"):
|
||||
row = LOGICS[key]
|
||||
given = {k: v for k, v in mau.values(MASTER, row).items() if v is not None}
|
||||
got = mf.run(MASTER, key, given)
|
||||
for line in got["줄"]:
|
||||
assert line["묶음"] in mb.BUNDLES
|
||||
seen.add(line["묶음"])
|
||||
if line.get("장비"):
|
||||
assert set(line["비목"]) == set(mf.COST_ITEMS)
|
||||
total = sum((line["비목"].get(k) or Decimal(0)) for line in got["줄"] for k in line["비목"])
|
||||
assert abs(total - got["계"]) < 1
|
||||
assert {"인력", "재료", "하위"} <= seen
|
||||
@@ -0,0 +1,59 @@
|
||||
"""712-2 지킴 — 단위 한 벌(`master_unit`) · 다섯 자리가 같은 모듈을 씀 · 알약 배 전수."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "resources/master_data/scripts"))
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import check_master as cm # noqa: E402
|
||||
import master_basis as mbs # noqa: E402
|
||||
import master_formula as mf # noqa: E402
|
||||
import master_material as mm # noqa: E402
|
||||
import master_pick as mp # noqa: E402
|
||||
import master_unit as mu # noqa: E402
|
||||
|
||||
|
||||
def test_alias_and_count_group():
|
||||
assert mu.plain("EA") == mu.plain("ea") == "개"
|
||||
assert mu.plain("Ton") == mu.plain("t") == mu.plain("톤") == "톤"
|
||||
assert mu.plain("㎥") == "m3" and mu.plain("리터") == mu.plain("ℓ") == "ℓ"
|
||||
assert mu.same_kind("본", "개") and mu.same_kind("매", "EA")
|
||||
assert not mu.same_kind("조", "개") # 한 조 = 여럿 — 묶음 밖
|
||||
assert mu.note("본", "개") == "본 = 개(개수 1:1)"
|
||||
assert mm.plain_unit("EA") == mu.plain("EA")
|
||||
assert mp.convert is mu.convert and mp.UNITS is mu.UNITS
|
||||
|
||||
|
||||
def test_same_kind_convert():
|
||||
assert mu.convert(Decimal(1000), "톤", "kg") == Decimal(1) # 톤 당 1000 원 = kg 당 1 원
|
||||
assert mu.convert(Decimal(5), "kg", "톤") == Decimal(5000)
|
||||
assert mu.convert(Decimal(7), "m", "km") == Decimal(7000)
|
||||
assert mu.factor("kg", "ton") == Decimal("0.001") # B08 「/1000」(kg → ton) 손 식과 같은 값
|
||||
assert mu.note("kg", "톤") == "kg → 톤 ×1000"
|
||||
assert mu.convert(Decimal(3), "kg", "ℓ") == Decimal(3) # 밀도 = 경고만 · 그대로
|
||||
assert "성격 다름" in mu.note("ℓ", "㎾h")
|
||||
|
||||
|
||||
def test_volume_weight_only_when_picked():
|
||||
"""부피 ↔ 무게 = 단위중량 표 고른 줄이 있을 때만 · 기본 줄 없음."""
|
||||
master = mf.Master(cm.load())
|
||||
assert mu.factor("㎥", "톤") is None
|
||||
assert "단위중량(CC000337)" in mu.note("㎥", "톤")
|
||||
rho = mu.unit_weight(master, "모래", "건조")
|
||||
assert rho is not None and rho > 1000
|
||||
assert mu.factor("㎥", "톤", rho) == rho / 1000 # 1 ㎥ = ρ/1000 톤
|
||||
assert mu.convert(Decimal(100), "㎥", "톤", rho) == Decimal(100) * 1000 / rho
|
||||
assert mu.unit_weight(master, "없는 종별", "건조") is None
|
||||
|
||||
|
||||
def test_result_per_one_parser():
|
||||
assert mu.per("원/10㎡") == [(Decimal(10), "㎡")]
|
||||
assert mu.per("원/1,000본") == [(Decimal(1000), "본")]
|
||||
assert mu.per("원/㎡ · 원/㎥(단위 따름)") == [(Decimal(1), "㎡"), (Decimal(1), "㎥")]
|
||||
assert mu.per("㎥/시간") == []
|
||||
assert mbs.result_per("원/ton") == [(Decimal(1), "ton")]
|
||||
@@ -0,0 +1,62 @@
|
||||
"""714-1 지킴 — 계산서 줄 덧칸(쓰는 입력 · 묶음 · 기준단위 · 환산 근거) · M01 `/text` 와
|
||||
M02 `/logic-lib/sheet` 가 같은 줄 꼴(`master_sheet_line`)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as store # noqa: E402
|
||||
from M01_MasterData import M01_MasterData_Store_Sheet as sheet # noqa: E402
|
||||
from M02_MasterTemplete import M02_Logic_Standard as standard # noqa: E402
|
||||
|
||||
msl = sheet.msl
|
||||
|
||||
|
||||
def _text(key: str) -> dict:
|
||||
auto = {k: v for k, v in store.auto(key)["값"].items() if v is not None}
|
||||
return sheet.text(key, auto)
|
||||
|
||||
|
||||
def _lines(got: dict, groups: str = "groups") -> list[dict]:
|
||||
return [line for g in got[groups] for line in g["줄"]]
|
||||
|
||||
|
||||
def test_line_keys_and_bundle():
|
||||
got = _text("GF000103") # 구조물터파기 — 인력 + 굴착기 기계경비 줄
|
||||
assert got["ok"] and got["기준단위"] == "㎥"
|
||||
lines = _lines(got)
|
||||
assert all({"쓰는입력", "묶음", "기준단위", "환산근거"} <= set(line) for line in lines)
|
||||
machine = [line for line in lines if line.get("장비")]
|
||||
assert machine and all(line["묶음"] == "경비" for line in machine)
|
||||
assert {"지역", "보정작업"} <= set(machine[0]["쓰는입력"])
|
||||
assert any(line["묶음"] == "인력" for line in lines)
|
||||
|
||||
|
||||
def test_uses_follow_middle_values():
|
||||
"""중간값을 거슬러 입력까지 — 돌쌓기 석공 품은 높이 · 쌓기 · 돌 입력을 씀."""
|
||||
lines = _lines(_text("GF000219"))
|
||||
assert {"높이", "쌓기", "돌"} <= set(lines[0]["쓰는입력"])
|
||||
|
||||
|
||||
def test_basis_note_tenfold():
|
||||
"""표 10㎡당 → 결과 원/㎡ 「÷10」 · 결과도 원/10㎡ 면 근거 없음."""
|
||||
one = _lines(_text("GC000037"))
|
||||
assert one[0]["기준단위"] == "10㎡" and one[0]["환산근거"] == "10㎡당 → 1㎡당 ÷10"
|
||||
ten = _text("GF000124")
|
||||
assert ten["기준단위"] == "10㎡" and all(not x["환산근거"] for x in _lines(ten))
|
||||
|
||||
|
||||
def test_two_routes_same_shape():
|
||||
"""M01 `/text` 와 M02 계산서 — 줄 차례 · 덧칸 값이 같음(금액 끝수만 다를 수 있음)."""
|
||||
for key in ("GF000169", "GF000103", "GF000219"):
|
||||
m01 = _lines(_text(key))
|
||||
m02 = _lines(standard.sheet(key, {}), "묶음")
|
||||
assert len(m01) == len(m02)
|
||||
for a, b in zip(m01, m02):
|
||||
assert set(b) == {"이름", "글", "금액", *msl.KEYS}
|
||||
assert a["이름"] == b["이름"]
|
||||
assert [a.get(k) for k in msl.KEYS] == [b.get(k) for k in msl.KEYS]
|
||||
Reference in New Issue
Block a user