odoo-addon-mis-builder 17.0.1.2.1.1__py3-none-any.whl → 18.0.1.0.0.11__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 (57) hide show
  1. odoo/addons/mis_builder/README.rst +7 -6
  2. odoo/addons/mis_builder/__manifest__.py +3 -3
  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 +13 -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 +42 -25
  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 +12 -21
  34. odoo/addons/mis_builder/static/src/components/mis_report_widget.scss +68 -0
  35. odoo/addons/mis_builder/static/src/components/mis_report_widget.xml +9 -14
  36. odoo/addons/mis_builder/static/src/scss/report.scss +49 -0
  37. odoo/addons/mis_builder/tests/__init__.py +1 -0
  38. odoo/addons/mis_builder/tests/common.py +2 -4
  39. odoo/addons/mis_builder/tests/fake_models.py +18 -1
  40. odoo/addons/mis_builder/tests/test_aep.py +7 -69
  41. odoo/addons/mis_builder/tests/test_data_sources.py +4 -11
  42. odoo/addons/mis_builder/tests/test_kpi_data.py +1 -5
  43. odoo/addons/mis_builder/tests/test_mis_report_instance.py +21 -17
  44. odoo/addons/mis_builder/tests/test_multi_company_aep.py +3 -3
  45. odoo/addons/mis_builder/tests/test_pro_rata_read_group.py +105 -0
  46. odoo/addons/mis_builder/views/mis_report.xml +38 -43
  47. odoo/addons/mis_builder/views/mis_report_instance.xml +37 -40
  48. odoo/addons/mis_builder/views/mis_report_style.xml +6 -6
  49. odoo/addons/mis_builder/wizard/mis_builder_dashboard.py +3 -3
  50. odoo/addons/mis_builder/wizard/mis_builder_dashboard.xml +6 -6
  51. {odoo_addon_mis_builder-17.0.1.2.1.1.dist-info → odoo_addon_mis_builder-18.0.1.0.0.11.dist-info}/METADATA +12 -11
  52. odoo_addon_mis_builder-18.0.1.0.0.11.dist-info/RECORD +87 -0
  53. odoo/addons/mis_builder/static/src/components/mis_report_widget.css +0 -67
  54. odoo/addons/mis_builder/static/src/css/report.css +0 -46
  55. odoo_addon_mis_builder-17.0.1.2.1.1.dist-info/RECORD +0 -86
  56. {odoo_addon_mis_builder-17.0.1.2.1.1.dist-info → odoo_addon_mis_builder-18.0.1.0.0.11.dist-info}/WHEEL +0 -0
  57. {odoo_addon_mis_builder-17.0.1.2.1.1.dist-info → odoo_addon_mis_builder-18.0.1.0.0.11.dist-info}/top_level.txt +0 -0
@@ -11,9 +11,8 @@ from collections import defaultdict
11
11
  import dateutil
12
12
  import pytz
13
13
 
14
- from odoo import _, api, fields, models
14
+ from odoo import api, fields, models
15
15
  from odoo.exceptions import UserError, ValidationError
16
- from odoo.models import expression as osv_expression
17
16
  from odoo.tools.safe_eval import (
18
17
  datetime as safe_datetime,
19
18
  )
@@ -112,9 +111,9 @@ class MisReportKpi(models.Model):
112
111
  )
113
112
  type = fields.Selection(
114
113
  [
115
- (TYPE_NUM, _("Numeric")),
116
- (TYPE_PCT, _("Percentage")),
117
- (TYPE_STR, _("String")),
114
+ (TYPE_NUM, "Numeric"),
115
+ (TYPE_PCT, "Percentage"),
116
+ (TYPE_STR, "String"),
118
117
  ],
119
118
  required=True,
120
119
  string="Value type",
@@ -122,16 +121,16 @@ class MisReportKpi(models.Model):
122
121
  )
123
122
  compare_method = fields.Selection(
124
123
  [
125
- (CMP_DIFF, _("Difference")),
126
- (CMP_PCT, _("Percentage")),
127
- (CMP_NONE, _("None")),
124
+ (CMP_DIFF, "Difference"),
125
+ (CMP_PCT, "Percentage"),
126
+ (CMP_NONE, "None"),
128
127
  ],
129
128
  required=True,
130
129
  string="Comparison Method",
131
130
  default=CMP_PCT,
132
131
  )
