diff --git a/website_sale_aplicoop/__manifest__.py b/website_sale_aplicoop/__manifest__.py index 3de62ce..a566e3d 100644 --- a/website_sale_aplicoop/__manifest__.py +++ b/website_sale_aplicoop/__manifest__.py @@ -37,6 +37,7 @@ "views/res_partner_views.xml", "views/res_config_settings_views.xml", "views/website_templates.xml", + "views/website_sale_disable_cart.xml", "views/product_template_views.xml", "views/sale_order_views.xml", "views/stock_picking_views.xml", diff --git a/website_sale_aplicoop/controllers/website_sale.py b/website_sale_aplicoop/controllers/website_sale.py index dec32e7..b302a49 100644 --- a/website_sale_aplicoop/controllers/website_sale.py +++ b/website_sale_aplicoop/controllers/website_sale.py @@ -1511,6 +1511,87 @@ class AplicoopWebsiteSale(WebsiteSale): status=status, ) + def _validate_items_for_group_order(self, items, group_order): + """Validate items from historical order against current group order availability. + + Args: + items: list of dicts with keys: product_id, product_name, quantity, price + group_order: group.order record + + Returns: + dict with keys: + - available_items: list of items that are available in current group order + - unavailable_items: list of items that are NOT available + - unavailable_products: set of product IDs that are unavailable + - warning_message: str, message about unavailable items (or empty) + """ + if not items: + return { + "available_items": [], + "unavailable_items": [], + "unavailable_products": set(), + "warning_message": "", + } + + # Get available products for the current group order + try: + available_products = request.env[ + "group.order" + ]._get_products_for_group_order(group_order.id) + available_product_ids = set(available_products.ids) + except Exception as e: + _logger.error( + "Error getting available products for group_order %d: %s", + group_order.id, + e, + ) + # If something fails, return all items as available (failsafe) + return { + "available_items": items, + "unavailable_items": [], + "unavailable_products": set(), + "warning_message": "", + } + + # Separate items into available and unavailable + available_items = [] + unavailable_items = [] + unavailable_product_ids = set() + + for item in items: + product_id = item.get("product_id") + if product_id in available_product_ids: + available_items.append(item) + else: + unavailable_items.append(item) + unavailable_product_ids.add(product_id) + + # Build warning message if there are unavailable items + warning_message = "" + if unavailable_items: + unavailable_names = [ + item.get("product_name", "Unknown") for item in unavailable_items + ] + warning_message = request.env._( + "%(count)d product(s) from your saved order are no longer available in this group order: %(names)s. " + "Only available products will be loaded.", + count=len(unavailable_items), + names=", ".join(unavailable_names), + ) + _logger.warning( + "load_order_from_history: %d unavailable items in group_order %d (products: %s)", + len(unavailable_items), + group_order.id, + unavailable_product_ids, + ) + + return { + "available_items": available_items, + "unavailable_items": unavailable_items, + "unavailable_products": unavailable_product_ids, + "warning_message": warning_message, + } + def _find_recent_draft_order(self, partner_id, group_order): """Find most recent draft sale.order for partner in the active order period. @@ -2218,12 +2299,31 @@ class AplicoopWebsiteSale(WebsiteSale): status=404, ) + # Determine if cutoff date for the current cycle has already passed + try: + today = fields.Date.today() + cutoff_passed = False + cutoff_date_str = None + if group_order.cutoff_date: + cutoff_passed = group_order.cutoff_date < today + # Convert to ISO-like string for frontend (YYYY-MM-DD) + cutoff_date_str = str(group_order.cutoff_date) + except Exception: + cutoff_passed = False + cutoff_date_str = None + response_data = { "success": True, "order_id": group_order.id, "state": group_order.state, "is_open": group_order.state == "open", - "action": "clear_cart" if group_order.state != "open" else "none", + "action": ( + "clear_cart" + if (group_order.state != "open" or cutoff_passed) + else "none" + ), + "cutoff_passed": cutoff_passed, + "cutoff_date": cutoff_date_str, } return request.make_response( json.dumps(response_data), @@ -2975,9 +3075,14 @@ class AplicoopWebsiteSale(WebsiteSale): if sale_order.group_order_id.id != group_order_id: return request.redirect("/eskaera/%d" % sale_order.group_order_id.id) + # Get the current group_order (the one being viewed, not necessarily the one from the history) + group_order = request.env["group.order"].sudo().browse(group_order_id) + if not group_order.exists(): + return request.redirect("/shop") + # Extract items from the order (skip delivery product) # Use the delivery_product_id from the group_order - delivery_product = sale_order.group_order_id.delivery_product_id + delivery_product = group_order.delivery_product_id delivery_product_id = delivery_product.id if delivery_product else None items = [] @@ -2995,6 +3100,21 @@ class AplicoopWebsiteSale(WebsiteSale): } ) + # Validate items against current group order availability + validation_result = self._validate_items_for_group_order(items, group_order) + available_items = validation_result["available_items"] + unavailable_items = validation_result["unavailable_items"] + warning_message = validation_result["warning_message"] + + _logger.info( + "load_order_from_history: Loaded %d items, %d available, %d unavailable from sale_order %d into group_order %d", + len(items), + len(available_items), + len(unavailable_items), + sale_order_id, + group_order_id, + ) + # Store items in localStorage by passing via URL parameter or session # We'll use sessionStorage in JavaScript to avoid URL length limits @@ -3019,13 +3139,19 @@ class AplicoopWebsiteSale(WebsiteSale): "website_sale_aplicoop.eskaera_load_from_history", { "group_order_id": group_order_id, - "items_json": json.dumps(items), # Pass serialized JSON + "items_json": json.dumps( + available_items + ), # Pass ONLY available items "sale_order": sale_order, "sale_order_name": sale_order.name, # Pass order reference "pickup_day": pickup_day_to_restore, # Pass pickup day (or None if different group) "pickup_date": pickup_date_to_restore, # Pass pickup date (or None if different group) "home_delivery": home_delivery_to_restore, # Pass home delivery flag (or None if different group) "same_group_order": same_group_order, # Indicate if from same group order + "unavailable_items": unavailable_items, # List of unavailable items + "warning_message": warning_message, # Warning about unavailable products + "has_unavailable_items": len(unavailable_items) + > 0, # Boolean flag for template }, ), ) @@ -3269,3 +3395,50 @@ class AplicoopWebsiteSale(WebsiteSale): "empty_cart": "Your cart is empty", "added_to_cart": "added to cart", } + + # ================================================================ + # CART REDIRECT METHODS - Redirect /shop/cart routes to /eskaera + # ================================================================ + + @http.route(["/shop/cart"], type="http", auth="public", website=True) + def cart_redirect(self, access_token=None, revive="", **post): + """Redirect /shop/cart to /eskaera (no standard cart).""" + _logger.info("🛒 Redirecting /shop/cart → /eskaera") + return http.redirect_with_hash("/eskaera") + + @http.route( + ["/shop/cart/update"], + type="http", + auth="public", + website=True, + methods=["POST"], + ) + def cart_update_redirect(self, **post): + """Redirect /shop/cart/update to /eskaera (no standard cart).""" + _logger.info("🛒 Redirecting /shop/cart/update → /eskaera") + return http.redirect_with_hash("/eskaera") + + @http.route( + ["/shop/cart/update_json"], + type="http", + auth="public", + website=True, + methods=["POST"], + csrf=False, + ) + def cart_update_json_redirect(self, **post): + """Redirect /shop/cart/update_json to /eskaera (no standard cart).""" + _logger.info("🛒 Redirecting /shop/cart/update_json → /eskaera") + return http.redirect_with_hash("/eskaera") + + @http.route( + ["/shop/cart_quantity"], + type="http", + auth="public", + website=True, + methods=["GET"], + ) + def cart_quantity_redirect(self): + """Redirect /shop/cart_quantity to /eskaera (no standard cart).""" + _logger.info("🛒 Redirecting /shop/cart_quantity → /eskaera") + return http.redirect_with_hash("/eskaera") diff --git a/website_sale_aplicoop/models/group_order.py b/website_sale_aplicoop/models/group_order.py index 399e508..3d30765 100644 --- a/website_sale_aplicoop/models/group_order.py +++ b/website_sale_aplicoop/models/group_order.py @@ -882,7 +882,7 @@ class GroupOrder(models.Model): ) def _create_picking_batches_for_sale_orders(self, sale_orders): - """Create stock.picking.batch grouped by consumer_group_id. + """Create stock.picking.batch grouped by picking type for this group order. Args: sale_orders: Recordset of confirmed sale.order @@ -902,9 +902,13 @@ class GroupOrder(models.Model): ) grouped_pickings[picking.picking_type_id.id] |= picking - scheduled_date = self.pickup_date - if not scheduled_date and self.delivery_date: - scheduled_date = self.delivery_date - timedelta(days=1) + scheduled_date = None + if self.pickup_date: + scheduled_date = fields.Datetime.to_datetime(self.pickup_date) + elif self.delivery_date: + scheduled_date = fields.Datetime.to_datetime( + self.delivery_date - timedelta(days=1) + ) for picking_type_id, pickings in grouped_pickings.items(): if not pickings: diff --git a/website_sale_aplicoop/static/src/js/website_sale.js b/website_sale_aplicoop/static/src/js/website_sale.js index 9e194a6..4c5bdb9 100644 --- a/website_sale_aplicoop/static/src/js/website_sale.js +++ b/website_sale_aplicoop/static/src/js/website_sale.js @@ -222,12 +222,14 @@ var pickupDayKey = "load_from_history_pickup_day_" + this.orderId; var pickupDateKey = "load_from_history_pickup_date_" + this.orderId; var homeDeliveryKey = "load_from_history_home_delivery_" + this.orderId; + var warningKey = "load_from_history_warning_" + this.orderId; var itemsJson = sessionStorage.getItem(storageKey); var orderName = sessionStorage.getItem(orderNameKey); var pickupDay = sessionStorage.getItem(pickupDayKey); var pickupDate = sessionStorage.getItem(pickupDateKey); var homeDelivery = sessionStorage.getItem(homeDeliveryKey) === "true"; + var warningMessage = sessionStorage.getItem(warningKey); console.log("DEBUG: _loadFromHistory called for orderId:", this.orderId); console.log("DEBUG: sessionStorageKey:", storageKey); @@ -240,6 +242,7 @@ homeDelivery, "(empty means different group order)" ); + console.log("DEBUG: warningMessage:", warningMessage); if (!itemsJson || itemsJson === "[object Object]") { console.log("No valid items from history found in sessionStorage"); @@ -248,6 +251,7 @@ sessionStorage.removeItem(pickupDayKey); sessionStorage.removeItem(pickupDateKey); sessionStorage.removeItem(homeDeliveryKey); + sessionStorage.removeItem(warningKey); return; } @@ -269,6 +273,7 @@ sessionStorage.removeItem(pickupDayKey); sessionStorage.removeItem(pickupDateKey); sessionStorage.removeItem(homeDeliveryKey); + sessionStorage.removeItem(warningKey); return; } @@ -349,12 +354,19 @@ } this._showNotification(message, "success", 3000); + // Show warning if some products were unavailable + if (warningMessage) { + console.log("Showing warning about unavailable products:", warningMessage); + this._showNotification(warningMessage, "warning", 5000); + } + // Clear sessionStorage sessionStorage.removeItem(storageKey); sessionStorage.removeItem(orderNameKey); sessionStorage.removeItem(pickupDayKey); sessionStorage.removeItem(pickupDateKey); sessionStorage.removeItem(homeDeliveryKey); + sessionStorage.removeItem(warningKey); } catch (e) { console.error("Error loading from history:", e); console.error("itemsJson was:", itemsJson); @@ -435,8 +447,32 @@ } try { var data = JSON.parse(xhr.responseText || "{}"); - if (data && data.is_open === false) { + // Clear cart if order is closed, action requests clearance, or cutoff already passed + var shouldClear = false; + if (data) { + if (data.is_open === false) { + shouldClear = true; + } + if (data.action && data.action === "clear_cart") { + shouldClear = true; + } + if (data.cutoff_passed === true) { + shouldClear = true; + } + } + if (shouldClear) { + console.log( + "[groupOrderShop] check-status: clearing cart (reason:", + data, + ")" + ); self._clearCurrentOrderCartSilently(); + // Update on-screen cart if visible + try { + self._updateCartDisplay(); + } catch (err) { + console.warn("_updateCartDisplay failed after clearing cart:", err); + } } } catch (e) { console.warn("[groupOrderShop] check-status parse error", e); diff --git a/website_sale_aplicoop/tests/test_cron_picking_batch.py b/website_sale_aplicoop/tests/test_cron_picking_batch.py index f2cb3b5..152be12 100644 --- a/website_sale_aplicoop/tests/test_cron_picking_batch.py +++ b/website_sale_aplicoop/tests/test_cron_picking_batch.py @@ -361,6 +361,7 @@ class TestCronPickingBatch(TransactionCase): # Call second time group_order._confirm_linked_sale_orders() + so.invalidate_recordset() batch_second = so.picking_ids[0].batch_id batch_count_second = self.env["stock.picking.batch"].search_count( diff --git a/website_sale_aplicoop/views/load_from_history_templates.xml b/website_sale_aplicoop/views/load_from_history_templates.xml index 1f254df..5e3fb12 100644 --- a/website_sale_aplicoop/views/load_from_history_templates.xml +++ b/website_sale_aplicoop/views/load_from_history_templates.xml @@ -19,12 +19,17 @@ var homeDelivery = ; var sameGroupOrder = ; + // Product availability warning + var hasUnavailableItems = ; + var warningMessage = ''; + console.log('load_from_history template: groupOrderId=', groupOrderId); console.log('load_from_history template: saleOrderName=', saleOrderName); console.log('load_from_history template: pickupDay=', pickupDay); console.log('load_from_history template: pickupDate=', pickupDate); console.log('load_from_history template: homeDelivery=', homeDelivery); console.log('load_from_history template: sameGroupOrder=', sameGroupOrder); + console.log('load_from_history template: hasUnavailableItems=', hasUnavailableItems); console.log('load_from_history template: itemsJson type=', typeof itemsJson); console.log('load_from_history template: itemsJson value=', itemsJson); @@ -47,6 +52,12 @@ console.log('Skipped saving pickup fields (different group order - will use current group order days)'); } + // Store warning about unavailable products if they exist + if (hasUnavailableItems === 'true') { + sessionStorage['load_from_history_warning_' + groupOrderId] = warningMessage; + console.log('Unavailable products detected:', warningMessage); + } + console.log('Saved to sessionStorage[load_from_history_' + groupOrderId + ']:', itemsJsonString); console.log('Saved order name to sessionStorage[load_from_history_order_name_' + groupOrderId + ']:', saleOrderName); diff --git a/website_sale_aplicoop/views/website_sale_disable_cart.xml b/website_sale_aplicoop/views/website_sale_disable_cart.xml new file mode 100644 index 0000000..1733dd8 --- /dev/null +++ b/website_sale_aplicoop/views/website_sale_disable_cart.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + +