Technical Reference - SH All-In-One Helpdesk
Complete technical documentation for developers and integrators.
Table of Contents
- Module Structure
- Models Reference
- API Methods
- Security Model
- Hooks and Events
- Customization Points
- Database Schema
Module Structure
sh_all_in_one_helpdesk/
├── __init__.py
├── __manifest__.py
├── controllers/
│ ├── __init__.py
│ ├── main.py # HTTP endpoints
│ └── portal.py # Portal controllers
├── data/
│ ├── helpdesk_data.xml
│ ├── helpdesk_stage_data.xml
│ ├── helpdesk_email_data.xml
│ ├── helpdesk_cron_data.xml
│ └── helpdesk_reminder_*.xml
├── lib/
│ ├── __init__.py
│ └── html2text.py # Bundled HTML converter
├── models/
│ ├── __init__.py
│ ├── helpdesk_ticket.py
│ ├── helpdesk_team.py
│ ├── helpdesk_stages.py
│ ├── helpdesk_config_settings.py
│ ├── helpdesk_policies.py
│ ├── helpdesk_alarm.py
│ └── ... (other models)
├── security/
│ ├── sh_helpdesk_security.xml
│ ├── send_mail_security.xml
│ └── ir.model.access.csv
├── sh_helpdesk_so/ # Sales integration
├── sh_helpdesk_po/ # Purchase integration
├── sh_helpdesk_invoice/ # Invoice integration
├── sh_helpdesk_timesheet/ # Timesheet integration
├── sh_helpdesk_task/ # Task integration
├── sh_helpdesk_crm/ # CRM integration
├── static/
│ └── src/
│ ├── css/
│ ├── js/
│ └── xml/
├── views/
│ └── *.xml
└── wizard/
├── __init__.py
└── mail_compose.py
Models Reference
helpdesk.ticket
Primary ticket model with full field reference.
class HelpdeskTicket(models.Model):
_name = 'helpdesk.ticket'
_inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin']
_description = 'Helpdesk Ticket'
Core Fields
| Field |
Type |
Required |
Description |
name |
Char |
Auto |
Ticket number (sequence) |
partner_id |
Many2one |
Yes |
Customer contact |
ticket_subject |
Char |
Yes |
Brief description |
description |
Html |
No |
Detailed description |
stage_id |
Many2one |
No |
Current stage |
team_id |
Many2one |
No |
Assigned team |
user_id |
Many2one |
No |
Primary assignee |
sh_user_ids |
Many2many |
No |
Additional assignees |
priority |
Many2one |
No |
Priority level |
category_id |
Many2one |
No |
Category |
sub_category_id |
Many2one |
No |
Sub-category |
ticket_type |
Many2one |
No |
Ticket type |
tag_ids |
Many2many |
No |
Tags |
product_ids |
Many2many |
No |
Related products |
Customer Fields
| Field |
Type |
Description |
email |
Char |
Customer email |
mobile_no |
Char |
Customer mobile |
person_name |
Char |
Contact person name |
Status Fields
| Field |
Type |
Values |
state |
Selection |
customer_replied, staff_replied |
active |
Boolean |
Archive flag |
Date Fields
| Field |
Type |
Description |
create_date |
Datetime |
Creation timestamp |
close_date |
Datetime |
When closed |
cancel_date |
Datetime |
When cancelled |
replied_date |
Datetime |
Last reply |
sh_due_date |
Datetime |
Due date/reminder |
Computed Fields
| Field |
Type |
Compute Method |
sh_sla_deadline |
Datetime |
From SLA status |
sh_status |
Selection |
SLA pass/fail |
done_stage_boolean |
Boolean |
Is done stage |
open_boolean |
Boolean |
Is open |
Key Methods
def action_approve(self):
"""Advance to next stage, send emails."""
def action_done(self):
"""Mark as resolved, move to done stage."""
def action_closed(self):
"""Close ticket, record close info."""
def action_cancel(self):
"""Cancel ticket, record reason."""
def action_open(self):
"""Reopen closed ticket."""
def helpdesk_sla_create(self, team_id, ticket_type):
"""Create SLA status records for ticket."""
def change_sh_status(self):
"""Update SLA status based on current state."""
@api.model
def _run_auto_close_ticket(self):
"""Cron: Auto-close inactive tickets."""
helpdesk.team
class HelpdeskTeam(models.Model):
_name = 'helpdesk.team'
_description = 'Helpdesk Team'
| Field |
Type |
Description |
name |
Char |
Team name |
team_head |
Many2one(res.users) |
Team leader |
team_members |
Many2many(res.users) |
Team members |
sh_resource_calendar_id |
Many2one |
Working hours |
sla_count |
Integer |
Computed SLA count |
helpdesk.stages
class HelpdeskStages(models.Model):
_name = 'helpdesk.stages'
_description = 'Helpdesk Stages'
_order = 'sequence'
| Field |
Type |
Description |
name |
Char |
Stage name |
sequence |
Integer |
Sort order |
mail_template_ids |
Many2many |
Email templates |
sh_next_stage |
Many2one |
Auto-advance stage |
sh_group_ids |
Many2many |
Required groups |
is_cancel_button_visible |
Boolean |
Show cancel |
is_done_button_visible |
Boolean |
Show done |
sh.helpdesk.sla
class SHHelpdeskSLA(models.Model):
_name = 'sh.helpdesk.sla'
_description = 'SLA Policy'
| Field |
Type |
Description |
name |
Char |
Policy name |
sh_team_id |
Many2one |
Target team |
sh_ticket_type_id |
Many2one |
Target type (optional) |
sh_sla_target_type |
Selection |
reaching_stage / assign_to |
sh_stage_id |
Many2one |
Target stage |
sh_days |
Integer |
Days |
sh_hours |
Integer |
Hours |
sh_minutes |
Integer |
Minutes |
sh.helpdesk.sla.status
class SHHelpdeskSLAStatus(models.Model):
_name = 'sh.helpdesk.sla.status'
_description = 'SLA Status'
| Field |
Type |
Description |
sh_ticket_id |
Many2one |
Parent ticket |
sh_sla_id |
Many2one |
SLA policy |
sh_deadline |
Datetime |
Computed deadline |
sh_status |
Selection |
passed/failed/partial |
sh_done_sla_date |
Datetime |
When target reached |
color |
Integer |
UI color indicator |
Deadline Computation
@api.depends('sh_sla_id', 'sh_ticket_id')
def _compute_sh_deadline(self):
"""
Calculate deadline using team's working calendar.
Uses resource.calendar.plan_days() and plan_hours().
"""
res.company (Extensions)
Additional fields added to company model for configuration:
# Stage configuration
new_stage_id = fields.Many2one('helpdesk.stages')
done_stage_id = fields.Many2one('helpdesk.stages')
cancel_stage_id = fields.Many2one('helpdesk.stages')
close_stage_id = fields.Many2one('helpdesk.stages')
reopen_stage_id = fields.Many2one('helpdesk.stages')
# Feature toggles
category = fields.Boolean('Enable Category')
sub_category = fields.Boolean('Enable Sub-Category')
customer_rating = fields.Boolean('Enable Rating')
sh_display_multi_user = fields.Boolean('Multi-User')
# Defaults
sh_default_team_id = fields.Many2one('helpdesk.team')
sh_default_user_id = fields.Many2one('res.users')
# Email configuration
allocation_mail_template_id = fields.Many2one('mail.template')
reply_mail_template_id = fields.Many2one('mail.template')
API Methods
HTTP Endpoints
Base URL: /web/helpdesk/ or /helpdesk/
Dashboard Data
@http.route('/get-ticket-counter-data', type='http', auth='user')
def get_ticket_counter_data(self, **kw):
"""
Returns ticket counts by stage for dashboard.
Parameters:
filter: today|yesterday|weekly|monthly|yearly|custom
start_date: ISO date (for custom)
end_date: ISO date (for custom)
team: team ID
team_leader: user ID
user: user ID
Returns:
JSON with stage counts
"""
Ticket Operations
@http.route('/open-ticket', type='http', auth='user', methods=['POST'])
def open_ticket(self, **kw):
"""
Opens ticket(s) in form view.
Parameters:
ticket_ids: comma-separated IDs
Returns:
Action to open tickets
"""
Feedback
@http.route('/helpdesk/ticket/feedback/<int:ticket_id>',
type='http', auth='public', methods=['POST'])
def submit_feedback(self, ticket_id, **kw):
"""
Submit customer feedback.
Parameters:
rating: 1-6 scale
comment: text feedback
"""
Report Download
@http.route('/download/ht/<int:ticket_id>', type='http', auth='public')
def download_report(self, ticket_id, access_token=None):
"""
Download ticket PDF report.
Requires valid access_token for security.
"""
Portal Routes
@http.route('/my/tickets', type='http', auth='user', website=True)
def portal_tickets(self, **kw):
"""
Customer portal ticket list.
Access controlled by sh_portal_user_access field:
- user: own tickets only
- leader: team tickets
- manager: all tickets
"""
Security Model
Groups Hierarchy
<record id="helpdesk_group_user" model="res.groups">
<field name="name">Support User</field>
<field name="category_id" ref="base.module_category_helpdesk"/>
</record>
<record id="helpdesk_group_team_leader" model="res.groups">
<field name="name">Team Leader</field>
<field name="implied_ids" eval="[(4, ref('helpdesk_group_user'))]"/>
</record>
<record id="helpdesk_group_manager" model="res.groups">
<field name="name">Support Manager</field>
<field name="implied_ids" eval="[(4, ref('helpdesk_group_team_leader'))]"/>
</record>
Record Rules
<!-- User: own tickets only -->
<record id="helpdesk_ticket_user_rule" model="ir.rule">
<field name="domain_force">
['|',
('user_id', '=', user.id),
('sh_user_ids', 'in', user.id)]
</field>
<field name="groups" eval="[(4, ref('helpdesk_group_user'))]"/>
</record>
<!-- Leader: team tickets -->
<record id="helpdesk_ticket_leader_rule" model="ir.rule">
<field name="domain_force">
['|', '|', '|',
('user_id', '=', user.id),
('sh_user_ids', 'in', user.id),
('team_head', '=', user.id),
('team_id.team_head', '=', user.id)]
</field>
<field name="groups" eval="[(4, ref('helpdesk_group_team_leader'))]"/>
</record>
<!-- Manager: all tickets -->
<record id="helpdesk_ticket_manager_rule" model="ir.rule">
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('helpdesk_group_manager'))]"/>
</record>
Access Rights Matrix
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ticket_user,ticket.user,model_helpdesk_ticket,helpdesk_group_user,1,1,1,0
access_ticket_manager,ticket.manager,model_helpdesk_ticket,helpdesk_group_manager,1,1,1,1
access_team_user,team.user,model_helpdesk_team,helpdesk_group_user,1,0,0,0
access_team_manager,team.manager,model_helpdesk_team,helpdesk_group_manager,1,1,1,1
Hooks and Events
Ticket Creation Hook
@api.model
def create(self, vals):
# Auto-generate sequence
vals['name'] = self.env['ir.sequence'].next_by_code('helpdesk.ticket.sequence')
# Auto-create partner if email provided
if vals.get('email') and not vals.get('partner_id'):
partner = self.env['res.partner'].create({
'name': vals.get('person_name', vals['email']),
'email': vals['email']
})
vals['partner_id'] = partner.id
ticket = super().create(vals)
# Create SLA status records
ticket.helpdesk_sla_create(ticket.team_id, ticket.ticket_type)
# Send creation email
if ticket.stage_id.mail_template_ids:
for template in ticket.stage_id.mail_template_ids:
template.send_mail(ticket.id)
return ticket
Stage Change Hook
def write(self, vals):
if 'stage_id' in vals:
# Send stage transition emails
new_stage = self.env['helpdesk.stages'].browse(vals['stage_id'])
for template in new_stage.mail_template_ids:
for ticket in self:
template.send_mail(ticket.id)
# Update SLA status
self.change_sh_status()
return super().write(vals)
Message Posted Hook
class MailMessage(models.Model):
_inherit = 'mail.message'
@api.model
def create(self, vals):
if vals.get('model') == 'helpdesk.ticket':
ticket = self.env['helpdesk.ticket'].browse(vals['res_id'])
# Update state based on author
if vals.get('author_id') == ticket.partner_id.id:
ticket.state = 'customer_replied'
elif vals.get('author_id') != 2: # Not OdooBot
ticket.state = 'staff_replied'
return super().create(vals)
Customization Points
Adding Custom Fields
# In custom module's models.py
class HelpdeskTicketCustom(models.Model):
_inherit = 'helpdesk.ticket'
custom_field = fields.Char('Custom Field')
custom_related = fields.Many2one('custom.model')
Custom Stage Actions
class HelpdeskTicketCustom(models.Model):
_inherit = 'helpdesk.ticket'
def action_custom_stage(self):
"""Custom action for specific stage transition."""
custom_stage = self.env.ref('your_module.custom_stage')
self.write({'stage_id': custom_stage.id})
# Custom logic here
Custom Email Templates
<record id="custom_ticket_template" model="mail.template">
<field name="name">Custom Ticket Notification</field>
<field name="model_id" ref="sh_all_in_one_helpdesk.model_helpdesk_ticket"/>
<field name="subject">[${object.name}] Custom Notification</field>
<field name="body_html"><![CDATA[
<p>Hello ${object.partner_id.name},</p>
<p>Custom message here.</p>
<p>Ticket: ${object.name}</p>
]]></field>
</record>
Custom SLA Calculations
class CustomSLAStatus(models.Model):
_inherit = 'sh.helpdesk.sla.status'
@api.depends('sh_sla_id', 'sh_ticket_id')
def _compute_sh_deadline(self):
# Custom deadline calculation
for status in self:
# Your custom logic
pass
Database Schema
Core Tables
-- Main ticket table
CREATE TABLE helpdesk_ticket (
id SERIAL PRIMARY KEY,
name VARCHAR,
partner_id INTEGER REFERENCES res_partner(id),
ticket_subject VARCHAR,
description TEXT,
stage_id INTEGER REFERENCES helpdesk_stages(id),
team_id INTEGER REFERENCES helpdesk_team(id),
user_id INTEGER REFERENCES res_users(id),
priority INTEGER REFERENCES helpdesk_priority(id),
state VARCHAR,
active BOOLEAN DEFAULT TRUE,
create_date TIMESTAMP,
write_date TIMESTAMP,
close_date TIMESTAMP,
cancel_date TIMESTAMP
);
-- Team table
CREATE TABLE helpdesk_team (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
team_head INTEGER REFERENCES res_users(id),
sh_resource_calendar_id INTEGER REFERENCES resource_calendar(id)
);
-- Many2many: team members
CREATE TABLE helpdesk_team_res_users_rel (
team_id INTEGER REFERENCES helpdesk_team(id),
user_id INTEGER REFERENCES res_users(id),
PRIMARY KEY (team_id, user_id)
);
-- Stages table
CREATE TABLE helpdesk_stages (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
sequence INTEGER,
is_cancel_button_visible BOOLEAN,
is_done_button_visible BOOLEAN
);
-- SLA Policy table
CREATE TABLE sh_helpdesk_sla (
id SERIAL PRIMARY KEY,
name VARCHAR NOT NULL,
sh_team_id INTEGER REFERENCES helpdesk_team(id),
sh_ticket_type_id INTEGER REFERENCES helpdesk_ticket_type(id),
sh_sla_target_type VARCHAR,
sh_stage_id INTEGER REFERENCES helpdesk_stages(id),
sh_days INTEGER,
sh_hours INTEGER,
sh_minutes INTEGER
);
-- SLA Status (per ticket)
CREATE TABLE sh_helpdesk_sla_status (
id SERIAL PRIMARY KEY,
sh_ticket_id INTEGER REFERENCES helpdesk_ticket(id) ON DELETE CASCADE,
sh_sla_id INTEGER REFERENCES sh_helpdesk_sla(id),
sh_deadline TIMESTAMP,
sh_status VARCHAR,
sh_done_sla_date TIMESTAMP
);
Indexes
-- Performance indexes
CREATE INDEX idx_ticket_stage ON helpdesk_ticket(stage_id);
CREATE INDEX idx_ticket_team ON helpdesk_ticket(team_id);
CREATE INDEX idx_ticket_user ON helpdesk_ticket(user_id);
CREATE INDEX idx_ticket_partner ON helpdesk_ticket(partner_id);
CREATE INDEX idx_ticket_create_date ON helpdesk_ticket(create_date);
CREATE INDEX idx_sla_status_ticket ON sh_helpdesk_sla_status(sh_ticket_id);
Query Optimization
# Good: Use search with limit
tickets = self.env['helpdesk.ticket'].search([
('stage_id', '=', stage_id)
], limit=100)
# Good: Prefetch related records
tickets = self.env['helpdesk.ticket'].search([]).with_prefetch()
for ticket in tickets:
ticket.partner_id # Already loaded
# Bad: N+1 queries
for ticket in tickets:
# This causes separate query per ticket
print(ticket.partner_id.name)
# Good: Read all at once
data = tickets.read(['partner_id'])
Batch Operations
# Good: Batch create
records = []
for data in data_list:
records.append(data)
self.env['helpdesk.ticket'].create(records)
# Good: Batch write
tickets.write({'stage_id': new_stage.id})
Next: Integrations