diff --git a/product_sale_price_from_pricelist/CHANGELOG.md b/product_sale_price_from_pricelist/CHANGELOG.md index 2c09d71..0da015c 100644 --- a/product_sale_price_from_pricelist/CHANGELOG.md +++ b/product_sale_price_from_pricelist/CHANGELOG.md @@ -5,15 +5,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [18.0.2.7.0] - 2026-02-27 - -### Added - -- New action/button on sales orders to update non-invoiced orders from current product sale price (skips notes/downpayments, keeps discounts) -- Server action binding for mass update from list view -- Tests covering sales order update flow and invoiced order exclusion -- Translations updated (es, eu) for the new UI strings - ## [18.0.2.6.0] - 2026-02-14 ### Fixed diff --git a/product_sale_price_from_pricelist/README.rst b/product_sale_price_from_pricelist/README.rst index f91561b..12320f3 100644 --- a/product_sale_price_from_pricelist/README.rst +++ b/product_sale_price_from_pricelist/README.rst @@ -29,7 +29,6 @@ This module automatically calculates and updates product sale prices based on th * **Batch Updates**: Update theoretical prices for multiple products * **Product Flags**: Mark products for price updates and track status * **Variant Architecture**: All business logic in product.product for proper pricelist handling -* **Sales Orders Update**: Action/button to sync sale order lines (not invoiced) with current product sale price **Table of contents** @@ -96,12 +95,6 @@ Usage #. System recalculates theoretical prices based on current settings #. Review and apply changes -**Update Sales Orders from List Price:** - -#. Go to Sales → Orders (form or list) and use **Update with List Price** -#. Only orders not invoiced/canceled are processed -#. The action copies current product sale price to normal lines (skipping notes and downpayments) and keeps discounts - **Price Calculation Flow:** #. Purchase order is received diff --git a/product_sale_price_from_pricelist/README_DEV.md b/product_sale_price_from_pricelist/README_DEV.md index 24b3b3e..6fd86ff 100644 --- a/product_sale_price_from_pricelist/README_DEV.md +++ b/product_sale_price_from_pricelist/README_DEV.md @@ -15,7 +15,6 @@ Automatically calculate and update product sale prices based on the last purchas - **UoM Conversion**: Handles different purchase and sale units of measure - **Batch Updates**: Update theoretical prices for multiple products - **Product Flags**: Mark products for price updates and track status -- **Sales Orders Update**: Action/button to sync non-invoiced sale order lines with current product sale price ## Configuration @@ -40,7 +39,6 @@ Automatically calculate and update product sale prices based on the last purchas 3. Sale price is calculated from the configured pricelist using the cost price 4. Taxes are automatically applied based on product tax settings 5. Go to **Products > Update Theoretical Prices** to batch update prices -6. Go to **Sales > Orders** and use **Update with List Price** (form or list). Applies only to non-invoiced/non-canceled orders, skips notes and downpayments, preserves discounts. ## Technical Architecture diff --git a/product_sale_price_from_pricelist/__manifest__.py b/product_sale_price_from_pricelist/__manifest__.py index 5c18831..45f8f85 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.7.0", + "version": "18.0.2.6.0", "category": "product", "summary": "Set sale price from pricelist based on last purchase price", "author": "Odoo Community Association (OCA), Criptomart", @@ -20,6 +20,5 @@ "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 cf57d19..272316f 100644 --- a/product_sale_price_from_pricelist/i18n/es.po +++ b/product_sale_price_from_pricelist/i18n/es.po @@ -187,32 +187,6 @@ 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 deleted file mode 100644 index e709215..0000000 --- a/product_sale_price_from_pricelist/i18n/eu.po +++ /dev/null @@ -1,43 +0,0 @@ -# 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 97fb5c6..f4e7395 100644 --- a/product_sale_price_from_pricelist/models/__init__.py +++ b/product_sale_price_from_pricelist/models/__init__.py @@ -1,7 +1,6 @@ -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 +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 diff --git a/product_sale_price_from_pricelist/models/sale_order.py b/product_sale_price_from_pricelist/models/sale_order.py deleted file mode 100644 index 2895f31..0000000 --- a/product_sale_price_from_pricelist/models/sale_order.py +++ /dev/null @@ -1,72 +0,0 @@ -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/readme/USAGE.rst b/product_sale_price_from_pricelist/readme/USAGE.rst index 0f48773..dd66ef2 100644 --- a/product_sale_price_from_pricelist/readme/USAGE.rst +++ b/product_sale_price_from_pricelist/readme/USAGE.rst @@ -12,12 +12,6 @@ #. System recalculates theoretical prices based on current settings #. Review and apply changes -**Update Sales Orders from List Price:** - -#. Go to Sales → Orders (form or list) and click **Update with List Price** -#. Only orders not invoiced or canceled are processed -#. Copies current product sale price to normal lines (skips notes/sections and downpayments) and preserves discounts - **Price Calculation Flow:** #. Purchase order is received 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 deleted file mode 100644 index 0eefca0..0000000 --- a/product_sale_price_from_pricelist/tests/test_sale_order_update.py +++ /dev/null @@ -1,95 +0,0 @@ -# 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 deleted file mode 100644 index dd04f5a..0000000 --- a/product_sale_price_from_pricelist/views/sale_order_view.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - Update with List Price - - - list,form - code - records.action_update_lines_from_list_price() - - - - - sale.order.form.update.list.price - sale.order - - - - + + + +
+

