diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index b1da0c3..418e309 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
+# 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
@@ -307,18 +399,18 @@ access_model_user,model.name.user,model_model_name,base.group_user,1,1,1,0
```javascript
odoo.define("module.tour", function (require) {
- "use strict";
- var tour = require("web_tour.tour");
- tour.register(
- "tour_name",
- {
- test: true,
- url: "/web",
- },
- [
- // Tour steps
- ],
- );
+ "use strict";
+ var tour = require("web_tour.tour");
+ tour.register(
+ "tour_name",
+ {
+ test: true,
+ url: "/web",
+ },
+ [
+ // Tour steps
+ ],
+ );
});
```
@@ -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+
diff --git a/product_price_category_supplier/README.rst b/product_price_category_supplier/README.rst
new file mode 100644
index 0000000..90f2085
--- /dev/null
+++ b/product_price_category_supplier/README.rst
@@ -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)
diff --git a/website_sale_aplicoop/views/res_config_settings_views.xml b/website_sale_aplicoop/views/res_config_settings_views.xml
index fc1c206..e1537f0 100644
--- a/website_sale_aplicoop/views/res_config_settings_views.xml
+++ b/website_sale_aplicoop/views/res_config_settings_views.xml
@@ -5,7 +5,7 @@
res.config.settings
-
+
Aplicoop Settings