Odoo · RGPD · Portal
Muchos servicios distinguen entre quien contrata y paga (el cliente, un res.partner facturable) y la persona sobre la que se guardan datos sensibles: un paciente, un empleado, un beneficiario. Mezclar las dos cosas en el mismo registro 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
Cliente frente a perfil sensible
class ServiceProfile(models.Model):
_name = 'service.profile'
_description = 'Service Profile'
name = fields.Char(required=True)
birth_date = fields.Date()
partner_id = fields.Many2one('res.partner') # Titular / pagador
class ServiceRequest(models.Model):
_name = 'service.request'
_inherit = ['mail.thread']
partner_id = fields.Many2one('res.partner', string='Customer')
profile_id = fields.Many2one(
'service.profile',
string='Profile',
required=True,
)
El cliente del portal ve sus solicitudes vía partner_id; los datos personales del perfil viven en un modelo con ACL más restrictivos y campos con groups=.
Minimización de datos: no copies nombre o fecha de nacimiento del perfil a
res.partner«por comodidad». Duplicar es duplicar riesgo.
Permisos
Tres roles, tres dominios
| Rol | Ve | Record rule típica |
|---|---|---|
| Cliente (portal) | Sus solicitudes y documentos entregados | ('partner_id', '=', user.partner_id.id) |
| Técnico | Solicitudes asignadas | ('technician_id', '=', user.id) |
| Admin | Todo (auditoría) | Sin regla restrictiva + grupo system |
<record id="rule_profile_technician" model="ir.rule">
<field name="name">Profile: assigned technician only</field>
<field name="model_id" ref="model_service_profile"/>
<field name="domain_force">[
('request_ids.technician_id', '=', user.id)
]</field>
<field name="groups" eval="[(4, ref('group_service_technician'))]"/>
</record>
Campos sensibles
groups= y chatter selectivo
private_notes = fields.Text(
string='Private Notes',
groups='my_module.group_service_technician',
)
signed_document = fields.Binary(
groups='my_module.group_service_user,'
'my_module.group_service_technician',
)
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 datos personales puede filtrarlos en notificaciones por email. Valora los mixins solo donde aporten valor y configura subtipos restrictivos.
Portal
Controladores portal sin fugas
@http.route('/my/requests', type='http', auth='user', website=True)
def portal_requests(self, **kw):
records = request.env['service.request'].search([
('partner_id', '=', request.env.user.partner_id.id),
])
return request.render('my_module.portal_request_list', {'records': records})
@http.route('/my/requests/document/<int:document_id>', auth='user')
def download_document(self, document_id):
document = request.env['service.document'].browse(document_id)
if document.request_id.partner_id != request.env.user.partner_id:
raise NotFound()
if not document.is_signed:
raise NotFound()
return self._stream_pdf(document)
Trazabilidad
Tracking, retención e inmutabilidad
- Estados con
tracking=True— quién movió la solicitud y cuándo. - Documentos firmados inmutables — bloquear
writetras la firma. - Derecho de supresión — procedimiento documentado (anonimizar frente a borrar en cascada).
- Logs — accesos a descargas en
ir.loggingo en un modelo de auditoría.
def write(self, vals):
signed = self.filtered('is_signed')
if signed:
raise UserError('Signed documents cannot be modified.')
return super().write(vals)
Resumen
Separa el partner comercial del perfil sensible, define record rules por rol, restringe los campos privados con groups= y valida la propiedad en las rutas del portal. RGPD en Odoo es arquitectura + permisos + procesos, no un checkbox legal.