(More or less) proper definition of "months", added more receipt preferences.

This commit is contained in:
s3lph 2018-09-08 20:44:40 +02:00
parent 1c5b442fea
commit 79e6f83c72
5 changed files with 126 additions and 13 deletions

View file

@ -577,6 +577,8 @@ class MatematDatabase(object):
user.balance = old_balance + amount user.balance = old_balance + amount
def check_receipt_due(self, user: User) -> bool: def check_receipt_due(self, user: User) -> bool:
if not isinstance(user.receipt_pref, ReceiptPreference):
raise TypeError()
if user.receipt_pref == ReceiptPreference.NONE or user.email is None: if user.receipt_pref == ReceiptPreference.NONE or user.email is None:
return False return False
with self.db.transaction() as c: with self.db.transaction() as c:
@ -587,14 +589,10 @@ class MatematDatabase(object):
ON r.user_id = u.user_id ON r.user_id = u.user_id
WHERE u.user_id = :user_id WHERE u.user_id = :user_id
''', [user.id]) ''', [user.id])
last_receipt: int = c.fetchone()[0] last_receipt: datetime = datetime.fromtimestamp(c.fetchone()[0])
if user.receipt_pref == ReceiptPreference.MONTHLY: next_receipt_due: datetime = user.receipt_pref.next_receipt_due(last_receipt)
date_diff: int = timedelta(days=31).total_seconds()
elif user.receipt_pref == ReceiptPreference.YEARLY: return datetime.utcnow() > next_receipt_due
date_diff = timedelta(days=365).total_seconds()
else:
raise ValueError()
return datetime.utcnow().timestamp() > last_receipt + date_diff
def create_receipt(self, user: User, write: bool = False) -> Receipt: def create_receipt(self, user: User, write: bool = False) -> Receipt:
transactions: List[Transaction] = [] transactions: List[Transaction] = []

View file

@ -1,5 +1,10 @@
from typing import Callable
from enum import Enum from enum import Enum
from datetime import datetime, timedelta
from matemat.util.monthdelta import add_months
class ReceiptPreference(Enum): class ReceiptPreference(Enum):
""" """
@ -10,8 +15,10 @@ class ReceiptPreference(Enum):
e = object.__new__(cls) e = object.__new__(cls)
# The enum's internal value # The enum's internal value
e._value_: int = args[0] e._value_: int = args[0]
# The function calculating the date after which a new receipt is due.
e._datefunc: Callable[[datetime], datetime] = args[1]
# The human-readable description # The human-readable description
e._human_readable: str = args[1] e._human_readable: str = args[2]
return e return e
@property @property
@ -21,17 +28,35 @@ class ReceiptPreference(Enum):
""" """
return self._human_readable return self._human_readable
def next_receipt_due(self, d: datetime) -> datetime:
return self._datefunc(d)
""" """
No receipts should be generated. No receipts should be generated.
""" """
NONE = 0, 'No receipts' NONE = 0, (lambda d: None), 'No receipts'
"""
A receipt should be generated once a week.
"""
WEEKLY = 1, (lambda d: d + timedelta(weeks=1)), 'Weekly'
""" """
A receipt should be generated once a month. A receipt should be generated once a month.
""" """
MONTHLY = 2, 'Aggregated, monthly' MONTHLY = 2, (lambda d: add_months(d, 1)), 'Monthly'
"""
A receipt should be generated once every three month.
"""
QUARTERLY = 3, (lambda d: add_months(d, 3)), 'Quarterly'
"""
A receipt should be generated once every six month.
"""
BIANNUALLY = 4, (lambda d: add_months(d, 6)), 'Biannually'
""" """
A receipt should be generated once a year. A receipt should be generated once a year.
""" """
YEARLY = 3, 'Aggregated, yearly' YEARLY = 5, (lambda d: add_months(d, 12)), 'Annually'

View file

