Files
tradon/modules/sale_subscription/recurrence.py
2026-03-14 09:42:12 +00:00

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)