odoo-addon-mis-builder 17.0.1.3.0__py3-none-any.whl → 18.0.1.1.0__py3-none-any.whl

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.
Files changed (55) hide show
  1. odoo/addons/mis_builder/README.rst +7 -6
  2. odoo/addons/mis_builder/__manifest__.py +1 -1
  3. odoo/addons/mis_builder/datas/ir_cron.xml +1 -3
  4. odoo/addons/mis_builder/i18n/ca.po +8 -51
  5. odoo/addons/mis_builder/i18n/de.po +4 -39
  6. odoo/addons/mis_builder/i18n/el.po +4 -39
  7. odoo/addons/mis_builder/i18n/el_GR.po +4 -39
  8. odoo/addons/mis_builder/i18n/es.po +12 -65
  9. odoo/addons/mis_builder/i18n/fr.po +12 -65
  10. odoo/addons/mis_builder/i18n/hr.po +4 -39
  11. odoo/addons/mis_builder/i18n/it.po +13 -80
  12. odoo/addons/mis_builder/i18n/mis_builder.pot +18 -125
  13. odoo/addons/mis_builder/i18n/nl.po +4 -39
  14. odoo/addons/mis_builder/i18n/nl_NL.po +4 -39
  15. odoo/addons/mis_builder/i18n/pt.po +4 -39
  16. odoo/addons/mis_builder/i18n/pt_BR.po +13 -64
  17. odoo/addons/mis_builder/i18n/sv.po +12 -64
  18. odoo/addons/mis_builder/i18n/tr.po +4 -39
  19. odoo/addons/mis_builder/i18n/zh_CN.po +78 -154
  20. odoo/addons/mis_builder/models/aep.py +62 -160
  21. odoo/addons/mis_builder/models/aggregate.py +4 -4
  22. odoo/addons/mis_builder/models/kpimatrix.py +9 -10
  23. odoo/addons/mis_builder/models/mis_kpi_data.py +5 -7
  24. odoo/addons/mis_builder/models/mis_report.py +47 -58
  25. odoo/addons/mis_builder/models/mis_report_instance.py +43 -24
  26. odoo/addons/mis_builder/models/mis_report_style.py +9 -12
  27. odoo/addons/mis_builder/models/mis_report_subreport.py +5 -4
  28. odoo/addons/mis_builder/models/prorata_read_group_mixin.py +51 -31
  29. odoo/addons/mis_builder/models/simple_array.py +2 -2
  30. odoo/addons/mis_builder/readme/CONTRIBUTORS.md +1 -0
  31. odoo/addons/mis_builder/report/mis_report_instance_xlsx.py +2 -2
  32. odoo/addons/mis_builder/static/description/index.html +5 -4
  33. odoo/addons/mis_builder/static/src/components/mis_report_widget.esm.js +38 -22
  34. odoo/addons/mis_builder/static/src/components/mis_report_widget.scss +9 -0
  35. odoo/addons/mis_builder/static/src/components/mis_report_widget.xml +16 -14
  36. odoo/addons/mis_builder/tests/__init__.py +1 -0
  37. odoo/addons/mis_builder/tests/common.py +2 -4
  38. odoo/addons/mis_builder/tests/fake_models.py +18 -1
  39. odoo/addons/mis_builder/tests/test_aep.py +7 -69
  40. odoo/addons/mis_builder/tests/test_data_sources.py +4 -11
  41. odoo/addons/mis_builder/tests/test_kpi_data.py +1 -5
  42. odoo/addons/mis_builder/tests/test_mis_report_instance.py +21 -17
  43. odoo/addons/mis_builder/tests/test_multi_company_aep.py +3 -3
  44. odoo/addons/mis_builder/tests/test_pro_rata_read_group.py +105 -0
  45. odoo/addons/mis_builder/views/mis_report.xml +38 -43
  46. odoo/addons/mis_builder/views/mis_report_instance.xml +38 -40
  47. odoo/addons/mis_builder/views/mis_report_style.xml +6 -6
  48. odoo/addons/mis_builder/wizard/mis_builder_dashboard.py +3 -3
  49. odoo/addons/mis_builder/wizard/mis_builder_dashboard.xml +6 -6
  50. {odoo_addon_mis_builder-17.0.1.3.0.dist-info → odoo_addon_mis_builder-18.0.1.1.0.dist-info}/METADATA +12 -11
  51. odoo_addon_mis_builder-18.0.1.1.0.dist-info/RECORD +87 -0
  52. odoo/addons/mis_builder/static/src/components/mis_report_widget.css +0 -67
  53. odoo_addon_mis_builder-17.0.1.3.0.dist-info/RECORD +0 -87
  54. {odoo_addon_mis_builder-17.0.1.3.0.dist-info → odoo_addon_mis_builder-18.0.1.1.0.dist-info}/WHEEL +0 -0
  55. {odoo_addon_mis_builder-17.0.1.3.0.dist-info → odoo_addon_mis_builder-18.0.1.1.0.dist-info}/top_level.txt +0 -0
@@ -5,13 +5,14 @@ import logging
5
5
  import re
6
6
  from collections import defaultdict
7
7
 
8
- from odoo import _, fields
8
+ from odoo import fields
9
9
  from odoo.exceptions import UserError
10
10
  from odoo.models import expression
11
11
  from odoo.tools.float_utils import float_is_zero
12
12
  from odoo.tools.safe_eval import datetime, dateutil, safe_eval, time
13
13
 
14
14
  from .accounting_none import AccountingNone
15
+ from .simple_array import SimpleArray
15
16
 
16
17
  _logger = logging.getLogger(__name__)
17
18
 
@@ -23,72 +24,15 @@ def _is_domain(s):
23
24
  return _DOMAIN_START_RE.match(s)
24
25
 
25
26
 
26
- class Accumulator:
27
- """A simple class to accumulate debit, credit and custom field values.
28
-
29
- >>> acc1 = Accumulator(["f1", "f2"])
30
- >>> acc1.debit
31
- AccountingNone
32
- >>> acc1.credit
33
- AccountingNone
34
- >>> acc1.custom_fields
35
- {'f1': AccountingNone, 'f2': AccountingNone}
36
- >>> acc1.add_debit_credit(10, 20)
37
- >>> acc1.debit, acc1.credit
38
- (10, 20)
39
- >>> acc1.add_custom_field("f1", 10)
40
- >>> acc1.custom_fields
41
- {'f1': 10, 'f2': AccountingNone}
42
- >>> acc2 = Accumulator(["f1", "f2"])
43
- >>> acc2.add_debit_credit(21, 31)
44
- >>> acc2.add_custom_field("f2", 41)
45
- >>> acc1 += acc2
46
- >>> acc1.debit, acc1.credit
47
- (31, 51)
48
- >>> acc1.custom_fields
49
- {'f1': 10, 'f2': 41}
50
- """
51
-
52
- def __init__(self, custom_field_names=()):
53
- self.debit = AccountingNone
54
- self.credit = AccountingNone
55
- self.custom_fields = {
56
- custom_field: AccountingNone for custom_field in custom_field_names
57
- }
58
-
59
- def has_data(self):
60
- return (
61
- self.debit is not AccountingNone
62
- or self.credit is not AccountingNone
63
- or any(v is not AccountingNone for v in self.custom_fields.values())
64
- )
65
-
66
- def add_debit_credit(self, debit, credit):
67
- self.debit += debit
68
- self.credit += credit
69
-
70
- def add_custom_field(self, field, value):
71
- self.custom_fields[field] += value
72
-
73
- def __iadd__(self, other):
74
- self.debit += other.debit
75
- self.credit += other.credit
76
- for field in self.custom_fields:
77
- self.custom_fields[field] += other.custom_fields[field]
78
- return self
79
-
80
-
81
27
  class AccountingExpressionProcessor:
82
28
  """Processor for accounting expressions.
83
29
 
84
- Expressions of the form
85
- <field><mode>(.fieldname)?[accounts][optional move line domain]
30
+ Expressions of the form <field><mode>[accounts][optional move line domain]
86
31
  are supported, where:
87
32
  * field is bal, crd, deb, pbal (positive balances only),
88
- nbal (negative balance only), fld (custom field)
33
+ nbal (negative balance only)
89
34
  * mode is i (initial balance), e (ending balance),
90
35
  p (moves over period)
91
- * .fieldname is used only with fldp and specifies the field name to sum
92
36
  * there is also a special u mode (unallocated P&L) which computes
93
37
  the sum from the beginning until the beginning of the fiscal year
94
38
  of the period; it is only meaningful for P&L accounts
@@ -102,7 +46,6 @@ class AccountingExpressionProcessor:
102
46
  over the period (it is the same as balp[70]);
103
47
  * bali[70,60]: balance of accounts 70 and 60 at the start of period;
104
48
  * bale[1%]: balance of accounts starting with 1 at end of period.
105
- * fldp.quantity[60%]: sum of the quantity field of moves on accounts 60
106
49
 
107
50
  How to use:
108
51
  * repeatedly invoke parse_expr() for each expression containing
@@ -134,9 +77,8 @@ class AccountingExpressionProcessor:
134
77
  MODE_UNALLOCATED = "u"
135
78
 
136
79
  _ACC_RE = re.compile(
137
- r"(?P<field>\bbal|\bpbal|\bnbal|\bcrd|\bdeb|\bfld)"
80
+ r"(?P<field>\bbal|\bpbal|\bnbal|\bcrd|\bdeb)"
138
81
  r"(?P<mode>[piseu])?"
139
- r"(?P<fld_name>\.[a-zA-Z0-9_]+)?"
140
82
  r"\s*"
141
83
  r"(?P<account_sel>_[a-zA-Z0-9]+|\[.*?\])"
142
84
  r"\s*"
@@ -150,7 +92,7 @@ class AccountingExpressionProcessor:
150
92
  self.currency = companies.mapped("currency_id")
151
93
  if len(self.currency) > 1:
152
94
  raise UserError(
153
- _(
95
+ self.env._(
154
96
  "If currency_id is not provided, "
155
97
  "all companies must have the same currency."
156
98
  )
@@ -168,8 +110,6 @@ class AccountingExpressionProcessor:
168
110
  # a first query to get the initial balance and another
169
111
  # to get the variation, so it's a bit slower
170
112
  self.smart_end = True
171
- # custom field to query and sum
172
- self._custom_fields = set()
173
113
  # Account model
174
114
  self._account_model = self.env[account_model].with_context(active_test=False)
175
115
 
@@ -189,7 +129,7 @@ class AccountingExpressionProcessor:
189
129
  def _parse_match_object(self, mo):
190
130
  """Split a match object corresponding to an accounting variable
191
131
 
192
- Returns field, mode, fld_name, account domain, move line domain.
132
+ Returns field, mode, account domain, move line domain.
193
133
  """
194
134
  domain_eval_context = {
195
135
  "ref": self.env.ref,
@@ -198,16 +138,12 @@ class AccountingExpressionProcessor:
198
138
  "datetime": datetime,
199
139
  "dateutil": dateutil,
200
140
  }
201
- field, mode, fld_name, account_sel, ml_domain = mo.groups()
141
+ field, mode, account_sel, ml_domain = mo.groups()
202
142
  # handle some legacy modes
203
143
  if not mode:
204
144
  mode = self.MODE_VARIATION
205
145
  elif mode == "s":
206
146
  mode = self.MODE_END
207
- # custom fields
208
- if fld_name:
209
- assert fld_name[0] == "."
210
- fld_name = fld_name[1:] # strip leading dot
211
147
  # convert account selector to account domain
212
148
  if account_sel.startswith("_"):
213
149
  # legacy bal_NNN%
@@ -230,7 +166,7 @@ class AccountingExpressionProcessor:
230
166
  ml_domain = tuple(safe_eval(ml_domain, domain_eval_context))
231
167
  else:
232
168
  ml_domain = tuple()
233
- return field, mode, fld_name, acc_domain, ml_domain
169
+ return field, mode, acc_domain, ml_domain
234
170
 
235
171
  def parse_expr(self, expr):
236
172
  """Parse an expression, extracting accounting variables.
@@ -241,7 +177,7 @@ class AccountingExpressionProcessor:
241
177
  and mode.
