feat(configurator): fp.pricing.rule — formula-based pricing engine with complexity surcharges
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,3 +6,6 @@
|
||||
from . import fp_treatment
|
||||
from . import fp_part_catalog
|
||||
from . import fp_coating_config
|
||||
from . import fp_pricing_complexity_surcharge
|
||||
from . import fp_pricing_rule
|
||||
from . import sale_order
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FpPricingComplexitySurcharge(models.Model):
|
||||
"""Complexity-based surcharge line on a pricing rule."""
|
||||
_name = 'fp.pricing.complexity.surcharge'
|
||||
_description = 'Fusion Plating — Pricing Complexity Surcharge'
|
||||
_order = 'complexity'
|
||||
|
||||
rule_id = fields.Many2one('fp.pricing.rule', string='Pricing Rule', required=True, ondelete='cascade')
|
||||
complexity = fields.Selection(
|
||||
[('simple', 'Simple'), ('moderate', 'Moderate'), ('complex', 'Complex'), ('very_complex', 'Very Complex')],
|
||||
string='Complexity', required=True,
|
||||
)
|
||||
surcharge_percent = fields.Float(string='Surcharge %', help='Additional percentage on top of base price.')
|
||||
|
||||
_sql_constraints = [
|
||||
('fp_pricing_surcharge_rule_complexity_uniq', 'unique(rule_id, complexity)',
|
||||
'Only one surcharge per complexity level per rule.'),
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
# Part of the Fusion Plating product family.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class FpPricingRule(models.Model):
|
||||
"""Formula-based pricing rule.
|
||||
|
||||
Rules are matched by coating config, substrate material, and
|
||||
certification level. The first matching rule (by sequence) wins.
|
||||
Global rules (no filters set) act as fallbacks.
|
||||
"""
|
||||
_name = 'fp.pricing.rule'
|
||||
_description = 'Fusion Plating — Pricing Rule'
|
||||
_order = 'sequence, id'
|
||||
|
||||
name = fields.Char(string='Rule Name', required=True)
|
||||
coating_config_id = fields.Many2one('fp.coating.config', string='Coating Config',
|
||||
help='Leave blank for a global rule.')
|
||||
substrate_material = fields.Selection(
|
||||
[('aluminium', 'Aluminium'), ('steel', 'Steel'), ('stainless', 'Stainless Steel'),
|
||||
('copper', 'Copper'), ('titanium', 'Titanium'), ('other', 'Other')],
|
||||
string='Substrate Material', help='Leave blank to match all materials.',
|
||||
)
|
||||
certification_level = fields.Selection(
|
||||
[('commercial', 'Commercial'), ('mil_spec', 'Mil-Spec'),
|
||||
('nadcap', 'Nadcap'), ('nuclear', 'Nuclear (CSA N299)')],
|
||||
string='Certification Level', help='Leave blank to match all levels.',
|
||||
)
|
||||
pricing_method = fields.Selection(
|
||||
[('per_sqin', 'Per Square Inch'), ('per_sqft', 'Per Square Foot'),
|
||||
('per_piece', 'Per Piece'), ('flat_rate', 'Flat Rate')],
|
||||
string='Pricing Method', required=True, default='per_sqin',
|
||||
)
|
||||
currency_id = fields.Many2one('res.currency', string='Currency',
|
||||
default=lambda self: self.env.company.currency_id)
|
||||
base_rate = fields.Monetary(string='Base Rate', currency_field='currency_id',
|
||||
help='Price per unit (sq in, sq ft, piece, or flat).')
|
||||
thickness_factor = fields.Float(string='Thickness Factor', default=1.0,
|
||||
help='Multiplier per mil of coating thickness. 1.0 = no adjustment.')
|
||||
complexity_surcharge_ids = fields.One2many('fp.pricing.complexity.surcharge', 'rule_id',
|
||||
string='Complexity Surcharges')
|
||||
masking_rate_per_zone = fields.Monetary(string='Masking Rate / Zone', currency_field='currency_id')
|
||||
setup_fee = fields.Monetary(string='Setup Fee', currency_field='currency_id',
|
||||
help='One-time setup fee per batch.')
|
||||
minimum_charge = fields.Monetary(string='Minimum Charge', currency_field='currency_id',
|
||||
help='Floor price.')
|
||||
rush_surcharge_percent = fields.Float(string='Rush Surcharge %')
|
||||
sequence = fields.Integer(string='Sequence', default=10)
|
||||
active = fields.Boolean(string='Active', default=True)
|
||||
notes = fields.Text(string='Notes')
|
||||
@@ -8,3 +8,9 @@ access_fp_part_catalog_manager,fp.part.catalog.manager,model_fp_part_catalog,fus
|
||||
access_fp_coating_config_operator,fp.coating.config.operator,model_fp_coating_config,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_coating_config_estimator,fp.coating.config.estimator,model_fp_coating_config,fusion_plating_configurator.group_fp_estimator,1,1,1,0
|
||||
access_fp_coating_config_manager,fp.coating.config.manager,model_fp_coating_config,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_pricing_rule_operator,fp.pricing.rule.operator,model_fp_pricing_rule,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_pricing_rule_estimator,fp.pricing.rule.estimator,model_fp_pricing_rule,fusion_plating_configurator.group_fp_estimator,1,1,1,0
|
||||
access_fp_pricing_rule_manager,fp.pricing.rule.manager,model_fp_pricing_rule,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
access_fp_pricing_surcharge_operator,fp.pricing.complexity.surcharge.operator,model_fp_pricing_complexity_surcharge,fusion_plating.group_fusion_plating_operator,1,0,0,0
|
||||
access_fp_pricing_surcharge_estimator,fp.pricing.complexity.surcharge.estimator,model_fp_pricing_complexity_surcharge,fusion_plating_configurator.group_fp_estimator,1,1,1,0
|
||||
access_fp_pricing_surcharge_manager,fp.pricing.complexity.surcharge.manager,model_fp_pricing_complexity_surcharge,fusion_plating.group_fusion_plating_manager,1,1,1,1
|
||||
|
||||
|
@@ -1,2 +1,131 @@
|
||||
<?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>
|
||||
|
||||
<!-- ===== Pricing Rule List View ===== -->
|
||||
<record id="view_fp_pricing_rule_list" model="ir.ui.view">
|
||||
<field name="name">fp.pricing.rule.list</field>
|
||||
<field name="model">fp.pricing.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Pricing Rules" decoration-muted="not active">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="coating_config_id"/>
|
||||
<field name="substrate_material"/>
|
||||
<field name="certification_level"/>
|
||||
<field name="pricing_method"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
<field name="base_rate"/>
|
||||
<field name="minimum_charge"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Pricing Rule Form View ===== -->
|
||||
<record id="view_fp_pricing_rule_form" model="ir.ui.view">
|
||||
<field name="name">fp.pricing.rule.form</field>
|
||||
<field name="model">fp.pricing.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Pricing Rule">
|
||||
<sheet>
|
||||
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1><field name="name" placeholder="e.g. EN Mid-Phos Aluminium — Commercial"/></h1>
|
||||
</div>
|
||||
<group string="Filters">
|
||||
<group>
|
||||
<field name="coating_config_id"/>
|
||||
<field name="substrate_material"/>
|
||||
<field name="certification_level"/>
|
||||
</group>
|
||||
<group>
|
||||
<div class="text-muted" colspan="2">
|
||||
Leave filter fields blank to create a global rule
|
||||
that matches any configuration.
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Pricing">
|
||||
<group>
|
||||
<field name="pricing_method"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="base_rate"/>
|
||||
<field name="thickness_factor"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="masking_rate_per_zone"/>
|
||||
<field name="setup_fee"/>
|
||||
<field name="minimum_charge"/>
|
||||
<field name="rush_surcharge_percent"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Complexity Surcharges" name="surcharges">
|
||||
<field name="complexity_surcharge_ids">
|
||||
<list editable="bottom">
|
||||
<field name="complexity"/>
|
||||
<field name="surcharge_percent"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Notes" name="notes">
|
||||
<field name="notes" placeholder="Internal notes about this pricing rule..."/>
|
||||
</page>
|
||||
</notebook>
|
||||
<group>
|
||||
<field name="sequence"/>
|
||||
<field name="active" widget="boolean_toggle"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Pricing Rule Search View ===== -->
|
||||
<record id="view_fp_pricing_rule_search" model="ir.ui.view">
|
||||
<field name="name">fp.pricing.rule.search</field>
|
||||
<field name="model">fp.pricing.rule</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="coating_config_id"/>
|
||||
<separator/>
|
||||
<filter string="Per Square Inch" name="per_sqin" domain="[('pricing_method','=','per_sqin')]"/>
|
||||
<filter string="Per Square Foot" name="per_sqft" domain="[('pricing_method','=','per_sqft')]"/>
|
||||
<filter string="Per Piece" name="per_piece" domain="[('pricing_method','=','per_piece')]"/>
|
||||
<filter string="Flat Rate" name="flat_rate" domain="[('pricing_method','=','flat_rate')]"/>
|
||||
<separator/>
|
||||
<filter string="Archived" name="inactive" domain="[('active','=',False)]"/>
|
||||
<group>
|
||||
<filter string="Coating Config" name="group_coating_config" context="{'group_by':'coating_config_id'}"/>
|
||||
<filter string="Pricing Method" name="group_pricing_method" context="{'group_by':'pricing_method'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- ===== Window Action ===== -->
|
||||
<record id="action_fp_pricing_rule" model="ir.actions.act_window">
|
||||
<field name="name">Pricing Rules</field>
|
||||
<field name="res_model">fp.pricing.rule</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_fp_pricing_rule_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No pricing rules defined yet
|
||||
</p>
|
||||
<p>
|
||||
Define formula-based pricing rules matched by coating
|
||||
configuration, substrate material, and certification level.
|
||||
The first matching rule (by sequence) wins.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user