diff --git a/stock_picking_batch_custom/README.rst b/stock_picking_batch_custom/README.rst index 964a1a7..9893a8f 100644 --- a/stock_picking_batch_custom/README.rst +++ b/stock_picking_batch_custom/README.rst @@ -1,66 +1,13 @@ -============================ +=============================== Stock Picking Batch Custom -============================ +=============================== -Visión general -============== -Este módulo amplía las operaciones detalladas y añade un resumen por producto -en los lotes de picking: +.. contents:: + :local: -- ``picking_partner_id`` (Partner del albarán) para identificar cliente/proveedor. -- ``product_categ_id`` (Categoría de producto) para ordenar y agrupar. -- ``is_collected`` (Recogido) como check manual en cada línea para marcar si se ha - recolectado. -- Nueva pestaña **Product Summary** con totales por producto (demandado, hecho, - pendiente), categoría y el check de recogido consolidado. - -Instalación -=========== - -Actualizar o instalar el módulo: - -:: - - docker-compose run --rm odoo odoo -d odoo --stop-after-init -u stock_picking_batch_custom - -Configuración -============= - -No requiere configuración adicional. Para usar las columnas: - -- Abrir un **Lote de picking**. -- Ir a la pestaña **Detailed Operations**. -- Abrir el **selector de columnas** y activar *Partner*, *Product Category* y *Collected* según necesidad. - -Uso -=== - -1. Accede a **Inventory > Operations > Batch Transfers** y abre un lote. -2. Pestaña **Detailed Operations**: usa el selector de columnas para activar: - - - **Partner** (``picking_partner_id``) para ver el cliente/proveedor. - - **Product Category** (``product_categ_id``) para ordenar/agrupación por categoría. - - **Collected** (``is_collected``) para marcar manualmente líneas recolectadas. - -3. Pestaña **Product Summary**: consulta los totales por producto (demandado, - hecho y pendiente) y marca el check de recogido consolidado si corresponde. - -4. Ordena o agrupa por categoría en cualquiera de las vistas según convenga. - -Contribuidores -============== - -* Criptomart - -Créditos -======== - -Autor ------ - -* Criptomart - -Financiador ------------ - -* Elika Bilbo +.. include:: readme/DESCRIPTION.rst +.. include:: readme/INSTALL.rst +.. include:: readme/CONFIGURE.rst +.. include:: readme/USAGE.rst +.. include:: readme/CONTRIBUTORS.rst +.. include:: readme/CREDITS.rst diff --git a/stock_picking_batch_custom/__manifest__.py b/stock_picking_batch_custom/__manifest__.py index 27e80bd..4882701 100644 --- a/stock_picking_batch_custom/__manifest__.py +++ b/stock_picking_batch_custom/__manifest__.py @@ -13,8 +13,6 @@ "stock_picking_batch", ], "data": [ - "security/ir.model.access.csv", "views/stock_move_line_views.xml", - "views/stock_picking_batch_views.xml", ], } diff --git a/stock_picking_batch_custom/models/__init__.py b/stock_picking_batch_custom/models/__init__.py index 24709d5..5a228fe 100644 --- a/stock_picking_batch_custom/models/__init__.py +++ b/stock_picking_batch_custom/models/__init__.py @@ -1,2 +1 @@ from . import stock_move_line # noqa: F401 -from . import stock_picking_batch # noqa: F401 diff --git a/stock_picking_batch_custom/models/stock_move_line.py b/stock_picking_batch_custom/models/stock_move_line.py index 82bd6d2..cb4ad48 100644 --- a/stock_picking_batch_custom/models/stock_move_line.py +++ b/stock_picking_batch_custom/models/stock_move_line.py @@ -10,14 +10,8 @@ class StockMoveLine(models.Model): product_categ_id = fields.Many2one( comodel_name="product.category", - string="Product Category (Batch)", + string="Product Category", related="product_id.categ_id", store=True, readonly=True, ) - - is_collected = fields.Boolean( - string="Collected", - default=False, - copy=False, - ) diff --git a/stock_picking_batch_custom/models/stock_picking_batch.py b/stock_picking_batch_custom/models/stock_picking_batch.py deleted file mode 100644 index c2c77fc..0000000 --- a/stock_picking_batch_custom/models/stock_picking_batch.py +++ /dev/null @@ -1,202 +0,0 @@ -# Copyright 2026 Criptomart -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import api -from odoo import fields -from odoo import models - - -class StockPickingBatch(models.Model): - _inherit = "stock.picking.batch" - - summary_line_ids = fields.One2many( - comodel_name="stock.picking.batch.summary.line", - inverse_name="batch_id", - string="Product Summary", - compute="_compute_summary_line_ids", - store=True, - readonly=True, - ) - - @api.depends( - "move_line_ids.move_id.product_id", - "move_line_ids.move_id.product_uom_qty", - "move_line_ids.move_id.quantity", - "move_line_ids.move_id.product_uom", - "move_line_ids.move_id.state", - ) - def _compute_summary_line_ids(self): - """Aggregate move quantities per product and keep collected flag. - - - Demand: move.product_uom_qty converted to product UoM. - - Done: move.quantity converted to product UoM. - - Pending: demand - done. - - Keep is_collected value if product already present. - - Skip cancelled moves. - """ - - for batch in self: - # Base: limpiar si no hay líneas - if not batch.move_line_ids: - batch.summary_line_ids = [fields.Command.clear()] - continue - - previous_collected = { - line.product_id.id: line.is_collected - for line in batch.summary_line_ids - if line.product_id - } - # Use regular dict to avoid accidental creation of empty entries - aggregates = {} - - # Demand per move (count once per move) - for move in batch.move_ids: - if move.state == "cancel" or not move.product_id: - continue - product = move.product_id - product_uom = product.uom_id - demand = move.product_uom._compute_quantity( - move.product_uom_qty, product_uom, rounding_method="HALF-UP" - ) - if product.id not in aggregates: - aggregates[product.id] = { - "product_id": product.id, - "product_uom_id": product_uom.id, - "qty_demanded": 0.0, - "qty_done": 0.0, - } - aggregates[product.id]["qty_demanded"] += demand - - # Done per move line - for line in batch.move_line_ids: - move = line.move_id - if move and move.state == "cancel": - continue - product = line.product_id - if not product: - continue - product_uom = product.uom_id - done = line.product_uom_id._compute_quantity( - line.quantity, product_uom, rounding_method="HALF-UP" - ) - if product.id not in aggregates: - aggregates[product.id] = { - "product_id": product.id, - "product_uom_id": product_uom.id, - "qty_demanded": 0.0, - "qty_done": 0.0, - } - aggregates[product.id]["qty_done"] += done - - commands = [] - products_in_totals = set() - existing_by_product = { - line.product_id.id: line for line in batch.summary_line_ids - } - - for product_id, values in aggregates.items(): - # Double-check: skip if product_id is not valid - if not product_id or not values.get("product_id"): - continue - products_in_totals.add(product_id) - existing_line = existing_by_product.get(product_id) - is_collected = previous_collected.get(product_id, False) - if existing_line: - commands.append( - fields.Command.update( - existing_line.id, - { - "product_uom_id": values["product_uom_id"], - "qty_demanded": values["qty_demanded"], - "qty_done": values["qty_done"], - "is_collected": is_collected, - }, - ) - ) - else: - # Ensure all required fields are present before create - if values["product_id"] and values["product_uom_id"]: - commands.append( - fields.Command.create( - { - "product_id": values["product_id"], - "product_uom_id": values["product_uom_id"], - "qty_demanded": values["qty_demanded"], - "qty_done": values["qty_done"], - "is_collected": is_collected, - } - ) - ) - - obsolete_lines = [ - fields.Command.unlink(line.id) - for line in batch.summary_line_ids - if line.product_id.id not in products_in_totals - ] - - if commands or obsolete_lines: - batch.summary_line_ids = commands + obsolete_lines - else: - batch.summary_line_ids = [fields.Command.clear()] - - -class StockPickingBatchSummaryLine(models.Model): - _name = "stock.picking.batch.summary.line" - _description = "Batch Product Summary Line" - _order = "product_categ_id, product_id" - - _sql_constraints = [ - ( - "product_required", - "CHECK(product_id IS NOT NULL)", - "Product is required for summary lines.", - ), - ] - - batch_id = fields.Many2one( - comodel_name="stock.picking.batch", - ondelete="cascade", - required=True, - index=True, - ) - product_id = fields.Many2one( - comodel_name="product.product", - required=True, - index=True, - ) - product_categ_id = fields.Many2one( - comodel_name="product.category", - string="Product Category", - related="product_id.categ_id", - store=True, - readonly=True, - ) - product_uom_id = fields.Many2one( - comodel_name="uom.uom", - string="Unit of Measure", - required=True, - ) - qty_demanded = fields.Float( - string="Demanded Quantity", - digits="Product Unit of Measure", - required=True, - default=0.0, - ) - qty_done = fields.Float( - string="Done Quantity", - digits="Product Unit of Measure", - required=True, - default=0.0, - ) - qty_pending = fields.Float( - string="Pending Quantity", - digits="Product Unit of Measure", - compute="_compute_qty_pending", - store=True, - ) - is_collected = fields.Boolean(string="Collected", default=False) - - @api.depends("qty_demanded", "qty_done") - def _compute_qty_pending(self): - for line in self: - line.qty_pending = line.qty_demanded - line.qty_done diff --git a/stock_picking_batch_custom/readme/DESCRIPTION.rst b/stock_picking_batch_custom/readme/DESCRIPTION.rst index da6edad..d8fc673 100644 --- a/stock_picking_batch_custom/readme/DESCRIPTION.rst +++ b/stock_picking_batch_custom/readme/DESCRIPTION.rst @@ -1,14 +1,11 @@ -Este módulo amplía las operaciones detalladas y añade un resumen por producto -en los lotes de picking: +Este módulo añade dos columnas opcionales en las operaciones detalladas de los +lotes de picking: - ``picking_partner_id`` (Partner del albarán) para que el personal de almacén - identifique rápido el cliente/proveedor. -- ``product_categ_id`` (Categoría de producto) para ordenar y agrupar. -- ``is_collected`` (Recogido) como check manual en cada línea para marcar si se - ha recolectado. -- Nueva pestaña **Product Summary** con totales por producto (demandado, hecho, - pendiente), categoría y el check de recogido consolidado. + pueda identificar rápidamente el cliente/proveedor asociado. +- ``product_categ_id`` (Categoría de producto) para permitir ordenación y + agrupación por categoría. -Las columnas se añaden como ``optional="hide"`` en la vista de líneas del lote, -de modo que el usuario puede activarlas desde el selector de columnas sin -recargar la vista por defecto. +Ambas columnas se añaden como ``optional="hide"`` en la vista de líneas del +lote, de modo que el usuario puede activarlas desde el selector de columnas sin +cargar la vista por defecto. diff --git a/stock_picking_batch_custom/readme/USAGE.rst b/stock_picking_batch_custom/readme/USAGE.rst index 89f41e8..d652a4e 100644 --- a/stock_picking_batch_custom/readme/USAGE.rst +++ b/stock_picking_batch_custom/readme/USAGE.rst @@ -6,9 +6,5 @@ Uso - **Partner** (``picking_partner_id``) para ver el cliente/proveedor. - **Product Category** (``product_categ_id``) para ordenar/agrupación por categoría. - - **Collected** (``is_collected``) para marcar manualmente líneas recolectadas. -3. Pestaña **Product Summary**: consulta los totales por producto (demandado, - hecho y pendiente) y marca el check de recogido consolidado si corresponde. - -4. Ordena o agrupa por categoría en cualquiera de las vistas según convenga. +3. Ordena o agrupa por la columna de categoría según convenga. diff --git a/stock_picking_batch_custom/security/ir.model.access.csv b/stock_picking_batch_custom/security/ir.model.access.csv deleted file mode 100644 index 57ebabc..0000000 --- a/stock_picking_batch_custom/security/ir.model.access.csv +++ /dev/null @@ -1,2 +0,0 @@ -id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_stock_picking_batch_summary_line_user,stock_picking_batch_summary_line_user,model_stock_picking_batch_summary_line,base.group_user,1,1,1,0 diff --git a/stock_picking_batch_custom/tests/__init__.py b/stock_picking_batch_custom/tests/__init__.py deleted file mode 100644 index 161ba0b..0000000 --- a/stock_picking_batch_custom/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import test_batch_summary # noqa: F401 diff --git a/stock_picking_batch_custom/tests/test_batch_summary.py b/stock_picking_batch_custom/tests/test_batch_summary.py deleted file mode 100644 index e8139e9..0000000 --- a/stock_picking_batch_custom/tests/test_batch_summary.py +++ /dev/null @@ -1,289 +0,0 @@ -# Copyright 2026 Criptomart -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo.tests import tagged -from odoo.tests.common import TransactionCase - - -@tagged("-at_install", "post_install") -class TestBatchSummary(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.uom_unit = cls.env.ref("uom.product_uom_unit") - cls.uom_dozen = cls.env.ref("uom.product_uom_dozen") - cls.location_src = cls.env.ref("stock.stock_location_stock") - cls.location_dest = cls.env.ref("stock.stock_location_customers") - cls.picking_type = cls.env.ref("stock.picking_type_out") - - cls.product = cls.env["product.product"].create( - { - "name": "Test Product", - "uom_id": cls.uom_unit.id, - "uom_po_id": cls.uom_unit.id, - } - ) - - cls.batch = cls.env["stock.picking.batch"].create( - {"name": "Batch Test", "picking_type_id": cls.picking_type.id} - ) - - cls.picking1 = cls.env["stock.picking"].create( - { - "picking_type_id": cls.picking_type.id, - "location_id": cls.location_src.id, - "location_dest_id": cls.location_dest.id, - "batch_id": cls.batch.id, - } - ) - cls.picking2 = cls.env["stock.picking"].create( - { - "picking_type_id": cls.picking_type.id, - "location_id": cls.location_src.id, - "location_dest_id": cls.location_dest.id, - "batch_id": cls.batch.id, - } - ) - - # Helpers - def _add_move(self, picking, qty_demanded, qty_done, uom): - move = self.env["stock.move"].create( - { - "name": self.product.name, - "product_id": self.product.id, - "product_uom_qty": qty_demanded, - "product_uom": uom.id, - "picking_id": picking.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - } - ) - if qty_done: - self.env["stock.move.line"].create( - { - "move_id": move.id, - "picking_id": picking.id, - "product_id": self.product.id, - "product_uom_id": self.product.uom_id.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - "quantity": qty_done, - } - ) - return move - - def _recompute_batch(self): - self.batch.invalidate_recordset() - self.batch._compute_summary_line_ids() - - def _create_batch_with_pickings(self): - batch = self.env["stock.picking.batch"].create( - {"name": "Batch Flow", "picking_type_id": self.picking_type.id} - ) - picking = self.env["stock.picking"].create( - { - "picking_type_id": self.picking_type.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - } - ) - move = self.env["stock.move"].create( - { - "name": self.product.name, - "product_id": self.product.id, - "product_uom_qty": 2.0, - "product_uom": self.uom_unit.id, - "picking_id": picking.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - } - ) - self.env["stock.move.line"].create( - { - "move_id": move.id, - "picking_id": picking.id, - "product_id": self.product.id, - "product_uom_id": self.uom_unit.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - "quantity": 2.0, - } - ) - picking.batch_id = batch.id - return batch - - # Tests - def test_totals_and_pending_with_conversion(self): - """Totals aggregate per product with UoM conversion and pending.""" - - # demand 12 units, done 5 units - self._add_move(self.picking1, qty_demanded=12, qty_done=5, uom=self.uom_unit) - # demand 2 dozens (24 units), done 6 units - self._add_move(self.picking2, qty_demanded=2, qty_done=6, uom=self.uom_dozen) - - self._recompute_batch() - - self.assertEqual(len(self.batch.summary_line_ids), 1) - line = self.batch.summary_line_ids - self.assertEqual(line.product_id, self.product) - self.assertEqual(line.product_uom_id, self.uom_unit) - self.assertAlmostEqual(line.qty_demanded, 36.0) - self.assertAlmostEqual(line.qty_done, 11.0) - self.assertAlmostEqual(line.qty_pending, 25.0) - - def test_collected_flag_preserved_on_recompute(self): - """Collected stays checked after totals change.""" - - self._add_move(self.picking1, qty_demanded=1, qty_done=1, uom=self.uom_unit) - self._recompute_batch() - - line = self.batch.summary_line_ids - line.is_collected = True - - # Add more demand/done to trigger an update - self._add_move(self.picking1, qty_demanded=3, qty_done=2, uom=self.uom_unit) - self._recompute_batch() - - self.assertTrue(self.batch.summary_line_ids.is_collected) - - def test_cancelled_moves_are_ignored(self): - """Cancelled moves do not count in the summary and lines are removed.""" - - move1 = self._add_move( - self.picking1, qty_demanded=4, qty_done=2, uom=self.uom_unit - ) - move2 = self._add_move( - self.picking2, qty_demanded=6, qty_done=3, uom=self.uom_unit - ) - self._recompute_batch() - self.assertEqual(len(self.batch.summary_line_ids), 1) - - move1.write({"state": "cancel"}) - move2.write({"state": "cancel"}) - self._recompute_batch() - - self.assertFalse(self.batch.summary_line_ids) - - def test_no_required_product_error_on_confirm(self): - """Confirming batch with pickings must not create summary lines without product.""" - - batch = self._create_batch_with_pickings() - - # Trigger compute via confirm flow - batch.action_confirm() - - self.assertTrue(batch.summary_line_ids) - self.assertFalse( - batch.summary_line_ids.filtered(lambda line: not line.product_id) - ) - - def test_empty_batch_add_pickings_then_confirm(self): - """Create empty draft batch, add multiple pickings, then confirm.""" - - # 1. Create empty batch in draft state - batch = self.env["stock.picking.batch"].create( - {"name": "Batch Empty Start", "picking_type_id": self.picking_type.id} - ) - self.assertEqual(batch.state, "draft") - self.assertFalse(batch.picking_ids) - self.assertFalse(batch.summary_line_ids) - - # 2. Create pickings with moves (without batch assignment) - product2 = self.env["product.product"].create( - { - "name": "Test Product 2", - "uom_id": self.uom_unit.id, - "uom_po_id": self.uom_unit.id, - } - ) - - picking_a = self.env["stock.picking"].create( - { - "picking_type_id": self.picking_type.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - } - ) - self.env["stock.move"].create( - { - "name": self.product.name, - "product_id": self.product.id, - "product_uom_qty": 5.0, - "product_uom": self.uom_unit.id, - "picking_id": picking_a.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - } - ) - - picking_b = self.env["stock.picking"].create( - { - "picking_type_id": self.picking_type.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - } - ) - self.env["stock.move"].create( - { - "name": product2.name, - "product_id": product2.id, - "product_uom_qty": 10.0, - "product_uom": self.uom_unit.id, - "picking_id": picking_b.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - } - ) - - picking_c = self.env["stock.picking"].create( - { - "picking_type_id": self.picking_type.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - } - ) - self.env["stock.move"].create( - { - "name": self.product.name, - "product_id": self.product.id, - "product_uom_qty": 3.0, - "product_uom": self.uom_unit.id, - "picking_id": picking_c.id, - "location_id": self.location_src.id, - "location_dest_id": self.location_dest.id, - } - ) - - # 3. Add pickings to the batch - picking_a.batch_id = batch - picking_b.batch_id = batch - picking_c.batch_id = batch - - self.assertEqual(len(batch.picking_ids), 3) - - # 4. Confirm the batch — this should not raise product_id required error - batch.action_confirm() - - self.assertEqual(batch.state, "in_progress") - - # 5. Verify summary lines are correct - self.assertTrue(batch.summary_line_ids) - self.assertFalse( - batch.summary_line_ids.filtered(lambda line: not line.product_id) - ) - - # Two products expected - products_in_summary = batch.summary_line_ids.mapped("product_id") - self.assertIn(self.product, products_in_summary) - self.assertIn(product2, products_in_summary) - - # Check aggregated quantities - line_product1 = batch.summary_line_ids.filtered( - lambda line: line.product_id == self.product - ) - self.assertAlmostEqual(line_product1.qty_demanded, 8.0) # 5 + 3 - - line_product2 = batch.summary_line_ids.filtered( - lambda line: line.product_id == product2 - ) - self.assertAlmostEqual(line_product2.qty_demanded, 10.0) diff --git a/stock_picking_batch_custom/views/stock_move_line_views.xml b/stock_picking_batch_custom/views/stock_move_line_views.xml index b857a2c..a000a97 100644 --- a/stock_picking_batch_custom/views/stock_move_line_views.xml +++ b/stock_picking_batch_custom/views/stock_move_line_views.xml @@ -11,9 +11,6 @@ - - - diff --git a/stock_picking_batch_custom/views/stock_picking_batch_views.xml b/stock_picking_batch_custom/views/stock_picking_batch_views.xml deleted file mode 100644 index 43b3ae2..0000000 --- a/stock_picking_batch_custom/views/stock_picking_batch_views.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - stock.picking.batch.summary.line.tree - stock.picking.batch.summary.line - - - - - - - - - - - - - - - stock.picking.batch.form.summary - stock.picking.batch - - - - - - - - - - diff --git a/website_sale_aplicoop/static/src/js/website_sale.js b/website_sale_aplicoop/static/src/js/website_sale.js index 3f76c3c..b88bbee 100644 --- a/website_sale_aplicoop/static/src/js/website_sale.js +++ b/website_sale_aplicoop/static/src/js/website_sale.js @@ -694,70 +694,6 @@ return; } - // Shared handler for add-to-cart to reuse across grid/document listeners - var handleAddToCart = function (e) { - var cartBtn = e.target.closest(".add-to-cart-btn"); - if (!cartBtn) return; - - e.preventDefault(); - var form = cartBtn.closest(".add-to-cart-form"); - if (!form) return; - - var productId = form.getAttribute("data-product-id"); - var productName = form.getAttribute("data-product-name") || "Product"; - var productPrice = parseFloat(form.getAttribute("data-product-price")) || 0; - var quantityInput = form.querySelector(".product-qty"); - var quantity = quantityInput ? parseFloat(quantityInput.value) : 1; - - // Block add-to-cart if product is flagged out of stock (from template) - var isOutOfStock = - (form.getAttribute("data-out-of-stock") || "false") === "true" || - (cartBtn.getAttribute("data-out-of-stock") || "false") === "true"; - - // Fallback guards in case cached markup drops the data attribute - if (!isOutOfStock) { - var btnTitle = (cartBtn.getAttribute("title") || "").toLowerCase(); - var btnAria = (cartBtn.getAttribute("aria-label") || "").toLowerCase(); - var iconEl = cartBtn.querySelector("i"); - var hasBanIcon = iconEl && iconEl.classList.contains("fa-ban"); - - if ( - hasBanIcon || - btnTitle.includes("out of stock") || - btnTitle.includes("sin stock") || - btnAria.includes("out of stock") || - btnAria.includes("sin stock") - ) { - isOutOfStock = true; - } - } - if (isOutOfStock) { - var labels = self._getLabels(); - self._showNotification( - labels.out_of_stock || "Product is out of stock", - "warning" - ); - return; - } - - console.log("Adding:", { - productId: productId, - productName: productName, - productPrice: productPrice, - quantity: quantity, - }); - - if (quantity > 0) { - self._addToCart(productId, productName, productPrice, quantity); - } else { - var labels2 = self._getLabels(); - self._showNotification( - labels2.invalid_quantity || "Please enter a valid quantity", - "warning" - ); - } - }; - // First, adjust quantity steps for all existing inputs var unitInputs = document.querySelectorAll(".product-qty"); console.log("=== ADJUSTING QUANTITY STEPS (from data-quantity-step) ==="); @@ -843,16 +779,67 @@ } }); - // Add to cart button (via event delegation on grid) - productsGrid.addEventListener("click", handleAddToCart); + // Add to cart button (via event delegation) + productsGrid.addEventListener("click", function (e) { + var cartBtn = e.target.closest(".add-to-cart-btn"); + if (!cartBtn) return; - // Also attach a document-level delegation as fallback for dynamically - // inserted products (infinite scroll) in case the grid listener is lost - // after DOM replacement. - if (!this._docCartListenerAttached) { - document.addEventListener("click", handleAddToCart, true); - this._docCartListenerAttached = true; - } + e.preventDefault(); + var form = cartBtn.closest(".add-to-cart-form"); + var productId = form.getAttribute("data-product-id"); + var productName = form.getAttribute("data-product-name") || "Product"; + var productPrice = parseFloat(form.getAttribute("data-product-price")) || 0; + var quantityInput = form.querySelector(".product-qty"); + var quantity = quantityInput ? parseFloat(quantityInput.value) : 1; + + // Block add-to-cart if product is flagged out of stock (from template) + var isOutOfStock = + (form.getAttribute("data-out-of-stock") || "false") === "true" || + (cartBtn.getAttribute("data-out-of-stock") || "false") === "true"; + + // Fallback guards in case cached markup drops the data attribute + if (!isOutOfStock) { + var btnTitle = (cartBtn.getAttribute("title") || "").toLowerCase(); + var btnAria = (cartBtn.getAttribute("aria-label") || "").toLowerCase(); + var iconEl = cartBtn.querySelector("i"); + var hasBanIcon = iconEl && iconEl.classList.contains("fa-ban"); + + if ( + hasBanIcon || + btnTitle.includes("out of stock") || + btnTitle.includes("sin stock") || + btnAria.includes("out of stock") || + btnAria.includes("sin stock") + ) { + isOutOfStock = true; + } + } + if (isOutOfStock) { + var labels = self._getLabels(); + self._showNotification( + labels.out_of_stock || "Product is out of stock", + "warning" + ); + return; + } + + console.log("Adding:", { + productId: productId, + productName: productName, + productPrice: productPrice, + quantity: quantity, + }); + + if (quantity > 0) { + self._addToCart(productId, productName, productPrice, quantity); + } else { + var labels = self._getLabels(); + self._showNotification( + labels.invalid_quantity || "Please enter a valid quantity", + "warning" + ); + } + }); }, _addToCart: function (productId, productName, productPrice, quantity) {