build: configurar herramientas de verificación OCA

- Instalar pre-commit con 25 hooks configurados
- Configurar black 26.1.0 para formateo de código Python
- Configurar isort 7.0.0 para ordenación de imports
- Configurar flake8 7.3.0 con flake8-bugbear
- Configurar pylint 3.1.1 con pylint-odoo 9.1.2
- Añadir autoflake y pyupgrade para mejoras automáticas
- Configurar prettier para formateo de XML/JSON/YAML
- Crear .editorconfig para consistencia de editor
- Crear Makefile con comandos útiles
- Crear check_addon.sh para verificación rápida de addons
- Actualizar configuración de VS Code con extensiones recomendadas
- Añadir documentación completa de uso
- Aplicar formateo automático a archivos existentes
- Deshabilitar setuptools-odoo (no soporta Odoo 18.0 aún)
- Deshabilitar fix-encoding-pragma (obsoleto, usar pyupgrade)
This commit is contained in:
snt 2026-02-11 16:09:41 +01:00
parent 5b9c6e3211
commit fe137dc265
224 changed files with 18376 additions and 0 deletions

View file

@ -0,0 +1,90 @@
========================
Product Get Price Helper
========================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:5fb33150c2c1ee21fd7bc337113f150dccba97ee06c9dfd5a01d2f17ce567509
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproduct--attribute-lightgray.png?logo=github
:target: https://github.com/OCA/product-attribute/tree/18.0/product_get_price_helper
:alt: OCA/product-attribute
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/product-attribute-18-0/product-attribute-18-0-product_get_price_helper
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/product-attribute&target_branch=18.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
Adds a helper function \_get_price() on product.product to compute the
product price based on pricelist, fiscal position, company and date.
The method returns a dict such as:
.. code:: python
{
"value": 600.0,
"tax_included": True,
"discount": 20.0,
"original_value": 750.0,
}
**Table of contents**
.. contents::
:local:
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/product-attribute/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/product-attribute/issues/new?body=module:%20product_get_price_helper%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
-------
* ACSONE SA/NV
Contributors
------------
- Sébastien BEAU <sebastien.beau@akretion.com>
- Simone Orsi <simahawk@gmail.com>
- Quentin Groulard <quentin.groulard@acsone.eu>
Maintainers
-----------
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/product-attribute <https://github.com/OCA/product-attribute/tree/18.0/product_get_price_helper>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View file

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

View file

@ -0,0 +1,15 @@
# Copyright 2023 ACSONE SA/NV
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "Product Get Price Helper",
"summary": """
This module provides a helper function to compute product prices.""",
"version": "18.0.1.1.0",
"license": "AGPL-3",
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/product-attribute",
"depends": ["account", "product"],
"data": [],
"demo": ["demo/account.xml", "demo/pricelist.xml"],
}

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="tax_1" model="account.tax">
<field name="name">Tax inc demo</field>
<field eval="15" name="amount" />
<field name="amount_type">percent</field>
<field name="type_tax_use">sale</field>
<field eval="1" name="price_include" />
</record>
<record id="tax_2" model="account.tax">
<field name="name">Tax exc demo</field>
<field eval="15" name="amount" />
<field name="amount_type">percent</field>
<field name="type_tax_use">sale</field>
</record>
<record id="fiscal_position_0" model="account.fiscal.position">
<field name="name">Default</field>
<field eval="1" name="auto_apply" />
<field name="country_id" ref="base.fr" />
</record>
<record id="fiscal_position_1" model="account.fiscal.position">
<field name="name">Business</field>
<field eval="1" name="auto_apply" />
<field eval="1" name="vat_required" />
<field name="country_id" ref="base.fr" />
</record>
<record id="position_tax_1" model="account.fiscal.position.tax">
<field name="position_id" ref="fiscal_position_1" />
<field name="tax_src_id" ref="tax_1" />
<field name="tax_dest_id" ref="tax_2" />
</record>
</odoo>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!-- pylint:disable=xml-duplicate-record-id -->
<odoo>
<record id="pricelist_1" model="product.pricelist">
<field name="name">Business Pricelist</field>
<field name="currency_id" ref="base.USD" />
</record>
<record id="item_1" model="product.pricelist.item">
<field name="base">list_price</field>
<field name="percent_price" eval="20" />
<field name="name">Default Business Pricelist Line</field>
<field name="pricelist_id" ref="pricelist_1" />
<field name="compute_price">percentage</field>
</record>
<!--
FORCE ONLY ONE ITEM ON THE PRICE LIST
When a price list is created, odoo assign a default price list item/
To be sure that only our new item is assigned to the price list
we reassign the item_ids....
-->
<record id="pricelist_1" model="product.pricelist">
<field
name="item_ids"
eval="[Command.set(ref('product_get_price_helper.item_1'))]"
/>
</record>
</odoo>

