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 @@
+
+
+
+
+
+ Products with stock ≤ this value show "Low Stock" ribbon. Stock = 0 shows "Out of Stock" ribbon.
+
+
+
+
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 @@
/>