242
178
  """
243
179
  for mo in self._ACC_RE.finditer(expr):
244
- field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
180
+ _, mode, acc_domain, ml_domain = self._parse_match_object(mo)
245
181
  if mode == self.MODE_END and self.smart_end:
246
182
  modes = (self.MODE_INITIAL, self.MODE_VARIATION, self.MODE_END)
247
183
  else:
@@ -249,40 +185,26 @@ class AccountingExpressionProcessor:
249
185
  for mode in modes:
250
186
  key = (ml_domain, mode)
251
187
  self._map_account_ids[key].add(acc_domain)
252
- if field == "fld":
253
- if mode != self.MODE_VARIATION:
254
- raise UserError(
255
- _(
256
- "`fld` can only be used with mode `p` (variation) "
257
- "in expression %s",
258
- expr,
259
- )
260
- )
261
- if not fld_name:
262
- raise UserError(
263
- _("`fld` must have a field name in exression %s", expr)
264
- )
265
- self._custom_fields.add(fld_name)
266
- else:
267
- if fld_name:
268
- raise UserError(
269
- _(
270
- "`%(field)s` cannot have a field name "
271
- "in expression %(expr)s",
272
- field=field,
273
- expr=expr,
274
- )
275
- )
276
188
 
277
189
  def done_parsing(self):
278
190
  """Replace account domains by account ids in map"""
279
191
  for key, acc_domains in self._map_account_ids.items():
280
192
  all_account_ids = set()
281
193
  for acc_domain in acc_domains:
282
- acc_domain_with_company = expression.AND(
283
- [acc_domain, [("company_id", "in", self.companies.ids)]]
284
- )
285
- account_ids = self._account_model.search(acc_domain_with_company).ids
194
+ # XXX It is apparently not possible to search accounts by code
195
+ # across multiple companies at once (due to how _search_code is
196
+ # implemented for instance), so we have to search each company
197
+ # separately.
198
+ account_ids = []
199
+ for company in self.companies:
200
+ acc_domain_with_company = expression.AND(
201
+ [acc_domain, [("company_ids", "=", company.id)]]
202
+ )
203
+ account_ids += (
204
+ self._account_model.with_company(company)
205
+ .search(acc_domain_with_company)
206
+ .ids
207
+ )
286
208
  self._account_ids_by_acc_domain[acc_domain].update(account_ids)
287
209
  all_account_ids.update(account_ids)
288
210
  self._map_account_ids[key] = list(all_account_ids)
@@ -299,7 +221,7 @@ class AccountingExpressionProcessor:
299
221
  """
300
222
  account_ids = set()
301
223
  for mo in self._ACC_RE.finditer(expr):
302
- _, _, _, acc_domain, _ = self._parse_match_object(mo)
224
+ field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
303
225
  account_ids.update(self._account_ids_by_acc_domain[acc_domain])
304
226
  return account_ids
305
227
 
@@ -313,7 +235,7 @@ class AccountingExpressionProcessor:
313
235
  aml_domains = []
314
236
  date_domain_by_mode = {}
315
237
  for mo in self._ACC_RE.finditer(expr):
316
- field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
238
+ field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
317
239
  aml_domain = list(ml_domain)
318
240
  account_ids = set()
319
241
  account_ids.update(self._account_ids_by_acc_domain[acc_domain])
@@ -329,8 +251,6 @@ class AccountingExpressionProcessor:
329
251
  aml_domain.append(("credit", "<>", 0.0))
330
252
  elif field == "deb":
331
253
  aml_domain.append(("debit", "<>", 0.0))
332
- elif fld_name:
333
- aml_domain.append((fld_name, "!=", False))
334
254
  aml_domains.append(expression.normalize_domain(aml_domain))
335
255
  if mode not in date_domain_by_mode:
336
256
  date_domain_by_mode[mode] = self.get_aml_domain_for_dates(
@@ -406,10 +326,10 @@ class AccountingExpressionProcessor:
406
326
  aml_model = self.env[aml_model]
407
327
  aml_model = aml_model.with_context(active_test=False)
408
328
  company_rates = self._get_company_rates(date_to)
409
- # {(domain, mode): {account_id: Accumulator}}
329
+ # {(domain, mode): {account_id: (debit, credit)}}
410
330
  self._data = defaultdict(
411
331
  lambda: defaultdict(
412
- lambda: Accumulator(self._custom_fields),
332
+ lambda: SimpleArray((AccountingNone, AccountingNone)),
413
333
  )
414
334
  )
415
335
  domain_by_mode = {}
@@ -431,49 +351,36 @@ class AccountingExpressionProcessor:
431
351
  # fetch sum of debit/credit, grouped by account_id
432
352
  _logger.debug("read_group domain: %s", domain)
433
353
  try:
434
- accs = aml_model.read_group(
354
+ accs = aml_model.with_context(
355
+ allowed_company_ids=self.companies.ids
356
+ )._read_group(
435
357
  domain,
436
- [
437
- "debit",
438
- "credit",
439
- "account_id",
440
- "company_id",
441
- *self._custom_fields,
442
- ],
443
- ["account_id", "company_id"],
444
- lazy=False,
358
+ groupby=("account_id", "company_id"),
359
+ aggregates=("debit:sum", "credit:sum"),
445
360
  )
446
361
  except ValueError as e:
447
362
  raise UserError(
448
- _(
363
+ self.env._(
449
364
  'Error while querying move line source "%(model_name)s". '
450
365
  "This is likely due to a filter or expression referencing "
451
366
  "a field that does not exist in the model.\n\n"
452
- "The technical error message is: %(exception)s. "
453
- )
454
- % dict(
367
+ "The technical error message is: %(exception)s. ",
455
368
  model_name=aml_model._description,
456
369
  exception=e,
457
370
  )
458
371
  ) from e
459
- for acc in accs:
460
- rate, dp = company_rates[acc["company_id"][0]]
461
- debit = acc["debit"] or 0.0
462
- credit = acc["credit"] or 0.0
372
+ for account_id, company_id, debit, credit in accs:
373
+ rate, dp = company_rates[company_id.id]
374
+ debit = debit or 0.0
375
+ credit = credit or 0.0
463
376
  if mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED) and float_is_zero(
464
377
  debit - credit, precision_digits=self.dp
465
378
  ):
466
379
  # in initial mode, ignore accounts with 0 balance
467
380
  continue
468
- # due to branches, it's possible to have multiple groups
469
- # with the same account_id, because multiple companies can
470
- # use the same account
471
- account_data = self._data[key][acc["account_id"][0]]
472
- account_data.add_debit_credit(debit * rate, credit * rate)
473
- for field_name in self._custom_fields:
474
- account_data.add_custom_field(
475
- field_name, acc[field_name] or AccountingNone
476
- )
381
+ # due to branches, it's possible to have multiple acc
382
+ # with the same account_id
383
+ self._data[key][account_id.id] += (debit * rate, credit * rate)
477
384
  # compute ending balances by summing initial and variation
478
385
  for key in ends:
479
386
  domain, mode = key
@@ -481,8 +388,11 @@ class AccountingExpressionProcessor:
481
388
  variation_data = self._data[(domain, self.MODE_VARIATION)]
482
389
  account_ids = set(initial_data.keys()) | set(variation_data.keys())
483
390
  for account_id in account_ids:
484
- self._data[key][account_id] += initial_data[account_id]
485
- self._data[key][account_id] += variation_data[account_id]
391
+ di, ci = initial_data.get(account_id, (AccountingNone, AccountingNone))
392
+ dv, cv = variation_data.get(
393
+ account_id, (AccountingNone, AccountingNone)
394
+ )
395
+ self._data[key][account_id] = (di + dv, ci + cv)
486
396
 
487
397
  def replace_expr(self, expr):
488
398
  """Replace accounting variables in an expression by their amount.
@@ -493,30 +403,25 @@ class AccountingExpressionProcessor:
493
403
  """
494
404
 
495
405
  def f(mo):
496
- field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
406
+ field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
497
407
  key = (ml_domain, mode)
498
408
  account_ids_data = self._data[key]
499
409
  v = AccountingNone
500
410
  account_ids = self._account_ids_by_acc_domain[acc_domain]
501
411
  for account_id in account_ids:
502
- entry = account_ids_data[account_id]
503
- debit = entry.debit
504
- credit = entry.credit
412
+ debit, credit = account_ids_data.get(
413
+ account_id, (AccountingNone, AccountingNone)
414
+ )
505
415
  if field == "bal":
506
416
  v += debit - credit
507
- elif field == "pbal":
508
- if debit >= credit:
509
- v += debit - credit
510
- elif field == "nbal":
511
- if debit < credit:
512
- v += debit - credit
417
+ elif field == "pbal" and debit >= credit:
418
+ v += debit - credit
419
+ elif field == "nbal" and debit < credit:
420
+ v += debit - credit
513
421
  elif field == "deb":
514
422
  v += debit
515
423
  elif field == "crd":
516
424
  v += credit
517
- else:
518
- assert field == "fld"
519
- v += entry.custom_fields[fld_name]
520
425
  # in initial balance mode, assume 0 is None
521
426
  # as it does not make sense to distinguish 0 from "no data"
522
427
  if (
@@ -539,7 +444,7 @@ class AccountingExpressionProcessor:
539
444
  """
540
445
 
541
446
  def f(mo):
542
- field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
447
+ field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
543
448
  key = (ml_domain, mode)
544
449
  # first check if account_id is involved in
545
450
  # the current expression part
@@ -547,9 +452,9 @@ class AccountingExpressionProcessor:
547
452
  return "(AccountingNone)"
548
453
  # here we know account_id is involved in acc_domain
549
454
  account_ids_data = self._data[key]
550
- entry = account_ids_data[account_id]
551
- debit = entry.debit
552
- credit = entry.credit
455
+ debit, credit = account_ids_data.get(
456
+ account_id, (AccountingNone, AccountingNone)
457
+ )
553
458
  if field == "bal":
554
459
  v = debit - credit
555
460
  elif field == "pbal":
@@ -566,9 +471,6 @@ class AccountingExpressionProcessor:
566
471
  v = debit
567
472
  elif field == "crd":
568
473
  v = credit
569
- else:
570
- assert field == "fld"
571
- v = entry.custom_fields[fld_name]
572
474
  # in initial balance mode, assume 0 is None
573
475
  # as it does not make sense to distinguish 0 from "no data"
574
476
  if (
@@ -582,11 +484,11 @@ class AccountingExpressionProcessor:
582
484
  account_ids = set()
583
485
  for expr in exprs:
584
486
  for mo in self._ACC_RE.finditer(expr):
585
- _, mode, _, acc_domain, ml_domain = self._parse_match_object(mo)
487
+ field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
586
488
  key = (ml_domain, mode)
587
489
  account_ids_data = self._data[key]
588
490
  for account_id in self._account_ids_by_acc_domain[acc_domain]:
589
- if account_ids_data[account_id].has_data():
491
+ if account_id in account_ids_data:
590
492
  account_ids.add(account_id)
591
493
 
592
494
  for account_id in account_ids:
@@ -602,7 +504,7 @@ class AccountingExpressionProcessor:
602
504
  aep.parse_expr(expr)
603
505
  aep.done_parsing()
604
506
  aep.do_queries(date_from, date_to)
605
- return {k: (v.debit, v.credit) for k, v in aep._data[((), mode)].items()}
507
+ return aep._data[((), mode)]
606
508
 
607
509
  @classmethod
608
510
  def get_balances_initial(cls, companies, date):
@@ -64,11 +64,11 @@ def _min(*args):
64
64
  >>> min()
65
65
  Traceback (most recent call last):
66
66
  File "<stdin>", line 1, in ?
67
- TypeError: min expected at least 1 argument, got 0
67
+ TypeError: min expected 1 arguments, got 0
68
68
  >>> _min()
69
69
  Traceback (most recent call last):
70
70
  File "<stdin>", line 1, in ?
71
- TypeError: min expected at least 1 argument, got 0
71
+ TypeError: min expected 1 arguments, got 0
72
72
  >>> min([])
73
73
  Traceback (most recent call last):
74
74
  File "<stdin>", line 1, in ?
@@ -107,11 +107,11 @@ def _max(*args):
107
107
  >>> max()
108
108
  Traceback (most recent call last):
109
109
  File "<stdin>", line 1, in ?
110
- TypeError: max expected at least 1 argument, got 0
110
+ TypeError: max expected 1 arguments, got 0
111
111
  >>> _max()
112
112
  Traceback (most recent call last):
113
113
  File "<stdin>", line 1, in ?
114
- TypeError: max expected at least 1 argument, got 0
114
+ TypeError: max expected 1 arguments, got 0
115
115
  >>> max([])
116
116
  Traceback (most recent call last):
117
117
  File "<stdin>", line 1, in ?
@@ -4,7 +4,6 @@
4
4
  import logging
5
5
  from collections import OrderedDict, defaultdict
6
6
 
7
- from odoo import _
8
7
  from odoo.exceptions import UserError
9
8
 
10
9
  from .accounting_none import AccountingNone
@@ -245,10 +244,9 @@ class KpiMatrix:
245
244
  self.lang, row.style_props, kpi.type, val
246
245
  )
247
246
  if row.kpi.multi and subcol.subkpi:
248
- val_comment = "{}.{} = {}".format(
249
- row.kpi.name,
250
- subcol.subkpi.name,
251
- row.kpi._get_expression_str_for_subkpi(subcol.subkpi),
247
+ val_comment = (
248
+ f"{row.kpi.name}.{subcol.subkpi.name} = "
249
+ f"{row.kpi._get_expression_str_for_subkpi(subcol.subkpi)}"
252
250
  )
253
251
  else:
254
252
  val_comment = f"{row.kpi.name} = {row.kpi.expression}"
@@ -309,7 +307,7 @@ class KpiMatrix:
309
307
  common_subkpis = self._common_subkpis([col, base_col])
310
308
  if (col.subkpis or base_col.subkpis) and not common_subkpis:
311
309
  raise UserError(
312
- _(
310
+ self.env._(
313
311
  "Columns %(descr)s and %(base_descr)s are not comparable",
314
312
  descr=col.description,
315
313
  base_descr=base_col.description,
@@ -395,11 +393,12 @@ class KpiMatrix:
395
393
  common_subkpis = self._common_subkpis(sumcols)
396
394
  if any(c.subkpis for c in sumcols) and not common_subkpis:
397
395
  raise UserError(
398
- _(
399
- "Sum cannot be computed in column {} "
396
+ self.env._(
397
+ "Sum cannot be computed in column %s "
400
398
  "because the columns to sum have no "
401
- "common subkpis"
402
- ).format(label)
399
+ "common subkpis",
400
+ label,
401
+ )
403
402
  )
404
403
  sum_col = KpiMatrixCol(
405
404
  sumcol_key,
@@ -3,7 +3,7 @@
3
3
 
4
4
  from collections import defaultdict
5
5
 
6
- from odoo import _, api, fields, models
6
+ from odoo import api, fields, models
7
7
  from odoo.exceptions import UserError
8
8
  from odoo.osv import expression
9
9
 
@@ -60,11 +60,9 @@ class MisKpiData(models.AbstractModel):
60
60
  subkpi_name = "." + subkpi_name
61
61
  else:
62
62
  subkpi_name = ""
63
- rec.name = "{}{}: {} - {}".format(
64
- rec.kpi_expression_id.kpi_id.name,
65
- subkpi_name,
66
- rec.date_from,
67
- rec.date_to,
63
+ rec.name = (
64
+ f"{rec.kpi_expression_id.kpi_id.name}{subkpi_name}: "
65
+ f"{rec.date_from} - {rec.date_to}"
68
66
  )
69
67
 
70
68
  @api.model
@@ -99,7 +97,7 @@ class MisKpiData(models.AbstractModel):
99
97
  res_avg[item.kpi_expression_id].append((i_days, item.amount))
100
98
  else:
101
99
  raise UserError(
102
- _(
100
+ self.env._(
103
101
  "Unexpected accumulation method %(method)s for %(name)s.",
104
102
  method=item.kpi_expression_id.kpi_id.accumulation_method,
105
103
  name=item.name,