u
This commit is contained in:
@@ -93,7 +93,7 @@ export const order = {
|
|||||||
* POST /api/order/:id/shipment
|
* POST /api/order/:id/shipment
|
||||||
* Creates shipment from order.
|
* Creates shipment from order.
|
||||||
* @param {number|string} id Order ID
|
* @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 }
|
* → 201 { shipment: Shipment }
|
||||||
*/
|
*/
|
||||||
// #region POST /api/order/:id/shipment — createShipment
|
// #region POST /api/order/:id/shipment — createShipment
|
||||||
|
|||||||
@@ -343,6 +343,9 @@ export function getManifestDirtyDetails(current, baseline) {
|
|||||||
if (String(cl.unit ?? '').trim() !== String(bl.unit ?? '').trim()) {
|
if (String(cl.unit ?? '').trim() !== String(bl.unit ?? '').trim()) {
|
||||||
lineFieldDiffs.unit = true;
|
lineFieldDiffs.unit = true;
|
||||||
}
|
}
|
||||||
|
if (Boolean(cl.shippable ?? true) !== Boolean(bl.shippable ?? true)) {
|
||||||
|
lineFieldDiffs.shippable = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(lineFieldDiffs).length > 0) {
|
if (Object.keys(lineFieldDiffs).length > 0) {
|
||||||
dirtyLineKeys.push(key);
|
dirtyLineKeys.push(key);
|
||||||
|
|||||||
@@ -453,7 +453,7 @@ export function isOrderPaid(order) {
|
|||||||
|
|
||||||
export function isOrderDelivered(order) {
|
export function isOrderDelivered(order) {
|
||||||
if (!order) return false;
|
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) {
|
export function getOrderDate(order) {
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ class ManifestForm extends PureComponent {
|
|||||||
paidTotal: initialEdit?.paidTotal ?? null,
|
paidTotal: initialEdit?.paidTotal ?? null,
|
||||||
paid: Boolean(initialEdit?.paid),
|
paid: Boolean(initialEdit?.paid),
|
||||||
shipped: Boolean(initialEdit?.shipped),
|
shipped: Boolean(initialEdit?.shipped),
|
||||||
|
isShipped: Boolean(initialEdit?.isShipped ?? initialEdit?.shipped),
|
||||||
delivered: Boolean(initialEdit?.delivered),
|
delivered: Boolean(initialEdit?.delivered),
|
||||||
isDirty: Boolean(initialEdit?.isDirty),
|
isDirty: Boolean(initialEdit?.isDirty),
|
||||||
dirtyFields: initialEdit?.dirtyFields || null,
|
dirtyFields: initialEdit?.dirtyFields || null,
|
||||||
@@ -249,6 +250,7 @@ class ManifestForm extends PureComponent {
|
|||||||
paidTotal: state.paidTotal ?? null,
|
paidTotal: state.paidTotal ?? null,
|
||||||
paid: Boolean(state.paid),
|
paid: Boolean(state.paid),
|
||||||
shipped: Boolean(state.shipped),
|
shipped: Boolean(state.shipped),
|
||||||
|
isShipped: Boolean(state.isShipped ?? state.shipped),
|
||||||
delivered: Boolean(state.delivered),
|
delivered: Boolean(state.delivered),
|
||||||
isDirty: Boolean(state.isDirty),
|
isDirty: Boolean(state.isDirty),
|
||||||
dirtyFields: state.dirtyFields,
|
dirtyFields: state.dirtyFields,
|
||||||
@@ -477,6 +479,7 @@ class ManifestForm extends PureComponent {
|
|||||||
paidTotal: doc.paidTotal ?? null,
|
paidTotal: doc.paidTotal ?? null,
|
||||||
paid: Boolean(doc.paid),
|
paid: Boolean(doc.paid),
|
||||||
shipped: Boolean(doc.shipped),
|
shipped: Boolean(doc.shipped),
|
||||||
|
isShipped: Boolean(doc.isShipped ?? doc.shipped),
|
||||||
delivered: Boolean(doc.delivered),
|
delivered: Boolean(doc.delivered),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -812,24 +815,52 @@ class ManifestForm extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isFullyDelivered() {
|
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 lines = this.state.lines || [];
|
||||||
const validLines = lines.filter((l) => l.sku && Number(l.amount) > 0);
|
const shippableLines = lines.filter((l) => l.shippable !== false && Number(l.amount) > 0);
|
||||||
if (validLines.length === 0) return false;
|
if (shippableLines.length === 0) return lines.length > 0;
|
||||||
|
|
||||||
const shippedBySku = {};
|
const shippedBySku = {};
|
||||||
|
const shippedByOrderLineId = {};
|
||||||
|
const shippedByName = {};
|
||||||
for (const sh of this.state.shipments || []) {
|
for (const sh of this.state.shipments || []) {
|
||||||
for (const sl of sh.lines || []) {
|
for (const sl of sh.lines || []) {
|
||||||
|
const amt = Number(sl.amount) || 0;
|
||||||
if (sl.sku) {
|
if (sl.sku) {
|
||||||
const k = String(sl.sku).trim().toLowerCase();
|
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 remainingShippedByName = { ...shippedByName };
|
||||||
const k = String(l.sku).trim().toLowerCase();
|
|
||||||
return (shippedBySku[k] || 0) >= Number(l.amount);
|
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);
|
const lineToRemove = prev.lines.find((line) => line.key === key);
|
||||||
if (lineToRemove && this.props.kind === 'order') {
|
if (lineToRemove && this.props.kind === 'order') {
|
||||||
const rawSku = lineToRemove.sku != null ? String(lineToRemove.sku).trim().toLowerCase() : '';
|
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(
|
const isShipped = Boolean(
|
||||||
rawSku &&
|
|
||||||
(prev.shipments || []).some((sh) =>
|
(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) {
|
if (isShipped) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Fragment, PureComponent } from 'react';
|
|||||||
import { Link as RouterLink } from 'react-router-dom';
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
import Chip from '@mui/material/Chip';
|
import Chip from '@mui/material/Chip';
|
||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
@@ -155,6 +156,7 @@ class ManifestFormLine extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleStartEdit(columnId) {
|
handleStartEdit(columnId) {
|
||||||
|
if (columnId === 'pos' || columnId === 'shippable') return;
|
||||||
const { saving } = this.props;
|
const { saving } = this.props;
|
||||||
const isFullyLedgered = this.isFullyLedgered();
|
const isFullyLedgered = this.isFullyLedgered();
|
||||||
const isDisabled = saving || isFullyLedgered || (columnId === 'sku' && Boolean(this.props.isOnShipment));
|
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 (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: isFirstLine ? 40 : 41,
|
||||||
|
minHeight: isFirstLine ? 40 : 41,
|
||||||
|
maxHeight: isFirstLine ? 40 : 41,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
position: 'relative',
|
||||||
|
ml: isFirstInput ? 0 : '-1px',
|
||||||
|
width: isFirstInput ? '100%' : 'calc(100% + 1px)',
|
||||||
|
mt: isFirstLine ? 0 : '-1px',
|
||||||
|
borderRadius,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: isFieldDirty
|
||||||
|
? 'warning.main'
|
||||||
|
: isFullyLedgered
|
||||||
|
? LOCKED_CELL_BORDER
|
||||||
|
: 'rgba(0, 0, 0, 0.23)',
|
||||||
|
zIndex: 1,
|
||||||
|
cursor: isDisabled ? 'default' : 'pointer',
|
||||||
|
...(isFullyLedgered && !isFieldDirty && {
|
||||||
|
bgcolor: LOCKED_CELL_BG,
|
||||||
|
}),
|
||||||
|
...(isFieldDirty && {
|
||||||
|
bgcolor: 'rgba(237, 108, 2, 0.08)',
|
||||||
|
zIndex: 2,
|
||||||
|
}),
|
||||||
|
'&:hover': isDisabled
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
borderColor: isFieldDirty ? 'warning.main' : 'rgba(0, 0, 0, 0.87)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={isDisabled ? undefined : () => onChange(line.key, 'shippable', !isShippable)}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
size="small"
|
||||||
|
checked={isShippable}
|
||||||
|
disabled={isDisabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onChange(line.key, 'shippable', e.target.checked);
|
||||||
|
}}
|
||||||
|
slotProps={{
|
||||||
|
input: { 'aria-label': isShippable ? 'Lieferbar' : 'Nicht lieferbar' },
|
||||||
|
}}
|
||||||
|
sx={{ p: 0.5 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let val = line[id];
|
let val = line[id];
|
||||||
if (val == null || val === '') {
|
if (val == null || val === '') {
|
||||||
if (id === 'name' && this.state.cachedArticle?.name) {
|
if (id === 'name' && this.state.cachedArticle?.name) {
|
||||||
|
|||||||
@@ -107,10 +107,14 @@ class ManifestLines extends PureComponent {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{(() => {
|
{(() => {
|
||||||
const shippedSkus = new Set();
|
const shippedSkus = new Set();
|
||||||
|
const shippedOrderLineIds = new Set();
|
||||||
|
const shippedNames = new Set();
|
||||||
if (kind === 'order' && Array.isArray(shipments)) {
|
if (kind === 'order' && Array.isArray(shipments)) {
|
||||||
for (const sh of shipments) {
|
for (const sh of shipments) {
|
||||||
for (const sl of sh.lines || []) {
|
for (const sl of sh.lines || []) {
|
||||||
if (sl.sku) shippedSkus.add(String(sl.sku).trim().toLowerCase());
|
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) => {
|
return lines.map((line, index) => {
|
||||||
const lineKey = line.key ?? index;
|
const lineKey = line.key ?? index;
|
||||||
const rawSku = line.sku != null ? String(line.sku).trim() : '';
|
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 isDelivery = kind === 'delivery';
|
||||||
const matched = isDelivery
|
const matched = isDelivery
|
||||||
? supplierArticlesByLineKey[lineKey] ||
|
? supplierArticlesByLineKey[lineKey] ||
|
||||||
|
|||||||
@@ -29,17 +29,39 @@ import { formatStock } from '../article/searchFormat';
|
|||||||
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
|
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
|
||||||
import { todayInputValue } from './manifestConfig';
|
import { todayInputValue } from './manifestConfig';
|
||||||
|
|
||||||
// MARK: helper to sum already shipped quantities per SKU across order shipments
|
// MARK: helper to sum already shipped quantities per SKU, orderLineId, and line name across shipments
|
||||||
function computeShippedBySku(shipments) {
|
export function computeShippedSummary(shipments) {
|
||||||
const map = {};
|
const bySku = {};
|
||||||
|
const byOrderLineId = {};
|
||||||
|
const byName = {};
|
||||||
|
|
||||||
for (const sh of shipments || []) {
|
for (const sh of shipments || []) {
|
||||||
for (const sl of sh.lines || []) {
|
for (const sl of sh.lines || []) {
|
||||||
|
const amt = Number(sl.amount) || 0;
|
||||||
if (sl.sku) {
|
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
|
// 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.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.handleToggleAll = this.handleToggleAll.bind(this);
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
this.handleShipAll = this.handleShipAll.bind(this);
|
this.handleShipAll = this.handleShipAll.bind(this);
|
||||||
@@ -128,35 +151,146 @@ export default class OrderShipmentDialog extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
calculateInitialState(props) {
|
calculateInitialState(props) {
|
||||||
const shippedBySku = computeShippedBySku(props.shipments);
|
const { bySku, byOrderLineId, byName } = computeShippedSummary(props.shipments);
|
||||||
|
const remainingShippedByName = { ...byName };
|
||||||
const initialAmounts = {};
|
const initialAmounts = {};
|
||||||
const initialSelected = new Set();
|
const initialSelected = new Set();
|
||||||
|
|
||||||
for (const l of props.lines || []) {
|
(props.lines || []).forEach((l, idx) => {
|
||||||
if (!l.sku) continue;
|
const isShippable = l.shippable !== false;
|
||||||
|
const key = getItemKey(l, idx);
|
||||||
const ordered = Number(l.amount) || 0;
|
const ordered = Number(l.amount) || 0;
|
||||||
const shipped = shippedBySku[l.sku] || 0;
|
const sku = l.sku ? String(l.sku).trim() : null;
|
||||||
const rem = Math.max(0, ordered - shipped);
|
const orderLineId = l.id != null ? l.id : null;
|
||||||
if (rem > 0) {
|
|
||||||
initialAmounts[l.sku] = String(rem);
|
let shipped = 0;
|
||||||
initialSelected.add(l.sku);
|
if (sku) {
|
||||||
} else {
|
shipped = bySku[sku] || 0;
|
||||||
initialAmounts[l.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 };
|
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) => {
|
this.setState((prev) => {
|
||||||
const nextSelected = new Set(prev.selectedSkus);
|
const nextSelected = new Set(prev.selectedSkus);
|
||||||
const nextAmounts = { ...prev.lineAmounts };
|
const nextAmounts = { ...prev.lineAmounts };
|
||||||
if (nextSelected.has(sku)) {
|
if (nextSelected.has(itemKey)) {
|
||||||
nextSelected.delete(sku);
|
nextSelected.delete(itemKey);
|
||||||
} else {
|
} else {
|
||||||
nextSelected.add(sku);
|
nextSelected.add(itemKey);
|
||||||
if (!Number(nextAmounts[sku]) || Number(nextAmounts[sku]) <= 0) {
|
if (!Number(nextAmounts[itemKey]) || Number(nextAmounts[itemKey]) <= 0) {
|
||||||
nextAmounts[sku] = String(remaining);
|
nextAmounts[itemKey] = String(remaining);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { selectedSkus: nextSelected, lineAmounts: nextAmounts };
|
return { selectedSkus: nextSelected, lineAmounts: nextAmounts };
|
||||||
@@ -165,15 +299,15 @@ export default class OrderShipmentDialog extends PureComponent {
|
|||||||
|
|
||||||
handleToggleAll(openItems) {
|
handleToggleAll(openItems) {
|
||||||
this.setState((prev) => {
|
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 nextSelected = new Set();
|
||||||
const nextAmounts = { ...prev.lineAmounts };
|
const nextAmounts = { ...prev.lineAmounts };
|
||||||
|
|
||||||
if (!allSelected) {
|
if (!allSelected) {
|
||||||
for (const it of openItems) {
|
for (const it of openItems) {
|
||||||
nextSelected.add(it.sku);
|
nextSelected.add(it.itemKey);
|
||||||
if (!Number(nextAmounts[it.sku]) || Number(nextAmounts[it.sku]) <= 0) {
|
if (!Number(nextAmounts[it.itemKey]) || Number(nextAmounts[it.itemKey]) <= 0) {
|
||||||
nextAmounts[it.sku] = String(it.remaining);
|
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) => {
|
this.setState((prev) => {
|
||||||
const nextAmounts = { ...prev.lineAmounts, [sku]: val };
|
const nextAmounts = { ...prev.lineAmounts, [itemKey]: val };
|
||||||
const nextSelected = new Set(prev.selectedSkus);
|
const nextSelected = new Set(prev.selectedSkus);
|
||||||
const num = Number(val);
|
const num = Number(val);
|
||||||
if (num > 0) {
|
if (num > 0) {
|
||||||
nextSelected.add(sku);
|
nextSelected.add(itemKey);
|
||||||
} else {
|
} else {
|
||||||
nextSelected.delete(sku);
|
nextSelected.delete(itemKey);
|
||||||
}
|
}
|
||||||
return { lineAmounts: nextAmounts, selectedSkus: nextSelected };
|
return { lineAmounts: nextAmounts, selectedSkus: nextSelected };
|
||||||
});
|
});
|
||||||
@@ -203,11 +337,17 @@ export default class OrderShipmentDialog extends PureComponent {
|
|||||||
const { date, externalNumber, lineAmounts, selectedSkus, saving } = this.state;
|
const { date, externalNumber, lineAmounts, selectedSkus, saving } = this.state;
|
||||||
if (saving || !orderId) return;
|
if (saving || !orderId) return;
|
||||||
|
|
||||||
|
const items = this.getItems();
|
||||||
const linesToShip = [];
|
const linesToShip = [];
|
||||||
for (const sku of selectedSkus) {
|
for (const it of items) {
|
||||||
const amount = Number(lineAmounts[sku]);
|
if (!it.isShippable || !selectedSkus.has(it.itemKey)) continue;
|
||||||
|
const amount = Number(lineAmounts[it.itemKey]);
|
||||||
if (amount > 0) {
|
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) });
|
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
|
// #region POST /api/order/:id/shipment — ship all remaining
|
||||||
async handleShipAll() {
|
async handleShipAll() {
|
||||||
const { orderId, onClose, onSuccess } = this.props;
|
const { orderId, onClose, onSuccess } = this.props;
|
||||||
@@ -275,73 +413,14 @@ export default class OrderShipmentDialog extends PureComponent {
|
|||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { open, onClose, orderId, orderNumber, lines = [], shipments = [] } = this.props;
|
const { open, onClose, orderNumber } = this.props;
|
||||||
const { date, externalNumber, lineAmounts, selectedSkus, saving, error } = this.state;
|
const { date, externalNumber, saving, error } = this.state;
|
||||||
const nextShipmentNumber = getNextShipmentNumber(orderNumber, shipments);
|
const nextShipmentNumber = getNextShipmentNumber(orderNumber, this.props.shipments);
|
||||||
|
|
||||||
const shippedBySku = computeShippedBySku(shipments);
|
const items = this.getItems();
|
||||||
const activeLines = lines.filter((l) => Boolean(l.sku));
|
const shippableItems = items.filter((it) => it.isShippable);
|
||||||
const currentOrderId = orderId != null ? Number(orderId) : null;
|
const openItems = shippableItems.filter((it) => it.remaining > 0);
|
||||||
|
const allShipped = shippableItems.length > 0 && openItems.length === 0;
|
||||||
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 isAllSelected = openItems.length > 0 && openItems.every((it) => it.isSelected);
|
const isAllSelected = openItems.length > 0 && openItems.every((it) => it.isSelected);
|
||||||
const selectedCount = openItems.filter((it) => it.isSelected).length;
|
const selectedCount = openItems.filter((it) => it.isSelected).length;
|
||||||
|
|
||||||
@@ -451,10 +530,11 @@ export default class OrderShipmentDialog extends PureComponent {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
items.map((item) => {
|
items.map((item) => {
|
||||||
const isClosed = item.remaining <= 0;
|
const isNonShippable = !item.isShippable;
|
||||||
|
const isClosed = isNonShippable || item.remaining <= 0;
|
||||||
return (
|
return (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={item.sku}
|
key={item.itemKey}
|
||||||
hover={!isClosed}
|
hover={!isClosed}
|
||||||
sx={{
|
sx={{
|
||||||
opacity: isClosed ? 0.55 : 1,
|
opacity: isClosed ? 0.55 : 1,
|
||||||
@@ -466,17 +546,17 @@ export default class OrderShipmentDialog extends PureComponent {
|
|||||||
size="small"
|
size="small"
|
||||||
checked={item.isSelected && !isClosed}
|
checked={item.isSelected && !isClosed}
|
||||||
disabled={saving || isClosed}
|
disabled={saving || isClosed}
|
||||||
onChange={() => this.handleToggleSku(item.sku, item.remaining)}
|
onChange={() => this.handleToggleItem(item.itemKey, item.remaining)}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 600 }}>{item.sku}</TableCell>
|
<TableCell sx={{ fontWeight: 600 }}>{item.sku || '–'}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
<Typography variant="body2" sx={{ fontWeight: 500 }}>
|
||||||
{item.name || '–'}
|
{item.name || '–'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">{item.ordered}</TableCell>
|
<TableCell align="right">{item.ordered}</TableCell>
|
||||||
<TableCell align="right">{item.shipped}</TableCell>
|
<TableCell align="right">{isNonShippable ? '–' : item.shipped}</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
align="right"
|
align="right"
|
||||||
sx={{
|
sx={{
|
||||||
@@ -484,7 +564,7 @@ export default class OrderShipmentDialog extends PureComponent {
|
|||||||
color: item.remaining > 0 ? 'primary.main' : 'text.disabled',
|
color: item.remaining > 0 ? 'primary.main' : 'text.disabled',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.remaining}
|
{isNonShippable ? '–' : item.remaining}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
align="right"
|
align="right"
|
||||||
@@ -492,14 +572,14 @@ export default class OrderShipmentDialog extends PureComponent {
|
|||||||
sx={{
|
sx={{
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
color:
|
color:
|
||||||
!item.cached || item.stockVal == null
|
!item.sku || !item.cached || item.stockVal == null
|
||||||
? 'text.disabled'
|
? 'text.disabled'
|
||||||
: item.stockVal <= 0
|
: item.stockVal <= 0
|
||||||
? 'error.main'
|
? 'error.main'
|
||||||
: 'text.secondary',
|
: 'text.secondary',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!item.cached ? '…' : item.isTracked === false ? '∞' : formatStock(item.stockVal)}
|
{!item.sku ? '–' : !item.cached ? '…' : item.isTracked === false ? '∞' : formatStock(item.stockVal)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
align="right"
|
align="right"
|
||||||
@@ -507,7 +587,7 @@ export default class OrderShipmentDialog extends PureComponent {
|
|||||||
sx={{
|
sx={{
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color:
|
color:
|
||||||
!item.cached || item.availableVal == null
|
!item.sku || !item.cached || item.availableVal == null
|
||||||
? 'text.disabled'
|
? 'text.disabled'
|
||||||
: item.availableVal <= 0
|
: item.availableVal <= 0
|
||||||
? 'error.main'
|
? 'error.main'
|
||||||
@@ -516,10 +596,12 @@ export default class OrderShipmentDialog extends PureComponent {
|
|||||||
: 'text.secondary',
|
: 'text.secondary',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!item.cached ? '…' : item.isTracked === false ? '∞' : formatStock(item.availableVal)}
|
{!item.sku ? '–' : !item.cached ? '…' : item.isTracked === false ? '∞' : formatStock(item.availableVal)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
{isClosed ? (
|
{isNonShippable ? (
|
||||||
|
<Chip size="small" label="Nicht lieferbar" variant="outlined" sx={{ height: 22, fontSize: '0.7rem' }} />
|
||||||
|
) : isClosed ? (
|
||||||
<Chip size="small" label="Geliefert" color="success" variant="outlined" sx={{ height: 22, fontSize: '0.7rem' }} />
|
<Chip size="small" label="Geliefert" color="success" variant="outlined" sx={{ height: 22, fontSize: '0.7rem' }} />
|
||||||
) : (
|
) : (
|
||||||
<TextField
|
<TextField
|
||||||
@@ -527,7 +609,7 @@ export default class OrderShipmentDialog extends PureComponent {
|
|||||||
type="number"
|
type="number"
|
||||||
disabled={saving || !item.isSelected}
|
disabled={saving || !item.isSelected}
|
||||||
value={item.shipAmount}
|
value={item.shipAmount}
|
||||||
onChange={(e) => this.handleAmountChange(item.sku, e.target.value)}
|
onChange={(e) => this.handleAmountChange(item.itemKey, e.target.value)}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
htmlInput: {
|
htmlInput: {
|
||||||
min: 0,
|
min: 0,
|
||||||
|
|||||||
@@ -32,7 +32,17 @@ import { getMaxDocumentNumber } from '../../utils/documentNumber';
|
|||||||
import PartyFilterPanel, { PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
|
import PartyFilterPanel, { PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
|
||||||
import withNavigate from '../withNavigate';
|
import withNavigate from '../withNavigate';
|
||||||
import ShipmentDetailDialog from './ShipmentDetailDialog';
|
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 {
|
import {
|
||||||
formatDate,
|
formatDate,
|
||||||
loadParties,
|
loadParties,
|
||||||
@@ -73,6 +83,7 @@ class NewShipmentModal extends PureComponent {
|
|||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (!prevProps.open && this.props.open) {
|
if (!prevProps.open && this.props.open) {
|
||||||
const openOrders = (this.props.orders || []).filter((o) => {
|
const openOrders = (this.props.orders || []).filter((o) => {
|
||||||
|
if (o.isShipped != null) return !o.isShipped;
|
||||||
const ordQty = Number(o.orderedQuantity) || 0;
|
const ordQty = Number(o.orderedQuantity) || 0;
|
||||||
const shpQty = Number(o.shippedQuantity) || 0;
|
const shpQty = Number(o.shippedQuantity) || 0;
|
||||||
return ordQty === 0 || shpQty < ordQty;
|
return ordQty === 0 || shpQty < ordQty;
|
||||||
@@ -89,23 +100,35 @@ class NewShipmentModal extends PureComponent {
|
|||||||
const order = orders.find((o) => o.id === Number(orderId));
|
const order = orders.find((o) => o.id === Number(orderId));
|
||||||
const lines = Array.isArray(order?.lines) ? order.lines : [];
|
const lines = Array.isArray(order?.lines) ? order.lines : [];
|
||||||
|
|
||||||
const shippedBySku = {};
|
const { bySku, byOrderLineId, byName } = computeShippedSummary(order?.shipments);
|
||||||
for (const sh of order?.shipments || []) {
|
const remainingShippedByName = { ...byName };
|
||||||
for (const sl of sh.lines || []) {
|
|
||||||
if (sl.sku) shippedBySku[sl.sku] = (shippedBySku[sl.sku] || 0) + (Number(sl.amount) || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lineAmounts = {};
|
const lineAmounts = {};
|
||||||
for (const l of lines) {
|
lines.forEach((l, idx) => {
|
||||||
if (!l.sku) continue;
|
if (l.shippable === false) return;
|
||||||
|
const key = getShipmentLineKey(l, idx);
|
||||||
const ordered = Number(l.amount) || 0;
|
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);
|
const remaining = Math.max(0, ordered - shipped);
|
||||||
if (remaining > 0) {
|
if (remaining > 0) {
|
||||||
lineAmounts[l.sku] = String(remaining);
|
lineAmounts[key] = String(remaining);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
orderId,
|
orderId,
|
||||||
@@ -114,22 +137,34 @@ class NewShipmentModal extends PureComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAmountChange(sku, val) {
|
handleAmountChange(key, val) {
|
||||||
this.setState((prev) => ({
|
this.setState((prev) => ({
|
||||||
lineAmounts: { ...prev.lineAmounts, [sku]: val },
|
lineAmounts: { ...prev.lineAmounts, [key]: val },
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSubmit() {
|
async handleSubmit() {
|
||||||
const { orderId, date, externalNumber, lineAmounts } = this.state;
|
const { orderId, date, externalNumber, lineAmounts } = this.state;
|
||||||
|
const { orders = [] } = this.props;
|
||||||
|
const currentOrder = orders.find((o) => o.id === Number(orderId));
|
||||||
if (!orderId) {
|
if (!orderId) {
|
||||||
enqueueSnackbar('Bitte wählen Sie einen Auftrag aus.', { variant: 'warning' });
|
enqueueSnackbar('Bitte wählen Sie einen Auftrag aus.', { variant: 'warning' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = Object.entries(lineAmounts)
|
const lines = [];
|
||||||
.filter(([, amt]) => Number(amt) > 0)
|
(currentOrder?.lines || []).forEach((l, idx) => {
|
||||||
.map(([sku, amt]) => ({ sku, amount: Number(amt) }));
|
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) {
|
if (lines.length === 0) {
|
||||||
enqueueSnackbar('Mindestens eine Position muss mit Menge > 0 geliefert werden.', { variant: 'warning' });
|
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 currentOrder = orders.find((o) => o.id === Number(orderId));
|
||||||
const lines = Array.isArray(currentOrder?.lines) ? currentOrder.lines : [];
|
const lines = Array.isArray(currentOrder?.lines) ? currentOrder.lines : [];
|
||||||
|
|
||||||
const shippedBySku = {};
|
const { bySku, byOrderLineId, byName } = computeShippedSummary(currentOrder?.shipments);
|
||||||
for (const sh of currentOrder?.shipments || []) {
|
const remainingShippedByName = { ...byName };
|
||||||
for (const sl of sh.lines || []) {
|
|
||||||
if (sl.sku) shippedBySku[sl.sku] = (shippedBySku[sl.sku] || 0) + (Number(sl.amount) || 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppDialog
|
<AppDialog
|
||||||
@@ -236,15 +267,32 @@ class NewShipmentModal extends PureComponent {
|
|||||||
Lieferbare Positionen für Auftrag {currentOrder.number}:
|
Lieferbare Positionen für Auftrag {currentOrder.number}:
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
{lines.map((line) => {
|
{lines.map((line, idx) => {
|
||||||
|
const key = getShipmentLineKey(line, idx);
|
||||||
|
const isShippable = line.shippable !== false;
|
||||||
const orderedAmt = Number(line.amount) || 0;
|
const orderedAmt = Number(line.amount) || 0;
|
||||||
const shippedAmt = shippedBySku[line.sku] || 0;
|
const sku = line.sku ? String(line.sku).trim() : null;
|
||||||
const remaining = Math.max(0, orderedAmt - shippedAmt);
|
const orderLineId = line.id != null ? line.id : null;
|
||||||
const currentVal = lineAmounts[line.sku] ?? '';
|
|
||||||
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
key={line.sku}
|
key={key}
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -252,26 +300,33 @@ class NewShipmentModal extends PureComponent {
|
|||||||
p: 1.25,
|
p: 1.25,
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
bgcolor: SURFACE_RAISED,
|
bgcolor: SURFACE_RAISED,
|
||||||
|
opacity: !isShippable ? 0.6 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ minWidth: 0, flex: 1, pr: 2 }}>
|
<Box sx={{ minWidth: 0, flex: 1, pr: 2 }}>
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600, ...ellipsisSx }}>
|
<Typography variant="body2" sx={{ fontWeight: 600, ...ellipsisSx }}>
|
||||||
{line.sku} — {line.name || '–'}
|
{line.sku ? `${line.sku} — ` : ''}{line.name || '–'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
Bestellt: {orderedAmt} • Bisher geliefert: {shippedAmt} • Noch offen: <strong>{remaining}</strong>
|
{!isShippable
|
||||||
|
? 'Nicht lieferbare Position (z.B. Dienstleistung)'
|
||||||
|
: `Bestellt: ${orderedAmt} • Bisher geliefert: ${shippedAmt} • Noch offen: ${remaining}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<TextField
|
{!isShippable ? (
|
||||||
size="small"
|
<Chip size="small" label="Nicht lieferbar" variant="outlined" sx={{ height: 22, fontSize: '0.7rem' }} />
|
||||||
type="number"
|
) : (
|
||||||
label="Liefern"
|
<TextField
|
||||||
value={currentVal}
|
size="small"
|
||||||
onChange={(e) => this.handleAmountChange(line.sku, e.target.value)}
|
type="number"
|
||||||
disabled={remaining <= 0}
|
label="Liefern"
|
||||||
slotProps={{ htmlInput: { min: 0, max: remaining, step: 'any', style: { textAlign: 'right' } } }}
|
value={currentVal}
|
||||||
sx={{ width: 110 }}
|
onChange={(e) => this.handleAmountChange(key, e.target.value)}
|
||||||
/>
|
disabled={remaining <= 0}
|
||||||
|
slotProps={{ htmlInput: { min: 0, max: remaining, step: 'any', style: { textAlign: 'right' } } }}
|
||||||
|
sx={{ width: 110 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ export function emptyLine() {
|
|||||||
pu: '1',
|
pu: '1',
|
||||||
unit: '',
|
unit: '',
|
||||||
netprice: '',
|
netprice: '',
|
||||||
|
shippable: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +168,7 @@ export function lineFromApi(row) {
|
|||||||
unit: row.unit ?? '',
|
unit: row.unit ?? '',
|
||||||
netprice: netVal,
|
netprice: netVal,
|
||||||
taxrate: taxVal,
|
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. */
|
/** Build create/update line array. Throws Error on client validation. */
|
||||||
export function buildLines(lines, { requireAtLeastOne }) {
|
export function buildLines(lines, { requireAtLeastOne, kind } = {}) {
|
||||||
const outLines = [];
|
const outLines = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
@@ -255,6 +257,9 @@ export function buildLines(lines, { requireAtLeastOne }) {
|
|||||||
if (name) row.name = name;
|
if (name) row.name = name;
|
||||||
if (rebate != null) row.rebate = rebate;
|
if (rebate != null) row.rebate = rebate;
|
||||||
if (pu != null) row.pu = pu;
|
if (pu != null) row.pu = pu;
|
||||||
|
if (kind !== 'delivery') {
|
||||||
|
row.shippable = line.shippable !== undefined ? Boolean(line.shippable) : true;
|
||||||
|
}
|
||||||
outLines.push(row);
|
outLines.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,7 +508,7 @@ export async function buildManifestBody(
|
|||||||
date,
|
date,
|
||||||
};
|
};
|
||||||
if (shouldIncludeLines) {
|
if (shouldIncludeLines) {
|
||||||
body.lines = buildLines(form.lines, { requireAtLeastOne: shouldRequireLine });
|
body.lines = buildLines(form.lines, { requireAtLeastOne: shouldRequireLine, kind: 'delivery' });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let number = String(form.number ?? '').trim();
|
let number = String(form.number ?? '').trim();
|
||||||
@@ -516,7 +521,7 @@ export async function buildManifestBody(
|
|||||||
date,
|
date,
|
||||||
};
|
};
|
||||||
if (shouldIncludeLines) {
|
if (shouldIncludeLines) {
|
||||||
body.lines = buildLines(form.lines, { requireAtLeastOne: shouldRequireLine });
|
body.lines = buildLines(form.lines, { requireAtLeastOne: shouldRequireLine, kind });
|
||||||
}
|
}
|
||||||
if (useExternalNumber || form.externalNumber != null) {
|
if (useExternalNumber || form.externalNumber != null) {
|
||||||
body.externalNumber = externalNumber || null;
|
body.externalNumber = externalNumber || null;
|
||||||
@@ -564,13 +569,24 @@ export const LINE_COLUMNS = [
|
|||||||
{ id: 'gross', label: 'Brutto', width: 95, align: 'right', type: 'number' },
|
{ 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 = [
|
export const ORDER_LINE_COLUMNS = [
|
||||||
{ id: 'pos', label: 'Pos', width: 44, align: 'center' },
|
{ id: 'pos', label: 'Pos', width: 44, align: 'center' },
|
||||||
{ id: 'sku', label: 'ArtNr', width: 130, align: 'left', type: 'text' },
|
{ id: 'sku', label: 'ArtNr', width: 130, align: 'left', type: 'text' },
|
||||||
{ id: 'name', label: 'Name', width: 200, 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: '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: 'tax', label: 'MwSt', width: 70, align: 'right', type: 'number' },
|
||||||
{ id: 'rebate', label: 'Rabatt', width: 75, 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: 'net', label: 'Netto', width: 95, align: 'right', type: 'number' },
|
||||||
@@ -710,7 +726,7 @@ export function manifestConfig(cfgOrKind) {
|
|||||||
listAutomat: quoteAutomat,
|
listAutomat: quoteAutomat,
|
||||||
partiesByIdFromList: (parties) =>
|
partiesByIdFromList: (parties) =>
|
||||||
Object.fromEntries(parties.map((row) => [row.id, row])),
|
Object.fromEntries(parties.map((row) => [row.id, row])),
|
||||||
columns: LINE_COLUMNS,
|
columns: QUOTE_LINE_COLUMNS,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (kind === 'order') {
|
if (kind === 'order') {
|
||||||
|
|||||||
Reference in New Issue
Block a user