1# -*- coding: utf-8 -*- 2# Part of Odoo. See LICENSE file for full copyright and licensing details. 3 4import ast 5from datetime import timedelta, datetime 6from random import randint 7 8from odoo import api, fields, models, tools, SUPERUSER_ID, _ 9from odoo.exceptions import UserError, AccessError, ValidationError, RedirectWarning 10from odoo.tools.misc import format_date, get_lang 11from odoo.osv.expression import OR 12 13from .project_task_recurrence import DAYS, WEEKS 14 15class ProjectTaskType(models.Model): 16 _name = 'project.task.type' 17 _description = 'Task Stage' 18 _order = 'sequence, id' 19 20 def _get_default_project_ids(self): 21 default_project_id = self.env.context.get('default_project_id') 22 return [default_project_id] if default_project_id else None 23 24 active = fields.Boolean('Active', default=True) 25 name = fields.Char(string='Stage Name', required=True, translate=True) 26 description = fields.Text(translate=True) 27 sequence = fields.Integer(default=1) 28 project_ids = fields.Many2many('project.project', 'project_task_type_rel', 'type_id', 'project_id', string='Projects', 29 default=_get_default_project_ids) 30 legend_blocked = fields.Char( 31 'Red Kanban Label', default=lambda s: _('Blocked'), translate=True, required=True, 32 help='Override the default value displayed for the blocked state for kanban selection, when the task or issue is in that stage.') 33 legend_done = fields.Char( 34 'Green Kanban Label', default=lambda s: _('Ready'), translate=True, required=True, 35 help='Override the default value displayed for the done state for kanban selection, when the task or issue is in that stage.') 36 legend_normal = fields.Char( 37 'Grey Kanban Label', default=lambda s: _('In Progress'), translate=True, required=True, 38 help='Override the default value displayed for the normal state for kanban selection, when the task or issue is in that stage.') 39 mail_template_id = fields.Many2one( 40 'mail.template', 41 string='Email Template', 42 domain=[('model', '=', 'project.task')], 43 help="If set an email will be sent to the customer when the task or issue reaches this step.") 44 fold = fields.Boolean(string='Folded in Kanban', 45 help='This stage is folded in the kanban view when there are no records in that stage to display.') 46 rating_template_id = fields.Many2one( 47 'mail.template', 48 string='Rating Email Template', 49 domain=[('model', '=', 'project.task')], 50 help="If set and if the project's rating configuration is 'Rating when changing stage', then an email will be sent to the customer when the task reaches this step.") 51 auto_validation_kanban_state = fields.Boolean('Automatic kanban status', default=False, 52 help="Automatically modify the kanban state when the customer replies to the feedback for this stage.\n" 53 " * A good feedback from the customer will update the kanban state to 'ready for the new stage' (green bullet).\n" 54 " * A medium or a bad feedback will set the kanban state to 'blocked' (red bullet).\n") 55 is_closed = fields.Boolean('Closing Stage', help="Tasks in this stage are considered as closed.") 56 disabled_rating_warning = fields.Text(compute='_compute_disabled_rating_warning') 57 58 def unlink_wizard(self, stage_view=False): 59 self = self.with_context(active_test=False) 60 # retrieves all the projects with a least 1 task in that stage 61 # a task can be in a stage even if the project is not assigned to the stage 62 readgroup = self.with_context(active_test=False).env['project.task'].read_group([('stage_id', 'in', self.ids)], ['project_id'], ['project_id']) 63 project_ids = list(set([project['project_id'][0] for project in readgroup] + self.project_ids.ids)) 64 65 wizard = self.with_context(project_ids=project_ids).env['project.task.type.delete.wizard'].create({ 66 'project_ids': project_ids, 67 'stage_ids': self.ids 68 }) 69 70 context = dict(self.env.context) 71 context['stage_view'] = stage_view 72 return { 73 'name': _('Delete Stage'), 74 'view_mode': 'form', 75 'res_model': 'project.task.type.delete.wizard', 76 'views': [(self.env.ref('project.view_project_task_type_delete_wizard').id, 'form')], 77 'type': 'ir.actions.act_window', 78 'res_id': wizard.id, 79 'target': 'new', 80 'context': context, 81 } 82 83 def write(self, vals): 84 if 'active' in vals and not vals['active']: 85 self.env['project.task'].search([('stage_id', 'in', self.ids)]).write({'active': False}) 86 return super(ProjectTaskType, self).write(vals) 87 88 @api.depends('project_ids', 'project_ids.rating_active') 89 def _compute_disabled_rating_warning(self): 90 for stage in self: 91 disabled_projects = stage.project_ids.filtered(lambda p: not p.rating_active) 92 if disabled_projects: 93 stage.disabled_rating_warning = '\n'.join('- %s' % p.name for p in disabled_projects) 94 else: 95 stage.disabled_rating_warning = False 96 97 98class Project(models.Model): 99 _name = "project.project" 100 _description = "Project" 101 _inherit = ['portal.mixin', 'mail.alias.mixin', 'mail.thread', 'rating.parent.mixin'] 102 _order = "sequence, name, id" 103 _rating_satisfaction_days = False # takes all existing ratings 104 _check_company_auto = True 105 106 def _compute_attached_docs_count(self): 107 Attachment = self.env['ir.attachment'] 108 for project in self: 109 project.doc_count = Attachment.search_count([ 110 '|', 111 '&', 112 ('res_model', '=', 'project.project'), ('res_id', '=', project.id), 113 '&', 114 ('res_model', '=', 'project.task'), ('res_id', 'in', project.task_ids.ids) 115 ]) 116 117 def _compute_task_count(self): 118 task_data = self.env['project.task'].read_group([('project_id', 'in', self.ids), '|', '&', ('stage_id.is_closed', '=', False), ('stage_id.fold', '=', False), ('stage_id', '=', False)], ['project_id'], ['project_id']) 119 result = dict((data['project_id'][0], data['project_id_count']) for data in task_data) 120 for project in self: 121 project.task_count = result.get(project.id, 0) 122 123 def attachment_tree_view(self): 124 action = self.env['ir.actions.act_window']._for_xml_id('base.action_attachment') 125 action['domain'] = str([ 126 '|', 127 '&', 128 ('res_model', '=', 'project.project'), 129 ('res_id', 'in', self.ids), 130 '&', 131 ('res_model', '=', 'project.task'), 132 ('res_id', 'in', self.task_ids.ids) 133 ]) 134 action['context'] = "{'default_res_model': '%s','default_res_id': %d}" % (self._name, self.id) 135 return action 136 137 def _compute_is_favorite(self): 138 for project in self: 139 project.is_favorite = self.env.user in project.favorite_user_ids 140 141 def _inverse_is_favorite(self): 142 favorite_projects = not_fav_projects = self.env['project.project'].sudo() 143 for project in self: 144 if self.env.user in project.favorite_user_ids: 145 favorite_projects |= project 146 else: 147 not_fav_projects |= project 148 149 # Project User has no write access for project. 150 not_fav_projects.write({'favorite_user_ids': [(4, self.env.uid)]}) 151 favorite_projects.write({'favorite_user_ids': [(3, self.env.uid)]}) 152 153 def _get_default_favorite_user_ids(self): 154 return [(6, 0, [self.env.uid])] 155 156 name = fields.Char("Name", index=True, required=True, tracking=True) 157 description = fields.Html() 158 active = fields.Boolean(default=True, 159 help="If the active field is set to False, it will allow you to hide the project without removing it.") 160 sequence = fields.Integer(default=10, help="Gives the sequence order when displaying a list of Projects.") 161 partner_id = fields.Many2one('res.partner', string='Customer', auto_join=True, tracking=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") 162 partner_email = fields.Char( 163 compute='_compute_partner_email', inverse='_inverse_partner_email', 164 string='Email', readonly=False, store=True, copy=False) 165 partner_phone = fields.Char( 166 compute='_compute_partner_phone', inverse='_inverse_partner_phone', 167 string="Phone", readonly=False, store=True, copy=False) 168 company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company) 169 currency_id = fields.Many2one('res.currency', related="company_id.currency_id", string="Currency", readonly=True) 170 analytic_account_id = fields.Many2one('account.analytic.account', string="Analytic Account", copy=False, ondelete='set null', 171 domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", check_company=True, 172 help="Analytic account to which this project is linked for financial management. " 173 "Use an analytic account to record cost and revenue on your project.") 174 175 favorite_user_ids = fields.Many2many( 176 'res.users', 'project_favorite_user_rel', 'project_id', 'user_id', 177 default=_get_default_favorite_user_ids, 178 string='Members') 179 is_favorite = fields.Boolean(compute='_compute_is_favorite', inverse='_inverse_is_favorite', string='Show Project on dashboard', 180 help="Whether this project should be displayed on your dashboard.") 181 label_tasks = fields.Char(string='Use Tasks as', default='Tasks', help="Label used for the tasks of the project.", translate=True) 182 tasks = fields.One2many('project.task', 'project_id', string="Task Activities") 183 resource_calendar_id = fields.Many2one( 184 'resource.calendar', string='Working Time', 185 related='company_id.resource_calendar_id') 186 type_ids = fields.Many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', string='Tasks Stages') 187 task_count = fields.Integer(compute='_compute_task_count', string="Task Count") 188 task_ids = fields.One2many('project.task', 'project_id', string='Tasks', 189 domain=['|', ('stage_id.fold', '=', False), ('stage_id', '=', False)]) 190 color = fields.Integer(string='Color Index') 191 user_id = fields.Many2one('res.users', string='Project Manager', default=lambda self: self.env.user, tracking=True) 192 alias_enabled = fields.Boolean(string='Use email alias', compute='_compute_alias_enabled', readonly=False) 193 alias_id = fields.Many2one('mail.alias', string='Alias', ondelete="restrict", required=True, 194 help="Internal email associated with this project. Incoming emails are automatically synchronized " 195 "with Tasks (or optionally Issues if the Issue Tracker module is installed).") 196 privacy_visibility = fields.Selection([ 197 ('followers', 'Invited internal users'), 198 ('employees', 'All internal users'), 199 ('portal', 'Invited portal users and all internal users'), 200 ], 201 string='Visibility', required=True, 202 default='portal', 203 help="Defines the visibility of the tasks of the project:\n" 204 "- Invited internal users: employees may only see the followed project and tasks.\n" 205 "- All internal users: employees may see all project and tasks.\n" 206 "- Invited portal and all internal users: employees may see everything." 207 " Portal users may see project and tasks followed by\n" 208 " them or by someone of their company.") 209 210 allowed_user_ids = fields.Many2many('res.users', compute='_compute_allowed_users', inverse='_inverse_allowed_user') 211 allowed_internal_user_ids = fields.Many2many('res.users', 'project_allowed_internal_users_rel', 212 string="Allowed Internal Users", default=lambda self: self.env.user, domain=[('share', '=', False)]) 213 allowed_portal_user_ids = fields.Many2many('res.users', 'project_allowed_portal_users_rel', string="Allowed Portal Users", domain=[('share', '=', True)]) 214 doc_count = fields.Integer(compute='_compute_attached_docs_count', string="Number of documents attached") 215 date_start = fields.Date(string='Start Date') 216 date = fields.Date(string='Expiration Date', index=True, tracking=True) 217 subtask_project_id = fields.Many2one('project.project', string='Sub-task Project', ondelete="restrict", 218 help="Project in which sub-tasks of the current project will be created. It can be the current project itself.") 219 allow_subtasks = fields.Boolean('Sub-tasks', default=lambda self: self.env.user.has_group('project.group_subtask_project')) 220 allow_recurring_tasks = fields.Boolean('Recurring Tasks', default=lambda self: self.env.user.has_group('project.group_project_recurring_tasks')) 221 222 # rating fields 223 rating_request_deadline = fields.Datetime(compute='_compute_rating_request_deadline', store=True) 224 rating_active = fields.Boolean('Customer Ratings', default=lambda self: self.env.user.has_group('project.group_project_rating')) 225 rating_status = fields.Selection( 226 [('stage', 'Rating when changing stage'), 227 ('periodic', 'Periodical Rating') 228 ], 'Customer Ratings Status', default="stage", required=True, 229 help="How to get customer feedback?\n" 230 "- Rating when changing stage: an email will be sent when a task is pulled in another stage.\n" 231 "- Periodical Rating: email will be sent periodically.\n\n" 232 "Don't forget to set up the mail templates on the stages for which you want to get the customer's feedbacks.") 233 rating_status_period = fields.Selection([ 234 ('daily', 'Daily'), 235 ('weekly', 'Weekly'), 236 ('bimonthly', 'Twice a Month'), 237 ('monthly', 'Once a Month'), 238 ('quarterly', 'Quarterly'), 239 ('yearly', 'Yearly')], 'Rating Frequency', required=True, default='monthly') 240 241 _sql_constraints = [ 242 ('project_date_greater', 'check(date >= date_start)', 'Error! project start-date must be lower than project end-date.') 243 ] 244 245 @api.depends('partner_id.email') 246 def _compute_partner_email(self): 247 for project in self: 248 if project.partner_id and project.partner_id.email != project.partner_email: 249 project.partner_email = project.partner_id.email 250 251 def _inverse_partner_email(self): 252 for project in self: 253 if project.partner_id and project.partner_email != project.partner_id.email: 254 project.partner_id.email = project.partner_email 255 256 @api.depends('partner_id.phone') 257 def _compute_partner_phone(self): 258 for project in self: 259 if project.partner_id and project.partner_phone != project.partner_id.phone: 260 project.partner_phone = project.partner_id.phone 261 262 def _inverse_partner_phone(self): 263 for project in self: 264 if project.partner_id and project.partner_phone != project.partner_id.phone: 265 project.partner_id.phone = project.partner_phone 266 267 @api.onchange('alias_enabled') 268 def _onchange_alias_name(self): 269 if not self.alias_enabled: 270 self.alias_name = False 271 272 def _compute_alias_enabled(self): 273 for project in self: 274 project.alias_enabled = project.alias_domain and project.alias_id.alias_name 275 276 @api.depends('allowed_internal_user_ids', 'allowed_portal_user_ids') 277 def _compute_allowed_users(self): 278 for project in self: 279 users = project.allowed_internal_user_ids | project.allowed_portal_user_ids 280 project.allowed_user_ids = users 281 282 def _inverse_allowed_user(self): 283 for project in self: 284 allowed_users = project.allowed_user_ids 285 project.allowed_portal_user_ids = allowed_users.filtered('share') 286 project.allowed_internal_user_ids = allowed_users - project.allowed_portal_user_ids 287 288 def _compute_access_url(self): 289 super(Project, self)._compute_access_url() 290 for project in self: 291 project.access_url = '/my/project/%s' % project.id 292 293 def _compute_access_warning(self): 294 super(Project, self)._compute_access_warning() 295 for project in self.filtered(lambda x: x.privacy_visibility != 'portal'): 296 project.access_warning = _( 297 "The project cannot be shared with the recipient(s) because the privacy of the project is too restricted. Set the privacy to 'Visible by following customers' in order to make it accessible by the recipient(s).") 298 299 @api.depends('rating_status', 'rating_status_period') 300 def _compute_rating_request_deadline(self): 301 periods = {'daily': 1, 'weekly': 7, 'bimonthly': 15, 'monthly': 30, 'quarterly': 90, 'yearly': 365} 302 for project in self: 303 project.rating_request_deadline = fields.datetime.now() + timedelta(days=periods.get(project.rating_status_period, 0)) 304 305 @api.model 306 def _map_tasks_default_valeus(self, task, project): 307 """ get the default value for the copied task on project duplication """ 308 return { 309 'stage_id': task.stage_id.id, 310 'name': task.name, 311 'company_id': project.company_id.id, 312 } 313 314 def map_tasks(self, new_project_id): 315 """ copy and map tasks from old to new project """ 316 project = self.browse(new_project_id) 317 tasks = self.env['project.task'] 318 # We want to copy archived task, but do not propagate an active_test context key 319 task_ids = self.env['project.task'].with_context(active_test=False).search([('project_id', '=', self.id)], order='parent_id').ids 320 old_to_new_tasks = {} 321 for task in self.env['project.task'].browse(task_ids): 322 # preserve task name and stage, normally altered during copy 323 defaults = self._map_tasks_default_valeus(task, project) 324 if task.parent_id: 325 # set the parent to the duplicated task 326 defaults['parent_id'] = old_to_new_tasks.get(task.parent_id.id, False) 327 new_task = task.copy(defaults) 328 old_to_new_tasks[task.id] = new_task.id 329 tasks += new_task 330 331 return project.write({'tasks': [(6, 0, tasks.ids)]}) 332 333 @api.returns('self', lambda value: value.id) 334 def copy(self, default=None): 335 if default is None: 336 default = {} 337 if not default.get('name'): 338 default['name'] = _("%s (copy)") % (self.name) 339 project = super(Project, self).copy(default) 340 if self.subtask_project_id == self: 341 project.subtask_project_id = project 342 for follower in self.message_follower_ids: 343 project.message_subscribe(partner_ids=follower.partner_id.ids, subtype_ids=follower.subtype_ids.ids) 344 if 'tasks' not in default: 345 self.map_tasks(project.id) 346 return project 347 348 @api.model 349 def create(self, vals): 350 # Prevent double project creation 351 self = self.with_context(mail_create_nosubscribe=True) 352 project = super(Project, self).create(vals) 353 if not vals.get('subtask_project_id'): 354 project.subtask_project_id = project.id 355 if project.privacy_visibility == 'portal' and project.partner_id.user_ids: 356 project.allowed_user_ids |= project.partner_id.user_ids 357 return project 358 359 def write(self, vals): 360 allowed_users_changed = 'allowed_portal_user_ids' in vals or 'allowed_internal_user_ids' in vals 361 if allowed_users_changed: 362 allowed_users = {project: project.allowed_user_ids for project in self} 363 # directly compute is_favorite to dodge allow write access right 364 if 'is_favorite' in vals: 365 vals.pop('is_favorite') 366 self._fields['is_favorite'].determine_inverse(self) 367 res = super(Project, self).write(vals) if vals else True 368 369 if allowed_users_changed: 370 for project in self: 371 permission_removed = allowed_users.get(project) - project.allowed_user_ids 372 allowed_portal_users_removed = permission_removed.filtered('share') 373 project.message_unsubscribe(allowed_portal_users_removed.partner_id.commercial_partner_id.ids) 374 for task in project.task_ids: 375 task.allowed_user_ids -= permission_removed 376 377 if 'allow_recurring_tasks' in vals and not vals.get('allow_recurring_tasks'): 378 self.env['project.task'].search([('project_id', 'in', self.ids), ('recurring_task', '=', True)]).write({'recurring_task': False}) 379 380 if 'active' in vals: 381 # archiving/unarchiving a project does it on its tasks, too 382 self.with_context(active_test=False).mapped('tasks').write({'active': vals['active']}) 383 if vals.get('partner_id') or vals.get('privacy_visibility'): 384 for project in self.filtered(lambda project: project.privacy_visibility == 'portal'): 385 project.allowed_user_ids |= project.partner_id.user_ids 386 387 return res 388 389 def action_unlink(self): 390 wizard = self.env['project.delete.wizard'].create({ 391 'project_ids': self.ids 392 }) 393 394 return { 395 'name': _('Confirmation'), 396 'view_mode': 'form', 397 'res_model': 'project.delete.wizard', 398 'views': [(self.env.ref('project.project_delete_wizard_form').id, 'form')], 399 'type': 'ir.actions.act_window', 400 'res_id': wizard.id, 401 'target': 'new', 402 'context': self.env.context, 403 } 404 405 def unlink(self): 406 # Check project is empty 407 for project in self.with_context(active_test=False): 408 if project.tasks: 409 raise UserError(_('You cannot delete a project containing tasks. You can either archive it or first delete all of its tasks.')) 410 # Delete the empty related analytic account 411 analytic_accounts_to_delete = self.env['account.analytic.account'] 412 for project in self: 413 if project.analytic_account_id and not project.analytic_account_id.line_ids: 414 analytic_accounts_to_delete |= project.analytic_account_id 415 result = super(Project, self).unlink() 416 analytic_accounts_to_delete.unlink() 417 return result 418 419 def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None): 420 """ 421 Subscribe to all existing active tasks when subscribing to a project 422 And add the portal user subscribed to allowed portal users 423 """ 424 res = super(Project, self).message_subscribe(partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids) 425 project_subtypes = self.env['mail.message.subtype'].browse(subtype_ids) if subtype_ids else None 426 task_subtypes = (project_subtypes.mapped('parent_id') | project_subtypes.filtered(lambda sub: sub.internal or sub.default)).ids if project_subtypes else None 427 if not subtype_ids or task_subtypes: 428 self.mapped('tasks').message_subscribe( 429 partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=task_subtypes) 430 if partner_ids: 431 all_users = self.env['res.partner'].browse(partner_ids).user_ids 432 portal_users = all_users.filtered('share') 433 internal_users = all_users - portal_users 434 self.allowed_portal_user_ids |= portal_users 435 self.allowed_internal_user_ids |= internal_users 436 return res 437 438 def message_unsubscribe(self, partner_ids=None, channel_ids=None): 439 """ Unsubscribe from all tasks when unsubscribing from a project """ 440 self.mapped('tasks').message_unsubscribe(partner_ids=partner_ids, channel_ids=channel_ids) 441 return super(Project, self).message_unsubscribe(partner_ids=partner_ids, channel_ids=channel_ids) 442 443 def _alias_get_creation_values(self): 444 values = super(Project, self)._alias_get_creation_values() 445 values['alias_model_id'] = self.env['ir.model']._get('project.task').id 446 if self.id: 447 values['alias_defaults'] = defaults = ast.literal_eval(self.alias_defaults or "{}") 448 defaults['project_id'] = self.id 449 return values 450 451 # --------------------------------------------------- 452 # Actions 453 # --------------------------------------------------- 454 455 def toggle_favorite(self): 456 favorite_projects = not_fav_projects = self.env['project.project'].sudo() 457 for project in self: 458 if self.env.user in project.favorite_user_ids: 459 favorite_projects |= project 460 else: 461 not_fav_projects |= project 462 463 # Project User has no write access for project. 464 not_fav_projects.write({'favorite_user_ids': [(4, self.env.uid)]}) 465 favorite_projects.write({'favorite_user_ids': [(3, self.env.uid)]}) 466 467 def action_view_tasks(self): 468 action = self.with_context(active_id=self.id, active_ids=self.ids) \ 469 .env.ref('project.act_project_project_2_project_task_all') \ 470 .sudo().read()[0] 471 action['display_name'] = self.name 472 return action 473 474 def action_view_account_analytic_line(self): 475 """ return the action to see all the analytic lines of the project's analytic account """ 476 action = self.env["ir.actions.actions"]._for_xml_id("analytic.account_analytic_line_action") 477 action['context'] = {'default_account_id': self.analytic_account_id.id} 478 action['domain'] = [('account_id', '=', self.analytic_account_id.id)] 479 return action 480 481 def action_view_all_rating(self): 482 """ return the action to see all the rating of the project and activate default filters""" 483 action = self.env['ir.actions.act_window']._for_xml_id('project.rating_rating_action_view_project_rating') 484 action['name'] = _('Ratings of %s') % (self.name,) 485 action_context = ast.literal_eval(action['context']) if action['context'] else {} 486 action_context.update(self._context) 487 action_context['search_default_parent_res_name'] = self.name 488 action_context.pop('group_by', None) 489 return dict(action, context=action_context) 490 491 # --------------------------------------------------- 492 # Business Methods 493 # --------------------------------------------------- 494 495 @api.model 496 def _create_analytic_account_from_values(self, values): 497 analytic_account = self.env['account.analytic.account'].create({ 498 'name': values.get('name', _('Unknown Analytic Account')), 499 'company_id': values.get('company_id') or self.env.company.id, 500 'partner_id': values.get('partner_id'), 501 'active': True, 502 }) 503 return analytic_account 504 505 def _create_analytic_account(self): 506 for project in self: 507 analytic_account = self.env['account.analytic.account'].create({ 508 'name': project.name, 509 'company_id': project.company_id.id, 510 'partner_id': project.partner_id.id, 511 'active': True, 512 }) 513 project.write({'analytic_account_id': analytic_account.id}) 514 515 # --------------------------------------------------- 516 # Rating business 517 # --------------------------------------------------- 518 519 # This method should be called once a day by the scheduler 520 @api.model 521 def _send_rating_all(self): 522 projects = self.search([ 523 ('rating_active', '=', True), 524 ('rating_status', '=', 'periodic'), 525 ('rating_request_deadline', '<=', fields.Datetime.now()) 526 ]) 527 for project in projects: 528 project.task_ids._send_task_rating_mail() 529 project._compute_rating_request_deadline() 530 self.env.cr.commit() 531 532 533class Task(models.Model): 534 _name = "project.task" 535 _description = "Task" 536 _date_name = "date_assign" 537 _inherit = ['portal.mixin', 'mail.thread.cc', 'mail.activity.mixin', 'rating.mixin'] 538 _mail_post_access = 'read' 539 _order = "priority desc, sequence, id desc" 540 _check_company_auto = True 541 542 def _get_default_stage_id(self): 543 """ Gives default stage_id """ 544 project_id = self.env.context.get('default_project_id') 545 if not project_id: 546 return False 547 return self.stage_find(project_id, [('fold', '=', False), ('is_closed', '=', False)]) 548 549 @api.model 550 def _default_company_id(self): 551 if self._context.get('default_project_id'): 552 return self.env['project.project'].browse(self._context['default_project_id']).company_id 553 return self.env.company 554 555 @api.model 556 def _read_group_stage_ids(self, stages, domain, order): 557 search_domain = [('id', 'in', stages.ids)] 558 if 'default_project_id' in self.env.context: 559 search_domain = ['|', ('project_ids', '=', self.env.context['default_project_id'])] + search_domain 560 561 stage_ids = stages._search(search_domain, order=order, access_rights_uid=SUPERUSER_ID) 562 return stages.browse(stage_ids) 563 564 active = fields.Boolean(default=True) 565 name = fields.Char(string='Title', tracking=True, required=True, index=True) 566 description = fields.Html(string='Description') 567 priority = fields.Selection([ 568 ('0', 'Normal'), 569 ('1', 'Important'), 570 ], default='0', index=True, string="Priority") 571 sequence = fields.Integer(string='Sequence', index=True, default=10, 572 help="Gives the sequence order when displaying a list of tasks.") 573 stage_id = fields.Many2one('project.task.type', string='Stage', compute='_compute_stage_id', 574 store=True, readonly=False, ondelete='restrict', tracking=True, index=True, 575 default=_get_default_stage_id, group_expand='_read_group_stage_ids', 576 domain="[('project_ids', '=', project_id)]", copy=False) 577 tag_ids = fields.Many2many('project.tags', string='Tags') 578 kanban_state = fields.Selection([ 579 ('normal', 'In Progress'), 580 ('done', 'Ready'), 581 ('blocked', 'Blocked')], string='Kanban State', 582 copy=False, default='normal', required=True) 583 kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Kanban State Label', tracking=True) 584 create_date = fields.Datetime("Created On", readonly=True, index=True) 585 write_date = fields.Datetime("Last Updated On", readonly=True, index=True) 586 date_end = fields.Datetime(string='Ending Date', index=True, copy=False) 587 date_assign = fields.Datetime(string='Assigning Date', index=True, copy=False, readonly=True) 588 date_deadline = fields.Date(string='Deadline', index=True, copy=False, tracking=True) 589 date_last_stage_update = fields.Datetime(string='Last Stage Update', 590 index=True, 591 copy=False, 592 readonly=True) 593 project_id = fields.Many2one('project.project', string='Project', 594 compute='_compute_project_id', store=True, readonly=False, 595 index=True, tracking=True, check_company=True, change_default=True) 596 planned_hours = fields.Float("Initially Planned Hours", help='Time planned to achieve this task (including its sub-tasks).', tracking=True) 597 subtask_planned_hours = fields.Float("Sub-tasks Planned Hours", compute='_compute_subtask_planned_hours', help="Sum of the time planned of all the sub-tasks linked to this task. Usually less or equal to the initially time planned of this task.") 598 user_id = fields.Many2one('res.users', 599 string='Assigned to', 600 default=lambda self: self.env.uid, 601 index=True, tracking=True) 602 partner_id = fields.Many2one('res.partner', 603 string='Customer', 604 compute='_compute_partner_id', store=True, readonly=False, 605 domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") 606 partner_is_company = fields.Boolean(related='partner_id.is_company', readonly=True) 607 commercial_partner_id = fields.Many2one(related='partner_id.commercial_partner_id') 608 partner_email = fields.Char( 609 compute='_compute_partner_email', inverse='_inverse_partner_email', 610 string='Email', readonly=False, store=True, copy=False) 611 partner_phone = fields.Char( 612 compute='_compute_partner_phone', inverse='_inverse_partner_phone', 613 string="Phone", readonly=False, store=True, copy=False) 614 ribbon_message = fields.Char('Ribbon message', compute='_compute_ribbon_message') 615 partner_city = fields.Char(related='partner_id.city', readonly=False) 616 manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id', readonly=True) 617 company_id = fields.Many2one( 618 'res.company', string='Company', compute='_compute_company_id', store=True, readonly=False, 619 required=True, copy=True, default=_default_company_id) 620 color = fields.Integer(string='Color Index') 621 user_email = fields.Char(related='user_id.email', string='User Email', readonly=True, related_sudo=False) 622 attachment_ids = fields.One2many('ir.attachment', compute='_compute_attachment_ids', string="Main Attachments", 623 help="Attachment that don't come from message.") 624 # In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id 625 displayed_image_id = fields.Many2one('ir.attachment', domain="[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]", string='Cover Image') 626 legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, related_sudo=False) 627 legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False) 628 legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False) 629 is_closed = fields.Boolean(related="stage_id.is_closed", string="Closing Stage", readonly=True, related_sudo=False) 630 parent_id = fields.Many2one('project.task', string='Parent Task', index=True) 631 child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks", context={'active_test': False}) 632 subtask_project_id = fields.Many2one('project.project', related="project_id.subtask_project_id", string='Sub-task Project', readonly=True) 633 allow_subtasks = fields.Boolean(string="Allow Sub-tasks", related="project_id.allow_subtasks", readonly=True) 634 subtask_count = fields.Integer("Sub-task count", compute='_compute_subtask_count') 635 email_from = fields.Char(string='Email From', help="These people will receive email.", index=True, 636 compute='_compute_email_from', store="True", readonly=False) 637 allowed_user_ids = fields.Many2many('res.users', string="Visible to", groups='project.group_project_manager', compute='_compute_allowed_user_ids', store=True, readonly=False, copy=False) 638 project_privacy_visibility = fields.Selection(related='project_id.privacy_visibility', string="Project Visibility") 639 # Computed field about working time elapsed between record creation and assignation/closing. 640 working_hours_open = fields.Float(compute='_compute_elapsed', string='Working hours to assign', store=True, group_operator="avg") 641 working_hours_close = fields.Float(compute='_compute_elapsed', string='Working hours to close', store=True, group_operator="avg") 642 working_days_open = fields.Float(compute='_compute_elapsed', string='Working days to assign', store=True, group_operator="avg") 643 working_days_close = fields.Float(compute='_compute_elapsed', string='Working days to close', store=True, group_operator="avg") 644 # customer portal: include comment and incoming emails in communication history 645 website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])]) 646 647 # recurrence fields 648 allow_recurring_tasks = fields.Boolean(related='project_id.allow_recurring_tasks') 649 recurring_task = fields.Boolean(string="Recurrent") 650 recurring_count = fields.Integer(string="Tasks in Recurrence", compute='_compute_recurring_count') 651 recurrence_id = fields.Many2one('project.task.recurrence', copy=False) 652 recurrence_update = fields.Selection([ 653 ('this', 'This task'), 654 ('subsequent', 'This and following tasks'), 655 ('all', 'All tasks'), 656 ], default='this', store=False) 657 recurrence_message = fields.Char(string='Next Recurrencies', compute='_compute_recurrence_message') 658 659 repeat_interval = fields.Integer(string='Repeat Every', default=1, compute='_compute_repeat', readonly=False) 660 repeat_unit = fields.Selection([ 661 ('day', 'Days'), 662 ('week', 'Weeks'), 663 ('month', 'Months'), 664 ('year', 'Years'), 665 ], default='week', compute='_compute_repeat', readonly=False) 666 repeat_type = fields.Selection([ 667 ('forever', 'Forever'), 668 ('until', 'End Date'), 669 ('after', 'Number of Repetitions'), 670 ], default="forever", string="Until", compute='_compute_repeat', readonly=False) 671 repeat_until = fields.Date(string="End Date", compute='_compute_repeat', readonly=False) 672 repeat_number = fields.Integer(string="Repetitions", default=1, compute='_compute_repeat', readonly=False) 673 674 repeat_on_month = fields.Selection([ 675 ('date', 'Date of the Month'), 676 ('day', 'Day of the Month'), 677 ], default='date', compute='_compute_repeat', readonly=False) 678 679 repeat_on_year = fields.Selection([ 680 ('date', 'Date of the Year'), 681 ('day', 'Day of the Year'), 682 ], default='date', compute='_compute_repeat', readonly=False) 683 684 mon = fields.Boolean(string="Mon", compute='_compute_repeat', readonly=False) 685 tue = fields.Boolean(string="Tue", compute='_compute_repeat', readonly=False) 686 wed = fields.Boolean(string="Wed", compute='_compute_repeat', readonly=False) 687 thu = fields.Boolean(string="Thu", compute='_compute_repeat', readonly=False) 688 fri = fields.Boolean(string="Fri", compute='_compute_repeat', readonly=False) 689 sat = fields.Boolean(string="Sat", compute='_compute_repeat', readonly=False) 690 sun = fields.Boolean(string="Sun", compute='_compute_repeat', readonly=False) 691 692 repeat_day = fields.Selection([ 693 (str(i), str(i)) for i in range(1, 32) 694 ], compute='_compute_repeat', readonly=False) 695 repeat_week = fields.Selection([ 696 ('first', 'First'), 697 ('second', 'Second'), 698 ('third', 'Third'), 699 ('last', 'Last'), 700 ], default='first', compute='_compute_repeat', readonly=False) 701 repeat_weekday = fields.Selection([ 702 ('mon', 'Monday'), 703 ('tue', 'Tuesday'), 704 ('wed', 'Wednesday'), 705 ('thu', 'Thursday'), 706 ('fri', 'Friday'), 707 ('sat', 'Saturday'), 708 ('sun', 'Sunday'), 709 ], string='Day Of The Week', compute='_compute_repeat', readonly=False) 710 repeat_month = fields.Selection([ 711 ('january', 'January'), 712 ('february', 'February'), 713 ('march', 'March'), 714 ('april', 'April'), 715 ('may', 'May'), 716 ('june', 'June'), 717 ('july', 'July'), 718 ('august', 'August'), 719 ('september', 'September'), 720 ('october', 'October'), 721 ('november', 'November'), 722 ('december', 'December'), 723 ], compute='_compute_repeat', readonly=False) 724 725 repeat_show_dow = fields.Boolean(compute='_compute_repeat_visibility') 726 repeat_show_day = fields.Boolean(compute='_compute_repeat_visibility') 727 repeat_show_week = fields.Boolean(compute='_compute_repeat_visibility') 728 repeat_show_month = fields.Boolean(compute='_compute_repeat_visibility') 729 730 @api.model 731 def _get_recurrence_fields(self): 732 return ['repeat_interval', 'repeat_unit', 'repeat_type', 'repeat_until', 'repeat_number', 733 'repeat_on_month', 'repeat_on_year', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 734 'sun', 'repeat_day', 'repeat_week', 'repeat_month', 'repeat_weekday'] 735 736 @api.depends('recurring_task', 'repeat_unit', 'repeat_on_month', 'repeat_on_year') 737 def _compute_repeat_visibility(self): 738 for task in self: 739 task.repeat_show_day = task.recurring_task and (task.repeat_unit == 'month' and task.repeat_on_month == 'date') or (task.repeat_unit == 'year' and task.repeat_on_year == 'date') 740 task.repeat_show_week = task.recurring_task and (task.repeat_unit == 'month' and task.repeat_on_month == 'day') or (task.repeat_unit == 'year' and task.repeat_on_year == 'day') 741 task.repeat_show_dow = task.recurring_task and task.repeat_unit == 'week' 742 task.repeat_show_month = task.recurring_task and task.repeat_unit == 'year' 743 744 @api.depends('recurring_task') 745 def _compute_repeat(self): 746 rec_fields = self._get_recurrence_fields() 747 defaults = self.default_get(rec_fields) 748 for task in self: 749 for f in rec_fields: 750 if task.recurrence_id: 751 task[f] = task.recurrence_id[f] 752 else: 753 if task.recurring_task: 754 task[f] = defaults.get(f) 755 else: 756 task[f] = False 757 758 def _get_weekdays(self, n=1): 759 self.ensure_one() 760 if self.repeat_unit == 'week': 761 return [fn(n) for day, fn in DAYS.items() if self[day]] 762 return [DAYS.get(self.repeat_weekday)(n)] 763 764 @api.depends( 765 'recurring_task', 'repeat_interval', 'repeat_unit', 'repeat_type', 'repeat_until', 766 'repeat_number', 'repeat_on_month', 'repeat_on_year', 'mon', 'tue', 'wed', 'thu', 'fri', 767 'sat', 'sun', 'repeat_day', 'repeat_week', 'repeat_month', 'repeat_weekday') 768 def _compute_recurrence_message(self): 769 self.recurrence_message = False 770 for task in self.filtered(lambda t: t.recurring_task and t._is_recurrence_valid()): 771 date = fields.Date.today() 772 number_occurrences = min(5, task.repeat_number if task.repeat_type == 'after' else 5) 773 delta = task.repeat_interval if task.repeat_unit == 'day' else 1 774 recurring_dates = self.env['project.task.recurrence']._get_next_recurring_dates( 775 date + timedelta(days=delta), 776 task.repeat_interval, 777 task.repeat_unit, 778 task.repeat_type, 779 task.repeat_until, 780 task.repeat_on_month, 781 task.repeat_on_year, 782 task._get_weekdays(WEEKS.get(task.repeat_week)), 783 task.repeat_day, 784 task.repeat_week, 785 task.repeat_month, 786 count=number_occurrences) 787 date_format = self.env['res.lang']._lang_get(self.env.user.lang).date_format 788 task.recurrence_message = '<ul>' 789 for date in recurring_dates[:5]: 790 task.recurrence_message += '<li>%s</li>' % date.strftime(date_format) 791 if task.repeat_type == 'after' and task.repeat_number > 5 or task.repeat_type == 'forever' or len(recurring_dates) > 5: 792 task.recurrence_message += '<li>...</li>' 793 task.recurrence_message += '</ul>' 794 if task.repeat_type == 'until': 795 task.recurrence_message += _('<p><em>Number of tasks: %(tasks_count)s</em></p>') % {'tasks_count': len(recurring_dates)} 796 797 def _is_recurrence_valid(self): 798 self.ensure_one() 799 return self.repeat_interval > 0 and\ 800 (not self.repeat_show_dow or self._get_weekdays()) and\ 801 (self.repeat_type != 'after' or self.repeat_number) and\ 802 (self.repeat_type != 'until' or self.repeat_until and self.repeat_until > fields.Date.today()) 803 804 @api.depends('recurrence_id') 805 def _compute_recurring_count(self): 806 self.recurring_count = 0 807 recurring_tasks = self.filtered(lambda l: l.recurrence_id) 808 count = self.env['project.task'].read_group([('recurrence_id', 'in', recurring_tasks.recurrence_id.ids)], ['id'], 'recurrence_id') 809 tasks_count = {c.get('recurrence_id')[0]: c.get('recurrence_id_count') for c in count} 810 for task in recurring_tasks: 811 task.recurring_count = tasks_count.get(task.recurrence_id.id, 0) 812 813 @api.depends('partner_id.email') 814 def _compute_partner_email(self): 815 for task in self: 816 if task.partner_id and task.partner_id.email != task.partner_email: 817 task.partner_email = task.partner_id.email 818 819 def _inverse_partner_email(self): 820 for task in self: 821 if task.partner_id and task.partner_email != task.partner_id.email: 822 task.partner_id.email = task.partner_email 823 824 @api.depends('partner_id.phone') 825 def _compute_partner_phone(self): 826 for task in self: 827 if task.partner_id and task.partner_phone != task.partner_id.phone: 828 task.partner_phone = task.partner_id.phone 829 830 def _inverse_partner_phone(self): 831 for task in self: 832 if task.partner_id and task.partner_phone != task.partner_id.phone: 833 task.partner_id.phone = task.partner_phone 834 835 @api.depends('partner_email', 'partner_phone', 'partner_id') 836 def _compute_ribbon_message(self): 837 for task in self: 838 will_write_email = task.partner_id and task.partner_email != task.partner_id.email 839 will_write_phone = task.partner_id and task.partner_phone != task.partner_id.phone 840 841 if will_write_email and will_write_phone: 842 task.ribbon_message = _('By saving this change, the customer email and phone number will also be updated.') 843 elif will_write_email: 844 task.ribbon_message = _('By saving this change, the customer email will also be updated.') 845 elif will_write_phone: 846 task.ribbon_message = _('By saving this change, the customer phone number will also be updated.') 847 else: 848 task.ribbon_message = False 849 850 @api.constrains('parent_id') 851 def _check_parent_id(self): 852 if not self._check_recursion(): 853 raise ValidationError(_('Error! You cannot create recursive hierarchy of tasks.')) 854 855 @api.constrains('allowed_user_ids') 856 def _check_no_portal_allowed(self): 857 for task in self.filtered(lambda t: t.project_id.privacy_visibility != 'portal'): 858 portal_users = task.allowed_user_ids.filtered('share') 859 if portal_users: 860 user_names = ', '.join(portal_users[:10].mapped('name')) 861 raise ValidationError(_("The project visibility setting doesn't allow portal users to see the project's tasks. (%s)", user_names)) 862 863 def _compute_attachment_ids(self): 864 for task in self: 865 attachment_ids = self.env['ir.attachment'].search([('res_id', '=', task.id), ('res_model', '=', 'project.task')]).ids 866 message_attachment_ids = task.mapped('message_ids.attachment_ids').ids # from mail_thread 867 task.attachment_ids = [(6, 0, list(set(attachment_ids) - set(message_attachment_ids)))] 868 869 @api.depends('project_id.allowed_user_ids', 'project_id.privacy_visibility') 870 def _compute_allowed_user_ids(self): 871 for task in self: 872 portal_users = task.allowed_user_ids.filtered('share') 873 internal_users = task.allowed_user_ids - portal_users 874 if task.project_id.privacy_visibility == 'followers': 875 task.allowed_user_ids |= task.project_id.allowed_internal_user_ids 876 task.allowed_user_ids -= portal_users 877 elif task.project_id.privacy_visibility == 'portal': 878 task.allowed_user_ids |= task.project_id.allowed_portal_user_ids 879 if task.project_id.privacy_visibility != 'portal': 880 task.allowed_user_ids -= portal_users 881 elif task.project_id.privacy_visibility != 'followers': 882 task.allowed_user_ids -= internal_users 883 884 @api.depends('create_date', 'date_end', 'date_assign') 885 def _compute_elapsed(self): 886 task_linked_to_calendar = self.filtered( 887 lambda task: task.project_id.resource_calendar_id and task.create_date 888 ) 889 for task in task_linked_to_calendar: 890 dt_create_date = fields.Datetime.from_string(task.create_date) 891 892 if task.date_assign: 893 dt_date_assign = fields.Datetime.from_string(task.date_assign) 894 duration_data = task.project_id.resource_calendar_id.get_work_duration_data(dt_create_date, dt_date_assign, compute_leaves=True) 895 task.working_hours_open = duration_data['hours'] 896 task.working_days_open = duration_data['days'] 897 else: 898 task.working_hours_open = 0.0 899 task.working_days_open = 0.0 900 901 if task.date_end: 902 dt_date_end = fields.Datetime.from_string(task.date_end) 903 duration_data = task.project_id.resource_calendar_id.get_work_duration_data(dt_create_date, dt_date_end, compute_leaves=True) 904 task.working_hours_close = duration_data['hours'] 905 task.working_days_close = duration_data['days'] 906 else: 907 task.working_hours_close = 0.0 908 task.working_days_close = 0.0 909 910 (self - task_linked_to_calendar).update(dict.fromkeys( 911 ['working_hours_open', 'working_hours_close', 'working_days_open', 'working_days_close'], 0.0)) 912 913 @api.depends('stage_id', 'kanban_state') 914 def _compute_kanban_state_label(self): 915 for task in self: 916 if task.kanban_state == 'normal': 917 task.kanban_state_label = task.legend_normal 918 elif task.kanban_state == 'blocked': 919 task.kanban_state_label = task.legend_blocked 920 else: 921 task.kanban_state_label = task.legend_done 922 923 def _compute_access_url(self): 924 super(Task, self)._compute_access_url() 925 for task in self: 926 task.access_url = '/my/task/%s' % task.id 927 928 def _compute_access_warning(self): 929 super(Task, self)._compute_access_warning() 930 for task in self.filtered(lambda x: x.project_id.privacy_visibility != 'portal'): 931 task.access_warning = _( 932 "The task cannot be shared with the recipient(s) because the privacy of the project is too restricted. Set the privacy of the project to 'Visible by following customers' in order to make it accessible by the recipient(s).") 933 934 @api.depends('child_ids.planned_hours') 935 def _compute_subtask_planned_hours(self): 936 for task in self: 937 task.subtask_planned_hours = sum(child_task.planned_hours + child_task.subtask_planned_hours for child_task in task.child_ids) 938 939 @api.depends('child_ids') 940 def _compute_subtask_count(self): 941 for task in self: 942 task.subtask_count = len(task._get_all_subtasks()) 943 944 @api.onchange('company_id') 945 def _onchange_task_company(self): 946 if self.project_id.company_id != self.company_id: 947 self.project_id = False 948 949 @api.depends('project_id.company_id') 950 def _compute_company_id(self): 951 for task in self.filtered(lambda task: task.project_id): 952 task.company_id = task.project_id.company_id 953 954 @api.depends('project_id') 955 def _compute_stage_id(self): 956 for task in self: 957 if task.project_id: 958 if task.project_id not in task.stage_id.project_ids: 959 task.stage_id = task.stage_find(task.project_id.id, [ 960 ('fold', '=', False), ('is_closed', '=', False)]) 961 else: 962 task.stage_id = False 963 964 @api.returns('self', lambda value: value.id) 965 def copy(self, default=None): 966 if default is None: 967 default = {} 968 if not default.get('name'): 969 default['name'] = _("%s (copy)", self.name) 970 if self.recurrence_id: 971 default['recurrence_id'] = self.recurrence_id.copy().id 972 return super(Task, self).copy(default) 973 974 @api.constrains('parent_id') 975 def _check_parent_id(self): 976 for task in self: 977 if not task._check_recursion(): 978 raise ValidationError(_('Error! You cannot create recursive hierarchy of task(s).')) 979 980 @api.model 981 def get_empty_list_help(self, help): 982 tname = _("task") 983 project_id = self.env.context.get('default_project_id', False) 984 if project_id: 985 name = self.env['project.project'].browse(project_id).label_tasks 986 if name: tname = name.lower() 987 988 self = self.with_context( 989 empty_list_help_id=self.env.context.get('default_project_id'), 990 empty_list_help_model='project.project', 991 empty_list_help_document_name=tname, 992 ) 993 return super(Task, self).get_empty_list_help(help) 994 995 def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None): 996 """ 997 Add the users subscribed to allowed portal users 998 """ 999 res = super(Task, self).message_subscribe(partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids) 1000 if partner_ids: 1001 new_allowed_users = self.env['res.partner'].browse(partner_ids).user_ids.filtered('share') 1002 tasks = self.filtered(lambda task: task.project_id.privacy_visibility == 'portal') 1003 tasks.sudo().write({'allowed_user_ids': [(4, user.id) for user in new_allowed_users]}) 1004 return res 1005 1006 # ---------------------------------------- 1007 # Case management 1008 # ---------------------------------------- 1009 1010 def stage_find(self, section_id, domain=[], order='sequence'): 1011 """ Override of the base.stage method 1012 Parameter of the stage search taken from the lead: 1013 - section_id: if set, stages must belong to this section or 1014 be a default stage; if not set, stages must be default 1015 stages 1016 """ 1017 # collect all section_ids 1018 section_ids = [] 1019 if section_id: 1020 section_ids.append(section_id) 1021 section_ids.extend(self.mapped('project_id').ids) 1022 search_domain = [] 1023 if section_ids: 1024 search_domain = [('|')] * (len(section_ids) - 1) 1025 for section_id in section_ids: 1026 search_domain.append(('project_ids', '=', section_id)) 1027 search_domain += list(domain) 1028 # perform search, return the first found 1029 return self.env['project.task.type'].search(search_domain, order=order, limit=1).id 1030 1031 # ------------------------------------------------ 1032 # CRUD overrides 1033 # ------------------------------------------------ 1034 @api.model 1035 def default_get(self, default_fields): 1036 vals = super(Task, self).default_get(default_fields) 1037 1038 days = list(DAYS.keys()) 1039 week_start = fields.Datetime.today().weekday() 1040 1041 if all(d in default_fields for d in days): 1042 vals[days[week_start]] = True 1043 if 'repeat_day' in default_fields: 1044 vals['repeat_day'] = str(fields.Datetime.today().day) 1045 if 'repeat_month' in default_fields: 1046 vals['repeat_month'] = self._fields.get('repeat_month').selection[fields.Datetime.today().month - 1][0] 1047 if 'repeat_until' in default_fields: 1048 vals['repeat_until'] = fields.Date.today() + timedelta(days=7) 1049 if 'repeat_weekday' in default_fields: 1050 vals['repeat_weekday'] = self._fields.get('repeat_weekday').selection[week_start][0] 1051 1052 return vals 1053 1054 @api.model_create_multi 1055 def create(self, vals_list): 1056 default_stage = dict() 1057 for vals in vals_list: 1058 project_id = vals.get('project_id') or self.env.context.get('default_project_id') 1059 if project_id and not "company_id" in vals: 1060 vals["company_id"] = self.env["project.project"].browse( 1061 project_id 1062 ).company_id.id or self.env.company.id 1063 if project_id and "stage_id" not in vals: 1064 # 1) Allows keeping the batch creation of tasks 1065 # 2) Ensure the defaults are correct (and computed once by project), 1066 # by using default get (instead of _get_default_stage_id or _stage_find), 1067 if project_id not in default_stage: 1068 default_stage[project_id] = self.with_context( 1069 default_project_id=project_id 1070 ).default_get(['stage_id']).get('stage_id') 1071 vals["stage_id"] = default_stage[project_id] 1072 # user_id change: update date_assign 1073 if vals.get('user_id'): 1074 vals['date_assign'] = fields.Datetime.now() 1075 # Stage change: Update date_end if folded stage and date_last_stage_update 1076 if vals.get('stage_id'): 1077 vals.update(self.update_date_end(vals['stage_id'])) 1078 vals['date_last_stage_update'] = fields.Datetime.now() 1079 # recurrence 1080 rec_fields = vals.keys() & self._get_recurrence_fields() 1081 if rec_fields and vals.get('recurring_task') is True: 1082 rec_values = {rec_field: vals[rec_field] for rec_field in rec_fields} 1083 rec_values['next_recurrence_date'] = fields.Datetime.today() 1084 recurrence = self.env['project.task.recurrence'].create(rec_values) 1085 vals['recurrence_id'] = recurrence.id 1086 tasks = super().create(vals_list) 1087 for task in tasks: 1088 if task.project_id.privacy_visibility == 'portal': 1089 task._portal_ensure_token() 1090 return tasks 1091 1092 def write(self, vals): 1093 now = fields.Datetime.now() 1094 if 'parent_id' in vals and vals['parent_id'] in self.ids: 1095 raise UserError(_("Sorry. You can't set a task as its parent task.")) 1096 if 'active' in vals and not vals.get('active') and any(self.mapped('recurrence_id')): 1097 # TODO: show a dialog to stop the recurrence 1098 raise UserError(_('You cannot archive recurring tasks. Please, disable the recurrence first.')) 1099 # stage change: update date_last_stage_update 1100 if 'stage_id' in vals: 1101 vals.update(self.update_date_end(vals['stage_id'])) 1102 vals['date_last_stage_update'] = now 1103 # reset kanban state when changing stage 1104 if 'kanban_state' not in vals: 1105 vals['kanban_state'] = 'normal' 1106 # user_id change: update date_assign 1107 if vals.get('user_id') and 'date_assign' not in vals: 1108 vals['date_assign'] = now 1109 1110 # recurrence fields 1111 rec_fields = vals.keys() & self._get_recurrence_fields() 1112 if rec_fields: 1113 rec_values = {rec_field: vals[rec_field] for rec_field in rec_fields} 1114 for task in self: 1115 if task.recurrence_id: 1116 task.recurrence_id.write(rec_values) 1117 elif vals.get('recurring_task'): 1118 rec_values['next_recurrence_date'] = fields.Datetime.today() 1119 recurrence = self.env['project.task.recurrence'].create(rec_values) 1120 task.recurrence_id = recurrence.id 1121 1122 if 'recurring_task' in vals and not vals.get('recurring_task'): 1123 self.recurrence_id.unlink() 1124 1125 tasks = self 1126 recurrence_update = vals.pop('recurrence_update', 'this') 1127 if recurrence_update != 'this': 1128 recurrence_domain = [] 1129 if recurrence_update == 'subsequent': 1130 for task in self: 1131 recurrence_domain = OR([recurrence_domain, ['&', ('recurrence_id', '=', task.recurrence_id.id), ('create_date', '>=', task.create_date)]]) 1132 else: 1133 recurrence_domain = [('recurrence_id', 'in', self.recurrence_id.ids)] 1134 tasks |= self.env['project.task'].search(recurrence_domain) 1135 1136 result = super(Task, tasks).write(vals) 1137 # rating on stage 1138 if 'stage_id' in vals and vals.get('stage_id'): 1139 self.filtered(lambda x: x.project_id.rating_active and x.project_id.rating_status == 'stage')._send_task_rating_mail(force_send=True) 1140 return result 1141 1142 def update_date_end(self, stage_id): 1143 project_task_type = self.env['project.task.type'].browse(stage_id) 1144 if project_task_type.fold or project_task_type.is_closed: 1145 return {'date_end': fields.Datetime.now()} 1146 return {'date_end': False} 1147 1148 def unlink(self): 1149 if any(self.mapped('recurrence_id')): 1150 # TODO: show a dialog to stop the recurrence 1151 raise UserError(_('You cannot delete recurring tasks. Please, disable the recurrence first.')) 1152 return super().unlink() 1153 1154 # --------------------------------------------------- 1155 # Subtasks 1156 # --------------------------------------------------- 1157 1158 @api.depends('parent_id.partner_id', 'project_id.partner_id') 1159 def _compute_partner_id(self): 1160 """ 1161 If a task has no partner_id, use the project partner_id if any, or else the parent task partner_id. 1162 Once the task partner_id has been set: 1163 1) if the project partner_id changes, the task partner_id is automatically changed also. 1164 2) if the parent task partner_id changes, the task partner_id remains the same. 1165 """ 1166 for task in self: 1167 if task.partner_id: 1168 if task.project_id.partner_id: 1169 task.partner_id = task.project_id.partner_id 1170 else: 1171 task.partner_id = task.project_id.partner_id or task.parent_id.partner_id 1172 1173 @api.depends('partner_id.email', 'parent_id.email_from') 1174 def _compute_email_from(self): 1175 for task in self: 1176 task.email_from = task.partner_id.email or ((task.partner_id or task.parent_id) and task.email_from) or task.parent_id.email_from 1177 1178 @api.depends('parent_id.project_id.subtask_project_id') 1179 def _compute_project_id(self): 1180 for task in self: 1181 if not task.project_id: 1182 task.project_id = task.parent_id.project_id.subtask_project_id 1183 1184 # --------------------------------------------------- 1185 # Mail gateway 1186 # --------------------------------------------------- 1187 1188 def _track_template(self, changes): 1189 res = super(Task, self)._track_template(changes) 1190 test_task = self[0] 1191 if 'stage_id' in changes and test_task.stage_id.mail_template_id: 1192 res['stage_id'] = (test_task.stage_id.mail_template_id, { 1193 'auto_delete_message': True, 1194 'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'), 1195 'email_layout_xmlid': 'mail.mail_notification_light' 1196 }) 1197 return res 1198 1199 def _creation_subtype(self): 1200 return self.env.ref('project.mt_task_new') 1201 1202 def _track_subtype(self, init_values): 1203 self.ensure_one() 1204 if 'kanban_state_label' in init_values and self.kanban_state == 'blocked': 1205 return self.env.ref('project.mt_task_blocked') 1206 elif 'kanban_state_label' in init_values and self.kanban_state == 'done': 1207 return self.env.ref('project.mt_task_ready') 1208 elif 'stage_id' in init_values: 1209 return self.env.ref('project.mt_task_stage') 1210 return super(Task, self)._track_subtype(init_values) 1211 1212 def _notify_get_groups(self, msg_vals=None): 1213 """ Handle project users and managers recipients that can assign 1214 tasks and create new one directly from notification emails. Also give 1215 access button to portal users and portal customers. If they are notified 1216 they should probably have access to the document. """ 1217 groups = super(Task, self)._notify_get_groups(msg_vals=msg_vals) 1218 local_msg_vals = dict(msg_vals or {}) 1219 self.ensure_one() 1220 1221 project_user_group_id = self.env.ref('project.group_project_user').id 1222 1223 group_func = lambda pdata: pdata['type'] == 'user' and project_user_group_id in pdata['groups'] 1224 if self.project_id.privacy_visibility == 'followers': 1225 allowed_user_ids = self.project_id.allowed_internal_user_ids.partner_id.ids 1226 group_func = lambda pdata: pdata['type'] == 'user' and project_user_group_id in pdata['groups'] and pdata['id'] in allowed_user_ids 1227 new_group = ('group_project_user', group_func, {}) 1228 1229 if not self.user_id and not self.stage_id.fold: 1230 take_action = self._notify_get_action_link('assign', **local_msg_vals) 1231 project_actions = [{'url': take_action, 'title': _('I take it')}] 1232 new_group[2]['actions'] = project_actions 1233 1234 groups = [new_group] + groups 1235 1236 if self.project_id.privacy_visibility == 'portal': 1237 allowed_user_ids = self.project_id.allowed_portal_user_ids.partner_id.ids 1238 groups.insert(0, ( 1239 'allowed_portal_users', 1240 lambda pdata: pdata['type'] == 'portal' and pdata['id'] in allowed_user_ids, 1241 {} 1242 )) 1243 1244 portal_privacy = self.project_id.privacy_visibility == 'portal' 1245 for group_name, group_method, group_data in groups: 1246 if group_name in ('customer', 'user') or group_name == 'portal_customer' and not portal_privacy: 1247 group_data['has_button_access'] = False 1248 elif group_name == 'portal_customer' and portal_privacy: 1249 group_data['has_button_access'] = True 1250 1251 return groups 1252 1253 def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None): 1254 """ Override to set alias of tasks to their project if any. """ 1255 aliases = self.sudo().mapped('project_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None) 1256 res = {task.id: aliases.get(task.project_id.id) for task in self} 1257 leftover = self.filtered(lambda rec: not rec.project_id) 1258 if leftover: 1259 res.update(super(Task, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names)) 1260 return res 1261 1262 def email_split(self, msg): 1263 email_list = tools.email_split((msg.get('to') or '') + ',' + (msg.get('cc') or '')) 1264 # check left-part is not already an alias 1265 aliases = self.mapped('project_id.alias_name') 1266 return [x for x in email_list if x.split('@')[0] not in aliases] 1267 1268 @api.model 1269 def message_new(self, msg, custom_values=None): 1270 """ Overrides mail_thread message_new that is called by the mailgateway 1271 through message_process. 1272 This override updates the document according to the email. 1273 """ 1274 # remove default author when going through the mail gateway. Indeed we 1275 # do not want to explicitly set user_id to False; however we do not 1276 # want the gateway user to be responsible if no other responsible is 1277 # found. 1278 create_context = dict(self.env.context or {}) 1279 create_context['default_user_id'] = False 1280 if custom_values is None: 1281 custom_values = {} 1282 defaults = { 1283 'name': msg.get('subject') or _("No Subject"), 1284 'email_from': msg.get('from'), 1285 'planned_hours': 0.0, 1286 'partner_id': msg.get('author_id') 1287 } 1288 defaults.update(custom_values) 1289 1290 task = super(Task, self.with_context(create_context)).message_new(msg, custom_values=defaults) 1291 email_list = task.email_split(msg) 1292 partner_ids = [p.id for p in self.env['mail.thread']._mail_find_partner_from_emails(email_list, records=task, force_create=False) if p] 1293 task.message_subscribe(partner_ids) 1294 return task 1295 1296 def message_update(self, msg, update_vals=None): 1297 """ Override to update the task according to the email. """ 1298 email_list = self.email_split(msg) 1299 partner_ids = [p.id for p in self.env['mail.thread']._mail_find_partner_from_emails(email_list, records=self, force_create=False) if p] 1300 self.message_subscribe(partner_ids) 1301 return super(Task, self).message_update(msg, update_vals=update_vals) 1302 1303 def _message_get_suggested_recipients(self): 1304 recipients = super(Task, self)._message_get_suggested_recipients() 1305 for task in self: 1306 if task.partner_id: 1307 reason = _('Customer Email') if task.partner_id.email else _('Customer') 1308 task._message_add_suggested_recipient(recipients, partner=task.partner_id, reason=reason) 1309 elif task.email_from: 1310 task._message_add_suggested_recipient(recipients, email=task.email_from, reason=_('Customer Email')) 1311 return recipients 1312 1313 def _notify_email_header_dict(self): 1314 headers = super(Task, self)._notify_email_header_dict() 1315 if self.project_id: 1316 current_objects = [h for h in headers.get('X-Odoo-Objects', '').split(',') if h] 1317 current_objects.insert(0, 'project.project-%s, ' % self.project_id.id) 1318 headers['X-Odoo-Objects'] = ','.join(current_objects) 1319 if self.tag_ids: 1320 headers['X-Odoo-Tags'] = ','.join(self.tag_ids.mapped('name')) 1321 return headers 1322 1323 def _message_post_after_hook(self, message, msg_vals): 1324 if message.attachment_ids and not self.displayed_image_id: 1325 image_attachments = message.attachment_ids.filtered(lambda a: a.mimetype == 'image') 1326 if image_attachments: 1327 self.displayed_image_id = image_attachments[0] 1328 1329 if self.email_from and not self.partner_id: 1330 # we consider that posting a message with a specified recipient (not a follower, a specific one) 1331 # on a document without customer means that it was created through the chatter using 1332 # suggested recipients. This heuristic allows to avoid ugly hacks in JS. 1333 new_partner = message.partner_ids.filtered(lambda partner: partner.email == self.email_from) 1334 if new_partner: 1335 self.search([ 1336 ('partner_id', '=', False), 1337 ('email_from', '=', new_partner.email), 1338 ('stage_id.fold', '=', False)]).write({'partner_id': new_partner.id}) 1339 return super(Task, self)._message_post_after_hook(message, msg_vals) 1340 1341 def action_assign_to_me(self): 1342 self.write({'user_id': self.env.user.id}) 1343 1344 # If depth == 1, return only direct children 1345 # If depth == 3, return children to third generation 1346 # If depth <= 0, return all children without depth limit 1347 def _get_all_subtasks(self, depth=0): 1348 children = self.mapped('child_ids').filtered(lambda children: children.active) 1349 if not children: 1350 return self.env['project.task'] 1351 if depth == 1: 1352 return children 1353 return children + children._get_all_subtasks(depth - 1) 1354 1355 def action_open_parent_task(self): 1356 return { 1357 'name': _('Parent Task'), 1358 'view_mode': 'form', 1359 'res_model': 'project.task', 1360 'res_id': self.parent_id.id, 1361 'type': 'ir.actions.act_window', 1362 'context': dict(self._context, create=False) 1363 } 1364 1365 def action_subtask(self): 1366 action = self.env["ir.actions.actions"]._for_xml_id("project.project_task_action_sub_task") 1367 1368 # display all subtasks of current task 1369 action['domain'] = [('id', 'child_of', self.id), ('id', '!=', self.id)] 1370 1371 # update context, with all default values as 'quick_create' does not contains all field in its view 1372 if self._context.get('default_project_id'): 1373 default_project = self.env['project.project'].browse(self.env.context['default_project_id']) 1374 else: 1375 default_project = self.project_id.subtask_project_id or self.project_id 1376 ctx = dict(self.env.context) 1377 ctx = {k: v for k, v in ctx.items() if not k.startswith('search_default_')} 1378 ctx.update({ 1379 'default_name': self.env.context.get('name', self.name) + ':', 1380 'default_parent_id': self.id, # will give default subtask field in `default_get` 1381 'default_company_id': default_project.company_id.id if default_project else self.env.company.id, 1382 }) 1383 1384 action['context'] = ctx 1385 1386 return action 1387 1388 def action_recurring_tasks(self): 1389 return { 1390 'name': 'Tasks in Recurrence', 1391 'type': 'ir.actions.act_window', 1392 'res_model': 'project.task', 1393 'view_mode': 'tree,form', 1394 'domain': [('recurrence_id', 'in', self.recurrence_id.ids)], 1395 } 1396 1397 # --------------------------------------------------- 1398 # Rating business 1399 # --------------------------------------------------- 1400 1401 def _send_task_rating_mail(self, force_send=False): 1402 for task in self: 1403 rating_template = task.stage_id.rating_template_id 1404 if rating_template: 1405 task.rating_send_request(rating_template, lang=task.partner_id.lang, force_send=force_send) 1406 1407 def rating_get_partner_id(self): 1408 res = super(Task, self).rating_get_partner_id() 1409 if not res and self.project_id.partner_id: 1410 return self.project_id.partner_id 1411 return res 1412 1413 def rating_apply(self, rate, token=None, feedback=None, subtype_xmlid=None): 1414 return super(Task, self).rating_apply(rate, token=token, feedback=feedback, subtype_xmlid="project.mt_task_rating") 1415 1416 def _rating_get_parent_field_name(self): 1417 return 'project_id' 1418 1419 1420class ProjectTags(models.Model): 1421 """ Tags of project's tasks """ 1422 _name = "project.tags" 1423 _description = "Project Tags" 1424 1425 def _get_default_color(self): 1426 return randint(1, 11) 1427 1428 name = fields.Char('Name', required=True) 1429 color = fields.Integer(string='Color', default=_get_default_color) 1430 1431 _sql_constraints = [ 1432 ('name_uniq', 'unique (name)', "Tag name already exists!"), 1433 ] 1434