View file

@ -0,0 +1,47 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * product_get_price_helper
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-03-24 10:06+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10.2\n"
#. module: product_get_price_helper
#: model:account.fiscal.position,name:product_get_price_helper.fiscal_position_1
msgid "Business"
msgstr "Impresa"
#. module: product_get_price_helper
#: model:product.pricelist,name:product_get_price_helper.pricelist_1
msgid "Business Pricelist"
msgstr "Listino commerciale"
#. module: product_get_price_helper
#: model:account.fiscal.position,name:product_get_price_helper.fiscal_position_0
msgid "Default"
msgstr "Predefinito"
#. module: product_get_price_helper
#: model:ir.model,name:product_get_price_helper.model_product_product
msgid "Product Variant"
msgstr "Variante prodotto"
#. module: product_get_price_helper
#: model:account.tax,name:product_get_price_helper.tax_2
msgid "Tax exc demo"
msgstr "Demo imposta esclusa"
#. module: product_get_price_helper
#: model:account.tax,name:product_get_price_helper.tax_1
msgid "Tax inc demo"
msgstr "Demo imposta inclusa"

View file

@ -0,0 +1,44 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * product_get_price_helper
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \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_get_price_helper
#: model:account.fiscal.position,name:product_get_price_helper.fiscal_position_1
msgid "Business"
msgstr ""
#. module: product_get_price_helper
#: model:product.pricelist,name:product_get_price_helper.pricelist_1
msgid "Business Pricelist"
msgstr ""
#. module: product_get_price_helper
#: model:account.fiscal.position,name:product_get_price_helper.fiscal_position_0
msgid "Default"
msgstr ""
#. module: product_get_price_helper
#: model:ir.model,name:product_get_price_helper.model_product_product
msgid "Product Variant"
msgstr ""
#. module: product_get_price_helper
#: model:account.tax,name:product_get_price_helper.tax_2
msgid "Tax exc demo"
msgstr ""
#. module: product_get_price_helper
#: model:account.tax,name:product_get_price_helper.tax_1
msgid "Tax inc demo"
msgstr ""

View file

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

View file

@ -0,0 +1,120 @@
# Copyright 2017 Akretion (http://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# Copyright 2021 Camptocamp (http://www.camptocamp.com).
# @author Simone Orsi <simahawk@gmail.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models
from odoo.tools import float_compare
from odoo.tools import float_is_zero
from ..utils import float_round
class ProductProduct(models.Model):
_inherit = "product.product"
def _get_price(
self,
qty=1.0,
pricelist=None,
fposition=None,
company=None,
date=None,
price_unit=None,
):
"""Computes the product prices
:param qty: The product quantity, used to apply pricelist rules.
:param pricelist: Optional. Get prices for a specific pricelist.
:param fposition: Optional. Apply fiscal position to product taxes.
:param company: Optional.
:param date: Optional.
:returns: dict with the following keys:
<value> The product unitary price
<tax_included> True if product taxes are included in <price>.
If the pricelist.discount_policy is "without_discount":
<original_value> The original price (before pricelist is applied).
<discount> The discounted percentage.
"""
self.ensure_one()
AccountTax = self.env["account.tax"]
# Apply company
product = self.with_company(company) if company else self
company = company or self.env.company
# Always filter taxes by the company
taxes = product.taxes_id.filtered(lambda tax: tax.company_id == company)
# Apply fiscal position
taxes = fposition.map_tax(taxes) if fposition else taxes
# Set context. Some of the methods used here depend on these values
product_context = dict(
self.env.context,
quantity=qty,
pricelist=pricelist.id if pricelist else None,
fiscal_position=fposition,
date=date,
)
product = product.with_context(**product_context)
pricelist = pricelist.with_context(**product_context) if pricelist else None
if price_unit is None:
price_unit = (
pricelist._get_product_price(product, qty, date=date)
if pricelist
else product.lst_price
)
price_unit = AccountTax._fix_tax_included_price_company(
price_unit, product.taxes_id, taxes, company
)
price_dp = self.env["decimal.precision"].precision_get("Product Price")
price_unit = float_round(price_unit, price_dp)
res = {
"value": price_unit,
"tax_included": any(tax.price_include for tax in taxes),
# Default values in case price.discount_policy != "without_discount"
"original_value": price_unit,
"discount": 0.0,
}
if pricelist:
rule_id = pricelist._get_product_rule(product, qty, date=date)
pl_item = self.env["product.pricelist.item"].browse(rule_id)
if not pl_item.exists():
return res
original_price_unit = product.lst_price
if not pl_item._show_discount():
# If the pricelist does not show the discount, we return the price as is
if float_is_zero(original_price_unit, precision_digits=price_dp):
res["original_value"] = 0.0
return res
# Get the price rule
price_unit, _ = pricelist._get_product_price_rule(product, qty, date=date)
# Get the price before applying the pricelist
price_dp = self.env["decimal.precision"].precision_get("Product Price")
# Compute discount
if not float_is_zero(
original_price_unit, precision_digits=price_dp
) and float_compare(
original_price_unit, price_unit, precision_digits=price_dp
):
discount = (
(original_price_unit - price_unit) / original_price_unit * 100
)
# Apply the right precision on discount
discount_dp = self.env["decimal.precision"].precision_get("Discount")
discount = float_round(discount, discount_dp)
else:
discount = 0.00
# Compute prices
original_price_unit = AccountTax._fix_tax_included_price_company(
original_price_unit, product.taxes_id, taxes, company
)
original_price_unit = float_round(original_price_unit, price_dp)
res.update(
{
"original_value": original_price_unit,
"discount": discount,
}
)
return res

