mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 17:31:59 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
719
odoo-bringout-oca-ocb-mail/mail/models/mail_activity.py
Normal file
719
odoo-bringout-oca-ocb-mail/mail/models/mail_activity.py
Normal file
|
|
@ -0,0 +1,719 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import pytz
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, exceptions, fields, models, _, Command
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import is_html_empty
|
||||
from odoo.tools.misc import clean_context, get_lang
|
||||
|
||||
|
||||
class MailActivity(models.Model):
|
||||
""" An actual activity to perform. Activities are linked to
|
||||
documents using res_id and res_model_id fields. Activities have a deadline
|
||||
that can be used in kanban view to display a status. Once done activities
|
||||
are unlinked and a message is posted. This message has a new activity_type_id
|
||||
field that indicates the activity linked to the message. """
|
||||
_name = 'mail.activity'
|
||||
_description = 'Activity'
|
||||
_order = 'date_deadline ASC, id ASC'
|
||||
_rec_name = 'summary'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(MailActivity, self).default_get(fields)
|
||||
if not fields or 'res_model_id' in fields and res.get('res_model'):
|
||||
res['res_model_id'] = self.env['ir.model']._get(res['res_model']).id
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _default_activity_type(self):
|
||||
default_vals = self.default_get(['res_model_id', 'res_model'])
|
||||
if not default_vals.get('res_model_id'):
|
||||
return False
|
||||
|
||||
current_model = self.env["ir.model"].sudo().browse(default_vals['res_model_id']).model
|
||||
return self._default_activity_type_for_model(current_model)
|
||||
|
||||
@api.model
|
||||
def _default_activity_type_for_model(self, model):
|
||||
todo_id = self.env['ir.model.data']._xmlid_to_res_id('mail.mail_activity_data_todo', raise_if_not_found=False)
|
||||
activity_type_todo = self.env['mail.activity.type'].browse(todo_id) if todo_id else self.env['mail.activity.type']
|
||||
if activity_type_todo and activity_type_todo.active and \
|
||||
(activity_type_todo.res_model == model or not activity_type_todo.res_model):
|
||||
return activity_type_todo
|
||||
activity_type_model = self.env['mail.activity.type'].search([('res_model', '=', model)], limit=1)
|
||||
if activity_type_model:
|
||||
return activity_type_model
|
||||
activity_type_generic = self.env['mail.activity.type'].search([('res_model', '=', False)], limit=1)
|
||||
return activity_type_generic
|
||||
|
||||
# owner
|
||||
res_model_id = fields.Many2one(
|
||||
'ir.model', 'Document Model',
|
||||
index=True, ondelete='cascade', required=True)
|
||||
res_model = fields.Char(
|
||||
'Related Document Model',
|
||||
index=True, related='res_model_id.model', compute_sudo=True, store=True, readonly=True)
|
||||
res_id = fields.Many2oneReference(string='Related Document ID', index=True, model_field='res_model')
|
||||
res_name = fields.Char(
|
||||
'Document Name', compute='_compute_res_name', compute_sudo=True, store=True,
|
||||
readonly=True)
|
||||
# activity
|
||||
activity_type_id = fields.Many2one(
|
||||
'mail.activity.type', string='Activity Type',
|
||||
domain="['|', ('res_model', '=', False), ('res_model', '=', res_model)]", ondelete='restrict',
|
||||
default=_default_activity_type)
|
||||
activity_category = fields.Selection(related='activity_type_id.category', readonly=True)
|
||||
activity_decoration = fields.Selection(related='activity_type_id.decoration_type', readonly=True)
|
||||
icon = fields.Char('Icon', related='activity_type_id.icon', readonly=True)
|
||||
summary = fields.Char('Summary')
|
||||
note = fields.Html('Note', sanitize_style=True)
|
||||
date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.context_today)
|
||||
automated = fields.Boolean(
|
||||
'Automated activity', readonly=True,
|
||||
help='Indicates this activity has been created automatically and not by any user.')
|
||||
# description
|
||||
user_id = fields.Many2one(
|
||||
'res.users', 'Assigned to',
|
||||
default=lambda self: self.env.user,
|
||||
index=True, required=True)
|
||||
request_partner_id = fields.Many2one('res.partner', string='Requesting Partner')
|
||||
state = fields.Selection([
|
||||
('overdue', 'Overdue'),
|
||||
('today', 'Today'),
|
||||
('planned', 'Planned')], 'State',
|
||||
compute='_compute_state')
|
||||
recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type")
|
||||
previous_activity_type_id = fields.Many2one('mail.activity.type', string='Previous Activity Type', readonly=True)
|
||||
has_recommended_activities = fields.Boolean(
|
||||
'Next activities available',
|
||||
compute='_compute_has_recommended_activities') # technical field for UX purpose
|
||||
mail_template_ids = fields.Many2many(related='activity_type_id.mail_template_ids', readonly=True)
|
||||
chaining_type = fields.Selection(related='activity_type_id.chaining_type', readonly=True)
|
||||
# access
|
||||
can_write = fields.Boolean(compute='_compute_can_write') # used to hide buttons if the current user has no access
|
||||
|
||||
_sql_constraints = [
|
||||
# Required on a Many2one reference field is not sufficient as actually
|
||||
# writing 0 is considered as a valid value, because this is an integer field.
|
||||
# We therefore need a specific constraint check.
|
||||
('check_res_id_is_set',
|
||||
'CHECK(res_id IS NOT NULL AND res_id !=0 )',
|
||||
'Activities have to be linked to records with a not null res_id.')
|
||||
]
|
||||
|
||||
@api.onchange('previous_activity_type_id')
|
||||
def _compute_has_recommended_activities(self):
|
||||
for record in self:
|
||||
record.has_recommended_activities = bool(record.previous_activity_type_id.suggested_next_type_ids)
|
||||
|
||||
@api.onchange('previous_activity_type_id')
|
||||
def _onchange_previous_activity_type_id(self):
|
||||
for record in self:
|
||||
if record.previous_activity_type_id.triggered_next_type_id:
|
||||
record.activity_type_id = record.previous_activity_type_id.triggered_next_type_id
|
||||
|
||||
@api.depends('res_model', 'res_id')
|
||||
def _compute_res_name(self):
|
||||
for activity in self:
|
||||
activity.res_name = activity.res_model and \
|
||||
self.env[activity.res_model].browse(activity.res_id).display_name
|
||||
|
||||
@api.depends('date_deadline')
|
||||
def _compute_state(self):
|
||||
for record in self.filtered(lambda activity: activity.date_deadline):
|
||||
tz = record.user_id.sudo().tz
|
||||
date_deadline = record.date_deadline
|
||||
record.state = self._compute_state_from_date(date_deadline, tz)
|
||||
|
||||
@api.model
|
||||
def _compute_state_from_date(self, date_deadline, tz=False):
|
||||
date_deadline = fields.Date.from_string(date_deadline)
|
||||
today_default = date.today()
|
||||
today = today_default
|
||||
if tz:
|
||||
today_utc = pytz.utc.localize(datetime.utcnow())
|
||||
today_tz = today_utc.astimezone(pytz.timezone(tz))
|
||||
today = date(year=today_tz.year, month=today_tz.month, day=today_tz.day)
|
||||
diff = (date_deadline - today)
|
||||
if diff.days == 0:
|
||||
return 'today'
|
||||
elif diff.days < 0:
|
||||
return 'overdue'
|
||||
else:
|
||||
return 'planned'
|
||||
|
||||
@api.depends('res_model', 'res_id', 'user_id')
|
||||
def _compute_can_write(self):
|
||||
valid_records = self._filter_access_rules('write')
|
||||
for record in self:
|
||||
record.can_write = record in valid_records
|
||||
|
||||
@api.onchange('activity_type_id')
|
||||
def _onchange_activity_type_id(self):
|
||||
if self.activity_type_id:
|
||||
if self.activity_type_id.summary:
|
||||
self.summary = self.activity_type_id.summary
|
||||
self.date_deadline = self._calculate_date_deadline(self.activity_type_id)
|
||||
self.user_id = self.activity_type_id.default_user_id or self.env.user
|
||||
if self.activity_type_id.default_note:
|
||||
self.note = self.activity_type_id.default_note
|
||||
|
||||
def _calculate_date_deadline(self, activity_type):
|
||||
# Date.context_today is correct because date_deadline is a Date and is meant to be
|
||||
# expressed in user TZ
|
||||
base = fields.Date.context_today(self)
|
||||
if activity_type.delay_from == 'previous_activity' and 'activity_previous_deadline' in self.env.context:
|
||||
base = fields.Date.from_string(self.env.context.get('activity_previous_deadline'))
|
||||
return base + relativedelta(**{activity_type.delay_unit: activity_type.delay_count})
|
||||
|
||||
@api.onchange('recommended_activity_type_id')
|
||||
def _onchange_recommended_activity_type_id(self):
|
||||
if self.recommended_activity_type_id:
|
||||
self.activity_type_id = self.recommended_activity_type_id
|
||||
|
||||
def _filter_access_rules(self, operation):
|
||||
# write / unlink: valid for creator / assigned
|
||||
if operation in ('write', 'unlink'):
|
||||
valid = super(MailActivity, self)._filter_access_rules(operation)
|
||||
if valid and valid == self:
|
||||
return self
|
||||
else:
|
||||
valid = self.env[self._name]
|
||||
return self._filter_access_rules_remaining(valid, operation, '_filter_access_rules')
|
||||
|
||||
def _filter_access_rules_python(self, operation):
|
||||
# write / unlink: valid for creator / assigned
|
||||
if operation in ('write', 'unlink'):
|
||||
valid = super(MailActivity, self)._filter_access_rules_python(operation)
|
||||
if valid and valid == self:
|
||||
return self
|
||||
else:
|
||||
valid = self.env[self._name]
|
||||
return self._filter_access_rules_remaining(valid, operation, '_filter_access_rules_python')
|
||||
|
||||
def _filter_access_rules_remaining(self, valid, operation, filter_access_rules_method):
|
||||
""" Return the subset of ``self`` for which ``operation`` is allowed.
|
||||
A custom implementation is done on activities as this document has some
|
||||
access rules and is based on related document for activities that are
|
||||
not covered by those rules.
|
||||
|
||||
Access on activities are the following :
|
||||
|
||||
* create: (``mail_post_access`` or write) right on related documents;
|
||||
* read: read rights on related documents;
|
||||
* write: access rule OR
|
||||
(``mail_post_access`` or write) rights on related documents);
|
||||
* unlink: access rule OR
|
||||
(``mail_post_access`` or write) rights on related documents);
|
||||
"""
|
||||
# compute remaining for hand-tailored rules
|
||||
remaining = self - valid
|
||||
remaining_sudo = remaining.sudo()
|
||||
|
||||
# fall back on related document access right checks. Use the same as defined for mail.thread
|
||||
# if available; otherwise fall back on read for read, write for other operations.
|
||||
activity_to_documents = dict()
|
||||
for activity in remaining_sudo:
|
||||
# write / unlink: if not updating self or assigned, limit to automated activities to avoid
|
||||
# updating other people's activities. As unlinking a document bypasses access rights checks
|
||||
# on related activities this will not prevent people from deleting documents with activities
|
||||
# create / read: just check rights on related document
|
||||
activity_to_documents.setdefault(activity.res_model, list()).append(activity.res_id)
|
||||
for doc_model, doc_ids in activity_to_documents.items():
|
||||
if hasattr(self.env[doc_model], '_mail_post_access'):
|
||||
doc_operation = self.env[doc_model]._mail_post_access
|
||||
elif operation == 'read':
|
||||
doc_operation = 'read'
|
||||
else:
|
||||
doc_operation = 'write'
|
||||
right = self.env[doc_model].check_access_rights(doc_operation, raise_exception=False)
|
||||
if right:
|
||||
valid_doc_ids = getattr(self.env[doc_model].browse(doc_ids), filter_access_rules_method)(doc_operation)
|
||||
valid += remaining.filtered(lambda activity: activity.res_model == doc_model and activity.res_id in valid_doc_ids.ids)
|
||||
|
||||
return valid
|
||||
|
||||
def _check_access_assignation(self):
|
||||
""" Check assigned user (user_id field) has access to the document. Purpose
|
||||
is to allow assigned user to handle their activities. For that purpose
|
||||
assigned user should be able to at least read the document. We therefore
|
||||
raise an UserError if the assigned user has no access to the document. """
|
||||
for model, activity_data in self._classify_by_model().items():
|
||||
# group activities / user, in order to batch the check of ACLs
|
||||
per_user = dict()
|
||||
for activity in activity_data['activities'].filtered(lambda act: act.user_id):
|
||||
if activity.user_id not in per_user:
|
||||
per_user[activity.user_id] = activity
|
||||
else:
|
||||
per_user[activity.user_id] += activity
|
||||
for user, activities in per_user.items():
|
||||
RecordModel = self.env[model].with_user(user).with_context(
|
||||
allowed_company_ids=user.company_ids.ids
|
||||
)
|
||||
try:
|
||||
RecordModel.check_access_rights('read')
|
||||
except exceptions.AccessError:
|
||||
raise exceptions.UserError(
|
||||
_('Assigned user %s has no access to the document and is not able to handle this activity.',
|
||||
user.display_name))
|
||||
else:
|
||||
try:
|
||||
target_records = self.env[model].browse(activities.mapped('res_id'))
|
||||
target_records.check_access_rule('read')
|
||||
except exceptions.AccessError:
|
||||
raise exceptions.UserError(
|
||||
_('Assigned user %s has no access to the document and is not able to handle this activity.',
|
||||
user.display_name))
|
||||
|
||||
# ------------------------------------------------------
|
||||
# ORM overrides
|
||||
# ------------------------------------------------------
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
activities = super(MailActivity, self).create(vals_list)
|
||||
|
||||
# find partners related to responsible users, separate readable from unreadable
|
||||
if any(user != self.env.user for user in activities.user_id):
|
||||
user_partners = activities.user_id.partner_id
|
||||
readable_user_partners = user_partners._filter_access_rules_python('read')
|
||||
else:
|
||||
readable_user_partners = self.env.user.partner_id
|
||||
|
||||
# when creating activities for other: send a notification to assigned user;
|
||||
# in case of manually done activity also check target has rights on document
|
||||
# otherwise we prevent its creation. Automated activities are checked since
|
||||
# they are integrated into business flows that should not crash.
|
||||
if self.env.context.get('mail_activity_quick_update'):
|
||||
activities_to_notify = self.env['mail.activity']
|
||||
else:
|
||||
activities_to_notify = activities.filtered(lambda act: act.user_id != self.env.user)
|
||||
activities_to_notify.filtered(lambda act: not act.automated)._check_access_assignation()
|
||||
if activities_to_notify:
|
||||
to_sudo = activities_to_notify.filtered(lambda act: act.user_id.partner_id not in readable_user_partners)
|
||||
other = activities_to_notify - to_sudo
|
||||
to_sudo.sudo().action_notify()
|
||||
other.action_notify()
|
||||
|
||||
# subscribe (batch by model and user to speedup)
|
||||
for model, activity_data in activities._classify_by_model().items():
|
||||
per_user = dict()
|
||||
for activity in activity_data['activities'].filtered(lambda act: act.user_id):
|
||||
if activity.user_id not in per_user:
|
||||
per_user[activity.user_id] = [activity.res_id]
|
||||
else:
|
||||
per_user[activity.user_id].append(activity.res_id)
|
||||
for user, res_ids in per_user.items():
|
||||
pids = user.partner_id.ids if user.partner_id in readable_user_partners else user.sudo().partner_id.ids
|
||||
self.env[model].browse(res_ids).message_subscribe(partner_ids=pids)
|
||||
|
||||
# send notifications about activity creation
|
||||
todo_activities = activities.filtered(lambda act: act.date_deadline <= fields.Date.today())
|
||||
if todo_activities:
|
||||
self.env['bus.bus']._sendmany([
|
||||
(activity.user_id.partner_id, 'mail.activity/updated', {'activity_created': True})
|
||||
for activity in todo_activities
|
||||
])
|
||||
return activities
|
||||
|
||||
def read(self, fields=None, load='_classic_read'):
|
||||
""" When reading specific fields, read calls _read that manually applies ir rules
|
||||
(_apply_ir_rules), instead of calling check_access_rule.
|
||||
|
||||
Meaning that our custom rules enforcing from '_filter_access_rules' and
|
||||
'_filter_access_rules_python' are bypassed in that case.
|
||||
To make sure we apply our custom security rules, we force a call to 'check_access_rule'. """
|
||||
|
||||
self.check_access_rule('read')
|
||||
return super(MailActivity, self).read(fields=fields, load=load)
|
||||
|
||||
def write(self, values):
|
||||
if values.get('user_id'):
|
||||
user_changes = self.filtered(lambda activity: activity.user_id.id != values.get('user_id'))
|
||||
pre_responsibles = user_changes.mapped('user_id.partner_id')
|
||||
res = super(MailActivity, self).write(values)
|
||||
|
||||
if values.get('user_id'):
|
||||
if values['user_id'] != self.env.uid:
|
||||
to_check = user_changes.filtered(lambda act: not act.automated)
|
||||
to_check._check_access_assignation()
|
||||
if not self.env.context.get('mail_activity_quick_update', False):
|
||||
user_changes.action_notify()
|
||||
for activity in user_changes:
|
||||
self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id])
|
||||
|
||||
# send bus notifications
|
||||
todo_activities = user_changes.filtered(lambda act: act.date_deadline <= fields.Date.today())
|
||||
if todo_activities:
|
||||
self.env['bus.bus']._sendmany([
|
||||
[partner, 'mail.activity/updated', {'activity_created': True}]
|
||||
for partner in todo_activities.user_id.partner_id
|
||||
])
|
||||
self.env['bus.bus']._sendmany([
|
||||
[partner, 'mail.activity/updated', {'activity_deleted': True}]
|
||||
for partner in pre_responsibles
|
||||
])
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
todo_activities = self.filtered(lambda act: act.date_deadline <= fields.Date.today())
|
||||
if todo_activities:
|
||||
self.env['bus.bus']._sendmany([
|
||||
[partner, 'mail.activity/updated', {'activity_deleted': True}]
|
||||
for partner in todo_activities.user_id.partner_id
|
||||
])
|
||||
return super(MailActivity, self).unlink()
|
||||
|
||||
@api.model
|
||||
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
|
||||
""" Override that adds specific access rights of mail.activity, to remove
|
||||
ids uid could not see according to our custom rules. Please refer to
|
||||
_filter_access_rules_remaining for more details about those rules.
|
||||
|
||||
The method is inspired by what has been done on mail.message. """
|
||||
|
||||
# Rules do not apply to administrator
|
||||
if self.env.is_superuser():
|
||||
return super(MailActivity, self)._search(
|
||||
args, offset=offset, limit=limit, order=order,
|
||||
count=count, access_rights_uid=access_rights_uid)
|
||||
# Perform a super with count as False, to have the ids, not a counter
|
||||
ids = super(MailActivity, self)._search(
|
||||
args, offset=offset, limit=limit, order=order,
|
||||
count=False, access_rights_uid=access_rights_uid)
|
||||
if not ids and count:
|
||||
return 0
|
||||
elif not ids:
|
||||
return ids
|
||||
|
||||
# check read access rights before checking the actual rules on the given ids
|
||||
super(MailActivity, self.with_user(access_rights_uid or self._uid)).check_access_rights('read')
|
||||
|
||||
self.flush_model(['res_model', 'res_id'])
|
||||
activities_to_check = []
|
||||
for sub_ids in self._cr.split_for_in_conditions(ids):
|
||||
self._cr.execute("""
|
||||
SELECT DISTINCT activity.id, activity.res_model, activity.res_id
|
||||
FROM "%s" activity
|
||||
WHERE activity.id = ANY (%%(ids)s) AND activity.res_id != 0""" % self._table, dict(ids=list(sub_ids)))
|
||||
activities_to_check += self._cr.dictfetchall()
|
||||
|
||||
activity_to_documents = {}
|
||||
for activity in activities_to_check:
|
||||
activity_to_documents.setdefault(activity['res_model'], set()).add(activity['res_id'])
|
||||
|
||||
allowed_ids = set()
|
||||
for doc_model, doc_ids in activity_to_documents.items():
|
||||
# fall back on related document access right checks. Use the same as defined for mail.thread
|
||||
# if available; otherwise fall back on read
|
||||
if hasattr(self.env[doc_model], '_mail_post_access'):
|
||||
doc_operation = self.env[doc_model]._mail_post_access
|
||||
else:
|
||||
doc_operation = 'read'
|
||||
DocumentModel = self.env[doc_model].with_user(access_rights_uid or self._uid)
|
||||
right = DocumentModel.check_access_rights(doc_operation, raise_exception=False)
|
||||
if right:
|
||||
valid_docs = DocumentModel.browse(doc_ids)._filter_access_rules(doc_operation)
|
||||
valid_doc_ids = set(valid_docs.ids)
|
||||
allowed_ids.update(
|
||||
activity['id'] for activity in activities_to_check
|
||||
if activity['res_model'] == doc_model and activity['res_id'] in valid_doc_ids)
|
||||
|
||||
if count:
|
||||
return len(allowed_ids)
|
||||
else:
|
||||
# re-construct a list based on ids, because 'allowed_ids' does not keep the original order
|
||||
id_list = [id for id in ids if id in allowed_ids]
|
||||
return id_list
|
||||
|
||||
@api.model
|
||||
def _read_group_raw(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
|
||||
""" The base _read_group_raw method implementation computes a where based on a given domain
|
||||
(_where_calc) and manually applies ir rules (_apply_ir_rules).
|
||||
|
||||
Meaning that our custom rules enforcing from '_filter_access_rules' and
|
||||
'_filter_access_rules_python' are bypassed in that case.
|
||||
|
||||
This overrides re-uses the _search implementation to force the read group domain to allowed
|
||||
ids only, that are computed based on our custom rules (see _filter_access_rules_remaining
|
||||
for more details). """
|
||||
|
||||
# Rules do not apply to administrator
|
||||
if not self.env.is_superuser():
|
||||
allowed_ids = self._search(domain, count=False)
|
||||
if allowed_ids:
|
||||
domain = expression.AND([domain, [('id', 'in', allowed_ids)]])
|
||||
else:
|
||||
# force void result if no allowed ids found
|
||||
domain = expression.AND([domain, [(0, '=', 1)]])
|
||||
|
||||
return super(MailActivity, self)._read_group_raw(
|
||||
domain=domain, fields=fields, groupby=groupby, offset=offset,
|
||||
limit=limit, orderby=orderby, lazy=lazy,
|
||||
)
|
||||
|
||||
def name_get(self):
|
||||
res = []
|
||||
for record in self:
|
||||
name = record.summary or record.activity_type_id.display_name
|
||||
res.append((record.id, name))
|
||||
return res
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Business Methods
|
||||
# ------------------------------------------------------
|
||||
|
||||
def action_notify(self):
|
||||
if not self:
|
||||
return
|
||||
for activity in self:
|
||||
if activity.user_id.lang:
|
||||
# Send the notification in the assigned user's language
|
||||
activity = activity.with_context(lang=activity.user_id.lang)
|
||||
|
||||
model_description = activity.env['ir.model']._get(activity.res_model).display_name
|
||||
body = activity.env['ir.qweb']._render(
|
||||
'mail.message_activity_assigned',
|
||||
{
|
||||
'activity': activity,
|
||||
'model_description': model_description,
|
||||
'is_html_empty': is_html_empty,
|
||||
},
|
||||
minimal_qcontext=True
|
||||
)
|
||||
record = activity.env[activity.res_model].browse(activity.res_id)
|
||||
if activity.user_id:
|
||||
record.message_notify(
|
||||
partner_ids=activity.user_id.partner_id.ids,
|
||||
body=body,
|
||||
record_name=activity.res_name,
|
||||
model_description=model_description,
|
||||
email_layout_xmlid='mail.mail_notification_layout',
|
||||
subject=_('"%(activity_name)s: %(summary)s" assigned to you',
|
||||
activity_name=activity.res_name,
|
||||
summary=activity.summary or activity.activity_type_id.name),
|
||||
subtitles=[_('Activity: %s', activity.activity_type_id.name),
|
||||
_('Deadline: %s', activity.date_deadline.strftime(get_lang(activity.env).date_format))]
|
||||
)
|
||||
|
||||
def action_done(self):
|
||||
""" Wrapper without feedback because web button add context as
|
||||
parameter, therefore setting context to feedback """
|
||||
return self.action_feedback()
|
||||
|
||||
def action_feedback(self, feedback=False, attachment_ids=None):
|
||||
messages, _next_activities = self.with_context(
|
||||
clean_context(self.env.context)
|
||||
)._action_done(feedback=feedback, attachment_ids=attachment_ids)
|
||||
return messages[0].id if messages else False
|
||||
|
||||
def action_done_schedule_next(self):
|
||||
""" Wrapper without feedback because web button add context as
|
||||
parameter, therefore setting context to feedback """
|
||||
return self.action_feedback_schedule_next()
|
||||
|
||||
def action_feedback_schedule_next(self, feedback=False, attachment_ids=None):
|
||||
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_id=self.res_id,
|
||||
default_res_model=self.res_model,
|
||||
)
|
||||
_messages, next_activities = self._action_done(feedback=feedback, attachment_ids=attachment_ids) # will unlink activity, dont access self after that
|
||||
if next_activities:
|
||||
return False
|
||||
return {
|
||||
'name': _('Schedule an Activity'),
|
||||
'context': ctx,
|
||||
'view_mode': 'form',
|
||||
'res_model': 'mail.activity',
|
||||
'views': [(False, 'form')],
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def _action_done(self, feedback=False, attachment_ids=None):
|
||||
""" Private implementation of marking activity as done: posting a message, deleting activity
|
||||
(since done), and eventually create the automatical next activity (depending on config).
|
||||
:param feedback: optional feedback from user when marking activity as done
|
||||
:param attachment_ids: list of ir.attachment ids to attach to the posted mail.message
|
||||
:returns (messages, activities) where
|
||||
- messages is a recordset of posted mail.message
|
||||
- activities is a recordset of mail.activity of forced automically created activities
|
||||
"""
|
||||
# marking as 'done'
|
||||
messages = self.env['mail.message']
|
||||
next_activities_values = []
|
||||
|
||||
# Search for all attachments linked to the activities we are about to unlink. This way, we
|
||||
# can link them to the message posted and prevent their deletion.
|
||||
attachments = self.env['ir.attachment'].search_read([
|
||||
('res_model', '=', self._name),
|
||||
('res_id', 'in', self.ids),
|
||||
], ['id', 'res_id'])
|
||||
|
||||
activity_attachments = defaultdict(list)
|
||||
for attachment in attachments:
|
||||
activity_id = attachment['res_id']
|
||||
activity_attachments[activity_id].append(attachment['id'])
|
||||
|
||||
for model, activity_data in self._classify_by_model().items():
|
||||
records = self.env[model].browse(activity_data['record_ids'])
|
||||
for record, activity in zip(records, activity_data['activities']):
|
||||
# extract value to generate next activities
|
||||
if activity.chaining_type == 'trigger':
|
||||
vals = activity.with_context(activity_previous_deadline=activity.date_deadline)._prepare_next_activity_values()
|
||||
next_activities_values.append(vals)
|
||||
|
||||
# post message on activity, before deleting it
|
||||
activity_message = record.message_post_with_view(
|
||||
'mail.message_activity_done',
|
||||
values={
|
||||
'activity': activity,
|
||||
'feedback': feedback,
|
||||
'display_assignee': activity.user_id != self.env.user
|
||||
},
|
||||
subtype_id=self.env['ir.model.data']._xmlid_to_res_id('mail.mt_activities'),
|
||||
mail_activity_type_id=activity.activity_type_id.id,
|
||||
attachment_ids=[Command.link(attachment_id) for attachment_id in attachment_ids] if attachment_ids else [],
|
||||
)
|
||||
|
||||
# Moving the attachments in the message
|
||||
# TODO: Fix void res_id on attachment when you create an activity with an image
|
||||
# directly, see route /web_editor/attachment/add
|
||||
if activity_attachments[activity.id]:
|
||||
message_attachments = self.env['ir.attachment'].browse(activity_attachments[activity.id])
|
||||
if message_attachments:
|
||||
message_attachments.write({
|
||||
'res_id': activity_message.id,
|
||||
'res_model': activity_message._name,
|
||||
})
|
||||
activity_message.attachment_ids = message_attachments
|
||||
messages += activity_message
|
||||
|
||||
next_activities = self.env['mail.activity']
|
||||
if next_activities_values:
|
||||
next_activities = self.env['mail.activity'].create(next_activities_values)
|
||||
|
||||
self.unlink() # will unlink activity, dont access `self` after that
|
||||
|
||||
return messages, next_activities
|
||||
|
||||
def action_close_dialog(self):
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def action_open_document(self):
|
||||
""" Opens the related record based on the model and ID """
|
||||
self.ensure_one()
|
||||
return {
|
||||
'res_id': self.res_id,
|
||||
'res_model': self.res_model,
|
||||
'target': 'current',
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form',
|
||||
}
|
||||
|
||||
def activity_format(self):
|
||||
activities = self.read()
|
||||
mail_template_ids = set([template_id for activity in activities for template_id in activity["mail_template_ids"]])
|
||||
mail_template_info = self.env["mail.template"].browse(mail_template_ids).read(['id', 'name'])
|
||||
mail_template_dict = dict([(mail_template['id'], mail_template) for mail_template in mail_template_info])
|
||||
for activity in activities:
|
||||
activity['mail_template_ids'] = [mail_template_dict[mail_template_id] for mail_template_id in activity['mail_template_ids']]
|
||||
return activities
|
||||
|
||||
@api.model
|
||||
def get_activity_data(self, res_model, domain):
|
||||
activity_domain = [('res_model', '=', res_model)]
|
||||
if domain:
|
||||
res = self.env[res_model].search(domain)
|
||||
activity_domain.append(('res_id', 'in', res.ids))
|
||||
grouped_activities = self.env['mail.activity'].read_group(
|
||||
activity_domain,
|
||||
['res_id', 'activity_type_id', 'ids:array_agg(id)', 'date_deadline:min(date_deadline)'],
|
||||
['res_id', 'activity_type_id'],
|
||||
lazy=False)
|
||||
# filter out unreadable records
|
||||
if not domain:
|
||||
res_ids = tuple(a['res_id'] for a in grouped_activities)
|
||||
res = self.env[res_model].search([('id', 'in', res_ids)])
|
||||
grouped_activities = [a for a in grouped_activities if a['res_id'] in res.ids]
|
||||
res_id_to_deadline = {}
|
||||
activity_data = defaultdict(dict)
|
||||
for group in grouped_activities:
|
||||
res_id = group['res_id']
|
||||
activity_type_id = (group.get('activity_type_id') or (False, False))[0]
|
||||
res_id_to_deadline[res_id] = group['date_deadline'] if (res_id not in res_id_to_deadline or group['date_deadline'] < res_id_to_deadline[res_id]) else res_id_to_deadline[res_id]
|
||||
state = self._compute_state_from_date(group['date_deadline'], self.user_id.sudo().tz)
|
||||
activity_data[res_id][activity_type_id] = {
|
||||
'count': group['__count'],
|
||||
'ids': group['ids'],
|
||||
'state': state,
|
||||
'o_closest_deadline': group['date_deadline'],
|
||||
}
|
||||
activity_type_infos = []
|
||||
activity_type_ids = self.env['mail.activity.type'].search(
|
||||
['|', ('res_model', '=', res_model), ('res_model', '=', False)])
|
||||
for elem in sorted(activity_type_ids, key=lambda item: item.sequence):
|
||||
mail_template_info = []
|
||||
for mail_template_id in elem.mail_template_ids:
|
||||
mail_template_info.append({"id": mail_template_id.id, "name": mail_template_id.name})
|
||||
activity_type_infos.append([elem.id, elem.name, mail_template_info])
|
||||
|
||||
return {
|
||||
'activity_types': activity_type_infos,
|
||||
'activity_res_ids': sorted(res_id_to_deadline, key=lambda item: res_id_to_deadline[item]),
|
||||
'grouped_activities': activity_data,
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# TOOLS
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
def _classify_by_model(self):
|
||||
""" To ease batch computation of various activities related methods they
|
||||
are classified by model. Activities not linked to a valid record through
|
||||
res_model / res_id are ignored.
|
||||
|
||||
:return dict: for each model having at least one activity in self, have
|
||||
a sub-dict containing
|
||||
* activities: activities related to that model;
|
||||
* record IDs: record linked to the activities of that model, in same
|
||||
order;
|
||||
"""
|
||||
data_by_model = {}
|
||||
for activity in self.filtered(lambda act: act.res_model and act.res_id):
|
||||
if activity.res_model not in data_by_model:
|
||||
data_by_model[activity.res_model] = {
|
||||
'activities': self.env['mail.activity'],
|
||||
'record_ids': [],
|
||||
}
|
||||
data_by_model[activity.res_model]['activities'] += activity
|
||||
data_by_model[activity.res_model]['record_ids'].append(activity.res_id)
|
||||
return data_by_model
|
||||
|
||||
def _prepare_next_activity_values(self):
|
||||
""" Prepare the next activity values based on the current activity record and applies _onchange methods
|
||||
:returns a dict of values for the new activity
|
||||
"""
|
||||
self.ensure_one()
|
||||
vals = self.default_get(self.fields_get())
|
||||
|
||||
vals.update({
|
||||
'previous_activity_type_id': self.activity_type_id.id,
|
||||
'res_id': self.res_id,
|
||||
'res_model': self.res_model,
|
||||
'res_model_id': self.env['ir.model']._get(self.res_model).id,
|
||||
})
|
||||
virtual_activity = self.new(vals)
|
||||
virtual_activity._onchange_previous_activity_type_id()
|
||||
virtual_activity._onchange_activity_type_id()
|
||||
return virtual_activity._convert_to_write(virtual_activity._cache)
|
||||
Loading…
Add table
Add a link
Reference in a new issue