${ + labels.draft_exists_message || "A draft already exists" + }

+

${ + labels.draft_two_options || "You have two options:" + }

+ + +
+
+ ${ + labels.draft_option1_title || "Option 1" + } +
+
+

${labels.draft_option1_desc || "Merge with existing"}

+
    +
  • ${existing_items.length} ${ + labels.draft_items_count || "items" + } - ${labels.draft_existing_items || "Existing"}
  • +
  • ${current_items.length} ${ + labels.draft_items_count || "items" + } - ${labels.draft_current_items || "Current"}
  • +
+

+ ${labels.draft_merge_note || "Products will be merged"} +

+
+
+ + +
+
+ ${ + labels.draft_option2_title || "Option 2" + } +
+
+

${labels.draft_option2_desc || "Replace with current"}

+

+ ${ + labels.draft_replace_warning || + "Old draft will be deleted" + } +

+
+
+
+ + +
+ + + +
+ + + `; + + // Remove existing modal if any + var existingModal = document.getElementById("draftConflictModal"); + if (existingModal) { + existingModal.remove(); + } + + // Add modal to body + document.body.insertAdjacentHTML("beforeend", modalHTML); + var modalElement = document.getElementById("draftConflictModal"); + + // Handle close buttons + document.querySelectorAll(".draft-modal-close").forEach(function (btn) { + btn.addEventListener("click", function () { + modalElement.remove(); + }); + }); + + // Handle merge button + document.getElementById("mergeBtn").addEventListener("click", function () { + var existingId = this.getAttribute("data-existing-id"); + modalElement.remove(); + self._executeSaveDraftWithAction("merge", existingId); + }); + + // Handle replace button + document.getElementById("replaceBtn").addEventListener("click", function () { + var existingId = this.getAttribute("data-existing-id"); + modalElement.remove(); + self._executeSaveDraftWithAction("replace", existingId); + }); + + // Close modal when clicking outside + modalElement.addEventListener("click", function (e) { + if (e.target === modalElement) { + modalElement.remove(); + } + }); + }, + + _executeSaveDraftWithAction: function (action, existingDraftId) { + /** + * Execute save draft with merge or replace action. + */ + var self = this; + var items = []; + + Object.keys(this.cart).forEach(function (productId) { + var item = self.cart[productId]; + items.push({ + product_id: productId, + product_name: item.name, + quantity: item.qty, + product_price: item.price, + }); + }); + + var orderData = { + order_id: this.orderId, + items: items, + merge_action: action, + existing_draft_id: existingDraftId, + }; + + var xhr = new XMLHttpRequest(); + xhr.open("POST", "/eskaera/save-order", true); + xhr.setRequestHeader("Content-Type", "application/json"); + + xhr.onload = function () { + if (xhr.status === 200) { + try { + var data = JSON.parse(xhr.responseText); + console.log("Response:", data); + if (data.success) { + // Use server-provided labels instead of hardcoding + var labels = self._getLabels(); + + // Use the translated messages from server + var msg = data.merged + ? "✓ " + + (labels.draft_merged_success || "Draft merged successfully") + : "✓ " + + (labels.draft_replaced_success || "Draft replaced successfully"); + + self._showNotification(msg, "success", 5000); + } else { + var labels = self._getLabels(); + self._showNotification( + "Error: " + (data.error || labels.error_unknown || "Unknown error"), + "danger" + ); + } + } catch (e) { + console.error("Error parsing response:", e); + self._showNotification("Error processing response", "danger"); + } + } else { + try { + var errorData = JSON.parse(xhr.responseText); + self._showNotification( + "Error " + xhr.status + ": " + (errorData.error || "Request error"), + "danger" + ); + } catch (e) { + self._showNotification( + "Error saving order (HTTP " + xhr.status + ")", + "danger" + ); + } + } + }; + + xhr.onerror = function () { + self._showNotification("Connection error", "danger"); + }; + + xhr.send(JSON.stringify(orderData)); + }, + _confirmOrder: function () { console.log("=== _confirmOrder started ==="); console.log("orderId:", this.orderId); diff --git a/website_sale_aplicoop/tests/test_pricing_with_pricelist.py b/website_sale_aplicoop/tests/test_pricing_with_pricelist.py index 8eb4114..f992915 100644 --- a/website_sale_aplicoop/tests/test_pricing_with_pricelist.py +++ b/website_sale_aplicoop/tests/test_pricing_with_pricelist.py @@ -16,8 +16,6 @@ Coverage: from odoo.tests import tagged from odoo.tests.common import TransactionCase -from ..controllers.website_sale import AplicoopWebsiteSale - @tagged("post_install", "-at_install") class TestPricingWithPricelist(TransactionCase): @@ -51,7 +49,6 @@ class TestPricingWithPricelist(TransactionCase): "company_ids": [(6, 0, [self.company.id])], } ) - self.partner = self.user.partner_id # Get or create default tax group tax_group = self.env["account.tax.group"].search( @@ -164,9 +161,6 @@ class TestPricingWithPricelist(TransactionCase): } ) - # Controller helper - self.controller = AplicoopWebsiteSale() - # Create group order self.group_order = self.env["group.order"].create( { @@ -499,52 +493,3 @@ class TestPricingWithPricelist(TransactionCase): except Exception: # If it raises, that's also acceptable behavior self.assertTrue(True, "Negative quantity properly rejected") - - def test_pricing_helper_uses_config_pricelist_and_taxes(self): - """Pricing helper must apply configured pricelist and include taxes for display.""" - - website = self.env.ref("website.default_website").sudo() - website.write( - { - "show_line_subtotals_tax_selection": "tax_included", - "company_id": self.company.id, - } - ) - - pricelist_discount = self.env["product.pricelist"].create( - { - "name": "Config Pricelist 10%", - "company_id": self.company.id, - "item_ids": [ - ( - 0, - 0, - { - "compute_price": "percentage", - "percent_price": 10.0, - "applied_on": "3_global", - }, - ) - ], - } - ) - - self.env["ir.config_parameter"].sudo().set_param( - "website_sale_aplicoop.pricelist_id", pricelist_discount.id - ) - - product = self.product_with_tax # 100€ + 21% - - pricing = self.controller._get_pricing_info( - product, - pricelist_discount, - quantity=1.0, - partner=self.partner, - ) - - # price_unit uses pricelist (10% discount) - self.assertAlmostEqual(pricing["price_unit"], 90.0, places=2) - # display price must include taxes (21% on discounted price) - self.assertAlmostEqual(pricing["price"], 108.9, places=2) - self.assertTrue(pricing.get("tax_included")) - self.assertTrue(pricing.get("has_discounted_price")) diff --git a/website_sale_aplicoop/tests/test_save_order_endpoints.py b/website_sale_aplicoop/tests/test_save_order_endpoints.py index 7a559db..4e975fe 100644 --- a/website_sale_aplicoop/tests/test_save_order_endpoints.py +++ b/website_sale_aplicoop/tests/test_save_order_endpoints.py @@ -16,8 +16,6 @@ from datetime import timedelta from odoo.tests.common import TransactionCase -from ..controllers.website_sale import AplicoopWebsiteSale - class TestSaveOrderEndpoints(TransactionCase): """Test suite for order-saving endpoints.""" @@ -96,51 +94,6 @@ class TestSaveOrderEndpoints(TransactionCase): # Associate product with group order self.group_order.product_ids = [(4, self.product.id)] - # Helper: controller instance for pure helpers - self.controller = AplicoopWebsiteSale() - - def _build_line(self, product, qty, price): - return ( - 0, - 0, - {"product_id": product.id, "product_uom_qty": qty, "price_unit": price}, - ) - - def test_merge_or_replace_replaces_existing_draft(self): - """Existing draft must be replaced (not merged) with new lines.""" - - # Existing draft with one line - existing = self.env["sale.order"].create( - { - "partner_id": self.member_partner.id, - "group_order_id": self.group_order.id, - "pickup_day": self.group_order.pickup_day, - "pickup_date": self.group_order.pickup_date, - "home_delivery": self.group_order.home_delivery, - "order_line": [self._build_line(self.product, 1, 10.0)], - "state": "draft", - } - ) - - new_lines = [self._build_line(self.product, 3, 99.0)] - - result = self.controller._merge_or_replace_draft( - self.group_order, - self.user, - new_lines, - existing, - self.group_order.id, - ) - - self.assertEqual(result.id, existing.id, "Should reuse existing draft") - self.assertEqual(len(result.order_line), 1, "Only one line should remain") - self.assertEqual( - result.order_line[0].product_uom_qty, 3, "Quantity must be replaced" - ) - self.assertEqual( - result.order_line[0].price_unit, 99.0, "Price must be replaced" - ) - def test_save_eskaera_draft_creates_order_with_group_order_id(self): """ Test that save_eskaera_draft() creates a sale.order with group_order_id. diff --git a/website_sale_aplicoop/views/website_templates.xml b/website_sale_aplicoop/views/website_templates.xml index 1300fe1..c7964df 100644 --- a/website_sale_aplicoop/views/website_templates.xml +++ b/website_sale_aplicoop/views/website_templates.xml @@ -349,11 +349,6 @@ - -