"""Order Book — pre-season booking coverage (the forward view sitting unused in sale.order). Giftware pre-books: confirmed orders carry FUTURE delivery windows (commitment_date), so the order book for the season is known months ahead. The curve compares THIS year's cumulative booked $ for season delivery against last year's same-week curve — 20% under with 12 weeks to go = cut the container program NOW; over = expand POs / push laggard agents. Season window: deliveries Aug 1 – Dec 31 (the Q4 giftware peak; env-overridable later if the business adds a spring season). x-axis = weeks before Oct 1 (nominal peak). """ import datetime as dt import core.odoo as O import core.periods as P SEASON_FROM = (8, 1) # deliveries from Aug 1 SEASON_TO = (12, 31) # through Dec 31 PEAK = (10, 1) # nominal peak for the weeks-before axis def _season_orders(year, team_id=None, cutoff=None): """Confirmed orders with commitment_date inside `year`'s season window, booked up to `cutoff` (order date). Returns [(order_date, amount)].""" ex = O.excluded_partner_ids() dom = [('state', 'in', ['sale', 'done']), ('commitment_date', '>=', f'{year}-{SEASON_FROM[0]:02d}-{SEASON_FROM[1]:02d} 00:00:00'), ('commitment_date', '<=', f'{year}-{SEASON_TO[0]:02d}-{SEASON_TO[1]:02d} 23:59:59')] dom.append(('team_id', '=', team_id) if team_id is not None else ('team_id', 'in', O.TEAM_IDS)) if cutoff: dom.append(('date_order', '<=', f'{cutoff} 23:59:59')) if ex: dom.append(('partner_id', 'not in', list(ex))) rows = O.search_read('sale.order', dom, ['date_order', 'amount_untaxed']) return [(str(r.get('date_order') or '')[:10], r.get('amount_untaxed') or 0.0) for r in rows if r.get('date_order')] def curve(team_id=None, t=None, years_back=2): """Cumulative booked-$ curves, one per season year, aligned on weeks-before-peak. The current year's curve stops at today; prior years run to their season end.""" t = t or P.today() ty = t.year out_rows, totals = [], {} for yr in range(ty - years_back, ty + 1): peak = dt.date(yr, *PEAK) cutoff = t.isoformat() if yr == ty else None orders = _season_orders(yr, team_id, cutoff) orders.sort() cum = 0.0 weekly = {} for d, amt in orders: cum += amt wk = (peak - dt.date.fromisoformat(d)).days // 7 weekly[wk] = cum # last write per week = cumulative at week end for wk, v in sorted(weekly.items(), reverse=True): out_rows.append({'year': str(yr), 'weeks_before_peak': -wk, 'booked': v}) totals[str(yr)] = cum # same-week-LY comparison for the headline wk_now = (dt.date(ty, *PEAK) - t).days // 7 ly_same = 0.0 for r in out_rows: if r['year'] == str(ty - 1) and r['weeks_before_peak'] <= -wk_now: ly_same = max(ly_same, r['booked']) return {'rows': out_rows, 'totals': totals, 'ty': str(ty), 'ly': str(ty - 1), 'booked_ty': totals.get(str(ty), 0.0), 'ly_same_week': ly_same, 'weeks_to_peak': wk_now} def by_category(team_id=None, t=None): """Booked $ per product category, this season TY vs LY-as-of-the-same-date. (read_group cannot group by a dot-path — group by product, map to category locally.)""" t = t or P.today() ty = t.year per_prod = {} for yr, cut in ((ty, t.isoformat()), (ty - 1, t.replace(year=ty - 1).isoformat())): ex = O.excluded_partner_ids() dom = [('order_id.state', 'in', ['sale', 'done']), ('order_id.commitment_date', '>=', f'{yr}-{SEASON_FROM[0]:02d}-{SEASON_FROM[1]:02d} 00:00:00'), ('order_id.commitment_date', '<=', f'{yr}-{SEASON_TO[0]:02d}-{SEASON_TO[1]:02d} 23:59:59'), ('order_id.date_order', '<=', f'{cut} 23:59:59'), ('product_id', '!=', False)] dom.append(('order_id.team_id', '=', team_id) if team_id is not None else ('order_id.team_id', 'in', O.TEAM_IDS)) if ex: dom.append(('order_partner_id', 'not in', list(ex))) for g in O.read_group('sale.order.line', dom, ['price_subtotal:sum'], ['product_id'], lazy=False): pid = O.m2o_id(g.get('product_id')) if not pid: continue e = per_prod.setdefault(pid, {'ty': 0.0, 'ly': 0.0}) e['ty' if yr == ty else 'ly'] += g.get('price_subtotal') or 0.0 cats = {} pids = list(per_prod) for i in range(0, len(pids), 2000): for p in O.search_read('product.product', [('id', 'in', pids[i:i + 2000]), ('active', 'in', [True, False])], ['categ_id']): cats[p['id']] = O.m2o_name(p.get('categ_id')) or '(none)' out = {} for pid, v in per_prod.items(): cat = cats.get(pid, '(none)') e = out.setdefault(cat, {'category': cat, 'ty': 0.0, 'ly': 0.0}) e['ty'] += v['ty'] e['ly'] += v['ly'] rows = list(out.values()) for r in rows: r['delta_pct'] = ((r['ty'] / r['ly'] - 1) * 100.0) if r['ly'] else None rows.sort(key=lambda x: -x['ty']) return rows def validate(t=None, team_id=None): """The client-side cumulative end point ties one server-side aggregate over the exact same domain (sum_field) — the independent arithmetic path.""" t = t or P.today() b = curve(team_id, t) ty = int(b['ty']) ex = O.excluded_partner_ids() dom = [('state', 'in', ['sale', 'done']), ('commitment_date', '>=', f'{ty}-{SEASON_FROM[0]:02d}-{SEASON_FROM[1]:02d} 00:00:00'), ('commitment_date', '<=', f'{ty}-{SEASON_TO[0]:02d}-{SEASON_TO[1]:02d} 23:59:59'), ('date_order', '<=', f'{t.isoformat()} 23:59:59')] dom.append(('team_id', '=', team_id) if team_id is not None else ('team_id', 'in', O.TEAM_IDS)) if ex: dom.append(('partner_id', 'not in', list(ex))) srv = O.sum_field('sale.order', dom, 'amount_untaxed') return [{'check': f'{ty} season booked $ — client cumulative vs server sum', 'a': round(b['booked_ty'], 2), 'b': round(srv, 2), 'gap': round(b['booked_ty'] - srv, 2), 'ok': abs(b['booked_ty'] - srv) < 1.0}]