PROJECT MODULE AND THEME ADDED IN SHARED MODULES

This commit is contained in:
pranay 2025-11-04 11:57:04 +05:30
parent c3883f6a18
commit 5f267e96da
30 changed files with 1324 additions and 126 deletions

View File

@ -0,0 +1,2 @@
from . import models, wizards
from .hooks import post_init_hook

View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
{
'name': 'Project Task Timesheet Extended',
'version': '18.0.1.0.0',
'category': 'Project',
'summary': 'Enhancements and extended features for Projects, Tasks, and Timesheets',
'description': """
Project Task Timesheet Extended
===============================
This module extends and enhances the functionality of Odoo's Project, Task, and Timesheet modules.
Key Features:
--------------
- Project - Set Team and members
- Additional tools and views for project management.
- Extended task features and custom workflows.
- Improved timesheet tracking and reporting.
""",
'author': 'Gadi Pranay Sai Durga Kumar',
'website': 'https://ftprotech.in', # change if you have another URL
'license': 'LGPL-3',
'depends': [
'project',
'hr_timesheet',
'base',
],
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'wizards/project_user_assign_wizard.xml',
'view/teams.xml',
'view/task_stages.xml',
'view/project.xml',
'view/project_task.xml',
],
'assets': {
},
'installable': True,
'application': False,
'auto_install': False,
'post_init_hook': 'post_init_hook',
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
</odoo>

View File

@ -0,0 +1,142 @@
# hooks.py
from odoo import api, SUPERUSER_ID
def post_init_hook(env):
# Create shared project sequence if it doesn't exist
project_sequence = env['ir.sequence'].sudo().search([('code', '=', 'project.project.sequence')], limit=1)
if not project_sequence:
project_sequence = env['ir.sequence'].sudo().create({
'name': "Project Sequence",
'implementation': 'no_gap',
'padding': 3,
'use_date_range': False,
'prefix': 'PROJ-',
'code': 'project.project.sequence',
})
public_rule = env.ref('project.project_public_members_rule', raise_if_not_found=False)
if public_rule:
public_rule.write({
'perm_read': True,
'perm_write': False,
'perm_create': False,
'perm_unlink': False,
})
private_task_rule = env.ref('project.ir_rule_private_task', raise_if_not_found=False)
if private_task_rule:
private_task_rule.write({
'domain_force': """[
'&',
('project_id', '!=', False),
('project_id.privacy_visibility', '!=', 'followers'),
'|',
'&',
('is_generic', '=', True),
'|', '|',
('project_id', '!=', False),
('parent_id', '!=', False),
('user_ids', 'in', user.id),
'&',
('is_generic', '=', False),
('user_ids', 'in', user.id),
]
"""
})
task_visibility_rule = env.ref('project.task_visibility_rule', raise_if_not_found=False)
if task_visibility_rule:
task_visibility_rule.write({
'domain_force' : """
[
'|',
'&',
('project_id', '!=', False),
'|', '|',
'&',
('project_id.privacy_visibility', '!=', 'followers'),
'|',
('is_generic', '=', True),
('user_ids', 'in', user.id),
'&',
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
('is_generic', '=', True),
'&',
('user_ids', 'in', user.id),
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
'&',
('project_id', '=', False),
'|',
('message_partner_ids', 'in', [user.partner_id.id]),
('user_ids', 'in', user.id),
]
"""
})
task_visibility_rule_project_user = env.ref('project.task_visibility_rule_project_user', raise_if_not_found=False)
if task_visibility_rule_project_user:
task_visibility_rule_project_user.write({
'domain_force': """
[
'|',
'&',
('project_id', '!=', False),
'|', '|',
'&',
('project_id.privacy_visibility', '!=', 'followers'),
'|',
('is_generic', '=', True),
('user_ids', 'in', user.id),
'&',
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
('is_generic', '=', True),
'&',
('user_ids', 'in', user.id),
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
'&',
('project_id', '=', False),
'|',
('message_partner_ids', 'in', [user.partner_id.id]),
('user_ids', 'in', user.id),
]
"""
})
# Get all projects without sequence_name, sorted by creation date
projects = env['project.project'].search([('sequence_name', '=', False)], order='create_date asc')
# Assign sequence numbers to projects
for project in projects:
project.sequence_name = project_sequence.next_by_id()
# Process all projects to ensure they have task sequences
all_projects = env['project.project'].search([])
for project in all_projects:
if not project.task_sequence_id:
task_sequence = env['ir.sequence'].sudo().create({
'name': f"Task Sequence for {project.sequence_name}",
'implementation': 'no_gap',
'padding': 3,
'use_date_range': False,
'prefix': f"{project.sequence_name}/TASK-",
})
project.task_sequence_id = task_sequence
# Get all tasks without sequence_name, grouped by project
tasks = env['project.task'].search([('sequence_name', '=', False)], order='create_date asc')
# Group tasks by project
project_tasks = {}
for task in tasks:
if task.project_id:
if task.project_id.id not in project_tasks:
project_tasks[task.project_id.id] = []
project_tasks[task.project_id.id].append(task)
# Assign sequence numbers to tasks
for project_id, task_list in project_tasks.items():
project = env['project.project'].browse(project_id)
if project.task_sequence_id:
for task in task_list:
task.sequence_name = project.task_sequence_id.next_by_id()

View File

@ -0,0 +1,4 @@
from . import teams
from . import task_stages
from . import project
from . import project_task

View File

@ -0,0 +1,61 @@
from odoo import api, fields, models, _
class ProjectProject(models.Model):
_inherit = 'project.project'
sequence_name = fields.Char("Project Number", copy=False, readonly=True)
task_sequence_id = fields.Many2one(
'ir.sequence',
string="Task Sequence",
readonly=True,
copy=False,
help="Sequence for tasks of this project"
)
@api.model
def _get_shared_project_sequence(self):
"""Get or create a shared sequence for all projects"""
sequence = self.env['ir.sequence'].sudo().search([('code', '=', 'project.project.sequence')], limit=1)
if not sequence:
sequence = self.env['ir.sequence'].sudo().create({
'name': _("Project Sequence"),
'implementation': 'no_gap',
'padding': 3,
'use_date_range': False,
'prefix': 'PROJ-',
'code': 'project.project.sequence',
})
return sequence
@api.model_create_multi
def create(self, vals_list):
projects = super().create(vals_list)
sequence = self._get_shared_project_sequence()
for project in projects:
if not project.sequence_name:
project.sequence_name = sequence.next_by_id()
return projects
project_lead = fields.Many2one("res.users", string="Project Lead")
members_ids = fields.Many2many('res.users', 'project_user_rel', 'project_id',
'user_id', 'Project Members', help="""Project's
members are users who can have an access to
the tasks related to this project."""
)
user_id = fields.Many2one('res.users', string='Project Manager', default=lambda self: self.env.user, tracking=True,
domain=lambda self: [('groups_id', 'in', [self.env.ref('project.group_project_manager').id,self.env.ref('project_task_timesheet_extended.group_project_supervisor').id]),('share','=',False)],)
def add_users(self):
return {
'type': 'ir.actions.act_window',
'name': 'Add Users',
'res_model': 'project.user.assign.wizard',
'view_mode': 'form',
'view_id': self.env.ref('project_task_timesheet_extended.project_user_assignment_form_view').id,
'target': 'new',
'context': {'default_members_ids':[(6, 0, self.members_ids.ids)],
},
}

View File

@ -0,0 +1,48 @@
from odoo import api, fields, models, _
class projectTask(models.Model):
_inherit = 'project.task'
_rec_name = 'name'
sequence_name = fields.Char("Sequence", copy=False)
is_generic = fields.Boolean(string='Generic',default=True, help='All the followers would be able to see this task if the generic is set to true else only the assigned users would have the access to it')
assigned_team = fields.Many2one("internal.teams")
@api.onchange("assigned_team")
def onchange_assigned_team(self):
for rec in self:
if rec.assigned_team:
user_ids = rec.assigned_team.members_ids.ids
if rec.assigned_team.team_lead:
user_ids.append(rec.assigned_team.team_lead.id)
rec.user_ids = [(6, 0, user_ids)]
else:
rec.user_ids = [(5, 0, 0)]
@api.model_create_multi
def create(self, vals_list):
tasks = super().create(vals_list)
# Group tasks by project to avoid creating multiple sequences for the same project
project_dict = {}
for task in tasks:
if task.project_id:
if task.project_id.id not in project_dict:
project_dict[task.project_id.id] = task.project_id
# Create task sequences for projects that don't have one
for project in project_dict.values():
if not project.task_sequence_id:
task_sequence = self.env['ir.sequence'].sudo().create({
'name': _("Task Sequence for %s") % project.sequence_name,
'implementation': 'no_gap',
'padding': 3,
'use_date_range': False,
'prefix': f"{project.sequence_name}/TASK-",
})
project.task_sequence_id = task_sequence
# Assign sequence numbers to tasks
for task in tasks:
if task.project_id and task.project_id.task_sequence_id:
task.sequence_name = task.project_id.task_sequence_id.next_by_id()
return tasks

View File

@ -0,0 +1,7 @@
from odoo import api, fields, models, _
class TaskStages(models.Model):
_inherit = 'project.task.type'
team_id = fields.Many2one('internal.teams','Assigned to')
approval_by = fields.Selection([('assigned_team_lead','Assigned Team Lead'),('project_manager','Project Manager'),('project_lead','Project Lead')])

View File

@ -0,0 +1,19 @@
from odoo import api, fields, models
class InternalTeams(models.Model):
_name = "internal.teams"
team_name = fields.Text("Team Name", required=True)
team_lead = fields.Many2one("res.users", string="Team Lead")
members_ids = fields.Many2many('res.users', 'internal_team_user_rel', 'team_id',
'user_id', 'Team Members', help="""Team Members are the users who are working in this particular team."""
)
active = fields.Boolean(default=True, help="Set active to false to hide the Teams without removing it.")
def _compute_display_name(self):
""" Custom display_name in case a registration is nott linked to an attendee
"""
for registration in self:
registration.display_name = registration.team_name

View File