133
132
  accumulation_method = fields.Selection(
134
- [(ACC_SUM, _("Sum")), (ACC_AVG, _("Average")), (ACC_NONE, _("None"))],
133
+ [(ACC_SUM, "Sum"), (ACC_AVG, "Average"), (ACC_NONE, "None")],
135
134
  required=True,
136
135
  default=ACC_SUM,
137
136
  help="Determines how values of this kpi spanning over a "
@@ -159,8 +158,8 @@ class MisReportKpi(models.Model):
159
158
  for record in self:
160
159
  if not _is_valid_python_var(record.name):
161
160
  raise ValidationError(
162
- _("KPI name ({}) must be a valid python identifier").format(
163
- record.name
161
+ self.env._(
162
+ "KPI name (%s) must be a valid python identifier", record.name
164
163
  )
165
164
  )
166
165
 
@@ -265,8 +264,9 @@ class MisReportSubkpi(models.Model):
265
264
  for record in self:
266
265
  if not _is_valid_python_var(record.name):
267
266
  raise ValidationError(
268
- _("Sub-KPI name ({}) must be a valid python identifier").format(
269
- record.name
267
+ self.env._(
268
+ "Sub-KPI name (%s) must be a valid python identifier",
269
+ record.name,
270
270
  )
271
271
  )
272
272
 
@@ -312,48 +312,35 @@ class MisReportKpiExpression(models.Model):
312
312
  kpi = rec.kpi_id
313
313
  subkpi = rec.subkpi_id
314
314
  if subkpi:
315
- name = "{} / {} ({}.{})".format(
316
- kpi.description, subkpi.description, kpi.name, subkpi.name
315
+ name = (
316
+ f"{kpi.description} / {subkpi.description} "
317
+ f"({kpi.name}.{subkpi.name})"
317
318
  )
318
319
  else:
319
320
  name = rec.kpi_id.display_name
320
321
  rec.display_name = name
321
322
 
322
323
  @api.model
323
- def _name_search(self, name, domain=None, operator="ilike", limit=None, order=None):
324
- # TODO maybe implement negative search operators, although
325
- # there is not really a use case for that
326
- domain = domain or []
327
- splitted_name = name.split(".", 2)
328
- name_search_domain = []
329
- if "." in name:
330
- kpi_name, subkpi_name = splitted_name[0], splitted_name[1]
331
- name_search_domain = osv_expression.AND(
332
- [
333
- name_search_domain,
334
- [
335
- "|",
336
- "|",
337
- "&",
338
- ("kpi_id.name", "=", kpi_name),
339
- ("subkpi_id.name", operator, subkpi_name),
340
- ("kpi_id.description", operator, name),
341
- ("subkpi_id.description", operator, name),
342
- ],
343
- ]
344
- )
345
- name_search_domain = osv_expression.OR(
346
- [
347
- name_search_domain,
348
- [
349
- "|",
350
- ("kpi_id.name", operator, name),
351
- ("kpi_id.description", operator, name),
352
- ],
324
+ def _search_display_name(self, operator, value):
325
+ if "." in value:
326
+ kpi_name, subkpi_name = value.split(".", 1)
327
+ name_search_domain = [
328
+ "|",
329
+ "|",
330
+ "&",
331
+ ("kpi_id.name", "=", kpi_name),
332
+ ("subkpi_id.name", operator, subkpi_name),
333
+ ("kpi_id.description", operator, value),
334
+ ("subkpi_id.description", operator, value),
353
335
  ]
354
- )
355
- domain = osv_expression.AND([domain, name_search_domain])
356
- return self._search(domain, limit=limit, order=order)
336
+ else:
337
+ name_search_domain = [
338
+ "|",
339
+ ("kpi_id.name", operator, value),
340
+ ("kpi_id.description", operator, value),
341
+ ]
342
+
343
+ return name_search_domain
357
344
 
358
345
 
359
346
  class MisReportQuery(models.Model):
@@ -382,10 +369,10 @@ class MisReportQuery(models.Model):
382
369
  )
383
370
  aggregate = fields.Selection(
384
371
  [
385
- ("sum", _("Sum")),
386
- ("avg", _("Average")),
387
- ("min", _("Min")),
388
- ("max", _("Max")),
372
+ ("sum", "Sum"),
373
+ ("avg", "Average"),
374
+ ("min", "Min"),
375
+ ("max", "Max"),
389
376
  ],
390
377
  )
391
378
  date_field = fields.Many2one(
@@ -406,8 +393,8 @@ class MisReportQuery(models.Model):
406
393
  for record in self:
407
394
  if not _is_valid_python_var(record.name):
408
395
  raise ValidationError(
409
- _("Query name ({}) must be valid python identifier").format(
410
- record.name
396
+ self.env._(
397
+ "Query name (%s) must be valid python identifier", record.name
411
398
  )
412
399
  )
413
400
 
@@ -528,7 +515,7 @@ class MisReport(models.Model):
528
515
  def copy(self, default=None):
529
516
  self.ensure_one()
530
517
  default = dict(default or [])
531
- default["name"] = _("%s (copy)") % self.name
518
+ default["name"] = self.env._("%s (copy)", self.name)
532
519
  new = super().copy(default)
533
520
  # after a copy, we have new subkpis, but the expressions
534
521
  # subkpi_id fields still point to the original one, so
@@ -722,7 +709,7 @@ class MisReport(models.Model):
722
709
  vals = vals[0]
723
710
  if len(vals) != col.colspan:
724
711
  raise SubKPITupleLengthError(
725
- _(
712
+ self.env._(
726
713
  'KPI "%(kpi)s" is valued as a tuple of '
727
714
  "length %(length)s while a tuple of length"
728
715
  "%(expected_length)s is expected.",
@@ -735,7 +722,7 @@ class MisReport(models.Model):
735
722
  vals = (vals[0],) * col.colspan
736
723
  else:
737
724
  raise SubKPIUnknownTypeError(
738
- _(
725
+ self.env._(
739
726
  'KPI "%(kpi)s" has type %(type)s while a tuple was '
740
727
  "expected.\n\nThis can be fixed by either:\n\t- "
741
728
  "Changing the KPI value to a tuple of length "
@@ -940,7 +927,9 @@ class MisReport(models.Model):
940
927
  # all (in Odoo 13+, there is also the cancel state that we must ignore)
941
928
  return [("parent_state", "in", ("posted", "draft"))]
942
929
  else:
943
- raise UserError(_("Unexpected value %s for target_move.") % (target_move,))
930
+ raise UserError(
931
+ self.env._("Unexpected value %s for target_move.", target_move)
932
+ )
944
933
 
945
934
  def evaluate(
946
935
  self,
@@ -8,7 +8,7 @@ import logging
8
8
 
9
9
  from dateutil.relativedelta import relativedelta
10
10
 
11
- from odoo import _, api, fields, models
11
+ from odoo import api, fields, models
12
12
  from odoo.exceptions import UserError, ValidationError
13
13
 
14
14
  from .aep import AccountingExpressionProcessor as AEP
@@ -58,7 +58,9 @@ class MisReportInstancePeriodSum(models.Model):
58
58
  for rec in self:
59
59
  if rec.period_id == rec.period_to_sum_id:
60
60
  raise ValidationError(
61
- _("You cannot sum period %s with itself.") % rec.period_id.name
61
+ self.env._(
62
+ "You cannot sum period %s with itself.", rec.period_id.name
63
+ )
62
64
  )
63
65
 
64
66
 
@@ -187,11 +189,11 @@ class MisReportInstancePeriod(models.Model):
187
189
  )
188
190
  type = fields.Selection(
189
191
  [
190
- ("d", _("Day")),
191
- ("w", _("Week")),
192
- ("m", _("Month")),
193
- ("y", _("Year")),
194
- ("date_range", _("Date Range")),
192
+ ("d", "Day"),
193
+ ("w", "Week"),
194
+ ("m", "Month"),
195
+ ("y", "Year"),
196
+ ("date_range", "Date Range"),
195
197
  ],
196
198
  string="Period type",
197
199
  )
@@ -306,7 +308,7 @@ class MisReportInstancePeriod(models.Model):
306
308
  if record.source == SRC_ACTUALS:
307
309
  if not record.report_instance_id.report_id:
308
310
  raise UserError(
309
- _(
311
+ self.env._(
310
312
  "Please select a report template and/or "
311
313
  "save the report before adding columns."
312
314
  )
@@ -338,13 +340,13 @@ class MisReportInstancePeriod(models.Model):
338
340
  report_account_model = record.report_id.account_model
339
341
  if record_model != report_account_model:
340
342
  raise ValidationError(
341
- _(
343
+ self.env._(
342
344
  "Actual (alternative) models used in columns must "
343
345
  "have the same account model in the Account field and must "
344
346
  "be the same defined in the "
345
- "report template: %s"
347
+ "report template: %s",
348
+ report_account_model,
346
349
  )
347
- % report_account_model
348
350
  )
349
351
 
350
352
  @api.onchange("date_range_id")
@@ -414,14 +416,20 @@ class MisReportInstancePeriod(models.Model):
414
416
  if rec.source in (SRC_ACTUALS, SRC_ACTUALS_ALT):
415
417
  if rec.mode == MODE_NONE:
416
418
  raise DateFilterRequired(
417
- _("A date filter is mandatory for this source " "in column %s.")
418
- % rec.name
419
+ self.env._(
420
+ "A date filter is mandatory for this source "
421
+ "in column %s.",
422
+ rec.name,
423
+ )
419
424
  )
420
425
  elif rec.source in (SRC_SUMCOL, SRC_CMPCOL):
421
426
  if rec.mode != MODE_NONE:
422
427
  raise DateFilterForbidden(
423
- _("No date filter is allowed for this source " "in column %s.")
424
- % rec.name
428
+ self.env._(
429
+ "No date filter is allowed for this source "
430
+ "in column %s.",
431
+ rec.name,
432
+ )
425
433
  )
426
434
 
427
435
  @api.constrains("source", "source_cmpcol_from_id", "source_cmpcol_to_id")
@@ -430,11 +438,13 @@ class MisReportInstancePeriod(models.Model):
430
438
  if rec.source == SRC_CMPCOL:
431
439
  if not rec.source_cmpcol_from_id or not rec.source_cmpcol_to_id:
432
440
  raise ValidationError(
433
- _("Please provide both columns to compare in %s.") % rec.name
441
+ self.env._(
442
+ "Please provide both columns to compare in %s.", rec.name
443
+ )
434
444
  )
435
445
  if rec.source_cmpcol_from_id == rec or rec.source_cmpcol_to_id == rec:
436
446
  raise ValidationError(
437
- _("Column %s cannot be compared to itrec.") % rec.name
447
+ self.env._("Column %s cannot be compared to itrec.", rec.name)
438
448
  )
439
449
  if (
440
450
  rec.source_cmpcol_from_id.report_instance_id
@@ -443,8 +453,11 @@ class MisReportInstancePeriod(models.Model):
443
453
  != rec.report_instance_id
444
454
  ):
445
455
  raise ValidationError(
446
- _("Columns to compare must belong to the same report " "in %s")
447
- % rec.name
456
+ self.env._(
457
+ "Columns to compare must belong to the same report "
458
+ "in %s",
459
+ rec.name,
460
+ )
448
461
  )
449
462
 
450
463
  def copy_data(self, default=None):
@@ -652,7 +665,7 @@ class MisReportInstance(models.Model):
652
665
  def copy(self, default=None):
653
666
  self.ensure_one()
654
667
  default = dict(default or {})
655
- default["name"] = _("%s (copy)") % self.name
668
+ default["name"] = self.env._("%s (copy)", self.name)
656
669
  return super().copy(default)
657
670
 
658
671
  def _format_date(self, date):
@@ -789,8 +802,10 @@ class MisReportInstance(models.Model):
789
802
  def _add_column_move_lines(self, aep, kpi_matrix, period, label, description):
790
803
  if not period.date_from or not period.date_to:
791
804
  raise UserError(
792
- _("Column %s with move lines source must have from/to dates.")
793
- % (period.name,)
805
+ self.env._(
806
+ "Column %s with move lines source must have from/to dates.",
807
+ period.name,
808
+ )
794
809
  )
795
810
  expression_evaluator = ExpressionEvaluator(
796
811
  aep,
@@ -862,7 +877,7 @@ class MisReportInstance(models.Model):
862
877
  elif period.date_from and period.date_to:
863
878
  date_from = self._format_date(period.date_from)
864
879
  date_to = self._format_date(period.date_to)
865
- description = _(
880
+ description = self.env._(
866
881
  "from %(date_from)s to %(date_to)s",
867
882
  date_from=date_from,
868
883
  date_to=date_to,
@@ -879,12 +894,14 @@ class MisReportInstance(models.Model):
879
894
 
880
895
  @api.model
881
896
  def _get_drilldown_views_and_orders(self):
882
- return {"tree": 1, "form": 2, "pivot": 3, "graph": 4}
897
+ return {"list": 1, "form": 2, "pivot": 3, "graph": 4}
883
898
 
884
899
  @api.model
885
900
  def _get_drilldown_model_views(self, model_name):
886
901
  self.ensure_one()
887
- views_records = self.env["ir.ui.view"].search([("model", "=", model_name)])
902
+ views_records = (
903
+ self.env["ir.ui.view"].sudo().search([("model", "=", model_name)])
904
+ )
888
905
  views_records = set(views_records.mapped("type"))
889
906
  views_order = self._get_drilldown_views_and_orders()
890
907
  views = {view_type for view_type in views_records if view_type in views_order}
@@ -5,7 +5,7 @@
5
5
 
6
6
  import sys
7
7
 
8
- from odoo import _, api, fields, models
8
+ from odoo import api, fields, models
9
9
  from odoo.exceptions import ValidationError
10
10
 
11
11
  from .accounting_none import AccountingNone
@@ -56,7 +56,7 @@ class MisReportKpiStyle(models.Model):
56
56
  for record in self:
57
57
  if record.indent_level < 0:
58
58
  raise ValidationError(
59
- _("Indent level must be greater than " "or equal to 0")
59
+ self.env._("Indent level must be greater than or equal to 0")
60
60
  )
61
61
 
62
62
  _font_style_selection = [("normal", "Normal"), ("italic", "Italic")]
@@ -84,6 +84,7 @@ class MisReportKpiStyle(models.Model):
84
84
  }
85
85
 
86
86
  # style name
87
+ # TODO enforce uniqueness
87
88
  name = fields.Char(string="Style name", required=True)
88
89
 
89
90
  # color
@@ -118,11 +119,11 @@ class MisReportKpiStyle(models.Model):
118
119
  divider_inherit = fields.Boolean(default=True)
119
120
  divider = fields.Selection(
120
121
  [
121
- ("1e-6", _("µ")),
122
- ("1e-3", _("m")),
123
- ("1", _("1")),
124
- ("1e3", _("k")),
125
- ("1e6", _("M")),
122
+ ("1e-6", "µ"),
123
+ ("1e-3", "m"),
124
+ ("1", "1"),
125
+ ("1e3", "k"),
126
+ ("1e6", "M"),
126
127
  ],
127
128
  string="Factor",
128
129
  default="1",
@@ -132,10 +133,6 @@ class MisReportKpiStyle(models.Model):
132
133
  hide_always_inherit = fields.Boolean(default=True)
133
134
  hide_always = fields.Boolean(default=False)
134
135
 
135
- _sql_constraints = [
136
- ("style_name_uniq", "unique(name)", "Style name should be unique")
137
- ]
138
-
139
136
  @api.model
140
137
  def merge(self, styles):
141
138
  """Merge several styles, giving priority to the last.
@@ -239,7 +236,7 @@ class MisReportKpiStyle(models.Model):
239
236
  if var_type == TYPE_PCT:
240
237
  delta = value - base_value
241
238
  if delta and round(delta, (style_props.dp or 0) + 2) != 0:
242
- delta_style.update(divider=0.01, prefix="", suffix=_("pp"))
239
+ delta_style.update(divider=0.01, prefix="", suffix=self.env._("pp"))
243
240
  else:
244
241
  delta = AccountingNone
245
242
  elif var_type == TYPE_NUM:
@@ -1,7 +1,7 @@
1
1
  # Copyright 2020 ACSONE SA/NV (<http://acsone.eu>)
2
2
  # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
3
3
 
4
- from odoo import _, api, fields, models
4
+ from odoo import api, fields, models
5
5
  from odoo.exceptions import ValidationError
6
6
 
7
7
  from .mis_report import _is_valid_python_var
@@ -50,8 +50,9 @@ class MisReportSubReport(models.Model):
50
50
  for rec in self:
51
51
  if not _is_valid_python_var(rec.name):
52
52
  raise InvalidNameError(
53
- _("Subreport name ({}) must be a valid python identifier").format(
54
- rec.name
53
+ self.env._(
54
+ "Subreport name (%s) must be a valid python identifier",
55
+ rec.name,
55
56
  )
56
57
  )
57
58
 
@@ -69,6 +70,6 @@ class MisReportSubReport(models.Model):
69
70
 
70
71
  for rec in self:
71
72
  if _has_subreport(rec.subreport_id, rec.report_id):
72
- raise ParentLoopError(_("Subreport loop detected"))
73
+ raise ParentLoopError(self.env._("Subreport loop detected"))
73
74
 
74
75
  # TODO check subkpi compatibility in subreports
@@ -1,11 +1,15 @@
1
1
  # Copyright 2020 ACSONE SA/NV
2
2
  # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3
3
 
4
- from odoo import _, api, fields, models
4
+ from collections import defaultdict
5
+ from itertools import chain
6
+
7
+ from odoo import api, fields, models
5
8
  from odoo.exceptions import UserError
6
9
  from odoo.fields import Date
7
10
 
8
11
  from .mis_kpi_data import intersect_days
12
+ from .simple_array import SimpleArray
9
13
 
10
14
 
11
15
  class ProRataReadGroupMixin(models.AbstractModel):
@@ -18,8 +22,7 @@ class ProRataReadGroupMixin(models.AbstractModel):
18
22
  compute=lambda self: None,
19
23
  search="_search_date",
20
24
  help=(
21
- "Dummy field that adapts searches on date "
22
- "to searches on date_from/date_to."
25
+ "Dummy field that adapts searches on date to searches on date_from/date_to."
23
26
  ),
24
27
  )
25
28
 
@@ -29,7 +32,7 @@ class ProRataReadGroupMixin(models.AbstractModel):
29
32
  elif operator in ("<=", "<"):
30
33
  return [("date_from", operator, value)]
31
34
  raise UserError(
32
- _("Unsupported operator %s for searching on date") % (operator,)
35
+ self.env._("Unsupported operator %s for searching on date", operator)
33
36
  )
34
37
 
35
38
  @api.model
@@ -37,15 +40,36 @@ class ProRataReadGroupMixin(models.AbstractModel):
37
40
  return intersect_days(item_dt_from, item_dt_to, dt_from, dt_to)
38
41
 
39
42
  @api.model
40
- def read_group(
41
- self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True
43
+ def _prorata(self, item, dt_from, dt_to, sum_field):
44
+ if sum_field == "__count":
45
+ return 1
46
+ item_dt_from = Date.from_string(item["date_from"])
47
+ item_dt_to = Date.from_string(item["date_to"])
48
+ i_days, item_days = self._intersect_days(
49
+ item_dt_from, item_dt_to, dt_from, dt_to
50
+ )
51
+ return item[sum_field] * i_days / item_days
52
+
53
+ @api.model
54
+ def _read_group(
55
+ self,
56
+ domain,
57
+ groupby=(),
58
+ aggregates=(),
59
+ having=(),
60
+ offset=0,
61
+ limit=None,
62
+ order=None,
42
63
  ):
43
- """Override read_group to perform pro-rata temporis adjustments.
64
+ """Override _read_group to perform pro-rata temporis adjustments.
44
65
 
45
- When read_group is invoked with a domain that filters on
66
+ When _read_group is invoked with a domain that filters on
46
67
  a time period (date >= from and date <= to, or
47
68
  date_from <= to and date_to >= from), adjust the accumulated
48
69
  values pro-rata temporis.
70
+
71
+ This mechanism works in specific cases and is primarily designed to
72
+ make AEP work with budget tables.
49
73
  """
50
74
  date_from = None
51
75
  date_to = None
@@ -64,33 +88,29 @@ class ProRataReadGroupMixin(models.AbstractModel):
64
88
  if (
65
89
  date_from is not None
66
90
  and date_to is not None
67
- and not any(":" in f for f in fields)
91
+ and all(a.endswith(":sum") or a == "__count" for a in aggregates)
92
+ and not any(":" in g for g in groupby)
68
93
  ):
69
94
  dt_from = Date.from_string(date_from)
70
95
  dt_to = Date.from_string(date_to)
71
- res = {}
72
- sum_fields = set(fields) - set(groupby)
73
- read_fields = set(fields + ["date_from", "date_to"])
74
- for item in self.search(domain).read(read_fields):
96
+ stripped_aggregates = [a.rstrip(":sum") for a in aggregates]
97
+ sum_fields = filter(lambda a: a != "__count", stripped_aggregates)
98
+ read_fields = [*groupby, *sum_fields, "date_from", "date_to"]
99
+ # res is a dictionary with a tuple of groupby field names as keys,
100
+ # and sums of aggregate fields as values.
101
+ res = defaultdict(lambda: SimpleArray((0.0,) * len(aggregates)))
102
+ for item in self.search_fetch(domain, read_fields):
75
103
  key = tuple(item[k] for k in groupby)
76
- if key not in res:
77
- res[key] = {k: item[k] for k in groupby}
78
- res[key].update({k: 0.0 for k in sum_fields})
79
- res_item = res[key]
80
- for sum_field in sum_fields:
81
- item_dt_from = Date.from_string(item["date_from"])
82
- item_dt_to = Date.from_string(item["date_to"])
83
- i_days, item_days = self._intersect_days(
84
- item_dt_from, item_dt_to, dt_from, dt_to
85
- )
86
- res_item[sum_field] += item[sum_field] * i_days / item_days
87
- return res.values()
88
- return super().read_group(
104
+ res[key] += SimpleArray(
105
+ self._prorata(item, dt_from, dt_to, f) for f in stripped_aggregates
106
+ )
107
+ return [tuple(chain(k, v)) for k, v in res.items()]
108
+ return super()._read_group(
89
109
  domain,
90
- fields,
91
110
  groupby,
92
- offset=offset,
93
- limit=limit,
94
- orderby=orderby,
95
- lazy=lazy,
111
+ aggregates,
112
+ having,
113
+ offset,
114
+ limit,
115
+ order,
96
116
  )
@@ -1,6 +1,6 @@
1
1
  # Copyright 2014 ACSONE SA/NV (<http://acsone.eu>)
2
2
  # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
3
- """ A trivial immutable array that supports basic arithmetic operations.
3
+ """A trivial immutable array that supports basic arithmetic operations.
4
4
 
5
5
  >>> a = SimpleArray((1.0, 2.0, 3.0))
6
6
  >>> b = SimpleArray((4.0, 5.0, 6.0))
@@ -104,7 +104,7 @@ class SimpleArray(tuple):
104
104
 
105
105
  if isinstance(other, tuple):
106
106
  if len(other) != len(self):
107
- raise TypeError("tuples must have same length for %s" % op)
107
+ raise TypeError(f"tuples must have same length for {op}")
108
108
  return self.__class__(map(_o2, self, other))
109
109
  else:
110
110
  return self.__class__(_o2(z, other) for z in self)
@@ -27,3 +27,4 @@
27
27
  - Hoang Diep \<<hoang@trobz.com>\>
28
28
  - Miquel Pascual \<<mpascual@apsl.net>\>
29
29
  - Antoni Marroig \<<amarroig@apsl.net>\>
30
+ - Chau Le \<<chaulb@trobz.com>\>
@@ -6,7 +6,7 @@ import numbers
6
6
  from collections import defaultdict
7
7
  from datetime import datetime
8
8
 
9
- from odoo import _, fields, models
9
+ from odoo import fields, models
10
10
 
11
11
  from ..models.accounting_none import AccountingNone
12
12
  from ..models.data_error import DataError
@@ -161,7 +161,7 @@ class MisBuilderXlsx(models.AbstractModel):
161
161
  now_tz = fields.Datetime.context_timestamp(
162
162
  self.env["res.users"], datetime.now()
163
163
  )
164
- create_date = _(
164
+ create_date = self.env._(
165
165
  "Generated on %(gen_date)s at %(gen_time)s",
166
166
  gen_date=now_tz.strftime(lang.date_format),
167
167
  gen_time=now_tz.strftime(lang.time_format),