Práctica: Módulo viajes. Ejercicio 1
Un viaje puede tener varias paradas y cada parada pertenece a un único viaje. Antes de mirar la solución, crea el modelo, su relación inversa, vistas de formulario/lista/búsqueda/calendario y una entrada de menú.
1. Modelo y mapa seguro #
Fichero models/parada.py:
1from urllib.parse import urlencode 2from markupsafe import Markup 3from odoo import api, fields, models 4 5 6class Parada(models.Model): 7 _name = 'viajes.parada' 8 _description = 'Parada de viaje' 9 _rec_name = 'titulo' 10 11 titulo = fields.Char(required=True) 12 descripcion = fields.Text() 13 fecha = fields.Date(default=fields.Date.context_today) 14 duracion = fields.Float(help='Duración en horas') 15 localizacion = fields.Char() 16 viaje_id = fields.Many2one('viajes.viaje', required=True, ondelete='cascade') 17 completada = fields.Boolean(default=False) 18 mapa_embed = fields.Html(compute='_compute_mapa_embed', sanitize=False) 19 20 def marcar_completada(self): 21 for record in self: 22 record.completada = not record.completada 23 24 @api.depends('localizacion') 25 def _compute_mapa_embed(self): 26 for record in self: 27 if not record.localizacion: 28 record.mapa_embed = '<p>Ubicación no disponible</p>' 29 continue 30 query = urlencode({'q': record.localizacion, 'output': 'embed'}) 31 url = 'https://www.google.com/maps?' + query 32 record.mapa_embed = Markup( 33 '<iframe title="Mapa de la parada" width="100%%" height="300" ' 34 'loading="lazy" referrerpolicy="no-referrer" src="%s"></iframe>' 35 ) % url
El botón es un método de acción, no un método computado: no lleva @api.depends. La duración de una parada son horas, por eso aquí sí usamos Float; la duración del viaje continúa en días enteros.
El mapa conserva sanitize=False únicamente porque el HTML lo genera una plantilla fija controlada por el desarrollador. El host es constante, urlencode codifica la localización y Markup escapa el valor interpolado. No marques entradas de usuario como Markup ni admitas HTML o URLs arbitrarias. El campo es de solo lectura; incluir Google Maps supone enviar esa localización al proveedor cuando se visualiza.
2. Relación inversa e importaciones #
Fichero models/viaje_paradas.py:
1from odoo import fields, models 2 3 4class ViajeParadas(models.Model): 5 _inherit = 'viajes.viaje' 6 7 paradas_ids = fields.One2many('viajes.parada', 'viaje_id')
Añade from . import parada y from . import viaje_paradas a models/__init__.py.
3. Vistas y navegación #
Fichero views/parada.xml:
1<odoo> 2<record id="parada_form_view" model="ir.ui.view"> 3 <field name="name">parada.form</field> 4 <field name="model">viajes.parada</field> 5 <field name="arch" type="xml"> 6 <form><sheet> 7 <group> 8 <field name="viaje_id"/><field name="titulo"/><field name="fecha"/> 9 <field name="duracion"/><field name="localizacion"/><field name="completada"/> 10 <button name="marcar_completada" type="object" string="Cambiar completada"/> 11 </group> 12 <field name="descripcion"/> 13 <field name="mapa_embed" widget="html" readonly="True"/> 14 </sheet></form> 15 </field> 16 </record> 17 <record id="parada_list_view" model="ir.ui.view"> 18 <field name="name">parada.list</field> 19 <field name="model">viajes.parada</field> 20 <field name="arch" type="xml"> 21 <list><field name="titulo"/><field name="fecha"/><field name="viaje_id"/></list> 22 </field> 23 </record> 24 <record id="parada_search_view" model="ir.ui.view"> 25 <field name="name">parada.search</field> 26 <field name="model">viajes.parada</field> 27 <field name="arch" type="xml"> 28 <search><field name="titulo"/><field name="localizacion"/></search> 29 </field> 30 </record> 31 <record id="parada_calendar_view" model="ir.ui.view"> 32 <field name="name">parada.calendar</field> 33 <field name="model">viajes.parada</field> 34 <field name="arch" type="xml"> 35 <calendar date_start="fecha"><field name="titulo"/></calendar> 36 </field> 37 </record> 38 <record id="parada_list_action" model="ir.actions.act_window"> 39 <field name="name">Paradas</field> 40 <field name="res_model">viajes.parada</field> 41 <field name="view_mode">list,form,calendar</field> 42 <field name="search_view_id" ref="parada_search_view"/> 43 </record> 44 <record id="viaje_paradas_form" model="ir.ui.view"> 45 <field name="name">viaje.paradas.form</field> 46 <field name="model">viajes.viaje</field> 47 <field name="inherit_id" ref="viaje_relaciones_form"/> 48 <field name="arch" type="xml"> 49 <xpath expr="//notebook[@name='detalles']" position="inside"> 50 <page string="Paradas"><field name="paradas_ids"> 51 <list><field name="titulo"/><field name="fecha"/><field name="localizacion"/></list> 52 </field></page> 53 </xpath> 54 </field> 55 </record> 56</odoo>
Añade views/parada.xml a data, después de relaciones y antes de menús. Añade esta fila al CSV:
1access_viajes_parada,Manage parada,viajes.parada,base.group_user,crud,
Y dentro de <odoo> en menús:
1<menuitem id="parada_menu" name="Paradas" parent="viajes_menu" action="parada_list_action"/>
Actualiza el módulo. El formulario solo utiliza campos definidos hasta esta lección: todavía no existe fecha_fin, ni los indicadores de la parte 5. Crea paradas desde el viaje, verifica su calendario y pulsa dos veces el botón para comprobar el cambio de estado.
Prueba una localización vacía, una dirección con & y comillas y una cadena con etiquetas HTML: deben viajar como texto codificado en el parámetro, sin generar elementos adicionales. Al borrar un viaje se borran sus paradas por cascade; el vehículo conserva la política restrict para sus viajes.