feat(793-2): 가져온 식 793-2a 내보내기 꼴 받음 — 자원 덧칸 · 글줄 이름 → cid · 자리들 합 · 단계 · 중간 검산 · 빈 산근 받음
실무 여덟 파일 455 블록 검산 같음 · 상수 자리 찾기 「(11)」 겹침 고침(M01 식엔 그 꼴 없음 = 옛 상수덮개 차례 그대로) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,7 +57,7 @@ def _parser() -> type:
|
||||
self.ws()
|
||||
at = self.i
|
||||
node = super().primary()
|
||||
if node[0] == "num":
|
||||
if node[0] == "num" and self.s[at : at + 1] != "(": # 「(11)」 = 안쪽 수만
|
||||
self.spots.append(("num", at, self.i))
|
||||
return node
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ COSTS = ("노무비", "재료비", "경비")
|
||||
KINDS = ("인력", "재료", "기계", "로직")
|
||||
ROW = ("이름", "결과단위", "입력", "중간", "호표", "덧줄", "단계", "끝전", "조정", "끝수")
|
||||
LINE = ("종류", "이름", "규격", "단위", "수량", "비목", "단가", "값묶음", "이음", "번지", "합밖")
|
||||
LINE += ("자리", "출처", "비고")
|
||||
LINE += ("자리", "출처", "비고", "자원") # 자원 = 경쟁사 코드 · 엑셀 단가 원본(793-2a · 셈 안 씀)
|
||||
EXTRA = ("이름", "식", "비목", "번지", "합밖", "자리", "출처", "비고")
|
||||
META = ("원본", "검산", "근거후보", "바꿈") # 검산 = 저장 때 서버가 다시 적음(`stamp`)
|
||||
MANY = 300 # 호표 · 덧줄 · 중간 줄 수
|
||||
@@ -172,8 +172,8 @@ def clean_row(raw: Any, whole: Any = None) -> dict[str, Any]:
|
||||
if not re.match(r"^원/\S+$", str(raw.get("결과단위") or "").strip()):
|
||||
raise sd.DocError("가져온 로직 · 결과단위는 「원/단위」")
|
||||
items, extras, mids = raw.get("호표") or [], raw.get("덧줄") or [], raw.get("중간") or []
|
||||
if not all(isinstance(x, list) for x in (items, extras, mids)) or not items:
|
||||
raise sd.DocError("가져온 로직 · 호표(한 줄 이상) · 덧줄 · 중간은 목록")
|
||||
if not all(isinstance(x, list) for x in (items, extras, mids)):
|
||||
raise sd.DocError("가져온 로직 · 호표 · 덧줄 · 중간은 목록") # 빈 산근(0 원) = 받음
|
||||
if max(len(items), len(extras), len(mids)) > MANY:
|
||||
raise sd.DocError(f"가져온 로직 · 줄은 {MANY} 까지")
|
||||
out["호표"] = [_line(x, f"호표 {n}째", whole) for n, x in enumerate(items, 1)]
|
||||
@@ -223,9 +223,28 @@ def clean(raw: Any, out: dict) -> dict:
|
||||
if basis not in BASIS:
|
||||
raise sd.DocError("단가기준은 엑셀 · 우리")
|
||||
got |= {"로직": clean_row(meta["로직"]), "단가기준": basis}
|
||||
if got.get("원본"):
|
||||
got["원본"] = _named(got["원본"], got["로직"])
|
||||
return out | ({KEY: got} if got else {})
|
||||
|
||||
|
||||
def _named(src: dict, row: dict) -> dict:
|
||||
"""원본 글줄 `자리` · `자리들` = 셈 줄 이름(793-2a) → 줄 정체 cid(이미 cid · `mid:` · `단계:` =
|
||||
그대로 · 블록 안 줄 이름은 겹치지 않음)."""
|
||||
ids = sd.line_ids(row_of({KEY: {"로직": row}}) or {})
|
||||
by: dict[str, str] = {}
|
||||
for one, cid in zip([*row["호표"], *(row.get("덧줄") or [])], ids):
|
||||
by.setdefault(str(one.get("이름")), cid)
|
||||
lines = []
|
||||
for one in src.get("글줄") or []:
|
||||
if isinstance(one, dict):
|
||||
one = {**one, **({"자리": by.get(one["자리"], one["자리"])} if one.get("자리") else {})}
|
||||
if isinstance(one.get("자리들"), list):
|
||||
one["자리들"] = [by.get(x, x) for x in one["자리들"]]
|
||||
lines.append(one)
|
||||
return {**src, "글줄": lines} if "글줄" in src else src
|
||||
|
||||
|
||||
def same_part(doc: Any) -> dict | None:
|
||||
"""같은 내용 지문 몫(`M02_Library_Same`) — 로직 · 단가기준(원본 · 검산 · 근거후보 뺌)."""
|
||||
if not is_imported(doc):
|
||||
@@ -268,7 +287,7 @@ def _same(mine: Any, excel: Any) -> bool:
|
||||
"""셈 ≈ 엑셀 — 엑셀 값 끝자리 한 칸 안(엑셀 보이는 값 = 끊은 값)."""
|
||||
if mine is None or excel is None:
|
||||
return False
|
||||
want = Decimal(str(excel))
|
||||
want = round(Decimal(str(excel)), 6) # 엑셀 float 꼬리(8109.000000000001) 뗌
|
||||
step = Decimal(1).scaleb(min(want.normalize().as_tuple().exponent, 0))
|
||||
return abs(Decimal(str(mine)) - want) < step
|
||||
|
||||
@@ -287,25 +306,45 @@ def check(doc: dict, whole: Any = None) -> dict[str, Any]:
|
||||
except sd.DocError as e:
|
||||
return {"같음": False, "까닭": str(e), "셈": None, "엑셀": excel, "줄": []}
|
||||
mine = _money(got)
|
||||
lines = _lines(src, {x.get("cid"): x.get("금액") for x in got.get("줄") or []})
|
||||
now = {x.get("cid"): x.get("금액") for x in got.get("줄") or []} | _inner(bare, whole)
|
||||
lines = [x for x in (_compare(one, now) for one in src.get("글줄") or []) if x]
|
||||
ok = bool(mine) and all(_same(mine.get(k), excel.get(k)) for k in excel) and excel != {}
|
||||
why = None if ok else (got.get("까닭") or ("엑셀 금액 없음" if not excel else "금액 다름"))
|
||||
ok = ok and all(x["같음"] for x in lines)
|
||||
return _json({"같음": ok, "까닭": why, "셈": mine, "엑셀": excel, "줄": lines})
|
||||
|
||||
|
||||
def _lines(src: dict, now: dict) -> list[dict]:
|
||||
out = []
|
||||
for one in src.get("글줄") or []:
|
||||
spot, want = one.get("자리"), one.get("값")
|
||||
if not spot or want is None:
|
||||
continue
|
||||
got = now.get(spot)
|
||||
same = _same(got, want)
|
||||
why = None if same else ("짝 줄 없음" if spot not in now else f"셈 {got} · 엑셀 {want}")
|
||||
out.append({"자리": spot, "행": one.get("행"), "셈": got, "엑셀": want, "같음": same})
|
||||
out[-1] |= {"까닭": why} if why else {}
|
||||
return out
|
||||
def _inner(doc: dict, whole: Any) -> dict:
|
||||
"""엔진 날 셈의 중간 `mid:이름` · 단계 `단계:이름` 계(검산 맞댈 값) · 못 셈 = 빈."""
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
m01, key = calc.labor.m01, str(doc.get("키") or "SK-000000")
|
||||
over = calc._Over(whole, key, {**row_of(doc), "키": key})
|
||||
try:
|
||||
auto = {k: v for k, v in m01.mau.values(over, over.logic(key)).items() if v is not None}
|
||||
given = {**auto, **m01._dec(doc.get("입력값") or {})}
|
||||
got = calc.labor.mcut.engine(over, key, m01.cm.mf.run(over, key, given))
|
||||
except Exception: # noqa: BLE001 — 엔진 FormulaError · 셈 틀림 = 맞댈 값 없음
|
||||
return {}
|
||||
out = {f"mid:{k}": v for k, v in (got.get("중간") or {}).items()}
|
||||
return out | {f"단계:{x.get('이름')}": x.get("계") for x in got.get("단계") or []}
|
||||
|
||||
|
||||
def _compare(one: Any, now: dict, loose: bool = False) -> dict | None:
|
||||
"""글줄 하나 맞댐 — 자리(들) 셈 합 ≈ 엑셀 `값` · 맞댈 것 없음 = None · `loose` = 셈에 없는
|
||||
자리면 None(화면 보기 · 단계 값 없음)."""
|
||||
if not isinstance(one, dict) or one.get("값") is None:
|
||||
return None
|
||||
spots = one.get("자리들") or ([one["자리"]] if one.get("자리") else [])
|
||||
if not spots or (loose and any(s not in now for s in spots)):
|
||||
return None
|
||||
want = one["값"]
|
||||
vals = [now.get(s) for s in spots]
|
||||
got = None if None in vals else sum(Decimal(str(v)) for v in vals)
|
||||
same = _same(got, want)
|
||||
why = None if same else ("짝 줄 없음" if got is None else f"셈 {got} · 엑셀 {want}")
|
||||
out = {"자리": spots[0], "행": one.get("행"), "셈": got, "엑셀": want, "같음": same}
|
||||
return out | ({"자리들": spots} if len(spots) > 1 else {}) | ({"까닭": why} if why else {})
|
||||
|
||||
|
||||
def more(out: dict, clean_: dict) -> dict:
|
||||
@@ -316,14 +355,13 @@ def more(out: dict, clean_: dict) -> dict:
|
||||
src = meta.get("원본") or {}
|
||||
spots = (out.get("시트") or {}).get("자리") or {}
|
||||
now = {k: v.get("지금") for k, v in spots.items()}
|
||||
lines = {x["자리"]: x for x in _lines(src, now)}
|
||||
texts = [
|
||||
{k: one.get(k) for k in ("행", "글", "자리")}
|
||||
| ({"엑셀": one["값"]} if one.get("값") is not None else {})
|
||||
| ({"지금": lines[one["자리"]]["셈"], "같음": lines[one["자리"]]["같음"]}
|
||||
if one.get("자리") in lines else {})
|
||||
for one in src.get("글줄") or []
|
||||
] # fmt: skip
|
||||
texts = []
|
||||
for one in src.get("글줄") or []:
|
||||
hit = _compare(one, now, loose=True) or {}
|
||||
text = {k: one.get(k) for k in ("행", "글", "자리")}
|
||||
text |= {"자리들": one["자리들"]} if one.get("자리들") else {}
|
||||
text |= {"엑셀": one["값"]} if one.get("값") is not None else {}
|
||||
texts.append(text | ({"지금": hit["셈"], "같음": hit["같음"]} if hit else {}))
|
||||
head = {k: src.get(k) for k in ("파일", "시트", "행", "갈래", "엑셀금액")}
|
||||
view = {"단가기준": meta.get("단가기준"), **head, "글줄": texts, "이음없음": unlinked(clean_)}
|
||||
view |= {k: meta[k] for k in ("검산", "근거후보", "바꿈") if meta.get(k)}
|
||||
@@ -388,7 +426,11 @@ def _rekey(doc: dict, move: dict) -> dict:
|
||||
out["더한줄"] = [_rate_rekey(x, move) for x in doc.get("더한줄") or []]
|
||||
src = doc[KEY].get("원본") or {}
|
||||
if src.get("글줄"):
|
||||
lines = [{**t, "자리": move.get(t.get("자리"), t.get("자리"))} for t in src["글줄"]]
|
||||
lines = [
|
||||
{**t, "자리": move.get(t.get("자리"), t.get("자리"))}
|
||||
| ({"자리들": [move.get(x, x) for x in t["자리들"]]} if t.get("자리들") else {})
|
||||
for t in src["글줄"]
|
||||
]
|
||||
out[KEY] = {**out[KEY], "원본": {**src, "글줄": lines}}
|
||||
return out
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ def test_우리_기초로_바꾸기():
|
||||
(lambda r: r["호표"][0].pop("단가"), "둘 다 없음"),
|
||||
(lambda r: r["호표"][0].update(이음="MT999999"), "없는 이음"),
|
||||
(lambda r: r["덧줄"][0].update(식="번지.A01.재료비 * "), "덧줄"),
|
||||
(lambda r: r.update(호표=[]), "호표"),
|
||||
(lambda r: r.update(호표="줄"), "목록"),
|
||||
],
|
||||
)
|
||||
def test_틀린_가져온_로직(change, why):
|
||||
@@ -345,3 +345,47 @@ def test_원본_보존_사본():
|
||||
assert doc == keep # 받은 문서 안 고침(화면 캐시 = 답 문서로 바꿈)
|
||||
assert refs.kind_of("SK-000001") == "sangeun" and MINE["층"] == "personal"
|
||||
assert flat.flat_doc("sangeun", doc) == doc # 참조 없음 = 그대로
|
||||
|
||||
|
||||
# ── 실무 블록(793-2a 내보내기 → 가져온 식 SK) ─────────────────────────────
|
||||
REAL = [("봉화", "D00102"), ("동부청영월", "D00003"), ("울진소광", "D01489"), ("북부청임도", 18)]
|
||||
RULE = {"봉화": "비목버림", "동부청영월": "합버림_큰나머지", "울진소광": "비목버림"}
|
||||
|
||||
|
||||
def _emitted(tag, code):
|
||||
import helper_m02_hopyo_golden as golden
|
||||
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
from M02_MasterTemplete import M02_San_Emit as em
|
||||
from M02_MasterTemplete import M02_San_Est as est
|
||||
from M02_MasterTemplete import M02_San_Stmate as stm
|
||||
|
||||
if not golden.FILES[tag].exists():
|
||||
pytest.skip(f"실무 엑셀 없음 {tag}")
|
||||
data = book.load(golden.FILES[tag])
|
||||
if tag in RULE:
|
||||
blk = next(b for b in data["블록"] if b["코드"] == code)
|
||||
return em.stmate(blk, stm.parse_block(blk["몸"]), data["단가"], RULE[tag], True, tag)
|
||||
blk = next(b for b in data["블록"] if b["번"] == code)
|
||||
want = data["총괄"][blk["번"]]
|
||||
return em.est(blk, est.parse_block(blk["글"], data["단가"]), want, True, tag)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("tag", "code"), REAL)
|
||||
def test_실무_블록_가져온_식(tag, code, world):
|
||||
out = _emitted(tag, code)
|
||||
with store.at(world["personal"]):
|
||||
made = imp.create(out["로직"], out["원본"], f"{tag} {code}")
|
||||
doc = store.read("sangeun", made["이름"])["문서"]
|
||||
ans = calc.calc_doc(doc)
|
||||
check = made["검산"]
|
||||
excel = {k: round(v) for k, v in out["원본"]["엑셀금액"].items()}
|
||||
assert check["같음"] is True and check["셈"] == excel and len(check["줄"]) >= 3
|
||||
assert all(":" in x["자리"] for x in check["줄"]) # 줄 이름 → cid · mid: · 단계:
|
||||
assert ans["원래"] == out["원본"]["엑셀금액"] and round(ans["기준"]["계"]) == excel["계"]
|
||||
shown = [x for x in ans[imp.KEY]["글줄"] if "같음" in x]
|
||||
assert shown and all(x["같음"] for x in shown)
|
||||
assert _frozen_total(doc, _whole()) == excel["계"]
|
||||
ours = imp.to_price(doc, "우리")
|
||||
assert {k: round(float(v)) for k, v in ours["전"].items()} == excel
|
||||
assert len(ours["이음없음"]) < len(doc[imp.KEY]["로직"]["호표"]) # 1차 줄 판정 이음
|
||||
|
||||
Reference in New Issue
Block a user