# -*- 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. 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.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): '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)]) 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') nextrun = fields.One2Many( string='Next Execution Date', size=1, field='planner', model_name='cashbook.planner.nextrun') @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), }) 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}.get(params.get('weekday', self.weekday), None) if count is None: count = 5 count = 1 if count < 1 else 100 if count > 100 else count 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) if interval is None: interval = 1 interval = 1 if interval < 1 else 10 if interval > 10 else interval assert (monthday is None) or (pweekday is None), \ "weekday and monthday cannot be used together" dtrule = rrule( freq=frequ, byweekday=pweekday, 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 @classmethod def default_start_date(cls): """ get today as start-date Returns: date: date of today """ IrDate = Pool().get('ir.date') return IrDate.today() @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') 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( start_date=IrDate.today(), count=1) if next_date: next_date = next_date[0] else: 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) @classmethod def cronjob(cls): pass # ens ScheduledBooking