---
topic: separator
title: Decimal and thousands separators for weight, amount and length
summary: ttrennzeichen via SeparatorController — one POST rewrites all three unit rows, and a missing row id makes the save insert instead of update.
appliesTo:
  - separator.units.save
derivedFrom:
  - includes/src/Router/Controller/Backend/SeparatorController.php:26-57
  - includes/src/Router/Controller/Backend/SeparatorController.php:63-91
  - includes/src/PlausiTrennzeichen.php:16-38
  - includes/src/Catalog/Separator.php:74-108
  - includes/src/Catalog/Separator.php:213-241
  - includes/src/Router/Controller/Backend/AbstractBackendController.php:137-166
  - includes/defines_inc.php:557-559
  - admin/templates/bootstrap/trennzeichen.tpl:11-125
---

## Domain model

One table, `ttrennzeichen`, and it is tiny: `kTrennzeichen` (PK), `kSprache`, `nEinheit`,
`nDezimalstellen`, `cDezimalZeichen`, `cTausenderZeichen`. Exactly **three rows per language**, one
per unit, held that way by `UNIQUE(kSprache, nEinheit)`.

The unit ids are constants and their order is a trap (`defines_inc.php:557-559`):

| unit   | `nEinheit` | field suffix |
|--------|-----------|--------------|
| weight | `1`       | `_1`         |
| length | `2`       | `_2`         |
| amount | `3`       | `_3`         |

**Amount is 3, length is 2** — the screen lists weight, amount, length, so the visual order and the
numeric order disagree. `Separator::getUnit()` (`:110-136`) reads these rows for every rendered
weight, quantity and length, cached under `CACHING_GROUP_CORE`.

## The one action

`separator.units.save` — route `separator`, permission `SETTINGS_SEPARATOR_VIEW`, dispatch
`save=1` (`SeparatorController.php:33`). There is no other branch and no delete: the rows are
created by the installer and by `Separator::insertMissingRow()`, never by this screen.

It is **one form for all three units**. The controller loops weight → amount → length
(`:67`) and, per unit, builds a `Separator` from `nDezimal_$u`, `cDezZeichen_$u` and
`cTausenderZeichen_$u`. A unit whose three keys are not all present is **skipped silently**
(`:68-70`) — that is a per-unit `continue`, not an error, so a partial POST is a partial save that
still reports success.

Then the branch that matters (`:78-81`):

```php
$idx = 'kTrennzeichen_' . $unt;
if (isset($post[$idx])) {
    $separator->setTrennzeichen((int)$post[$idx])->update();
} elseif (!$separator->save()) {
    return false;
}
```

**With the id it is an UPDATE; without it, an INSERT.** The insert cannot succeed — the
`(kSprache, nEinheit)` pair already exists — so `save()` returns `false`, `getResponse()` adds
`errorConfigSave`, and the units already processed in this loop **stay written**. There is no
rollback inside the controller; the invoker's transaction is what saves you, and only if you did not
ask for a real run. So: always send `kTrennzeichen_1`, `kTrennzeichen_3` and `kTrennzeichen_2`, read
fresh from the table.

## Language

Every row is language-scoped, and the screen edits **one** language: `$this->currentLanguageID`,
which is `$_SESSION['editLanguageID']`. Through MCP the session is built fresh per call and has no
`editLanguageID`, so `AbstractBackendController::setLanguage()` (`:148-156`) falls back to the shop
default. **Only the default language is reachable here** — the ids of any other language belong to
rows this action will never address, and sending one would rewrite that row's `kSprache` and
`nEinheit` into a duplicate pair, which fails silently. Other languages: the backend, with the
language switcher.

## Plausi

`PlausiTrennzeichen::doPlausi()` (`:16-38`) checks nine keys — for each of the three units:

- `nDezimal_$u` must be **present** (any value; `''` passes and casts to `0`).
- `cDezZeichen_$u` must be present and **non-empty** — this is the only length check.
- `cTausenderZeichen_$u` must be **present**; empty is fine (no thousands separator).

One failure fails the whole POST: `errorFillRequired`, nothing written, the screen re-renders your
input. Note the method returns `false` even on success — the controller ignores the return value and
reads `getPlausiVar()` instead (`:36-38`).

## Recipe

1. `run_sql_read "SELECT kTrennzeichen, kSprache, nEinheit, nDezimalstellen, cDezimalZeichen,
   cTausenderZeichen FROM ttrennzeichen ORDER BY kSprache, nEinheit"` — take the three rows whose
   `kSprache` is the `cShopStandard='Y'` language.
2. Build all twelve arguments. **Resend the values you are not changing**: a unit you leave out is
   not preserved, it is skipped, and a key you leave out is a failed insert.
3. Invoke, then check `verify.ok`. The three `rowExists` clauses compare each row back field by
   field, and the `rowCount` clause pins the language to exactly three rows — that is what catches
   the stray insert, which no alert would tell you about.

## Blast radius

`nDezimalstellen` is not decoration: it **rounds what the customer sees**. Weights at `0` decimals
render `0,75 kg` as `1 kg` on every product page, in the cart and on the invoice PDF. The save also
flushes `CACHING_GROUP_CORE`, `_CATEGORY`, `_OPTION` and `_ARTICLE` (`:82-87`) — the whole catalogue
re-renders, and a cache flush is the one part of this action no undo can put back.
