From 2a005a9d5a1f70cfd4907cd48c229e76583bc39d Mon Sep 17 00:00:00 2001 From: snt Date: Wed, 25 Feb 2026 19:48:39 +0100 Subject: [PATCH] feat(website_sale_aplicoop): sistema de ribbons basado en stock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Añadidos ribbons 'Sin Stock' (rojo) y 'Pocas Existencias' (amarillo) - Nuevo campo configurable: umbral de stock bajo (default: 5.0) - Campos computed en product.product: * is_out_of_stock: True cuando qty_available <= 0 * is_low_stock: True cuando 0 < qty_available <= threshold * dynamic_ribbon_id: ribbon automático según nivel de stock - Ordenamiento mejorado: productos con stock primero, sin stock al final - Template actualizado: * Muestra ribbon de stock en tarjeta de producto * Deshabilita botón add-to-cart cuando producto sin stock * Cambia icono a 'fa-ban' en productos sin stock - Vista de configuración: campo 'Low Stock Threshold' en Settings > Shop Performance - Solo aplica a productos type='consu' (almacenables) - Tests: 112 pasados, 0 fallos --- website_sale_aplicoop/__manifest__.py | 2 + .../data/product_ribbon_data.xml | 22 +++++++ website_sale_aplicoop/models/group_order.py | 9 ++- .../models/product_extension.py | 65 +++++++++++++++++++ .../models/res_config_settings.py | 8 +++ .../views/res_config_settings_views.xml | 14 ++++ .../views/website_templates.xml | 18 +++-- 7 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 website_sale_aplicoop/data/product_ribbon_data.xml diff --git a/website_sale_aplicoop/__manifest__.py b/website_sale_aplicoop/__manifest__.py index ac9b0dd..b0cf8d3 100644 --- a/website_sale_aplicoop/__manifest__.py +++ b/website_sale_aplicoop/__manifest__.py @@ -26,6 +26,8 @@ "data/website_menus.xml", # Datos: Cron jobs "data/cron.xml", + # Datos: Product ribbons for stock levels + "data/product_ribbon_data.xml", # Vistas de seguridad "security/ir.model.access.csv", "security/record_rules.xml", diff --git a/website_sale_aplicoop/data/product_ribbon_data.xml b/website_sale_aplicoop/data/product_ribbon_data.xml new file mode 100644 index 0000000..f79aff7 --- /dev/null +++ b/website_sale_aplicoop/data/product_ribbon_data.xml @@ -0,0 +1,22 @@ + + + + + + + + Sin Stock + left + #FFFFFF + #d9534f + + + + + Pocas Existencias + left + #FFFFFF + #ffc107 + + + diff --git a/website_sale_aplicoop/models/group_order.py b/website_sale_aplicoop/models/group_order.py index 4016eaf..8090f27 100644 --- a/website_sale_aplicoop/models/group_order.py +++ b/website_sale_aplicoop/models/group_order.py @@ -477,9 +477,14 @@ class GroupOrder(models.Model): len(products), ) - # Sort products by website_sequence (primary) and name (secondary, case-insensitive) + # Sort products: in-stock first, then out-of-stock, maintaining sequence+name within each group + # is_out_of_stock is Boolean: False (in stock) comes first, True (out of stock) comes last return products.sorted( - lambda p: (p.product_tmpl_id.website_sequence, (p.name or "").lower()) + lambda p: ( + p.is_out_of_stock, # Boolean: False < True, so in-stock products first + p.product_tmpl_id.website_sequence, + (p.name or "").lower(), + ) ) def _get_products_paginated(self, order_id, page=1, per_page=20): diff --git a/website_sale_aplicoop/models/product_extension.py b/website_sale_aplicoop/models/product_extension.py index cc1dda8..6fa1134 100644 --- a/website_sale_aplicoop/models/product_extension.py +++ b/website_sale_aplicoop/models/product_extension.py @@ -21,6 +21,71 @@ class ProductProduct(models.Model): help="Group orders where this product is available", ) + is_out_of_stock = fields.Boolean( + string="Is Out of Stock", + compute="_compute_stock_ribbons", + store=False, + help="True if qty_available <= 0", + ) + + is_low_stock = fields.Boolean( + string="Is Low Stock", + compute="_compute_stock_ribbons", + store=False, + help="True if 0 < qty_available <= threshold", + ) + + dynamic_ribbon_id = fields.Many2one( + "product.ribbon", + string="Dynamic Stock Ribbon", + compute="_compute_stock_ribbons", + store=False, + help="Auto-assigned ribbon based on stock levels", + ) + + @api.depends("qty_available", "type") + def _compute_stock_ribbons(self): + """Compute stock-based ribbons dynamically.""" + # Obtener ribbons (usar sudo para evitar permisos) + out_of_stock_ribbon = self.env.ref( + "website_sale_aplicoop.out_of_stock_ribbon", raise_if_not_found=False + ) + low_stock_ribbon = self.env.ref( + "website_sale_aplicoop.low_stock_ribbon", raise_if_not_found=False + ) + + # Obtener threshold de configuración + threshold = float( + self.env["ir.config_parameter"] + .sudo() + .get_param("website_sale_aplicoop.low_stock_threshold", default="5.0") + ) + + for product in self: + # Solo para productos almacenables (type='consu' o 'product' en algunos casos) + # En Odoo 18: 'consu' = Goods (almacenable), 'service' = Service, 'combo' = Combo + if product.type != "consu": + product.is_out_of_stock = False + product.is_low_stock = False + product.dynamic_ribbon_id = False + continue + + # Lógica de stock + qty = product.qty_available + + if qty <= 0: + product.is_out_of_stock = True + product.is_low_stock = False + product.dynamic_ribbon_id = out_of_stock_ribbon + elif qty <= threshold: + product.is_out_of_stock = False + product.is_low_stock = True + product.dynamic_ribbon_id = low_stock_ribbon + else: + product.is_out_of_stock = False + product.is_low_stock = False + product.dynamic_ribbon_id = False # No ribbon + @api.model def _get_products_for_group_order(self, order_id): """Backward-compatible delegation to `group.order` discovery. diff --git a/website_sale_aplicoop/models/res_config_settings.py b/website_sale_aplicoop/models/res_config_settings.py index f4a56f0..d7f7ef8 100644 --- a/website_sale_aplicoop/models/res_config_settings.py +++ b/website_sale_aplicoop/models/res_config_settings.py @@ -27,6 +27,14 @@ class ResConfigSettings(models.TransientModel): help="Number of products to load per page in group order shop. Minimum 5, Maximum 100.", ) + low_stock_threshold = fields.Float( + string="Low Stock Threshold", + config_parameter="website_sale_aplicoop.low_stock_threshold", + default=5.0, + help="Products with stock below or equal to this value will show 'Low Stock' ribbon. " + "Products with stock = 0 will show 'Out of Stock' ribbon and cannot be added to cart.", + ) + @staticmethod def _get_products_per_page_selection(records): """Return default page sizes.""" diff --git a/website_sale_aplicoop/views/res_config_settings_views.xml b/website_sale_aplicoop/views/res_config_settings_views.xml index f2892a6..5fa5b66 100644 --- a/website_sale_aplicoop/views/res_config_settings_views.xml +++ b/website_sale_aplicoop/views/res_config_settings_views.xml @@ -53,6 +53,20 @@ +
+
+
+
+
diff --git a/website_sale_aplicoop/views/website_templates.xml b/website_sale_aplicoop/views/website_templates.xml index b609928..8a35549 100644 --- a/website_sale_aplicoop/views/website_templates.xml +++ b/website_sale_aplicoop/views/website_templates.xml @@ -1095,6 +1095,15 @@ /> + + + + +
@@ -1266,13 +1275,14 @@ />