ifc5d: match cost schedule export columns to the Bonsai cost panel (#6251)

Stefano's final ask on #6251 was specific: the ODS/XLSX export should
show exactly what the cost panel shows, ID (Identification), Name,
Quantity, Value, Total Cost, no more, no less. The previous fix in
this PR removed the internal bookkeeping columns but still exported
Description, Unit and a per-category cost breakdown (Labor Cost,
Material Cost, etc), none of which appear in the panel.

Presentation formats (.ods/.xlsx) now use an explicit allow-list of
columns instead of a block-list of internal ones, and relabel headers
to match the panel's own wording (ID / Value / Total Cost). The .csv
format is unchanged: csv2ifc still reads back the extra bookkeeping
columns for the import round trip, which is why it keeps them.

Also add a "Download CSV" button to the browser costing view
(Generate spreadsheet browser), which previously only offered a
clipboard-based Copy Selected. It reuses the already-rendered table
(respecting the user's column visibility settings) and triggers a
real file download, dropping only the UI-only Actions column.

AI-generated with Claude Code; reviewed and tested by Petru Conduraru.

(cherry picked from commit 1df738d968)
This commit is contained in:
Petru Conduraru
2026-07-23 09:24:36 +03:00
committed by Dion Moult
parent 1d1bb2276d
commit e308ecaeb4
3 changed files with 139 additions and 19 deletions
+28
View File
@@ -120,6 +120,34 @@ class TestCsv2Ifc:
assert len(list(Path(temp_csv_dir).glob("*.ods"))) == 1
assert len(list(Path(temp_csv_dir).glob("*.xlsx"))) == 1
def test_xlsx_columns_match_cost_panel(self):
"""ODS/XLSX are presentation formats: they must show exactly what the
Bonsai cost panel shows (ID, Name, Quantity, Value, Total Cost), no
internal bookkeeping columns, no Description/Unit, no per-category
cost breakdown. See #6251."""
import openpyxl
ifc_file = self.setup_ifc_file()
csv_filepath = Path(__file__).parent.parent / "sample_cost_schedule_house_FR.csv"
ifc5d.csv2ifc.Csv2Ifc(str(csv_filepath), ifc_file).execute()
with tempfile.TemporaryDirectory("w") as temp_dir:
writer = ifc5d.ifc5Dspreadsheet.Ifc5DXlsxWriter(ifc_file, temp_dir)
writer.write()
workbook = openpyxl.load_workbook(next(Path(temp_dir).glob("*.xlsx")))
worksheet = workbook.active
headers = [cell.value for cell in next(worksheet.iter_rows())]
assert headers == ["ID", "Name", "Quantity", "Value", "Total Cost"]
# A leaf item (has quantity and value) gets Quantity * Value.
leaf_row = next(row for row in worksheet.iter_rows(min_row=2) if row[0].value == "DB.1.1")
assert leaf_row[4].value == "=C{}*D{}".format(leaf_row[0].row, leaf_row[0].row)
# A parent/sum item gets the sum of its direct children's Total Cost.
parent_row = next(row for row in worksheet.iter_rows(min_row=2) if row[0].value == "DB.1")
assert parent_row[4].value.startswith("=SUM(")
class TestSerialiseCostQuantities:
def test_quantity_name_with_special_characters_round_trips_as_json(self):