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.
@@ -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:0905c54fc67af6b27dd21cebeffdfb327bf4c8f2179938b30b4943c499516ad5
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.12",
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"
@@ -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 <field><mode>[accounts][optional move line domain]
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
- _, mode, acc_domain, ml_domain = self._parse_match_object(mo)
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
- field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
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: (debit, credit)}}
319
- self._data = defaultdict(dict)
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
- ["debit", "credit", "account_id", "company_id"],
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
- 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
+ )
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
- di, ci = initial_data.get(account_id, (AccountingNone, AccountingNone))
376
- dv, cv = variation_data.get(
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
- debit, credit = account_ids_data.get(
397
- account_id, (AccountingNone, AccountingNone)
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" and debit >= credit:
402
- v += debit - credit
403
- elif field == "nbal" and debit < credit:
404
- 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
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
- debit, credit = account_ids_data.get(
440
- account_id, (AccountingNone, AccountingNone)
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
- field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
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 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:0905c54fc67af6b27dd21cebeffdfb327bf4c8f2179938b30b4943c499516ad5
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(
@@ -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(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
+ ):
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
- {"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
+ },
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
- >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.12.1
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:0905c54fc67af6b27dd21cebeffdfb327bf4c8f2179938b30b4943c499516ad5
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=cgfo7wi1O1awHvyFo2bghX6eF6xDXUrZSpUkaU1SkcY,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=wK1kE7ZxsBZ878w2URb3AVWL-KtNZ6iHSuP3Y-DkxQ8,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=hl7pG2SMt3ACSkcTZvHOL7gkNwJ1dAw5nonTadOVKpI,22493
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=r2jPgGJLH6l8NQoio2pL6G5qKSzR720j8IMkbe8L_m0,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=qvvx1jE-zQRZ8Vm-eDUBTSl_oR4ogM6DqDLelS8gjcI,15870
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.12.1.dist-info/METADATA,sha256=7QWU4pCInrqh_O8p-a2xYSFFaszN5fRe1RcHugFq5X8,30464
85
- odoo_addon_mis_builder-16.0.5.1.12.1.dist-info/WHEEL,sha256=9fEMia4zL7ZuZbnCOrcYogUhmn4XFIVaJ8G4YGI31xc,81
86
- odoo_addon_mis_builder-16.0.5.1.12.1.dist-info/top_level.txt,sha256=QE6RBQ0QX5f4eFuUcGgU5Kbq1A_qJcDs-e_vpr6pmfU,4
87
- odoo_addon_mis_builder-16.0.5.1.12.1.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,,