diff --git a/src/components/manifest/ManifestFormLine.jsx b/src/components/manifest/ManifestFormLine.jsx
index d501604..431157c 100644
--- a/src/components/manifest/ManifestFormLine.jsx
+++ b/src/components/manifest/ManifestFormLine.jsx
@@ -8,7 +8,6 @@ import IconButton from '@mui/material/IconButton';
import TableCell from '@mui/material/TableCell';
import TableRow from '@mui/material/TableRow';
import TextField from '@mui/material/TextField';
-import Tooltip from '@mui/material/Tooltip';
import Typography from '@mui/material/Typography';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
@@ -608,9 +607,6 @@ class ManifestFormLine extends PureComponent {
: (rawWarehouses.length > 0 || isTracked
? rawWarehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0)
: null));
- const warehouseTooltip = rawWarehouses.length > 0
- ? rawWarehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ')
- : (isTracked === false ? 'Keine Bestandsführung' : undefined);
const lineAmount = parseNum(getFirstVal(this.props.line?.amount, this.props.line?.menge)) ?? 0;
return (
@@ -730,93 +726,65 @@ class ManifestFormLine extends PureComponent {
}}
>
{/* Bestand */}
- stockVal
+ ? 'warning.main'
+ : 'text.secondary',
+ fontSize: '0.72rem',
+ }}
>
- stockVal
- ? 'warning.main'
- : 'text.secondary',
- fontSize: '0.72rem',
- }}
- >
- Bestand: {isTracked === false ? '∞' : formatStock(stockVal)}
-
-
+ Bestand: {isTracked === false ? '∞' : formatStock(stockVal)}
+
{/* Marge */}
- 0 && effectiveMarge != null ? ` · Nach ${rebate}% Rabatt: ${effectiveMarge.toFixed(1).replace('.', ',')}%` : ''}`
- : 'Kein EK- oder VK-Preis hinterlegt'
- }
+
+
-
- Marge: {marge != null ? `${Math.floor(marge)}%` : '—'}
-
-
+ Marge: {marge != null ? `${Math.floor(marge)}%` : '—'}
+
+
{/* EK Netto */}
-
-
- EK Netto: {ekNetto != null ? formatPrice(ekNetto) : '—'}
-
-
+ EK Netto: {ekNetto != null ? formatPrice(ekNetto) : '—'}
+
+
{/* EK Brutto */}
-
-
- EK Brutto: {ekBrutto != null ? formatPrice(ekBrutto) : '—'}
-
-
+ EK Brutto: {ekBrutto != null ? formatPrice(ekBrutto) : '—'}
+
+
{/* VK Netto */}
0 || isDeliveryTracked
? rawWarehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0)
: null));
- const deliveryWarehouseTooltip = rawWarehouses.length > 0
- ? rawWarehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ')
- : (isDeliveryTracked === false ? 'Keine Bestandsführung' : undefined);
return (
{/* Unlink button at the front */}
{this.props.onRemoveSupplierArticle && supplierArticle.id != null && (
-
-
-
-
-
-
-
+
+
+
+
+
)}
{/* Article Photo Thumbnail */}
@@ -1109,48 +1066,34 @@ class ManifestFormLine extends PureComponent {
}}
>
{/* Bestand */}
-
+ Bestand: {isDeliveryTracked === false ? '∞' : formatStock(deliveryStockVal)}
+
+
+ {/* Marge */}
+ {margeDelivery != null && (
- Bestand: {isDeliveryTracked === false ? '∞' : formatStock(deliveryStockVal)}
+ Marge: {Math.floor(margeDelivery)}%
-
-
- {/* Marge */}
- {margeDelivery != null && (
-
-
- Marge: {Math.floor(margeDelivery)}%
-
-
)}
{priceNet != null && (
@@ -1363,13 +1306,6 @@ class ManifestFormLine extends PureComponent {
aria-label="Zeile entfernen"
disabled={saving || Boolean(this.props.isOnShipment) || isFullyLedgered}
onClick={this.handleRemove}
- title={
- isFullyLedgered
- ? 'Position ist bereits vollständig im Ledger verbucht und kann nicht entfernt werden'
- : this.props.isOnShipment
- ? 'Artikel ist bereits auf einem Lieferschein und kann nicht entfernt werden'
- : 'Zeile entfernen'
- }
sx={{ p: 0.35 }}
>
diff --git a/src/components/manifest/ManifestHeader.jsx b/src/components/manifest/ManifestHeader.jsx
index ef28419..d55602c 100644
--- a/src/components/manifest/ManifestHeader.jsx
+++ b/src/components/manifest/ManifestHeader.jsx
@@ -27,6 +27,9 @@ import entityAutomat from '../../automats/entityAutomat';
import customergroupAutomat from '../../automats/customergroupAutomat';
import supplierAutomat from '../../automats/supplierAutomat';
import numberAutomat from '../../automats/numberAutomat';
+import quoteAutomat from '../../automats/quoteAutomat';
+import orderAutomat from '../../automats/orderAutomat';
+import deliveryAutomat from '../../automats/deliveryAutomat';
import { getNextDocumentNumber, ensureDocumentNumber } from '../../utils/documentNumber';
import {
SURFACE_RAISED,
@@ -233,11 +236,30 @@ class ManifestHeader extends PureComponent {
this.props.onPdfUpload?.(file);
}
+ subscribeListAutomat(kind) {
+ this.unsubList?.();
+ this.unsubList = null;
+ const automat =
+ kind === 'quote'
+ ? quoteAutomat
+ : kind === 'order'
+ ? orderAutomat
+ : kind === 'delivery'
+ ? deliveryAutomat
+ : null;
+ if (automat) {
+ this.unsubList = automat.subscribe(this, () => {
+ if (!this._cancelled) this.forceUpdate();
+ });
+ }
+ }
+
componentDidMount() {
this._cancelled = false;
this.unsubNumber = numberAutomat.subscribe(this, () => {
this.forceUpdate();
});
+ this.subscribeListAutomat(this.props.kind);
void numberAutomat.actions.ensureLoaded().catch(() => {});
if (!this.props.number && this.props.docId == null) {
void ensureDocumentNumber(this.props.kind)
@@ -249,6 +271,9 @@ class ManifestHeader extends PureComponent {
}
componentDidUpdate(prevProps) {
+ if (prevProps.kind !== this.props.kind) {
+ this.subscribeListAutomat(this.props.kind);
+ }
if (
!this.props.number &&
this.props.docId == null &&
@@ -265,6 +290,7 @@ class ManifestHeader extends PureComponent {
componentWillUnmount() {
this._cancelled = true;
this.unsubNumber?.();
+ this.unsubList?.();
}
render() {
diff --git a/src/utils/documentNumber.js b/src/utils/documentNumber.js
index 346b85d..876c5ca 100644
--- a/src/utils/documentNumber.js
+++ b/src/utils/documentNumber.js
@@ -2,6 +2,10 @@ import numberAutomat from '../automats/numberAutomat';
import quoteAutomat from '../automats/quoteAutomat';
import orderAutomat from '../automats/orderAutomat';
import deliveryAutomat from '../automats/deliveryAutomat';
+import { order as orderApi, quote as quoteApi, delivery as deliveryApi } from '../api';
+
+/** Highest integer counter found per document kind. */
+const maxKnownNumberByKind = new Map();
/**
* Formats a number pattern given prefix, padding width, and counter.
@@ -20,6 +24,7 @@ export function formatNumberPattern(prefix, padding, next) {
/**
* Returns the next document number for quote, order, or delivery based on company sequences.
+ * Always verifies that the returned number does not collide with existing documents.
*
* @param {'quote'|'order'|'delivery'|string} kind
* @returns {string}
@@ -27,59 +32,63 @@ export function formatNumberPattern(prefix, padding, next) {
export function getNextDocumentNumber(kind) {
const numbers = numberAutomat.state.items ?? [];
const seq = numbers.find((r) => r.type === kind);
- if (seq) {
- return formatNumberPattern(seq.prefix, seq.padding, seq.next);
- }
-
- // Detect style from existing company sequences (sku, delivery, entity)
- const skuSeq = numbers.find((r) => r.type === 'sku');
- const deliverySeq = numbers.find((r) => r.type === 'delivery');
- const entitySeq = numbers.find((r) => r.type === 'entity');
- const refSeq = skuSeq || deliverySeq || entitySeq;
-
- const pad = refSeq ? Math.max(0, Number(refSeq.padding) || 0) : 6;
- const prefixStyle = refSeq?.prefix ?? '';
let prefix = '';
+ let pad = 6;
let startNum = 1;
- if (kind === 'delivery') {
- prefix = prefixStyle.startsWith('DEL-') ? 'DEL-' : (prefixStyle.startsWith('L-') ? 'L-' : 'LS-');
- } else if (
- prefixStyle.startsWith('ART-') ||
- prefixStyle.startsWith('LS-') ||
- prefixStyle.startsWith('KD-') ||
- prefixStyle.endsWith('-')
- ) {
- // Standard German ERP prefix style (ART-, KD-, LS-, KORR-)
- prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : '');
- } else if (
- prefixStyle.startsWith('SKU-') ||
- prefixStyle.startsWith('DEL-') ||
- prefixStyle.startsWith('C-')
- ) {
- prefix = kind === 'quote' ? 'Q-' : (kind === 'order' ? 'ORD-' : '');
- } else if (
- prefixStyle.startsWith('A-') ||
- prefixStyle.startsWith('L-') ||
- prefixStyle.startsWith('K-')
- ) {
- prefix = kind === 'quote' ? 'AG-' : (kind === 'order' ? 'AU-' : '');
- } else if (!prefixStyle && refSeq?.next >= 10000) {
- prefix = '';
- startNum = kind === 'quote' ? 40001 : 50001;
+ if (seq) {
+ prefix = seq.prefix ?? '';
+ pad = Math.max(0, Number(seq.padding) || 0);
+ startNum = Math.max(1, Number(seq.next) || 1);
} else {
- prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : '');
+ // Detect style from existing company sequences (sku, delivery, entity)
+ const skuSeq = numbers.find((r) => r.type === 'sku');
+ const deliverySeq = numbers.find((r) => r.type === 'delivery');
+ const entitySeq = numbers.find((r) => r.type === 'entity');
+ const refSeq = skuSeq || deliverySeq || entitySeq;
+
+ pad = refSeq ? Math.max(0, Number(refSeq.padding) || 0) : 6;
+ const prefixStyle = refSeq?.prefix ?? '';
+
+ if (kind === 'delivery') {
+ prefix = prefixStyle.startsWith('DEL-') ? 'DEL-' : (prefixStyle.startsWith('L-') ? 'L-' : 'LS-');
+ } else if (
+ prefixStyle.startsWith('ART-') ||
+ prefixStyle.startsWith('LS-') ||
+ prefixStyle.startsWith('KD-') ||
+ prefixStyle.endsWith('-')
+ ) {
+ // Standard German ERP prefix style (ART-, KD-, LS-, KORR-)
+ prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : '');
+ } else if (
+ prefixStyle.startsWith('SKU-') ||
+ prefixStyle.startsWith('DEL-') ||
+ prefixStyle.startsWith('C-')
+ ) {
+ prefix = kind === 'quote' ? 'Q-' : (kind === 'order' ? 'ORD-' : '');
+ } else if (
+ prefixStyle.startsWith('A-') ||
+ prefixStyle.startsWith('L-') ||
+ prefixStyle.startsWith('K-')
+ ) {
+ prefix = kind === 'quote' ? 'AG-' : (kind === 'order' ? 'AU-' : '');
+ } else if (!prefixStyle && refSeq?.next >= 10000) {
+ prefix = '';
+ startNum = kind === 'quote' ? 40001 : 50001;
+ } else {
+ prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : '');
+ }
}
// Check existing docs from quoteAutomat / orderAutomat / deliveryAutomat to avoid collisions
const listAutomat = kind === 'quote' ? quoteAutomat : (kind === 'order' ? orderAutomat : (kind === 'delivery' ? deliveryAutomat : null));
const existingDocs = Array.isArray(listAutomat?.state?.items) ? listAutomat.state.items : [];
- let maxFound = startNum - 1;
+ let maxFound = Math.max(startNum - 1, maxKnownNumberByKind.get(kind) ?? 0);
for (const doc of existingDocs) {
if (!doc || typeof doc !== 'object') continue;
- const docNum = String(doc.number ?? '').trim();
+ const docNum = String(doc.number ?? doc.orderNumber ?? '').trim();
if (!docNum) continue;
if (prefix && docNum.startsWith(prefix)) {
const rest = docNum.slice(prefix.length);
@@ -95,12 +104,15 @@ export function getNextDocumentNumber(kind) {
}
}
+ maxKnownNumberByKind.set(kind, maxFound);
+
const nextVal = maxFound >= startNum ? maxFound + 1 : startNum;
return formatNumberPattern(prefix, pad, nextVal);
}
/**
- * Asynchronously ensures required stores are loaded, then returns the next document number.
+ * Asynchronously ensures required stores are loaded, queries recent documents to detect
+ * the highest existing number, then returns the next document number.
*
* @param {'quote'|'order'|'delivery'|string} kind
* @returns {Promise}
@@ -111,6 +123,51 @@ export async function ensureDocumentNumber(kind) {
numberAutomat.actions.ensureLoaded().catch(() => {}),
listAutomat?.actions?.ensureLoaded ? listAutomat.actions.ensureLoaded().catch(() => {}) : Promise.resolve(),
]);
+
+ try {
+ let recentDocs = [];
+ if (kind === 'order') {
+ const res = await orderApi.list({ offset: 0, limit: 10 });
+ recentDocs = Array.isArray(res?.orders) ? res.orders : [];
+ } else if (kind === 'quote') {
+ const res = await quoteApi.list({ offset: 0, limit: 10 });
+ recentDocs = Array.isArray(res?.quotes) ? res.quotes : [];
+ } else if (kind === 'delivery') {
+ const res = await deliveryApi.list({ offset: 0, limit: 10 });
+ recentDocs = Array.isArray(res?.deliveries) ? res.deliveries : [];
+ }
+
+ if (recentDocs.length > 0) {
+ const numbers = numberAutomat.state.items ?? [];
+ const seq = numbers.find((r) => r.type === kind);
+ let prefix = seq?.prefix;
+ if (prefix == null) {
+ prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : 'LS-');
+ }
+
+ let maxFound = maxKnownNumberByKind.get(kind) ?? 0;
+ for (const doc of recentDocs) {
+ const docNum = String(doc.number ?? doc.orderNumber ?? '').trim();
+ if (!docNum) continue;
+ if (prefix && docNum.startsWith(prefix)) {
+ const rest = docNum.slice(prefix.length);
+ const parsed = parseInt(rest, 10);
+ if (!Number.isNaN(parsed) && parsed > maxFound) {
+ maxFound = parsed;
+ }
+ } else if (!prefix) {
+ const parsed = parseInt(docNum, 10);
+ if (!Number.isNaN(parsed) && parsed > maxFound) {
+ maxFound = parsed;
+ }
+ }
+ }
+ maxKnownNumberByKind.set(kind, maxFound);
+ }
+ } catch (err) {
+ console.warn(`[documentNumber] ensureDocumentNumber error fetching latest ${kind}:`, err);
+ }
+
return getNextDocumentNumber(kind);
}