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',
|
||||
'data/data.xml',
|
||||
'views/masters.xml',
|
||||
'views/groups.xml',
|
||||
'views/login.xml',
|
||||
'views/menu_access_control_views.xml',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ class CustomMasterLogin(Home):
|
|||
# load your masters
|
||||
masters = request.env['master.control'].sudo().search([])
|
||||
response.qcontext['masters'] = masters
|
||||
request.env['ir.ui.menu'].sudo().clear_caches()
|
||||
request.env['ir.ui.menu'].sudo()._visible_menu_ids()
|
||||
|
||||
# After successful login
|
||||
if request.session.uid and master_selected:
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from . import masters
|
||||
from . import ir_http
|
||||
from . import groups
|
||||
from . import models
|
||||
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):
|
||||
_name = 'master.control'
|
||||
|
|
@ -10,6 +10,13 @@ class MasterControl(models.Model):
|
|||
default_show = fields.Boolean(default=True)
|
||||
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):
|
||||
"""Generate category → groups list"""
|
||||
|
|
@ -42,6 +49,8 @@ class MasterControl(models.Model):
|
|||
# UPDATE GROUPS (Detect new groups)
|
||||
# -----------------------------------------
|
||||
def action_update_groups(self):
|
||||
import pdb
|
||||
pdb.set_trace()
|
||||
for rec in self:
|
||||
created_count = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
from passlib.apps import master_context
|
||||
|
||||
from odoo import models, fields, api, tools, _
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo.http import request
|
||||
|
||||
class IrUiMenu(models.Model):
|
||||
_inherit = 'ir.ui.menu'
|
||||
|
|
@ -8,41 +10,100 @@ class IrUiMenu(models.Model):
|
|||
@api.model
|
||||
@tools.ormcache('frozenset(self.env.user.groups_id.ids)', 'debug')
|
||||
def _visible_menu_ids(self, debug=False):
|
||||
""" Return the ids of the menu items visible to the user. """
|
||||
# retrieve all menus, and determine which ones are visible
|
||||
"""Return the IDs of menu items visible to the current user based on permissions and active master."""
|
||||
# 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}
|
||||
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())
|
||||
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
|
||||
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
|
||||
group_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))
|
||||
|
||||
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)}
|
||||
# Filter menus by group permissions
|
||||
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)
|
||||
for action in menus.mapped('action'):
|
||||
if action:
|
||||
actions_by_model[action._name].add(action.id)
|
||||
|
||||
existing_actions = {
|
||||
action
|
||||
for model_name, action_ids in actions_by_model.items()
|
||||
for action in self.env[model_name].browse(action_ids).exists()
|
||||
}
|
||||
|
||||
action_menus = menus.filtered(lambda m: m.action and m.action in existing_actions)
|
||||
folder_menus = menus - action_menus
|
||||
visible = self.browse()
|
||||
|
||||
# process action menus, check whether their action is allowed
|
||||
# Model access check configuration
|
||||
access = self.env['ir.model.access']
|
||||
MODEL_BY_TYPE = {
|
||||
'ir.actions.act_window': 'res_model',
|
||||
|
|
@ -50,21 +111,22 @@ class IrUiMenu(models.Model):
|
|||
'ir.actions.server': 'model_name',
|
||||
}
|
||||
|
||||
# performance trick: determine the ids to prefetch by type
|
||||
# Prefetch action data for performance
|
||||
prefetch_ids = defaultdict(list)
|
||||
for action in action_menus.mapped('action'):
|
||||
prefetch_ids[action._name].append(action.id)
|
||||
|
||||
# Check access for each action menu
|
||||
for menu in action_menus:
|
||||
action = menu.action
|
||||
action = action.with_prefetch(prefetch_ids[action._name])
|
||||
action = menu.action.with_prefetch(prefetch_ids[action._name])
|
||||
model_name = action._name in MODEL_BY_TYPE and action[MODEL_BY_TYPE[action._name]]
|
||||
if not model_name or access.check(model_name, 'read', False):
|
||||
# make menu visible, and its folder ancestors, too
|
||||
visible += menu
|
||||
menu = menu.parent_id
|
||||
while menu and menu in folder_menus and menu not in visible:
|
||||
visible += menu
|
||||
menu = menu.parent_id
|
||||
|
||||
return set(visible.ids)
|
||||
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'
|
||||
|
||||
_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)
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<field name="control_unit"/>
|
||||
<field name="user_ids" widget="many2many_tags"/>
|
||||
<field name="master_control"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
|
@ -80,6 +81,7 @@
|
|||
<sheet>
|
||||
<group>
|
||||
<field name="control_unit"/>
|
||||
<field name="master_control"/>
|
||||
<field name="user_ids" widget="many2many_tags"/>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
|
|
|
|||
|
|
@ -32,13 +32,17 @@ Key Features:
|
|||
'security/security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/data.xml',
|
||||
'data/project_roles_data.xml',
|
||||
'wizards/project_user_assign_wizard.xml',
|
||||
'wizards/roles_user_assign_wizard.xml',
|
||||
'wizards/internal_team_members_wizard.xml',
|
||||
'wizards/project_stage_update_wizard.xml',
|
||||
'wizards/task_reject_reason_wizard.xml',
|
||||
'view/teams.xml',
|
||||
'view/project_roles_master.xml',
|
||||
'view/project_stages.xml',
|
||||
'view/task_stages.xml',
|
||||
'view/deployment_log.xml',
|
||||
'view/project.xml',
|
||||
'view/project_task.xml',
|
||||
'view/timesheets.xml',
|
||||
|
|
@ -47,6 +51,9 @@ Key Features:
|
|||
# 'view/project_task_gantt.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend':{
|
||||
'project_task_timesheet_extended/static/src/css/delopyment.css'
|
||||
}
|
||||
},
|
||||
'installable': True,
|
||||
'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 project_roles_master
|
||||
from . import project_sprint
|
||||
from . import task_documents
|
||||
from . import project_architecture_design
|
||||
|
|
@ -8,6 +9,7 @@ from . import project_costings
|
|||
from . import project_code_commit
|
||||
from . import project_stages
|
||||
from . import task_stages
|
||||
from . import deployment_log
|
||||
from . import project
|
||||
from . import project_task
|
||||
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.exceptions import UserError, ValidationError
|
||||
|
||||
from markupsafe import Markup
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
|
||||
class ProjectProject(models.Model):
|
||||
_inherit = 'project.project'
|
||||
|
|
@ -25,6 +27,634 @@ class ProjectProject(models.Model):
|
|||
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
|
||||
def _get_default_projects_channel(self):
|
||||
"""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,
|
||||
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',
|
||||
'user_id', 'Project Members', help="""Project's
|
||||
members are users who can have an access to
|
||||
the tasks related to this project."""
|
||||
)
|
||||
user_id = fields.Many2one('res.users', string='Project Manager', default=lambda self: self.env.user, tracking=True,
|
||||
domain=lambda self: [('groups_id', 'in', [self.env.ref('project.group_project_manager').id,self.env.ref('project_task_timesheet_extended.group_project_supervisor').id]),('share','=',False)],)
|
||||
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())
|
||||
|
||||
|
|
@ -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."""
|
||||
if self.project_id:
|
||||
# If project has dates → use them
|
||||
self.start_date = self.project_id.start_date or date.today()
|
||||
self.end_date = self.project_id.end_date or self.start_date
|
||||
self.start_date = self.project_id.date_start or date.today()
|
||||
self.end_date = self.project_id.date or self.start_date
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# 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_approval_by = fields.Selection(related='stage_id.approval_by')
|
||||
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()
|
||||
submission_date = fields.Datetime()
|
||||
project_id = fields.Many2one('project.project')
|
||||
approval_by_users = fields.Many2many('res.users', compute="_compute_all_project_managers")
|
||||
note = fields.Text()
|
||||
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):
|
||||
for rec in self:
|
||||
|
|
@ -50,647 +54,3 @@ class projectStagesApprovalFlow(models.Model):
|
|||
|
||||
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')
|
||||
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):
|
||||
"""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_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_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_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_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_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'))]"/>
|
||||
</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">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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" ?>
|
||||
<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="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.edit_project"/>
|
||||
|
|
@ -39,29 +39,60 @@
|
|||
|
||||
</group>
|
||||
</xpath>
|
||||
<!-- <xpath expr="//field[@name='label_tasks']" position="before">-->
|
||||
<!-- <group string="Project Channel">-->
|
||||
<!-- <field name="discuss_channel_id" widget="many2one"-->
|
||||
<!-- context="{'default_parent_id': default_projects_channel_id}"/>-->
|
||||
<!-- <button name="action_create_project_channel"-->
|
||||
<!-- 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>-->
|
||||
<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"/>
|
||||
|
||||
<page name="settings" position="after">
|
||||
<page string="Team">
|
||||
<group>
|
||||
<button name="add_users" type="object" string="Add/Update" class="btn-primary"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="members_ids" widget="many2many_avatar_user" nolabel="1"
|
||||
options="{'no_create': True, 'no_create_edit': True, 'no_open': True, 'no_quick_create': True}" readonly="1"/>
|
||||
<button type="object" name="project_proceed_further"
|
||||
string="Approve & Proceed"
|
||||
class="oe_highlight"
|
||||
invisible="not assign_approval_flow or not show_approval_button"/>
|
||||
|
||||
</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 name="task_stages" string="Task Stages">
|
||||
<field name="type_ids" context="{'project_id': id}" options="{'no_open': True}">
|
||||
|
|
@ -71,6 +102,7 @@
|
|||
<field name="team_id"/>
|
||||
<field name="approval_by"/>
|
||||
<field name="fold"/>
|
||||
<field name="involved_user_ids" widget="many2many_tags"/>
|
||||
<button name="create_or_update_data"
|
||||
type="object"
|
||||
string="Update"
|
||||
|
|
@ -78,8 +110,603 @@
|
|||
</list>
|
||||
</field>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
|
|
@ -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>
|
||||
</field>
|
||||
</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>
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
<xpath expr="//field[@name='name']" position="after">
|
||||
<field name="team_id" optional="show"/>
|
||||
<field name="approval_by" optional="show"/>
|
||||
<field name="involved_user_ids" optional="show"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from . import project_user_assign_wizard
|
||||
from . import roles_user_assign_wizard
|
||||
from . import internal_team_members_wizard
|
||||
from . import project_stage_update_wizard
|
||||
from . import task_reject_reason_wizard
|
||||
|
|
@ -15,8 +15,19 @@ class ProjectStageUpdateWizard(models.TransientModel):
|
|||
('project_lead', 'Project Lead / Manager')
|
||||
], 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):
|
||||
"""Create/update the stage and sync tasks and project links."""
|
||||
self.ensure_one()
|
||||
|
|
@ -29,6 +40,7 @@ class ProjectStageUpdateWizard(models.TransientModel):
|
|||
('team_id', '=', self.team_id.id),
|
||||
('approval_by', '=', self.approval_by),
|
||||
('fold', '=', self.fold),
|
||||
('involved_user_ids','=',self.involved_user_ids.ids)
|
||||
], limit=1)
|
||||
|
||||
if existing_stage:
|
||||
|
|
@ -41,6 +53,7 @@ class ProjectStageUpdateWizard(models.TransientModel):
|
|||
'approval_by': self.approval_by ,
|
||||
'fold': self.fold,
|
||||
'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
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@
|
|||
<field name="project_id" invisible="1"/>
|
||||
<field name="stage_id"/>
|
||||
<field name="name"/>
|
||||
<field name="team_id"/>
|
||||
<field name="team_id" required="approval_by == 'assigned_team_lead'"/>
|
||||
<field name="approval_by"/>
|
||||
<field name="involved_user_ids" widget="many2many_tags"/>
|
||||
<field name="related_user_ids" invisible="1"/>
|
||||
<field name="fold"/>
|
||||
</group>
|
||||
<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")
|
||||
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))
|
||||
if not record.exists():
|
||||
return {"error": "Record not found"}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,19 @@
|
|||
<img t-att-src="props.url"
|
||||
style="max-width:100%; max-height:80vh;" />
|
||||
</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="
|
||||
props.mimetype.startsWith('audio') or
|
||||
props.mimetype.endsWith('mp3') or
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ patch(BinaryField.prototype, {
|
|||
},
|
||||
|
||||
async onPreviewFile(ev) {
|
||||
debugger;
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue