u
This commit is contained in:
@@ -332,6 +332,8 @@ export default class ArticleSearch extends PureComponent {
|
||||
if (
|
||||
this.state.indexStatus !== prevState.indexStatus ||
|
||||
this.props.excludeSkus !== prevProps.excludeSkus ||
|
||||
this.props.includeSkus !== prevProps.includeSkus ||
|
||||
this.props.extraArticles !== prevProps.extraArticles ||
|
||||
this.props.allowParent !== prevProps.allowParent ||
|
||||
this.props.categories !== prevProps.categories
|
||||
) {
|
||||
@@ -355,6 +357,8 @@ export default class ArticleSearch extends PureComponent {
|
||||
runQuery = (queryText) => {
|
||||
const {
|
||||
excludeSkus = [],
|
||||
includeSkus = null,
|
||||
extraArticles = [],
|
||||
allowParent = false,
|
||||
categories = [],
|
||||
mode,
|
||||
@@ -364,6 +368,8 @@ export default class ArticleSearch extends PureComponent {
|
||||
// full match set; the autocomplete popper keeps a small option cap.
|
||||
const res = searchArticlesIndex(queryText, {
|
||||
excludeSkus,
|
||||
includeSkus,
|
||||
extraArticles,
|
||||
allowParent,
|
||||
categories,
|
||||
offset: 0,
|
||||
|
||||
@@ -11,6 +11,8 @@ import Alert from '@mui/material/Alert';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import ArticleSearch from '../article/ArticleSearch';
|
||||
|
||||
import deliveryEditorAutomat from '../../automats/deliveryEditorAutomat';
|
||||
|
||||
export default class ArticleSearchDialog extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -26,6 +28,11 @@ export default class ArticleSearchDialog extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
this.setState({ assigningSku: null, error: null });
|
||||
this.props.onClose?.();
|
||||
};
|
||||
|
||||
handleSelect = async (article) => {
|
||||
const { onAssign, line, kind = 'delivery' } = this.props;
|
||||
const isDelivery = kind === 'delivery';
|
||||
@@ -41,26 +48,51 @@ export default class ArticleSearchDialog extends PureComponent {
|
||||
await onAssign(article, line);
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
assigningSku: null,
|
||||
error:
|
||||
err?.message ||
|
||||
(isDelivery
|
||||
? 'Fehler beim Verknüpfen des Lieferanten-Artikels.'
|
||||
? (line ? 'Fehler beim Verknüpfen des Lieferanten-Artikels.' : 'Fehler beim Hinzufügen des Artikels.')
|
||||
: 'Fehler beim Zuordnen des Artikels.'),
|
||||
});
|
||||
} finally {
|
||||
this.setState({ assigningSku: null });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { open, onClose, line, kind = 'delivery' } = this.props;
|
||||
const { open, onClose, line, kind = 'delivery', supplierArticles: propSupplierArticles } = this.props;
|
||||
const { assigningSku, error } = this.state;
|
||||
const isDelivery = kind === 'delivery';
|
||||
const onlySupplierArticles = isDelivery && !line;
|
||||
const rawSku = line?.sku != null ? String(line.sku).trim() : '';
|
||||
|
||||
const supplierArticlesList = onlySupplierArticles
|
||||
? (propSupplierArticles ?? deliveryEditorAutomat.state.supplierArticles ?? [])
|
||||
: null;
|
||||
|
||||
const includeSkus = onlySupplierArticles
|
||||
? supplierArticlesList.map((sa) => sa.article?.sku || sa.sku).filter(Boolean)
|
||||
: null;
|
||||
|
||||
const extraArticles = onlySupplierArticles
|
||||
? supplierArticlesList.map((sa) => ({
|
||||
id: sa.article?.id ?? sa.id,
|
||||
sku: sa.article?.sku || sa.sku,
|
||||
name: sa.article?.name || sa.name || sa.sku || '',
|
||||
brand: sa.article?.brand,
|
||||
brandName: sa.article?.brand?.name || sa.article?.brandName,
|
||||
stock: sa.article?.stock ?? sa.stock,
|
||||
available: sa.article?.available,
|
||||
price: sa.price?.net ?? sa.article?.price ?? sa.article?.vknetto,
|
||||
eans: sa.article?.eans || [sa.sku].filter(Boolean),
|
||||
isParent: false,
|
||||
}))
|
||||
: [];
|
||||
|
||||
return (
|
||||
<AppDialog
|
||||
open={Boolean(open)}
|
||||
onClose={assigningSku ? undefined : onClose}
|
||||
onClose={this.handleClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
aria-labelledby="article-search-dialog-title"
|
||||
@@ -89,18 +121,29 @@ export default class ArticleSearchDialog extends PureComponent {
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="h6" component="div" fontWeight={600}>
|
||||
{line ? 'Existierenden Artikel zuordnen' : 'Artikel auswählen'}
|
||||
{line
|
||||
? 'Existierenden Artikel zuordnen'
|
||||
: isDelivery
|
||||
? 'Lieferanten-Artikel auswählen'
|
||||
: 'Artikel auswählen'}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{isDelivery ? 'Lieferanten-SKU' : 'SKU'}: <strong>{rawSku || '–'}</strong>
|
||||
{line?.name ? ` · ${line.name}` : ''}
|
||||
{line ? (
|
||||
<>
|
||||
{isDelivery ? 'Lieferanten-SKU' : 'SKU'}: <strong>{rawSku || '–'}</strong>
|
||||
{line?.name ? ` · ${line.name}` : ''}
|
||||
</>
|
||||
) : isDelivery ? (
|
||||
'Nur mit dem ausgewählten Lieferanten verknüpfte Artikel'
|
||||
) : (
|
||||
'Artikel für Beleg auswählen'
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Schließen"
|
||||
disabled={Boolean(assigningSku)}
|
||||
onClick={onClose}
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
@@ -123,12 +166,21 @@ export default class ArticleSearchDialog extends PureComponent {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{onlySupplierArticles && supplierArticlesList.length === 0 && (
|
||||
<Alert severity="info" sx={{ mb: 2, flexShrink: 0 }}>
|
||||
Für diesen Lieferanten sind noch keine Artikel verknüpft.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<ArticleSearch
|
||||
mode="list"
|
||||
autoFocus
|
||||
allowParent={false}
|
||||
busySku={assigningSku}
|
||||
actionLabel={assigningSku ? 'Zuordnen…' : 'Zuordnen'}
|
||||
initialQuery={line?.name ? String(line.name) : ''}
|
||||
includeSkus={includeSkus}
|
||||
extraArticles={extraArticles}
|
||||
actionLabel={assigningSku ? (line ? 'Zuordnen…' : 'Hinzufügen…') : (line ? 'Zuordnen' : 'Hinzufügen')}
|
||||
onSelect={this.handleSelect}
|
||||
/>
|
||||
</DialogContent>
|
||||
@@ -136,8 +188,7 @@ export default class ArticleSearchDialog extends PureComponent {
|
||||
<DialogActions sx={{ px: 2, py: 1.5 }}>
|
||||
<Button
|
||||
size="small"
|
||||
disabled={Boolean(assigningSku)}
|
||||
onClick={onClose}
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { PureComponent } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Attachment from './Attachment';
|
||||
|
||||
export class AttachmentList extends PureComponent {
|
||||
@@ -13,14 +12,6 @@ export class AttachmentList extends PureComponent {
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 2.5, pt: 1.5, borderTop: '1px solid', borderColor: 'divider' }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
fontWeight={600}
|
||||
sx={{ mb: 1, display: 'block', textTransform: 'uppercase', letterSpacing: '0.05em' }}
|
||||
>
|
||||
Dateianhänge
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={1.5} sx={{ flexWrap: 'wrap', gap: 1.5 }}>
|
||||
{attachments.map((attachment, index) => (
|
||||
<Attachment
|
||||
|
||||
@@ -34,12 +34,75 @@ import {
|
||||
fetchDeliveryLedgerSummary,
|
||||
bookDeliveryLines,
|
||||
} from '../../utils/deliveryLedger';
|
||||
import { getArticle } from '../../automats/artikelAutomat';
|
||||
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||
import { supplier as supplierApi } from '../../api';
|
||||
import deliveryEditorAutomat, { buildSupplierArticlesMap } from '../../automats/deliveryEditorAutomat';
|
||||
import {
|
||||
SURFACE_BASE,
|
||||
SURFACE_RAISED,
|
||||
} from '../../colors';
|
||||
|
||||
function resolveLineSkus(line, delivery, supplierArticlesMap = {}) {
|
||||
const rawSku = line?.sku ? String(line.sku).trim() : '';
|
||||
const supplierId = delivery?.supplierId ?? delivery?.partyId;
|
||||
|
||||
// 1. Direct line properties if available
|
||||
let supplierSku =
|
||||
line?.supplierSku ||
|
||||
line?.supplier_sku ||
|
||||
line?.supplierArticle?.sku ||
|
||||
'';
|
||||
let ownSku =
|
||||
line?.articleSku ||
|
||||
line?.article_sku ||
|
||||
line?.article?.sku ||
|
||||
line?.supplierArticle?.article?.sku ||
|
||||
'';
|
||||
|
||||
// 2. Check supplierArticlesMap
|
||||
const sa = rawSku
|
||||
? (supplierArticlesMap[rawSku] || supplierArticlesMap[rawSku.toLowerCase()])
|
||||
: null;
|
||||
|
||||
if (sa) {
|
||||
if (!supplierSku && sa.sku) supplierSku = sa.sku;
|
||||
if (!ownSku && sa.article?.sku) ownSku = sa.article.sku;
|
||||
}
|
||||
|
||||
// 3. Check loaded article details
|
||||
const cachedArticle =
|
||||
line?.article ||
|
||||
(ownSku ? getArticle(ownSku) : null) ||
|
||||
(rawSku ? getArticle(rawSku) : null);
|
||||
|
||||
if (cachedArticle) {
|
||||
if (!ownSku && cachedArticle.sku) {
|
||||
ownSku = cachedArticle.sku;
|
||||
}
|
||||
const sup =
|
||||
(supplierId && cachedArticle.suppliers?.find((s) => s.entityId === supplierId)) ||
|
||||
cachedArticle.suppliers?.[0];
|
||||
if (!supplierSku && sup?.sku) {
|
||||
supplierSku = sup.sku;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Fallback heuristics if one is still missing
|
||||
if (!supplierSku && !ownSku) {
|
||||
ownSku = rawSku;
|
||||
} else if (!supplierSku && rawSku && rawSku !== ownSku) {
|
||||
supplierSku = rawSku;
|
||||
} else if (!ownSku && rawSku && rawSku !== supplierSku) {
|
||||
ownSku = rawSku;
|
||||
}
|
||||
|
||||
return {
|
||||
supplierSku: supplierSku || '–',
|
||||
ownSku: ownSku || '–',
|
||||
};
|
||||
}
|
||||
|
||||
export default class BookDeliveryDialog extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -61,19 +124,23 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
}
|
||||
|
||||
const initialWarehouses = warehouseAutomat.state.warehouses || [];
|
||||
const initialSupplierMap =
|
||||
props.supplierArticlesMap ||
|
||||
(deliveryEditorAutomat?.state?.supplierArticlesMap ?? {});
|
||||
|
||||
this.state = {
|
||||
summary: props.summary ?? null,
|
||||
loading: !props.summary,
|
||||
error: null,
|
||||
amountsToBook: initialAmounts,
|
||||
transactionText: props.delivery?.number
|
||||
transactionText: props.delivery?.externalNumber || (props.delivery?.number
|
||||
? `Lieferung ${props.delivery.number}`
|
||||
: 'Lieferung Wareneingang',
|
||||
: 'Lieferung Wareneingang'),
|
||||
isBooking: false,
|
||||
warehouses: initialWarehouses,
|
||||
selectedWarehouseId: initialWarehouses.length === 1 ? initialWarehouses[0].id : '',
|
||||
warehouseError: null,
|
||||
supplierArticlesMap: initialSupplierMap,
|
||||
};
|
||||
|
||||
this.handleAmountChange = this.handleAmountChange.bind(this);
|
||||
@@ -84,6 +151,17 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
this.handleConfirmBook = this.handleConfirmBook.bind(this);
|
||||
}
|
||||
|
||||
loadSupplierArticlesIfNeeded() {
|
||||
const supplierId = this.props.delivery?.supplierId ?? this.props.delivery?.partyId;
|
||||
if (supplierId && Object.keys(this.state.supplierArticlesMap).length === 0) {
|
||||
void supplierApi.get(supplierId).then((res) => {
|
||||
const articles = res?.supplier?.articles || res?.articles || [];
|
||||
const map = buildSupplierArticlesMap(articles);
|
||||
this.setState({ supplierArticlesMap: map });
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.unsubWarehouse = warehouseAutomat.subscribe((state) => {
|
||||
const warehouses = state.warehouses || [];
|
||||
@@ -98,6 +176,7 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
});
|
||||
});
|
||||
void warehouseAutomat.actions.load().catch(() => {});
|
||||
this.loadSupplierArticlesIfNeeded();
|
||||
|
||||
if (this.props.open) {
|
||||
void this.loadSummary();
|
||||
@@ -111,6 +190,9 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.open && !prevProps.open) {
|
||||
const currentWarehouses = this.state.warehouses || warehouseAutomat.state.warehouses || [];
|
||||
const currentSupplierMap =
|
||||
this.props.supplierArticlesMap ||
|
||||
(deliveryEditorAutomat?.state?.supplierArticlesMap ?? this.state.supplierArticlesMap ?? {});
|
||||
this.setState(
|
||||
{
|
||||
summary: this.props.summary ?? null,
|
||||
@@ -120,13 +202,15 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
selectedWarehouseId:
|
||||
this.state.selectedWarehouseId ||
|
||||
(currentWarehouses.length === 1 ? currentWarehouses[0].id : ''),
|
||||
transactionText: this.props.delivery?.number
|
||||
transactionText: this.props.delivery?.externalNumber || (this.props.delivery?.number
|
||||
? `Lieferung ${this.props.delivery.number}`
|
||||
: 'Lieferung Wareneingang',
|
||||
: 'Lieferung Wareneingang'),
|
||||
isBooking: false,
|
||||
supplierArticlesMap: currentSupplierMap,
|
||||
},
|
||||
() => {
|
||||
void warehouseAutomat.actions.load().catch(() => {});
|
||||
this.loadSupplierArticlesIfNeeded();
|
||||
if (this.props.summary) {
|
||||
this.initAmountsFromSummary(this.props.summary, this.props.prefillLineId);
|
||||
} else {
|
||||
@@ -284,105 +368,111 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
renderHeaderInfo() {
|
||||
renderHeaderInfo({ hasTrackedLinesToBook, isWarehouseMissing } = {}) {
|
||||
const { delivery, partyName } = this.props;
|
||||
const { summary } = this.state;
|
||||
const {
|
||||
transactionText,
|
||||
isBooking,
|
||||
summary,
|
||||
warehouses,
|
||||
selectedWarehouseId,
|
||||
warehouseError,
|
||||
} = this.state;
|
||||
const isFullyBooked = (summary?.lines?.length ?? 0) > 0 && summary?.status === 'booked';
|
||||
|
||||
return (
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={{
|
||||
p: 1.5,
|
||||
mb: 2,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 3,
|
||||
backgroundColor: SURFACE_BASE,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Nummer
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{delivery?.number || '–'}
|
||||
</Typography>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1.5,
|
||||
mb: 1.5,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Datum"
|
||||
value={formatDate(delivery?.date) || '–'}
|
||||
disabled
|
||||
sx={{ flex: { xs: '1 1 100%', sm: '1 1 110px' }, minWidth: { sm: 100 } }}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Lieferant"
|
||||
value={partyName || '–'}
|
||||
disabled
|
||||
sx={{ flex: { xs: '1 1 100%', sm: '2 1 180px' }, minWidth: { sm: 140 } }}
|
||||
/>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Nummer"
|
||||
value={delivery?.number || '–'}
|
||||
disabled
|
||||
sx={{ flex: { xs: '1 1 100%', sm: '1 1 110px' }, minWidth: { sm: 100 } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{delivery?.externalNumber && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Referenz
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{delivery.externalNumber}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Lieferdatum
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{formatDate(delivery?.date)}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Referenz"
|
||||
value={transactionText}
|
||||
onChange={this.handleTransactionTextChange}
|
||||
disabled={isBooking || isFullyBooked}
|
||||
sx={{ flex: { xs: '1 1 100%', sm: '1 1 200px' } }}
|
||||
/>
|
||||
<FormControl
|
||||
size="small"
|
||||
required={hasTrackedLinesToBook}
|
||||
error={Boolean(warehouseError) || isWarehouseMissing}
|
||||
disabled={isBooking || isFullyBooked}
|
||||
sx={{ flex: { xs: '1 1 100%', sm: '1 1 200px' } }}
|
||||
>
|
||||
<InputLabel id="warehouse-select-label">
|
||||
Ziellager{hasTrackedLinesToBook ? ' *' : ''}
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="warehouse-select-label"
|
||||
id="warehouse-select"
|
||||
value={selectedWarehouseId ?? ''}
|
||||
label={`Ziellager${hasTrackedLinesToBook ? ' *' : ''}`}
|
||||
onChange={this.handleWarehouseChange}
|
||||
startAdornment={
|
||||
<WarehouseIcon
|
||||
sx={{ mr: 1, color: 'text.secondary', fontSize: 20 }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{warehouses.length === 0 ? (
|
||||
<MenuItem value="" disabled>
|
||||
Keine Lager vorhanden
|
||||
</MenuItem>
|
||||
) : (
|
||||
warehouses.map((wh) => (
|
||||
<MenuItem key={wh.id} value={wh.id}>
|
||||
{wh.name}
|
||||
</MenuItem>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
{warehouseError && (
|
||||
<FormHelperText>{warehouseError}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
{partyName && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Lieferant
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
{partyName}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Aktueller Status:
|
||||
</Typography>
|
||||
{summary?.status === 'booked' && (
|
||||
<Chip
|
||||
size="small"
|
||||
icon={<CheckCircleOutlineOutlinedIcon />}
|
||||
label="Vollständig gebucht"
|
||||
color="success"
|
||||
variant="outlined"
|
||||
sx={{ fontWeight: 600 }}
|
||||
/>
|
||||
)}
|
||||
{summary?.status === 'partial' && (
|
||||
<Chip
|
||||
size="small"
|
||||
icon={<Inventory2OutlinedIcon />}
|
||||
label={`Teilweise gebucht (${summary.bookedPositions}/${summary.totalPositions})`}
|
||||
color="warning"
|
||||
variant="outlined"
|
||||
sx={{ fontWeight: 600 }}
|
||||
/>
|
||||
)}
|
||||
{summary?.status === 'unbooked' && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={`Offen (${summary.totalPositions} Pos.)`}
|
||||
variant="outlined"
|
||||
sx={{ color: 'text.secondary', borderColor: 'divider' }}
|
||||
/>
|
||||
)}
|
||||
{summary?.status === 'empty' && (
|
||||
<Chip size="small" label="Keine Positionen" variant="outlined" />
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
renderLinesTable() {
|
||||
const { summary, amountsToBook, isBooking } = this.state;
|
||||
const { summary, amountsToBook, isBooking, supplierArticlesMap } = this.state;
|
||||
const lines = summary?.lines || [];
|
||||
|
||||
if (lines.length === 0) {
|
||||
@@ -398,7 +488,10 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
<Table size="small">
|
||||
<TableHead sx={{ backgroundColor: SURFACE_RAISED }}>
|
||||
<TableRow>
|
||||
<TableCell sx={{ fontWeight: 600 }}>SKU</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, width: 44 }} align="center">Pos.</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Lieferanten-SKU</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Eigene SKU</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>EAN</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Artikel</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>Liefermenge</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>Bereits gebucht</TableCell>
|
||||
@@ -406,29 +499,45 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{lines.map((line) => {
|
||||
{lines.map((line, index) => {
|
||||
const toBook = amountsToBook[line.id] ?? 0;
|
||||
const isAlreadyFullyBooked = line.isBooked && line.remainingAmount <= 0;
|
||||
const posNumber = line.pos ?? line.position ?? (index + 1);
|
||||
const { supplierSku, ownSku } = resolveLineSkus(
|
||||
line,
|
||||
this.props.delivery,
|
||||
supplierArticlesMap
|
||||
);
|
||||
const cachedArticle =
|
||||
line.article ||
|
||||
(ownSku && ownSku !== '–' ? getArticle(ownSku) : null) ||
|
||||
(line.sku ? getArticle(line.sku) : null);
|
||||
const eans = line.ean
|
||||
? [line.ean]
|
||||
: line.eans || cachedArticle?.eans || cachedArticle?.articleean || [];
|
||||
const primaryEan = Array.isArray(eans) ? (eans[0] ?? '') : (eans ?? '');
|
||||
|
||||
return (
|
||||
<TableRow key={line.id} hover>
|
||||
<TableCell sx={{ fontFamily: 'monospace', fontSize: '0.85rem' }}>
|
||||
{line.sku || '–'}
|
||||
<TableCell align="center" sx={{ color: 'text.secondary', fontSize: '0.85rem' }}>
|
||||
{posNumber}
|
||||
</TableCell>
|
||||
<TableCell sx={{ maxWidth: 280 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }} noWrap>
|
||||
{line.name || '–'}
|
||||
</Typography>
|
||||
{!line.trackStock && (
|
||||
<Chip
|
||||
size="small"
|
||||
label="Ohne Lagerbestand"
|
||||
variant="outlined"
|
||||
sx={{ fontSize: '0.7rem', height: 20, color: 'text.secondary', borderColor: 'divider' }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<TableCell sx={{ fontFamily: 'monospace', fontSize: '0.85rem' }}>
|
||||
{supplierSku}
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontFamily: 'monospace', fontSize: '0.85rem' }}>
|
||||
{ownSku}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{ fontFamily: 'monospace', fontSize: '0.85rem' }}
|
||||
title={Array.isArray(eans) && eans.length > 1 ? eans.join(', ') : undefined}
|
||||
>
|
||||
{primaryEan || '–'}
|
||||
</TableCell>
|
||||
<TableCell sx={{ maxWidth: 260 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }} noWrap>
|
||||
{line.name || '–'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||
@@ -524,7 +633,7 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
<AppDialog
|
||||
open={Boolean(open)}
|
||||
onClose={isBooking ? undefined : onClose}
|
||||
maxWidth="md"
|
||||
maxWidth="lg"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle
|
||||
@@ -538,7 +647,7 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Inventory2OutlinedIcon color="primary" />
|
||||
<Typography variant="h6" component="span" sx={{ fontWeight: 600 }}>
|
||||
Lieferung {delivery?.number || ''} verbuchen
|
||||
Lieferung verbuchen
|
||||
</Typography>
|
||||
</Box>
|
||||
<IconButton size="small" onClick={onClose} disabled={isBooking}>
|
||||
@@ -546,8 +655,8 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent dividers sx={{ pt: 2 }}>
|
||||
{this.renderHeaderInfo()}
|
||||
<DialogContent dividers sx={{ pt: 2, borderBottom: 'none' }}>
|
||||
{this.renderHeaderInfo({ hasTrackedLinesToBook, isWarehouseMissing })}
|
||||
|
||||
{loading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6, gap: 2, alignItems: 'center' }}>
|
||||
@@ -568,125 +677,21 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
)}
|
||||
{this.renderLinesTable()}
|
||||
|
||||
{!isFullyBooked && hasLines && (
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
onClick={this.handleBookAll}
|
||||
disabled={isBooking}
|
||||
>
|
||||
Alle offenen Mengen übernehmen
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
variant="text"
|
||||
color="inherit"
|
||||
onClick={this.handleResetAmounts}
|
||||
disabled={isBooking}
|
||||
>
|
||||
Zurücksetzen
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{hasTrackedLinesToBook && warehouses.length === 0 && !isFullyBooked && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
Es ist noch kein Lager eingerichtet. Für lagergeführte Artikel legen Sie bitte unter „Artikel → Lager“ zuerst ein Lager an.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{hasLines && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 2,
|
||||
mb: 2,
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
}}
|
||||
>
|
||||
{hasTrackStock && (
|
||||
<FormControl
|
||||
size="small"
|
||||
required={hasTrackedLinesToBook}
|
||||
error={Boolean(warehouseError)}
|
||||
disabled={isBooking || isFullyBooked}
|
||||
sx={{ minWidth: { sm: 240 }, flex: { sm: '0 0 auto' } }}
|
||||
>
|
||||
<InputLabel id="warehouse-select-label">
|
||||
Ziellager {hasTrackedLinesToBook ? '*' : ''}
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="warehouse-select-label"
|
||||
id="warehouse-select"
|
||||
value={selectedWarehouseId ?? ''}
|
||||
label={`Ziellager ${hasTrackedLinesToBook ? '*' : ''}`}
|
||||
onChange={this.handleWarehouseChange}
|
||||
startAdornment={
|
||||
<WarehouseIcon
|
||||
sx={{ mr: 1, color: 'text.secondary', fontSize: 20 }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{warehouses.length === 0 ? (
|
||||
<MenuItem value="" disabled>
|
||||
Keine Lager vorhanden
|
||||
</MenuItem>
|
||||
) : (
|
||||
warehouses.map((wh) => (
|
||||
<MenuItem key={wh.id} value={wh.id}>
|
||||
{wh.name}
|
||||
</MenuItem>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
{warehouseError ? (
|
||||
<FormHelperText>{warehouseError}</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText>
|
||||
{hasTrackedLinesToBook
|
||||
? 'Pflichtfeld: Ziellager für lagergeführte Artikel'
|
||||
: 'Ziellager (optional)'}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
label="Buchungstext (Ledger-Referenz)"
|
||||
value={transactionText}
|
||||
onChange={this.handleTransactionTextChange}
|
||||
disabled={isBooking || isFullyBooked}
|
||||
helperText="Wird als Transaktionstext in das Ledger übernommen"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isFullyBooked ? (
|
||||
<Alert severity="success" icon={<CheckCircleOutlineOutlinedIcon />}>
|
||||
Alle Positionen dieser Lieferung sind bereits vollständig im Ledger verbucht.
|
||||
</Alert>
|
||||
) : countLinesToBook > 0 ? (
|
||||
<Alert severity={isWarehouseMissing ? 'warning' : 'info'}>
|
||||
{isWarehouseMissing ? (
|
||||
<>
|
||||
Bitte wählen Sie ein <strong>Ziellager</strong> aus, um die lagergeführten Artikel zu buchen.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Es werden <strong>{totalToBook} Stück</strong> für{' '}
|
||||
<strong>{countLinesToBook} Position{countLinesToBook === 1 ? '' : 'en'}</strong>{' '}
|
||||
{hasTrackedLinesToBook && selectedWarehouse ? (
|
||||
<>in das Lager <strong>„{selectedWarehouse?.name || '–'}“</strong> verbucht.</>
|
||||
) : (
|
||||
<>als Dokumentenbuchung im Ledger verbucht (ohne Lagerbestandsführung).</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
) : isWarehouseMissing ? (
|
||||
<Alert severity="warning">
|
||||
Bitte wählen Sie ein <strong>Ziellager</strong> aus, um die lagergeführten Artikel zu buchen.
|
||||
</Alert>
|
||||
) : hasLines ? (
|
||||
) : countLinesToBook === 0 && hasLines ? (
|
||||
<Alert severity="warning">
|
||||
Wählen Sie mindestens eine Menge größer als 0 aus, um Positionen zu verbuchen.
|
||||
</Alert>
|
||||
|
||||
@@ -251,18 +251,18 @@ class ManifestFormLine extends PureComponent {
|
||||
'&:hover': isClickable ? { bgcolor: 'rgba(21, 128, 61, 0.16)' } : undefined,
|
||||
}
|
||||
: isPartial
|
||||
? {
|
||||
bgcolor: '#d97706',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
'&:hover': isClickable ? { bgcolor: '#b45309' } : undefined,
|
||||
}
|
||||
: {
|
||||
bgcolor: 'primary.main',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
'&:hover': isClickable ? { bgcolor: 'primary.dark' } : undefined,
|
||||
}),
|
||||
? {
|
||||
bgcolor: '#d97706',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
'&:hover': isClickable ? { bgcolor: '#b45309' } : undefined,
|
||||
}
|
||||
: {
|
||||
bgcolor: 'primary.main',
|
||||
color: '#ffffff',
|
||||
border: 'none',
|
||||
'&:hover': isClickable ? { bgcolor: 'primary.dark' } : undefined,
|
||||
}),
|
||||
'& .MuiChip-label': {
|
||||
color: 'inherit',
|
||||
px: 0.75,
|
||||
@@ -588,10 +588,10 @@ class ManifestFormLine extends PureComponent {
|
||||
vkPrice?.net != null
|
||||
? Number(vkPrice.net)
|
||||
: cachedArticle.net != null
|
||||
? Number(cachedArticle.net)
|
||||
: cachedArticle.price != null
|
||||
? Number(cachedArticle.price)
|
||||
: null;
|
||||
? Number(cachedArticle.net)
|
||||
: cachedArticle.price != null
|
||||
? Number(cachedArticle.price)
|
||||
: null;
|
||||
const vkTaxRate =
|
||||
vkPrice?.taxRate ??
|
||||
vkPrice?.taxrate ??
|
||||
@@ -601,12 +601,12 @@ class ManifestFormLine extends PureComponent {
|
||||
19;
|
||||
const vkBrutto = vkPrice
|
||||
? priceBrutto(vkPrice) ??
|
||||
(vkNetto != null && vkTaxRate != null
|
||||
? Math.round((vkNetto * (1 + Number(vkTaxRate) / 100) + Number.EPSILON) * 100) / 100
|
||||
: null)
|
||||
(vkNetto != null && vkTaxRate != null
|
||||
? Math.round((vkNetto * (1 + Number(vkTaxRate) / 100) + Number.EPSILON) * 100) / 100
|
||||
: null)
|
||||
: vkNetto != null
|
||||
? Math.round((vkNetto * (1 + Number(vkTaxRate) / 100) + Number.EPSILON) * 100) / 100
|
||||
: null;
|
||||
? Math.round((vkNetto * (1 + Number(vkTaxRate) / 100) + Number.EPSILON) * 100) / 100
|
||||
: null;
|
||||
|
||||
const suppliers =
|
||||
Array.isArray(cachedArticle.suppliers) && cachedArticle.suppliers.length > 0
|
||||
@@ -1206,45 +1206,35 @@ class ManifestFormLine extends PureComponent {
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
||||
{this.props.kind === 'delivery' && Boolean(this.props.onBookLine) && (
|
||||
<Tooltip
|
||||
title={
|
||||
!ledgerLine
|
||||
? 'Lieferung zuerst speichern, um diese Position zu verbuchen'
|
||||
: ledgerLine.isBooked
|
||||
? 'Position bereits verbucht (Klicken zum Einsehen)'
|
||||
: `Position verbuchen (${ledgerLine.remainingAmount > 0 ? `${ledgerLine.remainingAmount} Stk.` : ''})`
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Position verbuchen"
|
||||
disabled={saving || !ledgerLine}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (ledgerLine) {
|
||||
this.props.onBookLine(ledgerLine);
|
||||
}
|
||||
}}
|
||||
color={
|
||||
!ledgerLine
|
||||
? 'default'
|
||||
: ledgerLine.isBooked
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label="Position verbuchen"
|
||||
disabled={saving || !ledgerLine}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (ledgerLine) {
|
||||
this.props.onBookLine(ledgerLine);
|
||||
}
|
||||
}}
|
||||
color={
|
||||
!ledgerLine
|
||||
? 'default'
|
||||
: ledgerLine.isBooked
|
||||
? 'success'
|
||||
: ledgerLine.bookedAmount > 0
|
||||
? 'warning'
|
||||
: 'primary'
|
||||
}
|
||||
sx={{ p: 0.35 }}
|
||||
>
|
||||
{ledgerLine?.isBooked ? (
|
||||
<CheckCircleOutlineOutlinedIcon sx={{ fontSize: 18 }} />
|
||||
) : (
|
||||
<Inventory2OutlinedIcon sx={{ fontSize: 18 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
? 'warning'
|
||||
: 'primary'
|
||||
}
|
||||
sx={{ p: 0.35 }}
|
||||
>
|
||||
{ledgerLine?.isBooked ? (
|
||||
<CheckCircleOutlineOutlinedIcon sx={{ fontSize: 18 }} />
|
||||
) : (
|
||||
<Inventory2OutlinedIcon sx={{ fontSize: 18 }} />
|
||||
)}
|
||||
</IconButton>
|
||||
</span>
|
||||
)}
|
||||
{this.props.onMove && (
|
||||
<>
|
||||
|
||||
@@ -5,7 +5,6 @@ import Button from '@mui/material/Button';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
||||
@@ -277,8 +276,8 @@ class ManifestHeader extends PureComponent {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Tile 1: 3-wide Kopfdaten Tile */}
|
||||
<PanelTile wide={3} minHeight={160} color={headerColor} title="Kopfdaten">
|
||||
{/* Tile 1: 3-wide Tile */}
|
||||
<PanelTile wide={3} minHeight={160} color={headerColor}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -410,42 +409,6 @@ class ManifestHeader extends PureComponent {
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{docId != null && kind === 'delivery' && ledgerSummary && (
|
||||
<Chip
|
||||
size="small"
|
||||
icon={
|
||||
ledgerSummary.status === 'booked' ? (
|
||||
<CheckCircleOutlineOutlinedIcon sx={{ fontSize: 16 }} />
|
||||
) : (
|
||||
<Inventory2OutlinedIcon sx={{ fontSize: 16 }} />
|
||||
)
|
||||
}
|
||||
label={
|
||||
ledgerSummary.status === 'booked'
|
||||
? `Lager: Gebucht (${ledgerSummary.bookedPositions}/${ledgerSummary.totalPositions})`
|
||||
: ledgerSummary.status === 'partial'
|
||||
? `Lager: Teilweise (${ledgerSummary.bookedPositions}/${ledgerSummary.totalPositions})`
|
||||
: ledgerSummary.status === 'empty'
|
||||
? 'Lager: Keine Pos.'
|
||||
: `Lager: Offen (${ledgerSummary.totalPositions} Pos.)`
|
||||
}
|
||||
color={
|
||||
ledgerSummary.status === 'booked'
|
||||
? 'success'
|
||||
: ledgerSummary.status === 'partial'
|
||||
? 'warning'
|
||||
: 'default'
|
||||
}
|
||||
variant="outlined"
|
||||
onClick={onOpenBookDialog}
|
||||
sx={{
|
||||
cursor: onOpenBookDialog ? 'pointer' : 'default',
|
||||
fontWeight: 600,
|
||||
height: 32,
|
||||
px: 0.5,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<AttachmentList
|
||||
kind={kind}
|
||||
|
||||
@@ -23,6 +23,7 @@ class ManifestLines extends PureComponent {
|
||||
kind = 'delivery',
|
||||
lines,
|
||||
saving,
|
||||
partyId,
|
||||
linesColor,
|
||||
dirtyLineFields = {},
|
||||
shipments,
|
||||
@@ -68,7 +69,6 @@ class ManifestLines extends PureComponent {
|
||||
wide={wide}
|
||||
minHeight={280}
|
||||
color={linesColor}
|
||||
title="Artikel"
|
||||
sx={{
|
||||
...(wide >= 4 ? { gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' } } : {}),
|
||||
...(!hasArticles
|
||||
@@ -115,17 +115,7 @@ class ManifestLines extends PureComponent {
|
||||
}
|
||||
|
||||
if (!lines || lines.length === 0) {
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length + 1}
|
||||
align="center"
|
||||
sx={{ py: 3, color: 'text.secondary', fontSize: '0.82rem' }}
|
||||
>
|
||||
Keine Positionen vorhanden
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return lines.map((line, index) => {
|
||||
@@ -190,16 +180,24 @@ class ManifestLines extends PureComponent {
|
||||
>
|
||||
Zeile
|
||||
</Button>
|
||||
{onSearchArticle && (
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<SearchIcon />}
|
||||
disabled={saving}
|
||||
onClick={() => onSearchArticle(null)}
|
||||
>
|
||||
Artikel suchen
|
||||
</Button>
|
||||
)}
|
||||
{onSearchArticle && (() => {
|
||||
const searchArticleDisabled = saving || (kind === 'delivery' && !partyId);
|
||||
const searchArticleTitle =
|
||||
kind === 'delivery' && !partyId
|
||||
? 'Bitte zuerst einen Lieferanten auswählen'
|
||||
: undefined;
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<SearchIcon />}
|
||||
disabled={searchArticleDisabled}
|
||||
onClick={() => onSearchArticle(null)}
|
||||
title={searchArticleTitle}
|
||||
>
|
||||
Artikel suchen
|
||||
</Button>
|
||||
);
|
||||
})()}
|
||||
{(() => {
|
||||
const hasShippedLines = kind === 'order' && Array.isArray(shipments) && shipments.some((sh) => (sh.lines?.length || 0) > 0);
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user