diff --git a/src/api/order.js b/src/api/order.js
index 299ed10..aa81e34 100644
--- a/src/api/order.js
+++ b/src/api/order.js
@@ -93,7 +93,7 @@ export const order = {
* POST /api/order/:id/shipment
* Creates shipment from order.
* @param {number|string} id Order ID
- * @param {object} [body] Optional payload: { date?, externalNumber?, lines?: Array<{ sku: string, amount?: number }> }
+ * @param {object} [body] Optional payload: { date?, externalNumber?, lines?: Array<{ sku?: string, orderLineId?: number, amount?: number }> }
* → 201 { shipment: Shipment }
*/
// #region POST /api/order/:id/shipment — createShipment
diff --git a/src/automats/manifestDraftAutomat.js b/src/automats/manifestDraftAutomat.js
index 60cde05..573028c 100644
--- a/src/automats/manifestDraftAutomat.js
+++ b/src/automats/manifestDraftAutomat.js
@@ -343,6 +343,9 @@ export function getManifestDirtyDetails(current, baseline) {
if (String(cl.unit ?? '').trim() !== String(bl.unit ?? '').trim()) {
lineFieldDiffs.unit = true;
}
+ if (Boolean(cl.shippable ?? true) !== Boolean(bl.shippable ?? true)) {
+ lineFieldDiffs.shippable = true;
+ }
if (Object.keys(lineFieldDiffs).length > 0) {
dirtyLineKeys.push(key);
diff --git a/src/automats/orderAutomat.js b/src/automats/orderAutomat.js
index 0b9947b..f41a9e7 100644
--- a/src/automats/orderAutomat.js
+++ b/src/automats/orderAutomat.js
@@ -453,7 +453,7 @@ export function isOrderPaid(order) {
export function isOrderDelivered(order) {
if (!order) return false;
- return Boolean(order.shipped ?? order.delivered ?? order.isDelivered ?? order.is_delivered ?? order.deliveredAt);
+ return Boolean(order.isShipped ?? order.shipped ?? order.delivered ?? order.isDelivered ?? order.is_delivered ?? order.deliveredAt);
}
export function getOrderDate(order) {
diff --git a/src/components/manifest/ManifestForm.jsx b/src/components/manifest/ManifestForm.jsx
index 1952b72..77c4f56 100644
--- a/src/components/manifest/ManifestForm.jsx
+++ b/src/components/manifest/ManifestForm.jsx
@@ -106,6 +106,7 @@ class ManifestForm extends PureComponent {
paidTotal: initialEdit?.paidTotal ?? null,
paid: Boolean(initialEdit?.paid),
shipped: Boolean(initialEdit?.shipped),
+ isShipped: Boolean(initialEdit?.isShipped ?? initialEdit?.shipped),
delivered: Boolean(initialEdit?.delivered),
isDirty: Boolean(initialEdit?.isDirty),
dirtyFields: initialEdit?.dirtyFields || null,
@@ -249,6 +250,7 @@ class ManifestForm extends PureComponent {
paidTotal: state.paidTotal ?? null,
paid: Boolean(state.paid),
shipped: Boolean(state.shipped),
+ isShipped: Boolean(state.isShipped ?? state.shipped),
delivered: Boolean(state.delivered),
isDirty: Boolean(state.isDirty),
dirtyFields: state.dirtyFields,
@@ -477,6 +479,7 @@ class ManifestForm extends PureComponent {
paidTotal: doc.paidTotal ?? null,
paid: Boolean(doc.paid),
shipped: Boolean(doc.shipped),
+ isShipped: Boolean(doc.isShipped ?? doc.shipped),
delivered: Boolean(doc.delivered),
};
@@ -812,24 +815,52 @@ class ManifestForm extends PureComponent {
}
isFullyDelivered() {
- if (this.state.delivered) return true;
+ if (this.state.delivered || this.state.isShipped || this.state.shipped) return true;
const lines = this.state.lines || [];
- const validLines = lines.filter((l) => l.sku && Number(l.amount) > 0);
- if (validLines.length === 0) return false;
+ const shippableLines = lines.filter((l) => l.shippable !== false && Number(l.amount) > 0);
+ if (shippableLines.length === 0) return lines.length > 0;
const shippedBySku = {};
+ const shippedByOrderLineId = {};
+ const shippedByName = {};
for (const sh of this.state.shipments || []) {
for (const sl of sh.lines || []) {
+ const amt = Number(sl.amount) || 0;
if (sl.sku) {
const k = String(sl.sku).trim().toLowerCase();
- shippedBySku[k] = (shippedBySku[k] || 0) + (Number(sl.amount) || 0);
+ shippedBySku[k] = (shippedBySku[k] || 0) + amt;
+ }
+ if (sl.orderLineId != null) {
+ shippedByOrderLineId[sl.orderLineId] = (shippedByOrderLineId[sl.orderLineId] || 0) + amt;
+ }
+ if (sl.name) {
+ const n = String(sl.name).trim().toLowerCase();
+ shippedByName[n] = (shippedByName[n] || 0) + amt;
}
}
}
- return validLines.every((l) => {
- const k = String(l.sku).trim().toLowerCase();
- return (shippedBySku[k] || 0) >= Number(l.amount);
+ const remainingShippedByName = { ...shippedByName };
+
+ return shippableLines.every((l) => {
+ const amt = Number(l.amount) || 0;
+ if (l.sku) {
+ const k = String(l.sku).trim().toLowerCase();
+ return (shippedBySku[k] || 0) >= amt;
+ }
+ if (l.id != null && shippedByOrderLineId[l.id] != null) {
+ return shippedByOrderLineId[l.id] >= amt;
+ }
+ if (l.name) {
+ const n = String(l.name).trim().toLowerCase();
+ const pool = remainingShippedByName[n] || 0;
+ if (pool >= amt) {
+ remainingShippedByName[n] = pool - amt;
+ return true;
+ }
+ return false;
+ }
+ return false;
});
}
@@ -838,10 +869,15 @@ class ManifestForm extends PureComponent {
const lineToRemove = prev.lines.find((line) => line.key === key);
if (lineToRemove && this.props.kind === 'order') {
const rawSku = lineToRemove.sku != null ? String(lineToRemove.sku).trim().toLowerCase() : '';
+ const orderLineId = lineToRemove.id;
+ const lineName = lineToRemove.name != null ? String(lineToRemove.name).trim().toLowerCase() : '';
const isShipped = Boolean(
- rawSku &&
(prev.shipments || []).some((sh) =>
- (sh.lines || []).some((sl) => sl.sku && String(sl.sku).trim().toLowerCase() === rawSku)
+ (sh.lines || []).some((sl) =>
+ (rawSku && sl.sku && String(sl.sku).trim().toLowerCase() === rawSku) ||
+ (orderLineId != null && sl.orderLineId != null && sl.orderLineId === orderLineId) ||
+ (!rawSku && !sl.sku && lineName && sl.name && String(sl.name).trim().toLowerCase() === lineName)
+ )
)
);
if (isShipped) {
diff --git a/src/components/manifest/ManifestFormLine.jsx b/src/components/manifest/ManifestFormLine.jsx
index 8cab002..fba22cc 100644
--- a/src/components/manifest/ManifestFormLine.jsx
+++ b/src/components/manifest/ManifestFormLine.jsx
@@ -2,6 +2,7 @@ import { Fragment, PureComponent } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
+import Checkbox from '@mui/material/Checkbox';
import Chip from '@mui/material/Chip';
import CircularProgress from '@mui/material/CircularProgress';
import IconButton from '@mui/material/IconButton';
@@ -155,6 +156,7 @@ class ManifestFormLine extends PureComponent {
}
handleStartEdit(columnId) {
+ if (columnId === 'pos' || columnId === 'shippable') return;
const { saving } = this.props;
const isFullyLedgered = this.isFullyLedgered();
const isDisabled = saving || isFullyLedgered || (columnId === 'sku' && Boolean(this.props.isOnShipment));
@@ -236,6 +238,79 @@ class ManifestFormLine extends PureComponent {
);
}
+ if (id === 'shippable') {
+ const isShippable = line.shippable !== false;
+ const isFullyLedgered = this.isFullyLedgered();
+ const isDisabled = saving || isFullyLedgered;
+
+ const hasPosCol = columns?.[0]?.id === 'pos';
+ const isFirstInput = hasPosCol ? colIdx === 1 : colIdx === 0;
+ const isLast = colIdx === totalCols - 1;
+
+ const topLeftRadius = isFirstInput && isFirstLine ? 4 : 0;
+ const topRightRadius = isLast && isFirstLine ? 4 : 0;
+ const bottomRightRadius = isLast && isLastLine ? 4 : 0;
+ const bottomLeftRadius = isFirstInput && isLastLine ? 4 : 0;
+ const borderRadius = `${topLeftRadius}px ${topRightRadius}px ${bottomRightRadius}px ${bottomLeftRadius}px`;
+
+ const { dirtyFields } = this.props;
+ const isFieldDirty = Boolean(dirtyFields && (dirtyFields.__all__ || dirtyFields[id]));
+
+ return (
+ onChange(line.key, 'shippable', !isShippable)}
+ >
+ {
+ e.stopPropagation();
+ onChange(line.key, 'shippable', e.target.checked);
+ }}
+ slotProps={{
+ input: { 'aria-label': isShippable ? 'Lieferbar' : 'Nicht lieferbar' },
+ }}
+ sx={{ p: 0.5 }}
+ />
+
+ );
+ }
+
let val = line[id];
if (val == null || val === '') {
if (id === 'name' && this.state.cachedArticle?.name) {
diff --git a/src/components/manifest/ManifestLines.jsx b/src/components/manifest/ManifestLines.jsx
index f172eae..03878e8 100644
--- a/src/components/manifest/ManifestLines.jsx
+++ b/src/components/manifest/ManifestLines.jsx
@@ -107,10 +107,14 @@ class ManifestLines extends PureComponent {
{(() => {
const shippedSkus = new Set();
+ const shippedOrderLineIds = new Set();
+ const shippedNames = new Set();
if (kind === 'order' && Array.isArray(shipments)) {
for (const sh of shipments) {
for (const sl of sh.lines || []) {
if (sl.sku) shippedSkus.add(String(sl.sku).trim().toLowerCase());
+ if (sl.orderLineId != null) shippedOrderLineIds.add(sl.orderLineId);
+ if (!sl.sku && sl.name) shippedNames.add(String(sl.name).trim().toLowerCase());
}
}
}
@@ -122,7 +126,12 @@ class ManifestLines extends PureComponent {
return lines.map((line, index) => {
const lineKey = line.key ?? index;
const rawSku = line.sku != null ? String(line.sku).trim() : '';
- const isOnShipment = kind === 'order' && Boolean(rawSku && shippedSkus.has(rawSku.toLowerCase()));
+ const lineName = line.name != null ? String(line.name).trim() : '';
+ const isOnShipment = kind === 'order' && Boolean(
+ (rawSku && shippedSkus.has(rawSku.toLowerCase())) ||
+ (line.id != null && shippedOrderLineIds.has(line.id)) ||
+ (!rawSku && lineName && shippedNames.has(lineName.toLowerCase()))
+ );
const isDelivery = kind === 'delivery';
const matched = isDelivery
? supplierArticlesByLineKey[lineKey] ||
diff --git a/src/components/manifest/OrderShipmentDialog.jsx b/src/components/manifest/OrderShipmentDialog.jsx
index f908cb2..2b1bec4 100644
--- a/src/components/manifest/OrderShipmentDialog.jsx
+++ b/src/components/manifest/OrderShipmentDialog.jsx
@@ -29,17 +29,39 @@ import { formatStock } from '../article/searchFormat';
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
import { todayInputValue } from './manifestConfig';
-// MARK: helper to sum already shipped quantities per SKU across order shipments
-function computeShippedBySku(shipments) {
- const map = {};
+// MARK: helper to sum already shipped quantities per SKU, orderLineId, and line name across shipments
+export function computeShippedSummary(shipments) {
+ const bySku = {};
+ const byOrderLineId = {};
+ const byName = {};
+
for (const sh of shipments || []) {
for (const sl of sh.lines || []) {
+ const amt = Number(sl.amount) || 0;
if (sl.sku) {
- map[sl.sku] = (map[sl.sku] || 0) + (Number(sl.amount) || 0);
+ const k = String(sl.sku).trim();
+ bySku[k] = (bySku[k] || 0) + amt;
+ }
+ if (sl.orderLineId != null) {
+ byOrderLineId[sl.orderLineId] = (byOrderLineId[sl.orderLineId] || 0) + amt;
+ }
+ if (sl.name) {
+ const n = String(sl.name).trim().toLowerCase();
+ byName[n] = (byName[n] || 0) + amt;
}
}
}
- return map;
+ return { bySku, byOrderLineId, byName };
+}
+
+function getItemKey(line, index) {
+ if (line.sku && String(line.sku).trim()) {
+ return `sku:${String(line.sku).trim()}`;
+ }
+ if (line.id != null) {
+ return `id:${line.id}`;
+ }
+ return `idx:${index}`;
}
// MARK: helper to determine next shipment number preview
@@ -78,7 +100,8 @@ export default class OrderShipmentDialog extends PureComponent {
};
this.handleAmountChange = this.handleAmountChange.bind(this);
- this.handleToggleSku = this.handleToggleSku.bind(this);
+ this.handleToggleItem = this.handleToggleItem.bind(this);
+ this.handleToggleSku = this.handleToggleItem; // Alias for backward compatibility
this.handleToggleAll = this.handleToggleAll.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleShipAll = this.handleShipAll.bind(this);
@@ -128,35 +151,146 @@ export default class OrderShipmentDialog extends PureComponent {
}
calculateInitialState(props) {
- const shippedBySku = computeShippedBySku(props.shipments);
+ const { bySku, byOrderLineId, byName } = computeShippedSummary(props.shipments);
+ const remainingShippedByName = { ...byName };
const initialAmounts = {};
const initialSelected = new Set();
- for (const l of props.lines || []) {
- if (!l.sku) continue;
+ (props.lines || []).forEach((l, idx) => {
+ const isShippable = l.shippable !== false;
+ const key = getItemKey(l, idx);
const ordered = Number(l.amount) || 0;
- const shipped = shippedBySku[l.sku] || 0;
- const rem = Math.max(0, ordered - shipped);
- if (rem > 0) {
- initialAmounts[l.sku] = String(rem);
- initialSelected.add(l.sku);
- } else {
- initialAmounts[l.sku] = '0';
+ const sku = l.sku ? String(l.sku).trim() : null;
+ const orderLineId = l.id != null ? l.id : null;
+
+ let shipped = 0;
+ if (sku) {
+ shipped = bySku[sku] || 0;
+ } else if (orderLineId != null && byOrderLineId[orderLineId] != null) {
+ shipped = byOrderLineId[orderLineId];
+ } else if (l.name) {
+ const n = String(l.name).trim().toLowerCase();
+ const pool = remainingShippedByName[n] || 0;
+ const consumed = Math.min(ordered, pool);
+ shipped = consumed;
+ remainingShippedByName[n] = pool - consumed;
}
- }
+
+ const rem = isShippable ? Math.max(0, ordered - shipped) : 0;
+ if (isShippable && rem > 0) {
+ initialAmounts[key] = String(rem);
+ initialSelected.add(key);
+ } else {
+ initialAmounts[key] = '0';
+ }
+ });
+
return { initialAmounts, initialSelected };
}
- handleToggleSku(sku, remaining) {
+ getItems() {
+ const { lines = [], shipments = [], orderId } = this.props;
+ const { lineAmounts, selectedSkus } = this.state;
+ const currentOrderId = orderId != null ? Number(orderId) : null;
+ const { bySku, byOrderLineId, byName } = computeShippedSummary(shipments);
+ const remainingShippedByName = { ...byName };
+
+ return (lines || []).map((line, idx) => {
+ const itemKey = getItemKey(line, idx);
+ const isShippable = line.shippable !== false;
+ const ordered = Number(line.amount) || 0;
+ const sku = line.sku ? String(line.sku).trim() : null;
+ const orderLineId = line.id != null ? line.id : null;
+
+ let shipped = 0;
+ if (sku) {
+ shipped = bySku[sku] || 0;
+ } else if (orderLineId != null && byOrderLineId[orderLineId] != null) {
+ shipped = byOrderLineId[orderLineId];
+ } else if (line.name) {
+ const n = String(line.name).trim().toLowerCase();
+ const pool = remainingShippedByName[n] || 0;
+ const consumed = Math.min(ordered, pool);
+ shipped = consumed;
+ remainingShippedByName[n] = pool - consumed;
+ }
+
+ const remaining = isShippable ? Math.max(0, ordered - shipped) : 0;
+ const isSelected = selectedSkus.has(itemKey);
+ const shipAmount = lineAmounts[itemKey] ?? (remaining > 0 ? String(remaining) : '0');
+
+ let cached = null;
+ let isTracked = null;
+ let stockVal = null;
+ let availableVal = null;
+ let reservedTitle = undefined;
+ let stockTitle = undefined;
+
+ if (sku) {
+ cached = getArticle(sku);
+ const rawWarehouses = Array.isArray(cached?.warehouses) ? cached.warehouses : [];
+ isTracked = cached ? (cached.track_stock ?? cached.trackStock ?? (rawWarehouses.length > 0)) : null;
+ stockVal = cached == null || isTracked === false
+ ? null
+ : (cached.stock != null && !Number.isNaN(Number(cached.stock))
+ ? Number(cached.stock)
+ : (rawWarehouses.length > 0 || isTracked
+ ? rawWarehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0)
+ : null));
+
+ const undeliveredOrders = Array.isArray(cached?.undeliveredOrders) ? cached.undeliveredOrders : [];
+ const otherOrders = currentOrderId != null
+ ? undeliveredOrders.filter((ord) => Number(ord.orderId) !== currentOrderId)
+ : undeliveredOrders;
+ const reservedInOtherOrders = otherOrders.reduce((sum, ord) => sum + (Number(ord.amount) || 0), 0);
+ availableVal = cached == null || isTracked === false
+ ? null
+ : (stockVal != null ? stockVal - reservedInOtherOrders : null);
+
+ reservedTitle = isTracked !== false && reservedInOtherOrders > 0
+ ? `Bereits in anderen Aufträgen reserviert:\n${otherOrders
+ .map((ord) => `• ${ord.orderNumber || (ord.orderId ? `Auftrag #${ord.orderId}` : 'Auftrag')}: ${formatStock(ord.amount)}${ord.customerName ? ` (${ord.customerName})` : ''}`)
+ .join('\n')}`
+ : undefined;
+
+ stockTitle = isTracked !== false && rawWarehouses.length > 1
+ ? `Lagerbestände:\n${rawWarehouses
+ .map((wh) => `• ${wh.name || 'Lager'}: ${formatStock(wh.stock)}`)
+ .join('\n')}`
+ : undefined;
+ }
+
+ return {
+ ...line,
+ itemKey,
+ sku,
+ orderLineId,
+ isShippable,
+ ordered,
+ shipped,
+ remaining,
+ isSelected,
+ shipAmount,
+ cached,
+ isTracked,
+ stockVal,
+ availableVal,
+ reservedTitle,
+ stockTitle,
+ };
+ });
+ }
+
+ handleToggleItem(itemKey, remaining) {
this.setState((prev) => {
const nextSelected = new Set(prev.selectedSkus);
const nextAmounts = { ...prev.lineAmounts };
- if (nextSelected.has(sku)) {
- nextSelected.delete(sku);
+ if (nextSelected.has(itemKey)) {
+ nextSelected.delete(itemKey);
} else {
- nextSelected.add(sku);
- if (!Number(nextAmounts[sku]) || Number(nextAmounts[sku]) <= 0) {
- nextAmounts[sku] = String(remaining);
+ nextSelected.add(itemKey);
+ if (!Number(nextAmounts[itemKey]) || Number(nextAmounts[itemKey]) <= 0) {
+ nextAmounts[itemKey] = String(remaining);
}
}
return { selectedSkus: nextSelected, lineAmounts: nextAmounts };
@@ -165,15 +299,15 @@ export default class OrderShipmentDialog extends PureComponent {
handleToggleAll(openItems) {
this.setState((prev) => {
- const allSelected = openItems.length > 0 && openItems.every((it) => prev.selectedSkus.has(it.sku));
+ const allSelected = openItems.length > 0 && openItems.every((it) => prev.selectedSkus.has(it.itemKey));
const nextSelected = new Set();
const nextAmounts = { ...prev.lineAmounts };
if (!allSelected) {
for (const it of openItems) {
- nextSelected.add(it.sku);
- if (!Number(nextAmounts[it.sku]) || Number(nextAmounts[it.sku]) <= 0) {
- nextAmounts[it.sku] = String(it.remaining);
+ nextSelected.add(it.itemKey);
+ if (!Number(nextAmounts[it.itemKey]) || Number(nextAmounts[it.itemKey]) <= 0) {
+ nextAmounts[it.itemKey] = String(it.remaining);
}
}
}
@@ -181,15 +315,15 @@ export default class OrderShipmentDialog extends PureComponent {
});
}
- handleAmountChange(sku, val) {
+ handleAmountChange(itemKey, val) {
this.setState((prev) => {
- const nextAmounts = { ...prev.lineAmounts, [sku]: val };
+ const nextAmounts = { ...prev.lineAmounts, [itemKey]: val };
const nextSelected = new Set(prev.selectedSkus);
const num = Number(val);
if (num > 0) {
- nextSelected.add(sku);
+ nextSelected.add(itemKey);
} else {
- nextSelected.delete(sku);
+ nextSelected.delete(itemKey);
}
return { lineAmounts: nextAmounts, selectedSkus: nextSelected };
});
@@ -203,11 +337,17 @@ export default class OrderShipmentDialog extends PureComponent {
const { date, externalNumber, lineAmounts, selectedSkus, saving } = this.state;
if (saving || !orderId) return;
+ const items = this.getItems();
const linesToShip = [];
- for (const sku of selectedSkus) {
- const amount = Number(lineAmounts[sku]);
+ for (const it of items) {
+ if (!it.isShippable || !selectedSkus.has(it.itemKey)) continue;
+ const amount = Number(lineAmounts[it.itemKey]);
if (amount > 0) {
- linesToShip.push({ sku, amount });
+ if (it.sku) {
+ linesToShip.push({ sku: it.sku, amount });
+ } else if (it.orderLineId != null) {
+ linesToShip.push({ orderLineId: it.orderLineId, amount });
+ }
}
}
@@ -240,9 +380,7 @@ export default class OrderShipmentDialog extends PureComponent {
this.setState({ saving: false, error: err?.message || String(err) });
}
}
- // #endregion
-
- // MARK: calls orderApi.createShipment to ship all remaining lines
+ // // MARK: calls orderApi.createShipment to ship all remaining lines
// #region POST /api/order/:id/shipment — ship all remaining
async handleShipAll() {
const { orderId, onClose, onSuccess } = this.props;
@@ -275,73 +413,14 @@ export default class OrderShipmentDialog extends PureComponent {
// #endregion
render() {
- const { open, onClose, orderId, orderNumber, lines = [], shipments = [] } = this.props;
- const { date, externalNumber, lineAmounts, selectedSkus, saving, error } = this.state;
- const nextShipmentNumber = getNextShipmentNumber(orderNumber, shipments);
+ const { open, onClose, orderNumber } = this.props;
+ const { date, externalNumber, saving, error } = this.state;
+ const nextShipmentNumber = getNextShipmentNumber(orderNumber, this.props.shipments);
- const shippedBySku = computeShippedBySku(shipments);
- const activeLines = lines.filter((l) => Boolean(l.sku));
- const currentOrderId = orderId != null ? Number(orderId) : null;
-
- const items = activeLines.map((line) => {
- const ordered = Number(line.amount) || 0;
- const shipped = shippedBySku[line.sku] || 0;
- const remaining = Math.max(0, ordered - shipped);
- const isSelected = selectedSkus.has(line.sku);
- const shipAmount = lineAmounts[line.sku] ?? (remaining > 0 ? String(remaining) : '0');
-
- // Stock & reservation details from artikelAutomat
- const cached = getArticle(line.sku);
- const rawWarehouses = Array.isArray(cached?.warehouses) ? cached.warehouses : [];
- const isTracked = cached ? (cached.track_stock ?? cached.trackStock ?? (rawWarehouses.length > 0)) : null;
- const stockVal = cached == null || isTracked === false
- ? null
- : (cached.stock != null && !Number.isNaN(Number(cached.stock))
- ? Number(cached.stock)
- : (rawWarehouses.length > 0 || isTracked
- ? rawWarehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0)
- : null));
-
- const undeliveredOrders = Array.isArray(cached?.undeliveredOrders) ? cached.undeliveredOrders : [];
- const otherOrders = currentOrderId != null
- ? undeliveredOrders.filter((ord) => Number(ord.orderId) !== currentOrderId)
- : undeliveredOrders;
- const reservedInOtherOrders = otherOrders.reduce((sum, ord) => sum + (Number(ord.amount) || 0), 0);
- const availableVal = cached == null || isTracked === false
- ? null
- : (stockVal != null ? stockVal - reservedInOtherOrders : null);
-
- const reservedTitle = isTracked !== false && reservedInOtherOrders > 0
- ? `Bereits in anderen Aufträgen reserviert:\n${otherOrders
- .map((ord) => `• ${ord.orderNumber || (ord.orderId ? `Auftrag #${ord.orderId}` : 'Auftrag')}: ${formatStock(ord.amount)}${ord.customerName ? ` (${ord.customerName})` : ''}`)
- .join('\n')}`
- : undefined;
-
- const stockTitle = isTracked !== false && rawWarehouses.length > 1
- ? `Lagerbestände:\n${rawWarehouses
- .map((wh) => `• ${wh.name || 'Lager'}: ${formatStock(wh.stock)}`)
- .join('\n')}`
- : undefined;
-
- return {
- ...line,
- ordered,
- shipped,
- remaining,
- isSelected,
- shipAmount,
- cached,
- isTracked,
- stockVal,
- availableVal,
- reservedInOtherOrders,
- reservedTitle,
- stockTitle,
- };
- });
-
- const openItems = items.filter((it) => it.remaining > 0);
- const allShipped = activeLines.length > 0 && openItems.length === 0;
+ const items = this.getItems();
+ const shippableItems = items.filter((it) => it.isShippable);
+ const openItems = shippableItems.filter((it) => it.remaining > 0);
+ const allShipped = shippableItems.length > 0 && openItems.length === 0;
const isAllSelected = openItems.length > 0 && openItems.every((it) => it.isSelected);
const selectedCount = openItems.filter((it) => it.isSelected).length;
@@ -451,10 +530,11 @@ export default class OrderShipmentDialog extends PureComponent {
) : (
items.map((item) => {
- const isClosed = item.remaining <= 0;
+ const isNonShippable = !item.isShippable;
+ const isClosed = isNonShippable || item.remaining <= 0;
return (
this.handleToggleSku(item.sku, item.remaining)}
+ onChange={() => this.handleToggleItem(item.itemKey, item.remaining)}
/>
- {item.sku}
+ {item.sku || '–'}
{item.name || '–'}
{item.ordered}
- {item.shipped}
+ {isNonShippable ? '–' : item.shipped}
0 ? 'primary.main' : 'text.disabled',
}}
>
- {item.remaining}
+ {isNonShippable ? '–' : item.remaining}
- {!item.cached ? '…' : item.isTracked === false ? '∞' : formatStock(item.stockVal)}
+ {!item.sku ? '–' : !item.cached ? '…' : item.isTracked === false ? '∞' : formatStock(item.stockVal)}
- {!item.cached ? '…' : item.isTracked === false ? '∞' : formatStock(item.availableVal)}
+ {!item.sku ? '–' : !item.cached ? '…' : item.isTracked === false ? '∞' : formatStock(item.availableVal)}
- {isClosed ? (
+ {isNonShippable ? (
+
+ ) : isClosed ? (
) : (
this.handleAmountChange(item.sku, e.target.value)}
+ onChange={(e) => this.handleAmountChange(item.itemKey, e.target.value)}
slotProps={{
htmlInput: {
min: 0,
diff --git a/src/components/manifest/ShipmentList.jsx b/src/components/manifest/ShipmentList.jsx
index 38014b5..d44222f 100644
--- a/src/components/manifest/ShipmentList.jsx
+++ b/src/components/manifest/ShipmentList.jsx
@@ -32,7 +32,17 @@ import { getMaxDocumentNumber } from '../../utils/documentNumber';
import PartyFilterPanel, { PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
import withNavigate from '../withNavigate';
import ShipmentDetailDialog from './ShipmentDetailDialog';
-import { getNextShipmentNumber } from './OrderShipmentDialog';
+import { getNextShipmentNumber, computeShippedSummary } from './OrderShipmentDialog';
+
+function getShipmentLineKey(line, index) {
+ if (line.sku && String(line.sku).trim()) {
+ return `sku:${String(line.sku).trim()}`;
+ }
+ if (line.id != null) {
+ return `id:${line.id}`;
+ }
+ return `idx:${index}`;
+}
import {
formatDate,
loadParties,
@@ -73,6 +83,7 @@ class NewShipmentModal extends PureComponent {
componentDidUpdate(prevProps) {
if (!prevProps.open && this.props.open) {
const openOrders = (this.props.orders || []).filter((o) => {
+ if (o.isShipped != null) return !o.isShipped;
const ordQty = Number(o.orderedQuantity) || 0;
const shpQty = Number(o.shippedQuantity) || 0;
return ordQty === 0 || shpQty < ordQty;
@@ -89,23 +100,35 @@ class NewShipmentModal extends PureComponent {
const order = orders.find((o) => o.id === Number(orderId));
const lines = Array.isArray(order?.lines) ? order.lines : [];
- const shippedBySku = {};
- for (const sh of order?.shipments || []) {
- for (const sl of sh.lines || []) {
- if (sl.sku) shippedBySku[sl.sku] = (shippedBySku[sl.sku] || 0) + (Number(sl.amount) || 0);
- }
- }
+ const { bySku, byOrderLineId, byName } = computeShippedSummary(order?.shipments);
+ const remainingShippedByName = { ...byName };
const lineAmounts = {};
- for (const l of lines) {
- if (!l.sku) continue;
+ lines.forEach((l, idx) => {
+ if (l.shippable === false) return;
+ const key = getShipmentLineKey(l, idx);
const ordered = Number(l.amount) || 0;
- const shipped = shippedBySku[l.sku] || 0;
+ const sku = l.sku ? String(l.sku).trim() : null;
+ const orderLineId = l.id != null ? l.id : null;
+
+ let shipped = 0;
+ if (sku) {
+ shipped = bySku[sku] || 0;
+ } else if (orderLineId != null && byOrderLineId[orderLineId] != null) {
+ shipped = byOrderLineId[orderLineId];
+ } else if (l.name) {
+ const n = String(l.name).trim().toLowerCase();
+ const pool = remainingShippedByName[n] || 0;
+ const consumed = Math.min(ordered, pool);
+ shipped = consumed;
+ remainingShippedByName[n] = pool - consumed;
+ }
+
const remaining = Math.max(0, ordered - shipped);
if (remaining > 0) {
- lineAmounts[l.sku] = String(remaining);
+ lineAmounts[key] = String(remaining);
}
- }
+ });
this.setState({
orderId,
@@ -114,22 +137,34 @@ class NewShipmentModal extends PureComponent {
});
}
- handleAmountChange(sku, val) {
+ handleAmountChange(key, val) {
this.setState((prev) => ({
- lineAmounts: { ...prev.lineAmounts, [sku]: val },
+ lineAmounts: { ...prev.lineAmounts, [key]: val },
}));
}
async handleSubmit() {
const { orderId, date, externalNumber, lineAmounts } = this.state;
+ const { orders = [] } = this.props;
+ const currentOrder = orders.find((o) => o.id === Number(orderId));
if (!orderId) {
enqueueSnackbar('Bitte wählen Sie einen Auftrag aus.', { variant: 'warning' });
return;
}
- const lines = Object.entries(lineAmounts)
- .filter(([, amt]) => Number(amt) > 0)
- .map(([sku, amt]) => ({ sku, amount: Number(amt) }));
+ const lines = [];
+ (currentOrder?.lines || []).forEach((l, idx) => {
+ if (l.shippable === false) return;
+ const key = getShipmentLineKey(l, idx);
+ const amt = Number(lineAmounts[key]);
+ if (amt > 0) {
+ if (l.sku) {
+ lines.push({ sku: l.sku, amount: amt });
+ } else if (l.id != null) {
+ lines.push({ orderLineId: l.id, amount: amt });
+ }
+ }
+ });
if (lines.length === 0) {
enqueueSnackbar('Mindestens eine Position muss mit Menge > 0 geliefert werden.', { variant: 'warning' });
@@ -164,12 +199,8 @@ class NewShipmentModal extends PureComponent {
const currentOrder = orders.find((o) => o.id === Number(orderId));
const lines = Array.isArray(currentOrder?.lines) ? currentOrder.lines : [];
- const shippedBySku = {};
- for (const sh of currentOrder?.shipments || []) {
- for (const sl of sh.lines || []) {
- if (sl.sku) shippedBySku[sl.sku] = (shippedBySku[sl.sku] || 0) + (Number(sl.amount) || 0);
- }
- }
+ const { bySku, byOrderLineId, byName } = computeShippedSummary(currentOrder?.shipments);
+ const remainingShippedByName = { ...byName };
return (
- {lines.map((line) => {
+ {lines.map((line, idx) => {
+ const key = getShipmentLineKey(line, idx);
+ const isShippable = line.shippable !== false;
const orderedAmt = Number(line.amount) || 0;
- const shippedAmt = shippedBySku[line.sku] || 0;
- const remaining = Math.max(0, orderedAmt - shippedAmt);
- const currentVal = lineAmounts[line.sku] ?? '';
+ const sku = line.sku ? String(line.sku).trim() : null;
+ const orderLineId = line.id != null ? line.id : null;
+
+ let shippedAmt = 0;
+ if (sku) {
+ shippedAmt = bySku[sku] || 0;
+ } else if (orderLineId != null && byOrderLineId[orderLineId] != null) {
+ shippedAmt = byOrderLineId[orderLineId];
+ } else if (line.name) {
+ const n = String(line.name).trim().toLowerCase();
+ const pool = remainingShippedByName[n] || 0;
+ const consumed = Math.min(orderedAmt, pool);
+ shippedAmt = consumed;
+ remainingShippedByName[n] = pool - consumed;
+ }
+
+ const remaining = isShippable ? Math.max(0, orderedAmt - shippedAmt) : 0;
+ const currentVal = lineAmounts[key] ?? '';
return (
- {line.sku} — {line.name || '–'}
+ {line.sku ? `${line.sku} — ` : ''}{line.name || '–'}
- Bestellt: {orderedAmt} • Bisher geliefert: {shippedAmt} • Noch offen: {remaining}
+ {!isShippable
+ ? 'Nicht lieferbare Position (z.B. Dienstleistung)'
+ : `Bestellt: ${orderedAmt} • Bisher geliefert: ${shippedAmt} • Noch offen: ${remaining}`}
- this.handleAmountChange(line.sku, e.target.value)}
- disabled={remaining <= 0}
- slotProps={{ htmlInput: { min: 0, max: remaining, step: 'any', style: { textAlign: 'right' } } }}
- sx={{ width: 110 }}
- />
+ {!isShippable ? (
+
+ ) : (
+ this.handleAmountChange(key, e.target.value)}
+ disabled={remaining <= 0}
+ slotProps={{ htmlInput: { min: 0, max: remaining, step: 'any', style: { textAlign: 'right' } } }}
+ sx={{ width: 110 }}
+ />
+ )}
);
})}
diff --git a/src/components/manifest/manifestConfig.js b/src/components/manifest/manifestConfig.js
index 3eec2b1..57a87be 100644
--- a/src/components/manifest/manifestConfig.js
+++ b/src/components/manifest/manifestConfig.js
@@ -136,6 +136,7 @@ export function emptyLine() {
pu: '1',
unit: '',
netprice: '',
+ shippable: true,
};
}
@@ -167,6 +168,7 @@ export function lineFromApi(row) {
unit: row.unit ?? '',
netprice: netVal,
taxrate: taxVal,
+ shippable: row.shippable !== undefined ? Boolean(row.shippable) : true,
};
}
@@ -197,7 +199,7 @@ export function hasArticleLines(lines) {
}
/** Build create/update line array. Throws Error on client validation. */
-export function buildLines(lines, { requireAtLeastOne }) {
+export function buildLines(lines, { requireAtLeastOne, kind } = {}) {
const outLines = [];
const errors = [];
@@ -255,6 +257,9 @@ export function buildLines(lines, { requireAtLeastOne }) {
if (name) row.name = name;
if (rebate != null) row.rebate = rebate;
if (pu != null) row.pu = pu;
+ if (kind !== 'delivery') {
+ row.shippable = line.shippable !== undefined ? Boolean(line.shippable) : true;
+ }
outLines.push(row);
}
@@ -503,7 +508,7 @@ export async function buildManifestBody(
date,
};
if (shouldIncludeLines) {
- body.lines = buildLines(form.lines, { requireAtLeastOne: shouldRequireLine });
+ body.lines = buildLines(form.lines, { requireAtLeastOne: shouldRequireLine, kind: 'delivery' });
}
} else {
let number = String(form.number ?? '').trim();
@@ -516,7 +521,7 @@ export async function buildManifestBody(
date,
};
if (shouldIncludeLines) {
- body.lines = buildLines(form.lines, { requireAtLeastOne: shouldRequireLine });
+ body.lines = buildLines(form.lines, { requireAtLeastOne: shouldRequireLine, kind });
}
if (useExternalNumber || form.externalNumber != null) {
body.externalNumber = externalNumber || null;
@@ -564,13 +569,24 @@ export const LINE_COLUMNS = [
{ id: 'gross', label: 'Brutto', width: 95, align: 'right', type: 'number' },
];
-export const QUOTE_LINE_COLUMNS = LINE_COLUMNS;
+export const QUOTE_LINE_COLUMNS = [
+ { id: 'pos', label: 'Pos', width: 44, align: 'center' },
+ { id: 'amount', label: 'Anzahl', width: 80, align: 'right', type: 'number' },
+ { id: 'sku', label: 'ArtNr', width: 130, align: 'left', type: 'text' },
+ { id: 'name', label: 'Name', width: 200, align: 'left', type: 'text' },
+ { id: 'shippable', label: 'Lieferbar', width: 68, align: 'center', type: 'boolean' },
+ { id: 'tax', label: 'MwSt', width: 70, align: 'right', type: 'number' },
+ { id: 'rebate', label: 'Rabatt', width: 75, align: 'right', type: 'number' },
+ { id: 'net', label: 'Netto', width: 95, align: 'right', type: 'number' },
+ { id: 'gross', label: 'Brutto', width: 95, align: 'right', type: 'number' },
+];
export const ORDER_LINE_COLUMNS = [
{ id: 'pos', label: 'Pos', width: 44, align: 'center' },
{ id: 'sku', label: 'ArtNr', width: 130, align: 'left', type: 'text' },
{ id: 'name', label: 'Name', width: 200, align: 'left', type: 'text' },
{ id: 'amount', label: 'Anzahl', width: 80, align: 'right', type: 'number' },
+ { id: 'shippable', label: 'Lieferbar', width: 68, align: 'center', type: 'boolean' },
{ id: 'tax', label: 'MwSt', width: 70, align: 'right', type: 'number' },
{ id: 'rebate', label: 'Rabatt', width: 75, align: 'right', type: 'number' },
{ id: 'net', label: 'Netto', width: 95, align: 'right', type: 'number' },
@@ -710,7 +726,7 @@ export function manifestConfig(cfgOrKind) {
listAutomat: quoteAutomat,
partiesByIdFromList: (parties) =>
Object.fromEntries(parties.map((row) => [row.id, row])),
- columns: LINE_COLUMNS,
+ columns: QUOTE_LINE_COLUMNS,
};
}
if (kind === 'order') {