odoo-addon-mis-builder 16.0.5.1.13__py3-none-any.whl → 16.0.5.2.0.1__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.
@@ -73,9 +73,10 @@ msgstr ""
73
73
  #: model_terms:ir.ui.view,arch_db:mis_builder.mis_report_view_kpi_form
74
74
  msgid ""
75
75
  "<code>bal</code>, <code>crd</code>, <code>deb</code>, <code>\n"
76
- " pbal</code>, <code>nbal</code> : "
77
- "balance, debit, credit,\n"
78
- " positive balance, negative balance."
76
+ " pbal</code>, <code>nbal</code>, "
77
+ "<code>fld</code> : balance, debit, credit,\n"
78
+ " positive balance, negative balance,\n"
79
+ " other numerical field."
79
80
  msgstr ""
80
81
 
81
82
  #. module: mis_builder
@@ -1626,7 +1627,7 @@ msgstr ""
1626
1627
  msgid ""
1627
1628
  "The following special elements are recognized in the expressions\n"
1628
1629
  " to compute accounting data: <code>{bal|"
1629
- "crd|deb|pbal|nbal}{pieu}[account\n"
1630
+ "crd|deb|pbal|nbal|fld}{pieu}(.fieldname)[account\n"
1630
1631
  " selector][journal items domain]</code>."
1631
1632
  msgstr ""
1632
1633
 
@@ -1727,6 +1728,27 @@ msgstr ""
1727
1728
  msgid "You cannot sum period %s with itself."
1728
1729
  msgstr ""
1729
1730
 
1731
+ #. module: mis_builder
1732
+ #. odoo-python
1733
+ #: code:addons/mis_builder/models/aep.py:0
1734
+ #, python-format
1735
+ msgid "`%(field)s` cannot have a field name in expression %(expr)s"
1736
+ msgstr ""
1737
+
1738
+ #. module: mis_builder
1739
+ #. odoo-python
1740
+ #: code:addons/mis_builder/models/aep.py:0
1741
+ #, python-format
1742
+ msgid "`fld` can only be used with mode `p` (variation) in expression %s"
1743
+ msgstr ""
1744
+
1745
+ #. module: mis_builder
1746
+ #. odoo-python
1747
+ #: code:addons/mis_builder/models/aep.py:0
1748
+ #, python-format
1749
+ msgid "`fld` must have a field name in exression %s"
1750
+ msgstr ""
1751
+
1730
1752
  #. module: mis_builder
1731
1753
  #. odoo-python
1732
1754
  #: code:addons/mis_builder/models/mis_report_instance.py:0
@@ -1767,6 +1789,14 @@ msgstr "küçük"
1767
1789
  msgid "versus"
1768
1790
  msgstr ""
1769
1791
 
1792
+ #. module: mis_builder
1793
+ #: model_terms:ir.ui.view,arch_db:mis_builder.mis_report_view_kpi_form
1794
+ msgid ""
1795
+ "when <code>fld</code> is used : a field name specifier\n"
1796
+ " must be provided (e.g. <code>fldp."
1797
+ "quantity</code>"
1798
+ msgstr ""
1799
+
1770
1800
  #. module: mis_builder
1771
1801
  #: model:ir.model.fields.selection,name:mis_builder.selection__mis_report_style__font_size__x-large
1772
1802
  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.1
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