diff --git a/src/utils/table.py b/src/utils/table.py index 52b07e4..4790410 100644 --- a/src/utils/table.py +++ b/src/utils/table.py @@ -2,6 +2,7 @@ import os import uuid import polars as pl +import xlsxwriter from dash import no_update from polars import selectors as cs from unidecode import unidecode @@ -557,3 +558,32 @@ def invert_columns(columns): COLUMNS = schema.names() + +_EXCEL_MIN_COLUMN_WIDTH = 132 # ≈ 3.5 cm à 96 DPI +_EXCEL_HEADER_FORMAT = { + "bold": True, + "bg_color": "#b33821", + "font_color": "white", +} +_EXCEL_COLUMN_WIDTHS = { + "objet": 350, + "acheteur_nom": 250, + "titulaire_nom": 250, + "acheteur_id": 160, +} + + +def write_styled_excel(df: pl.DataFrame, buffer, worksheet: str = "DECP") -> None: + col_widths = { + col: max(_EXCEL_MIN_COLUMN_WIDTH, _EXCEL_COLUMN_WIDTHS.get(col, 0)) + for col in df.columns + } + wb = xlsxwriter.Workbook(buffer, {"default_format_properties": {"text_wrap": True}}) + ws = wb.add_worksheet(worksheet) + df.write_excel( + workbook=wb, + worksheet=ws, + header_format=_EXCEL_HEADER_FORMAT, + column_widths=col_widths, + ) + wb.close() diff --git a/tests/test_excel.py b/tests/test_excel.py new file mode 100644 index 0000000..9f9b022 --- /dev/null +++ b/tests/test_excel.py @@ -0,0 +1,54 @@ +import io + +import openpyxl +import polars as pl + +from src.utils.table import write_styled_excel + + +def test_write_styled_excel_header_bold_and_color(): + df = pl.DataFrame({"objet": ["marché A"], "montant": [1000.0]}) + buf = io.BytesIO() + write_styled_excel(df, buf) + buf.seek(0) + wb = openpyxl.load_workbook(buf) + ws = wb.active + cell = ws.cell(row=1, column=1) + assert cell.font.bold is True + assert cell.fill.fgColor.rgb == "FFB33821" + assert cell.font.color.rgb == "FFFFFFFF" + + +def test_write_styled_excel_data_cell_text_wrap(): + df = pl.DataFrame({"objet": ["marché A"]}) + buf = io.BytesIO() + write_styled_excel(df, buf) + buf.seek(0) + wb = openpyxl.load_workbook(buf) + ws = wb.active + cell = ws.cell(row=2, column=1) + assert cell.alignment.wrap_text is True + + +def test_write_styled_excel_known_column_wider_than_minimum(): + # "objet" a une largeur explicite (350px) > minimum (132px) + # "autre" n'a pas de largeur explicite → reçoit le minimum + df = pl.DataFrame({"autre": ["x"], "objet": ["y"]}) + buf = io.BytesIO() + write_styled_excel(df, buf) + buf.seek(0) + wb = openpyxl.load_workbook(buf) + ws = wb.active + width_autre = ws.column_dimensions["A"].width # colonne 1 = "autre" + width_objet = ws.column_dimensions["B"].width # colonne 2 = "objet" + assert width_autre > 0 + assert width_objet > width_autre + + +def test_write_styled_excel_custom_worksheet_name(): + df = pl.DataFrame({"col": ["val"]}) + buf = io.BytesIO() + write_styled_excel(df, buf, worksheet="2025") + buf.seek(0) + wb = openpyxl.load_workbook(buf) + assert "2025" in wb.sheetnames