Ampliació del mòdul escola (port 8070)

Afegim seqüències, validacions, onchange, constraints, *smart buttons*, vistes i cerques avançades. Manté la solució Many2one del professor mostrant el nom complet.

0) Context

1) Què afegirem

2) Actualitza el manifest

Edita ~/odoo-dev/custom-addons/escola/__manifest__.py i afegeix el fitxer de seqüències i noves vistes/cerques:

{
  "name": "Escola",
  "version": "17.0.2.0.0",
  "category": "Education",
  "summary": "Gestió d'alumnes, mòduls i professors (ampliat)",
  "author": "Tu",
  "depends": ["base"],
  "data": [
    "security/ir.model.access.csv",

    "data/ir_sequence.xml",

    "views/escola_actions.xml",
    "views/escola_menus.xml",

    "views/alumne_views.xml",
    "views/modul_views.xml",
    "views/professor_views.xml",

    "views/search_views.xml"
  ],
  "installable": True,
  "application": True
}

3) Dades: seqüència per a alumnes

Crea ~/odoo-dev/custom-addons/escola/data/ir_sequence.xml:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
  <record id="seq_escola_alumne_ref" model="ir.sequence">
    <field name="name">Escola Alumne Ref</field>
    <field name="code">escola.alumne.ref</field>
    <field name="prefix">ALU/%(year)s/</field>
    <field name="padding">4</field>
    <field name="company_id" eval="False"/>
  </record>
</odoo>

4) Models — ampliacions

Carpeta ~/odoo-dev/custom-addons/escola/models.

4.1 alumne.py (seqüència + validació d’email)

from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
from odoo.tools import email_re

class EscolaAlumne(models.Model):
    _name = "escola.alumne"
    _description = "Alumne"

    name = fields.Char(string="Nom i cognoms", required=True)
    ref = fields.Char(string="Referència", readonly=True, copy=False, index=True)
    email = fields.Char()

    modul_ids = fields.Many2many(
        "escola.modul",
        "escola_alumne_modul_rel",
        "alumne_id", "modul_id",
        string="Mòduls"
    )

    @api.model
    def create(self, vals):
        if not vals.get("ref"):
            seq = self.env["ir.sequence"].next_by_code("escola.alumne.ref")
            vals["ref"] = seq or _("ALU/NOSEQ")
        return super().create(vals)

    @api.constrains("email")
    def _check_email(self):
        for rec in self:
            if rec.email and not email_re.match(rec.email):
                raise ValidationError(_("Email no vàlid: %s") % rec.email)

4.2 modul.py (codi únic + onchange a majúscules)

from odoo import models, fields, api
from odoo.exceptions import ValidationError

class EscolaModul(models.Model):
    _name = "escola.modul"
    _description = "Mòdul formatiu"
    _sql_constraints = [
        ("code_unique", "unique(code)", "El codi del mòdul ha de ser únic.")
    ]

    name = fields.Char(string="Nom del mòdul", required=True)
    code = fields.Char(string="Codi", required=True)
    credits = fields.Integer(string="Crèdits", default=0)
    start_date = fields.Date(string="Data d'inici")

    professor_id = fields.Many2one("escola.professor", string="Professor", ondelete="set null")
    alumne_ids = fields.Many2many(
        "escola.alumne",
        "escola_alumne_modul_rel",
        "modul_id", "alumne_id",
        string="Alumnes"
    )

    @api.onchange("code")
    def _onchange_code_upper(self):
        if self.code:
            self.code = self.code.upper()

4.3 professor.py (actiu + comptador + smart button)

from odoo import models, fields, api

