feat: Implement mfr truncation fix and enhance invoice handling
Playwright Tests / test (push) Canceled after 0s

- Added a new SQL script to address the "String or binary data would be truncated" error in mfr sync by widening the Note fields in service objects and companies to NVARCHAR(MAX).
- Updated the table types and associated stored procedures to accommodate the changes, ensuring compatibility with existing data.
- Queued a re-sync for previously rejected rows due to truncation issues.
- Created application logins for fuchs_fds and fuchs_web with placeholder passwords for initial setup.
- Introduced functions for invoice backdating and invoice date retrieval to streamline invoice processing.
- Developed a password generation function with customizable parameters for length and character types.
- Added a random number generation function to support password creation.
This commit is contained in:
Stefan
2026-09-30 16:56:08 +02:00
parent 7357f60120
commit c741535aec
79 changed files with 2739 additions and 143 deletions
+1
View File
@@ -446,3 +446,4 @@ public class MfrClientFactory : IMfrClientFactory, IDisposable
- The SQL schema lives in the **`Fuchs_Database`** SSDT project (source of truth). The backend is SQL-first: it calls stored procedures, table-valued types (e.g. `fds__tt__bankingtransactions`) and functions via OCORE ADO.NET helpers.
- When changing a stored procedure's name/parameters or a table type, update both the SSDT project and the calling C# in the same change, and keep the MT940/CAMT banking column mappings aligned with `fds__tt__bankingtransactions`.
- The SSDT project is the compile step for all T-SQL: it references the `master` system dacpac and treats T-SQL warnings as errors (`TreatTSqlWarningsAsErrors`, ADR 0036), so an unresolved or ambiguous reference fails `msbuild Fuchs_Database/FuchsDatabase.sqlproj` instead of surfacing at runtime.
@@ -0,0 +1,68 @@
---
status: Accepted
date: 2026-09-30
applyTo:
- "Fuchs_Database/dbo/Functions/fds__fn_invoiceBackdate.sql"
- "Fuchs_Database/dbo/Stored Procedures/fds__setInvoiceFinal.sql"
- "Fuchs/code/InvoiceDates.cs"
- "Fuchs/Services/ERechnungMapper.cs"
- "Fuchs/Services/InvoiceService.cs"
- "Fuchs/Controllers/IntranetController.InvoiceDraft.cs"
- "Fuchs/Controllers/IntranetController.Requests.cs"
- "Fuchs/js/intranet/modules/fis.inv_shared.js"
supersededBy: ""
---
# 0032 — Invoices may be backdated to the last working day of the previous month (admins only)
## Context
Work done at the end of a month is often invoiced in the first days of the next month. The customer
wants those invoices dated to the previous month. The invoice date was always the moment of
finalisation (`fds__setInvoiceFinal`: `DateFinalized = GETUTCDATE()`), and the invoice number is
drawn from the year of that date (`fds__newInvoiceId(Year(...))`).
Two invoice dates also existed side by side: the PDF printed `DateCreated` (draft creation) as
"Rechnungsdatum", while the embedded ZUGFeRD/XRechnung issue date (BT-2) used `DateFinalized` — a
visible date and an e-invoice date that could disagree.
## Decision
- **One invoice date.** PDF "Rechnungsdatum" and e-invoice issue date both come from
`InvoiceDates.IssueDate`: `DateFinalized` (converted from the stored UTC to the German calendar
day), falling back to `DateCreated` for drafts.
- **Backdating option.** When finalising, a user with `fds_inv > 2` may date the invoice to the
**last working day (Mon–Fri) of the previous month** — a fixed date, not a free choice. Public
holidays are not excluded (possible later extension).
- **Only while no invoice exists after that date.** Invoice numbers must ascend with invoice dates,
so the option is offered and accepted only while no invoice has been finalized after the backdate
day (a 2 h margin covers the UTC storage).
- **Single source of truth:** `[dbo].[fds__fn_invoiceBackdate](@authuser)` returns the allowed date or
NULL (authorisation + eligibility). `inv/dpreview` returns it so the preview dialog shows the
per-invoice checkbox; `req/sconf` passes `@backdate = 1`, and `fds__setInvoiceFinal` re-evaluates
the function and `THROW`s (60001) when it is no longer allowed — nothing is finalised then.
- **Serialized finalisation.** `fds__setInvoiceFinal` runs check + numbering `UPDATE` in one
transaction under an exclusive, transaction-owned app lock (`sp_getapplock`, resource
`fds__setInvoiceFinal`) taken by every finalisation, so a concurrent normal finalisation cannot
slip between the eligibility check and the backdated write and break the date/number order.
- `req/sconf` maps only SQL error 60001 to the "no longer possible, create without backdating"
message. A 0-row result (invoice already final/sent, e.g. a double submit) is reported as
"already finalised" — never as a backdate rejection, which would invite a duplicate invoice.
- A backdated invoice is stored with `DateFinalized = <day> 12:00` (UTC, stays on the same German
day), its number comes from that day's year, and `DueDate` is recomputed from the payment term
anchored on that day.
## Consequences
- The PDF of every newly finalised invoice now shows the finalisation date instead of the draft
creation date (intended: it now matches the e-invoice and the number sequence).
- In January the option yields a December date and therefore a number from the previous year's range.
- Lists, reports, DATEV and reminders follow the same invoice date — see ADR 0035.
- Telemetry: `fuchs.invoices.backdated` (outcome ok / rejected / already_final / error); successful
backdating logs EventId `InvoiceBackdated`.
## Alternatives considered
- **Free date picker for admins** — rejected: breaks the date/number order too easily; the need is
exactly "previous month's last working day".
- **Global admin switch "backdate everything until the first invoice of the month"** — rejected by
the customer in favour of an explicit per-invoice checkbox.
@@ -0,0 +1,52 @@
---
status: Accepted
date: 2026-09-30
applyTo:
- "Fuchs_Database/dbo/Tables/mfr__*.sql"
- "Fuchs_Database/dbo/User Defined Types/mfr__tt__*.sql"
- "Fuchs_Database/dbo/Stored Procedures/mfr__updt__*.sql"
- "Fuchs_DataService/FdsMfrClient.cs"
- "Fuchs_DataService/FdsColumnWidths.cs"
supersededBy: ""
---
# 0033 — mfr free-text columns are NVARCHAR(MAX); table, table type and copies stay aligned
## Context
mfr publishes no length limits for its string properties (`MFR_RESTClient/Docs/mfr_interface_description.md`).
`[mfr__serviceobjects].[Note]` and `[mfr__companies].[Note]` were `NVARCHAR(255)`, as were their
merge types `mfr__tt__serviceobjects` / `mfr__tt__companies`. From 2026-09-25, users typed longer
notes in mfr (ServiceObject up to 753 chars, Company up to 723). The `mfr__updt__*` merge proc copies
the staging table into the table type, so SQL Server rejected every batch holding such a row
(msg 8152, which names no column). That included every Company/Contact/ServiceRequest sync that
`$expand`s the row. The result was about 1000 errors in five days, plus rows the short sync never
re-fetched, because it only reads rows newer than the local `DateModified` watermark. Merge failures
were silent until 8bcec40 began logging them, so earlier occurrences went unnoticed.
## Decision
- Free-text mfr properties (`Note`, `Description`, `Comment`, …) are stored as `NVARCHAR(MAX)`.
Bounded widths are kept only for values mfr structurally keeps short (ids, codes, names), and
those are revisited when data proves otherwise.
- A width change is made in the target table, its `mfr__tt__*` type **and** every table variable
or type that copies the row with `SELECT *` (e.g. `@company` in `fds__getRequest_details`,
`fds__prepInvoice`, `fds__prepStorno_recreate`), all in the same change. Otherwise the truncation
just moves to the next copy.
- Truncating at the mapping layer was rejected. The data service does **not** truncate
silently. Instead, when a merge fails with 8152/2628, `FdsColumnWidths` compares the staged
batch against the narrower of the table and type widths. The error log then names column, longest
length, limit and an entity Id (`over-long columns: Note: 753 > 255 chars (1 row, e.g. Id=…)`).
This lookup runs only on the failure path.
## Consequences
- Changing a `mfr__tt__*` type means DROP + CREATE (types cannot be ALTERed). The procs that
DECLARE the type must be dropped first and recreated. See
`Fuchs_Database/Scripts/2026-09-30_mfr_truncation_fix.sql` for the pattern, including grants.
- After fixing a rejected merge, queue the affected Ids in `fds__mfr_updaterequests`. The short
sync will not revisit them, and the nightly full reset is disabled in `fds__getUpdateableTables`.
- `NVARCHAR(MAX)` columns cannot be index key columns. None of the widened columns is indexed.
## Alternatives considered
- **Truncate to the column width in `SetRowValue`**: this would keep syncing, but it loses user text
silently. For keys such as `ExternalId` (DATEV customer number) it would corrupt data. Rejected.
- **Widen to a larger fixed size (e.g. 1000, like `mfr__contacts.Note`)**: this just moves the
cliff. mfr enforces no limit. Rejected for free text.
@@ -0,0 +1,64 @@
---
status: Accepted
date: 2026-09-30
applyTo:
- "Fuchs_Database/dbo/Functions/fds__fn_IntermediateIsAllocatedToOther.sql"
- "Fuchs_Database/dbo/Stored Procedures/fds__setInvoiceFinal.sql"
- "Fuchs_Database/dbo/Stored Procedures/fds__prepInvoice.sql"
- "Fuchs_Database/dbo/Stored Procedures/fds__prepStorno_recreate.sql"
- "Fuchs_Database/dbo/Stored Procedures/fds__createStorno_*.sql"
- "Fuchs_Database/Scripts/2026-09-30_backfill_intermediate_invoice_allocations.sql"
supersededBy: ""
---
# 0034 — An Abschlagsrechnung credit follows the live invoice
## Context
A Schlussrechnung credits the Abschlagsrechnungen (intermediate invoices, `InvoiceType = 'i'`)
already paid for the same service requests via `IntInvoiceAllocation` lines. Finalising it records
the claim in `fds__invoice_details.AllocatedTo_InvId` of each credited intermediate so that
`fds__prepInvoice` does not offer the same Abschlagsrechnung again.
Until 2026-09-30 that claim was never actually written (the editor dropped `reference_InvId`, the
MERGE aborted on the NULL). The moment it is written, the question "what happens to the claim when
the crediting invoice is cancelled?" becomes real: a Storno (simple/copy) or a Storno + recreate
leaves `AllocatedTo_InvId` pointing at the cancelled invoice, and the next invoice for the same
service requests would silently bill the customer again for money already paid.
## Decision
- **A claim counts only while the crediting invoice is alive.** An `AllocatedTo_InvId` that points
at a cancelled invoice (`IsCanceled = 1`), at a non-existent invoice, or at the invoice that the
one being finalised replaces (`CanceledInvoice` line → `@Replaces`) is void: the Abschlagsrechnung
is offered again by `fds__prepInvoice` / `fds__prepStorno_recreate` and may be taken over by the
next finalisation. The single source of truth for "already credited elsewhere" is
`[dbo].[fds__fn_IntermediateIsAllocatedToOther]`; `fds__setInvoiceFinal`'s `existing` CTE is its
set-based twin and must apply the same rule.
- **The link is not cleared on cancellation.** Cancellation has several paths (storno simple/copy,
storno + recreate, the retired `fds__setInvoiceCancelled`); rather than releasing the link in each,
the readers ignore dead links. The last claimant therefore stays visible for auditing.
- **A Stornorechnung (`InvoiceType = 'c'`) never claims.** `fds__createStorno_copy` mirrors the
cancelled invoice's `IntInvoiceAllocation` lines (with their references) so the storno document
shows them, but finalising the storno must not move the allocation onto it.
- **Storno + recreate carries the references.** `fds__prepStorno_recreate` emits its credit lines
with the same type as `fds__prepInvoice` (`IntInvoiceAllocation`, not `PartialInvoice`) and the
credited invoice in `reference_InvId`, and its `CanceledInvoice` line references the replaced
invoice — otherwise the replacement neither takes over the credit nor records what it replaces.
## Consequences
- The recreate flow now produces `Replaces_InvId` / `DateCancelled` on the replaced invoice via the
regular `fds__setInvoiceFinal` bookkeeping (it did not before: no reference reached the proc).
- A second recreate (cancel the replacement, recreate again) keeps the credit because the claim on
the replaced invoice is treated as void during the replacement's finalisation.
- The one-off backfill script records the *earliest* live claimant; it is consistent with this rule.
- An intermediate that is itself cancelled is not offered (unchanged).
## Alternatives considered
- **Release `AllocatedTo_InvId` when the crediting invoice is cancelled** — rejected: four write
paths to keep in sync, and the audit trail of who last credited the Abschlagsrechnung is lost.
- **Keep `PartialInvoice` for recreated credit lines and teach `fds__setInvoiceFinal` both types** —
rejected: one line type per meaning; `PartialInvoice` remains the Abschlagsbetrag charge line of an
intermediate invoice.
@@ -0,0 +1,63 @@
---
status: Accepted
date: 2026-09-30
applyTo:
- "Fuchs_Database/dbo/Functions/fds__fn_invoiceDate.sql"
- "Fuchs_Database/dbo/Functions/fds__r_getBalanceThisMonth.sql"
- "Fuchs_Database/dbo/Stored Procedures/fds__getDatevExports.sql"
- "Fuchs_Database/dbo/Stored Procedures/fds__getInvoices_list*.sql"
- "Fuchs_Database/dbo/Stored Procedures/fds__getFDSDocument.sql"
- "Fuchs_Database/dbo/Stored Procedures/fds__getReportDocument.sql"
- "Fuchs_Database/dbo/Stored Procedures/fds__getRequest_details.sql"
- "Fuchs_Database/dbo/Stored Procedures/fds__r_*.sql"
- "Fuchs_Database/dbo/Stored Procedures/fds__*Reminder*.sql"
- "Fuchs/code/InvoiceDates.cs"
- "Fuchs/Services/ReminderService.cs"
- "Fuchs/Services/*DraftEditService.cs"
supersededBy: ""
---
# 0035 — The invoice date is `DateFinalized` everywhere
## Context
ADR 0032 made `DateFinalized` the invoice date for the PDF, the e-invoice (BT-2) and the number
sequence, and introduced backdating (which only moves `DateFinalized`). Everything else — invoice
lists, revenue reports and charts, the DATEV export, document lookups, the request details — still
dated and grouped invoices by `DateCreated`, the moment the *draft* was created. A backdated invoice
therefore showed September on the document and October in every list, report and in DATEV, and any
invoice drafted in one month and finalised in the next had the same split.
## Decision
- **One invoice date, everywhere:** the German calendar day of `DateFinalized`, falling back to
`DateCreated` only for drafts. Both columns are stored in UTC.
- **One implementation per side:** `[dbo].[fds__fn_invoiceDate](DateFinalized, DateCreated)` in SQL and
`InvoiceDates.IssueDate` in C#. No proc converts or picks the date itself.
- Applies to: the DATEV export (period filter, `Belegdatum`, `Belegfeld 2`), the invoice lists
(`fds__getInvoices_list`, `list2`, `list_vario`: period filter, `Belegdatum`, `DateOfCreation`,
ordering, overdue fallback), the revenue reports (`fds__r_*`: year/month filters and grouping,
`fds__r_getBalanceThisMonth`), document lookups (`fds__getFDSDocument`, `fds__getReportDocument`),
`fds__getRequest_details` (new column `InvoiceDate`, shown in the request's invoice list), and the
invoice date shown in reminders (`fds__prepReminder`, `fds__createReminder`, `fds__getReminder`).
- **Reminders use the same rule for their own date:** the reminder PDF's date is the reminder's
`DateFinalized` (German day), draft: `DateCreated`.
- **No transition safeguard.** The switch applies to all periods, past and future; already exported
DATEV periods are not reconciled (confirmed by the business owner, 2026-09-30).
- `DateCreated` keeps its meaning — the creation timestamp of the row — and is still returned where
callers read it as such. Draft previews stamp it with `DateTime.UtcNow` to match the stored column.
## Consequences
- Reports and exports for a past period can differ from what they showed before this change: an
invoice drafted in month M and finalised in M+1 now counts in M+1.
- Filters on the invoice date are not SARGable (they were not before either: `CAST(DateCreated as date)`).
Acceptable at the current table size; an indexed computed column is the follow-up if it matters.
- Any new list, report or export that shows or groups invoices by date must use
`fds__fn_invoiceDate` — never `DateCreated` or a raw `DateFinalized`.
## Alternatives considered
- **Keep `DateCreated` in lists/reports, `DateFinalized` only on documents** — rejected: the same
invoice would carry two dates, and backdating would never reach the books.
- **Transition safeguard for already exported DATEV periods** — rejected by the business owner.
@@ -0,0 +1,85 @@
---
status: Accepted
date: 2026-09-30
applyTo:
- "Fuchs_Database/**"
supersededBy: ""
---
# 0036 — The database project build is the T-SQL gate
## Context
A rewrite of `fds__getDatevExports` added a `JOIN @inv as i` next to `fds__invoices as iv`
and left an unqualified `[file_guid]` in the select list. SQL Server rejects that statement
with Msg 209 (ambiguous column name) — but only when it runs, and the working-tree SQL runs
nowhere: the schema lives in the `Fuchs_Database` SSDT project, the dev database only gets
what is explicitly published to it, and the xUnit suite cannot bind T-SQL. The defect reached
code review as the only check.
The SSDT build would have caught it: it reported the line as `SQL71502` ("unresolved reference
… or ambiguous"). Nobody saw that because
- the project did not build at all — the two `CREATE USER … FOR LOGIN` files referenced logins
that were not in the model, which SSDT treats as error `SQL71501`;
- the genuine warning sat among ~160 identical `SQL71502` warnings caused by references to
`sys.*`, `INFORMATION_SCHEMA`, `sp_executesql`, `sp_getapplock` (no `master` reference) and
to `ocms_fn_generatePassword`, an OCMS helper the project never contained;
- a warning does not fail anything.
The same sweep found that `ocms_fn_generatePassword` (and its dependency `ocms_fn_randbetween`)
is missing on the dev database as well, so every id-generating function (`fds__fn_invoice_id`,
`fds__fn_reminder_id`, `fis_fn_id`, …) fails there with Msg 4121.
## Decision
1. `FuchsDatabase.sqlproj` references the `master` system dacpac
(`$(DacPacRootPath)\…\SqlServer\170\SQLSchemas\master.dacpac`), so system objects bind.
2. Everything the SQL references is in the project. The OCMS helpers `ocms_fn_generatePassword`
and `ocms_fn_randbetween` are vendored copies of the OCMSsharp `ocms_mt_database` files.
`Security/Logins.sql` scripts the two application logins with a placeholder,
`MUST_CHANGE` password so the users bind; real passwords stay in Key Vault and SSDT never
compares passwords, so existing logins are untouched on publish.
3. `TreatTSqlWarningsAsErrors` is on. Suppressed: `71501` (dynamic SQL / cross-database
references) and `71558` (identifier differs only by case — harmless under the `CI` model
collation). Every other T-SQL warning, in particular `SQL71502`, fails the build.
4. Building the database project is part of the workflow whenever `Fuchs_Database/**` changes
(`msbuild Fuchs_Database/FuchsDatabase.sqlproj -p:Configuration=Debug`, from the Visual
Studio MSBuild — `dotnet build` cannot build the legacy SSDT project format). A red database
build is a failure like a red C# build.
5. The C# side of the contract is a test: `Fuchs.Tests/SqlContractTests` scans every
`[dbo].[…]` named in C# strings (`Fuchs`, `Fuchs_DataService`) and checks it against the
SSDT files — the object exists, every `EXECUTE` binds (no unknown names, no surplus
positional arguments, every parameter without a default supplied), every function call
passes the declared argument count, and every `mfr__updt__*` proc declares the
`@tblname, @referencetable, @tgtid` signature `FdsMfrClient` calls dynamically.
## Consequences
- An unqualified column that becomes ambiguous, a typo in a column name, or a call to a
function that is not in the project is a build error before review, not a Msg 209/4121 at
a customer.
- `SELECT *` inside `EXISTS (…)` over joined tables trips `SQL71502` in SSDT even though SQL
Server accepts it; write `SELECT 1` there (done for `fds__fn_IntermediateIsAllocatedToOther`
and `fis_admin_testGroupMembership`).
- New procedures that use `sys.*`/`sp_*` need no suppression any more. A new reference that is
genuinely outside the model (another database, dynamic SQL) is suppressed per statement or
by adding the object to the project — never by widening `SuppressTSqlWarnings` for `71502`.
- A proc signature change that breaks a C# caller fails `dotnet test`, not the first request.
Only literal SQL is covered: dynamically built names (`FuchsVisualization`'s report procs,
resolved from `sys.parameters` at runtime) and result-set column names are not.
- The build proves binding, not behaviour: publishing to the dev database (`SqlPackage
/Action:Publish` with `ExcludeObjectTypes=Logins;Users;Permissions;RoleMembership`, or the
schema compare) and exercising the code path remains the runtime check.
- The dev database needs the two `ocms_*` helpers published before any id-generating path
works there.
## Alternatives considered
- **Run the changed procedures against the dev database before review.** Needs a publish of
every working-tree change first, and does not cover code paths the reviewer does not think
to exercise; the compile-time check is free and total.
- **Suppress `SQL71502` and keep the build warning-free that way.** Removes exactly the signal
that catches this class of bug.
- **A C#-side test that parses the SQL for unqualified columns.** A parser-level heuristic
without a schema cannot decide ambiguity; the SSDT model can.
+31 -1
View File
@@ -181,6 +181,36 @@ Confirming the preview modal posts to **`req/sconf`**
`[InvoiceId] = fds__newInvoiceId(YEAR())` → format `R<year>-<0000>`
(the draft only ever had the internal numeric `Id`). `Version` is bumped
and any linked "replaces" bookkeeping (storno/credit chains) is updated.
- **Invoice date** = `DateFinalized` — printed on the PDF and used as the
e-invoice issue date (`InvoiceDates.IssueDate`, ADR 0032).
- **Backdating (ADR 0032):** users with `fds_inv > 2` see a checkbox in the
preview dialog (`inv/dpreview` returns `backdate`) to date the invoice to
the last working day (Mon–Fri) of the previous month, as long as no
invoice has been finalised after that day. `req/sconf` then passes
`@backdate = 1`; `fds__fn_invoiceBackdate` is re-checked and the proc
throws 60001 (nothing finalised) if it is no longer allowed. Number year and
`DueDate` follow the backdated day. The proc serializes all finalisations
(transaction + `sp_getapplock`) so the check and the numbering cannot
interleave with a concurrent finalisation. The controller maps 60001 to
the "create again without backdating" message; a 0-row result (already
final/sent, e.g. double submit) is reported as "already finalised".
- **Same date everywhere (ADR 0035):** invoice lists, revenue reports,
the DATEV export (period + `Belegdatum`), request details and reminders
all use `[dbo].[fds__fn_invoiceDate]` — `DateFinalized` as German
calendar day, drafts fall back to `DateCreated` — so a backdated invoice
appears in the previous month everywhere, including DATEV. The reminder
PDF's own date follows the same rule (reminder `DateFinalized`).
- **Abschlagsrechnungen (ADR 0034):** `IntInvoiceAllocation` lines carry the
credited intermediate invoice in `reference_InvId` (seeded by
`fds__prepInvoice` and `fds__prepStorno_recreate`, round-tripped by the
editor as item `ref`); finalisation records it in
`fds__invoice_details.AllocatedTo_InvId` so the same Abschlagsrechnung is
not offered again. The claim only counts while the crediting invoice is
alive: once it is cancelled (storno, or replaced via storno + recreate)
`fds__fn_IntermediateIsAllocatedToOther` treats the Abschlagsrechnung as
free again and the next finalisation takes the allocation over. A
Stornorechnung (`'c'`) never claims an allocation, even though a storno
copy mirrors the credit lines.
2. On success (`IsFinal == true` returned), the controller:
- Reloads the invoice via `LoadInvoiceAsync`.
- Calls `StoreInvoiceDocumentFileAsync` → renders the **final** PDF
@@ -283,7 +313,7 @@ Once an invoice is `IsFinal`, the request/invoice list menus
| **View / display PDF** | `$inv.disp(id, 'inv')` | `inv/rdoc` | Renders page images for on-screen display only. If a finalised, non-external invoice has no stored PDF, it is regenerated and persisted first (ADR 0025) |
| **Download / open PDF** | `req/idoc` or `inv/pget`-derived | `req/idoc` | Serves the stored (or freshly rendered) PDF inline |
| **Mark paid / unpaid** | `$inv.setPyd`/`setUpd` | `inv/setpyd` / `inv/setupd` | `fds__setInvoicePayed` / `fds__setInvoiceUNPayed` |
| **Storno (cancel)** | `$inv.storno` → `$inv.cSt` | `inv/storno` (mode `simple`/`copy`) | `fds__createStorno_simple` / `fds__createStorno_copy`; the replaced invoice is cross-linked (`Replaces_InvId`) and cancelled once the storno itself is finalised |
| **Storno (cancel)** | `$inv.storno` → `$inv.cSt` (first `inv/pget`: refreshes mfr entities only for numeric mfr ids, no-op for Intranet ids) | `inv/storno` (mode `simple`/`copy`) | `fds__createStorno_simple` / `fds__createStorno_copy`; the replaced invoice is cross-linked (`Replaces_InvId`) and cancelled once the storno itself is finalised |
| **Credit note** | `$inv.credit` → `$inv.cSt` | `inv/credit` (mode `credit`) | `fds__createCredit_simple` — same preview/finalise/email loop as a normal invoice |
| **Continue editing a draft** | `$inv.cntInv` / `clCntInv` | `inv/get` | Only available while `isFinal == false`; re-opens the same stateless editor |
+5
View File
@@ -49,6 +49,11 @@ Serviceaufträge kommen aus dem MFR-ERP-System und werden regelmäßig automatis
### Erstellen / Bearbeiten
1. Aus einem Serviceauftrag eine Rechnung **vorbereiten** → Vorschau prüfen.
2. **Speichern** (Entwurf) oder weiter **bearbeiten**. Entwürfe tragen ein Wasserzeichen.
3. **Rechnung erstellen** in der Vorschau vergibt Rechnungsnummer und Rechnungsdatum (= Tag der Erstellung).
- **Rechnungsdatum Vormonat (nur Admins):** Solange im neuen Monat noch keine Rechnung erstellt wurde,
erscheint in der Vorschau das Häkchen „Rechnungsdatum auf den letzten Werktag des Vormonats setzen
(TT.MM.JJJJ)“. Gesetzt, erhält die Rechnung dieses Datum; Rechnungsnummer und Fälligkeit richten sich
danach. Werktage sind Mo–Fr (Feiertage werden nicht berücksichtigt).
### Sets (Positionen zusammenfassen)
- Ein **Set** aus mfr fasst die darauf folgenden Positionen zusammen; mit **„Auf Setpreis