@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
  (function(exports, _odoo_owl) {
@@ -4027,6 +4027,18 @@ set(value) {
4027
4027
  default: return "";
4028
4028
  }
4029
4029
  }
4030
+ /**
4031
+ * Only converts the decimal separator, ignore number format such as % or dates
4032
+ */
4033
+ function toLocaleString(data, locale) {
4034
+ const value = toValue(data);
4035
+ switch (typeof value) {
4036
+ case "string": return value;
4037
+ case "number": return value.toString().replace(".", locale.decimalSeparator);
4038
+ case "boolean": return value ? "TRUE" : "FALSE";
4039
+ default: return "";
4040
+ }
4041
+ }
4030
4042
  /** Normalize string by setting it to lowercase and replacing accent letters with plain letters */
4031
4043
  const normalizeString = memoize(function normalizeString(str) {
4032
4044
  return str.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
@@ -5959,6 +5971,9 @@ set(value) {
5959
5971
  }
5960
5972
  };
5961
5973
  const functionRegistry = new FunctionRegistry();
5974
+ /** Formulas using one of these functions are never squished: they are always exported
5975
+ * as a full formula string, and they don't become the base formula of the next cells. */
5976
+ const NonSquishableFunctionRegistry = new Registry();
5962
5977
 
5963
5978
  //#endregion
5964
5979
  //#region src/helpers/references.ts
@@ -15477,18 +15492,21 @@ set(value) {
15477
15492
  figureRegistry.add("chart", {
15478
15493
  Component: ChartFigure,
15479
15494
  SidePanelComponent: "ChartPanel",
15480
- menuBuilder: getChartMenuActions
15495
+ menuBuilder: getChartMenuActions,
15496
+ isThemeDependant: true
15481
15497
  });
15482
15498
  figureRegistry.add("image", {
15483
15499
  Component: ImageFigure,
15484
15500
  keepRatio: true,
15485
15501
  minFigSize: 20,
15486
15502
  borderWidth: 0,
15487
- menuBuilder: getImageMenuActions
15503
+ menuBuilder: getImageMenuActions,
15504
+ isThemeDependant: false
15488
15505
  });
15489
15506
  figureRegistry.add("carousel", {
15490
15507
  Component: CarouselFigure,
15491
- menuBuilder: getCarouselMenuActions
15508
+ menuBuilder: getCarouselMenuActions,
15509
+ isThemeDependant: true
15492
15510
  });
15493
15511
 
15494
15512
  //#endregion
@@ -15517,6 +15535,9 @@ set(value) {
15517
15535
  get isSelected() {
15518
15536
  return this.env.model.getters.getSelectedFigureIds().includes(this.props.figureUI.id);
15519
15537
  }
15538
+ get isThemeDependant() {
15539
+ return figureRegistry.get(this.props.figureUI.tag).isThemeDependant;
15540
+ }
15520
15541
  get figureRegistry() {
15521
15542
  return figureRegistry;
15522
15543
  }
@@ -51151,7 +51172,8 @@ set(value) {
51151
51172
  * The result of this method is:
51152
51173
  * - if the cell is not a formula, returns the content as is and resets the base formula
51153
51174
  * - if the cell is a formula:
51154
- * - 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
51175
+ * - if there is no previous formula, or the normalized formula is different from the previous one or it contains blacklisted functions (see NonSquishableFunctionRegistry),
51176
+ * resets the base formula to this one and returns the full formula string
51155
51177
  * - else, compares the literal values and range dependencies to the previous formula, and for each parameter:
51156
51178
  * - for numbers: returns a relative change (+N or -N) if possible, else the full number or "=" if unchanged
51157
51179
  * - for strings: returns the full string if changed, else "="
@@ -51159,6 +51181,10 @@ set(value) {
51159
51181
  * */
51160
51182
  squish(cell, forSheetId) {
51161
51183
  if (cell.isFormula) {
51184
+ if (NonSquishableFunctionRegistry.getAll().some((functionName) => cell.compiledFormula.usesSymbol(functionName))) {
51185
+ this.resetBaseFormula();
51186
+ return cell.compiledFormula.toFormulaString(this.getters, this.rangeToStringOptions);
51187
+ }
51162
51188
  let numbers = [];
51163
51189
  let strings = [];
51164
51190
  let references = [];
@@ -62419,7 +62445,7 @@ set(value) {
62419
62445
  }
62420
62446
  message = {
62421
62447
  ...message,
62422
- commands: this.commandSquisher.squish(revision.commands)
62448
+ commands: revision.commands
62423
62449
  };
62424
62450
  }
62425
62451
  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.
@@ -77835,12 +77861,13 @@ set(value) {
77835
77861
  }
77836
77862
  sortFilterZone(sortDirection) {
77837
77863
  const filterPosition = this.props.filterPosition;
77838
- const tableZone = this.table?.range.zone;
77864
+ const table = this.table;
77865
+ const tableZone = table?.range.zone;
77839
77866
  if (!filterPosition || !tableZone || tableZone.top === tableZone.bottom) return;
77840
77867
  const sheetId = this.env.model.getters.getActiveSheetId();
77841
77868
  const contentZone = {
77842
77869
  ...tableZone,
77843
- top: tableZone.top + 1
77870
+ top: tableZone.top + table.config.numberOfHeaders
77844
77871
  };
77845
77872
  const sortAnchor = {
77846
77873
  col: filterPosition.col,
@@ -80097,7 +80124,7 @@ set(value) {
80097
80124
  else if (_format === 0) {
80098
80125
  const rowSeparator = this.locale.decimalSeparator === "," ? "/" : ",";
80099
80126
  return transposeMatrix(_array).flatMap((row) => row.map((value) => {
80100
- return isEvaluationError(value.value) ? value.value : toString(value);
80127
+ return isEvaluationError(value.value) ? value.value : toLocaleString(value, this.locale);
80101
80128
  })).join(rowSeparator);
80102
80129
  } else return new EvaluationError(_t("Format must be 0 or 1"));
80103
80130
  },
@@ -85863,7 +85890,7 @@ set(value) {
85863
85890
  description: _t("Concatenation of two values."),
85864
85891
  args: [arg("value1 (string)", _t("The value to which value2 will be appended.")), arg("value2 (string)", _t("The value to append to value1."))],
85865
85892
  compute: function(value1, value2) {
85866
- return toString(value1) + toString(value2);
85893
+ return toLocaleString(value1, this.locale) + toLocaleString(value2, this.locale);
85867
85894
  },
85868
85895
  isExported: true
85869
85896
  };
@@ -86746,7 +86773,7 @@ set(value) {
86746
86773
  description: _t("Remove non-printable characters from a piece of text."),
86747
86774
  args: [arg("text (string)", _t("The text whose non-printable characters are to be removed."))],
86748
86775
  compute: function(text) {
86749
- const _text = toString(text);
86776
+ const _text = toLocaleString(text, this.locale);
86750
86777
  let cleanedStr = "";
86751
86778
  for (const char of _text) if (char && char.charCodeAt(0) > 31) cleanedStr += char;
86752
86779
  return cleanedStr;
@@ -86757,7 +86784,7 @@ set(value) {
86757
86784
  description: _t("Appends strings to one another."),
86758
86785
  args: [arg("string (string, range<string>, repeating)", _t("String to append in sequence."))],
86759
86786
  compute: function(...datas) {
86760
- return reduceAny(datas, (acc, a) => acc + toString(a), "");
86787
+ return reduceAny(datas, (acc, a) => acc + toLocaleString(a, this.locale), "");
86761
86788
  },
86762
86789
  isExported: true
86763
86790
  };
@@ -86765,7 +86792,7 @@ set(value) {
86765
86792
  description: _t("Tests whether two strings are identical."),
86766
86793
  args: [arg("string1 (string)", _t("The first string to compare.")), arg("string2 (string)", _t("The second string to compare."))],
86767
86794
  compute: function(string1, string2) {
86768
- return toString(string1) === toString(string2);
86795
+ return toLocaleString(string1, this.locale) === toLocaleString(string2, this.locale);
86769
86796
  },
86770
86797
  isExported: true
86771
86798
  };
@@ -86777,8 +86804,8 @@ set(value) {
86777
86804
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
86778
86805
  ],
86779
86806
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
86780
- const _searchFor = toString(searchFor);
86781
- const _textToSearch = toString(textToSearch);
86807
+ const _searchFor = toLocaleString(searchFor, this.locale);
86808
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
86782
86809
  const _startingAt = toNumber(startingAt, this.locale);
86783
86810
  if (_textToSearch === "") return new EvaluationError(_t("The text_to_search must be non-empty."));
86784
86811
  if (_startingAt < 1) return new EvaluationError(_t("The starting_at (%s) must be greater than or equal to 1.", _startingAt));
@@ -86815,8 +86842,8 @@ set(value) {
86815
86842
  description: _t("Concatenates elements of arrays with delimiter."),
86816
86843
  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."))],
86817
86844
  compute: function(delimiter, ...valuesOrArrays) {
86818
- const _delimiter = toString(delimiter);
86819
- return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toString(a), "");
86845
+ const _delimiter = toLocaleString(delimiter, this.locale);
86846
+ return reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toLocaleString(a, this.locale), "");
86820
86847
  }
86821
86848
  };
86822
86849
  const LEFT = {
@@ -86825,7 +86852,7 @@ set(value) {
86825
86852
  compute: function(text, ...args) {
86826
86853
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
86827
86854
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters));
86828
- return toString(text).substring(0, _numberOfCharacters);
86855
+ return toLocaleString(text, this.locale).substring(0, _numberOfCharacters);
86829
86856
  },
86830
86857
  isExported: true
86831
86858
  };
@@ -86833,7 +86860,7 @@ set(value) {
86833
86860
  description: _t("Length of a string."),
86834
86861
  args: [arg("text (string)", _t("The string whose length will be returned."))],
86835
86862
  compute: function(text) {
86836
- return toString(text).length;
86863
+ return toLocaleString(text, this.locale).length;
86837
86864
  },
86838
86865
  isExported: true
86839
86866
  };
@@ -86841,7 +86868,7 @@ set(value) {
86841
86868
  description: _t("Converts a specified string to lowercase."),
86842
86869
  args: [arg("text (string)", _t("The string to convert to lowercase."))],
86843
86870
  compute: function(text) {
86844
- return toString(text).toLowerCase();
86871
+ return toLocaleString(text, this.locale).toLowerCase();
86845
86872
  },
86846
86873
  isExported: true
86847
86874
  };
@@ -86853,7 +86880,7 @@ set(value) {
86853
86880
  arg("extract_length (number)", _t("The length of the segment to extract."))
86854
86881
  ],
86855
86882
  compute: function(text, starting_at, extract_length) {
86856
- const _text = toString(text);
86883
+ const _text = toLocaleString(text, this.locale);
86857
86884
  const _starting_at = toNumber(starting_at, this.locale);
86858
86885
  const _extract_length = toNumber(extract_length, this.locale);
86859
86886
  if (_starting_at < 1) return new EvaluationError(_t("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString()));
@@ -86866,7 +86893,7 @@ set(value) {
86866
86893
  description: _t("Capitalizes each word in a specified string."),
86867
86894
  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."))],
86868
86895
  compute: function(text) {
86869
- return toString(text).replace(wordRegex, (word) => {
86896
+ return toLocaleString(text, this.locale).replace(wordRegex, (word) => {
86870
86897
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
86871
86898
  });
86872
86899
  },
@@ -87016,8 +87043,8 @@ set(value) {
87016
87043
  if (_position < 1) return new EvaluationError(_t("The position (%s) must be greater than or equal to 1.", _position));
87017
87044
  const _length = toNumber(length, this.locale);
87018
87045
  if (_length < 0) return new EvaluationError(_t("The length (%s) must be positive or zero.", _length));
87019
- const _text = toString(text);
87020
- const _newText = toString(newText);
87046
+ const _text = toLocaleString(text, this.locale);
87047
+ const _newText = toLocaleString(newText, this.locale);
87021
87048
  return _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length);
87022
87049
  },
87023
87050
  isExported: true
@@ -87028,7 +87055,7 @@ set(value) {
87028
87055
  compute: function(text, ...args) {
87029
87056
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
87030
87057
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or zero.", _numberOfCharacters));
87031
- const _text = toString(text);
87058
+ const _text = toLocaleString(text, this.locale);
87032
87059
  const stringLength = _text.length;
87033
87060
  return _text.substring(stringLength - _numberOfCharacters, stringLength);
87034
87061
  },
@@ -87042,8 +87069,8 @@ set(value) {
87042
87069
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
87043
87070
  ],
87044
87071
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
87045
- const _searchFor = toString(searchFor).toLowerCase();
87046
- const _textToSearch = toString(textToSearch).toLowerCase();
87072
+ const _searchFor = toLocaleString(searchFor, this.locale).toLowerCase();
87073
+ const _textToSearch = toLocaleString(textToSearch, this.locale).toLowerCase();
87047
87074
  const _startingAt = toNumber(startingAt, this.locale);
87048
87075
  if (_textToSearch === "") return {
87049
87076
  value: CellErrorType.GenericError,
@@ -87073,8 +87100,8 @@ set(value) {
87073
87100
  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."))
87074
87101
  ],
87075
87102
  compute: function(text, delimiter, splitByEach = { value: SPLIT_DEFAULT_SPLIT_BY_EACH }, removeEmptyText = { value: SPLIT_DEFAULT_REMOVE_EMPTY_TEXT }) {
87076
- const _text = toString(text);
87077
- const _delimiter = escapeRegExp(toString(delimiter));
87103
+ const _text = toLocaleString(text, this.locale);
87104
+ const _delimiter = escapeRegExp(toLocaleString(delimiter, this.locale));
87078
87105
  const _splitByEach = toBoolean(splitByEach);
87079
87106
  const _removeEmptyText = toBoolean(removeEmptyText);
87080
87107
  if (_delimiter.length <= 0) return new EvaluationError(_t("The delimiter (%s) must be not be empty.", _delimiter));
@@ -87096,10 +87123,10 @@ set(value) {
87096
87123
  compute: function(textToSearch, searchFor, replaceWith, occurrenceNumber) {
87097
87124
  const _occurrenceNumber = toNumber(occurrenceNumber, this.locale);
87098
87125
  if (_occurrenceNumber < 0) return new EvaluationError(_t("The occurrenceNumber (%s) must be positive or null.", _occurrenceNumber));
87099
- const _textToSearch = toString(textToSearch);
87100
- const _searchFor = toString(searchFor);
87126
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
87127
+ const _searchFor = toLocaleString(searchFor, this.locale);
87101
87128
  if (_searchFor === "") return _textToSearch;
87102
- const _replaceWith = toString(replaceWith);
87129
+ const _replaceWith = toLocaleString(replaceWith, this.locale);
87103
87130
  const reg = new RegExp(escapeRegExp(_searchFor), "g");
87104
87131
  if (_occurrenceNumber === 0) return _textToSearch.replace(reg, _replaceWith);
87105
87132
  let n = 0;
@@ -87122,10 +87149,10 @@ set(value) {
87122
87149
  arg("texts (string, range<string>, repeating)", _t("Text item to join."))
87123
87150
  ],
87124
87151
  compute: function(delimiter, ignoreEmpty = { value: TEXTJOIN_DEFAULT_IGNORE_EMPTY }, ...textsOrArrays) {
87125
- const _delimiter = toString(delimiter);
87152
+ const _delimiter = toLocaleString(delimiter, this.locale);
87126
87153
  const _ignoreEmpty = toBoolean(ignoreEmpty);
87127
87154
  let n = 0;
87128
- return reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toString(a) === "") ? (n++ ? acc + _delimiter : "") + toString(a) : acc, "");
87155
+ return reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toLocaleString(a, this.locale) === "") ? (n++ ? acc + _delimiter : "") + toLocaleString(a, this.locale) : acc, "");
87129
87156
  },
87130
87157
  isExported: true
87131
87158
  };
@@ -87178,7 +87205,7 @@ set(value) {
87178
87205
  description: _t("Removes space characters."),
87179
87206
  args: [arg("text (string)", _t("The text or reference to a cell containing text to be trimmed."))],
87180
87207
  compute: function(text) {
87181
- return trimContent(toString(text));
87208
+ return trimContent(toLocaleString(text, this.locale));
87182
87209
  },
87183
87210
  isExported: true
87184
87211
  };
@@ -87186,7 +87213,7 @@ set(value) {
87186
87213
  description: _t("Converts a specified string to uppercase."),
87187
87214
  args: [arg("text (string)", _t("The string to convert to uppercase."))],
87188
87215
  compute: function(text) {
87189
- return toString(text).toUpperCase();
87216
+ return toLocaleString(text, this.locale).toUpperCase();
87190
87217
  },
87191
87218
  isExported: true
87192
87219
  };
@@ -87283,7 +87310,7 @@ set(value) {
87283
87310
  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."))],
87284
87311
  compute: function(url, linkLabel) {
87285
87312
  const processedUrl = toString(url).trim();
87286
- const processedLabel = toString(linkLabel) || processedUrl;
87313
+ const processedLabel = toLocaleString(linkLabel, this.locale) || processedUrl;
87287
87314
  if (processedUrl === "") return processedLabel;
87288
87315
  return markdownLink(processedLabel, processedUrl);
87289
87316
  },
@@ -87429,7 +87456,8 @@ set(value) {
87429
87456
  migrationStepRegistry,
87430
87457
  chartJsExtensionRegistry,
87431
87458
  onIterationEndEvaluationRegistry,
87432
- specificRangeTransformRegistry
87459
+ specificRangeTransformRegistry,
87460
+ NonSquishableFunctionRegistry
87433
87461
  };
87434
87462
  const helpers = {
87435
87463
  arg,
@@ -87731,8 +87759,8 @@ exports.stores = stores;
87731
87759
  exports.tokenColors = tokenColors;
87732
87760
  exports.tokenize = tokenize;
87733
87761
 
87734
- __info__.version = "19.4.6";
87735
- __info__.date = "2026-08-10T12:33:32.728Z";
87736
- __info__.hash = "5c667fe";
87762
+ __info__.version = "19.4.8";
87763
+ __info__.date = "2026-08-19T09:25:13.754Z";
87764
+ __info__.hash = "9dd828e";
87737
87765
 
87738
87766
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);