Skip to content

feat: Deposit & Final Invoices (Abschlagsrechnung / Schlussrechnung) - #349

Merged
clstaudt merged 5 commits into
mainfrom
feat/326-deposit-final-invoices
Aug 23, 2026
Merged

feat: Deposit & Final Invoices (Abschlagsrechnung / Schlussrechnung)#349
clstaudt merged 5 commits into
mainfrom
feat/326-deposit-final-invoices

Conversation

@clstaudt

@clstaudt clstaudt commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Closes #326

Rebased onto current main (the branch was 171 commits behind, predating the ContractCharge system and the non-nullable InvoiceItem.VAT_category), then finished.

What this does

A fixed-price contract can carry a payment schedule — milestones by percentage or by amount. Each milestone is billed as a deposit invoice (Abschlagsrechnung); the last one produces a final invoice (Schlussrechnung) that states the full contract price, deducts every deposit already issued with the VAT contained in each, and shows the remaining balance.

  • Schema: PaymentMilestone table, Invoice.document_type extended with "deposit" / "final", deposit_for_id chain, milestone_id, Alembic revision 1100c34b90c6 (re-parented onto head 9cad5ae77a79).
  • Invoicing: generate_deposit_invoice() / generate_final_invoice(), create_deposit / create_final intents, deposits carry the contract's VAT_category, paid status propagates from a settlement to the deposits it settles.
  • Rendering: document-type banner, deposit context (which milestone of which contract), per-deposit deduction lines with "VAT included therein", and remaining balance — in all seven templates, driven by shared partials in templates/_shared/ so the settlement layout cannot drift between skins.
  • UI: payment-schedule editor on contracts, document-type picker in the create dialog (final enabled only once deposits exist), open-milestone picker, deposit chain grouping, milestone schedule badge.

Scope decisions

  • Contract charges are not billed on deposits. A deposit bills a milestone amount and nothing else; charges appear on the final invoice, where the full contract scope is settled.
  • Deposit and final invoices ship as PDF without embedded ZUGFeRD XML, with a logged warning. The builder emits type code 380 with full document totals, while EN16931 wants 386 for a prepayment and BT-113 prepaid amounts on the settlement — emitting 380 would state an amount the client does not owe. Correct prepayment support is follow-up scope.

Fixed since the draft

  • Stale milestone badge — the schedule progress is now derived from the invoice chain, which the view reloads, instead of from milestone.invoiced flags on a contract snapshot embedded in an older invoice. Ordinary invoices on a schedule contract no longer show the badge at all.
  • Final-invoice rendering failures — invoices are reloaded from the data source after the insert, so the deduction payload renders from a session-bound graph.
  • Deposits silently dropped from the deduction list — writing the milestone flags through save_invoice merged a contract graph loaded before the settlement existed, whose cascade wrote the old deposit_for_id back, leaving only the newest deposit deducted. Linking and flagging now go through targeted writes (link_deposits_to_final, mark_milestones_invoiced).

Test plan

  • uv run pytest — 580 passed, 1 skipped; ruff check / ruff format --check clean; npx tsc --noEmit clean.
  • tuttle_tests/test_deposit_invoices.py: VAT settlement math against the exact example in Deposit & Final Invoices (Abschlagsrechnung / Schlussrechnung) #326 (10,000 net / 1,900 VAT / 11,900 gross, less a 5,950 deposit containing 950 VAT, leaves 5,950), schedule validation (percentages sum to 100, amounts sum to the fixed price, no mixing, invoiced milestones cannot be repriced or removed), and the e-invoice guard.
  • test_rpc_dispatch.py::test_deposit_and_final_invoice_lifecycle: a 40/40/20 schedule end to end over real RPC — two deposits, the final deducting both, JSON-serialisable throughout (a deposit chain is what provokes DetachedInstanceError), paid status reaching the whole chain.
  • Deposit and final PDFs rendered in all seven templates, EN and DE, and read for layout and localisation.
  • Playwright Electron smoke run of the schedule → deposit → final flow (ui/scripts/smoke-deposit.ts); screenshots in the thread below.

Demo data

Two schedules, per the AGENTS.md demo-data rule: Sam Lowry's Heating Repair is settled (deposit + final, 50/50), and Central Services' Heating Modernisation is mid-flight — one deposit paid, two milestones open — so the workflow can be walked in the app.

Made with Cursor

@clstaudt
clstaudt force-pushed the feat/326-deposit-final-invoices branch from fb7f6db to 2d8b53c Compare August 8, 2026 11:51
@clstaudt
clstaudt marked this pull request as ready for review August 8, 2026 12:02
@clstaudt
clstaudt requested a review from aaronspring August 8, 2026 12:03
@clstaudt clstaudt added this to the 4.3 milestone Aug 9, 2026

@aaronspring aaronspring left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

never worked with Abschlagsrechnung but seems a useful feature.

Comments:

  • Invoice Details view:
    • same for "Final" and "Half upfront": what should the details view show? currently the different deposits and the remaining balance.
  • only for fixed-price contracts. do you see deposit invoices used for time-based?
  • in contracts -> fixed-price, if I add milestones and then click on collapse, payment schedule disappears and only reappears if I click on "add milestone". should rather be > collapsed hiding the milestones only and when user wants to add should click on > collapsed

