feat: add write_styled_excel utility for styled Excel exports #83
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user