Compare commits
10 commits
0d5f0be88c
...
eb6b53db1a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb6b53db1a | ||
|
|
9807feef90 | ||
|
|
8b728b8b7c | ||
|
|
23e156a13e | ||
|
|
a128c1ee1e | ||
|
|
1f37f289ba | ||
|
|
10ae5bcbf6 | ||
|
|
d90f043617 | ||
|
|
a1317b8ade | ||
|
|
5ba8ddda92 |
10 changed files with 2176 additions and 331 deletions
193
.github/copilot-instructions.md
vendored
193
.github/copilot-instructions.md
vendored
|
|
@ -147,7 +147,7 @@ addons-cm/
|
|||
### Local Development
|
||||
|
||||
```bash
|
||||
# Iniciar entorno
|
||||
# Iniciar entorno (puertos: 8070=web, 8073=longpolling)
|
||||
docker-compose up -d
|
||||
|
||||
# Actualizar addon
|
||||
|
|
@ -158,20 +158,37 @@ 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
|
||||
# Ejecutar todos los checks (usa .pre-commit-config.yaml)
|
||||
pre-commit run --all-files
|
||||
|
||||
# O usar Makefile
|
||||
make lint # Solo linting
|
||||
make format # Formatear código
|
||||
make check-addon # Verificar addon específico
|
||||
# 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
|
||||
```
|
||||
|
||||
### 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
|
||||
|
|
@ -179,6 +196,37 @@ make check-addon # Verificar addon específico
|
|||
- 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
|
||||
|
|
@ -233,6 +281,35 @@ 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`)
|
||||
|
|
@ -287,7 +364,22 @@ 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**: Use `product_sale_price_from_pricelist` with proper configuration
|
||||
**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
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
|
|
@ -364,11 +456,41 @@ 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
|
||||
|
||||
|
|
@ -397,6 +519,61 @@ 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
|
||||
|
|
@ -406,6 +583,6 @@ Tags: `[ADD]`, `[FIX]`, `[IMP]`, `[REF]`, `[REM]`, `[I18N]`, `[DOC]`
|
|||
|
||||
---
|
||||
|
||||
**Last Updated**: 2026-02-12
|
||||
**Last Updated**: 2026-02-16
|
||||
**Odoo Version**: 18.0
|
||||
**Python Version**: 3.10+
|
||||
|
|
|
|||
189
product_price_category_supplier/README.rst
Normal file
189
product_price_category_supplier/README.rst
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
======================================
|
||||
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)
|
||||
|
|
@ -2,7 +2,8 @@
|
|||
# @author Santi Noreña (<santi@criptomart.net>)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import fields, models
|
||||
from odoo import fields
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ProductPricelistItem(models.Model):
|
||||
|
|
@ -13,8 +14,8 @@ class ProductPricelistItem(models.Model):
|
|||
ondelete={"last_purchase_price": "set default"},
|
||||
)
|
||||
|
||||
def _compute_price(self, product, qty, uom, date, currency=None):
|
||||
result = super()._compute_price(product, qty, uom, date, currency)
|
||||
def _compute_price(self, product, quantity, uom, date, currency=None):
|
||||
result = super()._compute_price(product, quantity, uom, date, currency)
|
||||
if self.compute_price == "formula" and self.base == "last_purchase_price":
|
||||
result = product.sudo().last_purchase_price_received
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@
|
|||
"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",
|
||||
|
|
|
|||
|
|
@ -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,6 +313,406 @@ 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.
|
||||
|
|
@ -359,7 +759,6 @@ 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")
|
||||
|
|
@ -488,8 +887,8 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
category_id,
|
||||
len(products),
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
except (ValueError, TypeError) as e:
|
||||
_logger.warning("eskaera_shop: Invalid category filter: %s", str(e))
|
||||
|
||||
# Prepare supplier info dict: {product.id: 'Supplier (City)'}
|
||||
product_supplier_info = {}
|
||||
|
|
@ -504,71 +903,17 @@ 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 = None
|
||||
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():
|
||||
# Log pricelist selection status
|
||||
if pricelist:
|
||||
_logger.info(
|
||||
"eskaera_shop: Using configured Aplicoop pricelist: %s (id=%s, currency=%s)",
|
||||
"eskaera_shop: Using 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."
|
||||
)
|
||||
|
|
@ -795,57 +1140,8 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
)
|
||||
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(
|
||||
"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,
|
||||
)
|
||||
# Resolve pricelist using centralized helper
|
||||
pricelist = self._resolve_pricelist()
|
||||
|
||||
if not pricelist:
|
||||
_logger.error(
|
||||
|
|
@ -1071,7 +1367,6 @@ 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(
|
||||
|
|
@ -1586,7 +1881,7 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
|
||||
# Find if product already exists in draft
|
||||
existing_line = existing_draft.order_line.filtered(
|
||||
lambda l: l.product_id.id == product_id
|
||||
lambda line: line.product_id.id == product_id
|
||||
)
|
||||
|
||||
if existing_line:
|
||||
|
|
@ -1624,7 +1919,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 %d", existing_draft_id)
|
||||
_logger.info("Deleted existing draft %s", existing_draft_id)
|
||||
|
||||
# Create new draft with current items
|
||||
sale_order = request.env["sale.order"].create(
|
||||
|
|
@ -1740,77 +2035,32 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
|
||||
_logger.info("confirm_eskaera data received: %s", data)
|
||||
|
||||
# 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": "order_id is required"}),
|
||||
[("Content-Type", "application/json")],
|
||||
status=400,
|
||||
)
|
||||
|
||||
# Convert to int
|
||||
# Validate request using helper
|
||||
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,
|
||||
)
|
||||
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))
|
||||
return request.make_response(
|
||||
json.dumps({"error": f"Order is {group_order.state}"}),
|
||||
json.dumps({"error": str(e)}),
|
||||
[("Content-Type", "application/json")],
|
||||
status=400,
|
||||
)
|
||||
|
||||
current_user = request.env.user
|
||||
_logger.info("Current user: %d", current_user.id)
|
||||
|
||||
# Validate that the user has a partner_id
|
||||
if not current_user.partner_id:
|
||||
_logger.error(
|
||||
"confirm_eskaera: User %d has no partner_id", current_user.id
|
||||
)
|
||||
# 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))
|
||||
return request.make_response(
|
||||
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"}),
|
||||
json.dumps({"error": str(e)}),
|
||||
[("Content-Type", "application/json")],
|
||||
status=400,
|
||||
)
|
||||
|
|
@ -1839,47 +2089,6 @@ 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
|
||||
|
|
@ -1941,64 +2150,14 @@ class AplicoopWebsiteSale(WebsiteSale):
|
|||
_logger.error("sale_order_lines: %s", sale_order_lines)
|
||||
raise
|
||||
|
||||
# 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}"
|
||||
# Build confirmation message using helper
|
||||
message_data = self._build_confirmation_message(
|
||||
sale_order, group_order, is_delivery
|
||||
)
|
||||
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}"
|
||||
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"]
|
||||
|
||||
response_data = {
|
||||
"success": True,
|
||||
|
|
@ -2110,9 +2269,6 @@ 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
|
||||
|
|
|
|||
12
website_sale_aplicoop/data/website_menus.xml
Normal file
12
website_sale_aplicoop/data/website_menus.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Website Menu Item for Eskaera (Group Orders) -->
|
||||
<record id="website_eskaera_menu" model="website.menu">
|
||||
<field name="name">Eskaera</field>
|
||||
<field name="url">/eskaera</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">50</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
353
website_sale_aplicoop/tests/test_helper_methods_phase1.py
Normal file
353
website_sale_aplicoop/tests/test_helper_methods_phase1.py
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
# 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
|
||||
286
website_sale_aplicoop/tests/test_phase2_eskaera_shop.py
Normal file
286
website_sale_aplicoop/tests/test_phase2_eskaera_shop.py
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
# 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
|
||||
669
website_sale_aplicoop/tests/test_phase3_confirm_eskaera.py
Normal file
669
website_sale_aplicoop/tests/test_phase3_confirm_eskaera.py
Normal file
|
|
@ -0,0 +1,669 @@
|
|||
# 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)
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="website.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='website_info_settings']" position="after">
|
||||
<xpath expr="//block[@id='website_info_settings']" position="after">
|
||||
<h2>Aplicoop Settings</h2>
|
||||
<div class="row mt16 o_settings_container" id="aplicoop_settings">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue