Invoices
Invoices are the financial endpoint of the CRM lineage. They get spawned from accepted quotes (single or split across installments), from converted contracts, or created standalone for ad-hoc billing. Every invoice has a gap-free sequence number (invoice_number), supports Storno (cancellation per §14 / §11 UStG), and can be sent to the customer’s billing_email independently of their primary email.
Seeded defaults are examples only. Payment-term templates (“Net 14”, “Sofort fällig”, etc.), default Skonto windows, IBAN / QR-bill setup, and VAT rates are placeholders. Confirm with your bank and your tax advisor before issuing the first real invoice, especially if you’re issuing Swiss QR-bills (the swissqrbill library is used; formatting choices like creditor reference type can vary by bank).
Prerequisites
- Feature flag:
billsenabled in Settings → Features.quotes=falseforcesbills=false. - Business profile: complete, including a bank account (
business_bank_accounts). Invoices refuse to render without one — the issuer block needs an IBAN. - Customer: existing record in CRM → Accounts.
Invoice lifecycle
| Status | Meaning |
|---|---|
scheduled | Created with a future scheduled_send_at; the scheduler will send it on that date |
pending_delivery | Spawned by an installment plan with a delivery trigger (on_delivery or on_acceptance) — waits for the trigger event |
sent | Emailed to the customer; PDF persisted |
partially_paid | A partial payment has been logged but the open amount > 0 |
paid | Fully paid (manual mark or payment-check confirmation) |
overdue | Past due date, not paid — reminders engage based on reminder_level |
cancelled | A Storno has been issued for this invoice |
Creating an invoice
Three paths to spawn an invoice:
Standalone
From CRM → Invoices, click New invoice. Same editor shape as the quote editor — pick customer, line items, payment terms. Save as draft or send immediately. No quote / contract upstream → no deal_uuid lineage.
From a quote
When you convert an accepted quote, the Invoice toggle (on by default) spawns one invoice if the quote has no installment plan, or N sibling invoices if it has one. All siblings share the quote’s deal_uuid.
From a contract
createFromContract spawns an invoice using the contract’s line items as a starting point. Same deal_uuid chain.
From hour entries
The “Bill these hours” action on a customer with per_event billing cadence (see Hours logging) creates a single invoice with one line item per unbilled hour entry.
Installment plans
When a quote (or accepted offer) is broken into N installments, conversion spawns N sibling invoices in one transaction. Each carries:
- A unique
invoice_number(gap-free — see Numbering) - The same
deal_uuidand a sharedinstallment_plan_id - An
installment_index(1 of N, 2 of N, …) - An
installment_label(“Anzahlung 30 %”, “Rate 2: nach Lieferung”, …) - A trigger:
scheduled: send on a fixed dateon_acceptance: send immediately after the parent quote is accepted (typical for an Anzahlung)on_delivery: send when the linked event is marked delivered (typical for the final installment)
Editing the plan as a whole after spawn is not yet shipped — see project backlog. Today, after siblings exist:
- You can edit individual siblings (line items, dates) but not the plan structure.
- To restructure the plan, Storno every unsent / unpaid sibling and respawn from a new quote.
Sending an invoice
The Send action:
- Re-renders the PDF (issuer block, line items, totals, payment instructions, Swiss QR-bill on EUR/CHF if
crm_invoices_qr_enabledis true). - Persists the PDF to storage.
- Routes the email per the billing-recipient resolution below.
- Transitions to
sent, stampssent_at.
Scheduled invoices use the same path automatically on scheduled_send_at.
An invoice is sent once
On the beta channel from v3.170.0-beta.0 (PicPeak PR #1982 ) only the Resend button on the invoice’s detail page sends an invoice that is already sent or overdue a second time. Every automatic sender — the scheduler, a workflow’s send step, delivery-triggered installments and a manually triggered monthly bill — sends an invoice once and skips one that is already out, without an email. Two quick clicks on Send queue one email. Storno documents are sent once in the same way.
The built-in booking workflows prepare a draft invoice and stop at a review gate under Approvals. If you send or pay that invoice by hand, or cancel it before it was sent, PicPeak answers the gate for you: sending or paying it counts as a confirm, cancelling it as a deny, and the workflow carries on from there instead of mailing the invoice again. When a booking prepared several invoices (deposit and final), the gate stays open until every one of them has been handled. A gate whose confirm path does not lead to sending the invoice is never answered this way.
On earlier builds and on stable, confirming such a gate after sending the invoice by hand mails it to the customer a second time.
Changing the customer or the gallery
The customer of a saved invoice cannot be changed. On the beta channel from v3.168.2-beta.0 (PicPeak PR #1909 ) the editor shows the customer read-only once the invoice exists, and the API answers 409 INVOICE_CUSTOMER_LOCKED. Cancel the invoice and create a new one for the other customer: monthly drafts, quote lineage, installment siblings and re-billed costs all hang off the customer, so moving a single invoice would split them.
An invoice can be linked only to a gallery you created; a super admin can link any gallery. Being able to open a gallery is not enough — the built-in admin role, a role with permission to manage all galleries, and an admin assigned to a gallery’s team can see it but cannot link invoices to it. A link to a gallery that is not yours is refused (403), and the gallery page hides Create invoice for it. The gallery must also belong to the invoice’s customer. The same PR makes an edit of a scheduled invoice save its VAT code and linked gallery; earlier builds dropped both.
Email routing
Invoices, Storno documents, and payment reminders use the billing_email-aware routing introduced in PR #555:
| Document class | To: | CC: |
|---|---|---|
| Invoice, Storno, payment reminder | billing_email if set, else email | email if billing_email took the To: slot, plus any per-document cc_pdf_email |
| Quote, contract, event reminder, gallery share | email (always — decision-maker address) | per-document cc_pdf_email if set |
| Payment-check, paid-notification | Admin contact (internal flow) | — |
This means a customer with a separate accounts-payable email ([email protected]) gets the invoice there, with their primary contact CC’d. Quotes and contracts still go to the decision-maker.
Payment-check tokens
When crm_invoices_payment_check_enabled is true, each sent invoice mints an invoice_payment_check_tokens row with a per-invoice token. The customer can visit /bills/check/<token> to confirm they’ve paid (typically after triggering the bank transfer).
The customer side:
- Shows the invoice details + total
- Shows a “Confirm payment” button
- Optionally collects the customer’s payment method + reference
On submit, the admin gets a invoice_payment_check_notification email with what the customer reported. The admin then verifies against the bank statement and either:
- Marks the invoice
paidin the admin UI (transitions, queues theinvoice_paid_receiptemail to the customer) - Or flags a mismatch and follows up directly
The payment-check token does NOT automatically mark the invoice paid — it’s a customer-initiated claim that the admin verifies. This avoids the social-engineering risk of “click here to mark your invoice paid”.
In builds containing PicPeak PR #1809 only the newest payment-check link of an invoice works. Sending a new one retires the older links once the new email is queued, and a link answers 410 after it was used or replaced. A link for an invoice that is no longer sent or overdue — already paid, cancelled or replaced — is refused instead of changing the invoice.
Reminders
When crm_invoices_reminders_enabled is true, the scheduler walks overdue invoices and sends a series of reminder emails based on reminder_level:
- Level 1:
invoice_reminder_first(defaults to 7 days past due) - Level 2:
invoice_reminder_second(defaults to 14 days past due, more direct tone) - Level 3:
invoice_reminder_final(defaults to 30 days past due, mentions late-fee accrual)
After level 3, no automatic reminders fire — admin handles direct follow-up.
If crm_invoices_late_fee_enabled is true, each reminder cycle can also accrue a configurable late-fee amount to late_fee_amount_minor. This shows on the customer’s bills tab but does NOT modify the invoice total — the invoice itself is immutable once sent (§14 UStG); the late fee is a separate line on the next correspondence.
Storno / cancellation
§14 (4) Nr. 4 UStG (Germany) and §11 UStG (Austria) require issued invoices to be immutable. To cancel an invoice:
- Issue a Storno — a new document with a fresh
invoice_number(same sequence as regular invoices) whosekind='storno'andcancels_invoice_idpoints at the original. - The Storno carries the original line items with negated totals, plus a header line referencing the cancelled invoice number.
- The original invoice transitions to
cancelled; the Storno is the legal cancellation record.
To split or replace a sent invoice, the rule is also Storno + new: Storno the original, then issue one or more new invoices (sharing deal_uuid for lineage). Never edit a sent invoice in place — even if the schema would technically let you.
The detail page exposes a Issue Storno action on any sent / partially_paid / overdue / paid invoice. Refunding the customer for any payment already received is outside PicPeak — issue the bank transfer or refund through your payment provider; the Storno is the document trail, not the money movement.
Monthly drafts
When a customer is configured with billing_cadence='monthly', instead of issuing per-hour-or-per-event invoices, PicPeak maintains a single running draft invoice per period (per monthly_period_start / monthly_period_end). Every billable hour entry or event auto-appends as a line item to the running draft.
On the period’s billing_cycle_day, the scheduler:
- Finalises the running draft → assigns invoice_number, freezes totals, transitions to
scheduled(orsentifscheduled_send_atwas now). - Opens a fresh draft for the next period.
The admin can also manually trigger an out-of-cycle send via the Trigger monthly bill action on the customer detail page (useful when a long-running project completes mid-period and the customer wants the invoice now).
Numbering
Invoice numbers come from claimNextSequence against the document_sequences table — atomic, gap-free, per-year. Default format is INV-{YEAR}-{SEQ:04d} (e.g. INV-2026-0042). Configurable in Settings → CRM → Document numbering.
Storno documents draw from the same sequence — they get the next number after the most recent regular invoice. This is required by §14 (4) Nr. 4 UStG; a Storno is a kind of invoice for sequencing purposes.
VAT code on the invoice
When the Accounting module is on, the invoice editor exposes a VAT code dropdown (VatRateSelect) populated from Settings → Accounting → VAT codes. Pick the code (e.g. UN81 for Swiss normal-rate output VAT) and the rate is read from the code definition.
The code is snapshotted onto the invoice (migration 130): vat_code_snapshot stores the code, name, rate, and account number at the moment of save. A later rename or rate change in Settings → Accounting does NOT retroactively rewrite historical invoices or the journal export — the snapshot wins. This matters when your Treuhänder asks for a re-export of last quarter after you’ve renamed codes.
When the Accounting module is off, the editor falls back to the legacy single-rate input (vat_rate on the invoice) and the code column is empty in any export.
VAT / free-text note
Settings → CRM → Invoices → VAT / free-text note on invoices prints one free-text line directly under the VAT row on every invoice PDF, or in place of that row on an invoice that carries no VAT. Leave it empty to hide it. Quotes do not print it: the note is a statement an invoice makes about itself.
Nothing about it is jurisdiction-specific — you type the exact wording you need, whether that is the Austrian Kleinunternehmer clause, the German § 19 equivalent, a reverse-charge note, or something else. The totals block grows to fit, so a long note cannot collide with the figures.
Confirm the exact wording with your tax advisor. PicPeak prints what you type and does not check it against any jurisdiction’s rules.
Service date
On the beta channel from v3.144.2-beta.0, the invoice PDF has a service-date row. When the service date falls on the invoice date, Settings → CRM → Invoices → Service date equal to the invoice date decides whether the row says so in words (the default), prints the date again, or is left out; a service period always prints. See Admin Settings → CRM. Stable (v3.134.x) has neither the setting nor the row.
Per-customer feature override
customer_accounts.feature_bills toggle on the customer detail page hides the bills tab for that customer when off. Gating is master AND per-customer — see Customer accounts → Per-customer CRM toggles.