dodil/erp-brick-ar
DODIL ERP brick: accounts receivable
src/brick_ar/domain/deferral.py
"""Deferred revenue (AR-46): a service sold ahead is not earned on the day it is invoiced. It is posted to deferred revenue at issue and
released to revenue by the DAYS of each calendar month in the service period; the last month takes the remainder, so a line releases
exactly its net. A credit note takes back the unreleased part first."""
from datetime import date, timedelta
from decimal import Decimal
from brick_ar.domain.dates import end_of_month
from brick_ar.domain.money import ZERO, money
def period_id(d: date) -> str:
return f"{d.year:04d}-{d.month:02d}"
def bounds(pid: str) -> tuple:
y, m = pid.split("-")
first = date(int(y), int(m), 1)
return first, end_of_month(first)
def is_deferred(line, issue_date: date, on: bool = True) -> bool:
"""A line with a service period that runs past the month it was invoiced in is earned later; anything else is revenue at issue."""
if not on or not line.service_from or not line.service_to or line.deducts_invoice_id:
return False
return line.service_to > end_of_month(issue_date)
def months(line, issue_date: date) -> list:
"""The calendar months of the service, from the service start to its end."""
d, out = line.service_from.replace(day=1), []
while d <= line.service_to:
out.append(period_id(d))
d = end_of_month(d) + timedelta(days=1)
return out
def release_amount(*, net: Decimal, released: Decimal, reversed_: Decimal, service_from: date, service_to: date, last_day: date) -> Decimal:
"""What to release for the month ending `last_day`: the days of the service through that day over its total, of what is still
deferred (net less the part a credit note took back), less what was already released; the final month takes the remainder."""
remaining = net - released - reversed_
if remaining <= 0 or last_day < service_from:
return ZERO
if last_day >= service_to:
return remaining
total_days = (service_to - service_from).days + 1
through = (min(last_day, service_to) - service_from).days + 1
target = money((net - reversed_) * through / total_days)
return min(max(target - released, ZERO), remaining)