from .dates_add_days import add_days from .dates_add_months import add_months from .dates_month_boundaries import month_boundaries from .finance_payment_terms_due_date_types import PaymentTerms def _is_int(value: object) -> bool: return isinstance(value, int) and not isinstance(value, bool) def _due_date_only(invoice_date: str, terms: PaymentTerms) -> str: n = terms.n if terms.kind in ("net", "end-of-month", "days-after-month-end"): if not _is_int(n) or n < 0: raise ValueError("payment terms n must be 0 or more, received %s" % (n,)) if terms.kind == "net": return add_days(invoice_date, n) if terms.kind == "end-of-month": # Move to the target month first, then take its end: the day of the # month is irrelevant, so add_months' clamping cannot shift the answer. return month_boundaries(add_months(invoice_date, n)).end return add_days(month_boundaries(invoice_date).end, n) if terms.kind == "day-of-next-month": if not _is_int(n) or n < 1 or n > 31: raise ValueError("day-of-next-month needs a day from 1 to 31, received %s" % (n,)) nxt = month_boundaries(add_months(month_boundaries(invoice_date).start, 1)) return add_days(nxt.start, min(n, nxt.days) - 1) raise ValueError('unknown payment terms kind "%s"' % (terms.kind,)) def payment_due_date(invoice_date: str, terms: PaymentTerms) -> str: """The date an invoice falls due under its payment terms. Net N is N calendar days, never "one month": 31 January net 30 is 2 March in 2026. The discount fields do not move the due date, but they are checked here so that terms whose discount outlasts the due date never get further. """ due = _due_date_only(invoice_date, terms) bp = terms.discount_basis_points if not _is_int(bp) or bp < 0 or bp > 10000: raise ValueError("discount must be 0 to 10000 basis points, received %s" % (bp,)) days = terms.discount_days if not _is_int(days) or days < 0: raise ValueError("discount days must be 0 or more, received %s" % (days,)) if bp > 0 and add_days(invoice_date, days) > due: raise ValueError("the discount period ends after the due date %s" % (due,)) return due