config: compute holidays + test
This commit is contained in:
parent
3c65390284
commit
b54a8d678f
4 changed files with 188 additions and 8 deletions
142
config.py
142
config.py
|
@ -4,14 +4,17 @@
|
||||||
# full copyright notices and license terms.
|
# 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.pool import PoolMeta, Pool
|
||||||
from trytond.model import fields
|
from trytond.model import fields
|
||||||
|
|
||||||
holidays = fields.Char(
|
holidays = fields.Char(
|
||||||
string='Holidays', help='Semicolon separate list of dates:' +
|
string='Holidays', help='Semicolon separate list of dates: ' +
|
||||||
' yyyy-mm-dd = single date, mm-dd = annual repetition, ' +
|
'yyyy-mm-dd = single date, mm-dd = annual repetition, ' +
|
||||||
'easter = Easter Sunday, ascension = Ascension Day, offset ' +
|
'easter[greg|jul|orth] = Easter Sunday, ascension = Ascension Day, ' +
|
||||||
'with +/-n e.g.: easter+1 = Easter Monday')
|
'whitsun = Whitsunday, offset with :+/-n e.g.: easter:+1 = Easter Monday')
|
||||||
|
|
||||||
|
|
||||||
class Configuration(metaclass=PoolMeta):
|
class Configuration(metaclass=PoolMeta):
|
||||||
|
@ -19,6 +22,137 @@ class Configuration(metaclass=PoolMeta):
|
||||||
|
|
||||||
holidays = fields.MultiValue(holidays)
|
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
|
@classmethod
|
||||||
def multivalue_model(cls, field):
|
def multivalue_model(cls, field):
|
||||||
""" get model for value
|
""" get model for value
|
||||||
|
|
|
@ -91,8 +91,8 @@ msgid "Holidays"
|
||||||
msgstr "Feiertage"
|
msgstr "Feiertage"
|
||||||
|
|
||||||
msgctxt "help:cashbook.configuration,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"
|
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 = Ostersonntag, ascension = Christi Himmelfahrt, Offset mit +/-n z.B: easter+1 = Ostermontag"
|
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"
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -67,8 +67,8 @@ msgid "Holidays"
|
||||||
msgstr "Holidays"
|
msgstr "Holidays"
|
||||||
|
|
||||||
msgctxt "help:cashbook.configuration,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"
|
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 = 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[greg|jul|orth] = Easter Sunday, ascension = Ascension Day, whitsun = Whitsunday, offset with :+/-n e.g.: easter:+1 = Easter Monday"
|
||||||
|
|
||||||
msgctxt "model:cashbook.planner,name:"
|
msgctxt "model:cashbook.planner,name:"
|
||||||
msgid "Scheduled Booking"
|
msgid "Scheduled Booking"
|
||||||
|
|
|
@ -116,6 +116,52 @@ class PlannerTestCase(object):
|
||||||
'Depot | 0.00 usd | Open | 0.0000 u')
|
'Depot | 0.00 usd | Open | 0.0000 u')
|
||||||
return book
|
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()
|
@with_transaction()
|
||||||
def test_planner_create_job(self):
|
def test_planner_create_job(self):
|
||||||
""" create job, check rule + constraints
|
""" create job, check rule + constraints
|
||||||
|
|
Loading…
Reference in a new issue