19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:27 +01:00
parent d1963a3c3a
commit 2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions

View file

@ -2,6 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import onboarding_onboarding
from . import onboarding_step
from . import onboarding_onboarding_step
from . import onboarding_progress
from . import onboarding_progress_step

View file

@ -5,7 +5,7 @@ from odoo import api, fields, models
from odoo.addons.onboarding.models.onboarding_progress import ONBOARDING_PROGRESS_STATES
class Onboarding(models.Model):
class OnboardingOnboarding(models.Model):
_name = 'onboarding.onboarding'
_description = 'Onboarding'
_order = 'sequence asc, id desc'
@ -13,15 +13,15 @@ class Onboarding(models.Model):
name = fields.Char('Name of the onboarding', translate=True)
# One word identifier used to define the onboarding panel's route: `/onboarding/{route_name}`.
route_name = fields.Char('One word name', required=True)
step_ids = fields.One2many('onboarding.onboarding.step', 'onboarding_id', 'Onboarding steps')
step_ids = fields.Many2many('onboarding.onboarding.step', string='Onboarding steps')
is_per_company = fields.Boolean('Should be done per company?', default=True)
text_completed = fields.Char(
'Message at completion', default=lambda s: s.env._('Nice work! Your configuration is done.'),
help='Text shown on onboarding when completed')
panel_background_color = fields.Selection(
[('orange', 'Orange'), ('blue', 'Blue'), ('violet', 'Violet'), ('none', 'None')],
string="Panel's Background color", default='orange',
help="Color gradient added to the panel's background.")
panel_background_image = fields.Binary("Panel's background image")
is_per_company = fields.Boolean(
'Should be done per company?', compute='_compute_is_per_company', readonly=True, store=False,
)
panel_close_action_name = fields.Char(
'Closing action', help='Name of the onboarding model action to execute when closing the panel.')
@ -37,19 +37,29 @@ class Onboarding(models.Model):
help='All Onboarding Progress Records (across companies).')
sequence = fields.Integer(default=10)
_sql_constraints = [
('route_name_uniq', 'UNIQUE (route_name)', 'Onboarding alias must be unique.'),
]
_route_name_uniq = models.Constraint(
'UNIQUE (route_name)',
'Onboarding alias must be unique.',
)
@api.depends('progress_ids', 'progress_ids.company_id', 'step_ids', 'step_ids.is_per_company')
def _compute_is_per_company(self):
# Once an onboarding is made "per-company", there is no drawback to simply still consider
# it per-company even when if its last per-company step is unlinked. This allows to avoid
# handling the merging of existing progress (step) records.
onboardings_with_per_company_steps_or_progress = self.filtered(
lambda o: o.progress_ids.company_id or (True in o.step_ids.mapped('is_per_company')))
onboardings_with_per_company_steps_or_progress.is_per_company = True
(self - onboardings_with_per_company_steps_or_progress).is_per_company = False
@api.depends_context('company')
@api.depends('progress_ids', 'progress_ids.is_onboarding_closed', 'progress_ids.onboarding_state')
@api.depends('progress_ids', 'progress_ids.is_onboarding_closed', 'progress_ids.onboarding_state', 'progress_ids.company_id')
def _compute_current_progress(self):
for onboarding in self:
current_progress_id = onboarding.progress_ids.filtered(
lambda progress: progress.company_id.id in {False, self.env.company.id})
if current_progress_id:
if len(current_progress_id) > 1:
current_progress_id = current_progress_id.sorted('create_date', reverse=True)[0]
onboarding.current_onboarding_state = current_progress_id.onboarding_state
onboarding.current_progress_id = current_progress_id
onboarding.is_onboarding_closed = current_progress_id.is_onboarding_closed
@ -58,28 +68,44 @@ class Onboarding(models.Model):
onboarding.current_progress_id = False
onboarding.is_onboarding_closed = False
def write(self, vals):
"""Recompute progress step ids if new steps are added/removed."""
already_linked_steps = self.step_ids
res = super().write(vals)
if self.step_ids != already_linked_steps:
self.progress_ids._recompute_progress_step_ids()
return res
def action_close(self):
"""Close the onboarding panel."""
self.current_progress_id.action_close()
@api.model
def action_close_panel(self, xmlid):
"""Close the onboarding panel identified by its `xmlid`.
If not found, quietly do nothing.
"""
if onboarding := self.env.ref(xmlid, raise_if_not_found=False):
onboarding.action_close()
def action_refresh_progress_ids(self):
"""Re-initialize onboarding progress records (after step is_per_company change).
Meant to be called when `is_per_company` of linked steps is modified (or per-company
steps are added to an onboarding).
"""
onboardings_to_refresh_progress = self.filtered(
lambda o: o.is_per_company and o.progress_ids and not o.progress_ids.company_id
)
onboardings_to_refresh_progress.progress_ids.unlink()
onboardings_to_refresh_progress._create_progress()
def action_toggle_visibility(self):
self.current_progress_id.action_toggle_visibility()
def write(self, values):
if 'is_per_company' in values:
onboardings_per_company_update = self.filtered(
lambda onboarding: onboarding.is_per_company != values['is_per_company'])
res = super().write(values)
if 'is_per_company' in values:
# When changing this parameter, all progress (onboarding and steps) is reset.
onboardings_per_company_update.progress_ids.unlink()
return res
def _search_or_create_progress(self):
"""Create Progress record(s) as necessary for the context.
"""
"""Create Progress record(s) as necessary for the context."""
onboardings_without_progress = self.filtered(lambda onboarding: not onboarding.current_progress_id)
onboardings_without_progress._create_progress()
return self.current_progress_id
@ -88,20 +114,22 @@ class Onboarding(models.Model):
return self.env['onboarding.progress'].create([
{
'company_id': self.env.company.id if onboarding.is_per_company else False,
'onboarding_id': onboarding.id
} for onboarding in self
'onboarding_id': onboarding.id,
'progress_step_ids': onboarding.step_ids.progress_ids.filtered(
lambda p: p.company_id.id in [False, self.env.company.id]
),
}
for onboarding in self
])
def _prepare_rendering_values(self):
self.ensure_one()
values = {
'bg_image': f'/web/image/onboarding.onboarding/{self.id}/panel_background_image',
'classes': f'o_onboarding_{self.panel_background_color}'
if self.panel_background_color not in {False, 'none'} else '',
'close_method': self.panel_close_action_name,
'close_model': 'onboarding.onboarding',
'steps': self.step_ids,
'state': self.current_progress_id._get_and_update_onboarding_state(),
'text_completed': self.text_completed,
}
return values

