From 634629fedc814fcf5531ff4be2fa1d1a77840b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Mart=C3=ADnez?= Date: Fri, 8 Mar 2024 15:28:06 +0100 Subject: [PATCH] [IMP] crm_salesperson_planner: Refactoring of the template to make it usable and with simplified code. TT48238 --- .../crm_salesperson_planner_visit_template.py | 245 +++++++++++++----- .../test_crm_salesperson_planner_visit.py | 14 + ..._crm_salesperson_planner_visit_template.py | 139 +++++++++- ...lesperson_planner_visit_template_views.xml | 2 - ...lesperson_planner_visit_template_create.py | 7 +- 5 files changed, 330 insertions(+), 77 deletions(-) diff --git a/crm_salesperson_planner/models/crm_salesperson_planner_visit_template.py b/crm_salesperson_planner/models/crm_salesperson_planner_visit_template.py index dbddc433f0d..9771d7e57d2 100644 --- a/crm_salesperson_planner/models/crm_salesperson_planner_visit_template.py +++ b/crm_salesperson_planner/models/crm_salesperson_planner_visit_template.py @@ -8,21 +8,51 @@ from odoo import _, api, fields, models from odoo.exceptions import ValidationError +from odoo.addons.base.models.res_partner import _tz_get +from odoo.addons.calendar.models.calendar_recurrence import ( + BYDAY_SELECTION, + END_TYPE_SELECTION, + MONTH_BY_SELECTION, + RRULE_TYPE_SELECTION, + WEEKDAY_SELECTION, +) + class CrmSalespersonPlannerVisitTemplate(models.Model): _name = "crm.salesperson.planner.visit.template" _description = "Crm Salesperson Planner Visit Template" - _inherit = "calendar.event" + _inherit = ["mail.thread"] + # We cannot inherit from calendar.event for several reasons: + # 1- There are many compute recursion fields that would not allow to change them. + # 2- Recurrence is only created correctly if the model is calendar.event + # 3- We want to generate visits ("events") manually when we want and only the ones + # we want. name = fields.Char( string="Visit Template Number", default="/", readonly=True, copy=False, ) + description = fields.Html() + user_id = fields.Many2one( + comodel_name="res.users", + string="Salesperson", + tracking=True, + default=lambda self: self.env.user, + domain=lambda self: [ + ("groups_id", "in", self.env.ref("sales_team.group_sale_salesman").id) + ], + ) + partner_id = fields.Many2one( + comodel_name="res.partner", + string="Scheduled by", + related="user_id.partner_id", + readonly=True, + ) partner_ids = fields.Many2many( + comodel_name="res.partner", string="Customer", - relation="salesperson_planner_res_partner_rel", default=False, required=True, ) @@ -35,18 +65,13 @@ class CrmSalespersonPlannerVisitTemplate(models.Model): string="Company", default=lambda self: self.env.company, ) - user_id = fields.Many2one( - string="Salesperson", - tracking=True, - default=lambda self: self.env.user, - domain=lambda self: [ - ("groups_id", "in", self.env.ref("sales_team.group_sale_salesman").id) - ], - ) - categ_ids = fields.Many2many( - relation="visit_category_rel", + categ_ids = fields.Many2many(comodel_name="calendar.event.type", string="Tags") + alarm_ids = fields.Many2many( + comodel_name="calendar.alarm", + string="Reminders", + ondelete="restrict", + help="Notifications sent to all attendees to remind of the meeting.", ) - alarm_ids = fields.Many2many(relation="visit_calendar_event_rel") state = fields.Selection( string="Status", required=True, @@ -71,29 +96,62 @@ class CrmSalespersonPlannerVisitTemplate(models.Model): auto_validate = fields.Boolean(default=True) last_visit_date = fields.Date(compute="_compute_last_visit_date", store=True) final_date = fields.Date(string="Repeat Until") - allday = fields.Boolean(default=True) - # Set all compute=_compute_recurrence fields of calendar.event as store=True. - # We want to manage the value of the fields manually and we don't want to depend - # on recurrence_id field (only possible with calendar.event). - # We don't use the recurrency field either because it is unnecessary. - rrule = fields.Char(store=True) - rrule_type = fields.Selection(store=True, default="daily", required=True) - event_tz = fields.Selection(store=True) - end_type = fields.Selection(store=True) - interval = fields.Integer(store=True) - count = fields.Integer(store=True) - mon = fields.Boolean(store=True) - tue = fields.Boolean(store=True) - wed = fields.Boolean(store=True) - thu = fields.Boolean(store=True) - fri = fields.Boolean(store=True) - sat = fields.Boolean(store=True) - sun = fields.Boolean(store=True) - month_by = fields.Selection(store=True) - day = fields.Integer(store=True) - weekday = fields.Selection(store=True) - byday = fields.Selection(store=True) - until = fields.Date(store=True) + start = fields.Datetime( + required=True, + tracking=True, + default=fields.Date.today, + help="Start date of an event, without time for full days events", + ) + stop = fields.Datetime( + required=True, + tracking=True, + default=lambda self: fields.Datetime.today() + timedelta(hours=1), + compute="_compute_stop", + readonly=False, + store=True, + help="Stop date of an event, without time for full days events", + ) + allday = fields.Boolean(string="All Day", default=True) + start_date = fields.Date( + store=True, + tracking=True, + compute="_compute_dates", + inverse="_inverse_dates", + ) + stop_date = fields.Date( + string="End Date", + store=True, + tracking=True, + compute="_compute_dates", + inverse="_inverse_dates", + ) + duration = fields.Float(compute="_compute_duration", store=True, readonly=False) + rrule = fields.Char(string="Recurrent Rule") + rrule_type = fields.Selection( + RRULE_TYPE_SELECTION, + string="Recurrence", + help="Let the event automatically repeat at that interval", + default="daily", + required=True, + ) + event_tz = fields.Selection(_tz_get, string="Timezone") + end_type = fields.Selection(END_TYPE_SELECTION, string="Recurrence Termination") + interval = fields.Integer( + string="Repeat Every", help="Repeat every (Days/Week/Month/Year)" + ) + count = fields.Integer(string="Repeat", help="Repeat x times") + mon = fields.Boolean() + tue = fields.Boolean() + wed = fields.Boolean() + thu = fields.Boolean() + fri = fields.Boolean() + sat = fields.Boolean() + sun = fields.Boolean() + month_by = fields.Selection(MONTH_BY_SELECTION, string="Option") + day = fields.Integer(string="Date of month") + weekday = fields.Selection(WEEKDAY_SELECTION) + byday = fields.Selection(BYDAY_SELECTION) + until = fields.Date() _sql_constraints = [ ( @@ -120,6 +178,55 @@ def _compute_last_visit_date(self): for sel in self.filtered(lambda x: x.visit_ids): sel.last_visit_date = sel.visit_ids.sorted(lambda x: x.date)[-1].date + @api.depends("start", "duration") + def _compute_stop(self): + """Same method as in calendar.event.""" + for item in self: + item.stop = item.start and item.start + timedelta( + minutes=round((item.duration or 1.0) * 60) + ) + if item.allday: + item.stop -= timedelta(seconds=1) + + @api.depends("allday", "start", "stop") + def _compute_dates(self): + """Same method as in calendar.event.""" + for item in self: + if item.allday and item.start and item.stop: + item.start_date = item.start.date() + item.stop_date = item.stop.date() + else: + item.start_date = False + item.stop_date = False + + @api.depends("stop", "start") + def _compute_duration(self): + """Same method as in calendar.event.""" + for item in self: + item.duration = self._get_duration(item.start, item.stop) + + def _get_duration(self, start, stop): + """Same method as in calendar.event.""" + if not start or not stop: + return 0 + duration = (stop - start).total_seconds() / 3600 + return round(duration, 2) + + def _inverse_dates(self): + """Same method as in calendar.event.""" + for item in self: + if item.allday: + enddate = fields.Datetime.from_string(item.stop_date) + enddate = enddate.replace(hour=18) + startdate = fields.Datetime.from_string(item.start_date) + startdate = startdate.replace(hour=8) + item.write( + { + "start": startdate.replace(tzinfo=None), + "stop": enddate.replace(tzinfo=None), + } + ) + @api.constrains("partner_ids") def _constrains_partner_ids(self): for item in self: @@ -146,12 +253,6 @@ def create(self, vals_list): ) return super().create(vals_list) - # overwrite - # Calling _update_cron from default write funciont is not - # necessary in this case - def write(self, vals): - return super(models.Model, self).write(vals) - def action_view_salesperson_planner_visit(self): action = self.env["ir.actions.act_window"]._for_xml_id( "crm_salesperson_planner.all_crm_salesperson_planner_visit_action" @@ -189,29 +290,51 @@ def _prepare_crm_salesperson_planner_visit_vals(self, dates): for date in dates ] + # Get the date range from calendar.recurrence, that way the values obtained will + # be correct (except for incompatible cases). + def _get_start_range_dates(self): + """Method to get all dates (sorted) in the range.""" + duration = self.stop - self.start + ranges = ( + self.env["calendar.recurrence"] + .new( + { + "rrule_type": self.rrule_type, + "interval": self.interval, + "month_by": self.month_by, + "weekday": self.weekday, + "byday": self.byday, + "count": self.count, + "end_type": self.end_type, + "until": self.until, + "mon": self.mon, + "tue": self.tue, + "wed": self.wed, + "thu": self.thu, + "fri": self.fri, + "sat": self.sat, + "sun": self.sun, + } + ) + ._range_calculation(self, duration) + ) + start_dates = [] + for start, _stop in ranges: + start_dates.append(start.date()) + return sorted(start_dates) + def _get_max_date(self): - return self.until or self._increase_date(self.start_date, self.count) - - def _increase_date(self, date, value): - if self.rrule_type == "daily": - date += timedelta(days=value) - elif self.rrule_type == "weekly": - date += timedelta(weeks=value) - elif self.rrule_type == "monthly": - date += timedelta(months=value) - elif self.rrule_type == "yearly": - date += timedelta(years=value) - return date + """The maximum date will be the last of the range.""" + return self._get_start_range_dates()[-1] def _get_recurrence_dates(self, items): + """For the n items, get only those that are not already generated.""" + start_dates = self._get_start_range_dates() dates = [] - max_date = self._get_max_date() - from_date = self._increase_date(self.last_visit_date or self.start_date, 1) - if max_date > from_date: - for _x in range(items): - if from_date <= max_date: - dates.append(from_date) - from_date = self._increase_date(from_date, 1) + visit_dates = self.visit_ids.mapped("date") + for _date in start_dates[:items]: + if _date not in visit_dates: + dates.append(_date) return dates def _create_visits(self, days=7): diff --git a/crm_salesperson_planner/tests/test_crm_salesperson_planner_visit.py b/crm_salesperson_planner/tests/test_crm_salesperson_planner_visit.py index e252429a1cc..3d1a8381e8c 100644 --- a/crm_salesperson_planner/tests/test_crm_salesperson_planner_visit.py +++ b/crm_salesperson_planner/tests/test_crm_salesperson_planner_visit.py @@ -5,12 +5,23 @@ from odoo import fields from odoo.tests import common +from odoo.tools import mute_logger class TestCrmSalespersonPlannerVisitBase(common.TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() + cls.env = cls.env( + context=dict( + cls.env.context, + mail_create_nolog=True, + mail_create_nosubscribe=True, + mail_notrack=True, + no_reset_password=True, + tracking_disable=True, + ) + ) cls.visit_model = cls.env["crm.salesperson.planner.visit"] cls.partner_model = cls.env["res.partner"] cls.close_model = cls.env["crm.salesperson.planner.visit.close.reason"] @@ -94,6 +105,7 @@ def config_close_wiz(self, att_close_type, vals): ) close_wiz.action_close_reason_apply() + @mute_logger("odoo.models.unlink") def test_crm_salesperson_close_wiz_cancel(self): self.visit1.action_confirm() self.assertEqual(self.visit1.state, "confirm") @@ -108,6 +120,7 @@ def test_crm_salesperson_close_wiz_cancel(self): 2, ) + @mute_logger("odoo.models.unlink") def test_crm_salesperson_close_wiz_cancel_resch(self): self.visit1.action_confirm() self.assertEqual(self.visit1.state, "confirm") @@ -132,6 +145,7 @@ def test_crm_salesperson_close_wiz_cancel_resch(self): 1, ) + @mute_logger("odoo.models.unlink") def test_crm_salesperson_close_wiz_cancel_img(self): self.visit1.action_confirm() self.assertEqual(self.visit1.state, "confirm") diff --git a/crm_salesperson_planner/tests/test_crm_salesperson_planner_visit_template.py b/crm_salesperson_planner/tests/test_crm_salesperson_planner_visit_template.py index 4cf6add1e07..2df099e56d6 100644 --- a/crm_salesperson_planner/tests/test_crm_salesperson_planner_visit_template.py +++ b/crm_salesperson_planner/tests/test_crm_salesperson_planner_visit_template.py @@ -1,17 +1,28 @@ # Copyright 2021 Sygel - Valentin Vinagre # Copyright 2021 Sygel - Manuel Regidor +# Copyright 2024 Tecnativa - Víctor Martínez # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) - from datetime import timedelta from odoo import exceptions, fields from odoo.tests import common +from odoo.tools import mute_logger class TestCrmSalespersonPlannerVisitTemplate(common.TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() + cls.env = cls.env( + context=dict( + cls.env.context, + mail_create_nolog=True, + mail_create_nosubscribe=True, + mail_notrack=True, + no_reset_password=True, + tracking_disable=True, + ) + ) cls.visit_template_model = cls.env["crm.salesperson.planner.visit.template"] cls.partner_model = cls.env["res.partner"] cls.close_reason_mode = cls.env["crm.salesperson.planner.visit.close.reason"] @@ -47,7 +58,7 @@ def test_01_repeat_days(self): ) self.visit_template_base.action_validate() self.visit_template_base.create_visits(days=4) - self.assertEqual(self.visit_template_base.visit_ids_count, 4) + self.assertEqual(len(self.visit_template_base.visit_ids), 4) self.assertEqual( len( self.visit_template_base.visit_ids.filtered( @@ -65,9 +76,8 @@ def test_01_repeat_days(self): 0, ) self.assertEqual(self.visit_template_base.state, "in-progress") - self.visit_template_base.create_visits(days=9) - self.visit_template_base._compute_visit_ids_count() - self.assertEqual(self.visit_template_base.visit_ids_count, 10) + self.visit_template_base.create_visits(days=10) + self.assertEqual(len(self.visit_template_base.visit_ids), 10) self.assertEqual( len( self.visit_template_base.visit_ids.filtered( @@ -98,7 +108,7 @@ def test_02_repeat_days_autovalidate(self): ) self.visit_template_base.action_validate() self.visit_template_base.create_visits(days=4) - self.assertEqual(self.visit_template_base.visit_ids_count, 4) + self.assertEqual(len(self.visit_template_base.visit_ids), 4) self.assertEqual( len( self.visit_template_base.visit_ids.filtered( @@ -116,9 +126,8 @@ def test_02_repeat_days_autovalidate(self): 4, ) self.assertEqual(self.visit_template_base.state, "in-progress") - self.visit_template_base.create_visits(days=9) - self.visit_template_base._compute_visit_ids_count() - self.assertEqual(self.visit_template_base.visit_ids_count, 10) + self.visit_template_base.create_visits(days=10) + self.assertEqual(len(self.visit_template_base.visit_ids), 10) self.assertEqual( len( self.visit_template_base.visit_ids.filtered( @@ -162,6 +171,7 @@ def test_03_change_visit_date(self): ) self.assertEqual(visit_0.date, fields.Date.today() + timedelta(days=14)) + @mute_logger("odoo.models.unlink") def test_04_cancel_visit(self): visit_template = self.visit_template_base.copy() visit_template.write( @@ -183,3 +193,114 @@ def test_04_cancel_visit(self): self.assertFalse(first_visit.calendar_event_id) first_visit.unlink() self.assertEqual(len(visit_template.visit_ids), 9) + + def test_05_repeat_weeks(self): + self.visit_template_base.write( + { + "start_date": "2024-03-08", + "interval": 1, + "rrule_type": "weekly", + "tue": True, + "end_type": "end_date", + "until": "2024-07-02", + } + ) + self.visit_template_base.action_validate() + self.assertFalse(self.visit_template_base.visit_ids) + create_model = self.env["crm.salesperson.planner.visit.template.create"] + create_item = create_model.with_context( + active_id=self.visit_template_base.id + ).create({"date_to": "2024-07-02"}) + create_item.create_visits() + self.assertEqual(self.visit_template_base.state, "done") + visit_dates = self.visit_template_base.visit_ids.mapped("date") + self.assertIn(fields.Date.from_string("2024-03-19"), visit_dates) + self.assertEqual( + self.visit_template_base.last_visit_date, + fields.Date.from_string("2024-07-02"), + ) + + def test_06_repeat_months_count_01(self): + self.visit_template_base.write( + { + "start_date": "2024-03-08", + "interval": 1, + "rrule_type": "monthly", + "end_type": "count", + "count": 2, + "month_by": "date", + "day": 1, + } + ) + self.visit_template_base.action_validate() + self.assertFalse(self.visit_template_base.visit_ids) + create_model = self.env["crm.salesperson.planner.visit.template.create"] + create_item = create_model.with_context( + active_id=self.visit_template_base.id + ).create({"date_to": "2024-12-13"}) + create_item.create_visits() + self.assertEqual(self.visit_template_base.state, "done") + self.assertEqual(len(self.visit_template_base.visit_ids), 2) + visit_dates = self.visit_template_base.visit_ids.mapped("date") + self.assertIn(fields.Date.from_string("2024-04-01"), visit_dates) + self.assertEqual( + self.visit_template_base.last_visit_date, + fields.Date.from_string("2024-05-01"), + ) + + def test_06_repeat_months_count_02(self): + self.visit_template_base.write( + { + "start_date": "2024-03-08", + "interval": 1, + "rrule_type": "monthly", + "end_type": "count", + "count": 2, + "month_by": "date", + "day": 1, + } + ) + self.visit_template_base.action_validate() + self.assertFalse(self.visit_template_base.visit_ids) + create_model = self.env["crm.salesperson.planner.visit.template.create"] + create_item = create_model.with_context( + active_id=self.visit_template_base.id + ).create({"date_to": "2024-12-13"}) + create_item.create_visits() + self.assertEqual(self.visit_template_base.state, "done") + self.assertEqual(len(self.visit_template_base.visit_ids), 2) + visit_dates = self.visit_template_base.visit_ids.mapped("date") + self.assertIn(fields.Date.from_string("2024-04-01"), visit_dates) + self.assertEqual( + self.visit_template_base.last_visit_date, + fields.Date.from_string("2024-05-01"), + ) + + def test_06_repeat_months_count_03(self): + self.visit_template_base.write( + { + "start_date": "2024-03-08", + "interval": 1, + "rrule_type": "monthly", + "end_type": "count", + "count": 2, + "month_by": "day", + "byday": "1", + "weekday": "MON", + } + ) + self.visit_template_base.action_validate() + self.assertFalse(self.visit_template_base.visit_ids) + create_model = self.env["crm.salesperson.planner.visit.template.create"] + create_item = create_model.with_context( + active_id=self.visit_template_base.id + ).create({"date_to": "2024-12-13"}) + create_item.create_visits() + self.assertEqual(self.visit_template_base.state, "done") + self.assertEqual(len(self.visit_template_base.visit_ids), 2) + visit_dates = self.visit_template_base.visit_ids.mapped("date") + self.assertIn(fields.Date.from_string("2024-04-01"), visit_dates) + self.assertEqual( + self.visit_template_base.last_visit_date, + fields.Date.from_string("2024-05-06"), + ) diff --git a/crm_salesperson_planner/views/crm_salesperson_planner_visit_template_views.xml b/crm_salesperson_planner/views/crm_salesperson_planner_visit_template_views.xml index 503d466f96d..875bc0e0e7f 100644 --- a/crm_salesperson_planner/views/crm_salesperson_planner_visit_template_views.xml +++ b/crm_salesperson_planner/views/crm_salesperson_planner_visit_template_views.xml @@ -91,8 +91,6 @@ /> - - diff --git a/crm_salesperson_planner/wizards/crm_salesperson_planner_visit_template_create.py b/crm_salesperson_planner/wizards/crm_salesperson_planner_visit_template_create.py index 1746757241a..ea278d38382 100644 --- a/crm_salesperson_planner/wizards/crm_salesperson_planner_visit_template_create.py +++ b/crm_salesperson_planner/wizards/crm_salesperson_planner_visit_template_create.py @@ -29,9 +29,6 @@ def create_visits(self): days = (self.date_to - fields.Date.context_today(self)).days if days < 0: raise ValidationError(_("The date can't be earlier than today")) - visits = self.env["crm.salesperson.planner.visit"].create( - template._create_visits(days=days) - ) - if visits and template.auto_validate: - visits.action_confirm() + # Create visits + auto-confirm + auto-done + template.create_visits(days=days) return {"type": "ir.actions.act_window_close"}