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
- `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`

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` |
| **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.

View File

@@ -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 /> },

View File

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

View File

@@ -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 },

View File

@@ -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';

View File

@@ -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 });

View File

@@ -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"

View File

@@ -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 ? (

View File

@@ -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()}

View File

@@ -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}

View File

@@ -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 &bdquo;{rawSku}&ldquo; gefunden.
Kein Artikel für ArtNr &bdquo;{rawSku}&ldquo; 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>
);

View File

@@ -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"

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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.',

View File

@@ -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';
}

View File

@@ -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 '';

View File

@@ -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={{

View File

@@ -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>

View File

@@ -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(),