[MIG] bemade_margin_vendor_pricelist to 18.0

This commit is contained in:
Marc Durepos 2025-09-10 09:43:32 -04:00
parent 2d0577d7e3
commit 7f62a24819
10 changed files with 762 additions and 0 deletions

View file

@ -0,0 +1 @@
from . import models

View file

@ -0,0 +1,31 @@
#
# Bemade Inc.
#
# Copyright (C) September 2023 Bemade Inc. (<https://www.bemade.org>).
# Author: Marc Durepos (Contact : marc@bemade.org)
#
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
# or modified copies of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
{
"name": "Sales Margin on Vendor Price",
"version": "18.0.0.0.5",
"summary": "Enables calculation of sales margins based on vendor pricelists.",
"description": """Adds a new method for calculating """,
"author": "Bemade Inc.",
"website": "https://www.bemade.org",
"license": "LGPL-3",
"depends": ["sale_stock_margin", "sale_management", "stock"],
"data": ["views/sale_order.xml"],
"installable": True,
"auto_install": False,
}

View file

@ -0,0 +1,58 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * bemade_margin_vendor_pricelist
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 15.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-10-19 15:08+0000\n"
"PO-Revision-Date: 2023-10-19 15:08+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: bemade_margin_vendor_pricelist
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order__margin_actual
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_actual
msgid "Margin"
msgstr ""
#. module: bemade_margin_vendor_pricelist
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order__margin_percent_actual
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_percent_actual
msgid "Margin (%)"
msgstr ""
#. module: bemade_margin_vendor_pricelist
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_percent_vendor
msgid "Margin (%) on Vendor Price"
msgstr ""
#. module: bemade_margin_vendor_pricelist
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_vendor
msgid "Margin on Vendor Price"
msgstr ""
#. module: bemade_margin_vendor_pricelist
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__purchase_price_actual
msgid "Purchase Price"
msgstr ""
#. module: bemade_margin_vendor_pricelist
#: model:ir.model,name:bemade_margin_vendor_pricelist.model_sale_order
msgid "Sales Order"
msgstr ""
#. module: bemade_margin_vendor_pricelist
#: model:ir.model,name:bemade_margin_vendor_pricelist.model_sale_order_line
msgid "Sales Order Line"
msgstr ""
#. module: bemade_margin_vendor_pricelist
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__purchase_price_vendor
msgid "Vendor Price"
msgstr ""

View file

@ -0,0 +1,58 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * bemade_margin_vendor_pricelist
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 15.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-10-19 15:08+0000\n"
"PO-Revision-Date: 2023-10-19 15:08+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: bemade_margin_vendor_pricelist
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order__margin_actual
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_actual
msgid "Margin"
msgstr "Marge"
#. module: bemade_margin_vendor_pricelist
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order__margin_percent_actual
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_percent_actual
msgid "Margin (%)"
msgstr "Marge (%)"
#. module: bemade_margin_vendor_pricelist
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_percent_vendor
msgid "Margin (%) on Vendor Price"
msgstr "Marge (%) sur le prix du fournisseur"
#. module: bemade_margin_vendor_pricelist
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__margin_vendor
msgid "Margin on Vendor Price"
msgstr "Marge sur le prix du fournisseur"
#. module: bemade_margin_vendor_pricelist
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__purchase_price_actual
msgid "Purchase Price"
msgstr "Prix d'achat"
#. module: bemade_margin_vendor_pricelist
#: model:ir.model,name:bemade_margin_vendor_pricelist.model_sale_order
msgid "Sales Order"
msgstr "Commande de vente"
#. module: bemade_margin_vendor_pricelist
#: model:ir.model,name:bemade_margin_vendor_pricelist.model_sale_order_line
msgid "Sales Order Line"
msgstr "Article de commande de vente"
#. module: bemade_margin_vendor_pricelist
#: model:ir.model.fields,field_description:bemade_margin_vendor_pricelist.field_sale_order_line__purchase_price_vendor
msgid "Vendor Price"
msgstr "Prix du fournisseur"

View file

@ -0,0 +1,2 @@
from . import sale_order
from . import sale_order_line

View file

@ -0,0 +1,23 @@
from odoo import models, fields, api, _
from odoo.tools.float_utils import float_is_zero, float_compare
class SaleOrder(models.Model):
_inherit = 'sale.order'
gross_profit = fields.Monetary("Gross Profit", compute='_compute_margin_actual', store=False)
gross_profit_percent = fields.Float(
string='Gross Profit (%)',
compute='_compute_margin_actual',
store=False,
group_operator='avg'
)
@api.depends('order_line.gross_profit', 'amount_untaxed')
def _compute_margin_actual(self):
for order in self:
order.gross_profit = sum(order.order_line.mapped('gross_profit'))
order.gross_profit_percent = order.amount_untaxed and order.gross_profit / order.amount_untaxed

View file

@ -0,0 +1,141 @@
from odoo import models, fields, api, _
from odoo.tools.float_utils import float_is_zero, float_compare
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
purchase_price_vendor = fields.Float(
compute="_compute_purchase_price_vendor",
string="Vendor Price",
groups="base.group_user",
digits="Product Price",
)
purchase_price_actual = fields.Float(
compute="_compute_actual_gp",
digits="Product Price",
groups="base.group_user",
string="Purchase Price",
)
gross_profit = fields.Float(
compute="_compute_actual_gp",
digits="Product Price",
groups="base.group_user",
string="Gross Profit",
)
gross_profit_percent = fields.Float(
compute="_compute_actual_gp",
groups="base.group_user",
string="Gross Profit (%)",
)
@api.depends(
"purchase_price",
"purchase_price_vendor",
"move_ids.product_id",
"move_ids.product_id.qty_available",
"move_ids.state",
"qty_to_deliver",
)
def _compute_actual_gp(self):
"""We want to use the gross profit based on average inventory valuation when the
sale order line will be completely fulfilled (or has been fulfilled) from stock.
For product not yet in stock we want to use the vendor price. We can also have
blended calculations (partly on vendor price, partly on stock valuation). This
occurs when an order has been or would be partially fulfilled from available
stock.
:return:
"""
non_product_lines = self.filtered(lambda r: not r.product_id)
non_product_lines.purchase_price_actual = 0.0
non_product_lines.gross_profit = 0.0
non_product_lines.gross_profit_percent = 0.0
for line in self - non_product_lines:
stock_missing = line._determine_missing_stock()
if float_is_zero(
stock_missing, precision_rounding=line.product_uom.rounding
):
# everything is coming from stock, use inventory valuation
line.purchase_price_actual = line.purchase_price
elif (
float_compare(
line.product_uom_qty,
stock_missing,
precision_rounding=line.product_uom.rounding,
)
== 0
):
# everything is coming from the vendor, use vendor pricing
line.purchase_price_actual = line.purchase_price_vendor
else:
# we have a mix, use blended pricing
qty_from_stock = line.product_uom_qty - stock_missing
line.purchase_price_actual = (
(
stock_missing * line.purchase_price_vendor
+ qty_from_stock * line.purchase_price
)
/ line.product_uom_qty
if line.product_uom_qty != 0
else 0
)
line.gross_profit = line.price_subtotal - (
line.purchase_price_actual * line.product_uom_qty
)
line.gross_profit_percent = (
line.price_subtotal and line.gross_profit / line.price_subtotal
if line.price_subtotal != 0
else 0
)
def _determine_missing_stock(self) -> float:
"""Compute how much stock is missing to meet an order line's demand. In the
case of a quotation line, available stock is checked as if the order were to be
placed immediately.
:return: The quantity missing from available stock to fulfill the line, in
the unit of measure matching self.product_uom.
"""
self.ensure_one()
is_order = self.order_id.state == "sale"
qty_available = max(0, self.product_id.qty_available)
if is_order and self.qty_to_deliver <= 0:
return 0
elif is_order and self.qty_to_deliver > 0:
reserved = sum(
[
q.reserved_quantity
for q in self.move_ids.mapped("move_line_ids").mapped("quant_id")
]
)
return max(0, self.qty_to_deliver - reserved - qty_available)
else:
# This is a quotation, don't bother with stock reservations
# Also, if available is negative for some reason,
return max(0, self.product_uom_qty - qty_available)
@api.depends("product_id", "product_id.seller_ids", "product_id.seller_ids.price")
def _compute_purchase_price_vendor(self):
for line in self:
product = line.product_id
suppinfos = product.seller_ids.sorted("sequence")
if not suppinfos:
line.purchase_price_vendor = 0.0
continue
suppinfo = suppinfos[0]
purch_currency = suppinfo.currency_id
to_cur = line.currency_id or line.order_id.currency_id
line.purchase_price_vendor = (
purch_currency._convert(
from_amount=suppinfo.price,
to_currency=to_cur,
company=line.company_id or self.env.company,
date=line.order_id.date_order or fields.Date.today(),
round=False,
)
if to_cur and suppinfo.price
else suppinfo.price
)

View file

@ -0,0 +1 @@
from . import test_margins

View file

@ -0,0 +1,390 @@
from odoo.tests import TransactionCase, tagged
from unittest.mock import patch, PropertyMock
@tagged("-at_install", "post_install")
class TestSaleOrderLine(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Create a test vendor
cls.vendor = cls.env["res.partner"].create(
{
"name": "Test Vendor",
"supplier_rank": 1,
}
)
# Create a test product
cls.product = cls.env["product.product"].create(
{
"name": "Test Product",
"type": "consu",
"is_storable": True,
"list_price": 100.0,
"standard_price": 80.0,
}
)
# Link supplier info to the product
cls.supplierinfo = cls.env["product.supplierinfo"].create(
{
"partner_id": cls.vendor.id,
"product_tmpl_id": cls.product.product_tmpl_id.id,
"min_qty": 1,
"price": 75.0, # Vendor price
"currency_id": cls.env.ref(
"base.USD"
).id, # Assuming USD for simplicity
}
)
# Create a test sale order
cls.sale_order = cls.env["sale.order"].create(
{
"partner_id": cls.env.ref("base.res_partner_1").id,
# Assuming a default partner
}
)
# Create a test sale order line
cls.sale_order_line = cls.env["sale.order.line"].create(
{
"order_id": cls.sale_order.id,
"product_id": cls.product.id,
"product_uom_qty": 10,
"price_unit": 120.0,
}
)
def test_purchase_price_all_from_stock(self):
"""Test when all quantity is fulfilled from stock."""
with patch.object(
self.sale_order_line.product_id.__class__,
"qty_available",
new_callable=PropertyMock,
) as mock_qty_available:
mock_qty_available.return_value = 10 # All available
# Recompute the fields
self.sale_order_line._compute_actual_gp()
# Assert purchase_price_actual equals purchase_price (from stock)
self.assertEqual(
self.sale_order_line.purchase_price_actual,
self.sale_order_line.purchase_price,
"Purchase price should be from stock when all quantity is available.",
)
# Assert gross_profit is calculated correctly
expected_gross_profit = self.sale_order_line.price_subtotal - (
self.sale_order_line.purchase_price_actual
* self.sale_order_line.product_uom_qty
)
self.assertAlmostEqual(
self.sale_order_line.gross_profit,
expected_gross_profit,
msg="Gross profit should be correctly calculated from stock purchase price.",
)
# Ensure the SO is correct as well
self.assertSaleOrderComputesLikeSaleOrderLine()
def assertSaleOrderComputesLikeSaleOrderLine(self):
self.assertAlmostEqual(
self.sale_order_line.gross_profit,
self.sale_order.gross_profit,
)
self.assertAlmostEqual(
self.sale_order_line.gross_profit_percent,
self.sale_order.gross_profit_percent,
)
def test_purchase_price_all_from_vendor(self):
"""Test when all quantity is fulfilled from vendor."""
with patch.object(
self.sale_order_line.product_id.__class__,
"qty_available",
new_callable=PropertyMock,
) as mock_qty_available:
mock_qty_available.return_value = 0
# Assert purchase_price_actual equals purchase_price_vendor (from vendor)
self.assertEqual(
self.sale_order_line.purchase_price_actual,
self.sale_order_line.purchase_price_vendor,
"Purchase price should be from vendor when all quantity is missing from stock.",
)
# Assert gross_profit is calculated correctly
expected_gross_profit = self.sale_order_line.price_subtotal - (
self.sale_order_line.purchase_price_actual
* self.sale_order_line.product_uom_qty
)
self.assertAlmostEqual(
self.sale_order_line.gross_profit,
expected_gross_profit,
msg="Gross profit should be correctly calculated from vendor purchase price.",
)
# Ensure the SO is correct as well
self.assertSaleOrderComputesLikeSaleOrderLine()
def test_purchase_price_mixed(self):
"""Test when quantity is partially fulfilled from stock and partially from vendor."""
with patch.object(
self.sale_order_line.product_id.__class__,
"qty_available",
new_callable=PropertyMock,
) as mock_qty_available:
mock_qty_available.return_value = 5
# Calculate expected blended purchase price
qty_from_vendor = 5
qty_from_stock = 5
expected_purchase_price = (
(qty_from_vendor * self.sale_order_line.purchase_price_vendor)
+ (qty_from_stock * self.sale_order_line.purchase_price)
) / self.sale_order_line.product_uom_qty
self.assertAlmostEqual(
self.sale_order_line.purchase_price_actual,
expected_purchase_price,
msg="Purchase price should be a blend of stock and vendor prices.",
)
# Assert gross_profit is calculated correctly
expected_gross_profit = self.sale_order_line.price_subtotal - (
expected_purchase_price * self.sale_order_line.product_uom_qty
)
self.assertAlmostEqual(
self.sale_order_line.gross_profit,
expected_gross_profit,
msg="Gross profit should be correctly calculated from blended purchase price.",
)
# Ensure the SO is correct as well
self.assertSaleOrderComputesLikeSaleOrderLine()
def test_negative_available_stock_zero_qty_ordered(self):
"""We had a division by zero error with a zero product_uom_qty field
and a negative available stock. This test aims to root out this and
other possible causes of a div by zero error."""
with patch.object(
self.sale_order_line.product_id.__class__,
"qty_available",
new_callable=PropertyMock,
) as mock_qty_available:
mock_qty_available.return_value = -1 # Negative available stock
# Set product_uom_qty to zero
self.sale_order_line.product_uom_qty = 0
# Recompute the fields
self.sale_order_line._compute_actual_gp()
# Assert gross_profit is 0
self.assertEqual(
self.sale_order_line.gross_profit,
0.0,
"Gross profit should be 0 when product_uom_qty is zero.",
)
# Assert gross_profit_percent is 0
self.assertEqual(
self.sale_order_line.gross_profit_percent,
0.0,
"Gross profit percent should be 0 when product_uom_qty is zero.",
)
# Ensure the SO is correct as well
self.assertSaleOrderComputesLikeSaleOrderLine()
def test_purchase_price_all_from_stock_confirmed(self):
"""Test when all quantity is fulfilled from stock."""
self.sale_order.action_confirm()
with patch.object(
self.sale_order_line.product_id.__class__,
"qty_available",
new_callable=PropertyMock,
) as mock_qty_available:
mock_qty_available.return_value = 10 # All available
# Recompute the fields
self.sale_order_line._compute_actual_gp()
# Assert purchase_price_actual equals purchase_price (from stock)
self.assertEqual(
self.sale_order_line.purchase_price_actual,
self.sale_order_line.purchase_price,
"Purchase price should be from stock when all quantity is available.",
)
# Assert gross_profit is calculated correctly
expected_gross_profit = self.sale_order_line.price_subtotal - (
self.sale_order_line.purchase_price_actual
* self.sale_order_line.product_uom_qty
)
self.assertAlmostEqual(
self.sale_order_line.gross_profit,
expected_gross_profit,
msg="Gross profit should be correctly calculated from stock purchase price.",
)
# Ensure the SO is correct as well
self.assertSaleOrderComputesLikeSaleOrderLine()
def test_purchase_price_all_from_vendor_confirmed(self):
"""Test when all quantity is fulfilled from vendor."""
self.sale_order.action_confirm()
with patch.object(
self.sale_order_line.product_id.__class__,
"qty_available",
new_callable=PropertyMock,
) as mock_qty_available:
mock_qty_available.return_value = 0
# Assert purchase_price_actual equals purchase_price_vendor (from vendor)
self.assertEqual(
self.sale_order_line.purchase_price_actual,
self.sale_order_line.purchase_price_vendor,
"Purchase price should be from vendor when all quantity is missing from stock.",
)
# Assert gross_profit is calculated correctly
expected_gross_profit = self.sale_order_line.price_subtotal - (
self.sale_order_line.purchase_price_actual
* self.sale_order_line.product_uom_qty
)
self.assertAlmostEqual(
self.sale_order_line.gross_profit,
expected_gross_profit,
msg="Gross profit should be correctly calculated from vendor purchase price.",
)
# Ensure the SO is correct as well
self.assertSaleOrderComputesLikeSaleOrderLine()
def test_purchase_price_mixed_confirmed(self):
"""Test when quantity is partially fulfilled from stock and partially from vendor."""
self.sale_order.action_confirm()
with patch.object(
self.sale_order_line.product_id.__class__,
"qty_available",
new_callable=PropertyMock,
) as mock_qty_available:
mock_qty_available.return_value = 5
# Calculate expected blended purchase price
qty_from_vendor = 5
qty_from_stock = 5
expected_purchase_price = (
(qty_from_vendor * self.sale_order_line.purchase_price_vendor)
+ (qty_from_stock * self.sale_order_line.purchase_price)
) / self.sale_order_line.product_uom_qty
self.assertAlmostEqual(
self.sale_order_line.purchase_price_actual,
expected_purchase_price,
msg="Purchase price should be a blend of stock and vendor prices.",
)
# Assert gross_profit is calculated correctly
expected_gross_profit = self.sale_order_line.price_subtotal - (
expected_purchase_price * self.sale_order_line.product_uom_qty
)
self.assertAlmostEqual(
self.sale_order_line.gross_profit,
expected_gross_profit,
msg="Gross profit should be correctly calculated from blended purchase price.",
)
# Ensure the SO is correct as well
self.assertSaleOrderComputesLikeSaleOrderLine()
def test_negative_available_stock_zero_qty_ordered_confirmed(self):
"""We had a division by zero error with a zero product_uom_qty field
and a negative available stock. This test aims to root out this and
other possible causes of a div by zero error."""
self.sale_order.action_confirm()
with patch.object(
self.sale_order_line.product_id.__class__,
"qty_available",
new_callable=PropertyMock,
) as mock_qty_available:
mock_qty_available.return_value = -1 # Negative available stock
# Set product_uom_qty to zero
self.sale_order_line.product_uom_qty = 0
# Recompute the fields
self.sale_order_line._compute_actual_gp()
# Assert gross_profit is 0
self.assertEqual(
self.sale_order_line.gross_profit,
0.0,
"Gross profit should be 0 when product_uom_qty is zero.",
)
# Assert gross_profit_percent is 0
self.assertEqual(
self.sale_order_line.gross_profit_percent,
0.0,
"Gross profit percent should be 0 when product_uom_qty is zero.",
)
# Ensure the SO is correct as well
self.assertSaleOrderComputesLikeSaleOrderLine()
def test_determine_missing_stock_all_reserved(self):
"""Test the actual _determine_missing_stock method when all stock is reserved."""
# Set initial stock for the product
location = self.env["stock.warehouse"].search([])[0].lot_stock_id
quant = self.env["stock.quant"].create(
{
"product_id": self.product.id,
"location_id": location.id,
"inventory_quantity": 10,
"product_uom_id": self.product.uom_id.id,
}
)
quant.action_apply_inventory()
self.sale_order.action_confirm()
# Force the move to be reserved
self.sale_order.picking_ids.action_assign()
# Now, call _determine_missing_stock without mocking
missing_stock = self.sale_order_line._determine_missing_stock()
# Assert that missing_stock is 0
self.assertEqual(
missing_stock,
0,
"_determine_missing_stock should return 0 when all stock is reserved.",
)
# Additionally, verify that purchase_price_actual is from stock
self.sale_order_line._compute_actual_gp()
self.assertEqual(
self.sale_order_line.purchase_price_actual,
self.sale_order_line.purchase_price,
"Purchase price should be from stock when all stock is reserved.",
)
# Assert gross_profit is calculated correctly
expected_gross_profit = self.sale_order_line.price_subtotal - (
self.sale_order_line.purchase_price_actual
* self.sale_order_line.product_uom_qty
)
self.assertAlmostEqual(
self.sale_order_line.gross_profit,
expected_gross_profit,
msg="Gross profit should be correctly calculated when all stock is reserved.",
)
# Assert gross_profit_percent is calculated correctly
if self.sale_order_line.price_subtotal:
expected_gross_profit_percent = (
self.sale_order_line.gross_profit / self.sale_order_line.price_subtotal
)
else:
expected_gross_profit_percent = 0.0
self.assertAlmostEqual(
self.sale_order_line.gross_profit_percent,
expected_gross_profit_percent,
msg="Gross profit percent should be correctly calculated when all stock is reserved.",
)
# Ensure the SO is correct as well
self.assertSaleOrderComputesLikeSaleOrderLine()

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data>
<record model="ir.ui.view" id="sale_margin_sale_order_inherit">
<field name="name">sale.order.margin.view.form.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale_margin.sale_margin_sale_order"/>
<field name="arch" type="xml">
<field name="margin" position="replace">
<field name="margin" invisible="True"/>
<field name="gross_profit" class="oe_inline"/>
</field>
<label for="margin" position="replace">
<label for="gross_profit" groups="base.group_user"/>
</label>
<field name="margin_percent" position="replace">
<field name="margin_percent" invisible="True"/>
<field name="gross_profit_percent" nolabel="1" class="oe_inline"
widget="percentage" groups="base.group_user"/>
</field>
</field>
</record>
<record model="ir.ui.view" id="sale_margin_sale_order_line_inherit">
<field name="name">sale.order.line.margin.view.form.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale_margin.sale_margin_sale_order_line"/>
<field name="arch" type="xml">
<field name="purchase_price" position="replace">
<field name="purchase_price" invisible="True"/>
<field name="purchase_price_actual" groups="base.group_user"/>
</field>
</field>
</record>
<record model="ir.ui.view" id="sale_margin_sale_order_line_form_inherit">
<field name="name">sale.order.view.form</field>
<field name="inherit_id" ref="sale_margin.sale_margin_sale_order_line_form"/>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<field name="purchase_price" position="replace">
<field name="purchase_price" column_invisible="True"/>
<field name="purchase_price_actual" optional="hide"/>
</field>
<field name="margin" position="replace">
<field name="margin" column_invisible="True"/>
<field name="gross_profit" optional="hide"/>
</field>
<field name="margin_percent" position="replace">
<field name="margin_percent" column_invisible="True"/>
<field name="gross_profit_percent" optional="hide"
widget="percentage" groups="base.group_user"/>
</field>
</field>
</record>
<!-- For now, we don't override the pivot and graph views because our fields
are not stored and this will cause performance issues. -->
</data>
</odoo>