Skip to content

Commit 38754dc

Browse files
committed
[ADD] sale_order_net_price_control
Controls the net price of the products when they are sold. It has to be higher than the last price on a purchase invoice and higher than the lowest pricelist. Allows users who are in the 'Sell Products at Any Price' group to bypass all controls.
1 parent 86f1818 commit 38754dc

18 files changed

Lines changed: 837 additions & 0 deletions
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
============================
2+
Sale Order Net Price Control
3+
============================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:57e5220b2d044eefc93047453d0c4f5987fe98801ee6e0a63cfc9eedde0e1233
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
18+
:alt: License: LGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-solvosci%2Fslv--sale-lightgray.png?logo=github
20+
:target: https://github.com/solvosci/slv-sale/tree/17.0/sale_order_net_price_control
21+
:alt: solvosci/slv-sale
22+
23+
|badge1| |badge2| |badge3|
24+
25+
Controls the net price of the products when they are sold.
26+
It has to be higher than the last price on a purchase invoice and higher than the lowest pricelist.
27+
Allows users who are in the 'Sell Products at Any Price' group to bypass all controls.
28+
29+
**Table of contents**
30+
31+
.. contents::
32+
:local:
33+
34+
Bug Tracker
35+
===========
36+
37+
Bugs are tracked on `GitHub Issues <https://github.com/solvosci/slv-sale/issues>`_.
38+
In case of trouble, please check there if your issue has already been reported.
39+
If you spotted it first, help us to smash it by providing a detailed and welcomed
40+
`feedback <https://github.com/solvosci/slv-sale/issues/new?body=module:%20sale_order_net_price_control%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
41+
42+
Do not contact contributors directly about support or help with technical issues.
43+
44+
Credits
45+
=======
46+
47+
Authors
48+
~~~~~~~
49+
50+
* Solvos
51+
52+
Contributors
53+
~~~~~~~~~~~~
54+
55+
* Iria Alonso <iria.alonso@solvos.es>
56+
57+
Maintainers
58+
~~~~~~~~~~~
59+
60+
This module is part of the `solvosci/slv-sale <https://github.com/solvosci/slv-sale/tree/17.0/sale_order_net_price_control>`_ project on GitHub.
61+
62+
You are welcome to contribute.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# © 2025 Solvos Consultoría Informática (<http://www.solvos.es>)
2+
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
3+
{
4+
"name": "Sale Order Net Price Control",
5+
"summary": """
6+
Controls the net price of the products when they are sold.
7+
It has to be higher than the last price on a purchase invoice and higher than the lowest pricelist.
8+
Allows users who are in the 'Sell Products at Any Price' group to bypass all controls.
9+
""",
10+
"author": "Solvos",
11+
"license": "LGPL-3",
12+
"version": "17.0.1.0.0",
13+
'category': "Operations/Sale",
14+
"website": "https://github.com/solvosci/slv-sale",
15+
"depends": ["sale"],
16+
"data": [
17+
"security/sale_order_security.xml",
18+
"views/res_config_settings_views.xml",
19+
"views/sale_order_views.xml",
20+
"views/product_template.xml",
21+
],
22+
'installable': True,
23+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Translation of Odoo Server.
2+
# This file contains the translation of the following modules:
3+
# * sale_order_net_price_control
4+
#
5+
msgid ""
6+
msgstr ""
7+
"Project-Id-Version: Odoo Server 17.0\n"
8+
"Report-Msgid-Bugs-To: \n"
9+
"POT-Creation-Date: 2025-06-17 11:20+0000\n"
10+
"PO-Revision-Date: 2025-06-17 11:20+0000\n"
11+
"Last-Translator: \n"
12+
"Language-Team: \n"
13+
"MIME-Version: 1.0\n"
14+
"Content-Type: text/plain; charset=UTF-8\n"
15+
"Content-Transfer-Encoding: \n"
16+
"Plural-Forms: \n"
17+
18+
#. module: sale_order_net_price_control
19+
#: model:ir.model.fields,field_description:sale_order_net_price_control.field_product_product__admits_offer_prices
20+
#: model:ir.model.fields,field_description:sale_order_net_price_control.field_product_template__admits_offer_prices
21+
#: model:ir.model.fields,field_description:sale_order_net_price_control.field_sale_order_line__admits_offer_prices
22+
msgid "Admits Offer Prices"
23+
msgstr "Admite precios de oferta"
24+
25+
#. module: sale_order_net_price_control
26+
#: model:ir.model,name:sale_order_net_price_control.model_res_company
27+
msgid "Companies"
28+
msgstr "Compañías"
29+
30+
#. module: sale_order_net_price_control
31+
#: model:ir.model,name:sale_order_net_price_control.model_res_config_settings
32+
msgid "Config Settings"
33+
msgstr "Ajustes de configuración"
34+
35+
#. module: sale_order_net_price_control
36+
#. odoo-python
37+
#: code:addons/sale_order_net_price_control/models/sale_order_line.py:0
38+
#, python-format
39+
msgid ""
40+
"Net price of '%s', '%.1f' must be higher that the last purchase invoice "
41+
"price '%s'"
42+
msgstr ""
43+
"El precio neto de '%s', '%.1f' debe ser mayor que el precio de la última "
44+
"factura de compra '%s'"
45+
46+
#. module: sale_order_net_price_control
47+
#. odoo-python
48+
#: code:addons/sale_order_net_price_control/models/sale_order_line.py:0
49+
#, python-format
50+
msgid ""
51+
"Net price of '%s', '%.1f' must be higher that the lowest pricelist '%s', "
52+
"'%s'"
53+
msgstr ""
54+
"El precio neto de '%s', '%.1f' debe ser mayor que el precio de la tarifa '%s',"
55+
"'%s'"
56+
57+
#. module: sale_order_net_price_control
58+
#: model:ir.model.fields,field_description:sale_order_net_price_control.field_sale_order_line__offer_price
59+
msgid "Offer Price"
60+
msgstr "Precio de oferta"
61+
62+
#. module: sale_order_net_price_control
63+
#: model:ir.model.fields,field_description:sale_order_net_price_control.field_res_company__lowest_pricelist_id
64+
#: model:ir.model.fields,field_description:sale_order_net_price_control.field_res_config_settings__lowest_pricelist_id
65+
#: model_terms:ir.ui.view,arch_db:sale_order_net_price_control.res_config_settings_view_form
66+
msgid "Pricelist"
67+
msgstr "Tarifa"
68+
69+
#. module: sale_order_net_price_control
70+
#: model:ir.model,name:sale_order_net_price_control.model_product_template
71+
msgid "Product"
72+
msgstr "Producto"
73+
74+
#. module: sale_order_net_price_control
75+
#: model_terms:ir.ui.view,arch_db:sale_order_net_price_control.res_config_settings_view_form
76+
msgid "Reference base pricelist for minimum price control"
77+
msgstr "Tarifa base de referencia para control de precios mínimos"
78+
79+
#. module: sale_order_net_price_control
80+
#: model:ir.model,name:sale_order_net_price_control.model_sale_order_line
81+
msgid "Sales Order Line"
82+
msgstr "Línea de pedido de venta"
83+
84+
#. module: sale_order_net_price_control
85+
#: model:res.groups,name:sale_order_net_price_control.group_sale_at_any_price
86+
msgid "Sell Products at Any Price"
87+
msgstr "Vender productos a cualquier precio"
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Translation of Odoo Server.
2+
# This file contains the translation of the following modules:
3+
# * sale_order_net_price_control
4+
#
5+
msgid ""
6+
msgstr ""
7+
"Project-Id-Version: Odoo Server 17.0\n"
8+
"Report-Msgid-Bugs-To: \n"
9+
"POT-Creation-Date: 2025-06-17 11:20+0000\n"
10+
"PO-Revision-Date: 2025-06-17 11:20+0000\n"
11+
"Last-Translator: \n"
12+
"Language-Team: \n"
13+
"MIME-Version: 1.0\n"
14+
"Content-Type: text/plain; charset=UTF-8\n"
15+
"Content-Transfer-Encoding: \n"
16+
"Plural-Forms: \n"
17+
18+
#. module: sale_order_net_price_control
19+
#: model:ir.model.fields,field_description:sale_order_net_price_control.field_product_product__admits_offer_prices
20+
#: model:ir.model.fields,field_description:sale_order_net_price_control.field_product_template__admits_offer_prices
21+
#: model:ir.model.fields,field_description:sale_order_net_price_control.field_sale_order_line__admits_offer_prices
22+
msgid "Admits Offer Prices"
23+
msgstr ""
24+
25+
#. module: sale_order_net_price_control
26+
#: model:ir.model,name:sale_order_net_price_control.model_res_company
27+
msgid "Companies"
28+
msgstr ""
29+
30+
#. module: sale_order_net_price_control
31+
#: model:ir.model,name:sale_order_net_price_control.model_res_config_settings
32+
msgid "Config Settings"
33+
msgstr ""
34+
35+
#. module: sale_order_net_price_control
36+
#. odoo-python
37+
#: code:addons/sale_order_net_price_control/models/sale_order_line.py:0
38+
#, python-format
39+
msgid ""
40+
"Net price of '%s', '%.1f' must be higher that the last purchase invoice "
41+
"price '%s'"
42+
msgstr ""
43+
44+
#. module: sale_order_net_price_control
45+
#. odoo-python
46+
#: code:addons/sale_order_net_price_control/models/sale_order_line.py:0
47+
#, python-format
48+
msgid ""
49+
"Net price of '%s', '%.1f' must be higher that the lowest pricelist '%s', "
50+
"'%s'"
51+
msgstr ""
52+
53+
#. module: sale_order_net_price_control
54+
#: model:ir.model.fields,field_description:sale_order_net_price_control.field_sale_order_line__offer_price
55+
msgid "Offer Price"
56+
msgstr ""
57+
58+
#. module: sale_order_net_price_control
59+
#: model:ir.model.fields,field_description:sale_order_net_price_control.field_res_company__lowest_pricelist_id
60+
#: model:ir.model.fields,field_description:sale_order_net_price_control.field_res_config_settings__lowest_pricelist_id
61+
#: model_terms:ir.ui.view,arch_db:sale_order_net_price_control.res_config_settings_view_form
62+
msgid "Pricelist"
63+
msgstr ""
64+
65+
#. module: sale_order_net_price_control
66+
#: model:ir.model,name:sale_order_net_price_control.model_product_template
67+
msgid "Product"
68+
msgstr ""
69+
70+
#. module: sale_order_net_price_control
71+
#: model_terms:ir.ui.view,arch_db:sale_order_net_price_control.res_config_settings_view_form
72+
msgid "Reference base pricelist for minimum price control"
73+
msgstr ""
74+
75+
#. module: sale_order_net_price_control
76+
#: model:ir.model,name:sale_order_net_price_control.model_sale_order_line
77+
msgid "Sales Order Line"
78+
msgstr ""
79+
80+
#. module: sale_order_net_price_control
81+
#: model:res.groups,name:sale_order_net_price_control.group_sale_at_any_price
82+
msgid "Sell Products at Any Price"
83+
msgstr ""
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from . import sale_order_line
2+
from . import res_company
3+
from . import res_config_settings
4+
from . import product_template
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# © 2025 Solvos Consultoría Informática (<http://www.solvos.es>)
2+
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
3+
4+
from odoo import models, fields
5+
6+
7+
class ProductTemplate(models.Model):
8+
_inherit = "product.template"
9+
10+
admits_offer_prices = fields.Boolean()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# © 2025 Solvos Consultoría Informática (<http://www.solvos.es>)
2+
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
3+
4+
from odoo import models, fields
5+
6+
7+
class ResCompany(models.Model):
8+
_inherit = "res.company"
9+
10+
lowest_pricelist_id = fields.Many2one(comodel_name='product.pricelist', string='Pricelist')
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# © 2025 Solvos Consultoría Informática (<http://www.solvos.es>)
2+
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
3+
4+
from odoo import models, fields
5+
6+
7+
class ResConfigSettings(models.TransientModel):
8+
_inherit = 'res.config.settings'
9+
10+
lowest_pricelist_id = fields.Many2one(related="company_id.lowest_pricelist_id", readonly=False, string='Pricelist')
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# © 2025 Solvos Consultoría Informática (<http://www.solvos.es>)
2+
# License LGPL-3 - See http://www.gnu.org/licenses/lgpl-3.0.html
3+
4+
from odoo import models, _, fields
5+
from odoo.exceptions import ValidationError
6+
7+
8+
class SaleOrderLine(models.Model):
9+
_inherit = "sale.order.line"
10+
11+
offer_price = fields.Boolean()
12+
admits_offer_prices = fields.Boolean(related='product_id.admits_offer_prices', store=True)
13+
14+
def _compute_amount(self):
15+
super()._compute_amount()
16+
if not self.env.user.has_group('sale_order_net_price_control.group_sale_at_any_price'):
17+
18+
for record in self.filtered(lambda r: r.product_id and r.product_uom_qty > 0):
19+
20+
net_price_sale = record.price_subtotal / record.product_uom_qty
21+
22+
purchase_invoice = self.env['account.move.line'].search([
23+
('product_id', '=', record.product_id.id),
24+
('quantity', '>', 0),
25+
('move_id.move_type', '=', 'in_invoice'),
26+
('move_id.state', '=', 'posted'),
27+
('display_type', '=', 'product'),
28+
], limit=1, order='name desc')
29+
30+
if purchase_invoice:
31+
invoice_line = purchase_invoice.filtered(
32+
lambda x: x.product_id.id == record.product_id.id
33+
)
34+
net_price_purchase = invoice_line.price_subtotal / invoice_line.quantity
35+
36+
if net_price_sale <= net_price_purchase:
37+
raise ValidationError(_(
38+
"Net price of '%s', '%.1f' must be higher that the last purchase invoice price '%s'"
39+
)%(record.product_template_id.name,net_price_sale,net_price_purchase) )
40+
if not record.offer_price:
41+
if net_price_sale <= record._get_price_from_lowest_pricelist_id():
42+
raise ValidationError(_(
43+
"Net price of '%s', '%.1f' must be higher that the lowest pricelist '%s', '%s'"
44+
)%(record.product_template_id.name,net_price_sale,self.env.user.company_id.lowest_pricelist_id.name,record._get_price_from_lowest_pricelist_id()))
45+
46+
def _get_price_from_lowest_pricelist_id(self):
47+
company = self._context.get("company_id", False) and self.env["res.company"].browse(
48+
self._context["company_id"]
49+
) or self.env.user.company_id
50+
51+
sale_order_obj = self.env['sale.order']
52+
53+
order_line_vals = []
54+
for line in self:
55+
line_vals = {
56+
"product_id": line.product_id.id,
57+
"product_uom_qty": line.product_uom_qty,
58+
"order_id": False,
59+
}
60+
order_line_vals.append(line_vals)
61+
62+
so_vals = {
63+
"partner_id": self.order_id.partner_id.id,
64+
"pricelist_id": company.lowest_pricelist_id.id,
65+
"order_line": order_line_vals,
66+
}
67+
valued_order = sale_order_obj.new(so_vals)
68+
69+
price = valued_order.order_line.price_unit
70+
discount = valued_order.order_line.discount
71+
return price - (price * (discount/100))

0 commit comments

Comments
 (0)