diff --git a/CORRECCION_PRECIOS_IVA.md b/CORRECCION_PRECIOS_IVA.md deleted file mode 100644 index a18b011..0000000 --- a/CORRECCION_PRECIOS_IVA.md +++ /dev/null @@ -1,175 +0,0 @@ -# Resumen de Corrección: Precios sin IVA en Tienda Online - -## Problema Identificado -La tienda online de Aplicoop mostraba precios **sin impuestos incluidos** cuando debería mostrar precios con IVA. - -## Causa Raíz -El método `_get_price()` del addon OCA `product_get_price_helper` retorna el precio base **sin impuestos** por defecto. El campo `tax_included` solo indica si el producto tiene impuestos con `price_include=True`, pero no calcula automáticamente el precio con impuestos añadidos. - -## Solución Implementada - -### 1. Nuevo Método: `_compute_price_with_taxes()` -**Archivo**: `website_sale_aplicoop/controllers/website_sale.py` - -```python -def _compute_price_with_taxes(self, product_variant, base_price, pricelist=None, fposition=None): - """ - Calcula el precio con impuestos incluidos. - - Args: - product_variant: product.product recordset - base_price: float - precio base sin impuestos - pricelist: product.pricelist recordset (opcional) - fposition: account.fiscal.position recordset (opcional) - - Returns: - float - precio con impuestos incluidos - """ - # 1. Obtener impuestos del producto - taxes = product_variant.taxes_id.filtered( - lambda tax: tax.company_id == request.env.company - ) - - # 2. Aplicar posición fiscal si existe - if fposition: - taxes = fposition.map_tax(taxes) - - # 3. Si no hay impuestos, retornar precio base - if not taxes: - return base_price - - # 4. Calcular impuestos usando compute_all() - tax_result = taxes.compute_all( - base_price, - currency=pricelist.currency_id if pricelist else request.env.company.currency_id, - quantity=1.0, - product=product_variant, - ) - - # 5. Retornar precio CON impuestos incluidos - return tax_result['total_included'] -``` - -### 2. Actualización en `eskaera_shop()` -El método que muestra la lista de productos ahora calcula precios con IVA: - -**ANTES:** -```python -price = price_info.get('value', 0.0) # Sin impuestos -product_price_info[product.id] = { - 'price': price, # ❌ Sin IVA - 'tax_included': price_info.get('tax_included', True), -} -``` - -**DESPUÉS:** -```python -base_price = price_info.get('value', 0.0) # Precio base sin impuestos - -# Calcular precio CON impuestos -price_with_taxes = self._compute_price_with_taxes( - product_variant, - base_price, - pricelist, - request.website.fiscal_position_id -) - -product_price_info[product.id] = { - 'price': price_with_taxes, # ✓ CON IVA incluido - 'tax_included': True, # Ahora siempre True -} -``` - -### 3. Actualización en `add_to_eskaera_cart()` -El método que añade productos al carrito también calcula con IVA: - -**ANTES:** -```python -price_with_tax = price_info.get('value', product.list_price) # ❌ Sin IVA -``` - -**DESPUÉS:** -```python -base_price = price_info.get('value', product.list_price) - -# Calcular precio CON impuestos -price_with_tax = self._compute_price_with_taxes( - product_variant, - base_price, - pricelist, - request.website.fiscal_position_id -) # ✓ CON IVA incluido -``` - -## Ejemplo Práctico - -### Producto con IVA 21% -- **Precio base**: 100.00 € -- **IVA (21%)**: 21.00 € -- **Precio mostrado**: **121.00 €** ✓ - -### Producto con IVA 10% -- **Precio base**: 100.00 € -- **IVA (10%)**: 10.00 € -- **Precio mostrado**: **110.00 €** ✓ - -### Producto sin IVA -- **Precio base**: 100.00 € -- **IVA**: 0.00 € -- **Precio mostrado**: **100.00 €** ✓ - -## Tests Creados - -### Archivo: `test_price_with_taxes_included.py` -Contiene tests unitarios que verifican: - -1. ✓ Cálculo correcto de IVA 21% -2. ✓ Cálculo correcto de IVA 10% -3. ✓ Productos sin IVA -4. ✓ Múltiples impuestos -5. ✓ Posiciones fiscales -6. ✓ Precios con alta precisión -7. ✓ Comportamiento de OCA `_get_price()` - -## Archivos Modificados - -1. **`website_sale_aplicoop/controllers/website_sale.py`** - - Añadido método `_compute_price_with_taxes()` - - Actualizado `eskaera_shop()` para usar precios con IVA - - Actualizado `add_to_eskaera_cart()` para usar precios con IVA - -2. **`website_sale_aplicoop/tests/test_price_with_taxes_included.py`** (nuevo) - - 13 tests para verificar cálculos de impuestos - -## Validación - -Para verificar la corrección en producción: - -```bash -# 1. Reiniciar Odoo -docker-compose restart odoo - -# 2. Actualizar el módulo -docker-compose exec odoo odoo -c /etc/odoo/odoo.conf -d odoo -u website_sale_aplicoop --stop-after-init - -# 3. Verificar en navegador -# Ir a: http://localhost:8069/eskaera -# Los precios ahora deberían mostrar IVA incluido -``` - -## Beneficios - -✅ **Transparencia**: Los usuarios ven el precio final que pagarán -✅ **Cumplimiento legal**: Obligatorio mostrar precios con IVA en B2C -✅ **Consistencia**: Todos los precios mostrados incluyen impuestos -✅ **Mantenibilidad**: Código limpio y documentado -✅ **Testeable**: Tests unitarios comprueban el funcionamiento - -## Notas Técnicas - -- El método utiliza `taxes.compute_all()` de Odoo, que es el estándar para calcular impuestos -- Respeta las posiciones fiscales configuradas -- Compatible con múltiples impuestos por producto -- Maneja correctamente productos sin impuestos -- El precio base (sin IVA) se usa internamente para cálculos de descuentos -- El precio final (con IVA) se muestra al usuario diff --git a/TEST_MANUAL.md b/TEST_MANUAL.md deleted file mode 100644 index 85989f0..0000000 --- a/TEST_MANUAL.md +++ /dev/null @@ -1,74 +0,0 @@ -# Test Manual - Verificar Precios con IVA - -## Para verificar que la corrección funciona: - -### 1. Reiniciar Odoo y actualizar el módulo - -```bash -cd /home/snt/Documentos/lab/odoo/kidekoop/addons-cm - -# Actualizar el módulo -docker-compose restart odoo -docker-compose exec odoo odoo -c /etc/odoo/odoo.conf -d odoo -u website_sale_aplicoop --stop-after-init -docker-compose restart odoo -``` - -### 2. Verificar en el navegador - -1. Ir a: http://localhost:8069 -2. Iniciar sesión -3. Navegar a la tienda: http://localhost:8069/eskaera -4. Verificar que los precios mostrados incluyen IVA - -### 3. Test de ejemplo - -**Producto:** Pan integral (ejemplo) -- **Precio base:** 2.50 € -- **IVA (10%):** 0.25 € -- **Precio esperado en tienda:** **2.75 €** - -## Cambios Realizados - -### Archivo modificado: `controllers/website_sale.py` - -1. **Nuevo método agregado (línea ~20)**: - ```python - def _compute_price_with_taxes(self, product_variant, base_price, pricelist=None, fposition=None): - """Calcula el precio con impuestos incluidos.""" - ``` - -2. **Método `eskaera_shop()` actualizado (línea ~516)**: - - Ahora calcula `price_with_taxes` usando el nuevo método - - Retorna precios CON IVA incluido - -3. **Método `add_to_eskaera_cart()` actualizado (línea ~720)**: - - Calcula precio CON IVA antes de retornar - - Garantiza consistencia en carrito - -## Verificación de Sintaxis - -```bash -# Verificar que no hay errores de sintaxis -cd /home/snt/Documentos/lab/odoo/kidekoop/addons-cm -python3 -m py_compile website_sale_aplicoop/controllers/website_sale.py -echo "✓ Sin errores de sintaxis" -``` - -## Tests Unitarios Creados - -Archivo: `website_sale_aplicoop/tests/test_price_with_taxes_included.py` - -Contiene 13 tests que verifican: -- Cálculo correcto de IVA 21% -- Cálculo correcto de IVA 10% -- Productos sin IVA -- Múltiples impuestos -- Posiciones fiscales -- Y más... - -## Problema Solucionado - -**ANTES:** Los precios mostraban 100.00 € (sin IVA) -**DESPUÉS:** Los precios muestran 121.00 € (con IVA 21%) - -✅ **Corrección aplicada exitosamente** diff --git a/check_tax_config.sh b/check_tax_config.sh deleted file mode 100755 index ff65e98..0000000 --- a/check_tax_config.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash -# Verificar configuración de impuestos - -echo "==========================================" -echo "Verificando configuración de impuestos" -echo "==========================================" - -docker-compose exec -T db psql -U odoo -d odoo << 'SQL' --- Verificar impuestos de venta y su configuración de price_include -SELECT - at.id, - at.name, - at.amount, - at.price_include, - at.type_tax_use, - rc.name as company -FROM account_tax at -LEFT JOIN res_company rc ON at.company_id = rc.id -WHERE at.type_tax_use = 'sale' - AND at.active = true -ORDER BY at.amount DESC -LIMIT 20; -SQL - -echo "" -echo "Nota: Si price_include = false (f), entonces el precio NO incluye IVA" -echo " Si price_include = true (t), entonces el precio SÍ incluye IVA" diff --git a/product_main_seller/README.rst b/product_main_seller/README.rst deleted file mode 100644 index 22ed7f8..0000000 --- a/product_main_seller/README.rst +++ /dev/null @@ -1,97 +0,0 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - -=================== -Product Main Vendor -=================== - -.. - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! This file is generated by oca-gen-addon-readme !! - !! changes will be overwritten. !! - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:c353b8931ca2140d6d80974f00d4e5e073b737283dc2770fe718a8842cd6bd4e - !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png - :target: https://odoo-community.org/page/development-status - :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png - :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html - :alt: License: AGPL-3 -.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpurchase--workflow-lightgray.png?logo=github - :target: https://github.com/OCA/purchase-workflow/tree/18.0/product_main_seller - :alt: OCA/purchase-workflow -.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/purchase-workflow-18-0/purchase-workflow-18-0-product_main_seller - :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png - :target: https://runboat.odoo-community.org/builds?repo=OCA/purchase-workflow&target_branch=18.0 - :alt: Try me on Runboat - -|badge1| |badge2| |badge3| |badge4| |badge5| - -This module extends the Odoo Product module to compute and display the -main Vendor of each products. The main vendor is the first vendor in the -vendors list. - -|image1| - -.. |image1| image:: https://raw.githubusercontent.com/OCA/purchase-workflow/18.0/product_main_seller/static/description/product_tree_view.png - -**Table of contents** - -.. contents:: - :local: - -Bug Tracker -=========== - -Bugs are tracked on `GitHub Issues `_. -In case of trouble, please check there if your issue has already been reported. -If you spotted it first, help us to smash it by providing a detailed and welcomed -`feedback `_. - -Do not contact contributors directly about support or help with technical issues. - -Credits -======= - -Authors -------- - -* GRAP - -Contributors ------------- - -- Quentin Dupont (quentin.dupont@grap.coop) - -Maintainers ------------ - -This module is maintained by the OCA. - -.. image:: https://odoo-community.org/logo.png - :alt: Odoo Community Association - :target: https://odoo-community.org - -OCA, or the Odoo Community Association, is a nonprofit organization whose -mission is to support the collaborative development of Odoo features and -promote its widespread use. - -.. |maintainer-legalsylvain| image:: https://github.com/legalsylvain.png?size=40px - :target: https://github.com/legalsylvain - :alt: legalsylvain -.. |maintainer-quentinDupont| image:: https://github.com/quentinDupont.png?size=40px - :target: https://github.com/quentinDupont - :alt: quentinDupont - -Current `maintainers `__: - -|maintainer-legalsylvain| |maintainer-quentinDupont| - -This module is part of the `OCA/purchase-workflow `_ project on GitHub. - -You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_main_seller/__init__.py b/product_main_seller/__init__.py deleted file mode 100644 index 6d58305..0000000 --- a/product_main_seller/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import models -from .hooks import pre_init_hook diff --git a/product_main_seller/__manifest__.py b/product_main_seller/__manifest__.py deleted file mode 100644 index 76c768f..0000000 --- a/product_main_seller/__manifest__.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (C) 2022 - Today: GRAP (http://www.grap.coop) -# @author: Quentin Dupont (quentin.dupont@grap.coop) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -{ - "name": "Product Main Vendor", - "summary": "Main Vendor for a product", - "version": "18.0.1.0.0", - "category": "Purchase", - "author": "GRAP,Odoo Community Association (OCA)", - "website": "https://github.com/OCA/purchase-workflow", - "license": "AGPL-3", - "depends": ["purchase"], - "maintainers": ["legalsylvain", "quentinDupont"], - "data": [ - "views/view_product_product.xml", - "views/view_product_template.xml", - ], - "installable": True, - "pre_init_hook": "pre_init_hook", -} diff --git a/product_main_seller/hooks.py b/product_main_seller/hooks.py deleted file mode 100644 index eceb699..0000000 --- a/product_main_seller/hooks.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2024-Today - Sylvain Le GAL (GRAP) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -import logging - -_logger = logging.getLogger(__name__) - - -def pre_init_hook(env): - _logger.info("Initializing column main_seller_id on table product_template") - cr = env.cr - cr.execute(""" - ALTER TABLE product_template - ADD COLUMN IF NOT EXISTS main_seller_id integer; - """) - cr.execute(""" - WITH numbered_supplierinfos as ( - SELECT *, ROW_number() over ( - partition BY product_tmpl_id - ORDER BY sequence, min_qty desc, price - ) as row_number - FROM product_supplierinfo - ), - - first_supplierinfos as ( - SELECT * from numbered_supplierinfos - WHERE row_number = 1 - ) - - UPDATE product_template pt - SET main_seller_id = first_supplierinfos.partner_id - FROM first_supplierinfos - WHERE pt.id = first_supplierinfos.product_tmpl_id; - """) diff --git a/product_main_seller/i18n/fr.po b/product_main_seller/i18n/fr.po deleted file mode 100644 index f245722..0000000 --- a/product_main_seller/i18n/fr.po +++ /dev/null @@ -1,37 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * product_main_seller -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 16.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-08 20:26+0000\n" -"PO-Revision-Date: 2024-07-08 20:26+0000\n" -"Last-Translator: \n" -"Language-Team: \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: product_main_seller -#: model:ir.model.fields,field_description:product_main_seller.field_product_product__main_seller_id -#: model:ir.model.fields,field_description:product_main_seller.field_product_template__main_seller_id -#: model_terms:ir.ui.view,arch_db:product_main_seller.view_product_template_search -msgid "Main Vendor" -msgstr "Fournisseur principal" - -#. module: product_main_seller -#: model:ir.model,name:product_main_seller.model_product_template -msgid "Product" -msgstr "Produit" - -#. module: product_main_seller -#: model:ir.model.fields,help:product_main_seller.field_product_product__main_seller_id -#: model:ir.model.fields,help:product_main_seller.field_product_template__main_seller_id -msgid "Put your supplier info in first position to set as main vendor" -msgstr "" -"Définir une information fournisseur en première position pour le définir " -"comme fournisseur principal" diff --git a/product_main_seller/i18n/it.po b/product_main_seller/i18n/it.po deleted file mode 100644 index 2bf8009..0000000 --- a/product_main_seller/i18n/it.po +++ /dev/null @@ -1,37 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * product_main_seller -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 16.0\n" -"Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2024-09-06 15:06+0000\n" -"Last-Translator: mymage \n" -"Language-Team: none\n" -"Language: it\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 5.6.2\n" - -#. module: product_main_seller -#: model:ir.model.fields,field_description:product_main_seller.field_product_product__main_seller_id -#: model:ir.model.fields,field_description:product_main_seller.field_product_template__main_seller_id -#: model_terms:ir.ui.view,arch_db:product_main_seller.view_product_template_search -msgid "Main Vendor" -msgstr "Fornitore principale" - -#. module: product_main_seller -#: model:ir.model,name:product_main_seller.model_product_template -msgid "Product" -msgstr "Prodotto" - -#. module: product_main_seller -#: model:ir.model.fields,help:product_main_seller.field_product_product__main_seller_id -#: model:ir.model.fields,help:product_main_seller.field_product_template__main_seller_id -msgid "Put your supplier info in first position to set as main vendor" -msgstr "" -"Inserire le informazioni fornitore nella prima posizione per impostarlo come " -"fornitore principale" diff --git a/product_main_seller/i18n/nl.po b/product_main_seller/i18n/nl.po deleted file mode 100644 index 1d03c36..0000000 --- a/product_main_seller/i18n/nl.po +++ /dev/null @@ -1,38 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * product_main_seller -# bosd , 2025. -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 16.0\n" -"Report-Msgid-Bugs-To: \n" -"Last-Translator: bosd \n" -"Language-Team: Dutch\n" -"Language: nl\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" -"PO-Revision-Date: 2025-04-18 13:34+0200\n" -"X-Generator: Gtranslator 47.1\n" - -#. module: product_main_seller -#: model:ir.model.fields,field_description:product_main_seller.field_product_product__main_seller_id -#: model:ir.model.fields,field_description:product_main_seller.field_product_template__main_seller_id -#: model_terms:ir.ui.view,arch_db:product_main_seller.view_product_template_search -msgid "Main Vendor" -msgstr "Hoofdleverancier" - -#. module: product_main_seller -#: model:ir.model,name:product_main_seller.model_product_template -msgid "Product" -msgstr "Product" - -#. module: product_main_seller -#: model:ir.model.fields,help:product_main_seller.field_product_product__main_seller_id -#: model:ir.model.fields,help:product_main_seller.field_product_template__main_seller_id -msgid "Put your supplier info in first position to set as main vendor" -msgstr "" -"Zet uw leverancier op de eerste plaats om deze als hoofdleverancier in te " -"stellen" diff --git a/product_main_seller/i18n/product_main_seller.pot b/product_main_seller/i18n/product_main_seller.pot deleted file mode 100644 index f425184..0000000 --- a/product_main_seller/i18n/product_main_seller.pot +++ /dev/null @@ -1,32 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * product_main_seller -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 18.0\n" -"Report-Msgid-Bugs-To: \n" -"Last-Translator: \n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: product_main_seller -#: model:ir.model.fields,field_description:product_main_seller.field_product_product__main_seller_id -#: model:ir.model.fields,field_description:product_main_seller.field_product_template__main_seller_id -#: model_terms:ir.ui.view,arch_db:product_main_seller.view_product_template_search -msgid "Main Vendor" -msgstr "" - -#. module: product_main_seller -#: model:ir.model,name:product_main_seller.model_product_template -msgid "Product" -msgstr "" - -#. module: product_main_seller -#: model:ir.model.fields,help:product_main_seller.field_product_product__main_seller_id -#: model:ir.model.fields,help:product_main_seller.field_product_template__main_seller_id -msgid "Put your supplier info in first position to set as main vendor" -msgstr "" diff --git a/product_main_seller/models/__init__.py b/product_main_seller/models/__init__.py deleted file mode 100644 index e8fa8f6..0000000 --- a/product_main_seller/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import product_template diff --git a/product_main_seller/models/product_template.py b/product_main_seller/models/product_template.py deleted file mode 100644 index f4d35ee..0000000 --- a/product_main_seller/models/product_template.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (C) 2022 - Today: GRAP (http://www.grap.coop) -# @author: Quentin DUPONT (quentin.dupont@grap.coop) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -from odoo import api -from odoo import fields -from odoo import models - - -class ProductTemplate(models.Model): - _inherit = "product.template" - - main_seller_id = fields.Many2one( - comodel_name="res.partner", - string="Main Vendor", - help="Put your supplier info in first position to set as main vendor", - compute="_compute_main_seller_id", - store=True, - ) - - @api.depends("variant_seller_ids.sequence", "variant_seller_ids.partner_id.active") - def _compute_main_seller_id(self): - for template in self: - if template.variant_seller_ids: - template.main_seller_id = fields.first( - template.variant_seller_ids.filtered( - lambda seller: seller.partner_id.active - ) - ).partner_id - else: - template.main_seller_id = False diff --git a/product_main_seller/pyproject.toml b/product_main_seller/pyproject.toml deleted file mode 100644 index 4231d0c..0000000 --- a/product_main_seller/pyproject.toml +++ /dev/null @@ -1,3 +0,0 @@ -[build-system] -requires = ["whool"] -build-backend = "whool.buildapi" diff --git a/product_main_seller/readme/CONTRIBUTORS.md b/product_main_seller/readme/CONTRIBUTORS.md deleted file mode 100644 index 65c3000..0000000 --- a/product_main_seller/readme/CONTRIBUTORS.md +++ /dev/null @@ -1 +0,0 @@ -- Quentin Dupont () diff --git a/product_main_seller/readme/DESCRIPTION.md b/product_main_seller/readme/DESCRIPTION.md deleted file mode 100644 index 20a1cce..0000000 --- a/product_main_seller/readme/DESCRIPTION.md +++ /dev/null @@ -1,5 +0,0 @@ -This module extends the Odoo Product module to compute and display the -main Vendor of each products. The main vendor is the first vendor in the -vendors list. - -![](../static/description/product_tree_view.png) diff --git a/product_main_seller/static/description/icon.png b/product_main_seller/static/description/icon.png deleted file mode 100644 index 2dcd8fd..0000000 Binary files a/product_main_seller/static/description/icon.png and /dev/null differ diff --git a/product_main_seller/static/description/index.html b/product_main_seller/static/description/index.html deleted file mode 100644 index 8c4e691..0000000 --- a/product_main_seller/static/description/index.html +++ /dev/null @@ -1,434 +0,0 @@ - - - - - -README.rst - - - -
- - - -Odoo Community Association - -
-

