Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[16.0] fieldservice_recurring: Forward ports from 14 #1137

Merged
merged 14 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion fieldservice_recurring/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, "
Expand All @@ -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": [
Expand Down
7 changes: 7 additions & 0 deletions fieldservice_recurring/migrations/16.0.2.0.0/pre-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Copyright (C) 2022 Raphaël Reverdy <raphael.reverdy@akretion.com>
# 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';")
1 change: 1 addition & 0 deletions fieldservice_recurring/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
fsm_frequency,
fsm_recurring_template,
fsm_recurring,
fsm_team,
)
50 changes: 40 additions & 10 deletions fieldservice_recurring/models/fsm_frequency.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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):
Expand Down
6 changes: 3 additions & 3 deletions fieldservice_recurring/models/fsm_frequency_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion fieldservice_recurring/models/fsm_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
]
Expand Down
46 changes: 15 additions & 31 deletions fieldservice_recurring/models/fsm_recurring.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
25 changes: 25 additions & 0 deletions fieldservice_recurring/models/fsm_team.py
Original file line number Diff line number Diff line change
@@ -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"
)
30 changes: 22 additions & 8 deletions fieldservice_recurring/tests/test_fsm_recurring.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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()
Expand Down Expand Up @@ -177,23 +192,23 @@ 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,
}
)
test_recurring = self.Recurring.create(
{
"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,
}
)
test_recurring1 = self.Recurring.create(
{
"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,
}
)
Expand All @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
1 change: 1 addition & 0 deletions fieldservice_recurring/views/fsm_order.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
class="oe_stat_button"
icon="fa-calendar"
attrs="{'invisible': [('fsm_recurring_id', '=', False)]}"
groups="fieldservice_recurring.group_fsm_recurring"
>
<span class="o_stat_text">Recurring Order</span>
<field name="fsm_recurring_id" attrs="{'invisible': 1}" />
Expand Down
Loading
Loading