u
This commit is contained in:
@@ -15,11 +15,12 @@ import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import getBySku from './getBySku.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import slugify from '../slugify.js';
|
||||
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
||||
|
||||
export default function addChild(provider) {
|
||||
const resolve = resolveArticleBySku(provider);
|
||||
const load = getBySku(provider);
|
||||
const resolveSlug = resolveUniqueSlug(provider);
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
|
||||
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||
@@ -117,10 +118,46 @@ export default function addChild(provider) {
|
||||
throw new Error('Attribute value is required for child article');
|
||||
}
|
||||
|
||||
// Compute child SKU: father.sku + '-' + finalValueId
|
||||
const childSku = `${father.sku}-${finalValueId}`;
|
||||
// Check if father already has a variant with this attribute value
|
||||
const existingVariantRes = await runner.query(
|
||||
`SELECT a.id
|
||||
FROM article a
|
||||
INNER JOIN articleattributevalue aav ON aav.article_id = a.id
|
||||
WHERE a.fatherarticle_id = @fatherId
|
||||
AND aav.attributevalue_id = @finalValueId`,
|
||||
{ fatherId, finalValueId },
|
||||
);
|
||||
if (existingVariantRes.rows.length > 0) {
|
||||
throw new Error('Variant with this attribute value already exists');
|
||||
}
|
||||
|
||||
// Check if child SKU already exists
|
||||
// Compute child SKU:
|
||||
// If options.sku is provided, use it.
|
||||
// Otherwise, derive using next free index: fatherSku-1, fatherSku-2, ...
|
||||
let childSku = (typeof options.sku === 'string' && options.sku.trim()) || null;
|
||||
if (!childSku) {
|
||||
const existingSkusRes = await runner.query(
|
||||
'SELECT sku FROM article WHERE company_id = @companyId AND sku LIKE @prefix',
|
||||
{ companyId, prefix: `${father.sku}-%` },
|
||||
);
|
||||
const usedIndices = new Set();
|
||||
const prefix = `${father.sku}-`;
|
||||
for (const row of existingSkusRes.rows) {
|
||||
if (row.sku && row.sku.startsWith(prefix)) {
|
||||
const numPart = row.sku.slice(prefix.length);
|
||||
if (/^\d+$/.test(numPart)) {
|
||||
usedIndices.add(parseInt(numPart, 10));
|
||||
}
|
||||
}
|
||||
}
|
||||
let nextIndex = 1;
|
||||
while (usedIndices.has(nextIndex)) {
|
||||
nextIndex++;
|
||||
}
|
||||
childSku = `${father.sku}-${nextIndex}`;
|
||||
}
|
||||
|
||||
// Check if child SKU already exists (e.g. orphan holding SKU or custom SKU)
|
||||
const existingChildRes = await runner.query(
|
||||
'SELECT id FROM article WHERE sku = @childSku AND company_id = @companyId',
|
||||
{ childSku, companyId },
|
||||
@@ -129,9 +166,16 @@ export default function addChild(provider) {
|
||||
throw new Error(`Child article "${childSku}" already exists`);
|
||||
}
|
||||
|
||||
// Next fathersort
|
||||
const countRes = await runner.query(getChildCountSql, { articleId: fatherId });
|
||||
const fathersort = countRes.rows[0]?.count ?? 0;
|
||||
// Next free fathersort (smallest non-negative integer not used by any child of this father)
|
||||
const existingSortsRes = await runner.query(
|
||||
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
|
||||
{ fatherId },
|
||||
);
|
||||
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
|
||||
let fathersort = 0;
|
||||
while (usedSorts.has(fathersort)) {
|
||||
fathersort++;
|
||||
}
|
||||
|
||||
// Create child article
|
||||
const rawTrack = options.track_stock ?? options.trackStock;
|
||||
@@ -186,7 +230,11 @@ export default function addChild(provider) {
|
||||
|
||||
// Create article text
|
||||
const childName = options.name?.trim() || `${fatherName} - ${finalValue}`;
|
||||
const childSlug = slugify(childName);
|
||||
const childSlug = await resolveSlug(runner, {
|
||||
companyId,
|
||||
languageId,
|
||||
name: childName,
|
||||
});
|
||||
await runner.query(createArticleTextSql, {
|
||||
articleId: childId,
|
||||
languageId,
|
||||
@@ -194,13 +242,20 @@ export default function addChild(provider) {
|
||||
slug: childSlug,
|
||||
});
|
||||
|
||||
// History
|
||||
// History on child
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: childId,
|
||||
companyuserId,
|
||||
text: `Artikel "${childName}" (${childSku}) als Variante von "${father.sku}" angelegt`,
|
||||
});
|
||||
|
||||
// History on father
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: fatherId,
|
||||
companyuserId,
|
||||
text: `Variante "${childName}" (${childSku}) hinzugefügt`,
|
||||
});
|
||||
|
||||
return load(googleAuthId, childSku, runner);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,12 +11,13 @@ import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
||||
import ensureDefaultUnit from '../unit/ensureDefault.js';
|
||||
import nextSku from '../numbers/nextSku.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import slugify from '../slugify.js';
|
||||
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
||||
|
||||
export default function createUnderParent(provider) {
|
||||
const access = resolveCategoryAccess(provider);
|
||||
const ensureUnit = ensureDefaultUnit(provider);
|
||||
const allocateSku = nextSku(provider);
|
||||
const resolveSlug = resolveUniqueSlug(provider);
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
|
||||
const createArticleSql = isSqlite ? SQLITE_CREATE_ARTICLE : MSSQL_CREATE_ARTICLE;
|
||||
@@ -53,7 +54,7 @@ export default function createUnderParent(provider) {
|
||||
|
||||
const unitId = await ensureUnit(runner, languageId, companyId);
|
||||
const sku = await allocateSku(companyId, runner);
|
||||
const slug = slugify(name);
|
||||
const slug = await resolveSlug(runner, { companyId, languageId, name });
|
||||
|
||||
let article;
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as mssqlQueries from '../../providers/mssql/queries/articleDelete.js';
|
||||
import * as sqliteQueries from '../../providers/sqlite3/queries/articleDelete.js';
|
||||
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
||||
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
@@ -7,6 +9,7 @@ export default function del(provider) {
|
||||
const resolve = resolveArticleBySku(provider);
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||
|
||||
/**
|
||||
* Deletes an article by its SKU, refusing when it still has children unless cascade is true.
|
||||
@@ -40,6 +43,8 @@ export default function del(provider) {
|
||||
const listIds = await runner.query(queries.GET_LIST_IDS, { articleId: id });
|
||||
const { articlelistId, optionslistId } = listIds.rows[0] ?? {};
|
||||
|
||||
await runner.query(queries.CREATE_TOMBSTONE, { articleId: id });
|
||||
|
||||
await runner.query(queries.DELETE_PICS_BY_ARTICLE, { articleId: id });
|
||||
|
||||
const deleted = await runner.query(queries.DELETE_BY_ID, { articleId: id });
|
||||
@@ -64,16 +69,38 @@ export default function del(provider) {
|
||||
await deleteArticleAndLists(id);
|
||||
};
|
||||
|
||||
const fatherRes = await runner.query(
|
||||
'SELECT fatherarticle_id FROM article WHERE id = @articleId',
|
||||
{ articleId },
|
||||
);
|
||||
const fatherArticleId = fatherRes.rows[0]?.fatherarticle_id;
|
||||
|
||||
const childCount = await runner.query(queries.GET_CHILD_COUNT, { articleId });
|
||||
if (childCount.rows[0].count > 0) {
|
||||
if (!cascade) {
|
||||
throw new Error('Article has children');
|
||||
}
|
||||
await deleteSubtree(articleId);
|
||||
return;
|
||||
} else {
|
||||
await deleteArticleAndLists(articleId);
|
||||
}
|
||||
|
||||
await deleteArticleAndLists(articleId);
|
||||
if (fatherArticleId != null) {
|
||||
const remainingChildrenRes = await runner.query(
|
||||
'SELECT id, fathersort FROM article WHERE fatherarticle_id = @fatherArticleId ORDER BY fathersort ASC, id ASC',
|
||||
{ fatherArticleId },
|
||||
);
|
||||
for (let i = 0; i < remainingChildrenRes.rows.length; i++) {
|
||||
const childRow = remainingChildrenRes.rows[i];
|
||||
if (Number(childRow.fathersort) !== i) {
|
||||
await runner.query(articleQueries.SET_FATHER_SORT, {
|
||||
articleId: childRow.id,
|
||||
fatherArticleId,
|
||||
fathersort: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (tx) {
|
||||
|
||||
@@ -1,28 +1,50 @@
|
||||
import { LIST_BY_ARTICLE_ID as MSSQL_QUERY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { LIST_BY_ARTICLE_ID as SQLITE_QUERY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import { LIST_BY_ARTICLE_ID as MSSQL_QUERY, LIST_BY_ARTICLE_ID_LIMITED as MSSQL_QUERY_LIMITED } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { LIST_BY_ARTICLE_ID as SQLITE_QUERY, LIST_BY_ARTICLE_ID_LIMITED as SQLITE_QUERY_LIMITED } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
export default function getHistoryBySku(provider) {
|
||||
const resolve = resolveArticleBySku(provider);
|
||||
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
const sql = isSqlite ? SQLITE_QUERY : MSSQL_QUERY;
|
||||
const sqlLimited = isSqlite ? SQLITE_QUERY_LIMITED : MSSQL_QUERY_LIMITED;
|
||||
|
||||
/**
|
||||
* Returns the change history of an article by its SKU.
|
||||
*
|
||||
* @param {number} googleAuthId - The id of the Google auth user.
|
||||
* @param {string} sku - The article SKU.
|
||||
* @param {object} [opts] - Optional options.
|
||||
* @param {number} [opts.limit] - Maximum number of entries to return (newest first).
|
||||
* @param {object} [tx] - Optional transaction.
|
||||
* @returns {Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>} The article history entries.
|
||||
*
|
||||
* @example
|
||||
* const history = await store.article.getHistoryBySku(42, 'SKU-1');
|
||||
* // => [{ id, companyuserId, date, text }]
|
||||
*
|
||||
* const recent = await store.article.getHistoryBySku(42, 'SKU-1', { limit: 5 });
|
||||
* // => up to 5 most recent entries
|
||||
*/
|
||||
return async (googleAuthId, sku, tx) => {
|
||||
const { articleId } = await resolve(googleAuthId, sku, tx);
|
||||
const runner = makeRunner(provider, tx);
|
||||
const result = await runner.query(sql, { articleId });
|
||||
return async (googleAuthId, sku, opts, tx) => {
|
||||
// Support legacy call signature: getHistoryBySku(googleAuthId, sku, tx)
|
||||
let limit = null;
|
||||
let resolvedTx = tx;
|
||||
if (opts != null && typeof opts === 'object' && !opts._isTx) {
|
||||
limit = opts.limit != null ? Number(opts.limit) : null;
|
||||
} else if (opts != null) {
|
||||
resolvedTx = opts;
|
||||
}
|
||||
|
||||
const runner = makeRunner(provider, resolvedTx);
|
||||
const { articleId } = await resolve(googleAuthId, sku, runner);
|
||||
|
||||
let result;
|
||||
if (limit != null && Number.isInteger(limit) && limit > 0) {
|
||||
result = await runner.query(sqlLimited, { articleId, limit });
|
||||
} else {
|
||||
result = await runner.query(sql, { articleId });
|
||||
}
|
||||
|
||||
return result.rows.map((row) => ({
|
||||
id: row.id,
|
||||
@@ -32,4 +54,3 @@ export default function getHistoryBySku(provider) {
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA
|
||||
import ensureByPath from '../category/ensureByPath.js';
|
||||
import ensureDefaultUnit from '../unit/ensureDefault.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import slugify from '../slugify.js';
|
||||
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
||||
|
||||
// Normalizes a numeric value that may arrive as a string using German decimal
|
||||
// notation (e.g. "11,00"), a JS number, or null/undefined. Returns null when empty.
|
||||
@@ -49,6 +49,7 @@ export default function importRows(provider) {
|
||||
const companyAccess = resolveSelectedCompanyAccess(provider);
|
||||
const ensureUnit = ensureDefaultUnit(provider);
|
||||
const ensureCategoryByPath = ensureByPath(provider);
|
||||
const resolveSlug = resolveUniqueSlug(provider);
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
|
||||
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||
@@ -167,7 +168,7 @@ export default function importRows(provider) {
|
||||
try {
|
||||
await runInTx(async (runner) => {
|
||||
const unitId = await ensureUnit(runner, companyId, languageId);
|
||||
const slug = slugify(name);
|
||||
const slug = await resolveSlug(runner, { companyId, languageId, name });
|
||||
|
||||
const rawTrack = row.track_stock ?? row.trackStock;
|
||||
const trackStock = rawTrack == null ? 1 : (rawTrack ? 1 : 0);
|
||||
@@ -286,22 +287,52 @@ export default function importRows(provider) {
|
||||
const child = await runner.query(articleQueries.GET_ID_BY_SKU, { sku, companyId });
|
||||
const childId = child.rows[0]?.articleId;
|
||||
if (childId) {
|
||||
const father = await runner.query(articleQueries.GET_ID_BY_SKU, { sku: fatherSku, companyId });
|
||||
const fatherId = father.rows[0]?.articleId;
|
||||
const father = await runner.query(
|
||||
'SELECT id, is_father FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
|
||||
{ sku: fatherSku, companyId },
|
||||
);
|
||||
const fatherRow = father.rows[0];
|
||||
const fatherId = fatherRow?.id;
|
||||
if (fatherId && fatherId !== childId) {
|
||||
const childCount = await runner.query(getChildCountSql, {
|
||||
articleId: fatherId,
|
||||
});
|
||||
const count = childCount.rows[0].count;
|
||||
const wasFather = Boolean(fatherRow.is_father);
|
||||
|
||||
const existingSortsRes = await runner.query(
|
||||
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
|
||||
{ fatherId },
|
||||
);
|
||||
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
|
||||
let fathersort = 0;
|
||||
while (usedSorts.has(fathersort)) {
|
||||
fathersort++;
|
||||
}
|
||||
await runner.query(articleQueries.UPDATE_FATHER_BY_SKU, {
|
||||
sku,
|
||||
companyId,
|
||||
fatherArticleId: fatherId,
|
||||
fathersort: count,
|
||||
fathersort,
|
||||
});
|
||||
await runner.query(articleQueries.SET_IS_FATHER, {
|
||||
articleId: fatherId,
|
||||
});
|
||||
|
||||
if (!wasFather) {
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: fatherId,
|
||||
companyuserId,
|
||||
text: 'Artikel zu Vaterartikel konvertiert',
|
||||
});
|
||||
}
|
||||
const childName = String(row?.name ?? sku).trim();
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: fatherId,
|
||||
companyuserId,
|
||||
text: `Variante "${childName}" (${sku}) hinzugefügt`,
|
||||
});
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: childId,
|
||||
companyuserId,
|
||||
text: `Artikel "${childName}" (${sku}) als Variante von "${fatherSku}" zugewiesen`,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function removeChild(provider) {
|
||||
*/
|
||||
return async (googleAuthId, fatherSku, childSku, options = {}, tx) => {
|
||||
const run = async (runner) => {
|
||||
const { articleId: fatherId, companyuserId } = await resolve(
|
||||
const { articleId: fatherId, languageId, companyuserId } = await resolve(
|
||||
googleAuthId,
|
||||
fatherSku,
|
||||
runner,
|
||||
@@ -45,9 +45,45 @@ export default function removeChild(provider) {
|
||||
throw new Error('Child article does not belong to this father');
|
||||
}
|
||||
|
||||
// Fetch child name before any deletion (needed for history regardless of path)
|
||||
const childNameRes = await runner.query(
|
||||
`SELECT at.name FROM articletext at
|
||||
INNER JOIN article a ON a.id = at.article_id
|
||||
WHERE a.id = @childId AND at.language_id = @languageId`,
|
||||
{ childId, languageId },
|
||||
);
|
||||
const childName = childNameRes.rows[0]?.name || childSku;
|
||||
|
||||
const recompactFatherSort = async () => {
|
||||
const remainingChildrenRes = await runner.query(
|
||||
'SELECT id, fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC, id ASC',
|
||||
{ fatherId },
|
||||
);
|
||||
for (let i = 0; i < remainingChildrenRes.rows.length; i++) {
|
||||
const childRow = remainingChildrenRes.rows[i];
|
||||
if (Number(childRow.fathersort) !== i) {
|
||||
await runner.query(queries.SET_FATHER_SORT, {
|
||||
articleId: childRow.id,
|
||||
fatherArticleId: fatherId,
|
||||
fathersort: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (options.deleteArticle) {
|
||||
const cascade = options.cascade !== undefined ? Boolean(options.cascade) : true;
|
||||
await del(googleAuthId, childSku, { cascade }, runner);
|
||||
|
||||
await recompactFatherSort();
|
||||
|
||||
// History on father (child article is gone so we can only log on father)
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: fatherId,
|
||||
companyuserId,
|
||||
text: `Variante "${childName}" (${childSku}) gelöscht`,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
fatherSku,
|
||||
@@ -81,13 +117,22 @@ export default function removeChild(provider) {
|
||||
);
|
||||
}
|
||||
|
||||
// Add history
|
||||
await recompactFatherSort();
|
||||
|
||||
// Add history on child
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: childId,
|
||||
companyuserId,
|
||||
text: `Artikel "${childSku}" von Vaterartikel "${fatherSku}" getrennt`,
|
||||
});
|
||||
|
||||
// Add history on father
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: fatherId,
|
||||
companyuserId,
|
||||
text: `Variante "${childName}" (${childSku}) getrennt`,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
fatherSku,
|
||||
|
||||
51
src/store/article/resolveUniqueSlug.js
Normal file
51
src/store/article/resolveUniqueSlug.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { LIST_SLUGS_BY_COMPANY_AND_LANGUAGE as MSSQL_LIST_SLUGS } from '../../providers/mssql/queries/articletext.js';
|
||||
import { LIST_SLUGS_BY_COMPANY_AND_LANGUAGE as SQLITE_LIST_SLUGS } from '../../providers/sqlite3/queries/articletext.js';
|
||||
import slugify from '../slugify.js';
|
||||
|
||||
export default function resolveUniqueSlug(provider) {
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
const listSlugsSql = isSqlite ? SQLITE_LIST_SLUGS : MSSQL_LIST_SLUGS;
|
||||
|
||||
/**
|
||||
* Resolves a company- and language-unique slug for an article.
|
||||
* Disambiguates duplicate slugs with a numeric suffix (-2, -3, ...).
|
||||
*
|
||||
* @param {object} runner - DB runner or tx.
|
||||
* @param {object} params
|
||||
* @param {number} params.companyId - The company ID.
|
||||
* @param {number} params.languageId - The language ID.
|
||||
* @param {string} params.name - The article name.
|
||||
* @param {number} [params.excludeArticleId] - Optional article ID to exclude (e.g. for updates).
|
||||
* @returns {Promise<string>} The unique slug.
|
||||
*/
|
||||
return async (runner, { companyId, languageId, name, excludeArticleId }) => {
|
||||
const baseSlug = slugify(name) || 'article';
|
||||
const likePattern = `${baseSlug}-%`;
|
||||
|
||||
const res = await runner.query(listSlugsSql, {
|
||||
companyId,
|
||||
languageId,
|
||||
baseSlug,
|
||||
likePattern,
|
||||
});
|
||||
|
||||
const existingSlugs = new Set();
|
||||
for (const row of res.rows) {
|
||||
const artId = row.articleId ?? row.article_id;
|
||||
if (excludeArticleId != null && artId === excludeArticleId) {
|
||||
continue;
|
||||
}
|
||||
existingSlugs.add(row.slug);
|
||||
}
|
||||
|
||||
if (!existingSlugs.has(baseSlug)) {
|
||||
return baseSlug;
|
||||
}
|
||||
|
||||
let suffix = 2;
|
||||
while (existingSlugs.has(`${baseSlug}-${suffix}`)) {
|
||||
suffix++;
|
||||
}
|
||||
return `${baseSlug}-${suffix}`;
|
||||
};
|
||||
}
|
||||
@@ -47,25 +47,29 @@ export default function setBrandIdBySku(provider) {
|
||||
throw new Error('Invalid brandId');
|
||||
}
|
||||
|
||||
// Read old brand for history
|
||||
// Read old brand name (null = no brand assigned)
|
||||
const oldBrandRes = await runner.query(
|
||||
`SELECT b.name FROM article a LEFT JOIN brand b ON b.id = a.brand_id WHERE a.id = @articleId`,
|
||||
{ articleId },
|
||||
);
|
||||
const oldBrandName = oldBrandRes.rows[0]?.name ?? '';
|
||||
const oldBrandName = oldBrandRes.rows[0]?.name ?? null;
|
||||
|
||||
let resolvedBrandId = null;
|
||||
let newBrandName = '';
|
||||
let newBrandName = null;
|
||||
if (brandId !== null) {
|
||||
const brand = await runner.query(getBrandSql, {
|
||||
brandId,
|
||||
companyId,
|
||||
});
|
||||
if (!brand.rows[0]) {
|
||||
// Verify brand belongs to this company
|
||||
const brandCheck = await runner.query(getBrandSql, { brandId, companyId });
|
||||
if (!brandCheck.rows[0]) {
|
||||
throw new Error('Brand not found');
|
||||
}
|
||||
resolvedBrandId = brandId;
|
||||
newBrandName = brand.rows[0].name ?? '';
|
||||
|
||||
// GET_BRAND_BY_COMPANY_AND_ID only selects `id`, so fetch name separately
|
||||
const brandNameRes = await runner.query(
|
||||
'SELECT name FROM brand WHERE id = @brandId',
|
||||
{ brandId },
|
||||
);
|
||||
newBrandName = brandNameRes.rows[0]?.name ?? null;
|
||||
}
|
||||
|
||||
const result = await runner.query(updateBrandSql, {
|
||||
@@ -77,12 +81,12 @@ export default function setBrandIdBySku(provider) {
|
||||
throw new Error('Article not found');
|
||||
}
|
||||
|
||||
// Write history if brand changed
|
||||
// null = no brand; treat as distinct sentinel so first assignment is logged
|
||||
if (oldBrandName !== newBrandName) {
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Marke geändert von "${oldBrandName}" auf "${newBrandName}"`,
|
||||
text: `Marke ge\u00e4ndert von "${oldBrandName ?? ''}" auf "${newBrandName ?? ''}"`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -94,4 +98,3 @@ export default function setBrandIdBySku(provider) {
|
||||
return article;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
||||
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
||||
import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
|
||||
import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js';
|
||||
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||
import queryBySku from './queryBySku.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import makeRunner from '../runner.js';
|
||||
@@ -13,6 +15,7 @@ export default function setFatherBySku(provider) {
|
||||
|
||||
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
|
||||
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||
|
||||
/**
|
||||
* Links an article to a father article (V/K compound) by their SKUs.
|
||||
@@ -30,14 +33,47 @@ export default function setFatherBySku(provider) {
|
||||
*/
|
||||
return async (googleAuthId, sku, fatherSku, tx) => {
|
||||
const runSet = async (runner) => {
|
||||
const { articleId, languageId, companyId } = await resolve(googleAuthId, sku, runner);
|
||||
const { articleId, languageId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
|
||||
|
||||
const childTextRes = await runner.query(
|
||||
'SELECT name FROM articletext WHERE article_id = @articleId AND language_id = @languageId',
|
||||
{ articleId, languageId },
|
||||
);
|
||||
const childName = childTextRes.rows[0]?.name || sku;
|
||||
|
||||
if (fatherSku == null || fatherSku === '') {
|
||||
const prevFatherRes = await runner.query(
|
||||
'SELECT fatherarticle_id FROM article WHERE id = @articleId',
|
||||
{ articleId },
|
||||
);
|
||||
const prevFatherId = prevFatherRes.rows[0]?.fatherarticle_id;
|
||||
|
||||
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
|
||||
sku,
|
||||
companyId,
|
||||
fatherArticleId: null,
|
||||
});
|
||||
|
||||
if (prevFatherId) {
|
||||
const prevFatherTextRes = await runner.query(
|
||||
'SELECT sku FROM article WHERE id = @prevFatherId',
|
||||
{ prevFatherId },
|
||||
);
|
||||
const prevFatherSku = prevFatherTextRes.rows[0]?.sku || String(prevFatherId);
|
||||
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Artikel "${sku}" von Vaterartikel "${prevFatherSku}" getrennt`,
|
||||
});
|
||||
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: prevFatherId,
|
||||
companyuserId,
|
||||
text: `Variante "${childName}" (${sku}) getrennt`,
|
||||
});
|
||||
}
|
||||
|
||||
const unlinked = await load(articleId, languageId, runner);
|
||||
if (!unlinked) {
|
||||
throw new Error('Article not found');
|
||||
@@ -45,8 +81,12 @@ export default function setFatherBySku(provider) {
|
||||
return unlinked;
|
||||
}
|
||||
|
||||
const father = await runner.query(queries.GET_ID_BY_SKU, { sku: fatherSku, companyId });
|
||||
const fatherId = father.rows[0]?.articleId;
|
||||
const father = await runner.query(
|
||||
'SELECT id, is_father, sku FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
|
||||
{ sku: fatherSku, companyId },
|
||||
);
|
||||
const fatherRow = father.rows[0];
|
||||
const fatherId = fatherRow?.id;
|
||||
if (!fatherId) {
|
||||
throw new Error('Father not found');
|
||||
}
|
||||
@@ -54,22 +94,49 @@ export default function setFatherBySku(provider) {
|
||||
throw new Error('Invalid father');
|
||||
}
|
||||
|
||||
const childCount = await runner.query(getChildCountSql, {
|
||||
articleId: fatherId,
|
||||
});
|
||||
const count = childCount.rows[0].count;
|
||||
const wasFather = Boolean(fatherRow.is_father);
|
||||
|
||||
const existingSortsRes = await runner.query(
|
||||
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
|
||||
{ fatherId },
|
||||
);
|
||||
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
|
||||
let fathersort = 0;
|
||||
while (usedSorts.has(fathersort)) {
|
||||
fathersort++;
|
||||
}
|
||||
|
||||
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
|
||||
sku,
|
||||
companyId,
|
||||
fatherArticleId: fatherId,
|
||||
fathersort: count,
|
||||
fathersort,
|
||||
});
|
||||
|
||||
await runner.query(queries.SET_IS_FATHER, {
|
||||
articleId: fatherId,
|
||||
});
|
||||
|
||||
if (!wasFather) {
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: fatherId,
|
||||
companyuserId,
|
||||
text: 'Artikel zu Vaterartikel konvertiert',
|
||||
});
|
||||
}
|
||||
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId: fatherId,
|
||||
companyuserId,
|
||||
text: `Variante "${childName}" (${sku}) hinzugefügt`,
|
||||
});
|
||||
|
||||
await runner.query(insertHistorySql, {
|
||||
articleId,
|
||||
companyuserId,
|
||||
text: `Artikel "${sku}" als Variante von "${fatherSku}" zugewiesen`,
|
||||
});
|
||||
|
||||
const article = await load(articleId, languageId, runner);
|
||||
if (!article) {
|
||||
throw new Error('Article not found');
|
||||
|
||||
@@ -11,7 +11,7 @@ import { GET_FULL_BY_SI as SQLITE_GET_UNIT } from '../../providers/sqlite3/queri
|
||||
import queryBySku from './queryBySku.js';
|
||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import slugify from '../slugify.js';
|
||||
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
||||
|
||||
function formatUnitLabel(amount, symbol) {
|
||||
const sym = symbol ?? '';
|
||||
@@ -24,6 +24,7 @@ function formatUnitLabel(amount, symbol) {
|
||||
export default function updateBySku(provider) {
|
||||
const resolve = resolveArticleBySku(provider);
|
||||
const load = queryBySku(provider);
|
||||
const resolveSlug = resolveUniqueSlug(provider);
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
|
||||
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||
@@ -144,7 +145,12 @@ export default function updateBySku(provider) {
|
||||
}
|
||||
|
||||
if (hasName) {
|
||||
const slug = slugify(patch.name);
|
||||
const slug = await resolveSlug(runner, {
|
||||
companyId,
|
||||
languageId,
|
||||
name: patch.name,
|
||||
excludeArticleId: articleId,
|
||||
});
|
||||
const update = await runner.query(updateTextSql, {
|
||||
articleId,
|
||||
languageId,
|
||||
|
||||
Reference in New Issue
Block a user