odoo-addon-mis-builder 16.0.5.1.12.1__py3-none-any.whl → 16.0.5.2.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 +1 -1
- odoo/addons/mis_builder/__manifest__.py +1 -1
- odoo/addons/mis_builder/i18n/mis_builder.pot +32 -3
- odoo/addons/mis_builder/models/aep.py +139 -32
- odoo/addons/mis_builder/models/aggregate.py +4 -4
- odoo/addons/mis_builder/static/description/index.html +1 -1
- odoo/addons/mis_builder/tests/test_aep.py +51 -4
- odoo/addons/mis_builder/views/mis_report.xml +9 -4
- {odoo_addon_mis_builder-16.0.5.1.12.1.dist-info → odoo_addon_mis_builder-16.0.5.2.0.dist-info}/METADATA +2 -2
- {odoo_addon_mis_builder-16.0.5.1.12.1.dist-info → odoo_addon_mis_builder-16.0.5.2.0.dist-info}/RECORD +12 -12
- {odoo_addon_mis_builder-16.0.5.1.12.1.dist-info → odoo_addon_mis_builder-16.0.5.2.0.dist-info}/WHEEL +0 -0
- {odoo_addon_mis_builder-16.0.5.1.12.1.dist-info → odoo_addon_mis_builder-16.0.5.2.0.dist-info}/top_level.txt +0 -0
@@ -7,7 +7,7 @@ MIS Builder
|
|
7
7
|
!! This file is generated by oca-gen-addon-readme !!
|
8
8
|
!! changes will be overwritten. !!
|
9
9
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
10
|
-
!! source digest: sha256:
|
10
|
+
!! source digest: sha256:2c8b76b489e5323dae1133488deb91d0bcbf86be319044b51399b56b566e3c8b
|
11
11
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
12
12
|
|
13
13
|
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
|
@@ -64,8 +64,9 @@ msgstr ""
|
|
64
64
|
#: model_terms:ir.ui.view,arch_db:mis_builder.mis_report_view_kpi_form
|
65
65
|
msgid ""
|
66
66
|
"<code>bal</code>, <code>crd</code>, <code>deb</code>, <code>\n"
|
67
|
-
" pbal</code>, <code>nbal</code> : balance, debit, credit,\n"
|
68
|
-
" positive balance, negative balance
|
67
|
+
" pbal</code>, <code>nbal</code>, <code>fld</code> : balance, debit, credit,\n"
|
68
|
+
" positive balance, negative balance,\n"
|
69
|
+
" other numerical field."
|
69
70
|
msgstr ""
|
70
71
|
|
71
72
|
#. module: mis_builder
|
@@ -1603,7 +1604,7 @@ msgstr ""
|
|
1603
1604
|
#: model_terms:ir.ui.view,arch_db:mis_builder.mis_report_view_kpi_form
|
1604
1605
|
msgid ""
|
1605
1606
|
"The following special elements are recognized in the expressions\n"
|
1606
|
-
" to compute accounting data: <code>{bal|crd|deb|pbal|nbal}{pieu}[account\n"
|
1607
|
+
" to compute accounting data: <code>{bal|crd|deb|pbal|nbal|fld}{pieu}(.fieldname)[account\n"
|
1607
1608
|
" selector][journal items domain]</code>."
|
1608
1609
|
msgstr ""
|
1609
1610
|
|
@@ -1704,6 +1705,27 @@ msgstr ""
|
|
1704
1705
|
msgid "You cannot sum period %s with itself."
|
1705
1706
|
msgstr ""
|
1706
1707
|
|
1708
|
+
#. module: mis_builder
|
1709
|
+
#. odoo-python
|
1710
|
+
#: code:addons/mis_builder/models/aep.py:0
|
1711
|
+
#, python-format
|
1712
|
+
msgid "`%(field)s` cannot have a field name in expression %(expr)s"
|
1713
|
+
msgstr ""
|
1714
|
+
|
1715
|
+
#. module: mis_builder
|
1716
|
+
#. odoo-python
|
1717
|
+
#: code:addons/mis_builder/models/aep.py:0
|
1718
|
+
#, python-format
|
1719
|
+
msgid "`fld` can only be used with mode `p` (variation) in expression %s"
|
1720
|
+
msgstr ""
|
1721
|
+
|
1722
|
+
#. module: mis_builder
|
1723
|
+
#. odoo-python
|
1724
|
+
#: code:addons/mis_builder/models/aep.py:0
|
1725
|
+
#, python-format
|
1726
|
+
msgid "`fld` must have a field name in exression %s"
|
1727
|
+
msgstr ""
|
1728
|
+
|
1707
1729
|
#. module: mis_builder
|
1708
1730
|
#. odoo-python
|
1709
1731
|
#: code:addons/mis_builder/models/mis_report_instance.py:0
|
@@ -1744,6 +1766,13 @@ msgstr ""
|
|
1744
1766
|
msgid "versus"
|
1745
1767
|
msgstr ""
|
1746
1768
|
|
1769
|
+
#. module: mis_builder
|
1770
|
+
#: model_terms:ir.ui.view,arch_db:mis_builder.mis_report_view_kpi_form
|
1771
|
+
msgid ""
|
1772
|
+
"when <code>fld</code> is used : a field name specifier\n"
|
1773
|
+
" must be provided (e.g. <code>fldp.quantity</code>"
|
1774
|
+
msgstr ""
|
1775
|
+
|
1747
1776
|
#. module: mis_builder
|
1748
1777
|
#: model:ir.model.fields.selection,name:mis_builder.selection__mis_report_style__font_size__x-large
|
1749
1778
|
msgid "x-large"
|
@@ -23,15 +23,65 @@ def _is_domain(s):
|
|
23
23
|
return _DOMAIN_START_RE.match(s)
|
24
24
|
|
25
25
|
|
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 add_debit_credit(self, debit, credit):
|
60
|
+
self.debit += debit
|
61
|
+
self.credit += credit
|
62
|
+
|
63
|
+
def add_custom_field(self, field, value):
|
64
|
+
self.custom_fields[field] += value
|
65
|
+
|
66
|
+
def __iadd__(self, other):
|
67
|
+
self.debit += other.debit
|
68
|
+
self.credit += other.credit
|
69
|
+
for field in self.custom_fields:
|
70
|
+
self.custom_fields[field] += other.custom_fields[field]
|
71
|
+
return self
|
72
|
+
|
73
|
+
|
26
74
|
class AccountingExpressionProcessor:
|
27
75
|
"""Processor for accounting expressions.
|
28
76
|
|
29
|
-
Expressions of the form
|
77
|
+
Expressions of the form
|
78
|
+
<field><mode>(.fieldname)?[accounts][optional move line domain]
|
30
79
|
are supported, where:
|
31
80
|
* field is bal, crd, deb, pbal (positive balances only),
|
32
|
-
nbal (negative balance only)
|
81
|
+
nbal (negative balance only), fld (custom field)
|
33
82
|
* mode is i (initial balance), e (ending balance),
|
34
83
|
p (moves over period)
|
84
|
+
* .fieldname is used only with fldp and specifies the field name to sum
|
35
85
|
* there is also a special u mode (unallocated P&L) which computes
|
36
86
|
the sum from the beginning until the beginning of the fiscal year
|
37
87
|
of the period; it is only meaningful for P&L accounts
|
@@ -45,6 +95,7 @@ class AccountingExpressionProcessor:
|
|
45
95
|
over the period (it is the same as balp[70]);
|
46
96
|
* bali[70,60]: balance of accounts 70 and 60 at the start of period;
|
47
97
|
* bale[1%]: balance of accounts starting with 1 at end of period.
|
98
|
+
* fldp.quantity[60%]: sum of the quantity field of moves on accounts 60
|
48
99
|
|
49
100
|
How to use:
|
50
101
|
* repeatedly invoke parse_expr() for each expression containing
|
@@ -76,8 +127,9 @@ class AccountingExpressionProcessor:
|
|
76
127
|
MODE_UNALLOCATED = "u"
|
77
128
|
|
78
129
|
_ACC_RE = re.compile(
|
79
|
-
r"(?P<field>\bbal|\bpbal|\bnbal|\bcrd|\bdeb)"
|
130
|
+
r"(?P<field>\bbal|\bpbal|\bnbal|\bcrd|\bdeb|\bfld)"
|
80
131
|
r"(?P<mode>[piseu])?"
|
132
|
+
r"(?P<fld_name>\.[a-zA-Z0-9_]+)?"
|
81
133
|
r"\s*"
|
82
134
|
r"(?P<account_sel>_[a-zA-Z0-9]+|\[.*?\])"
|
83
135
|
r"\s*"
|
@@ -109,6 +161,8 @@ class AccountingExpressionProcessor:
|
|
109
161
|
# a first query to get the initial balance and another
|
110
162
|
# to get the variation, so it's a bit slower
|
111
163
|
self.smart_end = True
|
164
|
+
# custom field to query and sum
|
165
|
+
self._custom_fields = set()
|
112
166
|
# Account model
|
113
167
|
self._account_model = self.env[account_model].with_context(active_test=False)
|
114
168
|
|
@@ -128,7 +182,7 @@ class AccountingExpressionProcessor:
|
|
128
182
|
def _parse_match_object(self, mo):
|
129
183
|
"""Split a match object corresponding to an accounting variable
|
130
184
|
|
131
|
-
Returns field, mode, account domain, move line domain.
|
185
|
+
Returns field, mode, fld_name, account domain, move line domain.
|
132
186
|
"""
|
133
187
|
domain_eval_context = {
|
134
188
|
"ref": self.env.ref,
|
@@ -137,12 +191,16 @@ class AccountingExpressionProcessor:
|
|
137
191
|
"datetime": datetime,
|
138
192
|
"dateutil": dateutil,
|
139
193
|
}
|
140
|
-
field, mode, account_sel, ml_domain = mo.groups()
|
194
|
+
field, mode, fld_name, account_sel, ml_domain = mo.groups()
|
141
195
|
# handle some legacy modes
|
142
196
|
if not mode:
|
143
197
|
mode = self.MODE_VARIATION
|
144
198
|
elif mode == "s":
|
145
199
|
mode = self.MODE_END
|
200
|
+
# custom fields
|
201
|
+
if fld_name:
|
202
|
+
assert fld_name[0] == "."
|
203
|
+
fld_name = fld_name[1:] # strip leading dot
|
146
204
|
# convert account selector to account domain
|
147
205
|
if account_sel.startswith("_"):
|
148
206
|
# legacy bal_NNN%
|
@@ -165,7 +223,7 @@ class AccountingExpressionProcessor:
|
|
165
223
|
ml_domain = tuple(safe_eval(ml_domain, domain_eval_context))
|
166
224
|
else:
|
167
225
|
ml_domain = tuple()
|
168
|
-
return field, mode, acc_domain, ml_domain
|
226
|
+
return field, mode, fld_name, acc_domain, ml_domain
|
169
227
|
|
170
228
|
def parse_expr(self, expr):
|
171
229
|
"""Parse an expression, extracting accounting variables.
|
@@ -176,7 +234,7 @@ class AccountingExpressionProcessor:
|
|
176
234
|
and mode.
|
177
235
|
"""
|
178
236
|
for mo in self._ACC_RE.finditer(expr):
|
179
|
-
|
237
|
+
field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
|
180
238
|
if mode == self.MODE_END and self.smart_end:
|
181
239
|
modes = (self.MODE_INITIAL, self.MODE_VARIATION, self.MODE_END)
|
182
240
|
else:
|
@@ -184,6 +242,30 @@ class AccountingExpressionProcessor:
|
|
184
242
|
for mode in modes:
|
185
243
|
key = (ml_domain, mode)
|
186
244
|
self._map_account_ids[key].add(acc_domain)
|
245
|
+
if field == "fld":
|
246
|
+
if mode != self.MODE_VARIATION:
|
247
|
+
raise UserError(
|
248
|
+
_(
|
249
|
+
"`fld` can only be used with mode `p` (variation) "
|
250
|
+
"in expression %s",
|
251
|
+
expr,
|
252
|
+
)
|
253
|
+
)
|
254
|
+
if not fld_name:
|
255
|
+
raise UserError(
|
256
|
+
_("`fld` must have a field name in exression %s", expr)
|
257
|
+
)
|
258
|
+
self._custom_fields.add(fld_name)
|
259
|
+
else:
|
260
|
+
if fld_name:
|
261
|
+
raise UserError(
|
262
|
+
_(
|
263
|
+
"`%(field)s` cannot have a field name "
|
264
|
+
"in expression %(expr)s",
|
265
|
+
field=field,
|
266
|
+
expr=expr,
|
267
|
+
)
|
268
|
+
)
|
187
269
|
|
188
270
|
def done_parsing(self):
|
189
271
|
"""Replace account domains by account ids in map"""
|
@@ -210,7 +292,7 @@ class AccountingExpressionProcessor:
|
|
210
292
|
"""
|
211
293
|
account_ids = set()
|
212
294
|
for mo in self._ACC_RE.finditer(expr):
|
213
|
-
|
295
|
+
_, _, _, acc_domain, _ = self._parse_match_object(mo)
|
214
296
|
account_ids.update(self._account_ids_by_acc_domain[acc_domain])
|
215
297
|
return account_ids
|
216
298
|
|
@@ -224,7 +306,7 @@ class AccountingExpressionProcessor:
|
|
224
306
|
aml_domains = []
|
225
307
|
date_domain_by_mode = {}
|
226
308
|
for mo in self._ACC_RE.finditer(expr):
|
227
|
-
field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
|
309
|
+
field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
|
228
310
|
aml_domain = list(ml_domain)
|
229
311
|
account_ids = set()
|
230
312
|
account_ids.update(self._account_ids_by_acc_domain[acc_domain])
|
@@ -240,6 +322,8 @@ class AccountingExpressionProcessor:
|
|
240
322
|
aml_domain.append(("credit", "<>", 0.0))
|
241
323
|
elif field == "deb":
|
242
324
|
aml_domain.append(("debit", "<>", 0.0))
|
325
|
+
elif fld_name:
|
326
|
+
aml_domain.append((fld_name, "!=", False))
|
243
327
|
aml_domains.append(expression.normalize_domain(aml_domain))
|
244
328
|
if mode not in date_domain_by_mode:
|
245
329
|
date_domain_by_mode[mode] = self.get_aml_domain_for_dates(
|
@@ -315,8 +399,12 @@ class AccountingExpressionProcessor:
|
|
315
399
|
aml_model = self.env[aml_model]
|
316
400
|
aml_model = aml_model.with_context(active_test=False)
|
317
401
|
company_rates = self._get_company_rates(date_to)
|
318
|
-
# {(domain, mode): {account_id:
|
319
|
-
self._data = defaultdict(
|
402
|
+
# {(domain, mode): {account_id: Accumulator}}
|
403
|
+
self._data = defaultdict(
|
404
|
+
lambda: defaultdict(
|
405
|
+
lambda: Accumulator(self._custom_fields),
|
406
|
+
)
|
407
|
+
)
|
320
408
|
domain_by_mode = {}
|
321
409
|
ends = []
|
322
410
|
for key in self._map_account_ids:
|
@@ -338,7 +426,13 @@ class AccountingExpressionProcessor:
|
|
338
426
|
try:
|
339
427
|
accs = aml_model.read_group(
|
340
428
|
domain,
|
341
|
-
[
|
429
|
+
[
|
430
|
+
"debit",
|
431
|
+
"credit",
|
432
|
+
"account_id",
|
433
|
+
"company_id",
|
434
|
+
*self._custom_fields,
|
435
|
+
],
|
342
436
|
["account_id", "company_id"],
|
343
437
|
lazy=False,
|
344
438
|
)
|
@@ -364,7 +458,15 @@ class AccountingExpressionProcessor:
|
|
364
458
|
):
|
365
459
|
# in initial mode, ignore accounts with 0 balance
|
366
460
|
continue
|
367
|
-
|
461
|
+
# due to branches, it's possible to have multiple groups
|
462
|
+
# with the same account_id, because multiple companies can
|
463
|
+
# use the same account
|
464
|
+
account_data = self._data[key][acc["account_id"][0]]
|
465
|
+
account_data.add_debit_credit(debit * rate, credit * rate)
|
466
|
+
for field_name in self._custom_fields:
|
467
|
+
account_data.add_custom_field(
|
468
|
+
field_name, acc[field_name] or AccountingNone
|
469
|
+
)
|
368
470
|
# compute ending balances by summing initial and variation
|
369
471
|
for key in ends:
|
370
472
|
domain, mode = key
|
@@ -372,11 +474,8 @@ class AccountingExpressionProcessor:
|
|
372
474
|
variation_data = self._data[(domain, self.MODE_VARIATION)]
|
373
475
|
account_ids = set(initial_data.keys()) | set(variation_data.keys())
|
374
476
|
for account_id in account_ids:
|
375
|
-
|
376
|
-
|
377
|
-
account_id, (AccountingNone, AccountingNone)
|
378
|
-
)
|
379
|
-
self._data[key][account_id] = (di + dv, ci + cv)
|
477
|
+
self._data[key][account_id] += initial_data[account_id]
|
478
|
+
self._data[key][account_id] += variation_data[account_id]
|
380
479
|
|
381
480
|
def replace_expr(self, expr):
|
382
481
|
"""Replace accounting variables in an expression by their amount.
|
@@ -387,25 +486,30 @@ class AccountingExpressionProcessor:
|
|
387
486
|
"""
|
388
487
|
|
389
488
|
def f(mo):
|
390
|
-
field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
|
489
|
+
field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
|
391
490
|
key = (ml_domain, mode)
|
392
491
|
account_ids_data = self._data[key]
|
393
492
|
v = AccountingNone
|
394
493
|
account_ids = self._account_ids_by_acc_domain[acc_domain]
|
395
494
|
for account_id in account_ids:
|
396
|
-
|
397
|
-
|
398
|
-
|
495
|
+
entry = account_ids_data[account_id]
|
496
|
+
debit = entry.debit
|
497
|
+
credit = entry.credit
|
399
498
|
if field == "bal":
|
400
499
|
v += debit - credit
|
401
|
-
elif field == "pbal"
|
402
|
-
|
403
|
-
|
404
|
-
|
500
|
+
elif field == "pbal":
|
501
|
+
if debit >= credit:
|
502
|
+
v += debit - credit
|
503
|
+
elif field == "nbal":
|
504
|
+
if debit < credit:
|
505
|
+
v += debit - credit
|
405
506
|
elif field == "deb":
|
406
507
|
v += debit
|
407
508
|
elif field == "crd":
|
408
509
|
v += credit
|
510
|
+
else:
|
511
|
+
assert field == "fld"
|
512
|
+
v += entry.custom_fields[fld_name]
|
409
513
|
# in initial balance mode, assume 0 is None
|
410
514
|
# as it does not make sense to distinguish 0 from "no data"
|
411
515
|
if (
|
@@ -428,7 +532,7 @@ class AccountingExpressionProcessor:
|
|
428
532
|
"""
|
429
533
|
|
430
534
|
def f(mo):
|
431
|
-
field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
|
535
|
+
field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
|
432
536
|
key = (ml_domain, mode)
|
433
537
|
# first check if account_id is involved in
|
434
538
|
# the current expression part
|
@@ -436,9 +540,9 @@ class AccountingExpressionProcessor:
|
|
436
540
|
return "(AccountingNone)"
|
437
541
|
# here we know account_id is involved in acc_domain
|
438
542
|
account_ids_data = self._data[key]
|
439
|
-
|
440
|
-
|
441
|
-
|
543
|
+
entry = account_ids_data[account_id]
|
544
|
+
debit = entry.debit
|
545
|
+
credit = entry.credit
|
442
546
|
if field == "bal":
|
443
547
|
v = debit - credit
|
444
548
|
elif field == "pbal":
|
@@ -455,6 +559,9 @@ class AccountingExpressionProcessor:
|
|
455
559
|
v = debit
|
456
560
|
elif field == "crd":
|
457
561
|
v = credit
|
562
|
+
else:
|
563
|
+
assert field == "fld"
|
564
|
+
v = entry.custom_fields[fld_name]
|
458
565
|
# in initial balance mode, assume 0 is None
|
459
566
|
# as it does not make sense to distinguish 0 from "no data"
|
460
567
|
if (
|
@@ -468,7 +575,7 @@ class AccountingExpressionProcessor:
|
|
468
575
|
account_ids = set()
|
469
576
|
for expr in exprs:
|
470
577
|
for mo in self._ACC_RE.finditer(expr):
|
471
|
-
|
578
|
+
_, mode, _, acc_domain, ml_domain = self._parse_match_object(mo)
|
472
579
|
key = (ml_domain, mode)
|
473
580
|
account_ids_data = self._data[key]
|
474
581
|
for account_id in self._account_ids_by_acc_domain[acc_domain]:
|
@@ -488,7 +595,7 @@ class AccountingExpressionProcessor:
|
|
488
595
|
aep.parse_expr(expr)
|
489
596
|
aep.done_parsing()
|
490
597
|
aep.do_queries(date_from, date_to)
|
491
|
-
return aep._data[((), mode)]
|
598
|
+
return {k: (v.debit, v.credit) for k, v in aep._data[((), mode)].items()}
|
492
599
|
|
493
600
|
@classmethod
|
494
601
|
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 1
|
67
|
+
TypeError: min expected at least 1 argument, got 0
|
68
68
|
>>> _min()
|
69
69
|
Traceback (most recent call last):
|
70
70
|
File "<stdin>", line 1, in ?
|
71
|
-
TypeError: min expected 1
|
71
|
+
TypeError: min expected at least 1 argument, 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 1
|
110
|
+
TypeError: max expected at least 1 argument, got 0
|
111
111
|
>>> _max()
|
112
112
|
Traceback (most recent call last):
|
113
113
|
File "<stdin>", line 1, in ?
|
114
|
-
TypeError: max expected 1
|
114
|
+
TypeError: max expected at least 1 argument, got 0
|
115
115
|
>>> max([])
|
116
116
|
Traceback (most recent call last):
|
117
117
|
File "<stdin>", line 1, in ?
|
@@ -367,7 +367,7 @@ ul.auto-toc {
|
|
367
367
|
!! This file is generated by oca-gen-addon-readme !!
|
368
368
|
!! changes will be overwritten. !!
|
369
369
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
370
|
-
!! source digest: sha256:
|
370
|
+
!! source digest: sha256:2c8b76b489e5323dae1133488deb91d0bcbf86be319044b51399b56b566e3c8b
|
371
371
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
372
372
|
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/mis-builder/tree/16.0/mis_builder"><img alt="OCA/mis-builder" src="https://img.shields.io/badge/github-OCA%2Fmis--builder-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/mis-builder-16-0/mis-builder-16-0-mis_builder"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/mis-builder&target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
373
373
|
<p>This module allows you to build Management Information Systems dashboards.
|
@@ -9,9 +9,13 @@ from odoo import fields
|
|
9
9
|
from odoo.exceptions import UserError
|
10
10
|
from odoo.tools.safe_eval import safe_eval
|
11
11
|
|
12
|
+
from ..models import aep
|
12
13
|
from ..models.accounting_none import AccountingNone
|
13
14
|
from ..models.aep import AccountingExpressionProcessor as AEP
|
14
15
|
from ..models.aep import _is_domain
|
16
|
+
from .common import load_doctests
|
17
|
+
|
18
|
+
load_tests = load_doctests(aep)
|
15
19
|
|
16
20
|
|
17
21
|
class TestAEP(common.TransactionCase):
|
@@ -66,6 +70,7 @@ class TestAEP(common.TransactionCase):
|
|
66
70
|
amount=300,
|
67
71
|
debit_acc=self.account_ar,
|
68
72
|
credit_acc=self.account_in,
|
73
|
+
credit_quantity=3,
|
69
74
|
)
|
70
75
|
# create move in March this year
|
71
76
|
self._create_move(
|
@@ -83,6 +88,7 @@ class TestAEP(common.TransactionCase):
|
|
83
88
|
self.aep.parse_expr("bali[700IN]")
|
84
89
|
self.aep.parse_expr("bale[700IN]")
|
85
90
|
self.aep.parse_expr("balp[700IN]")
|
91
|
+
self.aep.parse_expr("balp[700NA]") # account that does not exist
|
86
92
|
self.aep.parse_expr("bali[400AR]")
|
87
93
|
self.aep.parse_expr("bale[400AR]")
|
88
94
|
self.aep.parse_expr("balp[400AR]")
|
@@ -90,6 +96,7 @@ class TestAEP(common.TransactionCase):
|
|
90
96
|
self.aep.parse_expr("crdp[700I%]")
|
91
97
|
self.aep.parse_expr("bali[400%]")
|
92
98
|
self.aep.parse_expr("bale[700%]")
|
99
|
+
self.aep.parse_expr("fldp.quantity[700%]")
|
93
100
|
self.aep.parse_expr("balp[]" "[('account_id.code', '=', '400AR')]")
|
94
101
|
self.aep.parse_expr(
|
95
102
|
"balp[]" "[('account_id.account_type', '=', " " 'asset_receivable')]"
|
@@ -104,17 +111,32 @@ class TestAEP(common.TransactionCase):
|
|
104
111
|
self.aep.parse_expr("bal_700IN") # deprecated
|
105
112
|
self.aep.parse_expr("bals[700IN]") # deprecated
|
106
113
|
|
107
|
-
def _create_move(
|
114
|
+
def _create_move(
|
115
|
+
self, date, amount, debit_acc, credit_acc, post=True, credit_quantity=0
|
116
|
+
):
|
108
117
|
move = self.move_model.create(
|
109
118
|
{
|
110
119
|
"journal_id": self.journal.id,
|
111
120
|
"date": fields.Date.to_string(date),
|
112
121
|
"line_ids": [
|
113
|
-
(0, 0, {"name": "/", "debit": amount, "account_id": debit_acc.id}),
|
114
122
|
(
|
115
123
|
0,
|
116
124
|
0,
|
117
|
-
{
|
125
|
+
{
|
126
|
+
"name": "/",
|
127
|
+
"debit": amount,
|
128
|
+
"account_id": debit_acc.id,
|
129
|
+
},
|
130
|
+
),
|
131
|
+
(
|
132
|
+
0,
|
133
|
+
0,
|
134
|
+
{
|
135
|
+
"name": "/",
|
136
|
+
"credit": amount,
|
137
|
+
"account_id": credit_acc.id,
|
138
|
+
"quantity": credit_quantity,
|
139
|
+
},
|
118
140
|
),
|
119
141
|
],
|
120
142
|
}
|
@@ -144,6 +166,20 @@ class TestAEP(common.TransactionCase):
|
|
144
166
|
self.assertEqual(self.company.fiscalyear_last_day, 31)
|
145
167
|
self.assertEqual(self.company.fiscalyear_last_month, "12")
|
146
168
|
|
169
|
+
def test_parse_expr_error_handling(self):
|
170
|
+
aep = AEP(self.company)
|
171
|
+
with self.assertRaises(UserError) as cm:
|
172
|
+
aep.parse_expr("fldi.quantity[700%]")
|
173
|
+
self.assertIn(
|
174
|
+
"`fld` can only be used with mode `p` (variation)", str(cm.exception)
|
175
|
+
)
|
176
|
+
with self.assertRaises(UserError) as cm:
|
177
|
+
aep.parse_expr("fldp[700%]")
|
178
|
+
self.assertIn("`fld` must have a field name", str(cm.exception))
|
179
|
+
with self.assertRaises(UserError) as cm:
|
180
|
+
aep.parse_expr("balp.quantity[700%]")
|
181
|
+
self.assertIn("`bal` cannot have a field name", str(cm.exception))
|
182
|
+
|
147
183
|
def test_aep_basic(self):
|
148
184
|
self.aep.done_parsing()
|
149
185
|
# let's query for december
|
@@ -193,6 +229,10 @@ class TestAEP(common.TransactionCase):
|
|
193
229
|
# check ending balance
|
194
230
|
self.assertEqual(self._eval("bale[400AR]"), 400)
|
195
231
|
self.assertEqual(self._eval("bale[700IN]"), -300)
|
232
|
+
# check result for non existing account
|
233
|
+
self.assertIs(self._eval("bale[700NA]"), AccountingNone)
|
234
|
+
# check fldp.quantity
|
235
|
+
self.assertEqual(self._eval("fldp.quantity[700%]"), 3)
|
196
236
|
|
197
237
|
# let's query for March
|
198
238
|
self._do_queries(
|
@@ -224,12 +264,19 @@ class TestAEP(common.TransactionCase):
|
|
224
264
|
self.assertEqual(self._eval("debp[400A%]"), 500)
|
225
265
|
self.assertEqual(self._eval("bal_700IN"), -500)
|
226
266
|
self.assertEqual(self._eval("bals[700IN]"), -800)
|
267
|
+
# check fldp.quantity
|
268
|
+
self.assertEqual(self._eval("fldp.quantity[700%]"), 0)
|
227
269
|
|
228
270
|
# unallocated p&l from previous year
|
229
271
|
self.assertEqual(self._eval("balu[]"), -100)
|
230
|
-
|
231
272
|
# TODO allocate profits, and then...
|
232
273
|
|
274
|
+
# let's query for December where there is no data
|
275
|
+
self._do_queries(
|
276
|
+
datetime.date(self.curr_year, 12, 1), datetime.date(self.curr_year, 12, 31)
|
277
|
+
)
|
278
|
+
self.assertIs(self._eval("balp[700IN]"), AccountingNone)
|
279
|
+
|
233
280
|
def test_aep_by_account(self):
|
234
281
|
self.aep.done_parsing()
|
235
282
|
self._do_queries(
|
@@ -170,19 +170,24 @@
|
|
170
170
|
<p
|
171
171
|
> The following special elements are recognized in the expressions
|
172
172
|
to compute accounting data: <code
|
173
|
-
>{bal|crd|deb|pbal|nbal}{pieu}[account
|
173
|
+
>{bal|crd|deb|pbal|nbal|fld}{pieu}(.fieldname)[account
|
174
174
|
selector][journal items domain]</code>. </p>
|
175
175
|
<ul>
|
176
176
|
<li>
|
177
177
|
<code>bal</code>, <code>crd</code>, <code
|
178
178
|
>deb</code>, <code>
|
179
|
-
pbal</code>, <code
|
180
|
-
>
|
181
|
-
positive balance, negative balance
|
179
|
+
pbal</code>, <code>nbal</code>, <code
|
180
|
+
>fld</code> : balance, debit, credit,
|
181
|
+
positive balance, negative balance,
|
182
|
+
other numerical field. </li>
|
182
183
|
<li>
|
183
184
|
<code>p</code>, <code>i</code>, <code
|
184
185
|
>e</code> : respectively variation over the period,
|
185
186
|
initial balance, ending balance </li>
|
187
|
+
<li>when <code
|
188
|
+
>fld</code> is used : a field name specifier
|
189
|
+
must be provided (e.g. <code
|
190
|
+
>fldp.quantity</code></li>
|
186
191
|
<li> The <b
|
187
192
|
>account selector</b> is a like expression on the
|
188
193
|
account code (eg <code
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: odoo-addon-mis_builder
|
3
|
-
Version: 16.0.5.
|
3
|
+
Version: 16.0.5.2.0
|
4
4
|
Requires-Python: >=3.10
|
5
5
|
Requires-Dist: odoo-addon-date_range>=16.0dev,<16.1dev
|
6
6
|
Requires-Dist: odoo-addon-report_xlsx>=16.0dev,<16.1dev
|
@@ -25,7 +25,7 @@ MIS Builder
|
|
25
25
|
!! This file is generated by oca-gen-addon-readme !!
|
26
26
|
!! changes will be overwritten. !!
|
27
27
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
28
|
-
!! source digest: sha256:
|
28
|
+
!! source digest: sha256:2c8b76b489e5323dae1133488deb91d0bcbf86be319044b51399b56b566e3c8b
|
29
29
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
30
30
|
|
31
31
|
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
|
@@ -1,6 +1,6 @@
|
|
1
|
-
odoo/addons/mis_builder/README.rst,sha256=
|
1
|
+
odoo/addons/mis_builder/README.rst,sha256=cex0nZCQC3_G89UAQr0diGkhEKrnekmbHMHyahqu0Ck,29747
|
2
2
|
odoo/addons/mis_builder/__init__.py,sha256=-8wG-57WxKfMMA5_TFzpubBcfthcsAP5vjTkhXKf7ds,185
|
3
|
-
odoo/addons/mis_builder/__manifest__.py,sha256=
|
3
|
+
odoo/addons/mis_builder/__manifest__.py,sha256=nK2bf1OJhoQD7PKtyEeGQBYg_gTaKFIDIc8POvwf--I,1556
|
4
4
|
odoo/addons/mis_builder/datas/ir_cron.xml,sha256=H5nnz-4CqZPVveJdRoJ_DrBOOsihK4XwOjWcSMYv68A,558
|
5
5
|
odoo/addons/mis_builder/i18n/ca.po,sha256=_hw-pIoOudeREKA5F6I3ZzNFoi5ScbAjt-n8w-K0-TI,89958
|
6
6
|
odoo/addons/mis_builder/i18n/de.po,sha256=N_3JJJ-4ofgF3CyPVcRmBu--FS9OlNcauLwrzB1R7Ok,65267
|
@@ -10,7 +10,7 @@ odoo/addons/mis_builder/i18n/es.po,sha256=Xa_UQEBXkHapuBf8iQeKTD4JyAAGgKzeov7N5O
|
|
10
10
|
odoo/addons/mis_builder/i18n/fr.po,sha256=Na0W0U_mP8bi8tedcvzn8LPglIacwH7GR1K82xrEyUk,82680
|
11
11
|
odoo/addons/mis_builder/i18n/hr.po,sha256=t7Z4FbdbdkDXRFKqhVNz2dNnabwv1725hJoG8VMrAvE,67345
|
12
12
|
odoo/addons/mis_builder/i18n/it.po,sha256=5DDJtgum-XgFaLX6IFm1omM1JdKUE2PEzZUkUqZVKMo,76184
|
13
|
-
odoo/addons/mis_builder/i18n/mis_builder.pot,sha256
|
13
|
+
odoo/addons/mis_builder/i18n/mis_builder.pot,sha256=aO4Ce34mTfRmLpCjpYfUWRa7uqXWBNBtLThScqz-R0M,63001
|
14
14
|
odoo/addons/mis_builder/i18n/nl.po,sha256=2xbRSRD-PZAQZC54RwlKQUIBFsGdGdfsfbb_EWqg2Nw,74688
|
15
15
|
odoo/addons/mis_builder/i18n/nl_NL.po,sha256=d3RKauD0_rcouw1Na_BOAdceBniIXRG414iGfkgTbGo,74780
|
16
16
|
odoo/addons/mis_builder/i18n/pt.po,sha256=2wzm81ssyHtsfrZCNcM8PibuY8J8VgfoizvNM74NbX4,64056
|
@@ -21,8 +21,8 @@ odoo/addons/mis_builder/migrations/16.0.1.0.1/post-migration.py,sha256=kSvTOKc5R
|
|
21
21
|
odoo/addons/mis_builder/migrations/16.0.5.0.0/end-migrate.py,sha256=YquWWej_lW3mhkYTSQPqEC_BucmxtLRgwGWU1My7fo8,326
|
22
22
|
odoo/addons/mis_builder/models/__init__.py,sha256=_h9tbt-H0JjtHL6kI7-wiEnd5u2qkP855DevyjxLBbI,331
|
23
23
|
odoo/addons/mis_builder/models/accounting_none.py,sha256=6EE6x4slUbyP2TVnHpilsWu8XqWhGZMt79uBOmNLcVs,4156
|
24
|
-
odoo/addons/mis_builder/models/aep.py,sha256=
|
25
|
-
odoo/addons/mis_builder/models/aggregate.py,sha256=
|
24
|
+
odoo/addons/mis_builder/models/aep.py,sha256=hPO7mVTfYLKSvexEBWRDE8hWvod6KwzyobSg2k4r69U,26306
|
25
|
+
odoo/addons/mis_builder/models/aggregate.py,sha256=ffOZq2-R7st-1Hsw-lFfiwkMa46ToJZ7m0K6Z9P_gwQ,2892
|
26
26
|
odoo/addons/mis_builder/models/data_error.py,sha256=ltTIrFCbsxYXmdPTVFVJj3ejZxyc83NEsIFQs2s3-L0,442
|
27
27
|
odoo/addons/mis_builder/models/expression_evaluator.py,sha256=honBPA9VOg0q_BOPuB8CkMA_wcoMUMxDbB-ULnjRpb4,2459
|
28
28
|
odoo/addons/mis_builder/models/kpimatrix.py,sha256=otTvqqvLOINVL1jbudn2PhvWwURDtmsoNN5TzM0hIwI,19812
|
@@ -53,7 +53,7 @@ odoo/addons/mis_builder/static/description/ex_report_preview.png,sha256=NKBRn70E
|
|
53
53
|
odoo/addons/mis_builder/static/description/ex_report_settings.png,sha256=T7j_3DUUtdZ6XKsAvJ3uKsK_-GtRl0sRPD68ZOa6ebA,103790
|
54
54
|
odoo/addons/mis_builder/static/description/ex_report_template.png,sha256=c8KgzyGD5QiZNCmxaVR4u3h1K0TJN50uXZWoNsieN8A,100278
|
55
55
|
odoo/addons/mis_builder/static/description/icon.png,sha256=0OCahdqDvaS_CHV97ZCbajW0_AcBLVVvja_EJA2mC6s,4770
|
56
|
-
odoo/addons/mis_builder/static/description/index.html,sha256=
|
56
|
+
odoo/addons/mis_builder/static/description/index.html,sha256=ctFlUJJKjeQ17RuWqBGKJDBwRzAL66wHR0vEWSpPzMc,55964
|
57
57
|
odoo/addons/mis_builder/static/src/components/mis_report_widget.css,sha256=ew7iozGAv9ue3KPf_8qA_AhHlNGis7pJu8A7tc6CO4Y,1198
|
58
58
|
odoo/addons/mis_builder/static/src/components/mis_report_widget.esm.js,sha256=UzKMeuffo1BT5VmOi0Um9xtplbvdjcrc-_KOkZrTc_A,5528
|
59
59
|
odoo/addons/mis_builder/static/src/components/mis_report_widget.xml,sha256=eKbIbOWmsc_qdT_zYuFdqxZIYpf6JEQGTcXE0vcLax4,5679
|
@@ -62,7 +62,7 @@ odoo/addons/mis_builder/tests/__init__.py,sha256=ilgojed90T4NL_YXgVf3CACd8fo7nxp
|
|
62
62
|
odoo/addons/mis_builder/tests/common.py,sha256=7LqnpPyJ8yOi5gA4CwiKZaep6NGY_vIDnK36u8WVC1o,1918
|
63
63
|
odoo/addons/mis_builder/tests/fake_models.py,sha256=DWJsXiIVVXZwXfLQ-9TmUs3eM5KBCXibS2qRXgkFt1c,177
|
64
64
|
odoo/addons/mis_builder/tests/test_accounting_none.py,sha256=satnhbRuI4DdWIOpe-mG7sW9KlamgnyvoVaPQlqZ9FA,239
|
65
|
-
odoo/addons/mis_builder/tests/test_aep.py,sha256=
|
65
|
+
odoo/addons/mis_builder/tests/test_aep.py,sha256=o931AKWU-VlPGNMocslyMCng8k7JZ5pkPoJdyykkGQM,17701
|
66
66
|
odoo/addons/mis_builder/tests/test_aggregate.py,sha256=ZXEVW0hMZYUIJM6ikQpmBH9Xc9TaY46xYbBsH8vcXDQ,226
|
67
67
|
odoo/addons/mis_builder/tests/test_data_sources.py,sha256=ao0FFFxvgvDfGZJzQ_ErTIRc8HM9JD_-8nr8OGeSu_g,7461
|
68
68
|
odoo/addons/mis_builder/tests/test_kpi_data.py,sha256=PrgfJIkrw5liJ8InLoXCGXMD-uc7y23J-7R-UYxWIO8,4860
|
@@ -75,13 +75,13 @@ odoo/addons/mis_builder/tests/test_simple_array.py,sha256=PHzSr_vb4Wn7dfwjaFSAlu
|
|
75
75
|
odoo/addons/mis_builder/tests/test_subreport.py,sha256=I7tQdhjaei15lzhXQLrbL80mlFtAyeoV-gHcs6xaEUo,3202
|
76
76
|
odoo/addons/mis_builder/tests/test_target_move.py,sha256=Jw7F9QMHVekOr-hoAy6kKCNcYAZAmfjOUbqcgjPw9Ps,1296
|
77
77
|
odoo/addons/mis_builder/tests/test_utc_midnight.py,sha256=mET_qOOC3qsOJ4MK_yTRocicW_NvKdRhjtpVJjLYaaU,917
|
78
|
-
odoo/addons/mis_builder/views/mis_report.xml,sha256=
|
78
|
+
odoo/addons/mis_builder/views/mis_report.xml,sha256=9iacm2GDBaINfypwgb3mxQYkz-412g3eku3E3bhrWbY,15906
|
79
79
|
odoo/addons/mis_builder/views/mis_report_instance.xml,sha256=dhOu5S7NwT4Yf5xSTOF0gcliwJN77fQTPJhqd4zpSaU,19213
|
80
80
|
odoo/addons/mis_builder/views/mis_report_style.xml,sha256=HRsXhEZZYbuZg7qJyfhdnERiTplX7uaN3e0dMunGoD0,4896
|
81
81
|
odoo/addons/mis_builder/wizard/__init__.py,sha256=qlc_LcwA6U3Wgv-qC9uacPjZXV3jShrV_RBiuKCBNPA,158
|
82
82
|
odoo/addons/mis_builder/wizard/mis_builder_dashboard.py,sha256=DIas3iie9fb2WzJ2eGcrVc7y64IuRyxSG7tJrcV-53g,3404
|
83
83
|
odoo/addons/mis_builder/wizard/mis_builder_dashboard.xml,sha256=ArG86hdyRWm531nK0hYU-j1N87yXtpDVpSuKTmOB47I,1440
|
84
|
-
odoo_addon_mis_builder-16.0.5.
|
85
|
-
odoo_addon_mis_builder-16.0.5.
|
86
|
-
odoo_addon_mis_builder-16.0.5.
|
87
|
-
odoo_addon_mis_builder-16.0.5.
|
84
|
+
odoo_addon_mis_builder-16.0.5.2.0.dist-info/METADATA,sha256=HyBplR9zkov3Lk6w8HoikWWzSjvzNKDbjSMs2s8Kk_I,30461
|
85
|
+
odoo_addon_mis_builder-16.0.5.2.0.dist-info/WHEEL,sha256=9fEMia4zL7ZuZbnCOrcYogUhmn4XFIVaJ8G4YGI31xc,81
|
86
|
+
odoo_addon_mis_builder-16.0.5.2.0.dist-info/top_level.txt,sha256=QE6RBQ0QX5f4eFuUcGgU5Kbq1A_qJcDs-e_vpr6pmfU,4
|
87
|
+
odoo_addon_mis_builder-16.0.5.2.0.dist-info/RECORD,,
|
{odoo_addon_mis_builder-16.0.5.1.12.1.dist-info → odoo_addon_mis_builder-16.0.5.2.0.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|