diff --git a/addons/privacy/__init__.py b/addons/privacy/__init__.py
new file mode 100644
index 00000000000..2ae6446f9dc
--- /dev/null
+++ b/addons/privacy/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import models
+from . import wizard
diff --git a/addons/privacy/__manifest__.py b/addons/privacy/__manifest__.py
new file mode 100644
index 00000000000..b557c83b821
--- /dev/null
+++ b/addons/privacy/__manifest__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': 'Privacy',
+ 'category': 'Hidden',
+ 'version': '1.0',
+ 'description': """""",
+ 'depends': ['mail'],
+ 'data': [
+ 'wizard/privacy_lookup_wizard_views.xml',
+ 'views/privacy_log_views.xml',
+ 'security/ir.model.access.csv',
+ 'data/ir_actions_server_data.xml',
+ ],
+ 'auto_install': True,
+ 'assets': {},
+ 'license': 'LGPL-3',
+}
diff --git a/addons/privacy/data/ir_actions_server_data.xml b/addons/privacy/data/ir_actions_server_data.xml
new file mode 100644
index 00000000000..aa1abe7c4d6
--- /dev/null
+++ b/addons/privacy/data/ir_actions_server_data.xml
@@ -0,0 +1,24 @@
+
+
+
+ Archive Selection
+
+
+ list
+ code
+
+records.action_archive_all()
+
+
+
+
+ Delete Selection
+
+
+ list
+ code
+
+records.action_unlink_all()
+
+
+
diff --git a/addons/privacy/models/__init__.py b/addons/privacy/models/__init__.py
new file mode 100644
index 00000000000..202b5ea8fef
--- /dev/null
+++ b/addons/privacy/models/__init__.py
@@ -0,0 +1,5 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import res_partner
+from . import privacy_log
diff --git a/addons/privacy/models/privacy_log.py b/addons/privacy/models/privacy_log.py
new file mode 100644
index 00000000000..accfab410fd
--- /dev/null
+++ b/addons/privacy/models/privacy_log.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+
+
+class PrivacyLog(models.Model):
+ _name = 'privacy.log'
+ _description = 'Privacy Log'
+
+ date = fields.Datetime(default=fields.Datetime.now, required=True)
+ anonymized_name = fields.Char(required=True)
+ anonymized_email = fields.Char(required=True)
+ user_id = fields.Many2one(
+ 'res.users', string="Handled By", required=True,
+ default=lambda self: self.env.user)
+ execution_details = fields.Text()
+ records_description = fields.Text(string="Found Records")
+ additional_note = fields.Text()
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ vals['anonymized_name'] = self._anonymize_name(vals['anonymized_name'])
+ vals['anonymized_email'] = self._anonymize_email(vals['anonymized_email'])
+ return super().create(vals_list)
+
+ def _anonymize_name(self, label):
+ if not label:
+ return ''
+ if '@' in label:
+ return self._anonymize_email(label)
+ return ' '.join(e[0] + '*' * (len(e) - 1) for e in label.split(' ') if e)
+
+ def _anonymize_email(self, label):
+ def _anonymize_user(label):
+ return '.'.join(e[0] + '*' * (len(e) - 1) for e in label.split('.') if e)
+ def _anonymize_domain(label):
+ if label in ['gmail.com', 'hotmail.com', 'yahoo.com']: # More than half of addresses domains
+ return label
+ split_domain = label.split('.')
+ return '.'.join([e[0] + '*' * (len(e) - 1) for e in split_domain[:-1] if e] + [split_domain[-1]])
+ if not label or '@' not in label:
+ return UserError(_('This email address is not valid (%s)', label))
+ user, domain = label.split('@')
+ return '{}@{}'.format(_anonymize_user(user), _anonymize_domain(domain))
diff --git a/addons/privacy/models/res_partner.py b/addons/privacy/models/res_partner.py
new file mode 100644
index 00000000000..5c1cfdc3076
--- /dev/null
+++ b/addons/privacy/models/res_partner.py
@@ -0,0 +1,17 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import models
+
+
+class ResPartner(models.Model):
+ _inherit = 'res.partner'
+
+ def action_privacy_lookup(self):
+ self.ensure_one()
+ action = self.env['ir.actions.act_window']._for_xml_id('privacy.action_privacy_lookup_wizard')
+ action['context'] = {
+ 'default_email': self.email,
+ 'default_name': self.name,
+ }
+ return action
diff --git a/addons/privacy/security/ir.model.access.csv b/addons/privacy/security/ir.model.access.csv
new file mode 100644
index 00000000000..74e260b8d21
--- /dev/null
+++ b/addons/privacy/security/ir.model.access.csv
@@ -0,0 +1,4 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+privacy.access_privacy_lookup_wizard,access_privacy_lookup_wizard,privacy.model_privacy_lookup_wizard,base.group_system,1,1,1,1
+privacy.access_privacy_lookup_wizard_line,access_privacy_lookup_wizard_line,privacy.model_privacy_lookup_wizard_line,base.group_system,1,1,1,0
+privacy.access_privacy_log,access_privacy_log,privacy.model_privacy_log,base.group_system,1,1,1,1
\ No newline at end of file
diff --git a/addons/privacy/tests/__init__.py b/addons/privacy/tests/__init__.py
new file mode 100644
index 00000000000..4031b256e28
--- /dev/null
+++ b/addons/privacy/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_privacy_wizard
diff --git a/addons/privacy/tests/test_privacy_wizard.py b/addons/privacy/tests/test_privacy_wizard.py
new file mode 100644
index 00000000000..ef302b7d1b4
--- /dev/null
+++ b/addons/privacy/tests/test_privacy_wizard.py
@@ -0,0 +1,152 @@
+# # -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.tests.common import TransactionCase, tagged
+
+
+@tagged('post_install', '-at_install', 'privacy')
+class TestPrivacyWizard(TransactionCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.partner = cls.env['res.partner'].create({
+ 'name': 'Rintin Tin',
+ 'email': 'rintin.tin@gmail.com'})
+
+ def test_wizard(self):
+ wizard = self.env['privacy.lookup.wizard'].with_context(
+ default_email=self.partner.email,
+ default_name=self.partner.name,
+ ).create({})
+
+ # Lookup
+ wizard.action_lookup()
+ self.assertEqual(len(wizard.line_ids), 1)
+ self.assertEqual(wizard.line_ids.res_id, self.partner.id)
+ self.assertEqual(wizard.line_ids.res_model, self.partner._name)
+ self.assertFalse(wizard.log_id)
+
+ # Archive
+ wizard.line_ids.is_active = False
+ wizard.line_ids._onchange_is_active()
+ self.assertFalse(self.partner.active)
+ self.assertEqual(wizard.execution_details, 'Archived Contact #%s' % (self.partner.id))
+ self.assertTrue(wizard.log_id)
+ self.assertEqual(wizard.log_id.anonymized_name, 'R***** T**')
+ self.assertEqual(wizard.log_id.anonymized_email, 'r*****.t**@gmail.com')
+ self.assertEqual(wizard.log_id.execution_details, 'Archived Contact #%s' % (self.partner.id))
+ self.assertEqual(wizard.log_id.records_description, 'Contact (1): #%s' % (self.partner.id))
+
+ # Delete
+ wizard.line_ids.action_unlink()
+ self.assertEqual(wizard.execution_details, 'Deleted Contact #%s' % (self.partner.id))
+ self.assertEqual(wizard.log_id.execution_details, 'Deleted Contact #%s' % (self.partner.id))
+
+ def test_wizard_multi_company(self):
+ # Check that the record is spotted, even if not available on the Reference field
+ self.env['ir.rule'].create({
+ 'name': 'Multi-Company Rule',
+ 'model_id': self.env.ref('base.model_res_partner').id,
+ 'domain_force': "['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]"
+ })
+ company_2 = self.env['res.company'].create({'name': 'Company 2'})
+ other_partner = self.env['res.partner'].create({
+ 'name': 'Rintin Tin',
+ 'email': 'rintin.tin@gmail.com',
+ 'company_id': company_2.id
+ })
+ self.assertNotEqual(self.partner.company_id, other_partner.company_id)
+
+ wizard = self.env['privacy.lookup.wizard'].with_context(
+ default_email=self.partner.email,
+ default_name=self.partner.name,
+ ).with_user(self.env.ref('base.user_admin')).create({})
+
+ # Lookup
+ wizard.action_lookup()
+ self.assertEqual(len(wizard.line_ids), 2)
+ self.assertEqual(wizard.line_ids[1].resource_ref, self.partner)
+ self.assertEqual(wizard.line_ids[0].resource_ref, None)
+
+ def test_wizard_direct_reference(self):
+ bank = self.env['res.bank'].create({
+ 'name': 'ING',
+ 'bic': 'BBRUBEBB',
+ 'email': 'rintin.tin@gmail.com'
+ })
+
+ wizard = self.env['privacy.lookup.wizard'].with_context(
+ default_email=self.partner.email,
+ default_name=self.partner.name,
+ ).create({})
+
+ # Lookup
+ wizard.action_lookup()
+ self.assertEqual(len(wizard.line_ids), 2)
+ self.assertEqual(wizard.line_ids[0].res_id, self.partner.id)
+ self.assertEqual(wizard.line_ids[0].res_model, self.partner._name)
+
+ self.assertEqual(wizard.line_ids[1].res_id, bank.id)
+ self.assertEqual(wizard.line_ids[1].res_model, bank._name)
+
+ def test_wizard_indirect_reference(self):
+ self.env.company.partner_id = self.partner
+
+ wizard = self.env['privacy.lookup.wizard'].with_context(
+ default_email=self.partner.email,
+ default_name=self.partner.name,
+ ).create({})
+
+ # Lookup
+ wizard.action_lookup()
+ self.assertEqual(len(wizard.line_ids), 2)
+ self.assertEqual(wizard.line_ids[0].res_id, self.partner.id)
+ self.assertEqual(wizard.line_ids[0].res_model, self.partner._name)
+
+ self.assertEqual(wizard.line_ids[1].res_id, self.env.company.id)
+ self.assertEqual(wizard.line_ids[1].res_model, self.env.company._name)
+
+ def test_wizard_indirect_reference_cascade(self):
+ # Don't retrieve ondelete cascade records
+ self.env["res.partner.bank"].create({
+ 'acc_number': '0123-%s' % self.partner.id,
+ 'partner_id': self.partner.id,
+ 'company_id': self.env.company.id
+ })
+
+ wizard = self.env['privacy.lookup.wizard'].with_context(
+ default_email=self.partner.email,
+ default_name=self.partner.name,
+ ).create({})
+
+ # Lookup
+ wizard.action_lookup()
+ self.assertEqual(len(wizard.line_ids), 1)
+ self.assertEqual(wizard.line_ids[0].res_id, self.partner.id)
+ self.assertEqual(wizard.line_ids[0].res_model, self.partner._name)
+
+ def test_wizard_unique_log(self):
+ # Check that the log remains unique
+ self.env['res.partner'].create({
+ 'name': 'Rintin Tin',
+ 'email': 'rintin.tin@gmail.com'})
+
+ wizard = self.env['privacy.lookup.wizard'].with_context(
+ default_email=self.partner.email,
+ default_name=self.partner.name,
+ ).create({})
+
+ # Lookup
+ wizard.action_lookup()
+ self.assertEqual(len(wizard.line_ids), 2)
+
+ wizard.line_ids[0].is_active = False
+ wizard.line_ids[0]._onchange_is_active()
+ wizard.execution_details
+ self.assertEqual(1, self.env['privacy.log'].search_count([('anonymized_email', '=', 'r*****.t**@gmail.com')]))
+
+ wizard.line_ids[1].is_active = False
+ wizard.line_ids[1]._onchange_is_active()
+ wizard.execution_details
+ self.assertEqual(1, self.env['privacy.log'].search_count([('anonymized_email', '=', 'r*****.t**@gmail.com')]))
diff --git a/addons/privacy/views/privacy_log_views.xml b/addons/privacy/views/privacy_log_views.xml
new file mode 100644
index 00000000000..aea925db0c3
--- /dev/null
+++ b/addons/privacy/views/privacy_log_views.xml
@@ -0,0 +1,73 @@
+
+
+
+ privacy.log.view.tree
+ privacy.log
+
+
+
+
+
+
+
+
+
+
+
+
+ privacy.log.view.form
+ privacy.log
+
+
+
+
+
+
+ Privacy Logs
+ privacy.log
+ tree,form
+ {'create': False}
+
+
+
+ Privacy Logs
+ privacy.log
+ form
+ {'create': False}
+
+
+
+
+
+
diff --git a/addons/privacy/wizard/__init__.py b/addons/privacy/wizard/__init__.py
new file mode 100644
index 00000000000..17273b6f2b7
--- /dev/null
+++ b/addons/privacy/wizard/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import privacy_lookup_wizard
diff --git a/addons/privacy/wizard/privacy_lookup_wizard.py b/addons/privacy/wizard/privacy_lookup_wizard.py
new file mode 100644
index 00000000000..de207297d67
--- /dev/null
+++ b/addons/privacy/wizard/privacy_lookup_wizard.py
@@ -0,0 +1,329 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from collections import defaultdict
+
+from odoo import api, fields, models, tools, _
+from odoo.exceptions import UserError
+from odoo.tools import pycompat
+
+
+class PrivacyLookupWizard(models.TransientModel):
+ _name = 'privacy.lookup.wizard'
+ _description = 'Privacy Lookup Wizard'
+ _transient_max_count = 0
+ _transient_max_hours = 24
+
+ name = fields.Char(required=True)
+ email = fields.Char(required=True)
+ line_ids = fields.One2many('privacy.lookup.wizard.line', 'wizard_id')
+ execution_details = fields.Text(compute='_compute_execution_details', store=True)
+ log_id = fields.Many2one('privacy.log')
+ records_description = fields.Text(compute='_compute_records_description')
+ line_count = fields.Integer(compute='_compute_line_count')
+
+ @api.depends('line_ids')
+ def _compute_line_count(self):
+ for wizard in self:
+ wizard.line_count = len(wizard.line_ids)
+
+ def name_get(self):
+ return [(w.id, _('Privacy Lookup')) for w in self]
+
+ def _get_query_models_blacklist(self):
+ return [
+ # Already Managed
+ 'res.partner',
+ 'res.users',
+ # Ondelete Cascade
+ 'mail.notification',
+ 'mail.followers',
+ 'mail.channel.partner',
+ # Special case for direct messages
+ 'mail.message',
+ ]
+
+ def _get_query(self):
+ name = "%s" % (self.name.strip())
+ email = "%%%s%%" % pycompat.to_text(self.email.strip())
+ email_normalized = tools.email_normalize(self.email.strip())
+
+ # Step 1: Retrieve users/partners liked to email address or name
+ query = """
+ WITH indirect_references AS (
+ SELECT id
+ FROM res_partner
+ WHERE email_normalized = %s
+ OR name ilike %s)
+ SELECT
+ %s AS res_model_id,
+ id AS res_id,
+ active AS is_active
+ FROM res_partner
+ WHERE id IN (SELECT id FROM indirect_references)
+ UNION ALL
+ SELECT
+ %s AS res_model_id,
+ id AS res_id,
+ active AS is_active
+ FROM res_users
+ WHERE (
+ (login ilike %s)
+ OR
+ (partner_id IN (
+ SELECT id
+ FROM res_partner
+ WHERE email ilike %s or name ilike %s)))
+ """
+ values = [
+ # Indirect references CTE
+ email_normalized, name,
+ # Search on res.partner
+ self.env['ir.model.data']._xmlid_to_res_id('base.model_res_partner'),
+ # Search on res.users
+ self.env['ir.model.data']._xmlid_to_res_id('base.model_res_users'), email, email, name,
+ ]
+
+ # Step 2: Special case for direct messages
+ query += """
+ UNION ALL
+ SELECT
+ %s AS res_model_id,
+ id AS res_id,
+ True AS is_active
+ FROM mail_message
+ WHERE author_id IN (SELECT id FROM indirect_references)
+ """
+ values += [
+ self.env['ir.model.data']._xmlid_to_res_id('mail.model_mail_message'),
+ ]
+
+ # Step 3: Retrieve info on other models
+ blacklisted_models = self._get_query_models_blacklist()
+ for model_name in self.env:
+ if model_name in blacklisted_models:
+ continue
+ table_name = model_name.replace('.', '_')
+
+ model = self.env[model_name]
+ if model._transient or model._transient or not model._auto:
+ continue
+ res_model_id = self.env['ir.model'].search([('model', '=', model_name)]).id
+ has_active = 'active' in model
+ has_additional_query = False
+ additional_query = """
+ UNION ALL
+ SELECT
+ %s AS res_model_id,
+ id AS res_id,
+ {active} AS is_active
+ FROM {table_name}
+ WHERE
+ """.format(table_name=table_name, active='active' if has_active else True)
+ additional_values = [
+ res_model_id
+ ]
+
+ # 3.1 Search Basic Personal Data Records (aka email/name usage)
+ for field_name in ['email_normalized', 'email', 'email_from', 'company_email']:
+ if field_name in model and model._fields[field_name].store:
+ has_additional_query = True
+ rec_name = model._rec_name or 'name'
+ is_normalized = field_name == 'email_normalized' or (model_name == 'mailing.trace' and field_name == 'email')
+ if rec_name in model and model._fields[model._rec_name].type == 'char':
+ additional_query += """
+ {field_name} {search_type} %s OR {rec_name} ilike %s
+ """.format(
+ field_name=field_name,
+ search_type='=' if is_normalized else 'ilike', # Manage Foo Bar
+ rec_name=rec_name)
+ additional_values += [email_normalized if is_normalized else email, name]
+ else:
+ additional_query += """
+ {field_name} {search_type} %s
+ """.format(
+ field_name=field_name,
+ search_type='=' if is_normalized else 'ilike') # Manage Foo Bar
+ additional_values += [email_normalized if is_normalized else email]
+ if is_normalized:
+ break
+
+ # 3.2 Search Indirect Personal Data References (aka partner_id)
+ partner_fields = [
+ field_name for field_name, field in model._fields.items() \
+ if field.comodel_name == 'res.partner' and field.store and field.type == 'many2one' and field.ondelete != 'cascade']
+ if partner_fields:
+ for field_name in partner_fields:
+ additional_query += """
+ {or_clause}{table_field_name} in (SELECT id FROM indirect_references)""".format(
+ or_clause='OR ' if has_additional_query else '',
+ table_field_name='"%s"."%s"' % (table_name, field_name))
+ has_additional_query = True
+
+ if has_additional_query:
+ query += additional_query
+ values += additional_values
+ return query, values
+
+ def action_lookup(self):
+ self.ensure_one()
+ query, values = self._get_query()
+ self.env['base'].flush()
+ self.env.cr.execute(query, tuple(values))
+ results = self.env.cr.dictfetchall()
+ self.line_ids = [(5, 0, 0)] + [(0, 0, reference) for reference in results]
+ return self.action_open_lines()
+
+ def _post_log(self):
+ self.ensure_one()
+ if not self.log_id and self.execution_details:
+ self.log_id = self.env['privacy.log'].create({
+ 'anonymized_name': self.name,
+ 'anonymized_email': self.email,
+ 'execution_details': self.execution_details,
+ 'records_description': self.records_description,
+ })
+ else:
+ self.log_id.execution_details = self.execution_details
+ self.log_id.records_description = self.records_description
+
+ @api.depends('line_ids.execution_details')
+ def _compute_execution_details(self):
+ for wizard in self:
+ wizard.execution_details = '\n'.join(line.execution_details for line in wizard.line_ids if line.execution_details)
+ wizard._post_log()
+
+ @api.depends('line_ids')
+ def _compute_records_description(self):
+ for wizard in self:
+ if not wizard.line_ids:
+ wizard.records_description = ''
+ continue
+ records_by_model = defaultdict(list)
+ for line in wizard.line_ids:
+ records_by_model[line.res_model_id].append(line.res_id)
+ wizard.records_description = '\n'.join('{model_name} ({count}): {ids_str}'.format(
+ model_name=model.name if not self.env.user.user_has_groups('base.group_no_one') else '%s - %s' % (model.name, model.model),
+ count=len(ids),
+ ids_str=', '.join('#%s' % (rec_id) for rec_id in ids),
+ ) for model, ids in records_by_model.items())
+
+ def action_open_lines(self):
+ self.ensure_one()
+ action = self.env['ir.actions.act_window']._for_xml_id('privacy.action_privacy_lookup_wizard_line')
+ action['domain'] = [('wizard_id', '=', self.id)]
+ return action
+
+
+class PrivacyLookupWizardLine(models.TransientModel):
+ _name = 'privacy.lookup.wizard.line'
+ _description = 'Privacy Lookup Wizard Line'
+ _transient_max_count = 0
+ _transient_max_hours = 24
+
+ @api.model
+ def _selection_target_model(self):
+ return [(model.model, model.name) for model in self.env['ir.model'].sudo().search([])]
+
+ wizard_id = fields.Many2one('privacy.lookup.wizard')
+ res_id = fields.Integer(
+ string="Resource ID",
+ required=True)
+ res_name = fields.Char(
+ string='Resource name',
+ compute='_compute_res_name',
+ store=True)
+ res_model_id = fields.Many2one(
+ 'ir.model',
+ 'Related Document Model',
+ ondelete='cascade')
+ res_model = fields.Char(
+ string='Document Model',
+ related='res_model_id.model',
+ store=True,
+ readonly=True)
+ resource_ref = fields.Reference(
+ string='Record',
+ selection='_selection_target_model',
+ compute='_compute_resource_ref',
+ inverse='_set_resource_ref')
+ has_active = fields.Boolean(compute='_compute_has_active', store=True)
+ is_active = fields.Boolean()
+ is_unlinked = fields.Boolean()
+ execution_details = fields.Char(default='')
+
+ @api.depends('res_model', 'res_id', 'is_unlinked')
+ def _compute_resource_ref(self):
+ for line in self:
+ if line.res_model and line.res_model in self.env and not line.is_unlinked:
+ # Exclude records that can't be read (eg: multi-company ir.rule)
+ try:
+ self.env[line.res_model].browse(line.res_id).check_access_rule('read')
+ line.resource_ref = '%s,%s' % (line.res_model, line.res_id or 0)
+ except Exception:
+ line.resource_ref = None
+ else:
+ line.resource_ref = None
+
+ def _set_resource_ref(self):
+ for line in self:
+ if line.resource_ref:
+ line.res_id = line.resource_ref.id
+
+ @api.depends('res_model_id')
+ def _compute_has_active(self):
+ for line in self:
+ if not line.res_model_id:
+ line.has_active = False
+ continue
+ line.has_active = 'active' in self.env[line.res_model]
+
+ @api.depends('res_model', 'res_id')
+ def _compute_res_name(self):
+ for line in self:
+ if not line.res_id or not line.res_model:
+ continue
+ record = self.env[line.res_model].sudo().browse(line.res_id)
+ if not record.exists():
+ continue
+ name = record.name_get()
+ line.res_name = name[0][1] if name else ('%s/%s') % (line.res_model_id.name, line.res_id)
+
+ @api.onchange('is_active')
+ def _onchange_is_active(self):
+ for line in self:
+ if not line.res_model_id or not line.res_id:
+ continue
+ action = _('Unarchived') if line.is_active else _('Archived')
+ line.execution_details = '%s %s #%s' % (action, line.res_model_id.name, line.res_id)
+ self.env[line.res_model].sudo().browse(line.res_id).write({'active': line.is_active})
+
+ def action_unlink(self):
+ self.ensure_one()
+ if self.is_unlinked:
+ raise UserError(_('The record is already unlinked.'))
+ self.env[self.res_model].sudo().browse(self.res_id).unlink()
+ self.execution_details = '%s %s #%s' % (_('Deleted'), self.res_model_id.name, self.res_id)
+ self.is_unlinked = True
+
+ def action_archive_all(self):
+ for line in self:
+ if not line.has_active or not line.is_active:
+ continue
+ line.is_active = False
+ line._onchange_is_active()
+
+ def action_unlink_all(self):
+ for line in self:
+ if line.is_unlinked:
+ continue
+ line.action_unlink()
+
+ def action_open_record(self):
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'form',
+ 'res_id': self.res_id,
+ 'res_model': self.res_model,
+ }
diff --git a/addons/privacy/wizard/privacy_lookup_wizard_views.xml b/addons/privacy/wizard/privacy_lookup_wizard_views.xml
new file mode 100644
index 00000000000..e147f80178c
--- /dev/null
+++ b/addons/privacy/wizard/privacy_lookup_wizard_views.xml
@@ -0,0 +1,111 @@
+
+
+
+ privacy.lookup.wizard.view.form
+ privacy.lookup.wizard
+
+
+
+
+
+
+ Privacy Lookup
+ privacy.lookup.wizard
+ form
+
+ current
+
+
+
+ privacy.lookup.wizard.line.view.tree
+ privacy.lookup.wizard.line
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ privacy.lookup.wizard.line.view.search
+ privacy.lookup.wizard.line
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Privacy Lookup Line
+ privacy.lookup.wizard.line
+ tree
+
+ {'search_default_group_by_res_model_id': 1, 'no_create_edit': True}
+ current
+
+
+
+ Privacy Lookup
+
+
+ form
+ code
+
+action = record.action_privacy_lookup()
+
+
+
+
+ Privacy Lookup
+
+
+ form
+ code
+
+action = record.partner_id.action_privacy_lookup()
+
+
+