Wizards en Odoo

De Jose Castillo Aliaga
Ir a la navegación Ir a la búsqueda

Els wizards d'Odoo permeten fer un asistent interactiu per a que l'usuari complete una tasca. Com que no ha d'agafar les dades dirèctament en un formulari, si no que va ajundant-lo a completar-lo, no pot ser guardat en la base de dades fins al final.

Aquest article forma part del curs d'Odoo, el qual té com a pàgina principal e inicial la de Odoo

Els wizards en Odoo són models que estenen la classe TransientModel en compte de Model. Aquesta classe és molt pareguda, però:

  • Les dades no són persistents, encara que es guarden temporalment en la base de dades.
  • A partir de odoo 14 necessiten permisos
  • Els records dels wizards poden tindre referències Many2one amb el records dels models normals, però no al contrari.

En realitat, no estem fent res


Wizard bàsic

A continuació anem a veure un exemple de wizard que sols mostra un formulari i crea una instºancia d'un model a partir de les dades del formulari:

class wizard(models.TransientModel):
     _name = 'mmog.wizard'
     def _default_attacker(self):
         return self.env['mmog.fortress'].browse(self._context.get('active_id')) # El context conté, entre altre coses, 
                                                                                 #el active_id del model que està obert.
     fortress_attacker = fields.Many2one('mmog.fortress',default=_default_attacker)
     fortress_target = fields.Many2one('mmog.fortress')
     soldiers_sent = fields.Integer(default=1)

     @api.multi
     def launch(self):
       if self.fortress_attacker.soldiers >= self.soldiers_sent:
          self.env['mmog.attack'].create({'fortress_attacking':self.fortress_attacker.id,
                                          'fortress_defender':self.fortress_target.id,
                                          'data':fields.datetime.now(),'soldiers_sent':self.soldiers_sent})
       return {}

En el python cal observar la classe de la que hereta, el default, que extrau el active_id del form que a llançat el wizard i el mètode que és cridat pel botó de la vista.

        <record model="ir.ui.view" id="wizard_mmog_fortress_view">
            <field name="name">wizard.mmog.fortress</field>
            <field name="model">mmog.wizard</field>
            <field name="arch" type="xml">
                <form string="Select fortress">
                    <group>
                        <field name="fortress_attacker"/>
                        <field name="fortress_target"/>
                        <field name="soldiers_sent"/>
                    </group>
                    <footer>
                        <button name="launch" type="object"
                                string="Launch" class="oe_highlight"/>
                        or
                        <button special="cancel" string="Cancel"/>
                    </footer>

                </form>
            </field>
        </record>

        <act_window id="launch_mmog_fortress_wizard"
                    name="Launch attack"
                    binding_model="mmog.fortress"
                    res_model="mmog.wizard"
                    view_mode="form"
                    target="new"
                    />

     <!--  Forma alternativa recomanada per el manual d'odoo a partir de la versió 14

          <record id="launch_mmog_fortress_wizard" model="ir.actions.act_window">
            <field name="name">Launch attack</field>
            <field name="res_model">mmog.wizard</field>
            <field name="view_mode">form</field>
            <field name="target">new</field>
            <field name="binding_model_id" ref="model_mmog_fortress"/>
         </record>  -->

En la vista, tenim creat un form normal amb dos botons. Un d'ells és especial per a cancel·lar el wizard. L'altre crida al mètode. També s'ha creat un action indicant el src_model sobre el que treballa i el model del wizard que utilitza. Els action que criden a wizard tenen l'atribut target a new per a que llance una finestra emergent.

L'etiqueta act_window és un alies de record model ir.actions.act_window on es poden posar tots els atributs de l'acció no com a fields sino com a atributs de l'etiqueta.
binding_model és el model on es pot llançar el wizard. Amb això només ja apareix en el menú superior d'accions. Però podem fer un botó que el cride de forma més intuïtiva. En alguns tutorials el trobareu com res_model, però és per a versions anteriors a la 13.
 <button name="%(launch_mmog_fortress_wizard)d" type="action" string="Launch attack" class="oe_highlight" />

Si volem, podem ficar un botó que cride al action del wizard. Observem la sintaxi del name, que és igual sempre que el button siga de tipus action, ja que és l'anomenat XML id.


Wizard amb assistent

En aquest exemple anem a fer un wizard amb assistent. Per començar, cal crear un camp state amb varis valors possibles:

      state = fields.Selection([
        ('pelis', "Movie Selection"),
        ('dia', "Day Selection"),                                                                        
      ], default='pelis')
                    
      @api.multi      
      def action_pelis(self):
        self.state = 'pelis'
        return {
            'type': 'ir.actions.act_window',
            'res_model': self._name,
            'res_id': self.id,
            'view_mode': 'form',
            'target': 'new',
        }

                                                      
                                
      @api.multi                        
      def action_dia(self):              
        self.state = 'dia'                      
        return {
            'type': 'ir.actions.act_window',
            'res_model': self._name,
            'res_id': self.id,
            'view_mode': 'form',
            'target': 'new',
        }

I uns botons que van fent que passe d'un estar a un altre:

                    <header>
                        <button name="action_pelis" type="object"
                                string="Reset to movie selection"
                                states="dia"/>
                        <button name="action_dia" type="object"
                                string="Select dia" states="pelis"
                                class="oe_highlight"/>
                        <field name="state" widget="statusbar"/>
                    </header>

                    <group states="pelis">
                        <field name="cine"/>
                        <field name="pelicules"/>
                    </group>
                    <group states="dia">
                        <field name="dia"/>
                    </group>

Després es pot fer que el formulari tinga un aspecte diferent depèn del valor de state

Els wizards poden tornar a recarregar la vista des de la que són cridats:

return {
    'name': 'Reserves',
    'view_type': 'form',
    'view_mode': 'form',   # Pot ser form, tree, kanban...
    'res_model': 'wizards.reserves', # El model de destí
    'res_id': reserva.id,       # El id concret per obrir el form
   # 'view_id': self.ref('wizards.reserves_form') # Opcional si hi ha més d'una vista posible.
    'context': self._context,   # El context es pot ampliar per afegir opcions
    'type': 'ir.actions.act_window',
    'target': 'current',  # Si ho fem en current, canvia la finestra actual.
}

L'exemple anterior és la manera llarga i completa de cridar a una vista en concret, però si sols necessitem refrescar la vista cridada, podem afegir:

return {
    'type': 'ir.actions.client',
    'tag': 'reload',
}

Exemple Complet de Wizards

El codi complet de l'exemple està a: [1]

Wizard cridat en un botó del formulari:

Els wizards, generalment, necessiten una vista i un action que la cride:

      <record model="ir.ui.view" id="wizards.w_reserves">
            <field name="name">wizard reserves</field>
            <field name="model">wizards.w_reserves</field>
            <field name="arch" type="xml">
		    <form>
	             <header>
                        <field name="state" widget="statusbar"/>
                    </header>
                    <group>
			    <h4>   <field name="teatre"/></h4>
		    </group>
		    <group states="obra">
			    <field name="obra"/>
		    </group>
		    <group states = "actuacio">
			    <field name="actuacio"/>
		    </group>
		    <group states="butaca,fin">
                        <field name="butaca"/>
 
                    </group>
                    <footer>
			    <button states="fin" name="reserva" type="object"
					    string="Reserva" class="oe_highlight"/>
                        or
                        <button special="cancel" string="Cancel"/>
                    </footer>
 
                </form>
            </field>
        </record>
 
        <act_window id="wizards.w_reserves_action"
                    name="Crear reserves"
                    src_model="wizards.teatres"
                    res_model="wizards.w_reserves"
                    view_mode="form"
                    target="new"
                    />

Observem, en especial, el header amb el camp state i els groups amd states per ser mostrats condicionalment. Això permetrà crear un assistent.

Per a que funcione la vista, és necessari el model i el codi del controlador del wizard:

