feat(configurator): fp.quote.configurator — pricing engine + SO creation

Add the core configurator model that collects part geometry, coating
config, and pricing inputs, calculates a price from matching pricing
rules (scored by specificity), and creates sale orders on confirmation.

- fp.quote.configurator model with mail.thread, sequence numbering
- Stored computed price with full breakdown HTML table
- Estimator override price support
- Auto-population from part catalog and coating config onchanges
- Surface area normalization (sq in/ft/cm/m)
- Specificity-scored rule matching (coating > substrate > cert level)
- action_create_quotation creates SO with FP-SERVICE product
- Form/list/search views with statusbar and chatter
- ACL: operator (read), estimator (read/write/create), manager (full)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-12 18:35:08 -04:00
parent 5143245f57
commit d3e2614620
4 changed files with 516 additions and 1 deletions

View File

@@ -1,2 +1,171 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo></odoo>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<!-- ===== Configurator Form View ===== -->
<record id="view_fp_quote_configurator_form" model="ir.ui.view">
<field name="name">fp.quote.configurator.form</field>
<field name="model">fp.quote.configurator</field>
<field name="arch" type="xml">
<form string="Quote Configurator">
<header>
<button name="action_create_quotation"
string="Create Quotation"
type="object"
class="btn-primary"
confirm="This will create a Sale Order from this configurator session. Continue?"
invisible="state != 'draft'"/>
<button name="action_cancel"
string="Cancel"
type="object"
invisible="state != 'draft'"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" readonly="1"/>
</h1>
</div>
<!-- Customer + Part / Coating + Quantity -->
<group>
<group string="Customer &amp; Part">
<field name="partner_id"/>
<field name="part_catalog_id"/>
</group>
<group string="Coating &amp; Quantity">
<field name="coating_config_id"/>
<field name="quantity"/>
<field name="batch_size"/>
</group>
</group>
<!-- Geometry / Options -->
<group>
<group string="Geometry">
<field name="surface_area"/>
<field name="surface_area_uom"/>
<field name="thickness_requested"/>
<field name="substrate_material"/>
</group>
<group string="Options">
<field name="complexity"/>
<field name="masking_zones"/>
<field name="rush_order"/>
<field name="turnaround_days"/>
</group>
</group>
<!-- Delivery / Fees -->
<group>
<group string="Delivery &amp; Fees">
<field name="delivery_method"/>
<field name="shipping_fee"/>
<field name="delivery_fee"/>
</group>
<group>
<field name="currency_id" invisible="1"/>
</group>
</group>
<separator string="Pricing"/>
<group>
<group>
<field name="calculated_price" widget="monetary" readonly="1"
class="fw-bold fs-4"/>
</group>
<group>
<field name="estimator_override_price" widget="monetary"/>
</group>
</group>
<group>
<field name="price_breakdown_html" readonly="1" nolabel="1" colspan="2"/>
</group>
<notebook>
<page string="Sale Order" name="sale_order">
<group>
<field name="sale_order_id" readonly="1"/>
</group>
</page>
<page string="Notes" name="notes">
<field name="notes" placeholder="Internal notes about this quote..."/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<!-- ===== Configurator List View ===== -->
<record id="view_fp_quote_configurator_list" model="ir.ui.view">
<field name="name">fp.quote.configurator.list</field>
<field name="model">fp.quote.configurator</field>
<field name="arch" type="xml">
<list string="Quote Configurators"
decoration-info="state == 'draft'"
decoration-muted="state == 'cancelled'"
default_order="create_date desc">
<field name="create_date" string="Date"/>
<field name="name"/>
<field name="partner_id"/>
<field name="coating_config_id"/>
<field name="surface_area"/>
<field name="quantity"/>
<field name="currency_id" column_invisible="1"/>
<field name="calculated_price"/>
<field name="estimator_override_price" string="Final Price"/>
<field name="state" widget="badge"
decoration-success="state == 'confirmed'"
decoration-info="state == 'draft'"
decoration-danger="state == 'cancelled'"/>
</list>
</field>
</record>
<!-- ===== Configurator Search View ===== -->
<record id="view_fp_quote_configurator_search" model="ir.ui.view">
<field name="name">fp.quote.configurator.search</field>
<field name="model">fp.quote.configurator</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="partner_id"/>
<field name="coating_config_id"/>
<separator/>
<filter string="Draft" name="draft" domain="[('state', '=', 'draft')]"/>
<filter string="Confirmed" name="confirmed" domain="[('state', '=', 'confirmed')]"/>
<filter string="Cancelled" name="cancelled" domain="[('state', '=', 'cancelled')]"/>
<group>
<filter string="Customer" name="group_customer" context="{'group_by': 'partner_id'}"/>
<filter string="Coating Config" name="group_coating" context="{'group_by': 'coating_config_id'}"/>
<filter string="Status" name="group_state" context="{'group_by': 'state'}"/>
</group>
</search>
</field>
</record>
<!-- ===== Window Action ===== -->
<record id="action_fp_quote_configurator" model="ir.actions.act_window">
<field name="name">Quote Configurator</field>
<field name="res_model">fp.quote.configurator</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_fp_quote_configurator_search"/>
<field name="context">{'search_default_draft': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new quote configurator session
</p>
<p>
Select a customer and coating configuration, enter part geometry,
and the pricing engine will calculate a quote. The estimator can
override the calculated price before creating a sale order.
</p>
</field>
</record>
</odoo>