diff --git a/product_sale_price_from_pricelist/__manifest__.py b/product_sale_price_from_pricelist/__manifest__.py index 45f8f85..5c18831 100644 --- a/product_sale_price_from_pricelist/__manifest__.py +++ b/product_sale_price_from_pricelist/__manifest__.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { # noqa: B018 "name": "Product Sale Price from Pricelist", - "version": "18.0.2.6.0", + "version": "18.0.2.7.0", "category": "product", "summary": "Set sale price from pricelist based on last purchase price", "author": "Odoo Community Association (OCA), Criptomart", @@ -20,5 +20,6 @@ "data": [ "views/actions.xml", "views/product_view.xml", + "views/sale_order_view.xml", ], } diff --git a/product_sale_price_from_pricelist/i18n/es.po b/product_sale_price_from_pricelist/i18n/es.po index 272316f..cf57d19 100644 --- a/product_sale_price_from_pricelist/i18n/es.po +++ b/product_sale_price_from_pricelist/i18n/es.po @@ -187,6 +187,32 @@ msgstr "Triple descuento" msgid "Update Sales Price" msgstr "Actualizar Precio de Venta" +#. module: product_sale_price_from_pricelist +#: model:ir.actions.server,name:product_sale_price_from_pricelist.action_sale_update_from_list_price +#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price +msgid "Update with List Price" +msgstr "Actualizar con precio de venta" + +#. module: product_sale_price_from_pricelist +#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price +msgid "Update skipped" +msgstr "Actualización omitida" + +#. module: product_sale_price_from_pricelist +#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price +msgid "No sales orders to update. Only non-invoiced and non-canceled orders are processed." +msgstr "No hay pedidos de venta que actualizar. Solo se procesan los no facturados ni cancelados." + +#. module: product_sale_price_from_pricelist +#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price +msgid "Sales price update" +msgstr "Actualización de precios de venta" + +#. module: product_sale_price_from_pricelist +#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price +msgid "Updated %(lines)s lines across %(orders)s sales orders." +msgstr "Se actualizaron %(lines)s líneas en %(orders)s pedidos de venta." + #. module: product_sale_price_from_pricelist #: model:ir.model.fields.selection,name:product_sale_price_from_pricelist.selection__product_template__last_purchase_price_compute_type__without_discounts msgid "Without discounts" diff --git a/product_sale_price_from_pricelist/i18n/eu.po b/product_sale_price_from_pricelist/i18n/eu.po new file mode 100644 index 0000000..e709215 --- /dev/null +++ b/product_sale_price_from_pricelist/i18n/eu.po @@ -0,0 +1,43 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# \t* product_sale_price_from_pricelist +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-02-27 00:00+0000\n" +"PO-Revision-Date: 2026-02-27 00:00+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: eu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: product_sale_price_from_pricelist +#: model:ir.actions.server,name:product_sale_price_from_pricelist.action_sale_update_from_list_price +#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price +msgid "Update with List Price" +msgstr "Eguneratu tarifa-prezioarekin" + +#. module: product_sale_price_from_pricelist +#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price +msgid "Update skipped" +msgstr "Eguneratzea saltatu da" + +#. module: product_sale_price_from_pricelist +#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price +msgid "No sales orders to update. Only non-invoiced and non-canceled orders are processed." +msgstr "Ez dago eguneratzeko salmenta-agindurik. Soilik fakturatu gabe eta bertan behera utzi gabe daudenak prozesatzen dira." + +#. module: product_sale_price_from_pricelist +#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price +msgid "Sales price update" +msgstr "Salmenta-prezioaren eguneraketa" + +#. module: product_sale_price_from_pricelist +#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price +msgid "Updated %(lines)s lines across %(orders)s sales orders." +msgstr "Eguneratu dira %(orders)s salmenta-agindutako %(lines)s lerro." diff --git a/product_sale_price_from_pricelist/models/__init__.py b/product_sale_price_from_pricelist/models/__init__.py index f4e7395..97fb5c6 100644 --- a/product_sale_price_from_pricelist/models/__init__.py +++ b/product_sale_price_from_pricelist/models/__init__.py @@ -1,6 +1,7 @@ -from . import product_pricelist -from . import product_pricelist_item -from . import product_product -from . import product_template -from . import res_config -from . import stock_move +from . import product_pricelist # noqa: F401 +from . import product_pricelist_item # noqa: F401 +from . import product_product # noqa: F401 +from . import product_template # noqa: F401 +from . import res_config # noqa: F401 +from . import stock_move # noqa: F401 +from . import sale_order # noqa: F401 diff --git a/product_sale_price_from_pricelist/models/sale_order.py b/product_sale_price_from_pricelist/models/sale_order.py new file mode 100644 index 0000000..2895f31 --- /dev/null +++ b/product_sale_price_from_pricelist/models/sale_order.py @@ -0,0 +1,72 @@ +import logging + +from odoo import models + +_logger = logging.getLogger(__name__) + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def action_update_lines_from_list_price(self): + """Copy current product list price into sale order lines. + + Applies to orders that are not fully invoiced. Skips display and + downpayment lines, keeps discounts. Totals/taxes are recomputed by + field dependencies when the lines change. + """ + + eligible_orders = self.filtered( + lambda so: so.invoice_status not in ("invoiced", "up_to_date") + and so.state != "cancel" + ) + + if not eligible_orders: + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": self.env._("Update skipped"), + "message": self.env._( + "No sales orders to update. Only non-invoiced and non-canceled orders are processed." + ), + "type": "warning", + "sticky": False, + }, + } + + total_lines = 0 + for order in eligible_orders: + lines = order.order_line.filtered( + lambda line: not line.display_type + and not line.is_downpayment + and line.product_id + ) + for line in lines: + price_unit = line.product_id.uom_id._compute_price( + line.product_id.lst_price, line.product_uom + ) + line.price_unit = price_unit + + total_lines += len(lines) + _logger.info( + "[SALE PRICE UPDATE] Order %s (%s): updated %s lines", + order.name, + order.id, + len(lines), + ) + + return { + "type": "ir.actions.client", + "tag": "display_notification", + "params": { + "title": self.env._("Sales price update"), + "message": self.env._( + "Updated %(lines)s lines across %(orders)s sales orders.", + lines=total_lines, + orders=len(eligible_orders), + ), + "type": "success" if total_lines else "warning", + "sticky": False, + }, + } diff --git a/product_sale_price_from_pricelist/tests/test_sale_order_update.py b/product_sale_price_from_pricelist/tests/test_sale_order_update.py new file mode 100644 index 0000000..0eefca0 --- /dev/null +++ b/product_sale_price_from_pricelist/tests/test_sale_order_update.py @@ -0,0 +1,95 @@ +# Copyright (C) 2026: Criptomart (https://criptomart.net) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.tests import tagged +from odoo.tests.common import TransactionCase + + +@tagged("post_install", "-at_install") +class TestSaleOrderUpdate(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env.ref("base.res_partner_1") + + cls.product = cls.env["product.product"].create( + { + "name": "Test Product SO", + "list_price": 120.0, + "standard_price": 80.0, + } + ) + + cls.downpayment_product = cls.env["product.product"].create( + { + "name": "Downpayment Product", + "type": "service", + "list_price": 10.0, + } + ) + + def _create_sale_order(self, values=None): + vals = { + "partner_id": self.partner.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom": self.product.uom_id.id, + "product_uom_qty": 2.0, + "price_unit": 50.0, + "tax_id": [(6, 0, self.product.taxes_id.ids)], + }, + ), + (0, 0, {"display_type": "line_note", "name": "Note"}), + ( + 0, + 0, + { + "product_id": self.downpayment_product.id, + "product_uom": self.downpayment_product.uom_id.id, + "product_uom_qty": 1.0, + "price_unit": 10.0, + "is_downpayment": True, + }, + ), + ], + } + if values: + vals.update(values) + return self.env["sale.order"].create(vals) + + def test_update_lines_copies_list_price(self): + order = self._create_sale_order() + + order.action_update_lines_from_list_price() + + line = order.order_line.filtered(lambda line: line.product_id == self.product) + self.assertTrue(line) + self.assertEqual(line.price_unit, self.product.list_price) + + # Downpayment and display lines remain unchanged + down_line = order.order_line.filtered("is_downpayment") + self.assertTrue(down_line) + self.assertEqual(down_line.price_unit, 10.0) + + def test_invoiced_orders_are_skipped(self): + order = self._create_sale_order() + order.action_confirm() + invoice = order._create_invoices() + invoice.action_post() + order._compute_invoice_status() + self.assertEqual(order.invoice_status, "invoiced") + + # Change product price to ensure potential change + order.order_line.filtered( + lambda line: line.product_id == self.product + ).price_unit = 5.0 + + order.action_update_lines_from_list_price() + + # Price should remain untouched because order is invoiced + line = order.order_line.filtered(lambda line: line.product_id == self.product) + self.assertEqual(line.price_unit, 5.0) diff --git a/product_sale_price_from_pricelist/views/sale_order_view.xml b/product_sale_price_from_pricelist/views/sale_order_view.xml new file mode 100644 index 0000000..dd04f5a --- /dev/null +++ b/product_sale_price_from_pricelist/views/sale_order_view.xml @@ -0,0 +1,47 @@ + + + + + + + Update with List Price + + + list,form + code + records.action_update_lines_from_list_price() + + + + + sale.order.form.update.list.price + sale.order + + + +