first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
+7
View File
@@ -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
@@ -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."))