config: compute holidays + test
This commit is contained in:
parent
3c65390284
commit
b54a8d678f
4 changed files with 188 additions and 8 deletions
138
config.py
138
config.py
|
@ -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')
|
||||
'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
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
||||
####################
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue