PMT, UNIVERSAL ATTACHMENT PREVIEW, MENU CONTROL CENTER INTEGRATION
This commit is contained in:
parent
c93d208990
commit
6f77059f85
|
|
@ -15,6 +15,7 @@
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/data.xml',
|
'data/data.xml',
|
||||||
'views/masters.xml',
|
'views/masters.xml',
|
||||||
|
'views/groups.xml',
|
||||||
'views/login.xml',
|
'views/login.xml',
|
||||||
'views/menu_access_control_views.xml',
|
'views/menu_access_control_views.xml',
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ class CustomMasterLogin(Home):
|
||||||
# load your masters
|
# load your masters
|
||||||
masters = request.env['master.control'].sudo().search([])
|
masters = request.env['master.control'].sudo().search([])
|
||||||
response.qcontext['masters'] = masters
|
response.qcontext['masters'] = masters
|
||||||
|
request.env['ir.ui.menu'].sudo().clear_caches()
|
||||||
|
request.env['ir.ui.menu'].sudo()._visible_menu_ids()
|
||||||
|
|
||||||
# After successful login
|
# After successful login
|
||||||
if request.session.uid and master_selected:
|
if request.session.uid and master_selected:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
from . import masters
|
from . import masters
|
||||||
|
from . import ir_http
|
||||||
|
from . import groups
|
||||||
from . import models
|
from . import models
|
||||||
from . import menu
|
from . import menu
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.http import request
|
||||||
|
|
||||||
|
|
||||||
|
class ResGroups(models.Model):
|
||||||
|
_inherit = 'res.groups'
|
||||||
|
|
||||||
|
is_visible_for_master = fields.Boolean(
|
||||||
|
compute='_compute_is_visible_for_master',
|
||||||
|
search='_search_is_visible_for_master'
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends_context('active_master')
|
||||||
|
def _compute_is_visible_for_master(self):
|
||||||
|
master_code = request.session.active_master
|
||||||
|
if not master_code:
|
||||||
|
self.update({'is_visible_for_master': True})
|
||||||
|
return
|
||||||
|
|
||||||
|
master_control = self.env['master.control'].sudo().search([
|
||||||
|
('code', '=', master_code)
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
if not master_control:
|
||||||
|
self.update({'is_visible_for_master': True})
|
||||||
|
return
|
||||||
|
|
||||||
|
# If NO access_group_ids -> show ALL groups
|
||||||
|
if not master_control.access_group_ids:
|
||||||
|
self.update({'is_visible_for_master': True})
|
||||||
|
return
|
||||||
|
|
||||||
|
visible_group_ids = master_control.access_group_ids.filtered(
|
||||||
|
lambda line: line.show_group
|
||||||
|
).mapped('group_id').ids
|
||||||
|
|
||||||
|
# If there are no 'show_group = True' -> show ALL groups
|
||||||
|
if not visible_group_ids:
|
||||||
|
self.update({'is_visible_for_master': True})
|
||||||
|
return
|
||||||
|
|
||||||
|
for group in self:
|
||||||
|
group.is_visible_for_master = group.id in visible_group_ids
|
||||||
|
|
||||||
|
|
||||||
|
def _search_is_visible_for_master(self, operator, value):
|
||||||
|
if operator != '=' or not value:
|
||||||
|
return []
|
||||||
|
|
||||||
|
master_code = request.session.active_master
|
||||||
|
if not master_code:
|
||||||
|
return []
|
||||||
|
|
||||||
|
master_control = self.env['master.control'].sudo().search([
|
||||||
|
('code', '=', master_code)
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
if not master_control:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# If NO access_group_ids → show ALL groups → no domain filter
|
||||||
|
if not master_control.access_group_ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
visible_group_ids = master_control.access_group_ids.filtered(
|
||||||
|
lambda line: line.show_group
|
||||||
|
).mapped('group_id').ids
|
||||||
|
|
||||||
|
# If none of the lines have show_group=True → show ALL
|
||||||
|
if not visible_group_ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [('id', 'in', visible_group_ids)]
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import odoo
|
||||||
|
from odoo import api, models, fields
|
||||||
|
from odoo.http import request
|
||||||
|
from odoo import http
|
||||||
|
|
||||||
|
class IrHttp(models.AbstractModel):
|
||||||
|
_inherit = 'ir.http'
|
||||||
|
|
||||||
|
def session_info(self):
|
||||||
|
info = super().session_info()
|
||||||
|
active_master = http.request.session.get("active_master")
|
||||||
|
if active_master:
|
||||||
|
info['user_context']['active_master'] = active_master
|
||||||
|
else:
|
||||||
|
info['user_context']['active_master'] = ''
|
||||||
|
return info
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from odoo import models, fields, _
|
from odoo import models, fields, _, api
|
||||||
|
|
||||||
class MasterControl(models.Model):
|
class MasterControl(models.Model):
|
||||||
_name = 'master.control'
|
_name = 'master.control'
|
||||||
|
|
@ -10,6 +10,13 @@ class MasterControl(models.Model):
|
||||||
default_show = fields.Boolean(default=True)
|
default_show = fields.Boolean(default=True)
|
||||||
access_group_ids = fields.One2many('group.access.line','master_control_id',string='Roles')
|
access_group_ids = fields.One2many('group.access.line','master_control_id',string='Roles')
|
||||||
|
|
||||||
|
@api.depends('name', 'code')
|
||||||
|
def _compute_display_name(self):
|
||||||
|
for record in self:
|
||||||
|
if record.name:
|
||||||
|
record.display_name = record.name + (f' ({record.code})' if record.code else '')
|
||||||
|
else:
|
||||||
|
record.display_name = False
|
||||||
|
|
||||||
def action_generate_groups(self):
|
def action_generate_groups(self):
|
||||||
"""Generate category → groups list"""
|
"""Generate category → groups list"""
|
||||||
|
|
@ -42,6 +49,8 @@ class MasterControl(models.Model):
|
||||||
# UPDATE GROUPS (Detect new groups)
|
# UPDATE GROUPS (Detect new groups)
|
||||||
# -----------------------------------------
|
# -----------------------------------------
|
||||||
def action_update_groups(self):
|
def action_update_groups(self):
|
||||||
|
import pdb
|
||||||
|
pdb.set_trace()
|
||||||
for rec in self:
|
for rec in self:
|
||||||
created_count = 0
|
created_count = 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
from passlib.apps import master_context
|
||||||
|
|
||||||
from odoo import models, fields, api, tools, _
|
from odoo import models, fields, api, tools, _
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from odoo.http import request
|
||||||
|
|
||||||
class IrUiMenu(models.Model):
|
class IrUiMenu(models.Model):
|
||||||
_inherit = 'ir.ui.menu'
|
_inherit = 'ir.ui.menu'
|
||||||
|
|
@ -8,41 +10,100 @@ class IrUiMenu(models.Model):
|
||||||
@api.model
|
@api.model
|
||||||
@tools.ormcache('frozenset(self.env.user.groups_id.ids)', 'debug')
|
@tools.ormcache('frozenset(self.env.user.groups_id.ids)', 'debug')
|
||||||
def _visible_menu_ids(self, debug=False):
|
def _visible_menu_ids(self, debug=False):
|
||||||
""" Return the ids of the menu items visible to the user. """
|
"""Return the IDs of menu items visible to the current user based on permissions and active master."""
|
||||||
# retrieve all menus, and determine which ones are visible
|
# Clear existing cache to ensure fresh menu visibility calculation
|
||||||
|
self.env['ir.ui.menu'].sudo().clear_caches()
|
||||||
|
|
||||||
|
# Retrieve all menus with required fields
|
||||||
context = {'ir.ui.menu.full_list': True}
|
context = {'ir.ui.menu.full_list': True}
|
||||||
menus = self.with_context(context).search_fetch([], ['action', 'parent_id']).sudo()
|
menus = self.with_context(context).search_fetch([], ['action', 'parent_id']).sudo()
|
||||||
|
|
||||||
# first discard all menus with groups the user does not have
|
# Get active master and control configuration
|
||||||
|
active_master = request.session.get('active_master')
|
||||||
|
master_control = False
|
||||||
|
control_unit = False
|
||||||
|
|
||||||
|
if active_master:
|
||||||
|
master_control = self.env['master.control'].sudo().search(
|
||||||
|
[('code', '=', active_master)], limit=1
|
||||||
|
)
|
||||||
|
if master_control:
|
||||||
|
control_unit = self.env['menu.access.control'].sudo().search([
|
||||||
|
('user_ids', 'ilike', self.env.user.id),
|
||||||
|
('master_control', '=', master_control.id)
|
||||||
|
])
|
||||||
|
|
||||||
|
# Get user groups and exclude technical group in non-debug mode
|
||||||
group_ids = set(self.env.user._get_group_ids())
|
group_ids = set(self.env.user._get_group_ids())
|
||||||
if not debug:
|
if not debug:
|
||||||
parent_menus = self.env['menu.access.control'].sudo().search([('user_ids','ilike',self.env.user.id)]).access_menu_line_ids.filtered(lambda menu: not(menu.is_main_menu)).menu_id.ids
|
group_ids -= {
|
||||||
sub_menus = self.env['menu.access.control'].sudo().search([('user_ids','ilike',self.env.user.id)]).access_sub_menu_line_ids.filtered(lambda menu: not(menu.is_main_menu)).menu_id.ids
|
self.env['ir.model.data']._xmlid_to_res_id(
|
||||||
|
'base.group_no_one', raise_if_not_found=False
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
hide_menus_list = list(set(parent_menus + sub_menus))
|
# Filter menus by group permissions
|
||||||
|
|
||||||
menus = menus.filtered(lambda menu: (menu.id not in hide_menus_list))
|
|
||||||
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(
|
menus = menus.filtered(
|
||||||
lambda menu: not (menu.groups_id and group_ids.isdisjoint(menu.groups_id._ids)))
|
lambda menu: not (menu.groups_id and group_ids.isdisjoint(menu.groups_id._ids))
|
||||||
|
)
|
||||||
|
|
||||||
# take apart menus that have an action
|
# Determine menus to hide based on access control
|
||||||
|
hide_menus_list = self._get_hidden_menu_ids(control_unit, master_control, debug)
|
||||||
|
menus = menus.filtered(lambda menu: menu.id not in hide_menus_list)
|
||||||
|
|
||||||
|
# Process menus with actions
|
||||||
|
visible = self._process_action_menus(menus)
|
||||||
|
|
||||||
|
return set(visible.ids)
|
||||||
|
|
||||||
|
def _get_hidden_menu_ids(self, control_unit, master_control, debug):
|
||||||
|
"""Helper method to determine menu IDs that should be hidden from the user."""
|
||||||
|
if debug and control_unit:
|
||||||
|
# In debug mode with control unit, use its specific menu restrictions
|
||||||
|
parent_menus = control_unit.access_menu_line_ids.filtered(
|
||||||
|
lambda menu: not menu.is_main_menu
|
||||||
|
).menu_id.ids
|
||||||
|
sub_menus = control_unit.access_sub_menu_line_ids.filtered(
|
||||||
|
lambda menu: not menu.is_main_menu
|
||||||
|
).menu_id.ids
|
||||||
|
elif not debug:
|
||||||
|
# In non-debug mode, determine menus to hide based on control configuration
|
||||||
|
domain = [('user_ids', 'ilike', self.env.user.id)]
|
||||||
|
if master_control:
|
||||||
|
domain.append(('master_control', '=', master_control.id))
|
||||||
|
|
||||||
|
access_controls = self.env['menu.access.control'].sudo().search(domain)
|
||||||
|
parent_menus = access_controls.access_menu_line_ids.filtered(
|
||||||
|
lambda menu: not menu.is_main_menu
|
||||||
|
).menu_id.ids
|
||||||
|
sub_menus = access_controls.access_sub_menu_line_ids.filtered(
|
||||||
|
lambda menu: not menu.is_main_menu
|
||||||
|
).menu_id.ids
|
||||||
|
else:
|
||||||
|
# Default case: no menus to hide
|
||||||
|
return []
|
||||||
|
|
||||||
|
return list(set(parent_menus + sub_menus))
|
||||||
|
|
||||||
|
def _process_action_menus(self, menus):
|
||||||
|
"""Process menus with actions and determine visibility based on model access."""
|
||||||
|
# Separate menus with actions from folder menus
|
||||||
actions_by_model = defaultdict(set)
|
actions_by_model = defaultdict(set)
|
||||||
for action in menus.mapped('action'):
|
for action in menus.mapped('action'):
|
||||||
if action:
|
if action:
|
||||||
actions_by_model[action._name].add(action.id)
|
actions_by_model[action._name].add(action.id)
|
||||||
|
|
||||||
existing_actions = {
|
existing_actions = {
|
||||||
action
|
action
|
||||||
for model_name, action_ids in actions_by_model.items()
|
for model_name, action_ids in actions_by_model.items()
|
||||||
for action in self.env[model_name].browse(action_ids).exists()
|
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)
|
action_menus = menus.filtered(lambda m: m.action and m.action in existing_actions)
|
||||||
folder_menus = menus - action_menus
|
folder_menus = menus - action_menus
|
||||||
visible = self.browse()
|
visible = self.browse()
|
||||||
|
|
||||||
# process action menus, check whether their action is allowed
|
# Model access check configuration
|
||||||
access = self.env['ir.model.access']
|
access = self.env['ir.model.access']
|
||||||
MODEL_BY_TYPE = {
|
MODEL_BY_TYPE = {
|
||||||
'ir.actions.act_window': 'res_model',
|
'ir.actions.act_window': 'res_model',
|
||||||
|
|
@ -50,21 +111,22 @@ class IrUiMenu(models.Model):
|
||||||
'ir.actions.server': 'model_name',
|
'ir.actions.server': 'model_name',
|
||||||
}
|
}
|
||||||
|
|
||||||
# performance trick: determine the ids to prefetch by type
|
# Prefetch action data for performance
|
||||||
prefetch_ids = defaultdict(list)
|
prefetch_ids = defaultdict(list)
|
||||||
for action in action_menus.mapped('action'):
|
for action in action_menus.mapped('action'):
|
||||||
prefetch_ids[action._name].append(action.id)
|
prefetch_ids[action._name].append(action.id)
|
||||||
|
|
||||||
|
# Check access for each action menu
|
||||||
for menu in action_menus:
|
for menu in action_menus:
|
||||||
action = menu.action
|
action = menu.action.with_prefetch(prefetch_ids[action._name])
|
||||||
action = action.with_prefetch(prefetch_ids[action._name])
|
|
||||||
model_name = action._name in MODEL_BY_TYPE and action[MODEL_BY_TYPE[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)
|
if not model_name or access.check(model_name, 'read', False):
|
||||||
|
# Make menu visible and its folder ancestors
|
||||||
|
visible += menu
|
||||||
|
parent = menu.parent_id
|
||||||
|
while parent and parent in folder_menus and parent not in visible:
|
||||||
|
visible += parent
|
||||||
|
parent = parent.parent_id
|
||||||
|
|
||||||
|
return visible
|
||||||
|
|
@ -28,11 +28,12 @@ class MenuAccessControl(models.Model):
|
||||||
_rec_name = 'control_unit'
|
_rec_name = 'control_unit'
|
||||||
|
|
||||||
_sql_constraints = [
|
_sql_constraints = [
|
||||||
('unique_control_unit', 'UNIQUE(control_unit)', "Only one service can exist with a specific control_unit. Please don't confuse me 🤪.")
|
('unique_control_unit', 'UNIQUE(control_unit, master_control)', "Only one service can exist with a specific control_unit & Master. Please don't confuse me 🤪.")
|
||||||
]
|
]
|
||||||
|
|
||||||
control_unit = fields.Many2one('menu.control.units',required=True)
|
control_unit = fields.Many2one('menu.control.units',required=True)
|
||||||
user_ids = fields.Many2many('res.users', string="Users", related='control_unit.user_ids')
|
user_ids = fields.Many2many('res.users', string="Users", related='control_unit.user_ids')
|
||||||
|
master_control = fields.Many2one('master.control')
|
||||||
|
|
||||||
|
|
||||||
access_menu_line_ids = fields.One2many(
|
access_menu_line_ids = fields.One2many(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="base.action_res_groups" model="ir.actions.act_window">
|
||||||
|
<field name="name">Roles</field>
|
||||||
|
<field name="res_model">res.groups</field>
|
||||||
|
<field name="domain">[('is_visible_for_master', '=', True)]</field>
|
||||||
|
<!-- <field name="context">{'search_default_filter_no_share': 1}</field>-->
|
||||||
|
<!-- <field name="help">A group is a set of functional areas that will be assigned to the user in order to give-->
|
||||||
|
<!-- them access and rights to specific applications and tasks in the system. You can create custom groups or-->
|
||||||
|
<!-- edit the ones existing by default in order to customize the view of the menu that users will be able to-->
|
||||||
|
<!-- see. Whether they can have a read, write, create and delete access right can be managed from here.-->
|
||||||
|
<!-- </field>-->
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem action="base.action_res_groups" name="Roles" id="base.menu_action_res_groups" parent="base.menu_users" groups="base.group_user" sequence="3"/>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -69,6 +69,7 @@
|
||||||
<list>
|
<list>
|
||||||
<field name="control_unit"/>
|
<field name="control_unit"/>
|
||||||
<field name="user_ids" widget="many2many_tags"/>
|
<field name="user_ids" widget="many2many_tags"/>
|
||||||
|
<field name="master_control"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
@ -80,6 +81,7 @@
|
||||||
<sheet>
|
<sheet>
|
||||||
<group>
|
<group>
|
||||||
<field name="control_unit"/>
|
<field name="control_unit"/>
|
||||||
|
<field name="master_control"/>
|
||||||
<field name="user_ids" widget="many2many_tags"/>
|
<field name="user_ids" widget="many2many_tags"/>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
|
|
|
||||||
|
|
@ -32,13 +32,17 @@ Key Features:
|
||||||
'security/security.xml',
|
'security/security.xml',
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/data.xml',
|
'data/data.xml',
|
||||||
|
'data/project_roles_data.xml',
|
||||||
'wizards/project_user_assign_wizard.xml',
|
'wizards/project_user_assign_wizard.xml',
|
||||||
|
'wizards/roles_user_assign_wizard.xml',
|
||||||
'wizards/internal_team_members_wizard.xml',
|
'wizards/internal_team_members_wizard.xml',
|
||||||
'wizards/project_stage_update_wizard.xml',
|
'wizards/project_stage_update_wizard.xml',
|
||||||
'wizards/task_reject_reason_wizard.xml',
|
'wizards/task_reject_reason_wizard.xml',
|
||||||
'view/teams.xml',
|
'view/teams.xml',
|
||||||
|
'view/project_roles_master.xml',
|
||||||
'view/project_stages.xml',
|
'view/project_stages.xml',
|
||||||
'view/task_stages.xml',
|
'view/task_stages.xml',
|
||||||
|
'view/deployment_log.xml',
|
||||||
'view/project.xml',
|
'view/project.xml',
|
||||||
'view/project_task.xml',
|
'view/project_task.xml',
|
||||||
'view/timesheets.xml',
|
'view/timesheets.xml',
|
||||||
|
|
@ -47,6 +51,9 @@ Key Features:
|
||||||
# 'view/project_task_gantt.xml',
|
# 'view/project_task_gantt.xml',
|
||||||
],
|
],
|
||||||
'assets': {
|
'assets': {
|
||||||
|
'web.assets_backend':{
|
||||||
|
'project_task_timesheet_extended/static/src/css/delopyment.css'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'application': False,
|
'application': False,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Administrative Roles -->
|
||||||
|
<record id="role_admin" model="project.role">
|
||||||
|
<field name="name">Admin</field>
|
||||||
|
<field name="role_level">administrative</field>
|
||||||
|
<field name="description">Full system access
|
||||||
|
Create and manage projects
|
||||||
|
Configure applications and integrations
|
||||||
|
Validate submitted work when required
|
||||||
|
Override approvals when necessary
|
||||||
|
Manage user access and permissions</field>
|
||||||
|
<field name="color">10</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="role_program_manager" model="project.role">
|
||||||
|
<field name="name">Program Manager</field>
|
||||||
|
<field name="role_level">administrative</field>
|
||||||
|
<field name="description">Oversee multiple projects
|
||||||
|
Review progress and performance
|
||||||
|
Coordinate resource distribution
|
||||||
|
Support project-level decision-making</field>
|
||||||
|
<field name="color">11</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Project Leadership Roles -->
|
||||||
|
<record id="role_project_manager" model="project.role">
|
||||||
|
<field name="name">Project Manager</field>
|
||||||
|
<field name="role_level">leadership</field>
|
||||||
|
<field name="description">Lead one or more projects
|
||||||
|
Approve development, QA, and deployment stages
|
||||||
|
Manage scope, timeline, budget, and deliverables
|
||||||
|
Validate final project closure
|
||||||
|
Coordinate communication with stakeholders</field>
|
||||||
|
<field name="color">5</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="role_project_lead" model="project.role">
|
||||||
|
<field name="name">Project Lead</field>
|
||||||
|
<field name="role_level">leadership</field>
|
||||||
|
<field name="description">Manage technical execution of the project
|
||||||
|
Validate development output
|
||||||
|
Review and merge code
|
||||||
|
Coordinate with QA teams
|
||||||
|
Support Team Leads in execution</field>
|
||||||
|
<field name="color">6</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="role_team_lead" model="project.role">
|
||||||
|
<field name="name">Team Lead</field>
|
||||||
|
<field name="role_level">leadership</field>
|
||||||
|
<field name="description">Lead module-level or team-level execution
|
||||||
|
Assign tasks to Developers and QA
|
||||||
|
Validate development work
|
||||||
|
Monitor daily progress
|
||||||
|
Ensure delivery quality and adherence to process</field>
|
||||||
|
<field name="color">7</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="role_financial_manager" model="project.role">
|
||||||
|
<field name="name">Financial Manager</field>
|
||||||
|
<field name="role_level">leadership</field>
|
||||||
|
<field name="description">Manage project budgets
|
||||||
|
Validate financial feasibility
|
||||||
|
Approve expense-related considerations</field>
|
||||||
|
<field name="color">8</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Execution Roles (Employee-Level Roles) -->
|
||||||
|
<record id="role_qa_lead" model="project.role">
|
||||||
|
<field name="name">QA Lead</field>
|
||||||
|
<field name="role_level">execution</field>
|
||||||
|
<field name="description">Oversees all QA activity
|
||||||
|
Coordinates test cycles
|
||||||
|
Validates QA results
|
||||||
|
Ensures quality standards are met</field>
|
||||||
|
<field name="color">3</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="role_qa_engineer" model="project.role">
|
||||||
|
<field name="name">QA Engineer / Tester</field>
|
||||||
|
<field name="role_level">execution</field>
|
||||||
|
<field name="description">Executes test cases
|
||||||
|
Logs defects
|
||||||
|
Validates fixes
|
||||||
|
Supports QA lead in test management</field>
|
||||||
|
<field name="color">4</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="role_developer" model="project.role">
|
||||||
|
<field name="name">Developer</field>
|
||||||
|
<field name="role_level">execution</field>
|
||||||
|
<field name="description">Executes assigned development tasks
|
||||||
|
Updates task progress and logs timesheets
|
||||||
|
Submits completed work for approval</field>
|
||||||
|
<field name="color">2</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="role_user" model="project.role">
|
||||||
|
<field name="name">IT</field>
|
||||||
|
<field name="role_level">execution</field>
|
||||||
|
<field name="description">Performs generic tasks
|
||||||
|
Provides inputs as required</field>
|
||||||
|
<field name="color">1</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from . import teams
|
from . import teams
|
||||||
|
from . import project_roles_master
|
||||||
from . import project_sprint
|
from . import project_sprint
|
||||||
from . import task_documents
|
from . import task_documents
|
||||||
from . import project_architecture_design
|
from . import project_architecture_design
|
||||||
|
|
@ -8,6 +9,7 @@ from . import project_costings
|
||||||
from . import project_code_commit
|
from . import project_code_commit
|
||||||
from . import project_stages
|
from . import project_stages
|
||||||
from . import task_stages
|
from . import task_stages
|
||||||
|
from . import deployment_log
|
||||||
from . import project
|
from . import project
|
||||||
from . import project_task
|
from . import project_task
|
||||||
from . import timesheets
|
from . import timesheets
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
class DeploymentLog(models.Model):
|
||||||
|
_name = 'project.deployment.log'
|
||||||
|
_description = "Project Deployment Log"
|
||||||
|
_order = 'deployment_date desc'
|
||||||
|
|
||||||
|
project_id = fields.Many2one(
|
||||||
|
'project.project',
|
||||||
|
string="Project",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade"
|
||||||
|
)
|
||||||
|
|
||||||
|
deployment_date = fields.Datetime(string="Deployment Date", required=True)
|
||||||
|
|
||||||
|
deployment_ready = fields.Boolean(string="Deployment Ready?")
|
||||||
|
qa_signoff = fields.Boolean(string="QA Signoff")
|
||||||
|
client_signoff = fields.Boolean(string="Client Signoff")
|
||||||
|
backup_completed = fields.Boolean(string="Backup Completed?")
|
||||||
|
|
||||||
|
deployment_version = fields.Char(string="Version")
|
||||||
|
deployed_by = fields.Char(string="Deployed By")
|
||||||
|
|
||||||
|
deployment_notes = fields.Text(string="Notes")
|
||||||
|
|
||||||
|
deployment_files_ids = fields.Many2many(
|
||||||
|
'ir.attachment',
|
||||||
|
string="Deployment Files"
|
||||||
|
)
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
from odoo import api, fields, models, _
|
from odoo import api, fields, models, _
|
||||||
from odoo.exceptions import UserError, ValidationError
|
from odoo.exceptions import UserError, ValidationError
|
||||||
|
from markupsafe import Markup
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import pytz
|
||||||
|
|
||||||
class ProjectProject(models.Model):
|
class ProjectProject(models.Model):
|
||||||
_inherit = 'project.project'
|
_inherit = 'project.project'
|
||||||
|
|
@ -25,6 +27,634 @@ class ProjectProject(models.Model):
|
||||||
string="Default Projects Channel"
|
string="Default Projects Channel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
project_stages = fields.One2many('project.stages.approval.flow', 'project_id')
|
||||||
|
assign_approval_flow = fields.Boolean(default=False)
|
||||||
|
project_sponsor = fields.Many2one('res.users')
|
||||||
|
show_project_chatter = fields.Boolean(default=False)
|
||||||
|
project_vision = fields.Text(
|
||||||
|
string="Project Vision",
|
||||||
|
help="Concise statement describing the project's ultimate goal and purpose"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Requirement Documentation
|
||||||
|
description = fields.Html("Requirement Description")
|
||||||
|
requirement_file = fields.Binary("Requirement Document")
|
||||||
|
requirement_file_name = fields.Char("Requirement File Name")
|
||||||
|
|
||||||
|
# Feasibility Assessment
|
||||||
|
feasibility_html = fields.Html("Feasibility Assessment")
|
||||||
|
feasibility_file = fields.Binary("Feasibility Document")
|
||||||
|
feasibility_file_name = fields.Char("Feasibility File Name")
|
||||||
|
|
||||||
|
manager_level_edit_access = fields.Boolean(compute="_compute_has_manager_level_edit_access")
|
||||||
|
approval_status = fields.Selection([
|
||||||
|
('submitted', 'Submitted'),
|
||||||
|
('reject', 'Rejected')
|
||||||
|
])
|
||||||
|
show_submission_button = fields.Boolean(compute="_compute_access_check")
|
||||||
|
show_approval_button = fields.Boolean(compute="_compute_access_check")
|
||||||
|
show_refuse_button = fields.Boolean(compute="_compute_access_check")
|
||||||
|
show_back_button = fields.Boolean(compute="_compute_access_check")
|
||||||
|
project_activity_log = fields.Html(string="Project Activity Log")
|
||||||
|
project_scope = fields.Html(string="Scope", default=lambda self: """
|
||||||
|
<h3>Scope Description</h3><br/><br/>
|
||||||
|
<p><b>1. In Scope Items?</b></p><br/>
|
||||||
|
<p><b>2. Out Scope Items?</b></p><br/>
|
||||||
|
""")
|
||||||
|
risk_ids = fields.One2many(
|
||||||
|
"project.risk",
|
||||||
|
"project_id",
|
||||||
|
string="Project Risks"
|
||||||
|
)
|
||||||
|
# stage_id = fields.Many2one(domain="[('id','in',showable_stage_ids or [])]")
|
||||||
|
|
||||||
|
showable_stage_ids = fields.Many2many('project.project.stage',compute='_compute_project_project_stages')
|
||||||
|
|
||||||
|
# fields:
|
||||||
|
estimated_amount = fields.Float(string="Estimated Amount")
|
||||||
|
total_budget_amount = fields.Float(string="Total Budget Amount", compute="_compute_total_budget", store=True)
|
||||||
|
|
||||||
|
# Manpower
|
||||||
|
resource_cost_ids = fields.One2many(
|
||||||
|
"project.resource.cost",
|
||||||
|
"project_id",
|
||||||
|
string="Resource Costs"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Material
|
||||||
|
material_cost_ids = fields.One2many(
|
||||||
|
"project.material.cost",
|
||||||
|
"project_id",
|
||||||
|
string="Material Costs"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Equipment
|
||||||
|
equipment_cost_ids = fields.One2many(
|
||||||
|
"project.equipment.cost",
|
||||||
|
"project_id",
|
||||||
|
string="Equipment Costs"
|
||||||
|
)
|
||||||
|
|
||||||
|
architecture_design_ids = fields.One2many(
|
||||||
|
"project.architecture.design",
|
||||||
|
"project_id",
|
||||||
|
string="Architecture & Design"
|
||||||
|
)
|
||||||
|
|
||||||
|
require_sprint = fields.Boolean(
|
||||||
|
string="Require Sprints?",
|
||||||
|
default=False,
|
||||||
|
help="Enable sprint-based planning for this project."
|
||||||
|
)
|
||||||
|
|
||||||
|
sprint_ids = fields.One2many(
|
||||||
|
"project.sprint",
|
||||||
|
"project_id",
|
||||||
|
string="Project Sprints"
|
||||||
|
)
|
||||||
|
|
||||||
|
commit_step_ids = fields.One2many(
|
||||||
|
'project.commit.step',
|
||||||
|
'project_id',
|
||||||
|
string="Commit Steps"
|
||||||
|
)
|
||||||
|
|
||||||
|
development_document_ids = fields.One2many(
|
||||||
|
"task.development.document",
|
||||||
|
"project_id",
|
||||||
|
string="Development Documents"
|
||||||
|
)
|
||||||
|
|
||||||
|
testing_document_ids = fields.One2many(
|
||||||
|
"task.testing.document",
|
||||||
|
"project_id",
|
||||||
|
string="Testing Documents"
|
||||||
|
)
|
||||||
|
development_notes = fields.Html()
|
||||||
|
testing_notes = fields.Html()
|
||||||
|
deployment_log_ids = fields.One2many(
|
||||||
|
'project.deployment.log',
|
||||||
|
'project_id',
|
||||||
|
string="Deployment Logs"
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.depends('require_sprint','project_stages','assign_approval_flow')
|
||||||
|
def _compute_project_project_stages(self):
|
||||||
|
for rec in self:
|
||||||
|
project_stages = self.env['project.project.stage'].sudo().search([])
|
||||||
|
if rec.assign_approval_flow:
|
||||||
|
project_stages = rec.project_stages.filtered(lambda x: x.activate).stage_id + self.env.ref("project_task_timesheet_extended.project_project_stage_sprint_planning")
|
||||||
|
|
||||||
|
stage_ids = self.env['project.project.stage'].sudo().search([('id','in',project_stages.ids),('active', '=', True), ('company_id','in',[self.env.company.id,False])]).ids
|
||||||
|
|
||||||
|
if not rec.require_sprint:
|
||||||
|
stage_ids = self.env['project.project.stage'].sudo().search([
|
||||||
|
('id', 'in', project_stages.ids),
|
||||||
|
('active', '=', True),('company_id','in',[self.env.company.id,False]), ('id', '!=', self.env.ref(
|
||||||
|
"project_task_timesheet_extended.project_project_stage_sprint_planning").id)
|
||||||
|
]).ids
|
||||||
|
|
||||||
|
rec.showable_stage_ids = stage_ids
|
||||||
|
|
||||||
|
@api.depends("resource_cost_ids.total_cost", "material_cost_ids.total_cost", "equipment_cost_ids.total_cost")
|
||||||
|
def _compute_total_budget(self):
|
||||||
|
for project in self:
|
||||||
|
project.total_budget_amount = (
|
||||||
|
sum(project.resource_cost_ids.mapped("total_cost"))
|
||||||
|
+ sum(project.material_cost_ids.mapped("total_cost"))
|
||||||
|
+ sum(project.equipment_cost_ids.mapped("total_cost"))
|
||||||
|
)
|
||||||
|
|
||||||
|
def fetch_project_task_stage_users(self):
|
||||||
|
for project in self:
|
||||||
|
users_list = list()
|
||||||
|
if project.assign_approval_flow:
|
||||||
|
users_list.extend(project.project_stages.involved_users.ids)
|
||||||
|
else:
|
||||||
|
users_list.extend(project.showable_stage_ids.user_ids.ids)
|
||||||
|
|
||||||
|
if project.project_sponsor:
|
||||||
|
users_list.append(project.project_sponsor.id)
|
||||||
|
if project.user_id:
|
||||||
|
users_list.append(project.user_id.id)
|
||||||
|
if project.project_lead:
|
||||||
|
users_list.append(project.project_lead.id)
|
||||||
|
if project.type_ids:
|
||||||
|
for task_stage in project.type_ids:
|
||||||
|
if task_stage.team_id:
|
||||||
|
users_list.append(task_stage.team_id.team_lead.id)
|
||||||
|
if task_stage.involved_user_ids:
|
||||||
|
users_list.extend(task_stage.involved_user_ids.ids)
|
||||||
|
users_list = list(set(users_list))
|
||||||
|
base_users = project.members_ids.ids
|
||||||
|
removed_ids = set(base_users).difference(users_list)
|
||||||
|
newly_added_ids = set(users_list).difference(base_users)
|
||||||
|
project.update({
|
||||||
|
"members_ids": [(6, 0, users_list)],
|
||||||
|
"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))])],
|
||||||
|
})
|
||||||
|
project.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))])],
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Fetch Resource Data Button
|
||||||
|
# --------------------------------------------------------
|
||||||
|
def action_fetch_resource_data(self):
|
||||||
|
"""Fetch all members' employee records and create manpower cost lines with full auto-fill."""
|
||||||
|
for project in self:
|
||||||
|
|
||||||
|
# Project users = members + project manager + project lead
|
||||||
|
users = project.members_ids | project.user_id | project.project_lead
|
||||||
|
|
||||||
|
# Fetch employees linked to those users
|
||||||
|
employees = self.env["hr.employee"].search([("user_id", "in", users.ids)])
|
||||||
|
|
||||||
|
for emp in employees:
|
||||||
|
|
||||||
|
# Avoid duplicate manpower lines
|
||||||
|
existing = project.resource_cost_ids.filtered(lambda r: r.employee_id == emp)
|
||||||
|
if existing:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get active contract for salary details
|
||||||
|
contract = emp.contract_id
|
||||||
|
|
||||||
|
monthly_salary = contract.wage if contract else 0.0
|
||||||
|
daily_rate = (contract.wage / 30) if contract and contract.wage else 0.0
|
||||||
|
|
||||||
|
# Project Dates
|
||||||
|
start_date = project.date_start or fields.Date.today()
|
||||||
|
end_date = project.date or start_date
|
||||||
|
|
||||||
|
# Duration
|
||||||
|
duration = (end_date - start_date).days + 1 if start_date and end_date else 0
|
||||||
|
|
||||||
|
# Total Cost
|
||||||
|
total_cost = daily_rate * duration if daily_rate else 0
|
||||||
|
|
||||||
|
# Create manpower line
|
||||||
|
self.env["project.resource.cost"].create({
|
||||||
|
"project_id": project.id,
|
||||||
|
"employee_id": emp.id,
|
||||||
|
"monthly_salary": monthly_salary,
|
||||||
|
"daily_rate": daily_rate,
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": end_date,
|
||||||
|
"duration_days": duration,
|
||||||
|
"total_cost": total_cost,
|
||||||
|
})
|
||||||
|
|
||||||
|
@api.depends("project_stages", "stage_id", "approval_status")
|
||||||
|
def _compute_access_check(self):
|
||||||
|
"""Compute visibility of action buttons based on user permissions and project state"""
|
||||||
|
for project in self:
|
||||||
|
project.show_submission_button = False
|
||||||
|
project.show_approval_button = False
|
||||||
|
project.show_refuse_button = False
|
||||||
|
project.show_back_button = False
|
||||||
|
|
||||||
|
if not project.assign_approval_flow:
|
||||||
|
continue
|
||||||
|
|
||||||
|
user = self.env.user
|
||||||
|
project_manager = project.user_id
|
||||||
|
project_sponsor = project.project_sponsor
|
||||||
|
|
||||||
|
# Current approval timeline for this stage
|
||||||
|
current_approval_timeline = project.project_stages.filtered(
|
||||||
|
lambda s: s.stage_id == project.stage_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Compute button visibility based on approval flow
|
||||||
|
if current_approval_timeline:
|
||||||
|
line = current_approval_timeline[0]
|
||||||
|
assigned_to = line.assigned_to
|
||||||
|
responsible_lead = line.approval_by
|
||||||
|
|
||||||
|
# Submission button for assigned users
|
||||||
|
if (assigned_to == user and
|
||||||
|
project.approval_status != "submitted" and
|
||||||
|
assigned_to != responsible_lead):
|
||||||
|
project.show_submission_button = True
|
||||||
|
|
||||||
|
# Approval/refusal buttons for responsible leads
|
||||||
|
if (project.approval_status == "submitted" and
|
||||||
|
responsible_lead == user):
|
||||||
|
project.show_approval_button = True
|
||||||
|
project.show_refuse_button = True
|
||||||
|
|
||||||
|
# Direct approval when no assigned user
|
||||||
|
if not assigned_to and responsible_lead == user:
|
||||||
|
project.show_approval_button = True
|
||||||
|
|
||||||
|
# Direct approval when assigned user is also responsible
|
||||||
|
if (assigned_to == responsible_lead and
|
||||||
|
user == assigned_to):
|
||||||
|
project.show_approval_button = True
|
||||||
|
else:
|
||||||
|
# Managers can approve without specific flow
|
||||||
|
if user.has_group("project.group_project_manager"):
|
||||||
|
project.show_approval_button = True
|
||||||
|
|
||||||
|
# Managers get additional permissions
|
||||||
|
if user in [project_manager] or user.has_group("project.group_project_manager"):
|
||||||
|
project.show_back_button = True
|
||||||
|
if user.has_group("project.group_project_manager"):
|
||||||
|
project.show_approval_button = True
|
||||||
|
|
||||||
|
# Stage-specific button visibility
|
||||||
|
if project.stage_id:
|
||||||
|
stages = self.env['project.project.stage'].search([('id','in',project.showable_stage_ids.ids),
|
||||||
|
('id', '!=', self.env.ref("project.project_project_stage_3").id)
|
||||||
|
])
|
||||||
|
if stages:
|
||||||
|
first_stage = stages.sorted('sequence')[0]
|
||||||
|
last_stage = stages.sorted('sequence')[-1]
|
||||||
|
|
||||||
|
if project.stage_id == first_stage:
|
||||||
|
project.show_back_button = False
|
||||||
|
if project.stage_id == last_stage:
|
||||||
|
project.show_submission_button = False
|
||||||
|
project.show_approval_button = False
|
||||||
|
project.show_refuse_button = False
|
||||||
|
|
||||||
|
@api.depends("user_id")
|
||||||
|
def _compute_has_manager_level_edit_access(self):
|
||||||
|
"""Determine if current user has manager-level edit permissions"""
|
||||||
|
for rec in self:
|
||||||
|
rec.manager_level_edit_access = (
|
||||||
|
rec.user_id == self.env.user or
|
||||||
|
self.env.user.has_group("project.group_project_manager")
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_show_project_chatter(self):
|
||||||
|
"""Toggle visibility of project chatter"""
|
||||||
|
for project in self:
|
||||||
|
project.show_project_chatter = not project.show_project_chatter
|
||||||
|
|
||||||
|
def action_assign_approval_flow(self):
|
||||||
|
"""Configure approval flow for project stages"""
|
||||||
|
for project in self:
|
||||||
|
if not project.project_sponsor or not project.user_id:
|
||||||
|
raise ValidationError(_("Sponsor and Manager are required to assign Stage Approvals"))
|
||||||
|
|
||||||
|
project.assign_approval_flow = not project.assign_approval_flow
|
||||||
|
|
||||||
|
if project.assign_approval_flow:
|
||||||
|
# Clear existing records
|
||||||
|
project.project_stages.unlink()
|
||||||
|
|
||||||
|
# Fetch all project stages
|
||||||
|
stages = self.env['project.project.stage'].sudo().search([('active', '=', True),
|
||||||
|
('company_id', 'in', [self.env.company.id, False])])
|
||||||
|
|
||||||
|
for stage in stages:
|
||||||
|
# Determine approval authority based on stage configuration
|
||||||
|
approval_by = (
|
||||||
|
project.user_id.id if stage.sudo().approval_by == 'project_manager' else
|
||||||
|
project.project_sponsor.id if stage.sudo().approval_by == 'project_sponsor' else
|
||||||
|
False
|
||||||
|
)
|
||||||
|
assigned_to = (approval_by if approval_by in stage.user_ids.ids else
|
||||||
|
min(stage.user_ids, key=lambda u: u.id).id if stage.user_ids else False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.env['project.stages.approval.flow'].sudo().create({
|
||||||
|
'project_id': project.id,
|
||||||
|
'stage_id': stage.id,
|
||||||
|
'approval_by': approval_by,
|
||||||
|
'assigned_to': assigned_to,
|
||||||
|
'involved_users': [(6, 0, stage.user_ids.ids)],
|
||||||
|
'assigned_date': fields.Datetime.now() if stage == project.stage_id else False,
|
||||||
|
'submission_date': False,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Log approval flow assignment
|
||||||
|
self.sudo()._add_activity_log("Approval flow assigned by %s" % self.env.user.name)
|
||||||
|
self.sudo()._post_to_project_channel(
|
||||||
|
_("Approval flow configured for project %s") % project.name
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
project.sudo().project_stages.unlink()
|
||||||
|
self.sudo()._add_activity_log("Approval flow removed by %s" % self.env.user.name)
|
||||||
|
self.sudo()._post_to_project_channel(
|
||||||
|
_("Approval flow removed for project %s") % project.name
|
||||||
|
)
|
||||||
|
|
||||||
|
def submit_project_for_approval(self):
|
||||||
|
"""Submit project for current stage approval"""
|
||||||
|
for project in self:
|
||||||
|
project.sudo().approval_status = "submitted"
|
||||||
|
current_stage = project.sudo().stage_id
|
||||||
|
current_approval_timeline = project.sudo().project_stages.filtered(
|
||||||
|
lambda s: s.stage_id == project.sudo().stage_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if current_approval_timeline:
|
||||||
|
current_approval_timeline.sudo().submission_date = fields.Datetime.now()
|
||||||
|
|
||||||
|
stage_line = project.sudo().project_stages.filtered(lambda s: s.stage_id == current_stage)
|
||||||
|
responsible_user = stage_line.sudo().approval_by if stage_line else False
|
||||||
|
|
||||||
|
# Create activity log
|
||||||
|
activity_log = "%s : %s submitted for approval to %s" % (
|
||||||
|
current_stage.sudo().name,
|
||||||
|
self.env.user.name,
|
||||||
|
responsible_user.sudo().name if responsible_user else "N/A"
|
||||||
|
)
|
||||||
|
project.sudo()._add_activity_log(activity_log)
|
||||||
|
|
||||||
|
# Post to project channel
|
||||||
|
if responsible_user:
|
||||||
|
channel_message = _("Project %s submitted for approval at stage %s. %s please review.") % (
|
||||||
|
project.sudo().name,
|
||||||
|
current_stage.sudo().name,
|
||||||
|
project.sudo()._create_odoo_mention(responsible_user.partner_id)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
channel_message = _("Project %s submitted for approval at stage %s") % (
|
||||||
|
project.sudo().name,
|
||||||
|
current_stage.sudo().name
|
||||||
|
)
|
||||||
|
project.sudo()._post_to_project_channel(channel_message)
|
||||||
|
|
||||||
|
# Send notification
|
||||||
|
if responsible_user:
|
||||||
|
project.sudo().message_post(
|
||||||
|
body=activity_log,
|
||||||
|
partner_ids=[responsible_user.sudo().partner_id.id],
|
||||||
|
message_type='notification',
|
||||||
|
subtype_xmlid='mail.mt_comment'
|
||||||
|
)
|
||||||
|
|
||||||
|
def project_proceed_further(self):
|
||||||
|
"""Advance project to next stage after approval"""
|
||||||
|
for project in self:
|
||||||
|
current_stage = project.stage_id
|
||||||
|
next_stage = self.env["project.project.stage"].search([
|
||||||
|
('sequence', '>', project.stage_id.sequence),
|
||||||
|
('id', '!=', self.env.ref("project.project_project_stage_3").id),
|
||||||
|
('id', 'in', project.showable_stage_ids.ids),
|
||||||
|
], order="sequence asc", limit=1)
|
||||||
|
|
||||||
|
current_approval_timeline = project.project_stages.filtered(
|
||||||
|
lambda s: s.stage_id == project.stage_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if current_approval_timeline:
|
||||||
|
current_approval_timeline.submission_date = fields.Datetime.now()
|
||||||
|
if not current_approval_timeline.assigned_date:
|
||||||
|
current_approval_timeline.assigned_date = fields.Datetime.now()
|
||||||
|
|
||||||
|
if next_stage:
|
||||||
|
next_approval_timeline = project.project_stages.filtered(
|
||||||
|
lambda s: s.stage_id == next_stage
|
||||||
|
)
|
||||||
|
if next_approval_timeline and not next_approval_timeline.assigned_date:
|
||||||
|
next_approval_timeline.assigned_date = fields.Datetime.now()
|
||||||
|
|
||||||
|
project.stage_id = next_stage
|
||||||
|
project.approval_status = ""
|
||||||
|
|
||||||
|
# Create activity log
|
||||||
|
activity_log = "%s approved by %s → moved to %s" % (
|
||||||
|
current_stage.name,
|
||||||
|
self.env.user.name,
|
||||||
|
next_stage.name
|
||||||
|
)
|
||||||
|
project._add_activity_log(activity_log)
|
||||||
|
|
||||||
|
# Post to project channel
|
||||||
|
next_user = next_approval_timeline.assigned_to if next_approval_timeline else False
|
||||||
|
if next_user:
|
||||||
|
channel_message = _("Project %s approved at stage %s and moved to %s. %s please proceed.") % (
|
||||||
|
project.name,
|
||||||
|
current_stage.name,
|
||||||
|
next_stage.name,
|
||||||
|
project._create_odoo_mention(next_user.partner_id)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
channel_message = _("Project %s approved at stage %s and moved to %s") % (
|
||||||
|
project.name,
|
||||||
|
current_stage.name,
|
||||||
|
next_stage.name
|
||||||
|
)
|
||||||
|
project._post_to_project_channel(channel_message)
|
||||||
|
|
||||||
|
# Send notification
|
||||||
|
if next_user:
|
||||||
|
project.message_post(
|
||||||
|
body=activity_log,
|
||||||
|
partner_ids=[next_user.partner_id.id],
|
||||||
|
message_type='notification',
|
||||||
|
subtype_xmlid='mail.mt_comment'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Last stage completed
|
||||||
|
project.approval_status = ""
|
||||||
|
activity_log = "%s fully approved and completed" % project.name
|
||||||
|
project._add_activity_log(activity_log)
|
||||||
|
project._post_to_project_channel(
|
||||||
|
_("Project %s completed and fully approved") % project.name
|
||||||
|
)
|
||||||
|
project.message_post(body=activity_log)
|
||||||
|
|
||||||
|
def reject_and_return(self, reason=None):
|
||||||
|
"""Reject project at current stage with optional reason"""
|
||||||
|
for project in self:
|
||||||
|
reason = reason or ""
|
||||||
|
current_stage = project.stage_id
|
||||||
|
project.approval_status = "reject"
|
||||||
|
|
||||||
|
# Create activity log
|
||||||
|
activity_log = "%s rejected by %s — %s" % (
|
||||||
|
current_stage.name,
|
||||||
|
self.env.user.name,
|
||||||
|
reason
|
||||||
|
)
|
||||||
|
project._add_activity_log(activity_log)
|
||||||
|
|
||||||
|
# Update approval timeline
|
||||||
|
current_approval_timeline = project.project_stages.filtered(
|
||||||
|
lambda s: s.stage_id == project.stage_id
|
||||||
|
)
|
||||||
|
if current_approval_timeline:
|
||||||
|
current_approval_timeline.note = f"Reject Reason: {reason}"
|
||||||
|
|
||||||
|
# Post to project channel
|
||||||
|
channel_message = _("Project %s rejected at stage %s. Reason: %s") % (
|
||||||
|
project.name,
|
||||||
|
current_stage.name,
|
||||||
|
reason
|
||||||
|
)
|
||||||
|
project._post_to_project_channel(channel_message)
|
||||||
|
|
||||||
|
# Send notification
|
||||||
|
project.message_post(body=activity_log)
|
||||||
|
|
||||||
|
# Notify responsible users
|
||||||
|
if current_approval_timeline:
|
||||||
|
responsible_user = (
|
||||||
|
current_approval_timeline.assigned_to or
|
||||||
|
current_approval_timeline.approval_by
|
||||||
|
)
|
||||||
|
if responsible_user:
|
||||||
|
project.message_post(
|
||||||
|
body=_("Project %s has been rejected and returned to you") % project.name,
|
||||||
|
partner_ids=[responsible_user.partner_id.id],
|
||||||
|
message_type='notification',
|
||||||
|
subtype_xmlid='mail.mt_comment'
|
||||||
|
)
|
||||||
|
|
||||||
|
def project_back_button(self):
|
||||||
|
"""Revert project to previous stage"""
|
||||||
|
for project in self:
|
||||||
|
prev_stage = self.env["project.project.stage"].search([
|
||||||
|
('sequence', '<', project.stage_id.sequence),
|
||||||
|
('id', 'in', project.showable_stage_ids.ids)
|
||||||
|
], order="sequence desc", limit=1)
|
||||||
|
|
||||||
|
if not prev_stage:
|
||||||
|
raise ValidationError(_("No previous stage available."))
|
||||||
|
|
||||||
|
# Create activity log
|
||||||
|
activity_log = "%s reverted back to %s by %s" % (
|
||||||
|
project.stage_id.name,
|
||||||
|
prev_stage.name,
|
||||||
|
self.env.user.name
|
||||||
|
)
|
||||||
|
project._add_activity_log(activity_log)
|
||||||
|
|
||||||
|
# Post to project channel
|
||||||
|
channel_message = _("Project %s reverted from %s back to %s") % (
|
||||||
|
project.name,
|
||||||
|
project.stage_id.name,
|
||||||
|
prev_stage.name
|
||||||
|
)
|
||||||
|
project._post_to_project_channel(channel_message)
|
||||||
|
|
||||||
|
# Update stage
|
||||||
|
project.stage_id = prev_stage
|
||||||
|
project.message_post(body=activity_log)
|
||||||
|
|
||||||
|
def action_open_reject_wizard(self):
|
||||||
|
"""Open rejection wizard for projects"""
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"name": _("Reject Project"),
|
||||||
|
"res_model": "project.reject.reason.wizard",
|
||||||
|
"view_mode": "form",
|
||||||
|
"target": "new",
|
||||||
|
"context": {"default_project_id": self.id},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Activity Log Helper Methods
|
||||||
|
def _get_current_datetime_formatted(self):
|
||||||
|
"""Get current datetime in 'DD-MMM-YYYY HH:MM AM/PM' format"""
|
||||||
|
now = fields.Datetime.context_timestamp(self, fields.datetime.now())
|
||||||
|
formatted_date = now.strftime('%d-%b-%Y %I:%M %p').upper()
|
||||||
|
return formatted_date[1:] if formatted_date.startswith('0') else formatted_date
|
||||||
|
|
||||||
|
def _add_activity_log(self, activity_text):
|
||||||
|
"""Add formatted entry to project activity log"""
|
||||||
|
formatted_datetime = self._get_current_datetime_formatted()
|
||||||
|
for project in self:
|
||||||
|
log_entry = f"[{formatted_datetime}] {activity_text}"
|
||||||
|
if project.project_activity_log:
|
||||||
|
project.project_activity_log = Markup(project.project_activity_log) + Markup('<br>') + Markup(log_entry)
|
||||||
|
else:
|
||||||
|
project.project_activity_log = Markup(log_entry)
|
||||||
|
|
||||||
|
def _post_to_project_channel(self, message_body, mention_partners=None):
|
||||||
|
"""Post message to project's discuss channel with proper mentions"""
|
||||||
|
for project in self:
|
||||||
|
if not project.id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get project channel
|
||||||
|
channel = (
|
||||||
|
project.discuss_channel_id or
|
||||||
|
project.default_projects_channel_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if channel:
|
||||||
|
formatted_message = self._format_message_with_odoo_mentions(
|
||||||
|
message_body,
|
||||||
|
mention_partners
|
||||||
|
)
|
||||||
|
channel.message_post(
|
||||||
|
body=Markup(formatted_message),
|
||||||
|
message_type='comment',
|
||||||
|
subtype_xmlid='mail.mt_comment',
|
||||||
|
author_id=self.env.user.partner_id.id
|
||||||
|
)
|
||||||
|
|
||||||
|
def _format_message_with_odoo_mentions(self, message_body, mention_partners=None):
|
||||||
|
"""Format message with proper Odoo @mentions"""
|
||||||
|
if not mention_partners:
|
||||||
|
return f'<div>{message_body}</div>'
|
||||||
|
|
||||||
|
message_parts = ['<div>', message_body]
|
||||||
|
for partner in mention_partners:
|
||||||
|
if partner and partner.name:
|
||||||
|
mention_html = f'<a href="#" data-oe-model="res.partner" data-oe-id="{partner.id}">@{partner.name}</a>'
|
||||||
|
message_parts.append(mention_html)
|
||||||
|
message_parts.append('</div>')
|
||||||
|
return ' '.join(message_parts)
|
||||||
|
|
||||||
|
def _create_odoo_mention(self, partner):
|
||||||
|
"""Create Odoo mention link for a partner"""
|
||||||
|
if not partner:
|
||||||
|
return ""
|
||||||
|
return f'<a href="#" data-oe-model="res.partner" data-oe-id="{partner.id}">@{partner.name}</a>'
|
||||||
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_default_projects_channel(self):
|
def _get_default_projects_channel(self):
|
||||||
"""Get or create the default Projects Channel"""
|
"""Get or create the default Projects Channel"""
|
||||||
|
|
@ -148,14 +778,16 @@ class ProjectProject(models.Model):
|
||||||
# self.env.ref('project_task_timesheet_extended.task_type_hold').id,
|
# self.env.ref('project_task_timesheet_extended.task_type_hold').id,
|
||||||
return self.env['project.task.type'].browse(default_stage_ids)
|
return self.env['project.task.type'].browse(default_stage_ids)
|
||||||
|
|
||||||
project_lead = fields.Many2one("res.users", string="Project Lead")
|
|
||||||
|
project_lead = fields.Many2one("res.users", string="Project Lead",
|
||||||
|
domain=lambda self: [('id','in',self.env.ref('project_task_timesheet_extended.role_project_lead').user_ids.ids)])
|
||||||
members_ids = fields.Many2many('res.users', 'project_user_rel', 'project_id',
|
members_ids = fields.Many2many('res.users', 'project_user_rel', 'project_id',
|
||||||
'user_id', 'Project Members', help="""Project's
|
'user_id', 'Project Members', help="""Project's
|
||||||
members are users who can have an access to
|
members are users who can have an access to
|
||||||
the tasks related to this project."""
|
the tasks related to this project."""
|
||||||
)
|
)
|
||||||
user_id = fields.Many2one('res.users', string='Project Manager', default=lambda self: self.env.user, tracking=True,
|
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)],)
|
domain=lambda self: [('id','in',self.env.ref('project_task_timesheet_extended.role_project_manager').user_ids.ids),('groups_id', 'in', [self.env.ref('project.group_project_manager').id,self.env.ref('project_task_timesheet_extended.group_project_supervisor').id]),('share','=',False)],)
|
||||||
|
|
||||||
type_ids = fields.Many2many(default=lambda self: self._default_type_ids())
|
type_ids = fields.Many2many(default=lambda self: self._default_type_ids())
|
||||||
|
|
||||||
|
|
@ -186,3 +818,69 @@ class ProjectProject(models.Model):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectTask(models.Model):
|
||||||
|
_inherit = 'project.task'
|
||||||
|
|
||||||
|
|
||||||
|
def _default_sprint_id(self):
|
||||||
|
"""Return the current active (in-progress) sprint of the project."""
|
||||||
|
if 'project_id' in self._context:
|
||||||
|
project_id = self._context.get('project_id')
|
||||||
|
sprint = self.env['project.sprint'].search([
|
||||||
|
('project_id', '=', project_id),
|
||||||
|
('status', '=', 'in_progress')
|
||||||
|
], limit=1)
|
||||||
|
return sprint.id
|
||||||
|
return False
|
||||||
|
|
||||||
|
sprint_id = fields.Many2one(
|
||||||
|
"project.sprint",
|
||||||
|
string="Sprint",
|
||||||
|
default=_default_sprint_id
|
||||||
|
)
|
||||||
|
|
||||||
|
require_sprint = fields.Boolean(
|
||||||
|
related="project_id.require_sprint",
|
||||||
|
store=False
|
||||||
|
)
|
||||||
|
|
||||||
|
commit_step_ids = fields.One2many(
|
||||||
|
'project.commit.step',
|
||||||
|
'task_id',
|
||||||
|
string="Commit Steps"
|
||||||
|
)
|
||||||
|
|
||||||
|
show_task_chatter = fields.Boolean(default=False)
|
||||||
|
|
||||||
|
development_document_ids = fields.One2many(
|
||||||
|
"task.development.document",
|
||||||
|
"task_id",
|
||||||
|
string="Development Documents"
|
||||||
|
)
|
||||||
|
|
||||||
|
testing_document_ids = fields.One2many(
|
||||||
|
"task.testing.document",
|
||||||
|
"task_id",
|
||||||
|
string="Testing Documents"
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.onchange("project_id")
|
||||||
|
def _onchange_project_id_sprint_required(self):
|
||||||
|
for task in self:
|
||||||
|
if task.project_id and not task.project_id.require_sprint:
|
||||||
|
task.sprint_id = False
|
||||||
|
else:
|
||||||
|
if task.project_id and task.project_id.require_sprint:
|
||||||
|
sprint = self.env['project.sprint'].search([
|
||||||
|
('project_id', '=', task.project_id.id),
|
||||||
|
('status', '=', 'in_progress')
|
||||||
|
], limit=1)
|
||||||
|
task.sprint_id = sprint.id
|
||||||
|
|
||||||
|
|
||||||
|
def action_show_project_task_chatter(self):
|
||||||
|
"""Toggle visibility of project chatter"""
|
||||||
|
for project in self:
|
||||||
|
project.show_task_chatter = not project.show_task_chatter
|
||||||
|
|
@ -25,8 +25,8 @@ class ProjectResourceCost(models.Model):
|
||||||
"""Auto-fill start_date and end_date from project."""
|
"""Auto-fill start_date and end_date from project."""
|
||||||
if self.project_id:
|
if self.project_id:
|
||||||
# If project has dates → use them
|
# If project has dates → use them
|
||||||
self.start_date = self.project_id.start_date or date.today()
|
self.start_date = self.project_id.date_start or date.today()
|
||||||
self.end_date = self.project_id.end_date or self.start_date
|
self.end_date = self.project_id.date or self.start_date
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
# 2. If employee selected → load salary
|
# 2. If employee selected → load salary
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectRole(models.Model):
|
||||||
|
_name = 'project.role'
|
||||||
|
_description = 'Project Role Management'
|
||||||
|
_order = 'role_level, name'
|
||||||
|
|
||||||
|
ROLE_LEVELS = [
|
||||||
|
('administrative', 'Administrative'),
|
||||||
|
('leadership', 'Project Leadership'),
|
||||||
|
('execution', 'Execution'),
|
||||||
|
]
|
||||||
|
|
||||||
|
name = fields.Char(
|
||||||
|
string='Role Name',
|
||||||
|
required=True,
|
||||||
|
help="Name of the project role"
|
||||||
|
)
|
||||||
|
description = fields.Text(
|
||||||
|
string='Description',
|
||||||
|
help="Detailed description of the role responsibilities"
|
||||||
|
)
|
||||||
|
user_ids = fields.Many2many(
|
||||||
|
'res.users',
|
||||||
|
'project_role_user_rel',
|
||||||
|
'role_id',
|
||||||
|
'user_id',
|
||||||
|
string='Assigned Users',
|
||||||
|
help="Users assigned to this role"
|
||||||
|
)
|
||||||
|
active = fields.Boolean(
|
||||||
|
string='Active',
|
||||||
|
default=True,
|
||||||
|
help="If unchecked, the role will be hidden but not deleted"
|
||||||
|
)
|
||||||
|
color = fields.Integer(
|
||||||
|
string='Color Index',
|
||||||
|
default=0,
|
||||||
|
help="Color index for kanban view"
|
||||||
|
)
|
||||||
|
role_level = fields.Selection(
|
||||||
|
ROLE_LEVELS,
|
||||||
|
string='Authority Level',
|
||||||
|
required=True,
|
||||||
|
help="Structured authority level of the role"
|
||||||
|
)
|
||||||
|
|
||||||
|
def action_update_users(self):
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'name': 'Add Users',
|
||||||
|
'res_model': 'roles.user.assign.wizard',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'view_id': self.env.ref('project_task_timesheet_extended.roles_user_assignment_form_view').id,
|
||||||
|
'target': 'new',
|
||||||
|
'context': {'default_members_ids': [(6, 0, self.user_ids.ids)],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
def action_view_users(self):
|
||||||
|
"""Open users assigned to this role"""
|
||||||
|
self.ensure_one()
|
||||||
|
action = {
|
||||||
|
'name': f'Users in {self.name} Role',
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'res.users',
|
||||||
|
'view_mode': 'list,form',
|
||||||
|
'domain': [('id', 'in', self.user_ids.ids)],
|
||||||
|
'context': {'default_role_id': self.id},
|
||||||
|
}
|
||||||
|
if len(self.user_ids) == 1:
|
||||||
|
action.update({
|
||||||
|
'view_mode': 'form',
|
||||||
|
'res_id': self.user_ids.id,
|
||||||
|
})
|
||||||
|
return action
|
||||||
|
|
||||||
|
class ProjectStage(models.Model):
|
||||||
|
_inherit = 'project.project.stage'
|
||||||
|
|
||||||
|
role_ids = fields.Many2many(
|
||||||
|
'project.role',
|
||||||
|
'project_stage_role_rel',
|
||||||
|
'stage_id',
|
||||||
|
'role_id',
|
||||||
|
string='Default Access By Roles',
|
||||||
|
help="Roles assigned to this project stage"
|
||||||
|
)
|
||||||
|
user_ids = fields.Many2many(
|
||||||
|
'res.users',
|
||||||
|
compute='_compute_user_ids',
|
||||||
|
string='Related Role Users',
|
||||||
|
help="Users assigned to the roles of this stage"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _compute_user_ids(self):
|
||||||
|
"""Compute users from assigned roles"""
|
||||||
|
for stage in self:
|
||||||
|
stage.user_ids = stage.role_ids.mapped('user_ids')
|
||||||
|
|
@ -17,13 +17,17 @@ class projectStagesApprovalFlow(models.Model):
|
||||||
stage_id = fields.Many2one('project.project.stage')
|
stage_id = fields.Many2one('project.project.stage')
|
||||||
stage_approval_by = fields.Selection(related='stage_id.approval_by')
|
stage_approval_by = fields.Selection(related='stage_id.approval_by')
|
||||||
approval_by = fields.Many2one("res.users", domain="[('id','in',approval_by_users)]")
|
approval_by = fields.Many2one("res.users", domain="[('id','in',approval_by_users)]")
|
||||||
assigned_to = fields.Many2one("res.users")
|
assigned_to = fields.Many2one("res.users", domain="[('id','in',related_stage_users)]")
|
||||||
|
related_stage_users = fields.Many2many("res.users",related="stage_id.user_ids")
|
||||||
assigned_date = fields.Datetime()
|
assigned_date = fields.Datetime()
|
||||||
submission_date = fields.Datetime()
|
submission_date = fields.Datetime()
|
||||||
project_id = fields.Many2one('project.project')
|
project_id = fields.Many2one('project.project')
|
||||||
approval_by_users = fields.Many2many('res.users', compute="_compute_all_project_managers")
|
approval_by_users = fields.Many2many('res.users', compute="_compute_all_project_managers")
|
||||||
note = fields.Text()
|
note = fields.Text()
|
||||||
manager_level_edit_access = fields.Boolean(compute="_compute_manager_level_edit_access")
|
manager_level_edit_access = fields.Boolean(compute="_compute_manager_level_edit_access")
|
||||||
|
activate = fields.Boolean(default=True)
|
||||||
|
involved_users = fields.Many2many('res.users', 'project_stage_approval_user_rel', 'project_stage_approval_id',
|
||||||
|
'user_id',string="Related Users",domain="[('id','in',related_stage_users)]")
|
||||||
|
|
||||||
def _compute_manager_level_edit_access(self):
|
def _compute_manager_level_edit_access(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
|
|
@ -50,647 +54,3 @@ class projectStagesApprovalFlow(models.Model):
|
||||||
|
|
||||||
rec.approval_by_users = [(6, 0, pm_users)]
|
rec.approval_by_users = [(6, 0, pm_users)]
|
||||||
|
|
||||||
|
|
||||||
class ProjectProject(models.Model):
|
|
||||||
_inherit = 'project.project'
|
|
||||||
|
|
||||||
project_stages = fields.One2many('project.stages.approval.flow', 'project_id')
|
|
||||||
assign_approval_flow = fields.Boolean(default=False)
|
|
||||||
project_sponsor = fields.Many2one('res.users')
|
|
||||||
show_project_chatter = fields.Boolean(default=False)
|
|
||||||
project_vision = fields.Text(
|
|
||||||
string="Project Vision",
|
|
||||||
help="Concise statement describing the project's ultimate goal and purpose"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Requirement Documentation
|
|
||||||
description = fields.Html("Requirement Description")
|
|
||||||
requirement_file = fields.Binary("Requirement Document")
|
|
||||||
requirement_file_name = fields.Char("Requirement File Name")
|
|
||||||
|
|
||||||
# Feasibility Assessment
|
|
||||||
feasibility_html = fields.Html("Feasibility Assessment")
|
|
||||||
feasibility_file = fields.Binary("Feasibility Document")
|
|
||||||
feasibility_file_name = fields.Char("Feasibility File Name")
|
|
||||||
|
|
||||||
manager_level_edit_access = fields.Boolean(compute="_compute_has_manager_level_edit_access")
|
|
||||||
approval_status = fields.Selection([
|
|
||||||
('submitted', 'Submitted'),
|
|
||||||
('reject', 'Rejected')
|
|
||||||
])
|
|
||||||
show_submission_button = fields.Boolean(compute="_compute_access_check")
|
|
||||||
show_approval_button = fields.Boolean(compute="_compute_access_check")
|
|
||||||
show_refuse_button = fields.Boolean(compute="_compute_access_check")
|
|
||||||
show_back_button = fields.Boolean(compute="_compute_access_check")
|
|
||||||
project_activity_log = fields.Html(string="Project Activity Log")
|
|
||||||
project_scope = fields.Html(string="Scope", default=lambda self: """
|
|
||||||
<h3>Scope Description</h3><br/><br/>
|
|
||||||
<p><b>1. In Scope Items?</b></p><br/>
|
|
||||||
<p><b>2. Out Scope Items?</b></p><br/>
|
|
||||||
""")
|
|
||||||
risk_ids = fields.One2many(
|
|
||||||
"project.risk",
|
|
||||||
"project_id",
|
|
||||||
string="Project Risks"
|
|
||||||
)
|
|
||||||
# stage_id = fields.Many2one(domain="[('id','in',showable_stage_ids or [])]")
|
|
||||||
|
|
||||||
showable_stage_ids = fields.Many2many('project.project.stage',compute='_compute_project_project_stages')
|
|
||||||
|
|
||||||
# fields:
|
|
||||||
estimated_amount = fields.Float(string="Estimated Amount")
|
|
||||||
total_budget_amount = fields.Float(string="Total Budget Amount", compute="_compute_total_budget", store=True)
|
|
||||||
|
|
||||||
# Manpower
|
|
||||||
resource_cost_ids = fields.One2many(
|
|
||||||
"project.resource.cost",
|
|
||||||
"project_id",
|
|
||||||
string="Resource Costs"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Material
|
|
||||||
material_cost_ids = fields.One2many(
|
|
||||||
"project.material.cost",
|
|
||||||
"project_id",
|
|
||||||
string="Material Costs"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Equipment
|
|
||||||
equipment_cost_ids = fields.One2many(
|
|
||||||
"project.equipment.cost",
|
|
||||||
"project_id",
|
|
||||||
string="Equipment Costs"
|
|
||||||
)
|
|
||||||
|
|
||||||
architecture_design_ids = fields.One2many(
|
|
||||||
"project.architecture.design",
|
|
||||||
"project_id",
|
|
||||||
string="Architecture & Design"
|
|
||||||
)
|
|
||||||
|
|
||||||
require_sprint = fields.Boolean(
|
|
||||||
string="Require Sprints?",
|
|
||||||
default=False,
|
|
||||||
help="Enable sprint-based planning for this project."
|
|
||||||
)
|
|
||||||
|
|
||||||
sprint_ids = fields.One2many(
|
|
||||||
"project.sprint",
|
|
||||||
"project_id",
|
|
||||||
string="Project Sprints"
|
|
||||||
)
|
|
||||||
|
|
||||||
commit_step_ids = fields.One2many(
|
|
||||||
'project.commit.step',
|
|
||||||
'project_id',
|
|
||||||
string="Commit Steps"
|
|
||||||
)
|
|
||||||
|
|
||||||
development_document_ids = fields.One2many(
|
|
||||||
"task.development.document",
|
|
||||||
"project_id",
|
|
||||||
string="Development Documents"
|
|
||||||
)
|
|
||||||
|
|
||||||
testing_document_ids = fields.One2many(
|
|
||||||
"task.testing.document",
|
|
||||||
"project_id",
|
|
||||||
string="Testing Documents"
|
|
||||||
)
|
|
||||||
development_notes = fields.Html()
|
|
||||||
testing_notes = fields.Html()
|
|
||||||
|
|
||||||
@api.depends('require_sprint')
|
|
||||||
def _compute_project_project_stages(self):
|
|
||||||
for rec in self:
|
|
||||||
stage_ids = self.env['project.project.stage'].sudo().search([('active', '=', True), ('company_id','in',[self.env.company.id,False])]).ids
|
|
||||||
|
|
||||||
if not rec.require_sprint:
|
|
||||||
stage_ids = self.env['project.project.stage'].sudo().search([
|
|
||||||
('active', '=', True),('company_id','in',[self.env.company.id,False]), ('id', '!=', self.env.ref(
|
|
||||||
"project_task_timesheet_extended.project_project_stage_sprint_planning").id)
|
|
||||||
]).ids
|
|
||||||
rec.showable_stage_ids = stage_ids
|
|
||||||
|
|
||||||
@api.depends("resource_cost_ids.total_cost", "material_cost_ids.total_cost", "equipment_cost_ids.total_cost")
|
|
||||||
def _compute_total_budget(self):
|
|
||||||
for project in self:
|
|
||||||
project.total_budget_amount = (
|
|
||||||
sum(project.resource_cost_ids.mapped("total_cost"))
|
|
||||||
+ sum(project.material_cost_ids.mapped("total_cost"))
|
|
||||||
+ sum(project.equipment_cost_ids.mapped("total_cost"))
|
|
||||||
)
|
|
||||||
|
|
||||||
# --------------------------------------------------------
|
|
||||||
# Fetch Resource Data Button
|
|
||||||
# --------------------------------------------------------
|
|
||||||
def action_fetch_resource_data(self):
|
|
||||||
"""Fetch all members' employee records and create manpower cost lines with full auto-fill."""
|
|
||||||
for project in self:
|
|
||||||
|
|
||||||
# Project users = members + project manager + project lead
|
|
||||||
users = project.members_ids | project.user_id | project.project_lead
|
|
||||||
|
|
||||||
# Fetch employees linked to those users
|
|
||||||
employees = self.env["hr.employee"].search([("user_id", "in", users.ids)])
|
|
||||||
|
|
||||||
for emp in employees:
|
|
||||||
|
|
||||||
# Avoid duplicate manpower lines
|
|
||||||
existing = project.resource_cost_ids.filtered(lambda r: r.employee_id == emp)
|
|
||||||
if existing:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Get active contract for salary details
|
|
||||||
contract = emp.contract_id
|
|
||||||
|
|
||||||
monthly_salary = contract.wage if contract else 0.0
|
|
||||||
daily_rate = (contract.wage / 30) if contract and contract.wage else 0.0
|
|
||||||
|
|
||||||
# Project Dates
|
|
||||||
start_date = project.date_start or fields.Date.today()
|
|
||||||
end_date = project.date or start_date
|
|
||||||
|
|
||||||
# Duration
|
|
||||||
duration = (end_date - start_date).days + 1 if start_date and end_date else 0
|
|
||||||
|
|
||||||
# Total Cost
|
|
||||||
total_cost = daily_rate * duration if daily_rate else 0
|
|
||||||
|
|
||||||
# Create manpower line
|
|
||||||
self.env["project.resource.cost"].create({
|
|
||||||
"project_id": project.id,
|
|
||||||
"employee_id": emp.id,
|
|
||||||
"monthly_salary": monthly_salary,
|
|
||||||
"daily_rate": daily_rate,
|
|
||||||
"start_date": start_date,
|
|
||||||
"end_date": end_date,
|
|
||||||
"duration_days": duration,
|
|
||||||
"total_cost": total_cost,
|
|
||||||
})
|
|
||||||
|
|
||||||
@api.depends("project_stages", "stage_id", "approval_status")
|
|
||||||
def _compute_access_check(self):
|
|
||||||
"""Compute visibility of action buttons based on user permissions and project state"""
|
|
||||||
for project in self:
|
|
||||||
project.show_submission_button = False
|
|
||||||
project.show_approval_button = False
|
|
||||||
project.show_refuse_button = False
|
|
||||||
project.show_back_button = False
|
|
||||||
|
|
||||||
if not project.assign_approval_flow:
|
|
||||||
continue
|
|
||||||
|
|
||||||
user = self.env.user
|
|
||||||
project_manager = project.user_id
|
|
||||||
project_sponsor = project.project_sponsor
|
|
||||||
|
|
||||||
# Current approval timeline for this stage
|
|
||||||
current_approval_timeline = project.project_stages.filtered(
|
|
||||||
lambda s: s.stage_id == project.stage_id
|
|
||||||
)
|
|
||||||
|
|
||||||
# Compute button visibility based on approval flow
|
|
||||||
if current_approval_timeline:
|
|
||||||
line = current_approval_timeline[0]
|
|
||||||
assigned_to = line.assigned_to
|
|
||||||
responsible_lead = line.approval_by
|
|
||||||
|
|
||||||
# Submission button for assigned users
|
|
||||||
if (assigned_to == user and
|
|
||||||
project.approval_status != "submitted" and
|
|
||||||
assigned_to != responsible_lead):
|
|
||||||
project.show_submission_button = True
|
|
||||||
|
|
||||||
# Approval/refusal buttons for responsible leads
|
|
||||||
if (project.approval_status == "submitted" and
|
|
||||||
responsible_lead == user):
|
|
||||||
project.show_approval_button = True
|
|
||||||
project.show_refuse_button = True
|
|
||||||
|
|
||||||
# Direct approval when no assigned user
|
|
||||||
if not assigned_to and responsible_lead == user:
|
|
||||||
project.show_approval_button = True
|
|
||||||
|
|
||||||
# Direct approval when assigned user is also responsible
|
|
||||||
if (assigned_to == responsible_lead and
|
|
||||||
user == assigned_to):
|
|
||||||
project.show_approval_button = True
|
|
||||||
else:
|
|
||||||
# Managers can approve without specific flow
|
|
||||||
if user.has_group("project.group_project_manager"):
|
|
||||||
project.show_approval_button = True
|
|
||||||
|
|
||||||
# Managers get additional permissions
|
|
||||||
if user in [project_manager] or user.has_group("project.group_project_manager"):
|
|
||||||
project.show_back_button = True
|
|
||||||
if user.has_group("project.group_project_manager"):
|
|
||||||
project.show_approval_button = True
|
|
||||||
|
|
||||||
# Stage-specific button visibility
|
|
||||||
if project.stage_id:
|
|
||||||
stages = self.env['project.project.stage'].search([('id','in',project.showable_stage_ids.ids),
|
|
||||||
('id', '!=', self.env.ref("project.project_project_stage_3").id)
|
|
||||||
])
|
|
||||||
if stages:
|
|
||||||
first_stage = stages.sorted('sequence')[0]
|
|
||||||
last_stage = stages.sorted('sequence')[-1]
|
|
||||||
|
|
||||||
if project.stage_id == first_stage:
|
|
||||||
project.show_back_button = False
|
|
||||||
if project.stage_id == last_stage:
|
|
||||||
project.show_submission_button = False
|
|
||||||
project.show_approval_button = False
|
|
||||||
project.show_refuse_button = False
|
|
||||||
|
|
||||||
@api.depends("user_id")
|
|
||||||
def _compute_has_manager_level_edit_access(self):
|
|
||||||
"""Determine if current user has manager-level edit permissions"""
|
|
||||||
for rec in self:
|
|
||||||
rec.manager_level_edit_access = (
|
|
||||||
rec.user_id == self.env.user or
|
|
||||||
self.env.user.has_group("project.group_project_manager")
|
|
||||||
)
|
|
||||||
|
|
||||||
def action_show_project_chatter(self):
|
|
||||||
"""Toggle visibility of project chatter"""
|
|
||||||
for project in self:
|
|
||||||
project.show_project_chatter = not project.show_project_chatter
|
|
||||||
|
|
||||||
def action_assign_approval_flow(self):
|
|
||||||
"""Configure approval flow for project stages"""
|
|
||||||
for project in self:
|
|
||||||
if not project.project_sponsor or not project.user_id:
|
|
||||||
raise ValidationError(_("Sponsor and Manager are required to assign Stage Approvals"))
|
|
||||||
|
|
||||||
project.assign_approval_flow = not project.assign_approval_flow
|
|
||||||
|
|
||||||
if project.assign_approval_flow:
|
|
||||||
# Clear existing records
|
|
||||||
project.project_stages.unlink()
|
|
||||||
|
|
||||||
# Fetch all project stages
|
|
||||||
stages = self.env['project.project.stage'].sudo().search([('id','in',project.showable_stage_ids.ids)])
|
|
||||||
|
|
||||||
for stage in stages:
|
|
||||||
# Determine approval authority based on stage configuration
|
|
||||||
approval_by = (
|
|
||||||
project.user_id.id if stage.sudo().approval_by == 'project_manager' else
|
|
||||||
project.project_sponsor.id if stage.sudo().approval_by == 'project_sponsor' else
|
|
||||||
False
|
|
||||||
)
|
|
||||||
|
|
||||||
self.env['project.stages.approval.flow'].sudo().create({
|
|
||||||
'project_id': project.id,
|
|
||||||
'stage_id': stage.id,
|
|
||||||
'approval_by': approval_by,
|
|
||||||
'assigned_to': approval_by,
|
|
||||||
'assigned_date': fields.Datetime.now() if stage == project.stage_id else False,
|
|
||||||
'submission_date': False,
|
|
||||||
})
|
|
||||||
|
|
||||||
# Log approval flow assignment
|
|
||||||
self.sudo()._add_activity_log("Approval flow assigned by %s" % self.env.user.name)
|
|
||||||
self.sudo()._post_to_project_channel(
|
|
||||||
_("Approval flow configured for project %s") % project.name
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
project.sudo().project_stages.unlink()
|
|
||||||
self.sudo()._add_activity_log("Approval flow removed by %s" % self.env.user.name)
|
|
||||||
self.sudo()._post_to_project_channel(
|
|
||||||
_("Approval flow removed for project %s") % project.name
|
|
||||||
)
|
|
||||||
|
|
||||||
def submit_project_for_approval(self):
|
|
||||||
"""Submit project for current stage approval"""
|
|
||||||
for project in self:
|
|
||||||
project.sudo().approval_status = "submitted"
|
|
||||||
current_stage = project.sudo().stage_id
|
|
||||||
current_approval_timeline = project.sudo().project_stages.filtered(
|
|
||||||
lambda s: s.stage_id == project.sudo().stage_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if current_approval_timeline:
|
|
||||||
current_approval_timeline.sudo().submission_date = fields.Datetime.now()
|
|
||||||
|
|
||||||
stage_line = project.sudo().project_stages.filtered(lambda s: s.stage_id == current_stage)
|
|
||||||
responsible_user = stage_line.sudo().approval_by if stage_line else False
|
|
||||||
|
|
||||||
# Create activity log
|
|
||||||
activity_log = "%s : %s submitted for approval to %s" % (
|
|
||||||
current_stage.sudo().name,
|
|
||||||
self.env.user.name,
|
|
||||||
responsible_user.sudo().name if responsible_user else "N/A"
|
|
||||||
)
|
|
||||||
project.sudo()._add_activity_log(activity_log)
|
|
||||||
|
|
||||||
# Post to project channel
|
|
||||||
if responsible_user:
|
|
||||||
channel_message = _("Project %s submitted for approval at stage %s. %s please review.") % (
|
|
||||||
project.sudo().name,
|
|
||||||
current_stage.sudo().name,
|
|
||||||
project.sudo()._create_odoo_mention(responsible_user.partner_id)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
channel_message = _("Project %s submitted for approval at stage %s") % (
|
|
||||||
project.sudo().name,
|
|
||||||
current_stage.sudo().name
|
|
||||||
)
|
|
||||||
project.sudo()._post_to_project_channel(channel_message)
|
|
||||||
|
|
||||||
# Send notification
|
|
||||||
if responsible_user:
|
|
||||||
project.sudo().message_post(
|
|
||||||
body=activity_log,
|
|
||||||
partner_ids=[responsible_user.sudo().partner_id.id],
|
|
||||||
message_type='notification',
|
|
||||||
subtype_xmlid='mail.mt_comment'
|
|
||||||
)
|
|
||||||
|
|
||||||
def project_proceed_further(self):
|
|
||||||
"""Advance project to next stage after approval"""
|
|
||||||
for project in self:
|
|
||||||
current_stage = project.stage_id
|
|
||||||
next_stage = self.env["project.project.stage"].search([
|
|
||||||
('sequence', '>', project.stage_id.sequence),
|
|
||||||
('id', '!=', self.env.ref("project.project_project_stage_3").id),
|
|
||||||
('id', 'in', project.showable_stage_ids.ids),
|
|
||||||
], order="sequence asc", limit=1)
|
|
||||||
|
|
||||||
current_approval_timeline = project.project_stages.filtered(
|
|
||||||
lambda s: s.stage_id == project.stage_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if current_approval_timeline:
|
|
||||||
current_approval_timeline.submission_date = fields.Datetime.now()
|
|
||||||
if not current_approval_timeline.assigned_date:
|
|
||||||
current_approval_timeline.assigned_date = fields.Datetime.now()
|
|
||||||
|
|
||||||
if next_stage:
|
|
||||||
next_approval_timeline = project.project_stages.filtered(
|
|
||||||
lambda s: s.stage_id == next_stage
|
|
||||||
)
|
|
||||||
if next_approval_timeline and not next_approval_timeline.assigned_date:
|
|
||||||
next_approval_timeline.assigned_date = fields.Datetime.now()
|
|
||||||
|
|
||||||
project.stage_id = next_stage
|
|
||||||
project.approval_status = ""
|
|
||||||
|
|
||||||
# Create activity log
|
|
||||||
activity_log = "%s approved by %s → moved to %s" % (
|
|
||||||
current_stage.name,
|
|
||||||
self.env.user.name,
|
|
||||||
next_stage.name
|
|
||||||
)
|
|
||||||
project._add_activity_log(activity_log)
|
|
||||||
|
|
||||||
# Post to project channel
|
|
||||||
next_user = next_approval_timeline.assigned_to if next_approval_timeline else False
|
|
||||||
if next_user:
|
|
||||||
channel_message = _("Project %s approved at stage %s and moved to %s. %s please proceed.") % (
|
|
||||||
project.name,
|
|
||||||
current_stage.name,
|
|
||||||
next_stage.name,
|
|
||||||
project._create_odoo_mention(next_user.partner_id)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
channel_message = _("Project %s approved at stage %s and moved to %s") % (
|
|
||||||
project.name,
|
|
||||||
current_stage.name,
|
|
||||||
next_stage.name
|
|
||||||
)
|
|
||||||
project._post_to_project_channel(channel_message)
|
|
||||||
|
|
||||||
# Send notification
|
|
||||||
if next_user:
|
|
||||||
project.message_post(
|
|
||||||
body=activity_log,
|
|
||||||
partner_ids=[next_user.partner_id.id],
|
|
||||||
message_type='notification',
|
|
||||||
subtype_xmlid='mail.mt_comment'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Last stage completed
|
|
||||||
project.approval_status = ""
|
|
||||||
activity_log = "%s fully approved and completed" % project.name
|
|
||||||
project._add_activity_log(activity_log)
|
|
||||||
project._post_to_project_channel(
|
|
||||||
_("Project %s completed and fully approved") % project.name
|
|
||||||
)
|
|
||||||
project.message_post(body=activity_log)
|
|
||||||
|
|
||||||
def reject_and_return(self, reason=None):
|
|
||||||
"""Reject project at current stage with optional reason"""
|
|
||||||
for project in self:
|
|
||||||
reason = reason or ""
|
|
||||||
current_stage = project.stage_id
|
|
||||||
project.approval_status = "reject"
|
|
||||||
|
|
||||||
# Create activity log
|
|
||||||
activity_log = "%s rejected by %s — %s" % (
|
|
||||||
current_stage.name,
|
|
||||||
self.env.user.name,
|
|
||||||
reason
|
|
||||||
)
|
|
||||||
project._add_activity_log(activity_log)
|
|
||||||
|
|
||||||
# Update approval timeline
|
|
||||||
current_approval_timeline = project.project_stages.filtered(
|
|
||||||
lambda s: s.stage_id == project.stage_id
|
|
||||||
)
|
|
||||||
if current_approval_timeline:
|
|
||||||
current_approval_timeline.note = f"Reject Reason: {reason}"
|
|
||||||
|
|
||||||
# Post to project channel
|
|
||||||
channel_message = _("Project %s rejected at stage %s. Reason: %s") % (
|
|
||||||
project.name,
|
|
||||||
current_stage.name,
|
|
||||||
reason
|
|
||||||
)
|
|
||||||
project._post_to_project_channel(channel_message)
|
|
||||||
|
|
||||||
# Send notification
|
|
||||||
project.message_post(body=activity_log)
|
|
||||||
|
|
||||||
# Notify responsible users
|
|
||||||
if current_approval_timeline:
|
|
||||||
responsible_user = (
|
|
||||||
current_approval_timeline.assigned_to or
|
|
||||||
current_approval_timeline.approval_by
|
|
||||||
)
|
|
||||||
if responsible_user:
|
|
||||||
project.message_post(
|
|
||||||
body=_("Project %s has been rejected and returned to you") % project.name,
|
|
||||||
partner_ids=[responsible_user.partner_id.id],
|
|
||||||
message_type='notification',
|
|
||||||
subtype_xmlid='mail.mt_comment'
|
|
||||||
)
|
|
||||||
|
|
||||||
def project_back_button(self):
|
|
||||||
"""Revert project to previous stage"""
|
|
||||||
for project in self:
|
|
||||||
prev_stage = self.env["project.project.stage"].search([
|
|
||||||
('sequence', '<', project.stage_id.sequence),
|
|
||||||
('id', 'in', project.showable_stage_ids.ids)
|
|
||||||
], order="sequence desc", limit=1)
|
|
||||||
|
|
||||||
if not prev_stage:
|
|
||||||
raise ValidationError(_("No previous stage available."))
|
|
||||||
|
|
||||||
# Create activity log
|
|
||||||
activity_log = "%s reverted back to %s by %s" % (
|
|
||||||
project.stage_id.name,
|
|
||||||
prev_stage.name,
|
|
||||||
self.env.user.name
|
|
||||||
)
|
|
||||||
project._add_activity_log(activity_log)
|
|
||||||
|
|
||||||
# Post to project channel
|
|
||||||
channel_message = _("Project %s reverted from %s back to %s") % (
|
|
||||||
project.name,
|
|
||||||
project.stage_id.name,
|
|
||||||
prev_stage.name
|
|
||||||
)
|
|
||||||
project._post_to_project_channel(channel_message)
|
|
||||||
|
|
||||||
# Update stage
|
|
||||||
project.stage_id = prev_stage
|
|
||||||
project.message_post(body=activity_log)
|
|
||||||
|
|
||||||
def action_open_reject_wizard(self):
|
|
||||||
"""Open rejection wizard for projects"""
|
|
||||||
self.ensure_one()
|
|
||||||
return {
|
|
||||||
"type": "ir.actions.act_window",
|
|
||||||
"name": _("Reject Project"),
|
|
||||||
"res_model": "project.reject.reason.wizard",
|
|
||||||
"view_mode": "form",
|
|
||||||
"target": "new",
|
|
||||||
"context": {"default_project_id": self.id},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Activity Log Helper Methods
|
|
||||||
def _get_current_datetime_formatted(self):
|
|
||||||
"""Get current datetime in 'DD-MMM-YYYY HH:MM AM/PM' format"""
|
|
||||||
now = fields.Datetime.context_timestamp(self, fields.datetime.now())
|
|
||||||
formatted_date = now.strftime('%d-%b-%Y %I:%M %p').upper()
|
|
||||||
return formatted_date[1:] if formatted_date.startswith('0') else formatted_date
|
|
||||||
|
|
||||||
def _add_activity_log(self, activity_text):
|
|
||||||
"""Add formatted entry to project activity log"""
|
|
||||||
formatted_datetime = self._get_current_datetime_formatted()
|
|
||||||
for project in self:
|
|
||||||
log_entry = f"[{formatted_datetime}] {activity_text}"
|
|
||||||
if project.project_activity_log:
|
|
||||||
project.project_activity_log = Markup(project.project_activity_log) + Markup('<br>') + Markup(log_entry)
|
|
||||||
else:
|
|
||||||
project.project_activity_log = Markup(log_entry)
|
|
||||||
|
|
||||||
def _post_to_project_channel(self, message_body, mention_partners=None):
|
|
||||||
"""Post message to project's discuss channel with proper mentions"""
|
|
||||||
for project in self:
|
|
||||||
if not project.id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Get project channel
|
|
||||||
channel = (
|
|
||||||
project.discuss_channel_id or
|
|
||||||
project.default_projects_channel_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if channel:
|
|
||||||
formatted_message = self._format_message_with_odoo_mentions(
|
|
||||||
message_body,
|
|
||||||
mention_partners
|
|
||||||
)
|
|
||||||
channel.message_post(
|
|
||||||
body=Markup(formatted_message),
|
|
||||||
message_type='comment',
|
|
||||||
subtype_xmlid='mail.mt_comment',
|
|
||||||
author_id=self.env.user.partner_id.id
|
|
||||||
)
|
|
||||||
|
|
||||||
def _format_message_with_odoo_mentions(self, message_body, mention_partners=None):
|
|
||||||
"""Format message with proper Odoo @mentions"""
|
|
||||||
if not mention_partners:
|
|
||||||
return f'<div>{message_body}</div>'
|
|
||||||
|
|
||||||
message_parts = ['<div>', message_body]
|
|
||||||
for partner in mention_partners:
|
|
||||||
if partner and partner.name:
|
|
||||||
mention_html = f'<a href="#" data-oe-model="res.partner" data-oe-id="{partner.id}">@{partner.name}</a>'
|
|
||||||
message_parts.append(mention_html)
|
|
||||||
message_parts.append('</div>')
|
|
||||||
return ' '.join(message_parts)
|
|
||||||
|
|
||||||
def _create_odoo_mention(self, partner):
|
|
||||||
"""Create Odoo mention link for a partner"""
|
|
||||||
if not partner:
|
|
||||||
return ""
|
|
||||||
return f'<a href="#" data-oe-model="res.partner" data-oe-id="{partner.id}">@{partner.name}</a>'
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectTask(models.Model):
|
|
||||||
_inherit = 'project.task'
|
|
||||||
|
|
||||||
|
|
||||||
def _default_sprint_id(self):
|
|
||||||
"""Return the current active (in-progress) sprint of the project."""
|
|
||||||
if 'project_id' in self._context:
|
|
||||||
project_id = self._context.get('project_id')
|
|
||||||
sprint = self.env['project.sprint'].search([
|
|
||||||
('project_id', '=', project_id),
|
|
||||||
('status', '=', 'in_progress')
|
|
||||||
], limit=1)
|
|
||||||
return sprint.id
|
|
||||||
return False
|
|
||||||
|
|
||||||
sprint_id = fields.Many2one(
|
|
||||||
"project.sprint",
|
|
||||||
string="Sprint",
|
|
||||||
default=_default_sprint_id
|
|
||||||
)
|
|
||||||
|
|
||||||
require_sprint = fields.Boolean(
|
|
||||||
related="project_id.require_sprint",
|
|
||||||
store=False
|
|
||||||
)
|
|
||||||
|
|
||||||
commit_step_ids = fields.One2many(
|
|
||||||
'project.commit.step',
|
|
||||||
'task_id',
|
|
||||||
string="Commit Steps"
|
|
||||||
)
|
|
||||||
|
|
||||||
show_task_chatter = fields.Boolean(default=False)
|
|
||||||
|
|
||||||
development_document_ids = fields.One2many(
|
|
||||||
"task.development.document",
|
|
||||||
"task_id",
|
|
||||||
string="Development Documents"
|
|
||||||
)
|
|
||||||
|
|
||||||
testing_document_ids = fields.One2many(
|
|
||||||
"task.testing.document",
|
|
||||||
"task_id",
|
|
||||||
string="Testing Documents"
|
|
||||||
)
|
|
||||||
|
|
||||||
@api.onchange("project_id")
|
|
||||||
def _onchange_project_id_sprint_required(self):
|
|
||||||
for task in self:
|
|
||||||
if task.project_id and not task.project_id.require_sprint:
|
|
||||||
task.sprint_id = False
|
|
||||||
else:
|
|
||||||
if task.project_id and task.project_id.require_sprint:
|
|
||||||
sprint = self.env['project.sprint'].search([
|
|
||||||
('project_id', '=', task.project_id.id),
|
|
||||||
('status', '=', 'in_progress')
|
|
||||||
], limit=1)
|
|
||||||
task.sprint_id = sprint.id
|
|
||||||
|
|
||||||
|
|
||||||
def action_show_project_task_chatter(self):
|
|
||||||
"""Toggle visibility of project chatter"""
|
|
||||||
for project in self:
|
|
||||||
project.show_task_chatter = not project.show_task_chatter
|
|
||||||
|
|
@ -6,6 +6,13 @@ class TaskStages(models.Model):
|
||||||
|
|
||||||
team_id = fields.Many2one('internal.teams','Assigned to')
|
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 / Manager')])
|
approval_by = fields.Selection([('assigned_team_lead','Assigned Team Lead'),('project_manager','Project Manager'),('project_lead','Project Lead / Manager')])
|
||||||
|
involved_user_ids = fields.Many2many('res.users')
|
||||||
|
|
||||||
|
@api.onchange('team_id')
|
||||||
|
def onchange_team_id(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.team_id and rec.team_id.all_members_ids:
|
||||||
|
rec.involved_user_ids = [(6,0,rec.team_id.all_members_ids.ids)]
|
||||||
|
|
||||||
def create_or_update_data(self):
|
def create_or_update_data(self):
|
||||||
"""Open wizard for updating this stage inside a project context."""
|
"""Open wizard for updating this stage inside a project context."""
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ internal_teams_admin,internal.teams.admin,model_internal_teams,project.group_pro
|
||||||
internal_teams_manager,internal.teams.manager,model_internal_teams,project.group_project_user,1,1,1,0
|
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
|
internal_teams_user,internal.teams.user,model_internal_teams,base.group_user,1,0,0,0
|
||||||
|
|
||||||
|
access_project_role_user,project.role.user,model_project_role,base.group_user,1,0,0,0
|
||||||
|
access_project_role_manager,project.role.manager,model_project_role,project.group_project_manager,1,1,1,1
|
||||||
|
|
||||||
access_project_sprint_user,access.project.sprint.user,model_project_sprint,project.group_project_user,1,1,1,1
|
access_project_sprint_user,access.project.sprint.user,model_project_sprint,project.group_project_user,1,1,1,1
|
||||||
access_project_sprint_manager,access.project.sprint.manager,model_project_sprint,project.group_project_manager,1,1,1,1
|
access_project_sprint_manager,access.project.sprint.manager,model_project_sprint,project.group_project_manager,1,1,1,1
|
||||||
|
|
||||||
|
|
@ -30,7 +33,10 @@ access_task_testing_document,access_task_testing_document,model_task_testing_doc
|
||||||
|
|
||||||
project_user_assign_wizard_manager,project.user.assign.wizard,model_project_user_assign_wizard,project_task_timesheet_extended.group_project_supervisor,1,1,1,1
|
project_user_assign_wizard_manager,project.user.assign.wizard,model_project_user_assign_wizard,project_task_timesheet_extended.group_project_supervisor,1,1,1,1
|
||||||
project_user_assign_wizard_admin,project.user.assign.wizard.admin,model_project_user_assign_wizard,project.group_project_manager,1,1,1,1
|
project_user_assign_wizard_admin,project.user.assign.wizard.admin,model_project_user_assign_wizard,project.group_project_manager,1,1,1,1
|
||||||
project_user_assign_wizard_user,project.user.assign.wizard.user,model_project_user_assign_wizard,project.group_project_manager,1,0,0,0
|
project_user_assign_wizard_user,project.user.assign.wizard.user,model_project_user_assign_wizard,base.group_user,1,0,0,0
|
||||||
|
|
||||||
|
roles_user_assign_wizard_user,roles.user.assign.wizard.user,model_roles_user_assign_wizard,base.group_user,1,1,1,1
|
||||||
|
|
||||||
|
|
||||||
project_user_project_reject_reason_wizard,project.reject.reason.wizard.user,model_project_reject_reason_wizard,base.group_user,1,1,1,1
|
project_user_project_reject_reason_wizard,project.reject.reason.wizard.user,model_project_reject_reason_wizard,base.group_user,1,1,1,1
|
||||||
project_user_task_reject_reason_wizard,task.reject.reason.wizard.user,model_task_reject_reason_wizard,base.group_user,1,1,1,1
|
project_user_task_reject_reason_wizard,task.reject.reason.wizard.user,model_task_reject_reason_wizard,base.group_user,1,1,1,1
|
||||||
|
|
@ -51,3 +57,5 @@ access_project_task_time_lines_user,access_project_task_time_lines_user,model_pr
|
||||||
access_project_task_time_lines_manager,access_project_task_time_lines_manager,model_project_task_time_lines,project.group_project_manager,1,1,1,1
|
access_project_task_time_lines_manager,access_project_task_time_lines_manager,model_project_task_time_lines,project.group_project_manager,1,1,1,1
|
||||||
|
|
||||||
access_user_task_availability,user.task.availability.access,model_user_task_availability,base.group_user,1,0,0,0
|
access_user_task_availability,user.task.availability.access,model_user_task_availability,base.group_user,1,0,0,0
|
||||||
|
|
||||||
|
access_project_deployment_log_user,access.project.deployment.log.user,model_project_deployment_log,base.group_user,1,1,1,1
|
||||||
|
|
|
@ -6,6 +6,12 @@
|
||||||
<field name="implied_ids" eval="[(4, ref('project.group_project_user'))]"/>
|
<field name="implied_ids" eval="[(4, ref('project.group_project_user'))]"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="group_project_lead" model="res.groups">
|
||||||
|
<field name="name">Project Lead</field>
|
||||||
|
<field name="category_id" ref="base.module_category_services_project"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
<record id="project.group_project_manager" model="res.groups">
|
<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'))]"/>
|
<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>
|
</record>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
.deployment_card {
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployment_header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployment_version {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployment_date {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployment_status_container {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployment_status {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 3px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deployment_notes {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #f8f8ff;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #333;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2d8f2d;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
|
||||||
|
<odoo>
|
||||||
|
<record id="view_project_deployment_log_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">project.deployment.log.kanban</field>
|
||||||
|
<field name="model">project.deployment.log</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban class="o_kanban_small_column">
|
||||||
|
<field name="deployment_date"/>
|
||||||
|
<field name="deployment_ready"/>
|
||||||
|
<field name="qa_signoff"/>
|
||||||
|
<field name="client_signoff"/>
|
||||||
|
<field name="backup_completed"/>
|
||||||
|
<field name="deployment_version"/>
|
||||||
|
<field name="deployed_by"/>
|
||||||
|
<field name="deployment_notes"/>
|
||||||
|
<field name="deployment_files_ids"/>
|
||||||
|
|
||||||
|
<templates>
|
||||||
|
<t t-name="kanban-box">
|
||||||
|
<div class="oe_kanban_global_click o_kanban_record">
|
||||||
|
<div class="o_kanban_primary_left">
|
||||||
|
<strong>
|
||||||
|
<t t-esc="record.deployment_version.value or 'Version ?'"/>
|
||||||
|
</strong>
|
||||||
|
<br/>
|
||||||
|
<small>
|
||||||
|
<t t-esc="record.deployment_date.value"/>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_kanban_primary_right">
|
||||||
|
<ul style="list-style:none; padding:0;">
|
||||||
|
<li>Deployment Ready:
|
||||||
|
<t t-esc="record.deployment_ready.value and '✓' or '✗'"/>
|
||||||
|
</li>
|
||||||
|
<li>QA Signoff:
|
||||||
|
<t t-esc="record.qa_signoff.value and '✓' or '✗'"/>
|
||||||
|
</li>
|
||||||
|
<li>Client Signoff:
|
||||||
|
<t t-esc="record.client_signoff.value and '✓' or '✗'"/>
|
||||||
|
</li>
|
||||||
|
<li>Backup:
|
||||||
|
<t t-esc="record.backup_completed.value and '✓' or '✗'"/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="o_kanban_footer">
|
||||||
|
<t t-if="record.deployment_notes.value">
|
||||||
|
<span><t t-esc="record.deployment_notes.value"/></span>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<record id="project_project_inherit_form_view" model="ir.ui.view">
|
<record id="project_project_inherit_form_view2" model="ir.ui.view">
|
||||||
<field name="name">project.project.inherit.form.view</field>
|
<field name="name">project.project.inherit.form.view</field>
|
||||||
<field name="model">project.project</field>
|
<field name="model">project.project</field>
|
||||||
<field name="inherit_id" ref="project.edit_project"/>
|
<field name="inherit_id" ref="project.edit_project"/>
|
||||||
|
|
@ -39,29 +39,60 @@
|
||||||
|
|
||||||
</group>
|
</group>
|
||||||
</xpath>
|
</xpath>
|
||||||
<!-- <xpath expr="//field[@name='label_tasks']" position="before">-->
|
<xpath expr="//form/header" position="inside">
|
||||||
<!-- <group string="Project Channel">-->
|
<button type="object" name="submit_project_for_approval"
|
||||||
<!-- <field name="discuss_channel_id" widget="many2one"-->
|
string="Submit for Approval"
|
||||||
<!-- context="{'default_parent_id': default_projects_channel_id}"/>-->
|
class="oe_highlight"
|
||||||
<!-- <button name="action_create_project_channel"-->
|
invisible="not assign_approval_flow or not show_submission_button"/>
|
||||||
<!-- string="Create Project Channel"-->
|
|
||||||
<!-- type="object"-->
|
|
||||||
<!-- class="btn-primary"-->
|
|
||||||
<!-- attrs="{'invisible': [('discuss_channel_id', '!=', False)]}"/>-->
|
|
||||||
<!-- <field name="default_projects_channel_id" invisible="1"/>-->
|
|
||||||
<!-- </group>-->
|
|
||||||
<!-- </xpath>-->
|
|
||||||
|
|
||||||
<page name="settings" position="after">
|
<button type="object" name="project_proceed_further"
|
||||||
<page string="Team">
|
string="Approve & Proceed"
|
||||||
<group>
|
class="oe_highlight"
|
||||||
<button name="add_users" type="object" string="Add/Update" class="btn-primary"/>
|
invisible="not assign_approval_flow or not show_approval_button"/>
|
||||||
</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>
|
<button type="object" name="action_open_reject_wizard"
|
||||||
|
string="Reject"
|
||||||
|
class="oe_highlight"
|
||||||
|
invisible="not assign_approval_flow or not show_refuse_button"/>
|
||||||
|
|
||||||
|
<button type="object" name="project_back_button"
|
||||||
|
string="Go Back"
|
||||||
|
class="oe_highlight"
|
||||||
|
invisible="not assign_approval_flow or not show_back_button"/>
|
||||||
|
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//form" position="inside">
|
||||||
|
<field name="showable_stage_ids" invisible="1"/>
|
||||||
|
<field name="assign_approval_flow" invisible="1"/>
|
||||||
|
<field name="manager_level_edit_access" invisible="1"/>
|
||||||
|
<field name="show_submission_button" invisible="1"/>
|
||||||
|
<field name="show_approval_button" invisible="1"/>
|
||||||
|
<field name="show_refuse_button" invisible="1"/>
|
||||||
|
<field name="show_back_button" invisible="1"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='stage_id']" position="attributes">
|
||||||
|
<attribute name="domain">[('id', 'in', showable_stage_ids)]</attribute>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
|
||||||
|
<xpath expr="//page[@name='settings']" position="before">
|
||||||
|
<page name="project_stages" string="Project Stages" invisible="not assign_approval_flow">
|
||||||
|
<field name="project_stages" options="{'no_create': True, 'no_open': True, 'no_delete': True}">
|
||||||
|
<list editable="bottom" delete="0" create="0">
|
||||||
|
<field name="stage_id" readonly="1"/>
|
||||||
|
<field name="approval_by" readonly="not manager_level_edit_access"/>
|
||||||
|
<field name="assigned_to" readonly="not manager_level_edit_access"/>
|
||||||
|
<field name="involved_users" widget="many2many_tags"/>
|
||||||
|
<field name="assigned_date" readonly="1" optional="hide"/>
|
||||||
|
<field name="submission_date" readonly="1" optional="hide"/>
|
||||||
|
<field name="note" optional="show" readonly="not manager_level_edit_access"/>
|
||||||
|
<field name="project_id" invisible="1" column_invisible="1"/>
|
||||||
|
<field name="stage_approval_by" invisible="1" column_invisible="1"/>
|
||||||
|
<field name="approval_by_users" invisible="1" column_invisible="1"/>
|
||||||
|
<field name="manager_level_edit_access" invisible="1" column_invisible="1"/>
|
||||||
|
<field name="activate"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
</page>
|
</page>
|
||||||
<page name="task_stages" string="Task Stages">
|
<page name="task_stages" string="Task Stages">
|
||||||
<field name="type_ids" context="{'project_id': id}" options="{'no_open': True}">
|
<field name="type_ids" context="{'project_id': id}" options="{'no_open': True}">
|
||||||
|
|
@ -71,6 +102,7 @@
|
||||||
<field name="team_id"/>
|
<field name="team_id"/>
|
||||||
<field name="approval_by"/>
|
<field name="approval_by"/>
|
||||||
<field name="fold"/>
|
<field name="fold"/>
|
||||||
|
<field name="involved_user_ids" widget="many2many_tags"/>
|
||||||
<button name="create_or_update_data"
|
<button name="create_or_update_data"
|
||||||
type="object"
|
type="object"
|
||||||
string="Update"
|
string="Update"
|
||||||
|
|
@ -78,8 +110,603 @@
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
|
<page string="Team">
|
||||||
|
<group>
|
||||||
|
<button name="fetch_project_task_stage_users" type="object" string="Fetch Users from Related Stages" class="btn-primary"/>
|
||||||
|
<button name="add_users" type="object" string="Add/Update" class="btn-secondary"/>
|
||||||
|
</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>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//chatter" position="attributes">
|
||||||
|
<attribute name="invisible">not show_project_chatter</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='user_id']" position="before">
|
||||||
|
<field name="project_sponsor" widget="many2one_avatar_user"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//header/field[@name='stage_id']" position="attributes">
|
||||||
|
<attribute name="readonly">assign_approval_flow</attribute>
|
||||||
|
</xpath>
|
||||||
|
<page name="description" position="attributes">
|
||||||
|
<attribute name="string">Initiation</attribute>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<xpath expr="//field[@name='description']" position="attributes">
|
||||||
|
<attribute name="invisible">1</attribute>
|
||||||
|
</xpath>
|
||||||
|
<page name="description" position="inside">
|
||||||
|
<group>
|
||||||
|
<field name="project_vision"
|
||||||
|
placeholder="Eg: Build a mobile app that allows users to order groceries and track delivery in real time."
|
||||||
|
readonly="not manager_level_edit_access"/>
|
||||||
|
</group>
|
||||||
|
<group string="Requirements Document">
|
||||||
|
|
||||||
|
<div class="o_row" style="align-items: flex-start;">
|
||||||
|
|
||||||
|
<!-- LEFT SIDE -->
|
||||||
|
<div class="o_col" style="width: 70%;">
|
||||||
|
|
||||||
|
<!-- HTML field (visible when NO file uploaded) -->
|
||||||
|
<field name="description"
|
||||||
|
widget="html" force_save="1" readonly="not manager_level_edit_access"
|
||||||
|
invisible="requirement_file" placeholder="The system should allow user login,
|
||||||
|
Users should be able to add items to a cart."/>
|
||||||
|
|
||||||
|
<!-- PDF Viewer (visible when file exists) -->
|
||||||
|
<field name="requirement_file"
|
||||||
|
widget="binary" force_save="1" readonly="not manager_level_edit_access"
|
||||||
|
invisible="not requirement_file"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RIGHT SIDE -->
|
||||||
|
<div class="o_col" style="width: 30%; padding-left: 20px;">
|
||||||
|
|
||||||
|
<!-- Upload button (visible when NO file exists) -->
|
||||||
|
<field name="requirement_file"
|
||||||
|
widget="binary" force_save="1"
|
||||||
|
filename="requirement_file_name"
|
||||||
|
invisible="requirement_file or not manager_level_edit_access"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ===================== -->
|
||||||
|
<!-- FEASIBILITY ASSESSMENT -->
|
||||||
|
<!-- ===================== -->
|
||||||
|
<group string="Feasibility Assessment">
|
||||||
|
|
||||||
|
<div class="o_row" style="align-items: flex-start;">
|
||||||
|
|
||||||
|
<!-- LEFT SIDE -->
|
||||||
|
<div class="o_col" style="width: 70%;">
|
||||||
|
|
||||||
|
<!-- HTML field -->
|
||||||
|
<field name="feasibility_html"
|
||||||
|
widget="html" force_save="1"
|
||||||
|
readonly="not manager_level_edit_access"
|
||||||
|
invisible="feasibility_file"
|
||||||
|
placeholder="Check whether the project is technically, financially, and operationally possible."/>
|
||||||
|
|
||||||
|
<!-- PDF Viewer -->
|
||||||
|
<field name="feasibility_file"
|
||||||
|
widget="binary" force_save="1" readonly="not manager_level_edit_access"
|
||||||
|
invisible="not feasibility_file"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RIGHT SIDE -->
|
||||||
|
<div class="o_col" style="width: 30%; padding-left: 20px;">
|
||||||
|
|
||||||
|
<!-- Upload Field -->
|
||||||
|
<field name="feasibility_file"
|
||||||
|
widget="binary" force_save="1"
|
||||||
|
filename="feasibility_file_name"
|
||||||
|
invisible="feasibility_file or not manager_level_edit_access"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<xpath expr="//page[@name='settings']" position="inside">
|
||||||
|
<group>
|
||||||
|
<group name="group_sprint_requirement_management" string="Project Sprint" col="1"
|
||||||
|
class="row mt16 o_settings_container">
|
||||||
|
<div>
|
||||||
|
<setting class="col-lg-12" id="project_sprint_requirement_settings"
|
||||||
|
help="Enable it if you want to add Sprints for the project">
|
||||||
|
<field name="require_sprint" force_save="1"/>
|
||||||
|
</setting>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<xpath expr="//sheet" position="inside">
|
||||||
|
<widget name="web_ribbon" title="Rejected" bg_color="text-bg-danger"
|
||||||
|
invisible="approval_status != 'reject'"/>
|
||||||
|
<widget name="web_ribbon" title="Rejected" invisible="approval_status != 'submitted'"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page string="Project Activity Log" invisible="not assign_approval_flow or not show_project_chatter">
|
||||||
|
<field name="project_activity_log" widget="html" options="{'sanitize': False}" readonly="1"
|
||||||
|
force_save="1"/>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='date_start']" position="attributes">
|
||||||
|
<attribute name="invisible">1</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='date']" position="attributes">
|
||||||
|
<attribute name="invisible">1</attribute>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<xpath expr="//sheet/notebook/page[@name='settings']" position="after">
|
||||||
|
<page name="planning" string="Planning (Budget & Deadlines)">
|
||||||
|
<group>
|
||||||
|
|
||||||
|
<group string="project Scope">
|
||||||
|
<field name="project_scope" nolabel="1"/>
|
||||||
|
</group>
|
||||||
|
<group string="Timelines">
|
||||||
|
<group>
|
||||||
|
|
||||||
|
<field name="date_start" string="Planned Date" widget="daterange"
|
||||||
|
options='{"end_date_field": "date", "always_range": "1"}'
|
||||||
|
required="date_start or date"/>
|
||||||
|
<field name="date" invisible="1" required="date_start"/>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
<group></group>
|
||||||
|
<group>
|
||||||
|
<field name="allocated_hours" widget="timesheet_uom_no_toggle" optional="hide"
|
||||||
|
invisible="allocated_hours == 0 or not allow_timesheets"
|
||||||
|
groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||||
|
<field name="effective_hours" widget="timesheet_uom_no_toggle" optional="hide"
|
||||||
|
invisible="effective_hours == 0 or not allow_timesheets"
|
||||||
|
groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||||
|
<field name="remaining_hours" widget="timesheet_uom_no_toggle"
|
||||||
|
decoration-danger="remaining_hours < 0"
|
||||||
|
decoration-warning="allocated_hours > 0 and (remaining_hours / allocated_hours) < 0.2"
|
||||||
|
optional="hide"
|
||||||
|
invisible="allocated_hours == 0 or not allow_timesheets"
|
||||||
|
groups="hr_timesheet.group_hr_timesheet_user"
|
||||||
|
/>
|
||||||
|
<field name="estimated_hours" widget="timesheet_uom_no_toggle"
|
||||||
|
invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
|
||||||
|
<field name="task_estimated_hours" widget="timesheet_uom_no_toggle"
|
||||||
|
invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||||
|
<field name="actual_hours" widget="timesheet_uom_no_toggle"
|
||||||
|
invisible="not allow_timesheets"
|
||||||
|
groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
<group string="Risk Management Plan">
|
||||||
|
<field name="risk_ids" nolabel="1">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="project_id" invisible="1" column_invisible="1"/>
|
||||||
|
<field name="risk_description" width="30%"/>
|
||||||
|
<field name="probability" width="20%"/>
|
||||||
|
<field name="impact" width="20%"/>
|
||||||
|
<field name="mitigation_plan" width="30%"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
<group string="Budget Planning">
|
||||||
|
<group>
|
||||||
|
<field name="estimated_amount"/>
|
||||||
|
<field name="total_budget_amount" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<notebook>
|
||||||
|
<!-- MANPOWER TAB -->
|
||||||
|
<page string="Manpower">
|
||||||
|
|
||||||
|
<button name="action_fetch_resource_data"
|
||||||
|
type="object"
|
||||||
|
string="Fetch Resource Data"
|
||||||
|
class="oe_highlight"
|
||||||
|
icon="fa-refresh"/>
|
||||||
|
|
||||||
|
<field name="resource_cost_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="monthly_salary"/>
|
||||||
|
<field name="daily_rate"/>
|
||||||
|
<field name="start_date" optional="hide"/>
|
||||||
|
<field name="end_date" optional="hide"/>
|
||||||
|
<field name="duration_days" readonly="1"/>
|
||||||
|
<field name="total_cost"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<!-- MATERIAL TAB -->
|
||||||
|
<page string="Material">
|
||||||
|
<field name="material_cost_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="material_name" width="30%"/>
|
||||||
|
<field name="qty" width="20%"/>
|
||||||
|
<field name="unit_cost" width="20%"/>
|
||||||
|
<field name="total_cost" readonly="1" width="20%"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<!-- EQUIPMENT TAB -->
|
||||||
|
<page string="Equipments/Others">
|
||||||
|
<field name="equipment_cost_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="equipment_name" string="Equip/Others" width="30%"/>
|
||||||
|
<field name="duration_hours" width="20%"/>
|
||||||
|
<field name="hourly_rate" width="20%"/>
|
||||||
|
<field name="total_cost" readonly="1" width="20%"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
</notebook>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//sheet/notebook" position="inside">
|
||||||
|
<page name="architecture_design" string="Architecture & Design">
|
||||||
|
|
||||||
|
<field name="architecture_design_ids">
|
||||||
|
<list>
|
||||||
|
<field name="tech_stack"/>
|
||||||
|
<field name="design_status"/>
|
||||||
|
<field name="architecture_notes" optional="hide"/>
|
||||||
|
<field name="database_notes" optional="hide"/>
|
||||||
|
<field name="reviewer_comments" optional="hide"/>
|
||||||
|
</list>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group string="System Architecture">
|
||||||
|
<field name="architecture_diagram"/>
|
||||||
|
<field name="tech_stack"/>
|
||||||
|
<field name="architecture_notes"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<group string="UI / UX">
|
||||||
|
<field name="ui_wireframe"/>
|
||||||
|
<field name="ux_flow"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group string="Database Design">
|
||||||
|
<field name="er_diagram"/>
|
||||||
|
<field name="database_notes"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group string="Review">
|
||||||
|
<field name="design_status"/>
|
||||||
|
<field name="reviewer_comments"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</page>
|
||||||
|
<page name="project_sprints" string="Sprints" invisible="not require_sprint">
|
||||||
|
<field name="sprint_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="sprint_name" width="20%"/>
|
||||||
|
<field name="date_start" width="20%"/>
|
||||||
|
<field name="date_end" width="20%"/>
|
||||||
|
<field name="allocated_hours" width="20%"/>
|
||||||
|
<field name="status" width="20%"/>
|
||||||
|
<field name="done_date" optional="hide"/>
|
||||||
|
<field name="note" optional="hide"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page name="development" string="Development Details">
|
||||||
|
<group string="Documents">
|
||||||
|
<field name="development_document_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="file" filename="file_name" widget="binary"/>
|
||||||
|
<field name="file_name"/>
|
||||||
|
<field name="notes"/>
|
||||||
|
</list>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="file" filename="file_name" widget="binary"/>
|
||||||
|
<field name="file_name"/>
|
||||||
|
<field name="notes"/>
|
||||||
|
</group>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group string="Code Commit Documents">
|
||||||
|
<field name="commit_step_ids" readonly="1" create="0" edit="0">
|
||||||
|
<list>
|
||||||
|
|
||||||
|
<field name="task_id"/>
|
||||||
|
<field name="sprint_id" optional="hide"/>
|
||||||
|
<field name="commit_date"/>
|
||||||
|
<field name="commit_code" widget="html" optional="hide"/>
|
||||||
|
<field name="commit_message"/>
|
||||||
|
<field name="branch_name"/>
|
||||||
|
<field name="files_changed"/>
|
||||||
|
<field name="notes" optional="hide"/>
|
||||||
|
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
<group string="Notes">
|
||||||
|
<field name="development_notes" nolabel="1" placeholder="click hear to write comments"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="Testing Documents">
|
||||||
|
<group string="Documents">
|
||||||
|
<field name="testing_document_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="file" filename="file_name"/>
|
||||||
|
<field name="file_name"/>
|
||||||
|
<field name="notes"/>
|
||||||
|
</list>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="file" filename="file_name"/>
|
||||||
|
<field name="file_name"/>
|
||||||
|
<field name="notes"/>
|
||||||
|
</group>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
<group string="Notes">
|
||||||
|
<field name="testing_notes" nolabel="1" placeholder="click hear to write comments"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="Deployment" name="deployment">
|
||||||
|
<field name="deployment_log_ids" mode="kanban" colspan="4" nolabel="1">
|
||||||
|
<kanban class="o_kanban_small_column o_deployment_kanban">
|
||||||
|
<field name="deployment_date"/>
|
||||||
|
<field name="deployment_ready"/>
|
||||||
|
<field name="qa_signoff"/>
|
||||||
|
<field name="client_signoff"/>
|
||||||
|
<field name="backup_completed"/>
|
||||||
|
<field name="deployment_version"/>
|
||||||
|
<field name="deployed_by"/>
|
||||||
|
<field name="deployment_notes"/>
|
||||||
|
<field name="deployment_files_ids"/>
|
||||||
|
|
||||||
|
<templates>
|
||||||
|
<t t-name="kanban-box">
|
||||||
|
<div class="oe_kanban_global_click o_kanban_record deployment_card">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="deployment_header">
|
||||||
|
<div class="deployment_version">
|
||||||
|
<t t-esc="record.deployment_version.value or 'Version ?'"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="deployment_date">
|
||||||
|
<i class="fa fa-calendar"></i>
|
||||||
|
<t t-esc="record.deployment_date.value"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status -->
|
||||||
|
<div class="deployment_status_container">
|
||||||
|
<div class="deployment_status">
|
||||||
|
<span>Deployment Ready:</span>
|
||||||
|
<span class="badge">
|
||||||
|
<t t-esc="record.deployment_ready.value and '✔' or '✘'"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="deployment_status">
|
||||||
|
<span>QA Signoff:</span>
|
||||||
|
<span class="badge">
|
||||||
|
<t t-esc="record.qa_signoff.value and '✔' or '✘'"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="deployment_status">
|
||||||
|
<span>Client Signoff:</span>
|
||||||
|
<span class="badge">
|
||||||
|
<t t-esc="record.client_signoff.value and '✔' or '✘'"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="deployment_status">
|
||||||
|
<span>Backup:</span>
|
||||||
|
<span class="badge">
|
||||||
|
<t t-esc="record.backup_completed.value and '✔' or '✘'"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<t t-if="record.deployment_notes.value">
|
||||||
|
<div class="deployment_notes">
|
||||||
|
<strong>Notes:</strong>
|
||||||
|
<br/>
|
||||||
|
<span>
|
||||||
|
<t t-esc="record.deployment_notes.value"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
</kanban>
|
||||||
|
<form string="Deployment Log">
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="deployment_version"/>
|
||||||
|
<field name="deployment_date"/>
|
||||||
|
<field name="deployed_by"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<field name="deployment_ready"/>
|
||||||
|
<field name="qa_signoff"/>
|
||||||
|
<field name="client_signoff"/>
|
||||||
|
<field name="backup_completed"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<notebook>
|
||||||
|
<page string="Notes">
|
||||||
|
<field name="deployment_notes" placeholder="Deployment details, changes, steps..."/>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<page string="Files">
|
||||||
|
<field name="deployment_files_ids" widget="many2many"
|
||||||
|
options="{'no_create_edit': False, 'no_create': False}"
|
||||||
|
create="true"
|
||||||
|
mode="list,form">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="name" column_invisible="1"/>
|
||||||
|
<field name="datas" filename="name" widget="60%"/>
|
||||||
|
<field name="mimetype" widget="30%"/>
|
||||||
|
</list>
|
||||||
|
<form string="Deployment File">
|
||||||
|
<group>
|
||||||
|
<field name="datas" filename="name"/>
|
||||||
|
<field name="mimetype"/>
|
||||||
|
</group>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="project_invoice_form_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">project.invoice.inherit.form.view</field>
|
||||||
|
<field name="model">project.project</field>
|
||||||
|
<field name="inherit_id" ref="hr_timesheet.project_invoice_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='allocated_hours']" position="attributes">
|
||||||
|
<attribute name="invisible">True</attribute>
|
||||||
|
<!-- <field name="estimated_hours" widget="timesheet_uom_no_toggle" invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>-->
|
||||||
|
<!-- <field name="task_estimated_hours" widget="timesheet_uom_no_toggle" invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>-->
|
||||||
|
<!-- <field name="actual_hours" widget="timesheet_uom_no_toggle" invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>-->
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='effective_hours']" position="attributes">
|
||||||
|
<attribute name="invisible">True</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='remaining_hours']" position="attributes">
|
||||||
|
<attribute name="invisible">True</attribute>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="view_task_form_commit_steps" model="ir.ui.view">
|
||||||
|
<field name="name">project.task.form.commit.steps</field>
|
||||||
|
<field name="model">project.task</field>
|
||||||
|
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page string="Commit Steps">
|
||||||
|
<field name="commit_step_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="commit_date"/>
|
||||||
|
<field name="commit_code" widget="html"/>
|
||||||
|
<field name="commit_message"/>
|
||||||
|
<field name="branch_name"/>
|
||||||
|
<field name="files_changed"/>
|
||||||
|
<field name="notes"/>
|
||||||
|
</list>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<group>
|
||||||
|
<field name="commit_code" widget="html"/>
|
||||||
|
<field name="commit_message"/>
|
||||||
|
<field name="branch_name"/>
|
||||||
|
<field name="files_changed"/>
|
||||||
|
<field name="commit_date"/>
|
||||||
|
<field name="notes"/>
|
||||||
|
</group>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<!-- Development Documents Tab -->
|
||||||
|
<page string="Development Documents">
|
||||||
|
<field name="development_document_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="file" filename="file_name"/>
|
||||||
|
<field name="file_name"/>
|
||||||
|
<field name="notes"/>
|
||||||
|
</list>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="file" filename="file_name"/>
|
||||||
|
<field name="file_name"/>
|
||||||
|
<field name="notes"/>
|
||||||
|
</group>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<!-- Testing Documents Tab -->
|
||||||
|
<page string="Testing Documents">
|
||||||
|
<field name="testing_document_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="file" filename="file_name"/>
|
||||||
|
<field name="file_name"/>
|
||||||
|
<field name="notes"/>
|
||||||
|
</list>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="file" filename="file_name"/>
|
||||||
|
<field name="file_name"/>
|
||||||
|
<field name="notes"/>
|
||||||
|
</group>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<xpath expr="//chatter" position="attributes">
|
||||||
|
<attribute name="invisible">not show_task_chatter</attribute>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<!-- List View -->
|
||||||
|
<record id="view_project_role_list" model="ir.ui.view">
|
||||||
|
<field name="name">project.role.list</field>
|
||||||
|
<field name="model">project.role</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Project Roles" decoration-info="active==False" multi_edit="1">
|
||||||
|
<field name="role_level"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="user_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Form View -->
|
||||||
|
<record id="view_project_role_form" model="ir.ui.view">
|
||||||
|
<field name="name">project.role.form</field>
|
||||||
|
<field name="model">project.role</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Project Role">
|
||||||
|
<header>
|
||||||
|
<button name="action_view_users" type="object"
|
||||||
|
class="oe_stat_button" icon="fa-users"
|
||||||
|
invisible="not user_ids" string="Users">
|
||||||
|
<field name="user_ids" widget="statinfo" string="" nolable="1"/>
|
||||||
|
</button>
|
||||||
|
<field name="active" widget="boolean_toggle"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button name="action_update_users" type="object" string="Modify Users" class="btn-primary"/>
|
||||||
|
</div>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Role Name..."/>
|
||||||
|
</h1>
|
||||||
|
<h2>
|
||||||
|
<field name="role_level" readonly="1"/>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="color" widget="color_picker"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Description">
|
||||||
|
<field name="description" placeholder="Enter role description..."/>
|
||||||
|
</page>
|
||||||
|
<page string="Assigned Users">
|
||||||
|
<field name="user_ids" can_create="False" can_write="False">
|
||||||
|
<list create="0" edit="0" delete="0">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="login"/>
|
||||||
|
<field name="company_id"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Kanban View -->
|
||||||
|
<record id="view_project_role_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">project.role.kanban</field>
|
||||||
|
<field name="model">project.role</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban class="o_kanban_mobile">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="color"/>
|
||||||
|
<field name="user_ids"/>
|
||||||
|
<field name="role_level"/>
|
||||||
|
<field name="active"/>
|
||||||
|
<templates>
|
||||||
|
<t t-name="kanban-box">
|
||||||
|
<div t-attf-class="oe_kanban_card oe_kanban_global_click #{!active ? 'o_kanban_disabled' : ''}">
|
||||||
|
<div t-attf-class="oe_kanban_card_header oe_kanban_color_#{color}">
|
||||||
|
<div class="o_kanban_record_top">
|
||||||
|
<div class="o_kanban_record_headings">
|
||||||
|
<strong class="o_kanban_record_title">
|
||||||
|
<field name="name"/>
|
||||||
|
</strong>
|
||||||
|
<span class="badge badge-pill badge-light">
|
||||||
|
<field name="role_level"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="oe_kanban_card_content">
|
||||||
|
<div class="o_kanban_tags_section">
|
||||||
|
<field name="user_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Search View -->
|
||||||
|
<record id="view_project_role_search" model="ir.ui.view">
|
||||||
|
<field name="name">project.role.search</field>
|
||||||
|
<field name="model">project.role</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Project Roles">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="user_ids"/>
|
||||||
|
<field name="role_level"/>
|
||||||
|
<separator/>
|
||||||
|
<filter name="active" string="Active" domain="[('active', '=', True)]"/>
|
||||||
|
<filter name="inactive" string="Inactive" domain="[('active', '=', False)]"/>
|
||||||
|
<separator/>
|
||||||
|
<group expand="0" string="Group By">
|
||||||
|
<filter name="group_active" string="Status" context="{'group_by': 'active'}"/>
|
||||||
|
</group>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action Window -->
|
||||||
|
<record id="action_project_role" model="ir.actions.act_window">
|
||||||
|
<field name="name">Project Roles</field>
|
||||||
|
<field name="res_model">project.role</field>
|
||||||
|
<field name="view_mode">list,kanban,form</field>
|
||||||
|
<field name="context">{'search_default_active': 1}</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Create your first project role
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Define roles for your projects and assign users to them.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menu Item -->
|
||||||
|
<menuitem id="menu_project_role_root"
|
||||||
|
name="Project Roles"
|
||||||
|
sequence="65"
|
||||||
|
parent="project.menu_main_pm"/>
|
||||||
|
<menuitem id="menu_project_role"
|
||||||
|
name="Roles"
|
||||||
|
action="action_project_role"
|
||||||
|
parent="menu_project_role_root"
|
||||||
|
sequence="10"/>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="project_project_stage_list" model="ir.ui.view">
|
||||||
|
<field name="name">project.project.stage.list.inherit</field>
|
||||||
|
<field name="model">project.project.stage</field>
|
||||||
|
<field name="inherit_id" ref="project.project_project_stage_view_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='fold']" position="after">
|
||||||
|
<field name="role_ids" widget="many2many_tags" options="{'color_field': 'color','no_open': True, 'no_create': True,'no_edit': True,'no_quick_create': True}"/>
|
||||||
|
<field name="user_ids" widget="many2many_tags" options="{'color_field': 'color','no_open': True, 'no_create': True,'no_edit': True,'no_quick_create': True}"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Extend Project Stage Form View -->
|
||||||
|
<record id="project_project_stage_form" model="ir.ui.view">
|
||||||
|
<field name="name">project.project.stage.form.inherit</field>
|
||||||
|
<field name="model">project.project.stage</field>
|
||||||
|
<field name="inherit_id" ref="project.project_project_stage_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='fold']" position="after">
|
||||||
|
<field name="role_ids" options="{'color_field': 'color','no_open': True, 'no_create': True,'no_edit': True,'no_quick_create': True}">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="user_ids" widget="many2many_tags" options="{'color_field': 'color','no_open': True, 'no_create': True,'no_edit': True,'no_quick_create': True}"/>
|
||||||
|
</list>
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="description"/>
|
||||||
|
<field name="user_ids" options="{'color_field': 'color','no_open': True, 'no_create': True,'no_edit': True,'no_quick_create': True}"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
<field name="user_ids" widget="many2many_tags" options="{'color_field': 'color','no_open': True, 'no_create': True,'no_edit': True,'no_quick_create': True}" readonly="1"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Extend Project Stage Kanban View -->
|
||||||
|
<!-- <record id="project_project_stage_kanban" model="ir.ui.view">-->
|
||||||
|
<!-- <field name="name">project.project.stage.kanban.inherit</field>-->
|
||||||
|
<!-- <field name="model">project.project.stage</field>-->
|
||||||
|
<!-- <field name="inherit_id" ref="project.project_project_stage_view_kanban"/>-->
|
||||||
|
<!-- <field name="arch" type="xml">-->
|
||||||
|
<!-- <xpath expr="//kanban/template/t/" position="inside">-->
|
||||||
|
<!-- <div class="o_kanban_tags_section">-->
|
||||||
|
<!-- <field name="role_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- <div class="o_kanban_tags_section">-->
|
||||||
|
<!-- <field name="user_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<!-- </xpath>-->
|
||||||
|
<!-- </field>-->
|
||||||
|
<!-- </record>-->
|
||||||
|
</odoo>
|
||||||
|
|
@ -10,531 +10,4 @@
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="project_project_inherit_form_view2" 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="//form/header" position="inside">
|
|
||||||
<button type="object" name="submit_project_for_approval"
|
|
||||||
string="Submit for Approval"
|
|
||||||
class="oe_highlight"
|
|
||||||
invisible="not assign_approval_flow or not show_submission_button"/>
|
|
||||||
|
|
||||||
<button type="object" name="project_proceed_further"
|
|
||||||
string="Approve & Proceed"
|
|
||||||
class="oe_highlight"
|
|
||||||
invisible="not assign_approval_flow or not show_approval_button"/>
|
|
||||||
|
|
||||||
<button type="object" name="action_open_reject_wizard"
|
|
||||||
string="Reject"
|
|
||||||
class="oe_highlight"
|
|
||||||
invisible="not assign_approval_flow or not show_refuse_button"/>
|
|
||||||
|
|
||||||
<button type="object" name="project_back_button"
|
|
||||||
string="Go Back"
|
|
||||||
class="oe_highlight"
|
|
||||||
invisible="not assign_approval_flow or not show_back_button"/>
|
|
||||||
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//form" position="inside">
|
|
||||||
<field name="showable_stage_ids" invisible="1"/>
|
|
||||||
<field name="assign_approval_flow" invisible="1"/>
|
|
||||||
<field name="manager_level_edit_access" invisible="1"/>
|
|
||||||
<field name="show_submission_button" invisible="1"/>
|
|
||||||
<field name="show_approval_button" invisible="1"/>
|
|
||||||
<field name="show_refuse_button" invisible="1"/>
|
|
||||||
<field name="show_back_button" invisible="1"/>
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//field[@name='stage_id']" position="attributes">
|
|
||||||
<attribute name="domain">[('id', 'in', showable_stage_ids)]</attribute>
|
|
||||||
</xpath>
|
|
||||||
|
|
||||||
|
|
||||||
<xpath expr="//page[@name='settings']" position="before">
|
|
||||||
<page name="project_stages" string="Project Stages" invisible="not assign_approval_flow">
|
|
||||||
<field name="project_stages" options="{'no_create': True, 'no_open': True, 'no_delete': True}">
|
|
||||||
<list editable="bottom" delete="0" create="0">
|
|
||||||
<field name="stage_id" readonly="1"/>
|
|
||||||
<field name="approval_by" readonly="not manager_level_edit_access"/>
|
|
||||||
<field name="assigned_to" readonly="not manager_level_edit_access"/>
|
|
||||||
<field name="assigned_date" readonly="1" optional="hide"/>
|
|
||||||
<field name="submission_date" readonly="1" optional="hide"/>
|
|
||||||
<field name="note" optional="show" readonly="not manager_level_edit_access"/>
|
|
||||||
<field name="project_id" invisible="1" column_invisible="1"/>
|
|
||||||
<field name="stage_approval_by" invisible="1" column_invisible="1"/>
|
|
||||||
<field name="approval_by_users" invisible="1" column_invisible="1"/>
|
|
||||||
<field name="manager_level_edit_access" invisible="1" column_invisible="1"/>
|
|
||||||
</list>
|
|
||||||
</field>
|
|
||||||
</page>
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//chatter" position="attributes">
|
|
||||||
<attribute name="invisible">not show_project_chatter</attribute>
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//field[@name='user_id']" position="before">
|
|
||||||
<field name="project_sponsor" widget="many2one_avatar_user"/>
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//header/field[@name='stage_id']" position="attributes">
|
|
||||||
<attribute name="readonly">assign_approval_flow</attribute>
|
|
||||||
</xpath>
|
|
||||||
<page name="description" position="attributes">
|
|
||||||
<attribute name="string">Initiation</attribute>
|
|
||||||
</page>
|
|
||||||
|
|
||||||
<xpath expr="//field[@name='description']" position="attributes">
|
|
||||||
<attribute name="invisible">1</attribute>
|
|
||||||
</xpath>
|
|
||||||
<page name="description" position="inside">
|
|
||||||
<group>
|
|
||||||
<field name="project_vision" placeholder="Eg: Build a mobile app that allows users to order groceries and track delivery in real time." readonly="not manager_level_edit_access"/>
|
|
||||||
</group>
|
|
||||||
<group string="Requirements Document">
|
|
||||||
|
|
||||||
<div class="o_row" style="align-items: flex-start;">
|
|
||||||
|
|
||||||
<!-- LEFT SIDE -->
|
|
||||||
<div class="o_col" style="width: 70%;">
|
|
||||||
|
|
||||||
<!-- HTML field (visible when NO file uploaded) -->
|
|
||||||
<field name="description"
|
|
||||||
widget="html" force_save="1" readonly="not manager_level_edit_access"
|
|
||||||
invisible="requirement_file" placeholder="The system should allow user login,
|
|
||||||
Users should be able to add items to a cart."/>
|
|
||||||
|
|
||||||
<!-- PDF Viewer (visible when file exists) -->
|
|
||||||
<field name="requirement_file"
|
|
||||||
widget="binary" force_save="1" readonly="not manager_level_edit_access"
|
|
||||||
invisible="not requirement_file"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- RIGHT SIDE -->
|
|
||||||
<div class="o_col" style="width: 30%; padding-left: 20px;">
|
|
||||||
|
|
||||||
<!-- Upload button (visible when NO file exists) -->
|
|
||||||
<field name="requirement_file"
|
|
||||||
widget="binary" force_save="1"
|
|
||||||
filename="requirement_file_name"
|
|
||||||
invisible="requirement_file or not manager_level_edit_access"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</group>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- ===================== -->
|
|
||||||
<!-- FEASIBILITY ASSESSMENT -->
|
|
||||||
<!-- ===================== -->
|
|
||||||
<group string="Feasibility Assessment">
|
|
||||||
|
|
||||||
<div class="o_row" style="align-items: flex-start;">
|
|
||||||
|
|
||||||
<!-- LEFT SIDE -->
|
|
||||||
<div class="o_col" style="width: 70%;">
|
|
||||||
|
|
||||||
<!-- HTML field -->
|
|
||||||
<field name="feasibility_html"
|
|
||||||
widget="html" force_save="1"
|
|
||||||
readonly="not manager_level_edit_access"
|
|
||||||
invisible="feasibility_file" placeholder="Check whether the project is technically, financially, and operationally possible."/>
|
|
||||||
|
|
||||||
<!-- PDF Viewer -->
|
|
||||||
<field name="feasibility_file"
|
|
||||||
widget="binary" force_save="1" readonly="not manager_level_edit_access"
|
|
||||||
invisible="not feasibility_file"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- RIGHT SIDE -->
|
|
||||||
<div class="o_col" style="width: 30%; padding-left: 20px;">
|
|
||||||
|
|
||||||
<!-- Upload Field -->
|
|
||||||
<field name="feasibility_file"
|
|
||||||
widget="binary" force_save="1"
|
|
||||||
filename="feasibility_file_name"
|
|
||||||
invisible="feasibility_file or not manager_level_edit_access"/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</group>
|
|
||||||
</page>
|
|
||||||
<xpath expr="//page[@name='settings']" position="inside">
|
|
||||||
<group>
|
|
||||||
<group name="group_sprint_requirement_management" string="Project Sprint" col="1"
|
|
||||||
class="row mt16 o_settings_container">
|
|
||||||
<div>
|
|
||||||
<setting class="col-lg-12" id="project_sprint_requirement_settings"
|
|
||||||
help="Enable it if you want to add Sprints for the project">
|
|
||||||
<field name="require_sprint"/>
|
|
||||||
</setting>
|
|
||||||
</div>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
</group>
|
|
||||||
</xpath>
|
|
||||||
|
|
||||||
<xpath expr="//sheet" position="inside">
|
|
||||||
<widget name="web_ribbon" title="Rejected" bg_color="text-bg-danger" invisible="approval_status != 'reject'" />
|
|
||||||
<widget name="web_ribbon" title="Rejected" invisible="approval_status != 'submitted'" />
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//notebook" position="inside">
|
|
||||||
<page string="Project Activity Log" invisible="not assign_approval_flow or not show_project_chatter">
|
|
||||||
<field name="project_activity_log" widget="html" options="{'sanitize': False}" readonly="1"
|
|
||||||
force_save="1"/>
|
|
||||||
</page>
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//field[@name='date_start']" position="attributes">
|
|
||||||
<attribute name="invisible">1</attribute>
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//field[@name='date']" position="attributes">
|
|
||||||
<attribute name="invisible">1</attribute>
|
|
||||||
</xpath>
|
|
||||||
|
|
||||||
<xpath expr="//sheet/notebook/page[@name='settings']" position="after">
|
|
||||||
<page name="planning" string="Planning (Budget & Deadlines)">
|
|
||||||
<group>
|
|
||||||
|
|
||||||
<group string="project Scope">
|
|
||||||
<field name="project_scope" nolabel="1"/>
|
|
||||||
</group>
|
|
||||||
<group string="Timelines">
|
|
||||||
<group>
|
|
||||||
|
|
||||||
<field name="date_start" string="Planned Date" widget="daterange"
|
|
||||||
options='{"end_date_field": "date", "always_range": "1"}'
|
|
||||||
required="date_start or date"/>
|
|
||||||
<field name="date" invisible="1" required="date_start"/>
|
|
||||||
|
|
||||||
</group>
|
|
||||||
<group></group>
|
|
||||||
<group>
|
|
||||||
<field name="allocated_hours" widget="timesheet_uom_no_toggle" optional="hide"
|
|
||||||
invisible="allocated_hours == 0 or not allow_timesheets"
|
|
||||||
groups="hr_timesheet.group_hr_timesheet_user"/>
|
|
||||||
<field name="effective_hours" widget="timesheet_uom_no_toggle" optional="hide"
|
|
||||||
invisible="effective_hours == 0 or not allow_timesheets"
|
|
||||||
groups="hr_timesheet.group_hr_timesheet_user"/>
|
|
||||||
<field name="remaining_hours" widget="timesheet_uom_no_toggle"
|
|
||||||
decoration-danger="remaining_hours < 0"
|
|
||||||
decoration-warning="allocated_hours > 0 and (remaining_hours / allocated_hours) < 0.2"
|
|
||||||
optional="hide"
|
|
||||||
invisible="allocated_hours == 0 or not allow_timesheets"
|
|
||||||
groups="hr_timesheet.group_hr_timesheet_user"
|
|
||||||
/>
|
|
||||||
<field name="estimated_hours" widget="timesheet_uom_no_toggle"
|
|
||||||
invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>
|
|
||||||
</group>
|
|
||||||
<group>
|
|
||||||
|
|
||||||
<field name="task_estimated_hours" widget="timesheet_uom_no_toggle"
|
|
||||||
invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>
|
|
||||||
<field name="actual_hours" widget="timesheet_uom_no_toggle"
|
|
||||||
invisible="not allow_timesheets"
|
|
||||||
groups="hr_timesheet.group_hr_timesheet_user"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
</group>
|
|
||||||
<group string="Risk Management Plan">
|
|
||||||
<field name="risk_ids" nolabel="1">
|
|
||||||
<list editable="bottom">
|
|
||||||
<field name="project_id" invisible="1" column_invisible="1"/>
|
|
||||||
<field name="risk_description" width="30%"/>
|
|
||||||
<field name="probability" width="20%"/>
|
|
||||||
<field name="impact" width="20%"/>
|
|
||||||
<field name="mitigation_plan" width="30%"/>
|
|
||||||
</list>
|
|
||||||
</field>
|
|
||||||
|
|
||||||
</group>
|
|
||||||
<group string="Budget Planning">
|
|
||||||
<group>
|
|
||||||
<field name="estimated_amount"/>
|
|
||||||
<field name="total_budget_amount" readonly="1"/>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<notebook>
|
|
||||||
<!-- MANPOWER TAB -->
|
|
||||||
<page string="Manpower">
|
|
||||||
|
|
||||||
<button name="action_fetch_resource_data"
|
|
||||||
type="object"
|
|
||||||
string="Fetch Resource Data"
|
|
||||||
class="oe_highlight"
|
|
||||||
icon="fa-refresh"/>
|
|
||||||
|
|
||||||
<field name="resource_cost_ids">
|
|
||||||
<list editable="bottom">
|
|
||||||
<field name="employee_id"/>
|
|
||||||
<field name="monthly_salary"/>
|
|
||||||
<field name="daily_rate"/>
|
|
||||||
<field name="start_date" optional="hide"/>
|
|
||||||
<field name="end_date" optional="hide"/>
|
|
||||||
<field name="duration_days" readonly="1"/>
|
|
||||||
<field name="total_cost"/>
|
|
||||||
</list>
|
|
||||||
</field>
|
|
||||||
</page>
|
|
||||||
|
|
||||||
<!-- MATERIAL TAB -->
|
|
||||||
<page string="Material">
|
|
||||||
<field name="material_cost_ids">
|
|
||||||
<list editable="bottom">
|
|
||||||
<field name="material_name" width="30%"/>
|
|
||||||
<field name="qty" width="20%"/>
|
|
||||||
<field name="unit_cost" width="20%"/>
|
|
||||||
<field name="total_cost" readonly="1" width="20%"/>
|
|
||||||
</list>
|
|
||||||
</field>
|
|
||||||
</page>
|
|
||||||
|
|
||||||
<!-- EQUIPMENT TAB -->
|
|
||||||
<page string="Equipments/Others">
|
|
||||||
<field name="equipment_cost_ids">
|
|
||||||
<list editable="bottom">
|
|
||||||
<field name="equipment_name" string="Equip/Others" width="30%"/>
|
|
||||||
<field name="duration_hours" width="20%"/>
|
|
||||||
<field name="hourly_rate" width="20%"/>
|
|
||||||
<field name="total_cost" readonly="1" width="20%"/>
|
|
||||||
</list>
|
|
||||||
</field>
|
|
||||||
</page>
|
|
||||||
|
|
||||||
</notebook>
|
|
||||||
</group>
|
|
||||||
</page>
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//sheet/notebook" position="inside">
|
|
||||||
<page name="architecture_design" string="Architecture & Design">
|
|
||||||
|
|
||||||
<field name="architecture_design_ids">
|
|
||||||
<list>
|
|
||||||
<field name="tech_stack"/>
|
|
||||||
<field name="design_status"/>
|
|
||||||
<field name="architecture_notes" optional="hide"/>
|
|
||||||
<field name="database_notes" optional="hide"/>
|
|
||||||
<field name="reviewer_comments" optional="hide"/>
|
|
||||||
</list>
|
|
||||||
|
|
||||||
<form>
|
|
||||||
<sheet>
|
|
||||||
<group string="System Architecture">
|
|
||||||
<field name="architecture_diagram"/>
|
|
||||||
<field name="tech_stack"/>
|
|
||||||
<field name="architecture_notes"/>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<group>
|
|
||||||
<group string="UI / UX">
|
|
||||||
<field name="ui_wireframe"/>
|
|
||||||
<field name="ux_flow"/>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<group string="Database Design">
|
|
||||||
<field name="er_diagram"/>
|
|
||||||
<field name="database_notes"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<group string="Review">
|
|
||||||
<field name="design_status"/>
|
|
||||||
<field name="reviewer_comments"/>
|
|
||||||
</group>
|
|
||||||
</sheet>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</field>
|
|
||||||
|
|
||||||
</page>
|
|
||||||
<page name="project_sprints" string="Sprints" invisible="not require_sprint">
|
|
||||||
<field name="sprint_ids">
|
|
||||||
<list editable="bottom">
|
|
||||||
<field name="sprint_name" width="20%"/>
|
|
||||||
<field name="date_start" width="20%"/>
|
|
||||||
<field name="date_end" width="20%"/>
|
|
||||||
<field name="allocated_hours" width="20%"/>
|
|
||||||
<field name="status" width="20%"/>
|
|
||||||
<field name="done_date" optional="hide"/>
|
|
||||||
<field name="note" optional="hide"/>
|
|
||||||
</list>
|
|
||||||
</field>
|
|
||||||
</page>
|
|
||||||
<page name="development" string="Development Details">
|
|
||||||
<group string="Documents">
|
|
||||||
<field name="development_document_ids">
|
|
||||||
<list editable="bottom">
|
|
||||||
<field name="name"/>
|
|
||||||
<field name="file" filename="file_name" widget="binary"/>
|
|
||||||
<field name="file_name"/>
|
|
||||||
<field name="notes"/>
|
|
||||||
</list>
|
|
||||||
|
|
||||||
<form>
|
|
||||||
<group>
|
|
||||||
<field name="name"/>
|
|
||||||
<field name="file" filename="file_name" widget="binary"/>
|
|
||||||
<field name="file_name"/>
|
|
||||||
<field name="notes"/>
|
|
||||||
</group>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<group string="Code Commit Documents">
|
|
||||||
<field name="commit_step_ids" readonly="1" create="0" edit="0">
|
|
||||||
<list>
|
|
||||||
|
|
||||||
<field name="task_id"/>
|
|
||||||
<field name="sprint_id" optional="hide"/>
|
|
||||||
<field name="commit_date"/>
|
|
||||||
<field name="commit_code" widget="html" optional="hide"/>
|
|
||||||
<field name="commit_message"/>
|
|
||||||
<field name="branch_name"/>
|
|
||||||
<field name="files_changed"/>
|
|
||||||
<field name="notes" optional="hide"/>
|
|
||||||
|
|
||||||
</list>
|
|
||||||
</field>
|
|
||||||
|
|
||||||
</group>
|
|
||||||
<group string="Notes">
|
|
||||||
<field name="development_notes" nolabel="1" placeholder="click hear to write comments"/>
|
|
||||||
</group>
|
|
||||||
</page>
|
|
||||||
<page string="Testing Documents">
|
|
||||||
<group string="Documents">
|
|
||||||
<field name="testing_document_ids">
|
|
||||||
<list editable="bottom">
|
|
||||||
<field name="name"/>
|
|
||||||
<field name="file" filename="file_name"/>
|
|
||||||
<field name="file_name"/>
|
|
||||||
<field name="notes"/>
|
|
||||||
</list>
|
|
||||||
|
|
||||||
<form>
|
|
||||||
<group>
|
|
||||||
<field name="name"/>
|
|
||||||
<field name="file" filename="file_name"/>
|
|
||||||
<field name="file_name"/>
|
|
||||||
<field name="notes"/>
|
|
||||||
</group>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
|
|
||||||
</group>
|
|
||||||
<group string="Notes">
|
|
||||||
<field name="testing_notes" nolabel="1" placeholder="click hear to write comments"/>
|
|
||||||
</group>
|
|
||||||
</page>
|
|
||||||
</xpath>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
|
|
||||||
<record id="project_invoice_form_inherit" model="ir.ui.view">
|
|
||||||
<field name="name">project.invoice.inherit.form.view</field>
|
|
||||||
<field name="model">project.project</field>
|
|
||||||
<field name="inherit_id" ref="hr_timesheet.project_invoice_form"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//field[@name='allocated_hours']" position="attributes">
|
|
||||||
<attribute name="invisible">True</attribute>
|
|
||||||
<!-- <field name="estimated_hours" widget="timesheet_uom_no_toggle" invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>-->
|
|
||||||
<!-- <field name="task_estimated_hours" widget="timesheet_uom_no_toggle" invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>-->
|
|
||||||
<!-- <field name="actual_hours" widget="timesheet_uom_no_toggle" invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>-->
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//field[@name='effective_hours']" position="attributes">
|
|
||||||
<attribute name="invisible">True</attribute>
|
|
||||||
</xpath>
|
|
||||||
<xpath expr="//field[@name='remaining_hours']" position="attributes">
|
|
||||||
<attribute name="invisible">True</attribute>
|
|
||||||
</xpath>
|
|
||||||
|
|
||||||
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
|
|
||||||
<record id="view_task_form_commit_steps" model="ir.ui.view">
|
|
||||||
<field name="name">project.task.form.commit.steps</field>
|
|
||||||
<field name="model">project.task</field>
|
|
||||||
<field name="inherit_id" ref="project.view_task_form2"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
|
|
||||||
<xpath expr="//notebook" position="inside">
|
|
||||||
<page string="Commit Steps">
|
|
||||||
<field name="commit_step_ids">
|
|
||||||
<list editable="bottom">
|
|
||||||
<field name="commit_date"/>
|
|
||||||
<field name="commit_code" widget="html"/>
|
|
||||||
<field name="commit_message"/>
|
|
||||||
<field name="branch_name"/>
|
|
||||||
<field name="files_changed"/>
|
|
||||||
<field name="notes"/>
|
|
||||||
</list>
|
|
||||||
|
|
||||||
<form>
|
|
||||||
<group>
|
|
||||||
<field name="commit_code" widget="html"/>
|
|
||||||
<field name="commit_message"/>
|
|
||||||
<field name="branch_name"/>
|
|
||||||
<field name="files_changed"/>
|
|
||||||
<field name="commit_date"/>
|
|
||||||
<field name="notes"/>
|
|
||||||
</group>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</page>
|
|
||||||
<!-- Development Documents Tab -->
|
|
||||||
<page string="Development Documents">
|
|
||||||
<field name="development_document_ids">
|
|
||||||
<list editable="bottom">
|
|
||||||
<field name="name"/>
|
|
||||||
<field name="file" filename="file_name"/>
|
|
||||||
<field name="file_name"/>
|
|
||||||
<field name="notes"/>
|
|
||||||
</list>
|
|
||||||
|
|
||||||
<form>
|
|
||||||
<group>
|
|
||||||
<field name="name"/>
|
|
||||||
<field name="file" filename="file_name"/>
|
|
||||||
<field name="file_name"/>
|
|
||||||
<field name="notes"/>
|
|
||||||
</group>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</page>
|
|
||||||
|
|
||||||
<!-- Testing Documents Tab -->
|
|
||||||
<page string="Testing Documents">
|
|
||||||
<field name="testing_document_ids">
|
|
||||||
<list editable="bottom">
|
|
||||||
<field name="name"/>
|
|
||||||
<field name="file" filename="file_name"/>
|
|
||||||
<field name="file_name"/>
|
|
||||||
<field name="notes"/>
|
|
||||||
</list>
|
|
||||||
|
|
||||||
<form>
|
|
||||||
<group>
|
|
||||||
<field name="name"/>
|
|
||||||
<field name="file" filename="file_name"/>
|
|
||||||
<field name="file_name"/>
|
|
||||||
<field name="notes"/>
|
|
||||||
</group>
|
|
||||||
</form>
|
|
||||||
</field>
|
|
||||||
</page>
|
|
||||||
</xpath>
|
|
||||||
|
|
||||||
<xpath expr="//chatter" position="attributes">
|
|
||||||
<attribute name="invisible">not show_task_chatter</attribute>
|
|
||||||
</xpath>
|
|
||||||
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
<xpath expr="//field[@name='name']" position="after">
|
<xpath expr="//field[@name='name']" position="after">
|
||||||
<field name="team_id" optional="show"/>
|
<field name="team_id" optional="show"/>
|
||||||
<field name="approval_by" optional="show"/>
|
<field name="approval_by" optional="show"/>
|
||||||
|
<field name="involved_user_ids" optional="show"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from . import project_user_assign_wizard
|
from . import project_user_assign_wizard
|
||||||
|
from . import roles_user_assign_wizard
|
||||||
from . import internal_team_members_wizard
|
from . import internal_team_members_wizard
|
||||||
from . import project_stage_update_wizard
|
from . import project_stage_update_wizard
|
||||||
from . import task_reject_reason_wizard
|
from . import task_reject_reason_wizard
|
||||||
|
|
@ -15,8 +15,19 @@ class ProjectStageUpdateWizard(models.TransientModel):
|
||||||
('project_lead', 'Project Lead / Manager')
|
('project_lead', 'Project Lead / Manager')
|
||||||
], readonly=False)
|
], readonly=False)
|
||||||
fold = fields.Boolean(string='Folded in Kanban', readonly=False)
|
fold = fields.Boolean(string='Folded in Kanban', readonly=False)
|
||||||
|
involved_user_ids = fields.Many2many('res.users', domain="[('id','in',related_user_ids)]")
|
||||||
|
related_user_ids = fields.Many2many(related="team_id.all_members_ids")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@api.onchange('team_id')
|
||||||
|
def onchange_team_id(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.team_id and rec.team_id.all_members_ids:
|
||||||
|
rec.involved_user_ids = [(6,0,rec.team_id.all_members_ids.ids)]
|
||||||
|
else:
|
||||||
|
rec.involved_user_ids = [(5, 0)]
|
||||||
|
|
||||||
def action_save_changes(self):
|
def action_save_changes(self):
|
||||||
"""Create/update the stage and sync tasks and project links."""
|
"""Create/update the stage and sync tasks and project links."""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
@ -29,6 +40,7 @@ class ProjectStageUpdateWizard(models.TransientModel):
|
||||||
('team_id', '=', self.team_id.id),
|
('team_id', '=', self.team_id.id),
|
||||||
('approval_by', '=', self.approval_by),
|
('approval_by', '=', self.approval_by),
|
||||||
('fold', '=', self.fold),
|
('fold', '=', self.fold),
|
||||||
|
('involved_user_ids','=',self.involved_user_ids.ids)
|
||||||
], limit=1)
|
], limit=1)
|
||||||
|
|
||||||
if existing_stage:
|
if existing_stage:
|
||||||
|
|
@ -41,6 +53,7 @@ class ProjectStageUpdateWizard(models.TransientModel):
|
||||||
'approval_by': self.approval_by ,
|
'approval_by': self.approval_by ,
|
||||||
'fold': self.fold,
|
'fold': self.fold,
|
||||||
'sequence': old_stage.sequence, # optional: keep same order
|
'sequence': old_stage.sequence, # optional: keep same order
|
||||||
|
'involved_user_ids': [(6,0,self.involved_user_ids.ids)]
|
||||||
})
|
})
|
||||||
|
|
||||||
# If new_stage is different from old_stage → update references
|
# If new_stage is different from old_stage → update references
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@
|
||||||
<field name="project_id" invisible="1"/>
|
<field name="project_id" invisible="1"/>
|
||||||
<field name="stage_id"/>
|
<field name="stage_id"/>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="team_id"/>
|
<field name="team_id" required="approval_by == 'assigned_team_lead'"/>
|
||||||
<field name="approval_by"/>
|
<field name="approval_by"/>
|
||||||
|
<field name="involved_user_ids" widget="many2many_tags"/>
|
||||||
|
<field name="related_user_ids" invisible="1"/>
|
||||||
<field name="fold"/>
|
<field name="fold"/>
|
||||||
</group>
|
</group>
|
||||||
<footer>
|
<footer>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
class RolesUserAssignWizard(models.TransientModel):
|
||||||
|
_name = "roles.user.assign.wizard"
|
||||||
|
|
||||||
|
team_ids = fields.Many2many("internal.teams")
|
||||||
|
members_ids = fields.Many2many(
|
||||||
|
comodel_name='res.users',
|
||||||
|
relation='roles_assign_members_rel',
|
||||||
|
column1='wizard_id',
|
||||||
|
column2='user_id',
|
||||||
|
string="Members",
|
||||||
|
help="Members are the users who are working in this particular Project"
|
||||||
|
)
|
||||||
|
|
||||||
|
@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_roles(self):
|
||||||
|
record_id = self.env.context.get('active_id')
|
||||||
|
record = self.env['project.role'].sudo().search([('id','=',record_id)])
|
||||||
|
record.update({
|
||||||
|
"user_ids": [(6, 0, self.members_ids.ids)],
|
||||||
|
})
|
||||||
|
return {'type': 'ir.actions.act_window_close'}
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="roles_user_assignment_form_view" model="ir.ui.view">
|
||||||
|
<field name="name">roles.user.assign.wizard.form.view</field>
|
||||||
|
<field name="model">roles.user.assign.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="team_ids" widget="many2many_tags"/>
|
||||||
|
<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_roles" 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="roles_user_assignment_action_tree" model="ir.actions.act_window">
|
||||||
|
<field name="name">roles User Assignment</field>
|
||||||
|
<field name="res_model">roles.user.assign.wizard</field>
|
||||||
|
<field name="binding_view_types">form</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
|
|
@ -58,6 +58,9 @@ class AttachmentPreview(http.Controller):
|
||||||
|
|
||||||
@http.route("/lookup_or_create/attachment", type="json", auth="user")
|
@http.route("/lookup_or_create/attachment", type="json", auth="user")
|
||||||
def lookup_or_create_attachment(self, model, res_id, field):
|
def lookup_or_create_attachment(self, model, res_id, field):
|
||||||
|
if model == 'ir.attachment':
|
||||||
|
attach = request.env['ir.attachment'].sudo().browse(res_id)
|
||||||
|
else:
|
||||||
record = request.env[model].sudo().browse(int(res_id))
|
record = request.env[model].sudo().browse(int(res_id))
|
||||||
if not record.exists():
|
if not record.exists():
|
||||||
return {"error": "Record not found"}
|
return {"error": "Record not found"}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,19 @@
|
||||||
<img t-att-src="props.url"
|
<img t-att-src="props.url"
|
||||||
style="max-width:100%; max-height:80vh;" />
|
style="max-width:100%; max-height:80vh;" />
|
||||||
</t>
|
</t>
|
||||||
|
<t t-elif="props.mimetype.startsWith('text')
|
||||||
|
or props.filename.endsWith('.txt')
|
||||||
|
or props.filename.endsWith('.log')
|
||||||
|
or props.filename.endsWith('.md')
|
||||||
|
or props.filename.endsWith('.json')
|
||||||
|
or props.filename.endsWith('.xml')
|
||||||
|
or props.filename.endsWith('.py')
|
||||||
|
">
|
||||||
|
<iframe t-att-src="props.url"
|
||||||
|
style="width:100%; height:500px;"
|
||||||
|
frameborder="0">
|
||||||
|
</iframe>
|
||||||
|
</t>
|
||||||
<t t-elif="
|
<t t-elif="
|
||||||
props.mimetype.startsWith('audio') or
|
props.mimetype.startsWith('audio') or
|
||||||
props.mimetype.endsWith('mp3') or
|
props.mimetype.endsWith('mp3') or
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ patch(BinaryField.prototype, {
|
||||||
},
|
},
|
||||||
|
|
||||||
async onPreviewFile(ev) {
|
async onPreviewFile(ev) {
|
||||||
|
debugger;
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue