Files
Odoo-Modules/fusion_accounting_bank_rec/services/pattern_extractor.py
2026-04-19 10:45:30 -04:00

75 lines
2.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Aggregate per-partner reconciliation patterns from precedent rows.
Computes typical amount range, cadence, preferred strategy, common memo
tokens. Output is a dict suitable for create/write on fusion.reconcile.pattern.
"""
from collections import Counter
from statistics import median
def extract_pattern_for_partner(env, *, company_id, partner_id) -> dict:
"""Compute the pattern aggregate for one (company, partner) pair.
Returns vals dict suitable for env['fusion.reconcile.pattern'].create()."""
Precedent = env['fusion.reconcile.precedent'].sudo()
precedents = Precedent.search([
('company_id', '=', company_id),
('partner_id', '=', partner_id),
], order='reconciled_at desc', limit=200)
if not precedents:
return {
'company_id': company_id,
'partner_id': partner_id,
'reconcile_count': 0,
}
amounts = sorted(precedents.mapped('amount'))
counts = precedents.mapped('matched_move_line_count')
single_count = sum(1 for c in counts if c == 1)
multi_count = sum(1 for c in counts if c > 1)
if multi_count > single_count:
pref_strategy = 'multi_invoice'
elif _amounts_concentrated(amounts):
pref_strategy = 'exact_amount'
else:
pref_strategy = 'fifo'
reconcile_dates = sorted([p.reconciled_at for p in precedents if p.reconciled_at])
if len(reconcile_dates) >= 2:
deltas = [(reconcile_dates[i+1] - reconcile_dates[i]).days
for i in range(len(reconcile_dates) - 1)]
cadence = sum(deltas) / len(deltas) if deltas else 0.0
else:
cadence = 0.0
token_counter = Counter()
for p in precedents:
if p.memo_tokens:
for tok in p.memo_tokens.split(','):
token_counter[tok.strip()] += 1
# Keep tokens appearing in >=30% of precedents (min floor of 2 occurrences)
threshold = max(2, len(precedents) * 0.3)
common_tokens = ','.join(t for t, c in token_counter.most_common() if c >= threshold)
return {
'company_id': company_id,
'partner_id': partner_id,
'reconcile_count': len(precedents),
'typical_amount_range': f"${min(amounts):,.2f} ${max(amounts):,.2f} (median ${median(amounts):,.2f})",
'typical_cadence_days': round(cadence, 1),
'pref_strategy': pref_strategy,
'common_memo_tokens': common_tokens,
}
def _amounts_concentrated(amounts: list[float]) -> bool:
"""True if amounts cluster around a few values (suggests exact-amount strategy)."""
if len(amounts) < 3:
return True
med = median(amounts)
within_5pct = sum(1 for a in amounts if abs(a - med) / max(med, 1) < 0.05)
return within_5pct / len(amounts) >= 0.6