[ADD] privacy: Add privacy lookup / management tool
Purpose ======= This commit brings a new tool to retrieve all the related pieces of information about a customer, based on his name and/or email address. This could be useful for users who want to apply the right to be forgotten (for example under the GDPR regulation), or simply to retrieve all the records linked to someone (leads, contracts, sales orders, ...). The tools is actually a wizard, available from the contact/user form view. The 2 main steps are the following: 1/ Lookup: We search all the references of the contact by scrapping the existing models, either if his email address is written on a supposedly email field, or if his name is written into a record rec_name (aka name of a lead), or also if there is a M2O to a res.partner that matches the email address or the name (For readability purposes, we exclude the ondelete=cascade fields as the record will be dropped with the contact itself. 2/ Management: For each one of the occurences, we display a line on the wizard for which we allow 3 actions: Open the record, Archive (if possible), Delete. Each time an action is performed that altered the record itself, a anonymized log (jean.luc@example.com becoming j***.l**@e******.com) is created/adapted for history purposes. Taskid: 2827911 Part-of: odoo/odoo#89312
This commit is contained in:
@@ -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
|
||||
@@ -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',
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="ir_actions_server_archive_all" model="ir.actions.server">
|
||||
<field name="name">Archive Selection</field>
|
||||
<field name="model_id" ref="privacy.model_privacy_lookup_wizard_line"/>
|
||||
<field name="binding_model_id" ref="privacy.model_privacy_lookup_wizard_line"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
records.action_archive_all()
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_actions_server_unlink_all" model="ir.actions.server">
|
||||
<field name="name">Delete Selection</field>
|
||||
<field name="model_id" ref="privacy.model_privacy_lookup_wizard_line"/>
|
||||
<field name="binding_model_id" ref="privacy.model_privacy_lookup_wizard_line"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
records.action_unlink_all()
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -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
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_privacy_wizard
|
||||
@@ -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')]))
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="privacy_log_view_list" model="ir.ui.view">
|
||||
<field name="name">privacy.log.view.tree</field>
|
||||
<field name="model">privacy.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Privacy Logs">
|
||||
<field name="date" />
|
||||
<field name="anonymized_name"/>
|
||||
<field name="anonymized_email"/>
|
||||
<field name="user_id" widget="many2one_avatar_user"/>
|
||||
<field name="execution_details"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="privacy_log_view_form" model="ir.ui.view">
|
||||
<field name="name">privacy.log.view.form</field>
|
||||
<field name="model">privacy.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Privacy Log">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="anonymized_name"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="anonymized_email"/>
|
||||
<field name="date"/>
|
||||
<field name="user_id" widget="many2one_avatar_user"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="records_description"/>
|
||||
<field name="execution_details"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="additional_note"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="privacy_log_action" model="ir.actions.act_window">
|
||||
<field name="name">Privacy Logs</field>
|
||||
<field name="res_model">privacy.log</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'create': False}</field>
|
||||
</record>
|
||||
|
||||
<record id="privacy_log_form_action" model="ir.actions.act_window">
|
||||
<field name="name">Privacy Logs</field>
|
||||
<field name="res_model">privacy.log</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="context">{'create': False}</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id="privacy_menu"
|
||||
name="Privacy"
|
||||
parent="base.menu_custom"
|
||||
sequence="26"/>
|
||||
|
||||
<menuitem
|
||||
name="Privacy Logs"
|
||||
action="privacy_log_action"
|
||||
id="pricacy_log_menu"
|
||||
parent="privacy_menu"
|
||||
sequence="1"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import privacy_lookup_wizard
|
||||
@@ -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 <foo@bar.com>
|
||||
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 <foo@bar.com>
|
||||
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,
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="privacy_lookup_wizard_view_form" model="ir.ui.view">
|
||||
<field name="name">privacy.lookup.wizard.view.form</field>
|
||||
<field name="model">privacy.lookup.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Privacy Lookup">
|
||||
<header>
|
||||
<button string="Lookup" name="action_lookup" type="object" class="oe_highlight" data-hotkey="q" attrs="{'invisible': [('line_ids', '!=', [])]}"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button
|
||||
name="action_open_lines"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-file-text-o">
|
||||
<field name="line_count" string="References" widget="statinfo"/>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="email" attrs="{'readonly': [('line_ids', '!=', [])]}"/>
|
||||
<field name="name" attrs="{'readonly': [('line_ids', '!=', [])]}"/>
|
||||
<field name="records_description" invisible="1"/>
|
||||
<field name="execution_details" attrs="{'invisible': [('execution_details', '=', '')]}"/>
|
||||
<field name="log_id" invisible="1"/>
|
||||
</group>
|
||||
<field name="line_ids" attrs="{'invisible': [('line_ids', '=', [])]}" options="{'no_open': True, 'no_create': True}" invisible="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_privacy_lookup_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Privacy Lookup</field>
|
||||
<field name="res_model">privacy.lookup.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="privacy_lookup_wizard_view_form"/>
|
||||
<field name="target">current</field>
|
||||
</record>
|
||||
|
||||
<record id="privacy_lookup_wizard_line_view_tree" model="ir.ui.view">
|
||||
<field name="name">privacy.lookup.wizard.line.view.tree</field>
|
||||
<field name="model">privacy.lookup.wizard.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree create="0">
|
||||
<field name="res_model_id"/>
|
||||
<field name="res_name" invisible="1"/>
|
||||
<field name="res_model" invisible="1"/>
|
||||
<field name="resource_ref" options="{'model_field': 'res_model_id', 'no_create': True}" attrs="{'invisible': [('resource_ref', '=', False)]}"/>
|
||||
<button name="action_open_record" type="object" icon="fa-external-link" attrs="{'invisible': [('resource_ref', '=', False)]}" title="Open Record"/>
|
||||
<field name="res_id" string="ID"/>
|
||||
<field name="has_active" invisible="1"/>
|
||||
<field name="execution_details" invisible="1"/>
|
||||
<field name="is_active" widget="boolean_toggle" attrs="{'invisible': ['|', ('is_unlinked', '=', True), ('has_active', '=', False)]}"/>
|
||||
<field name="is_unlinked" invisible="True"/>
|
||||
<button name="action_unlink" string="Delete" type="object" icon="fa-trash" attrs="{'invisible': [('is_unlinked', '=', True)]}" confirm="This operation is irreversible. Do you wish to proceed to the record deletion ?"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="privacy_lookup_wizard_line_view_search" model="ir.ui.view">
|
||||
<field name="name">privacy.lookup.wizard.line.view.search</field>
|
||||
<field name="model">privacy.lookup.wizard.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search References">
|
||||
<field name="res_model_id"/>
|
||||
<field name="has_active" invisible="1"/>
|
||||
<field name="is_active" invisible="1"/>
|
||||
<filter string="Can be archived" name="can_be_archived" domain="[('has_active', '=', True)]"/>
|
||||
<filter string="Archived" name="archived" domain="[('is_active', '=', False)]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Model" name="group_by_res_model_id" domain="[]" context="{'group_by': 'res_model_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_privacy_lookup_wizard_line" model="ir.actions.act_window">
|
||||
<field name="name">Privacy Lookup Line</field>
|
||||
<field name="res_model">privacy.lookup.wizard.line</field>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="privacy_lookup_wizard_line_view_tree"/>
|
||||
<field name="context">{'search_default_group_by_res_model_id': 1, 'no_create_edit': True}</field>
|
||||
<field name="target">current</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_action_server_action_privacy_lookup_partner" model="ir.actions.server">
|
||||
<field name="name">Privacy Lookup</field>
|
||||
<field name="model_id" ref="base.model_res_partner"/>
|
||||
<field name="binding_model_id" ref="base.model_res_partner"/>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
action = record.action_privacy_lookup()
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="ir_action_server_action_privacy_lookup_user" model="ir.actions.server">
|
||||
<field name="name">Privacy Lookup</field>
|
||||
<field name="model_id" ref="base.model_res_users"/>
|
||||
<field name="binding_model_id" ref="base.model_res_users"/>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
action = record.partner_id.action_privacy_lookup()
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user