This commit is contained in:
seb
2026-11-01 05:37:15 +01:00
parent e6c15dd053
commit 8f9c941699
32 changed files with 145 additions and 210 deletions

View File

@@ -17,11 +17,11 @@ export function register(app, basePath = '') {
* /article/{sku}/prices:
* put:
* tags: [Article]
* summary: Replace POS prices by SKU (net > gross; price=net, tax=taxRate %)
* summary: Replace POS prices by SKU (net > gross)
* description: |
* Replace-all prices for the company POS customer group.
* `net` takes precedence over `price` (legacy alias for net) and `gross`.
* `tax` and `taxRate` both specify the tax rate percentage (e.g. 19 for 19%), not monetary tax.
* `net` takes precedence over `gross`.
* `taxRate` specifies the tax rate percentage (e.g. 19 for 19%), not monetary tax.
* When `gross` is provided, net price is computed as `gross / (1 + taxRate / 100)`.
* Prices are strictly scoped to the company's designated POS customer group (customergroupId returned on GET).
* Empty `prices` clears all POS prices. Returns the same shape as GET by SKU.
@@ -50,19 +50,13 @@ export function register(app, basePath = '') {
* properties:
* net:
* type: number
* description: Net price (excl. VAT). Either `net`, `gross`, or legacy `price` is required.
* description: Net price (excl. VAT). Either `net` or `gross` is required.
* gross:
* type: number
* description: Gross price (incl. VAT). Net is computed as `gross / (1 + taxRate / 100)`.
* price:
* type: number
* description: Legacy alias for `net`.
* taxRate:
* type: number
* description: Tax rate percentage (e.g. 19 for 19%, 7 for 7%, 0 for 0%).
* tax:
* type: number
* description: Alias for `taxRate` percentage.
* minmultiplier:
* type: number
* description: Defaults to 1; must be unique per request.

View File

@@ -14,6 +14,7 @@ import {
parseFullHash,
articleErrorStatus,
interpolateString,
interpolateObjects,
} from './shared.js';
/**
@@ -433,16 +434,7 @@ export function register(app, basePath = '') {
? content
: [];
const interpolatedObjects = objects.map(obj => {
const cloned = { ...obj };
if (typeof cloned.text === 'string') {
cloned.text = interpolateString(cloned.text, article);
}
if (typeof cloned.code === 'string') {
cloned.code = interpolateString(cloned.code, article);
}
return cloned;
});
const interpolatedObjects = interpolateObjects(objects, article);
const printPayload = await renderLabel({
width: template.width,

View File

@@ -117,20 +117,57 @@ export function articleErrorStatus(error) {
return 500;
}
const CURRENCY_FORMATTER = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
});
export function formatPrice(amount) {
if (amount == null || !Number.isFinite(Number(amount))) return '';
return CURRENCY_FORMATTER.format(Number(amount));
}
export function interpolateString(str, article) {
if (typeof str !== 'string') return str;
const net = article.net_price ?? article.prices?.[0]?.net;
const taxRate = article.prices?.[0]?.taxRate ?? 0;
const gross = net != null ? +(net * (1 + taxRate / 100)).toFixed(2) : null;
const ean = (article.eans && article.eans.length > 0) ? article.eans[0] : '';
const basePrice = Array.isArray(article?.prices) && article.prices.length > 0
? article.prices[0]
: null;
const net = basePrice?.net != null && Number.isFinite(Number(basePrice.net))
? Number(basePrice.net)
: null;
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
: null;
return str
.replace(/\{\{(?:ARTICLE_NAME|article_name|name)\}\}/gi, article.name || '')
.replace(/\{\{SKU\}\}/gi, article.sku || '')
.replace(/\{\{(?:NET_PRICE|net_price)\}\}/gi, net != null ? net.toString() : '')
.replace(/\{\{(?:GROSS_PRICE|gross_price)\}\}/gi, gross != null ? gross.toString() : '')
.replace(/\{\{(?:PRICE|price)\}\}/gi, gross != null ? gross.toString() : (net != null ? net.toString() : ''))
.replace(/\{\{(?:EAN|ean)\}\}/gi, ean);
.replace(/\{\{name\}\}/gi, article?.name || '')
.replace(/\{\{price\}\}/gi, formatPrice(gross));
}
export function interpolateObjects(objects, article) {
if (!Array.isArray(objects)) return [];
const ean = Array.isArray(article?.eans) && article.eans.length > 0 ? String(article.eans[0]) : '';
return objects.map((obj) => {
const cloned = { ...obj };
if (typeof cloned.text === 'string') {
cloned.text = interpolateString(cloned.text, article);
}
if (cloned.type === 'barcode' || typeof cloned.code === 'string') {
const rawCode = String(cloned.code ?? '').trim();
if (rawCode === '0' || /^0+$/.test(rawCode)) {
if (ean) {
cloned.code = ean;
}
} else if (typeof cloned.code === 'string') {
cloned.code = interpolateString(cloned.code, article);
}
}
return cloned;
});
}
// #endregion

View File

@@ -253,7 +253,7 @@ export function register(app, basePath = '') {
const parsed = parseSku(req.params.sku);
if (parsed.error) return res.status(400).json({ error: parsed.error });
if (req.body?.sku || req.body?.childSku) {
if (req.body?.sku) {
return res.status(400).json({
error: 'sku is not allowed when creating a child article; omit sku to auto-generate or use PUT to attach an existing article',
});
@@ -282,9 +282,7 @@ export function register(app, basePath = '') {
const parsed = parseSku(req.params.sku);
if (parsed.error) return res.status(400).json({ error: parsed.error });
const childSku = typeof req.body?.sku === 'string' && req.body.sku.trim()
? req.body.sku.trim()
: (typeof req.body?.childSku === 'string' && req.body.childSku.trim() ? req.body.childSku.trim() : '');
const childSku = typeof req.body?.sku === 'string' ? req.body.sku.trim() : '';
if (!childSku) {
return res.status(400).json({ error: 'sku is required' });
}
@@ -354,7 +352,7 @@ export function register(app, basePath = '') {
const parsedChild = parseSku(req.params.childSku);
if (parsedChild.error) return res.status(400).json({ error: 'childSku is required' });
const deleteArticle = req.query.deleteArticle === 'true' || req.body?.deleteArticle === true;
const deleteArticle = req.query.deleteArticle === 'true';
try {
const result = await db.removeChild(req.user.id, parsed.sku, parsedChild.sku, {

View File

@@ -141,7 +141,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: skuParsed.error });
}
const propertiesInput = req.body?.properties ?? req.body;
const propertiesInput = req.body?.properties;
if (!Array.isArray(propertiesInput)) {
return res.status(400).json({ error: 'properties array required' });
}

View File

@@ -252,12 +252,12 @@ export function register(app, basePath = '') {
// #region POST /backup/:id/vfs/recover
// MARK: handles POST /backup/:id/vfs/recover to restore a VFS file from a backup
const handleRecover = async (req, res) => {
const filePath = req.params.path || req.body?.path || req.body?.file || req.body?.filename || req.query?.path;
const filePath = req.body?.path;
if (!filePath) {
return res.status(400).json({ error: 'File path is required' });
}
const requestedCompanyId = req.body?.companyId ?? req.query?.companyId;
const requestedCompanyId = req.body?.companyId;
const authorized = await resolveAuthorizedCompany(req, requestedCompanyId);
if (authorized.error) {
return res.status(authorized.status).json({ error: authorized.error });

View File

@@ -384,8 +384,6 @@ export function register(app, basePath = '') {
let includeChildren = true;
if (req.query.includeChildren !== undefined) {
includeChildren = req.query.includeChildren !== 'false' && req.query.includeChildren !== '0';
} else if (req.query.variants !== undefined) {
includeChildren = req.query.variants !== 'false' && req.query.variants !== '0';
}
try {
@@ -395,7 +393,7 @@ export function register(app, basePath = '') {
.map((a) => ({
...a,
childCount: Number(a.childCount ?? 0),
isParent: Boolean(a.isParent ?? a.is_parent),
isParent: Boolean(a.isParent),
smallThumbW: a.smallThumbW ?? null,
smallThumbH: a.smallThumbH ?? null,
smallThumbBinary: a.smallThumbBinary ?? null,

View File

@@ -153,7 +153,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: idParsed.error });
}
const propertiesInput = req.body?.properties ?? req.body;
const propertiesInput = req.body?.properties;
if (!Array.isArray(propertiesInput)) {
return res.status(400).json({ error: 'properties array required' });
}

View File

@@ -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 } from './article/shared.js';
import { interpolateString, interpolateObjects } from './article/shared.js';
// #region Helpers
@@ -590,16 +590,15 @@ export function register(app, basePath = '') {
// Resolve sample article for placeholder interpolation
let article = null;
if (req.body?.sku) {
try {
const result = await articleDb.getBySku(req.user.id, req.body.sku);
article = result?.article;
} catch {
// ignore
const skuStr = String(req.body.sku).trim();
const result = await articleDb.getBySku(req.user.id, skuStr);
if (!result?.article) {
return res.status(404).json({ error: 'Article not found' });
}
}
if (!article) {
article = result.article;
} else {
article = {
name: req.body?.name || template?.name || 'Musterartikel',
name: template?.name || 'Musterartikel',
sku: 'TEST-12345',
prices: [{ net: 19.99, taxRate: 19 }],
eans: ['4012345678901'],
@@ -625,16 +624,7 @@ export function register(app, basePath = '') {
objects = [];
}
const interpolatedObjects = objects.map((obj) => {
const cloned = { ...obj };
if (typeof cloned.text === 'string') {
cloned.text = interpolateString(cloned.text, article);
}
if (typeof cloned.code === 'string') {
cloned.code = interpolateString(cloned.code, article);
}
return cloned;
});
const interpolatedObjects = interpolateObjects(objects, article);
const printPayload = await renderLabel({
width: width || 448,

View File

@@ -101,7 +101,7 @@ export function register(app, basePath = '') {
}
const manifestarticleId = Number(req.params.manifestarticleId);
const transactionType = req.query.transaction_type ?? req.query.transactionType;
const transactionType = req.query.transaction_type;
try {
const ledger = await db.listByManifestarticleId(
@@ -186,7 +186,7 @@ export function register(app, basePath = '') {
}
const sku = req.params.sku;
const transactionType = req.query.transaction_type ?? req.query.transactionType;
const transactionType = req.query.transaction_type;
try {
const ledger = await db.listBySku(req.user.id, sku, transactionType);
@@ -254,9 +254,9 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: selected.error });
}
const rawManifestarticleId = req.query.manifestarticle_id ?? req.query.manifestarticleId;
const rawManifestarticleId = req.query.manifestarticle_id;
const sku = req.query.sku;
const transactionType = req.query.transaction_type ?? req.query.transactionType;
const transactionType = req.query.transaction_type;
if (rawManifestarticleId == null && (sku == null || !String(sku).trim())) {
return res.status(400).json({

View File

@@ -190,12 +190,8 @@ export function register(app, basePath = '') {
}
const pairingpin = String(randomInt(100000, 999999));
const rootCategoryId = req.body.rootCategoryId !== undefined
? req.body.rootCategoryId
: req.body.root_category_id;
const syncRootArticles = req.body.syncRootArticles !== undefined
? req.body.syncRootArticles
: req.body.sync_root_articles;
const rootCategoryId = req.body.rootCategoryId;
const syncRootArticles = req.body.syncRootArticles;
const sort = req.body.sort !== undefined ? Number(req.body.sort) : undefined;
if (sort !== undefined && !Number.isInteger(sort)) {
return res.status(400).json({ error: 'sort must be an integer' });
@@ -319,14 +315,12 @@ export function register(app, basePath = '') {
payload.name = req.body.name.trim();
}
if (req.body.rootCategoryId !== undefined || req.body.root_category_id !== undefined) {
const rawCat = req.body.rootCategoryId !== undefined ? req.body.rootCategoryId : req.body.root_category_id;
payload.rootCategoryId = rawCat != null ? Number(rawCat) : null;
if (req.body.rootCategoryId !== undefined) {
payload.rootCategoryId = req.body.rootCategoryId != null ? Number(req.body.rootCategoryId) : null;
}
if (req.body.syncRootArticles !== undefined || req.body.sync_root_articles !== undefined) {
const rawSync = req.body.syncRootArticles !== undefined ? req.body.syncRootArticles : req.body.sync_root_articles;
payload.syncRootArticles = Boolean(rawSync);
if (req.body.syncRootArticles !== undefined) {
payload.syncRootArticles = Boolean(req.body.syncRootArticles);
}
if (req.body.sort !== undefined) {

View File

@@ -221,7 +221,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: selected.error });
}
const rawApId = req.body?.agentprinterId ?? req.body?.agentprinterid;
const rawApId = req.body?.agentprinterId;
const parsedApId = parsePositiveInt(rawApId, 'agentprinterId');
if (parsedApId.error) {
return res.status(400).json({ error: parsedApId.error });
@@ -302,7 +302,7 @@ export function register(app, basePath = '') {
}
try {
const rawApId = req.body?.agentprinterId ?? req.query?.agentprinterId;
const rawApId = req.body?.agentprinterId;
const agentprinterId = rawApId != null ? Number(rawApId) : undefined;
const allJobs = await printerjobDb.list(req.user.id, { agentprinterId });
const cancellable = allJobs.filter((j) => j.state === 'pending' || j.state === 'processing');

View File

@@ -517,7 +517,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: idParsed.error });
}
const skus = parseSkus(req.query.skus ?? req.query.sku);
const skus = parseSkus(req.query.skus);
try {
const articles = await db.listArticlesBySkus(req.user.id, idParsed.id, skus);
@@ -540,7 +540,7 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: idParsed.error });
}
const skus = parseSkus(req.body?.skus ?? req.body?.sku);
const skus = parseSkus(req.body?.skus);
try {
const articles = await db.listArticlesBySkus(req.user.id, idParsed.id, skus);

View File

@@ -685,7 +685,7 @@ export function register(app, basePath = '') {
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const raw = req.body?.path || req.body?.dir || req.body?.name;
const raw = req.body?.path;
const parsed = parseVfsRef(raw);
if (parsed.error) {
return res.status(400).json({ error: parsed.error });
@@ -787,8 +787,8 @@ export function register(app, basePath = '') {
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const rawOld = req.body?.oldPath || req.body?.from || req.body?.source;
const rawNew = req.body?.newPath || req.body?.to || req.body?.target;
const rawOld = req.body?.oldPath;
const rawNew = req.body?.newPath;
const parsedOld = parseVfsRef(rawOld);
if (parsedOld.error) {
return res.status(400).json({ error: `oldPath: ${parsedOld.error}` });