class w_reserves(models.TransientModel):   # La classe és transientModel
     _name = 'wizards.w_reserves'
 
     def _default_teatre(self):                    
         return self.env['wizards.teatres'].browse(self._context.get('active_id')) 
         # El context conté, entre altre coses, el active_id del model que està obert.
 
     teatre = fields.Many2one('wizards.teatres',default=_default_teatre)
     obra = fields.Many2one('wizards.obres')
     actuacio = fields.Many2one('wizards.actuacions',required=True)
     butaca = fields.Many2one('wizards.butaques',required=True)
     state = fields.Selection([     # El camp state és per a crear l'assistent.
        ('teatre', "Teatre Selection"),
        ('obra', "Obra Selection"),                                             
        ('actuacio', "Actuacio Selection"),
        ('butaca', "butaca Selection"),
        ('fin', "Fin"),
        ], default='teatre')
 
 
     @api.onchange('teatre')   
     # Tots aquests onchange serveixen per ajudar a 
     # seleccionar les coses a l'usuari amb filtres
     def _oc_teatre(self):
        if len(self.teatre) > 0:
         actuacions = self.env['wizards.actuacions'].search([('teatre','=',self.teatre.id)])
         print(actuacions)
         obres = actuacions.mapped('obra')
         print(obres)
         self.state='obra'    
         # Canviem el state per a donar continuitat a l'assistent.
         return { 'domain': {'obra': [('id', 'in', obres.ids)]},}    
         # Modifiquem el filtre del següent field.

     @api.onchange('obra')
     def _oc_obra(self):
        if len(self.obra) > 0:
          actuacions = self.env['wizards.actuacions'].search([('teatre','=',self.teatre.id),('obra','=',self.obra.id)])

          self.state='actuacio'
          return { 'domain': {'actuacio': [('id', 'in', actuacions.ids)]},}


     @api.onchange('actuacio')
     def _oc_actuacio(self):
        if len(self.actuacio) > 0:
          print('butaques ******************************************')
          butaques = self.env['wizards.butaques'].search([('teatre','=',self.actuacio.teatre.id)])
          b_reservades = self.actuacio.reserves.mapped('butaca')
          print(b_reservades)
          b_disponibles = butaques - b_reservades    
          # Despres d'obtindre totes les butaques li llevem les reservades.
          print(b_disponibles)

          self.state='butaca'
          return { 'domain': {'butaca': [('id', 'in', b_disponibles.ids)]},}

     @api.onchange('butaca')
     def _oc_butaca(self):
        if len(self.butaca) > 0:
            self.state='fin'

     @api.multi
     def reserva(self):
         reserva = self.env['wizards.reserves'].create({
              'actuacio':self.actuacio.id,
               'butaca':self.butaca.id,
               'name':str(self.actuacio.name)+" - "+str(self.butaca.name)
               })
         return {     
         # Aquest return crea un action que, al ser cridat pel client,
         # obri el formulari amb la reserva creada.
    'name': 'Reserves',
    'view_type': 'form',
    'view_mode': 'form',
    'res_model': 'wizards.reserves',
    'res_id': reserva.id,
    'context': self._context,
    'type': 'ir.actions.act_window',
    'target': 'current',
                 }

Ara, al formulari del teatre, li afegim un botó per obrir el wizard:

	<button name="%(wizards.w_reserves_action)d" string="Crear Reserva" type="action"/>

Wizard cridat des del menú dropdown de action (el desplegable de dalt):

En aquest cas, hem de crear un action window però amb un binding_model_id:

 <record id="wizards.w_reserves_pagar_action" model="ir.actions.act_window">
  <field name="name">Pagar varies reserves</field>
  <field name="type">ir.actions.act_window</field>
  <field name="res_model">wizards.w_pagar_reserves</field>
  <field name="view_type">form</field>
  <field name="view_mode">form</field>
  <field name="target">new</field>
  <field name="binding_model_id" ref="wizards.model_wizards_reserves"  />
</record>

El codi python és molt simple, en aquest cas, sols canvia que atén a la variable de context active_ids en compte de active_id:

class w_pagar_reserves(models.TransientModel):
    _name = 'wizards.w_pagar_reserves'
 
    def _default_reserves(self):
         return self.env['wizards.reserves'].browse(self._context.get('active_ids')) # El context conté, entre altre coses, els active_ids dels models que es seleccionen en un tree.
 
    reserves = fields.Many2many('wizards.reserves',default=_default_reserves)

    def pagar(self):
        for r in self.reserves:
            r.write({'pagada':True})

El formulari del wizard també és molt senzill:

        <record model="ir.ui.view" id="wizards.w_pagar_reserves">
            <field name="name">wizard pagar reserves</field>
            <field name="model">wizards.w_pagar_reserves</field>
            <field name="arch" type="xml">
		    <form>
		    <group >
			    <field name="reserves"/>
		    </group>
                    <footer>
			    <button name="pagar" type="object"
		            string="Pagar" class="oe_highlight"/>
                        or
                        <button special="cancel" string="Cancel"/>
                    </footer>
 
                </form>
            </field>
        </record>