Technical Update: CRM Stage Movement Reporting
Branch: crm_stage_movement_reporting, cut from updated_crm_strategy (carries the milestone date field commit)
Module: bista_menmd_si
Status: Complete: all requirements from the original ticket built, tested, and functioning, including all filter fields
Dependency
Built on top of the milestone date fields already on crm.lead (date_qualified, date_appointment_set, date_pitched, date_won, date_lost, date_disqualified) from the separate milestone tracking project, now merged into this same module/branch lineage.
What was built
1. models/crm_stage_movement_report.py: New SQL view model (_auto = False), unions all six milestone dates plus create_date (as "New") into one row-per-stage-event table. Powers Pipeline Progression and Rep Performance via a single pivot/list/graph view set (views/crm_stage_movement_report_views.xml), grouped/filtered by Salesperson, State, Month, Lead Source, and Pricelist Presented.
2. models/crm_lead.py (extended): Added reached_qualified / reached_appointment_set / reached_pitched / reached_won (stored integers, 0/1) for Conversion Funnel counts, and duration_new_to_qualified / duration_qualified_to_appt / duration_appt_to_pitched / duration_pitched_to_won (stored floats, group_operator='avg') for Opportunity Aging. Also added pricelist_presented_id (Many2one to product.pricelist) and pricelist_presented_date (Datetime, auto-stamped on first set, system-protected the same way the milestone dates are).
3. Row-level security: security/crm_stage_movement_report_security.xml, two ir.rule records mirroring crm.lead's existing Personal Leads / All Leads rules (Own Documents Only vs. All Documents). Verified via a real login test: a restricted user saw 6 records vs. 90 unrestricted: confirmed working as intended.
4. Dashboard widgets: Three Dynamic Dashboard SQL Query charts on the CRM dashboard: "Pitched vs Won", "Conversion Funnel by Stage" (four-transition percentage bar chart, custom SQL since the native KPI Percentage type has no way to set a per-transition denominator), and "Opportunity Aging by Stage" (avg days per transition). These are static SQL widgets, not filterable in the UI itself, they show the dashboard-level view only.
5. Conversion Funnel and Opportunity Aging pivot menus: views/crm_lead_funnel_aging_views.xml, real Odoo pivot/graph views on crm.lead under CRM > Reporting, filterable by Salesperson, State, and Pricelist Presented, distinct from the static dashboard charts above.
6. Pricelist Presented form fields: views/crm_lead_pricelist_presented_view_form.xml, adds the two new fields to the Opportunity form (anchored after the date_conversion field, not expected_revenue, see note below), so reps can set it directly while working a deal.
Scoped out during the build
- Territory field: originally planned (state→territory mapping model), but the requirement was updated to use State directly instead. The crm.territory.mapping model was scrapped before being committed: nothing left in the branch related to it.
- Historical backfill: not possible. Stage-change tracking wasn't capturing before the milestone fields went live today, so there's no underlying data to backfill from. Reporting is forward-only from August 20, 2026.
Bugs found and fixed during testing
- Form field placement: expected_revenue sits inside custom flex-styled title markup on the base Opportunity form, not a plain group. Anchoring position="after" there inserted successfully but rendered invisibly. Fixed by anchoring to date_conversion instead, a normal group field.
Known limitations worth flagging
- No company_id filtering on the report model: confirmed fine since this is a single-company instance, but worth a note if that ever changes.
- Current data volume is very small (a handful of test records moved through stages within minutes during today's testing): Conversion Funnel and Opportunity Aging numbers will look noisy/unrepresentative until real pipeline activity accumulates over the next few weeks.
- The three Dynamic Dashboard charts are static SQL, not filterable by the user. All filtering (including Pricelist Presented) is available on the pivot views under CRM > Reporting instead.
Files changed (this branch, on top of the milestone commit)
- models/crm_stage_movement_report.py (new)
- models/crm_lead.py (extended: reached_/duration_/pricelist_presented_* fields)
- models/__init__.py (import added)
- views/crm_stage_movement_report_views.xml (new)
- views/crm_lead_funnel_aging_views.xml (new)
- views/crm_lead_pricelist_presented_view_form.xml (new)
- security/crm_stage_movement_report_security.xml (new)
- security/ir.model.access.csv (access rule added)
- __manifest__.py (data entries added)
- 3 Dynamic Dashboard chart records (data, not code: configured directly in the UI)
Recommended next step
Ready for your review and merge. No outstanding gaps against the original ticket. One local-only item to check on the target environment before or after merge: the orphaned duplicate view noted above.