class EscolaProfessor(models.Model):
    _name = "escola.professor"
    _description = "Professor"
    _rec_name = "name"

    first_name = fields.Char(string="Nom", required=True)
    last_name  = fields.Char(string="Cognoms", required=True)
    name = fields.Char(string="Nom complet", compute="_compute_name", store=True)
    email = fields.Char()
    active = fields.Boolean(default=True)

    modules_count = fields.Integer(compute="_compute_modules_count", readonly=True)
    modul_ids = fields.One2many("escola.modul", "professor_id", string="Mòduls")

    @api.depends('first_name', 'last_name')
    def _compute_name(self):
        for rec in self:
            parts = [p for p in [rec.first_name, rec.last_name] if p]
            rec.name = " ".join(parts) if parts else False

    @api.depends("modul_ids")
    def _compute_modules_count(self):
        for rec in self:
            rec.modules_count = len(rec.modul_ids)

    def action_open_moduls(self):
        self.ensure_one()
        return {
            "type": "ir.actions.act_window",
            "name": "Mòduls del professor",
            "res_model": "escola.modul",
            "view_mode": "tree,form",
            "domain": [("professor_id", "=", self.id)],
            "target": "current",
        }

5) Vistes — millores

5.1 alumne_views.xml

<odoo>
  <record id="view_alumne_tree" model="ir.ui.view">
    <field name="name">escola.alumne.tree</field>
    <field name="model">escola.alumne</field>
    <field name="arch" type="xml">
      <tree>
        <field name="ref"/>
        <field name="name"/>
        <field name="email"/>
        <field name="modul_ids" widget="many2many_tags"/>
      </tree>
    </field>
  </record>

  <record id="view_alumne_form" model="ir.ui.view">
    <field name="name">escola.alumne.form</field>
    <field name="model">escola.alumne</field>
    <field name="arch" type="xml">
      <form>
        <sheet>
          <group>
            <field name="ref" readonly="1"/>
            <field name="name"/>
            <field name="email"/>
            <field name="modul_ids"/>
          </group>
        </sheet>
      </form>
    </field>
  </record>

  <record id="view_alumne_kanban" model="ir.ui.view">
    <field name="name">escola.alumne.kanban</field>
    <field name="model">escola.alumne</field>
    <field name="arch" type="xml">
      <kanban default_group_by="modul_ids">
        <templates>
          <t t-name="kanban-box">
            <div class="oe_kanban_global_click">
              <strong><field name="name"/></strong>
              <div><field name="ref"/></div>
              <div><field name="modul_ids" widget="many2many_tags"/></div>
            </div>
          </t>
        </templates>
      </kanban>
    </field>
  </record>
</odoo>

5.2 modul_views.xml

<odoo>
  <record id="view_modul_tree" model="ir.ui.view">
    <field name="name">escola.modul.tree</field>
    <field name="model">escola.modul</field>
    <field name="arch" type="xml">
      <tree>
        <field name="code"/>
        <field name="name"/>
        <field name="credits"/>
        <field name="start_date"/>
        <field name="professor_id"/>
      </tree>
    </field>
  </record>

  <record id="view_modul_form" model="ir.ui.view">
    <field name="name">escola.modul.form</field>
    <field name="model">escola.modul</field>
    <field name="arch" type="xml">
      <form>
        <sheet>
          <group>
            <field name="code"/>
            <field name="name"/>
            <field name="credits"/>
            <field name="start_date"/>
            <field name="professor_id"/>
            <field name="alumne_ids" widget="many2many_tags"/>
          </group>
        </sheet>
      </form>
    </field>
  </record>
</odoo>

5.3 professor_views.xml (smart button)

<odoo>
  <record id="view_professor_tree" model="ir.ui.view">
    <field name="name">escola.professor.tree</field>
    <field name="model">escola.professor</field>
    <field name="arch" type="xml">
      <tree>
        <field name="name"/>
        <field name="email"/>
        <field name="active"/>
        <field name="modules_count"/>
      </tree>
    </field>
  </record>

  <record id="view_professor_form" model="ir.ui.view">
    <field name="name">escola.professor.form</field>
    <field name="model">escola.professor</field>
    <field name="arch" type="xml">
      <form>
        <sheet>
          <div class="oe_button_box" name="button_box">
            <button type="object" name="action_open_moduls" class="oe_stat_button" icon="fa-book">
              <div class="o_stat_info">
                <field name="modules_count" widget="statinfo" string="Mòduls"/>
              </div>
            </button>
          </div>
          <group>
            <field name="first_name"/>
            <field name="last_name"/>
            <field name="name" readonly="1"/>
            <field name="email"/>
            <field name="active"/>
          </group>
        </sheet>
      </form>
    </field>
  </record>
