New 'Earnings' interface for viewing/tracking earnings across reporting periods (e.g for BAS)
This commit is contained in:
parent
3361537a59
commit
ebdf29cdbd
10 changed files with 1561 additions and 4 deletions
173
tests/test_earnings.py
Normal file
173
tests/test_earnings.py
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
from bouquin.earnings import (
|
||||
aggregate_invoices_by_month,
|
||||
aggregate_payments_by_month,
|
||||
invoice_reporting_amount_cents,
|
||||
)
|
||||
|
||||
|
||||
def _invoice(
|
||||
db,
|
||||
project_id,
|
||||
number,
|
||||
currency="AUD",
|
||||
tax_rate=10.0,
|
||||
issue_date="2026-04-01",
|
||||
):
|
||||
return db.create_invoice(
|
||||
project_id=project_id,
|
||||
invoice_number=number,
|
||||
issue_date=issue_date,
|
||||
due_date=issue_date,
|
||||
currency=currency,
|
||||
tax_label="GST" if tax_rate else None,
|
||||
tax_rate_percent=tax_rate,
|
||||
detail_mode="summary",
|
||||
line_items=[("Consulting", 10.0, 1000)],
|
||||
time_log_ids=[],
|
||||
)
|
||||
|
||||
|
||||
def test_partial_payments_mark_invoice_paid_only_when_fully_settled(fresh_db):
|
||||
project_id = fresh_db.add_project("Client A")
|
||||
invoice_id = _invoice(fresh_db, project_id, "INV-1")
|
||||
|
||||
first = fresh_db.add_invoice_payment(
|
||||
invoice_id,
|
||||
received_at="2026-04-15",
|
||||
invoice_amount_cents=5500,
|
||||
reporting_currency="AUD",
|
||||
reporting_amount_cents=5500,
|
||||
note="First half",
|
||||
)
|
||||
assert first > 0
|
||||
assert fresh_db.get_invoice_with_project(invoice_id)["paid_at"] is None
|
||||
|
||||
second = fresh_db.add_invoice_payment(
|
||||
invoice_id,
|
||||
received_at="2026-05-03",
|
||||
invoice_amount_cents=5500,
|
||||
reporting_currency="AUD",
|
||||
reporting_amount_cents=5500,
|
||||
note="Balance",
|
||||
)
|
||||
assert second > first
|
||||
assert fresh_db.get_invoice_with_project(invoice_id)["paid_at"] == "2026-05-03"
|
||||
|
||||
fresh_db.delete_invoice_payment(second)
|
||||
assert fresh_db.get_invoice_with_project(invoice_id)["paid_at"] is None
|
||||
|
||||
|
||||
def test_invoice_basis_uses_invoice_date_value_not_later_bank_receipt(fresh_db):
|
||||
project_id = fresh_db.add_project("Foreign Client")
|
||||
invoice_id = _invoice(fresh_db, project_id, "USD-1", currency="USD")
|
||||
|
||||
# Value of the invoice in AUD on the invoice-date conversion basis.
|
||||
fresh_db.set_invoice_reporting_value(
|
||||
invoice_id,
|
||||
reporting_currency="AUD",
|
||||
reporting_total_cents=16500,
|
||||
note="Invoice-date FX rate",
|
||||
)
|
||||
|
||||
# The eventual bank receipt is later and a different AUD amount.
|
||||
fresh_db.add_invoice_payment(
|
||||
invoice_id,
|
||||
received_at="2026-05-20",
|
||||
invoice_amount_cents=11000,
|
||||
reporting_currency="AUD",
|
||||
reporting_amount_cents=17000,
|
||||
note="Actual receipt",
|
||||
)
|
||||
|
||||
invoice_rows = fresh_db.get_invoices_for_earnings_range("2026-04-01", "2026-06-30")
|
||||
monthly = aggregate_invoices_by_month(
|
||||
invoice_rows, "AUD", "2026-04-01", "2026-06-30"
|
||||
)
|
||||
|
||||
assert monthly[0].month == "2026-04"
|
||||
assert monthly[0].sales_inc_tax_cents == 16500
|
||||
assert monthly[0].tax_cents == 1500
|
||||
assert monthly[0].sales_ex_tax_cents == 15000
|
||||
assert monthly[0].entry_count == 1
|
||||
assert monthly[1].sales_inc_tax_cents == 0
|
||||
|
||||
payment_rows = fresh_db.get_payments_for_range("2026-04-01", "2026-06-30")
|
||||
payment_monthly = aggregate_payments_by_month(
|
||||
payment_rows, "2026-04-01", "2026-06-30"
|
||||
)
|
||||
assert payment_monthly[0].sales_inc_tax_cents == 0
|
||||
assert payment_monthly[1].sales_inc_tax_cents == 17000
|
||||
|
||||
|
||||
def test_same_currency_invoice_needs_no_manual_reporting_value(fresh_db):
|
||||
project_id = fresh_db.add_project("Local Client")
|
||||
_invoice(fresh_db, project_id, "AUD-1", currency="AUD")
|
||||
|
||||
rows = fresh_db.get_invoices_for_earnings_range("2026-04-01", "2026-04-30")
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["reporting_total_cents"] is None
|
||||
assert invoice_reporting_amount_cents(rows[0], "AUD") == 11000
|
||||
|
||||
monthly = aggregate_invoices_by_month(rows, "AUD", "2026-04-01", "2026-04-30")
|
||||
assert monthly[0].sales_inc_tax_cents == 11000
|
||||
assert monthly[0].tax_cents == 1000
|
||||
|
||||
|
||||
def test_invoices_are_aggregated_by_issue_month(fresh_db):
|
||||
project_id = fresh_db.add_project("Client B")
|
||||
_invoice(
|
||||
fresh_db,
|
||||
project_id,
|
||||
"INV-APR",
|
||||
tax_rate=None,
|
||||
issue_date="2026-04-30",
|
||||
)
|
||||
_invoice(
|
||||
fresh_db,
|
||||
project_id,
|
||||
"INV-JUN",
|
||||
tax_rate=None,
|
||||
issue_date="2026-06-01",
|
||||
)
|
||||
|
||||
rows = fresh_db.get_invoices_for_earnings_range("2026-04-01", "2026-06-30")
|
||||
monthly = aggregate_invoices_by_month(rows, "AUD", "2026-04-01", "2026-06-30")
|
||||
|
||||
assert [r.sales_inc_tax_cents for r in monthly] == [10000, 0, 10000]
|
||||
assert [r.entry_count for r in monthly] == [1, 0, 1]
|
||||
|
||||
|
||||
def test_foreign_invoice_without_reporting_value_is_not_guessed(fresh_db):
|
||||
project_id = fresh_db.add_project("Foreign Client")
|
||||
_invoice(fresh_db, project_id, "USD-MISSING", currency="USD")
|
||||
|
||||
rows = fresh_db.get_invoices_for_earnings_range("2026-04-01", "2026-04-30")
|
||||
assert invoice_reporting_amount_cents(rows[0], "AUD") is None
|
||||
|
||||
monthly = aggregate_invoices_by_month(rows, "AUD", "2026-04-01", "2026-04-30")
|
||||
assert monthly[0].sales_inc_tax_cents == 0
|
||||
assert monthly[0].entry_count == 0
|
||||
|
||||
|
||||
def test_reporting_value_is_invalidated_when_invoice_date_changes(fresh_db):
|
||||
project_id = fresh_db.add_project("Foreign Client")
|
||||
invoice_id = _invoice(fresh_db, project_id, "USD-EDIT", currency="USD")
|
||||
fresh_db.set_invoice_reporting_value(invoice_id, "AUD", 16500, "RBA")
|
||||
|
||||
fresh_db.set_invoice_field_by_id(invoice_id, "issue_date", "2026-04-02")
|
||||
invoice = fresh_db.get_invoice_with_project(invoice_id)
|
||||
assert invoice["reporting_currency"] is None
|
||||
assert invoice["reporting_total_cents"] is None
|
||||
assert invoice["reporting_note"] is None
|
||||
|
||||
|
||||
def test_legacy_paid_invoice_is_reported_as_unstructured_for_payment_basis(fresh_db):
|
||||
project_id = fresh_db.add_project("Legacy Client")
|
||||
invoice_id = _invoice(fresh_db, project_id, "LEGACY-1")
|
||||
fresh_db.set_invoice_field_by_id(invoice_id, "paid_at", "2026-04-22")
|
||||
fresh_db.set_invoice_field_by_id(invoice_id, "payment_note", "Received AUD 110.00")
|
||||
|
||||
missing = fresh_db.get_paid_invoices_without_payments("2026-04-01", "2026-06-30")
|
||||
|
||||
assert len(missing) == 1
|
||||
assert missing[0]["invoice_number"] == "LEGACY-1"
|
||||
Loading…
Add table
Add a link
Reference in a new issue