Merge branch 'sub_laptop_3' of C:/Aislo_wt/sub_laptop_3 into main_laptop_1

This commit is contained in:
2026-11-11 20:00:24 +09:00
21 changed files with 627 additions and 20 deletions
+2
View File
@@ -90,6 +90,8 @@ def _est_line(ln: dict) -> dict:
one = {k: ln.get(k) for k in _EST_KEYS if ln.get(k) not in (None, [], False, "")}
if one.get("자원"):
one["자원"] = {k: ln["자원"].get(k) for k in _RES_KEYS}
if ln["자원"].get("같은구성"): # 816-2 같은 파일에서 구성이 같은 단독 기계
one["자원"]["같은구성"] = ln["자원"]["같은구성"]
return one
+1 -1
View File
@@ -196,7 +196,7 @@ def hourly_link(eq: str, part: str) -> dict | None:
logic = machine.logic_of(code)
if not logic:
return None
given = {"기계": _lit(code)}
given = {"기계": _lit(code)} if machine.takes_machine(logic) else {}
for spec in logic.get("입력") or []:
if spec.get("이름") != "기계":
given[spec["이름"]] = _lit((spec.get("고르기") or [""])[0])
+4 -2
View File
@@ -320,7 +320,8 @@ class Block:
r |= {"단가": prices}
if quoted := self.quote(r)[1]: # 815-1 K5 — 세 줄 견적 = 값묶음 그대로 · 표지 한 번
self.mark(rows[0], quoted.removeprefix(NOTE))
parts = em.machine_parts(r) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
make = (rows[0].get("자원") or {}).get("같은구성") # 816-2 같은 파일 구성 대조
parts = em.machine_parts(r, make) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
r |= {"찾기": parts[0]} if parts else {}
r |= {"조합첫": True} if len(parts) > 1 else {}
head = next(
@@ -366,7 +367,8 @@ class Block:
price = (ln.get("연결단가") or [ln["결과"][-1]])[0]
r = self.res(ln, ln["비목"], price if got else ln["결과"][-1])
name = self.o.names.item(f"{ln.get('품명') or r.get('이름') or '줄'} {COMP[ln['비목']]}")
parts = em.machine_parts(r) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
make = (ln.get("자원") or {}).get("같은구성") # 816-2 같은 파일 구성 대조
parts = em.machine_parts(r, make) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
r |= {"찾기": parts[0]} if parts else {}
r |= {"조합첫": True} if len(parts) > 1 else {}
link, many = (parts[0]["키"], None) if parts else self.quote(r)
+1 -1
View File
@@ -140,7 +140,7 @@ def _fixed(fx: dict, qty: str | None, rates: list[dict]) -> list[dict]:
got = mh.hourly(str(link["기계"]))
if not got.get("ok") or not got.get("로직"):
return [_warn(f"기계 「{name}」 — {got.get('reason') or '시간당 경비 못 셈'}")]
machine = {"기계": f"'{got['원문번호']}'"}
machine = {"기계": f"'{got['원문번호']}'"} if mh.takes_machine(mh.logic_of(got["원문번호"]) or {}) else {} # fmt: skip
return _pick("기계", qty, 키=got["로직"], 이름=name, 규격=spec, 입력=machine)
price = fx.get("단가") or {}
costs = [c for c in COSTS if Decimal(str(price.get(c) or 0))] or ["재료비"]
+42
View File
@@ -305,6 +305,48 @@ def find_all(name: Any, spec: Any = "") -> list[dict[str, Any]]:
return out
# ── 같은 파일 안 구성 대조로 가름(816-2 · 요구 773-3) ───────────────────────────
MADE = "같은 파일 구성 같음"
def peers(machines: dict[Any, tuple[str, str, dict]]) -> dict[Any, list[list[str]]]:
"""같은 파일 기계 {코드: (이름, 규격, 구성 {손료 [[계수, 가격]], 주연료, 조종원})} → {코드: 구성이 같은
단독 기계(손료 줄 하나) [이름, 규격] 들} — 낱 기계 = 구성 통째 같음 · 조합 = 손료 줄마다 그 줄 하나뿐인
단독 기계. 같은 파일 · 같은 때의 값끼리(우리 표 값과 안 맞댐)."""
alone = {c: m for c, m in machines.items() if len(m[2].get("손료") or []) == 1}
out: dict[Any, list[list[str]]] = {}
for c, (_, _, make) in machines.items():
loss = make.get("손료") or []
if len(loss) == 1:
got = [o for o, m in alone.items() if o != c and m[2] == make]
else:
got = [o for o, m in alone.items() if o != c and m[2]["손료"][0] in loss]
if got:
out[c] = [[alone[o][0], alone[o][1]] for o in got]
return out
def narrow(parts: list[dict[str, Any]], same: list | None) -> list[dict[str, Any]]:
"""「후보 여럿」 부분 → 같은 파일에서 구성이 같은 단독 기계 이름 · 규격으로 다시 찾아(`find`) 그 후보
가운데 키 하나로 모이면 그 줄 · 여럿 · 없음 · 대조 없음 = 그대로(우리 값으로 넘겨짚지 않음)."""
if not same:
return parts
found = [(n, s, find(n, s)) for n, s in same]
out = []
for p in parts:
if p["판정"] == MANY:
mine = {c["키"] for c in p["후보"]}
keys = {
g["키"] for _, _, g in found if g["판정"] in (LINKED, ALIAS) and g["키"] in mine
}
if len(keys) == 1:
key = keys.pop()
n, s = next((n, s) for n, s, g in found if g["키"] == key)
p = {**p, **_ans(LINKED, [table()[2][key]], n, f"{MADE} — {n} {s}".strip())}
out.append(p)
return out
def mark(got: dict[str, Any], name: Any, spec: Any) -> str | None:
"""못 이은 기계 표지 글(「참고 · 」 머리) — 이음 · 별칭 = None."""
said = f"「{' '.join(x for x in (str(name or ''), str(spec or '')) if x)}」"
+22 -2
View File
@@ -56,7 +56,7 @@ def logic_of(code: str) -> dict | None:
(「조종원 없는 기종」 목록에 겹친 기계 — 진동롤러(핸드가이드식) · 고소작업차 …)."""
fit = [r for r in _family() if code in [str(c) for c in _codes(r)]]
if not fit:
return None
return _own(code)
op = _operated(code)
def order(row: dict) -> tuple:
@@ -67,6 +67,25 @@ def logic_of(code: str) -> dict | None:
return min(fit, key=order)
def _own(code: str) -> dict | None:
"""그 기계만의 시간당 경비 식(816-1 · 4륜구동 1톤 덤프트럭 = GF000145 · 816-3 체인톱 = GF000337) —
「기계」 입력 없는 기계경비식이 그 원문번호 기계 줄 값(`EQ….가격` · `EQ….손료계수`)을 읽으면 그 식."""
rows = store.loaded()[1].index.get("EQ", {}).values()
keys = [f"{r.get('키')}." for r in rows if code and str(r.get("원문번호")) == code]
for data in store.loaded()[0].values():
for row in data.get("줄") or [] if data.get("그룹") == "로직" else []:
if (row.get("부품") or {}).get("갈래") != "기계경비식" or takes_machine(row):
continue
if any(k in str(row.get("중간")) + str(row.get("호표")) for k in keys):
return row
return None
def takes_machine(logic: dict) -> bool:
"""식이 「기계」 입력(원문번호 고르기)을 받는지 — 안 받는 식(GF000145)엔 기계='…' 를 안 넘김."""
return any(i.get("이름") == "기계" for i in logic.get("입력") or [])
def hourly(key: str) -> dict[str, Any]:
"""`{ok, 이름, 규격, 로직, 원문번호, 노무비, 재료비, 경비, 계}` · 못 셈 = `{ok: False, reason}`."""
if not key.startswith("EQ"):
@@ -84,7 +103,8 @@ def hourly(key: str) -> dict[str, Any]:
for i in logic.get("입력") or []
if i.get("이름") != "기계"
)
call = f"로직({logic['키']}, 기계='{code}'{args})"
mine = f", 기계='{code}'" if takes_machine(logic) else ""
call = f"로직({logic['키']}{mine}{args})"
line = {"종류": "로직", "요소": call, "이름": "기계", "단위": "시간", "수량": "1"}
holder = {
"키": "", "원문번호": "", "이름": "기계 시간", "결과단위": "원/식", "출처": "자체",
+9 -1
View File
@@ -22,7 +22,15 @@ STM_SHEETS = (
"경비목록표",
"중기목록표",
)
EST_SHEETS = ("단가산출총괄표", "단가산출", "기계경비총괄표", "자재단가", "노임단가", "견적단가")
EST_SHEETS = (
"단가산출총괄표",
"단가산출",
"기계경비총괄표",
"기계경비",
"자재단가",
"노임단가",
"견적단가",
)
_X7, _X5 = "_x0007_", "_x0005_"
_HEAD = re.compile(r"BDCOD\|([A-Z]\d+).*?POSS\|(\d+).*?POSE\|(\d+)")
+4 -3
View File
@@ -178,13 +178,14 @@ def reprice(logic: dict, where: dict, price: dict[str, float]) -> int:
# ── 기계 부분(812-9 · 조합 기계 = 줄 둘) ─────────────────────────────────
def machine_parts(src: dict) -> list[dict]:
"""기계 자원 → 부분 찾기(`M02_Machine_Find.find_all` · 조합 = 둘 이상) · 기계 아님 = []."""
def machine_parts(src: dict, same: list | None = None) -> list[dict]:
"""기계 자원 → 부분 찾기(`M02_Machine_Find.find_all` · 조합 = 둘 이상) · 기계 아님 = [] ·
same = 같은 파일에서 구성이 같은 단독 기계 [[이름, 규격]](816-2 · 있으면 「후보 여럿」을 가름)."""
if src.get("갈래") != "기계":
return []
from M02_MasterTemplete import M02_Machine_Find as mfind
return mfind.find_all(src.get("이름"), src.get("규격"))
return mfind.narrow(mfind.find_all(src.get("이름"), src.get("규격")), same)
def first_spec(one: dict, parts: list[dict]) -> dict:
+33 -2
View File
@@ -228,9 +228,36 @@ def marks(text: str) -> dict[str, Any]:
return out
def makes(t: dict[str, list[dict[str, Any]]]) -> dict[str, dict[str, Any]]:
"""기계 구성(816-2 · MachMainDat — TitleCode 머리 다음 줄부터 빈 줄까지) → {코드: {손료 [[계수, 가격]
(Index -3 GN · TU)] · 주연료 ℓ(-2 「주연료」 GN) · 조종원(-1 GN > 0)}} — 손료 줄 없는 기계는 뺌."""
out: dict[str, dict[str, Any]] = {}
cur = None
for r in t.get("MachMainDat", []):
if r.get("TitleCode"):
cur = out.setdefault(r["TitleCode"], {"손료": [], "주연료": None, "조종원": False})
elif not r.get("Name"):
cur = None
elif cur is not None:
i, gn = r.get("Index"), r.get("GN")
if i == -3 and gn:
cur["손료"].append([gn, r.get("TU")])
elif i == -2 and cur["주연료"] is None and re.sub(r"\s", "", r["Name"]) == "주연료":
cur["주연료"] = gn
elif i == -1 and (gn or 0) > 0:
cur["조종원"] = True
return {k: v for k, v in out.items() if v["손료"]}
def resources(t: dict[str, list[dict[str, Any]]]) -> dict[tuple[int, str], dict[str, Any]]:
"""(LinkIndex, 코드) → 자원 머리(표 · 이름 · 규격 · 단위 · 저장 금액)."""
"""(LinkIndex, 코드) → 자원 머리(표 · 이름 · 규격 · 단위 · 저장 금액 · 기계 = 같은 파일에서 구성이
같은 단독 기계 `같은구성` [[이름, 규격]] · 816-2)."""
from M02_MasterTemplete import M02_Machine_Find as mfind
out = {}
heads = {r.get("Code"): r for r in t.get("MachTitleDat", [])}
made = {c: (heads[c].get("Name"), heads[c].get("Size"), m) for c, m in makes(t).items() if c in heads} # fmt: skip
same = mfind.peers(made)
for link, name in SOURCE.items():
for r in t.get(name, []):
if r.get("Code"):
@@ -240,7 +267,11 @@ def resources(t: dict[str, list[dict[str, Any]]]) -> dict[tuple[int, str], dict[
"규격": r.get("Size"),
"단위": r.get("Unite"),
"저장": _amounts(r, AMT),
}
} | (
{"같은구성": same[r["Code"]]}
if name == "MachTitleDat" and r["Code"] in same
else {}
)
return out
+33
View File
@@ -145,9 +145,42 @@ def resources(sheets: dict[str, list]) -> dict[str, list[dict[str, Any]]]:
vals = {k: float(v or 0) for k, v in vals.items()}
got.append({**_drop(one), "표": sheet, "값": vals} | ({"칸": note} if note else {}))
out[kind] = got
from M02_MasterTemplete import M02_Machine_Find as mfind
same = mfind.peers(makes(sheets.get("기계경비") or []))
for one in out["기계"]: # 816-2 같은 파일에서 구성이 같은 단독 기계
if got := same.get((flat(one["이름"]), flat(one["규격"]))):
one["같은구성"] = got
return out
def makes(rows: list[list[Any]]) -> dict[tuple[str, str], dict[str, Any]]:
"""EST 엑셀 「기계경비」 시트(816-2) — 「N. 이름 · 규격」 머리 아래 빈 줄까지: 「손료 가격 ₩× 계수 ×
10(-7)」 · 「주연료 종류 ℓ」 · 「직종 1 인」 → {(이름, 규격) 붙임: (이름, 규격, {손료 [[계수, 가격]] ·
주연료 ℓ · 조종원})}."""
out: dict[tuple[str, str], tuple[str, str, dict[str, Any]]] = {}
cur = None
for row in rows:
v = [c for c in row if c not in (None, "")]
head = re.match(r"\d+\.\s*(.+)", str(v[0])) if v else None
if head:
spec = v[1] if len(v) > 1 and isinstance(v[1], str) else ""
name = head.group(1).strip()
make = {"손료": [], "주연료": None, "조종원": False}
cur = out.setdefault((flat(name), flat(spec)), (name, spec, make))[2]
elif not v:
cur = None
elif cur is not None:
word = flat(v[0])
if word.startswith("손료") and "₩×" in v and (at := v.index("₩×") + 1) < len(v):
cur["손료"].append([number(v[at]), number(v[1])])
elif word == "주연료" and len(v) > 2 and cur["주연료"] is None:
cur["주연료"] = number(v[2])
elif len(v) > 2 and v[2] == "인" and (number(v[1]) or 0) > 0:
cur["조종원"] = True
return {k: m for k, m in out.items() if m[2]["손료"] and None not in m[2]["손료"][0]}
def _drop(one: dict) -> dict:
return {k: v for k, v in one.items() if k != "줄"}
+5 -1
View File
@@ -154,7 +154,11 @@ class Lines:
f"단산{one['번']}" if kind == "산근" else f"{one['표']}#{one['번']}"
) # 원 코드 없음 = 표 줄 번
out = {"코드": code, "종류": kind, "표": one["표"]}
return out | {k: one[k] for k in ("이름", "규격", "단위")} | {"저장": dict(one["값"])}
out |= {
k: one[k]
for k in ("이름", "규격", "단위") + (("같은구성",) if "같은구성" in one else ())
}
return out | {"저장": dict(one["값"])}
def link(self, ln: dict, kind: str, one: dict | None) -> None:
ln["연결"] = LINK[kind] if kind in LINK else 62
+3 -1
View File
@@ -1,5 +1,5 @@
{
"다음": {"LB": 409, "EQ": 655, "RT": 20, "MN": 7000, "MT": 30719, "MO": 74, "MP": 364, "FX": 6, "CF": 159, "QF": 477, "GF": 337, "CC": 528, "GC": 1195, "QC": 2060, "GX": 5, "UA": 38},
"다음": {"LB": 409, "EQ": 656, "RT": 20, "MN": 7000, "MT": 30719, "MO": 74, "MP": 364, "FX": 6, "CF": 159, "QF": 477, "GF": 338, "CC": 528, "GC": 1195, "QC": 2060, "GX": 5, "UA": 38},
"폐기": {"MT000001": "자재품목 휘발유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000002": "자재품목 경유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000003": "자재품목 선박용경유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000004": "자재품목 중유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000005": "자재품목 등유 — 값이 비고 글뿐 · 유가 테이블로 감", "LB000399": "이름 「〃」 — 직종이 아니라 되풀이 기호 · 소요량 13-6-9 에서 윗줄 직종으로 바로잡음", "MN": "재료_나라장터자재 — old 로 옮김 · 로직은 자재품목·품셈재료로 갈아 끼움", "LB000372": "미확보 고급기능사 — 측량노임 항공사진고급기능사 옛이름으로 옮김", "LB000393": "미확보 리베팅공 — 건설노임 철골공 옛이름으로 옮김(2010 통합표 연번10)", "LB000405": "미확보 Belt Conveyor 설치공 — 품셈 13-10-1 주② 배분으로 다섯 직종에 나눔", "MT000006": "자재품목 전력 — 재료_유가전력 전력 줄로 감", "MT000007": "자재품목 전력 — 재료_유가전력 전력 줄로 감", "LB000368": "미확보 기술사 — 기술사(건설) 옛이름으로 옮김", "LB000369": "미확보 특급기술자 — 특급기술자(건설) 옛이름으로 옮김", "LB000370": "미확보 고급기술자 — 고급기술자(건설) 옛이름으로 옮김", "LB000361": "미확보 중급기술자 — 중급기술자(건설) 옛이름으로 옮김", "LB000364": "미확보 초급기술자 — 초급기술자(건설) 옛이름으로 옮김", "LB000365": "미확보 고급숙련기술자 — 고급숙련기술자(건설) 옛이름으로 옮김", "LB000366": "미확보 중급숙련기술자 — 중급숙련기술자(건설) 옛이름으로 옮김", "LB000367": "미확보 초급숙련기술자 — 초급숙련기술자(건설) 옛이름으로 옮김", "LB000389": "미확보 기계기사 — 초급기술자(기계·설비) 옛이름으로 옮김", "LB000388": "미확보 기계산업기사 — 초급숙련기술자(기계·설비) 옛이름으로 옮김", "MO000055": {"원문번호": "주택용저압:기본_200이하", "파일": "재료_유가전력.json"}, "MO000056": {"원문번호": "주택용저압:기본_201~400", "파일": "재료_유가전력.json"}, "MO000057": {"원문번호": "주택용저압:기본_400초과", "파일": "재료_유가전력.json"}, "MO000059": {"원문번호": "주택용저압:전력량_다음200", "파일": "재료_유가전력.json"}, "MO000060": {"원문번호": "주택용저압:전력량_400초과", "파일": "재료_유가전력.json"}, "MO000061": {"원문번호": "주택용저압:월간최저요금", "파일": "재료_유가전력.json"}, "MO000062": {"원문번호": "일반용갑I저압:기본", "파일": "재료_유가전력.json"}, "MO000063": {"원문번호": "일반용갑I저압:전력량_여름철", "파일": "재료_유가전력.json"}, "MO000064": {"원문번호": "일반용갑I저압:전력량_봄가을철", "파일": "재료_유가전력.json"}, "MO000065": {"원문번호": "일반용갑I저압:전력량_겨울철", "파일": "재료_유가전력.json"}, "MO000066": {"원문번호": "산업용갑I저압:기본", "파일": "재료_유가전력.json"}, "MO000067": {"원문번호": "산업용갑I저압:전력량_여름철", "파일": "재료_유가전력.json"}, "MO000068": {"원문번호": "산업용갑I저압:전력량_봄가을철", "파일": "재료_유가전력.json"}, "MO000069": {"원문번호": "산업용갑I저압:전력량_겨울철", "파일": "재료_유가전력.json"}, "MO000070": {"원문번호": "임시전력갑:기본요금적용", "파일": "재료_유가전력.json"}, "MO000071": {"원문번호": "임시전력갑:전력량요금적용", "파일": "재료_유가전력.json"}, "MO000072": {"원문번호": "임시전력갑:월간최저요금", "파일": "재료_유가전력.json"}, "GC001145": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001148": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001151": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001156": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001159": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001166": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)"},
"키": {
"CC000001": {"원문번호": "공통 1-3-1 콘크리트 및 포장용 재료", "파일": "계수_건설품셈_01장_적용기준.json"},
@@ -1341,6 +1341,7 @@
"EQ000652": {"원문번호": "2-2-11 부착용집게(0.2~0.8㎥)", "파일": "기계.json"},
"EQ000653": {"원문번호": "2-2-11 트럭탑재형크레인", "파일": "기계.json"},
"EQ000654": {"원문번호": "2-2-11 타이어형크레인", "파일": "기계.json"},
"EQ000655": {"원문번호": "10-9 덤프트럭(4륜구동) 1ton", "파일": "기계.json"},
"FX000001": {"원문번호": "0000001", "파일": "환율_한국은행환율.json"},
"FX000002": {"원문번호": "0000002", "파일": "환율_한국은행환율.json"},
"FX000003": {"원문번호": "0000003", "파일": "환율_한국은행환율.json"},
@@ -2870,6 +2871,7 @@
"GF000334": {"원문번호": "8-3 원목 박피", "파일": "로직_산림품셈_08장_방제.json"},
"GF000335": {"원문번호": "10-1", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
"GF000336": {"원문번호": "10-2", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
"GF000337": {"원문번호": "2-2-1 체인톱 시간당 기계경비", "파일": "로직_산림품셈_02장_소요재료_및_기계손료.json"},
"GX000001": {"원문번호": "13-4-1 돌쌓기 메쌓기(인력)", "파일": "로직_자체.json"},
"GX000002": {"원문번호": "12-34-4 암거이음받침 채움재(T=20㎜)", "파일": "로직_자체.json"},
"GX000003": {"원문번호": "13-4-1 돌쌓기 메쌓기(인력) (2)", "파일": "로직_자체.json"},
+19
View File
@@ -39702,6 +39702,25 @@
"값": {},
"출처": {},
"비고": "「건설공사 표준품셈 공통부문, 8-3-3 [20]운반 및 하역기계, (2104)크레인(타이어)(2025)」 적용"
},
{
"키": "EQ000655",
"원문번호": "10-9",
"원문": "산림품셈",
"구분": "임업기계",
"상세구분": "덤프트럭",
"이름": "덤프트럭(4륜구동)",
"규격": "1ton",
"단위": {
"가격": "천원"
},
"값": {
"가격": 28000
},
"출처": {
"가격": "산림품셈 10-9"
},
"비고": "원문 이름: 4륜구동 1톤 덤프트럭 · 중기가격 = 조달청 등재가격(기본값 28,000 천원 = EST 2026-07 「덤프트럭 1.0Ton」 · 바꿀 수 있음) · 작업량은 건설품셈 · 손료 및 운전경비 = 2.5톤 덤프트럭(0602-0025) · 주 연료 = 2.5톤의 80% · 시간당 경비 = GF000145"
}
]
}
@@ -0,0 +1,114 @@
{
"그룹": "로직",
"원문": "산림품셈",
"차례": 0,
"판": "2026-01-01",
"줄": [
{
"키": "GF000337",
"원문번호": "2-2-1",
"구분": "산림품셈",
"상세구분": "02장 소요재료 및 기계손료",
"이름": "체인톱 시간당 기계경비",
"결과단위": "원/시간",
"출처": "산림품셈 2-1-1 · 2-2-1",
"부품": {
"갈래": "기계경비식"
},
"개정": [
2026
],
"소유": "공용",
"비고": "1일 1대당(2-1-1 주연료 · 체인오일 · 잡품 · 2-2-1 손료계수) × 시간환산(1일 8시간) · 체인톱 가격 = 재료 「동력체인톱」 · 체인오일 = 재료 「작동유」(GF000076 체인톱 줄과 같음) · 가져온 시간당 「엔진톱」 줄을 잇는 식",
"입력": [
{
"이름": "지역",
"고르기": [
"전국평균",
"전국",
"서울",
"경기",
"강원",
"충북",
"충남",
"전북",
"경북",
"경남",
"부산",
"제주",
"대구",
"인천",
"대전",
"울산",
"세종",
"전남광주"
],
"출처": "건설품셈 공통 8-1-7 5"
}
],
"중간": [
{
"이름": "시간환산",
"식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모",
"출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)"
}
],
"호표": [
{
"종류": "재료",
"요소": "MO:휘발유:{지역}",
"이름": "휘발유(체인톱)",
"단위": "ℓ",
"수량": "찾기(QF000001, 기계명='체인톱', 재료명='보통휘발유(주연료)').주연료 * 시간환산",
"비목": "재료비",
"출처": "산림품셈 2-1-1 주③"
},
{
"종류": "재료",
"요소": {
"구분": "기계·설비",
"상세구분": "기계류",
"검색어": "작동유"
},
"이름": "체인오일",
"단위": "ℓ",
"수량": "찾기(QF000001, 기계명='체인톱', 재료명='체인오일(일반오일)').주연료 * 시간환산",
"비목": "재료비",
"출처": "산림품셈 2-1-1 주④"
},
{
"종류": "재료",
"요소": {
"구분": "기계·설비",
"상세구분": "기계류",
"검색어": "동력체인톱"
},
"이름": "체인톱 손료",
"단위": "대",
"수량": "EQ000620.손료계수 * 시간환산",
"비목": "경비",
"출처": "산림품셈 2-2-1"
}
],
"덧줄": [
{
"이름": "잡품",
"식": "줄.'휘발유(체인톱)' * 찾기(QF000001, 기계명='체인톱', 재료명='보통휘발유(주연료)').잡품 / 100",
"비목": "재료비",
"출처": "산림품셈 2-1-1 주⑤"
}
],
"끝수": null,
"변수표": {
"지역": {
"기호": "S",
"아래": "지역"
},
"시간환산": {
"기호": "Q",
"아래": "시간환산"
}
}
}
]
}
@@ -522,7 +522,7 @@
"부품": { "갈래": "기계경비식" },
"개정": [2026],
"소유": "공용",
"비고": "손료·운전경비는 2.5톤(0602-0025) · 주연료 2.5톤의 80% · 중기가격 조달청 등재가격은 요소 없음 · 작업량(건설품셈 덤프트럭 식)·10-9-1 운반량 쓰는 식 요소 없음 · 주② 제잡비 비율 원문에 없음",
"비고": "손료·운전경비는 2.5톤(0602-0025) · 주연료 2.5톤의 80% · 중기가격 조달청 등재가격 = 기계표 EQ000655 덤프트럭(4륜구동) 1ton 가격 · 작업량(건설품셈 덤프트럭 식)·10-9-1 운반량 쓰는 식 요소 없음 · 주② 제잡비 비율 원문에 없음",
"입력": [
{
"이름": "지역",
@@ -531,7 +531,7 @@
}
],
"중간": [
{ "이름": "손료", "식": "버림(EQ000069.가격 * 1000 * (EQ000069.상각비계수 + EQ000069.정비비계수 + EQ000069.관리비계수) / 10000000, 0)", "출처": "산림품셈 10-9 · 건설품셈 공통 8-1-5 12" },
{ "이름": "손료", "식": "버림(EQ000655.가격 * 1000 * (EQ000069.상각비계수 + EQ000069.정비비계수 + EQ000069.관리비계수) / 10000000, 0)", "출처": "산림품셈 10-9 · 건설품셈 공통 8-1-5 12" },
{ "이름": "비율", "식": "찾기(QF000330, 구분='2.5톤 덤프 주 연료 대비').비율", "출처": "산림품셈 10-9" },
{ "이름": "직종", "식": "EQ000069.조종원직종" },
{ "이름": "시간환산", "식": "찾기(CC000336, 구분='시간환산').분자 / 찾기(CC000336, 구분='시간환산').분모", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)" }
@@ -0,0 +1,44 @@
{
"그룹": "수식문서",
"로직파일": "로직_산림품셈_02장_소요재료_및_기계손료.json",
"줄": [
{
"꼴": "수식문서",
"판": 1,
"키": "GF000337",
"이름": "체인톱 시간당 기계경비",
"결과단위": "원/시간",
"출처": "산림품셈 2-1-1 · 2-2-1",
"부품": {
"갈래": "기계경비식"
},
"변수": [
{"열쇠": "지역", "기호": "S", "아래": "지역", "위": "", "이름": "지역", "단위": "", "출처": "건설품셈 공통 8-1-7 5", "연결": {"갈래": "선택", "목록": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"], "값": "전국평균"}},
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)", "연결": {"갈래": "식"}},
{"열쇠": "Q_분자^표", "기호": "Q", "아래": "분자", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분자", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분자"}},
{"열쇠": "Q_분모^표", "기호": "Q", "아래": "분모", "위": "표", "이름": "건설기계 조종원 노임 시간 환산 계수.분모", "단위": "", "연결": {"갈래": "표", "표": "CC000336", "조건": {"구분": "'시간환산'"}, "칸": "분모"}},
{"열쇠": "Q_주연료^표", "기호": "Q", "아래": "주연료", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.주연료", "단위": "수(단위는 줄의 단위 칸 따름)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'체인톱'", "재료명": "'보통휘발유(주연료)'"}, "칸": "주연료"}},
{"열쇠": "P_휘발유", "기호": "P", "아래": "휘발유", "위": "", "이름": "휘발유단가", "단위": "", "연결": {"갈래": "요소", "키": "MO:휘발유:{S_지역}"}},
{"열쇠": "Q_주연료2^표", "기호": "Q", "아래": "주연료2", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.주연료", "단위": "수(단위는 줄의 단위 칸 따름)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'체인톱'", "재료명": "'체인오일(일반오일)'"}, "칸": "주연료"}},
{"열쇠": "P_체인오일", "기호": "P", "아래": "체인오일", "위": "", "이름": "체인오일단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "작동유"}, "단위": "ℓ"}},
{"열쇠": "k_체인톱", "기호": "k", "아래": "체인톱", "위": "", "이름": "EQ000620.손료계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000620", "칸": "손료계수"}},
{"열쇠": "P_체인톱손료", "기호": "P", "아래": "체인톱손료", "위": "", "이름": "체인톱 손료단가", "단위": "", "연결": {"갈래": "요소", "재료": {"구분": "기계·설비", "상세구분": "기계류", "검색어": "동력체인톱"}, "단위": "대"}},
{"열쇠": "Q_잡^표", "기호": "Q", "아래": "잡", "위": "표", "이름": "체인톱·예취기·아키아윈치·2드럼 케이블윈치·천공기 연료.잡품", "단위": "%(주연료비의)", "연결": {"갈래": "표", "표": "QF000001", "조건": {"기계명": "'체인톱'", "재료명": "'보통휘발유(주연료)'"}, "칸": "잡품"}}
],
"식": [
{"기호": "Q_시간환산", "식": "Q_분자^표 ÷ Q_분모^표"}
],
"금액": [
{"이름": "휘발유(체인톱)", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료^표 × Q_시간환산", "단가": "P_휘발유", "출처": "산림품셈 2-1-1 주③", "기호": "C_휘발유"},
{"이름": "체인오일", "단위": "ℓ", "비목": "재료비", "수량": "Q_주연료2^표 × Q_시간환산", "단가": "P_체인오일", "출처": "산림품셈 2-1-1 주④"},
{"이름": "체인톱 손료", "단위": "대", "비목": "경비", "수량": "k_체인톱 × Q_시간환산", "단가": "P_체인톱손료", "출처": "산림품셈 2-2-1"},
{"이름": "잡품", "단위": "식", "비목": "재료비", "단가": "C_휘발유 × Q_잡^표 ÷ 100", "출처": "산림품셈 2-1-1 주⑤"}
],
"끝수": {
"줄": {"방법": "버림", "자리": 1},
"비목": {"방법": "버림", "자리": 0},
"끝전": "비목버림"
}
}
]
}
@@ -482,7 +482,7 @@
{"열쇠": "비율", "기호": "r", "아래": "비율", "위": "", "이름": "비율", "단위": "", "출처": "산림품셈 10-9", "연결": {"갈래": "표", "표": "QF000330", "조건": {"구분": "'2.5톤 덤프 주 연료 대비'"}, "칸": "비율"}},
{"열쇠": "직종", "기호": "S", "아래": "직종", "위": "", "이름": "직종", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "조종원직종"}},
{"열쇠": "시간환산", "기호": "Q", "아래": "시간환산", "위": "", "이름": "시간환산", "단위": "", "출처": "건설품셈 공통 1-1-3 2(일일 작업시간 8시간)", "연결": {"갈래": "식"}},
{"열쇠": "P_덤프트럭", "기호": "P", "아래": "덤프트럭", "위": "", "이름": "EQ000069.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "가격"}},
{"열쇠": "P_덤프트럭", "기호": "P", "아래": "덤프트럭", "위": "", "이름": "EQ000655.가격", "단위": "", "연결": {"갈래": "요소", "키": "EQ000655", "칸": "가격"}},
{"열쇠": "k_덤프트럭", "기호": "k", "아래": "덤프트럭", "위": "", "이름": "EQ000069.상각비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "상각비계수"}},
{"열쇠": "k_덤프트럭2", "기호": "k", "아래": "덤프트럭2", "위": "", "이름": "EQ000069.정비비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "정비비계수"}},
{"열쇠": "k_덤프트럭3", "기호": "k", "아래": "덤프트럭3", "위": "", "이름": "EQ000069.관리비계수", "단위": "", "연결": {"갈래": "요소", "키": "EQ000069", "칸": "관리비계수"}},
+3 -3
View File
@@ -1,7 +1,7 @@
"""812-9 지킴 — 가져온 기계 잇기(`M02_Machine_Find` · 요구 718 · 719).
(a) 기계표 전체에서 찾음 — 낱말표 밖 기종(로우더 · 공기 압축기 · 램 머 · 라인마커 …) · 후보 여럿이던 것
(물탱크 · 크레인(무한궤도) · 머캐덤 · 콘크리트 펌프차 …) · 지어 맞추기 없음(1.0Ton = 우리 표에 없음)
(물탱크 · 크레인(무한궤도) · 머캐덤 · 콘크리트 펌프차 …) · 지어 맞추기 없음(3.0Ton = 우리 표에 없음)
(b) 표기 정규화 · 괄호 낱말(무한궤도 · 타이어 · 일반용 · 표에 없는 낱말 발파 = 안 봄) · 범위 · 복수 규격
(c) 조합 기계 = 둘(대형브레이카 +굴착기 · 0.7+브레이카 · +부착용집게 · +진동콤팩터 · 덤프트럭(자동덮개))
(d) 별칭 먼저 — M01 기계 줄 「별칭」 칸(815-1 · 시험 안 임시 별칭) · 이름 + 규격 통째 = 그 EQ · 이름만 =
@@ -104,7 +104,7 @@ def test_master_alias(name: str, spec: str, eq: str) -> None:
def test_no_guess() -> None:
"""우리 표에 없는 규격 = 못 찾음(가까운 규격으로 안 이음) · 기종 모름 = 별칭 몫 · 여럿 = 여럿."""
got = mf.find("덤프트럭", "1.0Ton")
got = mf.find("덤프트럭", "3.0Ton")
assert got["판정"] == mf.NONE and got["까닭"] == "우리 표에 없음(규격)" and got["키"] is None
assert got["후보"][0]["규격"] == "2.5ton" # 보여주기만 — 가까운 규격 먼저
for name, spec in (("에어 호스", "1.91cm"), ("모노펌프", "5-20㎥/hr")): # 승인 전 = 별칭 몫
@@ -113,7 +113,7 @@ def test_no_guess() -> None:
got = mf.find("크레인(트럭)", "10Ton") # 「트럭」 = 기계표 괄호에 없는 낱말 → 안 봄
assert got["판정"] == mf.MANY and got["수"] == 2
assert mf.mark(got, "크레인(트럭)", "10Ton").startswith("참고 · 기계 후보 2 — 고르기")
assert "기계 못 찾음" in mf.mark(mf.find("덤프트럭", "1.0Ton"), "덤프트럭", "1.0Ton")
assert "기계 못 찾음" in mf.mark(mf.find("덤프트럭", "3.0Ton"), "덤프트럭", "3.0Ton")
assert mf.mark(mf.find("물탱크", "5500ℓ"), "물탱크", "5500ℓ") is None
+85
View File
@@ -0,0 +1,85 @@
"""816-1 지킴 — 기계표 「덤프트럭(4륜구동) 1ton」(산림품셈 10-9 · 요구 769-1 · 770-1).
(a) 기계표 줄 · 값 — 원문 산림품셈 10-9 · 가격 28,000 천원(조달청 등재가격 기본값) · 키대장
(b) GF000145(4륜구동 1톤 덤프트럭 시간당 기계경비)가 그 줄 가격을 읽음 · 계수 · 연료 · 조종원은 2.5 ton
(c) 잇기 — 가져온 「덤프트럭 1.0Ton · 1톤 · 1ton」 → 새 줄 · 「덤프트럭 2.5ton」 → EQ000069 그대로
(d) 시간당 경비 한 길 — `M02_Machine_Hourly.hourly` · `M02_Doc_Import.hourly_link` = GF000145
"""
from __future__ import annotations
import importlib
import sys
from decimal import Decimal
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
for p in (ROOT, ROOT / "resources/master_data/scripts"):
if str(p) not in sys.path:
sys.path.insert(0, str(p))
from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402
from M02_MasterTemplete import M02_Doc_Import as di # noqa: E402
from M02_MasterTemplete import M02_Machine_Find as mf # noqa: E402
from M02_MasterTemplete import M02_Machine_Hourly as mh # noqa: E402
ml = importlib.import_module("master_doc_link")
mc = importlib.import_module("master_doc_calc")
NEW, OLD = "EQ000655", "EQ000069"
def _whole():
return m01.loaded()[1]
def _docs() -> dict:
return ml.load_all()
def test_row() -> None:
row = _whole().get(NEW)
assert (row["원문"], row["원문번호"], row["상세구분"]) == ("산림품셈", "10-9", "덤프트럭")
assert (row["이름"], row["규격"]) == ("덤프트럭(4륜구동)", "1ton")
assert row["값"] == {"가격": 28000} and row["출처"]["가격"] == "산림품셈 10-9"
assert "4륜구동 1톤 덤프트럭" in row["비고"]
def test_gf145_reads_price() -> None:
doc = _docs()["GF000145"]
reads = {(v["연결"].get("키"), v["연결"].get("칸")) for v in doc["변수"]}
assert (NEW, "가격") in reads and (OLD, "가격") not in reads
assert {
(OLD, c) for c in ("상각비계수", "정비비계수", "관리비계수", "주연료", "조종원")
} <= reads
got = mc.calc(doc, ml.Links(_whole(), _docs()))
old = _whole().get(OLD)["값"]
loss = int(Decimal(28000 * 1000) * sum(Decimal(str(old[k])) for k in ("상각비계수", "정비비계수", "관리비계수")) / Decimal(10**7)) # fmt: skip
assert got["셈됨"] and got["경비"] == loss == 8307
def test_find() -> None:
for spec in ("1.0Ton", "1톤", "1ton"):
got = mf.find("덤프트럭", spec)
assert got["키"] == NEW and got["판정"] == mf.LINKED, spec
assert mf.find("덤프트럭", "2.5ton")["키"] == OLD
assert mf.find("덤프트럭", "15Ton")["키"] == "EQ000074"
def test_hourly_one_way() -> None:
assert mh.logic_of("10-9")["키"] == "GF000145"
assert mh.logic_of("0602-0025")["키"] != "GF000145" # 2.5 ton = 일반 식 그대로
got = mh.hourly(NEW)
doc = mc.calc(_docs()["GF000145"], ml.Links(_whole(), _docs()))
assert got["ok"] and got["로직"] == "GF000145"
assert [got[k] for k in ("노무비", "재료비", "경비", "계")] == [float(doc[k]) for k in ("노무비", "재료비", "경비", "계")] # fmt: skip
link = di.hourly_link(NEW, "경비")
assert link["키"] == "GF000145" and "기계" not in link["입력"] and link["칸"] == "경비"
def test_gf283_option() -> None:
"""10-9-1 덤프트럭 운반 「1톤 4륜구동」 옵션 = GF000145 값으로 셈."""
links = ml.Links(_whole(), _docs())
one = mc.calc(_docs()["GF000283"], links, {"덤프트럭": "1톤 4륜구동"})
big = mc.calc(_docs()["GF000283"], links, {"덤프트럭": "0602-0025"})
assert one["셈됨"] and big["셈됨"] and one["노무비"] == big["노무비"] # 조종원 = 2.5 ton
assert one["경비"] > big["경비"] # 손료 = 1 톤 가격 28,000 > 2.5 ton 21,982
@@ -0,0 +1,129 @@
"""816-2 지킴 — 「후보 여럿」 기계를 같은 파일 안 구성 대조로 가름(요구 770-2 · 773-3 · 방향 장부 N1).
잣대 = 같은 파일 · 같은 때의 값끼리(손료 계수 · 가격 · 주연료 · 조종원) — 우리 기계표 값과 맞대지 않음.
(a) EST 원파일 MachMainDat 묶기 — main 기계 1,049 대 모두 구성
(b) 견본 셋 — 대형브레이카 +굴착기(1.0㎥) → 같은 파일 굴착기(무한궤도) 1.0㎥ → EQ000022 · 크레인(트럭) 10Ton →
크레인(타이어) 10Ton → EQ000128 · 굴삭기(유압식백호) 0.6㎥ → EQ000019
(c) 파일 값이 우리 표와 달라도 같은 파일 대조로 가름 · 우리 표 값과 같다는 것만으로는 안 이음 · 같은 구성
없음 · 여럿 = 그대로
(d) EST 엑셀 「기계경비」 시트 길 — 산출근거 줄 → 같은 대조 · 2016 사방 엑셀 기계 12 대 구성 읽음
"""
from __future__ import annotations
import sys
from functools import lru_cache
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
HERE = Path(__file__).resolve().parent
for p in (ROOT, HERE):
if str(p) not in sys.path:
sys.path.insert(0, str(p))
from M02_MasterTemplete import M02_Machine_Find as mf # noqa: E402
from M02_MasterTemplete import M02_San_Book as book # noqa: E402
from M02_MasterTemplete import M02_San_Emit as em # noqa: E402
from M02_MasterTemplete import M02_San_Estx as estx # noqa: E402
from M02_MasterTemplete import M02_San_Xlsx as sx # noqa: E402
MAIN = ROOT / "resources/knowledge/original/경쟁사 프로그램/EST Plus 2/다산소프트/2026-07-main.estx"
COMBO = ("대형브레이카", "+굴착기(1.0㎥)")
@lru_cache(maxsize=1)
def _tables() -> dict:
return estx.tables(MAIN.read_bytes())
@lru_cache(maxsize=1)
def _main() -> dict:
res = estx.resources(_tables()).values()
return {(r["이름"], r["규격"]): r for r in res if r["표"] == "MachTitleDat"}
def _keys(name: str, spec: str, same: list | None) -> list:
return [(p["판정"], p["키"]) for p in em.machine_parts({"갈래": "기계", "이름": name, "규격": spec}, same)] # fmt: skip
def test_main_makes() -> None:
made = estx.makes(_tables())
assert len(made) == 1049 and all(m["손료"] for m in made.values())
code = next(r["Code"] for r in _tables()["MachTitleDat"] if (r["Name"], r["Size"]) == COMBO)
assert made[code] == {"손료": [[2085.0, 142069000.0], [6601.0, 28692000.0]], "주연료": 19.5, "조종원": True} # fmt: skip
assert ["굴착기(무한궤도)", "1.0㎥"] in _main()[COMBO]["같은구성"]
def test_samples() -> None:
for (name, spec), want in (
(COMBO, "EQ000022"),
(("크레인(트럭)", "10Ton"), "EQ000128"),
(("굴삭기(유압식백호)", "0.6㎥"), "EQ000019"),
):
same = _main()[(name, spec)]["같은구성"]
assert any(v == mf.MANY for v, _ in _keys(name, spec, None)), name
after = _keys(name, spec, same)
assert (mf.LINKED, want) in after and all(v != mf.MANY for v, _ in after), name
got = em.machine_parts({"갈래": "기계", "이름": "크레인(트럭)", "규격": "10Ton"}, _main()[("크레인(트럭)", "10Ton")]["같은구성"]) # fmt: skip
assert got[0]["까닭"].startswith(mf.MADE) and "크레인(타이어)" in got[0]["까닭"]
def test_file_values_only() -> None:
"""파일 값이 우리 표와 달라도 같은 파일 대조로 가름 · 우리 표 값과 같아도 같은 파일에 짝 없으면 그대로."""
odd = {"손료": [[9999.0, 1.0]], "주연료": 1.0, "조종원": True} # 우리 표에 없는 값
files = {
"a": ("굴착기", "1.0㎥", odd),
"b": ("굴착기(무한궤도)", "1.0㎥", odd),
"c": ("굴착기(타이어)", "1.0㎥", {**odd, "주연료": 2.0}),
}
same = mf.peers(files)
assert same["a"] == [["굴착기(무한궤도)", "1.0㎥"]]
assert _keys("굴착기", "1.0㎥", same["a"]) == [(mf.LINKED, "EQ000022")]
ours = {
"손료": [[2085.0, 142069000.0]],
"주연료": 19.5,
"조종원": True,
} # 우리 표 무한궤도 값 그대로
alone = mf.peers({"a": ("굴착기", "1.0㎥", ours), "c": ("굴착기(타이어)", "1.0㎥", odd)})
assert "a" not in alone and _keys("굴착기", "1.0㎥", alone.get("a")) == [(mf.MANY, None)]
both = mf.peers(
{**files, "c": ("굴착기(타이어)", "1.0㎥", odd)}
) # 같은 구성 둘(형식 다름) = 여럿
assert _keys("굴착기", "1.0㎥", both["a"])[0][0] == mf.MANY
def test_keep_when_unsure() -> None:
same = _main()[("굴삭기(유압식백호)", "1.0㎥;암보정")].get(
"같은구성"
) # 같은 파일 무한궤도(발파) 2,405
assert ["굴착기(무한궤도)", "1.0㎥(발파)"] in same
assert _keys("굴삭기(유압식백호)", "1.0㎥;암보정", same) == [(mf.LINKED, "EQ000022")]
assert _keys("크레인(트럭)", "10Ton", None) == [(mf.MANY, None)]
def test_excel_sheet() -> None:
rows = [
["기계경비"],
["명 칭", "규 격", "산 출 근 거"],
["1. 굴삭기", "1.0㎥", 0.0, 28259.0],
["손 료", 100000000.0, "₩×", 2038.0, "×", "10(-7)", 20380.0],
["주연료", "경유", 19.5, "ℓ", "×", 993.0, 19363.5],
["건설기계운전사", 1.0, "인", "×", 135644.0],
[],
["2. 굴삭기(무한궤도)", "1.0㎥", 0.0, 28259.0],
["손 료", 100000000.0, "₩×", 2038.0, "×", "10(-7)", 20380.0],
["주연료", "경유", 19.5, "ℓ", "×", 993.0, 19363.5],
["건설기계운전사", 1.0, "인", "×", 135644.0],
[],
]
made = sx.makes(rows)
assert made[("굴삭기", "1.0㎥")][2] == {"손료": [[2038.0, 100000000.0]], "주연료": 19.5, "조종원": True} # fmt: skip
same = mf.peers(made)[("굴삭기", "1.0㎥")]
assert same == [["굴삭기(무한궤도)", "1.0㎥"]]
assert _keys("굴삭기", "1.0㎥", same) == [(mf.LINKED, "EQ000022")]
import test_797_1a_import_flags as practice
sad = practice.FILES.get("2016사방")
if sad:
sheets = book.sheets(sad.name, sad.read_bytes())
assert len(sx.makes(sheets["기계경비"])) == 12
+71
View File
@@ -0,0 +1,71 @@
"""816-3 지킴 — 가져온 「엔진톱」 = 우리 체인톱 EQ000620(요구 771 · 방향 장부 N2).
(a) 별칭 길 — 사용자가 EQ000620 「별칭」 칸에 「엔진톱 18", 엔진톱」 을 넣으면(시험 = 임시 별칭 · 마스터엔
안 넣음) 인치 따옴표가 깨지지 않고 「엔진톱 18"」 · 「엔진톱」(규격 빔) → EQ000620 · 다른 톱 · 다른 규격은 안 이음
(b) 시간당 = GF000337 「체인톱 시간당 기계경비」 = 산림품셈 1 일 1 대당(2-1-1 휘발유 · 체인오일 · 잡품 ·
2-2-1 손료) ÷ 8 — GF000076 체인톱 줄(1 일 1 대당) 단가 그대로 · 수량 × 시간환산
(c) 이음 한 길 — `M02_Machine_Hourly.hourly` · `M02_Doc_Import.hourly_link` = GF000337(기계 입력 없음)
"""
from __future__ import annotations
import importlib
import sys
from decimal import Decimal
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
for p in (ROOT, ROOT / "resources/master_data/scripts"):
if str(p) not in sys.path:
sys.path.insert(0, str(p))
from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402
from M02_MasterTemplete import M02_Doc_Import as di # noqa: E402
from M02_MasterTemplete import M02_Machine_Find as mf # noqa: E402
from M02_MasterTemplete import M02_Machine_Hourly as mh # noqa: E402
from test_815_1_import_link import aliases # noqa: E402 — M01 별칭 칸 임시
ml = importlib.import_module("master_doc_link")
mc = importlib.import_module("master_doc_calc")
SAW = "EQ000620"
def _calc(key: str) -> dict:
docs = ml.load_all()
return mc.calc(docs[key], ml.Links(m01.loaded()[1], docs))
def test_alias() -> None:
am = importlib.import_module("master_alias")
assert not m01.loaded()[1].get(SAW).get("별칭") # 별칭 값 = 사용자 몫
assert mf.find("엔진톱", '18"')["키"] is None
for text in ('엔진톱 18", 엔진톱', '"엔진톱 18"", 엔진톱'):
assert am.names(text) == ['엔진톱 18"', "엔진톱"], text
with aliases("EQ", {SAW: '엔진톱 18", 엔진톱'}):
for name, spec in (("엔진톱", '18"'), ("엔진톱", ""), ("엔진톱", '18 "')):
got = mf.find(name, spec)
assert (got["판정"], got["키"]) == (mf.ALIAS, SAW), (name, spec)
for name, spec in (("엔진톱", '24"'), ("체인톱", '18"')):
assert mf.find(name, spec)["키"] is None, (name, spec)
def test_hourly_is_day_over_8() -> None:
hour = _calc("GF000337")
day = {x["이름"]: x for x in _calc("GF000076")["줄"]} # 같은 체인톱 줄(1 일 1 대당 × 벌목부수)
lines = {x["이름"]: x for x in hour["줄"]}
assert set(lines) == {"휘발유(체인톱)", "체인오일", "체인톱 손료", "잡품"}
for name in ("휘발유(체인톱)", "체인오일", "체인톱 손료"):
assert lines[name]["단가"] == day[name]["단가"], name
assert lines["휘발유(체인톱)"]["수량"] == Decimal("5.6") / 8
assert lines["체인톱 손료"]["수량"] == Decimal("0.0084") / 8
assert hour["셈됨"] and hour["노무비"] == 0 and hour["계"] > 0
def test_one_way() -> None:
got = mh.hourly(SAW)
doc = _calc("GF000337")
assert got["ok"] and got["로직"] == "GF000337"
assert [got[k] for k in ("재료비", "경비", "계")] == [float(doc[k]) for k in ("재료비", "경비", "계")] # fmt: skip
link = di.hourly_link(SAW, "재료비")
assert link["키"] == "GF000337" and "기계" not in link["입력"]
assert mh.hourly("EQ000621")["ok"] is False # 예취기 = 시간당 식 없음 그대로