Compare commits

...

10 Commits

Author SHA1 Message Date
seb
479cac85a5 u 2026-10-18 22:01:56 +02:00
seb
0a9deac0da u 2026-10-18 21:29:13 +02:00
seb
abd40ff4fe u 2026-10-18 21:05:25 +02:00
seb
5faea634ba u 2026-10-18 21:04:18 +02:00
seb
35cfea91f8 u 2026-10-18 20:55:48 +02:00
seb
9f9310e1ff u 2026-10-18 20:54:08 +02:00
seb
c211040cd3 u 2026-10-18 20:34:17 +02:00
seb
ac9034685f u 2026-10-18 20:21:27 +02:00
seb
328ba164ec u 2026-10-18 19:55:28 +02:00
seb
180f65f4d6 u 2026-10-18 19:51:54 +02:00
22 changed files with 457 additions and 149 deletions

View File

@@ -140,11 +140,11 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
### Reusable types ### Reusable types
- `T1` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T20[], externalNumber?: str|null, createdAt?: str, shipping?: T19|null, shippings?: T19[], payment?: T30|null, payments?: T30[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T3[], files?: T9[] }` - `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T20[], externalNumber?: str|null, createdAt?: str, shipping?: T19|null, shippings?: T19[], payment?: T30|null, payments?: T30[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T3[], files?: T9[] }`
- `T2` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T27, brand: T26|null, categoryIds: int[], photos: T24[], eans: str[], prices: T35[], suppliers: T11[] }` - `T2` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T27, brand: T26|null, categoryIds: int[], photos: T24[], eans: str[], prices: T35[], suppliers: T11[] }`
- `T3` = `{ id: int, number: str, type: str, date: str, lines: T20[], shippings: T19[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T9[], state?: str }` - `T3` = `{ id: int, number: str, type: str, date: str, lines: T20[], shippings: T19[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T9[], state?: str }`
- `T4` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T20[], externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T9[] }` - `T4` = `{ id: int, number: str, type: str, date: str, lines: T20[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T9[] }`
- `T5` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T20[], files: T9[], externalNumber?: str|null, createdAt?: str, ledger?: T39 }` - `T5` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T20[], files: T9[], externalNumber?: str|null, createdAt?: str, ledger?: T39 }`
- `T6` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T7|null }` - `T6` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T7|null }`
- `T7` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T12, totalSizeBytes: int, durationMs: int }` - `T7` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T12, totalSizeBytes: int, durationMs: int }`
- `T8` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T21[] }, thumb?: str|null }` - `T8` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T21[] }, thumb?: str|null }`
@@ -534,9 +534,9 @@ _none_
#### List deliveries — `GET /delivery` #### List deliveries — `GET /delivery`
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, onlyUnbooked?: bool, supplierId?: int, seekMonths?: int`
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T39 }[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T39 }[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create delivery — `POST /delivery` #### Create delivery — `POST /delivery`
- Params: `body: { externalNumber: str, date: str, lines: T23[], supplierId?: int|null, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }` - Params: `body: { externalNumber: str, date: str, supplierId: int, lines: T23[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `201 { delivery: T5 }` - Returns: `201 { delivery: T5 }`
#### Delete delivery — `DELETE /delivery/{id}` #### Delete delivery — `DELETE /delivery/{id}`
- Params: `id: int` - Params: `id: int`
@@ -750,7 +750,7 @@ _none_
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int` - Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int`
- Returns: `200 { orders: T1[], total: int, unfilteredTotal: int, offset: int, limit: int }` - Returns: `200 { orders: T1[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create order — `POST /order` #### Create order — `POST /order`
- Params: `body: { number: str, date: str, lines: T23[], entityId?: int|null, externalNumber?: str|null }` - Params: `body: { number: str, date: str, entityId: int, lines: T23[], externalNumber?: str|null }`
- Returns: `201 { order: T1 }` - Returns: `201 { order: T1 }`
#### Delete order — `DELETE /order/{id}` #### Delete order — `DELETE /order/{id}`
- Params: `id: int` - Params: `id: int`

View File

@@ -27,12 +27,12 @@ Reference guide for the backend LLM agent when emitting route navigation actions
| **New Delivery Draft** | `/lieferung/neu` | Static (receipt & draft line editor) | `/lieferung/neu` | | **New Delivery Draft** | `/lieferung/neu` | Static (receipt & draft line editor) | `/lieferung/neu` |
| **Delivery Detail** | `/lieferung/:deliveryId` | Delivery document ID: `/lieferung/${deliveryId}` | `/lieferung/42` | | **Delivery Detail** | `/lieferung/:deliveryId` | Delivery document ID: `/lieferung/${deliveryId}` | `/lieferung/42` |
| **Suppliers Directory** | `/lieferung/lieferanten` | Static | `/lieferung/lieferanten` | | **Suppliers Directory** | `/lieferung/lieferanten` | Static | `/lieferung/lieferanten` |
| **Quote List (Angebote)** | `/auftrag` | Static | `/auftrag` | | **Order List (Auftrag / Bestellungen)** | `/auftrag` | Static (legacy `/auftrag/bestellung` redirects here) | `/auftrag` |
| **New Quote** | `/auftrag/neuesangebot` | Static draft form (Neues Angebot) | `/auftrag/neuesangebot` |
| **Quote Detail** | `/auftrag/:quoteId` | Quote document ID: `/auftrag/${quoteId}` | `/auftrag/1001` |
| **Order List (Bestellungen)**| `/auftrag/bestellung` | Static | `/auftrag/bestellung` |
| **New Order** | `/auftrag/bestellung/neu` | Static draft form | `/auftrag/bestellung/neu` | | **New Order** | `/auftrag/bestellung/neu` | Static draft form | `/auftrag/bestellung/neu` |
| **Order Detail** | `/auftrag/bestellung/:orderId` | Order document ID: `/auftrag/bestellung/${orderId}` | `/auftrag/bestellung/2005` | | **Order Detail** | `/auftrag/bestellung/:orderId` | Order document ID: `/auftrag/bestellung/${orderId}` | `/auftrag/bestellung/2005` |
| **Quote List (Angebote)** | `/auftrag/angebot` | Static | `/auftrag/angebot` |
| **New Quote** | `/auftrag/neuesangebot` | Static draft form (Neues Angebot) | `/auftrag/neuesangebot` |
| **Quote Detail** | `/auftrag/:quoteId` or `/auftrag/angebot/:quoteId` | Quote document ID: `/auftrag/${quoteId}` | `/auftrag/1001` |
| **Shipments (Ausliefern)** | `/auftrag/ausliefern` | Static (shipment manifests & parcels) | `/auftrag/ausliefern` | | **Shipments (Ausliefern)** | `/auftrag/ausliefern` | Static (shipment manifests & parcels) | `/auftrag/ausliefern` |
| **Payments (Zahlungen)** | `/auftrag/zahlungen` | Static (order payments ledger) | `/auftrag/zahlungen` | | **Payments (Zahlungen)** | `/auftrag/zahlungen` | Static (order payments ledger) | `/auftrag/zahlungen` |
| **Carriers (Versandarten)** | `/auftrag/versandarten` | Static (Versanddienstleister & Versandarten) | `/auftrag/versandarten` | | **Carriers (Versandarten)** | `/auftrag/versandarten` | Static (Versanddienstleister & Versandarten) | `/auftrag/versandarten` |
@@ -70,8 +70,8 @@ Reference guide for the backend LLM agent when emitting route navigation actions
### Quotes & Orders (`/auftrag`) ### Quotes & Orders (`/auftrag`)
- Distinction: - Distinction:
- Quotes (Angebote) live at `/auftrag` and `/auftrag/:quoteId`. - Orders (Auftrag / Bestellungen) live at `/auftrag` and `/auftrag/bestellung/:orderId`.
- Orders (Bestellungen) live at `/auftrag/bestellung` and `/auftrag/bestellung/:orderId`. - Quotes (Angebote) live at `/auftrag/angebot` and `/auftrag/:quoteId` (or `/auftrag/angebot/:quoteId`).
- Shipments (Ausliefern) live at `/auftrag/ausliefern` (connected to shipment manifests and packages). - Shipments (Ausliefern) live at `/auftrag/ausliefern` (connected to shipment manifests and packages).
- Payments (Zahlungen) live at `/auftrag/zahlungen` (payment bookings and payment balances). - Payments (Zahlungen) live at `/auftrag/zahlungen` (payment bookings and payment balances).
- Both IDs (`quoteId`, `orderId`) are document IDs. - Both IDs (`quoteId`, `orderId`) are document IDs.

View File

@@ -99,10 +99,13 @@ const routes = [
path: 'auftrag', path: 'auftrag',
element: <SectionLayout sectionPath="/auftrag" />, element: <SectionLayout sectionPath="/auftrag" />,
children: [ children: [
{ index: true, element: <AuftragAngebotPage /> }, { index: true, element: <AuftragBestellungPage /> },
{ path: 'angebot', element: <AuftragAngebotPage /> },
{ path: 'angebot/:quoteId', element: <AuftragAngebotEditPage /> },
{ path: 'neuesangebot', element: <AuftragAngebotNeuPage /> }, { path: 'neuesangebot', element: <AuftragAngebotNeuPage /> },
{ path: 'angebot/neu', element: <Navigate to="/auftrag/neuesangebot" replace /> },
{ path: 'neu', element: <Navigate to="/auftrag/neuesangebot" replace /> }, { path: 'neu', element: <Navigate to="/auftrag/neuesangebot" replace /> },
{ path: 'bestellung', element: <AuftragBestellungPage /> }, { path: 'bestellung', element: <Navigate to="/auftrag" replace /> },
{ path: 'bestellung/neu', element: <AuftragBestellungNeuPage /> }, { path: 'bestellung/neu', element: <AuftragBestellungNeuPage /> },
{ path: 'bestellung/:orderId', element: <AuftragBestellungEditPage /> }, { path: 'bestellung/:orderId', element: <AuftragBestellungEditPage /> },
{ path: 'ausliefern', element: <AuftragAusliefernPage /> }, { path: 'ausliefern', element: <AuftragAusliefernPage /> },

View File

@@ -30,7 +30,7 @@ export function createInitialManifestDraftState(kind) {
partyId: null, partyId: null,
number: '', number: '',
externalNumber: '', externalNumber: '',
lines: [emptyLine()], lines: kind === 'order' ? [] : [emptyLine()],
attachments: [], attachments: [],
}; };
} }

View File

@@ -47,7 +47,7 @@ import {
import { backendErrorFrom, isBackendFailure } from '../utils/backendError'; import { backendErrorFrom, isBackendFailure } from '../utils/backendError';
const NUMBER_SCHEMES = [ const NUMBER_SCHEMES = [
{ type: 'sku', label: 'Artikel-SKU', icon: Inventory2Icon }, { type: 'sku', label: 'Artikelnummer (ArtNr)', icon: Inventory2Icon },
{ type: 'entity', label: 'Kundennummer', icon: PeopleIcon }, { type: 'entity', label: 'Kundennummer', icon: PeopleIcon },
{ type: 'delivery', label: 'Lieferschein', icon: LocalShippingIcon }, { type: 'delivery', label: 'Lieferschein', icon: LocalShippingIcon },
{ type: 'adjustment', label: 'Bestandskorrektur', icon: TuneIcon }, { type: 'adjustment', label: 'Bestandskorrektur', icon: TuneIcon },

View File

@@ -77,14 +77,14 @@ export default function SectionLayout({ sectionPath }) {
const isDetail = const isDetail =
isArticleDetail || isKundeDetail || isLieferungDetail || isAngebotDoc; isArticleDetail || isKundeDetail || isLieferungDetail || isAngebotDoc;
const angebotLabel =
subNav?.find((item) => item.segment === '')?.title ?? subNav?.find((item) => item.segment === '')?.label ?? 'Angebot';
let pageTitle = isDetail let pageTitle = isDetail
? title ? title
: (activeSub?.title ?? activeSub?.label ?? title); : (activeSub?.title ?? activeSub?.label ?? title);
if (pathname === '/auftrag/neuesangebot' || pathname === '/auftrag/neu') pageTitle = 'Neues Angebot'; if (pathname === '/auftrag/neuesangebot' || pathname === '/auftrag/neu' || pathname === '/auftrag/angebot/neu') pageTitle = 'Neues Angebot';
else if (isAngebotDoc) pageTitle = angebotLabel; else if (pathname === '/auftrag/bestellung/neu') pageTitle = 'Neuer Auftrag';
else if (pathname === '/auftrag/bestellung/neu') pageTitle = 'Neue Bestellung'; else if (pathname.startsWith('/auftrag/bestellung/')) pageTitle = 'Bestellung';
else if (pathname.startsWith('/auftrag/angebot/')) pageTitle = 'Angebot';
else if (isAngebotDoc) pageTitle = 'Angebot';
const showsRenameSwitch = activeSub?.segment === 'kategorien'; const showsRenameSwitch = activeSub?.segment === 'kategorien';

View File

@@ -280,7 +280,7 @@ export default class ArticleImageSearchDialog extends PureComponent {
suggestions.push({ label: `${brandName} ${articleName}`, value: `${brandName} ${articleName}` }); suggestions.push({ label: `${brandName} ${articleName}`, value: `${brandName} ${articleName}` });
} }
if (sku && sku !== query && sku !== articleName) { if (sku && sku !== query && sku !== articleName) {
suggestions.push({ label: `SKU: ${sku}`, value: sku }); suggestions.push({ label: `ArtNr: ${sku}`, value: sku });
} }
if (firstEan && firstEan !== query) { if (firstEan && firstEan !== query) {
suggestions.push({ label: `EAN: ${firstEan}`, value: firstEan }); suggestions.push({ label: `EAN: ${firstEan}`, value: firstEan });

View File

@@ -147,7 +147,7 @@ export default class ArticleSupplierDialog extends PureComponent {
)} )}
<TextInput <TextInput
label="Lieferanten-Artikelnummer (SKU)" label="Lieferanten-Artikelnummer (ArtNr)"
value={sku} value={sku}
onChange={(e) => this.setState({ sku: e.target.value })} onChange={(e) => this.setState({ sku: e.target.value })}
placeholder="z. B. LF-09412" placeholder="z. B. LF-09412"

View File

@@ -130,7 +130,7 @@ export default class ArticleSearchDialog extends PureComponent {
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
{line ? ( {line ? (
<> <>
{isDelivery ? 'Lieferanten-SKU' : 'SKU'}: <strong>{rawSku || '–'}</strong> {isDelivery ? 'Lieferanten-ArtNr' : 'ArtNr'}: <strong>{rawSku || '–'}</strong>
{line?.name ? ` · ${line.name}` : ''} {line?.name ? ` · ${line.name}` : ''}
</> </>
) : isDelivery ? ( ) : isDelivery ? (

View File

@@ -505,8 +505,8 @@ export default class BookDeliveryDialog extends PureComponent {
<TableHead sx={{ backgroundColor: SURFACE_RAISED }}> <TableHead sx={{ backgroundColor: SURFACE_RAISED }}>
<TableRow> <TableRow>
<TableCell sx={{ fontWeight: 600, width: 44 }} align="center">Pos.</TableCell> <TableCell sx={{ fontWeight: 600, width: 44 }} align="center">Pos.</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Lieferanten-SKU</TableCell> <TableCell sx={{ fontWeight: 600 }}>Lieferanten-ArtNr</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Eigene SKU</TableCell> <TableCell sx={{ fontWeight: 600 }}>Eigene ArtNr</TableCell>
<TableCell sx={{ fontWeight: 600 }}>EAN</TableCell> <TableCell sx={{ fontWeight: 600 }}>EAN</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Artikel</TableCell> <TableCell sx={{ fontWeight: 600 }}>Artikel</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>Bestand</TableCell> <TableCell align="right" sx={{ fontWeight: 600 }}>Bestand</TableCell>
@@ -780,7 +780,7 @@ export default class BookDeliveryDialog extends PureComponent {
{prefillLine && ( {prefillLine && (
<Alert severity="info" sx={{ mb: 2 }}> <Alert severity="info" sx={{ mb: 2 }}>
Vorausgewählt zum Verbuchen: Position{' '} Vorausgewählt zum Verbuchen: Position{' '}
<strong>{prefillLine.sku ? `SKU ${prefillLine.sku}` : prefillLine.name}</strong> ({amountsToBook[prefillLine.id] || 0} Stk.). Andere Positionen stehen auf 0. <strong>{prefillLine.sku ? `ArtNr ${prefillLine.sku}` : prefillLine.name}</strong> ({amountsToBook[prefillLine.id] || 0} Stk.). Andere Positionen stehen auf 0.
</Alert> </Alert>
)} )}
{this.renderLinesTable()} {this.renderLinesTable()}

View File

@@ -69,13 +69,20 @@ class ManifestForm extends PureComponent {
initialDoc?.date || initialDoc?.date ||
(props.mode === 'create' ? todayInputValue() : ''); (props.mode === 'create' ? todayInputValue() : '');
if ( if (props.mode === 'create' && this.draftAutomat) {
props.mode === 'create' && if (!this.draftAutomat.state.date) {
this.draftAutomat &&
!this.draftAutomat.state.date
) {
this.draftAutomat.setState({ date: initialDate }); this.draftAutomat.setState({ date: initialDate });
} }
if (
props.kind === 'order' &&
Array.isArray(this.draftAutomat.state.lines) &&
this.draftAutomat.state.lines.length === 1 &&
!this.draftAutomat.state.lines[0]?.sku &&
!this.draftAutomat.state.lines[0]?.name
) {
this.draftAutomat.setState({ lines: [] });
}
}
this.state = { this.state = {
status: props.mode === 'edit' && !initialEdit ? 'loading' : (partiesReady ? 'ready' : 'loadingParties'), status: props.mode === 'edit' && !initialEdit ? 'loading' : (partiesReady ? 'ready' : 'loadingParties'),
@@ -86,7 +93,11 @@ class ManifestForm extends PureComponent {
partyId: initialDoc?.partyId ?? null, partyId: initialDoc?.partyId ?? null,
number: initialDoc?.number || '', number: initialDoc?.number || '',
externalNumber: initialDoc?.externalNumber || '', externalNumber: initialDoc?.externalNumber || '',
lines: Array.isArray(initialDoc?.lines) ? initialDoc.lines : (props.mode === 'create' ? [emptyLine()] : []), lines: Array.isArray(initialDoc?.lines)
? (props.kind === 'order' && props.mode === 'create' && initialDoc.lines.length === 1 && !initialDoc.lines[0]?.sku && !initialDoc.lines[0]?.name
? []
: initialDoc.lines)
: (props.mode === 'create' && props.kind !== 'order' ? [emptyLine()] : []),
attachments: initialDoc?.attachments || [], attachments: initialDoc?.attachments || [],
shipments: initialEdit?.shipments || [], shipments: initialEdit?.shipments || [],
payments: initialEdit?.payments || [], payments: initialEdit?.payments || [],
@@ -209,6 +220,7 @@ class ManifestForm extends PureComponent {
void this.loadEdit(); void this.loadEdit();
} else { } else {
void this.loadPartiesOnly(); void this.loadPartiesOnly();
void ensureDocumentNumber(this.props.kind).catch(() => {});
} }
if (this.draftAutomat) { if (this.draftAutomat) {
@@ -857,6 +869,7 @@ class ManifestForm extends PureComponent {
handleRemoveAllLines() { handleRemoveAllLines() {
this.setState((prev) => { this.setState((prev) => {
let nextLines = [];
if (this.props.kind === 'order') { if (this.props.kind === 'order') {
const shippedSkus = new Set(); const shippedSkus = new Set();
for (const sh of prev.shipments || []) { for (const sh of prev.shipments || []) {
@@ -865,7 +878,7 @@ class ManifestForm extends PureComponent {
} }
} }
if (shippedSkus.size > 0) { if (shippedSkus.size > 0) {
const remaining = prev.lines.filter((l) => { const remaining = (prev.lines || []).filter((l) => {
const raw = l.sku != null ? String(l.sku).trim().toLowerCase() : ''; const raw = l.sku != null ? String(l.sku).trim().toLowerCase() : '';
return shippedSkus.has(raw); return shippedSkus.has(raw);
}); });
@@ -874,10 +887,10 @@ class ManifestForm extends PureComponent {
return null; return null;
} }
enqueueSnackbar('Nicht ausgelieferte Positionen wurden entfernt.', { variant: 'info' }); enqueueSnackbar('Nicht ausgelieferte Positionen wurden entfernt.', { variant: 'info' });
return { lines: remaining, isDirty: true }; nextLines = remaining;
} }
} }
return { lines: [], isDirty: true }; return { lines: nextLines, isDirty: true };
}); });
} }
@@ -1170,7 +1183,7 @@ class ManifestForm extends PureComponent {
{ {
kind: cfg.kind, kind: cfg.kind,
partyField: cfg.partyField, partyField: cfg.partyField,
requireAtLeastOne: true, requireAtLeastOne: !isEdit,
externalNumber: cfg.externalNumber, externalNumber: cfg.externalNumber,
isUpdate: isEdit, isUpdate: isEdit,
mode, mode,
@@ -1323,7 +1336,7 @@ class ManifestForm extends PureComponent {
isDirty={this.state.isDirty} isDirty={this.state.isDirty}
dirtyFields={this.state.dirtyFields} dirtyFields={this.state.dirtyFields}
dirtySummary={this.state.dirtySummary} dirtySummary={this.state.dirtySummary}
onRestore={this.props.mode === 'edit' && this.state.isDirty ? this.handleRestore : undefined} onRestore={this.props.mode === 'edit' ? this.handleRestore : undefined}
onPdfUpload={kind === 'delivery' ? this.handlePdfUpload : undefined} onPdfUpload={kind === 'delivery' ? this.handlePdfUpload : undefined}
onPdfDelete={this.handleAttachmentDelete} onPdfDelete={this.handleAttachmentDelete}
onPdfCreate={kind === 'quote' || kind === 'order' ? this.handleCreatePdf : undefined} onPdfCreate={kind === 'quote' || kind === 'order' ? this.handleCreatePdf : undefined}

View File

@@ -22,6 +22,7 @@ import LinkOffOutlinedIcon from '@mui/icons-material/LinkOffOutlined';
import { getFirstVal, parseNum, LINE_COLUMNS } from './manifestConfig'; import { getFirstVal, parseNum, LINE_COLUMNS } from './manifestConfig';
import ArticlePhoto from '../article/ArticlePhoto'; import ArticlePhoto from '../article/ArticlePhoto';
import { formatPrice, grossFromNet } from '../../utils/price'; import { formatPrice, grossFromNet } from '../../utils/price';
import { formatStock } from '../article/searchFormat';
import { primaryPrice, priceBrutto } from '../../utils/articleDetail'; import { primaryPrice, priceBrutto } from '../../utils/articleDetail';
import artikelAutomat, { createArticleSelector, getArticle } from '../../automats/artikelAutomat'; import artikelAutomat, { createArticleSelector, getArticle } from '../../automats/artikelAutomat';
import { artikelDetailPath } from '../article/articleRoute'; import { artikelDetailPath } from '../article/articleRoute';
@@ -578,9 +579,40 @@ class ManifestFormLine extends PureComponent {
? Math.round((ekNetto * (1 + Number(ekTaxRate) / 100) + Number.EPSILON) * 100) / 100 ? Math.round((ekNetto * (1 + Number(ekTaxRate) / 100) + Number.EPSILON) * 100) / 100
: null; : null;
const marge =
vkNetto > 0 && ekNetto != null
? ((vkNetto - ekNetto) / vkNetto) * 100
: vkBrutto > 0 && ekBrutto != null
? ((vkBrutto - ekBrutto) / vkBrutto) * 100
: null;
const rebate = parseNum(this.props.line?.rebate) ?? 0;
const effectiveNet =
vkNetto != null && Number.isFinite(vkNetto)
? vkNetto * (1 - rebate / 100)
: null;
const effectiveMarge =
effectiveNet != null && effectiveNet > 0 && ekNetto != null
? ((effectiveNet - ekNetto) / effectiveNet) * 100
: null;
const hasPriceInfo = const hasPriceInfo =
vkNetto != null || vkBrutto != null || ekNetto != null || ekBrutto != null || kind === 'quote' || kind === 'order'; vkNetto != null || vkBrutto != null || ekNetto != null || ekBrutto != null || kind === 'quote' || kind === 'order';
const rawWarehouses = Array.isArray(cachedArticle.warehouses) ? cachedArticle.warehouses : [];
const isTracked = cachedArticle.track_stock ?? cachedArticle.trackStock ?? (rawWarehouses.length > 0);
const stockVal = isTracked === false
? null
: (cachedArticle.stock != null && !Number.isNaN(Number(cachedArticle.stock))
? Number(cachedArticle.stock)
: (rawWarehouses.length > 0 || isTracked
? rawWarehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0)
: null));
const warehouseTooltip = rawWarehouses.length > 0
? rawWarehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ')
: (isTracked === false ? 'Keine Bestandsführung' : undefined);
const lineAmount = parseNum(getFirstVal(this.props.line?.amount, this.props.line?.menge)) ?? 0;
return ( return (
<Box <Box
sx={{ sx={{
@@ -697,6 +729,55 @@ class ManifestFormLine extends PureComponent {
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}} }}
> >
{/* Bestand */}
<Tooltip
title={
warehouseTooltip
? `Lagerbestand: ${formatStock(stockVal)} (${warehouseTooltip})`
: isTracked === false
? 'Keine Bestandsführung (unbegrenzt verfügbar)'
: `Verfügbarer Lagerbestand: ${formatStock(stockVal)}`
}
>
<Typography
variant="caption"
fontWeight={600}
sx={{
color:
stockVal == null
? 'text.disabled'
: stockVal <= 0
? 'error.main'
: lineAmount > stockVal
? 'warning.main'
: 'text.secondary',
fontSize: '0.72rem',
}}
>
Bestand: {isTracked === false ? '∞' : formatStock(stockVal)}
</Typography>
</Tooltip>
{/* Marge */}
<Tooltip
title={
marge != null
? `Marge: ${marge.toFixed(1).replace('.', ',')}% (EK Netto ${formatPrice(ekNetto)} → VK Netto ${formatPrice(vkNetto)})${rebate > 0 && effectiveMarge != null ? ` · Nach ${rebate}% Rabatt: ${effectiveMarge.toFixed(1).replace('.', ',')}%` : ''}`
: 'Kein EK- oder VK-Preis hinterlegt'
}
>
<Typography
variant="caption"
fontWeight={600}
sx={{
color: marge != null ? (marge < 0 ? 'error.main' : 'text.secondary') : 'text.disabled',
fontSize: '0.72rem',
}}
>
Marge: {marge != null ? `${Math.floor(marge)}%` : '—'}
</Typography>
</Tooltip>
{/* EK Netto */} {/* EK Netto */}
<Tooltip <Tooltip
title={ title={
@@ -793,7 +874,7 @@ class ManifestFormLine extends PureComponent {
)} )}
<HelpOutlineIcon sx={{ fontSize: 15, color: 'warning.main' }} /> <HelpOutlineIcon sx={{ fontSize: 15, color: 'warning.main' }} />
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.72rem' }}> <Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.72rem' }}>
Kein Artikel für SKU &bdquo;{rawSku}&ldquo; gefunden. Kein Artikel für ArtNr &bdquo;{rawSku}&ldquo; gefunden.
</Typography> </Typography>
</Box> </Box>
); );
@@ -867,6 +948,26 @@ class ManifestFormLine extends PureComponent {
: null) : null)
: null; : null;
const priceNetNum = Number(priceNet);
const vkNettoNum = Number(vkNetto);
const margeDelivery =
vkNettoNum > 0 && priceNet != null && Number.isFinite(priceNetNum)
? ((vkNettoNum - priceNetNum) / vkNettoNum) * 100
: null;
const rawWarehouses = Array.isArray(fullArticle.warehouses) ? fullArticle.warehouses : [];
const isDeliveryTracked = fullArticle.track_stock ?? fullArticle.trackStock ?? (rawWarehouses.length > 0);
const deliveryStockVal = isDeliveryTracked === false
? null
: (fullArticle.stock != null && !Number.isNaN(Number(fullArticle.stock))
? Number(fullArticle.stock)
: (rawWarehouses.length > 0 || isDeliveryTracked
? rawWarehouses.reduce((sum, wh) => sum + (Number(wh.stock) || 0), 0)
: null));
const deliveryWarehouseTooltip = rawWarehouses.length > 0
? rawWarehouses.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock)}`).join(' · ')
: (isDeliveryTracked === false ? 'Keine Bestandsführung' : undefined);
return ( return (
<Box <Box
sx={{ sx={{
@@ -981,7 +1082,7 @@ class ManifestFormLine extends PureComponent {
{/* Supplier SKU if distinct */} {/* Supplier SKU if distinct */}
{supplierArticle.sku && supplierArticle.sku !== rawSku && supplierArticle.sku !== article.sku && ( {supplierArticle.sku && supplierArticle.sku !== rawSku && supplierArticle.sku !== article.sku && (
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}> <Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
(Lief.-SKU: <strong>{supplierArticle.sku}</strong>) (Lief.-ArtNr: <strong>{supplierArticle.sku}</strong>)
</Typography> </Typography>
)} )}
@@ -996,8 +1097,8 @@ class ManifestFormLine extends PureComponent {
</Typography> </Typography>
)} )}
{/* Stored Purchase Price (Netto), VK Netto & VK Brutto */} {/* Stored Purchase Price (Netto), VK Netto, VK Brutto, Marge & Bestand */}
{(priceNet != null || vkNetto != null || vkBrutto != null) && ( {(priceNet != null || vkNetto != null || vkBrutto != null || deliveryStockVal != null) && (
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -1007,6 +1108,51 @@ class ManifestFormLine extends PureComponent {
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}} }}
> >
{/* Bestand */}
<Tooltip
title={
deliveryWarehouseTooltip
? `Lagerbestand: ${formatStock(deliveryStockVal)} (${deliveryWarehouseTooltip})`
: isDeliveryTracked === false
? 'Keine Bestandsführung (unbegrenzt verfügbar)'
: `Aktueller Lagerbestand: ${formatStock(deliveryStockVal)}`
}
>
<Typography
variant="caption"
fontWeight={600}
sx={{
color:
deliveryStockVal == null
? 'text.disabled'
: deliveryStockVal <= 0
? 'error.main'
: 'text.secondary',
fontSize: '0.72rem',
}}
>
Bestand: {isDeliveryTracked === false ? '∞' : formatStock(deliveryStockVal)}
</Typography>
</Tooltip>
{/* Marge */}
{margeDelivery != null && (
<Tooltip
title={`Marge: ${margeDelivery.toFixed(1).replace('.', ',')}% (EK Netto ${formatPrice(priceNet)} → VK Netto ${formatPrice(vkNetto)})`}
>
<Typography
variant="caption"
fontWeight={600}
sx={{
color: margeDelivery < 0 ? 'error.main' : 'text.secondary',
fontSize: '0.72rem',
}}
>
Marge: {Math.floor(margeDelivery)}%
</Typography>
</Tooltip>
)}
{priceNet != null && ( {priceNet != null && (
<Typography <Typography
variant="caption" variant="caption"
@@ -1093,8 +1239,8 @@ class ManifestFormLine extends PureComponent {
<HelpOutlineIcon sx={{ fontSize: 15, color: 'warning.main' }} /> <HelpOutlineIcon sx={{ fontSize: 15, color: 'warning.main' }} />
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.72rem' }}> <Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.72rem' }}>
{this.props.kind === 'delivery' {this.props.kind === 'delivery'
? `Kein Lieferanten-Artikel für SKU „${rawSku}“ gefunden.` ? `Kein Lieferanten-Artikel für ArtNr „${rawSku}“ gefunden.`
: `Kein Artikel für SKU „${rawSku}“ gefunden.`} : `Kein Artikel für ArtNr „${rawSku}“ gefunden.`}
</Typography> </Typography>
</Box> </Box>
); );

View File

@@ -26,6 +26,8 @@ import { notifyError, notifyErrorMessage } from '../../utils/notify';
import entityAutomat from '../../automats/entityAutomat'; import entityAutomat from '../../automats/entityAutomat';
import customergroupAutomat from '../../automats/customergroupAutomat'; import customergroupAutomat from '../../automats/customergroupAutomat';
import supplierAutomat from '../../automats/supplierAutomat'; import supplierAutomat from '../../automats/supplierAutomat';
import numberAutomat from '../../automats/numberAutomat';
import { getNextDocumentNumber, ensureDocumentNumber } from '../../utils/documentNumber';
import { import {
SURFACE_RAISED, SURFACE_RAISED,
} from '../../colors'; } from '../../colors';
@@ -231,6 +233,40 @@ class ManifestHeader extends PureComponent {
this.props.onPdfUpload?.(file); this.props.onPdfUpload?.(file);
} }
componentDidMount() {
this._cancelled = false;
this.unsubNumber = numberAutomat.subscribe(this, () => {
this.forceUpdate();
});
void numberAutomat.actions.ensureLoaded().catch(() => {});
if (!this.props.number && this.props.docId == null) {
void ensureDocumentNumber(this.props.kind)
.then(() => {
if (!this._cancelled) this.forceUpdate();
})
.catch(() => {});
}
}
componentDidUpdate(prevProps) {
if (
!this.props.number &&
this.props.docId == null &&
(prevProps.kind !== this.props.kind || prevProps.docId !== this.props.docId)
) {
void ensureDocumentNumber(this.props.kind)
.then(() => {
if (!this._cancelled) this.forceUpdate();
})
.catch(() => {});
}
}
componentWillUnmount() {
this._cancelled = true;
this.unsubNumber?.();
}
render() { render() {
const { const {
kind, kind,
@@ -273,6 +309,9 @@ class ManifestHeader extends PureComponent {
const hasArticles = this.hasArticles(); const hasArticles = this.hasArticles();
const isEdit = docId != null; const isEdit = docId != null;
const previewNumber =
this.props.previewNumber ??
(!number && !isEdit ? getNextDocumentNumber(kind) : null);
const hasAttachments = Array.isArray(attachments) && attachments.length > 0; const hasAttachments = Array.isArray(attachments) && attachments.length > 0;
const hasParty = partyId != null && partyId !== ''; const hasParty = partyId != null && partyId !== '';
const partyValid = kind !== 'order' || hasParty; const partyValid = kind !== 'order' || hasParty;
@@ -376,8 +415,7 @@ class ManifestHeader extends PureComponent {
<TextField <TextField
{...params} {...params}
inputRef={this.partyInputRef} inputRef={this.partyInputRef}
label={dirtyFields?.partyId ? `${partyLabel} *` : partyLabel} label={partyLabel}
required={kind === 'order'}
error={isPartyEmpty} error={isPartyEmpty}
onKeyDown={this.handlePartyKeyDown} onKeyDown={this.handlePartyKeyDown}
slotProps={{ slotProps={{
@@ -401,12 +439,25 @@ class ManifestHeader extends PureComponent {
<SingleLineTextEdit <SingleLineTextEdit
size="small" size="small"
label="Nummer" label="Nummer"
value={number || ''} value={number || previewNumber || ''}
disabled disabled
placeholder="–" placeholder="–"
title={
!number && previewNumber
? 'Vorschau der nächsten Nummer (wird beim Speichern vergeben)'
: undefined
}
slotProps={{
inputLabel: { shrink: true },
}}
sx={{ sx={{
flex: { xs: '1 1 100%', sm: '1 1 120px' }, flex: { xs: '1 1 100%', sm: '1 1 120px' },
minWidth: { sm: 100 }, minWidth: { sm: 100 },
...(!number && previewNumber && {
'& .MuiInputBase-input.Mui-disabled': {
fontStyle: 'italic',
},
}),
}} }}
/> />
{useExternalNumber ? ( {useExternalNumber ? (
@@ -482,27 +533,45 @@ class ManifestHeader extends PureComponent {
> >
{saving ? 'Speichern…' : 'Speichern'} {saving ? 'Speichern…' : 'Speichern'}
</Button> </Button>
{isDirty && onRestore && ( {(() => {
const canRestore = Boolean(onRestore && isDirty && !saving && !orderCreating);
return (
<Button <Button
variant="outlined" variant="outlined"
color="warning" color={canRestore ? 'warning' : 'inherit'}
size="small" size="small"
startIcon={<RestoreIcon />} startIcon={<RestoreIcon />}
disabled={saving || orderCreating} disabled={!canRestore}
onClick={onRestore} onClick={canRestore ? onRestore : undefined}
title={
canRestore
? 'Änderungen verwerfen und ursprünglichen Stand wiederherstellen'
: 'Keine Änderungen zum Wiederherstellen'
}
sx={{ sx={{
height: 34, height: 34,
width: '100%',
fontWeight: 700, fontWeight: 700,
fontSize: '0.8rem', fontSize: '0.8rem',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
textTransform: 'none', textTransform: 'none',
borderWidth: 1.5, borderWidth: 1.5,
'&:hover': { borderWidth: 1.5 }, '&:hover': { borderWidth: 1.5 },
...(!canRestore && {
borderColor: 'action.disabled',
color: 'text.disabled',
'&.Mui-disabled': {
borderColor: 'action.disabled',
color: 'text.disabled',
borderWidth: 1.5,
},
}),
}} }}
> >
Wiederherstellen Wiederherstellen
</Button> </Button>
)} );
})()}
{docId != null && kind === 'delivery' && onOpenBookDialog && Boolean(ledgerSummary && (ledgerSummary.totalPositions > 0 || ledgerSummary.lines?.length > 0 || ledgerSummary.hasTrackStock)) && ( {docId != null && kind === 'delivery' && onOpenBookDialog && Boolean(ledgerSummary && (ledgerSummary.totalPositions > 0 || ledgerSummary.lines?.length > 0 || ledgerSummary.hasTrackStock)) && (
<Button <Button
variant={ledgerSummary?.status === 'booked' ? 'outlined' : 'contained'} variant={ledgerSummary?.status === 'booked' ? 'outlined' : 'contained'}
@@ -562,14 +631,34 @@ class ManifestHeader extends PureComponent {
{orderCreating ? 'Wird erstellt…' : 'Bestellung erstellen'} {orderCreating ? 'Wird erstellt…' : 'Bestellung erstellen'}
</Button> </Button>
)} )}
{(kind === 'quote' || kind === 'order' || kind === 'delivery') && onPdfCreate && docId != null && Boolean(number) && ( {(kind === 'quote' || kind === 'order' || kind === 'delivery') && (() => {
const canCreatePdf = Boolean(
onPdfCreate &&
docId != null &&
Boolean(number) &&
!saving &&
!pdfCreating &&
!orderCreating
);
return (
<Button <Button
type="button" type="button"
variant="outlined" variant="outlined"
color={canCreatePdf ? 'primary' : 'inherit'}
startIcon={pdfCreating ? <CircularProgress size={16} color="inherit" /> : <PictureAsPdfIcon />} startIcon={pdfCreating ? <CircularProgress size={16} color="inherit" /> : <PictureAsPdfIcon />}
disabled={saving || pdfCreating || orderCreating} disabled={!canCreatePdf}
onClick={onPdfCreate} onClick={canCreatePdf ? onPdfCreate : undefined}
title="PDF erstellen" title={
pdfCreating
? 'PDF wird erstellt…'
: canCreatePdf
? 'PDF erstellen'
: docId == null
? 'PDF kann erst nach dem Speichern erstellt werden'
: !number
? 'Dokumentnummer erforderlich für PDF'
: 'PDF erstellen'
}
sx={{ sx={{
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
height: 40, height: 40,
@@ -578,11 +667,20 @@ class ManifestHeader extends PureComponent {
boxSizing: 'border-box', boxSizing: 'border-box',
fontWeight: 600, fontWeight: 600,
fontSize: '0.875rem', fontSize: '0.875rem',
...(!canCreatePdf && {
borderColor: 'action.disabled',
color: 'text.disabled',
'&.Mui-disabled': {
borderColor: 'action.disabled',
color: 'text.disabled',
},
}),
}} }}
> >
{pdfCreating ? 'Wird erstellt…' : 'PDF erstellen'} {pdfCreating ? 'Wird erstellt…' : 'PDF erstellen'}
</Button> </Button>
)} );
})()}
{onPdfUpload ? ( {onPdfUpload ? (
<FileDropzone <FileDropzone
accept="application/pdf,.pdf,text/plain,.txt,text/csv,.csv" accept="application/pdf,.pdf,text/plain,.txt,text/csv,.csv"

View File

@@ -199,8 +199,28 @@ class ManifestLines extends PureComponent {
); );
})()} })()}
{(() => { {(() => {
const hasShippedLines = kind === 'order' && Array.isArray(shipments) && shipments.some((sh) => (sh.lines?.length || 0) > 0); const shippedSkus = new Set();
const hasLedgeredLines = kind === 'delivery' && Array.isArray(lines) && lines.some((line) => { if (kind === 'order' && Array.isArray(shipments)) {
for (const sh of shipments) {
for (const sl of sh.lines || []) {
if (sl.sku) shippedSkus.add(String(sl.sku).trim().toLowerCase());
}
}
}
const allLinesShipped =
kind === 'order' &&
shippedSkus.size > 0 &&
Array.isArray(lines) &&
lines.length > 0 &&
lines.every((l) => {
const raw = l?.sku != null ? String(l.sku).trim().toLowerCase() : '';
return raw && shippedSkus.has(raw);
});
const allLinesLedgered =
kind === 'delivery' &&
Array.isArray(lines) &&
lines.length > 0 &&
lines.every((line) => {
const ledgerLine = line?.id != null ? ledgerLinesById?.[line.id] : null; const ledgerLine = line?.id != null ? ledgerLinesById?.[line.id] : null;
return Boolean( return Boolean(
ledgerLine?.isBooked || ledgerLine?.isBooked ||
@@ -210,11 +230,11 @@ class ManifestLines extends PureComponent {
line?.is_booked line?.is_booked
); );
}); });
const disabled = saving || !lines || lines.length === 0 || hasShippedLines || hasLedgeredLines; const disabled = saving || !lines || lines.length === 0 || allLinesShipped || allLinesLedgered;
const title = hasLedgeredLines const title = allLinesLedgered
? 'Lieferung enthält bereits im Ledger verbuchte Positionen' ? 'Alle Positionen der Lieferung sind bereits im Ledger verbucht'
: hasShippedLines : allLinesShipped
? 'Auftrag enthält bereits ausgelieferte Positionen' ? 'Alle Positionen des Auftrags sind bereits ausgeliefert'
: 'Alle Zeilen entfernen'; : 'Alle Zeilen entfernen';
return ( return (
<Button <Button

View File

@@ -359,7 +359,7 @@ export default class OrderShipmentDialog extends PureComponent {
onChange={() => this.handleToggleAll(openItems)} onChange={() => this.handleToggleAll(openItems)}
/> />
</TableCell> </TableCell>
<TableCell sx={{ fontWeight: 600 }}>SKU</TableCell> <TableCell sx={{ fontWeight: 600 }}>ArtNr</TableCell>
<TableCell sx={{ fontWeight: 600 }}>Artikel</TableCell> <TableCell sx={{ fontWeight: 600 }}>Artikel</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>Bestellt</TableCell> <TableCell align="right" sx={{ fontWeight: 600 }}>Bestellt</TableCell>
<TableCell align="right" sx={{ fontWeight: 600 }}>Geliefert</TableCell> <TableCell align="right" sx={{ fontWeight: 600 }}>Geliefert</TableCell>

View File

@@ -347,7 +347,7 @@ export default class ShipmentDetailDialog extends PureComponent {
<Table size="small"> <Table size="small">
<TableHead sx={{ bgcolor: SURFACE_RAISED }}> <TableHead sx={{ bgcolor: SURFACE_RAISED }}>
<TableRow> <TableRow>
<TableCell sx={{ fontWeight: 600, py: 0.75 }}>SKU</TableCell> <TableCell sx={{ fontWeight: 600, py: 0.75 }}>ArtNr</TableCell>
<TableCell sx={{ fontWeight: 600, py: 0.75 }}>Bezeichnung</TableCell> <TableCell sx={{ fontWeight: 600, py: 0.75 }}>Bezeichnung</TableCell>
<TableCell align="right" sx={{ fontWeight: 600, py: 0.75 }}>Menge</TableCell> <TableCell align="right" sx={{ fontWeight: 600, py: 0.75 }}>Menge</TableCell>
</TableRow> </TableRow>

View File

@@ -224,7 +224,7 @@ export function buildLines(lines, { requireAtLeastOne }) {
const missing = []; const missing = [];
if (!sku && !name) { if (!sku && !name) {
missing.push('Artikel (SKU oder Name)'); missing.push('Artikel (ArtNr oder Name)');
} }
if (amount == null) { if (amount == null) {
missing.push(rawAmount != null ? 'gültige Menge' : 'Menge'); missing.push(rawAmount != null ? 'gültige Menge' : 'Menge');
@@ -555,36 +555,36 @@ export async function buildManifestBody(
export const LINE_COLUMNS = [ export const LINE_COLUMNS = [
{ id: 'pos', label: 'Pos', width: 44, align: 'center' }, { id: 'pos', label: 'Pos', width: 44, align: 'center' },
{ id: 'amount', label: 'Amount', width: 80, align: 'right', type: 'number' }, { id: 'amount', label: 'Anzahl', width: 80, align: 'right', type: 'number' },
{ id: 'sku', label: 'SKU', 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: 'tax', label: 'Tax', width: 70, align: 'right', type: 'number' }, { id: 'tax', label: 'MwSt', width: 70, align: 'right', type: 'number' },
{ id: 'rebate', label: 'Rebate', width: 75, align: 'right', type: 'number' }, { id: 'rebate', label: 'Rabatt', width: 75, align: 'right', type: 'number' },
{ id: 'net', label: 'Net', width: 95, align: 'right', type: 'number' }, { id: 'net', label: 'Netto', width: 95, align: 'right', type: 'number' },
{ id: 'gross', label: 'Gross', 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 = LINE_COLUMNS;
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: 'SKU', 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: 'Amount', width: 80, align: 'right', type: 'number' }, { id: 'amount', label: 'Anzahl', width: 80, align: 'right', type: 'number' },
{ id: 'tax', label: 'Tax', width: 70, align: 'right', type: 'number' }, { id: 'tax', label: 'MwSt', width: 70, align: 'right', type: 'number' },
{ id: 'rebate', label: 'Rebate', width: 75, align: 'right', type: 'number' }, { id: 'rebate', label: 'Rabatt', width: 75, align: 'right', type: 'number' },
{ id: 'net', label: 'Net', width: 95, align: 'right', type: 'number' }, { id: 'net', label: 'Netto', width: 95, align: 'right', type: 'number' },
{ id: 'gross', label: 'Gross', width: 95, align: 'right', type: 'number' }, { id: 'gross', label: 'Brutto', width: 95, align: 'right', type: 'number' },
]; ];
export const DELIVERY_LINE_COLUMNS = [ export const DELIVERY_LINE_COLUMNS = [
{ id: 'pos', label: 'Pos', width: 44, align: 'center' }, { id: 'pos', label: 'Pos', width: 44, align: 'center' },
{ id: 'sku', label: 'SKU', width: 130, align: 'left', type: 'text' }, { id: 'sku', label: 'ArtNr', width: 130, align: 'left', type: 'text' },
{ id: 'name', label: 'Name', width: 220, align: 'left', type: 'text' }, { id: 'name', label: 'Name', width: 220, align: 'left', type: 'text' },
{ id: 'amount', label: 'Amount', width: 88, align: 'right', type: 'number' }, { id: 'amount', label: 'Anzahl', width: 88, align: 'right', type: 'number' },
{ id: 'pu', label: 'PU', width: 80, align: 'right', type: 'number' }, { id: 'pu', label: 'PU', width: 80, align: 'right', type: 'number' },
{ id: 'taxrate', label: 'Tax', width: 80, align: 'right', type: 'number' }, { id: 'taxrate', label: 'MwSt', width: 80, align: 'right', type: 'number' },
{ id: 'netprice', label: 'Net', width: 104, align: 'right', type: 'number' }, { id: 'netprice', label: 'Netto', width: 104, align: 'right', type: 'number' },
]; ];
/** @typedef {'delivery'|'quote'|'order'} ManifestKind */ /** @typedef {'delivery'|'quote'|'order'} ManifestKind */
@@ -687,7 +687,7 @@ export function manifestConfig(cfgOrKind) {
listField: 'quotes', listField: 'quotes',
docField: 'quote', docField: 'quote',
partyField: 'entityId', partyField: 'entityId',
listPath: '/auftrag', listPath: '/auftrag/angebot',
createPath: '/auftrag/neuesangebot', createPath: '/auftrag/neuesangebot',
editPath: (id) => `/auftrag/${id}`, editPath: (id) => `/auftrag/${id}`,
dateLabel: 'Gültig bis', dateLabel: 'Gültig bis',
@@ -720,13 +720,13 @@ export function manifestConfig(cfgOrKind) {
listField: 'orders', listField: 'orders',
docField: 'order', docField: 'order',
partyField: 'entityId', partyField: 'entityId',
listPath: '/auftrag/bestellung', listPath: '/auftrag',
createPath: '/auftrag/bestellung/neu', createPath: '/auftrag/bestellung/neu',
editPath: (id) => `/auftrag/bestellung/${id}`, editPath: (id) => `/auftrag/bestellung/${id}`,
dateLabel: 'Lieferdatum', dateLabel: 'Lieferdatum',
partyLabel: 'Kunde', partyLabel: 'Kunde',
emptyList: 'Noch keine Bestellungen.', emptyList: 'Noch keine Bestellungen.',
createButton: 'Neue Bestellung', createButton: 'Neuer Auftrag',
createdMsg: 'Bestellung angelegt.', createdMsg: 'Bestellung angelegt.',
savedMsg: 'Bestellung gespeichert.', savedMsg: 'Bestellung gespeichert.',
deletedMsg: 'Bestellung gelöscht.', deletedMsg: 'Bestellung gelöscht.',

View File

@@ -55,8 +55,8 @@ function getInitialTarget() {
const p = window.location.pathname; const p = window.location.pathname;
if (p.startsWith('/lieferung')) return 'delivery'; if (p.startsWith('/lieferung')) return 'delivery';
if (p.startsWith('/auftrag/ausliefern')) return 'shipment'; if (p.startsWith('/auftrag/ausliefern')) return 'shipment';
if (p.startsWith('/auftrag/bestellung')) return 'order'; if (p.startsWith('/auftrag/angebot')) return 'quote';
if (p.startsWith('/auftrag')) return 'quote'; if (p.startsWith('/auftrag')) return 'order';
} }
return 'order'; return 'order';
} }

View File

@@ -202,18 +202,18 @@ export const SUB_NAV = {
'/auftrag': [ '/auftrag': [
{ {
segment: '', segment: '',
label: 'Auftrag',
title: 'Auftrag',
icon: ShoppingCartIcon,
help: 'Bestellungen prüfen, freigeben und kommissionieren',
},
{
segment: 'angebot',
label: 'Angebot', label: 'Angebot',
title: 'Angebot', title: 'Angebot',
icon: RequestQuoteIcon, icon: RequestQuoteIcon,
help: 'Angebote für Kunden kalkulieren und versenden', help: 'Angebote für Kunden kalkulieren und versenden',
}, },
{
segment: 'bestellung',
label: 'Bestellung',
title: 'Bestellung',
icon: ShoppingCartIcon,
help: 'Bestellungen prüfen, freigeben und kommissionieren',
},
{ {
segment: 'ausliefern', segment: 'ausliefern',
label: 'Ausliefern', label: 'Ausliefern',
@@ -336,11 +336,16 @@ export function resolveActiveSub(pathname, sectionPath) {
return null; return null;
} }
// Keep Bestellung active for nested paths (e.g. /auftrag/bestellung/neu). // Keep sub active for nested paths (e.g. /auftrag/ausliefern, /auftrag/angebot).
if (sectionPath === '/auftrag' && rest && knownSegments.has(rest)) { if (sectionPath === '/auftrag' && rest && knownSegments.has(rest)) {
return rest; return rest;
} }
// Treat /auftrag/bestellung as segment ''
if (sectionPath === '/auftrag' && rest === 'bestellung') {
return '';
}
// Treat /infos/articlehistory as segment '' // Treat /infos/articlehistory as segment ''
if (sectionPath === '/infos' && rest === 'articlehistory') { if (sectionPath === '/infos' && rest === 'articlehistory') {
return ''; return '';

View File

@@ -635,7 +635,7 @@ class ArtikelSearchPage extends Component {
sx={{ maxWidth: 520, flexGrow: 1 }} sx={{ maxWidth: 520, flexGrow: 1 }}
autoFocus autoFocus
autoComplete="off" autoComplete="off"
placeholder="(EAN, SKU, Name)" placeholder="(EAN, ArtNr, Name)"
value={input} value={input}
onChange={this.handleInputChange} onChange={this.handleInputChange}
slotProps={{ slotProps={{

View File

@@ -160,6 +160,7 @@ class LieferungNeuPage extends PureComponent {
this.handleRemoveLine = this.handleRemoveLine.bind(this); this.handleRemoveLine = this.handleRemoveLine.bind(this);
this.handleMoveLine = this.handleMoveLine.bind(this); this.handleMoveLine = this.handleMoveLine.bind(this);
this.handleRemoveAllLines = this.handleRemoveAllLines.bind(this); this.handleRemoveAllLines = this.handleRemoveAllLines.bind(this);
this.handleRestore = this.handleRestore.bind(this);
this.handleSave = this.handleSave.bind(this); this.handleSave = this.handleSave.bind(this);
this.handlePdfUpload = this.handlePdfUpload.bind(this); this.handlePdfUpload = this.handlePdfUpload.bind(this);
this.handlePdfRescan = this.handlePdfRescan.bind(this); this.handlePdfRescan = this.handlePdfRescan.bind(this);
@@ -527,7 +528,7 @@ class LieferungNeuPage extends PureComponent {
const internalSku = article?.sku; const internalSku = article?.sku;
if (!internalSku) { if (!internalSku) {
const msg = 'Artikel hat keine SKU.'; const msg = 'Artikel hat keine ArtNr.';
enqueueSnackbar(msg, { variant: 'error' }); enqueueSnackbar(msg, { variant: 'error' });
throw new Error(msg); throw new Error(msg);
} }
@@ -686,6 +687,24 @@ class LieferungNeuPage extends PureComponent {
handleRemoveAllLines() { handleRemoveAllLines() {
this.automat.actions.clearLineitems(); this.automat.actions.clearLineitems();
this.setState({ lines: [] });
}
handleRestore() {
if (!this._initialDoc) return;
const base = this._initialDoc;
this.automat.actions.setDate(base.date);
if (base.partyId != null) {
this.automat.actions.setDeliveryEntity(base.partyId);
}
this.automat.actions.setExternalNumber(base.externalNumber || '');
const restoredLines = (base.lines || []).map((l) => ({ ...l }));
this.automat.actions.setLineitems(restoredLines);
this.automat.actions.setAttachments([...(base.attachments || [])]);
this.setState({
lines: restoredLines,
});
enqueueSnackbar('Änderungen verworfen und ursprünglicher Stand wiederhergestellt.', { variant: 'info' });
} }
async handlePdfUpload(file) { async handlePdfUpload(file) {
@@ -1331,6 +1350,7 @@ class LieferungNeuPage extends PureComponent {
onNumberChange={this.handleNumberChange} onNumberChange={this.handleNumberChange}
onExternalNumberChange={this.handleExternalNumberChange} onExternalNumberChange={this.handleExternalNumberChange}
docId={this.props.docId} docId={this.props.docId}
onRestore={isEdit ? this.handleRestore : undefined}
ledgerSummary={this.state.ledgerSummary} ledgerSummary={this.state.ledgerSummary}
onOpenBookDialog={this.handleOpenBookDialog} onOpenBookDialog={this.handleOpenBookDialog}
/> />
@@ -1497,8 +1517,8 @@ class LieferungNeuPage extends PureComponent {
<TableHead sx={{ bgcolor: SURFACE_RAISED }}> <TableHead sx={{ bgcolor: SURFACE_RAISED }}>
<TableRow> <TableRow>
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Zeitpunkt</TableCell> <TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Zeitpunkt</TableCell>
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lieferanten SKU</TableCell> <TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lieferanten-ArtNr</TableCell>
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Eigene SKU</TableCell> <TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Eigene ArtNr</TableCell>
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Name</TableCell> <TableCell sx={{ fontWeight: 600, py: 0.5 }}>Name</TableCell>
<TableCell align="right" sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Menge geliefert</TableCell> <TableCell align="right" sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Menge geliefert</TableCell>
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lager</TableCell> <TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lager</TableCell>

View File

@@ -1,6 +1,7 @@
import numberAutomat from '../automats/numberAutomat'; import numberAutomat from '../automats/numberAutomat';
import quoteAutomat from '../automats/quoteAutomat'; import quoteAutomat from '../automats/quoteAutomat';
import orderAutomat from '../automats/orderAutomat'; import orderAutomat from '../automats/orderAutomat';
import deliveryAutomat from '../automats/deliveryAutomat';
/** /**
* Formats a number pattern given prefix, padding width, and counter. * Formats a number pattern given prefix, padding width, and counter.
@@ -18,9 +19,9 @@ export function formatNumberPattern(prefix, padding, next) {
} }
/** /**
* Returns the next document number for quote or order based on company sequences. * Returns the next document number for quote, order, or delivery based on company sequences.
* *
* @param {'quote'|'order'|string} kind * @param {'quote'|'order'|'delivery'|string} kind
* @returns {string} * @returns {string}
*/ */
export function getNextDocumentNumber(kind) { export function getNextDocumentNumber(kind) {
@@ -42,7 +43,9 @@ export function getNextDocumentNumber(kind) {
let prefix = ''; let prefix = '';
let startNum = 1; let startNum = 1;
if ( if (kind === 'delivery') {
prefix = prefixStyle.startsWith('DEL-') ? 'DEL-' : (prefixStyle.startsWith('L-') ? 'L-' : 'LS-');
} else if (
prefixStyle.startsWith('ART-') || prefixStyle.startsWith('ART-') ||
prefixStyle.startsWith('LS-') || prefixStyle.startsWith('LS-') ||
prefixStyle.startsWith('KD-') || prefixStyle.startsWith('KD-') ||
@@ -69,8 +72,8 @@ export function getNextDocumentNumber(kind) {
prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : ''); prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : '');
} }
// Check existing docs from quoteAutomat / orderAutomat to avoid collisions // Check existing docs from quoteAutomat / orderAutomat / deliveryAutomat to avoid collisions
const listAutomat = kind === 'quote' ? quoteAutomat : (kind === 'order' ? orderAutomat : null); const listAutomat = kind === 'quote' ? quoteAutomat : (kind === 'order' ? orderAutomat : (kind === 'delivery' ? deliveryAutomat : null));
const existingDocs = Array.isArray(listAutomat?.state?.items) ? listAutomat.state.items : []; const existingDocs = Array.isArray(listAutomat?.state?.items) ? listAutomat.state.items : [];
let maxFound = startNum - 1; let maxFound = startNum - 1;
@@ -99,11 +102,11 @@ export function getNextDocumentNumber(kind) {
/** /**
* Asynchronously ensures required stores are loaded, then returns the next document number. * Asynchronously ensures required stores are loaded, then returns the next document number.
* *
* @param {'quote'|'order'|string} kind * @param {'quote'|'order'|'delivery'|string} kind
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
export async function ensureDocumentNumber(kind) { export async function ensureDocumentNumber(kind) {
const listAutomat = kind === 'quote' ? quoteAutomat : (kind === 'order' ? orderAutomat : null); const listAutomat = kind === 'quote' ? quoteAutomat : (kind === 'order' ? orderAutomat : (kind === 'delivery' ? deliveryAutomat : null));
await Promise.all([ await Promise.all([
numberAutomat.actions.ensureLoaded().catch(() => {}), numberAutomat.actions.ensureLoaded().catch(() => {}),
listAutomat?.actions?.ensureLoaded ? listAutomat.actions.ensureLoaded().catch(() => {}) : Promise.resolve(), listAutomat?.actions?.ensureLoaded ? listAutomat.actions.ensureLoaded().catch(() => {}) : Promise.resolve(),