# Bestand und Bestellmenge

Standardmäßig ermittelt FINN.ghost den Bestand eines Artikels aus der SelectLine-Ansicht
`SL_vGetFreierArtikelBestand`: **Bestand minus Reserviert**, bei Stücklisten die kleinste
Stückzahl, die sich aus den Bestandteilen bauen lässt. Für alle Fälle, in denen das nicht
passt, gibt es `customStock` — eine eigene SQL-Abfrage, die den Bestand liefert.

<p class="callout success">Typische Gründe: Es sollen nur bestimmte Läger in den Shop gemeldet werden, ein Sicherheitsbestand soll abgezogen werden, oder ein Sperrlager soll unberücksichtigt bleiben.</p>

## customStock

Alle drei Shop-Anbindungen kennen die Einstellung. Der Schlüssel ist derselbe, Abschnitt
und Platzhalter unterscheiden sich:

| Anbindung | Schlüssel | Platzhalter |
| -------------- | --------------------- | ---------------------- |
| Shopware 6 | `sw6.customStock` | `${article.Number}` |
| Shopify | `shopify.customStock` | `${nummer}` |
| FINN.webshop | `shop.customStock` | `${artikelnummer}` |

<p class="callout danger">Der Platzhalter ist je Anbindung ein anderer. Wird der falsche verwendet, wird er nicht ersetzt — die Abfrage läuft dann mit dem Text <code>${nummer}</code> als Artikelnummer, findet nichts und liefert Bestand <strong>0</strong> für jeden Artikel.</p>

### Was die Abfrage liefern muss

Eine Spalte mit dem Namen **`Bestand`**. Ausgewertet wird die **erste Zeile** des
Ergebnisses.

Das Beispiel meldet nur die Läger `01` und `02` und zieht die reservierte Menge ab:

```json
{
  "sw6": {
    "customStock": "SELECT SUM(ISNULL(Bestand,0)-ISNULL(Reserviert,0)) AS Bestand FROM dbo.SL_vGetFreierArtikelBestandMitLager WHERE Artikelnummer = '${article.Number}' AND Lager IN ('01','02')"
  }
}
```

<p class="callout success">Die Ansicht <code>SL_vGetFreierArtikelBestandMitLager</code> ist für solche Abfragen der bequemste Einstieg: dieselben Werte wie in der Standardberechnung, nur zusätzlich je Lager. Für Standorte statt Läger gibt es <code>SL_vGetFreierArtikelBestandMitStandort</code>.</p>

| | |
| ----------------------------------- | --------------------------------------------- |
| **Spalte heißt nicht `Bestand`** | Ergebnis wird als 0 gewertet |
| **Abfrage liefert keine Zeile** | Bestand 0 |
| **Ergebnis ist negativ** | wird auf 0 angehoben |
| **Mehrere Zeilen** | nur die erste zählt |

<p class="callout warning">Der Wert wird als ganze Zahl übertragen. Nachkommastellen fallen weg — bei Artikeln, die in Metern oder Kilogramm geführt werden, ist das zu bedenken.</p>

### Was die Abfrage ersetzt

Ist `customStock` gesetzt, entscheidet **allein** die Abfrage. Die Standardlogik läuft
nicht mehr — und damit auch nichts, was daran hängt:

| Läuft nicht mehr | Folge |
| ----------------------------- | --------------------------------------------- |
| Abzug von **Reserviert** | muss in der Abfrage selbst stehen |
| Auflösung von **Stücklisten** | Set- und Behälterartikel liefern den Wert der Abfrage, nicht die baubare Menge |

<p class="callout danger">Die Stücklistenauflösung ist der Punkt, der am häufigsten übersehen wird. Ohne <code>customStock</code> meldet FINN.ghost für einen Set-Artikel die Menge, die sich aus den Bestandteilen bauen lässt. Mit <code>customStock</code> liefert die Abfrage den Wert — und der eigene Lagerbestand eines Set-Artikels ist in der Regel 0.</p>

Welche Artikel überhaupt übertragen werden, ändert `customStock` **nicht**: Die Auswahl
nach *Shopaktiv* und *Inaktiv* passiert davor, beim Zusammenstellen der Artikelliste. Ein
nicht shopaktiver Artikel wird also weiterhin gar nicht erst gemeldet.

<p class="callout warning">Bei <strong>Shopify</strong> ist das anders: Dort gehört die Prüfung auf <em>Shopaktiv</em> und <em>Inaktiv</em> zur Bestandsermittlung selbst und wird von <code>shopify.customStock</code> mit übergangen — ebenso <code>shopify.zeroStockField</code>. Beides gehört dann in die Abfrage.</p>

### Rückfall auf die Standardberechnung (nur Shopware 6)

