diff --git a/.flake8 b/.flake8
index d5708c3..cb28e98 100644
--- a/.flake8
+++ b/.flake8
@@ -4,7 +4,7 @@
[flake8]
max-line-length = 88
-max-complexity = 30
+max-complexity = 16
# B = bugbear
# B9 = bugbear opinionated (incl line length)
select = C,E,F,W,B,B9
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index a62dd49..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,61 +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.
-
-### QWeb Template Best Practices
-
-**CRÍTICO**: QWeb tiene limitaciones estrictas con lógica compleja. **Siempre mover lógica al controller**:
-
-```python
-# ❌ MAL - QWeb no puede parsear esto
-#
-
-# ✅ CORRECTO - Preparar datos en controller
-class WebsiteController:
- def _prepare_product_display_info(self, product, price_info):
- """Pre-procesar todos los valores para QWeb."""
- price = price_info.get(product.id, {}).get('price') or product.list_price or 0.0
- return {
- 'display_price': float(price),
- 'safe_uom_category': product.uom_id.category_id.name or '',
- }
-
-# En template: acceso simple, sin lógica
-#
-```
-
-Ver [docs/QWEB_BEST_PRACTICES.md](../docs/QWEB_BEST_PRACTICES.md) para más detalles.
-
## Common Patterns
### Extending Models
@@ -305,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`)
@@ -388,46 +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
-
-### QWeb Template Errors
-
-**Problem**: `TypeError: 'NoneType' object is not callable` in templates
-**Solution**:
-
-1. Move complex logic from template to controller
-2. Use simple attribute access in templates (no conditionals)
-3. Pre-process all display values in Python
-4. See [docs/QWEB_BEST_PRACTICES.md](../docs/QWEB_BEST_PRACTICES.md) for patterns
-
-**Example Pattern**:
-
-```python
-# Controller: prepare clean data
-def _prepare_display_info(self, product):
- return {
- 'price': product.price or 0.0,
- 'uom': product.uom_id.name or '',
- }
-
-# Template: use simple access
-
-```
+**Solution**: Use `product_sale_price_from_pricelist` with proper configuration
## Testing Guidelines
@@ -447,18 +307,18 @@ def _prepare_display_info(self, product):
```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
+ ],
+ );
});
```
@@ -504,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
@@ -567,76 +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
-- **Lazy Loading**: Carga configurable de productos (v18.0.1.3.0+)
-- **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
-
-**Configuración Lazy Loading** (v18.0.1.3.0+):
-
-```
-Settings > Website > Shop Performance
- [✓] Enable Lazy Loading
- [20] Products Per Page
-```
-
-**Mejoras Recientes**:
-
-- v18.0.1.3.1: Fixes críticos de cálculo de fechas
-- v18.0.1.3.0: Lazy loading, mejora de rendimiento de 10-20s → 500-800ms
-- Refactor de template rendering: Mover lógica QWeb al controller
-
-Ver [website_sale_aplicoop/README.md](../website_sale_aplicoop/README.md) y [docs/LAZY_LOADING.md](../docs/LAZY_LOADING.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
@@ -646,13 +406,6 @@ last_purchase_price_compute_type != "manual_update" # Para auto-cálculo
---
-**Last Updated**: 2026-02-18
+**Last Updated**: 2026-02-12
**Odoo Version**: 18.0
**Python Version**: 3.10+
-
-## Recent Changes Summary
-
-- **2026-02-18**: Refactor `product_main_seller` - Remover alias innecesario `default_supplier_id`
-- **2026-02-16**: v18.0.1.3.1 fixes críticos de cálculo de fechas en Eskaera
-- **2026-02-12**: v18.0.1.3.0 Lazy loading y fixes de template rendering QWeb
-- **2026-02-02**: UI improvements y date calculation fixes
diff --git a/.gitignore b/.gitignore
index a3957ae..e03074a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -130,3 +130,4 @@ dmypy.json
# Pyre type checker
.pyre/
+
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3e97523..beb2928 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,142 +1,142 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
exclude: |
- (?x)
- # NOT INSTALLABLE ADDONS
- # END NOT INSTALLABLE ADDONS
- # Files and folders generated by bots, to avoid loops
- /setup/|/README\.rst$|/static/description/index\.html$|
- # Maybe reactivate this when all README files include prettier ignore tags?
- ^README\.md$|
- # Library files can have extraneous formatting (even minimized)
- /static/(src/)?lib/|
- # Repos using Sphinx to generate docs don't need prettying
- ^docs/_templates/.*\.html$|
- # You don't usually want a bot to modify your legal texts
- (LICENSE.*|COPYING.*)
+ (?x)
+ # NOT INSTALLABLE ADDONS
+ # END NOT INSTALLABLE ADDONS
+ # Files and folders generated by bots, to avoid loops
+ /setup/|/README\.rst$|/static/description/index\.html$|
+ # Maybe reactivate this when all README files include prettier ignore tags?
+ ^README\.md$|
+ # Library files can have extraneous formatting (even minimized)
+ /static/(src/)?lib/|
+ # Repos using Sphinx to generate docs don't need prettying
+ ^docs/_templates/.*\.html$|
+ # You don't usually want a bot to modify your legal texts
+ (LICENSE.*|COPYING.*)
default_language_version:
- python: python3
- node: "16.17.0"
+ python: python3
+ node: "16.17.0"
repos:
- - repo: local
- hooks:
- # These files are most likely copier diff rejection junks; if found,
- # review them manually, fix the problem (if needed) and remove them
- - id: forbidden-files
- name: forbidden files
- entry: found forbidden files; remove them
- language: fail
- files: "\\.rej$"
- - repo: https://github.com/oca/maintainer-tools
- rev: 71aa4caec15e8c1456b4da19e9f39aa0aa7377a9
- hooks:
- # update the NOT INSTALLABLE ADDONS section above
- - id: oca-update-pre-commit-excluded-addons
- - repo: https://github.com/myint/autoflake
- rev: v2.3.1
- hooks:
- - id: autoflake
- args: ["-i", "--ignore-init-module-imports"]
- - repo: https://github.com/psf/black
- rev: 26.1.0
- hooks:
- - id: black
- - repo: https://github.com/pre-commit/mirrors-prettier
- rev: v4.0.0-alpha.8
- hooks:
- - id: prettier
- name: prettier + plugin-xml
- additional_dependencies:
- - "prettier@2.7.1"
- - "@prettier/plugin-xml@2.2.0"
- args:
- - --plugin=@prettier/plugin-xml
- files: \.(css|htm|html|js|json|json5|scss|toml|xml|yaml|yml)$
- - repo: https://github.com/pre-commit/mirrors-eslint
- rev: v10.0.0
- hooks:
- - id: eslint
- verbose: true
- args:
- - --color
- - --fix
- - repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v6.0.0
- hooks:
- - id: trailing-whitespace
- # exclude autogenerated files
- exclude: /README\.rst$|\.pot?$
- - id: end-of-file-fixer
- # exclude autogenerated files
- exclude: /README\.rst$|\.pot?$
- - id: debug-statements
- - id: check-case-conflict
- - id: check-docstring-first
- - id: check-executables-have-shebangs
- - id: check-merge-conflict
- # exclude files where underlines are not distinguishable from merge conflicts
- exclude: /README\.rst$|^docs/.*\.rst$
- - id: check-symlinks
- - id: check-xml
- - id: mixed-line-ending
- args: ["--fix=lf"]
- - repo: https://github.com/asottile/pyupgrade
- rev: v3.21.2
- hooks:
- - id: pyupgrade
- args: ["--py38-plus"]
- - repo: https://github.com/PyCQA/isort
- rev: 7.0.0
- hooks:
- - id: isort
- name: isort except __init__.py
- args:
- - --settings=.
- exclude: /__init__\.py$
- # setuptools-odoo deshabilitado temporalmente (no soporta Odoo 18.0)
- # - repo: https://github.com/acsone/setuptools-odoo
- # rev: 3.3.2
- # hooks:
- # - id: setuptools-odoo-make-default
- # - id: setuptools-odoo-get-requirements
- # args:
- # - --output
- # - requirements.txt
- # - --header
- # - "# generated from manifests external_dependencies"
- - repo: https://github.com/PyCQA/flake8
- rev: 7.3.0
- hooks:
- - id: flake8
- name: flake8
- additional_dependencies: ["flake8-bugbear==23.12.2"]
- - repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.19.1
- hooks:
- - id: mypy
- # do not run on test files or __init__ files (mypy does not support
- # namespace packages)
- exclude: (/tests/|/__init__\.py$)
- additional_dependencies:
- - "lxml"
- - "odoo-stubs"
- - "types-python-dateutil"
- - "types-pytz"
- - "types-requests"
- - "types-setuptools"
- - repo: https://github.com/PyCQA/pylint
- rev: v4.0.4
- hooks:
- - id: pylint
- name: pylint with optional checks
- args:
- - --rcfile=.pylintrc
- - --exit-zero
- verbose: true
- additional_dependencies: &pylint_deps
- - pylint-odoo==10.0.0
- - id: pylint
- name: pylint with mandatory checks
- args:
- - --rcfile=.pylintrc-mandatory
- additional_dependencies: *pylint_deps
+ - repo: local
+ hooks:
+ # These files are most likely copier diff rejection junks; if found,
+ # review them manually, fix the problem (if needed) and remove them
+ - id: forbidden-files
+ name: forbidden files
+ entry: found forbidden files; remove them
+ language: fail
+ files: "\\.rej$"
+ - repo: https://github.com/oca/maintainer-tools
+ rev: 71aa4caec15e8c1456b4da19e9f39aa0aa7377a9
+ hooks:
+ # update the NOT INSTALLABLE ADDONS section above
+ - id: oca-update-pre-commit-excluded-addons
+ - repo: https://github.com/myint/autoflake
+ rev: v2.3.1
+ hooks:
+ - id: autoflake
+ args: ["-i", "--ignore-init-module-imports"]
+ - repo: https://github.com/psf/black
+ rev: 26.1.0
+ hooks:
+ - id: black
+ - repo: https://github.com/pre-commit/mirrors-prettier
+ rev: v4.0.0-alpha.8
+ hooks:
+ - id: prettier
+ name: prettier + plugin-xml
+ additional_dependencies:
+ - "prettier@2.7.1"
+ - "@prettier/plugin-xml@2.2.0"
+ args:
+ - --plugin=@prettier/plugin-xml
+ files: \.(css|htm|html|js|json|json5|scss|toml|xml|yaml|yml)$
+ - repo: https://github.com/pre-commit/mirrors-eslint
+ rev: v10.0.0
+ hooks:
+ - id: eslint
+ verbose: true
+ args:
+ - --color
+ - --fix
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v6.0.0
+ hooks:
+ - id: trailing-whitespace
+ # exclude autogenerated files
+ exclude: /README\.rst$|\.pot?$
+ - id: end-of-file-fixer
+ # exclude autogenerated files
+ exclude: /README\.rst$|\.pot?$
+ - id: debug-statements
+ - id: check-case-conflict
+ - id: check-docstring-first
+ - id: check-executables-have-shebangs
+ - id: check-merge-conflict
+ # exclude files where underlines are not distinguishable from merge conflicts
+ exclude: /README\.rst$|^docs/.*\.rst$
+ - id: check-symlinks
+ - id: check-xml
+ - id: mixed-line-ending
+ args: ["--fix=lf"]
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v3.21.2
+ hooks:
+ - id: pyupgrade
+ args: ["--py38-plus"]
+ - repo: https://github.com/PyCQA/isort
+ rev: 7.0.0
+ hooks:
+ - id: isort
+ name: isort except __init__.py
+ args:
+ - --settings=.
+ exclude: /__init__\.py$
+ # setuptools-odoo deshabilitado temporalmente (no soporta Odoo 18.0)
+ # - repo: https://github.com/acsone/setuptools-odoo
+ # rev: 3.3.2
+ # hooks:
+ # - id: setuptools-odoo-make-default
+ # - id: setuptools-odoo-get-requirements
+ # args:
+ # - --output
+ # - requirements.txt
+ # - --header
+ # - "# generated from manifests external_dependencies"
+ - repo: https://github.com/PyCQA/flake8
+ rev: 7.3.0
+ hooks:
+ - id: flake8
+ name: flake8
+ additional_dependencies: ["flake8-bugbear==23.12.2"]
+ - repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v1.19.1
+ hooks:
+ - id: mypy
+ # do not run on test files or __init__ files (mypy does not support
+ # namespace packages)
+ exclude: (/tests/|/__init__\.py$)
+ additional_dependencies:
+ - "lxml"
+ - "odoo-stubs"
+ - "types-python-dateutil"
+ - "types-pytz"
+ - "types-requests"
+ - "types-setuptools"
+ - repo: https://github.com/PyCQA/pylint
+ rev: v4.0.4
+ hooks:
+ - id: pylint
+ name: pylint with optional checks
+ args:
+ - --rcfile=.pylintrc
+ - --exit-zero
+ verbose: true
+ additional_dependencies: &pylint_deps
+ - pylint-odoo==10.0.0
+ - id: pylint
+ name: pylint with mandatory checks
+ args:
+ - --rcfile=.pylintrc-mandatory
+ additional_dependencies: *pylint_deps
diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md
deleted file mode 100644
index ad46d50..0000000
--- a/DOCUMENTATION.md
+++ /dev/null
@@ -1,225 +0,0 @@
-# 📚 Documentación del Proyecto - Índice
-
-## 🚀 Lazy Loading v18.0.1.3.0 - Documentación Rápida
-
-¿Buscas información sobre la nueva feature de lazy loading? Empieza aquí:
-
-### ⚡ Solo tengo 5 minutos
-👉 **[docs/LAZY_LOADING_QUICK_START.md](docs/LAZY_LOADING_QUICK_START.md)** - TL;DR y setup rápido
-
-### 🔧 Necesito instalar / actualizar
-👉 **[docs/UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md](docs/UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md)** - Paso a paso con validación y troubleshooting
-
-### 🎓 Quiero entender la arquitectura
-👉 **[docs/LAZY_LOADING.md](docs/LAZY_LOADING.md)** - Detalles técnicos completos
-
-### 📍 No sé dónde empezar
-👉 **[docs/LAZY_LOADING_DOCS_INDEX.md](docs/LAZY_LOADING_DOCS_INDEX.md)** - Índice con guía de selección por rol
-
----
-
-## 📖 Documentación General del Proyecto
-
-### Quick Links
-| Categoría | Documento | Propósito |
-|-----------|-----------|----------|
-| **Start** | [README.md](README.md) | Descripción general del proyecto |
-| **Development** | [.github/copilot-instructions.md](.github/copilot-instructions.md) | Guía para desarrollo con IA |
-| **All Docs** | [docs/README.md](docs/README.md) | Índice completo de documentación técnica |
-
----
-
-## 📂 Estructura de Documentación
-
-```
-addons-cm/
-├── README.md # Descripción general del proyecto
-│
-├── docs/ # 📚 Documentación técnica
-│ ├── README.md # Índice de todos los docs técnicos
-│ │
-│ ├── 🚀 LAZY LOADING (v18.0.1.3.0)
-│ ├── LAZY_LOADING_QUICK_START.md # ⚡ 5 min - Lo esencial
-│ ├── LAZY_LOADING_DOCS_INDEX.md # 📍 Índice con guía por rol
-│ ├── LAZY_LOADING.md # 🎓 Detalles técnicos
-│ ├── UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md # 🔧 Instalación
-│ │
-│ ├── 📋 OTROS DOCS
-│ ├── LINTERS_README.md # Herramientas de código
-│ ├── TRANSLATIONS.md # Sistema de traducciones
-│ ├── INSTALACION_COMPLETA.md # Instalación del proyecto
-│ ├── RESUMEN_INSTALACION.md # Resumen de instalación
-│ ├── CORRECCION_PRECIOS_IVA.md # Precios e impuestos
-│ └── TEST_MANUAL.md # Testing manual
-│
-├── website_sale_aplicoop/ # 📦 Addon principal
-│ ├── README.md # Features y configuración
-│ └── CHANGELOG.md # Historial de versiones
-│
-└── DOCUMENTATION_UPDATE_SUMMARY.md # 📋 Resumen de cambios (Este proyecto)
-```
-
----
-
-## 🎯 Guía Rápida por Tipo de Usuario
-
-### 👤 Administrador del Sistema
-1. **Instalación**: [UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md](docs/UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md)
-2. **Configuración**: Settings → Website → Shop Settings
-3. **Troubleshooting**: Sección de troubleshooting en UPGRADE_INSTRUCTIONS
-4. **Performance**: Sección "Verificación de Rendimiento"
-
-### 👨💻 Desarrollador
-1. **Arquitectura**: [docs/LAZY_LOADING.md](docs/LAZY_LOADING.md)
-2. **Código**: Sección "Code Changes" en LAZY_LOADING.md
-3. **Testing**: Sección "Debugging & Testing"
-4. **Mejoras**: "Future Improvements" al final
-
-### 🎓 Alguien Nuevo en el Proyecto
-1. **Start**: [README.md](README.md)
-2. **Features**: [website_sale_aplicoop/README.md](website_sale_aplicoop/README.md)
-3. **Lazy Loading**: [docs/LAZY_LOADING_DOCS_INDEX.md](docs/LAZY_LOADING_DOCS_INDEX.md)
-4. **Detalles Técnicos**: [.github/copilot-instructions.md](.github/copilot-instructions.md)
-
-### 🚀 Alguien que Solo Quiere Setup Rápido
-1. [docs/LAZY_LOADING_QUICK_START.md](docs/LAZY_LOADING_QUICK_START.md) (5 min)
-2. Done! ✅
-
----
-
-## 📊 Resumen de Documentación
-
-### Lazy Loading Feature (v18.0.1.3.0)
-
-**Problema Solucionado**:
-- ❌ Antes: Página tarda 10-20 segundos en cargar todos los productos y calcular precios
-
-**Solución**:
-- ✅ Después: Página carga en 500-800ms (20x más rápido)
-- ✅ Productos se cargan bajo demanda con botón "Load More"
-- ✅ Configurable: Activable/desactivable, items por página ajustable
-
-**Documentación Incluida**:
-- ✅ Quick Start (5 min)
-- ✅ Upgrade Instructions (paso a paso)
-- ✅ Technical Documentation (detalles completos)
-- ✅ Troubleshooting (4 escenarios comunes)
-- ✅ Performance Metrics (verificación)
-
----
-
-## 🔗 Enlaces Directos
-
-### Lazy Loading
-- [⚡ Quick Start](docs/LAZY_LOADING_QUICK_START.md) - Start here (5 min)
-- [🔧 Upgrade Instructions](docs/UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md) - Installation & Config
-- [🎓 Technical Docs](docs/LAZY_LOADING.md) - Deep dive
-- [📍 Documentation Index](docs/LAZY_LOADING_DOCS_INDEX.md) - Navigation guide
-
-### Proyecto General
-- [📋 Project README](README.md) - Descripción general
-- [📚 Technical Docs](docs/README.md) - Índice de todos los docs
-- [🤖 Copilot Guide](.github/copilot-instructions.md) - Desarrollo con IA
-- [🧪 Testing](docs/TEST_MANUAL.md) - Manual testing
-
-### Addons Específicos
-- [🛍️ website_sale_aplicoop](website_sale_aplicoop/README.md) - Sistema eskaera
-- [💰 product_sale_price_from_pricelist](product_sale_price_from_pricelist/README.md) - Auto-pricing
-- [📦 product_price_category_supplier](product_price_category_supplier/README.md) - Categorías por proveedor
-- [🐛 account_invoice_triple_discount_readonly](account_invoice_triple_discount_readonly/README.md) - Fix de descuentos
-
----
-
-## 📞 ¿Necesitas Ayuda?
-
-### Selecciona tu situación:
-
-| Situación | Qué leer |
-|-----------|----------|
-| "¿Qué es lazy loading?" | [LAZY_LOADING_QUICK_START.md](docs/LAZY_LOADING_QUICK_START.md) |
-| "¿Cómo instalo?" | [UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md](docs/UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md) |
-| "¿Cómo configuro?" | UPGRADE_INSTRUCTIONS → Configuration |
-| "¿Cómo verifico que funciona?" | UPGRADE_INSTRUCTIONS → Performance Verification |
-| "Algo no funciona" | UPGRADE_INSTRUCTIONS → Troubleshooting |
-| "¿Cómo hago rollback?" | UPGRADE_INSTRUCTIONS → Rollback Instructions |
-| "Detalles técnicos completos" | [LAZY_LOADING.md](docs/LAZY_LOADING.md) |
-| "¿Qué archivos fueron modificados?" | LAZY_LOADING.md → Code Changes |
-| "¿Cómo hago testing?" | LAZY_LOADING.md → Debugging & Testing |
-
----
-
-## ✅ Estado de Documentación
-
-- ✅ **Implementación**: Completada (v18.0.1.3.0)
-- ✅ **Quick Start**: Disponible (5 min)
-- ✅ **Upgrade Guide**: Disponible (paso a paso)
-- ✅ **Technical Docs**: Disponible (600+ líneas)
-- ✅ **Troubleshooting**: Disponible (4+ escenarios)
-- ✅ **Performance Metrics**: Documentadas (20x mejora)
-- ✅ **Backward Compatibility**: Confirmada (desactivable)
-
----
-
-## 🎓 Aprendizaje Rápido
-
-Para entender rápidamente cómo funciona:
-
-1. **El Problema** (2 min): Lee intro de [LAZY_LOADING_QUICK_START.md](docs/LAZY_LOADING_QUICK_START.md)
-2. **La Solución** (2 min): Lee "Installation" en QUICK_START
-3. **Verificación** (1 min): Sigue "Verificación Rápida" en QUICK_START
-4. **Listo** ✅
-
-Para profundizar → [LAZY_LOADING.md](docs/LAZY_LOADING.md)
-
----
-
-## 📈 Impacto de Performance
-
-| Métrica | Antes | Después | Mejora |
-|---------|-------|---------|--------|
-| Carga inicial | 10-20s | 500-800ms | **20x** 🚀 |
-| Carga página 2 | — | 200-400ms | — |
-| DOM size | 1000+ elementos | 20 elementos | **50x** |
-| Rendimiento | Lento | Rápido | ✅ |
-
----
-
-## 📝 Cambios Principales
-
-### Archivos Modificados (5)
-1. `/models/res_config_settings.py` - Configuración
-2. `/models/group_order.py` - Paginación backend
-3. `/controllers/website_sale.py` - Endpoints HTTP
-4. `/views/website_templates.xml` - Templates QWeb
-5. `/static/src/js/website_sale.js` - AJAX JavaScript
-
-### Documentación Creada (4)
-1. LAZY_LOADING_QUICK_START.md
-2. LAZY_LOADING_DOCS_INDEX.md
-3. LAZY_LOADING.md
-4. UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md
-
----
-
-## 🚀 Siguientes Pasos
-
-### Inmediato
-- [ ] Lee [LAZY_LOADING_QUICK_START.md](docs/LAZY_LOADING_QUICK_START.md)
-- [ ] Actualiza a v18.0.1.3.0 si no lo has hecho
-
-### Corto Plazo
-- [ ] Configura en Settings (si es necesario)
-- [ ] Verifica performance (sección "Verificación" en docs)
-
-### Largo Plazo
-- [ ] Monitorea performance en producción
-- [ ] Considera mejoras futuras (ver LAZY_LOADING.md)
-
----
-
-**Última Actualización**: 2026-02-16
-**Versión de Documentación**: 1.0
-**Odoo Version**: 18.0+
-**Lazy Loading Version**: 18.0.1.3.0+
-
-Para comenzar, selecciona la sección que más te interese arriba ☝️
diff --git a/DOCUMENTATION_UPDATE_SUMMARY.md b/DOCUMENTATION_UPDATE_SUMMARY.md
deleted file mode 100644
index b48219d..0000000
--- a/DOCUMENTATION_UPDATE_SUMMARY.md
+++ /dev/null
@@ -1,273 +0,0 @@
-# 📋 Resumen de Documentación Creada - Lazy Loading v18.0.1.3.0
-
-## ✅ Actualización Completa
-
-Se ha completado la actualización de toda la documentación del proyecto para reflejar la nueva feature de lazy loading implementada en `website_sale_aplicoop`.
-
----
-
-## 📄 Archivos Creados y Actualizados
-
-### 🆕 Nuevos Archivos Creados
-
-#### 1. [docs/LAZY_LOADING_QUICK_START.md](../docs/LAZY_LOADING_QUICK_START.md)
-**Tipo**: Guía Rápida (~100 líneas)
-**Contenido**:
-- ✅ TL;DR - Lo más importante
-- ✅ Qué necesitas hacer (actualizar y listo)
-- ✅ Métricas de mejora de performance (20x más rápido)
-- ✅ Configuración opcional (enable/disable, items per page)
-- ✅ Troubleshooting rápido (5 problemas comunes)
-- ✅ Verificación rápida (cómo comprobar que funciona)
-- ✅ Rollback instructions
-- ✅ Enlaces a documentación completa
-
-**Audiencia**: Usuarios que quieren "instalar y olvidar"
-
----
-
-#### 2. [docs/LAZY_LOADING.md](../docs/LAZY_LOADING.md)
-**Tipo**: Documentación Técnica Completa (~600 líneas)
-**Contenido**:
-- ✅ Descripción detallada del problema (carga 10-20s)
-- ✅ Solución implementada (lazy loading + configuración)
-- ✅ Arquitectura y diseño del sistema
-- ✅ Cambios de código por archivo (5 archivos modificados)
-- ✅ Configuración en res_config_settings
-- ✅ Endpoints HTTP (eskaera_shop, load_eskaera_page)
-- ✅ Métricas de rendimiento (20x más rápido)
-- ✅ Guía de testing y debugging
-- ✅ Troubleshooting avanzado
-- ✅ Roadmap de mejoras futuras
-
-**Audiencia**: Desarrolladores, Administradores Técnicos
-
----
-
-#### 3. [docs/UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md](../docs/UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md)
-**Tipo**: Guía de Actualización e Instalación (~180 líneas)
-**Contenido**:
-- ✅ Resumen de cambios en v18.0.1.3.0
-- ✅ Pasos de actualización paso a paso
-- ✅ Configuración de settings (3 opciones)
-- ✅ Valores recomendados y explicaciones
-- ✅ Checklist de validación post-instalación (4 pasos)
-- ✅ Troubleshooting de problemas comunes (4 escenarios):
- - "Load More" button not appearing
- - Products not loading on button click
- - Spinner never disappears
- - Page crashes after loading products
-- ✅ Método de verificación de rendimiento
-- ✅ Instrucciones de rollback
-- ✅ Notas importantes sobre comportamiento
-
-**Audiencia**: Administradores de Sistema, DevOps
-
----
-
-#### 3. [docs/LAZY_LOADING_DOCS_INDEX.md](../docs/LAZY_LOADING_DOCS_INDEX.md)
-**Tipo**: Índice Centralizado de Documentación
-**Contenido**:
-- ✅ Overview de la feature
-- ✅ Índice de los 4 documentos relacionados
-- ✅ Guía de selección (qué leer según tu rol)
-- ✅ Resumen de cambios de código
-- ✅ Checklist de implementación
-- ✅ Notas importantes y limitaciones
-- ✅ Enlaces rápidos a todos los docs
-- ✅ Información de impacto y performance
-
-**Audiencia**: Todos (punto de partida recomendado)
-
----
-
-#### 4. [website_sale_aplicoop/CHANGELOG.md](../website_sale_aplicoop/CHANGELOG.md)
-**Tipo**: Registro de Cambios
-**Contenido**:
-- ✅ v18.0.1.3.0: Lazy loading feature (2 puntos)
-- ✅ v18.0.1.2.0: UI improvements (3 puntos)
-- ✅ v18.0.1.0.0: Initial release
-
-**Audiencia**: Todos
-
----
-
-### 🔄 Archivos Actualizados
-
-#### 5. [README.md](../README.md) - Proyecto Principal
-**Cambios realizados**:
-- ✅ Añadido emoji 🚀 a website_sale_aplicoop en tabla de componentes
-- ✅ Añadida nota sobre lazy loading en v18.0.1.3.0 con referencia a docs
-- ✅ Añadidos dos enlaces nuevos en sección "Documentos Principales":
- - 🚀 [Lazy Loading Documentation](docs/LAZY_LOADING.md)
- - 📦 [Upgrade Instructions v18.0.1.3.0](docs/UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md)
-
----
-
-#### 6. [docs/README.md](../docs/README.md) - Índice de Documentación Técnica
-**Cambios realizados**:
-- ✅ Añadida nueva sección "Performance & Features (Nuevas)"
-- ✅ Tres nuevos enlaces:
- - [LAZY_LOADING_DOCS_INDEX.md](LAZY_LOADING_DOCS_INDEX.md)
- - [LAZY_LOADING.md](LAZY_LOADING.md)
- - [UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md](UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md)
-
----
-
-#### 7. [website_sale_aplicoop/README.md](../website_sale_aplicoop/README.md) - Addon Específico
-**Cambios realizados** (realizados en fase anterior):
-- ✅ Añadida feature de lazy loading en lista de features
-- ✅ Actualizado changelog con v18.0.1.3.0
-- ✅ Descripción detallada de lazy loading en changelog
-
----
-
-## 🎯 Estructura de Documentación Recomendada
-
-### Para Administradores/Usuarios:
-```
-1. Lee: docs/LAZY_LOADING_DOCS_INDEX.md (orientación)
-2. Luego: docs/UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md (instalación)
-3. Si hay dudas: Consulta sección de configuración en website_sale_aplicoop/README.md
-4. Si hay problemas: Troubleshooting en UPGRADE_INSTRUCTIONS
-```
-
-### Para Desarrolladores:
-```
-1. Lee: docs/LAZY_LOADING_DOCS_INDEX.md (visión general)
-2. Luego: docs/LAZY_LOADING.md (arquitectura técnica)
-3. Revisa: Cambios de código en LAZY_LOADING.md (sección "Code Changes")
-4. Debugging: Sección "Debugging & Testing" en LAZY_LOADING.md
-5. Mejoras: "Future Improvements" al final de LAZY_LOADING.md
-```
-
-### Para Troubleshooting:
-```
-1. Primero: docs/UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md (Troubleshooting section)
-2. Si persiste: docs/LAZY_LOADING.md (Debugging & Testing)
-3. Para rollback: UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md (Rollback Instructions)
-```
-
----
-
-## 📊 Cobertura de Documentación
-
-| Tema | Covered | Donde |
-|------|---------|-------|
-| **Problem Statement** | ✅ | LAZY_LOADING.md, UPGRADE_INSTRUCTIONS |
-| **Solution Overview** | ✅ | LAZY_LOADING_DOCS_INDEX.md, LAZY_LOADING.md |
-| **Architecture** | ✅ | LAZY_LOADING.md |
-| **Code Changes** | ✅ | LAZY_LOADING.md (por archivo) |
-| **Configuration** | ✅ | UPGRADE_INSTRUCTIONS, website_sale_aplicoop/README.md |
-| **Installation** | ✅ | UPGRADE_INSTRUCTIONS |
-| **Testing** | ✅ | LAZY_LOADING.md |
-| **Troubleshooting** | ✅ | UPGRADE_INSTRUCTIONS, LAZY_LOADING.md |
-| **Performance Metrics** | ✅ | Todos los docs |
-| **Rollback** | ✅ | UPGRADE_INSTRUCTIONS |
-| **Future Improvements** | ✅ | LAZY_LOADING.md |
-
----
-
-## 🔗 Matriz de Enlaces
-
-Todos los documentos están interconectados para facilitar la navegación:
-
-```
-README.md (principal)
-├── docs/LAZY_LOADING_DOCS_INDEX.md (índice)
-│ ├── docs/LAZY_LOADING.md (técnico)
-│ ├── docs/UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md (instalación)
-│ ├── website_sale_aplicoop/README.md (addon)
-│ └── website_sale_aplicoop/CHANGELOG.md (historial)
-├── docs/README.md (índice de docs)
-└── website_sale_aplicoop/README.md (addon directo)
-```
-
----
-
-## 📈 Métricas de la Documentación
-
-| Métrica | Valor |
-|---------|-------|
-| **Archivos nuevos creados** | 4 |
-| **Archivos actualizados** | 4 |
-| **Líneas de documentación** | ~1,400+ |
-| **Secciones documentadas** | 20+ |
-| **Ejemplos incluidos** | 15+ |
-| **Problemas cubiertos en troubleshooting** | 4 |
-| **Mejoras futuras documentadas** | 4 |
-
----
-
-## ✨ Highlights de la Documentación
-
-### 📌 Punto de Entrada Único
-- **[docs/LAZY_LOADING_DOCS_INDEX.md](../docs/LAZY_LOADING_DOCS_INDEX.md)** - Índice con guía de selección según rol
-
-### 📌 Documentación Técnica Completa
-- **[docs/LAZY_LOADING.md](../docs/LAZY_LOADING.md)** - 600+ líneas de detalles técnicos, cambios de código, testing, debugging
-
-### 📌 Guía Práctica de Instalación
-- **[docs/UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md](../docs/UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md)** - Paso a paso con checklist de validación y troubleshooting
-
-### 📌 Changelog Detallado
-- **[website_sale_aplicoop/CHANGELOG.md](../website_sale_aplicoop/CHANGELOG.md)** - Historial completo de versiones
-
-### 📌 README Actualizado
-- **[README.md](../README.md)** - Referencia al nuevo feature con enlaces
-
----
-
-## 🚀 Próximos Pasos
-
-La documentación está completa y lista para:
-
-1. ✅ **Publicación**: Todos los archivos están listos para ser compartidos
-2. ✅ **Integración**: Enlaces cruzados correctamente configurados
-3. ✅ **Accesibilidad**: Índice centralizado para encontrar información fácilmente
-4. ✅ **Mantenibilidad**: Estructura clara para futuras actualizaciones
-
-### Sugerencias Futuras:
-- Crear video tutorial (5-10 min) demostrando lazy loading en acción
-- Agregar métricas en vivo de performance en Settings UI
-- Crear tests automatizados para validar configuración
-
----
-
-## 📞 Preguntas Frecuentes Documentadas
-
-| Pregunta | Respuesta en |
-|----------|-------------|
-| ¿Qué es lazy loading? | LAZY_LOADING.md intro |
-| ¿Cómo instalo? | UPGRADE_INSTRUCTIONS |
-| ¿Cómo configuro? | UPGRADE_INSTRUCTIONS + website_sale_aplicoop/README.md |
-| ¿Cómo veo mejora de performance? | UPGRADE_INSTRUCTIONS (Performance Verification) |
-| ¿Qué pasa si falla? | UPGRADE_INSTRUCTIONS (Troubleshooting) |
-| ¿Puedo deshabilitarlo? | Sí, UPGRADE_INSTRUCTIONS sección Configuration |
-| ¿Cómo hago rollback? | UPGRADE_INSTRUCTIONS (Rollback Instructions) |
-| ¿Detalles técnicos? | LAZY_LOADING.md |
-
----
-
-## 🎓 Aprendizaje de Documentación
-
-Esta documentación demuestra:
-- ✅ Documentación técnica completa y detallada
-- ✅ Guías prácticas paso a paso
-- ✅ Índices centralizados para fácil navegación
-- ✅ Troubleshooting proactivo
-- ✅ Interconexión de documentos
-- ✅ Diferentes niveles de profundidad (overview → técnico)
-- ✅ Cobertura completa de usuario y desarrollador
-
----
-
-**Estado**: ✅ COMPLETADO
-**Documentación Creada**: 3 archivos nuevos, 4 actualizados
-**Líneas Totales**: 1,200+
-**Fecha**: 2026-02-16
-**Versión Aplicable**: 18.0.1.3.0+
-
----
-
-¿Necesitas que ajuste algo en la documentación o que cree documentos adicionales?
diff --git a/README.md b/README.md
index dd547b3..4ce28ad 100644
--- a/README.md
+++ b/README.md
@@ -38,13 +38,7 @@ Este repositorio contiene los addons personalizados para Kidekoop, un sistema co
| [account_invoice_triple_discount_readonly](account_invoice_triple_discount_readonly/) | Fix para bug de descuentos acumulados | ✅ Estable |
| [product_price_category_supplier](product_price_category_supplier/) | Gestión de categorías por proveedor | ✅ Estable |
| [product_sale_price_from_pricelist](product_sale_price_from_pricelist/) | Auto-cálculo precio venta desde compra | ✅ Estable |
-| [website_sale_aplicoop](website_sale_aplicoop/) | Sistema completo de eskaera web | ✅ **v18.0.1.3.1** - Estable |
-
-**✨ Feature v18.0.1.3.0**: `website_sale_aplicoop` incluye **lazy loading configurable** para mejorar el rendimiento de carga de productos (10-20s → 500-800ms).
-
-**🔧 Fixes v18.0.1.3.1**: Correcciones críticas en cálculo de fechas y refactor de template rendering para evitar errores QWeb.
-
-Ver [docs/LAZY_LOADING.md](docs/LAZY_LOADING.md) y [docs/FINAL_SOLUTION_SUMMARY.md](docs/FINAL_SOLUTION_SUMMARY.md) para detalles.
+| [website_sale_aplicoop](website_sale_aplicoop/) | Sistema completo de eskaera web | ✅ Estable |
## 🚀 Quick Start
@@ -163,8 +157,6 @@ Cada addon incluye su propio README.md con:
- [GitHub Copilot Instructions](.github/copilot-instructions.md) - Guía para desarrollo con AI
- [Documentación Técnica](docs/) - Guías de instalación, linters, y troubleshooting
-- **[🚀 Lazy Loading Documentation](docs/LAZY_LOADING.md)** - Guía técnica completa sobre la nueva feature de carga lazy
-- **[📦 Upgrade Instructions v18.0.1.3.0](docs/UPGRADE_INSTRUCTIONS_v18.0.1.3.0.md)** - Guía de actualización e instalación de lazy loading
- [Makefile](Makefile) - Comandos disponibles
- [requirements.txt](requirements.txt) - Dependencias Python
- [oca_dependencies.txt](oca_dependencies.txt) - Repositorios OCA necesarios
diff --git a/account_invoice_triple_discount_readonly/tests/test_account_move.py b/account_invoice_triple_discount_readonly/tests/test_account_move.py
index a84ff41..4b86fa5 100644
--- a/account_invoice_triple_discount_readonly/tests/test_account_move.py
+++ b/account_invoice_triple_discount_readonly/tests/test_account_move.py
@@ -10,88 +10,72 @@ class TestAccountMove(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
-
+
# Create a partner
- cls.partner = cls.env["res.partner"].create(
- {
- "name": "Test Customer",
- "email": "customer@test.com",
- }
- )
-
+ cls.partner = cls.env["res.partner"].create({
+ "name": "Test Customer",
+ "email": "customer@test.com",
+ })
+
# Create a product
- cls.product = cls.env["product.product"].create(
- {
- "name": "Test Product Invoice",
- "type": "consu",
- "list_price": 200.0,
- "standard_price": 100.0,
- }
- )
-
+ cls.product = cls.env["product.product"].create({
+ "name": "Test Product Invoice",
+ "type": "consu",
+ "list_price": 200.0,
+ "standard_price": 100.0,
+ })
+
# Create tax
- cls.tax = cls.env["account.tax"].create(
- {
- "name": "Test Tax 10%",
- "amount": 10.0,
- "amount_type": "percent",
- "type_tax_use": "sale",
- }
- )
-
+ cls.tax = cls.env["account.tax"].create({
+ "name": "Test Tax 10%",
+ "amount": 10.0,
+ "amount_type": "percent",
+ "type_tax_use": "sale",
+ })
+
# Create an invoice
- cls.invoice = cls.env["account.move"].create(
- {
- "move_type": "out_invoice",
- "partner_id": cls.partner.id,
- "invoice_date": "2026-01-01",
- }
- )
-
+ cls.invoice = cls.env["account.move"].create({
+ "move_type": "out_invoice",
+ "partner_id": cls.partner.id,
+ "invoice_date": "2026-01-01",
+ })
+
# Create invoice line
- cls.invoice_line = cls.env["account.move.line"].create(
- {
- "move_id": cls.invoice.id,
- "product_id": cls.product.id,
- "quantity": 5,
- "price_unit": 200.0,
- "discount1": 10.0,
- "discount2": 5.0,
- "discount3": 2.0,
- "tax_ids": [(6, 0, [cls.tax.id])],
- }
- )
+ cls.invoice_line = cls.env["account.move.line"].create({
+ "move_id": cls.invoice.id,
+ "product_id": cls.product.id,
+ "quantity": 5,
+ "price_unit": 200.0,
+ "discount1": 10.0,
+ "discount2": 5.0,
+ "discount3": 2.0,
+ "tax_ids": [(6, 0, [cls.tax.id])],
+ })
def test_invoice_line_discount_readonly(self):
"""Test that discount field is readonly in invoice lines"""
field = self.invoice_line._fields["discount"]
- self.assertTrue(
- field.readonly, "Discount field should be readonly in invoice lines"
- )
+ self.assertTrue(field.readonly, "Discount field should be readonly in invoice lines")
def test_invoice_line_write_with_explicit_discounts(self):
"""Test writing invoice line with explicit discounts"""
- self.invoice_line.write(
- {
- "discount": 30.0, # Should be ignored
- "discount1": 15.0,
- "discount2": 10.0,
- "discount3": 5.0,
- }
- )
-
+ self.invoice_line.write({
+ "discount": 30.0, # Should be ignored
+ "discount1": 15.0,
+ "discount2": 10.0,
+ "discount3": 5.0,
+ })
+
self.assertEqual(self.invoice_line.discount1, 15.0)
self.assertEqual(self.invoice_line.discount2, 10.0)
self.assertEqual(self.invoice_line.discount3, 5.0)
def test_invoice_line_legacy_discount(self):
"""Test legacy discount behavior in invoice lines"""
- self.invoice_line.write(
- {
- "discount": 20.0,
- }
- )
-
+ self.invoice_line.write({
+ "discount": 20.0,
+ })
+
# Should map to discount1 and reset others
self.assertEqual(self.invoice_line.discount1, 20.0)
self.assertEqual(self.invoice_line.discount2, 0.0)
@@ -99,14 +83,12 @@ class TestAccountMove(TransactionCase):
def test_invoice_line_price_calculation(self):
"""Test that price subtotal is calculated correctly with triple discount"""
- self.invoice_line.write(
- {
- "discount1": 10.0,
- "discount2": 5.0,
- "discount3": 0.0,
- }
- )
-
+ self.invoice_line.write({
+ "discount1": 10.0,
+ "discount2": 5.0,
+ "discount3": 0.0,
+ })
+
# Base: 5 * 200 = 1000
# After 10% discount: 900
# After 5% discount: 855
@@ -117,19 +99,17 @@ class TestAccountMove(TransactionCase):
def test_multiple_invoice_lines(self):
"""Test multiple invoice lines with different discounts"""
- line2 = self.env["account.move.line"].create(
- {
- "move_id": self.invoice.id,
- "product_id": self.product.id,
- "quantity": 3,
- "price_unit": 150.0,
- "discount1": 20.0,
- "discount2": 10.0,
- "discount3": 5.0,
- "tax_ids": [(6, 0, [self.tax.id])],
- }
- )
-
+ line2 = self.env["account.move.line"].create({
+ "move_id": self.invoice.id,
+ "product_id": self.product.id,
+ "quantity": 3,
+ "price_unit": 150.0,
+ "discount1": 20.0,
+ "discount2": 10.0,
+ "discount3": 5.0,
+ "tax_ids": [(6, 0, [self.tax.id])],
+ })
+
# Verify both lines have correct discounts
self.assertEqual(self.invoice_line.discount1, 10.0)
self.assertEqual(line2.discount1, 20.0)
@@ -140,13 +120,11 @@ class TestAccountMove(TransactionCase):
"""Test updating quantity doesn't affect discounts"""
initial_discount1 = self.invoice_line.discount1
initial_discount2 = self.invoice_line.discount2
-
- self.invoice_line.write(
- {
- "quantity": 10,
- }
- )
-
+
+ self.invoice_line.write({
+ "quantity": 10,
+ })
+
# Discounts should remain unchanged
self.assertEqual(self.invoice_line.discount1, initial_discount1)
self.assertEqual(self.invoice_line.discount2, initial_discount2)
@@ -156,13 +134,11 @@ class TestAccountMove(TransactionCase):
def test_invoice_line_update_price(self):
"""Test updating price doesn't affect discounts"""
initial_discount1 = self.invoice_line.discount1
-
- self.invoice_line.write(
- {
- "price_unit": 250.0,
- }
- )
-
+
+ self.invoice_line.write({
+ "price_unit": 250.0,
+ })
+
# Discount should remain unchanged
self.assertEqual(self.invoice_line.discount1, initial_discount1)
# Price should be updated
@@ -170,20 +146,18 @@ class TestAccountMove(TransactionCase):
def test_invoice_with_zero_discounts(self):
"""Test invoice line with all zero discounts"""
- self.invoice_line.write(
- {
- "discount1": 0.0,
- "discount2": 0.0,
- "discount3": 0.0,
- }
- )
-
+ self.invoice_line.write({
+ "discount1": 0.0,
+ "discount2": 0.0,
+ "discount3": 0.0,
+ })
+
# All discounts should be zero
self.assertEqual(self.invoice_line.discount, 0.0)
self.assertEqual(self.invoice_line.discount1, 0.0)
self.assertEqual(self.invoice_line.discount2, 0.0)
self.assertEqual(self.invoice_line.discount3, 0.0)
-
+
# Subtotal should be quantity * price
expected = 5 * 200
self.assertEqual(self.invoice_line.price_subtotal, expected)
@@ -191,23 +165,23 @@ class TestAccountMove(TransactionCase):
def test_invoice_line_combined_operations(self):
"""Test combined operations on invoice line"""
# Update multiple fields at once
- self.invoice_line.write(
- {
- "quantity": 8,
- "price_unit": 180.0,
- "discount1": 12.0,
- "discount2": 6.0,
- "discount3": 0.0, # Reset discount3 explicitly
- }
- )
-
+ self.invoice_line.write({
+ "quantity": 8,
+ "price_unit": 180.0,
+ "discount1": 12.0,
+ "discount2": 6.0,
+ "discount3": 0.0, # Reset discount3 explicitly
+ })
+
# All fields should be updated correctly
self.assertEqual(self.invoice_line.quantity, 8)
self.assertEqual(self.invoice_line.price_unit, 180.0)
self.assertEqual(self.invoice_line.discount1, 12.0)
self.assertEqual(self.invoice_line.discount2, 6.0)
self.assertEqual(self.invoice_line.discount3, 0.0)
-
+
# Calculate expected subtotal: 8 * 180 * (1-0.12) * (1-0.06)
expected = 8 * 180 * 0.88 * 0.94
- self.assertAlmostEqual(self.invoice_line.price_subtotal, expected, places=2)
+ self.assertAlmostEqual(
+ self.invoice_line.price_subtotal, expected, places=2
+ )
diff --git a/account_invoice_triple_discount_readonly/tests/test_purchase_order.py b/account_invoice_triple_discount_readonly/tests/test_purchase_order.py
index cd12fe5..fc22dd0 100644
--- a/account_invoice_triple_discount_readonly/tests/test_purchase_order.py
+++ b/account_invoice_triple_discount_readonly/tests/test_purchase_order.py
@@ -10,47 +10,37 @@ class TestPurchaseOrder(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
-
+
# Create a supplier
- cls.supplier = cls.env["res.partner"].create(
- {
- "name": "Test Supplier",
- "email": "supplier@test.com",
- "supplier_rank": 1,
- }
- )
-
- # Create a product template first, then get the variant
- cls.product_template = cls.env["product.template"].create(
- {
- "name": "Test Product PO",
- "type": "consu",
- "list_price": 150.0,
- "standard_price": 80.0,
- }
- )
- # Get the auto-created product variant
- cls.product = cls.product_template.product_variant_ids[0]
-
+ cls.supplier = cls.env["res.partner"].create({
+ "name": "Test Supplier",
+ "email": "supplier@test.com",
+ "supplier_rank": 1,
+ })
+
+ # Create a product
+ cls.product = cls.env["product.product"].create({
+ "name": "Test Product PO",
+ "type": "product",
+ "list_price": 150.0,
+ "standard_price": 80.0,
+ })
+
# Create a purchase order
- cls.purchase_order = cls.env["purchase.order"].create(
- {
- "partner_id": cls.supplier.id,
- }
- )
-
+ cls.purchase_order = cls.env["purchase.order"].create({
+ "partner_id": cls.supplier.id,
+ })
+
# Create purchase order line
- cls.po_line = cls.env["purchase.order.line"].create(
- {
- "order_id": cls.purchase_order.id,
- "product_id": cls.product.id,
- "product_qty": 10,
- "price_unit": 150.0,
- "discount1": 10.0,
- "discount2": 5.0,
- "discount3": 2.0,
- }
- )
+ cls.po_line = cls.env["purchase.order.line"].create({
+ "order_id": cls.purchase_order.id,
+ "product_id": cls.product.id,
+ "product_qty": 10,
+ "price_unit": 150.0,
+ "discount1": 10.0,
+ "discount2": 5.0,
+ "discount3": 2.0,
+ })
def test_po_line_discount_readonly(self):
"""Test that discount field is readonly in PO lines"""
@@ -59,27 +49,23 @@ class TestPurchaseOrder(TransactionCase):
def test_po_line_write_with_explicit_discounts(self):
"""Test writing PO line with explicit discounts"""
- self.po_line.write(
- {
- "discount": 25.0, # Should be ignored
- "discount1": 12.0,
- "discount2": 8.0,
- "discount3": 4.0,
- }
- )
-
+ self.po_line.write({
+ "discount": 25.0, # Should be ignored
+ "discount1": 12.0,
+ "discount2": 8.0,
+ "discount3": 4.0,
+ })
+
self.assertEqual(self.po_line.discount1, 12.0)
self.assertEqual(self.po_line.discount2, 8.0)
self.assertEqual(self.po_line.discount3, 4.0)
def test_po_line_legacy_discount(self):
"""Test legacy discount behavior in PO lines"""
- self.po_line.write(
- {
- "discount": 18.0,
- }
- )
-
+ self.po_line.write({
+ "discount": 18.0,
+ })
+
# Should map to discount1 and reset others
self.assertEqual(self.po_line.discount1, 18.0)
self.assertEqual(self.po_line.discount2, 0.0)
@@ -87,35 +73,33 @@ class TestPurchaseOrder(TransactionCase):
def test_po_line_price_calculation(self):
"""Test that price subtotal is calculated correctly with triple discount"""
- self.po_line.write(
- {
- "discount1": 15.0,
- "discount2": 10.0,
- "discount3": 5.0,
- }
- )
-
+ self.po_line.write({
+ "discount1": 15.0,
+ "discount2": 10.0,
+ "discount3": 5.0,
+ })
+
# Base: 10 * 150 = 1500
# After 15% discount: 1275
# After 10% discount: 1147.5
# After 5% discount: 1090.125
expected_subtotal = 10 * 150 * 0.85 * 0.90 * 0.95
- self.assertAlmostEqual(self.po_line.price_subtotal, expected_subtotal, places=2)
+ self.assertAlmostEqual(
+ self.po_line.price_subtotal, expected_subtotal, places=2
+ )
def test_multiple_po_lines(self):
"""Test multiple PO lines with different discounts"""
- line2 = self.env["purchase.order.line"].create(
- {
- "order_id": self.purchase_order.id,
- "product_id": self.product.id,
- "product_qty": 5,
- "price_unit": 120.0,
- "discount1": 20.0,
- "discount2": 15.0,
- "discount3": 10.0,
- }
- )
-
+ line2 = self.env["purchase.order.line"].create({
+ "order_id": self.purchase_order.id,
+ "product_id": self.product.id,
+ "product_qty": 5,
+ "price_unit": 120.0,
+ "discount1": 20.0,
+ "discount2": 15.0,
+ "discount3": 10.0,
+ })
+
# Verify both lines have correct discounts
self.assertEqual(self.po_line.discount1, 15.0)
self.assertEqual(line2.discount1, 20.0)
@@ -126,13 +110,11 @@ class TestPurchaseOrder(TransactionCase):
"""Test updating quantity doesn't affect discounts"""
initial_discount1 = self.po_line.discount1
initial_discount2 = self.po_line.discount2
-
- self.po_line.write(
- {
- "product_qty": 20,
- }
- )
-
+
+ self.po_line.write({
+ "product_qty": 20,
+ })
+
# Discounts should remain unchanged
self.assertEqual(self.po_line.discount1, initial_discount1)
self.assertEqual(self.po_line.discount2, initial_discount2)
@@ -142,13 +124,11 @@ class TestPurchaseOrder(TransactionCase):
def test_po_line_update_price(self):
"""Test updating price doesn't affect discounts"""
initial_discount1 = self.po_line.discount1
-
- self.po_line.write(
- {
- "price_unit": 200.0,
- }
- )
-
+
+ self.po_line.write({
+ "price_unit": 200.0,
+ })
+
# Discount should remain unchanged
self.assertEqual(self.po_line.discount1, initial_discount1)
# Price should be updated
@@ -156,20 +136,18 @@ class TestPurchaseOrder(TransactionCase):
def test_po_with_zero_discounts(self):
"""Test PO line with all zero discounts"""
- self.po_line.write(
- {
- "discount1": 0.0,
- "discount2": 0.0,
- "discount3": 0.0,
- }
- )
-
+ self.po_line.write({
+ "discount1": 0.0,
+ "discount2": 0.0,
+ "discount3": 0.0,
+ })
+
# All discounts should be zero
self.assertEqual(self.po_line.discount, 0.0)
self.assertEqual(self.po_line.discount1, 0.0)
self.assertEqual(self.po_line.discount2, 0.0)
self.assertEqual(self.po_line.discount3, 0.0)
-
+
# Subtotal should be quantity * price
expected = 10 * 150
self.assertEqual(self.po_line.price_subtotal, expected)
@@ -177,40 +155,38 @@ class TestPurchaseOrder(TransactionCase):
def test_po_line_combined_operations(self):
"""Test combined operations on PO line"""
# Update multiple fields at once
- self.po_line.write(
- {
- "product_qty": 15,
- "price_unit": 175.0,
- "discount1": 18.0,
- "discount2": 12.0,
- "discount3": 6.0,
- }
- )
-
+ self.po_line.write({
+ "product_qty": 15,
+ "price_unit": 175.0,
+ "discount1": 18.0,
+ "discount2": 12.0,
+ "discount3": 6.0,
+ })
+
# All fields should be updated correctly
self.assertEqual(self.po_line.product_qty, 15)
self.assertEqual(self.po_line.price_unit, 175.0)
self.assertEqual(self.po_line.discount1, 18.0)
self.assertEqual(self.po_line.discount2, 12.0)
self.assertEqual(self.po_line.discount3, 6.0)
-
+
# Calculate expected subtotal
expected = 15 * 175 * 0.82 * 0.88 * 0.94
- self.assertAlmostEqual(self.po_line.price_subtotal, expected, places=2)
+ self.assertAlmostEqual(
+ self.po_line.price_subtotal, expected, places=2
+ )
def test_po_confirm_with_discounts(self):
"""Test confirming PO doesn't alter discounts"""
- self.po_line.write(
- {
- "discount1": 10.0,
- "discount2": 5.0,
- "discount3": 2.0,
- }
- )
-
+ self.po_line.write({
+ "discount1": 10.0,
+ "discount2": 5.0,
+ "discount3": 2.0,
+ })
+
# Confirm the purchase order
self.purchase_order.button_confirm()
-
+
# Discounts should remain unchanged after confirmation
self.assertEqual(self.po_line.discount1, 10.0)
self.assertEqual(self.po_line.discount2, 5.0)
diff --git a/account_invoice_triple_discount_readonly/tests/test_triple_discount_mixin.py b/account_invoice_triple_discount_readonly/tests/test_triple_discount_mixin.py
index 33b931f..f4600a2 100644
--- a/account_invoice_triple_discount_readonly/tests/test_triple_discount_mixin.py
+++ b/account_invoice_triple_discount_readonly/tests/test_triple_discount_mixin.py
@@ -10,45 +10,35 @@ class TestTripleDiscountMixin(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
-
+
# Create a partner
- cls.partner = cls.env["res.partner"].create(
- {
- "name": "Test Partner",
- }
- )
-
- # Create a product template first, then get the variant
- cls.product_template = cls.env["product.template"].create(
- {
- "name": "Test Product",
- "type": "consu",
- "list_price": 100.0,
- "standard_price": 50.0,
- }
- )
- # Get the auto-created product variant
- cls.product = cls.product_template.product_variant_ids[0]
-
+ cls.partner = cls.env["res.partner"].create({
+ "name": "Test Partner",
+ })
+
+ # Create a product
+ cls.product = cls.env["product.product"].create({
+ "name": "Test Product",
+ "type": "product",
+ "list_price": 100.0,
+ "standard_price": 50.0,
+ })
+
# Create a purchase order
- cls.purchase_order = cls.env["purchase.order"].create(
- {
- "partner_id": cls.partner.id,
- }
- )
-
+ cls.purchase_order = cls.env["purchase.order"].create({
+ "partner_id": cls.partner.id,
+ })
+
# Create a purchase order line
- cls.po_line = cls.env["purchase.order.line"].create(
- {
- "order_id": cls.purchase_order.id,
- "product_id": cls.product.id,
- "product_qty": 10,
- "price_unit": 100.0,
- "discount1": 10.0,
- "discount2": 5.0,
- "discount3": 2.0,
- }
- )
+ cls.po_line = cls.env["purchase.order.line"].create({
+ "order_id": cls.purchase_order.id,
+ "product_id": cls.product.id,
+ "product_qty": 10,
+ "price_unit": 100.0,
+ "discount1": 10.0,
+ "discount2": 5.0,
+ "discount3": 2.0,
+ })
def test_discount_field_is_readonly(self):
"""Test that the discount field is readonly"""
@@ -58,20 +48,18 @@ class TestTripleDiscountMixin(TransactionCase):
def test_write_with_explicit_discounts(self):
"""Test writing with explicit discount1, discount2, discount3"""
# Write with explicit discounts
- self.po_line.write(
- {
- "discount": 20.0, # This should be ignored
- "discount1": 15.0,
- "discount2": 10.0,
- "discount3": 5.0,
- }
- )
-
+ self.po_line.write({
+ "discount": 20.0, # This should be ignored
+ "discount1": 15.0,
+ "discount2": 10.0,
+ "discount3": 5.0,
+ })
+
# Verify explicit discounts were applied
self.assertEqual(self.po_line.discount1, 15.0)
self.assertEqual(self.po_line.discount2, 10.0)
self.assertEqual(self.po_line.discount3, 5.0)
-
+
# The computed discount field should reflect the combined discounts
# Formula: 100 - (100 * (1 - 0.15) * (1 - 0.10) * (1 - 0.05))
expected_discount = 100 - (100 * 0.85 * 0.90 * 0.95)
@@ -79,13 +67,11 @@ class TestTripleDiscountMixin(TransactionCase):
def test_write_only_discount1(self):
"""Test writing only discount1 explicitly"""
- self.po_line.write(
- {
- "discount": 25.0, # This should be ignored
- "discount1": 20.0,
- }
- )
-
+ self.po_line.write({
+ "discount": 25.0, # This should be ignored
+ "discount1": 20.0,
+ })
+
# Only discount1 should change
self.assertEqual(self.po_line.discount1, 20.0)
# Others should remain unchanged
@@ -94,13 +80,11 @@ class TestTripleDiscountMixin(TransactionCase):
def test_write_only_discount2(self):
"""Test writing only discount2 explicitly"""
- self.po_line.write(
- {
- "discount": 30.0, # This should be ignored
- "discount2": 12.0,
- }
- )
-
+ self.po_line.write({
+ "discount": 30.0, # This should be ignored
+ "discount2": 12.0,
+ })
+
# Only discount2 should change
self.assertEqual(self.po_line.discount2, 12.0)
# Others should remain unchanged from previous test
@@ -109,13 +93,11 @@ class TestTripleDiscountMixin(TransactionCase):
def test_write_only_discount3(self):
"""Test writing only discount3 explicitly"""
- self.po_line.write(
- {
- "discount": 35.0, # This should be ignored
- "discount3": 8.0,
- }
- )
-
+ self.po_line.write({
+ "discount": 35.0, # This should be ignored
+ "discount3": 8.0,
+ })
+
# Only discount3 should change
self.assertEqual(self.po_line.discount3, 8.0)
# Others should remain unchanged from previous tests
@@ -125,21 +107,17 @@ class TestTripleDiscountMixin(TransactionCase):
def test_write_legacy_discount_only(self):
"""Test legacy behavior: writing only discount field"""
# Reset to known state first
- self.po_line.write(
- {
- "discount1": 10.0,
- "discount2": 5.0,
- "discount3": 2.0,
- }
- )
-
+ self.po_line.write({
+ "discount1": 10.0,
+ "discount2": 5.0,
+ "discount3": 2.0,
+ })
+
# Write only discount (legacy behavior)
- self.po_line.write(
- {
- "discount": 25.0,
- }
- )
-
+ self.po_line.write({
+ "discount": 25.0,
+ })
+
# Should map to discount1 and reset others
self.assertEqual(self.po_line.discount1, 25.0)
self.assertEqual(self.po_line.discount2, 0.0)
@@ -148,24 +126,20 @@ class TestTripleDiscountMixin(TransactionCase):
def test_write_multiple_times(self):
"""Test writing multiple times to ensure consistency"""
# First write
- self.po_line.write(
- {
- "discount1": 10.0,
- "discount2": 10.0,
- }
- )
-
+ self.po_line.write({
+ "discount1": 10.0,
+ "discount2": 10.0,
+ })
+
self.assertEqual(self.po_line.discount1, 10.0)
self.assertEqual(self.po_line.discount2, 10.0)
-
+
# Second write
- self.po_line.write(
- {
- "discount": 5.0,
- "discount3": 5.0,
- }
- )
-
+ self.po_line.write({
+ "discount": 5.0,
+ "discount3": 5.0,
+ })
+
# discount3 should change, others remain
self.assertEqual(self.po_line.discount1, 10.0)
self.assertEqual(self.po_line.discount2, 10.0)
@@ -173,14 +147,12 @@ class TestTripleDiscountMixin(TransactionCase):
def test_write_zero_discounts(self):
"""Test writing zero discounts"""
- self.po_line.write(
- {
- "discount1": 0.0,
- "discount2": 0.0,
- "discount3": 0.0,
- }
- )
-
+ self.po_line.write({
+ "discount1": 0.0,
+ "discount2": 0.0,
+ "discount3": 0.0,
+ })
+
self.assertEqual(self.po_line.discount1, 0.0)
self.assertEqual(self.po_line.discount2, 0.0)
self.assertEqual(self.po_line.discount3, 0.0)
@@ -189,22 +161,18 @@ class TestTripleDiscountMixin(TransactionCase):
def test_write_combined_scenario(self):
"""Test a realistic combined scenario"""
# Initial state
- self.po_line.write(
- {
- "discount1": 15.0,
- "discount2": 5.0,
- "discount3": 0.0,
- }
- )
-
+ self.po_line.write({
+ "discount1": 15.0,
+ "discount2": 5.0,
+ "discount3": 0.0,
+ })
+
# User tries to update discount field (should be ignored if explicit discounts present)
- self.po_line.write(
- {
- "discount": 50.0,
- "discount1": 20.0,
- }
- )
-
+ self.po_line.write({
+ "discount": 50.0,
+ "discount1": 20.0,
+ })
+
# discount1 should be updated, others unchanged
self.assertEqual(self.po_line.discount1, 20.0)
self.assertEqual(self.po_line.discount2, 5.0)
@@ -212,14 +180,12 @@ class TestTripleDiscountMixin(TransactionCase):
def test_discount_calculation_accuracy(self):
"""Test that discount calculation is accurate"""
- self.po_line.write(
- {
- "discount1": 10.0,
- "discount2": 10.0,
- "discount3": 10.0,
- }
- )
-
+ self.po_line.write({
+ "discount1": 10.0,
+ "discount2": 10.0,
+ "discount3": 10.0,
+ })
+
# Combined discount: 100 - (100 * 0.9 * 0.9 * 0.9) = 27.1
expected = 100 - (100 * 0.9 * 0.9 * 0.9)
self.assertAlmostEqual(self.po_line.discount, expected, places=2)
@@ -227,15 +193,13 @@ class TestTripleDiscountMixin(TransactionCase):
def test_write_without_discount_field(self):
"""Test writing other fields without touching discount fields"""
initial_discount1 = self.po_line.discount1
-
+
# Write other fields
- self.po_line.write(
- {
- "product_qty": 20,
- "price_unit": 150.0,
- }
- )
-
+ self.po_line.write({
+ "product_qty": 20,
+ "price_unit": 150.0,
+ })
+
# Discounts should remain unchanged
self.assertEqual(self.po_line.discount1, initial_discount1)
# But other fields should be updated
diff --git a/docker-compose.yml b/docker-compose.yml
index c83e9da..711ac1b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -21,8 +21,8 @@ services:
db:
condition: service_healthy
ports:
- - "8069:8069"
- - "8072:8072"
+ - "8070:8069"
+ - "8073:8072"
environment:
HOST: 0.0.0.0
PORT: "8069"
diff --git a/docs/FINAL_SOLUTION_SUMMARY.md b/docs/FINAL_SOLUTION_SUMMARY.md
deleted file mode 100644
index f038cd6..0000000
--- a/docs/FINAL_SOLUTION_SUMMARY.md
+++ /dev/null
@@ -1,290 +0,0 @@
-# Template Error - FINAL SOLUTION
-
-**Status**: ✅ **PERMANENTLY FIXED** via commit 5721687
-
-## Problem Statement
-
-The `TypeError: 'NoneType' object is not callable` error in the `website_sale_aplicoop.eskaera_shop_products` template was caused by QWeb's strict parsing limitations.
-
-**Error Location**:
-```
-Template: website_sale_aplicoop.eskaera_shop_products
-Path: /t/t/div/div/form
-Node:
-```
-
-## Root Cause Analysis
-
-QWeb template engine cannot parse:
-
-1. **Complex nested conditionals in t-set**:
- ```xml
- ❌ FAILS
-
- ```
-
-2. **Chained 'or' operators in t-attf-* attributes**:
- ```xml
- ❌ FAILS
- t-attf-data-price="{{ price_info.get('price') or product.list_price or 0 }}"
- ```
-
-3. **Deep object attribute chains with conditionals**:
- ```xml
- ❌ FAILS
- t-set="uom" t-value="product.uom_id.category_id.name if (product.uom_id and product.uom_id.category_id) else ''"
- ```
-
-## Solution Approach
-
-**Move ALL logic from template to controller** where Python can safely process it.
-
-### Previous Failed Attempts
-
-| Commit | Approach | Result |
-|--------|----------|--------|
-| df57233 | Add `or` operators in attributes | ❌ Still failed |
-| 0a0cf5a | Complex nested conditionals in t-set | ❌ Still failed |
-| 8e5a4a3 | Three-step pattern with `or` chains | ⚠️ Partially worked but template still had logic |
-
-### Final Solution (Commit 5721687)
-
-**Strategy**: Let Python do all the work, pass clean data to template
-
-#### Step 1: Create Helper Method in Controller
-
-```python
-def _prepare_product_display_info(self, product, product_price_info):
- """Pre-process all display values for QWeb safety.
-
- Returns dict with:
- - display_price: float, never None
- - safe_uom_category: string, never None
- """
- # Get price - all logic here, not in template
- price_data = product_price_info.get(product.id, {})
- price = price_data.get("price", product.list_price) if price_data else product.list_price
- price_safe = float(price) if price else 0.0
-
- # Get UoM - all logic here, not in template
- uom_category_name = ""
- if product.uom_id:
- if product.uom_id.category_id:
- uom_category_name = product.uom_id.category_id.name or ""
-
- return {
- "display_price": price_safe,
- "safe_uom_category": uom_category_name,
- }
-```
-
-#### Step 2: Build Dict in Both Endpoints
-
-```python
-# In eskaera_shop() method
-product_display_info = {}
-for product in products:
- display_info = self._prepare_product_display_info(product, product_price_info)
- product_display_info[product.id] = display_info
-
-# In load_eskaera_page() method (lazy loading)
-product_display_info = {}
-for product in products_page:
- display_info = self._prepare_product_display_info(product, product_price_info)
- product_display_info[product.id] = display_info
-```
-
-#### Step 3: Pass to Template
-
-```python
-return request.render(
- "website_sale_aplicoop.eskaera_shop",
- {
- # ... other variables ...
- "product_display_info": product_display_info,
- }
-)
-```
-
-#### Step 4: Simplify Template to Simple Variable References
-
-```xml
-
-
-
-
-
-
-
-```
-
-## Why This Works
-
-1. **Python handles complexity**: Conditional logic runs in Python where it's safe
-2. **Template gets clean data**: Only simple variable references, no expressions
-3. **QWeb is happy**: `.get()` method calls are simple enough for QWeb parser
-4. **No None values**: Values are pre-processed to never be None
-5. **Maintainable**: Clear separation: Controller = logic, Template = display
-
-## Files Modified
-
-### website_sale_aplicoop/controllers/website_sale.py
-
-**Added**:
-- `_prepare_product_display_info(product, product_price_info)` method (lines 390-417)
-- Calls to `_prepare_product_display_info()` in `eskaera_shop()` (lines 1062-1065)
-- Calls to `_prepare_product_display_info()` in `load_eskaera_page()` (lines 1260-1263)
-- Pass `product_display_info` to both template renders
-
-**Total additions**: ~55 lines of Python
-
-### website_sale_aplicoop/views/website_templates.xml
-
-**Changed**:
-- Line ~1170: `display_price` - from complex conditional to simple `dict.get()`
-- Line ~1225: `safe_uom_category` - from nested conditional to simple `dict.get()`
-
-**Total changes**: -10 lines of complex XML, +5 lines of simple XML
-
-## Verification
-
-✅ Module loads without parsing errors:
-```
-Module website_sale_aplicoop loaded in 0.62s, 612 queries (+612 other)
-```
-
-✅ Template variables in database match expectations
-
-✅ No runtime errors when accessing eskaera_shop page
-
-## Key Learnings
-
-### QWeb Parsing Rules
-
-**Safe in t-set**:
-- ✅ `dict.get('key')`
-- ✅ `dict.get('key', default)`
-- ✅ Simple method calls with literals
-- ✅ Basic `or` between simple values (with caution)
-
-**Unsafe in t-set**:
-- ❌ Nested `if-else` conditionals
-- ❌ Complex boolean expressions
-- ❌ Chained method calls with conditionals
-
-**For attributes (t-attf-*)**:
-- ✅ Simple variable references: `{{ var }}`
-- ✅ Simple method calls: `{{ obj.method() }}`
-- ⚠️ `or` operators may work but unreliable
-- ❌ Anything complex
-
-### Best Practice Pattern
-
-```
-CONTROLLER (Python):
-┌─────────────────────────────────────────┐
-│ Process data │
-│ Handle None/defaults │
-│ Build clean dicts │
-│ Return display-ready values │
-└──────────────────┬──────────────────────┘
- │
- ↓
- product_display_info
- │
- ↓
-TEMPLATE (QWeb):
-┌──────────────────────────────────────────┐
-│ Simple dict.get() calls only │
-│ NO conditional logic │
-│ NO complex expressions │
-│ Just display variables │
-└──────────────────────────────────────────┘
-```
-
-This pattern ensures QWeb stays happy while keeping code clean and maintainable.
-
-## Deployment Checklist
-
-- ✅ Code committed (5721687)
-- ✅ Module loads without errors
-- ✅ Template renders without 500 error
-- ✅ Pre-commit hooks satisfied
-- ✅ Ready for production
-
-## Future Prevention
-
-When adding new display logic to templates:
-
-1. **Ask**: "Does this involve conditional logic?"
- - If NO → Can go in template
- - If YES → Must go in controller
-
-2. **Never put in template**:
- - `if-else` statements
- - Complex `or` chains
- - Deep attribute chains with fallbacks
- - Method calls that might return None
-
-3. **Always process in controller**:
- - Pre-calculate values
- - Handle None cases
- - Build display dicts
- - Pass to template
-
----
-
-**Solution Complexity**: ⭐⭐ (Simple and elegant)
-**Code Quality**: ⭐⭐⭐⭐⭐ (Clean separation of concerns)
-**Maintainability**: ⭐⭐⭐⭐⭐ (Easy to extend)
-**Production Ready**: ✅ YES
-
----
-
-## FINAL VERIFICATION (2026-02-16 - Session Complete)
-
-### ✅ All Tests Passing
-
-**1. Database Template Verification**:
-```
-SELECT id, key FROM ir_ui_view
-WHERE key = 'website_sale_aplicoop.eskaera_shop_products';
-Result: 4591 | website_sale_aplicoop.eskaera_shop_products ✅
-```
-
-**2. Template Content Check**:
-```
-SELECT arch_db::text LIKE '%order_id_safe%' FROM ir_ui_view
-WHERE id = 4591;
-Result: t (TRUE) ✅
-```
-
-**3. Module Load Test**:
-```
-odoo -d odoo -u website_sale_aplicoop --stop-after-init
-Result: Module loaded in 0.63s, 612 queries, NO ERRORS ✅
-```
-
-**4. Web Interface Test**:
-```
-curl -s -i http://localhost:8069/web | head -1
-Result: HTTP/1.1 200 OK - No 500 errors ✅
-```
-
-**5. Lazy Loading Pages**:
-```
-/eskaera/2/load-page?page=2 HTTP/1.1" 200 ✅
-/eskaera/2/load-page?page=3 HTTP/1.1" 200 ✅
-```
-
-**6. Odoo Log Verification**:
-- No TypeError in logs ✅
-- No traceback in logs ✅
-- No NoneType is not callable errors ✅
-
-### Status: ✅ PRODUCTION READY
-
-The template error has been fully resolved and verified. All systems operational.
diff --git a/docs/FIX_TEMPLATE_ERROR_SUMMARY.md b/docs/FIX_TEMPLATE_ERROR_SUMMARY.md
deleted file mode 100644
index 58f2222..0000000
--- a/docs/FIX_TEMPLATE_ERROR_SUMMARY.md
+++ /dev/null
@@ -1,332 +0,0 @@
-# Fix Template Error Summary - website_sale_aplicoop
-
-**Date**: 2026-02-16
-**Final Status**: ✅ PERMANENTLY RESOLVED
-**Solution Commit**: 5721687
-**Version**: 18.0.1.1.1
-
----
-
-## Problem
-
-The `eskaera_shop_products` QWeb template was throwing a `TypeError: 'NoneType' object is not callable` error when loading the store page.
-
-### Root Cause - QWeb Parsing Limitations
-
-QWeb has strict limitations on what expressions it can parse:
-
-1. **Complex nested conditionals in t-set fail**
- ```xml
- ❌
- ```
-
-2. **Direct 'or' in attributes unreliable**
- ```xml
- ❌
- ```
-
-3. **Deep object chains with conditionals fail**
- ```xml
- ❌ t-set="uom" t-value="product.uom_id.category_id.name if product.uom_id.category_id else ''"
- ```
-
----
-
-## Solution
-
-### Architecture: Move Logic to Controller
-
-**Final insight**: Don't fight QWeb's limitations. Move ALL complex logic to the Python controller where it belongs.
-
-#### The Pattern
-
-```
-CONTROLLER (Python)
- ↓ (process data, handle None)
-product_display_info = {
- product.id: {
- 'display_price': 10.99, # Always a float, never None
- 'safe_uom_category': 'Weight' # Always a string, never None
- }
-}
- ↓ (pass clean data to template)
-TEMPLATE (QWeb)
- ↓ (simple dict.get() calls, no logic)
-
-```
-
-#### Implementation
-
-**In Controller** - Added `_prepare_product_display_info()` method:
-
-```python
-def _prepare_product_display_info(self, product, product_price_info):
- """Pre-process all display values for QWeb safety.
-
- All logic happens HERE in Python, not in template.
- Returns dict with safe values ready for display.
- """
- # Get price - handle None safely
- price_data = product_price_info.get(product.id, {})
- price = price_data.get("price", product.list_price) if price_data else product.list_price
- price_safe = float(price) if price else 0.0
-
- # Get UoM category - handle None/nested attributes safely
- uom_category_name = ""
- if product.uom_id:
- if product.uom_id.category_id:
- uom_category_name = product.uom_id.category_id.name or ""
-
- return {
- "display_price": price_safe, # Never None
- "safe_uom_category": uom_category_name, # Never None
- }
-```
-
-**In Template** - Simple dict.get() calls:
-
-```xml
-
-
-
-
-
-
-
-```
-
----
-
-## What Changed
-
-### Files Modified
-
-1. **website_sale_aplicoop/controllers/website_sale.py**
- - Added `_prepare_product_display_info()` method (lines 390-417)
- - Generate `product_display_info` dict in `eskaera_shop()` (lines 1062-1065)
- - Generate `product_display_info` dict in `load_eskaera_page()` (lines 1260-1263)
- - Pass to template renders
-
-2. **website_sale_aplicoop/views/website_templates.xml**
- - Removed complex conditional expressions from template
- - Replaced with simple `dict.get()` calls
- - No business logic remains in template
-
-### Iteration History
-
-| Commit | Approach | Result |
-|--------|----------|--------|
-| df57233 | Add `or` operators in attributes | ❌ Error persisted |
-| 0a0cf5a | Complex nested conditionals in t-set | ❌ Error persisted |
-| 8e5a4a3 | Three-step pattern with `or` chains | ⚠️ Error persisted |
-| 5721687 | Move logic to controller | ✅ SOLVED |
-
-
-
-### Why This Works
-
-1. **Two-step computation**: Separates extraction from fallback logic
-2. **Python short-circuit evaluation**: `or` operator properly handles None values
-3. **Avoids complex conditionals**: Simple `or` chains instead of nested `if-else`
-4. **QWeb-compatible**: The `or` operator works reliably when value is pre-extracted
-5. **Readable**: Clear intent - extract value, then fall back
-
----
-
-## Changes Made
-
-### File: `website_sale_aplicoop/views/website_templates.xml`
-
-**Location 1**: Price computation (lines 1165-1177)
-
-**Before**:
-```xml
-
-
-
-```
-
-**After**:
-```xml
-
-
-
-
-```
-
-**Location 2**: Form element (lines 1215-1228)
-
-**Before**:
-```xml
-
-
-