"""Price compliance — selling below the customer's pricelist tier (pocket-price floor). Discount FIELDS are unused in this Odoo (verified dead), but reps can still key a low price_unit directly — leakage that is invisible today. Per (customer × SKU) over the LTM: realized unit price (Σ subtotal ÷ Σ qty — discount-true) vs the customer's applicable pricelist tier (fixed-price rules, qty break matched on the average order quantity). Exceptions below THRESHOLD of tier form the worklist, each valued at its annualized leak (= (tier − realized) × LTM qty). Pricing canon: ~1% realization ≈ 8-9% operating profit. v1 simplifications (documented, deliberate): - qty break matched on AVG order qty per (customer, SKU) — not per order line; - fixed-price rules only (this instance prices via fixed tiers, like the legacy SKU app); - LTM window, wholesale scope. """ import datetime as dt import core.odoo as O import core.periods as P import modules.customers as cust THRESHOLD = 0.97 # below 97% of tier = exception def _rules(): """All date-valid fixed-price pricelist rules, indexed by (pricelist, product-variant) and (pricelist, product-template). Highest min_quantity ≤ qty wins at match time.""" today = P.today().isoformat() rows = O.search_read( 'product.pricelist.item', [('compute_price', '=', 'fixed'), ('applied_on', 'in', ['0_product_variant', '1_product'])], ['pricelist_id', 'product_id', 'product_tmpl_id', 'min_quantity', 'fixed_price', 'date_start', 'date_end', 'applied_on']) ok = [] for r in rows: ds = str(r.get('date_start') or '')[:10] de = str(r.get('date_end') or '')[:10] if (ds and ds > today) or (de and de < today): continue ok.append(r) by_var, by_tmpl = {}, {} for r in ok: pl = O.m2o_id(r['pricelist_id']) if r['applied_on'] == '0_product_variant' and r.get('product_id'): by_var.setdefault((pl, O.m2o_id(r['product_id'])), []).append(r) elif r.get('product_tmpl_id'): by_tmpl.setdefault((pl, O.m2o_id(r['product_tmpl_id'])), []).append(r) return ok, by_var, by_tmpl def _tier_for(pl, pid, tmpl, qty, by_var, by_tmpl): """The applicable fixed tier price: variant rules beat template rules; within a scope, the highest qty break ≤ qty wins.""" for cands in (by_var.get((pl, pid)), by_tmpl.get((pl, tmpl))): if not cands: continue eligible = [r for r in cands if (r.get('min_quantity') or 0) <= max(qty, 1)] if eligible: best = max(eligible, key=lambda r: r.get('min_quantity') or 0) return best.get('fixed_price') or None return None def matched_pairs(team_id=None, t=None): """The shared substrate: every (customer × SKU) pair bought in the LTM with its applicable fixed tier attached (tier=None when the customer has no pricelist / no rule matches). Used by the compliance sweep here AND by the price corridor + pocket waterfall (modules/reprice.py) — one definition of 'realized vs tier' everywhere.""" t = t or P.today() lf, lt = P.ltm(t) # realized price per (customer, SKU) — one server-side aggregate g = O.read_group('sale.order.line', O.sale_line_domain(lf, lt, team_id), ['price_subtotal:sum', 'product_uom_qty:sum'], ['order_partner_id', 'product_id'], lazy=False) pairs = [] for r in g: pid = O.m2o_id(r.get('order_partner_id')) prod = O.m2o_id(r.get('product_id')) qty = r.get('product_uom_qty') or 0.0 rev = r.get('price_subtotal') or 0.0 n = r.get('__count') or 1 if pid and prod and qty > 0 and rev > 0: pairs.append({'pid': pid, 'customer': O.m2o_name(r.get('order_partner_id')), 'prod': prod, 'product': O.m2o_name(r.get('product_id')), 'qty': qty, 'rev': rev, 'lines': n, 'unit': rev / qty, 'avg_order_qty': qty / n}) # customer pricelists + product template/code lookups pids = list({p['pid'] for p in pairs}) plists = {} for i in range(0, len(pids), 2000): for r in O.search_read('res.partner', [('id', 'in', pids[i:i + 2000])], ['property_product_pricelist']): plists[r['id']] = O.m2o_id(r.get('property_product_pricelist')) prods = list({p['prod'] for p in pairs}) pinfo = {} for i in range(0, len(prods), 2000): for r in O.search_read('product.product', [('id', 'in', prods[i:i + 2000]), ('active', 'in', [True, False])], ['product_tmpl_id', 'default_code']): pinfo[r['id']] = {'tmpl': O.m2o_id(r.get('product_tmpl_id')), 'code': (r.get('default_code') or '').strip()} all_rules, by_var, by_tmpl = _rules() for p in pairs: info = pinfo.get(p['prod'], {}) p['sku'] = info.get('code', '') pl = plists.get(p['pid']) tier = (_tier_for(pl, p['prod'], info.get('tmpl'), p['avg_order_qty'], by_var, by_tmpl) if pl else None) p['tier'] = tier if (tier and tier > 0) else None return {'pairs': pairs, 'checked': len(pairs), 'n_rules': len(all_rules), 'window': (lf, lt)} def build(team_id=None, t=None, pre=None): """The compliance sweep. Returns dict(rows[exceptions], checked, matched, kpis…).""" mp = pre or matched_pairs(team_id, t) matched_list = [p for p in mp['pairs'] if p.get('tier')] exceptions = [p for p in matched_list if p['unit'] / p['tier'] < THRESHOLD] attrs = cust._partner_attrs(list({p['pid'] for p in exceptions})) rows = [] for p in exceptions: tier = p['tier'] rows.append({ 'pid': p['pid'], 'customer': p['customer'], 'sku': p['sku'], 'product': p['product'], 'agent': (attrs.get(p['pid']) or {}).get('agent', '(none)'), 'tier_price': tier, 'realized': p['unit'], 'pct_of_tier': p['unit'] / tier * 100.0, 'qty_ltm': p['qty'], 'rev_ltm': p['rev'], 'leak': (tier - p['unit']) * p['qty'], 'lines': p['lines'], }) rows.sort(key=lambda x: -x['leak']) return { 'rows': rows, 'checked': mp['checked'], 'matched': len(matched_list), 'n_rules': mp['n_rules'], 'leak_total': sum(r['leak'] for r in rows), 'exception_rate': (len(rows) / len(matched_list) * 100.0) if matched_list else 0.0, 'window': mp['window'], } def validate(t=None, team_id=None): """(1) rule pull complete vs search_count; (2) the TOP exception re-derived row-level: its realized unit price recomputed from its raw order lines (independent path) must match the read_group aggregate the sweep used.""" b = build(team_id, t) n = O.get_odoo().search_count( 'product.pricelist.item', [('compute_price', '=', 'fixed'), ('applied_on', 'in', ['0_product_variant', '1_product'])]) checks = [{'check': 'fixed pricelist rules — pull complete (pre date-filter)', 'a': b['n_rules'], 'b': f'<= {n}', 'gap': '', 'ok': b['n_rules'] <= n}] if b['rows']: top = b['rows'][0] lf, lt = b['window'] lines = O.search_read('sale.order.line', O.sale_line_domain(lf, lt, team_id, partner_ids=[top['pid']], extra=[('product_id.default_code', '=', top['sku'])]), ['price_subtotal', 'product_uom_qty']) qty = sum(l.get('product_uom_qty') or 0 for l in lines) rev = sum(l.get('price_subtotal') or 0 for l in lines) unit = rev / qty if qty else 0.0 checks.append({'check': f"top exception realized price — {top['sku']}", 'a': round(unit, 4), 'b': round(top['realized'], 4), 'gap': round(unit - top['realized'], 4), 'ok': abs(unit - top['realized']) < 0.01}) return checks