View file

@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View file

@ -0,0 +1,3 @@
- Sébastien BEAU \<<sebastien.beau@akretion.com>\>
- Simone Orsi \<<simahawk@gmail.com>\>
- Quentin Groulard \<<quentin.groulard@acsone.eu>\>

View file

@ -0,0 +1,13 @@
Adds a helper function \_get_price() on product.product to compute the
product price based on pricelist, fiscal position, company and date.
The method returns a dict such as:
``` python
{
"value": 600.0,
"tax_included": True,
"discount": 20.0,
"original_value": 750.0,
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View file

@ -0,0 +1,435 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
<title>Product Get Price Helper</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: gray; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic, pre.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="product-get-price-helper">
<h1 class="title">Product Get Price Helper</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:5fb33150c2c1ee21fd7bc337113f150dccba97ee06c9dfd5a01d2f17ce567509
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/product-attribute/tree/18.0/product_get_price_helper"><img alt="OCA/product-attribute" src="https://img.shields.io/badge/github-OCA%2Fproduct--attribute-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/product-attribute-18-0/product-attribute-18-0-product_get_price_helper"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/product-attribute&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>Adds a helper function _get_price() on product.product to compute the
product price based on pricelist, fiscal position, company and date.</p>
<p>The method returns a dict such as:</p>
<pre class="code python literal-block">
<span class="p">{</span><span class="w">
</span> <span class="s2">&quot;value&quot;</span><span class="p">:</span> <span class="mf">600.0</span><span class="p">,</span><span class="w">
</span> <span class="s2">&quot;tax_included&quot;</span><span class="p">:</span> <span class="kc">True</span><span class="p">,</span><span class="w">
</span> <span class="s2">&quot;discount&quot;</span><span class="p">:</span> <span class="mf">20.0</span><span class="p">,</span><span class="w">
</span> <span class="s2">&quot;original_value&quot;</span><span class="p">:</span> <span class="mf">750.0</span><span class="p">,</span><span class="w">
</span><span class="p">}</span>
</pre>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-1">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-2">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-3">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-4">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-5">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#toc-entry-1">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/product-attribute/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/product-attribute/issues/new?body=module:%20product_get_price_helper%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#toc-entry-2">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#toc-entry-3">Authors</a></h2>
<ul class="simple">
<li>ACSONE SA/NV</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#toc-entry-4">Contributors</a></h2>
<ul class="simple">
<li>Sébastien BEAU &lt;<a class="reference external" href="mailto:sebastien.beau&#64;akretion.com">sebastien.beau&#64;akretion.com</a>&gt;</li>
<li>Simone Orsi &lt;<a class="reference external" href="mailto:simahawk&#64;gmail.com">simahawk&#64;gmail.com</a>&gt;</li>
<li>Quentin Groulard &lt;<a class="reference external" href="mailto:quentin.groulard&#64;acsone.eu">quentin.groulard&#64;acsone.eu</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#toc-entry-5">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org">
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
</a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/product-attribute/tree/18.0/product_get_price_helper">OCA/product-attribute</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View file

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

View file

@ -0,0 +1,275 @@
# Copyright 2017 Akretion (http://www.akretion.com).
# @author Benoît GUILLOT <benoit.guillot@akretion.com>
# Copyright 2025 Camptocamp (http://www.camptocamp.com).
# @author Simone Orsi <simone.orsi@camptocamp.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from unittest import mock
from odoo.tests import TransactionCase
class ProductCase(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.template = cls.env.ref("product.product_product_4_product_template")
cls.variant = cls.env.ref("product.product_product_4b")
cls.template.taxes_id = cls.env.ref("product_get_price_helper.tax_1")
cls.env.user.company_id.currency_id = cls.env.ref("base.USD")
cls.base_pricelist = cls.env["product.pricelist"].create(
{"name": "Base Pricelist", "currency_id": cls.env.ref("base.USD").id}
)
cls.base_pricelist.currency_id = cls.env.ref("base.USD")
cls.variant.currency_id = cls.env.ref("base.USD")
def test_product_simple_get_price(self):
self.variant.taxes_id.price_include = True
self.assertEqual(
self.variant._get_price(),
{
"discount": 0.0,
"original_value": 750.0,
"tax_included": True,
"value": 750.0,
},
)
self.variant.taxes_id.price_include = False
self.assertEqual(
self.variant._get_price(),
{
"discount": 0.0,
"original_value": 750.0,
"tax_included": False,
"value": 750.0,
},
)
def test_product_price_rounding(self):
# Odony example: https://gist.github.com/odony/5269a695545902e7e23e761e20a9ec8c
self.env["product.pricelist.item"].create(
{
"pricelist_id": self.base_pricelist.id,
"product_id": self.variant.id,
"base": "list_price",
"applied_on": "0_product_variant",
"compute_price": "percentage",
"percent_price": 50,
}
)
self.variant.list_price = 423.4
self.assertEqual(
self.variant._get_price(pricelist=self.base_pricelist)["value"], 211.70
)
def test_product_get_price(self):
# self.base_pricelist doesn't define a tax mapping. We are tax included
fiscal_position_fr = self.env.ref("product_get_price_helper.fiscal_position_0")
self.variant.taxes_id.price_include = True
price = self.variant._get_price(
pricelist=self.base_pricelist, fposition=fiscal_position_fr
)
self.assertDictEqual(
price,
{
"discount": 0.0,
"original_value": 750.0,
"tax_included": True,
"value": 750.0,
},
)
# promotion price list define a discount of 20% on all product
promotion_price_list = self.env.ref("product_get_price_helper.pricelist_1")
price = self.variant._get_price(
pricelist=promotion_price_list, fposition=fiscal_position_fr
)
self.assertDictEqual(
price,
{
"discount": 0.0,
"original_value": 600.0,
"tax_included": True,
"value": 600.0,
},
)
# use a fiscal position defining a mapping from tax included to tax
# excluded
tax_exclude_fiscal_position = self.env.ref(
"product_get_price_helper.fiscal_position_1"
)
price = self.variant._get_price(
pricelist=self.base_pricelist, fposition=tax_exclude_fiscal_position
)
self.assertDictEqual(
price,
{
"discount": 0.0,
"original_value": 652.17,
"tax_included": False,
"value": 652.17,
},
)
price = self.variant._get_price(
pricelist=promotion_price_list, fposition=tax_exclude_fiscal_position
)
self.assertDictEqual(
price,
{
"discount": 0.0,
"original_value": 521.74,
"tax_included": False,
"value": 521.74,
},
)
def test_product_get_price_zero(self):
# Test that discount calculation does not fail if original price is 0
self.variant.list_price = 0
self.env["product.pricelist.item"].create(
{
"product_id": self.variant.id,
"pricelist_id": self.base_pricelist.id,
"fixed_price": 10,
}
)
fiscal_position_fr = self.env.ref("product_get_price_helper.fiscal_position_0")
self.variant.taxes_id.price_include = True
price = self.variant.with_context(foo=1)._get_price(
pricelist=self.base_pricelist, fposition=fiscal_position_fr
)
self.assertDictEqual(
price,
{
"discount": 0.0,
"original_value": 0.0,
"tax_included": True,
"value": 10.0,
},
)
# FIXME v18 we cannot use `_show_discount` method
# because it relies on `sale.group_discount_per_so_line` from sale module.
# See https://github.com/odoo/odoo/issues/202035
# The test should be updated when the issue is fixed to use
# self.env.user.groups_id |= self.env.ref("sale.group_discount_per_so_line")
@mock.patch(
"odoo.addons.product.models.product_pricelist_item.PricelistItem._show_discount"
)
def test_product_get_price_per_qty(self, show_discount):
show_discount.return_value = False
self.variant.taxes_id.price_include = True
# Define a promotion price for the product with min_qty = 10
fposition = self.env.ref("product_get_price_helper.fiscal_position_0")
pricelist = self.base_pricelist
self.env["product.pricelist.item"].create(
{
"name": "Discount on Product when Qty >= 10",
"pricelist_id": pricelist.id,
"base": "list_price",
"compute_price": "percentage",
"percent_price": "20",
"applied_on": "0_product_variant",
"product_id": self.variant.id,
"min_quantity": 10.0,
}
)
# Case 1 (qty = 1.0). No discount is applied
price = self.variant._get_price(
qty=1.0, pricelist=pricelist, fposition=fposition
)
self.assertDictEqual(
price,
{
"discount": 0.0,
"original_value": 750.0,
"tax_included": True,
"value": 750.0,
},
)
# Case 2 (qty = 10.0). Discount is applied
# promotion price list define a discount of 20% on all product
price = self.variant._get_price(
qty=10.0, pricelist=pricelist, fposition=fposition
)
self.assertDictEqual(
price,
{
"discount": 0.0,
"original_value": 600.0,
"tax_included": True,
"value": 600.0,
},
)
@mock.patch(
"odoo.addons.product.models.product_pricelist_item.PricelistItem._show_discount"
)
def test_product_get_price_discount_policy(self, show_discount):
self.variant.taxes_id.price_include = True
show_discount.return_value = False
# Ensure that discount is with 2 digits
self.env.ref("product.decimal_discount").digits = 2
# self.base_pricelist doesn't define a tax mapping. We are tax included
# Discount policy: do not show the discount.
fiscal_position_fr = self.env.ref("product_get_price_helper.fiscal_position_0")
price = self.variant._get_price(
pricelist=self.base_pricelist, fposition=fiscal_position_fr
)
self.assertDictEqual(
price,
{
"tax_included": True,
"value": 750.0,
"discount": 0.0,
"original_value": 750.0,
},
)
# promotion price list define a discount of 20% on all product
# Discount policy: show the discount.
show_discount.return_value = True
promotion_price_list = self.env.ref("product_get_price_helper.pricelist_1")
price = self.variant._get_price(
pricelist=promotion_price_list, fposition=fiscal_position_fr
)
self.assertDictEqual(
price,
{
"tax_included": True,
"value": 600.0,
"discount": 20.0,
"original_value": 750.0,
},
)
# use the fiscal position defining a mapping from tax included to tax
# excluded
# Tax mapping should not impact the computation of the discount and
# the original value
tax_exclude_fiscal_position = self.env.ref(
"product_get_price_helper.fiscal_position_1"
)
show_discount.return_value = False
price = self.variant._get_price(
pricelist=self.base_pricelist, fposition=tax_exclude_fiscal_position
)
self.assertDictEqual(
price,
{
"tax_included": False,
"value": 652.17,
"discount": 0.0,
"original_value": 652.17,
},
)
show_discount.return_value = True
price = self.variant._get_price(
pricelist=promotion_price_list, fposition=tax_exclude_fiscal_position
)
self.assertDictEqual(
price,
{
"tax_included": False,
"value": 521.74,
"discount": 20.0,
"original_value": 652.17,
},
)

View file

@ -0,0 +1,13 @@
# Copyright 2021 Camptocamp (http://www.camptocamp.com).
# @author Simone Orsi <simahawk@gmail.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.tools import float_repr
def float_round(value, dp):
# be carefull odoo rounding implementation do not return the shortest
# representation of a float this mean if the price_unit is 211.70
# you will have 211.70000000000002
# Odony exemple: https://gist.github.com/odony/5269a695545902e7e23e761e20a9ec8c
return float(float_repr(value, dp))