Invoices | Cine Power Planner

Invoices

Phase 3 of the Accounting domain — draft, finalize, send, and chase German-compliant invoices with EN 16931 (ZUGFeRD/Factur-X) e-invoice attachment

Visibility (production): As of v0.223.8 the entire Accounting surface (sidebar entries, /accounting/* routes, Settings → Accounting tab) is gated to owner accounts in production. Dev / staging builds keep everything visible. This page documents the feature for owners and for the eventual general-availability release.

Overview

The Invoices module lives at /accounting/invoices and lets you bill clients from inside Cine Power Planner — no Lexware / sevDesk / spreadsheet detour. Every invoice is local-first (you can draft and finalize offline; sending requires network), legally compliant with § 14 UStG mandatory fields, attaches a machine-readable ZUGFeRD / Factur-X EN 16931 COMFORT XML so customers' accounting software can consume it without re-keying, and supports the full German Mahnwesen (dunning) cycle when invoices go overdue.

Six capture paths feed the same Invoice entity:

  1. Manual — blank form. Pick a contact, add line items, finalize.
  2. From a project — open the project's Billing tab, click + Create invoice draft. Labor + gear + sub-rental costs roll into the draft as line items with a live-sync link to the source.
  3. From a contract — the contract's BillingTab carries a + Create invoice draft CTA that prefills producer + deliverables.
  4. From a sub-rental — the sub-rental detail surface has the same CTA; line items mirror the rental return data.
  5. From a timesheet — TimeTracking detail → + Create invoice draft seeds line items from logged hours per crew member; multi-select project + date-range filter on the editor.
  6. From an expense — the expense detail panel shows a + Add to invoice picker that surfaces matching invoice drafts (same project + same customer).

The Invoices feature ships in V1 (this release). Three things this line listed as deferred have since shipped and now live elsewhere in the app:

  • Recurring invoices — Settings → Accounting → Recurring invoices.
  • Anzahlungsrechnung + Schlussrechnung — start one from the invoice list; the chain is shown on each document.
  • Bank-feed reconciliation — Settings → Accounting → Banking, see Banking.

Still deferred to follow-up specs (requirements.md § Out of Scope): the customer-facing client portal, multi-PSP payment links, outbound XRechnung (B2G) — inbound e-invoices already import, see Importing e-invoices — and DATEV Z3 / IDEA-XML export.

Which system allocates the numbers

Settings → Accounting → Invoice settings → System of record. Per company, one of:

ChoiceAllocatesIssues
Cine Power Planner (default)every document numberall six document kinds
sevDesk / Lexwarequote + invoice numbersquotes and invoices only

Exactly one system may keep the books for a company at a time — § 14 (4) no. 4 UStG. Two systems allocating in parallel produce gaps or duplicates in the number range, which surfaces at an audit rather than at the time. That is why this is a single choice and not a set of per-provider switches.

Four kinds never leave. Auftragsbestätigung, Lieferschein, Gutschrift and Mahnung stay in Cine Power Planner whichever provider you choose, because no provider represents them. Switching to a provider asks you to confirm those four by name — so nobody goes looking, after the hand-over, for a document kind that was never there.

Two things block the switch:

  • Missing credentials. A provider is only selectable once its API credentials are stored. Until then the option is visible but inert, with the reason on the row.
  • Open documents. While documents of the current year are still open the setting is locked, and it names how many (and which) are holding it. If that count cannot be established, the switch stays locked rather than allowed — the guard fails closed.

Switching does not move documents that already exist. Numbers Cine Power Planner has already allocated stay with Cine Power Planner.

Invoice lifecycle

An invoice's status follows a strict state machine. Once you leave draft, customer + line items + tax + dates are immutable (§ 14 UStG requirement); tags + internal notes remain editable. "Notes visible to the customer" freezes with the rest of the document — it is printed on the invoice, so letting it change after issue would make a re-render disagree with what the customer received.

The two notes fields are not interchangeable. Internal notes is yours alone and never reaches the PDF, the e-invoice XML or the customer. Notes visible to the customer is printed on the invoice, under the closing note — it is where a sub-rental's billing basis lands when you convert an accepted quote. Each field states its destination under its heading.

draft   ──finalize──▶  pending  ──drain queue──▶  sent
draft   ◀──abort────  pending                                  (abort returns to draft)
                                       ──mark paid──▶  paid
                                       ──auto flip──▶  overdue
                                                       ──mark paid──▶  paid
sent / paid / overdue  ──Storno──▶  cancelled  (creates Stornorechnung)
  • draft — fully editable. Lives only on your device until you finalize.
  • pending — you have requested finalize but the atomic invoice-number RPC has not yet succeeded (typical when you finalize offline). The auto-drain hook flushes the queue the next time you come online. You can still abort the pending finalize back to draft.
  • sent — finalized AND emailed to the customer. Customer snapshot, line items, totals, dates, and tax treatment are locked. The PDF + ZUGFeRD XML attachment is now byte-deterministic and never regenerates.
  • paid — bookkeeping signal that the customer has settled. Mahnwesen halts.

Progress billing (Abschlags-/Schlussrechnung): create down-payment invoices via "New Abschlagsrechnung" (normal numbering and finalize flow). Once paid, the chain banner in the editor offers "Create Schlussrechnung" — the closing invoice carries your full performance lines plus an itemized deduction per paid Abschlag (§ 14 Abs. 5 UStG), billing exactly the remainder. Down-payment VAT lands in the UStVA of the payment month, even under Soll-Besteuerung.

Automatic dispatch (optional): under Settings → Accounting → Dunning you can enable per-stage automatic dispatch — due Mahnungen are then issued and emailed once per day when you open the accounting area (default OFF; invoices without a customer email stay in the worklist for manual handling).

  • overdue — auto-flipped when dueDate passes. The app re-checks on every visible-page event (PWA / Capacitor compatible — no background timer needed).
  • cancelled — your only legal way to undo a finalized invoice is a Stornorechnung (countersign invoice with TypeCode 381). See Cancelling an invoice below.

Transitions outside the valid set are rejected by the client validator AND the Supabase RLS / status-transition trigger. Every transition writes a statusHistory entry ({status, at, byUid, channel?, recipients?, messageId?, paymentMethod?, note?}) to the invoice's append-only history array — audit trail visible on the detail panel.

Creating an invoice

Manual

  1. Open Invoices in the sidebar → click + New invoice.
  2. Pick a customer from the contact picker (or create a new contact inline). If the contact has invoicing prefill (customerType, customerLanguage, defaultCustomerReference, defaultPaymentTermsPreset, VAT ID, address), those fields auto-populate.
  3. Add line items via:
    • + Add product — typeahead over your Products catalogue (see Products catalogue below). Snapshots the product's defaultUnit / defaultUnitPrice / defaultTaxRate / defaultDescription at add time.
    • + Add freeform — blank row. Type description + quantity + price.
    • + From timesheet — picker over timesheets in the same project. Logged hours per crew member become one line per crew.
    • + From expense — picker over unbilled expenses linked to the same project. Snapshot at add time (expense lines do NOT live-sync).
  4. Pick the tax treatmentstandard (default), reverse_charge (B2B intra-EU or § 13b UStG), outside_eu (export), or kleinunternehmer (§ 19 UStG if your profile flag is on).
  5. Set Leistungsdatum (date or date-range; auto-filled from source if linked).
  6. Optional: Global discount (percent or absolute, applied to net per-rate-group before VAT — single-step VAT rounding per group). Skonto (early-pay discount, e.g. 2 % within 7 days).
  7. Optional: Customer reference (max 64 chars; printed prominently on the PDF).
  8. Optional: Payment terms presetnet7 / net14 / net30 / net60 / due_on_receipt / custom. The due date is derived from the preset (or custom-set).
  9. Save. The draft auto-saves every 500 ms. Click Finalize to assign the invoice number (next from your year-resetting counter), freeze customer + line items + totals + dates + tax, and unlock the Send action.

From a project

  1. Open the project → Billing tab.
  2. Click + Create invoice draft.
  3. The new draft seeds from the project: customer = production company; line items = labor (per crew) + gear (per category) + sub-rentals + linked expenses. Each line carries a sourceRef so the Live sync banner keeps the draft in step with project changes until you finalize.
  4. Lines you manually edit get a manualOverride flag — those lines stop auto-syncing. Source-derived lines whose source has been deleted get a "Source removed" chip and a Remove from invoice CTA — never auto-removed (you decide).
  5. After finalize the link to the source persists for cross-reference but the data is frozen.

From a sub-rental, timesheet, or contract

Same pattern as project: open the source surface, click + Create invoice draft, draft seeds from the source.

Brutto vs Netto display

Toggle the Brutto / Netto display switch on the line-items table to choose how prices are shown. Netto is the canonical storage (§ 14 UStG-compliant); Brutto is display-only (PAngV default for B2C customers). Totals are computed identically — the toggle only affects the per-line price input column.

Line-item discounts

Each line carries an optional lineDiscount (percent or absolute, ≥ 0). Distinct from the header-level globalDiscount which applies to net per-rate-group across the whole invoice.

Discount to a target total (v0.711.0)

Both the document discount and each line discount carry a Discount to a target total helper (the calculator button). Type the total you want to end up at — 300 where the line or document currently prices at 400 — and the percentage that reaches it is computed for you (25 %).

  • It measures against the pre-discount base: Σ preDiscountBasis across rate groups for the document, quantity × unit price for a line (in whichever Brutto/Netto mode the row is showing — a percentage is a ratio, so the answer is the same either way).
  • It always writes a percentage, so applying it sets the discount type to percent.
  • The readout shows the total the rounded percentage actually reaches before you apply it. Two decimal places of percentage cannot always land on an exact cent: €333.33 off €1000 becomes €333.30, and you see that first.
  • Applying is explicit (the Apply button, or Enter) — nothing is written while you are still typing.
  • With no priced lines there is nothing to discount, so the button is disabled and says why.

Recurring invoices

Turn any invoice into a repeating schedule that auto-generates draft invoices on a cadence — retainers, monthly service fees, equipment leases.

Creating a schedule

On the invoices list, open an invoice's menu and choose Make recurring. The schedule snapshots that invoice's customer, line items, tax treatment, and terms — editing the original invoice afterwards does not change the schedule. Set:

  • Cadencemonthly, quarterly, yearly, or custom (every N months).
  • Start date — the first cycle.
  • Stop condition (optional) — an end date and/or a maximum number of occurrences. You can also pause and resume a schedule at any time.

How drafts are generated

Generation is client-pull: when you open the Invoices list, the app catches the schedule up — creating one draft per cycle that has come due since the last run, through today. Generated drafts carry a "Generated from a recurring schedule" badge.

  • Schedules only ever produce drafts — you review and Finalize each one, which is where the gapless § 14 UStG number is assigned (so a cycle you never finalize leaves no gap in the sequence).
  • Catch-up is bounded to 60 cycles per pass as a runaway guard; a longer backlog drains over subsequent opens.
  • The cycle date becomes the draft's issue date.

Managing schedules

Settings → Accounting → Recurring invoices lists every schedule with its customer, cadence, next run, and active/paused state. From there you can edit the cadence, next-run date, or stop condition, pause or resume it, or delete it. Deleting a schedule leaves already-generated drafts untouched.

Tax treatment

Pick the right taxTreatment for the buyer and the supply — the app injects the statutory exemption text on the PDF + ZUGFeRD XML, locale-aware per the customer's language.

Treatment + customer localeBuyer countryStatutory text injected
kleinunternehmer + DEany"Gemäß § 19 UStG wird keine Umsatzsteuer berechnet." (BMF-recommended wording)
kleinunternehmer + ENany"VAT not charged under § 19 of the German VAT Act (Kleinunternehmerregelung)."
reverse_charge + DE= DE"Steuerschuldnerschaft des Leistungsempfängers (§ 13b UStG)."
reverse_charge + EN= DE"Reverse charge — recipient liable for VAT under § 13b UStG."
reverse_charge + DE≠ DE & ∈ EU"Steuerschuldnerschaft des Leistungsempfängers (Art. 196 MwStSystRL)."
reverse_charge + EN≠ DE & ∈ EU"Reverse charge — VAT to be paid by the recipient under Art. 196 of Council Directive 2006/112/EC."
outside_eu + DE∉ EU"Steuerfreie Ausfuhrlieferung (§ 4 Nr. 1a UStG i.V.m. § 6 UStG)."
outside_eu + EN∉ EU"Tax-exempt export under § 4 Nr. 1a of the German VAT Act (export to non-EU)."
standard + anyanyNo exemption text — standard rate breakdown is shown.

The exemption text is statutory — NOT editable via text templates. The dropdown label is locale-aware (DE / EN); the internal enum stays stable across versions.

Status flow in practice

Finalize

Click Finalize in the editor toolbar. The app:

  1. Validates that every § 14 UStG mandatory field is present (seller details, buyer details, dates, line items, totals).
  2. Calls the atomic issue_invoice_number(p_year) RPC. Year resets on Jan 1 (Europe/Berlin TZ); numbers are gap-free.
  3. Writes a statusHistory entry. The PDF + ZUGFeRD XML attachment is generated and pinned to the row.
  4. Locks customer + line items + totals + dates + tax. The lock is enforced both client-side (LOCKED_INVOICE_FIELDS_AT_FINALIZED) AND server-side (Postgres trigger invoices_01_locked_field_check).
  5. Unlocks the Send, Mark as paid, and Storno actions.

If you finalize while offline, the row enters pending and joins the offline-finalize queue. The next time you come online the auto-drain hook flushes the queue — and re-checks the row's status before each call, so a status that moved past pending (e.g. you marked it paid on another device) is skipped (no double-issue).

Mark as paid

Click Mark as paid (or Mark all paid on a bulk selection). The modal asks for:

  • Paid at — date (defaults to today).
  • Payment methodbank_transfer / cash / paypal / sepa / other.
  • Mahnwesen settlement — only appears if dunningStage >= 1. Pick principal_only (close the invoice but waive Mahnwesen fees), full (Mahnwesen + invoice both settled), or partial.
  • Note — optional free text.

Status moves sent / overdue → paid. A statusHistory entry records the channel + payment method + settlement choice.

Overdue auto-flip

The app auto-flips sent → overdue when dueDate passes. The check runs on every visible-page event (no background timer needed — PWA + Capacitor friendly) with a 4-hour localStorage cache to avoid hammering. The row gets an orange chip in the list view + a "Mahnung" badge once you issue one.

Cancelling an invoice (Stornorechnung)

The only legally clean way to undo a finalized invoice is a Stornorechnung — a countersign invoice with negated amounts that references the original. The app handles the full flow:

  1. Open the invoice → ⋯ menuCancel (Storno).
  2. The confirmation modal shows you a preview of the Storno PDF (same layout as the original but with negated line totals + a STORNO banner). If dunningStage >= 1 you can optionally check Waive outstanding Mahnwesen.
  3. Confirm. The cancel_invoice RPC atomically:
    • Issues the next number from the SAME counter (gap-free).
    • Creates the Storno invoice (isStorno: true, cancelsInvoiceId = original.id).
    • Restates ALL § 14 UStG mandatory fields on the Storno PDF + XML (per BMF: a Storno must be a self-contained legal document).
    • Sets the original's cancelledByInvoiceId = storno.id, cancelledAt = now(), status = 'cancelled'.
    • The ZUGFeRD XML emits TypeCode 381 (<udt:TypeCode>381</udt:TypeCode>) and a <ram:BillingReferencedDocument> pointing to the original.

You cannot Storno a Storno. The validator + RPC + UI all reject it. If you need to undo a Storno, create a regular new invoice with positive amounts.

Correcting an invoice (Rechnungskorrektur / credit note)

Storno covers full cancellation — but a paid invoice, a partial refund (Minderleistung), or a goodwill credit needs a Rechnungskorrektur (kaufmännische Gutschrift). The document is deliberately titled "Rechnungskorrektur", not "Gutschrift": since 2013 a document titled bare "Gutschrift" legally denotes self-billing (§ 14 Abs. 2 UStG).

  1. Invoices list → ⋯ menu on any finalized/sent/paid/partially-paid invoice → Create Rechnungskorrektur.
  2. The line picker shows, per position: original net · already credited · remaining creditable. Enter the NET amount to credit per line (or Full / Credit everything). Over-crediting is blocked — across ALL prior corrections of that invoice.
  3. Create draft opens the correction in the editor. Line amounts are fixed (delete the draft and re-pick to change them); dates, notes and tags stay editable. VAT is inherited per line from the original (incl. reverse charge / Kleinunternehmer).
  4. Finalize allocates a gap-free number from its own RK- sequence and locks the document under GoBD (10-year retention, immutable).
  5. Settle it either by linking the outgoing refund bank transaction (Banking), or via Als verrechnet markieren (offset against another claim). Settlement drives the cash-basis tax effect: ist-UStVA and EÜR recognize the credit in the settlement period; soll-UStVA already reduced at the document date.
  6. The PDF is titled Rechnungskorrektur with the mandatory "zu Rechnung Nr. X vom Y" reference; the ZUGFeRD XML emits TypeCode 381 + a BillingReferencedDocument.

Effects everywhere else: the original's outstanding amount drops by the credited total (Mahnwesen recomputes — an invoice credited to €0 leaves the dunning worklist), DATEV exports post the correction on the Haben side, and the invoices list shows a Korrektur chip plus a Korrekturen filter.

Rules: an invoice with a Storno cannot also be corrected (and vice versa); a credit note itself can be neither storniert nor gemahnt; a mistaken DRAFT is simply deleted — a mistaken FINALIZED correction is offset by a new invoice.

Sending via email

Click Send in the editor toolbar (or Bulk send on a list selection). The modal lets you:

  • Edit recipients — defaults from the customer snapshot; you can add CC / BCC.
  • Pick an email template — your default if not specified (you can save multiple under Settings → Accounting → Templates).
  • Edit subject + body — variables like {{invoiceNumber}}, {{customerName}}, {{dueDate}}, {{total}}, {{paymentTermsLabel}} resolve at send-time. The body supports a safe Markdown-lite subset (bold, italic, links, paragraphs) rendered as React nodes — never via dangerouslySetInnerHTML. A live preview shows what the recipient will see.
  • Pick attachments — the invoice PDF is always attached; you can optionally attach the standalone XML, receipts linked to invoice line items, or freeform files.

Hit Send. The email is dispatched via the send-email-notification edge function on our self-hosted Supabase. If you're offline, the send is queued via useEmailQueueAutoDrain and flushes on window.online. Status moves finalized → sent; the recipient list + message-ID are written to statusHistory.

Idempotency. A 60-second per-{invoiceId, recipientSet} dedupe guards against double-send. Both client-side (IDB) and server-side (email_send_dedupe table).

Per-recipient rate limit. 10 emails / hour / recipient (M8 from the v0.226.2 security audit) — beyond that the server returns 429.

Downloading PDF

Every invoice can be downloaded as a PDF at any point:

  • Draft — preview-only PDF with a "DRAFT / QUOTE" watermark.
  • Finalized — the byte-deterministic legal PDF with the embedded Factur-X XML attachment (factur-x.xml).
  • Cancelled — the original PDF stays unchanged; download the Storno PDF separately.
  • Mahnung — separate PDF per Mahnung; withdrawn Mahnungen carry a red WITHDRAWN watermark.

The PDF is a PDF/A-3 document so the embedded XML is preserved across archival conversions. Customers can drop the file into their accounting software (Lexware, sevDesk, DATEV, etc.) and the XML is read directly — no re-keying.

ZUGFeRD / Factur-X EN 16931 COMFORT

The embedded XML is EN 16931 COMFORT profile — the most expressive ZUGFeRD profile for B2B, with:

  • Per-line <ram:CategoryCode> (S standard, AE reverse-charge, E exempt, G outside-EU, Z Kleinunternehmer).
  • Per-line <ram:UnitCode> mapped to UN/ECE Rec 20 (H87 pieces, HUR hour, DAY day, MTK square metre, MTR metre, KGM kilogram, etc.).
  • Header global discount as a positive <ram:AllowanceCharge> with ChargeIndicator=false.
  • Skonto as <ram:SpecifiedTradePaymentTerms>.
  • SEPA <ram:SpecifiedTradeSettlementPaymentMeans> with TypeCode 58.
  • Dual <ram:SpecifiedTaxRegistration> for both Steuernummer (FC) and USt-IdNr (VA) when present (Pass-4 D-10).
  • Storno reference (<ram:BillingReferencedDocument> → original) + TypeCode 381.
  • Locale-aware <ram:ExemptionReason> (DE / EN) per treatment × buyer country.

Every emitted XML is validated against a pure-JS schematron CI gate on every build (30+ rules across BR-_ / BR-CO-_ / BR-S/AE/E/G / BR-DEC-* families). No XML can ship without validating against the active rule set.

Standalone XML export

If you need the Factur-X XML separately (e.g. for an EDI workflow), open the invoice → ⋯ menuExport XML. The file is named <invoiceNumber>.xml.

Mahnwesen (dunning)

The German Mahnwesen lets you escalate from a polite payment reminder through three formal Mahnungen, each carrying late-payment interest (Verzugszinsen per § 288 BGB) + an optional Mahngebühr + a one-time Verzugspauschale (§ 288 Abs. 5 BGB B2B only). The app implements the full state machine.

Stages

Defaults seed 4 stages — rename them, change the days-after-due / Mahngebühr, mark a stage optional, or edit each stage's bilingual email template (subject + body, with {invoiceNumber} / {customerName} / {stageName} merge variables) under Settings → Accounting → Dunning.

StageDefault nameDays after dueDefault MahngebührVerzugszinsen?
0Zahlungserinnerung7€0Optional
11. Mahnung14€2.50Yes
22. Mahnung28€5.00Yes
33. Mahnung42€10.00Yes

You can optionally skip a stage — handy if you want to ship straight to "1. Mahnung" without a polite Zahlungserinnerung first.

Issuing a Mahnung

  1. Open an overdue invoice → Issue Mahnung.
  2. The modal shows the breakdown:
    • Verzugszinsen(basiszinssatz + spread) × outstanding × days / 365, where spread = 5 pp (B2C) or 9 pp (B2B). If the Basiszinssatz changed mid-period (Bundesbank Jan 1 + Jul 1 updates), the calculator integrates per-segment and sums. Day-count is exclusive (actual/365 banking convention).
    • Mahngebühr — your per-stage default (overridable per Mahnung).
    • Verzugspauschale40€40, B2B ONLY, one-time per invoice. Auto-claimed on the first B2B Mahnung; the verzugspauschaleClaimed flag prevents re-claim.
  3. Confirm. The Mahnung is server-issued via issue_mahnung RPC, the PDF is generated (separate plain PDF — Mahnungen are NOT § 14 UStG invoices so no ZUGFeRD attachment), and the invoice's dunningStage advances.
  4. Status auto-flips sent → overdue if it hasn't already.

Dunning worklist (review + batch)

Instead of opening each overdue invoice by hand, Accounting → Dunning (/accounting/dunning) lists every overdue invoice that has reached its next Mahnung stage — sorted most-overdue first, with the outstanding amount and which stage is due. Filter between Due now and All overdue (not-yet-due rows are read-only and show "due in N days"). Issue a single invoice's next Mahnung from its row, or select several and Issue selected in one batch (each goes to the customer's saved email; invoices with no email or no Basiszinssatz are skipped and reported). Nothing is ever sent automatically — every issuance is your explicit action. When invoices are due, a once-per-day notification + a count badge on the Accounting tile flag them.

Withdrawing a Mahnung

If you issued a Mahnung in error (e.g. the customer paid before you saw it), click Withdraw last Mahnung. The Mahnung gets withdrawn: true + a red WITHDRAWN watermark on its PDF; the verzugspauschaleClaimed flag resets so a future Mahnung can re-claim Pauschale40 if appropriate.

Basiszinssatz (§ 247 BGB)

The Bundesbank publishes the Basiszinssatz on Jan 1 + Jul 1 every year. You set it under Settings → Accounting → Dunning → Base interest rate (Basiszinssatz) (the € 40 Verzugspauschale toggle lives there too). Setting the current rate auto-records a dated history entry, and you can add past rates by hand, so Mahnungen issued today reflect the rate that was in force during the late-payment period (not just the current rate).

If the rate is stale (>200 days old), the app shows a non-blocking nudge to update it.

CSV import/export for products

The Products catalogue (Settings → Accounting → Products) supports CSV round-trip:

Export

Click Export CSV. Output is RFC-4180 (UTF-8 + BOM). Cells starting with =, +, -, or @ are prefixed with a single quote to prevent Excel formula-injection (SEC-INV-013).

Import

Click Import CSV. The 2-step preview-then-commit modal:

  1. Preview — parses with papaparse strict mode, sanitizes cell-injection prefixes, runs per-cell type coercion, and shows you any rows with errors (line-numbered).
  2. Commit — once you confirm, the valid rows write to your catalogue. Errored rows are skipped (you can fix the source CSV and re-import).

File size cap. 25 MB per file (SEC-INV-014).

The same two-step pattern is available for invoices themselves (Bulk → Import CSV) using a two-table schema (invoices + lineItems).

Templates

Settings → Accounting → Templates lets you save reusable text bundles. Each template has 6 DE+EN slots:

  • subject — email subject (with {{invoiceNumber}} etc. variables)
  • intro — opening paragraph
  • dueDateSentence — payment instruction
  • paymentTerms — bank details / payment methods
  • footer — closing line
  • signatureLine — sender signature

Variables resolved at send-time: {{invoiceNumber}}, {{invoiceDate}}, {{dueDate}}, {{customerName}}, {{total}}, {{leistungsdatum}}, {{paymentTermsLabel}}, {{senderName}}. The clickable cheatsheet on the template editor inserts variable tokens for you.

Default template safety net

Every user gets one default template on first hydration named "Standard" with sensible DE+EN copy. If you delete the only template, the app auto-seeds "Standard" again. If you delete the default but keep others, the most-recently-updated sibling auto-becomes the new default — you can never end up without a default.

Settings

The accounting settings nav (Settings → Accounting) has fifteen sub-pages. Six of them shape invoices:

Categories

The shared SKR04 category catalogue (lives at v0.226 — see docs/user/ACCOUNTING.md). Categories are read by both Expenses and Invoices.

Products

Your reusable product catalogue. Each product carries name DE+EN, default unit, default unit price, default tax rate, default description, SKU. Soft-archive hides from picker; hard-delete is allowed for customs.

Templates

Your invoice text templates (see Templates above).

Counter / Numbering

Read-only peek at your invoice counter for the current year + next-number preview. Set your prefix here — invoice numbers follow {prefix?}-YYYY-NNNN. Year resets on Jan 1 (Europe/Berlin TZ). Storno uses the next number from the SAME counter (gap-free).

Email defaults

Sender / Reply-To / Subject / Body (DE+EN) shown to new invoices by default. The Markdown-lite preview renders as React nodes (no XSS surface). Click any variable chip to insert it into the body.

Invoice settings

  • Kleinunternehmer (§ 19 UStG) — toggle the profile flag. The YTD-Umsatz advisory tracker uses the Wachstumschancengesetz 2024 thresholds: €25 000 = next-year limit (you become Regelbesteuerer in year N+1; KU status in year N preserved) — soft banner at 80 %, hard forward-looking banner at €25 000. €100 000 = immediate-effect limit — blocking modal; in-flight kleinunternehmer drafts cannot be finalized until you toggle the flag off.
  • EPC069-12 Giro-Code QR — toggle to add a Giro-Code QR to the invoice PDF payment block. Customers scan with their banking app to prefill SEPA transfer details. The IBAN is validated per format (DE / FR / NL / etc.) before QR encoding.
  • Mahnwesen settings — rename stages, set per-stage daysAfterDue + defaultMahngebuehr, toggle stages as optional, disable Verzugspauschale40 globally (e.g. as a goodwill choice for a B2B customer).
  • Basiszinssatz — current rate + history.
  • Steuernummer + USt-IdNr — both can be set; the app prints both on the PDF + ZUGFeRD when present (Pass-4 D-10).

Multi-language — DE/EN customer locales

Each customer carries a customerLanguage (de or en). The invoice PDF + email subject / body / preview render in that language. Statutory exemption text is locale-aware (DE / EN); legal references (§ 19 UStG, § 286/288 BGB, § 4 Nr. 1a UStG, Art. 196 MwStSystRL) stay verbatim regardless of locale.

The UI itself follows your account language — independent of the customer's language.

Bulk operations

Select multiple invoices in the list view to unlock the bulk action bar:

  • Bulk send — sends each selected invoice via email. Status-aware: rows already paid or cancelled are filtered out + surfaced as "skipped" count. Per-row send rate-limited to 5 / sec sustained with backoff (REQ-INV-114).
  • Bulk mark paid — same paid-modal as the single action; applies to every selection.
  • Bulk re-link project — change the linkedProjectId on N invoices at once.
  • Bulk tags — add / remove tags across the selection.
  • Bulk delete — only allowed on draft rows (finalized invoices cannot be deleted — only Storno'd).
  • Bulk export — CSV / PDF (combined per-invoice multi-page) / ZIP (per-year folders).

Selection persists across virtualized scroll (REQ-INV-110); you don't lose your bulk picks when you scroll a 500-row list.

Cross-feature linking

Invoices interconnect with the rest of the app:

  • Contacts — the contact-editor carries an Invoicing fieldset (customer type, customer language, default customer reference, default payment terms preset, VAT ID, address). A Linked invoices panel on the contact detail shows every invoice for this customer.
  • Projects — the Billing tab on each project shows linked invoices + a + Create invoice draft CTA (this replaces the legacy per-project draft-preview modal flow).
  • Sub-rentals — same pattern: linked-invoices panel + + Create invoice draft CTA on the sub-rental detail.
  • TimeTracking+ Create invoice draft on the timesheet detail; the invoice editor's TimesheetPicker filters by project + date range.
  • Expenses — the expense detail panel shows a LinkedInvoiceChip when the expense has been added to an invoice, and a Cancelled chip when the linked invoice has been Storno'd.
  • AccountingPage — the Outstanding invoices tile shows the sum of pending + sent + overdue invoice totals alongside the expense summary.

Offline behavior

  • Drafting + editing works offline indefinitely. Auto-save writes to IndexedDB every 500 ms; OPFS holds a backup; the cloud sync engine pushes deltas when you come back online.
  • Finalize works offline — the row enters pending and joins the finalize queue. The useInvoiceQueueAutoDrain hook flushes the queue on window.online; each drain re-checks the row's status before calling the RPC (Pass-4 D-14 status-check guard prevents double-issue when the row was advanced on another device).
  • Send works offline — the send is queued via useEmailQueueAutoDrain and flushes on window.online.
  • PDF generation works offline (uses pdfmake + the embedded ZUGFeRD XML emitter — no server round-trip).

Storage & privacy

  • Invoices, products, and templates are entity-encrypted at rest before leaving the local IDB (same pattern as expenses since v0.223.3).
  • The Supabase tables are RLS-protected: owner_uid = auth.uid() OR auth.uid() = projects.owner_uid (joined via linkedProjectId) for invoices; owner-only for products and invoice_templates.
  • Mahnungen live as a sub-array on the Invoice row (JSONB column); RLS naturally inherits from the parent.
  • Email send authorization happens at the edge function: invoice.owner_uid === caller.sub is verified before any SMTP relay (SEC-INV-007).
  • Attachments (custom files added to the send modal) go to a private invoice-attachments Storage bucket under <uid>/<invoiceId>/<filename>; reads use short-lived signed URLs (1 h expiry).

The Invoices module implements:

  • § 14 UStG — mandatory invoice fields (seller, buyer, dates, line items, totals, exemption text per § 14 Abs. 4 Nr. 8). Validated at finalize time.
  • § 14b UStG / § 147 Abs. 3 AO — 10-year retention. V1 satisfies this passively — Supabase keeps rows indefinitely, no automated purge exists, RLS preserves access for the owner. Active retention guardrails (deletion-warning modals, soft-archive with date-based hard-delete) are deferred to a follow-up SDD pass.
  • § 14 Abs. 4 Nr. 4 UStG — gap-free, year-resetting invoice numbers via the atomic issue_invoice_number(p_year) RPC. Storno uses the next number from the SAME counter to preserve the gap-free guarantee.
  • § 19 UStG — Kleinunternehmer flow with Wachstumschancengesetz 2024 thresholds (€25 000 next-year / €100 000 immediate).
  • § 13b UStG — reverse-charge intra-DE with the BMF-recommended exemption text.
  • § 4 Nr. 1a UStG — outside-EU export exemption with the BMF-recommended exemption text.
  • § 286 BGB — Verzug start-date semantics (= dueDate + 1 day per Pass-5 D-26).
  • § 288 BGB — Verzugszinsen calculation (Basiszinssatz + 5 pp B2C / 9 pp B2B; actual/365 day-count; rate-segment integration across Basiszinssatz changes).
  • § 288 Abs. 5 BGB — Verzugspauschale40 (B2B one-time, €40 default, withdraw-resets).
  • GoBD (BMF 28.11.2019) — append-only statusHistory, append-only mahnungen[], server-side enforcement via Postgres triggers (invoices_02_status_history, invoices_04_mahnungen_trigger). No "edit history" UI — only forward transitions; Storno is the only legal undo.
  • EN 16931 — emitted ZUGFeRD/Factur-X XML conforms to the COMFORT profile; validated against the in-repo pure-JS schematron CI gate on every build.

Disclaimer. The Kleinunternehmer YTD-Umsatz tracker is informational only. You are responsible for ensuring compliance with § 19 UStG and the Wachstumschancengesetz thresholds (SEC-INV-015). Likewise the Mahnwesen Verzugszinsen calculator is informational; the legal compliance burden remains with you (talk to your Steuerberater for edge cases — partial-pay allocations, foreign-currency invoices, etc.).

Custom PDF layouts — page design (v0.409.0, #227)

The document layout designer (Accounting → Document layouts, /accounting/layout) styles the PDFs generated for invoices, quotes, order confirmations, delivery notes and Mahnungen (opt-in via "Use my custom layouts for generated PDFs").

  • Standard design. Out of the box, template-driven PDFs use the same design language as every other PDF the app exports: your brand accent colour (from Settings → Account → Signature & Branding) on the title, header rule, table hairlines, grand total and footer. No configuration needed.
  • Page design panel. While no block is selected, the inspector column shows Page design with five document-wide controls: accent colour, text colour, base font size (7–16 pt; headings and small print scale along) and page margins (Compact / Normal / Spacious). A font-family picker is planned; the renderer currently ships only the embedded Ubuntu face. Leave any field empty to fall back to the standard design. The live preview updates as you edit; Save persists the design per document type.
  • Per-block design. Selecting a block still opens its own inspector with block-level style overrides (font size, colour, alignment, spacing) — block overrides beat the page design.
  • Finalized invoices never change. The page design freezes together with the block layout into the § 14b snapshot when an invoice is finalized; later edits to the template only affect future documents.

Last Updated: 2026-09-10 Version: 0.790.2