168 lines
6.7 KiB
Python
168 lines
6.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
|
|
|
import logging
|
|
from odoo import models, fields, api
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ProductProduct(models.Model):
|
|
_inherit = 'product.product'
|
|
|
|
x_fi_price_offset = fields.Float(
|
|
string='Price Offset',
|
|
default=0.0,
|
|
help='Margin-calculated price adjustment, kept separate from '
|
|
'manual attribute price extras.')
|
|
x_fi_variant_margin_pct = fields.Float(
|
|
string='Margin (%)',
|
|
compute='_compute_variant_margin',
|
|
inverse='_inverse_variant_margin',
|
|
store=True, readonly=False,
|
|
help='Profit margin based on (cost + shipping + extra cost). '
|
|
'Extra Price from attributes is added on top, outside margin.')
|
|
x_fi_margin_override = fields.Boolean(
|
|
string='Margin Override',
|
|
default=False,
|
|
help='When set, the template "Apply Margin" button '
|
|
'and automatic propagation skip this variant.')
|
|
x_fi_variant_profit = fields.Float(
|
|
string='Profit',
|
|
compute='_compute_variant_profit',
|
|
help='Total sale price minus total cost.')
|
|
x_fi_shipping_cost = fields.Float(
|
|
string='Shipping Cost',
|
|
default=0.0,
|
|
help='Per-unit shipping cost.')
|
|
x_fi_cost_extra = fields.Float(
|
|
string='Attribute Extra Cost',
|
|
compute='_compute_cost_extra', store=True,
|
|
help='Sum of Extra Cost from all attribute values for this variant.')
|
|
|
|
@api.depends('product_template_attribute_value_ids.x_fi_extra_cost')
|
|
def _compute_cost_extra(self):
|
|
for product in self:
|
|
product.x_fi_cost_extra = sum(
|
|
product.product_template_attribute_value_ids.mapped(
|
|
'x_fi_extra_cost'))
|
|
|
|
# ── Include price offset in pricelist / SO pricing ──
|
|
|
|
def _get_attributes_extra_price(self):
|
|
extra = super()._get_attributes_extra_price()
|
|
return extra + (self.x_fi_price_offset or 0.0)
|
|
|
|
# ── Override lst_price to include our price offset ──
|
|
|
|
@api.depends('list_price', 'price_extra', 'x_fi_price_offset')
|
|
@api.depends_context('uom')
|
|
def _compute_product_lst_price(self):
|
|
to_uom = None
|
|
if 'uom' in self._context:
|
|
to_uom = self.env['uom.uom'].browse(self._context['uom'])
|
|
for product in self:
|
|
if to_uom:
|
|
list_price = product.uom_id._compute_price(
|
|
product.list_price, to_uom)
|
|
else:
|
|
list_price = product.list_price
|
|
product.lst_price = (
|
|
list_price
|
|
+ product.price_extra
|
|
+ (product.x_fi_price_offset or 0.0))
|
|
|
|
def _set_product_lst_price(self):
|
|
for product in self:
|
|
if self._context.get('uom'):
|
|
value = (
|
|
float(product.lst_price) * product.uom_id.factor
|
|
/ self.env['uom.uom'].browse(
|
|
self._context['uom']).factor)
|
|
else:
|
|
value = product.lst_price
|
|
value -= product.price_extra
|
|
value -= (product.x_fi_price_offset or 0.0)
|
|
product.write({'list_price': value})
|
|
|
|
# ── Margin / Profit ──
|
|
# effective_cost = standard_price + shipping + extra_cost
|
|
# base_sale_price = list_price + x_fi_price_offset (margin applies here)
|
|
# final_price = base_sale_price + price_extra (surcharge on top)
|
|
# margin = (base_sale_price - effective_cost) / base_sale_price
|
|
|
|
@api.depends('list_price', 'price_extra', 'x_fi_price_offset',
|
|
'standard_price', 'x_fi_shipping_cost', 'x_fi_cost_extra')
|
|
def _compute_variant_margin(self):
|
|
for var in self:
|
|
base = var.list_price + (var.x_fi_price_offset or 0.0)
|
|
eff = (var.standard_price
|
|
+ (var.x_fi_shipping_cost or 0.0)
|
|
+ (var.x_fi_cost_extra or 0.0))
|
|
if base > 0 and eff > 0:
|
|
var.x_fi_variant_margin_pct = round(
|
|
((base - eff) / base) * 100, 2)
|
|
else:
|
|
var.x_fi_variant_margin_pct = 0.0
|
|
|
|
def _inverse_variant_margin(self):
|
|
for var in self:
|
|
margin = var.x_fi_variant_margin_pct or 0.0
|
|
eff = (var.standard_price
|
|
+ (var.x_fi_shipping_cost or 0.0)
|
|
+ (var.x_fi_cost_extra or 0.0))
|
|
if eff > 0 and 0 < margin < 100:
|
|
base = round(eff / (1 - margin / 100), 2)
|
|
var.x_fi_price_offset = round(base - var.list_price, 2)
|
|
|
|
@api.depends('list_price', 'price_extra', 'x_fi_price_offset',
|
|
'standard_price', 'x_fi_shipping_cost', 'x_fi_cost_extra')
|
|
def _compute_variant_profit(self):
|
|
for var in self:
|
|
lst = (var.list_price
|
|
+ var.price_extra
|
|
+ (var.x_fi_price_offset or 0.0))
|
|
eff = (var.standard_price
|
|
+ (var.x_fi_shipping_cost or 0.0)
|
|
+ (var.x_fi_cost_extra or 0.0))
|
|
var.x_fi_variant_profit = lst - eff
|
|
|
|
# ── Onchange handlers ──
|
|
|
|
@api.onchange('x_fi_variant_margin_pct')
|
|
def _onchange_variant_margin(self):
|
|
for var in self:
|
|
margin = var.x_fi_variant_margin_pct or 0.0
|
|
eff = (var.standard_price
|
|
+ (var.x_fi_shipping_cost or 0.0)
|
|
+ (var.x_fi_cost_extra or 0.0))
|
|
if eff > 0 and 0 < margin < 100:
|
|
base = round(eff / (1 - margin / 100), 2)
|
|
var.x_fi_price_offset = round(base - var.list_price, 2)
|
|
|
|
@api.onchange('standard_price', 'x_fi_shipping_cost')
|
|
def _onchange_variant_cost(self):
|
|
"""When cost or shipping changes, recalculate price to keep margin."""
|
|
for var in self:
|
|
margin = var.x_fi_variant_margin_pct or 0.0
|
|
eff = (var.standard_price
|
|
+ (var.x_fi_shipping_cost or 0.0)
|
|
+ (var.x_fi_cost_extra or 0.0))
|
|
if eff > 0 and 0 < margin < 100:
|
|
base = round(eff / (1 - margin / 100), 2)
|
|
var.x_fi_price_offset = round(base - var.list_price, 2)
|
|
|
|
def _apply_margin_to_variant(self, margin_pct):
|
|
"""Set this variant's price offset to achieve the given margin."""
|
|
self.ensure_one()
|
|
eff = (self.standard_price
|
|
+ (self.x_fi_shipping_cost or 0.0)
|
|
+ (self.x_fi_cost_extra or 0.0))
|
|
if eff <= 0 or margin_pct <= 0 or margin_pct >= 100:
|
|
return
|
|
base = round(eff / (1 - margin_pct / 100), 2)
|
|
new_offset = round(base - self.list_price, 2)
|
|
if abs((self.x_fi_price_offset or 0.0) - new_offset) > 0.001:
|
|
self.x_fi_price_offset = new_offset
|