19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:58 +01:00
parent 20e6dadd87
commit 4b94f0abc5
205 changed files with 24700 additions and 14614 deletions

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import res_users
from . import ir_http
from . import tour

View file

@ -1,16 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class Http(models.AbstractModel):
class IrHttp(models.AbstractModel):
_inherit = 'ir.http'
def session_info(self):
result = super().session_info()
if result['is_admin']:
demo_modules_count = self.env['ir.module.module'].sudo().search_count([('demo', '=', True)])
result['web_tours'] = self.env['web_tour.tour'].get_consumed_tours()
result['tour_disable'] = demo_modules_count > 0
result["tour_enabled"] = self.env.user.tour_enabled
result['current_tour'] = self.env["web_tour.tour"].get_current_tour()
return result

View file

@ -0,0 +1,18 @@
from odoo import models, fields, api, modules
class ResUsers(models.Model):
_inherit = "res.users"
tour_enabled = fields.Boolean(compute='_compute_tour_enabled', store=True, readonly=False, string="Onboarding")
@api.depends("create_date")
def _compute_tour_enabled(self):
demo_modules_count = self.env['ir.module.module'].sudo().search_count([('demo', '=', True)])
for user in self:
user.tour_enabled = user._is_admin() and demo_modules_count == 0 and not modules.module.current_test
@api.model
def switch_tour_enabled(self, val):
self.env.user.sudo().tour_enabled = val
return self.env.user.tour_enabled

View file

@ -1,30 +1,112 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo import api, fields, models, Command
import json
import base64
class Tour(models.Model):
_name = "web_tour.tour"
class Web_TourTour(models.Model):
_name = 'web_tour.tour'
_description = "Tours"
_log_access = False
_order = "sequence, name, id"
name = fields.Char(string="Tour name", required=True)
user_id = fields.Many2one('res.users', string='Consumed by')
name = fields.Char(required=True)
step_ids = fields.One2many("web_tour.tour.step", "tour_id")
url = fields.Char(string="Starting URL", default="/odoo")
sharing_url = fields.Char(compute="_compute_sharing_url", string="Sharing URL")
rainbow_man_message = fields.Html(default="<b>Good job!</b> You went through all steps of this tour.", translate=True)
sequence = fields.Integer(default=1000)
custom = fields.Boolean(string="Custom")
user_consumed_ids = fields.Many2many("res.users")
_uniq_name = models.Constraint(
'unique(name)',
"A tour already exists with this name . Tour's name must be unique!",
)
@api.depends("name")
def _compute_sharing_url(self):
for tour in self:
tour.sharing_url = f"{tour.get_base_url()}/odoo?tour={tour.name}"
@api.model
def consume(self, tour_names):
""" Sets given tours as consumed, meaning that
these tours won't be active anymore for that user """
if not self.env.user.has_group('base.group_user'):
# Only internal users can use this method.
# TODO master: update ir.model.access records instead of using sudo()
return
for name in tour_names:
self.sudo().create({'name': name, 'user_id': self.env.uid})
def consume(self, tourName):
if self.env.user and self.env.user._is_internal():
tour_id = self.search([("name", "=", tourName)])
if tour_id:
tour_id.sudo().user_consumed_ids = [Command.link(self.env.user.id)]
return self.get_current_tour()
@api.model
def get_consumed_tours(self):
""" Returns the list of consumed tours for the current user """
return [t.name for t in self.search([('user_id', '=', self.env.uid)])]
def get_current_tour(self):
if self.env.user and self.env.user.tour_enabled and self.env.user._is_internal():
tours_to_run = self.search([("custom", "=", False), ("user_consumed_ids", "not in", self.env.user.id)])
return bool(tours_to_run[:1]) and tours_to_run[:1]._get_tour_json()
@api.model
def get_tour_json_by_name(self, tour_name):
tour_id = self.search([("name", "=", tour_name)])
return tour_id._get_tour_json()
def _get_tour_json(self):
tour_json = self.read(fields={
"name",
"url",
"custom"
})[0]
del tour_json["id"]
tour_json["steps"] = self.step_ids.get_steps_json()
tour_json["rainbowManMessage"] = self.rainbow_man_message
return tour_json
def export_js_file(self):
js_content = f"""import {{ registry }} from '@web/core/registry';
registry.category("web_tour.tours").add("{self.name}", {{
url: "{self.url}",
steps: () => {json.dumps(self.step_ids.get_steps_json(), indent=4)}
}})"""
attachment_id = self.env["ir.attachment"].create({
"datas": base64.b64encode(bytes(js_content, 'utf-8')),
"name": f"{self.name}.js",
"mimetype": "application/javascript",
"res_model": "web_tour.tour",
"res_id": self.id,
})
return {
"type": "ir.actions.act_url",
"url": f"/web/content/{attachment_id.id}?download=true",
}
class Web_TourTourStep(models.Model):
_name = 'web_tour.tour.step'
_description = "Tour's step"
_order = "sequence, id"
trigger = fields.Char(required=True)
content = fields.Char()
tooltip_position = fields.Selection(selection=[
["bottom", "Bottom"],
["top", "Top"],
["right", "Right"],
["left", "left"],
], default="bottom")
tour_id = fields.Many2one("web_tour.tour", required=True, index=True, ondelete="cascade")
run = fields.Char()
sequence = fields.Integer()
def get_steps_json(self):
steps = []
for step in self.read(fields=["trigger", "content", "run", "tooltip_position"]):
del step["id"]
step["tooltipPosition"] = step["tooltip_position"]
del step["tooltip_position"]
if not step["content"]:
del step["content"]
steps.append(step)
return steps