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
-
-
-
-
-
-
-
-
-
-
- sale.order.tree.update.list.price
- sale.order
-
-
-
- state == 'cancel'
-
-
-
-
-
-
diff --git a/stock_picking_batch_custom/README.rst b/stock_picking_batch_custom/README.rst
deleted file mode 100644
index 9893a8f..0000000
--- a/stock_picking_batch_custom/README.rst
+++ /dev/null
@@ -1,13 +0,0 @@
-===============================
-Stock Picking Batch Custom
-===============================
-
-.. contents::
- :local:
-
-.. include:: readme/DESCRIPTION.rst
-.. include:: readme/INSTALL.rst
-.. include:: readme/CONFIGURE.rst
-.. include:: readme/USAGE.rst
-.. include:: readme/CONTRIBUTORS.rst
-.. include:: readme/CREDITS.rst
diff --git a/stock_picking_batch_custom/__init__.py b/stock_picking_batch_custom/__init__.py
deleted file mode 100644
index ce9807d..0000000
--- a/stock_picking_batch_custom/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import models # noqa: F401
diff --git a/stock_picking_batch_custom/__manifest__.py b/stock_picking_batch_custom/__manifest__.py
deleted file mode 100644
index 4882701..0000000
--- a/stock_picking_batch_custom/__manifest__.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# Copyright 2026 Criptomart
-# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-{ # noqa: B018
- "name": "Stock Picking Batch Custom",
- "version": "18.0.1.0.0",
- "category": "Warehouse",
- "summary": "Extra columns for batch detailed operations",
- "author": "Odoo Community Association (OCA), Criptomart",
- "maintainers": ["Criptomart"],
- "website": "https://github.com/Criptomart",
- "license": "AGPL-3",
- "depends": [
- "stock_picking_batch",
- ],
- "data": [
- "views/stock_move_line_views.xml",
- ],
-}
diff --git a/stock_picking_batch_custom/models/__init__.py b/stock_picking_batch_custom/models/__init__.py
deleted file mode 100644
index 5a228fe..0000000
--- a/stock_picking_batch_custom/models/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import stock_move_line # noqa: F401
diff --git a/stock_picking_batch_custom/models/stock_move_line.py b/stock_picking_batch_custom/models/stock_move_line.py
deleted file mode 100644
index cb4ad48..0000000
--- a/stock_picking_batch_custom/models/stock_move_line.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Copyright 2026 Criptomart
-# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-
-from odoo import fields
-from odoo import models
-
-
-class StockMoveLine(models.Model):
- _inherit = "stock.move.line"
-
- product_categ_id = fields.Many2one(
- comodel_name="product.category",
- string="Product Category",
- related="product_id.categ_id",
- store=True,
- readonly=True,
- )
diff --git a/stock_picking_batch_custom/readme/CONFIGURE.rst b/stock_picking_batch_custom/readme/CONFIGURE.rst
deleted file mode 100644
index 2213143..0000000
--- a/stock_picking_batch_custom/readme/CONFIGURE.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-Configuración
-=============
-
-No requiere configuración adicional. Para usar las columnas:
-
-- Abrir un **Lote de picking**.
-- Ir a la pestaña **Detailed Operations**.
-- Abrir el **selector de columnas** y activar *Partner* y *Product Category* según necesidad.
diff --git a/stock_picking_batch_custom/readme/CONTRIBUTORS.rst b/stock_picking_batch_custom/readme/CONTRIBUTORS.rst
deleted file mode 100644
index 46076fe..0000000
--- a/stock_picking_batch_custom/readme/CONTRIBUTORS.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Contribuidores
-==============
-
-* Criptomart
diff --git a/stock_picking_batch_custom/readme/CREDITS.rst b/stock_picking_batch_custom/readme/CREDITS.rst
deleted file mode 100644
index 0fee3c5..0000000
--- a/stock_picking_batch_custom/readme/CREDITS.rst
+++ /dev/null
@@ -1,12 +0,0 @@
-Créditos
-========
-
-Autor
------
-
-* Criptomart
-
-Financiador
------------
-
-* Elika Bilbo
diff --git a/stock_picking_batch_custom/readme/DESCRIPTION.rst b/stock_picking_batch_custom/readme/DESCRIPTION.rst
deleted file mode 100644
index d8fc673..0000000
--- a/stock_picking_batch_custom/readme/DESCRIPTION.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-Este módulo añade dos columnas opcionales en las operaciones detalladas de los
-lotes de picking:
-
-- ``picking_partner_id`` (Partner del albarán) para que el personal de almacén
- pueda identificar rápidamente el cliente/proveedor asociado.
-- ``product_categ_id`` (Categoría de producto) para permitir ordenación y
- agrupación por categoría.
-
-Ambas columnas se añaden como ``optional="hide"`` en la vista de líneas del
-lote, de modo que el usuario puede activarlas desde el selector de columnas sin
-cargar la vista por defecto.
diff --git a/stock_picking_batch_custom/readme/INSTALL.rst b/stock_picking_batch_custom/readme/INSTALL.rst
deleted file mode 100644
index b259091..0000000
--- a/stock_picking_batch_custom/readme/INSTALL.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-Instalación
-===========
-
-Actualizar o instalar el módulo:
-
-::
-
- docker-compose run --rm odoo odoo -d odoo --stop-after-init -u stock_picking_batch_custom
diff --git a/stock_picking_batch_custom/readme/USAGE.rst b/stock_picking_batch_custom/readme/USAGE.rst
deleted file mode 100644
index d652a4e..0000000
--- a/stock_picking_batch_custom/readme/USAGE.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-Uso
-===
-
-1. Accede a **Inventory > Operations > Batch Transfers** y abre un lote.
-2. Pestaña **Detailed Operations**: usa el selector de columnas para activar:
-
- - **Partner** (``picking_partner_id``) para ver el cliente/proveedor.
- - **Product Category** (``product_categ_id``) para ordenar/agrupación por categoría.
-
-3. Ordena o agrupa por la columna de categoría según convenga.
diff --git a/stock_picking_batch_custom/static/description/icon.png b/stock_picking_batch_custom/static/description/icon.png
deleted file mode 100644
index d4a79d1..0000000
Binary files a/stock_picking_batch_custom/static/description/icon.png and /dev/null differ
diff --git a/stock_picking_batch_custom/views/stock_move_line_views.xml b/stock_picking_batch_custom/views/stock_move_line_views.xml
deleted file mode 100644
index a000a97..0000000
--- a/stock_picking_batch_custom/views/stock_move_line_views.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
- stock.move.line.list.batch.custom
- stock.move.line
-
-
-
-
-
-
-
-
-
-
-
diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py
index e3fac6b..9158b79 100644
--- a/website_sale_aplicoop/controllers/website_sale.py
+++ b/website_sale_aplicoop/controllers/website_sale.py
@@ -6,7 +6,6 @@ import logging
from datetime import datetime
from datetime import timedelta
-from odoo import fields
from odoo import http
from odoo.http import request
@@ -303,6 +302,7 @@ class AplicoopWebsiteSale(WebsiteSale):
# Agregar a los hijos de su padre
category_map[parent_id]["children"].append(cat_info)
+ # Ordenar raíces y sus hijos por nombre
def sort_hierarchy(items):
items.sort(key=lambda x: x["name"])
for item in items:
@@ -325,49 +325,87 @@ class AplicoopWebsiteSale(WebsiteSale):
Returns:
product.pricelist record or False if none found
"""
- env = request.env
- website = request.website
pricelist = None
- # 1) Configured pricelist from settings
+ # Try to get configured Aplicoop pricelist first
try:
- param_value = (
- env["ir.config_parameter"]
+ aplicoop_pricelist_id = (
+ request.env["ir.config_parameter"]
.sudo()
.get_param("website_sale_aplicoop.pricelist_id")
)
- if param_value:
+ if aplicoop_pricelist_id:
pricelist = (
- env["product.pricelist"].browse(int(param_value)).exists() or None
+ request.env["product.pricelist"]
+ .sudo()
+ .browse(int(aplicoop_pricelist_id))
)
- except Exception as e:
- _logger.warning("_resolve_pricelist: error reading config param: %s", e)
+ if pricelist.exists():
+ _logger.info(
+ "_resolve_pricelist: Using configured Aplicoop pricelist: %s (id=%s)",
+ pricelist.name,
+ pricelist.id,
+ )
+ return pricelist
+ else:
+ _logger.warning(
+ "_resolve_pricelist: Configured Aplicoop pricelist (id=%s) not found",
+ aplicoop_pricelist_id,
+ )
+ except Exception as err:
+ _logger.warning(
+ "_resolve_pricelist: Error getting Aplicoop pricelist: %s", str(err)
+ )
- # 2) Website current pricelist
- if not pricelist:
- try:
- pricelist = website._get_current_pricelist()
- except Exception as e:
- _logger.warning(
- "_resolve_pricelist: fallback to website pricelist failed: %s", e
+ # Fallback to website pricelist
+ try:
+ pricelist = request.website._get_current_pricelist()
+ if pricelist:
+ _logger.info(
+ "_resolve_pricelist: Using website pricelist: %s (id=%s)",
+ pricelist.name,
+ pricelist.id,
)
+ return pricelist
+ except Exception as err:
+ _logger.warning(
+ "_resolve_pricelist: Error getting website pricelist: %s", str(err)
+ )
- # 3) First active pricelist as fallback
- if not pricelist:
- pricelist = env["product.pricelist"].sudo().search([], limit=1)
+ # Final fallback to first active pricelist
+ pricelist = (
+ request.env["product.pricelist"]
+ .sudo()
+ .search([("active", "=", True)], limit=1)
+ )
+ if pricelist:
+ _logger.info(
+ "_resolve_pricelist: Using first active pricelist: %s (id=%s)",
+ pricelist.name,
+ pricelist.id,
+ )
+ return pricelist
- return pricelist
+ _logger.error(
+ "_resolve_pricelist: ERROR - No pricelist found! Pricing may fail."
+ )
+ return False
def _prepare_product_display_info(self, product, product_price_info):
- """Build display info for a product using precomputed prices.
+ """Prepare all display information for a product in a QWeb-safe way.
+
+ This function pre-processes all values that might be None or require
+ conditional logic, so the template can use simple variable references
+ without complex expressions that confuse QWeb's parser.
Args:
- product (product.product): Product variant.
- product_price_info: dict with price data keyed by product.id.
+ product: product.template record
+ product_price_info: dict with 'price', 'list_price', etc.
Returns:
- dict with display_price, safe_uom_category, quantity_step
+ dict with all pre-processed display values ready for template
"""
+ # Safety: Get price, ensure it's a float
price_data = product_price_info.get(product.id, {})
price = (
price_data.get("price", product.list_price)
@@ -376,14 +414,19 @@ class AplicoopWebsiteSale(WebsiteSale):
)
price_safe = float(price) if price else 0.0
+ # Safety: Get UoM category name (use sudo for read-only display to avoid ACL issues)
uom_category_name = ""
- quantity_step = 1
+ quantity_step = 1 # Default step for integer quantities (Units)
if product.uom_id:
uom = product.uom_id.sudo()
if uom.category_id:
uom_category_name = uom.category_id.sudo().name or ""
+
+ # Use XML IDs to detect fractional UoM categories (multilingual robust)
+ # This works regardless of translation/language
try:
+ # Get external ID for the UoM category
ir_model_data = request.env["ir.model.data"].sudo()
external_id = ir_model_data.search(
[
@@ -394,16 +437,18 @@ class AplicoopWebsiteSale(WebsiteSale):
)
if external_id:
+ # Standard Odoo UoM categories requiring fractional step
fractional_categories = [
- "uom.product_uom_categ_kgm",
- "uom.product_uom_categ_vol",
- "uom.uom_categ_length",
- "uom.uom_categ_surface",
+ "uom.product_uom_categ_kgm", # Weight (kg, g, ton, etc.)
+ "uom.product_uom_categ_vol", # Volume (L, m³, etc.)
+ "uom.uom_categ_length", # Length/Distance (m, km, etc.)
+ "uom.uom_categ_surface", # Surface (m², ha, etc.)
]
full_xmlid = f"{external_id.module}.{external_id.name}"
if full_xmlid in fractional_categories:
quantity_step = 0.1
except Exception as e:
+ # Fallback to integer step on error
_logger.warning(
"_prepare_product_display_info: Error detecting UoM category XML ID for product %s: %s",
product.id,
@@ -416,70 +461,6 @@ class AplicoopWebsiteSale(WebsiteSale):
"quantity_step": quantity_step,
}
- def _get_pricing_info(self, product, pricelist, quantity=1.0, partner=None):
- """Compute pricing with taxes like website_sale but using a given pricelist.
-
- Returns a dict with:
- - price_unit: raw pricelist price (before taxes), suitable for sale.order.line
- - price: display price (tax included/excluded per website setting)
- - list_price: display list price (pre-discount) with same tax display
- - has_discounted_price: bool
- """
-
- try:
- env = request.env
- website = request.website
- except RuntimeError:
- env = product.env
- website = env["website"].get_current_website()
-
- partner = partner or env.user.partner_id
- currency = pricelist.currency_id
- company = website.company_id or product.company_id or env.company
-
- price, rule_id = pricelist._get_product_price_rule(
- product=product,
- quantity=quantity,
- target_currency=currency,
- )
-
- price_before_discount = price
- pricelist_item = env["product.pricelist.item"].browse(rule_id)
- if pricelist_item and pricelist_item._show_discount_on_shop():
- price_before_discount = pricelist_item._compute_price_before_discount(
- product=product,
- quantity=quantity or 1.0,
- date=fields.Date.context_today(pricelist),
- uom=product.uom_id,
- currency=currency,
- )
-
- has_discounted_price = price_before_discount > price
-
- fiscal_position = website.fiscal_position_id.sudo()
- product_taxes = product.sudo().taxes_id._filter_taxes_by_company(company)
- taxes = (
- fiscal_position.map_tax(product_taxes) if product_taxes else product_taxes
- )
- tax_display = "total_included"
-
- def compute_display(amount):
- if not taxes:
- return amount
- return taxes.compute_all(amount, currency, 1, product, partner)[tax_display]
-
- display_price = compute_display(price)
- display_list_price = compute_display(price_before_discount)
-
- return {
- "price_unit": price,
- "price": display_price,
- "list_price": display_list_price,
- "has_discounted_price": has_discounted_price,
- "discount": display_list_price - display_price,
- "tax_included": tax_display == "total_included",
- }
-
def _compute_price_info(self, products, pricelist):
"""Compute price info dict for a list of products using the given pricelist.
@@ -492,13 +473,23 @@ class AplicoopWebsiteSale(WebsiteSale):
)
if product_variant and pricelist:
try:
- pricing = self._get_pricing_info(
- product_variant,
- pricelist,
- quantity=1.0,
- partner=request.env.user.partner_id,
+ price_info = product_variant._get_price(
+ qty=1.0,
+ pricelist=pricelist,
+ fposition=request.website.fiscal_position_id,
)
- product_price_info[product.id] = pricing
+ price = price_info.get("value", 0.0)
+ original_price = price_info.get("original_value", 0.0)
+ discount = price_info.get("discount", 0.0)
+ has_discount = discount > 0
+
+ product_price_info[product.id] = {
+ "price": price,
+ "list_price": original_price,
+ "has_discounted_price": has_discount,
+ "discount": discount,
+ "tax_included": price_info.get("tax_included", True),
+ }
except Exception as e:
_logger.warning(
"_compute_price_info: Error getting price for product %s (id=%s): %s. Using list_price fallback.",
@@ -507,23 +498,19 @@ class AplicoopWebsiteSale(WebsiteSale):
str(e),
)
product_price_info[product.id] = {
- "price_unit": product.list_price,
"price": product.list_price,
"list_price": product.list_price,
"has_discounted_price": False,
"discount": 0.0,
- "tax_included": request.website.show_line_subtotals_tax_selection
- != "tax_excluded",
+ "tax_included": False,
}
else:
product_price_info[product.id] = {
- "price_unit": product.list_price,
"price": product.list_price,
"list_price": product.list_price,
"has_discounted_price": False,
"discount": 0.0,
- "tax_included": request.website.show_line_subtotals_tax_selection
- != "tax_excluded",
+ "tax_included": False,
}
return product_price_info
@@ -827,7 +814,7 @@ class AplicoopWebsiteSale(WebsiteSale):
return order_id, group_order, current_user, items, is_delivery
- def _process_cart_items(self, items, group_order, pricelist=None):
+ def _process_cart_items(self, items, group_order):
"""Process cart items and build sale.order line data.
Args:
@@ -841,13 +828,12 @@ class AplicoopWebsiteSale(WebsiteSale):
ValueError: if no valid items after processing
"""
sale_order_lines = []
- pricelist = pricelist or self._resolve_pricelist()
- partner = request.env.user.partner_id
for item in items:
try:
product_id = int(item.get("product_id"))
quantity = float(item.get("quantity", 1))
+ price = float(item.get("product_price", 0))
product = request.env["product.product"].sudo().browse(product_id)
if not product.exists():
@@ -860,17 +846,10 @@ class AplicoopWebsiteSale(WebsiteSale):
product_in_lang = product.with_context(lang=request.env.lang)
product_name = product_in_lang.name
- pricing = self._get_pricing_info(
- product,
- pricelist,
- quantity=quantity,
- partner=partner,
- )
-
line_data = {
"product_id": product_id,
"product_uom_qty": quantity,
- "price_unit": pricing.get("price_unit", product.list_price),
+ "price_unit": price or product.list_price,
"name": product_name, # Force the translated product name
}
_logger.info("_process_cart_items: Adding line: %s", line_data)
@@ -1039,7 +1018,19 @@ class AplicoopWebsiteSale(WebsiteSale):
return sale_order
def _build_confirmation_message(self, sale_order, group_order, is_delivery):
- """Build localized confirmation message for confirm_eskaera."""
+ """Build localized confirmation message for confirm_eskaera.
+
+ Translates message and pickup/delivery info according to user's language.
+ Handles day names and date formatting.
+
+ Args:
+ sale_order: sale.order record just created
+ group_order: group.order record
+ is_delivery: boolean indicating if home delivery
+
+ Returns:
+ dict with message, pickup_day, pickup_date, pickup_day_index
+ """
# Get pickup day index, localized name and date string using helper
pickup_day_name, pickup_date_str, pickup_day_index = self._format_pickup_info(
group_order, is_delivery
@@ -1229,29 +1220,86 @@ class AplicoopWebsiteSale(WebsiteSale):
group_order,
current_user,
sale_order_lines,
+ merge_action,
+ existing_draft_id,
existing_drafts,
order_id,
):
- """Replace existing draft (if any) with new lines, else create it."""
+ """Handle merge/replace logic for drafts and return (sale_order, merge_success).
- if existing_drafts:
- draft = existing_drafts[0].sudo()
+ existing_drafts: recordset of existing draft orders (may be empty)
+ """
+ # Merge
+ if merge_action == "merge" and existing_draft_id:
+ existing_draft = (
+ request.env["sale.order"].sudo().browse(int(existing_draft_id))
+ )
+ if existing_draft.exists():
+ for new_line_data in sale_order_lines:
+ product_id = new_line_data[2]["product_id"]
+ new_quantity = new_line_data[2]["product_uom_qty"]
+ new_price = new_line_data[2]["price_unit"]
+
+ # Capture product_id as default arg to avoid late-binding in lambda (fix B023)
+ existing_line = existing_draft.order_line.filtered(
+ lambda line, pid=product_id: line.product_id.id == pid
+ )
+ if existing_line:
+ # Use sudo() to avoid permission issues with portal users
+ existing_line.sudo().write(
+ {
+ "product_uom_qty": existing_line.product_uom_qty
+ + new_quantity
+ }
+ )
+ _logger.info(
+ "Merged item: product_id=%d, new total quantity=%.2f",
+ product_id,
+ existing_line.product_uom_qty,
+ )
+ else:
+ # Use sudo() to avoid permission issues with portal users
+ existing_draft.order_line.sudo().create(
+ {
+ "order_id": existing_draft.id,
+ "product_id": product_id,
+ "product_uom_qty": new_quantity,
+ "price_unit": new_price,
+ }
+ )
+ _logger.info(
+ "Added new item to draft: product_id=%d, quantity=%.2f",
+ product_id,
+ new_quantity,
+ )
+
+ return existing_draft, True
+
+ # Replace
+ if merge_action == "replace" and existing_draft_id and existing_drafts:
+ existing_drafts.unlink()
_logger.info(
- "Replacing existing draft order %s for partner %s",
- draft.id,
- current_user.partner_id.id,
+ "Deleted existing draft(s) for replace: %s",
+ existing_drafts.mapped("id"),
)
- draft.write(
- {
- "order_line": [(5, 0, 0)] + sale_order_lines,
- "group_order_id": order_id,
- "pickup_day": group_order.pickup_day,
- "pickup_date": group_order.pickup_date,
- "home_delivery": group_order.home_delivery,
- }
- )
- return draft
+ order_vals = {
+ "partner_id": current_user.partner_id.id,
+ "order_line": sale_order_lines,
+ "state": "draft",
+ "group_order_id": order_id,
+ "pickup_day": group_order.pickup_day,
+ "pickup_date": group_order.pickup_date,
+ "home_delivery": group_order.home_delivery,
+ }
+ # Get salesperson for order creation (portal users need this)
+ salesperson = self._get_salesperson_for_order(current_user.partner_id)
+ if salesperson:
+ order_vals["user_id"] = salesperson.id
+ sale_order = request.env["sale.order"].sudo().create(order_vals)
+ return sale_order, False
+
+ # Default: create new draft
order_vals = {
"partner_id": current_user.partner_id.id,
"order_line": sale_order_lines,
@@ -1267,7 +1315,7 @@ class AplicoopWebsiteSale(WebsiteSale):
order_vals["user_id"] = salesperson.id
sale_order = request.env["sale.order"].sudo().create(order_vals)
- return sale_order
+ return sale_order, False
def _decode_json_body(self):
"""Safely decode JSON body from request. Returns dict or raises ValueError."""
@@ -1997,9 +2045,7 @@ class AplicoopWebsiteSale(WebsiteSale):
# Build sale.order lines and create draft using helpers
try:
- sale_order_lines = self._process_cart_items(
- items, group_order, pricelist=self._resolve_pricelist()
- )
+ sale_order_lines = self._process_cart_items(items, group_order)
except ValueError as e:
return request.make_response(
json.dumps({"error": str(e)}),
@@ -2125,21 +2171,13 @@ class AplicoopWebsiteSale(WebsiteSale):
# Extract items from the draft order
items = []
- pricelist = self._resolve_pricelist()
- partner = current_user.partner_id
for line in draft_order.order_line:
- pricing = self._get_pricing_info(
- line.product_id,
- pricelist,
- quantity=line.product_uom_qty,
- partner=partner,
- )
items.append(
{
"product_id": line.product_id.id,
"product_name": line.product_id.name,
"quantity": line.product_uom_qty,
- "product_price": pricing.get("price", line.price_unit),
+ "product_price": line.price_unit,
}
)
@@ -2268,6 +2306,8 @@ class AplicoopWebsiteSale(WebsiteSale):
# Get cart items
items = data.get("items", [])
+ merge_action = data.get("merge_action") # 'merge' or 'replace'
+ existing_draft_id = data.get("existing_draft_id") # ID if replacing
if not items:
_logger.warning(
@@ -2294,6 +2334,35 @@ class AplicoopWebsiteSale(WebsiteSale):
)
)
+ # If draft exists and no action specified, return the existing draft info
+ if existing_drafts and not merge_action:
+ existing_draft = existing_drafts[0] # Get first draft
+ existing_items = [
+ {
+ "product_id": line.product_id.id,
+ "product_name": line.product_id.name,
+ "quantity": line.product_uom_qty,
+ "product_price": line.price_unit,
+ }
+ for line in existing_draft.order_line
+ ]
+
+ return request.make_response(
+ json.dumps(
+ {
+ "success": False,
+ "existing_draft": True,
+ "existing_draft_id": existing_draft.id,
+ "existing_items": existing_items,
+ "current_items": items,
+ "message": request.env._(
+ "A draft already exists for this week."
+ ),
+ }
+ ),
+ [("Content-Type", "application/json")],
+ )
+
_logger.info(
"Creating draft sale.order with %d items for partner %d",
len(items),
@@ -2302,9 +2371,7 @@ class AplicoopWebsiteSale(WebsiteSale):
# Create sales.order lines from items using shared helper
try:
- sale_order_lines = self._process_cart_items(
- items, group_order, pricelist=self._resolve_pricelist()
- )
+ sale_order_lines = self._process_cart_items(items, group_order)
except ValueError as e:
return request.make_response(
json.dumps({"error": str(e)}),
@@ -2313,10 +2380,12 @@ class AplicoopWebsiteSale(WebsiteSale):
)
# Delegate merge/replace/create logic to helper
- sale_order = self._merge_or_replace_draft(
+ sale_order, merge_success = self._merge_or_replace_draft(
group_order,
current_user,
sale_order_lines,
+ merge_action,
+ existing_draft_id,
existing_drafts,
order_id,
)
@@ -2335,8 +2404,13 @@ class AplicoopWebsiteSale(WebsiteSale):
json.dumps(
{
"success": True,
- "message": request.env._("Order saved as draft"),
+ "message": (
+ request.env._("Merged with existing draft")
+ if merge_success
+ else request.env._("Order saved as draft")
+ ),
"sale_order_id": sale_order.id,
+ "merged": merge_success,
}
),
[("Content-Type", "application/json")],
@@ -2407,9 +2481,7 @@ class AplicoopWebsiteSale(WebsiteSale):
# Process cart items using helper
try:
- sale_order_lines = self._process_cart_items(
- items, group_order, pricelist=self._resolve_pricelist()
- )
+ sale_order_lines = self._process_cart_items(items, group_order)
except ValueError as e:
_logger.warning("confirm_eskaera: Cart processing error: %s", str(e))
return request.make_response(
diff --git a/website_sale_aplicoop/models/group_order.py b/website_sale_aplicoop/models/group_order.py
index a4da72c..8090f27 100644
--- a/website_sale_aplicoop/models/group_order.py
+++ b/website_sale_aplicoop/models/group_order.py
@@ -680,44 +680,4 @@ class GroupOrder(models.Model):
order._compute_cutoff_date()
order._compute_pickup_date()
order._compute_delivery_date()
- order._confirm_linked_sale_orders()
_logger.info("Cron: Date update completed")
-
- def _confirm_linked_sale_orders(self):
- """Confirm draft/sent sale orders linked to this group order.
-
- This is triggered by the daily cron so that weekly orders generated
- from the website are confirmed automatically once dates are refreshed.
- """
- self.ensure_one()
- SaleOrder = self.env["sale.order"].sudo()
- sale_orders = SaleOrder.search(
- [
- ("group_order_id", "=", self.id),
- ("state", "in", ["draft", "sent"]),
- ]
- )
-
- if not sale_orders:
- _logger.info(
- "Cron: No sale orders to confirm for group order %s (%s)",
- self.id,
- self.name,
- )
- return
-
- _logger.info(
- "Cron: Confirming %d sale orders for group order %s (%s)",
- len(sale_orders),
- self.id,
- self.name,
- )
-
- try:
- sale_orders.action_confirm()
- except Exception:
- _logger.exception(
- "Cron: Error confirming sale orders for group order %s (%s)",
- self.id,
- self.name,
- )
diff --git a/website_sale_aplicoop/static/src/js/home_delivery.js b/website_sale_aplicoop/static/src/js/home_delivery.js
index d7d3d5a..68de98b 100644
--- a/website_sale_aplicoop/static/src/js/home_delivery.js
+++ b/website_sale_aplicoop/static/src/js/home_delivery.js
@@ -213,14 +213,6 @@
var cartKey = "eskaera_" + this.orderId + "_cart";
localStorage.setItem(cartKey, JSON.stringify(cart));
- // Mantener sincronizado el carrito en memoria (sidebar de tienda)
- if (window.groupOrderShop && window.groupOrderShop.cart !== undefined) {
- window.groupOrderShop.cart = cart;
- if (typeof window.groupOrderShop._updateCartDisplay === "function") {
- window.groupOrderShop._updateCartDisplay();
- }
- }
-
// Re-render checkout summary without reloading
setTimeout(function () {
// Use the global function from checkout_labels.js
diff --git a/website_sale_aplicoop/static/src/js/website_sale.js b/website_sale_aplicoop/static/src/js/website_sale.js
index 0e01f88..65010d9 100644
--- a/website_sale_aplicoop/static/src/js/website_sale.js
+++ b/website_sale_aplicoop/static/src/js/website_sale.js
@@ -449,7 +449,7 @@
var tooltipMap = {
"save-cart-btn": "save_cart",
"reload-cart-btn": "reload_cart",
- "confirm-order-btn": "save_cart",
+ "confirm-order-btn": "confirm_order",
"remove-from-cart": "remove_item",
};
@@ -634,9 +634,9 @@
if (confirmBtn) {
confirmBtn.addEventListener("click", function (e) {
- console.log("[CLICK] confirm-order-btn clicked (save draft)");
+ console.log("[CLICK] confirm-order-btn clicked");
e.preventDefault();
- self._saveOrderDraft();
+ self._confirmOrder();
});
}
@@ -1313,7 +1313,6 @@
var orderData = {
order_id: this.orderId,
items: items,
- merge_action: "replace",
};
var xhr = new XMLHttpRequest();
@@ -1333,6 +1332,9 @@
data.sale_order_id +
")";
self._showNotification("✓ " + successMsg, "success", 5000);
+ } else if (data.existing_draft) {
+ // A draft already exists - show modal with merge/replace options
+ self._showDraftConflictModal(data);
} else {
self._showNotification(
"Error: " + (data.error || labels.error_unknown || "Unknown error"),
@@ -1497,14 +1499,10 @@
console.log("[_saveOrderDraft] Starting - this.orderId:", this.orderId);
var self = this;
- var labels = this._getLabels();
- var cartKey = "eskaera_" + this.orderId + "_cart";
- var storedCart = localStorage.getItem(cartKey);
- var cart = storedCart ? JSON.parse(storedCart) : this.cart;
var items = [];
- Object.keys(cart).forEach(function (productId) {
- var item = cart[productId];
+ Object.keys(this.cart).forEach(function (productId) {
+ var item = self.cart[productId];
items.push({
product_id: productId,
product_name: item.name,
@@ -1529,11 +1527,14 @@
console.log("Response:", data);
if (data.success) {
- var successMsg =
- labels.draft_saved_success ||
- labels.draft_saved ||
- "Order saved as draft successfully";
- self._showNotification("\u2713 " + successMsg, "success", 5000);
+ self._showNotification(
+ "✓ Order saved as draft successfully",
+ "success",
+ 5000
+ );
+ } else if (data.existing_draft) {
+ // A draft already exists - show modal with merge/replace options
+ self._showDraftConflictModal(data);
} else {
self._showNotification(
"Error: " + (data.error || labels.error_unknown || "Unknown error"),
@@ -1571,6 +1572,306 @@
xhr.send(JSON.stringify(orderData));
},
+ _showDraftConflictModal: function (data) {
+ /**
+ * Show modal with merge/replace options for existing draft.
+ * Uses labels from window.groupOrderShop.labels or falls back to defaults.
+ */
+ var self = this;
+
+ // Get labels - they should already be loaded by page init
+ var labels =
+ window.groupOrderShop && window.groupOrderShop.labels
+ ? window.groupOrderShop.labels
+ : self._getDefaultLabels();
+
+ console.log(
+ "[_showDraftConflictModal] Using labels:",
+ Object.keys(labels).length,
+ "keys available"
+ );
+
+ var existing_items = data.existing_items || [];
+ var current_items = data.current_items || [];
+ var existing_draft_id = data.existing_draft_id;
+
+ // Create modal HTML with inline styles (no Bootstrap needed)
+ var modalHTML = `
+
+
+
+
+
${
+ labels.draft_already_exists || "Draft Already Exists"
+ }
+
+
+
+
+
+
${
+ 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 @@
-
-
-
@@ -523,6 +518,16 @@
+
+
+
+ Important:
+
+
+ Once you confirm this order, you will not be able to modify it. Please review carefully before confirming.
+
+
+