# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from urllib.parse import urlencode from odoo import http from odoo.addons.website_sale.controllers.main import WebsiteSale from odoo.http import request SIGNUP_FLAG = "membership_signup=1" class WebsiteSaleMembershipSignup(WebsiteSale): def _membership_signup_is_required(self, product_id): """Return True iff an anonymous visitor adding `product_id` to the cart must be redirected to /web/signup first. """ website = request.website return website._membership_signup_required_for( product_id, request.env.user._is_public() ) def _membership_signup_redirect_url(self, product_id): """Build the `/web/signup?membership_signup=1&redirect=` URL preserving the current language. """ product = request.env["product.product"].browse(int(product_id)) tmpl = product.product_tmpl_id product_url = "/shop/%s" % request.env["ir.http"]._slug(tmpl) query = urlencode({"membership_signup": "1", "redirect": product_url}) return "/web/signup?%s" % query def _membership_signup_mark_session(self, product_id): request.session["membership_signup_active"] = True request.session["membership_signup_product_id"] = int(product_id) @http.route() def cart_update( self, product_id, add_qty=1, set_qty=0, product_custom_attribute_values=None, no_variant_attribute_value_ids=None, **kwargs ): if self._membership_signup_is_required(product_id): self._membership_signup_mark_session(product_id) return request.redirect(self._membership_signup_redirect_url(product_id)) return super().cart_update( product_id=product_id, add_qty=add_qty, set_qty=set_qty, product_custom_attribute_values=product_custom_attribute_values, no_variant_attribute_value_ids=no_variant_attribute_value_ids, **kwargs ) @http.route() def cart_update_json( self, product_id, line_id=None, add_qty=None, set_qty=None, display=True, product_custom_attribute_values=None, no_variant_attribute_value_ids=None, **kwargs ): if self._membership_signup_is_required(product_id): self._membership_signup_mark_session(product_id) # The JSON route cannot perform an HTTP redirect on its own; signal # the frontend so it can navigate to the signup URL. The standard # "Add to Cart" on a product page uses /shop/cart/update (http), # which is the primary path for this flow (RF-01). The JSON route # covers add-to-cart from snippets/wishlist on the same page # (`stayOnPageOption` / `o_we_buy_now` express). # # The JS handler in # `static/src/js/website_membership_signup_required.js` reads # `data.force_redirect` and navigates to it. The other keys are # NOT consumed by our handler: they exist to keep the core's # `cartHandlerMixin._addToCartInPage` (in # `website_sale/static/src/js/website_sale_utils.js`) from crashing. # That method does, in order: # if (data.cart_quantity && ...) updateCartNavBar(data); # showCartNotification(call, data.notification_info); # `showCartNotification` accesses `props.lines` and `props.warning` # on `data.notification_info`; if it is `undefined` (as it was in # the first iteration, which only returned `{force_redirect: ...}`) # the access raises `TypeError: can't access property "lines", # props is undefined` and the redirect never fires. # # Shape (mirrors the empty-cart branch in # `WebsiteSaleController.cart_update_json`): # cart_quantity = 0 -> falsy, so `updateCartNavBar` is skipped. # notification_info = {lines: [], warning: ""} # -> `showCartNotification` no-ops on both empty fields. return { "force_redirect": self._membership_signup_redirect_url(product_id), "cart_quantity": 0, "notification_info": {"lines": [], "warning": ""}, } return super().cart_update_json( product_id=product_id, line_id=line_id, add_qty=add_qty, set_qty=set_qty, display=display, product_custom_attribute_values=product_custom_attribute_values, no_variant_attribute_value_ids=no_variant_attribute_value_ids, **kwargs ) def _get_mandatory_address_fields(self, country_sudo): # EMES: the phone field is not mandatory at checkout, for any flow # (membership or regular). The core's default set in # `website_sale/controllers/main.py:_get_mandatory_address_fields` # includes 'phone' alongside name/street/city/country_id. Removing it # here cascades to both billing and delivery mandatory fields (both # delegates to this method) and propagates to the frontend: the # hidden `required_fields` input sent to the address form no longer # lists 'phone', so `address.js` neither marks the input as # `:required` nor shows the red asterisk, and the server-side # `_check_billing_address` / `_check_delivery_address` no longer # reject empty phones. fields = super()._get_mandatory_address_fields(country_sudo) fields.discard("phone") return fields def _get_mandatory_fields(self): # Portal's `_get_mandatory_fields` (in # `portal/controllers/portal.py`) returns # `["name", "phone", "email", "street", "city", "country_id"]`, # and `website_sale`'s `_get_mandatory_billing_address_fields` # UNIONS that set with the one from `_get_mandatory_address_fields`. # So even after we drop 'phone' from the address set above, the # portal set re-adds it for billing. We need to drop it here too. fields = list(super()._get_mandatory_fields()) return [f for f in fields if f != "phone"]