Skip to content

Commit

Permalink
[IMP] Custom fields in accounting expressions
Browse files Browse the repository at this point in the history
fldp.quantity[]
  • Loading branch information
sbidoul committed Nov 11, 2024
1 parent bff3cd1 commit 9b56994
Showing 1 changed file with 60 additions and 28 deletions.
88 changes: 60 additions & 28 deletions mis_builder/models/aep.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ class AccountingExpressionProcessor:
MODE_UNALLOCATED = "u"

_ACC_RE = re.compile(
r"(?P<field>\bbal|\bpbal|\bnbal|\bcrd|\bdeb)"
r"(?P<field>\bbal|\bpbal|\bnbal|\bcrd|\bdeb|\bfld)"
r"(?P<mode>[piseu])?"
r"(?P<fld_name>\.[a-zA-Z0-9_]+)?"
r"\s*"
r"(?P<account_sel>_[a-zA-Z0-9]+|\[.*?\])"
r"\s*"
Expand Down Expand Up @@ -109,6 +110,8 @@ def __init__(self, companies, currency=None, account_model="account.account"):
# a first query to get the initial balance and another
# to get the variation, so it's a bit slower
self.smart_end = True
# custom field to query and sum
self._custom_fields = set()
# Account model
self._account_model = self.env[account_model].with_context(active_test=False)

Expand Down Expand Up @@ -137,12 +140,15 @@ def _parse_match_object(self, mo):
"datetime": datetime,
"dateutil": dateutil,
}
field, mode, account_sel, ml_domain = mo.groups()
field, mode, fld_name, account_sel, ml_domain = mo.groups()
# handle some legacy modes
if not mode:
mode = self.MODE_VARIATION
elif mode == "s":
mode = self.MODE_END
# custom fields
if fld_name:
fld_name = fld_name[1:] # strip leading dot
# convert account selector to account domain
if account_sel.startswith("_"):
# legacy bal_NNN%
Expand All @@ -165,7 +171,7 @@ def _parse_match_object(self, mo):
ml_domain = tuple(safe_eval(ml_domain, domain_eval_context))
else:
ml_domain = tuple()
return field, mode, acc_domain, ml_domain
return field, mode, fld_name, acc_domain, ml_domain

def parse_expr(self, expr):
"""Parse an expression, extracting accounting variables.
Expand All @@ -176,14 +182,16 @@ def parse_expr(self, expr):
and mode.
"""
for mo in self._ACC_RE.finditer(expr):
_, mode, acc_domain, ml_domain = self._parse_match_object(mo)
_, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
if mode == self.MODE_END and self.smart_end:
modes = (self.MODE_INITIAL, self.MODE_VARIATION, self.MODE_END)
else:
modes = (mode,)
for mode in modes:
key = (ml_domain, mode)
self._map_account_ids[key].add(acc_domain)
if fld_name:
self._custom_fields.add(fld_name)

def done_parsing(self):
"""Replace account domains by account ids in map"""
Expand All @@ -210,7 +218,7 @@ def get_account_ids_for_expr(self, expr):
"""
account_ids = set()
for mo in self._ACC_RE.finditer(expr):
field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
_, _, _, acc_domain, _ = self._parse_match_object(mo)
account_ids.update(self._account_ids_by_acc_domain[acc_domain])
return account_ids

Expand All @@ -224,7 +232,7 @@ def get_aml_domain_for_expr(self, expr, date_from, date_to, account_id=None):
aml_domains = []
date_domain_by_mode = {}
for mo in self._ACC_RE.finditer(expr):
field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
aml_domain = list(ml_domain)
account_ids = set()
account_ids.update(self._account_ids_by_acc_domain[acc_domain])
Expand All @@ -240,6 +248,8 @@ def get_aml_domain_for_expr(self, expr, date_from, date_to, account_id=None):
aml_domain.append(("credit", "<>", 0.0))
elif field == "deb":
aml_domain.append(("debit", "<>", 0.0))
elif fld_name:
aml_domain.append((fld_name, "!=", False))
aml_domains.append(expression.normalize_domain(aml_domain))
if mode not in date_domain_by_mode:
date_domain_by_mode[mode] = self.get_aml_domain_for_dates(
Expand Down Expand Up @@ -315,7 +325,7 @@ def do_queries(
aml_model = self.env[aml_model]
aml_model = aml_model.with_context(active_test=False)
company_rates = self._get_company_rates(date_to)
# {(domain, mode): {account_id: (debit, credit)}}
# {(domain, mode): {account_id: {debit: ..., credit: ..., *custom_fields)}}
self._data = defaultdict(dict)
domain_by_mode = {}
ends = []
Expand All @@ -338,7 +348,13 @@ def do_queries(
try:
accs = aml_model.read_group(
domain,
["debit", "credit", "account_id", "company_id"],
[
"debit",
"credit",
"account_id",
"company_id",
*self._custom_fields,
],
["account_id", "company_id"],
lazy=False,
)
Expand All @@ -364,19 +380,24 @@ def do_queries(
):
# in initial mode, ignore accounts with 0 balance
continue
self._data[key][acc["account_id"][0]] = (debit * rate, credit * rate)
entry = {"debit": debit * rate, "credit": credit * rate}
for field_name in self._custom_fields:
entry[field_name] = acc[field_name] or 0.0
self._data[key][acc["account_id"][0]] = entry
# compute ending balances by summing initial and variation
for key in ends:
domain, mode = key
initial_data = self._data[(domain, self.MODE_INITIAL)]
variation_data = self._data[(domain, self.MODE_VARIATION)]
account_ids = set(initial_data.keys()) | set(variation_data.keys())
for account_id in account_ids:
di, ci = initial_data.get(account_id, (AccountingNone, AccountingNone))
dv, cv = variation_data.get(
account_id, (AccountingNone, AccountingNone)
)
self._data[key][account_id] = (di + dv, ci + cv)
ientry = initial_data.get(account_id, {})
ventry = variation_data.get(account_id, {})
entry = {
f: ientry.get(f, AccountingNone) + ventry.get(f, AccountingNone)
for f in ("debit", "credit", *self._custom_fields)
}
self._data[key][account_id] = entry

def replace_expr(self, expr):
"""Replace accounting variables in an expression by their amount.
Expand All @@ -387,25 +408,30 @@ def replace_expr(self, expr):
"""

def f(mo):
field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
key = (ml_domain, mode)
account_ids_data = self._data[key]
v = AccountingNone
account_ids = self._account_ids_by_acc_domain[acc_domain]
for account_id in account_ids:
debit, credit = account_ids_data.get(
account_id, (AccountingNone, AccountingNone)
)
entry = account_ids_data.get(account_id, {})
debit = entry.get("debit", AccountingNone)
credit = entry.get("credit", AccountingNone)
if field == "bal":
v += debit - credit
elif field == "pbal" and debit >= credit:
v += debit - credit
elif field == "nbal" and debit < credit:
v += debit - credit
elif field == "pbal":
if debit >= credit:
v += debit - credit
elif field == "nbal":
if debit < credit:
v += debit - credit
elif field == "deb":
v += debit
elif field == "crd":
v += credit
else:
assert field == "fld"
v += entry.get(fld_name, AccountingNone)
# in initial balance mode, assume 0 is None
# as it does not make sense to distinguish 0 from "no data"
if (
Expand All @@ -428,17 +454,17 @@ def replace_exprs_by_account_id(self, exprs):
"""

def f(mo):
field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
field, mode, fld_name, acc_domain, ml_domain = self._parse_match_object(mo)
key = (ml_domain, mode)
# first check if account_id is involved in
# the current expression part
if account_id not in self._account_ids_by_acc_domain[acc_domain]:
return "(AccountingNone)"
# here we know account_id is involved in acc_domain
account_ids_data = self._data[key]
debit, credit = account_ids_data.get(
account_id, (AccountingNone, AccountingNone)
)
entry = account_ids_data.get(account_id, {})
debit = entry.get("debit", AccountingNone)
credit = entry.get("credit", AccountingNone)
if field == "bal":
v = debit - credit
elif field == "pbal":
Expand All @@ -455,6 +481,9 @@ def f(mo):
v = debit
elif field == "crd":
v = credit
else:
assert field == "fld"
v = entry.get(fld_name, AccountingNone)
# in initial balance mode, assume 0 is None
# as it does not make sense to distinguish 0 from "no data"
if (
Expand All @@ -468,7 +497,7 @@ def f(mo):
account_ids = set()
for expr in exprs:
for mo in self._ACC_RE.finditer(expr):
field, mode, acc_domain, ml_domain = self._parse_match_object(mo)
_, mode, _, acc_domain, ml_domain = self._parse_match_object(mo)
key = (ml_domain, mode)
account_ids_data = self._data[key]
for account_id in self._account_ids_by_acc_domain[acc_domain]:
Expand All @@ -488,7 +517,10 @@ def _get_balances(cls, mode, companies, date_from, date_to):
aep.parse_expr(expr)
aep.done_parsing()
aep.do_queries(date_from, date_to)
return aep._data[((), mode)]
return {
k: (v.get("debit", AccountingNone), v.get("credit", AccountingNone))
for k, v in aep._data[((), mode)].items()
}

@classmethod
def get_balances_initial(cls, companies, date):
Expand Down

0 comments on commit 9b56994

Please sign in to comment.