This commit is contained in:
seb
2026-10-23 20:31:34 +02:00
parent 57347bdef9
commit b9bd0380cb
7 changed files with 533 additions and 78 deletions

View File

@@ -21,6 +21,10 @@ import customergroupAutomat from '../../automats/customergroupAutomat';
import supplierAutomat from '../../automats/supplierAutomat';
import orderAutomat from '../../automats/orderAutomat';
import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat';
import warehouseAutomat from '../../automats/warehouseAutomat';
import sseAutomat from '../../automats/sseAutomat';
import OrderLedgerPanel from './OrderLedgerPanel';
import { fetchOrderLedgerEntries } from '../../utils/orderLedger';
import { netFromGross, grossFromNet, getArticleGross } from '../../utils/price';
import { resolveSalesPrice } from '../../utils/articleDetail';
import {
@@ -102,6 +106,7 @@ class ManifestForm extends PureComponent {
shipments: initialEdit?.shipments || [],
payments: initialEdit?.payments || [],
shippings: initialEdit?.shippings || [],
ledgerEntries: [],
orderTotal: initialEdit?.orderTotal ?? null,
paidTotal: initialEdit?.paidTotal ?? null,
paid: Boolean(initialEdit?.paid),
@@ -217,8 +222,20 @@ class ManifestForm extends PureComponent {
}
});
if (warehouseAutomat.state.status === 'idle') {
void warehouseAutomat.actions.load();
}
if (this.props.mode === 'edit') {
void this.loadEdit();
if (this.props.kind === 'order') {
this.unsubSseLedger = sseAutomat?.actions?.on?.('ledger', () => {
void this.reloadLedger();
});
this.unsubSseStock = sseAutomat?.actions?.on?.('stock', () => {
void this.reloadLedger();
});
}
} else {
void this.loadPartiesOnly();
void ensureDocumentNumber(this.props.kind).catch(() => {});
@@ -359,12 +376,28 @@ class ManifestForm extends PureComponent {
this.unsubParties?.();
this.unsubGroups?.();
this.unsubCompany?.();
this.unsubSseLedger?.();
this.unsubSseStock?.();
if (this._parseAbortController) {
this._parseAbortController.abort();
this._parseAbortController = null;
}
}
async reloadLedger() {
if (this.props.kind !== 'order' || this.props.mode !== 'edit' || !this.props.docId) return;
try {
const ledgerEntries = await fetchOrderLedgerEntries(this.props.docId, {
shipments: this.state.shipments,
lines: this.state.lines,
});
if (this._cancelled) return;
this.setState({ ledgerEntries });
} catch (err) {
console.warn('[ManifestForm] Failed to reload ledger entries:', err);
}
}
async loadPartiesOnly() {
if (this.state.parties.length === 0) {
this.setState({ status: 'loadingParties', error: null, partiesLoading: true });
@@ -464,6 +497,18 @@ class ManifestForm extends PureComponent {
}
}
let ledgerEntries = [];
if (kind === 'order') {
try {
ledgerEntries = await fetchOrderLedgerEntries(docId, {
shipments,
lines: doc.lines,
});
} catch (lErr) {
console.warn('[ManifestForm] Failed to load ledger entries:', lErr);
}
}
const loadedDoc = {
date: dateInputValue(doc.date),
partyId: doc[cfg.partyField],
@@ -492,6 +537,7 @@ class ManifestForm extends PureComponent {
parties,
partiesLoading: false,
...loadedDoc,
ledgerEntries,
isDirty: false,
dirtyFields: null,
dirtyLineKeys: [],
@@ -1409,6 +1455,9 @@ class ManifestForm extends PureComponent {
navigate={this.props.navigate}
/>
) : null}
{status === 'ready' && kind === 'order' && this.props.mode === 'edit' ? (
<OrderLedgerPanel entries={this.state.ledgerEntries} />
) : null}
</PanelTileGrid>
<ManifestPdfDialog
open={pdfDialogOpen}

View File

@@ -0,0 +1,96 @@
import { PureComponent } from 'react';
import Paper from '@mui/material/Paper';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import { PanelTile } from 'dashgui/panels';
import { SURFACE_BASE, SURFACE_RAISED } from '../../colors';
export default class OrderLedgerPanel extends PureComponent {
render() {
const { entries = [] } = this.props;
if (!Array.isArray(entries) || entries.length === 0) {
return null;
}
return (
<PanelTile
wide={4}
color={SURFACE_BASE}
sx={{
gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' },
}}
>
<TableContainer component={Paper} variant="outlined">
<Table size="small">
<TableHead sx={{ bgcolor: SURFACE_RAISED }}>
<TableRow>
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Zeitpunkt</TableCell>
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>ArtNr</TableCell>
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Name</TableCell>
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lieferschein / Beleg</TableCell>
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lager</TableCell>
<TableCell align="right" sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Menge verbucht</TableCell>
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Referenz</TableCell>
</TableRow>
</TableHead>
<TableBody>
{entries.map((entry) => {
const isNegative = entry.rawAmount != null && entry.rawAmount < 0;
return (
<TableRow key={entry.id} hover>
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
<Typography variant="caption">
{entry.created_at ? new Date(entry.created_at).toLocaleString('de-DE') : '–'}
</Typography>
</TableCell>
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 600 }}>
{entry.sku}
</Typography>
</TableCell>
<TableCell sx={{ py: 0.5 }}>
<Typography variant="caption">
{entry.name}
</Typography>
</TableCell>
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
<Typography variant="caption">
{entry.documentNumber || '–'}
</Typography>
</TableCell>
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
<Typography variant="caption">
{entry.warehouseName}
</Typography>
</TableCell>
<TableCell
align="right"
sx={{
py: 0.5,
fontWeight: 700,
color: isNegative ? 'error.main' : 'success.main',
whiteSpace: 'nowrap',
}}
>
{entry.mengeVerbucht}
</TableCell>
<TableCell sx={{ py: 0.5 }}>
<Typography variant="caption">
{entry.transaction_text || '–'}
</Typography>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
</PanelTile>
);
}
}

View File

@@ -328,7 +328,7 @@ export default class ShipmentDetailDialog extends PureComponent {
this.setState((prev) => ({
shipment: { ...prev.shipment, state: newState },
}));
const label = newState === 'delivered' ? 'Versendet' : newState === 'returned' ? 'Retoure' : 'Neu';
const label = newState === 'delivered' ? 'Versendet' : 'Neu';
enqueueSnackbar(`Lieferstatus geändert: ${label}`, { variant: 'success' });
await Promise.all([
orderAutomat.actions.reload(),
@@ -631,20 +631,9 @@ export default class ShipmentDetailDialog extends PureComponent {
</Button>
)}
{shipment.state === 'delivered' && (
<Button
variant="contained"
color="warning"
disabled={busyAction}
onClick={() => this.handleStateChange('returned')}
sx={{ textTransform: 'none', fontWeight: 700 }}
>
{busyAction ? 'Wird gebucht…' : 'Retoure'}
</Button>
)}
{shipment.state === 'returned' && (
<Chip
label="Retoure"
color="error"
label="Versendet"
color="success"
variant="outlined"
sx={{ fontWeight: 700, height: 28, fontSize: '0.8rem' }}
/>

View File

@@ -424,12 +424,6 @@ class DeliveryStateMenu extends PureComponent {
>
Geliefert (Zugestellt)
</MenuItem>
<MenuItem
selected={currentState === 'returned'}
onClick={() => this.handleSelectState('returned')}
>
Retoure (Retourniert)
</MenuItem>
</Menu>
</Box>
);
@@ -824,14 +818,6 @@ class ShipmentSearchField extends PureComponent {
onClick={() => shipmentAutomat.actions.setStateFilter('delivered')}
sx={{ fontWeight: 600, cursor: 'pointer', height: 26 }}
/>
<Chip
size="small"
label="Retoure"
color={stateFilter === 'returned' ? 'error' : 'default'}
variant={stateFilter === 'returned' ? 'filled' : 'outlined'}
onClick={() => shipmentAutomat.actions.setStateFilter('returned')}
sx={{ fontWeight: 600, cursor: 'pointer', height: 26 }}
/>
</Box>
{/* Right: Negativeseek controls (-1 Month, -1 Year, Reset) */}

View File

@@ -18,6 +18,7 @@ export { default as BookDeliveryDialog } from './BookDeliveryDialog';
export { default as OrderShipmentDialog } from './OrderShipmentDialog';
export { default as ShipmentDetailDialog } from './ShipmentDetailDialog';
export { default as OrderDocumentsTiles } from './OrderDocumentsTiles';
export { default as OrderLedgerPanel } from './OrderLedgerPanel';
export { default as PartyFilterPanel, PartyFilterButton, PartyFilterChip } from './PartyFilterPanel';
export { withManifestId } from './withManifestId';
export {