diff --git a/README.rst b/README.rst index 51a3cf8..3f53504 100644 --- a/README.rst +++ b/README.rst @@ -14,6 +14,22 @@ Requires Changes ======= -*7.0.0 - 24.02.2024* +*7.0.5 - 08.06.2024* -- init \ No newline at end of file +- add: re-arrange date of occurence to working day + +*7.0.4 - 11.03.2024* + +- add: default-order for table + +*7.0.3 - 10.03.2024* + +- new placeholder 'rate' for description of booking + +*7.0.2 - 10.03.2024* + +- fix permissions + +*7.0.1 - 09.03.2024* + +- works diff --git a/__init__.py b/__init__.py index 3e86618..68ef3e4 100644 --- a/__init__.py +++ b/__init__.py @@ -9,10 +9,13 @@ from .planner import ScheduledBooking, ScheduledBookingCashbookRel from .cashbook import Cashbook, CashbookLine from .cron import Cron from .nextrun import NextRun +from .config import Configuration, UserConfiguration def register(): Pool.register( + Configuration, + UserConfiguration, Rule, ScheduledBooking, NextRun, diff --git a/config.py b/config.py new file mode 100644 index 0000000..e20aaea --- /dev/null +++ b/config.py @@ -0,0 +1,211 @@ +# -*- 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, timedelta +from dateutil.easter import ( + easter, EASTER_JULIAN, EASTER_ORTHODOX, EASTER_WESTERN) +from trytond.pool import PoolMeta, Pool +from trytond.model import fields +from trytond.transaction import Transaction +from trytond.report import Report + + +holidays = fields.Char( + string='Holidays', help='Semicolon separate list of dates: ' + + 'yyyy-mm-dd = single date, mm-dd = annual repetition, ' + + 'easter[greg|jul|orth] = Easter Sunday, ascension = Ascension Day, ' + + 'whitsun = Whitsunday, offset with :+/-n e.g.: easter:+1 = Easter Monday') + + +class Configuration(metaclass=PoolMeta): + __name__ = 'cashbook.configuration' + + holidays = fields.MultiValue(holidays) + holidays_info = fields.Function(fields.Char( + string='Holidays', readonly=True, + help='Holidays in the current year.'), 'on_change_with_holidays_info') + + @fields.depends('holidays') + def on_change_with_holidays_info(self, name=None): + """ get string of generated holidays for years in + context-value 'holiday_years' or current year + + Args: + name (str, optional): field. Defaults to None. + + Returns: + str: formatted holidays in language of user + """ + pool = Pool() + Config = pool.get('cashbook.configuration') + context = Transaction().context + + years = context.get('holiday_years', []) + cfg1 = Config.get_singleton() + if cfg1: + dates = cfg1.holiday_dates(years) + dates.sort() + return '|'.join([ + Report.format_date(x) for x in dates]) + + def holiday_dates(self, years=[]): + """ get list of dates for list of years + + Args: + years (list, optional): years to get holidays for. Defaults to []. + + Returns: + list of date: holidays for requestd years + """ + pool = Pool() + IrDate = pool.get('ir.date') + Config = pool.get('cashbook.configuration') + + if not years: + years = [IrDate.today().year] + + cfg1 = Config.get_singleton() + if not (cfg1 and cfg1.holidays and isinstance(cfg1.holidays, str)): + return [] + return Config.holiday_parseconfig(cfg1.holidays, years)['dates'] + + @classmethod + def holiday_parseconfig(cls, holiday_string, years=[]): + """ read holiday config, generate parsed list of defines + + Args: + holiday_string (str): holiday definition string + years (list of int): years to generate dates for + + Returns: + dict: {'definition': '', + 'dates': []} + """ + IrDate = Pool().get('ir.date') + + def parse_date_offet(offset_str): + """ parse offset string + + Args: + offset_str (str): '+n' or '-n' + + Returns: + tuple: (int(offset), 'offset-string') + """ + # decode ':+n' or ':-n' + offset_value = 0 + plus_sign = 1 + if offset_str: + plus_sign = -1 if offset_str.startswith('-') else 1 + date_offset = offset_str[1:] + if date_offset.isdigit(): + offset_value = int(date_offset) + return (offset_value * plus_sign, '%(sign)s%(amount)d' % { + 'sign': '+' if plus_sign >= 0 else '-', + 'amount': offset_value}) + + def parse_date_definition(date_str, years): + """ parse date definition string, generate list of + dates + + Args: + date_str (str): definition string + years (list of int): years to generate dates for + + Returns: + _type_: _description_ + """ + dates = [] + date_def = '' + easter_type = { + 'greg': EASTER_WESTERN, 'jul': EASTER_JULIAN, + 'orth': EASTER_ORTHODOX} + + date_str = date_str.lower() + # first parse easter-based dates + for dt_calc in [ + {'type': 'easter', 'days': 0}, + {'type': 'ascension', 'days': 39}, + {'type': 'whitsun', 'days': 49}]: + if date_str.startswith(dt_calc['type']): + e_meth = date_str[len(dt_calc['type']):] + easter_meth = easter_type.get(e_meth, EASTER_WESTERN) + dates.extend([ + easter(x, easter_meth) + + timedelta(days=dt_calc['days']) + for x in years]) + date_def = date_str + + # if not detected try date string + if not date_def: + date_fields = date_str.split('-') + try: + if len(date_fields) == 3: + dates.append(date.fromisoformat(date_str)) + date_def = date_str + elif len(date_fields) == 2: + for year in years: + dates.append(date.fromisoformat( + str(year) + '-' + date_str)) + date_def = date_str + except Exception: + pass + return (dates, date_def) + + if not (holiday_string and isinstance(holiday_string, str)): + return {'definition': '', 'dates': []} + + if not years: + years = [IrDate.today().year] + + parsed_str = [] + parsed_dates = [] + for datedef in holiday_string.split(';'): + if not datedef: + continue + + datedef = datedef.strip().split(':') + date_offset = datedef[1] if len(datedef) > 1 else '' + + (date_offset, offset_str) = parse_date_offet(date_offset) + (date_lst, date_def) = parse_date_definition(datedef[0], years) + + parsed_dates.extend([ + x + timedelta(days=date_offset) + for x in date_lst]) + if date_def: + if date_offset != 0: + date_def += ':' + offset_str + parsed_str.append(date_def) + return {'definition': ';'.join(parsed_str), 'dates': parsed_dates} + + @classmethod + def multivalue_model(cls, field): + """ get model for value + """ + pool = Pool() + + if field in ['holidays']: + return pool.get('cashbook.configuration_user') + return super(Configuration, cls).multivalue_model(field) + + @classmethod + def default_holidays(cls, **pattern): + return cls.multivalue_model('holidays').default_holidays() + +# end Configuration + + +class UserConfiguration(metaclass=PoolMeta): + __name__ = 'cashbook.configuration_user' + + holidays = holidays + + @classmethod + def default_holidays(cls): + return 'easter+1;easter-2;ascension;05-01;12-25;12-26;01-01;' + +# end CashbookLine diff --git a/config.xml b/config.xml new file mode 100644 index 0000000..19802e9 --- /dev/null +++ b/config.xml @@ -0,0 +1,15 @@ + + + + + + + cashbook.configuration + + configuration_form + + + + diff --git a/group.xml b/group.xml index 5ae80f0..105a2ef 100644 --- a/group.xml +++ b/group.xml @@ -7,6 +7,7 @@ full copyright notices and license terms. --> Cashbook - Scheduled Bookings + diff --git a/ir.py b/ir.py index 7cfd296..d3ec250 100644 --- a/ir.py +++ b/ir.py @@ -14,7 +14,9 @@ class Rule(metaclass=PoolMeta): """ list of models to add 'user_id' to context """ result = super(Rule, cls)._context_modelnames() - result.append('cashbook.planner') + result.extend([ + 'cashbook.planner', + 'cashbook.planner.nextrun']) return result # end Rule diff --git a/locale/de.po b/locale/de.po index 92cfe06..87af133 100644 --- a/locale/de.po +++ b/locale/de.po @@ -79,6 +79,30 @@ msgid "User in companies" msgstr "Benutzer im Unternehmen" +########################## +# cashbook.configuration # +########################## +msgctxt "view:cashbook.configuration:" +msgid "Scheduled Bookings" +msgstr "geplante Buchungen" + +msgctxt "field:cashbook.configuration,holidays:" +msgid "Holidays" +msgstr "Feiertage" + +msgctxt "help:cashbook.configuration,holidays:" +msgid "Semicolon separate list of dates: yyyy-mm-dd = single date, mm-dd = annual repetition, easter[greg|jul|orth] = Easter Sunday, ascension = Ascension Day, whitsun = Whitsunday, offset with :+/-n e.g.: easter:+1 = Easter Monday" +msgstr "Semikolon getrennte Liste von Datumswerten: yyyy-mm-dd = Einzeldatum, mm-dd = jährliche Wiederholung, easter[greg|jul|orth] = Ostersonntag, ascension = Christi Himmelfahrt, whitsun = Pfingstsonntag, Offset mit :+/-n z.B: easter:+1 = Ostermontag" + +msgctxt "field:cashbook.configuration,holidays_info:" +msgid "Holidays" +msgstr "Feiertage" + +msgctxt "help:cashbook.configuration,holidays_info:" +msgid "Holidays in the current year." +msgstr "Feiertage im aktuellen Jahr." + + #################### # cashbook.planner # #################### @@ -107,8 +131,8 @@ msgid "Booking text" msgstr "Buchungstext" msgctxt "view:cashbook.planner:" -msgid "Available placeholders: ${date} ${month} ${year} ${amount} ${quantity}" -msgstr "verfügbare Platzhalter: ${date} ${month} ${year} ${amount} ${quantity}" +msgid "Available placeholders: ${date} ${month} ${year} ${amount} ${quantity} ${rate}" +msgstr "verfügbare Platzhalter: ${date} ${month} ${year} ${amount} ${quantity} ${rate}" msgctxt "view:cashbook.planner:" msgid "Cashbook lines" @@ -204,7 +228,7 @@ msgstr "Tag des Monats" msgctxt "help:cashbook.planner,monthday:" msgid "If you want the rule to run on a specific day of the month, select the day here." -msgstr "Wenn die Regel an einem bestimmten Tag im Monat ausgeführt soll, wählen Siehier den Tag." +msgstr "Wenn die Regel an einem bestimmten Tag im Monat ausgeführt soll, wählen Sie hier den Tag." msgctxt "field:cashbook.planner,interval:" msgid "Interval" @@ -234,7 +258,7 @@ msgctxt "field:cashbook.planner,nextrun:" msgid "Next Execution Date" msgstr "Nächster Ausführungstermin" -msgctxt "field:cashbook.planner,nextrun_link:" +msgctxt "field:cashbook.planner,nextrun_date:" msgid "Next Execution Date" msgstr "Nächster Ausführungstermin" @@ -318,6 +342,30 @@ msgctxt "help:cashbook.planner,cashbook_lines:" msgid "This cash book lines was generated by the current scheduled booking." msgstr "Diese Kassenbuchzeilen wurden durch die aktuelle geplante Buchung generiert." +msgctxt "field:cashbook.planner,party:" +msgid "Party" +msgstr "Partei" + +msgctxt "field:cashbook.planner,move_event:" +msgid "If no business day" +msgstr "Wenn kein Werktag" + +msgctxt "selection:cashbook.planner,move_event:" +msgid "unchanged" +msgstr "unverändert" + +msgctxt "selection:cashbook.planner,move_event:" +msgid "Business day before original date" +msgstr "Geschäftstag vor ursprünglichem Datum" + +msgctxt "selection:cashbook.planner,move_event:" +msgid "Business day after original date" +msgstr "Geschäftstag nach ursprünglichem Datum" + +msgctxt "help:cashbook.planner,move_event:" +msgid "If the date of execution falls on a weekend or holiday, it can be moved to a business day." +msgstr "Wenn das Datum der Ausführung auf ein Wochenende oder Feiertag fällt, kann es auf einen Geschäftstag verschoben werden." + ############################ # cashbook.planner.nextrun # diff --git a/locale/en.po b/locale/en.po index a556b20..cb63fac 100644 --- a/locale/en.po +++ b/locale/en.po @@ -58,6 +58,26 @@ msgctxt "model:ir.rule.group,name:rg_nextrun_companies" msgid "User in companies" msgstr "User in companies" +msgctxt "view:cashbook.configuration:" +msgid "Scheduled Bookings" +msgstr "Scheduled Bookings" + +msgctxt "field:cashbook.configuration,holidays:" +msgid "Holidays" +msgstr "Holidays" + +msgctxt "help:cashbook.configuration,holidays:" +msgid "Semicolon separate list of dates: yyyy-mm-dd = single date, mm-dd = annual repetition, easter[greg|jul|orth] = Easter Sunday, ascension = Ascension Day, whitsun = Whitsunday, offset with :+/-n e.g.: easter:+1 = Easter Monday" +msgstr "Semicolon separate list of dates: yyyy-mm-dd = single date, mm-dd = annual repetition, easter[greg|jul|orth] = Easter Sunday, ascension = Ascension Day, whitsun = Whitsunday, offset with :+/-n e.g.: easter:+1 = Easter Monday" + +msgctxt "field:cashbook.configuration,holidays_info:" +msgid "Holidays" +msgstr "Holidays" + +msgctxt "help:cashbook.configuration,holidays_info:" +msgid "Holidays in the current year." +msgstr "Holidays in the current year." + msgctxt "model:cashbook.planner,name:" msgid "Scheduled Booking" msgstr "Scheduled Booking" @@ -83,8 +103,8 @@ msgid "Booking text" msgstr "Booking text" msgctxt "view:cashbook.planner:" -msgid "Available placeholders: ${date} ${month} ${year} ${amount} ${quantity}" -msgstr "Available placeholders: ${date} ${month} ${year} ${amount} ${quantity}" +msgid "Available placeholders: ${date} ${month} ${year} ${amount} ${quantity} ${rate}" +msgstr "Available placeholders: ${date} ${month} ${year} ${amount} ${quantity} ${rate}" msgctxt "view:cashbook.planner:" msgid "Cashbook lines" @@ -210,7 +230,7 @@ msgctxt "field:cashbook.planner,nextrun:" msgid "Next Execution Date" msgstr "Next Execution Date" -msgctxt "field:cashbook.planner,nextrun_link:" +msgctxt "field:cashbook.planner,nextrun_date:" msgid "Next Execution Date" msgstr "Next Execution Date" @@ -294,6 +314,30 @@ msgctxt "help:cashbook.planner,cashbook_lines:" msgid "This cash book lines was generated by the current scheduled booking." msgstr "This cash book lines was generated by the current scheduled booking." +msgctxt "field:cashbook.planner,party:" +msgid "Party" +msgstr "Party" + +msgctxt "field:cashbook.planner,move_event:" +msgid "If no business day" +msgstr "If no business day" + +msgctxt "selection:cashbook.planner,move_event:" +msgid "unchanged" +msgstr "unchanged" + +msgctxt "selection:cashbook.planner,move_event:" +msgid "Business day before original date" +msgstr "Business day before original date" + +msgctxt "selection:cashbook.planner,move_event:" +msgid "Business day after original date" +msgstr "Business day after original date" + +msgctxt "help:cashbook.planner,move_event:" +msgid "If the date of execution falls on a weekend or holiday, it can be moved to a business day." +msgstr "If the date of execution falls on a weekend or holiday, it can be moved to a business day." + msgctxt "model:cashbook.planner.nextrun,name:" msgid "Next Execution Date" msgstr "Next Execution Date" diff --git a/planner.py b/planner.py index eb07b1e..547de8e 100644 --- a/planner.py +++ b/planner.py @@ -34,6 +34,11 @@ SEL_WEEKDAY = [ ('0', 'Monday'), ('1', 'Tuesday'), ('2', 'Wednesday'), ('3', 'Thursday'), ('4', 'Friday'), ('5', 'Saturday'), ('6', 'Sunday')] +SEL_MOVE_EVENT = [ + ('nop', 'unchanged'), + ('before', 'Business day before original date'), + ('after', 'Business day after original date') +] class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): @@ -98,10 +103,13 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): nextrun = fields.One2Many( string='Next Execution Date', size=1, field='planner', model_name='cashbook.planner.nextrun') - nextrun_link = fields.Function(fields.Many2One( - string='Next Execution Date', readonly=True, - model_name='cashbook.planner.nextrun'), - 'on_change_with_nextrun_link') + nextrun_date = fields.Function(fields.Date( + string='Next Execution Date', readonly=True), + 'on_change_with_nextrun_date', searcher='search_nextrun_date') + move_event = fields.Selection( + string='If no business day', required=True, selection=SEL_MOVE_EVENT, + help='If the date of execution falls on a weekend or holiday, ' + + 'it can be moved to a business day.') bookingtype = fields.Selection( string='Type', selection=sel_bookingtype, required=True, @@ -159,6 +167,8 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): @classmethod def __setup__(cls): super(ScheduledBooking, cls).__setup__() + cls._order.insert(0, ('name', 'ASC')) + cls._order.insert(0, ('nextrun_date', 'ASC')) t = cls.__table__() cls._sql_indexes.update({ Index( @@ -189,7 +199,8 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): self.booktransf.name if self.booktransf else self.category.rec_name if self.category else '-', - self.nextrun_link.rec_name if self.nextrun_link else '-', + Report.format_date(self.nextrun_date, lang=None) + if self.nextrun_date else '-', Report.format_currency( self.amount, lang=None, currency=self.cashbook.currency) ]) @@ -206,11 +217,42 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): used instead of the stored values, Defaults to {}, allowed: frequ, weekday, start_date, end_date (preferred over 'count'), - monthday, interval, setpos + monthday, interval, setpos, move_event Returns: list: date values, result of rrlue """ + def get_moved_date(xdate, m_mode): + """ re-arrange xdate to a working day + + Args: + xdate (date): date to move to a working day + move_mode (str): move mode: + nop - no operation + after/before - move date to after/before input date + + Returns: + date: re-arranged date + """ + Config = Pool().get('cashbook.configuration') + config = Config.get_singleton() + + assert m_mode in ['nop', 'after', 'before'], 'invalid move_mode' + + if (not config) or (m_mode == 'nop'): + return xdate + + holidays = config.holiday_dates([xdate.year, xdate.year + 1]) + day_cnt = ( + 1 if m_mode == 'after' + else -1 if m_mode == 'before' else 0) + + if day_cnt != 0: + while (xdate in holidays) or (xdate.weekday() in [5, 6]): + # re-arrange + xdate = xdate + timedelta(days=day_cnt) + return xdate + pfrequ = { 'year': YEARLY, 'month': MONTHLY, 'week': WEEKLY, 'day': DAILY} pweekday = { @@ -224,6 +266,10 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): end_date = params.get('end_date', self.end_date) frequ = pfrequ[params.get('frequ', self.frequ)] + move_event = params.get('move_event', self.move_event) + if move_event not in ['nop', 'before', 'after']: + move_event = 'nop' + setpos = params.get('setpos', self.setpos) if setpos is not None: setpos = 1 if setpos < 1 else 4 if setpos > 4 else setpos @@ -251,7 +297,12 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): for x in dtrule: if (query_date and (x.date() >= query_date)) or \ (query_date is None): - result.append(x.date()) + x_date = get_moved_date(x.date(), move_event) + + # if date was re-arranged backwards and we are before + # query_date - skip it + if x_date >= query_date: + result.append(x_date) if len(result) >= count: break return result @@ -302,22 +353,22 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): return self.cashbook.currency.id @fields.depends('nextrun') - def on_change_with_nextrun_link(self, name=None): + def on_change_with_nextrun_date(self, name=None): """ get nextrun-record if exist Args: name (str, optional): field name. Defaults to None. Returns: - int: id of nextrun-record or None + date: date of nextrun or None """ if self.nextrun: - return self.nextrun[0].id + return self.nextrun[0].date return None @fields.depends( 'start_date', 'end_date', 'frequ', 'weekday', 'monthday', - 'interval', 'setpos') + 'interval', 'setpos', 'move_event') def on_change_with_nextdates(self, name=None): """ Calculates the next 5 appointments based on the configured rule, returns a formatted date list @@ -402,6 +453,54 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): """ self.on_change_frequ() + @staticmethod + def order_nextrun_date(tables): + """ get query to sort by date of next execution + + Args: + tables (list): tables + + Returns: + list of query: sort-query + """ + pool = Pool() + Nextrun = pool.get('cashbook.planner.nextrun') + Planner2 = pool.get('cashbook.planner') + tab_nxrun = Nextrun.__table__() + tab_plan = Planner2.__table__() + table, _ = tables[None] + + query = tab_plan.join( + tab_nxrun, + condition=tab_nxrun.planner == tab_plan.id + ).select( + tab_nxrun.date, + where=tab_plan.id == table.id) + return [query] + + @classmethod + def search_nextrun_date(cls, name, clause): + """ get query for search on 'nextrun_date' + + Args: + name (str): name of field to search on + clause (dict): search clause + + Returns: + list of dict: search clause + """ + return [('nextrun.date',) + tuple(clause[1:])] + + @classmethod + def default_move_event(cls): + """ 'no operation' as default for + business-day occurence + + Returns: + str: nop + """ + return 'nop' + @classmethod def default_wfcheck(cls): """ False as default for wf-state 'checked' @@ -515,6 +614,15 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): quantity_txt = Report.format_number( quantity, lang=None, digits=uom_digits) + asset_rate = '-' + if quantity and amount is not None: + asset_rate = '%(rate)s %(currency)s/%(uom)s' % { + 'rate': Report.format_number( + amount / quantity, lang=None, + digits=to_book.currency.digits), + 'currency': to_book.currency.symbol, + 'uom': to_book.quantity_uom.symbol} + return Template(linedata.get('description')).safe_substitute({ 'date': Report.format_date(line_date, lang=None), 'month': line_date.month, @@ -522,7 +630,8 @@ class ScheduledBooking(DeactivableMixin, ModelSQL, ModelView): 'amount': Report.format_currency( amount, lang=None, currency=from_book.currency) if (amount is not None) and from_book else '-', - 'quantity': quantity_txt}) + 'quantity': quantity_txt, + 'rate': asset_rate}) @classmethod def update_next_occurence(cls, records, query_date=None): diff --git a/tests/planner.py b/tests/planner.py index 9eaa7b2..2bd39e0 100644 --- a/tests/planner.py +++ b/tests/planner.py @@ -60,6 +60,7 @@ class PlannerTestCase(object): self.assertEqual(job.monthday, 1) self.assertEqual(job.interval, 1) self.assertEqual(job.setpos, None) + self.assertEqual(job.move_event, 'nop') self.assertEqual( job.nextdates, '05/01/2022 | 06/01/2022 | 07/01/2022 | 08/01/2022 |' + @@ -115,11 +116,63 @@ class PlannerTestCase(object): 'Depot | 0.00 usd | Open | 0.0000 u') return book + @with_transaction() + def test_func_holiday_parseconfig(self): + """ check function holiday_parseconfig() + """ + Config = Pool().get('cashbook.configuration') + + company = self.prep_company() + with Transaction().set_context({'company': company.id}): + # check valid data + result = Config.holiday_parseconfig( + '2022-05-01;12-25;12-25:+1;easter;easter:-2;easterjul;' + + 'ascension;whitsun;whitsun:+1;', + [2022, 2023, 2024]) + self.assertEqual(result, { + 'definition': '2022-05-01;12-25;12-25:+1;easter;easter:-2;' + + 'easterjul;ascension;whitsun;whitsun:+1', + 'dates': [ + date(2022, 5, 1), date(2022, 12, 25), date(2023, 12, 25), + date(2024, 12, 25), date(2022, 12, 26), date(2023, 12, 26), + date(2024, 12, 26), date(2022, 4, 17), date(2023, 4, 9), + date(2024, 3, 31), date(2022, 4, 15), date(2023, 4, 7), + date(2024, 3, 29), date(2022, 4, 11), date(2023, 4, 3), + date(2024, 4, 22), date(2022, 5, 26), date(2023, 5, 18), + date(2024, 5, 9), date(2022, 6, 5), date(2023, 5, 28), + date(2024, 5, 19), date(2022, 6, 6), date(2023, 5, 29), + date(2024, 5, 20)]}) + + # check invalid data + self.assertEqual( + Config.holiday_parseconfig('not-a-value;'), + {'definition': '', 'dates': []}) + + # check no data + self.assertEqual( + Config.holiday_parseconfig(''), + {'definition': '', 'dates': []}) + self.assertEqual( + Config.holiday_parseconfig(None), + {'definition': '', 'dates': []}) + + with Transaction().set_context({'holiday_years': [2022]}): + cfg1 = Config(holidays='2022-05-01;easter;whitsun') + cfg1.save() + self.assertEqual( + cfg1.holiday_dates([2022]), + [date(2022, 5, 1), date(2022, 4, 17), date(2022, 6, 5)]) + self.assertEqual( + cfg1.holidays_info, + '04/17/2022|05/01/2022|06/05/2022') + @with_transaction() def test_planner_create_job(self): """ create job, check rule + constraints """ - Planner = Pool().get('cashbook.planner') + pool = Pool() + Planner = pool.get('cashbook.planner') + Config = pool.get('cashbook.configuration') job = self.prep_create_job() self.assertEqual( @@ -192,6 +245,53 @@ class PlannerTestCase(object): [date(2022, 5, 11), date(2022, 6, 8), date(2022, 7, 13), date(2022, 8, 10), date(2022, 9, 14), date(2022, 10, 12)]) + # set up holidays + cfg1 = Config( + holidays='01-01;05-01;easter:+1;easter:-2;ascension;whitsun:+1') + cfg1.save() + # 1st of may, should be moved to 2nd of may + self.assertEqual( + job._compute_dates_by_rrule( + query_date=date(2022, 4, 25), count=3, + params={ + 'end_date': None, 'start_date': date(2022, 5, 1), + 'move_event': 'after', 'weekday': None, + 'setpos': None, 'interval': 1, 'frequ': 'year', + 'monthday': None}), + [date(2022, 5, 2), date(2023, 5, 2), date(2024, 5, 2)]) + # easter of 2022, occurence-date moved to tuesday after easter'22 + self.assertEqual( + job._compute_dates_by_rrule( + query_date=date(2022, 4, 10), count=3, + params={ + 'end_date': None, 'start_date': date(2022, 4, 17), + 'move_event': 'after', 'weekday': None, + 'setpos': None, + 'interval': 1, 'frequ': 'month', 'monthday': None}), + [date(2022, 4, 19), date(2022, 5, 17), date(2022, 6, 17)]) + # easter of 2022, monthly, occurence-date moved to + # thursday before easter'22 + self.assertEqual( + job._compute_dates_by_rrule( + query_date=date(2022, 4, 10), count=3, + params={ + 'end_date': None, 'start_date': date(2022, 4, 17), + 'move_event': 'before', 'weekday': None, + 'setpos': None, + 'interval': 1, 'frequ': 'month', 'monthday': None}), + [date(2022, 4, 14), date(2022, 5, 17), date(2022, 6, 17)]) + # easter of 2022, monthly, check next occurence after easter + # recompute date at moved occurence-date+1 + self.assertEqual( + job._compute_dates_by_rrule( + query_date=date(2022, 4, 15), count=3, + params={ + 'end_date': None, 'start_date': date(2022, 4, 17), + 'move_event': 'before', 'weekday': None, + 'setpos': None, + 'interval': 1, 'frequ': 'month', 'monthday': None}), + [date(2022, 5, 17), date(2022, 6, 17), date(2022, 7, 15)]) + Planner.write(*[[job], { 'frequ': 'year', 'start_date': date(2022, 5, 1), 'setpos': None, 'monthday': None, 'interval': 1, @@ -292,6 +392,20 @@ class PlannerTestCase(object): Planner.update_next_occurence([job], query_date=date(2022, 5, 25)) self.assertEqual(len(job.nextrun), 1) self.assertEqual(job.nextrun[0].date, date(2022, 6, 1)) + self.assertEqual(job.nextrun_date, date(2022, 6, 1)) + + # check searcher + order + self.assertEqual( + Planner.search( + [('nextrun_date', '=', date(2022, 6, 1))], + order=[('nextrun_date', 'ASC')]), + [job]) + self.assertEqual( + Planner.search_count([('nextrun_date', '=', date(2022, 6, 1))]), + 1) + self.assertEqual( + Planner.search_count([('nextrun_date', '=', date(2022, 6, 2))]), + 0) Planner.update_next_occurence([job], query_date=date(2022, 5, 30)) self.assertEqual(len(job.nextrun), 1) @@ -364,12 +478,13 @@ class PlannerTestCase(object): asset_book = self.prep_planner_asset_book() self.assertEqual(Planner.fill_placeholder({ 'date': date(2022, 5, 2), - 'amount': Decimal('12.4567'), + 'amount': Decimal('126.4567'), 'quantity': Decimal('32.4423'), 'cashbook': asset_book.id, + 'booktransf': asset_book.id, 'description': '- ${amount} - ${date} - ${month} - ' + - '${year} - ${quantity}'}), - '- usd12.46 - 05/02/2022 - 5 - 2022 - 32.4423\xa0u') + '${year} - ${quantity} - ${rate}'}), + '- usd126.46 - 05/02/2022 - 5 - 2022 - 32.4423\xa0u - 3.90 usd/u') @with_transaction() def test_planner_cronjobs_booking_with_category(self): diff --git a/tryton.cfg b/tryton.cfg index e414f70..2eb87a2 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -1,5 +1,5 @@ [tryton] -version=7.0.0 +version=7.0.5 depends: cashbook extras_depend: @@ -8,6 +8,7 @@ xml: group.xml planner.xml nextrun.xml + config.xml cashbook.xml cron.xml menu.xml diff --git a/view/configuration_form.xml b/view/configuration_form.xml new file mode 100644 index 0000000..fca0b6d --- /dev/null +++ b/view/configuration_form.xml @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/view/planner_form.xml b/view/planner_form.xml index 95e04e8..86e826f 100644 --- a/view/planner_form.xml +++ b/view/planner_form.xml @@ -12,8 +12,8 @@ full copyright notices and license terms. -->