Bei Shopware 6 gibt es einen Ausweg für einzelne Artikel: Liefert die Abfrage den Wert
**`-9999`**, verwirft FINN.ghost das Ergebnis und rechnet für diesen Artikel wie ohne
`customStock`.

```sql
SELECT CASE WHEN ... THEN 0 ELSE -9999 END AS Bestand FROM ART WHERE Artikelnummer = '${article.Number}'
```

<p class="callout success">Damit lässt sich die Sonderlogik auf die Artikel beschränken, die sie brauchen. Alle anderen laufen weiter über die geprüfte Standardberechnung — inklusive Stücklistenauflösung.</p>

<p class="callout info">Shopify und FINN.webshop kennen diesen Rückfall nicht. Dort gilt die Abfrage für jeden Artikel.</p>

### Besonderheit bei Shopify

Shopify führt Bestände je Standort. `shopify.customStock` liefert **einen** Wert, und
dieser wird auf **alle** eingerichteten Standorte geschrieben — nicht aufgeteilt.

<p class="callout warning">Bei mehreren Standorten summiert Shopify die Bestände. Ein Wert von 10 bei drei Standorten wird im Shop zu 30 verfügbaren Stück. Wer mit mehreren Standorten arbeitet, sollte <code>customStock</code> dort nicht einsetzen.</p>

Zusätzlich greift die Abfrage nur, wenn die Bestandsführung für den Artikel aktiv ist.
Ist sie abgeschaltet, überträgt Shopify keinen Bestand — dann läuft die Abfrage nicht.

## Weitere Einstellungen zum Bestand

### Shopware 6

| Schlüssel | Typ | Wirkung |
| --------------------- | -------- | ------------------------------------------------------- |
| `sw6.ignoreReserved` | Schalter | rechnet mit **Bestand** statt Bestand minus Reserviert. Reservierungen aus offenen Aufträgen mindern den Shopbestand dann nicht. |
| `sw6.customPurchase` | Abfrage | eigene Abfrage für die **maximale Bestellmenge**. Platzhalter `${article}`, Spalte `Bestand`. |
| `sw6.maxPurchaseField` | Feldname | Artikelfeld mit der maximalen Bestellmenge je Artikel. |

<p class="callout warning">Ist <code>sw6.maxPurchaseField</code> gesetzt und im Artikel gefüllt, gilt dieser Wert — <code>sw6.customPurchase</code> kommt dann für diesen Artikel nicht mehr zum Zug. Das Feld hat Vorrang.</p>

<p class="callout info"><code>sw6.ignoreReserved</code> wirkt nur, wenn <code>sw6.customStock</code> <strong>nicht</strong> gesetzt ist oder für den Artikel <code>-9999</code> liefert. Sonst rechnet ohnehin nur die eigene Abfrage.</p>

### Shopify

| Schlüssel | Typ | Wirkung |
| ------------------------------------- | -------- | --------------------------------------- |
| `shopify.zeroStockField` | Feldname | Artikelfeld; ist es `True`, wird Bestand **0** gemeldet, obwohl Bestand vorhanden ist. Wirkt auch über den Variantenartikel und über Stücklisten: hat ein Bestandteil das Feld gesetzt, ist die ganze Stückliste 0. |
| `shopify.inventoryTrackDisableField` | Feldname | Artikelfeld; ist es `True`, wird die **Bestandsführung** für diesen Artikel in Shopify abgeschaltet. Der Artikel ist dann unbegrenzt bestellbar. |

<p class="callout success"><code>shopify.zeroStockField</code> ist der saubere Weg, einzelne Artikel im Shop auf nicht verfügbar zu setzen, ohne sie in der SelectLine anzufassen — und deutlich einfacher als eine eigene Abfrage.</p>

## Wenn der Bestand im Shop nicht stimmt

| Beobachtung | Erste Prüfung |
| ------------------------------------------ | ------------------------------------------------ |
| **alle** Artikel stehen auf 0 | Platzhalter falsch geschrieben, oder Spalte heißt nicht `Bestand` |
| Stücklisten stehen zu niedrig | `customStock` löst Stücklisten nicht auf |
| bei Shopify: gesperrte Artikel bestellbar | `shopify.zeroStockField` wird von der Abfrage übergangen |
| Shopify zeigt ein Vielfaches | mehrere Standorte, der Wert wird je Standort geschrieben |
| Bestand ändert sich nicht mehr | Lauf steht mit SQL-Fehler im Protokoll |

Die Abfrage lässt sich vor dem Eintragen im SQL Management Studio prüfen — dort den
Platzhalter durch eine echte Artikelnummer ersetzen. Meldungen der Läufe stehen im
Protokoll, siehe [Log Informationen](https://wiki.dako-it.com/books/finnghost-systemhandbuch/page/log-informationen).

## Nächster Schritt

[Shopware 6](https://wiki.dako-it.com/books/finnghost-systemhandbuch/page/shopware-6)