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 @@ />