Consolidating prices¶
Collapse the same price on many device types into one price class, so that a price change touches one row instead of forty.
Route: /price/migration · Permission: price_view to read the plan,
price_edit to apply it
Why¶
A price row applies to exactly one device type. If the same amount sits on forty types, thirty-nine of them are copies. That stays invisible as long as nothing changes, and turns expensive the moment something does: every price round, every correction and every new validity has to hit all forty individually.
The price class is where such a kind of device is priced once. This page finds those kinds in your data, creates the classes and lifts the amount up.
Where the prompt comes from
The price overview (/price/overview) lists the largest copy clusters: how
many amounts are assigned more than once, and how many device types hang off
them. The Consolidate button there leads here.
Step 1: what the consolidation is recognised by¶
The page reads an additional field holding the article number on the price
rows. The default is Artikelnummer; if the field is named differently in your
installation, enter that name at the top.
The reasoning: the three-part number (100.110.01) is the machine-readable
trace of a classification your lab made by hand long ago. The same number means
"same kind of device, same effort". Rebuilding the classes by hand means typing
out what is already there.
Step 2: what the data says¶
Reading the plan writes nothing. It answers the question nobody can answer up front: are the numbers clean enough to build classes from?
The table shows, per number:
| Column | Meaning |
|---|---|
| Number | the value of the additional field, unchanged |
| Rows | price rows carrying this number |
| Device types | how many types fall under it, with sample names |
| Amounts | how many amount slots can be carried over, and how many conflict |
| Future rows | rows with a later validity; they stay untouched |
| Note | why a group is not carried over |
Two notes occur:
- only one device type — a number on a single row is not a classification. Turning it into a class would trade one price row for a class plus a price row plus a membership: more upkeep for the same statement. A class pays off from the second device type onwards.
- class already exists — a class with this number is already there, typically from an earlier run. No second one is created, but its amounts can still be filled in.
Amount slots¶
What is carried over is not "one price per number" but one price per slot: price group × complexity. Price lookup filters both exactly, so every combination present in the data needs its own class row — otherwise that very combination would fall to "no price" after the change.
Step 3: deciding conflicts¶
A slot conflicts when its currently valid rows carry different amounts. Which one becomes the class price is a business question; the application does not decide it for you.
For each conflict the page lists the amounts that occur, with the number of rows carrying each — most frequent first. That is information, not a preselection.
What a decision does, and what it does not
The chosen amount dissolves the copies carrying exactly that amount. The deviating rows stay as type-level rows and are afterwards precisely what they always were: exceptions. A decision therefore never rewrites prices.
Conflicts left open are not carried over. Their rows stay unchanged, and the step can be repeated later.
Tax rate, unit, quantity and discount of the class row come from a row of the slot carrying the chosen amount. What you decide is the amount, not the whole set.
Step 4: applying¶
Two checkboxes control how far the run goes:
Carry over class amounts. Without it, only the classes and their device types are created; the prices stay where they are. That is a reasonable first run when you want the assignment to settle first. Amounts can be filled in at any time.
Retire superseded type rows. Sets the end date of the copies to yesterday. The box is only available together with the amounts: without a class amount nothing covers the retired rows, and the slot would fall to "no price".
Nothing is deleted
Retiring means an end date, not a deletion. The rows stay readable, the history stays complete, and the step can be rolled back. Only what the class amount actually covers is retired — rows with a deviating amount, types sitting in another class, and rows whose validity starts today are left standing.
After the run, one sentence states what happened and what was left out: skipped device types, conflicts left open, class amounts that already existed, type rows left standing.
What is different afterwards¶
- The price list shows a class tree instead of a long list of individual type prices; exceptions are indented under their class.
- Changing the price of a consolidated kind is a change to one row, found on the price class detail page.
- The Price rows tab becomes what it is for: looking things up, correcting them, and exceptions.
On the console¶
The same path runs as a command, without conflict decisions:
# report, writes nothing
docker exec calserver-api-v2 php artisan price-classes:derive
# create classes and members
docker exec calserver-api-v2 php artisan price-classes:derive --apply --force
# ... plus the class amounts (conflict-free slots only)
docker exec calserver-api-v2 php artisan price-classes:derive --apply --amounts --force
# ... and retire the superseded type rows
docker exec calserver-api-v2 php artisan price-classes:derive --apply --amounts --retire --force
The command never carries over a conflict — that is what the page is for.
The other axis: complexity rows as a surcharge¶
The consolidation above bundles the device axis (many types, one price). The same duplication exists on the complexity axis: where "DAkkS costs more than ISO" used to require its own price row per complexity, a lab with 800 device types and three complexities carries 2400 rows — 1600 of which repeat the same statement: "plus 30 percent".
A command measures that and consolidates it:
# Report: which complexities exist, how many percent their rows sit above the
# base price, and how much they agree. Writes nothing.
docker exec calserver-api-v2 php artisan prices:complexity-to-surcharge
# Create one surcharge per complexity, with the measured percentage
docker exec calserver-api-v2 php artisan prices:complexity-to-surcharge --apply --force
# ... and retire the rows the surcharge reproduces
docker exec calserver-api-v2 php artisan prices:complexity-to-surcharge --apply --retire --force
Three things make the step safe:
- The suggestion is measured, not averaged. With 400 types at +30 % and three at +25 %, the answer is "+30 %, and those three stay" — not an average that appears in no row and changes all 403.
- Only rows the surcharge reproduces exactly are retired. The calculation uses the same method as production. Everything else stays — and a maintained complexity row still beats the surcharge.
- Nothing is deleted. A retired row gets an end date and stays valid for reference dates in the past.
Set your own rate with --complexity='DAkkS' --percent=30.
Another route: a price list you already have¶
If the classification lives in a spreadsheet rather than in the numbers,
price-classes:import reads it including amounts (CSV or Excel, mandatory
columns number and price class). Details in
preisliste.en.md.