PROJECT MODULE AND THEME ADDED IN SHARED MODULES
This commit is contained in:
parent
c3883f6a18
commit
5f267e96da
|
|
@ -0,0 +1,2 @@
|
|||
from . import models, wizards
|
||||
from .hooks import post_init_hook
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
from . import teams
|
||||
from . import task_stages
|
||||
from . import project
|
||||
from . import project_task
|
||||
|
|
@ -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)],
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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')])
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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">[
|
||||
'&', '&',
|
||||
('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">[
|
||||
'&', '&', '&',
|
||||
('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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import project_user_assign_wizard
|
||||
|
|
@ -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'}
|
||||
|
|
@ -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>
|
||||
|
|
@ -1 +1,2 @@
|
|||
# from . import models
|
||||
from . import models
|
||||
from . import controllers
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from . import controllers
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
quick_user_access = fields.Many2many('res.users',string="Quick Access")
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
@ -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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
<!-- <!– Position the dropdown above the button –>-->
|
||||
<!-- <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>
|
||||
|
|
@ -3,64 +3,64 @@
|
|||
<!-- Template for sidebar menu icon-->
|
||||
<t t-inherit="web.NavBar.AppsMenu" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//Dropdown" position="replace">
|
||||
<ul class="o_menu_apps">
|
||||
<li class="dropdown show">
|
||||
<a class="full" data-bs-toggle="collapse"
|
||||
data-bs-target="#Appmenu" aria-expanded="true"
|
||||
href="#"
|
||||
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"/>
|
||||
</a>
|
||||
<div class="dropdown-menu cybro-main-menu collapse"
|
||||
id="Appmenu" role="menu" x-placement="top-start"
|
||||
style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(5px, -1px, 0px);">
|
||||
<div class="sidebar-user">
|
||||
<t t-set="user_img"
|
||||
t-value="'/web/image?model=res.users&field=image_1920&id='+user_id"/>
|
||||
<img t-att-src="user_img" alt="User Avatar" loading="lazy"/>
|
||||
<div class="sidebar-username">
|
||||
<span t-esc="user_name"/>
|
||||
</div>
|
||||
</div>
|
||||
<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"
|
||||
t-on-input="_searchMenusSchedule">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<i class="fa fa-search"/>
|
||||
</div>
|
||||
</div>
|
||||
<input type="search"
|
||||
autocomplete="off"
|
||||
placeholder="Search menus..."
|
||||
class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-results col-md-10 ml-auto mr-auto"/>
|
||||
</div>
|
||||
<div class="nav-container">
|
||||
<div class="app-menu">
|
||||
<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"
|
||||
t-att-data-menu-id="app.menuID" t-att-data-menu-xmlid="app.xmlID"
|
||||
t-att-data-action-id="app.actionID">
|
||||
<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;"
|
||||
t-attf-src="{{app.webIconData}}"/>
|
||||
<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;"
|
||||
t-attf-src="{{app.webIconData}}"/>
|
||||
<span class="a_app_menu_title">
|
||||
<t t-esc="app.name"/>
|
||||
</span>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- <ul class="o_menu_apps">-->
|
||||
<!-- <li class="dropdown show">-->
|
||||
<!-- <a class="full" data-bs-toggle="collapse"-->
|
||||
<!-- data-bs-target="#Appmenu" aria-expanded="true"-->
|
||||
<!-- href="#"-->
|
||||
<!-- 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"/>-->
|
||||
<!-- </a>-->
|
||||
<!-- <div class="dropdown-menu cybro-main-menu collapse"-->
|
||||
<!-- id="Appmenu" role="menu" x-placement="top-start"-->
|
||||
<!-- style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(5px, -1px, 0px);">-->
|
||||
<!-- <div class="sidebar-user">-->
|
||||
<!-- <t t-set="user_img"-->
|
||||
<!-- t-value="'/web/image?model=res.users&field=image_1920&id='+user_id"/>-->
|
||||
<!-- <img t-att-src="user_img" alt="User Avatar" loading="lazy"/>-->
|
||||
<!-- <div class="sidebar-username">-->
|
||||
<!-- <span t-esc="user_name"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <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"-->
|
||||
<!-- t-on-input="_searchMenusSchedule">-->
|
||||
<!-- <div class="input-group">-->
|
||||
<!-- <div class="input-group-prepend">-->
|
||||
<!-- <div class="input-group-text">-->
|
||||
<!-- <i class="fa fa-search"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <input type="search"-->
|
||||
<!-- autocomplete="off"-->
|
||||
<!-- placeholder="Search menus..."-->
|
||||
<!-- class="form-control"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="search-results col-md-10 ml-auto mr-auto"/>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="nav-container">-->
|
||||
<!-- <div class="app-menu">-->
|
||||
<!-- <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"-->
|
||||
<!-- t-att-data-menu-id="app.menuID" t-att-data-menu-xmlid="app.xmlID"-->
|
||||
<!-- t-att-data-action-id="app.actionID">-->
|
||||
<!-- <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;"-->
|
||||
<!-- t-attf-src="{{app.webIconData}}"/>-->
|
||||
<!-- <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;"-->
|
||||
<!-- t-attf-src="{{app.webIconData}}"/>-->
|
||||
<!-- <span class="a_app_menu_title">-->
|
||||
<!-- <t t-esc="app.name"/>-->
|
||||
<!-- </span>-->
|
||||
<!-- </a>-->
|
||||
<!-- </t>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </li>-->
|
||||
<!-- </ul>-->
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
|
|
@ -125,9 +125,23 @@
|
|||
<div class="sidebar-companyname">FTPROTECH</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">
|
||||
<li>
|
||||
<li class="sidebar-menu-item">
|
||||
<a role="menuitem"
|
||||
t-att-href="app.actionPath ? `/odoo/${app.actionPath}` : `/web#action=${app.actionID}`"
|
||||
class="dropdown-item o_app mt0"
|
||||
|
|
@ -139,10 +153,13 @@
|
|||
t-att-title="app.name"
|
||||
class="o-app-icon"
|
||||
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"
|
||||
class="o-app-icon"
|
||||
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"/>
|
||||
</div>
|
||||
</a>
|
||||
|
|
@ -150,6 +167,18 @@
|
|||
</t>
|
||||
</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>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@
|
|||
<field name="model">ir.ui.menu</field>
|
||||
<field name="inherit_id" ref="base.edit_menu_access"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='sequence']" position="after">
|
||||
<field name="is_not_main_menu"/>
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page name="quick_access" string="Quick Access">
|
||||
<field name="quick_user_access" widget="many2many_tags"/>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
Loading…
Reference in New Issue