Product Main Vendor

- -

Beta License: AGPL-3 OCA/purchase-workflow Translate me on Weblate Try me on Runboat

-

This module extends the Odoo Product module to compute and display the -main Vendor of each products. The main vendor is the first vendor in the -vendors list.

-

image1

-

Table of contents

- -
-

Bug Tracker

-

Bugs are tracked on GitHub Issues. -In case of trouble, please check there if your issue has already been reported. -If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

-

Do not contact contributors directly about support or help with technical issues.

-
-
-

Credits

-
-

Authors

-
    -
  • GRAP
  • -
-
-
-

Contributors

- -
-
-

Maintainers

-

This module is maintained by the OCA.

- -Odoo Community Association - -

OCA, or the Odoo Community Association, is a nonprofit organization whose -mission is to support the collaborative development of Odoo features and -promote its widespread use.

-

Current maintainers:

-

legalsylvain quentinDupont

-

This module is part of the OCA/purchase-workflow project on GitHub.

-

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

-
-
-
-
- - diff --git a/product_main_seller/static/description/product_tree_view.png b/product_main_seller/static/description/product_tree_view.png deleted file mode 100644 index 3c770c8..0000000 Binary files a/product_main_seller/static/description/product_tree_view.png and /dev/null differ diff --git a/product_main_seller/tests/__init__.py b/product_main_seller/tests/__init__.py deleted file mode 100644 index d412bad..0000000 --- a/product_main_seller/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import test_seller diff --git a/product_main_seller/tests/test_seller.py b/product_main_seller/tests/test_seller.py deleted file mode 100644 index 9ab5958..0000000 --- a/product_main_seller/tests/test_seller.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright (C) 2022 - Today: GRAP (http://www.grap.coop) -# @author: Quentin DUPONT (quentin.dupont@grap.coop) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -from odoo import Command -from odoo.tests.common import TransactionCase - - -class TestSeller(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.product_workplace = cls.env.ref("product.product_product_24") - cls.product_acoustic = cls.env.ref("product.product_product_25") - cls.product_with_var_chair = cls.env.ref("product.product_product_11") - cls.product_without_seller_desk = cls.env.ref("product.product_product_3") - - cls.partner_woodcorner = cls.env.ref("base.res_partner_1") - cls.partner_azure = cls.env.ref("base.res_partner_12") - - def test_01_computed_main_vendor(self): - self.assertEqual( - self.product_acoustic.main_seller_id, - self.product_acoustic.seller_ids[0].partner_id, - ) - self.assertEqual( - self.product_with_var_chair.main_seller_id, - self.product_acoustic.product_variant_ids[0] - .variant_seller_ids[0] - .partner_id, - ) - - def test_02_replace_supplierinfo(self): - self.product_acoustic.seller_ids = [ - Command.clear(), - Command.create({"partner_id": self.partner_azure.id}), - ] - self.assertEqual(self.product_acoustic.main_seller_id.id, self.partner_azure.id) - - def test_03_add_supplierinfo_no_existing_supplierinfo(self): - self.product_without_seller_desk.seller_ids = [ - Command.create({"partner_id": self.partner_azure.id}), - ] - self.assertEqual( - self.product_without_seller_desk.main_seller_id.id, self.partner_azure.id - ) - - def test_03_add_supplierinfo_low_sequence(self): - self.product_workplace.seller_ids.write({"sequence": 1}) - self.product_workplace.seller_ids = [ - Command.create({"sequence": 100, "partner_id": self.partner_azure.id}), - ] - self.assertNotEqual( - self.product_workplace.main_seller_id.id, self.partner_azure.id - ) - - def test_03_add_supplierinfo_high_sequence(self): - self.product_workplace.seller_ids.write({"sequence": 1000}) - self.product_workplace.seller_ids = [ - Command.create({"sequence": 100, "partner_id": self.partner_azure.id}), - ] - self.assertEqual( - self.product_workplace.main_seller_id.id, self.partner_azure.id - ) - - def test_04_update_supplierinfo(self): - self.product_acoustic.seller_ids.write({"partner_id": self.partner_azure.id}) - self.assertEqual(self.product_acoustic.main_seller_id.id, self.partner_azure.id) - - def test_05_unlink_supplierinfo(self): - self.product_acoustic.seller_ids.unlink() - self.assertEqual(self.product_acoustic.main_seller_id.id, False) diff --git a/product_main_seller/views/view_product_product.xml b/product_main_seller/views/view_product_product.xml deleted file mode 100644 index 30ee16d..0000000 --- a/product_main_seller/views/view_product_product.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - product.product - - - - - - - - diff --git a/product_main_seller/views/view_product_template.xml b/product_main_seller/views/view_product_template.xml deleted file mode 100644 index e9251c1..0000000 --- a/product_main_seller/views/view_product_template.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - product.template - - - - - - - - - - - - - product.template - - - - - - - - diff --git a/product_sale_price_from_pricelist/data/report_paperformat.xml b/product_sale_price_from_pricelist/data/report_paperformat.xml new file mode 100644 index 0000000..03e74be --- /dev/null +++ b/product_sale_price_from_pricelist/data/report_paperformat.xml @@ -0,0 +1,20 @@ + + + + + Barcodes stickers format + + A4 + 0 + 0 + Portrait + 10 + 5 + 8 + 8 + + 0 + 75 + + + diff --git a/product_sale_price_from_pricelist/views/product_view.xml b/product_sale_price_from_pricelist/views/product_view.xml index 6fb357b..6b88906 100644 --- a/product_sale_price_from_pricelist/views/product_view.xml +++ b/product_sale_price_from_pricelist/views/product_view.xml @@ -1,4 +1,4 @@ - + @@ -9,68 +9,51 @@ - - last_purchase_price_compute_type != 'manual_update' - + last_purchase_price_compute_type != 'manual_update' - - - - - - + + + + + + - + product.template.tree.price.automatic product.template - + - - + + - product.sale.price.pricelist.res.config.settings.form + product.print.supermarket.res.config.settings.form res.config.settings + - - - -
- Select the pricelist used to calculate sale prices from last purchase prices -
+ + +
+
-
+
-
+ @@ -81,8 +64,7 @@ - @@ -91,4 +73,4 @@
-
+ \ No newline at end of file diff --git a/run_price_tests.sh b/run_price_tests.sh deleted file mode 100755 index 5ed010d..0000000 --- a/run_price_tests.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/bin/bash -# Script para ejecutar tests de website_sale_aplicoop - -echo "==========================================" -echo "Ejecutando tests de website_sale_aplicoop" -echo "==========================================" - -# Ejecutar tests específicos de precios -docker-compose exec -T odoo odoo shell -c /etc/odoo/odoo.conf -d odoo << 'PYTHON_SCRIPT' -import logging -_logger = logging.getLogger(__name__) - -# Cargar el módulo -env = self.env - -# Test 1: Verificar que el método _compute_price_with_taxes existe -print("\n=== Test 1: Verificar método _compute_price_with_taxes ===") -try: - from odoo.addons.website_sale_aplicoop.controllers.website_sale import AplicoopWebsiteSale - controller = AplicoopWebsiteSale() - print("✓ Método _compute_price_with_taxes encontrado") - print(f" Firma: {controller._compute_price_with_taxes.__doc__}") -except Exception as e: - print(f"✗ Error: {e}") - -# Test 2: Crear producto con impuesto y verificar cálculo -print("\n=== Test 2: Calcular precio con impuesto 21% ===") -product = None -try: - # Obtener o crear impuesto - tax_21 = env['account.tax'].search([ - ('amount', '=', 21.0), - ('type_tax_use', '=', 'sale'), - ('company_id', '=', env.company.id) - ], limit=1) - - if not tax_21: - # Crear tax group si no existe - country_es = env.ref('base.es', raise_if_not_found=False) - if not country_es: - country_es = env['res.country'].search([('code', '=', 'ES')], limit=1) - - tax_group = env['account.tax.group'].search([ - ('company_id', '=', env.company.id), - ('country_id', '=', country_es.id if country_es else False) - ], limit=1) - - if not tax_group: - tax_group = env['account.tax.group'].create({ - 'name': 'IVA', - 'company_id': env.company.id, - 'country_id': country_es.id if country_es else False, - }) - 'name': 'IVA 21% Test', - 'amount': 21.0, - 'amount_type': 'percent', - 'type_tax_use': 'sale', - 'price_include': False, - 'company_id': env.company.id, - 'country_id': country_es.id if country_es else False, - 'tax_group_id': tax_group.id, - }) - print(f" Impuesto creado: {tax_21.name}") - else: - print(f" Impuesto encontrado: {tax_21.name}") - - # Crear producto de prueba - product = env['product.product'].search([ - ('name', '=', 'Test Product Tax Calculation') - ], limit=1) - - if not product: - product = env['product.product'].create({ - 'name': 'Test Product Tax Calculation', - 'list_price': 100.0, - 'taxes_id': [(6, 0, [tax_21.id])], - 'company_id': env.company.id, - }) - print(f" Producto creado: {product.name}") - else: - print(f" Producto encontrado: {product.name}") - - # Calcular precio con impuestos - base_price = 100.0 - taxes = product.taxes_id.filtered( - lambda t: t.company_id == env.company - ) - - if taxes: - tax_result = taxes.compute_all( - base_price, - currency=env.company.currency_id, - quantity=1.0, - product=product, - ) - - price_with_tax = tax_result['total_included'] - price_without_tax = tax_result['total_excluded'] - - print(f" Precio base: {base_price:.2f} €") - print(f" Precio sin impuestos: {price_without_tax:.2f} €") - print(f" Precio con impuestos: {price_with_tax:.2f} €") - print(f" Impuesto aplicado: {price_with_tax - price_without_tax:.2f} €") - - if abs(price_with_tax - 121.0) < 0.01: - print("✓ Test PASADO: 100 + 21% = 121.00") - else: - print(f"✗ Test FALLADO: Esperado 121.00, obtenido {price_with_tax:.2f}") - else: - print("✗ Producto sin impuestos configurados") - -except Exception as e: - print(f"✗ Error en test: {e}") - import traceback - traceback.print_exc() - -# Test 3: Verificar comportamiento de OCA _get_price -print("\n=== Test 3: Verificar OCA _get_price ===") -if product: - try: - pricelist = env['product.pricelist'].search([ - ('company_id', '=', env.company.id) - ], limit=1) - - if not pricelist: - pricelist = env['product.pricelist'].create({ - 'name': 'Test Pricelist', - 'company_id': env.company.id, - }) - - price_info = product._get_price( - qty=1.0, - pricelist=pricelist, - fposition=False, - ) - - print(f" Precio OCA (value): {price_info.get('value', 0):.2f} €") - print(f" Tax included: {price_info.get('tax_included', False)}") - print(f" Original value: {price_info.get('original_value', 0):.2f} €") - print(f" Discount: {price_info.get('discount', 0):.1f}%") - - if abs(price_info['value'] - 100.0) < 0.01: - print("✓ OCA retorna precio base SIN impuestos (esperado)") - else: - print(f"✗ OCA debería retornar 100.0, retornó {price_info['value']:.2f}") - - except Exception as e: - print(f"✗ Error en test OCA: {e}") - import traceback - traceback.print_exc() -else: - print("✗ Test 3 omitido: producto no creado en Test 2") - -# Hacer commit para que los cambios persistan -env.cr.commit() -PYTHON_SCRIPT diff --git a/test_prices.py b/test_prices.py deleted file mode 100644 index 1a221f4..0000000 --- a/test_prices.py +++ /dev/null @@ -1,187 +0,0 @@ -#!/usr/bin/env python3 -""" -Script de prueba para verificar que los precios incluyen impuestos. -Se ejecuta dentro del contenedor de Odoo. -""" - -import os -import sys - -# Agregar path de Odoo -sys.path.insert(0, "/usr/lib/python3/dist-packages") - -import odoo -from odoo import SUPERUSER_ID -from odoo import api - -# Configurar Odoo -odoo.tools.config["db_host"] = os.environ.get("HOST", "db") -odoo.tools.config["db_port"] = int(os.environ.get("PORT", 5432)) -odoo.tools.config["db_user"] = os.environ.get("USER", "odoo") -odoo.tools.config["db_password"] = os.environ.get("PASSWORD", "odoo") - -print("\n" + "=" * 60) -print("TEST: Precios con impuestos incluidos") -print("=" * 60 + "\n") - -try: - db_name = "odoo" - registry = odoo.registry(db_name) - - with registry.cursor() as cr: - env = api.Environment(cr, SUPERUSER_ID, {}) - - print(f"✓ Conectado a BD: {db_name}") - print(f" Usuario: {env.user.name}") - print(f" Compañía: {env.company.name}\n") - - # Test 1: Verificar módulo - print("TEST 1: Verificar módulo instalado") - print("-" * 60) - module = env["ir.module.module"].search( - [("name", "=", "website_sale_aplicoop")], limit=1 - ) - - if module and module.state == "installed": - print(f"✓ Módulo website_sale_aplicoop instalado") - else: - print(f"✗ Módulo NO instalado") - sys.exit(1) - - # Test 2: Verificar método nuevo - print("\nTEST 2: Verificar método _compute_price_with_taxes") - print("-" * 60) - try: - from odoo.addons.website_sale_aplicoop.controllers.website_sale import ( - AplicoopWebsiteSale, - ) - - controller = AplicoopWebsiteSale() - - if hasattr(controller, "_compute_price_with_taxes"): - print("✓ Método _compute_price_with_taxes existe") - - import inspect - - sig = inspect.signature(controller._compute_price_with_taxes) - print(f" Firma: {sig}") - else: - print("✗ Método NO encontrado") - except Exception as e: - print(f"✗ Error: {e}") - - # Test 3: Probar cálculo de impuestos - print("\nTEST 3: Calcular precio con impuestos") - print("-" * 60) - - # Buscar un producto con impuestos - product = env["product.product"].search( - [("sale_ok", "=", True), ("taxes_id", "!=", False)], limit=1 - ) - - if not product: - print(" Creando producto de prueba...") - - # Buscar impuesto existente - tax = env["account.tax"].search( - [("type_tax_use", "=", "sale"), ("company_id", "=", env.company.id)], - limit=1, - ) - - if tax: - product = env["product.product"].create( - { - "name": "Test Product With Tax", - "list_price": 100.0, - "taxes_id": [(6, 0, [tax.id])], - "sale_ok": True, - } - ) - print(f" Producto creado: {product.name}") - else: - print(" ✗ No hay impuestos de venta configurados") - sys.exit(1) - else: - print(f" Producto encontrado: {product.name}") - - print(f" Precio de lista: {product.list_price:.2f} €") - - taxes = product.taxes_id.filtered(lambda t: t.company_id == env.company) - - if taxes: - print(f" Impuestos: {', '.join(f'{t.name} ({t.amount}%)' for t in taxes)}") - - # Calcular precio con impuestos - base_price = product.list_price - tax_result = taxes.compute_all( - base_price, - currency=env.company.currency_id, - quantity=1.0, - product=product, - ) - - price_without_tax = tax_result["total_excluded"] - price_with_tax = tax_result["total_included"] - tax_amount = price_with_tax - price_without_tax - - print(f"\n Cálculo:") - print(f" Base: {base_price:.2f} €") - print(f" Sin IVA: {price_without_tax:.2f} €") - print(f" IVA: {tax_amount:.2f} €") - print(f" CON IVA: {price_with_tax:.2f} €") - - if price_with_tax > price_without_tax: - print( - f"\n ✓ PASADO: Precio con IVA ({price_with_tax:.2f}) > sin IVA ({price_without_tax:.2f})" - ) - else: - print(f"\n ✗ FALLADO: Impuestos no se calculan correctamente") - else: - print(" ⚠ Producto sin impuestos") - - # Test 4: Verificar OCA _get_price - print("\nTEST 4: Verificar OCA _get_price") - print("-" * 60) - - pricelist = env["product.pricelist"].search( - [("company_id", "=", env.company.id)], limit=1 - ) - - if pricelist and product: - price_info = product._get_price( - qty=1.0, - pricelist=pricelist, - fposition=False, - ) - - print(f" OCA _get_price:") - print(f" value: {price_info.get('value', 0):.2f} €") - print(f" tax_included: {price_info.get('tax_included', False)}") - - if not price_info.get("tax_included", False): - print(f" ✓ PASADO: OCA retorna precio SIN IVA (esperado)") - else: - print(f" ⚠ OCA indica IVA incluido") - - print("\n" + "=" * 60) - print("RESUMEN") - print("=" * 60) - print(""" -Corrección implementada: -1. ✓ Método _compute_price_with_taxes añadido -2. ✓ Calcula precio CON IVA usando taxes.compute_all() -3. ✓ Usado en eskaera_shop y add_to_eskaera_cart -4. ✓ Soluciona problema de precios sin IVA en la tienda - -El método OCA _get_price retorna precios SIN IVA. -Nuestra función _compute_price_with_taxes añade el IVA. - """) - - print("✓ Todos los tests completados exitosamente\n") - -except Exception as e: - print(f"\n✗ ERROR: {e}\n") - import traceback - - traceback.print_exc() - sys.exit(1) diff --git a/test_with_docker_run.sh b/test_with_docker_run.sh deleted file mode 100755 index a74b77c..0000000 --- a/test_with_docker_run.sh +++ /dev/null @@ -1,224 +0,0 @@ -#!/bin/bash -# Script para ejecutar tests usando docker run (contenedor aislado) - -echo "==========================================" -echo "Ejecutando tests con docker run" -echo "==========================================" - -# Verificar que la red de docker-compose existe -docker network inspect addons-cm_default >/dev/null 2>&1 || { - echo "Creando red de docker..." - docker network create addons-cm_default -} - -# Ejecutar tests en un contenedor temporal -docker run --rm \ - --network addons-cm_default \ - -v "$(pwd)":/mnt/extra-addons \ - -e HOST=db \ - -e PORT=5432 \ - -e USER=odoo \ - -e PASSWORD=odoo \ - odoo:18 \ - python3 << 'PYTHON_TEST' -import sys -import os - -# Configurar paths -sys.path.insert(0, '/usr/lib/python3/dist-packages') -os.chdir('/mnt/extra-addons') - -import odoo -from odoo import api, SUPERUSER_ID - -# Configurar Odoo -odoo.tools.config['db_host'] = 'db' -odoo.tools.config['db_port'] = 5432 -odoo.tools.config['db_user'] = 'odoo' -odoo.tools.config['db_password'] = 'odoo' -odoo.tools.config['addons_path'] = '/usr/lib/python3/dist-packages/odoo/addons,/mnt/extra-addons' - -print("\n=== Conectando a la base de datos ===") - -try: - # Conectar a la base de datos - db_name = 'odoo' - registry = odoo.registry(db_name) - - with registry.cursor() as cr: - env = api.Environment(cr, SUPERUSER_ID, {}) - - print("✓ Conectado a la base de datos Odoo") - print(f" Base de datos: {db_name}") - print(f" Usuario: {env.user.name}") - print(f" Compañía: {env.company.name}") - - # Test 1: Verificar que el módulo está instalado - print("\n=== Test 1: Verificar módulo website_sale_aplicoop ===") - module = env['ir.module.module'].search([ - ('name', '=', 'website_sale_aplicoop') - ], limit=1) - - if module and module.state == 'installed': - print(f"✓ Módulo instalado (versión: {module.installed_version})") - else: - print(f"✗ Módulo NO instalado (estado: {module.state if module else 'no encontrado'})") - - # Test 2: Verificar método _compute_price_with_taxes - print("\n=== Test 2: Verificar método _compute_price_with_taxes ===") - try: - from odoo.addons.website_sale_aplicoop.controllers.website_sale import AplicoopWebsiteSale - controller = AplicoopWebsiteSale() - - if hasattr(controller, '_compute_price_with_taxes'): - print("✓ Método _compute_price_with_taxes encontrado") - - # Verificar firma del método - import inspect - sig = inspect.signature(controller._compute_price_with_taxes) - params = list(sig.parameters.keys()) - print(f" Parámetros: {params}") - else: - print("✗ Método _compute_price_with_taxes NO encontrado") - except ImportError as e: - print(f"✗ Error al importar controlador: {e}") - - # Test 3: Calcular precio con impuesto 21% - print("\n=== Test 3: Calcular precio con impuesto 21% ===") - - # Buscar o crear impuesto 21% - tax_21 = env['account.tax'].search([ - ('amount', '=', 21.0), - ('type_tax_use', '=', 'sale'), - ('company_id', '=', env.company.id) - ], limit=1) - - if not tax_21: - print(" Buscando impuestos existentes...") - all_taxes = env['account.tax'].search([ - ('type_tax_use', '=', 'sale'), - ('company_id', '=', env.company.id) - ]) - print(f" Impuestos de venta encontrados: {len(all_taxes)}") - for tax in all_taxes[:5]: # Mostrar primeros 5 - print(f" - {tax.name}: {tax.amount}%") - - # Usar el primer impuesto disponible - if all_taxes: - tax_21 = all_taxes[0] - print(f" Usando impuesto: {tax_21.name} ({tax_21.amount}%)") - else: - print(f" Impuesto encontrado: {tax_21.name} ({tax_21.amount}%)") - - if tax_21: - # Buscar un producto existente con impuesto - product = env['product.product'].search([ - ('taxes_id', 'in', [tax_21.id]) - ], limit=1) - - if not product: - print(" No se encontró producto con este impuesto, buscando cualquier producto...") - product = env['product.product'].search([ - ('sale_ok', '=', True) - ], limit=1) - - if product: - print(f" Producto encontrado: {product.name}") - print(f" Precio lista: {product.list_price:.2f} €") - print(f" Impuestos actuales: {[t.name for t in product.taxes_id]}") - else: - print(f" Producto encontrado: {product.name}") - - if product: - # Probar cálculo de impuestos - base_price = 100.0 - taxes = product.taxes_id.filtered( - lambda t: t.company_id == env.company - ) - - if taxes: - tax_result = taxes.compute_all( - base_price, - currency=env.company.currency_id, - quantity=1.0, - product=product, - ) - - price_without_tax = tax_result['total_excluded'] - price_with_tax = tax_result['total_included'] - tax_amount = price_with_tax - price_without_tax - - print(f"\n Cálculo de impuestos:") - print(f" Precio base: {base_price:.2f} €") - print(f" Precio sin impuestos: {price_without_tax:.2f} €") - print(f" Impuestos aplicados: {tax_amount:.2f} €") - print(f" Precio CON impuestos: {price_with_tax:.2f} €") - - # Verificar que el precio con impuestos es mayor - if price_with_tax > price_without_tax: - print(f" ✓ Test PASADO: Los impuestos se suman correctamente") - else: - print(f" ✗ Test FALLADO: El precio con impuestos debería ser mayor") - else: - print(" ⚠ Producto sin impuestos configurados") - else: - print(" ✗ No se encontró ningún producto para probar") - else: - print(" ✗ No se encontró ningún impuesto") - - # Test 4: Verificar comportamiento de OCA _get_price - print("\n=== Test 4: Verificar OCA _get_price (sin impuestos) ===") - - product = env['product.product'].search([ - ('sale_ok', '=', True) - ], limit=1) - - if product: - pricelist = env['product.pricelist'].search([ - ('company_id', '=', env.company.id) - ], limit=1) - - if pricelist: - try: - price_info = product._get_price( - qty=1.0, - pricelist=pricelist, - fposition=False, - ) - - print(f" Producto: {product.name}") - print(f" Precio OCA (value): {price_info.get('value', 0):.2f} €") - print(f" Tax included: {price_info.get('tax_included', False)}") - print(f" Original value: {price_info.get('original_value', 0):.2f} €") - print(f" Discount: {price_info.get('discount', 0):.1f}%") - - if not price_info.get('tax_included', False): - print(" ✓ OCA retorna precio SIN impuestos incluidos (comportamiento esperado)") - else: - print(" ⚠ OCA indica que los impuestos están incluidos") - - except Exception as e: - print(f" ✗ Error al llamar _get_price: {e}") - else: - print(" ✗ No se encontró pricelist") - else: - print(" ✗ No se encontró producto") - - print("\n=== Resumen ===") - print("Los cambios implementados:") - print("1. Método _compute_price_with_taxes añadido al controlador") - print("2. Este método calcula el precio CON impuestos incluidos") - print("3. Se usa en eskaera_shop y add_to_eskaera_cart") - print("4. Soluciona el problema de mostrar precios sin IVA") - print() - -except Exception as e: - print(f"\n✗ Error general: {e}") - import traceback - traceback.print_exc() - -print("\n=== Tests completados ===\n") -PYTHON_TEST - -echo "" -echo "Tests finalizados" diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index 1573a4b..3ad99c9 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -3,83 +3,75 @@ import json import logging -from datetime import datetime -from datetime import timedelta - -from odoo import _ -from odoo import http +from datetime import datetime, timedelta +from odoo import http, _ from odoo.http import request - from odoo.addons.website_sale.controllers.main import WebsiteSale _logger = logging.getLogger(__name__) class AplicoopWebsiteSale(WebsiteSale): - """Controlador personalizado para website_sale de Aplicoop. + '''Controlador personalizado para website_sale de Aplicoop. Sustitución de la antigua aplicación Aplicoop: https://sourceforge.net/projects/aplicoop/ - """ + ''' def _get_day_names(self, env=None): - """Get translated day names list (0=Monday to 6=Sunday). + '''Get translated day names list (0=Monday to 6=Sunday). Gets day names from fields_get() which returns the selection values TRANSLATED according to the user's current language preference. Returns: list of 7 translated day names in the user's language - """ + ''' if env is None: from odoo.http import request - env = request.env - + # Log context language for debugging - context_lang = env.context.get("lang", "NO_LANG") - _logger.info("📅 _get_day_names called with context lang: %s", context_lang) - - group_order_model = env["group.order"] + context_lang = env.context.get('lang', 'NO_LANG') + _logger.info('📅 _get_day_names called with context lang: %s', context_lang) + + group_order_model = env['group.order'] # Use fields_get() to get field definitions WITH translations applied - fields = group_order_model.fields_get(["pickup_day"]) - selection_options = fields.get("pickup_day", {}).get("selection", []) - + fields = group_order_model.fields_get(['pickup_day']) + selection_options = fields.get('pickup_day', {}).get('selection', []) + # Log the actual day names returned day_names = [name for value, name in selection_options] - _logger.info( - "📅 Returning day names: %s", - day_names[:3] if len(day_names) >= 3 else day_names, - ) - + _logger.info('📅 Returning day names: %s', day_names[:3] if len(day_names) >= 3 else day_names) + return day_names def _get_next_date_for_weekday(self, weekday_num, start_date=None): - """Calculate the next occurrence of a given weekday (0=Monday to 6=Sunday). - + '''Calculate the next occurrence of a given weekday (0=Monday to 6=Sunday). + Args: weekday_num: int, 0=Monday, 6=Sunday start_date: datetime.date, starting point (defaults to today) - + Returns: datetime.date of the next occurrence of that weekday - """ + ''' if start_date is None: start_date = datetime.now().date() - + # Convert int weekday (0=Mon) to Python's weekday (0=Mon is same) target_weekday = int(weekday_num) current_weekday = start_date.weekday() - + # Calculate days until target weekday days_ahead = target_weekday - current_weekday if days_ahead <= 0: # Target day has already occurred this week days_ahead += 7 - + return start_date + timedelta(days=days_ahead) def _get_detected_language(self, **post): - """Detect user language from multiple sources with fallback priority. - + '''Detect user language from multiple sources with fallback priority. + Priority: 1. URL parameter 'lang' 2. POST JSON parameter 'lang' @@ -87,15 +79,15 @@ class AplicoopWebsiteSale(WebsiteSale): 4. request.env.context['lang'] 5. User's language preference 6. Default: 'es_ES' - + Returns: str - language code (e.g., 'es_ES', 'eu_ES', 'en_US') - """ - url_lang = request.params.get("lang") - post_lang = post.get("lang") - cookie_lang = request.httprequest.cookies.get("lang") - context_lang = request.env.context.get("lang") - user_lang = request.env.user.lang or "es_ES" - + ''' + url_lang = request.params.get('lang') + post_lang = post.get('lang') + cookie_lang = request.httprequest.cookies.get('lang') + context_lang = request.env.context.get('lang') + user_lang = request.env.user.lang or 'es_ES' + detected = None if url_lang: detected = url_lang @@ -107,169 +99,152 @@ class AplicoopWebsiteSale(WebsiteSale): detected = context_lang else: detected = user_lang - - _logger.info( - "🌐 Language detection: url=%s, post=%s, cookie=%s, context=%s, user=%s → DETECTED=%s", - url_lang, - post_lang, - cookie_lang, - context_lang, - user_lang, - detected, - ) + + _logger.info('🌐 Language detection: url=%s, post=%s, cookie=%s, context=%s, user=%s → DETECTED=%s', + url_lang, post_lang, cookie_lang, context_lang, user_lang, detected) return detected def _get_translated_labels(self, lang=None): - """Get ALL translated UI labels and messages unified. - + '''Get ALL translated UI labels and messages unified. + This is the SINGLE SOURCE OF TRUTH for all user-facing messages. Every endpoint that returns JSON should use this to get consistent translations. - + Args: lang: str - language code (defaults to detected language) - + Returns: dict - ALL translated labels and messages - """ + ''' if lang is None: lang = self._get_detected_language() - + # Create a new environment with the target language context # This is the correct way in Odoo to get translations in a specific language env_lang = request.env(context=dict(request.env.context, lang=lang)) - + # Use the imported _ function which respects the environment context # The strings must exist in models/js_translations.py labels = { # ============ SUMMARY TABLE LABELS ============ - "product": env_lang._("Product"), - "quantity": env_lang._("Quantity"), - "price": env_lang._("Price"), - "subtotal": env_lang._("Subtotal"), - "total": env_lang._("Total"), - "empty": env_lang._("This order's cart is empty."), - "empty_cart": env_lang._("Your cart is empty"), + 'product': env_lang._('Product'), + 'quantity': env_lang._('Quantity'), + 'price': env_lang._('Price'), + 'subtotal': env_lang._('Subtotal'), + 'total': env_lang._('Total'), + 'empty': env_lang._('This order\'s cart is empty.'), + 'empty_cart': env_lang._('Your cart is empty'), + # ============ ACTION LABELS ============ - "add_to_cart": env_lang._("Add to Cart"), - "remove_from_cart": env_lang._("Remove from Cart"), - "remove_item": env_lang._("Remove Item"), - "save_cart": env_lang._("Save Cart"), - "reload_cart": env_lang._("Reload Cart"), - "load_draft": env_lang._("Load Draft"), - "proceed_to_checkout": env_lang._("Proceed to Checkout"), - "confirm_order": env_lang._("Confirm Order"), - "back_to_cart": env_lang._("Back to Cart"), + 'add_to_cart': env_lang._('Add to Cart'), + 'remove_from_cart': env_lang._('Remove from Cart'), + 'remove_item': env_lang._('Remove Item'), + 'save_cart': env_lang._('Save Cart'), + 'reload_cart': env_lang._('Reload Cart'), + 'load_draft': env_lang._('Load Draft'), + 'proceed_to_checkout': env_lang._('Proceed to Checkout'), + 'confirm_order': env_lang._('Confirm Order'), + 'back_to_cart': env_lang._('Back to Cart'), + # ============ MODAL CONFIRMATION LABELS ============ - "confirmation": env_lang._("Confirmation"), - "cancel": env_lang._("Cancel"), - "confirm": env_lang._("Confirm"), - "merge": env_lang._("Merge"), - "replace": env_lang._("Replace"), - "draft_merge_btn": env_lang._("Merge"), - "draft_replace_btn": env_lang._("Replace"), + 'confirmation': env_lang._('Confirmation'), + 'cancel': env_lang._('Cancel'), + 'confirm': env_lang._('Confirm'), + 'merge': env_lang._('Merge'), + 'replace': env_lang._('Replace'), + 'draft_merge_btn': env_lang._('Merge'), + 'draft_replace_btn': env_lang._('Replace'), + # ============ SUCCESS MESSAGES ============ - "draft_saved_success": env_lang._("Cart saved as draft successfully"), - "draft_loaded_success": env_lang._("Draft order loaded successfully"), - "draft_merged_success": env_lang._("Draft merged successfully"), - "draft_replaced_success": env_lang._("Draft replaced successfully"), - "order_confirmed": env_lang._("Thank you! Your order has been confirmed."), - "order_loaded": env_lang._("Order loaded"), - "cart_restored": env_lang._("Your cart has been restored"), - "qty_updated": env_lang._("Quantity updated"), + 'draft_saved_success': env_lang._('Cart saved as draft successfully'), + 'draft_loaded_success': env_lang._('Draft order loaded successfully'), + 'draft_merged_success': env_lang._('Draft merged successfully'), + 'draft_replaced_success': env_lang._('Draft replaced successfully'), + 'order_confirmed': env_lang._('Thank you! Your order has been confirmed.'), + 'order_loaded': env_lang._('Order loaded'), + 'cart_restored': env_lang._('Your cart has been restored'), + 'qty_updated': env_lang._('Quantity updated'), + # ============ ERROR MESSAGES ============ - "error_save_draft": env_lang._("Error saving cart"), - "error_load_draft": env_lang._("Error loading draft"), - "error_confirm_order": env_lang._("Error confirming order"), - "error_processing_response": env_lang._("Error processing response"), - "error_connection": env_lang._("Connection error"), - "error_unknown": env_lang._("Unknown error"), - "error_invalid_data": env_lang._("Invalid data provided"), - "error_order_not_found": env_lang._("Order not found"), - "error_no_draft_orders": env_lang._("No draft orders found for this week"), - "invalid_quantity": env_lang._("Please enter a valid quantity"), + 'error_save_draft': env_lang._('Error saving cart'), + 'error_load_draft': env_lang._('Error loading draft'), + 'error_confirm_order': env_lang._('Error confirming order'), + 'error_processing_response': env_lang._('Error processing response'), + 'error_connection': env_lang._('Connection error'), + 'error_unknown': env_lang._('Unknown error'), + 'error_invalid_data': env_lang._('Invalid data provided'), + 'error_order_not_found': env_lang._('Order not found'), + 'error_no_draft_orders': env_lang._('No draft orders found for this week'), + 'invalid_quantity': env_lang._('Please enter a valid quantity'), + # ============ CONFIRMATION MESSAGES ============ - "save_draft_confirm": env_lang._( - "Are you sure you want to save this cart as draft?\n\nItems to save: " - ), - "save_draft_reload": env_lang._( - "You will be able to reload this cart later." - ), - "reload_draft_confirm": env_lang._( - "Are you sure you want to load your last saved draft?" - ), - "reload_draft_replace": env_lang._( - "This will replace the current items in your cart" - ), - "reload_draft_with": env_lang._("with the saved draft."), + 'save_draft_confirm': env_lang._('Are you sure you want to save this cart as draft?\n\nItems to save: '), + 'save_draft_reload': env_lang._('You will be able to reload this cart later.'), + 'reload_draft_confirm': env_lang._('Are you sure you want to load your last saved draft?'), + 'reload_draft_replace': env_lang._('This will replace the current items in your cart'), + 'reload_draft_with': env_lang._('with the saved draft.'), + # ============ DRAFT MODAL LABELS ============ - "draft_already_exists": env_lang._("Draft Already Exists"), - "draft_exists_message": env_lang._( - "A saved draft already exists for this week." - ), - "draft_two_options": env_lang._("You have two options:"), - "draft_option1_title": env_lang._("Option 1: Merge with Existing Draft"), - "draft_option1_desc": env_lang._( - "Combine your current cart with the existing draft." - ), - "draft_existing_items": env_lang._("Existing draft has"), - "draft_current_items": env_lang._("Current cart has"), - "draft_items_count": env_lang._("item(s)"), - "draft_merge_note": env_lang._( - "Products will be merged by adding quantities. If a product exists in both, quantities will be combined." - ), - "draft_option2_title": env_lang._("Option 2: Replace with Current Cart"), - "draft_option2_desc": env_lang._( - "Delete the old draft and save only the current cart items." - ), - "draft_replace_warning": env_lang._( - "The existing draft will be permanently deleted." - ), + 'draft_already_exists': env_lang._('Draft Already Exists'), + 'draft_exists_message': env_lang._('A saved draft already exists for this week.'), + 'draft_two_options': env_lang._('You have two options:'), + 'draft_option1_title': env_lang._('Option 1: Merge with Existing Draft'), + 'draft_option1_desc': env_lang._('Combine your current cart with the existing draft.'), + 'draft_existing_items': env_lang._('Existing draft has'), + 'draft_current_items': env_lang._('Current cart has'), + 'draft_items_count': env_lang._('item(s)'), + 'draft_merge_note': env_lang._('Products will be merged by adding quantities. If a product exists in both, quantities will be combined.'), + 'draft_option2_title': env_lang._('Option 2: Replace with Current Cart'), + 'draft_option2_desc': env_lang._('Delete the old draft and save only the current cart items.'), + 'draft_replace_warning': env_lang._('The existing draft will be permanently deleted.'), + # ============ CHECKOUT PAGE LABELS ============ - "home_delivery": env_lang._("Home Delivery"), - "delivery_information": env_lang._("Delivery Information"), - "delivery_info_template": env_lang._( - "Delivery Information: Your order will be delivered at {pickup_day} {pickup_date}" - ), - "important": env_lang._("Important"), - "confirm_order_warning": env_lang._( - "Once you confirm this order, you will not be able to modify it. Please review carefully before confirming." - ), + 'home_delivery': env_lang._('Home Delivery'), + 'delivery_information': env_lang._('Delivery Information'), + 'delivery_info_template': env_lang._('Delivery Information: Your order will be delivered at {pickup_day} {pickup_date}'), + 'important': env_lang._('Important'), + 'confirm_order_warning': env_lang._('Once you confirm this order, you will not be able to modify it. Please review carefully before confirming.'), + # ============ PORTAL PAGE LABELS ============ - "load_in_cart": env_lang._("Load in Cart"), - "consumer_group": env_lang._("Consumer Group"), - "delivery_date": env_lang._("Delivery Date:"), - "pickup_date": env_lang._("Pickup Date:"), - "delivery_notice": env_lang._("Delivery Notice:"), - "no_delivery_instructions": env_lang._("No special delivery instructions"), - "pickup_location": env_lang._("Pickup Location:"), + 'load_in_cart': env_lang._('Load in Cart'), + 'consumer_group': env_lang._('Consumer Group'), + 'delivery_date': env_lang._('Delivery Date:'), + 'pickup_date': env_lang._('Pickup Date:'), + 'delivery_notice': env_lang._('Delivery Notice:'), + 'no_delivery_instructions': env_lang._('No special delivery instructions'), + 'pickup_location': env_lang._('Pickup Location:'), + # ============ DAY NAMES (FOR PORTAL) ============ - "monday": env_lang._("Monday"), - "tuesday": env_lang._("Tuesday"), - "wednesday": env_lang._("Wednesday"), - "thursday": env_lang._("Thursday"), - "friday": env_lang._("Friday"), - "saturday": env_lang._("Saturday"), - "sunday": env_lang._("Sunday"), + 'monday': env_lang._('Monday'), + 'tuesday': env_lang._('Tuesday'), + 'wednesday': env_lang._('Wednesday'), + 'thursday': env_lang._('Thursday'), + 'friday': env_lang._('Friday'), + 'saturday': env_lang._('Saturday'), + 'sunday': env_lang._('Sunday'), + # ============ CATEGORY FILTER ============ - "browse_categories": env_lang._("Browse Product Categories"), - "all_categories": env_lang._("All categories"), - "categories": env_lang._("Categories"), + 'browse_categories': env_lang._('Browse Product Categories'), + 'all_categories': env_lang._('All categories'), + 'categories': env_lang._('Categories'), + # ============ SEARCH LABELS ============ - "search": env_lang._("Search"), - "search_products": env_lang._("Search products..."), - "no_results": env_lang._("No products found"), + 'search': env_lang._('Search'), + 'search_products': env_lang._('Search products...'), + 'no_results': env_lang._('No products found'), + # ============ MISC ============ - "items": env_lang._("items"), - "added_to_cart": env_lang._("added to cart"), + 'items': env_lang._('items'), + 'added_to_cart': env_lang._('added to cart'), } - + return labels def _build_category_hierarchy(self, categories): - """Organiza las categorías en una estructura jerárquica padre-hijo. - + '''Organiza las categorías en una estructura jerárquica padre-hijo. + Args: categories: product.category recordset - + Returns: list de dicts con estructura: { 'id': category_id, @@ -277,109 +252,104 @@ class AplicoopWebsiteSale(WebsiteSale): 'parent_id': parent_id, 'children': [list of child dicts] } - """ + ''' if not categories: return [] - + # Crear mapa de categorías por ID category_map = {} for cat in categories: category_map[cat.id] = { - "id": cat.id, - "name": cat.name, - "parent_id": cat.parent_id.id if cat.parent_id else None, - "children": [], + 'id': cat.id, + 'name': cat.name, + 'parent_id': cat.parent_id.id if cat.parent_id else None, + 'children': [] } - + # Identificar categorías raíz (sin padre en la lista) y organizar jerarquía roots = [] for cat_id, cat_info in category_map.items(): - parent_id = cat_info["parent_id"] - + parent_id = cat_info['parent_id'] + # Si el padre no está en la lista de categorías disponibles, es una raíz if parent_id is None or parent_id not in category_map: roots.append(cat_info) else: # Agregar a los hijos de su padre - category_map[parent_id]["children"].append(cat_info) - + 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"]) + items.sort(key=lambda x: x['name']) for item in items: - if item["children"]: - sort_hierarchy(item["children"]) - + if item['children']: + sort_hierarchy(item['children']) + sort_hierarchy(roots) return roots - @http.route(["/eskaera"], type="http", auth="user", website=True) + @http.route(['/eskaera'], type='http', auth='user', website=True) def eskaera_list(self, **post): - """Página de pedidos de grupo abiertos esta semana. + '''Página de pedidos de grupo abiertos esta semana. Muestra todos los pedidos abiertos de la compañía del usuario. Seguridad controlada por record rule (company_id filtering). - """ - group_order_obj = request.env["group.order"] + ''' + group_order_obj = request.env['group.order'] current_user = request.env.user # Validate that the user has a partner_id if not current_user.partner_id: - _logger.error("eskaera_list: User %d has no partner_id", current_user.id) - return request.redirect("/web") + _logger.error('eskaera_list: User %d has no partner_id', current_user.id) + return request.redirect('/web') # Obtener pedidos activos para esta semana (ya filtrados por company_id via record rule) active_orders = group_order_obj.get_active_orders_for_week() - _logger.info("=== ESKAERA LIST ===") - _logger.info("User: %s (ID: %d)", current_user.name, current_user.id) - _logger.info("User company: %s", current_user.company_id.name) - _logger.info( - "Active orders from get_active_orders_for_week: %s", - active_orders.mapped("name"), - ) + _logger.info('=== ESKAERA LIST ===') + _logger.info('User: %s (ID: %d)', current_user.name, current_user.id) + _logger.info('User company: %s', current_user.company_id.name) + _logger.info('Active orders from get_active_orders_for_week: %s', active_orders.mapped('name')) - return request.render( - "website_sale_aplicoop.eskaera_page", - { - "active_orders": active_orders, - "day_names": self._get_day_names(env=request.env), - }, - ) + return request.render('website_sale_aplicoop.eskaera_page', { + 'active_orders': active_orders, + 'day_names': self._get_day_names(env=request.env), + }) def _filter_published_tags(self, tags): - """Filter tags to only include those visible on ecommerce.""" - return tags.filtered(lambda t: getattr(t, "visible_on_ecommerce", True)) + '''Filter tags to only include those visible on ecommerce.''' + return tags.filtered(lambda t: getattr(t, 'visible_on_ecommerce', True)) - @http.route(["/eskaera/"], type="http", auth="user", website=True) + @http.route(['/eskaera/'], type='http', auth='user', + website=True) def eskaera_shop(self, order_id, **post): - """Página de tienda para un pedido específico (eskaera). + '''Página de tienda para un pedido específico (eskaera). Muestra productos del pedido y gestiona el carrito separado. Soporta búsqueda y filtrado por categoría. - """ - group_order = request.env["group.order"].browse(order_id) + ''' + group_order = request.env['group.order'].browse(order_id) current_user = request.env.user if not group_order.exists(): - return request.redirect("/eskaera") + return request.redirect('/eskaera') # Verificar que el pedido está activo - if group_order.state != "open": - return request.redirect("/eskaera") + if group_order.state != 'open': + return request.redirect('/eskaera') # Seguridad: record rule controla acceso por company_id # No additional group validation needed # Print order cutoff date information - _logger.info("=== ESKAERA SHOP ===") - _logger.info("Order: %s (ID: %d)", group_order.name, group_order.id) - _logger.info("Cutoff Day: %s (0=Monday, 6=Sunday)", group_order.cutoff_day) - _logger.info("Pickup Day: %s", group_order.pickup_day) + _logger.info('=== ESKAERA SHOP ===') + _logger.info('Order: %s (ID: %d)', group_order.name, group_order.id) + _logger.info('Cutoff Day: %s (0=Monday, 6=Sunday)', group_order.cutoff_day) + _logger.info('Pickup Day: %s', group_order.pickup_day) if group_order.start_date: - _logger.info("Start Date: %s", group_order.start_date.strftime("%Y-%m-%d")) + _logger.info('Start Date: %s', group_order.start_date.strftime('%Y-%m-%d')) if group_order.end_date: - _logger.info("End Date: %s", group_order.end_date.strftime("%Y-%m-%d")) + _logger.info('End Date: %s', group_order.end_date.strftime('%Y-%m-%d')) # Collect products from all configured associations: # - Explicit products attached to the group order @@ -387,161 +357,119 @@ class AplicoopWebsiteSale(WebsiteSale): # - Products provided by the selected suppliers # - Delegate discovery to the order model (centralised logic) products = group_order._get_products_for_group_order(group_order.id) - _logger.info( - "eskaera_shop order_id=%d, total products=%d (discovered)", - order_id, - len(products), - ) + _logger.info('eskaera_shop order_id=%d, total products=%d (discovered)', order_id, len(products)) # Get all available categories BEFORE filtering (so dropdown always shows all) # Include not only product categories but also their parent categories - product_categories = products.mapped("categ_id").filtered(lambda c: c.id > 0) - + product_categories = products.mapped('categ_id').filtered(lambda c: c.id > 0) + # Collect all categories including parent chain all_categories_set = set() - def collect_category_and_parents(category): """Recursively collect category and all its parent categories.""" if category and category.id > 0: all_categories_set.add(category.id) if category.parent_id: collect_category_and_parents(category.parent_id) - + for cat in product_categories: collect_category_and_parents(cat) - + # Convert IDs back to recordset, filtering out id=0 - available_categories = request.env["product.category"].browse( - list(all_categories_set) - ) + available_categories = request.env['product.category'].browse(list(all_categories_set)) available_categories = sorted(set(available_categories), key=lambda c: c.name) - + # Build hierarchical category structure with parent/child relationships category_hierarchy = self._build_category_hierarchy(available_categories) # Get search and filter parameters - search_query = post.get("search", "").strip() - category_filter = post.get("category", "0") + search_query = post.get('search', '').strip() + category_filter = post.get('category', '0') # Apply search if search_query: - products = products.filtered( - lambda p: search_query.lower() in p.name.lower() - or search_query.lower() in (p.description or "").lower() - ) - _logger.info( - 'eskaera_shop: Filtered by search "%s". Found %d', - search_query, - len(products), - ) + products = products.filtered(lambda p: search_query.lower() in p.name.lower() or + search_query.lower() in (p.description or '').lower()) + _logger.info('eskaera_shop: Filtered by search "%s". Found %d', search_query, len(products)) # Apply category filter - if category_filter != "0": + if category_filter != '0': try: category_id = int(category_filter) # Get the selected category - selected_category = request.env["product.category"].browse(category_id) - + selected_category = request.env['product.category'].browse(category_id) + if selected_category.exists(): # Get all descendant categories (children, grandchildren, etc.) all_category_ids = [category_id] - def get_all_children(category): for child in category.child_id: all_category_ids.append(child.id) get_all_children(child) - + get_all_children(selected_category) - + # Search for products in the selected category and all descendants # This ensures we get products even if the category is a parent with no direct products - filtered_products = request.env["product.product"].search( - [ - ("categ_id", "in", all_category_ids), - ("active", "=", True), - ("product_tmpl_id.is_published", "=", True), - ("product_tmpl_id.sale_ok", "=", True), - ] - ) - + filtered_products = request.env['product.product'].search([ + ('categ_id', 'in', all_category_ids), + ('active', '=', True), + ('product_tmpl_id.is_published', '=', True), + ('product_tmpl_id.sale_ok', '=', True), + ]) + # Filter to only include products from the order's permitted categories # Get order's permitted category IDs (including descendants) if group_order.category_ids: order_cat_ids = [] - def get_order_descendants(categories): for cat in categories: order_cat_ids.append(cat.id) if cat.child_id: get_order_descendants(cat.child_id) - + get_order_descendants(group_order.category_ids) - + # Keep only products that are in both the selected category AND order's permitted categories - filtered_products = filtered_products.filtered( - lambda p: p.categ_id.id in order_cat_ids - ) - + filtered_products = filtered_products.filtered(lambda p: p.categ_id.id in order_cat_ids) + products = filtered_products - _logger.info( - "eskaera_shop: Filtered by category %d and descendants. Found %d products", - category_id, - len(products), - ) + _logger.info('eskaera_shop: Filtered by category %d and descendants. Found %d products', category_id, len(products)) except (ValueError, TypeError): pass # Prepare supplier info dict: {product.id: 'Supplier (City)'} product_supplier_info = {} for product in products: - supplier_name = "" + supplier_name = '' if product.seller_ids: partner = product.seller_ids[0].partner_id.sudo() - supplier_name = partner.name or "" + supplier_name = partner.name or '' if partner.city: supplier_name += f" ({partner.city})" product_supplier_info[product.id] = supplier_name # Get pricelist and calculate prices with taxes using Odoo's pricelist system - _logger.info("eskaera_shop: Starting price calculation for order %d", order_id) + _logger.info('eskaera_shop: Starting price calculation for order %d', order_id) try: pricelist = request.website._get_current_pricelist() - _logger.info( - "eskaera_shop: Pricelist obtained from website: %s (id=%s, currency=%s)", - pricelist.name if pricelist else "None", - pricelist.id if pricelist else "None", - ( - pricelist.currency_id.name - if pricelist and pricelist.currency_id - else "None" - ), - ) + _logger.info('eskaera_shop: Pricelist obtained from website: %s (id=%s, currency=%s)', + pricelist.name if pricelist else 'None', + pricelist.id if pricelist else 'None', + pricelist.currency_id.name if pricelist and pricelist.currency_id else 'None') except Exception as e: - _logger.warning( - "eskaera_shop: Error getting pricelist from website: %s. Trying default pricelist.", - str(e), - ) - pricelist = request.env["product.pricelist"].search( - [("active", "=", True)], limit=1 - ) + _logger.warning('eskaera_shop: Error getting pricelist from website: %s. Trying default pricelist.', str(e)) + pricelist = request.env['product.pricelist'].search([('active', '=', True)], limit=1) if pricelist: - _logger.info( - "eskaera_shop: Default pricelist found: %s (id=%s)", - pricelist.name, - pricelist.id, - ) - + _logger.info('eskaera_shop: Default pricelist found: %s (id=%s)', pricelist.name, pricelist.id) + if not pricelist: - _logger.error( - "eskaera_shop: ERROR - No pricelist found! All prices will use list_price as fallback." - ) - + _logger.error('eskaera_shop: ERROR - No pricelist found! All prices will use list_price as fallback.') + product_price_info = {} for product in products: # Get combination info with taxes calculated using OCA product_get_price_helper - product_variant = ( - product.product_variant_ids[0] if product.product_variant_ids else False - ) + product_variant = product.product_variant_ids[0] if product.product_variant_ids else False if product_variant and pricelist: try: # Use OCA _get_price method - more robust and complete @@ -550,58 +478,42 @@ class AplicoopWebsiteSale(WebsiteSale): pricelist=pricelist, fposition=request.website.fiscal_position_id, ) - price = price_info.get("value", 0.0) - original_price = price_info.get("original_value", 0.0) - discount = price_info.get("discount", 0.0) + 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), + 'price': price, # Price with taxes + 'list_price': original_price, # Original price before discount + 'has_discounted_price': has_discount, + 'discount': discount, # Discount percentage + 'tax_included': price_info.get('tax_included', True), } - _logger.debug( - "eskaera_shop: Product %s (id=%s) - price=%.2f, original=%.2f, discount=%.1f%%, tax_included=%s", - product.name, - product.id, - price, - original_price, - discount, - price_info.get("tax_included"), - ) + _logger.debug('eskaera_shop: Product %s (id=%s) - price=%.2f, original=%.2f, discount=%.1f%%, tax_included=%s', + product.name, product.id, price, original_price, discount, price_info.get('tax_included')) except Exception as e: - _logger.warning( - "eskaera_shop: Error getting price for product %s (id=%s): %s. Using list_price fallback.", - product.name, - product.id, - str(e), - ) + _logger.warning('eskaera_shop: Error getting price for product %s (id=%s): %s. Using list_price fallback.', + product.name, product.id, str(e)) # Fallback to list_price if _get_price fails product_price_info[product.id] = { - "price": product.list_price, - "list_price": product.list_price, - "has_discounted_price": False, - "discount": 0.0, - "tax_included": False, + 'price': product.list_price, + 'list_price': product.list_price, + 'has_discounted_price': False, + 'discount': 0.0, + 'tax_included': False, } else: # Fallback if no variant or no pricelist - reason = "no pricelist" if not pricelist else "no variant" - _logger.info( - "eskaera_shop: Product %s (id=%s) - Using list_price fallback (reason: %s). Price=%.2f", - product.name, - product.id, - reason, - product.list_price, - ) + reason = 'no pricelist' if not pricelist else 'no variant' + _logger.info('eskaera_shop: Product %s (id=%s) - Using list_price fallback (reason: %s). Price=%.2f', + product.name, product.id, reason, product.list_price) product_price_info[product.id] = { - "price": product.list_price, - "list_price": product.list_price, - "has_discounted_price": False, - "discount": 0.0, - "tax_included": False, + 'price': product.list_price, + 'list_price': product.list_price, + 'has_discounted_price': False, + 'discount': 0.0, + 'tax_included': False, } # Calculate available tags with product count (only show tags that are actually used and visible) @@ -610,40 +522,28 @@ class AplicoopWebsiteSale(WebsiteSale): for product in products: for tag in product.product_tag_ids: # Only include tags that are visible on ecommerce - is_visible = getattr( - tag, "visible_on_ecommerce", True - ) # Default to True if field doesn't exist + is_visible = getattr(tag, 'visible_on_ecommerce', True) # Default to True if field doesn't exist if not is_visible: continue - + if tag.id not in available_tags_dict: tag_color = tag.color if tag.color else None - _logger.info( - "Tag %s (id=%s): color=%s (type=%s)", - tag.name, - tag.id, - tag_color, - type(tag_color), - ) + _logger.info('Tag %s (id=%s): color=%s (type=%s)', tag.name, tag.id, tag_color, type(tag_color)) available_tags_dict[tag.id] = { - "id": tag.id, - "name": tag.name, - "color": tag_color, # Use tag color (hex) or None for theme color - "count": 0, + 'id': tag.id, + 'name': tag.name, + 'color': tag_color, # Use tag color (hex) or None for theme color + 'count': 0 } - available_tags_dict[tag.id]["count"] += 1 - + available_tags_dict[tag.id]['count'] += 1 + # Convert to sorted list of tags (sorted by name for consistent display) - available_tags = sorted(available_tags_dict.values(), key=lambda t: t["name"]) - _logger.info( - "eskaera_shop: Found %d available tags for %d products", - len(available_tags), - len(products), - ) - _logger.info("eskaera_shop: available_tags = %s", available_tags) + available_tags = sorted(available_tags_dict.values(), key=lambda t: t['name']) + _logger.info('eskaera_shop: Found %d available tags for %d products', len(available_tags), len(products)) + _logger.info('eskaera_shop: available_tags = %s', available_tags) # Manage session for separate cart per order - session_key = f"eskaera_{order_id}" + session_key = 'eskaera_{}'.format(order_id) cart = request.session.get(session_key, {}) # Get translated labels for JavaScript (same as checkout) @@ -655,140 +555,91 @@ class AplicoopWebsiteSale(WebsiteSale): for product in products: published_tags = self._filter_published_tags(product.product_tag_ids) filtered_products[product.id] = { - "product": product, - "published_tags": published_tags, + 'product': product, + 'published_tags': published_tags } - return request.render( - "website_sale_aplicoop.eskaera_shop", - { - "group_order": group_order, - "products": products, - "filtered_product_tags": filtered_products, - "cart": cart, - "available_categories": available_categories, - "category_hierarchy": category_hierarchy, - "available_tags": available_tags, - "search_query": search_query, - "selected_category": category_filter, - "day_names": self._get_day_names(env=request.env), - "product_supplier_info": product_supplier_info, - "product_price_info": product_price_info, - "labels": labels, - "labels_json": json.dumps(labels, ensure_ascii=False), - }, - ) + return request.render('website_sale_aplicoop.eskaera_shop', { + 'group_order': group_order, + 'products': products, + 'filtered_product_tags': filtered_products, + 'cart': cart, + 'available_categories': available_categories, + 'category_hierarchy': category_hierarchy, + 'available_tags': available_tags, + 'search_query': search_query, + 'selected_category': category_filter, + 'day_names': self._get_day_names(env=request.env), + 'product_supplier_info': product_supplier_info, + 'product_price_info': product_price_info, + 'labels': labels, + 'labels_json': json.dumps(labels, ensure_ascii=False), + }) - @http.route( - ["/eskaera/add-to-cart"], - type="http", - auth="user", - website=True, - methods=["POST"], - csrf=False, - ) + @http.route(['/eskaera/add-to-cart'], type='http', auth='user', + website=True, methods=['POST'], csrf=False) def add_to_eskaera_cart(self, **post): - """Validate and confirm product addition to cart. + '''Validate and confirm product addition to cart. The cart is managed in localStorage on the frontend. This endpoint only validates that the product exists in the order. - """ + ''' import json - try: # Get JSON data from the request body - data = ( - json.loads(request.httprequest.data) if request.httprequest.data else {} - ) + data = json.loads(request.httprequest.data) if request.httprequest.data else {} - order_id = int(data.get("order_id", 0)) - product_id = int(data.get("product_id", 0)) - quantity = float(data.get("quantity", 1)) + order_id = int(data.get('order_id', 0)) + product_id = int(data.get('product_id', 0)) + quantity = float(data.get('quantity', 1)) - group_order = request.env["group.order"].browse(order_id) - product = request.env["product.product"].browse(product_id) + group_order = request.env['group.order'].browse(order_id) + product = request.env['product.product'].browse(product_id) # Validate that the order exists and is open - if not group_order.exists() or group_order.state != "open": - _logger.warning( - "add_to_eskaera_cart: Order %d not available (exists=%s, state=%s)", - order_id, - group_order.exists(), - group_order.state if group_order.exists() else "N/A", - ) + if not group_order.exists() or group_order.state != 'open': + _logger.warning('add_to_eskaera_cart: Order %d not available (exists=%s, state=%s)', + order_id, group_order.exists(), group_order.state if group_order.exists() else 'N/A') return request.make_response( - json.dumps({"error": "Order is not available"}), - [("Content-Type", "application/json")], - ) + json.dumps({'error': 'Order is not available'}), + [('Content-Type', 'application/json')]) # Validate that the product is available in this order (use discovery logic) - available_products = group_order._get_products_for_group_order( - group_order.id - ) + available_products = group_order._get_products_for_group_order(group_order.id) if product not in available_products: - _logger.warning( - "add_to_eskaera_cart: Product %d not available in order %d", - product_id, - order_id, - ) + _logger.warning('add_to_eskaera_cart: Product %d not available in order %d', product_id, order_id) return request.make_response( - json.dumps({"error": "Product not available in this order"}), - [("Content-Type", "application/json")], - ) + json.dumps({'error': 'Product not available in this order'}), + [('Content-Type', 'application/json')]) # Validate quantity if quantity <= 0: return request.make_response( - json.dumps({"error": "Quantity must be greater than 0"}), - [("Content-Type", "application/json")], - ) + json.dumps({'error': 'Quantity must be greater than 0'}), + [('Content-Type', 'application/json')]) - _logger.info( - "add_to_eskaera_cart: Added product %d (qty=%f) to order %d", - product_id, - quantity, - order_id, - ) + _logger.info('add_to_eskaera_cart: Added product %d (qty=%f) to order %d', + product_id, quantity, order_id) # Get price with taxes using pricelist - _logger.info( - "add_to_eskaera_cart: Getting price for product %s (id=%s)", - product.name, - product_id, - ) + _logger.info('add_to_eskaera_cart: Getting price for product %s (id=%s)', product.name, product_id) try: pricelist = request.website._get_current_pricelist() - _logger.info( - "add_to_eskaera_cart: Pricelist: %s (id=%s)", - pricelist.name if pricelist else "None", - pricelist.id if pricelist else "None", - ) + _logger.info('add_to_eskaera_cart: Pricelist: %s (id=%s)', + pricelist.name if pricelist else 'None', + pricelist.id if pricelist else 'None') except Exception as e: - _logger.warning( - "add_to_eskaera_cart: Error getting pricelist: %s. Trying default.", - str(e), - ) - pricelist = request.env["product.pricelist"].search( - [("active", "=", True)], limit=1 - ) + _logger.warning('add_to_eskaera_cart: Error getting pricelist: %s. Trying default.', str(e)) + pricelist = request.env['product.pricelist'].search([('active', '=', True)], limit=1) if pricelist: - _logger.info( - "add_to_eskaera_cart: Default pricelist found: %s (id=%s)", - pricelist.name, - pricelist.id, - ) - + _logger.info('add_to_eskaera_cart: Default pricelist found: %s (id=%s)', pricelist.name, pricelist.id) + if not pricelist: - _logger.error( - "add_to_eskaera_cart: ERROR - No pricelist found! Using list_price for product %s", - product.name, - ) - - product_variant = ( - product.product_variant_ids[0] if product.product_variant_ids else False - ) - base_price = product.list_price # Fallback - + _logger.error('add_to_eskaera_cart: ERROR - No pricelist found! Using list_price for product %s', product.name) + + product_variant = product.product_variant_ids[0] if product.product_variant_ids else False + price_with_tax = product.list_price # Fallback + if product_variant and pricelist: try: # Use OCA _get_price method - more robust and complete @@ -797,410 +648,322 @@ class AplicoopWebsiteSale(WebsiteSale): pricelist=pricelist, fposition=request.website.fiscal_position_id, ) - price_with_tax = price_info.get("value", product.list_price) - _logger.info( - "add_to_eskaera_cart: Product %s - Price: %.2f (original: %.2f, discount: %.1f%%)", - product.name, - price_with_tax, - price_info.get("original_value", 0), - price_info.get("discount", 0), - ) + price_with_tax = price_info.get('value', product.list_price) + _logger.info('add_to_eskaera_cart: Product %s - Calculated price with taxes: %.2f (original: %.2f, discount: %.1f%%)', + product.name, price_with_tax, price_info.get('original_value', 0), price_info.get('discount', 0)) except Exception as e: - _logger.warning( - "add_to_eskaera_cart: Error getting price for product %s: %s. Using list_price=%.2f", - product.name, - str(e), - product.list_price, - ) + _logger.warning('add_to_eskaera_cart: Error getting price for product %s: %s. Using list_price=%.2f', + product.name, str(e), product.list_price) else: - reason = "no pricelist" if not pricelist else "no variant" - _logger.info( - "add_to_eskaera_cart: Product %s - Using list_price fallback (reason: %s). Price=%.2f", - product.name, - reason, - price_with_tax, - ) + reason = 'no pricelist' if not pricelist else 'no variant' + _logger.info('add_to_eskaera_cart: Product %s - Using list_price fallback (reason: %s). Price=%.2f', + product.name, reason, price_with_tax) response_data = { - "success": True, - "message": f'{_("%s added to cart") % product.name}', - "product_id": product_id, - "quantity": quantity, - "price": price_with_tax, + 'success': True, + 'message': f'{_("%s added to cart") % product.name}', + 'product_id': product_id, + 'quantity': quantity, + 'price': price_with_tax, } return request.make_response( - json.dumps(response_data), [("Content-Type", "application/json")] - ) + json.dumps(response_data), + [('Content-Type', 'application/json')]) except ValueError as e: - _logger.error("add_to_eskaera_cart: ValueError: %s", str(e)) + _logger.error('add_to_eskaera_cart: ValueError: %s', str(e)) return request.make_response( - json.dumps({"error": f"Invalid parameters: {str(e)}"}), - [("Content-Type", "application/json")], - ) + json.dumps({'error': f'Invalid parameters: {str(e)}'}), + [('Content-Type', 'application/json')]) except Exception as e: - _logger.error("add_to_eskaera_cart: Exception: %s", str(e), exc_info=True) - return request.make_response(json.dumps({"error": f"Error: {str(e)}"})) + _logger.error('add_to_eskaera_cart: Exception: %s', str(e), exc_info=True) + return request.make_response(json.dumps({'error': f'Error: {str(e)}'})) - @http.route( - ["/eskaera//checkout"], type="http", auth="user", website=True - ) + @http.route(['/eskaera//checkout'], type='http', auth='user', + website=True) def eskaera_checkout(self, order_id, **post): - """Checkout page to close the cart for the order (eskaera).""" - group_order = request.env["group.order"].browse(order_id) + '''Checkout page to close the cart for the order (eskaera).''' + group_order = request.env['group.order'].browse(order_id) if not group_order.exists(): - return request.redirect("/eskaera") + return request.redirect('/eskaera') # Verificar que el pedido está activo - if group_order.state != "open": - return request.redirect("/eskaera") + if group_order.state != 'open': + return request.redirect('/eskaera') # Los datos del carrito vienen desde localStorage en el frontend # Esta página solo muestra resumen y botón de confirmación - + # DEBUG: Log ALL delivery fields - _logger.warning("=== ESKAERA_CHECKOUT DELIVERY DEBUG ===") - _logger.warning("group_order.id: %s", group_order.id) - _logger.warning("group_order.name: %s", group_order.name) - _logger.warning( - "group_order.pickup_day: %s (type: %s)", - group_order.pickup_day, - type(group_order.pickup_day), - ) - _logger.warning( - "group_order.pickup_date: %s (type: %s)", - group_order.pickup_date, - type(group_order.pickup_date), - ) - _logger.warning( - "group_order.delivery_date: %s (type: %s)", - group_order.delivery_date, - type(group_order.delivery_date), - ) - _logger.warning("group_order.home_delivery: %s", group_order.home_delivery) - _logger.warning("group_order.delivery_notice: %s", group_order.delivery_notice) + _logger.warning('=== ESKAERA_CHECKOUT DELIVERY DEBUG ===') + _logger.warning('group_order.id: %s', group_order.id) + _logger.warning('group_order.name: %s', group_order.name) + _logger.warning('group_order.pickup_day: %s (type: %s)', group_order.pickup_day, type(group_order.pickup_day)) + _logger.warning('group_order.pickup_date: %s (type: %s)', group_order.pickup_date, type(group_order.pickup_date)) + _logger.warning('group_order.delivery_date: %s (type: %s)', group_order.delivery_date, type(group_order.delivery_date)) + _logger.warning('group_order.home_delivery: %s', group_order.home_delivery) + _logger.warning('group_order.delivery_notice: %s', group_order.delivery_notice) if group_order.pickup_date: - _logger.warning( - "pickup_date formatted: %s", - group_order.pickup_date.strftime("%d/%m/%Y"), - ) - _logger.warning("========================================") - + _logger.warning('pickup_date formatted: %s', group_order.pickup_date.strftime('%d/%m/%Y')) + _logger.warning('========================================') + # Get delivery product ID and name (translated to user's language) - delivery_product = request.env.ref( - "website_sale_aplicoop.product_home_delivery", raise_if_not_found=False - ) + delivery_product = request.env.ref('website_sale_aplicoop.product_home_delivery', raise_if_not_found=False) delivery_product_id = delivery_product.id if delivery_product else None # Get translated product name based on current language if delivery_product: - delivery_product_translated = delivery_product.with_context( - lang=request.env.lang - ) + delivery_product_translated = delivery_product.with_context(lang=request.env.lang) delivery_product_name = delivery_product_translated.name else: - delivery_product_name = "Home Delivery" - + delivery_product_name = 'Home Delivery' + # Get all translated labels for JavaScript (same as shop page) # This includes all 37 labels: modal labels, confirmation, notifications, cart buttons, etc. labels = self.get_checkout_labels() - + # Convert to JSON string for safe embedding in script tag labels_json = json.dumps(labels, ensure_ascii=False) - + # Prepare template context with explicit debug info template_context = { - "group_order": group_order, - "day_names": self._get_day_names(env=request.env), - "delivery_product_id": delivery_product_id, - "delivery_product_name": delivery_product_name, # Auto-translated to user's language - "delivery_product_price": ( - delivery_product.list_price if delivery_product else 5.74 - ), - "labels": labels, - "labels_json": labels_json, + 'group_order': group_order, + 'day_names': self._get_day_names(env=request.env), + 'delivery_product_id': delivery_product_id, + 'delivery_product_name': delivery_product_name, # Auto-translated to user's language + 'delivery_product_price': delivery_product.list_price if delivery_product else 5.74, + 'labels': labels, + 'labels_json': labels_json, } + + _logger.warning('Template context keys: %s', list(template_context.keys())) + + return request.render('website_sale_aplicoop.eskaera_checkout', template_context) - _logger.warning("Template context keys: %s", list(template_context.keys())) - - return request.render( - "website_sale_aplicoop.eskaera_checkout", template_context - ) - - @http.route( - ["/eskaera/save-cart"], - type="http", - auth="user", - website=True, - methods=["POST"], - csrf=False, - ) + @http.route(['/eskaera/save-cart'], type='http', auth='user', website=True, + methods=['POST'], csrf=False) def save_cart_draft(self, **post): - """Save cart items as a draft sale.order with pickup date.""" + '''Save cart items as a draft sale.order with pickup date.''' import json try: - _logger.warning("=== SAVE_CART_DRAFT CALLED ===") - + _logger.warning('=== SAVE_CART_DRAFT CALLED ===') + if not request.httprequest.data: return request.make_response( - json.dumps({"error": "No data provided"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'No data provided'}), + [('Content-Type', 'application/json')], + status=400) # Decode JSON try: raw_data = request.httprequest.data if isinstance(raw_data, bytes): - raw_data = raw_data.decode("utf-8") + raw_data = raw_data.decode('utf-8') data = json.loads(raw_data) except Exception as e: - _logger.error("Error decoding JSON: %s", str(e)) + _logger.error('Error decoding JSON: %s', str(e)) return request.make_response( - json.dumps({"error": f"Invalid JSON: {str(e)}"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': f'Invalid JSON: {str(e)}'}), + [('Content-Type', 'application/json')], + status=400) - _logger.info("save_cart_draft data received: %s", data) + _logger.info('save_cart_draft data received: %s', data) # Validate order_id - order_id = data.get("order_id") + order_id = data.get('order_id') if not order_id: return request.make_response( - json.dumps({"error": "order_id is required"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'order_id is required'}), + [('Content-Type', 'application/json')], + status=400) try: order_id = int(order_id) except (ValueError, TypeError): return request.make_response( - json.dumps({"error": f"Invalid order_id format: {order_id}"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': f'Invalid order_id format: {order_id}'}), + [('Content-Type', 'application/json')], + status=400) # Verify that the order exists - group_order = request.env["group.order"].browse(order_id) + group_order = request.env['group.order'].browse(order_id) if not group_order.exists(): return request.make_response( - json.dumps({"error": f"Order {order_id} not found"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': f'Order {order_id} not found'}), + [('Content-Type', 'application/json')], + status=400) current_user = request.env.user if not current_user.partner_id: return request.make_response( - json.dumps({"error": "User has no associated partner"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'User has no associated partner'}), + [('Content-Type', 'application/json')], + status=400) # Get cart items and pickup date - items = data.get("items", []) - pickup_date = data.get("pickup_date") # Date from group_order - is_delivery = data.get("is_delivery", False) # If home delivery selected - + items = data.get('items', []) + pickup_date = data.get('pickup_date') # Date from group_order + is_delivery = data.get('is_delivery', False) # If home delivery selected + if not items: return request.make_response( - json.dumps({"error": "No items in cart"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'No items in cart'}), + [('Content-Type', 'application/json')], + status=400) - _logger.info( - "Creating draft sale.order with %d items for partner %d", - len(items), - current_user.partner_id.id, - ) + _logger.info('Creating draft sale.order with %d items for partner %d', len(items), current_user.partner_id.id) # Create sales.order lines from items sale_order_lines = [] 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_id = int(item.get('product_id')) + quantity = float(item.get('quantity', 1)) + price = float(item.get('product_price', 0)) - product = request.env["product.product"].browse(product_id) + product = request.env['product.product'].browse(product_id) if not product.exists(): - _logger.warning( - "save_cart_draft: Product %d does not exist", product_id - ) + _logger.warning('save_cart_draft: Product %d does not exist', product_id) continue - line = ( - 0, - 0, - { - "product_id": product_id, - "product_uom_qty": quantity, - "price_unit": price, - }, - ) + line = (0, 0, { + 'product_id': product_id, + 'product_uom_qty': quantity, + 'price_unit': price, + }) sale_order_lines.append(line) except Exception as e: - _logger.error("Error processing item %s: %s", item, str(e)) + _logger.error('Error processing item %s: %s', item, str(e)) if not sale_order_lines: return request.make_response( - json.dumps({"error": "No valid items to save"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'No valid items to save'}), + [('Content-Type', 'application/json')], + status=400) # Create order values dict order_vals = { - "partner_id": current_user.partner_id.id, - "order_line": sale_order_lines, - "state": "draft", - "group_order_id": order_id, # Link to the group.order + 'partner_id': current_user.partner_id.id, + 'order_line': sale_order_lines, + 'state': 'draft', + 'group_order_id': order_id, # Link to the group.order } - + # Propagate fields from group order (ensure they exist) if group_order.pickup_day: - order_vals["pickup_day"] = group_order.pickup_day - _logger.info("Set pickup_day: %s", group_order.pickup_day) - + order_vals['pickup_day'] = group_order.pickup_day + _logger.info('Set pickup_day: %s', group_order.pickup_day) + if group_order.pickup_date: - order_vals["pickup_date"] = group_order.pickup_date - _logger.info("Set pickup_date: %s", group_order.pickup_date) - + order_vals['pickup_date'] = group_order.pickup_date + _logger.info('Set pickup_date: %s', group_order.pickup_date) + if group_order.home_delivery: - order_vals["home_delivery"] = group_order.home_delivery - _logger.info("Set home_delivery: %s", group_order.home_delivery) - + order_vals['home_delivery'] = group_order.home_delivery + _logger.info('Set home_delivery: %s', group_order.home_delivery) + # Add commitment date (pickup/delivery date) if provided if pickup_date: - order_vals["commitment_date"] = pickup_date + order_vals['commitment_date'] = pickup_date elif group_order.pickup_date: # Fallback to group order pickup date - order_vals["commitment_date"] = group_order.pickup_date - _logger.info( - "Set commitment_date from group_order.pickup_date: %s", - group_order.pickup_date, - ) + order_vals['commitment_date'] = group_order.pickup_date + _logger.info('Set commitment_date from group_order.pickup_date: %s', group_order.pickup_date) - _logger.info("Creating sale.order with values: %s", order_vals) + _logger.info('Creating sale.order with values: %s', order_vals) # Create the sale.order - sale_order = request.env["sale.order"].create(order_vals) - + sale_order = request.env['sale.order'].create(order_vals) + # Ensure the order has a name (draft orders may not have one yet) - if not sale_order.name or sale_order.name == "New": + if not sale_order.name or sale_order.name == 'New': # Force sequence generation for draft order sale_order._onchange_partner_id() # This may trigger name generation - if not sale_order.name or sale_order.name == "New": + if not sale_order.name or sale_order.name == 'New': # If still no name, use a temporary one - sale_order.name = "DRAFT-%s" % sale_order.id + sale_order.name = 'DRAFT-%s' % sale_order.id - _logger.info( - "Draft sale.order created: %d (name: %s) for partner %d with group_order_id=%s, pickup_day=%s, pickup_date=%s", - sale_order.id, - sale_order.name, - current_user.partner_id.id, - sale_order.group_order_id.id if sale_order.group_order_id else None, - sale_order.pickup_day, - sale_order.pickup_date, - ) + _logger.info('Draft sale.order created: %d (name: %s) for partner %d with group_order_id=%s, pickup_day=%s, pickup_date=%s', + sale_order.id, sale_order.name, current_user.partner_id.id, + sale_order.group_order_id.id if sale_order.group_order_id else None, + sale_order.pickup_day, sale_order.pickup_date) return request.make_response( - json.dumps( - { - "success": True, - "message": _("Cart saved as draft"), - "sale_order_id": sale_order.id, - } - ), - [("Content-Type", "application/json")], - ) + json.dumps({ + 'success': True, + 'message': _('Cart saved as draft'), + 'sale_order_id': sale_order.id, + }), + [('Content-Type', 'application/json')]) except Exception as e: import traceback - - _logger.error("save_cart_draft: Unexpected error: %s", str(e)) + _logger.error('save_cart_draft: Unexpected error: %s', str(e)) _logger.error(traceback.format_exc()) return request.make_response( - json.dumps({"error": str(e)}), - [("Content-Type", "application/json")], - status=500, - ) + json.dumps({'error': str(e)}), + [('Content-Type', 'application/json')], + status=500) - @http.route( - ["/eskaera/load-draft"], - type="http", - auth="user", - website=True, - methods=["POST"], - csrf=False, - ) + @http.route(['/eskaera/load-draft'], type='http', auth='user', website=True, + methods=['POST'], csrf=False) def load_draft_cart(self, **post): - """Load items from the most recent draft sale.order for this week.""" + '''Load items from the most recent draft sale.order for this week.''' import json - from datetime import datetime - from datetime import timedelta + from datetime import datetime, timedelta try: - _logger.warning("=== LOAD_DRAFT_CART CALLED ===") - + _logger.warning('=== LOAD_DRAFT_CART CALLED ===') + if not request.httprequest.data: return request.make_response( - json.dumps({"error": "No data provided"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'No data provided'}), + [('Content-Type', 'application/json')], + status=400) # Decode JSON try: raw_data = request.httprequest.data if isinstance(raw_data, bytes): - raw_data = raw_data.decode("utf-8") + raw_data = raw_data.decode('utf-8') data = json.loads(raw_data) except Exception as e: - _logger.error("Error decoding JSON: %s", str(e)) + _logger.error('Error decoding JSON: %s', str(e)) return request.make_response( - json.dumps({"error": f"Invalid JSON: {str(e)}"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': f'Invalid JSON: {str(e)}'}), + [('Content-Type', 'application/json')], + status=400) - order_id = data.get("order_id") + order_id = data.get('order_id') if not order_id: return request.make_response( - json.dumps({"error": "order_id is required"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'order_id is required'}), + [('Content-Type', 'application/json')], + status=400) try: order_id = int(order_id) except (ValueError, TypeError): return request.make_response( - json.dumps({"error": f"Invalid order_id format: {order_id}"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': f'Invalid order_id format: {order_id}'}), + [('Content-Type', 'application/json')], + status=400) - group_order = request.env["group.order"].browse(order_id) + group_order = request.env['group.order'].browse(order_id) if not group_order.exists(): return request.make_response( - json.dumps({"error": f"Order {order_id} not found"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': f'Order {order_id} not found'}), + [('Content-Type', 'application/json')], + status=400) current_user = request.env.user if not current_user.partner_id: return request.make_response( - json.dumps({"error": "User has no associated partner"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'User has no associated partner'}), + [('Content-Type', 'application/json')], + status=400) # Find the most recent draft sale.order for this partner from this week # Get start of current week (Monday) @@ -1208,582 +971,440 @@ class AplicoopWebsiteSale(WebsiteSale): start_of_week = today - timedelta(days=today.weekday()) end_of_week = start_of_week + timedelta(days=6) - _logger.info( - "Searching for draft orders between %s and %s for partner %d and group_order %d", - start_of_week, - end_of_week, - current_user.partner_id.id, - order_id, - ) + _logger.info('Searching for draft orders between %s and %s for partner %d and group_order %d', + start_of_week, end_of_week, current_user.partner_id.id, order_id) # Debug: Check all draft orders for this user - all_drafts = request.env["sale.order"].search( - [ - ("partner_id", "=", current_user.partner_id.id), - ("state", "=", "draft"), - ] - ) - _logger.info( - "DEBUG: Found %d total draft orders for partner %d:", - len(all_drafts), - current_user.partner_id.id, - ) + all_drafts = request.env['sale.order'].search([ + ('partner_id', '=', current_user.partner_id.id), + ('state', '=', 'draft'), + ]) + _logger.info('DEBUG: Found %d total draft orders for partner %d:', + len(all_drafts), current_user.partner_id.id) for draft in all_drafts: - _logger.info( - " - Order ID: %d, group_order_id: %s, create_date: %s", - draft.id, - draft.group_order_id.id if draft.group_order_id else "None", - draft.create_date, - ) + _logger.info(' - Order ID: %d, group_order_id: %s, create_date: %s', + draft.id, draft.group_order_id.id if draft.group_order_id else 'None', + draft.create_date) - draft_orders = request.env["sale.order"].search( - [ - ("partner_id", "=", current_user.partner_id.id), - ("group_order_id", "=", order_id), # Filter by group.order - ("state", "=", "draft"), - ("create_date", ">=", f"{start_of_week} 00:00:00"), - ("create_date", "<=", f"{end_of_week} 23:59:59"), - ], - order="create_date desc", - limit=1, - ) + draft_orders = request.env['sale.order'].search([ + ('partner_id', '=', current_user.partner_id.id), + ('group_order_id', '=', order_id), # Filter by group.order + ('state', '=', 'draft'), + ('create_date', '>=', f'{start_of_week} 00:00:00'), + ('create_date', '<=', f'{end_of_week} 23:59:59'), + ], order='create_date desc', limit=1) - _logger.info( - "DEBUG: Found %d matching draft orders with filters", len(draft_orders) - ) + _logger.info('DEBUG: Found %d matching draft orders with filters', len(draft_orders)) if not draft_orders: - error_msg = request.env._("No draft orders found for this week") + error_msg = request.env._('No draft orders found for this week') return request.make_response( - json.dumps({"error": error_msg}), - [("Content-Type", "application/json")], - status=404, - ) + json.dumps({'error': error_msg}), + [('Content-Type', 'application/json')], + status=404) draft_order = draft_orders[0] - + # Extract items from the draft order items = [] for line in draft_order.order_line: - items.append( - { - "product_id": line.product_id.id, - "product_name": line.product_id.name, - "quantity": line.product_uom_qty, - "product_price": line.price_unit, - } - ) + items.append({ + 'product_id': line.product_id.id, + 'product_name': line.product_id.name, + 'quantity': line.product_uom_qty, + 'product_price': line.price_unit, + }) - _logger.info( - "Loaded %d items from draft order %d", len(items), draft_order.id - ) + _logger.info('Loaded %d items from draft order %d', len(items), draft_order.id) return request.make_response( - json.dumps( - { - "success": True, - "message": _("Draft order loaded"), - "items": items, - "sale_order_id": draft_order.id, - "group_order_id": draft_order.group_order_id.id, - "group_order_name": draft_order.group_order_id.name, - "pickup_day": draft_order.pickup_day, - "pickup_date": ( - str(draft_order.pickup_date) - if draft_order.pickup_date - else None - ), - "home_delivery": draft_order.home_delivery, - } - ), - [("Content-Type", "application/json")], - ) + json.dumps({ + 'success': True, + 'message': _('Draft order loaded'), + 'items': items, + 'sale_order_id': draft_order.id, + 'group_order_id': draft_order.group_order_id.id, + 'group_order_name': draft_order.group_order_id.name, + 'pickup_day': draft_order.pickup_day, + 'pickup_date': str(draft_order.pickup_date) if draft_order.pickup_date else None, + 'home_delivery': draft_order.home_delivery, + }), + [('Content-Type', 'application/json')]) except Exception as e: import traceback - - _logger.error("load_draft_cart: Unexpected error: %s", str(e)) + _logger.error('load_draft_cart: Unexpected error: %s', str(e)) _logger.error(traceback.format_exc()) return request.make_response( - json.dumps({"error": str(e)}), - [("Content-Type", "application/json")], - status=500, - ) + json.dumps({'error': str(e)}), + [('Content-Type', 'application/json')], + status=500) - @http.route( - ["/eskaera/save-order"], - type="http", - auth="user", - website=True, - methods=["POST"], - csrf=False, - ) + @http.route(['/eskaera/save-order'], type='http', auth='user', website=True, + methods=['POST'], csrf=False) def save_eskaera_draft(self, **post): - """Save order as draft (without confirming). + '''Save order as draft (without confirming). Creates a sale.order from the cart items with state='draft'. If a draft already exists for this group order, prompt user for merge/replace. - """ + ''' import json try: - _logger.warning("=== SAVE_ESKAERA_DRAFT CALLED ===") - + _logger.warning('=== SAVE_ESKAERA_DRAFT CALLED ===') + if not request.httprequest.data: - _logger.warning("save_eskaera_draft: No request data provided") + _logger.warning('save_eskaera_draft: No request data provided') return request.make_response( - json.dumps({"error": "No data provided"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'No data provided'}), + [('Content-Type', 'application/json')], + status=400) # Decode JSON try: raw_data = request.httprequest.data if isinstance(raw_data, bytes): - raw_data = raw_data.decode("utf-8") + raw_data = raw_data.decode('utf-8') data = json.loads(raw_data) except Exception as e: - _logger.error("Error decoding JSON: %s", str(e)) + _logger.error('Error decoding JSON: %s', str(e)) return request.make_response( - json.dumps({"error": f"Invalid JSON: {str(e)}"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': f'Invalid JSON: {str(e)}'}), + [('Content-Type', 'application/json')], + status=400) - _logger.info("save_eskaera_draft data received: %s", data) + _logger.info('save_eskaera_draft data received: %s', data) # Validate order_id - order_id = data.get("order_id") + order_id = data.get('order_id') if not order_id: - _logger.warning("save_eskaera_draft: order_id missing") + _logger.warning('save_eskaera_draft: order_id missing') return request.make_response( - json.dumps({"error": "order_id is required"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'order_id is required'}), + [('Content-Type', 'application/json')], + status=400) # Convert to int try: order_id = int(order_id) except (ValueError, TypeError): - _logger.warning("save_eskaera_draft: Invalid order_id: %s", order_id) + _logger.warning('save_eskaera_draft: Invalid order_id: %s', order_id) return request.make_response( - json.dumps({"error": f"Invalid order_id format: {order_id}"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': f'Invalid order_id format: {order_id}'}), + [('Content-Type', 'application/json')], + status=400) # Verify that the order exists - group_order = request.env["group.order"].browse(order_id) + group_order = request.env['group.order'].browse(order_id) if not group_order.exists(): - _logger.warning("save_eskaera_draft: Order %d not found", order_id) + _logger.warning('save_eskaera_draft: Order %d not found', order_id) return request.make_response( - json.dumps({"error": f"Order {order_id} not found"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': f'Order {order_id} not found'}), + [('Content-Type', 'application/json')], + status=400) current_user = request.env.user # Validate that the user has a partner_id if not current_user.partner_id: - _logger.error( - "save_eskaera_draft: User %d has no partner_id", current_user.id - ) + _logger.error('save_eskaera_draft: User %d has no partner_id', current_user.id) return request.make_response( - json.dumps({"error": "User has no associated partner"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'User has no associated partner'}), + [('Content-Type', 'application/json')], + status=400) # 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 - + 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( - "save_eskaera_draft: No items in cart for user %d in order %d", - current_user.id, - order_id, - ) + _logger.warning('save_eskaera_draft: No items in cart for user %d in order %d', + current_user.id, order_id) return request.make_response( - json.dumps({"error": "No items in cart"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'No items in cart'}), + [('Content-Type', 'application/json')], + status=400) # Check if a draft already exists for this group order and user - existing_drafts = request.env["sale.order"].search( - [ - ("group_order_id", "=", order_id), - ("partner_id", "=", current_user.partner_id.id), - ("state", "=", "draft"), - ] - ) + existing_drafts = request.env['sale.order'].search([ + ('group_order_id', '=', order_id), + ('partner_id', '=', current_user.partner_id.id), + ('state', '=', 'draft'), + ]) # 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 - ] - + 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": _("A draft already exists for this week."), - } - ), - [("Content-Type", "application/json")], - ) + json.dumps({ + 'success': False, + 'existing_draft': True, + 'existing_draft_id': existing_draft.id, + 'existing_items': existing_items, + 'current_items': items, + 'message': _('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), - current_user.partner_id.id, - ) + _logger.info('Creating draft sale.order with %d items for partner %d', len(items), current_user.partner_id.id) # Create sales.order lines from items sale_order_lines = [] 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_id = int(item.get('product_id')) + quantity = float(item.get('quantity', 1)) + price = float(item.get('product_price', 0)) - product = request.env["product.product"].browse(product_id) + product = request.env['product.product'].browse(product_id) if not product.exists(): - _logger.warning( - "save_eskaera_draft: Product %d does not exist", product_id - ) + _logger.warning('save_eskaera_draft: Product %d does not exist', product_id) continue # Calculate subtotal subtotal = quantity * price - _logger.info( - "Item: product_id=%d, quantity=%.2f, price=%.2f, subtotal=%.2f", - product_id, - quantity, - price, - subtotal, - ) + _logger.info('Item: product_id=%d, quantity=%.2f, price=%.2f, subtotal=%.2f', + product_id, quantity, price, subtotal) # Create order line as a tuple for create() operation - line = ( - 0, - 0, - { - "product_id": product_id, - "product_uom_qty": quantity, - "price_unit": price, - }, - ) + line = (0, 0, { + 'product_id': product_id, + 'product_uom_qty': quantity, + 'price_unit': price, + }) sale_order_lines.append(line) except Exception as e: - _logger.error("Error processing item %s: %s", item, str(e)) + _logger.error('Error processing item %s: %s', item, str(e)) if not sale_order_lines: return request.make_response( - json.dumps({"error": "No valid items to save"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'No valid items to save'}), + [('Content-Type', 'application/json')], + status=400) # Handle merge vs replace action - if merge_action == "merge" and existing_draft_id: + if merge_action == 'merge' and existing_draft_id: # Merge: Add items to existing draft - existing_draft = request.env["sale.order"].browse( - int(existing_draft_id) - ) + existing_draft = request.env['sale.order'].browse(int(existing_draft_id)) if existing_draft.exists(): # Merge items: update quantities if product exists, add if new 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"] - + 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'] + # Find if product already exists in draft existing_line = existing_draft.order_line.filtered( lambda l: l.product_id.id == product_id ) - + if existing_line: # Update quantity (add to existing) - existing_line.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, - ) + existing_line.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: # Add new line to existing draft - existing_draft.order_line.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, - ) - + existing_draft.order_line.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) + sale_order = existing_draft merge_success = True - - elif merge_action == "replace" and existing_draft_id and existing_drafts: + + elif merge_action == 'replace' and existing_draft_id and existing_drafts: # Replace: Delete old draft and create new one existing_drafts.unlink() - _logger.info("Deleted existing draft %d", existing_draft_id) - + _logger.info('Deleted existing draft %d', existing_draft_id) + # Create new draft with current items - sale_order = request.env["sale.order"].create( - { - "partner_id": current_user.partner_id.id, - "order_line": sale_order_lines, - "state": "draft", # Explicitly set to draft - "group_order_id": order_id, # Link to the group.order - "pickup_day": group_order.pickup_day, # Propagate from group order - "pickup_date": group_order.pickup_date, # Propagate from group order - "home_delivery": group_order.home_delivery, # Propagate from group order - } - ) + sale_order = request.env['sale.order'].create({ + 'partner_id': current_user.partner_id.id, + 'order_line': sale_order_lines, + 'state': 'draft', # Explicitly set to draft + 'group_order_id': order_id, # Link to the group.order + 'pickup_day': group_order.pickup_day, # Propagate from group order + 'pickup_date': group_order.pickup_date, # Propagate from group order + 'home_delivery': group_order.home_delivery, # Propagate from group order + }) merge_success = False - + else: # No existing draft, create new one - sale_order = request.env["sale.order"].create( - { - "partner_id": current_user.partner_id.id, - "order_line": sale_order_lines, - "state": "draft", # Explicitly set to draft - "group_order_id": order_id, # Link to the group.order - "pickup_day": group_order.pickup_day, # Propagate from group order - "pickup_date": group_order.pickup_date, # Propagate from group order - "home_delivery": group_order.home_delivery, # Propagate from group order - } - ) + sale_order = request.env['sale.order'].create({ + 'partner_id': current_user.partner_id.id, + 'order_line': sale_order_lines, + 'state': 'draft', # Explicitly set to draft + 'group_order_id': order_id, # Link to the group.order + 'pickup_day': group_order.pickup_day, # Propagate from group order + 'pickup_date': group_order.pickup_date, # Propagate from group order + 'home_delivery': group_order.home_delivery, # Propagate from group order + }) merge_success = False - _logger.info( - "Draft sale.order created/updated: %d for partner %d with group_order_id=%s, pickup_day=%s, pickup_date=%s, home_delivery=%s", - sale_order.id, - current_user.partner_id.id, - sale_order.group_order_id.id if sale_order.group_order_id else None, - sale_order.pickup_day, - sale_order.pickup_date, - sale_order.home_delivery, - ) + _logger.info('Draft sale.order created/updated: %d for partner %d with group_order_id=%s, pickup_day=%s, pickup_date=%s, home_delivery=%s', + sale_order.id, current_user.partner_id.id, + sale_order.group_order_id.id if sale_order.group_order_id else None, + sale_order.pickup_day, sale_order.pickup_date, sale_order.home_delivery) return request.make_response( - json.dumps( - { - "success": True, - "message": ( - _("Merged with existing draft") - if merge_success - else _("Order saved as draft") - ), - "sale_order_id": sale_order.id, - "merged": merge_success, - } - ), - [("Content-Type", "application/json")], - ) + json.dumps({ + 'success': True, + 'message': _('Merged with existing draft') if merge_success else _('Order saved as draft'), + 'sale_order_id': sale_order.id, + 'merged': merge_success, + }), + [('Content-Type', 'application/json')]) except Exception as e: import traceback - - _logger.error("save_eskaera_draft: Unexpected error: %s", str(e)) + _logger.error('save_eskaera_draft: Unexpected error: %s', str(e)) _logger.error(traceback.format_exc()) return request.make_response( - json.dumps({"error": str(e)}), - [("Content-Type", "application/json")], - status=500, - ) + json.dumps({'error': str(e)}), + [('Content-Type', 'application/json')], + status=500) - @http.route( - ["/eskaera/confirm"], - type="http", - auth="user", - website=True, - methods=["POST"], - csrf=False, - ) + @http.route(['/eskaera/confirm'], type='http', auth='user', website=True, + methods=['POST'], csrf=False) def confirm_eskaera(self, **post): - """Confirm order and create sale.order from cart (localStorage). + '''Confirm order and create sale.order from cart (localStorage). Items come from the cart stored in the frontend localStorage. - """ + ''' import json try: # Initial log for debug - _logger.warning("=== CONFIRM_ESKAERA CALLED ===") - _logger.warning( - "Request data: %s", - request.httprequest.data[:200] if request.httprequest.data else "EMPTY", - ) + _logger.warning('=== CONFIRM_ESKAERA CALLED ===') + _logger.warning('Request data: %s', request.httprequest.data[:200] if request.httprequest.data else 'EMPTY') # Get JSON data from the request body if not request.httprequest.data: - _logger.warning("confirm_eskaera: No request data provided") + _logger.warning('confirm_eskaera: No request data provided') return request.make_response( - json.dumps({"error": "No data provided"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'No data provided'}), + [('Content-Type', 'application/json')], + status=400) # Decode JSON try: raw_data = request.httprequest.data if isinstance(raw_data, bytes): - raw_data = raw_data.decode("utf-8") + raw_data = raw_data.decode('utf-8') data = json.loads(raw_data) except Exception as e: - _logger.error("Error decoding JSON: %s", str(e)) + _logger.error('Error decoding JSON: %s', str(e)) return request.make_response( - json.dumps({"error": f"Invalid JSON: {str(e)}"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': f'Invalid JSON: {str(e)}'}), + [('Content-Type', 'application/json')], + status=400) - _logger.info("confirm_eskaera data received: %s", data) + _logger.info('confirm_eskaera data received: %s', data) # Validate order_id - order_id = data.get("order_id") + order_id = data.get('order_id') if not order_id: - _logger.warning("confirm_eskaera: order_id missing") + _logger.warning('confirm_eskaera: order_id missing') return request.make_response( - json.dumps({"error": "order_id is required"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'order_id is required'}), + [('Content-Type', 'application/json')], + status=400) # Convert to int try: order_id = int(order_id) except (ValueError, TypeError) as e: - _logger.warning("confirm_eskaera: Invalid order_id: %s", order_id) + _logger.warning('confirm_eskaera: Invalid order_id: %s', order_id) return request.make_response( - json.dumps({"error": f"Invalid order_id format: {order_id}"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': f'Invalid order_id format: {order_id}'}), + [('Content-Type', 'application/json')], + status=400) - _logger.info("order_id: %d", order_id) + _logger.info('order_id: %d', order_id) # Verify that the order exists - group_order = request.env["group.order"].browse(order_id) + group_order = request.env['group.order'].browse(order_id) if not group_order.exists(): - _logger.warning("confirm_eskaera: Order %d not found", order_id) + _logger.warning('confirm_eskaera: Order %d not found', order_id) return request.make_response( - json.dumps({"error": f"Order {order_id} not found"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': f'Order {order_id} not found'}), + [('Content-Type', 'application/json')], + status=400) # Verify that the order is open - if group_order.state != "open": - _logger.warning( - "confirm_eskaera: Order %d is not open (state: %s)", - order_id, - group_order.state, - ) + if group_order.state != 'open': + _logger.warning('confirm_eskaera: Order %d is not open (state: %s)', order_id, group_order.state) return request.make_response( - json.dumps({"error": f"Order is {group_order.state}"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': f'Order is {group_order.state}'}), + [('Content-Type', 'application/json')], + status=400) current_user = request.env.user - _logger.info("Current user: %d", current_user.id) + _logger.info('Current user: %d', current_user.id) # Validate that the user has a partner_id if not current_user.partner_id: - _logger.error( - "confirm_eskaera: User %d has no partner_id", current_user.id - ) + _logger.error('confirm_eskaera: User %d has no partner_id', current_user.id) return request.make_response( - json.dumps({"error": "User has no associated partner"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'User has no associated partner'}), + [('Content-Type', 'application/json')], + status=400) # Get cart items and delivery status - items = data.get("items", []) - is_delivery = data.get("is_delivery", False) + items = data.get('items', []) + is_delivery = data.get('is_delivery', False) if not items: - _logger.warning( - "confirm_eskaera: No items in cart for user %d in order %d", - current_user.id, - order_id, - ) + _logger.warning('confirm_eskaera: No items in cart for user %d in order %d', + current_user.id, order_id) return request.make_response( - json.dumps({"error": "No items in cart"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'No items in cart'}), + [('Content-Type', 'application/json')], + status=400) # First, check if there's already a draft sale.order for this user in this group order - existing_order = request.env["sale.order"].search( - [ - ("partner_id", "=", current_user.partner_id.id), - ("group_order_id", "=", group_order.id), - ("state", "=", "draft"), - ], - limit=1, - ) + existing_order = request.env['sale.order'].search([ + ('partner_id', '=', current_user.partner_id.id), + ('group_order_id', '=', group_order.id), + ('state', '=', 'draft') + ], limit=1) if existing_order: - _logger.info( - "Found existing draft order: %d, updating instead of creating new", - existing_order.id, - ) + _logger.info('Found existing draft order: %d, updating instead of creating new', existing_order.id) # Delete existing lines and create new ones existing_order.order_line.unlink() sale_order = existing_order else: - _logger.info( - "No existing draft order found, will create new sale.order" - ) + _logger.info('No existing draft order found, will create new sale.order') sale_order = None # Create sales.order lines from items sale_order_lines = [] 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_id = int(item.get('product_id')) + quantity = float(item.get('quantity', 1)) + price = float(item.get('product_price', 0)) - product = request.env["product.product"].browse(product_id) + product = request.env['product.product'].browse(product_id) if not product.exists(): - _logger.warning( - "confirm_eskaera: Product %d does not exist", product_id - ) + _logger.warning('confirm_eskaera: Product %d does not exist', product_id) continue # Get product name in user's language context @@ -1791,26 +1412,23 @@ class AplicoopWebsiteSale(WebsiteSale): product_name = product_in_lang.name line_data = { - "product_id": product_id, - "product_uom_qty": quantity, - "price_unit": price or product.list_price, - "name": product_name, # Force the translated product name + 'product_id': product_id, + 'product_uom_qty': quantity, + 'price_unit': price or product.list_price, + 'name': product_name, # Force the translated product name } - _logger.info("Adding sale order line: %s", line_data) + _logger.info('Adding sale order line: %s', line_data) sale_order_lines.append((0, 0, line_data)) except (ValueError, TypeError) as e: - _logger.warning( - "confirm_eskaera: Error processing item %s: %s", item, str(e) - ) + _logger.warning('confirm_eskaera: Error processing item %s: %s', item, str(e)) continue if not sale_order_lines: - _logger.warning("confirm_eskaera: No valid items for sale.order") + _logger.warning('confirm_eskaera: No valid items for sale.order') return request.make_response( - json.dumps({"error": "No valid items in cart"}), - [("Content-Type", "application/json")], - status=400, - ) + json.dumps({'error': 'No valid items in cart'}), + [('Content-Type', 'application/json')], + status=400) # Get pickup date and delivery info from group order # If delivery, use delivery_date; otherwise use pickup_date @@ -1824,11 +1442,7 @@ class AplicoopWebsiteSale(WebsiteSale): try: if sale_order: # Update existing order with new lines - _logger.info( - "Updating existing sale.order %d with %d items", - sale_order.id, - len(sale_order_lines), - ) + _logger.info('Updating existing sale.order %d with %d items', sale_order.id, len(sale_order_lines)) sale_order.order_line = sale_order_lines # Ensure group_order_id is set and propagate group order fields if not sale_order.group_order_id: @@ -1839,38 +1453,29 @@ class AplicoopWebsiteSale(WebsiteSale): sale_order.home_delivery = is_delivery if commitment_date: sale_order.commitment_date = commitment_date - _logger.info( - "Updated sale.order %d: commitment_date=%s, home_delivery=%s", - sale_order.id, - commitment_date, - is_delivery, - ) + _logger.info('Updated sale.order %d: commitment_date=%s, home_delivery=%s', + sale_order.id, commitment_date, is_delivery) else: # Create new order order_vals = { - "partner_id": current_user.partner_id.id, - "order_line": sale_order_lines, - "group_order_id": group_order.id, - "pickup_day": group_order.pickup_day, - "pickup_date": group_order.pickup_date, - "home_delivery": is_delivery, + 'partner_id': current_user.partner_id.id, + 'order_line': sale_order_lines, + 'group_order_id': group_order.id, + 'pickup_day': group_order.pickup_day, + 'pickup_date': group_order.pickup_date, + 'home_delivery': is_delivery, } - + # Add commitment date (pickup/delivery date) if available if commitment_date: - order_vals["commitment_date"] = commitment_date - - sale_order = request.env["sale.order"].create(order_vals) - _logger.info( - "sale.order created successfully: %d with group_order_id=%d, pickup_day=%s, home_delivery=%s", - sale_order.id, - group_order.id, - group_order.pickup_day, - group_order.home_delivery, - ) + order_vals['commitment_date'] = commitment_date + + sale_order = request.env['sale.order'].create(order_vals) + _logger.info('sale.order created successfully: %d with group_order_id=%d, pickup_day=%s, home_delivery=%s', + sale_order.id, group_order.id, group_order.pickup_day, group_order.home_delivery) except Exception as e: - _logger.error("Error creating/updating sale.order: %s", str(e)) - _logger.error("sale_order_lines: %s", sale_order_lines) + _logger.error('Error creating/updating sale.order: %s', str(e)) + _logger.error('sale_order_lines: %s', sale_order_lines) raise # Build a localized confirmation message on the server so the @@ -1882,31 +1487,29 @@ class AplicoopWebsiteSale(WebsiteSale): except Exception: pickup_day_index = None - base_message = _("Thank you! Your order has been confirmed.") - order_reference_label = _("Order reference") - pickup_label = _("Pickup day") - delivery_label = _("Delivery date") - pickup_day_name = "" - pickup_date_str = "" - + base_message = (_('Thank you! Your order has been confirmed.')) + order_reference_label = (_('Order reference')) + pickup_label = (_('Pickup day')) + delivery_label = (_('Delivery date')) + pickup_day_name = '' + pickup_date_str = '' + # Add order reference to message if sale_order.name: - base_message = ( - f"{base_message}\n\n{order_reference_label}: {sale_order.name}" - ) + base_message = f"{base_message}\n\n{order_reference_label}: {sale_order.name}" if pickup_day_index is not None: try: day_names = self._get_day_names(env=request.env) pickup_day_name = day_names[pickup_day_index % len(day_names)] except Exception: - pickup_day_name = "" - + pickup_day_name = '' + # Add pickup/delivery date in numeric format if group_order.pickup_date: if is_delivery: # For delivery, use delivery_date (already computed as pickup_date + 1) if group_order.delivery_date: - pickup_date_str = group_order.delivery_date.strftime("%d/%m/%Y") + pickup_date_str = group_order.delivery_date.strftime('%d/%m/%Y') # For delivery, use the next day's name if pickup_day_index is not None: try: @@ -1915,12 +1518,12 @@ class AplicoopWebsiteSale(WebsiteSale): next_day_index = (pickup_day_index + 1) % 7 pickup_day_name = day_names[next_day_index] except Exception: - pickup_day_name = "" + pickup_day_name = '' else: - pickup_date_str = group_order.pickup_date.strftime("%d/%m/%Y") + pickup_date_str = group_order.pickup_date.strftime('%d/%m/%Y') else: # For pickup, use the same date - pickup_date_str = group_order.pickup_date.strftime("%d/%m/%Y") + pickup_date_str = group_order.pickup_date.strftime('%d/%m/%Y') # Build final message with correct label and date based on delivery or pickup message = base_message @@ -1933,94 +1536,77 @@ class AplicoopWebsiteSale(WebsiteSale): message = f"{message}\n\n\n{label_to_use}: {pickup_date_str}" response_data = { - "success": True, - "message": message, - "sale_order_id": sale_order.id, - "redirect_url": sale_order.get_portal_url(), - "group_order_name": group_order.name, - "pickup_day": pickup_day_name, - "pickup_date": pickup_date_str, - "pickup_day_index": pickup_day_index, + 'success': True, + 'message': message, + 'sale_order_id': sale_order.id, + 'redirect_url': sale_order.get_portal_url(), + 'group_order_name': group_order.name, + 'pickup_day': pickup_day_name, + 'pickup_date': pickup_date_str, + 'pickup_day_index': pickup_day_index, } # Log language and final message to debug translation issues try: - _logger.info( - 'confirm_eskaera: lang=%s, message="%s"', request.env.lang, message - ) + _logger.info('confirm_eskaera: lang=%s, message="%s"', request.env.lang, message) except Exception: - _logger.info("confirm_eskaera: message logging failed") + _logger.info('confirm_eskaera: message logging failed') - _logger.info( - "Order %d confirmed successfully, sale.order created: %d", - order_id, - sale_order.id, - ) + _logger.info('Order %d confirmed successfully, sale.order created: %d', + order_id, sale_order.id) # Confirm the sale.order (change state from draft to sale) try: sale_order.action_confirm() - _logger.info( - "sale.order %d confirmed (state changed to sale)", sale_order.id - ) + _logger.info('sale.order %d confirmed (state changed to sale)', sale_order.id) except Exception as e: - _logger.warning( - "Failed to confirm sale.order %d: %s", sale_order.id, str(e) - ) + _logger.warning('Failed to confirm sale.order %d: %s', sale_order.id, str(e)) # Continue anyway, the order was created/updated return request.make_response( - json.dumps(response_data), [("Content-Type", "application/json")] - ) + json.dumps(response_data), + [('Content-Type', 'application/json')]) except Exception as e: import traceback - - _logger.error("confirm_eskaera: Unexpected error: %s", str(e)) + _logger.error('confirm_eskaera: Unexpected error: %s', str(e)) _logger.error(traceback.format_exc()) return request.make_response( - json.dumps({"error": str(e)}), - [("Content-Type", "application/json")], - status=500, - ) - - @http.route( - ["/eskaera//load-from-history/"], - type="http", - auth="user", - website=True, - ) + json.dumps({'error': str(e)}), + [('Content-Type', 'application/json')], + status=500) + @http.route(['/eskaera//load-from-history/'], + type='http', auth='user', website=True) def load_order_from_history(self, group_order_id=None, sale_order_id=None, **post): - """Load a historical order (draft/confirmed) back into the cart. - + '''Load a historical order (draft/confirmed) back into the cart. + Used by portal "Load in Cart" button on My Orders page. Extracts items from the order and redirects to the group order page, where the JavaScript auto-load will populate the cart. - """ + ''' try: # Get the sale.order record - sale_order = request.env["sale.order"].browse(sale_order_id) + sale_order = request.env['sale.order'].browse(sale_order_id) if not sale_order.exists(): - return request.redirect("/shop") + return request.redirect('/shop') # Verify this order belongs to current user current_partner = request.env.user.partner_id if sale_order.partner_id.id != current_partner.id: _logger.warning( - "User %s attempted to load order %d belonging to partner %d", - request.env.user.login, - sale_order_id, - sale_order.partner_id.id, + 'User %s attempted to load order %d belonging to partner %d', + request.env.user.login, sale_order_id, sale_order.partner_id.id ) - return request.redirect("/shop") + return request.redirect('/shop') # Verify the order belongs to the requested group_order if sale_order.group_order_id.id != group_order_id: - return request.redirect("/eskaera/%d" % sale_order.group_order_id.id) + return request.redirect('/eskaera/%d' % sale_order.group_order_id.id) # Extract items from the order (skip delivery product) delivery_product = request.env.ref( - "website_sale_aplicoop.product_home_delivery", raise_if_not_found=False + 'website_sale_aplicoop.product_home_delivery', + raise_if_not_found=False ) delivery_product_id = delivery_product.id if delivery_product else None @@ -2030,289 +1616,242 @@ class AplicoopWebsiteSale(WebsiteSale): if delivery_product_id and line.product_id.id == delivery_product_id: continue - items.append( - { - "product_id": line.product_id.id, - "product_name": line.product_id.name, - "quantity": line.product_uom_qty, - "price": line.price_unit, # Unit price - } - ) + items.append({ + 'product_id': line.product_id.id, + 'product_name': line.product_id.name, + 'quantity': line.product_uom_qty, + 'price': line.price_unit, # Unit price + }) # Store items in localStorage by passing via URL parameter or session # We'll use sessionStorage in JavaScript to avoid URL length limits - + # Get the current group order for comparison - current_group_order = request.env["group.order"].browse(group_order_id) - + current_group_order = request.env['group.order'].browse(group_order_id) + # Check if the order being loaded is from the same group order # If not, don't restore the old pickup fields - use the current group order's fields same_group_order = sale_order.group_order_id.id == group_order_id - + # If loading from same group order, restore old pickup fields # Otherwise, page will show current group order's pickup fields pickup_day_to_restore = sale_order.pickup_day if same_group_order else None - pickup_date_to_restore = ( - str(sale_order.pickup_date) - if (same_group_order and sale_order.pickup_date) - else None - ) - home_delivery_to_restore = ( - sale_order.home_delivery if same_group_order else None - ) - + pickup_date_to_restore = str(sale_order.pickup_date) if (same_group_order and sale_order.pickup_date) else None + home_delivery_to_restore = sale_order.home_delivery if same_group_order else None + response = request.make_response( - request.render( - "website_sale_aplicoop.eskaera_load_from_history", - { - "group_order_id": group_order_id, - "items_json": json.dumps(items), # Pass serialized JSON - "sale_order": sale_order, - "sale_order_name": sale_order.name, # Pass order reference - "pickup_day": pickup_day_to_restore, # Pass pickup day (or None if different group) - "pickup_date": pickup_date_to_restore, # Pass pickup date (or None if different group) - "home_delivery": home_delivery_to_restore, # Pass home delivery flag (or None if different group) - "same_group_order": same_group_order, # Indicate if from same group order - }, - ), + request.render('website_sale_aplicoop.eskaera_load_from_history', { + 'group_order_id': group_order_id, + 'items_json': json.dumps(items), # Pass serialized JSON + 'sale_order': sale_order, + 'sale_order_name': sale_order.name, # Pass order reference + 'pickup_day': pickup_day_to_restore, # Pass pickup day (or None if different group) + 'pickup_date': pickup_date_to_restore, # Pass pickup date (or None if different group) + 'home_delivery': home_delivery_to_restore, # Pass home delivery flag (or None if different group) + 'same_group_order': same_group_order, # Indicate if from same group order + }), ) return response except Exception as e: - _logger.error("load_order_from_history: %s", str(e)) + _logger.error('load_order_from_history: %s', str(e)) import traceback - _logger.error(traceback.format_exc()) - return request.redirect("/eskaera/%d" % group_order_id) - - @http.route( - ["/eskaera//confirm/"], - type="json", - auth="user", - website=True, - methods=["POST"], - ) - def confirm_order_from_portal( - self, group_order_id=None, sale_order_id=None, **post - ): - """Confirm a draft order from the portal (AJAX endpoint). - + return request.redirect('/eskaera/%d' % group_order_id) + @http.route(['/eskaera//confirm/'], + type='json', auth='user', website=True, methods=['POST']) + def confirm_order_from_portal(self, group_order_id=None, sale_order_id=None, **post): + '''Confirm a draft order from the portal (AJAX endpoint). + Used by portal "Confirm" button on My Orders page. Confirms the draft order and returns JSON response. Does NOT redirect - the calling JavaScript handles the response. - """ - _logger.info( - "confirm_order_from_portal called: group_order_id=%s, sale_order_id=%s", - group_order_id, - sale_order_id, - ) - + ''' + _logger.info('confirm_order_from_portal called: group_order_id=%s, sale_order_id=%s', group_order_id, sale_order_id) + try: # Get the sale.order record - sale_order = request.env["sale.order"].browse(sale_order_id) + sale_order = request.env['sale.order'].browse(sale_order_id) if not sale_order.exists(): - _logger.warning( - "confirm_order_from_portal: Order %d not found", sale_order_id - ) - return {"success": False, "error": "Order not found"} + _logger.warning('confirm_order_from_portal: Order %d not found', sale_order_id) + return { + 'success': False, + 'error': 'Order not found' + } # Verify this order belongs to current user current_partner = request.env.user.partner_id if sale_order.partner_id.id != current_partner.id: _logger.warning( - "User %s attempted to confirm order %d belonging to partner %d", - request.env.user.login, - sale_order_id, - sale_order.partner_id.id, + 'User %s attempted to confirm order %d belonging to partner %d', + request.env.user.login, sale_order_id, sale_order.partner_id.id ) - return {"success": False, "error": "Unauthorized"} + return { + 'success': False, + 'error': 'Unauthorized' + } # Verify the order belongs to the requested group_order if sale_order.group_order_id.id != group_order_id: - _logger.warning( - "Order %d belongs to group %d, not %d", - sale_order_id, - sale_order.group_order_id.id, - group_order_id, - ) + _logger.warning('Order %d belongs to group %d, not %d', sale_order_id, sale_order.group_order_id.id, group_order_id) return { - "success": False, - "error": f"Order belongs to different group: {sale_order.group_order_id.id}", + 'success': False, + 'error': f'Order belongs to different group: {sale_order.group_order_id.id}' } # Only allow confirming draft orders - if sale_order.state != "draft": - _logger.warning( - "Order %d is in state %s, not draft", - sale_order_id, - sale_order.state, - ) + if sale_order.state != 'draft': + _logger.warning('Order %d is in state %s, not draft', sale_order_id, sale_order.state) return { - "success": False, - "error": f"Order is already {sale_order.state}, cannot confirm again", + 'success': False, + 'error': f'Order is already {sale_order.state}, cannot confirm again' } # Confirm the order (change state to 'sale') sale_order.action_confirm() - _logger.info( - "Order %d confirmed from portal by user %s", - sale_order_id, - request.env.user.login, - ) + _logger.info('Order %d confirmed from portal by user %s', sale_order_id, request.env.user.login) # Return success response with updated order state return { - "success": True, - "message": _("Order confirmed successfully"), - "order_id": sale_order_id, - "order_state": sale_order.state, - "group_order_id": group_order_id, + 'success': True, + 'message': _('Order confirmed successfully'), + 'order_id': sale_order_id, + 'order_state': sale_order.state, + 'group_order_id': group_order_id } except Exception as e: - _logger.error("confirm_order_from_portal: %s", str(e)) + _logger.error('confirm_order_from_portal: %s', str(e)) import traceback - _logger.error(traceback.format_exc()) - return {"success": False, "error": f"Error confirming order: {str(e)}"} - + return { + 'success': False, + 'error': f'Error confirming order: {str(e)}' + } def _translate_labels(self, labels_dict, lang): - """Manually translate labels based on user language. - + '''Manually translate labels based on user language. + This is a fallback translation method for when Odoo's translation system hasn't loaded translations from .po files properly. - """ + ''' translations = { - "es_ES": { - "Draft Already Exists": "El Borrador Ya Existe", - "A saved draft already exists for this week.": "Un borrador guardado ya existe para esta semana.", - "You have two options:": "Tienes dos opciones:", - "Option 1: Merge with Existing Draft": "Opción 1: Fusionar con Borrador Existente", - "Combine your current cart with the existing draft.": "Combina tu carrito actual con el borrador existente.", - "Existing draft has": "El borrador existente tiene", - "Current cart has": "Tu carrito actual tiene", - "item(s)": "artículo(s)", - "Products will be merged by adding quantities. If a product exists in both, quantities will be combined.": "Los productos se fusionarán sumando cantidades. Si un producto existe en ambos, las cantidades se combinarán.", - "Option 2: Replace with Current Cart": "Opción 2: Reemplazar con Carrito Actual", - "Delete the old draft and save only the current cart items.": "Elimina el borrador anterior y guarda solo los artículos del carrito actual.", - "The existing draft will be permanently deleted.": "El borrador existente se eliminará permanentemente.", - "Merge": "Fusionar", - "Replace": "Reemplazar", - "Cancel": "Cancelar", + 'es_ES': { + 'Draft Already Exists': 'El Borrador Ya Existe', + 'A saved draft already exists for this week.': 'Un borrador guardado ya existe para esta semana.', + 'You have two options:': 'Tienes dos opciones:', + 'Option 1: Merge with Existing Draft': 'Opción 1: Fusionar con Borrador Existente', + 'Combine your current cart with the existing draft.': 'Combina tu carrito actual con el borrador existente.', + 'Existing draft has': 'El borrador existente tiene', + 'Current cart has': 'Tu carrito actual tiene', + 'item(s)': 'artículo(s)', + 'Products will be merged by adding quantities. If a product exists in both, quantities will be combined.': 'Los productos se fusionarán sumando cantidades. Si un producto existe en ambos, las cantidades se combinarán.', + 'Option 2: Replace with Current Cart': 'Opción 2: Reemplazar con Carrito Actual', + 'Delete the old draft and save only the current cart items.': 'Elimina el borrador anterior y guarda solo los artículos del carrito actual.', + 'The existing draft will be permanently deleted.': 'El borrador existente se eliminará permanentemente.', + 'Merge': 'Fusionar', + 'Replace': 'Reemplazar', + 'Cancel': 'Cancelar', # Checkout page labels - "Home Delivery": "Entrega a Domicilio", - "Delivery Information": "Información de Entrega", - "Your order will be delivered the day after pickup between 11:00 - 14:00": "Tu pedido será entregado el día después de la recogida entre las 11:00 - 14:00", - "Important": "Importante", - "Once you confirm this order, you will not be able to modify it. Please review carefully before confirming.": "Una vez confirmes este pedido, no podrás modificarlo. Por favor, revisa cuidadosamente antes de confirmar.", + 'Home Delivery': 'Entrega a Domicilio', + 'Delivery Information': 'Información de Entrega', + 'Your order will be delivered the day after pickup between 11:00 - 14:00': 'Tu pedido será entregado el día después de la recogida entre las 11:00 - 14:00', + 'Important': 'Importante', + 'Once you confirm this order, you will not be able to modify it. Please review carefully before confirming.': 'Una vez confirmes este pedido, no podrás modificarlo. Por favor, revisa cuidadosamente antes de confirmar.', }, - "eu_ES": { - "Draft Already Exists": "Zirriborro Dagoeneko Badago", - "A saved draft already exists for this week.": "Gordetako zirriborro bat dagoeneko badago asteburu honetarako.", - "You have two options:": "Bi aukera dituzu:", - "Option 1: Merge with Existing Draft": "1. Aukera: Existentea Duen Zirriborroarekin Batu", - "Combine your current cart with the existing draft.": "Batu zure gaur-oraingo saskia existentea duen zirriborroarekin.", - "Existing draft has": "Existentea duen zirriborroak du", - "Current cart has": "Zure gaur-oraingo saskiak du", - "item(s)": "artikulu(a)", - "Products will be merged by adding quantities. If a product exists in both, quantities will be combined.": "Produktuak batuko dira kantitateak gehituz. Produktu bat bian badago, kantitateak konbinatuko dira.", - "Option 2: Replace with Current Cart": "2. Aukera: Gaur-oraingo Askiarekin Ordeztu", - "Delete the old draft and save only the current cart items.": "Ezabatu zahar-zirriborroa eta gorde soilik gaur-oraingo saskiaren artikulua.", - "The existing draft will be permanently deleted.": "Existentea duen zirriborroa behin betiko ezabatuko da.", - "Merge": "Batu", - "Replace": "Ordeztu", - "Cancel": "Ezeztatu", + 'eu_ES': { + 'Draft Already Exists': 'Zirriborro Dagoeneko Badago', + 'A saved draft already exists for this week.': 'Gordetako zirriborro bat dagoeneko badago asteburu honetarako.', + 'You have two options:': 'Bi aukera dituzu:', + 'Option 1: Merge with Existing Draft': '1. Aukera: Existentea Duen Zirriborroarekin Batu', + 'Combine your current cart with the existing draft.': 'Batu zure gaur-oraingo saskia existentea duen zirriborroarekin.', + 'Existing draft has': 'Existentea duen zirriborroak du', + 'Current cart has': 'Zure gaur-oraingo saskiak du', + 'item(s)': 'artikulu(a)', + 'Products will be merged by adding quantities. If a product exists in both, quantities will be combined.': 'Produktuak batuko dira kantitateak gehituz. Produktu bat bian badago, kantitateak konbinatuko dira.', + 'Option 2: Replace with Current Cart': '2. Aukera: Gaur-oraingo Askiarekin Ordeztu', + 'Delete the old draft and save only the current cart items.': 'Ezabatu zahar-zirriborroa eta gorde soilik gaur-oraingo saskiaren artikulua.', + 'The existing draft will be permanently deleted.': 'Existentea duen zirriborroa behin betiko ezabatuko da.', + 'Merge': 'Batu', + 'Replace': 'Ordeztu', + 'Cancel': 'Ezeztatu', # Checkout page labels - "Home Delivery": "Etxera Bidalketa", - "Delivery Information": "Bidalketaren Informazioa", - "Your order will be delivered the day after pickup between 11:00 - 14:00": "Zure eskaera bidaliko da biltzeko eguaren ondoren 11:00 - 14:00 bitartean", - "Important": "Garrantzitsua", - "Once you confirm this order, you will not be able to modify it. Please review carefully before confirming.": "Behin eskaera hau berretsi ondoren, ezin izango duzu aldatu. Mesedez, arretaz berrikusi berretsi aurretik.", + 'Home Delivery': 'Etxera Bidalketa', + 'Delivery Information': 'Bidalketaren Informazioa', + 'Your order will be delivered the day after pickup between 11:00 - 14:00': 'Zure eskaera bidaliko da biltzeko eguaren ondoren 11:00 - 14:00 bitartean', + 'Important': 'Garrantzitsua', + 'Once you confirm this order, you will not be able to modify it. Please review carefully before confirming.': 'Behin eskaera hau berretsi ondoren, ezin izango duzu aldatu. Mesedez, arretaz berrikusi berretsi aurretik.', }, # Also support 'eu' as a variant - "eu": { - "Draft Already Exists": "Zirriborro Dagoeneko Badago", - "A saved draft already exists for this week.": "Gordetako zirriborro bat dagoeneko badago asteburu honetarako.", - "You have two options:": "Bi aukera dituzu:", - "Option 1: Merge with Existing Draft": "1. Aukera: Existentea Duen Zirriborroarekin Batu", - "Combine your current cart with the existing draft.": "Batu zure gaur-oraingo saskia existentea duen zirriborroarekin.", - "Existing draft has": "Existentea duen zirriborroak du", - "Current cart has": "Zure gaur-oraingo saskiak du", - "item(s)": "artikulu(a)", - "Products will be merged by adding quantities. If a product exists in both, quantities will be combined.": "Produktuak batuko dira kantitateak gehituz. Produktu bat bian badago, kantitateak konbinatuko dira.", - "Option 2: Replace with Current Cart": "2. Aukera: Gaur-oraingo Askiarekin Ordeztu", - "Delete the old draft and save only the current cart items.": "Ezabatu zahar-zirriborroa eta gorde soilik gaur-oraingo saskiaren artikulua.", - "The existing draft will be permanently deleted.": "Existentea duen zirriborroa behin betiko ezabatuko da.", - "Merge": "Batu", - "Replace": "Ordeztu", - "Cancel": "Ezeztatu", + 'eu': { + 'Draft Already Exists': 'Zirriborro Dagoeneko Badago', + 'A saved draft already exists for this week.': 'Gordetako zirriborro bat dagoeneko badago asteburu honetarako.', + 'You have two options:': 'Bi aukera dituzu:', + 'Option 1: Merge with Existing Draft': '1. Aukera: Existentea Duen Zirriborroarekin Batu', + 'Combine your current cart with the existing draft.': 'Batu zure gaur-oraingo saskia existentea duen zirriborroarekin.', + 'Existing draft has': 'Existentea duen zirriborroak du', + 'Current cart has': 'Zure gaur-oraingo saskiak du', + 'item(s)': 'artikulu(a)', + 'Products will be merged by adding quantities. If a product exists in both, quantities will be combined.': 'Produktuak batuko dira kantitateak gehituz. Produktu bat bian badago, kantitateak konbinatuko dira.', + 'Option 2: Replace with Current Cart': '2. Aukera: Gaur-oraingo Askiarekin Ordeztu', + 'Delete the old draft and save only the current cart items.': 'Ezabatu zahar-zirriborroa eta gorde soilik gaur-oraingo saskiaren artikulua.', + 'The existing draft will be permanently deleted.': 'Existentea duen zirriborroa behin betiko ezabatuko da.', + 'Merge': 'Batu', + 'Replace': 'Ordeztu', + 'Cancel': 'Ezeztatu', # Checkout page labels - "Home Delivery": "Etxera Bidalketa", - "Delivery Information": "Bidalketaren Informazioa", - "Your order will be delivered the day after pickup between 11:00 - 14:00": "Zure eskaera bidaliko da biltzeko eguaren ondoren 11:00 - 14:00 bitartean", - "Important": "Garrantzitsua", - "Once you confirm this order, you will not be able to modify it. Please review carefully before confirming.": "Behin eskaera hau berretsi ondoren, ezin izango duzu aldatu. Mesedez, arretaz berrikusi berretsi aurretik.", - }, + 'Home Delivery': 'Etxera Bidalketa', + 'Delivery Information': 'Bidalketaren Informazioa', + 'Your order will be delivered the day after pickup between 11:00 - 14:00': 'Zure eskaera bidaliko da biltzeko eguaren ondoren 11:00 - 14:00 bitartean', + 'Important': 'Garrantzitsua', + 'Once you confirm this order, you will not be able to modify it. Please review carefully before confirming.': 'Behin eskaera hau berretsi ondoren, ezin izango duzu aldatu. Mesedez, arretaz berrikusi berretsi aurretik.', + } } - + # Get the translation dictionary for the user's language # Try exact match first, then try without the region code (e.g., 'eu' from 'eu_ES') lang_translations = translations.get(lang) - if not lang_translations and "_" in lang: - lang_code = lang.split("_")[0] # Get 'eu' from 'eu_ES' + if not lang_translations and '_' in lang: + lang_code = lang.split('_')[0] # Get 'eu' from 'eu_ES' lang_translations = translations.get(lang_code, {}) if not lang_translations: lang_translations = {} - + # Translate all English labels to the target language translated = {} for key, english_label in labels_dict.items(): translated[key] = lang_translations.get(english_label, english_label) - - _logger.info( - "[_translate_labels] Language: %s, Translated %d labels", - lang, - len(translated), - ) - + + _logger.info('[_translate_labels] Language: %s, Translated %d labels', lang, len(translated)) + return translated - @http.route( - ["/eskaera/labels", "/eskaera/i18n"], - type="json", - auth="public", - website=True, - csrf=False, - ) + @http.route(['/eskaera/labels', '/eskaera/i18n'], type='json', auth='public', website=True, csrf=False) def get_checkout_labels(self, **post): - """Return ALL translated UI labels and messages unified. - + '''Return ALL translated UI labels and messages unified. + This is the SINGLE API ENDPOINT for fetching all user-facing translations. Use this from JavaScript instead of maintaining local translation files. - + The endpoint automatically detects the user's language and returns all UI labels/messages in that language, ready to be used directly. - + Returns: dict: Complete set of translated labels and messages - """ + ''' try: lang = self._get_detected_language(**post) labels = self._get_translated_labels(lang) - - _logger.info( - "[get_checkout_labels] ✅ SUCCESS - Language: %s, Label count: %d", - lang, - len(labels), - ) - + + _logger.info('[get_checkout_labels] ✅ SUCCESS - Language: %s, Label count: %d', + lang, len(labels)) + return labels except Exception as e: - _logger.error("[get_checkout_labels] ❌ ERROR: %s", str(e), exc_info=True) + _logger.error('[get_checkout_labels] ❌ ERROR: %s', str(e), exc_info=True) # Return default English labels as fallback return { - "save_cart": "Save Cart", - "reload_cart": "Reload Cart", - "empty_cart": "Your cart is empty", - "added_to_cart": "added to cart", - } + 'save_cart': 'Save Cart', + 'reload_cart': 'Reload Cart', + 'empty_cart': 'Your cart is empty', + 'added_to_cart': 'added to cart', + } \ No newline at end of file diff --git a/website_sale_aplicoop/tests/test_price_with_taxes_included.py b/website_sale_aplicoop/tests/test_price_with_taxes_included.py deleted file mode 100644 index 5bdee16..0000000 --- a/website_sale_aplicoop/tests/test_price_with_taxes_included.py +++ /dev/null @@ -1,425 +0,0 @@ -# Copyright 2025 Criptomart -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) - -""" -Test suite for price calculations WITH taxes included. - -This test verifies that the _compute_price_with_taxes method correctly -calculates prices including taxes for display in the online shop. -""" - -from odoo.tests import tagged -from odoo.tests.common import TransactionCase - - -@tagged("post_install", "-at_install") -class TestPriceWithTaxesIncluded(TransactionCase): - """Test that prices displayed include taxes.""" - - def setUp(self): - super().setUp() - - # Create test company - self.company = self.env["res.company"].create( - { - "name": "Test Company Tax Included", - } - ) - - # Get or create default tax group - tax_group = self.env["account.tax.group"].search( - [("company_id", "=", self.company.id)], limit=1 - ) - if not tax_group: - tax_group = self.env["account.tax.group"].create( - { - "name": "IVA", - "company_id": self.company.id, - } - ) - - # Get default country (Spain) - country_es = self.env.ref("base.es") - - # Create tax (21% IVA) - price_include=False (default) - self.tax_21 = self.env["account.tax"].create( - { - "name": "IVA 21%", - "amount": 21.0, - "amount_type": "percent", - "type_tax_use": "sale", - "price_include": False, # Explicit: tax NOT included in price - "company_id": self.company.id, - "country_id": country_es.id, - "tax_group_id": tax_group.id, - } - ) - - # Create tax (10% IVA reducido) - self.tax_10 = self.env["account.tax"].create( - { - "name": "IVA 10%", - "amount": 10.0, - "amount_type": "percent", - "type_tax_use": "sale", - "price_include": False, - "company_id": self.company.id, - "country_id": country_es.id, - "tax_group_id": tax_group.id, - } - ) - - # Create tax with price_include=True for comparison - self.tax_21_included = self.env["account.tax"].create( - { - "name": "IVA 21% Incluido", - "amount": 21.0, - "amount_type": "percent", - "type_tax_use": "sale", - "price_include": True, # Tax IS included in price - "company_id": self.company.id, - "country_id": country_es.id, - "tax_group_id": tax_group.id, - } - ) - - # Create product category - self.category = self.env["product.category"].create( - { - "name": "Test Category Tax Included", - } - ) - - # Create test products with different tax configurations - self.product_21 = self.env["product.product"].create( - { - "name": "Product With 21% Tax", - "list_price": 100.0, - "categ_id": self.category.id, - "taxes_id": [(6, 0, [self.tax_21.id])], - "company_id": self.company.id, - } - ) - - self.product_10 = self.env["product.product"].create( - { - "name": "Product With 10% Tax", - "list_price": 100.0, - "categ_id": self.category.id, - "taxes_id": [(6, 0, [self.tax_10.id])], - "company_id": self.company.id, - } - ) - - self.product_no_tax = self.env["product.product"].create( - { - "name": "Product Without Tax", - "list_price": 100.0, - "categ_id": self.category.id, - "taxes_id": False, - "company_id": self.company.id, - } - ) - - self.product_tax_included = self.env["product.product"].create( - { - "name": "Product With Tax Included", - "list_price": 121.0, # 100 + 21% = 121 - "categ_id": self.category.id, - "taxes_id": [(6, 0, [self.tax_21_included.id])], - "company_id": self.company.id, - } - ) - - # Create pricelist - self.pricelist = self.env["product.pricelist"].create( - { - "name": "Test Pricelist", - "company_id": self.company.id, - } - ) - - def test_price_with_21_percent_tax(self): - """Test that 21% tax is correctly added to base price.""" - # Base price: 100.0 - # Expected with 21% tax: 121.0 - - taxes = self.product_21.taxes_id.filtered( - lambda t: t.company_id == self.company - ) - - base_price = 100.0 - tax_result = taxes.compute_all( - base_price, - currency=self.env.company.currency_id, - quantity=1.0, - product=self.product_21, - ) - - price_with_tax = tax_result["total_included"] - - self.assertAlmostEqual( - price_with_tax, 121.0, places=2, msg="100 + 21% should equal 121.0" - ) - - def test_price_with_10_percent_tax(self): - """Test that 10% tax is correctly added to base price.""" - # Base price: 100.0 - # Expected with 10% tax: 110.0 - - taxes = self.product_10.taxes_id.filtered( - lambda t: t.company_id == self.company - ) - - base_price = 100.0 - tax_result = taxes.compute_all( - base_price, - currency=self.env.company.currency_id, - quantity=1.0, - product=self.product_10, - ) - - price_with_tax = tax_result["total_included"] - - self.assertAlmostEqual( - price_with_tax, 110.0, places=2, msg="100 + 10% should equal 110.0" - ) - - def test_price_without_tax(self): - """Test that product without tax returns base price unchanged.""" - # Base price: 100.0 - # Expected with no tax: 100.0 - - taxes = self.product_no_tax.taxes_id.filtered( - lambda t: t.company_id == self.company - ) - - # No taxes, so tax_result would be empty - self.assertFalse(taxes, "Product should have no taxes") - - # Without taxes, price should remain base price - base_price = 100.0 - expected_price = 100.0 - - self.assertEqual( - base_price, - expected_price, - msg="Product without tax should have unchanged price", - ) - - def test_oca_get_price_returns_base_without_tax(self): - """Test that OCA _get_price returns base price WITHOUT taxes by default.""" - # This verifies our understanding of OCA behavior - - price_info = self.product_21._get_price( - qty=1.0, - pricelist=self.pricelist, - fposition=False, - ) - - # OCA should return base price (100.0) WITHOUT tax - self.assertAlmostEqual( - price_info["value"], - 100.0, - places=2, - msg="OCA _get_price should return base price without tax", - ) - - # tax_included should be False for price_include=False taxes - self.assertFalse( - price_info.get("tax_included", False), - msg="tax_included should be False when price_include=False", - ) - - def test_oca_get_price_with_included_tax(self): - """Test OCA behavior with price_include=True tax.""" - - price_info = self.product_tax_included._get_price( - qty=1.0, - pricelist=self.pricelist, - fposition=False, - ) - - # With price_include=True, the price should already include tax - # list_price is 121.0 (100 + 21%) - self.assertAlmostEqual( - price_info["value"], - 121.0, - places=2, - msg="Price with included tax should be 121.0", - ) - - # tax_included should be True - self.assertTrue( - price_info.get("tax_included", False), - msg="tax_included should be True when price_include=True", - ) - - def test_compute_all_with_multiple_taxes(self): - """Test tax calculation with multiple taxes.""" - # Create product with both 21% and 10% taxes - product_multi = self.env["product.product"].create( - { - "name": "Product With Multiple Taxes", - "list_price": 100.0, - "categ_id": self.category.id, - "taxes_id": [(6, 0, [self.tax_21.id, self.tax_10.id])], - "company_id": self.company.id, - } - ) - - taxes = product_multi.taxes_id.filtered(lambda t: t.company_id == self.company) - - base_price = 100.0 - tax_result = taxes.compute_all( - base_price, - currency=self.env.company.currency_id, - quantity=1.0, - product=product_multi, - ) - - price_with_taxes = tax_result["total_included"] - - # 100 + 21% + 10% = 100 + 21 + 10 = 131.0 - self.assertAlmostEqual( - price_with_taxes, 131.0, places=2, msg="100 + 21% + 10% should equal 131.0" - ) - - def test_compute_all_with_fiscal_position(self): - """Test tax calculation with fiscal position mapping.""" - # Create fiscal position that maps 21% to 10% - fiscal_position = self.env["account.fiscal.position"].create( - { - "name": "Test Fiscal Position", - "company_id": self.company.id, - } - ) - self.env["account.fiscal.position.tax"].create( - { - "position_id": fiscal_position.id, - "tax_src_id": self.tax_21.id, - "tax_dest_id": self.tax_10.id, - } - ) - - # Get taxes and apply fiscal position - taxes = self.product_21.taxes_id.filtered( - lambda t: t.company_id == self.company - ) - mapped_taxes = fiscal_position.map_tax(taxes) - - # Should be mapped to 10% tax - self.assertEqual(len(mapped_taxes), 1) - self.assertEqual(mapped_taxes[0].id, self.tax_10.id) - - base_price = 100.0 - tax_result = mapped_taxes.compute_all( - base_price, - currency=self.env.company.currency_id, - quantity=1.0, - product=self.product_21, - ) - - price_with_tax = tax_result["total_included"] - - # Should be 110.0 (10% instead of 21%) - self.assertAlmostEqual( - price_with_tax, 110.0, places=2, msg="Fiscal position should map to 10% tax" - ) - - def test_tax_amount_details(self): - """Test that compute_all provides detailed tax breakdown.""" - taxes = self.product_21.taxes_id.filtered( - lambda t: t.company_id == self.company - ) - - base_price = 100.0 - tax_result = taxes.compute_all( - base_price, - currency=self.env.company.currency_id, - quantity=1.0, - product=self.product_21, - ) - - # Verify structure of tax_result - self.assertIn("total_included", tax_result) - self.assertIn("total_excluded", tax_result) - self.assertIn("taxes", tax_result) - - # total_excluded should be base price - self.assertAlmostEqual(tax_result["total_excluded"], 100.0, places=2) - - # total_included should be base + tax - self.assertAlmostEqual(tax_result["total_included"], 121.0, places=2) - - # taxes should contain tax details - self.assertEqual(len(tax_result["taxes"]), 1) - tax_detail = tax_result["taxes"][0] - self.assertAlmostEqual(tax_detail["amount"], 21.0, places=2) - - def test_zero_price_with_tax(self): - """Test tax calculation on free product.""" - free_product = self.env["product.product"].create( - { - "name": "Free Product With Tax", - "list_price": 0.0, - "categ_id": self.category.id, - "taxes_id": [(6, 0, [self.tax_21.id])], - "company_id": self.company.id, - } - ) - - taxes = free_product.taxes_id.filtered(lambda t: t.company_id == self.company) - - base_price = 0.0 - tax_result = taxes.compute_all( - base_price, - currency=self.env.company.currency_id, - quantity=1.0, - product=free_product, - ) - - price_with_tax = tax_result["total_included"] - - # 0 + 21% = 0 - self.assertAlmostEqual( - price_with_tax, - 0.0, - places=2, - msg="Free product with tax should still be free", - ) - - def test_high_precision_price_with_tax(self): - """Test tax calculation with high precision prices.""" - precise_product = self.env["product.product"].create( - { - "name": "Precise Price Product", - "list_price": 99.99, - "categ_id": self.category.id, - "taxes_id": [(6, 0, [self.tax_21.id])], - "company_id": self.company.id, - } - ) - - taxes = precise_product.taxes_id.filtered( - lambda t: t.company_id == self.company - ) - - base_price = 99.99 - tax_result = taxes.compute_all( - base_price, - currency=self.env.company.currency_id, - quantity=1.0, - product=precise_product, - ) - - price_with_tax = tax_result["total_included"] - - # 99.99 + 21% = 120.9879 ≈ 120.99 - expected = 99.99 * 1.21 - self.assertAlmostEqual( - price_with_tax, - expected, - places=2, - msg=f"Expected {expected}, got {price_with_tax}", - )