diff --git a/addons_extensions/project_task_timesheet_extended/__init__.py b/addons_extensions/project_task_timesheet_extended/__init__.py
new file mode 100644
index 000000000..119a556b9
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/__init__.py
@@ -0,0 +1,2 @@
+from . import models, wizards
+from .hooks import post_init_hook
diff --git a/addons_extensions/project_task_timesheet_extended/__manifest__.py b/addons_extensions/project_task_timesheet_extended/__manifest__.py
new file mode 100644
index 000000000..95f62283d
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/__manifest__.py
@@ -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',
+}
diff --git a/addons_extensions/project_task_timesheet_extended/data/ir_sequence_data.xml b/addons_extensions/project_task_timesheet_extended/data/ir_sequence_data.xml
new file mode 100644
index 000000000..75cafa13d
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/data/ir_sequence_data.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/hooks.py b/addons_extensions/project_task_timesheet_extended/hooks.py
new file mode 100644
index 000000000..6ee15a629
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/hooks.py
@@ -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()
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/models/__init__.py b/addons_extensions/project_task_timesheet_extended/models/__init__.py
new file mode 100644
index 000000000..000e815e7
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/models/__init__.py
@@ -0,0 +1,4 @@
+from . import teams
+from . import task_stages
+from . import project
+from . import project_task
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/models/project.py b/addons_extensions/project_task_timesheet_extended/models/project.py
new file mode 100644
index 000000000..cb0244d2b
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/models/project.py
@@ -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)],
+ },
+ }
+
diff --git a/addons_extensions/project_task_timesheet_extended/models/project_task.py b/addons_extensions/project_task_timesheet_extended/models/project_task.py
new file mode 100644
index 000000000..4af1a847b
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/models/project_task.py
@@ -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
diff --git a/addons_extensions/project_task_timesheet_extended/models/task_stages.py b/addons_extensions/project_task_timesheet_extended/models/task_stages.py
new file mode 100644
index 000000000..c8e629713
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/models/task_stages.py
@@ -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')])
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/models/teams.py b/addons_extensions/project_task_timesheet_extended/models/teams.py
new file mode 100644
index 000000000..8c7facac6
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/models/teams.py
@@ -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
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv b/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv
new file mode 100644
index 000000000..7a3a73591
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/security/ir.model.access.csv
@@ -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
diff --git a/addons_extensions/project_task_timesheet_extended/security/security.xml b/addons_extensions/project_task_timesheet_extended/security/security.xml
new file mode 100644
index 000000000..ec6ac9ab6
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/security/security.xml
@@ -0,0 +1,70 @@
+
+
+
+ Manager
+
+
+
+
+
+
+
+
+
+
+ Manager: Own Projects
+
+
+ [('user_id', '=', user.id)]
+
+
+
+
+
+
+
+ Project/Task: project supervisor: see all tasks linked to his assigned project or its own tasks
+
+ [
+ ('project_id.user_id','=',user.id),
+ '|', ('project_id', '!=', False),
+ ('user_ids', 'in', user.id),
+ ]
+
+
+
+
+ Project/Task: project users: don't see non generic tasks
+
+ [
+ '&', '&',
+ ('project_id', '!=', False),
+ ('is_generic', '=', False),
+ ('user_ids', 'not in', user.id),
+ ]
+
+
+
+
+
+
+ Project/Task: project lead: see all tasks
+
+ [
+ '&', '&', '&',
+ ('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)
+ ]
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/view/project.xml b/addons_extensions/project_task_timesheet_extended/view/project.xml
new file mode 100644
index 000000000..9ff099542
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/view/project.xml
@@ -0,0 +1,39 @@
+
+
+
+ project.project.inherit.form.view
+ project.project
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/view/project_task.xml b/addons_extensions/project_task_timesheet_extended/view/project_task.xml
new file mode 100644
index 000000000..ef64027e6
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/view/project_task.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+ project.task.form.inherit
+ project.task
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/view/task_stages.xml b/addons_extensions/project_task_timesheet_extended/view/task_stages.xml
new file mode 100644
index 000000000..04e8743dc
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/view/task_stages.xml
@@ -0,0 +1,15 @@
+
+
+
+ project.task.type.list.inherit
+ project.task.type
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/view/teams.xml b/addons_extensions/project_task_timesheet_extended/view/teams.xml
new file mode 100644
index 000000000..470c850e9
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/view/teams.xml
@@ -0,0 +1,70 @@
+
+
+
+ internal.teams.list.view
+ internal.teams
+
+
+
+
+
+
+
+
+
+ internal.teams.form.view
+ internal.teams
+
+
+
+
+
+
+ Internal Teams
+ internal.teams
+ form
+ list,form
+
+
+
+
+
+
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/wizards/__init__.py b/addons_extensions/project_task_timesheet_extended/wizards/__init__.py
new file mode 100644
index 000000000..80528d740
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/wizards/__init__.py
@@ -0,0 +1 @@
+from . import project_user_assign_wizard
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/wizards/project_user_assign_wizard.py b/addons_extensions/project_task_timesheet_extended/wizards/project_user_assign_wizard.py
new file mode 100644
index 000000000..a9d957c0a
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/wizards/project_user_assign_wizard.py
@@ -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'}
\ No newline at end of file
diff --git a/addons_extensions/project_task_timesheet_extended/wizards/project_user_assign_wizard.xml b/addons_extensions/project_task_timesheet_extended/wizards/project_user_assign_wizard.xml
new file mode 100644
index 000000000..3cc81d594
--- /dev/null
+++ b/addons_extensions/project_task_timesheet_extended/wizards/project_user_assign_wizard.xml
@@ -0,0 +1,92 @@
+
+
+
+ project.user.assign.wizard.form.view
+ project.user.assign.wizard
+
+
+
+
+
+
+ Project User Assignment
+ project.user.assign.wizard
+ form
+ list,form
+
+
+
\ No newline at end of file
diff --git a/third_party_addons/dodger_blue/__init__.py b/third_party_addons/dodger_blue/__init__.py
index 52980b464..38718f084 100644
--- a/third_party_addons/dodger_blue/__init__.py
+++ b/third_party_addons/dodger_blue/__init__.py
@@ -1 +1,2 @@
-# from . import models
\ No newline at end of file
+from . import models
+from . import controllers
\ No newline at end of file
diff --git a/third_party_addons/dodger_blue/__manifest__.py b/third_party_addons/dodger_blue/__manifest__.py
index f4b1b6ed9..d4b3cf4bb 100644
--- a/third_party_addons/dodger_blue/__manifest__.py
+++ b/third_party_addons/dodger_blue/__manifest__.py
@@ -12,7 +12,7 @@
"depends":['web','base'],
"data": [
'views/login_templates.xml',
- # 'views/ir_menu.xml'
+ 'views/ir_menu.xml'
],
'assets': {
'web.assets_frontend': [
@@ -25,6 +25,11 @@
'dodger_blue/static/src/scss/colors.scss',
'dodger_blue/static/src/scss/theme_style_backend.scss',
'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',
diff --git a/third_party_addons/dodger_blue/controllers/__init__.py b/third_party_addons/dodger_blue/controllers/__init__.py
new file mode 100644
index 000000000..a03bfd097
--- /dev/null
+++ b/third_party_addons/dodger_blue/controllers/__init__.py
@@ -0,0 +1 @@
+from . import controllers
\ No newline at end of file
diff --git a/third_party_addons/dodger_blue/controllers/controllers.py b/third_party_addons/dodger_blue/controllers/controllers.py
new file mode 100644
index 000000000..3701267f7
--- /dev/null
+++ b/third_party_addons/dodger_blue/controllers/controllers.py
@@ -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
diff --git a/third_party_addons/dodger_blue/models/model.py b/third_party_addons/dodger_blue/models/model.py
index 3558b2a45..517bcde28 100644
--- a/third_party_addons/dodger_blue/models/model.py
+++ b/third_party_addons/dodger_blue/models/model.py
@@ -5,62 +5,4 @@ from collections import defaultdict
class IrUiMenu(models.Model):
_inherit = 'ir.ui.menu'
- is_not_main_menu = fields.Boolean()
-
- @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)
\ No newline at end of file
+ quick_user_access = fields.Many2many('res.users',string="Quick Access")
\ No newline at end of file
diff --git a/third_party_addons/dodger_blue/static/src/js/quick_access_button.js b/third_party_addons/dodger_blue/static/src/js/quick_access_button.js
new file mode 100644
index 000000000..108eb6771
--- /dev/null
+++ b/third_party_addons/dodger_blue/static/src/js/quick_access_button.js
@@ -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 = `
+
+ Failed to load data. Please try again.
+
+ `;
+ }
+ });
+ // 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;
+ }
+}
\ No newline at end of file
diff --git a/third_party_addons/dodger_blue/static/src/js/quick_access_setup.js b/third_party_addons/dodger_blue/static/src/js/quick_access_setup.js
new file mode 100644
index 000000000..9277b508c
--- /dev/null
+++ b/third_party_addons/dodger_blue/static/src/js/quick_access_setup.js
@@ -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");
\ No newline at end of file
diff --git a/third_party_addons/dodger_blue/static/src/scss/quick_access.scss b/third_party_addons/dodger_blue/static/src/scss/quick_access.scss
new file mode 100644
index 000000000..34de919b5
--- /dev/null
+++ b/third_party_addons/dodger_blue/static/src/scss/quick_access.scss
@@ -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;
+// }
+// }
+// }
+// }
+// }
\ No newline at end of file
diff --git a/third_party_addons/dodger_blue/static/src/scss/theme_style_backend.scss b/third_party_addons/dodger_blue/static/src/scss/theme_style_backend.scss
index f056bcdb0..df644fa12 100644
--- a/third_party_addons/dodger_blue/static/src/scss/theme_style_backend.scss
+++ b/third_party_addons/dodger_blue/static/src/scss/theme_style_backend.scss
@@ -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;
}
+.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 {
content: " ";
display: block;
@@ -944,6 +952,7 @@ a.dropdown-item.o_app.cybro-mainmenu {
border: none !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 {
color: $bg-white !important;
font-size: 1.08333333rem;
@@ -999,6 +1008,43 @@ h4.o_onboarding_step_title.mt16 a {
cursor: pointer;
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 {
color: $bg-white !important;
}
@@ -1480,10 +1526,133 @@ top:109x !important;
.o_menu_sections a{
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{
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{
color:$primary-color !important;
}
@@ -1541,7 +1710,11 @@ padding-left: 25px !important;
}
.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;
padding: 10px 13px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
- background-color: #343a4f; // darker header strip
+// background-color: #343a4f; // darker header strip
overflow: hidden;
flex-shrink: 0; /* Prevents shrinking */
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
.sidebar-main-menus {
padding: 15px 0;
diff --git a/third_party_addons/dodger_blue/static/src/xml/quick_access_button.xml b/third_party_addons/dodger_blue/static/src/xml/quick_access_button.xml
new file mode 100644
index 000000000..e1cf963d5
--- /dev/null
+++ b/third_party_addons/dodger_blue/static/src/xml/quick_access_button.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/third_party_addons/dodger_blue/static/src/xml/sidebar_menu_icon_templates.xml b/third_party_addons/dodger_blue/static/src/xml/sidebar_menu_icon_templates.xml
index 98b42246c..031e0a796 100644
--- a/third_party_addons/dodger_blue/static/src/xml/sidebar_menu_icon_templates.xml
+++ b/third_party_addons/dodger_blue/static/src/xml/sidebar_menu_icon_templates.xml
@@ -3,64 +3,64 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -125,9 +125,23 @@
-
+
+
+
+
+
-
+
@@ -150,6 +167,18 @@
+
+
+
diff --git a/third_party_addons/dodger_blue/views/ir_menu.xml b/third_party_addons/dodger_blue/views/ir_menu.xml
index 13497c9a6..e9377a906 100644
--- a/third_party_addons/dodger_blue/views/ir_menu.xml
+++ b/third_party_addons/dodger_blue/views/ir_menu.xml
@@ -7,8 +7,10 @@
ir.ui.menu
-
-
+
+
+
+