Compare commits
10 Commits
94f385e753
...
479cac85a5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
479cac85a5 | ||
|
|
0a9deac0da | ||
|
|
abd40ff4fe | ||
|
|
5faea634ba | ||
|
|
35cfea91f8 | ||
|
|
9f9310e1ff | ||
|
|
c211040cd3 | ||
|
|
ac9034685f | ||
|
|
328ba164ec | ||
|
|
180f65f4d6 |
@@ -140,11 +140,11 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
|
||||
|
||||
### 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[] }`
|
||||
- `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[] }`
|
||||
- `T5` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T20[], files: T9[], externalNumber?: str|null, createdAt?: str, ledger?: T39 }`
|
||||
- `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, 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 }`
|
||||
- `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 }`
|
||||
@@ -534,9 +534,9 @@ _none_
|
||||
|
||||
#### List deliveries — `GET /delivery`
|
||||
- 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`
|
||||
- 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 }`
|
||||
#### Delete delivery — `DELETE /delivery/{id}`
|
||||
- Params: `id: int`
|
||||
@@ -750,7 +750,7 @@ _none_
|
||||
- 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 }`
|
||||
#### 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 }`
|
||||
#### Delete order — `DELETE /order/{id}`
|
||||
- Params: `id: int`
|
||||
|
||||
@@ -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` |
|
||||
| **Delivery Detail** | `/lieferung/:deliveryId` | Delivery document ID: `/lieferung/${deliveryId}` | `/lieferung/42` |
|
||||
| **Suppliers Directory** | `/lieferung/lieferanten` | Static | `/lieferung/lieferanten` |
|
||||
| **Quote List (Angebote)** | `/auftrag` | Static | `/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` |
|
||||
| **Order List (Auftrag / Bestellungen)** | `/auftrag` | Static (legacy `/auftrag/bestellung` redirects here) | `/auftrag` |
|
||||
| **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` |
|
||||
| **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` |
|
||||
| **Payments (Zahlungen)** | `/auftrag/zahlungen` | Static (order payments ledger) | `/auftrag/zahlungen` |
|
||||
| **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`)
|
||||
- Distinction:
|
||||
- Quotes (Angebote) live at `/auftrag` and `/auftrag/:quoteId`.
|
||||
- Orders (Bestellungen) live at `/auftrag/bestellung` and `/auftrag/bestellung/:orderId`.
|
||||
- Orders (Auftrag / Bestellungen) live at `/auftrag` 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).
|
||||
- Payments (Zahlungen) live at `/auftrag/zahlungen` (payment bookings and payment balances).
|
||||
- Both IDs (`quoteId`, `orderId`) are document IDs.
|
||||
|
||||
@@ -99,10 +99,13 @@ const routes = [
|
||||
path: 'auftrag',
|
||||
element: <SectionLayout sectionPath="/auftrag" />,
|
||||
children: [
|
||||
{ index: true, element: <AuftragAngebotPage /> },
|
||||
{ index: true, element: <AuftragBestellungPage /> },
|
||||
{ path: 'angebot', element: <AuftragAngebotPage /> },
|
||||
{ path: 'angebot/:quoteId', element: <AuftragAngebotEditPage /> },
|
||||
{ path: 'neuesangebot', element: <AuftragAngebotNeuPage /> },
|
||||
{ path: 'angebot/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/:orderId', element: <AuftragBestellungEditPage /> },
|
||||
{ path: 'ausliefern', element: <AuftragAusliefernPage /> },
|
||||
|
||||
@@ -30,7 +30,7 @@ export function createInitialManifestDraftState(kind) {
|
||||
partyId: null,
|
||||
number: '',
|
||||
externalNumber: '',
|
||||
lines: [emptyLine()],
|
||||
lines: kind === 'order' ? [] : [emptyLine()],
|
||||
attachments: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ import {
|
||||
import { backendErrorFrom, isBackendFailure } from '../utils/backendError';
|
||||
|
||||
const NUMBER_SCHEMES = [
|
||||
{ type: 'sku', label: 'Artikel-SKU', icon: Inventory2Icon },
|
||||
{ type: 'sku', label: 'Artikelnummer (ArtNr)', icon: Inventory2Icon },
|
||||
{ type: 'entity', label: 'Kundennummer', icon: PeopleIcon },
|
||||
{ type: 'delivery', label: 'Lieferschein', icon: LocalShippingIcon },
|
||||
{ type: 'adjustment', label: 'Bestandskorrektur', icon: TuneIcon },
|
||||
|
||||
@@ -77,14 +77,14 @@ export default function SectionLayout({ sectionPath }) {
|
||||
const isDetail =
|
||||
isArticleDetail || isKundeDetail || isLieferungDetail || isAngebotDoc;
|
||||
|
||||
const angebotLabel =
|
||||
subNav?.find((item) => item.segment === '')?.title ?? subNav?.find((item) => item.segment === '')?.label ?? 'Angebot';
|
||||
let pageTitle = isDetail
|
||||
? title
|
||||
: (activeSub?.title ?? activeSub?.label ?? title);
|
||||
if (pathname === '/auftrag/neuesangebot' || pathname === '/auftrag/neu') pageTitle = 'Neues Angebot';
|
||||
else if (isAngebotDoc) pageTitle = angebotLabel;
|
||||
else if (pathname === '/auftrag/bestellung/neu') pageTitle = 'Neue Bestellung';
|
||||
if (pathname === '/auftrag/neuesangebot' || pathname === '/auftrag/neu' || pathname === '/auftrag/angebot/neu') pageTitle = 'Neues Angebot';
|
||||
else if (pathname === '/auftrag/bestellung/neu') pageTitle = 'Neuer Auftrag';
|
||||
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';
|
||||
|
||||
|
||||
@@ -280,7 +280,7 @@ export default class ArticleImageSearchDialog extends PureComponent {
|
||||
suggestions.push({ label: `${brandName} ${articleName}`, value: `${brandName} ${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) {
|
||||
suggestions.push({ label: `EAN: ${firstEan}`, value: firstEan });
|
||||
|
||||
@@ -147,7 +147,7 @@ export default class ArticleSupplierDialog extends PureComponent {
|
||||
)}
|
||||
|
||||
<TextInput
|
||||
label="Lieferanten-Artikelnummer (SKU)"
|
||||
label="Lieferanten-Artikelnummer (ArtNr)"
|
||||
value={sku}
|
||||
onChange={(e) => this.setState({ sku: e.target.value })}
|
||||
placeholder="z. B. LF-09412"
|
||||
|
||||
@@ -130,7 +130,7 @@ export default class ArticleSearchDialog extends PureComponent {
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{line ? (
|
||||
<>
|
||||
{isDelivery ? 'Lieferanten-SKU' : 'SKU'}: <strong>{rawSku || '–'}</strong>
|
||||
{isDelivery ? 'Lieferanten-ArtNr' : 'ArtNr'}: <strong>{rawSku || '–'}</strong>
|
||||
{line?.name ? ` · ${line.name}` : ''}
|
||||
</>
|
||||
) : isDelivery ? (
|
||||
|
||||
@@ -505,8 +505,8 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
<TableHead sx={{ backgroundColor: SURFACE_RAISED }}>
|
||||
<TableRow>
|
||||
<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 }}>Lieferanten-ArtNr</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Eigene ArtNr</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>EAN</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Artikel</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>Bestand</TableCell>
|
||||
@@ -780,7 +780,7 @@ export default class BookDeliveryDialog extends PureComponent {
|
||||
{prefillLine && (
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
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>
|
||||
)}
|
||||
{this.renderLinesTable()}
|
||||
|
||||
@@ -69,12 +69,19 @@ class ManifestForm extends PureComponent {
|
||||
initialDoc?.date ||
|
||||
(props.mode === 'create' ? todayInputValue() : '');
|
||||
|
||||
if (
|
||||
props.mode === 'create' &&
|
||||
this.draftAutomat &&
|
||||
!this.draftAutomat.state.date
|
||||
) {
|
||||
this.draftAutomat.setState({ date: initialDate });
|
||||
if (props.mode === 'create' && this.draftAutomat) {
|
||||
if (!this.draftAutomat.state.date) {
|
||||
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 = {
|
||||
@@ -86,7 +93,11 @@ class ManifestForm extends PureComponent {
|
||||
partyId: initialDoc?.partyId ?? null,
|
||||
number: initialDoc?.number || '',
|
||||
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 || [],
|
||||
shipments: initialEdit?.shipments || [],
|
||||
payments: initialEdit?.payments || [],
|
||||
@@ -209,6 +220,7 @@ class ManifestForm extends PureComponent {
|
||||
void this.loadEdit();
|
||||
} else {
|
||||
void this.loadPartiesOnly();
|
||||
void ensureDocumentNumber(this.props.kind).catch(() => {});
|
||||
}
|
||||
|
||||
if (this.draftAutomat) {
|
||||
@@ -857,6 +869,7 @@ class ManifestForm extends PureComponent {
|
||||
|
||||
handleRemoveAllLines() {
|
||||
this.setState((prev) => {
|
||||
let nextLines = [];
|
||||
if (this.props.kind === 'order') {
|
||||
const shippedSkus = new Set();
|
||||
for (const sh of prev.shipments || []) {
|
||||
@@ -865,7 +878,7 @@ class ManifestForm extends PureComponent {
|
||||
}
|
||||
}
|
||||
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() : '';
|
||||
return shippedSkus.has(raw);
|
||||
});
|
||||
@@ -874,10 +887,10 @@ class ManifestForm extends PureComponent {
|
||||
return null;
|
||||
}
|
||||
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,
|
||||
partyField: cfg.partyField,
|
||||
requireAtLeastOne: true,
|
||||
requireAtLeastOne: !isEdit,
|
||||
externalNumber: cfg.externalNumber,
|
||||
isUpdate: isEdit,
|
||||
mode,
|
||||
@@ -1323,7 +1336,7 @@ class ManifestForm extends PureComponent {
|
||||
isDirty={this.state.isDirty}
|
||||
dirtyFields={this.state.dirtyFields}
|
||||
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}
|
||||
onPdfDelete={this.handleAttachmentDelete}
|
||||
onPdfCreate={kind === 'quote' || kind === 'order' ? this.handleCreatePdf : undefined}
|
||||
|
||||
@@ -22,6 +22,7 @@ import LinkOffOutlinedIcon from '@mui/icons-material/LinkOffOutlined';
|
||||
import { getFirstVal, parseNum, LINE_COLUMNS } from './manifestConfig';
|
||||
import ArticlePhoto from '../article/ArticlePhoto';
|
||||
import { formatPrice, grossFromNet } from '../../utils/price';
|
||||
import { formatStock } from '../article/searchFormat';
|
||||
import { primaryPrice, priceBrutto } from '../../utils/articleDetail';
|
||||
import artikelAutomat, { createArticleSelector, getArticle } from '../../automats/artikelAutomat';
|
||||
import { artikelDetailPath } from '../article/articleRoute';
|
||||
@@ -578,9 +579,40 @@ class ManifestFormLine extends PureComponent {
|
||||
? Math.round((ekNetto * (1 + Number(ekTaxRate) / 100) + Number.EPSILON) * 100) / 100
|
||||
: 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 =
|
||||
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 (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -697,6 +729,55 @@ class ManifestFormLine extends PureComponent {
|
||||
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 */}
|
||||
<Tooltip
|
||||
title={
|
||||
@@ -793,7 +874,7 @@ class ManifestFormLine extends PureComponent {
|
||||
)}
|
||||
<HelpOutlineIcon sx={{ fontSize: 15, color: 'warning.main' }} />
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.72rem' }}>
|
||||
Kein Artikel für SKU „{rawSku}“ gefunden.
|
||||
Kein Artikel für ArtNr „{rawSku}“ gefunden.
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
@@ -867,6 +948,26 @@ class ManifestFormLine extends PureComponent {
|
||||
: 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 (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -981,7 +1082,7 @@ class ManifestFormLine extends PureComponent {
|
||||
{/* Supplier SKU if distinct */}
|
||||
{supplierArticle.sku && supplierArticle.sku !== rawSku && supplierArticle.sku !== article.sku && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
|
||||
(Lief.-SKU: <strong>{supplierArticle.sku}</strong>)
|
||||
(Lief.-ArtNr: <strong>{supplierArticle.sku}</strong>)
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -996,8 +1097,8 @@ class ManifestFormLine extends PureComponent {
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Stored Purchase Price (Netto), VK Netto & VK Brutto */}
|
||||
{(priceNet != null || vkNetto != null || vkBrutto != null) && (
|
||||
{/* Stored Purchase Price (Netto), VK Netto, VK Brutto, Marge & Bestand */}
|
||||
{(priceNet != null || vkNetto != null || vkBrutto != null || deliveryStockVal != null) && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
@@ -1007,6 +1108,51 @@ class ManifestFormLine extends PureComponent {
|
||||
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 && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
@@ -1093,8 +1239,8 @@ class ManifestFormLine extends PureComponent {
|
||||
<HelpOutlineIcon sx={{ fontSize: 15, color: 'warning.main' }} />
|
||||
<Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.72rem' }}>
|
||||
{this.props.kind === 'delivery'
|
||||
? `Kein Lieferanten-Artikel für SKU „${rawSku}“ gefunden.`
|
||||
: `Kein Artikel für SKU „${rawSku}“ gefunden.`}
|
||||
? `Kein Lieferanten-Artikel für ArtNr „${rawSku}“ gefunden.`
|
||||
: `Kein Artikel für ArtNr „${rawSku}“ gefunden.`}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -26,6 +26,8 @@ import { notifyError, notifyErrorMessage } from '../../utils/notify';
|
||||
import entityAutomat from '../../automats/entityAutomat';
|
||||
import customergroupAutomat from '../../automats/customergroupAutomat';
|
||||
import supplierAutomat from '../../automats/supplierAutomat';
|
||||
import numberAutomat from '../../automats/numberAutomat';
|
||||
import { getNextDocumentNumber, ensureDocumentNumber } from '../../utils/documentNumber';
|
||||
import {
|
||||
SURFACE_RAISED,
|
||||
} from '../../colors';
|
||||
@@ -231,6 +233,40 @@ class ManifestHeader extends PureComponent {
|
||||
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() {
|
||||
const {
|
||||
kind,
|
||||
@@ -273,6 +309,9 @@ class ManifestHeader extends PureComponent {
|
||||
|
||||
const hasArticles = this.hasArticles();
|
||||
const isEdit = docId != null;
|
||||
const previewNumber =
|
||||
this.props.previewNumber ??
|
||||
(!number && !isEdit ? getNextDocumentNumber(kind) : null);
|
||||
const hasAttachments = Array.isArray(attachments) && attachments.length > 0;
|
||||
const hasParty = partyId != null && partyId !== '';
|
||||
const partyValid = kind !== 'order' || hasParty;
|
||||
@@ -376,8 +415,7 @@ class ManifestHeader extends PureComponent {
|
||||
<TextField
|
||||
{...params}
|
||||
inputRef={this.partyInputRef}
|
||||
label={dirtyFields?.partyId ? `${partyLabel} *` : partyLabel}
|
||||
required={kind === 'order'}
|
||||
label={partyLabel}
|
||||
error={isPartyEmpty}
|
||||
onKeyDown={this.handlePartyKeyDown}
|
||||
slotProps={{
|
||||
@@ -401,12 +439,25 @@ class ManifestHeader extends PureComponent {
|
||||
<SingleLineTextEdit
|
||||
size="small"
|
||||
label="Nummer"
|
||||
value={number || ''}
|
||||
value={number || previewNumber || ''}
|
||||
disabled
|
||||
placeholder="–"
|
||||
title={
|
||||
!number && previewNumber
|
||||
? 'Vorschau der nächsten Nummer (wird beim Speichern vergeben)'
|
||||
: undefined
|
||||
}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
}}
|
||||
sx={{
|
||||
flex: { xs: '1 1 100%', sm: '1 1 120px' },
|
||||
minWidth: { sm: 100 },
|
||||
...(!number && previewNumber && {
|
||||
'& .MuiInputBase-input.Mui-disabled': {
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
{useExternalNumber ? (
|
||||
@@ -482,27 +533,45 @@ class ManifestHeader extends PureComponent {
|
||||
>
|
||||
{saving ? 'Speichern…' : 'Speichern'}
|
||||
</Button>
|
||||
{isDirty && onRestore && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="warning"
|
||||
size="small"
|
||||
startIcon={<RestoreIcon />}
|
||||
disabled={saving || orderCreating}
|
||||
onClick={onRestore}
|
||||
sx={{
|
||||
height: 34,
|
||||
fontWeight: 700,
|
||||
fontSize: '0.8rem',
|
||||
whiteSpace: 'nowrap',
|
||||
textTransform: 'none',
|
||||
borderWidth: 1.5,
|
||||
'&:hover': { borderWidth: 1.5 },
|
||||
}}
|
||||
>
|
||||
Wiederherstellen
|
||||
</Button>
|
||||
)}
|
||||
{(() => {
|
||||
const canRestore = Boolean(onRestore && isDirty && !saving && !orderCreating);
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color={canRestore ? 'warning' : 'inherit'}
|
||||
size="small"
|
||||
startIcon={<RestoreIcon />}
|
||||
disabled={!canRestore}
|
||||
onClick={canRestore ? onRestore : undefined}
|
||||
title={
|
||||
canRestore
|
||||
? 'Änderungen verwerfen und ursprünglichen Stand wiederherstellen'
|
||||
: 'Keine Änderungen zum Wiederherstellen'
|
||||
}
|
||||
sx={{
|
||||
height: 34,
|
||||
width: '100%',
|
||||
fontWeight: 700,
|
||||
fontSize: '0.8rem',
|
||||
whiteSpace: 'nowrap',
|
||||
textTransform: 'none',
|
||||
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
|
||||
</Button>
|
||||
);
|
||||
})()}
|
||||
{docId != null && kind === 'delivery' && onOpenBookDialog && Boolean(ledgerSummary && (ledgerSummary.totalPositions > 0 || ledgerSummary.lines?.length > 0 || ledgerSummary.hasTrackStock)) && (
|
||||
<Button
|
||||
variant={ledgerSummary?.status === 'booked' ? 'outlined' : 'contained'}
|
||||
@@ -562,27 +631,56 @@ class ManifestHeader extends PureComponent {
|
||||
{orderCreating ? 'Wird erstellt…' : 'Bestellung erstellen'}
|
||||
</Button>
|
||||
)}
|
||||
{(kind === 'quote' || kind === 'order' || kind === 'delivery') && onPdfCreate && docId != null && Boolean(number) && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
startIcon={pdfCreating ? <CircularProgress size={16} color="inherit" /> : <PictureAsPdfIcon />}
|
||||
disabled={saving || pdfCreating || orderCreating}
|
||||
onClick={onPdfCreate}
|
||||
title="PDF erstellen"
|
||||
sx={{
|
||||
whiteSpace: 'nowrap',
|
||||
height: 40,
|
||||
width: '100%',
|
||||
px: 1.5,
|
||||
boxSizing: 'border-box',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.875rem',
|
||||
}}
|
||||
>
|
||||
{pdfCreating ? 'Wird erstellt…' : 'PDF erstellen'}
|
||||
</Button>
|
||||
)}
|
||||
{(kind === 'quote' || kind === 'order' || kind === 'delivery') && (() => {
|
||||
const canCreatePdf = Boolean(
|
||||
onPdfCreate &&
|
||||
docId != null &&
|
||||
Boolean(number) &&
|
||||
!saving &&
|
||||
!pdfCreating &&
|
||||
!orderCreating
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color={canCreatePdf ? 'primary' : 'inherit'}
|
||||
startIcon={pdfCreating ? <CircularProgress size={16} color="inherit" /> : <PictureAsPdfIcon />}
|
||||
disabled={!canCreatePdf}
|
||||
onClick={canCreatePdf ? onPdfCreate : undefined}
|
||||
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={{
|
||||
whiteSpace: 'nowrap',
|
||||
height: 40,
|
||||
width: '100%',
|
||||
px: 1.5,
|
||||
boxSizing: 'border-box',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.875rem',
|
||||
...(!canCreatePdf && {
|
||||
borderColor: 'action.disabled',
|
||||
color: 'text.disabled',
|
||||
'&.Mui-disabled': {
|
||||
borderColor: 'action.disabled',
|
||||
color: 'text.disabled',
|
||||
},
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{pdfCreating ? 'Wird erstellt…' : 'PDF erstellen'}
|
||||
</Button>
|
||||
);
|
||||
})()}
|
||||
{onPdfUpload ? (
|
||||
<FileDropzone
|
||||
accept="application/pdf,.pdf,text/plain,.txt,text/csv,.csv"
|
||||
|
||||
@@ -199,22 +199,42 @@ class ManifestLines extends PureComponent {
|
||||
);
|
||||
})()}
|
||||
{(() => {
|
||||
const hasShippedLines = kind === 'order' && Array.isArray(shipments) && shipments.some((sh) => (sh.lines?.length || 0) > 0);
|
||||
const hasLedgeredLines = kind === 'delivery' && Array.isArray(lines) && lines.some((line) => {
|
||||
const ledgerLine = line?.id != null ? ledgerLinesById?.[line.id] : null;
|
||||
return Boolean(
|
||||
ledgerLine?.isBooked ||
|
||||
(ledgerLine && ledgerLine.targetAmount > 0 && ledgerLine.remainingAmount <= 0) ||
|
||||
(ledgerLine && ledgerLine.bookedAmount > 0) ||
|
||||
line?.isBooked ||
|
||||
line?.is_booked
|
||||
);
|
||||
});
|
||||
const disabled = saving || !lines || lines.length === 0 || hasShippedLines || hasLedgeredLines;
|
||||
const title = hasLedgeredLines
|
||||
? 'Lieferung enthält bereits im Ledger verbuchte Positionen'
|
||||
: hasShippedLines
|
||||
? 'Auftrag enthält bereits ausgelieferte Positionen'
|
||||
const shippedSkus = new Set();
|
||||
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;
|
||||
return Boolean(
|
||||
ledgerLine?.isBooked ||
|
||||
(ledgerLine && ledgerLine.targetAmount > 0 && ledgerLine.remainingAmount <= 0) ||
|
||||
(ledgerLine && ledgerLine.bookedAmount > 0) ||
|
||||
line?.isBooked ||
|
||||
line?.is_booked
|
||||
);
|
||||
});
|
||||
const disabled = saving || !lines || lines.length === 0 || allLinesShipped || allLinesLedgered;
|
||||
const title = allLinesLedgered
|
||||
? 'Alle Positionen der Lieferung sind bereits im Ledger verbucht'
|
||||
: allLinesShipped
|
||||
? 'Alle Positionen des Auftrags sind bereits ausgeliefert'
|
||||
: 'Alle Zeilen entfernen';
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -359,7 +359,7 @@ export default class OrderShipmentDialog extends PureComponent {
|
||||
onChange={() => this.handleToggleAll(openItems)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>SKU</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>ArtNr</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600 }}>Artikel</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>Bestellt</TableCell>
|
||||
<TableCell align="right" sx={{ fontWeight: 600 }}>Geliefert</TableCell>
|
||||
|
||||
@@ -347,7 +347,7 @@ export default class ShipmentDetailDialog extends PureComponent {
|
||||
<Table size="small">
|
||||
<TableHead sx={{ bgcolor: SURFACE_RAISED }}>
|
||||
<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 align="right" sx={{ fontWeight: 600, py: 0.75 }}>Menge</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -224,7 +224,7 @@ export function buildLines(lines, { requireAtLeastOne }) {
|
||||
|
||||
const missing = [];
|
||||
if (!sku && !name) {
|
||||
missing.push('Artikel (SKU oder Name)');
|
||||
missing.push('Artikel (ArtNr oder Name)');
|
||||
}
|
||||
if (amount == null) {
|
||||
missing.push(rawAmount != null ? 'gültige Menge' : 'Menge');
|
||||
@@ -555,36 +555,36 @@ export async function buildManifestBody(
|
||||
|
||||
export const LINE_COLUMNS = [
|
||||
{ id: 'pos', label: 'Pos', width: 44, align: 'center' },
|
||||
{ id: 'amount', label: 'Amount', width: 80, align: 'right', type: 'number' },
|
||||
{ id: 'sku', label: 'SKU', width: 130, align: 'left', type: 'text' },
|
||||
{ id: 'amount', label: 'Anzahl', width: 80, align: 'right', type: 'number' },
|
||||
{ id: 'sku', label: 'ArtNr', width: 130, 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: 'rebate', label: 'Rebate', width: 75, align: 'right', type: 'number' },
|
||||
{ id: 'net', label: 'Net', width: 95, align: 'right', type: 'number' },
|
||||
{ id: 'gross', label: 'Gross', width: 95, align: 'right', type: 'number' },
|
||||
{ id: 'tax', label: 'MwSt', width: 70, align: 'right', type: 'number' },
|
||||
{ id: 'rebate', label: 'Rabatt', width: 75, align: 'right', type: 'number' },
|
||||
{ id: 'net', label: 'Netto', 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 ORDER_LINE_COLUMNS = [
|
||||
{ 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: 'amount', label: 'Amount', width: 80, align: 'right', type: 'number' },
|
||||
{ id: 'tax', label: 'Tax', width: 70, align: 'right', type: 'number' },
|
||||
{ id: 'rebate', label: 'Rebate', width: 75, align: 'right', type: 'number' },
|
||||
{ id: 'net', label: 'Net', width: 95, align: 'right', type: 'number' },
|
||||
{ id: 'gross', label: 'Gross', width: 95, align: 'right', type: 'number' },
|
||||
{ id: 'amount', label: 'Anzahl', width: 80, align: 'right', type: 'number' },
|
||||
{ id: 'tax', label: 'MwSt', width: 70, align: 'right', type: 'number' },
|
||||
{ id: 'rebate', label: 'Rabatt', width: 75, align: 'right', type: 'number' },
|
||||
{ id: 'net', label: 'Netto', width: 95, align: 'right', type: 'number' },
|
||||
{ id: 'gross', label: 'Brutto', width: 95, align: 'right', type: 'number' },
|
||||
];
|
||||
|
||||
export const DELIVERY_LINE_COLUMNS = [
|
||||
{ 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: '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: 'taxrate', label: 'Tax', width: 80, align: 'right', type: 'number' },
|
||||
{ id: 'netprice', label: 'Net', width: 104, align: 'right', type: 'number' },
|
||||
{ id: 'taxrate', label: 'MwSt', width: 80, align: 'right', type: 'number' },
|
||||
{ id: 'netprice', label: 'Netto', width: 104, align: 'right', type: 'number' },
|
||||
];
|
||||
|
||||
/** @typedef {'delivery'|'quote'|'order'} ManifestKind */
|
||||
@@ -687,7 +687,7 @@ export function manifestConfig(cfgOrKind) {
|
||||
listField: 'quotes',
|
||||
docField: 'quote',
|
||||
partyField: 'entityId',
|
||||
listPath: '/auftrag',
|
||||
listPath: '/auftrag/angebot',
|
||||
createPath: '/auftrag/neuesangebot',
|
||||
editPath: (id) => `/auftrag/${id}`,
|
||||
dateLabel: 'Gültig bis',
|
||||
@@ -720,13 +720,13 @@ export function manifestConfig(cfgOrKind) {
|
||||
listField: 'orders',
|
||||
docField: 'order',
|
||||
partyField: 'entityId',
|
||||
listPath: '/auftrag/bestellung',
|
||||
listPath: '/auftrag',
|
||||
createPath: '/auftrag/bestellung/neu',
|
||||
editPath: (id) => `/auftrag/bestellung/${id}`,
|
||||
dateLabel: 'Lieferdatum',
|
||||
partyLabel: 'Kunde',
|
||||
emptyList: 'Noch keine Bestellungen.',
|
||||
createButton: 'Neue Bestellung',
|
||||
createButton: 'Neuer Auftrag',
|
||||
createdMsg: 'Bestellung angelegt.',
|
||||
savedMsg: 'Bestellung gespeichert.',
|
||||
deletedMsg: 'Bestellung gelöscht.',
|
||||
|
||||
@@ -55,8 +55,8 @@ function getInitialTarget() {
|
||||
const p = window.location.pathname;
|
||||
if (p.startsWith('/lieferung')) return 'delivery';
|
||||
if (p.startsWith('/auftrag/ausliefern')) return 'shipment';
|
||||
if (p.startsWith('/auftrag/bestellung')) return 'order';
|
||||
if (p.startsWith('/auftrag')) return 'quote';
|
||||
if (p.startsWith('/auftrag/angebot')) return 'quote';
|
||||
if (p.startsWith('/auftrag')) return 'order';
|
||||
}
|
||||
return 'order';
|
||||
}
|
||||
|
||||
21
src/nav.js
21
src/nav.js
@@ -202,18 +202,18 @@ export const SUB_NAV = {
|
||||
'/auftrag': [
|
||||
{
|
||||
segment: '',
|
||||
label: 'Auftrag',
|
||||
title: 'Auftrag',
|
||||
icon: ShoppingCartIcon,
|
||||
help: 'Bestellungen prüfen, freigeben und kommissionieren',
|
||||
},
|
||||
{
|
||||
segment: 'angebot',
|
||||
label: 'Angebot',
|
||||
title: 'Angebot',
|
||||
icon: RequestQuoteIcon,
|
||||
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',
|
||||
label: 'Ausliefern',
|
||||
@@ -336,11 +336,16 @@ export function resolveActiveSub(pathname, sectionPath) {
|
||||
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)) {
|
||||
return rest;
|
||||
}
|
||||
|
||||
// Treat /auftrag/bestellung as segment ''
|
||||
if (sectionPath === '/auftrag' && rest === 'bestellung') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Treat /infos/articlehistory as segment ''
|
||||
if (sectionPath === '/infos' && rest === 'articlehistory') {
|
||||
return '';
|
||||
|
||||
@@ -635,7 +635,7 @@ class ArtikelSearchPage extends Component {
|
||||
sx={{ maxWidth: 520, flexGrow: 1 }}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
placeholder="(EAN, SKU, Name)"
|
||||
placeholder="(EAN, ArtNr, Name)"
|
||||
value={input}
|
||||
onChange={this.handleInputChange}
|
||||
slotProps={{
|
||||
|
||||
@@ -160,6 +160,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
this.handleRemoveLine = this.handleRemoveLine.bind(this);
|
||||
this.handleMoveLine = this.handleMoveLine.bind(this);
|
||||
this.handleRemoveAllLines = this.handleRemoveAllLines.bind(this);
|
||||
this.handleRestore = this.handleRestore.bind(this);
|
||||
this.handleSave = this.handleSave.bind(this);
|
||||
this.handlePdfUpload = this.handlePdfUpload.bind(this);
|
||||
this.handlePdfRescan = this.handlePdfRescan.bind(this);
|
||||
@@ -527,7 +528,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
|
||||
const internalSku = article?.sku;
|
||||
if (!internalSku) {
|
||||
const msg = 'Artikel hat keine SKU.';
|
||||
const msg = 'Artikel hat keine ArtNr.';
|
||||
enqueueSnackbar(msg, { variant: 'error' });
|
||||
throw new Error(msg);
|
||||
}
|
||||
@@ -686,6 +687,24 @@ class LieferungNeuPage extends PureComponent {
|
||||
|
||||
handleRemoveAllLines() {
|
||||
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) {
|
||||
@@ -1331,6 +1350,7 @@ class LieferungNeuPage extends PureComponent {
|
||||
onNumberChange={this.handleNumberChange}
|
||||
onExternalNumberChange={this.handleExternalNumberChange}
|
||||
docId={this.props.docId}
|
||||
onRestore={isEdit ? this.handleRestore : undefined}
|
||||
ledgerSummary={this.state.ledgerSummary}
|
||||
onOpenBookDialog={this.handleOpenBookDialog}
|
||||
/>
|
||||
@@ -1497,8 +1517,8 @@ class LieferungNeuPage extends PureComponent {
|
||||
<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' }}>Lieferanten SKU</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Eigene SKU</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lieferanten-ArtNr</TableCell>
|
||||
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Eigene ArtNr</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 sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lager</TableCell>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import numberAutomat from '../automats/numberAutomat';
|
||||
import quoteAutomat from '../automats/quoteAutomat';
|
||||
import orderAutomat from '../automats/orderAutomat';
|
||||
import deliveryAutomat from '../automats/deliveryAutomat';
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
export function getNextDocumentNumber(kind) {
|
||||
@@ -42,7 +43,9 @@ export function getNextDocumentNumber(kind) {
|
||||
let prefix = '';
|
||||
let startNum = 1;
|
||||
|
||||
if (
|
||||
if (kind === 'delivery') {
|
||||
prefix = prefixStyle.startsWith('DEL-') ? 'DEL-' : (prefixStyle.startsWith('L-') ? 'L-' : 'LS-');
|
||||
} else if (
|
||||
prefixStyle.startsWith('ART-') ||
|
||||
prefixStyle.startsWith('LS-') ||
|
||||
prefixStyle.startsWith('KD-') ||
|
||||
@@ -69,8 +72,8 @@ export function getNextDocumentNumber(kind) {
|
||||
prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : '');
|
||||
}
|
||||
|
||||
// Check existing docs from quoteAutomat / orderAutomat to avoid collisions
|
||||
const listAutomat = kind === 'quote' ? quoteAutomat : (kind === 'order' ? orderAutomat : null);
|
||||
// Check existing docs from quoteAutomat / orderAutomat / deliveryAutomat to avoid collisions
|
||||
const listAutomat = kind === 'quote' ? quoteAutomat : (kind === 'order' ? orderAutomat : (kind === 'delivery' ? deliveryAutomat : null));
|
||||
const existingDocs = Array.isArray(listAutomat?.state?.items) ? listAutomat.state.items : [];
|
||||
let maxFound = startNum - 1;
|
||||
|
||||
@@ -99,11 +102,11 @@ export function getNextDocumentNumber(kind) {
|
||||
/**
|
||||
* 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>}
|
||||
*/
|
||||
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([
|
||||
numberAutomat.actions.ensureLoaded().catch(() => {}),
|
||||
listAutomat?.actions?.ensureLoaded ? listAutomat.actions.ensureLoaded().catch(() => {}) : Promise.resolve(),
|
||||
|
||||
Reference in New Issue
Block a user