From e377abdc35af100e54f14ba9408c70b0e37acf77 Mon Sep 17 00:00:00 2001 From: brian10048 Date: Mon, 13 May 2019 09:21:50 -0400 Subject: [PATCH 001/138] [ADD][11.0] fieldservice_recurring (#179) [ADD] fieldservice_recurring --- fieldservice_recurring/README.rst | 156 ++++++ fieldservice_recurring/__init__.py | 4 + fieldservice_recurring/__manifest__.py | 35 ++ fieldservice_recurring/data/ir_sequence.xml | 13 + .../data/recurring_cron.xml | 15 + fieldservice_recurring/models/__init__.py | 10 + .../models/fsm_frequency.py | 148 ++++++ .../models/fsm_frequency_set.py | 40 ++ fieldservice_recurring/models/fsm_order.py | 23 + .../models/fsm_recurring.py | 239 +++++++++ .../models/fsm_recurring_template.py | 25 + fieldservice_recurring/readme/CONFIGURE.rst | 26 + .../readme/CONTRIBUTORS.rst | 1 + fieldservice_recurring/readme/CREDITS.rst | 3 + fieldservice_recurring/readme/DESCRIPTION.rst | 6 + fieldservice_recurring/readme/INSTALL.rst | 4 + fieldservice_recurring/readme/ROADMAP.rst | 2 + fieldservice_recurring/readme/USAGE.rst | 8 + .../security/ir.model.access.csv | 7 + .../security/recurring_security.xml | 25 + .../security/res_groups.xml | 15 + .../static/description/icon.png | Bin 0 -> 17808 bytes .../static/description/index.html | 494 ++++++++++++++++++ .../views/fsm_frequency.xml | 126 +++++ .../views/fsm_frequency_set.xml | 73 +++ .../views/fsm_recurring.xml | 108 ++++ .../views/fsm_recurring_template.xml | 70 +++ 27 files changed, 1676 insertions(+) create mode 100644 fieldservice_recurring/README.rst create mode 100644 fieldservice_recurring/__init__.py create mode 100644 fieldservice_recurring/__manifest__.py create mode 100644 fieldservice_recurring/data/ir_sequence.xml create mode 100644 fieldservice_recurring/data/recurring_cron.xml create mode 100644 fieldservice_recurring/models/__init__.py create mode 100644 fieldservice_recurring/models/fsm_frequency.py create mode 100644 fieldservice_recurring/models/fsm_frequency_set.py create mode 100644 fieldservice_recurring/models/fsm_order.py create mode 100644 fieldservice_recurring/models/fsm_recurring.py create mode 100644 fieldservice_recurring/models/fsm_recurring_template.py create mode 100644 fieldservice_recurring/readme/CONFIGURE.rst create mode 100644 fieldservice_recurring/readme/CONTRIBUTORS.rst create mode 100644 fieldservice_recurring/readme/CREDITS.rst create mode 100644 fieldservice_recurring/readme/DESCRIPTION.rst create mode 100644 fieldservice_recurring/readme/INSTALL.rst create mode 100644 fieldservice_recurring/readme/ROADMAP.rst create mode 100644 fieldservice_recurring/readme/USAGE.rst create mode 100644 fieldservice_recurring/security/ir.model.access.csv create mode 100644 fieldservice_recurring/security/recurring_security.xml create mode 100644 fieldservice_recurring/security/res_groups.xml create mode 100644 fieldservice_recurring/static/description/icon.png create mode 100644 fieldservice_recurring/static/description/index.html create mode 100644 fieldservice_recurring/views/fsm_frequency.xml create mode 100644 fieldservice_recurring/views/fsm_frequency_set.xml create mode 100644 fieldservice_recurring/views/fsm_recurring.xml create mode 100644 fieldservice_recurring/views/fsm_recurring_template.xml diff --git a/fieldservice_recurring/README.rst b/fieldservice_recurring/README.rst new file mode 100644 index 0000000000..81454406c9 --- /dev/null +++ b/fieldservice_recurring/README.rst @@ -0,0 +1,156 @@ +=================================== +Field Service Recurring Work Orders +=================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Ffield--service-lightgray.png?logo=github + :target: https://github.com/OCA/field-service/tree/11.0/fieldservice_recurring + :alt: OCA/field-service +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/field-service-11-0/field-service-11-0-fieldservice_recurring + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/264/11.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows you to manage your recurring field service work orders + +Recurring settings are configured via the FSM Frequency model. Multiple +FSM Frequency can be combined on a FSM Frequency Rule Set which enables +highly configurable recurring rules calculated using the dateutil rrule +python library. + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +To install Field Service and have the mapping features, you need to install GeoEngine. + +Please refer to the installation instructions available at: +https://github.com/OCA/geospatial/tree/11.0/base_geoengine + +Configuration +============= + +To configure this module, you need to: + +* Setup your Frequencies to establish recurring rules + +1. In fieldservice app go to Menu > Configuration > Orders > Frequencies +2. Create a Frequency +3. Setup your Frequency by giving it a descriptive name, set your interval + and the interval type. Use the additional settings to build a recurring rule + based on python's dateutil rrule parameters. + + +* Setup your Frequency Rule Sets used to calculate recurring order dates + +1. In fieldservice app go to Menu > Configuration > Orders > Frequency Rule Set +2. Create a Frequency Rule Set +3. Setup your Frequency Rule by first giving it a descriptive name. Complete + the form by entering the number of days ahead this rule will schedule work. +4. Finally, choose which Frequencies this rule will use to compute the dates + used for scheduling. + + +* Setup your recurring order templates to define standard recurring orders + +1. In fieldservice app go to Menu > Configuration > Orders > Recurring Templates +2. Name the template and set fields to define which order template is repeated + and what Frequency Rule Set will be used + +Usage +===== + +To use this module, you need to: + +* In fieldservice app go to Menu > Operations > Recurring Orders +* Create a new Recurring Order model +* Select a Recurring Template and modify as needed. +* Set other fields for fsm location, etc +* Confirm the recurrence to create first order +* Future orders will be created via cron task + +Known issues / Roadmap +====================== + +The roadmap of the Field Service application is documented on +`Github `_. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Brian McMaster +* Open Source Integrators + +Contributors +~~~~~~~~~~~~ + +* Brian McMaster + +Other credits +~~~~~~~~~~~~~ + +The development of this module has been financially supported by: + +* Open Source Integrators + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-wolfhall| image:: https://github.com/wolfhall.png?size=40px + :target: https://github.com/wolfhall + :alt: wolfhall +.. |maintainer-max3903| image:: https://github.com/max3903.png?size=40px + :target: https://github.com/max3903 + :alt: max3903 +.. |maintainer-brian10048| image:: https://github.com/brian10048.png?size=40px + :target: https://github.com/brian10048 + :alt: brian10048 + +Current `maintainers `__: + +|maintainer-wolfhall| |maintainer-max3903| |maintainer-brian10048| + +This module is part of the `OCA/field-service `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fieldservice_recurring/__init__.py b/fieldservice_recurring/__init__.py new file mode 100644 index 0000000000..b9b7889936 --- /dev/null +++ b/fieldservice_recurring/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2019 - TODAY, Brian McMaster, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/fieldservice_recurring/__manifest__.py b/fieldservice_recurring/__manifest__.py new file mode 100644 index 0000000000..08be7203ac --- /dev/null +++ b/fieldservice_recurring/__manifest__.py @@ -0,0 +1,35 @@ +# Copyright (C) 2019 - TODAY, Brian McMaster, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Field Service Recurring Work Orders', + 'summary': 'Manage recurring field service works', + 'version': '11.0.0.1.1', + 'category': 'Field Service', + 'author': + 'Brian McMaster, ' + 'Open Source Integrators, ' + 'Odoo Community Association (OCA)', + 'website': 'https://github.com/OCA/field-service', + 'depends': [ + 'fieldservice', + ], + 'data': [ + 'data/ir_sequence.xml', + 'security/res_groups.xml', + 'security/ir.model.access.csv', + 'security/recurring_security.xml', + 'views/fsm_frequency.xml', + 'views/fsm_frequency_set.xml', + 'views/fsm_recurring_template.xml', + 'views/fsm_recurring.xml', + 'data/recurring_cron.xml', + ], + 'license': 'AGPL-3', + 'development_status': 'Beta', + 'maintainers': [ + 'wolfhall', + 'max3903', + 'brian10048', + ], +} diff --git a/fieldservice_recurring/data/ir_sequence.xml b/fieldservice_recurring/data/ir_sequence.xml new file mode 100644 index 0000000000..d259927119 --- /dev/null +++ b/fieldservice_recurring/data/ir_sequence.xml @@ -0,0 +1,13 @@ + + + + + + FSM Recurring Order + fsm.recurring + RFO + 3 + + + + diff --git a/fieldservice_recurring/data/recurring_cron.xml b/fieldservice_recurring/data/recurring_cron.xml new file mode 100644 index 0000000000..733f706f13 --- /dev/null +++ b/fieldservice_recurring/data/recurring_cron.xml @@ -0,0 +1,15 @@ + + + + + Field Service: generate recurring orders + + code + model._cron_scheduled_task() + 1 + days + -1 + + + + diff --git a/fieldservice_recurring/models/__init__.py b/fieldservice_recurring/models/__init__.py new file mode 100644 index 0000000000..9d2cd7b25b --- /dev/null +++ b/fieldservice_recurring/models/__init__.py @@ -0,0 +1,10 @@ +# Copyright (C) 2019 - TODAY, Brian McMaster +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import ( + fsm_order, + fsm_frequency_set, + fsm_frequency, + fsm_recurring_template, + fsm_recurring, +) diff --git a/fieldservice_recurring/models/fsm_frequency.py b/fieldservice_recurring/models/fsm_frequency.py new file mode 100644 index 0000000000..0acb76d788 --- /dev/null +++ b/fieldservice_recurring/models/fsm_frequency.py @@ -0,0 +1,148 @@ +# Copyright (C) 2019 - TODAY, Brian McMaster, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU +from dateutil.rrule import YEARLY, MONTHLY, WEEKLY, DAILY +from dateutil.rrule import rrule + +from odoo import fields, models, api, _ +from odoo.exceptions import UserError + + +WEEKDAYS = { + 'mo': MO, + 'tu': TU, + 'we': WE, + 'th': TH, + 'fr': FR, + 'sa': SA, + 'su': SU +} + +FREQUENCIES = { + 'yearly': YEARLY, + 'monthly': MONTHLY, + 'weekly': WEEKLY, + 'daily': DAILY, +} + + +class FSMFrequency(models.Model): + _name = 'fsm.frequency' + _description = 'Frequency Rule for Field Service Orders' + _inherit = ['mail.thread'] + + name = fields.Char('Name', required=True) + active = fields.Boolean(default=True) + interval = fields.Integer( + string='Repeat Every', help="The number of intervals between events", + default=1, required=True, track_visibility='onchange') + interval_type = fields.Selection( + FREQUENCIES, string='Interval Type', + required=True, track_visibility='onchange') + is_exclusive = fields.Boolean( + string='Exclusive Rule?', default=False, + help="""Checking this box will make this an exclusive rule. Exclusive + rules prevent the configured days from being a schedule option""") + + use_bymonthday = fields.Boolean( + string='Use Day of Month', + help="""When selected you will be able to specify which calendar day + of the month the event occurs on""") + month_day = fields.Integer( + string='Day of Month', track_visibility='onchange') + + use_byweekday = fields.Boolean( + string='Use Days of Week', + help="""When selected you will be able to choose which days of the + week the scheduler will include (or exclude if Exclusive rule)""") + mo = fields.Boolean('Monday', default=False) + tu = fields.Boolean('Tuesday', default=False) + we = fields.Boolean('Wednesday', default=False) + th = fields.Boolean('Thursday', default=False) + fr = fields.Boolean('Friday', default=False) + sa = fields.Boolean('Saturday', default=False) + su = fields.Boolean('Sunday', default=False) + + use_bymonth = fields.Boolean(string='Use Months') + jan = fields.Boolean('January', default=False) + feb = fields.Boolean('February', default=False) + mar = fields.Boolean('March', default=False) + apr = fields.Boolean('April', default=False) + may = fields.Boolean('May', default=False) + jun = fields.Boolean('June', default=False) + jul = fields.Boolean('July', default=False) + aug = fields.Boolean('August', default=False) + sep = fields.Boolean('September', default=False) + oct = fields.Boolean('October', default=False) + nov = fields.Boolean('November', default=False) + dec = fields.Boolean('December', default=False) + + use_setpos = fields.Boolean(string='Use Position') + set_pos = fields.Integer( + string="By Position", + help="""Specify an occurrence number, positive or negative, + corresponding to the nth occurrence of the rule inside + the frequency period. For example, -1 if combined with a + 'Monthly' frequency, and a weekday of (MO, TU, WE, TH, FR), + will result in the last work day of every month.""") + + @api.constrains('set_pos') + def _check_set_pos(self): + if self.use_setpos: + if not (-366 < self.set_pos < 366): + raise UserError(_("Position must be between -366 and 366")) + + @api.constrains('month_day') + def _check_month_day(self): + if self.use_bymonthday: + if not (1 <= self.month_day <= 31): + raise UserError(_("'Day of Month must be between 1 and 31")) + + def _get_rrule(self, dtstart=None, until=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(), + ) + + def _byweekday(self): + """ + Checks day of week booleans and builds the value for rrule parameter + @returns: {list} byweekday: list of WEEKDAY values used for rrule + """ + self.ensure_one() + if not self.use_byweekday: + return None + weekdays = ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su'] + byweekday = [WEEKDAYS[field] for field in weekdays if self[field]] + return byweekday + + def _bymonth(self): + """ + Checks month booleans and builds the value for rrule parameter + @returns: {list} bymonth: list of integers used for rrule + """ + self.ensure_one() + if not self.use_bymonth: + return None + months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', + 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] + bymonth = [months.index(field) + 1 for field in months if self[field]] + return bymonth + + def _bymonthday(self): + self.ensure_one() + if not self.use_bymonthday: + return None + return self.month_day + + def _bysetpos(self): + self.ensure_one() + if not self.use_setpos or self.set_pos == 0: + return None + return self.set_pos diff --git a/fieldservice_recurring/models/fsm_frequency_set.py b/fieldservice_recurring/models/fsm_frequency_set.py new file mode 100644 index 0000000000..f45cdd7038 --- /dev/null +++ b/fieldservice_recurring/models/fsm_frequency_set.py @@ -0,0 +1,40 @@ +# Copyright (C) 2019 - TODAY, Brian McMaster, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from dateutil.rrule import rruleset + +from odoo import fields, models + + +class FSMFrequencySet(models.Model): + _name = 'fsm.frequency.set' + _description = 'Frequency Rule Set for Field Service Orders' + _inherit = ['mail.thread'] + + name = fields.Char('Name', required=True) + active = fields.Boolean(default=True) + fsm_frequency_ids = fields.Many2many( + 'fsm.frequency', track_visibility='onchange', + string='Frequency Rules') + schedule_days = fields.Integer( + string='Days Ahead to Schedule', default='30', + help="""The number of days from today that the scheduler will generate + orders for this rule""", track_visibility='onchange') + buffer_early = fields.Integer( + string='Early Buffer', track_visibility='onchange', + help="""The allowed number of days before the computed schedule date + that an event can be done""") + buffer_late = fields.Integer( + string='Late Buffer', track_visibility='onchange', + help="""The allowed number of days after the computed schedule date + that an event can be done""") + + def _get_rruleset(self, dtstart=None, until=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)) + else: + rset.exrule(rule._get_rrule(dtstart)) + return rset diff --git a/fieldservice_recurring/models/fsm_order.py b/fieldservice_recurring/models/fsm_order.py new file mode 100644 index 0000000000..0b24a51600 --- /dev/null +++ b/fieldservice_recurring/models/fsm_order.py @@ -0,0 +1,23 @@ +# Copyright (C) 2019 - TODAY, Brian McMaster, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import timedelta + +from odoo import fields + +from odoo.addons.base_geoengine import geo_model + + +class FSMOrder(geo_model.GeoModel): + _inherit = 'fsm.order' + + fsm_recurring_id = fields.Many2one( + 'fsm.recurring', 'Recurring Order', readonly=True) + + def _compute_request_late(self): + if not self.fsm_recurring_id: + return super(FSMOrder, self)._compute_request_late() + else: + days_late = self.fsm_recurring_id.fsm_frequency_set_id.buffer_late + self.request_late = fields.Datetime.from_string( + self.scheduled_date_start) + timedelta(days=days_late) diff --git a/fieldservice_recurring/models/fsm_recurring.py b/fieldservice_recurring/models/fsm_recurring.py new file mode 100644 index 0000000000..dfff401208 --- /dev/null +++ b/fieldservice_recurring/models/fsm_recurring.py @@ -0,0 +1,239 @@ +# Copyright (C) 2019 - TODAY, Brian McMaster, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime +from dateutil.rrule import rruleset +from dateutil.relativedelta import relativedelta + +from odoo import fields, models, api, _ +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + + +class FSMRecurringOrder(models.Model): + _name = 'fsm.recurring' + _description = 'Recurring Field Service Order' + _inherit = ['mail.thread', 'mail.activity.mixin'] + + name = fields.Char(string='Name', required=True, index=True, copy=False, + default=lambda self: _('New')) + state = fields.Selection([ + ('draft', 'Draft'), + ('progress', 'In Progress'), + ('pending', 'To Renew'), + ('close', 'Closed'), + ('cancel', 'Cancelled')], readonly=True, + default='draft', track_visibility='onchange') + fsm_recurring_template_id = fields.Many2one( + 'fsm.recurring.template', 'Recurring Template', + readonly=True, states={'draft': [('readonly', False)]}) + customer_id = fields.Many2one( + 'res.partner', string='Contact', + domain=[('customer', '=', True)], + change_default=True, index=True, + track_visibility='always',) + location_id = fields.Many2one( + 'fsm.location', string='Location', index=True, required=True) + description = fields.Text(string='Description') + fsm_frequency_set_id = fields.Many2one( + 'fsm.frequency.set', 'Frequency Set', required=True) + start_date = fields.Datetime(String='Start Date') + end_date = fields.Datetime( + string='End Date', + help="Recurring orders will not be made after this date") + max_orders = fields.Integer( + string='Maximum Orders', + help="Maximium number of orders that will be created") + fsm_order_template_id = fields.Many2one( + 'fsm.template', string='Order Template', + help="This is the order template that will be recurring") + company_id = fields.Many2one( + 'res.company', 'Company', + default=lambda self: self.env.user.company_id) + fsm_order_ids = fields.One2many( + 'fsm.order', 'fsm_recurring_id', string='Orders', + copy=False, readonly=True) + fsm_order_count = fields.Integer( + 'Orders Count', compute='_compute_order_count') + + @api.multi + @api.depends('fsm_order_ids') + def _compute_order_count(self): + data = self.env['fsm.order'].read_group( + [('fsm_recurring_id', 'in', self.ids), + ('stage_id', '!=', self.env.ref( + 'fieldservice.fsm_stage_cancelled').id)], + ['fsm_recurring_id'], ['fsm_recurring_id']) + count_data = dict((item['fsm_recurring_id'][0], + item['fsm_recurring_id_count']) for item in data) + for recurring in self: + recurring.fsm_order_count = count_data.get(recurring.id, 0) + + @api.onchange('fsm_recurring_template_id') + def onchange_recurring_template_id(self): + if not self.fsm_recurring_template_id: + return + values = self.populate_from_template() + self.update(values) + + def populate_from_template(self, template=False): + if not template: + template = self.fsm_recurring_template_id + vals = { + 'fsm_frequency_set_id': template.fsm_frequency_set_id, + 'max_orders': template.max_orders, + 'description': template.description, + 'fsm_order_template_id': template.fsm_order_template_id, + 'company_id': template.company_id, + } + return vals + + @api.model + def create(self, vals): + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code( + 'fsm.recurring') or _('New') + return super(FSMRecurringOrder, self).create(vals) + + @api.multi + def action_start(self): + for rec in self: + if not rec.start_date: + rec.start_date = datetime.now() + start_date = fields.Datetime.from_string(rec.start_date) + rec._create_order(date=start_date) + rec.write({'state': 'progress'}) + + @api.multi + def action_renew(self): + return self.action_start() + + @api.multi + def action_cancel(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'}) + + def _get_rruleset(self): + self.ensure_one() + ruleset = rruleset() + if self.state != 'progress': + return ruleset + # set next_date which is used as the rrule 'dtstart' parameter + next_date = datetime.now() + last_order = self.env['fsm.order'].search([ + ('fsm_recurring_id', '=', self.id), + ('stage_id', '!=', self.env.ref( + 'fieldservice.fsm_stage_cancelled').id) + ], offset=0, limit=1, order='scheduled_date_start desc') + if last_order: + next_date = fields.Datetime.from_string( + last_order.scheduled_date_start) + # set thru_date to use as rrule 'until' parameter + days_ahead = self.fsm_frequency_set_id.schedule_days + request_thru_date = datetime.now() + relativedelta(days=+days_ahead) + request_thru_str = request_thru_date.strftime( + DEFAULT_SERVER_DATETIME_FORMAT) + if self.end_date and (self.end_date < request_thru_str): + thru_date = fields.Datetime.from_string(self.end_date) + else: + thru_date = request_thru_date + # use variables to calulate and return the rruleset object + ruleset = self.fsm_frequency_set_id._get_rruleset(dtstart=next_date, + until=thru_date) + return ruleset + + def _prepare_order_values(self, date=None): + self.ensure_one() + schedule_date = date if date else datetime.now() + days_early = self.fsm_frequency_set_id.buffer_early + earliest_date = schedule_date + relativedelta(days=-days_early) + return { + 'fsm_recurring_id': self.id, + 'customer_id': self.customer_id.id, + 'location_id': self.location_id.id, + 'scheduled_date_start': schedule_date, + 'request_early': str(earliest_date), + 'description': self.description, + 'template_id': self.fsm_order_template_id.id, + 'company_id': self.company_id.id, + } + + def _create_order(self, date): + self.ensure_one() + vals = self._prepare_order_values(date) + return self.env['fsm.order'].create(vals) + + @api.model + def _cron_generate_orders(self): + """ + Executed by Cron task to create field service orders from any + recurring orders which are in progress, or to renew, and up to + the max orders allowed by the recurring order + @return {recordset} orders: all the order objects created + """ + orders = self.env['fsm.order'] + for rec in self.env['fsm.recurring'].search([ + ('state', 'in', ('progress', 'pending')) + ]): + schedule_dates = rec._get_rruleset() + order_dates = [] + for order in rec.fsm_order_ids: + if order.scheduled_date_start: + order_dates.append( + datetime.strptime(order.scheduled_date_start, + DEFAULT_SERVER_DATETIME_FORMAT + ).date()) + max_orders = rec.max_orders if rec.max_orders > 0 else False + order_count = rec.fsm_order_count + for date in schedule_dates: + if date.date() in order_dates: + continue + if max_orders >= order_count or not max_orders: + orders += rec._create_order(date=date) + order_count += 1 + return orders + + @api.model + def _cron_manage_expiration(self): + """ + Executed by Cron task to put all 'pending' 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 + """ + to_close = self.env['fsm.recurring'] + pending_rec = self.env['fsm.recurring'].search([ + ('state', '=', 'pending')]) + for rec in pending_rec: + if rec.end_date and \ + rec.end_date <= fields.Date.to_string(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 = fields.Date.to_string( + 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=fields.Date.from_string(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): + self._cron_generate_orders() + self._cron_manage_expiration() diff --git a/fieldservice_recurring/models/fsm_recurring_template.py b/fieldservice_recurring/models/fsm_recurring_template.py new file mode 100644 index 0000000000..a18ff94097 --- /dev/null +++ b/fieldservice_recurring/models/fsm_recurring_template.py @@ -0,0 +1,25 @@ +# Copyright (C) 2019 - TODAY, Brian McMaster, Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FSMRecurringTemplate(models.Model): + _name = 'fsm.recurring.template' + _description = 'Recurring Field Service Order Template' + _inherit = "mail.thread" + + name = fields.Char(string='Name', required=True) + active = fields.Boolean(default=True) + description = fields.Text(string='Description') + fsm_frequency_set_id = fields.Many2one( + 'fsm.frequency.set', 'Frequency Set', required=True) + max_orders = fields.Integer( + string='Maximum Orders', + help="Maximium number of orders that will be created") + fsm_order_template_id = fields.Many2one( + 'fsm.template', string='Order Template', + help="This is the order template that will be recurring") + company_id = fields.Many2one( + 'res.company', 'Company', + default=lambda self: self.env.user.company_id) diff --git a/fieldservice_recurring/readme/CONFIGURE.rst b/fieldservice_recurring/readme/CONFIGURE.rst new file mode 100644 index 0000000000..2737ecd261 --- /dev/null +++ b/fieldservice_recurring/readme/CONFIGURE.rst @@ -0,0 +1,26 @@ +To configure this module, you need to: + +* Setup your Frequencies to establish recurring rules + +1. In fieldservice app go to Menu > Configuration > Orders > Frequencies +2. Create a Frequency +3. Setup your Frequency by giving it a descriptive name, set your interval + and the interval type. Use the additional settings to build a recurring rule + based on python's dateutil rrule parameters. + + +* Setup your Frequency Rule Sets used to calculate recurring order dates + +1. In fieldservice app go to Menu > Configuration > Orders > Frequency Rule Set +2. Create a Frequency Rule Set +3. Setup your Frequency Rule by first giving it a descriptive name. Complete + the form by entering the number of days ahead this rule will schedule work. +4. Finally, choose which Frequencies this rule will use to compute the dates + used for scheduling. + + +* Setup your recurring order templates to define standard recurring orders + +1. In fieldservice app go to Menu > Configuration > Orders > Recurring Templates +2. Name the template and set fields to define which order template is repeated + and what Frequency Rule Set will be used diff --git a/fieldservice_recurring/readme/CONTRIBUTORS.rst b/fieldservice_recurring/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..39ef3fa3d7 --- /dev/null +++ b/fieldservice_recurring/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Brian McMaster diff --git a/fieldservice_recurring/readme/CREDITS.rst b/fieldservice_recurring/readme/CREDITS.rst new file mode 100644 index 0000000000..0eff0acf4e --- /dev/null +++ b/fieldservice_recurring/readme/CREDITS.rst @@ -0,0 +1,3 @@ +The development of this module has been financially supported by: + +* Open Source Integrators diff --git a/fieldservice_recurring/readme/DESCRIPTION.rst b/fieldservice_recurring/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..1187440ed3 --- /dev/null +++ b/fieldservice_recurring/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ +This module allows you to manage your recurring field service work orders + +Recurring settings are configured via the FSM Frequency model. Multiple +FSM Frequency can be combined on a FSM Frequency Rule Set which enables +highly configurable recurring rules calculated using the dateutil rrule +python library. diff --git a/fieldservice_recurring/readme/INSTALL.rst b/fieldservice_recurring/readme/INSTALL.rst new file mode 100644 index 0000000000..c010f8f6a2 --- /dev/null +++ b/fieldservice_recurring/readme/INSTALL.rst @@ -0,0 +1,4 @@ +To install Field Service and have the mapping features, you need to install GeoEngine. + +Please refer to the installation instructions available at: +https://github.com/OCA/geospatial/tree/11.0/base_geoengine diff --git a/fieldservice_recurring/readme/ROADMAP.rst b/fieldservice_recurring/readme/ROADMAP.rst new file mode 100644 index 0000000000..f607015959 --- /dev/null +++ b/fieldservice_recurring/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +The roadmap of the Field Service application is documented on +`Github `_. diff --git a/fieldservice_recurring/readme/USAGE.rst b/fieldservice_recurring/readme/USAGE.rst new file mode 100644 index 0000000000..cabac9823d --- /dev/null +++ b/fieldservice_recurring/readme/USAGE.rst @@ -0,0 +1,8 @@ +To use this module, you need to: + +* In fieldservice app go to Menu > Operations > Recurring Orders +* Create a new Recurring Order model +* Select a Recurring Template and modify as needed. +* Set other fields for fsm location, etc +* Confirm the recurrence to create first order +* Future orders will be created via cron task diff --git a/fieldservice_recurring/security/ir.model.access.csv b/fieldservice_recurring/security/ir.model.access.csv new file mode 100644 index 0000000000..b764ecf997 --- /dev/null +++ b/fieldservice_recurring/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_fsm_frequency_fsm_user,fsm.frequency.user,model_fsm_frequency,fieldservice.group_fsm_user,1,1,1,0 +access_fsm_frequency_fsm_manager,fsm.frequency.manager,model_fsm_frequency,fieldservice.group_fsm_manager,1,1,1,1 +access_fsm_recurring_template_fsm_user,fsm.recurring.template.user,model_fsm_recurring_template,fieldservice.group_fsm_user,1,0,0,0 +access_fsm_recurring_template_fsm_manager,fsm.recurring.template.manager,model_fsm_recurring_template,fieldservice.group_fsm_manager,1,1,1,1 +access_fsm_recurring_fsm_user,fsm.recurring.user,model_fsm_recurring,fieldservice.group_fsm_user,1,1,1,0 +access_fsm_recurring_fsm_manager,fsm.recurring.manager,model_fsm_recurring,fieldservice.group_fsm_manager,1,1,1,1 diff --git a/fieldservice_recurring/security/recurring_security.xml b/fieldservice_recurring/security/recurring_security.xml new file mode 100644 index 0000000000..bde032b1e4 --- /dev/null +++ b/fieldservice_recurring/security/recurring_security.xml @@ -0,0 +1,25 @@ + + + + + FSM Frequency multi-company + + + ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])] + + + + FSM Recurring Template multi-company + + + ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])] + + + + FSM Recurring Order multi-company + + + ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])] + + + diff --git a/fieldservice_recurring/security/res_groups.xml b/fieldservice_recurring/security/res_groups.xml new file mode 100644 index 0000000000..266ac9e671 --- /dev/null +++ b/fieldservice_recurring/security/res_groups.xml @@ -0,0 +1,15 @@ + + + + + + Manage Recurring Orders + + + + + + + + diff --git a/fieldservice_recurring/static/description/icon.png b/fieldservice_recurring/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..955674d8f0b8c47de3ffa9db25cb109fbe4a1091 GIT binary patch literal 17808 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 literal 0 HcmV?d00001 diff --git a/fieldservice_recurring/static/description/index.html b/fieldservice_recurring/static/description/index.html new file mode 100644 index 0000000000..873c16f9ca --- /dev/null +++ b/fieldservice_recurring/static/description/index.html @@ -0,0 +1,494 @@ + + + + + + +Field Service Recurring Work Orders + + + +
+

Field Service Recurring Work Orders

+ + +

Beta License: AGPL-3 OCA/field-service Translate me on Weblate Try me on Runbot

+

This module allows you to manage your recurring field service work orders

+

Recurring settings are configured via the FSM Frequency model. Multiple +FSM Frequency can be combined on a FSM Frequency Rule Set which enables +highly configurable recurring rules calculated using the dateutil rrule +python library.

+

Table of contents

+ +
+

Installation

+

To install Field Service and have the mapping features, you need to install GeoEngine.

+

Please refer to the installation instructions available at: +https://github.com/OCA/geospatial/tree/11.0/base_geoengine

+
+
+

Configuration

+

To configure this module, you need to:

+
    +
  • Setup your Frequencies to establish recurring rules
  • +
+
    +
  1. In fieldservice app go to Menu > Configuration > Orders > Frequencies
  2. +
  3. Create a Frequency
  4. +
  5. Setup your Frequency by giving it a descriptive name, set your interval +and the interval type. Use the additional settings to build a recurring rule +based on python???s dateutil rrule parameters.
  6. +
+
    +
  • Setup your Frequency Rule Sets used to calculate recurring order dates
  • +
+
    +
  1. In fieldservice app go to Menu > Configuration > Orders > Frequency Rule Set
  2. +
  3. Create a Frequency Rule Set
  4. +
  5. Setup your Frequency Rule by first giving it a descriptive name. Complete +the form by entering the number of days ahead this rule will schedule work.
  6. +
  7. Finally, choose which Frequencies this rule will use to compute the dates +used for scheduling.
  8. +
+
    +
  • Setup your recurring order templates to define standard recurring orders
  • +
+
    +
  1. In fieldservice app go to Menu > Configuration > Orders > Recurring Templates
  2. +
  3. Name the template and set fields to define which order template is repeated +and what Frequency Rule Set will be used
  4. +
+
+
+

Usage

+

To use this module, you need to:

+
    +
  • In fieldservice app go to Menu > Operations > Recurring Orders
  • +
  • Create a new Recurring Order model
  • +
  • Select a Recurring Template and modify as needed.
  • +
  • Set other fields for fsm location, etc
  • +
  • Confirm the recurrence to create first order
  • +
  • Future orders will be created via cron task
  • +
+
+
+

Known issues / Roadmap

+

The roadmap of the Field Service application is documented on +Github.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Brian McMaster
  • +
  • Open Source Integrators
  • +
+
+
+

Contributors

+ +
+
+

Other credits

+

The development of this module has been financially supported by:

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainers:

+

wolfhall max3903 brian10048

+

This module is part of the OCA/field-service project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/fieldservice_recurring/views/fsm_frequency.xml b/fieldservice_recurring/views/fsm_frequency.xml new file mode 100644 index 0000000000..02fe47735c --- /dev/null +++ b/fieldservice_recurring/views/fsm_frequency.xml @@ -0,0 +1,126 @@ + + + + + + fsm.frequency.tree + fsm.frequency + + + + + + + + + + + + fsm.frequency.form + fsm.frequency + +
+ +
+ +
+
+

+ +

+
+ + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ + + +
+
+
+ + + +
+ + + + + + + + + + + + + + +
+
+ +
+
+
+
+
+ + + Field Service Frequencies + fsm.frequency + form + tree,form + +

+ Define a Frequency for scheduling recurring orders here. +

+
+
+ + + +
diff --git a/fieldservice_recurring/views/fsm_frequency_set.xml b/fieldservice_recurring/views/fsm_frequency_set.xml new file mode 100644 index 0000000000..90a5138300 --- /dev/null +++ b/fieldservice_recurring/views/fsm_frequency_set.xml @@ -0,0 +1,73 @@ + + + + + + fsm.frequency.set.tree + fsm.frequency.set + + + + + + + + + fsm.frequency.set.form + fsm.frequency.set + +
+ +
+ +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + +
+
+
+
+ + + Field Service Frequency Rule Sets + fsm.frequency.set + form + tree,form + +

+ Define a Frequency Rule Set for scheduling recurring orders here. +

+
+
+ + + +
diff --git a/fieldservice_recurring/views/fsm_recurring.xml b/fieldservice_recurring/views/fsm_recurring.xml new file mode 100644 index 0000000000..ee5ea6906a --- /dev/null +++ b/fieldservice_recurring/views/fsm_recurring.xml @@ -0,0 +1,108 @@ + + + + + + fsm.recurring.tree + fsm.recurring + + + + + + + + + + + + + + fsm.recurring.form + fsm.recurring + +
+
+
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + +
+
+
+
+ + + Field Service Recurring Orders + fsm.recurring + form + tree,form + +

+ Add a Field Service Recurring Orders here. +

+
+
+ + +
diff --git a/fieldservice_recurring/views/fsm_recurring_template.xml b/fieldservice_recurring/views/fsm_recurring_template.xml new file mode 100644 index 0000000000..b1f7a2f1be --- /dev/null +++ b/fieldservice_recurring/views/fsm_recurring_template.xml @@ -0,0 +1,70 @@ + + + + + + fsm.recurring.template.tree + fsm.recurring.template + + + + + + + + + + fsm.recurring.template.form + fsm.recurring.template + +
+ +
+ +
+
+

+ +

+
+ + + + + + + + + + +
+
+
+
+
+
+ + + Field Service Recurring Templates + fsm.recurring.template + form + tree,form + +

+ Add a Field Service Recurring Template here. +

+
+
+ + + +
From 526fa03a50bb41c4c48f967514b0a64dfd8f8aca Mon Sep 17 00:00:00 2001 From: brian10048 Date: Tue, 14 May 2019 20:09:40 -0400 Subject: [PATCH 002/138] [FIX] fieldservice_recurring (#209) --- fieldservice_recurring/models/fsm_frequency.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fieldservice_recurring/models/fsm_frequency.py b/fieldservice_recurring/models/fsm_frequency.py index 0acb76d788..c28b58d588 100644 --- a/fieldservice_recurring/models/fsm_frequency.py +++ b/fieldservice_recurring/models/fsm_frequency.py @@ -26,6 +26,13 @@ 'daily': DAILY, } +FREQUENCY_SELECT = [ + ('yearly', 'Yearly'), + ('monthly', 'Monthly'), + ('weekly', 'Weekly'), + ('daily', 'Daily') +] + class FSMFrequency(models.Model): _name = 'fsm.frequency' @@ -38,7 +45,7 @@ class FSMFrequency(models.Model): string='Repeat Every', help="The number of intervals between events", default=1, required=True, track_visibility='onchange') interval_type = fields.Selection( - FREQUENCIES, string='Interval Type', + FREQUENCY_SELECT, string='Interval Type', required=True, track_visibility='onchange') is_exclusive = fields.Boolean( string='Exclusive Rule?', default=False, From 3d94ac781848ac881f81e6dd921f7c5747418fc9 Mon Sep 17 00:00:00 2001 From: Kitti U Date: Thu, 27 Jun 2019 09:29:46 +0700 Subject: [PATCH 003/138] [12.0][MIG] fieldservice_recurring --- fieldservice_recurring/README.rst | 13 +- fieldservice_recurring/__manifest__.py | 2 +- .../models/fsm_frequency.py | 4 +- fieldservice_recurring/models/fsm_order.py | 10 +- .../models/fsm_recurring.py | 27 ++-- .../readme/CONTRIBUTORS.rst | 1 + fieldservice_recurring/readme/INSTALL.rst | 2 +- .../security/ir.model.access.csv | 2 + .../static/description/index.html | 9 +- fieldservice_recurring/tests/__init__.py | 4 + .../tests/test_fsm_recurring.py | 150 ++++++++++++++++++ 11 files changed, 187 insertions(+), 37 deletions(-) create mode 100644 fieldservice_recurring/tests/__init__.py create mode 100644 fieldservice_recurring/tests/test_fsm_recurring.py diff --git a/fieldservice_recurring/README.rst b/fieldservice_recurring/README.rst index 81454406c9..610f8fd3a8 100644 --- a/fieldservice_recurring/README.rst +++ b/fieldservice_recurring/README.rst @@ -14,13 +14,13 @@ Field Service Recurring Work Orders :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Ffield--service-lightgray.png?logo=github - :target: https://github.com/OCA/field-service/tree/11.0/fieldservice_recurring + :target: https://github.com/OCA/field-service/tree/12.0/fieldservice_recurring :alt: OCA/field-service .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/field-service-11-0/field-service-11-0-fieldservice_recurring + :target: https://translation.odoo-community.org/projects/field-service-12-0/field-service-12-0-fieldservice_recurring :alt: Translate me on Weblate .. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/264/11.0 + :target: https://runbot.odoo-community.org/runbot/264/12.0 :alt: Try me on Runbot |badge1| |badge2| |badge3| |badge4| |badge5| @@ -43,7 +43,7 @@ Installation To install Field Service and have the mapping features, you need to install GeoEngine. Please refer to the installation instructions available at: -https://github.com/OCA/geospatial/tree/11.0/base_geoengine +https://github.com/OCA/geospatial/tree/12.0/base_geoengine Configuration ============= @@ -99,7 +99,7 @@ Bug Tracker Bugs are tracked on `GitHub Issues `_. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed -`feedback `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -116,6 +116,7 @@ Contributors ~~~~~~~~~~~~ * Brian McMaster +* Kitti Upariphutthiphone Other credits ~~~~~~~~~~~~~ @@ -151,6 +152,6 @@ Current `maintainers `__: |maintainer-wolfhall| |maintainer-max3903| |maintainer-brian10048| -This module is part of the `OCA/field-service `_ project on GitHub. +This module is part of the `OCA/field-service `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/fieldservice_recurring/__manifest__.py b/fieldservice_recurring/__manifest__.py index 08be7203ac..d58151ee21 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 works', - 'version': '11.0.0.1.1', + 'version': '12.0.1.0.0', 'category': 'Field Service', 'author': 'Brian McMaster, ' diff --git a/fieldservice_recurring/models/fsm_frequency.py b/fieldservice_recurring/models/fsm_frequency.py index c28b58d588..8f0d361ca0 100644 --- a/fieldservice_recurring/models/fsm_frequency.py +++ b/fieldservice_recurring/models/fsm_frequency.py @@ -51,7 +51,9 @@ class FSMFrequency(models.Model): string='Exclusive Rule?', default=False, help="""Checking this box will make this an exclusive rule. Exclusive rules prevent the configured days from being a schedule option""") - + company_id = fields.Many2one( + 'res.company', 'Company', + default=lambda self: self.env.user.company_id) use_bymonthday = fields.Boolean( string='Use Day of Month', help="""When selected you will be able to specify which calendar day diff --git a/fieldservice_recurring/models/fsm_order.py b/fieldservice_recurring/models/fsm_order.py index 0b24a51600..8618452426 100644 --- a/fieldservice_recurring/models/fsm_order.py +++ b/fieldservice_recurring/models/fsm_order.py @@ -3,12 +3,10 @@ from datetime import timedelta -from odoo import fields +from odoo import models, fields -from odoo.addons.base_geoengine import geo_model - -class FSMOrder(geo_model.GeoModel): +class FSMOrder(models.Model): _inherit = 'fsm.order' fsm_recurring_id = fields.Many2one( @@ -19,5 +17,5 @@ def _compute_request_late(self): return super(FSMOrder, self)._compute_request_late() else: days_late = self.fsm_recurring_id.fsm_frequency_set_id.buffer_late - self.request_late = fields.Datetime.from_string( - self.scheduled_date_start) + timedelta(days=days_late) + self.request_late = \ + self.scheduled_date_start + timedelta(days=days_late) diff --git a/fieldservice_recurring/models/fsm_recurring.py b/fieldservice_recurring/models/fsm_recurring.py index dfff401208..35e8cfd859 100644 --- a/fieldservice_recurring/models/fsm_recurring.py +++ b/fieldservice_recurring/models/fsm_recurring.py @@ -6,7 +6,6 @@ from dateutil.relativedelta import relativedelta from odoo import fields, models, api, _ -from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT class FSMRecurringOrder(models.Model): @@ -99,8 +98,7 @@ def action_start(self): for rec in self: if not rec.start_date: rec.start_date = datetime.now() - start_date = fields.Datetime.from_string(rec.start_date) - rec._create_order(date=start_date) + rec._create_order(date=rec.start_date) rec.write({'state': 'progress'}) @api.multi @@ -128,15 +126,12 @@ def _get_rruleset(self): 'fieldservice.fsm_stage_cancelled').id) ], offset=0, limit=1, order='scheduled_date_start desc') if last_order: - next_date = fields.Datetime.from_string( - last_order.scheduled_date_start) + next_date = last_order.scheduled_date_start # set thru_date to use as rrule 'until' parameter days_ahead = self.fsm_frequency_set_id.schedule_days request_thru_date = datetime.now() + relativedelta(days=+days_ahead) - request_thru_str = request_thru_date.strftime( - DEFAULT_SERVER_DATETIME_FORMAT) - if self.end_date and (self.end_date < request_thru_str): - thru_date = fields.Datetime.from_string(self.end_date) + if self.end_date and (self.end_date < request_thru_date): + thru_date = self.end_date else: thru_date = request_thru_date # use variables to calulate and return the rruleset object @@ -157,7 +152,7 @@ def _prepare_order_values(self, date=None): 'request_early': str(earliest_date), 'description': self.description, 'template_id': self.fsm_order_template_id.id, - 'company_id': self.company_id.id, + # 'company_id': self.company_id.id, } def _create_order(self, date): @@ -181,10 +176,7 @@ def _cron_generate_orders(self): order_dates = [] for order in rec.fsm_order_ids: if order.scheduled_date_start: - order_dates.append( - datetime.strptime(order.scheduled_date_start, - DEFAULT_SERVER_DATETIME_FORMAT - ).date()) + order_dates.append(order.scheduled_date_start.date()) max_orders = rec.max_orders if rec.max_orders > 0 else False order_count = rec.fsm_order_count for date in schedule_dates: @@ -210,15 +202,14 @@ def _cron_manage_expiration(self): ('state', '=', 'pending')]) for rec in pending_rec: if rec.end_date and \ - rec.end_date <= fields.Date.to_string(datetime.today()): + 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 = fields.Date.to_string( - datetime.today() + relativedelta(days=+30)) + expire_date = datetime.today() + relativedelta(days=+30) open_rec = self.env['fsm.recurring'].search([ ('state', '=', 'progress')]) for rec in open_rec: @@ -228,7 +219,7 @@ def _cron_manage_expiration(self): if rec.max_orders > 0: orders_in_30 = rec.fsm_order_count orders_in_30 += rec.fsm_frequency_set_id._get_rruleset( - until=fields.Date.from_string(expire_date)).count() + until=expire_date).count() if orders_in_30 >= rec.max_orders: to_renew += rec to_renew.write({'state': 'pending'}) diff --git a/fieldservice_recurring/readme/CONTRIBUTORS.rst b/fieldservice_recurring/readme/CONTRIBUTORS.rst index 39ef3fa3d7..c144064af9 100644 --- a/fieldservice_recurring/readme/CONTRIBUTORS.rst +++ b/fieldservice_recurring/readme/CONTRIBUTORS.rst @@ -1 +1,2 @@ * Brian McMaster +* Kitti Upariphutthiphone diff --git a/fieldservice_recurring/readme/INSTALL.rst b/fieldservice_recurring/readme/INSTALL.rst index c010f8f6a2..8571f65376 100644 --- a/fieldservice_recurring/readme/INSTALL.rst +++ b/fieldservice_recurring/readme/INSTALL.rst @@ -1,4 +1,4 @@ To install Field Service and have the mapping features, you need to install GeoEngine. Please refer to the installation instructions available at: -https://github.com/OCA/geospatial/tree/11.0/base_geoengine +https://github.com/OCA/geospatial/tree/12.0/base_geoengine diff --git a/fieldservice_recurring/security/ir.model.access.csv b/fieldservice_recurring/security/ir.model.access.csv index b764ecf997..999522c3ac 100644 --- a/fieldservice_recurring/security/ir.model.access.csv +++ b/fieldservice_recurring/security/ir.model.access.csv @@ -1,6 +1,8 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_fsm_frequency_fsm_user,fsm.frequency.user,model_fsm_frequency,fieldservice.group_fsm_user,1,1,1,0 access_fsm_frequency_fsm_manager,fsm.frequency.manager,model_fsm_frequency,fieldservice.group_fsm_manager,1,1,1,1 +access_fsm_frequency_fsm_set_user,fsm.frequency.set.user,model_fsm_frequency_set,fieldservice.group_fsm_user,1,1,1,0 +access_fsm_frequency_fsm_set_manager,fsm.frequency.set.manager,model_fsm_frequency_set,fieldservice.group_fsm_manager,1,1,1,1 access_fsm_recurring_template_fsm_user,fsm.recurring.template.user,model_fsm_recurring_template,fieldservice.group_fsm_user,1,0,0,0 access_fsm_recurring_template_fsm_manager,fsm.recurring.template.manager,model_fsm_recurring_template,fieldservice.group_fsm_manager,1,1,1,1 access_fsm_recurring_fsm_user,fsm.recurring.user,model_fsm_recurring,fieldservice.group_fsm_user,1,1,1,0 diff --git a/fieldservice_recurring/static/description/index.html b/fieldservice_recurring/static/description/index.html index 873c16f9ca..7e768d7ecf 100644 --- a/fieldservice_recurring/static/description/index.html +++ b/fieldservice_recurring/static/description/index.html @@ -367,7 +367,7 @@

Field Service Recurring Work Orders

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

Beta License: AGPL-3 OCA/field-service Translate me on Weblate Try me on Runbot

+

Beta License: AGPL-3 OCA/field-service Translate me on Weblate Try me on Runbot

This module allows you to manage your recurring field service work orders

Recurring settings are configured via the FSM Frequency model. Multiple FSM Frequency can be combined on a FSM Frequency Rule Set which enables @@ -394,7 +394,7 @@

Field Service Recurring Work Orders

Installation

To install Field Service and have the mapping features, you need to install GeoEngine.

Please refer to the installation instructions available at: -https://github.com/OCA/geospatial/tree/11.0/base_geoengine

+https://github.com/OCA/geospatial/tree/12.0/base_geoengine

Configuration

@@ -451,7 +451,7 @@

Bug Tracker

Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed -feedback.

+feedback.

Do not contact contributors directly about support or help with technical issues.

@@ -467,6 +467,7 @@

Authors

Contributors

@@ -485,7 +486,7 @@

Maintainers

promote its widespread use.

Current maintainers:

wolfhall max3903 brian10048

-

This module is part of the OCA/field-service project on GitHub.

+

This module is part of the OCA/field-service project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

diff --git a/fieldservice_recurring/tests/__init__.py b/fieldservice_recurring/tests/__init__.py new file mode 100644 index 0000000000..cca202bfb8 --- /dev/null +++ b/fieldservice_recurring/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from . import test_fsm_recurring diff --git a/fieldservice_recurring/tests/test_fsm_recurring.py b/fieldservice_recurring/tests/test_fsm_recurring.py new file mode 100644 index 0000000000..0cd71d70ca --- /dev/null +++ b/fieldservice_recurring/tests/test_fsm_recurring.py @@ -0,0 +1,150 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from math import ceil +from odoo import fields +from odoo.tests.common import TransactionCase + + +class FSMRecurringCase(TransactionCase): + + def setUp(self): + super(FSMRecurringCase, self).setUp() + self.Recurring = self.env['fsm.recurring'] + self.Frequency = self.env['fsm.frequency'] + self.FrequencySet = self.env['fsm.frequency.set'] + # create a Partner to be converted to FSM Location/Person + self.test_loc_partner = self.env['res.partner'].\ + create({ + 'name': 'Test Loc Partner', + 'phone': 'ABC', + 'email': 'tlp@email.com', + }) + # create expected FSM Location to compare to converted FSM Location + self.test_location = self.env['fsm.location'].\ + create({ + 'name': 'Test Location', + 'phone': '123', + 'email': 'tp@email.com', + 'partner_id': self.test_loc_partner.id, + 'owner_id': self.test_loc_partner.id, + 'customer_id': self.test_loc_partner.id, + }) + + def test_cron_generate_orders_rule1(self): + """Test recurring order with following rule, + - Work order Monday to Friday, exclude odd week Wednesday + """ + rules = self.Frequency + # Frequency Rule for + rules += self.Frequency.create({ + 'name': 'All weekdays', + 'interval_type': 'monthly', 'use_byweekday': True, + 'mo': True, 'tu': True, 'we': True, 'th': True, 'fr': True, + }) + # Exclusive Rule for odd Wednesday + for ex in [1, 3, 5]: + rules += self.Frequency.create({ + 'name': 'Exclude Wed-%s' % ex, 'is_exclusive': True, + 'interval_type': 'monthly', 'use_byweekday': True, + 'we': True, 'use_setpos': True, 'set_pos': ex, + }) + # Frequency Rule Set + fr_set = self.FrequencySet.create({ + 'name': 'Monday to Friday, exclude odd Wednesday', + 'schedule_days': 30, + 'fsm_frequency_ids': [(6, 0, rules.ids)] + }) + # Create Recurring Orddr link to this rule set + recurring = self.Recurring.create({ + 'fsm_frequency_set_id': fr_set.id, + 'location_id': self.test_location.id, + 'start_date': fields.Datetime.today(), + }) + recurring.action_start() + # Run schedule job now, to compute the future work orders + recurring._cron_scheduled_task() + # Test none of the scheduled date (except on recurring start date), + # are on weekend or odd wednesday + all_dates = recurring.fsm_order_ids.filtered( + lambda l: l.scheduled_date_start != recurring.start_date).\ + mapped('scheduled_date_start') + days = set([x.weekday() for x in all_dates]) + mon_to_fri = set([0, 1, 2, 3, 4]) + self.assertTrue(days <= mon_to_fri) + wednesdays = [x for x in all_dates if x.weekday() == 2] + + def _week_of_month(dt): + first_day = dt.replace(day=1) + adjusted_dom = dt.day + first_day.weekday() + return int(ceil(adjusted_dom/7.0)) + + wednesday_weeks = set([_week_of_month(x) for x in wednesdays]) + even_weeks = set([2, 4, 6]) + self.assertTrue(wednesday_weeks < even_weeks) + + def test_cron_generate_orders_rule2(self): + """Test recurring order with following rule, + - Work Order every 3 weeks, for + """ + rules = self.Frequency + # Frequency Rule + rules += self.Frequency.create({ + 'name': 'Every 3 weeks', + 'interval': 3, 'interval_type': 'weekly', + }) + # Frequency Rule Set + fr_set = self.FrequencySet.create({ + 'name': 'Every 3 weeks', + 'schedule_days': 100, + 'fsm_frequency_ids': [(6, 0, rules.ids)] + }) + # Create Recurring Orddr link to this rule set + recurring = self.Recurring.create({ + 'fsm_frequency_set_id': fr_set.id, + 'location_id': self.test_location.id, + 'start_date': fields.Datetime.today(), + }) + recurring.action_start() + # Run schedule job now, to compute the future work orders + recurring._cron_scheduled_task() + # Test date are 3 weeks apart (21 days) + all_dates = recurring.fsm_order_ids.mapped('scheduled_date_start') + x = False + for d in all_dates: + if x: + diff_days = (d-x).days + self.assertEqual(diff_days, 21) + x = d + + def test_cron_generate_orders_rule3(self): + """Test recurring order with following rule, + - Work Order every last day of the month only ending by 31th + """ + rules = self.Frequency + # Frequency Rule + rules += self.Frequency.create({ + 'name': '31th only', + 'interval': 1, 'interval_type': 'monthly', + 'use_bymonthday': True, 'month_day': 31, + }) + # Frequency Rule Set + fr_set = self.FrequencySet.create({ + 'name': '31th only', + 'schedule_days': 365, + 'fsm_frequency_ids': [(6, 0, rules.ids)] + }) + # Create Recurring Orddr link to this rule set + recurring = self.Recurring.create({ + 'fsm_frequency_set_id': fr_set.id, + 'location_id': self.test_location.id, + 'start_date': fields.Datetime.today(), + }) + recurring.action_start() + # Run schedule job now, to compute the future work orders + recurring._cron_scheduled_task() + # Test date are 31st + all_dates = recurring.fsm_order_ids.filtered( + lambda l: l.scheduled_date_start != recurring.start_date).\ + mapped('scheduled_date_start') + for d in all_dates: + self.assertEqual(d.day, 31) From f4044de386108b666540104e2ea748746b0b369e Mon Sep 17 00:00:00 2001 From: hparfr Date: Wed, 17 Jul 2019 13:13:42 +0200 Subject: [PATCH 004/138] Add button-box to recurring order form --- fieldservice_recurring/static/description/index.html | 2 +- fieldservice_recurring/views/fsm_recurring.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/fieldservice_recurring/static/description/index.html b/fieldservice_recurring/static/description/index.html index 7e768d7ecf..fe4d952b68 100644 --- a/fieldservice_recurring/static/description/index.html +++ b/fieldservice_recurring/static/description/index.html @@ -3,7 +3,7 @@ - + Field Service Recurring Work Orders