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',
|
||||
'analytic',
|
||||
'project_gantt',
|
||||
'hr',
|
||||
'hr_contract',
|
||||
],
|
||||
'data': [
|
||||
'security/security.xml',
|
||||
|
|
@ -35,6 +37,7 @@ Key Features:
|
|||
'wizards/project_stage_update_wizard.xml',
|
||||
'wizards/task_reject_reason_wizard.xml',
|
||||
'view/teams.xml',
|
||||
'view/project_stages.xml',
|
||||
'view/task_stages.xml',
|
||||
'view/project.xml',
|
||||
'view/project_task.xml',
|
||||
|
|
|
|||
|
|
@ -12,6 +12,43 @@
|
|||
</field>
|
||||
</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">-->
|
||||
<!-- <field name="name">Reward User</field>-->
|
||||
<!-- <field name="model_id" ref="project.model_project_task"/>-->
|
||||
|
|
@ -83,4 +120,103 @@
|
|||
<!-- <field name="user_id" eval="False"/>-->
|
||||
<!-- </record>-->
|
||||
</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>
|
||||
|
|
@ -1,7 +1,15 @@
|
|||
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 project
|
||||
from . import project_task
|
||||
from . import timesheets
|
||||
# from . import project_task_gantt
|
||||
from . import user_availability
|
||||
from . import user_availability
|
||||
|
|
|
|||
|
|
@ -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_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_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_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
|
||||
|
||||
|
||||
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_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>
|
||||
</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>
|
||||
|
|
@ -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 {
|
||||
"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="target">new</field>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -85,18 +85,18 @@
|
|||
</record>
|
||||
|
||||
<!-- Default template user for new users signing in -->
|
||||
<record id="template_portal_user_id" model="res.users">
|
||||
<field name="name">Portal User Template</field>
|
||||
<field name="login">portaltemplate</field>
|
||||
<field name="active" eval="False"/>
|
||||
<field name="groups_id" eval="[Command.set([ref('base.group_portal')])]"/>
|
||||
<field name="signature" /> <!-- Needed for avoiding the _compute_signature triggering on each update -->
|
||||
</record>
|
||||
<!-- <record id="template_portal_user_id" model="res.users">-->
|
||||
<!-- <field name="name">Portal User Template</field>-->
|
||||
<!-- <field name="login">portaltemplate</field>-->
|
||||
<!-- <field name="active" eval="False"/>-->
|
||||
<!-- <field name="groups_id" eval="[Command.set([ref('base.group_portal')])]"/>-->
|
||||
<!-- <field name="signature" /> <!– Needed for avoiding the _compute_signature triggering on each update –>-->
|
||||
<!-- </record>-->
|
||||
|
||||
<record id="default_template_user_config" model="ir.config_parameter">
|
||||
<field name="key">base.template_portal_user_id</field>
|
||||
<field name="value" ref="template_portal_user_id"/>
|
||||
</record>
|
||||
<!-- <record id="default_template_user_config" model="ir.config_parameter">-->
|
||||
<!-- <field name="key">base.template_portal_user_id</field>-->
|
||||
<!-- <field name="value" ref="template_portal_user_id"/>-->
|
||||
<!-- </record>-->
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
|
|||
Loading…
Reference in New Issue