This commit is contained in:
seb
2026-10-16 19:36:08 +02:00
parent 4a7a02a0a4
commit 7ad8a3a301
5 changed files with 167 additions and 95 deletions

View File

@@ -47,6 +47,7 @@ export default class BookDeliveryDialog extends PureComponent {
let initialAmounts = {}; let initialAmounts = {};
if (Array.isArray(props.summary?.lines)) { if (Array.isArray(props.summary?.lines)) {
for (const line of props.summary.lines) { for (const line of props.summary.lines) {
if (!line.trackStock) continue;
if (props.prefillLineId != null) { if (props.prefillLineId != null) {
initialAmounts[line.id] = initialAmounts[line.id] =
line.id === props.prefillLineId line.id === props.prefillLineId
@@ -174,6 +175,7 @@ export default class BookDeliveryDialog extends PureComponent {
const amounts = {}; const amounts = {};
if (Array.isArray(summary?.lines)) { if (Array.isArray(summary?.lines)) {
for (const line of summary.lines) { for (const line of summary.lines) {
if (!line.trackStock) continue;
if (prefillLineId != null) { if (prefillLineId != null) {
amounts[line.id] = amounts[line.id] =
line.id === prefillLineId line.id === prefillLineId
@@ -208,6 +210,7 @@ export default class BookDeliveryDialog extends PureComponent {
if (!summary?.lines) return; if (!summary?.lines) return;
const amounts = {}; const amounts = {};
for (const line of summary.lines) { for (const line of summary.lines) {
if (!line.trackStock) continue;
amounts[line.id] = line.remainingAmount > 0 ? line.remainingAmount : 0; amounts[line.id] = line.remainingAmount > 0 ? line.remainingAmount : 0;
} }
this.setState({ amountsToBook: amounts }); this.setState({ amountsToBook: amounts });
@@ -218,6 +221,7 @@ export default class BookDeliveryDialog extends PureComponent {
if (!summary?.lines) return; if (!summary?.lines) return;
const amounts = {}; const amounts = {};
for (const line of summary.lines) { for (const line of summary.lines) {
if (!line.trackStock) continue;
amounts[line.id] = 0; amounts[line.id] = 0;
} }
this.setState({ amountsToBook: amounts }); this.setState({ amountsToBook: amounts });
@@ -229,6 +233,7 @@ export default class BookDeliveryDialog extends PureComponent {
if (!summary?.lines) return; if (!summary?.lines) return;
const linesToBook = summary.lines const linesToBook = summary.lines
.filter((line) => line.trackStock)
.map((line) => ({ .map((line) => ({
manifestarticleId: line.id, manifestarticleId: line.id,
sku: line.sku, sku: line.sku,
@@ -382,12 +387,12 @@ export default class BookDeliveryDialog extends PureComponent {
renderLinesTable() { renderLinesTable() {
const { summary, amountsToBook, isBooking } = this.state; const { summary, amountsToBook, isBooking } = this.state;
const lines = summary?.lines || []; const lines = (summary?.lines || []).filter((l) => l.trackStock);
if (lines.length === 0) { if (lines.length === 0) {
return ( return (
<Alert severity="info" sx={{ my: 2 }}> <Alert severity="info" sx={{ my: 2 }}>
Diese Lieferung enthält keine Artikelpositionen zum Einbuchen. Diese Lieferung enthält keine lagergeführten Artikel zum Einbuchen.
</Alert> </Alert>
); );
} }
@@ -490,7 +495,8 @@ export default class BookDeliveryDialog extends PureComponent {
warehouseError, warehouseError,
} = this.state; } = this.state;
const lines = summary?.lines || []; const lines = (summary?.lines || []).filter((l) => l.trackStock);
const hasTrackStock = lines.length > 0;
const prefillLine = const prefillLine =
this.props.prefillLineId != null this.props.prefillLineId != null
? lines.find((l) => l.id === this.props.prefillLineId) ? lines.find((l) => l.id === this.props.prefillLineId)
@@ -502,7 +508,7 @@ export default class BookDeliveryDialog extends PureComponent {
const countLinesToBook = lines.filter( const countLinesToBook = lines.filter(
(l) => Number(amountsToBook[l.id]) > 0 (l) => Number(amountsToBook[l.id]) > 0
).length; ).length;
const isFullyBooked = summary?.status === 'booked'; const isFullyBooked = hasTrackStock && summary?.status === 'booked';
const selectedWarehouse = warehouses.find((w) => w.id === selectedWarehouseId); const selectedWarehouse = warehouses.find((w) => w.id === selectedWarehouseId);
const isWarehouseMissing = !selectedWarehouseId; const isWarehouseMissing = !selectedWarehouseId;
@@ -554,7 +560,7 @@ export default class BookDeliveryDialog extends PureComponent {
)} )}
{this.renderLinesTable()} {this.renderLinesTable()}
{!isFullyBooked && lines.length > 0 && ( {!isFullyBooked && hasTrackStock && (
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}> <Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
<Button <Button
size="small" size="small"
@@ -576,75 +582,77 @@ export default class BookDeliveryDialog extends PureComponent {
</Box> </Box>
)} )}
{warehouses.length === 0 && !isFullyBooked && ( {hasTrackStock && warehouses.length === 0 && !isFullyBooked && (
<Alert severity="error" sx={{ mb: 2 }}> <Alert severity="error" sx={{ mb: 2 }}>
Es ist noch kein Lager eingerichtet. Bitte legen Sie unter „Artikel → Lager“ zuerst ein Lager an. Es ist noch kein Lager eingerichtet. Bitte legen Sie unter „Artikel → Lager“ zuerst ein Lager an.
</Alert> </Alert>
)} )}
<Box {hasTrackStock && (
sx={{ <Box
display: 'flex', sx={{
gap: 2, display: 'flex',
mb: 2, gap: 2,
flexDirection: { xs: 'column', sm: 'row' }, mb: 2,
}} flexDirection: { xs: 'column', sm: 'row' },
> }}
<FormControl
size="small"
required
error={Boolean(warehouseError)}
disabled={isBooking || isFullyBooked}
sx={{ minWidth: { sm: 240 }, flex: { sm: '0 0 auto' } }}
> >
<InputLabel id="warehouse-select-label">Ziellager *</InputLabel> <FormControl
<Select size="small"
labelId="warehouse-select-label" required
id="warehouse-select" error={Boolean(warehouseError)}
value={selectedWarehouseId ?? ''} disabled={isBooking || isFullyBooked}
label="Ziellager *" sx={{ minWidth: { sm: 240 }, flex: { sm: '0 0 auto' } }}
onChange={this.handleWarehouseChange}
startAdornment={
<WarehouseIcon
sx={{ mr: 1, color: 'text.secondary', fontSize: 20 }}
/>
}
> >
{warehouses.length === 0 ? ( <InputLabel id="warehouse-select-label">Ziellager *</InputLabel>
<MenuItem value="" disabled> <Select
Keine Lager vorhanden labelId="warehouse-select-label"
</MenuItem> id="warehouse-select"
) : ( value={selectedWarehouseId ?? ''}
warehouses.map((wh) => ( label="Ziellager *"
<MenuItem key={wh.id} value={wh.id}> onChange={this.handleWarehouseChange}
{wh.name} startAdornment={
<WarehouseIcon
sx={{ mr: 1, color: 'text.secondary', fontSize: 20 }}
/>
}
>
{warehouses.length === 0 ? (
<MenuItem value="" disabled>
Keine Lager vorhanden
</MenuItem> </MenuItem>
)) ) : (
warehouses.map((wh) => (
<MenuItem key={wh.id} value={wh.id}>
{wh.name}
</MenuItem>
))
)}
</Select>
{warehouseError ? (
<FormHelperText>{warehouseError}</FormHelperText>
) : (
<FormHelperText>Pflichtfeld: Ziellager für die Buchung</FormHelperText>
)} )}
</Select> </FormControl>
{warehouseError ? (
<FormHelperText>{warehouseError}</FormHelperText>
) : (
<FormHelperText>Pflichtfeld: Ziellager für die Buchung</FormHelperText>
)}
</FormControl>
<TextField <TextField
fullWidth fullWidth
size="small" size="small"
label="Buchungstext (Ledger-Referenz)" label="Buchungstext (Ledger-Referenz)"
value={transactionText} value={transactionText}
onChange={this.handleTransactionTextChange} onChange={this.handleTransactionTextChange}
disabled={isBooking || isFullyBooked} disabled={isBooking || isFullyBooked}
helperText="Wird als Transaktionstext in das Ledger übernommen" helperText="Wird als Transaktionstext in das Ledger übernommen"
/> />
</Box> </Box>
)}
{isFullyBooked ? ( {isFullyBooked ? (
<Alert severity="success" icon={<CheckCircleOutlineOutlinedIcon />}> <Alert severity="success" icon={<CheckCircleOutlineOutlinedIcon />}>
Alle Artikel dieser Lieferung sind bereits vollständig im Lager verbucht. Alle Artikel dieser Lieferung sind bereits vollständig im Lager verbucht.
</Alert> </Alert>
) : totalToBook > 0 ? ( ) : hasTrackStock && totalToBook > 0 ? (
<Alert severity={selectedWarehouseId ? 'info' : 'warning'}> <Alert severity={selectedWarehouseId ? 'info' : 'warning'}>
{selectedWarehouseId ? ( {selectedWarehouseId ? (
<> <>
@@ -659,11 +667,11 @@ export default class BookDeliveryDialog extends PureComponent {
</> </>
)} )}
</Alert> </Alert>
) : ( ) : hasTrackStock ? (
<Alert severity="warning"> <Alert severity="warning">
Wählen Sie mindestens eine Menge größer als 0 aus, um den Lagerzugang zu buchen. Wählen Sie mindestens eine Menge größer als 0 aus, um den Lagerzugang zu buchen.
</Alert> </Alert>
)} ) : null}
</> </>
)} )}
</DialogContent> </DialogContent>

View File

@@ -184,7 +184,7 @@ class ManifestFormLine extends PureComponent {
renderLedgerChip() { renderLedgerChip() {
const { ledgerLine, onBookLine } = this.props; const { ledgerLine, onBookLine } = this.props;
if (!ledgerLine) return null; if (!ledgerLine || !ledgerLine.trackStock) return null;
const isBooked = ledgerLine.isBooked; const isBooked = ledgerLine.isBooked;
const isPartial = !isBooked && ledgerLine.bookedAmount > 0; const isPartial = !isBooked && ledgerLine.bookedAmount > 0;
const isClickable = Boolean(onBookLine); const isClickable = Boolean(onBookLine);

View File

@@ -511,7 +511,7 @@ class ManifestHeader extends PureComponent {
Wiederherstellen Wiederherstellen
</Button> </Button>
)} )}
{docId != null && kind === 'delivery' && onOpenBookDialog && ( {docId != null && kind === 'delivery' && onOpenBookDialog && ledgerSummary?.hasTrackStock && (
<Button <Button
variant={ledgerSummary?.status === 'booked' ? 'outlined' : 'contained'} variant={ledgerSummary?.status === 'booked' ? 'outlined' : 'contained'}
color={ color={

View File

@@ -1234,6 +1234,8 @@ class LieferungNeuPage extends PureComponent {
} }
allEntries.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || ''))); allEntries.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
if (!ledgerSummary.hasTrackStock && allEntries.length === 0) return null;
return ( return (
<PanelTile <PanelTile
wide={4} wide={4}
@@ -1328,25 +1330,27 @@ class LieferungNeuPage extends PureComponent {
)} )}
</Box> </Box>
<Button {ledgerSummary.hasTrackStock && (
variant={status === 'booked' ? 'outlined' : 'contained'} <Button
color={status === 'booked' ? 'success' : status === 'partial' ? 'warning' : 'primary'} variant={status === 'booked' ? 'outlined' : 'contained'}
startIcon={ color={status === 'booked' ? 'success' : status === 'partial' ? 'warning' : 'primary'}
status === 'booked' ? ( startIcon={
<CheckCircleOutlineOutlinedIcon /> status === 'booked' ? (
) : ( <CheckCircleOutlineOutlinedIcon />
<Inventory2OutlinedIcon /> ) : (
) <Inventory2OutlinedIcon />
} )
onClick={this.handleOpenBookDialog} }
sx={{ fontWeight: 600, textTransform: 'none' }} onClick={this.handleOpenBookDialog}
> sx={{ fontWeight: 600, textTransform: 'none' }}
{status === 'booked' >
? 'Buchungsdetails anzeigen' {status === 'booked'
: status === 'partial' ? 'Buchungsdetails anzeigen'
? 'Restliche Mengen einbuchen' : status === 'partial'
: 'Ins Lager buchen'} ? 'Restliche Mengen einbuchen'
</Button> : 'Ins Lager buchen'}
</Button>
)}
</Box> </Box>
{allEntries.length > 0 && ( {allEntries.length > 0 && (

View File

@@ -1,5 +1,34 @@
import { delivery as defaultDeliveryApi, ledger as defaultLedgerApi } from '../api'; import { delivery as defaultDeliveryApi, ledger as defaultLedgerApi } from '../api';
import artikelAutomat from '../automats/artikelAutomat'; import artikelAutomat, { getArticle } from '../automats/artikelAutomat';
import articleIndexAutomat from '../automats/articleIndexAutomat';
/**
* Determines whether a delivery line represents an article that tracks stock.
*
* @param {object} line Delivery line
* @param {object|null} [article] Optional pre-resolved article
* @returns {boolean}
*/
export function isLineStockTracked(line, article = null) {
if (!line) return false;
if (line.track_stock !== undefined) {
return line.track_stock !== false && line.track_stock !== 0 && line.track_stock !== '0';
}
if (line.trackStock !== undefined) {
return Boolean(line.trackStock);
}
if (line.article?.track_stock !== undefined) {
return line.article.track_stock !== false && line.article.track_stock !== 0 && line.article.track_stock !== '0';
}
const sku = line.sku ? String(line.sku).trim() : null;
if (!sku) return false;
const art = article || getArticle(sku) || articleIndexAutomat.state.items?.find((item) => String(item.sku) === sku);
if (art?.track_stock !== undefined) {
return art.track_stock !== false && art.track_stock !== 0 && art.track_stock !== '0';
}
return false;
}
/** /**
* @typedef {Object} DeliveryLineLedgerStatus * @typedef {Object} DeliveryLineLedgerStatus
@@ -10,6 +39,8 @@ import artikelAutomat from '../automats/artikelAutomat';
* @property {number} bookedAmount - Quantity booked in ledger * @property {number} bookedAmount - Quantity booked in ledger
* @property {number} remainingAmount - Quantity remaining to be booked * @property {number} remainingAmount - Quantity remaining to be booked
* @property {boolean} isBooked - Whether this position is fully booked * @property {boolean} isBooked - Whether this position is fully booked
* @property {boolean} trackStock - Whether this article tracks inventory
* @property {boolean} isBookable - Whether this position can be booked into the warehouse
* @property {Array<Object>} ledgerEntries - Ledger entries for this line * @property {Array<Object>} ledgerEntries - Ledger entries for this line
*/ */
@@ -22,6 +53,7 @@ import artikelAutomat from '../automats/artikelAutomat';
* @property {number} bookedPositions * @property {number} bookedPositions
* @property {number} totalQuantity * @property {number} totalQuantity
* @property {number} bookedQuantity * @property {number} bookedQuantity
* @property {boolean} hasTrackStock
* @property {Array<DeliveryLineLedgerStatus>} lines * @property {Array<DeliveryLineLedgerStatus>} lines
*/ */
@@ -48,6 +80,7 @@ export async function fetchDeliveryLedgerSummary(deliveryId, { deliveryApi = def
bookedPositions: 0, bookedPositions: 0,
totalQuantity: 0, totalQuantity: 0,
bookedQuantity: 0, bookedQuantity: 0,
hasTrackStock: false,
lines: [], lines: [],
}; };
} }
@@ -56,20 +89,42 @@ export async function fetchDeliveryLedgerSummary(deliveryId, { deliveryApi = def
const linesWithLedger = await Promise.all( const linesWithLedger = await Promise.all(
rawLines.map(async (line) => { rawLines.map(async (line) => {
let ledgerEntries = []; let ledgerEntries = [];
try { let article = null;
const ledgerRes = await ledgerApi.listByManifestarticleId(line.id, 'DELIVERY');
ledgerEntries = Array.isArray(ledgerRes?.ledger) ? ledgerRes.ledger : []; const promises = [
} catch (err) { ledgerApi.listByManifestarticleId(line.id, 'DELIVERY').catch((err) => {
console.error(`[deliveryLedger] Failed to fetch ledger for line ${line.id}:`, err); console.error(`[deliveryLedger] Failed to fetch ledger for line ${line.id}:`, err);
return null;
}),
];
if (line.sku) {
const cached = getArticle(line.sku);
if (cached) {
article = cached;
} else {
promises.push(
artikelAutomat.actions.loadArticle(line.sku).catch(() => null)
);
}
} }
const results = await Promise.all(promises);
const ledgerRes = results[0];
if (results[1]) {
article = results[1];
}
ledgerEntries = Array.isArray(ledgerRes?.ledger) ? ledgerRes.ledger : [];
const trackStock = isLineStockTracked(line, article);
const deliveryEntries = ledgerEntries.filter( const deliveryEntries = ledgerEntries.filter(
(e) => String(e.transaction_type).toUpperCase() === 'DELIVERY' (e) => String(e.transaction_type).toUpperCase() === 'DELIVERY'
); );
const bookedAmount = deliveryEntries.reduce((sum, e) => sum + Number(e.amount || 0), 0); const bookedAmount = deliveryEntries.reduce((sum, e) => sum + Number(e.amount || 0), 0);
const targetAmount = Number(line.amount || 0); const targetAmount = Number(line.amount || 0);
const remainingAmount = Math.max(0, targetAmount - bookedAmount); const remainingAmount = trackStock ? Math.max(0, targetAmount - bookedAmount) : 0;
const isBooked = targetAmount > 0 ? bookedAmount >= targetAmount : bookedAmount > 0; const isBooked = trackStock ? (targetAmount > 0 ? bookedAmount >= targetAmount : bookedAmount > 0) : true;
return { return {
...line, ...line,
@@ -77,18 +132,22 @@ export async function fetchDeliveryLedgerSummary(deliveryId, { deliveryApi = def
bookedAmount, bookedAmount,
remainingAmount, remainingAmount,
isBooked, isBooked,
trackStock,
isBookable: trackStock,
ledgerEntries: deliveryEntries, ledgerEntries: deliveryEntries,
}; };
}) })
); );
const totalPositions = linesWithLedger.length; const stockLines = linesWithLedger.filter((l) => l.trackStock);
const bookedPositions = linesWithLedger.filter((l) => l.isBooked).length; const totalPositions = stockLines.length;
const totalQuantity = linesWithLedger.reduce((sum, l) => sum + l.targetAmount, 0); const bookedPositions = stockLines.filter((l) => l.isBooked).length;
const bookedQuantity = linesWithLedger.reduce((sum, l) => sum + l.bookedAmount, 0); const totalQuantity = stockLines.reduce((sum, l) => sum + l.targetAmount, 0);
const bookedQuantity = stockLines.reduce((sum, l) => sum + l.bookedAmount, 0);
const hasTrackStock = stockLines.length > 0;
let status; let status;
if (totalPositions === 0) { if (!hasTrackStock) {
status = 'empty'; status = 'empty';
} else if (bookedPositions === totalPositions && bookedQuantity >= totalQuantity) { } else if (bookedPositions === totalPositions && bookedQuantity >= totalQuantity) {
status = 'booked'; status = 'booked';
@@ -106,6 +165,7 @@ export async function fetchDeliveryLedgerSummary(deliveryId, { deliveryApi = def
bookedPositions, bookedPositions,
totalQuantity, totalQuantity,
bookedQuantity, bookedQuantity,
hasTrackStock,
lines: linesWithLedger, lines: linesWithLedger,
}; };
} }