Project timesheet updates

This commit is contained in:
Pranay 2025-11-25 16:45:09 +05:30
parent 44e5ee7e2f
commit 20d22c1f04
23 changed files with 7207 additions and 24 deletions

View File

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

View File

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

View File

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

View File

@ -25,6 +25,8 @@ Key Features:
'base', 'base',
'analytic', 'analytic',
'project_gantt', 'project_gantt',
'hr',
'hr_contract',
], ],
'data': [ 'data': [
'security/security.xml', 'security/security.xml',
@ -35,6 +37,7 @@ Key Features:
'wizards/project_stage_update_wizard.xml', 'wizards/project_stage_update_wizard.xml',
'wizards/task_reject_reason_wizard.xml', 'wizards/task_reject_reason_wizard.xml',
'view/teams.xml', 'view/teams.xml',
'view/project_stages.xml',
'view/task_stages.xml', 'view/task_stages.xml',
'view/project.xml', 'view/project.xml',
'view/project_task.xml', 'view/project_task.xml',

View File

@ -12,6 +12,43 @@
</field> </field>
</record> </record>
<record id="action_show_project_chatter" model="ir.actions.server">
<field name="name">Show/Hide Chatter</field>
<field name="model_id" ref="project.model_project_project"/>
<field name="binding_model_id" ref="project.model_project_project"/>
<field name="binding_type">action</field>
<field name="state">code</field>
<field name="code">
if records:
action = records.action_show_project_chatter()
</field>
</record>
<record id="action_show_project_task_chatter" model="ir.actions.server">
<field name="name">Show/Hide Chatter</field>
<field name="model_id" ref="project.model_project_task"/>
<field name="binding_model_id" ref="project.model_project_task"/>
<field name="binding_type">action</field>
<field name="state">code</field>
<field name="code">
if records:
action = records.action_show_project_task_chatter()
</field>
</record>
<record id="action_assign_approval_flow" model="ir.actions.server">
<field name="name">Enable/Disable Stage Approvals</field>
<field name="model_id" ref="project.model_project_project"/>
<field name="binding_model_id" ref="project.model_project_project"/>
<field name="binding_type">action</field>
<field name="state">code</field>
<field name="code">
if records:
action = records.action_assign_approval_flow()
</field>
</record>
<!-- <record id="action_reward_user" model="ir.actions.server">--> <!-- <record id="action_reward_user" model="ir.actions.server">-->
<!-- <field name="name">Reward User</field>--> <!-- <field name="name">Reward User</field>-->
<!-- <field name="model_id" ref="project.model_project_task"/>--> <!-- <field name="model_id" ref="project.model_project_task"/>-->
@ -83,4 +120,103 @@
<!-- <field name="user_id" eval="False"/>--> <!-- <field name="user_id" eval="False"/>-->
<!-- </record>--> <!-- </record>-->
</data> </data>
<data noupdate="1">
<record id="project_project_stage_initiate" model="project.project.stage">
<field name="name">Initial</field>
<field name="approval_by">project_sponsor</field>
<field name="sequence">1</field>
<field name="fold" eval="False"/>
</record>
<record id="project_project_stage_planning" model="project.project.stage">
<field name="name">Planning</field>
<field name="approval_by">project_manager</field>
<field name="sequence">2</field>
<field name="fold" eval="False"/>
</record>
<record id="project_project_stage_architecture" model="project.project.stage">
<field name="name">Architecture &amp; 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 &amp; 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 &amp; Support</field>
<field name="approval_by">project_manager</field>
<field name="sequence">8</field>
<field name="fold" eval="False"/>
</record>
<record id="project_project_stage_closer" model="project.project.stage">
<field name="name">Closer</field>
<field name="approval_by">project_sponsor</field>
<field name="sequence">9</field>
<field name="fold" eval="True"/>
</record>
<record id="project.project_project_stage_0" model="project.project.stage">
<field name="sequence">10</field>
<field name="name">To Do</field>
<field name="active">False</field>
</record>
<record id="project.project_project_stage_1" model="project.project.stage">
<field name="sequence">15</field>
<field name="name">In Progress</field>
<field name="active">False</field>
</record>
<record id="project.project_project_stage_2" model="project.project.stage">
<field name="sequence">20</field>
<field name="name">Done</field>
<field name="fold" eval="True"/>
<field name="active">False</field>
</record>
<record id="project.project_project_stage_3" model="project.project.stage">
<field name="sequence">25</field>
<field name="name">Cancelled</field>
<field name="fold" eval="True"/>
<field name="approval_by">project_manager</field>
</record>
</data>
</odoo> </odoo>

View File

@ -1,4 +1,12 @@
from . import teams from . import teams
from . import project_sprint
from . import task_documents
from . import project_architecture_design
from . import project_risk
from . import project_resource_cost
from . import project_costings
from . import project_code_commit
from . import project_stages
from . import task_stages from . import task_stages
from . import project from . import project
from . import project_task from . import project_task

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,12 +3,42 @@ internal_teams_admin,internal.teams.admin,model_internal_teams,project.group_pro
internal_teams_manager,internal.teams.manager,model_internal_teams,project.group_project_user,1,1,1,0 internal_teams_manager,internal.teams.manager,model_internal_teams,project.group_project_user,1,1,1,0
internal_teams_user,internal.teams.user,model_internal_teams,base.group_user,1,0,0,0 internal_teams_user,internal.teams.user,model_internal_teams,base.group_user,1,0,0,0
access_project_sprint_user,access.project.sprint.user,model_project_sprint,project.group_project_user,1,1,1,1
access_project_sprint_manager,access.project.sprint.manager,model_project_sprint,project.group_project_manager,1,1,1,1
access_project_architecture_design_user,access_project_architecture_design_user,model_project_architecture_design,project.group_project_user,1,1,1,1
access_project_architecture_design_manager,access_project_architecture_design_manager,model_project_architecture_design,project.group_project_manager,1,1,1,1
access_project_risk_user,access_project_risk_user,model_project_risk,project.group_project_user,1,1,1,1
access_project_risk_manager,access_project_risk_manager,model_project_risk,project.group_project_manager,1,1,1,1
access_project_resource_cost_user,project.resource.cost.user,model_project_resource_cost,project.group_project_user,1,1,1,1
access_project_resource_cost_manager,project.resource.cost.manager,model_project_resource_cost,project.group_project_manager,1,1,1,1
access_project_material_cost_user,project.material.cost.user,model_project_material_cost,project.group_project_user,1,1,1,1
access_project_material_cost_manager,project.material.cost.manager,model_project_material_cost,project.group_project_manager,1,1,1,1
access_project_equipment_cost_user,project.equipment.cost.user,model_project_equipment_cost,project.group_project_user,1,1,1,1
access_project_equipment_cost_manager,project.equipment.cost.manager,model_project_equipment_cost,project.group_project_manager,1,1,1,1
access_project_commit_step_user,access_project_commit_step_user,model_project_commit_step,project.group_project_user,1,1,1,1
access_project_commit_step_manager,access_project_commit_step_manager,model_project_commit_step,project.group_project_manager,1,1,1,1
access_task_development_document,access_task_development_document,model_task_development_document,base.group_user,1,1,1,1
access_task_testing_document,access_task_testing_document,model_task_testing_document,base.group_user,1,1,1,1
project_user_assign_wizard_manager,project.user.assign.wizard,model_project_user_assign_wizard,project_task_timesheet_extended.group_project_supervisor,1,1,1,1 project_user_assign_wizard_manager,project.user.assign.wizard,model_project_user_assign_wizard,project_task_timesheet_extended.group_project_supervisor,1,1,1,1
project_user_assign_wizard_admin,project.user.assign.wizard.admin,model_project_user_assign_wizard,project.group_project_manager,1,1,1,1 project_user_assign_wizard_admin,project.user.assign.wizard.admin,model_project_user_assign_wizard,project.group_project_manager,1,1,1,1
project_user_assign_wizard_user,project.user.assign.wizard.user,model_project_user_assign_wizard,project.group_project_manager,1,0,0,0 project_user_assign_wizard_user,project.user.assign.wizard.user,model_project_user_assign_wizard,project.group_project_manager,1,0,0,0
project_user_project_reject_reason_wizard,project.reject.reason.wizard.user,model_project_reject_reason_wizard,base.group_user,1,1,1,1
project_user_task_reject_reason_wizard,task.reject.reason.wizard.user,model_task_reject_reason_wizard,base.group_user,1,1,1,1 project_user_task_reject_reason_wizard,task.reject.reason.wizard.user,model_task_reject_reason_wizard,base.group_user,1,1,1,1
access_project_stages_approval_flow_admin,access.project.stages.approval.flow.admin,model_project_stages_approval_flow,project.group_project_manager,1,1,1,1
access_project_stages_approval_flow_user,access.project.stages.approval.flow.user,model_project_stages_approval_flow,base.group_user,1,1,0,0
project_internal_team_members_wizard,internal.team.members.wizard.manager,model_internal_team_members_wizard,base.group_user,1,1,1,1 project_internal_team_members_wizard,internal.team.members.wizard.manager,model_internal_team_members_wizard,base.group_user,1,1,1,1
project_project_stage_update_wizard,project.stage.update.wizard.manager,model_project_stage_update_wizard,base.group_user,1,1,1,1 project_project_stage_update_wizard,project.stage.update.wizard.manager,model_project_stage_update_wizard,base.group_user,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 internal_teams_manager internal.teams.manager model_internal_teams project.group_project_user 1 1 1 0
4 internal_teams_user internal.teams.user model_internal_teams base.group_user 1 0 0 0
5 project_user_assign_wizard_manager access_project_sprint_user project.user.assign.wizard access.project.sprint.user model_project_user_assign_wizard model_project_sprint project_task_timesheet_extended.group_project_supervisor project.group_project_user 1 1 1 1
6 access_project_sprint_manager access.project.sprint.manager model_project_sprint project.group_project_manager 1 1 1 1
7 access_project_architecture_design_user access_project_architecture_design_user model_project_architecture_design project.group_project_user 1 1 1 1
8 access_project_architecture_design_manager access_project_architecture_design_manager model_project_architecture_design project.group_project_manager 1 1 1 1
9 access_project_risk_user access_project_risk_user model_project_risk project.group_project_user 1 1 1 1
10 access_project_risk_manager access_project_risk_manager model_project_risk project.group_project_manager 1 1 1 1
11 access_project_resource_cost_user project.resource.cost.user model_project_resource_cost project.group_project_user 1 1 1 1
12 access_project_resource_cost_manager project.resource.cost.manager model_project_resource_cost project.group_project_manager 1 1 1 1
13 access_project_material_cost_user project.material.cost.user model_project_material_cost project.group_project_user 1 1 1 1
14 access_project_material_cost_manager project.material.cost.manager model_project_material_cost project.group_project_manager 1 1 1 1
15 access_project_equipment_cost_user project.equipment.cost.user model_project_equipment_cost project.group_project_user 1 1 1 1
16 access_project_equipment_cost_manager project.equipment.cost.manager model_project_equipment_cost project.group_project_manager 1 1 1 1
17 access_project_commit_step_user access_project_commit_step_user model_project_commit_step project.group_project_user 1 1 1 1
18 access_project_commit_step_manager access_project_commit_step_manager model_project_commit_step project.group_project_manager 1 1 1 1
19 access_task_development_document access_task_development_document model_task_development_document base.group_user 1 1 1 1
20 access_task_testing_document access_task_testing_document model_task_testing_document base.group_user 1 1 1 1
21 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
22 project_user_assign_wizard_admin project.user.assign.wizard.admin model_project_user_assign_wizard project.group_project_manager 1 1 1 1
23 project_user_assign_wizard_user project.user.assign.wizard.user model_project_user_assign_wizard project.group_project_manager 1 0 0 0
24 project_user_project_reject_reason_wizard project.reject.reason.wizard.user model_project_reject_reason_wizard base.group_user 1 1 1 1
25 project_user_task_reject_reason_wizard task.reject.reason.wizard.user model_task_reject_reason_wizard base.group_user 1 1 1 1
26 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
27 access_project_stages_approval_flow_user access.project.stages.approval.flow.user model_project_stages_approval_flow base.group_user 1 1 0 0
28 project_internal_team_members_wizard internal.team.members.wizard.manager model_internal_team_members_wizard base.group_user 1 1 1 1
29 project_project_stage_update_wizard project.stage.update.wizard.manager model_project_stage_update_wizard base.group_user 1 1 1 1
30 access_project_project_supervisor project.project project.model_project_project project_task_timesheet_extended.group_project_supervisor 1 1 1 0
31 project_user_assign_wizard_admin access_project_project_stage_supervisor project.user.assign.wizard.admin project.project_stage.supervisor model_project_user_assign_wizard project.model_project_project_stage project.group_project_manager project_task_timesheet_extended.group_project_supervisor 1 1 1 1 0
32 project_user_assign_wizard_user access_project_task_type_supervisor project.user.assign.wizard.user project.task.type supervisor model_project_user_assign_wizard project.model_project_task_type project.group_project_manager project_task_timesheet_extended.group_project_supervisor 1 0 1 0 1 0 1
33 project_user_task_reject_reason_wizard access_project_tags_supervisor task.reject.reason.wizard.user project.project_tags_supervisor model_task_reject_reason_wizard project.model_project_tags base.group_user project_task_timesheet_extended.group_project_supervisor 1 1 1 1
34 project_internal_team_members_wizard access_project_task_time_lines_user internal.team.members.wizard.manager access_project_task_time_lines_user model_internal_team_members_wizard model_project_task_time_lines base.group_user 1 1 1 1
35 access_project_task_time_lines_manager access_project_task_time_lines_manager model_project_task_time_lines project.group_project_manager 1 1 1 1
36 project_project_stage_update_wizard access_user_task_availability project.stage.update.wizard.manager user.task.availability.access model_project_stage_update_wizard model_user_task_availability base.group_user 1 1 0 1 0 1 0
37
38
39
40
41
42
43
44

View File

@ -82,16 +82,4 @@
</field> </field>
</record> </record>
<record id="project_invoice_form_inherit" model="ir.ui.view">
<field name="name">project.invoice.inherit.form.view</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="hr_timesheet.project_invoice_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='allocated_hours']" position="after">
<field name="estimated_hours" widget="timesheet_uom_no_toggle" invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>
<field name="task_estimated_hours" widget="timesheet_uom_no_toggle" invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>
<field name="actual_hours" widget="timesheet_uom_no_toggle" invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>
</xpath>
</field>
</record>
</odoo> </odoo>

View File

@ -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 &amp; Proceed"
class="oe_highlight"
invisible="not assign_approval_flow or not show_approval_button"/>
<button type="object" name="action_open_reject_wizard"
string="Reject"
class="oe_highlight"
invisible="not assign_approval_flow or not show_refuse_button"/>
<button type="object" name="project_back_button"
string="Go Back"
class="oe_highlight"
invisible="not assign_approval_flow or not show_back_button"/>
</xpath>
<xpath expr="//form" position="inside">
<field name="showable_stage_ids" invisible="1"/>
<field name="assign_approval_flow" invisible="1"/>
<field name="manager_level_edit_access" invisible="1"/>
<field name="show_submission_button" invisible="1"/>
<field name="show_approval_button" invisible="1"/>
<field name="show_refuse_button" invisible="1"/>
<field name="show_back_button" invisible="1"/>
</xpath>
<xpath expr="//field[@name='stage_id']" position="attributes">
<attribute name="domain">[('id', 'in', showable_stage_ids)]</attribute>
</xpath>
<xpath expr="//page[@name='settings']" position="before">
<page name="project_stages" string="Project Stages" invisible="not assign_approval_flow">
<field name="project_stages" options="{'no_create': True, 'no_open': True, 'no_delete': True}">
<list editable="bottom" delete="0" create="0">
<field name="stage_id" readonly="1"/>
<field name="approval_by" readonly="not manager_level_edit_access"/>
<field name="assigned_to" readonly="not manager_level_edit_access"/>
<field name="assigned_date" readonly="1" optional="hide"/>
<field name="submission_date" readonly="1" optional="hide"/>
<field name="note" optional="show" readonly="not manager_level_edit_access"/>
<field name="project_id" invisible="1" column_invisible="1"/>
<field name="stage_approval_by" invisible="1" column_invisible="1"/>
<field name="approval_by_users" invisible="1" column_invisible="1"/>
<field name="manager_level_edit_access" invisible="1" column_invisible="1"/>
</list>
</field>
</page>
</xpath>
<xpath expr="//chatter" position="attributes">
<attribute name="invisible">not show_project_chatter</attribute>
</xpath>
<xpath expr="//field[@name='user_id']" position="before">
<field name="project_sponsor" widget="many2one_avatar_user"/>
</xpath>
<xpath expr="//header/field[@name='stage_id']" position="attributes">
<attribute name="readonly">assign_approval_flow</attribute>
</xpath>
<page name="description" position="attributes">
<attribute name="string">Initiation</attribute>
</page>
<xpath expr="//field[@name='description']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<page name="description" position="inside">
<group>
<field name="project_vision" placeholder="Eg: Build a mobile app that allows users to order groceries and track delivery in real time." readonly="not manager_level_edit_access"/>
</group>
<group string="Requirements Document">
<div class="o_row" style="align-items: flex-start;">
<!-- LEFT SIDE -->
<div class="o_col" style="width: 70%;">
<!-- HTML field (visible when NO file uploaded) -->
<field name="description"
widget="html" force_save="1" readonly="not manager_level_edit_access"
invisible="requirement_file" placeholder="The system should allow user login,
Users should be able to add items to a cart."/>
<!-- PDF Viewer (visible when file exists) -->
<field name="requirement_file"
widget="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 &amp; Deadlines)">
<group>
<group string="project Scope">
<field name="project_scope" nolabel="1"/>
</group>
<group string="Timelines">
<group>
<field name="date_start" string="Planned Date" widget="daterange"
options='{"end_date_field": "date", "always_range": "1"}'
required="date_start or date"/>
<field name="date" invisible="1" required="date_start"/>
</group>
<group></group>
<group>
<field name="allocated_hours" widget="timesheet_uom_no_toggle" optional="hide"
invisible="allocated_hours == 0 or not allow_timesheets"
groups="hr_timesheet.group_hr_timesheet_user"/>
<field name="effective_hours" widget="timesheet_uom_no_toggle" optional="hide"
invisible="effective_hours == 0 or not allow_timesheets"
groups="hr_timesheet.group_hr_timesheet_user"/>
<field name="remaining_hours" widget="timesheet_uom_no_toggle"
decoration-danger="remaining_hours &lt; 0"
decoration-warning="allocated_hours > 0 and (remaining_hours / allocated_hours) &lt; 0.2"
optional="hide"
invisible="allocated_hours == 0 or not allow_timesheets"
groups="hr_timesheet.group_hr_timesheet_user"
/>
<field name="estimated_hours" widget="timesheet_uom_no_toggle"
invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>
</group>
<group>
<field name="task_estimated_hours" widget="timesheet_uom_no_toggle"
invisible="not allow_timesheets" groups="hr_timesheet.group_hr_timesheet_user"/>
<field name="actual_hours" widget="timesheet_uom_no_toggle"
invisible="not allow_timesheets"
groups="hr_timesheet.group_hr_timesheet_user"/>
</group>
</group>
</group>
<group string="Risk Management Plan">
<field name="risk_ids" nolabel="1">
<list editable="bottom">
<field name="project_id" invisible="1" column_invisible="1"/>
<field name="risk_description" width="30%"/>
<field name="probability" width="20%"/>
<field name="impact" width="20%"/>
<field name="mitigation_plan" width="30%"/>
</list>
</field>
</group>
<group string="Budget Planning">
<group>
<field name="estimated_amount"/>
<field name="total_budget_amount" readonly="1"/>
</group>
<notebook>
<!-- MANPOWER TAB -->
<page string="Manpower">
<button name="action_fetch_resource_data"
type="object"
string="Fetch Resource Data"
class="oe_highlight"
icon="fa-refresh"/>
<field name="resource_cost_ids">
<list editable="bottom">
<field name="employee_id"/>
<field name="monthly_salary"/>
<field name="daily_rate"/>
<field name="start_date" optional="hide"/>
<field name="end_date" optional="hide"/>
<field name="duration_days" readonly="1"/>
<field name="total_cost"/>
</list>
</field>
</page>
<!-- MATERIAL TAB -->
<page string="Material">
<field name="material_cost_ids">
<list editable="bottom">
<field name="material_name" width="30%"/>
<field name="qty" width="20%"/>
<field name="unit_cost" width="20%"/>
<field name="total_cost" readonly="1" width="20%"/>
</list>
</field>
</page>
<!-- EQUIPMENT TAB -->
<page string="Equipments/Others">
<field name="equipment_cost_ids">
<list editable="bottom">
<field name="equipment_name" string="Equip/Others" width="30%"/>
<field name="duration_hours" width="20%"/>
<field name="hourly_rate" width="20%"/>
<field name="total_cost" readonly="1" width="20%"/>
</list>
</field>
</page>
</notebook>
</group>
</page>
</xpath>
<xpath expr="//sheet/notebook" position="inside">
<page name="architecture_design" string="Architecture &amp; Design">
<field name="architecture_design_ids">
<list>
<field name="tech_stack"/>
<field name="design_status"/>
<field name="architecture_notes" optional="hide"/>
<field name="database_notes" optional="hide"/>
<field name="reviewer_comments" optional="hide"/>
</list>
<form>
<sheet>
<group string="System Architecture">
<field name="architecture_diagram"/>
<field name="tech_stack"/>
<field name="architecture_notes"/>
</group>
<group>
<group string="UI / UX">
<field name="ui_wireframe"/>
<field name="ux_flow"/>
</group>
<group string="Database Design">
<field name="er_diagram"/>
<field name="database_notes"/>
</group>
</group>
<group string="Review">
<field name="design_status"/>
<field name="reviewer_comments"/>
</group>
</sheet>
</form>
</field>
</page>
<page name="project_sprints" string="Sprints" invisible="not require_sprint">
<field name="sprint_ids">
<list editable="bottom">
<field name="sprint_name" width="20%"/>
<field name="date_start" width="20%"/>
<field name="date_end" width="20%"/>
<field name="allocated_hours" width="20%"/>
<field name="status" width="20%"/>
<field name="done_date" optional="hide"/>
<field name="note" optional="hide"/>
</list>
</field>
</page>
<page name="development" string="Development Details">
<group string="Documents">
<field name="development_document_ids">
<list editable="bottom">
<field name="name"/>
<field name="file" filename="file_name"/>
<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>

View File

@ -20,3 +20,25 @@ class TaskRejectReasonWizard(models.TransientModel):
return { return {
"type": "ir.actions.act_window_close" "type": "ir.actions.act_window_close"
} }
class ProjectRejectReasonWizard(models.TransientModel):
_name = "project.reject.reason.wizard"
_description = "Project Rejection Reason Wizard"
reason = fields.Text(string="Rejection Reason", required=True)
project_id = fields.Many2one("project.project", string="Project", required=True)
def action_reject(self):
"""Trigger the rejection action on the selected task"""
self.ensure_one()
if not self.reason:
raise UserError(_("Please enter a reason for rejection."))
# Call the existing reject method on the task
self.project_id.reject_and_return(reason=self.reason)
return {
"type": "ir.actions.act_window_close"
}

View File

@ -22,4 +22,27 @@
<field name="view_mode">form</field> <field name="view_mode">form</field>
<field name="target">new</field> <field name="target">new</field>
</record> </record>
<record id="view_project_reject_reason_wizard" model="ir.ui.view">
<field name="name">project.reject.reason.wizard.form</field>
<field name="model">project.reject.reason.wizard</field>
<field name="arch" type="xml">
<form string="Reject Task">
<group>
<field name="reason" placeholder="Enter the reason for rejection..."/>
</group>
<footer>
<button string="Reject" type="object" name="action_reject" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_project_reject_reason_wizard" model="ir.actions.act_window">
<field name="name">Reject Task</field>
<field name="res_model">project.reject.reason.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo> </odoo>

View File

@ -85,18 +85,18 @@
</record> </record>
<!-- Default template user for new users signing in --> <!-- Default template user for new users signing in -->
<record id="template_portal_user_id" model="res.users"> <!-- <record id="template_portal_user_id" model="res.users">-->
<field name="name">Portal User Template</field> <!-- <field name="name">Portal User Template</field>-->
<field name="login">portaltemplate</field> <!-- <field name="login">portaltemplate</field>-->
<field name="active" eval="False"/> <!-- <field name="active" eval="False"/>-->
<field name="groups_id" eval="[Command.set([ref('base.group_portal')])]"/> <!-- <field name="groups_id" eval="[Command.set([ref('base.group_portal')])]"/>-->
<field name="signature" /> <!-- Needed for avoiding the _compute_signature triggering on each update --> <!-- <field name="signature" /> &lt;!&ndash; Needed for avoiding the _compute_signature triggering on each update &ndash;&gt;-->
</record> <!-- </record>-->
<record id="default_template_user_config" model="ir.config_parameter"> <!-- <record id="default_template_user_config" model="ir.config_parameter">-->
<field name="key">base.template_portal_user_id</field> <!-- <field name="key">base.template_portal_user_id</field>-->
<field name="value" ref="template_portal_user_id"/> <!-- <field name="value" ref="template_portal_user_id"/>-->
</record> <!-- </record>-->
</data> </data>
</odoo> </odoo>