Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions product_cost_usd/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3

Product Price Cost in USD:
==========================

This module adds a field to handle cost in USD on products.
The approach is to have the USD as the base currency,
so that the sale prices in company currency can be
kept up to date through the exchange rate.

Features:
---------
- New 'Cost in USD' field on the Product form.
- Validate 'Cost in USD' so that it is not less than the list price of the
supplier.
- Avoid save a 'Cost in USD' when product does not has assigned a supplier
with price in USD.
- Allowed pricelist computation based on Cost in USD.
- Added unit tests to validate constrains and pricelists computation.
- Compatibility with the module `sale_margin
<https://github.com/odoo/odoo/tree/10.0/addons/sale_margin>`_


Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/vauxoo/addons-vauxoo/issues>`_.
In case of trouble, please check there if your issue has already been reported.

Credits
=======

Contributors
------------

* Jose Suniaga <josemiguel@vauxoo.com>
1 change: 1 addition & 0 deletions product_cost_usd/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
23 changes: 23 additions & 0 deletions product_cost_usd/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "Product Price Cost in USD",
"summary": """
This module adds the field Cost in USD to the Product form.
""",
"version": "19.0.1.0.0",
"author": "Vauxoo",
"category": "Sales/Sales",
"website": "https://vauxoo.com",
"license": "LGPL-3",
"depends": [
"sale_margin",
"sale_stock_margin",
],
"demo": [
"demo/product_pricelist_demo.xml",
],
"data": [
"data/res_currency_data.xml",
"views/product_template_views.xml",
],
"installable": True,
}
6 changes: 6 additions & 0 deletions product_cost_usd/data/res_currency_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo noupdate="1">
<record id="base.USD" model="res.currency">
<field name="active" eval="True" />
</record>
</odoo>
14 changes: 14 additions & 0 deletions product_cost_usd/demo/product_pricelist_demo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="pricelist_15_usd" model="product.pricelist">
<field name="name">Pricelist 15% USD</field>
<field name="currency_id" ref="base.USD" />
</record>

<record id="pricelist_15_usd_line" model="product.pricelist.item">
<field name="compute_price">formula</field>
<field name="base">standard_price_usd</field>
<field name="price_discount">-15</field>
<field name="pricelist_id" ref="pricelist_15_usd" />
</record>
</odoo>
113 changes: 113 additions & 0 deletions product_cost_usd/i18n/es.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * product_cost_usd
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 19.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-05-06 21:00+0000\n"
"PO-Revision-Date: 2026-05-06 21:00+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: product_cost_usd
#: model:ir.model.fields,help:product_cost_usd.field_product_pricelist_item__base
msgid ""
"Base price for computation.\n"
"Sales Price: The base price will be the Sales Price.\n"
"Cost Price: The base price will be the cost price.\n"
"Other Pricelist: Computation of the base price based on another Pricelist."
msgstr ""
"Precio base para el cálculo.\n"
"Precio de venta: El precio base será el precio de venta.\n"
"Precio de coste: El precio base será el precio de coste.\n"
"Otra lista de precios: Cálculo del precio base de acuerdo con otra lista de precios."

#. module: product_cost_usd
#: model:ir.model.fields,field_description:product_cost_usd.field_product_pricelist_item__base
msgid "Based on"
msgstr "Basado en"

#. module: product_cost_usd
#: model:ir.model.fields,field_description:product_cost_usd.field_product_product__standard_price_usd
#: model:ir.model.fields,field_description:product_cost_usd.field_product_template__standard_price_usd
#: model:ir.model.fields.selection,name:product_cost_usd.selection__product_pricelist_item__base__standard_price_usd
msgid "Cost in USD"
msgstr "Costo en USD"

#. module: product_cost_usd
#: model:ir.model.fields,field_description:product_cost_usd.field_product_product__currency_usd_id
#: model:ir.model.fields,field_description:product_cost_usd.field_product_template__currency_usd_id
msgid "Currency USD"
msgstr "Moneda USD"

#. module: product_cost_usd
#: model:ir.model.fields,field_description:product_cost_usd.field_product_pricelist_item__display_name
#: model:ir.model.fields,field_description:product_cost_usd.field_product_template__display_name
#: model:ir.model.fields,field_description:product_cost_usd.field_sale_order_line__display_name
msgid "Display Name"
msgstr "Nombre mostrado"

#. module: product_cost_usd
#: model:ir.model.fields,field_description:product_cost_usd.field_product_pricelist_item__id
#: model:ir.model.fields,field_description:product_cost_usd.field_product_template__id
#: model:ir.model.fields,field_description:product_cost_usd.field_sale_order_line__id
msgid "ID"
msgstr ""

#. module: product_cost_usd
#: model:ir.model.fields,help:product_cost_usd.field_product_product__standard_price_usd
#: model:ir.model.fields,help:product_cost_usd.field_product_template__standard_price_usd
msgid "Price cost of the product in USD currency"
msgstr "Precio coste del producto en moneda USD"

#. module: product_cost_usd
#: model:ir.model,name:product_cost_usd.model_product_pricelist_item
msgid "Pricelist Rule"
msgstr "Regla de la lista de precios"

#. module: product_cost_usd
#: model:ir.model,name:product_cost_usd.model_product_template
msgid "Product"
msgstr "Producto"

#. module: product_cost_usd
#: model:ir.model,name:product_cost_usd.model_sale_order_line
msgid "Sales Order Line"
msgstr "Línea de pedido de venta"

#. module: product_cost_usd
#: model:ir.model.fields,help:product_cost_usd.field_product_product__currency_usd_id
#: model:ir.model.fields,help:product_cost_usd.field_product_template__currency_usd_id
msgid "Technical field to show the price fields as USD in the products"
msgstr ""
"Campo técnico para mostrar los campos de precio como USD en los productos"

#. module: product_cost_usd
#. odoo-python
#: code:addons/product_cost_usd/models/product_template.py:0
msgid ""
"You cannot create or modify a product if the cost in USD is less than the supplier list price.\n"
"\n"
"- Supplier list price = %s\n"
"- Cost in USD = %s"
msgstr ""
"No puede crear o modificar un producto si el costo en USD es menor que el precio del proveedor.\n"
"\n"
"- Precio del proveedor = %s\n"
"- Costo en USD = %s"

#. module: product_cost_usd
#. odoo-python
#: code:addons/product_cost_usd/models/product_template.py:0
msgid ""
"You must have at least one supplier with price in USD before assigning a "
"Cost in USD"
msgstr ""
"Debe tener al menos un proveedor con precio en USD antes de asignar un Costo"
" en USD al producto"
3 changes: 3 additions & 0 deletions product_cost_usd/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import product_template
from . import product_pricelist_item
from . import sale_order_line
42 changes: 42 additions & 0 deletions product_cost_usd/models/product_pricelist_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from odoo import fields, models


class PricelistItem(models.Model):
_inherit = "product.pricelist.item"

base = fields.Selection(
selection_add=[("standard_price_usd", "Cost in USD")], ondelete={"standard_price_usd": "set default"}
)

def _compute_base_price(self, product, quantity, uom, date, currency, **kwargs):
"""Compute the base price for a pricelist item based on the USD standard price.

This method overrides the native base price computation to inject our custom
'standard_price_usd' base. When this specific base is selected in the pricelist
item, the method performs the following pipeline:
1. Retrieves the raw USD cost from the product (`standard_price_usd`).
2. Adjusts the price according to the requested Unit of Measure (UoM) to ensure
correct pricing for different quantities (e.g., dozens vs. units).
3. Converts the adjusted USD price into the pricelist's target currency.

By returning the exact converted base price at this stage, we allow Odoo's
native pricing engine to seamlessly handle subsequent operations like discounts,
surcharges, and financial rounding rules.
"""
if self.base != "standard_price_usd":
return super()._compute_base_price(product, quantity, uom, date, currency, **kwargs)
target_currency = currency or self.currency_id
usd_currency = self.env.ref("base.USD", raise_if_not_found=False)

# 1. Retrieve the raw USD cost from the product
price = product.standard_price_usd

# 2. Unit of Measure conversion
if product.uom_id != uom:
price = product.uom_id._compute_price(price, uom)

# 3. Convert from USD to the target pricelist currency
# We set round=False to allow Odoo's native engine to apply its own rounding rules at the end
if usd_currency and target_currency != usd_currency:
price = usd_currency._convert(price, target_currency, self.env.company, date, round=False)
return price
55 changes: 55 additions & 0 deletions product_cost_usd/models/product_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from odoo import api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import float_compare


class ProductTemplate(models.Model):
_inherit = "product.template"

currency_usd_id = fields.Many2one(
"res.currency",
string="Currency USD",
compute="_compute_currency_usd_id",
help="Technical field to show the price fields as USD in the products",
)
standard_price_usd = fields.Float(
string="Cost in USD",
digits="Product Price",
help="Price cost of the product in USD currency",
)

def _compute_currency_usd_id(self):
currency_usd = self.env.ref("base.USD")
for product in self:
product.currency_usd_id = currency_usd

@api.constrains("standard_price_usd", "seller_ids")
def check_cost_and_price(self):
"""Validate 'Cost in USD' usability.

Usability conditions:
- Before set a 'Cost in USD' in a product at least one supplier should
have price in USD.
- The Cost in USD cannot be less than supplier price.
"""
usd_currency = self.env.ref("base.USD")
prec = self.env["decimal.precision"].precision_get("Product Price")
for product in self:
usd_seller = product.seller_ids.filtered(lambda x: x.currency_id == usd_currency)[:1]
list_price = usd_seller.price
standard_price_usd = product.standard_price_usd
if not usd_seller and float_compare(standard_price_usd, 0, precision_digits=prec) > 0:
raise ValidationError(
self.env._("You must have at least one supplier with price in USD before assigning a Cost in USD")
)
if usd_seller and float_compare(list_price, standard_price_usd, precision_digits=prec) > 0:
raise ValidationError(
self.env._(
"You cannot create or modify a product if the cost in USD"
" is less than the supplier list price.\n\n"
"- Supplier list price = %s\n"
"- Cost in USD = %s",
list_price,
standard_price_usd,
)
)
37 changes: 37 additions & 0 deletions product_cost_usd/models/sale_order_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from odoo import api, models


class SaleOrderLine(models.Model):
_inherit = "sale.order.line"

@api.depends("product_id", "company_id", "currency_id", "product_uom_id")
def _compute_purchase_price(self):
"""Inherited to recalculate purchase price when pricelist item is
based on cost in USD.
"""
res = super()._compute_purchase_price()
for line in self:
line = line.with_company(line.company_id)
product = line.product_id
if not product:
continue
pricelist = line.order_id.pricelist_id
date = line.order_id.date_order
price_rule = pricelist._compute_price_rule(
products=product,
quantity=1.0,
currency=pricelist.currency_id,
uom=line.product_uom_id,
date=date,
)
_price, rule = price_rule.get(product.id, (0.0, False))
suitable_rule_id = self.env["product.pricelist.item"].browse(rule)
if suitable_rule_id.base != "standard_price_usd":
continue
currency_usd = self.env.ref("base.USD")
to_cur = pricelist.currency_id
purchase_price = product.standard_price_usd
if line.product_uom_id != product.uom_id:
purchase_price = product.uom_id._compute_price(purchase_price, line.product_uom_id)
line.purchase_price = currency_usd._convert(purchase_price, to_cur, line.company_id, date, round=False)
return res
Binary file added product_cost_usd/static/description/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading