odoo-addon-mis-builder 16.0.5.1.13__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.
@@ -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:1379fb4aeb496d5e42c47ad6590f7fdcfa6a96557e74aac59cd960808a5f0383
10
+ !! source digest: sha256:2c8b76b489e5323dae1133488deb91d0bcbf86be319044b51399b56b566e3c8b
11
11
  !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12
12
 
13
13
  .. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
@@ -3,7 +3,7 @@
3
3
 
4
4
  {
5
5
  "name": "MIS Builder",
6
- "version": "16.0.5.1.13",
6
+ "version": "16.0.5.2.0",
7
7
  "category": "Reporting",
8
8
  "summary": """
9
9
  Build 'Management Information System' Reports and Dashboards
@@ -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"
@@ -12,7 +12,6 @@ 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
16
15
 
17
16
  _logger = logging.getLogger(__name__)
18
17
 
@@ -24,15 +23,65 @@ def _is_domain(s):
24
23
  return _DOMAIN_START_RE.match(s)
25
24
 
26
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
+
27
74
  class AccountingExpressionProcessor:
28
75
  """Processor for accounting expressions.
29
76
 
30
- Expressions of the form <field><mode>[accounts][optional move line domain]
77
+ Expressions of the form
78
+ <field><mode>(.fieldname)?[accounts][optional move line domain]
31
79
  are supported, where:
32
80
  * field is bal, crd, deb, pbal (positive balances only),
33
- nbal (negative balance only)
81
+ nbal (negative balance only), fld (custom field)
34
82
  * mode is i (initial balance), e (ending balance),
35
83
  p (moves over period)
84
+ * .fieldname is used only with fldp and specifies the field name to sum
36
85
  * there is also a special u mode (unallocated P&L) which computes
37
86
  the sum from the beginning until the beginning of the fiscal year
38
87
  of the period; it is only meaningful for P&L accounts
@@ -46,6 +95,7 @@ class AccountingExpressionProcessor:
46
95
  over the period (it is the same as balp[70]);
47
96
  * bali[70,60]: balance of accounts 70 and 60 at the start of period;
48
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
49
99
 
50
100
  How to use:
51
101
  * repeatedly invoke parse_expr() for each expression containing
@@ -77,8 +127,9 @@ class AccountingExpressionProcessor:
77
127
  MODE_UNALLOCATED = "u"
78
128
 
79
129
  _ACC_RE = re.compile(
80
- r"(?P<field>\bbal|\bpbal|\bnbal|\bcrd|\bdeb)"
130
+ r"(?P<field>\bbal|\bpbal|\bnbal|\bcrd|\bdeb|\bfld)"
81
131
  r"(?P<mode>[piseu])?"
132
+ r"(?P<fld_name>\.[a-zA-Z0-9_]+)?"
82
133
  r"\s*"
83
134
  r"(?P<account_sel>_[a-zA-Z0-9]+|\[.*?\])"
84
135
  r"\s*"
@@ -110,6 +161,8 @@ class AccountingExpressionProcessor:
110
161
  # a first query to get the initial balance and another
111
162
  # to get the variation, so it's a bit slower
112
163
  self.smart_end = True
164
+ # custom field to query and sum
165
+ self._custom_fields = set()
113
166
  # Account model
114
167
  self._account_model = self.env[account_model].with_context(active_test=False)
115
168
 
@@ -129,7 +182,7 @@ class AccountingExpressionProcessor:
129
182
  def _parse_match_object(self, mo):
130
183
  """Split a match object corresponding to an accounting variable
131
184
 
132
- Returns field, mode, account domain, move line domain.
185
+ Returns field, mode, fld_name, account domain, move line domain.
133
186
  """
134
187
  domain_eval_context = {
135
188
  "ref": self.env.ref,
@@ -138,12 +191,16 @@ class AccountingExpressionProcessor:
138
191
  "datetime": datetime,
139
192
  "dateutil": dateutil,
140
193
  }
141
- field, mode, account_sel, ml_domain = mo.groups()
194
+ field, mode, fld_name, account_sel, ml_domain = mo.groups()
142
195
  # handle some legacy modes
143
196
  if not mode:
144
197
  mode = self.MODE_VARIATION
145
198
  elif mode == "s":
146
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
147
204
  # convert account selector to account domain
148
205
  if account_sel.startswith("_"):
149
206
  # legacy bal_NNN%
@@ -166,7 +223,7 @@ class AccountingExpressionProcessor:
166
223
  ml_domain = tuple(safe_eval(ml_domain, domain_eval_context))
167
224
  else:
168
225
  ml_domain = tuple()
169
- return field, mode, acc_domain, ml_domain
226
+ return field, mode, fld_name, acc_domain, ml_domain
170
227
 
171
228
  def parse_expr(self, expr):
172
229
  """Parse an expression, extracting accounting variables.
@@ -177,7 +234,7 @@ class AccountingExpressionProcessor:
177
234
  and mode.
178
235
  """
179
236
  for mo in self._ACC_RE.finditer(expr):
180
- _, mode, acc_domain, ml_domain = self._parse_match_object(mo)
237
+ field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
181
238
  if mode == self.MODE_END and self.smart_end:
182
239
  modes = (self.MODE_INITIAL, self.MODE_VARIATION, self.MODE_END)
183
240
  else:
@@ -185,6 +242,30 @@ class AccountingExpressionProcessor:
185
242
  for mode in modes:
186
243
  key = (ml_domain, mode)
187
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
+ )
188
269
 
189
270
  def done_parsing(self):
190
271
  """Replace account domains by account ids in map"""
@@ -211,7 +292,7 @@ class AccountingExpressionProcessor:
211
292
  """
212
293
  account_ids = set()
213
294
  for mo in self._ACC_RE.finditer(expr):
214
- field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
295
+ _, _, _, acc_domain, _ = self._parse_match_object(mo)
215
296
  account_ids.update(self._account_ids_by_acc_domain[acc_domain])
216
297
  return account_ids
217
298
 
@@ -225,7 +306,7 @@ class AccountingExpressionProcessor:
225
306
  aml_domains = []
226
307
  date_domain_by_mode = {}
227
308
  for mo in self._ACC_RE.finditer(expr):
228
- 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)
229
310
  aml_domain = list(ml_domain)
230
311
  account_ids = set()
231
312
  account_ids.update(self._account_ids_by_acc_domain[acc_domain])
@@ -241,6 +322,8 @@ class AccountingExpressionProcessor:
241
322
  aml_domain.append(("credit", "<>", 0.0))
242
323
  elif field == "deb":
243
324
  aml_domain.append(("debit", "<>", 0.0))
325
+ elif fld_name:
326
+ aml_domain.append((fld_name, "!=", False))
244
327
  aml_domains.append(expression.normalize_domain(aml_domain))
245
328
  if mode not in date_domain_by_mode:
246
329
  date_domain_by_mode[mode] = self.get_aml_domain_for_dates(
@@ -316,10 +399,10 @@ class AccountingExpressionProcessor:
316
399
  aml_model = self.env[aml_model]
317
400
  aml_model = aml_model.with_context(active_test=False)
318
401
  company_rates = self._get_company_rates(date_to)
319
- # {(domain, mode): {account_id: (debit, credit)}}
402
+ # {(domain, mode): {account_id: Accumulator}}
320
403
  self._data = defaultdict(
321
404
  lambda: defaultdict(
322
- lambda: SimpleArray((AccountingNone, AccountingNone)),
405
+ lambda: Accumulator(self._custom_fields),
323
406
  )
324
407
  )
325
408
  domain_by_mode = {}
@@ -343,7 +426,13 @@ class AccountingExpressionProcessor:
343
426
  try:
344
427
  accs = aml_model.read_group(
345
428
  domain,
346
- ["debit", "credit", "account_id", "company_id"],
429
+ [
430
+ "debit",
431
+ "credit",
432
+ "account_id",
433
+ "company_id",
434
+ *self._custom_fields,
435
+ ],
347
436
  ["account_id", "company_id"],
348
437
  lazy=False,
349
438
  )
@@ -369,9 +458,15 @@ class AccountingExpressionProcessor:
369
458
  ):
370
459
  # in initial mode, ignore accounts with 0 balance
371
460
  continue
372
- # due to branches, it's possible to have multiple acc
373
- # with the same account_id
374
- self._data[key][acc["account_id"][0]] += (debit * rate, credit * rate)
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
+ )
375
470
  # compute ending balances by summing initial and variation
376
471
  for key in ends:
377
472
  domain, mode = key
@@ -379,11 +474,8 @@ class AccountingExpressionProcessor:
379
474
  variation_data = self._data[(domain, self.MODE_VARIATION)]
380
475
  account_ids = set(initial_data.keys()) | set(variation_data.keys())
381
476
  for account_id in account_ids:
382
- di, ci = initial_data.get(account_id, (AccountingNone, AccountingNone))
383
- dv, cv = variation_data.get(
384
- account_id, (AccountingNone, AccountingNone)
385
- )
386
- 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]
387
479
 
388
480
  def replace_expr(self, expr):
389
481
  """Replace accounting variables in an expression by their amount.
@@ -394,25 +486,30 @@ class AccountingExpressionProcessor:
394
486
  """
395
487
 
396
488
  def f(mo):
397
- 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)
398
490
  key = (ml_domain, mode)
399
491
  account_ids_data = self._data[key]
400
492
  v = AccountingNone
401
493
  account_ids = self._account_ids_by_acc_domain[acc_domain]
402
494
  for account_id in account_ids:
403
- debit, credit = account_ids_data.get(
404
- account_id, (AccountingNone, AccountingNone)
405
- )
495
+ entry = account_ids_data[account_id]
496
+ debit = entry.debit
497
+ credit = entry.credit
406
498
  if field == "bal":
407
499
  v += debit - credit
408
- elif field == "pbal" and debit >= credit:
409
- v += debit - credit
410
- elif field == "nbal" and debit < credit:
411
- v += debit - credit
500
+ elif field == "pbal":
501
+ if debit >= credit:
502
+ v += debit - credit
503
+ elif field == "nbal":
504
+ if debit < credit:
505
+ v += debit - credit
412
506
  elif field == "deb":
413
507
  v += debit
414
508
  elif field == "crd":
415
509
  v += credit
510
+ else:
511
+ assert field == "fld"
512
+ v += entry.custom_fields[fld_name]
416
513
  # in initial balance mode, assume 0 is None
417
514
  # as it does not make sense to distinguish 0 from "no data"
418
515
  if (
@@ -435,7 +532,7 @@ class AccountingExpressionProcessor:
435
532
  """
436
533
 
437
534
  def f(mo):
438
- 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)
439
536
  key = (ml_domain, mode)
440
537
  # first check if account_id is involved in
441
538
  # the current expression part
@@ -443,9 +540,9 @@ class AccountingExpressionProcessor:
443
540
  return "(AccountingNone)"
444
541
  # here we know account_id is involved in acc_domain
445
542
  account_ids_data = self._data[key]
446
- debit, credit = account_ids_data.get(
447
- account_id, (AccountingNone, AccountingNone)
448
- )
543
+ entry = account_ids_data[account_id]
544
+ debit = entry.debit
545
+ credit = entry.credit
449
546
  if field == "bal":
450
547
  v = debit - credit
451
548
  elif field == "pbal":
@@ -462,6 +559,9 @@ class AccountingExpressionProcessor:
462
559
  v = debit
463
560
  elif field == "crd":
464
561
  v = credit
562
+ else:
563
+ assert field == "fld"
564
+ v = entry.custom_fields[fld_name]
465
565
  # in initial balance mode, assume 0 is None
466
566
  # as it does not make sense to distinguish 0 from "no data"
467
567
  if (
@@ -475,7 +575,7 @@ class AccountingExpressionProcessor:
475
575
  account_ids = set()
476
576
  for expr in exprs:
477
577
  for mo in self._ACC_RE.finditer(expr):
478
- field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
578
+ _, mode, _, acc_domain, ml_domain = self._parse_match_object(mo)
479
579
  key = (ml_domain, mode)
480
580
  account_ids_data = self._data[key]
481
581
  for account_id in self._account_ids_by_acc_domain[acc_domain]:
@@ -495,7 +595,7 @@ class AccountingExpressionProcessor:
495
595
  aep.parse_expr(expr)
496
596
  aep.done_parsing()
497
597
  aep.do_queries(date_from, date_to)
498
- return aep._data[((), mode)]
598
+ return {k: (v.debit, v.credit) for k, v in aep._data[((), mode)].items()}
499
599
 
500
600
  @classmethod
501
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 arguments, got 0
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 arguments, got 0
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 arguments, got 0
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 arguments, got 0
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:1379fb4aeb496d5e42c47ad6590f7fdcfa6a96557e74aac59cd960808a5f0383
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&amp;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(
@@ -91,6 +96,7 @@ class TestAEP(common.TransactionCase):
91
96
  self.aep.parse_expr("crdp[700I%]")
92
97
  self.aep.parse_expr("bali[400%]")
93
98
  self.aep.parse_expr("bale[700%]")
99
+ self.aep.parse_expr("fldp.quantity[700%]")
94
100
  self.aep.parse_expr("balp[]" "[('account_id.code', '=', '400AR')]")
95
101
  self.aep.parse_expr(
96
102
  "balp[]" "[('account_id.account_type', '=', " " 'asset_receivable')]"
@@ -105,17 +111,32 @@ class TestAEP(common.TransactionCase):
105
111
  self.aep.parse_expr("bal_700IN") # deprecated
106
112
  self.aep.parse_expr("bals[700IN]") # deprecated
107
113
 
108
- def _create_move(self, date, amount, debit_acc, credit_acc, post=True):
114
+ def _create_move(
115
+ self, date, amount, debit_acc, credit_acc, post=True, credit_quantity=0
116
+ ):
109
117
  move = self.move_model.create(
110
118
  {
111
119
  "journal_id": self.journal.id,
112
120
  "date": fields.Date.to_string(date),
113
121
  "line_ids": [
114
- (0, 0, {"name": "/", "debit": amount, "account_id": debit_acc.id}),
115
122
  (
116
123
  0,
117
124
  0,
118
- {"name": "/", "credit": amount, "account_id": credit_acc.id},
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
+ },
119
140
  ),
120
141
  ],
121
142
  }
@@ -145,6 +166,20 @@ class TestAEP(common.TransactionCase):
145
166
  self.assertEqual(self.company.fiscalyear_last_day, 31)
146
167
  self.assertEqual(self.company.fiscalyear_last_month, "12")
147
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
+
148
183
  def test_aep_basic(self):
149
184
  self.aep.done_parsing()
150
185
  # let's query for december
@@ -196,6 +231,8 @@ class TestAEP(common.TransactionCase):
196
231
  self.assertEqual(self._eval("bale[700IN]"), -300)
197
232
  # check result for non existing account
198
233
  self.assertIs(self._eval("bale[700NA]"), AccountingNone)
234
+ # check fldp.quantity
235
+ self.assertEqual(self._eval("fldp.quantity[700%]"), 3)
199
236
 
200
237
  # let's query for March
201
238
  self._do_queries(
@@ -227,6 +264,8 @@ class TestAEP(common.TransactionCase):
227
264
  self.assertEqual(self._eval("debp[400A%]"), 500)
228
265
  self.assertEqual(self._eval("bal_700IN"), -500)
229
266
  self.assertEqual(self._eval("bals[700IN]"), -800)
267
+ # check fldp.quantity
268
+ self.assertEqual(self._eval("fldp.quantity[700%]"), 0)
230
269
 
231
270
  # unallocated p&l from previous year
232
271
  self.assertEqual(self._eval("balu[]"), -100)
@@ -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
- >nbal</code> : balance, debit, credit,
181
- positive balance, negative balance. </li>
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.1.13
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:1379fb4aeb496d5e42c47ad6590f7fdcfa6a96557e74aac59cd960808a5f0383
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=2UXjJo9FpSxVRruuu_F_L2lsRL1ZAa_xQMwG8hSz5PA,29747
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=id_TbwsZOZ2DjhJOHZBNS5efJZkAybxYeWOqHbGJbFY,1557
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=-pF8toPMa87vKcacFbgFT6QWneZqXhyhn36wGK8VGO4,62113
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=cwQHlOEemPaTh6SWqpg6dZqZAbQgAcWp2e245V0iSos,22768
25
- odoo/addons/mis_builder/models/aggregate.py,sha256=jLULkhMPh4FBJ9T8Al2Qf9uUwBjhS5t8W2J2tRrYQAA,2860
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=XVPBjtQQ_X7CuZjIyLjv5Y13avrm8sbgVFYsgV_CuPc,55964
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=VG6Wwn2VfguY9tn9AyeGfJrFTbSic-q-fF4A9cEPJMA,16304
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=pSc0-kg00Y-fR3WtSJd4OlBNG2U2pf6eV_w_ppbFgOA,15537
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.1.13.dist-info/METADATA,sha256=qj-W3x5VevtSBXlnq5W4AkDR4IgjsFavDOK_dKDBZvU,30462
85
- odoo_addon_mis_builder-16.0.5.1.13.dist-info/WHEEL,sha256=9fEMia4zL7ZuZbnCOrcYogUhmn4XFIVaJ8G4YGI31xc,81
86
- odoo_addon_mis_builder-16.0.5.1.13.dist-info/top_level.txt,sha256=QE6RBQ0QX5f4eFuUcGgU5Kbq1A_qJcDs-e_vpr6pmfU,4
87
- odoo_addon_mis_builder-16.0.5.1.13.dist-info/RECORD,,
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,,