config: compute holidays + test

This commit is contained in:
Frederik Jaeckel 2024-06-01 23:37:06 +02:00
parent 3c65390284
commit b54a8d678f
4 changed files with 188 additions and 8 deletions

142
config.py
View file

@ -4,14 +4,17 @@
# 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
holidays = fields.Char(
string='Holidays', help='Semicolon separate list of dates:' +
' yyyy-mm-dd = single date, mm-dd = annual repetition, ' +
'easter = Easter Sunday, ascension = Ascension Day, offset ' +
'with +/-n e.g.: easter+1 = Easter Monday')
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):
@ -19,6 +22,137 @@ class Configuration(metaclass=PoolMeta):
holidays = fields.MultiValue(holidays)
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': '<parsed definition string>',
'dates': [<requested 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

View file

@ -91,8 +91,8 @@ 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 = Easter Sunday, ascension = Ascension Day, 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 = Ostersonntag, ascension = Christi Himmelfahrt, Offset mit +/-n z.B: easter+1 = Ostermontag"
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"
####################

View file

@ -67,8 +67,8 @@ 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 = Easter Sunday, ascension = Ascension Day, 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 = Easter Sunday, ascension = Ascension Day, offset with +/-n e.g.: easter+1 = Easter Monday"
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 "model:cashbook.planner,name:"
msgid "Scheduled Booking"

View file

@ -116,6 +116,52 @@ 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': []})
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)])
@with_transaction()
def test_planner_create_job(self):
""" create job, check rule + constraints