Initial commit: OCA Technical packages (595 packages)

This commit is contained in:
Ernad Husremovic 2025-08-29 15:43:03 +02:00
commit 2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions

View file

@ -0,0 +1,5 @@
from . import mail_mail
from . import mail_template
from . import privacy_activity
from . import privacy_consent
from . import res_partner

View file

@ -0,0 +1,64 @@
# Copyright 2018 Tecnativa - Jairo Llopis
# Copyright 2022 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models
class MailMail(models.Model):
_inherit = "mail.mail"
def _postprocess_sent_message(
self, success_pids, failure_reason=False, failure_type=None
):
"""Write consent status after sending message."""
# Know if mail was successfully sent to a privacy consent
res_ids = []
for mail in self:
if (
mail.mail_message_id.model == "privacy.consent"
and mail.state == "sent"
and success_pids
and not failure_reason
and not failure_type
):
res_ids.append(mail.mail_message_id.res_id)
if res_ids:
consents = self.env["privacy.consent"].search(
[
("id", "in", res_ids),
("state", "=", "draft"),
("partner_id", "in", [par.id for par in success_pids]),
]
)
consents.write({"state": "sent"})
return super()._postprocess_sent_message(
success_pids=success_pids,
failure_reason=failure_reason,
failure_type=failure_type,
)
def _send_prepare_body(self):
"""Replace privacy consent magic links.
This replacement is done here instead of directly writing it into
the ``mail.template`` to avoid writing the tokeinzed URL
in the mail thread for the ``privacy.consent`` record,
which would enable any reader of such thread to impersonate the
subject and choose in its behalf.
"""
result = super()._send_prepare_body()
# Avoid polluting other model mails
if self.model != "privacy.consent":
return result
# Tokenize consent links
consent = self.env["privacy.consent"].browse(self.mail_message_id.res_id)
result = result.replace(
"/privacy/consent/accept/",
consent._url(True),
)
result = result.replace(
"/privacy/consent/reject/",
consent._url(False),
)
return result

View file

@ -0,0 +1,35 @@
# Copyright 2018 Tecnativa - Jairo Llopis
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from lxml import html
from odoo import _, api, models
from odoo.exceptions import ValidationError
class MailTemplate(models.Model):
_inherit = "mail.template"
@api.constrains("body_html", "model")
def _check_consent_links_in_body_html(self):
"""Body for ``privacy.consent`` templates needs placeholder links."""
links = [
"//a[@href='/privacy/consent/{}/']".format(action)
for action in ("accept", "reject")
]
for one in self:
if one.model != "privacy.consent":
continue
doc = html.document_fromstring(one.body_html)
for link in links:
if not doc.xpath(link):
raise ValidationError(
_(
"Missing privacy consent link placeholders. "
"You need at least these two links:\n"
'<a href="%(consent_url)s">Accept</a>\n'
'<a href="%(reject_url)s">Reject</a>',
consent_url="/privacy/consent/accept/",
reject_url="/privacy/consent/reject/",
)
)

View file

