Odoo · RGPD · Portal
En proyectos con datos de menores o salud, mezclar titular del servicio (res.partner facturable) con el perfil evaluado es un error de diseño y de cumplimiento. Odoo no trae RGPD «de serie»: hay que modelarlo con separación de entidades, permisos granulares y trazabilidad.
Modelo
Partner vs sujeto evaluado
class DiagnosticTestSubject(models.Model):
_name = 'diagnostic.test.subject'
_description = 'Diagnostic Test Subject'
name = fields.Char(required=True)
birth_date = fields.Date()
partner_id = fields.Many2one('res.partner') # Tutor / pagador
class DiagnosticTest(models.Model):
_name = 'diagnostic.test'
_inherit = ['mail.thread']
partner_id = fields.Many2one('res.partner', string='Customer')
subject_id = fields.Many2one(
'diagnostic.test.subject',
string='Candidate',
required=True,
)
El cliente portal ve sus pedidos vía partner_id; los datos del menor viven en un modelo con ACL más restrictivos y campos con groups=.
Minimización de datos: no copies nombre/fecha de nacimiento del menor a
res.partner«por comodidad». Duplicar es duplicar riesgo.
Permisos
Tres roles, tres dominios
| Rol | Ve | Record rule típica |
|---|---|---|
| Cliente (portal) | Sus diagnósticos e informes entregados | ('partner_id', '=', user.partner_id.id) |
| Profesional | Diagnósticos asignados | ('professional_id', '=', user.id) |
| Admin | Todo (auditoría) | Sin regla restrictiva + grupo system |
<record id="rule_subject_professional" model="ir.rule">
<field name="name">Subject: assigned professional only</field>
<field name="model_id" ref="model_diagnostic_test_subject"/>
<field name="domain_force">[
('test_ids.professional_id', '=', user.id)
]</field>
<field name="groups" eval="[(4, ref('group_diagnostic_professional'))]"/>
</record>
Campos sensibles
groups= y chatter selectivo
clinical_notes = fields.Text(
string='Clinical Notes',
groups='diagnostic_tests.group_diagnostic_professional',
)
report_signed = fields.Binary(
groups='diagnostic_tests.group_diagnostic_user,'
'diagnostic_tests.group_diagnostic_professional',
)
Los campos con groups= desaparecen de vistas y RPC para quien no pertenece al grupo. Complementa — no sustituye — record rules.
mail.thread en registros con PII puede filtrar datos en notificaciones email. Valora mixins solo donde aporte valor y configura subtipos restrictivos.
Portal
Controladores portal sin fugas
@http.route('/my/diagnostics', type='http', auth='user', website=True)
def portal_diagnostics(self, **kw):
tests = request.env['diagnostic.test'].search([
('partner_id', '=', request.env.user.partner_id.id),
])
return request.render('diagnostic_tests.portal_list', {'tests': tests})
@http.route('/my/diagnostic/report/<int:report_id>', auth='user')
def download_report(self, report_id):
report = request.env['diagnostic.test.report'].browse(report_id)
if report.diagnostic_id.partner_id != request.env.user.partner_id:
raise NotFound()
if not report.is_signed:
raise NotFound()
return self._stream_pdf(report)
Trazabilidad
Tracking, retención e inmutabilidad
- Estados con
tracking=True— quién movió el diagnóstico y cuándo. - Informes firmados inmutables — bloquear
writetras firma digital. - Derecho de supresión — procedimiento documentado (anonimizar vs borrar cascada).
- Logs — accesos a descarga de informes en
ir.loggingo modelo de auditoría.
def write(self, vals):
signed = self.filtered('is_signed')
if signed:
raise UserError('Signed reports cannot be modified.')
return super().write(vals)
Resumen
Separa partner comercial y perfil sensible, define record rules por rol, restringe campos clínicos con groups= y valida ownership en rutas portal. RGPD en Odoo es arquitectura + permisos + procesos, no un checkbox legal.