This commit is contained in:
seb
2026-10-14 23:58:37 +02:00
parent e2a2d02cb4
commit ed7bb80006
5 changed files with 200 additions and 48 deletions

View File

@@ -87,8 +87,8 @@ class AppSidebar extends PureComponent {
disablePadding disablePadding
sx={{ sx={{
flexGrow: 1, flexGrow: 1,
display: 'flex', display: { xs: 'flex', lg: 'block' },
flexDirection: { xs: 'row', lg: 'column' }, flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
height: { xs: '100%', lg: 'auto' }, height: { xs: '100%', lg: 'auto' },
overflowX: { xs: 'auto', lg: 'visible' }, overflowX: { xs: 'auto', lg: 'visible' },
@@ -104,6 +104,8 @@ class AppSidebar extends PureComponent {
<ListItemButton <ListItemButton
key={path} key={path}
type="button" type="button"
aria-label={label}
title={label}
selected={active} selected={active}
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
onClick={() => { onClick={() => {
@@ -131,7 +133,7 @@ class AppSidebar extends PureComponent {
borderRadius: { xs: 1, lg: 0 }, borderRadius: { xs: 1, lg: 0 },
width: { xs: 'auto', lg: '100%' }, width: { xs: 'auto', lg: '100%' },
height: { xs: '100%', lg: 'auto' }, height: { xs: '100%', lg: 'auto' },
px: { xs: 1.25, sm: 1.5 }, px: { xs: active ? 1.25 : 1, sm: active ? 1.5 : 1.25, md: 1.5 },
py: { xs: 0.75, sm: 1, lg: 1.25 }, py: { xs: 0.75, sm: 1, lg: 1.25 },
borderLeft: { xs: 'none', lg: '3px solid transparent' }, borderLeft: { xs: 'none', lg: '3px solid transparent' },
borderBottom: { xs: '3px solid transparent', lg: 'none' }, borderBottom: { xs: '3px solid transparent', lg: 'none' },
@@ -141,7 +143,8 @@ class AppSidebar extends PureComponent {
'& .MuiListItemIcon-root': { '& .MuiListItemIcon-root': {
color: 'inherit', color: 'inherit',
minWidth: { xs: 'auto', lg: 36 }, minWidth: { xs: 'auto', lg: 36 },
mr: { xs: 0.75, lg: 0 }, mr: active ? { xs: 0.75, lg: 0 } : { xs: 0, md: 0.75, lg: 0 },
justifyContent: 'center',
}, },
'&:hover': { '&:hover': {
bgcolor: theme.nav.itemHover, bgcolor: theme.nav.itemHover,
@@ -172,7 +175,10 @@ class AppSidebar extends PureComponent {
fontWeight: active ? 600 : 500, fontWeight: active ? 600 : 500,
noWrap: true, noWrap: true,
}} }}
sx={{ m: 0 }} sx={{
m: { xs: 0, lg: 'initial' },
display: active ? 'block' : { xs: 'none', md: 'block' },
}}
/> />
</ListItemButton> </ListItemButton>
); );
@@ -239,25 +245,34 @@ class AppSubNav extends PureComponent {
<Box <Box
component="nav" component="nav"
aria-label="Unternavigation" aria-label="Unternavigation"
sx={{ sx={(theme) => ({
width: { xs: 60, sm: 72, md: SUB_NAV_WIDTH }, width: { xs: '100%', sm: 72, md: SUB_NAV_WIDTH },
flexShrink: 0, flexShrink: 0,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: { xs: 'row', sm: 'column' },
alignItems: 'center', alignItems: 'center',
gap: 1.5, gap: { xs: 1, sm: 1.5 },
position: 'sticky', position: 'sticky',
top: { xs: 52, sm: 56, lg: 0 }, top: { xs: 52, sm: 56, lg: 0 },
zIndex: { xs: 1090, sm: 'auto' },
alignSelf: 'flex-start', alignSelf: 'flex-start',
height: { height: {
xs: 'calc(var(--df-vh, 100vh) - 52px)', xs: 'auto',
sm: 'calc(var(--df-vh, 100vh) - 56px)', sm: 'calc(var(--df-vh, 100vh) - 56px)',
lg: 'var(--df-vh, 100vh)', lg: 'var(--df-vh, 100vh)',
}, },
py: { xs: 2, lg: 4 }, py: { xs: 0.75, sm: 2, lg: 4 },
ml: { xs: 1, sm: 2, lg: 3 }, px: { xs: 1.5, sm: 0 },
mr: { xs: 1, sm: 1.5, lg: 2 }, ml: { xs: 0, sm: 2, lg: 3 },
}} mr: { xs: 0, sm: 1.5, lg: 2 },
overflowX: { xs: 'auto', sm: 'visible' },
scrollbarWidth: 'none',
'&::-webkit-scrollbar': { display: 'none' },
bgcolor: { xs: theme.nav?.mainBackground || 'background.default', sm: 'transparent' },
borderBottom: { xs: '1px solid', sm: 'none' },
borderColor: { xs: 'divider', sm: 'transparent' },
boxSizing: 'border-box',
})}
> >
{subNav.map(({ segment, label, title, help, icon: Icon }) => { {subNav.map(({ segment, label, title, help, icon: Icon }) => {
const active = segment === activeSub; const active = segment === activeSub;
@@ -266,6 +281,7 @@ class AppSubNav extends PureComponent {
<ButtonBase <ButtonBase
key={segment || 'index'} key={segment || 'index'}
type="button" type="button"
title={label}
aria-label={label} aria-label={label}
aria-current={active ? 'page' : undefined} aria-current={active ? 'page' : undefined}
disableRipple disableRipple
@@ -290,15 +306,17 @@ class AppSubNav extends PureComponent {
navigate(segment ? `${activeNav}/${segment}` : activeNav); navigate(segment ? `${activeNav}/${segment}` : activeNav);
}} }}
sx={(theme) => ({ sx={(theme) => ({
width: { xs: 60, sm: 72, md: SUB_NAV_WIDTH }, width: { xs: 'auto', sm: 72, md: SUB_NAV_WIDTH },
py: 1, py: { xs: 0.5, sm: 1 },
px: 0.5, px: { xs: active ? 1.25 : 0.75, sm: 0.5 },
borderRadius: 2, borderRadius: 2,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: { xs: 'row', sm: 'column' },
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
gap: 0.5, gap: { xs: active ? 0.75 : 0, sm: 0.5 },
flexShrink: 0,
whiteSpace: 'nowrap',
color: active color: active
? theme.nav?.itemActiveAccent || NAV_ITEM_ACTIVE_ACCENT ? theme.nav?.itemActiveAccent || NAV_ITEM_ACTIVE_ACCENT
: theme.palette.text.secondary, : theme.palette.text.secondary,
@@ -314,18 +332,19 @@ class AppSubNav extends PureComponent {
})} })}
> >
<HelpBadge title={title || label} help={help} placement="right"> <HelpBadge title={title || label} help={help} placement="right">
<Icon sx={{ fontSize: { xs: 24, sm: 28, md: 32 } }} /> <Icon sx={{ fontSize: { xs: 20, sm: 28, md: 32 } }} />
</HelpBadge> </HelpBadge>
{label ? ( {label ? (
<Typography <Typography
variant="caption" variant="caption"
noWrap noWrap
sx={{ sx={{
fontSize: { xs: '0.62rem', sm: '0.68rem' }, fontSize: { xs: '0.75rem', sm: '0.68rem' },
fontWeight: 500, fontWeight: 500,
lineHeight: 1.2, lineHeight: 1.2,
textAlign: 'center', textAlign: 'center',
maxWidth: '100%', maxWidth: '100%',
display: active ? 'block' : { xs: 'none', sm: 'block' },
}} }}
> >
{label} {label}
@@ -358,7 +377,7 @@ export default function AppShell() {
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: { xs: 'column', sm: 'row' },
flexGrow: 1, flexGrow: 1,
minWidth: 0, minWidth: 0,
}} }}

View File

@@ -17,7 +17,7 @@ import artikelAutomat, { getArticle } from '../../automats/artikelAutomat';
import entityAutomat from '../../automats/entityAutomat'; import entityAutomat from '../../automats/entityAutomat';
import supplierAutomat from '../../automats/supplierAutomat'; import supplierAutomat from '../../automats/supplierAutomat';
import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat'; import companyAutomat, { selectCompanyTaxrate } from '../../automats/companyAutomat';
import { netFromGross, getArticleGross } from '../../utils/price'; import { netFromGross, grossFromNet, getArticleGross } from '../../utils/price';
import { import {
buildManifestBody, buildManifestBody,
restoreAttachmentsFromApi, restoreAttachmentsFromApi,
@@ -421,10 +421,11 @@ class ManifestForm extends PureComponent {
if (field === 'gross') { if (field === 'gross') {
const taxVal = parseNum(line.tax ?? line.taxrate) ?? 0; const taxVal = parseNum(line.tax ?? line.taxrate) ?? 0;
const rebateVal = parseNum(line.rebate) ?? 0;
const grossNum = parseNum(value); const grossNum = parseNum(value);
let updatedNet = line.net; let updatedNet = line.net;
if (grossNum != null) { if (grossNum != null) {
const netNum = netFromGross(grossNum, taxVal); const netNum = netFromGross(grossNum, taxVal, rebateVal);
if (netNum != null) { if (netNum != null) {
updatedNet = String(netNum); updatedNet = String(netNum);
} }
@@ -441,11 +442,12 @@ class ManifestForm extends PureComponent {
if (field === 'net' || field === 'netprice') { if (field === 'net' || field === 'netprice') {
const taxVal = parseNum(line.tax ?? line.taxrate) ?? 0; const taxVal = parseNum(line.tax ?? line.taxrate) ?? 0;
const rebateVal = parseNum(line.rebate) ?? 0;
const netNum = parseNum(value); const netNum = parseNum(value);
let updatedGross = line.gross; let updatedGross = line.gross;
if (netNum != null) { if (netNum != null) {
const grossNum = Math.round((netNum * (1 + taxVal / 100) + Number.EPSILON) * 100) / 100; const grossNum = grossFromNet(netNum, taxVal, rebateVal);
updatedGross = String(grossNum); updatedGross = grossNum != null ? String(grossNum) : '';
} else if (value === '') { } else if (value === '') {
updatedGross = ''; updatedGross = '';
} }
@@ -460,11 +462,12 @@ class ManifestForm extends PureComponent {
if (field === 'tax' || field === 'taxrate') { if (field === 'tax' || field === 'taxrate') {
const taxNum = parseNum(value) ?? 0; const taxNum = parseNum(value) ?? 0;
const rebateVal = parseNum(line.rebate) ?? 0;
const netNum = parseNum(line.net ?? line.netprice); const netNum = parseNum(line.net ?? line.netprice);
let updatedGross = line.gross; let updatedGross = line.gross;
if (netNum != null) { if (netNum != null) {
const grossNum = Math.round((netNum * (1 + taxNum / 100) + Number.EPSILON) * 100) / 100; const grossNum = grossFromNet(netNum, taxNum, rebateVal);
updatedGross = String(grossNum); updatedGross = grossNum != null ? String(grossNum) : '';
} }
return { return {
...line, ...line,
@@ -475,6 +478,22 @@ class ManifestForm extends PureComponent {
}; };
} }
if (field === 'rebate') {
const rebateNum = parseNum(value) ?? 0;
const taxVal = parseNum(line.tax ?? line.taxrate) ?? 0;
const netNum = parseNum(line.net ?? line.netprice);
let updatedGross = line.gross;
if (netNum != null) {
const grossNum = grossFromNet(netNum, taxVal, rebateNum);
updatedGross = grossNum != null ? String(grossNum) : '';
}
return {
...line,
rebate: value,
gross: updatedGross,
};
}
return { return {
...line, ...line,
[field]: value, [field]: value,
@@ -508,13 +527,15 @@ class ManifestForm extends PureComponent {
: ''; : '';
const netNum = parseNum(netVal); const netNum = parseNum(netVal);
const taxNum = parseNum(taxVal); const taxNum = parseNum(taxVal);
const rebateNum = parseNum(l.rebate) ?? 0;
const artGross = getArticleGross(art, taxNum ?? 0); const artGross = getArticleGross(art, taxNum ?? 0);
let grossVal = ''; let grossVal = '';
if (artGross != null) { if (artGross != null && rebateNum === 0) {
grossVal = String(artGross); grossVal = String(artGross);
} else if (netNum != null) { } else if (netNum != null) {
const g = Math.round((netNum * (1 + (taxNum ?? 0) / 100) + Number.EPSILON) * 100) / 100; grossVal = String(grossFromNet(netNum, taxNum ?? 0, rebateNum));
grossVal = String(g); } else if (artGross != null) {
grossVal = String(Math.round((artGross * (1 - rebateNum / 100) + Number.EPSILON) * 100) / 100);
} }
const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19'; const normTax = selectCompanyTaxrate() != null ? String(selectCompanyTaxrate()) : '19';
const finalTax = const finalTax =
@@ -640,13 +661,15 @@ class ManifestForm extends PureComponent {
: normTax; : normTax;
const netNum = parseNum(netVal); const netNum = parseNum(netVal);
const taxNum = parseNum(taxVal); const taxNum = parseNum(taxVal);
const rebateNum = parseNum(targetLine.rebate) ?? 0;
const artGross = getArticleGross(article, taxNum ?? 0); const artGross = getArticleGross(article, taxNum ?? 0);
let grossVal = ''; let grossVal = '';
if (artGross != null) { if (artGross != null && rebateNum === 0) {
grossVal = String(artGross); grossVal = String(artGross);
} else if (netNum != null) { } else if (netNum != null) {
const g = Math.round((netNum * (1 + (taxNum ?? 0) / 100) + Number.EPSILON) * 100) / 100; grossVal = String(grossFromNet(netNum, taxNum ?? 0, rebateNum));
grossVal = String(g); } else if (artGross != null) {
grossVal = String(Math.round((artGross * (1 - rebateNum / 100) + Number.EPSILON) * 100) / 100);
} }
this.setState((prev) => ({ this.setState((prev) => ({

View File

@@ -19,11 +19,13 @@ import SearchIcon from '@mui/icons-material/Search';
import LinkOffOutlinedIcon from '@mui/icons-material/LinkOffOutlined'; import LinkOffOutlinedIcon from '@mui/icons-material/LinkOffOutlined';
import { getFirstVal, parseNum, LINE_COLUMNS } from './manifestConfig'; import { getFirstVal, parseNum, LINE_COLUMNS } from './manifestConfig';
import ArticlePhoto from '../article/ArticlePhoto'; import ArticlePhoto from '../article/ArticlePhoto';
import { formatPrice } from '../../utils/price'; import { formatPrice, grossFromNet } from '../../utils/price';
import { primaryPrice, priceBrutto } from '../../utils/articleDetail'; import { primaryPrice, priceBrutto } from '../../utils/articleDetail';
import artikelAutomat, { createArticleSelector, getArticle } from '../../automats/artikelAutomat'; import artikelAutomat, { createArticleSelector, getArticle } from '../../automats/artikelAutomat';
import { artikelDetailPath } from '../article/articleRoute'; import { artikelDetailPath } from '../article/articleRoute';
import { BrandLabel } from '../brand'; import { BrandLabel } from '../brand';
import { articleSupplierAutomat } from '../../automats/articleDetailAutomat';
import { selectCompanyTaxrate } from '../../automats/companyAutomat';
import { import {
BORDER, BORDER,
} from '../../colors'; } from '../../colors';
@@ -35,6 +37,7 @@ class ManifestFormLine extends PureComponent {
this.state = { this.state = {
cachedArticle: sku ? getArticle(sku) : null, cachedArticle: sku ? getArticle(sku) : null,
loadingArticle: false, loadingArticle: false,
fallbackSuppliers: [],
}; };
this.handleRemove = this.handleRemove.bind(this); this.handleRemove = this.handleRemove.bind(this);
this.handleBlur = this.handleBlur.bind(this); this.handleBlur = this.handleBlur.bind(this);
@@ -64,14 +67,18 @@ class ManifestFormLine extends PureComponent {
componentWillUnmount() { componentWillUnmount() {
this.unsubArticle?.(); this.unsubArticle?.();
this.unsubSupplier?.();
} }
subscribeArticle(sku) { subscribeArticle(sku) {
this.unsubArticle?.(); this.unsubArticle?.();
this.unsubArticle = null; this.unsubArticle = null;
this.unsubSupplier?.();
this.unsubSupplier = null;
if (!sku) { if (!sku) {
if (this.state.cachedArticle || this.state.loadingArticle) { if (this.state.cachedArticle || this.state.loadingArticle || this.state.fallbackSuppliers?.length) {
this.setState({ cachedArticle: null, loadingArticle: false }); this.setState({ cachedArticle: null, loadingArticle: false, fallbackSuppliers: [] });
} }
return; return;
} }
@@ -99,6 +106,15 @@ class ManifestFormLine extends PureComponent {
...(art ? { loadingArticle: false } : {}), ...(art ? { loadingArticle: false } : {}),
}; };
}); });
// Fallback: subscribe to articleSupplierAutomat if cachedArticle doesn't have suppliers
if (!current?.suppliers?.length) {
const supKey = articleSupplierAutomat.key(sku);
this.unsubSupplier = supKey.subscribe(this, (item) => ({
fallbackSuppliers: Array.isArray(item?.data) ? item.data : [],
}));
void articleSupplierAutomat.load(sku).catch(() => {});
}
} }
handleRemove() { handleRemove() {
@@ -235,8 +251,9 @@ class ManifestFormLine extends PureComponent {
} else if (id === 'gross') { } else if (id === 'gross') {
const netNum = parseNum(line.netprice ?? line.net); const netNum = parseNum(line.netprice ?? line.net);
const taxNum = parseNum(line.taxrate ?? line.tax) ?? 0; const taxNum = parseNum(line.taxrate ?? line.tax) ?? 0;
const rebateNum = parseNum(line.rebate) ?? 0;
if (netNum != null) { if (netNum != null) {
val = String(Math.round((netNum * (1 + taxNum / 100) + Number.EPSILON) * 100) / 100); val = String(grossFromNet(netNum, taxNum, rebateNum));
} }
} }
} }
@@ -375,6 +392,41 @@ class ManifestFormLine extends PureComponent {
: null) : null)
: null; : null;
const suppliers =
Array.isArray(cachedArticle.suppliers) && cachedArticle.suppliers.length > 0
? cachedArticle.suppliers
: (this.state.fallbackSuppliers ?? []);
const topSupplier =
suppliers.find((s) => s.sort === 0) ??
(suppliers.length > 0
? [...suppliers].sort((a, b) => (Number(a.sort) || 0) - (Number(b.sort) || 0))[0]
: null);
const ekNetto =
topSupplier?.price?.net != null
? Number(topSupplier.price.net)
: topSupplier?.netPrice != null
? Number(topSupplier.netPrice)
: topSupplier?.net != null
? Number(topSupplier.net)
: null;
const ekTaxRate =
topSupplier?.price?.taxRate != null
? Number(topSupplier.price.taxRate)
: topSupplier?.taxRate != null
? Number(topSupplier.taxRate)
: parseNum(this.props.line?.taxrate ?? this.props.line?.tax) ?? selectCompanyTaxrate() ?? 19;
const ekBrutto =
ekNetto != null
? Math.round((ekNetto * (1 + Number(ekTaxRate) / 100) + Number.EPSILON) * 100) / 100
: null;
const hasPriceInfo =
vkNetto != null || vkBrutto != null || ekNetto != null || ekBrutto != null || kind === 'quote';
return ( return (
<Box <Box
sx={{ sx={{
@@ -480,8 +532,8 @@ class ManifestFormLine extends PureComponent {
</Typography> </Typography>
)} )}
{/* VK Netto & VK Brutto: omit end on 2nd line for order */} {/* EK Netto, EK Brutto, VK Netto, VK Brutto */}
{!isOrder && (vkNetto != null || vkBrutto != null) && ( {!isOrder && hasPriceInfo && (
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@@ -491,6 +543,47 @@ class ManifestFormLine extends PureComponent {
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}} }}
> >
{/* EK Netto */}
<Tooltip
title={
topSupplier?.supplierName
? `Lieferant: ${topSupplier.supplierName}${topSupplier.sku ? ` (Lief.-Art.-Nr.: ${topSupplier.sku})` : ''} (Sort: ${topSupplier.sort ?? 0})`
: 'Kein Lieferant mit EK-Preis hinterlegt'
}
>
<Typography
variant="caption"
fontWeight={600}
sx={{
color: ekNetto != null ? 'text.secondary' : 'text.disabled',
fontSize: '0.72rem',
}}
>
EK Netto: {ekNetto != null ? formatPrice(ekNetto) : '—'}
</Typography>
</Tooltip>
{/* EK Brutto */}
<Tooltip
title={
topSupplier?.supplierName
? `Lieferant: ${topSupplier.supplierName} (inkl. ${ekTaxRate}% MwSt.)`
: 'Kein Lieferant mit EK-Preis hinterlegt'
}
>
<Typography
variant="caption"
fontWeight={600}
sx={{
color: ekBrutto != null ? 'text.secondary' : 'text.disabled',
fontSize: '0.72rem',
}}
>
EK Brutto: {ekBrutto != null ? formatPrice(ekBrutto) : '—'}
</Typography>
</Tooltip>
{/* VK Netto */}
{vkNetto != null && ( {vkNetto != null && (
<Typography <Typography
variant="caption" variant="caption"
@@ -500,6 +593,8 @@ class ManifestFormLine extends PureComponent {
VK Netto: {formatPrice(vkNetto)} VK Netto: {formatPrice(vkNetto)}
</Typography> </Typography>
)} )}
{/* VK Brutto */}
{vkBrutto != null && ( {vkBrutto != null && (
<Typography <Typography
variant="caption" variant="caption"

View File

@@ -107,12 +107,13 @@ export function lineFromApi(row) {
const taxVal = row.taxrate != null ? String(row.taxrate) : row.tax != null ? String(row.tax) : ''; const taxVal = row.taxrate != null ? String(row.taxrate) : row.tax != null ? String(row.tax) : '';
const netNum = parseNum(netVal); const netNum = parseNum(netVal);
const taxNum = parseNum(taxVal); const taxNum = parseNum(taxVal);
const rebateNum = parseNum(row.rebate) ?? 0;
let grossVal = ''; let grossVal = '';
if (row.gross != null && row.gross !== '') { if (row.gross != null && row.gross !== '') {
grossVal = String(row.gross); grossVal = String(row.gross);
} else if (netNum != null) { } else if (netNum != null) {
const rate = taxNum ?? 0; const rate = taxNum ?? 0;
const g = Math.round((netNum * (1 + rate / 100) + Number.EPSILON) * 100) / 100; const g = Math.round((netNum * (1 + rate / 100) * (1 - rebateNum / 100) + Number.EPSILON) * 100) / 100;
grossVal = String(g); grossVal = String(g);
} }
return { return {
@@ -175,10 +176,10 @@ export function buildLines(lines, { requireAtLeastOne }) {
const tax = parseNum(rawTax); const tax = parseNum(rawTax);
const rawGross = getFirstVal(line.gross); const rawGross = getFirstVal(line.gross);
const gross = parseNum(rawGross); const gross = parseNum(rawGross);
if (net == null && gross != null && tax != null) {
net = netFromGross(gross, tax);
}
const rebate = parseNum(getFirstVal(line.rebate)); const rebate = parseNum(getFirstVal(line.rebate));
if (net == null && gross != null && tax != null) {
net = netFromGross(gross, tax, rebate ?? 0);
}
const rawPu = getFirstVal(line.pu, line.vpe); const rawPu = getFirstVal(line.pu, line.vpe);
const pu = parseNum(rawPu); const pu = parseNum(rawPu);
const blank = !sku && !name && net == null && gross == null; const blank = !sku && !name && net == null && gross == null;

View File

@@ -45,12 +45,26 @@ export function parsePriceInput(value) {
} }
/** Convert gross (brutto) → net for PUT /prices. Rounded to 4 decimal places. */ /** Convert gross (brutto) → net for PUT /prices. Rounded to 4 decimal places. */
export function netFromGross(gross, taxRate) { export function netFromGross(gross, taxRate, rebate = 0) {
if (gross == null || taxRate == null) return null; if (gross == null || taxRate == null) return null;
if (!Number.isFinite(gross) || !Number.isFinite(taxRate) || taxRate < 0) return null; if (!Number.isFinite(gross) || !Number.isFinite(taxRate) || taxRate < 0) return null;
const factor = 1 + taxRate / 100; const taxFactor = 1 + taxRate / 100;
if (factor <= 0) return null; if (taxFactor <= 0) return null;
return Math.round((gross / factor) * 10000) / 10000; const r = Number.isFinite(rebate) ? rebate : 0;
const rebateFactor = 1 - r / 100;
if (rebateFactor <= 0) return 0;
return Math.round((gross / (taxFactor * rebateFactor)) * 10000) / 10000;
}
/** Convert net → gross taking taxRate and optional rebate into account. Rounded to 2 decimal places. */
export function grossFromNet(net, taxRate = 0, rebate = 0) {
if (net == null) return null;
const netNum = Number(net);
if (!Number.isFinite(netNum)) return null;
const taxNum = Number(taxRate) || 0;
const rebateNum = Number(rebate) || 0;
const factor = (1 + taxNum / 100) * (1 - rebateNum / 100);
return Math.round((netNum * factor + Number.EPSILON) * 100) / 100;
} }
/** /**