diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py index 40ee6ae2a3b..c9538412b10 100644 --- a/addons/website/models/__init__.py +++ b/addons/website/models/__init__.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import assets +from . import base_partner_merge from . import ir_actions_server from . import ir_asset from . import ir_attachment diff --git a/addons/website/models/base_partner_merge.py b/addons/website/models/base_partner_merge.py new file mode 100644 index 00000000000..635a455b0bf --- /dev/null +++ b/addons/website/models/base_partner_merge.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +from odoo import api, models + + +class MergePartnerAutomatic(models.TransientModel): + _inherit = 'base.partner.merge.automatic.wizard' + + @api.model + def _update_foreign_keys(self, src_partners, dst_partner): + # Case 1: there is a visitor for both src and dst partners. + # Need to merge visitors before `super` to avoid SQL partner_id unique + # constraint to raise as it will change partner_id of the visitor + # record(s) to the `dst_partner` which already exists. + dst_visitor = dst_partner.visitor_ids and dst_partner.visitor_ids[0] + if dst_visitor: + for visitor in src_partners.visitor_ids: + visitor._merge_visitor(dst_visitor) + + super()._update_foreign_keys(src_partners, dst_partner) + + # Case 2: there is a visitor only for src_partners. + # Need to fix the "de-sync" values between `access_token` and + # `partner_id`. + self.env.cr.execute(""" + UPDATE website_visitor + SET access_token = partner_id + WHERE partner_id::int != access_token::int + AND partner_id = %s; + """, (dst_partner.id,)) diff --git a/addons/website/tests/test_website_visitor.py b/addons/website/tests/test_website_visitor.py index e909069e8ef..4767c8dbca5 100644 --- a/addons/website/tests/test_website_visitor.py +++ b/addons/website/tests/test_website_visitor.py @@ -390,3 +390,109 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo): 'url': self.tracked_page_2.url })] } + + def test_merge_partner_with_visitor_both(self): + """ See :meth:`test_merge_partner_with_visitor_single` """ + # Setup a visitor for demo and none for admin + Visitor = self.env['website.visitor'] + (self.partner_demo + self.partner_admin).visitor_ids.unlink() + [visitor_demo, visitor_admin] = Visitor.create([{ + 'partner_id': self.partner_demo.id, + 'access_token': self.partner_demo.id, + }, { + 'partner_id': self.partner_admin.id, + 'access_token': self.partner_admin.id, + }]) + # | id | access_token | partner_id | + # | -- | ------------ | ---------- | + # | 1 | demo_id | demo_id | + # | | 1062141 | 1062141 | + # | 2 | admin_id | admin_id | + # | | 5013266 | 5013266 | + self.assertTrue(visitor_demo.partner_id.id == int(visitor_demo.access_token) == self.partner_demo.id) + self.assertTrue(visitor_admin.partner_id.id == int(visitor_admin.access_token) == self.partner_admin.id) + + self.env['website.track'].create([{ + 'visitor_id': visitor_demo.id, + 'url': '/demo' + }, { + 'visitor_id': visitor_admin.id, + 'url': '/admin' + }]) + self.assertEqual(visitor_demo.website_track_ids.url, '/demo') + self.assertEqual(visitor_admin.website_track_ids.url, '/admin') + + # Merge demo partner in admin partner + self.env['base.partner.merge.automatic.wizard']._merge( + (self.partner_admin + self.partner_demo).ids, + self.partner_admin + ) + # Should be + # | id | access_token | partner_id | + # | -- | ------------ | ---------- | + # | 2 | admin_id | admin_id | + # | | 5013266 | 5013266 | + self.assertTrue(visitor_admin.exists()) + self.assertFalse(visitor_demo.exists()) + self.assertFalse(Visitor.search_count([('partner_id', '=', self.partner_demo.id)]), + "The demo visitor should've been merged (and deleted) with the admin one.") + # Track check + self.assertEqual(visitor_admin.website_track_ids.mapped('url'), ['/admin', '/demo']) + + def test_merge_partner_with_visitor_single(self): + """ The partner merge feature of Odoo is auto discovering relations to + ``res_partner`` to change the field value, in raw SQL. + It will change the ``partner_id`` field of visitor without changing the + ``access_token``, which is supposed to be the same value (``partner_id`` + is just a stored computed field holding the ``access_token`` value if it + is an integer value). + This partner_id/access_token "de-sync" need to be handled, this is done + in ``_update_foreign_keys()`` website override. + This test is ensuring that it works as it should. + + There is 2 possible cases: + + 1. There is a visitor for partner 1, none for partner 2. Partner 1 is + merged into partner 2, making partner_id of visitor from partner 1 + becoming partner 2. + -> The ``access_token`` value should also be updated from 1 to 2. + 2. There is a visitor for both partners and partner 1 is merged into + partner 2. + -> Both visitor should be merged too, so data are aggregated into a + single visitor. + + Case 1 is tested here. + Cade 2 is tested in :meth:`test_merge_partner_with_visitor_both`. + """ + # Setup a visitor for demo and none for admin + Visitor = self.env['website.visitor'] + (self.partner_demo + self.partner_admin).visitor_ids.unlink() + visitor_demo = Visitor.create({ + 'partner_id': self.partner_demo.id, + 'access_token': self.partner_demo.id, + }) + # | id | access_token | partner_id | + # | -- | ------------ | ---------- | + # | 1 | demo_id | demo_id | + # | | 1062141 | 1062141 | + self.assertTrue(visitor_demo.partner_id.id == int(visitor_demo.access_token) == self.partner_demo.id) + + # Merge demo partner in admin partner + self.env['base.partner.merge.automatic.wizard']._merge( + (self.partner_admin + self.partner_demo).ids, + self.partner_admin + ) + # This should not happen.. + # | id | access_token | partner_id | + # | -- | ------------ | ---------- | + # | 1 | demo_id | admin_id | <-- Mismatch + # | | 1062141 | 5013266 | + # .. it should be: + # | id | access_token | partner_id | + # | -- | ------------ | ---------- | + # | 1 | admin_id | admin_id | <-- No mismatch, became admin_id + # | | 5013266 | 5013266 | + self.assertTrue(visitor_demo.partner_id.id == int(visitor_demo.access_token) == self.partner_admin.id, + "The demo visitor should now be linked to the admin partner.") + self.assertFalse(Visitor.search_count([('partner_id', '=', self.partner_demo.id)]), + "The demo visitor should've been merged (and deleted) with the admin one.")