Accounting | Cine Power Planner

Accounting — Expense Tracking

Phase 2 of the Accounting domain — receipt capture, expense entry, category management aligned with German tax filings (Anlage EÜR + UStVA)

Visibility (production): As of v0.441.0 (#256) the entire Accounting surface (/accounting/* routes, accounting settings pages, the banking dashboard tile) requires a Pro subscription. Free accounts see an upgrade screen; existing accounting data stays stored and synced and remains exportable via Settings → Backup & Data → Backup (GoBD access preserved). Dev builds keep everything visible via the paywall dev bypass.

Overview

Accounting captures receipts (rental house invoices, fuel, lodging, meals, locations, office supplies) inside Cine Power Planner so producers stop juggling Lexware / sevDesk / spreadsheets and email threads. Each expense optionally links to one project and one invoice, carries a German SKR04 category, and is mapped to the lines on Anlage EÜR (Einnahmen-Überschuss-Rechnung) and the Vorsteuer-Kennziffern on UStVA (Umsatzsteuervoranmeldung) so the categorized expenses tunnel cleanly into the two German tax filings.

Three capture paths feed the same Expense entity:

  1. Scan — phone camera with auto-quad detection + OCR pre-fill (DE + EN).
  2. Upload — drag-drop or file picker; PDF (digital text + scanned), JPG, PNG, HEIC accepted; PDFs use a smart text-first extraction (pdfRasterizer) that falls back to OCR only when native text < 50 chars per page.
  3. Manual — blank form for cash receipts or missing-receipt scenarios.

Phase 2 ships the Expense entity, the ExpenseCategory entity, the Settings → Accounting tab for category management, and the Bewirtungsbeleg sub-feature (German hospitality-receipt compliance per § 4 Abs. 7 EStG). Capabilities that were originally deferred have since shipped as their own surfaces — travel per-diem (Spesen), DATEV export, bank-statement reconciliation (Banking), dunning / Mahnwesen (Invoices), and the GoBD cash book (Kassenbuch). Manual FX entry remains the rule — there is still no automatic ECB rate feed. See docs/features/expense-tracking/requirements.md for the original Phase 2 scope.

Overview dashboard

/accounting/overview (the Overview tile on the Accounting hub) is a read-only, cash-basis snapshot of your business for a period you choose — money actually received and paid, the same basis as the Anlage EÜR.

Pick a period. Presets cover This month, Last month, This quarter, This year (year-to-date) and Last year, plus a Custom from/to range. The choice is saved in the page URL, so you can bookmark or share a specific view. For any non-annual period a note reminds you this is a working view, not an official filing.

Six headline figures, each showing the change versus the previous comparable period and linking to its underlying list:

  • Income (received) and Expenses (paid) — gross cash in and out for the period.
  • Net profit — income minus expenses (cash basis).
  • VAT position — USt you collected minus Vorsteuer you paid (hidden if you are a Kleinunternehmer*in).
  • Open receivables — the total still owed to you across unpaid and partially-paid invoices, with overdue amounts called out. This is a snapshot "as of today", not bound to the selected period.
  • Cash flow (net) — bank inflow minus outflow for the period, plus a count of still-unmatched transactions.

Partially-deductible categories (Bewirtung 70 %). Because the Overview shares the Anlage EÜR's taxable basis, an expense whose category caps the deduction (deductibilityPct < 100 — geschäftliche Bewirtung is 70 %, durchlaufende Posten 0 %) counts toward Expenses, Net profit, and the category breakdown at its deductible amount, not its gross cash value: 70 % × net + the full Vorsteuer (§ 4 Abs. 5 EStG Nr. 2; the input VAT stays 100 % deductible per § 15 Abs. 1a UStG and is unchanged in the VAT position). A €119 business meal (€100 net + €19 VAT) therefore lowers profit by €89, not €119. The actual €119 cash outflow still appears in Cash flow and on the bank statement.

Below the cards, a month-by-month bar chart compares income against expenses, and an expenses-by-category breakdown ranks where the money went.

Every figure is scoped to the active company (for multi-company users). The Overview never exports — for the Anlage EÜR, UStVA, ZM, or DATEV files your accountant needs, follow the Export link to Reports.

Deleted invoices and expenses are excluded from every one of those — the Overview, the EÜR and UStVA filings, and (since v0.703.55) the DATEV Buchungsstapel. Deleting a row leaves a hidden tombstone so the deletion reaches your other devices; before that version the Buchungsstapel read it as a real document and sent your accountant a booking for something you had removed.

Profitability & BWA

/accounting/profitability (the Profitability & BWA tile) answers two questions the Overview can't: which projects make money, and what does a management profit-and-loss for this period look like. It has two tabs.

Project profitability (the default tab) is accrual-based — it counts what you have invoiced and incurred, regardless of whether the money has moved yet. For every project you see:

  • Revenue — the net total (excluding VAT) of that project's issued invoices (drafts and cancelled invoices are ignored).
  • Direct cost — the net total of expenses linked to the project.
  • Labor — approved and pending crew time-cost submissions for the project. Crew you've marked as own labour (Eigenleistung) in the project's Budget tab are not counted here: their fees stay in your margin, exactly like on the Budget tab, and show up as a small "+ … own labour" note under the labor amount instead.
  • Margin and Margin % — revenue minus direct cost minus labor, with a small bar (green when positive, red when negative).

Invoices and expenses that aren't linked to any project roll up into a single Overhead row, and a Total row reconciles everything. By default the table shows lifetime figures; tick Filter by period to narrow it to a chosen range. If your crew labor data can't be loaded (for example you're offline), the table still shows — margins simply exclude labor, with a note saying so.

BWA / P&L (the second tab) is a cash-basis management statement for a single period: revenue actually received, one line per expense category, the operating result, and the operating margin. It uses the same cash basis as the Overview and the Anlage EÜR. One-off fixed-asset disposals are left out so the statement reflects ordinary operations.

Like the Overview, this screen is read-only, scoped to the active company, and doesn't export — it's a steering view, not a filing.

Expense lifecycle

An expense's allowed status transitions depend on whether the linked project (if any) is owned by you. The status enum stores the literal value; UI labels and allowed transitions are computed at read time from (linkedProjectId, current user uid, project.owner_uid).

Self-mode — draft → final → paid

Used when the expense has no project link OR is linked to a project you own.

  • draft — fully editable. Default for new captures.
  • final — money fields lock (amount, currency, tax, vendor, receipt number, attachments). You can still edit linkedProjectId, linkedInvoiceId, and status. Demote back to draft via the "Unlock" button (creates a statusEvent).
  • paid — bookkeeping signal that you have personally settled the expense outside the app.

Collab-mode — draft → submitted → approved → reimbursed

Used when the expense is linked to a project you do NOT own (crew submitting for reimbursement by the producer).

  • draft — crew member is preparing the receipt.
  • submitted — crew member submitted. Money fields lock as in self-mode. Only the project owner can advance.
  • approved — project owner has reviewed and approved for reimbursement.
  • reimbursed — project owner OR the crew owner marks paid (owner self-marks when settled outside the app).

Transitions outside the valid set are rejected by the validator AND the Supabase RLS / status-transition trigger. Each transition writes a statusEvent ({status, at, byUid}) to the expense's append-only history array — audit trail visible on the detail panel for full transparency.

Capture flows

Scan receipt (phone camera)

  1. Tap Scan receipt on the Expenses list. The camera launches with quad-detect overlay (highlighting the paper edges).
  2. Hold steady — auto-shutter fires when the detected quadrilateral is stable.
  3. The image is compressed (compressImage ≤500 KB / 1024 px long edge), OCR runs in the background (Tesseract.js — DE + EN), and the editable form opens pre-filled with vendor, vendor address, gross amount, net amount, tax rate, tax amount, currency, receipt number, date, and (v0.337.0) payment method — tender-block evidence like Rückgeld/Gegeben selects Bar, a named card scheme (Girocard, VISA, EC-Karte…) selects Bank; only strong evidence flips the Bank default. Mixed-VAT receipts (7 % + 19 %) pre-fill the rate carrying the larger tax amount and always show the review chip. Each pre-filled field carries a per-field confidence chip — values below threshold show a "needs review" indicator.
  4. Edit anything that's wrong, fill in description / category / linked project, save.

Camera permission: Browser asks once. Native (Capacitor) builds use @capacitor-community/camera-preview — the system permission dialog appears the first time. If you decline, capture falls back to the upload flow.

Upload file

  1. Click Upload or drag a file onto the Expenses list.
  2. Accepted formats: PDF (digital text + scanned), JPG, PNG, HEIC. HEIC is auto-converted to JPG. Multi-page PDFs preserve all pages. Files >500 KB are compressed.
  3. PDF smart routing (v0.226.x scanner-reliability slug): native-text PDFs (Lexware / sevDesk / e-invoices) extract in <500 ms with no OCR cost. Scanned PDFs fall back to OCR. The 10-page cap counts pages that actually need OCR, not the pages in the file: a 50-page native-text e-invoice extracts in full with no warning, while a scan warns once its eleventh page would have to be OCR'd.
  4. Encrypted PDFs are rejected with a typed-error toast — remove the password locally and re-upload.
  5. "From cache" badge (v0.226.x): if the same blob (SHA-256 match) was previously OCR'd within the last 30 days, the cached text is returned instantly. A pill badge marks the result; click "Run again" to re-run OCR.

Where the receipt goes, and how to see it again (v0.721.0)

Every capture route keeps the document, and the expense shows it back to you.

  • Two copies, one of them untouched. The compressed image above is a reading copy — it keeps the page fast and it is what OCR reads. Since v0.721.0 the file you actually supplied is stored beside it, unmodified: the 12 MP photo, the iPhone HEIC, the original PDF. That is what GoBD means by Belege im Original, and it is the copy that still has the line items on a photographed A4 invoice. Before this version only the ≤1024 px reading copy survived.
  • The receipt appears on the expense. Open any expense and the Receipt card sits directly under the status row. Images render inline with click-to-zoom; PDFs open in an embedded viewer with the browser's own paging and zoom. When a separate original was kept, a Download original button appears beneath it.
  • It works offline and before the upload lands. A freshly captured receipt is served straight from the device's pending-upload queue. Once the upload reaches the cloud the app mints a short-lived signed URL instead — the receipts bucket is private, so nothing is ever served from a public link.
  • "Stored but could not be loaded" ≠ "no receipt attached". The first means the upload has not reached the cloud yet or you are offline; use Try again. The second means the booking genuinely has no document, and is what the GoBD Beleg fehlt chip on the list is counting.

Storage cost of the second copy. The offline upload queue holds five blobs. Keeping the original uses a second slot per receipt, so capturing many receipts offline can fill it. When that happens the original is skipped, not the expense — the booking still saves with its reading copy, and the app does not claim an original it never stored.

Covered surfaces: Scan receipt, Upload file, batch drag-and-drop, receipts and vendor bills arriving through the share sheet, and e-invoice import (which already stored the untouched file — it only lacked the durable link to it).

Manual entry

Cash receipts, missing-receipt scenarios, or anything you'd rather type directly. Click Enter manually to open a blank form. All the same fields are available; attachment is optional. A soft warning surfaces on the detail panel when kind='receipt' has no attachment — non-blocking; you can dismiss or attach later.

Bewirtungsbeleg compliance

When a Bewirtung category (SKR04 6822 geschäftlich or 6820 betrieblich) is chosen, a dedicated Bewirtungsbeleg fieldset appears on every capture surface (Manual / Upload / Scan / Detail). The fieldset captures the § 4 Abs. 7 EStG required record-keeping:

FieldPurpose
Typegeschäftlich (customer / partner — 70 % deductible) vs betrieblich (own employees — 100 % deductible)
AnlassConcrete business reason. Generic phrases like "Geschäftsessen" alone are rejected by the Finanzamt — be specific.
TeilnehmerFull names of every attendee, including you, the host.
OrtPlace of hospitality (restaurant). Usually = vendor; filling either is sufficient.
TrinkgeldTip in the original currency. Toggle "printed by restaurant" if it's on the bill (not handwritten).
UnterschriftAttestation checkbox + optional digital signature drawn on the scanned receipt. Hard legal requirement — the Finanzamt can disallow the deduction without a signed paper receipt.

Soft-warning banner

The detail panel renders a non-blocking soft-warning banner listing every gap:

  • Missing type / Anlass / Teilnehmer / Ort
  • Generic Anlass (matched against denylist: "Geschäftsessen", "Business meeting", "Mittagessen", "Lunch", "Meeting", "Kaffee", …)
  • Missing signature attestation
  • Bill > €250 gross hint — additionally the § 14 UStG Rechnungs-Anforderungen apply (itemized line items, restaurant Steuernummer / USt-IdNr., host name printed by the restaurant — handwritten is not enough per BMF 30.06.2021)

The validator never blocks save on incomplete Bewirtungsbeleg state — it only shape-validates the bewirtung sub-object. Drafts can be filed with attendee lists missing and filled in later.

Branches at a glance

BranchSKR04EÜR ZeileUStVA KZBA-AbzugUse for
geschäftlich6822436670 %Bewirtung von Geschäftspartnern / Kunden / Lieferanten / freien Mitarbeitern
betrieblich68202766100 %Bewirtung der eigenen Arbeitnehmer (Betriebsfeier, Mitarbeiteressen, Schulungen, Außendienst)

Vorsteuer: For both branches, the input-VAT (Vorsteuer) is fully deductible — only the Betriebsausgabe is cut to 70 % on geschäftlich (§ 4 Abs. 5 EStG Nr. 2).

Settings → Accounting

Open Settings → Accounting to manage your expense categories. On first sign-in the app seeds 37 SKR04 categories tailored for film production, each carrying:

  • code — SKR04 numeric code (DATEV-compatible)
  • nameDe / nameEn — bilingual label
  • defaultTaxRate — 0 / 7 / 19 (UStG-Sätze)
  • euerLine — Zeile on Anlage EÜR (e.g. 44 for Reisekosten Unternehmer, 43 for Bewirtung 70 %)
  • ustvaKey — Vorsteuer-Kennziffer on UStVA (e.g. 66 standard, 84+67 for § 13b reverse-charge, null for VAT-exempt)
  • deductibilityPct — BA-Abzug % (default 100). Two built-ins depart from it: 6822 geschäftliche Bewirtung at 70, and 1370 Durchlaufende Posten at 0 — a pass-through billed on to a client is not your expense at all, so none of it reduces your profit.

Available actions

  • Edit any built-in row — change label, tax rate, EÜR line, UStVA Kennziffer, deductibility %. The migration backfill preserves your customizations when the app pulls in new built-ins from the seed.
  • Hide built-ins you don't use — they survive but stop appearing in pickers. Use the "Show hidden" toggle to bring them back.
  • Add a custom category with your own code / labels / metadata. Customs are hard-deletable.
  • Restore default categories — non-destructive merge. Re-applies the SKR04 defaults; existing categories are not modified, only missing built-ins are appended. Use this if you've hidden a row and want it back, or after a major release that adds new built-ins.

Tax-form badges

Each row displays three coloured badges:

BadgeMeaning
EÜR Z<n>Line on Anlage EÜR — where this category lands on the EÜR form for tax filing
UStVA KZ <key>UStVA Vorsteuer-Kennziffer — where the input tax lands on the monthly / quarterly UStVA filing
<pct>% BADeductible portion of the gross amount as Betriebsausgabe (only shown when < 100 %70 % BA on Bewirtung 6822, 0 % BA on Durchlaufende Posten 1370)

Hover any badge for the inline explanation in your locale.

Migration from v0.224.0

If you signed up before v0.225.0, the original 12-row seed was missing euerLine / ustvaKey / deductibilityPct metadata. On next hydration useExpenseCategoriesSeed runs automatically and:

  1. Backfills the metadata onto your existing built-ins by code-match.
  2. Appends the 16 new built-in codes added in v0.225.0 (Sammelposten 0675, AfA 0440, Fremdleistungen 5900, § 13b Software 5925, separate Studio-/Locationmiete 6320, Reisekosten Arbeitnehmer 6650, tatsächliche Verpflegung 6673, Kfz aufgeschlüsselt 6520/6540, Geschenke 6610, Werbekosten 6600, Bürobedarf 6815, IHK 6410, Bankgebühren 6855, etc.).

Your customizations are preserved — renamed labels, overridden defaultTaxRate, hidden flag, user-set euerLine / ustvaKey / deductibilityPct are never overwritten. Backfill only fills what's missing.

Multi-currency

Each expense stores currencyOriginal (ISO 4217 — EUR, USD, GBP, CHF, SEK, DKK, NOK, PLN, CZK, JPY, or any free-typed ISO code), amountOriginal, and a user-entered fxRate. The app computes and stores amountEur at entry time for historical stability — you can re-enter the FX rate when finalizing if your bank's settlement rate differed from what you typed at capture.

No external FX API in Phase 2 — the user enters the rate. Producers can reject a submitted expense if the rate is clearly wrong (the value is prominently displayed in their view of submitted expenses).

Fixed-Asset Register (Anlageverzeichnis)

Equipment you own and use over several years is written off against tax over its useful life — AfA (Absetzung für Abnutzung). The Fixed-Asset Register at Accounting → Fixed assets (/accounting/assets) is your formal Anlageverzeichnis: for a chosen tax year it lists every business asset with acquisition cost → accumulated AfA → current Restbuchwert (book value) + status, covering owned gear and dedicated non-gear assets (a vehicle, computer, furniture …), handles disposals (Abgänge), and posts the year's AfA into the Anlage EÜR. (This replaces the former standalone AfA panel; AfA posting works exactly as before, now spanning gear and dedicated assets.)

How an asset is classified

Routing is automatic, by net acquisition price:

Acquisition price (net)TreatmentEÜR Zeile
≤ €800 (GWG)Sofortabschreibung — full write-off in the acquisition year, €0 thereafter31
> €800Lineare AfA — straight-line to €0 over the useful life (depreciation_years), pro-rata in year 129

Each owned-gear unit depreciates from its own purchase_date and purchase_price; multiple units of one item depreciate independently and sum into the year's figure. Items past their useful life show €0 and the status fully depreciated.

Dedicated (non-gear) assets + mid-life adoption

Click Add asset to register a non-gear business asset (name, class, net cost, acquisition date, useful life). For an asset you bought before you started bookkeeping in the app, tick mid-life adoption and enter its opening book value + AfA already taken as of a year — the schedule continues forward from there and prior years are never recomputed. Dedicated assets are company-scoped, synced, editable and deletable (deletion is confirmed first).

Disposals (Abgang)

Use the Record disposal action on any row (gear or dedicated) to mark it sold, scrapped, or withdrawn to private use:

  • AfA stops pro-rata at the disposal month, and the Restbuchwert at disposal is computed.
  • Sold: the net proceeds book as business income (EÜR line "Veräußerung Anlagevermögen"), the Restbuchwert books as an expense — the net is your gain/loss; the sale VAT flows to the UStVA.
  • Scrapped: the full remaining Restbuchwert is a loss.
  • Withdrawn (Privatentnahme): booked at book value (no gain/loss — an Entnahme isn't a deductible loss).

Disposals are reversible — Remove disposal undoes the booking.

Post a year's AfA

  1. Pick the tax year (defaults to the last closed year — AfA is booked per calendar year).
  2. In the Depreciation (AfA) panel, click Post AfA and confirm. One 0 %-VAT expense is posted per asset — amount = the year's AfA, dated 31 Dec, described AfA <year>: <name>, tagged auto-generated, and linked to its gear item or dedicated asset.
  3. The posted AfA appears on the EÜR (line 29 linear / line 31 GWG). Posting is idempotent — re-posting replaces the year's set, never duplicates; Remove deletes it. Manual expenses you entered yourself are never touched. The disposal-year AfA respects the pro-rata cutoff, so it never double-counts with the Restbuchwert write-off.

Export

From the register (or the Anlageverzeichnis panel on Accounting → Reports), export the formal register for a tax year as a PDF (register + Abgänge + totals) or an accountant-/DATEV-friendly CSV. The EÜR export ZIP also gains an anlagenabgang-<year>.csv disposal audit trail.

How an asset is classified (recap)

Routing is automatic by net acquisition price — ≤ €800 ⇒ GWG (full write-off in the acquisition year, EÜR line 31); > €800 ⇒ linear (straight-line to €0 over the useful life, pro-rata in year 1, EÜR line 29). Multi-unit gear depreciates each unit from its own purchase date.

Linked projects and invoices

  • One project link per expense. If a single receipt spans multiple projects, create separate expenses with manually-split amounts.
  • One invoice link per expense. Expenses linked to a project show up on that project's Billing tab.
  • "Add from expenses" picker on an invoice draft surfaces all unbilled expenses linked to the same project — click to append them as InvoiceLineItems (category: 'extra', sourceRef: {kind: 'expense', id}).

Offline behavior

  • Capturing while offline saves the receipt to IndexedDB and queues the upload via enqueueStorageUpload.
  • The expense shows a local blob: URL thumbnail until sync resolves.
  • On reconnect, the queue drains; the signed cloud URL replaces the local URL; the expense entity is updated.
  • The offline queue holds five receipts, not fifty. MAX_PENDING_UPLOADS is 5, and the 25 MB MAX_BLOB_BYTES cap is per blob, not a total budget. The sixth capture is refused with a quota warning until the queue drains, so a long stretch without signal is not the place to bank a whole day of receipts — reconnect periodically and let the queue empty.
  • Keeping the untouched original (above) consumes a second slot per receipt, so in practice a fully offline run fills after two or three captures. When a slot is unavailable the original is skipped and the expense still saves with its reading copy.

Storage & privacy

  • Receipts live in a private Supabase Storage bucket (receipts) under your uid prefix: <uid>/<expenseId>.<ext>.
  • Reads use short-lived signed URLs (1 h expiry) minted by the get-receipt-signed-url edge function after the RLS check.
  • The bucket has no public access — there is no permanent URL for any receipt.
  • RLS policy: you see your own expenses and any expense linked to a project you own. Non-owner project members do not see your receipts even if they have access to the project itself (contracts-style isolation).
  • Authentication — auth model + project ownership
  • Billing & Subscription — Stripe tiers (separate from this accounting surface)
  • Projects — the project-link target
  • Cloud Sync Consent — how receipts traverse the sync engine
  • Developer specs: docs/features/expense-tracking/requirements.md, docs/features/scanner-reliability/status.md

Last Updated: 2026-09-10 Version: 0.790.2