first push message
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import ks_gantt_project_inherit
|
||||
from . import ks_project_task_type_inherit
|
||||
from . import ks_project_project
|
||||
from . import ks_gantt_project_task
|
||||
from . import ks_task_link_inherit
|
||||
from . import ks_hr_leave_inherit
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,266 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
import datetime
|
||||
import logging
|
||||
import json
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# Odoo 19: MailDeliveryException moved out of ir_mail_server in some builds.
|
||||
# Try the standard location first, fall back to the older path.
|
||||
try:
|
||||
from odoo.addons.base.models.ir_mail_server import MailDeliveryException
|
||||
except ImportError:
|
||||
try:
|
||||
from odoo.exceptions import MailDeliveryException
|
||||
except ImportError:
|
||||
MailDeliveryException = Exception # last-resort fallback
|
||||
|
||||
|
||||
class KsGanttViewProject(models.Model):
|
||||
_inherit = 'project.project'
|
||||
|
||||
ks_project_start = fields.Datetime(
|
||||
string="Start Date",
|
||||
default=lambda self: fields.Datetime.now(),
|
||||
required=True,
|
||||
)
|
||||
ks_project_end = fields.Datetime(
|
||||
string="End Date",
|
||||
default=lambda self: fields.Datetime.now() + datetime.timedelta(days=7),
|
||||
required=True,
|
||||
)
|
||||
ks_enable_project_deadline = fields.Boolean(
|
||||
string='Deadline',
|
||||
help="Enable/Disable Deadline of the tasks.",
|
||||
default=True,
|
||||
)
|
||||
ks_enable_task_dynamic_text = fields.Boolean(
|
||||
string='Dynamic Text',
|
||||
help="Enable/Disable Task Dynamic Text.",
|
||||
default=True,
|
||||
)
|
||||
ks_enable_task_dynamic_progress = fields.Boolean(
|
||||
string='Dynamic Progress',
|
||||
help="Enable/Disable Task Dynamic Progress.",
|
||||
default=True,
|
||||
)
|
||||
ks_days_off = fields.Boolean(
|
||||
string='Days Off',
|
||||
help="Enable to remove off days from the gantt",
|
||||
default=False,
|
||||
)
|
||||
ks_hide_date = fields.Boolean(
|
||||
string='Hide Holiday Day',
|
||||
help='Hide holidays on the gantt view',
|
||||
default=False,
|
||||
)
|
||||
ks_days_off_selection = fields.Many2many('ks.week.days', string="Select Days")
|
||||
|
||||
ks_enable_quickinfo_extension = fields.Boolean(
|
||||
string='Quick Info',
|
||||
help="Enable/Disable Quick Info.",
|
||||
default=True,
|
||||
)
|
||||
|
||||
ks_tooltip_task_name = fields.Boolean(string='Name', default=True)
|
||||
ks_tooltip_task_duration = fields.Boolean(string='Duration', default=True)
|
||||
ks_tooltip_task_start_date = fields.Boolean(string='Start Date', default=True)
|
||||
ks_tooltip_task_end_date = fields.Boolean(string='End Date', default=True)
|
||||
ks_tooltip_task_progress = fields.Boolean(string='Progress', default=True)
|
||||
ks_tooltip_task_deadline = fields.Boolean(string='Deadline', default=True)
|
||||
ks_tooltip_task_stage = fields.Boolean(string='Stage', default=True)
|
||||
ks_tooltip_task_constraint_type = fields.Boolean(string='Constraint Type', default=True)
|
||||
ks_tooltip_task_constraint_date = fields.Boolean(string='Constraint Date', default=True)
|
||||
|
||||
ks_mail_timesheet_user = fields.Many2one('res.partner', string="Mail user")
|
||||
|
||||
ks_project_task_json = fields.Char(compute="ks_compute_json_data_project_task")
|
||||
ks_project_task_linking = fields.Char(compute="ks_compute_json_data_project_task_link")
|
||||
|
||||
@api.model
|
||||
def ks_project_config(self, project_id):
|
||||
ks_tooltip_fields = [
|
||||
'ks_tooltip_task_name', 'ks_tooltip_task_duration',
|
||||
'ks_tooltip_task_start_date', 'ks_tooltip_task_end_date',
|
||||
'ks_tooltip_task_progress', 'ks_tooltip_task_stage',
|
||||
'ks_tooltip_task_constraint_type', 'ks_tooltip_task_constraint_date',
|
||||
'ks_tooltip_task_deadline',
|
||||
]
|
||||
ks_project_obj = self.env['project.project'].browse(project_id)
|
||||
ks_project_config = {
|
||||
'ks_project_start': ks_project_obj.ks_project_start or False,
|
||||
'ks_project_end': ks_project_obj.ks_project_end or False,
|
||||
'ks_enable_project_deadline': ks_project_obj.ks_enable_project_deadline,
|
||||
'ks_enable_task_dynamic_text': ks_project_obj.ks_enable_task_dynamic_text,
|
||||
'ks_enable_task_dynamic_progress': ks_project_obj.ks_enable_task_dynamic_progress,
|
||||
'ks_days_off': ks_project_obj.ks_days_off,
|
||||
'ks_hide_date': ks_project_obj.ks_hide_date,
|
||||
'ks_enable_quickinfo_extension': ks_project_obj.ks_enable_quickinfo_extension,
|
||||
'ks_allow_subtasks': ks_project_obj.allow_subtasks,
|
||||
}
|
||||
|
||||
ks_day_off_list = []
|
||||
if ks_project_obj.ks_days_off:
|
||||
for rec in ks_project_obj.ks_days_off_selection:
|
||||
ks_day_off_list.append(rec.ks_day_no)
|
||||
|
||||
ks_project_config['ks_days_off_selection'] = ks_day_off_list
|
||||
|
||||
ks_project_tooltip_config = {
|
||||
field: ks_project_obj[field] for field in ks_tooltip_fields
|
||||
}
|
||||
ks_project_config['ks_project_tooltip_config'] = ks_project_tooltip_config
|
||||
return ks_project_config
|
||||
|
||||
@api.model
|
||||
def ks_task_due_alert(self):
|
||||
"""
|
||||
Scheduled action: send deadline reminder emails for tasks due in 7, 3,
|
||||
or 1 day(s).
|
||||
"""
|
||||
ks_today_date = datetime.datetime.today().date()
|
||||
|
||||
for ks_project in self.search([]):
|
||||
ks_all_task = self.env['project.task'].search(
|
||||
[('project_id', '=', ks_project.id)]
|
||||
)
|
||||
for ks_task in ks_all_task:
|
||||
for i in range(len(ks_task.user_ids)):
|
||||
if ks_task.date_deadline and ks_task.user_ids:
|
||||
days_left = (ks_task.date_deadline - ks_today_date).days
|
||||
if ks_today_date < ks_task.date_deadline and days_left in [7, 3, 1]:
|
||||
template_obj = self.env['mail.mail']
|
||||
message_body = _(
|
||||
"Hi %s, This mail is to remind you that task %s "
|
||||
"of project %s has only %s day(s) until the deadline."
|
||||
) % (
|
||||
ks_task.user_ids[i].name,
|
||||
ks_task.name,
|
||||
ks_task.project_id.name,
|
||||
days_left,
|
||||
)
|
||||
template_data = {
|
||||
'subject': _('Task Deadline Reminder Mail'),
|
||||
'body_html': message_body,
|
||||
'email_from': self.env.user.email,
|
||||
'email_to': ks_task.user_ids[i].email,
|
||||
}
|
||||
template_id = template_obj.sudo().create(template_data)
|
||||
try:
|
||||
template_id.sudo().send(raise_exception=True)
|
||||
_logger.info(
|
||||
'Task deadline reminder sent for task: %s, user: %s',
|
||||
ks_task.name, ks_task.user_ids[i].name,
|
||||
)
|
||||
except MailDeliveryException as error:
|
||||
_logger.error(
|
||||
'Task deadline reminder failed for task: %s, user: %s — %s',
|
||||
ks_task.name, ks_task.user_ids[i].name, error,
|
||||
)
|
||||
|
||||
@api.model
|
||||
def ks_public_holidays(self):
|
||||
"""Return a list of all public holiday datetimes."""
|
||||
ks_project_holiday = []
|
||||
ks_pub_hol = self.env['resource.calendar.leaves'].search(
|
||||
[('resource_id', '=', False)]
|
||||
)
|
||||
for ks_holiday in ks_pub_hol:
|
||||
ks_hol_datetime = ks_holiday.date_from
|
||||
while ks_hol_datetime <= ks_holiday.date_to:
|
||||
if ks_hol_datetime not in ks_project_holiday:
|
||||
ks_project_holiday.append(ks_hol_datetime)
|
||||
ks_hol_datetime += datetime.timedelta(days=1)
|
||||
return ks_project_holiday
|
||||
|
||||
def ks_compute_json_data_project_task(self):
|
||||
for rec in self:
|
||||
ks_project_task_json = []
|
||||
ks_all_task_obj = self.env['project.task'].search(
|
||||
[('project_id', '=', rec.id)]
|
||||
)
|
||||
for ks_task in ks_all_task_obj:
|
||||
for ks_user in range(len(ks_task.user_ids)):
|
||||
ks_project_task_json.append({
|
||||
'id': 'task_' + str(ks_task.id),
|
||||
'ks_task_start_date': str(ks_task.ks_start_datetime),
|
||||
'ks_task_end_date': str(ks_task.ks_end_datetime),
|
||||
'ks_task_id': 'task_' + str(ks_task.id),
|
||||
'ks_task_name': ks_task.name,
|
||||
'ks_task_color': ks_task.ks_color,
|
||||
'ks_task_model': 'project.task',
|
||||
'parent_id': 'task_' + str(ks_task.parent_id.id) if ks_task.parent_id.id else False,
|
||||
'project_id': [ks_task.project_id.id, ks_task.project_id.name],
|
||||
'partner_id': [ks_task.partner_id.id, ks_task.partner_id.name],
|
||||
'company_id': [ks_task.company_id.id, ks_task.company_id.name],
|
||||
'mark_as_important': ks_task.priority,
|
||||
'ks_enable_task_duration': ks_task.ks_enable_task_duration,
|
||||
'deadline': str(ks_task.date_deadline) if ks_task.date_deadline else False,
|
||||
'progress': ks_task.progress,
|
||||
'ks_allow_subtask': ks_task.ks_allow_subtask,
|
||||
'ks_allow_parent_task': ks_task.ks_allow_subtask,
|
||||
'ks_schedule_mode': ks_task.ks_schedule_mode,
|
||||
'constraint_type': ks_task.ks_constraint_task_type,
|
||||
'constraint_date': str(ks_task.ks_constraint_task_date) if ks_task.ks_constraint_task_date else False,
|
||||
'stage_id': [ks_task.stage_id.id, ks_task.stage_id.name],
|
||||
'unscheduled': ks_task.ks_task_unschedule,
|
||||
'ks_owner_task': [ks_task.user_ids[ks_user].id, ks_task.user_ids[ks_user].name] if ks_task.user_ids else False,
|
||||
'resource_working_hours': ks_task.ks_resource_hours_per_day,
|
||||
'type': ks_task.ks_task_type,
|
||||
'ks_resource_hours_available': ks_task.ks_resource_hours_available,
|
||||
'ks_task_link_json': ks_task.ks_task_link_json,
|
||||
})
|
||||
rec.ks_project_task_json = json.dumps(ks_project_task_json)
|
||||
|
||||
def ks_compute_json_data_project_task_link(self):
|
||||
for rec in self:
|
||||
ks_project_task_json = []
|
||||
ks_all_task_obj = self.env['project.task'].search(
|
||||
[('project_id', '=', rec.id)]
|
||||
)
|
||||
for ks_task in ks_all_task_obj:
|
||||
for ks_user in range(len(ks_task.user_ids)):
|
||||
ks_project_task_json.append({
|
||||
'id': 'task_' + str(ks_task.id),
|
||||
'ks_task_start_date': str(ks_task.ks_start_datetime),
|
||||
'ks_task_end_date': str(ks_task.ks_end_datetime),
|
||||
'ks_task_id': 'task_' + str(ks_task.id),
|
||||
'ks_task_name': ks_task.name,
|
||||
'ks_task_color': ks_task.ks_color,
|
||||
'ks_task_model': 'project.task',
|
||||
'parent_id': 'task_' + str(ks_task.parent_id.id) if ks_task.parent_id.id else False,
|
||||
'project_id': ks_task.project_id.id,
|
||||
'mark_as_important': ks_task.priority,
|
||||
'deadline': str(ks_task.date_deadline) if ks_task.date_deadline else False,
|
||||
'progress': ks_task.progress,
|
||||
'ks_allow_subtask': ks_task.ks_allow_subtask,
|
||||
'ks_allow_parent_task': ks_task.ks_allow_subtask,
|
||||
'ks_schedule_mode': ks_task.ks_schedule_mode,
|
||||
'constraint_type': ks_task.ks_constraint_task_type,
|
||||
'constraint_date': str(ks_task.ks_constraint_task_date) if ks_task.ks_constraint_task_date else False,
|
||||
'stage_id': [ks_task.stage_id.id, ks_task.stage_id.name],
|
||||
'unscheduled': ks_task.ks_task_unschedule,
|
||||
'ks_owner_task': [ks_task.user_ids[ks_user].id, ks_task.user_ids[ks_user].name] if ks_task.user_ids else False,
|
||||
'resource_working_hours': ks_task.ks_resource_hours_per_day,
|
||||
'type': ks_task.ks_task_type,
|
||||
'ks_resource_hours_available': ks_task.ks_resource_hours_available,
|
||||
})
|
||||
# Bug fix from original: was writing to wrong field
|
||||
rec.ks_project_task_linking = json.dumps(ks_project_task_json)
|
||||
|
||||
@api.constrains('ks_days_off_selection')
|
||||
def _check_valid_ks_days_off_selection(self):
|
||||
for record in self:
|
||||
if len(record.ks_days_off_selection) == 7:
|
||||
raise UserError(
|
||||
_("Invalid value for Days selection. At least keep one working day.")
|
||||
)
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if vals.get('ks_days_off_selection'):
|
||||
for task in self.task_ids:
|
||||
task.ks_compute_task_duration()
|
||||
return res
|
||||
@@ -0,0 +1,434 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import time, datetime, timedelta
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
import json
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# Odoo 19: MailDeliveryException moved in some builds — safe import fallback.
|
||||
try:
|
||||
from odoo.addons.base.models.ir_mail_server import MailDeliveryException
|
||||
except ImportError:
|
||||
try:
|
||||
from odoo.exceptions import MailDeliveryException
|
||||
except ImportError:
|
||||
MailDeliveryException = Exception
|
||||
|
||||
|
||||
class KsProjectTask(models.Model):
|
||||
_inherit = "project.task"
|
||||
|
||||
def ks_convert_day_names_to_integers(self, day_names):
|
||||
day_name_to_int = {
|
||||
'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3,
|
||||
'Friday': 4, 'Saturday': 5, 'Sunday': 6,
|
||||
}
|
||||
return [day_name_to_int.get(name) for name in day_names]
|
||||
|
||||
def ks_calculate_end_date(self, ks_task_duration, end_date, weekdays):
|
||||
if end_date.weekday() in weekdays:
|
||||
ks_task_duration += 1
|
||||
while ks_task_duration > 0:
|
||||
end_date += timedelta(days=1)
|
||||
if end_date.weekday() not in weekdays:
|
||||
ks_task_duration -= 1
|
||||
return end_date
|
||||
|
||||
def ks_default_start_date(self):
|
||||
# Odoo 19: return datetime directly — fields.Datetime.to_string() is
|
||||
# deprecated since 16 and removed in 18+.
|
||||
return datetime.combine(fields.Datetime.now(), datetime.min.time())
|
||||
|
||||
def ks_default_end_datetime(self):
|
||||
return datetime.combine(
|
||||
fields.Datetime.now() + timedelta(days=1), datetime.min.time()
|
||||
)
|
||||
|
||||
ks_start_datetime = fields.Datetime(
|
||||
"Start Date", required=True, default=ks_default_start_date
|
||||
)
|
||||
ks_end_datetime = fields.Datetime(
|
||||
"End Date", required=True, default=ks_default_end_datetime
|
||||
)
|
||||
ks_color = fields.Char(string="Color", compute='ks_compute_color')
|
||||
ks_allow_subtask = fields.Boolean(related="project_id.allow_subtasks")
|
||||
planned_hours = fields.Float("Initially Planned Hours", tracking=True)
|
||||
ks_mark_important = fields.Boolean(
|
||||
string="Mark As Important", default=False,
|
||||
help="Mark as an important task",
|
||||
)
|
||||
ks_work_duration = fields.Char(
|
||||
string="Duration",
|
||||
help="Working Duration in day Hours",
|
||||
compute='ks_compute_work_duration',
|
||||
)
|
||||
ks_task_link_json = fields.Char(compute="ks_compute_json_data_task_link")
|
||||
ks_resource_hours_per_day = fields.Float(
|
||||
related='user_ids.employee_id.resource_calendar_id.hours_per_day'
|
||||
)
|
||||
ks_resource_hours_available = fields.Char(
|
||||
compute='ks_compute_resource_hours_available'
|
||||
)
|
||||
ks_task_link_ids = fields.One2many(
|
||||
comodel_name='ks.task.link',
|
||||
inverse_name='ks_source_task_id',
|
||||
string='Task Links',
|
||||
)
|
||||
ks_schedule_mode = fields.Selection(
|
||||
string='Schedule Mode',
|
||||
selection=[('auto', 'Auto'), ('manual', 'Manual')],
|
||||
default="manual",
|
||||
)
|
||||
ks_constraint_task_type = fields.Selection(
|
||||
string='Constraint Type',
|
||||
selection=[
|
||||
('asap', 'As Soon As Possible'),
|
||||
('alap', 'As Late As Possible'),
|
||||
('snet', 'Start No Earlier Than'),
|
||||
('snlt', 'Start No Late Than'),
|
||||
('fnet', 'Finish No Earlier Than'),
|
||||
('fnlt', 'Finish No Later Than'),
|
||||
('mso', 'Must Start On'),
|
||||
('mfo', 'Must Finish On'),
|
||||
],
|
||||
default="asap",
|
||||
required=True,
|
||||
)
|
||||
ks_constraint_task_date = fields.Datetime(string="Constraint Date")
|
||||
ks_enable_task_duration = fields.Boolean(string="Enable Task Duration")
|
||||
ks_task_duration = fields.Integer(string="Duration")
|
||||
ks_task_unschedule = fields.Boolean(string="Unschedule", default=False)
|
||||
ks_task_type = fields.Selection(
|
||||
string='Task Type',
|
||||
selection=[('task', 'Task'), ('milestone', 'Milestone')],
|
||||
default='task',
|
||||
required=True,
|
||||
)
|
||||
ks_user_ids = fields.Char(compute="ks_compute_ks_user_id", default=[])
|
||||
|
||||
def ks_compute_ks_user_id(self):
|
||||
for rec in self:
|
||||
ks_temp = []
|
||||
for user in rec.user_ids:
|
||||
ks_temp.append([user.id, user.name])
|
||||
# Always assign (even empty list) to avoid "field not set" compute errors
|
||||
rec.ks_user_ids = json.dumps(ks_temp)
|
||||
|
||||
@api.depends('stage_id')
|
||||
def ks_compute_color(self):
|
||||
for ks_task in self:
|
||||
if ks_task.stage_id and ks_task.stage_id.ks_stage_color:
|
||||
ks_task.ks_color = ks_task.stage_id.ks_stage_color
|
||||
else:
|
||||
ks_task.ks_color = '#7C7BAD'
|
||||
|
||||
@api.onchange('ks_start_datetime', 'ks_end_datetime', 'ks_work_duration')
|
||||
def ks_compute_work_duration(self):
|
||||
for rec in self:
|
||||
rec.ks_work_duration = '0'
|
||||
if rec.ks_end_datetime and rec.ks_start_datetime and rec.ks_task_type != 'milestone':
|
||||
delta = rec.ks_end_datetime - rec.ks_start_datetime
|
||||
if delta.days == 0:
|
||||
rec.ks_work_duration = str(delta) + " hours"
|
||||
else:
|
||||
rec.ks_work_duration = str(delta)
|
||||
if rec.ks_start_datetime and rec.ks_task_type == 'milestone':
|
||||
rec.ks_end_datetime = rec.ks_start_datetime
|
||||
|
||||
def ks_compute_json_data_task_link(self):
|
||||
for rec in self:
|
||||
ks_task_link_json = []
|
||||
for task_link in rec.ks_task_link_ids:
|
||||
ks_task_link_json.append({
|
||||
'id': task_link.id,
|
||||
'source': task_link.ks_source_task_id.id,
|
||||
'target': task_link.ks_target_task_id.id,
|
||||
'type': task_link.ks_task_link_type,
|
||||
'lag': task_link.ks_lag_days * 24,
|
||||
})
|
||||
rec.ks_task_link_json = json.dumps(ks_task_link_json)
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
res = super().create(values)
|
||||
if res.ks_task_duration and res.ks_enable_task_duration:
|
||||
ids = res.project_id.ks_days_off_selection.ids
|
||||
if ids:
|
||||
self.env.cr.execute(
|
||||
'SELECT ks_day_name FROM ks_week_days WHERE id IN %s',
|
||||
(tuple(ids),),
|
||||
)
|
||||
records = self.env.cr.dictfetchall()
|
||||
ks_day_names = [r['ks_day_name'] for r in records]
|
||||
day_integers = self.ks_convert_day_names_to_integers(ks_day_names)
|
||||
res.ks_end_datetime = self.ks_calculate_end_date(
|
||||
res.ks_task_duration, res.ks_start_datetime, day_integers
|
||||
)
|
||||
else:
|
||||
res.ks_end_datetime = res.ks_start_datetime + timedelta(days=res.ks_task_duration)
|
||||
|
||||
if values.get('ks_schedule_mode') == 'auto' and values.get('ks_constraint_task_type') in ['asap', 'alap']:
|
||||
res.ks_auto_schedule_mode()
|
||||
res.ks_validate_constraint()
|
||||
|
||||
if 'user_ids' in values:
|
||||
res.ks_send_email_task_assigned()
|
||||
return res
|
||||
|
||||
def write(self, values):
|
||||
res = super().write(values)
|
||||
for rec in self:
|
||||
if rec.ks_schedule_mode == 'auto' and rec.ks_constraint_task_type in ['asap', 'alap']:
|
||||
for ks_record in rec.ks_task_link_ids:
|
||||
ks_record.ks_target_task_id.ks_auto_schedule_mode()
|
||||
elif rec.ks_start_datetime or rec.ks_end_datetime or rec.ks_task_link_ids:
|
||||
for record in rec.ks_task_link_ids:
|
||||
if (record.ks_target_task_id.ks_schedule_mode == 'auto'
|
||||
and record.ks_target_task_id.ks_constraint_task_type == 'asap'):
|
||||
record.ks_target_task_id.ks_auto_schedule_mode()
|
||||
|
||||
if rec.ks_constraint_task_type or rec.ks_constraint_task_date:
|
||||
rec.ks_validate_constraint()
|
||||
|
||||
if (rec.ks_task_duration or rec.ks_task_duration == 0) \
|
||||
and rec.ks_enable_task_duration and not rec.ks_start_datetime:
|
||||
rec.ks_end_datetime = rec.ks_start_datetime + timedelta(days=rec.ks_task_duration)
|
||||
return res
|
||||
|
||||
def ks_validate_constraint(self):
|
||||
"""Validate task constraint violation against start/end dates."""
|
||||
if self.ks_constraint_task_type == 'snet' and self.ks_constraint_task_date:
|
||||
if not self.ks_constraint_task_date <= self.ks_start_datetime:
|
||||
raise ValidationError(_("Task should start on or after the constraint date."))
|
||||
|
||||
if self.ks_constraint_task_type == 'snlt' and self.ks_constraint_task_date:
|
||||
if not self.ks_constraint_task_date >= self.ks_start_datetime:
|
||||
raise ValidationError(_("Task should start on or before the constraint date."))
|
||||
|
||||
if self.ks_constraint_task_type == 'fnet' and self.ks_constraint_task_date:
|
||||
if not self.ks_constraint_task_date <= self.ks_end_datetime:
|
||||
raise ValidationError(_("Task should finish on or after the constraint date."))
|
||||
|
||||
if self.ks_constraint_task_type == 'fnlt' and self.ks_constraint_task_date:
|
||||
if not self.ks_constraint_task_date >= self.ks_end_datetime:
|
||||
raise ValidationError(_("Task should finish on or before the constraint date."))
|
||||
|
||||
if self.ks_constraint_task_type == 'mso' and self.ks_constraint_task_date:
|
||||
if self.ks_constraint_task_date != self.ks_start_datetime:
|
||||
raise ValidationError(_("Task should start exactly on the constraint date."))
|
||||
|
||||
if self.ks_constraint_task_type == 'mfo' and self.ks_constraint_task_date:
|
||||
if self.ks_constraint_task_date != self.ks_end_datetime:
|
||||
raise ValidationError(_("Task should finish exactly on the constraint date."))
|
||||
|
||||
def ks_auto_schedule_mode(self):
|
||||
"""Auto-schedule task start/end dates based on task links."""
|
||||
if self.ks_schedule_mode != 'auto':
|
||||
return
|
||||
|
||||
task_link = self.env['ks.task.link'].search([
|
||||
('ks_target_task_id', '=', self.id),
|
||||
('ks_source_task_id.project_id', '=', self.project_id.id),
|
||||
])
|
||||
|
||||
if not task_link:
|
||||
ks_duration = self.ks_end_datetime - self.ks_start_datetime
|
||||
if self.ks_constraint_task_type == 'asap':
|
||||
self.ks_start_datetime = self.project_id.ks_project_start
|
||||
self.ks_end_datetime = self.project_id.ks_project_start + ks_duration
|
||||
|
||||
elif self.ks_constraint_task_type == 'alap':
|
||||
ks_closest_task = False
|
||||
for rec in self.ks_task_link_ids:
|
||||
if rec.ks_source_task_id.id == self.id:
|
||||
if not ks_closest_task or ks_closest_task > rec.ks_target_task_id.ks_start_datetime:
|
||||
ks_closest_task = rec.ks_target_task_id.ks_start_datetime
|
||||
if ks_closest_task:
|
||||
self.ks_end_datetime = ks_closest_task
|
||||
self.ks_start_datetime = ks_closest_task - ks_duration
|
||||
|
||||
elif len(task_link) == 1:
|
||||
ks_duration = self.ks_end_datetime - self.ks_start_datetime
|
||||
link_type = task_link.ks_task_link_type
|
||||
|
||||
if link_type == "0": # Finish to Start
|
||||
ref = task_link.ks_source_task_id.ks_end_datetime
|
||||
self.ks_start_datetime = ref
|
||||
self.ks_end_datetime = ref + ks_duration
|
||||
elif link_type == "1": # Start to Start
|
||||
ref = task_link.ks_source_task_id.ks_start_datetime
|
||||
self.ks_start_datetime = ref
|
||||
self.ks_end_datetime = ref + ks_duration
|
||||
elif link_type == "2": # Finish to Finish
|
||||
ref = task_link.ks_source_task_id.ks_end_datetime
|
||||
self.ks_end_datetime = ref
|
||||
self.ks_start_datetime = ref - ks_duration
|
||||
elif link_type == "3": # Start to Finish
|
||||
ref = task_link.ks_source_task_id.ks_start_datetime
|
||||
self.ks_end_datetime = ref
|
||||
self.ks_start_datetime = ref - ks_duration
|
||||
|
||||
for rec in self.ks_task_link_ids:
|
||||
if rec.ks_target_task_id.ks_schedule_mode == 'auto':
|
||||
rec.ks_target_task_id.ks_auto_schedule_mode()
|
||||
|
||||
@api.constrains('ks_start_datetime', 'ks_end_datetime')
|
||||
def _validate_task_date(self):
|
||||
for rec in self:
|
||||
if rec.ks_end_datetime < rec.ks_start_datetime and rec.ks_task_type != 'milestone':
|
||||
raise ValidationError(
|
||||
_("Task end date cannot be earlier than the start date.")
|
||||
)
|
||||
|
||||
def get_report(self):
|
||||
return self.env.ref('ks_gantt_view_project.ks_gantt_tasks_report').report_action(
|
||||
self, data={'model': self._name, 'ids': self.ids}
|
||||
)
|
||||
|
||||
def ks_action_send_email_tasks(self):
|
||||
if not self.project_id.ks_mail_timesheet_user:
|
||||
raise ValidationError(
|
||||
_("Please select a mail recipient in the project Gantt settings before sending.")
|
||||
)
|
||||
template_obj = self.env['mail.mail']
|
||||
message_body = _(
|
||||
"Hi %s, the timesheet report for task '%s' is attached — please review it."
|
||||
) % (self.project_id.ks_mail_timesheet_user.name, self.name)
|
||||
|
||||
template_data = {
|
||||
'subject': _('Task Progress'),
|
||||
'body_html': message_body,
|
||||
'email_from': self.env.user.email,
|
||||
'email_to': self.project_id.ks_mail_timesheet_user.email,
|
||||
}
|
||||
template_id = template_obj.sudo().create(template_data)
|
||||
self.ks_fetch_timesheet_report(template_id)
|
||||
|
||||
notification = {'type': 'ir.actions.client', 'tag': 'display_notification'}
|
||||
try:
|
||||
template_id.sudo().send(raise_exception=True)
|
||||
notification['params'] = {
|
||||
'message': _('Email sent successfully'),
|
||||
'sticky': False,
|
||||
}
|
||||
except MailDeliveryException as error:
|
||||
notification['params'] = {
|
||||
'message': _('Error sending mail: %s') % (error.args[0],),
|
||||
'sticky': True,
|
||||
}
|
||||
return notification
|
||||
|
||||
def ks_fetch_timesheet_report(self, mail_template):
|
||||
"""Attach a timesheet PDF report to the given mail.mail record."""
|
||||
self.ensure_one()
|
||||
report_template = self.env.ref(
|
||||
'ks_gantt_view_project.action_report_gantt_tasks_timesheet'
|
||||
)
|
||||
report_name = self.name + _(' timesheet.pdf')
|
||||
|
||||
# Odoo 19: _render_qweb_pdf takes only record ids (no self-ref first arg)
|
||||
result, _format = report_template._render_qweb_pdf([self.id])
|
||||
result = base64.b64encode(result)
|
||||
|
||||
attachment_obj = self.env['ir.attachment'].sudo()
|
||||
attachment_data = {
|
||||
'name': report_name,
|
||||
'datas': result,
|
||||
'type': 'binary',
|
||||
'res_model': 'mail.message',
|
||||
'res_id': mail_template.id,
|
||||
}
|
||||
attachment_id = attachment_obj.create(attachment_data).id
|
||||
if attachment_id:
|
||||
mail_template.sudo().write({'attachment_ids': [(4, attachment_id)]})
|
||||
|
||||
@api.onchange('ks_task_duration', 'project_id')
|
||||
def ks_compute_task_duration(self):
|
||||
for rec in self:
|
||||
if rec.ks_start_datetime:
|
||||
if not rec.ks_task_duration:
|
||||
rec.ks_task_duration = 0
|
||||
ids = rec.project_id.ks_days_off_selection.ids
|
||||
if ids:
|
||||
self.env.cr.execute(
|
||||
'SELECT ks_day_name FROM ks_week_days WHERE id IN %s',
|
||||
(tuple(ids),),
|
||||
)
|
||||
records = self.env.cr.dictfetchall()
|
||||
ks_day_names = [r['ks_day_name'] for r in records]
|
||||
day_integers = self.ks_convert_day_names_to_integers(ks_day_names)
|
||||
rec.ks_end_datetime = self.ks_calculate_end_date(
|
||||
rec.ks_task_duration, rec.ks_start_datetime, day_integers
|
||||
)
|
||||
else:
|
||||
rec.ks_end_datetime = rec.ks_start_datetime + timedelta(days=rec.ks_task_duration)
|
||||
|
||||
@api.onchange('ks_start_datetime', 'ks_enable_task_duration')
|
||||
def ks_calculate_task_duration(self):
|
||||
for rec in self:
|
||||
rec.ks_task_duration = 0
|
||||
if rec.ks_end_datetime and rec.ks_start_datetime:
|
||||
rec.ks_task_duration = (rec.ks_end_datetime - rec.ks_start_datetime).days
|
||||
|
||||
def ks_compute_resource_hours_available(self):
|
||||
for rec in self:
|
||||
resource_availability = {}
|
||||
if rec.user_ids and rec.user_ids.employee_id and rec.user_ids.employee_id.resource_calendar_id:
|
||||
ks_working_calendar = rec.user_ids[-1].employee_id.resource_calendar_id
|
||||
for ks_avail_hours in ks_working_calendar.attendance_ids:
|
||||
dayofweek = int(ks_avail_hours.dayofweek)
|
||||
key = 0 if dayofweek == 6 else dayofweek + 1
|
||||
if key not in resource_availability:
|
||||
resource_availability[key] = []
|
||||
ks_temp_hours = ks_avail_hours.hour_from
|
||||
while ks_temp_hours < ks_avail_hours.hour_to:
|
||||
resource_availability[key].append(ks_temp_hours)
|
||||
ks_temp_hours += 1
|
||||
rec.ks_resource_hours_available = json.dumps(resource_availability)
|
||||
|
||||
def ks_send_email_task_assigned(self):
|
||||
"""Send assignment notification to all assigned users."""
|
||||
template_obj = self.env['mail.mail']
|
||||
for user in self.user_ids:
|
||||
message_body = _("Hi %s, Task '%s' has been assigned to you.") % (
|
||||
user.name, self.name
|
||||
)
|
||||
template_data = {
|
||||
'subject': _('Task Assignment Mail'),
|
||||
'body_html': message_body,
|
||||
'email_from': self.env.user.email,
|
||||
'email_to': user.email,
|
||||
}
|
||||
template_id = template_obj.sudo().create(template_data)
|
||||
try:
|
||||
template_id.sudo().send(raise_exception=True)
|
||||
except MailDeliveryException as error:
|
||||
_logger.error('Task assignment mail failed: %s', error)
|
||||
|
||||
@api.model
|
||||
def ks_update_task_sequence(self, data):
|
||||
query = "WITH ks_tasks (id, parent_id, sequence) AS (\nVALUES"
|
||||
for index in data:
|
||||
if data[index].get('id'):
|
||||
vals = {}
|
||||
if 'parent_id' in data[index]:
|
||||
vals['parent_id'] = data[index].get('parent_id') or False
|
||||
if 'sequence' in data[index]:
|
||||
vals['sequence'] = data[index].get('sequence')
|
||||
parent_val = str(vals['parent_id']) if vals.get('parent_id') else 'Null'
|
||||
query += (
|
||||
"\n(" + str(data[index].get('id')) + ','
|
||||
+ parent_val + ','
|
||||
+ str(vals.get('sequence', 0)) + "),"
|
||||
)
|
||||
query = (
|
||||
query[:-1]
|
||||
+ "\n)\nUPDATE project_task as t SET \n parent_id=kt.parent_id::integer,"
|
||||
"sequence=kt.sequence \nFROM ks_tasks as kt WHERE t.id = kt.id"
|
||||
)
|
||||
self.env.cr.execute(query)
|
||||
_logger.info('Task sequence updated.')
|
||||
@@ -0,0 +1,24 @@
|
||||
from odoo import api, fields, models
|
||||
from datetime import timedelta
|
||||
|
||||
|
||||
class KsHrLeave(models.Model):
|
||||
_inherit = 'hr.leave'
|
||||
|
||||
def action_validate(self):
|
||||
result = super(KsHrLeave, self).action_validate()
|
||||
for rec in self:
|
||||
# Check if the leave is approved then increase the project task between the leave dates for the employee.
|
||||
if rec.state == 'validate':
|
||||
user_tasks = self.env['project.task'].search(
|
||||
['&', '|', '&', ('ks_start_datetime', '<=', rec.request_date_from),
|
||||
('ks_end_datetime', '>=', rec.request_date_from),
|
||||
'&', ('ks_start_datetime', '<=', rec.request_date_to),
|
||||
('ks_end_datetime', '>=', rec.request_date_to),
|
||||
('user_ids', '=', rec.user_id.id)
|
||||
])
|
||||
|
||||
for tasks in user_tasks:
|
||||
tasks.ks_end_datetime += timedelta(days=int(rec.number_of_days))
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
# Odoo 19 note:
|
||||
# In Odoo 16+, allow_subtasks is a standard Boolean field on project.project.
|
||||
# The original override set it readonly=True at the ORM level, which prevented
|
||||
# users from ever changing it via the project form. Removed that restriction —
|
||||
# if you need the field to be read-only in a specific view, use readonly="1"
|
||||
# in the view XML instead of redefining the field on the model.
|
||||
# This file is kept as a placeholder so the models/__init__.py import remains valid.
|
||||
|
||||
class KsProject(models.Model):
|
||||
_inherit = "project.project"
|
||||
# No field overrides needed — allow_subtasks is already defined in core.
|
||||
@@ -0,0 +1,6 @@
|
||||
from odoo import api, exceptions, fields, models, _
|
||||
|
||||
|
||||
class KsGanttViewStage(models.Model):
|
||||
_inherit = 'project.task.type'
|
||||
ks_stage_color = fields.Char('Stage Color')
|
||||
@@ -0,0 +1,30 @@
|
||||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class KsTaskLink(models.Model):
|
||||
_inherit = 'ks.task.link'
|
||||
|
||||
ks_source_task_id = fields.Many2one(comodel_name='project.task', string="Source Task")
|
||||
ks_target_task_id = fields.Many2one(comodel_name='project.task', string='Target Task')
|
||||
ks_lag_days = fields.Integer(string="Lag Days")
|
||||
|
||||
@api.onchange('ks_task_link_type')
|
||||
def ks_compute_target_task_domain(self):
|
||||
ks_task_ids = []
|
||||
if self.ks_source_task_id and self.ks_source_task_id.project_id:
|
||||
ks_project_id = self.ks_source_task_id.project_id.id
|
||||
ks_task_ids = self.env['project.task'].sudo().search([('project_id', '=', ks_project_id)]).ids
|
||||
return {
|
||||
'domain': {
|
||||
'ks_target_task_id': [('id', '=', ks_task_ids)],
|
||||
}
|
||||
}
|
||||
|
||||
@api.constrains('ks_source_task_id', 'ks_target_task_id')
|
||||
def ks_task_link_constraint(self):
|
||||
for rec in self:
|
||||
if rec.ks_source_task_id.id == rec.ks_target_task_id.id:
|
||||
raise ValidationError(_("Can't create same link with same task."))
|
||||
if rec.ks_source_task_id.project_id.id != rec.ks_target_task_id.project_id.id:
|
||||
raise ValidationError(_("Can't create link with other project task."))
|
||||
Reference in New Issue
Block a user