2024-02-24 22:43:59 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# This file is part of the cashbook-planner from m-ds for Tryton.
|
|
|
|
# The COPYRIGHT file at the top level of this repository contains the
|
|
|
|
# full copyright notices and license terms.
|
|
|
|
|
2024-02-25 22:10:40 +00:00
|
|
|
from datetime import date
|
|
|
|
from dateutil.rrule import (
|
|
|
|
rrule, YEARLY, MONTHLY, WEEKLY, DAILY, MO, TU, WE, TH, FR, SA, SU)
|
2024-02-24 22:43:59 +00:00
|
|
|
from trytond.model import ModelSQL, ModelView, fields, Index, DeactivableMixin
|
|
|
|
from trytond.transaction import Transaction
|
|
|
|
from trytond.pool import Pool
|
2024-02-25 22:10:40 +00:00
|
|
|
from trytond.report import Report
|
|
|
|
from trytond.pyson import Eval, Bool, If, And
|
2024-02-24 22:43:59 +00:00
|
|
|
|
|
|
|
DEF_NONE = None
|
2024-02-25 22:10:40 +00:00
|
|
|
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')]
|
2024-02-24 22:43:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView):
|
|
|
|
'Scheduled Booking'
|
|
|
|
__name__ = 'cashbook.planner'
|
|
|
|
|
|
|
|
company = fields.Many2One(
|
|
|
|
string='Company', model_name='company.company',
|
|
|
|
required=True, ondelete="RESTRICT")
|
|
|
|
name = fields.Char(string='Name', required=True)
|
|
|
|
description = fields.Text(string='Description')
|
|
|
|
cashbook = fields.Many2One(
|
|
|
|
string='Cashbook', required=True,
|
|
|
|
help='Cash book for which the planned posting is to be executed.',
|
|
|
|
model_name='cashbook.book', ondelete='CASCADE',
|
|
|
|
domain=[('btype', '!=', None)])
|
|
|
|
start_date = fields.Date(string='Start Date', required=True)
|
|
|
|
end_date = fields.Date(
|
|
|
|
string='End Date', depends=['start_date'],
|
|
|
|
states={'readonly': ~Bool(Eval('start_date'))},
|
|
|
|
domain=[
|
|
|
|
'OR',
|
|
|
|
('end_date', '>', Eval('start_date')),
|
|
|
|
('end_date', '=', DEF_NONE)])
|
2024-02-25 22:10:40 +00:00
|
|
|
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')
|
2024-02-27 21:40:59 +00:00
|
|
|
nextrun = fields.One2Many(
|
|
|
|
string='Next Execution Date', size=1, field='planner',
|
|
|
|
model_name='cashbook.planner.nextrun')
|
2024-02-25 22:10:40 +00:00
|
|
|
|
2024-02-24 22:43:59 +00:00
|
|
|
@classmethod
|
|
|
|
def __setup__(cls):
|
|
|
|
super(ScheduledBooking, cls).__setup__()
|
|
|
|
t = cls.__table__()
|
|
|
|
cls._sql_indexes.update({
|
|
|
|
Index(
|
|
|
|
t,
|
|
|
|
(t.company, Index.Equality())),
|
|
|
|
Index(
|
|
|
|
t,
|
|
|
|
(t.start_date, Index.Range(order='ASC'))),
|
|
|
|
Index(
|
|
|
|
t,
|
|
|
|
(t.end_date, Index.Range(order='ASC')),
|
|
|
|
where=t.end_date != DEF_NONE),
|
|
|
|
})
|
|
|
|
|
2024-02-29 22:20:19 +00:00
|
|
|
def _compute_dates_by_rrule(self, query_date=None, count=5, params={}):
|
2024-02-25 22:10:40 +00:00
|
|
|
""" run rrule with values from record or from 'params'
|
|
|
|
|
|
|
|
Args:
|
2024-02-29 22:20:19 +00:00
|
|
|
query_date (date, optional): Start date as a filter for
|
2024-02-25 22:10:40 +00:00
|
|
|
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,
|
2024-02-26 21:46:20 +00:00
|
|
|
'99': None}.get(params.get('weekday', self.weekday), None)
|
2024-02-25 22:10:40 +00:00
|
|
|
|
2024-02-26 21:46:20 +00:00
|
|
|
if count is None:
|
|
|
|
count = 5
|
|
|
|
count = 1 if count < 1 else 100 if count > 100 else count
|
2024-02-25 22:10:40 +00:00
|
|
|
|
|
|
|
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)
|
2024-02-26 21:46:20 +00:00
|
|
|
if interval is None:
|
|
|
|
interval = 1
|
2024-02-25 22:10:40 +00:00
|
|
|
interval = 1 if interval < 1 else 10 if interval > 10 else interval
|
|
|
|
|
2024-02-26 21:46:20 +00:00
|
|
|
assert (monthday is None) or (pweekday is None), \
|
|
|
|
"weekday and monthday cannot be used together"
|
|
|
|
|
2024-02-25 22:10:40 +00:00
|
|
|
dtrule = rrule(
|
2024-02-26 21:46:20 +00:00
|
|
|
freq=frequ, byweekday=pweekday,
|
2024-02-25 22:10:40 +00:00
|
|
|
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:
|
2024-02-29 22:20:19 +00:00
|
|
|
if (query_date and (x.date() >= query_date)) or \
|
|
|
|
(query_date is None):
|
2024-02-25 22:10:40 +00:00
|
|
|
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:
|
2024-02-29 22:20:19 +00:00
|
|
|
nextrun_querydate (date, optional): start date for dates in result,
|
2024-02-25 22:10:40 +00:00
|
|
|
defaults to today if not set or None
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
string: formatted list of dates
|
|
|
|
"""
|
|
|
|
IrDate = Pool().get('ir.date')
|
|
|
|
context = Transaction().context
|
|
|
|
|
2024-02-29 22:20:19 +00:00
|
|
|
query_date = context.get('nextrun_querydate', None)
|
|
|
|
if not isinstance(query_date, date):
|
|
|
|
query_date = IrDate.today()
|
2024-02-25 22:10:40 +00:00
|
|
|
|
|
|
|
return ' | '.join([
|
|
|
|
Report.format_date(x)
|
|
|
|
for x in self._compute_dates_by_rrule(
|
2024-02-29 22:20:19 +00:00
|
|
|
query_date=query_date,
|
2024-02-25 22:10:40 +00:00
|
|
|
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'
|
|
|
|
|
2024-02-24 22:43:59 +00:00
|
|
|
@staticmethod
|
|
|
|
def default_company():
|
|
|
|
return Transaction().context.get('company') or None
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def default_start_date(cls):
|
|
|
|
""" get today as start-date
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
date: date of today
|
|
|
|
"""
|
|
|
|
IrDate = Pool().get('ir.date')
|
|
|
|
return IrDate.today()
|
|
|
|
|
2024-02-27 21:40:59 +00:00
|
|
|
@classmethod
|
|
|
|
def update_next_occurence(cls, records):
|
|
|
|
""" compute date of next execution, create/update nextrun-record,
|
|
|
|
delete nextrun-record if scheduled booking is disabled
|
|
|
|
|
|
|
|
Args:
|
|
|
|
records (list): scheduled-booking records
|
|
|
|
"""
|
|
|
|
pool = Pool()
|
|
|
|
IrDate = pool.get('ir.date')
|
|
|
|
NextRun = pool.get('cashbook.planner.nextrun')
|
2024-02-29 22:20:19 +00:00
|
|
|
context = Transaction().context
|
2024-02-27 21:40:59 +00:00
|
|
|
|
|
|
|
to_create = []
|
|
|
|
to_write = []
|
|
|
|
to_delete = []
|
|
|
|
for record in records:
|
|
|
|
if not record.active:
|
|
|
|
# delete nextrun-record if disabled
|
|
|
|
if record.nextrun:
|
|
|
|
to_delete.extend(record.nextrun)
|
|
|
|
elif record.active:
|
|
|
|
# get next-run date
|
|
|
|
next_date = record._compute_dates_by_rrule(
|
2024-02-29 22:20:19 +00:00
|
|
|
query_date=context.get(
|
|
|
|
'nextrun_querydate', IrDate.today()),
|
|
|
|
count=1)
|
2024-02-27 21:40:59 +00:00
|
|
|
if next_date:
|
|
|
|
next_date = next_date[0]
|
|
|
|
else:
|
2024-02-29 22:20:19 +00:00
|
|
|
if record.nextrun:
|
|
|
|
to_delete.extend(record.nextrun)
|
2024-02-27 21:40:59 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
if not record.nextrun:
|
|
|
|
# add record if not exist
|
|
|
|
to_create.append({'planner': record.id, 'date': next_date})
|
|
|
|
else:
|
|
|
|
# update existing records
|
|
|
|
for nxrun in record.nextrun:
|
|
|
|
if nxrun.date != next_date:
|
|
|
|
to_write.extend([[nxrun], {'date': next_date}])
|
|
|
|
if to_create:
|
|
|
|
NextRun.create(to_create)
|
|
|
|
if to_delete:
|
|
|
|
NextRun.delete(to_delete)
|
|
|
|
if to_write:
|
|
|
|
NextRun.write(*to_write)
|
|
|
|
|
2024-02-29 22:20:19 +00:00
|
|
|
@classmethod
|
|
|
|
def create(cls, vlist):
|
|
|
|
""" update nextrun-records on create of planner-records
|
|
|
|
|
|
|
|
Args:
|
|
|
|
vlist (list of dict): values to create records
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
list: created records
|
|
|
|
"""
|
|
|
|
records = super(ScheduledBooking, cls).create(vlist)
|
|
|
|
cls.update_next_occurence(records)
|
|
|
|
return records
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def write(cls, *args):
|
|
|
|
""" update nextrun-records on create of planner-records
|
|
|
|
"""
|
|
|
|
to_update = []
|
|
|
|
actions = iter(args)
|
|
|
|
for records, values in zip(actions, actions):
|
|
|
|
to_update.extend(records)
|
|
|
|
super(ScheduledBooking, cls).write(*args)
|
|
|
|
cls.update_next_occurence(records)
|
|
|
|
|
2024-02-27 21:40:59 +00:00
|
|
|
@classmethod
|
|
|
|
def cronjob(cls):
|
|
|
|
pass
|
|
|
|
|
2024-02-24 22:43:59 +00:00
|
|
|
# ens ScheduledBooking
|