diff --git a/privacy/security/data_protection.xml b/privacy/security/data_protection.xml
index 086d640..4a2ed9c 100644
--- a/privacy/security/data_protection.xml
+++ b/privacy/security/data_protection.xml
@@ -19,6 +19,7 @@
Data Protection Manager
+
diff --git a/privacy_partner_report/README.rst b/privacy_partner_report/README.rst
new file mode 100644
index 0000000..beb439d
--- /dev/null
+++ b/privacy_partner_report/README.rst
@@ -0,0 +1,67 @@
+.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png
+ :target: https://www.gnu.org/licenses/agpl
+ :alt: License: AGPL-3
+
+======================
+Privacy Partner Report
+======================
+
+This module helps a company to identify all the transactions that a specific
+partner is involved in, with the possibility to export the associated data.
+
+
+Usage
+=====
+
+To use this module, you need to:
+
+#. Go to menu of ``Privacy > Reports > Partner Report``.
+#. Select a partner, and then wait some seconds until a list of models appear.
+#. Click on ``Export XLSX``.
+
+.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
+ :alt: Try me on Runbot
+ :target: https://runbot.odoo-community.org/runbot/263/10.0
+
+Known issues / Roadmap
+======================
+
+* Remove controller workaround if https://github.com/odoo/odoo/pull/24964 is merged.
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub Issues
+`_. In case of trouble, please
+check there if your issue has already been reported. If you spotted it first,
+help us smash it by providing detailed and welcomed feedback.
+
+Credits
+=======
+
+Images
+------
+
+* Odoo Community Association: `Icon `_.
+
+Contributors
+------------
+
+* Miquel Raïch
+
+Do not contact contributors directly about support or help with technical issues.
+
+Maintainer
+----------
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+This module is maintained by the OCA.
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+To contribute to this module, please visit https://odoo-community.org.
diff --git a/privacy_partner_report/__init__.py b/privacy_partner_report/__init__.py
new file mode 100644
index 0000000..f07586e
--- /dev/null
+++ b/privacy_partner_report/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+from . import controllers
+from . import report
+from . import wizard
diff --git a/privacy_partner_report/__manifest__.py b/privacy_partner_report/__manifest__.py
new file mode 100644
index 0000000..51c92b6
--- /dev/null
+++ b/privacy_partner_report/__manifest__.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Copyright 2018 Eficent Business and IT Consulting Services S.L.
+# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
+{
+ 'name': 'Privacy Partner Report',
+ 'version': '10.0.1.0.0',
+ 'category': 'GDPR',
+ 'summary': 'Show the transactions that a specific partner is involved in.',
+ 'author': "Eficent, "
+ "Odoo Community Association (OCA)",
+ 'website': 'https://github.com/OCA/data-protection/',
+ 'license': 'AGPL-3',
+ 'depends': ['privacy', 'report_xlsx'],
+ 'data': [
+ 'wizard/privacy_report_partner_wizard.xml',
+ 'views/privacy_report.xml',
+ 'views/privacy_menu_view.xml',
+ ],
+ 'installable': True,
+ 'maintainers': ['mreficent'],
+}
diff --git a/privacy_partner_report/controllers/__init__.py b/privacy_partner_report/controllers/__init__.py
new file mode 100644
index 0000000..757b12a
--- /dev/null
+++ b/privacy_partner_report/controllers/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from . import main
diff --git a/privacy_partner_report/controllers/main.py b/privacy_partner_report/controllers/main.py
new file mode 100644
index 0000000..13b6c5a
--- /dev/null
+++ b/privacy_partner_report/controllers/main.py
@@ -0,0 +1,21 @@
+# Copyright 2018 Eficent Business and IT Consulting Services, S.L.
+# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
+
+import json
+from odoo import http
+
+from odoo.addons.web.controllers.main import serialize_exception
+from odoo.addons.web.controllers.main import Reports
+
+
+class ReportsExtended(Reports):
+ # HACK of https://github.com/odoo/odoo/pull/24964
+
+ @http.route()
+ @serialize_exception
+ def index(self, action, token):
+ action = json.loads(action)
+ if "data" in action.keys() and action["data"]:
+ action["datas"] = action["data"]
+ action = json.dumps(action)
+ return super(ReportsExtended, self).index(action, token)
diff --git a/privacy_partner_report/report/__init__.py b/privacy_partner_report/report/__init__.py
new file mode 100644
index 0000000..38a00d0
--- /dev/null
+++ b/privacy_partner_report/report/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from . import privacy_partner_xlsx
diff --git a/privacy_partner_report/report/privacy_partner_xlsx.py b/privacy_partner_report/report/privacy_partner_xlsx.py
new file mode 100644
index 0000000..030afc9
--- /dev/null
+++ b/privacy_partner_report/report/privacy_partner_xlsx.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+# Copyright 2018 Eficent Business and IT Consulting Services S.L.
+# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
+
+import logging
+from odoo.report import report_sxw
+from odoo.tools.translate import _
+
+_logger = logging.getLogger(__name__)
+
+try:
+ from openerp.addons.report_xlsx.report.report_xlsx import ReportXlsx
+except ImportError:
+ _logger.debug("report_xlsx not installed, Excel export non functional")
+
+ class ReportXlsx(object):
+ def __init__(self, *args, **kwargs):
+ pass
+
+
+class ReportPartnerXlsx(ReportXlsx):
+
+ def _search_longest_row(self, tables):
+ res = 0
+ for table in tables:
+ for model in tables[table]:
+ if len(tables[table][model]) > 0:
+ if len(tables[table][model][0]) > res:
+ res = len(tables[table][model][0])
+ return res
+
+ def generate_xlsx_report(self, workbook, data, objects):
+ partner = data['form'].get('partner_id', False)
+ partner = self.env['res.partner'].sudo().browse(partner[0])
+ workbook.set_properties({
+ 'comments': 'Created with Python and XlsxWriter from Odoo'})
+ sheet = workbook.add_worksheet(_('Partner Data'))
+ sheet.set_landscape()
+ sheet.fit_to_pages(1, 0)
+ sheet.set_zoom(75)
+ sheet.set_column(0, self._search_longest_row(data['tables']), 25)
+ title_style = workbook.add_format(
+ {'bold': True, 'bg_color': '#FFFFCC', 'border': 2})
+ sheet.set_row(0, None, None, {'collapsed': 1})
+ sheet.write_row(1, 0, ["Partner: " + partner.display_name],
+ title_style)
+ i = 3
+ first_row = i+2
+ for table in sorted(data['tables'].keys()):
+ for model in sorted(data['tables'][table].keys()):
+ rows = len(data['tables'][table][model])
+ if rows:
+ style = workbook.add_format()
+ style.set_bold(True)
+ style.set_border(2)
+ sheet.write_row(i, 0, [model], style)
+ i += 1
+ j = 0
+ for column in data['tables'][table][model][0]:
+ style = workbook.add_format()
+ style.set_bold(True)
+ if j == 0:
+ style.set_left(1)
+ if j == len(data['tables'][table][model][0]) - 1:
+ style.set_right(1)
+ style.set_top(1)
+ style.set_bottom(1)
+ sheet.write_row(i, j, [column], style)
+ j += 1
+ for row in data['tables'][table][model]:
+ i += 1
+ j = 0
+ for column in row:
+ style = workbook.add_format()
+ if j == 0:
+ style.set_left(1)
+ if j == len(row) - 1:
+ style.set_right(1)
+ if i == rows + first_row - 1:
+ style.set_bottom(1)
+ if row[column]:
+ sheet.write_row(i, j, [row[column]], style)
+ else:
+ sheet.write_row(i, j, [''], style)
+ j += 1
+ i += 2
+ first_row = i+2
+
+
+ReportPartnerXlsx(
+ 'report.privacy.report_partner_xlsx',
+ 'privacy.partner.report',
+ parser=report_sxw.rml_parse
+)
diff --git a/privacy_partner_report/static/description/icon.png b/privacy_partner_report/static/description/icon.png
new file mode 100644
index 0000000..3a0328b
Binary files /dev/null and b/privacy_partner_report/static/description/icon.png differ
diff --git a/privacy_partner_report/views/privacy_menu_view.xml b/privacy_partner_report/views/privacy_menu_view.xml
new file mode 100644
index 0000000..a1cad2d
--- /dev/null
+++ b/privacy_partner_report/views/privacy_menu_view.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/privacy_partner_report/views/privacy_report.xml b/privacy_partner_report/views/privacy_report.xml
new file mode 100644
index 0000000..ae11dbc
--- /dev/null
+++ b/privacy_partner_report/views/privacy_report.xml
@@ -0,0 +1,11 @@
+
+
+
+ Privacy Partner Data
+ privacy.partner.report
+ ir.actions.report.xml
+ privacy.report_partner_xlsx
+ xlsx
+
+
+
diff --git a/privacy_partner_report/wizard/__init__.py b/privacy_partner_report/wizard/__init__.py
new file mode 100644
index 0000000..bcc6c24
--- /dev/null
+++ b/privacy_partner_report/wizard/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from . import privacy_report_partner
diff --git a/privacy_partner_report/wizard/privacy_report_partner.py b/privacy_partner_report/wizard/privacy_report_partner.py
new file mode 100644
index 0000000..8d50fab
--- /dev/null
+++ b/privacy_partner_report/wizard/privacy_report_partner.py
@@ -0,0 +1,226 @@
+# -*- coding: utf-8 -*-
+# Copyright 2018 Eficent Business and IT Consulting Services S.L.
+# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
+from odoo import api, fields, models, _
+from odoo.exceptions import UserError
+
+
+class PrivacyPartnerReport(models.TransientModel):
+ _name = "privacy.partner.report"
+ _description = "Privacy Partner Report"
+
+ company_id = fields.Many2one(
+ comodel_name='res.company',
+ string='Company',
+ required=True,
+ default=lambda self: self.env.user.company_id,
+ )
+ partner_id = fields.Many2one(
+ comodel_name='res.partner',
+ string='Partner',
+ required=True,
+ )
+ table_ids = fields.Many2many(
+ comodel_name='privacy.partner.data',
+ string='Models with related partner data',
+ )
+
+ @api.onchange('partner_id')
+ def _onchange_partner_id(self):
+ if self.partner_id:
+ data = self._get_tables_from_partner(self.partner_id)
+ names = self._get_table_names(data)
+ tables = self.env['privacy.partner.data']
+ for name in sorted(names):
+ vals = self._get_default_table(
+ name=name,
+ data=[t for t in data if t[0] == name and not t[5]],
+ )
+ if vals:
+ tables |= self.env['privacy.partner.data'].create(vals)
+ self.table_ids = tables
+ else:
+ self.table_ids = self.env['privacy.partner.data']
+ return {
+ 'domain': {
+ 'table_ids': [
+ ('id', 'in', self.table_ids.ids)],
+ },
+ }
+
+ @api.onchange('company_id')
+ def _onchange_company_id(self):
+ if not self.company_id:
+ self.company_id = self.env.user.company_id
+ return {
+ 'domain': {
+ 'partner_id': [
+ ('company_id', 'in', [self.company_id.id, False])],
+ },
+ }
+
+ @api.multi
+ def button_export_xlsx(self):
+ self.ensure_one()
+ return self.check_report(xlsx_report=True)
+
+ def _build_contexts(self, data):
+ result = {}
+ result['partner_id'] = data['form']['partner_id'][0] or False
+ result['company_id'] = data['form']['company_id'][0] or False
+ result['table_ids'] = 'table_ids' in data['form'] and \
+ data['form']['table_ids'] or False
+ return result
+
+ def _clean_data(self, model, rows):
+ cleaned_rows = []
+ for i, row in enumerate(rows):
+ cleaned_rows.append({})
+ for key, value in row.items():
+ label = self.env[model]._fields[key].string or key
+ if self.env[model]._fields[key].store:
+ if 'many2one' == self.env[model]._fields[key].type:
+ comodel = self.env[model]._fields[key].comodel_name
+ if value:
+ record = self.env[comodel].sudo().browse(value)
+ cleaned_rows[i][label] = \
+ record.display_name.encode('utf8')
+ else:
+ cleaned_rows[i][label] = rows[i][key]
+ elif '2many' not in self.env[model]._fields[key].type:
+ cleaned_rows[i][label] = rows[i][key]
+ return cleaned_rows
+
+ @api.multi
+ def check_report(self, xlsx_report=False):
+ self.ensure_one()
+ data = {}
+ data['ids'] = self.env.context.get('active_ids', [])
+ data['model'] = self.env.context.get('active_model', 'ir.ui.menu')
+ data['form'] = self.read(['partner_id', 'company_id', 'table_ids'])[0]
+ used_context = self._build_contexts(data)
+ data['form']['used_context'] = dict(
+ used_context, lang=self.env.context.get('lang', 'en_US'))
+ return self._print_report(data=data, xlsx_report=xlsx_report)
+
+ @api.multi
+ def compute_data_for_report(self, data):
+ if not data.get('form'):
+ raise UserError(
+ _("Form content is missing, this report cannot be printed."))
+ partner = data['form'].get('partner_id', False)
+ if not partner:
+ raise UserError(
+ _("No provided partner."))
+ partner = self.env['res.partner'].sudo().browse(partner[0])
+ tables = data['form'].get('table_ids', False)
+ if tables:
+ tables = self.env['privacy.partner.data'].browse(tables)
+ tables = self._get_rows_from_tables(tables, partner)
+ data.update({'tables': tables, })
+ return data
+
+ def _exclude_column(self, model, column):
+
+ # https://github.com/odoo/odoo/issues/24927
+ if model in ('mail.compose.message', 'survey.mail.compose.message'):
+ if column in ('needaction_partner_ids', 'starred_partner_ids'):
+ return True
+ # feel free to add more specific cases meanwhile the issue is not fixed
+
+ return False
+
+ def _get_default_table(self, name, data):
+ if data:
+ data_type = data[0][4]
+ res = self.env[data[0][1]]
+ for t in data:
+ res |= self.env[t[1]].sudo().browse(t[3])
+ if res:
+ values = {
+ 'name': name,
+ 'model_id': self.env['ir.model'].sudo().search(
+ [('model', '=', res._name)]).id,
+ 'count_rows': len(res.ids),
+ 'type': data_type,
+ }
+ return values
+ return {}
+
+ def _get_model_from_table(self, table, partner):
+ new_tables = {}
+ for model in table.model_id:
+ rows = self._get_rows_from_model(model, partner)
+ new_tables[model.display_name.encode('utf8')] = rows
+ return new_tables
+
+ def _get_rows_from_model(self, model, partner):
+ lines = self.env[model.model]
+ columns = [k for k, v in self.env[model.model]._fields.items()
+ if v.comodel_name == 'res.partner' and
+ v.store and not self._exclude_column(model.model, k)]
+ for column in columns:
+ lines |= self.env[model.model].sudo().search(
+ [(column, '=', partner.id)])
+ rows = lines.sudo().read(load=False)
+ rows = self._clean_data(model.model, rows)
+ return rows
+
+ def _get_rows_from_tables(self, tables, partner):
+ new_tables = {}
+ for table in tables:
+ data_table = self._get_model_from_table(table, partner)
+ new_tables[str(table.name)] = data_table
+ return new_tables
+
+ def _get_table_names(self, data):
+ names = []
+ for t in data:
+ if t[3] and not t[5] and t[0] not in names:
+ names.append(t[0])
+ return names
+
+ def _get_tables_from_partner(self, partner):
+ tables = [t[0] for t in [
+ [[self.env[m]._table, m, k, self.env[m].sudo().search(
+ [(k, '=', partner.id)]).ids, v.type, self.env[m]._transient]
+ for k, v in self.env[m]._fields.items()
+ if v.comodel_name == 'res.partner' and self.env[m]._auto and
+ v.store and not self._exclude_column(m, k)]
+ for m in [x for x in self.env.registry.keys()]] if t]
+ for i, t in enumerate(tables):
+ if t[4] == 'many2many':
+ if t[3]:
+ relation = self.env[t[1]]._fields[t[2]].relation
+ if relation:
+ tables[i][0] = relation
+ return tables
+
+ def _print_report(self, data, xlsx_report=False):
+ records = self.env[data['model']].sudo().browse(data.get('ids', []))
+ processed_data = self.compute_data_for_report(data)
+ if xlsx_report:
+ return self.env['report'].with_context(landscape=True).get_action(
+ records=records, report_name='privacy.report_partner_xlsx',
+ data=processed_data)
+
+
+class PrivacyPartnerData(models.TransientModel):
+ _name = "privacy.partner.data"
+ _description = "Privacy Partner Data"
+
+ name = fields.Char(
+ string='Database Table',
+ )
+ model_id = fields.Many2one(
+ comodel_name='ir.model',
+ ondelete='cascade',
+ string='Models',
+ )
+ type = fields.Char(
+ string="Type",
+ )
+ count_rows = fields.Integer(
+ default=0,
+ string='Number of lines',
+ )
diff --git a/privacy_partner_report/wizard/privacy_report_partner_wizard.xml b/privacy_partner_report/wizard/privacy_report_partner_wizard.xml
new file mode 100644
index 0000000..bc75960
--- /dev/null
+++ b/privacy_partner_report/wizard/privacy_report_partner_wizard.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+ Partner Report
+ privacy.partner.report
+
+
+
+
+
+
+
+