@@ -523,6 +524,18 @@
+
+
+
+
+
diff --git a/addons/crm/__manifest__.py b/addons/crm/__manifest__.py
index b30f5ce5d15..ff62e0ca2a0 100644
--- a/addons/crm/__manifest__.py
+++ b/addons/crm/__manifest__.py
@@ -41,7 +41,6 @@ Dashboard for CRM will include:
'web_tour',
],
'data': [
- 'data/crm_activity_data.xml',
'data/crm_data.xml',
'data/crm_stage_data.xml',
'data/sales_config_settings_data.xml',
@@ -53,7 +52,6 @@ Dashboard for CRM will include:
'security/ir.model.access.csv',
'wizard/base_partner_merge_views.xml',
- 'wizard/crm_activity_log_views.xml',
'wizard/crm_lead_lost_views.xml',
'wizard/crm_lead_to_opportunity_views.xml',
'wizard/crm_merge_opportunities_views.xml',
@@ -64,7 +62,6 @@ Dashboard for CRM will include:
'views/crm_templates.xml',
'views/crm_views.xml',
- 'views/crm_activity_views.xml',
'views/crm_stage_views.xml',
'views/crm_lead_views.xml',
'views/calendar_views.xml',
@@ -74,8 +71,8 @@ Dashboard for CRM will include:
],
'demo': [
'data/crm_demo.xml',
+ 'data/mail_activity_demo.xml',
'data/crm_lead_demo.xml',
- 'data/crm_activity_demo.xml',
'data/base_action_rule_demo.xml',
],
'css': ['static/src/css/crm.css'],
diff --git a/addons/crm/data/crm_activity_data.xml b/addons/crm/data/crm_activity_data.xml
deleted file mode 100644
index cb45e243e97..00000000000
--- a/addons/crm/data/crm_activity_data.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
- Email
-
-
- Call
-
-
- Task
-
-
-
-
diff --git a/addons/crm/data/crm_activity_demo.xml b/addons/crm/data/crm_activity_demo.xml
deleted file mode 100644
index 6c1bb4a25ba..00000000000
--- a/addons/crm/data/crm_activity_demo.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
- Follow-up Quote
- Follow-up quote
- 30
-
-
-
- Make Quote
- Send a quotation
- 15
-
-
-
-
- Call for Demo
- 10
- Call for a demo
-
-
-
-
- 5
-
-
-
-
-
- 15
-
-
-
-
-
diff --git a/addons/crm/data/crm_lead_demo.xml b/addons/crm/data/crm_lead_demo.xml
index a4b40a6d984..e0d65f889c8 100644
--- a/addons/crm/data/crm_lead_demo.xml
+++ b/addons/crm/data/crm_lead_demo.xml
@@ -374,9 +374,6 @@ Andrew]]>
1
-
-
- Meeting to go over pricing information.
@@ -402,9 +399,6 @@ Andrew]]>
2
-
-
- Send Catalog by Email
@@ -427,9 +421,6 @@ Andrew]]>
1
-
-
- Call to get system requirements
@@ -516,9 +507,6 @@ Andrew]]>
2
-
-
- Convert to quote
@@ -542,9 +530,6 @@ Andrew]]>
1
-
-
- Send our service pricelist
@@ -577,9 +562,6 @@ Andrew]]>
1
-
-
- Call to get training needs
@@ -628,9 +610,6 @@ Andrew]]>
1
-
-
- Followup on the proposal
@@ -653,7 +632,6 @@ Andrew]]>
+1 312 349 23242
-
@@ -672,7 +650,6 @@ Andrew]]>
Cannon Hill Park2
-
@@ -691,7 +668,6 @@ Andrew]]>
1
-
@@ -708,8 +684,6 @@ Andrew]]>
0
-
-
@@ -728,8 +702,6 @@ Andrew]]>
Lima0
-
-
@@ -753,9 +725,6 @@ Andrew]]>
C1414CMS Capital Federal2
-
-
- Conf call with technical service
@@ -778,9 +747,6 @@ Andrew]]>
2
-
-
- Send Catalog by Email
@@ -804,9 +770,6 @@ Andrew]]>
2
-
-
- Send Catalog by Email
@@ -817,5 +780,100 @@ Andrew]]>
eval="[ref('crm_case_20'), ref('crm_case_23'), ref('crm_case_27')],
{'install_mode': True}"/>
+
+
+
+
+
+ Meeting to go over pricing information.
+
+
+
+
+
+
+
+ Send Catalog by Email
+
+
+
+
+
+
+ Call to get system requirements
+
+
+
+
+
+
+ Convert to quote
+
+
+
+
+
+
+ Send our service pricelist
+
+
+
+
+
+
+ Call to get training needs
+
+
+
+
+
+
+ Followup on the proposal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Conf call with technical service
+
+
+
+
+
+
+ Send Catalog by Email
+
+
diff --git a/addons/crm/data/mail_activity_demo.xml b/addons/crm/data/mail_activity_demo.xml
new file mode 100644
index 00000000000..320c3b4baf2
--- /dev/null
+++ b/addons/crm/data/mail_activity_demo.xml
@@ -0,0 +1,24 @@
+
+
+
+ Follow-up Quote
+ fa-file-text-o
+
+ Follow-up quote
+ 30
+
+
+ Make Quote
+ fa-file-text-o
+
+ Send a quotation
+ 15
+
+
+ Call for Demo
+ fa-phone
+
+ 10
+ Call for a demo
+
+
diff --git a/addons/crm/models/__init__.py b/addons/crm/models/__init__.py
index 6edf901680d..02caa223da7 100644
--- a/addons/crm/models/__init__.py
+++ b/addons/crm/models/__init__.py
@@ -3,7 +3,6 @@
import res_users
import calendar
-import crm_activity
import crm_lead
import crm_stage
import crm_team
diff --git a/addons/crm/models/crm_activity.py b/addons/crm/models/crm_activity.py
deleted file mode 100644
index 4be550c5564..00000000000
--- a/addons/crm/models/crm_activity.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import api, fields, models
-
-
-class CrmActivity(models.Model):
- ''' CrmActivity is a model introduced in Odoo v9 that models activities
- performed in CRM, like phone calls, sending emails, making demonstrations,
- ... Users are able to configure their custom activities.
-
- Each activity can configure recommended next activities. This allows to model
- light custom workflows. This way sales manager can configure their crm
- workflow that salepersons will use in their daily job.
-
- CrmActivity inherits from mail.message.subtype. This allows users to follow
- some activities through subtypes. Each activity will generate messages with
- the matching subtypes, allowing reporting and statistics computation based
- on mail.message.subtype model. '''
-
- _name = 'crm.activity'
- _description = 'CRM Activity'
- _inherits = {'mail.message.subtype': 'subtype_id'}
- _rec_name = 'name'
- _order = "sequence"
-
- days = fields.Integer('Number of days', default=0,
- help='Number of days before executing the action, allowing you to plan the date of the action.')
- sequence = fields.Integer('Sequence', default=0)
- team_id = fields.Many2one('crm.team', string='Sales Team')
- subtype_id = fields.Many2one('mail.message.subtype', string='Message Subtype', required=True, ondelete='cascade')
- recommended_activity_ids = fields.Many2many(
- 'crm.activity', 'crm_activity_rel', 'activity_id', 'recommended_id',
- string='Recommended Next Activities')
- preceding_activity_ids = fields.Many2many(
- 'crm.activity', 'crm_activity_rel', 'recommended_id', 'activity_id',
- string='Preceding Activities')
-
- # setting a default value on inherited fields is a bit involved
- res_model = fields.Char('Model', related='subtype_id.res_model', inherited=True, default='crm.lead')
- internal = fields.Boolean('Internal Only', related='subtype_id.internal', inherited=True, default=True)
- default = fields.Boolean('Default', related='subtype_id.default', inherited=True, default=False)
-
- @api.multi
- def unlink(self):
- activities = self.search([('subtype_id', '=', self.subtype_id.id)])
- # to ensure that the subtype is only linked the current activity
- if len(activities) == 1:
- self.subtype_id.unlink()
- return super(CrmActivity, self).unlink()
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index 86651ef22c1..b2ed22ba7d4 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -52,11 +52,10 @@ CRM_LEAD_FIELDS_TO_MERGE = [
class Lead(FormatAddress, models.Model):
-
_name = "crm.lead"
_description = "Lead/Opportunity"
- _order = "priority desc,date_action,id desc"
- _inherit = ['mail.thread', 'utm.mixin']
+ _order = "priority desc,activity_date_deadline,id desc"
+ _inherit = ['mail.thread', 'mail.activity.mixin', 'utm.mixin']
_mail_mass_mailing = _('Leads / Opportunities')
def _default_probability(self):
@@ -116,12 +115,6 @@ class Lead(FormatAddress, models.Model):
probability = fields.Float('Probability', group_operator="avg", default=lambda self: self._default_probability())
planned_revenue = fields.Float('Expected Revenue', track_visibility='always')
date_deadline = fields.Date('Expected Closing', help="Estimate of the date on which the opportunity will be won.")
-
- # CRM Actions
- next_activity_id = fields.Many2one("crm.activity", string="Next Activity", index=True)
- date_action = fields.Date('Next Activity Date', index=True)
- title_action = fields.Char('Next Activity Summary')
-
color = fields.Integer('Color Index', default=0)
partner_address_name = fields.Char('Partner Contact Name', related='partner_id.name', readonly=True)
partner_address_email = fields.Char('Partner Contact Email', related='partner_id.email', readonly=True)
@@ -170,8 +163,8 @@ class Lead(FormatAddress, models.Model):
today = date.today()
for lead in self:
kanban_state = 'grey'
- if lead.date_action:
- lead_date = fields.Date.from_string(lead.date_action)
+ if lead.activity_date_deadline:
+ lead_date = fields.Date.from_string(lead.activity_date_deadline)
if lead_date >= today:
kanban_state = 'green'
else:
@@ -270,18 +263,6 @@ class Lead(FormatAddress, models.Model):
if self.state_id:
self.country_id = self.state_id.country_id.id
- @api.onchange('next_activity_id')
- def _onchange_next_activity_id(self):
- values = {
- 'title_action': False,
- 'date_action': False,
- }
- if self.next_activity_id:
- values['title_action'] = self.next_activity_id.description
- if self.next_activity_id.days:
- values['date_action'] = fields.Datetime.to_string(datetime.now() + timedelta(days=self.next_activity_id.days))
- self.update(values)
-
# ----------------------------------------
# ORM override (CRUD, fields_view_get, ...)
# ----------------------------------------
@@ -934,13 +915,13 @@ class Lead(FormatAddress, models.Model):
if date_deadline < date.today():
result['closing']['overdue'] += 1
# Next activities
- if opp.next_activity_id and opp.date_action:
- date_action = fields.Date.from_string(opp.date_action)
- if date_action == date.today():
+ for activity in opp.activity_ids:
+ date_deadline = fields.Date.from_string(activity.date_deadline)
+ if date_deadline == date.today():
result['activity']['today'] += 1
- if date.today() <= date_action <= date.today() + timedelta(days=7):
+ if date.today() <= date_deadline <= date.today() + timedelta(days=7):
result['activity']['next_7_days'] += 1
- if date_action < date.today():
+ if date_deadline < date.today():
result['activity']['overdue'] += 1
# Won in Opportunities
if opp.date_closed:
@@ -957,19 +938,19 @@ class Lead(FormatAddress, models.Model):
# crm.activity is a very messy model so we need to do that in order to retrieve the actions done.
self._cr.execute("""
SELECT
- m.id,
- m.subtype_id,
- m.date,
- l.user_id,
- l.type
- FROM mail_message M
- LEFT JOIN crm_lead L ON (M.res_id = L.id)
- INNER JOIN crm_activity A ON (M.subtype_id = A.subtype_id)
+ mail_message.id,
+ mail_message.subtype_id,
+ mail_message.mail_activity_type_id,
+ mail_message.date,
+ crm_lead.user_id,
+ crm_lead.type
+ FROM mail_message
+ LEFT JOIN crm_lead ON (mail_message.res_id = crm_lead.id)
+ INNER JOIN mail_activity_type activity_type ON (mail_message.mail_activity_type_id = activity_type.id)
WHERE
- (M.model = 'crm.lead') AND (L.user_id = %s) AND (L.type = 'opportunity')
+ (mail_message.model = 'crm.lead') AND (crm_lead.user_id = %s) AND (crm_lead.type = 'opportunity')
""", (self._uid,))
activites_done = self._cr.dictfetchall()
-
for activity in activites_done:
if activity['date']:
date_act = fields.Date.from_string(activity['date'])
diff --git a/addons/crm/report/crm_activity_report.py b/addons/crm/report/crm_activity_report.py
index dd2dbc3925f..976886a3f5a 100644
--- a/addons/crm/report/crm_activity_report.py
+++ b/addons/crm/report/crm_activity_report.py
@@ -18,7 +18,8 @@ class ActivityReport(models.Model):
team_id = fields.Many2one('crm.team', 'Sales Team', readonly=True)
lead_id = fields.Many2one('crm.lead', "Lead", readonly=True)
subject = fields.Char('Summary', readonly=True)
- subtype_id = fields.Many2one('mail.message.subtype', 'Activity', readonly=True)
+ subtype_id = fields.Many2one('mail.message.subtype', 'Subtype', readonly=True)
+ mail_activity_type_id = fields.Many2one('mail.activity.type', 'Activity Type', readonly=True)
country_id = fields.Many2one('res.country', 'Country', readonly=True)
company_id = fields.Many2one('res.company', 'Company', readonly=True)
stage_id = fields.Many2one('crm.stage', 'Stage', readonly=True)
@@ -37,6 +38,7 @@ class ActivityReport(models.Model):
select
m.id,
m.subtype_id,
+ m.mail_activity_type_id,
m.author_id,
m.date,
m.subject,
@@ -57,5 +59,5 @@ class ActivityReport(models.Model):
on
(m.res_id = l.id)
WHERE
- (m.model = 'crm.lead')
+ (m.model = 'crm.lead' AND m.mail_activity_type_id IS NOT NULL)
)""")
diff --git a/addons/crm/report/crm_activity_report_views.xml b/addons/crm/report/crm_activity_report_views.xml
index 78c6c3f68b4..689d9fa01f4 100644
--- a/addons/crm/report/crm_activity_report_views.xml
+++ b/addons/crm/report/crm_activity_report_views.xml
@@ -6,7 +6,7 @@
crm.activity.report
-
+
@@ -17,7 +17,7 @@
crm.activity.report
-
+
@@ -28,7 +28,7 @@
crm.activity.report
-
+
@@ -58,7 +58,7 @@
crm.activity.reportformpivot,graph
- {'search_default_my_activity': True}
+ {'search_default_activities_my': True}[]
diff --git a/addons/crm/security/ir.model.access.csv b/addons/crm/security/ir.model.access.csv
index 919f2c7a17d..b1490fb08e2 100644
--- a/addons/crm/security/ir.model.access.csv
+++ b/addons/crm/security/ir.model.access.csv
@@ -18,9 +18,6 @@ access_crm_lead_tag_manager,crm_lead_tag manager,model_crm_lead_tag,sales_team.g
access_crm_lost_reason_manager,crm.lost.reason.manager,model_crm_lost_reason,sales_team.group_sale_manager,1,1,1,1
access_crm_lost_reason_salesman,crm.lost.reason.salesman,model_crm_lost_reason,sales_team.group_sale_salesman,1,0,0,0
access_crm_lost_reason_user,crm.lost.reason.user,model_crm_lost_reason,base.group_user,1,0,0,0
-access_crm_activity_user,crm.activity.user,model_crm_activity,base.group_user,1,1,1,0
-access_crm_activity_manager,crm.activity.manager,model_crm_activity,sales_team.group_sale_manager,1,1,1,1
-access_crm_activity_salesman,crm.activity.salesman,model_crm_activity,sales_team.group_sale_salesman,1,1,1,0
access_crm_activity_report_user,crm.activity.report.user,model_crm_activity_report,base.group_user,1,0,0,0
access_calendar_event_manager,calendar.event.manager,calendar.model_calendar_event,sales_team.group_sale_manager,1,1,1,1
access_calendar_event,calendar.event,calendar.model_calendar_event,sales_team.group_sale_salesman,1,1,1,0
diff --git a/addons/crm/static/src/js/tour.js b/addons/crm/static/src/js/tour.js
index 4ad9b8093f2..7d27e6205d3 100644
--- a/addons/crm/static/src/js/tour.js
+++ b/addons/crm/static/src/js/tour.js
@@ -35,18 +35,22 @@ tour.register('crm_tour', {
position: "right",
run: "drag_and_drop .o_opportunity_kanban .o_kanban_group:eq(2) ",
}, {
- trigger: ".o_kanban_record:not(.o_updating) .oe_kanban_status_grey",
+ trigger: ".o_kanban_record:not(.o_updating) .o_activity_color_default",
extra_trigger: ".o_opportunity_kanban",
- content: _t("This opportunity has no next activity scheduled. Click to set one."),
+ content: _t("This opportunity has no activity planned. Click to check them."),
position: "bottom"
}, {
- trigger: ".o_recommended_activity .o_radio_item",
+ trigger: ".o_schedule_activity",
+ extra_trigger: ".o_opportunity_kanban",
+ content: _t("Schedule an activity by clicking here"),
+ position: "bottom"
+}, {
+ trigger: ".modal-body .o_form_field_many2one",
extra_trigger: ".o_opportunity_kanban",
content: _t("
You will be able to customize your followup activities. Examples:
introductory email
call 10 days after
second call 3 days after, ...
Select a standard activity for now on.
"),
- position: "left",
+ position: "bottom",
run: function (actions) {
- actions.auto(this.$anchor.children("input").first());
- actions.auto(".modal-footer .btn-primary");
+ actions.auto(".modal-footer .btn-default");
},
}, {
trigger: ".o_kanban_record",
diff --git a/addons/crm/tests/test_crm_activity.py b/addons/crm/tests/test_crm_activity.py
index 01747143ace..b18840e75f7 100644
--- a/addons/crm/tests/test_crm_activity.py
+++ b/addons/crm/tests/test_crm_activity.py
@@ -3,52 +3,47 @@
from .common import TestCrmCases
from odoo import fields
-from datetime import date
+from datetime import datetime, timedelta
-class TestCrmActivity(TestCrmCases):
+class TestCrmMailActivity(TestCrmCases):
def setUp(self):
- super(TestCrmActivity, self).setUp()
+ super(TestCrmMailActivity, self).setUp()
# Set up activities
- Activity = self.env['crm.activity']
- self.activity3 = Activity.create({
+ lead_model_id = self.env['ir.model'].search([('model', '=', 'crm.lead')]).id
+ ActivityType = self.env['mail.activity.type']
+ self.activity3 = ActivityType.create({
'name': 'Celebrate the sale',
'days': 3,
- 'description': 'ACT 3 : Beers for everyone because I am a good salesman !',
- 'internal': True,
- 'res_model': 'crm.lead',
+ 'summary': 'ACT 3 : Beers for everyone because I am a good salesman !',
+ 'res_model_id': lead_model_id,
})
- self.activity2 = Activity.create({
+ self.activity2 = ActivityType.create({
'name': 'Call for Demo',
'days': 6,
- 'description': 'ACT 2 : I want to show you my ERP !',
- 'internal': True,
- 'res_model': 'crm.lead',
- 'recommended_activity_ids': [(6, 0, [self.activity3.id])],
+ 'summary': 'ACT 2 : I want to show you my ERP !',
+ 'res_model_id': lead_model_id,
})
- self.activity1 = Activity.create({
+ self.activity1 = ActivityType.create({
'name': 'Initial Contact',
'days': 5,
- 'description': 'ACT 1 : Presentation, barbecue, ... ',
- 'internal': True,
- 'res_model': 'crm.lead',
- 'recommended_activity_ids': [(6, 0, [self.activity2.id])],
+ 'summary': 'ACT 1 : Presentation, barbecue, ... ',
+ 'res_model_id': lead_model_id,
})
# I create an opportunity, as salesman
self.partner_client = self.env.ref("base.res_partner_1")
- Lead = self.env['crm.lead'].sudo(self.crm_salesman.id)
- self.lead = Lead.create({
+ self.lead = self.env['crm.lead'].sudo(self.crm_salesman.id).create({
+ 'name': 'Test Opp',
'type': 'opportunity',
- 'name': 'Test Opportunity Activity Log',
'partner_id': self.partner_client.id,
'team_id': self.env.ref("sales_team.team_sales_department").id,
'user_id': self.crm_salesman.id,
})
def test_crm_activity_recipients(self):
- """ This test case check :
+ """ This test case checks
- no internal subtype followed by client
- activity subtype are not default ones
- only activity followers are recipients when this kind of activity is logged
@@ -60,73 +55,60 @@ class TestCrmActivity(TestCrmCases):
self.lead.message_subscribe([self.partner_client.id])
# Check the client is not follower of any internal subtype
- is_internal_subtype_for_client = self.lead.message_follower_ids.filtered(lambda fol: fol.partner_id.id == self.partner_client.id).mapped('subtype_ids.internal')
- self.assertFalse(any(is_internal_subtype_for_client), 'Partner client is following an internal subtype')
+ internal_subtypes = self.lead.message_follower_ids.filtered(lambda fol: fol.partner_id == self.partner_client).mapped('subtype_ids').filtered(lambda subtype: subtype.internal)
+ self.assertFalse(internal_subtypes)
# Add sale manager as follower of default subtypes
self.lead.message_subscribe([self.crm_salemanager.partner_id.id])
- # Make the sale manager follower of the activity subtype
- manager_follower = self.env['mail.followers'].sudo().search([('res_model', '=', 'crm.lead'), ('res_id', '=', self.lead.id), ('partner_id', '=', self.crm_salemanager.partner_id.id)])
- manager_follower.write({
- 'subtype_ids': [(4, activity.subtype_id.id)]
- })
- # Instanciate wizard, trigger onchange lead and set the next activity
- ActivityLogWizard = self.env['crm.activity.log'].sudo(self.crm_salesman.id)
- wizard = ActivityLogWizard.create({
+ activity = self.env['mail.activity'].sudo(self.crm_salesman.id).create({
+ 'activity_type_id': self.activity1.id,
'note': 'Content of the activity to log',
- 'lead_id': self.lead.id,
+ 'res_id': self.lead.id,
+ 'res_model_id': self.env.ref('crm.model_crm_lead').id,
})
- wizard.onchange_lead_id()
- wizard.write({
- 'next_activity_id': activity.id,
- })
- wizard.action_log()
+ activity._onchange_activity_type_id()
+ self.assertEqual(self.lead.activity_type_id, self.activity1)
+ self.assertEqual(self.lead.activity_summary, self.activity1.summary)
+ # self.assertEqual(self.lead.activity_date_deadline, self.activity1.summary)
- # Check message recipients
+ # mark as done, check lead and posted message
+ activity.action_done()
+ self.assertFalse(self.lead.activity_type_id.id)
+ self.assertFalse(self.lead.activity_ids)
activity_message = self.lead.message_ids[0]
- self.assertEqual(activity_message.needaction_partner_ids, self.crm_salemanager.partner_id, 'Only the crm manager should be notified by the activity')
- self.assertEqual(self.lead.next_activity_id.id, False, 'When logging activity, the next activity planned is erased')
+ self.assertEqual(activity_message.needaction_partner_ids, self.crm_salemanager.partner_id)
+ self.assertEqual(activity_message.subtype_id, self.env.ref('mail.mt_activities'))
def test_crm_activity_next_action(self):
""" This test case set the next activity on a lead, log another, and schedule a third. """
# Add the next activity (like we set it from a form view)
- self.lead.write({
- 'next_activity_id': self.activity1.id
+ lead_model_id = self.env['ir.model'].search([('model', '=', 'crm.lead')]).id
+ activity = self.env['mail.activity'].sudo(self.crm_salesman.id).create({
+ 'activity_type_id': self.activity1.id,
+ 'summary': 'My Own Summary',
+ 'res_id': self.lead.id,
+ 'res_model_id': lead_model_id,
})
- self.lead._onchange_next_activity_id()
+ activity._onchange_activity_type_id()
# Check the next activity is correct
- self.assertEqual(self.lead.title_action, self.activity1.description, 'Activity title should be the same on the lead and on the chosen activity')
+ self.assertEqual(self.lead.activity_summary, activity.summary)
+ self.assertEqual(self.lead.activity_type_id, activity.activity_type_id)
+ # self.assertEqual(fields.Datetime.from_string(self.lead.activity_date_deadline), datetime.now() + timedelta(days=activity.activity_type_id.days))
- # Instanciate the wizard to schedule the next activity
- wizard = self.env['crm.activity.log'].sudo(self.crm_salesman.id).create({
+ activity.write({
+ 'activity_type_id': self.activity2.id,
+ 'summary': '',
'note': 'Content of the activity to log',
- 'lead_id': self.lead.id,
})
- wizard.onchange_lead_id()
- wizard.write({
- 'next_activity_id': self.activity2.id,
- })
- wizard.action_log()
+ activity._onchange_activity_type_id()
+
+ self.assertEqual(self.lead.activity_summary, activity.activity_type_id.summary)
+ self.assertEqual(self.lead.activity_type_id, activity.activity_type_id)
+ # self.assertEqual(fields.Datetime.from_string(self.lead.activity_date_deadline), datetime.now() + timedelta(days=activity.activity_type_id.days))
+
+ activity.action_done()
# Check the next activity on the lead has been removed
- self.assertFalse(self.lead.next_activity_id.id, 'No next activity should be set on lead, since we jsut log another activity')
-
- # Instanciate the wizard to schedule the next activity
- self.env['crm.activity.log'].sudo(self.crm_salesman.id).create({
- 'next_activity_id': self.activity3.id,
- 'note': 'Content of the activity to log',
- 'lead_id': self.lead.id,
- })
- wizard.onchange_lead_id()
- wizard.write({
- 'next_activity_id': self.activity3.id,
- })
- wizard.onchange_next_activity_id()
- wizard.action_schedule()
-
- # Check the activity is well scheldule on lead
- delta_days = (fields.Date.from_string(self.lead.date_action) - date.today()).days
- self.assertEqual(self.activity3.days, delta_days, 'The action date should be in the number of days set up on the activity 3')
- self.assertEqual(self.lead.title_action, self.activity3.description, 'Activity title should be the same on the lead and on the activity 3')
+ self.assertFalse(self.lead.activity_type_id)
diff --git a/addons/crm/views/crm_activity_views.xml b/addons/crm/views/crm_activity_views.xml
deleted file mode 100644
index 0950ccec890..00000000000
--- a/addons/crm/views/crm_activity_views.xml
+++ /dev/null
@@ -1,67 +0,0 @@
-
-
-
-
- crm.activity.search
- crm.activity
-
-
-
-
-
-
-
-
-
- crm.activity.form
- crm.activity
-
-
-
-
-
-
- crm.activity.tree
- crm.activity
-
-
-
-
-
-
-
-
-
-
-
- Activities
- crm.activity
- form
- tree,form
-
-
@@ -316,6 +330,22 @@
+
+
+ My Next Activities
+ hr.applicant
+ form
+ tree,form,kanban,calendar
+
+ {'search_default_activities_my': 1}
+ [('activity_date_deadline', '!=', False)]
+
+
+ Currently there are not any activity scheduled.
+
+
+
+
Jobs Sourceshr.recruitment.source
@@ -499,7 +529,7 @@
process and follow up all operations: meetings, interviews, etc.
Applicants and their attached CV are created automatically when an email is sent.
- If you install the document management modules, all resumes are indexed automatically,
+ If you install the document management modules, all resumes are indexed automatically,
so that you can easily search through their content.
@@ -526,7 +556,7 @@
-
+ graph
@@ -549,7 +579,18 @@
+ id="menu_crm_case_categ0_act_job" sequence="2"/>
+
+
+
+
diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py
index 36fc7771e9f..bfc0d78008b 100644
--- a/addons/mail/__manifest__.py
+++ b/addons/mail/__manifest__.py
@@ -36,10 +36,12 @@ Main Features
'views/mail_followers_views.xml',
'views/mail_channel_views.xml',
'views/mail_shortcode_views.xml',
+ 'views/mail_activity_views.xml',
'views/res_config_views.xml',
'data/mail_data.xml',
'data/mail_channel_data.xml',
'data/mail_shortcode_data.xml',
+ 'data/mail_activity_data.xml',
'security/mail_security.xml',
'security/ir.model.access.csv',
'views/mail_alias_views.xml',
@@ -57,6 +59,7 @@ Main Features
'installable': True,
'application': True,
'qweb': [
+ 'static/src/xml/activity.xml',
'static/src/xml/client_action.xml',
'static/src/xml/composer.xml',
'static/src/xml/chatter.xml',
@@ -66,5 +69,6 @@ Main Features
'static/src/xml/chat_window.xml',
'static/src/xml/extended_chat_window.xml',
'static/src/xml/announcement.xml',
+ 'static/src/xml/web_kanban_activity.xml',
],
}
diff --git a/addons/mail/data/mail_activity_data.xml b/addons/mail/data/mail_activity_data.xml
new file mode 100644
index 00000000000..8e9a213dcff
--- /dev/null
+++ b/addons/mail/data/mail_activity_data.xml
@@ -0,0 +1,27 @@
+
+
+
+
+ Email
+ fa-envelope
+ Send an Email
+
+
+ Call
+ fa-phone
+ Make a Call
+ 2
+
+
+ Meeting
+ fa-users
+ Reminder for meeting
+
+
+ Todo
+ fa-tasks
+ Task to perform
+ 5
+
+
+
diff --git a/addons/mail/data/mail_data.xml b/addons/mail/data/mail_data.xml
index 4ca940fecd1..aa4cae02f30 100644
--- a/addons/mail/data/mail_data.xml
+++ b/addons/mail/data/mail_data.xml
@@ -56,6 +56,12 @@
+
+ Activities
+
+
+
+
diff --git a/addons/mail/models/__init__.py b/addons/mail/models/__init__.py
index 2426a6dc9e6..486b687eeb2 100644
--- a/addons/mail/models/__init__.py
+++ b/addons/mail/models/__init__.py
@@ -6,6 +6,7 @@ import mail_alias
import mail_followers
import mail_notification
import mail_message
+import mail_activity
import mail_mail
import mail_thread
import mail_channel
diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py
new file mode 100644
index 00000000000..8f72110a8b9
--- /dev/null
+++ b/addons/mail/models/mail_activity.py
@@ -0,0 +1,226 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from datetime import date, datetime, timedelta
+
+from odoo import api, fields, models
+
+
+class MailActivityType(models.Model):
+ """ Activity Types are used to categorize activities. Each type is a different
+ kind of activity e.g. call, mail, meeting. An activity can be generic i.e.
+ available for all models using activities; or specific to a model in which
+ case res_model_id field should be used. """
+ _name = 'mail.activity.type'
+ _description = 'Activity Type'
+ _rec_name = 'name'
+ _order = 'sequence'
+
+ name = fields.Char('Name', required=True, translate=True)
+ summary = fields.Char('Summary', translate=True)
+ sequence = fields.Integer('Sequence', default=10)
+ days = fields.Integer(
+ '# Days', default=0,
+ help='Number of days before executing the action. It allows to plan the action deadline.')
+ icon = fields.Char('Icon', help="Font awesome icon e.g. fa-tasks")
+ res_model_id = fields.Many2one(
+ 'ir.model', 'Model', index=True,
+ help='Specify a model if the activity should be specific to a model'
+ 'and not available when managing activities for other models.')
+
+
+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'
+ _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'].sudo().search([('model', '=', res['res_model'])]).id
+ return res
+
+ # owner
+ res_id = fields.Integer('Related Document ID', index=True, required=True)
+ res_model_id = fields.Many2one(
+ 'ir.model', 'Related Document Model',
+ index=True, ondelete='cascade', required=True)
+ res_model = fields.Char(
+ 'Related Document Model',
+ index=True, related='res_model_id.model', store=True, readonly=True)
+ res_name = fields.Char(
+ 'Document Name', compute='_compute_res_name', store=True,
+ help="Display name of the related document.", readonly=True)
+ # activity
+ activity_type_id = fields.Many2one(
+ 'mail.activity.type', 'Activity',
+ domain="['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]")
+ icon = fields.Char('Icon', related='activity_type_id.icon')
+ summary = fields.Char('Summary')
+ note = fields.Html('Note')
+ date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.today)
+ # description
+ user_id = fields.Many2one(
+ 'res.users', 'Assigned to',
+ default=lambda self: self.env.user,
+ index=True, required=True)
+ state = fields.Selection([
+ ('overdue', 'Overdue'),
+ ('today', 'Today'),
+ ('planned', 'Planned')], 'State',
+ compute='_compute_state')
+
+ @api.depends('res_model', 'res_id')
+ def _compute_res_name(self):
+ for activity in self:
+ activity.res_name = self.env[activity.res_model].browse(activity.res_id).name_get()[0][1]
+
+ @api.depends('date_deadline')
+ def _compute_state(self):
+ today = date.today()
+ for record in self.filtered(lambda activity: activity.date_deadline):
+ date_deadline = fields.Date.from_string(record.date_deadline)
+ diff = (date_deadline - today)
+ if diff.days == 0:
+ record.state = 'today'
+ elif diff.days < 0:
+ record.state = 'overdue'
+ else:
+ record.state = 'planned'
+
+ @api.onchange('activity_type_id')
+ def _onchange_activity_type_id(self):
+ if self.activity_type_id:
+ if not self.summary:
+ self.summary = self.activity_type_id.summary
+ self.date_deadline = (datetime.now() + timedelta(days=self.activity_type_id.days))
+
+ @api.model
+ def create(self, values):
+ activity = super(MailActivity, self).create(values)
+ activity.post_assignation_message()
+ return activity
+
+ def write(self, values):
+ res = super(MailActivity, self).write(values)
+ if values.get('user_id'):
+ self.post_assignation_message()
+ return res
+
+ def post_assignation_message(self):
+ for activity in self:
+ record = self.env[activity.res_model].browse(activity.res_id)
+ record.with_context(
+ mail_post_autofollow=True
+ ).message_post_with_view(
+ 'mail.message_activity_assigned',
+ values={'activity': activity},
+ partner_ids=[activity.user_id.partner_id.id],
+ subtype_id=self.env.ref('mail.mt_note').id,
+ )
+
+ @api.multi
+ def action_done(self):
+ message = self.env['mail.message']
+ for activity in self:
+ record = self.env[activity.res_model].browse(activity.res_id)
+ record.message_post_with_view(
+ 'mail.message_activity_done',
+ values={'activity': activity},
+ subtype_id=self.env.ref('mail.mt_activities').id,
+ mail_activity_type_id=activity.activity_type_id.id,
+ )
+ message |= record.message_ids[0]
+
+ self.unlink()
+ return message.ids and message.ids[0] or False
+
+ @api.multi
+ def action_close_dialog(self):
+ return {'type': 'ir.actions.act_window_close'}
+
+
+class MailActivityMixin(models.AbstractModel):
+ """ Mail Activity Mixin is a mixin class to use if you want to add activities
+ management on a model. It works like the mail.thread mixin. It defines
+ an activity_ids one2many field toward activities using res_id and res_model_id.
+ Various related / computed fields are also added to have a global status of
+ activities on documents.
+
+ Activities come with a new JS widget for the form view. It is integrated in the
+ Chatter widget although it is a separate widget. It displays activities linked
+ to the current record and allow to schedule, edit and mark done activities.
+ Use widget="mail_activity" on activity_ids field in form view to use it.
+
+ There is also a kanban widget defined. It defines a small widget to integrate
+ in kanban vignettes. It allow to manage activities directly from the kanban
+ view. Use widget="kanban_activity" on activitiy_ids field in kanban view to
+ use it."""
+ _name = 'mail.activity.mixin'
+ _description = 'Activity Mixin'
+
+ activity_ids = fields.One2many(
+ 'mail.activity', 'res_id', 'Activities',
+ auto_join=True,
+ domain=lambda self: [('res_model', '=', self._name)])
+ activity_state = fields.Selection([
+ ('overdue', 'Overdue'),
+ ('today', 'Today'),
+ ('planned', 'Planned')], string='State',
+ compute='_compute_activity_state',
+ help='Status based on activities\nOverdue: Due date is already passed\n'
+ 'Today: Activity date is today\nPlanned: Future activities.')
+ activity_user_id = fields.Many2one(
+ 'res.users', 'Responsible',
+ related='activity_ids.user_id',
+ search='_search_activity_user_id')
+ activity_type_id = fields.Many2one(
+ 'mail.activity.type', 'Next Activity Type',
+ related='activity_ids.activity_type_id',
+ search='_search_activity_type_id')
+ activity_date_deadline = fields.Date(
+ 'Next Activity Deadline', related='activity_ids.date_deadline',
+ readonly=True, store=True) # store to enable ordering + search
+ activity_summary = fields.Char(
+ 'Next Activity Summary', related='activity_ids.summary',
+ search='_search_activity_summary')
+
+ @api.depends('activity_ids.state')
+ def _compute_activity_state(self):
+ for record in self:
+ states = record.activity_ids.mapped('state')
+ if 'overdue' in states:
+ record.activity_state = 'overdue'
+ elif 'today' in states:
+ record.activity_state = 'today'
+ elif 'planned' in states:
+ record.activity_state = 'planned'
+
+ @api.model
+ def _search_activity_user_id(self, operator, operand):
+ return [('activity_ids.user_id', operator, operand)]
+
+ @api.model
+ def _search_activity_type_id(self, operator, operand):
+ return [('activity_ids.activity_type_id', operator, operand)]
+
+ @api.model
+ def _search_activity_summary(self, operator, operand):
+ return [('activity_ids.summary', operator, operand)]
+
+ @api.multi
+ def unlink(self):
+ """ Override unlink to delete records activities through (res_model, res_id). """
+ record_ids = self.ids
+ result = super(MailActivityMixin, self).unlink()
+ self.env['mail.activity'].sudo().search(
+ [('res_model', '=', self._name), ('res_id', 'in', record_ids)]
+ ).unlink()
+ return result
diff --git a/addons/mail/models/mail_followers.py b/addons/mail/models/mail_followers.py
index 304003fa4fc..9187917643a 100644
--- a/addons/mail/models/mail_followers.py
+++ b/addons/mail/models/mail_followers.py
@@ -66,11 +66,16 @@ class Followers(models.Model):
default_subtypes = self.env['mail.message.subtype'].search([
('default', '=', True),
'|', ('res_model', '=', res_model), ('res_model', '=', False)])
+ external_default_subtypes = default_subtypes.filtered(lambda subtype: not subtype.internal)
if force_mode:
+ employee_pids = self.env['res.users'].sudo().search([('partner_id', 'in', partner_data.keys()), ('share', '=', False)]).mapped('partner_id').ids
for pid, data in partner_data.iteritems():
if not data:
- partner_data[pid] = default_subtypes.ids
+ if pid not in employee_pids:
+ partner_data[pid] = external_default_subtypes.ids
+ else:
+ partner_data[pid] = default_subtypes.ids
for cid, data in channel_data.iteritems():
if not data:
channel_data[cid] = default_subtypes.ids
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index a9751f7fd28..8fc1fae099b 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -61,6 +61,9 @@ class Message(models.Model):
"message, comment for other messages such as user replies",
oldname='type')
subtype_id = fields.Many2one('mail.message.subtype', 'Subtype', ondelete='set null', index=True)
+ mail_activity_type_id = fields.Many2one(
+ 'mail.activity.type', 'Mail Activity Type',
+ index=True, ondelete='set null')
# origin
email_from = fields.Char(
'From', default=_get_default_from,
diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py
index c75752416be..3bdec9313db 100644
--- a/addons/mail/models/res_partner.py
+++ b/addons/mail/models/res_partner.py
@@ -14,7 +14,7 @@ class Partner(models.Model):
""" Update partner to add a field about notification preferences. Add a generic opt-out field that can be used
to restrict usage of automatic email templates. """
_name = "res.partner"
- _inherit = ['res.partner', 'mail.thread']
+ _inherit = ['res.partner', 'mail.thread', 'mail.activity.mixin']
_mail_flat_thread = False
_mail_mass_mailing = _('Customers')
diff --git a/addons/mail/security/ir.model.access.csv b/addons/mail/security/ir.model.access.csv
index e869287d2f4..79c264ccc98 100644
--- a/addons/mail/security/ir.model.access.csv
+++ b/addons/mail/security/ir.model.access.csv
@@ -33,4 +33,8 @@ access_mail_template,mail.template,model_mail_template,base.group_user,1,1,1,0
access_mail_template_system,mail.template_system,model_mail_template,base.group_system,1,1,1,1
access_mail_shortcode,mail.shortcode,model_mail_shortcode,base.group_user,1,1,1,1
access_mail_shortcode_portal,mail.shortcode.portal,model_mail_shortcode,base.group_portal,1,0,0,0
-access_mail_test_all,mail.test.all,model_mail_test,base.group_user,1,1,1,1
\ No newline at end of file
+access_mail_test_all,mail.test.all,model_mail_test,base.group_user,1,1,1,1
+access_mail_activity_all,mail.activity.all,model_mail_activity,,1,0,0,0
+access_mail_activity_user,mail.activity.user,model_mail_activity,base.group_user,1,1,1,1
+access_mail_activity_type_all,mail.activity.type.all,model_mail_activity_type,,1,0,0,0
+access_mail_activity_type_user,mail.activity.type.user,model_mail_activity_type,base.group_user,1,1,1,1
\ No newline at end of file
diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js
new file mode 100644
index 00000000000..aa929445488
--- /dev/null
+++ b/addons/mail/static/src/js/activity.js
@@ -0,0 +1,218 @@
+odoo.define('mail.activity', function (require) {
+"use strict";
+
+var core = require('web.core');
+var form_common = require('web.form_common');
+var Model = require("web.Model");
+var Widget = require('web.Widget');
+var ChatManager = require('mail.chat_manager');
+var QWeb = core.qweb;
+var _t = core._t;
+
+
+/**
+ * Set the 'label_delay' entry in activity data according to the deadline date
+ * @param {Array} list of activity Object
+ * @return {Array} : list of modified activity Object
+ */
+var set_delay_label = function(activities){
+ var today = moment().startOf('day');
+ var today_str = today.format('YYYY-MM-DD');
+ _.each(activities, function(activity){
+ var to_display = '';
+ var deadline = moment(activity.date_deadline + ' 00:00:00');
+ var diff = deadline.diff(today, 'days', true); // true means no rounding
+ if(diff === 0){
+ to_display = _t('Today');
+ }else{
+ if(diff < 0){ // overdue
+ if(diff === -1){
+ to_display = _t('Yesterday');
+ }else{
+ to_display = _.str.sprintf(_t('%d days overdue'), Math.abs(diff));
+ }
+ }else{ // due
+ if(diff === 1){
+ to_display = _t('Tomorrow');
+ }else{
+ to_display = _.str.sprintf(_t('Due in %d days'), Math.abs(diff));
+ }
+ }
+ }
+ activity['label_delay'] = to_display;
+ });
+ return activities;
+};
+
+// -----------------------------------------------------------------------------
+// Activities Widget ('mail_activity' widget)
+//
+// Since it is displayed on a form view, it extends 'AbstractField' widget.
+//
+// Note: the activity widget is moved inside the chatter by the chatter itself
+// for layout purposes.
+// -----------------------------------------------------------------------------
+
+var Activity = form_common.AbstractField.extend({
+ className: 'o_mail_activity',
+
+ events: {
+ "click .o_activity_edit": "on_activity_edit",
+ "click .o_activity_unlink": "on_activity_unlink",
+ "click .o_activity_done": "on_activity_done",
+ },
+
+ init: function () {
+ this._super.apply(this, arguments);
+
+ this.context = this.options.context || {};
+ this.activities = [];
+ this.Activity = new Model('mail.activity');
+ },
+
+ start: function () {
+ var self = this;
+
+ // Hide the chatter in 'create' mode
+ this.view.on("change:actual_mode", this, this.check_visibility);
+
+ // find chatter if any
+ this.chatter = this.field_manager.fields.message_ids;
+
+ return this._super();
+ },
+
+ get_res_id: function () {
+ return this.view.datarecord.id;
+ },
+
+ check_visibility: function () {
+ this.set({"force_invisible": this.view.get("actual_mode") === "create"});
+ },
+
+ set_value: function (_value) {
+ this.value = _value;
+ this._super(_value);
+ },
+
+ fetch_and_render_value: function () {
+ var self = this;
+
+ return new Model(this.view.dataset.model)
+ .call("read", [[this.get_res_id()], ['activity_ids']])
+ .then(function (results) {
+ self.value = results[0]['activity_ids'];
+ return self.render_value();
+ });
+ },
+
+ render_value: function () {
+ var self = this;
+
+ return this.Activity
+ .call("read", [this.value])
+ .then(function (results) {
+ this.activities = set_delay_label(results);
+ self.$el.html(QWeb.render('mail.activity_items', {
+ activities: this.activities,
+ }));
+ });
+ },
+
+ on_activity_schedule: function (event) {
+ event.preventDefault();
+ var self = this;
+ var action = {
+ type: 'ir.actions.act_window',
+ res_model: 'mail.activity',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [
+ [false, 'form']
+ ],
+ target: 'new',
+ context: _.extend({
+ default_res_id: this.get_res_id(),
+ default_res_model: this.view.dataset.model,
+ }, {'mark_done': true}, this.context),
+ res_id: false,
+ };
+ return this.do_action(action, {
+ on_close: function() {
+ self.fetch_and_render_value();
+ self.chatter.refresh_followers();
+ ChatManager.get_messages({model: self.res_model, res_id: self.get_res_id()});
+ },
+ });
+ },
+
+ on_activity_edit: function (event) {
+ event.preventDefault();
+ var self = this;
+ var activity_id = this.$(event.currentTarget).data('activity-id');
+ var action = {
+ type: 'ir.actions.act_window',
+ res_model: 'mail.activity',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [
+ [false, 'form']
+ ],
+ target: 'new',
+ context: _.extend({
+ default_res_id: this.get_res_id(),
+ default_res_model: this.model,
+ }, {'mark_done': true}, this.context),
+ res_id: activity_id,
+ }
+ this.do_action(action, {
+ on_close: function () {
+ self.fetch_and_render_value();
+ self.chatter.refresh_followers();
+ ChatManager.get_messages({model: self.res_model, res_id: self.get_res_id()});
+ },
+ });
+ },
+
+ on_activity_unlink: function (event) {
+ event.preventDefault();
+ var self = this;
+ var activity_id = this.$(event.currentTarget).data('activity-id');
+ this.Activity
+ .call("unlink", [[activity_id]])
+ .then(function (res) {
+ self.fetch_and_render_value();
+ });
+ },
+
+ on_activity_done: function (event) {
+ event.preventDefault();
+ var self = this;
+ var activity_id = this.$(event.currentTarget).data('activity-id');
+ this.Activity
+ .call("action_done", [[activity_id]])
+ .then(function (msg_id) {
+ self.fetch_and_render_value();
+
+ self.chatter.msg_ids.unshift(msg_id);
+
+ // to stop scrollbar flickering add min height of the thread and remove after
+ // render. on render thread it will remove and add all it's element which cause flickering
+ var thread = self.chatter.thread;
+ thread.$el.css('min-height', thread.$el.height());
+ self.chatter.fetch_and_render_thread(self.chatter.msg_ids).then(function () {
+ thread.$el.css('min-height', '');
+ });
+
+ });
+ },
+});
+
+core.form_widget_registry.add('mail_activity', Activity);
+
+return {
+ Activity: Activity,
+ set_delay_label: set_delay_label,
+};
+
+});
diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js
index 8291237a2de..c55c04d71c4 100644
--- a/addons/mail/static/src/js/chatter.js
+++ b/addons/mail/static/src/js/chatter.js
@@ -10,7 +10,9 @@ var config = require('web.config');
var core = require('web.core');
var form_common = require('web.form_common');
var framework = require('web.framework');
+var Model = require("web.Model");
var web_utils = require('web.utils');
+var Widget = require('web.Widget');
var _t = core._t;
var QWeb = core.qweb;
@@ -278,8 +280,9 @@ var Chatter = form_common.AbstractField.extend({
template: 'mail.Chatter',
events: {
- "click .o_chatter_button_new_message": "on_open_composer_new_message",
- "click .o_chatter_button_log_note": "on_open_composer_log_note",
+ 'click .o_chatter_button_new_message': 'on_open_composer_new_message',
+ 'click .o_chatter_button_log_note': 'on_open_composer_log_note',
+ 'click .o_activity_schedule': 'on_activity_schedule',
},
init: function () {
@@ -288,6 +291,10 @@ var Chatter = form_common.AbstractField.extend({
this.res_id = undefined;
this.context = this.options.context || {};
this.dp = new web_utils.DropPrevious();
+ // extends display options
+ this.options = _.extend(this.options || {}, {
+ 'display_activity_button': this.field_manager.fields.activity_ids
+ });
},
willStart: function () {
@@ -315,6 +322,12 @@ var Chatter = form_common.AbstractField.extend({
this.followers.on('followers_update', this, this.on_followers_update);
}
+ // Move the activities widget (if any) inside the chatter
+ this.activities = this.field_manager.fields.activity_ids
+ if (this.activities) {
+ this.$('.o_chatter_topbar').after(this.activities.$el);
+ }
+
this.thread = new ChatThread(this, {
display_order: ChatThread.ORDER.DESC,
display_document_link: false,
@@ -476,6 +489,13 @@ var Chatter = form_common.AbstractField.extend({
this.followers.read_value();
}
},
+
+ on_activity_schedule: function (event) {
+ if (this.activities) {
+ this.activities.on_activity_schedule(event);
+ }
+ },
+
// composer toggle
on_open_composer_new_message: function () {
this.open_composer();
@@ -501,7 +521,7 @@ var Chatter = form_common.AbstractField.extend({
this.composer.on('input_focused', this, function () {
this.composer.mention_set_prefetched_partners(this.mention_suggestions || []);
});
- this.composer.insertBefore(this.$('.o_mail_thread')).then(function () {
+ this.composer.insertAfter(this.$('.o_chatter_topbar')).then(function () {
// destroy existing composer
if (old_composer) {
old_composer.destroy();
diff --git a/addons/mail/static/src/js/web_kanban_activity.js b/addons/mail/static/src/js/web_kanban_activity.js
new file mode 100644
index 00000000000..9e3f813142c
--- /dev/null
+++ b/addons/mail/static/src/js/web_kanban_activity.js
@@ -0,0 +1,106 @@
+odoo.define('web.kanban.activity', function (require) {
+"use strict";
+
+var core = require('web.core');
+var Model = require("web.Model");
+var Widget = require('web.Widget');
+var kanban_widgets = require('web_kanban.widgets');
+var AbstractField = kanban_widgets.AbstractField;
+var fields_registry = kanban_widgets.registry;
+var set_delay_label = require('mail.activity').set_delay_label;
+
+var QWeb = core.qweb;
+
+
+var KanbanActivity = AbstractField.extend({
+ template: 'mail.KanbanActivity',
+
+ events: {
+ "click .o_activity_btn": "get_activity_logs",
+ "click .o_schedule_activity": "on_activity_schedule",
+ "click .o_mark_as_done": "on_activity_done",
+ },
+
+ init: function(parent, field, $node) {
+ this._super.apply(this, arguments);
+ this.parent = parent;
+ this.activity_state = parent.record.activity_state.raw_value;
+ this.selection = _.object(parent.fields.activity_state.selection);
+ this.Activity = new Model("mail.activity");
+ },
+
+ get_activity_logs: function(event) {
+ event.preventDefault();
+ var self = this;
+ this.$('.o_activity').html(QWeb.render("mail.KanbanActivityLoading"));
+ this.Activity
+ .call("read", [this.field.raw_value])
+ .then(function (records) {
+ records = set_delay_label(records);
+ self.$('.o_activity').html($(QWeb.render("mail.KanbanActivityDropdown", {
+ selection: self.selection,
+ records: _.groupBy(records, 'state'),
+ uid: self.session.uid,
+ })));
+ });
+ },
+
+ reset_state: function () {
+ var self = this;
+ new Model(self.parent.model).query(['activity_ids', 'activity_state'])
+ .filter([['id', '=', self.parent.id]])
+ .first()
+ .then(function (record) {
+ var state_color_class = 'o_activity_color_' + (record.activity_state || 'default');
+ var current_state_class = 'o_activity_color_' + (self.activity_state || 'default');
+ self.$(".o_activity_btn > span").removeClass(current_state_class).addClass(state_color_class);
+ self.activity_state = record.activity_state;
+ self.parent.record.activity_ids.raw_value = record.activity_ids;
+ });
+ },
+
+ on_activity_schedule: function(event) {
+ var self = this;
+ var activity_id = this.$(event.currentTarget).data('activity-id') || false;
+ var action = {
+ type: 'ir.actions.act_window',
+ res_model: 'mail.activity',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [
+ [false, 'form']
+ ],
+ target: 'new',
+ context: _.extend({
+ default_res_id: this.parent.id,
+ default_res_model: this.parent.model,
+ }, {'mark_done': true}, this.context),
+ res_id: activity_id,
+ };
+ this.do_action(action, {
+ on_close: function() {
+ self.reset_state();
+ self.get_activity_logs(event);
+ }
+ });
+ },
+
+ on_activity_done: function (event) {
+ event.stopPropagation();
+ var self = this;
+ var activity_id = this.$(event.currentTarget).data('activity-id');
+ this.Activity
+ .call("action_done", [[activity_id]])
+ .then(function (result) {
+ self.get_activity_logs(event);
+ self.reset_state();
+ });
+ },
+});
+
+fields_registry.add('kanban_activity', KanbanActivity);
+
+return {
+ KanbanActivity: KanbanActivity
+};
+});
diff --git a/addons/mail/static/src/less/mail_activity.less b/addons/mail/static/src/less/mail_activity.less
new file mode 100644
index 00000000000..e4aa44ffd29
--- /dev/null
+++ b/addons/mail/static/src/less/mail_activity.less
@@ -0,0 +1,105 @@
+/* Common */
+.o_mail_activity {
+ &.o_form_field {
+ display: block;
+ }
+
+ .o_thread_date_separator.o_border_dashed {
+ border-bottom: dashed #eeeeee 2px;
+ }
+
+ .o_thread_message_core.o_mail_note {
+ padding: 0px 3px;
+ }
+
+ .o_activity_color_default {
+ color: @gray;
+ }
+
+ .o_activity_color_planned {
+ color: @brand-primary;
+ }
+ .o_activity_color_overdue {
+ color: @brand-danger;
+ }
+ .o_activity_color_today {
+ color: @brand-success;
+ }
+
+ .o_activity_link {
+ color: @gray;
+ &:hover {
+ color: @brand-success;
+ }
+ }
+
+}
+
+/* Kanban View */
+.o_kanban_record {
+ .o_kanban_inline_block {
+ display: inline-block;
+ }
+
+ .o_mail_activity {
+ .o_activity_btn {
+ span.fa {
+ vertical-align: -35%;
+ }
+ }
+
+ .o_activity_link_kanban {
+ visibility: hidden;
+ }
+
+ ul.o_activity {
+ width: 290px;
+
+ & > li:last-child {
+ background-color: @odoo-brand-lightsecondary;
+ color: @gray-dark;
+ line-height: 3em;
+ }
+ li > ul.o_activity_log {
+ max-height: 300px;
+ overflow-y: auto;
+ color: @text-color;
+
+ li {
+ width: 100%;
+ display: inline-block;
+
+ &.o_activity_label {
+ line-height: 2.5em;
+ padding: 0px 5px;
+ background-color: @odoo-brand-lightsecondary;
+ }
+
+ &.o_schedule_activity {
+ padding: 8px 10px 8px 0px;
+ vertical-align: middle;
+ &:hover {
+ background-color: @gray-lighter;
+ .o_activity_link_kanban {
+ visibility: visible;
+ }
+ }
+
+ .o_activity_title {
+ width: 85%;
+ padding: 0px 5px 0px 10px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+ }
+ }
+
+ .o_no_activity {
+ padding: 10px;
+ cursor: initial;
+ }
+ }
+ }
+}
diff --git a/addons/mail/static/src/less/thread.less b/addons/mail/static/src/less/thread.less
index 7a9efa45731..0f2dd407575 100644
--- a/addons/mail/static/src/less/thread.less
+++ b/addons/mail/static/src/less/thread.less
@@ -3,7 +3,7 @@
@mail-thread-side-date-opacity: 0.6;
@mail-thread-note: @gray-lighter;
-.o_mail_thread {
+.o_mail_thread, .o_mail_activity {
.o_thread_show_more {
text-align: center;
}
diff --git a/addons/mail/static/src/xml/activity.xml b/addons/mail/static/src/xml/activity.xml
new file mode 100644
index 00000000000..0f699b6c2d1
--- /dev/null
+++ b/addons/mail/static/src/xml/activity.xml
@@ -0,0 +1,37 @@
+
+
+
+