schedule_ods already collects every cell style attribute from the sheet,
borders included, and then draws each cell as a rectangle carrying one
hardcoded `.border` class; schedule_xlsx does the same from openpyxl. The
borders set in LibreOffice or Excel are therefore discarded, and a thick
outline with thin inner rules cannot reach the drawing.
The inline rectangle in both branches becomes draw_cell(), which takes a
background color and the borders already resolved, so it is independent of
the source format: get_cell_borders reads .ods style attributes and
get_xlsx_cell_borders reads openpyxl Sides. A cell that declares no border
keeps the `.border` class and draws as before; one that declares them gets
a line per side, which a single rectangle stroke cannot express. Width,
style and color are read in any order per the CSS shorthand grammar; dotted
and dashed become a dash pattern scaled by the line width, while double and
the relief styles draw plain. XLSX_BORDERS converts Excel's named weights
into widths, since it names a weight instead of measuring one.
A cell declaring `border: none` now draws no line where it used to take the
stylesheet's. This is deliberate: turning a rule off from the spreadsheet is
half of what per-cell borders are for. Cells declaring nothing are
unaffected. Adjacent cells still each stroke the edge they share, so where
neighbours differ the later one wins, as the overlapping rectangles did.
Generated with the assistance of an AI coding tool.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
schedule_ods already collects every cell style attribute from the spreadsheet — background, font size, weight, style, color, wrap — and uses them. Borders are collected too, and discarded: every cell is drawn as a rectangle carrying one hardcoded class_="border" whose stroke comes from the stylesheet. schedule_xlsx does the same from openpyxl. A thick outline with thin inner rules, the usual look of an architectural schedule, cannot reach the drawing no matter what is set in LibreOffice or Excel.
What changed
The inline rectangle in both branches becomes draw_cell(), which takes a background color and the borders already resolved, so it is independent of the source format. Each branch translates its own:
A cell that declares no border keeps the .border class and draws exactly as before. One that declares them gets a line per side, which a single rectangle stroke cannot express.
Width, style and color are read in any order, per the CSS border shorthand grammar (<width> || <style> || <color>), rather than assuming the order LibreOffice happens to emit. dotted and dashed become a dash pattern scaled by the line width; double and the relief styles draw plain.
XLSX_BORDERS converts Excel's fourteen named weights into widths and dash patterns. Excel names a weight instead of measuring one, so that table is a choice rather than a reading of the file, and is commented as such.
One deliberate behaviour change
A cell that declares border: none now draws no line, where before it took the stylesheet's. This is intentional and is half the point of the change: a sheet that says a side has no border should not be given one.
Cells that declare nothing about borders are unaffected — that is the compatibility guarantee, and it covers every schedule IfcCsv writes, since it emits no cell styles at all.
Left as it is
Adjacent cells each stroke the edge they share, so where two neighbours declare it differently the later one wins. The rectangles overlapped the same way before, with the same stroke on both sides. Spreadsheet applications resolve such conflicts by precedence, which this does not attempt.
Testing
src/bonsai/test/bim/module/drawing/test_scheduler.py — 20 tests over both translators and the drawing: the untouched-cell guarantee, the border: none change, per-side overrides of the shorthand, token order, dash patterns, and the openpyxl side. One trap they pin down: asked for the ARGB of a theme color, openpyxl returns its own complaint as a string instead of raising, so the length of the value is what distinguishes a real ARGB from it.
black and ruff clean on both files.
Notes
AI disclosure
Per AGENTS.md: this contribution was written with the assistance of an AI coding tool, in whole — both the change to scheduler.py and the new test file, which carries the required header comment.