0) Context
- Treballes amb la BD
dev_escolai el servidor a http://localhost:8070 (vegeu Pàg. 1). - Ja tens el mòdul
escolabàsic creat (Pàg. 2) amb Alumnes, Mòduls i Professors. - El Many2one de Professor ja mostra el nom complet (amb
_rec_name = "name").
1) Què afegirem
- Alumne: camp
refautomàtic amb seqüència, validació d’email. - Mòdul: codi únic (constraint SQL), onchange per posar-lo en majúscules, crèdits i data d’inici.
- Professor: actiu/ina (toggle), comptador de mòduls i smart button per obrir-los.
- Vistes: form/tree millorats, cerques (filters + group by), kanban d’alumnes.
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
- Recarrega/actualitza només el mòdul:
i torna a arrencar normalment:odoo -c ~/.odoorc.dev -d dev_escola -u escola --stop-after-initodoo -c ~/.odoorc.dev -d dev_escola --http-port=8070 --dev=all - Proves ràpides:
- Alumnes: crear alumnes — es genera
refautomàtic (ALU/YYYY/0001...). Valida email. - Mòduls: crear amb
codeen 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.
- Alumnes: crear alumnes — es genera
9) Troubleshooting (resum)
Many2one mostra “escola.professor,1”
Mantenim
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í).
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
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).
Canvia el codi o elimina el duplicat; és un unique SQL (funciona com previst).
10) Checklist final
- Manifest actualitzat amb
data/ir_sequence.xmliviews/search_views.xml. - Models ampliats: seqüència d’alumnes, validador d’email, codi de mòdul únic + majúscules, professor amb comptador i acció.
- Vistes millorades + *smart button* + kanban + cerques.
- Upgrade del mòdul i proves a
http://localhost:8070.