u
This commit is contained in:
@@ -17,6 +17,10 @@ In `dashfront`, state is managed outside the React component lifecycle using **A
|
|||||||
5. **No Context / No Hooks in PureComponents**: Automats bypass React Context providers and hooks, eliminating reconciliation cascades.
|
5. **No Context / No Hooks in PureComponents**: Automats bypass React Context providers and hooks, eliminating reconciliation cascades.
|
||||||
6. **Automatic Coordination**: Domain and configuration automats subscribe to [`authAutomat`](./authAutomat.md) to automatically bootstrap on login, switch data when the active company changes, and reset on logout.
|
6. **Automatic Coordination**: Domain and configuration automats subscribe to [`authAutomat`](./authAutomat.md) to automatically bootstrap on login, switch data when the active company changes, and reset on logout.
|
||||||
|
|
||||||
|
## Domain & Data Model Guides
|
||||||
|
|
||||||
|
- [**Artikeltypen-Matrix**](./article-types-matrix.md): Spezifikation und Matrix für non-standard Artikeltypen (*Stückliste, Variations-Vater, Variations-Kind, Varianten-Baum*).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Automats Directory
|
## Automats Directory
|
||||||
|
|||||||
139
docs/article-types-matrix.md
Normal file
139
docs/article-types-matrix.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Artikeltypen-Matrix (Non-Standard Articles Documentation)
|
||||||
|
|
||||||
|
Dieses Dokument definiert das Verhalten, die Geschäftsregeln und das Datenmodell für die verschiedenen Artikeltypen im Dash ERP/POS-System:
|
||||||
|
- **Standard-Artikel** (einfacher / eigenständiger Artikel)
|
||||||
|
- **Stückliste** (BOM / Set / Bundle / Baugruppe)
|
||||||
|
- **Variations-Vater** (Parent Variant / Modell / Abstrakter Gruppenartikel)
|
||||||
|
- **Variations-Kind** (Child Variant / Konkrete Ausprägung)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Übersicht & Matrix
|
||||||
|
|
||||||
|
| Kriterium / Frage | Standard-Artikel | Stückliste (BOM / Set) | Variations-Vater | Variations-Kind |
|
||||||
|
| :--- | :---: | :---: | :---: | :---: |
|
||||||
|
| **Kann auch Stückliste sein?** | ✅ Ja *(umwandelbar)* | ℹ️ *Ist Stückliste* (mehrstufig schachtelbar) | ❌ **Nein** *(System blockiert)* | ✅ **Ja** *(Kind-Variante als Bundle)* |
|
||||||
|
| **Kann auch Variations-Vater sein?** | ✅ Ja *(aktivierbar)* | ❌ **Nein** *(System blockiert)* | ℹ️ *Ist Vater* | ✅ **Ja** *(als Zwischenknoten im Baum)* |
|
||||||
|
| **Kann auch Variations-Kind sein?** | ✅ Ja *(zuordenbar)* | ⚠️ **Ja** *(als Kind-Stückliste)* | ✅ **Ja** *(als Zwischenknoten im Baum)* | ℹ️ *Ist Kind* |
|
||||||
|
| **Kann Optionen haben?** (`optionslist`) | ✅ Ja | ✅ **Ja** *(koexistiert)* | ❌ **Nein** *(System blockiert)* | ✅ **Ja** *(als Blattknoten / Verkaufsartikel)* |
|
||||||
|
| **Hat eigenen physischen Bestand?** | ✅ **Ja** | ⚠️ **Virtuell** *(Komponenten-Minimum)* / Montage | ❌ **Nein** *(auf Variantenebene)* | ⚠️ **Nur als Blattknoten** *(Zwischenknoten: Nein)* |
|
||||||
|
| **Eigene Eigenschaften** *(Hersteller, Kategorie)* | ✅ **Ja** | ✅ **Ja** *(eigene Kategorie & Marke)* | ✅ **Ja** *(Basis-Attribute, vererbt nach unten)* | ⚠️ **Ja / Geerbt** *(vom Elternknoten)* |
|
||||||
|
| **Hat Lieferanten?** (`articlesupplier`) | ✅ **Ja** | ✅ **Ja** *(wenn Set als 1 SKU bezogen wird)* | ❌ **Nein** *(auf Variantenebene)* | ⚠️ **Nur als Blattknoten** *(Zwischenknoten: Nein)* |
|
||||||
|
| **Kann auf Beleg sein?** *(Kasse, Rechnung, etc.)* | ✅ **Ja** | ✅ **Ja** *(als Set-Position verkaufbar)* | ❌ **Nein** *(abstrakt, nicht buchbar)* | ⚠️ **Nur als Blattknoten** *(vollständig spezifiziert)* |
|
||||||
|
| **Hat eigenen Preis?** (`price`-Tabelle) | ✅ **Ja** | ✅ **Ja** *(fester Bundle-Verkaufspreis)* | ❌ **Nein** *(wird auf Variantenebene geführt)* | ⚠️ **Nur als Blattknoten** *(Zwischenknoten: Nein)* |
|
||||||
|
| **Hat individuelle Fotos?** (`articlepic`) | ✅ **Ja** | ✅ **Ja** *(Foto des Gesamt-Sets)* | ✅ **Ja** *(Modell-/Familienfoto)* | ✅ **Ja** *(variantenspezifisch z. B. Farbe Rot)* |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Varianten-Baum: Das Rollenmodell (Hierarchische Varianten)
|
||||||
|
|
||||||
|
Varianten bilden im System eine **Baum-Struktur** ab (z. B. für mehrdimensionale Varianten wie *Modell $\rightarrow$ Farbe $\rightarrow$ Größe*).
|
||||||
|
|
||||||
|
```text
|
||||||
|
[Wurzel: T-Shirt Classic] <- Reiner Vaterartikel (is_father = 1, fatherarticle_id = NULL)
|
||||||
|
│
|
||||||
|
├── [Zwischenknoten: Rot] <- Kind UND Vater (fatherarticle_id != NULL, is_father = 1)
|
||||||
|
│ ├── [Größe S] <- Blattknoten / Reines Kind (Endprodukt / verkaufbare SKU)
|
||||||
|
│ └── [Größe M] <- Blattknoten / Reines Kind (Endprodukt / verkaufbare SKU)
|
||||||
|
│
|
||||||
|
└── [Zwischenknoten: Blau] <- Kind UND Vater (fatherarticle_id != NULL, is_father = 1)
|
||||||
|
├── [Größe S] <- Blattknoten / Reines Kind (Endprodukt / verkaufbare SKU)
|
||||||
|
└── [Größe L] <- Blattknoten / Reines Kind (Endprodukt / verkaufbare SKU)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Die zwei Zustände von Knoten im Varianten-Baum:
|
||||||
|
|
||||||
|
1. **Innere Knoten / Zwischenknoten (Vater & Kind zugleich)**:
|
||||||
|
- Gruppieren weitere Unterausprägungen (z. B. Farbvariante „Rot“ mit untergeordneten Größen).
|
||||||
|
- Sind noch **nicht vollständig spezifiziert** $\rightarrow$ verhalten sich wie Vaterartikel:
|
||||||
|
- **Kein** eigener physischer Lagerbestand.
|
||||||
|
- **Kein** eigener Preis.
|
||||||
|
- **Kein** Lieferant.
|
||||||
|
- **Nicht** direkt auf Belegen / Kasse buchbar.
|
||||||
|
- **Fotos**: Dienen oft als Träger für Farbfotos, die von allen darunterliegenden Größen geerbt werden.
|
||||||
|
|
||||||
|
2. **Blattknoten (Reines Kind am Ende des Astes)**:
|
||||||
|
- Vollständig spezifizierter physischer Verkaufs- und Einkaufsartikel.
|
||||||
|
- Eigener Lagerbestand, Barcode/EAN, Lieferantenkonditionen, Verkaufspreise und Belegfähigkeit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Detail-Spezifikation nach Artikeltyp
|
||||||
|
|
||||||
|
### A. Stückliste (BOM / Bill of Materials / Bundle / Set)
|
||||||
|
*Technischer Bezug*: `article.articlelist_id -> articlelist`
|
||||||
|
|
||||||
|
* **Kann auch Stückliste sein?**:
|
||||||
|
- **Ja (Mehrstufig)**: Eine Stückliste ist bereits eine Stückliste. Komponenten können ihrerseits Stücklisten sein (verschachtelte Baugruppen). Selbstreferenz / Zirkelbezug ist verboten.
|
||||||
|
* **Kann auch Variations-Vater sein?**:
|
||||||
|
- **Nein**: Im Frontend und Backend blockiert (`Ein Stücklisten-Artikel kann kein Vaterartikel sein.`). Ein Vater ist ein abstraktes Gruppierungselement, kein Bündel konkreter Teile.
|
||||||
|
* **Kann auch Variations-Kind sein?**:
|
||||||
|
- **Ja**: Ein Blattknoten im Variantenbaum kann als Stückliste definiert sein (z. B. *„Geschenkset T-Shirt – Größe L“* besteht aus *T-Shirt L + Box + Karte*).
|
||||||
|
* **Kann Optionen haben?**:
|
||||||
|
- **Ja**: `articlelist` und `optionslist` koexistieren harmonisch nebeneinander (z. B. PC-Set mit wählbaren Upgrades).
|
||||||
|
* **Hat Bestand?**:
|
||||||
|
- **Virtuell abgeleitet**: Ergibt sich dynamisch aus der kleinsten verfügbaren Komponentenmenge:
|
||||||
|
$$\text{Bestand}_{\text{Set}} = \min_{i} \left( \left\lfloor \frac{\text{Bestand}_i}{\text{Menge}_i} \right\rfloor \right)$$
|
||||||
|
Bei Verkauf werden die Komponenten aus dem Lager abgebucht.
|
||||||
|
* **Eigene Eigenschaften (Hersteller, Kategorie)?**:
|
||||||
|
- **Ja**: Eigenständige Zuweisung von Marke (`brand_id`), Kategorien (`categoryarticle`), Name und Beschreibung.
|
||||||
|
* **Hat Lieferanten?**:
|
||||||
|
- **Ja**: Wenn der Lieferant das fertige Set als **1 Artikel** liefert (z. B. Handelsset, Werkzeugkoffer oder Starterkit mit eigener Lieferanten-SKU und Set-EK). Bei interner Montage liegen die Lieferanten stattdessen auf den Komponenten.
|
||||||
|
* **Kann auf Beleg sein?**:
|
||||||
|
- **Ja**: Voll belegfähig auf Kasse (POS), Angebot, Auftrag, Lieferschein und Rechnung.
|
||||||
|
* **Hat einen Preis?**:
|
||||||
|
- **Ja**: Eigener Verkaufspreis in `price` (Set-Preis mit Steuer und Preisstaffeln).
|
||||||
|
* **Hat individuelle Fotos?**:
|
||||||
|
- **Ja**: Zeigt das Set/Bundle als Gesamtheit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B. Variations-Vater (Parent Variant Article)
|
||||||
|
*Technischer Bezug*: `article.is_father = 1`, `fatherarticle_id = NULL` *(Wurzelknoten)*
|
||||||
|
|
||||||
|
* **Kann auch Stückliste sein?**:
|
||||||
|
- **Nein**: Vaterartikel können keine Stückliste besitzen.
|
||||||
|
* **Kann auch Variations-Vater sein?**:
|
||||||
|
- **Ja**: Ist der Vater.
|
||||||
|
* **Kann auch Variations-Kind sein?**:
|
||||||
|
- **Ja, als Zwischenknoten**: Im Variantenbaum kann ein Vaterartikel seinerseits Kind eines übergeordneten Vaters sein (`fatherarticle_id != NULL`).
|
||||||
|
* **Kann Optionen haben?**:
|
||||||
|
- **Nein**: Optionen werden erst an der konkreten verkaufbaren Variante angehängt.
|
||||||
|
* **Hat Bestand?**:
|
||||||
|
- **Nein**: Wird auf Variantenebene geführt.
|
||||||
|
* **Eigene Eigenschaften (Hersteller, Kategorie)?**:
|
||||||
|
- **Ja**: Definiert gemeinsame Basiseigenschaften (Hersteller, Hauptkategorie, Grundbeschreibung), die von den Kindern geerbt werden.
|
||||||
|
* **Hat Lieferanten?**:
|
||||||
|
- **Nein**: Lieferanten und EK-Preise werden auf Variantenebene gepflegt.
|
||||||
|
* **Kann auf Beleg sein?**:
|
||||||
|
- **Nein**: Da abstrakt (nicht greifbar / nicht ausgeliefert), kann ein Vater nie auf einem Beleg stehen.
|
||||||
|
* **Hat einen Preis?**:
|
||||||
|
- **Nein**: Eigene Preise existieren nur an den konkreten Ausprägungen (im Katalog ggf. nur abgeleitete Anzeige wie *„ab X €“*).
|
||||||
|
* **Hat individuelle Fotos?**:
|
||||||
|
- **Ja**: Dient als Haupt-/Familienbild im Produktkatalog.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C. Variations-Kind (Child Variant Article)
|
||||||
|
*Technischer Bezug*: `article.fatherarticle_id -> article.id`
|
||||||
|
|
||||||
|
* **Kann auch Stückliste sein?**:
|
||||||
|
- **Ja**: Als Blattknoten kann die Variante als Set aus mehreren Komponenten aufgebaut sein.
|
||||||
|
* **Kann auch Variations-Vater sein?**:
|
||||||
|
- **Ja**: Kann als Zwischenknoten im Baum weitere Unterausprägungen besitzen.
|
||||||
|
* **Kann auch Variations-Kind sein?**:
|
||||||
|
- **Ja**: Ist ein Kind des übergeordneten Knotens.
|
||||||
|
* **Kann Optionen haben?**:
|
||||||
|
- **Ja**: Als verkaufbarer Blattknoten können Optionen zugewiesen werden.
|
||||||
|
* **Hat Bestand?**:
|
||||||
|
- **Ja (als Blattknoten)**: Physischer Lagerbestand pro Lagerplatz mit Zu-/Abgangsbuchungen. *(Als Zwischenknoten: Nein).*
|
||||||
|
* **Eigene Eigenschaften (Hersteller, Kategorie)?**:
|
||||||
|
- **Ja / Geerbt**: Erbt standardmäßig Hersteller und Kategorie vom Elternknoten, kann aber abweichende Merkmale und Texte besitzen.
|
||||||
|
* **Hat Lieferanten?**:
|
||||||
|
- **Ja (als Blattknoten)**: Eigene Lieferantenbeziehungen, Lieferanten-SKU, Gebindegrößen (PU) und Einkaufspreise.
|
||||||
|
* **Kann auf Beleg sein?**:
|
||||||
|
- **Ja (als Blattknoten)**: Ist der eigentliche Verkaufsartikel mit Barcode/EAN für Kasse, Angebot, Lieferschein und Rechnung.
|
||||||
|
* **Hat einen Preis?**:
|
||||||
|
- **Ja (als Blattknoten)**: Eigene Preise, Steuerklassen, Staffelpreise und Kundengruppen-Preise.
|
||||||
|
* **Hat individuelle Fotos?**:
|
||||||
|
- **Ja**: Spezifisches Foto der Ausprägung (mit Fallback auf Elternknoten/Wurzel, falls keines vorhanden ist).
|
||||||
@@ -758,6 +758,7 @@ export class StorageManager extends ApiClient {
|
|||||||
name: a.name,
|
name: a.name,
|
||||||
eans: a.eans ?? [],
|
eans: a.eans ?? [],
|
||||||
categories: Array.isArray(a.categoryIds) ? a.categoryIds.join(',') : null,
|
categories: Array.isArray(a.categoryIds) ? a.categoryIds.join(',') : null,
|
||||||
|
isFather: Boolean(a.isFather ?? a.is_father),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { enqueueSnackbar } from 'notistack';
|
|||||||
import { artikelDetailPath } from '../articleRoute';
|
import { artikelDetailPath } from '../articleRoute';
|
||||||
import { article as articleApi } from '../../../api';
|
import { article as articleApi } from '../../../api';
|
||||||
import attributeAutomat, { selectAttributes } from '../../../automats/attributeAutomat';
|
import attributeAutomat, { selectAttributes } from '../../../automats/attributeAutomat';
|
||||||
import artikelAutomat from '../../../automats/artikelAutomat';
|
import artikelAutomat, { getArticle } from '../../../automats/artikelAutomat';
|
||||||
import { notifyError } from '../../../utils/notify';
|
import { notifyError } from '../../../utils/notify';
|
||||||
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
import { StorageManagerContext } from '../../../api/storageManagerContext';
|
||||||
|
|
||||||
@@ -352,6 +352,16 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isTargetFather = Boolean(
|
||||||
|
stuecklisteTargetArticle.isFather ||
|
||||||
|
stuecklisteTargetArticle.is_father ||
|
||||||
|
getArticle(stuecklisteTargetArticle.sku)?.isFather
|
||||||
|
);
|
||||||
|
if (isTargetFather) {
|
||||||
|
enqueueSnackbar('Ein Vaterartikel kann nicht als Komponente einer Stückliste hinzugefügt werden.', { variant: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentItems = Array.isArray(article.articlelist) ? article.articlelist : [];
|
const currentItems = Array.isArray(article.articlelist) ? article.articlelist : [];
|
||||||
const count = Math.max(1, Number(stuecklisteCount) || 1);
|
const count = Math.max(1, Number(stuecklisteCount) || 1);
|
||||||
|
|
||||||
@@ -468,6 +478,16 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isTargetFather = Boolean(
|
||||||
|
optionTargetArticle.isFather ||
|
||||||
|
optionTargetArticle.is_father ||
|
||||||
|
getArticle(optionTargetArticle.sku)?.isFather
|
||||||
|
);
|
||||||
|
if (isTargetFather) {
|
||||||
|
enqueueSnackbar('Ein Vaterartikel kann nicht als Option hinzugefügt werden.', { variant: 'error' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentOptions = Array.isArray(article.options) ? article.options : [];
|
const currentOptions = Array.isArray(article.options) ? article.options : [];
|
||||||
const count = Math.max(1, Number(optionCount) || 1);
|
const count = Math.max(1, Number(optionCount) || 1);
|
||||||
|
|
||||||
@@ -1376,8 +1396,10 @@ export default class ArticleViewLists extends PureComponent {
|
|||||||
(a) => !assignedAttributeIds.has(a.id)
|
(a) => !assignedAttributeIds.has(a.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter available articles for Autocomplete (cannot select self)
|
// Filter available articles for Autocomplete (cannot select self, and cannot select father articles)
|
||||||
const selectableOtherArticles = allArticles.filter((a) => a.sku !== articleData.sku);
|
const selectableOtherArticles = allArticles.filter(
|
||||||
|
(a) => a.sku !== articleData.sku && !a.isFather && !a.is_father
|
||||||
|
);
|
||||||
|
|
||||||
// Find father attribute definition & values
|
// Find father attribute definition & values
|
||||||
const fatherAttrDef = isFather
|
const fatherAttrDef = isFather
|
||||||
|
|||||||
Reference in New Issue
Block a user