Compare commits
4 Commits
20d83b00fc
...
45c1067d9f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45c1067d9f | ||
|
|
582bf04476 | ||
|
|
c0ae79ba6b | ||
|
|
01d17ebb25 |
@@ -4,6 +4,130 @@ description: >-
|
|||||||
Reference documentation for backend service.
|
Reference documentation for backend service.
|
||||||
Use this skill whenever working with backend APIs.
|
Use this skill whenever working with backend APIs.
|
||||||
---
|
---
|
||||||
|
# we should cache in client (at least) : company,language,tax,unit,unittext,customergroup,warehouse,account,brand,category,categorytext,attributename,attributevalue,propertyname,companysetting,printer,labeltemplate,posterminal,posterminalcategory
|
||||||
|
|
||||||
|
todos:
|
||||||
|
|
||||||
|
1. Replace 13 List GETs with 1 /bootstrap Endpoint
|
||||||
|
Because these reference tables are cached in memory on login/company-switch and invalidated via GET /events SSE, all individual list endpoints can be merged into a single GET /bootstrap (or folded into GET /auth/me / PUT /auth/selected-company):
|
||||||
|
|
||||||
|
GET /account
|
||||||
|
|
||||||
|
GET /attributename (already returns nested values: T28[] in T16)
|
||||||
|
|
||||||
|
GET /brand (unless you have thousands of brands requiring server pagination)
|
||||||
|
|
||||||
|
GET /carrier
|
||||||
|
|
||||||
|
GET /category/tree
|
||||||
|
|
||||||
|
GET /company & GET /company/setting
|
||||||
|
|
||||||
|
GET /customergroup
|
||||||
|
|
||||||
|
GET /labeltemplate (metadata list without the heavy content.objects array)
|
||||||
|
|
||||||
|
GET /posterminal
|
||||||
|
|
||||||
|
GET /printer
|
||||||
|
|
||||||
|
GET /propertyname
|
||||||
|
|
||||||
|
GET /supplier
|
||||||
|
|
||||||
|
GET /unit
|
||||||
|
|
||||||
|
GET /warehouse
|
||||||
|
|
||||||
|
|
||||||
|
Drop 6 Redundant Single-Item & Child GET Endpoints
|
||||||
|
Once the parent lists above are in the client cache, looking up a single item by ID or key over the network is dead weight:
|
||||||
|
|
||||||
|
Endpoint to Remove,Why It's Redundant with Client Cache
|
||||||
|
GET /attributename/{attributeId}/value,"GET /attributename returns T16[], which already embeds values: T28[]."
|
||||||
|
GET /carrier/{id},Already in cached carriers: T31[].
|
||||||
|
GET /company/setting/{key},Already in cached settings map.
|
||||||
|
GET /printer/{id},Already in cached printers: T22[].
|
||||||
|
GET /warehouse/{id},Already in cached warehouses list.
|
||||||
|
GET /order/{id}/paymentGET /order/{id}/shipmentGET /order/{id}/shipping,"GET /order/{id} returns T1, which already embeds payments: T30[], shipments: T3[], and shippings: T19[]."
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Consolidate Micro-Mutation Endpoints (~20 Routes Saved)
|
||||||
|
Several resources expose separate endpoints for single-column updates or duplicate HTTP verbs. Folding these into their parent PATCH or PUT routes cuts massive routing boilerplate:
|
||||||
|
|
||||||
|
Article single-field setters (Save 4 routes):
|
||||||
|
PUT /article/{sku}/brand, PUT /article/{sku}/track_stock, PUT /article/{sku}/weight, and POST /article/{sku}/parent all return T2/T32. Fold brandId, track_stock, weight, isParent, and attributeNameId directly into PATCH /article/{sku}.
|
||||||
|
|
||||||
|
Article sub-resource getters (Save 4 routes):
|
||||||
|
If you embed attributeValueIds: int[], properties: { propertyId: int, value: str }[], articlelist, and optionslist directly inside T2 (GET /article/{sku}), you can drop GET /article/{sku}/attributevalue, GET /article/{sku}/property, GET /article/{sku}/articlelist, and GET /article/{sku}/optionslist.
|
||||||
|
|
||||||
|
POS Terminal category toggles (Save 4 routes):
|
||||||
|
DELETE /posterminal/{id}/all, PUT /posterminal/{id}/all, DELETE /posterminal/{id}/category/{categoryId}, and PUT /posterminal/{id}/category/{categoryId} can all be replaced by passing categoryIds?: int[] to PATCH /posterminal/{id}.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Duplicate PUT + PATCH & Export verbs (Save 4 routes):
|
||||||
|
|
||||||
|
labeltemplate/{id}, printer/{id}, and warehouse/{id} each define both PATCH and PUT with identical request bodies and 200 responses. Drop the PUT routes.
|
||||||
|
|
||||||
|
company/export defines both GET and POST for the exact same SSE stream.
|
||||||
|
|
||||||
|
Ledger & Supplier query duplicates (Save 4 routes):
|
||||||
|
|
||||||
|
GET /ledger/manifestarticle/{id} and GET /ledger/sku/{sku} are 100% covered by GET /ledger?manifestarticle_id=...&sku=....
|
||||||
|
|
||||||
|
GET /supplier/{id}/articles?skus=... and POST /supplier/{id}/articles do the exact same thing.
|
||||||
|
|
||||||
|
DELETE /article/{sku}/property/{propertyId} and DELETE /category/{id}/property/{propertyId} are redundant when you already use bulk PUT .../property to replace the property array.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Unify Document File Attachments (Save 8 Routes)
|
||||||
|
delivery, order, quote, and shipment all repeat identical attachment routes because they are all backed by manifest and manifestattachment in the database:
|
||||||
|
|
||||||
|
GET /{delivery|order|quote|shipment}/{id}/file/{fileId} (4 routes)
|
||||||
|
|
||||||
|
DELETE /{delivery|order|quote|shipment}/{id}/file/{fileId} (4 routes)
|
||||||
|
|
||||||
|
POST /{delivery|order|quote|shipment}/{id}/pdf (4 routes)
|
||||||
|
|
||||||
|
Since file.id is a primary key (or scoped via manifestattachment), you can replace all 8 file routes with GET /file/{fileId} and DELETE /file/{fileId} (saving 6 routes).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
5. Slim Down API Payloads (Stop Joining Cached Data on the Server)
|
||||||
|
Even for endpoints you keep, the client cache lets you remove server-side SQL JOINs and strip redundant strings from your response types:
|
||||||
|
|
||||||
|
T2 (GET /article/{sku}) & GET /category/{id}/articles:
|
||||||
|
|
||||||
|
Currently returns full unit: T27 ({ si, basesi, basesifactor, symbol, name }) and brand: T26 ({ id, name, fullHash, thumbW, thumbH, thumbHash }).
|
||||||
|
|
||||||
|
Reduction: Return only unitId: int (or unitSi: str) and brandId: int|null. The client resolves the unit symbols, conversion factors, brand name, and brand logo hash instantly from the cache.
|
||||||
|
|
||||||
|
GET /article/{sku}/attributevalue & GET /category/{id}/attributevalue:
|
||||||
|
|
||||||
|
Currently joins and returns attributeId, attributeName, value, sort, exclusive.
|
||||||
|
|
||||||
|
Reduction: Return only attributeValueIds: int[]. The client already has the entire attributename -> attributevalue tree cached.
|
||||||
|
|
||||||
|
GET /article/{sku}/property & GET /category/{id}/property:
|
||||||
|
|
||||||
|
Currently joins propertyname to return name: str and exclusive: bool on every property row.
|
||||||
|
|
||||||
|
Reduction: Return { propertyId: int, value: str, sort: int }[] and resolve name and exclusive from the cached propertyname table.
|
||||||
|
|
||||||
|
T11 (articlesupplier) & T19 (shipping):
|
||||||
|
|
||||||
|
Strip supplierName?: str from T11 (resolve via entityId in cached supplier list) and strip carrier?: str from T19 (resolve via carrierId in cached carrier list).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Dash API Routes Overview (All)
|
# Dash API Routes Overview (All)
|
||||||
|
|
||||||
|
|||||||
@@ -242,6 +242,8 @@ export function sortArticlesByRelevance(articles, query) {
|
|||||||
export function searchArticlesIndex(query, options = {}) {
|
export function searchArticlesIndex(query, options = {}) {
|
||||||
const {
|
const {
|
||||||
excludeSkus = [],
|
excludeSkus = [],
|
||||||
|
includeSkus = null,
|
||||||
|
extraArticles = [],
|
||||||
allowParent = false,
|
allowParent = false,
|
||||||
categories = [],
|
categories = [],
|
||||||
offset = 0,
|
offset = 0,
|
||||||
@@ -253,7 +255,15 @@ export function searchArticlesIndex(query, options = {}) {
|
|||||||
void articleIndexAutomat.actions.ensureLoaded().catch(() => {});
|
void articleIndexAutomat.actions.ensureLoaded().catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
const articles = articleIndexAutomat.state.items ?? [];
|
let articles = articleIndexAutomat.state.items ?? [];
|
||||||
|
if (Array.isArray(extraArticles) && extraArticles.length > 0) {
|
||||||
|
const existingSkus = new Set(articles.map((a) => String(a.sku || '').toLowerCase()));
|
||||||
|
const missing = extraArticles.filter((a) => a.sku && !existingSkus.has(String(a.sku).toLowerCase()));
|
||||||
|
if (missing.length > 0) {
|
||||||
|
articles = [...articles, ...missing];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const q = String(query ?? '').trim();
|
const q = String(query ?? '').trim();
|
||||||
|
|
||||||
const rawMatches = q ? queryFlexSearch(q, articles) : articles;
|
const rawMatches = q ? queryFlexSearch(q, articles) : articles;
|
||||||
@@ -266,6 +276,10 @@ export function searchArticlesIndex(query, options = {}) {
|
|||||||
if (excludedSet.size > 0) {
|
if (excludedSet.size > 0) {
|
||||||
pool = pool.filter((a) => a.sku && !excludedSet.has(String(a.sku).toLowerCase()));
|
pool = pool.filter((a) => a.sku && !excludedSet.has(String(a.sku).toLowerCase()));
|
||||||
}
|
}
|
||||||
|
if (includeSkus != null) {
|
||||||
|
const includedSet = new Set((includeSkus ?? []).map((s) => String(s).toLowerCase()));
|
||||||
|
pool = pool.filter((a) => a.sku && includedSet.has(String(a.sku).toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
let matchesList = pool;
|
let matchesList = pool;
|
||||||
|
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ export function createDeliveryEditorAutomat(options = {}) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await supplierApi.getArticles(entityId, skus);
|
const res = await supplierApi.getArticles(entityId);
|
||||||
const articles = Array.isArray(res?.articles) ? res.articles : [];
|
const articles = Array.isArray(res?.articles) ? res.articles : [];
|
||||||
|
|
||||||
const map = buildSupplierArticlesMap(articles);
|
const map = buildSupplierArticlesMap(articles);
|
||||||
|
|||||||
@@ -332,6 +332,8 @@ export default class ArticleSearch extends PureComponent {
|
|||||||
if (
|
if (
|
||||||
this.state.indexStatus !== prevState.indexStatus ||
|
this.state.indexStatus !== prevState.indexStatus ||
|
||||||
this.props.excludeSkus !== prevProps.excludeSkus ||
|
this.props.excludeSkus !== prevProps.excludeSkus ||
|
||||||
|
this.props.includeSkus !== prevProps.includeSkus ||
|
||||||
|
this.props.extraArticles !== prevProps.extraArticles ||
|
||||||
this.props.allowParent !== prevProps.allowParent ||
|
this.props.allowParent !== prevProps.allowParent ||
|
||||||
this.props.categories !== prevProps.categories
|
this.props.categories !== prevProps.categories
|
||||||
) {
|
) {
|
||||||
@@ -355,6 +357,8 @@ export default class ArticleSearch extends PureComponent {
|
|||||||
runQuery = (queryText) => {
|
runQuery = (queryText) => {
|
||||||
const {
|
const {
|
||||||
excludeSkus = [],
|
excludeSkus = [],
|
||||||
|
includeSkus = null,
|
||||||
|
extraArticles = [],
|
||||||
allowParent = false,
|
allowParent = false,
|
||||||
categories = [],
|
categories = [],
|
||||||
mode,
|
mode,
|
||||||
@@ -364,6 +368,8 @@ export default class ArticleSearch extends PureComponent {
|
|||||||
// full match set; the autocomplete popper keeps a small option cap.
|
// full match set; the autocomplete popper keeps a small option cap.
|
||||||
const res = searchArticlesIndex(queryText, {
|
const res = searchArticlesIndex(queryText, {
|
||||||
excludeSkus,
|
excludeSkus,
|
||||||
|
includeSkus,
|
||||||
|
extraArticles,
|
||||||
allowParent,
|
allowParent,
|
||||||
categories,
|
categories,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import Alert from '@mui/material/Alert';
|
|||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import ArticleSearch from '../article/ArticleSearch';
|
import ArticleSearch from '../article/ArticleSearch';
|
||||||
|
|
||||||
|
import deliveryEditorAutomat from '../../automats/deliveryEditorAutomat';
|
||||||
|
|
||||||
export default class ArticleSearchDialog extends PureComponent {
|
export default class ArticleSearchDialog extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -26,6 +28,11 @@ export default class ArticleSearchDialog extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleClose = () => {
|
||||||
|
this.setState({ assigningSku: null, error: null });
|
||||||
|
this.props.onClose?.();
|
||||||
|
};
|
||||||
|
|
||||||
handleSelect = async (article) => {
|
handleSelect = async (article) => {
|
||||||
const { onAssign, line, kind = 'delivery' } = this.props;
|
const { onAssign, line, kind = 'delivery' } = this.props;
|
||||||
const isDelivery = kind === 'delivery';
|
const isDelivery = kind === 'delivery';
|
||||||
@@ -41,26 +48,51 @@ export default class ArticleSearchDialog extends PureComponent {
|
|||||||
await onAssign(article, line);
|
await onAssign(article, line);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({
|
this.setState({
|
||||||
assigningSku: null,
|
|
||||||
error:
|
error:
|
||||||
err?.message ||
|
err?.message ||
|
||||||
(isDelivery
|
(isDelivery
|
||||||
? 'Fehler beim Verknüpfen des Lieferanten-Artikels.'
|
? (line ? 'Fehler beim Verknüpfen des Lieferanten-Artikels.' : 'Fehler beim Hinzufügen des Artikels.')
|
||||||
: 'Fehler beim Zuordnen des Artikels.'),
|
: 'Fehler beim Zuordnen des Artikels.'),
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
this.setState({ assigningSku: null });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { open, onClose, line, kind = 'delivery' } = this.props;
|
const { open, onClose, line, kind = 'delivery', supplierArticles: propSupplierArticles } = this.props;
|
||||||
const { assigningSku, error } = this.state;
|
const { assigningSku, error } = this.state;
|
||||||
const isDelivery = kind === 'delivery';
|
const isDelivery = kind === 'delivery';
|
||||||
|
const onlySupplierArticles = isDelivery && !line;
|
||||||
const rawSku = line?.sku != null ? String(line.sku).trim() : '';
|
const rawSku = line?.sku != null ? String(line.sku).trim() : '';
|
||||||
|
|
||||||
|
const supplierArticlesList = onlySupplierArticles
|
||||||
|
? (propSupplierArticles ?? deliveryEditorAutomat.state.supplierArticles ?? [])
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const includeSkus = onlySupplierArticles
|
||||||
|
? supplierArticlesList.map((sa) => sa.article?.sku || sa.sku).filter(Boolean)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const extraArticles = onlySupplierArticles
|
||||||
|
? supplierArticlesList.map((sa) => ({
|
||||||
|
id: sa.article?.id ?? sa.id,
|
||||||
|
sku: sa.article?.sku || sa.sku,
|
||||||
|
name: sa.article?.name || sa.name || sa.sku || '',
|
||||||
|
brand: sa.article?.brand,
|
||||||
|
brandName: sa.article?.brand?.name || sa.article?.brandName,
|
||||||
|
stock: sa.article?.stock ?? sa.stock,
|
||||||
|
available: sa.article?.available,
|
||||||
|
price: sa.price?.net ?? sa.article?.price ?? sa.article?.vknetto,
|
||||||
|
eans: sa.article?.eans || [sa.sku].filter(Boolean),
|
||||||
|
isParent: false,
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppDialog
|
<AppDialog
|
||||||
open={Boolean(open)}
|
open={Boolean(open)}
|
||||||
onClose={assigningSku ? undefined : onClose}
|
onClose={this.handleClose}
|
||||||
maxWidth="md"
|
maxWidth="md"
|
||||||
fullWidth
|
fullWidth
|
||||||
aria-labelledby="article-search-dialog-title"
|
aria-labelledby="article-search-dialog-title"
|
||||||
@@ -89,18 +121,29 @@ export default class ArticleSearchDialog extends PureComponent {
|
|||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h6" component="div" fontWeight={600}>
|
<Typography variant="h6" component="div" fontWeight={600}>
|
||||||
{line ? 'Existierenden Artikel zuordnen' : 'Artikel auswählen'}
|
{line
|
||||||
|
? 'Existierenden Artikel zuordnen'
|
||||||
|
: isDelivery
|
||||||
|
? 'Lieferanten-Artikel auswählen'
|
||||||
|
: 'Artikel auswählen'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{line ? (
|
||||||
|
<>
|
||||||
{isDelivery ? 'Lieferanten-SKU' : 'SKU'}: <strong>{rawSku || '–'}</strong>
|
{isDelivery ? 'Lieferanten-SKU' : 'SKU'}: <strong>{rawSku || '–'}</strong>
|
||||||
{line?.name ? ` · ${line.name}` : ''}
|
{line?.name ? ` · ${line.name}` : ''}
|
||||||
|
</>
|
||||||
|
) : isDelivery ? (
|
||||||
|
'Nur mit dem ausgewählten Lieferanten verknüpfte Artikel'
|
||||||
|
) : (
|
||||||
|
'Artikel für Beleg auswählen'
|
||||||
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
aria-label="Schließen"
|
aria-label="Schließen"
|
||||||
disabled={Boolean(assigningSku)}
|
onClick={this.handleClose}
|
||||||
onClick={onClose}
|
|
||||||
>
|
>
|
||||||
<CloseIcon fontSize="small" />
|
<CloseIcon fontSize="small" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -123,12 +166,21 @@ export default class ArticleSearchDialog extends PureComponent {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{onlySupplierArticles && supplierArticlesList.length === 0 && (
|
||||||
|
<Alert severity="info" sx={{ mb: 2, flexShrink: 0 }}>
|
||||||
|
Für diesen Lieferanten sind noch keine Artikel verknüpft.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
<ArticleSearch
|
<ArticleSearch
|
||||||
mode="list"
|
mode="list"
|
||||||
autoFocus
|
autoFocus
|
||||||
allowParent={false}
|
allowParent={false}
|
||||||
busySku={assigningSku}
|
busySku={assigningSku}
|
||||||
actionLabel={assigningSku ? 'Zuordnen…' : 'Zuordnen'}
|
initialQuery={line?.name ? String(line.name) : ''}
|
||||||
|
includeSkus={includeSkus}
|
||||||
|
extraArticles={extraArticles}
|
||||||
|
actionLabel={assigningSku ? (line ? 'Zuordnen…' : 'Hinzufügen…') : (line ? 'Zuordnen' : 'Hinzufügen')}
|
||||||
onSelect={this.handleSelect}
|
onSelect={this.handleSelect}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -136,8 +188,7 @@ export default class ArticleSearchDialog extends PureComponent {
|
|||||||
<DialogActions sx={{ px: 2, py: 1.5 }}>
|
<DialogActions sx={{ px: 2, py: 1.5 }}>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
disabled={Boolean(assigningSku)}
|
onClick={this.handleClose}
|
||||||
onClick={onClose}
|
|
||||||
>
|
>
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import Typography from '@mui/material/Typography';
|
|
||||||
import Attachment from './Attachment';
|
import Attachment from './Attachment';
|
||||||
|
|
||||||
export class AttachmentList extends PureComponent {
|
export class AttachmentList extends PureComponent {
|
||||||
@@ -13,14 +12,6 @@ export class AttachmentList extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ mt: 2.5, pt: 1.5, borderTop: '1px solid', borderColor: 'divider' }}>
|
<Box sx={{ mt: 2.5, pt: 1.5, borderTop: '1px solid', borderColor: 'divider' }}>
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
color="text.secondary"
|
|
||||||
fontWeight={600}
|
|
||||||
sx={{ mb: 1, display: 'block', textTransform: 'uppercase', letterSpacing: '0.05em' }}
|
|
||||||
>
|
|
||||||
Dateianhänge
|
|
||||||
</Typography>
|
|
||||||
<Stack direction="row" spacing={1.5} sx={{ flexWrap: 'wrap', gap: 1.5 }}>
|
<Stack direction="row" spacing={1.5} sx={{ flexWrap: 'wrap', gap: 1.5 }}>
|
||||||
{attachments.map((attachment, index) => (
|
{attachments.map((attachment, index) => (
|
||||||
<Attachment
|
<Attachment
|
||||||
|
|||||||
@@ -27,14 +27,19 @@ import CloseIcon from '@mui/icons-material/Close';
|
|||||||
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
import Inventory2OutlinedIcon from '@mui/icons-material/Inventory2Outlined';
|
||||||
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined';
|
||||||
import WarehouseIcon from '@mui/icons-material/Warehouse';
|
import WarehouseIcon from '@mui/icons-material/Warehouse';
|
||||||
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import { enqueueSnackbar } from 'notistack';
|
import { enqueueSnackbar } from 'notistack';
|
||||||
import { notifyError } from '../../utils/notify';
|
import { notifyError } from '../../utils/notify';
|
||||||
import { formatDate } from './manifestConfig';
|
import { formatDate, resolveLineSkus } from './manifestConfig';
|
||||||
import {
|
import {
|
||||||
fetchDeliveryLedgerSummary,
|
fetchDeliveryLedgerSummary,
|
||||||
bookDeliveryLines,
|
bookDeliveryLines,
|
||||||
} from '../../utils/deliveryLedger';
|
} from '../../utils/deliveryLedger';
|
||||||
|
import { formatStock } from '../article/searchFormat';
|
||||||
|
import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
|
||||||
import warehouseAutomat from '../../automats/warehouseAutomat';
|
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||||
|
import { supplier as supplierApi } from '../../api';
|
||||||
|
import deliveryEditorAutomat, { buildSupplierArticlesMap } from '../../automats/deliveryEditorAutomat';
|
||||||
import {
|
import {
|
||||||
SURFACE_BASE,
|
SURFACE_BASE,
|
||||||
SURFACE_RAISED,
|
SURFACE_RAISED,
|
||||||
@@ -61,19 +66,24 @@ export default class BookDeliveryDialog extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialWarehouses = warehouseAutomat.state.warehouses || [];
|
const initialWarehouses = warehouseAutomat.state.warehouses || [];
|
||||||
|
const initialSupplierMap =
|
||||||
|
props.supplierArticlesMap ||
|
||||||
|
(deliveryEditorAutomat?.state?.supplierArticlesMap ?? {});
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
summary: props.summary ?? null,
|
summary: props.summary ?? null,
|
||||||
loading: !props.summary,
|
loading: !props.summary,
|
||||||
error: null,
|
error: null,
|
||||||
amountsToBook: initialAmounts,
|
amountsToBook: initialAmounts,
|
||||||
transactionText: props.delivery?.number
|
transactionText: props.delivery?.externalNumber || (props.delivery?.number
|
||||||
? `Lieferung ${props.delivery.number}`
|
? `Lieferung ${props.delivery.number}`
|
||||||
: 'Lieferung Wareneingang',
|
: 'Lieferung Wareneingang'),
|
||||||
isBooking: false,
|
isBooking: false,
|
||||||
warehouses: initialWarehouses,
|
warehouses: initialWarehouses,
|
||||||
selectedWarehouseId: initialWarehouses.length === 1 ? initialWarehouses[0].id : '',
|
selectedWarehouseId: initialWarehouses.length === 1 ? initialWarehouses[0].id : '',
|
||||||
warehouseError: null,
|
warehouseError: null,
|
||||||
|
supplierArticlesMap: initialSupplierMap,
|
||||||
|
enablingTrackStockBySku: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleAmountChange = this.handleAmountChange.bind(this);
|
this.handleAmountChange = this.handleAmountChange.bind(this);
|
||||||
@@ -82,6 +92,18 @@ export default class BookDeliveryDialog extends PureComponent {
|
|||||||
this.handleBookAll = this.handleBookAll.bind(this);
|
this.handleBookAll = this.handleBookAll.bind(this);
|
||||||
this.handleResetAmounts = this.handleResetAmounts.bind(this);
|
this.handleResetAmounts = this.handleResetAmounts.bind(this);
|
||||||
this.handleConfirmBook = this.handleConfirmBook.bind(this);
|
this.handleConfirmBook = this.handleConfirmBook.bind(this);
|
||||||
|
this.handleEnableTrackStock = this.handleEnableTrackStock.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSupplierArticlesIfNeeded() {
|
||||||
|
const supplierId = this.props.delivery?.supplierId ?? this.props.delivery?.partyId;
|
||||||
|
if (supplierId && Object.keys(this.state.supplierArticlesMap).length === 0) {
|
||||||
|
void supplierApi.get(supplierId).then((res) => {
|
||||||
|
const articles = res?.supplier?.articles || res?.articles || [];
|
||||||
|
const map = buildSupplierArticlesMap(articles);
|
||||||
|
this.setState({ supplierArticlesMap: map });
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -97,7 +119,11 @@ export default class BookDeliveryDialog extends PureComponent {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
this.unsubArticle = artikelAutomat.subscribe(() => {
|
||||||
|
this.forceUpdate();
|
||||||
|
});
|
||||||
void warehouseAutomat.actions.load().catch(() => {});
|
void warehouseAutomat.actions.load().catch(() => {});
|
||||||
|
this.loadSupplierArticlesIfNeeded();
|
||||||
|
|
||||||
if (this.props.open) {
|
if (this.props.open) {
|
||||||
void this.loadSummary();
|
void this.loadSummary();
|
||||||
@@ -106,11 +132,83 @@ export default class BookDeliveryDialog extends PureComponent {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.unsubWarehouse?.();
|
this.unsubWarehouse?.();
|
||||||
|
this.unsubArticle?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleEnableTrackStock(line, targetSku) {
|
||||||
|
if (!targetSku) return;
|
||||||
|
this.setState((prev) => ({
|
||||||
|
enablingTrackStockBySku: {
|
||||||
|
...prev.enablingTrackStockBySku,
|
||||||
|
[targetSku]: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await artikelAutomat.actions.setTrackStock(targetSku, true);
|
||||||
|
this.setState((prev) => {
|
||||||
|
if (!prev.summary?.lines) {
|
||||||
|
return {
|
||||||
|
enablingTrackStockBySku: {
|
||||||
|
...prev.enablingTrackStockBySku,
|
||||||
|
[targetSku]: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const updatedLines = prev.summary.lines.map((l) => {
|
||||||
|
const rawSku = l.sku ? String(l.sku).trim() : '';
|
||||||
|
const lineOwnSku = l.articleSku || l.article?.sku || '';
|
||||||
|
if (
|
||||||
|
l.id === line.id ||
|
||||||
|
rawSku === targetSku ||
|
||||||
|
lineOwnSku === targetSku ||
|
||||||
|
(l.article && String(l.article.sku) === targetSku)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...l,
|
||||||
|
trackStock: true,
|
||||||
|
article: l.article
|
||||||
|
? { ...l.article, track_stock: true }
|
||||||
|
: { sku: targetSku, track_stock: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return l;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
summary: {
|
||||||
|
...prev.summary,
|
||||||
|
hasTrackStock: true,
|
||||||
|
lines: updatedLines,
|
||||||
|
},
|
||||||
|
enablingTrackStockBySku: {
|
||||||
|
...prev.enablingTrackStockBySku,
|
||||||
|
[targetSku]: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
enqueueSnackbar(`Lagerbestand für „${targetSku}“ aktiviert.`, { variant: 'success' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[BookDeliveryDialog] Failed to enable track_stock:', err);
|
||||||
|
enqueueSnackbar(
|
||||||
|
err?.message || `Fehler beim Aktivieren des Lagerbestands für „${targetSku}“.`,
|
||||||
|
{ variant: 'error' }
|
||||||
|
);
|
||||||
|
this.setState((prev) => ({
|
||||||
|
enablingTrackStockBySku: {
|
||||||
|
...prev.enablingTrackStockBySku,
|
||||||
|
[targetSku]: false,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.open && !prevProps.open) {
|
if (this.props.open && !prevProps.open) {
|
||||||
const currentWarehouses = this.state.warehouses || warehouseAutomat.state.warehouses || [];
|
const currentWarehouses = this.state.warehouses || warehouseAutomat.state.warehouses || [];
|
||||||
|
const currentSupplierMap =
|
||||||
|
this.props.supplierArticlesMap ||
|
||||||
|
(deliveryEditorAutomat?.state?.supplierArticlesMap ?? this.state.supplierArticlesMap ?? {});
|
||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
summary: this.props.summary ?? null,
|
summary: this.props.summary ?? null,
|
||||||
@@ -120,13 +218,15 @@ export default class BookDeliveryDialog extends PureComponent {
|
|||||||
selectedWarehouseId:
|
selectedWarehouseId:
|
||||||
this.state.selectedWarehouseId ||
|
this.state.selectedWarehouseId ||
|
||||||
(currentWarehouses.length === 1 ? currentWarehouses[0].id : ''),
|
(currentWarehouses.length === 1 ? currentWarehouses[0].id : ''),
|
||||||
transactionText: this.props.delivery?.number
|
transactionText: this.props.delivery?.externalNumber || (this.props.delivery?.number
|
||||||
? `Lieferung ${this.props.delivery.number}`
|
? `Lieferung ${this.props.delivery.number}`
|
||||||
: 'Lieferung Wareneingang',
|
: 'Lieferung Wareneingang'),
|
||||||
isBooking: false,
|
isBooking: false,
|
||||||
|
supplierArticlesMap: currentSupplierMap,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
void warehouseAutomat.actions.load().catch(() => {});
|
void warehouseAutomat.actions.load().catch(() => {});
|
||||||
|
this.loadSupplierArticlesIfNeeded();
|
||||||
if (this.props.summary) {
|
if (this.props.summary) {
|
||||||
this.initAmountsFromSummary(this.props.summary, this.props.prefillLineId);
|
this.initAmountsFromSummary(this.props.summary, this.props.prefillLineId);
|
||||||
} else {
|
} else {
|
||||||
@@ -284,105 +384,111 @@ export default class BookDeliveryDialog extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderHeaderInfo() {
|
renderHeaderInfo({ hasTrackedLinesToBook, isWarehouseMissing } = {}) {
|
||||||
const { delivery, partyName } = this.props;
|
const { delivery, partyName } = this.props;
|
||||||
const { summary } = this.state;
|
const {
|
||||||
|
transactionText,
|
||||||
|
isBooking,
|
||||||
|
summary,
|
||||||
|
warehouses,
|
||||||
|
selectedWarehouseId,
|
||||||
|
warehouseError,
|
||||||
|
} = this.state;
|
||||||
|
const isFullyBooked = (summary?.lines?.length ?? 0) > 0 && summary?.status === 'booked';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Box sx={{ mb: 2 }}>
|
||||||
variant="outlined"
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
p: 1.5,
|
|
||||||
mb: 2,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: 3,
|
gap: 1.5,
|
||||||
backgroundColor: SURFACE_BASE,
|
mb: 1.5,
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<TextField
|
||||||
<Typography variant="caption" color="text.secondary" display="block">
|
|
||||||
Nummer
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
|
||||||
{delivery?.number || '–'}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{delivery?.externalNumber && (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary" display="block">
|
|
||||||
Referenz
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
|
||||||
{delivery.externalNumber}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary" display="block">
|
|
||||||
Lieferdatum
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2">
|
|
||||||
{formatDate(delivery?.date)}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{partyName && (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary" display="block">
|
|
||||||
Lieferant
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
|
||||||
{partyName}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box sx={{ ml: 'auto', display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
Aktueller Status:
|
|
||||||
</Typography>
|
|
||||||
{summary?.status === 'booked' && (
|
|
||||||
<Chip
|
|
||||||
size="small"
|
size="small"
|
||||||
icon={<CheckCircleOutlineOutlinedIcon />}
|
label="Datum"
|
||||||
label="Vollständig gebucht"
|
value={formatDate(delivery?.date) || '–'}
|
||||||
color="success"
|
disabled
|
||||||
variant="outlined"
|
sx={{ flex: { xs: '1 1 100%', sm: '1 1 110px' }, minWidth: { sm: 100 } }}
|
||||||
sx={{ fontWeight: 600 }}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<TextField
|
||||||
{summary?.status === 'partial' && (
|
|
||||||
<Chip
|
|
||||||
size="small"
|
size="small"
|
||||||
icon={<Inventory2OutlinedIcon />}
|
label="Lieferant"
|
||||||
label={`Teilweise gebucht (${summary.bookedPositions}/${summary.totalPositions})`}
|
value={partyName || '–'}
|
||||||
color="warning"
|
disabled
|
||||||
variant="outlined"
|
sx={{ flex: { xs: '1 1 100%', sm: '2 1 180px' }, minWidth: { sm: 140 } }}
|
||||||
sx={{ fontWeight: 600 }}
|
|
||||||
/>
|
/>
|
||||||
)}
|
<TextField
|
||||||
{summary?.status === 'unbooked' && (
|
|
||||||
<Chip
|
|
||||||
size="small"
|
size="small"
|
||||||
label={`Offen (${summary.totalPositions} Pos.)`}
|
label="Nummer"
|
||||||
variant="outlined"
|
value={delivery?.number || '–'}
|
||||||
sx={{ color: 'text.secondary', borderColor: 'divider' }}
|
disabled
|
||||||
|
sx={{ flex: { xs: '1 1 100%', sm: '1 1 110px' }, minWidth: { sm: 100 } }}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{summary?.status === 'empty' && (
|
|
||||||
<Chip size="small" label="Keine Positionen" variant="outlined" />
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 1.5,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
size="small"
|
||||||
|
label="Referenz"
|
||||||
|
value={transactionText}
|
||||||
|
onChange={this.handleTransactionTextChange}
|
||||||
|
disabled={isBooking || isFullyBooked}
|
||||||
|
sx={{ flex: { xs: '1 1 100%', sm: '1 1 200px' } }}
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
size="small"
|
||||||
|
required={hasTrackedLinesToBook}
|
||||||
|
error={Boolean(warehouseError) || isWarehouseMissing}
|
||||||
|
disabled={isBooking || isFullyBooked}
|
||||||
|
sx={{ flex: { xs: '1 1 100%', sm: '1 1 200px' } }}
|
||||||
|
>
|
||||||
|
<InputLabel id="warehouse-select-label">
|
||||||
|
Ziellager{hasTrackedLinesToBook ? ' *' : ''}
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="warehouse-select-label"
|
||||||
|
id="warehouse-select"
|
||||||
|
value={selectedWarehouseId ?? ''}
|
||||||
|
label={`Ziellager${hasTrackedLinesToBook ? ' *' : ''}`}
|
||||||
|
onChange={this.handleWarehouseChange}
|
||||||
|
startAdornment={
|
||||||
|
<WarehouseIcon
|
||||||
|
sx={{ mr: 1, color: 'text.secondary', fontSize: 20 }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{warehouses.length === 0 ? (
|
||||||
|
<MenuItem value="" disabled>
|
||||||
|
Keine Lager vorhanden
|
||||||
|
</MenuItem>
|
||||||
|
) : (
|
||||||
|
warehouses.map((wh) => (
|
||||||
|
<MenuItem key={wh.id} value={wh.id}>
|
||||||
|
{wh.name}
|
||||||
|
</MenuItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
{warehouseError && (
|
||||||
|
<FormHelperText>{warehouseError}</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLinesTable() {
|
renderLinesTable() {
|
||||||
const { summary, amountsToBook, isBooking } = this.state;
|
const { summary, amountsToBook, isBooking, supplierArticlesMap, enablingTrackStockBySku } = this.state;
|
||||||
const lines = summary?.lines || [];
|
const lines = summary?.lines || [];
|
||||||
|
|
||||||
if (lines.length === 0) {
|
if (lines.length === 0) {
|
||||||
@@ -398,37 +504,148 @@ export default class BookDeliveryDialog extends PureComponent {
|
|||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableHead sx={{ backgroundColor: SURFACE_RAISED }}>
|
<TableHead sx={{ backgroundColor: SURFACE_RAISED }}>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell sx={{ fontWeight: 600 }}>SKU</TableCell>
|
<TableCell sx={{ fontWeight: 600, width: 44 }} align="center">Pos.</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600 }}>Lieferanten-SKU</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600 }}>Eigene SKU</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600 }}>EAN</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 600 }}>Artikel</TableCell>
|
<TableCell sx={{ fontWeight: 600 }}>Artikel</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600 }}>Bestand</TableCell>
|
||||||
<TableCell align="right" sx={{ fontWeight: 600 }}>Liefermenge</TableCell>
|
<TableCell align="right" sx={{ fontWeight: 600 }}>Liefermenge</TableCell>
|
||||||
<TableCell align="right" sx={{ fontWeight: 600 }}>Bereits gebucht</TableCell>
|
<TableCell align="right" sx={{ fontWeight: 600 }}>Bereits gebucht</TableCell>
|
||||||
<TableCell align="right" sx={{ fontWeight: 600, width: 140 }}>Jetzt verbuchen</TableCell>
|
<TableCell align="right" sx={{ fontWeight: 600, width: 140 }}>Jetzt verbuchen</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{lines.map((line) => {
|
{lines.map((line, index) => {
|
||||||
const toBook = amountsToBook[line.id] ?? 0;
|
const toBook = amountsToBook[line.id] ?? 0;
|
||||||
const isAlreadyFullyBooked = line.isBooked && line.remainingAmount <= 0;
|
const isAlreadyFullyBooked = line.isBooked && line.remainingAmount <= 0;
|
||||||
|
const posNumber = line.pos ?? line.position ?? (index + 1);
|
||||||
|
const { supplierSku, ownSku } = resolveLineSkus(
|
||||||
|
line,
|
||||||
|
this.props.delivery,
|
||||||
|
supplierArticlesMap
|
||||||
|
);
|
||||||
|
const cachedArticle =
|
||||||
|
line.article ||
|
||||||
|
(ownSku && ownSku !== '–' ? getArticle(ownSku) : null) ||
|
||||||
|
(line.sku ? getArticle(line.sku) : null);
|
||||||
|
const targetSku =
|
||||||
|
cachedArticle?.sku ||
|
||||||
|
(ownSku && ownSku !== '–' ? ownSku : null) ||
|
||||||
|
(line.sku ? String(line.sku).trim() : null);
|
||||||
|
|
||||||
|
if (targetSku && !cachedArticle) {
|
||||||
|
void artikelAutomat.actions.loadArticle(targetSku).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTracked =
|
||||||
|
Boolean(line.trackStock) ||
|
||||||
|
(cachedArticle?.track_stock !== undefined
|
||||||
|
? cachedArticle.track_stock !== false &&
|
||||||
|
cachedArticle.track_stock !== 0 &&
|
||||||
|
cachedArticle.track_stock !== '0'
|
||||||
|
: false);
|
||||||
|
|
||||||
|
const isEnablingThis = targetSku ? Boolean(enablingTrackStockBySku?.[targetSku]) : false;
|
||||||
|
|
||||||
|
let currentStock = null;
|
||||||
|
if (cachedArticle) {
|
||||||
|
if (Array.isArray(cachedArticle.warehouses) && cachedArticle.warehouses.length > 0) {
|
||||||
|
currentStock = cachedArticle.warehouses.reduce(
|
||||||
|
(sum, wh) => sum + (Number(wh.stock) || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
} else if (cachedArticle.stock != null) {
|
||||||
|
currentStock = Number(cachedArticle.stock);
|
||||||
|
}
|
||||||
|
} else if (line.stock != null) {
|
||||||
|
currentStock = Number(line.stock);
|
||||||
|
}
|
||||||
|
|
||||||
|
const warehouseBreakdown =
|
||||||
|
Array.isArray(cachedArticle?.warehouses) && cachedArticle.warehouses.length > 0
|
||||||
|
? cachedArticle.warehouses
|
||||||
|
.map((w) => `${w.name || 'Lager'}: ${formatStock(w.stock ?? 0)}`)
|
||||||
|
.join(' • ')
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const eans = line.ean
|
||||||
|
? [line.ean]
|
||||||
|
: line.eans || cachedArticle?.eans || cachedArticle?.articleean || [];
|
||||||
|
const primaryEan = Array.isArray(eans) ? (eans[0] ?? '') : (eans ?? '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={line.id} hover>
|
<TableRow key={line.id} hover>
|
||||||
<TableCell sx={{ fontFamily: 'monospace', fontSize: '0.85rem' }}>
|
<TableCell align="center" sx={{ color: 'text.secondary', fontSize: '0.85rem' }}>
|
||||||
{line.sku || '–'}
|
{posNumber}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell sx={{ maxWidth: 280 }}>
|
<TableCell sx={{ fontFamily: 'monospace', fontSize: '0.85rem' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
{supplierSku}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ fontFamily: 'monospace', fontSize: '0.85rem' }}>
|
||||||
|
{ownSku}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
sx={{ fontFamily: 'monospace', fontSize: '0.85rem' }}
|
||||||
|
title={Array.isArray(eans) && eans.length > 1 ? eans.join(', ') : undefined}
|
||||||
|
>
|
||||||
|
{primaryEan || '–'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ maxWidth: 260 }}>
|
||||||
<Typography variant="body2" sx={{ fontWeight: 500 }} noWrap>
|
<Typography variant="body2" sx={{ fontWeight: 500 }} noWrap>
|
||||||
{line.name || '–'}
|
{line.name || '–'}
|
||||||
</Typography>
|
</Typography>
|
||||||
{!line.trackStock && (
|
</TableCell>
|
||||||
<Chip
|
<TableCell align="right" title={warehouseBreakdown || undefined}>
|
||||||
|
{isTracked ? (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
color: 'text.primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentStock != null ? formatStock(currentStock) : '0'}
|
||||||
|
</Typography>
|
||||||
|
) : targetSku ? (
|
||||||
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
label="Ohne Lagerbestand"
|
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{ fontSize: '0.7rem', height: 20, color: 'text.secondary', borderColor: 'divider' }}
|
color="primary"
|
||||||
/>
|
disabled={isBooking || isEnablingThis}
|
||||||
|
onClick={() => this.handleEnableTrackStock(line, targetSku)}
|
||||||
|
startIcon={
|
||||||
|
isEnablingThis ? (
|
||||||
|
<CircularProgress size={12} color="inherit" />
|
||||||
|
) : (
|
||||||
|
<AddIcon sx={{ fontSize: 13 }} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
height: 22,
|
||||||
|
fontSize: '0.68rem',
|
||||||
|
textTransform: 'none',
|
||||||
|
py: 0,
|
||||||
|
px: 0.75,
|
||||||
|
minWidth: 'auto',
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Aktivieren
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: '0.85rem',
|
||||||
|
color: 'text.disabled',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
—
|
||||||
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">
|
<TableCell align="right">
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
@@ -524,7 +741,7 @@ export default class BookDeliveryDialog extends PureComponent {
|
|||||||
<AppDialog
|
<AppDialog
|
||||||
open={Boolean(open)}
|
open={Boolean(open)}
|
||||||
onClose={isBooking ? undefined : onClose}
|
onClose={isBooking ? undefined : onClose}
|
||||||
maxWidth="md"
|
maxWidth="lg"
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
>
|
||||||
<DialogTitle
|
<DialogTitle
|
||||||
@@ -538,7 +755,7 @@ export default class BookDeliveryDialog extends PureComponent {
|
|||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||||
<Inventory2OutlinedIcon color="primary" />
|
<Inventory2OutlinedIcon color="primary" />
|
||||||
<Typography variant="h6" component="span" sx={{ fontWeight: 600 }}>
|
<Typography variant="h6" component="span" sx={{ fontWeight: 600 }}>
|
||||||
Lieferung {delivery?.number || ''} verbuchen
|
Lieferung verbuchen
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton size="small" onClick={onClose} disabled={isBooking}>
|
<IconButton size="small" onClick={onClose} disabled={isBooking}>
|
||||||
@@ -546,8 +763,8 @@ export default class BookDeliveryDialog extends PureComponent {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<DialogContent dividers sx={{ pt: 2 }}>
|
<DialogContent dividers sx={{ pt: 2, borderBottom: 'none' }}>
|
||||||
{this.renderHeaderInfo()}
|
{this.renderHeaderInfo({ hasTrackedLinesToBook, isWarehouseMissing })}
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6, gap: 2, alignItems: 'center' }}>
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 6, gap: 2, alignItems: 'center' }}>
|
||||||
@@ -568,125 +785,21 @@ export default class BookDeliveryDialog extends PureComponent {
|
|||||||
)}
|
)}
|
||||||
{this.renderLinesTable()}
|
{this.renderLinesTable()}
|
||||||
|
|
||||||
{!isFullyBooked && hasLines && (
|
|
||||||
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
onClick={this.handleBookAll}
|
|
||||||
disabled={isBooking}
|
|
||||||
>
|
|
||||||
Alle offenen Mengen übernehmen
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
variant="text"
|
|
||||||
color="inherit"
|
|
||||||
onClick={this.handleResetAmounts}
|
|
||||||
disabled={isBooking}
|
|
||||||
>
|
|
||||||
Zurücksetzen
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasTrackedLinesToBook && warehouses.length === 0 && !isFullyBooked && (
|
{hasTrackedLinesToBook && warehouses.length === 0 && !isFullyBooked && (
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
Es ist noch kein Lager eingerichtet. Für lagergeführte Artikel legen Sie bitte unter „Artikel → Lager“ zuerst ein Lager an.
|
Es ist noch kein Lager eingerichtet. Für lagergeführte Artikel legen Sie bitte unter „Artikel → Lager“ zuerst ein Lager an.
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasLines && (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
gap: 2,
|
|
||||||
mb: 2,
|
|
||||||
flexDirection: { xs: 'column', sm: 'row' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{hasTrackStock && (
|
|
||||||
<FormControl
|
|
||||||
size="small"
|
|
||||||
required={hasTrackedLinesToBook}
|
|
||||||
error={Boolean(warehouseError)}
|
|
||||||
disabled={isBooking || isFullyBooked}
|
|
||||||
sx={{ minWidth: { sm: 240 }, flex: { sm: '0 0 auto' } }}
|
|
||||||
>
|
|
||||||
<InputLabel id="warehouse-select-label">
|
|
||||||
Ziellager {hasTrackedLinesToBook ? '*' : ''}
|
|
||||||
</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="warehouse-select-label"
|
|
||||||
id="warehouse-select"
|
|
||||||
value={selectedWarehouseId ?? ''}
|
|
||||||
label={`Ziellager ${hasTrackedLinesToBook ? '*' : ''}`}
|
|
||||||
onChange={this.handleWarehouseChange}
|
|
||||||
startAdornment={
|
|
||||||
<WarehouseIcon
|
|
||||||
sx={{ mr: 1, color: 'text.secondary', fontSize: 20 }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{warehouses.length === 0 ? (
|
|
||||||
<MenuItem value="" disabled>
|
|
||||||
Keine Lager vorhanden
|
|
||||||
</MenuItem>
|
|
||||||
) : (
|
|
||||||
warehouses.map((wh) => (
|
|
||||||
<MenuItem key={wh.id} value={wh.id}>
|
|
||||||
{wh.name}
|
|
||||||
</MenuItem>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
{warehouseError ? (
|
|
||||||
<FormHelperText>{warehouseError}</FormHelperText>
|
|
||||||
) : (
|
|
||||||
<FormHelperText>
|
|
||||||
{hasTrackedLinesToBook
|
|
||||||
? 'Pflichtfeld: Ziellager für lagergeführte Artikel'
|
|
||||||
: 'Ziellager (optional)'}
|
|
||||||
</FormHelperText>
|
|
||||||
)}
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
size="small"
|
|
||||||
label="Buchungstext (Ledger-Referenz)"
|
|
||||||
value={transactionText}
|
|
||||||
onChange={this.handleTransactionTextChange}
|
|
||||||
disabled={isBooking || isFullyBooked}
|
|
||||||
helperText="Wird als Transaktionstext in das Ledger übernommen"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isFullyBooked ? (
|
{isFullyBooked ? (
|
||||||
<Alert severity="success" icon={<CheckCircleOutlineOutlinedIcon />}>
|
<Alert severity="success" icon={<CheckCircleOutlineOutlinedIcon />}>
|
||||||
Alle Positionen dieser Lieferung sind bereits vollständig im Ledger verbucht.
|
Alle Positionen dieser Lieferung sind bereits vollständig im Ledger verbucht.
|
||||||
</Alert>
|
</Alert>
|
||||||
) : countLinesToBook > 0 ? (
|
) : isWarehouseMissing ? (
|
||||||
<Alert severity={isWarehouseMissing ? 'warning' : 'info'}>
|
<Alert severity="warning">
|
||||||
{isWarehouseMissing ? (
|
|
||||||
<>
|
|
||||||
Bitte wählen Sie ein <strong>Ziellager</strong> aus, um die lagergeführten Artikel zu buchen.
|
Bitte wählen Sie ein <strong>Ziellager</strong> aus, um die lagergeführten Artikel zu buchen.
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
Es werden <strong>{totalToBook} Stück</strong> für{' '}
|
|
||||||
<strong>{countLinesToBook} Position{countLinesToBook === 1 ? '' : 'en'}</strong>{' '}
|
|
||||||
{hasTrackedLinesToBook && selectedWarehouse ? (
|
|
||||||
<>in das Lager <strong>„{selectedWarehouse?.name || '–'}“</strong> verbucht.</>
|
|
||||||
) : (
|
|
||||||
<>als Dokumentenbuchung im Ledger verbucht (ohne Lagerbestandsführung).</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Alert>
|
</Alert>
|
||||||
) : hasLines ? (
|
) : countLinesToBook === 0 && hasLines ? (
|
||||||
<Alert severity="warning">
|
<Alert severity="warning">
|
||||||
Wählen Sie mindestens eine Menge größer als 0 aus, um Positionen zu verbuchen.
|
Wählen Sie mindestens eine Menge größer als 0 aus, um Positionen zu verbuchen.
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|||||||
@@ -30,8 +30,15 @@ import { articleSupplierAutomat } from '../../automats/articleDetailAutomat';
|
|||||||
import { selectCompanyTaxrate } from '../../automats/companyAutomat';
|
import { selectCompanyTaxrate } from '../../automats/companyAutomat';
|
||||||
import {
|
import {
|
||||||
BORDER,
|
BORDER,
|
||||||
|
GREEN,
|
||||||
|
alpha,
|
||||||
} from '../../colors';
|
} from '../../colors';
|
||||||
|
|
||||||
|
const LOCKED_ROW_BG = alpha(GREEN, 0.08);
|
||||||
|
const LOCKED_ROW_HOVER_BG = alpha(GREEN, 0.12);
|
||||||
|
const LOCKED_CELL_BORDER = alpha(GREEN, 0.32);
|
||||||
|
const LOCKED_CELL_BG = alpha(GREEN, 0.08);
|
||||||
|
|
||||||
class ManifestFormLine extends PureComponent {
|
class ManifestFormLine extends PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@@ -60,6 +67,16 @@ class ManifestFormLine extends PureComponent {
|
|||||||
return props.line?.sku ? String(props.line.sku).trim() : null;
|
return props.line?.sku ? String(props.line.sku).trim() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isFullyLedgered(props = this.props) {
|
||||||
|
const { ledgerLine, line } = props;
|
||||||
|
return Boolean(
|
||||||
|
ledgerLine?.isBooked ||
|
||||||
|
(ledgerLine && ledgerLine.targetAmount > 0 && ledgerLine.remainingAmount <= 0) ||
|
||||||
|
line?.isBooked ||
|
||||||
|
line?.is_booked
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.subscribeArticle(this.getArticleSku());
|
this.subscribeArticle(this.getArticleSku());
|
||||||
}
|
}
|
||||||
@@ -125,7 +142,7 @@ class ManifestFormLine extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleRemove() {
|
handleRemove() {
|
||||||
if (this.props.isOnShipment) return;
|
if (this.props.isOnShipment || this.isFullyLedgered()) return;
|
||||||
this.props.onRemove(this.props.line.key);
|
this.props.onRemove(this.props.line.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +156,8 @@ class ManifestFormLine extends PureComponent {
|
|||||||
|
|
||||||
handleStartEdit(columnId) {
|
handleStartEdit(columnId) {
|
||||||
const { saving } = this.props;
|
const { saving } = this.props;
|
||||||
const isDisabled = saving || (columnId === 'sku' && Boolean(this.props.isOnShipment));
|
const isFullyLedgered = this.isFullyLedgered();
|
||||||
|
const isDisabled = saving || isFullyLedgered || (columnId === 'sku' && Boolean(this.props.isOnShipment));
|
||||||
if (isDisabled) return;
|
if (isDisabled) return;
|
||||||
if (this.state.editingField !== columnId) {
|
if (this.state.editingField !== columnId) {
|
||||||
this.setState({ editingField: columnId });
|
this.setState({ editingField: columnId });
|
||||||
@@ -162,6 +180,7 @@ class ManifestFormLine extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleCreateArticle() {
|
handleCreateArticle() {
|
||||||
|
if (this.isFullyLedgered()) return;
|
||||||
const { line, onCreateArticle } = this.props;
|
const { line, onCreateArticle } = this.props;
|
||||||
if (onCreateArticle) {
|
if (onCreateArticle) {
|
||||||
onCreateArticle(line);
|
onCreateArticle(line);
|
||||||
@@ -169,6 +188,7 @@ class ManifestFormLine extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleSearchArticle() {
|
handleSearchArticle() {
|
||||||
|
if (this.isFullyLedgered()) return;
|
||||||
const { line, onSearchArticle } = this.props;
|
const { line, onSearchArticle } = this.props;
|
||||||
if (onSearchArticle) {
|
if (onSearchArticle) {
|
||||||
onSearchArticle(line);
|
onSearchArticle(line);
|
||||||
@@ -176,105 +196,14 @@ class ManifestFormLine extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleRemoveSupplierArticle() {
|
handleRemoveSupplierArticle() {
|
||||||
|
if (this.isFullyLedgered()) return;
|
||||||
const { line, supplierArticle, onRemoveSupplierArticle } = this.props;
|
const { line, supplierArticle, onRemoveSupplierArticle } = this.props;
|
||||||
if (onRemoveSupplierArticle) {
|
if (onRemoveSupplierArticle) {
|
||||||
onRemoveSupplierArticle(line, supplierArticle);
|
onRemoveSupplierArticle(line, supplierArticle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLedgerChip() {
|
|
||||||
const { ledgerLine, onBookLine } = this.props;
|
|
||||||
if (!ledgerLine) return null;
|
|
||||||
const isBooked = ledgerLine.isBooked;
|
|
||||||
const isPartial = !isBooked && ledgerLine.bookedAmount > 0;
|
|
||||||
const isClickable = Boolean(onBookLine);
|
|
||||||
const isTracked = Boolean(ledgerLine.trackStock);
|
|
||||||
|
|
||||||
const tooltipTitle = isTracked
|
|
||||||
? isBooked
|
|
||||||
? `Vollständig im Lager gebucht: ${ledgerLine.bookedAmount} Stück (Klicken für Details)`
|
|
||||||
: isPartial
|
|
||||||
? `Teilweise im Lager gebucht: ${ledgerLine.bookedAmount} von ${ledgerLine.targetAmount} Stück (Klicken zum Einbuchen dieser Position)`
|
|
||||||
: `Noch nicht im Lager gebucht (${ledgerLine.targetAmount} Stück – Klicken zum Einbuchen dieser Position)`
|
|
||||||
: isBooked
|
|
||||||
? `Vollständig im Ledger verbucht: ${ledgerLine.bookedAmount} Stück (Klicken für Details)`
|
|
||||||
: isPartial
|
|
||||||
? `Teilweise im Ledger verbucht: ${ledgerLine.bookedAmount} von ${ledgerLine.targetAmount} Stück (Klicken zum Verbuchen dieser Position)`
|
|
||||||
: `Noch nicht im Ledger verbucht (${ledgerLine.targetAmount} Stück – Klicken zum Verbuchen dieser Position)`;
|
|
||||||
|
|
||||||
const label = isTracked
|
|
||||||
? isBooked
|
|
||||||
? `Lager: ${ledgerLine.bookedAmount} Stk. gebucht`
|
|
||||||
: isPartial
|
|
||||||
? `Lager: ${ledgerLine.bookedAmount}/${ledgerLine.targetAmount} Stk.`
|
|
||||||
: 'Lager: offen'
|
|
||||||
: isBooked
|
|
||||||
? `Gebucht: ${ledgerLine.bookedAmount} Stk.`
|
|
||||||
: isPartial
|
|
||||||
? `Gebucht: ${ledgerLine.bookedAmount}/${ledgerLine.targetAmount} Stk.`
|
|
||||||
: 'Verbuchen: offen';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip title={tooltipTitle}>
|
|
||||||
<Chip
|
|
||||||
size="small"
|
|
||||||
clickable={isClickable}
|
|
||||||
onClick={
|
|
||||||
isClickable
|
|
||||||
? (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onBookLine(ledgerLine);
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
icon={
|
|
||||||
isBooked ? (
|
|
||||||
<CheckCircleOutlineOutlinedIcon sx={{ '&&': { fontSize: 13, color: 'inherit' } }} />
|
|
||||||
) : (
|
|
||||||
<Inventory2OutlinedIcon sx={{ '&&': { fontSize: 13, color: 'inherit' } }} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
label={label}
|
|
||||||
variant={isBooked ? 'outlined' : 'filled'}
|
|
||||||
sx={{
|
|
||||||
height: 22,
|
|
||||||
fontSize: '0.7rem',
|
|
||||||
fontWeight: 700,
|
|
||||||
cursor: isClickable ? 'pointer' : 'default',
|
|
||||||
...(isBooked
|
|
||||||
? {
|
|
||||||
bgcolor: 'rgba(21, 128, 61, 0.08)',
|
|
||||||
color: '#15803d',
|
|
||||||
borderColor: '#15803d',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: 'solid',
|
|
||||||
'&:hover': isClickable ? { bgcolor: 'rgba(21, 128, 61, 0.16)' } : undefined,
|
|
||||||
}
|
|
||||||
: isPartial
|
|
||||||
? {
|
|
||||||
bgcolor: '#d97706',
|
|
||||||
color: '#ffffff',
|
|
||||||
border: 'none',
|
|
||||||
'&:hover': isClickable ? { bgcolor: '#b45309' } : undefined,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
bgcolor: 'primary.main',
|
|
||||||
color: '#ffffff',
|
|
||||||
border: 'none',
|
|
||||||
'&:hover': isClickable ? { bgcolor: 'primary.dark' } : undefined,
|
|
||||||
}),
|
|
||||||
'& .MuiChip-label': {
|
|
||||||
color: 'inherit',
|
|
||||||
px: 0.75,
|
|
||||||
},
|
|
||||||
'& .MuiChip-icon': {
|
|
||||||
color: 'inherit',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderField(column, colIdx, totalCols) {
|
renderField(column, colIdx, totalCols) {
|
||||||
const { id, align, type } = column;
|
const { id, align, type } = column;
|
||||||
@@ -363,8 +292,11 @@ class ManifestFormLine extends PureComponent {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isEditing = this.state.editingField === id;
|
const isEditing = this.state.editingField === id;
|
||||||
const isDisabled = saving || (id === 'sku' && Boolean(this.props.isOnShipment));
|
const isFullyLedgered = this.isFullyLedgered();
|
||||||
const titleText = id === 'sku' && this.props.isOnShipment
|
const isDisabled = saving || isFullyLedgered || (id === 'sku' && Boolean(this.props.isOnShipment));
|
||||||
|
const titleText = isFullyLedgered
|
||||||
|
? 'Position ist bereits vollständig im Ledger verbucht und kann nicht geändert werden'
|
||||||
|
: id === 'sku' && this.props.isOnShipment
|
||||||
? 'Artikel ist bereits auf einem Lieferschein und kann nicht geändert werden'
|
? 'Artikel ist bereits auf einem Lieferschein und kann nicht geändert werden'
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@@ -454,9 +386,10 @@ class ManifestFormLine extends PureComponent {
|
|||||||
<Box
|
<Box
|
||||||
tabIndex={isDisabled ? -1 : 0}
|
tabIndex={isDisabled ? -1 : 0}
|
||||||
role={isDisabled ? undefined : 'button'}
|
role={isDisabled ? undefined : 'button'}
|
||||||
onClick={() => this.handleStartEdit(id)}
|
onClick={isDisabled ? undefined : () => this.handleStartEdit(id)}
|
||||||
onFocus={() => this.handleStartEdit(id)}
|
onFocus={isDisabled ? undefined : () => this.handleStartEdit(id)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
|
if (isDisabled) return;
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.handleStartEdit(id);
|
this.handleStartEdit(id);
|
||||||
@@ -483,11 +416,16 @@ class ManifestFormLine extends PureComponent {
|
|||||||
? 'warning.main'
|
? 'warning.main'
|
||||||
: isError
|
: isError
|
||||||
? 'error.main'
|
? 'error.main'
|
||||||
|
: isFullyLedgered
|
||||||
|
? LOCKED_CELL_BORDER
|
||||||
: 'rgba(0, 0, 0, 0.23)',
|
: 'rgba(0, 0, 0, 0.23)',
|
||||||
cursor: isDisabled ? 'default' : 'text',
|
cursor: isDisabled ? 'default' : 'text',
|
||||||
touchAction: 'manipulation',
|
touchAction: 'manipulation',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
|
...(isFullyLedgered && !isFieldDirty && !isError && {
|
||||||
|
bgcolor: LOCKED_CELL_BG,
|
||||||
|
}),
|
||||||
...(isFieldDirty && {
|
...(isFieldDirty && {
|
||||||
bgcolor: 'rgba(237, 108, 2, 0.08)',
|
bgcolor: 'rgba(237, 108, 2, 0.08)',
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
@@ -941,13 +879,19 @@ class ManifestFormLine extends PureComponent {
|
|||||||
>
|
>
|
||||||
{/* Unlink button at the front */}
|
{/* Unlink button at the front */}
|
||||||
{this.props.onRemoveSupplierArticle && supplierArticle.id != null && (
|
{this.props.onRemoveSupplierArticle && supplierArticle.id != null && (
|
||||||
<Tooltip title="Lieferanten-Verknüpfung aufheben">
|
<Tooltip
|
||||||
|
title={
|
||||||
|
this.isFullyLedgered()
|
||||||
|
? 'Position ist bereits vollständig im Ledger verbucht und Verknüpfung kann nicht aufgehoben werden'
|
||||||
|
: 'Lieferanten-Verknüpfung aufheben'
|
||||||
|
}
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="error"
|
color="error"
|
||||||
aria-label="Lieferanten-Verknüpfung entfernen"
|
aria-label="Lieferanten-Verknüpfung entfernen"
|
||||||
disabled={this.props.saving || this.props.supplierArticleBusy}
|
disabled={this.props.saving || this.props.supplierArticleBusy || this.isFullyLedgered()}
|
||||||
onClick={this.handleRemoveSupplierArticle}
|
onClick={this.handleRemoveSupplierArticle}
|
||||||
sx={{ p: 0.25, mr: -0.25 }}
|
sx={{ p: 0.25, mr: -0.25 }}
|
||||||
>
|
>
|
||||||
@@ -1093,8 +1037,6 @@ class ManifestFormLine extends PureComponent {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{this.renderLedgerChip()}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1107,6 +1049,7 @@ class ManifestFormLine extends PureComponent {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="text"
|
variant="text"
|
||||||
startIcon={<AddIcon sx={{ fontSize: 14 }} />}
|
startIcon={<AddIcon sx={{ fontSize: 14 }} />}
|
||||||
|
disabled={this.isFullyLedgered()}
|
||||||
onClick={this.handleCreateArticle}
|
onClick={this.handleCreateArticle}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: '0.7rem',
|
fontSize: '0.7rem',
|
||||||
@@ -1129,6 +1072,7 @@ class ManifestFormLine extends PureComponent {
|
|||||||
size="small"
|
size="small"
|
||||||
variant="text"
|
variant="text"
|
||||||
startIcon={<SearchIcon sx={{ fontSize: 14 }} />}
|
startIcon={<SearchIcon sx={{ fontSize: 14 }} />}
|
||||||
|
disabled={this.isFullyLedgered()}
|
||||||
onClick={this.handleSearchArticle}
|
onClick={this.handleSearchArticle}
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: '0.7rem',
|
fontSize: '0.7rem',
|
||||||
@@ -1152,15 +1096,6 @@ class ManifestFormLine extends PureComponent {
|
|||||||
? `Kein Lieferanten-Artikel für SKU „${rawSku}“ gefunden.`
|
? `Kein Lieferanten-Artikel für SKU „${rawSku}“ gefunden.`
|
||||||
: `Kein Artikel für SKU „${rawSku}“ gefunden.`}
|
: `Kein Artikel für SKU „${rawSku}“ gefunden.`}
|
||||||
</Typography>
|
</Typography>
|
||||||
{this.renderLedgerChip()}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.ledgerLine) {
|
|
||||||
return (
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, py: 0.25 }}>
|
|
||||||
{this.renderLedgerChip()}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1170,12 +1105,24 @@ class ManifestFormLine extends PureComponent {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { columns = LINE_COLUMNS, saving, ledgerLine } = this.props;
|
const { columns = LINE_COLUMNS, saving, ledgerLine } = this.props;
|
||||||
|
const isFullyLedgered = this.isFullyLedgered();
|
||||||
const secondLine = this.renderSecondLine();
|
const secondLine = this.renderSecondLine();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{/* Row 1: Editable Line Inputs */}
|
{/* Row 1: Editable Line Inputs */}
|
||||||
<TableRow hover sx={{ '& > td': { borderBottom: 'none', p: 0 } }}>
|
<TableRow
|
||||||
|
hover
|
||||||
|
sx={{
|
||||||
|
'& > td': { borderBottom: 'none', p: 0 },
|
||||||
|
...(isFullyLedgered && {
|
||||||
|
bgcolor: LOCKED_ROW_BG,
|
||||||
|
'&:hover': {
|
||||||
|
bgcolor: LOCKED_ROW_HOVER_BG,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
{columns.map((col, colIdx) => (
|
{columns.map((col, colIdx) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
key={col.id}
|
key={col.id}
|
||||||
@@ -1202,19 +1149,13 @@ class ManifestFormLine extends PureComponent {
|
|||||||
verticalAlign: 'middle',
|
verticalAlign: 'middle',
|
||||||
width: (this.props.onMove ? 80 : 36) + (this.props.kind === 'delivery' && Boolean(this.props.onBookLine) ? 36 : 0),
|
width: (this.props.onMove ? 80 : 36) + (this.props.kind === 'delivery' && Boolean(this.props.onBookLine) ? 36 : 0),
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
|
...(isFullyLedgered && {
|
||||||
|
bgcolor: LOCKED_ROW_BG,
|
||||||
|
}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.25 }}>
|
||||||
{this.props.kind === 'delivery' && Boolean(this.props.onBookLine) && (
|
{this.props.kind === 'delivery' && Boolean(this.props.onBookLine) && (
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
!ledgerLine
|
|
||||||
? 'Lieferung zuerst speichern, um diese Position zu verbuchen'
|
|
||||||
: ledgerLine.isBooked
|
|
||||||
? 'Position bereits verbucht (Klicken zum Einsehen)'
|
|
||||||
: `Position verbuchen (${ledgerLine.remainingAmount > 0 ? `${ledgerLine.remainingAmount} Stk.` : ''})`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
@@ -1244,7 +1185,6 @@ class ManifestFormLine extends PureComponent {
|
|||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
|
||||||
)}
|
)}
|
||||||
{this.props.onMove && (
|
{this.props.onMove && (
|
||||||
<>
|
<>
|
||||||
@@ -1275,9 +1215,15 @@ class ManifestFormLine extends PureComponent {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
aria-label="Zeile entfernen"
|
aria-label="Zeile entfernen"
|
||||||
disabled={saving || Boolean(this.props.isOnShipment)}
|
disabled={saving || Boolean(this.props.isOnShipment) || isFullyLedgered}
|
||||||
onClick={this.handleRemove}
|
onClick={this.handleRemove}
|
||||||
title={this.props.isOnShipment ? 'Artikel ist bereits auf einem Lieferschein und kann nicht entfernt werden' : 'Zeile entfernen'}
|
title={
|
||||||
|
isFullyLedgered
|
||||||
|
? 'Position ist bereits vollständig im Ledger verbucht und kann nicht entfernt werden'
|
||||||
|
: this.props.isOnShipment
|
||||||
|
? 'Artikel ist bereits auf einem Lieferschein und kann nicht entfernt werden'
|
||||||
|
: 'Zeile entfernen'
|
||||||
|
}
|
||||||
sx={{ p: 0.35 }}
|
sx={{ p: 0.35 }}
|
||||||
>
|
>
|
||||||
<DeleteOutlinedIcon sx={{ fontSize: 18 }} />
|
<DeleteOutlinedIcon sx={{ fontSize: 18 }} />
|
||||||
@@ -1288,7 +1234,14 @@ class ManifestFormLine extends PureComponent {
|
|||||||
|
|
||||||
{/* Row 2: Matched Supplier Article Details */}
|
{/* Row 2: Matched Supplier Article Details */}
|
||||||
{secondLine && (
|
{secondLine && (
|
||||||
<TableRow sx={{ '& > td': { pt: 0.25, pb: 0.75, borderBottom: 'none' } }}>
|
<TableRow
|
||||||
|
sx={{
|
||||||
|
'& > td': { pt: 0.25, pb: 0.75, borderBottom: 'none' },
|
||||||
|
...(isFullyLedgered && {
|
||||||
|
bgcolor: LOCKED_ROW_BG,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={columns.length + 1}
|
colSpan={columns.length + 1}
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import Button from '@mui/material/Button';
|
|||||||
import CircularProgress from '@mui/material/CircularProgress';
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Chip from '@mui/material/Chip';
|
|
||||||
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
import UploadFileIcon from '@mui/icons-material/UploadFile';
|
||||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||||
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
|
||||||
@@ -277,8 +276,8 @@ class ManifestHeader extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Tile 1: 3-wide Kopfdaten Tile */}
|
{/* Tile 1: 3-wide Tile */}
|
||||||
<PanelTile wide={3} minHeight={160} color={headerColor} title="Kopfdaten">
|
<PanelTile wide={3} minHeight={160} color={headerColor}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -410,42 +409,6 @@ class ManifestHeader extends PureComponent {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{docId != null && kind === 'delivery' && ledgerSummary && (
|
|
||||||
<Chip
|
|
||||||
size="small"
|
|
||||||
icon={
|
|
||||||
ledgerSummary.status === 'booked' ? (
|
|
||||||
<CheckCircleOutlineOutlinedIcon sx={{ fontSize: 16 }} />
|
|
||||||
) : (
|
|
||||||
<Inventory2OutlinedIcon sx={{ fontSize: 16 }} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
label={
|
|
||||||
ledgerSummary.status === 'booked'
|
|
||||||
? `Lager: Gebucht (${ledgerSummary.bookedPositions}/${ledgerSummary.totalPositions})`
|
|
||||||
: ledgerSummary.status === 'partial'
|
|
||||||
? `Lager: Teilweise (${ledgerSummary.bookedPositions}/${ledgerSummary.totalPositions})`
|
|
||||||
: ledgerSummary.status === 'empty'
|
|
||||||
? 'Lager: Keine Pos.'
|
|
||||||
: `Lager: Offen (${ledgerSummary.totalPositions} Pos.)`
|
|
||||||
}
|
|
||||||
color={
|
|
||||||
ledgerSummary.status === 'booked'
|
|
||||||
? 'success'
|
|
||||||
: ledgerSummary.status === 'partial'
|
|
||||||
? 'warning'
|
|
||||||
: 'default'
|
|
||||||
}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={onOpenBookDialog}
|
|
||||||
sx={{
|
|
||||||
cursor: onOpenBookDialog ? 'pointer' : 'default',
|
|
||||||
fontWeight: 600,
|
|
||||||
height: 32,
|
|
||||||
px: 0.5,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
<AttachmentList
|
<AttachmentList
|
||||||
kind={kind}
|
kind={kind}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class ManifestLines extends PureComponent {
|
|||||||
kind = 'delivery',
|
kind = 'delivery',
|
||||||
lines,
|
lines,
|
||||||
saving,
|
saving,
|
||||||
|
partyId,
|
||||||
linesColor,
|
linesColor,
|
||||||
dirtyLineFields = {},
|
dirtyLineFields = {},
|
||||||
shipments,
|
shipments,
|
||||||
@@ -68,7 +69,6 @@ class ManifestLines extends PureComponent {
|
|||||||
wide={wide}
|
wide={wide}
|
||||||
minHeight={280}
|
minHeight={280}
|
||||||
color={linesColor}
|
color={linesColor}
|
||||||
title="Artikel"
|
|
||||||
sx={{
|
sx={{
|
||||||
...(wide >= 4 ? { gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' } } : {}),
|
...(wide >= 4 ? { gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' } } : {}),
|
||||||
...(!hasArticles
|
...(!hasArticles
|
||||||
@@ -115,17 +115,7 @@ class ManifestLines extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!lines || lines.length === 0) {
|
if (!lines || lines.length === 0) {
|
||||||
return (
|
return null;
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={columns.length + 1}
|
|
||||||
align="center"
|
|
||||||
sx={{ py: 3, color: 'text.secondary', fontSize: '0.82rem' }}
|
|
||||||
>
|
|
||||||
Keine Positionen vorhanden
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines.map((line, index) => {
|
return lines.map((line, index) => {
|
||||||
@@ -190,25 +180,49 @@ class ManifestLines extends PureComponent {
|
|||||||
>
|
>
|
||||||
Zeile
|
Zeile
|
||||||
</Button>
|
</Button>
|
||||||
{onSearchArticle && (
|
{onSearchArticle && (() => {
|
||||||
|
const searchArticleDisabled = saving || (kind === 'delivery' && !partyId);
|
||||||
|
const searchArticleTitle =
|
||||||
|
kind === 'delivery' && !partyId
|
||||||
|
? 'Bitte zuerst einen Lieferanten auswählen'
|
||||||
|
: undefined;
|
||||||
|
return (
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
startIcon={<SearchIcon />}
|
startIcon={<SearchIcon />}
|
||||||
disabled={saving}
|
disabled={searchArticleDisabled}
|
||||||
onClick={() => onSearchArticle(null)}
|
onClick={() => onSearchArticle(null)}
|
||||||
|
title={searchArticleTitle}
|
||||||
>
|
>
|
||||||
Artikel suchen
|
Artikel suchen
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
);
|
||||||
|
})()}
|
||||||
{(() => {
|
{(() => {
|
||||||
const hasShippedLines = kind === 'order' && Array.isArray(shipments) && shipments.some((sh) => (sh.lines?.length || 0) > 0);
|
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'
|
||||||
|
: 'Alle Zeilen entfernen';
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
startIcon={<DeleteSweepOutlinedIcon />}
|
startIcon={<DeleteSweepOutlinedIcon />}
|
||||||
disabled={saving || !lines || lines.length === 0 || hasShippedLines}
|
disabled={disabled}
|
||||||
onClick={onRemoveAllLines}
|
onClick={onRemoveAllLines}
|
||||||
title={hasShippedLines ? 'Auftrag enthält bereits ausgelieferte Positionen' : 'Alle Zeilen entfernen'}
|
title={title}
|
||||||
>
|
>
|
||||||
Alle Zeilen entfernen
|
Alle Zeilen entfernen
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -707,3 +707,67 @@ export function manifestConfig(cfgOrKind) {
|
|||||||
}
|
}
|
||||||
throw new Error(`Unknown manifest kind: ${kind}`);
|
throw new Error(`Unknown manifest kind: ${kind}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves supplier SKU and own SKU for a manifest line.
|
||||||
|
*/
|
||||||
|
export function resolveLineSkus(line, delivery, supplierArticlesMap = {}) {
|
||||||
|
const rawSku = line?.sku ? String(line.sku).trim() : '';
|
||||||
|
const supplierId = delivery?.supplierId ?? delivery?.partyId;
|
||||||
|
|
||||||
|
// 1. Direct line properties if available
|
||||||
|
let supplierSku =
|
||||||
|
line?.supplierSku ||
|
||||||
|
line?.supplier_sku ||
|
||||||
|
line?.supplierArticle?.sku ||
|
||||||
|
'';
|
||||||
|
let ownSku =
|
||||||
|
line?.articleSku ||
|
||||||
|
line?.article_sku ||
|
||||||
|
line?.article?.sku ||
|
||||||
|
line?.supplierArticle?.article?.sku ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
// 2. Check supplierArticlesMap
|
||||||
|
const sa = rawSku
|
||||||
|
? (supplierArticlesMap[rawSku] || supplierArticlesMap[rawSku.toLowerCase()])
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (sa) {
|
||||||
|
if (!supplierSku && sa.sku) supplierSku = sa.sku;
|
||||||
|
if (!ownSku && sa.article?.sku) ownSku = sa.article.sku;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check loaded article details
|
||||||
|
const cachedArticle =
|
||||||
|
line?.article ||
|
||||||
|
(ownSku ? getArticle(ownSku) : null) ||
|
||||||
|
(rawSku ? getArticle(rawSku) : null);
|
||||||
|
|
||||||
|
if (cachedArticle) {
|
||||||
|
if (!ownSku && cachedArticle.sku) {
|
||||||
|
ownSku = cachedArticle.sku;
|
||||||
|
}
|
||||||
|
const sup =
|
||||||
|
(supplierId && cachedArticle.suppliers?.find((s) => s.entityId === supplierId)) ||
|
||||||
|
cachedArticle.suppliers?.[0];
|
||||||
|
if (!supplierSku && sup?.sku) {
|
||||||
|
supplierSku = sup.sku;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fallback heuristics if one is still missing
|
||||||
|
if (!supplierSku && !ownSku) {
|
||||||
|
ownSku = rawSku;
|
||||||
|
} else if (!supplierSku && rawSku && rawSku !== ownSku) {
|
||||||
|
supplierSku = rawSku;
|
||||||
|
} else if (!ownSku && rawSku && rawSku !== supplierSku) {
|
||||||
|
ownSku = rawSku;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
supplierSku: supplierSku || '–',
|
||||||
|
ownSku: ownSku || '–',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ import {
|
|||||||
ArticleSearchDialog,
|
ArticleSearchDialog,
|
||||||
BookDeliveryDialog,
|
BookDeliveryDialog,
|
||||||
} from '../../components/manifest';
|
} from '../../components/manifest';
|
||||||
import { loadParties, apiFor } from '../../components/manifest/manifestConfig';
|
import { loadParties, apiFor, resolveLineSkus } from '../../components/manifest/manifestConfig';
|
||||||
|
import warehouseAutomat from '../../automats/warehouseAutomat';
|
||||||
import { article as articleApi, delivery as deliveryApi } from '../../api';
|
import { article as articleApi, delivery as deliveryApi } from '../../api';
|
||||||
import { getCategoryState } from '../../automats/categoryAutomat';
|
import { getCategoryState } from '../../automats/categoryAutomat';
|
||||||
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
import { requireArray, requireDefined, requireObject } from '../../utils/apiShape';
|
||||||
@@ -79,6 +80,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
ledgerSummary: null,
|
ledgerSummary: null,
|
||||||
bookDeliveryDialogOpen: false,
|
bookDeliveryDialogOpen: false,
|
||||||
bookDeliveryPrefillLineId: null,
|
bookDeliveryPrefillLineId: null,
|
||||||
|
warehouses: warehouseAutomat.state.warehouses || [],
|
||||||
};
|
};
|
||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
this.handleRetry = this.handleRetry.bind(this);
|
this.handleRetry = this.handleRetry.bind(this);
|
||||||
@@ -123,6 +125,10 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
this._settingsUnsub = userSettingsAutomat.subscribe(this, (state) => ({
|
this._settingsUnsub = userSettingsAutomat.subscribe(this, (state) => ({
|
||||||
viewerPosition: state.viewerPosition,
|
viewerPosition: state.viewerPosition,
|
||||||
}));
|
}));
|
||||||
|
this._warehouseUnsub = warehouseAutomat.subscribe(this, (state) => ({
|
||||||
|
warehouses: state.warehouses,
|
||||||
|
}));
|
||||||
|
void warehouseAutomat.actions.load().catch(() => {});
|
||||||
this._cancelled = false;
|
this._cancelled = false;
|
||||||
if (this.props.docId != null) {
|
if (this.props.docId != null) {
|
||||||
setLastDeliveryPath(`/lieferung/${this.props.docId}`);
|
setLastDeliveryPath(`/lieferung/${this.props.docId}`);
|
||||||
@@ -161,6 +167,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
this.unsubscribe?.();
|
this.unsubscribe?.();
|
||||||
this._settingsUnsub?.();
|
this._settingsUnsub?.();
|
||||||
|
this._warehouseUnsub?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPartiesOnly() {
|
async loadPartiesOnly() {
|
||||||
@@ -431,18 +438,67 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
async handleAssignArticleForLine(article, line) {
|
async handleAssignArticleForLine(article, line) {
|
||||||
const { partyId } = this.state;
|
const { partyId } = this.state;
|
||||||
if (!partyId) {
|
if (!partyId) {
|
||||||
enqueueSnackbar('Bitte wählen Sie zuerst einen Lieferanten aus.', { variant: 'warning' });
|
const msg = 'Bitte wählen Sie zuerst einen Lieferanten aus.';
|
||||||
return;
|
enqueueSnackbar(msg, { variant: 'warning' });
|
||||||
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
const internalSku = article?.sku;
|
const internalSku = article?.sku;
|
||||||
if (!internalSku) {
|
if (!internalSku) {
|
||||||
enqueueSnackbar('Artikel hat keine SKU.', { variant: 'error' });
|
const msg = 'Artikel hat keine SKU.';
|
||||||
return;
|
enqueueSnackbar(msg, { variant: 'error' });
|
||||||
|
throw new Error(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
const supplierSku = line?.sku;
|
const supplierSku = line?.sku;
|
||||||
|
|
||||||
|
if (!line) {
|
||||||
|
const supplierArticle =
|
||||||
|
this.state.supplierArticlesMap?.[internalSku.toLowerCase()] ||
|
||||||
|
this.state.supplierArticlesMap?.[internalSku] ||
|
||||||
|
this.state.supplierArticles?.find(
|
||||||
|
(sa) => sa.article?.sku === internalSku || sa.sku === internalSku
|
||||||
|
);
|
||||||
|
|
||||||
|
const supplierSkuVal = supplierArticle?.sku || '';
|
||||||
|
const netVal =
|
||||||
|
supplierArticle?.price?.net != null
|
||||||
|
? String(supplierArticle.price.net)
|
||||||
|
: article.price != null
|
||||||
|
? String(article.price)
|
||||||
|
: article.net != null
|
||||||
|
? String(article.net)
|
||||||
|
: '';
|
||||||
|
const taxVal =
|
||||||
|
supplierArticle?.price?.taxRate != null
|
||||||
|
? String(supplierArticle.price.taxRate)
|
||||||
|
: article.taxrate != null
|
||||||
|
? String(article.taxrate)
|
||||||
|
: article.tax != null
|
||||||
|
? String(article.tax)
|
||||||
|
: '';
|
||||||
|
const puVal = supplierArticle?.pu != null ? Number(supplierArticle.pu) : 1;
|
||||||
|
|
||||||
|
this.automat.actions.addLineitem({
|
||||||
|
amount: 1,
|
||||||
|
sku: supplierSkuVal || internalSku,
|
||||||
|
name: article.name || supplierArticle?.article?.name || '',
|
||||||
|
tax: taxVal,
|
||||||
|
taxrate: taxVal,
|
||||||
|
net: netVal,
|
||||||
|
netprice: netVal,
|
||||||
|
pu: puVal,
|
||||||
|
});
|
||||||
|
|
||||||
|
enqueueSnackbar(`Artikel "${internalSku}" hinzugefügt.`, { variant: 'success' });
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
articleSearchDialogOpen: false,
|
||||||
|
articleSearchLine: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await articleApi.addSupplier(internalSku, {
|
await articleApi.addSupplier(internalSku, {
|
||||||
entityId: partyId,
|
entityId: partyId,
|
||||||
@@ -454,9 +510,15 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (supplierSku) {
|
if (supplierSku) {
|
||||||
this.automat.actions.fetchSupplierArticleForSku(supplierSku, line.key);
|
await this.automat.actions.fetchSupplierArticleForSku(supplierSku, line.key);
|
||||||
} else {
|
}
|
||||||
this.automat.actions.fetchSupplierArticles(partyId);
|
await this.automat.actions.fetchSupplierArticles(partyId);
|
||||||
|
|
||||||
|
if (!line.name && article.name) {
|
||||||
|
this.automat.actions.updateLineitem({
|
||||||
|
...line,
|
||||||
|
name: article.name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
enqueueSnackbar(
|
enqueueSnackbar(
|
||||||
@@ -1131,6 +1193,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
columns={DELIVERY_LINE_COLUMNS}
|
columns={DELIVERY_LINE_COLUMNS}
|
||||||
lines={lines}
|
lines={lines}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
|
partyId={this.state.partyId}
|
||||||
wide={
|
wide={
|
||||||
this.state.viewerPosition === 'right' && this.isViewerOpen()
|
this.state.viewerPosition === 'right' && this.isViewerOpen()
|
||||||
? 2
|
? 2
|
||||||
@@ -1180,6 +1243,8 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
open={this.state.articleSearchDialogOpen}
|
open={this.state.articleSearchDialogOpen}
|
||||||
line={this.state.articleSearchLine}
|
line={this.state.articleSearchLine}
|
||||||
kind="delivery"
|
kind="delivery"
|
||||||
|
partyId={this.state.partyId}
|
||||||
|
supplierArticles={this.state.supplierArticles}
|
||||||
onClose={this.handleCloseArticleSearch}
|
onClose={this.handleCloseArticleSearch}
|
||||||
onAssign={this.handleAssignArticleForLine}
|
onAssign={this.handleAssignArticleForLine}
|
||||||
/>
|
/>
|
||||||
@@ -1197,6 +1262,7 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
summary={this.state.ledgerSummary}
|
summary={this.state.ledgerSummary}
|
||||||
prefillLineId={this.state.bookDeliveryPrefillLineId}
|
prefillLineId={this.state.bookDeliveryPrefillLineId}
|
||||||
partyName={this.state.deliveryEntities?.find((p) => p.id === this.state.partyId)?.name}
|
partyName={this.state.deliveryEntities?.find((p) => p.id === this.state.partyId)?.name}
|
||||||
|
supplierArticlesMap={this.state.supplierArticlesMap}
|
||||||
onClose={this.handleCloseBookDialog}
|
onClose={this.handleCloseBookDialog}
|
||||||
onBookSuccess={this.handleBookSuccess}
|
onBookSuccess={this.handleBookSuccess}
|
||||||
/>
|
/>
|
||||||
@@ -1206,27 +1272,42 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderLedgerPanel() {
|
renderLedgerPanel() {
|
||||||
const { ledgerSummary } = this.state;
|
const { ledgerSummary, warehouses = [] } = this.state;
|
||||||
if (!ledgerSummary) return null;
|
if (!ledgerSummary) return null;
|
||||||
|
|
||||||
const {
|
const { lines } = ledgerSummary;
|
||||||
status,
|
const delivery = this.state.delivery || this.automat?.state?.delivery;
|
||||||
totalPositions,
|
const supplierArticlesMap = this.state.supplierArticlesMap || {};
|
||||||
bookedPositions,
|
|
||||||
totalQuantity,
|
|
||||||
bookedQuantity,
|
|
||||||
lines,
|
|
||||||
} = ledgerSummary;
|
|
||||||
|
|
||||||
const allEntries = [];
|
const allEntries = [];
|
||||||
if (Array.isArray(lines)) {
|
if (Array.isArray(lines)) {
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (Array.isArray(line.ledgerEntries)) {
|
if (Array.isArray(line.ledgerEntries)) {
|
||||||
|
const { supplierSku, ownSku } = resolveLineSkus(line, delivery, supplierArticlesMap);
|
||||||
|
const lineName = line.name || line.article?.name || line.description || '–';
|
||||||
|
const deliveredAmount = line.amount ?? line.count ?? line.targetAmount ?? '–';
|
||||||
|
|
||||||
for (const entry of line.ledgerEntries) {
|
for (const entry of line.ledgerEntries) {
|
||||||
|
const hasWarehouse = entry.warehouse_id != null;
|
||||||
|
const wh = hasWarehouse
|
||||||
|
? (warehouses.find((w) => Number(w.id) === Number(entry.warehouse_id)) ||
|
||||||
|
warehouseAutomat.state.warehouses?.find((w) => Number(w.id) === Number(entry.warehouse_id)))
|
||||||
|
: null;
|
||||||
|
const warehouseName = wh?.name || (hasWarehouse ? `Lager #${entry.warehouse_id}` : '–');
|
||||||
|
|
||||||
|
const rawBookedAmount = entry.warehouse_amount ?? (hasWarehouse ? entry.amount : null);
|
||||||
|
const mengeEingebucht = rawBookedAmount != null
|
||||||
|
? (Number(rawBookedAmount) > 0 ? `+${rawBookedAmount}` : String(rawBookedAmount))
|
||||||
|
: '–';
|
||||||
|
|
||||||
allEntries.push({
|
allEntries.push({
|
||||||
...entry,
|
...entry,
|
||||||
lineSku: line.sku,
|
supplierSku,
|
||||||
lineName: line.name,
|
ownSku,
|
||||||
|
name: lineName,
|
||||||
|
deliveredAmount,
|
||||||
|
warehouseName,
|
||||||
|
mengeEingebucht,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1234,140 +1315,29 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
}
|
}
|
||||||
allEntries.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
|
allEntries.sort((a, b) => String(b.created_at || '').localeCompare(String(a.created_at || '')));
|
||||||
|
|
||||||
if (!ledgerSummary.hasTrackStock && allEntries.length === 0) return null;
|
if (allEntries.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelTile
|
<PanelTile
|
||||||
wide={4}
|
wide={4}
|
||||||
title="Lagerstatus (Ledger)"
|
title="Buchungen"
|
||||||
color={SURFACE_BASE}
|
color={SURFACE_BASE}
|
||||||
sx={{
|
sx={{
|
||||||
gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' },
|
gridColumn: { xs: '1 / -1', sm: '1 / -1', md: 'span 4' },
|
||||||
border: '1px solid',
|
|
||||||
borderColor:
|
|
||||||
status === 'booked'
|
|
||||||
? 'success.light'
|
|
||||||
: status === 'partial'
|
|
||||||
? 'warning.light'
|
|
||||||
: 'divider',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack spacing={2}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'flex',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
gap: 3,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 3, alignItems: 'center' }}>
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary" display="block">
|
|
||||||
Status
|
|
||||||
</Typography>
|
|
||||||
{status === 'booked' && (
|
|
||||||
<Chip
|
|
||||||
size="small"
|
|
||||||
icon={<CheckCircleOutlineOutlinedIcon />}
|
|
||||||
label="Vollständig gebucht"
|
|
||||||
color="success"
|
|
||||||
variant="outlined"
|
|
||||||
sx={{ fontWeight: 600, height: 26 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{status === 'partial' && (
|
|
||||||
<Chip
|
|
||||||
size="small"
|
|
||||||
icon={<Inventory2OutlinedIcon />}
|
|
||||||
label={`Teilweise gebucht (${bookedPositions}/${totalPositions})`}
|
|
||||||
color="warning"
|
|
||||||
variant="outlined"
|
|
||||||
sx={{ fontWeight: 600, height: 26 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{status === 'unbooked' && (
|
|
||||||
<Chip
|
|
||||||
size="small"
|
|
||||||
label={`Offen (${totalPositions} Positionen)`}
|
|
||||||
variant="outlined"
|
|
||||||
sx={{ color: 'text.secondary', borderColor: 'divider', fontWeight: 500, height: 26 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{status === 'empty' && (
|
|
||||||
<Chip size="small" label="Keine Artikel" variant="outlined" />
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary" display="block">
|
|
||||||
Positionen
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
|
||||||
{bookedPositions} / {totalPositions} gebucht
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary" display="block">
|
|
||||||
Menge
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
|
||||||
{bookedQuantity} / {totalQuantity} Stk. gebucht
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{totalQuantity > bookedQuantity && (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" color="text.secondary" display="block">
|
|
||||||
Noch offen
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600, color: 'warning.dark' }}>
|
|
||||||
{totalQuantity - bookedQuantity} Stk.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{ledgerSummary.hasTrackStock && (
|
|
||||||
<Button
|
|
||||||
variant={status === 'booked' ? 'outlined' : 'contained'}
|
|
||||||
color={status === 'booked' ? 'success' : status === 'partial' ? 'warning' : 'primary'}
|
|
||||||
startIcon={
|
|
||||||
status === 'booked' ? (
|
|
||||||
<CheckCircleOutlineOutlinedIcon />
|
|
||||||
) : (
|
|
||||||
<Inventory2OutlinedIcon />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onClick={this.handleOpenBookDialog}
|
|
||||||
sx={{ fontWeight: 600, textTransform: 'none' }}
|
|
||||||
>
|
|
||||||
{status === 'booked'
|
|
||||||
? 'Buchungsdetails anzeigen'
|
|
||||||
: status === 'partial'
|
|
||||||
? 'Restliche Mengen einbuchen'
|
|
||||||
: 'Ins Lager buchen'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{allEntries.length > 0 && (
|
|
||||||
<Box>
|
|
||||||
<Typography variant="caption" sx={{ fontWeight: 700, color: 'text.secondary', mb: 1, display: 'block' }}>
|
|
||||||
Bisherige Ledger-Buchungen ({allEntries.length}):
|
|
||||||
</Typography>
|
|
||||||
<TableContainer component={Paper} variant="outlined">
|
<TableContainer component={Paper} variant="outlined">
|
||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableHead sx={{ bgcolor: SURFACE_RAISED }}>
|
<TableHead sx={{ bgcolor: SURFACE_RAISED }}>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Zeitpunkt</TableCell>
|
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Zeitpunkt</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Typ</TableCell>
|
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lieferanten SKU</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>SKU / Artikel</TableCell>
|
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Eigene SKU</TableCell>
|
||||||
<TableCell align="right" sx={{ fontWeight: 600, py: 0.5 }}>Menge</TableCell>
|
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Name</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Buchungstext</TableCell>
|
<TableCell align="right" sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Menge geliefert</TableCell>
|
||||||
<TableCell sx={{ fontWeight: 600, py: 0.5, fontFamily: 'monospace', fontSize: '0.75rem' }}>Prüfsumme (prev_hash)</TableCell>
|
<TableCell sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Lager</TableCell>
|
||||||
|
<TableCell align="right" sx={{ fontWeight: 600, py: 0.5, whiteSpace: 'nowrap' }}>Menge eingebucht</TableCell>
|
||||||
|
<TableCell sx={{ fontWeight: 600, py: 0.5 }}>Referenz</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -1378,46 +1348,52 @@ class LieferungNeuPage extends PureComponent {
|
|||||||
{entry.created_at ? new Date(entry.created_at).toLocaleString('de-DE') : '–'}
|
{entry.created_at ? new Date(entry.created_at).toLocaleString('de-DE') : '–'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell sx={{ py: 0.5 }}>
|
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||||
<Chip
|
<Typography variant="caption" sx={{ fontFamily: 'monospace' }}>
|
||||||
size="small"
|
{entry.supplierSku}
|
||||||
label={entry.transaction_type}
|
</Typography>
|
||||||
color="success"
|
</TableCell>
|
||||||
variant="outlined"
|
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||||
sx={{ height: 20, fontSize: '0.68rem', fontWeight: 600 }}
|
<Typography variant="caption" sx={{ fontFamily: 'monospace', fontWeight: 600 }}>
|
||||||
/>
|
{entry.ownSku}
|
||||||
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell sx={{ py: 0.5 }}>
|
<TableCell sx={{ py: 0.5 }}>
|
||||||
<Typography variant="caption" sx={{ fontWeight: 600, fontFamily: 'monospace' }}>
|
<Typography variant="caption">
|
||||||
{entry.lineSku || entry.sku || '–'}
|
{entry.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
{entry.lineName && (
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 1 }}>
|
|
||||||
{entry.lineName}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right" sx={{ py: 0.5, fontWeight: 700, color: 'success.main' }}>
|
<TableCell align="right" sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||||
+{entry.amount}
|
<Typography variant="caption">
|
||||||
|
{entry.deliveredAmount}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ py: 0.5, whiteSpace: 'nowrap' }}>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{entry.warehouseName}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
align="right"
|
||||||
|
sx={{
|
||||||
|
py: 0.5,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: entry.mengeEingebucht !== '–' ? 'success.main' : 'text.disabled',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{entry.mengeEingebucht}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell sx={{ py: 0.5 }}>
|
<TableCell sx={{ py: 0.5 }}>
|
||||||
<Typography variant="caption">
|
<Typography variant="caption">
|
||||||
{entry.transaction_text || '–'}
|
{entry.transaction_text || '–'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell sx={{ py: 0.5, fontFamily: 'monospace', fontSize: '0.7rem', color: 'text.secondary' }}>
|
|
||||||
<Tooltip title={entry.prev_hash}>
|
|
||||||
<span>{entry.prev_hash ? `${entry.prev_hash.slice(0, 16)}…` : '–'}</span>
|
|
||||||
</Tooltip>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</PanelTile>
|
</PanelTile>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,10 +118,41 @@ export async function fetchDeliveryLedgerSummary(deliveryId, { deliveryApi = def
|
|||||||
|
|
||||||
const trackStock = isLineStockTracked(line, article);
|
const trackStock = isLineStockTracked(line, article);
|
||||||
|
|
||||||
const deliveryEntries = ledgerEntries.filter((e) => {
|
const acceptanceEntries = ledgerEntries.filter((e) => {
|
||||||
const t = String(e.transaction_type).toUpperCase();
|
const t = String(e.transaction_type).toUpperCase();
|
||||||
return t === 'ACCEPTANCE' || t === 'RETURN' || t === 'DELIVERY';
|
return t === 'ACCEPTANCE' || t === 'RETURN' || t === 'DELIVERY';
|
||||||
});
|
});
|
||||||
|
const warehouseEntries = ledgerEntries.filter((e) => {
|
||||||
|
const t = String(e.transaction_type).toUpperCase();
|
||||||
|
return t === 'WAREHOUSE';
|
||||||
|
});
|
||||||
|
|
||||||
|
let deliveryEntries;
|
||||||
|
if (acceptanceEntries.length > 0) {
|
||||||
|
deliveryEntries = acceptanceEntries.map((acc, idx) => {
|
||||||
|
const matchingWh =
|
||||||
|
warehouseEntries.find(
|
||||||
|
(w) =>
|
||||||
|
w.warehouse_id != null &&
|
||||||
|
(w.transaction_text === acc.transaction_text ||
|
||||||
|
Math.abs(new Date(w.created_at) - new Date(acc.created_at)) < 15000)
|
||||||
|
) || warehouseEntries[idx] || null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
warehouse_id: matchingWh?.warehouse_id ?? acc.warehouse_id ?? null,
|
||||||
|
warehouse_amount: matchingWh?.amount ?? null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (warehouseEntries.length > 0) {
|
||||||
|
deliveryEntries = warehouseEntries.map((w) => ({
|
||||||
|
...w,
|
||||||
|
warehouse_id: w.warehouse_id ?? null,
|
||||||
|
warehouse_amount: w.amount ?? null,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
deliveryEntries = [];
|
||||||
|
}
|
||||||
const bookedAmount = deliveryEntries.reduce((sum, e) => sum + Number(e.amount || 0), 0);
|
const bookedAmount = deliveryEntries.reduce((sum, e) => sum + Number(e.amount || 0), 0);
|
||||||
const targetAmount = Number(line.amount || 0);
|
const targetAmount = Number(line.amount || 0);
|
||||||
const remainingAmount = Math.max(0, targetAmount - bookedAmount);
|
const remainingAmount = Math.max(0, targetAmount - bookedAmount);
|
||||||
@@ -129,6 +160,7 @@ export async function fetchDeliveryLedgerSummary(deliveryId, { deliveryApi = def
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...line,
|
...line,
|
||||||
|
article,
|
||||||
targetAmount,
|
targetAmount,
|
||||||
bookedAmount,
|
bookedAmount,
|
||||||
remainingAmount,
|
remainingAmount,
|
||||||
|
|||||||
Reference in New Issue
Block a user