| Date | 13 Jun 2026 |
| Commit | 7af78a8 (dev pushed to origin/main; prod pull pending) |
| Issue | #17 |
| Wiki | Updated through Session 24 |
A full Credit Notes workflow, loosely modelled on Billingbooth. A credit note is issued against a specific invoice and reduces that invoice's outstanding balance. When outstanding hits zero, the invoice auto-flips to paid; deleting a credit restores outstanding and reverts status.
NavGroup component, with Credit Notes as a child entry. The chevron toggles expand/collapse; clicking the parent label still navigates to /invoices.Credit Note action that opens an Issue modal (amount + reason). Amount is capped at the invoice's current outstanding./credit-notes) — list of all credit notes with Send, PDF, Delete actions per row.| File | Change |
|---|---|
apps/billing/models.py |
CreditNote gains pdf_path, sent_at, sent_to, notes. New Invoice.credits_total_aud property. amount_outstanding now returns total - paid - credits. |
apps/billing/migrations/0005_* |
Auto-generated, applied on dev. |
apps/billing/utils.py |
generate_credit_note_number() → CN-YYYY-NNNN. |
apps/billing/serializers.py |
CreditNoteSerializer. |
apps/billing/views.py |
CreditNoteViewSet (create / destroy / download / send). Helper _recompute_invoice_status_for_credits() runs after every create/destroy. delete-invoice and delete-run now refuse when credit notes exist; ?force=true cascades through with PDF cleanup. |
apps/billing/pdf.py |
generate_credit_note_pdf() (separate template, no payment link). |
apps/billing/email.py |
send_credit_note_email() (PDF attached, no checkout URL — credits aren't paid by the customer). |
apps/api/urls.py |
/api/credit-notes/ route registered. |
| Templates | templates/creditnote/creditnote.html, templates/email/creditnote_email.{html,txt}. |
| File | Change |
|---|---|
frontend/src/components/Sidebar.jsx |
New NavGroup component; Invoices now has a children array. |
frontend/src/pages/Invoices.jsx |
Credit Note action + Issue modal. Force-delete prompt now lists credit numbers + amounts. |
frontend/src/pages/BillingRuns.jsx |
Force-delete modal split into paid-invoices + credited-invoices sections; button label generalised. |
frontend/src/pages/CreditNotes.jsx |
New list page (send / download / delete). |
frontend/src/App.jsx |
/credit-notes route. |
apps/payments/views.py:164. Always call _recompute_invoice_status_for_credits(invoice) after a credit change; don't reimplement the math.PROTECT FK with explicit force-cascade — CreditNote.invoice stays PROTECT (silent drop on invoice delete would be an audit failure). delete-invoice / delete-run explicitly count credit notes and refuse with HTTP 400; ?force=true deletes them + PDFs alongside the cascade.Credit Note CN-YYYY-NNNN from Yealin Communications — $X against YC-YYYY-NNNN.CN-YYYY-NNNN sequence is query-based, per year — same shape as generate_invoice_number(). Inherits the same GH #5 weakness (reset on full-year wipe); the fix that lands for GH #5 must cover both.| Item | Where |
|---|---|
Pull 7af78a8 on prod, run migrate, rebuild frontend, restart gunicorn + celery |
Deploy workflow |
| Expose Credit Notes in customer portal (admin-only today) | Backlog E |
| Reports — surface credit-note totals against revenue | Backlog H |
| Sequence-table fix covers both invoice and credit-note numbers | Backlog A / GH #5 |
Tested on dev (yealin-dev.bylaw.com.au):
paid.sent.?force=true succeeded and removed the credit + its PDF.7af78a8 (Session 24 main changelog).CN-YYYY-NNNN. When #5 is fixed, generalise the sequence-table approach.invoiced flag and the credit-note PROTECT FK in the same operation when the comprehensive cleanup lands.