Merge branch 'sub_laptop_3' of C:/Aislo_wt/sub_laptop_3 into main_laptop_1
This commit is contained in:
@@ -248,7 +248,11 @@ function rows(
|
||||
}
|
||||
});
|
||||
if (Object.keys(at).length) scope.번지 = at;
|
||||
const name = [line.이름, (line as { 규격?: string }).규격].filter(Boolean).join(" ");
|
||||
const spec = (line as { 규격?: string }).규격;
|
||||
const name =
|
||||
spec && !String(line.이름 ?? "").includes(spec)
|
||||
? `${line.이름 ?? ""} ${spec}`.trim()
|
||||
: String(line.이름 ?? ""); // 이름에 규격이 들면 한 번만(814-1)
|
||||
const qty =
|
||||
line.수량 !== undefined && line.수량 !== null && line.수량 !== "" ? line.수량 : null;
|
||||
const qtyNode = (cost?: string): (string | HTMLElement)[] =>
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
STmate = 원문 그대로 `M02_San_Emit.stmate`(원파일 원문 = 엑셀 숨은 G 열 · 단가 = COSTN ·
|
||||
끝전 = 옵션 SAN_TYP 1 비목버림 / 0 합버림_큰나머지 · 810-1 저장값 대조로 확인).
|
||||
블록 번 = 엑셀 길과 같음(EST = 산근 표 차례 1부터 = 「단산N」 · STmate = 코드 숫자).
|
||||
같은 공사 = 엑셀 산근 블록이 모두 원파일 블록과 금액 · 이름이 같음(`same`) — 둘 다 있으면 원파일.
|
||||
일위대가(EST HeadMainDat) → 실무자료 호표 꼴(`hopyo`) · STmate 일위대가 구성은 못 읽음(BDQTY 난독화) →
|
||||
호표 없음(엑셀을 같이 올리면 엑셀 호표 + 원파일 산근).
|
||||
올리기 = 파일 하나(814-1 · 요구 742-1 — 같은 공사 엑셀 붙이기 걷음) · 원파일 = 단가산출표 판(`grid` ·
|
||||
산출근거 머리) + 산출근거 · 일위대가 안 만듦(요구 743-2 · `hopyo` = 단산만).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -122,6 +121,9 @@ def emit(saved: dict, no: int, keys: bool = True) -> dict[str, Any]:
|
||||
marks = sum(1 for one in out["원본"]["글줄"] if one.get("표지"))
|
||||
sure = len(out["로직"]["호표"]) + len(out["로직"].get("덧줄") or []) + len(out["로직"]["중간"])
|
||||
out["원본"] |= {"원파일": True, "갈래": "STmate", "확정": {"확정": sure, "표지": marks}}
|
||||
out["원본"] |= (
|
||||
{"규격": blk["규격"]} if blk.get("규격") else {}
|
||||
) # 연결 찾기 근거 · 이름 꼬리(814-1)
|
||||
return _excel(saved, out, blk)
|
||||
|
||||
|
||||
@@ -154,89 +156,52 @@ def inner(saved: dict, logic: dict) -> list[tuple[dict, int | None, str | None]]
|
||||
return out
|
||||
|
||||
|
||||
# ── 같은 공사 · 일위대가 ───────────────────────────────────────────────
|
||||
def same(saved: dict, sheets: dict) -> bool:
|
||||
"""엑셀 산근 블록이 모두 원파일 같은 번 블록과 계 금액(1 원 안)이 같음 = 같은 공사."""
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
|
||||
have = numbers(saved)
|
||||
kind = book.kind_of(sheets)
|
||||
if not kind or (kind == "STmate") != (saved["꼴"] == "STmate"):
|
||||
return False
|
||||
if kind == "STmate":
|
||||
excel = {int(b["코드"][1:]): b["머리"].get("C") for b in book.stmate_blocks(sheets)}
|
||||
else:
|
||||
excel = {int(n): t.get("T") for n, t in book.est_prices(sheets)[1].items()}
|
||||
if not excel:
|
||||
return False
|
||||
for n, total in excel.items():
|
||||
blk = have.get(int(n))
|
||||
if blk is None or abs(float(total or 0) - float((blk.get("저장") or {}).get("T") or 0)) > 1:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
GALLAE = {6: "노임", 5: "자재", 105: "기계", 104: "단가산출", 103: "호표", 21: "기타", 101: "기타"}
|
||||
# ── 단가산출표 판(814-1 · 요구 742-2 · 743-2) ───────────────────────────
|
||||
LIST = "단가산출표"
|
||||
_LIST_HEAD = ("코드", "이름", "규격", "단위", "재료비", "노무비", "경비", "합계")
|
||||
_LIST_COST = ("M", "L", "S", "T")
|
||||
|
||||
|
||||
def hopyo(saved: dict) -> dict[str, Any]:
|
||||
"""EST 일위대가(HeadMainDat) → 실무자료 읽기 꼴(`M02_Hopyo_Excel.read_book` 짝 · 호표 · 단산) ·
|
||||
STmate = 호표 없음(구성 못 읽음)."""
|
||||
blocks = numbers(saved)
|
||||
names = {str(b.get("코드")): n for n, b in blocks.items()}
|
||||
heads = {str(h.get("코드")): n for n, h in enumerate(saved.get("일위") or [], 1)}
|
||||
"""원파일 → 실무자료 읽기 꼴 — 단산(산출근거 머리)만 · 일위대가 안 만듦(요구 743-2 · HeadMainDat 안 씀)."""
|
||||
out: dict[str, Any] = {"꼴": "원파일", "시트": saved["꼴"], "호표": [], "검산": {"어긋남": []}}
|
||||
out["단산"] = [
|
||||
{"번": n, "이름": b.get("이름"), "단위": b.get("단위"), "금액": _cost(b.get("저장"))}
|
||||
for n, b in blocks.items()
|
||||
for n, b in numbers(saved).items()
|
||||
]
|
||||
if saved["꼴"] != "EST":
|
||||
return out
|
||||
for n, h in enumerate(saved.get("일위") or [], 1):
|
||||
lines = []
|
||||
for k, ln in enumerate(h.get("줄") or []):
|
||||
link = ln.get("연결")
|
||||
code = str((ln.get("자원") or {}).get("코드") or "")
|
||||
ref = None
|
||||
if link == 104 and code in names:
|
||||
ref = {"종류": "단산", "번": names[code]}
|
||||
elif link == 103 and code in heads:
|
||||
ref = {"종류": "일위", "번": heads[code]}
|
||||
kind = "%" if ln.get("연결") in (52, 58, 62) else GALLAE.get(link or 0, "기타")
|
||||
if kind == "자재" and str(ln.get("단위") or "") == "인":
|
||||
kind = "노임"
|
||||
if not any((ln.get("단가") or {}).values()) and kind not in ("%",):
|
||||
kind = "별도계상" if not ref else kind
|
||||
lines.append(
|
||||
{
|
||||
"차례": k,
|
||||
"이름": ln.get("품명") or "",
|
||||
"규격": ln.get("규격") or "",
|
||||
"수량": _dec(ln.get("수량")),
|
||||
"단위": ln.get("단위") or "",
|
||||
"단가": _cost(ln.get("단가")),
|
||||
"금액": _cost(ln.get("저장")),
|
||||
"비고": "",
|
||||
"참조": ref,
|
||||
"갈래": kind,
|
||||
"행": ln.get("행"),
|
||||
"코드": code or None,
|
||||
}
|
||||
)
|
||||
out["호표"].append(
|
||||
{
|
||||
"번": n,
|
||||
"이름": h.get("이름"),
|
||||
"규격": h.get("규격") or "",
|
||||
"단위": h.get("단위") or "",
|
||||
"금액": _cost(h.get("저장")),
|
||||
"행": [n, n],
|
||||
"줄": lines,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def grid(saved: dict) -> dict[str, Any]:
|
||||
"""원파일 → 단가산출표 한 시트 격자(`M02_Hopyo_Grid.read` 꼴) — 산출근거 머리(코드 · 이름 · 규격 ·
|
||||
단위 · 저장 금액) 한 줄씩 · 행 = 1 머리 + 블록 차례."""
|
||||
cells: dict[str, dict] = {}
|
||||
cols = "ABCDEFGH"
|
||||
for c, head in zip(cols, _LIST_HEAD):
|
||||
cells[f"{c}1"] = {"값": head}
|
||||
rows = []
|
||||
for at, (n, b) in enumerate(numbers(saved).items(), 2):
|
||||
cost = b.get("저장") or {}
|
||||
vals = [b.get("코드"), b.get("이름"), b.get("규격"), b.get("단위")]
|
||||
vals += [_dec(cost.get(k)) for k in _LIST_COST]
|
||||
for c, v in zip(cols, vals):
|
||||
if v not in (None, ""):
|
||||
cells[f"{c}{at}"] = {"값": v if isinstance(v, (int, float)) else str(v)}
|
||||
rows.append({"번": n, "이름": b.get("이름"), "행": [at, at]})
|
||||
sheet = {"id": "s1", "이름": LIST, "칸": cells, "틀고정": {"행": 1}}
|
||||
sheet["열"] = {c: {"폭": w} for c, w in zip(cols, (10, 40, 18, 6, 12, 12, 12, 12))}
|
||||
book = {"종류": "통합문서", "판": 1, "열": "", "서식": [{}], "시트": [sheet], "활성": "s1"}
|
||||
return {
|
||||
"책": book,
|
||||
"호표": [],
|
||||
"단산": rows,
|
||||
"보기": {LIST: "s1"},
|
||||
"원본": {LIST: LIST},
|
||||
"목록": {"호표": [], "단산": [{"번": r["번"], "행": r["행"][0]} for r in rows]},
|
||||
"줄": [],
|
||||
}
|
||||
|
||||
|
||||
def _dec(v: Any) -> int | float | None:
|
||||
"""수 → 엑셀 읽기와 같은 꼴(정수 = int · 아니면 float · 없음 = None)."""
|
||||
if v is None:
|
||||
|
||||
@@ -226,6 +226,8 @@ def _link_of(item: dict) -> tuple[dict | None, str]:
|
||||
}
|
||||
return link | ({"칸": item["참조비목"]} if item.get("참조비목") else {}), ""
|
||||
key = str(item.get("이음") or "")
|
||||
if not key and (item.get("자원") or {}).get("조합식"):
|
||||
return None, f"우리 조합 식 {item['자원']['조합식']} 에 듦(첫 줄) — 이 줄 값 0"
|
||||
if not key:
|
||||
return None, ""
|
||||
if item.get("종류") == "로직":
|
||||
@@ -233,12 +235,59 @@ def _link_of(item: dict) -> tuple[dict | None, str]:
|
||||
if key.startswith("EQ"):
|
||||
got = hourly_link(key, str(item.get("비목") or ""))
|
||||
return got, "" if got else f"기계 {key} 시간당 경비 자료 없음 — 엑셀 단가 그대로"
|
||||
if key.startswith("MO") and (got := _oil(key)):
|
||||
return got, ""
|
||||
link = {"갈래": "요소", "키": key}
|
||||
if key[:2] == dc.cv.mm.LINKED: # 품셈재료 = 줄 단위로 값이 갈림(M01 변환과 같음)
|
||||
link["단위"] = str(item.get("단위") or "")
|
||||
return link, ""
|
||||
|
||||
|
||||
OPTION = "_옵션" # 연결이 부르는 옵션 변수(문서에 없으면 `_attach` 가 더함)
|
||||
|
||||
|
||||
def _oil(key: str) -> dict | None:
|
||||
"""유가 · 전력 이음(814-1 · 요구 748) — 우리 기초 꼴: 유가 = `MO:경유:{S_지역}` + 지역 옵션(기본
|
||||
전국평균) · 전력 = 전력량요금 계약종별 고르기 + 계약종별 옵션(기본 일반용(갑)Ⅰ 저압)."""
|
||||
whole = imp._whole()
|
||||
try:
|
||||
row = whole.get(key)
|
||||
except Exception: # noqa: BLE001 — 없는 키 = 보통 요소 연결
|
||||
return None
|
||||
rows = list(whole.index.get("MO", {}).values())
|
||||
if row.get("구분") == "전력":
|
||||
kinds = list(dict.fromkeys(str(r.get("상세구분")) for r in rows if r.get("구분") == "전력" and r.get("이름") == row.get("이름"))) # fmt: skip
|
||||
var = _choice("전력 계약종별", "전력계약종별", kinds, str(row.get("상세구분")))
|
||||
link = {
|
||||
"갈래": "요소",
|
||||
"재료": {"이름": row.get("이름"), "계약종별": "입력"},
|
||||
"단위": "㎾h",
|
||||
}
|
||||
return link | {"계약종별": dc.md.var_id(var), OPTION: var}
|
||||
head = str(row.get("원문번호") or "").split(":")[0]
|
||||
if not head or row.get("구분") != "유류":
|
||||
return None
|
||||
places = [
|
||||
str(r.get("상세구분")) for r in rows if str(r.get("원문번호") or "").split(":")[0] == head
|
||||
]
|
||||
places.sort(key=lambda x: x != "전국평균")
|
||||
var = _choice("지역", "지역", places, str(row.get("상세구분")))
|
||||
return {"갈래": "요소", "키": f"MO:{head}:{{{dc.md.var_id(var)}}}", OPTION: var}
|
||||
|
||||
|
||||
def _choice(key: str, below: str, items: list[str], value: str) -> dict:
|
||||
link = {"갈래": "선택", "목록": items, "값": value}
|
||||
return {
|
||||
"열쇠": key,
|
||||
"기호": "S",
|
||||
"아래": below,
|
||||
"위": "",
|
||||
"이름": key,
|
||||
"단위": "",
|
||||
"연결": link,
|
||||
}
|
||||
|
||||
|
||||
MACHINES = "_기계" # 문서 셈 동안 잠깐 — `derive` 가 머리 `가져온.기계` 로 옮김
|
||||
|
||||
|
||||
@@ -280,7 +329,15 @@ def _attach(doc: dict, row: dict, ids: list[str], src: dict[str, dict]) -> None:
|
||||
if note:
|
||||
var["설명"] = note
|
||||
if link is None:
|
||||
if not note and not item.get("이음"): # 못 이은 까닭(814-1 — 흠 아님 · 우리 정보 없음)
|
||||
from M02_MasterTemplete import M02_San_Link as san_link
|
||||
|
||||
var["설명"] = f"이음 없음 — {san_link.why(item)} · 엑셀 단가 그대로"
|
||||
continue
|
||||
if (opt := link.pop(OPTION, None)) and opt["열쇠"] not in {
|
||||
v.get("열쇠") for v in doc["변수"]
|
||||
}:
|
||||
doc["변수"].append(opt) # 유가 지역 · 전력 계약종별 옵션(문서에 하나)
|
||||
if item.get("참조비목") and isinstance(excel, dict):
|
||||
excel = excel[item["참조비목"]]
|
||||
line["단가"], line["비목"] = dc.md.var_id(var), item["참조비목"]
|
||||
|
||||
@@ -68,20 +68,26 @@ def _rename_steps(doc: dict, swap: dict[str, str]) -> None:
|
||||
st["줄"] = got
|
||||
|
||||
|
||||
def _bundle_of(doc: dict, at: int) -> list[int] | None:
|
||||
"""at 에서 시작하는 기계 세 줄(노무비 · 재료비 · 경비) — 같은 기계 이음 · 같은 수량."""
|
||||
def _bundle_of(doc: dict, at: int, who: dict[str, str] | None = None) -> list[int] | None:
|
||||
"""at 에서 시작하는 기계 세 줄(노무비 · 재료비 · 경비 · 차례 아무렇게) — 같은 기계 이음 · 같은 수량 ·
|
||||
시간당 경비 이음 없음(숫자)이면 같은 기계 자원(`who` 줄 이름 → 자원 · 814-1)."""
|
||||
money = doc.get("금액") or []
|
||||
three = money[at : at + 3]
|
||||
if len(three) < 3 or [x.get("비목") for x in three] != list(COSTS):
|
||||
if len(three) < 3 or sorted(str(x.get("비목")) for x in three) != sorted(COSTS):
|
||||
return None
|
||||
vars_ = {v.get("열쇠"): v for v in doc.get("변수") or []}
|
||||
links = [(vars_.get(str(x.get("단가"))) or {}).get("연결") or {} for x in three]
|
||||
if any(x.get("갈래") != "단가산출" or "기계" not in (x.get("입력") or {}) for x in links):
|
||||
if all(x.get("갈래") == "숫자" and not isinstance(x.get("값"), dict) for x in links):
|
||||
mine = {(who or {}).get(str(x.get("이름"))) for x in three}
|
||||
if len(mine) != 1 or None in mine:
|
||||
return None
|
||||
elif any(x.get("갈래") != "단가산출" or "기계" not in (x.get("입력") or {}) for x in links):
|
||||
return None
|
||||
same = {(x.get("키"), str(x.get("입력"))) for x in links}
|
||||
if len(same) != 1 or [x.get("칸") for x in links] != list(COSTS):
|
||||
elif len({(x.get("키"), str(x.get("입력"))) for x in links}) != 1 or [
|
||||
x.get("칸") for x in links
|
||||
] != [x.get("비목") for x in three]:
|
||||
return None
|
||||
for k in ("수량", "규격", "자리", "합밖"):
|
||||
for k in ("수량", "규격", "자리", "합밖", "번지"):
|
||||
if len({str(x.get(k) or "") for x in three}) != 1:
|
||||
return None
|
||||
names = {str(x.get("이름")) for x in three}
|
||||
@@ -113,30 +119,70 @@ def _rewrite(text: Any, swap: dict[str, str]) -> Any:
|
||||
return pat.sub(lambda m: swap[m.group(0)], text)
|
||||
|
||||
|
||||
def machines(doc: dict, title: dict[str, str]) -> None:
|
||||
"""② EST 기계 세 줄 → 한 기계 줄(비목 묶음) · 부르던 식 = 비목 칸. `title` = {줄 이름: 자원 이름}."""
|
||||
AFTER = "_뒤" # 옮긴 조합 다음 줄 — 차림이 원본 행 차례로 다시 섞지 않게(다듬기 사이 임시)
|
||||
|
||||
|
||||
def regroup(doc: dict, who: dict[str, str], parts: set[str]) -> None:
|
||||
"""814-1 — 조합 기계 비목 세 줄 사이에 낀 다음 줄(값 0)을 세 줄 뒤로 · 같은 기계끼리 모음
|
||||
(첫 줄 · 다음 줄이 엇갈려 항목이 겹치지 않게 · 값 0 줄이라 셈 그대로). `parts` = 조합 다음 줄 이름."""
|
||||
money = doc.get("금액") or []
|
||||
at = 0
|
||||
while at < len(money):
|
||||
head = who.get(str(money[at].get("이름")))
|
||||
if head is None or str(money[at].get("이름")) in parts:
|
||||
at += 1
|
||||
continue
|
||||
end, mains, rest = at, [], []
|
||||
while end < len(money):
|
||||
name = str(money[end].get("이름"))
|
||||
if name in parts:
|
||||
rest.append(money[end])
|
||||
elif who.get(name) == head and len(mains) < 3:
|
||||
mains.append(money[end])
|
||||
else:
|
||||
break
|
||||
end += 1
|
||||
if len(mains) > 1 and rest:
|
||||
seen = list(dict.fromkeys(who.get(str(x.get("이름"))) for x in rest))
|
||||
rest.sort(key=lambda x: seen.index(who.get(str(x.get("이름"))))) # 같은 기계끼리
|
||||
for x in rest:
|
||||
x[AFTER] = True # 차림(`NativeLayout.apply`) 원본 행 = 앞 줄 행 뒤
|
||||
money[at:end] = mains + rest
|
||||
at = max(end, at + 1)
|
||||
|
||||
|
||||
def machines(doc: dict, title: dict[str, str], who: dict[str, str] | None = None) -> None:
|
||||
"""② 기계 세 줄 → 한 기계 줄(비목 묶음) · 부르던 식 = 비목 칸. `title` = {줄 이름: 자원 이름} ·
|
||||
`who` = {줄 이름: 기계 자원 열쇠}(시간당 경비 이음 없는 세 줄 · 814-1)."""
|
||||
money = doc.get("금액") or []
|
||||
vars_ = {v.get("열쇠"): v for v in doc.get("변수") or []}
|
||||
out, swap, at, used = [], {}, 0, set()
|
||||
while at < len(money):
|
||||
idx = _bundle_of(doc, at)
|
||||
idx = _bundle_of(doc, at, who)
|
||||
if idx is None:
|
||||
out.append(money[at])
|
||||
at += 1
|
||||
continue
|
||||
three = [money[i] for i in idx]
|
||||
first = vars_[str(three[0]["단가"])]
|
||||
link = {k: v for k, v in first["연결"].items() if k not in ("칸", "엑셀")}
|
||||
excel = {
|
||||
c: Decimal(str((vars_[str(x["단가"])]["연결"]).get("엑셀") or 0))
|
||||
for c, x in zip(COSTS, three)
|
||||
}
|
||||
link["엑셀"] = excel
|
||||
name = title.get(str(three[0].get("이름"))) or str(three[0].get("이름"))
|
||||
three = sorted((money[i] for i in idx), key=lambda x: COSTS.index(x["비목"]))
|
||||
first = vars_[str(money[idx[0]]["단가"])]
|
||||
if first["연결"].get("갈래") == "숫자": # 시간당 경비 이음 없음 = 엑셀 값 묶음
|
||||
link = {
|
||||
"갈래": "숫자",
|
||||
"값": {c: (vars_[str(x["단가"])]["연결"]).get("값") for c, x in zip(COSTS, three)},
|
||||
}
|
||||
else:
|
||||
link = {k: v for k, v in first["연결"].items() if k not in ("칸", "엑셀")}
|
||||
link["엑셀"] = {
|
||||
c: Decimal(str((vars_[str(x["단가"])]["연결"]).get("엑셀") or 0))
|
||||
for c, x in zip(COSTS, three)
|
||||
}
|
||||
lead = money[idx[0]]
|
||||
name = title.get(str(lead.get("이름"))) or str(lead.get("이름"))
|
||||
first["연결"], first["이름"] = link, f"{name} 단가"
|
||||
if name in {str(x.get("이름")) for x in money} - {str(x.get("이름")) for x in three}:
|
||||
name = f"{name} ({at + 1})" # 같은 이름 줄이 있으면 겹치지 않게
|
||||
line = {k: v for k, v in three[0].items() if k != "기호"} | {"이름": name, "비목": "묶음"}
|
||||
line = {k: v for k, v in lead.items() if k != "기호"} | {"이름": name, "비목": "묶음"}
|
||||
line["단가"] = lead["단가"]
|
||||
_rename_steps(doc, {str(x.get("이름")): name for x in three})
|
||||
if any(x.get("기호") for x in three): # 부르던 식 = 번지 칸(엔진 `번지.X.노무비`)
|
||||
place = _place(doc, name, used)
|
||||
@@ -147,7 +193,8 @@ def machines(doc: dict, title: dict[str, str]) -> None:
|
||||
doc["변수"] = [
|
||||
v
|
||||
for v in doc["변수"]
|
||||
if v is first or v.get("열쇠") not in {str(x["단가"]) for x in three[1:]}
|
||||
if v is first
|
||||
or v.get("열쇠") not in {str(x["단가"]) for x in three} - {str(lead["단가"])}
|
||||
]
|
||||
out.append(line)
|
||||
at += 3
|
||||
@@ -166,11 +213,18 @@ def shape(doc: dict, meta: dict) -> dict:
|
||||
src = meta.get("원본") or {}
|
||||
lay.tag(doc, meta) # 812-3 원파일 차림 — 금액 줄 ↔ 로직 줄(끝에 `apply`)
|
||||
names(doc, list(src.get("글줄") or []))
|
||||
title = {}
|
||||
title, who, parts = {}, {}, set()
|
||||
for item in (meta.get("로직") or {}).get("호표") or []:
|
||||
res = item.get("자원") or {}
|
||||
if res.get("갈래") == "기계" and res.get("이름"):
|
||||
title[str(item.get("이름"))] = " ".join(x for x in (res["이름"], res.get("규격")) if x)
|
||||
machines(doc, title)
|
||||
key = (res.get("코드"), res.get("이름"), res.get("규격"))
|
||||
who[str(item.get("이름"))] = repr(key)
|
||||
if res.get("조합"):
|
||||
parts.add(str(item.get("이름")))
|
||||
regroup(doc, who, parts)
|
||||
machines(doc, title, who)
|
||||
lay.apply(doc, meta)
|
||||
for line in doc.get("금액") or []:
|
||||
line.pop(AFTER, None)
|
||||
return doc
|
||||
|
||||
@@ -618,6 +618,7 @@ def _off(doc: dict, vid: str, value: Any, seen: set) -> dict:
|
||||
def match(imported: dict, links_imp: Any, index: Index, links: Any) -> dict[str, Any]:
|
||||
"""가져온 식 문서 → 연결후보 `{판정, 키, 이름, 옵션, 다른, 참고, 금액, 후보수}`."""
|
||||
links = links if isinstance(links, Memo) else Memo(links)
|
||||
imported = note.bare(imported) # 엑셀 이름 규격 · 단위 꼬리 뗌(814-1 · 맞대기만)
|
||||
got_imp = _calc(imported, links_imp, {})
|
||||
if got_imp is None:
|
||||
return {"판정": NONE, "까닭": "가져온 식을 우리 단가로 못 셈", "후보수": 0}
|
||||
|
||||
@@ -57,7 +57,9 @@ def judge(sk: dict) -> dict[str, Any]:
|
||||
doc = dim.of(sk)
|
||||
links_imp.learn(doc)
|
||||
got = dm.match(doc, links_imp, index(), dm.ml.Links(whole, dc.folder()))
|
||||
return {**got, "때": utc_now_text()}
|
||||
from M02_MasterTemplete import M02_San_Link as san_link # 우리 정보로 셈 줄 수(814-1 띠)
|
||||
|
||||
return {**got, "우리정보": san_link.tally(sk[imp.KEY].get("로직") or {}), "때": utc_now_text()}
|
||||
|
||||
|
||||
def find(key: str, version: str) -> dict[str, Any]:
|
||||
|
||||
@@ -230,10 +230,27 @@ def rate_pairs(left: list[dict], right: list[dict], take: Any) -> tuple[list[dic
|
||||
for a, b in take(lambda a, b: same(a, b, "대상", "비목")): # 로직 같음 · 율 값만 다름
|
||||
value(a, b)
|
||||
for a, b in take(lambda a, b: same(a, b, "대상")):
|
||||
diffs.append({"항": "비목", "글": f"율 줄 {name(a, b)} 비목 다름"})
|
||||
tool = val.tool_cost(a, b) # 공구손료 비목만 다름 = 값(814-1)
|
||||
(notes if tool else diffs).append(
|
||||
tool or {"항": "비목", "글": f"율 줄 {name(a, b)} 비목 다름"}
|
||||
)
|
||||
value(a, b)
|
||||
for a, b in take(lambda a, b: same(a, b, "율")):
|
||||
diffs.append({"항": "율 대상", "글": f"{name(a, b)} 대상 {target(a)} ↔ {target(b)}"})
|
||||
for a, b in take(is_rate):
|
||||
diffs.append({"항": "율 대상", "글": f"{name(a, b)} 대상 {target(a)} ↔ {target(b)}"})
|
||||
return diffs, notes
|
||||
|
||||
|
||||
# ── 이름 꼬리(814-1 · 요구 743-3) ─────────────────────────────────────────
|
||||
def bare(doc: dict) -> dict:
|
||||
"""엑셀 단가산출 이름의 규격 · 단위 꼬리(「구조물터파기(인력) 경질토사 / M3」) 뗀 맞댈 문서 —
|
||||
보이는 · 저장 이름은 그대로(연결 찾기만 이 사본을 씀)."""
|
||||
name = str(doc.get("이름") or "")
|
||||
unit = re.sub(r"^원\s*/\s*", "", str(doc.get("결과단위") or "")).strip()
|
||||
spec = str((doc.get("가져온") or {}).get("규격") or "").strip()
|
||||
got = re.sub(rf"\s*/\s*{re.escape(unit)}\s*$", "", name) if unit else name
|
||||
if got != name and spec and got.rstrip().endswith(spec) and got.rstrip() != spec:
|
||||
got = got.rstrip()[: -len(spec)]
|
||||
got = got.strip()
|
||||
return doc if not got or got == name else {**doc, "이름": got}
|
||||
|
||||
@@ -27,6 +27,19 @@ CHECK = (*VALUE, "옵션 확인") # 기계 작업 꼴이 확정으로 붙지
|
||||
_NUMBER = re.compile(r"^\s*\d+(?:\.\d+)?\s*$")
|
||||
|
||||
|
||||
# ── 공구손료 율 비목(814-1 · 요구 744 C1) ─────────────────────────────────
|
||||
TOOL = "공구손료 비목"
|
||||
|
||||
|
||||
def tool_cost(a: dict, b: dict) -> dict | None:
|
||||
"""공구손료 율 줄 짝 — 대상은 같고 비목만 다름(실무 재료비 ↔ 우리 경비) = 값(참고 · 확정 안 막음 ·
|
||||
[바꾸기] = 우리 기초 사본 = 우리 비목) · 공구손료 아님 = None."""
|
||||
if "공구" not in str(a.get("이름")) + str(b.get("이름")):
|
||||
return None
|
||||
say = f"{a['이름']} 비목 {'·'.join(a['비목'])} ↔ {'·'.join(b['비목'])} — 바꾸면 우리 비목"
|
||||
return {"항": TOOL, "글": say}
|
||||
|
||||
|
||||
# ── 기종 ──────────────────────────────────────────────────────────────
|
||||
def row_of(code: Any) -> Any:
|
||||
"""원문번호 → 기계표 줄(`M02_Machine_Find.Row`) · 없음 None."""
|
||||
|
||||
@@ -299,7 +299,7 @@ class Block:
|
||||
self.got["확정"] += bool(got)
|
||||
self.register(ln, f"줄.'{name}'")
|
||||
|
||||
def triple(self, rows: list[dict], total: dict | None) -> None:
|
||||
def triple(self, rows: list[dict], total: dict | None, back: int = 0) -> None:
|
||||
"""기계 · 산근 · 견적 세 줄 → 로직 값묶음 한 줄(수량 식 · 자리 같을 때) · 아니면 줄마다."""
|
||||
got = [self.money(ln) for ln in rows]
|
||||
parts = [ln["비목"] for ln in rows]
|
||||
@@ -307,8 +307,8 @@ class Block:
|
||||
if not same:
|
||||
for ln, g in zip(rows, got):
|
||||
self.machine_line(ln, g)
|
||||
if total:
|
||||
self.subtotal(total, [call for _, call, _ in self.done[-len(rows) :]])
|
||||
if total: # 소계 = 세 줄 + 바로 앞 같은 기계 낱 줄(`back`)
|
||||
self.subtotal(total, [call for _, call, _ in self.done[-(len(rows) + back) :]])
|
||||
return
|
||||
qty, place = got[0]
|
||||
prices = {ln["비목"]: (ln.get("연결단가") or [0])[0] for ln in rows}
|
||||
@@ -325,8 +325,10 @@ class Block:
|
||||
title = head.get("품명") or r.get("원이름") or r.get("이름") or "줄"
|
||||
name = self.o.names.item(title)
|
||||
one = {"이름": name, "규격": r.get("규격") or "", "단위": r.get("단위") or "", "수량": qty}
|
||||
link = em.hourly_text(parts[0]["키"]) if parts else None
|
||||
one |= {"종류": "로직", "값묶음": em.bundle(r), "이음": link, "자원": r}
|
||||
one |= {"종류": "로직", "값묶음": em.bundle(r), "이음": None, "자원": r}
|
||||
em.first_spec(one, parts)
|
||||
if parts: # 814-1 조합(굴착기 + 브레이커 · 집게) = 우리 조합 식 한 줄
|
||||
one["이음"] = em.combo_link(one, parts) or em.hourly_text(parts[0]["키"])
|
||||
one |= {"자리": place} if place else {}
|
||||
value = total["결과"][-1] if total and total.get("결과") else None
|
||||
for ln in rows: # 노 · 재 · 경 글 = 펼침(맞댈 값은 한 줄 계 — 소계 글 · 없으면 첫 줄)
|
||||
@@ -362,6 +364,7 @@ class Block:
|
||||
r |= {"찾기": parts[0]} if parts else {}
|
||||
r |= {"조합첫": True} if len(parts) > 1 else {}
|
||||
one = em.resource(name, r.get("단위"), qty, r, parts[0]["키"] if parts else None)
|
||||
em.first_spec(one, parts)
|
||||
one |= {"자리": place} if place else {}
|
||||
self.o.add_line(one, True, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||||
self.got["확정"] += bool(got)
|
||||
@@ -548,8 +551,22 @@ class Block:
|
||||
rows.append(lines[i + len(rows)])
|
||||
nxt = lines[i + len(rows)] if i + len(rows) < len(lines) else None
|
||||
total = nxt if nxt and nxt.get("연결") == link and nxt.get("비목") == "T" else None
|
||||
back, seen = (
|
||||
0,
|
||||
{r.get("비목") for r in rows},
|
||||
) # 바로 앞 같은 기계 낱 줄(얼린 줄 · 814-1)
|
||||
while i - back > 0 and len(seen) < 3:
|
||||
b = lines[i - back - 1]
|
||||
if (
|
||||
(b.get("자원") or {}).get("코드") != code
|
||||
or b.get("비목") in seen
|
||||
or not b.get("결과")
|
||||
):
|
||||
break
|
||||
seen.add(b.get("비목"))
|
||||
back += 1
|
||||
if all(r.get("결과") for r in rows):
|
||||
self.triple(rows, total)
|
||||
self.triple(rows, total, back)
|
||||
else:
|
||||
for r in rows:
|
||||
self.mark(r, "결과 없는 세 줄")
|
||||
|
||||
@@ -85,6 +85,9 @@ def apply(doc: dict, meta: dict) -> None:
|
||||
if j is not None:
|
||||
at[j] = min(at.get(j, t["행"]), t["행"])
|
||||
rows = _fill([at.get(j) for j in range(len(money))])
|
||||
for j in range(1, len(rows)): # 조합 다음 줄을 세 줄 뒤로 옮긴 것(814-1 `Import_Shape.regroup`)
|
||||
if money[j].get("_뒤"):
|
||||
rows[j] = max(rows[j], rows[j - 1])
|
||||
order = sorted(range(len(money)), key=lambda j: (rows[j], j))
|
||||
if order != list(range(len(money))) and _safe(doc, order):
|
||||
doc["금액"] = money = [money[j] for j in order]
|
||||
@@ -193,8 +196,9 @@ def _items(doc: dict, texts: list[dict], rows: list[int], stm: bool) -> list[dic
|
||||
cut = False
|
||||
else:
|
||||
one = mach.get(price_sym(money[val]) or "") if kind == "금액" else None
|
||||
if one and one.get("조합"):
|
||||
if one and one.get("조합") and not (cur and cur.get("_조합") == _mach_title(one)):
|
||||
cur = new(_mach_title(one), bool(cur and cur["가나다"]))
|
||||
cur["_조합"] = _mach_title(one) # 같은 기계 다음 줄(비목 셋)은 한 항목
|
||||
elif one and cur is not None and "+" in cur["제목"]:
|
||||
cur["제목"] = _mach_title(one)
|
||||
cur = cur if cur is not None else new()
|
||||
|
||||
@@ -82,13 +82,9 @@ def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]:
|
||||
grid = hopyo_grid.read(name, data, parsed)
|
||||
except Exception: # 격자는 보기용 덤 — 못 만들어도 올리기는 됨(보기 창 404)
|
||||
grid = None
|
||||
row = hopyo_store.save(store.root(), name, parsed, who, grid, mark)
|
||||
cannot = from_hopyo.keep(
|
||||
store.root(), row["키"], name, data
|
||||
) # 산근 시트(793-3b 가져온 식 열기)
|
||||
return (
|
||||
{**row, "읽기불가": cannot} if cannot else row
|
||||
) # 797-1 가져온 식 못 씀 사유 → 화면 토스트
|
||||
# 산근 시트(793-3b 가져온 식 열기) = HP 를 목록에 보이기 전에 보관(814-1) · 못 씀 사유 → 화면 토스트(797-1)
|
||||
keep = lambda key: from_hopyo.keep(store.root(), key, name, data) # noqa: E731
|
||||
return hopyo_store.save(store.root(), name, parsed, who, grid, mark, keep)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo")
|
||||
|
||||
@@ -124,8 +124,10 @@ def save(
|
||||
who: Any,
|
||||
grid: dict[str, Any] | None = None,
|
||||
mark: str | None = None,
|
||||
first: Any = None,
|
||||
) -> dict[str, Any]:
|
||||
"""읽은 결과(+ 격자) → 새 키로 씀 → 목록 한 줄 · `mark` = 올린 엑셀 지문."""
|
||||
"""읽은 결과(+ 격자) → 새 키로 씀 → 목록 한 줄 · `mark` = 올린 엑셀 지문 ·
|
||||
`first(키)` = HP 를 목록에 보이기 전에 할 일(산근 보관 — 올린 직후 409 막음 · 814-1) · 돌려준 값 = 답 `읽기불가`."""
|
||||
with _LOCK:
|
||||
have = [
|
||||
int(m.group(1)) for p in _dir(folder).glob("HP-*.json") if (m := _KEY.match(p.stem))
|
||||
@@ -145,9 +147,14 @@ def save(
|
||||
if grid is not None:
|
||||
book = {**grid["책"], "열": key}
|
||||
atomic_write_json(_grid_path(folder, key), {**grid, "책": book})
|
||||
cannot = first(key) if first else None
|
||||
atomic_write_json(_path(folder, key), doc)
|
||||
_put_done(folder, log)
|
||||
return {**brief(doc), **({"지문": mark} if mark else {})}
|
||||
return {
|
||||
**brief(doc),
|
||||
**({"지문": mark} if mark else {}),
|
||||
**({"읽기불가": cannot} if cannot else {}),
|
||||
}
|
||||
|
||||
|
||||
def grid(folder: str | Path, key: str) -> dict[str, Any]:
|
||||
|
||||
@@ -83,3 +83,24 @@ def hourly(key: str) -> dict[str, Any]:
|
||||
"원문번호": code,
|
||||
**sums,
|
||||
}
|
||||
|
||||
|
||||
def combo(main: str, part: str) -> tuple[str, str] | None:
|
||||
"""조합 기계(굴착기 + 대형 브레이커 · 부착용 집게 · 814-1) → 우리 조합 식 (키, 부름 글)
|
||||
`로직(GC000271, 기계='…', 브레이커='…', 지역='전국평균', …)` — 두 기계가 그 식 고르기에 있을 때만."""
|
||||
try:
|
||||
codes = [str(store.loaded()[1].get(k).get("원문번호") or "") for k in (main, part)]
|
||||
except cm.mf.FormulaError:
|
||||
return None
|
||||
for row in sorted(_family(), key=lambda r: str(r.get("키"))):
|
||||
ins = row.get("입력") or []
|
||||
attach = [i for i in ins if i.get("이름") not in _PLAIN and i.get("고르기")]
|
||||
if len(attach) != 1 or codes[0] not in [str(c) for c in _codes(row)]:
|
||||
continue
|
||||
if codes[1] not in [str(c) for c in attach[0]["고르기"]]:
|
||||
continue
|
||||
given = {"기계": codes[0], attach[0]["이름"]: codes[1]}
|
||||
given |= {i["이름"]: (i.get("고르기") or [""])[0] for i in ins if i["이름"] not in given}
|
||||
args = "".join(f", {k}='{v}'" for k, v in given.items())
|
||||
return str(row["키"]), f"로직({row['키']}{args})"
|
||||
return None
|
||||
|
||||
@@ -61,7 +61,7 @@ interface EditAnswer {
|
||||
문서: ImportedDoc;
|
||||
전?: Money4 | null;
|
||||
후?: Money4 | null;
|
||||
이음없음?: { 자리: string; 이름: string }[];
|
||||
이음없음?: { 자리: string; 이름: string; 까닭?: string }[];
|
||||
/** qty 답(797-2) */
|
||||
줄?: QtyRow[];
|
||||
집계?: QtyTally;
|
||||
|
||||
@@ -54,7 +54,7 @@ export const tx = (k: keyof typeof TEXT): string =>
|
||||
export interface PanelState {
|
||||
doc: ImportedDoc;
|
||||
/** 이음 없는 자원(서버 price 답) */
|
||||
unlinked: { 자리: string; 이름: string }[];
|
||||
unlinked: { 자리: string; 이름: string; 까닭?: string }[];
|
||||
/** 소요량 · 계수 이음 집계(서버 qty 답 · 797-2) */
|
||||
qty: QtyTally | null;
|
||||
/** 마지막 단가 바꿈 전후 */
|
||||
@@ -149,7 +149,8 @@ export function importedPanel(s: PanelState, a: PanelActs): HTMLElement {
|
||||
const kids: HTMLElement[] = [createToolbar({ items: head })];
|
||||
if (here && s.unlinked.length) {
|
||||
const items = s.unlinked.map((u) => {
|
||||
const b = createButton({ label: `${u.이름} · ${tx("Link")}`, size: 24, variant: "ghost", disabled: s.readOnly || s.busy, onClick: () => a.link(u.자리, u.이름) }); // prettier-ignore
|
||||
const label = `${u.이름}${u.까닭 ? ` (${u.까닭})` : ""} · ${tx("Link")}`; // 못 이은 까닭(814-1)
|
||||
const b = createButton({ label, size: 24, variant: "ghost", disabled: s.readOnly || s.busy, onClick: () => a.link(u.자리, u.이름) }); // prettier-ignore
|
||||
b.dataset.importedLink = u.자리;
|
||||
return b;
|
||||
});
|
||||
|
||||
@@ -275,7 +275,7 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody {
|
||||
const lastOf = (sid: string): string =>
|
||||
lastColumn(book.시트.find((x) => x.id === sid) ?? first);
|
||||
const more = { ...opt, target, grid, last: lastOf, sheet: () => sheet, host: left };
|
||||
nav = ids.일위대가표 ? Nav.createSheetNav(more) : null;
|
||||
nav = ids.일위대가표 || ids.단가산출표 ? Nav.createSheetNav(more) : null; // 원파일 = 단가산출표 판만(814-1)
|
||||
if (nav) warn.after(nav.root);
|
||||
sk = nav ? createSheetSk({ ...opt, target, nav, side: right, ...(opt.onSkChange ? { onChange: opt.onSkChange } : {}) }) : null; // prettier-ignore
|
||||
sheet = createSpreadsheet(left, book, {
|
||||
@@ -290,6 +290,7 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody {
|
||||
if (no !== null && no !== current) mark(no);
|
||||
},
|
||||
});
|
||||
if (!ids.일위대가표 && ids.단가산출표) sk?.at({ 시트: first.id, 칸: "A1" }); // 일위대가 없음 = 단가산출 목록 먼저
|
||||
nav?.marks(briefs);
|
||||
// 머리 행 칸을 눌러 뗌 → 그 호표 영역 고름(끌기 · 방향키 도중엔 안 바꿈 · 줄 행은 그 줄 그대로)
|
||||
left.querySelector(".ss-grid-host")?.addEventListener("mouseup", (e) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Side.ts
|
||||
* 좌측 「일위대가 불러오기」 컨테이너(PLAN 324-4 · 718-1 한시 · 785-4) —
|
||||
* · [불러오기](구조물 조합 층 단추 꼴 `m02-layers__btn`) = 바로 엑셀 고르기 창 → 올림 → 본 칸 불러오기 보기
|
||||
* · [불러오기](구조물 조합 층 단추 꼴 `m02-layers__btn`) = 바로 파일 하나 고르기 창(엑셀 · 경쟁사 .estx · .STC · 814-1) → 올림 → 본 칸 불러오기 보기
|
||||
* (`M02_MasterTemplete_Hopyo_Import` · 엑셀 + 오른쪽 호표 목록 · 가운데 상세 페이지 없음 · 누름 하나)
|
||||
* · 결과 층 고름 = 작은 고름(쓸 수 있는 층 둘 이상일 때만 · 일반 = 개인 하나라 안 보임)
|
||||
* · 「작업 중」 줄 = 층마다 서버 목록(같은 엑셀 작업 중 이어서) · 누르면 본 칸 불러오기 보기
|
||||
@@ -67,7 +67,7 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
});
|
||||
start.classList.add("m02-layers__btn");
|
||||
start.title =
|
||||
"다른 프로그램 일위대가 엑셀 → 우리 일위대가(한시 · 끝내거나 떠나면 올린 파일 지움)";
|
||||
"파일 하나 — 경쟁사 엑셀(.xls · .xlsx) 또는 경쟁사 파일(.estx · .STC) → 우리 정보로 다시 만듦(한시 · 끝내거나 떠나면 올린 파일 지움)";
|
||||
start.dataset.hopyoWizardOpen = "";
|
||||
const seg = el("div", { className: "m02-side__row" });
|
||||
seg.hidden = true;
|
||||
@@ -145,14 +145,10 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
if (!file) return;
|
||||
start.disabled = true;
|
||||
uploadHopyo(scopeOf(pick), file)
|
||||
.then((got: HopyoFile & { 이어서?: boolean; 원파일?: boolean; 붙음?: string[] | null }) => {
|
||||
.then((got: HopyoFile & { 이어서?: boolean; 원파일?: boolean }) => {
|
||||
const tool = /\.estx$/i.test(file.name) ? "EST" : "STmate"; // 810-3 원파일 올림 안내
|
||||
if (got.원파일)
|
||||
showToast(
|
||||
`원파일 · ${tool} — 호표 ${got.호표수}${got.붙음?.length ? ` · 같은 공사 엑셀 ${got.붙음.join(", ")} 에 붙음` : ""}`,
|
||||
"success",
|
||||
);
|
||||
else if (got.이어서) showToast(`같은 엑셀 작업 중 — ${got.키} 이어서`, "info");
|
||||
if (got.이어서) showToast(`같은 파일 작업 중 — ${got.키} 이어서`, "info");
|
||||
else if (got.원파일) showToast(`${tool} 파일 — 단가산출표 · 산출근거`, "success");
|
||||
else showToast(`올림 — 호표 ${got.호표수}`, got.어긋남 ? "warning" : "success");
|
||||
if (got.읽기불가) showToast(`가져온 식으로 열 수 없음 — ${got.읽기불가}`, "warning");
|
||||
return show(pick, got, true);
|
||||
|
||||
@@ -91,7 +91,7 @@ function originCard(o: Origin, band: HTMLElement | null): HTMLElement {
|
||||
{ label: tx("Rows"), value: o.행 },
|
||||
{ label: tx("When"), value: o.때 },
|
||||
{ label: tx("Marks"), value: tx("MarksN", o.표지.length) },
|
||||
...(o.확정 ? [{ label: tx("Sure"), value: o.확정 }] : []),
|
||||
...(o.확정 ? [{ label: o.확정이름 ?? tx("Sure"), value: o.확정 }] : []),
|
||||
]),
|
||||
marks,
|
||||
el("strong", { text: tx("Clauses") }),
|
||||
|
||||
@@ -28,6 +28,8 @@ from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_San_Eval as ev
|
||||
from M02_MasterTemplete import M02_San_Expr as ex
|
||||
from M02_MasterTemplete.M02_San_Link import combo_link, hourly_text # noqa: F401 — 밖에서 em. 으로 부름
|
||||
from M02_MasterTemplete.M02_San_Link import m01 as _m01
|
||||
|
||||
COMP = {"L": "노무비", "M": "재료비", "S": "경비"}
|
||||
_CODE = {v: k for k, v in COMP.items()}
|
||||
@@ -175,61 +177,6 @@ def reprice(logic: dict, where: dict, price: dict[str, float]) -> int:
|
||||
return n
|
||||
|
||||
|
||||
def _m01(kind: str, src: dict) -> tuple[str | None, str | None]:
|
||||
"""1차 줄 판정 유일 짝 M01 키 → (키, 표지) — 없음 = (None, None) · 여럿 = (None, 표지 · 유가 ·
|
||||
환율만 — 재료 · 인력은 조용히 이음 없음) · 기계 = 기계 찾기(812-9) 못 이으면 늘 표지. 재료 줄 이름이 M01 유가전력(MO)이면
|
||||
유가 · 화폐 이름 그대로(달러 · USD …)면 환율(FX)로 판정(797-1 · 「유로폼」 같은 글 속 낱말은 아님)."""
|
||||
if kind not in ("인력", "재료", "기계"):
|
||||
return None, None
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Row as hrow
|
||||
|
||||
line = {k: src.get(k) or "" for k in ("이름", "규격", "단위")}
|
||||
if (
|
||||
kind == "기계"
|
||||
): # 812-9 — 하나 = 이음 · 아니면 표지(후보 여럿 · 못 찾음 · 조용히 넘기지 않음)
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
got = mfind.find(line["이름"], line["규격"])
|
||||
return got["키"], mfind.mark(got, line["이름"], line["규격"])
|
||||
if kind == "재료":
|
||||
if elem.oil_rows(line["이름"]):
|
||||
kind = "유가"
|
||||
elif line["이름"].strip().upper() in {x.upper() for pair in hrow.CURRENCY for x in pair}:
|
||||
kind = "환율"
|
||||
if kind == "유가": # MO 전부 → 줄 규격이 상세구분(전국 · 지역)이면 그것만
|
||||
got = [
|
||||
{"키": r["키"], "규격": str(r.get("상세구분") or "")}
|
||||
for r in elem.oil_rows(line["이름"])
|
||||
]
|
||||
spec = line["규격"].replace(" ", "")
|
||||
got = (
|
||||
[x for x in got if x["규격"] == spec]
|
||||
or [x for x in got if spec and spec in x["규격"]]
|
||||
or got
|
||||
)
|
||||
else:
|
||||
got = hrow.m01_picks(kind, line)
|
||||
keys = {x.get("키") for x in got if x.get("키")}
|
||||
if len(keys) == 1:
|
||||
return keys.pop(), None
|
||||
if len(keys) > 1 and kind in ("유가", "환율"):
|
||||
return None, f"{NOTE}{kind} 후보 여럿({len(keys)}) · 사람 확인"
|
||||
return None, None
|
||||
|
||||
|
||||
def hourly_text(key: str | None) -> str | None:
|
||||
"""기계 EQ → 시간당 기계경비 로직 부름 글(797-1 짝 고름) · 키 · 자료 없음 = None."""
|
||||
if not key:
|
||||
return None
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
|
||||
got = dim.hourly_link(key, "노무비")
|
||||
if not got:
|
||||
return None
|
||||
return f"로직({got['키']}{''.join(f', {k}={v}' for k, v in got['입력'].items())})"
|
||||
|
||||
|
||||
# ── 기계 부분(812-9 · 조합 기계 = 줄 둘) ─────────────────────────────────
|
||||
def machine_parts(src: dict) -> list[dict]:
|
||||
"""기계 자원 → 부분 찾기(`M02_Machine_Find.find_all` · 조합 = 둘 이상) · 기계 아님 = []."""
|
||||
@@ -240,6 +187,13 @@ def machine_parts(src: dict) -> list[dict]:
|
||||
return mfind.find_all(src.get("이름"), src.get("규격"))
|
||||
|
||||
|
||||
def first_spec(one: dict, parts: list[dict]) -> dict:
|
||||
"""조합 기계 첫 줄 규격 = 그 기계 규격(칩 글 · 「0.7+브레이카」 아님 · 814-1) — 자원 원문은 그대로."""
|
||||
if len(parts) > 1 and parts[0].get("규격") is not None:
|
||||
one["규격"] = parts[0]["규격"]
|
||||
return one
|
||||
|
||||
|
||||
def part_line(main: dict, part: dict, names: "Names") -> dict:
|
||||
"""조합 기계 다음 줄 — 같은 수량 식 · 값 0(파일 경비 = 한 덩이 = 첫 줄) · 제 이음."""
|
||||
res = {**main["자원"], "이름": part["이름"], "규격": part["규격"], "찾기": part, "조합": True}
|
||||
@@ -248,8 +202,9 @@ def part_line(main: dict, part: dict, names: "Names") -> dict:
|
||||
title = part["후보"][0]["이름"] if part["키"] else part["이름"]
|
||||
one = {"이름": names.item(str(title)), "규격": part["규격"], "단위": main.get("단위") or ""}
|
||||
one |= {"수량": main["수량"], "자원": res}
|
||||
if main.get("종류") == "로직":
|
||||
one |= {"종류": "로직", "값묶음": bundle(res), "이음": hourly_text(part["키"])}
|
||||
if main.get("종류") == "로직": # 우리 조합 식(814-1 `조합식`)에 든 다음 줄 = 이음 없음 · 값 0
|
||||
link = None if res.get("조합식") else hourly_text(part["키"])
|
||||
one |= {"종류": "로직", "값묶음": bundle(res), "이음": link}
|
||||
else:
|
||||
one |= {"종류": main["종류"], "비목": main["비목"], "단가": Decimal(0), "이음": part["키"]}
|
||||
return one | ({"자리": main["자리"]} if main.get("자리") else {})
|
||||
@@ -439,6 +394,8 @@ class _Stm:
|
||||
unt = (pr.get("meta") or {}).get("UNT")
|
||||
one_, to = node["자원"]["비목"], node["자원"]["옮김"]
|
||||
name = _label(node) or str(pr.get("이름") or code)
|
||||
if node.get(_MERGED): # 814-1 — 기계 세 줄(노 · 재 · 경) = 기계 한 줄 · 이름 = 기계
|
||||
name = str(pr.get("이름") or code)
|
||||
if unt in ev.SPECIAL: # 특수 % — 처음부터 비목 누계 · 앞 계표시
|
||||
dst = to or {"L": "L", "M": "M", "S": "S"}.get(code[0])
|
||||
if unt == "T%":
|
||||
@@ -463,11 +420,13 @@ class _Stm:
|
||||
res |= {"조합첫": True} if len(parts) > 1 else {}
|
||||
else:
|
||||
link, many = _m01(res["갈래"], res) if self.keys else (None, None)
|
||||
one = resource(o.names.item(name), pr.get("단위"), cf, res, link)
|
||||
one = first_spec(resource(o.names.item(name), pr.get("단위"), cf, res, link), parts)
|
||||
if parts and one["종류"] == "로직":
|
||||
one["이음"] = hourly_text(link)
|
||||
one["이음"] = combo_link(one, parts) or hourly_text(link)
|
||||
self.line(one, node, True, src)
|
||||
bare = {k: v for k, v in node.items() if k != "번지"}
|
||||
for at, glyph in node.get(_MERGED) or []: # 묶은 다른 두 줄 원문 = 같은 자리
|
||||
o.glyph(at, glyph, one["이름"], self.rows.get(at, {}).get("C"))
|
||||
bare = {k: v for k, v in node.items() if k not in ("번지", _MERGED)}
|
||||
for p in parts[1:]:
|
||||
self.line(part_line(one, p, o.names), bare, True, src)
|
||||
seen = (res.get("이름"), res.get("규격"))
|
||||
@@ -552,11 +511,84 @@ def _ratios(meta: dict) -> str:
|
||||
return " · ".join(f"{n} {v}" for n, v in pairs if v is not None and abs(_num(v) - 1) > 1e-9)
|
||||
|
||||
|
||||
_MERGED = "_묶음줄" # 세 줄을 묶은 줄 나무 — 나머지 두 줄 [(행, 글)]
|
||||
_BAD = ("셈 못 함", "엔진 꼴 없음")
|
||||
|
||||
|
||||
def triples(nodes: list[dict]) -> list[list[int]]:
|
||||
"""814-1 — 같은 기계(X 코드)의 노 · 재 · 경 세 참조 줄(같은 수량 식 · 번지 · 빼기 · 옮김 없음) 자리
|
||||
[[i, j, k], …] — 사이 빈 줄은 건너뜀."""
|
||||
out, k = [], 0
|
||||
while k < len(nodes):
|
||||
at, j = [k], k + 1
|
||||
while len(at) < 3 and j < len(nodes):
|
||||
if nodes[j].get("종류") == "빈" and not str(nodes[j].get("글") or "").strip():
|
||||
j += 1
|
||||
continue
|
||||
at.append(j)
|
||||
j += 1
|
||||
if len(at) == 3 and _same_machine([nodes[i] for i in at]):
|
||||
out.append(at)
|
||||
k = j
|
||||
else:
|
||||
k += 1
|
||||
return out
|
||||
|
||||
|
||||
def merge(nodes: list[dict], groups: list[list[int]]) -> list[dict]:
|
||||
"""세 줄 자리 → 비목 없는 한 줄(값묶음 · 조합이면 그 뒤 기계마다 줄) · 나머지 두 줄 원문은 같은 자리."""
|
||||
first = {g[0]: g for g in groups}
|
||||
drop = {i for g in groups for i in g[1:]}
|
||||
out = []
|
||||
for i, node in enumerate(nodes):
|
||||
if i in first:
|
||||
res = {**node["자원"], "비목": None, "옮김": None}
|
||||
rest = [(nodes[j].get("행"), nodes[j].get("글") or "") for j in first[i][1:]]
|
||||
out.append({**node, "자원": res, _MERGED: rest})
|
||||
elif i not in drop:
|
||||
out.append(node)
|
||||
return out
|
||||
|
||||
|
||||
def _same_machine(three: list[dict]) -> bool:
|
||||
if any(x.get("종류") != "참조" or x.get("오류") or not x.get("자원") for x in three):
|
||||
return False
|
||||
res = [x["자원"] for x in three]
|
||||
if not str(res[0].get("코드") or "").startswith("X") or len({r["코드"] for r in res}) != 1:
|
||||
return False
|
||||
if sorted(str(r.get("비목")) for r in res) != ["L", "M", "S"] or any(
|
||||
r.get("옮김") for r in res
|
||||
):
|
||||
return False
|
||||
return all(len({repr(x.get(k)) for x in three}) == 1 for k in ("식", "번지", "빼기"))
|
||||
|
||||
|
||||
def stmate(
|
||||
block: dict, nodes: list[dict], prices: dict, rule: str, keys: bool = False, file: str = ""
|
||||
) -> dict:
|
||||
"""STmate 블록(`M02_San_Book.stmate_blocks` 한 개) + 줄 나무 + 같은 책 단가 → {로직, 원본}.
|
||||
rule = 블록 끝전(비목버림 | 합버림_큰나머지 · 파일 설정) · keys = 자원 M01 키(이음) 찾기."""
|
||||
rule = 블록 끝전(비목버림 | 합버림_큰나머지 · 파일 설정) · keys = 자원 M01 키(이음) 찾기 ·
|
||||
기계 세 줄 = 한 줄(`triples` · 814-1) — 묶어서 셈 못 하는 줄이 생기면 묶지 않은 꼴."""
|
||||
groups = triples(nodes)
|
||||
out = _stmate(block, merge(nodes, groups), prices, rule, keys, file)
|
||||
bad = _bad(out)
|
||||
if not groups or not bad:
|
||||
return out
|
||||
took: list[
|
||||
list[int]
|
||||
] = [] # 묶어서 셈 못 하는 세 줄만 그대로(율 범위 C · 누계 % 가 비목 하나를 부름)
|
||||
base = _bad(_stmate(block, nodes, prices, rule, keys, file))
|
||||
for g in groups:
|
||||
if _bad(_stmate(block, merge(nodes, [*took, g]), prices, rule, keys, file)) <= base:
|
||||
took.append(g)
|
||||
return _stmate(block, merge(nodes, took), prices, rule, keys, file)
|
||||
|
||||
|
||||
def _bad(out: dict) -> int:
|
||||
return sum(1 for m in out["원본"]["표지"] if any(b in m for b in _BAD))
|
||||
|
||||
|
||||
def _stmate(block: dict, nodes: list[dict], prices: dict, rule: str, keys: bool, file: str) -> dict:
|
||||
me = prices.get(block["코드"]) or {}
|
||||
meta = me.get("meta") or {}
|
||||
o = Out(block.get("이름"), me.get("단위"), "STmate", block["코드"])
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
"""경쟁사 산근 줄 → M01 요소 이음(PLAN 814-1 · `M02_San_Emit` 에서 뗌 — 셈 꼴 글 · 표지 그대로).
|
||||
|
||||
`m01` = 1차 줄 판정 유일 짝(인력 · 재료 · 기계 · 유가 · 환율) · `hourly_text` = 기계 시간당 경비 부름 글 ·
|
||||
`combo_link` = 조합 기계(굴착기 + 브레이커 · 집게) 우리 조합 식 부름 글.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
NOTE = "참고 · " # `M02_San_Emit.NOTE` 와 같음(사람 확인 표지 머리)
|
||||
OIL_BASE = ("전국평균", "일반용(갑)Ⅰ 저압") # 유가 · 전력 기본(요구 748 · 우리 기초 기본값)
|
||||
BASE_REGION = "서울" # 같은 이름 · 규격 자재가 지역마다 있을 때 기본(우리 기초 검색어 꼴)
|
||||
_REGION = re.compile(r"\s*,\s*([가-힣]{2,4})\s*$") # 자재 이름 지역 꼬리 「, 서울」 · 「,서울」
|
||||
|
||||
|
||||
def m01(kind: str, src: dict) -> tuple[str | None, str | None]:
|
||||
"""1차 줄 판정 유일 짝 M01 키 → (키, 표지) — 없음 = (None, None) · 여럿 = (None, 표지 · 유가 ·
|
||||
환율만 — 재료 · 인력은 조용히 이음 없음) · 기계 = 기계 찾기(812-9) 못 이으면 늘 표지. 재료 줄 이름이 M01 유가전력(MO)이면
|
||||
유가 · 화폐 이름 그대로(달러 · USD …)면 환율(FX)로 판정(797-1 · 「유로폼」 같은 글 속 낱말은 아님)."""
|
||||
if kind not in ("인력", "재료", "기계"):
|
||||
return None, None
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Row as hrow
|
||||
|
||||
line = {k: src.get(k) or "" for k in ("이름", "규격", "단위")}
|
||||
if (
|
||||
kind == "기계"
|
||||
): # 812-9 — 하나 = 이음 · 아니면 표지(후보 여럿 · 못 찾음 · 조용히 넘기지 않음)
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
got = mfind.find(line["이름"], line["규격"])
|
||||
# 찾기 판정 없는 기계 줄 막음(814-1 · 줄 쌓기 표지 · 별칭)
|
||||
src.setdefault("찾기", {**got, "이름": got.get("이름") or line["이름"], "규격": got.get("규격") or line["규격"]}) # fmt: skip
|
||||
return got["키"], mfind.mark(got, line["이름"], line["규격"])
|
||||
if kind == "재료":
|
||||
if elem.oil_rows(line["이름"]):
|
||||
kind = "유가"
|
||||
elif line["이름"].strip().upper() in {x.upper() for pair in hrow.CURRENCY for x in pair}:
|
||||
kind = "환율"
|
||||
if kind == "유가": # MO 전부 → 줄 규격이 상세구분(전국 · 지역)이면 그것만
|
||||
got = [
|
||||
{"키": r["키"], "규격": str(r.get("상세구분") or "")}
|
||||
for r in elem.oil_rows(line["이름"])
|
||||
]
|
||||
spec = line["규격"].replace(" ", "")
|
||||
got = (
|
||||
[x for x in got if x["규격"] == spec]
|
||||
or [x for x in got if spec and spec in x["규격"]]
|
||||
or got
|
||||
)
|
||||
else:
|
||||
got = hrow.m01_picks(kind, line)
|
||||
keys = {x.get("키") for x in got if x.get("키")}
|
||||
if (
|
||||
len(keys) > 1 and kind == "유가"
|
||||
): # 814-1 — 전국평균 · 전력 = 일반용(갑)Ⅰ 저압(지역 · 계약종별은 옵션)
|
||||
keys = {x["키"] for x in got if x["규격"] in OIL_BASE} or keys
|
||||
elif len(keys) > 1 and kind == "재료": # 814-1 — 지역 꼬리 뗀 이름 · 규격 정확 일치
|
||||
keys = exact(line, got) or keys
|
||||
if len(keys) == 1:
|
||||
return keys.pop(), None
|
||||
if len(keys) > 1 and kind in ("유가", "환율"):
|
||||
return None, f"{NOTE}{kind} 후보 여럿({len(keys)}) · 사람 확인"
|
||||
return None, None
|
||||
|
||||
|
||||
def _flat(text: object) -> str:
|
||||
return re.sub(r"\s+", "", unicodedata.normalize("NFKC", str(text or ""))).lower()
|
||||
|
||||
|
||||
def region_of(name: object) -> tuple[str, str]:
|
||||
"""자재 이름 → (지역 꼬리 뗀 이름, 지역 | 빈 글)."""
|
||||
text = str(name or "")
|
||||
m = _REGION.search(text)
|
||||
return (text[: m.start()], m.group(1)) if m else (text, "")
|
||||
|
||||
|
||||
def exact(line: dict, got: list[dict]) -> set[str]:
|
||||
"""재료 후보 가운데 지역 꼬리 뗀 이름 · 규격이 줄과 정확히 같은 것(빈칸 · 전각 · 대소문자 정규화) —
|
||||
지역마다 여럿이면 줄 지역 › 서울 › 같은 단위 › 키 차례 첫째 하나 · 없음 = 빈 모음(사람 확인)."""
|
||||
name, mine = region_of(line.get("이름"))
|
||||
want = (_flat(name), _flat(line.get("규격")))
|
||||
hits = [x for x in got if x.get("키") and (_flat(region_of(x["이름"])[0]), _flat(x.get("규격"))) == want] # fmt: skip
|
||||
if not hits:
|
||||
return set()
|
||||
unit = _flat(line.get("단위"))
|
||||
|
||||
def rank(x: dict) -> tuple:
|
||||
where = region_of(x["이름"])[1]
|
||||
return (where != mine if mine else True, where != BASE_REGION, _flat(x.get("단위")) != unit, str(x["키"])) # fmt: skip
|
||||
|
||||
return {min(hits, key=rank)["키"]}
|
||||
|
||||
|
||||
def hourly_text(key: str | None) -> str | None:
|
||||
"""기계 EQ → 시간당 기계경비 로직 부름 글(797-1 짝 고름) · 키 · 자료 없음 = None."""
|
||||
if not key:
|
||||
return None
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
|
||||
got = dim.hourly_link(key, "노무비")
|
||||
if not got:
|
||||
return None
|
||||
return f"로직({got['키']}{''.join(f', {k}={v}' for k, v in got['입력'].items())})"
|
||||
|
||||
|
||||
def combo_link(one: dict, parts: list[dict]) -> str | None:
|
||||
"""조합 기계 둘이 우리 조합 식(GC000270 · 271) 고르기에 있으면 그 부름 글 · 줄 자원에 `조합식` 키."""
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as mh
|
||||
|
||||
got = mh.combo(parts[0]["키"], parts[1]["키"]) if len(parts) == 2 and all(p["키"] for p in parts) else None # fmt: skip
|
||||
if got:
|
||||
one["자원"]["조합식"] = got[0]
|
||||
return got[1] if got else None
|
||||
|
||||
|
||||
# ── 못 이은 줄 까닭(814-1 · 요구 745 C2) ─────────────────────────────────
|
||||
FREIGHT = ("운임", "화물", "운반비", "하차")
|
||||
|
||||
|
||||
def why(item: dict) -> str:
|
||||
"""이음 없는 가져온 줄 → 까닭 한 마디(문서 설명 · 띠 · 원문 카드) — 「없음」은 흠이 아니라 우리 정보로
|
||||
못 이은 까닭(자재 없음 · 운임표 없음 · 기계 표에 없음 · 별칭 대기 · 후보 여럿 · 시간당 경비 자료 없음 · 갈래 밖)."""
|
||||
res = item.get("자원") or {}
|
||||
kind = res.get("갈래") or item.get("종류")
|
||||
name = str(res.get("이름") or item.get("이름") or "")
|
||||
if res.get("조합식"):
|
||||
return f"조합 — 우리 조합 식 {res['조합식']} 에 듦"
|
||||
if kind == "기계":
|
||||
found = res.get("찾기") or {}
|
||||
verdict = found.get("판정")
|
||||
if verdict == "못 찾음":
|
||||
return (
|
||||
"별칭 대기"
|
||||
if str(found.get("까닭") or "").startswith("기종 모름")
|
||||
else "기계 표에 없음"
|
||||
)
|
||||
if verdict == "후보 여럿":
|
||||
return "기계 후보 여럿 — 고름"
|
||||
return "시간당 경비 자료 없음" if found.get("키") else "기계 찾기 없음"
|
||||
if kind == "재료":
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Row as hrow
|
||||
|
||||
if elem.oil_rows(name):
|
||||
return "유가 후보 여럿 — 고름"
|
||||
line = {"이름": name, "규격": res.get("규격") or "", "단위": res.get("단위") or ""}
|
||||
if hrow.m01_picks("재료", line):
|
||||
return "자재 후보 여럿 — 고름"
|
||||
return "운임표 없음" if any(w in name for w in FREIGHT) else "자재 없음(우리 자재표)"
|
||||
if kind == "인력":
|
||||
return "노임 없음"
|
||||
if kind == "단가산출":
|
||||
return "안쪽 단가산출 못 이음"
|
||||
return "갈래 밖(견적 · 고정 값)"
|
||||
|
||||
|
||||
def tally(logic: dict) -> dict:
|
||||
"""우리 정보로 셈 — 자원 줄 {됨, 줄, 조합(형제 단가산출 부름뿐)} (연결 판정 「없음」 띠 · 814-1)."""
|
||||
items = logic.get("호표") or []
|
||||
done = sum(
|
||||
1 for x in items if x.get("이음") or x.get("참조") or (x.get("자원") or {}).get("조합식")
|
||||
)
|
||||
inner = [x for x in items if (x.get("자원") or {}).get("갈래") == "단가산출" or x.get("참조")]
|
||||
return {"됨": done, "줄": len(items), "조합": bool(items) and len(inner) == len(items)}
|
||||
@@ -8,9 +8,9 @@
|
||||
같은 내용 SK 가 있으면 그것(다시씀).
|
||||
블록 번 = 판정 블록 번(STmate = 코드 「D00102」 숫자 · EST = 단가산출 블록 번).
|
||||
STmate 블록 끝전 = 파일 투표(`M02_San_Xlsx_Stmate.vote`).
|
||||
원파일(.estx · .stc · 810-2 `M02_Doc_FromNative`) = 같은 자리 칸 `원파일`(둘 꼴) — 있으면 원파일 우선
|
||||
(그 블록 번이 원파일에 있을 때) · 엑셀 올림 때 같은 공사 원파일이 다른 HP 에 있으면 붙임 · 원파일 올림 =
|
||||
같은 공사 엑셀 HP 에 붙임(없으면 원파일로 새 HP · `upload_native`).
|
||||
원파일(.estx · .stc · 810-2 `M02_Doc_FromNative`) = 같은 자리 칸 `원파일`(둘 꼴) · 올리기 = 파일 하나
|
||||
(814-1 · 요구 742-1 — 같은 공사 엑셀 · 원파일 붙이기 걷음) · 원파일 = 새 HP(단가산출표 판 격자 · 일위대가 없음 ·
|
||||
`upload_native`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -63,9 +63,7 @@ def keep(folder: str | Path, key: str, name: str, data: bytes) -> str | None:
|
||||
return f"산근 시트 읽기 오류 — {type(e).__name__}: {str(e)[:80]}"
|
||||
path = _path(folder, key)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
got = {"파일": name, "시트": sheets, "엑셀": excel}
|
||||
native = next((n for n in _natives(folder) if fn.same(n, sheets)), None)
|
||||
atomic_write_json(path, got | ({"원파일": native} if native else {}))
|
||||
atomic_write_json(path, {"파일": name, "시트": sheets, "엑셀": excel})
|
||||
return None
|
||||
|
||||
|
||||
@@ -85,23 +83,8 @@ def _excel_of(path: Path, saved: dict) -> dict:
|
||||
|
||||
|
||||
# ── 원파일(810-2) ──────────────────────────────────────────────────────
|
||||
def _saved(folder: str | Path) -> list[tuple[Path, dict]]:
|
||||
out = []
|
||||
for path in sorted((hopyo_store._dir(folder) / DIRNAME).glob("HP-*.json")):
|
||||
try:
|
||||
out.append((path, json.loads(path.read_bytes())))
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def _natives(folder: str | Path) -> list[dict]:
|
||||
return [d["원파일"] for _, d in _saved(folder) if isinstance(d.get("원파일"), dict)]
|
||||
|
||||
|
||||
def upload_native(folder: str | Path, name: str, data: bytes, who: Any) -> dict[str, Any]:
|
||||
"""원파일 올림 — 같은 공사 엑셀 HP 에 원파일을 붙임(`붙음` 키들 · 첫 HP 답) · 없으면 원파일로
|
||||
새 HP(EST = 일위대가 호표 · STmate = 호표 없음 · 산근만). 같은 파일 다시 = 그 HP(`이어서`)."""
|
||||
"""원파일 올림 — 새 HP(단가산출표 판 격자 · 산근만 · 일위대가 없음 · 814-1) · 같은 파일 다시 = 그 HP(`이어서`)."""
|
||||
mark = hopyo_store.fingerprint(data)
|
||||
if same := next((r for r in hopyo_store.rows(folder) if r.get("지문") == mark), None):
|
||||
return {**same, "이어서": True, "원파일": True}
|
||||
@@ -111,18 +94,13 @@ def upload_native(folder: str | Path, name: str, data: bytes, who: Any) -> dict[
|
||||
native = fn.read(name, data)
|
||||
except fn.NativeError as e:
|
||||
raise store.StoreError(400, str(e)) from e
|
||||
joined = []
|
||||
for path, got in _saved(folder):
|
||||
if got.get("시트") and fn.same(native, got["시트"]):
|
||||
atomic_write_json(path, {**got, "원파일": native})
|
||||
joined.append(path.stem)
|
||||
rows = {r["키"]: r for r in hopyo_store.rows(folder)}
|
||||
if joined and joined[0] in rows:
|
||||
return {**rows[joined[0]], "원파일": True, "붙음": joined}
|
||||
row = hopyo_store.save(folder, name, fn.hopyo(native), who, None, mark)
|
||||
path = _path(folder, row["키"])
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
atomic_write_json(path, {"파일": name, "원파일": native})
|
||||
|
||||
def first(key: str) -> None: # HP 를 목록에 보이기 전에 산근 보관(814-1)
|
||||
path = _path(folder, key)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
atomic_write_json(path, {"파일": name, "원파일": native})
|
||||
|
||||
row = hopyo_store.save(folder, name, fn.hopyo(native), who, fn.grid(native), mark, first)
|
||||
return {**row, "원파일": True}
|
||||
|
||||
|
||||
|
||||
@@ -304,11 +304,13 @@ def same_part(doc: Any) -> dict | None:
|
||||
|
||||
|
||||
def unlinked(doc: dict) -> list[dict]:
|
||||
"""이음 없는 자원 줄(표지 · 사람이 고름) `[{자리, 이름}]`."""
|
||||
"""이음 없는 자원 줄(표지 · 사람이 고름) `[{자리, 이름, 까닭}]`(까닭 = 814-1 `M02_San_Link.why`)."""
|
||||
from M02_MasterTemplete import M02_San_Link as link
|
||||
|
||||
row = row_of(doc) or {}
|
||||
ids = sd.line_ids(row)
|
||||
return [
|
||||
{"자리": ids[n], "이름": one.get("이름") or ""}
|
||||
{"자리": ids[n], "이름": one.get("이름") or "", "까닭": link.why(one)}
|
||||
for n, one in enumerate(doc[KEY]["로직"].get("호표") or [])
|
||||
if not one.get("이음") and not one.get("참조")
|
||||
]
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface MatchFound {
|
||||
금액?: { 가져온?: Record<string, number | null>; 우리?: Record<string, number | null> };
|
||||
후보수?: number;
|
||||
가까움?: { 키?: string; 이름?: string; 다른?: { 항: string; 글: string }[] };
|
||||
/** 우리 정보로 셈(814-1) — 자원 줄 이음 수 · 형제 단가산출 부름뿐 = 조합 */
|
||||
우리정보?: { 됨?: number; 줄?: number; 조합?: boolean };
|
||||
}
|
||||
|
||||
export type MatchAction = "찾기" | "바꾸기" | "되돌리기" | null;
|
||||
@@ -114,9 +116,13 @@ export function matchBand(sk: SkLike | null | undefined): MatchBand | null {
|
||||
};
|
||||
}
|
||||
const near = f.가까움;
|
||||
const mine = f.우리정보; // 「없음」 = 흠 아님(814-1 · 요구 745) — 우리 정보로 다시 만든 정도
|
||||
const made = mine?.줄 ? ` — 우리 정보로 셈 ${mine.됨 ?? 0}/${mine.줄} 줄` : "";
|
||||
return {
|
||||
tone: "info",
|
||||
text: `없음 — 우리 기초 후보 ${f.후보수 ?? 0} 개와 맞지 않음`,
|
||||
text: mine?.조합
|
||||
? `조합 — 다른 단가산출 ${mine.줄 ?? 0} 개 묶음(안의 단가산출로 셈)`
|
||||
: `우리 기초 없음${made}`,
|
||||
details: near ? [`가장 가까운 ${near.키 ?? ""} ${near.이름 ?? ""}`, ...lines(near.다른)] : [],
|
||||
action: null,
|
||||
};
|
||||
|
||||
@@ -42,6 +42,8 @@ export interface Origin {
|
||||
요약?: string;
|
||||
/** 원파일 확정 「확정 N · 표지 N · 추정 N · 확정률 N %」 */
|
||||
확정?: string;
|
||||
/** 확정 칸 이름 — 원파일 = 「원파일 확정」 · 엑셀 = 「엑셀 읽기 확정」(814-1) */
|
||||
확정이름?: string;
|
||||
}
|
||||
|
||||
const BOOKS: [RegExp, string][] = [
|
||||
@@ -111,6 +113,7 @@ export function originOf(meta: OriginMeta | null | undefined): Origin | null {
|
||||
...(sure
|
||||
? {
|
||||
확정: `확정 ${sure.확정 ?? 0} · 표지 ${sure.표지 ?? 0} · 추정 ${sure.추정 ?? 0} · 확정률 ${(Math.floor(rate * 1000) / 10).toFixed(1)} %`,
|
||||
확정이름: src.원파일 ? "원파일 확정" : "엑셀 읽기 확정",
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,79 @@
|
||||
# 814-1 불러오기 결정 반영 — 전후 전수 보고(요구 742 ~ 750 · 방향 장부 E 표 1 ~ 10)
|
||||
|
||||
- 대상 = 812-12 와 같은 자 — 원파일 EST 6 · STC 7(산근 2,220 블록) + 실무 엑셀 11(1,021 블록)
|
||||
- 전 = 812-12 보고 수 + 같은 자로 다시 잰 수(코드 바꾸기 전) · 후 = 814-1 반영 판(커밋 56a3e05 · 8914b21 · bed35e3)
|
||||
- 자세한 표 = `_목록_814-1_정합성.xlsx`(요약 전 · 후 · 연결 · 못 이은 줄 · 0 원 줄)
|
||||
- 화면에서 만든 SK 둘 · 올린 파일 셋 모두 지움 · 마스터 별칭 · 별칭 장부 · storage 층 사본 안 건드림
|
||||
|
||||
## 요약
|
||||
|
||||
| 잣대 | 전 | 후 | 판정 |
|
||||
|---|---|---|---|
|
||||
| 금액(엑셀 단가 그대로 셈 = 원본) | 원파일 2,220 · 엑셀 1,021 = 100 % | 원파일 2,220 · 엑셀 1,021 = 100 % | 그대로 |
|
||||
| 못 읽음 표지 | 0 | 0 | 그대로 |
|
||||
| 인력 · 형제 단가산출 이음 | 100 % · 100 % | 100 % · 100 % | 그대로 |
|
||||
| 기계 이음 | 90.8 %(5,699 / 6,279 줄) | **95.7 %**(4,601 / 4,807 줄 — 세 줄 묶은 뒤 줄 수) | 좋아짐 |
|
||||
| 기계 — 찾았는데 시간당 경비 없음 | 349 줄(26 종) | **0** | 풀림 |
|
||||
| 재료 이음 | 72.0 %(1,466 / 2,036) | **87.6 %**(1,784 / 2,036) | 좋아짐 |
|
||||
| 유가 · 전력 이음 | 0 %(0 / 29) | **100 %**(29 / 29) | 풀림 |
|
||||
| [우리 단가로] 블록 전부 우리 값(= 우리 정보로 만들어짐) | 원파일 71.9 %(1,596) · 엑셀 74.6 %(762) | 원파일 **85.5 %**(1,899) · 엑셀 **79.7 %**(814) | 좋아짐 |
|
||||
| [우리 단가로] 0 원 줄 있는 블록 | 원파일 236 · 엑셀 47 | 원파일 115 · 엑셀 4 | 줄어듦 |
|
||||
| STmate 기계 비목 세 줄(문서 줄 쌓기) | STC 285 블록 · 1,391 줄 | 남음 STC 2 블록 · 엑셀 6 블록(번지가 셋으로 갈린 꼴) | 풀림 |
|
||||
| 조합 기계 항목 겹침(같은 기계 항목이 여럿) | 83 블록 | **0** | 풀림 |
|
||||
| 차림 왕복 · TS 거울 | 100 % | 100 %(812-3 시험 · 7 통과) | 그대로 |
|
||||
| 1,513 M01 문서 금액 | — | 기본 1,513 같음 · 옵션 표본 18,797 중 다름 1(채택된 차이 그대로) | 그대로 |
|
||||
| 연결 판정(보조) 원파일 확정 · 근접 · 없음 · 기계 작업 | 339 · 865 · 818 · 198(15.3 %) | **463** · 784 · 761 · 212(20.9 %) · 못 셈 0 · 확정 다시 셈 다름 0 | 좋아짐 |
|
||||
| 연결 판정(보조) 엑셀 확정 · 근접 · 없음 · 기계 작업 | 205 · 260 · 472 · 84(20.1 %) | 211 · 271 · 447 · 92(20.7 %) · 못 셈 0 | 조금 |
|
||||
|
||||
## 항목별(E 표)
|
||||
|
||||
| # | 고친 것 | 화면 · 수 |
|
||||
|---|---|---|
|
||||
| 1 | 올리기 = 파일 하나 · 같은 공사 붙이기 걷음 | 엑셀이든 .estx · .STC 든 따로 새 작업 · 「같은 공사 엑셀에 붙음」 글 없음 |
|
||||
| 2 | 확장자 파일 = 단가산출표 판(코드 · 이름 · 규격 · 단위 · 재료비 · 노무비 · 경비 · 합계) + 산출근거 | 봉화 .STC 하나 → 65 블록 목록 · [내 단가산출로] = 산출근거 · 일위대가 안 만듦(EST 도 HeadMainDat 안 씀) |
|
||||
| 3 | 연결 찾기 = 엑셀 이름 규격 · 단위 꼬리 뗀 이름(「구조물터파기(인력) 경질토사 / M3」 → 「구조물터파기(인력)」) | 보이는 이름 그대로 · STmate 블록 규격도 근거 칸에 |
|
||||
| 4 | 기계 세 줄 = 기계 한 줄 · 조합 다음 줄 기계마다 한 항목 | 봉화 55 크롤러드릴 「노 무 비 · 재 료 비 · 경 비」 → 「크롤러드릴(탑승유압식) 110㎾」 한 줄 · main 360 덤프 자동덮개 시설 항목 셋 → 하나 · 금액 그대로 |
|
||||
| 5 | 시간당 경비 계산식 기계 목록에 빠진 기계 더함 | 384 대 가운데 351 대 더함(기계경비 92 · 조종원 직종 입력 18 · 조종원 없음 43 · 연료 없음 7 · 손료만 198) · 못 넣음 33 대(기계표 가격 · 계수 · 연료 종류 빔 · 잡재료율 「13∼18」 범위) · 조합(굴착기 + 대형 브레이커 · 부착용 집게) = 우리 조합 식 GC000271 · 270 한 줄 — 거창 2 [우리 단가로] 23,747 → 25,772(브레이커 0 원 풀림) |
|
||||
| 6 | 공구손료 율 비목 차이(재료비 ↔ 경비) = 참고 | 거창 41 합판거푸집 = 근접 → 확정(참고 「공구손료 비목」) |
|
||||
| 7 | 못 이은 줄 까닭 · 「없음」 띠 | 이음 없는 자원 띠 = 「대형 브레이커 (조합 — 우리 조합 식 GC000271 에 듦)」 꼴 · 문서 줄 설명 = 「이음 없음 — 자재 없음 · 엑셀 단가 그대로」 꼴 · 연결 「없음」 = 「우리 기초 없음 — 우리 정보로 셈 n/m 줄」 · 형제만 = 「조합」 |
|
||||
| 8 | 유가 = 전국평균 + 지역 옵션 · 전력 = 일반용(갑)Ⅰ 저압 + 계약종별 옵션 | 경유 · 등유 → `MO:경유:{S_지역}`(기본 전국평균) · 전력 → 114.4 원(엑셀 114.8 = 검산 칸) |
|
||||
| 9 | 재료 = 지역 꼬리 뗀 이름 · 규격 정확 일치 | 유리알 1호 고휘도 · 합판(내수) 12t(서울) · 외송각재 · 철못 N 25 · 구역화물 8.0톤 30km 이음 |
|
||||
| 10 | 가벼움 여섯 | 조합 첫 줄 칩 = 「굴삭기(무한궤도) 0.7」 · 북부청임도 33 소계 검산 맞음 · 기계 찾기 판정 없는 줄 7 → 0 · 올린 직후 409 = HP 보이기 전 산근 보관 · 엑셀 블록 「엑셀 읽기 확정」 · 전수 도우미 str() 뗌 |
|
||||
|
||||
## 남은 못 이음 — 까닭별(후)
|
||||
|
||||
| 갈래 | 까닭 | 줄 |
|
||||
|---|---|---|
|
||||
| 기계 | 후보 여럿(사람 고름 · C3 협의 중) | 88 |
|
||||
| 기계 | 못 찾음 — 별칭 대기 | 65 |
|
||||
| 기계 | 우리 기계표에 없음 | 53 |
|
||||
| 재료 | 후보 여럿(정확 일치 없음 — 접착제 · 유로폼 패널 · 각재 외송 …) | 238 |
|
||||
| 재료 | 우리 자재표에 없음 | 14 |
|
||||
| 갈래 밖 | 견적 · 운임 · 수수료(구역화물자동차 · 도선료 · 치즐 견적 …) | 59 |
|
||||
|
||||
| [우리 단가로] 0 원 줄(후) | 줄 | 뜻 |
|
||||
|---|---|---|
|
||||
| 조합 다음 줄 이음 · 덤프트럭 자동덮개시설(노무비 · 재료비 칸) | 82 | 부착 장비 = 손료(경비)만 — 노무 · 재료 칸 0 이 맞음 |
|
||||
| 조합 다음 줄 못 이음(굴착기 후보 여럿 · 별칭) | 46 | 사람 고름 대기 |
|
||||
| 재료 이음 · 우리 값 0(급수호스 · 시험 수수료 · 공기주입기 · 시스템동바리 …) | 102 | 우리 자재표 값 없음 |
|
||||
| 기계 낱 줄(발전기 노무비 칸 등) | 11 | 그 기계에 그 비목 없음 |
|
||||
|
||||
- 연결 「확정」 참고(원파일) — 수량 269 · 공구손료 비목 154(6 번) · 단가 차이 97 · 옵션 확인 94 · 수량 끝자리 72 · 품 숫자 65
|
||||
- 「없음」 = 흠 아님(요구 745) — 띠 글 「우리 기초 없음 — 우리 정보로 셈 n/m 줄」 · 근접 까닭 1위 = 줄 없음 1,361 · 기계 규격 475(후보 고르기 C3 협의 몫)
|
||||
|
||||
## 남은 흠
|
||||
|
||||
| # | 흠 | 예 | 갈래 |
|
||||
|---|---|---|---|
|
||||
| 1 | EST 「〇〇 노무비 · 재료비 · 경비」 가운데 수량 식이 다른 세 줄은 못 묶음 | main 360 덤프 재료비 × To | 원파일 로직 그대로(값 묶음으로 못 접음) |
|
||||
| 2 | STmate 세 줄이 번지 셋(A01 · A02 · A03)으로 갈린 꼴은 안 묶음 | 울진대흥 6 크로울러드릴 | 묶으면 번지 율 식이 깨짐 · 8 블록 |
|
||||
| 3 | 재료 지역 옵션 화면 칸 없음 — 서울 줄 기본으로 이음 | 합판(내수) 서울 | 자재표 = 키마다 지역(우리 기초도 서울 고정 검색어) |
|
||||
| 4 | 원파일 단가산출 목록 줄 아래 글 「일위대가 밖」 | 봉화 .STC 목록 | 일위대가를 안 만드니 늘 그 글 |
|
||||
| 5 | 연결 판정 띠(「우리 기초 없음 — 우리 정보로 셈」)는 [연결 찾기] 다시 누른 SK 부터 | 옛 SK | 기록된 판정에 칸 없음 |
|
||||
|
||||
## 화면(8005 · 5178 · 일반 계정 개인 층 · 「sub3임시」)
|
||||
|
||||
- 봉화 .STC 하나만 올림 → 단가산출표 판 65 블록 · 55 암발파 = 크롤러드릴 한 줄 · 검산 같음 · [우리 단가로] 16,629 → 16,020
|
||||
- 거창 .xlsx 하나만 올림 → 2 측구터파기(연암) = 가 · 굴착기 / 나 · 대형 브레이커 / 다 · 치즐 항목 · 브레이커 띠 「조합 — 우리 조합 식 GC000271 에 듦」 · [우리 단가로] 30,381 → 25,772 · 「같은 공사」 글 없음
|
||||
- 밝음 · 어두움 · 1366×768 가로 넘침 0 · 콘솔 오류 0(로그인 전 세션 401 하나 뺌) · 사진 = tmp(셋)
|
||||
- 만든 SK 둘 · 올린 HP 셋 지움
|
||||
@@ -0,0 +1,161 @@
|
||||
"""시간당 경비 계산식(GC000267 ~ GC000276) 「기계」 고르기 · 선택 목록에 원문에 있는데 빠진 기계 더함(PLAN 814-1 · 요구 744 ~ 747).
|
||||
|
||||
기계표(EQ) 가운데 어느 계산식 목록에도 없는 기계를 자료(기계표 값)에 따라 식 하나에 배정 — 지어 넣는 값 없음:
|
||||
연료(주연료 · 종류) · 조종원(인 · 직종) · 잡재료율 · 상각 · 정비 · 관리 계수 있음 = GC000268(기계경비)
|
||||
연료 · 조종원(인) 있음 · 직종 없음 = GC000275(조종원직종 입력)
|
||||
연료 있음 · 조종원 없음 = GC000272(조종원 없는 기종)
|
||||
연료 없음 · 조종원(인 · 직종) 있음 = GC000276(연료 없는 기종)
|
||||
연료 · 조종원(직종) 없음 · 가격 · 손료계수 있음 = GC000273(손료만 — 부착 장비 등)
|
||||
그 식이 부르는 기계표 값이 비거나 수 하나가 아니면(「13∼18」) 안 넣고 「못 넣음」 목록에 적음.
|
||||
8-1-7 손료 보정 식(GC000269 · GC000274)은 보정 기종 식에 그 기계 번호가 이미 있을 때만 같이 더함.
|
||||
목록 끝에 덧붙임(기본값 그대로 = 기존 문서 금액 같음). 로직 파일(`로직_…_건설기계.json` 입력 고르기)과
|
||||
수식 문서(`수식문서/수식_…_건설기계.json` 「기계」 선택 목록)를 같이 고침. 두 번 돌려도 같음.
|
||||
|
||||
쓰기: venv python resources/master_data/scripts/build_기계목록_빠진기계_814_1.py [--check]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
DATA = HERE.parent
|
||||
LOGIC = DATA / "로직_건설품셈_08장_건설기계.json"
|
||||
DOC = DATA / "수식문서" / "수식_건설품셈_08장_건설기계.json"
|
||||
FAMILY = [f"GC00027{i}" for i in range(6)] + ["GC000267", "GC000268", "GC000269"]
|
||||
FAMILY = sorted(set(FAMILY) | {"GC000276"})
|
||||
FIX = ("상각비계수", "정비비계수", "관리비계수")
|
||||
NEEDS = {
|
||||
"GC000268": ("가격", *FIX, "주연료종류", "주연료", "조종원직종", "조종원", "잡재료율"),
|
||||
"GC000275": ("가격", *FIX, "주연료종류", "주연료", "조종원", "잡재료율"),
|
||||
"GC000272": ("가격", *FIX, "주연료종류", "주연료", "잡재료율"),
|
||||
"GC000276": ("가격", *FIX, "조종원직종", "조종원"),
|
||||
"GC000273": ("가격", "손료계수"),
|
||||
}
|
||||
PAIR = {"GC000268": "GC000269", "GC000273": "GC000274"} # 8-1-7 보정 식
|
||||
|
||||
|
||||
TEXT = ("주연료종류", "조종원직종") # 글 칸 — 그 밖 = 수 하나(범위 「13∼18」 · 빈 값 = 없음)
|
||||
|
||||
|
||||
def _has(row: dict, field: str) -> bool:
|
||||
v = (row.get("값") or {}).get(field)
|
||||
if v in (None, ""):
|
||||
return False
|
||||
if field in TEXT:
|
||||
return bool(str(v).strip())
|
||||
try:
|
||||
return float(str(v).replace(",", "")) != 0
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def family_of(row: dict) -> tuple[str | None, list[str]]:
|
||||
"""기계표 한 줄 → (식 키 | None, 빈 값 이름들) — 연료 · 조종원 유무로 식 가족을 고름."""
|
||||
fuel, crew, job = _has(row, "주연료"), _has(row, "조종원"), _has(row, "조종원직종")
|
||||
if fuel and crew:
|
||||
key = "GC000268" if job else "GC000275"
|
||||
elif fuel:
|
||||
key = "GC000272"
|
||||
elif crew and job:
|
||||
key = "GC000276"
|
||||
else:
|
||||
key = "GC000273"
|
||||
miss = [f for f in NEEDS[key] if not _has(row, f)]
|
||||
return (None if miss else key), miss
|
||||
|
||||
|
||||
def _rows() -> list[dict]:
|
||||
data = json.loads((DATA / "기계.json").read_text("utf-8"))
|
||||
return [
|
||||
r for r in data.get("줄") or data.get("표") or [] if str(r.get("키", "")).startswith("EQ")
|
||||
]
|
||||
|
||||
|
||||
def _logic_lists(text: str) -> dict[str, list[str]]:
|
||||
data = json.loads(text)
|
||||
out = {}
|
||||
for row in data["줄"]:
|
||||
for i in row.get("입력") or []:
|
||||
if row.get("키") in FAMILY and i.get("이름") == "기계":
|
||||
out[row["키"]] = [str(c) for c in i.get("고르기") or []]
|
||||
return out
|
||||
|
||||
|
||||
def _span(text: str, key: str, start_pat: str) -> tuple[int, int]:
|
||||
"""그 키 블록 안 첫 `start_pat` 뒤 배열 [ … ] 의 (여는 괄호, 닫는 괄호) 자리."""
|
||||
at = text.index(f'"키": "{key}"')
|
||||
m = re.compile(start_pat).search(text, at)
|
||||
if not m:
|
||||
raise SystemExit(f"{key} 기계 목록 자리를 못 찾음")
|
||||
open_ = m.end() - 1
|
||||
close = text.index("]", open_)
|
||||
return open_, close
|
||||
|
||||
|
||||
def _append(text: str, key: str, codes: list[str], start_pat: str) -> str:
|
||||
if not codes:
|
||||
return text
|
||||
open_, close = _span(text, key, start_pat)
|
||||
body = text[open_ + 1 : close]
|
||||
if "\n" in body: # 여러 줄 꼴 — 마지막 줄 들여쓰기 그대로
|
||||
last = body.rstrip().rsplit("\n", 1)[-1]
|
||||
pad = last[: len(last) - len(last.lstrip())]
|
||||
add = "".join(f',\n{pad}"{c}"' for c in codes)
|
||||
cut = open_ + 1 + len(body.rstrip())
|
||||
return text[:cut] + add + text[cut:]
|
||||
add = "".join(f', "{c}"' for c in codes)
|
||||
return text[:close] + add + text[close:]
|
||||
|
||||
|
||||
LOGIC_PAT = r'"이름":\s*"기계",\s*"고르기":\s*\['
|
||||
DOC_PAT = r'"열쇠":\s*"기계"[^\n]*?"목록":\s*\['
|
||||
|
||||
|
||||
def plan() -> tuple[dict[str, list[str]], list[tuple[str, str, str, list[str]]]]:
|
||||
logic = LOGIC.read_text("utf-8")
|
||||
have = _logic_lists(logic)
|
||||
taken = {c for v in have.values() for c in v}
|
||||
fixes = {}
|
||||
for r in json.loads(logic)["줄"]:
|
||||
if r.get("키") in PAIR.values():
|
||||
fixes[r["키"]] = " ".join(str(x.get("식") or "") for x in r.get("중간") or [])
|
||||
add: dict[str, list[str]] = {}
|
||||
skipped = []
|
||||
for row in _rows():
|
||||
code = str(row.get("원문번호") or "")
|
||||
if not code or code in taken:
|
||||
continue
|
||||
key, miss = family_of(row)
|
||||
if key is None:
|
||||
skipped.append((code, str(row.get("이름")), str(row.get("규격") or ""), miss))
|
||||
continue
|
||||
if code in add.get(key, []):
|
||||
continue
|
||||
add.setdefault(key, []).append(code)
|
||||
if PAIR.get(key) and f"'{code}'" in fixes.get(PAIR[key], ""):
|
||||
add.setdefault(PAIR[key], []).append(code)
|
||||
return add, skipped
|
||||
|
||||
|
||||
def main(check: bool = False) -> None:
|
||||
add, skipped = plan()
|
||||
print("더함:", {k: len(v) for k, v in sorted(add.items())}, "· 못 넣음:", len(skipped))
|
||||
for code, name, spec, miss in skipped:
|
||||
print(f" 못 넣음 {code} {name} {spec} — 빈 값 {', '.join(miss)}")
|
||||
if check or not add:
|
||||
return
|
||||
logic, doc = LOGIC.read_text("utf-8"), DOC.read_text("utf-8")
|
||||
for key, codes in sorted(add.items()):
|
||||
logic = _append(logic, key, codes, LOGIC_PAT)
|
||||
doc = _append(doc, key, codes, DOC_PAT)
|
||||
json.loads(logic), json.loads(doc) # 꼴 확인
|
||||
LOGIC.write_text(logic, "utf-8", newline="")
|
||||
DOC.write_text(doc, "utf-8", newline="")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main("--check" in sys.argv)
|
||||
@@ -192,7 +192,99 @@
|
||||
"2502-0075",
|
||||
"5205-0035",
|
||||
"2702-0030",
|
||||
"2702-0060"
|
||||
"2702-0060",
|
||||
"0211-0060",
|
||||
"0211-0080",
|
||||
"0211-0100",
|
||||
"0221-0040",
|
||||
"0221-0070",
|
||||
"0302-0025",
|
||||
"0302-0172",
|
||||
"0302-0229",
|
||||
"0302-0287",
|
||||
"0302-0350",
|
||||
"0302-0500",
|
||||
"0406-0054",
|
||||
"0406-0115",
|
||||
"0406-0161",
|
||||
"0406-0206",
|
||||
"0503-0036",
|
||||
"0602-0200",
|
||||
"0602-0240",
|
||||
"0602-0320",
|
||||
"1106-0015",
|
||||
"1206-0010",
|
||||
"1206-0014",
|
||||
"1209-0001",
|
||||
"1209-0002",
|
||||
"1209-0004",
|
||||
"1209-0006",
|
||||
"1209-0007",
|
||||
"1209-0008",
|
||||
"1209-0013",
|
||||
"1306-0044",
|
||||
"1306-0060",
|
||||
"1406-0025",
|
||||
"1506-0011",
|
||||
"1506-0012",
|
||||
"1506-0015",
|
||||
"1506-0019",
|
||||
"1506-0025",
|
||||
"1506-0030",
|
||||
"1506-0037",
|
||||
"2602-0015",
|
||||
"2602-0025",
|
||||
"2602-0035",
|
||||
"2602-0045",
|
||||
"2702-0040",
|
||||
"3530-0015",
|
||||
"3530-0036",
|
||||
"3601-0204",
|
||||
"3601-0302",
|
||||
"3701-0200",
|
||||
"3805-0120",
|
||||
"4504-0021",
|
||||
"4504-0028",
|
||||
"5405-0150",
|
||||
"5701-0010",
|
||||
"7106-0035",
|
||||
"7505-0125",
|
||||
"7505-0200",
|
||||
"7505-0500",
|
||||
"7505-0700",
|
||||
"9010-0003",
|
||||
"9010-0006",
|
||||
"9010-0010",
|
||||
"9010-0012",
|
||||
"9010-0020",
|
||||
"9010-0022",
|
||||
"9010-0033",
|
||||
"9010-0040",
|
||||
"9010-0044",
|
||||
"9010-0060",
|
||||
"9010-0080",
|
||||
"9020-0010",
|
||||
"9020-0015",
|
||||
"9020-0016",
|
||||
"9020-0022",
|
||||
"9020-0035",
|
||||
"9020-0050",
|
||||
"9020-0072",
|
||||
"9020-0160",
|
||||
"9020-0180",
|
||||
"9020-0200",
|
||||
"9050-0075",
|
||||
"9050-0150",
|
||||
"9050-0450",
|
||||
"9050-0750",
|
||||
"9050-0850",
|
||||
"9090-0800",
|
||||
"9090-1000",
|
||||
"9090-1200",
|
||||
"9090-1300",
|
||||
"9090-1400",
|
||||
"9090-1500",
|
||||
"9090-1600"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -595,7 +687,50 @@
|
||||
"6540-0131",
|
||||
"3302-0038",
|
||||
"4430-0400",
|
||||
"7505-0100"
|
||||
"7505-0100",
|
||||
"6630-0003",
|
||||
"6630-0005",
|
||||
"6630-0007",
|
||||
"6630-0010",
|
||||
"6630-0013",
|
||||
"7811-0025",
|
||||
"7811-0030",
|
||||
"7811-0040",
|
||||
"7811-0045",
|
||||
"7811-0070",
|
||||
"7811-0120",
|
||||
"7812-0005",
|
||||
"7812-0007",
|
||||
"7812-0009",
|
||||
"7812-0018",
|
||||
"7812-0020",
|
||||
"7812-0035",
|
||||
"7812-0070",
|
||||
"7812-0100",
|
||||
"7812-0150",
|
||||
"7812-0200",
|
||||
"9030-0016",
|
||||
"9030-0018",
|
||||
"9030-0025",
|
||||
"9030-0035",
|
||||
"9030-0045",
|
||||
"9030-0050",
|
||||
"9030-0080",
|
||||
"9030-0100",
|
||||
"9030-0240",
|
||||
"9040-0010",
|
||||
"9040-0030",
|
||||
"9040-0050",
|
||||
"9040-0060",
|
||||
"9040-0100",
|
||||
"9040-0120",
|
||||
"9040-0200",
|
||||
"9040-0250",
|
||||
"9040-0300",
|
||||
"9040-0380",
|
||||
"9040-0680",
|
||||
"9070-0015",
|
||||
"9070-0020"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -755,7 +890,205 @@
|
||||
"7614-0300",
|
||||
"0250-0100",
|
||||
"5210-0010",
|
||||
"5210-0019"
|
||||
"5210-0019",
|
||||
"0103-0016",
|
||||
"0103-0019",
|
||||
"0103-0023",
|
||||
"0103-0027",
|
||||
"0103-0032",
|
||||
"0230-0004",
|
||||
"0230-0006",
|
||||
"0230-0007",
|
||||
"0230-0008",
|
||||
"0230-0010",
|
||||
"0240-0007",
|
||||
"0250-0080",
|
||||
"0407-0054",
|
||||
"0407-0092",
|
||||
"0407-0107",
|
||||
"0407-0161",
|
||||
"0407-0206",
|
||||
"0610-0150",
|
||||
"0610-0200",
|
||||
"0610-0240",
|
||||
"2115-0024",
|
||||
"2115-0031",
|
||||
"2115-0036",
|
||||
"2116-0031",
|
||||
"2116-0036",
|
||||
"2117-0022",
|
||||
"2117-0027",
|
||||
"2118-0010",
|
||||
"2210-0145",
|
||||
"3430-0300",
|
||||
"3450-0642",
|
||||
"4115-0100",
|
||||
"4115-0150",
|
||||
"4115-0200",
|
||||
"4115-0300",
|
||||
"4505-0015",
|
||||
"4505-0026",
|
||||
"4611-0075",
|
||||
"5105-0050",
|
||||
"5105-0100",
|
||||
"5105-0150",
|
||||
"5105-0200",
|
||||
"5111-0040",
|
||||
"5111-0050",
|
||||
"5111-0060",
|
||||
"5111-0076",
|
||||
"5111-0091",
|
||||
"5112-0001",
|
||||
"5112-0002",
|
||||
"5112-0003",
|
||||
"5112-0004",
|
||||
"5112-0005",
|
||||
"5113-0001",
|
||||
"5113-0002",
|
||||
"5113-0003",
|
||||
"5113-0004",
|
||||
"5113-0005",
|
||||
"5113-0006",
|
||||
"5113-0007",
|
||||
"5113-0008",
|
||||
"5113-0009",
|
||||
"5113-0010",
|
||||
"5113-0011",
|
||||
"5114-0001",
|
||||
"5114-0002",
|
||||
"5114-0003",
|
||||
"5114-0004",
|
||||
"5114-0005",
|
||||
"5114-0006",
|
||||
"5114-0007",
|
||||
"5114-0008",
|
||||
"5115-0030",
|
||||
"5115-0055",
|
||||
"5115-0075",
|
||||
"5115-0095",
|
||||
"5116-0001",
|
||||
"5116-0002",
|
||||
"5116-0003",
|
||||
"5116-0004",
|
||||
"5116-0005",
|
||||
"5116-0006",
|
||||
"5116-0007",
|
||||
"5116-0008",
|
||||
"5117-0001",
|
||||
"5117-0002",
|
||||
"5117-0003",
|
||||
"5117-0004",
|
||||
"5117-0005",
|
||||
"5117-0006",
|
||||
"5117-0007",
|
||||
"5117-0008",
|
||||
"5118-0001",
|
||||
"5118-0002",
|
||||
"5118-0003",
|
||||
"5118-0004",
|
||||
"5118-0005",
|
||||
"5118-0006",
|
||||
"5118-0007",
|
||||
"5119-0625",
|
||||
"5203-1800",
|
||||
"5203-2200",
|
||||
"5203-2700",
|
||||
"5203-3500",
|
||||
"5203-4500",
|
||||
"5330-0074",
|
||||
"5805-0002",
|
||||
"5805-0003",
|
||||
"5901-0006",
|
||||
"5901-0010",
|
||||
"5901-0016",
|
||||
"6202-0200",
|
||||
"6408-0015",
|
||||
"6408-0020",
|
||||
"6408-0030",
|
||||
"6408-0040",
|
||||
"6408-0050",
|
||||
"6408-0085",
|
||||
"6408-0100",
|
||||
"6515-0090",
|
||||
"6530-0030",
|
||||
"6530-0040",
|
||||
"6530-0045",
|
||||
"6530-0060",
|
||||
"6532-0220",
|
||||
"6550-0130",
|
||||
"7103-0010",
|
||||
"7104-0010",
|
||||
"7202-0008",
|
||||
"7202-0010",
|
||||
"7205-0125",
|
||||
"7206-0020",
|
||||
"7206-0070",
|
||||
"7206-0100",
|
||||
"7430-1100",
|
||||
"7430-1300",
|
||||
"7430-1500",
|
||||
"7430-2300",
|
||||
"7430-2500",
|
||||
"7612-0300",
|
||||
"7612-0400",
|
||||
"7613-0075",
|
||||
"7613-0150",
|
||||
"7613-0300",
|
||||
"7613-0400",
|
||||
"7613-0600",
|
||||
"7613-0900",
|
||||
"7620-0002",
|
||||
"7730-0080",
|
||||
"7730-0100",
|
||||
"7730-0125",
|
||||
"7740-0100",
|
||||
"7770-0004",
|
||||
"7930-0001",
|
||||
"7930-0002",
|
||||
"7930-0003",
|
||||
"7930-0005",
|
||||
"7930-0007",
|
||||
"7930-0010",
|
||||
"7930-0015",
|
||||
"7930-0020",
|
||||
"7930-0025",
|
||||
"7930-0030",
|
||||
"7930-0040",
|
||||
"7930-0050",
|
||||
"7930-0075",
|
||||
"7930-0100",
|
||||
"7935-0180",
|
||||
"7991-0050",
|
||||
"7991-0100",
|
||||
"7991-0500",
|
||||
"7993-0020",
|
||||
"8201-0100",
|
||||
"8202-0100",
|
||||
"8203-0019",
|
||||
"8204-0100",
|
||||
"8205-0100",
|
||||
"9060-0060",
|
||||
"9060-0100",
|
||||
"9060-0200",
|
||||
"9060-0300",
|
||||
"9060-0500",
|
||||
"9060-0600",
|
||||
"9080-0050",
|
||||
"9080-0080",
|
||||
"9080-0100",
|
||||
"9080-0120",
|
||||
"9080-0150",
|
||||
"9080-0200",
|
||||
"9080-0300",
|
||||
"9080-0500",
|
||||
"9080-0700",
|
||||
"9080-1000",
|
||||
"9080-1100",
|
||||
"9080-1400",
|
||||
"9080-1500",
|
||||
"9080-1750",
|
||||
"9080-2000",
|
||||
"9080-3000"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -963,7 +1296,25 @@
|
||||
"7110-0025",
|
||||
"7110-0013",
|
||||
"7120-0746",
|
||||
"7101-0450"
|
||||
"7101-0450",
|
||||
"0260-0355",
|
||||
"2107-0005",
|
||||
"2330-0005",
|
||||
"2330-0007",
|
||||
"2402-0001",
|
||||
"3302-0030",
|
||||
"3302-0047",
|
||||
"3302-0057",
|
||||
"3801-0120",
|
||||
"4304-0060",
|
||||
"4304-0061",
|
||||
"6330-0015",
|
||||
"6330-0022",
|
||||
"6330-0032",
|
||||
"6330-0040",
|
||||
"7205-0475",
|
||||
"7205-0540",
|
||||
"7360-0055"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1019,7 +1370,7 @@
|
||||
"소유": "공용",
|
||||
"비고": "원문 운전경비 표에 주연료가 빈 기종(공기식 크롤러드릴 등 — 동력은 공기압축기) — 손료 + 조종원",
|
||||
"입력": [
|
||||
{ "이름": "기계", "고르기": ["5401-0017"] },
|
||||
{ "이름": "기계", "고르기": ["5401-0017", "4108-0060", "4108-0090", "4108-0120", "4108-0150", "4108-0180", "4108-0210", "5401-0015"] },
|
||||
{
|
||||
"이름": "지역",
|
||||
"고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"],
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"갈래": "기계경비식"
|
||||
},
|
||||
"변수": [
|
||||
{"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["1730-0015", "7204-0055", "0602-0150", "2702-0020", "0101-0007", "0101-0010", "0101-0012", "0101-0019", "0101-0032", "2105-0005", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "4504-0032", "4504-0036", "4504-0041", "4504-0043", "4504-0047", "4504-0052", "4205-0010", "4205-0017", "4205-0020", "4205-0030", "4205-0040", "4205-0045", "2105-0002", "2105-0003", "2105-0010", "2105-0015", "2105-0018", "2502-0025", "0602-0025", "2502-0050", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "7204-0018", "7204-0160", "5205-0210", "5405-0110", "7505-0450", "5205-0255", "7505-0150", "5205-0103", "6701-0147", "6701-0184", "5205-0170", "7505-0350", "0302-0134", "7505-0100", "7505-0050", "5205-0071", "7505-0025", "0502-0036", "1306-0100", "1506-0032", "0121-0004", "1305-0007", "7204-0038", "7204-0065", "0302-0057", "5701-0020", "0302-0095", "3201-0003", "1106-0012", "1406-0015", "1206-0008", "3601-0102", "1306-0025", "0211-0018", "0602-0045", "0602-0060", "0602-0105", "1630-0080", "3901-0300", "7505-0250", "2502-0035", "1306-0120", "1106-0010", "3201-0001", "1406-0008", "3601-0202", "3601-0402", "3611-0142", "2502-0020", "2502-0075", "5205-0035", "2702-0030", "2702-0060"], "값": "1730-0015"}},
|
||||
{"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["1730-0015", "7204-0055", "0602-0150", "2702-0020", "0101-0007", "0101-0010", "0101-0012", "0101-0019", "0101-0032", "2105-0005", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "4504-0032", "4504-0036", "4504-0041", "4504-0043", "4504-0047", "4504-0052", "4205-0010", "4205-0017", "4205-0020", "4205-0030", "4205-0040", "4205-0045", "2105-0002", "2105-0003", "2105-0010", "2105-0015", "2105-0018", "2502-0025", "0602-0025", "2502-0050", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "7204-0018", "7204-0160", "5205-0210", "5405-0110", "7505-0450", "5205-0255", "7505-0150", "5205-0103", "6701-0147", "6701-0184", "5205-0170", "7505-0350", "0302-0134", "7505-0100", "7505-0050", "5205-0071", "7505-0025", "0502-0036", "1306-0100", "1506-0032", "0121-0004", "1305-0007", "7204-0038", "7204-0065", "0302-0057", "5701-0020", "0302-0095", "3201-0003", "1106-0012", "1406-0015", "1206-0008", "3601-0102", "1306-0025", "0211-0018", "0602-0045", "0602-0060", "0602-0105", "1630-0080", "3901-0300", "7505-0250", "2502-0035", "1306-0120", "1106-0010", "3201-0001", "1406-0008", "3601-0202", "3601-0402", "3611-0142", "2502-0020", "2502-0075", "5205-0035", "2702-0030", "2702-0060", "0211-0060", "0211-0080", "0211-0100", "0221-0040", "0221-0070", "0302-0025", "0302-0172", "0302-0229", "0302-0287", "0302-0350", "0302-0500", "0406-0054", "0406-0115", "0406-0161", "0406-0206", "0503-0036", "0602-0200", "0602-0240", "0602-0320", "1106-0015", "1206-0010", "1206-0014", "1209-0001", "1209-0002", "1209-0004", "1209-0006", "1209-0007", "1209-0008", "1209-0013", "1306-0044", "1306-0060", "1406-0025", "1506-0011", "1506-0012", "1506-0015", "1506-0019", "1506-0025", "1506-0030", "1506-0037", "2602-0015", "2602-0025", "2602-0035", "2602-0045", "2702-0040", "3530-0015", "3530-0036", "3601-0204", "3601-0302", "3701-0200", "3805-0120", "4504-0021", "4504-0028", "5405-0150", "5701-0010", "7106-0035", "7505-0125", "7505-0200", "7505-0500", "7505-0700", "9010-0003", "9010-0006", "9010-0010", "9010-0012", "9010-0020", "9010-0022", "9010-0033", "9010-0040", "9010-0044", "9010-0060", "9010-0080", "9020-0010", "9020-0015", "9020-0016", "9020-0022", "9020-0035", "9020-0050", "9020-0072", "9020-0160", "9020-0180", "9020-0200", "9050-0075", "9050-0150", "9050-0450", "9050-0750", "9050-0850", "9090-0800", "9090-1000", "9090-1200", "9090-1300", "9090-1400", "9090-1500", "9090-1600"], "값": "1730-0015"}},
|
||||
{"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}},
|
||||
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "연료", "기호": "S", "아래": "연료", "위": "", "이름": "연료", "단위": "", "출처": "건설품셈 공통 8-4 주①㉰", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료종류"}},
|
||||
@@ -272,7 +272,7 @@
|
||||
"갈래": "기계경비식"
|
||||
},
|
||||
"변수": [
|
||||
{"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["4611-0350", "7812-0015", "4506-0200", "4506-0400", "1730-0015", "1630-0080", "1305-0007", "2106-0002", "2106-0003", "2106-0005", "7210-0485", "7360-0090", "6540-0131", "3302-0038", "4430-0400", "7505-0100"], "값": "4611-0350"}},
|
||||
{"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["4611-0350", "7812-0015", "4506-0200", "4506-0400", "1730-0015", "1630-0080", "1305-0007", "2106-0002", "2106-0003", "2106-0005", "7210-0485", "7360-0090", "6540-0131", "3302-0038", "4430-0400", "7505-0100", "6630-0003", "6630-0005", "6630-0007", "6630-0010", "6630-0013", "7811-0025", "7811-0030", "7811-0040", "7811-0045", "7811-0070", "7811-0120", "7812-0005", "7812-0007", "7812-0009", "7812-0018", "7812-0020", "7812-0035", "7812-0070", "7812-0100", "7812-0150", "7812-0200", "9030-0016", "9030-0018", "9030-0025", "9030-0035", "9030-0045", "9030-0050", "9030-0080", "9030-0100", "9030-0240", "9040-0010", "9040-0030", "9040-0050", "9040-0060", "9040-0100", "9040-0120", "9040-0200", "9040-0250", "9040-0300", "9040-0380", "9040-0680", "9070-0015", "9070-0020"], "값": "4611-0350"}},
|
||||
{"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}},
|
||||
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "연료", "기호": "S", "아래": "연료", "위": "", "이름": "연료", "단위": "", "출처": "건설품셈 공통 8-4 주①㉰", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "주연료종류"}},
|
||||
@@ -306,7 +306,7 @@
|
||||
"결과단위": "원/시간",
|
||||
"출처": "건설품셈 공통 8-1-5 · 8-1-6",
|
||||
"변수": [
|
||||
{"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["7620-0003", "7621-0100", "7995-0050", "5210-0013", "5210-0027", "5220-0015", "7730-0150", "6516-0060", "6516-0120", "6516-0250", "6516-0300", "6105-0190", "6202-0060", "0230-0002", "6410-0080", "6410-0100", "6410-0120", "6410-0150", "6410-0200", "6901-0010", "6530-0090", "6530-0120", "6517-0100", "6517-0150", "6517-0200", "6517-0250", "6517-0300", "6510-0100", "6510-0150", "6510-0200", "6510-0250", "6510-0300", "6518-0100", "6518-0150", "6518-0200", "6518-0250", "6518-0300", "5501-0080", "7750-0016", "7750-0025", "7730-0050", "5401-0017", "5202-0127", "5202-0240", "5202-0300", "7611-0200", "7612-0200", "5204-0200", "5204-0300", "5204-0400", "5204-0500", "5204-0600", "5702-0095", "3430-0400", "7431-1300", "7740-0080", "7740-0150", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "2208-5008", "2208-5010", "2208-5012", "2208-5016", "2208-5020", "2105-0002", "2105-0003", "2105-0005", "2105-0010", "2105-0015", "2105-0018", "0201-0012", "0201-0020", "0201-0040", "0201-0060", "0201-0070", "0201-0080", "0201-0100", "0201-0120", "0201-0200", "6105-0390", "6202-0125", "7830-0081", "7950-0149", "7951-0066", "7952-0246", "7953-3400", "7954-1119", "7611-0300", "7431-1100", "7431-2300", "7431-2500", "7611-0400", "7611-0500", "7614-0300", "0250-0100", "5210-0010", "5210-0019"], "값": "7620-0003"}},
|
||||
{"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["7620-0003", "7621-0100", "7995-0050", "5210-0013", "5210-0027", "5220-0015", "7730-0150", "6516-0060", "6516-0120", "6516-0250", "6516-0300", "6105-0190", "6202-0060", "0230-0002", "6410-0080", "6410-0100", "6410-0120", "6410-0150", "6410-0200", "6901-0010", "6530-0090", "6530-0120", "6517-0100", "6517-0150", "6517-0200", "6517-0250", "6517-0300", "6510-0100", "6510-0150", "6510-0200", "6510-0250", "6510-0300", "6518-0100", "6518-0150", "6518-0200", "6518-0250", "6518-0300", "5501-0080", "7750-0016", "7750-0025", "7730-0050", "5401-0017", "5202-0127", "5202-0240", "5202-0300", "7611-0200", "7612-0200", "5204-0200", "5204-0300", "5204-0400", "5204-0500", "5204-0600", "5702-0095", "3430-0400", "7431-1300", "7740-0080", "7740-0150", "2101-0010", "2101-0015", "2101-0020", "2101-0025", "2101-0030", "2101-0035", "2101-0040", "2101-0050", "2101-0070", "2101-0080", "2101-0100", "2101-0150", "2101-0220", "2101-0280", "2101-0300", "2104-0010", "2104-0015", "2104-0020", "2104-0025", "2104-0030", "2104-0035", "2104-0040", "2104-0045", "2104-0050", "2104-0060", "2104-0070", "2104-0080", "2104-0100", "2104-0130", "2104-0160", "2104-0200", "2104-0220", "2104-0250", "2104-0300", "2208-5008", "2208-5010", "2208-5012", "2208-5016", "2208-5020", "2105-0002", "2105-0003", "2105-0005", "2105-0010", "2105-0015", "2105-0018", "0201-0012", "0201-0020", "0201-0040", "0201-0060", "0201-0070", "0201-0080", "0201-0100", "0201-0120", "0201-0200", "6105-0390", "6202-0125", "7830-0081", "7950-0149", "7951-0066", "7952-0246", "7953-3400", "7954-1119", "7611-0300", "7431-1100", "7431-2300", "7431-2500", "7611-0400", "7611-0500", "7614-0300", "0250-0100", "5210-0010", "5210-0019", "0103-0016", "0103-0019", "0103-0023", "0103-0027", "0103-0032", "0230-0004", "0230-0006", "0230-0007", "0230-0008", "0230-0010", "0240-0007", "0250-0080", "0407-0054", "0407-0092", "0407-0107", "0407-0161", "0407-0206", "0610-0150", "0610-0200", "0610-0240", "2115-0024", "2115-0031", "2115-0036", "2116-0031", "2116-0036", "2117-0022", "2117-0027", "2118-0010", "2210-0145", "3430-0300", "3450-0642", "4115-0100", "4115-0150", "4115-0200", "4115-0300", "4505-0015", "4505-0026", "4611-0075", "5105-0050", "5105-0100", "5105-0150", "5105-0200", "5111-0040", "5111-0050", "5111-0060", "5111-0076", "5111-0091", "5112-0001", "5112-0002", "5112-0003", "5112-0004", "5112-0005", "5113-0001", "5113-0002", "5113-0003", "5113-0004", "5113-0005", "5113-0006", "5113-0007", "5113-0008", "5113-0009", "5113-0010", "5113-0011", "5114-0001", "5114-0002", "5114-0003", "5114-0004", "5114-0005", "5114-0006", "5114-0007", "5114-0008", "5115-0030", "5115-0055", "5115-0075", "5115-0095", "5116-0001", "5116-0002", "5116-0003", "5116-0004", "5116-0005", "5116-0006", "5116-0007", "5116-0008", "5117-0001", "5117-0002", "5117-0003", "5117-0004", "5117-0005", "5117-0006", "5117-0007", "5117-0008", "5118-0001", "5118-0002", "5118-0003", "5118-0004", "5118-0005", "5118-0006", "5118-0007", "5119-0625", "5203-1800", "5203-2200", "5203-2700", "5203-3500", "5203-4500", "5330-0074", "5805-0002", "5805-0003", "5901-0006", "5901-0010", "5901-0016", "6202-0200", "6408-0015", "6408-0020", "6408-0030", "6408-0040", "6408-0050", "6408-0085", "6408-0100", "6515-0090", "6530-0030", "6530-0040", "6530-0045", "6530-0060", "6532-0220", "6550-0130", "7103-0010", "7104-0010", "7202-0008", "7202-0010", "7205-0125", "7206-0020", "7206-0070", "7206-0100", "7430-1100", "7430-1300", "7430-1500", "7430-2300", "7430-2500", "7612-0300", "7612-0400", "7613-0075", "7613-0150", "7613-0300", "7613-0400", "7613-0600", "7613-0900", "7620-0002", "7730-0080", "7730-0100", "7730-0125", "7740-0100", "7770-0004", "7930-0001", "7930-0002", "7930-0003", "7930-0005", "7930-0007", "7930-0010", "7930-0015", "7930-0020", "7930-0025", "7930-0030", "7930-0040", "7930-0050", "7930-0075", "7930-0100", "7935-0180", "7991-0050", "7991-0100", "7991-0500", "7993-0020", "8201-0100", "8202-0100", "8203-0019", "8204-0100", "8205-0100", "9060-0060", "9060-0100", "9060-0200", "9060-0300", "9060-0500", "9060-0600", "9080-0050", "9080-0080", "9080-0100", "9080-0120", "9080-0150", "9080-0200", "9080-0300", "9080-0500", "9080-0700", "9080-1000", "9080-1100", "9080-1400", "9080-1500", "9080-1750", "9080-2000", "9080-3000"], "값": "7620-0003"}},
|
||||
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "P_기계", "기호": "P", "아래": "기계", "위": "", "이름": "EQ:{기계}.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "가격"}},
|
||||
{"열쇠": "k_기계손료", "기호": "k", "아래": "기계손료", "위": "", "이름": "EQ:{기계}.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "손료계수"}}
|
||||
@@ -367,7 +367,7 @@
|
||||
"갈래": "기계경비식"
|
||||
},
|
||||
"변수": [
|
||||
{"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["2106-0002", "2106-0003", "2106-0005", "6802-0040", "6802-0060", "6802-0100", "6802-0120", "6802-0135", "6802-0160", "6801-0010", "6803-0100", "6803-0120", "4430-0400", "3302-0038", "3801-0795", "7330-0010", "7954-1119", "7110-0025", "7110-0013", "7120-0746", "7101-0450"], "값": "2106-0002"}},
|
||||
{"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["2106-0002", "2106-0003", "2106-0005", "6802-0040", "6802-0060", "6802-0100", "6802-0120", "6802-0135", "6802-0160", "6801-0010", "6803-0100", "6803-0120", "4430-0400", "3302-0038", "3801-0795", "7330-0010", "7954-1119", "7110-0025", "7110-0013", "7120-0746", "7101-0450", "0260-0355", "2107-0005", "2330-0005", "2330-0007", "2402-0001", "3302-0030", "3302-0047", "3302-0057", "3801-0120", "4304-0060", "4304-0061", "6330-0015", "6330-0022", "6330-0032", "6330-0040", "7205-0475", "7205-0540", "7360-0055"], "값": "2106-0002"}},
|
||||
{"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}},
|
||||
{"열쇠": "조종원직종", "기호": "S", "아래": "조종원직종", "위": "", "이름": "조종원직종", "단위": "", "출처": "건설품셈 공통 8-1-3 5", "연결": {"갈래": "선택", "목록": ["1048", "1049", "1050"], "값": "1048"}},
|
||||
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}},
|
||||
@@ -412,7 +412,7 @@
|
||||
"갈래": "기계경비식"
|
||||
},
|
||||
"변수": [
|
||||
{"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["5401-0017"], "값": "5401-0017"}},
|
||||
{"열쇠": "기계", "기호": "S", "아래": "기계", "위": "", "이름": "기계", "단위": "", "연결": {"갈래": "선택", "목록": ["5401-0017", "4108-0060", "4108-0090", "4108-0120", "4108-0150", "4108-0180", "4108-0210", "5401-0015"], "값": "5401-0017"}},
|
||||
{"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}},
|
||||
{"열쇠": "손료", "기호": "C", "아래": "손료", "위": "", "이름": "손료", "단위": "", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4", "연결": {"갈래": "식"}},
|
||||
{"열쇠": "직종", "기호": "S", "아래": "직종", "위": "", "이름": "직종", "단위": "", "출처": "건설품셈 공통 8-1-3 5", "연결": {"갈래": "요소", "키": "EQ:{S_기계}", "칸": "조종원직종"}},
|
||||
|
||||
@@ -63,7 +63,10 @@ def file_docs(t, dim, dm, whole, kind: str, tag: str) -> dict[int, dict]:
|
||||
continue
|
||||
for v in doc["변수"]:
|
||||
if dm.md.var_id(v) == line["단가"] and (v.get("연결") or {}).get("갈래") == "숫자":
|
||||
v["연결"] = {"갈래": "숫자", "값": {k: str(got[k]) for k in dm.md.COSTS}}
|
||||
v["연결"] = {
|
||||
"갈래": "숫자",
|
||||
"값": {k: got[k] for k in dm.md.COSTS},
|
||||
} # 수 그대로(814-1)
|
||||
made[no] = doc
|
||||
ours[no] = dm._calc(doc, links, {})
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
(b) 꼴 — 소계 = 합 밖 · 비목 칸 비움(문서 셈 금액만) · 기계 세 줄 = 묶음 한 줄 · 변수 = 이름(상수 아님)
|
||||
· 분수 그림 공식 = 변수 식 · 수 하나 변수 = 숫자 연결 + 출처 「원파일」 · 품셈 조건 글 = 「추정」 표지
|
||||
(c) 문서 셈 거울 — 비목 없는 합 밖 줄 파이썬 = TS
|
||||
(d) 올림 길 — 원파일 = 같은 공사 엑셀 HP 에 붙음 · 가져온 식 = 원파일 · 원파일만 = 새 HP(EST 호표 ·
|
||||
STmate 호표 없음) · 원파일 HP 호표 → 일위대가(SC) 금액 = 원파일 · 엑셀만 = 엑셀 길 그대로
|
||||
(d) 올림 길(814-1 파일 하나) — 원파일 = 새 HP(단가산출표 판 · 일위대가 없음 · 엑셀 HP 에 안 붙음) ·
|
||||
가져온 식 = 원파일 · 엑셀 HP = 엑셀 길 그대로
|
||||
원파일 · 실무 엑셀 없으면 그 시험만 skip(다른 PC).
|
||||
"""
|
||||
|
||||
@@ -33,7 +33,6 @@ from test_810_1_stc import FILES as STC # noqa: E402
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_FromNative as fn # noqa: E402
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Build as hb # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hs # noqa: E402
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402
|
||||
@@ -200,53 +199,34 @@ def _excel_hp(folder: Path, tag: str) -> str:
|
||||
return row["키"]
|
||||
|
||||
|
||||
def test_upload_native_joins_excel_and_wins(world) -> None:
|
||||
def test_upload_native_one_file_list_only(world) -> None:
|
||||
"""814-1 — 파일 하나: 원파일은 같은 공사 엑셀 HP 에 안 붙고 새 HP(단가산출표 판 · 일위대가 없음)."""
|
||||
folder = world["personal"]
|
||||
est = _file(EST["거창"])
|
||||
est, stc = _file(EST["거창"]), _file(STC["봉화"])
|
||||
with store.at(folder):
|
||||
hp = _excel_hp(folder, "거창")
|
||||
before = fh.emit(folder, hp, 15)
|
||||
assert not before["원본"].get("원파일") # 엑셀만 = 엑셀 길
|
||||
got = fh.upload_native(folder, est.name, est.read_bytes(), WHO)
|
||||
assert got["붙음"] == [hp] and got["키"] == hp and got["원파일"]
|
||||
assert got["원파일"] and got["키"] != hp and "붙음" not in got and got["호표수"] == 0
|
||||
assert not fh.emit(folder, hp, 15)["원본"].get("원파일") # 엑셀 HP = 엑셀 길 그대로
|
||||
again = fh.upload_native(folder, est.name, est.read_bytes(), WHO)
|
||||
assert again["붙음"] == [hp] # 다시 올려도 새 HP 없음
|
||||
assert len(hs.rows(folder)) == 1
|
||||
made = fh.make(folder, hp, 15) # 전석 메쌓기 — 안쪽 산근 둘 + 굴삭기
|
||||
assert again["이어서"] and again["키"] == got["키"] and len(hs.rows(folder)) == 2
|
||||
grid = hs.grid(folder, got["키"])
|
||||
assert grid["보기"] == {"단가산출표": "s1"} and not grid["호표"]
|
||||
assert len(grid["목록"]["단산"]) == len(fn.numbers(native("EST", "거창")))
|
||||
made = fh.make(folder, got["키"], 15) # 전석 메쌓기 — 안쪽 산근 둘 + 굴삭기
|
||||
sk = store.read("sangeun", made["이름"])["문서"]
|
||||
assert sk["가져온"]["원본"]["원파일"] and made["검산"]["같음"]
|
||||
refs = [x for x in sk["가져온"]["로직"]["호표"] if x.get("참조")]
|
||||
assert len(refs) == 2 # 산근 속 산근 = 형제 SK 참조(797-1)
|
||||
doc = dim.of(sk)
|
||||
assert doc["가져온"]["원파일"] is True
|
||||
ans = dim.run(doc, sk)
|
||||
assert len([x for x in sk["가져온"]["로직"]["호표"] if x.get("참조")]) == 2
|
||||
ans = dim.run(dim.of(sk), sk)
|
||||
assert (ans["노무비"], ans["재료비"], ans["경비"]) == (74880, 7029, 12306)
|
||||
# 엑셀 먼저 · 원파일 나중의 반대 — 원파일 HP 가 있으면 엑셀 올림에 붙음
|
||||
other = world["company"]
|
||||
with store.at(other):
|
||||
fh.upload_native(other, est.name, est.read_bytes(), WHO)
|
||||
hp2 = _excel_hp(other, "거창")
|
||||
assert fh.emit(other, hp2, 1)["원본"].get("원파일")
|
||||
|
||||
|
||||
def test_upload_native_alone_hopyo_to_sc(world) -> None:
|
||||
folder = world["personal"]
|
||||
est, stc = _file(EST["장수"]), _file(STC["봉화"])
|
||||
with store.at(folder):
|
||||
got = fh.upload_native(folder, est.name, est.read_bytes(), WHO)
|
||||
assert got["원파일"] and not got.get("붙음") and got["호표수"] == 3
|
||||
out = hb.build_all(folder, folder, got["키"], None, True, 1) # 제1호표 = SC + 부르는 SK
|
||||
assert [x["번"] for x in out["만듦"]] == [1] and out["같음"] == 1 and not out["실패"]
|
||||
assert out["단가산출"] and all(
|
||||
store.read("sangeun", k)["문서"]["가져온"]["원본"].get("원파일")
|
||||
for k in out["단가산출"]
|
||||
)
|
||||
st = fh.upload_native(folder, stc.name, stc.read_bytes(), WHO)
|
||||
assert st["원파일"] and st["호표수"] == 0 # STmate 일위대가 구성 못 읽음 — 산근만
|
||||
assert st["원파일"] and st["호표수"] == 0
|
||||
head = hs.grid(folder, st["키"])["책"]["시트"][0]["칸"]
|
||||
assert head["A1"]["값"] == "코드" and str(head["A2"]["값"]).startswith("D")
|
||||
made = fh.make(folder, st["키"], next(iter(fn.numbers(native("STC", "봉화")))))
|
||||
assert made["검산"]["같음"]
|
||||
with pytest.raises(store.StoreError):
|
||||
fh.upload_native(folder, "깨진.estx", b"PK\x03\x04garbage", WHO)
|
||||
fh.upload_native(folder, "깨진.estx", b"PKgarbage", WHO)
|
||||
|
||||
|
||||
def test_router_branch_by_extension() -> None:
|
||||
|
||||
@@ -65,13 +65,17 @@ def test_sure_one_block() -> None:
|
||||
assert got["금액"]["가져온"] == got["금액"]["우리"]
|
||||
|
||||
|
||||
def test_near_formwork_lists_differences() -> None:
|
||||
got = judge(41) # 합판거푸집 4회
|
||||
assert got["판정"] == dm.NEAR and got["키"] == "GC000211"
|
||||
def test_sure_formwork_tool_cost_is_value() -> None:
|
||||
got = judge(
|
||||
41
|
||||
) # 합판거푸집 4회 — 공구손료 재료비 ↔ 경비 = 값(814-1 · 요구 744 C1 · 옛 812-2 = 로직)
|
||||
assert got["판정"] == dm.SURE and got["키"] == "GC000211"
|
||||
assert got["옵션"]["사용횟수"] == "4회" and got["옵션"]["유형"] == "보통"
|
||||
kinds = {d["항"] for d in got["다른"]}
|
||||
assert kinds == {"비목"} # 공구손료 1 % 재료비 ↔ 경비 = 로직(812-2)
|
||||
assert [d["항"] for d in got["참고"]] == ["단가 차이"] # 대전 합판 단가 = 값(참고)
|
||||
assert not got["다른"]
|
||||
assert [d["항"] for d in got["참고"]] == [
|
||||
"단가 차이",
|
||||
"공구손료 비목",
|
||||
] # 대전 합판 단가 · 비목 = 값
|
||||
|
||||
|
||||
def test_req704_pipe_is_near_with_unit_rate_machine() -> None:
|
||||
@@ -155,7 +159,7 @@ def test_swap_only_when_sure_and_back(world) -> None:
|
||||
with store.at(folder):
|
||||
hp = fh.upload_native(folder, est.name, est.read_bytes(), t2.WHO)["키"]
|
||||
sure = fh.make(folder, hp, 33)["이름"]
|
||||
near = fh.make(folder, hp, 41)["이름"]
|
||||
near = fh.make(folder, hp, 19)["이름"] # 파형강관(근접 — 41 은 814-1 로 확정)
|
||||
old = store.read("sangeun", sure)
|
||||
money0 = _money(sure)
|
||||
found = msk.find(sure, old["판"])
|
||||
|
||||
@@ -122,10 +122,10 @@ def _judge(no: int) -> dict:
|
||||
|
||||
|
||||
def test_formwork_rate_ratio_pairs() -> None:
|
||||
got = _judge(41) # 합판거푸집 4회 ↔ GC000211
|
||||
assert got["판정"] == dm.NEAR and got["키"] == "GC000211"
|
||||
assert [d["항"] for d in got["다른"]] == ["비목"] and "1 %" in got["다른"][0]["글"]
|
||||
assert [d["항"] for d in got["참고"]] == ["단가 차이"]
|
||||
got = _judge(41) # 합판거푸집 4회 ↔ GC000211 — 공구손료 비목 차이 = 값(814-1 · 요구 744 C1)
|
||||
assert got["판정"] == dm.SURE and got["키"] == "GC000211" and not got["다른"]
|
||||
assert [d["항"] for d in got["참고"]] == ["단가 차이", "공구손료 비목"]
|
||||
assert "1 %" not in got["참고"][1]["글"] and "재료비" in got["참고"][1]["글"]
|
||||
|
||||
|
||||
def test_req704_still_near() -> None:
|
||||
|
||||
@@ -198,7 +198,10 @@ def test_combo_money_same() -> None:
|
||||
blk = fn.numbers(saved)[2]
|
||||
sk, doc, money = doc_and_money(fn.emit(saved, 2))
|
||||
names = [(m["이름"], m.get("규격")) for m in doc["금액"]]
|
||||
assert ("굴삭기(무한궤도)", "0.7+브레이카") in names and ("대형 브레이커", "0.7㎥") in names
|
||||
assert ("굴삭기(무한궤도)", "0.7") in names and (
|
||||
"대형 브레이커",
|
||||
"0.7㎥",
|
||||
) in names # 첫 줄 규격 = 그 기계(814-1)
|
||||
assert float(money["계"]) == pytest.approx(float(blk["저장"]["T"]), abs=1)
|
||||
found = {x["원래"]["이름"]: x for x in doc["가져온"]["기계"]}
|
||||
assert found["브레이카"]["조합"] is True and found["브레이카"]["찾기어"] == "대형 브레이커"
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||
"""814-1 지킴 — 불러오기 결정 열 가지(방향 장부 `_방향_불러오기.md` E 표 · 요구 742 ~ 750).
|
||||
|
||||
① 파일 하나(같은 공사 붙이기 걷음) ② 확장자 파일 = 단가산출표 판(산출근거 머리) · 일위대가 없음 ·
|
||||
③ 엑셀 이름 규격 · 단위 꼬리 뗀 맞대기 ④ 기계 세 줄 = 한 줄(STC · EST · 엑셀) · 조합 다음 줄 기계마다 한 항목 ·
|
||||
⑤ 계산식 기계 목록(빠진 기계 더함 · 기본값 그대로) · 조합 = 우리 조합 식 ⑥ 공구손료 비목 = 참고 ·
|
||||
⑦ 못 이은 줄 까닭 · 「없음」 띠 ⑧ 유가 = 전국평균 + 지역 옵션 · 전력 = 일반용(갑)Ⅰ 저압 + 계약종별 옵션 ·
|
||||
⑨ 재료 지역 꼬리 뗀 정확 일치 ⑩ 가벼움(조합 첫 줄 규격 · 소계 검산 · 기계 찾기 판정 · 409 · 확정 글 · 전수 도우미).
|
||||
원파일 · 실무 엑셀 없으면 그 시험만 skip(다른 PC).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[1]
|
||||
for p in (ROOT, HERE):
|
||||
if str(p) not in sys.path:
|
||||
sys.path.insert(0, str(p))
|
||||
|
||||
import test_797_1a_import_flags as practice # noqa: E402
|
||||
import test_810_2_native_doc as t # noqa: E402
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_FromNative as fn # noqa: E402
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim # noqa: E402
|
||||
from M02_MasterTemplete import M02_Doc_MatchSk as msk # noqa: E402
|
||||
from M02_MasterTemplete import M02_Doc_Match_Note as note # noqa: E402
|
||||
from M02_MasterTemplete import M02_Doc_NativeLayout as lay # noqa: E402
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hs # noqa: E402
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as mh # noqa: E402
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store # noqa: E402
|
||||
from M02_MasterTemplete import M02_San_Emit as em # noqa: E402
|
||||
from M02_MasterTemplete import M02_San_Link as link # noqa: E402
|
||||
from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh # noqa: E402
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp # noqa: E402
|
||||
from resources.tester.test_m02_labor_ref import world # noqa: E402, F401 — 같은 임시 층
|
||||
|
||||
SCRIPT = ROOT / "resources/master_data/scripts/build_기계목록_빠진기계_814_1.py"
|
||||
FAMILY = [f"GC0002{n}" for n in range(67, 77)]
|
||||
|
||||
|
||||
def excel(tag: str) -> dict:
|
||||
path = practice.FILES.get(tag)
|
||||
if not path or not path.exists():
|
||||
pytest.skip(f"실무 엑셀 없음 {tag}")
|
||||
from M02_MasterTemplete import M02_San_Xlsx as sx
|
||||
|
||||
return fn.pack(sx.read(path.name, path.read_bytes()), path.name)
|
||||
|
||||
|
||||
def doc_of(saved: dict, no: int) -> tuple[dict, dict, dict]:
|
||||
out = fn.emit(saved, no)
|
||||
sk, doc, got = t.doc_and_money(out)
|
||||
return out, doc, got
|
||||
|
||||
|
||||
def same_money(saved: dict, no: int, got: dict) -> bool:
|
||||
want = fn.numbers(saved)[no].get("저장") or {}
|
||||
return all(abs(float(got[k]) - float(want.get(c) or 0)) < 0.5 for c, k in t.COST[:3])
|
||||
|
||||
|
||||
# ── ① ② 파일 하나 · 단가산출표 판 ──────────────────────────────────────────
|
||||
def test_one_file_and_list_sheet(world) -> None:
|
||||
assert not hasattr(fn, "same") and not hasattr(fh, "_natives") # 붙이기 길 걷음
|
||||
stc = t._file(t.STC["봉화"])
|
||||
folder = world["personal"]
|
||||
with store.at(folder):
|
||||
got = fh.upload_native(folder, stc.name, stc.read_bytes(), t.WHO)
|
||||
assert got["원파일"] and got["호표수"] == 0 and "붙음" not in got
|
||||
grid = hs.grid(folder, got["키"])
|
||||
blocks = fn.numbers(t.native("STC", "봉화"))
|
||||
assert grid["보기"] == {"단가산출표": "s1"} and len(grid["단산"]) == len(blocks)
|
||||
head = grid["책"]["시트"][0]["칸"]
|
||||
assert [head[f"{c}1"]["값"] for c in "ABCDEFGH"] == list(fn._LIST_HEAD)
|
||||
first = next(iter(blocks.values()))
|
||||
assert head["A2"]["값"] == first["코드"] and head["H2"]["값"] == first["저장"]["T"]
|
||||
est = fn.hopyo(t.native("EST", "거창"))
|
||||
assert est["호표"] == [] and len(est["단산"]) == len(fn.numbers(t.native("EST", "거창")))
|
||||
|
||||
|
||||
# ── ③ 꼬리 뗀 맞대기 ─────────────────────────────────────────────────────
|
||||
def test_bare_name_tail() -> None:
|
||||
doc = {
|
||||
"이름": "구조물터파기(인력) 경질토사 / M3",
|
||||
"결과단위": "원/M3",
|
||||
"가져온": {"규격": "경질토사"},
|
||||
}
|
||||
assert note.bare(doc)["이름"] == "구조물터파기(인력)" and doc["이름"].endswith("/ M3")
|
||||
plain = {"이름": "측구터파기", "결과단위": "원/㎥", "가져온": {"규격": "토사"}}
|
||||
assert note.bare(plain) is plain
|
||||
|
||||
|
||||
# ── ④ 세 줄 묶기 · 조합 그룹 ─────────────────────────────────────────────
|
||||
def _machine_split(doc: dict, out: dict) -> int:
|
||||
"""문서에 남은 기계 비목 낱 줄(같은 기계 · 같은 수량 · 비목 셋이 따로) 묶음 수."""
|
||||
names = {h["이름"]: h for h in out["로직"]["호표"]}
|
||||
seen: dict[tuple, set] = {}
|
||||
for m in doc["금액"]:
|
||||
res = (names.get(m.get("이름")) or {}).get("자원") or {}
|
||||
if res.get("갈래") == "기계" and m.get("비목") in ("노무비", "재료비", "경비"):
|
||||
seen.setdefault((res.get("코드"), str(m.get("수량"))), set()).add(m["비목"])
|
||||
return sum(1 for v in seen.values() if len(v) == 3)
|
||||
|
||||
|
||||
def test_three_lines_one_machine_stc() -> None:
|
||||
saved = t.native("STC", "봉화")
|
||||
out, doc, got = doc_of(saved, 55)
|
||||
assert same_money(saved, 55, got) and _machine_split(doc, out) == 0
|
||||
crawl = next(m for m in doc["금액"] if str(m["이름"]).startswith("크롤러드릴"))
|
||||
assert crawl["비목"] == "묶음" # 「노 무 비 · 재 료 비 · 경 비」 → 기계 한 줄
|
||||
assert not any(str(h["이름"]).replace(" ", "") == "노무비" for h in out["로직"]["호표"])
|
||||
|
||||
|
||||
def test_three_lines_excel_stmate_and_est_combo_group() -> None:
|
||||
saved = excel("봉화")
|
||||
out, doc, got = doc_of(saved, 55)
|
||||
assert same_money(saved, 55, got) and _machine_split(doc, out) == 0
|
||||
est = t.native("EST", "main")
|
||||
out, doc, got = doc_of(est, 360)
|
||||
assert same_money(est, 360, got)
|
||||
titles = [it["제목"] for it in lay.stack_of(doc)]
|
||||
assert titles.count("덤프트럭 자동덮개시설 15톤용") == 1 # 다음 줄 셋 = 한 항목(엇갈림 없음)
|
||||
|
||||
|
||||
# ── ⑤ 계산식 기계 목록 · 조합 식 ─────────────────────────────────────────
|
||||
def _build():
|
||||
spec = importlib.util.spec_from_file_location("build_814_1", SCRIPT)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
def test_machine_lists_complete_and_defaults_kept() -> None:
|
||||
build = _build()
|
||||
add, skipped = build.plan()
|
||||
assert add == {} # 빠진 기계 모두 더함(다시 돌려도 같음)
|
||||
assert all(miss for *_, miss in skipped) # 못 넣음 = 기계표 빈 값(지어 넣지 않음)
|
||||
docs = json.loads(build.DOC.read_text("utf-8"))["줄"]
|
||||
for d in docs:
|
||||
if d["키"] in FAMILY:
|
||||
var = next(v for v in d["변수"] if v["열쇠"] == "기계")
|
||||
assert var["연결"]["값"] == var["연결"]["목록"][0] # 기본값 = 첫 기계 그대로
|
||||
whole = imp._whole()
|
||||
eq = {r.get("원문번호"): k for k, r in whole.index["EQ"].items()}
|
||||
for code in ("0230-0007", "0211-0060", "6630-0005", "0213-0060"): # 브레이커 · 굴착기(타이어) …
|
||||
if code in eq:
|
||||
assert mh.hourly(eq[code])["ok"], code
|
||||
|
||||
|
||||
def test_combo_uses_our_combo_formula() -> None:
|
||||
saved = excel("거창")
|
||||
out, doc, got = doc_of(saved, 2)
|
||||
assert same_money(saved, 2, got)
|
||||
main = next(h for h in out["로직"]["호표"] if (h.get("자원") or {}).get("조합첫"))
|
||||
part = next(h for h in out["로직"]["호표"] if (h.get("자원") or {}).get("조합"))
|
||||
assert str(main["이음"]).startswith("로직(GC000271") and part["이음"] is None
|
||||
assert "조합" in link.why(part)
|
||||
assert main["규격"] != (main["자원"] or {}).get("규격") # 칩 글 = 그 기계 규격(조합 글 아님)
|
||||
|
||||
|
||||
# ── ⑥ 공구손료 비목 = 참고 ───────────────────────────────────────────────
|
||||
def test_tool_rate_cost_is_value() -> None:
|
||||
a = {
|
||||
"갈래": "율",
|
||||
"이름": "공구손료",
|
||||
"율": "3",
|
||||
"대상": {("인력", "노무비")},
|
||||
"비목": ["재료비"],
|
||||
}
|
||||
b = {**a, "비목": ["경비"]}
|
||||
left, right = [a], [b]
|
||||
|
||||
def take(test):
|
||||
hits = [(x, y) for x in list(left) for y in list(right) if test(x, y)][:1]
|
||||
for x, y in hits:
|
||||
left.remove(x), right.remove(y)
|
||||
return hits
|
||||
|
||||
diffs, notes = note.rate_pairs(left, right, take)
|
||||
assert not diffs and notes[0]["항"] == "공구손료 비목"
|
||||
|
||||
|
||||
# ── ⑧ ⑨ 유가 · 전력 · 재료 ───────────────────────────────────────────────
|
||||
def test_oil_power_material_links() -> None:
|
||||
oil, _ = link.m01("재료", {"이름": "경 유", "규격": "", "단위": "ℓ"})
|
||||
power, _ = link.m01("재료", {"이름": "전 력", "규격": "", "단위": "KWH"})
|
||||
whole = imp._whole()
|
||||
assert whole.get(oil)["상세구분"] == "전국평균"
|
||||
assert whole.get(power)["상세구분"] == "일반용(갑)Ⅰ 저압"
|
||||
got = dim._oil(oil)
|
||||
assert got["키"] == "MO:경유:{S_지역}" and got[dim.OPTION]["연결"]["값"] == "전국평균"
|
||||
got = dim._oil(power)
|
||||
assert got["계약종별"] == "S_전력계약종별"
|
||||
assert got[dim.OPTION]["연결"]["값"] == "일반용(갑)Ⅰ 저압"
|
||||
ply, _ = link.m01("재료", {"이름": "합판(내수)", "규격": "12t×1220×2440mm", "단위": "㎡"})
|
||||
nail, _ = link.m01("재료", {"이름": "철못", "규격": "N 25", "단위": "kg"})
|
||||
assert whole.get(ply)["이름"] == "합판(내수), 서울" and whole.get(nail)["규격"] == "N 25"
|
||||
assert link.m01("재료", {"이름": "접착제", "규격": "", "단위": "kg"}) == (None, None)
|
||||
|
||||
|
||||
def test_oil_doc_region_option() -> None:
|
||||
saved = excel("2016사방")
|
||||
out, doc, got = doc_of(saved, 326)
|
||||
assert same_money(saved, 326, got)
|
||||
region = next(v for v in doc["변수"] if v["열쇠"] == "지역")
|
||||
assert region["연결"]["값"] == "전국평균"
|
||||
oil = next(v for v in doc["변수"] if str((v.get("연결") or {}).get("키", "")).startswith("MO:"))
|
||||
assert "엑셀" in oil["연결"] # 엑셀 값 = 검산용
|
||||
|
||||
|
||||
# ── ⑦ 까닭 · 띠 ──────────────────────────────────────────────────────────
|
||||
def test_unlinked_reasons_and_tally(monkeypatch) -> None:
|
||||
from M02_MasterTemplete import M02_Hopyo_Row as hrow
|
||||
|
||||
monkeypatch.setattr(hrow, "m01_picks", lambda kind, line: []) # 우리 자재표에 후보 없음
|
||||
assert link.why({"자원": {"갈래": "재료", "이름": "없는자재글자", "규격": ""}}).startswith(
|
||||
"자재 없음"
|
||||
)
|
||||
assert (
|
||||
link.why({"자원": {"갈래": "재료", "이름": "구역화물 운임", "규격": ""}}) == "운임표 없음"
|
||||
)
|
||||
gone = {"자원": {"갈래": "기계", "찾기": {"판정": "못 찾음", "까닭": "기종 모름"}}}
|
||||
assert link.why(gone) == "별칭 대기"
|
||||
sib = {"호표": [{"이름": "a", "참조": {"키": "SK-1"}, "자원": {"갈래": "단가산출"}}]}
|
||||
assert link.tally(sib) == {"됨": 1, "줄": 1, "조합": True}
|
||||
src = (ROOT / "M02_MasterTemplete/logic/M02_Sangeun_Match.ts").read_text("utf-8")
|
||||
assert "우리 기초 없음" in src and "우리 정보로 셈" in src
|
||||
assert "우리정보" in Path(msk.__file__).read_text("utf-8")
|
||||
|
||||
|
||||
# ── ⑩ 가벼움 ─────────────────────────────────────────────────────────────
|
||||
def test_light_fixes(world) -> None:
|
||||
saved = excel("북부청임도")
|
||||
out = fn.emit(saved, 33)
|
||||
chk = imp.check(t.sk_of(out), t.whole())
|
||||
assert chk["같음"] and all(x["같음"] for x in chk["줄"]) # 얼린 줄도 소계 식에
|
||||
src = {"이름": "진동 로울러(핸드가이드식)", "규격": "0.7Ton", "단위": "hr"}
|
||||
link.m01("기계", src)
|
||||
assert src["찾기"]["판정"] and src["찾기"]["이름"] # 기계 찾기 판정 붙음
|
||||
seen = []
|
||||
folder = world["personal"]
|
||||
with store.at(folder):
|
||||
|
||||
def first(key: str) -> None: # HP 를 목록에 보이기 전에 산근 보관(409 막음)
|
||||
seen.append(any(r["키"] == key for r in hs.rows(folder)))
|
||||
|
||||
hs.save(folder, "가.xlsx", {"호표": [], "단산": []}, t.WHO, None, "지문814", first)
|
||||
assert seen == [False]
|
||||
origin = (ROOT / "M02_MasterTemplete/logic/M02_Sangeun_Origin.ts").read_text("utf-8")
|
||||
assert "엑셀 읽기 확정" in origin
|
||||
helper = (HERE / "helper_810_4_sweep.py").read_text("utf-8")
|
||||
assert "str(got[k])" not in helper
|
||||
one = em.first_spec({"규격": "0.7+브레이카"}, [{"규격": "0.7㎥"}, {"규격": "0.7"}])
|
||||
assert one["규격"] == "0.7㎥"
|
||||
Reference in New Issue
Block a user