265 lines
8.1 KiB
Python
265 lines
8.1 KiB
Python
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
import datetime as dt
|
|
|
|
from dateutil.rrule import (
|
|
DAILY, FR, MO, MONTHLY, SA, SU, TH, TU, WE, WEEKLY, YEARLY, rrule,
|
|
rruleset)
|
|
|
|
from trytond.i18n import gettext
|
|
from trytond.model import ModelSQL, ModelView, fields
|
|
from trytond.pool import Pool
|
|
from trytond.transaction import Transaction
|
|
from trytond.wizard import Button, StateView, Wizard
|
|
|
|
from .exceptions import RecurrenceRuleValidationError
|
|
|
|
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 RecurrenceRuleSet(ModelSQL, ModelView):
|
|
__name__ = 'sale.subscription.recurrence.rule.set'
|
|
|
|
name = fields.Char(
|
|
"Name", required=True, translate=True,
|
|
help="The main identifier of the rule set.")
|
|
rules = fields.One2Many(
|
|
'sale.subscription.recurrence.rule', 'set_', "Rules")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order.insert(0, ('name', 'ASC'))
|
|
|
|
@classmethod
|
|
def default_rules(cls):
|
|
if Transaction().user == 0:
|
|
return []
|
|
return [{}]
|
|
|
|
def rruleset(self, dtstart):
|
|
set_ = rruleset(**self._rruleset())
|
|
for rule in self.rules:
|
|
if not rule.exclusive:
|
|
set_.rrule(rule.rrule(dtstart))
|
|
else:
|
|
set_.exrule(rule.rrule(dtstart))
|
|
return set_
|
|
|
|
def _rruleset(self):
|
|
return {}
|
|
|
|
|
|
class RecurrenceRule(ModelSQL, ModelView):
|
|
__name__ = 'sale.subscription.recurrence.rule'
|
|
|
|
set_ = fields.Many2One(
|
|
'sale.subscription.recurrence.rule.set', "Set",
|
|
required=True, ondelete='CASCADE',
|
|
help="Add the rule below the set.")
|
|
freq = fields.Selection([
|
|
('yearly', 'Yearly'),
|
|
('monthly', 'Monthly'),
|
|
('weekly', 'Weekly'),
|
|
('daily', 'Daily'),
|
|
], "Frequency", sort=False, required=True)
|
|
interval = fields.Integer("Interval", required=True)
|
|
byweekday = fields.Char(
|
|
"By Week Day",
|
|
help="A comma separated list of integers or weekday (MO, TU etc).")
|
|
bymonthday = fields.Char(
|
|
"By Month Day",
|
|
help="A comma separated list of integers.")
|
|
byyearday = fields.Char(
|
|
"By Year Day",
|
|
help="A comma separated list of integers.")
|
|
byweekno = fields.Char(
|
|
"By Week Number",
|
|
help="A comma separated list of integers (ISO8601).")
|
|
bymonth = fields.Char(
|
|
"By Month",
|
|
help="A comma separated list of integers.")
|
|
bysetpos = fields.Char(
|
|
"By Position",
|
|
help="A comma separated list of integers.")
|
|
week_start_day = fields.Many2One('ir.calendar.day', "Week Start Day")
|
|
|
|
exclusive = fields.Boolean(
|
|
"Exclusive",
|
|
help="If checked, dates which are part of this recurrence rule "
|
|
"will not be generated.")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls.__access__.add('set_')
|
|
|
|
@classmethod
|
|
def default_interval(cls):
|
|
return 1
|
|
|
|
@classmethod
|
|
def default_exclusive(cls):
|
|
return False
|
|
|
|
def rrule(self, dtstart):
|
|
return rrule(**self._rrule(dtstart))
|
|
|
|
def _rrule(self, dtstart):
|
|
return {
|
|
'dtstart': dtstart,
|
|
'freq': FREQUENCIES[self.freq],
|
|
'interval': self.interval,
|
|
'byweekday': self._byweekday,
|
|
'bymonthday': self._bymonthday,
|
|
'byyearday': self._byyearday,
|
|
'byweekno': self._byweekno,
|
|
'bymonth': self._bymonth,
|
|
'bysetpos': self._bysetpos,
|
|
'wkst': self.week_start_day.index if self.week_start_day else None,
|
|
}
|
|
|
|
@property
|
|
def _byweekday(self):
|
|
if not self.byweekday:
|
|
return None
|
|
byweekday = []
|
|
for weekday in self.byweekday.split(','):
|
|
try:
|
|
weekday = int(weekday)
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
if 0 <= weekday <= len(WEEKDAYS):
|
|
byweekday.append(weekday)
|
|
continue
|
|
else:
|
|
raise ValueError('Invalid weekday')
|
|
try:
|
|
cls = WEEKDAYS[weekday[:2]]
|
|
except KeyError:
|
|
raise ValueError('Invalid weekday')
|
|
if not weekday[2:]:
|
|
byweekday.append(cls)
|
|
else:
|
|
byweekday.append(cls(int(weekday[3:-1])))
|
|
return byweekday
|
|
|
|
@property
|
|
def _bymonthday(self):
|
|
if not self.bymonthday:
|
|
return None
|
|
return [int(md) for md in self.bymonthday.split(',')]
|
|
|
|
@property
|
|
def _byyearday(self):
|
|
if not self.byyearday:
|
|
return None
|
|
return [int(yd) for yd in self.byyearday.split(',')]
|
|
|
|
@property
|
|
def _byweekno(self):
|
|
if not self.byweekno:
|
|
return None
|
|
return [int(wn) for wn in self.byweekno.split(',')]
|
|
|
|
@property
|
|
def _bymonth(self):
|
|
if not self.bymonth:
|
|
return None
|
|
return [int(m) for m in self.bymonth.split(',')]
|
|
|
|
@property
|
|
def _bysetpos(self):
|
|
if not self.bysetpos:
|
|
return None
|
|
positions = []
|
|
for setpos in self.bysetpos.split(','):
|
|
setpos = int(setpos)
|
|
if -366 <= setpos <= 366:
|
|
positions.append(setpos)
|
|
else:
|
|
raise ValueError('Invalid setpos')
|
|
return positions
|
|
|
|
def pre_validate(self):
|
|
for name in ['byweekday', 'bymonthday', 'byyearday', 'byweekno',
|
|
'bymonth', 'bysetpos']:
|
|
self.check_by(name)
|
|
|
|
def check_by(self, name):
|
|
try:
|
|
getattr(self, '_%s' % name)
|
|
except ValueError as exception:
|
|
raise RecurrenceRuleValidationError(
|
|
gettext('sale_subscription.msg_recurrence_rule_invalid_by',
|
|
value=getattr(self, name),
|
|
recurrence_rule=self.rec_name,
|
|
exception=exception,
|
|
**self.__names__(name))) from exception
|
|
|
|
|
|
class TestRecurrenceRuleSet(Wizard):
|
|
__name__ = 'sale.subscription.recurrence.rule.set.test'
|
|
start_state = 'test'
|
|
test = StateView(
|
|
'sale.subscription.recurrence.rule.set.test',
|
|
'sale_subscription.recurrence_rule_set_test_view_form',
|
|
[Button("Close", 'end', 'tryton-close', default=True)])
|
|
|
|
def default_test(self, fields):
|
|
default = {}
|
|
if (self.model and self.model.__name__
|
|
== 'sale.subscription.recurrence.rule.set'):
|
|
if self.record:
|
|
default['recurrence'] = self.record.id
|
|
return default
|
|
|
|
|
|
class TestRecurrenceRuleSetView(ModelView):
|
|
__name__ = 'sale.subscription.recurrence.rule.set.test'
|
|
recurrence = fields.Many2One(
|
|
'sale.subscription.recurrence.rule.set',
|
|
"Subscription Recurrence", required=True)
|
|
start_date = fields.Date("Start Date", required=True)
|
|
count = fields.Integer("Count", required=True,
|
|
help="Used to determine how many occurences to compute.")
|
|
result = fields.One2Many(
|
|
'sale.subscription.recurrence.rule.set.test.result',
|
|
None, "Result", readonly=True)
|
|
|
|
@classmethod
|
|
def default_start_date(cls):
|
|
return Pool().get('ir.date').today()
|
|
|
|
@fields.depends('recurrence', 'start_date', 'count', 'result')
|
|
def on_change_with_result(self):
|
|
pool = Pool()
|
|
Result = pool.get('sale.subscription.recurrence.rule.set.test.result')
|
|
result = []
|
|
if self.recurrence and self.start_date and self.count:
|
|
rruleset = self.recurrence.rruleset(self.start_date)
|
|
datetime = dt.datetime.combine(self.start_date, dt.time())
|
|
for date in rruleset.xafter(datetime, self.count, inc=True):
|
|
result.append(Result(date=date.date()))
|
|
return result
|
|
|
|
|
|
class TestRecurrenceRuleSetViewResult(ModelView):
|
|
__name__ = 'sale.subscription.recurrence.rule.set.test.result'
|
|
date = fields.Date("Date", readonly=True)
|