Merge remote-tracking branches 'origin/sub_desktop_1', 'origin/sub_laptop_1' and 'origin/sub_laptop_2' into CODEX
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""로직 호표의 재료 줄을 「고르기 조건」 으로 바꿈 — 요소 「MP……」 → {구분, 상세구분[, 규격]}.
|
||||
|
||||
· 바탕 = 재료_품셈재료.json 의 「고르기」 칸 (`scripts/build_품셈재료_고르기.py` 가 채움)
|
||||
· 고르기 칸이 빈 품셈재료(연결이 이미 있는 줄 · 짝 없는 줄)는 옛 방식 그대로 둠
|
||||
· 줄 텍스트의 「"요소": "MP……"」 한 줄만 바꿈 — 나머지 글자·차례 그대로
|
||||
· 다시 돌려도 같은 결과(이미 바뀐 줄은 건드릴 것이 없음)
|
||||
|
||||
사용: python scripts/build_로직_재료_고르기.py (로직 파일 갱신)
|
||||
python scripts/build_로직_재료_고르기.py --보기 (바꿀 줄 수만 셈)
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
MASTER = Path(__file__).resolve().parents[1]
|
||||
SRC = MASTER / "재료_품셈재료.json"
|
||||
LINE = re.compile(r'^(\s*"요소": )"(MP\d{6})"(,?)\s*$')
|
||||
|
||||
|
||||
def main(show: bool) -> int:
|
||||
picks = {
|
||||
str(r["키"]): r.get("고르기") for r in json.loads(SRC.read_text(encoding="utf-8"))["줄"]
|
||||
}
|
||||
done, kept, files = 0, 0, 0
|
||||
for path in sorted(MASTER.glob("로직_*.json")):
|
||||
text = path.read_text(encoding="utf-8")
|
||||
out, hit = [], 0
|
||||
for line in text.split("\n"):
|
||||
m = LINE.match(line)
|
||||
cond = picks.get(m.group(2)) if m else None
|
||||
if cond:
|
||||
out.append(m.group(1) + json.dumps(cond, ensure_ascii=False) + m.group(3))
|
||||
hit += 1
|
||||
else:
|
||||
kept += bool(m)
|
||||
out.append(line)
|
||||
done += hit
|
||||
if hit:
|
||||
files += 1
|
||||
if not show:
|
||||
path.write_text("\n".join(out), encoding="utf-8", newline="\n")
|
||||
print(f"고르기 조건으로 바꾼 재료 줄 {done} · 옛 방식 그대로 {kept} · 파일 {files}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.exit(main("--보기" in sys.argv))
|
||||
@@ -481,12 +481,20 @@ R = [
|
||||
("식생·종자", "수목", r"촉촉소일|나무여과상자|만병초"),
|
||||
("석재", "경계석", r"빗물머금석"),
|
||||
("잡재료", "소모품", r"마킹펜|크레용|세척제|WHITEPAINT|보조통|암등|이단철개|열수축시트"),
|
||||
# 일감 3 — 뒤에 붙여 옛 결과는 그대로
|
||||
("운송·용역", "상하차", r"^(상|하)차비$"),
|
||||
("식생·종자", "초화·수생식물", r"^칡$"),
|
||||
("특수장비", "특수장비", r"AirHammer|콘쉴드|바탕면조정재|본우드"),
|
||||
]
|
||||
|
||||
RX = [(a, b, re.compile(c, re.I)) for a, b, c in R]
|
||||
|
||||
|
||||
def 분류(이름: str, 출처: str):
|
||||
# 화초·수생식물 줄 — 규격이 분·포트·cm·분얼·개화구·엽·년생 꼴 (이름 글자로는 못 가림)
|
||||
식물규격 = re.compile(r"^\d+(\.\d+)?cm$|포트|분얼|개화구|\d엽$|년생|\d+cm분$")
|
||||
|
||||
|
||||
def 분류(이름: str, 출처: str, 규격: str = ""):
|
||||
n = re.sub(r"\s+", "", 이름 or "")
|
||||
if 출처.startswith("자재품목 조달청"):
|
||||
return 조달청_분류.get(n, ("미분류", None))
|
||||
@@ -495,6 +503,8 @@ def 분류(이름: str, 출처: str):
|
||||
for a, b, rx in RX:
|
||||
if rx.search(n):
|
||||
return (a, b)
|
||||
if 규격 and 식물규격.search(규격.replace(" ", "")):
|
||||
return ("식생·종자", "초화·수생식물")
|
||||
return ("미분류", None)
|
||||
|
||||
|
||||
@@ -519,7 +529,8 @@ def main():
|
||||
if m and '"키": "MT' in ln:
|
||||
nm = json.loads('"' + NAME.search(ln).group(1) + '"')
|
||||
sc = json.loads('"' + SRC_.search(ln).group(1) + '"')
|
||||
a, b = 분류(nm, sc)
|
||||
sp = re.search(r'"규격": "((?:[^"\\]|\\.)*)"', ln)
|
||||
a, b = 분류(nm, sc, json.loads('"' + sp.group(1) + '"') if sp else "")
|
||||
ln = LINE.sub(lambda _: f'"구분": {q(a)}, "상세구분": {q(b)}', ln, count=1)
|
||||
if not nm:
|
||||
ln = ln.replace('"비고": null', f'"비고": "{NOTE_EMPTY}"', 1)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""재료_품셈재료.json 의 「고르기」 칸 채움 — 품셈 원문 이름 → 자재품목 구분 · 상세구분[· 규격].
|
||||
|
||||
· 바탕 = `build_자재품목_분류.분류` (sub2 가 자재품목을 가른 바로 그 글자 규칙) · 묶음 목록 ref/_묶음_자재품목.md
|
||||
· 손으로 바로잡은 줄은 OVERRIDE — 값이 None 이면 비움(까닭을 비고에 적음)
|
||||
· 규격은 원문 규격 낱말이 모두 든 후보가 있을 때만 붙임(없으면 구분 · 상세구분까지만)
|
||||
· 후보 0 이거나 못 가린 줄은 「고르기」 null + 비고에 까닭
|
||||
· 값 · 이름 · 키 · 연결은 건드리지 않음 · 다시 돌리면 같은 결과
|
||||
|
||||
사용: python scripts/build_품셈재료_고르기.py (JSON 갱신)
|
||||
python scripts/build_품셈재료_고르기.py --보기 (고칠 내용만 나열)
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
MASTER = HERE.parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
|
||||
import master_material as mm # noqa: E402
|
||||
from build_자재품목_분류 import 분류 # noqa: E402
|
||||
|
||||
SRC = MASTER / "재료_품셈재료.json"
|
||||
MT = MASTER / "재료_자재품목.json"
|
||||
|
||||
# 글자 규칙이 엉뚱한 묶음을 집은 줄 — 손으로 바로잡음(None = 비움)
|
||||
OVERRIDE = {
|
||||
"MP000012": None, # 사다리(ton) — 어느 묶음인지 원문만으로 못 가림
|
||||
"MP000015": ("단열·보온", "단열재"), # 문양 스티로폴 — 「문」 글자에 걸림
|
||||
"MP000024": ("관·이음관", "이음쇠·부속"), # 고무링(VR관)
|
||||
"MP000029": ("관·이음관", "이음쇠·부속"),
|
||||
"MP000034": ("관·이음관", "이음쇠·부속"),
|
||||
"MP000052": None, # 스페이셔(몰탈 블록)
|
||||
"MP000094": None, # 쉬즈관 — 강관 값으로 재면 어긋남
|
||||
"MP000101": None, # 내부PANEL(목재문) — 가설건축물 부재
|
||||
"MP000110": ("거푸집·가설재", "가설건축물"),
|
||||
"MP000111": ("거푸집·가설재", "가설건축물"),
|
||||
"MP000112": ("거푸집·가설재", "가설건축물"),
|
||||
"MP000134": None, # 모래주머니
|
||||
"MP000139": None, # HDPE 시트 — PE「관」 아님
|
||||
"MP000145": ("잡재료", "용접·가스"), # CO₂ 와이어
|
||||
"MP000147": ("잡재료", "용접·가스"), # 잠호용접와이어
|
||||
"MP000164": ("건축마감", "천장·바닥·마감재"), # 테라조 걸레받이
|
||||
"MP000165": ("건축마감", "천장·바닥·마감재"), # 합성수지 걸레받이
|
||||
"MP000195": ("잡재료", "용접·가스"), # 플럭스(브레이징용)
|
||||
"MP000200": ("철근·강재", "철근"), # 철물(D22 철근)
|
||||
"MP000209": None, # 버너(압접가열용)
|
||||
"MP000214": None, # 퓨즈(테르밋 용접용)
|
||||
"MP000250": None, # Lagging Sheet
|
||||
"MP000317": ("잡재료", "소모품"), # 톱날
|
||||
"MP000357": ("잡재료", "화약"), # 카트리지(화약)
|
||||
"MP000362": ("잡재료", "소모품"), # 폴리에틸렌필름
|
||||
}
|
||||
NO_GROUP = "자재품목 묶음에 짝이 없음 — 관리자가 화면에서 이음"
|
||||
NO_ROW = "자재품목에 그 묶음 줄이 없음"
|
||||
NOTE = re.compile(r"\s*·?\s*고르기 없음\([^)]*\)")
|
||||
FIELD = re.compile(r'("연결": (?:null|"[^"]*"|\{[^{}]*\}))(, "고르기": (?:null|\{[^{}]*\}))?')
|
||||
NOTE_FIELD = re.compile(r'(, "비고": )(null|"(?:[^"\\]|\\.)*")')
|
||||
|
||||
|
||||
def 고르기(row: dict, market: list[dict]) -> tuple[dict | None, str]:
|
||||
"""한 줄의 고르기 조건 — (조건 또는 None, 까닭)."""
|
||||
key = str(row.get("키"))
|
||||
if row.get("연결") is not None:
|
||||
return None, "" # 관리자가 이미 이은 줄 — 옛 방식 그대로
|
||||
if key in OVERRIDE:
|
||||
got = OVERRIDE[key]
|
||||
if got is None:
|
||||
return None, NO_GROUP
|
||||
sub, detail = got
|
||||
else:
|
||||
sub, detail = 분류(row.get("이름"), "")
|
||||
if sub == "미분류":
|
||||
return None, NO_GROUP
|
||||
hits = [r for r in market if r.get("구분") == sub and r.get("상세구분") == detail]
|
||||
if not hits:
|
||||
return None, NO_ROW
|
||||
want = [mm.plain_spec(w) for w in str(row.get("규격") or "").replace(",", " ").split()]
|
||||
if want and any(all(w in mm.plain_spec(r.get("규격")) for w in want) for r in hits):
|
||||
return {"구분": sub, "상세구분": detail, "규격": row["규격"]}, ""
|
||||
return {"구분": sub, "상세구분": detail}, ""
|
||||
|
||||
|
||||
def main(show: bool) -> int:
|
||||
market = json.loads(MT.read_text(encoding="utf-8"))["줄"]
|
||||
rows = {str(r["키"]): r for r in json.loads(SRC.read_text(encoding="utf-8"))["줄"]}
|
||||
out, n_pick, n_spec, n_blank = [], 0, 0, 0
|
||||
for line in SRC.read_text(encoding="utf-8").split("\n"):
|
||||
m = FIELD.search(line)
|
||||
if not m or '"키": "MP' not in line:
|
||||
out.append(line)
|
||||
continue
|
||||
row = rows[json.loads(line.split('"키": ', 1)[1].split(",", 1)[0])]
|
||||
cond, why = 고르기(row, market)
|
||||
n_pick += cond is not None
|
||||
n_spec += bool(cond and "규격" in cond)
|
||||
n_blank += cond is None
|
||||
text = json.dumps(cond, ensure_ascii=False) if cond else "null"
|
||||
line = FIELD.sub(lambda mm_: mm_.group(1) + f', "고르기": {text}', line, count=1)
|
||||
line = _note(line, why)
|
||||
if show and (cond or why):
|
||||
print(f"{row['키']} {row['이름']} | {row.get('규격', '')} | {text} {why}")
|
||||
out.append(line)
|
||||
print(f"고르기 {n_pick} 줄 (규격 조건 {n_spec}) · 빈 채 {n_blank} 줄")
|
||||
if not show:
|
||||
SRC.write_text("\n".join(out), encoding="utf-8", newline="\n")
|
||||
return 0
|
||||
|
||||
|
||||
def _note(line: str, why: str) -> str:
|
||||
"""비고 — 옛 「고르기 없음(…)」 을 지우고 까닭이 있으면 새로 붙임(비고 칸이 없으면 만듦)."""
|
||||
m = NOTE_FIELD.search(line)
|
||||
old = NOTE.sub("", (json.loads(m.group(2)) if m else None) or "").strip(" ·")
|
||||
text = " · ".join(x for x in (old, f"고르기 없음({why})" if why else "") if x)
|
||||
new = json.dumps(text, ensure_ascii=False) if text else "null"
|
||||
if m:
|
||||
return line[: m.start(2)] + new + line[m.end(2) :]
|
||||
if not text:
|
||||
return line
|
||||
cut = line.rindex(" }")
|
||||
return line[:cut] + f', "비고": {new}' + line[cut:]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.exit(main("--보기" in sys.argv))
|
||||
@@ -92,7 +92,7 @@ ROW_KEYS = {
|
||||
"중간",
|
||||
"끝수",
|
||||
),
|
||||
"품셈재료": ("키", "원문번호", "이름", "규격", "단위", "요구절", "연결"),
|
||||
"품셈재료": ("키", "원문번호", "이름", "규격", "단위", "요구절", "연결", "고르기"),
|
||||
}
|
||||
# 한 테이블 파일 — 줄의 갈래 칸 · 머리의 갈래 묶음
|
||||
MERGED = {"인력": "구분", "기계": "세부분류"}
|
||||
@@ -312,12 +312,17 @@ def _check_linked(where: str, row: dict) -> list[str]:
|
||||
|
||||
|
||||
def check_links(whole: mf.Master) -> list[str]:
|
||||
"""끊긴 키 — 품셈재료 연결 · 인력 준용이 가리키는 줄 · 추정 줄 값 = 준용 직종 값."""
|
||||
"""끊긴 키 — 품셈재료 연결 · 고르기 조건 · 인력 준용이 가리키는 줄 · 추정 줄 값 = 준용 직종 값."""
|
||||
out = []
|
||||
for key, row in whole.index.get("MP", {}).items():
|
||||
link = row.get("연결")
|
||||
if isinstance(link, str) and link not in whole.index.get(link[:2], {}):
|
||||
out.append(f"{whole.label(key)} · 연결 「{link}」 이 자재품목·유가전력에 없음")
|
||||
pick = row.get("고르기")
|
||||
if isinstance(pick, dict):
|
||||
out += mf.check_pick(whole, whole.label(key) + " 고르기", pick)
|
||||
elif pick is not None:
|
||||
out.append(f"{whole.label(key)} · 고르기 모양 「{pick}」 — 조건 묶음이나 null")
|
||||
for key, row in whole.index.get("LB", {}).items():
|
||||
for col in ("준용",):
|
||||
ref = row.get(col)
|
||||
|
||||
@@ -703,6 +703,7 @@ from master_material import ( # noqa: E402, F401
|
||||
TARIFF,
|
||||
candidates,
|
||||
check_pick,
|
||||
cond_text,
|
||||
element,
|
||||
pick,
|
||||
)
|
||||
|
||||
@@ -106,6 +106,11 @@ def _candidates(master: mf.Master, ref: str, row: dict, env: dict) -> list[str]:
|
||||
|
||||
|
||||
# ── 재료 고르기 ────────────────────────────────────────────────────────
|
||||
def cond_text(cond: dict) -> str:
|
||||
"""고르기 조건 한 글자 열쇠 — 「구분|상세구분|규격|대표」 · 화면 단가 미리보기가 줄을 찾는 이름."""
|
||||
return "|".join(str(cond.get(k) or "") for k in PICK_KEYS)
|
||||
|
||||
|
||||
def candidates(master: mf.Master, cond: dict, env: dict | None = None) -> list[str]:
|
||||
"""고르기 조건 안 자재품목 키 — 구분 · 상세구분 · 규격 낱말이 모두 든 줄 · 파일 차례대로.
|
||||
입력 `자재지역` 이 있으면 이름에 그 지역이 든 줄만(그런 줄이 없으면 조건 안 전부)."""
|
||||
|
||||
@@ -221,6 +221,10 @@ def region_pick(cands: dict, want: frozenset):
|
||||
sup = [k for k in cands if towns and towns <= k]
|
||||
if sup:
|
||||
return [r for k in sup for r in cands[k]], f"지역 포함 {len(sup)}줄"
|
||||
# 묶음 시군 가운데 일부만 조달청 줄이 있음 — 그 시군 줄만(같은 도 · 묶음 밖 시군이 낀 줄은 뺌)
|
||||
part = [k for k in cands if towns & k and k <= want | PROVINCE and k - PROVINCE <= want]
|
||||
if len(towns) > 1 and part:
|
||||
return [r for k in part for r in cands[k]], f"지역 일부 {len(part)}줄"
|
||||
return None, "지역 못 찾음"
|
||||
|
||||
|
||||
@@ -308,7 +312,7 @@ class Matcher:
|
||||
if cut != want:
|
||||
cands, why = region_pick(table, cut)
|
||||
if cands is None or (
|
||||
why.startswith(("지역 묶음", "지역 포함")) and "제외" in name
|
||||
why.startswith(("지역 묶음", "지역 포함", "지역 일부")) and "제외" in name
|
||||
): # 「~ 제외」 묶음은 짝짓지 않음
|
||||
return None, why if cands is None else "지역 못 찾음"
|
||||
corp = re.findall(
|
||||
@@ -538,6 +542,7 @@ def decide(x: dict, r: dict, day: str) -> tuple[int | None, str]:
|
||||
return None, f"관급: 조달청 값 못 채움 — {r['까닭']} ({day})"
|
||||
c = r["조달청"]
|
||||
who = f" {c['제조사']} 최저" if "최저" in r["까닭"] else ""
|
||||
who += " · 묶음 시군 일부만 조달청에 있음" if "지역 일부" in r["까닭"] else ""
|
||||
return r["새값"], (
|
||||
f"관급: 조달청 {METHOD.get(c['방식'], c['방식'])}{who} {day} · 계약 {c['계약시작']}~ · {c['지역']} · {c['인도']} · {c['부가세']}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user