@clstaudt clstaudt modified the milestones: 4.3, 4.4 Aug 15, 2026
clstaudt and others added 4 commits August 23, 2026 14:47
…srechnung)

Implements #326 — linked partial invoice chains with milestone-based
payment schedules.

Schema:
- Add PaymentMilestone table for contract payment schedules
- Extend Invoice.document_type with "deposit" and "final"
- Add deposit chain FK (deposit_for_id) and milestone FK
- Alembic migration for all new columns/tables

Invoicing logic:
- generate_deposit_invoice() for milestone-based deposits
- generate_final_invoice() showing full amount with deposit deductions
- Last-milestone shortcut auto-creates final invoice
- toggle_paid propagates across deposit chains

Rendering:
- Legally compliant PDF layout for deposit and final invoices
- Deposit deduction lines with VAT breakdown on final invoices
- i18n labels (EN/DE/ES) for all new document types

UI:
- Milestone editor on contract detail view
- Document type picker (Invoice / Milestone) in create dialog
- Deposit chain visualization in invoice list
- MilestoneScheduleBadge showing progress
- Deposit/final invoice detail views

Known issues:
- Milestone paid count badge may show stale data
- PDF rendering for final invoices needs session hydration fix

Co-authored-by: Cursor <cursoragent@cursor.com>
…voicing

Completes the deposit / final invoice workflow so a Schlussrechnung is
correct wherever it is rendered and cannot be mis-stated to a client:

- Shared Jinja partials and stylesheet under templates/_shared/ carry the
  document-type banner, deposit context and settlement lines, so all seven
  invoice skins deduct deposits identically instead of each restating the
  legally relevant layout.
- Deposits are linked to their settlement, and milestones flagged, through
  targeted writes. Merging whole invoice graphs let a stale contract
  snapshot write back the old deposit_for_id, which dropped every deposit
  but the newest from the deduction list.
- ZUGFeRD XML is skipped with a logged warning for document types the
  builder cannot express: it emits type code 380 with full totals, while
  EN16931 wants 386 for a prepayment and BT-113 prepaid amounts on the
  settlement.
- Demo data grows a schedule mid-flight (one deposit issued, two open) so
  the workflow can be walked in the app, plus a Playwright smoke script.

Co-authored-by: Cursor <cursoragent@cursor.com>
… own idiom

The coloured banner, boxed context and all-caps number label announced the
document type at full volume in every template, fighting the design of
skins that are deliberately quiet. An invoice already says what it is in
its heading and number — the type only needs to register, not to shout.

- Banners now appear for reminders only. Deposit and final carry the type
  as a "document type" meta-table row (modern, minimal, bold), a qualifier
  in the heading (grayshades, anvil, base) or their existing title block
  (classic).
- The deposit context loses its grey box and accent-coloured micro-labels;
  it reads as a short key/value list between two hairlines.
- The remaining banner keeps a single left rule in the template's accent
  colour instead of a filled, type-coded strip.

Co-authored-by: Cursor <cursoragent@cursor.com>
…e details

Review feedback from aaronspring on #349:

- Contract form: collapsing the payment schedule no longer makes it
  vanish. "Schedule enabled" and "collapsed" are now separate state;
  the collapsed disclosure reads "> N milestones" and re-expands in
  place, and saving no longer depends on the visual collapse state.
- Invoice details: a final invoice now leads with the settlement —
  contract total, deposits deducted, and the remaining balance due —
  instead of the same Subtotal/VAT/Total cards a deposit shows. A
  deposit invoice's details name the milestone it bills (with its
  percentage of the contract) and the contract total for reference.
- Re-parent the deposit/final migration onto the multiple-bank-accounts
  head that landed on main since the last rebase.

Deposit invoices stay fixed-price-only by design: an Abschlagsrechnung
bills a share of an agreed total, which a time-based contract does not
have — its analogue is the ordinary periodic invoice. Enforced in the
create dialog, the intents, and the generators.

Co-authored-by: Cursor <cursoragent@cursor.com>
@clstaudt
clstaudt force-pushed the feat/326-deposit-final-invoices branch from a2728c1 to 83e71ed Compare August 23, 2026 12:55
…en invoice list

Fixed-price contracts have rate=None, so revenue_from_calendar and
contract_revenue_forecast crashed with "unsupported operand type(s)
for *: 'decimal.Decimal' and 'NoneType'" when the demo data introduced
milestone-billed contracts with calendar events. Guard both call sites.

Widen the invoice list panel from 480px to 520px so deposit chain rows,
milestone labels, and amount columns have room to breathe.

Co-authored-by: Cursor <cursoragent@cursor.com>
@clstaudt
clstaudt added this pull request to the merge queue Aug 23, 2026
Merged via the queue into main with commit 5c5b544 Aug 23, 2026
3 checks passed
@clstaudt
clstaudt deleted the feat/326-deposit-final-invoices branch August 23, 2026 13:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deposit & Final Invoices (Abschlagsrechnung / Schlussrechnung)

2 participants