@ -486,7 +486,7 @@ class DatabaseTest(unittest.TestCase):
self.assertFalse(self.db.check_receipt_due(user4)) self.assertFalse(self.db.check_receipt_due(user4))
self.assertTrue(self.db.check_receipt_due(user5)) self.assertTrue(self.db.check_receipt_due(user5))
self.assertFalse(self.db.check_receipt_due(user6)) self.assertFalse(self.db.check_receipt_due(user6))
with self.assertRaises(ValueError): with self.assertRaises(TypeError):
self.db.check_receipt_due(user7) self.db.check_receipt_due(user7)
def test_create_receipt(self): def test_create_receipt(self):

View file

@ -0,0 +1,29 @@
from typing import Tuple
from datetime import datetime, timedelta
import calendar
def add_months(d: datetime, months: int) -> datetime:
"""
Add the given number of months to the passed date, considering the varying numbers of days in a month.
:param d: The date time to add to.
:param months: The number of months to add to.
:return: A datetime object offset by the requested number of months.
"""
if not isinstance(d, datetime) or not isinstance(months, int):
raise TypeError()
if months < 0:
raise ValueError('Can only add a positive number of months.')
nextmonth: Tuple[int, int] = (d.year, d.month)
days: int = 0
# Iterate the months between the passed date and the target month
for i in range(months):
days += calendar.monthlen(*nextmonth)
nextmonth = calendar.nextmonth(*nextmonth)
# Set the day of month temporarily to 1, then add the day offset to reach the 1st of the target month
newdate: datetime = d.replace(day=1) + timedelta(days=days)
# Re-set the day of month to the intended value, but capped by the max. day in the target month
newdate = newdate.replace(day=min(d.day, calendar.monthlen(newdate.year, newdate.month)))
return newdate

View file

@ -0,0 +1,61 @@
import unittest
from datetime import datetime
from matemat.util.monthdelta import add_months
class TestMonthDelta(unittest.TestCase):
def test_monthdelta_zero(self):
date = datetime(2018, 9, 8, 13, 37, 42)
offset_date = date
self.assertEqual(offset_date, add_months(date, 0))
def test_monthdelta_one(self):
date = datetime(2018, 9, 8, 13, 37, 42)
offset_date = date.replace(month=10)
self.assertEqual(offset_date, add_months(date, 1))
def test_monthdelta_two(self):
date = datetime(2018, 9, 8, 13, 37, 42)
offset_date = date.replace(month=11)
self.assertEqual(offset_date, add_months(date, 2))
def test_monthdelta_yearwrap(self):
date = datetime(2018, 9, 8, 13, 37, 42)
offset_date = date.replace(year=2019, month=1)
self.assertEqual(offset_date, add_months(date, 4))
def test_monthdelta_yearwrap_five(self):
date = datetime(2018, 9, 8, 13, 37, 42)
offset_date = date.replace(year=2023, month=3)
self.assertEqual(offset_date, add_months(date, 54))
def test_monthdelta_rounddown_31_30(self):
date = datetime(2018, 3, 31, 13, 37, 42)
offset_date = date.replace(month=4, day=30)
self.assertEqual(offset_date, add_months(date, 1))
def test_monthdelta_rounddown_feb(self):
date = datetime(2018, 1, 31, 13, 37, 42)
offset_date = date.replace(month=2, day=28)
self.assertEqual(offset_date, add_months(date, 1))
def test_monthdelta_rounddown_feb_leap(self):
date = datetime(2020, 1, 31, 13, 37, 42)
offset_date = date.replace(month=2, day=29)
self.assertEqual(offset_date, add_months(date, 1))
def test_fail_negative(self):
date = datetime(2020, 1, 31, 13, 37, 42)
with self.assertRaises(ValueError):
add_months(date, -1)
def test_fail_type(self):
date = datetime(2020, 1, 31, 13, 37, 42)
with self.assertRaises(TypeError):
add_months(date, 1.2)
with self.assertRaises(TypeError):
add_months(42, 1)