@ -0,0 +1,12 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
internal_teams_admin,internal.teams.admin,model_internal_teams,project.group_project_manager,1,1,1,1
internal_teams_manager,internal.teams.manager,model_internal_teams,project.group_project_user,1,1,1,0
internal_teams_user,internal.teams.user,model_internal_teams,base.group_user,1,0,0,0
project_user_assign_wizard_manager,project.user.assign.wizard,model_project_user_assign_wizard,project.group_project_manager,1,1,1,1
access_project_project_supervisor,project.project,project.model_project_project,project_task_timesheet_extended.group_project_supervisor,1,1,1,0
access_project_project_stage_supervisor,project.project_stage.supervisor,project.model_project_project_stage,project_task_timesheet_extended.group_project_supervisor,1,1,1,0
access_project_task_type_supervisor,project.task.type supervisor,project.model_project_task_type,project_task_timesheet_extended.group_project_supervisor,1,1,1,1
access_project_tags_supervisor,project.project_tags_supervisor,project.model_project_tags,project_task_timesheet_extended.group_project_supervisor,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 internal_teams_admin internal.teams.admin model_internal_teams project.group_project_manager 1 1 1 1
3 internal_teams_manager internal.teams.manager model_internal_teams project.group_project_user 1 1 1 0
4 internal_teams_user internal.teams.user model_internal_teams base.group_user 1 0 0 0
5 project_user_assign_wizard_manager project.user.assign.wizard model_project_user_assign_wizard project.group_project_manager 1 1 1 1
6 access_project_project_supervisor project.project project.model_project_project project_task_timesheet_extended.group_project_supervisor 1 1 1 0
7 access_project_project_stage_supervisor project.project_stage.supervisor project.model_project_project_stage project_task_timesheet_extended.group_project_supervisor 1 1 1 0
8 access_project_task_type_supervisor project.task.type supervisor project.model_project_task_type project_task_timesheet_extended.group_project_supervisor 1 1 1 1
9 access_project_tags_supervisor project.project_tags_supervisor project.model_project_tags project_task_timesheet_extended.group_project_supervisor 1 1 1 1

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="group_project_supervisor" model="res.groups">
<field name="name">Manager</field>
<field name="category_id" ref="base.module_category_services_project"/>
<field name="implied_ids" eval="[(4, ref('project.group_project_user'))]"/>
</record>
<record id="project.group_project_manager" model="res.groups">
<field name="implied_ids" eval="[(4, ref('project.group_project_user')),(4, ref('group_project_supervisor')), (4, ref('mail.group_mail_canned_response_admin'))]"/>
</record>
<data>
<record id="project_rule_manager_own_projects" model="ir.rule">
<field name="name">Manager: Own Projects</field>
<field name="model_id" ref="project.model_project_project"/>
<field name="groups" eval="[(4, ref('project_task_timesheet_extended.group_project_supervisor'))]"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="0"/>
</record>
<record model="ir.rule" id="project_supervisor_all_project_tasks_rule">
<field name="name">Project/Task: project supervisor: see all tasks linked to his assigned project or its own tasks</field>
<field name="model_id" ref="project.model_project_task"/>
<field name="domain_force">[
('project_id.user_id','=',user.id),
'|', ('project_id', '!=', False),
('user_ids', 'in', user.id),
]</field>
<field name="groups" eval="[(4,ref('project_task_timesheet_extended.group_project_supervisor'))]"/>
</record>
<record model="ir.rule" id="project_users_project_tasks_rule">
<field name="name">Project/Task: project users: don't see non generic tasks</field>
<field name="model_id" ref="project.model_project_task"/>
<field name="domain_force">[
'&amp;', '&amp;',
('project_id', '!=', False),
('is_generic', '=', False),
('user_ids', 'not in', user.id),
]
</field>
<field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/>
<field name="perm_read" eval="0"/>
</record>
<record model="ir.rule" id="project_users_project_lead_rule">
<field name="name">Project/Task: project lead: see all tasks</field>
<field name="model_id" ref="project.model_project_task"/>
<field name="domain_force">[
'&amp;', '&amp;', '&amp;',
('project_id', '!=', False),
('project_id.project_lead', '=', user.id),
'|', ('is_generic', '=', True), ('is_generic', '=', False),
'|', ('user_ids', 'in', user.id), ('user_ids', 'not in', user.id)
]
</field>
<field name="groups" eval="[(4,ref('base.group_user')),(4,ref('project.group_project_user'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="0"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="project_project_inherit_form_view" model="ir.ui.view">
<field name="name">project.project.inherit.form.view</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project.edit_project"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_title')]" position="after">
<group>
<h1>
<field name="sequence_name" readonly="1"/>
</h1>
</group>
</xpath>
<xpath expr="//field[@name='user_id']" position="after">
<field name="project_lead" widget="many2one_avatar_user"/>
</xpath>
<xpath expr="//field[@name='user_id']" position="replace">
<field name="user_id" widget="many2one_avatar_user"/>
</xpath>
<page name="settings" position="after">
<page string="Team">
<group>
<button name="add_users" type="object" string="Add/Update" class="btn-primary"/>
</group>
<group>
<field name="members_ids" widget="many2many_avatar_user" nolabel="1"
options="{'no_create': True, 'no_create_edit': True, 'no_open': True, 'no_quick_create': True}" readonly="1"/>
</group>
</page>
<page name="task_stages" string="Task Stages">
<field name="type_ids"/>
</page>
</page>
</field>
</record>
</odoo>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="project_task_form_inherit" model="ir.ui.view">
<field name="name">project.task.form.inherit</field>
<field name="model">project.task</field>
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_title','pe-0')]" position="after">
<group>
<h1><field name="sequence_name" readonly="1"/></h1>
</group>
</xpath>
<xpath expr="//field[@name='user_ids']" position="before">
<field name="assigned_team"/>
</xpath>
<xpath expr="//field[@name='user_ids']" position="after">
<field name="is_generic"/>
</xpath>
</field>
</record>
<!-- <record id="project.action_view_my_task" model="ir.actions.act_window">-->
<!-- <field name="name">My Tasks</field>-->
<!-- <field name="res_model">project.task</field>-->
<!-- <field name="view_mode">kanban,list,form,calendar,pivot,graph,activity</field>-->
<!-- <field name="context">{-->
<!-- 'search_default_open_tasks': 1,-->
<!-- 'all_task': 0,-->
<!-- 'default_user_ids': [(4, uid)],-->
<!-- 'default_is_custom':True-->
<!-- }</field>-->
<!-- <field name="search_view_id" ref="project.view_task_search_form"/>-->
<!-- <field name="domain">[('user_ids', 'in', uid)]</field>-->
<!-- <field name="help" type="html">-->
<!-- <p class="o_view_nocontent_smiling_face">-->
<!-- No tasks found. Let's create one!-->
<!-- </p>-->
<!-- <p>-->
<!-- Organize your tasks by dispatching them across the pipeline.<br/>-->
<!-- Collaborate efficiently by chatting in real-time or via email.-->
<!-- </p>-->
<!-- </field>-->
<!-- </record>-->
</data>
</odoo>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="task_type_tree_inherit" model="ir.ui.view">
<field name="name">project.task.type.list.inherit</field>
<field name="model">project.task.type</field>
<field name="inherit_id" ref="project.task_type_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="team_id" optional="show"/>
<field name="approval_by" optional="show"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="internal_teams_list_view" model="ir.ui.view">
<field name="name">internal.teams.list.view</field>
<field name="model">internal.teams</field>
<field name="arch" type="xml">
<list>
<field name="team_name"/>
<field name="team_lead"/>
<field name="members_ids" widget="many2many_tags"/>
</list>
</field>
</record>
<record id="internal_teams_form_view" model="ir.ui.view">
<field name="name">internal.teams.form.view</field>
<field name="model">internal.teams</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="team_name"/>
<field name="team_lead"/>
<notebook>
<page name="team_members" string="Team">
<field name="members_ids" widget="many2many">
<kanban quick_create="false" create="false" delete="true">
<field name="id"/>
<field name="name"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click" style="max-width: 200px">
<div class="o_kanban_record_top">
<img t-att-src="kanban_image('res.users', 'image_1920', record.id.raw_value)"
height="40" width="40"
class="oe_avatar oe_kanban_avatar_smallbox mb0"
alt="Avatar"/>
<div class="o_kanban_record_headings ml8">
<strong class="o_kanban_record_title">
<field name="name"/>
</strong>
</div>
<a t-if="! read_only_mode" type="delete" class="text-danger">
<i class="fa fa-times" title="Delete"></i>
</a>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</page>
</notebook>
</group>
</sheet>
</form>
</field>
</record>
<record id="internal_teams_action_tree" model="ir.actions.act_window">
<field name="name">Internal Teams</field>
<field name="res_model">internal.teams</field>
<field name="binding_view_types">form</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="internal_teams_menu" name="Internal Teams" action="internal_teams_action_tree"
parent="project.menu_project_config"/>
</odoo>

View File

@ -0,0 +1 @@
from . import project_user_assign_wizard

View File

@ -0,0 +1,32 @@
from odoo import api, fields, models
class ProjectUserAssignWizard(models.TransientModel):
_name = "project.user.assign.wizard"
team_ids = fields.Many2many("internal.teams")
members_ids = fields.Many2many('res.users', 'Members',
help="""Members are the users who are working in this particular Project"""
)
send_mail_notification = fields.Boolean()
@api.onchange('team_ids')
def fetch_members(self):
for team in self.team_ids:
self.update({"members_ids": [(4, user_id.id) for user_id in team.members_ids]+ [(4,team.team_lead.id)]})
def add_users_to_project(self):
record_id = self.env.context.get('active_id')
record = self.env['project.project'].sudo().search([('id','=',record_id)])
base_users = record.members_ids.ids
removed_ids = set(base_users).difference(self.members_ids.ids)
newly_added_ids = set(self.members_ids.ids).difference(base_users)
record.update({
"members_ids": [(6, 0, self.members_ids.ids)],
"message_partner_ids": [(4, user_id.partner_id.id) for user_id in self.env['res.users'].sudo().search([('id','in',list(newly_added_ids))])],
})
record.update({
"message_partner_ids": [(3, user_id.partner_id.id) for user_id in
self.env['res.users'].sudo().search([('id', 'in', list(removed_ids))])],
})
return {'type': 'ir.actions.act_window_close'}

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="project_user_assignment_form_view" model="ir.ui.view">
<field name="name">project.user.assign.wizard.form.view</field>
<field name="model">project.user.assign.wizard</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<div class="oe_row">
<field name="team_ids" widget="many2many_tags"/>
</div>
<field name="send_mail_notification"/>
<notebook>
<page name="team_members" string="Team">
<field name="members_ids" widget="many2many">
<kanban quick_create="false" create="false" delete="true">
<field name="id"/>
<field name="name"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click" style="max-width: 220px;">
<!-- Clean white card with subtle shadow -->
<div class="o_kanban_record bg-white rounded-3 shadow-sm border border-light overflow-hidden">
<!-- Header with subtle background -->
<div class="bg-light bg-opacity-10 py-3 px-3 border-bottom border-light">
<div class="d-flex align-items-center">
<!-- Avatar with subtle border -->
<img t-att-src="kanban_image('res.users', 'image_1920', record.id.raw_value)"
height="48" width="48"
class="oe_avatar rounded-circle border border-2 border-white shadow-sm"
alt="Avatar"/>
<div class="ms-3 flex-grow-1">
<strong class="o_kanban_record_title fs-5 text-dark">
<field name="name"/>
</strong>
</div>
<!-- Delete button with neutral styling -->
<a t-if="! read_only_mode" type="delete"
class="btn btn-sm btn-light rounded-circle p-1 d-flex align-items-center justify-content-center shadow-sm"
style="width: 28px; height: 28px;">
<i class="fa fa-times text-muted"></i>
</a>
</div>
</div>
<!-- Subtle content area -->
<div class="p-2 bg-white">
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-light text-dark border">
<i class="fa fa-user me-1"></i>
Member
</span>
<div class="d-flex">
<i class="fa fa-circle text-muted me-1"
style="font-size: 8px;"></i>
<span class="text-muted small">Active</span>
</div>
</div>
</div>
<!-- Bottom accent line -->
<div class="h-1 bg-light"></div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</page>
</notebook>
</group>
</sheet>
<footer>
<button name="add_users_to_project" type="object" string="Update Users" class="btn-primary"
data-hotkey="q"/>
<button special="cancel" data-hotkey="x" string="Cancel" class="btn-secondary"/>
</footer>
</form>
</field>
</record>
<record id="project_user_assignment_action_tree" model="ir.actions.act_window">
<field name="name">Project User Assignment</field>
<field name="res_model">project.user.assign.wizard</field>
<field name="binding_view_types">form</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -1 +1,2 @@
# from . import models from . import models
from . import controllers

View File

