dodil/erp-brick-gl
DODIL ERP brick: general ledger
src/brick_gl/domain/schedule.py
"""Schedules that must sum EXACTLY: an accrual or deferral released over the months it covers, and the months a recurring
entry is due. The last period takes the rounding remainder, so the total released equals the registration to the penny."""
import calendar
from decimal import Decimal
from brick_gl.domain import dates
from brick_gl.domain.money import largest_remainder, q2
def periods_between(first: str, last: str) -> list:
out, p = [], first
while p <= last:
out.append(p)
p = dates.next_period(p)
if len(out) > 1200:
raise ValueError("a schedule runs for at most 100 years")
return out
def release_schedule(amount: Decimal, first: str, last: str, method: str) -> dict:
"""{period_id: release}. Straight line: amount / months, each rounded to the cent, the LAST period taking the remainder.
Daily: in proportion to the days each month holds (largest remainder), so a part month carries its part."""
periods = periods_between(first, last)
n = len(periods)
if method == "daily":
days = [calendar.monthrange(int(p[:4]), int(p[5:]))[1] for p in periods]
shares = largest_remainder(q2(amount), [Decimal(d) for d in days])
return dict(zip(periods, shares))
each = (q2(amount) / n).quantize(Decimal("0.01"), rounding="ROUND_HALF_UP") if n else Decimal(0)
out = {p: each for p in periods[:-1]}
out[periods[-1]] = q2(amount) - sum(out.values(), Decimal(0))
return out
def recurring_due(frequency: str, first_period: str, last_period, period: str) -> bool:
"""Is a template due in a period: monthly every month, quarterly every third from its first, yearly every twelfth."""
if period < first_period or (last_period and period > last_period):
return False
offset = (int(period[:4]) - int(first_period[:4])) * 12 + int(period[5:]) - int(first_period[5:])
return offset % {"monthly": 1, "quarterly": 3, "yearly": 12}[frequency] == 0
def posting_date(period: str, posting_day) -> object:
start, end = dates.period_bounds(period)
if posting_day in (None, "last", ""):
return end
return start.replace(day=min(int(posting_day), end.day))