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