Add user-defined invoice set structure dialog (ADR 0016)
Playwright Tests / test (push) Canceled after 0s
Playwright Tests / test (push) Canceled after 0s
Lets a back-office user correct/define which invoice line belongs to which "set" and how deep it is nested, instead of relying only on the implicit "everything after a Set header until the next Set" grouping fds__prepInvoice derives. Sets may contain sets; every change applies immediately to the cached draft session and is reflected in the editor and PDF (ADR 0006/0008). Backend: - InvoiceSetStructure: pure parent-link forest (assign, change level, create, place/move-sibling, depth-first linearise + level recompute). - InvoiceDraftSetStructureDeltas: new inv/dpatch targets set.assign, set.level, set.create, set.move. - InvoiceSetPricing.Build is now order-preserving and nesting-aware (governed by the nearest converted ancestor); the set-item switch sums a header's transitive descendants so nested conversions never double-count. - FuchsPdf indents nested set members per level, mirroring the editor. Frontend: - New fis.inv_sets.js: "Set-Struktur bearbeiten" dialog — per-block table of all lines with drag & drop reorder (before/after/into), ▲▼ order and ◀▶ level buttons, fold/unfold per set (view-only), and a create bar for new Set/Text lines. Posts one delta per action; the dialog is a pure view of the server state (ADR 0006). - _Layout.cshtml / fis_main.js: cache-bust the dynamically imported per-module bundles (web/fis.<mdl>.<locale>.js/.css) with an asset version derived from the newest bundle write time, so a gulp rebuild is picked up without a stale cached module. Docs: ADR 0016, live-draft-editing concept, INVOICE_SET_PRICING.md, USER_GUIDE.md, ARCHITECTURE.md updated. Tests: InvoiceSetStructureTests (new) + additions to InvoiceDraftServiceTests/InvoiceSetPricingTests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -177,6 +177,8 @@ disable/failure). See [`Concepts/erechnung-output.md`](Concepts/erechnung-output
|
||||
Stateless services (`IPdfService`, `IBankingService`, `IMfrClientFactory`) are singletons; DB/request-scoped services are scoped (see `Program.cs`).
|
||||
The **Admin** module (`Do_Process_Admin`, `ISystemStatusService`) surfaces a live system-status/diagnostics page (host, SQL/Key Vault/blob/MFR connectivity, email config, test-email) restricted to `fds_sys` > 4 — see ADR [0011](Decisions/0011-admin-module-system-status.md) and the [concept doc](Concepts/admin-system-status.md).
|
||||
`FdsInvoiceData` / `FdsReminderData` are now **pure data holders** (parse + properties); loading, persistence and PDF generation live in the services (fully async — no `Task.Run(...).Wait()`).
|
||||
|
||||
Invoice/reminder drafts are edited **backend-authoritatively**: the truth is an in-memory `InvoiceDraftSession` mutated by single deltas (`IInvoiceDraftService`), including the user-defined **set structure** (`InvoiceSetStructure` — parent links, nesting, levels; ADR [0016](Decisions/0016-user-defined-set-structure.md)) and the set-price operations (`InvoiceSetPricing`, ADR [0009](Decisions/0009-block-setprice-modes-are-irreversible-mutations.md)). See the [live draft editing concept](Concepts/live-draft-editing.md) and ADRs [0006](Decisions/0006-backend-authoritative-draft-editing.md)/[0008](Decisions/0008-invoices-and-reminders-fully-backend-authoritative.md).
|
||||
`FuchsPdf` / `FuchsVisualization` remain as static rendering libraries used *by* the services. The earlier static, controller-coupled helpers (`FuchsWidgets`, `FuchsReports`, `Banking`, `FuchsFdsEmail`) have been removed.
|
||||
|
||||
### 4.4 SQL-First Data Access
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
status: Active
|
||||
lastUpdated: 2026-07-10
|
||||
lastUpdated: 2026-09-09
|
||||
applyTo:
|
||||
- "Fuchs/Services/InvoiceDraft*"
|
||||
- "Fuchs/Services/IInvoiceDraft*"
|
||||
- "Fuchs/code/InvoiceSetStructure.cs"
|
||||
- "Fuchs/Services/ReminderDraft*"
|
||||
- "Fuchs/Services/IReminderDraft*"
|
||||
- "Fuchs/code/InvoiceDraftSession.cs"
|
||||
@@ -18,6 +19,8 @@ applyTo:
|
||||
relatedDecisions:
|
||||
- "0006-backend-authoritative-draft-editing.md"
|
||||
- "0007-targeted-draft-signalr-groups.md"
|
||||
- "0009-block-setprice-modes-are-irreversible-mutations.md"
|
||||
- "0016-user-defined-set-structure.md"
|
||||
---
|
||||
|
||||
# Live draft editing (backend-authoritative invoice previews)
|
||||
@@ -83,6 +86,19 @@ Expiry: Server (timer) --SignalR draftExpiring{token,secondsLeft}--> warn "bit
|
||||
`BuildInvoiceRequestList`), applies deltas (`ApplyDelta`), builds the view-state DTO,
|
||||
flushes to the DB by reusing `IInvoiceService.RegisterInvoiceAsync` (no new persistence
|
||||
path), renders previews from a synthesised registration, and discards by reloading.
|
||||
- **Set structure** (ADR 0016). Every cached line carries a parent link (`SetItmId` → the
|
||||
owning `typ == "set"` line, null/self = top level); sets may nest. `fds__prepInvoice`'s
|
||||
implicit tagging is the initial structure; the user edits it in the "Set-Struktur
|
||||
bearbeiten" dialog (`$inv.sets`, `fis.inv_sets.js`) which posts one delta per action —
|
||||
`set.assign` { parent } (drop onto a set / pull out), `set.level` ±1 (indent under the
|
||||
preceding sibling set / outdent to the grandparent), `set.create` { typ: set|text, title,
|
||||
parent }, `set.move` ±1 | { anchor, where } (reorder among siblings / drag before or after
|
||||
a row; the subtree travels along). `InvoiceDraftSetStructureDeltas` delegates to the pure
|
||||
`InvoiceSetStructure` (assign / change level / create / place, then **linearise** the block depth-first and mirror
|
||||
`setId`/`level` onto the `items` contract). `Refresh` publishes `lvl` on every line
|
||||
(`RecomputeLevels`), rendered as an indent class in the editor and a per-level indent in the
|
||||
PDF. `item.setprice` sums a header's **transitive** descendants. Structure deltas never
|
||||
change values — only order, membership, levels and therefore position numbers.
|
||||
- **Cache** (`InvoiceDraftCache`, singleton) stores sessions by token with an idle sliding
|
||||
TTL; `InvoiceDraftExpiryService` (a `BackgroundService`) warns before, and evicts after,
|
||||
the TTL. TTL/warn-lead are configurable under `Fuchs:DraftEditing`.
|
||||
@@ -97,12 +113,15 @@ Expiry: Server (timer) --SignalR draftExpiring{token,secondsLeft}--> warn "bit
|
||||
## Key files
|
||||
- `Fuchs/code/InvoiceDraftSession.cs` — session + `ChangeHistoryEntry` + `InvoiceDraftSums`.
|
||||
- `Fuchs/code/InvoiceDraftCalculator.cs` — pure recompute + validate.
|
||||
- `Fuchs/code/InvoiceSetStructure.cs` — pure set forest: parent links, levels, linearise, assign/level/create (ADR 0016).
|
||||
- `Fuchs/Services/InvoiceDraftSetStructureDeltas.cs` — the `set.*` patch targets.
|
||||
- `Fuchs/Services/InvoiceDraftCache.cs` / `IInvoiceDraftCache.cs` — in-memory store + TTL.
|
||||
- `Fuchs/Services/InvoiceDraftEditService.cs` / `IInvoiceDraftService.cs` — orchestration + delta contract.
|
||||
- `Fuchs/Services/InvoiceDraftExpiryService.cs` — idle warn/evict monitor.
|
||||
- `Fuchs/Notifications/DraftPreviewHub.cs`, `DraftNotifier.cs`, `IDraftNotifier.cs` — targeted signals.
|
||||
- `Fuchs/Controllers/IntranetController.InvoiceDraft.cs` — `inv/d*` endpoints.
|
||||
- `Fuchs/js/intranet/fis_main.js`, `Fuchs/js/intranet/modules/fis.inv_shared.js` — client.
|
||||
- `Fuchs/js/intranet/modules/fis.inv_sets.js` — the set-structure dialog (`$inv.sets`), a view of `dstate.req`.
|
||||
|
||||
## Reminders (Zahlungserinnerung)
|
||||
|
||||
@@ -151,3 +170,5 @@ amount pair.
|
||||
## Related decisions
|
||||
- [0006 — Backend-authoritative draft editing](../Decisions/0006-backend-authoritative-draft-editing.md)
|
||||
- [0007 — Targeted draft SignalR groups](../Decisions/0007-targeted-draft-signalr-groups.md)
|
||||
- [0009 — Block set-price modes are irreversible mutations](../Decisions/0009-block-setprice-modes-are-irreversible-mutations.md)
|
||||
- [0016 — User-defined set structure](../Decisions/0016-user-defined-set-structure.md)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
status: Accepted
|
||||
date: 2026-09-09
|
||||
applyTo:
|
||||
- "Fuchs/code/InvoiceSetStructure.cs"
|
||||
- "Fuchs/code/InvoiceSetPricing.cs"
|
||||
- "Fuchs/Services/InvoiceDraft*"
|
||||
- "Fuchs/Services/IInvoiceDraft*"
|
||||
- "Fuchs/code/InvoiceDraftCalculator.cs"
|
||||
- "Fuchs/code/FuchsPdf.cs"
|
||||
- "Fuchs/js/intranet/modules/fis.inv_sets.js"
|
||||
- "Fuchs/js/intranet/modules/fis.inv_shared.js"
|
||||
- "Fuchs/Docs/INVOICE_SET_PRICING.md"
|
||||
supersededBy: ""
|
||||
---
|
||||
|
||||
# 0016 — Set membership is an explicit, user-editable parent link on the cached lines; sets may nest; the server linearises
|
||||
|
||||
## Context
|
||||
Set membership on an invoice draft was purely **implicit**: `fds__prepInvoice` tags every item
|
||||
that follows a zero-priced `Type = 'Set'` header (until the next such header) with that header's
|
||||
id (`SetItmId`), and the set-item switch (ADR [0009](0009-block-setprice-modes-are-irreversible-mutations.md),
|
||||
function 1) sums exactly those tagged lines. Users had no way to correct that grouping — a
|
||||
trailing item that does not belong, a material line that should sit inside the set, a set that
|
||||
should contain another set — nor to introduce a set (or a free-text line) that does not exist
|
||||
in mfr. The product owner asked for a dialog that lets the user define which line belongs to
|
||||
which set, create new set/text lines, change a line's level, and nest sets, with every change
|
||||
applied **immediately** to the cached draft and reflected in the editor.
|
||||
|
||||
## Decision
|
||||
1. **The implicit derivation stays as the initial structure.** `fds__prepInvoice`'s
|
||||
`SetItmId` window function is unchanged; a freshly opened draft shows exactly the grouping
|
||||
it delivered.
|
||||
|
||||
2. **Membership is an explicit parent link on every cached line.** Each line in a block's
|
||||
`itm` array carries `SetItmId` = the id of the `typ == "set"` line that owns it (null, or a
|
||||
self-reference as delivered for headers, = top level). The links form a forest per
|
||||
service-request block; the block is the boundary — a line is never assigned to a set of
|
||||
another block. `InvoiceSetStructure.ParentOf` normalises the link (self-reference, unknown
|
||||
id, non-set target → top level).
|
||||
|
||||
3. **Sets may contain sets.** A set's own `SetItmId` names its parent set. Moving a set keeps
|
||||
its members attached — they simply sit one level deeper/shallower. The depth of every line is
|
||||
published as `lvl` (co shape) / `level` (items contract) and mirrored onto `items[].setId`,
|
||||
so the PDF and the editor read one structure.
|
||||
|
||||
4. **Four new backend-authoritative deltas** (`inv/dpatch`, handled by
|
||||
`InvoiceDraftSetStructureDeltas` → pure `InvoiceSetStructure`):
|
||||
- `set.assign` (Ref = item, Value `{ parent }`; `""` = top level) — drop a line onto a set
|
||||
(it becomes the set's **last** member) or pull it out (placed right after the subtree it
|
||||
came from). Rejected as a no-op: unknown item, target not a set of the same block, target
|
||||
is the item itself or one of its descendants (cycle), parent unchanged.
|
||||
- `set.level` (Ref = item, Value ±1) — indent = join the nearest **preceding sibling set**
|
||||
as its last member; outdent = join the grandparent right after the former parent's
|
||||
subtree. No-op when no such move exists.
|
||||
- `set.create` (Ref = block, Value `{ typ: "set" | "text", title, parent }`) — a new
|
||||
zero-priced set header (convertible later via the item switch) or a free-text line, as the
|
||||
last member of `parent` / end of the block. Both line shapes are written; the title is
|
||||
HTML-encoded for the text line.
|
||||
- `set.move` (Ref = item, Value ±1 or `{ anchor, where: "before" | "after" }`) — reorder:
|
||||
one step up/down among the line's siblings, or become the anchor's sibling placed before
|
||||
it / after its subtree (drag between rows). The line's own subtree travels with it, so a
|
||||
set can be reordered folded or unfolded alike. Rejected: anchor inside the item's subtree.
|
||||
|
||||
5. **The server linearises after every structural change**: the block's lines are reordered
|
||||
depth-first (each set immediately followed by its members, recursively; siblings keep their
|
||||
relative order), levels are recomputed and the `items` contract is mirrored (order,
|
||||
`setId`, `level`). `InvoiceSetPricing.Build` is therefore **order-preserving**: it no longer
|
||||
regroups members next to their header at render time; a line is governed by its nearest
|
||||
converted ancestor (blank in SetPrice, dropped in SetOnly). Editor and PDF print the same
|
||||
sequence and the same per-level indent (ADR 0008).
|
||||
|
||||
6. **The set-item switch sums transitive descendants.** `item.setprice` on a set now sums and
|
||||
nulls the header's whole subtree: an unconverted nested set contributes its members'
|
||||
prices, an already-converted nested set contributes its own set price — so converting inner
|
||||
then outer, or outer directly, both conserve the total and count nothing twice.
|
||||
|
||||
7. **The dialog is a view, not a model.** `$inv.sets` (fis.inv_sets.js) renders
|
||||
`state.req[].itm` (pos, type, quantity, title, indent, ▲▼ order and ◀▶ level buttons, fold
|
||||
toggles, per-block create bar), posts one delta per action and re-renders from the next
|
||||
`dstate`. Folding (`collapsed` set ids) is browser-only view state. The dialog performs no
|
||||
restructuring, ordering or numbering itself (ADR 0006).
|
||||
|
||||
## Consequences
|
||||
- `Refresh` runs `InvoiceSetStructure.RecomputeLevels` on every patch, so a `block.replace`
|
||||
posted by the editor (rows carry `SetItmId` from `rrw`) keeps the user-defined structure; the
|
||||
editor never has to re-derive it. Levels are rendered as an indent class (`lvl1…`) in the
|
||||
editor and a per-level left indent in the PDF.
|
||||
- `invSumUpdate` now tags a nested **set** with its `setId` too (previously only non-set
|
||||
members were tagged), and `BuildSetDisplay` blanks the whole subtree of a converted set —
|
||||
including nested headers, which stay emphasised.
|
||||
- Structural deltas never change a value: totals, VAT splits and `NetByBlock` are unaffected;
|
||||
only positions are renumbered along the new order.
|
||||
- Row drag in the editor (the existing `Sortable`) reorders rows without touching parent links;
|
||||
a member dragged away from its set keeps its membership and indent. The structure dialog is
|
||||
the place to change membership.
|
||||
- Tests: `InvoiceSetStructureTests` (pure forest logic), `InvoiceDraftServiceTests` (deltas,
|
||||
history, nested set price, state), `InvoiceSetPricingTests` (nesting, order preservation).
|
||||
|
||||
## Alternatives considered
|
||||
- **Derive membership from row order (everything after a set until the next set)** as the
|
||||
live model: rejected — that is exactly what the user cannot correct, and it cannot express
|
||||
nesting or an item that follows a set but does not belong to it.
|
||||
- **A separate structure table (tree) beside the lines**: rejected — a parent link per line
|
||||
keeps the single `itm`/`items` shape the calculator, the PDF, the persistence path and the
|
||||
editor already consume, so nothing else had to be re-shaped.
|
||||
- **Render-time regrouping (keep `Build` moving members next to their header)**: rejected —
|
||||
with explicit links the server can guarantee contiguity by linearising once, and an
|
||||
order-preserving `Build` is the only way the editor and the PDF are guaranteed to show the
|
||||
same sequence.
|
||||
- **Client-side restructuring with a full block re-post**: rejected per ADR 0006/0008 — one
|
||||
delta per action, the server owns the mutation, the browser mirrors.
|
||||
@@ -1,8 +1,10 @@
|
||||
# Invoice "Set" Pricing — Design & Front-/Back-end Contract
|
||||
|
||||
> Governed by ADR [0009](Decisions/0009-block-setprice-modes-are-irreversible-mutations.md)
|
||||
> (the two menu modes) and ADR [0008](Decisions/0008-invoices-and-reminders-fully-backend-authoritative.md)
|
||||
> (everything server-side, PDF == editor). ADR 0009 **redefined** the two menu modes from the
|
||||
> (the two menu modes), ADR [0008](Decisions/0008-invoices-and-reminders-fully-backend-authoritative.md)
|
||||
> (everything server-side, PDF == editor) and ADR
|
||||
> [0016](Decisions/0016-user-defined-set-structure.md) (set membership is an explicit,
|
||||
> user-editable parent link; sets may nest — see "Set structure" below). ADR 0009 **redefined** the two menu modes from the
|
||||
> reversible, non-mutating display toggles this document previously described into irreversible,
|
||||
> per-service-request-block mutations — the text below reflects the redefinition.
|
||||
|
||||
@@ -40,6 +42,37 @@ for the full breakdown.
|
||||
> or removes (mode 3), and the set-item switch (mode 1) writes exactly the members' sum onto the
|
||||
> header.
|
||||
|
||||
## Set structure (ADR 0016): which line belongs to which set
|
||||
|
||||
`SetItmId` is no longer only what `fds__prepInvoice` derived from row order — it is the
|
||||
**explicit parent link** of every cached line, and the user can edit it:
|
||||
|
||||
- **Initial structure** = the implicit derivation (every item after a zero-priced Set header
|
||||
until the next one). Nothing changes for drafts the user never touches.
|
||||
- **"Set-Struktur bearbeiten"** (editor menu → `$inv.sets`, `fis.inv_sets.js`) shows every
|
||||
line per service-request block (Pos., Typ, Menge, Bezeichnung, indented by level). Drag a
|
||||
line onto a set → `set.assign { parent }` (it becomes the set's last member); drag it onto
|
||||
the block heading / the "oberste Ebene" zone → `set.assign { parent: "" }` (pulled out, placed
|
||||
after the subtree it came from). Drag a line to the **top/bottom edge** of another row →
|
||||
`set.move { anchor, where: before|after }` (reorder: it becomes that row's sibling; a set
|
||||
travels with its members, folded or not); ▲ / ▼ post `set.move` −1 / +1 (one step among
|
||||
siblings). The ◀ / ▶ buttons post `set.level` −1 / +1 (outdent to the
|
||||
grandparent / indent under the nearest preceding sibling set). Sets can be folded via the
|
||||
triangle (view state only). The per-block bar creates a
|
||||
new **Set** (zero-priced header, convertible via function 1) or **Text** line inside any set
|
||||
or at the top level → `set.create { typ, title, parent }`.
|
||||
- **Sets may contain sets.** A set dropped onto another set keeps its own members (one level
|
||||
deeper). Function 1 (`item.setprice`) sums the header's **transitive** descendants and nulls
|
||||
them all; converting inner-then-outer or outer directly conserves the total either way.
|
||||
- **Only sets are drop targets; a line never leaves its block.** Cycles (a set onto its own
|
||||
descendant) and no-op moves are rejected server-side (`InvoiceSetStructure`), the browser
|
||||
merely greys them out.
|
||||
- **The server linearises** after each structural delta (depth-first: set, then its members,
|
||||
recursively; siblings keep their order) and publishes `lvl`/`level`; `InvoiceSetPricing.Build`
|
||||
is order-preserving, so editor and PDF show the same sequence and the same per-level indent.
|
||||
Every change is applied immediately to the cached session and pushed back via
|
||||
`draftReady`/`dstate`; the dialog re-renders from that state.
|
||||
|
||||
## Back-end
|
||||
|
||||
> **Migration status (ADR 0009).** The code below still reflects the previous
|
||||
@@ -51,9 +84,14 @@ for the full breakdown.
|
||||
> migration lands so it stays a faithful description of the code.
|
||||
|
||||
- `Fuchs/code/InvoiceSetPricing.cs` — the authoritative transformation:
|
||||
`SetDisplayMode` + `Build(items, mode)` → ordered `InvoiceSetLine`s, each with
|
||||
`ShowPrice` and `IsSetHeader`. `ModeFromInvoiceOptions(...)` reads the mode
|
||||
`SetDisplayMode` + `Build(items, mode)` → `InvoiceSetLine`s **in input order**, each with
|
||||
`ShowPrice`, `IsSetHeader` and `Level` (nesting depth). A line is governed by its nearest
|
||||
*converted* ancestor set (blank in SetPrice, dropped in SetOnly); an unconverted header is
|
||||
blank with individually priced members. `ModeFromInvoiceOptions(...)` reads the mode
|
||||
from the invoice options. Fully covered by `Fuchs.Tests/InvoiceSetPricingTests.cs`.
|
||||
- `Fuchs/code/InvoiceSetStructure.cs` — the pure set forest (parent links, levels, depth-first
|
||||
linearisation, assign / change level / create); `Fuchs/Services/InvoiceDraftSetStructureDeltas.cs`
|
||||
maps the `set.*` patch targets onto it. Covered by `InvoiceSetStructureTests` + `InvoiceDraftServiceTests`.
|
||||
- `FuchsPdf.ApplyInvoice` renders through `InvoiceSetPricing.Build`: lines with
|
||||
`ShowPrice == false` render **blank** price/total cells (not `0,00 €`), and the
|
||||
set header line is rendered emphasised. Invoices without sets pass through
|
||||
@@ -82,11 +120,12 @@ Wired in `Fuchs/js/intranet/modules/fis.inv_shared.js` (bundled to
|
||||
C# PDF. This change closes that gap for **all** invoices, not just sets.)
|
||||
|
||||
3. **Set flags (function 1 only)** — `invSumUpdate` tags items as it builds `items[]`:
|
||||
an item with `type === 'set'` is a header (`id` = its set id); a member item's
|
||||
`setId` is taken directly from the server-computed `SetItmId` field on the row
|
||||
(`rrx.SetItmId`, populated by `fds__prepInvoice`'s `[SetItmID]` window function,
|
||||
anchored on the still-unconverted, zero-priced `'set'` header that owns it) —
|
||||
**not** re-derived from row order in the browser. `mfr__items` itself still has no
|
||||
an item with `type === 'set'` is a header (`id` = its set id); a member's
|
||||
`setId` is taken directly from the `SetItmId` field on the row
|
||||
(`rrx.SetItmId` — initially populated by `fds__prepInvoice`'s `[SetItmID]` window function,
|
||||
anchored on the still-unconverted, zero-priced `'set'` header that owns it, and thereafter
|
||||
maintained by the server per ADR 0016, including for a **nested set**, which is a member of
|
||||
its parent set) — **not** re-derived from row order in the browser. `mfr__items` itself still has no
|
||||
explicit member link; `fds__prepInvoice` computes `SetItmId` per request from the
|
||||
item list, so only items the server actually attributes to a set are tagged, and
|
||||
unrelated items following a set in the list are never swept in. The header row's
|
||||
|
||||
@@ -50,6 +50,20 @@ Serviceaufträge kommen aus dem MFR-ERP-System und werden regelmäßig automatis
|
||||
1. Aus einem Serviceauftrag eine Rechnung **vorbereiten** → Vorschau prüfen.
|
||||
2. **Speichern** (Entwurf) oder weiter **bearbeiten**. Entwürfe tragen ein Wasserzeichen.
|
||||
|
||||
### Sets (Positionen zusammenfassen)
|
||||
- Ein **Set** aus mfr fasst die darauf folgenden Positionen zusammen; mit **„Auf Setpreis
|
||||
umstellen“** (Waage-Symbol an der Set-Zeile) erhält das Set die Summe seiner Positionen als
|
||||
Preis, die Positionen selbst werden ohne Preis gedruckt (nicht rückgängig zu machen).
|
||||
- **„Set-Struktur bearbeiten“** (Menü) zeigt alle Positionen je Auftrag. Positionen per
|
||||
**Drag & Drop auf ein Set** ziehen, um sie zuzuordnen; auf die Auftragszeile oder den Bereich
|
||||
„oberste Ebene“ ziehen, um sie aus dem Set zu lösen. **Reihenfolge**: Zeile an den oberen
|
||||
bzw. unteren Rand einer anderen Zeile ziehen (davor/danach) oder ▲ / ▼ nutzen. Mit ◀ / ▶ eine
|
||||
Ebene höher/tiefer. Sets lassen sich über das Dreieck zuklappen und auch zugeklappt verschieben.
|
||||
Sets können Sets enthalten; ein verschobenes Set nimmt seine Positionen mit. Über die Leiste
|
||||
„Neu“ lassen sich neue **Set**- oder **Text**-Zeilen anlegen (auch innerhalb eines Sets).
|
||||
- Jede Änderung wird sofort übernommen und im Editor sowie in der Vorschau abgebildet
|
||||
(eingerückt je Ebene); die Rechnungssumme ändert sich dadurch nicht.
|
||||
|
||||
### Finalisieren & Versenden
|
||||
1. **Bestätigen/Finalisieren**: Die Rechnung erhält eine Rechnungsnummer, das PDF wird erzeugt und gespeichert.
|
||||
2. Ist eine E-Mail-Adresse hinterlegt, wird die Rechnung **automatisch als PDF-Anhang** per E-Mail versendet.
|
||||
|
||||
Reference in New Issue
Block a user