@ -12,7 +12,7 @@
"depends":['web','base'], "depends":['web','base'],
"data": [ "data": [
'views/login_templates.xml', 'views/login_templates.xml',
# 'views/ir_menu.xml' 'views/ir_menu.xml'
], ],
'assets': { 'assets': {
'web.assets_frontend': [ 'web.assets_frontend': [
@ -25,6 +25,11 @@
'dodger_blue/static/src/scss/colors.scss', 'dodger_blue/static/src/scss/colors.scss',
'dodger_blue/static/src/scss/theme_style_backend.scss', 'dodger_blue/static/src/scss/theme_style_backend.scss',
'dodger_blue/static/src/xml/sidebar_menu_icon_templates.xml', 'dodger_blue/static/src/xml/sidebar_menu_icon_templates.xml',
'dodger_blue/static/src/scss/quick_access.scss',
'dodger_blue/static/src/js/quick_access_button.js',
'dodger_blue/static/src/js/quick_access_setup.js',
'dodger_blue/static/src/xml/quick_access_button.xml',
], ],
}, },
'images': ['static/description/banner.jpg', 'images': ['static/description/banner.jpg',

View File

@ -0,0 +1 @@
from . import controllers

View File

@ -0,0 +1,22 @@
from odoo import http
from odoo.http import request
class QuickAccessController(http.Controller):
@http.route('/web/quick_access_menus', type='json', auth='user')
def get_quick_access_menus(self):
import pdb
pdb.set_trace()
user = request.env.user
menus = request.env['ir.ui.menu'].sudo().search([
('quick_user_access', 'in', [user.id])
])
result = []
for menu in menus:
result.append({
'id': menu.id,
'name': menu.name,
'action': menu.action.id if menu.action else False,
'children':[],
})
return result

View File

@ -5,62 +5,4 @@ from collections import defaultdict
class IrUiMenu(models.Model): class IrUiMenu(models.Model):
_inherit = 'ir.ui.menu' _inherit = 'ir.ui.menu'
is_not_main_menu = fields.Boolean() quick_user_access = fields.Many2many('res.users',string="Quick Access")
@api.model
@tools.ormcache('frozenset(self.env.user.groups_id.ids)', 'debug')
def _visible_menu_ids(self, debug=False):
""" Return the ids of the menu items visible to the user. """
# retrieve all menus, and determine which ones are visible
context = {'ir.ui.menu.full_list': True}
menus = self.with_context(context).search_fetch([], ['action', 'parent_id']).sudo()
# first discard all menus with groups the user does not have
group_ids = set(self.env.user._get_group_ids())
if not debug:
menus = menus.filtered(lambda menu: not (menu.is_not_main_menu))
group_ids = group_ids - {
self.env['ir.model.data']._xmlid_to_res_id('base.group_no_one', raise_if_not_found=False)}
menus = menus.filtered(
lambda menu: not (menu.groups_id and group_ids.isdisjoint(menu.groups_id._ids)))
# take apart menus that have an action
actions_by_model = defaultdict(set)
for action in menus.mapped('action'):
if action:
actions_by_model[action._name].add(action.id)
existing_actions = {
action
for model_name, action_ids in actions_by_model.items()
for action in self.env[model_name].browse(action_ids).exists()
}
action_menus = menus.filtered(lambda m: m.action and m.action in existing_actions)
folder_menus = menus - action_menus
visible = self.browse()
# process action menus, check whether their action is allowed
access = self.env['ir.model.access']
MODEL_BY_TYPE = {
'ir.actions.act_window': 'res_model',
'ir.actions.report': 'model',
'ir.actions.server': 'model_name',
}
# performance trick: determine the ids to prefetch by type
prefetch_ids = defaultdict(list)
for action in action_menus.mapped('action'):
prefetch_ids[action._name].append(action.id)
for menu in action_menus:
action = menu.action
action = action.with_prefetch(prefetch_ids[action._name])
model_name = action._name in MODEL_BY_TYPE and action[MODEL_BY_TYPE[action._name]]
if not model_name or access.check(model_name, 'read', False):
# make menu visible, and its folder ancestors, too
visible += menu
menu = menu.parent_id
while menu and menu in folder_menus and menu not in visible:
visible += menu
menu = menu.parent_id
return set(visible.ids)

View File

@ -0,0 +1,159 @@
/** @odoo-module **/
import { Component, onMounted, onWillUpdateProps } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class QuickAccessButton extends Component {
static template = "dodger_blue.QuickAccessButton";
static props = {};
setup() {
console.log("QuickAccessButton: Component setting up");
this.orm = useService("orm");
this.actionService = useService("action");
this.state = {
menus: [],
isOpen: false,
position: { x: window.innerWidth - 70, y: window.innerHeight - 70 },
isDragging: false,
dragOffset: { x: 0, y: 0 }
};
onMounted(() => {
console.log("QuickAccessButton: Component mounted");
console.log("Button position:", this.state.position);
});
this.fetchMenus();
}
async fetchMenus() {
console.log("QuickAccessButton: Fetching menus");
try {
fetch(`/web/quick_access_menus`, {
headers: { "X-Requested-With": "XMLHttpRequest" }
}).then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.text();
})
.then(html => {
if (loadingEl) loadingEl.style.display = 'none';
if (contentEl) {
contentEl.innerHTML = html;
contentEl.style.display = 'block';
initMatchingCandidates();
}
})
.catch(error => {
console.error('Error loading popup content:', error);
if (loadingEl) {
loadingEl.innerHTML = `
<div class="alert alert-danger">
Failed to load data. Please try again.
</div>
`;
}
});
// Using mock data with unique IDs for testing
this.state.menus = [
{ id: 1, name: 'Test Menu 1', action: null, children: [] },
{ id: 2, name: 'Test Menu 2', action: null, children: [
{ id: 3, name: 'Submenu 2.1', action: null, children: [] },
{ id: 4, name: 'Submenu 2.2', action: null, children: [] }
]},
{ id: 5, name: 'Test Menu 3', action: null, children: [] }
];
console.log("QuickAccessButton: Mock menus loaded", this.state.menus);
} catch (error) {
console.error("Error fetching quick access menus:", error);
}
}
toggleDropdown(ev) {
// Prevent event propagation to avoid immediate closing
ev.stopPropagation();
ev.preventDefault();
console.log("QuickAccessButton: Toggling dropdown");
this.state.isOpen = !this.state.isOpen;
console.log("Dropdown is now open:", this.state.isOpen);
// Check if dropdown is in DOM after a short delay
setTimeout(() => {
const dropdown = document.querySelector('.o_quick_access_dropdown');
if (dropdown) {
console.log("Dropdown element found in DOM");
// Force visibility
dropdown.style.display = 'block';
dropdown.style.visibility = 'visible';
dropdown.style.opacity = '1';
dropdown.style.zIndex = '2147483647';
} else {
console.error("Dropdown element not found in DOM");
}
}, 100);
}
checkDropdownVisibility() {
const dropdown = document.querySelector('.o_quick_access_dropdown');
if (dropdown) {
console.log("Dropdown element found in DOM");
console.log("Dropdown styles:", {
display: dropdown.style.display,
visibility: dropdown.style.visibility,
opacity: dropdown.style.opacity,
zIndex: dropdown.style.zIndex,
position: dropdown.style.position,
top: dropdown.style.top,
left: dropdown.style.left,
width: dropdown.style.width,
height: dropdown.style.height
});
const rect = dropdown.getBoundingClientRect();
console.log("Dropdown position:", rect);
// Check if dropdown is visible
if (rect.width > 0 && rect.height > 0) {
console.log("Dropdown appears to be visible");
} else {
console.log("Dropdown appears to be hidden");
}
} else {
console.error("Dropdown element not found in DOM");
}
}
startDrag(ev) {
console.log("QuickAccessButton: Starting drag");
this.state.isDragging = true;
this.state.dragOffset.x = ev.clientX - this.state.position.x;
this.state.dragOffset.y = ev.clientY - this.state.position.y;
ev.preventDefault();
}
onDrag(ev) {
if (!this.state.isDragging) return;
this.state.position.x = ev.clientX - this.state.dragOffset.x;
this.state.position.y = ev.clientY - this.state.dragOffset.y;
// Keep button within viewport
this.state.position.x = Math.max(0, Math.min(window.innerWidth - 60, this.state.position.x));
this.state.position.y = Math.max(0, Math.min(window.innerHeight - 60, this.state.position.y));
console.log("QuickAccessButton: Dragging to position", this.state.position);
}
stopDrag() {
this.state.isDragging = false;
}
openMenu(menu) {
console.log("QuickAccessButton: Opening menu", menu);
if (menu.action) {
this.actionService.doAction(menu.action);
}
this.state.isOpen = false;
}
}

View File

@ -0,0 +1,13 @@
/** @odoo-module **/
import { QuickAccessButton } from "./quick_access_button";
import { registry } from "@web/core/registry";
console.log("Registering QuickAccessButton component");
// Register the component in the main components registry
registry.category("main_components").add("QuickAccessButton", {
Component: QuickAccessButton,
});
console.log("QuickAccessButton component registered successfully");

View File

