From 318f02b8e8581f9cab995ec5e1f8c3dceb2e1dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 28 Feb 2020 13:43:07 +0000 Subject: [PATCH 1/5] [REF] base: replace toggle method by already-existing toggle_active for ir.ui.view Toggle method defined on ir.ui.view does the same job of toggle_active that is the generic one available on all models. Task ID 2170708 Community PR odoo/odoo#46563 --- addons/website/controllers/main.py | 2 +- odoo/addons/base/models/ir_ui_view.py | 6 ------ odoo/addons/base/tests/test_views.py | 6 +++--- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 9cc42507c92..52d5f8e711f 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -301,7 +301,7 @@ class Website(Home): @http.route('/website/toggle_switchable_view', type='json', auth='user', website=True) def toggle_switchable_view(self, view_key): - request.website.viewref(view_key).toggle() + request.website.viewref(view_key).toggle_active() @http.route('/website/reset_template', type='http', auth='user', methods=['POST'], website=True, csrf=False) def reset_template(self, view_id, mode='soft', redirect='/', **kwargs): diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index 3ed1603efb2..15d98396d04 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -511,12 +511,6 @@ actual arch. default = dict(default or {}, key=new_key) return super(View, self).copy(default) - def toggle(self): - """ Switches between enabled and disabled statuses - """ - for view in self: - view.write({'active': not view.active}) - # default view selection @api.model def default_view(self, model, view_type): diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py index c3afcc7d39f..10370071f4d 100644 --- a/odoo/addons/base/tests/test_views.py +++ b/odoo/addons/base/tests/test_views.py @@ -2634,7 +2634,7 @@ class TestOptionalViews(ViewCase): """ Change active states of v2 and v3, check that the results are as expected """ - self.v2.toggle() + self.v2.toggle_active() context = {'check_view_ids': self.View.search([]).ids} arch = self.v0.with_context(context).read_combined(['arch'])['arch'] self.assertEqual( @@ -2645,7 +2645,7 @@ class TestOptionalViews(ViewCase): ) ) - self.v3.toggle() + self.v3.toggle_active() context = {'check_view_ids': self.View.search([]).ids} arch = self.v0.with_context(context).read_combined(['arch'])['arch'] self.assertEqual( @@ -2657,7 +2657,7 @@ class TestOptionalViews(ViewCase): ) ) - self.v2.toggle() + self.v2.toggle_active() context = {'check_view_ids': self.View.search([]).ids} arch = self.v0.with_context(context).read_combined(['arch'])['arch'] self.assertEqual( From 9b76c6b7f1eec4057289de90d62b8a7a266b0aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 28 Feb 2020 13:08:36 +0000 Subject: [PATCH 2/5] [IMP] crm: better use toggle_active and add business behavior in it Purpose of this commit is to make use of toggle_active to implement business behavior linked to active field being changed. In CRM notably * when archiving: it is considered as lost, and therefore probability is set to 0; * when reactivating: void the lost reason and update probabilities for PLS; In this commit we also update tracking subtype to more clearly track lost / restored subtypes * lost: writing a lost reason (resetting it is not lost), archiving it if no other subtype; * restored: activating it again; Task ID 2170708 Community PR odoo/odoo#46563 --- addons/crm/models/crm_lead.py | 23 +++++++++++++++----- addons/crm/wizard/crm_lead_to_opportunity.py | 3 +-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index 59540383fc7..f791d4be431 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -486,8 +486,17 @@ class Lead(models.Model): # ------------------------------------------------------------ def toggle_active(self): + """ When archiving: mark probability as 0. When re-activating + update probability again, for leads and opportunities. """ res = super(Lead, self).toggle_active() - self.filtered(lambda lead: lead.active)._update_probability() + activated = self.filtered(lambda lead: lead.active) + archived = self.filtered(lambda lead: not lead.active) + if activated: + activated.write({'lost_reason': False}) + activated._update_probability() + if archived: + archived.write({'probability': 0, 'automated_probability': 0}) + archived._rebuild_pls_frequency_table_threshold() return res def _rebuild_pls_frequency_table_threshold(self): @@ -506,12 +515,14 @@ class Lead(models.Model): def action_set_lost(self, **additional_values): """ Lost semantic: probability = 0 or active = False """ - result = self.write({'active': False, 'probability': 0, 'automated_probability': 0, **additional_values}) - self._rebuild_pls_frequency_table_threshold() - return result + res = self.action_archive() + if additional_values: + self.write(dict(additional_values)) + return res def action_set_won(self): """ Won semantic: probability = 100 (active untouched) """ + self.action_unarchive() for lead in self: stage_id = lead._stage_find(domain=[('is_won', '=', True)]) lead.write({'stage_id': stage_id.id, 'probability': 100}) @@ -1039,12 +1050,14 @@ 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: + elif 'lost_reason' in init_values and self.lost_reason: return self.env.ref('crm.mt_lead_lost') elif 'stage_id' in init_values: return self.env.ref('crm.mt_lead_stage') elif 'active' in init_values and self.active: return self.env.ref('crm.mt_lead_restored') + elif 'active' in init_values and not self.active: + return self.env.ref('crm.mt_lead_lost') return super(Lead, self)._track_subtype(init_values) def _notify_get_groups(self): diff --git a/addons/crm/wizard/crm_lead_to_opportunity.py b/addons/crm/wizard/crm_lead_to_opportunity.py index e56dd0583d0..a00f76f12da 100644 --- a/addons/crm/wizard/crm_lead_to_opportunity.py +++ b/addons/crm/wizard/crm_lead_to_opportunity.py @@ -125,8 +125,7 @@ class Lead2OpportunityPartner(models.TransientModel): def _action_merge(self): to_merge = self.duplicated_lead_ids result_opportunity = to_merge.merge_opportunity(auto_unlink=False) - if not result_opportunity.active: - result_opportunity.write({'active': True, 'lost_reason': False}) + result_opportunity.action_unarchive() if result_opportunity.type == "lead": self._convert_and_allocate(result_opportunity, [self.user_id.id], team_id=self.team_id.id) From bea9bc3d6c51bfc1c4eaad2378caba83b7a5142a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 28 Feb 2020 13:42:30 +0000 Subject: [PATCH 3/5] [IMP] mail, mass_mailing, phone, survey: call toggle_active / action_(un)archive when possible Purpose of this commit is to call toggle_active or archive / unarchive methods instead of manually writing on active field. Indeed this allows to trigger business code related to archive / unarchive which is normally located in toggle_archive (called by action_archive and action_unarchive). Task ID 2170708 Community PR odoo/odoo#46563 --- addons/mail/models/mail_blacklist.py | 4 ++-- addons/mass_mailing/models/mailing_list.py | 2 +- addons/phone_validation/models/phone_blacklist.py | 4 ++-- addons/survey/models/survey_survey.py | 7 +++---- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/addons/mail/models/mail_blacklist.py b/addons/mail/models/mail_blacklist.py index b2d27238a2c..4c686c2a045 100644 --- a/addons/mail/models/mail_blacklist.py +++ b/addons/mail/models/mail_blacklist.py @@ -73,7 +73,7 @@ class MailBlackList(models.Model): normalized = tools.email_normalize(email) record = self.env["mail.blacklist"].with_context(active_test=False).search([('email', '=', normalized)]) if len(record) > 0: - record.write({'active': True}) + record.action_unarchive() else: record = self.create({'email': email}) return record @@ -89,7 +89,7 @@ class MailBlackList(models.Model): normalized = tools.email_normalize(email) record = self.env["mail.blacklist"].with_context(active_test=False).search([('email', '=', normalized)]) if len(record) > 0: - record.write({'active': False}) + record.action_archive() else: record = record.create({'email': email, 'active': False}) return record diff --git a/addons/mass_mailing/models/mailing_list.py b/addons/mass_mailing/models/mailing_list.py index b10d7cab06e..57173bdbdfb 100644 --- a/addons/mass_mailing/models/mailing_list.py +++ b/addons/mass_mailing/models/mailing_list.py @@ -129,7 +129,7 @@ class MassMailingList(models.Model): self.flush() self.invalidate_cache() if archive: - (src_lists - self).write({'active': False}) + (src_lists - self).action_archive() def close_dialog(self): return {'type': 'ir.actions.act_window_close'} diff --git a/addons/phone_validation/models/phone_blacklist.py b/addons/phone_validation/models/phone_blacklist.py index 3fd1c4b4bcc..17f59b7fc38 100644 --- a/addons/phone_validation/models/phone_blacklist.py +++ b/addons/phone_validation/models/phone_blacklist.py @@ -89,7 +89,7 @@ class PhoneBlackList(models.Model): records = self.env["phone.blacklist"].with_context(active_test=False).search([('number', 'in', numbers)]) todo = [n for n in numbers if n not in records.mapped('number')] if records: - records.write({'active': True}) + records.action_unarchive() if todo: records += self.create([{'number': n} for n in todo]) return records @@ -112,7 +112,7 @@ class PhoneBlackList(models.Model): records = self.env["phone.blacklist"].with_context(active_test=False).search([('number', 'in', numbers)]) todo = [n for n in numbers if n not in records.mapped('number')] if records: - records.write({'active': False}) + records.action_archive() if todo: records += self.create([{'number': n, 'active': False} for n in todo]) return records diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index 60558a94924..c08f13339ba 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -983,9 +983,8 @@ class Survey(models.Model): def _handle_certification_badges(self, vals): if vals.get('certification_give_badge'): # If badge already set on records, reactivate the ones that are not active. - surveys_with_badge = self.filtered(lambda survey: survey.certification_badge_id - and not survey.certification_badge_id.active) - surveys_with_badge.mapped('certification_badge_id').write({'active': True}) + surveys_with_badge = self.filtered(lambda survey: survey.certification_badge_id and not survey.certification_badge_id.active) + surveys_with_badge.mapped('certification_badge_id').action_unarchive() # (re-)create challenge and goal for survey in self: survey._create_certification_badge_trigger() @@ -994,7 +993,7 @@ class Survey(models.Model): badges = self.mapped('certification_badge_id') challenges_to_delete = self.env['gamification.challenge'].search([('reward_id', 'in', badges.ids)]) goals_to_delete = challenges_to_delete.mapped('line_ids').mapped('definition_id') - badges.write({'active': False}) + badges.action_archive() # delete all challenges and goals because not needed anymore (challenge lines are deleted in cascade) challenges_to_delete.unlink() goals_to_delete.unlink() From 6cf824933f36cd8111ec61a2bb2da5bb99a2ec6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 3 Apr 2020 09:58:57 +0000 Subject: [PATCH 4/5] [IMP] survey: cascade archive of a survey to its badge Purpose of this commit is to synchronize active status of certifications to their badges. When archiving or activating a certification, its badge is also archived or activated. Task ID 2170708 Community PR odoo/odoo#46563 --- addons/survey/models/survey_survey.py | 7 +++++++ addons/survey/tests/test_certification_badge.py | 16 ++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/addons/survey/models/survey_survey.py b/addons/survey/models/survey_survey.py index c08f13339ba..5d8a297363e 100644 --- a/addons/survey/models/survey_survey.py +++ b/addons/survey/models/survey_survey.py @@ -328,6 +328,12 @@ class Survey(models.Model): default = dict(default or {}, title=title) return super(Survey, self).copy_data(default) + def toggle_active(self): + super(Survey, self).toggle_active() + activated = self.filtered(lambda survey: survey.active) + activated.mapped('certification_badge_id').action_unarchive() + (self - activated).mapped('certification_badge_id').action_archive() + # ------------------------------------------------------------ # ANSWER MANAGEMENT # ------------------------------------------------------------ @@ -946,6 +952,7 @@ class Survey(models.Model): # ------------------------------------------------------------ # GAMIFICATION / BADGES # ------------------------------------------------------------ + def _prepare_challenge_category(self): return 'certification' diff --git a/addons/survey/tests/test_certification_badge.py b/addons/survey/tests/test_certification_badge.py index 2b7348be427..df31c5e68d9 100644 --- a/addons/survey/tests/test_certification_badge.py +++ b/addons/survey/tests/test_certification_badge.py @@ -8,6 +8,7 @@ from odoo.tools import mute_logger class TestCertificationBadge(common.TestSurveyCommon): + def setUp(self): super(TestCertificationBadge, self).setUp() self.certification_survey = self.env['survey.survey'].with_user(self.survey_manager).create({ @@ -49,6 +50,21 @@ class TestCertificationBadge(common.TestSurveyCommon): 'level': None, }) + def test_archive(self): + """ Archive status of survey is propagated to its badges. """ + self.certification_survey.write({ + 'certification_give_badge': True, + 'certification_badge_id': self.certification_badge.id + }) + + self.certification_survey.action_archive() + self.assertFalse(self.certification_survey.active) + self.assertFalse(self.certification_badge.active) + + self.certification_survey.action_unarchive() + self.assertTrue(self.certification_survey.active) + self.assertTrue(self.certification_badge.active) + def test_give_badge_without_badge(self): with mute_logger('odoo.sql_db'): with self.assertRaises(IntegrityError): From d73a92505c18b1e5b21f6aaa19b0aa158aa6e852 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 28 Feb 2020 13:43:15 +0000 Subject: [PATCH 5/5] [IMP] website_slides: implement archive cascade behavior in toggle_active Purpose of this commit is to make use of toggle_active to implement business behavior linked to active field being changed. In this commit we move the business specific code from write to toggle_active. When archiving a channel its slides are archived. In this commit we also track active field for courses and slides, allowing to see flag changes in chatter. Task ID 2170708 Community PR odoo/odoo#46563 --- addons/website_slides/models/slide_channel.py | 16 ++++++++++++---- addons/website_slides/models/slide_slide.py | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/addons/website_slides/models/slide_channel.py b/addons/website_slides/models/slide_channel.py index 43a496513ad..8a2bac24b5d 100644 --- a/addons/website_slides/models/slide_channel.py +++ b/addons/website_slides/models/slide_channel.py @@ -87,7 +87,7 @@ class Channel(models.Model): # description name = fields.Char('Name', translate=True, required=True) - active = fields.Boolean(default=True) + active = fields.Boolean(default=True, tracking=100) description = fields.Text('Description', translate=True, help="The description that is displayed on top of the course page, just below the title") description_short = fields.Text('Short Description', translate=True, help="The description that is displayed on the course card") description_html = fields.Html('Detailed Description', translate=tools.html_translate, sanitize_attributes=False, sanitize_form=False) @@ -391,14 +391,22 @@ class Channel(models.Model): if vals.get('user_id'): self._action_add_members(self.env['res.users'].sudo().browse(vals['user_id']).partner_id) - if 'active' in vals: - # archiving/unarchiving a channel does it on its slides, too - self.with_context(active_test=False).mapped('slide_ids').write({'active': vals['active']}) if 'enroll_group_ids' in vals: self._add_groups_members() return res + def toggle_active(self): + # archiving/unarchiving a channel does it on its slides, too + to_archive = self.filtered(lambda channel: channel.active) + to_activate = self.filtered(lambda channel: not channel.active) + res = super(Channel, self).toggle_active() + if to_archive: + to_archive.mapped('slide_ids').action_archive() + if to_activate: + to_activate.with_context(active_test=False).mapped('slide_ids').action_unarchive() + return res + @api.returns('mail.message', lambda value: value.id) def message_post(self, *, parent_id=False, subtype_id=False, **kwargs): """ Temporary workaround to avoid spam. If someone replies on a channel diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index b99a3723963..062cb8a4d13 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -127,7 +127,7 @@ class Slide(models.Model): # description name = fields.Char('Title', required=True, translate=True) - active = fields.Boolean(default=True) + active = fields.Boolean(default=True, tracking=100) sequence = fields.Integer('Sequence', default=0) user_id = fields.Many2one('res.users', string='Uploaded by', default=lambda self: self.env.uid) description = fields.Text('Description', translate=True) @@ -175,7 +175,7 @@ class Slide(models.Model): html_content = fields.Html("HTML Content", help="Custom HTML content for slides of type 'Web Page'.", translate=True, sanitize_form=False) # website website_id = fields.Many2one(related='channel_id.website_id', readonly=True) - date_published = fields.Datetime('Publish Date', readonly=True, tracking=True) + date_published = fields.Datetime('Publish Date', readonly=True, tracking=1) likes = fields.Integer('Likes', compute='_compute_user_info', store=True, compute_sudo=False) dislikes = fields.Integer('Dislikes', compute='_compute_user_info', store=True, compute_sudo=False) user_vote = fields.Integer('User vote', compute='_compute_user_info', compute_sudo=False)