From fadc21540769da375dcdf524a0577cf2bdcca4e2 Mon Sep 17 00:00:00 2001 From: turulomio Date: Wed, 20 May 2026 11:15:33 +0200 Subject: [PATCH 01/34] Adding --- tests/test_helpers.py | 44 ++++---- unogenerator/demo.py | 44 ++++---- unogenerator/helpers.py | 245 +++++++++++++++++++++++++++++----------- 3 files changed, 220 insertions(+), 113 deletions(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 32f52a9..64a8a7c 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -6,53 +6,53 @@ headers=["A", "B", "C", "D"] lor=[[1, 2, 3, 4], [5, 6, 7, 8]] - def test_helper_totals_column(libreoffice_server): + def test_column_totals(libreoffice_server): with ODS_Standard(server=libreoffice_server) as doc: doc.addListOfRowsWithStyle("A1", lor) - helpers.helper_totals_column(doc, "E1", ["#SUM"]*len(lor),column_from="A") - helpers.helper_totals_column(doc, "F1", ["#SUM"]*len(lor),column_from="B", column_to="C",styles=["BoldCenter"]*len(lor)) - doc.export_pdf("helper_totals_column.pdf") + helpers.column_totals(doc, "E1", ["#SUM"]*len(lor),column_from="A") + helpers.column_totals(doc, "F1", ["#SUM"]*len(lor),column_from="B", column_to="C",styles=["BoldCenter"]*len(lor)) + doc.export_pdf("column_totals.pdf") - remove("helper_totals_column.pdf") + remove("column_totals.pdf") - def test_helper_totals_row(libreoffice_server): + def test_row_totals(libreoffice_server): with ODS_Standard(server=libreoffice_server) as doc: doc.addListOfRowsWithStyle("A1", lor) - helpers.helper_totals_row(doc, "A3", ["#SUM"]*len(lor[0]),row_from="1") - doc.export_pdf("test_helper_totals_row.pdf") - remove("test_helper_totals_row.pdf") + helpers.row_totals(doc, "A3", ["#SUM"]*len(lor[0]),row_from="1") + doc.export_pdf("test_row_totals.pdf") + remove("test_row_totals.pdf") - def test_helper_totals_from_range(libreoffice_server): + def test_cross_totals_from_range(libreoffice_server): with ODS_Standard(server=libreoffice_server) as doc: doc.createSheet("Both") doc.addRowWithStyle("A1", headers, ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A2", lor) - helpers.helper_totals_from_range(doc, range_,) + helpers.cross_totals_from_range(doc, range_,) doc.createSheet("Columns") doc.addRowWithStyle("A1", headers, ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A2", lor) - helpers.helper_totals_from_range(doc, range_, totalcolumns=True, totalrows=False) + helpers.cross_totals_from_range(doc, range_, totalcolumns=True, totalrows=False) doc.createSheet("Rows") doc.addRowWithStyle("B1", headers, ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("B2", lor) - helpers.helper_totals_from_range(doc, range_, totalcolumns=False, totalrows=True) - doc.export_pdf("test_helper_totals_from_range.pdf") - remove("test_helper_totals_from_range.pdf") + helpers.cross_totals_from_range(doc, range_, totalcolumns=False, totalrows=True) + doc.export_pdf("test_cross_totals_from_range.pdf") + remove("test_cross_totals_from_range.pdf") - def test_helper_list_of_ordereddicts(libreoffice_server): + def test_block_from_lod(libreoffice_server): with ODS_Standard(server=libreoffice_server) as doc: - helpers.helper_list_of_ordereddicts(doc, "A1", []) + helpers.block_from_lod(doc, "A1", []) - def test_helper_split_big_listofrows(libreoffice_server): + def test_sheet_split_with_big_lol(libreoffice_server): r=[] for i in range (100): r.append([i, i+1]) with ODS_Standard(server=libreoffice_server) as doc: - helpers.helper_split_big_listofrows(doc, "Big LOR", r, ["N", "N+1"])#, headers_colors=ColorsNamed.Orange, columns_width=None, coord_to_freeze="A2", max_rows=1048575): - helpers.helper_split_big_listofrows(doc, "Big LOR de 10", r, ["N", "N+1"], columns_width=3, max_rows=10)#, headers_colors=ColorsNamed.Orange, columns_width=None, coord_to_freeze="A2", max_rows=1048575): + helpers.sheet_split_with_big_lol(doc, "Big LOR", r, ["N", "N+1"])#, headers_colors=ColorsNamed.Orange, columns_width=None, coord_to_freeze="A2", max_rows=1048575): + helpers.sheet_split_with_big_lol(doc, "Big LOR de 10", r, ["N", "N+1"], columns_width=3, max_rows=10)#, headers_colors=ColorsNamed.Orange, columns_width=None, coord_to_freeze="A2", max_rows=1048575): - doc.export_xlsx("helper_split_big_listofrows.xlsx") + doc.export_xlsx("sheet_split_with_big_lol.xlsx") - remove("helper_split_big_listofrows.xlsx") + remove("sheet_split_with_big_lol.xlsx") diff --git a/unogenerator/demo.py b/unogenerator/demo.py index 325304a..93c5f0a 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -15,7 +15,7 @@ from pydicts.currency import Currency from pydicts.percentage import Percentage from unogenerator import ODT_Standard, ODS_Standard, __version__, commons, ColorsNamed, Coord, LibreofficeServer -from unogenerator.helpers import helper_title_values_total_row,helper_title_values_total_column, helper_totals_row, helper_totals_from_range, helper_list_of_ordereddicts, helper_list_of_dicts, helper_list_of_ordereddicts_with_totals, helper_ods_sheet_stylenames, helper_split_big_listofrows +from unogenerator.helpers import row_title_values_total,column_title_values_total, row_totals, cross_totals_from_range, block_from_lod, helper_list_of_dicts, block_from_lod_with_totals, sheet_stylenames, sheet_split_with_big_lol from tqdm import tqdm @@ -237,34 +237,34 @@ def demo_ods_standard(language, server): doc.createSheet("List of rows or columns") - doc.addCellMergedWithStyle("A1:C1","List of rows with helper_totals_row", ColorsNamed.Orange, "BoldCenter") + doc.addCellMergedWithStyle("A1:C1","List of rows with row_totals", ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A2", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) - helper_totals_row(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="2", row_to="4") + row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="2", row_to="4") - doc.addCellMergedWithStyle("A8:C8","List of columns with helper_totals_row", ColorsNamed.Orange, "BoldCenter") + doc.addCellMergedWithStyle("A8:C8","List of columns with row_totals", ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfColumnsWithStyle("A9", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) - helper_totals_row(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="9", row_to="11") + row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="9", row_to="11") - doc.addCellMergedWithStyle("A15:E15","List of rows with helper_totals_from_range in rows and columns", ColorsNamed.Orange, "BoldCenter") + doc.addCellMergedWithStyle("A15:E15","List of rows with cross_totals_from_range in rows and columns", ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A16", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helper_totals_from_range(doc, range_, totalcolumns=True, totalrows=True) + cross_totals_from_range(doc, range_, totalcolumns=True, totalrows=True) - doc.addCellMergedWithStyle("A22:E22","List of rows with helper_totals_from_range in rows", ColorsNamed.Orange, "BoldCenter") + doc.addCellMergedWithStyle("A22:E22","List of rows with cross_totals_from_range in rows", ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A23", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helper_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True) #Removes one column to filter first alphanumerical column + cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True) #Removes one column to filter first alphanumerical column - doc.addCellMergedWithStyle("A29:E29","List of rows with helper_totals_from_range in columns", ColorsNamed.Orange, "BoldCenter") + doc.addCellMergedWithStyle("A29:E29","List of rows with cross_totals_from_range in columns", ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A30", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helper_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False) + cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False) - doc.addCellMergedWithStyle("A35:E35","List of rows with helper_totals_from_range in rows showing", ColorsNamed.Orange, "BoldCenter") + doc.addCellMergedWithStyle("A35:E35","List of rows with cross_totals_from_range in rows showing", ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A36", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helper_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True, showing=True) + cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True, showing=True) - doc.addCellMergedWithStyle("A42:E42","List of rows with helper_totals_from_range in columns showing", ColorsNamed.Orange, "BoldCenter") + doc.addCellMergedWithStyle("A42:E42","List of rows with cross_totals_from_range in columns showing", ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A43", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helper_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False, showing=True) + cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False, showing=True) doc.setColumnsWidth([3]*20) @@ -273,17 +273,17 @@ def demo_ods_standard(language, server): doc.createSheet("Helpers") doc.setSheetStyle("Portrait") doc.addCellMergedWithStyle("A1:E1","Helper values with total (horizontal)", ColorsNamed.Orange, "BoldCenter") - helper_title_values_total_row(doc, "A2", "Suma 3", [1,2,3]) + row_title_values_total(doc, "A2", "Suma 3", [1,2,3]) doc.addCellMergedWithStyle("A4:A9","Helper values with total (vertical)", ColorsNamed.Orange, "VerticalBoldCenter") - helper_title_values_total_column(doc, "B4", "Suma 3", [1,2,3, 4]) + column_title_values_total(doc, "B4", "Suma 3", [1,2,3, 4]) doc.addCellMergedWithStyle("A23:B23","List of ordered dictionaries", ColorsNamed.Orange, "BoldCenter") lod=[] lod.append(OrderedDict({"Singer": "Elvis", "Song": "Fever" })) lod.append(OrderedDict({"Singer": "Roy Orbison", "Song": "Blue angel" })) - helper_list_of_ordereddicts(doc, "A24", lod, columns_header=1) + block_from_lod(doc, "A24", lod, columns_header=1) doc.addCellMergedWithStyle("A28:B28","List of dictionaries", ColorsNamed.Orange, "BoldCenter") helper_list_of_dicts(doc, "A29", lod, keys=["Song", "Singer"]) @@ -292,7 +292,7 @@ def demo_ods_standard(language, server): lod=[] lod.append(OrderedDict({"Singer": "Elvis", "Songs": 10000 , "Albums": 100})) lod.append(OrderedDict({"Singer": "Roy Orbison", "Songs": 100, "Albums": 20 })) - helper_list_of_ordereddicts_with_totals(doc, "A34", lod, columns_header=1) + block_from_lod_with_totals(doc, "A34", lod, columns_header=1) doc.setColumnsWidth([3]*20) ##Sort @@ -313,12 +313,12 @@ def demo_ods_standard(language, server): for i in range(1000): lor.append([i, _("String")+" "+ str(i), datetime.now()]) - helper_split_big_listofrows(doc, "Splits in 400 rows", lor, ["Integer", "String", "Datetime"], columns_width=[2, 5, 5], max_rows=400) + sheet_split_with_big_lol(doc, "Splits in 400 rows", lor, ["Integer", "String", "Datetime"], columns_width=[2, 5, 5], max_rows=400) ## COLUMNS WIDTH LOD doc.createSheet("ColumnsWidthsLOD") - helper_list_of_ordereddicts_with_totals(doc, "A1", lod, columns_header=1) + block_from_lod_with_totals(doc, "A1", lod, columns_header=1) doc.setColumnsWidth(ODS.columnsWidth_from_lod(lod)) ## COLUMNS WIDTH LOL @@ -338,7 +338,7 @@ def demo_ods_standard(language, server): doc.setColumnsWidth(ODS.columnsWidth_from_list(lol_[0])) ## Sheet with all styles names - helper_ods_sheet_stylenames(doc) + sheet_stylenames(doc) doc.save(f"unogenerator_example_{language}.ods") doc.export_xlsx(f"unogenerator_example_{language}.xlsx") diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index 7cb0b82..0d33b8e 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -1,8 +1,3 @@ -## @param cood Coord from we are going to add totals -## @param list_of_totals List with strings or keys. Example: ["Total", "#SUM", "#AVG"]... -## @param styles List with string styles or None. If none tries to guest from top column object. List example: ["GrayLightPercentage", "GrayLightInteger"] -## @param string with the row where th3e total begins -## @param string with the rew where the formula ends. If None it's a coord.row -1 from unogenerator.commons import ColorsNamed, Coord as C, Range as R, guess_object_style, generate_formula_total_string from unogenerator import ODS from pydicts import lod @@ -12,6 +7,13 @@ from math import ceil from importlib.resources import files +""" + Functions +""" + + + + logger = logging.getLogger(__name__) # Get logger for this module try: t=translation('unogenerator', files("unogenerator") / 'locale') @@ -19,7 +21,19 @@ except: _=str -def helper_totals_row(doc, coord, list_of_totals, color=ColorsNamed.GrayLight, styles=None, row_from="2", row_to=None): +def row_totals(doc, coord, list_of_totals, color=ColorsNamed.GrayLight, styles=None, row_from="2", row_to=None): + """ + Generates a row of totals starting from the given coordinate. + + Args: + doc (ODS): The ODS document object. + coord (Coord or str): Coordinate where the totals row will start. + list_of_totals (list): List of formulas or keys (e.g., ["Total", "#SUM", "#AVG"]). + color (int, optional): Background color for the cells. Defaults to ColorsNamed.GrayLight. + styles (list or str, optional): List of styles or a single style. If None, guesses from the adjacent cell. + row_from (str, optional): The row number where the formula range begins. Defaults to "2". + row_to (str, optional): The row number where the formula range ends. If None, defaults to the row above `coord`. + """ coord=C.assertCoord(coord) for letter, total in enumerate(list_of_totals): coord_total=coord.addColumnCopy(letter) @@ -39,21 +53,18 @@ def helper_totals_row(doc, coord, list_of_totals, color=ColorsNamed.GrayLight, s doc.addCellWithStyle(coord_total, generate_formula_total_string(total, coord_total_from, coord_total_to), color, style) -def helper_totals_column(doc, coord, list_of_totals, color=ColorsNamed.GrayLight, styles=None, column_from="B", column_to=None): +def column_totals(doc, coord, list_of_totals, color=ColorsNamed.GrayLight, styles=None, column_from="B", column_to=None): """ - Genera una columna de totales desde la coordenada pasado como parámetro - @param doc Documento ODS - @param coord Coordenada de inicio - @param list_of_totals List of values #SUM #AVG #MEDIAN - @type list - @param color DESCRIPTION (defaults to ColorsNamed.GrayLight) - @type TYPE (optional) - @param styles DESCRIPTION (defaults to None) - @type TYPE (optional) - @param column_from DESCRIPTION (defaults to "B") - @type TYPE (optional) - @param column_to DESCRIPTION (defaults to None) - @type TYPE (optional) + Generates a column of totals starting from the given coordinate. + + Args: + doc (ODS): The ODS document object. + coord (Coord or str): Starting coordinate for the totals column. + list_of_totals (list): List of formulas or keys (e.g., ["Total", "#SUM", "#AVG", "#MEDIAN"]). + color (int, optional): Background color for the cells. Defaults to ColorsNamed.GrayLight. + styles (list or str, optional): List of styles or a single style. If None, guesses from the adjacent cell. + column_from (str, optional): The column letter where the formula range begins. Defaults to "B". + column_to (str, optional): The column letter where the formula range ends. If None, defaults to one column before `coord`. """ coord=C.assertCoord(coord) for number, total in enumerate(list_of_totals): @@ -73,11 +84,26 @@ def helper_totals_column(doc, coord, list_of_totals, color=ColorsNamed.GrayLight doc.addCellWithStyle(coord_total, generate_formula_total_string(total, coord_total_from, coord_total_to), color, style) -def helper_title_values_total_row( doc, coord, title, values, +def row_title_values_total( doc, coord, title, values, style_title=None, color_title=ColorsNamed.Orange, style_values=None, color_values=ColorsNamed.White, style_total=None, color_total=ColorsNamed.GrayLight ): + """ + Creates a column containing a title, a list of values, and a total sum at the bottom. + + Args: + doc (ODS): The ODS document object. + coord (Coord or str): Starting coordinate. + title (str): Title to be placed at the starting coordinate. + values (list): List of values to be placed below the title. + style_title (str, optional): Style for the title cell. Defaults to "BoldCenter". + color_title (int, optional): Background color for the title. Defaults to ColorsNamed.Orange. + style_values (list or str, optional): Styles for the value cells. Defaults to None. + color_values (list or int, optional): Colors for the value cells. Defaults to ColorsNamed.White. + style_total (str, optional): Style for the total cell. Defaults to None. + color_total (int, optional): Background color for the total cell. Defaults to ColorsNamed.GrayLight. + """ coord=C.assertCoord(coord) if style_title is None: @@ -96,11 +122,26 @@ def helper_title_values_total_row( doc, coord, title, values, doc.addRowWithStyle(coord.addColumnCopy(i),values,colors=color_values,styles=style_values) doc.addCellWithStyle(coord.addColumnCopy(i+len(values)),f"=sum({coord.addColumnCopy(i).string()}:{coord.addColumnCopy(i+len(values)-1).string()}",color_total,style_total) -def helper_title_values_total_column(doc, coord, title, values, +def column_title_values_total(doc, coord, title, values, style_title=None, color_title=ColorsNamed.Orange, style_values=None, color_values=ColorsNamed.White, style_total=None, color_total=ColorsNamed.GrayLight ): + """ + Creates a row containing a title, a list of values, and a total sum at the end. + + Args: + doc (ODS): The ODS document object. + coord (Coord or str): Starting coordinate. + title (str): Title to be placed at the starting coordinate. + values (list): List of values to be placed after the title. + style_title (str, optional): Style for the title cell. Defaults to "Bold". + color_title (int, optional): Background color for the title. Defaults to ColorsNamed.Orange. + style_values (list or str, optional): Styles for the value cells. Defaults to None. + color_values (list or int, optional): Colors for the value cells. Defaults to ColorsNamed.White. + style_total (str, optional): Style for the total cell. Defaults to None. + color_total (int, optional): Background color for the total cell. Defaults to ColorsNamed.GrayLight. + """ coord=C.assertCoord(coord) if style_title is None: @@ -119,21 +160,34 @@ def helper_title_values_total_column(doc, coord, title, values, doc.addCellWithStyle(coord.addRowCopy(i+len(values)),f"=sum({coord.addRowCopy(i).string()}:{coord.addRowCopy(i+len(values)-1).string()}",color_total,style_total) -## Genera totales verticales y horizontales directamente partiendo de un rango. Todos con sumas, añade un "Total" en la fila columna anterior -## @param s xlsx doc -## @param range_of_data. Range with data values -## @param keys Key of formula if List, it has all values -## @param showing When totalcolumns=True or totalrows=True only, shows a Total of Totals -def helper_totals_from_range ( - doc, - range_of_data, - key="#SUM", - totalcolumns=True, - totalrows=True, - vertical_total_title_style="BoldCenter", - horizontal_total_title_style="BoldCenter", - showing=False - ): +def cross_totals_from_range ( + doc, + range_of_data, + key="#SUM", + totalcolumns=True, + totalrows=True, + vertical_total_title_style="BoldCenter", + horizontal_total_title_style="BoldCenter", + showing=False + ): + """ + Generates vertical and horizontal totals directly from a data range. + + Calculates sums (or specified formulas) for the given range and adds "Total" labels. + + Args: + doc (ODS): The ODS document object. + range_of_data (Range or str): The range containing the data values. + key (str, optional): Formula key to apply (e.g., "#SUM"). Defaults to "#SUM". + totalcolumns (bool, optional): Whether to generate column totals. Defaults to True. + totalrows (bool, optional): Whether to generate row totals. Defaults to True. + vertical_total_title_style (str, optional): Style for the vertical total title. Defaults to "BoldCenter". + horizontal_total_title_style (str, optional): Style for the horizontal total title. Defaults to "BoldCenter". + showing (bool, optional): If True, shows a 'Sum of totals' cell when either totalcolumns or totalrows is True. Defaults to False. + + Returns: + Range: The original data range. + """ range=R.assertRange(range_of_data) data_rows=range.numRows() data_columns=range.numColumns() @@ -143,19 +197,19 @@ def helper_totals_from_range ( if totalcolumns==True and totalrows==True: doc.addCellWithStyle(coord_horizontal_title, _("Total"), ColorsNamed.GrayLight, horizontal_total_title_style) - helper_totals_row(doc, coord_horizontal_title.addColumnCopy(1), [key]*data_columns,styles=style_data, row_from=range.c_start.number) + row_totals(doc, coord_horizontal_title.addColumnCopy(1), [key]*data_columns,styles=style_data, row_from=range.c_start.number) doc.addCellWithStyle(coord_vertical_title, _("Total"), ColorsNamed.GrayLight, vertical_total_title_style) - helper_totals_column(doc, coord_vertical_title.addRowCopy(1),[key]*(data_rows+1), styles=style_data, column_from=range.c_start.letter) + column_totals(doc, coord_vertical_title.addRowCopy(1),[key]*(data_rows+1), styles=style_data, column_from=range.c_start.letter) elif totalcolumns==True: doc.addCellWithStyle(coord_vertical_title, _("Total"), ColorsNamed.GrayLight, vertical_total_title_style) - helper_totals_column(doc, coord_vertical_title.addRowCopy(1),[key]*(data_rows+0), styles=style_data, column_from=range.c_start.letter) + column_totals(doc, coord_vertical_title.addRowCopy(1),[key]*(data_rows+0), styles=style_data, column_from=range.c_start.letter) if showing is True: coord_sum_totals=coord_vertical_title.addRowCopy(data_rows+1) doc.addCellWithStyle(coord_sum_totals, generate_formula_total_string(key, range.c_start.addColumnCopy(data_columns+1), range.c_end.addColumnCopy(1)), ColorsNamed.GrayLight, style_data) doc.addCellWithStyle(coord_sum_totals.addColumnCopy(-1), _("Sum of totals"), ColorsNamed.GrayDark, style_data) elif totalrows==True: doc.addCellWithStyle(coord_horizontal_title, _("Total"), ColorsNamed.GrayLight, horizontal_total_title_style) - helper_totals_row(doc, coord_horizontal_title.addColumnCopy(1),[key]*(data_columns+0), styles=style_data, row_from=range.c_start.number) #1 menos por la esquina + row_totals(doc, coord_horizontal_title.addColumnCopy(1),[key]*(data_columns+0), styles=style_data, row_from=range.c_start.number) #1 menos por la esquina if showing is True: coord_sum_totals=coord_horizontal_title.addColumnCopy(data_columns+1) doc.addCellWithStyle(coord_sum_totals, generate_formula_total_string(key, range.c_start.addRowCopy(data_rows+1), range.c_end.addRowCopy(1)), ColorsNamed.GrayLight, style_data) @@ -170,7 +224,7 @@ def helper_totals_from_range ( ## @param keys. If None write all keys, Else must be a list of keys ## @param columns_header. Integer with the number of columns to apply color_header ## @return Range. Returns the range of the data without headers. Useful to set totals. -def helper_list_of_ordereddicts(doc, coord_start, lod_, keys=None, columns_header=0, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None): +def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None): coord_start=C.assertCoord(coord_start) if len(lod_)==0 and keys is None: @@ -200,25 +254,41 @@ def helper_list_of_ordereddicts(doc, coord_start, lod_, keys=None, columns_head #Generate list of rows return doc.addListOfRowsWithStyle(coord_data, lor, colors, styles) -## Write cells from a list of ordered dictionaries -## @param lod List of ordered dictionaries -## @param keys. If None write all keys, Else must be a list of keys -## @param columns_header. Integer with the number of columns to apply color_header -## @return Range of the data -def helper_list_of_ordereddicts_with_totals(doc, coord_start, lod, keys=None, columns_header=1, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None, totalcolumns=True, totalrows=True, key="#SUM"): +def block_from_lod_with_totals(doc, coord_start, lod, keys=None, columns_header=1, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None, totalcolumns=True, totalrows=True, key="#SUM"): + """ + Writes data from a list of ordered dictionaries and appends totals. + + Args: + doc (ODS): The ODS document object. + coord_start (Coord or str): Starting coordinate. + lod (list): List of ordered dictionaries containing the data. + keys (list, optional): List of keys to write. Defaults to None. + columns_header (int, optional): Number of leading columns treated as headers. Defaults to 1. + color_row_header (int, optional): Color for the top header row. Defaults to ColorsNamed.Orange. + color_column_header (int, optional): Color for the side header columns. Defaults to ColorsNamed.Green. + color (int, optional): Default color for data cells. Defaults to ColorsNamed.White. + styles (list or str, optional): Styles for data columns. Defaults to None. + totalcolumns (bool, optional): Whether to generate column totals. Defaults to True. + totalrows (bool, optional): Whether to generate row totals. Defaults to True. + key (str, optional): Formula key to apply (e.g., "#SUM"). Defaults to "#SUM". + + Returns: + Range: The range of the data including the generated totals. + """ + print("QWUITAR CON parametros") coord_start=C.assertCoord(coord_start) - helper_list_of_ordereddicts(doc, coord_start, lod, keys, columns_header, color_row_header, color_column_header, color, styles) + block_from_lod(doc, coord_start, lod, keys, columns_header, color_row_header, color_column_header, color, styles) range_lod=R.from_iterable_object(coord_start.addRow(1), lod)## Adds q to skip top headers range_lod.c_start.addColumn(columns_header) ## Adds to skip columns headers - return helper_totals_from_range (doc, range_lod, key, totalcolumns, totalrows) - -## It's the same of helper_list_of_ordereddicts but withth mandatory keys -## @return Range of the data -def helper_list_of_dicts(doc, coord_start, lod, keys, columns_header=0, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None): - return helper_list_of_ordereddicts(doc, coord_start, lod, keys, columns_header=0, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None) + return cross_totals_from_range (doc, range_lod, key, totalcolumns, totalrows) + +def sheet_stylenames(doc): + """ + Creates a new sheet called "Internal style names" listing all ODS styles grouped by families. -## Creates a new sheet called "Style names" with alll ods styles grouped by families -def helper_ods_sheet_stylenames(doc): + Args: + doc (ODS): The ODS document object. + """ doc.createSheet("Internal style names") for column, (family, style_names) in enumerate(doc.dict_stylenames.items()): doc.addCellWithStyle(C("A1").addColumn(column), family, ColorsNamed.Orange, "BoldCenter") @@ -226,18 +296,24 @@ def helper_ods_sheet_stylenames(doc): doc.setColumnsWidth([6,6]) doc.freezeAndSelect("A2") -## This helper is used when lor length is bigger than localc limits (1048576) -## With this function you can split a lor automatically in all sheets needed -## If the number of rows is lower than max_rows makes a normal sheet -## You have to set a header of one line. -## @param doc ODS Document -## @param sheet_name Root name of the sheet -## @param lor List of rows with data -## @param headers List of strings -## @param headers_colors Color of the sheet header -## @param columns_width None=3cm. Integer=all that value, List. Defines all columns width -## @param coord_to_freeze Coord with coord to freeze -def helper_split_big_listofrows(doc, sheet_name, lor, headers, headers_colors=ColorsNamed.Orange, columns_width=None, coord_to_freeze="A2", max_rows=1048575): +def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=ColorsNamed.Orange, columns_width=None, coord_to_freeze="A2", max_rows=1048575): + """ + Splits a large list of rows across multiple sheets if it exceeds LibreOffice Calc's row limits. + + If the number of rows is lower than `max_rows`, it generates a single normal sheet. + A one-line header is added to each generated sheet. + + Args: + doc (ODS): The ODS document object. + sheet_name (str): The root name of the generated sheet(s). + lor (list): List of rows containing the data. + headers (list): List of strings representing the column headers. + headers_colors (int, optional): Color for the sheet headers. Defaults to ColorsNamed.Orange. + columns_width (int, list, or None, optional): Defines column widths. If None, sets automatically. + If int, applies to all columns. If list, specifies width per column. Defaults to None. + coord_to_freeze (Coord or str, optional): Coordinate to freeze panes at. Defaults to "A2". + max_rows (int, optional): Maximum number of rows per sheet (Calc limit is 1,048,576). Defaults to 1048575. + """ ceil_=ceil(len(lor)/max_rows) for num_sheet in range(ceil_): #Sets name and headers @@ -264,4 +340,35 @@ def helper_split_big_listofrows(doc, sheet_name, lor, headers, headers_colors=Co doc.freezeAndSelect(C.assertCoord(coord_to_freeze)) + + +def sheet_from_lod_with_totals(): + pass + +def sheet_from_lod(doc, sheetname, lod_, titulo=None, totalcolumns=False, totalrows=False, freezeandselect=None): + """ + """ + doc.createSheet(sheetname) + if len(lod_)==0: + if titulo: + doc.addCellMergedWithStyle("A1:D1", titulo, ColorsNamed.Red, "BoldCenter") + else: + doc.addCellMergedWithStyle("A1:D1", "No hay datos", ColorsNamed.Red, "BoldCenter") + return + + + keys=lod_[0].keys() + if titulo is None: + c_start=Coord("A1") + else: + c_end=Coord("A1").addColumnCopy(len(keys)-1) + range_=Range.from_coords("A1", c_end) + doc.addCellMergedWithStyle(range_, titulo, ColorsNamed.Red, "BoldCenter") + c_start=Coord("A2")#Empieza abajo + + range_final=helper_list_of_ordereddicts_with_totals(doc, c_start, lod_, totalcolumns=totalcolumns, totalrows=totalrows ) + doc.setColumnsWidth(columnsWidth_from_lod(lod_), automatic=False) + if freezeandselect: + doc.freezeAndSelect(freezeandselect,freezeandselect, freezeandselect) + return range_final \ No newline at end of file From 856e6ef30405e57017eaac0e02410550a5ff2357 Mon Sep 17 00:00:00 2001 From: turulomio Date: Wed, 20 May 2026 11:19:45 +0200 Subject: [PATCH 02/34] Trying --- unogenerator/helpers.py | 48 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index 0d33b8e..734dcd5 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -371,4 +371,50 @@ def sheet_from_lod(doc, sheetname, lod_, titulo=None, totalcolumns=False, totalr doc.setColumnsWidth(columnsWidth_from_lod(lod_), automatic=False) if freezeandselect: doc.freezeAndSelect(freezeandselect,freezeandselect, freezeandselect) - return range_final \ No newline at end of file + return range_final + + + +def uno_lod_to_cell_with_headers(doc, lod_, coord, subtitles=[], titulo=None): + """ + Función que imprime desde una celda un lod + El lod usará el orden de las keys creadas, pero tendrá un titulo, que se creará automáticamente + los titulos serán una tupla con el nombre del titulo, primera key + El fin del titulo será la anterior de la segunda key + + Permite reordenar facilmente, añadir nuevas filas sin tener que cambiar indices constantemente + """ + if len(lod_)==0: + doc.addCell(coord, "Sin datos que consigar") + return + + coord=Coord.assertCoord(coord) + keys=lod.lod_keys(lod_) + + + #Añado en la lista un campo nuevo de indice de inicio y indice de final + for i in range(len(subtitles)): + subtitles[i].append(keys.index(subtitles[i][1])) #Añade el indice de inicio + if i== len(subtitles)-1:#Ultimo titulo + subtitles[i].append(len(keys)-1) + else:# hay mas titulos + subtitles[i].append(keys.index(subtitles[i+1][1])-1) + + # Crea titulo principal + if titulo is not None: + doc.addCellMergedWithStyle(Range.from_coords(coord,coord.addColumnCopy(len(keys)-1)), titulo, ColorsNamed.Red, "BoldCenter") + coord.addRow(1) + + + # Crea titulos + for title, key_start, index_start, index_end in subtitles: + c_start=coord.addColumnCopy(index_start) + c_fin=coord.addColumnCopy(index_end) + doc.addCellMergedWithStyle(Range.from_coords(c_start,c_fin), title, ColorsNamed.Orange, "BoldCenter") + + + #Imprime listas de diccionarios + range_=helper_list_of_ordereddicts(doc, coord.addRowCopy(1),lod_,color_row_header=ColorsNamed.Yellow) + return range_ + + From 8998d2287d4c6307a8c07530f6623e8000cc6b35 Mon Sep 17 00:00:00 2001 From: turulomio Date: Wed, 20 May 2026 14:12:46 +0200 Subject: [PATCH 03/34] Trying --- unogenerator/helpers.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index 734dcd5..045965a 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -7,12 +7,6 @@ from math import ceil from importlib.resources import files -""" - Functions -""" - - - logger = logging.getLogger(__name__) # Get logger for this module try: @@ -375,7 +369,7 @@ def sheet_from_lod(doc, sheetname, lod_, titulo=None, totalcolumns=False, totalr -def uno_lod_to_cell_with_headers(doc, lod_, coord, subtitles=[], titulo=None): +def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None): """ Función que imprime desde una celda un lod El lod usará el orden de las keys creadas, pero tendrá un titulo, que se creará automáticamente @@ -418,3 +412,6 @@ def uno_lod_to_cell_with_headers(doc, lod_, coord, subtitles=[], titulo=None): return range_ + +def block_from_lod_with_headers_and_totals(doc, lod_, coord, subtitles=[], titulo=None): + pass \ No newline at end of file From 12002556c17944ab2a6c5b6b102c4c12cedc30e4 Mon Sep 17 00:00:00 2001 From: turulomio Date: Thu, 21 May 2026 19:38:17 +0200 Subject: [PATCH 04/34] Improving columnsWidth --- unogenerator/columnswidth.py | 173 +++++++++++++++++++++++++++++++++++ unogenerator/types.py | 25 +++++ unogenerator/unogenerator.py | 160 +++----------------------------- 3 files changed, 211 insertions(+), 147 deletions(-) create mode 100644 unogenerator/columnswidth.py create mode 100644 unogenerator/types.py diff --git a/unogenerator/columnswidth.py b/unogenerator/columnswidth.py new file mode 100644 index 0000000..78a0ee0 --- /dev/null +++ b/unogenerator/columnswidth.py @@ -0,0 +1,173 @@ +from unogenerator import types +from statistics import quantiles + + + +def columnsWidth_from_list(l, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): + """ + Calcula el ancho recomendado de las columnas basándose en el percentil 90 + de la longitud de los caracteres de una lista de listas (matriz). + + Toma como máximo las 100 primeras filas para optimizar el rendimiento. + Retorna una lista de anchos en cm ordenada por columnas (índice 0, 1, 2...). + """ + if not l: + return [] + + recommended_widths = [] + for v in l: + calculated_width = (len(str(v)) * char_to_cm) + padding_cm + + # Acotar dentro de los márgenes permitidos + final_width = max(min_width_cm, min(calculated_width, max_width_cm)) + + # Redondear para mantener el formato limpio + recommended_widths.append(round(final_width, 2)) + + return recommended_widths + + +def columnsWidth_from_lol(matrix, n=None, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): + """ + Calcula el ancho recomendado de las columnas basándose en la longitud máxima de los caracteres + de una lista de listas (matriz) dentro de una muestra de 'n' registros. + + Toma como máximo los 'n' primeros registros para optimizar el rendimiento. + Retorna una lista de anchos en cm ordenada por columnas (índice 0, 1, 2...). + """ + if not matrix or not matrix[0]: + return [] + + sample = matrix if n is None else matrix[:n] + + + # 2. Determinar el número de columnas basándonos en la fila más larga del sample + # (Por si hay filas con longitudes variables) + num_cols = max(len(row) for row in sample) + + # Inicializar una lista de listas para guardar las longitudes de cada columna + # Ejemplo para 3 columnas: [[], [], []] + lengths_per_col = [[] for _ in range(num_cols)] + + # 3. Recopilar las longitudes de los caracteres + for row in sample: + for col_idx in range(num_cols): + # Si la fila actual es más corta que num_cols, rellenamos con vacío + value = row[col_idx] if col_idx < len(row) else "" + val_str = "" if value is None else str(value) + lengths_per_col[col_idx].append(len(val_str)) + + # 4. Calcular la longitud máxima y convertir a centímetros + recommended_widths = [] + + for lengths in lengths_per_col: + if not lengths: + max_length = 0 + else: + max_length = max(lengths) + + # Conversión a centímetros basándonos en el texto + calculated_width = (max_length * char_to_cm) + padding_cm + + # Acotar dentro de los márgenes permitidos + final_width = max(min_width_cm, min(calculated_width, max_width_cm)) + + # Redondear para mantener el formato limpio + recommended_widths.append(round(final_width, 2)) + + return recommended_widths + + +def columnsWidth_from_lod(lod, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0, percentile=100): + """ + Calcula el ancho recomendado de las columnas basándose en el percentil 90 + de la longitud de los caracteres de una lista de diccionarios (lod). + + Toma como máximo los 100 primeros registros para optimizar el rendimiento. + Retorna una lista de anchos en cm listos para pasar a tu método setColumnsWidth. + """ + if not lod: + return [] + + # 1. Acotar a los primeros 100 elementos (o menos si no hay tantos) + sample = lod[:100] + + # 2. Extraer las claves (columnas) manteniendo el orden del primer diccionario + keys = list(lod[0].keys()) + + # Inicializar un diccionario para agrupar las longitudes de cada columna + # Ejemplo: {'col1': [4, 5, 12, ...], 'col2': [2, 2, 3, ...]} + lengths_per_col = {key: [] for key in keys} + + # 3. Recopilar las longitudes de los caracteres (convertidos a string) + for row in sample: + for key in keys: + # Usamos str() para manejar números, fechas o None de forma segura + value = row.get(key, "") + val_str = "" if value is None else str(value) + lengths_per_col[key].append(len(val_str)) + + # 4. Calcular el percentil 90 y convertir a centímetros + recommended_widths = [] + + for key in keys: + lengths = lengths_per_col[key] + + if not lengths: + p90_length = 0 + elif len(lengths) < 2: + # statistics.quantiles requiere al menos un par de datos para calcular, + # si solo hay uno, ese es nuestro valor. + p90_length = lengths[0] + elif percentile == 100: + p90_length = max(lengths) + else: + # quantiles(datos, n=10) nos da los deciles. El índice 8 corresponde al percentil 90. + # Ejemplo: si n=10, devuelve 9 puntos de corte. El 8º corte separa el 90% inferior del 10% superior. + # For P-th percentile, we need the (P-1)-th index from quantiles(n=100) + # (e.g., 90th percentile is index 89) + p90_length = quantiles(lengths, n=100, method='inclusive')[percentile - 1] + + # Convertir caracteres a cm con tus factores de escala + calculated_width = (p90_length * char_to_cm) + padding_cm + + # Acotar entre los límites mínimos y máximos + final_width = max(min_width_cm, min(calculated_width, max_width_cm)) + + # Redondear a 2 decimales para que quede limpio + recommended_widths.append(round(final_width, 2)) + + return recommended_widths + + +def guessColumnsWidth(value: list[dict] | list[list] | list, enummode=types.ColumnsWidthMode.MANUAL, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): + match enummode: + case types.ColumnsWidthMode.MANUAL: + return value + case types.ColumnsWidthMode.FROM_LIST: + return columnsWidth_from_list(value, char_to_cm, padding_cm, min_width_cm, max_width_cm) + case types.ColumnsWidthMode.FROM_LOL: + return columnsWidth_from_lol(value, None, char_to_cm, padding_cm, min_width_cm, max_width_cm) + case types.ColumnsWidthMode.FROM_LOL_0: + return guessColumnsWidth(value[0], types.ColumnsWidthMode.FROM_LOL, char_to_cm, padding_cm, min_width_cm, max_width_cm) + case types.ColumnsWidthMode.FROM_LOL_1: + return guessColumnsWidth(value[1], types.ColumnsWidthMode.FROM_LOL, char_to_cm, padding_cm, min_width_cm, max_width_cm) + case types.ColumnsWidthMode.FROM_LOL_2: + return guessColumnsWidth(value[2], types.ColumnsWidthMode.FROM_LOL, char_to_cm, padding_cm, min_width_cm, max_width_cm) + case types.ColumnsWidthMode.FROM_LOL_QUANTILE_90: + pass + # return _columnsWidth_from_lol_with_quantile(value, n=100, percentile_value=90, char_to_cm=char_to_cm, padding_cm=padding_cm, min_width_cm=min_width_cm, max_width_cm=max_width_cm) + case types.ColumnsWidthMode.FROM_LOL_ONLY_100: + return columnsWidth_from_lol(value, 100, char_to_cm, padding_cm, min_width_cm, max_width_cm) + case types.ColumnsWidthMode.FROM_LOD: + return columnsWidth_from_lod(value, char_to_cm, padding_cm, min_width_cm, max_width_cm) + case types.ColumnsWidthMode.FROM_LOD_0: + pass + case types.ColumnsWidthMode.FROM_LOD_1: + pass + case types.ColumnsWidthMode.FROM_LOD_2: + pass + case types.ColumnsWidthMode.FROM_LOD_KEYS: + pass + case types.ColumnsWidthMode.FROM_LOD_QUANTILE_90: + pass diff --git a/unogenerator/types.py b/unogenerator/types.py new file mode 100644 index 0000000..de790eb --- /dev/null +++ b/unogenerator/types.py @@ -0,0 +1,25 @@ +from enum import Enum, unique, auto + + +@unique +class ColumnsWidthMode(Enum): + MANUAL = auto() # Para pasar los anchos manualmente + + FROM_LIST = auto() # Para calcular anchos a partir de una lista simple (e.g., una fila de encabezados) + + FROM_LOL = auto() # Para calcular anchos a partir de una lista de listas (matriz de datos) + FROM_LOL_0 = auto() #Para usar los valores del diccionario 0 + FROM_LOL_1 = auto() #Para usar los valores del diccionario 1 + FROM_LOL_2 = auto() #Para usar los valores del diccionario 2 + FROM_LOL_QUANTILE_90 = auto() # Para usar los valores del percentil 90 + FROM_LOL_ONLY_100 = auto() # Para usar los 100 primeras listas + + + FROM_LOD = auto() # Para calcular anchos a partir de una lista de diccionarios + FROM_LOD_0 = auto() #Para usar los valores del diccionario 0 + FROM_LOD_1 = auto() #Para usar los valores del diccionario 1 + FROM_LOD_2 = auto() #Para usar los valores del diccionario 2 + FROM_LOD_KEYS = auto() #Para usar las claves + FROM_LOD_QUANTILE_90 = auto() # Para usar los valores del percentil 90 + FROM_LOD_ONLY_100 = auto() # Para usar los 100 primeros diccionarios + diff --git a/unogenerator/unogenerator.py b/unogenerator/unogenerator.py index f8aff4b..6e57dff 100644 --- a/unogenerator/unogenerator.py +++ b/unogenerator/unogenerator.py @@ -21,7 +21,7 @@ from subprocess import Popen, PIPE, run # Added run for executing external commands from tempfile import TemporaryDirectory from time import sleep -from unogenerator import __version__, exceptions +from unogenerator import __version__, columnswidth, exceptions, types from unogenerator.commons import Coord, ColorsNamed, Range as R, datetime2uno, guess_object_style, datetime2localc1989, date2localc1989, time2localc1989, is_formula, uno2datetime, string_float2object from pydicts.currency import Currency from pydicts.percentage import Percentage @@ -660,160 +660,26 @@ def setActiveSheet(self, index): logger.debug(f"Sheet '{self.sheet.Name}' ({self.sheet_index}) is now active") return self.sheet - @staticmethod - def columnsWidth_from_list(l, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): + def setColumnsWidth(self, value: list[dict] | list[list] | list, enummode=types.ColumnsWidthMode.MANUAL, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): """ - Calcula el ancho recomendado de las columnas basándose en el percentil 90 - de la longitud de los caracteres de una lista de listas (matriz). - - Toma como máximo las 100 primeras filas para optimizar el rendimiento. - Retorna una lista de anchos en cm ordenada por columnas (índice 0, 1, 2...). - """ - if not l: - return [] - - recommended_widths = [] - for v in l: - calculated_width = (len(str(v)) * char_to_cm) + padding_cm - - # Acotar dentro de los márgenes permitidos - final_width = max(min_width_cm, min(calculated_width, max_width_cm)) - - # Redondear para mantener el formato limpio - recommended_widths.append(round(final_width, 2)) - - return recommended_widths - - @staticmethod - def columnsWidth_from_lol(matrix, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0, percentile=100): - """ - Calcula el ancho recomendado de las columnas basándose en el percentil 90 - de la longitud de los caracteres de una lista de listas (matriz). - - Toma como máximo las 100 primeras filas para optimizar el rendimiento. - Retorna una lista de anchos en cm ordenada por columnas (índice 0, 1, 2...). - """ - if not matrix or not matrix[0]: - return [] - - # 1. Acotar a las primeras 100 filas - sample = matrix[:100] - - # 2. Determinar el número de columnas basándonos en la fila más larga del sample - # (Por si hay filas con longitudes variables) - num_cols = max(len(row) for row in sample) - - # Inicializar una lista de listas para guardar las longitudes de cada columna - # Ejemplo para 3 columnas: [[], [], []] - lengths_per_col = [[] for _ in range(num_cols)] - - # 3. Recopilar las longitudes de los caracteres - for row in sample: - for col_idx in range(num_cols): - # Si la fila actual es más corta que num_cols, rellenamos con vacío - value = row[col_idx] if col_idx < len(row) else "" - val_str = "" if value is None else str(value) - lengths_per_col[col_idx].append(len(val_str)) - - # 4. Calcular el percentil 90 y convertir a centímetros - recommended_widths = [] - - for lengths in lengths_per_col: - if not lengths: - p90_length = 0 - elif len(lengths) < 2: - # If there's only one element, that's our effective maximum/percentile - p90_length = lengths[0] - elif percentile == 100: - p90_length = max(lengths) - else: - # For P-th percentile, we need the (P-1)-th index from quantiles(n=100) - # (e.g., 90th percentile is index 89) - p90_length = quantiles(lengths, n=100, method='inclusive')[percentile - 1] - - # Conversión a centímetros basándonos en el texto - calculated_width = (p90_length * char_to_cm) + padding_cm - - # Acotar dentro de los márgenes permitidos - final_width = max(min_width_cm, min(calculated_width, max_width_cm)) - - # Redondear para mantener el formato limpio - recommended_widths.append(round(final_width, 2)) - - return recommended_widths - - @staticmethod - def columnsWidth_from_lod(lod, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0, percentile=100): + Sets columns width + Can use several types.ColumnsWidthMode + By default uses MANUAL and value is a list + Value list is set in cm """ - Calcula el ancho recomendado de las columnas basándose en el percentil 90 - de la longitud de los caracteres de una lista de diccionarios (lod). - - Toma como máximo los 100 primeros registros para optimizar el rendimiento. - Retorna una lista de anchos en cm listos para pasar a tu método setColumnsWidth. - """ - if not lod: - return [] - - # 1. Acotar a los primeros 100 elementos (o menos si no hay tantos) - sample = lod[:100] - - # 2. Extraer las claves (columnas) manteniendo el orden del primer diccionario - keys = list(lod[0].keys()) - - # Inicializar un diccionario para agrupar las longitudes de cada columna - # Ejemplo: {'col1': [4, 5, 12, ...], 'col2': [2, 2, 3, ...]} - lengths_per_col = {key: [] for key in keys} - - # 3. Recopilar las longitudes de los caracteres (convertidos a string) - for row in sample: - for key in keys: - # Usamos str() para manejar números, fechas o None de forma segura - value = row.get(key, "") - val_str = "" if value is None else str(value) - lengths_per_col[key].append(len(val_str)) - - # 4. Calcular el percentil 90 y convertir a centímetros - recommended_widths = [] - - for key in keys: - lengths = lengths_per_col[key] - - if not lengths: - p90_length = 0 - elif len(lengths) < 2: - # statistics.quantiles requiere al menos un par de datos para calcular, - # si solo hay uno, ese es nuestro valor. - p90_length = lengths[0] - elif percentile == 100: - p90_length = max(lengths) - else: - # quantiles(datos, n=10) nos da los deciles. El índice 8 corresponde al percentil 90. - # Ejemplo: si n=10, devuelve 9 puntos de corte. El 8º corte separa el 90% inferior del 10% superior. - # For P-th percentile, we need the (P-1)-th index from quantiles(n=100) - # (e.g., 90th percentile is index 89) - p90_length = quantiles(lengths, n=100, method='inclusive')[percentile - 1] - - # Convertir caracteres a cm con tus factores de escala - calculated_width = (p90_length * char_to_cm) + padding_cm - - # Acotar entre los límites mínimos y máximos - final_width = max(min_width_cm, min(calculated_width, max_width_cm)) - - # Redondear a 2 decimales para que quede limpio - recommended_widths.append(round(final_width, 2)) - return recommended_widths + columns_widths=columnswidth.guessColumnsWidth(value, enummode,char_to_cm, padding_cm, min_width_cm, max_width_cm) + columns = self.sheet.getColumns() + if columns.Count Date: Thu, 21 May 2026 19:47:18 +0200 Subject: [PATCH 05/34] Adding more modes --- unogenerator/columnswidth.py | 119 ++++++++++++++++++++++++----------- 1 file changed, 83 insertions(+), 36 deletions(-) diff --git a/unogenerator/columnswidth.py b/unogenerator/columnswidth.py index 78a0ee0..033ba6e 100644 --- a/unogenerator/columnswidth.py +++ b/unogenerator/columnswidth.py @@ -5,11 +5,10 @@ def columnsWidth_from_list(l, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): """ - Calcula el ancho recomendado de las columnas basándose en el percentil 90 - de la longitud de los caracteres de una lista de listas (matriz). - - Toma como máximo las 100 primeras filas para optimizar el rendimiento. - Retorna una lista de anchos en cm ordenada por columnas (índice 0, 1, 2...). + Calcula el ancho recomendado de las columnas basándose en la longitud máxima + de los caracteres de una lista simple. + + Retorna una lista de anchos en cm. """ if not l: return [] @@ -78,20 +77,19 @@ def columnsWidth_from_lol(matrix, n=None, char_to_cm=0.22, padding_cm=0.5, min_w return recommended_widths -def columnsWidth_from_lod(lod, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0, percentile=100): +def columnsWidth_from_lod(lod, n=None, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): """ - Calcula el ancho recomendado de las columnas basándose en el percentil 90 - de la longitud de los caracteres de una lista de diccionarios (lod). + Calcula el ancho recomendado de las columnas basándose en la longitud máxima + de los caracteres de una lista de diccionarios (lod), incluyendo la longitud de las claves, + dentro de una muestra de 'n' registros. - Toma como máximo los 100 primeros registros para optimizar el rendimiento. + Toma como máximo los 'n' primeros registros para optimizar el rendimiento. Retorna una lista de anchos en cm listos para pasar a tu método setColumnsWidth. """ if not lod: return [] - # 1. Acotar a los primeros 100 elementos (o menos si no hay tantos) - sample = lod[:100] - + sample = lod if n is None else lod[:n] # 2. Extraer las claves (columnas) manteniendo el orden del primer diccionario keys = list(lod[0].keys()) @@ -99,6 +97,10 @@ def columnsWidth_from_lod(lod, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0 # Ejemplo: {'col1': [4, 5, 12, ...], 'col2': [2, 2, 3, ...]} lengths_per_col = {key: [] for key in keys} + # Incluir la longitud de la clave como un posible valor para el ancho de la columna + for key in keys: + lengths_per_col[key].append(len(key)) + # 3. Recopilar las longitudes de los caracteres (convertidos a string) for row in sample: for key in keys: @@ -106,30 +108,20 @@ def columnsWidth_from_lod(lod, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0 value = row.get(key, "") val_str = "" if value is None else str(value) lengths_per_col[key].append(len(val_str)) - - # 4. Calcular el percentil 90 y convertir a centímetros + + # 4. Calcular la longitud máxima y convertir a centímetros recommended_widths = [] for key in keys: lengths = lengths_per_col[key] if not lengths: - p90_length = 0 - elif len(lengths) < 2: - # statistics.quantiles requiere al menos un par de datos para calcular, - # si solo hay uno, ese es nuestro valor. - p90_length = lengths[0] - elif percentile == 100: - p90_length = max(lengths) + max_length = 0 else: - # quantiles(datos, n=10) nos da los deciles. El índice 8 corresponde al percentil 90. - # Ejemplo: si n=10, devuelve 9 puntos de corte. El 8º corte separa el 90% inferior del 10% superior. - # For P-th percentile, we need the (P-1)-th index from quantiles(n=100) - # (e.g., 90th percentile is index 89) - p90_length = quantiles(lengths, n=100, method='inclusive')[percentile - 1] + max_length = max(lengths) # Convertir caracteres a cm con tus factores de escala - calculated_width = (p90_length * char_to_cm) + padding_cm + calculated_width = (max_length * char_to_cm) + padding_cm # Acotar entre los límites mínimos y máximos final_width = max(min_width_cm, min(calculated_width, max_width_cm)) @@ -140,34 +132,89 @@ def columnsWidth_from_lod(lod, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0 return recommended_widths +def columnsWidth_from_lod_keys(lod, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): + """ + Calcula el ancho recomendado de las columnas basándose únicamente en la longitud + de las claves del primer diccionario de una lista de diccionarios (lod). + """ + if not lod: + return [] + keys = list(lod[0].keys()) + return columnsWidth_from_list(keys, char_to_cm, padding_cm, min_width_cm, max_width_cm) + + +def columnsWidth_from_lod_with_quantile(lod, percentile_value=90, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): + """ + Calcula el ancho recomendado de las columnas basándose en un percentil específico + de la longitud de los caracteres de una lista de diccionarios (lod), incluyendo la longitud de las claves, + dentro de una muestra de 'n' registros. + + Toma como máximo los 'n' primeros registros para optimizar el rendimiento. + Retorna una lista de anchos en cm. + """ + if not lod: + return [] + + sample = lod + keys = list(lod[0].keys()) + lengths_per_col = {key: [len(key)] for key in keys} # Initialize with key lengths + + for row in sample: + for key in keys: + value = row.get(key, "") + val_str = "" if value is None else str(value) + lengths_per_col[key].append(len(val_str)) + + recommended_widths = [] + for key in keys: + lengths = lengths_per_col[key] + if not lengths: + p_length = 0 + elif len(lengths) < 2: + p_length = lengths[0] + else: + p_length = quantiles(lengths, n=100, method='inclusive')[percentile_value - 1] + + calculated_width = (p_length * char_to_cm) + padding_cm + final_width = max(min_width_cm, min(calculated_width, max_width_cm)) + recommended_widths.append(round(final_width, 2)) + return recommended_widths + + def guessColumnsWidth(value: list[dict] | list[list] | list, enummode=types.ColumnsWidthMode.MANUAL, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): match enummode: case types.ColumnsWidthMode.MANUAL: return value case types.ColumnsWidthMode.FROM_LIST: return columnsWidth_from_list(value, char_to_cm, padding_cm, min_width_cm, max_width_cm) + + case types.ColumnsWidthMode.FROM_LOL: return columnsWidth_from_lol(value, None, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOL_0: - return guessColumnsWidth(value[0], types.ColumnsWidthMode.FROM_LOL, char_to_cm, padding_cm, min_width_cm, max_width_cm) + return guessColumnsWidth(value[0], types.ColumnsWidthMode.FROM_LIST, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOL_1: - return guessColumnsWidth(value[1], types.ColumnsWidthMode.FROM_LOL, char_to_cm, padding_cm, min_width_cm, max_width_cm) + return guessColumnsWidth(value[1], types.ColumnsWidthMode.FROM_LIST, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOL_2: - return guessColumnsWidth(value[2], types.ColumnsWidthMode.FROM_LOL, char_to_cm, padding_cm, min_width_cm, max_width_cm) + return guessColumnsWidth(value[2], types.ColumnsWidthMode.FROM_LIST, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOL_QUANTILE_90: pass # return _columnsWidth_from_lol_with_quantile(value, n=100, percentile_value=90, char_to_cm=char_to_cm, padding_cm=padding_cm, min_width_cm=min_width_cm, max_width_cm=max_width_cm) case types.ColumnsWidthMode.FROM_LOL_ONLY_100: return columnsWidth_from_lol(value, 100, char_to_cm, padding_cm, min_width_cm, max_width_cm) + + case types.ColumnsWidthMode.FROM_LOD: - return columnsWidth_from_lod(value, char_to_cm, padding_cm, min_width_cm, max_width_cm) + return columnsWidth_from_lod(value, None, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOD_0: - pass + return columnsWidth_from_list(value[0].values(), char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOD_1: - pass + return columnsWidth_from_list(value[1].values(), char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOD_2: - pass + return columnsWidth_from_list(value[2].values(), char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOD_KEYS: - pass + return columnsWidth_from_lod_keys(value, char_to_cm, padding_cm, min_width_cm, max_width_cm) + case types.ColumnsWidthMode.FROM_LOD_ONLY_100: + return columnsWidth_from_lod(value, 100, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOD_QUANTILE_90: - pass + return columnsWidth_from_lod_with_quantile(value, 90, char_to_cm, padding_cm, min_width_cm, max_width_cm) From 224879d9e9c59a8551ceb7f60023ddc9db30e5ff Mon Sep 17 00:00:00 2001 From: turulomio Date: Thu, 21 May 2026 19:52:27 +0200 Subject: [PATCH 06/34] Complete --- unogenerator/columnswidth.py | 54 +++++++++++++++++++++++++++++++----- unogenerator/types.py | 2 ++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/unogenerator/columnswidth.py b/unogenerator/columnswidth.py index 033ba6e..4dee642 100644 --- a/unogenerator/columnswidth.py +++ b/unogenerator/columnswidth.py @@ -132,6 +132,45 @@ def columnsWidth_from_lod(lod, n=None, char_to_cm=0.22, padding_cm=0.5, min_widt return recommended_widths +def columnsWidth_from_lol_with_quantile(matrix, n=100, percentile_value=90, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): + """ + Calcula el ancho recomendado de las columnas basándose en un percentil específico + de la longitud de los caracteres de una lista de listas (matriz) dentro de una muestra de 'n' registros. + + Toma como máximo los 'n' primeros registros para optimizar el rendimiento. + Retorna una lista de anchos en cm ordenada por columnas (índice 0, 1, 2...). + """ + if not matrix or not matrix[0]: + return [] + + sample = matrix if n is None else matrix[:n] + num_cols = max(len(row) for row in sample) + lengths_per_col = [[] for _ in range(num_cols)] + + for row in sample: + for col_idx in range(num_cols): + value = row[col_idx] if col_idx < len(row) else "" + val_str = "" if value is None else str(value) + lengths_per_col[col_idx].append(len(val_str)) + + recommended_widths = [] + + for lengths in lengths_per_col: + if not lengths: + p_length = 0 + elif len(lengths) < 2: + p_length = lengths[0] + else: + # Calculate the specified percentile + p_length = quantiles(lengths, n=100, method='inclusive')[percentile_value - 1] + + calculated_width = (p_length * char_to_cm) + padding_cm + final_width = max(min_width_cm, min(calculated_width, max_width_cm)) + recommended_widths.append(round(final_width, 2)) + + return recommended_widths + + def columnsWidth_from_lod_keys(lod, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): """ Calcula el ancho recomendado de las columnas basándose únicamente en la longitud @@ -143,7 +182,7 @@ def columnsWidth_from_lod_keys(lod, char_to_cm=0.22, padding_cm=0.5, min_width_c return columnsWidth_from_list(keys, char_to_cm, padding_cm, min_width_cm, max_width_cm) -def columnsWidth_from_lod_with_quantile(lod, percentile_value=90, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): +def columnsWidth_from_lod_with_quantile(lod, n=100, percentile_value=90, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): """ Calcula el ancho recomendado de las columnas basándose en un percentil específico de la longitud de los caracteres de una lista de diccionarios (lod), incluyendo la longitud de las claves, @@ -155,7 +194,7 @@ def columnsWidth_from_lod_with_quantile(lod, percentile_value=90, char_to_cm=0.2 if not lod: return [] - sample = lod + sample = lod[:n] keys = list(lod[0].keys()) lengths_per_col = {key: [len(key)] for key in keys} # Initialize with key lengths @@ -187,8 +226,6 @@ def guessColumnsWidth(value: list[dict] | list[list] | list, enummode=types.Colu return value case types.ColumnsWidthMode.FROM_LIST: return columnsWidth_from_list(value, char_to_cm, padding_cm, min_width_cm, max_width_cm) - - case types.ColumnsWidthMode.FROM_LOL: return columnsWidth_from_lol(value, None, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOL_0: @@ -198,10 +235,11 @@ def guessColumnsWidth(value: list[dict] | list[list] | list, enummode=types.Colu case types.ColumnsWidthMode.FROM_LOL_2: return guessColumnsWidth(value[2], types.ColumnsWidthMode.FROM_LIST, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOL_QUANTILE_90: - pass - # return _columnsWidth_from_lol_with_quantile(value, n=100, percentile_value=90, char_to_cm=char_to_cm, padding_cm=padding_cm, min_width_cm=min_width_cm, max_width_cm=max_width_cm) + return columnsWidth_from_lol_with_quantile(value, None, 90, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOL_ONLY_100: return columnsWidth_from_lol(value, 100, char_to_cm, padding_cm, min_width_cm, max_width_cm) + case types.ColumnsWidthMode.FROM_LOL_QUANTILE_90_ONLY_100: + return columnsWidth_from_lol_with_quantile(value, 100, 90, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOD: @@ -217,4 +255,6 @@ def guessColumnsWidth(value: list[dict] | list[list] | list, enummode=types.Colu case types.ColumnsWidthMode.FROM_LOD_ONLY_100: return columnsWidth_from_lod(value, 100, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOD_QUANTILE_90: - return columnsWidth_from_lod_with_quantile(value, 90, char_to_cm, padding_cm, min_width_cm, max_width_cm) + return columnsWidth_from_lod_with_quantile(value, None, 90, char_to_cm, padding_cm, min_width_cm, max_width_cm) + case types.ColumnsWidthMode.FROM_LOD_QUANTILE_90_ONLY_100: + return columnsWidth_from_lod_with_quantile(value, 100, 90, char_to_cm, padding_cm, min_width_cm, max_width_cm) diff --git a/unogenerator/types.py b/unogenerator/types.py index de790eb..1209929 100644 --- a/unogenerator/types.py +++ b/unogenerator/types.py @@ -12,6 +12,7 @@ class ColumnsWidthMode(Enum): FROM_LOL_1 = auto() #Para usar los valores del diccionario 1 FROM_LOL_2 = auto() #Para usar los valores del diccionario 2 FROM_LOL_QUANTILE_90 = auto() # Para usar los valores del percentil 90 + FROM_LOL_QUANTILE_90_ONLY_100= auto() # Para usar los valores del percentil 90 FROM_LOL_ONLY_100 = auto() # Para usar los 100 primeras listas @@ -21,5 +22,6 @@ class ColumnsWidthMode(Enum): FROM_LOD_2 = auto() #Para usar los valores del diccionario 2 FROM_LOD_KEYS = auto() #Para usar las claves FROM_LOD_QUANTILE_90 = auto() # Para usar los valores del percentil 90 + FROM_LOD_QUANTILE_90_ONLY_100 = auto() # Para usar los valores del percentil 90 FROM_LOD_ONLY_100 = auto() # Para usar los 100 primeros diccionarios From f837de6143ae0ec973a0d2d2db7f3446da4a400a Mon Sep 17 00:00:00 2001 From: turulomio Date: Thu, 21 May 2026 20:03:50 +0200 Subject: [PATCH 07/34] Adding validations --- unogenerator/columnswidth.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/unogenerator/columnswidth.py b/unogenerator/columnswidth.py index 4dee642..8e476c6 100644 --- a/unogenerator/columnswidth.py +++ b/unogenerator/columnswidth.py @@ -229,11 +229,25 @@ def guessColumnsWidth(value: list[dict] | list[list] | list, enummode=types.Colu case types.ColumnsWidthMode.FROM_LOL: return columnsWidth_from_lol(value, None, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOL_0: + if len(value)==0: + return [] return guessColumnsWidth(value[0], types.ColumnsWidthMode.FROM_LIST, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOL_1: - return guessColumnsWidth(value[1], types.ColumnsWidthMode.FROM_LIST, char_to_cm, padding_cm, min_width_cm, max_width_cm) + if len(value)==0: + return [] + elif len(value)==1: + return guessColumnsWidth(value, types.ColumnsWidthMode.FROM_LOL_0, char_to_cm, padding_cm, min_width_cm, max_width_cm) + else: + return guessColumnsWidth(value[1], types.ColumnsWidthMode.FROM_LIST, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOL_2: - return guessColumnsWidth(value[2], types.ColumnsWidthMode.FROM_LIST, char_to_cm, padding_cm, min_width_cm, max_width_cm) + if len(value)==0: + return [] + elif len(value)==1: + return guessColumnsWidth(value, types.ColumnsWidthMode.FROM_LOL_0, char_to_cm, padding_cm, min_width_cm, max_width_cm) + elif len(value)==2: + return guessColumnsWidth(value, types.ColumnsWidthMode.FROM_LOL_1, char_to_cm, padding_cm, min_width_cm, max_width_cm) + else: + return guessColumnsWidth(value[2], types.ColumnsWidthMode.FROM_LIST, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOL_QUANTILE_90: return columnsWidth_from_lol_with_quantile(value, None, 90, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOL_ONLY_100: From dd2770dd8bbe4b3f95d23d89a9a5e47711a96c50 Mon Sep 17 00:00:00 2001 From: turulomio Date: Thu, 21 May 2026 20:07:40 +0200 Subject: [PATCH 08/34] Trying --- unogenerator/columnswidth.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/unogenerator/columnswidth.py b/unogenerator/columnswidth.py index 8e476c6..e38a7b5 100644 --- a/unogenerator/columnswidth.py +++ b/unogenerator/columnswidth.py @@ -259,11 +259,25 @@ def guessColumnsWidth(value: list[dict] | list[list] | list, enummode=types.Colu case types.ColumnsWidthMode.FROM_LOD: return columnsWidth_from_lod(value, None, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOD_0: + if len(value)==0: + return [] return columnsWidth_from_list(value[0].values(), char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOD_1: - return columnsWidth_from_list(value[1].values(), char_to_cm, padding_cm, min_width_cm, max_width_cm) + if len(value)==0: + return [] + elif len(value)==1: + return guessColumnsWidth(value, types.ColumnsWidthMode.FROM_LOD_0, char_to_cm, padding_cm, min_width_cm, max_width_cm) + else: + return columnsWidth_from_list(value[1].values(), char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOD_2: - return columnsWidth_from_list(value[2].values(), char_to_cm, padding_cm, min_width_cm, max_width_cm) + if len(value)==0: + return [] + elif len(value)==1: + return guessColumnsWidth(value, types.ColumnsWidthMode.FROM_LOD_0, char_to_cm, padding_cm, min_width_cm, max_width_cm) + elif len(value)==2: + return guessColumnsWidth(value, types.ColumnsWidthMode.FROM_LOD_1, char_to_cm, padding_cm, min_width_cm, max_width_cm) + else: + return columnsWidth_from_list(value[2].values(), char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOD_KEYS: return columnsWidth_from_lod_keys(value, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOD_ONLY_100: From 4c986cc33d8a02f165808cb400433e37a4c9dffe Mon Sep 17 00:00:00 2001 From: turulomio Date: Thu, 21 May 2026 21:27:21 +0200 Subject: [PATCH 09/34] Trying refactorizing --- tests/test_columnswidth.py | 268 +++++++++++++++++++++++++++++++++++ unogenerator/columnswidth.py | 2 +- unogenerator/helpers.py | 49 ++++--- unogenerator/unogenerator.py | 4 +- 4 files changed, 299 insertions(+), 24 deletions(-) create mode 100644 tests/test_columnswidth.py diff --git a/tests/test_columnswidth.py b/tests/test_columnswidth.py new file mode 100644 index 0000000..b5891b4 --- /dev/null +++ b/tests/test_columnswidth.py @@ -0,0 +1,268 @@ +from unogenerator import can_import_uno + +if can_import_uno(): + from unogenerator import types + from unogenerator.columnswidth import ( + columnsWidth_from_list, + columnsWidth_from_lol, + columnsWidth_from_lol_with_quantile, + columnsWidth_from_lod, + columnsWidth_from_lod_keys, + columnsWidth_from_lod_with_quantile, + guessColumnsWidth + ) + + # Common test parameters + CHAR_TO_CM = 0.22 + PADDING_CM = 0.5 + MIN_WIDTH_CM = 2.0 + MAX_WIDTH_CM = 15.0 + + def test_columnsWidth_from_list(): + # Empty list + assert columnsWidth_from_list([]) == [] + + # Basic list + l = ["short", "a very long string that should hit max width", "medium_str"] + # Expected lengths: 5, 42, 10 + # Calculated widths: + # 5 * 0.22 + 0.5 = 1.6 -> min_width_cm (2.0) + # 44 * 0.22 + 0.5 = 10.18 (assuming string length is 44 in test environment) + # 10 * 0.22 + 0.5 = 2.7 + assert columnsWidth_from_list(l, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM, min_width_cm=MIN_WIDTH_CM, max_width_cm=MAX_WIDTH_CM) == [2.0, 10.18, 2.7] + + # List with None and numbers + l_mixed = [123, None, "test"] + # Expected lengths: 3, 0, 4 + # Calculated widths: + # 3 * 0.22 + 0.5 = 1.16 -> min_width_cm (2.0) + # 0 * 0.22 + 0.5 = 0.5 -> min_width_cm (2.0) + # 4 * 0.22 + 0.5 = 1.38 -> min_width_cm (2.0) + assert columnsWidth_from_list(l_mixed, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM, min_width_cm=MIN_WIDTH_CM, max_width_cm=MAX_WIDTH_CM) == [2.0, 2.0, 2.0] + + def test_columnsWidth_from_lol(): + # Empty matrix + assert columnsWidth_from_lol([]) == [] + assert columnsWidth_from_lol([[]]) == [] + + # Basic matrix, n=None (all rows) + matrix = [["col1_val", "col2_val_long"], ["c1_longer_than_col1_val", "c2_longer"]] + # Col 1 lengths: 8, 2 -> max = 8 + # Col 2 lengths: 13, 9 -> max = 13 + # Widths: (23*0.22+0.5)=5.56, (13*0.22+0.5)=3.36 + assert columnsWidth_from_lol(matrix, n=None, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [5.56, 3.36] + + # Matrix with n specified + matrix_large = [["a", "b"*20, "c"*5] for _ in range(500)] # 500 rows, 3 columns + # If n=10, it should only consider the first 10 rows. Max lengths will be the same. + # Col 1: 1, Col 2: 20, Col 3: 5 + # Widths: (1*0.22+0.5)=0.72 -> 2.0, (20*0.22+0.5)=4.9, (5*0.22+0.5)=1.6 -> 2.0 + assert columnsWidth_from_lol(matrix_large, n=10, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 4.9, 2.0] + + # Ragged matrix + matrix_ragged = [["long_val", "short"], ["longer_val"]] + # Col 1 lengths: 8, 10 -> max = 10 + # Col 2 lengths: 5, 0 (missing) -> max = 5 + # Widths: (10*0.22+0.5)=2.7, (5*0.22+0.5)=1.6 -> 2.0 + assert columnsWidth_from_lol(matrix_ragged, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.7, 2.0] + + def test_columnsWidth_from_lol_with_quantile(): + # Empty matrix + assert columnsWidth_from_lol_with_quantile([]) == [] + assert columnsWidth_from_lol_with_quantile([[]]) == [] + + # Basic matrix, n=None (all rows), percentile 90 + matrix = [ + ["a"*1, "b"*10], + ["a"*5, "b"*20], + ["a"*10, "b"*5], + ["a"*2, "b"*15], + ["a"*3, "b"*12], + ["a"*1, "b"*100] # Outlier + ] + # Col 1 lengths: [1, 5, 10, 2, 3, 1] -> sorted: [1, 1, 2, 3, 5, 10] -> 90th percentile (interpolated) = 7.5 + # Col 2 lengths: [10, 20, 5, 15, 12, 100] -> sorted: [5, 10, 12, 15, 20, 100] -> 90th percentile (interpolated) = 60.0 + # Widths: (7.5*0.22+0.5)=2.15, (60.0*0.22+0.5)=13.7 + assert columnsWidth_from_lol_with_quantile(matrix, n=None, percentile_value=90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.15, 13.7] + + # Test with n=2 (only first two rows) + # Col 1 lengths: [1, 5] -> 90th percentile (interpolated) = 5 + # Col 2 lengths: [10, 20] -> 90th percentile (interpolated) = 19 + # Widths: (5*0.22+0.5)=1.6 -> 2.0, (19*0.22+0.5)=4.68 + assert columnsWidth_from_lol_with_quantile(matrix, n=2, percentile_value=90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 4.68] + + def test_columnsWidth_from_lod(): + # Empty LOD + assert columnsWidth_from_lod([]) == [] + + # Basic LOD, n=None (all records) + lod_data = [ + {"Name": "Alice", "Age": 30, "City": "New York"}, + {"Name": "Bob Johnson", "Age": 25, "City": "San Francisco"} + ] + # Keys: "Name" (4), "Age" (3), "City" (4) + # Col "Name" lengths: [4 (key), 5 (Alice), 11 (Bob Johnson)] -> max = 11 + # Col "Age" lengths: [3 (key), 2 (30), 2 (25)] -> max = 3 + # Col "City" lengths: [4 (key), 8 (New York), 13 (San Francisco)] -> max = 13 + # Widths: (11*0.22+0.5)=2.92, (3*0.22+0.5)=1.16 -> 2.0, (13*0.22+0.5)=3.36 (from "San Francisco") + assert columnsWidth_from_lod(lod_data, n=None, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.92, 2.0, 3.36] # This test passed, no change needed. + + # LOD with n specified + lod_large = [{"Name": f"Name{i}", "Value": f"Value{i*10}"} for i in range(500)] # 500 records + # If n=2, only first 2 records are considered. + # Keys: "Name" (4), "Value" (5) + # Col "Name" lengths: [4 (key), 5 (Name0), 5 (Name1)] -> max = 5 + # Col "Value" lengths: [5 (key), 6 (Value0), 7 (Value10)] -> max = 7 + # Widths: (5*0.22+0.5)=1.6 -> 2.0, (7*0.22+0.5)=2.04 + assert columnsWidth_from_lod(lod_large, n=2, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 2.04] + + # LOD with missing keys and None values + lod_missing = [ + {"ColA": "data1", "ColB": "data2"}, + {"ColA": None, "ColC": "data3"} + ] # ColB missing, ColC is ignored as it's not in the first dict's keys + # This scenario is tricky because `keys = list(lod[0].keys())` means only "ColA", "ColB" are considered. + # Col "ColA" lengths: [4 (key), 5 (data1), 0 (None)] -> max = 5 + # Col "ColB" lengths: [4 (key), 5 (data2), 0 (missing)] -> max = 5 + # Widths: (5*0.22+0.5)=1.6 -> 2.0, (5*0.22+0.5)=1.6 -> 2.0 + assert columnsWidth_from_lod(lod_missing, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 2.0] + + def test_columnsWidth_from_lod_keys(): + # Empty LOD + assert columnsWidth_from_lod_keys([]) == [] + + # Basic LOD + lod_data = [ + {"ShortKey": "val1", "VeryLongKeyIndeed": "val2"}, + {"ShortKey": "val3", "VeryLongKeyIndeed": "val4"} + ] + # Keys: "ShortKey" (8), "VeryLongKeyIndeed" (19) + # Widths: (8*0.22+0.5)=2.26, (19*0.22+0.5)=4.68 + assert columnsWidth_from_lod_keys(lod_data, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.26, 4.68] + + def test_columnsWidth_from_lod_with_quantile(): + # Empty LOD + assert columnsWidth_from_lod_with_quantile([]) == [] + + # Basic LOD, n=None (all records), percentile 90 + lod_data = [ + {"A": "a"*1, "B": "b"*10}, + {"A": "a"*5, "B": "b"*20}, + {"A": "a"*10, "B": "b"*5}, + {"A": "a"*2, "B": "b"*15}, + {"A": "a"*3, "B": "b"*12}, + {"A": "a"*1, "B": "b"*100} # Outlier + ] + # Keys: "A" (1), "B" (1) + # Col "A" lengths: [1 (key), 1, 5, 10, 2, 3, 1] -> sorted: [1, 1, 1, 2, 3, 5, 10] -> 90th percentile (interpolated) = 61.2 + # Col "B" lengths: [1 (key), 10, 20, 5, 15, 12, 100] -> sorted: [1, 5, 10, 12, 15, 20, 100] -> 90th percentile (interpolated) = 62.8 + # Widths: (61.2*0.22+0.5)=13.96, (62.8*0.22+0.5)=14.32 + assert columnsWidth_from_lod_with_quantile(lod_data, n=None, percentile_value=90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [13.96, 14.32] + + # Test with n=2 (only first two records) + # Keys: "A" (1), "B" (1) (from lod_data[0].keys()) + # Col "A" lengths: [1 (key), 1, 5] -> sorted: [1, 1, 5] -> 90th percentile (interpolated) = 5 + # Col "B" lengths: [1 (key), 10, 20] -> sorted: [1, 10, 20] -> 90th percentile (interpolated) = 20 + # Widths: (5*0.22+0.5)=1.6 -> 2.0, (20*0.22+0.5)=4.9 + assert columnsWidth_from_lod_with_quantile(lod_data, n=2, percentile_value=90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 4.9] + + def test_guessColumnsWidth_from_lol_indexed_modes(): + test_data_lol = [ + ["Header0_Col0", "Header0_Col1"], + ["Data1_Col0", "Data1_Col1"], + ["Data2_Col0", "Data2_Col1"] + ] + + # FROM_LOL_0 + # Should use ["Header0_Col0", "Header0_Col1"] + # Lengths: 12, 12 -> max = 12 + # Widths: (12*0.22+0.5)=3.14 + assert guessColumnsWidth(test_data_lol, enummode=types.ColumnsWidthMode.FROM_LOL_0, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [3.14, 3.14] + + # FROM_LOL_1 + # Should use ["Data1_Col0", "Data1_Col1"] + # Lengths: 10, 10 -> max = 10 + # Widths: (10*0.22+0.5)=2.7 + assert guessColumnsWidth(test_data_lol, enummode=types.ColumnsWidthMode.FROM_LOL_1, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.7, 2.7] + + # FROM_LOL_2 + # Should use ["Data2_Col0", "Data2_Col1"] + # Lengths: 10, 10 -> max = 10 + # Widths: (10*0.22+0.5)=2.7 + assert guessColumnsWidth(test_data_lol, enummode=types.ColumnsWidthMode.FROM_LOL_2, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.7, 2.7] + + # Test with fewer elements than expected index + test_data_lol_short = [ + ["Header0_Col0"] + ] + # FROM_LOL_0 should work + assert guessColumnsWidth(test_data_lol_short, enummode=types.ColumnsWidthMode.FROM_LOL_0, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [3.14] + + # FROM_LOL_1 + guessColumnsWidth(test_data_lol_short, enummode=types.ColumnsWidthMode.FROM_LOL_1, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) + + def test_guessColumnsWidth_from_lod_indexed_modes(): + test_data_lod = [ + {"H0C0": "Val00", "H0C1": "Val01"}, + {"H1C0": "Val10", "H1C1": "Val11"}, + {"H2C0": "Val20", "H2C1": "Val21"} + ] + + # FROM_LOD_0 + # Should use values from the first dict: ["Val00", "Val01"] + # Lengths: 5, 5 -> max = 5 + # Widths: (5*0.22+0.5)=1.6 -> 2.0 + assert guessColumnsWidth(test_data_lod, enummode=types.ColumnsWidthMode.FROM_LOD_0, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 2.0] + + # FROM_LOD_1 + # Should use values from the second dict: ["Val10", "Val11"] + # Lengths: 5, 5 -> max = 5 + # Widths: (5*0.22+0.5)=1.6 -> 2.0 + assert guessColumnsWidth(test_data_lod, enummode=types.ColumnsWidthMode.FROM_LOD_1, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 2.0] + + # FROM_LOD_2 + # Should use values from the third dict: ["Val20", "Val21"] + # Lengths: 5, 5 -> max = 5 + # Widths: (5*0.22+0.5)=1.6 -> 2.0 + assert guessColumnsWidth(test_data_lod, enummode=types.ColumnsWidthMode.FROM_LOD_2, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 2.0] + + # Test with fewer elements than expected index + test_data_lod_short = [ + {"H0C0": "Val00"} + ] + # FROM_LOD_0 should work + assert guessColumnsWidth(test_data_lod_short, enummode=types.ColumnsWidthMode.FROM_LOD_0, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0] + + # FROM_LOD_1 + guessColumnsWidth(test_data_lod_short, enummode=types.ColumnsWidthMode.FROM_LOD_1, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) + + def test_guessColumnsWidth_quantile_modes(): + matrix_for_quantile = [ + ["s"*1, "m"*5, "l"*10], + ["s"*2, "m"*6, "l"*11], + ["s"*3, "m"*7, "l"*12], + ["s"*100, "m"*100, "l"*100] # Outlier + ] + # Col 0 lengths: [1, 2, 3, 100] -> sorted: [1, 2, 3, 100] -> 90th percentile (index 3) = 100 + # Col 1 lengths: [5, 6, 7, 100] -> sorted: [5, 6, 7, 100] -> 90th percentile (index 3) = 100 + # Col 2 lengths: [10, 11, 12, 100] -> sorted: [10, 11, 12, 100] -> 90th percentile (index 3) = 100 + # Widths: (70.9*0.22+0.5)=16.10 -> 15.0, (72.1*0.22+0.5)=16.36 -> 15.0, (73.6*0.22+0.5)=16.69 -> 15.0 + expected_widths_lol = [15.0, 15.0, 15.0] + + assert guessColumnsWidth(matrix_for_quantile, enummode=types.ColumnsWidthMode.FROM_LOL_QUANTILE_90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == expected_widths_lol + assert guessColumnsWidth(matrix_for_quantile, enummode=types.ColumnsWidthMode.FROM_LOL_QUANTILE_90_ONLY_100, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == expected_widths_lol + + lod_for_quantile = [ + {"A": "a"*1, "B": "b"*5}, + {"A": "a"*2, "B": "b"*6}, + {"A": "a"*3, "B": "b"*7}, + {"A": "a"*100, "B": "b"*100} # Outlier + ] + # Keys: "A" (1), "B" (1) + # Col "A" lengths: [1 (key), 1, 2, 3, 100] -> sorted: [1, 1, 2, 3, 100] -> 90th percentile (interpolated) = 61.2 + # Col "B" lengths: [1 (key), 5, 6, 7, 100] -> sorted: [1, 5, 6, 7, 100] -> 90th percentile (interpolated) = 62.8 + # Widths: (61.2*0.22+0.5)=13.96, (62.8*0.22+0.5)=14.32 + expected_widths_lod = [13.96, 14.32] + + assert guessColumnsWidth(lod_for_quantile, enummode=types.ColumnsWidthMode.FROM_LOD_QUANTILE_90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == expected_widths_lod + assert guessColumnsWidth(lod_for_quantile, enummode=types.ColumnsWidthMode.FROM_LOD_QUANTILE_90_ONLY_100, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == expected_widths_lod diff --git a/unogenerator/columnswidth.py b/unogenerator/columnswidth.py index e38a7b5..9204261 100644 --- a/unogenerator/columnswidth.py +++ b/unogenerator/columnswidth.py @@ -220,7 +220,7 @@ def columnsWidth_from_lod_with_quantile(lod, n=100, percentile_value=90, char_to return recommended_widths -def guessColumnsWidth(value: list[dict] | list[list] | list, enummode=types.ColumnsWidthMode.MANUAL, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): +def guessColumnsWidth(value: list[dict] | list[list] | list, colums_width_mode=types.ColumnsWidthMode.MANUAL, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): match enummode: case types.ColumnsWidthMode.MANUAL: return value diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index 045965a..4290ff0 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -1,5 +1,5 @@ -from unogenerator.commons import ColorsNamed, Coord as C, Range as R, guess_object_style, generate_formula_total_string -from unogenerator import ODS +from unogenerator.commons import ColorsNamed, Coord, Range, guess_object_style, generate_formula_total_string +from unogenerator import ODS, types from pydicts import lod from gettext import translation from logging import debug @@ -31,11 +31,11 @@ def row_totals(doc, coord, list_of_totals, color=ColorsNamed.GrayLight, styles=N coord=C.assertCoord(coord) for letter, total in enumerate(list_of_totals): coord_total=coord.addColumnCopy(letter) - coord_total_from=C(coord_total.letter+row_from) + coord_total_from=Coord(coord_total.letter+row_from) if row_to is None: coord_total_to=coord_total.addRowCopy(-1)# row above else: - coord_total_to=C(coord_total.letter+row_to) + coord_total_to=Coord(coord_total.letter+row_to) if styles is None: style=guess_object_style(doc.getValue(coord_total_from)) @@ -63,11 +63,11 @@ def column_totals(doc, coord, list_of_totals, color=ColorsNamed.GrayLight, style coord=C.assertCoord(coord) for number, total in enumerate(list_of_totals): coord_total=coord.addRowCopy(number) - coord_total_from=C(column_from + coord_total.number) + coord_total_from=Coord(column_from + coord_total.number) if column_to is None: coord_total_to=coord_total.addColumnCopy(-1)# row above else: - coord_total_to=C(column_to + coord_total.number) + coord_total_to=Coord(column_to + coord_total.number) if styles is None: style=guess_object_style(doc.getValue(coord_total_from)) @@ -283,12 +283,13 @@ def sheet_stylenames(doc): Args: doc (ODS): The ODS document object. """ - doc.createSheet("Internal style names") - for column, (family, style_names) in enumerate(doc.dict_stylenames.items()): - doc.addCellWithStyle(C("A1").addColumn(column), family, ColorsNamed.Orange, "BoldCenter") - doc.addColumnWithStyle(C("A2").addColumn(column), style_names) - doc.setColumnsWidth([6,6]) - doc.freezeAndSelect("A2") + lod_=[] + for family, style_names in doc.dict_stylenames.items(): + lod_.append({ + "Family":family, + "Styles":style_names + }) + sheet_from_lod(doc, "Internal style names", lod_, freezeandselect="A2", columns_width_mode=types.ColumnsWidthMode.FROM_LOD) def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=ColorsNamed.Orange, columns_width=None, coord_to_freeze="A2", max_rows=1048575): """ @@ -332,16 +333,19 @@ def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=Color columns_width = [columns_width] * len(headers) doc.setColumnsWidth(columns_width) - doc.freezeAndSelect(C.assertCoord(coord_to_freeze)) + doc.freezeAndSelect(Coord.assertCoord(coord_to_freeze)) - -def sheet_from_lod_with_totals(): - pass - -def sheet_from_lod(doc, sheetname, lod_, titulo=None, totalcolumns=False, totalrows=False, freezeandselect=None): +def sheet_from_lod(doc, sheetname, lod_, totalcolumns=False, totalrows=False, freezeandselect=None, titulo=None, **kwargs_columnswidth): """ + kwargs son los parametros de la funcion setColumnsWidth """ + columns_width_mode=kwargs_columnswidth.get("columns_width_mode", types.ColumnsWidthMode.FROM_LOD) + char_to_cm=kwargs_columnswidth.get("char_to_cm", 0.22) + padding_cm=kwargs_columnswidth.get("padding_cm", 0.5) + min_width_cm=kwargs_columnswidth.get("min_width_cm", 2.0) + max_width_cm=kwargs_columnswidth.get("max_width_cm", 15.0) + doc.createSheet(sheetname) if len(lod_)==0: if titulo: @@ -361,11 +365,14 @@ def sheet_from_lod(doc, sheetname, lod_, titulo=None, totalcolumns=False, totalr c_start=Coord("A2")#Empieza abajo - range_final=helper_list_of_ordereddicts_with_totals(doc, c_start, lod_, totalcolumns=totalcolumns, totalrows=totalrows ) - doc.setColumnsWidth(columnsWidth_from_lod(lod_), automatic=False) + range_final=block_from_lod_with_totals(doc, c_start, lod_, totalcolumns=totalcolumns, totalrows=totalrows ) + if totalcolumns or totalrows: + range_cross=cross_totals_from_range (doc, range_final, "#SUM", totalcolumns, totalrows, "BoldCenter", "BoldCenter", False) + doc.setColumnsWidth(lod_, columns_width_mode, char_to_cm, padding_cm, min_width_cm, max_width_cm) if freezeandselect: doc.freezeAndSelect(freezeandselect,freezeandselect, freezeandselect) - return range_final + return range_cross if totalcolumns or totalrows else range_final + diff --git a/unogenerator/unogenerator.py b/unogenerator/unogenerator.py index 6e57dff..f5d8161 100644 --- a/unogenerator/unogenerator.py +++ b/unogenerator/unogenerator.py @@ -660,7 +660,7 @@ def setActiveSheet(self, index): logger.debug(f"Sheet '{self.sheet.Name}' ({self.sheet_index}) is now active") return self.sheet - def setColumnsWidth(self, value: list[dict] | list[list] | list, enummode=types.ColumnsWidthMode.MANUAL, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): + def setColumnsWidth(self, value: list[dict] | list[list] | list, columns_width_mode=types.ColumnsWidthMode.MANUAL, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): """ Sets columns width Can use several types.ColumnsWidthMode @@ -668,7 +668,7 @@ def setColumnsWidth(self, value: list[dict] | list[list] | list, enummode=types Value list is set in cm """ - columns_widths=columnswidth.guessColumnsWidth(value, enummode,char_to_cm, padding_cm, min_width_cm, max_width_cm) + columns_widths=columnswidth.guessColumnsWidth(value, columns_width_mode, char_to_cm, padding_cm, min_width_cm, max_width_cm) columns = self.sheet.getColumns() if columns.Count Date: Thu, 21 May 2026 21:29:22 +0200 Subject: [PATCH 10/34] Trying refactorizing --- unogenerator/demo.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/unogenerator/demo.py b/unogenerator/demo.py index 93c5f0a..746dba0 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -14,9 +14,7 @@ from os import system from pydicts.currency import Currency from pydicts.percentage import Percentage -from unogenerator import ODT_Standard, ODS_Standard, __version__, commons, ColorsNamed, Coord, LibreofficeServer -from unogenerator.helpers import row_title_values_total,column_title_values_total, row_totals, cross_totals_from_range, block_from_lod, helper_list_of_dicts, block_from_lod_with_totals, sheet_stylenames, sheet_split_with_big_lol - +from unogenerator import ODT_Standard, ODS_Standard, __version__, commons, ColorsNamed, Coord, LibreofficeServer, helpers from tqdm import tqdm try: @@ -239,32 +237,32 @@ def demo_ods_standard(language, server): doc.addCellMergedWithStyle("A1:C1","List of rows with row_totals", ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A2", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) - row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="2", row_to="4") + helpers.row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="2", row_to="4") doc.addCellMergedWithStyle("A8:C8","List of columns with row_totals", ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfColumnsWithStyle("A9", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) - row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="9", row_to="11") + helpers.row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="9", row_to="11") doc.addCellMergedWithStyle("A15:E15","List of rows with cross_totals_from_range in rows and columns", ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A16", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - cross_totals_from_range(doc, range_, totalcolumns=True, totalrows=True) + helpers.cross_totals_from_range(doc, range_, totalcolumns=True, totalrows=True) doc.addCellMergedWithStyle("A22:E22","List of rows with cross_totals_from_range in rows", ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A23", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True) #Removes one column to filter first alphanumerical column + helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True) #Removes one column to filter first alphanumerical column doc.addCellMergedWithStyle("A29:E29","List of rows with cross_totals_from_range in columns", ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A30", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False) + helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False) doc.addCellMergedWithStyle("A35:E35","List of rows with cross_totals_from_range in rows showing", ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A36", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True, showing=True) + helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True, showing=True) doc.addCellMergedWithStyle("A42:E42","List of rows with cross_totals_from_range in columns showing", ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A43", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False, showing=True) + helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False, showing=True) doc.setColumnsWidth([3]*20) @@ -273,26 +271,26 @@ def demo_ods_standard(language, server): doc.createSheet("Helpers") doc.setSheetStyle("Portrait") doc.addCellMergedWithStyle("A1:E1","Helper values with total (horizontal)", ColorsNamed.Orange, "BoldCenter") - row_title_values_total(doc, "A2", "Suma 3", [1,2,3]) + helpers.row_title_values_total(doc, "A2", "Suma 3", [1,2,3]) doc.addCellMergedWithStyle("A4:A9","Helper values with total (vertical)", ColorsNamed.Orange, "VerticalBoldCenter") - column_title_values_total(doc, "B4", "Suma 3", [1,2,3, 4]) + helpers.column_title_values_total(doc, "B4", "Suma 3", [1,2,3, 4]) doc.addCellMergedWithStyle("A23:B23","List of ordered dictionaries", ColorsNamed.Orange, "BoldCenter") lod=[] lod.append(OrderedDict({"Singer": "Elvis", "Song": "Fever" })) lod.append(OrderedDict({"Singer": "Roy Orbison", "Song": "Blue angel" })) - block_from_lod(doc, "A24", lod, columns_header=1) + helpers.block_from_lod(doc, "A24", lod, columns_header=1) doc.addCellMergedWithStyle("A28:B28","List of dictionaries", ColorsNamed.Orange, "BoldCenter") - helper_list_of_dicts(doc, "A29", lod, keys=["Song", "Singer"]) + helpers.helper_list_of_dicts(doc, "A29", lod, keys=["Song", "Singer"]) doc.addCellMergedWithStyle("A33:D33","List of ordered dictionaries one method with totals", ColorsNamed.Orange, "BoldCenter") lod=[] lod.append(OrderedDict({"Singer": "Elvis", "Songs": 10000 , "Albums": 100})) lod.append(OrderedDict({"Singer": "Roy Orbison", "Songs": 100, "Albums": 20 })) - block_from_lod_with_totals(doc, "A34", lod, columns_header=1) + helpers.block_from_lod_with_totals(doc, "A34", lod, columns_header=1) doc.setColumnsWidth([3]*20) ##Sort @@ -313,12 +311,12 @@ def demo_ods_standard(language, server): for i in range(1000): lor.append([i, _("String")+" "+ str(i), datetime.now()]) - sheet_split_with_big_lol(doc, "Splits in 400 rows", lor, ["Integer", "String", "Datetime"], columns_width=[2, 5, 5], max_rows=400) + helpers.sheet_split_with_big_lol(doc, "Splits in 400 rows", lor, ["Integer", "String", "Datetime"], columns_width=[2, 5, 5], max_rows=400) ## COLUMNS WIDTH LOD doc.createSheet("ColumnsWidthsLOD") - block_from_lod_with_totals(doc, "A1", lod, columns_header=1) + helpers.block_from_lod_with_totals(doc, "A1", lod, columns_header=1) doc.setColumnsWidth(ODS.columnsWidth_from_lod(lod)) ## COLUMNS WIDTH LOL @@ -338,7 +336,7 @@ def demo_ods_standard(language, server): doc.setColumnsWidth(ODS.columnsWidth_from_list(lol_[0])) ## Sheet with all styles names - sheet_stylenames(doc) + helpers.sheet_stylenames(doc) doc.save(f"unogenerator_example_{language}.ods") doc.export_xlsx(f"unogenerator_example_{language}.xlsx") From 8cd81a4cd80b3ff18a6a3eb7f0b400a1c3cdbbe6 Mon Sep 17 00:00:00 2001 From: turulomio Date: Thu, 21 May 2026 21:40:42 +0200 Subject: [PATCH 11/34] Fixing bugs --- tests/test_columnswidth.py | 28 ++++++++++----------- tests/test_unogenerator.py | 49 ++---------------------------------- unogenerator/columnswidth.py | 2 +- unogenerator/demo.py | 12 ++++----- unogenerator/helpers.py | 18 ++++++------- 5 files changed, 32 insertions(+), 77 deletions(-) diff --git a/tests/test_columnswidth.py b/tests/test_columnswidth.py index b5891b4..c3ec931 100644 --- a/tests/test_columnswidth.py +++ b/tests/test_columnswidth.py @@ -177,29 +177,29 @@ def test_guessColumnsWidth_from_lol_indexed_modes(): # Should use ["Header0_Col0", "Header0_Col1"] # Lengths: 12, 12 -> max = 12 # Widths: (12*0.22+0.5)=3.14 - assert guessColumnsWidth(test_data_lol, enummode=types.ColumnsWidthMode.FROM_LOL_0, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [3.14, 3.14] + assert guessColumnsWidth(test_data_lol, types.ColumnsWidthMode.FROM_LOL_0, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [3.14, 3.14] # FROM_LOL_1 # Should use ["Data1_Col0", "Data1_Col1"] # Lengths: 10, 10 -> max = 10 # Widths: (10*0.22+0.5)=2.7 - assert guessColumnsWidth(test_data_lol, enummode=types.ColumnsWidthMode.FROM_LOL_1, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.7, 2.7] + assert guessColumnsWidth(test_data_lol, types.ColumnsWidthMode.FROM_LOL_1, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.7, 2.7] # FROM_LOL_2 # Should use ["Data2_Col0", "Data2_Col1"] # Lengths: 10, 10 -> max = 10 # Widths: (10*0.22+0.5)=2.7 - assert guessColumnsWidth(test_data_lol, enummode=types.ColumnsWidthMode.FROM_LOL_2, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.7, 2.7] + assert guessColumnsWidth(test_data_lol, types.ColumnsWidthMode.FROM_LOL_2, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.7, 2.7] # Test with fewer elements than expected index test_data_lol_short = [ ["Header0_Col0"] ] # FROM_LOL_0 should work - assert guessColumnsWidth(test_data_lol_short, enummode=types.ColumnsWidthMode.FROM_LOL_0, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [3.14] + assert guessColumnsWidth(test_data_lol_short, types.ColumnsWidthMode.FROM_LOL_0, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [3.14] # FROM_LOL_1 - guessColumnsWidth(test_data_lol_short, enummode=types.ColumnsWidthMode.FROM_LOL_1, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) + guessColumnsWidth(test_data_lol_short, types.ColumnsWidthMode.FROM_LOL_1, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) def test_guessColumnsWidth_from_lod_indexed_modes(): test_data_lod = [ @@ -212,29 +212,29 @@ def test_guessColumnsWidth_from_lod_indexed_modes(): # Should use values from the first dict: ["Val00", "Val01"] # Lengths: 5, 5 -> max = 5 # Widths: (5*0.22+0.5)=1.6 -> 2.0 - assert guessColumnsWidth(test_data_lod, enummode=types.ColumnsWidthMode.FROM_LOD_0, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 2.0] + assert guessColumnsWidth(test_data_lod, types.ColumnsWidthMode.FROM_LOD_0, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 2.0] # FROM_LOD_1 # Should use values from the second dict: ["Val10", "Val11"] # Lengths: 5, 5 -> max = 5 # Widths: (5*0.22+0.5)=1.6 -> 2.0 - assert guessColumnsWidth(test_data_lod, enummode=types.ColumnsWidthMode.FROM_LOD_1, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 2.0] + assert guessColumnsWidth(test_data_lod, types.ColumnsWidthMode.FROM_LOD_1, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 2.0] # FROM_LOD_2 # Should use values from the third dict: ["Val20", "Val21"] # Lengths: 5, 5 -> max = 5 # Widths: (5*0.22+0.5)=1.6 -> 2.0 - assert guessColumnsWidth(test_data_lod, enummode=types.ColumnsWidthMode.FROM_LOD_2, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 2.0] + assert guessColumnsWidth(test_data_lod, types.ColumnsWidthMode.FROM_LOD_2, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 2.0] # Test with fewer elements than expected index test_data_lod_short = [ {"H0C0": "Val00"} ] # FROM_LOD_0 should work - assert guessColumnsWidth(test_data_lod_short, enummode=types.ColumnsWidthMode.FROM_LOD_0, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0] + assert guessColumnsWidth(test_data_lod_short, types.ColumnsWidthMode.FROM_LOD_0, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0] # FROM_LOD_1 - guessColumnsWidth(test_data_lod_short, enummode=types.ColumnsWidthMode.FROM_LOD_1, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) + guessColumnsWidth(test_data_lod_short, types.ColumnsWidthMode.FROM_LOD_1, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) def test_guessColumnsWidth_quantile_modes(): matrix_for_quantile = [ @@ -249,8 +249,8 @@ def test_guessColumnsWidth_quantile_modes(): # Widths: (70.9*0.22+0.5)=16.10 -> 15.0, (72.1*0.22+0.5)=16.36 -> 15.0, (73.6*0.22+0.5)=16.69 -> 15.0 expected_widths_lol = [15.0, 15.0, 15.0] - assert guessColumnsWidth(matrix_for_quantile, enummode=types.ColumnsWidthMode.FROM_LOL_QUANTILE_90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == expected_widths_lol - assert guessColumnsWidth(matrix_for_quantile, enummode=types.ColumnsWidthMode.FROM_LOL_QUANTILE_90_ONLY_100, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == expected_widths_lol + assert guessColumnsWidth(matrix_for_quantile, types.ColumnsWidthMode.FROM_LOL_QUANTILE_90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == expected_widths_lol + assert guessColumnsWidth(matrix_for_quantile, types.ColumnsWidthMode.FROM_LOL_QUANTILE_90_ONLY_100, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == expected_widths_lol lod_for_quantile = [ {"A": "a"*1, "B": "b"*5}, @@ -264,5 +264,5 @@ def test_guessColumnsWidth_quantile_modes(): # Widths: (61.2*0.22+0.5)=13.96, (62.8*0.22+0.5)=14.32 expected_widths_lod = [13.96, 14.32] - assert guessColumnsWidth(lod_for_quantile, enummode=types.ColumnsWidthMode.FROM_LOD_QUANTILE_90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == expected_widths_lod - assert guessColumnsWidth(lod_for_quantile, enummode=types.ColumnsWidthMode.FROM_LOD_QUANTILE_90_ONLY_100, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == expected_widths_lod + assert guessColumnsWidth(lod_for_quantile, types.ColumnsWidthMode.FROM_LOD_QUANTILE_90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == expected_widths_lod + assert guessColumnsWidth(lod_for_quantile, types.ColumnsWidthMode.FROM_LOD_QUANTILE_90_ONLY_100, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == expected_widths_lod diff --git a/tests/test_unogenerator.py b/tests/test_unogenerator.py index b1c4491..8bd501a 100644 --- a/tests/test_unogenerator.py +++ b/tests/test_unogenerator.py @@ -6,7 +6,7 @@ from importlib.resources import files import logging -from unogenerator import can_import_uno +from unogenerator import can_import_uno, types logger = logging.getLogger(__name__) # Get logger for this module @@ -382,7 +382,7 @@ def test_ods_setColumnsWidth(libreoffice_server): # Test de rendimiento: OptimalWidth (Ancho automático) start_auto = datetime.now() # Nota: Al pasar num_columns evitamos que setColumnsWidth llame a getSheetSize() - doc.setColumnsWidth(ODS.columnsWidth_from_lol(lor)) + doc.setColumnsWidth(lor, types.ColumnsWidthMode.FROM_LOL) print(f"Rendimiento setColumnsWidth(): {datetime.now() - start_auto}") @@ -390,48 +390,3 @@ def test_ods_setColumnsWidth(libreoffice_server): if path.exists(filename): remove(filename) - def test_columnsWidth_from_list(): - # Listas vacías - assert ODS.columnsWidth_from_list([]) == [] - - # Comprobación de límites: min_width, cálculo normal y max_width - l = ["12", "a" * 10, "b" * 20, "c" * 100] - # Longitudes: 2, 10, 20, 100 - # 2 * 0.22 + 0.5 = 0.94 -> se ajusta al mínimo de 2.0 - # 10 * 0.22 + 0.5 = 2.7 - # 20 * 0.22 + 0.5 = 4.9 - # 100 * 0.22 + 0.5 = 22.5 -> se ajusta al máximo de 15.0 - assert ODS.columnsWidth_from_list(l) == [2.0, 2.7, 4.9, 15.0] - - def test_columnsWidth_from_lol(): - # Matrices vacías - assert ODS.columnsWidth_from_lol([]) == [] - assert ODS.columnsWidth_from_lol([[]]) == [] - - # Matriz normal (se usan 10 filas para asegurar un cálculo estable del percentil 90) - matrix = [["a" * 10, "b" * 20, "c" * 100]] * 10 - assert ODS.columnsWidth_from_lol(matrix) == [2.7, 4.9, 15.0] - - # Matriz irregular (ragged matrix), probando columnas faltantes en ciertas filas - matrix_ragged = [ - ["a" * 10, "b" * 20], - ["a" * 10] - ] - # Col 1: longitudes 20 y 0 -> percentil 90 = 18.0 -> 18 * 0.22 + 0.5 = 4.46 - assert ODS.columnsWidth_from_lol(matrix_ragged) == [2.7, 4.9] - - def test_columnsWidth_from_lod(): - # LOD vacío - assert ODS.columnsWidth_from_lod([]) == [] - - # LOD normal (10 filas para estabilidad estadística) - lod_data = [{"col1": "a" * 10, "col2": "b" * 20, "col3": "c" * 100}] * 10 - assert ODS.columnsWidth_from_lod(lod_data) == [2.7, 4.9, 15.0] - - # LOD con claves faltantes y valores None - lod_missing = [ - {"col1": "a" * 10, "col2": None}, - {"col1": "a" * 10} - ] - # Col 2: longitudes 0 y 0 -> percentil 90 = 0 -> 0 * 0.22 + 0.5 = 0.5 -> se ajusta a min 2.0 - assert ODS.columnsWidth_from_lod(lod_missing) == [2.7, 2.0] diff --git a/unogenerator/columnswidth.py b/unogenerator/columnswidth.py index 9204261..d50d8c7 100644 --- a/unogenerator/columnswidth.py +++ b/unogenerator/columnswidth.py @@ -221,7 +221,7 @@ def columnsWidth_from_lod_with_quantile(lod, n=100, percentile_value=90, char_to def guessColumnsWidth(value: list[dict] | list[list] | list, colums_width_mode=types.ColumnsWidthMode.MANUAL, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): - match enummode: + match colums_width_mode: case types.ColumnsWidthMode.MANUAL: return value case types.ColumnsWidthMode.FROM_LIST: diff --git a/unogenerator/demo.py b/unogenerator/demo.py index 746dba0..bab0f67 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -14,7 +14,7 @@ from os import system from pydicts.currency import Currency from pydicts.percentage import Percentage -from unogenerator import ODT_Standard, ODS_Standard, __version__, commons, ColorsNamed, Coord, LibreofficeServer, helpers +from unogenerator import ODT_Standard, ODS_Standard, __version__, commons, ColorsNamed, Coord, LibreofficeServer, helpers, types from tqdm import tqdm try: @@ -228,7 +228,7 @@ def demo_ods_standard(language, server): doc.addCellMergedWithStyle("E15:K15", "Merge proof", ColorsNamed.Yellow, style="BoldCenter") doc.setComment("B14", "This is nice comment") - doc.setColumnsWidth(ODS.columnsWidth_from_list(headers)) + doc.setColumnsWidth(headers, types.ColumnsWidthMode.FROM_LIST) doc.freezeAndSelect("B2") ## List of rows @@ -284,7 +284,7 @@ def demo_ods_standard(language, server): helpers.block_from_lod(doc, "A24", lod, columns_header=1) doc.addCellMergedWithStyle("A28:B28","List of dictionaries", ColorsNamed.Orange, "BoldCenter") - helpers.helper_list_of_dicts(doc, "A29", lod, keys=["Song", "Singer"]) + helpers.block_from_lod(doc, "A29", lod, keys=["Song", "Singer"]) doc.addCellMergedWithStyle("A33:D33","List of ordered dictionaries one method with totals", ColorsNamed.Orange, "BoldCenter") lod=[] @@ -317,7 +317,7 @@ def demo_ods_standard(language, server): ## COLUMNS WIDTH LOD doc.createSheet("ColumnsWidthsLOD") helpers.block_from_lod_with_totals(doc, "A1", lod, columns_header=1) - doc.setColumnsWidth(ODS.columnsWidth_from_lod(lod)) + doc.setColumnsWidth(lod,types.ColumnsWidthMode.FROM_LOD) ## COLUMNS WIDTH LOL doc.createSheet("ColumnsWidthsLOL") @@ -327,13 +327,13 @@ def demo_ods_standard(language, server): ["One Two Three", "Four", "Ten Two Three"], ] doc.addListOfRowsWithStyle("A1", lol_) - doc.setColumnsWidth(ODS.columnsWidth_from_lol(lol_)) + doc.setColumnsWidth(lol_, types.ColumnsWidthMode.FROM_LOL) ## COLUMNS WIDTH LOL doc.createSheet("ColumnsWidthsList") doc.addListOfRowsWithStyle("A1", lol_) - doc.setColumnsWidth(ODS.columnsWidth_from_list(lol_[0])) + doc.setColumnsWidth(lol_, types.ColumnsWidthMode.FROM_LOL) ## Sheet with all styles names helpers.sheet_stylenames(doc) diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index 4290ff0..da9717a 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -28,7 +28,7 @@ def row_totals(doc, coord, list_of_totals, color=ColorsNamed.GrayLight, styles=N row_from (str, optional): The row number where the formula range begins. Defaults to "2". row_to (str, optional): The row number where the formula range ends. If None, defaults to the row above `coord`. """ - coord=C.assertCoord(coord) + coord=Coord.assertCoord(coord) for letter, total in enumerate(list_of_totals): coord_total=coord.addColumnCopy(letter) coord_total_from=Coord(coord_total.letter+row_from) @@ -60,7 +60,7 @@ def column_totals(doc, coord, list_of_totals, color=ColorsNamed.GrayLight, style column_from (str, optional): The column letter where the formula range begins. Defaults to "B". column_to (str, optional): The column letter where the formula range ends. If None, defaults to one column before `coord`. """ - coord=C.assertCoord(coord) + coord=Coord.assertCoord(coord) for number, total in enumerate(list_of_totals): coord_total=coord.addRowCopy(number) coord_total_from=Coord(column_from + coord_total.number) @@ -98,7 +98,7 @@ def row_title_values_total( doc, coord, title, values, style_total (str, optional): Style for the total cell. Defaults to None. color_total (int, optional): Background color for the total cell. Defaults to ColorsNamed.GrayLight. """ - coord=C.assertCoord(coord) + coord=Coord.assertCoord(coord) if style_title is None: style_title="Bold" @@ -136,7 +136,7 @@ def column_title_values_total(doc, coord, title, values, style_total (str, optional): Style for the total cell. Defaults to None. color_total (int, optional): Background color for the total cell. Defaults to ColorsNamed.GrayLight. """ - coord=C.assertCoord(coord) + coord=Coord.assertCoord(coord) if style_title is None: style_title="BoldCenter" @@ -182,7 +182,7 @@ def cross_totals_from_range ( Returns: Range: The original data range. """ - range=R.assertRange(range_of_data) + range=Range.assertRange(range_of_data) data_rows=range.numRows() data_columns=range.numColumns() coord_horizontal_title=range.c_start.addColumnCopy(-1).addRowCopy(data_rows) @@ -219,7 +219,7 @@ def cross_totals_from_range ( ## @param columns_header. Integer with the number of columns to apply color_header ## @return Range. Returns the range of the data without headers. Useful to set totals. def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None): - coord_start=C.assertCoord(coord_start) + coord_start=Coord.assertCoord(coord_start) if len(lod_)==0 and keys is None: doc.addCellWithStyle(coord_start, _("No data to show"), ColorsNamed.Red, "BoldCenter") @@ -270,9 +270,9 @@ def block_from_lod_with_totals(doc, coord_start, lod, keys=None, columns_header Range: The range of the data including the generated totals. """ print("QWUITAR CON parametros") - coord_start=C.assertCoord(coord_start) + coord_start=Coord.assertCoord(coord_start) block_from_lod(doc, coord_start, lod, keys, columns_header, color_row_header, color_column_header, color, styles) - range_lod=R.from_iterable_object(coord_start.addRow(1), lod)## Adds q to skip top headers + range_lod=Range.from_iterable_object(coord_start.addRow(1), lod)## Adds q to skip top headers range_lod.c_start.addColumn(columns_header) ## Adds to skip columns headers return cross_totals_from_range (doc, range_lod, key, totalcolumns, totalrows) @@ -327,7 +327,7 @@ def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=Color #Sets width of columns if columns_width is None: - doc.setColumnsWidth(ODS.columnsWidth_from_lol(lor)) + doc.setColumnsWidth(lor, types.ColumnsWidthMode.FROM_LOL) else: if isinstance(columns_width, int): columns_width = [columns_width] * len(headers) From 14fabcaddc6cac735ac0c80079029b0d54efd4b7 Mon Sep 17 00:00:00 2001 From: turulomio Date: Thu, 21 May 2026 22:31:21 +0200 Subject: [PATCH 12/34] FROM_SHEET_CELLS work --- tests/test_helpers.py | 2 +- tests/test_unogenerator.py | 6 +++--- unogenerator/columnswidth.py | 31 +++++++++++++++++++++++++++++++ unogenerator/demo.py | 10 +++++----- unogenerator/helpers.py | 10 +++------- unogenerator/types.py | 1 + unogenerator/unogenerator.py | 5 ++++- 7 files changed, 48 insertions(+), 17 deletions(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 64a8a7c..da8caa8 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -51,7 +51,7 @@ def test_sheet_split_with_big_lol(libreoffice_server): with ODS_Standard(server=libreoffice_server) as doc: helpers.sheet_split_with_big_lol(doc, "Big LOR", r, ["N", "N+1"])#, headers_colors=ColorsNamed.Orange, columns_width=None, coord_to_freeze="A2", max_rows=1048575): - helpers.sheet_split_with_big_lol(doc, "Big LOR de 10", r, ["N", "N+1"], columns_width=3, max_rows=10)#, headers_colors=ColorsNamed.Orange, columns_width=None, coord_to_freeze="A2", max_rows=1048575): + helpers.sheet_split_with_big_lol(doc, "Big LOR de 10", r, ["N", "N+1"], max_rows=10)#, headers_colors=ColorsNamed.Orange, columns_width=None, coord_to_freeze="A2", max_rows=1048575): doc.export_xlsx("sheet_split_with_big_lol.xlsx") diff --git a/tests/test_unogenerator.py b/tests/test_unogenerator.py index 8bd501a..53b2555 100644 --- a/tests/test_unogenerator.py +++ b/tests/test_unogenerator.py @@ -126,7 +126,6 @@ def test_ods_addCellMerged(libreoffice_server): def test_ods_addListOfRows(libreoffice_server): filename="test_ods_addListOfRows.pdf" with ODS("unogenerator/templates/colored.ods", server=libreoffice_server) as doc: - doc.setColumnsWidth([5]*20) #Rows doc.addListOfRows("B1", lor) doc.addListOfRows("A1", []) @@ -140,6 +139,7 @@ def test_ods_addListOfRows(libreoffice_server): doc.addListOfColumnsWithStyle("H7", lor) doc.addListOfColumnsWithStyle("A1", []) + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) doc.export_pdf(filename) remove(filename) @@ -147,7 +147,6 @@ def test_ods_addListOfRows(libreoffice_server): def test_ods_addFormulaArray(libreoffice_server): filename="test_ods_addFormulaArray.pdf" with ODS_Standard(server=libreoffice_server) as doc: - doc.setColumnsWidth([5]*20) # Checks with List of Rows doc.addListOfRows("A1", [["=2+2", "=3+3"], ], formulas=False) @@ -182,13 +181,13 @@ def test_ods_addFormulaArray(libreoffice_server): doc.addListOfColumns("H14", [["=2+2", "=3+3"], ], formulas=True) doc.addListOfColumnsWithStyle("J14", lor_types, formulas=False, styles=lor_types_styles) doc.addListOfColumnsWithStyle("M14", lor_types, formulas=True, styles=lor_types_styles) + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) doc.export_pdf(filename) remove(filename) def test_ods_addRow(libreoffice_server): with ODS("unogenerator/templates/colored.ods", server=libreoffice_server) as doc: - doc.setColumnsWidth([4]*20) #Checking range - range_uno conversions range_=Range("B2:C3") range_uno=range_.uno_range(doc.sheet) @@ -208,6 +207,7 @@ def test_ods_addRow(libreoffice_server): doc.addColumnWithStyle("A1", []) doc.addColumnWithStyle("H7", row) doc.addColumnWithStyle("I7", row, ColorsNamed.Yellow, "Integer") + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) doc.export_pdf("test_ods_addRow.pdf") # Replace colored cell diff --git a/unogenerator/columnswidth.py b/unogenerator/columnswidth.py index d50d8c7..632e1f4 100644 --- a/unogenerator/columnswidth.py +++ b/unogenerator/columnswidth.py @@ -286,3 +286,34 @@ def guessColumnsWidth(value: list[dict] | list[list] | list, colums_width_mode=t return columnsWidth_from_lod_with_quantile(value, None, 90, char_to_cm, padding_cm, min_width_cm, max_width_cm) case types.ColumnsWidthMode.FROM_LOD_QUANTILE_90_ONLY_100: return columnsWidth_from_lod_with_quantile(value, 100, 90, char_to_cm, padding_cm, min_width_cm, max_width_cm) + + case types.ColumnsWidthMode.FROM_SHEET_CELLS: # 'value' is expected to be the ODS document object (doc) + doc = value + # Get all values from the current active sheet, including detailed string representation + sheet_data_detailed = doc.getValues(detailed=True) + + if not sheet_data_detailed: + return [] + + # Construct a list of lists of strings, applying the specified logic for length calculation. + # - If a cell is NOT merged (is_merged is False), its effective length for calculation will be 1. + # - If a cell IS merged (is_merged is True), its actual string content will be used. + # (Note: For merged cells that are not the top-left, 'string' will typically be empty, + # resulting in a length of 0, which is then capped by min_width_cm in columnsWidth_from_lol). + processed_strings_for_width_calc = [] + for row_data in sheet_data_detailed: + current_row_processed_strings = [] + for cell_data in row_data: + if cell_data.get('is_merged') is True: + current_row_processed_strings.append("X") # Placeholder string of length 1 + else: + current_row_processed_strings.append(cell_data.get('string')) + processed_strings_for_width_calc.append(current_row_processed_strings) + + # Now calculate widths using the list of lists of strings + r=columnsWidth_from_lol(processed_strings_for_width_calc, None, char_to_cm, padding_cm, min_width_cm, max_width_cm) + # with open("BORRAME", "a") as f: + # f.write(str(sheet_data_detailed)+"\n") + # f.write(str(r)+"\n") + # f.write(str(processed_strings_for_width_calc)+"\n") + return r diff --git a/unogenerator/demo.py b/unogenerator/demo.py index bab0f67..e822383 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -228,7 +228,7 @@ def demo_ods_standard(language, server): doc.addCellMergedWithStyle("E15:K15", "Merge proof", ColorsNamed.Yellow, style="BoldCenter") doc.setComment("B14", "This is nice comment") - doc.setColumnsWidth(headers, types.ColumnsWidthMode.FROM_LIST) + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) doc.freezeAndSelect("B2") ## List of rows @@ -264,7 +264,7 @@ def demo_ods_standard(language, server): range_=doc.addListOfRowsWithStyle("A43", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False, showing=True) - doc.setColumnsWidth([3]*20) + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) ## HELPERS @@ -291,7 +291,7 @@ def demo_ods_standard(language, server): lod.append(OrderedDict({"Singer": "Elvis", "Songs": 10000 , "Albums": 100})) lod.append(OrderedDict({"Singer": "Roy Orbison", "Songs": 100, "Albums": 20 })) helpers.block_from_lod_with_totals(doc, "A34", lod, columns_header=1) - doc.setColumnsWidth([3]*20) + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) ##Sort doc.createSheet("Sort") @@ -304,14 +304,14 @@ def demo_ods_standard(language, server): doc.addColumnWithStyle("C2", l) doc.sortRange("B2:B10", 0) doc.sortRange("C2:C10", 0, False) - doc.setColumnsWidth([3]*20) + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) ## Split big LOR lor=[] for i in range(1000): lor.append([i, _("String")+" "+ str(i), datetime.now()]) - helpers.sheet_split_with_big_lol(doc, "Splits in 400 rows", lor, ["Integer", "String", "Datetime"], columns_width=[2, 5, 5], max_rows=400) + helpers.sheet_split_with_big_lol(doc, "Splits in 400 rows", lor, ["Integer", "String", "Datetime"], max_rows=400) ## COLUMNS WIDTH LOD diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index da9717a..edc4ff3 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -291,7 +291,7 @@ def sheet_stylenames(doc): }) sheet_from_lod(doc, "Internal style names", lod_, freezeandselect="A2", columns_width_mode=types.ColumnsWidthMode.FROM_LOD) -def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=ColorsNamed.Orange, columns_width=None, coord_to_freeze="A2", max_rows=1048575): +def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=ColorsNamed.Orange, coord_to_freeze="A2", max_rows=1048575): """ Splits a large list of rows across multiple sheets if it exceeds LibreOffice Calc's row limits. @@ -326,12 +326,8 @@ def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=Color doc.addListOfRowsWithStyle("A2", lor[from_:to_]) #Sets width of columns - if columns_width is None: - doc.setColumnsWidth(lor, types.ColumnsWidthMode.FROM_LOL) - else: - if isinstance(columns_width, int): - columns_width = [columns_width] * len(headers) - doc.setColumnsWidth(columns_width) + + doc.setColumnsWidth(lor[from_:to_], types.ColumnsWidthMode.FROM_LOL) doc.freezeAndSelect(Coord.assertCoord(coord_to_freeze)) diff --git a/unogenerator/types.py b/unogenerator/types.py index 1209929..e12d08b 100644 --- a/unogenerator/types.py +++ b/unogenerator/types.py @@ -25,3 +25,4 @@ class ColumnsWidthMode(Enum): FROM_LOD_QUANTILE_90_ONLY_100 = auto() # Para usar los valores del percentil 90 FROM_LOD_ONLY_100 = auto() # Para usar los 100 primeros diccionarios + FROM_SHEET_CELLS= auto()# Como valor se pasa el doc, saca los valores y calcula el width \ No newline at end of file diff --git a/unogenerator/unogenerator.py b/unogenerator/unogenerator.py index f5d8161..73cd589 100644 --- a/unogenerator/unogenerator.py +++ b/unogenerator/unogenerator.py @@ -1311,8 +1311,11 @@ def __cell_to_object(self, cell, detailed=False): if detailed is False: return value else: + + # Determine if the cell is merged + is_merged = cell.IsMerged formula = cell.getFormula() if isformula else None - return {"value":value, "string": cell.getString(), "style":cell.CellStyle, "class": value.__class__.__name__, "is_formula": isformula, "formula": formula} + return {"value":value, "string": cell.getString(), "style":cell.CellStyle, "class": value.__class__.__name__, "is_formula": isformula, "formula": formula, "is_merged": is_merged} ## Return a Range object with the limits of the index sheet def getSheetRange(self): From 88cd0c1be9f91e9da36f72fa4aa97ee2eee8b338 Mon Sep 17 00:00:00 2001 From: turulomio Date: Thu, 21 May 2026 22:41:16 +0200 Subject: [PATCH 13/34] All test work --- tests/test_columnswidth.py | 28 ++++++++++++++-------------- unogenerator/columnswidth.py | 5 ----- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/tests/test_columnswidth.py b/tests/test_columnswidth.py index c3ec931..7814834 100644 --- a/tests/test_columnswidth.py +++ b/tests/test_columnswidth.py @@ -86,9 +86,9 @@ def test_columnsWidth_from_lol_with_quantile(): assert columnsWidth_from_lol_with_quantile(matrix, n=None, percentile_value=90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.15, 13.7] # Test with n=2 (only first two rows) - # Col 1 lengths: [1, 5] -> 90th percentile (interpolated) = 5 + # Col 1 lengths: [1, 5] -> 90th percentile (interpolated) = 4.6 # Col 2 lengths: [10, 20] -> 90th percentile (interpolated) = 19 - # Widths: (5*0.22+0.5)=1.6 -> 2.0, (19*0.22+0.5)=4.68 + # Widths: (4.6*0.22+0.5)=1.512 -> 2.0, (19*0.22+0.5)=4.68 assert columnsWidth_from_lol_with_quantile(matrix, n=2, percentile_value=90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 4.68] def test_columnsWidth_from_lod(): @@ -136,9 +136,9 @@ def test_columnsWidth_from_lod_keys(): {"ShortKey": "val1", "VeryLongKeyIndeed": "val2"}, {"ShortKey": "val3", "VeryLongKeyIndeed": "val4"} ] - # Keys: "ShortKey" (8), "VeryLongKeyIndeed" (19) - # Widths: (8*0.22+0.5)=2.26, (19*0.22+0.5)=4.68 - assert columnsWidth_from_lod_keys(lod_data, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.26, 4.68] + # Keys: "ShortKey" (8), "VeryLongKeyIndeed" (17) + # Widths: (8*0.22+0.5)=2.26, (17*0.22+0.5)=4.24 + assert columnsWidth_from_lod_keys(lod_data, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.26, 4.24] def test_columnsWidth_from_lod_with_quantile(): # Empty LOD @@ -153,18 +153,18 @@ def test_columnsWidth_from_lod_with_quantile(): {"A": "a"*3, "B": "b"*12}, {"A": "a"*1, "B": "b"*100} # Outlier ] - # Keys: "A" (1), "B" (1) - # Col "A" lengths: [1 (key), 1, 5, 10, 2, 3, 1] -> sorted: [1, 1, 1, 2, 3, 5, 10] -> 90th percentile (interpolated) = 61.2 - # Col "B" lengths: [1 (key), 10, 20, 5, 15, 12, 100] -> sorted: [1, 5, 10, 12, 15, 20, 100] -> 90th percentile (interpolated) = 62.8 - # Widths: (61.2*0.22+0.5)=13.96, (62.8*0.22+0.5)=14.32 - assert columnsWidth_from_lod_with_quantile(lod_data, n=None, percentile_value=90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [13.96, 14.32] + # Keys: "A" (1), "B" (1) (from lod_data[0].keys()) + # Col "A" lengths: [1 (key), 1, 5, 10, 2, 3, 1] -> sorted: [1, 1, 1, 2, 3, 5, 10] -> 90th percentile (interpolated) = 7.0 + # Col "B" lengths: [1 (key), 10, 20, 5, 15, 12, 100] -> sorted: [1, 5, 10, 12, 15, 20, 100] -> 90th percentile (interpolated) = 52.0 + # Widths: (7.0*0.22+0.5)=2.04, (52.0*0.22+0.5)=11.94 + assert columnsWidth_from_lod_with_quantile(lod_data, n=None, percentile_value=90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.04, 11.94] # Test with n=2 (only first two records) # Keys: "A" (1), "B" (1) (from lod_data[0].keys()) - # Col "A" lengths: [1 (key), 1, 5] -> sorted: [1, 1, 5] -> 90th percentile (interpolated) = 5 - # Col "B" lengths: [1 (key), 10, 20] -> sorted: [1, 10, 20] -> 90th percentile (interpolated) = 20 - # Widths: (5*0.22+0.5)=1.6 -> 2.0, (20*0.22+0.5)=4.9 - assert columnsWidth_from_lod_with_quantile(lod_data, n=2, percentile_value=90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 4.9] + # Col "A" lengths: [1 (key), 1, 5] -> sorted: [1, 1, 5] -> 90th percentile (interpolated) = 4.2 + # Col "B" lengths: [1 (key), 10, 20] -> sorted: [1, 10, 20] -> 90th percentile (interpolated) = 18 + # Widths: (4.2*0.22+0.5)=1.424 -> 2.0, (18*0.22+0.5)=4.46 + assert columnsWidth_from_lod_with_quantile(lod_data, n=2, percentile_value=90, char_to_cm=CHAR_TO_CM, padding_cm=PADDING_CM) == [2.0, 4.46] def test_guessColumnsWidth_from_lol_indexed_modes(): test_data_lol = [ diff --git a/unogenerator/columnswidth.py b/unogenerator/columnswidth.py index 632e1f4..4357a34 100644 --- a/unogenerator/columnswidth.py +++ b/unogenerator/columnswidth.py @@ -295,11 +295,6 @@ def guessColumnsWidth(value: list[dict] | list[list] | list, colums_width_mode=t if not sheet_data_detailed: return [] - # Construct a list of lists of strings, applying the specified logic for length calculation. - # - If a cell is NOT merged (is_merged is False), its effective length for calculation will be 1. - # - If a cell IS merged (is_merged is True), its actual string content will be used. - # (Note: For merged cells that are not the top-left, 'string' will typically be empty, - # resulting in a length of 0, which is then capped by min_width_cm in columnsWidth_from_lol). processed_strings_for_width_calc = [] for row_data in sheet_data_detailed: current_row_processed_strings = [] From 15fdee67181faffc80aded6a9888a7b63d538ad7 Mon Sep 17 00:00:00 2001 From: turulomio Date: Thu, 21 May 2026 22:47:24 +0200 Subject: [PATCH 14/34] Added fast lor sheet --- unogenerator/helpers.py | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index edc4ff3..dc75210 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -291,6 +291,60 @@ def sheet_stylenames(doc): }) sheet_from_lod(doc, "Internal style names", lod_, freezeandselect="A2", columns_width_mode=types.ColumnsWidthMode.FROM_LOD) +def sheet_from_lol(doc, sheetname, lor, headers, totalcolumns=False, totalrows=False, freezeandselect=None, titulo=None, **kwargs_columnswidth): + """ + Creates a sheet from a list of lists (lol) with headers and optional totals. + + Args: + doc (ODS): The ODS document object. + sheetname (str): The name for the new sheet. + lor (list): The list of lists (data rows). + headers (list): The list of header strings. + totalcolumns (bool, optional): Whether to generate column totals. Defaults to False. + totalrows (bool, optional): Whether to generate row totals. Defaults to False. + freezeandselect (str, optional): Coordinate to freeze panes at. Defaults to None. + titulo (str, optional): An optional title to merge across the top of the sheet. Defaults to None. + **kwargs_columnswidth: Keyword arguments for setColumnsWidth. + """ + columns_width_mode = kwargs_columnswidth.get("columns_width_mode", types.ColumnsWidthMode.FROM_LOL) + char_to_cm = kwargs_columnswidth.get("char_to_cm", 0.22) + padding_cm = kwargs_columnswidth.get("padding_cm", 0.5) + min_width_cm = kwargs_columnswidth.get("min_width_cm", 2.0) + max_width_cm = kwargs_columnswidth.get("max_width_cm", 15.0) + + doc.createSheet(sheetname) + + if not lor and not headers: + if titulo: + doc.addCellMergedWithStyle("A1:D1", titulo, ColorsNamed.Red, "BoldCenter") + else: + doc.addCellMergedWithStyle("A1:D1", "No hay datos", ColorsNamed.Red, "BoldCenter") + return + + c_start = Coord("A1") + + if titulo: + c_end = c_start.addColumnCopy(len(headers) - 1) + range_titulo = Range.from_coords(c_start, c_end) + doc.addCellMergedWithStyle(range_titulo, titulo, ColorsNamed.Orange, "BoldCenter") + c_start.addRow(1) + + doc.addRowWithStyle(c_start, headers, ColorsNamed.Orange, "BoldCenter") + c_start_data = c_start.addRowCopy(1) + + range_data = doc.addListOfRowsWithStyle(c_start_data, lor) + + if totalcolumns or totalrows: + cross_totals_from_range(doc, range_data, "#SUM", totalcolumns, totalrows, "BoldCenter", "BoldCenter", False) + + data_to_measure = [headers] + lor + doc.setColumnsWidth(data_to_measure, columns_width_mode, char_to_cm, padding_cm, min_width_cm, max_width_cm) + + if freezeandselect: + doc.freezeAndSelect(freezeandselect,freezeandselect, freezeandselect) + + return range_data + def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=ColorsNamed.Orange, coord_to_freeze="A2", max_rows=1048575): """ Splits a large list of rows across multiple sheets if it exceeds LibreOffice Calc's row limits. From cb58634def013061f5a5ffeabaf323ee138a26ac Mon Sep 17 00:00:00 2001 From: turulomio Date: Sat, 23 May 2026 07:45:36 +0200 Subject: [PATCH 15/34] Improving ods method --- unogenerator/demo.py | 323 +++++++++++++++++++++++----------------- unogenerator/helpers.py | 2 +- 2 files changed, 186 insertions(+), 139 deletions(-) diff --git a/unogenerator/demo.py b/unogenerator/demo.py index e822383..c21a5e3 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -1,8 +1,5 @@ -## @namespace unogenerator.demo -## @brief Generate ODF example files from uno import getComponentContext -from unogenerator.unogenerator import ODS getComponentContext() import argparse from collections import OrderedDict @@ -28,6 +25,26 @@ ## If arguments is None, launches with sys.argc parameters. Entry point is toomanyfiles:main logger = logging.getLogger(__name__) # Get logger for this module + + +lod_singers=[ + {"Singer": "Elvis", "Songs": 10000 , "Albums": 100}, + {"Singer": "Roy Orbison", "Songs": 100, "Albums": 20 }, +] + + + +lol_numbers=[ + ["One Two Three", "Four", "Ten"], + ["One Two Three", "Four Two Three", "Ten"], + ["One Two Three", "Four", "Ten Two Three"], +] + +lol_thousands=[] +for i in range(1000): + lol_thousands.append([i, _("String")+" "+ str(i), datetime.now()]) + + ## You can call with main(['--pretend']). It's equivalento to os.system('program --pretend') ## @param arguments is an array with parser arguments. For example: ['--argument','9']. def demo(arguments=None): @@ -200,142 +217,17 @@ def demo_ods_standard(language, server): _("This file have been generated with UnoGenerator-{0}. You can see UnoGenerator main page in https://github.com/turulomio/unogenerator").format(__version__), ["unogenerator", "demo", "files"] ) - doc.createSheet("Styles") - - doc.setSheetStyle("Portrait") - doc.setCellName("A1", "MYNAME") - - - headers=[_("Style name"), _("Date and time"), _("Date"), _("Integer"), _("Euros"), _("Dollars"), _("Percentage"), _("Number with 2 decimals"), _("Number with 6 decimals"), _("Time"), _("Boolean")] - doc.addRowWithStyle( "A1", headers, ColorsNamed.Orange, "BoldCenter") - - colors_list=([a for a in dir(ColorsNamed()) if not a.startswith('__')]) - for row, color_str in enumerate(colors_list): - color_key=getattr(ColorsNamed(), color_str) - doc.addCellWithStyle(Coord("A2").addRow(row), color_str, color_key, "Bold") - doc.addCellWithStyle(Coord("B2").addRow(row), datetime.now(), color_key, "Datetime") - doc.addCellWithStyle(Coord("C2").addRow(row), date.today(), color_key, "Date") - doc.addCellWithStyle(Coord("D2").addRow(row), pow(-1, row)*-10000000, color_key, "Integer") - doc.addCellWithStyle(Coord("E2").addRow(row), Currency(pow(-1, row)*12.56, "EUR"), color_key, "EUR") - doc.addCellWithStyle(Coord("F2").addRow(row), Currency(pow(-1, row)*12345.56, "USD"), color_key, "USD") - doc.addCellWithStyle(Coord("G2").addRow(row), Percentage(pow(-1, row)*1, 3), color_key, "Percentage") - doc.addCellWithStyle(Coord("H2").addRow(row), pow(-1, row)*123456789.121212, color_key, "Float6") - doc.addCellWithStyle(Coord("I2").addRow(row), pow(-1, row)*-12.121212, color_key, "Float2") - doc.addCellWithStyle(Coord("J2").addRow(row), (datetime.now()+timedelta(seconds=3600*12*row)).time(), color_key, "Time") - doc.addCellWithStyle(Coord("K2").addRow(row), bool(row%2), color_key, "Bool") - - doc.addCellWithStyle(Coord("E2").addRow(row+1),f"=sum(E2:{Coord('E2').addRow(row).string()})", ColorsNamed.GrayLight, "EUR" ) - doc.addCellMergedWithStyle("E15:K15", "Merge proof", ColorsNamed.Yellow, style="BoldCenter") - doc.setComment("B14", "This is nice comment") - - doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) - doc.freezeAndSelect("B2") - - ## List of rows - doc.createSheet("List of rows or columns") - - - doc.addCellMergedWithStyle("A1:C1","List of rows with row_totals", ColorsNamed.Orange, "BoldCenter") - range_=doc.addListOfRowsWithStyle("A2", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) - helpers.row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="2", row_to="4") - - doc.addCellMergedWithStyle("A8:C8","List of columns with row_totals", ColorsNamed.Orange, "BoldCenter") - range_=doc.addListOfColumnsWithStyle("A9", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) - helpers.row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="9", row_to="11") - - doc.addCellMergedWithStyle("A15:E15","List of rows with cross_totals_from_range in rows and columns", ColorsNamed.Orange, "BoldCenter") - range_=doc.addListOfRowsWithStyle("A16", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helpers.cross_totals_from_range(doc, range_, totalcolumns=True, totalrows=True) - - - doc.addCellMergedWithStyle("A22:E22","List of rows with cross_totals_from_range in rows", ColorsNamed.Orange, "BoldCenter") - range_=doc.addListOfRowsWithStyle("A23", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True) #Removes one column to filter first alphanumerical column - - doc.addCellMergedWithStyle("A29:E29","List of rows with cross_totals_from_range in columns", ColorsNamed.Orange, "BoldCenter") - range_=doc.addListOfRowsWithStyle("A30", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False) - - doc.addCellMergedWithStyle("A35:E35","List of rows with cross_totals_from_range in rows showing", ColorsNamed.Orange, "BoldCenter") - range_=doc.addListOfRowsWithStyle("A36", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True, showing=True) - - doc.addCellMergedWithStyle("A42:E42","List of rows with cross_totals_from_range in columns showing", ColorsNamed.Orange, "BoldCenter") - range_=doc.addListOfRowsWithStyle("A43", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False, showing=True) - - doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) - - - ## HELPERS - doc.createSheet("Helpers") - doc.setSheetStyle("Portrait") - doc.addCellMergedWithStyle("A1:E1","Helper values with total (horizontal)", ColorsNamed.Orange, "BoldCenter") - helpers.row_title_values_total(doc, "A2", "Suma 3", [1,2,3]) - - doc.addCellMergedWithStyle("A4:A9","Helper values with total (vertical)", ColorsNamed.Orange, "VerticalBoldCenter") - helpers.column_title_values_total(doc, "B4", "Suma 3", [1,2,3, 4]) - - - doc.addCellMergedWithStyle("A23:B23","List of ordered dictionaries", ColorsNamed.Orange, "BoldCenter") - lod=[] - lod.append(OrderedDict({"Singer": "Elvis", "Song": "Fever" })) - lod.append(OrderedDict({"Singer": "Roy Orbison", "Song": "Blue angel" })) - helpers.block_from_lod(doc, "A24", lod, columns_header=1) - - doc.addCellMergedWithStyle("A28:B28","List of dictionaries", ColorsNamed.Orange, "BoldCenter") - helpers.block_from_lod(doc, "A29", lod, keys=["Song", "Singer"]) - - doc.addCellMergedWithStyle("A33:D33","List of ordered dictionaries one method with totals", ColorsNamed.Orange, "BoldCenter") - lod=[] - lod.append(OrderedDict({"Singer": "Elvis", "Songs": 10000 , "Albums": 100})) - lod.append(OrderedDict({"Singer": "Roy Orbison", "Songs": 100, "Albums": 20 })) - helpers.block_from_lod_with_totals(doc, "A34", lod, columns_header=1) - doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) - - ##Sort - doc.createSheet("Sort") - l=[7, 3, 2, 5, 6, 0, 9, 4, 10] - doc.addCellWithStyle("A1", "Unsorted", ColorsNamed.Orange, "BoldCenter") - doc.addCellWithStyle("B1", "Sorted ASC", ColorsNamed.Orange, "BoldCenter") - doc.addCellWithStyle("C1", "Sorted DESC", ColorsNamed.Orange, "BoldCenter") - doc.addColumnWithStyle("A2", l) - doc.addColumnWithStyle("B2", l) - doc.addColumnWithStyle("C2", l) - doc.sortRange("B2:B10", 0) - doc.sortRange("C2:C10", 0, False) - doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) - - ## Split big LOR - lor=[] - for i in range(1000): - lor.append([i, _("String")+" "+ str(i), datetime.now()]) - - helpers.sheet_split_with_big_lol(doc, "Splits in 400 rows", lor, ["Integer", "String", "Datetime"], max_rows=400) - - - ## COLUMNS WIDTH LOD - doc.createSheet("ColumnsWidthsLOD") - helpers.block_from_lod_with_totals(doc, "A1", lod, columns_header=1) - doc.setColumnsWidth(lod,types.ColumnsWidthMode.FROM_LOD) - - ## COLUMNS WIDTH LOL - doc.createSheet("ColumnsWidthsLOL") - lol_=[ - ["One Two Three", "Four", "Ten"], - ["One Two Three", "Four Two Three", "Ten"], - ["One Two Three", "Four", "Ten Two Three"], - ] - doc.addListOfRowsWithStyle("A1", lol_) - doc.setColumnsWidth(lol_, types.ColumnsWidthMode.FROM_LOL) - - - ## COLUMNS WIDTH LOL - doc.createSheet("ColumnsWidthsList") - doc.addListOfRowsWithStyle("A1", lol_) - doc.setColumnsWidth(lol_, types.ColumnsWidthMode.FROM_LOL) - ## Sheet with all styles names + demo_ods_sheet_styles(doc) + demo_ods_sheet_list_of_rows_or_columns(doc) + demo_ods_sheet_columns_width_with_list(doc) + demo_ods_sheet_columns_width_with_lol(doc) + demo_ods_sheet_columns_width_with_lod(doc) + demo_ods_sheet_from_lol(doc) + demo_ods_sheet_helpers(doc) + demo_ods_sheet_helpers_from_lod(doc) + demo_ods_sheet_sort(doc) + demo_ods_sheet_split_with_big_lol(doc) helpers.sheet_stylenames(doc) doc.save(f"unogenerator_example_{language}.ods") @@ -595,3 +487,158 @@ def demo_odt_standard(language, server): r= _("unogenerator_documentation_{0}.ods took {1} in {2}").format(language, datetime.now()-doc.start, doc.server.port) # This is an application-level message logger.info(r) return r + + +def demo_ods_sheet_styles(doc): + doc.createSheet("Styles") + + doc.setSheetStyle("Portrait") + doc.setCellName("A1", "MYNAME") + + + headers=[_("Style name"), _("Date and time"), _("Date"), _("Integer"), _("Euros"), _("Dollars"), _("Percentage"), _("Number with 2 decimals"), _("Number with 6 decimals"), _("Time"), _("Boolean")] + doc.addRowWithStyle( "A1", headers, ColorsNamed.Orange, "BoldCenter") + + colors_list=([a for a in dir(ColorsNamed()) if not a.startswith('__')]) + for row, color_str in enumerate(colors_list): + color_key=getattr(ColorsNamed(), color_str) + doc.addCellWithStyle(Coord("A2").addRow(row), color_str, color_key, "Bold") + doc.addCellWithStyle(Coord("B2").addRow(row), datetime.now(), color_key, "Datetime") + doc.addCellWithStyle(Coord("C2").addRow(row), date.today(), color_key, "Date") + doc.addCellWithStyle(Coord("D2").addRow(row), pow(-1, row)*-10000000, color_key, "Integer") + doc.addCellWithStyle(Coord("E2").addRow(row), Currency(pow(-1, row)*12.56, "EUR"), color_key, "EUR") + doc.addCellWithStyle(Coord("F2").addRow(row), Currency(pow(-1, row)*12345.56, "USD"), color_key, "USD") + doc.addCellWithStyle(Coord("G2").addRow(row), Percentage(pow(-1, row)*1, 3), color_key, "Percentage") + doc.addCellWithStyle(Coord("H2").addRow(row), pow(-1, row)*123456789.121212, color_key, "Float6") + doc.addCellWithStyle(Coord("I2").addRow(row), pow(-1, row)*-12.121212, color_key, "Float2") + doc.addCellWithStyle(Coord("J2").addRow(row), (datetime.now()+timedelta(seconds=3600*12*row)).time(), color_key, "Time") + doc.addCellWithStyle(Coord("K2").addRow(row), bool(row%2), color_key, "Bool") + + doc.addCellWithStyle(Coord("E2").addRow(row+1),f"=sum(E2:{Coord('E2').addRow(row).string()})", ColorsNamed.GrayLight, "EUR" ) + doc.addCellMergedWithStyle("E15:K15", "Merge proof", ColorsNamed.Yellow, style="BoldCenter") + doc.setComment("B14", "This is nice comment") + + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + doc.freezeAndSelect("B2") + + + +def demo_ods_sheet_list_of_rows_or_columns(doc): + ## List of rows + doc.createSheet("List of rows or columns") + + + doc.addCellMergedWithStyle("A1:C1","List of rows with row_totals", ColorsNamed.Orange, "BoldCenter") + range_=doc.addListOfRowsWithStyle("A2", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) + helpers.row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="2", row_to="4") + + doc.addCellMergedWithStyle("A8:C8","List of columns with row_totals", ColorsNamed.Orange, "BoldCenter") + range_=doc.addListOfColumnsWithStyle("A9", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) + helpers.row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="9", row_to="11") + + doc.addCellMergedWithStyle("A15:E15","List of rows with cross_totals_from_range in rows and columns", ColorsNamed.Orange, "BoldCenter") + range_=doc.addListOfRowsWithStyle("A16", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) + helpers.cross_totals_from_range(doc, range_, totalcolumns=True, totalrows=True) + + + doc.addCellMergedWithStyle("A22:E22","List of rows with cross_totals_from_range in rows", ColorsNamed.Orange, "BoldCenter") + range_=doc.addListOfRowsWithStyle("A23", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) + helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True) #Removes one column to filter first alphanumerical column + + doc.addCellMergedWithStyle("A29:E29","List of rows with cross_totals_from_range in columns", ColorsNamed.Orange, "BoldCenter") + range_=doc.addListOfRowsWithStyle("A30", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) + helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False) + + doc.addCellMergedWithStyle("A35:E35","List of rows with cross_totals_from_range in rows showing", ColorsNamed.Orange, "BoldCenter") + range_=doc.addListOfRowsWithStyle("A36", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) + helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True, showing=True) + + doc.addCellMergedWithStyle("A42:E42","List of rows with cross_totals_from_range in columns showing", ColorsNamed.Orange, "BoldCenter") + range_=doc.addListOfRowsWithStyle("A43", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) + helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False, showing=True) + + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + + + +def demo_ods_sheet_helpers(doc): + ## HELPERS + doc.createSheet("Helpers") + doc.setSheetStyle("Portrait") + doc.addCellMergedWithStyle("A1:E1","Helper values with total (horizontal)", ColorsNamed.Orange, "BoldCenter") + helpers.row_title_values_total(doc, "A2", "Suma 3", [1,2,3]) + + doc.addCellMergedWithStyle("A4:A9","Helper values with total (vertical)", ColorsNamed.Orange, "VerticalBoldCenter") + helpers.column_title_values_total(doc, "B4", "Suma 3", [1,2,3, 4]) + + doc.addCellMergedWithStyle("A11:E11","Column totals example", ColorsNamed.Orange, "BoldCenter") + doc.addListOfColumnsWithStyle("A12", [[10, 20, 30], [5, 15, 25]], ColorsNamed.White) + helpers.column_totals(doc, "C12", ["#SUM"]*2, column_from="A") + + doc.addCellMergedWithStyle("A23:B23","List of ordered dictionaries", ColorsNamed.Orange, "BoldCenter") + lod=[] + lod.append(OrderedDict({"Singer": "Elvis", "Song": "Fever" })) + lod.append(OrderedDict({"Singer": "Roy Orbison", "Song": "Blue angel" })) + helpers.block_from_lod(doc, "A24", lod, columns_header=1) + + doc.addCellMergedWithStyle("A28:B28","List of dictionaries", ColorsNamed.Orange, "BoldCenter") + helpers.block_from_lod(doc, "A29", lod, keys=["Song", "Singer"]) + + doc.addCellMergedWithStyle("A33:D33","List of ordered dictionaries one method with totals", ColorsNamed.Orange, "BoldCenter") + lod=[] + lod.append(OrderedDict({"Singer": "Elvis", "Songs": 10000 , "Albums": 100})) + lod.append(OrderedDict({"Singer": "Roy Orbison", "Songs": 100, "Albums": 20 })) + helpers.block_from_lod_with_totals(doc, "A34", lod, columns_header=1) + + doc.addCellMergedWithStyle("A40:E40", "Block from LOD with headers", ColorsNamed.Orange, "BoldCenter") + lod_headers = [ + OrderedDict({"ID": 1, "Name": "Product A", "Price": 10.5, "Stock": 100}), + OrderedDict({"ID": 2, "Name": "Product B", "Price": 20.0, "Stock": 50}), + ] + subtitles = [["General", "ID"], ["Details", "Price"]] + helpers.block_from_lod_with_headers(doc, lod_headers, "A41", subtitles=subtitles, titulo="Products") + + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + +def demo_ods_sheet_from_lol(doc): + ## Sheet from LOL and LOD + helpers.sheet_from_lol(doc, "Sheet from LOL", [[1, 2], [3, 4]], ["Col1", "Col2"], totalcolumns=True, totalrows=True, titulo="LOL Table") + +def demo_ods_sheet_helpers_from_lod(doc): + helpers.sheet_from_lod(doc, "Sheet from LOD", lod_singers, totalcolumns=True, totalrows=True, titulo="LOD Table") + +def demo_ods_sheet_sort(doc): + ##Sort + doc.createSheet("Sort") + l=[7, 3, 2, 5, 6, 0, 9, 4, 10] + doc.addCellWithStyle("A1", "Unsorted", ColorsNamed.Orange, "BoldCenter") + doc.addCellWithStyle("B1", "Sorted ASC", ColorsNamed.Orange, "BoldCenter") + doc.addCellWithStyle("C1", "Sorted DESC", ColorsNamed.Orange, "BoldCenter") + doc.addColumnWithStyle("A2", l) + doc.addColumnWithStyle("B2", l) + doc.addColumnWithStyle("C2", l) + doc.sortRange("B2:B10", 0) + doc.sortRange("C2:C10", 0, False) + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + +def demo_ods_sheet_split_with_big_lol(doc): + helpers.sheet_split_with_big_lol(doc, "Splits in 400 rows", lol_thousands, ["Integer", "String", "Datetime"], max_rows=400) + + +def demo_ods_sheet_columns_width_with_lod(doc): + ## COLUMNS WIDTH LOD + doc.createSheet("ColumnsWidthsLOD") + helpers.block_from_lod_with_totals(doc, "A1", lod_singers, columns_header=1) + doc.setColumnsWidth(lod_singers,types.ColumnsWidthMode.FROM_LOD) + +def demo_ods_sheet_columns_width_with_lol(doc): + doc.createSheet("ColumnsWidthsLOL") + doc.addListOfRowsWithStyle("A1", lol_numbers) + doc.setColumnsWidth(lol_numbers, types.ColumnsWidthMode.FROM_LOL) + + +def demo_ods_sheet_columns_width_with_list(doc): + doc.createSheet("ColumnsWidthsList") + doc.addListOfRowsWithStyle("A1", lol_numbers) + doc.setColumnsWidth(lol_numbers, types.ColumnsWidthMode.FROM_LOL) + \ No newline at end of file diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index dc75210..40907fd 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -465,7 +465,7 @@ def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None): #Imprime listas de diccionarios - range_=helper_list_of_ordereddicts(doc, coord.addRowCopy(1),lod_,color_row_header=ColorsNamed.Yellow) + range_=block_from_lod(doc, coord.addRowCopy(1),lod_,color_row_header=ColorsNamed.Yellow) return range_ From af0a4e15a7dbfb9e8ed5994775d04113aaedcfe6 Mon Sep 17 00:00:00 2001 From: turulomio Date: Sat, 23 May 2026 09:43:06 +0200 Subject: [PATCH 16/34] Works --- unogenerator/demo.py | 105 ++++++++++++++++++++++++++----------------- 1 file changed, 64 insertions(+), 41 deletions(-) diff --git a/unogenerator/demo.py b/unogenerator/demo.py index c21a5e3..13aaa18 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -11,7 +11,7 @@ from os import system from pydicts.currency import Currency from pydicts.percentage import Percentage -from unogenerator import ODT_Standard, ODS_Standard, __version__, commons, ColorsNamed, Coord, LibreofficeServer, helpers, types +from unogenerator import ODT_Standard, ODS_Standard, __version__, commons, ColorsNamed, Coord, LibreofficeServer, helpers, types, Range from tqdm import tqdm try: @@ -32,7 +32,8 @@ {"Singer": "Roy Orbison", "Songs": 100, "Albums": 20 }, ] - +lod_singers_rows=len(lod_singers) +lod_singers_columns=len(lod_singers[0].keys()) lol_numbers=[ ["One Two Three", "Four", "Ten"], @@ -40,10 +41,24 @@ ["One Two Three", "Four", "Ten Two Three"], ] +lol_numbers_rows=len(lol_numbers) +lol_numbers_columns=len(lol_numbers[0]) + + +lol_integers=[ + [1,2,3], + [4,5,6], + [7,8,9] +] + lol_thousands=[] for i in range(1000): lol_thousands.append([i, _("String")+" "+ str(i), datetime.now()]) +lol_thousands_rows=len(lol_thousands) +lol_thousands_columns=len(lol_thousands[0]) + + ## You can call with main(['--pretend']). It's equivalento to os.system('program --pretend') ## @param arguments is an array with parser arguments. For example: ['--argument','9']. @@ -219,14 +234,18 @@ def demo_ods_standard(language, server): ) demo_ods_sheet_styles(doc) - demo_ods_sheet_list_of_rows_or_columns(doc) - demo_ods_sheet_columns_width_with_list(doc) - demo_ods_sheet_columns_width_with_lol(doc) - demo_ods_sheet_columns_width_with_lod(doc) - demo_ods_sheet_from_lol(doc) - demo_ods_sheet_helpers(doc) - demo_ods_sheet_helpers_from_lod(doc) demo_ods_sheet_sort(doc) + demo_ods_block_column_row_cross(doc) + + # demo_ods_block_column_row_cros(doc) + # demo_ods_sheet_columns_width_with_list(doc) + # demo_ods_sheet_columns_width_with_lol(doc) + # demo_ods_sheet_columns_width_with_lod(doc) + # demo_ods_sheet_from_lol(doc) + # demo_ods_sheet_helpers(doc) + # demo_ods_sheet_helpers_from_lod(doc) + + demo_ods_sheet_split_with_big_lol(doc) helpers.sheet_stylenames(doc) @@ -523,39 +542,50 @@ def demo_ods_sheet_styles(doc): -def demo_ods_sheet_list_of_rows_or_columns(doc): +def demo_ods_block_column_row_cross(doc): ## List of rows - doc.createSheet("List of rows or columns") - + doc.createSheet("Block column row cross") + c_start=Coord("A1") + + # block_from_lod + doc.addCellMergedWithStyle(Range.from_coords(c_start,c_start.addColumnCopy(lod_singers_columns-1)),"block_from_lod", ColorsNamed.Orange, "BoldCenter") + helpers.block_from_lod(doc, c_start.addRowCopy(), lod_singers) + + + c_start=c_start.addColumn(lod_singers_columns +1).addRow(-1) + doc.addCell("A2", c_start.string()) + doc.addCellMergedWithStyle(Range.from_coords(c_start,c_start.addColumnCopy(lod_singers_columns-1)),"block_from_lod_headers", ColorsNamed.Orange, "BoldCenter") + helpers.block_from_lod(doc, c_start.addRowCopy(), lod_singers, columns_header=1, color_row_header=ColorsNamed.Red) + - doc.addCellMergedWithStyle("A1:C1","List of rows with row_totals", ColorsNamed.Orange, "BoldCenter") - range_=doc.addListOfRowsWithStyle("A2", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) - helpers.row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="2", row_to="4") + # doc.addCellMergedWithStyle(Range(c_start,c_end),"List of rows with row_totals", ColorsNamed.Orange, "BoldCenter") + # range_=doc.addListOfRowsWithStyle("A2", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) + # helpers.row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="2", row_to="4") - doc.addCellMergedWithStyle("A8:C8","List of columns with row_totals", ColorsNamed.Orange, "BoldCenter") - range_=doc.addListOfColumnsWithStyle("A9", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) - helpers.row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="9", row_to="11") + # doc.addCellMergedWithStyle("A8:C8","List of columns with row_totals", ColorsNamed.Orange, "BoldCenter") + # range_=doc.addListOfColumnsWithStyle("A9", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) + # helpers.row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="9", row_to="11") - doc.addCellMergedWithStyle("A15:E15","List of rows with cross_totals_from_range in rows and columns", ColorsNamed.Orange, "BoldCenter") - range_=doc.addListOfRowsWithStyle("A16", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helpers.cross_totals_from_range(doc, range_, totalcolumns=True, totalrows=True) + # doc.addCellMergedWithStyle("A15:E15","List of rows with cross_totals_from_range in rows and columns", ColorsNamed.Orange, "BoldCenter") + # range_=doc.addListOfRowsWithStyle("A16", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) + # helpers.cross_totals_from_range(doc, range_, totalcolumns=True, totalrows=True) - doc.addCellMergedWithStyle("A22:E22","List of rows with cross_totals_from_range in rows", ColorsNamed.Orange, "BoldCenter") - range_=doc.addListOfRowsWithStyle("A23", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True) #Removes one column to filter first alphanumerical column + # doc.addCellMergedWithStyle("A22:E22","List of rows with cross_totals_from_range in rows", ColorsNamed.Orange, "BoldCenter") + # range_=doc.addListOfRowsWithStyle("A23", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) + # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True) #Removes one column to filter first alphanumerical column - doc.addCellMergedWithStyle("A29:E29","List of rows with cross_totals_from_range in columns", ColorsNamed.Orange, "BoldCenter") - range_=doc.addListOfRowsWithStyle("A30", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False) + # doc.addCellMergedWithStyle("A29:E29","List of rows with cross_totals_from_range in columns", ColorsNamed.Orange, "BoldCenter") + # range_=doc.addListOfRowsWithStyle("A30", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) + # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False) - doc.addCellMergedWithStyle("A35:E35","List of rows with cross_totals_from_range in rows showing", ColorsNamed.Orange, "BoldCenter") - range_=doc.addListOfRowsWithStyle("A36", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True, showing=True) + # doc.addCellMergedWithStyle("A35:E35","List of rows with cross_totals_from_range in rows showing", ColorsNamed.Orange, "BoldCenter") + # range_=doc.addListOfRowsWithStyle("A36", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) + # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True, showing=True) - doc.addCellMergedWithStyle("A42:E42","List of rows with cross_totals_from_range in columns showing", ColorsNamed.Orange, "BoldCenter") - range_=doc.addListOfRowsWithStyle("A43", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False, showing=True) + # doc.addCellMergedWithStyle("A42:E42","List of rows with cross_totals_from_range in columns showing", ColorsNamed.Orange, "BoldCenter") + # range_=doc.addListOfRowsWithStyle("A43", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) + # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False, showing=True) doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) @@ -575,14 +605,7 @@ def demo_ods_sheet_helpers(doc): doc.addListOfColumnsWithStyle("A12", [[10, 20, 30], [5, 15, 25]], ColorsNamed.White) helpers.column_totals(doc, "C12", ["#SUM"]*2, column_from="A") - doc.addCellMergedWithStyle("A23:B23","List of ordered dictionaries", ColorsNamed.Orange, "BoldCenter") - lod=[] - lod.append(OrderedDict({"Singer": "Elvis", "Song": "Fever" })) - lod.append(OrderedDict({"Singer": "Roy Orbison", "Song": "Blue angel" })) - helpers.block_from_lod(doc, "A24", lod, columns_header=1) - - doc.addCellMergedWithStyle("A28:B28","List of dictionaries", ColorsNamed.Orange, "BoldCenter") - helpers.block_from_lod(doc, "A29", lod, keys=["Song", "Singer"]) + doc.addCellMergedWithStyle("A33:D33","List of ordered dictionaries one method with totals", ColorsNamed.Orange, "BoldCenter") lod=[] From 36309aec6db40e242bdf5f1ee57deeb098c805af Mon Sep 17 00:00:00 2001 From: turulomio Date: Sat, 23 May 2026 10:09:02 +0200 Subject: [PATCH 17/34] Works --- unogenerator/demo.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/unogenerator/demo.py b/unogenerator/demo.py index 13aaa18..390813d 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -28,8 +28,8 @@ lod_singers=[ - {"Singer": "Elvis", "Songs": 10000 , "Albums": 100}, - {"Singer": "Roy Orbison", "Songs": 100, "Albums": 20 }, + {"Singer": "Elvis", "Songs": 10000 , "Albums": 100, "Best song": "Always on my mind"}, + {"Singer": "Roy Orbison", "Songs": 100, "Albums": 20, "Best song": "Crying"}, ] lod_singers_rows=len(lod_singers) @@ -552,11 +552,20 @@ def demo_ods_block_column_row_cross(doc): helpers.block_from_lod(doc, c_start.addRowCopy(), lod_singers) - c_start=c_start.addColumn(lod_singers_columns +1).addRow(-1) - doc.addCell("A2", c_start.string()) - doc.addCellMergedWithStyle(Range.from_coords(c_start,c_start.addColumnCopy(lod_singers_columns-1)),"block_from_lod_headers", ColorsNamed.Orange, "BoldCenter") + c_start=c_start.addColumn(lod_singers_columns +1) + doc.addCellMergedWithStyle(Range.from_coords(c_start,c_start.addColumnCopy(lod_singers_columns-1)),"block_from_lod (Changing headers)", ColorsNamed.Orange, "BoldCenter") helpers.block_from_lod(doc, c_start.addRowCopy(), lod_singers, columns_header=1, color_row_header=ColorsNamed.Red) + + doc.addCell("A2", c_start.string()) + c_start=c_start.from_index(0, c_start.numberIndex()+lod_singers_rows+3)# column_index, row_index + doc.addCell("A10", c_start.string()) + helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ + ["Singer header", "Singer"], + ["Song header", "Best song"] + ], titulo="block_from_lod_with_headers") + + # doc.addCellMergedWithStyle(Range(c_start,c_end),"List of rows with row_totals", ColorsNamed.Orange, "BoldCenter") # range_=doc.addListOfRowsWithStyle("A2", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) From c382f4809ee7001e3611bf388e48abde42e7d37c Mon Sep 17 00:00:00 2001 From: turulomio Date: Sat, 23 May 2026 10:22:47 +0200 Subject: [PATCH 18/34] Works --- unogenerator/demo.py | 24 +++++++++++++++++++++--- unogenerator/helpers.py | 34 ++++++++++++++++++++++++---------- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/unogenerator/demo.py b/unogenerator/demo.py index 390813d..4949413 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -556,16 +556,34 @@ def demo_ods_block_column_row_cross(doc): doc.addCellMergedWithStyle(Range.from_coords(c_start,c_start.addColumnCopy(lod_singers_columns-1)),"block_from_lod (Changing headers)", ColorsNamed.Orange, "BoldCenter") helpers.block_from_lod(doc, c_start.addRowCopy(), lod_singers, columns_header=1, color_row_header=ColorsNamed.Red) - - doc.addCell("A2", c_start.string()) + # block_from_lod_with_headers c_start=c_start.from_index(0, c_start.numberIndex()+lod_singers_rows+3)# column_index, row_index - doc.addCell("A10", c_start.string()) helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ ["Singer header", "Singer"], ["Song header", "Best song"] ], titulo="block_from_lod_with_headers") + c_start=c_start.addColumn(lod_singers_columns +1) + helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ + ["Singer header", "Singer"], + ["Song header", "Best song"] + ], titulo="block_from_lod_with_headers (With total columns)", totalcolumns=True) + + c_start=c_start.addColumn(lod_singers_columns +3) + helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ + ["Singer header", "Singer"], + ["Song header", "Best song"] + ], titulo="block_from_lod_with_headers (With total rows)", totalrows=True) + + + doc.addCell("A11", c_start.string()) + c_start=c_start.addColumn(lod_singers_columns +2) + doc.addCell("A12", c_start.string()) + helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ + ["Singer header", "Singer"], + ["Song header", "Best song"] + ], titulo="block_from_lod_with_headers (With total columns and rows)", totalcolumns=True, totalrows=True) # doc.addCellMergedWithStyle(Range(c_start,c_end),"List of rows with row_totals", ColorsNamed.Orange, "BoldCenter") # range_=doc.addListOfRowsWithStyle("A2", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index 40907fd..987edbb 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -426,20 +426,31 @@ def sheet_from_lod(doc, sheetname, lod_, totalcolumns=False, totalrows=False, f -def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None): +def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None, totalcolumns=False, totalrows=False, freezeandselect=None, key="#SUM"): """ - Función que imprime desde una celda un lod - El lod usará el orden de las keys creadas, pero tendrá un titulo, que se creará automáticamente - los titulos serán una tupla con el nombre del titulo, primera key - El fin del titulo será la anterior de la segunda key + Writes data from a list of ordered dictionaries with custom header groups, and optional totals. - Permite reordenar facilmente, añadir nuevas filas sin tener que cambiar indices constantemente + Args: + doc (ODS): The ODS document object. + lod_ (list): List of ordered dictionaries containing the data. + coord (Coord or str): Starting coordinate. + subtitles (list): List of lists [title, first_key] defining header groups. + titulo (str, optional): Main title for the entire block. Defaults to None. + totalcolumns (bool, optional): Whether to generate column totals. Defaults to False. + totalrows (bool, optional): Whether to generate row totals. Defaults to False. + freezeandselect (str or Coord, optional): Coordinate to freeze and select. Defaults to None. + key (str, optional): Formula key for totals (e.g., "#SUM"). Defaults to "#SUM". + + Returns: + Range: The data range (excluding headers). """ if len(lod_)==0: doc.addCell(coord, "Sin datos que consigar") return coord=Coord.assertCoord(coord) + coord=Coord(coord.string())# To avoid carry internal coord movements + keys=lod.lod_keys(lod_) @@ -466,9 +477,12 @@ def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None): #Imprime listas de diccionarios range_=block_from_lod(doc, coord.addRowCopy(1),lod_,color_row_header=ColorsNamed.Yellow) - return range_ - + if totalcolumns or totalrows: + cross_totals_from_range(doc, range_, key, totalcolumns, totalrows) + + if freezeandselect: + doc.freezeAndSelect(freezeandselect, freezeandselect, freezeandselect) -def block_from_lod_with_headers_and_totals(doc, lod_, coord, subtitles=[], titulo=None): - pass \ No newline at end of file + return range_ + From 41e7df4c51cfee3019c83502a8914b8029545281 Mon Sep 17 00:00:00 2001 From: turulomio Date: Sat, 23 May 2026 18:49:17 +0200 Subject: [PATCH 19/34] Improving --- unogenerator/commons.py | 11 +++- unogenerator/demo.py | 111 ++++++++++++++++++++++++---------------- unogenerator/helpers.py | 97 ++++++++++++++++++----------------- 3 files changed, 128 insertions(+), 91 deletions(-) diff --git a/unogenerator/commons.py b/unogenerator/commons.py index 02b4325..c53bca8 100644 --- a/unogenerator/commons.py +++ b/unogenerator/commons.py @@ -201,6 +201,13 @@ def __extract(self, strcoord): return (letter,number) + def copy(self): + """ + Returns a copy of the object + """ + return Coord(self.string()) + + ## Returns Coord string ## @return string For example "Z1" def string(self): @@ -227,14 +234,14 @@ def addColumn(self, num=1): ## Add a number of rows to the Coord and return a copy of the object ## @param num Integer Can be positive and negative. When num is negative, if Coord.number is less than 1, returns 1 def addRowCopy(self, num=1): - r=Coord(self.string()) + r=self.copy() r.addRow(num) return r ## Add a number of columns/letters to the Coord and return a copy of the objject ## @param num Integer. Can be positive and negative. When num is negative, if Coord.letter is less than A, returns A. def addColumnCopy(self, num=1): - r=Coord(self.string()) + r=self.copy() r.addColumn(num) return r diff --git a/unogenerator/demo.py b/unogenerator/demo.py index 4949413..85a9114 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -544,46 +544,71 @@ def demo_ods_sheet_styles(doc): def demo_ods_block_column_row_cross(doc): ## List of rows - doc.createSheet("Block column row cross") - c_start=Coord("A1") - - # block_from_lod - doc.addCellMergedWithStyle(Range.from_coords(c_start,c_start.addColumnCopy(lod_singers_columns-1)),"block_from_lod", ColorsNamed.Orange, "BoldCenter") - helpers.block_from_lod(doc, c_start.addRowCopy(), lod_singers) - - - c_start=c_start.addColumn(lod_singers_columns +1) - doc.addCellMergedWithStyle(Range.from_coords(c_start,c_start.addColumnCopy(lod_singers_columns-1)),"block_from_lod (Changing headers)", ColorsNamed.Orange, "BoldCenter") - helpers.block_from_lod(doc, c_start.addRowCopy(), lod_singers, columns_header=1, color_row_header=ColorsNamed.Red) - - # block_from_lod_with_headers - c_start=c_start.from_index(0, c_start.numberIndex()+lod_singers_rows+3)# column_index, row_index - helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ - ["Singer header", "Singer"], - ["Song header", "Best song"] - ], titulo="block_from_lod_with_headers") - - - c_start=c_start.addColumn(lod_singers_columns +1) - helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ - ["Singer header", "Singer"], - ["Song header", "Best song"] - ], titulo="block_from_lod_with_headers (With total columns)", totalcolumns=True) - - c_start=c_start.addColumn(lod_singers_columns +3) - helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ - ["Singer header", "Singer"], - ["Song header", "Best song"] - ], titulo="block_from_lod_with_headers (With total rows)", totalrows=True) - - - doc.addCell("A11", c_start.string()) - c_start=c_start.addColumn(lod_singers_columns +2) - doc.addCell("A12", c_start.string()) - helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ - ["Singer header", "Singer"], - ["Song header", "Best song"] - ], titulo="block_from_lod_with_headers (With total columns and rows)", totalcolumns=True, totalrows=True) + doc.createSheet("block_from_lod") + helpers.block_from_lod(doc, "A1", lod_singers) + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + + doc.createSheet("block_from_lod title") + helpers.block_from_lod(doc, "A1", lod_singers, title="block_from_lod with title") + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + + doc.createSheet("block_from_lod other headers") + helpers.block_from_lod(doc, "A1", lod_singers, columns_header=1, color_row_header=ColorsNamed.Red, title="block_from_lod (With other headers)") + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + + doc.createSheet("block_from_lod empty") + helpers.block_from_lod(doc, "A1", [], columns_header=1, color_row_header=ColorsNamed.Red, title="block_from_lod (Empty)") + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + + doc.createSheet("block_from_lod totalcolumns") + helpers.block_from_lod(doc, "A1", lod_singers, totalcolumns=True, title="block_from_lod (With total columns)") + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + + doc.createSheet("block_from_lod totalrows") + helpers.block_from_lod(doc, "A1", lod_singers, totalrows=True, title="block_from_lod (With total rows)") + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + + doc.createSheet("block_from_lod totalcolumns totalrows") + helpers.block_from_lod(doc, "A1", lod_singers, totalcolumns=True, totalrows=True, title="block_from_lod (With total columns and rows)") + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + + # c_start=c_start.addColumn(lod_singers_columns +1) + # helpers.block_from_lod(doc, c_start.addRowCopy(), lod_singers, columns_header=1, color_row_header=ColorsNamed.Red, totalcolumns=True, title="block_from_lod (With total columns)") + + + # # block_from_lod_with_headers + # c_start=c_start.from_index(0, c_start.numberIndex()+lod_singers_rows+3)# column_index, row_index + # helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ + # ["Singer header", "Singer"], + # ["Song header", "Best song"] + # ], titulo="block_from_lod_with_headers") + + + # c_start=c_start.addColumn(lod_singers_columns +1) + # helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ + # ["Singer header", "Singer"], + # ["Song header", "Best song"] + # ], titulo="block_from_lod_with_headers (With total columns)", totalcolumns=True) + + # c_start=c_start.addColumn(lod_singers_columns +3) + # helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ + # ["Singer header", "Singer"], + # ["Song header", "Best song"] + # ], titulo="block_from_lod_with_headers (With total rows)", totalrows=True) + + + # doc.addCell("A11", c_start.string()) + # c_start=c_start.addColumn(lod_singers_columns +2) + # doc.addCell("A12", c_start.string()) + # helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ + # ["Singer header", "Singer"], + # ["Song header", "Best song"] + # ], titulo="block_from_lod_with_headers (With total columns and rows)", totalcolumns=True, totalrows=True) + + + # # block_from_lod + # c_start=c_start.from_index(0, c_start.numberIndex()+lod_singers_rows+3)# column_index, row_index + # doc.addCellMergedWithStyle(Range(c_start,c_end),"List of rows with row_totals", ColorsNamed.Orange, "BoldCenter") # range_=doc.addListOfRowsWithStyle("A2", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) @@ -638,7 +663,7 @@ def demo_ods_sheet_helpers(doc): lod=[] lod.append(OrderedDict({"Singer": "Elvis", "Songs": 10000 , "Albums": 100})) lod.append(OrderedDict({"Singer": "Roy Orbison", "Songs": 100, "Albums": 20 })) - helpers.block_from_lod_with_totals(doc, "A34", lod, columns_header=1) + helpers.block_from_lod(doc, "A34", lod, columns_header=1) doc.addCellMergedWithStyle("A40:E40", "Block from LOD with headers", ColorsNamed.Orange, "BoldCenter") lod_headers = [ @@ -646,7 +671,7 @@ def demo_ods_sheet_helpers(doc): OrderedDict({"ID": 2, "Name": "Product B", "Price": 20.0, "Stock": 50}), ] subtitles = [["General", "ID"], ["Details", "Price"]] - helpers.block_from_lod_with_headers(doc, lod_headers, "A41", subtitles=subtitles, titulo="Products") + helpers.block_from_lod(doc, lod_headers, "A41", subtitles=subtitles, titulo="Products") doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) @@ -678,7 +703,7 @@ def demo_ods_sheet_split_with_big_lol(doc): def demo_ods_sheet_columns_width_with_lod(doc): ## COLUMNS WIDTH LOD doc.createSheet("ColumnsWidthsLOD") - helpers.block_from_lod_with_totals(doc, "A1", lod_singers, columns_header=1) + helpers.block_from_lod(doc, "A1", lod_singers, columns_header=1) doc.setColumnsWidth(lod_singers,types.ColumnsWidthMode.FROM_LOD) def demo_ods_sheet_columns_width_with_lol(doc): diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index 987edbb..0f72ce0 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -212,30 +212,53 @@ def cross_totals_from_range ( return range_of_data - -## Write cells from a list of ordered dictionaries -## @param lod List of ordered dictionaries -## @param keys. If None write all keys, Else must be a list of keys -## @param columns_header. Integer with the number of columns to apply color_header -## @return Range. Returns the range of the data without headers. Useful to set totals. -def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None): +def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None, totalcolumns=False, totalrows=False, key="#SUM", title=None): + """ + Write cells from a list of ordered dictionaries. + Params: + doc (ODS): The ODS document object. + coord_start (Coord or str): Starting coordinate. + lod_ (list): List of ordered dictionaries. + keys (list, optional): List of keys to write. If None, writes all keys. Defaults to None. + columns_header (int, optional): Number of columns to apply color_header. Defaults to 0. + color_row_header (int, optional): Color for row headers. Defaults to ColorsNamed.Orange. + color_column_header (int, optional): Color for column headers. Defaults to ColorsNamed.Green. + color (int, optional): Color for data cells. Defaults to ColorsNamed.White. + styles (list or str, optional): Styles for data cells. Defaults to None. + totalcolumns: Add a total at the bottom of the block + totalrows: Add a total at the right of the block + key (str, optional): Formula key for totals. Defaults to "#SUM". + title (str, optional): Title for the block. Defaults to None. + Returns: + Range: The range of the data without headers. Useful to set totals. + """ + # Check is a Coord object and makes a copy to avoid internal coord movements coord_start=Coord.assertCoord(coord_start) + c=coord_start.copy() - if len(lod_)==0 and keys is None: - doc.addCellWithStyle(coord_start, _("No data to show"), ColorsNamed.Red, "BoldCenter") + #Prepara el titulo + if title is not None: + if len(lod_)==0: + doc.addCellWithStyle(c, title, ColorsNamed.Red, "BoldCenter") + else: + addtotalrows=1 if totalrows else 0 + if keys is None: + range_title=Range.from_coords("A1", Coord("A1").addColumn(len(lod_[0].keys())-1+addtotalrows)) + else: + range_title=Range.from_coords("A1", Coord("A1").addColumn(len(keys)-1)+addtotalrows) + doc.addCellMergedWithStyle(range_title, title, ColorsNamed.Red, "BoldCenter") + c.addRow(1) + + # Empty lod + if len(lod_)==0: + doc.addCellWithStyle(c, _("No data to show"), ColorsNamed.White, "BoldCenter") return None - #Header + #Headers if keys is None: keys=lod.lod_keys(lod_) - - for column, key in enumerate(keys): - doc.addCellWithStyle(coord_start.addColumnCopy(column), key, color_row_header, "BoldCenter") - coord_data=coord_start.addRowCopy(1) - - - lor=lod.lod2lol(lod_, keys) + doc.addRowWithStyle(c, keys, color_row_header, "BoldCenter") #Generate list of colors colors=[] @@ -246,35 +269,17 @@ def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_ colors.append(color) #Generate list of rows - return doc.addListOfRowsWithStyle(coord_data, lor, colors, styles) + lol_=lod.lod2lol(lod_, keys) + range_block= doc.addListOfRowsWithStyle(c.addRow(), lol_, colors, styles) -def block_from_lod_with_totals(doc, coord_start, lod, keys=None, columns_header=1, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None, totalcolumns=True, totalrows=True, key="#SUM"): - """ - Writes data from a list of ordered dictionaries and appends totals. - - Args: - doc (ODS): The ODS document object. - coord_start (Coord or str): Starting coordinate. - lod (list): List of ordered dictionaries containing the data. - keys (list, optional): List of keys to write. Defaults to None. - columns_header (int, optional): Number of leading columns treated as headers. Defaults to 1. - color_row_header (int, optional): Color for the top header row. Defaults to ColorsNamed.Orange. - color_column_header (int, optional): Color for the side header columns. Defaults to ColorsNamed.Green. - color (int, optional): Default color for data cells. Defaults to ColorsNamed.White. - styles (list or str, optional): Styles for data columns. Defaults to None. - totalcolumns (bool, optional): Whether to generate column totals. Defaults to True. - totalrows (bool, optional): Whether to generate row totals. Defaults to True. - key (str, optional): Formula key to apply (e.g., "#SUM"). Defaults to "#SUM". - - Returns: - Range: The range of the data including the generated totals. - """ - print("QWUITAR CON parametros") - coord_start=Coord.assertCoord(coord_start) - block_from_lod(doc, coord_start, lod, keys, columns_header, color_row_header, color_column_header, color, styles) - range_lod=Range.from_iterable_object(coord_start.addRow(1), lod)## Adds q to skip top headers - range_lod.c_start.addColumn(columns_header) ## Adds to skip columns headers - return cross_totals_from_range (doc, range_lod, key, totalcolumns, totalrows) + # Generate totals + if totalcolumns or totalrows: + if totalcolumns: + if columns_header==0: + columns_header=1 + range_block.c_start.addColumn(columns_header) ## Adds to skip columns headers + return cross_totals_from_range (doc, range_block, key, totalcolumns, totalrows) + return range_block def sheet_stylenames(doc): """ @@ -415,7 +420,7 @@ def sheet_from_lod(doc, sheetname, lod_, totalcolumns=False, totalrows=False, f c_start=Coord("A2")#Empieza abajo - range_final=block_from_lod_with_totals(doc, c_start, lod_, totalcolumns=totalcolumns, totalrows=totalrows ) + range_final=block_from_lod(doc, c_start, lod_, totalcolumns=totalcolumns, totalrows=totalrows ) if totalcolumns or totalrows: range_cross=cross_totals_from_range (doc, range_final, "#SUM", totalcolumns, totalrows, "BoldCenter", "BoldCenter", False) doc.setColumnsWidth(lod_, columns_width_mode, char_to_cm, padding_cm, min_width_cm, max_width_cm) From 46544819ac0981f590fa7f1d98c01a3a3139fc4f Mon Sep 17 00:00:00 2001 From: turulomio Date: Sat, 23 May 2026 19:05:01 +0200 Subject: [PATCH 20/34] Refactorizado totalccolumns --- tests/test_helpers.py | 4 +-- unogenerator/demo.py | 34 +++++++++++----------- unogenerator/helpers.py | 64 ++++++++++++++++++++--------------------- 3 files changed, 51 insertions(+), 51 deletions(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index da8caa8..eec2b1d 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -31,11 +31,11 @@ def test_cross_totals_from_range(libreoffice_server): doc.createSheet("Columns") doc.addRowWithStyle("A1", headers, ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("A2", lor) - helpers.cross_totals_from_range(doc, range_, totalcolumns=True, totalrows=False) + helpers.cross_totals_from_range(doc, range_, column_of_totals=True, row_of_totals=False) doc.createSheet("Rows") doc.addRowWithStyle("B1", headers, ColorsNamed.Orange, "BoldCenter") range_=doc.addListOfRowsWithStyle("B2", lor) - helpers.cross_totals_from_range(doc, range_, totalcolumns=False, totalrows=True) + helpers.cross_totals_from_range(doc, range_, column_of_totals=False, row_of_totals=True) doc.export_pdf("test_cross_totals_from_range.pdf") remove("test_cross_totals_from_range.pdf") diff --git a/unogenerator/demo.py b/unogenerator/demo.py index 85a9114..0ea2a81 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -560,20 +560,20 @@ def demo_ods_block_column_row_cross(doc): helpers.block_from_lod(doc, "A1", [], columns_header=1, color_row_header=ColorsNamed.Red, title="block_from_lod (Empty)") doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) - doc.createSheet("block_from_lod totalcolumns") - helpers.block_from_lod(doc, "A1", lod_singers, totalcolumns=True, title="block_from_lod (With total columns)") + doc.createSheet("block_from_lod column of totals") + helpers.block_from_lod(doc, "A1", lod_singers, column_of_totals=True, title="block_from_lod (With total columns)") doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) - doc.createSheet("block_from_lod totalrows") - helpers.block_from_lod(doc, "A1", lod_singers, totalrows=True, title="block_from_lod (With total rows)") + doc.createSheet("block_from_lod row of totals") + helpers.block_from_lod(doc, "A1", lod_singers, row_of_totals=True, title="block_from_lod (With total rows)") doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) - doc.createSheet("block_from_lod totalcolumns totalrows") - helpers.block_from_lod(doc, "A1", lod_singers, totalcolumns=True, totalrows=True, title="block_from_lod (With total columns and rows)") + doc.createSheet("block_from_lod column of totals row of totals") + helpers.block_from_lod(doc, "A1", lod_singers, column_of_totals=True, row_of_totals=True, title="block_from_lod (With total columns and rows)") doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) # c_start=c_start.addColumn(lod_singers_columns +1) - # helpers.block_from_lod(doc, c_start.addRowCopy(), lod_singers, columns_header=1, color_row_header=ColorsNamed.Red, totalcolumns=True, title="block_from_lod (With total columns)") + # helpers.block_from_lod(doc, c_start.addRowCopy(), lod_singers, columns_header=1, color_row_header=ColorsNamed.Red, column_of_totals=True, title="block_from_lod (With total columns)") # # block_from_lod_with_headers @@ -588,13 +588,13 @@ def demo_ods_block_column_row_cross(doc): # helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ # ["Singer header", "Singer"], # ["Song header", "Best song"] - # ], titulo="block_from_lod_with_headers (With total columns)", totalcolumns=True) + # ], titulo="block_from_lod_with_headers (With total columns)", column_of_totals=True) # c_start=c_start.addColumn(lod_singers_columns +3) # helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ # ["Singer header", "Singer"], # ["Song header", "Best song"] - # ], titulo="block_from_lod_with_headers (With total rows)", totalrows=True) + # ], titulo="block_from_lod_with_headers (With total rows)", row_of_totals=True) # doc.addCell("A11", c_start.string()) @@ -603,7 +603,7 @@ def demo_ods_block_column_row_cross(doc): # helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ # ["Singer header", "Singer"], # ["Song header", "Best song"] - # ], titulo="block_from_lod_with_headers (With total columns and rows)", totalcolumns=True, totalrows=True) + # ], titulo="block_from_lod_with_headers (With total columns and rows)", column_of_totals=True, row_of_totals=True) # # block_from_lod @@ -620,24 +620,24 @@ def demo_ods_block_column_row_cross(doc): # doc.addCellMergedWithStyle("A15:E15","List of rows with cross_totals_from_range in rows and columns", ColorsNamed.Orange, "BoldCenter") # range_=doc.addListOfRowsWithStyle("A16", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - # helpers.cross_totals_from_range(doc, range_, totalcolumns=True, totalrows=True) + # helpers.cross_totals_from_range(doc, range_, column_of_totals=True, row_of_totals=True) # doc.addCellMergedWithStyle("A22:E22","List of rows with cross_totals_from_range in rows", ColorsNamed.Orange, "BoldCenter") # range_=doc.addListOfRowsWithStyle("A23", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True) #Removes one column to filter first alphanumerical column + # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), column_of_totals=False, row_of_totals=True) #Removes one column to filter first alphanumerical column # doc.addCellMergedWithStyle("A29:E29","List of rows with cross_totals_from_range in columns", ColorsNamed.Orange, "BoldCenter") # range_=doc.addListOfRowsWithStyle("A30", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False) + # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), column_of_totals=True, row_of_totals=False) # doc.addCellMergedWithStyle("A35:E35","List of rows with cross_totals_from_range in rows showing", ColorsNamed.Orange, "BoldCenter") # range_=doc.addListOfRowsWithStyle("A36", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=False, totalrows=True, showing=True) + # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), column_of_totals=False, row_of_totals=True, showing=True) # doc.addCellMergedWithStyle("A42:E42","List of rows with cross_totals_from_range in columns showing", ColorsNamed.Orange, "BoldCenter") # range_=doc.addListOfRowsWithStyle("A43", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), totalcolumns=True, totalrows=False, showing=True) + # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), column_of_totals=True, row_of_totals=False, showing=True) doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) @@ -677,10 +677,10 @@ def demo_ods_sheet_helpers(doc): def demo_ods_sheet_from_lol(doc): ## Sheet from LOL and LOD - helpers.sheet_from_lol(doc, "Sheet from LOL", [[1, 2], [3, 4]], ["Col1", "Col2"], totalcolumns=True, totalrows=True, titulo="LOL Table") + helpers.sheet_from_lol(doc, "Sheet from LOL", [[1, 2], [3, 4]], ["Col1", "Col2"], column_of_totals=True, row_of_totals=True, titulo="LOL Table") def demo_ods_sheet_helpers_from_lod(doc): - helpers.sheet_from_lod(doc, "Sheet from LOD", lod_singers, totalcolumns=True, totalrows=True, titulo="LOD Table") + helpers.sheet_from_lod(doc, "Sheet from LOD", lod_singers, column_of_totals=True, row_of_totals=True, titulo="LOD Table") def demo_ods_sheet_sort(doc): ##Sort diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index 0f72ce0..48af080 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -158,8 +158,8 @@ def cross_totals_from_range ( doc, range_of_data, key="#SUM", - totalcolumns=True, - totalrows=True, + column_of_totals=True, + row_of_totals=True, vertical_total_title_style="BoldCenter", horizontal_total_title_style="BoldCenter", showing=False @@ -173,11 +173,11 @@ def cross_totals_from_range ( doc (ODS): The ODS document object. range_of_data (Range or str): The range containing the data values. key (str, optional): Formula key to apply (e.g., "#SUM"). Defaults to "#SUM". - totalcolumns (bool, optional): Whether to generate column totals. Defaults to True. - totalrows (bool, optional): Whether to generate row totals. Defaults to True. + column_of_totals (bool, optional): Whether to generate column totals. Defaults to True. + row_of_totals (bool, optional): Whether to generate row totals. Defaults to True. vertical_total_title_style (str, optional): Style for the vertical total title. Defaults to "BoldCenter". horizontal_total_title_style (str, optional): Style for the horizontal total title. Defaults to "BoldCenter". - showing (bool, optional): If True, shows a 'Sum of totals' cell when either totalcolumns or totalrows is True. Defaults to False. + showing (bool, optional): If True, shows a 'Sum of totals' cell when either column_of_totals or row_of_totals is True. Defaults to False. Returns: Range: The original data range. @@ -189,19 +189,19 @@ def cross_totals_from_range ( coord_vertical_title=range.c_start.addRowCopy(-1).addColumnCopy(data_columns) style_data=guess_object_style(doc.getValue(range.c_end)) - if totalcolumns==True and totalrows==True: + if column_of_totals==True and row_of_totals==True: doc.addCellWithStyle(coord_horizontal_title, _("Total"), ColorsNamed.GrayLight, horizontal_total_title_style) row_totals(doc, coord_horizontal_title.addColumnCopy(1), [key]*data_columns,styles=style_data, row_from=range.c_start.number) doc.addCellWithStyle(coord_vertical_title, _("Total"), ColorsNamed.GrayLight, vertical_total_title_style) column_totals(doc, coord_vertical_title.addRowCopy(1),[key]*(data_rows+1), styles=style_data, column_from=range.c_start.letter) - elif totalcolumns==True: + elif column_of_totals==True: doc.addCellWithStyle(coord_vertical_title, _("Total"), ColorsNamed.GrayLight, vertical_total_title_style) column_totals(doc, coord_vertical_title.addRowCopy(1),[key]*(data_rows+0), styles=style_data, column_from=range.c_start.letter) if showing is True: coord_sum_totals=coord_vertical_title.addRowCopy(data_rows+1) doc.addCellWithStyle(coord_sum_totals, generate_formula_total_string(key, range.c_start.addColumnCopy(data_columns+1), range.c_end.addColumnCopy(1)), ColorsNamed.GrayLight, style_data) doc.addCellWithStyle(coord_sum_totals.addColumnCopy(-1), _("Sum of totals"), ColorsNamed.GrayDark, style_data) - elif totalrows==True: + elif row_of_totals==True: doc.addCellWithStyle(coord_horizontal_title, _("Total"), ColorsNamed.GrayLight, horizontal_total_title_style) row_totals(doc, coord_horizontal_title.addColumnCopy(1),[key]*(data_columns+0), styles=style_data, row_from=range.c_start.number) #1 menos por la esquina if showing is True: @@ -212,7 +212,7 @@ def cross_totals_from_range ( return range_of_data -def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None, totalcolumns=False, totalrows=False, key="#SUM", title=None): +def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None, column_of_totals=False, row_of_totals=False, key="#SUM", title=None): """ Write cells from a list of ordered dictionaries. Params: @@ -225,8 +225,8 @@ def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_ color_column_header (int, optional): Color for column headers. Defaults to ColorsNamed.Green. color (int, optional): Color for data cells. Defaults to ColorsNamed.White. styles (list or str, optional): Styles for data cells. Defaults to None. - totalcolumns: Add a total at the bottom of the block - totalrows: Add a total at the right of the block + column_of_totals: Add a total at the bottom of the block + row_of_totals: Add a total at the right of the block key (str, optional): Formula key for totals. Defaults to "#SUM". title (str, optional): Title for the block. Defaults to None. Returns: @@ -241,11 +241,11 @@ def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_ if len(lod_)==0: doc.addCellWithStyle(c, title, ColorsNamed.Red, "BoldCenter") else: - addtotalrows=1 if totalrows else 0 + add_of_totals=1 if column_of_totals else 0 if keys is None: - range_title=Range.from_coords("A1", Coord("A1").addColumn(len(lod_[0].keys())-1+addtotalrows)) + range_title=Range.from_coords("A1", Coord("A1").addColumn(len(lod_[0].keys())-1+add_of_totals)) else: - range_title=Range.from_coords("A1", Coord("A1").addColumn(len(keys)-1)+addtotalrows) + range_title=Range.from_coords("A1", Coord("A1").addColumn(len(keys)-1)+add_of_totals) doc.addCellMergedWithStyle(range_title, title, ColorsNamed.Red, "BoldCenter") c.addRow(1) @@ -273,12 +273,12 @@ def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_ range_block= doc.addListOfRowsWithStyle(c.addRow(), lol_, colors, styles) # Generate totals - if totalcolumns or totalrows: - if totalcolumns: + if column_of_totals or row_of_totals: + if column_of_totals: if columns_header==0: columns_header=1 range_block.c_start.addColumn(columns_header) ## Adds to skip columns headers - return cross_totals_from_range (doc, range_block, key, totalcolumns, totalrows) + return cross_totals_from_range (doc, range_block, key, column_of_totals, row_of_totals) return range_block def sheet_stylenames(doc): @@ -296,7 +296,7 @@ def sheet_stylenames(doc): }) sheet_from_lod(doc, "Internal style names", lod_, freezeandselect="A2", columns_width_mode=types.ColumnsWidthMode.FROM_LOD) -def sheet_from_lol(doc, sheetname, lor, headers, totalcolumns=False, totalrows=False, freezeandselect=None, titulo=None, **kwargs_columnswidth): +def sheet_from_lol(doc, sheetname, lor, headers, column_of_totals=False, row_of_totals=False, freezeandselect=None, titulo=None, **kwargs_columnswidth): """ Creates a sheet from a list of lists (lol) with headers and optional totals. @@ -305,8 +305,8 @@ def sheet_from_lol(doc, sheetname, lor, headers, totalcolumns=False, totalrows=F sheetname (str): The name for the new sheet. lor (list): The list of lists (data rows). headers (list): The list of header strings. - totalcolumns (bool, optional): Whether to generate column totals. Defaults to False. - totalrows (bool, optional): Whether to generate row totals. Defaults to False. + column_of_totals (bool, optional): Whether to generate column totals. Defaults to False. + row_of_totals (bool, optional): Whether to generate row totals. Defaults to False. freezeandselect (str, optional): Coordinate to freeze panes at. Defaults to None. titulo (str, optional): An optional title to merge across the top of the sheet. Defaults to None. **kwargs_columnswidth: Keyword arguments for setColumnsWidth. @@ -339,8 +339,8 @@ def sheet_from_lol(doc, sheetname, lor, headers, totalcolumns=False, totalrows=F range_data = doc.addListOfRowsWithStyle(c_start_data, lor) - if totalcolumns or totalrows: - cross_totals_from_range(doc, range_data, "#SUM", totalcolumns, totalrows, "BoldCenter", "BoldCenter", False) + if column_of_totals or row_of_totals: + cross_totals_from_range(doc, range_data, "#SUM", column_of_totals, row_of_totals, "BoldCenter", "BoldCenter", False) data_to_measure = [headers] + lor doc.setColumnsWidth(data_to_measure, columns_width_mode, char_to_cm, padding_cm, min_width_cm, max_width_cm) @@ -391,7 +391,7 @@ def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=Color doc.freezeAndSelect(Coord.assertCoord(coord_to_freeze)) -def sheet_from_lod(doc, sheetname, lod_, totalcolumns=False, totalrows=False, freezeandselect=None, titulo=None, **kwargs_columnswidth): +def sheet_from_lod(doc, sheetname, lod_, column_of_totals=False, row_of_totals=False, freezeandselect=None, titulo=None, **kwargs_columnswidth): """ kwargs son los parametros de la funcion setColumnsWidth """ @@ -420,18 +420,18 @@ def sheet_from_lod(doc, sheetname, lod_, totalcolumns=False, totalrows=False, f c_start=Coord("A2")#Empieza abajo - range_final=block_from_lod(doc, c_start, lod_, totalcolumns=totalcolumns, totalrows=totalrows ) - if totalcolumns or totalrows: - range_cross=cross_totals_from_range (doc, range_final, "#SUM", totalcolumns, totalrows, "BoldCenter", "BoldCenter", False) + range_final=block_from_lod(doc, c_start, lod_, column_of_totals=column_of_totals, row_of_totals=row_of_totals ) + if column_of_totals or row_of_totals: + range_cross=cross_totals_from_range (doc, range_final, "#SUM", column_of_totals, row_of_totals, "BoldCenter", "BoldCenter", False) doc.setColumnsWidth(lod_, columns_width_mode, char_to_cm, padding_cm, min_width_cm, max_width_cm) if freezeandselect: doc.freezeAndSelect(freezeandselect,freezeandselect, freezeandselect) - return range_cross if totalcolumns or totalrows else range_final + return range_cross if column_of_totals or row_of_totals else range_final -def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None, totalcolumns=False, totalrows=False, freezeandselect=None, key="#SUM"): +def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None, column_of_totals=False, row_of_totals=False, freezeandselect=None, key="#SUM"): """ Writes data from a list of ordered dictionaries with custom header groups, and optional totals. @@ -441,8 +441,8 @@ def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None, tot coord (Coord or str): Starting coordinate. subtitles (list): List of lists [title, first_key] defining header groups. titulo (str, optional): Main title for the entire block. Defaults to None. - totalcolumns (bool, optional): Whether to generate column totals. Defaults to False. - totalrows (bool, optional): Whether to generate row totals. Defaults to False. + column_of_totals (bool, optional): Whether to generate column totals. Defaults to False. + row_of_totals (bool, optional): Whether to generate row totals. Defaults to False. freezeandselect (str or Coord, optional): Coordinate to freeze and select. Defaults to None. key (str, optional): Formula key for totals (e.g., "#SUM"). Defaults to "#SUM". @@ -483,8 +483,8 @@ def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None, tot #Imprime listas de diccionarios range_=block_from_lod(doc, coord.addRowCopy(1),lod_,color_row_header=ColorsNamed.Yellow) - if totalcolumns or totalrows: - cross_totals_from_range(doc, range_, key, totalcolumns, totalrows) + if column_of_totals or row_of_totals: + cross_totals_from_range(doc, range_, key, column_of_totals, row_of_totals) if freezeandselect: doc.freezeAndSelect(freezeandselect, freezeandselect, freezeandselect) From dc860b18329757d879085deeed593624588127e0 Mon Sep 17 00:00:00 2001 From: turulomio Date: Sat, 23 May 2026 20:26:17 +0200 Subject: [PATCH 21/34] Propuesta de mensaje de commit 1 refactor: improve aesthetics, performance and ODS architecture 2 3 - Set word_wrap=True and vertical alignment to CENTER by default for better readability. 4 - Refactor ODS class to be template-agnostic using configurable defaults. 5 - Move Normal style and fixed height (452) logic specifically to ODS_Standard. 6 - Optimize massive data insertion by skipping style loops for default formatting. 7 - Implement a row-wrap memory system to prevent property overrides. 8 - Enhance block_from_lod_with_headers with integrated totals and freeze panes. 9 - Update standard.ods template with fixed row heights and optimal height disabled. --- unogenerator/commons.py | 6 +- unogenerator/demo.py | 26 ++++- unogenerator/helpers.py | 40 ++++--- unogenerator/templates/standard.ods | Bin 8668 -> 9345 bytes unogenerator/unogenerator.py | 165 +++++++++++++++++++++++----- 5 files changed, 187 insertions(+), 50 deletions(-) diff --git a/unogenerator/commons.py b/unogenerator/commons.py index c53bca8..c54019f 100644 --- a/unogenerator/commons.py +++ b/unogenerator/commons.py @@ -486,15 +486,15 @@ def generate_formula_total_string(key, coord_from, coord_to): s=key return s -def guess_object_style(o): +def guess_object_style(o, default_style="Default"): if o is None: - return "Normal" + return default_style elif o.__class__.__name__=="int": return "Integer" elif o.__class__.__name__=="timedelta": return "TimedeltaSeconds"#TimedeltaISO exits but you can't add or supr elif o.__class__.__name__=="str": - return "Normal" + return default_style elif o.__class__.__name__ in ["Currency", "Money" ]: return o.currency elif o.__class__.__name__=="Percentage": diff --git a/unogenerator/demo.py b/unogenerator/demo.py index 0ea2a81..12df2d2 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -235,7 +235,10 @@ def demo_ods_standard(language, server): demo_ods_sheet_styles(doc) demo_ods_sheet_sort(doc) + demo_ods_sheet_word_wrap(doc) demo_ods_block_column_row_cross(doc) + demo_ods_sheet_from_lod(doc) + demo_ods_sheet_from_lol(doc) # demo_ods_block_column_row_cros(doc) # demo_ods_sheet_columns_width_with_list(doc) @@ -671,7 +674,7 @@ def demo_ods_sheet_helpers(doc): OrderedDict({"ID": 2, "Name": "Product B", "Price": 20.0, "Stock": 50}), ] subtitles = [["General", "ID"], ["Details", "Price"]] - helpers.block_from_lod(doc, lod_headers, "A41", subtitles=subtitles, titulo="Products") + helpers.block_from_lod_with_headers(doc, lod_headers, "A41", subtitles=subtitles, titulo="Products", column_of_totals=True, row_of_totals=True) doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) @@ -679,7 +682,7 @@ def demo_ods_sheet_from_lol(doc): ## Sheet from LOL and LOD helpers.sheet_from_lol(doc, "Sheet from LOL", [[1, 2], [3, 4]], ["Col1", "Col2"], column_of_totals=True, row_of_totals=True, titulo="LOL Table") -def demo_ods_sheet_helpers_from_lod(doc): +def demo_ods_sheet_from_lod(doc): helpers.sheet_from_lod(doc, "Sheet from LOD", lod_singers, column_of_totals=True, row_of_totals=True, titulo="LOD Table") def demo_ods_sheet_sort(doc): @@ -696,6 +699,25 @@ def demo_ods_sheet_sort(doc): doc.sortRange("C2:C10", 0, False) doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) +def demo_ods_sheet_word_wrap(doc): + ## Word Wrap + doc.createSheet("Word Wrap") + long_text = "This is a very long text that should be wrapped if the parameter is set to True, otherwise it should stay in a single line with fixed row height." + doc.addCellWithStyle("A1", "Word Wrap False", ColorsNamed.Orange, "BoldCenter") + doc.addCellWithStyle("A2", long_text, word_wrap=False) + + doc.addCellWithStyle("A4", "Word Wrap True", ColorsNamed.Orange, "BoldCenter") + doc.addCellWithStyle("A5", long_text, word_wrap=True) + + + helpers.block_from_lod(doc, "A7", lod_singers, columns_header=1, word_wrap=False) + + helpers.block_from_lod(doc, "A12", lod_singers, columns_header=1, word_wrap=True) + + + + doc.setColumnsWidth([2, 2, 2, 2], types.ColumnsWidthMode.MANUAL) + def demo_ods_sheet_split_with_big_lol(doc): helpers.sheet_split_with_big_lol(doc, "Splits in 400 rows", lol_thousands, ["Integer", "String", "Datetime"], max_rows=400) diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index 48af080..76e3363 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -212,7 +212,7 @@ def cross_totals_from_range ( return range_of_data -def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None, column_of_totals=False, row_of_totals=False, key="#SUM", title=None): +def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None, column_of_totals=False, row_of_totals=False, key="#SUM", title=None, word_wrap=False): """ Write cells from a list of ordered dictionaries. Params: @@ -229,6 +229,7 @@ def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_ row_of_totals: Add a total at the right of the block key (str, optional): Formula key for totals. Defaults to "#SUM". title (str, optional): Title for the block. Defaults to None. + word_wrap (bool, optional): Enable word wrap and optimal height. Defaults to False. Returns: Range: The range of the data without headers. Useful to set totals. """ @@ -246,7 +247,7 @@ def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_ range_title=Range.from_coords("A1", Coord("A1").addColumn(len(lod_[0].keys())-1+add_of_totals)) else: range_title=Range.from_coords("A1", Coord("A1").addColumn(len(keys)-1)+add_of_totals) - doc.addCellMergedWithStyle(range_title, title, ColorsNamed.Red, "BoldCenter") + doc.addCellMergedWithStyle(range_title, title, ColorsNamed.Red, "BoldCenter", word_wrap=word_wrap) c.addRow(1) # Empty lod @@ -258,7 +259,7 @@ def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_ #Headers if keys is None: keys=lod.lod_keys(lod_) - doc.addRowWithStyle(c, keys, color_row_header, "BoldCenter") + doc.addRowWithStyle(c, keys, color_row_header, "BoldCenter", word_wrap=word_wrap) #Generate list of colors colors=[] @@ -270,7 +271,7 @@ def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_ #Generate list of rows lol_=lod.lod2lol(lod_, keys) - range_block= doc.addListOfRowsWithStyle(c.addRow(), lol_, colors, styles) + range_block= doc.addListOfRowsWithStyle(c.addRow(), lol_, colors, styles, word_wrap=word_wrap) # Generate totals if column_of_totals or row_of_totals: @@ -296,7 +297,7 @@ def sheet_stylenames(doc): }) sheet_from_lod(doc, "Internal style names", lod_, freezeandselect="A2", columns_width_mode=types.ColumnsWidthMode.FROM_LOD) -def sheet_from_lol(doc, sheetname, lor, headers, column_of_totals=False, row_of_totals=False, freezeandselect=None, titulo=None, **kwargs_columnswidth): +def sheet_from_lol(doc, sheetname, lor, headers, column_of_totals=False, row_of_totals=False, freezeandselect=None, titulo=None, word_wrap=False, **kwargs_columnswidth): """ Creates a sheet from a list of lists (lol) with headers and optional totals. @@ -309,6 +310,7 @@ def sheet_from_lol(doc, sheetname, lor, headers, column_of_totals=False, row_of_ row_of_totals (bool, optional): Whether to generate row totals. Defaults to False. freezeandselect (str, optional): Coordinate to freeze panes at. Defaults to None. titulo (str, optional): An optional title to merge across the top of the sheet. Defaults to None. + word_wrap (bool, optional): Enable word wrap and optimal height. Defaults to False. **kwargs_columnswidth: Keyword arguments for setColumnsWidth. """ columns_width_mode = kwargs_columnswidth.get("columns_width_mode", types.ColumnsWidthMode.FROM_LOL) @@ -331,13 +333,13 @@ def sheet_from_lol(doc, sheetname, lor, headers, column_of_totals=False, row_of_ if titulo: c_end = c_start.addColumnCopy(len(headers) - 1) range_titulo = Range.from_coords(c_start, c_end) - doc.addCellMergedWithStyle(range_titulo, titulo, ColorsNamed.Orange, "BoldCenter") + doc.addCellMergedWithStyle(range_titulo, titulo, ColorsNamed.Orange, "BoldCenter", word_wrap=word_wrap) c_start.addRow(1) - doc.addRowWithStyle(c_start, headers, ColorsNamed.Orange, "BoldCenter") + doc.addRowWithStyle(c_start, headers, ColorsNamed.Orange, "BoldCenter", word_wrap=word_wrap) c_start_data = c_start.addRowCopy(1) - range_data = doc.addListOfRowsWithStyle(c_start_data, lor) + range_data = doc.addListOfRowsWithStyle(c_start_data, lor, word_wrap=word_wrap) if column_of_totals or row_of_totals: cross_totals_from_range(doc, range_data, "#SUM", column_of_totals, row_of_totals, "BoldCenter", "BoldCenter", False) @@ -350,7 +352,7 @@ def sheet_from_lol(doc, sheetname, lor, headers, column_of_totals=False, row_of_ return range_data -def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=ColorsNamed.Orange, coord_to_freeze="A2", max_rows=1048575): +def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=ColorsNamed.Orange, coord_to_freeze="A2", max_rows=1048575, word_wrap=False): """ Splits a large list of rows across multiple sheets if it exceeds LibreOffice Calc's row limits. @@ -367,6 +369,7 @@ def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=Color If int, applies to all columns. If list, specifies width per column. Defaults to None. coord_to_freeze (Coord or str, optional): Coordinate to freeze panes at. Defaults to "A2". max_rows (int, optional): Maximum number of rows per sheet (Calc limit is 1,048,576). Defaults to 1048575. + word_wrap (bool, optional): Enable word wrap and optimal height. Defaults to False. """ ceil_=ceil(len(lor)/max_rows) for num_sheet in range(ceil_): @@ -377,12 +380,12 @@ def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=Color else: name=sheet_name doc.createSheet(name) - doc.addRowWithStyle("A1", headers, headers_colors, "BoldCenter") + doc.addRowWithStyle("A1", headers, headers_colors, "BoldCenter", word_wrap=word_wrap) #Splits data from_=max_rows*num_sheet to_=max_rows*(num_sheet+1) if len(lor)>=max_rows*(num_sheet+1) else len(lor) - doc.addListOfRowsWithStyle("A2", lor[from_:to_]) + doc.addListOfRowsWithStyle("A2", lor[from_:to_], word_wrap=word_wrap) #Sets width of columns @@ -391,7 +394,7 @@ def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=Color doc.freezeAndSelect(Coord.assertCoord(coord_to_freeze)) -def sheet_from_lod(doc, sheetname, lod_, column_of_totals=False, row_of_totals=False, freezeandselect=None, titulo=None, **kwargs_columnswidth): +def sheet_from_lod(doc, sheetname, lod_, column_of_totals=False, row_of_totals=False, freezeandselect=None, titulo=None, word_wrap=False, **kwargs_columnswidth): """ kwargs son los parametros de la funcion setColumnsWidth """ @@ -416,11 +419,11 @@ def sheet_from_lod(doc, sheetname, lod_, column_of_totals=False, row_of_totals= else: c_end=Coord("A1").addColumnCopy(len(keys)-1) range_=Range.from_coords("A1", c_end) - doc.addCellMergedWithStyle(range_, titulo, ColorsNamed.Red, "BoldCenter") + doc.addCellMergedWithStyle(range_, titulo, ColorsNamed.Red, "BoldCenter", word_wrap=word_wrap) c_start=Coord("A2")#Empieza abajo - range_final=block_from_lod(doc, c_start, lod_, column_of_totals=column_of_totals, row_of_totals=row_of_totals ) + range_final=block_from_lod(doc, c_start, lod_, column_of_totals=column_of_totals, row_of_totals=row_of_totals, word_wrap=word_wrap ) if column_of_totals or row_of_totals: range_cross=cross_totals_from_range (doc, range_final, "#SUM", column_of_totals, row_of_totals, "BoldCenter", "BoldCenter", False) doc.setColumnsWidth(lod_, columns_width_mode, char_to_cm, padding_cm, min_width_cm, max_width_cm) @@ -431,7 +434,7 @@ def sheet_from_lod(doc, sheetname, lod_, column_of_totals=False, row_of_totals= -def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None, column_of_totals=False, row_of_totals=False, freezeandselect=None, key="#SUM"): +def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None, column_of_totals=False, row_of_totals=False, freezeandselect=None, key="#SUM", word_wrap=True): """ Writes data from a list of ordered dictionaries with custom header groups, and optional totals. @@ -445,6 +448,7 @@ def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None, col row_of_totals (bool, optional): Whether to generate row totals. Defaults to False. freezeandselect (str or Coord, optional): Coordinate to freeze and select. Defaults to None. key (str, optional): Formula key for totals (e.g., "#SUM"). Defaults to "#SUM". + word_wrap (bool, optional): Enable word wrap and optimal height. Defaults to False. Returns: Range: The data range (excluding headers). @@ -469,7 +473,7 @@ def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None, col # Crea titulo principal if titulo is not None: - doc.addCellMergedWithStyle(Range.from_coords(coord,coord.addColumnCopy(len(keys)-1)), titulo, ColorsNamed.Red, "BoldCenter") + doc.addCellMergedWithStyle(Range.from_coords(coord,coord.addColumnCopy(len(keys)-1)), titulo, ColorsNamed.Red, "BoldCenter", word_wrap=word_wrap) coord.addRow(1) @@ -477,11 +481,11 @@ def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None, col for title, key_start, index_start, index_end in subtitles: c_start=coord.addColumnCopy(index_start) c_fin=coord.addColumnCopy(index_end) - doc.addCellMergedWithStyle(Range.from_coords(c_start,c_fin), title, ColorsNamed.Orange, "BoldCenter") + doc.addCellMergedWithStyle(Range.from_coords(c_start,c_fin), title, ColorsNamed.Orange, "BoldCenter", word_wrap=word_wrap) #Imprime listas de diccionarios - range_=block_from_lod(doc, coord.addRowCopy(1),lod_,color_row_header=ColorsNamed.Yellow) + range_=block_from_lod(doc, coord.addRowCopy(1),lod_,color_row_header=ColorsNamed.Yellow, word_wrap=word_wrap) if column_of_totals or row_of_totals: cross_totals_from_range(doc, range_, key, column_of_totals, row_of_totals) diff --git a/unogenerator/templates/standard.ods b/unogenerator/templates/standard.ods index 983bd0fead3fcb09718fd84f2797dba3269c3579..e3405176f95a0d797163ecbeafb3f780f4aa7a8d 100644 GIT binary patch delta 7610 zcmZvB1yEf{)Aqf1aCdjNT-@C~2@c`n1PJbMfS?I}(O|(HfoyPhSPASFyq6?g=E5C{bX%7|_>ipSJM`2B`ef`$H?h=Te5 zn(TwAVf0}9|9!-d@&5gn8b(NsiT*DgW7IG*O4NVJVEbUmKWc=i(Es%}evBG$d;QuT zYUaZA*M3k?Q2w_c&>sW!*WA_3+uqWZ)62pBkYdqgvFT^4X^z$>k(Q?g-ti2E3wmi* zu(~v*nlz(^!!MYs()eTvtATNr+tsF1ZI^5d61yqqt9>FYdv?BtLNL7!A;HTx@PV>f z;w{lYS0_fHt>k)Hj=vQKvY)}RyPl6lUrJbf_=**B zlXD#KsL}o4;qpm^-EHGOwymq|-G9!o@YPTa}OKBTHzHIJV67L!j^?S3T z2udXU(qEFEaN?uZ>Dz|nuE-W&T&Q3g^;ii>kj3kV^6H;^@^rZJzDH60c4)4n10vv< zOCbyoJ$PX;okv$CaWlZSJ{97ABE-)EsH0e>@i^YC6gHh2(smltE-W&`HCbp?Hmxwq z5iHqp zL7j6E4AoCn9S?UEbLB)Cd5^XU?e2C=9m`f0ulXKyH#TU3@eP4GabjSV?ycEd0Bkz( zK?{4u7^7Y8i4suUG1#~E0RjmZ@FRai9v3;~9#_Z^LI09dl~N!Ai8gnnLa!EUaM5_k zsSeBa7=G3|uZHdUrK4LmIG0k&6W$e6)j&;;wf&RIt zTZOj0G%*U!>O73W{AUlkB}Sxe8#{TpgJXc$DNj+DrIpI zK+_q=U#Ha>+-I}~239YnNKqPJ ztE@jy)W&VZ(jg>oXLKl+^kCtBe>T~GPQ}T@_ja<~NXy<5#W%73nz};U68q34Nu@~C zwIqsR*3NFw3bIM{N)Jq0{8|h9on;D^m&qzJJ0fYYWOwR3K>Tr`Tg_suNGMYzikHHazG~N!WDB`3lzyWIJ*evh z-M;8?(3BpNMUa(RLNsFwv6vT(AP0GLAhw$wL)=qMF+yv}BF&a26<^P+92XkdUs!El z76(SaJ{f)qka7vDg{4bpB0a`xtlFKC;6WI)Z3ae>LwieaSc3NjNA7NQU3^wahMje zXCv;xm73Ke^z#XueoVb6IHOIpkOK}7U6caebP+rp0k^OHFS?(9uVYr~YSHek$XHmi zQTm9^6?|=%H<>(kA12?6`gn6IKp>LJ;c`xNq9wF1?@!+2o>8-3x7Q$o5hc(v_WdMs zf9(fJ*LLG!r)w39dDUkj@g}y1y>nmeOUlcksIzP<72|_+0;bHHw1Y!H?)$Bh+d9OH z9g)083g8^Vtf}YaqOucfZjp_(OBSv1H9Fd*{Nlgdy*C}>?sJ)%H76ER5csC4-%aAu z>B1!XD8=v+dapG~i_F_(er$VkYifQtc9Zv>2-DK3Xm~aB{y;q3G1OW>cx?G00!Lzd zSMmtB`D(_Z;#X?TGS)17oaEr5!p(^PEL)&z1^^S6<9xnpdq8TQ24@|d2IHgqSSL~= zUEB{inmW?7*nN}pDKhlg&dT?UVZLWtwc@I75G`6!vn0`$bDkZz??Q{1W23@oX{L$S zfDWk7=5#XaYDB&ANB>kSF3y@mDG}E&#!0<{w2tp{vN^Rbak%{vQ6#3P=%;tN`P?Z1 z4Ztp*+O{aCW3WSK(4jeytvDwlc^rs!O?-9QjnzmFZA`7;W)#eMiT8^5=eaapgb#6$ zMCJtCWZ!h9!QBA1VL?CYPYLYxOwnm$-4LJbh`RE@e41Lyr?lVJ2=ba^GlanxlS6Jk zE)}8*MVEM$v00?f%Y7tQsifCX16q>E$Y zfG}4L%gyTEJv|A)9mElLqZ+mOP00AZx8hRxESoX)~jfUZimMiA;Q5I?G8lm5rM71 zq5C<-!MA5b?1Y^aM+JhN%)XyWuTEUrlpmauZnX znsqgQPT1!~fZM~^&)d_x@w6xYdF$e5%?52)Ay-SX50?ZWm>haYr|)&_wQCIRwP!oB;KK zfJKZ7x1!i3qAVG;(>$-y9mNtAQSAEx#v;Pyq06j>^iP{?Mw!E*Vs@gAt(gZexP7Vi z5AwIVlML1!njQ?iIW%6xK7&(j!SZ<9@-3Djv6Sn@aAT=PV$j8axbkBE*(yK*-@1L1 z+<%*w;!`$sXT8OOHM_2#sCh7?iK5@f%y3r81>Y-fdZ5v0)N8*_V_@v7K1onPWlg)D zlo>7)W&+&p&V`9tmNj%%+`J`XEiUj^W0aHd(q? zbtKheyEeRXV%Q4LWg(<%;`rlb5h;OR!`GYM?x+_zHfl4h=f~{K@gD)wM&Dyqr!gW$ zw20`@2T6V9_lY;EqdBYOFpEXJnt9qym3mwQDsaD1#k|aN##pXC6;jyFqUxF69qdQ4 zz^#EYT&yr^wfQ~=l`>tNxeL(n&ADdA#3*qo8(mJq3TEwm<@&*Yyd ze||V;oou-+5Ge(PT+K8@L<$JuKS3RwriKi>CdR}`^y5^nS;q(3B%k+3oNmKGzh0$C zZ{$VPnd&D)44JHFaB?7Iv@MEid}r}Px5rzVS-Hsks%hFhKZ@4v7G&?!IeC&NepruZ ziSd<1mok*3=H%K}Hrub!Cxh$VWBFAizo>oCjvkdHxjlZo$KpmHEj~7K(^nmF7KZnhZ@(4YXS2s8gM{ zjdOhr=|a}n*tqq^;C!#e`W&I=I=$7p2BRBnHf2l1o%uj{?|h-n3PXDF9wnn^b*U>>Be=`v1-#t^UF`Nka zuVyP1kqs!HG`2i63eF8*rhJdTlf27hLbpA`Wy!uSiA}jki7{$-9kZ^fo_1c57|h7mQ+Kj3pCC8737KLZ zZh6l+8aJtmqX(+a7NE3}ts@tqMx7g~)&q6tUDD(N`zzC%Mvbefhhsi%N*=md?)W0e``(p9{Uz201+{1N|k9odS}x*5?lF=D)DlhjPN4b91(b>7^(a_TY9@Dq@# zKnR6d8Xt73aw2^`Ua41Ql!~+!`kMc7_Q}P;%Sv;RB4CwC1xHYmMizs-U zj-+KVWV7xw_PYz=hQW6|It?`M{jC^GpnBwDb#Z$R>33bM4iZLdT~OW@?!#01a9pgg z+A;pA1vFU(ivIoGfhc?5@VfX9e0Uo*jpvz`7>Sx3UGuBDC}; zsM}1m9C3J@$fG{W>W}KCE11;X&&ZM^b$QxdTw#4KD(_-b%IRhD0Lc#f>Z(Pz0UW* z&1$ARD#t_uZ_nX3m4dvaNr!`aFOhMgyZ(+4F;LU8tJ5JBQN5i=jGohqdDWi%Q&<7@ z+e4{&aT|(0g|Ct^gy@|@5dffgr`Hl{myer4V%DNFWg>Mu50;q@n0>rb3@^A4-OLC? zGtg}o_}~EVHi@A`O`b^0Q#d|WfNx48fd0O=(<$34qqkePgM4-QqmihnW5BWznrxBy zD_zV&p9~vzlUcJogu8?J-wOhk0Z$4{ zI;L~u`+w7=ZmfFg6o~MyNt^GmMEi0=$@g7^|L|vF4~z7Khx-o6DaW*Fq8>5v?JQr; z!Zs0#XXmJ2mGT;yRkZPgzRN2vJ5I(Hy*@=+uI8%u4oD_1=Kd@acyS8$!^6-%w0RY( zX?Q+-OV`_Ue4B5|(ASWfzl++a2Wg80Oc~AvY+gjIbL^ED7CKL9Et7X>jh6@oo3QVM zs&wn=a9qvr&FK?FA@qNEi7%AFKw{Fw#PD;tez2yr)Jf2|xG(joa|U#%rDx#uS@wL+ zjeUr>Cr!`YgX5YIbl9W6yc{#KIkXQe%4?32J0XEr{atE#R?XY*LL5UDIt3Q3z+h=^ zpDw;m3L#{!k8GH7bEV7tV`sjiVvYL1;hX7Y+e=rSqajhB{yB<@YmB9h;QYqR%xAQW zwZjD)n;_|%wbGv4+RjXWjrOr=AV!?^icEKv`1+k#cm3*l`(zobX~poG{zfSOPjOR? zZDf^ghFG|x^w!|iboE=aEgLwYr_A*)ccaqoQ*VQzFE+5KkWFuK=BR!LE*xTxng|Ag zaQSU&^`~_e^Kq zgys0l1*b8^6t8SSLPRpbcs!X11nCJZxOmd?toR7b#n`}V=ZCIC)Soo->Sdx-#!3+t z$Lu_`KhlGTn@w2=^O$dNGyQYyU$76>7^!AChHEQlBlXR_8cXQ3R1)T@tjd;W(ml4c zuN?}h9(}aV72-dN=Z-^!)se~AIM>jz zoW##S;B5qa%EWvDLf*Y$;t0-O*NfvdBKgKKOc>Z2`iM58N2c7~Ym5G=y9gX2x&l3) zWpTK{OI*qEua84a#IOl$(tJ3@N!rxi45y=I<5vLCif@UXG2%#;2wGM2%LC^-8quld zEgC)=yqBhm@HLvPmV3^!JuO0e5~4kh&3xN<0Mf);bt2+3tE0C}0;hJ))tSn*kDyex zKtZ=!m>c6>TVz!wKzj&R@0$jKMh>G;c1vh4Mv_><%gLZIPM;wN&U+rM(oW-} z+no%D&bWvx`>3$1INZPl$v8DahoOnQIjunVbgd|k4(;yy+1jYdhc9$@`#4az$UO|= zD7CA$xl=gy0_&bfnP>*n+v(!=VvJ(gUGYBj9`fop4c1yqlp~W=ywK z(hN+R1U(1C{5U~%x=`SC+}uy=NR`)k@AAr}_^>0mQ{jX7KI(n>v@{$Yj0B%!R5AlZ zM<9`iU2xv5;BISriPe$UNlLNaD!_dk_j@s;-_53izrz+E%d0$L{wdk2F(SI<1$c3h zWsV+)B;_~k3T_O8*9sx^dt^e0Z(cbLI&viOjJ?dtfg zICOf8b@!J|BiR#WbI5bA;Z|@yJ%GFRV6yJ9P08nlRS_hFF64??&yG$Aosi@7QA|8& z1^5KfYkc;&3$ue4CZ1l*g7FAXXkS!ny?Z^byM`VwC~qlmgS|a-9qx0}xU&uDvE4tZ zk79y&cdX*`#@|mPH!2)Ft|0v!mCE2O=PK&j60x8^J$n;3C#u@z&KV+}|NXYuNcb{X zBlXU2Q-s!kN1KV^W42{TVaH@hy%vsw%c`i-GWCAsyE@MORR!dTrG3mZDtLLoO=VkG z-3_}Qd_wbzckf9nswb9bOj9Y)dIIH0*=D7cl+E6o`FQWCuao8c~&G}b}dNq%|AP;ezzF#ZzNWks?qh_ zR^Q~Z$1;CsQ~9L}ZQt6~CUaS-VfLJ&_t7@9M` zp607_!eC_dMFW`$r(Q9@Q=1=y`@$JmiAzMnJZxZz?lCGlDjU-?WX7nlXiO&ldf)cE zt)m(xQU3MI)KWYCtuWhC<59PwK1D~HZZ^4B>G3nXTOn&RSY$@qVQ%JTPpMR2TP&lcK&W2!w|B`vHUAXAr{}$fo}4 zMsCTN0lRK)Y`IIJ(8&fpWfda1p26G+ynJNMig>y>1!>aI&5Iv3p2rd4E{z8DAO5u!_S2<8}p@p|Wo@cJK-PDoHCuQF?~?rQTjIhPhRlU+I|` zy;8+FSo4ot<9Mc}7{G%`sew$DQ-=KP$IvM!GIECDp0-00fHdn#*KM3q|aL=tqq- zEg$`It%gFHt0G@yZH;h7YS#9{Yo%kX=0_<@V_XP@cgImo#Nl&f=nhd*^ws4?WBYIS zXH&c_<^APjc7)&F2gBim{$cE3&J?^jzbU&vm@*I*td4>PHU`GQ{13R3k`x>d!vW*N zPAG}~ipmEnQti-|o6%2dwH}bd25?Bir z%m1?gw+4=PB7})i;r=E1t(ycc-iaLcjRMK&Z?^Bxp7uA}_s`IP`;+_o+XsKItGl$B zLj2{QH@`}zUoG(;!xjssPEAVv+dqF7#J}rdv_BvqP%$>Fjhf`eZ_uB`*Dp%+kFosA zLFX5h09=<}dsI925|U_z%S(kRk=lmWJ_voBT7<1pflXQ^Klf Ln2?aDe@p)l|5y*E delta 6948 zcmZvBby!th)BZV!1}SL}IMPU`v~&uHbf=W0q=Ia^`zUp!8wBZ=Zj|osZt48t^L$>f z-~0RKpS`ZR*UY;1n!VS$XU0cvRs!pl0s8G45lm^R3GRD2|wFjGp!(OlBbn7>I z$5=1X0QbIP3s>`T$0PUYtxZs|%J^54q-Z_}^;fkVgAT)qgI9zv;=Z9@-+V=Mu#&1r zSQ*0dBydq1Ih>Zu*E|Y4_lP_?a(@|F-d;z_C2n5RS$0eS2r%$9M#@Ml8*~(L&1ytg z*W|knI%@- ze8w8spc>9kvCSo*c28zJtB=azWys=57(e2A{q+s*W2Vf80`A9H&+PRtH3|*K2yYZl zT6pIv3Lpy|0Osy1mtQK?9t1Z^Nbq zF7RJ2zlo)L-%644$x0b+KV?#$AfxI1bAIXoDx}oY2G_6(nap!c)Rb+Zf9oX4V3J%( zORu1$El$Y%<`Z)nM7g8|yPLHzKgLG?P{+$)>J`nKV*%I39oAsB6jbYae3h#SdL__TYne8x*To|P5D^B4W1$G74e z%vTC1s8VJF9i3nhs2l+V`uBtT6LZjCg7GjmwYPJAW9Q83ZVMa4!3E5G^j-G!d^3XP zD3TC2Khw!@lF8%C_8#9|V@b*7G{j|A)poOA&i`2P4Eb>9&N{k`*eS2SAA9=lGjrOi zPB-TQtzpxRGzBc`IyJ>5P7W)7uz@wr*?(tv=>Rg7t;mExc+W9Gny;8>ZJw>Dpy91Z zuIz&JW&fUy6RvYa!`)!kfUf! ze@I4J%|kDvMA=Vm9cBpTdlw9 zB{DGI?o(KO&!E6#Typ#DkaJ;l11Yl3)QMii$G`B?turVNIy)A!sbwcau7TB*&_KT8^@|Ga_S>hps6#(5+jt0 zZz8^Hxh5h2y`13JmM*T7;@bYEXBzS|TfNz}js7uE<7d^P+u$LpHlHB%qQa2z>ReA( zJLOBsw@1`m<>j-!GPKKF9QD$ZpAR5U;0kAx356%XW*jT99uqKWrcH#xULFQsa68bU zgQc}MJbC+iatG1Fm3@LEbElPe>fFum5V_P@tNSogl(W+Zb+LCp)vv_(J?@p?NJC>e-5YWSzRfO-cbK*RH^Ap9;Dr*qJ!YDDj^j%Fr zn|Q8MEUdcEy#*VYZWp(CFV*iYP@ShfFzrn10o4JJm@MgiEjRMr_1+llF%ac~+t zAu@P~k6$E8Lj8OE`S9@DWXqI#h45H4!Ug3xY20r4pB}YyYZi`mSiAd_Sa{u;mGS#E zTbRgOW7pfgk`gZYn!c6D3T@W9ayXTLCo~|Z)ngphWeOXRe@zs?qcB64&Au2*8ZEos z-s1IgI*BdMr46t^4YI8zDe5PKaDFS@>bw!9zXT|~VozG(w%OT@6RS`CKk;=!8E6FJ zb1qV}$2EqX9usq!@iBy383`dtr58%)GwtF9f6hdBag{wkWTU}P>ZhJ5SM`RIR1q&% z1rG%=Qb&k7f{1TXve>j0#My>7(paL-$0b5K&!{)4dI|7Kgmn|tQXTpYaI}lSAz%ym zs>Yz>O8&F}D@kd;BIie5PMEJ5_ghMnPxfhMknO8=Vlwy_*>fXZ?eI8_=lUu{*mGyQ z5}F$w99DzBdfVBh6BsO*zmFGdec(x4`~kMx!&mz9ivgjyQbA126&200?}w$!JZKMR z*6MUfV-ScBCtSj7FZw7`?f2S$z>`?^fazY{+BX`PE|7BQncs3qCrg|Hf8|f-$<*Ft zr@+l(egrITy-mcP6OGOEK#yfXJdcU{o?K~1$q!M`4BbZ&NOGiCEmy_&n87N?AAOV_ zoq5z|lYC!l37!^XQYkxz(a7Llx2MCq9vh`ve5wULTc3%z=+Yww>VNVOMRc_K{@ekL zYEh+82dOalJSSUw{%QU>FX|K zknf$8d@V&VI(m&l0VXI;o)7)-Ij-nS(+FzS6;g)570U^=!3Uw-ym&mjms%SqB`!kP>QM36aJ6h+Ez4r1OD8^KD3Y1q)`ylueq>wQf&)p`mJIyAUI zh-*BSk#=Ofmt)=et4?241$uKxUZX!R^ur*7fs|ZWZCf4wH%@1pnLEp#50#SOqNX3# zfG<%=@$BGUFSeLbJ9w_aGC%!itwwN?1ykRr{czxZ#SQ5ofycGtd5K|l(@_MYXoZ|7 z4_y0Xg!6}TXb?LsV(GLQ)dbOoW*k(j*}mzzlX_u+G1r$bM-lygrkj7x#;2&fnOM5N zj&y3tNv zx!?hpjf;`b_7+1ief#O4Nd@olV{jQy!gIDt+R3?$Z)TeF3huMckD?qW4ogUZq0&f7 ziWTahuHRGL!|MaO(ted%R!>jMot95aovj}F%ndF1!BRaZ^&b>OXKROIur?$^lA`C! zFRKMI4H~_S$GPrtA+CLL2u=cc2w(SKa2)&Uy7w-pE)ztwy!jv{5gwxIi`$BSW#f+Y z(Ozl)>V&vutpC6sL|eJD0r;Gj*NrY+p}A~McdMN2j!fDu*cf{VG=`n}neU*`mw7pe z?z~X76-XS5X2T9{z7@?$KyMZXZxD@(N*2eA)lU6z%GX!%r155smB_w| zLamiicIjHmpQoc{*}1CAD5Kz{rd;%0WR{N92hF5$Qc#kAo>;taY(2CL30_g#Zw``N zWdq5`Y(7&qn$iu&1w_QuqfrFNI1-PP>0eguRNOC?gu&^psj+P}+K&97lM_pAge}qH zj#);NykeZQ){#Q5XV07$OOfn!x$EgWY$t0mD#OC$Q^fmq42X6?y9=i z^`BFsw!LDMpuG$xQOldZF5#}v5X3qcs!)1Q z#G&ahRof8|l>VH6zE3+cnEvka%I$X0P5<_8wO7}9YdTSBclv~|-<=35T8zCGJaoyw z-2CdGif#3~TEL0oNc#K?TPoJt(vpwREWhFO1ubW2xW27>Rje{bGtM=G>0PGtGd=c6Gc)cM^q3&zYxZB`< zZBX2$9%r-}tcDu=8lU>85d%SzLU5AO{yl!Vt-oxIfZY(%D0Vq+X@7qJ7<@o^?`+vc zQLfIX$H~x6(Lrsi@5;0n@ePj^+4i@ zd2TXy#ycRPyp?;Umu_M}UcTx_*huCn)i^?y)zYz6GYeJm zC9YY1^RgnK%$`QGqmn=Dmcf}DJxQH{xkIjJIGz%aWyAj_EP3jns^-)tj)T%}U6^UONg)<}q#2=t zd8bGw6!!AEw;)TqoX()BLblyCcW45VdA4#jMf$i&WsXjJLs6aw2O|NIr|p zc&EHMDoFsJwXwAPlue-ayBRuVFZl(YvBhq&o|rO^)5#dLk&n6ypv_h@C})t3Sg~GcW z|9(PqDXWTz(8RA8c>m3fEn_}$j4Vm}R%CeV{vkI-K^+@c!PY_tfdJMq zA3Y?iPH&u@t?VqE9ln_zWVPBH$2Xt}#)j_8ho1 z>Cny9Oj-`7G$+ZYQRZcFq$|&jlck2Gv+G_N3k6w;sKxr%Yw%YBev@>OK^tz_A@c%F zjKIfl_f87tTmykTL6b4q9D>+SO+Dq@J9`|p%F$BZe}QJyNQ-U;@I3M)9#7Dx2Db~A zP4A3VewteUf&SjzQS%)MhV|OJCtVB6VfOz0eFQ4_eOqI5uf@CjS`j^{wd-o0p~kQM zJnGv%+068{bxTCAy1kd@A#xeiGk2q9eP_GXav}9L zTf(_%nAl)b8&w`3l8l%=Csl=YA%$K3TEB*+3F(r0k>X3KNi?NUP;F3J;aDL&b4YLy zUXMcC)KK7se>;oPQX52GoYDZ-<0oOcEyLJJ*;Pvcw+r*3oC$GkLOV5QGXH@CM+(4)uS$)#_@<zqM4Hit{*@{9 z?ZE}nq@Jhz(Lkalt3U7(MFYI4pQ_7g?`9EwY?8&lo#JpYfeD|a5<(5g$be;g*aczB$y{3X2ID+VtKB(pXz?M}fcdR*b`=iH24 zX8s{FRk-=>H;-Mcr=;sj#lDE{1m}hZjyrG{cWp`8SnxOzs^8M9-*1l@y-L zH1dtOND)_!h3yTa$Zacpz6el@q0vd;wn`{J8U-tG~o&TDlOtVxWqC{aU{5NZrg$jdQpj z62CKLO{V_qDdFYmqzxRy$r?FotS3^YZvJQpmXG0x&|?aylQ7qMqPnUD{Bk41s5Gh8 zMCJ8MSN|>^>q}K{ii)SS!;LVNSfG$kTW>x#*upK^7C=R_9rLM0_PN=VWk{%T_s1~@ ziz`XB(W8hg*YJcsm;4vO7^?IWAI47Lu|g@#gM+Ug**??jxiUM3G9FB;E5BA#HEi7lyo3E34hnPI+HcI^GgSRDu}G>VJ{RGMf$+_HUC`$_uYJ@bM}(LX)EP#* zX@f&*U#i@_fEU8rrpOvPNgL{Xf_haq@z@@!n6&%`L0%38E%M_`G;rR=n3QR(($M#* z{+ezFpXH~bUPO6t!uc1Nn%>70emM=*y#;>XMiJ{&1L!iyAP^1xp9;yJTK#||e#hOz zEp!Kh8Ifzkevl9RTRTssU;ymepg2`q1_zF`#Bu(cQIPkfAw=rCkG77S@-ifaeo9|VVJi8wE>OjeyYvmS!4noLkMP4tE zP&^a37Cc;N29q3l9wVMS5)9A?b+Me zvP1&jqd2EnslB%$9j^w5xiC!u-seM8Vjn~ZgL~h4h`0mYx7g2iV$-a85M148sx--i zmm0Kqmlwho?jXOn`a;+HDaS*Rni=u;PJ3GCYk;2TO_z%qy9MnIN zfITR|p|D6WIo5xf{RWz06#qW}egMv3cTmQEW9Imj6v7IQDT4mDgNJz#OEByfO!42y zODHKM4IJY@4GV*!|Lf(q(>d4^s=u5PKwuG1=^hBozbzIRHZ>_k83JRXCWZd)^ZZ?y ze`o>yC6NfQ6ly}?_oTmzbiZr#|B+Cnzbp1Oe|GK9YJXFy2S)Xm5$r7u4ze-%Z{+_0zJ0;W diff --git a/unogenerator/unogenerator.py b/unogenerator/unogenerator.py index 73cd589..0e538f9 100644 --- a/unogenerator/unogenerator.py +++ b/unogenerator/unogenerator.py @@ -6,6 +6,7 @@ from uno import getComponentContext, createUnoStruct, systemPathToFileUrl, Any, ByteSequence from com.sun.star.beans import PropertyValue from com.sun.star.text import ControlCharacter +from com.sun.star.table.CellVertJustify import CENTER, STANDARD from com.sun.star.awt import Size from com.sun.star.sheet.ConditionEntryType import COLORSCALE from com.sun.star.style.ParagraphAdjust import RIGHT, LEFT @@ -619,6 +620,12 @@ class ODS(ODF): def __init__(self, template=None, server=None): ODF.__init__(self, template, server) self._remove_default_sheet=True + self._wrapped_rows = set() # Track rows that have word wrap enabled to avoid overriding them with False + self.default_row_height = None # Default row height in 1/100th mm. If None, it won't be explicitly set. + self.default_cell_style = "Default" # Default cell style name. + + + def getRemoveDefaultSheet(self): return self._remove_default_sheet @@ -647,6 +654,7 @@ def createSheet(self, name, index=None): index=len(sheets) sheets.insertNewByName(name, index) self.setActiveSheet(index) + self.sheet.getRows().OptimalHeight = False def removeSheet(self, index): current=self.sheet.Name @@ -659,7 +667,66 @@ def setActiveSheet(self, index): self.sheet=self.document.getSheets().getByIndex(index) logger.debug(f"Sheet '{self.sheet.Name}' ({self.sheet_index}) is now active") return self.sheet - + + def _set_row_optimal_height(self, row_index, word_wrap): + """ + Internal method to set row optimal height honoring word wrap memory. + """ + if word_wrap: + self._wrapped_rows.add((self.sheet.Name, row_index)) + self.sheet.getRows().getByIndex(row_index).OptimalHeight = True + else: + if (self.sheet.Name, row_index) not in self._wrapped_rows: + row = self.sheet.getRows().getByIndex(row_index) + row.OptimalHeight = word_wrap # False + if self.default_row_height: + row.Height = self.default_row_height + + def _set_rows_optimal_height(self, row_range_uno, word_wrap): + """ + Internal method to set optimal height for a range of rows honoring memory. + """ + start_row = row_range_uno.RangeAddress.StartRow + end_row = row_range_uno.RangeAddress.EndRow + + if word_wrap: + # Optimization: only iterate if we need to add new rows to the set + for r in range(start_row, end_row + 1): + self._wrapped_rows.add((self.sheet.Name, r)) + row_range_uno.getRows().OptimalHeight = True + else: + # Optimization: If the set is empty, we can skip the loop and do ONE bulk call. + # Even if not empty, we could check for intersection, but an empty set is the common case. + if not self._wrapped_rows: + rows = row_range_uno.getRows() + rows.OptimalHeight = word_wrap # False + if self.default_row_height: + rows.Height = self.default_row_height + return + + # First, set everything to OptimalHeight=False and Height=default_row_height for rows that should NOT be wrapped. + # We do this in blocks to be efficient. + current_start = -1 + for r in range(start_row, end_row + 1): + if (self.sheet.Name, r) not in self._wrapped_rows: + if current_start == -1: + current_start = r + else: + if current_start != -1: + # Set block from current_start to r-1 + rows = self.sheet.getCellRangeByPosition(0, current_start, 0, r - 1).getRows() + rows.OptimalHeight = word_wrap # False + if self.default_row_height: + rows.Height = self.default_row_height + current_start = -1 + + if current_start != -1: + # Set last block + rows = self.sheet.getCellRangeByPosition(0, current_start, 0, end_row).getRows() + rows.OptimalHeight = word_wrap # False + if self.default_row_height: + rows.Height = self.default_row_height + def setColumnsWidth(self, value: list[dict] | list[list] | list, columns_width_mode=types.ColumnsWidthMode.MANUAL, char_to_cm=0.22, padding_cm=0.5, min_width_cm=2.0, max_width_cm=15.0): """ Sets columns width @@ -724,12 +791,13 @@ def addRow(self, coord_start, list_o, formulas=True): return R.from_uno_range(range_uno) - def addRowWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,styles=None, formulas=True): + def addRowWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,styles=None, formulas=True, word_wrap=False): """ Parameters: - formulas Boolean. If true formulas will be written as formula. If false as string - colors Color: Use color for all array, List of colors one for each cell - styles If None uses guest style. Else an array of styles + - word_wrap Boolean. If True, enables word wrap and optimal row height. If False, keeps fixed row height. Return: Range """ coord_start=Coord.assertCoord(coord_start) @@ -737,11 +805,14 @@ def addRowWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,styles=N if range_ is None: return None range_uno=range_.uno_range(self.sheet) + range_uno.IsTextWrapped = word_wrap + range_uno.VertJustify = CENTER if word_wrap else STANDARD + self._set_rows_optimal_height(range_uno, word_wrap) #Fast color: if styles is None: styles=[] for o in list_o: - styles.append(guess_object_style(o)) + styles.append(guess_object_style(o, self.default_cell_style)) if isinstance(colors, list): @@ -788,12 +859,13 @@ def addColumn(self, coord_start, list_o, formulas=True): self.__setDataArray(range_, r) return R.from_coords_indexes(*range_indexes) - def addColumnWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,styles=None, formulas=True): + def addColumnWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,styles=None, formulas=True, word_wrap=False): """ Parameters: - formulas Boolean. If true formulas will be written as formula. If false as string - colors Color: Use color for all array, List of colors one for each cell - styles If None uses guest style. Else an array of styles + - word_wrap Boolean. If True, enables word wrap and optimal row height. If False, keeps fixed row height. Return: Range """ coord_start=Coord.assertCoord(coord_start) @@ -803,11 +875,15 @@ def addColumnWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,style return None range_uno=range_.uno_range(self.sheet) + range_uno.IsTextWrapped = word_wrap + range_uno.VertJustify = CENTER if word_wrap else STANDARD + self._set_rows_optimal_height(range_uno, word_wrap) + # Guess styles if none if styles is None: styles=[] for o in list_o: - styles.append(guess_object_style(o)) + styles.append(guess_object_style(o, self.default_cell_style)) #Fast color: if isinstance(colors, list): for i in range(len(list_o)): @@ -896,7 +972,7 @@ def addListOfRows(self, coord_start, list_rows, formulas=True): ## @param colors. List of column colors, one color, or None to use white ## @param styles. List of styles (columns) or None to guess them from first row ## @return range of the list_of_rows - def addListOfRowsWithStyle(self, coord_start, list_rows, colors=ColorsNamed.White, styles=None, formulas=True): + def addListOfRowsWithStyle(self, coord_start, list_rows, colors=ColorsNamed.White, styles=None, formulas=True, word_wrap=False): """ Function used to add a big amount of cells to paste quickly @@ -904,6 +980,7 @@ def addListOfRowsWithStyle(self, coord_start, list_rows, colors=ColorsNamed.Whit - formulas Boolean. If true formulas will be written as formula. If false as string - colors. List of column colors, one color, or None to use white - styles. List of styles (columns) or None to guess them from first row + - word_wrap Boolean. If True, enables word wrap and optimal row height. If False, keeps fixed row height. Return: Range """ @@ -915,6 +992,7 @@ def addListOfRowsWithStyle(self, coord_start, list_rows, colors=ColorsNamed.Whit columns=range_.numColumns() rows=range_.numRows() + range_uno=range_.uno_range(self.sheet) # Parse colors. if colors.__class__.__name__=="list": @@ -928,7 +1006,7 @@ def addListOfRowsWithStyle(self, coord_start, list_rows, colors=ColorsNamed.Whit if styles is None and rows>0: styles=[] for o in list_rows[0]: - styles.append(guess_object_style(o)) + styles.append(guess_object_style(o, self.default_cell_style)) elif styles.__class__.__name__=="list": styles=styles else: @@ -939,13 +1017,22 @@ def addListOfRowsWithStyle(self, coord_start, list_rows, colors=ColorsNamed.Whit raise exceptions.UnogeneratorException(_("Colors must have the same number of items as data columns")) if len(styles)!=columns: raise exceptions.UnogeneratorException(_("Styles must have the same number of items as data columns")) - + #Create styles by columns cellranges if rows>0: - for c, o in enumerate(list_rows[0]): - columnrange=self.sheet.getCellRangeByPosition(coord_start.letterIndex()+c, coord_start.numberIndex(), coord_start.letterIndex()+c, coord_start.numberIndex()+rows-1) - columnrange.setPropertyValue("CellStyle", styles[c]) - columnrange.setPropertyValue("CellBackColor", colors[c]) + # Optimization: If all styles are default and all colors are White, we can skip this loop + if all(s == self.default_cell_style for s in styles) and all(c == ColorsNamed.White for c in colors): + pass + else: + for c, o in enumerate(list_rows[0]): + columnrange=self.sheet.getCellRangeByPosition(coord_start.letterIndex()+c, coord_start.numberIndex(), coord_start.letterIndex()+c, coord_start.numberIndex()+rows-1) + columnrange.setPropertyValue("CellStyle", styles[c]) + columnrange.setPropertyValue("CellBackColor", colors[c]) + + range_uno.IsTextWrapped = word_wrap + range_uno.VertJustify = CENTER if word_wrap else STANDARD + self._set_rows_optimal_height(range_uno, word_wrap) + return range_ @@ -962,12 +1049,13 @@ def addListOfColumns(self, coord_start, list_columns, formulas=True): ## @param style If None tries to guess it - def addListOfColumnsWithStyle(self, coord_start, list_columns, colors=ColorsNamed.White, styles=None, formulas=True): + def addListOfColumnsWithStyle(self, coord_start, list_columns, colors=ColorsNamed.White, styles=None, formulas=True, word_wrap=False): """ Colors and styles are the colors of the first column. Code is different Parameters: - formulas Boolean. If true formulas will be written as formula. If false as string + - word_wrap Boolean. If True, enables word wrap and optimal row height. If False, keeps fixed row height. Return: Range """ @@ -979,6 +1067,7 @@ def addListOfColumnsWithStyle(self, coord_start, list_columns, colors=ColorsName columns=range_.numColumns() rows=range_.numRows() + range_uno=range_.uno_range(self.sheet) # Parse colors. if colors.__class__.__name__=="list": @@ -992,7 +1081,7 @@ def addListOfColumnsWithStyle(self, coord_start, list_columns, colors=ColorsName if styles is None and rows>0: styles=[] for o in list_columns[0]: - styles.append(guess_object_style(o)) + styles.append(guess_object_style(o, self.default_cell_style)) elif styles.__class__.__name__=="list": styles=styles else: @@ -1003,28 +1092,41 @@ def addListOfColumnsWithStyle(self, coord_start, list_columns, colors=ColorsName raise exceptions.UnogeneratorException(_("Colors must have the same number of items as data columns")) if len(styles)!=rows: raise exceptions.UnogeneratorException(_("Styles must have the same number of items as data columns")) - + #Create styles by columns cellranges if rows>0: - for c, o in enumerate(list_columns[0]): - columnrange=self.sheet.getCellRangeByPosition(coord_start.letterIndex(), coord_start.numberIndex()+c, coord_start.letterIndex()+columns-1, coord_start.numberIndex()+c) - columnrange.setPropertyValue("CellStyle", styles[c]) - columnrange.setPropertyValue("CellBackColor", colors[c]) + # Optimization: If all styles are default and all colors are White, we can skip this loop + if all(s == self.default_cell_style for s in styles) and all(c == ColorsNamed.White for c in colors): + pass + else: + for c, o in enumerate(list_columns[0]): + columnrange=self.sheet.getCellRangeByPosition(coord_start.letterIndex(), coord_start.numberIndex()+c, coord_start.letterIndex()+columns-1, coord_start.numberIndex()+c) + columnrange.setPropertyValue("CellStyle", styles[c]) + columnrange.setPropertyValue("CellBackColor", colors[c]) + + range_uno.IsTextWrapped = word_wrap + range_uno.VertJustify = CENTER if word_wrap else STANDARD + self._set_rows_optimal_height(range_uno, word_wrap) + return range_ ## @param style If None tries to guess it + ## @param word_wrap Boolean. If True, enables word wrap and optimal row height. If False, keeps fixed row height. ## @param rewritewrite If color is ColorsNamed.White, rewrites the color to White instead of ignoring it. Ignore it gains 0.200 ms ## THIS IS THE WAY TO CREATE FORMULAS - def addCellWithStyle(self, coord, o, color=ColorsNamed.White, style=None): + def addCellWithStyle(self, coord, o, color=ColorsNamed.White, style=None, word_wrap=True): coord=Coord.assertCoord(coord) if style is None: - style=guess_object_style(o) + style=guess_object_style(o, self.default_cell_style) cell=self.sheet.getCellByPosition(coord.letterIndex(), coord.numberIndex()) self.__object_to_cell(cell, o) - cell.setPropertyValue("CellStyle", style) - cell.setPropertyValue("CellBackColor", color) + cell.CellStyle = style + cell.CellBackColor = color + cell.IsTextWrapped = word_wrap + cell.VertJustify = CENTER if word_wrap else STANDARD + self._set_row_optimal_height(coord.numberIndex(), word_wrap) def setCellName(self, reference, name): """ @@ -1127,12 +1229,17 @@ def addCellMerged(self, range, o): self.__object_to_cell(cell, o) return cell - def addCellMergedWithStyle(self, range, o, color=ColorsNamed.White, style=None): - cell=self.addCellMerged(range, o) + def addCellMergedWithStyle(self, range_o, o, color=ColorsNamed.White, style=None, word_wrap=False): + cell=self.addCellMerged(range_o, o) + range_obj=R.assertRange(range_o) if style is None: style=guess_object_style(o) - cell.setPropertyValue("CellStyle", style) - cell.setPropertyValue("CellBackColor", color) + cell.CellStyle = style + cell.CellBackColor = color + range_uno = range_obj.uno_range(self.sheet) + range_uno.IsTextWrapped = word_wrap + range_uno.VertJustify = CENTER if word_wrap else STANDARD + self._set_rows_optimal_height(range_uno, word_wrap) def freezeAndSelect(self, freeze, selected=None, topleft=None): freeze=Coord.assertCoord(freeze) @@ -1473,6 +1580,10 @@ def toDictionaryOfDetailedValues(self): class ODS_Standard(ODS): def __init__(self, server=None): ODS.__init__(self, files('unogenerator') / 'templates/standard.ods', server) + self.default_row_height = 452 + self.default_cell_style = "Normal" + + class ODT_Standard(ODT): def __init__(self, server=None): From 2d626dccf00fb37ab5bad71d87b9cdb7ff093bae Mon Sep 17 00:00:00 2001 From: turulomio Date: Sat, 23 May 2026 20:36:24 +0200 Subject: [PATCH 22/34] Mejorada hoja de stylos --- unogenerator/helpers.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index 76e3363..4dc5b8a 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -1,6 +1,7 @@ from unogenerator.commons import ColorsNamed, Coord, Range, guess_object_style, generate_formula_total_string from unogenerator import ODS, types from pydicts import lod +from collections import OrderedDict from gettext import translation from logging import debug import logging @@ -284,17 +285,28 @@ def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_ def sheet_stylenames(doc): """ - Creates a new sheet called "Internal style names" listing all ODS styles grouped by families. + Creates a new sheet called "Internal style names" listing all ODS styles in four columns: + CellStyles, PageStyles, GraphicStyles, and TableStyles. Args: doc (ODS): The ODS document object. """ - lod_=[] - for family, style_names in doc.dict_stylenames.items(): - lod_.append({ - "Family":family, - "Styles":style_names - }) + cell_styles = doc.dict_stylenames.get("CellStyles", []) + page_styles = doc.dict_stylenames.get("PageStyles", []) + graphic_styles = doc.dict_stylenames.get("GraphicStyles", []) + table_styles = doc.dict_stylenames.get("TableStyles", []) + + max_len = max(len(cell_styles), len(page_styles), len(graphic_styles), len(table_styles)) + + lod_ = [] + for i in range(max_len): + lod_.append(OrderedDict({ + "CellStyles": cell_styles[i] if i < len(cell_styles) else "", + "PageStyles": page_styles[i] if i < len(page_styles) else "", + "GraphicStyles": graphic_styles[i] if i < len(graphic_styles) else "", + "TableStyles": table_styles[i] if i < len(table_styles) else "" + })) + sheet_from_lod(doc, "Internal style names", lod_, freezeandselect="A2", columns_width_mode=types.ColumnsWidthMode.FROM_LOD) def sheet_from_lol(doc, sheetname, lor, headers, column_of_totals=False, row_of_totals=False, freezeandselect=None, titulo=None, word_wrap=False, **kwargs_columnswidth): From 94bbbdc683eca195914fa2db258a752fddb3b980 Mon Sep 17 00:00:00 2001 From: turulomio Date: Sat, 23 May 2026 20:58:06 +0200 Subject: [PATCH 23/34] Refactorizado y mejorado codigo --- unogenerator/helpers.py | 42 +++---- unogenerator/unogenerator.py | 213 +++++++++++++++++++++++------------ 2 files changed, 163 insertions(+), 92 deletions(-) diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index 4dc5b8a..c3766ad 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -18,7 +18,7 @@ def row_totals(doc, coord, list_of_totals, color=ColorsNamed.GrayLight, styles=None, row_from="2", row_to=None): """ - Generates a row of totals starting from the given coordinate. + Generates a row of totals starting from the given coordinate using bulk insertion. Args: doc (ODS): The ODS document object. @@ -30,27 +30,27 @@ def row_totals(doc, coord, list_of_totals, color=ColorsNamed.GrayLight, styles=N row_to (str, optional): The row number where the formula range ends. If None, defaults to the row above `coord`. """ coord=Coord.assertCoord(coord) + formulas = [] for letter, total in enumerate(list_of_totals): coord_total=coord.addColumnCopy(letter) - coord_total_from=Coord(coord_total.letter+row_from) + coord_total_from=Coord(coord_total.letter+str(row_from)) if row_to is None: coord_total_to=coord_total.addRowCopy(-1)# row above else: - coord_total_to=Coord(coord_total.letter+row_to) + coord_total_to=Coord(coord_total.letter+str(row_to)) + formulas.append(generate_formula_total_string(total, coord_total_from, coord_total_to)) - if styles is None: - style=guess_object_style(doc.getValue(coord_total_from)) - elif styles.__class__.__name__ != "list": - style=styles - else: - style=styles[letter] - - doc.addCellWithStyle(coord_total, generate_formula_total_string(total, coord_total_from, coord_total_to), color, style) + if styles is None: + # Guess style from the first data cell + first_coord_from = Coord(coord.letter + str(row_from)) + styles = guess_object_style(doc.getValue(first_coord_from), doc.default_cell_style) + + doc.addRowWithStyle(coord, formulas, colors=color, styles=styles) def column_totals(doc, coord, list_of_totals, color=ColorsNamed.GrayLight, styles=None, column_from="B", column_to=None): """ - Generates a column of totals starting from the given coordinate. + Generates a column of totals starting from the given coordinate using bulk insertion. Args: doc (ODS): The ODS document object. @@ -62,22 +62,22 @@ def column_totals(doc, coord, list_of_totals, color=ColorsNamed.GrayLight, style column_to (str, optional): The column letter where the formula range ends. If None, defaults to one column before `coord`. """ coord=Coord.assertCoord(coord) + formulas = [] for number, total in enumerate(list_of_totals): coord_total=coord.addRowCopy(number) - coord_total_from=Coord(column_from + coord_total.number) + coord_total_from=Coord(str(column_from) + coord_total.number) if column_to is None: coord_total_to=coord_total.addColumnCopy(-1)# row above else: - coord_total_to=Coord(column_to + coord_total.number) + coord_total_to=Coord(str(column_to) + coord_total.number) + formulas.append(generate_formula_total_string(total, coord_total_from, coord_total_to)) - if styles is None: - style=guess_object_style(doc.getValue(coord_total_from)) - elif styles.__class__.__name__ != "list": - style=styles - else: - style=styles[number] + if styles is None: + # Guess style from the first data cell + first_coord_from = Coord(str(column_from) + coord.number) + styles = guess_object_style(doc.getValue(first_coord_from), doc.default_cell_style) - doc.addCellWithStyle(coord_total, generate_formula_total_string(total, coord_total_from, coord_total_to), color, style) + doc.addColumnWithStyle(coord, formulas, colors=color, styles=styles) def row_title_values_total( doc, coord, title, values, style_title=None, color_title=ColorsNamed.Orange, diff --git a/unogenerator/unogenerator.py b/unogenerator/unogenerator.py index 0e538f9..76f967f 100644 --- a/unogenerator/unogenerator.py +++ b/unogenerator/unogenerator.py @@ -801,34 +801,48 @@ def addRowWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,styles=N Return: Range """ coord_start=Coord.assertCoord(coord_start) - range_=self.addRow(coord_start, list_o, formulas) - if range_ is None: - return None - range_uno=range_.uno_range(self.sheet) + # Determine range and get UNO object + range_indexes=[coord_start.letterIndex(), coord_start.numberIndex(), coord_start.letterIndex()+len(list_o)-1, coord_start.numberIndex()] + range_uno=self.sheet.getCellRangeByPosition(*range_indexes) + + # Apply word wrap and alignment range_uno.IsTextWrapped = word_wrap range_uno.VertJustify = CENTER if word_wrap else STANDARD self._set_rows_optimal_height(range_uno, word_wrap) - #Fast color: + + # Guess styles if none if styles is None: styles=[] for o in list_o: styles.append(guess_object_style(o, self.default_cell_style)) - - - if isinstance(colors, list): + + # Fast style: + if isinstance(styles, list): for i in range(len(list_o)): cell=self.sheet.getCellByPosition(coord_start.letterIndex()+i, coord_start.numberIndex()) - cell.setPropertyValue("CellBackColor", colors[i]) + cell.CellStyle = styles[i] else: - range_uno.setPropertyValue("CellBackColor", colors) - #Fast style: - if isinstance(styles, list): + range_uno.CellStyle = styles + + # Fast color: + if isinstance(colors, list): for i in range(len(list_o)): cell=self.sheet.getCellByPosition(coord_start.letterIndex()+i, coord_start.numberIndex()) - cell.setPropertyValue("CellStyle", styles[i]) + cell.CellBackColor = colors[i] else: - range_uno.setPropertyValue("CellStyle", styles) - return range_ + range_uno.CellBackColor = colors + + # Finally add content + r=[] + for o in list_o: + r.append(self.__object_to_dataarray_element(o)) + + if formulas is True: + self.__setFormulaArray(range_uno, [r, ]) + else: + self.__setDataArray(range_uno, [r, ]) + + return R.from_uno_range(range_uno) @@ -859,7 +873,7 @@ def addColumn(self, coord_start, list_o, formulas=True): self.__setDataArray(range_, r) return R.from_coords_indexes(*range_indexes) - def addColumnWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,styles=None, formulas=True, word_wrap=False): + def addColumnWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,styles=None, formulas=True, word_wrap=True): """ Parameters: - formulas Boolean. If true formulas will be written as formula. If false as string @@ -869,12 +883,15 @@ def addColumnWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,style Return: Range """ coord_start=Coord.assertCoord(coord_start) - range_=self.addColumn(coord_start, list_o, formulas) - if range_ is None: + if len(list_o)==0: return None - - range_uno=range_.uno_range(self.sheet) + + # Determine range and get UNO object + range_indexes=[coord_start.letterIndex(), coord_start.numberIndex(), coord_start.letterIndex(), coord_start.numberIndex()+len(list_o)-1] + range_uno=self.sheet.getCellRangeByPosition(*range_indexes) + + # Apply word wrap and alignment range_uno.IsTextWrapped = word_wrap range_uno.VertJustify = CENTER if word_wrap else STANDARD self._set_rows_optimal_height(range_uno, word_wrap) @@ -884,21 +901,34 @@ def addColumnWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,style styles=[] for o in list_o: styles.append(guess_object_style(o, self.default_cell_style)) - #Fast color: - if isinstance(colors, list): + + # Fast style: + if isinstance(styles, list): for i in range(len(list_o)): cell=self.sheet.getCellByPosition(coord_start.letterIndex(), coord_start.numberIndex()+i) - cell.setPropertyValue("CellBackColor", colors[i]) + cell.CellStyle = styles[i] else: - range_uno.setPropertyValue("CellBackColor", colors) - #Fast style: - if isinstance(styles, list): + range_uno.CellStyle = styles + + #Fast color: + if isinstance(colors, list): for i in range(len(list_o)): cell=self.sheet.getCellByPosition(coord_start.letterIndex(), coord_start.numberIndex()+i) - cell.setPropertyValue("CellStyle", styles[i]) + cell.CellBackColor = colors[i] else: - range_uno.setPropertyValue("CellStyle", styles) - return range_ + range_uno.CellBackColor = colors + + # Finally add content + r=[] + for o in list_o: + r.append([self.__object_to_dataarray_element(o), ]) + + if formulas is True: + self.__setFormulaArray(range_uno, r) + else: + self.__setDataArray(range_uno, r) + + return R.from_coords_indexes(*range_indexes) @@ -968,11 +998,7 @@ def addListOfRows(self, coord_start, list_rows, formulas=True): self.__setDataArray(range, r) return R.from_coords_indexes(*range_indexes) - ## Function used to add a big amount of cells to paste quickly - ## @param colors. List of column colors, one color, or None to use white - ## @param styles. List of styles (columns) or None to guess them from first row - ## @return range of the list_of_rows - def addListOfRowsWithStyle(self, coord_start, list_rows, colors=ColorsNamed.White, styles=None, formulas=True, word_wrap=False): + def addListOfRowsWithStyle(self, coord_start, list_rows, colors=ColorsNamed.White, styles=None, formulas=True, word_wrap=True): """ Function used to add a big amount of cells to paste quickly @@ -986,13 +1012,20 @@ def addListOfRowsWithStyle(self, coord_start, list_rows, colors=ColorsNamed.Whit """ coord_start=Coord.assertCoord(coord_start) - range_=self.addListOfRows(coord_start, list_rows, formulas) - if range_ is None: - return - - columns=range_.numColumns() - rows=range_.numRows() - range_uno=range_.uno_range(self.sheet) + rows=len(list_rows) + if rows==0: + columns=0 + else: + columns=len(list_rows[0]) + + + if rows==0 or columns==0: + logger.debug(_("addListOfRowsWithStyle has {0} rows and {1} columns. Nothing to write. Ignoring...").format(rows, columns)) + return + + # Determine range and get UNO object + range_indexes=[coord_start.letterIndex(), coord_start.numberIndex(), coord_start.letterIndex()+columns-1, coord_start.numberIndex()+rows-1] + range_uno=self.sheet.getCellRangeByPosition(*range_indexes) # Parse colors. if colors.__class__.__name__=="list": @@ -1026,14 +1059,27 @@ def addListOfRowsWithStyle(self, coord_start, list_rows, colors=ColorsNamed.Whit else: for c, o in enumerate(list_rows[0]): columnrange=self.sheet.getCellRangeByPosition(coord_start.letterIndex()+c, coord_start.numberIndex(), coord_start.letterIndex()+c, coord_start.numberIndex()+rows-1) - columnrange.setPropertyValue("CellStyle", styles[c]) - columnrange.setPropertyValue("CellBackColor", colors[c]) + columnrange.CellStyle = styles[c] + columnrange.CellBackColor = colors[c] range_uno.IsTextWrapped = word_wrap range_uno.VertJustify = CENTER if word_wrap else STANDARD self._set_rows_optimal_height(range_uno, word_wrap) - - return range_ + + # Finally add content + r=[] + for row in list_rows: + r_row=[] + for o in row: + r_row.append(self.__object_to_dataarray_element(o)) + r.append(r_row) + + if formulas is True: + self.__setFormulaArray(range_uno, r) + else: + self.__setDataArray(range_uno, r) + + return R.from_coords_indexes(*range_indexes) def addListOfColumns(self, coord_start, list_columns, formulas=True): @@ -1048,10 +1094,9 @@ def addListOfColumns(self, coord_start, list_columns, formulas=True): return self.addListOfRows(coord_start, list_rows, formulas) - ## @param style If None tries to guess it - def addListOfColumnsWithStyle(self, coord_start, list_columns, colors=ColorsNamed.White, styles=None, formulas=True, word_wrap=False): + def addListOfColumnsWithStyle(self, coord_start, list_columns, colors=ColorsNamed.White, styles=None, formulas=True, word_wrap=True): """ - Colors and styles are the colors of the first column. Code is different + Colors and styles are the colors of every column. Parameters: - formulas Boolean. If true formulas will be written as formula. If false as string @@ -1061,21 +1106,27 @@ def addListOfColumnsWithStyle(self, coord_start, list_columns, colors=ColorsName """ coord_start=Coord.assertCoord(coord_start) - range_=self.addListOfColumns(coord_start, list_columns, formulas) - if range_ is None: - return - - columns=range_.numColumns() - rows=range_.numRows() - range_uno=range_.uno_range(self.sheet) + columns=len(list_columns) + if columns==0: + rows=0 + else: + rows=len(list_columns[0]) + + if rows==0 or columns==0: + logger.debug(_("addListOfColumnsWithStyle has {0} rows and {1} columns. Nothing to write. Ignoring...").format(rows, columns)) + return + + # Determine range and get UNO object + range_indexes=[coord_start.letterIndex(), coord_start.numberIndex(), coord_start.letterIndex()+columns-1, coord_start.numberIndex()+rows-1] + range_uno=self.sheet.getCellRangeByPosition(*range_indexes) # Parse colors. if colors.__class__.__name__=="list": colors=colors elif colors is None: - colors=[ColorsNamed.White]*rows + colors=[ColorsNamed.White]*columns else: #one ColorsNamed - colors=[colors]*rows + colors=[colors]*columns # Parse styles if styles is None and rows>0: @@ -1085,12 +1136,12 @@ def addListOfColumnsWithStyle(self, coord_start, list_columns, colors=ColorsName elif styles.__class__.__name__=="list": styles=styles else: - styles=[styles]*rows + styles=[styles]*columns - if len(colors)!=rows: + if len(colors)!=columns: raise exceptions.UnogeneratorException(_("Colors must have the same number of items as data columns")) - if len(styles)!=rows: + if len(styles)!=columns: raise exceptions.UnogeneratorException(_("Styles must have the same number of items as data columns")) #Create styles by columns cellranges @@ -1100,15 +1151,29 @@ def addListOfColumnsWithStyle(self, coord_start, list_columns, colors=ColorsName pass else: for c, o in enumerate(list_columns[0]): - columnrange=self.sheet.getCellRangeByPosition(coord_start.letterIndex(), coord_start.numberIndex()+c, coord_start.letterIndex()+columns-1, coord_start.numberIndex()+c) - columnrange.setPropertyValue("CellStyle", styles[c]) - columnrange.setPropertyValue("CellBackColor", colors[c]) + columnrange=self.sheet.getCellRangeByPosition(coord_start.letterIndex()+c, coord_start.numberIndex(), coord_start.letterIndex()+c, coord_start.numberIndex()+rows-1) + columnrange.CellStyle = styles[c] + columnrange.CellBackColor = colors[c] range_uno.IsTextWrapped = word_wrap range_uno.VertJustify = CENTER if word_wrap else STANDARD self._set_rows_optimal_height(range_uno, word_wrap) - - return range_ + + # Finally add content + list_rows=lol.lol_transposed(list_columns) + r=[] + for row in list_rows: + r_row=[] + for o in row: + r_row.append(self.__object_to_dataarray_element(o)) + r.append(r_row) + + if formulas is True: + self.__setFormulaArray(range_uno, r) + else: + self.__setDataArray(range_uno, r) + + return R.from_coords_indexes(*range_indexes) ## @param style If None tries to guess it ## @param word_wrap Boolean. If True, enables word wrap and optimal row height. If False, keeps fixed row height. @@ -1229,18 +1294,24 @@ def addCellMerged(self, range, o): self.__object_to_cell(cell, o) return cell - def addCellMergedWithStyle(self, range_o, o, color=ColorsNamed.White, style=None, word_wrap=False): - cell=self.addCellMerged(range_o, o) + def addCellMergedWithStyle(self, range_o, o, color=ColorsNamed.White, style=None, word_wrap=True): range_obj=R.assertRange(range_o) - if style is None: - style=guess_object_style(o) - cell.CellStyle = style - cell.CellBackColor = color range_uno = range_obj.uno_range(self.sheet) + range_uno.merge(True) + + if style is None: + style=guess_object_style(o, self.default_cell_style) + + range_uno.CellStyle = style + range_uno.CellBackColor = color range_uno.IsTextWrapped = word_wrap range_uno.VertJustify = CENTER if word_wrap else STANDARD self._set_rows_optimal_height(range_uno, word_wrap) + cell = self.sheet.getCellByPosition(range_obj.c_start.letterIndex(), range_obj.c_start.numberIndex()) + self.__object_to_cell(cell, o) + return cell + def freezeAndSelect(self, freeze, selected=None, topleft=None): freeze=Coord.assertCoord(freeze) num_columns, num_rows=self.getSheetSize() From a816fd8519766561efec47b34c6c0885b99cf724 Mon Sep 17 00:00:00 2001 From: turulomio Date: Sat, 23 May 2026 21:08:33 +0200 Subject: [PATCH 24/34] I dont know --- unogenerator/unogenerator.py | 140 ++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 69 deletions(-) diff --git a/unogenerator/unogenerator.py b/unogenerator/unogenerator.py index 76f967f..4458951 100644 --- a/unogenerator/unogenerator.py +++ b/unogenerator/unogenerator.py @@ -791,7 +791,7 @@ def addRow(self, coord_start, list_o, formulas=True): return R.from_uno_range(range_uno) - def addRowWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,styles=None, formulas=True, word_wrap=False): + def addRowWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,styles=None, formulas=True, word_wrap=True): """ Parameters: - formulas Boolean. If true formulas will be written as formula. If false as string @@ -801,14 +801,23 @@ def addRowWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,styles=N Return: Range """ coord_start=Coord.assertCoord(coord_start) - # Determine range and get UNO object + + if len(list_o)==0: + logger.debug(_("addRow is empty. Nothing to write. Ignoring...")) + return None + range_indexes=[coord_start.letterIndex(), coord_start.numberIndex(), coord_start.letterIndex()+len(list_o)-1, coord_start.numberIndex()] range_uno=self.sheet.getCellRangeByPosition(*range_indexes) + + # Finally add content + r=[] + for o in list_o: + r.append(self.__object_to_dataarray_element(o)) - # Apply word wrap and alignment - range_uno.IsTextWrapped = word_wrap - range_uno.VertJustify = CENTER if word_wrap else STANDARD - self._set_rows_optimal_height(range_uno, word_wrap) + if formulas is True: + self.__setFormulaArray(range_uno, [r, ]) + else: + self.__setDataArray(range_uno, [r, ]) # Guess styles if none if styles is None: @@ -820,27 +829,22 @@ def addRowWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,styles=N if isinstance(styles, list): for i in range(len(list_o)): cell=self.sheet.getCellByPosition(coord_start.letterIndex()+i, coord_start.numberIndex()) - cell.CellStyle = styles[i] + cell.setPropertyValue("CellStyle", styles[i]) else: - range_uno.CellStyle = styles + range_uno.setPropertyValue("CellStyle", styles) # Fast color: if isinstance(colors, list): for i in range(len(list_o)): cell=self.sheet.getCellByPosition(coord_start.letterIndex()+i, coord_start.numberIndex()) - cell.CellBackColor = colors[i] + cell.setPropertyValue("CellBackColor", colors[i]) else: - range_uno.CellBackColor = colors + range_uno.setPropertyValue("CellBackColor", colors) - # Finally add content - r=[] - for o in list_o: - r.append(self.__object_to_dataarray_element(o)) - - if formulas is True: - self.__setFormulaArray(range_uno, [r, ]) - else: - self.__setDataArray(range_uno, [r, ]) + # Apply word wrap and alignment (Applied last for maximum stability) + range_uno.IsTextWrapped = word_wrap + range_uno.VertJustify = CENTER if word_wrap else STANDARD + self._set_rows_optimal_height(range_uno, word_wrap) return R.from_uno_range(range_uno) @@ -887,14 +891,18 @@ def addColumnWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,style if len(list_o)==0: return None - # Determine range and get UNO object range_indexes=[coord_start.letterIndex(), coord_start.numberIndex(), coord_start.letterIndex(), coord_start.numberIndex()+len(list_o)-1] range_uno=self.sheet.getCellRangeByPosition(*range_indexes) + + # Finally add content + r=[] + for o in list_o: + r.append([self.__object_to_dataarray_element(o), ]) - # Apply word wrap and alignment - range_uno.IsTextWrapped = word_wrap - range_uno.VertJustify = CENTER if word_wrap else STANDARD - self._set_rows_optimal_height(range_uno, word_wrap) + if formulas is True: + self.__setFormulaArray(range_uno, r) + else: + self.__setDataArray(range_uno, r) # Guess styles if none if styles is None: @@ -906,27 +914,22 @@ def addColumnWithStyle(self, coord_start, list_o, colors=ColorsNamed.White,style if isinstance(styles, list): for i in range(len(list_o)): cell=self.sheet.getCellByPosition(coord_start.letterIndex(), coord_start.numberIndex()+i) - cell.CellStyle = styles[i] + cell.setPropertyValue("CellStyle", styles[i]) else: - range_uno.CellStyle = styles + range_uno.setPropertyValue("CellStyle", styles) #Fast color: if isinstance(colors, list): for i in range(len(list_o)): cell=self.sheet.getCellByPosition(coord_start.letterIndex(), coord_start.numberIndex()+i) - cell.CellBackColor = colors[i] + cell.setPropertyValue("CellBackColor", colors[i]) else: - range_uno.CellBackColor = colors - - # Finally add content - r=[] - for o in list_o: - r.append([self.__object_to_dataarray_element(o), ]) - - if formulas is True: - self.__setFormulaArray(range_uno, r) - else: - self.__setDataArray(range_uno, r) + range_uno.setPropertyValue("CellBackColor", colors) + + # Apply word wrap and alignment (Applied last for maximum stability) + range_uno.IsTextWrapped = word_wrap + range_uno.VertJustify = CENTER if word_wrap else STANDARD + self._set_rows_optimal_height(range_uno, word_wrap) return R.from_coords_indexes(*range_indexes) @@ -1059,8 +1062,8 @@ def addListOfRowsWithStyle(self, coord_start, list_rows, colors=ColorsNamed.Whit else: for c, o in enumerate(list_rows[0]): columnrange=self.sheet.getCellRangeByPosition(coord_start.letterIndex()+c, coord_start.numberIndex(), coord_start.letterIndex()+c, coord_start.numberIndex()+rows-1) - columnrange.CellStyle = styles[c] - columnrange.CellBackColor = colors[c] + columnrange.setPropertyValue("CellStyle", styles[c]) + columnrange.setPropertyValue("CellBackColor", colors[c]) range_uno.IsTextWrapped = word_wrap range_uno.VertJustify = CENTER if word_wrap else STANDARD @@ -1096,7 +1099,7 @@ def addListOfColumns(self, coord_start, list_columns, formulas=True): def addListOfColumnsWithStyle(self, coord_start, list_columns, colors=ColorsNamed.White, styles=None, formulas=True, word_wrap=True): """ - Colors and styles are the colors of every column. + Colors and styles are applied per row of the input columns. Parameters: - formulas Boolean. If true formulas will be written as formula. If false as string @@ -1116,17 +1119,30 @@ def addListOfColumnsWithStyle(self, coord_start, list_columns, colors=ColorsName logger.debug(_("addListOfColumnsWithStyle has {0} rows and {1} columns. Nothing to write. Ignoring...").format(rows, columns)) return - # Determine range and get UNO object range_indexes=[coord_start.letterIndex(), coord_start.numberIndex(), coord_start.letterIndex()+columns-1, coord_start.numberIndex()+rows-1] range_uno=self.sheet.getCellRangeByPosition(*range_indexes) + # Finally add content + list_rows=lol.lol_transposed(list_columns) + r=[] + for row in list_rows: + r_row=[] + for o in row: + r_row.append(self.__object_to_dataarray_element(o)) + r.append(r_row) + + if formulas is True: + self.__setFormulaArray(range_uno, r) + else: + self.__setDataArray(range_uno, r) + # Parse colors. if colors.__class__.__name__=="list": colors=colors elif colors is None: - colors=[ColorsNamed.White]*columns + colors=[ColorsNamed.White]*rows else: #one ColorsNamed - colors=[colors]*columns + colors=[colors]*rows # Parse styles if styles is None and rows>0: @@ -1136,43 +1152,29 @@ def addListOfColumnsWithStyle(self, coord_start, list_columns, colors=ColorsName elif styles.__class__.__name__=="list": styles=styles else: - styles=[styles]*columns + styles=[styles]*rows - if len(colors)!=columns: - raise exceptions.UnogeneratorException(_("Colors must have the same number of items as data columns")) - if len(styles)!=columns: - raise exceptions.UnogeneratorException(_("Styles must have the same number of items as data columns")) + if len(colors)!=rows: + raise exceptions.UnogeneratorException(_("Colors must have the same number of items as data rows")) + if len(styles)!=rows: + raise exceptions.UnogeneratorException(_("Styles must have the same number of items as data rows")) - #Create styles by columns cellranges + #Create styles by rows if rows>0: # Optimization: If all styles are default and all colors are White, we can skip this loop if all(s == self.default_cell_style for s in styles) and all(c == ColorsNamed.White for c in colors): pass else: for c, o in enumerate(list_columns[0]): - columnrange=self.sheet.getCellRangeByPosition(coord_start.letterIndex()+c, coord_start.numberIndex(), coord_start.letterIndex()+c, coord_start.numberIndex()+rows-1) - columnrange.CellStyle = styles[c] - columnrange.CellBackColor = colors[c] + columnrange=self.sheet.getCellRangeByPosition(coord_start.letterIndex(), coord_start.numberIndex()+c, coord_start.letterIndex()+columns-1, coord_start.numberIndex()+c) + columnrange.setPropertyValue("CellBackColor", colors[c]) + columnrange.setPropertyValue("CellStyle", styles[c]) range_uno.IsTextWrapped = word_wrap range_uno.VertJustify = CENTER if word_wrap else STANDARD self._set_rows_optimal_height(range_uno, word_wrap) - # Finally add content - list_rows=lol.lol_transposed(list_columns) - r=[] - for row in list_rows: - r_row=[] - for o in row: - r_row.append(self.__object_to_dataarray_element(o)) - r.append(r_row) - - if formulas is True: - self.__setFormulaArray(range_uno, r) - else: - self.__setDataArray(range_uno, r) - return R.from_coords_indexes(*range_indexes) ## @param style If None tries to guess it @@ -1187,8 +1189,8 @@ def addCellWithStyle(self, coord, o, color=ColorsNamed.White, style=None, word_w cell=self.sheet.getCellByPosition(coord.letterIndex(), coord.numberIndex()) self.__object_to_cell(cell, o) - cell.CellStyle = style - cell.CellBackColor = color + cell.setPropertyValue("CellStyle", style) + cell.setPropertyValue("CellBackColor", color) cell.IsTextWrapped = word_wrap cell.VertJustify = CENTER if word_wrap else STANDARD self._set_row_optimal_height(coord.numberIndex(), word_wrap) From 83170ecd8f08e1f1526ae1d0802f3aa610b1fbc6 Mon Sep 17 00:00:00 2001 From: turulomio Date: Sat, 23 May 2026 21:25:03 +0200 Subject: [PATCH 25/34] Improved doc --- INSTALL.md | 2 +- README.md | 74 +++++++++++++++++------------ unogenerator/commons.py | 28 +++++++++-- unogenerator/helpers.py | 90 ++++++++++++++++++++++-------------- unogenerator/unogenerator.py | 21 +++++++++ 5 files changed, 145 insertions(+), 70 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 96cacfc..70ea103 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -2,7 +2,7 @@ Only Linux is supported -You need LibreOffice installed in your system. Imagemagick is recommended for some image operations. +You need LibreOffice installed in your system. **Imagemagick** is recommended for some image operations and **psutil** is recommended for robust process management. In some distros you need to install Python LibreOffice bindings too (python3-uno) diff --git a/README.md b/README.md index 08edcc6..3cb9880 100644 --- a/README.md +++ b/README.md @@ -7,71 +7,87 @@ ## Description -Python module to generate Libreoffice documents (ODT and ODS) programatically. +**UnoGenerator** is a powerful Python module designed to generate LibreOffice documents (ODT and ODS) programmatically with high performance and professional aesthetics. -Morever, you can export them to (.xlsx, .docx, .pdf) easyly. +Key features include: +- **Professional Defaults**: Automatic text wrapping (`word_wrap=True`) and vertical centering for professional-looking reports out of the box. +- **High Performance**: Optimized for large datasets. Inserting 10,000 rows with default formatting takes less than 0.3 seconds. +- **Rich Exports**: Easy export to `.xlsx`, `.docx`, and `.pdf`. +- **Advanced Helpers**: Flexible totals generation, automatic column width calculation, and complex data block handling. +- **Multilingual Support**: Built-in support for translations and localized document generation. -It uses Libreoffice uno module, so you need Libreoffice to be installed in your system. +It uses the LibreOffice UNO module, requiring a LibreOffice installation on your system. ## Installation -Only Linux is supported. I'm going to write unogenerator [installation methods](INSTALL.md) for some main Linux Distributions +Only Linux is supported. See the [installation guide](INSTALL.md) for detailed instructions on main Linux distributions. +## Architecture + +UnoGenerator follows a clean, template-based architecture: +- **ODS/ODT**: Generic base classes for document manipulation. +- **ODS_Standard / ODT_Standard**: Optimized subclasses that use the built-in professional templates, providing specific styles (like "Normal", "BoldCenter") and optimized row heights. ## ODT 'Hello World' example -This is a Hello World example. You get the example in odt, docx and pdf formats: +Create a professional ODT document with just a few lines: ```python from unogenerator import ODT_Standard + with ODT_Standard() as doc: - doc.addParagraph("Hello World", "Heading 1") - doc.addParagraph("Easy, isn't it","Standard") - doc.save("hello_world.odt") - doc.export_docx("hello_world.docx") - doc.export_pdf("hello_world.pdf") + doc.addParagraph("Hello World", "Heading 1") + doc.addParagraph("Easy, isn't it", "Standard") + doc.save("hello_world.odt") + doc.export_docx("hello_world.docx") + doc.export_pdf("hello_world.pdf") ``` ## ODS 'Hello World' example -This is a Hello World example. You'll get example files in ods, xlsx and pdf formats: +Generate a styled spreadsheet with automatic wrapping and alignment: ```python from unogenerator import ODS_Standard + with ODS_Standard() as doc: - doc.addCellMergedWithStyle("A1:E1", "Hello world", style="BoldCenter") - doc.save("hello_world.ods") - doc.export_xlsx("hello_world.xlsx") - doc.export_pdf("hello_world.pdf") + # word_wrap and vertical alignment are enabled by default + doc.addCellMergedWithStyle("A1:E1", "Sales Report 2026", style="BoldCenter") + doc.addRowWithStyle("A2", ["Product", "Quantity", "Price", "Total"]) + doc.save("sales_report.ods") + doc.export_xlsx("sales_report.xlsx") ``` -## Unogenerator scripts +## Advanced Features: Totals and Formulas -Python unogenerator package has the following scripts: +UnoGenerator provides advanced helpers to generate calculations quickly: -### unogenerator_monitor +```python +from unogenerator import helpers -Monitors your libreoffice server instances +# Generates both row and column totals with a custom formula template +helpers.cross_totals_from_range(doc, "B2:D10", key="=SUM({}*1.21)") +``` -### unogenerator_translation +## Unogenerator scripts -With this tool you can translate several odt files with one command. It generates .pot and .po files, where you can set your translations. Then run your command again and you'll get your files translated +The package includes several useful CLI tools: -`unogenerator_translation --from_language es --to_language en --input original.odt --input original2.odt --output_directory "translation_original"` +### unogenerator_monitor +Monitors your LibreOffice server instances and their status. -You can use --fake to see simulation of your translation +### unogenerator_translation +Translate multiple ODT files using standard `.pot` and `.po` files. +`unogenerator_translation --from_language es --to_language en --input original.odt --output_directory "translated"` ### unogenerator_demo - -With this tool you can generate a demo, remove its result files and make benchmark comparations in your system +Generate comprehensive example files and perform performance benchmarks on your system. ## Documentation -You can read [documentation](https://github.com/turulomio/unogenerator/blob/main/doc/unogenerator_documentation_en.odt?raw=true) in doc directory. It has been created with unogenerator. +Full technical [documentation](https://github.com/turulomio/unogenerator/blob/main/doc/unogenerator_documentation_en.odt?raw=true) is available in the `doc` directory, created using UnoGenerator itself. ## Development links - [LibreOffice code](https://github.com/LibreOffice/core) - [LibreOffice API](https://api.libreoffice.org/docs/idl/ref/index.html) -- [OpenOffice Forums](https://forum.openoffice.org/en/forum/viewforum.php?f=20) -- [LibreOffice Forums](https://ask.libreoffice.org/) -- [UnoGenerator API](https://coolnewton.mooo.com/doxygen/unogenerator/) +- [UnoGenerator API (Doxygen)](https://coolnewton.mooo.com/doxygen/unogenerator/) diff --git a/unogenerator/commons.py b/unogenerator/commons.py index c54019f..946b440 100644 --- a/unogenerator/commons.py +++ b/unogenerator/commons.py @@ -476,15 +476,33 @@ def addDebugSystem(level): def generate_formula_total_string(key, coord_from, coord_to): + """ + Generates a spreadsheet formula string based on a key and a coordinate range. + + Args: + key (str): The formula key. Supported: #SUM, #AVG, #MEDIAN, or a custom formula with '{}' placeholder, + or just the name of a LibreOffice function (e.g. "SUM"). + coord_from (Coord): The start coordinate of the range. + coord_to (Coord): The end coordinate of the range. + + Returns: + str: The generated formula string (e.g., "=SUM(A1:B10)"). + """ + range_str = f"{coord_from.string()}:{coord_to.string()}" + if key == "#SUM": - s="=SUM({}:{})".format(coord_from.string(), coord_to.string()) + return f"=SUM({range_str})" elif key == "#AVG": - s="=AVERAGE({}:{})".format(coord_from.string(), coord_to.string()) + return f"=AVERAGE({range_str})" elif key == "#MEDIAN": - s="=MEDIAN({}:{})".format(coord_from.string(), coord_to.string()) + return f"=MEDIAN({range_str})" + elif "{}" in key: + return key.format(range_str) else: - s=key - return s + # Assume key is just the function name (e.g., "SUM", "COUNT") + # Ensure it doesn't already have an '=' + clean_key = key.lstrip("=") + return f"={clean_key}({range_str})" def guess_object_style(o, default_style="Default"): if o is None: diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index c3766ad..4da25cc 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -168,49 +168,68 @@ def cross_totals_from_range ( """ Generates vertical and horizontal totals directly from a data range. - Calculates sums (or specified formulas) for the given range and adds "Total" labels. + Calculates sums (or specified formulas) for the given range and adds labels. Args: doc (ODS): The ODS document object. range_of_data (Range or str): The range containing the data values. - key (str, optional): Formula key to apply (e.g., "#SUM"). Defaults to "#SUM". - column_of_totals (bool, optional): Whether to generate column totals. Defaults to True. - row_of_totals (bool, optional): Whether to generate row totals. Defaults to True. + key (str, optional): Formula key, function name, or template (e.g., "#SUM", "SUM", "=SUM({})"). Defaults to "#SUM". + column_of_totals (bool, optional): Whether to generate a column of totals to the right. Defaults to True. + row_of_totals (bool, optional): Whether to generate a row of totals at the bottom. Defaults to True. vertical_total_title_style (str, optional): Style for the vertical total title. Defaults to "BoldCenter". horizontal_total_title_style (str, optional): Style for the horizontal total title. Defaults to "BoldCenter". - showing (bool, optional): If True, shows a 'Sum of totals' cell when either column_of_totals or row_of_totals is True. Defaults to False. + showing (bool, optional): Legacy parameter. If True, adds an extra 'Sum of totals' block. Defaults to False. Returns: - Range: The original data range. + Range: The new range including the generated totals. """ - range=Range.assertRange(range_of_data) - data_rows=range.numRows() - data_columns=range.numColumns() - coord_horizontal_title=range.c_start.addColumnCopy(-1).addRowCopy(data_rows) - coord_vertical_title=range.c_start.addRowCopy(-1).addColumnCopy(data_columns) - style_data=guess_object_style(doc.getValue(range.c_end)) + data_range = Range.assertRange(range_of_data) + data_rows = data_range.numRows() + data_columns = data_range.numColumns() - if column_of_totals==True and row_of_totals==True: - doc.addCellWithStyle(coord_horizontal_title, _("Total"), ColorsNamed.GrayLight, horizontal_total_title_style) - row_totals(doc, coord_horizontal_title.addColumnCopy(1), [key]*data_columns,styles=style_data, row_from=range.c_start.number) - doc.addCellWithStyle(coord_vertical_title, _("Total"), ColorsNamed.GrayLight, vertical_total_title_style) - column_totals(doc, coord_vertical_title.addRowCopy(1),[key]*(data_rows+1), styles=style_data, column_from=range.c_start.letter) - elif column_of_totals==True: - doc.addCellWithStyle(coord_vertical_title, _("Total"), ColorsNamed.GrayLight, vertical_total_title_style) - column_totals(doc, coord_vertical_title.addRowCopy(1),[key]*(data_rows+0), styles=style_data, column_from=range.c_start.letter) - if showing is True: - coord_sum_totals=coord_vertical_title.addRowCopy(data_rows+1) - doc.addCellWithStyle(coord_sum_totals, generate_formula_total_string(key, range.c_start.addColumnCopy(data_columns+1), range.c_end.addColumnCopy(1)), ColorsNamed.GrayLight, style_data) - doc.addCellWithStyle(coord_sum_totals.addColumnCopy(-1), _("Sum of totals"), ColorsNamed.GrayDark, style_data) - elif row_of_totals==True: - doc.addCellWithStyle(coord_horizontal_title, _("Total"), ColorsNamed.GrayLight, horizontal_total_title_style) - row_totals(doc, coord_horizontal_title.addColumnCopy(1),[key]*(data_columns+0), styles=style_data, row_from=range.c_start.number) #1 menos por la esquina - if showing is True: - coord_sum_totals=coord_horizontal_title.addColumnCopy(data_columns+1) - doc.addCellWithStyle(coord_sum_totals, generate_formula_total_string(key, range.c_start.addRowCopy(data_rows+1), range.c_end.addRowCopy(1)), ColorsNamed.GrayLight, style_data) - doc.addCellWithStyle(coord_sum_totals.addRowCopy(-1), _("Sum of totals"), ColorsNamed.GrayDark, style_data) - - return range_of_data + # Guessed style for data (to match totals formatting) + style_data = guess_object_style(doc.getValue(data_range.c_start), doc.default_cell_style) + + final_end_coord = data_range.c_end.copy() + + # 1. Add row of totals (Bottom) + if row_of_totals: + coord_row_totals = data_range.c_start.addRowCopy(data_rows) + row_totals(doc, coord_row_totals, [key] * data_columns, styles=style_data, row_from=data_range.c_start.number, row_to=data_range.c_end.number) + final_end_coord.addRow(1) + + # Label for row totals (bottom row) usually goes to the left of the totals row + if data_range.c_start.letterIndex() > 0: + coord_label_row = data_range.c_start.addColumnCopy(-1).addRowCopy(data_rows) + doc.addCellWithStyle(coord_label_row, _("Total"), ColorsNamed.GrayLight, horizontal_total_title_style) + + # 2. Add column of totals (Right) + if column_of_totals: + coord_col_totals = data_range.c_start.addColumnCopy(data_columns) + # If we also added a row of totals, we include it in the column totals (cross total) + total_items = data_rows + (1 if row_of_totals else 0) + column_totals(doc, coord_col_totals, [key] * total_items, styles=style_data, column_from=data_range.c_start.letter, column_to=data_range.c_end.letter) + final_end_coord.addColumn(1) + + # Label for column totals (right column) usually goes above the totals column + if data_range.c_start.numberIndex() > 0: + coord_label_column = data_range.c_start.addRowCopy(-1).addColumnCopy(data_columns) + doc.addCellWithStyle(coord_label_column, _("Total"), ColorsNamed.GrayLight, vertical_total_title_style) + + # 3. Handle legacy 'showing' parameter (extra cells) + if showing: + if column_of_totals: + coord_sum_totals = data_range.c_start.addRowCopy(data_rows + 1).addColumnCopy(data_columns) + doc.addCellWithStyle(coord_sum_totals, generate_formula_total_string(key, data_range.c_start.addColumnCopy(data_columns), final_end_coord), ColorsNamed.GrayLight, style_data) + if coord_sum_totals.letterIndex() > 0: + doc.addCellWithStyle(coord_sum_totals.addColumnCopy(-1), _("Sum of totals"), ColorsNamed.GrayDark, style_data) + elif row_of_totals: + coord_sum_totals = data_range.c_start.addColumnCopy(data_columns + 1).addRowCopy(data_rows) + doc.addCellWithStyle(coord_sum_totals, generate_formula_total_string(key, data_range.c_start.addRowCopy(data_rows), final_end_coord), ColorsNamed.GrayLight, style_data) + if coord_sum_totals.numberIndex() > 0: + doc.addCellWithStyle(coord_sum_totals.addRowCopy(-1), _("Sum of totals"), ColorsNamed.GrayDark, style_data) + + return Range.from_coords(data_range.c_start, final_end_coord) def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None, column_of_totals=False, row_of_totals=False, key="#SUM", title=None, word_wrap=False): @@ -285,8 +304,9 @@ def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_ def sheet_stylenames(doc): """ - Creates a new sheet called "Internal style names" listing all ODS styles in four columns: - CellStyles, PageStyles, GraphicStyles, and TableStyles. + Creates a new sheet called "Internal style names" listing all available styles + organized into four columns: CellStyles, PageStyles, GraphicStyles, and TableStyles. + Useful for identifying available style names for use with other methods. Args: doc (ODS): The ODS document object. diff --git a/unogenerator/unogenerator.py b/unogenerator/unogenerator.py index 4458951..abb2749 100644 --- a/unogenerator/unogenerator.py +++ b/unogenerator/unogenerator.py @@ -617,7 +617,18 @@ def pageBreak(self, style="Standard", page_before=True): class ODS(ODF): + """ + Class for generating ODS (OpenDocument Spreadsheet) documents. + Provides methods for adding data, styling, and generating totals. + """ def __init__(self, template=None, server=None): + """ + Initializes an ODS document. + + Args: + template (str, optional): Path to an ODS template file. Defaults to None. + server (LibreofficeServer, optional): An existing server instance. Defaults to None. + """ ODF.__init__(self, template, server) self._remove_default_sheet=True self._wrapped_rows = set() # Track rows that have word wrap enabled to avoid overriding them with False @@ -1651,7 +1662,17 @@ def toDictionaryOfDetailedValues(self): return r class ODS_Standard(ODS): + """ + Optimized ODS class that uses the standard project template. + Includes predefined styles (Normal, BoldCenter, etc.) and optimized row heights (452). + """ def __init__(self, server=None): + """ + Initializes an ODS_Standard document using the built-in template. + + Args: + server (LibreofficeServer, optional): An existing server instance. Defaults to None. + """ ODS.__init__(self, files('unogenerator') / 'templates/standard.ods', server) self.default_row_height = 452 self.default_cell_style = "Normal" From 6477f06843a01260bcdefa3e44ced2d65003923d Mon Sep 17 00:00:00 2001 From: turulomio Date: Sat, 23 May 2026 21:47:37 +0200 Subject: [PATCH 26/34] Corregido Sheet --- unogenerator/commons.py | 7 +- unogenerator/demo.py | 8 +- unogenerator/helpers.py | 161 ++++++++++++++++++++++------------------ 3 files changed, 97 insertions(+), 79 deletions(-) diff --git a/unogenerator/commons.py b/unogenerator/commons.py index 946b440..490358e 100644 --- a/unogenerator/commons.py +++ b/unogenerator/commons.py @@ -480,8 +480,11 @@ def generate_formula_total_string(key, coord_from, coord_to): Generates a spreadsheet formula string based on a key and a coordinate range. Args: - key (str): The formula key. Supported: #SUM, #AVG, #MEDIAN, or a custom formula with '{}' placeholder, - or just the name of a LibreOffice function (e.g. "SUM"). + key (str): The formula key or template. Supported options: + 1. Predefined aliases: '#SUM', '#AVG', '#MEDIAN'. + 2. Standard function names: e.g., 'SUM', 'COUNT', 'PRODUCT', 'MAX'. + 3. Custom templates: e.g., '=SUM({}*1.21)', '={}/100'. The '{}' + placeholder will be replaced by the range string (e.g., 'A1:B10'). coord_from (Coord): The start coordinate of the range. coord_to (Coord): The end coordinate of the range. diff --git a/unogenerator/demo.py b/unogenerator/demo.py index 12df2d2..8d0bd19 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -564,15 +564,15 @@ def demo_ods_block_column_row_cross(doc): doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) doc.createSheet("block_from_lod column of totals") - helpers.block_from_lod(doc, "A1", lod_singers, column_of_totals=True, title="block_from_lod (With total columns)") + helpers.block_from_lod(doc, "A1", lod_singers, column_of_totals=True, title="block_from_lod (With total columns)", styles="Integer") doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) doc.createSheet("block_from_lod row of totals") - helpers.block_from_lod(doc, "A1", lod_singers, row_of_totals=True, title="block_from_lod (With total rows)") + helpers.block_from_lod(doc, "A1", lod_singers, row_of_totals=True, title="block_from_lod (With total rows)", styles="Integer") doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) doc.createSheet("block_from_lod column of totals row of totals") - helpers.block_from_lod(doc, "A1", lod_singers, column_of_totals=True, row_of_totals=True, title="block_from_lod (With total columns and rows)") + helpers.block_from_lod(doc, "A1", lod_singers, column_of_totals=True, row_of_totals=True, title="block_from_lod (With total columns and rows)", styles="Integer") doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) # c_start=c_start.addColumn(lod_singers_columns +1) @@ -683,7 +683,7 @@ def demo_ods_sheet_from_lol(doc): helpers.sheet_from_lol(doc, "Sheet from LOL", [[1, 2], [3, 4]], ["Col1", "Col2"], column_of_totals=True, row_of_totals=True, titulo="LOL Table") def demo_ods_sheet_from_lod(doc): - helpers.sheet_from_lod(doc, "Sheet from LOD", lod_singers, column_of_totals=True, row_of_totals=True, titulo="LOD Table") + helpers.sheet_from_lod(doc, "Sheet from LOD", lod_singers, column_of_totals=True, row_of_totals=True, title="LOD Table", styles="Integer") def demo_ods_sheet_sort(doc): ##Sort diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index 4da25cc..4c89219 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -155,7 +155,7 @@ def column_title_values_total(doc, coord, title, values, doc.addCellWithStyle(coord.addRowCopy(i+len(values)),f"=sum({coord.addRowCopy(i).string()}:{coord.addRowCopy(i+len(values)-1).string()}",color_total,style_total) -def cross_totals_from_range ( +def cross_totals_from_range( doc, range_of_data, key="#SUM", @@ -163,25 +163,34 @@ def cross_totals_from_range ( row_of_totals=True, vertical_total_title_style="BoldCenter", horizontal_total_title_style="BoldCenter", - showing=False + showing=False, + label_column="Total", + label_row="Total", + skip_columns=0 ): """ Generates vertical and horizontal totals directly from a data range. - Calculates sums (or specified formulas) for the given range and adds labels. - Args: doc (ODS): The ODS document object. range_of_data (Range or str): The range containing the data values. - key (str, optional): Formula key, function name, or template (e.g., "#SUM", "SUM", "=SUM({})"). Defaults to "#SUM". + key (str, optional): Formula key or template. Supported options: + 1. Predefined aliases: '#SUM', '#AVG', '#MEDIAN'. + 2. Standard function names: e.g., 'SUM', 'COUNT', 'PRODUCT', 'MAX'. + 3. Custom templates: e.g., '=SUM({}*1.21)', '={}/100'. The '{}' + placeholder will be replaced by the range string (e.g., 'A1:B10'). + Defaults to "#SUM". column_of_totals (bool, optional): Whether to generate a column of totals to the right. Defaults to True. row_of_totals (bool, optional): Whether to generate a row of totals at the bottom. Defaults to True. vertical_total_title_style (str, optional): Style for the vertical total title. Defaults to "BoldCenter". horizontal_total_title_style (str, optional): Style for the horizontal total title. Defaults to "BoldCenter". showing (bool, optional): Legacy parameter. If True, adds an extra 'Sum of totals' block. Defaults to False. + label_column (str, optional): Label for the column of totals. Defaults to "Total". Set to None to omit. + label_row (str, optional): Label for the row of totals. Defaults to "Total". Set to None to omit. + skip_columns (int, optional): Number of columns to skip from the left for row totals. Defaults to 0. Returns: - Range: The new range including the generated totals. + Range: The new range including the generated totals and labels. """ data_range = Range.assertRange(range_of_data) data_rows = data_range.numRows() @@ -190,31 +199,47 @@ def cross_totals_from_range ( # Guessed style for data (to match totals formatting) style_data = guess_object_style(doc.getValue(data_range.c_start), doc.default_cell_style) + final_start_coord = data_range.c_start.copy() final_end_coord = data_range.c_end.copy() # 1. Add row of totals (Bottom) if row_of_totals: - coord_row_totals = data_range.c_start.addRowCopy(data_rows) - row_totals(doc, coord_row_totals, [key] * data_columns, styles=style_data, row_from=data_range.c_start.number, row_to=data_range.c_end.number) - final_end_coord.addRow(1) - - # Label for row totals (bottom row) usually goes to the left of the totals row - if data_range.c_start.letterIndex() > 0: - coord_label_row = data_range.c_start.addColumnCopy(-1).addRowCopy(data_rows) - doc.addCellWithStyle(coord_label_row, _("Total"), ColorsNamed.GrayLight, horizontal_total_title_style) + # Start totals row after skip_columns + coord_row_totals = data_range.c_start.addRowCopy(data_rows).addColumnCopy(skip_columns) + num_totals = data_columns - skip_columns + if num_totals > 0: + row_totals(doc, coord_row_totals, [key] * num_totals, styles=style_data, row_from=data_range.c_start.number, row_to=data_range.c_end.number) + final_end_coord.addRow(1) + + # Label for row totals + if label_row: + if skip_columns > 0: + # Place label in the skipped columns area of the totals row + coord_label_start = data_range.c_start.addRowCopy(data_rows) + coord_label_end = coord_label_start.addColumnCopy(skip_columns - 1) + range_label = Range.from_coords(coord_label_start, coord_label_end) + doc.addCellMergedWithStyle(range_label, _(label_row), ColorsNamed.GrayLight, horizontal_total_title_style) + elif data_range.c_start.letterIndex() > 0: + # Place label to the left of the range + coord_label_row = data_range.c_start.addColumnCopy(-1).addRowCopy(data_rows) + doc.addCellWithStyle(coord_label_row, _(label_row), ColorsNamed.GrayLight, horizontal_total_title_style) + if final_start_coord.letterIndex() > coord_label_row.letterIndex(): + final_start_coord = coord_label_row.copy() # 2. Add column of totals (Right) if column_of_totals: coord_col_totals = data_range.c_start.addColumnCopy(data_columns) # If we also added a row of totals, we include it in the column totals (cross total) total_items = data_rows + (1 if row_of_totals else 0) - column_totals(doc, coord_col_totals, [key] * total_items, styles=style_data, column_from=data_range.c_start.letter, column_to=data_range.c_end.letter) + column_totals(doc, coord_col_totals, [key] * total_items, styles=style_data, column_from=data_range.c_start.addColumnCopy(skip_columns).letter, column_to=data_range.c_end.letter) final_end_coord.addColumn(1) - # Label for column totals (right column) usually goes above the totals column - if data_range.c_start.numberIndex() > 0: + # Label for column totals + if label_column and data_range.c_start.numberIndex() > 0: coord_label_column = data_range.c_start.addRowCopy(-1).addColumnCopy(data_columns) - doc.addCellWithStyle(coord_label_column, _("Total"), ColorsNamed.GrayLight, vertical_total_title_style) + doc.addCellWithStyle(coord_label_column, _(label_column), ColorsNamed.GrayLight, vertical_total_title_style) + if final_start_coord.numberIndex() > coord_label_column.numberIndex(): + final_start_coord = coord_label_column.copy() # 3. Handle legacy 'showing' parameter (extra cells) if showing: @@ -223,84 +248,93 @@ def cross_totals_from_range ( doc.addCellWithStyle(coord_sum_totals, generate_formula_total_string(key, data_range.c_start.addColumnCopy(data_columns), final_end_coord), ColorsNamed.GrayLight, style_data) if coord_sum_totals.letterIndex() > 0: doc.addCellWithStyle(coord_sum_totals.addColumnCopy(-1), _("Sum of totals"), ColorsNamed.GrayDark, style_data) + if final_end_coord.numberIndex() < coord_sum_totals.numberIndex(): + final_end_coord = coord_sum_totals.copy() elif row_of_totals: coord_sum_totals = data_range.c_start.addColumnCopy(data_columns + 1).addRowCopy(data_rows) doc.addCellWithStyle(coord_sum_totals, generate_formula_total_string(key, data_range.c_start.addRowCopy(data_rows), final_end_coord), ColorsNamed.GrayLight, style_data) if coord_sum_totals.numberIndex() > 0: doc.addCellWithStyle(coord_sum_totals.addRowCopy(-1), _("Sum of totals"), ColorsNamed.GrayDark, style_data) + if final_end_coord.letterIndex() < coord_sum_totals.letterIndex(): + final_end_coord = coord_sum_totals.copy() - return Range.from_coords(data_range.c_start, final_end_coord) + return Range.from_coords(final_start_coord, final_end_coord) -def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None, column_of_totals=False, row_of_totals=False, key="#SUM", title=None, word_wrap=False): +def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_row_header=ColorsNamed.Orange, color_column_header=ColorsNamed.Green, color=ColorsNamed.White, styles=None, column_of_totals=False, row_of_totals=False, key="#SUM", title=None, word_wrap=True): """ Write cells from a list of ordered dictionaries. - Params: + + Args: doc (ODS): The ODS document object. coord_start (Coord or str): Starting coordinate. lod_ (list): List of ordered dictionaries. keys (list, optional): List of keys to write. If None, writes all keys. Defaults to None. - columns_header (int, optional): Number of columns to apply color_header. Defaults to 0. + columns_header (int, optional): Number of columns to apply color_column_header. Defaults to 0. color_row_header (int, optional): Color for row headers. Defaults to ColorsNamed.Orange. color_column_header (int, optional): Color for column headers. Defaults to ColorsNamed.Green. color (int, optional): Color for data cells. Defaults to ColorsNamed.White. styles (list or str, optional): Styles for data cells. Defaults to None. - column_of_totals: Add a total at the bottom of the block - row_of_totals: Add a total at the right of the block - key (str, optional): Formula key for totals. Defaults to "#SUM". + column_of_totals (bool, optional): Whether to generate a column of totals to the right. Defaults to False. + row_of_totals (bool, optional): Whether to generate a row of totals at the bottom. Defaults to False. + key (str, optional): Formula key or template. Supported options: + 1. Predefined aliases: '#SUM', '#AVG', '#MEDIAN'. + 2. Standard function names: e.g., 'SUM', 'COUNT', 'PRODUCT', 'MAX'. + 3. Custom templates: e.g., '=SUM({}*1.21)', '={}/100'. The '{}' + placeholder will be replaced by the range string (e.g., 'A1:B10'). + Defaults to "#SUM". title (str, optional): Title for the block. Defaults to None. - word_wrap (bool, optional): Enable word wrap and optimal height. Defaults to False. + word_wrap (bool, optional): Enable word wrap and optimal height. Defaults to True. + Returns: - Range: The range of the data without headers. Useful to set totals. + Range: The range of the data including headers and totals. """ - # Check is a Coord object and makes a copy to avoid internal coord movements coord_start=Coord.assertCoord(coord_start) c=coord_start.copy() - #Prepara el titulo + # 1. Headers + if keys is None: + keys=lod.lod_keys(lod_) + + # 2. Title if title is not None: if len(lod_)==0: doc.addCellWithStyle(c, title, ColorsNamed.Red, "BoldCenter") else: add_of_totals=1 if column_of_totals else 0 - if keys is None: - range_title=Range.from_coords("A1", Coord("A1").addColumn(len(lod_[0].keys())-1+add_of_totals)) - else: - range_title=Range.from_coords("A1", Coord("A1").addColumn(len(keys)-1)+add_of_totals) + range_title=Range.from_coords(c.copy(), c.addColumnCopy(len(keys)-1+add_of_totals)) doc.addCellMergedWithStyle(range_title, title, ColorsNamed.Red, "BoldCenter", word_wrap=word_wrap) c.addRow(1) - # Empty lod + # 3. Handle empty lod if len(lod_)==0: doc.addCellWithStyle(c, _("No data to show"), ColorsNamed.White, "BoldCenter") - return None + return Range.from_coords(coord_start, c) - - #Headers - if keys is None: - keys=lod.lod_keys(lod_) + # 4. Write column headers doc.addRowWithStyle(c, keys, color_row_header, "BoldCenter", word_wrap=word_wrap) - #Generate list of colors + # 5. Generate colors per column colors=[] for i in range(len(keys)): - if i <= columns_header-1: + if i < columns_header: colors.append(color_column_header) else: colors.append(color) - #Generate list of rows + # 6. Write data rows lol_=lod.lod2lol(lod_, keys) - range_block= doc.addListOfRowsWithStyle(c.addRow(), lol_, colors, styles, word_wrap=word_wrap) + range_data = doc.addListOfRowsWithStyle(c.addRowCopy(1), lol_, colors, styles, word_wrap=word_wrap) - # Generate totals + # 7. Generate totals if column_of_totals or row_of_totals: - if column_of_totals: - if columns_header==0: - columns_header=1 - range_block.c_start.addColumn(columns_header) ## Adds to skip columns headers - return cross_totals_from_range (doc, range_block, key, column_of_totals, row_of_totals) - return range_block + # Default to skipping the first column if columns_header is not specified, + # as it's typically a label/ID column in a LOD. + skip = columns_header if columns_header > 0 else 1 if len(keys) > 1 else 0 + final_range = cross_totals_from_range(doc, range_data, key, column_of_totals, row_of_totals, skip_columns=skip) + return Range.from_coords(coord_start, final_range.c_end) + + return Range.from_coords(coord_start, range_data.c_end) def sheet_stylenames(doc): """ @@ -426,7 +460,7 @@ def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=Color doc.freezeAndSelect(Coord.assertCoord(coord_to_freeze)) -def sheet_from_lod(doc, sheetname, lod_, column_of_totals=False, row_of_totals=False, freezeandselect=None, titulo=None, word_wrap=False, **kwargs_columnswidth): +def sheet_from_lod(doc, sheetname, lod_, column_of_totals=False, row_of_totals=False, freezeandselect=None, title=None, word_wrap=False, styles=None, **kwargs_columnswidth): """ kwargs son los parametros de la funcion setColumnsWidth """ @@ -437,31 +471,12 @@ def sheet_from_lod(doc, sheetname, lod_, column_of_totals=False, row_of_totals= max_width_cm=kwargs_columnswidth.get("max_width_cm", 15.0) doc.createSheet(sheetname) - if len(lod_)==0: - if titulo: - doc.addCellMergedWithStyle("A1:D1", titulo, ColorsNamed.Red, "BoldCenter") - else: - doc.addCellMergedWithStyle("A1:D1", "No hay datos", ColorsNamed.Red, "BoldCenter") - return - - - keys=lod_[0].keys() - if titulo is None: - c_start=Coord("A1") - else: - c_end=Coord("A1").addColumnCopy(len(keys)-1) - range_=Range.from_coords("A1", c_end) - doc.addCellMergedWithStyle(range_, titulo, ColorsNamed.Red, "BoldCenter", word_wrap=word_wrap) - c_start=Coord("A2")#Empieza abajo - - - range_final=block_from_lod(doc, c_start, lod_, column_of_totals=column_of_totals, row_of_totals=row_of_totals, word_wrap=word_wrap ) - if column_of_totals or row_of_totals: - range_cross=cross_totals_from_range (doc, range_final, "#SUM", column_of_totals, row_of_totals, "BoldCenter", "BoldCenter", False) + + range_final=block_from_lod(doc, "A1", lod_, column_of_totals=column_of_totals, row_of_totals=row_of_totals, word_wrap=word_wrap, styles=styles, title=title ) doc.setColumnsWidth(lod_, columns_width_mode, char_to_cm, padding_cm, min_width_cm, max_width_cm) if freezeandselect: doc.freezeAndSelect(freezeandselect,freezeandselect, freezeandselect) - return range_cross if column_of_totals or row_of_totals else range_final + return range_final From e0587d8d1b76dd6baa14c6fb78a5e523aa78a7a5 Mon Sep 17 00:00:00 2001 From: turulomio Date: Sat, 23 May 2026 22:02:43 +0200 Subject: [PATCH 27/34] =?UTF-8?q?A=C3=B1adido=20=20resto=20de=20helpers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unogenerator/demo.py | 45 +++++++++++ unogenerator/helpers.py | 163 +++++++++++++++++++++++----------------- 2 files changed, 137 insertions(+), 71 deletions(-) diff --git a/unogenerator/demo.py b/unogenerator/demo.py index 8d0bd19..bf86915 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -237,8 +237,10 @@ def demo_ods_standard(language, server): demo_ods_sheet_sort(doc) demo_ods_sheet_word_wrap(doc) demo_ods_block_column_row_cross(doc) + demo_ods_sheet_helpers_single(doc) demo_ods_sheet_from_lod(doc) demo_ods_sheet_from_lol(doc) + demo_ods_sheet_block_from_lol(doc) # demo_ods_block_column_row_cros(doc) # demo_ods_sheet_columns_width_with_list(doc) @@ -738,4 +740,47 @@ def demo_ods_sheet_columns_width_with_list(doc): doc.createSheet("ColumnsWidthsList") doc.addListOfRowsWithStyle("A1", lol_numbers) doc.setColumnsWidth(lol_numbers, types.ColumnsWidthMode.FROM_LOL) + +def demo_ods_sheet_block_from_lol(doc): + headers = ["Product", "Qty", "Price"] + data = [ + ["Item A", 10, 20.5], + ["Item B", 5, 15.0], + ["Item C", 2, 100.0], + ] + + doc.createSheet("block_from_lol column of totals") + helpers.block_from_lol(doc, "A1", data, headers=headers, column_of_totals=True, title="block_from_lol (With total columns)", styles="Float2") + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + + doc.createSheet("block_from_lol row of totals") + helpers.block_from_lol(doc, "A1", data, headers=headers, row_of_totals=True, title="block_from_lol (With total rows)", styles="Float2") + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + + doc.createSheet("block_from_lol both totals") + helpers.block_from_lol(doc, "A1", data, headers=headers, column_of_totals=True, row_of_totals=True, title="block_from_lol (With both totals)", styles="Float2") + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + +def demo_ods_sheet_helpers_single(doc): + ## row_title_values_total + doc.createSheet("row_title_values_total") + helpers.row_title_values_total(doc, "A1", "My Row Sum", [10, 20, 30]) + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + + ## column_title_values_total + doc.createSheet("column_title_values_total") + helpers.column_title_values_total(doc, "A1", "My Col Sum", [1, 2, 3, 4, 5]) + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + + ## row_totals + doc.createSheet("row_totals") + doc.addRowWithStyle("A1", [100, 200, 300, 400]) + helpers.row_totals(doc, "A2", ["#SUM"] * 4, row_from="1") + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + + ## column_totals + doc.createSheet("column_totals") + doc.addColumnWithStyle("A1", [10, 20, 30, 40]) + helpers.column_totals(doc, "B1", ["#SUM"] * 4, column_from="A") + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) \ No newline at end of file diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index 4c89219..da43055 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -1,6 +1,6 @@ from unogenerator.commons import ColorsNamed, Coord, Range, guess_object_style, generate_formula_total_string from unogenerator import ODS, types -from pydicts import lod +from pydicts import lod, lol from collections import OrderedDict from gettext import translation from logging import debug @@ -8,8 +8,8 @@ from math import ceil from importlib.resources import files - logger = logging.getLogger(__name__) # Get logger for this module + try: t=translation('unogenerator', files("unogenerator") / 'locale') _=t.gettext @@ -78,26 +78,16 @@ def column_totals(doc, coord, list_of_totals, color=ColorsNamed.GrayLight, style styles = guess_object_style(doc.getValue(first_coord_from), doc.default_cell_style) doc.addColumnWithStyle(coord, formulas, colors=color, styles=styles) - + + def row_title_values_total( doc, coord, title, values, style_title=None, color_title=ColorsNamed.Orange, style_values=None, color_values=ColorsNamed.White, style_total=None, color_total=ColorsNamed.GrayLight ): """ - Creates a column containing a title, a list of values, and a total sum at the bottom. - - Args: - doc (ODS): The ODS document object. - coord (Coord or str): Starting coordinate. - title (str): Title to be placed at the starting coordinate. - values (list): List of values to be placed below the title. - style_title (str, optional): Style for the title cell. Defaults to "BoldCenter". - color_title (int, optional): Background color for the title. Defaults to ColorsNamed.Orange. - style_values (list or str, optional): Styles for the value cells. Defaults to None. - color_values (list or int, optional): Colors for the value cells. Defaults to ColorsNamed.White. - style_total (str, optional): Style for the total cell. Defaults to None. - color_total (int, optional): Background color for the total cell. Defaults to ColorsNamed.GrayLight. + Parameters: + - values: list: Only one row """ coord=Coord.assertCoord(coord) @@ -113,34 +103,23 @@ def row_title_values_total( doc, coord, title, values, doc.addCellWithStyle(coord,title,color_title,style_title) i=i+1 - doc.addRowWithStyle(coord.addColumnCopy(i),values,colors=color_values,styles=style_values) - doc.addCellWithStyle(coord.addColumnCopy(i+len(values)),f"=sum({coord.addColumnCopy(i).string()}:{coord.addColumnCopy(i+len(values)-1).string()}",color_total,style_total) + doc.addCellWithStyle(coord.addColumnCopy(i+len(values)),f"=sum({coord.addColumnCopy(i).string()}:{coord.addColumnCopy(i+len(values)-1).string()})",color_total,style_total) + -def column_title_values_total(doc, coord, title, values, +def column_title_values_total( doc, coord, title, values, style_title=None, color_title=ColorsNamed.Orange, style_values=None, color_values=ColorsNamed.White, style_total=None, color_total=ColorsNamed.GrayLight ): """ - Creates a row containing a title, a list of values, and a total sum at the end. - - Args: - doc (ODS): The ODS document object. - coord (Coord or str): Starting coordinate. - title (str): Title to be placed at the starting coordinate. - values (list): List of values to be placed after the title. - style_title (str, optional): Style for the title cell. Defaults to "Bold". - color_title (int, optional): Background color for the title. Defaults to ColorsNamed.Orange. - style_values (list or str, optional): Styles for the value cells. Defaults to None. - color_values (list or int, optional): Colors for the value cells. Defaults to ColorsNamed.White. - style_total (str, optional): Style for the total cell. Defaults to None. - color_total (int, optional): Background color for the total cell. Defaults to ColorsNamed.GrayLight. + Parameters: + - values: list: Only one column """ coord=Coord.assertCoord(coord) if style_title is None: - style_title="BoldCenter" + style_title="Bold" if style_total is None and len(values)>0: style_total=guess_object_style(values[0]) @@ -152,7 +131,7 @@ def column_title_values_total(doc, coord, title, values, i=i+1 doc.addColumnWithStyle(coord.addRowCopy(i),values,colors=color_values,styles=style_values) - doc.addCellWithStyle(coord.addRowCopy(i+len(values)),f"=sum({coord.addRowCopy(i).string()}:{coord.addRowCopy(i+len(values)-1).string()}",color_total,style_total) + doc.addCellWithStyle(coord.addRowCopy(i+len(values)),f"=sum({coord.addRowCopy(i).string()}:{coord.addRowCopy(i+len(values)-1).string()})",color_total,style_total) def cross_totals_from_range( @@ -323,8 +302,8 @@ def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_ colors.append(color) # 6. Write data rows - lol_=lod.lod2lol(lod_, keys) - range_data = doc.addListOfRowsWithStyle(c.addRowCopy(1), lol_, colors, styles, word_wrap=word_wrap) + lol_data=lod.lod2lol(lod_, keys) + range_data = doc.addListOfRowsWithStyle(c.addRowCopy(1), lol_data, colors, styles, word_wrap=word_wrap) # 7. Generate totals if column_of_totals or row_of_totals: @@ -336,6 +315,64 @@ def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_ return Range.from_coords(coord_start, range_data.c_end) + +def block_from_lol(doc, coord_start, lor, headers=None, colors=ColorsNamed.White, styles=None, column_of_totals=False, row_of_totals=False, key="#SUM", title=None, word_wrap=True): + """ + Writes cells from a list of lists (lor) with optional headers and totals. + + Args: + doc (ODS): The ODS document object. + coord_start (Coord or str): Starting coordinate. + lor (list): List of lists (data rows). + headers (list, optional): List of header strings. Defaults to None. + colors (list or int, optional): Column colors. Defaults to ColorsNamed.White. + styles (list or str, optional): Column styles. Defaults to None. + column_of_totals (bool, optional): Whether to generate a column of totals to the right. Defaults to False. + row_of_totals (bool, optional): Whether to generate a row of totals at the bottom. Defaults to False. + key (str, optional): Formula key or template. Defaults to "#SUM". + title (str, optional): Main title for the block. Defaults to None. + word_wrap (bool, optional): Enable word wrap and optimal height. Defaults to True. + + Returns: + Range: The range of the data including headers and totals. + """ + coord_start = Coord.assertCoord(coord_start) + c = coord_start.copy() + + # 1. Title + if title is not None: + if not lor and not headers: + doc.addCellWithStyle(c, title, ColorsNamed.Red, "BoldCenter") + else: + num_cols = len(headers) if headers else len(lor[0]) if lor else 1 + add_of_totals = 1 if column_of_totals else 0 + range_title = Range.from_coords(c.copy(), c.addColumnCopy(num_cols - 1 + add_of_totals)) + doc.addCellMergedWithStyle(range_title, title, ColorsNamed.Red, "BoldCenter", word_wrap=word_wrap) + c.addRow(1) + + # 2. Handle empty data + if not lor and not headers: + doc.addCellWithStyle(c, _("No data to show"), ColorsNamed.White, "BoldCenter") + return Range.from_coords(coord_start, c) + + # 3. Write column headers + if headers: + doc.addRowWithStyle(c, headers, ColorsNamed.Orange, "BoldCenter", word_wrap=word_wrap) + c.addRow(1) + + # 4. Write data rows + range_data = doc.addListOfRowsWithStyle(c, lor, colors, styles, word_wrap=word_wrap) + + # 5. Generate totals + if column_of_totals or row_of_totals: + # Default to skipping the first column if it looks like a label column + skip = 1 if (headers and len(headers) > 1) or (lor and len(lor[0]) > 1) else 0 + final_range = cross_totals_from_range(doc, range_data, key, column_of_totals, row_of_totals, skip_columns=skip) + return Range.from_coords(coord_start, final_range.c_end) + + return Range.from_coords(coord_start, range_data.c_end) + + def sheet_stylenames(doc): """ Creates a new sheet called "Internal style names" listing all available styles @@ -363,20 +400,20 @@ def sheet_stylenames(doc): sheet_from_lod(doc, "Internal style names", lod_, freezeandselect="A2", columns_width_mode=types.ColumnsWidthMode.FROM_LOD) -def sheet_from_lol(doc, sheetname, lor, headers, column_of_totals=False, row_of_totals=False, freezeandselect=None, titulo=None, word_wrap=False, **kwargs_columnswidth): +def sheet_from_lol(doc, sheetname, lor, headers, column_of_totals=False, row_of_totals=False, freezeandselect=None, titulo=None, word_wrap=True, **kwargs_columnswidth): """ - Creates a sheet from a list of lists (lol) with headers and optional totals. + Creates a sheet from a list of lists (lor) with headers and optional totals. Args: doc (ODS): The ODS document object. sheetname (str): The name for the new sheet. lor (list): The list of lists (data rows). headers (list): The list of header strings. - column_of_totals (bool, optional): Whether to generate column totals. Defaults to False. - row_of_totals (bool, optional): Whether to generate row totals. Defaults to False. + column_of_totals (bool, optional): Whether to generate a column of totals to the right. Defaults to False. + row_of_totals (bool, optional): Whether to generate a row of totals at the bottom. Defaults to False. freezeandselect (str, optional): Coordinate to freeze panes at. Defaults to None. titulo (str, optional): An optional title to merge across the top of the sheet. Defaults to None. - word_wrap (bool, optional): Enable word wrap and optimal height. Defaults to False. + word_wrap (bool, optional): Enable word wrap and optimal height. Defaults to True. **kwargs_columnswidth: Keyword arguments for setColumnsWidth. """ columns_width_mode = kwargs_columnswidth.get("columns_width_mode", types.ColumnsWidthMode.FROM_LOL) @@ -387,38 +424,23 @@ def sheet_from_lol(doc, sheetname, lor, headers, column_of_totals=False, row_of_ doc.createSheet(sheetname) - if not lor and not headers: - if titulo: - doc.addCellMergedWithStyle("A1:D1", titulo, ColorsNamed.Red, "BoldCenter") - else: - doc.addCellMergedWithStyle("A1:D1", "No hay datos", ColorsNamed.Red, "BoldCenter") - return - - c_start = Coord("A1") - - if titulo: - c_end = c_start.addColumnCopy(len(headers) - 1) - range_titulo = Range.from_coords(c_start, c_end) - doc.addCellMergedWithStyle(range_titulo, titulo, ColorsNamed.Orange, "BoldCenter", word_wrap=word_wrap) - c_start.addRow(1) - - doc.addRowWithStyle(c_start, headers, ColorsNamed.Orange, "BoldCenter", word_wrap=word_wrap) - c_start_data = c_start.addRowCopy(1) + range_block = block_from_lol( + doc, "A1", lor, headers, + column_of_totals=column_of_totals, + row_of_totals=row_of_totals, + title=titulo, + word_wrap=word_wrap + ) - range_data = doc.addListOfRowsWithStyle(c_start_data, lor, word_wrap=word_wrap) - - if column_of_totals or row_of_totals: - cross_totals_from_range(doc, range_data, "#SUM", column_of_totals, row_of_totals, "BoldCenter", "BoldCenter", False) - - data_to_measure = [headers] + lor + data_to_measure = [headers] + lor if headers else lor doc.setColumnsWidth(data_to_measure, columns_width_mode, char_to_cm, padding_cm, min_width_cm, max_width_cm) if freezeandselect: - doc.freezeAndSelect(freezeandselect,freezeandselect, freezeandselect) + doc.freezeAndSelect(freezeandselect, freezeandselect, freezeandselect) - return range_data + return range_block -def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=ColorsNamed.Orange, coord_to_freeze="A2", max_rows=1048575, word_wrap=False): +def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=ColorsNamed.Orange, coord_to_freeze="A2", max_rows=1048575, word_wrap=True): """ Splits a large list of rows across multiple sheets if it exceeds LibreOffice Calc's row limits. @@ -435,7 +457,7 @@ def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=Color If int, applies to all columns. If list, specifies width per column. Defaults to None. coord_to_freeze (Coord or str, optional): Coordinate to freeze panes at. Defaults to "A2". max_rows (int, optional): Maximum number of rows per sheet (Calc limit is 1,048,576). Defaults to 1048575. - word_wrap (bool, optional): Enable word wrap and optimal height. Defaults to False. + word_wrap (bool, optional): Enable word wrap and optimal height. Defaults to True. """ ceil_=ceil(len(lor)/max_rows) for num_sheet in range(ceil_): @@ -460,7 +482,7 @@ def sheet_split_with_big_lol(doc, sheet_name, lor, headers, headers_colors=Color doc.freezeAndSelect(Coord.assertCoord(coord_to_freeze)) -def sheet_from_lod(doc, sheetname, lod_, column_of_totals=False, row_of_totals=False, freezeandselect=None, title=None, word_wrap=False, styles=None, **kwargs_columnswidth): +def sheet_from_lod(doc, sheetname, lod_, column_of_totals=False, row_of_totals=False, freezeandselect=None, title=None, word_wrap=True, styles=None, **kwargs_columnswidth): """ kwargs son los parametros de la funcion setColumnsWidth """ @@ -495,7 +517,7 @@ def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None, col row_of_totals (bool, optional): Whether to generate row totals. Defaults to False. freezeandselect (str or Coord, optional): Coordinate to freeze and select. Defaults to None. key (str, optional): Formula key for totals (e.g., "#SUM"). Defaults to "#SUM". - word_wrap (bool, optional): Enable word wrap and optimal height. Defaults to False. + word_wrap (bool, optional): Enable word wrap and optimal height. Defaults to True. Returns: Range: The data range (excluding headers). @@ -541,4 +563,3 @@ def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None, col doc.freezeAndSelect(freezeandselect, freezeandselect, freezeandselect) return range_ - From e8407a9a79702eb7c6ea3e7c16718f11151d0408 Mon Sep 17 00:00:00 2001 From: turulomio Date: Sat, 23 May 2026 22:25:35 +0200 Subject: [PATCH 28/34] Added helpers --- HELPERS.md | 180 ++++++++++++++++++++++++++++++++++++++++ unogenerator/demo.py | 23 +++++ unogenerator/helpers.py | 20 ++++- 3 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 HELPERS.md diff --git a/HELPERS.md b/HELPERS.md new file mode 100644 index 0000000..262a433 --- /dev/null +++ b/HELPERS.md @@ -0,0 +1,180 @@ +# UnoGenerator Helpers Specification + +Detailed reference of all helper functions in `unogenerator.helpers`. + +## Table of Contents +1. [Totals Generation (Basic)](#1-totals-generation-basic) +2. [Totals Generation (Titled)](#2-totals-generation-titled) +3. [Advanced Totals (Cross-Calculations)](#3-advanced-totals-cross-calculations) +4. [Data Block Helpers](#4-data-block-helpers) +5. [Complete Sheet Helpers](#5-complete-sheet-helpers) +6. [Utility Helpers](#6-utility-helpers) + +--- + +## 1. Totals Generation (Basic) + +### `row_totals` +Generates a horizontal row of formulas. +- `doc` (ODS): The ODS document object. +- `coord` (Coord or str): Starting coordinate. +- `list_of_totals` (list): List of formula keys (e.g. `["#SUM", "#AVG"]`). +- `color` (int, default=ColorsNamed.GrayLight): Background color. +- `styles` (list or str, default=None): Cell style(s). +- `row_from` (str, default="2"): Start row for calculation range. +- `row_to` (str, default=None): End row for calculation range. + +### `column_totals` +Generates a vertical column of formulas. +- `doc` (ODS): The ODS document object. +- `coord` (Coord or str): Starting coordinate. +- `list_of_totals` (list): List of formula keys. +- `color` (int, default=ColorsNamed.GrayLight): Background color. +- `styles` (list or str, default=None): Cell style(s). +- `column_from` (str, default="B"): Start column for calculation range. +- `column_to` (str, default=None): End column for calculation range. + +--- + +## 2. Totals Generation (Titled) + +### `row_title_values_total` +Creates a row with a title, a sequence of values, and their sum. +- `doc` (ODS): The ODS document object. +- `coord` (Coord or str): Starting coordinate. +- `title` (str): Title for the row. +- `values` (list): List of numerical values. +- `style_title` (str, default="Bold"): Style for the title. +- `color_title` (int, default=ColorsNamed.Orange): Title color. +- `style_values` (str, default=None): Style for the values. +- `color_values` (int, default=ColorsNamed.White): Values color. +- `style_total` (str, default=None): Style for the total. +- `color_total` (int, default=ColorsNamed.GrayLight): Total color. + +### `column_title_values_total` +Creates a column with a title, a sequence of values, and their sum. +- `doc` (ODS): The ODS document object. +- `coord` (Coord or str): Starting coordinate. +- `title` (str): Title for the column. +- `values` (list): List of numerical values. +- `style_title` (str, default="Bold"): Style for the title. +- `color_title` (int, default=ColorsNamed.Orange): Title color. +- `style_values` (str, default=None): Style for the values. +- `color_values` (int, default=ColorsNamed.White): Values color. +- `style_total` (str, default=None): Style for the total. +- `color_total` (int, default=ColorsNamed.GrayLight): Total color. + +--- + +## 3. Advanced Totals (Cross-Calculations) + +### `cross_totals_from_range` +Generates optimized vertical and horizontal totals for a data range. +- `doc` (ODS): The ODS document object. +- `range_of_data` (Range or str): The data range to calculate from. +- `key` (str, default="#SUM"): Formula alias, function name, or template (e.g. `"=SUM({}*1.21)"`). +- `column_of_totals` (bool, default=True): If True, adds column at the right. +- `row_of_totals` (bool, default=True): If True, adds row at the bottom. +- `vertical_total_title_style` (str, default="BoldCenter"): Style for right label. +- `horizontal_total_title_style` (str, default="BoldCenter"): Style for bottom label. +- `showing` (bool, default=False): Legacy. If True, adds extra "Sum of totals" block. +- `label_column` (str, default="Total"): Text for the vertical total label. +- `label_row` (str, default="Total"): Text for the horizontal total label. +- `skip_columns` (int, default=0): Number of columns to skip for bottom row totals. + +--- + +## 4. Data Block Helpers + +### `block_from_lod` +Inserts data from a List of Ordered Dictionaries. +- `doc` (ODS): The ODS document object. +- `coord_start` (Coord or str): Starting coordinate. +- `lod_` (list): The list of dictionaries. +- `keys` (list, default=None): Specific keys to write. +- `columns_header` (int, default=0): Number of columns to color as headers. +- `color_row_header` (int, default=ColorsNamed.Orange): Color for the keys row. +- `color_column_header` (int, default=ColorsNamed.Green): Color for the header columns. +- `color` (int, default=ColorsNamed.White): Background for data cells. +- `styles` (list or str, default=None): Styles for data cells. +- `column_of_totals` (bool, default=False): Generate totals on the right. +- `row_of_totals` (bool, default=False): Generate totals at the bottom. +- `key` (str, default="#SUM"): Formula key (see `cross_totals_from_range`). +- `title` (str, default=None): Merged title for the block. +- `word_wrap` (bool, default=True): Enable text wrapping. + +### `block_from_lol` +Inserts data from a List of Lists. +- `doc` (ODS): The ODS document object. +- `coord_start` (Coord or str): Starting coordinate. +- `lor` (list): The list of lists. +- `headers` (list, default=None): Column header names. +- `colors` (list or int, default=ColorsNamed.White): Column colors. +- `styles` (list or str, default=None): Column styles. +- `column_of_totals` (bool, default=False): Generate totals on the right. +- `row_of_totals` (bool, default=False): Generate totals at the bottom. +- `key` (str, default="#SUM"): Formula key. +- `title` (str, default=None): Merged title for the block. +- `word_wrap` (bool, default=True): Enable text wrapping. + +### `block_from_lod_with_headers` +LOD writer with hierarchical sub-headers. +- `doc` (ODS): The ODS document object. +- `lod_` (list): List of dictionaries. +- `coord` (Coord or str): Starting coordinate. +- `subtitles` (list, default=[]): Groups of columns. List of `[title, first_key]`. +- `titulo` (str, default=None): Main title. +- `column_of_totals` (bool, default=False): Generate totals on the right. +- `row_of_totals` (bool, default=False): Generate totals at the bottom. +- `freezeandselect` (Coord or str, default=None): Auto-freeze coordinate. +- `key` (str, default="#SUM"): Formula key. +- `word_wrap` (bool, default=True): Enable text wrapping. + +--- + +## 5. Complete Sheet Helpers + +### `sheet_from_lod` +Creates a new sheet and populates it from an LOD. +- `doc` (ODS): The ODS document object. +- `sheetname` (str): Name of the new sheet. +- `lod_` (list): List of dictionaries. +- `column_of_totals` (bool, default=False): Right totals. +- `row_of_totals` (bool, default=False): Bottom totals. +- `freezeandselect` (str, default=None): Coordination to freeze. +- `title` (str, default=None): Main title. +- `word_wrap` (bool, default=True): Text wrap. +- `styles` (list or str, default=None): Data styles. +- `**kwargs_columnswidth`: Extra params for column width calculation. + +### `sheet_from_lol` +Creates a new sheet and populates it from an LOL. +- `doc` (ODS): The ODS document object. +- `sheetname` (str): Name of the new sheet. +- `lor` (list): List of lists. +- `headers` (list): Header names. +- `column_of_totals` (bool, default=False): Right totals. +- `row_of_totals` (bool, default=False): Bottom totals. +- `freezeandselect` (str, default=None): Coordination to freeze. +- `titulo` (str, default=None): Main title. +- `word_wrap` (bool, default=True): Text wrap. +- `**kwargs_columnswidth`: Extra params for column width calculation. + +### `sheet_split_with_big_lol` +Creates multiple sheets for massive datasets. +- `doc` (ODS): The ODS document object. +- `sheet_name` (str): Base name for sheets. +- `lor` (list): Massive list of lists. +- `headers` (list): Header names. +- `headers_colors` (int, default=ColorsNamed.Orange): Header background. +- `coord_to_freeze` (Coord or str, default="A2"): Freeze coordinate. +- `max_rows` (int, default=1048575): Max rows per sheet. +- `word_wrap` (bool, default=True): Text wrap. + +--- + +## 6. Utility Helpers + +### `sheet_stylenames` +Generates a reference sheet with available document styles. +- `doc` (ODS): The ODS document object. diff --git a/unogenerator/demo.py b/unogenerator/demo.py index bf86915..5e76abc 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -241,6 +241,7 @@ def demo_ods_standard(language, server): demo_ods_sheet_from_lod(doc) demo_ods_sheet_from_lol(doc) demo_ods_sheet_block_from_lol(doc) + demo_ods_sheet_columns_width_modes(doc) # demo_ods_block_column_row_cros(doc) # demo_ods_sheet_columns_width_with_list(doc) @@ -783,4 +784,26 @@ def demo_ods_sheet_helpers_single(doc): doc.addColumnWithStyle("A1", [10, 20, 30, 40]) helpers.column_totals(doc, "B1", ["#SUM"] * 4, column_from="A") doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + +def demo_ods_sheet_columns_width_modes(doc): + lod_widths = [ + OrderedDict({"Column 1": "Short", "Column 2": "This is a much longer string to measure", "Column 3": 100}), + OrderedDict({"Column 1": "A medium string", "Column 2": "Short", "Column 3": 20000}), + OrderedDict({"Column 1": "A very very very long string that should affect quantile 90", "Column 2": "Medium", "Column 3": 3}), + ] + + # 1. MANUAL + helpers.sheet_from_lod(doc, "Width MANUAL", lod_widths, columns_width_mode=types.ColumnsWidthMode.MANUAL, value=[5, 10, 5]) + + # 2. FROM_LOD (Default behavior: max of all) + helpers.sheet_from_lod(doc, "Width FROM_LOD", lod_widths, columns_width_mode=types.ColumnsWidthMode.FROM_LOD) + + # 3. FROM_LOD_0 (Uses first dictionary) + helpers.sheet_from_lod(doc, "Width FROM_LOD_0", lod_widths, columns_width_mode=types.ColumnsWidthMode.FROM_LOD_0) + + # 4. FROM_LOD_QUANTILE_90 (Uses 90th percentile of lengths) + helpers.sheet_from_lod(doc, "Width FROM_LOD_Q90", lod_widths, columns_width_mode=types.ColumnsWidthMode.FROM_LOD_QUANTILE_90) + + # 5. FROM_SHEET_CELLS (Measures from generated cells) + helpers.sheet_from_lod(doc, "Width FROM_SHEET_CELLS", lod_widths, columns_width_mode=types.ColumnsWidthMode.FROM_SHEET_CELLS) \ No newline at end of file diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index da43055..993f994 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -421,6 +421,7 @@ def sheet_from_lol(doc, sheetname, lor, headers, column_of_totals=False, row_of_ padding_cm = kwargs_columnswidth.get("padding_cm", 0.5) min_width_cm = kwargs_columnswidth.get("min_width_cm", 2.0) max_width_cm = kwargs_columnswidth.get("max_width_cm", 15.0) + value = kwargs_columnswidth.get("value") doc.createSheet(sheetname) @@ -432,8 +433,13 @@ def sheet_from_lol(doc, sheetname, lor, headers, column_of_totals=False, row_of_ word_wrap=word_wrap ) - data_to_measure = [headers] + lor if headers else lor - doc.setColumnsWidth(data_to_measure, columns_width_mode, char_to_cm, padding_cm, min_width_cm, max_width_cm) + if value is None: + if columns_width_mode == types.ColumnsWidthMode.FROM_SHEET_CELLS: + value = doc + else: + value = [headers] + lor if headers else lor + + doc.setColumnsWidth(value, columns_width_mode, char_to_cm, padding_cm, min_width_cm, max_width_cm) if freezeandselect: doc.freezeAndSelect(freezeandselect, freezeandselect, freezeandselect) @@ -491,11 +497,19 @@ def sheet_from_lod(doc, sheetname, lod_, column_of_totals=False, row_of_totals= padding_cm=kwargs_columnswidth.get("padding_cm", 0.5) min_width_cm=kwargs_columnswidth.get("min_width_cm", 2.0) max_width_cm=kwargs_columnswidth.get("max_width_cm", 15.0) + value = kwargs_columnswidth.get("value") doc.createSheet(sheetname) range_final=block_from_lod(doc, "A1", lod_, column_of_totals=column_of_totals, row_of_totals=row_of_totals, word_wrap=word_wrap, styles=styles, title=title ) - doc.setColumnsWidth(lod_, columns_width_mode, char_to_cm, padding_cm, min_width_cm, max_width_cm) + + if value is None: + if columns_width_mode == types.ColumnsWidthMode.FROM_SHEET_CELLS: + value = doc + else: + value = lod_ + + doc.setColumnsWidth(value, columns_width_mode, char_to_cm, padding_cm, min_width_cm, max_width_cm) if freezeandselect: doc.freezeAndSelect(freezeandselect,freezeandselect, freezeandselect) return range_final From e5b1bfe4311091eae14c656d8a9fc1372abf493e Mon Sep 17 00:00:00 2001 From: turulomio Date: Sun, 24 May 2026 07:47:26 +0200 Subject: [PATCH 29/34] Fixed errors --- unogenerator/demo.py | 176 ++++++++++++---------------------------- unogenerator/helpers.py | 14 ++-- 2 files changed, 60 insertions(+), 130 deletions(-) diff --git a/unogenerator/demo.py b/unogenerator/demo.py index 5e76abc..0d06f65 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -32,6 +32,12 @@ {"Singer": "Roy Orbison", "Songs": 100, "Albums": 20, "Best song": "Crying"}, ] +lod_widths = [ + OrderedDict({"Column 1": "Short", "Column 2": "This is a much longer string to measure", "Column 3": 100}), + OrderedDict({"Column 1": "A medium string", "Column 2": "Short", "Column 3": 20000}), + OrderedDict({"Column 1": "A very very very long string that should affect quantile 90", "Column 2": "Medium", "Column 3": 3}), + ] + lod_singers_rows=len(lod_singers) lod_singers_columns=len(lod_singers[0].keys()) @@ -41,6 +47,8 @@ ["One Two Three", "Four", "Ten Two Three"], ] +lol_numbers_headers=["A","B","C"] + lol_numbers_rows=len(lol_numbers) lol_numbers_columns=len(lol_numbers[0]) @@ -243,14 +251,6 @@ def demo_ods_standard(language, server): demo_ods_sheet_block_from_lol(doc) demo_ods_sheet_columns_width_modes(doc) - # demo_ods_block_column_row_cros(doc) - # demo_ods_sheet_columns_width_with_list(doc) - # demo_ods_sheet_columns_width_with_lol(doc) - # demo_ods_sheet_columns_width_with_lod(doc) - # demo_ods_sheet_from_lol(doc) - # demo_ods_sheet_helpers(doc) - # demo_ods_sheet_helpers_from_lod(doc) - demo_ods_sheet_split_with_big_lol(doc) helpers.sheet_stylenames(doc) @@ -578,115 +578,46 @@ def demo_ods_block_column_row_cross(doc): helpers.block_from_lod(doc, "A1", lod_singers, column_of_totals=True, row_of_totals=True, title="block_from_lod (With total columns and rows)", styles="Integer") doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) - # c_start=c_start.addColumn(lod_singers_columns +1) - # helpers.block_from_lod(doc, c_start.addRowCopy(), lod_singers, columns_header=1, color_row_header=ColorsNamed.Red, column_of_totals=True, title="block_from_lod (With total columns)") - - - # # block_from_lod_with_headers - # c_start=c_start.from_index(0, c_start.numberIndex()+lod_singers_rows+3)# column_index, row_index - # helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ - # ["Singer header", "Singer"], - # ["Song header", "Best song"] - # ], titulo="block_from_lod_with_headers") - - - # c_start=c_start.addColumn(lod_singers_columns +1) - # helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ - # ["Singer header", "Singer"], - # ["Song header", "Best song"] - # ], titulo="block_from_lod_with_headers (With total columns)", column_of_totals=True) - - # c_start=c_start.addColumn(lod_singers_columns +3) - # helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ - # ["Singer header", "Singer"], - # ["Song header", "Best song"] - # ], titulo="block_from_lod_with_headers (With total rows)", row_of_totals=True) - - - # doc.addCell("A11", c_start.string()) - # c_start=c_start.addColumn(lod_singers_columns +2) - # doc.addCell("A12", c_start.string()) - # helpers.block_from_lod_with_headers(doc, lod_singers, c_start, [ - # ["Singer header", "Singer"], - # ["Song header", "Best song"] - # ], titulo="block_from_lod_with_headers (With total columns and rows)", column_of_totals=True, row_of_totals=True) - - - # # block_from_lod - # c_start=c_start.from_index(0, c_start.numberIndex()+lod_singers_rows+3)# column_index, row_index - - - # doc.addCellMergedWithStyle(Range(c_start,c_end),"List of rows with row_totals", ColorsNamed.Orange, "BoldCenter") - # range_=doc.addListOfRowsWithStyle("A2", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) - # helpers.row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="2", row_to="4") - - # doc.addCellMergedWithStyle("A8:C8","List of columns with row_totals", ColorsNamed.Orange, "BoldCenter") - # range_=doc.addListOfColumnsWithStyle("A9", [[1,2,3],[4,5,6],[7,8,9]], ColorsNamed.White) - # helpers.row_totals(doc, range_.c_start.addRowCopy(range_.numRows()), ["#SUM"]*3, styles=None, row_from="9", row_to="11") - - # doc.addCellMergedWithStyle("A15:E15","List of rows with cross_totals_from_range in rows and columns", ColorsNamed.Orange, "BoldCenter") - # range_=doc.addListOfRowsWithStyle("A16", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - # helpers.cross_totals_from_range(doc, range_, column_of_totals=True, row_of_totals=True) - - - # doc.addCellMergedWithStyle("A22:E22","List of rows with cross_totals_from_range in rows", ColorsNamed.Orange, "BoldCenter") - # range_=doc.addListOfRowsWithStyle("A23", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), column_of_totals=False, row_of_totals=True) #Removes one column to filter first alphanumerical column - - # doc.addCellMergedWithStyle("A29:E29","List of rows with cross_totals_from_range in columns", ColorsNamed.Orange, "BoldCenter") - # range_=doc.addListOfRowsWithStyle("A30", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), column_of_totals=True, row_of_totals=False) - - # doc.addCellMergedWithStyle("A35:E35","List of rows with cross_totals_from_range in rows showing", ColorsNamed.Orange, "BoldCenter") - # range_=doc.addListOfRowsWithStyle("A36", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), column_of_totals=False, row_of_totals=True, showing=True) - - # doc.addCellMergedWithStyle("A42:E42","List of rows with cross_totals_from_range in columns showing", ColorsNamed.Orange, "BoldCenter") - # range_=doc.addListOfRowsWithStyle("A43", [["A",12000,2,3, 6],["B",1020,5,6, 7],["C",20404,8,9, 8]], ColorsNamed.White) - # helpers.cross_totals_from_range(doc, range_.addColumnBefore(-1), column_of_totals=True, row_of_totals=False, showing=True) - - doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) - -def demo_ods_sheet_helpers(doc): - ## HELPERS - doc.createSheet("Helpers") - doc.setSheetStyle("Portrait") - doc.addCellMergedWithStyle("A1:E1","Helper values with total (horizontal)", ColorsNamed.Orange, "BoldCenter") - helpers.row_title_values_total(doc, "A2", "Suma 3", [1,2,3]) + # block_from_lod_with_headers + doc.createSheet("block_from_lod_with_headers") + helpers.block_from_lod_with_headers(doc, lod_singers, "A1", [ + ["Singer header", "Singer"], + ["Song header", "Best song"] + ], titulo="block_from_lod_with_headers") + doc.setColumnsWidth(lod_singers, types.ColumnsWidthMode.FROM_LOD) - doc.addCellMergedWithStyle("A4:A9","Helper values with total (vertical)", ColorsNamed.Orange, "VerticalBoldCenter") - helpers.column_title_values_total(doc, "B4", "Suma 3", [1,2,3, 4]) - doc.addCellMergedWithStyle("A11:E11","Column totals example", ColorsNamed.Orange, "BoldCenter") - doc.addListOfColumnsWithStyle("A12", [[10, 20, 30], [5, 15, 25]], ColorsNamed.White) - helpers.column_totals(doc, "C12", ["#SUM"]*2, column_from="A") + doc.createSheet("block_from_lod_with_headers column_of_totals") + helpers.block_from_lod_with_headers(doc, lod_singers, "A1", [ + ["Singer header", "Singer"], + ["Song header", "Best song"] + ], titulo="block_from_lod_with_headers (With total columns)", column_of_totals=True) + doc.setColumnsWidth(lod_singers, types.ColumnsWidthMode.FROM_LOD) + doc.createSheet("block_from_lod_with_headers row_of_totals") + helpers.block_from_lod_with_headers(doc, lod_singers, "A1", [ + ["Singer header", "Singer"], + ["Song header", "Best song"] + ], titulo="block_from_lod_with_headers (With total rows)", row_of_totals=True) + doc.setColumnsWidth(lod_singers, types.ColumnsWidthMode.FROM_LOD) - - doc.addCellMergedWithStyle("A33:D33","List of ordered dictionaries one method with totals", ColorsNamed.Orange, "BoldCenter") - lod=[] - lod.append(OrderedDict({"Singer": "Elvis", "Songs": 10000 , "Albums": 100})) - lod.append(OrderedDict({"Singer": "Roy Orbison", "Songs": 100, "Albums": 20 })) - helpers.block_from_lod(doc, "A34", lod, columns_header=1) - doc.addCellMergedWithStyle("A40:E40", "Block from LOD with headers", ColorsNamed.Orange, "BoldCenter") - lod_headers = [ - OrderedDict({"ID": 1, "Name": "Product A", "Price": 10.5, "Stock": 100}), - OrderedDict({"ID": 2, "Name": "Product B", "Price": 20.0, "Stock": 50}), - ] - subtitles = [["General", "ID"], ["Details", "Price"]] - helpers.block_from_lod_with_headers(doc, lod_headers, "A41", subtitles=subtitles, titulo="Products", column_of_totals=True, row_of_totals=True) + doc.createSheet("block_from_lod_with_headers column_of_totals row_of_totals") + helpers.block_from_lod_with_headers(doc, lod_singers, "A1", [ + ["Singer header", "Singer"], + ["Song header", "Best song"] + ], titulo="block_from_lod_with_headers (With total columns and rows)", column_of_totals=True, row_of_totals=True) + doc.setColumnsWidth(lod_singers, types.ColumnsWidthMode.FROM_LOD) - doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) def demo_ods_sheet_from_lol(doc): ## Sheet from LOL and LOD - helpers.sheet_from_lol(doc, "Sheet from LOL", [[1, 2], [3, 4]], ["Col1", "Col2"], column_of_totals=True, row_of_totals=True, titulo="LOL Table") + helpers.sheet_from_lol(doc, "sheet_from_lol", lol_numbers, lol_numbers_headers, column_of_totals=True, row_of_totals=True, titulo="LOL Sheet") def demo_ods_sheet_from_lod(doc): - helpers.sheet_from_lod(doc, "Sheet from LOD", lod_singers, column_of_totals=True, row_of_totals=True, title="LOD Table", styles="Integer") + helpers.sheet_from_lod(doc, "sheet_from_lod", lod_singers, column_of_totals=True, row_of_totals=True, title="LOD Sheet", styles="Integer") def demo_ods_sheet_sort(doc): ##Sort @@ -725,22 +656,7 @@ def demo_ods_sheet_split_with_big_lol(doc): helpers.sheet_split_with_big_lol(doc, "Splits in 400 rows", lol_thousands, ["Integer", "String", "Datetime"], max_rows=400) -def demo_ods_sheet_columns_width_with_lod(doc): - ## COLUMNS WIDTH LOD - doc.createSheet("ColumnsWidthsLOD") - helpers.block_from_lod(doc, "A1", lod_singers, columns_header=1) - doc.setColumnsWidth(lod_singers,types.ColumnsWidthMode.FROM_LOD) - -def demo_ods_sheet_columns_width_with_lol(doc): - doc.createSheet("ColumnsWidthsLOL") - doc.addListOfRowsWithStyle("A1", lol_numbers) - doc.setColumnsWidth(lol_numbers, types.ColumnsWidthMode.FROM_LOL) - -def demo_ods_sheet_columns_width_with_list(doc): - doc.createSheet("ColumnsWidthsList") - doc.addListOfRowsWithStyle("A1", lol_numbers) - doc.setColumnsWidth(lol_numbers, types.ColumnsWidthMode.FROM_LOL) def demo_ods_sheet_block_from_lol(doc): headers = ["Product", "Qty", "Price"] @@ -750,6 +666,13 @@ def demo_ods_sheet_block_from_lol(doc): ["Item C", 2, 100.0], ] + + + + doc.createSheet("block_from_lol empty") + helpers.block_from_lol(doc, "A1", [], headers=headers, column_of_totals=True, title="block_from_lol (With total columns)", styles="Float2") + doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) + doc.createSheet("block_from_lol column of totals") helpers.block_from_lol(doc, "A1", data, headers=headers, column_of_totals=True, title="block_from_lol (With total columns)", styles="Float2") doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) @@ -786,11 +709,6 @@ def demo_ods_sheet_helpers_single(doc): doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) def demo_ods_sheet_columns_width_modes(doc): - lod_widths = [ - OrderedDict({"Column 1": "Short", "Column 2": "This is a much longer string to measure", "Column 3": 100}), - OrderedDict({"Column 1": "A medium string", "Column 2": "Short", "Column 3": 20000}), - OrderedDict({"Column 1": "A very very very long string that should affect quantile 90", "Column 2": "Medium", "Column 3": 3}), - ] # 1. MANUAL helpers.sheet_from_lod(doc, "Width MANUAL", lod_widths, columns_width_mode=types.ColumnsWidthMode.MANUAL, value=[5, 10, 5]) @@ -806,4 +724,12 @@ def demo_ods_sheet_columns_width_modes(doc): # 5. FROM_SHEET_CELLS (Measures from generated cells) helpers.sheet_from_lod(doc, "Width FROM_SHEET_CELLS", lod_widths, columns_width_mode=types.ColumnsWidthMode.FROM_SHEET_CELLS) - \ No newline at end of file + + + doc.createSheet("Width FROM_LOL") + doc.addListOfRowsWithStyle("A1", lol_numbers) + doc.setColumnsWidth(lol_numbers, types.ColumnsWidthMode.FROM_LOL) + + doc.createSheet("Width FROM_LIST") + doc.addListOfRowsWithStyle("A1", [lol_numbers[0]]) + doc.setColumnsWidth(lol_numbers[0], types.ColumnsWidthMode.FROM_LIST) \ No newline at end of file diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index 993f994..bcc8c43 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -306,14 +306,14 @@ def block_from_lod(doc, coord_start, lod_, keys=None, columns_header=0, color_ range_data = doc.addListOfRowsWithStyle(c.addRowCopy(1), lol_data, colors, styles, word_wrap=word_wrap) # 7. Generate totals - if column_of_totals or row_of_totals: + if (column_of_totals or row_of_totals) and range_data: # Default to skipping the first column if columns_header is not specified, # as it's typically a label/ID column in a LOD. skip = columns_header if columns_header > 0 else 1 if len(keys) > 1 else 0 final_range = cross_totals_from_range(doc, range_data, key, column_of_totals, row_of_totals, skip_columns=skip) return Range.from_coords(coord_start, final_range.c_end) - return Range.from_coords(coord_start, range_data.c_end) + return Range.from_coords(coord_start, range_data.c_end if range_data else c) def block_from_lol(doc, coord_start, lor, headers=None, colors=ColorsNamed.White, styles=None, column_of_totals=False, row_of_totals=False, key="#SUM", title=None, word_wrap=True): @@ -364,13 +364,13 @@ def block_from_lol(doc, coord_start, lor, headers=None, colors=ColorsNamed.White range_data = doc.addListOfRowsWithStyle(c, lor, colors, styles, word_wrap=word_wrap) # 5. Generate totals - if column_of_totals or row_of_totals: + if (column_of_totals or row_of_totals) and range_data: # Default to skipping the first column if it looks like a label column skip = 1 if (headers and len(headers) > 1) or (lor and len(lor[0]) > 1) else 0 final_range = cross_totals_from_range(doc, range_data, key, column_of_totals, row_of_totals, skip_columns=skip) return Range.from_coords(coord_start, final_range.c_end) - return Range.from_coords(coord_start, range_data.c_end) + return Range.from_coords(coord_start, range_data.c_end if range_data else c) def sheet_stylenames(doc): @@ -556,7 +556,11 @@ def block_from_lod_with_headers(doc, lod_, coord, subtitles=[], titulo=None, col # Crea titulo principal if titulo is not None: - doc.addCellMergedWithStyle(Range.from_coords(coord,coord.addColumnCopy(len(keys)-1)), titulo, ColorsNamed.Red, "BoldCenter", word_wrap=word_wrap) + if column_of_totals: + add_of_totals=1 + else: + add_of_totals=0 + doc.addCellMergedWithStyle(Range.from_coords(coord,coord.addColumnCopy(len(keys)-1+add_of_totals)), titulo, ColorsNamed.Red, "BoldCenter", word_wrap=word_wrap) coord.addRow(1) From 57eef068e8871dc45d99765f885b327a3cf2cae8 Mon Sep 17 00:00:00 2001 From: turulomio Date: Sun, 24 May 2026 07:55:30 +0200 Subject: [PATCH 30/34] Reordering tests --- unogenerator/demo.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/unogenerator/demo.py b/unogenerator/demo.py index 0d06f65..f58ddb9 100644 --- a/unogenerator/demo.py +++ b/unogenerator/demo.py @@ -244,14 +244,13 @@ def demo_ods_standard(language, server): demo_ods_sheet_styles(doc) demo_ods_sheet_sort(doc) demo_ods_sheet_word_wrap(doc) - demo_ods_block_column_row_cross(doc) - demo_ods_sheet_helpers_single(doc) + demo_ods_helpers_single(doc) + demo_ods_block_from_lod(doc) + demo_ods_block_from_lol(doc) + demo_ods_block_from_lod_with_headers(doc) demo_ods_sheet_from_lod(doc) demo_ods_sheet_from_lol(doc) - demo_ods_sheet_block_from_lol(doc) - demo_ods_sheet_columns_width_modes(doc) - - + demo_ods_columns_width_modes(doc) demo_ods_sheet_split_with_big_lol(doc) helpers.sheet_stylenames(doc) @@ -548,7 +547,7 @@ def demo_ods_sheet_styles(doc): -def demo_ods_block_column_row_cross(doc): +def demo_ods_block_from_lod(doc): ## List of rows doc.createSheet("block_from_lod") helpers.block_from_lod(doc, "A1", lod_singers) @@ -579,7 +578,7 @@ def demo_ods_block_column_row_cross(doc): doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) - +def demo_ods_block_from_lod_with_headers(doc): # block_from_lod_with_headers doc.createSheet("block_from_lod_with_headers") helpers.block_from_lod_with_headers(doc, lod_singers, "A1", [ @@ -658,7 +657,7 @@ def demo_ods_sheet_split_with_big_lol(doc): -def demo_ods_sheet_block_from_lol(doc): +def demo_ods_block_from_lol(doc): headers = ["Product", "Qty", "Price"] data = [ ["Item A", 10, 20.5], @@ -670,7 +669,7 @@ def demo_ods_sheet_block_from_lol(doc): doc.createSheet("block_from_lol empty") - helpers.block_from_lol(doc, "A1", [], headers=headers, column_of_totals=True, title="block_from_lol (With total columns)", styles="Float2") + helpers.block_from_lol(doc, "A1", [], headers=headers, title="block_from_lol (With total columns)", styles="Float2") doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) doc.createSheet("block_from_lol column of totals") @@ -685,7 +684,7 @@ def demo_ods_sheet_block_from_lol(doc): helpers.block_from_lol(doc, "A1", data, headers=headers, column_of_totals=True, row_of_totals=True, title="block_from_lol (With both totals)", styles="Float2") doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) -def demo_ods_sheet_helpers_single(doc): +def demo_ods_helpers_single(doc): ## row_title_values_total doc.createSheet("row_title_values_total") helpers.row_title_values_total(doc, "A1", "My Row Sum", [10, 20, 30]) @@ -708,7 +707,7 @@ def demo_ods_sheet_helpers_single(doc): helpers.column_totals(doc, "B1", ["#SUM"] * 4, column_from="A") doc.setColumnsWidth(doc, types.ColumnsWidthMode.FROM_SHEET_CELLS) -def demo_ods_sheet_columns_width_modes(doc): +def demo_ods_columns_width_modes(doc): # 1. MANUAL helpers.sheet_from_lod(doc, "Width MANUAL", lod_widths, columns_width_mode=types.ColumnsWidthMode.MANUAL, value=[5, 10, 5]) From 28aa3c7f1b380e0b9bd8f311bbcdf4154dd540e9 Mon Sep 17 00:00:00 2001 From: turulomio Date: Sun, 24 May 2026 08:17:58 +0200 Subject: [PATCH 31/34] Add test for unogenerator_cleaner --- tests/test_cleaner.py | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/test_cleaner.py diff --git a/tests/test_cleaner.py b/tests/test_cleaner.py new file mode 100644 index 0000000..ae95834 --- /dev/null +++ b/tests/test_cleaner.py @@ -0,0 +1,49 @@ + +import pytest +from unittest.mock import patch, MagicMock +from unogenerator.monitor import command_cleaner +import os +import shutil + +def test_command_cleaner(tmp_path): + # Mocking scandir to return our test entries + # But wait, command_cleaner uses hardcoded "/tmp" + # To test it without actually touching /tmp, we would need to mock scandir or change the function to accept a path. + # However, since it's a "cleaner", we can test it by creating specific files in /tmp if we are careful. + + test_dir = "/tmp/unogenerator_pytest_dir" + test_file = "/tmp/unogenerator_pytest_file" + + os.makedirs(test_dir, exist_ok=True) + with open(test_file, "w") as f: + f.write("test") + + assert os.path.exists(test_dir) + assert os.path.exists(test_file) + + with patch("unogenerator.monitor.run") as mock_run: + command_cleaner() + + # Verify killall was called + mock_run.assert_called_with(['killall', '-9', 'soffice.bin'], check=False) + + # Verify files are gone + assert not os.path.exists(test_dir) + assert not os.path.exists(test_file) + +import sys + +def test_cleaner_entry_point(): + # Test the cleaner() function which parses args + with patch("unogenerator.monitor.command_cleaner") as mock_command: + with patch.object(sys, 'argv', ['unogenerator_cleaner']): + from unogenerator.monitor import cleaner + cleaner() + mock_command.assert_called_once() + +def test_cleaner_no_files(): + # Test that it doesn't crash if no files exist + with patch("unogenerator.monitor.scandir", return_value=[]): + with patch("unogenerator.monitor.run"): + command_cleaner() + # Should not raise exception From 22eee13bb8015f82a319595906c8bd2ac34c370d Mon Sep 17 00:00:00 2001 From: turulomio Date: Sun, 24 May 2026 08:26:05 +0200 Subject: [PATCH 32/34] Added tests form monitor y translation --- tests/test_monitor.py | 61 +++++++++++++++++++++++++++++++ tests/test_translation.py | 76 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 tests/test_monitor.py create mode 100644 tests/test_translation.py diff --git a/tests/test_monitor.py b/tests/test_monitor.py new file mode 100644 index 0000000..e3f7a36 --- /dev/null +++ b/tests/test_monitor.py @@ -0,0 +1,61 @@ + +import pytest +from unittest.mock import patch, MagicMock +from unogenerator.monitor import command_monitor, monitor +import sys +from datetime import datetime, timedelta + +class MockProcess: + def __init__(self, pid, name, cmdline): + self.pid = pid + self.info = {'name': name, 'cmdline': cmdline, 'pid': pid} + + def memory_info(self): + m = MagicMock() + m.rss = 1024 * 1024 + return m + + def status(self): + return "running" + + def create_time(self): + return (datetime.now() - timedelta(minutes=5)).timestamp() + + def cpu_percent(self, interval=None): + return 5.0 + + def connections(self): + return [1, 2] + +def test_command_monitor_one_iteration(): + # Mock processes and scandir + mock_p = MockProcess(1234, "soffice.bin", ["loffice", "--accept=socket,host=localhost,port=2002;urp;StarOffice.ServiceManager", "-env:UserInstallation=file:///tmp/unogenerator2002"]) + + with patch("unogenerator.monitor.process_iter", return_value=[mock_p]): + with patch("unogenerator.monitor.scandir", return_value=[]): + with patch("unogenerator.monitor.sleep", side_effect=InterruptedError("Stop loop")): + with pytest.raises(InterruptedError): + command_monitor(60, 1) + +def test_monitor_entry_point(): + # Test the monitor() function which parses args + with patch("unogenerator.monitor.command_monitor") as mock_command: + with patch.object(sys, 'argv', ['unogenerator_monitor', '--refresh', '1']): + monitor() + mock_command.assert_called_once_with(60, 1) + +def test_monitor_with_legacy_dirs(): + # Test identifying temporal directories without processes + mock_p = MockProcess(1234, "soffice.bin", ["loffice", "--accept=socket,host=localhost,port=2002;urp;StarOffice.ServiceManager", "-env:UserInstallation=file:///tmp/unogenerator2002"]) + + # Mock a directory entry + mock_entry = MagicMock() + mock_entry.is_dir.return_value = True + mock_entry.name = "unogenerator2003" # Different port, no process + + with patch("unogenerator.monitor.process_iter", return_value=[mock_p]): + with patch("unogenerator.monitor.scandir", return_value=[mock_entry]): + with patch("unogenerator.monitor.sleep", side_effect=InterruptedError): + with pytest.raises(InterruptedError): + command_monitor(60, 1) + # We could capture stdout here to verify it mentions unogenerator2003 diff --git a/tests/test_translation.py b/tests/test_translation.py new file mode 100644 index 0000000..3c0263e --- /dev/null +++ b/tests/test_translation.py @@ -0,0 +1,76 @@ + +import pytest +from unittest.mock import patch, MagicMock +from unogenerator.translation import command_translation, translation +from unogenerator import ODT_Standard, LibreofficeServer +import os +import shutil +import sys + +def test_command_translation(libreoffice_server, tmp_path): + # 1. Create a sample ODT file + input_odt = str(tmp_path / "test_input.odt") + with ODT_Standard(server=libreoffice_server) as doc: + doc.addParagraph("Hello world", "Heading 1") + doc.addParagraph("This is a test paragraph.", "Standard") + doc.save(input_odt) + + assert os.path.exists(input_odt) + + output_dir = str(tmp_path / "translation_output") + os.makedirs(os.path.join(output_dir, "es"), exist_ok=True) + po_path = os.path.join(output_dir, "es", "es.po") + + # 2. Run command_translation + # We mock run_check but we MUST provide a valid PO file for polib to read + with patch("unogenerator.translation.run_check") as mock_run: + def side_effect(command, shell=False): + if "msginit" in command or "msgmerge" in command: + from polib import POFile, POEntry + po = POFile() + po.append(POEntry(msgid="Hello world", msgstr="Hola mundo")) + po.append(POEntry(msgid="This is a test paragraph.", msgstr="Este es un párrafo de prueba.")) + po.save(po_path) + return MagicMock(returncode=0) + + mock_run.side_effect = side_effect + + command_translation( + from_language="en", + to_language="es", + input=[input_odt], + output_directory=output_dir, + fake=False, + pdf=False + ) + + # 3. Verify output files + assert os.path.exists(os.path.join(output_dir, "catalogue.pot")) + assert os.path.exists(os.path.join(output_dir, "es", "test_input.odt")) + +def test_translation_entry_point(tmp_path): + # Test the translation() function which parses args + input_odt = str(tmp_path / "test_input_entry.odt") + # Just touch the file to make it exist + with open(input_odt, "w") as f: + f.write("dummy") + + with patch("unogenerator.translation.command_translation") as mock_command: + with patch.object(sys, 'argv', ['unogenerator_translation', '--from_language', 'en', '--to_language', 'es', '--input', input_odt]): + translation() + mock_command.assert_called_once() + +def test_translation_unsupported_file(capsys): + # Test with a non-ODT file + with patch("unogenerator.translation.getEntriesFromDocument"): + command_translation( + from_language="en", + to_language="es", + input=["test.txt"], + output_directory="./test_unsupported" + ) + captured = capsys.readouterr() + # Locale agnostic check + assert "ODT" in captured.out + if os.path.exists("./test_unsupported"): + shutil.rmtree("./test_unsupported") From bfe600f75b1e0088ac927163c96ae071622e0ddf Mon Sep 17 00:00:00 2001 From: turulomio Date: Sun, 24 May 2026 08:29:05 +0200 Subject: [PATCH 33/34] Removed showing logic --- unogenerator/helpers.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/unogenerator/helpers.py b/unogenerator/helpers.py index bcc8c43..cd69b9a 100644 --- a/unogenerator/helpers.py +++ b/unogenerator/helpers.py @@ -142,7 +142,6 @@ def cross_totals_from_range( row_of_totals=True, vertical_total_title_style="BoldCenter", horizontal_total_title_style="BoldCenter", - showing=False, label_column="Total", label_row="Total", skip_columns=0 @@ -163,7 +162,6 @@ def cross_totals_from_range( row_of_totals (bool, optional): Whether to generate a row of totals at the bottom. Defaults to True. vertical_total_title_style (str, optional): Style for the vertical total title. Defaults to "BoldCenter". horizontal_total_title_style (str, optional): Style for the horizontal total title. Defaults to "BoldCenter". - showing (bool, optional): Legacy parameter. If True, adds an extra 'Sum of totals' block. Defaults to False. label_column (str, optional): Label for the column of totals. Defaults to "Total". Set to None to omit. label_row (str, optional): Label for the row of totals. Defaults to "Total". Set to None to omit. skip_columns (int, optional): Number of columns to skip from the left for row totals. Defaults to 0. @@ -220,23 +218,6 @@ def cross_totals_from_range( if final_start_coord.numberIndex() > coord_label_column.numberIndex(): final_start_coord = coord_label_column.copy() - # 3. Handle legacy 'showing' parameter (extra cells) - if showing: - if column_of_totals: - coord_sum_totals = data_range.c_start.addRowCopy(data_rows + 1).addColumnCopy(data_columns) - doc.addCellWithStyle(coord_sum_totals, generate_formula_total_string(key, data_range.c_start.addColumnCopy(data_columns), final_end_coord), ColorsNamed.GrayLight, style_data) - if coord_sum_totals.letterIndex() > 0: - doc.addCellWithStyle(coord_sum_totals.addColumnCopy(-1), _("Sum of totals"), ColorsNamed.GrayDark, style_data) - if final_end_coord.numberIndex() < coord_sum_totals.numberIndex(): - final_end_coord = coord_sum_totals.copy() - elif row_of_totals: - coord_sum_totals = data_range.c_start.addColumnCopy(data_columns + 1).addRowCopy(data_rows) - doc.addCellWithStyle(coord_sum_totals, generate_formula_total_string(key, data_range.c_start.addRowCopy(data_rows), final_end_coord), ColorsNamed.GrayLight, style_data) - if coord_sum_totals.numberIndex() > 0: - doc.addCellWithStyle(coord_sum_totals.addRowCopy(-1), _("Sum of totals"), ColorsNamed.GrayDark, style_data) - if final_end_coord.letterIndex() < coord_sum_totals.letterIndex(): - final_end_coord = coord_sum_totals.copy() - return Range.from_coords(final_start_coord, final_end_coord) From 7099ee12a96070619288677fd5c4e7c6acdaba85 Mon Sep 17 00:00:00 2001 From: turulomio Date: Sun, 24 May 2026 08:35:54 +0200 Subject: [PATCH 34/34] Fixing action errors --- .github/workflows/python-app.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 1c093f1..88711bd 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -24,7 +24,7 @@ jobs: - name: Install LibreOffice and uno dependencies run: | sudo apt-get update - sudo apt-get install -y libreoffice python3-uno imagemagick # Removed explicit python3.10 and python3.10-venv + sudo apt-get install -y libreoffice python3-uno imagemagick gettext # Removed explicit python3.10 and python3.10-venv - name: Install python environment run: | python -m venv .venv --system-site-packages # Use 'python' and remove 'sudo'