diff --git a/src/api/routes/article/media.js b/src/api/routes/article/media.js index 52d82cb..36d1461 100644 --- a/src/api/routes/article/media.js +++ b/src/api/routes/article/media.js @@ -14,6 +14,7 @@ import { parseFullHash, articleErrorStatus, interpolateString, + withMandatedOptionsPrice, interpolateObjects, } from './shared.js'; @@ -408,7 +409,7 @@ export function register(app, basePath = '') { let article; try { const result = await db.getBySku(req.user.id, sku); - article = result?.article; + article = result?.article ? await withMandatedOptionsPrice(req.user.id, result.article) : undefined; } catch (err) { const status = articleErrorStatus(err); if (status === 500) console.error('Get article by sku failed for print:', err); diff --git a/src/api/routes/article/shared.js b/src/api/routes/article/shared.js index 3731e0a..4235ba7 100644 --- a/src/api/routes/article/shared.js +++ b/src/api/routes/article/shared.js @@ -1,4 +1,5 @@ import { emit } from '../../sse.js'; +import * as articleDb from '../../../db/article.js'; // #region Helpers @@ -133,18 +134,42 @@ const BASEPRICE_FORMATTER = new Intl.NumberFormat('de-DE', { }); /** Formats the price per base unit, e.g. "€ 4,05 / L". Empty when unit data or price is missing. */ -export function formatBasePrice(net, taxRate, article) { +export function formatBasePrice(grossRaw, article) { const unit = article?.unit; const unitamount = Number(article?.unitamount); const factor = Number(unit?.basesifactor); - if (net == null || !unit || !(unitamount > 0) || !(factor > 0)) return ''; + if (grossRaw == null || !unit?.basesi || !(unitamount > 0) || !(factor > 0)) return ''; const baseAmount = unitamount * factor; - const perBase = (net * (1 + taxRate / 100)) / baseAmount; - const label = factor === 1 ? (unit.symbol || unit.basesi) : unit.basesi; - if (!label) return ''; + // Price per base unit equals the price itself: nothing to add. + if (Math.abs(baseAmount - 1) < 1e-9) return ''; + const perBase = grossRaw / baseAmount; + const label = factor === 1 && unit.symbol ? unit.symbol : unit.basesi; return `€ ${BASEPRICE_FORMATTER.format(perBase)} / ${label}`; } +function grossOf(price) { + const net = Number(price?.net); + if (price?.net == null || !Number.isFinite(net)) return null; + const tax = Number(price.taxRate); + return net * (1 + (Number.isFinite(tax) ? tax : 0) / 100); +} + +/** + * Returns the article with `mandatedOptionsGross` set to the summed gross price + * (price × count) of its mandated options, for label placeholders. + */ +export async function withMandatedOptionsPrice(userId, article) { + const mandated = Array.isArray(article?.options) ? article.options.filter((o) => o.mandated) : []; + if (mandated.length === 0) return article; + let extra = 0; + for (const opt of mandated) { + const result = await articleDb.getBySku(userId, opt.sku); + const gross = grossOf(result?.article?.prices?.[0]); + if (gross != null) extra += gross * (Number(opt.count) || 1); + } + return { ...article, mandatedOptionsGross: extra }; +} + export function interpolateString(str, article) { if (typeof str !== 'string') return str; const basePrice = Array.isArray(article?.prices) && article.prices.length > 0 @@ -156,13 +181,15 @@ export function interpolateString(str, article) { const taxRate = basePrice?.taxRate != null && Number.isFinite(Number(basePrice.taxRate)) ? Number(basePrice.taxRate) : 0; - const gross = net != null - ? Math.round((net * (1 + taxRate / 100) + Number.EPSILON) * 100) / 100 + const extra = Number(article?.mandatedOptionsGross) || 0; + const grossRaw = net != null ? net * (1 + taxRate / 100) + extra : null; + const gross = grossRaw != null + ? Math.round((grossRaw + Number.EPSILON) * 100) / 100 : null; return str .replace(/\{\{name\}\}/gi, article?.name || '') - .replace(/\{\{baseprice\}\}/gi, formatBasePrice(net, taxRate, article)) + .replace(/\{\{baseprice\}\}/gi, formatBasePrice(grossRaw, article)) .replace(/\{\{price\}\}/gi, formatPrice(gross)); } diff --git a/src/api/routes/labeltemplate.js b/src/api/routes/labeltemplate.js index 48cf175..b8b393a 100644 --- a/src/api/routes/labeltemplate.js +++ b/src/api/routes/labeltemplate.js @@ -8,7 +8,7 @@ import { pngToZpl } from '../../services/zplService.js'; import { notifyAgentJob } from '../agent/index.js'; import { emit } from '../sse.js'; import { applyZplCopies } from './zpljob.js'; -import { interpolateString, interpolateObjects } from './article/shared.js'; +import { interpolateString, interpolateObjects, withMandatedOptionsPrice } from './article/shared.js'; // #region Helpers @@ -102,7 +102,7 @@ export function register(app, basePath = '') { const skuStr = String(req.query.sku).trim(); const result = await articleDb.getBySku(req.user.id, skuStr); if (result?.article) { - article = result.article; + article = await withMandatedOptionsPrice(req.user.id, result.article); } } @@ -220,7 +220,7 @@ export function register(app, basePath = '') { : null; if (objects && objects.length > 0) { try { - const interpolated = interpolateObjects(objects, result.article); + const interpolated = interpolateObjects(objects, await withMandatedOptionsPrice(req.user.id, result.article)); const thumbBuffer = await renderThumb({ width: template.width, height: template.height, @@ -655,7 +655,7 @@ export function register(app, basePath = '') { if (!result?.article) { return res.status(404).json({ error: 'Article not found' }); } - article = result.article; + article = await withMandatedOptionsPrice(req.user.id, result.article); } else { article = { name: template?.name || 'Musterartikel',