View file

@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, Command, fields, models
from odoo.addons.onboarding.models.onboarding_progress import ONBOARDING_PROGRESS_STATES
from odoo.exceptions import ValidationError
class OnboardingOnboardingStep(models.Model):
_name = 'onboarding.onboarding.step'
_description = 'Onboarding Step'
_order = 'sequence asc, id asc'
_rec_name = 'title'
onboarding_ids = fields.Many2many('onboarding.onboarding', string='Onboardings')
title = fields.Char('Title', translate=True)
description = fields.Char('Description', translate=True)
button_text = fields.Char(
'Button text', required=True, default=lambda s: s.env._("Let's do it"), translate=True,
help="Text on the panel's button to start this step")
done_icon = fields.Char('Font Awesome Icon when completed', default='fa-star')
done_text = fields.Char(
'Text to show when step is completed', default=lambda s: s.env._('Step Completed!'), translate=True)
step_image = fields.Binary("Step Image")
step_image_filename = fields.Char("Step Image Filename")
step_image_alt = fields.Char(
'Alt Text for the Step Image', default='Onboarding Step Image', translate=True,
help='Show when impossible to load the image')
panel_step_open_action_name = fields.Char(
string='Opening action', required=False,
help='Name of the onboarding step model action to execute when opening the step, '
'e.g. action_open_onboarding_1_step_1')
current_progress_step_id = fields.Many2one(
'onboarding.progress.step', string='Step Progress',
compute='_compute_current_progress', help='Onboarding Progress Step for the current context (company).')
current_step_state = fields.Selection(
ONBOARDING_PROGRESS_STATES, string='Completion State', compute='_compute_current_progress')
progress_ids = fields.One2many(
'onboarding.progress.step', 'step_id', string='Onboarding Progress Step Records', readonly=True,
help='All related Onboarding Progress Step Records (across companies)')
is_per_company = fields.Boolean('Is per company', default=True)
sequence = fields.Integer(default=10)
@api.depends_context('company')
@api.depends('progress_ids', 'progress_ids.step_state')
def _compute_current_progress(self):
# When `is_per_company` is changed, `progress_ids` is updated (see `write`) which triggers this `_compute`.
existing_progress_steps = self.progress_ids.filtered_domain([
('step_id', 'in', self.ids),
('company_id', 'in', [False, self.env.company.id]),
])
for step in self:
if step in existing_progress_steps.step_id:
current_progress_step_id = existing_progress_steps.filtered(
lambda progress_step: progress_step.step_id == step)
step.current_progress_step_id = current_progress_step_id
step.current_step_state = current_progress_step_id.step_state
else:
step.current_progress_step_id = False
step.current_step_state = 'not_done'
@api.constrains('onboarding_ids')
def check_step_on_onboarding_has_action(self):
if steps_without_action := self.filtered(lambda step: step.onboarding_ids and not step.panel_step_open_action_name):
raise ValidationError(_(
'An "Opening Action" is required for the following steps to be '
'linked to an onboarding panel: %(step_titles)s',
step_titles=steps_without_action.mapped('title'),
))
def write(self, vals):
new_is_per_company = vals.get('is_per_company')
steps_changing_is_per_company = (
self.browse() if new_is_per_company is None
else self.filtered(lambda step: step.is_per_company != new_is_per_company)
)
already_linked_onboardings = self.onboarding_ids
res = super().write(vals)
# Progress is reset (to be done per-company or, for steps, to have a single record)
if steps_changing_is_per_company:
steps_changing_is_per_company.progress_ids.unlink()
self.onboarding_ids.action_refresh_progress_ids()
if self.onboarding_ids - already_linked_onboardings:
self.onboarding_ids.progress_ids._recompute_progress_step_ids()
return res
def action_set_just_done(self):
# Make sure progress records exist for the current context (company)
steps_without_progress = self.filtered(lambda step: not step.current_progress_step_id)
steps_without_progress._create_progress_steps()
return self.current_progress_step_id.action_set_just_done().step_id
@api.model
def action_validate_step(self, xml_id):
step = self.env.ref(xml_id, raise_if_not_found=False)
if not step:
return "NOT_FOUND"
return "JUST_DONE" if step.action_set_just_done() else "WAS_DONE"
@api.model
def _get_placeholder_filename(self, field):
if field == "step_image":
return 'base/static/img/onboarding_default.png'
return super()._get_placeholder_filename(field)
def _create_progress_steps(self):
"""Create progress step records as necessary to validate steps.
Only considers existing `onboarding.progress` records for the current
company or without company (depending on `is_per_company`).
"""
onboarding_progress_records = self.env['onboarding.progress'].search([
('onboarding_id', 'in', self.onboarding_ids.ids),
('company_id', 'in', [False, self.env.company.id])
])
progress_step_values = [
{
'step_id': step_id.id,
'progress_ids': [
Command.link(onboarding_progress_record.id)
for onboarding_progress_record
in onboarding_progress_records.filtered(lambda p: step_id in p.onboarding_id.step_ids)],
'company_id': self.env.company.id if step_id.is_per_company else False,
} for step_id in self
]
return self.env['onboarding.progress.step'].create(progress_step_values)

View file

@ -19,26 +19,29 @@ class OnboardingProgress(models.Model):
onboarding_state = fields.Selection(
ONBOARDING_PROGRESS_STATES, string='Onboarding progress', compute='_compute_onboarding_state', store=True)
is_onboarding_closed = fields.Boolean('Was panel closed?')
company_id = fields.Many2one('res.company')
company_id = fields.Many2one('res.company', ondelete='cascade')
onboarding_id = fields.Many2one(
'onboarding.onboarding', 'Related onboarding tracked', required=True, ondelete='cascade')
progress_step_ids = fields.One2many('onboarding.progress.step', 'progress_id', 'Progress Steps Trackers')
_sql_constraints = [
('onboarding_company_uniq', 'unique (onboarding_id,company_id)',
'There cannot be multiple records of the same onboarding completion for the same company.'),
]
'onboarding.onboarding', 'Related onboarding tracked', required=True, index=True, ondelete='cascade')
progress_step_ids = fields.Many2many('onboarding.progress.step', string='Progress Steps Trackers')
# not in _sql_constraint because COALESCE is not supported for PostgreSQL constraint
_onboarding_company_uniq = models.UniqueIndex("(onboarding_id, COALESCE(company_id, 0))")
@api.depends('onboarding_id.step_ids', 'progress_step_ids', 'progress_step_ids.step_state')
def _compute_onboarding_state(self):
progress_steps_data = self.env['onboarding.progress.step'].read_group(
[('progress_id', 'in', self.ids), ('step_state', 'in', ['just_done', 'done'])],
['progress_id'], ['progress_id']
)
result = dict((data['progress_id'][0], data['progress_id_count']) for data in progress_steps_data)
for progress in self:
progress.onboarding_state = (
'not_done' if result.get(progress.id, 0) != len(progress.onboarding_id.step_ids)
else 'done')
'not_done' if (
len(progress.progress_step_ids.filtered(lambda p: p.step_state in {'just_done', 'done'}))
!= len(progress.onboarding_id.step_ids)
)
else 'done'
)
def _recompute_progress_step_ids(self):
"""Update progress steps when a step (with existing progress) is added to an onboarding."""
for progress in self:
progress.progress_step_ids = progress.onboarding_id.step_ids.current_progress_step_id
def action_close(self):
self.is_onboarding_closed = True
@ -48,9 +51,11 @@ class OnboardingProgress(models.Model):
progress.is_onboarding_closed = not progress.is_onboarding_closed
def _get_and_update_onboarding_state(self):
"""Used to fetch the progress of an onboarding for rendering its panel and is expected to
be called by the onboarding controller. It also has the responsibility of updating the
'just_done' states into 'done' so that the 'just_done' states are only rendered once.
"""Fetch the progress of an onboarding for rendering its panel.
This method is expected to only be called by the onboarding controller.
It also has the responsibility of updating the 'just_done' state into
'done' so that the 'just_done' states are only rendered once.
"""
self.ensure_one()
onboarding_states_values = {}

View file

@ -10,18 +10,15 @@ class OnboardingProgressStep(models.Model):
_description = 'Onboarding Progress Step Tracker'
_rec_name = 'step_id'
progress_id = fields.Many2one(
'onboarding.progress', 'Related Onboarding Progress Tracker', required=True, ondelete='cascade')
progress_ids = fields.Many2many('onboarding.progress', string='Related Onboarding Progress Tracker')
step_state = fields.Selection(
ONBOARDING_PROGRESS_STATES, string='Onboarding Step Progress', default='not_done')
onboarding_id = fields.Many2one(related='progress_id.onboarding_id', string='Onboarding')
step_id = fields.Many2one(
'onboarding.onboarding.step', string='Onboarding Step', required=True, ondelete='cascade')
'onboarding.onboarding.step', string='Onboarding Step', required=True, index=True, ondelete='cascade')
_sql_constraints = [
('progress_step_uniq', 'unique (progress_id, step_id)',
'There cannot be multiple records of the same onboarding step completion for the same Progress record.'),
]
company_id = fields.Many2one('res.company', ondelete='cascade')
_company_uniq = models.UniqueIndex('(step_id, COALESCE(company_id, 0))')
def action_consolidate_just_done(self):
was_just_done = self.filtered(lambda progress: progress.step_state == 'just_done')

View file

@ -1,84 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from odoo.addons.onboarding.models.onboarding_progress import ONBOARDING_PROGRESS_STATES
class OnboardingStep(models.Model):
_name = 'onboarding.onboarding.step'
_description = 'Onboarding Step'
_order = 'sequence asc, id asc'
_rec_name = 'title'
onboarding_id = fields.Many2one(
'onboarding.onboarding', string='Onboarding', readonly=True, required=True, ondelete='cascade')
title = fields.Char('Title', translate=True)
description = fields.Char('Description', translate=True)
button_text = fields.Char(
'Button text', required=True, default=_("Let's do it"), translate=True,
help="Text on the panel's button to start this step")
done_icon = fields.Char('Font Awesome Icon when completed', default='fa-star')
done_text = fields.Char(
'Text to show when step is completed', default=_('Step Completed! - Click to review'), translate=True)
panel_step_open_action_name = fields.Char(
string='Opening action', required=True,
help='Name of the onboarding step model action to execute when opening the step, '
'e.g. action_open_onboarding_1_step_1')
current_progress_step_id = fields.Many2one(
'onboarding.progress.step', string='Step Progress',
compute='_compute_current_progress', help='Onboarding Progress Step for the current context (company).')
current_step_state = fields.Selection(
ONBOARDING_PROGRESS_STATES, string='Completion State', compute='_compute_current_progress')
progress_ids = fields.One2many(
'onboarding.progress.step', 'step_id', string='Onboarding Progress Step Records', readonly=True,
help='All related Onboarding Progress Step Records (across companies)')
sequence = fields.Integer(default=10)
@api.depends_context('company')
@api.depends('progress_ids', 'progress_ids.step_state')
def _compute_current_progress(self):
existing_progress_steps = self.progress_ids.filtered_domain([
('step_id', 'in', self.ids),
('progress_id.company_id', 'in', [False, self.env.company.id]),
])
for step in self:
if step in existing_progress_steps.step_id:
current_progress_step_id = existing_progress_steps.filtered(
lambda progress_step: progress_step.step_id == step)
if len(current_progress_step_id) > 1:
current_progress_step_id = current_progress_step_id.sorted('create_date', reverse=True)[0]
step.current_progress_step_id = current_progress_step_id
step.current_step_state = current_progress_step_id.step_state
else:
step.current_progress_step_id = False
step.current_step_state = 'not_done'
def action_set_just_done(self):
# Make sure progress records exist for the current context (company)
steps_without_progress = self.filtered(lambda step: not step.current_progress_step_id)
steps_without_progress._create_progress_steps()
return self.current_progress_step_id.action_set_just_done()
def _create_progress_steps(self):
onboarding_progress_records = self.env['onboarding.progress'].search([
('onboarding_id', 'in', self.onboarding_id.ids),
('company_id', 'in', [False, self.env.company.id])
])
progress_step_values = []
for onboarding_progress_record in onboarding_progress_records:
progress_step_values += [
{
'onboarding_id': onboarding_progress_record.onboarding_id.id,
'progress_id': onboarding_progress_record.id,
'step_id': step_id.id,
}
for step_id
in self.filtered(lambda step: self.onboarding_id == onboarding_progress_record.onboarding_id)
if step_id not in onboarding_progress_record.progress_step_ids.step_id
]
return self.env['onboarding.progress.step'].create(progress_step_values)