diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 04d80bb138c..42d526d7d08 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -2319,7 +2319,7 @@ class AccountMove(models.Model): if not self.is_invoice(include_receipts=True): if self.payment_id and 'state' in init_values: - self.payment_id.message_track(['state'], {self.payment_id.id: init_values}) + self.payment_id._message_track(['state'], {self.payment_id.id: init_values}) return super(AccountMove, self)._track_subtype(init_values) if 'payment_state' in init_values and self.payment_state == 'paid': diff --git a/addons/crm/__manifest__.py b/addons/crm/__manifest__.py index 55e0bcd8780..722950735fd 100644 --- a/addons/crm/__manifest__.py +++ b/addons/crm/__manifest__.py @@ -4,7 +4,7 @@ { 'name': 'CRM', - 'version': '1.6', + 'version': '1.7', 'category': 'Sales/CRM', 'sequence': 15, 'summary': 'Track leads and close opportunities', diff --git a/addons/crm/data/crm_lead_merge_template.xml b/addons/crm/data/crm_lead_merge_template.xml index a46087183db..af9e485123b 100644 --- a/addons/crm/data/crm_lead_merge_template.xml +++ b/addons/crm/data/crm_lead_merge_template.xml @@ -40,8 +40,8 @@
Priority:
-
- Lost Reason: +
+ Lost Reason:
Created on: diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 1f3daa6d1c9..61c8adfba25 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -213,7 +213,7 @@ class Lead(models.Model): automated_probability = fields.Float('Automated Probability', compute='_compute_probabilities', readonly=True, store=True) is_automated_probability = fields.Boolean('Is automated probability?', compute="_compute_is_automated_probability") # Won/Lost - lost_reason = fields.Many2one( + lost_reason_id = fields.Many2one( 'crm.lost.reason', string='Lost Reason', index=True, ondelete='restrict', tracking=True) # Statistics @@ -901,7 +901,7 @@ class Lead(models.Model): activated = self.filtered(lambda lead: lead.active) archived = self.filtered(lambda lead: not lead.active) if activated: - activated.write({'lost_reason': False}) + activated.write({'lost_reason_id': False}) activated._compute_probabilities() if archived: archived.write({'probability': 0, 'automated_probability': 0}) @@ -1355,9 +1355,9 @@ class Lead(models.Model): 'type': lambda fname, leads: 'opportunity' if any(lead.type == 'opportunity' for lead in leads) else 'lead', 'priority': lambda fname, leads: max(leads.mapped('priority')) if leads else False, 'tag_ids': lambda fname, leads: leads.mapped('tag_ids'), - 'lost_reason': lambda fname, leads: + 'lost_reason_id': lambda fname, leads: False if leads and leads[0].probability - else next((lead.lost_reason for lead in leads if lead.lost_reason), False), + else next((lead.lost_reason_id for lead in leads if lead.lost_reason_id), False), } def _merge_get_fields(self): @@ -1723,7 +1723,7 @@ class Lead(models.Model): self.ensure_one() if 'stage_id' in init_values and self.probability == 100 and self.stage_id: return self.env.ref('crm.mt_lead_won') - elif 'lost_reason' in init_values and self.lost_reason: + elif 'lost_reason_id' in init_values and self.lost_reason_id: return self.env.ref('crm.mt_lead_lost') elif 'stage_id' in init_values: return self.env.ref('crm.mt_lead_stage') diff --git a/addons/crm/models/crm_lost_reason.py b/addons/crm/models/crm_lost_reason.py index 4b848acbc5f..714a984db68 100644 --- a/addons/crm/models/crm_lost_reason.py +++ b/addons/crm/models/crm_lost_reason.py @@ -13,8 +13,12 @@ class LostReason(models.Model): leads_count = fields.Integer('Leads Count', compute='_compute_leads_count') def _compute_leads_count(self): - lead_data = self.env['crm.lead'].with_context(active_test=False).read_group([('lost_reason', 'in', self.ids)], ['lost_reason'], ['lost_reason']) - mapped_data = dict((data['lost_reason'][0], data['lost_reason_count']) for data in lead_data) + lead_data = self.env['crm.lead'].with_context(active_test=False).read_group( + [('lost_reason_id', 'in', self.ids)], + ['lost_reason_id'], + ['lost_reason_id'] + ) + mapped_data = dict((data['lost_reason_id'][0], data['lost_reason_id_count']) for data in lead_data) for reason in self: reason.leads_count = mapped_data.get(reason.id, 0) @@ -22,7 +26,7 @@ class LostReason(models.Model): return { 'name': _('Leads'), 'view_mode': 'tree,form', - 'domain': [('lost_reason', 'in', self.ids)], + 'domain': [('lost_reason_id', 'in', self.ids)], 'res_model': 'crm.lead', 'type': 'ir.actions.act_window', 'context': {'create': False, 'active_test': False}, diff --git a/addons/crm/report/crm_opportunity_report_views.xml b/addons/crm/report/crm_opportunity_report_views.xml index 42347272642..8f8a7432ea6 100644 --- a/addons/crm/report/crm_opportunity_report_views.xml +++ b/addons/crm/report/crm_opportunity_report_views.xml @@ -105,7 +105,7 @@ - + diff --git a/addons/crm/tests/common.py b/addons/crm/tests/common.py index 71633308398..02b35af3a3f 100644 --- a/addons/crm/tests/common.py +++ b/addons/crm/tests/common.py @@ -242,6 +242,10 @@ class TestCrmCommon(TestSalesCommon, MailCase): 'res_id': cls.activity_type_1.id, }) + def setUp(self): + super(TestCrmCommon, self).setUp() + self.flush_tracking() + @classmethod def _activate_multi_company(cls): cls.company_2 = cls.env['res.company'].create({ diff --git a/addons/crm/tests/test_crm_lead_lost.py b/addons/crm/tests/test_crm_lead_lost.py index 7f2819f7825..33d5f38ea41 100644 --- a/addons/crm/tests/test_crm_lead_lost.py +++ b/addons/crm/tests/test_crm_lead_lost.py @@ -4,9 +4,10 @@ from odoo.addons.crm.tests import common as crm_common from odoo.exceptions import AccessError from odoo.tests.common import tagged, users +from odoo.tools import mute_logger -@tagged('lead_manage') +@tagged('lead_manage', 'lead_lost') class TestLeadConvert(crm_common.TestCrmCommon): @classmethod @@ -18,29 +19,95 @@ class TestLeadConvert(crm_common.TestCrmCommon): @users('user_sales_salesman') def test_lead_lost(self): + """ Test setting a lead as lost using the wizard. Also check that an + 'html editor' void content used as feedback is not logged on the lead. """ + # Initial data + self.assertEqual(len(self.lead_1.message_ids), 1, 'Should contain creation message') + creation_message = self.lead_1.message_ids[0] + self.assertEqual(creation_message.subtype_id, self.env.ref('crm.mt_lead_create')) + + # Update responsible as ACLs is "own only" for user_sales_salesman self.lead_1.with_user(self.user_sales_manager).write({ 'user_id': self.user_sales_salesman.id, 'probability': 32, }) + self.flush_tracking() - lead = self.lead_1.with_user(self.env.user) + lead = self.env['crm.lead'].browse(self.lead_1.ids) + self.assertFalse(lead.lost_reason_id) self.assertEqual(lead.probability, 32) + self.assertEqual(len(lead.message_ids), 2, 'Should have tracked new responsible') + update_message = lead.message_ids[0] + self.assertEqual(update_message.subtype_id, self.env.ref('mail.mt_note')) + # mark as lost using the wizard lost_wizard = self.env['crm.lead.lost'].with_context({ 'active_ids': lead.ids, }).create({ - 'lost_reason_id': self.lost_reason.id + 'lost_reason_id': self.lost_reason.id, + 'lost_feedback': '

', # void content }) lost_wizard.action_lost_reason_apply() + self.flush_tracking() - self.assertEqual(lead.probability, 0) - self.assertEqual(lead.automated_probability, 0) + # check lead update self.assertFalse(lead.active) - self.assertEqual(lead.lost_reason, self.lost_reason) # TDE FIXME: should be called lost_reason_id non didjou + self.assertEqual(lead.automated_probability, 0) + self.assertEqual(lead.lost_reason_id, self.lost_reason) # TDE FIXME: should be called lost_reason_id non didjou + self.assertEqual(lead.probability, 0) + # check messages + self.assertEqual(len(lead.message_ids), 3, 'Should have logged a tracking message for lost lead with reason') + update_message = lead.message_ids[0] + self.assertEqual(update_message.subtype_id, self.env.ref('crm.mt_lead_lost')) + self.assertEqual(len(update_message.tracking_value_ids), 2, 'Tracking: active, lost reason') + self.assertTracking( + update_message, + [('active', 'boolean', True, False), + ('lost_reason_id', 'many2one', False, self.lost_reason) + ] + ) + + @users('user_sales_leads') + def test_lead_lost_batch_wfeedback(self): + """ Test setting leads as lost in batch using the wizard, including a log + message. """ + leads = self._create_leads_batch(lead_type='lead', count=10, probabilities=[10, 20, 30]) + self.assertEqual(len(leads), 10) + self.flush_tracking() + + lost_wizard = self.env['crm.lead.lost'].with_context({ + 'active_ids': leads.ids, + }).create({ + 'lost_reason_id': self.lost_reason.id, + 'lost_feedback': '

I cannot find it. It was in my closet and pouf, disappeared.

', + }) + lost_wizard.action_lost_reason_apply() + self.flush_tracking() + + for lead in leads: + # check content + self.assertFalse(lead.active) + self.assertEqual(lead.automated_probability, 0) + self.assertEqual(lead.probability, 0) + self.assertEqual(lead.lost_reason_id, self.lost_reason) + # check messages + self.assertEqual(len(lead.message_ids), 2, 'Should have 2 messages: creation, lost with log') + lost_message = lead.message_ids.filtered(lambda msg: msg.subtype_id == self.env.ref('crm.mt_lead_lost')) + self.assertTrue(lost_message) + self.assertTracking( + lost_message, + [('active', 'boolean', True, False), + ('lost_reason_id', 'many2one', False, self.lost_reason) + ] + ) + self.assertIn('

I cannot find it. It was in my closet and pouf, disappeared.

', lost_message.body, + 'Feedback should be included directly within tracking message') @users('user_sales_salesman') + @mute_logger('odoo.addons.base.models') def test_lead_lost_crm_rights(self): + """ Test ACLs of lost reasons management and usage """ lead = self.lead_1.with_user(self.env.user) # nice try little salesman but only managers can create lost reason to avoid bloating the DB diff --git a/addons/crm/tests/test_crm_lead_merge.py b/addons/crm/tests/test_crm_lead_merge.py index bbf73b70155..9f5b8bb2dfa 100644 --- a/addons/crm/tests/test_crm_lead_merge.py +++ b/addons/crm/tests/test_crm_lead_merge.py @@ -307,7 +307,7 @@ class TestLeadMerge(TestLeadMergeCommon): # because "lead_1" is not lost lost_reason = self.env['crm.lost.reason'].create({'name': 'Test Reason'}) self.lead_w_partner.write({ - 'lost_reason': lost_reason, + 'lost_reason_id': lost_reason, 'probability': 0, }) @@ -317,7 +317,7 @@ class TestLeadMerge(TestLeadMergeCommon): name='Nibbler Spacecraft Request', partner_id=self.contact_company_1, priority='2', - lost_reason=False, + lost_reason_id=False, tag_ids=all_tags): leads._merge_opportunity(auto_unlink=False, max_length=None) @@ -331,11 +331,11 @@ class TestLeadMerge(TestLeadMergeCommon): }) lost_reason = self.env['crm.lost.reason'].create({'name': 'Test Reason'}) - self.lead_w_partner.lost_reason = lost_reason + self.lead_w_partner.lost_reason_id = lost_reason leads = self.env['crm.lead'].browse(self.leads.ids)._sort_by_confidence_level(reverse=True) - with self.assertLeadMerged(leads[0], leads, lost_reason=lost_reason): + with self.assertLeadMerged(leads[0], leads, lost_reason_id=lost_reason): leads._merge_opportunity(auto_unlink=False, max_length=None) @users('user_sales_manager') diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 277d07edb80..44127d2899d 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -246,7 +246,7 @@ - + @@ -985,7 +985,7 @@ - + diff --git a/addons/crm/wizard/crm_lead_lost.py b/addons/crm/wizard/crm_lead_lost.py index 8f64a40579b..0db92f5a739 100644 --- a/addons/crm/wizard/crm_lead_lost.py +++ b/addons/crm/wizard/crm_lead_lost.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import fields, models, _ +from odoo.tools.mail import is_html_empty class CrmLeadLost(models.TransientModel): @@ -8,7 +10,20 @@ class CrmLeadLost(models.TransientModel): _description = 'Get Lost Reason' lost_reason_id = fields.Many2one('crm.lost.reason', 'Lost Reason') + lost_feedback = fields.Html( + 'Closing Note', sanitize=True, + help="Closing note logged in leads discussion history." + ) def action_lost_reason_apply(self): + self.ensure_one() leads = self.env['crm.lead'].browse(self.env.context.get('active_ids')) - return leads.action_set_lost(lost_reason=self.lost_reason_id.id) + if not is_html_empty(self.lost_feedback): + leads._track_set_log_message( + '

%s:

%s
' % ( + _('Lost Comment'), + self.lost_feedback + ) + ) + res = leads.action_set_lost(lost_reason_id=self.lost_reason_id.id) + return res diff --git a/addons/crm/wizard/crm_lead_lost_views.xml b/addons/crm/wizard/crm_lead_lost_views.xml index 79611a98e7c..b046443d5e8 100644 --- a/addons/crm/wizard/crm_lead_lost_views.xml +++ b/addons/crm/wizard/crm_lead_lost_views.xml @@ -5,9 +5,10 @@ crm.lead.lost
- + +