Files
autocad-mcp-pro/tests/test_understand_report.py
T
Umut CANandClaude Opus 5.5 8c1a6cb27c refactor(understand): one reading of units, plan copies, scale and rooms
Groups U and N were built side by side, so the takeoffs carried narrow
stand-ins for what group U reads properly. On main they read through U:

- describe.drawing_units is the one call every reader makes (kept records,
  labelled room faces as evidence); describe, scale_check and the takeoffs
  all use it. Without the room faces, a layout that declares inches over
  millimetres can tie three ways and fall to the header.
- layout_tags takes its plan copies from describe.prepare, room_regions its
  rooms from describe.find_rooms, and pipe_rows its scale from scale_check,
  per-room groups included. scale_stand_in and the takeoff's own unit bands
  and cluster grid are gone.
- test_understand_one_reading pins the agreement: one unit, one plan copy,
  one verdict and one set of rooms per drawing, across drawing_understand,
  drawing_scale_check and the two takeoffs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
2026-09-24 14:45:27 +03:00

254 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""The takeoff workbook: four sheets, three languages, CSV always, XLSX with openpyxl.
The result written is `pipe_rows` over a hand-built pair: a P&ID with two
runs (T4100-T4101 labelled Ø51, T4101-T4102 unlabelled) and a layout whose
INSUNITS says inches over millimetre geometry, measured on the layout (28 m
and 12 m).
"""
from __future__ import annotations
import csv
from pathlib import Path
import pytest
import engineering.understand.report as report
from engineering.understand.network import build_network
from engineering.understand.report import LANGS, SHEETS, write_workbook
from engineering.understand.snapshot import EntityRecord, Snapshot
from engineering.understand.takeoff import pipe_rows
PIPE = "PRODUCT"
WALLS = "WALLS"
SCHEMATIC = {
"verdict": "schematic",
"within_10pct": 0.25,
"pair_count": 12,
"median": 1.3,
"iqr": 0.8,
"factor": None,
"matched": [],
"ambiguous": [],
}
def rec(handle, kind, layer, points=(), **extra):
return EntityRecord(
handle=handle, type=kind, layer=layer, space="Model", points=tuple(points), **extra
)
def text(handle, value, at, height, layer="TEXT"):
x, y = at
return rec(
handle,
"TEXT",
layer,
(at,),
text=value,
height=height,
bbox=(x, y, x + height * len(value) * 0.6, y + height),
)
def snap(records, insunits, source):
layers = {r.layer: {"color": 7, "linetype": "Continuous"} for r in records}
return Snapshot(
source=source,
insunits=insunits,
extmin=None,
extmax=None,
layers=layers,
layouts=(),
records=tuple(records),
)
def tank(handle, tag, x0, y0):
return [
rec(
handle,
"LWPOLYLINE",
"EQUIPMENT",
((x0, y0), (x0 + 500.0, y0), (x0 + 500.0, y0 + 500.0), (x0, y0 + 500.0)),
closed=True,
bbox=(x0, y0, x0 + 500.0, y0 + 500.0),
),
text(handle + "T", tag, (x0 + 150.0, y0 + 225.0), 50.0),
]
@pytest.fixture
def result():
pid = snap(
[
*tank("A", "T4100", 0.0, 0.0),
*tank("B", "T4101", 3000.0, 0.0),
*tank("C", "T4102", 3000.0, 2000.0),
rec("L1", "LINE", PIPE, ((500.0, 250.0), (3000.0, 250.0))),
rec("L2", "LINE", PIPE, ((3250.0, 500.0), (3250.0, 2000.0))),
text("D1", "Ø51", (1500.0, 260.0), 50.0),
],
4,
"pid.dxf",
)
layout = snap(
[
rec(
"W",
"LWPOLYLINE",
WALLS,
((0, 0), (40000, 0), (40000, 20000), (0, 20000)),
closed=True,
bbox=(0.0, 0.0, 40000.0, 20000.0),
),
rec("X", "LINE", WALLS, ((20000, 0), (20000, 20000))),
text("R1", "ROOM 101", (5000.0, 10000.0), 250.0),
text("R2", "ROOM 102", (30000.0, 10000.0), 250.0),
text("T0", "T4100", (2000.0, 3000.0), 250.0),
text("T1", "T4101", (30000.0, 3000.0), 250.0),
text("T2", "T4102", (30000.0, 15000.0), 250.0),
],
1,
"layout.dxf",
)
return pipe_rows(build_network(pid, layers=[PIPE]), pid, layout, scale=SCHEMATIC)
METRAJ_HEADERS = {
"tr": [
"Mahal",
"Hizmet",
"Çap",
"Doğrudan (m)",
"Süreklilik (m)",
"Atanmamış (m)",
"Net (m)",
"Pay oranı",
"Pay (m)",
"Paylı toplam (m)",
"Şematik (m)",
"Durum",
"Hatlar",
],
"en": [
"Room",
"Service",
"Diameter",
"Direct (m)",
"Continuity (m)",
"Unassigned (m)",
"Net (m)",
"Allowance",
"Allowance (m)",
"Total incl. allowance (m)",
"Schematic (m)",
"Status",
"Runs",
],
"ru": [
"Помещение",
"Среда",
"Диаметр",
"Прямо (м)",
"По непрерывности (м)",
"Не назначено (м)",
"Нетто (м)",
"Запас",
"Запас (м)",
"Итого с запасом (м)",
"По схеме (м)",
"Статус",
"Линии",
],
}
TOTAL = {"tr": "TOPLAM", "en": "TOTAL", "ru": "ИТОГО"}
def read_csv(path):
with open(path, encoding="utf-8-sig", newline="") as handle:
return list(csv.reader(handle))
@pytest.mark.parametrize("lang", LANGS)
def test_csv_sheets_carry_the_headers_of_each_language(result, tmp_path, lang):
files = write_workbook(result, kind="pipe", lang=lang, path=str(tmp_path / "takeoff.xlsx"))
names = [Path(p).name for p in files["csv"]]
assert names == [
"takeoff_metraj.csv",
"takeoff_ozet.csv",
"takeoff_kontrol.csv",
"takeoff_metodoloji.csv",
]
metraj = read_csv(files["csv"][0])
assert metraj[0] == METRAJ_HEADERS[lang]
total = metraj[-1]
assert total[0] == TOTAL[lang]
assert float(total[6]) == pytest.approx(40.0) # 28 m + 12 m net
assert float(total[9]) == pytest.approx(48.0) # with the 20 % allowance
def test_the_workbook_has_the_four_sheets_and_numbers_stay_numbers(result, tmp_path):
openpyxl = pytest.importorskip("openpyxl")
files = write_workbook(result, kind="pipe", lang="tr", path=str(tmp_path / "takeoff.xlsx"))
assert files["refused"] is None
book = openpyxl.load_workbook(files["xlsx"])
assert book.sheetnames == list(SHEETS) == ["Metraj", "Özet", "Kontrol", "Metodoloji"]
rows = list(book["Metraj"].iter_rows(values_only=True))
assert list(rows[0]) == METRAJ_HEADERS["tr"]
assert rows[-1][0] == "TOPLAM"
assert rows[-1][6] == pytest.approx(40.0)
assert isinstance(rows[1][6], (int, float)) # a number cell, not the text '18.0'
assert rows[1][6] == pytest.approx(18.0) # room 101's share of the Ø51 run
def test_the_method_sheet_is_generated_from_the_result(result, tmp_path):
files = write_workbook(result, kind="pipe", lang="en", path=str(tmp_path / "t.xlsx"))
method = dict((row[0], row[1]) for row in read_csv(files["csv"][3])[1:] if len(row) == 2)
assert method["Length source"] == "layout"
assert method["Scale check: verdict"] == "schematic"
assert method["Scale check: pairs"] == "12"
assert method["Scale verified"] == "yes"
assert method["Units: layout"] == "INSUNITS in → mm"
assert "x first, then y" in method["Rule: route"]
warnings = [row[1] for row in read_csv(files["csv"][3]) if row[0] == "Warning"]
assert any("INSUNITS declares Inches (1)" in w for w in warnings)
def test_kontrol_lists_the_unassigned_diameter(result, tmp_path):
files = write_workbook(result, kind="pipe", lang="en", path=str(tmp_path / "t.xlsx"))
kontrol = read_csv(files["csv"][2])
assert kontrol[0] == ["Run", "Kind", "Service", "Tags", "Schematic (m)", "Reason"]
(row,) = kontrol[1:]
assert row[1] == "Diameter unassigned" and row[3] == "T4101, T4102"
assert row[5] == "No diameter label"
def test_without_openpyxl_the_csv_is_written_and_xlsx_is_refused(result, tmp_path, monkeypatch):
monkeypatch.setattr(report, "xlsx_available", lambda: False)
files = write_workbook(result, kind="pipe", lang="tr", path=str(tmp_path / "takeoff.xlsx"))
assert files["xlsx"] is None
assert not (tmp_path / "takeoff.xlsx").exists()
assert all(Path(p).exists() for p in files["csv"])
assert files["refused"]["capability"] == "xlsx_write"
assert files["refused"]["ok"] is False
assert '".[office]"' in files["refused"]["error"]
@pytest.mark.parametrize(
("kwargs", "message"),
[
({"kind": "steel"}, "kind: 'steel' unknown; choose from pipe"),
({"lang": "de"}, "lang: 'de' unknown; choose from tr, en, ru"),
({"path": "takeoff.ods"}, "path: the workbook is written as .xlsx"),
],
)
def test_refusals_come_before_any_file(result, tmp_path, kwargs, message):
arguments = {"kind": "pipe", "lang": "en", "path": str(tmp_path / "t.xlsx"), **kwargs}
if "path" in kwargs:
arguments["path"] = str(tmp_path / kwargs["path"])
with pytest.raises(ValueError, match="^" + message.replace(".", r"\.")):
write_workbook(result, **arguments)
assert list(tmp_path.iterdir()) == []