PMT, UNIVERSAL ATTACHMENT PREVIEW, MENU CONTROL CENTER INTEGRATION

This commit is contained in:
Pranay 2025-12-10 10:09:23 +05:30
parent c93d208990
commit 6f77059f85
35 changed files with 2323 additions and 1239 deletions

View File

@ -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',
],

View File

@ -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:

View File

@ -1,3 +1,5 @@
from . import masters
from . import ir_http
from . import groups
from . import models
from . import menu

View File

@ -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)]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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>

View File

@ -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">

View File

@ -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,

View File

@ -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>

View File

@ -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

View File

@ -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"
)

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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."""

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 internal_teams_manager internal.teams.manager model_internal_teams project.group_project_user 1 1 1 0
4 internal_teams_user internal.teams.user model_internal_teams base.group_user 1 0 0 0
5 access_project_sprint_user access_project_role_user access.project.sprint.user project.role.user model_project_sprint model_project_role project.group_project_user base.group_user 1 1 0 1 0 1 0
6 access_project_role_manager project.role.manager model_project_role project.group_project_manager 1 1 1 1
7 access_project_sprint_user access.project.sprint.user model_project_sprint project.group_project_user 1 1 1 1
8 access_project_sprint_manager access.project.sprint.manager model_project_sprint project.group_project_manager 1 1 1 1
9 access_project_sprint_manager access_project_architecture_design_user access.project.sprint.manager access_project_architecture_design_user model_project_sprint model_project_architecture_design project.group_project_manager project.group_project_user 1 1 1 1
10 access_project_architecture_design_user access_project_architecture_design_manager access_project_architecture_design_user access_project_architecture_design_manager model_project_architecture_design project.group_project_user project.group_project_manager 1 1 1 1
11 access_project_architecture_design_manager access_project_risk_user access_project_architecture_design_manager access_project_risk_user model_project_architecture_design model_project_risk project.group_project_manager project.group_project_user 1 1 1 1
33 access_project_project_supervisor project.project project.model_project_project project_task_timesheet_extended.group_project_supervisor 1 1 1 0
34 access_project_project_stage_supervisor project.project_stage.supervisor project.model_project_project_stage project_task_timesheet_extended.group_project_supervisor 1 1 1 0
35 access_project_task_type_supervisor project.task.type supervisor project.model_project_task_type project_task_timesheet_extended.group_project_supervisor 1 1 1 1
36 access_project_tags_supervisor project.project_tags_supervisor project.model_project_tags project_task_timesheet_extended.group_project_supervisor 1 1 1 1
37 access_project_task_time_lines_user access_project_task_time_lines_user model_project_task_time_lines base.group_user 1 1 1 1
38 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
39 access_user_task_availability user.task.availability.access model_user_task_availability base.group_user 1 0 0 0
40 access_project_task_time_lines_user access_project_deployment_log_user access_project_task_time_lines_user access.project.deployment.log.user model_project_task_time_lines model_project_deployment_log base.group_user 1 1 1 1
41
42
57
58
59
60
61

View File

@ -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>

View File

@ -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;
}

View File

@ -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>

View File

@ -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"/>
@ -31,7 +31,7 @@
string="Create Project Channel"
type="object"
class="btn-primary"
invisible = "discuss_channel_id"/>
invisible="discuss_channel_id"/>
</setting>
<field name="default_projects_channel_id" invisible="1"/>
</div>
@ -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 &amp; 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 &amp; 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 &lt; 0"
decoration-warning="allocated_hours > 0 and (remaining_hours / allocated_hours) &lt; 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 &amp; 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>

View File

@ -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>

View File

@ -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 &amp; 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 &amp; 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 &lt; 0"
decoration-warning="allocated_hours > 0 and (remaining_hours / allocated_hours) &lt; 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 &amp; 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>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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'}

View File

@ -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>

View File

@ -58,16 +58,19 @@ class AttachmentPreview(http.Controller):
@http.route("/lookup_or_create/attachment", type="json", auth="user")
def lookup_or_create_attachment(self, model, res_id, field):
record = request.env[model].sudo().browse(int(res_id))
if not record.exists():
return {"error": "Record not found"}
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"}
# Check existing attachment
attach = request.env["ir.attachment"].sudo().search([
("res_model", "=", model),
("res_id", "=", int(res_id)),
("res_field", "=", field)
], limit=1)
# Check existing attachment
attach = request.env["ir.attachment"].sudo().search([
("res_model", "=", model),
("res_id", "=", int(res_id)),
("res_field", "=", field)
], limit=1)
# Create attachment if missing
if not attach:

View File

@ -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

View File

@ -14,6 +14,7 @@ patch(BinaryField.prototype, {
},
async onPreviewFile(ev) {
debugger;
ev.stopPropagation();
ev.preventDefault();