@ -0,0 +1,145 @@
# Copyright 2018 Tecnativa - Jairo Llopis
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.safe_eval import safe_eval
class PrivacyActivity(models.Model):
_inherit = "privacy.activity"
server_action_id = fields.Many2one(
comodel_name="ir.actions.server",
string="Server action",
domain=[("model_id.model", "=", "privacy.consent")],
help="Run this action when a new consent request is created or its "
"acceptance status is updated.",
)
consent_ids = fields.One2many(
comodel_name="privacy.consent",
inverse_name="activity_id",
string="Consents",
)
consent_count = fields.Integer(
string="Consents count",
compute="_compute_consent_count",
)
consent_required = fields.Selection(
selection=[("auto", "Automatically"), ("manual", "Manually")],
string="Ask subjects for consent",
help="Enable if you need to track any kind of consent "
"from the affected subjects",
)
consent_template_id = fields.Many2one(
comodel_name="mail.template",
string="Email template",
default=lambda self: self._default_consent_template_id(),
domain=[("model", "=", "privacy.consent")],
help="Email to be sent to subjects to ask for consent. "
"A good template should include details about the current "
"consent request status, how to change it, and where to "
"get more information.",
)
default_consent = fields.Boolean(
string="Accepted by default",
help="Should we assume the subject has accepted if we receive no " "response?",
)
# Hidden helpers help user design new templates
consent_template_default_body_html = fields.Text(
compute="_compute_consent_template_defaults",
)
consent_template_default_subject = fields.Char(
compute="_compute_consent_template_defaults",
)
@api.model
def _default_consent_template_id(self):
return self.env.ref("privacy_consent.template_consent", False)
@api.depends("consent_ids")
def _compute_consent_count(self):
self.consent_count = 0
groups = self.env["privacy.consent"].read_group(
[("activity_id", "in", self.ids)],
["activity_id"],
["activity_id"],
)
for group in groups:
self.browse(group["activity_id"][0]).consent_count = group[
"activity_id_count"
]
def _compute_consent_template_defaults(self):
"""Used in context values, to help users design new templates."""
template = self._default_consent_template_id()
if template:
self.update(
{
"consent_template_default_body_html": template.body_html,
"consent_template_default_subject": template.subject,
}
)
@api.constrains("consent_required", "consent_template_id")
def _check_auto_consent_has_template(self):
"""Require a mail template to automate consent requests."""
for one in self:
if one.consent_required == "auto" and not one.consent_template_id:
raise ValidationError(
_("Specify a mail template to ask automated consent.")
)
@api.constrains("consent_required", "subject_find")
def _check_consent_required_subject_find(self):
for one in self:
if one.consent_required and not one.subject_find:
raise ValidationError(
_(
"Require consent is available only for subjects "
"in current database."
)
)
@api.model
def _cron_new_consents(self):
"""Ask all missing automatic consent requests."""
automatic = self.search([("consent_required", "=", "auto")])
automatic.action_new_consents()
@api.onchange("consent_required")
def _onchange_consent_required_subject_find(self):
"""Find subjects automatically if we require their consent."""
if self.consent_required:
self.subject_find = True
def action_new_consents(self):
"""Generate new consent requests."""
consents_vals = []
# Skip activitys where consent is not required
for one in self.with_context(active_test=False).filtered("consent_required"):
domain = [
("id", "not in", one.mapped("consent_ids.partner_id").ids),
("email", "!=", False),
] + safe_eval(one.subject_domain)
# Store values for creating missing consent requests
for missing in self.env["res.partner"].search(domain):
consents_vals.append(
{
"partner_id": missing.id,
"accepted": one.default_consent,
"activity_id": one.id,
}
)
# Create and send consent request emails for automatic activitys
consents = self.env["privacy.consent"].create(consents_vals)
consents.action_auto_ask()
# Redirect user to new consent requests generated
return {
"domain": [("id", "in", consents.ids)],
"name": _("Generated consents"),
"res_model": consents._name,
"type": "ir.actions.act_window",
"view_mode": "tree,form",
}

View file