@ -0,0 +1,73 @@
.o_quick_access_button {
position: fixed !important;
z-index: 2147483647 !important; // Maximum z-index value
cursor: move;
background-color: rgba(255, 87, 34, 0.8) !important; // Semi-transparent orange
border-radius: 50% !important;
width: 60px !important;
height: 60px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
box-shadow: 0 4px 8px rgba(0,0,0,0.5) !important;
border: 2px solid white !important;
.btn {
background: none !important;
border: none !important;
color: white !important;
font-size: 24px !important;
width: 100% !important;
height: 100% !important;
padding: 0 !important;
}
}
//
// .o_quick_access_dropdown {
// position: fixed !important;
// z-index: 2147483647 !important; // Maximum z-index value
// background-color: white !important;
// border: 1px solid rgba(0,0,0,0.15) !important;
// border-radius: 4px !important;
// box-shadow: 0 6px 12px rgba(0,0,0,0.175) !important;
// min-width: 250px !important;
// max-height: 400px !important;
// overflow-y: auto !important;
// padding: 5px 0 !important;
//
// .dropdown-menu {
// display: block !important;
// background: none !important;
// border: none !important;
// margin: 0 !important;
// padding: 0 !important;
//
// .dropdown-item {
// position: relative;
//
// a {
// display: block !important;
// padding: 8px 20px !important;
// color: #333 !important;
// text-decoration: none !important;
//
// &:hover {
// background-color: #f5f5f5 !important;
// }
// }
//
// .dropdown-submenu {
// position: relative;
// padding-left: 20px;
//
// &:before {
// content: "";
// position: absolute;
// left: 5px;
// top: 8px;
// font-size: 8px;
// }
// }
// }
// }
// }

View File