</odoo>

6) Cerques i filtres

Crea ~/odoo-dev/custom-addons/escola/views/search_views.xml:

<odoo>
  <record id="escola_alumne_search" model="ir.ui.view">
    <field name="name">escola.alumne.search</field>
    <field name="model">escola.alumne</field>
    <field name="arch" type="xml">
      <search string="Cerca alumnes">
        <field name="name"/>
        <field name="ref"/>
        <filter name="moduls" string="Amb mòduls" domain="[('modul_ids','!=',False)]"/>
        <group expand="0" string="Agrupa per">
          <filter name="grp_modul" string="Mòdul" context="{'group_by':'modul_ids'}"/>
        </group>
      </search>
    </field>
  </record>

  <record id="escola_modul_search" model="ir.ui.view">
    <field name="name">escola.modul.search</field>
    <field name="model">escola.modul</field>
    <field name="arch" type="xml">
      <search string="Cerca mòduls">
        <field name="name"/>
        <field name="code"/>
        <filter name="sense_professor" string="Sense professor" domain="[('professor_id','=',False)]"/>
        <group expand="0" string="Agrupa per">
          <filter name="grp_prof" string="Professor" context="{'group_by':'professor_id'}"/>
        </group>
      </search>
    </field>
  </record>

  <record id="escola_professor_search" model="ir.ui.view">
    <field name="name">escola.professor.search</field>
    <field name="model">escola.professor</field>
    <field name="arch" type="xml">
      <search string="Cerca professors">
        <field name="name"/>
        <filter name="actius" string="Actius" domain="[('active','=',True)]"/>
        <group expand="0" string="Agrupa per">
          <filter name="grp_actiu" string="Actiu" context="{'group_by':'active'}"/>
        </group>
      </search>
    </field>
  </record>
</odoo>
Odoo associa automàticament la vista de cerca al model si és l’única o la més específica carregada per al model.

7) Permisos (recordatori)

El mateix security/ir.model.access.csv de la Pàg. 2 segueix vàlid. Si no el tens:

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_escola_alumne,access.escola.alumne,model_escola_alumne,base.group_user,1,1,1,1
access_escola_modul,access.escola.modul,model_escola_modul,base.group_user,1,1,1,1
access_escola_professor,access.escola.professor,model_escola_professor,base.group_user,1,1,1,1

8) Instal·lar/actualitzar i provar

  1. Recarrega/actualitza només el mòdul:
    odoo -c ~/.odoorc.dev -d dev_escola -u escola --stop-after-init
    i torna a arrencar normalment:
    odoo -c ~/.odoorc.dev -d dev_escola --http-port=8070 --dev=all
  2. Proves ràpides:
    • Alumnes: crear alumnes — es genera ref automàtic (ALU/YYYY/0001...). Valida email.
    • Mòduls: crear amb code en minúscules — queda en MAJÚSCULES; impideix duplicats.
    • Professors: crear i assignar a mòduls — smart button mostra el recompte i obre els mòduls.
    • Prova cerques: filtres “Amb mòduls”, “Sense professor”, “Actius”; agrupa per mòdul/professor/actiu.

9) Troubleshooting (resum)

Many2one mostra “escola.professor,1”
Mantenim _rec_name = "name" i el name calculat amb nom+cognoms (ja aplicat a professor.py).
“External ID not found” en menús
Accions al manifest abans dels menús. IDs d’acció i de menú han de coincidir (ja està així).
Seqüència no es genera
Comprova que data/ir_sequence.xml està al manifest i que el code de la seqüència és el mateix que uses a next_by_code().
Constraint de codi de mòdul salta
Canvia el codi o elimina el duplicat; és un unique SQL (funciona com previst).

10) Checklist final

  1. Manifest actualitzat amb data/ir_sequence.xml i views/search_views.xml.
  2. Models ampliats: seqüència d’alumnes, validador d’email, codi de mòdul únic + majúscules, professor amb comptador i acció.
  3. Vistes millorades + *smart button* + kanban + cerques.
  4. Upgrade del mòdul i proves a http://localhost:8070.