diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 418e309..b1da0c3 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -49,39 +49,39 @@ Este repositorio contiene addons personalizados y modificados de Odoo 18.0. El p 1. **Estructura de carpeta i18n/**: - ``` - addon_name/ - ├── i18n/ - │ ├── es.po # Español (obligatorio) - │ ├── eu.po # Euskera (obligatorio) - │ └── addon_name.pot # Template (generado) - ``` + ``` + addon_name/ + ├── i18n/ + │ ├── es.po # Español (obligatorio) + │ ├── eu.po # Euskera (obligatorio) + │ └── addon_name.pot # Template (generado) + ``` 2. **NO usar `_()` en definiciones de campos a nivel de módulo**: - ```python - # ❌ INCORRECTO - causa warnings - from odoo import _ - name = fields.Char(string=_("Name")) + ```python + # ❌ INCORRECTO - causa warnings + from odoo import _ + name = fields.Char(string=_("Name")) - # ✅ CORRECTO - traducción se maneja por .po files - name = fields.Char(string="Name") - ``` + # ✅ CORRECTO - traducción se maneja por .po files + name = fields.Char(string="Name") + ``` 3. **Usar `_()` solo en métodos y código ejecutable**: - ```python - def action_confirm(self): - message = _("Confirmed successfully") - return {'warning': {'message': message}} - ``` + ```python + def action_confirm(self): + message = _("Confirmed successfully") + return {'warning': {'message': message}} + ``` 4. **Generar/actualizar traducciones**: - ```bash - # Exportar términos a traducir - Pedir al usuario generar a través de UI, no sabemos el método correcto para exportar SÓLO las cadenas del addon sin incluir todo el sistema. - ``` + ```bash + # Exportar términos a traducir + Pedir al usuario generar a través de UI, no sabemos el método correcto para exportar SÓLO las cadenas del addon sin incluir todo el sistema. + ``` Usar sólo polib y apend cadenas en los archivos .po, msmerge corrompe los archivos. @@ -147,7 +147,7 @@ addons-cm/ ### Local Development ```bash -# Iniciar entorno (puertos: 8070=web, 8073=longpolling) +# Iniciar entorno docker-compose up -d # Actualizar addon @@ -158,37 +158,20 @@ docker-compose logs -f odoo # Ejecutar tests docker-compose exec odoo odoo -d odoo --test-enable --stop-after-init -u addon_name - -# Acceder a shell de Odoo -docker-compose exec odoo bash - -# Acceder a PostgreSQL -docker-compose exec db psql -U odoo -d odoo ```` ### Quality Checks ```bash -# Ejecutar todos los checks (usa .pre-commit-config.yaml) +# Ejecutar todos los checks pre-commit run --all-files -# O usar Makefile (ver `make help` para todos los comandos) -make lint # Solo linting (pre-commit) -make format # Formatear código (black + isort) -make check-format # Verificar formateo sin modificar -make flake8 # Ejecutar flake8 -make pylint # Ejecutar pylint (todos) -make pylint-required # Solo verificaciones mandatorias -make clean # Limpiar archivos temporales +# O usar Makefile +make lint # Solo linting +make format # Formatear código +make check-addon # Verificar addon específico ``` -### Tools Configuration - -- **black**: Line length 88, target Python 3.10+ (ver `pyproject.toml`) -- **isort**: Profile black, sections: STDLIB > THIRDPARTY > ODOO > ODOO_ADDONS > FIRSTPARTY > LOCALFOLDER -- **flake8**: Ver `.flake8` para reglas específicas -- **pylint**: Configurado para Odoo con `pylint-odoo` plugin - ### Testing - Tests en `tests/` de cada addon @@ -196,37 +179,6 @@ make clean # Limpiar archivos temporales - Herencia: `odoo.tests.common.TransactionCase` - Ejecutar: `--test-enable` flag -## Critical Architecture Patterns - -### Product Variants Architecture - -**IMPORTANTE**: Los campos de lógica de negocio SIEMPRE van en `product.product` (variantes), no en `product.template`: - -```python -# ✅ CORRECTO - Lógica en product.product -class ProductProduct(models.Model): - _inherit = 'product.product' - - last_purchase_price_updated = fields.Boolean(default=False) - list_price_theoritical = fields.Float(default=0.0) - - def _compute_theoritical_price(self): - for product in self: - # Cálculo real por variante - pass - -# ✅ CORRECTO - Template solo tiene campos related -class ProductTemplate(models.Model): - _inherit = 'product.template' - - last_purchase_price_updated = fields.Boolean( - related='product_variant_ids.last_purchase_price_updated', - readonly=False - ) -``` - -**Por qué**: Evita problemas con pricelists y reportes que operan a nivel de variante. Ver `product_sale_price_from_pricelist` como ejemplo. - ## Common Patterns ### Extending Models @@ -281,35 +233,6 @@ return { } ``` -### Logging Pattern - -```python -import logging - -_logger = logging.getLogger(__name__) - -# En métodos de cálculo de precios, usar logging detallado: -_logger.info( - "[PRICE DEBUG] Product %s [%s]: base_price=%.2f, tax_amount=%.2f", - product.default_code or product.name, - product.id, - base_price, - tax_amount, -) -``` - -### Price Calculation Pattern - -```python -# Usar product_get_price_helper para cálculos consistentes -partial_price = product._get_price(qty=1, pricelist=pricelist) -base_price = partial_price.get('value', 0.0) or 0.0 - -# Siempre validar taxes -if not product.taxes_id: - raise UserError(_("No taxes defined for product %s") % product.name) -``` - ## Dependencies Management ### OCA Dependencies (`oca_dependencies.txt`) @@ -364,22 +287,7 @@ access_model_user,model.name.user,model_model_name,base.group_user,1,1,1,0 ### Price Calculation **Problem**: Prices not updating from pricelist -**Solution**: - -1. Use `product_sale_price_from_pricelist` with proper configuration -2. Set pricelist in Settings > Sales > Automatic Price Configuration -3. Ensure `last_purchase_price_compute_type` is NOT set to `manual_update` -4. Verify product has taxes configured (required for price calculation) - -### Product Variant Issues - -**Problem**: Computed fields not working in pricelists/reports -**Solution**: Move business logic from `product.template` to `product.product` and use `related` fields in template - -### Manifest Dependencies - -**Problem**: Module not loading, dependency errors -**Solution**: Check both `__manifest__.py` depends AND `oca_dependencies.txt` for OCA repos +**Solution**: Use `product_sale_price_from_pricelist` with proper configuration ## Testing Guidelines @@ -399,18 +307,18 @@ access_model_user,model.name.user,model_model_name,base.group_user,1,1,1,0 ```javascript odoo.define("module.tour", function (require) { - "use strict"; - var tour = require("web_tour.tour"); - tour.register( - "tour_name", - { - test: true, - url: "/web", - }, - [ - // Tour steps - ], - ); + "use strict"; + var tour = require("web_tour.tour"); + tour.register( + "tour_name", + { + test: true, + url: "/web", + }, + [ + // Tour steps + ], + ); }); ``` @@ -456,41 +364,11 @@ Cada addon debe tener un README.md con: 7. **Technical Details**: Modelos, campos, métodos 8. **Translations**: Estado de traducciones (si aplica) -### **manifest**.py Structure - -Todos los addons custom deben seguir esta estructura: - -```python -# Copyright YEAR - Today AUTHOR -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -{ # noqa: B018 - "name": "Addon Name", - "version": "18.0.X.Y.Z", # X=major, Y=minor, Z=patch - "category": "category_name", - "summary": "Short description", - "author": "Odoo Community Association (OCA), Your Company", - "maintainers": ["maintainer_github"], - "website": "https://github.com/OCA/repo", - "license": "AGPL-3", - "depends": [ - "base", - # Lista ordenada alfabéticamente - ], - "data": [ - "security/ir.model.access.csv", - "views/actions.xml", - "views/menu.xml", - "views/model_views.xml", - ], -} -``` - ### Code Comments - Docstrings en clases y métodos públicos - Comentarios inline para lógica compleja - TODOs con contexto completo -- Logging detallado en operaciones de precios/descuentos ## Version Control @@ -519,61 +397,6 @@ Tags: `[ADD]`, `[FIX]`, `[IMP]`, `[REF]`, `[REM]`, `[I18N]`, `[DOC]` - Indexes en campos frecuentemente buscados - Avoid N+1 queries con `prefetch` -## Key Business Features - -### Eskaera System (website_sale_aplicoop) - -Sistema completo de compras colaborativas para cooperativas de consumo: - -- **Group Orders**: Pedidos grupales con estados (draft → confirmed → collected → completed) -- **Separate Carts**: Carrito independiente por miembro y por grupo -- **Cutoff Dates**: Validación de fechas límite para pedidos -- **Pickup Management**: Gestión de días de recogida -- **Multi-language**: ES, EU, CA, GL, PT, FR, IT -- **Member Tracking**: Gestión de miembros activos/inactivos por grupo - -**Flujo típico**: - -1. Administrador crea grupo order con fechas (collection, cutoff, pickup) -2. Miembros añaden productos a su carrito individual -3. Sistema valida cutoff date antes de confirmar -4. Notificaciones automáticas al cambiar estados -5. Tracking de fulfillment por miembro - -Ver [website_sale_aplicoop/README.md](../website_sale_aplicoop/README.md) para detalles. - -### Triple Discount System - -Todos los documentos de compra/venta soportan 3 descuentos consecutivos: - -```python -# Ejemplo: Precio = 600.00 -# Desc. 1 = 50% → 300.00 -# Desc. 2 = 50% → 150.00 -# Desc. 3 = 50% → 75.00 -``` - -**IMPORTANTE**: Usar `account_invoice_triple_discount_readonly` para evitar bug de acumulación de descuentos. - -### Automatic Pricing System - -`product_sale_price_from_pricelist` calcula automáticamente precio de venta basado en: - -- Último precio de compra (`last_purchase_price_received`) -- Tipo de cálculo de descuentos (`last_purchase_price_compute_type`) -- Pricelist configurado en Settings -- Impuestos del producto - -**Configuración crítica**: - -```python -# En Settings > Sales > Automatic Price Configuration -product_pricelist_automatic = [ID_pricelist] - -# En producto -last_purchase_price_compute_type != "manual_update" # Para auto-cálculo -``` - ## Resources - **OCA Guidelines**: https://github.com/OCA/odoo-community.org/blob/master/website/Contribution/CONTRIBUTING.rst @@ -583,6 +406,6 @@ last_purchase_price_compute_type != "manual_update" # Para auto-cálculo --- -**Last Updated**: 2026-02-16 +**Last Updated**: 2026-02-12 **Odoo Version**: 18.0 **Python Version**: 3.10+ diff --git a/product_price_category_supplier/README.rst b/product_price_category_supplier/README.rst deleted file mode 100644 index 90f2085..0000000 --- a/product_price_category_supplier/README.rst +++ /dev/null @@ -1,189 +0,0 @@ -====================================== -Product Price Category - Supplier -====================================== - -Extiende ``res.partner`` (proveedores) con un campo de categoría de precio por -defecto y permite actualizar masivamente todos los productos de un proveedor con -esta categoría mediante un wizard. - -Funcionalidades -=============== - -- **Campo en Proveedores**: Añade campo ``default_price_category_id`` en la - pestaña "Compras" (Purchases) de res.partner -- **Actualización Masiva**: Botón que abre wizard modal para confirmar - actualización de todos los productos del proveedor -- **Columna Configurable**: Campo oculto en vista tree de partner, - visible/configurable desde menú de columnas -- **Control de Permisos**: Acceso restringido a - ``sales_team.group_sale_manager`` (Gestores de Ventas) - -Dependencias -============ - -- ``product_price_category`` (OCA addon base) -- ``product_pricelists_margins_custom`` (Addon del proyecto) -- ``sales_team`` (Odoo core) - -Instalación -=========== - -.. code-block:: bash - - docker-compose exec -T odoo odoo -d odoo -u product_price_category_supplier --stop-after-init - -Flujo de Uso -============ - -1. Abrir formulario de un **Proveedor** (res.partner) -2. Ir a pestaña **"Compras"** (Purchases) -3. En sección **"Price Category Settings"**, seleccionar **categoría de precio - por defecto** -4. Hacer clic en botón **"Apply to All Products"** -5. Se abre modal de confirmación mostrando: - - - Nombre del proveedor - - Categoría de precio a aplicar - - Cantidad de productos que serán actualizados - -6. Hacer clic **"Confirm"** para ejecutar actualización en bulk -7. Notificación de éxito mostrando cantidad de productos actualizados - -Campos -====== - -res.partner ------------ - -- ``default_price_category_id`` (Many2one → product.price.category) - - - Ubicación: Pestaña "Compras", sección "Price Category Settings" - - Obligatorio: No - - Ayuda: "Default price category for products from this supplier" - - Visible en tree: Oculto por defecto (column_invisible=1), configurable vía menú - -Modelos -======= - -wizard.update.product.category (Transient) -------------------------------------------- - -- ``partner_id`` (Many2one → res.partner) - Readonly -- ``partner_name`` (Char, related to partner_id.name) - Readonly -- ``price_category_id`` (Many2one → product.price.category) - Readonly -- ``product_count`` (Integer) - Cantidad de productos a actualizar - Readonly - -**Métodos**: - -- ``action_confirm()`` - Realiza bulk update de productos y retorna notificación - -Vistas -====== - -res.partner ------------ - -- **Form**: Campo + botón en pestaña "Compras" -- **Tree**: Campo oculto (column_invisible=1) - -wizard.update.product.category ------------------------------- - -- **Form**: Formulario modal con información de confirmación y botones - -Seguridad -========= - -Acceso al wizard restringido a grupo ``sales_team.group_sale_manager``: - -- Lectura: Sí -- Escritura: Sí -- Creación: Sí -- Borrado: Sí - -Comportamiento -============== - -Actualización de Productos --------------------------- - -Cuando el usuario confirma la acción: - -1. Se buscan todos los productos (``product.template``) donde: - - - ``default_supplier_id = partner_id`` (este proveedor es su proveedor por - defecto) - -2. Se actualizan en bulk (single SQL UPDATE) con: - - - ``price_category_id = default_price_category_id`` - -3. Se retorna notificación de éxito: - - - "X products updated with category 'CATEGORY_NAME'." - -**Nota**: La actualización SOBRESCRIBE cualquier ``price_category_id`` -existente en los productos. - -Extensión Futura -================ - -Para implementar defaults automáticos al crear productos desde un proveedor: - -.. code-block:: python - - # En models/product_template.py - @api.model_create_multi - def create(self, vals_list): - # Si se proporciona default_supplier_id sin price_category_id, - # usar default_price_category_id del proveedor - for vals in vals_list: - if vals.get('default_supplier_id') and not vals.get('price_category_id'): - supplier = self.env['res.partner'].browse(vals['default_supplier_id']) - if supplier.default_price_category_id: - vals['price_category_id'] = supplier.default_price_category_id.id - return super().create(vals_list) - -Traducciones -============ - -Para añadir/actualizar traducciones: - -.. code-block:: bash - - # Exportar strings - docker-compose exec -T odoo odoo -d odoo \ - --addons-path=/mnt/extra-addons/product_price_category_supplier \ - -i product_price_category_supplier \ - --i18n-export=/tmp/product_price_category_supplier.pot \ - --stop-after-init - - # Mergar en archivos .po existentes - cd product_price_category_supplier/i18n - for lang in es eu; do - msgmerge -U ${lang}.po product_price_category_supplier.pot - done - -Testing -======= - -Ejecutar tests: - -.. code-block:: bash - - docker-compose exec -T odoo odoo -d odoo \ - -i product_price_category_supplier \ - --test-enable --stop-after-init - -Créditos -======== - -Autor ------ - -Your Company - 2026 - -Licencia --------- - -AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) diff --git a/product_sale_price_from_pricelist/models/product_pricelist_item.py b/product_sale_price_from_pricelist/models/product_pricelist_item.py index 22f1eee..ba2e9ad 100644 --- a/product_sale_price_from_pricelist/models/product_pricelist_item.py +++ b/product_sale_price_from_pricelist/models/product_pricelist_item.py @@ -2,8 +2,7 @@ # @author Santi Noreña () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import fields -from odoo import models +from odoo import fields, models class ProductPricelistItem(models.Model): @@ -14,8 +13,8 @@ class ProductPricelistItem(models.Model): ondelete={"last_purchase_price": "set default"}, ) - def _compute_price(self, product, quantity, uom, date, currency=None): - result = super()._compute_price(product, quantity, uom, date, currency) + def _compute_price(self, product, qty, uom, date, currency=None): + result = super()._compute_price(product, qty, uom, date, currency) if self.compute_price == "formula" and self.base == "last_purchase_price": result = product.sudo().last_purchase_price_received return result diff --git a/website_sale_aplicoop/__manifest__.py b/website_sale_aplicoop/__manifest__.py index 74dd6ef..bd9883f 100644 --- a/website_sale_aplicoop/__manifest__.py +++ b/website_sale_aplicoop/__manifest__.py @@ -21,8 +21,6 @@ "data": [ # Datos: Grupos propios "data/groups.xml", - # Datos: Menús del website - "data/website_menus.xml", # Vistas de seguridad "security/ir.model.access.csv", "security/record_rules.xml", diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index 6608c8a..19a0ce3 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -293,7 +293,7 @@ class AplicoopWebsiteSale(WebsiteSale): # Identificar categorías raíz (sin padre en la lista) y organizar jerarquía roots = [] - for _cat_id, cat_info in category_map.items(): + for cat_id, cat_info in category_map.items(): parent_id = cat_info["parent_id"] # Si el padre no está en la lista de categorías disponibles, es una raíz @@ -313,406 +313,6 @@ class AplicoopWebsiteSale(WebsiteSale): sort_hierarchy(roots) return roots - # ========== PHASE 1: HELPER METHODS FOR VALIDATION AND CONFIGURATION ========== - - def _resolve_pricelist(self): - """Resolve the pricelist to use for pricing. - - Resolution order: - 1. Aplicoop configured pricelist (from settings) - 2. Website current pricelist - 3. First active pricelist (fallback) - - Returns: - product.pricelist record or False if none found - """ - pricelist = None - - # Try to get configured Aplicoop pricelist first - try: - aplicoop_pricelist_id = ( - request.env["ir.config_parameter"] - .sudo() - .get_param("website_sale_aplicoop.pricelist_id") - ) - if aplicoop_pricelist_id: - pricelist = request.env["product.pricelist"].browse( - int(aplicoop_pricelist_id) - ) - if pricelist.exists(): - _logger.info( - "_resolve_pricelist: Using configured Aplicoop pricelist: %s (id=%s)", - pricelist.name, - pricelist.id, - ) - return pricelist - else: - _logger.warning( - "_resolve_pricelist: Configured Aplicoop pricelist (id=%s) not found", - aplicoop_pricelist_id, - ) - except Exception as err: - _logger.warning( - "_resolve_pricelist: Error getting Aplicoop pricelist: %s", str(err) - ) - - # Fallback to website pricelist - try: - pricelist = request.website._get_current_pricelist() - if pricelist: - _logger.info( - "_resolve_pricelist: Using website pricelist: %s (id=%s)", - pricelist.name, - pricelist.id, - ) - return pricelist - except Exception as err: - _logger.warning( - "_resolve_pricelist: Error getting website pricelist: %s", str(err) - ) - - # Final fallback to first active pricelist - pricelist = request.env["product.pricelist"].search( - [("active", "=", True)], limit=1 - ) - if pricelist: - _logger.info( - "_resolve_pricelist: Using first active pricelist: %s (id=%s)", - pricelist.name, - pricelist.id, - ) - return pricelist - - _logger.error( - "_resolve_pricelist: ERROR - No pricelist found! Pricing may fail." - ) - return False - - def _validate_confirm_request(self, data): - """Validate all requirements for confirm order request. - - Validates: - - order_id exists and is valid integer - - group.order exists and is in open state - - user has associated partner_id - - items list is not empty - - Args: - data: dict with 'order_id' and 'items' keys - - Returns: - tuple: (order_id, group_order, current_user) - - Raises: - ValueError: if any validation fails - """ - # Validate order_id - order_id = data.get("order_id") - if not order_id: - raise ValueError("order_id is required") from None - - try: - order_id = int(order_id) - except (ValueError, TypeError) as err: - raise ValueError(f"Invalid order_id format: {order_id}") from err - - # Verify that the group.order exists - group_order = request.env["group.order"].browse(order_id) - if not group_order.exists(): - raise ValueError(f"Order {order_id} not found") from None - - # Verify that the order is in open state - if group_order.state != "open": - raise ValueError("Order is not available (not in open state)") from None - - # Validate user has partner_id - current_user = request.env.user - if not current_user.partner_id: - raise ValueError("User has no associated partner") from None - - # Validate items - items = data.get("items", []) - if not items: - raise ValueError("No items in cart") from None - - _logger.info( - "_validate_confirm_request: Valid request for order %d with %d items", - order_id, - len(items), - ) - - return order_id, group_order, current_user, items - - def _validate_draft_request(self, data): - """Validate all requirements for draft order request. - - Validates: - - order_id exists and is valid integer - - group.order exists - - user has associated partner_id - - items list is not empty - - Args: - data: dict with 'order_id' and 'items' keys - - Returns: - tuple: (order_id, group_order, current_user, items, merge_action, existing_draft_id) - - Raises: - ValueError: if any validation fails - """ - # Validate order_id - order_id = data.get("order_id") - if not order_id: - raise ValueError("order_id is required") - - try: - order_id = int(order_id) - except (ValueError, TypeError) as err: - raise ValueError(f"Invalid order_id format: {order_id}") from err - - # Verify that the group.order exists - group_order = request.env["group.order"].browse(order_id) - if not group_order.exists(): - raise ValueError(f"Order {order_id} not found") - - # Validate user has partner_id - current_user = request.env.user - if not current_user.partner_id: - raise ValueError("User has no associated partner") - - # Validate items - items = data.get("items", []) - if not items: - raise ValueError("No items in cart") - - # Get optional merge/replace parameters - merge_action = data.get("merge_action") - existing_draft_id = data.get("existing_draft_id") - - _logger.info( - "_validate_draft_request: Valid request for order %d with %d items (merge_action=%s)", - order_id, - len(items), - merge_action, - ) - - return ( - order_id, - group_order, - current_user, - items, - merge_action, - existing_draft_id, - ) - - def _validate_confirm_json(self, data): - """Validate JSON data and order for confirm_eskaera endpoint. - - Validates: - - order_id is present and valid integer - - group.order exists and is in 'open' state - - user has associated partner_id - - items list is not empty - - Args: - data: dict with 'order_id' and 'items' keys - - Returns: - tuple: (order_id, group_order, current_user, items, is_delivery) - - Raises: - ValueError: if any validation fails - """ - # Validate order_id - order_id = data.get("order_id") - if not order_id: - raise ValueError("order_id is required") - - try: - order_id = int(order_id) - except (ValueError, TypeError) as err: - raise ValueError(f"Invalid order_id format: {order_id}") from err - - # Verify that the order exists - group_order = request.env["group.order"].browse(order_id) - if not group_order.exists(): - raise ValueError(f"Order {order_id} not found") - - # Verify that the order is open - if group_order.state != "open": - raise ValueError(f"Order is {group_order.state}") - - # Validate user has partner_id - current_user = request.env.user - if not current_user.partner_id: - raise ValueError("User has no associated partner") - - # Validate items - items = data.get("items", []) - if not items: - raise ValueError("No items in cart") - - # Get delivery flag - is_delivery = data.get("is_delivery", False) - - _logger.info( - "_validate_confirm_json: Valid request for order %d with %d items (is_delivery=%s)", - order_id, - len(items), - is_delivery, - ) - - return order_id, group_order, current_user, items, is_delivery - - def _process_cart_items(self, items, group_order): - """Process cart items and build sale.order line data. - - Args: - items: list of item dicts with product_id, quantity, product_price - group_order: group.order record for context - - Returns: - list of (0, 0, line_dict) tuples ready for sale.order creation - - Raises: - ValueError: if no valid items after processing - """ - 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 = request.env["product.product"].browse(product_id) - if not product.exists(): - _logger.warning( - "_process_cart_items: Product %d does not exist", product_id - ) - continue - - # Get product name in user's language context - product_in_lang = product.with_context(lang=request.env.lang) - 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 - } - _logger.info("_process_cart_items: Adding line: %s", line_data) - sale_order_lines.append((0, 0, line_data)) - except (ValueError, TypeError) as e: - _logger.warning( - "_process_cart_items: Error processing item %s: %s", - item, - str(e), - ) - continue - - if not sale_order_lines: - raise ValueError("No valid items in cart") - - _logger.info( - "_process_cart_items: Created %d valid lines", len(sale_order_lines) - ) - return sale_order_lines - - def _build_confirmation_message(self, sale_order, group_order, is_delivery): - """Build localized confirmation message for confirm_eskaera. - - Translates message and pickup/delivery info according to user's language. - Handles day names and date formatting. - - Args: - sale_order: sale.order record just created - group_order: group.order record - is_delivery: boolean indicating if home delivery - - Returns: - dict with message, pickup_day, pickup_date, pickup_day_index - """ - # Get pickup day index - try: - pickup_day_index = int(group_order.pickup_day) - except Exception: - pickup_day_index = None - - # Initialize translatable strings - 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}" - ) - - # Get translated day names - 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 = "" - - # 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") - # For delivery, use the next day's name - if pickup_day_index is not None: - try: - day_names = self._get_day_names(env=request.env) - # Get the next day's name for delivery - next_day_index = (pickup_day_index + 1) % 7 - pickup_day_name = day_names[next_day_index] - except Exception: - pickup_day_name = "" - else: - 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") - - # Build final message with correct label and date based on delivery or pickup - message = base_message - label_to_use = delivery_label if is_delivery else pickup_label - if pickup_day_name and pickup_date_str: - message = ( - f"{message}\n\n\n{label_to_use}: {pickup_day_name} ({pickup_date_str})" - ) - elif pickup_day_name: - message = f"{message}\n\n\n{label_to_use}: {pickup_day_name}" - elif pickup_date_str: - message = f"{message}\n\n\n{label_to_use}: {pickup_date_str}" - - # Log for translation debugging - try: - _logger.info( - "_build_confirmation_message: lang=%s, message=%s", - request.env.lang, - message, - ) - except Exception: - _logger.info("_build_confirmation_message: message logging failed") - - return { - "message": message, - "pickup_day": pickup_day_name, - "pickup_date": pickup_date_str, - "pickup_day_index": pickup_day_index, - } - @http.route(["/eskaera"], type="http", auth="user", website=True) def eskaera_list(self, **post): """Página de pedidos de grupo abiertos esta semana. @@ -759,6 +359,7 @@ class AplicoopWebsiteSale(WebsiteSale): Soporta búsqueda y filtrado por categoría. """ group_order = request.env["group.order"].browse(order_id) + current_user = request.env.user if not group_order.exists(): return request.redirect("/eskaera") @@ -887,8 +488,8 @@ class AplicoopWebsiteSale(WebsiteSale): category_id, len(products), ) - except (ValueError, TypeError) as e: - _logger.warning("eskaera_shop: Invalid category filter: %s", str(e)) + except (ValueError, TypeError): + pass # Prepare supplier info dict: {product.id: 'Supplier (City)'} product_supplier_info = {} @@ -903,17 +504,71 @@ class AplicoopWebsiteSale(WebsiteSale): # Get pricelist and calculate prices with taxes using Odoo's pricelist system _logger.info("eskaera_shop: Starting price calculation for order %d", order_id) - pricelist = self._resolve_pricelist() + pricelist = None - # Log pricelist selection status - if pricelist: - _logger.info( - "eskaera_shop: Using pricelist %s (id=%s, currency=%s)", - pricelist.name, - pricelist.id, - pricelist.currency_id.name if pricelist.currency_id else "None", + # Try to get configured aplicoop pricelist first + try: + aplicoop_pricelist_id = ( + request.env["ir.config_parameter"] + .sudo() + .get_param("website_sale_aplicoop.pricelist_id") ) - else: + if aplicoop_pricelist_id: + pricelist = request.env["product.pricelist"].browse( + int(aplicoop_pricelist_id) + ) + if pricelist.exists(): + _logger.info( + "eskaera_shop: Using configured Aplicoop pricelist: %s (id=%s, currency=%s)", + pricelist.name, + pricelist.id, + pricelist.currency_id.name if pricelist.currency_id else "None", + ) + else: + pricelist = None + _logger.warning( + "eskaera_shop: Configured Aplicoop pricelist (id=%s) not found", + aplicoop_pricelist_id, + ) + except Exception as e: + _logger.warning( + "eskaera_shop: Error getting configured Aplicoop pricelist: %s", + str(e), + ) + + # Fallback to website pricelist + if not pricelist: + try: + pricelist = request.website._get_current_pricelist() + _logger.info( + "eskaera_shop: Using website pricelist: %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), + ) + + # Final fallback to any active pricelist + if not pricelist: + pricelist = request.env["product.pricelist"].search( + [("active", "=", True)], limit=1 + ) + if pricelist: + _logger.info( + "eskaera_shop: Using first active pricelist as fallback: %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." ) @@ -1140,8 +795,57 @@ class AplicoopWebsiteSale(WebsiteSale): ) pricelist = None - # Resolve pricelist using centralized helper - pricelist = self._resolve_pricelist() + # Try to get configured aplicoop pricelist first + try: + aplicoop_pricelist_id = ( + request.env["ir.config_parameter"] + .sudo() + .get_param("website_sale_aplicoop.pricelist_id") + ) + if aplicoop_pricelist_id: + pricelist = request.env["product.pricelist"].browse( + int(aplicoop_pricelist_id) + ) + if pricelist.exists(): + _logger.info( + "add_to_eskaera_cart: Using configured Aplicoop pricelist: %s (id=%s)", + pricelist.name, + pricelist.id, + ) + else: + pricelist = None + except Exception as e: + _logger.warning( + "add_to_eskaera_cart: Error getting configured Aplicoop pricelist: %s", + str(e), + ) + + # Fallback to website pricelist + if not pricelist: + try: + pricelist = request.website._get_current_pricelist() + _logger.info( + "add_to_eskaera_cart: Using website 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 website pricelist: %s", + str(e), + ) + + # Final fallback to any active pricelist + if not pricelist: + pricelist = request.env["product.pricelist"].search( + [("active", "=", True)], limit=1 + ) + if pricelist: + _logger.info( + "add_to_eskaera_cart: Using first active pricelist: %s (id=%s)", + pricelist.name, + pricelist.id, + ) if not pricelist: _logger.error( @@ -1367,6 +1071,7 @@ class AplicoopWebsiteSale(WebsiteSale): # 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 if not items: return request.make_response( @@ -1881,7 +1586,7 @@ class AplicoopWebsiteSale(WebsiteSale): # Find if product already exists in draft existing_line = existing_draft.order_line.filtered( - lambda line: line.product_id.id == product_id + lambda l: l.product_id.id == product_id ) if existing_line: @@ -1919,7 +1624,7 @@ class AplicoopWebsiteSale(WebsiteSale): 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 %s", 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( @@ -2035,32 +1740,77 @@ class AplicoopWebsiteSale(WebsiteSale): _logger.info("confirm_eskaera data received: %s", data) - # Validate request using helper - try: - ( - order_id, - group_order, - current_user, - items, - is_delivery, - ) = self._validate_confirm_json(data) - except ValueError as e: - _logger.warning("confirm_eskaera: Validation error: %s", str(e)) + # Validate order_id + order_id = data.get("order_id") + if not order_id: + _logger.warning("confirm_eskaera: order_id missing") return request.make_response( - json.dumps({"error": str(e)}), + 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) + return request.make_response( + json.dumps({"error": f"Invalid order_id format: {order_id}"}), + [("Content-Type", "application/json")], + status=400, + ) + + _logger.info("order_id: %d", order_id) + + # Verify that the order exists + group_order = request.env["group.order"].browse(order_id) + if not group_order.exists(): + _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, + ) + + # 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, + ) + return request.make_response( + 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) - # Process cart items using helper - try: - sale_order_lines = self._process_cart_items(items, group_order) - except ValueError as e: - _logger.warning("confirm_eskaera: Cart processing error: %s", str(e)) + # 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 + ) return request.make_response( - json.dumps({"error": str(e)}), + 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) + if not items: + _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, ) @@ -2089,6 +1839,47 @@ class AplicoopWebsiteSale(WebsiteSale): ) 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 = request.env["product.product"].browse(product_id) + if not product.exists(): + _logger.warning( + "confirm_eskaera: Product %d does not exist", product_id + ) + continue + + # Get product name in user's language context + product_in_lang = product.with_context(lang=request.env.lang) + 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 + } + _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) + ) + continue + + if not sale_order_lines: + _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, + ) + # Get pickup date and delivery info from group order # If delivery, use delivery_date; otherwise use pickup_date commitment_date = None @@ -2150,14 +1941,64 @@ class AplicoopWebsiteSale(WebsiteSale): _logger.error("sale_order_lines: %s", sale_order_lines) raise - # Build confirmation message using helper - message_data = self._build_confirmation_message( - sale_order, group_order, is_delivery - ) - message = message_data["message"] - pickup_day_name = message_data["pickup_day"] - pickup_date_str = message_data["pickup_date"] - pickup_day_index = message_data["pickup_day_index"] + # Build a localized confirmation message on the server so the + # client only needs to display the final string. Use `_()` to + # mark strings for translation and `_get_day_names()` to obtain + # the translated day name according to the user's language. + try: + pickup_day_index = int(group_order.pickup_day) + 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 = "" + + # Add order reference to message + if 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 = "" + + # 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") + # For delivery, use the next day's name + if pickup_day_index is not None: + try: + day_names = self._get_day_names(env=request.env) + # Get the next day's name for delivery + next_day_index = (pickup_day_index + 1) % 7 + pickup_day_name = day_names[next_day_index] + except Exception: + pickup_day_name = "" + else: + 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") + + # Build final message with correct label and date based on delivery or pickup + message = base_message + label_to_use = delivery_label if is_delivery else pickup_label + if pickup_day_name and pickup_date_str: + message = f"{message}\n\n\n{label_to_use}: {pickup_day_name} ({pickup_date_str})" + elif pickup_day_name: + message = f"{message}\n\n\n{label_to_use}: {pickup_day_name}" + elif pickup_date_str: + message = f"{message}\n\n\n{label_to_use}: {pickup_date_str}" response_data = { "success": True, @@ -2269,6 +2110,9 @@ class AplicoopWebsiteSale(WebsiteSale): # 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) + # 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 diff --git a/website_sale_aplicoop/data/website_menus.xml b/website_sale_aplicoop/data/website_menus.xml deleted file mode 100644 index a2a6e90..0000000 --- a/website_sale_aplicoop/data/website_menus.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Eskaera - /eskaera - - 50 - - - diff --git a/website_sale_aplicoop/tests/test_helper_methods_phase1.py b/website_sale_aplicoop/tests/test_helper_methods_phase1.py deleted file mode 100644 index 9284bb2..0000000 --- a/website_sale_aplicoop/tests/test_helper_methods_phase1.py +++ /dev/null @@ -1,353 +0,0 @@ -# Copyright 2026 Criptomart -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) - -""" -Test suite for Phase 1 refactoring helper methods. - -Tests for extracted helper methods that reduce cyclomatic complexity: -- _resolve_pricelist(): Consolidate pricelist resolution logic -- _validate_confirm_request(): Validate confirm order request -- _validate_draft_request(): Validate draft order request -""" - -from datetime import datetime -from datetime import timedelta - -from odoo.tests.common import TransactionCase - - -class TestResolvePricelist(TransactionCase): - """Test _resolve_pricelist() helper method.""" - - def setUp(self): - super().setUp() - self.pricelist_aplicoop = self.env["product.pricelist"].create( - { - "name": "Aplicoop Pricelist", - "currency_id": self.env.company.currency_id.id, - } - ) - - self.pricelist_website = self.env["product.pricelist"].create( - { - "name": "Website Pricelist", - "currency_id": self.env.company.currency_id.id, - } - ) - - self.website = self.env["website"].get_current_website() - self.website.pricelist_id = self.pricelist_website.id - - def test_resolve_pricelist_aplicoop_configured(self): - """Test pricelist resolution when Aplicoop pricelist is configured.""" - # Set Aplicoop pricelist in config - self.env["ir.config_parameter"].sudo().set_param( - "website_sale_aplicoop.pricelist_id", str(self.pricelist_aplicoop.id) - ) - - # When calling _resolve_pricelist, should return Aplicoop pricelist - # Placeholder: will be implemented with actual controller call - - def test_resolve_pricelist_fallback_to_website(self): - """Test fallback to website pricelist when Aplicoop not configured.""" - # Don't set Aplicoop pricelist in config (leave empty) - self.env["ir.config_parameter"].sudo().set_param( - "website_sale_aplicoop.pricelist_id", "" - ) - - # When calling _resolve_pricelist, should return website pricelist - # Placeholder: will be implemented with actual controller call - - def test_resolve_pricelist_fallback_to_first_active(self): - """Test final fallback to first active pricelist.""" - # Remove both configured pricelists - self.env["ir.config_parameter"].sudo().set_param( - "website_sale_aplicoop.pricelist_id", "" - ) - self.website.pricelist_id = False - - # When calling _resolve_pricelist, should return first active pricelist - # Placeholder: will be implemented with actual controller call - - -class TestValidateConfirmRequest(TransactionCase): - """Test _validate_confirm_request() helper method.""" - - def setUp(self): - super().setUp() - self.group = self.env["res.partner"].create( - { - "name": "Test Group", - "is_company": True, - } - ) - - self.member = self.env["res.partner"].create( - { - "name": "Group Member", - "email": "member@test.com", - } - ) - self.group.member_ids = [(4, self.member.id)] - - self.user = self.env["res.users"].create( - { - "name": "Test User", - "login": "testuser@test.com", - "email": "testuser@test.com", - "partner_id": self.member.id, - } - ) - - self.product = self.env["product.product"].create( - { - "name": "Test Product", - "type": "product", - "list_price": 100.0, - } - ) - - self.group_order = self.env["group.order"].create( - { - "name": "Test Order", - "group_ids": [(4, self.group.id)], - "start_date": datetime.now().date(), - "end_date": datetime.now().date() + timedelta(days=7), - "pickup_day": "3", - "cutoff_day": "0", - "state": "open", - } - ) - - def test_validate_confirm_valid_request(self): - """Test validation passes for valid confirm request.""" - _ = { - "order_id": str(self.group_order.id), - "items": [ - { - "product_id": str(self.product.id), - "quantity": 1.0, - "product_price": 100.0, - } - ], - "is_delivery": False, - } - - # Validation should pass without raising exception - # Placeholder: will be implemented with actual controller call - - def test_validate_confirm_missing_order_id(self): - """Test validation fails when order_id missing.""" - _ = { - "items": [{"product_id": "1", "quantity": 1.0}], - } - - # Validation should raise ValueError: "order_id is required" - # Placeholder: will be implemented with actual controller call - - def test_validate_confirm_invalid_order_id(self): - """Test validation fails for invalid order_id format.""" - _ = { - "order_id": "invalid", - "items": [{"product_id": "1", "quantity": 1.0}], - } - - # Validation should raise ValueError with "Invalid order_id format" - # Placeholder: will be implemented with actual controller call - - def test_validate_confirm_nonexistent_order(self): - """Test validation fails when order doesn't exist.""" - _ = { - "order_id": "99999", - "items": [{"product_id": "1", "quantity": 1.0}], - } - - # Validation should raise ValueError with "not found" - # Placeholder: will be implemented with actual controller call - - def test_validate_confirm_closed_order(self): - """Test validation fails when order is closed.""" - self.group_order.state = "confirmed" - - _ = { - "order_id": str(self.group_order.id), - "items": [{"product_id": "1", "quantity": 1.0}], - } - - # Validation should raise ValueError with "not available" - # Placeholder: will be implemented with actual controller call - - def test_validate_confirm_no_items(self): - """Test validation fails when no items provided.""" - _ = { - "order_id": str(self.group_order.id), - "items": [], - } - - # Validation should raise ValueError with "No items in cart" - # Placeholder: will be implemented with actual controller call - - def test_validate_confirm_user_no_partner(self): - """Test validation fails when user has no partner_id.""" - _ = self.env["res.users"].create( - { - "name": "User No Partner", - "login": "nopartner@test.com", - "email": "nopartner@test.com", - } - ) - - _ = { - "order_id": str(self.group_order.id), - "items": [{"product_id": "1", "quantity": 1.0}], - } - - # Validation should raise ValueError with "no associated partner" - # Placeholder: will be implemented with actual controller call - - -class TestValidateDraftRequest(TransactionCase): - """Test _validate_draft_request() helper method.""" - - def setUp(self): - super().setUp() - self.group = self.env["res.partner"].create( - { - "name": "Test Group", - "is_company": True, - } - ) - - self.member = self.env["res.partner"].create( - { - "name": "Group Member", - "email": "member@test.com", - } - ) - self.group.member_ids = [(4, self.member.id)] - - self.user = self.env["res.users"].create( - { - "name": "Test User", - "login": "testuser@test.com", - "email": "testuser@test.com", - "partner_id": self.member.id, - } - ) - - self.product = self.env["product.product"].create( - { - "name": "Test Product", - "type": "product", - "list_price": 100.0, - } - ) - - self.group_order = self.env["group.order"].create( - { - "name": "Test Order", - "group_ids": [(4, self.group.id)], - "start_date": datetime.now().date(), - "end_date": datetime.now().date() + timedelta(days=7), - "pickup_day": "3", - "cutoff_day": "0", - "state": "open", - } - ) - - def test_validate_draft_valid_request(self): - """Test validation passes for valid draft request.""" - _ = { - "order_id": str(self.group_order.id), - "items": [ - { - "product_id": str(self.product.id), - "quantity": 1.0, - "product_price": 100.0, - } - ], - } - - # Validation should pass without raising exception - # Placeholder: will be implemented with actual controller call - - def test_validate_draft_missing_order_id(self): - """Test validation fails when order_id missing.""" - _ = { - "items": [{"product_id": "1", "quantity": 1.0}], - } - - # Validation should raise ValueError: "order_id is required" - # Placeholder: will be implemented with actual controller call - - def test_validate_draft_invalid_order_id(self): - """Test validation fails for invalid order_id.""" - _ = { - "order_id": "invalid", - "items": [{"product_id": "1", "quantity": 1.0}], - } - - # Validation should raise ValueError with "Invalid order_id format" - # Placeholder: will be implemented with actual controller call - - def test_validate_draft_nonexistent_order(self): - """Test validation fails when order doesn't exist.""" - _ = { - "order_id": "99999", - "items": [{"product_id": "1", "quantity": 1.0}], - } - - # Validation should raise ValueError with "not found" - # Placeholder: will be implemented with actual controller call - - def test_validate_draft_no_items(self): - """Test validation fails when no items.""" - _ = { - "order_id": str(self.group_order.id), - "items": [], - } - - # Validation should raise ValueError with "No items in cart" - # Placeholder: will be implemented with actual controller call - - def test_validate_draft_user_no_partner(self): - """Test validation fails when user has no partner.""" - _ = self.env["res.users"].create( - { - "name": "User No Partner", - "login": "nopartner@test.com", - "email": "nopartner@test.com", - } - ) - - _ = { - "order_id": str(self.group_order.id), - "items": [{"product_id": "1", "quantity": 1.0}], - } - - # Validation should raise ValueError with "no associated partner" - # Placeholder: will be implemented with actual controller call - - def test_validate_draft_with_merge_action(self): - """Test validation passes when merge_action is specified.""" - _ = { - "order_id": str(self.group_order.id), - "items": [{"product_id": "1", "quantity": 1.0}], - "merge_action": "merge", - "existing_draft_id": "123", - } - - # Validation should pass and return merge_action and existing_draft_id - # Placeholder: will be implemented with actual controller call - - def test_validate_draft_with_replace_action(self): - """Test validation passes when replace_action is specified.""" - _ = { - "order_id": str(self.group_order.id), - "items": [{"product_id": "1", "quantity": 1.0}], - "merge_action": "replace", - "existing_draft_id": "123", - } - - # Validation should pass and return merge_action and existing_draft_id - # Placeholder: will be implemented with actual controller call diff --git a/website_sale_aplicoop/tests/test_phase2_eskaera_shop.py b/website_sale_aplicoop/tests/test_phase2_eskaera_shop.py deleted file mode 100644 index d4b8ada..0000000 --- a/website_sale_aplicoop/tests/test_phase2_eskaera_shop.py +++ /dev/null @@ -1,286 +0,0 @@ -# Copyright 2026 Criptomart -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) - -""" -Test suite for Phase 2 refactoring of eskaera_shop() method. - -Tests for refactored eskaera_shop using extracted helpers: -- Usage of _resolve_pricelist() instead of inline 3-tier fallback -- Extracted category filtering logic -- Price calculation with pricelist -- Search and category filter functionality -""" - -from datetime import datetime -from datetime import timedelta - -from odoo.tests.common import TransactionCase - - -class TestEskaeraShopobjInit(TransactionCase): - """Test eskaera_shop() initial validation and setup.""" - - def setUp(self): - super().setUp() - self.pricelist = self.env["product.pricelist"].create( - { - "name": "Test Pricelist", - "currency_id": self.env.company.currency_id.id, - } - ) - - self.group = self.env["res.partner"].create( - { - "name": "Test Group", - "is_company": True, - } - ) - - self.member = self.env["res.partner"].create( - { - "name": "Group Member", - "email": "member@test.com", - } - ) - self.group.member_ids = [(4, self.member.id)] - - self.user = self.env["res.users"].create( - { - "name": "Test User", - "login": "testuser@test.com", - "email": "testuser@test.com", - "partner_id": self.member.id, - } - ) - - self.category = self.env["product.category"].create( - { - "name": "Test Category", - } - ) - - self.product = self.env["product.product"].create( - { - "name": "Test Product", - "type": "product", - "list_price": 100.0, - "categ_id": self.category.id, - } - ) - - self.group_order = self.env["group.order"].create( - { - "name": "Test Order", - "group_ids": [(4, self.group.id)], - "start_date": datetime.now().date(), - "end_date": datetime.now().date() + timedelta(days=7), - "pickup_day": "3", - "cutoff_day": "0", - "state": "open", - "category_ids": [(4, self.category.id)], - } - ) - - def test_eskaera_shop_order_not_found(self): - """Test that eskaera_shop redirects when order doesn't exist.""" - # Nonexistent order_id should redirect to /eskaera - # Placeholder: will be tested via HttpCase with request.Client - - def test_eskaera_shop_order_not_open(self): - """Test that eskaera_shop redirects when order is not open.""" - self.group_order.state = "confirmed" - # Should redirect to /eskaera - # Placeholder: will be tested via HttpCase with request.Client - - def test_eskaera_shop_uses_resolve_pricelist(self): - """Test that eskaera_shop uses _resolve_pricelist() helper.""" - # Configure Aplicoop pricelist - self.env["ir.config_parameter"].sudo().set_param( - "website_sale_aplicoop.pricelist_id", str(self.pricelist.id) - ) - - # When eskaera_shop is called, should use _resolve_pricelist() - # Placeholder: will verify via mock or direct method call - - -class TestEskaeraShopcategoryHierarchy(TransactionCase): - """Test eskaera_shop category hierarchy building.""" - - def setUp(self): - super().setUp() - self.parent_category = self.env["product.category"].create( - { - "name": "Parent Category", - } - ) - - self.child_category = self.env["product.category"].create( - { - "name": "Child Category", - "parent_id": self.parent_category.id, - } - ) - - self.product1 = self.env["product.product"].create( - { - "name": "Product in Parent", - "type": "product", - "list_price": 100.0, - "categ_id": self.parent_category.id, - } - ) - - self.product2 = self.env["product.product"].create( - { - "name": "Product in Child", - "type": "product", - "list_price": 200.0, - "categ_id": self.child_category.id, - } - ) - - def test_category_hierarchy_includes_parents(self): - """Test that available_categories includes parent categories.""" - # When products have categories, category hierarchy should include parents - # Placeholder: verify category tree structure - - def test_category_filter_includes_descendants(self): - """Test that category filter includes child categories.""" - # When filtering by parent category, should include products from children - # Placeholder: verify filtered products - - -class TestEskaeraShopriceCalculation(TransactionCase): - """Test eskaera_shop price calculation with pricelist.""" - - def setUp(self): - super().setUp() - self.pricelist = self.env["product.pricelist"].create( - { - "name": "Test Pricelist", - "currency_id": self.env.company.currency_id.id, - } - ) - - self.category = self.env["product.category"].create( - { - "name": "Test Category", - } - ) - - self.product_no_tax = self.env["product.product"].create( - { - "name": "Product No Tax", - "type": "product", - "list_price": 100.0, - "categ_id": self.category.id, - "taxes_id": False, - } - ) - - # Create tax - self.tax = self.env["account.tax"].create( - { - "name": "Test Tax", - "type_tax_use": "sale", - "amount": 21.0, - "amount_type": "percent", - } - ) - - self.product_with_tax = self.env["product.product"].create( - { - "name": "Product With Tax", - "type": "product", - "list_price": 100.0, - "categ_id": self.category.id, - "taxes_id": [(4, self.tax.id)], - } - ) - - def test_price_calculation_uses_pricelist(self): - """Test that product prices are calculated using configured pricelist.""" - # Configure Aplicoop pricelist - self.env["ir.config_parameter"].sudo().set_param( - "website_sale_aplicoop.pricelist_id", str(self.pricelist.id) - ) - - # When eskaera_shop renders, should calculate prices via pricelist - # Placeholder: verify price_info dict populated - - def test_price_info_structure(self): - """Test that product_price_info has correct structure.""" - # product_price_info should have: price, list_price, has_discounted_price, discount, tax_included - # Placeholder: verify dict structure - - -class TestEskaeraShoosearch(TransactionCase): - """Test eskaera_shop search functionality.""" - - def setUp(self): - super().setUp() - self.category = self.env["product.category"].create( - { - "name": "Test Category", - } - ) - - self.product1 = self.env["product.product"].create( - { - "name": "Apple Juice", - "type": "product", - "list_price": 10.0, - "categ_id": self.category.id, - } - ) - - self.product2 = self.env["product.product"].create( - { - "name": "Orange Juice", - "type": "product", - "list_price": 12.0, - "categ_id": self.category.id, - "description": "Fresh orange juice from Spain", - } - ) - - self.product3 = self.env["product.product"].create( - { - "name": "Water", - "type": "product", - "list_price": 2.0, - "categ_id": self.category.id, - } - ) - - self.group_order = self.env["group.order"].create( - { - "name": "Test Order", - "start_date": datetime.now().date(), - "end_date": datetime.now().date() + timedelta(days=7), - "pickup_day": "3", - "cutoff_day": "0", - "state": "open", - "category_ids": [(4, self.category.id)], - } - ) - - def test_search_filters_by_name(self): - """Test that search query filters products by name.""" - # When search='apple', should return only Apple Juice - # Placeholder: verify filtered products - - def test_search_filters_by_description(self): - """Test that search query filters products by description.""" - # When search='spain', should return Orange Juice (matches description) - # Placeholder: verify filtered products - - def test_search_case_insensitive(self): - """Test that search is case insensitive.""" - # search='APPLE' should match 'Apple Juice' - # Placeholder: verify filtered products - - def test_search_empty_returns_all(self): - """Test that empty search returns all products.""" - # When search='', should return all products - # Placeholder: verify all products returned diff --git a/website_sale_aplicoop/tests/test_phase3_confirm_eskaera.py b/website_sale_aplicoop/tests/test_phase3_confirm_eskaera.py deleted file mode 100644 index 1614ab2..0000000 --- a/website_sale_aplicoop/tests/test_phase3_confirm_eskaera.py +++ /dev/null @@ -1,669 +0,0 @@ -# Copyright 2026 - Today Criptomart -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -""" -Test suite for Phase 3 refactoring of confirm_eskaera(). - -Tests the 3 helper methods created in Phase 3: -- _validate_confirm_json(): Validates JSON request data -- _process_cart_items(): Processes cart items into sale.order lines -- _build_confirmation_message(): Builds localized confirmation messages - -Includes tests for: -- Request validation with various error conditions -- Cart item processing with product context -- Multi-language message building (ES, EU, CA, GL, PT, FR, IT) -- Pickup vs delivery date handling -- Edge cases and error handling -""" - -import json -from datetime import date -from datetime import timedelta -from unittest.mock import Mock -from unittest.mock import patch - -from odoo import http -from odoo.tests.common import TransactionCase - - -class TestValidateConfirmJson(TransactionCase): - """Test _validate_confirm_json() helper method.""" - - def setUp(self): - super().setUp() - self.controller = http.request.env["website.sale"].browse([]) - self.user = self.env.ref("base.user_admin") - self.partner = self.env.ref("base.partner_admin") - - # Create test group order - self.group_order = self.env["group.order"].create( - { - "name": "Test Order Phase 3", - "state": "open", - "collection_date": date.today() + timedelta(days=3), - "cutoff_day": "3", # Thursday - "pickup_day": "5", # Saturday - } - ) - - @patch("odoo.http.request") - def test_validate_confirm_json_success(self, mock_request): - """Test successful validation of confirm JSON data.""" - mock_request.env = self.env.with_user(self.user) - - data = { - "order_id": self.group_order.id, - "items": [{"product_id": 1, "quantity": 2, "product_price": 10.0}], - "is_delivery": False, - } - - order_id, group_order, current_user, items, is_delivery = ( - self.controller._validate_confirm_json(data) - ) - - self.assertEqual(order_id, self.group_order.id) - self.assertEqual(group_order.id, self.group_order.id) - self.assertEqual(current_user.id, self.user.id) - self.assertEqual(len(items), 1) - self.assertFalse(is_delivery) - - @patch("odoo.http.request") - def test_validate_confirm_json_missing_order_id(self, mock_request): - """Test validation fails without order_id.""" - mock_request.env = self.env.with_user(self.user) - - data = {"items": [{"product_id": 1, "quantity": 2}]} - - with self.assertRaises(ValueError) as context: - self.controller._validate_confirm_json(data) - - self.assertIn("Missing order_id", str(context.exception)) - - @patch("odoo.http.request") - def test_validate_confirm_json_order_not_exists(self, mock_request): - """Test validation fails with non-existent order.""" - mock_request.env = self.env.with_user(self.user) - - data = { - "order_id": 99999, # Non-existent ID - "items": [{"product_id": 1, "quantity": 2}], - } - - with self.assertRaises(ValueError) as context: - self.controller._validate_confirm_json(data) - - self.assertIn("Order", str(context.exception)) - - @patch("odoo.http.request") - def test_validate_confirm_json_no_items(self, mock_request): - """Test validation fails without items in cart.""" - mock_request.env = self.env.with_user(self.user) - - data = { - "order_id": self.group_order.id, - "items": [], - } - - with self.assertRaises(ValueError) as context: - self.controller._validate_confirm_json(data) - - self.assertIn("No items in cart", str(context.exception)) - - @patch("odoo.http.request") - def test_validate_confirm_json_with_delivery_flag(self, mock_request): - """Test validation correctly handles is_delivery flag.""" - mock_request.env = self.env.with_user(self.user) - - data = { - "order_id": self.group_order.id, - "items": [{"product_id": 1, "quantity": 1}], - "is_delivery": True, - } - - _, _, _, _, is_delivery = self.controller._validate_confirm_json(data) - - self.assertTrue(is_delivery) - - -class TestProcessCartItems(TransactionCase): - """Test _process_cart_items() helper method.""" - - def setUp(self): - super().setUp() - self.controller = http.request.env["website.sale"].browse([]) - - # Create test products - self.product1 = self.env["product.product"].create( - { - "name": "Test Product 1", - "list_price": 15.0, - "type": "consu", - } - ) - self.product2 = self.env["product.product"].create( - { - "name": "Test Product 2", - "list_price": 25.0, - "type": "consu", - } - ) - - # Create test group order - self.group_order = self.env["group.order"].create( - { - "name": "Test Order for Cart", - "state": "open", - } - ) - - @patch("odoo.http.request") - def test_process_cart_items_success(self, mock_request): - """Test successful cart item processing.""" - mock_request.env = self.env - mock_request.env.lang = "es_ES" - - items = [ - { - "product_id": self.product1.id, - "quantity": 2, - "product_price": 15.0, - }, - { - "product_id": self.product2.id, - "quantity": 1, - "product_price": 25.0, - }, - ] - - result = self.controller._process_cart_items(items, self.group_order) - - self.assertEqual(len(result), 2) - self.assertEqual(result[0][0], 0) # Command (0, 0, vals) - self.assertEqual(result[0][1], 0) - self.assertIn("product_id", result[0][2]) - self.assertEqual(result[0][2]["product_uom_qty"], 2) - self.assertEqual(result[0][2]["price_unit"], 15.0) - - @patch("odoo.http.request") - def test_process_cart_items_uses_list_price_fallback(self, mock_request): - """Test cart processing uses list_price when product_price is 0.""" - mock_request.env = self.env - mock_request.env.lang = "es_ES" - - items = [ - { - "product_id": self.product1.id, - "quantity": 1, - "product_price": 0, # Should fallback to list_price - } - ] - - result = self.controller._process_cart_items(items, self.group_order) - - self.assertEqual(len(result), 1) - # Should use product.list_price as fallback - self.assertEqual(result[0][2]["price_unit"], self.product1.list_price) - - @patch("odoo.http.request") - def test_process_cart_items_skips_invalid_product(self, mock_request): - """Test cart processing skips non-existent products.""" - mock_request.env = self.env - mock_request.env.lang = "es_ES" - - items = [ - { - "product_id": 99999, # Non-existent - "quantity": 1, - "product_price": 10.0, - }, - { - "product_id": self.product1.id, - "quantity": 2, - "product_price": 15.0, - }, - ] - - result = self.controller._process_cart_items(items, self.group_order) - - # Should only process the valid product - self.assertEqual(len(result), 1) - self.assertEqual(result[0][2]["product_id"], self.product1.id) - - @patch("odoo.http.request") - def test_process_cart_items_empty_after_filtering(self, mock_request): - """Test cart processing raises error when no valid items remain.""" - mock_request.env = self.env - mock_request.env.lang = "es_ES" - - items = [{"product_id": 99999, "quantity": 1, "product_price": 10.0}] - - with self.assertRaises(ValueError) as context: - self.controller._process_cart_items(items, self.group_order) - - self.assertIn("No valid items", str(context.exception)) - - @patch("odoo.http.request") - def test_process_cart_items_translates_product_name(self, mock_request): - """Test cart processing uses translated product names.""" - mock_request.env = self.env - mock_request.env.lang = "eu_ES" # Basque - - # Add translation for product name - self.env["ir.translation"].create( - { - "type": "model", - "name": "product.product,name", - "module": "website_sale_aplicoop", - "lang": "eu_ES", - "res_id": self.product1.id, - "src": "Test Product 1", - "value": "Proba Produktua 1", - "state": "translated", - } - ) - - items = [ - { - "product_id": self.product1.id, - "quantity": 1, - "product_price": 15.0, - } - ] - - result = self.controller._process_cart_items(items, self.group_order) - - # Product name should be in Basque context - product_name = result[0][2]["name"] - self.assertIsNotNone(product_name) - # In real test, would be "Proba Produktua 1" but translation may not work in test - - -class TestBuildConfirmationMessage(TransactionCase): - """Test _build_confirmation_message() helper method.""" - - def setUp(self): - super().setUp() - self.controller = http.request.env["website.sale"].browse([]) - self.user = self.env.ref("base.user_admin") - self.partner = self.env.ref("base.partner_admin") - - # Create test group order with dates - pickup_date = date.today() + timedelta(days=5) - delivery_date = pickup_date + timedelta(days=1) - - self.group_order = self.env["group.order"].create( - { - "name": "Test Order Messages", - "state": "open", - "pickup_day": "5", # Saturday (0=Monday) - "pickup_date": pickup_date, - "delivery_date": delivery_date, - } - ) - - # Create test sale order - self.sale_order = self.env["sale.order"].create( - { - "partner_id": self.partner.id, - "group_order_id": self.group_order.id, - } - ) - - @patch("odoo.http.request") - def test_build_confirmation_message_pickup(self, mock_request): - """Test confirmation message for pickup (not delivery).""" - mock_request.env = self.env.with_context(lang="es_ES") - - result = self.controller._build_confirmation_message( - self.sale_order, self.group_order, is_delivery=False - ) - - self.assertIn("message", result) - self.assertIn("pickup_day", result) - self.assertIn("pickup_date", result) - self.assertIn("pickup_day_index", result) - - # Should contain "Thank you" text (or translation) - self.assertIn("Thank you", result["message"]) - - # Should contain order reference - self.assertIn(self.sale_order.name, result["message"]) - - # Should have pickup day index - self.assertEqual(result["pickup_day_index"], 5) - - @patch("odoo.http.request") - def test_build_confirmation_message_delivery(self, mock_request): - """Test confirmation message for home delivery.""" - mock_request.env = self.env.with_context(lang="es_ES") - - result = self.controller._build_confirmation_message( - self.sale_order, self.group_order, is_delivery=True - ) - - self.assertIn("message", result) - - # Should contain "Delivery date" label (or translation) - # and should use delivery_date, not pickup_date - message = result["message"] - self.assertIsNotNone(message) - - # Delivery day should be next day after pickup (Saturday -> Sunday) - # pickup_day_index=5 (Saturday), delivery should be 6 (Sunday) - # Note: _get_day_names would need to be mocked for exact day name - - @patch("odoo.http.request") - def test_build_confirmation_message_no_dates(self, mock_request): - """Test confirmation message when no pickup date is set.""" - mock_request.env = self.env.with_context(lang="es_ES") - - # Create order without dates - group_order_no_dates = self.env["group.order"].create( - { - "name": "Order No Dates", - "state": "open", - } - ) - - sale_order_no_dates = self.env["sale.order"].create( - { - "partner_id": self.partner.id, - "group_order_id": group_order_no_dates.id, - } - ) - - result = self.controller._build_confirmation_message( - sale_order_no_dates, group_order_no_dates, is_delivery=False - ) - - # Should still build message without dates - self.assertIn("message", result) - self.assertIn("Thank you", result["message"]) - - # Date fields should be empty - self.assertEqual(result["pickup_date"], "") - - @patch("odoo.http.request") - def test_build_confirmation_message_formats_date(self, mock_request): - """Test confirmation message formats dates correctly (DD/MM/YYYY).""" - mock_request.env = self.env.with_context(lang="es_ES") - - result = self.controller._build_confirmation_message( - self.sale_order, self.group_order, is_delivery=False - ) - - # Should have date in DD/MM/YYYY format - pickup_date_str = result["pickup_date"] - self.assertIsNotNone(pickup_date_str) - - # Verify format with regex - - date_pattern = r"\d{2}/\d{2}/\d{4}" - self.assertRegex(pickup_date_str, date_pattern) - - @patch("odoo.http.request") - def test_build_confirmation_message_multilang_es(self, mock_request): - """Test confirmation message in Spanish (es_ES).""" - mock_request.env = self.env.with_context(lang="es_ES") - - result = self.controller._build_confirmation_message( - self.sale_order, self.group_order, is_delivery=False - ) - - message = result["message"] - # Should contain translated strings (if translations loaded) - self.assertIsNotNone(message) - # In real scenario, would check for "¡Gracias!" or similar - - @patch("odoo.http.request") - def test_build_confirmation_message_multilang_eu(self, mock_request): - """Test confirmation message in Basque (eu_ES).""" - mock_request.env = self.env.with_context(lang="eu_ES") - - result = self.controller._build_confirmation_message( - self.sale_order, self.group_order, is_delivery=False - ) - - message = result["message"] - self.assertIsNotNone(message) - # In real scenario, would check for "Eskerrik asko!" or similar - - @patch("odoo.http.request") - def test_build_confirmation_message_multilang_ca(self, mock_request): - """Test confirmation message in Catalan (ca_ES).""" - mock_request.env = self.env.with_context(lang="ca_ES") - - result = self.controller._build_confirmation_message( - self.sale_order, self.group_order, is_delivery=False - ) - - message = result["message"] - self.assertIsNotNone(message) - # In real scenario, would check for "Gràcies!" or similar - - @patch("odoo.http.request") - def test_build_confirmation_message_multilang_gl(self, mock_request): - """Test confirmation message in Galician (gl_ES).""" - mock_request.env = self.env.with_context(lang="gl_ES") - - result = self.controller._build_confirmation_message( - self.sale_order, self.group_order, is_delivery=False - ) - - message = result["message"] - self.assertIsNotNone(message) - # In real scenario, would check for "Grazas!" or similar - - @patch("odoo.http.request") - def test_build_confirmation_message_multilang_pt(self, mock_request): - """Test confirmation message in Portuguese (pt_PT).""" - mock_request.env = self.env.with_context(lang="pt_PT") - - result = self.controller._build_confirmation_message( - self.sale_order, self.group_order, is_delivery=False - ) - - message = result["message"] - self.assertIsNotNone(message) - # In real scenario, would check for "Obrigado!" or similar - - @patch("odoo.http.request") - def test_build_confirmation_message_multilang_fr(self, mock_request): - """Test confirmation message in French (fr_FR).""" - mock_request.env = self.env.with_context(lang="fr_FR") - - result = self.controller._build_confirmation_message( - self.sale_order, self.group_order, is_delivery=False - ) - - message = result["message"] - self.assertIsNotNone(message) - # In real scenario, would check for "Merci!" or similar - - @patch("odoo.http.request") - def test_build_confirmation_message_multilang_it(self, mock_request): - """Test confirmation message in Italian (it_IT).""" - mock_request.env = self.env.with_context(lang="it_IT") - - result = self.controller._build_confirmation_message( - self.sale_order, self.group_order, is_delivery=False - ) - - message = result["message"] - self.assertIsNotNone(message) - # In real scenario, would check for "Grazie!" or similar - - -class TestConfirmEskaera_Integration(TransactionCase): - """Integration tests for confirm_eskaera() with all 3 helpers.""" - - def setUp(self): - super().setUp() - self.controller = http.request.env["website.sale"].browse([]) - self.user = self.env.ref("base.user_admin") - self.partner = self.env.ref("base.partner_admin") - - # Create test product - self.product = self.env["product.product"].create( - { - "name": "Integration Test Product", - "list_price": 20.0, - "type": "consu", - } - ) - - # Create test group order - self.group_order = self.env["group.order"].create( - { - "name": "Integration Test Order", - "state": "open", - "pickup_day": "5", - "pickup_date": date.today() + timedelta(days=5), - } - ) - - @patch("odoo.http.request") - def test_confirm_eskaera_full_flow_pickup(self, mock_request): - """Test full confirm_eskaera flow for pickup order.""" - mock_request.env = self.env.with_user(self.user) - mock_request.env.lang = "es_ES" - mock_request.httprequest = Mock() - - # Prepare request data - data = { - "order_id": self.group_order.id, - "items": [ - { - "product_id": self.product.id, - "quantity": 3, - "product_price": 20.0, - } - ], - "is_delivery": False, - } - - mock_request.httprequest.data = json.dumps(data).encode("utf-8") - - # Call confirm_eskaera - response = self.controller.confirm_eskaera() - - # Verify response - self.assertIsNotNone(response) - response_data = json.loads(response.data.decode("utf-8")) - - self.assertTrue(response_data.get("success")) - self.assertIn("message", response_data) - self.assertIn("sale_order_id", response_data) - - # Verify sale.order was created - sale_order_id = response_data["sale_order_id"] - sale_order = self.env["sale.order"].browse(sale_order_id) - - self.assertTrue(sale_order.exists()) - self.assertEqual(sale_order.partner_id.id, self.partner.id) - self.assertEqual(sale_order.group_order_id.id, self.group_order.id) - self.assertEqual(len(sale_order.order_line), 1) - self.assertEqual(sale_order.order_line[0].product_uom_qty, 3) - - @patch("odoo.http.request") - def test_confirm_eskaera_full_flow_delivery(self, mock_request): - """Test full confirm_eskaera flow for delivery order.""" - mock_request.env = self.env.with_user(self.user) - mock_request.env.lang = "es_ES" - mock_request.httprequest = Mock() - - # Add delivery_date to group order - self.group_order.delivery_date = self.group_order.pickup_date + timedelta( - days=1 - ) - - # Prepare request data - data = { - "order_id": self.group_order.id, - "items": [ - { - "product_id": self.product.id, - "quantity": 2, - "product_price": 20.0, - } - ], - "is_delivery": True, - } - - mock_request.httprequest.data = json.dumps(data).encode("utf-8") - - # Call confirm_eskaera - response = self.controller.confirm_eskaera() - - # Verify response - response_data = json.loads(response.data.decode("utf-8")) - - self.assertTrue(response_data.get("success")) - - # Verify sale.order has delivery flag - sale_order_id = response_data["sale_order_id"] - sale_order = self.env["sale.order"].browse(sale_order_id) - - self.assertTrue(sale_order.home_delivery) - # commitment_date should be delivery_date - self.assertEqual( - sale_order.commitment_date.date(), self.group_order.delivery_date - ) - - @patch("odoo.http.request") - def test_confirm_eskaera_updates_existing_draft(self, mock_request): - """Test confirm_eskaera updates existing draft order instead of creating new.""" - mock_request.env = self.env.with_user(self.user) - mock_request.env.lang = "es_ES" - mock_request.httprequest = Mock() - - # Create existing draft order - existing_order = self.env["sale.order"].create( - { - "partner_id": self.partner.id, - "group_order_id": self.group_order.id, - "state": "draft", - "order_line": [ - ( - 0, - 0, - { - "product_id": self.product.id, - "product_uom_qty": 1, - "price_unit": 20.0, - }, - ) - ], - } - ) - - existing_order_id = existing_order.id - - # Prepare new request data - data = { - "order_id": self.group_order.id, - "items": [ - { - "product_id": self.product.id, - "quantity": 5, # Different quantity - "product_price": 20.0, - } - ], - "is_delivery": False, - } - - mock_request.httprequest.data = json.dumps(data).encode("utf-8") - - # Call confirm_eskaera - response = self.controller.confirm_eskaera() - - response_data = json.loads(response.data.decode("utf-8")) - - # Should update existing order, not create new - self.assertEqual(response_data["sale_order_id"], existing_order_id) - - # Verify order was updated - existing_order.invalidate_recordset() - self.assertEqual(len(existing_order.order_line), 1) - self.assertEqual(existing_order.order_line[0].product_uom_qty, 5) diff --git a/website_sale_aplicoop/views/res_config_settings_views.xml b/website_sale_aplicoop/views/res_config_settings_views.xml index e1537f0..fc1c206 100644 --- a/website_sale_aplicoop/views/res_config_settings_views.xml +++ b/website_sale_aplicoop/views/res_config_settings_views.xml @@ -5,7 +5,7 @@ res.config.settings - +

Aplicoop Settings