diff --git a/fieldservice_recurring/__manifest__.py b/fieldservice_recurring/__manifest__.py index 6982ef81ee..3ced537919 100644 --- a/fieldservice_recurring/__manifest__.py +++ b/fieldservice_recurring/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Field Service Recurring Work Orders", "summary": "Manage recurring Field Service orders", - "version": "16.0.1.0.1", + "version": "16.0.2.0.0", "category": "Field Service", "author": "Brian McMaster, " "Open Source Integrators, " @@ -21,6 +21,7 @@ "views/fsm_order.xml", "views/fsm_recurring_template.xml", "views/fsm_recurring.xml", + "views/fsm_team.xml", "data/recurring_cron.xml", ], "demo": [ diff --git a/fieldservice_recurring/migrations/16.0.2.0.0/pre-migration.py b/fieldservice_recurring/migrations/16.0.2.0.0/pre-migration.py new file mode 100644 index 0000000000..9c3f2510f3 --- /dev/null +++ b/fieldservice_recurring/migrations/16.0.2.0.0/pre-migration.py @@ -0,0 +1,7 @@ +# Copyright (C) 2022 Raphaël Reverdy +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +def migrate(env, version): + env.execute("UPDATE fsm_recurring SET state = 'close' WHERE state = 'cancel';") + env.execute("UPDATE fsm_recurring SET state = 'progress' WHERE state = 'pending';") diff --git a/fieldservice_recurring/models/__init__.py b/fieldservice_recurring/models/__init__.py index 9d2cd7b25b..35c9f9fbaf 100644 --- a/fieldservice_recurring/models/__init__.py +++ b/fieldservice_recurring/models/__init__.py @@ -7,4 +7,5 @@ fsm_frequency, fsm_recurring_template, fsm_recurring, + fsm_team, ) diff --git a/fieldservice_recurring/models/fsm_frequency.py b/fieldservice_recurring/models/fsm_frequency.py index 1be5cc2177..9e03cd7cf7 100644 --- a/fieldservice_recurring/models/fsm_frequency.py +++ b/fieldservice_recurring/models/fsm_frequency.py @@ -1,6 +1,7 @@ # Copyright (C) 2019 Brian McMaster, Open Source Integrators # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import pytz from dateutil.rrule import ( DAILY, FR, @@ -114,18 +115,47 @@ def _check_month_day(self): if not (1 <= rec.month_day <= 31): raise UserError(_("'Day of Month must be between 1 and 31")) - def _get_rrule(self, dtstart=None, until=None): + def _get_rrule(self, dtstart=None, until=None, tz=None): self.ensure_one() freq = FREQUENCIES[self.interval_type] - return rrule( - freq, - interval=self.interval, - dtstart=dtstart, - until=until, - byweekday=self._byweekday(), - bymonth=self._bymonth(), - bymonthday=self._bymonthday(), - bysetpos=self._bysetpos(), + # localize dtstart and until to user timezone + tz = pytz.timezone( + tz or self._context.get("tz", None) or self.env.user.tz or "UTC" + ) + + if dtstart: + dtstart = pytz.timezone("UTC").localize(dtstart).astimezone(tz) + if until: + until = pytz.timezone("UTC").localize(until).astimezone(tz) + # We force until in the starting timezone to avoid incoherent results + until = tz.normalize(until.replace(tzinfo=dtstart.tzinfo)) + + return ( + # Replace original timezone with current date timezone + # without changing the time and force it back to UTC, + # this will keep the same final time even in case of + # daylight saving time change + # + # for instance recurring weekly + # from 2022-03-21 15:00:00+01:00 to 2022-04-11 15:30:00+02:00 + # will give: + # + # utc naive -> datetime timezone aware + # 2022-03-21 14:00:00 -> 2022-03-21 15:00:00+01:00 + # 2022-03-28 13:00:00 -> 2022-03-28 15:00:00+02:00 + date.replace(tzinfo=tz.normalize(date).tzinfo) + .astimezone(pytz.UTC) + .replace(tzinfo=None) + for date in rrule( + freq, + interval=self.interval, + dtstart=dtstart, + until=until, + byweekday=self._byweekday(), + bymonth=self._bymonth(), + bymonthday=self._bymonthday(), + bysetpos=self._bysetpos(), + ) ) def _byweekday(self): diff --git a/fieldservice_recurring/models/fsm_frequency_set.py b/fieldservice_recurring/models/fsm_frequency_set.py index dfa8425f20..9b9e159356 100644 --- a/fieldservice_recurring/models/fsm_frequency_set.py +++ b/fieldservice_recurring/models/fsm_frequency_set.py @@ -36,12 +36,12 @@ class FSMFrequencySet(models.Model): that an event can be done""", ) - def _get_rruleset(self, dtstart=None, until=None): + def _get_rruleset(self, dtstart=None, until=None, tz=None): self.ensure_one() rset = rruleset() for rule in self.fsm_frequency_ids: if not rule.is_exclusive: - rset.rrule(rule._get_rrule(dtstart, until)) + rset.rrule(rule._get_rrule(dtstart, until, tz)) else: - rset.exrule(rule._get_rrule(dtstart)) + rset.exrule(rule._get_rrule(dtstart, tz=tz)) return rset diff --git a/fieldservice_recurring/models/fsm_order.py b/fieldservice_recurring/models/fsm_order.py index 03a2a0aabe..867d753b7a 100644 --- a/fieldservice_recurring/models/fsm_order.py +++ b/fieldservice_recurring/models/fsm_order.py @@ -30,7 +30,9 @@ def create(self, vals_list): return super().create(vals_list) def action_view_fsm_recurring(self): - action = self.env.ref("fieldservice_recurring.action_fsm_recurring").read()[0] + action = self.env["ir.actions.act_window"]._for_xml_id( + "fieldservice_recurring.action_fsm_recurring" + ) action["views"] = [ (self.env.ref("fieldservice_recurring.fsm_recurring_form_view").id, "form") ] diff --git a/fieldservice_recurring/models/fsm_recurring.py b/fieldservice_recurring/models/fsm_recurring.py index 1a78b2a178..5d0f7d467a 100644 --- a/fieldservice_recurring/models/fsm_recurring.py +++ b/fieldservice_recurring/models/fsm_recurring.py @@ -15,7 +15,11 @@ class FSMRecurringOrder(models.Model): _inherit = ["mail.thread", "mail.activity.mixin"] def _default_team_id(self): - return self.env.ref("fieldservice.fsm_team_default") + return self.env["fsm.team"].search( + [["company_id", "in", (self.env.company.id, False)]], + limit=1, + order="sequence", + ) name = fields.Char( required=True, @@ -27,9 +31,8 @@ def _default_team_id(self): [ ("draft", "Draft"), ("progress", "In Progress"), - ("pending", "To Renew"), + ("suspend", "Suspended"), ("close", "Closed"), - ("cancel", "Cancelled"), ], readonly=True, default="draft", @@ -79,6 +82,7 @@ def _default_team_id(self): person_id = fields.Many2one( "fsm.person", string="Assigned To", index=True, tracking=True ) + equipment_ids = fields.Many2many("fsm.equipment") @api.depends("fsm_order_ids") def _compute_order_count(self): @@ -130,15 +134,12 @@ def action_start(self): rec.write({"state": "progress"}) rec._generate_orders() - def action_renew(self): - return self.action_start() - - def action_cancel(self): + def action_suspend(self): for order in self.fsm_order_ids.filtered( lambda o: o.stage_id.is_closed is False ): order.action_cancel() - return self.write({"state": "cancel"}) + return self.write({"state": "suspend"}) def _get_rruleset(self): self.ensure_one() @@ -191,6 +192,7 @@ def _prepare_order_values(self, date=None): "category_ids": [(6, False, self.fsm_order_template_id.category_ids.ids)], "company_id": self.company_id.id, "person_id": self.person_id.id, + "equipment_ids": [(6, 0, self.equipment_ids.ids)], } def _create_order(self, date): @@ -232,44 +234,26 @@ def _cron_generate_orders(self): """ return ( self.env["fsm.recurring"] - .search([("state", "in", ("progress", "pending"))]) + .search([("state", "=", "progress")]) ._generate_orders() ) @api.model def _cron_manage_expiration(self): """ - Executed by Cron task to put all 'pending' recurring orders into + Executed by Cron task to put all 'in progress' recurring orders into 'close' stage if it is after their end date or the max orders have - been generated. Next, the 'progress' recurring orders are put in - 'pending' stage by first checking if the end date is within the next - 30 days and then checking if the max number of orders will be created - within the next 30 days + been generated. """ to_close = self.env["fsm.recurring"] - pending_rec = self.env["fsm.recurring"].search([("state", "=", "pending")]) - for rec in pending_rec: + open_rec = self.env["fsm.recurring"].search([("state", "=", "progress")]) + for rec in open_rec: if rec.end_date and rec.end_date <= datetime.today(): to_close += rec continue if rec.max_orders > 0 and rec.fsm_order_count >= rec.max_orders: to_close += rec to_close.write({"state": "close"}) - to_renew = self.env["fsm.recurring"] - expire_date = datetime.today() + relativedelta(days=+30) - open_rec = self.env["fsm.recurring"].search([("state", "=", "progress")]) - for rec in open_rec: - if rec.end_date and rec.end_date <= expire_date: - to_renew += rec - continue - if rec.max_orders > 0: - orders_in_30 = rec.fsm_order_count - orders_in_30 += rec.fsm_frequency_set_id._get_rruleset( - until=expire_date - ).count() - if orders_in_30 >= rec.max_orders: - to_renew += rec - to_renew.write({"state": "pending"}) @api.model def _cron_scheduled_task(self): diff --git a/fieldservice_recurring/models/fsm_team.py b/fieldservice_recurring/models/fsm_team.py new file mode 100644 index 0000000000..988db6cffa --- /dev/null +++ b/fieldservice_recurring/models/fsm_team.py @@ -0,0 +1,25 @@ +# Copyright (C) 2022 Raphaël Reverdy (Akretion) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FSMTeam(models.Model): + _inherit = "fsm.team" + + def _compute_recurring_draft_count(self): + order_data = self.env["fsm.recurring"].read_group( + [ + ("team_id", "in", self.ids), + ("state", "=", "draft"), + ], + ["team_id"], + ["team_id"], + ) + result = {data["team_id"][0]: int(data["team_id_count"]) for data in order_data} + for team in self: + team.recurring_draft_count = result.get(team.id, 0) + + recurring_draft_count = fields.Integer( + compute="_compute_recurring_draft_count", string="Recurring in draft" + ) diff --git a/fieldservice_recurring/tests/test_fsm_recurring.py b/fieldservice_recurring/tests/test_fsm_recurring.py index 0aafb3ce17..573f38503f 100644 --- a/fieldservice_recurring/tests/test_fsm_recurring.py +++ b/fieldservice_recurring/tests/test_fsm_recurring.py @@ -15,6 +15,7 @@ class FSMRecurringCase(TransactionCase): @classmethod def setUpClass(cls): super(FSMRecurringCase, cls).setUpClass() + cls.Equipment = cls.env["fsm.equipment"] cls.Recurring = cls.env["fsm.recurring"] cls.Frequency = cls.env["fsm.frequency"] cls.FrequencySet = cls.env["fsm.frequency.set"] @@ -54,6 +55,20 @@ def setUpClass(cls): cls.fsm_recurring_template = cls.env["fsm.recurring.template"].create( {"name": "Test Template"} ) + cls.test_equipment = cls.Equipment.create({"name": "Equipment"}) + + def test_fsm_recurring_change_states(self): + recurring = self.Recurring.create( + { + "fsm_frequency_set_id": self.fr_set.id, + "location_id": self.test_location.id, + "start_date": fields.Datetime.now().replace(hour=12), + } + ) + recurring.action_start() + self.assertEqual(recurring.state, "progress") + recurring.action_suspend() + self.assertEqual(recurring.state, "suspend") def test_cron_generate_orders_rule1(self): """Test recurring order with following rule, @@ -99,7 +114,8 @@ def test_cron_generate_orders_rule1(self): { "fsm_frequency_set_id": fr_set.id, "location_id": self.test_location.id, - "start_date": fields.Datetime.today(), + "start_date": fields.Datetime.now().replace(hour=12), + "equipment_ids": [(6, 0, [self.test_equipment.id])], } ) test_recurring = self.Recurring.create( @@ -111,7 +127,6 @@ def test_cron_generate_orders_rule1(self): ) recurring.action_start() test_recurring.action_start() - test_recurring.action_renew() # Run schedule job now, to compute the future work orders recurring._cron_scheduled_task() recurring.onchange_recurring_template_id() @@ -177,7 +192,7 @@ def test_cron_generate_orders_rule2(self): { "fsm_frequency_set_id": fr_set.id, "location_id": self.test_location.id, - "start_date": fields.Datetime.today(), + "start_date": fields.Datetime.now().replace(hour=12), "end_date": expire_date1, } ) @@ -185,7 +200,7 @@ def test_cron_generate_orders_rule2(self): { "fsm_frequency_set_id": fr_set.id, "location_id": self.test_location.id, - "start_date": fields.Datetime.today(), + "start_date": fields.Datetime.now().replace(hour=12), "max_orders": 1, } ) @@ -193,7 +208,7 @@ def test_cron_generate_orders_rule2(self): { "fsm_frequency_set_id": fr_set.id, "location_id": self.test_location.id, - "start_date": fields.Datetime.today(), + "start_date": fields.Datetime.now().replace(hour=12), "max_orders": 1, } ) @@ -210,7 +225,7 @@ def test_cron_generate_orders_rule2(self): x = False for d in all_dates: if x: - diff_days = (d - x).days + diff_days = (d.date() - x.date()).days self.assertEqual(diff_days, 21) x = d @@ -252,7 +267,7 @@ def test_cron_generate_orders_rule3(self): { "fsm_frequency_set_id": fr_set.id, "location_id": self.test_location.id, - "start_date": fields.Datetime.today(), + "start_date": fields.Datetime.now().replace(hour=12), } ) recurring.action_start() @@ -292,4 +307,3 @@ def test_fsm_order(self): fsm_order = self.env["fsm.order"].create(order_vals) self.env["fsm.order"].create(order_vals2) fsm_order.action_view_fsm_recurring() - recurring.action_cancel() diff --git a/fieldservice_recurring/views/fsm_order.xml b/fieldservice_recurring/views/fsm_order.xml index 468c73dd46..527786bbe2 100644 --- a/fieldservice_recurring/views/fsm_order.xml +++ b/fieldservice_recurring/views/fsm_order.xml @@ -10,6 +10,7 @@ class="oe_stat_button" icon="fa-calendar" attrs="{'invisible': [('fsm_recurring_id', '=', False)]}" + groups="fieldservice_recurring.group_fsm_recurring" > Recurring Order diff --git a/fieldservice_recurring/views/fsm_recurring.xml b/fieldservice_recurring/views/fsm_recurring.xml index ef3824081a..3b559ce01d 100644 --- a/fieldservice_recurring/views/fsm_recurring.xml +++ b/fieldservice_recurring/views/fsm_recurring.xml @@ -14,6 +14,7 @@ + @@ -26,28 +27,19 @@