🧩 1. Module Structure
library_issue/├── __manifest__.py├── models/│ └── book_issue.py├── views/│ └── book_issue_views.xml├── security/│ └── ir.model.access.csv📦 2. Manifest
{'name': 'Library Issue System','version': '1.0','depends': ['base'],'data': ['security/ir.model.access.csv','views/book_issue_views.xml',],'installable': True,}🧠 3. Model with Business Logic (CORE PART)
from odoo import models, fields, apifrom odoo.exceptions import ValidationError
class BookIssue(models.Model): _name = 'library.issue' _description = 'Book Issue'
name = fields.Char(string="Reference", required=True, copy=False, default="New") user_id = fields.Many2one('res.users', string="User", required=True) book_name = fields.Char(string="Book", required=True) issue_date = fields.Date(default=fields.Date.today) return_date = fields.Date()
state = fields.Selection([ ('draft', 'Draft'), ('issued', 'Issued'), ('returned', 'Returned'), ('cancel', 'Cancelled') ], default='draft')
# AUTO SEQUENCE (Production practice) @api.model def create(self, vals): if vals.get('name', 'New') == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('library.issue') or 'New' return super().create(vals)
# BUTTON: Issue Book def action_issue(self): for rec in self: if rec.state != 'draft': raise ValidationError("Only draft can be issued!") rec.state = 'issued'
# BUTTON: Return Book def action_return(self): for rec in self: if rec.state != 'issued': raise ValidationError("Only issued books can be returned!") rec.state = 'returned'
# BUTTON: Cancel def action_cancel(self): self.state = 'cancel'
🎨 4. Views with Buttons (REAL UI)
<odoo>
<record id="view_book_issue_form" model="ir.ui.view"> <field name="name">book.issue.form</field> <field name="model">library.issue</field> <field name="arch" type="xml">
<form> <!-- HEADER BUTTONS --> <header> <button name="action_issue" type="object" string="Issue" class="btn-primary" states="draft"/>
<button name="action_return" type="object" string="Return" class="btn-success" states="issued"/>
<button name="action_cancel" type="object" string="Cancel" class="btn-danger" states="draft,issued"/>
<field name="state" widget="statusbar"/> </header>
<sheet> <group> <field name="name"/> <field name="user_id"/> <field name="book_name"/> <field name="issue_date"/> <field name="return_date"/> </group> </sheet> </form>
</field> </record>
<!-- TREE VIEW --> <record id="view_book_issue_tree" model="ir.ui.view"> <field name="name">book.issue.tree</field> <field name="model">library.issue</field> <field name="arch" type="xml"> <tree> <field name="name"/> <field name="user_id"/> <field name="book_name"/> <field name="state"/> </tree> </field> </record>
<!-- ACTION --> <record id="action_book_issue" model="ir.actions.act_window"> <field name="name">Book Issues</field> <field name="res_model">library.issue</field> <field name="view_mode">tree,form</field> </record>
<!-- MENU --> <menuitem id="menu_library_root" name="Library"/> <menuitem id="menu_issue" name="Issues" parent="menu_library_root" action="action_book_issue"/>
</odoo>
🔐 5. Security
id,name,model_id:id,perm_read,perm_write,perm_create,perm_unlinkaccess_issue,access.issue,model_library_issue,1,1,1,1⚙️ 6. Add Sequence (IMPORTANT IN REAL PROJECTS)
Create XML file or data:
<record id="seq_library_issue" model="ir.sequence"> <field name="name">Library Issue</field> <field name="code">library.issue</field> <field name="prefix">ISS/</field> <field name="padding">4</field></record>👉 Result: ISS/0001, ISS/0002
🚀 7. Real Production Flow
↓
Clicks "Issue" → Validates → Issued
↓
Clicks "Return" → Completed
↓
Optional Cancel anytime