Recurrence rule + tests
This commit is contained in:
parent
ebb1efe7a8
commit
ed350ba3e2
6 changed files with 564 additions and 19 deletions
213
planner.py
213
planner.py
|
@ -3,12 +3,26 @@
|
|||
# The COPYRIGHT file at the top level of this repository contains the
|
||||
# full copyright notices and license terms.
|
||||
|
||||
from datetime import date
|
||||
from dateutil.rrule import (
|
||||
rrule, YEARLY, MONTHLY, WEEKLY, DAILY, MO, TU, WE, TH, FR, SA, SU)
|
||||
from trytond.model import ModelSQL, ModelView, fields, Index, DeactivableMixin
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.pool import Pool
|
||||
from trytond.pyson import Eval, Bool
|
||||
from trytond.report import Report
|
||||
from trytond.pyson import Eval, Bool, If, And
|
||||
|
||||
DEF_NONE = None
|
||||
SEL_FREQU = [
|
||||
('year', 'Yearly'),
|
||||
('month', 'Monthly'),
|
||||
('week', 'Weekly'),
|
||||
('day', 'Daily')]
|
||||
SEL_WEEKDAY = [
|
||||
('99', '-'),
|
||||
('0', 'Monday'), ('1', 'Tuesday'), ('2', 'Wednesday'),
|
||||
('3', 'Thursday'), ('4', 'Friday'), ('5', 'Saturday'),
|
||||
('6', 'Sunday')]
|
||||
|
||||
|
||||
class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
|
||||
|
@ -33,6 +47,45 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
|
|||
'OR',
|
||||
('end_date', '>', Eval('start_date')),
|
||||
('end_date', '=', DEF_NONE)])
|
||||
frequ = fields.Selection(
|
||||
string='Frequency', required=True, selection=SEL_FREQU, sort=False)
|
||||
weekday = fields.Selection(
|
||||
string='Weekday', required=True, selection=SEL_WEEKDAY, sort=False,
|
||||
help='Select a day of the week if you want the rule to ' +
|
||||
'run on that day.',
|
||||
depends=['frequ'],
|
||||
states={'invisible': Eval('frequ') != 'month'})
|
||||
COND_SETPOS = And(Eval('weekday', '') != '99', Eval('frequ') == 'month')
|
||||
setpos = fields.Integer(
|
||||
string='Occurrence', depends=['weekday', 'frequ'],
|
||||
domain=[
|
||||
If(COND_SETPOS,
|
||||
[('setpos', '<=', 4), ('setpos', '>=', 1)],
|
||||
('setpos', '=', None))],
|
||||
help='For example, if you want to run the rule on the second ' +
|
||||
'Wednesday of the month, enter 2 here.',
|
||||
states={'required': COND_SETPOS, 'invisible': ~COND_SETPOS})
|
||||
COND_MONTHDAY = And(Eval('weekday', '') == '99', Eval('frequ') == 'month')
|
||||
monthday = fields.Integer(
|
||||
string='Day of month',
|
||||
help='If you want the rule to run on a specific day of the month, ' +
|
||||
'select the day here.',
|
||||
domain=[
|
||||
If(COND_MONTHDAY,
|
||||
[('monthday', '>=', 1), ('monthday', '<=', 31)],
|
||||
('monthday', '=', None))],
|
||||
depends=['weekday', 'frequ'],
|
||||
states={'required': COND_MONTHDAY, 'invisible': ~COND_MONTHDAY})
|
||||
interval = fields.Integer(
|
||||
string='Interval', required=True,
|
||||
help='Select an interval to run the rule on every n-th date.',
|
||||
domain=[('interval', '>=', 1), ('interval', '<=', 10)])
|
||||
nextdates = fields.Function(fields.Char(
|
||||
string='Next Dates', readonly=True,
|
||||
help='the next 5 appointments based on the configured rule'),
|
||||
'on_change_with_nextdates')
|
||||
|
||||
# rrule: frequ, dtstart, until, bymonthday
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
|
@ -51,6 +104,164 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
|
|||
where=t.end_date != DEF_NONE),
|
||||
})
|
||||
|
||||
def _compute_dates_by_rrule(self, start_date=None, count=5, params={}):
|
||||
""" run rrule with values from record or from 'params'
|
||||
|
||||
Args:
|
||||
start_date (date, optional): Start date as a filter for
|
||||
recurrences. Defaults to None.
|
||||
count (int, optional): number of recurrences in result.
|
||||
Defaults to 5. max value = 100
|
||||
params (dict, optional): Values in the dictionary are
|
||||
used instead of the stored values, Defaults to {},
|
||||
allowed: frequ, weekday, start_date,
|
||||
end_date (preferred over 'count'),
|
||||
monthday, interval, setpos
|
||||
|
||||
Returns:
|
||||
list: date values, result of rrlue
|
||||
"""
|
||||
pfrequ = {
|
||||
'year': YEARLY, 'month': MONTHLY, 'week': WEEKLY, 'day': DAILY}
|
||||
pweekday = {
|
||||
'0': MO, '1': TU, '2': WE, '3': TH, '4': FR, '5': SA, '6': SU,
|
||||
'99': None}
|
||||
|
||||
if (count is None) or (count > 100):
|
||||
count = 100
|
||||
if count < 1:
|
||||
count = 1
|
||||
|
||||
end_date = params.get('end_date', self.end_date)
|
||||
frequ = pfrequ[params.get('frequ', self.frequ)]
|
||||
|
||||
setpos = params.get('setpos', self.setpos)
|
||||
if setpos is not None:
|
||||
setpos = 1 if setpos < 1 else 4 if setpos > 4 else setpos
|
||||
|
||||
monthday = params.get('monthday', self.monthday)
|
||||
if monthday is not None:
|
||||
monthday = 1 if monthday < 1 else 31 if monthday > 31 else monthday
|
||||
|
||||
interval = params.get('interval', self.interval)
|
||||
interval = 1 if interval < 1 else 10 if interval > 10 else interval
|
||||
|
||||
dtrule = rrule(
|
||||
freq=frequ,
|
||||
byweekday=pweekday[params.get('weekday', self.weekday)],
|
||||
dtstart=params.get('start_date', self.start_date),
|
||||
until=end_date,
|
||||
bysetpos=setpos if frequ == MONTHLY else None,
|
||||
bymonthday=monthday, interval=interval)
|
||||
|
||||
result = []
|
||||
for x in dtrule:
|
||||
if (start_date and (x.date() >= start_date)) or \
|
||||
(start_date is None):
|
||||
result.append(x.date())
|
||||
if len(result) >= count:
|
||||
break
|
||||
return result
|
||||
|
||||
@fields.depends(
|
||||
'start_date', 'end_date', 'frequ', 'weekday', 'monthday',
|
||||
'interval', 'setpos')
|
||||
def on_change_with_nextdates(self, name=None):
|
||||
""" Calculates the next 5 appointments based on the configured rule,
|
||||
returns a formatted date list
|
||||
|
||||
Args:
|
||||
name (string, optional): name of field. Defaults to None.
|
||||
|
||||
context:
|
||||
start_date (date, optional): start date for dates in result,
|
||||
defaults to today if not set or None
|
||||
|
||||
Returns:
|
||||
string: formatted list of dates
|
||||
"""
|
||||
IrDate = Pool().get('ir.date')
|
||||
context = Transaction().context
|
||||
|
||||
start_date = context.get('start_date', None)
|
||||
if not isinstance(start_date, date):
|
||||
start_date = IrDate.today()
|
||||
|
||||
return ' | '.join([
|
||||
Report.format_date(x)
|
||||
for x in self._compute_dates_by_rrule(
|
||||
start_date=start_date,
|
||||
params={
|
||||
'start_date': self.start_date,
|
||||
'end_date': self.end_date,
|
||||
'frequ': self.frequ,
|
||||
'weekday': self.weekday,
|
||||
'monthday': self.monthday,
|
||||
'interval': self.interval,
|
||||
'setpos': self.setpos}
|
||||
)])
|
||||
|
||||
@fields.depends('frequ', 'setpos', 'weekday', 'monthday')
|
||||
def on_change_frequ(self):
|
||||
""" update fields
|
||||
"""
|
||||
if self.frequ and self.frequ == 'month':
|
||||
if self.weekday:
|
||||
if self.weekday == '99':
|
||||
if self.monthday is None:
|
||||
self.monthday = 1
|
||||
self.setpos = None
|
||||
else:
|
||||
if self.setpos is None:
|
||||
self.setpos = 1
|
||||
self.monthday = None
|
||||
else:
|
||||
self.setpos = None
|
||||
self.monthday = None
|
||||
self.weekday = '99'
|
||||
|
||||
@fields.depends('frequ', 'setpos', 'weekday', 'monthday')
|
||||
def on_change_weekday(self):
|
||||
""" clear day-of-month if weekday is used
|
||||
"""
|
||||
self.on_change_frequ()
|
||||
|
||||
@classmethod
|
||||
def default_interval(cls):
|
||||
""" get default for interval
|
||||
|
||||
Returns:
|
||||
int: 1 = each occurence
|
||||
"""
|
||||
return 1
|
||||
|
||||
@classmethod
|
||||
def default_weekday(cls):
|
||||
""" get default for weekday-rule
|
||||
|
||||
Returns:
|
||||
string: '99' = not set
|
||||
"""
|
||||
return '99'
|
||||
|
||||
@classmethod
|
||||
def default_monthday(cls):
|
||||
""" get default for day-of-month
|
||||
|
||||
Returns:
|
||||
int: 1
|
||||
"""
|
||||
return 1
|
||||
|
||||
@classmethod
|
||||
def default_frequ(cls):
|
||||
""" get default for frequency
|
||||
|
||||
Returns:
|
||||
string: 'month'
|
||||
"""
|
||||
return 'month'
|
||||
|
||||
@staticmethod
|
||||
def default_company():
|
||||
return Transaction().context.get('company') or None
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue