This commit is contained in:
seb
2026-10-17 03:47:09 +02:00
parent d729423249
commit e63abfe2c0
4 changed files with 168 additions and 101 deletions

View File

@@ -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' } }}
/>
<IconButton
type="button"

View File

@@ -21,6 +21,7 @@ import supplierAutomat from '../../automats/supplierAutomat';
import orderAutomat from '../../automats/orderAutomat';
import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat';
import { netFromGross, grossFromNet, getArticleGross } from '../../utils/price';
import { resolveSalesPrice } from '../../utils/articleDetail';
import {
buildManifestBody,
restoreAttachmentsFromApi,
@@ -119,6 +120,8 @@ class ManifestForm extends PureComponent {
this.handleExternalNumberChange.bind(this);
this.handleLineChange = this.handleLineChange.bind(this);
this.handleSkuBlur = this.handleSkuBlur.bind(this);
this.getCustomerGroupId = this.getCustomerGroupId.bind(this);
this.getOrFetchArticle = this.getOrFetchArticle.bind(this);
this.handleAddLine = this.handleAddLine.bind(this);
this.handleAddArticleLine = this.handleAddArticleLine.bind(this);
this.handleOpenArticleSearchForLine = this.handleOpenArticleSearchForLine.bind(this);
@@ -574,48 +577,51 @@ class ManifestForm extends PureComponent {
}));
}
getCustomerGroupId() {
const { partyId, parties } = this.state;
if (!partyId || !Array.isArray(parties)) return null;
const entity = parties.find((p) => 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,

View File

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

View File

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