diff --git a/src/components/article/boxes/PriceUnitRow.jsx b/src/components/article/boxes/PriceUnitRow.jsx index 8c7fd74..d08d588 100644 --- a/src/components/article/boxes/PriceUnitRow.jsx +++ b/src/components/article/boxes/PriceUnitRow.jsx @@ -136,7 +136,7 @@ export class PriceUnitCorner extends Component { size="small" checked={Boolean(hasUnit)} onChange={(event) => onToggleHasUnit(event.target.checked)} - inputProps={{ 'aria-label': 'Einheit' }} + slotProps={{ input: { 'aria-label': 'Einheit' } }} /> String(p.id) === String(partyId)); + return entity?.customergroupId ?? null; + } + + async getOrFetchArticle(articleOrSku) { + const sku = typeof articleOrSku === 'string' ? articleOrSku : articleOrSku?.sku; + const trimmed = String(sku ?? '').trim(); + if (!trimmed) return typeof articleOrSku === 'object' ? articleOrSku : null; + let art = getArticle(trimmed); + if (!art) { + try { + art = await artikelAutomat.actions.loadArticle(trimmed); + } catch { + // fallback + } + } + if (art) return art; + return typeof articleOrSku === 'object' ? articleOrSku : null; + } + async handleSkuBlur(key, sku) { const trimmed = String(sku ?? '').trim(); if (!trimmed) return; try { - const art = getArticle(trimmed) || (await artikelAutomat.actions.loadArticle(trimmed)); + const art = await this.getOrFetchArticle(trimmed); if (art && !this._cancelled) { + const normTax = selectCompanyTaxrate() != null ? Number(selectCompanyTaxrate()) : 19; + const customerGroupId = this.getCustomerGroupId(); + this.setState((prev) => ({ lines: prev.lines.map((l) => { if (l.key !== key) return l; - const netVal = - art.price != null - ? String(art.price) - : art.vknetto != null - ? String(art.vknetto) - : art.net != null - ? String(art.net) - : ''; - const taxVal = - art.taxrate != null - ? String(art.taxrate) - : art.tax != null - ? String(art.tax) - : ''; - const netNum = parseNum(netVal); - const taxNum = parseNum(taxVal); const rebateNum = parseNum(l.rebate) ?? 0; - const artGross = getArticleGross(art, taxNum ?? 0); - let grossVal = ''; - if (artGross != null && rebateNum === 0) { - grossVal = String(artGross); - } else if (netNum != null) { - grossVal = String(grossFromNet(netNum, taxNum ?? 0, rebateNum)); - } else if (artGross != null) { - grossVal = String(Math.round((artGross * (1 - rebateNum / 100) + Number.EPSILON) * 100) / 100); - } - const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19'; - const finalTax = - art.taxrate != null - ? String(art.taxrate) - : art.tax != null - ? String(art.tax) - : l.tax || normTax; + const { net, gross, taxRate } = resolveSalesPrice(art, { + customerGroupId, + defaultTaxRate: normTax, + rebate: rebateNum, + }); + const netVal = net != null ? String(net) : ''; + const grossVal = gross != null ? String(gross) : ''; + const finalTax = taxRate != null ? String(taxRate) : l.tax || String(normTax); + return { ...l, amount: l.amount || '1', @@ -624,9 +630,9 @@ class ManifestForm extends PureComponent { tax: finalTax, taxrate: finalTax, rebate: l.rebate || '0', - net: l.net || netVal, - netprice: l.netprice || netVal, - gross: l.gross || grossVal, + net: netVal || l.net, + netprice: netVal || l.netprice, + gross: grossVal || l.gross, unit: art.unit || l.unit || 'Stück', }; }), @@ -641,39 +647,27 @@ class ManifestForm extends PureComponent { this.setState((prev) => ({ lines: [...prev.lines, emptyLine()] })); } - handleAddArticleLine(article) { + async handleAddArticleLine(article) { if (!article) return; - const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19'; - const netVal = - article.price != null - ? String(article.price) - : article.vknetto != null - ? String(article.vknetto) - : article.net != null - ? String(article.net) - : ''; - const taxVal = - article.taxrate != null - ? String(article.taxrate) - : article.tax != null - ? String(article.tax) - : normTax; - const netNum = parseNum(netVal); - const taxNum = parseNum(taxVal); - const artGross = getArticleGross(article, taxNum ?? 0); - let grossVal = ''; - if (artGross != null) { - grossVal = String(artGross); - } else if (netNum != null) { - const g = Math.round((netNum * (1 + (taxNum ?? 0) / 100) + Number.EPSILON) * 100) / 100; - grossVal = String(g); - } + const fullArticle = (await this.getOrFetchArticle(article)) || article; + const normTax = selectCompanyTaxrate() != null ? Number(selectCompanyTaxrate()) : 19; + const customerGroupId = this.getCustomerGroupId(); + + const { net, gross, taxRate } = resolveSalesPrice(fullArticle, { + customerGroupId, + defaultTaxRate: normTax, + rebate: 0, + }); + + const netVal = net != null ? String(net) : ''; + const grossVal = gross != null ? String(gross) : ''; + const taxVal = taxRate != null ? String(taxRate) : String(normTax); const newLine = { key: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, amount: '1', - sku: article.sku || '', - name: article.name || '', + sku: fullArticle.sku || article.sku || '', + name: fullArticle.name || article.name || '', tax: taxVal, taxrate: taxVal, net: netVal, @@ -681,7 +675,7 @@ class ManifestForm extends PureComponent { gross: grossVal, rebate: '0', pu: '1', - unit: article.unit || 'Stück', + unit: fullArticle.unit || article.unit || 'Stück', }; this.setState((prev) => { @@ -706,43 +700,31 @@ class ManifestForm extends PureComponent { }); } - handleAssignArticleForLine(article, targetLine) { + async handleAssignArticleForLine(article, targetLine) { if (!article) return; if (!targetLine) { - this.handleAddArticleLine(article); + await this.handleAddArticleLine(article); this.setState({ articleSearchDialogOpen: false, articleSearchLine: null, }); return; } - const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19'; - const netVal = - article.price != null - ? String(article.price) - : article.vknetto != null - ? String(article.vknetto) - : article.net != null - ? String(article.net) - : ''; - const taxVal = - article.taxrate != null - ? String(article.taxrate) - : article.tax != null - ? String(article.tax) - : normTax; - const netNum = parseNum(netVal); - const taxNum = parseNum(taxVal); + + const fullArticle = (await this.getOrFetchArticle(article)) || article; + const normTax = selectCompanyTaxrate() != null ? Number(selectCompanyTaxrate()) : 19; + const customerGroupId = this.getCustomerGroupId(); const rebateNum = parseNum(targetLine.rebate) ?? 0; - const artGross = getArticleGross(article, taxNum ?? 0); - let grossVal = ''; - if (artGross != null && rebateNum === 0) { - grossVal = String(artGross); - } else if (netNum != null) { - grossVal = String(grossFromNet(netNum, taxNum ?? 0, rebateNum)); - } else if (artGross != null) { - grossVal = String(Math.round((artGross * (1 - rebateNum / 100) + Number.EPSILON) * 100) / 100); - } + + const { net, gross, taxRate } = resolveSalesPrice(fullArticle, { + customerGroupId, + defaultTaxRate: normTax, + rebate: rebateNum, + }); + + const netVal = net != null ? String(net) : ''; + const grossVal = gross != null ? String(gross) : ''; + const taxVal = taxRate != null ? String(taxRate) : String(normTax); this.setState((prev) => ({ lines: prev.lines.map((l) => { @@ -750,15 +732,15 @@ class ManifestForm extends PureComponent { return { ...l, amount: l.amount || '1', - sku: article.sku || l.sku, - name: article.name || l.name, - tax: taxVal || l.tax || normTax, - taxrate: taxVal || l.taxrate || normTax, + sku: fullArticle.sku || article.sku || l.sku, + name: fullArticle.name || article.name || l.name, + tax: taxVal || l.tax || String(normTax), + taxrate: taxVal || l.taxrate || String(normTax), rebate: l.rebate || '0', net: netVal || l.net, netprice: netVal || l.netprice, gross: grossVal || l.gross, - unit: article.unit || l.unit || 'Stück', + unit: fullArticle.unit || article.unit || l.unit || 'Stück', }; }), articleSearchDialogOpen: false, diff --git a/src/components/manifest/OrderList.jsx b/src/components/manifest/OrderList.jsx index a312be5..ca6c5dd 100644 --- a/src/components/manifest/OrderList.jsx +++ b/src/components/manifest/OrderList.jsx @@ -226,7 +226,7 @@ class OrderRow extends PureComponent { color="success" onClick={(e) => e.stopPropagation()} onChange={(e) => onTogglePaid?.(order, e.target.checked)} - inputProps={{ 'aria-label': `Auftrag ${order.number} bezahlt` }} + slotProps={{ input: { 'aria-label': `Auftrag ${order.number} bezahlt` } }} sx={{ p: 0.5 }} /> @@ -238,7 +238,7 @@ class OrderRow extends PureComponent { color="primary" onClick={(e) => e.stopPropagation()} onChange={(e) => onToggleDelivered?.(order, e.target.checked)} - inputProps={{ 'aria-label': `Auftrag ${order.number} geliefert` }} + slotProps={{ input: { 'aria-label': `Auftrag ${order.number} geliefert` } }} sx={{ p: 0.5 }} /> diff --git a/src/utils/articleDetail.js b/src/utils/articleDetail.js index b3a88e2..351259b 100644 --- a/src/utils/articleDetail.js +++ b/src/utils/articleDetail.js @@ -2,6 +2,7 @@ import { formatPriceAmount, + grossFromNet, netFromGross, parsePriceInput, } from './price'; @@ -156,3 +157,87 @@ export function formatPhotoSizeKb(fullSize) { export function formatPhotoDimensions(fullW, fullH) { return `${fullW}×${fullH}`; } + +/** + * Resolves the sales price (VK-Preis) and tax rate for an article. + * Inspects `article.prices` (matching optional `customerGroupId`, falling back to base price), + * top-level `article.net` / `article.price` / `article.vknetto`, and calculates gross with optional rebate. + * + * @param {object|null} article + * @param {object} [options] + * @param {number|string|null} [options.customerGroupId] + * @param {number|string} [options.defaultTaxRate=19] + * @param {number|string} [options.rebate=0] + * @returns {{ net: number|null, gross: number|null, taxRate: number|null }} + */ +export function resolveSalesPrice(article, { customerGroupId, defaultTaxRate = 19, rebate = 0 } = {}) { + const normTax = defaultTaxRate != null && Number.isFinite(Number(defaultTaxRate)) ? Number(defaultTaxRate) : 19; + if (!article) { + return { net: null, gross: null, taxRate: normTax }; + } + + const r = Number.isFinite(Number(rebate)) ? Number(rebate) : 0; + + let matchedPrice = null; + if (Array.isArray(article.prices) && article.prices.length > 0) { + if (customerGroupId != null) { + matchedPrice = + article.prices.find( + (p) => String(p.customergroupId) === String(customerGroupId) && (p.minmultiplier === 1 || p.minmultiplier == null) + ) || + article.prices.find((p) => String(p.customergroupId) === String(customerGroupId)); + } + if (!matchedPrice) { + matchedPrice = primaryPrice(article); + } + } + + let net = null; + let taxRate = null; + let gross = null; + + if (matchedPrice) { + if (matchedPrice.net != null && Number.isFinite(Number(matchedPrice.net))) { + net = Number(matchedPrice.net); + } + const rawTax = matchedPrice.taxRate ?? matchedPrice.tax; + if (rawTax != null && Number.isFinite(Number(rawTax))) { + taxRate = Number(rawTax); + } + if (matchedPrice.gross != null && Number.isFinite(Number(matchedPrice.gross))) { + gross = Number(matchedPrice.gross); + } else if (net != null && taxRate != null) { + gross = priceBrutto(matchedPrice); + } + } + + if (net == null) { + const rawNet = article.price ?? article.net ?? article.vknetto; + if (rawNet != null && Number.isFinite(Number(rawNet))) { + net = Number(rawNet); + } + } + + if (taxRate == null) { + const rawTax = article.taxRate ?? article.taxrate ?? article.tax; + taxRate = rawTax != null && Number.isFinite(Number(rawTax)) ? Number(rawTax) : normTax; + } + + if (gross == null && article.gross != null && Number.isFinite(Number(article.gross))) { + gross = Number(article.gross); + } + + if (net == null && gross != null && taxRate != null) { + net = netFromGross(gross, taxRate, 0); + } + + if (net != null && taxRate != null) { + if (r !== 0) { + gross = grossFromNet(net, taxRate, r); + } else if (gross == null) { + gross = Math.round((net * (1 + taxRate / 100) + Number.EPSILON) * 100) / 100; + } + } + + return { net, gross, taxRate }; +}