@ -0,0 +1,186 @@
# Copyright 2018 Tecnativa - Jairo Llopis
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
import hashlib
import hmac
from odoo import api, fields, models
class PrivacyConsent(models.Model):
_name = "privacy.consent"
_description = "Consent of data processing"
_inherit = "mail.thread"
_rec_name = "partner_id"
_sql_constraints = [
(
"unique_partner_activity",
"UNIQUE(partner_id, activity_id)",
"Duplicated partner in this data processing activity",
),
]
active = fields.Boolean(
default=True,
index=True,
)
accepted = fields.Boolean(
tracking=True,
help="Indicates current acceptance status, which can come from "
"subject's last answer, or from the default specified in the "
"related data processing activity.",
)
last_metadata = fields.Text(
readonly=True,
tracking=True,
help="Metadata from the last acceptance or rejection by the subject",
)
partner_id = fields.Many2one(
comodel_name="res.partner",
string="Subject",
required=True,
readonly=True,
tracking=True,
help="Subject asked for consent.",
)
activity_id = fields.Many2one(
comodel_name="privacy.activity",
string="Activity",
readonly=True,
required=True,
tracking=True,
)
state = fields.Selection(
selection=[
("draft", "Draft"),
("sent", "Awaiting response"),
("answered", "Answered"),
],
default="draft",
readonly=True,
required=True,
tracking=True,
)
def _creation_subtype(self):
return self.env.ref("privacy_consent.mt_consent_consent_new")
def _track_subtype(self, init_values):
"""Return specific subtypes."""
self.ensure_one()
if self.env.context.get("subject_answering"):
return self.env.ref("privacy_consent.mt_consent_acceptance_changed")
if "state" in init_values:
return self.env.ref("privacy_consent.mt_consent_state_changed")
return super()._track_subtype(init_values)
def _token(self):
"""Secret token to publicly authenticate this record."""
secret = self.env["ir.config_parameter"].sudo().get_param("database.secret")
params = "{}-{}-{}-{}".format(
self.env.cr.dbname,
self.id,
self.partner_id.id,
self.activity_id.id,
)
return hmac.new(
secret.encode("utf-8"),
params.encode("utf-8"),
hashlib.sha512,
).hexdigest()
def _url(self, accept):
"""Tokenized URL to let subject decide consent.
:param bool accept:
Indicates if you want the acceptance URL, or the rejection one.
"""
return "/privacy/consent/{}/{}/{}?db={}".format(
"accept" if accept else "reject",
self.id,
self._token(),
self.env.cr.dbname,
)
def _send_consent_notification(self):
"""Send email notification to subject."""
for one in self.with_context(
tpl_force_default_to=True,
mail_notify_user_signature=False,
mail_auto_subscribe_no_notify=True,
):
one.activity_id.consent_template_id.send_mail(one.id)
def _run_action(self):
"""Execute server action defined in data processing activity."""
for one in self.filtered(
lambda x: x.state != "draft" and x.activity_id.server_action_id
):
action = one.activity_id.server_action_id.with_context(
active_id=one.id,
active_ids=one.ids,
active_model=one._name,
)
action.run()
@api.model_create_multi
def create(self, vals_list):
"""Run server action on create."""
results = super().create(vals_list)
# Sync the default acceptance status
results._run_action()
return results
def write(self, vals):
"""Run server action on update."""
result = super().write(vals)
self._run_action()
return result
def message_get_suggested_recipients(self):
result = super().message_get_suggested_recipients()
reason = self._fields["partner_id"].string
for one in self:
one._message_add_suggested_recipient(
result,
partner=one.partner_id,
reason=reason,
)
return result
def action_manual_ask(self):
"""Let user manually ask for consent."""
return {
"context": {
"default_composition_mode": "comment",
"default_model": self._name,
"default_res_id": self.id,
"default_template_id": self.activity_id.consent_template_id.id,
"default_use_template": True,
"tpl_force_default_to": True,
},
"force_email": True,
"res_model": "mail.compose.message",
"target": "new",
"type": "ir.actions.act_window",
"view_mode": "form",
}
def action_auto_ask(self):
"""Automatically ask for consent."""
templated = self.filtered("activity_id.consent_template_id")
automated = templated.filtered(
lambda one: one.activity_id.consent_required == "auto"
)
automated._send_consent_notification()
def action_answer(self, answer, metadata=False):
"""Process answer.
:param bool answer:
Did the subject accept?
:param str metadata:
Metadata from last user acceptance or rejection request.
"""
self.write({"state": "answered", "accepted": answer, "last_metadata": metadata})

View file

@ -0,0 +1,33 @@
# Copyright 2018 Tecnativa - Jairo Llopis
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class ResPartner(models.Model):
_inherit = "res.partner"
privacy_consent_ids = fields.One2many(
comodel_name="privacy.consent",
inverse_name="partner_id",
string="Privacy consents",
)
privacy_consent_count = fields.Integer(
string="Consents",
compute="_compute_privacy_consent_count",
help="Privacy consent requests amount",
)
@api.depends("privacy_consent_ids")
def _compute_privacy_consent_count(self):
"""Count consent requests."""
self.privacy_consent_count = 0
groups = self.env["privacy.consent"].read_group(
[("partner_id", "in", self.ids)],
["partner_id"],
["partner_id"],
)
for group in groups:
self.browse(group["partner_id"][0]).privacy_consent_count = group[
"partner_id_count"
]