feat(B09): 뭉친 이름+갈래 열 표 읽기 — 돌쌓기 계열 성립
메인이 `reference` 오판 41건을 되살려(bc4b6837) 돌쌓기(장비)가 마스터에
들어왔으나, 그 표는 **이름도 값도 뭉쳐 오고 열이 규격 갈래**인 또 다른 모양이라
여전히 안 섰음 (「석공 보통인부 | 0.09 0.05 | 0.08 0.04 | 0.07 0.03」)
- 뭉친 이름·값을 갈래 열에 짝지어 읽음. 개수가 어긋나면 표째 버림
- 「굴착기+부착용 집게」는 **두 기종 조합**으로 보아 같은 시간을 둘 다 붙임
(TODO(미결) 해석 잠정 — 사용자 확인 대기)
- 규격이 **옆 칸**에 있고 갈래(무한궤도)가 안 적힌 기종은 규격 일치 + 무한궤도
우선으로 잠정 채택. 카탈로그 규격이 범위(0.6∼0.8)면 그 안에 드는지로 판정
- ⚠ 「1.04(1.17)」 괄호값은 **조건 시공 시 대안값**임이 원문에서 확인됨
(13-6-1 [주]② 흡출방지재 시공 시 ( ) 값). 기본은 괄호 밖, 대안값은
`alternative_amount` 로 남겨 화면에 「시공 시 다름」으로 보이게 함
- 라벨 줄 판정을 **모든 칸**으로 — 첫 칸만 보면 기초잡석 12-25 를 라벨 줄로
오해해 표째 가로챘음(그 탓에 기초잡석이 한때 다시 막혔음)
- 갈래 키를 **공백 없는 것**으로 통일(`#보통`), 원문 문구는 이름에 보존
(두 창 합의)
결과: 자원 축 259 → 354, 일위대가 145 → 176
돌쌓기(찰) 35cm이하 58,166.7 / 55cm이하 52,938.7 / 75cm이하 46,892.7 원/㎡
메쌓기·찰쌓기·붙이기 계열도 직경 3갈래로 섬
검증: pytest 184 통과(신규 1). 화면 실측 — 지금 인계 자료에 구조물 줄이 없어
표에는 아직 안 뜸(B08 이 갈래를 골라 보내면 그대로 금액이 섬)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -350,6 +350,24 @@ def parse_amount_expression(cell: str) -> Decimal | None:
|
|||||||
return Decimal(found.group(1)) * Decimal(found.group(2)) / Decimal(100)
|
return Decimal(found.group(1)) * Decimal(found.group(2)) / Decimal(100)
|
||||||
|
|
||||||
|
|
||||||
|
#: 「1.04(1.17)」 — 괄호 밖이 기본, 괄호 안이 **조건 시공 시** 값.
|
||||||
|
#: 근거: 품셈 13-6-1 [주]② 「흡출방지재를 시공하는 경우는 ( )의 값을 적용한다」
|
||||||
|
#: (13-6-2·13-6-3·13-7-1·13-7-2 도 같은 [주]).
|
||||||
|
#: ⚠ **기본은 괄호 밖** — 방지재 시공 여부가 설계 조건에 아직 없다(사용자 확정 대기).
|
||||||
|
#: 괄호 값을 쓰려면 그 조건이 들어와야 하므로, 지금은 값과 함께 **대안값을 남겨** 둔다.
|
||||||
|
_RE_ALTERNATIVE = re.compile(r"^(\d+(?:\.\d+)?)\s*[((](\d+(?:\.\d+)?)[))]$")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_amount_pair(cell: str) -> tuple[Decimal, Decimal | None] | None:
|
||||||
|
"""「1.04(1.17)」 → `(1.04, 1.17)`. 괄호가 없으면 `(값, None)`."""
|
||||||
|
text = _normalize(cell)
|
||||||
|
found = _RE_ALTERNATIVE.match(text)
|
||||||
|
if found:
|
||||||
|
return Decimal(found.group(1)), Decimal(found.group(2))
|
||||||
|
plain = parse_amount(text)
|
||||||
|
return None if plain is None else (plain, None)
|
||||||
|
|
||||||
|
|
||||||
def convert_amount(raw: Decimal, *, pum_form: str, basis_quantity: Decimal | None) -> Decimal:
|
def convert_amount(raw: Decimal, *, pum_form: str, basis_quantity: Decimal | None) -> Decimal:
|
||||||
"""표 형태에 맞춰 소요량으로 환산한다.
|
"""표 형태에 맞춰 소요량으로 환산한다.
|
||||||
|
|
||||||
@@ -383,6 +401,9 @@ class ResourceRow:
|
|||||||
amount: Decimal
|
amount: Decimal
|
||||||
amount_unit: str
|
amount_unit: str
|
||||||
raw_row_index: int
|
raw_row_index: int
|
||||||
|
#: 조건 시공 시 쓰는 대안값 — 「1.04(1.17)」의 1.17 (품셈 13-6-1 [주]②).
|
||||||
|
#: **기본값은 `amount`(괄호 밖)** 이고 이 값은 화면에 「시공 시 다름」으로 보인다.
|
||||||
|
alternative_amount: Decimal | None = None
|
||||||
#: 규격 갈래 — 열이 자원인 표에서 행 이름(「무근구조물」). 없으면 빈 문자열.
|
#: 규격 갈래 — 열이 자원인 표에서 행 이름(「무근구조물」). 없으면 빈 문자열.
|
||||||
#: **갈래마다 품이 다르므로 한 일위대가로 뭉치지 않는다.**
|
#: **갈래마다 품이 다르므로 한 일위대가로 뭉치지 않는다.**
|
||||||
variant: str = ""
|
variant: str = ""
|
||||||
@@ -404,6 +425,9 @@ class ResourceRow:
|
|||||||
"amount_unit": self.amount_unit,
|
"amount_unit": self.amount_unit,
|
||||||
"raw_row_index": self.raw_row_index,
|
"raw_row_index": self.raw_row_index,
|
||||||
"variant": self.variant,
|
"variant": self.variant,
|
||||||
|
"alternative_amount": (
|
||||||
|
None if self.alternative_amount is None else str(self.alternative_amount)
|
||||||
|
),
|
||||||
"group_ratio_pct": None if self.group_ratio_pct is None else str(self.group_ratio_pct),
|
"group_ratio_pct": None if self.group_ratio_pct is None else str(self.group_ratio_pct),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,6 +547,13 @@ def match_table(
|
|||||||
if match_transposed_table(node, table, catalog, result, basis_quantity, unit):
|
if match_transposed_table(node, table, catalog, result, basis_quantity, unit):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# 「석공 보통인부 | 0.09 0.05 | …」처럼 **이름도 값도 뭉쳐 오고 열이 갈래**인 표
|
||||||
|
# (돌쌓기 13-4 계열). 행-자원으로는 첫 이름조차 안 풀린다.
|
||||||
|
from B09_Estimation.B09_Estimation_ResourceAxis_Transposed import match_packed_rows
|
||||||
|
|
||||||
|
if match_packed_rows(node, table, catalog, result, basis_quantity, unit):
|
||||||
|
return
|
||||||
|
|
||||||
for index, row in enumerate(table.get("raw_row", [])):
|
for index, row in enumerate(table.get("raw_row", [])):
|
||||||
cells = [str(c) for c in row]
|
cells = [str(c) for c in row]
|
||||||
if not cells:
|
if not cells:
|
||||||
@@ -556,9 +587,13 @@ def match_table(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 숫자 셀이 없는 행은 자원 줄이 아니다(제목·설명 행) — 목록에 안 올린다.
|
# 숫자 셀이 없는 행은 자원 줄이 아니다(제목·설명 행) — 목록에 안 올린다.
|
||||||
amount_cell = next(
|
alternative: Decimal | None = None
|
||||||
(parse_amount(c) for c in value_cells if parse_amount(c) is not None), None
|
amount_cell = None
|
||||||
)
|
for cell in value_cells:
|
||||||
|
pair = parse_amount_pair(cell)
|
||||||
|
if pair is not None:
|
||||||
|
amount_cell, alternative = pair
|
||||||
|
break
|
||||||
if amount_cell is None:
|
if amount_cell is None:
|
||||||
# 「0.2 × 30%」 꼴은 값과 배분율이 한 칸에 있다 — 읽었으면 딱지 배분율은 버린다.
|
# 「0.2 × 30%」 꼴은 값과 배분율이 한 칸에 있다 — 읽었으면 딱지 배분율은 버린다.
|
||||||
expression = next(
|
expression = next(
|
||||||
@@ -638,6 +673,7 @@ def match_table(
|
|||||||
amount_unit=unit,
|
amount_unit=unit,
|
||||||
raw_row_index=index,
|
raw_row_index=index,
|
||||||
group_ratio_pct=group_ratio,
|
group_ratio_pct=group_ratio,
|
||||||
|
alternative_amount=alternative,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -196,6 +198,226 @@ def _match_two_row_table(
|
|||||||
return matched
|
return matched
|
||||||
|
|
||||||
|
|
||||||
|
#: 「석공 보통인부」처럼 **여러 자원이 한 칸에** 뭉쳐 오고, 값도 「0.09 0.05」로 뭉쳐 오며,
|
||||||
|
#: 열이 규격 갈래(35cm 이하 · 55cm 이하 · 75cm 이하)인 표. 돌쌓기 13-4 계열이 그 모양이다.
|
||||||
|
#:
|
||||||
|
#: ⚠ **개수가 하나라도 안 맞으면 표째 버린다** — 이름 2 개에 값 3 개면 어느 값이 누구
|
||||||
|
#: 것인지 단정할 수 없다. 오늘 여러 번 겪은 자리다.
|
||||||
|
_RE_PACKED_NUMBER = re.compile(r"\d+(?:\.\d+)?")
|
||||||
|
|
||||||
|
|
||||||
|
#: 「1.04(1.17)」의 괄호 값 — **조건 시공 시 대안값**이라 기본 수에 안 센다
|
||||||
|
#: (품셈 13-6-1 [주]②). 안 떼면 값이 하나 더 있는 것으로 보여 표가 통째로 버려진다.
|
||||||
|
_RE_ALTERNATIVE_TAIL = re.compile(r"(?<=\d)\s*[((]\s*\d+(?:\.\d+)?\s*[))]")
|
||||||
|
|
||||||
|
|
||||||
|
def _packed_numbers(cell: str) -> list:
|
||||||
|
text = _RE_ALTERNATIVE_TAIL.sub("", str(cell))
|
||||||
|
return [Decimal(t) for t in _RE_PACKED_NUMBER.findall(text)]
|
||||||
|
|
||||||
|
|
||||||
|
def _packed_names(cell: str) -> list[str]:
|
||||||
|
"""한 칸에 뭉친 이름들. 「굴착기+부착용 집게」는 **조합 기종**이라 통째로 둔다."""
|
||||||
|
text = " ".join(str(cell).split())
|
||||||
|
if "+" in text:
|
||||||
|
return [text]
|
||||||
|
return [part for part in text.split(" ") if part]
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_packed(catalog: ResourceCatalog, name: str, specs: list[str] | None = None) -> list:
|
||||||
|
"""이름 하나를 카탈로그 줄들로 푼다.
|
||||||
|
|
||||||
|
「굴착기+부착용 집게」처럼 **두 기종을 함께 쓰는 조합**은 둘 다 돌려준다 —
|
||||||
|
같은 시간을 둘이 함께 쓰므로 사용료도 둘 다 붙는다.
|
||||||
|
⚠ TODO(미결) 조합 표기의 해석은 잠정이다 — 사용자·메인 확인 대기(PLAN 9-6).
|
||||||
|
"""
|
||||||
|
parts = [part.strip() for part in str(name).split("+") if part.strip()]
|
||||||
|
found = []
|
||||||
|
for part in parts:
|
||||||
|
base, spec = split_name_and_spec(part)
|
||||||
|
entry = catalog.resolve(base, spec)
|
||||||
|
if entry is None and not spec:
|
||||||
|
candidates = catalog.by_name(base)
|
||||||
|
entry = candidates[0] if len(candidates) == 1 else None
|
||||||
|
if entry is None:
|
||||||
|
entry = _resolve_with_side_spec(catalog, base, specs or [])
|
||||||
|
if entry is None:
|
||||||
|
return []
|
||||||
|
found.append(entry)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
#: 갈래(무한궤도/타이어)를 안 적은 기종을 고를 때의 **잠정** 우선순위.
|
||||||
|
#: 품셈 13-4 는 「굴착기+부착용 집게 | 0.6㎥」로만 적는데 카탈로그는 갈래까지 나뉜다.
|
||||||
|
#: ⚠ TODO(미결) 임도 현장 표준이 무한궤도라 그쪽을 잠정 채택 — 사용자 확정 대기(PLAN 9-6).
|
||||||
|
_TRACK_PREFERENCE = ("무한궤도",)
|
||||||
|
|
||||||
|
|
||||||
|
_RE_SPEC_RANGE = re.compile(r"^(\d+(?:\.\d+)?)[∼~~-](\d+(?:\.\d+)?)$")
|
||||||
|
|
||||||
|
|
||||||
|
def _spec_matches(catalog_spec: str, wanted: str) -> bool:
|
||||||
|
"""카탈로그 규격이 표의 규격을 담는가.
|
||||||
|
|
||||||
|
카탈로그가 **범위**로 적는 경우가 있다 — 「부착용 집게 0.6∼0.8」은 0.6㎥ 를 담는다.
|
||||||
|
범위 밖이면 안 고른다.
|
||||||
|
"""
|
||||||
|
if catalog_spec == wanted or wanted.startswith(catalog_spec):
|
||||||
|
return True
|
||||||
|
found = _RE_SPEC_RANGE.match(catalog_spec)
|
||||||
|
if not found:
|
||||||
|
return False
|
||||||
|
numbers = _RE_PACKED_NUMBER.findall(wanted)
|
||||||
|
if not numbers:
|
||||||
|
return False
|
||||||
|
value = Decimal(numbers[0])
|
||||||
|
return Decimal(found.group(1)) <= value <= Decimal(found.group(2))
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_with_side_spec(catalog: ResourceCatalog, name: str, specs: list[str]):
|
||||||
|
"""이름에 갈래가 없고 규격이 **옆 칸**에 있는 기종을 고른다.
|
||||||
|
|
||||||
|
이름이 카탈로그 이름의 앞머리이고 규격이 **정확히 같을 때만** 고른다 —
|
||||||
|
규격이 다르면 안 고른다(엉뚱한 기종이 붙으면 사용료가 통째로 틀린다).
|
||||||
|
"""
|
||||||
|
if not name or not specs:
|
||||||
|
return None
|
||||||
|
wanted = {"".join(str(s).split()) for s in specs if str(s).strip()}
|
||||||
|
hits = []
|
||||||
|
for entry in catalog.entries:
|
||||||
|
if not entry.name.startswith(name):
|
||||||
|
continue
|
||||||
|
spec = "".join(str(entry.spec).split())
|
||||||
|
if spec and any(_spec_matches(spec, w) for w in wanted):
|
||||||
|
hits.append(entry)
|
||||||
|
if not hits:
|
||||||
|
return None
|
||||||
|
if len(hits) == 1:
|
||||||
|
return hits[0]
|
||||||
|
for word in _TRACK_PREFERENCE:
|
||||||
|
preferred = [e for e in hits if word in e.name]
|
||||||
|
if len(preferred) == 1:
|
||||||
|
return preferred[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def match_packed_rows(
|
||||||
|
node: dict,
|
||||||
|
table: dict,
|
||||||
|
catalog: ResourceCatalog,
|
||||||
|
result: AxisResult,
|
||||||
|
basis_quantity,
|
||||||
|
unit: str,
|
||||||
|
) -> bool:
|
||||||
|
"""뭉친 이름 + 갈래 열 표를 읽는다. 그런 표가 아니면 `False`."""
|
||||||
|
rows = [[str(c).strip() for c in row] for row in (table.get("raw_row") or [])]
|
||||||
|
if len(rows) < 2:
|
||||||
|
return False
|
||||||
|
|
||||||
|
labels = [cell for cell in rows[0] if cell]
|
||||||
|
# ⚠ 라벨에 수가 들어 있을 수 있다 — 「35cm 이하」. 수의 유무로 가르면 안 된다.
|
||||||
|
# 자원으로 **안 풀리는** 줄이면 갈래 라벨 줄로 본다.
|
||||||
|
if not labels:
|
||||||
|
return False
|
||||||
|
# ⚠ **첫 줄의 어느 칸이라도 자원이면 라벨 줄이 아니다.** 첫 칸만 보면 기초잡석
|
||||||
|
# 12-25 처럼 「소할(30%) | 할석공(인) | 0.2 × 30%」인 표를 라벨 줄로 오해해
|
||||||
|
# 표째 가로챈다(2026-09-08: 그 탓에 기초잡석이 다시 막혔다).
|
||||||
|
for cell in rows[0]:
|
||||||
|
if not cell:
|
||||||
|
continue
|
||||||
|
if any(_resolve_packed(catalog, name, []) for name in _packed_names(cell)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ⚠ **뭉친 표에만 쓴다.** 이 길이 넓으면 행-자원 표까지 가로채 자원 축이 줄어든다
|
||||||
|
# (2026-09-08 실측: 304 → 244 줄로 떨어졌다). 이름이 둘 이상 뭉쳤거나 값이 한 칸에
|
||||||
|
# 둘 이상 뭉친 줄이 **하나라도** 있어야 이 표로 본다.
|
||||||
|
packed = False
|
||||||
|
for cells in rows[1:]:
|
||||||
|
if not cells or not cells[0]:
|
||||||
|
continue
|
||||||
|
if len(_packed_names(cells[0])) > 1:
|
||||||
|
packed = True
|
||||||
|
break
|
||||||
|
if any(len(_packed_numbers(cell)) > 1 for cell in cells[1:]):
|
||||||
|
packed = True
|
||||||
|
break
|
||||||
|
if not packed:
|
||||||
|
return False
|
||||||
|
|
||||||
|
work_item_code = node.get("work_item_code", "")
|
||||||
|
table_id = str(table.get("pum_table_id", ""))
|
||||||
|
form = str(table.get("pum_form", ""))
|
||||||
|
staged: list = []
|
||||||
|
|
||||||
|
for index, cells in enumerate(rows[1:], start=1):
|
||||||
|
if not cells or not cells[0]:
|
||||||
|
continue
|
||||||
|
names = _packed_names(cells[0])
|
||||||
|
# 규격은 **옆 칸**에 있을 수 있다 — 「굴착기+부착용 집게 | 0.6㎥ | 시간 | …」.
|
||||||
|
side_specs = [cell for cell in cells[1:3] if cell]
|
||||||
|
resolved = [_resolve_packed(catalog, name, side_specs) for name in names]
|
||||||
|
if not all(resolved):
|
||||||
|
if _packed_numbers(" ".join(cells[1:])):
|
||||||
|
# 수가 있는데 이름을 못 풀었다 — 그 몫이 빠진 채 서면 안 된다.
|
||||||
|
result.partial_items[work_item_code] = f"{cells[0][:20]} 줄을 못 풀었습니다"
|
||||||
|
result.unmatched.append(
|
||||||
|
UnmatchedRow(
|
||||||
|
work_item_code=work_item_code,
|
||||||
|
pum_table_id=table_id,
|
||||||
|
cell=cells[0],
|
||||||
|
reason="뭉친 자원 줄의 이름을 못 풀었습니다 — 단가가 일부만 섭니다.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
groups = [_packed_numbers(cell) for cell in cells[1:]]
|
||||||
|
groups = [group for group in groups if group]
|
||||||
|
# 앞쪽에 규격·단위 칸이 낄 수 있다 — 「0.6㎥ | 시간 | 0.31 | 0.30 | 0.28」.
|
||||||
|
# 갈래 수만큼 **뒤에서** 잘라 쓴다.
|
||||||
|
if len(groups) > len(labels):
|
||||||
|
groups = groups[-len(labels) :]
|
||||||
|
if len(groups) != len(labels) or any(len(g) != len(names) for g in groups):
|
||||||
|
result.unmatched.append(
|
||||||
|
UnmatchedRow(
|
||||||
|
work_item_code=work_item_code,
|
||||||
|
pum_table_id=table_id,
|
||||||
|
cell=cells[0],
|
||||||
|
reason=(
|
||||||
|
f"값 묶음 {len(groups)} 개가 갈래 {len(labels)} 개와 안 맞습니다 "
|
||||||
|
"— 자리를 단정할 수 없어 버렸습니다."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
for label, group in zip(labels, groups):
|
||||||
|
for entries, amount in zip(resolved, group):
|
||||||
|
value = amount
|
||||||
|
if basis_quantity not in (None, 0, Decimal(1)):
|
||||||
|
value = value / basis_quantity
|
||||||
|
for entry in entries:
|
||||||
|
staged.append(
|
||||||
|
ResourceRow(
|
||||||
|
work_item_code=work_item_code,
|
||||||
|
pum_table_id=table_id,
|
||||||
|
pum_form=form,
|
||||||
|
resource_kind=entry.kind,
|
||||||
|
resource_code=entry.code,
|
||||||
|
resource_name=entry.name,
|
||||||
|
resource_spec=entry.spec,
|
||||||
|
amount=value,
|
||||||
|
amount_unit=unit,
|
||||||
|
raw_row_index=index,
|
||||||
|
variant=label,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not staged:
|
||||||
|
return False
|
||||||
|
result.rows.extend(staged)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def match_transposed_table(
|
def match_transposed_table(
|
||||||
node: dict[str, Any],
|
node: dict[str, Any],
|
||||||
table: dict[str, Any],
|
table: dict[str, Any],
|
||||||
|
|||||||
@@ -251,7 +251,11 @@ def build_unit_prices(axis: AxisResult | None = None) -> UnitPriceBuild:
|
|||||||
by_item.setdefault((row.work_item_code, getattr(row, "variant", "")), []).append(row)
|
by_item.setdefault((row.work_item_code, getattr(row, "variant", "")), []).append(row)
|
||||||
|
|
||||||
for (work_item_code, variant), rows in sorted(by_item.items()):
|
for (work_item_code, variant), rows in sorted(by_item.items()):
|
||||||
title_code = f"B-{work_item_code}" + (f"#{variant}" if variant else "")
|
# 갈래 키는 **내부 공백을 지운 것**, 화면 문구는 **원문 그대로**
|
||||||
|
# (2026-09-08 두 창 합의). 원문이 「보 통」·「보 통」으로 들쭉날쭉해
|
||||||
|
# 키에 공백을 남기면 한 칸 차이로 영영 안 맞는다. 공백 말고는 손대지 않는다.
|
||||||
|
variant_key = "".join(variant.split())
|
||||||
|
title_code = f"B-{work_item_code}" + (f"#{variant_key}" if variant_key else "")
|
||||||
if title_code in build.book.titles:
|
if title_code in build.book.titles:
|
||||||
continue
|
continue
|
||||||
# ⚠ **붙을 상세를 먼저 모으고, 하나도 없으면 제목도 안 세운다.**
|
# ⚠ **붙을 상세를 먼저 모으고, 하나도 없으면 제목도 안 세운다.**
|
||||||
@@ -284,7 +288,7 @@ def build_unit_prices(axis: AxisResult | None = None) -> UnitPriceBuild:
|
|||||||
unit=unit,
|
unit=unit,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if variant:
|
if variant_key:
|
||||||
build.variants.setdefault(work_item_code, []).append(variant)
|
build.variants.setdefault(work_item_code, []).append(variant)
|
||||||
# 배분율 표는 각 몫을 **그 비율만큼만** 센다 — 인력 원단위를 전량에 곱하면 틀린다.
|
# 배분율 표는 각 몫을 **그 비율만큼만** 센다 — 인력 원단위를 전량에 곱하면 틀린다.
|
||||||
for row, ref in attachable:
|
for row, ref in attachable:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user