Steuer-Manager
Generation workspace for UStVA transfer aids and EÜR drafts based on client or uploaded documents.
What it does
tax-manager is the bundled Finanzen module for generating two reviewable tax-work products from documents:
- a UStVA overview for manual transfer into ELSTER, for one month or quarter
- an EÜR draft for the tax adviser, for one calendar year
The UStVA output is a transfer aid only. The module does not use ERiC, does not submit or file anything with ELSTER or a tax authority, and does not replace the checks required in ELSTER. The EÜR output is a draft for review by a tax adviser, not a completed tax return.
The module is generation-focused: users select source documents, configure the output, let the specialized agent extract and classify every document, and receive deterministically calculated HTML and PDF output. It is not an account, category, bookkeeping, or document-matching workspace.
How to open and enable
- An admin installs the module via
POST /api/module-store/install-bundledwith slugtax-manager, or uses the install action in Module Store. - Module migrations run once and are tracked in
module_schema_migrations. - The module appears as
Finanzenin the launchpad and opens at/modules/tax-managerwith its React renderer insrc/components/modules/tax-manager/.
Migrations 001 through 004 remain in the repository and their legacy tables may remain in existing databases so deployments do not lose data. The active generation workflow does not read or write those tables. Migration 005_generation_workspace.sql creates the current data model; migration 006_reseed_specialized_agent.sql updates the bundled steuer-manager specialist to the current four-tool contract.
Workflow
The UI has two top-level views. Übersicht combines a compact upload area with a searchable, month-grouped list of every visible beleg, rechnung, and ust document plus every visible file stored in the global Steuerbelege folder. Uploads may be assigned to a Mandant before transfer. Berichte remains the generation step machine with its persistent history rail:
- Source: optionally choose a Mandant, link a folder, and/or drop files/folders in the always-visible upload area. A selected Mandant is clearable. Dropped files are uploaded into the central
dokumentearchive and assigned to the globalSteuerbelegefolder; when a Mandant is selected, it is sent with each upload. Ordner verknüpfen opens a picker with two tabs: Dokumente (the visibledocument_folderstree) and Google Drive (the caller's own Drive mirror, navigable with breadcrumbs and a folder-name search; only Drive-enabled accounts of the current user). The link is stored per user intax_manager_settings.source_folder_jsonand stays preselected for later reports until it is removed. On Weiter with a linked Google Drive folder, not-yet-imported importable files below that folder are imported into Dokumente in batches of 10 throughPOST /api/integrations/google/drive/items/import-folder, with a progress bar and Überspringen. - Configure: choose UStVA or EÜR and the applicable period. UStVA accepts a month or quarter; EÜR accepts a year. When a Mandant is selected, the module merges matching
beleg,rechnung, andustdocuments from the selected period with the files uploaded during the current source step. A linked folder adds every visible document in that folder and its subfolders whose date falls in the period, whatever its document type (the folder was chosen on purpose). The date is the extracteddatum; for Google Drive documents without one yet it falls back to the Drive modification time, otherwise to the import time. The candidate list notes the folder and how many of its files are still not imported. Session uploads remain visible, are deduplicated by document ID, and start pre-selected. - Generate: the module runs every selected file through the shared
scripts/document-extract.mjspipeline with bounded concurrency. The extractor reuses the file/config-hash OCR cache and performs local text/PDF extraction, Tesseract OCR, and configured vision fallback. Its capped text, method, and vision flag are stored with the generation document before the heuristic first pass. Extraction failures remain non-fatal: the item stayspendingwithextraction_failed. Thesteuer-manageragent then classifies every document, including unreadable, excluded, or duplicate items. - Calculate and render: the agent does not calculate totals. The module applies deterministic cent-based calculations, creates Prüfhinweise, and renders the stored HTML report.
- Review: the result view shows the report preview beside a compact Quelldokumente list with status, gross amount, quick view, and original-file download. The same preview/download actions are available in Übersicht and the candidate list. Deleted source documents retain their line-item snapshot without actions. Prüfhinweise remain in the generated HTML report. A correction is saved through the line-items endpoint and followed by another deterministic finalization.
- Extend a report: the result toolbar has Neu synchronisieren (when the report has a Mandant or source folder) and Dokumente hinzufügen. Both open one dialog: the sync section first imports new files below a Google Drive source folder (batched, with progress and skip) and then lists documents of the report's Mandant and/or source folder in its period that are not part of it yet (
GET /generations/:id/new-candidates); the upload section stores local files first in the report's linked folder, which is a Google Drive upload into that Drive folder followed by the import (POST /api/integrations/google/drive/items/upload), a Dokumente upload into that folder, or theSteuerbelegefolder for reports without a folder. The selected documents are added throughPOST /generations/:id/documents, andrun-agentstarts withdocument_idsso the specialist classifies only the new documents; existing and manually reviewed line items stay untouched, and the report is finalized again.
The page context published through onContextChange contains the selected mandantId, the linked sourceFolder (documents: or google_drive: plus its path), generationId, workflow step, report kind, and top-level view (overview or reports) so chat and live-agent surfaces can understand the open workspace.
Both upload surfaces use POST /api/documents/upload with folder_id set to the tax_folder_id returned by bootstrap. The handler ensures a root-level global folder named Steuerbelege exists using an insert-with-conflict/re-select sequence, so concurrent first loads converge on the same folder ID.
Clients
Web
The React module described above (src/components/modules/tax-manager/).
iOS and macOS
The Apple clients have a native SwiftUI Steuer-Manager screen in the side menu (MainAppSection.taxManager, route /modules/tax-manager, shown only while the tax-manager module is installed; Views/TaxManagerView.swift and the TaxManager* files next to it). It runs against the same module API and run-agent route and never loads the web page. The Übersicht / Berichte switch sits in the title bar on macOS and in the controls row on iPhone and iPad.
- Übersicht lists the tax documents grouped by month with sticky headers, filtered by the title search and the contact filter. Tapping a row downloads the original file and opens it in Quick Look. Download saves it through the save panel on macOS and uses Quick Look's share action on iOS. The upload area (optional contact, file and folder pickers, drag and drop on macOS and iPad) is a trailing column on macOS and iPad and the first section on iPhone. Folder uploads keep their relative paths as titles and skip hidden files such as
.DS_Store. - Berichte shows the generation history grouped by contact, with Neuer Bericht, delete by swipe or context menu, and a confirmation. On macOS and iPad the wizard is a detail pane next to the history. Opening a stored generation on macOS collapses the history column, and the title-bar toggle brings it back. On iPhone, a new report or a history row opens the wizard as a full-screen view.
- The wizard has the same four steps as the web: source (contact search, linked folder, and upload), configure (report kind, month or quarter, year, and the pre-selected candidate documents with Quick Look), generating (the agent's live tool statuses and document statuses, polled every 2.2 s, with Erneut versuchen after a failure), and result. The result shows the rendered HTML report, the source documents, the Prüfhinweise sheet, the PDF download, and the collapsible line items. A line item opens an editor for direction, Kennzahl, EÜR category, and amounts. The editor is a sheet on macOS and iPad and a full-screen view on iPhone. Speichern und finalisieren saves every line item as a manual review and finalizes again.
- The source step's Verknüpfter Ordner section links a Dokumente or Google Drive folder through the same settings API. Ordner verknüpfen opens the folder picker as a sheet on macOS and iPad and as a full-screen view on iPhone: a Dokumente | Google Drive switch, the indented Dokumente folder tree, and the Drive mirror's folders (account switch only with more than one Drive account, folder-name search with the parent folder as subtitle, breadcrumbs on macOS and iPad, one pushed screen per folder on iPhone). Rows and the current Drive folder have a Wählen control; the footer shows the selection and Ordner verknüpfen. The linked folder shows as a chip (
Google Drive · pathorDokumente · path) with a remove button and stays preselected for later reports. Weiter is enabled with a contact, uploads, or a linked folder; with a Google Drive folder it first imports the not-yet-imported files in batches of 10 with a progress bar and Überspringen. The configure step adds the folder's documents to the candidates and notes the folder path and the number of files still not imported. - The result header adds Neu synchronisieren (only for reports with a contact or source folder) and Dokumente hinzufügen, both disabled while the agent works on the report. They open the same add-documents view (
Views/TaxManagerAddDocuments.swift) as a sheet on macOS and iPad and as a full-screen view on iPhone. Neu synchronisieren starts the scan right away; Dokumente hinzufügen waits for Jetzt suchen or uploads. The scan imports new files of a Google Drive source folder first (batches of 10 with progress and Überspringen) and then listsGET /generations/:id/new-candidates, all preselected. Uploads go into the report's Google Drive folder (POST /api/integrations/google/drive/items/upload), its Dokumente folder, orSteuerbelege, and join the list preselected. Each row has a checkbox, date, amount, and Quick Look. {count} hinzufügen und neu berechnen posts/generations/:id/documents, startsrun-agentwith only the addeddocument_ids, and switches to the generating step; Erneut versuchen there stays limited to those documents. If nothing new was added, the result reloads. If the agent does not start, the sheet stays open with the error and the next confirm retries the same run. - The gear button edits the per-user agent model (
agent_model_ref; Standard uses the runtime default).
The screen publishes the same page context as the web module (moduleSlug: tax-manager, mandantId, sourceFolder, generationId, step, kind, view), so chat and live agents opened from it get the tax_manager_* tools. All strings use the shared German, English, and Italian taxManager.* keys.
Calculation rules
UStVA Kennzahlen subset
The first version supports only the following practical subset:
| Kennzahl | Meaning in this module | Deterministic treatment |
|---|---|---|
81 | Taxable revenue at 19% | Adds classified net and VAT cents |
86 | Taxable revenue at 7% | Adds classified net and VAT cents |
35_36 | Revenue at other tax rates | Adds classified net and VAT cents; rendered as 35/36 |
66 | Deductible input VAT on expenses | Adds classified VAT cents as input VAT |
83 | Zahllast | Calculated as VAT from 81 + 86 + 35_36 minus VAT from 66 |
sonstige | Not supported or not clearly assignable | Listed separately, excluded from all sums, and emitted as a Prüfhinweis |
Kennzahlen 48, 61, and 89 are out of scope in this version. Users must verify the supported subset and transfer the reviewed figures manually into ELSTER.
EÜR categories
The exact euer_category values accepted by reporting.mjs are:
| Direction | Values |
|---|---|
| Income | umsatz_19, umsatz_7, umsatz_steuerfrei, sonstige_einnahmen |
| Expense | waren_material, fremdleistungen, personal, raumkosten, versicherungen_beitraege, kfz, reise, werbung, telekommunikation_internet, buerobedarf, fortbildung_fachliteratur, bewirtung, geschenke, sonstige_ausgaben |
| Computed/reserved | gezahlte_vorsteuer is accepted by the line-item schema, while paid input VAT is calculated from expense VAT and rendered as its own report line |
For classified line items without a supported category, the calculator falls back to sonstige_einnahmen or sonstige_ausgaben according to direction. It totals net income plus received VAT, net expenses plus paid input VAT, and calculates Gewinn/Verlust as total income minus total expenses. The output is an adviser-facing draft; special cases such as Kleinunternehmer treatment remain outside this version's automated scope.
AfA handling
Line items marked afa_candidate are listed separately for tax-adviser review. They are excluded from the EÜR income and expense sums, contribute to the AfA candidate count and informational net total, and produce an afa_candidate Prüfhinweis. The marker is a review signal, not an automated depreciation decision.
Prüfhinweise
The deterministic report collector can emit these codes:
| Code | Trigger |
|---|---|
unreadable_document | Document status is unreadable |
pending_document | Classification is still pending |
possible_duplicate | The item is marked duplicate, carries that issue, or repeats counterparty + invoice number + gross amount |
missing_vat | A classified item has no VAT amount or carries that issue |
unassigned_kennzahl | UStVA classification is sonstige |
afa_candidate | The item requires AfA review |
date_outside_period | Document date is outside the configured period or the issue was submitted explicitly |
gross_net_mismatch | Net plus VAT differs from gross by more than 2 cents, or the issue was submitted explicitly |
vat_rate_mismatch | Calculated VAT from net and rate differs from the supplied VAT by more than 2 cents |
Additional submitted issue strings are preserved and shown with a generic message. Inconsistencies become review notes; the calculator never silently changes source amounts.
API reference
Base: /api/modules/tax-manager/api. Generations are workspace-shared work products by default: reads and writes are visible to every authenticated user (visibility_scope = 'shared'), plus any explicitly private generation owned by the calling user. owner_user_id remains for attribution only and is no longer a hard access filter, matching how dokumente is scoped.
| Method and path | Contract |
|---|---|
GET / | Returns module identity, legal_notice, and the active endpoint inventory |
GET /health | Returns { ok: true, module: "tax-manager", status: "running" } |
GET /bootstrap | Ensures the global Steuerbelege folder and returns its UUID as tax_folder_id together with up to 500 Mandanten, the 50 most recent workspace-visible generations, per-user settings, and legal_notice |
GET /settings | Returns { settings: { agent_model_ref, source_folder } } for the authenticated owner; both default to null. source_folder is the linked folder resolved to { kind, id, account_type, name, path }, or null when none is linked or it no longer exists or is no longer visible |
PUT /settings, POST /settings | Patches { agent_model_ref?: string | null, source_folder?: { kind: "documents" | "google_drive", id, account_type? } | null } for the authenticated owner; omitted fields keep their value. Model values are trimmed and capped at 240 characters; an empty value stores NULL and inherits the normal runtime default. A folder must be a visible Dokumente folder or a folder of the caller's own Drive mirror (404 otherwise); null removes the link |
GET /overview-documents?q=…&mandant_id=…&limit=… | Returns visible beleg, rechnung, and ust documents plus visible documents in Steuerbelege, including untyped folder entries. Returns tax_folder_id and document id, titel, type/date/amount/currency, Mandant identity, and in_tax_folder; q filters titles with ILIKE, mandant_id filters primary or related Mandanten, and limit defaults to 200 with a maximum of 500 |
GET /candidate-documents?mandant_id=…&folder_kind=…&folder_id=…&folder_account_type=…&kind=…&year=…&period_kind=…&period_index=… | Requires mandant_id and/or a folder (folder_kind documents or google_drive, folder_id, optional folder_account_type user/agent); resolves the month, quarter, or year. Mandant candidates are accessible beleg, rechnung, and ust documents linked to that Mandant; folder candidates are all accessible documents in the folder subtree (Dokumente) or imported from files below the Drive folder, dated by datum, then the Drive modification time, then the import time. Results are merged and deduplicated; source_folder returns the resolved folder with pending_import_count (importable Drive files not yet in Dokumente). month/quarter are accepted aliases for period_index; limit defaults to 200 and is capped at 500 |
POST /generations | Body: { kind, period_kind, year, period_index?, mandant_id?, source, source_folder?, document_ids[] }. kind is ustva or euer; source is mandant, upload, or folder. Mandant source requires mandant_id; folder source requires an accessible source_folder, whose resolved path is stored in source_folder_json; UStVA requires month/quarter and EÜR requires year. One to 500 accessible document IDs are required. Runs the shared cached OCR/vision extractor (maximum three files concurrently, 60-second per-file timeout), stores capped extraction output, and creates a draft generation with a heuristic line-item baseline |
GET /generations?mandant_id=…&kind=…&year=…&limit=… | Lists workspace-visible history (shared generations plus the caller's own private ones) newest first; default limit 50, maximum 200 |
GET /generations/:id | Returns one workspace-visible generation, all source-document line items, and legal_notice. Each document includes extracted_text (capped to 12,000 characters), extraction_method, and extraction_used_vision |
GET /generations/:id/new-candidates | Documents of the generation's Mandant and/or its source_folder_json folder in the generation period that are not part of it yet (same shape as candidate documents), plus source_folder with pending_import_count and has_sources; limit defaults to 200, max 500 |
POST /generations/:id/documents | Body { document_ids[] } (max 200 per call, 500 per generation). Adds accessible documents that are not in the generation yet: runs the shared extractor, stores heuristic baseline line items, leaves existing items untouched, and moves a finalized generation back to extracted. Returns { generation, added, skipped_existing }; 409 while the agent is running (extracting) |
POST /generations/:id/line-items | Bulk-upserts line_items (alias items), each with a source document_id. Accepts statuses pending, classified, unreadable, excluded, duplicate; directions income, expense; supported EÜR categories and UStVA Kennzahlen; non-negative integer cent amounts; VAT rate 0–100; confidence 0–1; issues, extraction source, and notes. Sets generation status to extracted |
POST /generations/:id/finalize | Re-runs deterministic UStVA/EÜR calculations, stores totals/Kennzahlen/Prüfhinweise/HTML, and sets status finalized. It is safe to run again after corrections |
GET /generations/:id/html | Returns the stored finalized report as private, non-cached text/html; returns 409 before finalization |
GET /generations/:id/pdf | Renders the stored finalized HTML as an A4 PDF download; returns 409 before finalization |
DELETE /generations/:id | Deletes a workspace-visible generation (shared, or owned and private); linked generation-document rows cascade |
POST /api/modules/tax-manager/run-agent is outside the bundled-module API base. It accepts { generation_id, document_ids? } (document_ids: up to 200 IDs already in the generation; the run then classifies only those documents and must not re-report existing line items), resolves steuer-manager, creates a dedicated new chat session per run (titled e.g. Steuer: UStVA-Übersicht Q2 2026 – <Mandant>, never the user's Hauptchat), creates the background specialist task in that session, marks the generation extracting, and returns the task and chat/pending-message IDs. The owner's agent_model_ref is copied to the dedicated session and passed as the specialist task's fallback model. Migration 007_module_settings.sql clears the bundled specialist's hardcoded default so this per-user fallback can apply; if an admin later sets a specialist default in the Agents panel, that default intentionally takes precedence. GET /api/modules/tax-manager/run-agent?task_id=… returns task state, tool statuses, and an optional reply preview for the generating step.
Data model
The active generation tables are created by bundled-modules/tax-manager/migrations/005_generation_workspace.sql; migration 007_module_settings.sql adds the per-user settings table, migration 008_generation_document_text.sql adds the shared-extractor snapshot fields, migration 010_folder_source.sql adds the folder source with source_folder_json on generations and settings, and migration 009_generation_visibility.sql adds visibility_scope (shared/private, defaulting to shared) so existing and new generations are workspace-visible work products by default, consistent with dokumente:
| Table | Purpose | Notable columns |
|---|---|---|
tax_manager_generations | Workspace-visible (or owner-private) report generation and rendered result | mandant_id, source (mandant/upload/folder), source_folder_json (folder used by a folder-source generation), kind (ustva/euer), period fields, status (draft/extracting/extracted/finalized/failed), visibility_scope (shared/private, default shared), agent_task_id, totals_json, kennzahlen_json, pruefhinweise_json, html, error/finalization timestamps |
tax_manager_generation_documents | Per-source-document extraction and classification | generation_id, nullable document_id, filename snapshot, status, direction, euer_category, kennzahl, integer cent amounts, VAT rate, document date, counterparty/invoice number, afa_candidate, confidence, issues, extraction source, notes, extracted_text, extraction_method, extraction_used_vision |
tax_manager_settings | Per-user module settings | Text owner_user_id primary key, nullable agent_model_ref, nullable source_folder_json (linked source folder { kind, id, account_type }), created/updated timestamps |
The (generation_id, document_id) pair is unique while document_id is present. Deleting a generation cascades to its line items; deleting a central document keeps the line-item snapshot and sets document_id to NULL.
Legacy tables introduced by migrations 001 through 004 remain in deployed databases for compatibility and data preservation, but the current handler and UI do not use them. They must not be treated as the active Steuer-Manager model.
Specialized agent and tools
The bundled specialized agent has handle steuer-manager and uses the clapilot-tax-manager plus clapilot-document-extraction skills. Its current Steuer-Manager tools are:
| Tool | Purpose |
|---|---|
tax_manager_list_generations | List owner-scoped generations with optional Mandant, kind, and year filters |
tax_manager_get_generation | Load one generation and every current source-document line item, including ready-to-classify extracted_text and extraction metadata |
tax_manager_submit_extraction | Submit bulk per-document extraction/classification results; no source document may be silently omitted |
tax_manager_finalize_generation | Invoke deterministic calculation and HTML rendering, then request a module refresh |
tax_manager_find_new_documents | List documents of a generation's Mandant and/or source folder in its period that are not part of it yet (read) |
tax_manager_add_documents | Add documents to an existing generation with extractor text and a heuristic baseline; the caller then classifies only those and finalizes again |
The specialist also receives documents_list, documents_get, mandanten_list, and mandanten_get. It treats extracted_text as the primary source, calls documents_get only when that field is missing or empty, and marks a document unreadable only if both sources have no usable content. Live Voice intentionally exposes only the two read tools; extraction and finalization belong to the specialized-agent workflow. The complete tool contracts are documented in Agent Tool Contracts.
Legal and operational boundaries
- Every report carries: “Dieser Bericht ist ein Entwurf und dient ausschließlich der Vorbereitung. Er stellt keine Steuerberatung dar und wird weder an ELSTER noch an Finanzbehörden oder andere Dritte übermittelt.”
- No endpoint performs ERiC integration, ELSTER submission, tax-office filing, or transmission to a third party.
- Agent classifications, OCR/vision output, Kennzahlen, categories, AfA candidates, dates, and amounts require human review.
- UStVA output is only a manual ELSTER transfer aid. EÜR output is only a draft for the tax adviser.
- The operator remains responsible for legal basis, access control, retention, data-processing agreements, and GDPR compliance for Mandanten data.
Troubleshooting
- No Mandant documents are offered: the document must be
beleg,rechnung, orust, fall inside the selected period bydatumor creation date, be accessible to the user, and be linked throughdokumente.mandant_idordocument_mandanten. - A file is missing from Übersicht: it must be visible to the user and either use a tax-candidate type or belong to the global
Steuerbelegefolder. - Generation does not advance: inspect the run-agent task status and confirm that
steuer-manageris enabled with the current four-tool allowlist. - A document cannot be read: submit it as
unreadablewith issues and notes; do not omit it. The finalized output will retain a Prüfhinweis. - HTML/PDF returns
409: finalize the generation first. After editing line items, finalize again to refresh deterministic totals and rendered output.
