Project timesheet updates
This commit is contained in:
parent
44e5ee7e2f
commit
20d22c1f04
|
|
@ -0,0 +1,450 @@
|
||||||
|
@import url('colors.css');
|
||||||
|
|
||||||
|
/* ===== ATS Custom Layout Reset ===== */
|
||||||
|
/* ===== Theme Toggle Styles ===== */
|
||||||
|
.theme-toggle-container {
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: auto; /* Push to bottom */
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background-color: var(--primary-light);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-icon {
|
||||||
|
display: none;
|
||||||
|
color: #fbbf24; /* Amber color for sun */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-icon {
|
||||||
|
color: #cbd5e1; /* Light gray for moon */
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .light-icon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .dark-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-text {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsed state styles */
|
||||||
|
.sidebar.collapsed .theme-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed .theme-toggle {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed .theme-toggle i {
|
||||||
|
margin-right: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.ats-app {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: "Segoe UI", sans-serif;
|
||||||
|
background-color: var(--body-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Main Layout ===== */
|
||||||
|
.ats-app .layout-container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Header ===== */
|
||||||
|
.ats-app .main-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1rem;
|
||||||
|
position: relative; /* For absolute positioning of toggle button */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo and title alignment */
|
||||||
|
.ats-app .main-header img {
|
||||||
|
height: 40px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .main-header span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 40px; /* Match logo height */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Layout Container ===== */
|
||||||
|
.ats-app .layout-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100%; /* Adjust for header height */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Sidebar ===== */
|
||||||
|
.ats-app .sidebar {
|
||||||
|
background-color: var(--sidebar-bg);
|
||||||
|
color: var(--sidebar-text);
|
||||||
|
padding: 1rem 1rem 2rem;
|
||||||
|
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: all 0.4s ease-in-out;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar States */
|
||||||
|
.ats-app .sidebar.expanded {
|
||||||
|
width: 240px;
|
||||||
|
min-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .sidebar.collapsed {
|
||||||
|
width: 60px;
|
||||||
|
min-width: 60px;
|
||||||
|
padding: 1rem 0.4rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .sidebar.collapsed .menu-item-text,
|
||||||
|
.ats-app .sidebar.collapsed .list-title,
|
||||||
|
.ats-app .sidebar.collapsed .main-header span {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Sidebar Menu ===== */
|
||||||
|
.ats-app .menu-list {
|
||||||
|
list-style: none;
|
||||||
|
padding-top: 1rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .list-title {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05rem;
|
||||||
|
margin: 1.5rem 1.5rem 0.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .menu-list a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.7rem 1rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .menu-list a i {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
min-width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .menu-list a:hover {
|
||||||
|
background-color: #DBEDFE;
|
||||||
|
color: #0061FF;
|
||||||
|
transform: translateX(6px);
|
||||||
|
box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .menu-list a.active {
|
||||||
|
background-color: #DBEDFE;
|
||||||
|
color: #0061FF;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .menu-list a.active::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 4px;
|
||||||
|
background-color: #DBEDFE;
|
||||||
|
border-radius: var(--radius-sm) 0 0 var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toggle Button (Improved) */
|
||||||
|
.ats-app .toggle-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 1.5rem;
|
||||||
|
right: -12px;
|
||||||
|
background-color: var(--surface-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
z-index: 20;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .toggle-btn:hover {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .sidebar.collapsed .toggle-btn {
|
||||||
|
right: -12px;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .sidebar.collapsed .toggle-btn:hover {
|
||||||
|
transform: rotate(180deg) translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Main Content Area ===== */
|
||||||
|
.ats-app .content-area {
|
||||||
|
flex: 1;
|
||||||
|
padding: 2rem;
|
||||||
|
background-color: var(--content-bg);
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.02);
|
||||||
|
border-left: 1px solid #dcdde1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Responsive Design ===== */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.ats-app .layout-container {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .sidebar {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .sidebar.expanded {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .sidebar.collapsed {
|
||||||
|
height: 60px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .menu-list {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .menu-list a {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.75rem;
|
||||||
|
gap: 0.25rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .menu-list a i {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .menu-list a.active::before {
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
top: auto;
|
||||||
|
bottom: 0;
|
||||||
|
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .list-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .toggle-btn {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
right: 1.5rem;
|
||||||
|
top: auto;
|
||||||
|
left: auto;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .content-area {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation for smoother transitions */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(5px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.ats-app .content-area > * {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Menu list styles */
|
||||||
|
.menu-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-list li {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-menus {
|
||||||
|
padding-left: 1 rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 12rem;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:hover {
|
||||||
|
background-color: var(--primary-light);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item.active {
|
||||||
|
background-color: var(--primary-light);
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
min-width: 24px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-text {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsed state styles */
|
||||||
|
.sidebar.collapsed .menu-item {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed .menu-icon {
|
||||||
|
margin-right: 0;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed .menu-item-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active state indicator for collapsed */
|
||||||
|
.sidebar.collapsed .menu-item.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 4px;
|
||||||
|
height: 24px;
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar.collapsed .menu-list {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed .menu-item {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 8px 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed .menu-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.collapsed .menu-item.active::after {
|
||||||
|
left: 50%;
|
||||||
|
top: auto;
|
||||||
|
bottom: 0;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 24px;
|
||||||
|
height: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
|
||||||
|
/* Background Colors */
|
||||||
|
--body-bg: #f5f6fa;
|
||||||
|
--sidebar-bg: #FAFCFF;
|
||||||
|
--create-model-bg: #FAFCFF;
|
||||||
|
--content-bg: #E3E9EF;
|
||||||
|
--active-search-bg: #ffffff;
|
||||||
|
--active-search-hover-bg: #f0f0f0;
|
||||||
|
--add-btn-bg: #3498db;
|
||||||
|
--add-btn-hover-bg: #2980b9;
|
||||||
|
--side-panel-bg: #FAFCFF;
|
||||||
|
--side-panel-item-hover: #f0f8ff;
|
||||||
|
--side-panel-item-selected: #d7eaff;
|
||||||
|
|
||||||
|
/* Text Colors */
|
||||||
|
--text-primary: #2f3542;
|
||||||
|
--text-secondary: #4B5865;
|
||||||
|
--text-muted: #6c757d;
|
||||||
|
--sidebar-text: #0F1419;
|
||||||
|
--active-search-text: #333;
|
||||||
|
--add-btn-color: #ffffff;
|
||||||
|
|
||||||
|
/* Border Colors */
|
||||||
|
--border-color: #dcdde1;
|
||||||
|
--border-light: #e0e0e0;
|
||||||
|
|
||||||
|
/* Shadow Colors */
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-dark: rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
/* Status Colors */
|
||||||
|
--status-new: #3498db;
|
||||||
|
--status-interview: #f39c12;
|
||||||
|
--status-hired: #2ecc71;
|
||||||
|
--status-rejected: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
/* Primary Colors - Professional Blue Tones */
|
||||||
|
--primary-blue: #4A90E2;
|
||||||
|
--primary-blue-dark: #357ABD;
|
||||||
|
--primary-blue-light: #5BA0F2;
|
||||||
|
|
||||||
|
/* Secondary Colors - Subtle & Professional */
|
||||||
|
--secondary-purple: #8E44AD;
|
||||||
|
--secondary-green: #27AE60;
|
||||||
|
--secondary-red: #C0392B;
|
||||||
|
--secondary-yellow: #F39C12;
|
||||||
|
|
||||||
|
/* Grayscale - Sophisticated Dark Grays */
|
||||||
|
--white: #1E1E1E;
|
||||||
|
--gray-100: #2A2A2A;
|
||||||
|
--gray-200: #333333;
|
||||||
|
--gray-300: #3D3D3D;
|
||||||
|
--gray-400: #474747;
|
||||||
|
--gray-500: #525252;
|
||||||
|
--gray-600: #959595;
|
||||||
|
--gray-700: #B0B0B0;
|
||||||
|
--gray-800: #CACACA;
|
||||||
|
--gray-900: #E5E5E5;
|
||||||
|
--black: #F5F5F5;
|
||||||
|
|
||||||
|
/* Semantic Colors - Balanced Visibility */
|
||||||
|
--success: #27AE60;
|
||||||
|
--info: #3498DB;
|
||||||
|
--warning: #F39C12;
|
||||||
|
--danger: #E74C3C;
|
||||||
|
|
||||||
|
/* Background Colors - Layered Depth */
|
||||||
|
--body-bg: #1A1A1A;
|
||||||
|
--sidebar-bg: #232323;
|
||||||
|
--create-model-bg: #232323;
|
||||||
|
--content-bg: #2A2A2A;
|
||||||
|
--active-search-bg: #2A2A2A;
|
||||||
|
--active-search-hover-bg: #333333;
|
||||||
|
--add-btn-bg: #4A90E2;
|
||||||
|
--add-btn-hover-bg: #357ABD;
|
||||||
|
--side-panel-bg: #232323;
|
||||||
|
--side-panel-item-hover: #2A2A2A;
|
||||||
|
--side-panel-item-selected: #2D4158;
|
||||||
|
|
||||||
|
/* Text Colors - High Contrast */
|
||||||
|
--text-primary: #E5E5E5;
|
||||||
|
--text-secondary: #B0B0B0;
|
||||||
|
--text-muted: #959595;
|
||||||
|
--sidebar-text: #E0E0E0;
|
||||||
|
--active-search-text: #F5F5F5;
|
||||||
|
--add-btn-color: #FFFFFF;
|
||||||
|
|
||||||
|
/* Border Colors - Subtle Definition */
|
||||||
|
--border-color: #3D3D3D;
|
||||||
|
--border-light: #333333;
|
||||||
|
|
||||||
|
/* Shadow Colors - Soft Depth */
|
||||||
|
--shadow-color: rgba(0, 0, 0, 0.25);
|
||||||
|
--shadow-dark: rgba(0, 0, 0, 0.4);
|
||||||
|
|
||||||
|
/* Status Colors - Professional Hierarchy */
|
||||||
|
--status-new: #4A90E2;
|
||||||
|
--status-interview: #F39C12;
|
||||||
|
--status-hired: #27AE60;
|
||||||
|
--status-rejected: #E74C3C;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,630 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
function initCandidatesPage() {
|
||||||
|
console.log("candidates Page Loaded");
|
||||||
|
const candidateDetailArea = document.getElementById("candidates-detail");
|
||||||
|
const container = document.querySelector('.ats-list-container'); // Added this line
|
||||||
|
const toggleBtn = document.getElementById("candidates-list-sidebar-toggle-btn"); // Added this line
|
||||||
|
const sidebar = document.getElementById("candidates-list-panel"); // Added this line
|
||||||
|
|
||||||
|
document.querySelectorAll(".ats-item.candidates-item").forEach(item => {
|
||||||
|
item.addEventListener("click", function() {
|
||||||
|
document.querySelectorAll(".candidates-item.selected").forEach(el => el.classList.remove("selected"));
|
||||||
|
this.classList.add("selected");
|
||||||
|
|
||||||
|
// Show the detail panel and add necessary classes
|
||||||
|
candidateDetailArea.style.display = 'block'; // Added this line
|
||||||
|
container.classList.add('ats-selected'); // Added this line
|
||||||
|
sidebar.classList.remove('collapsed'); // Added this line
|
||||||
|
toggleBtn.style.display = 'flex'; // Added this line
|
||||||
|
|
||||||
|
const candidateId = this.dataset.id;
|
||||||
|
console.log("Candidate ID:", candidateId); // Added for debugging
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
if (candidateDetailArea) {
|
||||||
|
candidateDetailArea.innerHTML = '<div class="text-center p-5"><i class="fa fa-spinner fa-spin fa-2x"></i><p class="mt-2">Loading candidate details...</p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/myATS/candidate/detail/${candidateId}`, {
|
||||||
|
headers: { "X-Requested-With": "XMLHttpRequest" }
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error('Network response was not ok');
|
||||||
|
return res.text();
|
||||||
|
})
|
||||||
|
.then(html => {
|
||||||
|
console.log("Response received"); // Added for debugging
|
||||||
|
if (candidateDetailArea) {
|
||||||
|
candidateDetailArea.innerHTML = html;
|
||||||
|
initCandidateDetailEdit();
|
||||||
|
|
||||||
|
// Add close button functionality
|
||||||
|
const closeBtn = candidateDetailArea.querySelector('.close-detail');
|
||||||
|
if (closeBtn) {
|
||||||
|
closeBtn.addEventListener('click', function() {
|
||||||
|
candidateDetailArea.style.display = 'none';
|
||||||
|
container.classList.remove('ats-selected');
|
||||||
|
document.querySelectorAll(".ats-item.candidates-item.selected").forEach(el => el.classList.remove("selected"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading candidate details:', error);
|
||||||
|
if (candidateDetailArea) {
|
||||||
|
candidateDetailArea.innerHTML = '<div class="alert alert-danger">Error loading candidate details. Please try again.</div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search functionality
|
||||||
|
const search = document.getElementById("candidates-search");
|
||||||
|
if (search) {
|
||||||
|
search.addEventListener("input", function() {
|
||||||
|
const query = this.value.toLowerCase();
|
||||||
|
let visibleCount = 0;
|
||||||
|
document.querySelectorAll(".ats-item.candidates-item").forEach(item => {
|
||||||
|
const match = item.textContent.toLowerCase().includes(query);
|
||||||
|
item.style.display = match ? "" : "none";
|
||||||
|
if (match) visibleCount++;
|
||||||
|
});
|
||||||
|
const countElement = document.getElementById("active-records-count");
|
||||||
|
if (countElement) {
|
||||||
|
countElement.textContent = visibleCount;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sidebar Toggle
|
||||||
|
if (toggleBtn && sidebar) { // Added this check
|
||||||
|
toggleBtn.addEventListener("click", function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
sidebar.classList.toggle("collapsed");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rest of your code remains the same...
|
||||||
|
const createCandidate = document.getElementById('add-candidate-create-btn');
|
||||||
|
const candidateModal = document.getElementById('candidate-form-modal');
|
||||||
|
const form = document.getElementById('candidate-form');
|
||||||
|
const closeModal = document.querySelectorAll('.candidate-form-close, .btn-cancel');
|
||||||
|
const avatarUpload = document.getElementById('avatar-upload');
|
||||||
|
const candidateImage = document.getElementById('candidate-image');
|
||||||
|
const avatarUploadIcon = document.querySelector('.avatar-upload i');
|
||||||
|
|
||||||
|
if (avatarUpload && candidateImage) {
|
||||||
|
// Make the entire avatar clickable
|
||||||
|
candidateImage.parentElement.style.cursor = 'pointer';
|
||||||
|
// Handle click on avatar
|
||||||
|
candidateImage.parentElement.addEventListener('click', function(e) {
|
||||||
|
if (e.target !== avatarUpload && e.target !== avatarUploadIcon) {
|
||||||
|
avatarUpload.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Handle file selection
|
||||||
|
avatarUpload.addEventListener('change', function(e) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file && file.type.match('image.*')) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
candidateImage.src = e.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume Upload Elements
|
||||||
|
const resumeUpload = document.getElementById('resume-upload');
|
||||||
|
const resumeDropzone = document.getElementById('resume-dropzone');
|
||||||
|
const resumePreview = document.getElementById('resume-preview');
|
||||||
|
const resumePlaceholder = document.querySelector('.resume-preview-placeholder');
|
||||||
|
const resumeIframe = document.getElementById('resume-iframe');
|
||||||
|
const resumeImage = document.getElementById('resume-image');
|
||||||
|
const unsupportedFormat = document.getElementById('unsupported-format');
|
||||||
|
const downloadResume = document.getElementById('download-resume');
|
||||||
|
const uploadResumeBtn = document.getElementById('upload-applicant-resume');
|
||||||
|
let currentResumeFile = null;
|
||||||
|
|
||||||
|
const saveBtn = document.getElementById('save-candidate');
|
||||||
|
if (saveBtn) {
|
||||||
|
saveBtn.addEventListener('click', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
createNewCandidate(form, candidateModal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createCandidate) {
|
||||||
|
createCandidate.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
candidateModal.style.display = 'flex';
|
||||||
|
setTimeout(() => {
|
||||||
|
candidateModal.classList.add('show');
|
||||||
|
}, 10);
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
setTimeout(() => {
|
||||||
|
initSelect2();
|
||||||
|
initResumeUploadHandlers();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeModal) {
|
||||||
|
closeModal.forEach(btn => {
|
||||||
|
btn.addEventListener('click', function() {
|
||||||
|
candidateModal.classList.remove('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
candidateModal.style.display = 'none';
|
||||||
|
}, 300);
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// candidateModal.addEventListener('click', function(e) {
|
||||||
|
// if (e.target === candidateModal) {
|
||||||
|
// candidateModal.classList.remove('show');
|
||||||
|
// setTimeout(() => {
|
||||||
|
// candidateModal.style.display = 'none';
|
||||||
|
// }, 300);
|
||||||
|
// document.body.style.overflow = '';
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
function formatUserOption(user) {
|
||||||
|
if (!user.id) return user.text;
|
||||||
|
var imageUrl = $(user.element).data('image') || '/web/static/img/placeholder.png';
|
||||||
|
var $container = $(
|
||||||
|
'<span style="display: flex; align-items: center;">' +
|
||||||
|
'<img class="user-avatar" src="' + imageUrl + '" style="width:24px;height:24px;border-radius:50%;margin-right:8px;object-fit:cover;" onerror="this.src=\'/web/static/img/placeholder.png\'" />' +
|
||||||
|
'<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">' + user.text + '</span>' +
|
||||||
|
'</span>'
|
||||||
|
);
|
||||||
|
return $container;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUserSelection(user) {
|
||||||
|
if (!user.id) return user.text;
|
||||||
|
var imageUrl = $(user.element).data('image') || '/web/static/img/placeholder.png';
|
||||||
|
var $container = $(
|
||||||
|
'<span style="display: flex; align-items: center; width: 100%;">' +
|
||||||
|
'<img class="user-avatar" src="' + imageUrl + '" style="width:24px;height:24px;border-radius:50%;margin-right:8px;object-fit:cover;" onerror="this.src=\'/web/static/img/placeholder.png\'" />' +
|
||||||
|
'<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-grow: 1;">' + user.text + '</span>' +
|
||||||
|
'</span>'
|
||||||
|
);
|
||||||
|
return $container;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSelect2() {
|
||||||
|
const candidateSkills = document.getElementById('candidate-skills');
|
||||||
|
if (candidateSkills) {
|
||||||
|
$(candidateSkills).select2({
|
||||||
|
placeholder: 'Select skills',
|
||||||
|
allowClear: true,
|
||||||
|
dropdownParent: $('.candidate-form-modal'),
|
||||||
|
width: '100%',
|
||||||
|
escapeMarkup: function(m) { return m; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const managerSelect = document.getElementById('manager');
|
||||||
|
if (managerSelect) {
|
||||||
|
$(managerSelect).select2({
|
||||||
|
placeholder: 'Select Manager',
|
||||||
|
allowClear: true,
|
||||||
|
templateResult: formatUserOption,
|
||||||
|
templateSelection: formatUserSelection,
|
||||||
|
escapeMarkup: function(m) { return m; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initResumeUploadHandlers() {
|
||||||
|
// Create remove button
|
||||||
|
const removeResumeBtn = document.createElement('button');
|
||||||
|
removeResumeBtn.innerHTML = '<i class="fas fa-trash"></i> Remove Resume';
|
||||||
|
removeResumeBtn.className = 'btn btn-danger btn-sm mt-2';
|
||||||
|
removeResumeBtn.style.display = 'none';
|
||||||
|
resumePreview.appendChild(removeResumeBtn);
|
||||||
|
|
||||||
|
// Handle remove resume
|
||||||
|
removeResumeBtn.addEventListener('click', function() {
|
||||||
|
resetResumePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetResumePreview() {
|
||||||
|
// Clear file input
|
||||||
|
resumeUpload.value = '';
|
||||||
|
currentResumeFile = null;
|
||||||
|
// Reset preview
|
||||||
|
resumePlaceholder.style.display = 'flex';
|
||||||
|
resumeIframe.style.display = 'none';
|
||||||
|
resumeImage.style.display = 'none';
|
||||||
|
unsupportedFormat.style.display = 'none';
|
||||||
|
removeResumeBtn.style.display = 'none';
|
||||||
|
// Reset iframe/src to prevent memory leaks
|
||||||
|
if (resumeIframe.src) {
|
||||||
|
URL.revokeObjectURL(resumeIframe.src);
|
||||||
|
resumeIframe.src = '';
|
||||||
|
}
|
||||||
|
if (resumeImage.src) {
|
||||||
|
URL.revokeObjectURL(resumeImage.src);
|
||||||
|
resumeImage.src = '';
|
||||||
|
}
|
||||||
|
if (downloadResume.href) {
|
||||||
|
URL.revokeObjectURL(downloadResume.href);
|
||||||
|
downloadResume.href = '#';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unified upload handler for both preview and parsing
|
||||||
|
uploadResumeBtn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = '.pdf,.doc,.docx,.txt,.jpg,.jpeg,.png';
|
||||||
|
fileInput.onchange = async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// First handle the preview
|
||||||
|
handleResumeFile(file);
|
||||||
|
|
||||||
|
// Then try to parse the resume if it's a parseable type
|
||||||
|
if (file.type.match(/pdf|msword|openxmlformats|text/)) {
|
||||||
|
// Show loading state
|
||||||
|
const button = uploadResumeBtn;
|
||||||
|
const originalText = button.innerHTML;
|
||||||
|
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing Resume...';
|
||||||
|
button.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('type', 'candidate');
|
||||||
|
|
||||||
|
const response = await fetch('/resume/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
credentials: 'same-origin'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
populateCandidateForm(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing Resume:', error);
|
||||||
|
showNotification('Failed to parse Resume. Please try again or enter manually.', 'danger');
|
||||||
|
} finally {
|
||||||
|
button.innerHTML = '<i class="fas fa-upload"></i> Upload Resume';
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fileInput.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle click on dropzone
|
||||||
|
resumeDropzone.addEventListener('click', function(e) {
|
||||||
|
if (e.target === this || e.target.classList.contains('upload-icon') ||
|
||||||
|
e.target.tagName === 'H5' || e.target.tagName === 'P') {
|
||||||
|
resumeUpload.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle drag and drop
|
||||||
|
resumeDropzone.addEventListener('dragover', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.classList.add('dragover');
|
||||||
|
this.style.borderColor = '#3498db';
|
||||||
|
this.style.backgroundColor = 'rgba(52, 152, 219, 0.1)';
|
||||||
|
});
|
||||||
|
|
||||||
|
resumeDropzone.addEventListener('dragleave', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.classList.remove('dragover');
|
||||||
|
this.style.borderColor = '';
|
||||||
|
this.style.backgroundColor = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
resumeDropzone.addEventListener('drop', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.classList.remove('dragover');
|
||||||
|
this.style.borderColor = '';
|
||||||
|
this.style.backgroundColor = '';
|
||||||
|
|
||||||
|
if (e.dataTransfer.files.length) {
|
||||||
|
const file = e.dataTransfer.files[0];
|
||||||
|
handleResumeFile(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle file selection from the regular input
|
||||||
|
resumeUpload.addEventListener('change', function(e) {
|
||||||
|
if (this.files.length) {
|
||||||
|
handleResumeFile(this.files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleResumeFile(file) {
|
||||||
|
const validTypes = [
|
||||||
|
'application/pdf',
|
||||||
|
'application/msword',
|
||||||
|
'application/wps-office.docx',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'text/plain'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!validTypes.includes(file.type)) {
|
||||||
|
alert('Please upload a valid file type (PDF, Word, Image, or Text)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentResumeFile = file;
|
||||||
|
|
||||||
|
// Hide placeholder
|
||||||
|
resumePlaceholder.style.display = 'none';
|
||||||
|
|
||||||
|
// Set up download link
|
||||||
|
const fileURL = URL.createObjectURL(file);
|
||||||
|
downloadResume.href = fileURL;
|
||||||
|
downloadResume.download = file.name;
|
||||||
|
removeResumeBtn.style.display = 'block';
|
||||||
|
|
||||||
|
// Check file type and show appropriate preview
|
||||||
|
if (file.type === 'application/pdf') {
|
||||||
|
// PDF preview
|
||||||
|
resumeIframe.src = fileURL;
|
||||||
|
resumeIframe.style.display = 'block';
|
||||||
|
resumeImage.style.display = 'none';
|
||||||
|
unsupportedFormat.style.display = 'none';
|
||||||
|
} else if (file.type.match('image.*')) {
|
||||||
|
// Image preview
|
||||||
|
resumeImage.src = fileURL;
|
||||||
|
resumeImage.style.display = 'block';
|
||||||
|
resumeIframe.style.display = 'none';
|
||||||
|
unsupportedFormat.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
// Unsupported format for preview
|
||||||
|
unsupportedFormat.style.display = 'flex';
|
||||||
|
resumeIframe.style.display = 'none';
|
||||||
|
resumeImage.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the actual resume-upload input
|
||||||
|
const dataTransfer = new DataTransfer();
|
||||||
|
dataTransfer.items.add(file);
|
||||||
|
resumeUpload.files = dataTransfer.files;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateCandidateForm(resumeData) {
|
||||||
|
// Add CSS for visual feedback
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
.populated-field {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.select2-populated .select2-selection--multiple {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
}
|
||||||
|
.select2-populated .select2-selection__choice {
|
||||||
|
background-color: #e8f5e9 !important;
|
||||||
|
border-color: #c8e6c9 !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// Helper function to set value with visual feedback
|
||||||
|
function setValueWithFeedback(elementId, value) {
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (element && value) {
|
||||||
|
element.value = value;
|
||||||
|
element.classList.add('populated-field');
|
||||||
|
|
||||||
|
// Special handling for Select2 if this element uses it
|
||||||
|
if ($(element).hasClass('select2-hidden-accessible')) {
|
||||||
|
$(element).next('.select2-container')
|
||||||
|
.find('.select2-selection')
|
||||||
|
.addClass('populated-field');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section 1: Basic Information
|
||||||
|
if (resumeData.personal_info) {
|
||||||
|
const personal = resumeData.personal_info;
|
||||||
|
|
||||||
|
// Set values with visual feedback
|
||||||
|
setValueWithFeedback('partner-name', personal.name);
|
||||||
|
setValueWithFeedback('email', personal.email);
|
||||||
|
setValueWithFeedback('phone', personal.phone);
|
||||||
|
setValueWithFeedback('linkedin', personal.linkedin);
|
||||||
|
|
||||||
|
// If any of these fields are Select2 elements, ensure they get styled
|
||||||
|
['partner-name', 'email', 'phone', 'linkedin'].forEach(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el && el.value && $(el).hasClass('select2-hidden-accessible')) {
|
||||||
|
$(el).next('.select2-container')
|
||||||
|
.find('.select2-selection')
|
||||||
|
.addClass('populated-field');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skills - Handle Select2 with background color change
|
||||||
|
if (resumeData.skills?.length) {
|
||||||
|
const skillValues = resumeData.skills.map(skill => skill.id);
|
||||||
|
$('#candidate-skills')
|
||||||
|
.val(skillValues)
|
||||||
|
.trigger('change')
|
||||||
|
.addClass('populated-field');
|
||||||
|
|
||||||
|
// Add class to Select2 container for styling
|
||||||
|
$('#candidate-skills').next('.select2-container')
|
||||||
|
.find('.select2-selection--multiple')
|
||||||
|
.addClass('select2-populated');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show notification
|
||||||
|
showNotification('Resume uploaded and fields populated successfully!', 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotification(message, type) {
|
||||||
|
// Check if notification container exists, create if not
|
||||||
|
let notificationContainer = document.getElementById('notification-container');
|
||||||
|
if (!notificationContainer) {
|
||||||
|
notificationContainer = document.createElement('div');
|
||||||
|
notificationContainer.id = 'notification-container';
|
||||||
|
notificationContainer.style.position = 'fixed';
|
||||||
|
notificationContainer.style.top = '20px';
|
||||||
|
notificationContainer.style.right = '20px';
|
||||||
|
notificationContainer.style.zIndex = '9999';
|
||||||
|
document.body.appendChild(notificationContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create notification element
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `alert alert-${type} alert-dismissible fade show`;
|
||||||
|
notification.innerHTML = `
|
||||||
|
${message}
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add to container
|
||||||
|
notificationContainer.appendChild(notification);
|
||||||
|
|
||||||
|
// Auto remove after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.classList.remove('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewCandidate(form, modal) {
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
form.reportValidity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// Basic Information
|
||||||
|
formData.append('sequence', document.getElementById('candidate-sequence').value);
|
||||||
|
formData.append('partner_name', document.getElementById('partner-name').value);
|
||||||
|
|
||||||
|
// Add image file if selected
|
||||||
|
const avatarUpload = document.getElementById('avatar-upload');
|
||||||
|
if (avatarUpload.files.length > 0) {
|
||||||
|
formData.append('image_1920', avatarUpload.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contact Information
|
||||||
|
formData.append('email', document.getElementById('email').value);
|
||||||
|
formData.append('phone', document.getElementById('phone').value);
|
||||||
|
formData.append('mobile', document.getElementById('alternate-phone').value);
|
||||||
|
formData.append('linkedin_profile', document.getElementById('linkedin').value);
|
||||||
|
formData.append('type_id', document.getElementById('type').value);
|
||||||
|
formData.append('user_id', document.getElementById('manager').value);
|
||||||
|
formData.append('availability', document.getElementById('availability').value);
|
||||||
|
|
||||||
|
// Skills
|
||||||
|
const skillOptions = document.getElementById('candidate-skills').selectedOptions;
|
||||||
|
for (let i = 0; i < skillOptions.length; i++) {
|
||||||
|
formData.append('skill_ids', skillOptions[i].value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add resume file if selected
|
||||||
|
const resumeUpload = document.getElementById('resume-upload');
|
||||||
|
if (resumeUpload.files.length > 0) {
|
||||||
|
formData.append('resume_file', resumeUpload.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional fields
|
||||||
|
formData.append('active', 'true');
|
||||||
|
formData.append('color', '0');
|
||||||
|
|
||||||
|
fetch('/myATS/candidate/create', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.error || response.statusText);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
modal.classList.remove('show');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
form.reset();
|
||||||
|
alert('Candidate created successfully!');
|
||||||
|
|
||||||
|
// Refresh the candidates list
|
||||||
|
fetch('/myATS/page/candidates', {
|
||||||
|
headers: { "X-Requested-With": "XMLHttpRequest" }
|
||||||
|
})
|
||||||
|
.then(res => res.text())
|
||||||
|
.then(html => {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
const newCandidate = doc.querySelector('.candidates-list-left');
|
||||||
|
const existingCandidatesList = document.querySelector('.candidates-list-left');
|
||||||
|
if (newCandidate && existingCandidatesList) {
|
||||||
|
existingCandidatesList.innerHTML = newCandidate.innerHTML;
|
||||||
|
initCandidatesPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || 'Failed to save Candidate');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert("Error saving changes: " + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initCandidateDetailEdit() {
|
||||||
|
|
||||||
|
// Improved smooth scroll navigation
|
||||||
|
document.querySelectorAll('.nav-link').forEach(link => {
|
||||||
|
link.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const targetId = this.getAttribute('href');
|
||||||
|
const targetElement = document.querySelector(targetId);
|
||||||
|
if (targetElement) {
|
||||||
|
scrollToTarget(targetElement);
|
||||||
|
// Highlight effect
|
||||||
|
targetElement.style.boxShadow = '0 0 0 3px rgba(13, 110, 253, 0.5)';
|
||||||
|
targetElement.style.transition = 'box-shadow 0.3s ease';
|
||||||
|
setTimeout(() => {
|
||||||
|
targetElement.style.boxShadow = 'none';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the page when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', initCandidatesPage);
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -25,6 +25,8 @@ Key Features:
|
||||||
'base',
|
'base',
|
||||||
'analytic',
|
'analytic',
|
||||||
'project_gantt',
|
'project_gantt',
|
||||||
|
'hr',
|
||||||
|
'hr_contract',
|
||||||
],
|
],
|
||||||
'data': [
|
'data': [
|
||||||
'security/security.xml',
|
'security/security.xml',
|
||||||
|
|
@ -35,6 +37,7 @@ Key Features:
|
||||||
'wizards/project_stage_update_wizard.xml',
|
'wizards/project_stage_update_wizard.xml',
|
||||||
'wizards/task_reject_reason_wizard.xml',
|
'wizards/task_reject_reason_wizard.xml',
|
||||||
'view/teams.xml',
|
'view/teams.xml',
|
||||||
|
'view/project_stages.xml',
|
||||||
'view/task_stages.xml',
|
'view/task_stages.xml',
|
||||||
'view/project.xml',
|
'view/project.xml',
|
||||||
'view/project_task.xml',
|
'view/project_task.xml',
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,43 @@
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="action_show_project_chatter" model="ir.actions.server">
|
||||||
|
<field name="name">Show/Hide Chatter</field>
|
||||||
|
<field name="model_id" ref="project.model_project_project"/>
|
||||||
|
<field name="binding_model_id" ref="project.model_project_project"/>
|
||||||
|
<field name="binding_type">action</field>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">
|
||||||
|
if records:
|
||||||
|
action = records.action_show_project_chatter()
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_show_project_task_chatter" model="ir.actions.server">
|
||||||
|
<field name="name">Show/Hide Chatter</field>
|
||||||
|
<field name="model_id" ref="project.model_project_task"/>
|
||||||
|
<field name="binding_model_id" ref="project.model_project_task"/>
|
||||||
|
<field name="binding_type">action</field>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">
|
||||||
|
if records:
|
||||||
|
action = records.action_show_project_task_chatter()
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_assign_approval_flow" model="ir.actions.server">
|
||||||
|
<field name="name">Enable/Disable Stage Approvals</field>
|
||||||
|
<field name="model_id" ref="project.model_project_project"/>
|
||||||
|
<field name="binding_model_id" ref="project.model_project_project"/>
|
||||||
|
<field name="binding_type">action</field>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">
|
||||||
|
if records:
|
||||||
|
action = records.action_assign_approval_flow()
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
<!-- <record id="action_reward_user" model="ir.actions.server">-->
|
<!-- <record id="action_reward_user" model="ir.actions.server">-->
|
||||||
<!-- <field name="name">Reward User</field>-->
|
<!-- <field name="name">Reward User</field>-->
|
||||||
<!-- <field name="model_id" ref="project.model_project_task"/>-->
|
<!-- <field name="model_id" ref="project.model_project_task"/>-->
|
||||||
|
|
@ -83,4 +120,103 @@
|
||||||
<!-- <field name="user_id" eval="False"/>-->
|
<!-- <field name="user_id" eval="False"/>-->
|
||||||
<!-- </record>-->
|
<!-- </record>-->
|
||||||
</data>
|
</data>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="project_project_stage_initiate" model="project.project.stage">
|
||||||
|
<field name="name">Initial</field>
|
||||||
|
<field name="approval_by">project_sponsor</field>
|
||||||
|
|
||||||
|
<field name="sequence">1</field>
|
||||||
|
<field name="fold" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_project_stage_planning" model="project.project.stage">
|
||||||
|
<field name="name">Planning</field>
|
||||||
|
<field name="approval_by">project_manager</field>
|
||||||
|
|
||||||
|
<field name="sequence">2</field>
|
||||||
|
<field name="fold" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_project_stage_architecture" model="project.project.stage">
|
||||||
|
<field name="name">Architecture & Design</field>
|
||||||
|
<field name="approval_by">project_manager</field>
|
||||||
|
|
||||||
|
<field name="sequence">3</field>
|
||||||
|
<field name="fold" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_project_stage_sprint_planning" model="project.project.stage">
|
||||||
|
<field name="name">Sprint Planning</field>
|
||||||
|
<field name="approval_by">project_manager</field>
|
||||||
|
|
||||||
|
<field name="sequence">4</field>
|
||||||
|
<field name="fold" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_project_stage_development" model="project.project.stage">
|
||||||
|
<field name="name">Development</field>
|
||||||
|
<field name="approval_by">project_manager</field>
|
||||||
|
|
||||||
|
<field name="sequence">5</field>
|
||||||
|
<field name="fold" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_project_stage_testing" model="project.project.stage">
|
||||||
|
<field name="name">Testing & QA</field>
|
||||||
|
<field name="approval_by">project_manager</field>
|
||||||
|
|
||||||
|
<field name="sequence">6</field>
|
||||||
|
<field name="fold" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_project_stage_deployment" model="project.project.stage">
|
||||||
|
<field name="name">Deployment</field>
|
||||||
|
<field name="approval_by">project_manager</field>
|
||||||
|
|
||||||
|
<field name="sequence">7</field>
|
||||||
|
<field name="fold" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_project_stage_maintenance" model="project.project.stage">
|
||||||
|
<field name="name">Maintenance & Support</field>
|
||||||
|
<field name="approval_by">project_manager</field>
|
||||||
|
|
||||||
|
<field name="sequence">8</field>
|
||||||
|
<field name="fold" eval="False"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_project_stage_closer" model="project.project.stage">
|
||||||
|
<field name="name">Closer</field>
|
||||||
|
<field name="approval_by">project_sponsor</field>
|
||||||
|
<field name="sequence">9</field>
|
||||||
|
<field name="fold" eval="True"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project.project_project_stage_0" model="project.project.stage">
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="name">To Do</field>
|
||||||
|
<field name="active">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project.project_project_stage_1" model="project.project.stage">
|
||||||
|
<field name="sequence">15</field>
|
||||||
|
<field name="name">In Progress</field>
|
||||||
|
<field name="active">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project.project_project_stage_2" model="project.project.stage">
|
||||||
|
<field name="sequence">20</field>
|
||||||
|
<field name="name">Done</field>
|
||||||
|
<field name="fold" eval="True"/>
|
||||||
|
<field name="active">False</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project.project_project_stage_3" model="project.project.stage">
|
||||||
|
<field name="sequence">25</field>
|
||||||
|
<field name="name">Cancelled</field>
|
||||||
|
<field name="fold" eval="True"/>
|
||||||
|
<field name="approval_by">project_manager</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
@ -1,4 +1,12 @@
|
||||||
from . import teams
|
from . import teams
|
||||||
|
from . import project_sprint
|
||||||
|
from . import task_documents
|
||||||
|
from . import project_architecture_design
|
||||||
|
from . import project_risk
|
||||||
|
from . import project_resource_cost
|
||||||
|
from . import project_costings
|
||||||
|
from . import project_code_commit
|
||||||
|
from . import project_stages
|
||||||
from . import task_stages
|
from . import task_stages
|
||||||
from . import project
|
from . import project
|
||||||
from . import project_task
|
from . import project_task
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
from odoo import api, Command, fields, models
|
||||||
|
|
||||||
|
class ProjectArchitectureDesign(models.Model):
|
||||||
|
_name = "project.architecture.design"
|
||||||
|
_description = "Architecture & Design"
|
||||||
|
|
||||||
|
project_id = fields.Many2one("project.project", required=True)
|
||||||
|
|
||||||
|
# System Architecture
|
||||||
|
architecture_diagram = fields.Binary(string="Architecture Diagram")
|
||||||
|
tech_stack = fields.Text(string="Tech Stack")
|
||||||
|
architecture_notes = fields.Text(string="Architecture Notes")
|
||||||
|
|
||||||
|
# UI/UX
|
||||||
|
ui_wireframe = fields.Binary(string="UI Wireframe")
|
||||||
|
ux_flow = fields.Binary(string="UX Flow Diagram")
|
||||||
|
|
||||||
|
# Database
|
||||||
|
er_diagram = fields.Binary(string="ER Diagram")
|
||||||
|
database_notes = fields.Text(string="Database Notes")
|
||||||
|
|
||||||
|
# Review
|
||||||
|
design_status = fields.Selection([
|
||||||
|
('pending', 'Pending'),
|
||||||
|
('approved', 'Approved'),
|
||||||
|
('changes_required', 'Changes Required')
|
||||||
|
], default="pending", string="Design Status")
|
||||||
|
|
||||||
|
reviewer_comments = fields.Text(string="Reviewer Comments")
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
class ProjectCommitStep(models.Model):
|
||||||
|
_name = 'project.commit.step'
|
||||||
|
_description = 'Commit Steps for Task'
|
||||||
|
_order = 'create_date desc'
|
||||||
|
|
||||||
|
task_id = fields.Many2one('project.task', string="Task", ondelete='cascade')
|
||||||
|
|
||||||
|
project_id = fields.Many2one('project.project',related='task_id.project_id')
|
||||||
|
commit_message = fields.Char(string="Commit Message", required=True)
|
||||||
|
commit_code = fields.Html(string="Commit Code")
|
||||||
|
branch_name = fields.Char(string="Branch")
|
||||||
|
files_changed = fields.Text(string="Files Changed")
|
||||||
|
commit_date = fields.Datetime(string="Commit Date", default=fields.Datetime.now)
|
||||||
|
notes = fields.Text(string="Notes")
|
||||||
|
sprint_id = fields.Many2one('project.sprint',related='task_id.sprint_id')
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
# ---------------------------------------
|
||||||
|
# MATERIAL COST
|
||||||
|
# ---------------------------------------
|
||||||
|
class ProjectMaterialCost(models.Model):
|
||||||
|
_name = "project.material.cost"
|
||||||
|
_description = "Material Cost for Project"
|
||||||
|
_order = "id desc"
|
||||||
|
|
||||||
|
project_id = fields.Many2one("project.project", string="Project", required=True)
|
||||||
|
material_name = fields.Char(string="Material Name", required=True)
|
||||||
|
|
||||||
|
qty = fields.Float(string="Quantity", default=1)
|
||||||
|
unit_cost = fields.Float(string="Unit Cost")
|
||||||
|
total_cost = fields.Float(string="Total Cost", compute="_compute_total_cost", store=True)
|
||||||
|
|
||||||
|
remarks = fields.Text(string="Remarks")
|
||||||
|
|
||||||
|
@api.depends("qty", "unit_cost")
|
||||||
|
def _compute_total_cost(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.total_cost = rec.qty * rec.unit_cost if rec.qty and rec.unit_cost else 0
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------
|
||||||
|
# EQUIPMENT COST
|
||||||
|
# ---------------------------------------
|
||||||
|
class ProjectEquipmentCost(models.Model):
|
||||||
|
_name = "project.equipment.cost"
|
||||||
|
_description = "Equipment Cost for Project"
|
||||||
|
_order = "id desc"
|
||||||
|
|
||||||
|
project_id = fields.Many2one("project.project", string="Project", required=True)
|
||||||
|
equipment_name = fields.Char(string="Equipment Name", required=True)
|
||||||
|
|
||||||
|
duration_hours = fields.Float(string="Duration (Hours)", default=1)
|
||||||
|
hourly_rate = fields.Float(string="Hourly Rate")
|
||||||
|
total_cost = fields.Float(string="Total Cost", compute="_compute_total_cost", store=True)
|
||||||
|
|
||||||
|
remarks = fields.Text(string="Remarks")
|
||||||
|
|
||||||
|
@api.depends("duration_hours", "hourly_rate")
|
||||||
|
def _compute_total_cost(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.total_cost = rec.duration_hours * rec.hourly_rate if rec.duration_hours and rec.hourly_rate else 0
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
from odoo import models, fields, api
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
class ProjectResourceCost(models.Model):
|
||||||
|
_name = "project.resource.cost"
|
||||||
|
_description = "Resource Cost for Project"
|
||||||
|
|
||||||
|
project_id = fields.Many2one("project.project", string="Project", required=True)
|
||||||
|
employee_id = fields.Many2one("hr.employee", string="Employee", required=True)
|
||||||
|
|
||||||
|
monthly_salary = fields.Float(string="Monthly Salary")
|
||||||
|
daily_rate = fields.Float(string="Daily Rate")
|
||||||
|
|
||||||
|
duration_days = fields.Integer(string="Estimated Working Days")
|
||||||
|
total_cost = fields.Float(string="Total Cost")
|
||||||
|
|
||||||
|
start_date = fields.Date(string="Estimated Start Date")
|
||||||
|
end_date = fields.Date(string="Estimated End Date")
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 1. Get project start & end dates when project is selected
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
@api.onchange('project_id')
|
||||||
|
def _onchange_project(self):
|
||||||
|
"""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
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 2. If employee selected → load salary
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
@api.onchange('employee_id')
|
||||||
|
def _onchange_employee(self):
|
||||||
|
if self.employee_id:
|
||||||
|
contract = self.employee_id.contract_id
|
||||||
|
if contract:
|
||||||
|
self.monthly_salary = contract.wage or 0.0
|
||||||
|
self.daily_rate = (contract.wage / 30) if contract.wage else 0.0
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 3. Daily ↔ Monthly salary sync
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
@api.onchange('daily_rate')
|
||||||
|
def _onchange_daily_rate(self):
|
||||||
|
if self.daily_rate:
|
||||||
|
self.monthly_salary = self.daily_rate * 30
|
||||||
|
|
||||||
|
if self.duration_days and self.daily_rate:
|
||||||
|
self.total_cost = self.daily_rate * self.duration_days
|
||||||
|
|
||||||
|
@api.onchange('monthly_salary')
|
||||||
|
def _onchange_monthly_salary(self):
|
||||||
|
if self.monthly_salary:
|
||||||
|
self.daily_rate = self.monthly_salary / 30
|
||||||
|
|
||||||
|
if self.duration_days and self.daily_rate:
|
||||||
|
self.total_cost = self.daily_rate * self.duration_days
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# 4. Date change → update duration and cost
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
@api.onchange('start_date', 'end_date')
|
||||||
|
def _onchange_dates(self):
|
||||||
|
if self.start_date and self.end_date:
|
||||||
|
duration = (self.end_date - self.start_date).days + 1
|
||||||
|
self.duration_days = max(duration, 0)
|
||||||
|
|
||||||
|
if self.duration_days and self.daily_rate:
|
||||||
|
self.total_cost = self.daily_rate * self.duration_days
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
class ProjectRisk(models.Model):
|
||||||
|
_name = "project.risk"
|
||||||
|
_description = "Project Risk Management"
|
||||||
|
_order = "id desc"
|
||||||
|
|
||||||
|
project_id = fields.Many2one(
|
||||||
|
"project.project",
|
||||||
|
string="Project",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade"
|
||||||
|
)
|
||||||
|
|
||||||
|
risk_description = fields.Text(string="Risk Description", required=True)
|
||||||
|
probability = fields.Selection([
|
||||||
|
('low', "Low"),
|
||||||
|
('medium', "Medium"),
|
||||||
|
('high', "High")
|
||||||
|
], string="Probability", required=True, default='medium')
|
||||||
|
|
||||||
|
impact = fields.Selection([
|
||||||
|
('low', "Low"),
|
||||||
|
('medium', "Medium"),
|
||||||
|
('high', "High")
|
||||||
|
], string="Impact", required=True, default='medium')
|
||||||
|
|
||||||
|
mitigation_plan = fields.Text(string="Mitigation Plan")
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
|
||||||
|
class ProjectSprint(models.Model):
|
||||||
|
_name = "project.sprint"
|
||||||
|
_description = "Project Sprint"
|
||||||
|
_order = "date_start desc"
|
||||||
|
|
||||||
|
project_id = fields.Many2one(
|
||||||
|
"project.project",
|
||||||
|
required=True,
|
||||||
|
ondelete="cascade"
|
||||||
|
)
|
||||||
|
|
||||||
|
sprint_name = fields.Char(string="Sprint Name", required=True)
|
||||||
|
date_start = fields.Date(string="Start Date", required=True)
|
||||||
|
date_end = fields.Date(string="End Date", required=True)
|
||||||
|
|
||||||
|
allocated_hours = fields.Float(string="Allocated Hours")
|
||||||
|
|
||||||
|
status = fields.Selection(
|
||||||
|
[
|
||||||
|
("draft", "Draft"),
|
||||||
|
("in_progress", "In Progress"),
|
||||||
|
("done", "Completed"),
|
||||||
|
],
|
||||||
|
default="draft",
|
||||||
|
string="Status"
|
||||||
|
)
|
||||||
|
done_date = fields.Date(string="Done Date")
|
||||||
|
note = fields.Text(string="Note")
|
||||||
|
|
@ -0,0 +1,696 @@
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from markupsafe import Markup
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectStages(models.Model):
|
||||||
|
_inherit = "project.project.stage"
|
||||||
|
|
||||||
|
approval_by = fields.Selection([('project_manager', 'Project Manager'), ('project_sponsor', 'Project Sponsor')])
|
||||||
|
|
||||||
|
|
||||||
|
class projectStagesApprovalFlow(models.Model):
|
||||||
|
_name = 'project.stages.approval.flow'
|
||||||
|
|
||||||
|
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_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")
|
||||||
|
|
||||||
|
def _compute_manager_level_edit_access(self):
|
||||||
|
for rec in self:
|
||||||
|
if rec.approval_by == self.env.user or rec.project_id.user_id == self.env.user or self.env.user.has_group("project.group_project_manager"):
|
||||||
|
rec.manager_level_edit_access = True
|
||||||
|
else:
|
||||||
|
rec.manager_level_edit_access = False
|
||||||
|
def _compute_all_project_managers(self):
|
||||||
|
# Get the project manager Odoo group
|
||||||
|
group_pm = self.env.ref("project.group_project_manager")
|
||||||
|
|
||||||
|
for rec in self:
|
||||||
|
# Search all users who belong to this group
|
||||||
|
pm_users = []
|
||||||
|
|
||||||
|
if rec.stage_approval_by == 'project_manager' and rec.project_id.user_id:
|
||||||
|
pm_users = rec.project_id.user_id.ids
|
||||||
|
elif rec.stage_approval_by == 'project_sponsor' and rec.project_id.project_sponsor:
|
||||||
|
pm_users = rec.project_id.project_sponsor.ids
|
||||||
|
else:
|
||||||
|
pm_users = self.env['res.users'].sudo().search([
|
||||||
|
('groups_id', 'in', group_pm.id)
|
||||||
|
]).ids
|
||||||
|
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Development Documents
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
class TaskDevelopmentDocument(models.Model):
|
||||||
|
_name = "task.development.document"
|
||||||
|
_description = "Development Documents"
|
||||||
|
_order = "create_date desc"
|
||||||
|
|
||||||
|
task_id = fields.Many2one(
|
||||||
|
"project.task",
|
||||||
|
string="Task",
|
||||||
|
ondelete="cascade"
|
||||||
|
)
|
||||||
|
|
||||||
|
project_id = fields.Many2one("project.project",string="Project",ondelete="cascade")
|
||||||
|
|
||||||
|
name = fields.Char(string="Document Name", required=True)
|
||||||
|
file = fields.Binary(string="Document File", required=True)
|
||||||
|
file_name = fields.Char(string="Filename")
|
||||||
|
notes = fields.Text(string="Notes")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
# Testing Documents
|
||||||
|
# ---------------------------------------------------------
|
||||||
|
class TaskTestingDocument(models.Model):
|
||||||
|
_name = "task.testing.document"
|
||||||
|
_description = "Testing Documents"
|
||||||
|
_order = "create_date desc"
|
||||||
|
|
||||||
|
task_id = fields.Many2one(
|
||||||
|
"project.task",
|
||||||
|
string="Task",
|
||||||
|
ondelete="cascade"
|
||||||
|
)
|
||||||
|
project_id = fields.Many2one("project.project",string="Project",ondelete="cascade")
|
||||||
|
|
||||||
|
name = fields.Char(string="Document Name", required=True)
|
||||||
|
file = fields.Binary(string="Testing File", required=True)
|
||||||
|
file_name = fields.Char(string="Filename")
|
||||||
|
notes = fields.Text(string="Notes")
|
||||||
|
|
@ -3,12 +3,42 @@ internal_teams_admin,internal.teams.admin,model_internal_teams,project.group_pro
|
||||||
internal_teams_manager,internal.teams.manager,model_internal_teams,project.group_project_user,1,1,1,0
|
internal_teams_manager,internal.teams.manager,model_internal_teams,project.group_project_user,1,1,1,0
|
||||||
internal_teams_user,internal.teams.user,model_internal_teams,base.group_user,1,0,0,0
|
internal_teams_user,internal.teams.user,model_internal_teams,base.group_user,1,0,0,0
|
||||||
|
|
||||||
|
access_project_sprint_user,access.project.sprint.user,model_project_sprint,project.group_project_user,1,1,1,1
|
||||||
|
access_project_sprint_manager,access.project.sprint.manager,model_project_sprint,project.group_project_manager,1,1,1,1
|
||||||
|
|
||||||
|
|
||||||
|
access_project_architecture_design_user,access_project_architecture_design_user,model_project_architecture_design,project.group_project_user,1,1,1,1
|
||||||
|
access_project_architecture_design_manager,access_project_architecture_design_manager,model_project_architecture_design,project.group_project_manager,1,1,1,1
|
||||||
|
|
||||||
|
access_project_risk_user,access_project_risk_user,model_project_risk,project.group_project_user,1,1,1,1
|
||||||
|
access_project_risk_manager,access_project_risk_manager,model_project_risk,project.group_project_manager,1,1,1,1
|
||||||
|
|
||||||
|
access_project_resource_cost_user,project.resource.cost.user,model_project_resource_cost,project.group_project_user,1,1,1,1
|
||||||
|
access_project_resource_cost_manager,project.resource.cost.manager,model_project_resource_cost,project.group_project_manager,1,1,1,1
|
||||||
|
|
||||||
|
access_project_material_cost_user,project.material.cost.user,model_project_material_cost,project.group_project_user,1,1,1,1
|
||||||
|
access_project_material_cost_manager,project.material.cost.manager,model_project_material_cost,project.group_project_manager,1,1,1,1
|
||||||
|
|
||||||
|
access_project_equipment_cost_user,project.equipment.cost.user,model_project_equipment_cost,project.group_project_user,1,1,1,1
|
||||||
|
access_project_equipment_cost_manager,project.equipment.cost.manager,model_project_equipment_cost,project.group_project_manager,1,1,1,1
|
||||||
|
|
||||||
|
access_project_commit_step_user,access_project_commit_step_user,model_project_commit_step,project.group_project_user,1,1,1,1
|
||||||
|
access_project_commit_step_manager,access_project_commit_step_manager,model_project_commit_step,project.group_project_manager,1,1,1,1
|
||||||
|
|
||||||
|
access_task_development_document,access_task_development_document,model_task_development_document,base.group_user,1,1,1,1
|
||||||
|
access_task_testing_document,access_task_testing_document,model_task_testing_document,base.group_user,1,1,1,1
|
||||||
|
|
||||||
project_user_assign_wizard_manager,project.user.assign.wizard,model_project_user_assign_wizard,project_task_timesheet_extended.group_project_supervisor,1,1,1,1
|
project_user_assign_wizard_manager,project.user.assign.wizard,model_project_user_assign_wizard,project_task_timesheet_extended.group_project_supervisor,1,1,1,1
|
||||||
project_user_assign_wizard_admin,project.user.assign.wizard.admin,model_project_user_assign_wizard,project.group_project_manager,1,1,1,1
|
project_user_assign_wizard_admin,project.user.assign.wizard.admin,model_project_user_assign_wizard,project.group_project_manager,1,1,1,1
|
||||||
project_user_assign_wizard_user,project.user.assign.wizard.user,model_project_user_assign_wizard,project.group_project_manager,1,0,0,0
|
project_user_assign_wizard_user,project.user.assign.wizard.user,model_project_user_assign_wizard,project.group_project_manager,1,0,0,0
|
||||||
|
|
||||||
|
project_user_project_reject_reason_wizard,project.reject.reason.wizard.user,model_project_reject_reason_wizard,base.group_user,1,1,1,1
|
||||||
project_user_task_reject_reason_wizard,task.reject.reason.wizard.user,model_task_reject_reason_wizard,base.group_user,1,1,1,1
|
project_user_task_reject_reason_wizard,task.reject.reason.wizard.user,model_task_reject_reason_wizard,base.group_user,1,1,1,1
|
||||||
|
|
||||||
|
|
||||||
|
access_project_stages_approval_flow_admin,access.project.stages.approval.flow.admin,model_project_stages_approval_flow,project.group_project_manager,1,1,1,1
|
||||||
|
access_project_stages_approval_flow_user,access.project.stages.approval.flow.user,model_project_stages_approval_flow,base.group_user,1,1,0,0
|
||||||
|
|
||||||
project_internal_team_members_wizard,internal.team.members.wizard.manager,model_internal_team_members_wizard,base.group_user,1,1,1,1
|
project_internal_team_members_wizard,internal.team.members.wizard.manager,model_internal_team_members_wizard,base.group_user,1,1,1,1
|
||||||
project_project_stage_update_wizard,project.stage.update.wizard.manager,model_project_stage_update_wizard,base.group_user,1,1,1,1
|
project_project_stage_update_wizard,project.stage.update.wizard.manager,model_project_stage_update_wizard,base.group_user,1,1,1,1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
|
@ -82,16 +82,4 @@
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="project_invoice_form_inherit" model="ir.ui.view">
|
|
||||||
<field name="name">project.invoice.inherit.form.view</field>
|
|
||||||
<field name="model">project.project</field>
|
|
||||||
<field name="inherit_id" ref="hr_timesheet.project_invoice_form"/>
|
|
||||||
<field name="arch" type="xml">
|
|
||||||
<xpath expr="//field[@name='allocated_hours']" position="after">
|
|
||||||
<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>
|
|
||||||
</field>
|
|
||||||
</record>
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
@ -0,0 +1,540 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="project_project_stage_list_inherit" 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='name']" position="after">
|
||||||
|
<field name="approval_by"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="project_project_inherit_form_view2" model="ir.ui.view">
|
||||||
|
<field name="name">project.project.inherit.form.view</field>
|
||||||
|
<field name="model">project.project</field>
|
||||||
|
<field name="inherit_id" ref="project.edit_project"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//form/header" position="inside">
|
||||||
|
<button type="object" name="submit_project_for_approval"
|
||||||
|
string="Submit for Approval"
|
||||||
|
class="oe_highlight"
|
||||||
|
invisible="not assign_approval_flow or not show_submission_button"/>
|
||||||
|
|
||||||
|
<button type="object" name="project_proceed_further"
|
||||||
|
string="Approve & Proceed"
|
||||||
|
class="oe_highlight"
|
||||||
|
invisible="not assign_approval_flow or not show_approval_button"/>
|
||||||
|
|
||||||
|
<button type="object" name="action_open_reject_wizard"
|
||||||
|
string="Reject"
|
||||||
|
class="oe_highlight"
|
||||||
|
invisible="not assign_approval_flow or not show_refuse_button"/>
|
||||||
|
|
||||||
|
<button type="object" name="project_back_button"
|
||||||
|
string="Go Back"
|
||||||
|
class="oe_highlight"
|
||||||
|
invisible="not assign_approval_flow or not show_back_button"/>
|
||||||
|
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//form" position="inside">
|
||||||
|
<field name="showable_stage_ids" invisible="1"/>
|
||||||
|
<field name="assign_approval_flow" invisible="1"/>
|
||||||
|
<field name="manager_level_edit_access" invisible="1"/>
|
||||||
|
<field name="show_submission_button" invisible="1"/>
|
||||||
|
<field name="show_approval_button" invisible="1"/>
|
||||||
|
<field name="show_refuse_button" invisible="1"/>
|
||||||
|
<field name="show_back_button" invisible="1"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='stage_id']" position="attributes">
|
||||||
|
<attribute name="domain">[('id', 'in', showable_stage_ids)]</attribute>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
|
||||||
|
<xpath expr="//page[@name='settings']" position="before">
|
||||||
|
<page name="project_stages" string="Project Stages" invisible="not assign_approval_flow">
|
||||||
|
<field name="project_stages" options="{'no_create': True, 'no_open': True, 'no_delete': True}">
|
||||||
|
<list editable="bottom" delete="0" create="0">
|
||||||
|
<field name="stage_id" readonly="1"/>
|
||||||
|
<field name="approval_by" readonly="not manager_level_edit_access"/>
|
||||||
|
<field name="assigned_to" readonly="not manager_level_edit_access"/>
|
||||||
|
<field name="assigned_date" readonly="1" optional="hide"/>
|
||||||
|
<field name="submission_date" readonly="1" optional="hide"/>
|
||||||
|
<field name="note" optional="show" readonly="not manager_level_edit_access"/>
|
||||||
|
<field name="project_id" invisible="1" column_invisible="1"/>
|
||||||
|
<field name="stage_approval_by" invisible="1" column_invisible="1"/>
|
||||||
|
<field name="approval_by_users" invisible="1" column_invisible="1"/>
|
||||||
|
<field name="manager_level_edit_access" invisible="1" column_invisible="1"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//chatter" position="attributes">
|
||||||
|
<attribute name="invisible">not show_project_chatter</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='user_id']" position="before">
|
||||||
|
<field name="project_sponsor" widget="many2one_avatar_user"/>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//header/field[@name='stage_id']" position="attributes">
|
||||||
|
<attribute name="readonly">assign_approval_flow</attribute>
|
||||||
|
</xpath>
|
||||||
|
<page name="description" position="attributes">
|
||||||
|
<attribute name="string">Initiation</attribute>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<xpath expr="//field[@name='description']" position="attributes">
|
||||||
|
<attribute name="invisible">1</attribute>
|
||||||
|
</xpath>
|
||||||
|
<page name="description" position="inside">
|
||||||
|
<group>
|
||||||
|
<field name="project_vision" placeholder="Eg: Build a mobile app that allows users to order groceries and track delivery in real time." readonly="not manager_level_edit_access"/>
|
||||||
|
</group>
|
||||||
|
<group string="Requirements Document">
|
||||||
|
|
||||||
|
<div class="o_row" style="align-items: flex-start;">
|
||||||
|
|
||||||
|
<!-- LEFT SIDE -->
|
||||||
|
<div class="o_col" style="width: 70%;">
|
||||||
|
|
||||||
|
<!-- HTML field (visible when NO file uploaded) -->
|
||||||
|
<field name="description"
|
||||||
|
widget="html" force_save="1" readonly="not manager_level_edit_access"
|
||||||
|
invisible="requirement_file" placeholder="The system should allow user login,
|
||||||
|
Users should be able to add items to a cart."/>
|
||||||
|
|
||||||
|
<!-- PDF Viewer (visible when file exists) -->
|
||||||
|
<field name="requirement_file"
|
||||||
|
widget="pdf_viewer" 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="pdf_viewer" 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="pdf_viewer" 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="pdf_viewer" force_save="1"
|
||||||
|
filename="feasibility_file_name"
|
||||||
|
invisible="feasibility_file or not manager_level_edit_access"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<xpath expr="//page[@name='settings']" position="inside">
|
||||||
|
<group>
|
||||||
|
<group name="group_sprint_requirement_management" string="Project Sprint" col="1"
|
||||||
|
class="row mt16 o_settings_container">
|
||||||
|
<div>
|
||||||
|
<setting class="col-lg-12" id="project_sprint_requirement_settings"
|
||||||
|
help="Enable it if you want to add Sprints for the project">
|
||||||
|
<field name="require_sprint"/>
|
||||||
|
</setting>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<xpath expr="//sheet" position="inside">
|
||||||
|
<widget name="web_ribbon" title="Rejected" bg_color="text-bg-danger" invisible="approval_status != 'reject'" />
|
||||||
|
<widget name="web_ribbon" title="Rejected" invisible="approval_status != 'submitted'" />
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page string="Project Activity Log" invisible="not assign_approval_flow or not show_project_chatter">
|
||||||
|
<field name="project_activity_log" widget="html" options="{'sanitize': False}" readonly="1"
|
||||||
|
force_save="1"/>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='date_start']" position="attributes">
|
||||||
|
<attribute name="invisible">1</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//field[@name='date']" position="attributes">
|
||||||
|
<attribute name="invisible">1</attribute>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<xpath expr="//sheet/notebook/page[@name='settings']" position="after">
|
||||||
|
<page name="planning" string="Planning (Budget & Deadlines)">
|
||||||
|
<group>
|
||||||
|
|
||||||
|
<group string="project Scope">
|
||||||
|
<field name="project_scope" nolabel="1"/>
|
||||||
|
</group>
|
||||||
|
<group string="Timelines">
|
||||||
|
<group>
|
||||||
|
|
||||||
|
<field name="date_start" string="Planned Date" widget="daterange"
|
||||||
|
options='{"end_date_field": "date", "always_range": "1"}'
|
||||||
|
required="date_start or date"/>
|
||||||
|
<field name="date" invisible="1" required="date_start"/>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
<group></group>
|
||||||
|
<group>
|
||||||
|
<field name="allocated_hours" widget="timesheet_uom_no_toggle" optional="hide"
|
||||||
|
invisible="allocated_hours == 0 or not allow_timesheets"
|
||||||
|
groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||||
|
<field name="effective_hours" widget="timesheet_uom_no_toggle" optional="hide"
|
||||||
|
invisible="effective_hours == 0 or not allow_timesheets"
|
||||||
|
groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||||
|
<field name="remaining_hours" widget="timesheet_uom_no_toggle"
|
||||||
|
decoration-danger="remaining_hours < 0"
|
||||||
|
decoration-warning="allocated_hours > 0 and (remaining_hours / allocated_hours) < 0.2"
|
||||||
|
optional="hide"
|
||||||
|
invisible="allocated_hours == 0 or not allow_timesheets"
|
||||||
|
groups="hr_timesheet.group_hr_timesheet_user"
|
||||||
|
/>
|
||||||
|
<field name="estimated_hours" widget="timesheet_uom_no_toggle"
|
||||||
|
invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
|
||||||
|
<field name="task_estimated_hours" widget="timesheet_uom_no_toggle"
|
||||||
|
invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||||
|
<field name="actual_hours" widget="timesheet_uom_no_toggle"
|
||||||
|
invisible="not allow_timesheets"
|
||||||
|
groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
<group string="Risk Management Plan">
|
||||||
|
<field name="risk_ids" nolabel="1">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="project_id" invisible="1" column_invisible="1"/>
|
||||||
|
<field name="risk_description" width="30%"/>
|
||||||
|
<field name="probability" width="20%"/>
|
||||||
|
<field name="impact" width="20%"/>
|
||||||
|
<field name="mitigation_plan" width="30%"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</group>
|
||||||
|
<group string="Budget Planning">
|
||||||
|
<group>
|
||||||
|
<field name="estimated_amount"/>
|
||||||
|
<field name="total_budget_amount" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<notebook>
|
||||||
|
<!-- MANPOWER TAB -->
|
||||||
|
<page string="Manpower">
|
||||||
|
|
||||||
|
<button name="action_fetch_resource_data"
|
||||||
|
type="object"
|
||||||
|
string="Fetch Resource Data"
|
||||||
|
class="oe_highlight"
|
||||||
|
icon="fa-refresh"/>
|
||||||
|
|
||||||
|
<field name="resource_cost_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="employee_id"/>
|
||||||
|
<field name="monthly_salary"/>
|
||||||
|
<field name="daily_rate"/>
|
||||||
|
<field name="start_date" optional="hide"/>
|
||||||
|
<field name="end_date" optional="hide"/>
|
||||||
|
<field name="duration_days" readonly="1"/>
|
||||||
|
<field name="total_cost"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<!-- MATERIAL TAB -->
|
||||||
|
<page string="Material">
|
||||||
|
<field name="material_cost_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="material_name" width="30%"/>
|
||||||
|
<field name="qty" width="20%"/>
|
||||||
|
<field name="unit_cost" width="20%"/>
|
||||||
|
<field name="total_cost" readonly="1" width="20%"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
<!-- EQUIPMENT TAB -->
|
||||||
|
<page string="Equipments/Others">
|
||||||
|
<field name="equipment_cost_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="equipment_name" string="Equip/Others" width="30%"/>
|
||||||
|
<field name="duration_hours" width="20%"/>
|
||||||
|
<field name="hourly_rate" width="20%"/>
|
||||||
|
<field name="total_cost" readonly="1" width="20%"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
|
||||||
|
</notebook>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//sheet/notebook" position="inside">
|
||||||
|
<page name="architecture_design" string="Architecture & Design">
|
||||||
|
|
||||||
|
<field name="architecture_design_ids">
|
||||||
|
<list>
|
||||||
|
<field name="tech_stack"/>
|
||||||
|
<field name="design_status"/>
|
||||||
|
<field name="architecture_notes" optional="hide"/>
|
||||||
|
<field name="database_notes" optional="hide"/>
|
||||||
|
<field name="reviewer_comments" optional="hide"/>
|
||||||
|
</list>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group string="System Architecture">
|
||||||
|
<field name="architecture_diagram"/>
|
||||||
|
<field name="tech_stack"/>
|
||||||
|
<field name="architecture_notes"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group>
|
||||||
|
<group string="UI / UX">
|
||||||
|
<field name="ui_wireframe"/>
|
||||||
|
<field name="ux_flow"/>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group string="Database Design">
|
||||||
|
<field name="er_diagram"/>
|
||||||
|
<field name="database_notes"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
<group string="Review">
|
||||||
|
<field name="design_status"/>
|
||||||
|
<field name="reviewer_comments"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</field>
|
||||||
|
|
||||||
|
</page>
|
||||||
|
<page name="project_sprints" string="Sprints" invisible="not require_sprint">
|
||||||
|
<field name="sprint_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="sprint_name" width="20%"/>
|
||||||
|
<field name="date_start" width="20%"/>
|
||||||
|
<field name="date_end" width="20%"/>
|
||||||
|
<field name="allocated_hours" width="20%"/>
|
||||||
|
<field name="status" width="20%"/>
|
||||||
|
<field name="done_date" optional="hide"/>
|
||||||
|
<field name="note" optional="hide"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page name="development" string="Development Details">
|
||||||
|
<group string="Documents">
|
||||||
|
<field name="development_document_ids">
|
||||||
|
<list editable="bottom">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="file" filename="file_name"/>
|
||||||
|
<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="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>
|
||||||
|
|
@ -20,3 +20,25 @@ class TaskRejectReasonWizard(models.TransientModel):
|
||||||
return {
|
return {
|
||||||
"type": "ir.actions.act_window_close"
|
"type": "ir.actions.act_window_close"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectRejectReasonWizard(models.TransientModel):
|
||||||
|
_name = "project.reject.reason.wizard"
|
||||||
|
_description = "Project Rejection Reason Wizard"
|
||||||
|
|
||||||
|
reason = fields.Text(string="Rejection Reason", required=True)
|
||||||
|
project_id = fields.Many2one("project.project", string="Project", required=True)
|
||||||
|
|
||||||
|
def action_reject(self):
|
||||||
|
"""Trigger the rejection action on the selected task"""
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.reason:
|
||||||
|
raise UserError(_("Please enter a reason for rejection."))
|
||||||
|
|
||||||
|
# Call the existing reject method on the task
|
||||||
|
self.project_id.reject_and_return(reason=self.reason)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.act_window_close"
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,27 @@
|
||||||
<field name="view_mode">form</field>
|
<field name="view_mode">form</field>
|
||||||
<field name="target">new</field>
|
<field name="target">new</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="view_project_reject_reason_wizard" model="ir.ui.view">
|
||||||
|
<field name="name">project.reject.reason.wizard.form</field>
|
||||||
|
<field name="model">project.reject.reason.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Reject Task">
|
||||||
|
<group>
|
||||||
|
<field name="reason" placeholder="Enter the reason for rejection..."/>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button string="Reject" type="object" name="action_reject" class="btn-primary"/>
|
||||||
|
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_project_reject_reason_wizard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Reject Task</field>
|
||||||
|
<field name="res_model">project.reject.reason.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -85,18 +85,18 @@
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<!-- Default template user for new users signing in -->
|
<!-- Default template user for new users signing in -->
|
||||||
<record id="template_portal_user_id" model="res.users">
|
<!-- <record id="template_portal_user_id" model="res.users">-->
|
||||||
<field name="name">Portal User Template</field>
|
<!-- <field name="name">Portal User Template</field>-->
|
||||||
<field name="login">portaltemplate</field>
|
<!-- <field name="login">portaltemplate</field>-->
|
||||||
<field name="active" eval="False"/>
|
<!-- <field name="active" eval="False"/>-->
|
||||||
<field name="groups_id" eval="[Command.set([ref('base.group_portal')])]"/>
|
<!-- <field name="groups_id" eval="[Command.set([ref('base.group_portal')])]"/>-->
|
||||||
<field name="signature" /> <!-- Needed for avoiding the _compute_signature triggering on each update -->
|
<!-- <field name="signature" /> <!– Needed for avoiding the _compute_signature triggering on each update –>-->
|
||||||
</record>
|
<!-- </record>-->
|
||||||
|
|
||||||
<record id="default_template_user_config" model="ir.config_parameter">
|
<!-- <record id="default_template_user_config" model="ir.config_parameter">-->
|
||||||
<field name="key">base.template_portal_user_id</field>
|
<!-- <field name="key">base.template_portal_user_id</field>-->
|
||||||
<field name="value" ref="template_portal_user_id"/>
|
<!-- <field name="value" ref="template_portal_user_id"/>-->
|
||||||
</record>
|
<!-- </record>-->
|
||||||
|
|
||||||
</data>
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue