@odoo/o-spreadsheet 19.4.6 → 19.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 19.4.6
6
- * @date 2026-08-10T12:33:32.728Z
7
- * @hash 5c667fe
5
+ * @version 19.4.8
6
+ * @date 2026-08-19T09:25:13.754Z
7
+ * @hash 9dd828e
8
8
  */
9
9
 
10
10
  import * as owl from "@odoo/owl";
@@ -4025,6 +4025,18 @@ function toString(data) {
4025
4025
  default: return "";
4026
4026
  }
4027
4027
  }
4028
+ /**
4029
+ * Only converts the decimal separator, ignore number format such as % or dates
4030
+ */
4031
+ function toLocaleString(data, locale) {
4032
+ const value = toValue(data);
4033
+ switch (typeof value) {
4034
+ case "string": return value;
4035
+ case "number": return value.toString().replace(".", locale.decimalSeparator);
4036
+ case "boolean": return value ? "TRUE" : "FALSE";
4037
+ default: return "";
4038
+ }
4039
+ }
4028
4040
  /** Normalize string by setting it to lowercase and replacing accent letters with plain letters */
4029
4041
  const normalizeString = memoize(function normalizeString(str) {
4030
4042
  return str.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
@@ -5957,6 +5969,9 @@ var FunctionRegistry = class extends Registry {
5957
5969
  }
5958
5970
  };
5959
5971
  const functionRegistry = new FunctionRegistry();
5972
+ /** Formulas using one of these functions are never squished: they are always exported
5973
+ * as a full formula string, and they don't become the base formula of the next cells. */
5974
+ const NonSquishableFunctionRegistry = new Registry();
5960
5975
 
5961
5976
  //#endregion
5962
5977
  //#region src/helpers/references.ts
@@ -15475,18 +15490,21 @@ const figureRegistry = new Registry();
15475
15490
  figureRegistry.add("chart", {
15476
15491
  Component: ChartFigure,
15477
15492
  SidePanelComponent: "ChartPanel",
15478
- menuBuilder: getChartMenuActions
15493
+ menuBuilder: getChartMenuActions,
15494
+ isThemeDependant: true
15479
15495
  });
15480
15496
  figureRegistry.add("image", {
15481
15497
  Component: ImageFigure,
15482
15498
  keepRatio: true,
15483
15499
  minFigSize: 20,
15484
15500
  borderWidth: 0,
15485
- menuBuilder: getImageMenuActions
15501
+ menuBuilder: getImageMenuActions,
15502
+ isThemeDependant: false
15486
15503
  });
15487
15504
  figureRegistry.add("carousel", {
15488
15505
  Component: CarouselFigure,
15489
- menuBuilder: getCarouselMenuActions
15506
+ menuBuilder: getCarouselMenuActions,
15507
+ isThemeDependant: true
15490
15508
  });
15491
15509
 
15492
15510
  //#endregion
@@ -15515,6 +15533,9 @@ var FigureComponent = class extends Component {
15515
15533
  get isSelected() {
15516
15534
  return this.env.model.getters.getSelectedFigureIds().includes(this.props.figureUI.id);
15517
15535
  }
15536
+ get isThemeDependant() {
15537
+ return figureRegistry.get(this.props.figureUI.tag).isThemeDependant;
15538
+ }
15518
15539
  get figureRegistry() {
15519
15540
  return figureRegistry;
15520
15541
  }
@@ -51149,7 +51170,8 @@ var Squisher = class {
51149
51170
  * The result of this method is:
51150
51171
  * - if the cell is not a formula, returns the content as is and resets the base formula
51151
51172
  * - if the cell is a formula:
51152
- * - if there is no previous formula, or the normalized formula is different from the previous one, resets the base formula to this one and returns the full formula string
51173
+ * - if there is no previous formula, or the normalized formula is different from the previous one or it contains blacklisted functions (see NonSquishableFunctionRegistry),
51174
+ * resets the base formula to this one and returns the full formula string
51153
51175
  * - else, compares the literal values and range dependencies to the previous formula, and for each parameter:
51154
51176
  * - for numbers: returns a relative change (+N or -N) if possible, else the full number or "=" if unchanged
51155
51177
  * - for strings: returns the full string if changed, else "="
@@ -51157,6 +51179,10 @@ var Squisher = class {
51157
51179
  * */
51158
51180
  squish(cell, forSheetId) {
51159
51181
  if (cell.isFormula) {
51182
+ if (NonSquishableFunctionRegistry.getAll().some((functionName) => cell.compiledFormula.usesSymbol(functionName))) {
51183
+ this.resetBaseFormula();
51184
+ return cell.compiledFormula.toFormulaString(this.getters, this.rangeToStringOptions);
51185
+ }
51160
51186
  let numbers = [];
51161
51187
  let strings = [];
51162
51188
  let references = [];
@@ -62417,7 +62443,7 @@ var Session = class extends EventBus {
62417
62443
  }
62418
62444
  message = {
62419
62445
  ...message,
62420
- commands: this.commandSquisher.squish(revision.commands)
62446
+ commands: revision.commands
62421
62447
  };
62422
62448
  }
62423
62449
  if (this.isReplayingInitialRevisions) throw new Error(`Trying to send a new revision while replaying initial revision. This can lead to endless dispatches every time the spreadsheet is open.
@@ -77833,12 +77859,13 @@ var FilterMenu = class extends Component {
77833
77859
  }
77834
77860
  sortFilterZone(sortDirection) {
77835
77861
  const filterPosition = this.props.filterPosition;
77836
- const tableZone = this.table?.range.zone;
77862
+ const table = this.table;
77863
+ const tableZone = table?.range.zone;
77837
77864
  if (!filterPosition || !tableZone || tableZone.top === tableZone.bottom) return;
77838
77865
  const sheetId = this.env.model.getters.getActiveSheetId();
77839
77866
  const contentZone = {
77840
77867
  ...tableZone,
77841
- top: tableZone.top + 1
77868
+ top: tableZone.top + table.config.numberOfHeaders
77842
77869
  };
77843
77870
  const sortAnchor = {
77844
77871
  col: filterPosition.col,
@@ -80095,7 +80122,7 @@ const ARRAYTOTEXT = {
80095
80122
  else if (_format === 0) {
80096
80123
  const rowSeparator = this.locale.decimalSeparator === "," ? "/" : ",";
80097
80124
  return transposeMatrix(_array).flatMap((row) => row.map((value) => {
80098
- return isEvaluationError(value.value) ? value.value : toString(value);
80125
+ return isEvaluationError(value.value) ? value.value : toLocaleString(value, this.locale);
80099
80126
  })).join(rowSeparator);
80100
80127
  } else return new EvaluationError(_t("Format must be 0 or 1"));
80101
80128
  },
@@ -85861,7 +85888,7 @@ const CONCAT = {
85861
85888
  description: _t("Concatenation of two values."),
85862
85889
  args: [arg("value1 (string)", _t("The value to which value2 will be appended.")), arg("value2 (string)", _t("The value to append to value1."))],
85863
85890
  compute: function(value1, value2) {
85864
- return toString(value1) + toString(value2);
85891
+ return toLocaleString(value1, this.locale) + toLocaleString(value2, this.locale);
85865
85892
  },
85866
85893
  isExported: true
85867
85894
  };
@@ -86744,7 +86771,7 @@ const CLEAN = {
86744
86771
  description: _t("Remove non-printable characters from a piece of text."),
86745
86772
  args: [arg("text (string)", _t("The text whose non-printable characters are to be removed."))],
86746
86773
  compute: function(text) {
86747
- const _text = toString(text);
86774
+ const _text = toLocaleString(text, this.locale);
86748
86775
  let cleanedStr = "";
86749
86776
  for (const char of _text) if (char && char.charCodeAt(0) > 31) cleanedStr += char;
86750
86777
  return cleanedStr;
@@ -86755,7 +86782,7 @@ const CONCATENATE = {
86755
86782
  description: _t("Appends strings to one another."),
86756
86783
  args: [arg("string (string, range<string>, repeating)", _t("String to append in sequence."))],
86757
86784
  compute: function(...datas) {
86758
- return reduceAny(datas, (acc, a) => acc + toString(a), "");
86785
+ return reduceAny(datas, (acc, a) => acc + toLocaleString(a, this.locale), "");
86759
86786
  },
86760
86787
  isExported: true
86761
86788
  };
@@ -86763,7 +86790,7 @@ const EXACT = {
86763
86790
  description: _t("Tests whether two strings are identical."),
86764
86791
  args: [arg("string1 (string)", _t("The first string to compare.")), arg("string2 (string)", _t("The second string to compare."))],
86765
86792
  compute: function(string1, string2) {
86766
- return toString(string1) === toString(string2);
86793
+ return toLocaleString(string1, this.locale) === toLocaleString(string2, this.locale);
86767
86794
  },
86768
86795
  isExported: true
86769
86796
  };
@@ -86775,8 +86802,8 @@ const FIND = {
86775
86802
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
86776
86803
  ],
86777
86804
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
86778
- const _searchFor = toString(searchFor);
86779
- const _textToSearch = toString(textToSearch);
86805
+ const _searchFor = toLocaleString(searchFor, this.locale);
86806
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
86780
86807
  const _startingAt = toNumber(startingAt, this.locale);
86781
86808
  if (_textToSearch === "") return new EvaluationError(_t("The text_to_search must be non-empty."));
86782
86809
  if (_startingAt < 1) return new EvaluationError(_t("The starting_at (%s) must be greater than or equal to 1.", _startingAt));
@@ -86813,8 +86840,8 @@ const JOIN = {
86813
86840
  description: _t("Concatenates elements of arrays with delimiter."),
86814
86841
  args: [arg("delimiter (string)", _t("The character or string to place between each concatenated value.")), arg("value_or_array (string, range<string>, repeating)", _t("Value to be appended using delimiter."))],
86815
86842
  compute: function(delimiter, ...valuesOrArrays) {
86816
- const _delimiter = toString(delimiter);
86817
- return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toString(a), "");
86843
+ const _delimiter = toLocaleString(delimiter, this.locale);
86844
+ return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toLocaleString(a, this.locale), "");
86818
86845
  }
86819
86846
  };
86820
86847
  const LEFT = {
@@ -86823,7 +86850,7 @@ const LEFT = {
86823
86850
  compute: function(text, ...args) {
86824
86851
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
86825
86852
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters));
86826
- return toString(text).substring(0, _numberOfCharacters);
86853
+ return toLocaleString(text, this.locale).substring(0, _numberOfCharacters);
86827
86854
  },
86828
86855
  isExported: true
86829
86856
  };
@@ -86831,7 +86858,7 @@ const LEN = {
86831
86858
  description: _t("Length of a string."),
86832
86859
  args: [arg("text (string)", _t("The string whose length will be returned."))],
86833
86860
  compute: function(text) {
86834
- return toString(text).length;
86861
+ return toLocaleString(text, this.locale).length;
86835
86862
  },
86836
86863
  isExported: true
86837
86864
  };
@@ -86839,7 +86866,7 @@ const LOWER = {
86839
86866
  description: _t("Converts a specified string to lowercase."),
86840
86867
  args: [arg("text (string)", _t("The string to convert to lowercase."))],
86841
86868
  compute: function(text) {
86842
- return toString(text).toLowerCase();
86869
+ return toLocaleString(text, this.locale).toLowerCase();
86843
86870
  },
86844
86871
  isExported: true
86845
86872
  };
@@ -86851,7 +86878,7 @@ const MID = {
86851
86878
  arg("extract_length (number)", _t("The length of the segment to extract."))
86852
86879
  ],
86853
86880
  compute: function(text, starting_at, extract_length) {
86854
- const _text = toString(text);
86881
+ const _text = toLocaleString(text, this.locale);
86855
86882
  const _starting_at = toNumber(starting_at, this.locale);
86856
86883
  const _extract_length = toNumber(extract_length, this.locale);
86857
86884
  if (_starting_at < 1) return new EvaluationError(_t("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString()));
@@ -86864,7 +86891,7 @@ const PROPER = {
86864
86891
  description: _t("Capitalizes each word in a specified string."),
86865
86892
  args: [arg("text_to_capitalize (string)", _t("The text which will be returned with the first letter of each word in uppercase and all other letters in lowercase."))],
86866
86893
  compute: function(text) {
86867
- return toString(text).replace(wordRegex, (word) => {
86894
+ return toLocaleString(text, this.locale).replace(wordRegex, (word) => {
86868
86895
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
86869
86896
  });
86870
86897
  },
@@ -87014,8 +87041,8 @@ const REPLACE = {
87014
87041
  if (_position < 1) return new EvaluationError(_t("The position (%s) must be greater than or equal to 1.", _position));
87015
87042
  const _length = toNumber(length, this.locale);
87016
87043
  if (_length < 0) return new EvaluationError(_t("The length (%s) must be positive or zero.", _length));
87017
- const _text = toString(text);
87018
- const _newText = toString(newText);
87044
+ const _text = toLocaleString(text, this.locale);
87045
+ const _newText = toLocaleString(newText, this.locale);
87019
87046
  return _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length);
87020
87047
  },
87021
87048
  isExported: true
@@ -87026,7 +87053,7 @@ const RIGHT = {
87026
87053
  compute: function(text, ...args) {
87027
87054
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
87028
87055
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or zero.", _numberOfCharacters));
87029
- const _text = toString(text);
87056
+ const _text = toLocaleString(text, this.locale);
87030
87057
  const stringLength = _text.length;
87031
87058
  return _text.substring(stringLength - _numberOfCharacters, stringLength);
87032
87059
  },
@@ -87040,8 +87067,8 @@ const SEARCH = {
87040
87067
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
87041
87068
  ],
87042
87069
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
87043
- const _searchFor = toString(searchFor).toLowerCase();
87044
- const _textToSearch = toString(textToSearch).toLowerCase();
87070
+ const _searchFor = toLocaleString(searchFor, this.locale).toLowerCase();
87071
+ const _textToSearch = toLocaleString(textToSearch, this.locale).toLowerCase();
87045
87072
  const _startingAt = toNumber(startingAt, this.locale);
87046
87073
  if (_textToSearch === "") return {
87047
87074
  value: CellErrorType.GenericError,
@@ -87071,8 +87098,8 @@ const SPLIT = {
87071
87098
  arg(`remove_empty_text (boolean, default=${SPLIT_DEFAULT_REMOVE_EMPTY_TEXT})`, _t("Whether or not to remove empty text messages from the split results. The default behavior is to treat consecutive delimiters as one (if TRUE). If FALSE, empty cells values are added between consecutive delimiters."))
87072
87099
  ],
87073
87100
  compute: function(text, delimiter, splitByEach = { value: SPLIT_DEFAULT_SPLIT_BY_EACH }, removeEmptyText = { value: SPLIT_DEFAULT_REMOVE_EMPTY_TEXT }) {
87074
- const _text = toString(text);
87075
- const _delimiter = escapeRegExp(toString(delimiter));
87101
+ const _text = toLocaleString(text, this.locale);
87102
+ const _delimiter = escapeRegExp(toLocaleString(delimiter, this.locale));
87076
87103
  const _splitByEach = toBoolean(splitByEach);
87077
87104
  const _removeEmptyText = toBoolean(removeEmptyText);
87078
87105
  if (_delimiter.length <= 0) return new EvaluationError(_t("The delimiter (%s) must be not be empty.", _delimiter));
@@ -87094,10 +87121,10 @@ const SUBSTITUTE = {
87094
87121
  compute: function(textToSearch, searchFor, replaceWith, occurrenceNumber) {
87095
87122
  const _occurrenceNumber = toNumber(occurrenceNumber, this.locale);
87096
87123
  if (_occurrenceNumber < 0) return new EvaluationError(_t("The occurrenceNumber (%s) must be positive or null.", _occurrenceNumber));
87097
- const _textToSearch = toString(textToSearch);
87098
- const _searchFor = toString(searchFor);
87124
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
87125
+ const _searchFor = toLocaleString(searchFor, this.locale);
87099
87126
  if (_searchFor === "") return _textToSearch;
87100
- const _replaceWith = toString(replaceWith);
87127
+ const _replaceWith = toLocaleString(replaceWith, this.locale);
87101
87128
  const reg = new RegExp(escapeRegExp(_searchFor), "g");
87102
87129
  if (_occurrenceNumber === 0) return _textToSearch.replace(reg, _replaceWith);
87103
87130
  let n = 0;
@@ -87120,10 +87147,10 @@ const TEXTJOIN = {
87120
87147
  arg("texts (string, range<string>, repeating)", _t("Text item to join."))
87121
87148
  ],
87122
87149
  compute: function(delimiter, ignoreEmpty = { value: TEXTJOIN_DEFAULT_IGNORE_EMPTY }, ...textsOrArrays) {
87123
- const _delimiter = toString(delimiter);
87150
+ const _delimiter = toLocaleString(delimiter, this.locale);
87124
87151
  const _ignoreEmpty = toBoolean(ignoreEmpty);
87125
87152
  let n = 0;
87126
- return reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toString(a) === "") ? (n++ ? acc + _delimiter : "") + toString(a) : acc, "");
87153
+ return reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toLocaleString(a, this.locale) === "") ? (n++ ? acc + _delimiter : "") + toLocaleString(a, this.locale) : acc, "");
87127
87154
  },
87128
87155
  isExported: true
87129
87156
  };
@@ -87176,7 +87203,7 @@ const TRIM = {
87176
87203
  description: _t("Removes space characters."),
87177
87204
  args: [arg("text (string)", _t("The text or reference to a cell containing text to be trimmed."))],
87178
87205
  compute: function(text) {
87179
- return trimContent(toString(text));
87206
+ return trimContent(toLocaleString(text, this.locale));
87180
87207
  },
87181
87208
  isExported: true
87182
87209
  };
@@ -87184,7 +87211,7 @@ const UPPER = {
87184
87211
  description: _t("Converts a specified string to uppercase."),
87185
87212
  args: [arg("text (string)", _t("The string to convert to uppercase."))],
87186
87213
  compute: function(text) {
87187
- return toString(text).toUpperCase();
87214
+ return toLocaleString(text, this.locale).toUpperCase();
87188
87215
  },
87189
87216
  isExported: true
87190
87217
  };
@@ -87281,7 +87308,7 @@ const HYPERLINK = {
87281
87308
  args: [arg("url (string)", _t("The full URL of the link enclosed in quotation marks.")), arg("link_label (string, optional)", _t("The text to display in the cell, enclosed in quotation marks."))],
87282
87309
  compute: function(url, linkLabel) {
87283
87310
  const processedUrl = toString(url).trim();
87284
- const processedLabel = toString(linkLabel) || processedUrl;
87311
+ const processedLabel = toLocaleString(linkLabel, this.locale) || processedUrl;
87285
87312
  if (processedUrl === "") return processedLabel;
87286
87313
  return markdownLink(processedLabel, processedUrl);
87287
87314
  },
@@ -87427,7 +87454,8 @@ const registries = {
87427
87454
  migrationStepRegistry,
87428
87455
  chartJsExtensionRegistry,
87429
87456
  onIterationEndEvaluationRegistry,
87430
- specificRangeTransformRegistry
87457
+ specificRangeTransformRegistry,
87458
+ NonSquishableFunctionRegistry
87431
87459
  };
87432
87460
  const helpers = {
87433
87461
  arg,
@@ -87635,6 +87663,6 @@ const chartHelpers = {
87635
87663
  //#endregion
87636
87664
  export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, BadExpressionError, CHART_TYPES, CellErrorType, CellValueType, CircularDependencyError, ClientDisconnectedError, ClipboardMIMEType, CommandResult, CompiledFormula, CorePlugin, CoreViewPlugin, DEFAULT_LOCALE, DEFAULT_LOCALES, DEFAULT_LOCALE_DIGIT_GROUPING, DIRECTION, DispatchResult, DivisionByZeroError, EvaluationError, InvalidReferenceError, LocalTransportService, Model, NEXT_VALUE, NotAvailableError, NumberTooLargeError, OrderedLayers, PREVIOUS_VALUE, PivotRuntimeDefinition, Registry, Revision, SPREADSHEET_DIMENSIONS, SplillBlockedError, Spreadsheet, SpreadsheetPivotTable, UIPlugin, UnknownFunctionError, __info__, addFunction, addRenderingLayer, astToFormula, availableConditionalFormatOperators, availableDataValidationOperators, availableFiltersOperators, borderPositions, borderStyles, canExecuteInReadonly, categories, chartHelpers, compatibility, components, composerFocusTypes, constants, convertAstNodes, coreTypes, createAutocompleteArgumentsProvider, errorTypes, filterDateCriterionOperators, filterNumberCriterionOperators, filterTextCriterionOperators, findCellInNewZone, functionCache, getCaretDownSvg, getCaretUpSvg, helpers, hooks, invalidSubtotalFormulasCommands, invalidateBordersCommands, invalidateCFEvaluationCommands, invalidateChartEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, isCoreCommand, isHeadersDependant, isMatrix, isPositionDependent, isRangeDependant, isSheetDependent, isTargetDependent, isZoneDependent, iterateAstNodes, links, load, lockedSheetAllowedCommands, parse, parseTokens, readonlyAllowedCommands, registries, schemeToColorScale, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize };
87637
87665
 
87638
- __info__.version = "19.4.6";
87639
- __info__.date = "2026-08-10T12:33:32.728Z";
87640
- __info__.hash = "5c667fe";
87666
+ __info__.version = "19.4.8";
87667
+ __info__.date = "2026-08-19T09:25:13.754Z";
87668
+ __info__.hash = "9dd828e";