# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import werkzeug.datastructures from odoo.exceptions import UserError from odoo.fields import Date from odoo.addons.website_membership_signup_required.controllers.main import ( AuthSignupHomeMembership as AuthSignupHome, ) from odoo.addons.website_membership_signup_required.controllers.website_sale import ( WebsiteSaleMembershipSignup, ) from odoo.addons.website.tools import MockRequest from odoo.tests import TransactionCase, tagged @tagged("post_install", "-at_install") class TestMembershipSignup(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() cls.website = cls.env.ref("website.default_website") cls.website.auth_signup_uninvited = "b2b" today = Date.to_date(Date.today()) cls.membership_product = cls.env["product.product"].create({ "name": "Membership Card", "membership": True, "list_price": 50.0, "membership_date_from": today.replace(month=1, day=1), "membership_date_to": today.replace(month=12, day=31), }) cls.regular_product = cls.env["product.product"].create({ "name": "Regular Mug", "membership": False, "list_price": 10.0, }) cls.no_user_partner = cls.env["res.partner"].create({ "firstname": "Backend", "lastname": "Partner", }) # -- RF-07 / RF-default ------------------------------------------------ def test_01_website_default_flag(self): new_website = self.env["website"].create({"name": "Aux site"}) self.assertTrue(new_website.membership_signup_required) # -- RF-01 helper ------------------------------------------------------- def test_02_is_required_membership_product_anonymous(self): self.assertTrue( self.website._membership_signup_required_for( self.membership_product.id, is_public=True ) ) # -- RF-05 ------------------------------------------------------------- def test_03_is_required_non_membership_product(self): self.assertFalse( self.website._membership_signup_required_for( self.regular_product.id, is_public=True ) ) # -- RF-06 ------------------------------------------------------------- def test_04_is_required_authenticated_user(self): self.assertFalse( self.website._membership_signup_required_for( self.membership_product.id, is_public=False ) ) # -- RF-07 ------------------------------------------------------------- def test_05_is_required_disabled_on_website(self): self.website.membership_signup_required = False self.assertFalse( self.website._membership_signup_required_for( self.membership_product.id, is_public=True ) ) # -- RF-03 ------------------------------------------------------------- def test_06_signup_scope_override_with_marker(self): Users = self.env["res.users"] # No marker -> delegates to super (b2b because of setUpClass). with MockRequest(self.env, website=self.website) as mock_request: mock_request.httprequest.args = werkzeug.datastructures.MultiDict() self.assertEqual(Users._get_signup_invitation_scope(), "b2b") # Query param marker -> b2c. with MockRequest(self.env, website=self.website) as mock_request: mock_request.httprequest.args = werkzeug.datastructures.MultiDict( {"membership_signup": "1"} ) self.assertEqual(Users._get_signup_invitation_scope(), "b2c") # Session flag marker -> b2c. with MockRequest(self.env, website=self.website) as mock_request: mock_request.httprequest.args = werkzeug.datastructures.MultiDict() mock_request.session["membership_signup_active"] = True self.assertEqual(Users._get_signup_invitation_scope(), "b2c") # Marker removed again -> back to b2b. with MockRequest(self.env, website=self.website) as mock_request: mock_request.httprequest.args = werkzeug.datastructures.MultiDict() self.assertEqual(Users._get_signup_invitation_scope(), "b2b") # -- RF-09 ------------------------------------------------------------- def test_07_prepare_signup_values_firstname_lastname(self): controller = AuthSignupHome() qcontext = { "login": "joohn.doe@example.com", "password": "verystrongpwd", "confirm_password": "verystrongpwd", "firstname": "John", "lastname": "Doe", "accepted_terms": "accepted", } with MockRequest(self.env, website=self.website): values = controller._prepare_signup_values(qcontext) self.assertEqual(values.get("firstname"), "John") self.assertEqual(values.get("lastname"), "Doe") # partner_firstname: `name` is composed (lastname + firstname, or # firstname + lastname depending on _get_names_order) and added by # our override so that the core signup check (no name or partner) # passes. partner_firstname's `res.partner.create` will later drop # this composed `name` and recompute it from the split fields. self.assertIn("name", values) self.assertIn("John", values["name"]) self.assertIn("Doe", values["name"]) def test_07b_prepare_signup_values_reject_without_terms(self): """Signup must be rejected if the legal terms checkbox is not checked (`accepted_terms` missing or not 'accepted'). """ import unittest controller = AuthSignupHome() base_qcontext = { "login": "no.terms@example.com", "password": "verystrongpwd", "confirm_password": "verystrongpwd", "firstname": "No", "lastname": "Terms", } # Missing the key entirely. with MockRequest(self.env, website=self.website): with self.assertRaises(UserError): controller._prepare_signup_values(dict(base_qcontext)) # Present but not 'accepted'. qcontext = dict(base_qcontext, accepted_terms="") with MockRequest(self.env, website=self.website): with self.assertRaises(UserError): controller._prepare_signup_values(qcontext) # Present and 'accepted' -> does not raise. qcontext = dict(base_qcontext, accepted_terms="accepted") with MockRequest(self.env, website=self.website): values = controller._prepare_signup_values(qcontext) self.assertEqual(values.get("firstname"), "No") # -- RF-08 ------------------------------------------------------------- def test_08_backend_sale_order_with_membership_product_without_user(self): order = self.env["sale.order"].create({ "partner_id": self.no_user_partner.id, "order_line": [(0, 0, { "product_id": self.membership_product.id, "product_uom_qty": 1.0, })], }) self.assertTrue(order) self.assertEqual(order.order_line.product_id, self.membership_product) # Confirming the SO drives membership creation (core `membership`). order.action_confirm() self.assertEqual(order.state, "sale") # -- Phone not mandatory at checkout (EMES) ---------------------------- def test_09_phone_not_mandatory_address_fields(self): """`phone` must not be part of the mandatory checkout fields, for any address (billing or delivery) and any flow on the EMES website. The core default (`website_sale/controllers/main.py: _get_mandatory_address_fields`) includes 'phone'; our override drops it. The frontend `address.js` then no longer marks the input `:required` nor shows the asterisk, and the server-side `_check_billing_address` / `_check_delivery_address` no longer reject empty phones. """ country = self.env.ref("base.es") ctrl = WebsiteSaleMembershipSignup() fields = ctrl._get_mandatory_address_fields(country) self.assertNotIn("phone", fields) # Default mandatory fields still present. self.assertIn("name", fields) self.assertIn("street", fields) self.assertIn("city", fields) self.assertIn("country_id", fields) def test_10_phone_not_in_portal_mandatory_fields(self): """`phone` must also be dropped from the portal-side mandatory field list (`portal/controllers/portal.py:_get_mandatory_fields` returns `["name", "phone", "email", "street", "city", "country_id"]`). `website_sale`'s `_get_mandatory_billing_address_fields` UNIONS that set with the address set from `_get_mandatory_address_fields`, so even with the address override alone, 'phone' would come back via the portal path for billing. Hence this second override. """ ctrl = WebsiteSaleMembershipSignup() fields = ctrl._get_mandatory_fields() self.assertNotIn("phone", fields) # Other portal mandatory fields still present. self.assertIn("name", fields) self.assertIn("email", fields) self.assertIn("country_id", fields) def test_11_phone_not_in_mandatory_billing_address_fields(self): """End-to-end check of the billing set: the union of `_get_mandatory_address_fields` + `_get_mandatory_fields` (which is what `_get_mandatory_billing_address_fields` returns) must NOT contain 'phone'. """ country = self.env.ref("base.es") ctrl = WebsiteSaleMembershipSignup() billing_fields = ctrl._get_mandatory_billing_address_fields(country) self.assertNotIn("phone", billing_fields) self.assertIn("email", billing_fields) # -- Token-based signup: pre-fill firstname / lastname ----------------- def test_12_signup_retrieve_info_returns_firstname_lastname(self): """When a partner is invited via token (the email-invitation flow), the core's `_signup_retrieve_info` only returns `name`. Our `res.partner` override adds `firstname` / `lastname` so the signup form (extended by this module to collect them separately) is pre-filled when the invited partner already has the split fields (which is the case with `partner_firstname` installed). The invited partner must NOT have a user yet, otherwise the token payload check (`user_ids`) won't match. """ partner = self.env["res.partner"].create({ "firstname": "Invited", "lastname": "Researcher", "email": "invited.researcher@example.com", "signup_type": "signup", }) self.assertFalse(partner.user_ids, "test precondition: no user yet") token = partner._generate_signup_token() info = self.env["res.partner"]._signup_retrieve_info(token) self.assertEqual(info.get("firstname"), "Invited") self.assertEqual(info.get("lastname"), "Researcher") self.assertEqual(info.get("name"), partner.name) self.assertEqual(info.get("email"), "invited.researcher@example.com") def test_13_signup_retrieve_info_legacy_partner_without_split(self): """A partner created without `firstname`/`lastname` (legacy, only `name`) should not crash the override. `firstname`/`lastname` come back as '' (falsy), `name` as the legacy value, and the token flow still works — the form will show empty readonly inputs for the split fields, which is the best we can do without re-splitting. """ partner = self.env["res.partner"].create({ "name": "Legacy Researcher", "email": "legacy@example.com", "signup_type": "signup", }) # partner_firstname's post_create hook may or may not have split # `name` automatically; we just check the override does not crash # and returns the contract keys. token = partner._generate_signup_token() info = self.env["res.partner"]._signup_retrieve_info(token) self.assertIn("firstname", info) self.assertIn("lastname", info) self.assertEqual(info.get("name"), partner.name)