u
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}` });
|
||||
|
||||
Reference in New Issue
Block a user