This commit is contained in:
seb
2026-10-20 05:33:48 +02:00
parent 42241ea541
commit 65c908f44e
2 changed files with 115 additions and 5 deletions

View File

@@ -131,7 +131,7 @@ Strip supplierName?: str from T11 (resolve via entityId in cached supplier list)
# Dash API Routes Overview (All)
Overview of all 263 API endpoints across all modules.
Overview of all 264 API endpoints across all modules.
### Notation
@@ -784,7 +784,7 @@ _none_
- Params: `id: int`
- Returns: `200 { shipments: T3[] }`
#### Create shipment for an order — `POST /order/{id}/shipment`
- Params: `id: int, body?: { date?: str, externalNumber?: str, lines?: { sku: str, amount?: num, name?: str, pu?: num, net?: num, rebate?: num, taxrate?: num, comment?: str }[] }`
- Params: `id: int, body: { lines: { sku: str, amount?: num, name?: str, pu?: num, net?: num, rebate?: num, taxrate?: num, comment?: str }[], date?: str, warehouseId?: int, externalNumber?: str }`
- Returns: `201 { shipment: T3 }`
#### List shippings of an order — `GET /order/{id}/shipping`
- Params: `id: int`
@@ -936,6 +936,9 @@ _none_
### Section: /shipment
#### List shipments — `GET /shipment`
- Params: `query: offset?: int, limit?: int, search?: str, state?: str, seekMonths?: int`
- Returns: `200 { shipments: { id: int, number: str, type: str, date: str, positions: int, totalQuantity: num, packageCount: int, state: str, entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, orderId?: int|null, orderNumber?: str|null, customerName?: str|null, carrier?: str|null, trackingCode?: str|null }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Delete shipment — `DELETE /shipment/{id}`
- Params: `id: int`
- Returns: `204 void`

View File

@@ -25,6 +25,8 @@ import { enqueueSnackbar } from 'notistack';
import AppDialog from '../AppDialog';
import { order as orderApi } from '../../api';
import { notifyError } from '../../utils/notify';
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
@@ -80,6 +82,23 @@ export default class OrderShipmentDialog extends PureComponent {
this.handleToggleAll = this.handleToggleAll.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleShipAll = this.handleShipAll.bind(this);
if (props.open) {
this.ensureArticlesLoaded(props.lines);
}
}
componentDidMount() {
this.unsubArticle = artikelAutomat.subscribe(() => {
this.forceUpdate();
});
if (this.props.open) {
this.ensureArticlesLoaded(this.props.lines);
}
}
componentWillUnmount() {
this.unsubArticle?.();
}
componentDidUpdate(prevProps) {
@@ -93,6 +112,18 @@ export default class OrderShipmentDialog extends PureComponent {
saving: false,
error: null,
});
this.ensureArticlesLoaded(this.props.lines);
} else if (this.props.open && prevProps.lines !== this.props.lines) {
this.ensureArticlesLoaded(this.props.lines);
}
}
ensureArticlesLoaded(lines) {
if (!Array.isArray(lines)) return;
for (const l of lines) {
if (l?.sku && !getArticle(l.sku)) {
artikelAutomat.actions.loadArticle(l.sku).catch(() => {});
}
}
}
@@ -244,12 +275,13 @@ export default class OrderShipmentDialog extends PureComponent {
// #endregion
render() {
const { open, onClose, orderNumber, lines = [], shipments = [] } = this.props;
const { open, onClose, orderId, orderNumber, lines = [], shipments = [] } = this.props;
const { date, externalNumber, lineAmounts, selectedSkus, saving, error } = this.state;
const nextShipmentNumber = getNextShipmentNumber(orderNumber, 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;
@@ -257,6 +289,40 @@ export default class OrderShipmentDialog extends PureComponent {
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,
@@ -264,6 +330,13 @@ export default class OrderShipmentDialog extends PureComponent {
remaining,
isSelected,
shipAmount,
cached,
isTracked,
stockVal,
availableVal,
reservedInOtherOrders,
reservedTitle,
stockTitle,
};
});
@@ -273,7 +346,7 @@ export default class OrderShipmentDialog extends PureComponent {
const selectedCount = openItems.filter((it) => it.isSelected).length;
return (
<AppDialog open={open} onClose={saving ? undefined : onClose} maxWidth="md" fullWidth>
<AppDialog open={open} onClose={saving ? undefined : onClose} maxWidth="lg" fullWidth>
<DialogTitle sx={{ m: 0, p: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Stack direction="row" spacing={1.5} sx={{ alignItems: 'center' }}>
<LocalShippingOutlinedIcon color="primary" />
@@ -364,13 +437,15 @@ export default class OrderShipmentDialog extends PureComponent {
<TableCell align="right" sx={{ fontWeight: 600 }}>Bestellt</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>Geliefert</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>Offen</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>Bestand</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>Verfügbar</TableCell>
<TableCell align="right" sx={{ fontWeight: 600, width: 130 }}>Liefermenge</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.length === 0 ? (
<TableRow>
<TableCell colSpan={7} align="center" sx={{ py: 3, color: 'text.secondary' }}>
<TableCell colSpan={9} align="center" sx={{ py: 3, color: 'text.secondary' }}>
Keine Artikel im Auftrag vorhanden.
</TableCell>
</TableRow>
@@ -411,6 +486,38 @@ export default class OrderShipmentDialog extends PureComponent {
>
{item.remaining}
</TableCell>
<TableCell
align="right"
title={item.stockTitle}
sx={{
fontWeight: 500,
color:
!item.cached || item.stockVal == null
? 'text.disabled'
: item.stockVal <= 0
? 'error.main'
: 'text.secondary',
}}
>
{!item.cached ? '…' : item.isTracked === false ? '∞' : formatStock(item.stockVal)}
</TableCell>
<TableCell
align="right"
title={item.reservedTitle}
sx={{
fontWeight: 600,
color:
!item.cached || item.availableVal == null
? 'text.disabled'
: item.availableVal <= 0
? 'error.main'
: item.remaining > item.availableVal
? 'warning.main'
: 'text.secondary',
}}
>
{!item.cached ? '…' : item.isTracked === false ? '∞' : formatStock(item.availableVal)}
</TableCell>
<TableCell align="right">
{isClosed ? (
<Chip size="small" label="Geliefert" color="success" variant="outlined" sx={{ height: 22, fontSize: '0.7rem' }} />