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.
- odoo/addons/mis_builder/README.rst +7 -6
- odoo/addons/mis_builder/__manifest__.py +1 -1
- odoo/addons/mis_builder/datas/ir_cron.xml +1 -3
- odoo/addons/mis_builder/i18n/ca.po +8 -51
- odoo/addons/mis_builder/i18n/de.po +4 -39
- odoo/addons/mis_builder/i18n/el.po +4 -39
- odoo/addons/mis_builder/i18n/el_GR.po +4 -39
- odoo/addons/mis_builder/i18n/es.po +12 -65
- odoo/addons/mis_builder/i18n/fr.po +12 -65
- odoo/addons/mis_builder/i18n/hr.po +4 -39
- odoo/addons/mis_builder/i18n/it.po +13 -80
- odoo/addons/mis_builder/i18n/mis_builder.pot +18 -125
- odoo/addons/mis_builder/i18n/nl.po +4 -39
- odoo/addons/mis_builder/i18n/nl_NL.po +4 -39
- odoo/addons/mis_builder/i18n/pt.po +4 -39
- odoo/addons/mis_builder/i18n/pt_BR.po +13 -64
- odoo/addons/mis_builder/i18n/sv.po +12 -64
- odoo/addons/mis_builder/i18n/tr.po +4 -39
- odoo/addons/mis_builder/i18n/zh_CN.po +78 -154
- odoo/addons/mis_builder/models/aep.py +62 -160
- odoo/addons/mis_builder/models/aggregate.py +4 -4
- odoo/addons/mis_builder/models/kpimatrix.py +9 -10
- odoo/addons/mis_builder/models/mis_kpi_data.py +5 -7
- odoo/addons/mis_builder/models/mis_report.py +47 -58
- odoo/addons/mis_builder/models/mis_report_instance.py +43 -24
- odoo/addons/mis_builder/models/mis_report_style.py +9 -12
- odoo/addons/mis_builder/models/mis_report_subreport.py +5 -4
- odoo/addons/mis_builder/models/prorata_read_group_mixin.py +51 -31
- odoo/addons/mis_builder/models/simple_array.py +2 -2
- odoo/addons/mis_builder/readme/CONTRIBUTORS.md +1 -0
- odoo/addons/mis_builder/report/mis_report_instance_xlsx.py +2 -2
- odoo/addons/mis_builder/static/description/index.html +5 -4
- odoo/addons/mis_builder/static/src/components/mis_report_widget.esm.js +38 -22
- odoo/addons/mis_builder/static/src/components/mis_report_widget.scss +9 -0
- odoo/addons/mis_builder/static/src/components/mis_report_widget.xml +16 -14
- odoo/addons/mis_builder/tests/__init__.py +1 -0
- odoo/addons/mis_builder/tests/common.py +2 -4
- odoo/addons/mis_builder/tests/fake_models.py +18 -1
- odoo/addons/mis_builder/tests/test_aep.py +7 -69
- odoo/addons/mis_builder/tests/test_data_sources.py +4 -11
- odoo/addons/mis_builder/tests/test_kpi_data.py +1 -5
- odoo/addons/mis_builder/tests/test_mis_report_instance.py +21 -17
- odoo/addons/mis_builder/tests/test_multi_company_aep.py +3 -3
- odoo/addons/mis_builder/tests/test_pro_rata_read_group.py +105 -0
- odoo/addons/mis_builder/views/mis_report.xml +38 -43
- odoo/addons/mis_builder/views/mis_report_instance.xml +38 -40
- odoo/addons/mis_builder/views/mis_report_style.xml +6 -6
- odoo/addons/mis_builder/wizard/mis_builder_dashboard.py +3 -3
- odoo/addons/mis_builder/wizard/mis_builder_dashboard.xml +6 -6
- {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
- odoo_addon_mis_builder-18.0.1.1.0.dist-info/RECORD +87 -0
- odoo/addons/mis_builder/static/src/components/mis_report_widget.css +0 -67
- odoo_addon_mis_builder-17.0.1.3.0.dist-info/RECORD +0 -87
- {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
- {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
@@ -11,9 +11,8 @@ from collections import defaultdict
|
|
11
11
|
import dateutil
|
12
12
|
import pytz
|
13
13
|
|
14
|
-
from odoo import
|
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,
|
116
|
-
(TYPE_PCT,
|
117
|
-
(TYPE_STR,
|
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,
|
126
|
-
(CMP_PCT,
|
127
|
-
(CMP_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,
|
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
|
-
_(
|
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
|
-
_(
|
269
|
-
|
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 =
|
316
|
-
kpi.description
|
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
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
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
|
-
|
356
|
-
|
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",
|
386
|
-
("avg",
|
387
|
-
("min",
|
388
|
-
("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
|
-
_(
|
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)"
|
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(
|
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
|
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
|
-
_(
|
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",
|
191
|
-
("w",
|
192
|
-
("m",
|
193
|
-
("y",
|
194
|
-
("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
|
-
_(
|
418
|
-
|
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
|
-
_(
|
424
|
-
|
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
|
-
_(
|
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."
|
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
|
-
_(
|
447
|
-
|
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):
|
@@ -579,6 +592,10 @@ class MisReportInstance(models.Model):
|
|
579
592
|
help="Search view to customize the filter box in the report widget.",
|
580
593
|
)
|
581
594
|
|
595
|
+
wide_display_by_default = fields.Boolean(
|
596
|
+
string="Open report in wide mode by default",
|
597
|
+
)
|
598
|
+
|
582
599
|
@api.depends("report_id.move_lines_source")
|
583
600
|
def _compute_widget_search_view_id(self):
|
584
601
|
for rec in self:
|
@@ -652,7 +669,7 @@ class MisReportInstance(models.Model):
|
|
652
669
|
def copy(self, default=None):
|
653
670
|
self.ensure_one()
|
654
671
|
default = dict(default or {})
|
655
|
-
default["name"] = _("%s (copy)"
|
672
|
+
default["name"] = self.env._("%s (copy)", self.name)
|
656
673
|
return super().copy(default)
|
657
674
|
|
658
675
|
def _format_date(self, date):
|
@@ -789,8 +806,10 @@ class MisReportInstance(models.Model):
|
|
789
806
|
def _add_column_move_lines(self, aep, kpi_matrix, period, label, description):
|
790
807
|
if not period.date_from or not period.date_to:
|
791
808
|
raise UserError(
|
792
|
-
_(
|
793
|
-
|
809
|
+
self.env._(
|
810
|
+
"Column %s with move lines source must have from/to dates.",
|
811
|
+
period.name,
|
812
|
+
)
|
794
813
|
)
|
795
814
|
expression_evaluator = ExpressionEvaluator(
|
796
815
|
aep,
|
@@ -862,7 +881,7 @@ class MisReportInstance(models.Model):
|
|
862
881
|
elif period.date_from and period.date_to:
|
863
882
|
date_from = self._format_date(period.date_from)
|
864
883
|
date_to = self._format_date(period.date_to)
|
865
|
-
description = _(
|
884
|
+
description = self.env._(
|
866
885
|
"from %(date_from)s to %(date_to)s",
|
867
886
|
date_from=date_from,
|
868
887
|
date_to=date_to,
|
@@ -879,7 +898,7 @@ class MisReportInstance(models.Model):
|
|
879
898
|
|
880
899
|
@api.model
|
881
900
|
def _get_drilldown_views_and_orders(self):
|
882
|
-
return {"
|
901
|
+
return {"list": 1, "form": 2, "pivot": 3, "graph": 4}
|
883
902
|
|
884
903
|
@api.model
|
885
904
|
def _get_drilldown_model_views(self, model_name):
|
@@ -5,7 +5,7 @@
|
|
5
5
|
|
6
6
|
import sys
|
7
7
|
|
8
|
-
from odoo import
|
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
|
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",
|
123
|
-
("1",
|
124
|
-
("1e3",
|
125
|
-
("1e6",
|
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
|
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
|
-
_(
|
54
|
-
|
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
|
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"
|
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
|
41
|
-
|
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
|
64
|
+
"""Override _read_group to perform pro-rata temporis adjustments.
|
44
65
|
|
45
|
-
When
|
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
|
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
|
-
|
72
|
-
sum_fields =
|
73
|
-
read_fields =
|
74
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
"""
|
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
|
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)
|
@@ -6,7 +6,7 @@ import numbers
|
|
6
6
|
from collections import defaultdict
|
7
7
|
from datetime import datetime
|
8
8
|
|
9
|
-
from odoo import
|
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),
|