How to reproduce: - Install crm with demo data - Open the crm app - From the menu "Configuration -> Activity Plans", create a plan - Open a lead - Schedule the plan just created for the lead The log message in the chatter display due date in the wrong format: Year(4)-month(2)-day(2) instead of the user date format (if your date format is that one, please change it for testing). This fixes the problem by displaying the date in the user format. Task-3639909 closes odoo/odoo#152217 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
330 lines
14 KiB
Python
330 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from markupsafe import Markup
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.addons.mail.tools.parser import parse_res_ids
|
|
from odoo.exceptions import ValidationError
|
|
from odoo.tools.misc import clean_context, format_date
|
|
|
|
|
|
class MailActivitySchedule(models.TransientModel):
|
|
_name = 'mail.activity.schedule'
|
|
_description = 'Activity schedule plan Wizard'
|
|
_batch_size = 500
|
|
|
|
@api.model
|
|
def default_get(self, fields_list):
|
|
res = super().default_get(fields_list)
|
|
context = self.env.context
|
|
active_res_ids = parse_res_ids(context.get('active_ids'))
|
|
if 'res_ids' in fields_list:
|
|
if active_res_ids and len(active_res_ids) <= self._batch_size:
|
|
res['res_ids'] = f"{context['active_ids']}"
|
|
elif not active_res_ids and context.get('active_id'):
|
|
res['res_ids'] = f"{[context['active_id']]}"
|
|
res_model = context.get('active_model') or context.get('params', {}).get('active_model', False)
|
|
if 'res_model' in fields_list:
|
|
res['res_model'] = res_model
|
|
return res
|
|
|
|
res_model_id = fields.Many2one(
|
|
'ir.model', string="Applies to",
|
|
compute="_compute_res_model_id", compute_sudo=True,
|
|
ondelete="cascade", precompute=True, readonly=False, required=True, store=True)
|
|
res_model = fields.Char("Model", readonly=True, required=True)
|
|
res_ids = fields.Text(
|
|
'Document IDs', compute='_compute_res_ids',
|
|
readonly=True, store=True, precompute=True)
|
|
is_batch_mode = fields.Boolean('Use in batch', compute='_compute_is_batch_mode')
|
|
company_id = fields.Many2one('res.company', 'Company',
|
|
compute='_compute_company_id', required=True)
|
|
# usage
|
|
error = fields.Html(compute='_compute_error')
|
|
has_error = fields.Boolean(compute='_compute_error')
|
|
# plan-based
|
|
plan_available_ids = fields.Many2many('mail.activity.plan', compute='_compute_plan_available_ids',
|
|
store=True, compute_sudo=True)
|
|
plan_id = fields.Many2one('mail.activity.plan', domain="[('id', 'in', plan_available_ids)]",
|
|
compute='_compute_plan_id', store=True, readonly=False)
|
|
plan_has_user_on_demand = fields.Boolean(related="plan_id.has_user_on_demand")
|
|
plan_assignation_summary = fields.Html(related='plan_id.assignation_summary')
|
|
plan_on_demand_user_id = fields.Many2one(
|
|
'res.users', 'Assigned To',
|
|
help='Choose assignation for activities with on demand assignation.',
|
|
default=lambda self: self.env.user)
|
|
plan_date_deadline = fields.Date(
|
|
'Plan Date', compute='_compute_plan_date_deadline',
|
|
store=True, readonly=False)
|
|
# activity-based
|
|
activity_type_id = fields.Many2one(
|
|
'mail.activity.type', string='Activity Type',
|
|
compute='_compute_activity_type_id', store=True, readonly=False,
|
|
domain="['|', ('res_model', '=', False), ('res_model', '=', res_model)]", ondelete='set null')
|
|
activity_category = fields.Selection(related='activity_type_id.category', readonly=True)
|
|
date_deadline = fields.Date(
|
|
'Due Date', compute="_compute_date_deadline",
|
|
readonly=False, store=True)
|
|
summary = fields.Char(
|
|
'Summary', compute="_compute_summary",
|
|
readonly=False, store=True)
|
|
note = fields.Html(
|
|
'Note', compute="_compute_note",
|
|
readonly=False, store=True, sanitize_style=True)
|
|
activity_user_id = fields.Many2one(
|
|
'res.users', 'Assigned to', compute='_compute_activity_user_id',
|
|
readonly=False, store=True)
|
|
chaining_type = fields.Selection(related='activity_type_id.chaining_type', readonly=True)
|
|
|
|
@api.depends('res_model')
|
|
def _compute_res_model_id(self):
|
|
for scheduler in self:
|
|
scheduler.res_model_id = self.env['ir.model']._get_id(scheduler.res_model)
|
|
|
|
@api.depends_context('active_ids')
|
|
def _compute_res_ids(self):
|
|
context = self.env.context
|
|
for scheduler in self.filtered(lambda scheduler: not scheduler.res_ids):
|
|
active_res_ids = parse_res_ids(context.get('active_ids'))
|
|
if active_res_ids and len(active_res_ids) <= self._batch_size:
|
|
scheduler.res_ids = f"{context['active_ids']}"
|
|
elif not active_res_ids and context.get('active_id'):
|
|
scheduler.res_ids = f"{[context['active_id']]}"
|
|
|
|
@api.depends('res_model_id', 'res_ids')
|
|
def _compute_company_id(self):
|
|
for scheduler in self:
|
|
applied_on = scheduler._get_applied_on_records()
|
|
scheduler.company_id = (applied_on and 'company_id' in applied_on[0]._fields and applied_on[0].company_id
|
|
) or self.env.company
|
|
|
|
@api.depends('company_id', 'res_model_id', 'res_ids',
|
|
'plan_id', 'plan_on_demand_user_id', 'plan_available_ids', # plan specific
|
|
'activity_type_id', 'activity_user_id') # activity specific
|
|
def _compute_error(self):
|
|
for scheduler in self:
|
|
errors = set()
|
|
applied_on = scheduler._get_applied_on_records()
|
|
if applied_on and ('company_id' in scheduler.env[applied_on._name]._fields and
|
|
len(applied_on.mapped('company_id')) > 1):
|
|
errors.add(_('The records must belong to the same company.'))
|
|
if scheduler.plan_id:
|
|
errors |= set(scheduler._check_plan_templates_error(applied_on))
|
|
if errors:
|
|
error_header = (
|
|
_('The plan "%(plan_name)s" cannot be launched:', plan_name=scheduler.plan_id.name) if scheduler.plan_id
|
|
else _('The activity cannot be launched:')
|
|
)
|
|
error_body = Markup('<ul>%s</ul>') % (
|
|
Markup().join(Markup('<li>%s</li>') % error for error in errors)
|
|
)
|
|
scheduler.error = f'{error_header}{error_body}'
|
|
scheduler.has_error = True
|
|
else:
|
|
scheduler.error = False
|
|
scheduler.has_error = False
|
|
|
|
@api.depends('res_ids')
|
|
def _compute_is_batch_mode(self):
|
|
for scheduler in self:
|
|
scheduler.is_batch_mode = len(scheduler._evaluate_res_ids()) > 1
|
|
|
|
@api.depends('company_id', 'res_model_id')
|
|
def _compute_plan_available_ids(self):
|
|
for scheduler in self:
|
|
scheduler.plan_available_ids = self.env['mail.activity.plan'].search(self._get_plan_available_base_domain())
|
|
|
|
@api.depends_context('plan_mode')
|
|
@api.depends('plan_available_ids')
|
|
def _compute_plan_id(self):
|
|
for scheduler in self:
|
|
if self.env.context.get('plan_mode'):
|
|
scheduler.plan_id = scheduler.env['mail.activity.plan'].search(
|
|
[('id', 'in', self.plan_available_ids.ids)], order='id', limit=1)
|
|
else:
|
|
scheduler.plan_id = False
|
|
|
|
@api.depends('res_model_id', 'res_ids')
|
|
def _compute_plan_date_deadline(self):
|
|
self.plan_date_deadline = False
|
|
|
|
@api.depends('res_model')
|
|
def _compute_activity_type_id(self):
|
|
for scheduler in self:
|
|
if not scheduler.activity_type_id:
|
|
scheduler.activity_type_id = scheduler.env['mail.activity']._default_activity_type_for_model(scheduler.res_model)
|
|
|
|
@api.depends('activity_type_id')
|
|
def _compute_date_deadline(self):
|
|
for scheduler in self:
|
|
if scheduler.activity_type_id:
|
|
scheduler.date_deadline = scheduler.env['mail.activity']._calculate_date_deadline(scheduler.activity_type_id)
|
|
elif not scheduler.date_deadline:
|
|
scheduler.date_deadline = fields.Date.context_today(scheduler)
|
|
|
|
@api.depends('activity_type_id')
|
|
def _compute_summary(self):
|
|
for scheduler in self:
|
|
if scheduler.activity_type_id.summary:
|
|
scheduler.summary = scheduler.activity_type_id.summary
|
|
|
|
@api.depends('activity_type_id')
|
|
def _compute_note(self):
|
|
for scheduler in self:
|
|
if scheduler.activity_type_id.default_note:
|
|
scheduler.note = scheduler.activity_type_id.default_note
|
|
|
|
@api.depends('activity_type_id')
|
|
def _compute_activity_user_id(self):
|
|
for scheduler in self:
|
|
if scheduler.activity_type_id.default_user_id:
|
|
scheduler.activity_user_id = scheduler.activity_type_id.default_user_id
|
|
elif not scheduler.activity_user_id:
|
|
scheduler.activity_user_id = self.env.user
|
|
|
|
# Any writable fields that can change error computed field
|
|
@api.constrains('res_model_id', 'res_ids', # records (-> responsible)
|
|
'plan_id', 'plan_on_demand_user_id', # plan specific
|
|
'activity_type_id', 'activity_user_id') # activity specific
|
|
def _check_consistency(self):
|
|
for scheduler in self.filtered('error'):
|
|
raise ValidationError(scheduler.error)
|
|
|
|
@api.constrains('res_ids')
|
|
def _check_res_ids(self):
|
|
""" Check res_ids is a valid list of integers (or Falsy). """
|
|
for scheduler in self:
|
|
scheduler._evaluate_res_ids()
|
|
|
|
# ------------------------------------------------------------
|
|
# PLAN-BASED SCHEDULING API
|
|
# ------------------------------------------------------------
|
|
|
|
def action_schedule_plan(self):
|
|
applied_on = self._get_applied_on_records()
|
|
for record in applied_on:
|
|
body = _('The plan "%(plan_name)s" has been started', plan_name=self.plan_id.name)
|
|
activity_descriptions = []
|
|
for template in self._plan_filter_activity_templates_to_schedule():
|
|
if template.responsible_type == 'on_demand':
|
|
responsible = self.plan_on_demand_user_id
|
|
else:
|
|
responsible = template._determine_responsible(self.plan_on_demand_user_id, record)['responsible']
|
|
date_deadline = self.env['mail.activity']._calculate_date_deadline(
|
|
template.activity_type_id, force_base_date=self.plan_date_deadline)
|
|
record.activity_schedule(
|
|
activity_type_id=template.activity_type_id.id,
|
|
automated=False,
|
|
summary=template.summary,
|
|
note=template.note,
|
|
user_id=responsible.id,
|
|
date_deadline=date_deadline
|
|
)
|
|
activity_descriptions.append(
|
|
_('%(activity)s, assigned to %(name)s, due on the %(deadline)s',
|
|
activity=template.summary or template.activity_type_id.name,
|
|
name=responsible.name, deadline=format_date(self.env, date_deadline)))
|
|
|
|
if activity_descriptions:
|
|
body += Markup('<ul>%s</ul>') % (
|
|
Markup().join(Markup('<li>%s</li>') % description for description in activity_descriptions)
|
|
)
|
|
record.message_post(body=body)
|
|
|
|
if len(applied_on) == 1:
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': self.res_model,
|
|
'res_id': applied_on.id,
|
|
'name': applied_on.display_name,
|
|
'view_mode': 'form',
|
|
'views': [(False, "form")],
|
|
}
|
|
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': self.res_model,
|
|
'name': _('Launch Plans'),
|
|
'view_mode': 'tree,form',
|
|
'target': 'current',
|
|
'domain': [('id', 'in', applied_on.ids)],
|
|
}
|
|
|
|
def _check_plan_templates_error(self, applied_on):
|
|
self.ensure_one()
|
|
return filter(
|
|
None, [
|
|
activity_template._determine_responsible(self.plan_on_demand_user_id, record)['error']
|
|
for activity_template in self.plan_id.template_ids
|
|
for record in applied_on
|
|
]
|
|
)
|
|
|
|
# ------------------------------------------------------------
|
|
# ACTIVITY-BASED SCHEDULING API
|
|
# ------------------------------------------------------------
|
|
|
|
def action_schedule_activities(self):
|
|
self._action_schedule_activities()
|
|
|
|
def action_schedule_activities_done(self):
|
|
self._action_schedule_activities().action_done()
|
|
|
|
def action_schedule_activities_done_and_schedule(self):
|
|
ctx = dict(
|
|
clean_context(self.env.context),
|
|
default_previous_activity_type_id=self.activity_type_id.id,
|
|
activity_previous_deadline=self.date_deadline,
|
|
default_res_ids=self.res_ids,
|
|
default_res_model=self.res_model,
|
|
)
|
|
_messages, next_activities = self._action_schedule_activities()._action_done()
|
|
if next_activities:
|
|
return False
|
|
return {
|
|
'name': _("Schedule Activity On Selected Records") if self.is_batch_mode else _("Schedule Activity"),
|
|
'context': ctx,
|
|
'view_mode': 'form',
|
|
'res_model': 'mail.activity.schedule',
|
|
'views': [(False, 'form')],
|
|
'type': 'ir.actions.act_window',
|
|
'target': 'new',
|
|
}
|
|
|
|
def _action_schedule_activities(self):
|
|
return self._get_applied_on_records().activity_schedule(
|
|
activity_type_id=self.activity_type_id.id,
|
|
automated=False,
|
|
summary=self.summary,
|
|
note=self.note,
|
|
user_id=self.activity_user_id.id,
|
|
date_deadline=self.date_deadline
|
|
)
|
|
|
|
# ------------------------------------------------------------
|
|
# TOOLS
|
|
# ------------------------------------------------------------
|
|
|
|
def _evaluate_res_ids(self):
|
|
""" Parse composer res_ids, which can be: an already valid list or
|
|
tuple (generally in code), a list or tuple as a string (coming from
|
|
actions). Void strings / missing values are evaluated as an empty list.
|
|
|
|
:return: a list of IDs (empty list in case of falsy strings)"""
|
|
self.ensure_one()
|
|
return parse_res_ids(self.res_ids) or []
|
|
|
|
def _get_applied_on_records(self):
|
|
return self.env[self.res_model].browse(self._evaluate_res_ids())
|
|
|
|
def _get_plan_available_base_domain(self):
|
|
self.ensure_one()
|
|
return [
|
|
'&', '|', ('company_id', '=', False), ('company_id', '=', self.company_id.id),
|
|
'|', ('res_model', '=', False), ('res_model', '=', self.res_model)
|
|
]
|
|
|
|
def _plan_filter_activity_templates_to_schedule(self):
|
|
return self.plan_id.template_ids
|