@ -344,6 +344,14 @@ color:$text-color-dark;
transition: background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out; transition: background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
} }
.btn span{
color: $text-color-gray;
}
.btn span:hover {
color: $text-title;
}
.o_form_view .o_form_statusbar > .o_statusbar_status > .o_arrow_button:not(:first-child):before, .o_form_view .o_form_statusbar > .o_statusbar_status > .o_arrow_button:not(:first-child):after { .o_form_view .o_form_statusbar > .o_statusbar_status > .o_arrow_button:not(:first-child):before, .o_form_view .o_form_statusbar > .o_statusbar_status > .o_arrow_button:not(:first-child):after {
content: " "; content: " ";
display: block; display: block;
@ -944,6 +952,7 @@ a.dropdown-item.o_app.cybro-mainmenu {
border: none !important; border: none !important;
box-shadow: 2px 4px 8px 2px rgba(67, 54, 251, .1) !important; box-shadow: 2px 4px 8px 2px rgba(67, 54, 251, .1) !important;
} }
.o_menu_apps .dropdown-menu .search-container .search-input .input-group .input-group-prepend span.fa { .o_menu_apps .dropdown-menu .search-container .search-input .input-group .input-group-prepend span.fa {
color: $bg-white !important; color: $bg-white !important;
font-size: 1.08333333rem; font-size: 1.08333333rem;
@ -999,6 +1008,43 @@ h4.o_onboarding_step_title.mt16 a {
cursor: pointer; cursor: pointer;
color: $bg-white !important; color: $bg-white !important;
} }
.o_main_navbar a.o_menu_brand {
position: relative;
display: inline-block;
transform-style: preserve-3d;
transform-origin: center center;
transition: transform 420ms cubic-bezier(.22,1,.36,1), color 300ms ease;
will-change: transform;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
padding-left: 10px;
}
.o_main_navbar a.o_menu_brand:hover {
transform: rotateZ(-7deg) translateY(-3px) scale(1.08);
color: #ffffff !important;
}
/* Elegant 3D motion with crisp edges */
// .o_main_navbar a.o_menu_brand:hover {
// transform: translateY(-2px) rotateX(40deg) scale(1.04);
// color: #ffffff !important;
// text-shadow:
// 0 0 2px rgba(255, 255, 255, 0.3),
// 0 2px 4px rgba(0, 0, 0, 0.2);
// filter: brightness(1.15);
// }
.o_main_navbar {
perspective: 1000px;
-webkit-perspective: 900px;
}
.o_menu_systray li a i { .o_menu_systray li a i {
color: $bg-white !important; color: $bg-white !important;
} }
@ -1480,10 +1526,133 @@ top:109x !important;
.o_menu_sections a{ .o_menu_sections a{
color:$bg-white !important; color:$bg-white !important;
} }
.o_menu_sections a,
.o_menu_sections button {
position: relative;
background: none !important;
text-decoration: none;
color: inherit;
padding: 5px 0;
display: inline-block;
transition: color 0.3s ease;
}
/* Create the glowing underline */
.o_menu_sections a::after,
.o_menu_sections button span::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 3px;
background: radial-gradient(
circle,
rgba(255, 255, 255, 1) 0%,
rgba(0, 255, 255, 0.8) 20%,
rgba(0, 255, 255, 0.3) 60%,
rgba(0, 255, 255, 0) 100%
);
transform: translateX(-50%);
transition: all 0.4s ease-in-out;
border-radius: 2px;
opacity: 0;
}
/* Animate on hover */
.o_menu_sections a:hover::after,
.o_menu_sections button span:hover::after {
width: 100%;
opacity: 1;
height: 4px;
filter: drop-shadow(0 0 8px rgba(0, 255, 255, 0.8));
}
/* Optional: color shift on hover */
.o_menu_sections a:hover,
.o_menu_sections button:hover {
color: #00ffff;
text-shadow: 0 0 6px rgba(0, 255, 255, 0.6);
}
.o_menu_systray i.fa{ .o_menu_systray i.fa{
color:$bg-white !important; color:$bg-white !important;
} }
.o_menu_systray i.fa{
color: $bg-white !important;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
filter: drop-shadow(0 0 0px rgba(0, 255, 255, 0));
}
.o_menu_systray i.fa:hover {
animation: babyGiggle 1.2s ease-in-out, gentleBob 1.2s 1.2s ease-in-out infinite alternate, colorPulse 2.4s ease-in-out infinite;
}
@keyframes babyGiggle {
0%, 100% {
transform: translateY(0) rotate(0deg) scale(1);
}
15% {
transform: translateY(-2px) rotate(-3deg) scale(1.05);
}
30% {
transform: translateY(1px) rotate(2deg) scale(1.02);
}
45% {
transform: translateY(-3px) rotate(-4deg) scale(1.08);
}
60% {
transform: translateY(2px) rotate(3deg) scale(1.03);
}
75% {
transform: translateY(-1px) rotate(-2deg) scale(1.05);
}
90% {
transform: translateY(1px) rotate(1deg) scale(1.02);
}
}
@keyframes gentleBob {
0% {
transform: translateY(0) rotate(0deg) scale(1);
}
50% {
transform: translateY(-3px) rotate(0.8deg) scale(1.03);
}
100% {
transform: translateY(-1px) rotate(-0.5deg) scale(1.01);
}
}
@keyframes colorPulse {
0%, 100% {
color: $bg-white !important;
filter: drop-shadow(0 0 0px rgba(0, 255, 255, 0));
text-shadow: none;
}
20% {
color: #a0f0ff !important;
filter: drop-shadow(0 0 3px rgba(0, 255, 255, 0.4));
text-shadow: 0 0 5px rgba(0, 255, 255, 0.3);
}
40% {
color: #60e0ff !important;
filter: drop-shadow(0 0 6px rgba(0, 255, 255, 0.6));
text-shadow: 0 0 8px rgba(0, 255, 255, 0.5);
}
60% {
color: #00ffff !important;
filter: drop-shadow(0 0 8px rgba(0, 255, 255, 0.8));
text-shadow: 0 0 12px rgba(0, 255, 255, 0.7);
}
80% {
color: #80e8ff !important;
filter: drop-shadow(0 0 5px rgba(0, 255, 255, 0.5));
text-shadow: 0 0 7px rgba(0, 255, 255, 0.4);
}
}
.o_menu_systray .show i.fa{ .o_menu_systray .show i.fa{
color:$primary-color !important; color:$primary-color !important;
} }
@ -1541,7 +1710,11 @@ padding-left: 25px !important;
} }
.sidebar-header { .sidebar-header {
background-color: $primary-color-dark; // background-color: $primary-color-dark;
}
.sidebar-search-container input {
display: block;
} }
} }
@ -1552,7 +1725,7 @@ padding-left: 25px !important;
height: 60px; height: 60px;
padding: 10px 13px; padding: 10px 13px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1); border-bottom: 1px solid rgba(255, 255, 255, 0.1);
background-color: #343a4f; // darker header strip // background-color: #343a4f; // darker header strip
overflow: hidden; overflow: hidden;
flex-shrink: 0; /* Prevents shrinking */ flex-shrink: 0; /* Prevents shrinking */
position: sticky; /* Keeps it in place */ position: sticky; /* Keeps it in place */
@ -1581,6 +1754,38 @@ padding-left: 25px !important;
} }
} }
// User profile section
.sidebar-search {
height: 60px;
padding: 15px 0;
overflow: hidden;
flex-shrink: 0; /* Prevents shrinking */
position: sticky; /* Keeps it in place */
z-index: 1;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-search-container {
display: flex;
align-items: center;
padding: 0;
i {
padding-left: 15px
// width: 40px;
// height: 40px;
// border-radius: 50%;
// object-fit: cover;
}
input {
border-radius: 10px;
border: 1px solid #ccc;
padding: 6px 10px;
display: none;
}
}
// Menu items // Menu items
.sidebar-main-menus { .sidebar-main-menus {
padding: 15px 0; padding: 15px 0;

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates id="template" xml:space="preserve">
<t t-name="dodger_blue.QuickAccessButton" owl="1">
<div class="o_quick_access_button"
t-att-style="`left: ${state.position.x}px; top: ${state.position.y}px;`"
t-on-mousedown="startDrag">
<button class="btn btn-primary rounded-circle"
t-on-click="toggleDropdown"
title="Quick Access">
<i class="fa fa-rocket"/>
</button>
</div>
</t>
<!-- &lt;!&ndash; Position the dropdown above the button &ndash;&gt;-->
<!-- <div t-if="state.isOpen" class="o_quick_access_dropdown"-->
<!-- t-att-style="`left: ${state.position.x}px; bottom: ${window.innerHeight - state.position.y + 10}px;`">-->
<!-- <div class="dropdown-menu show">-->
<!-- <t t-foreach="state.menus" t-as="menu" t-key="menu.id">-->
<!-- <t t-call="dodger_blue.menu_item"/>-->
<!-- </t>-->
<!-- </div>-->
<!-- </div>-->
<!-- </t>-->
<!-- <t t-name="dodger_blue.menu_item" owl="1">-->
<!-- <div class="dropdown-item">-->
<!-- <a href="#" t-on-click="(ev) => this.openMenu(menu)">-->
<!-- <t t-esc="menu.name"/>-->
<!-- </a>-->
<!-- <div t-if="menu.children" class="dropdown-submenu">-->
<!-- <t t-foreach="menu.children" t-as="child" t-key="child.id">-->
<!-- <t t-call="dodger_blue.menu_item" t-call-context="{'menu': child}"/>-->
<!-- </t>-->
<!-- </div>-->
<!-- </div>-->
<!-- </t>-->
</templates>

View File

@ -3,64 +3,64 @@
<!-- Template for sidebar menu icon--> <!-- Template for sidebar menu icon-->
<t t-inherit="web.NavBar.AppsMenu" t-inherit-mode="extension" owl="1"> <t t-inherit="web.NavBar.AppsMenu" t-inherit-mode="extension" owl="1">
<xpath expr="//Dropdown" position="replace"> <xpath expr="//Dropdown" position="replace">
<ul class="o_menu_apps"> <!-- <ul class="o_menu_apps">-->
<li class="dropdown show"> <!-- <li class="dropdown show">-->
<a class="full" data-bs-toggle="collapse" <!-- <a class="full" data-bs-toggle="collapse"-->
data-bs-target="#Appmenu" aria-expanded="true" <!-- data-bs-target="#Appmenu" aria-expanded="true"-->
href="#" <!-- href="#"-->
t-attf-style="height: 46px !important;padding: 0 10px !important;color: #fff !important;line-height: 46px !important;transition: .3s all ease !important;"> <!-- t-attf-style="height: 46px !important;padding: 0 10px !important;color: #fff !important;line-height: 46px !important;transition: .3s all ease !important;">-->
<i class="fa fa-bars"/> <!-- <i class="fa fa-bars"/>-->
</a> <!-- </a>-->
<div class="dropdown-menu cybro-main-menu collapse" <!-- <div class="dropdown-menu cybro-main-menu collapse"-->
id="Appmenu" role="menu" x-placement="top-start" <!-- id="Appmenu" role="menu" x-placement="top-start"-->
style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(5px, -1px, 0px);"> <!-- style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(5px, -1px, 0px);">-->
<div class="sidebar-user"> <!-- <div class="sidebar-user">-->
<t t-set="user_img" <!-- <t t-set="user_img"-->
t-value="'/web/image?model=res.users&amp;field=image_1920&amp;id='+user_id"/> <!-- t-value="'/web/image?model=res.users&amp;field=image_1920&amp;id='+user_id"/>-->
<img t-att-src="user_img" alt="User Avatar" loading="lazy"/> <!-- <img t-att-src="user_img" alt="User Avatar" loading="lazy"/>-->
<div class="sidebar-username"> <!-- <div class="sidebar-username">-->
<span t-esc="user_name"/> <!-- <span t-esc="user_name"/>-->
</div> <!-- </div>-->
</div> <!-- </div>-->
<div class="search-container form-row align-items-center m-auto mb-5 col-5"> <!-- <div class="search-container form-row align-items-center m-auto mb-5 col-5">-->
<div class="search-input col-md-10 ml-auto mr-auto mb-5" <!-- <div class="search-input col-md-10 ml-auto mr-auto mb-5"-->
t-on-input="_searchMenusSchedule"> <!-- t-on-input="_searchMenusSchedule">-->
<div class="input-group"> <!-- <div class="input-group">-->
<div class="input-group-prepend"> <!-- <div class="input-group-prepend">-->
<div class="input-group-text"> <!-- <div class="input-group-text">-->
<i class="fa fa-search"/> <!-- <i class="fa fa-search"/>-->
</div> <!-- </div>-->
</div> <!-- </div>-->
<input type="search" <!-- <input type="search"-->
autocomplete="off" <!-- autocomplete="off"-->
placeholder="Search menus..." <!-- placeholder="Search menus..."-->
class="form-control"/> <!-- class="form-control"/>-->
</div> <!-- </div>-->
</div> <!-- </div>-->
<div class="search-results col-md-10 ml-auto mr-auto"/> <!-- <div class="search-results col-md-10 ml-auto mr-auto"/>-->
</div> <!-- </div>-->
<div class="nav-container"> <!-- <div class="nav-container">-->
<div class="app-menu"> <!-- <div class="app-menu">-->
<t t-foreach="menuService.getApps()" t-as="app" t-key="app_index"> <!-- <t t-foreach="menuService.getApps()" t-as="app" t-key="app_index">-->
<a role="menuitem" t-attf-href="/odoo/{{app.actionPath}}" class="dropdown-item o_app mt0" <!-- <a role="menuitem" t-attf-href="/odoo/{{app.actionPath}}" class="dropdown-item o_app mt0"-->
t-att-data-menu-id="app.menuID" t-att-data-menu-xmlid="app.xmlID" <!-- t-att-data-menu-id="app.menuID" t-att-data-menu-xmlid="app.xmlID"-->
t-att-data-action-id="app.actionID"> <!-- t-att-data-action-id="app.actionID">-->
<img t-if="app.webIcon.toString().includes('.png')" t-att-title="app.name" <!-- <img t-if="app.webIcon.toString().includes('.png')" t-att-title="app.name"-->
style="width: 25px !important;height: 25px !important;border-radius: 5px !important;padding:0 !important;" <!-- style="width: 25px !important;height: 25px !important;border-radius: 5px !important;padding:0 !important;"-->
t-attf-src="{{app.webIconData}}"/> <!-- t-attf-src="{{app.webIconData}}"/>-->
<img t-if="app.webIcon.toString().includes('.svg')" t-att-title="app.name" <!-- <img t-if="app.webIcon.toString().includes('.svg')" t-att-title="app.name"-->
style="width: 25px !important;height: 25px !important;border-radius: 5px !important;padding:0 !important;" <!-- style="width: 25px !important;height: 25px !important;border-radius: 5px !important;padding:0 !important;"-->
t-attf-src="{{app.webIconData}}"/> <!-- t-attf-src="{{app.webIconData}}"/>-->
<span class="a_app_menu_title"> <!-- <span class="a_app_menu_title">-->
<t t-esc="app.name"/> <!-- <t t-esc="app.name"/>-->
</span> <!-- </span>-->
</a> <!-- </a>-->
</t> <!-- </t>-->
</div> <!-- </div>-->
</div> <!-- </div>-->
</div> <!-- </div>-->
</li> <!-- </li>-->
</ul> <!-- </ul>-->
</xpath> </xpath>
</t> </t>
@ -125,9 +125,23 @@
<div class="sidebar-companyname">FTPROTECH</div> <div class="sidebar-companyname">FTPROTECH</div>
</div> </div>
</div> </div>
<div class="sidebar-main-menus" role="menu">
<!-- Search Bar Section -->
<div class="sidebar-search p-2">
<div class="sidebar-search-container" style="display: flex; align-items: center; gap: 8px;">
<i class="fa fa-search" style="font-size: 18px; color: #555;"></i>
<input type="text"
id="sidebar-menu-search"
class="form-control"
placeholder="Search menu..."
onkeyup="filterSidebarMenus(this.value)"
style="flex: 1;"/>
</div>
</div>
<div class="sidebar-main-menus" role="menu" id="sidebar-menu-list">
<t t-foreach="menuService.getApps()" t-as="app" t-key="app_index"> <t t-foreach="menuService.getApps()" t-as="app" t-key="app_index">
<li> <li class="sidebar-menu-item">
<a role="menuitem" <a role="menuitem"
t-att-href="app.actionPath ? `/odoo/${app.actionPath}` : `/web#action=${app.actionID}`" t-att-href="app.actionPath ? `/odoo/${app.actionPath}` : `/web#action=${app.actionID}`"
class="dropdown-item o_app mt0" class="dropdown-item o_app mt0"
@ -139,10 +153,13 @@
t-att-title="app.name" t-att-title="app.name"
class="o-app-icon" class="o-app-icon"
t-attf-src="{{app.webIconData}}"/> t-attf-src="{{app.webIconData}}"/>
<img t-if="app.webIcon.toString().includes('.svg')" <img t-elif="app.webIcon.toString().includes('.svg')"
t-att-title="app.name" t-att-title="app.name"
class="o-app-icon" class="o-app-icon"
t-attf-src="{{app.webIconData}}"/> t-attf-src="{{app.webIconData}}"/>
<img t-else="" class="img img-fluid o-app-icon" alt="Image"
src="/base/static/description/icon.png"
name="icon"/>
<span class="o-app-name" t-esc="app.name"/> <span class="o-app-name" t-esc="app.name"/>
</div> </div>
</a> </a>
@ -150,6 +167,18 @@
</t> </t>
</div> </div>
</div> </div>
<!-- JS for Search Filter -->
<script>
function filterSidebarMenus(searchText) {
searchText = searchText.toLowerCase();
const items = document.querySelectorAll('#sidebar-menu-list .sidebar-menu-item');
items.forEach(item => {
const name = item.querySelector('.o-app-name').innerText.toLowerCase();
item.style.display = name.includes(searchText) ? '' : 'none';
});
}
</script>
</xpath> </xpath>
</t> </t>
</templates> </templates>

View File

@ -7,8 +7,10 @@
<field name="model">ir.ui.menu</field> <field name="model">ir.ui.menu</field>
<field name="inherit_id" ref="base.edit_menu_access"/> <field name="inherit_id" ref="base.edit_menu_access"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//field[@name='sequence']" position="after"> <xpath expr="//notebook" position="inside">
<field name="is_not_main_menu"/> <page name="quick_access" string="Quick Access">
<field name="quick_user_access" widget="many2many_tags"/>
</page>
</xpath> </xpath>
</field> </field>
</record> </record>