This commit is contained in:
seb
2026-10-04 21:22:05 +02:00
parent 0268583ae3
commit 4718cb5726
48 changed files with 758 additions and 758 deletions

View File

@@ -34,24 +34,24 @@ export default function addChild(provider) {
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Adds a child article to a father article (creates and attaches with auto-generated SKU).
* Adds a child article to a parent article (creates and attaches with auto-generated SKU).
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} fatherSku - The father article SKU.
* @param {string} parentSku - The parent article SKU.
* @param {object} options
* @param {boolean} [options.attach=false] - If true, attaches an existing article instead of creating.
* @param {number} [options.attributeValueId] - Existing attribute value ID for father's attribute.
* @param {string} [options.value] - String value to find or create under father's attribute.
* @param {string} [options.name] - Custom name for child article (defaults to father name + value).
* @param {number} [options.attributeValueId] - Existing attribute value ID for parent's attribute.
* @param {string} [options.value] - String value to find or create under parent's attribute.
* @param {string} [options.name] - Custom name for child article (defaults to parent name + value).
* @param {number} [options.unitamount] - Optional unitamount override.
* @param {number} [options.weight] - Optional weight override.
* @param {boolean|number} [options.track_stock] - Optional track_stock override.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<object>} The created child article detail.
*/
return async (googleAuthId, fatherSku, options = {}, tx) => {
return async (googleAuthId, parentSku, options = {}, tx) => {
if (options.attach) {
return attach(googleAuthId, fatherSku, options, tx);
return attach(googleAuthId, parentSku, options, tx);
}
const run = async (runner) => {
@@ -59,31 +59,31 @@ export default function addChild(provider) {
throw new Error('sku is not allowed when creating a child article; omit sku to auto-generate or use PUT to attach an existing article');
}
const { articleId: fatherId, companyId, languageId, companyuserId } = await resolve(
const { articleId: parentId, companyId, languageId, companyuserId } = await resolve(
googleAuthId,
fatherSku,
parentSku,
runner,
);
// Fetch father article fields
const fatherRes = await runner.query(
'SELECT id, sku, company_id, unit_id, unitamount, weight, track_stock, brand_id, is_father, fatherarticle_id, fatherattributename_id FROM article WHERE id = @fatherId',
{ fatherId },
// Fetch parent article fields
const parentRes = await runner.query(
'SELECT id, sku, company_id, unit_id, unitamount, weight, track_stock, brand_id, is_parent, parentarticle_id, parentattributename_id FROM article WHERE id = @parentId',
{ parentId },
);
const father = fatherRes.rows[0];
if (!father) {
throw new Error('Father article not found');
const parent = parentRes.rows[0];
if (!parent) {
throw new Error('Parent article not found');
}
if (!father.is_father || !father.fatherattributename_id) {
throw new Error('Article is not a father article');
if (!parent.is_parent || !parent.parentattributename_id) {
throw new Error('Article is not a parent article');
}
// Fetch father's text / name
const fatherTextRes = await runner.query(
'SELECT name FROM articletext WHERE article_id = @fatherId AND language_id = @languageId',
{ fatherId, languageId },
// Fetch parent's text / name
const parentTextRes = await runner.query(
'SELECT name FROM articletext WHERE article_id = @parentId AND language_id = @languageId',
{ parentId, languageId },
);
const fatherName = fatherTextRes.rows[0]?.name || father.sku;
const parentName = parentTextRes.rows[0]?.name || parent.sku;
// Determine attribute value
let finalValueId = null;
@@ -98,15 +98,15 @@ export default function addChild(provider) {
companyId,
});
const row = valRes.rows[0];
if (!row || row.attributeId !== father.fatherattributename_id) {
throw new Error('Attribute value not found or does not match father attribute');
if (!row || row.attributeId !== parent.parentattributename_id) {
throw new Error('Attribute value not found or does not match parent attribute');
}
finalValueId = row.id;
finalValue = row.value;
} else if (typeof options.value === 'string' && options.value.trim()) {
const valStr = options.value.trim();
const existingValRes = await runner.query(attrValQueries.GET_BY_ATTRIBUTE_AND_VALUE, {
attributeId: father.fatherattributename_id,
attributeId: parent.parentattributename_id,
value: valStr,
});
if (existingValRes.rows[0]) {
@@ -114,11 +114,11 @@ export default function addChild(provider) {
finalValue = existingValRes.rows[0].value;
} else {
const sortRes = await runner.query(attrValQueries.GET_NEXT_SORT, {
attributeId: father.fatherattributename_id,
attributeId: parent.parentattributename_id,
});
const sort = sortRes.rows[0]?.sort ?? 0;
const createdValRes = await runner.query(attrValQueries.CREATE, {
attributeId: father.fatherattributename_id,
attributeId: parent.parentattributename_id,
value: valStr,
sort,
});
@@ -129,26 +129,26 @@ export default function addChild(provider) {
throw new Error('Attribute value is required for child article');
}
// Check if father already has a variant with this attribute value
// Check if parent 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
WHERE a.parentarticle_id = @parentId
AND aav.attributevalue_id = @finalValueId`,
{ fatherId, finalValueId },
{ parentId, finalValueId },
);
if (existingVariantRes.rows.length > 0) {
throw new Error('Variant with this attribute value already exists');
}
// Compute child SKU using next free index: fatherSku-1, fatherSku-2, ...
// Compute child SKU using next free index: parentSku-1, parentSku-2, ...
const existingSkusRes = await runner.query(
'SELECT sku FROM article WHERE company_id = @companyId AND sku LIKE @prefix',
{ companyId, prefix: `${father.sku}-%` },
{ companyId, prefix: `${parent.sku}-%` },
);
const usedIndices = new Set();
const prefix = `${father.sku}-`;
const prefix = `${parent.sku}-`;
for (const row of existingSkusRes.rows) {
if (row.sku && row.sku.startsWith(prefix)) {
const numPart = row.sku.slice(prefix.length);
@@ -161,42 +161,42 @@ export default function addChild(provider) {
while (usedIndices.has(nextIndex)) {
nextIndex++;
}
const childSku = `${father.sku}-${nextIndex}`;
const childSku = `${parent.sku}-${nextIndex}`;
// Next free fathersort (smallest non-negative integer not used by any child of this father)
// Next free parentsort (smallest non-negative integer not used by any child of this parent)
const existingSortsRes = await runner.query(
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
{ fatherId },
'SELECT parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC',
{ parentId },
);
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
let fathersort = 0;
while (usedSorts.has(fathersort)) {
fathersort++;
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.parentsort)));
let parentsort = 0;
while (usedSorts.has(parentsort)) {
parentsort++;
}
// Auto-create child article
const rawTrack = options.track_stock ?? options.trackStock;
const finalTrackStock = rawTrack == null ? father.track_stock : (rawTrack ? 1 : 0);
const finalTrackStock = rawTrack == null ? parent.track_stock : (rawTrack ? 1 : 0);
const childRes = await runner.query(queries.CREATE_CHILD, {
companyId,
sku: childSku,
unitId: father.unit_id,
unitamount: options.unitamount ?? father.unitamount,
weight: options.weight ?? father.weight,
unitId: parent.unit_id,
unitamount: options.unitamount ?? parent.unitamount,
weight: options.weight ?? parent.weight,
trackStock: finalTrackStock,
brandId: father.brand_id,
fatherArticleId: fatherId,
fathersort,
brandId: parent.brand_id,
parentArticleId: parentId,
parentsort,
});
const childId = childRes.rows[0]?.id ?? childRes.insertId;
// Copy existing attribute values from father (ancestor attributes that father has)
const fatherAttrsRes = await runner.query(
'SELECT attributevalue_id, sort FROM articleattributevalue WHERE article_id = @fatherId',
{ fatherId },
// Copy existing attribute values from parent (ancestor attributes that parent has)
const parentAttrsRes = await runner.query(
'SELECT attributevalue_id, sort FROM articleattributevalue WHERE article_id = @parentId',
{ parentId },
);
for (const faRow of fatherAttrsRes.rows) {
for (const faRow of parentAttrsRes.rows) {
if (faRow.attributevalue_id !== finalValueId) {
await runner.query(articleAttrValQueries.INSERT_ROW, {
articleId: childId,
@@ -213,10 +213,10 @@ export default function addChild(provider) {
sort: 0,
});
// Copy father's categories
// Copy parent's categories
const catRes = await runner.query(
'SELECT category_id FROM categoryarticle WHERE article_id = @fatherId',
{ fatherId },
'SELECT category_id FROM categoryarticle WHERE article_id = @parentId',
{ parentId },
);
for (const catRow of catRes.rows) {
await runner.query(addCategoryArticleSql, {
@@ -226,7 +226,7 @@ export default function addChild(provider) {
}
// Create article text
const childName = options.name?.trim() || `${fatherName} - ${finalValue}`;
const childName = options.name?.trim() || `${parentName} - ${finalValue}`;
const childSlug = await resolveSlug(runner, {
companyId,
languageId,
@@ -243,12 +243,12 @@ export default function addChild(provider) {
await runner.query(insertHistorySql, {
articleId: childId,
companyuserId,
text: `Artikel "${childName}" (${childSku}) als Variante von "${father.sku}" angelegt`,
text: `Artikel "${childName}" (${childSku}) als Variante von "${parent.sku}" angelegt`,
});
// History on father
// History on parent
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: `Variante "${childName}" (${childSku}) hinzugefügt`,
});

View File

@@ -26,23 +26,23 @@ export default function attachChild(provider) {
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Attaches an existing article as a child to a father article.
* Attaches an existing article as a child to a parent article.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} fatherSku - The father article SKU.
* @param {string} parentSku - The parent article SKU.
* @param {object} options
* @param {string} options.sku - The child article SKU to attach.
* @param {string} [options.childSku] - Alias for options.sku.
* @param {number} [options.attributeValueId] - Existing attribute value ID for father's attribute.
* @param {string} [options.value] - String value to find or create under father's attribute.
* @param {boolean} [options.reparent=false] - Whether to allow re-parenting if child belongs to another father.
* @param {number} [options.attributeValueId] - Existing attribute value ID for parent's attribute.
* @param {string} [options.value] - String value to find or create under parent's attribute.
* @param {boolean} [options.reparent=false] - Whether to allow re-parenting if child belongs to another parent.
* @param {number} [options.unitamount] - Optional unitamount override.
* @param {number} [options.weight] - Optional weight override.
* @param {boolean|number} [options.track_stock] - Optional track_stock override.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<{article: object, children: Array<object>}>}
*/
return async (googleAuthId, fatherSku, options = {}, tx) => {
return async (googleAuthId, parentSku, options = {}, tx) => {
const run = async (runner) => {
const childSku = (typeof options.sku === 'string' && options.sku.trim())
|| (typeof options.childSku === 'string' && options.childSku.trim())
@@ -52,35 +52,35 @@ export default function attachChild(provider) {
throw new Error('sku is required to attach an existing article');
}
const { articleId: fatherId, companyId, languageId, companyuserId } = await resolve(
const { articleId: parentId, companyId, languageId, companyuserId } = await resolve(
googleAuthId,
fatherSku,
parentSku,
runner,
);
// Fetch father article fields
const fatherRes = await runner.query(
'SELECT id, sku, company_id, is_father, fatherattributename_id FROM article WHERE id = @fatherId',
{ fatherId },
// Fetch parent article fields
const parentRes = await runner.query(
'SELECT id, sku, company_id, is_parent, parentattributename_id FROM article WHERE id = @parentId',
{ parentId },
);
const father = fatherRes.rows[0];
if (!father) {
throw new Error('Father article not found');
const parent = parentRes.rows[0];
if (!parent) {
throw new Error('Parent article not found');
}
if (!father.is_father || !father.fatherattributename_id) {
throw new Error('Article is not a father article');
if (!parent.is_parent || !parent.parentattributename_id) {
throw new Error('Article is not a parent article');
}
// Check child article exists in company
const childRes = await runner.query(
'SELECT id, sku, company_id, fatherarticle_id, is_father FROM article WHERE sku = @childSku AND company_id = @companyId',
'SELECT id, sku, company_id, parentarticle_id, is_parent FROM article WHERE sku = @childSku AND company_id = @companyId',
{ childSku, companyId },
);
const child = childRes.rows[0];
if (!child) {
throw new Error('Article not found');
}
if (child.id === fatherId) {
if (child.id === parentId) {
throw new Error('An article cannot be a child of itself');
}
@@ -97,15 +97,15 @@ export default function attachChild(provider) {
companyId,
});
const row = valRes.rows[0];
if (!row || row.attributeId !== father.fatherattributename_id) {
throw new Error('Attribute value not found or does not match father attribute');
if (!row || row.attributeId !== parent.parentattributename_id) {
throw new Error('Attribute value not found or does not match parent attribute');
}
finalValueId = row.id;
finalValue = row.value;
} else if (typeof options.value === 'string' && options.value.trim()) {
const valStr = options.value.trim();
const existingValRes = await runner.query(attrValQueries.GET_BY_ATTRIBUTE_AND_VALUE, {
attributeId: father.fatherattributename_id,
attributeId: parent.parentattributename_id,
value: valStr,
});
if (existingValRes.rows[0]) {
@@ -113,11 +113,11 @@ export default function attachChild(provider) {
finalValue = existingValRes.rows[0].value;
} else {
const sortRes = await runner.query(attrValQueries.GET_NEXT_SORT, {
attributeId: father.fatherattributename_id,
attributeId: parent.parentattributename_id,
});
const sort = sortRes.rows[0]?.sort ?? 0;
const createdValRes = await runner.query(attrValQueries.CREATE, {
attributeId: father.fatherattributename_id,
attributeId: parent.parentattributename_id,
value: valStr,
sort,
});
@@ -128,78 +128,78 @@ export default function attachChild(provider) {
throw new Error('Attribute value is required for child article');
}
const isAlreadyChildOfThisFather = child.fatherarticle_id === fatherId;
const isAlreadyChildOfThisParent = child.parentarticle_id === parentId;
if (child.fatherarticle_id != null && !isAlreadyChildOfThisFather && !options.reparent) {
throw new Error('Article already belongs to another father');
if (child.parentarticle_id != null && !isAlreadyChildOfThisParent && !options.reparent) {
throw new Error('Article already belongs to another parent');
}
// Check if father already has a different child with this attribute value
// Check if parent already has a different child 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
WHERE a.parentarticle_id = @parentId
AND a.id != @childId
AND aav.attributevalue_id = @finalValueId`,
{ fatherId, childId: child.id, finalValueId },
{ parentId, childId: child.id, finalValueId },
);
if (existingVariantRes.rows.length > 0) {
throw new Error('Variant with this attribute value already exists');
}
// If re-parenting from another father, unparent and recompact previous father
if (child.fatherarticle_id != null && !isAlreadyChildOfThisFather && options.reparent) {
const prevFatherId = child.fatherarticle_id;
// If re-parenting from another parent, unparent and recompact previous parent
if (child.parentarticle_id != null && !isAlreadyChildOfThisParent && options.reparent) {
const prevParentId = child.parentarticle_id;
await runner.query(insertHistorySql, {
articleId: prevFatherId,
articleId: prevParentId,
companyuserId,
text: `Variante (${childSku}) getrennt (umgehängt zu "${father.sku}")`,
text: `Variante (${childSku}) getrennt (umgehängt zu "${parent.sku}")`,
});
// Recompact previous father's children
// Recompact previous parent's children
const prevRemainingRes = await runner.query(
'SELECT id, fathersort FROM article WHERE fatherarticle_id = @prevFatherId AND id != @childId ORDER BY fathersort ASC, id ASC',
{ prevFatherId, childId: child.id },
'SELECT id, parentsort FROM article WHERE parentarticle_id = @prevParentId AND id != @childId ORDER BY parentsort ASC, id ASC',
{ prevParentId, childId: child.id },
);
for (let i = 0; i < prevRemainingRes.rows.length; i++) {
const row = prevRemainingRes.rows[i];
if (Number(row.fathersort) !== i) {
await runner.query(queries.SET_FATHER_SORT, {
if (Number(row.parentsort) !== i) {
await runner.query(queries.SET_PARENT_SORT, {
articleId: row.id,
fatherArticleId: prevFatherId,
fathersort: i,
parentArticleId: prevParentId,
parentsort: i,
});
}
}
}
// Assign fathersort if not already child of this father
let fathersort;
if (isAlreadyChildOfThisFather) {
// Assign parentsort if not already child of this parent
let parentsort;
if (isAlreadyChildOfThisParent) {
const currentSortRes = await runner.query(
'SELECT fathersort FROM article WHERE id = @childId',
'SELECT parentsort FROM article WHERE id = @childId',
{ childId: child.id },
);
fathersort = Number(currentSortRes.rows[0]?.fathersort ?? 0);
parentsort = Number(currentSortRes.rows[0]?.parentsort ?? 0);
} else {
const existingSortsRes = await runner.query(
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
{ fatherId },
'SELECT parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC',
{ parentId },
);
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
fathersort = 0;
while (usedSorts.has(fathersort)) {
fathersort++;
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.parentsort)));
parentsort = 0;
while (usedSorts.has(parentsort)) {
parentsort++;
}
}
// Link child to father
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
// Link child to parent
await runner.query(queries.UPDATE_PARENT_BY_SKU, {
sku: childSku,
companyId,
fatherArticleId: fatherId,
fathersort,
parentArticleId: parentId,
parentsort,
});
// Apply optional field overrides
@@ -223,15 +223,15 @@ export default function attachChild(provider) {
});
}
// Remove existing attribute value for this father's attribute on child (if any)
if (father.fatherattributename_id) {
// Remove existing attribute value for this parent's attribute on child (if any)
if (parent.parentattributename_id) {
await runner.query(
`DELETE FROM articleattributevalue
WHERE article_id = @childId
AND attributevalue_id IN (
SELECT id FROM attributevalue WHERE attribute_id = @attributeId
)`,
{ childId: child.id, attributeId: father.fatherattributename_id },
{ childId: child.id, attributeId: parent.parentattributename_id },
);
}
await runner.query(articleAttrValQueries.INSERT_ROW, {
@@ -240,12 +240,12 @@ export default function attachChild(provider) {
sort: 0,
});
// Copy ancestor attributes from father (if missing on child)
const fatherAttrsRes = await runner.query(
'SELECT attributevalue_id, sort FROM articleattributevalue WHERE article_id = @fatherId',
{ fatherId },
// Copy ancestor attributes from parent (if missing on child)
const parentAttrsRes = await runner.query(
'SELECT attributevalue_id, sort FROM articleattributevalue WHERE article_id = @parentId',
{ parentId },
);
for (const faRow of fatherAttrsRes.rows) {
for (const faRow of parentAttrsRes.rows) {
if (faRow.attributevalue_id !== finalValueId) {
const hasAttrRes = await runner.query(
'SELECT 1 FROM articleattributevalue WHERE article_id = @childId AND attributevalue_id = @attrValId',
@@ -261,10 +261,10 @@ export default function attachChild(provider) {
}
}
// Copy father's categories (if missing on child)
// Copy parent's categories (if missing on child)
const catRes = await runner.query(
'SELECT category_id FROM categoryarticle WHERE article_id = @fatherId',
{ fatherId },
'SELECT category_id FROM categoryarticle WHERE article_id = @parentId',
{ parentId },
);
for (const catRow of catRes.rows) {
const hasCatRes = await runner.query(
@@ -287,22 +287,22 @@ export default function attachChild(provider) {
const childName = childTextRes.rows[0]?.name || childSku;
// History only if not already child
if (!isAlreadyChildOfThisFather) {
if (!isAlreadyChildOfThisParent) {
await runner.query(insertHistorySql, {
articleId: child.id,
companyuserId,
text: `Artikel "${childName}" (${childSku}) als Variante von "${father.sku}" zugewiesen`,
text: `Artikel "${childName}" (${childSku}) als Variante von "${parent.sku}" zugewiesen`,
});
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: `Variante "${childName}" (${childSku}) hinzugefügt`,
});
}
const updated = await load(googleAuthId, childSku, runner);
const children = await list(googleAuthId, fatherSku, runner);
const children = await list(googleAuthId, parentSku, runner);
return {
article: updated.article,

View File

@@ -69,11 +69,11 @@ export default function del(provider) {
await deleteArticleAndLists(id);
};
const fatherRes = await runner.query(
'SELECT fatherarticle_id FROM article WHERE id = @articleId',
const parentRes = await runner.query(
'SELECT parentarticle_id FROM article WHERE id = @articleId',
{ articleId },
);
const fatherArticleId = fatherRes.rows[0]?.fatherarticle_id;
const parentArticleId = parentRes.rows[0]?.parentarticle_id;
const childCount = await runner.query(queries.GET_CHILD_COUNT, { articleId });
if (childCount.rows[0].count > 0) {
@@ -85,18 +85,18 @@ export default function del(provider) {
await deleteArticleAndLists(articleId);
}
if (fatherArticleId != null) {
if (parentArticleId != null) {
const remainingChildrenRes = await runner.query(
'SELECT id, fathersort FROM article WHERE fatherarticle_id = @fatherArticleId ORDER BY fathersort ASC, id ASC',
{ fatherArticleId },
'SELECT id, parentsort FROM article WHERE parentarticle_id = @parentArticleId ORDER BY parentsort ASC, id ASC',
{ parentArticleId },
);
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, {
if (Number(childRow.parentsort) !== i) {
await runner.query(articleQueries.SET_PARENT_SORT, {
articleId: childRow.id,
fatherArticleId,
fathersort: i,
parentArticleId,
parentsort: i,
});
}
}

View File

@@ -18,7 +18,7 @@ export default function getBySku(provider) {
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>} The full article detail.
* @returns {Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>} The full article detail.
* @throws {Error} If the article is not found or the user is not authorized.
*
* @example
@@ -118,16 +118,16 @@ export default function getBySku(provider) {
sort: Number(item.sort ?? 0),
})),
childCount: a.childCount,
isFather: Boolean(a.isFather),
fatherAttributeNameId: a.fatherAttributeNameId ?? null,
fatherAttributeName: a.fatherAttributeName ?? null,
fatherAttributeExclusive: a.fatherAttributeExclusive != null ? Boolean(a.fatherAttributeExclusive) : null,
fatherArticleId: a.fatherArticleId,
fatherSku: a.fatherSku,
isParent: Boolean(a.isParent),
parentAttributeNameId: a.parentAttributeNameId ?? null,
parentAttributeName: a.parentAttributeName ?? null,
parentAttributeExclusive: a.parentAttributeExclusive != null ? Boolean(a.parentAttributeExclusive) : null,
parentArticleId: a.parentArticleId,
parentSku: a.parentSku,
children: parseJsonArray(a.childrenJson).map((c) => ({
id: c.id,
sku: c.sku,
fathersort: c.fathersort,
parentsort: c.parentsort,
name: c.name ?? null,
attributeValueId: c.attributeValueId ?? null,
attributeValue: c.attributeValue ?? null,

View File

@@ -87,7 +87,7 @@ export default function importRows(provider) {
* Imports article rows, creating articles by their SKU and returning per-row
* import, skip and error outcomes.
*
* The import runs in **two passes** so a `fatherSku` that references a row
* The import runs in **two passes** so a `parentSku` that references a row
* appearing later in the file still resolves. A row is **skipped** when its
* SKU already exists in the company (upsert mode is not applied), and an EAN
* is skipped when already used by another row.
@@ -97,7 +97,7 @@ export default function importRows(provider) {
* @param {Array<object>} input.rows - The article rows to import.
* @param {string} input.rows[].sku - The article SKU.
* @param {string} input.rows[].name - The article name.
* @param {string} [input.rows[].vk] - `'V'` (father/compound) or `'K'` (variant) marker.
* @param {string} [input.rows[].vk] - `'V'` (parent/compound) or `'K'` (variant) marker.
* @param {number|string} [input.rows[].parentId] - The id of the parent category.
* @param {string} [input.rows[].parentCategory] - The category breadcrumb path, e.g. `'Flowers | CBN'`.
* @param {string} [input.rows[].brand] - The brand name to upsert.
@@ -105,7 +105,7 @@ export default function importRows(provider) {
* @param {number|string} [input.rows[].tax] - The tax rate in percent (e.g. `19`).
* @param {Array<string>|string} [input.rows[].eans] - The EANs to set.
* @param {boolean|number} [input.rows[].track_stock=true] - Whether inventory stock is tracked.
* @param {string} [input.rows[].fatherSku] - The father article SKU for a V/K compound link.
* @param {string} [input.rows[].parentSku] - The parent article SKU for a V/K compound link.
* @param {number|string} [input.rows[].deposit] - The deposit (Pfand) value; currently ignored.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>} The import outcome.
@@ -114,7 +114,7 @@ export default function importRows(provider) {
* const result = await store.article.importRows({
* googleAuthId: 42,
* rows: [
* { sku: '42001190-1', name: '420Strainz 1g', vk: 'K', parentCategory: 'Flowers | CBN', brand: 'Seedstockers', net: '11,00', tax: 19, eans: ['4262557690455'], fatherSku: '42001190' },
* { sku: '42001190-1', name: '420Strainz 1g', vk: 'K', parentCategory: 'Flowers | CBN', brand: 'Seedstockers', net: '11,00', tax: 19, eans: ['4262557690455'], parentSku: '42001190' },
* { sku: '42001190', name: '420Strainz', vk: 'V', parentCategory: 'Flowers | CBN', brand: 'Seedstockers', net: '0,00', tax: 19 },
* ],
* });
@@ -275,63 +275,63 @@ export default function importRows(provider) {
}
}
// Pass 2: wire V/K father links (fathers may be created in pass 1).
// Pass 2: wire V/K parent links (parents may be created in pass 1).
for (const row of rows) {
const sku = String(row?.sku ?? '').trim();
const fatherSku =
row?.fatherSku != null ? String(row.fatherSku).trim() : '';
if (!sku || !fatherSku) continue;
const parentSku =
row?.parentSku != null ? String(row.parentSku).trim() : '';
if (!sku || !parentSku) continue;
try {
await runInTx(async (runner) => {
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(
'SELECT id, is_father FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
{ sku: fatherSku, companyId },
const parent = await runner.query(
'SELECT id, is_parent FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
{ sku: parentSku, companyId },
);
const fatherRow = father.rows[0];
const fatherId = fatherRow?.id;
if (fatherId && fatherId !== childId) {
const wasFather = Boolean(fatherRow.is_father);
const parentRow = parent.rows[0];
const parentId = parentRow?.id;
if (parentId && parentId !== childId) {
const wasParent = Boolean(parentRow.is_parent);
const existingSortsRes = await runner.query(
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
{ fatherId },
'SELECT parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC',
{ parentId },
);
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
let fathersort = 0;
while (usedSorts.has(fathersort)) {
fathersort++;
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.parentsort)));
let parentsort = 0;
while (usedSorts.has(parentsort)) {
parentsort++;
}
await runner.query(articleQueries.UPDATE_FATHER_BY_SKU, {
await runner.query(articleQueries.UPDATE_PARENT_BY_SKU, {
sku,
companyId,
fatherArticleId: fatherId,
fathersort,
parentArticleId: parentId,
parentsort,
});
await runner.query(articleQueries.SET_IS_FATHER, {
articleId: fatherId,
await runner.query(articleQueries.SET_IS_PARENT, {
articleId: parentId,
});
if (!wasFather) {
if (!wasParent) {
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: 'Artikel zu Vaterartikel konvertiert',
});
}
const childName = String(row?.name ?? sku).trim();
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: `Variante "${childName}" (${sku}) hinzugefügt`,
});
await runner.query(insertHistorySql, {
articleId: childId,
companyuserId,
text: `Artikel "${childName}" (${sku}) als Variante von "${fatherSku}" zugewiesen`,
text: `Artikel "${childName}" (${sku}) als Variante von "${parentSku}" zugewiesen`,
});
}
}

View File

@@ -9,40 +9,40 @@ export default function listChildren(provider) {
const queries = isSqlite ? sqliteArticle : mssqlArticle;
/**
* Lists all children of a father article.
* Lists all children of a parent article.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} fatherSku - The father article SKU.
* @param {string} parentSku - The parent article SKU.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, sku: string, fathersort: number, trackStock: boolean, unitamount: number, weight: number, name: string|null, slug: string|null, attributeValueId: number|null, attributeValue: string|null}>>}
* @returns {Promise<Array<{id: number, sku: string, parentsort: number, trackStock: boolean, unitamount: number, weight: number, name: string|null, slug: string|null, attributeValueId: number|null, attributeValue: string|null}>>}
*/
return async (googleAuthId, fatherSku, tx) => {
return async (googleAuthId, parentSku, tx) => {
const run = async (runner) => {
const { articleId: fatherId, languageId } = await resolve(
const { articleId: parentId, languageId } = await resolve(
googleAuthId,
fatherSku,
parentSku,
runner,
);
const fatherRes = await runner.query(
'SELECT is_father, fatherattributename_id FROM article WHERE id = @fatherId',
{ fatherId },
const parentRes = await runner.query(
'SELECT is_parent, parentattributename_id FROM article WHERE id = @parentId',
{ parentId },
);
const father = fatherRes.rows[0];
if (!father || !father.is_father) {
const parent = parentRes.rows[0];
if (!parent || !parent.is_parent) {
return [];
}
const result = await runner.query(queries.LIST_CHILDREN_BY_FATHER_ID, {
fatherArticleId: fatherId,
const result = await runner.query(queries.LIST_CHILDREN_BY_PARENT_ID, {
parentArticleId: parentId,
languageId,
attributeId: father.fatherattributename_id,
attributeId: parent.parentattributename_id,
});
return result.rows.map((row) => ({
id: row.id,
sku: row.sku,
fathersort: Number(row.fathersort ?? 0),
parentsort: Number(row.parentsort ?? 0),
trackStock: Boolean(row.trackStock ?? 1),
unitamount: Number(row.unitamount ?? 1),
weight: Number(row.weight ?? 0),

View File

@@ -35,8 +35,8 @@ function shapeArticleDetail(row, categoryIds, photos, eans, prices) {
optionsCount: row.optionsCount,
articleListCount: row.articleListCount,
childCount: row.childCount,
fatherArticleId: row.fatherArticleId,
fatherSku: row.fatherSku,
parentArticleId: row.parentArticleId,
parentSku: row.parentSku,
};
}

View File

@@ -15,34 +15,34 @@ export default function removeChild(provider) {
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Removes a child article from a father article, either by unlinking or deleting it.
* Removes a child article from a parent article, either by unlinking or deleting it.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} fatherSku - The father article SKU.
* @param {string} parentSku - The parent article SKU.
* @param {string} childSku - The child article SKU.
* @param {object} [options]
* @param {boolean} [options.deleteArticle=false] - Whether to delete the child article entity entirely.
* @param {boolean} [options.cascade=true] - Whether to cascade delete child subtrees when deleteArticle is true.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>}
* @returns {Promise<{success: boolean, parentSku: string, childSku: string, deleted: boolean}>}
*/
return async (googleAuthId, fatherSku, childSku, options = {}, tx) => {
return async (googleAuthId, parentSku, childSku, options = {}, tx) => {
const run = async (runner) => {
const { articleId: fatherId, languageId, companyuserId } = await resolve(
const { articleId: parentId, languageId, companyuserId } = await resolve(
googleAuthId,
fatherSku,
parentSku,
runner,
);
const { articleId: childId } = await resolve(googleAuthId, childSku, runner);
// Verify child relationship
const childRes = await runner.query(
'SELECT fatherarticle_id FROM article WHERE id = @childId',
'SELECT parentarticle_id FROM article WHERE id = @childId',
{ childId },
);
const child = childRes.rows[0];
if (!child || child.fatherarticle_id !== fatherId) {
throw new Error('Child article does not belong to this father');
if (!child || child.parentarticle_id !== parentId) {
throw new Error('Child article does not belong to this parent');
}
// Fetch child name before any deletion (needed for history regardless of path)
@@ -54,18 +54,18 @@ export default function removeChild(provider) {
);
const childName = childNameRes.rows[0]?.name || childSku;
const recompactFatherSort = async () => {
const recompactParentSort = async () => {
const remainingChildrenRes = await runner.query(
'SELECT id, fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC, id ASC',
{ fatherId },
'SELECT id, parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC, id ASC',
{ parentId },
);
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, {
if (Number(childRow.parentsort) !== i) {
await runner.query(queries.SET_PARENT_SORT, {
articleId: childRow.id,
fatherArticleId: fatherId,
fathersort: i,
parentArticleId: parentId,
parentsort: i,
});
}
}
@@ -75,67 +75,67 @@ export default function removeChild(provider) {
const cascade = options.cascade !== undefined ? Boolean(options.cascade) : true;
await del(googleAuthId, childSku, { cascade }, runner);
await recompactFatherSort();
await recompactParentSort();
// History on father (child article is gone so we can only log on father)
// History on parent (child article is gone so we can only log on parent)
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: `Variante "${childName}" (${childSku}) gelöscht`,
});
return {
success: true,
fatherSku,
parentSku,
childSku,
deleted: true,
};
}
// Fetch father's attribute name id
const fatherRes = await runner.query(
'SELECT fatherattributename_id FROM article WHERE id = @fatherId',
{ fatherId },
// Fetch parent's attribute name id
const parentRes = await runner.query(
'SELECT parentattributename_id FROM article WHERE id = @parentId',
{ parentId },
);
const fatherAttributeNameId = fatherRes.rows[0]?.fatherattributename_id;
const parentAttributeNameId = parentRes.rows[0]?.parentattributename_id;
// Unlink child from father
// Unlink child from parent
await runner.query(queries.UNLINK_CHILD, {
articleId: childId,
fatherArticleId: fatherId,
parentArticleId: parentId,
});
// Remove the child's attribute value link for the father's attribute
if (fatherAttributeNameId) {
// Remove the child's attribute value link for the parent's attribute
if (parentAttributeNameId) {
await runner.query(
`DELETE FROM articleattributevalue
WHERE article_id = @childId
AND attributevalue_id IN (
SELECT id FROM attributevalue WHERE attribute_id = @fatherAttributeNameId
SELECT id FROM attributevalue WHERE attribute_id = @parentAttributeNameId
)`,
{ childId, fatherAttributeNameId },
{ childId, parentAttributeNameId },
);
}
await recompactFatherSort();
await recompactParentSort();
// Add history on child
await runner.query(insertHistorySql, {
articleId: childId,
companyuserId,
text: `Artikel "${childSku}" von Vaterartikel "${fatherSku}" getrennt`,
text: `Artikel "${childSku}" von Vaterartikel "${parentSku}" getrennt`,
});
// Add history on father
// Add history on parent
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: `Variante "${childName}" (${childSku}) getrennt`,
});
return {
success: true,
fatherSku,
parentSku,
childSku,
deleted: false,
};

View File

@@ -8,7 +8,7 @@ import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
export default function setFatherBySku(provider) {
export default function setParentBySku(provider) {
const resolve = resolveArticleBySku(provider);
const load = queryBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
@@ -18,20 +18,20 @@ export default function setFatherBySku(provider) {
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Links an article to a father article (V/K compound) by their SKUs.
* Links an article to a parent article (V/K compound) by their SKUs.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The child article SKU.
* @param {string|null} fatherSku - The father article SKU, or `null` to unlink.
* @param {string|null} parentSku - The parent article SKU, or `null` to unlink.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<object>} The updated child article.
* @throws {Error} If the article or father is not found, or the link is invalid.
* @throws {Error} If the article or parent is not found, or the link is invalid.
*
* @example
* const article = await store.article.setFatherBySku(42, '42001190-1', '42001190');
* // => { id, sku, name, fatherSku: '42001190', ... }
* const article = await store.article.setParentBySku(42, '42001190-1', '42001190');
* // => { id, sku, name, parentSku: '42001190', ... }
*/
return async (googleAuthId, sku, fatherSku, tx) => {
return async (googleAuthId, sku, parentSku, tx) => {
const runSet = async (runner) => {
const { articleId, languageId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
@@ -41,34 +41,34 @@ export default function setFatherBySku(provider) {
);
const childName = childTextRes.rows[0]?.name || sku;
if (fatherSku == null || fatherSku === '') {
const prevFatherRes = await runner.query(
'SELECT fatherarticle_id FROM article WHERE id = @articleId',
if (parentSku == null || parentSku === '') {
const prevParentRes = await runner.query(
'SELECT parentarticle_id FROM article WHERE id = @articleId',
{ articleId },
);
const prevFatherId = prevFatherRes.rows[0]?.fatherarticle_id;
const prevParentId = prevParentRes.rows[0]?.parentarticle_id;
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
await runner.query(queries.UPDATE_PARENT_BY_SKU, {
sku,
companyId,
fatherArticleId: null,
parentArticleId: null,
});
if (prevFatherId) {
const prevFatherTextRes = await runner.query(
'SELECT sku FROM article WHERE id = @prevFatherId',
{ prevFatherId },
if (prevParentId) {
const prevParentTextRes = await runner.query(
'SELECT sku FROM article WHERE id = @prevParentId',
{ prevParentId },
);
const prevFatherSku = prevFatherTextRes.rows[0]?.sku || String(prevFatherId);
const prevParentSku = prevParentTextRes.rows[0]?.sku || String(prevParentId);
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: `Artikel "${sku}" von Vaterartikel "${prevFatherSku}" getrennt`,
text: `Artikel "${sku}" von Vaterartikel "${prevParentSku}" getrennt`,
});
await runner.query(insertHistorySql, {
articleId: prevFatherId,
articleId: prevParentId,
companyuserId,
text: `Variante "${childName}" (${sku}) getrennt`,
});
@@ -81,52 +81,52 @@ export default function setFatherBySku(provider) {
return unlinked;
}
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 parent = await runner.query(
'SELECT id, is_parent, sku FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
{ sku: parentSku, companyId },
);
const fatherRow = father.rows[0];
const fatherId = fatherRow?.id;
if (!fatherId) {
throw new Error('Father not found');
const parentRow = parent.rows[0];
const parentId = parentRow?.id;
if (!parentId) {
throw new Error('Parent not found');
}
if (fatherId === articleId) {
throw new Error('Invalid father');
if (parentId === articleId) {
throw new Error('Invalid parent');
}
const wasFather = Boolean(fatherRow.is_father);
const wasParent = Boolean(parentRow.is_parent);
const existingSortsRes = await runner.query(
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
{ fatherId },
'SELECT parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC',
{ parentId },
);
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
let fathersort = 0;
while (usedSorts.has(fathersort)) {
fathersort++;
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.parentsort)));
let parentsort = 0;
while (usedSorts.has(parentsort)) {
parentsort++;
}
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
await runner.query(queries.UPDATE_PARENT_BY_SKU, {
sku,
companyId,
fatherArticleId: fatherId,
fathersort,
parentArticleId: parentId,
parentsort,
});
await runner.query(queries.SET_IS_FATHER, {
articleId: fatherId,
await runner.query(queries.SET_IS_PARENT, {
articleId: parentId,
});
if (!wasFather) {
if (!wasParent) {
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: 'Artikel zu Vaterartikel konvertiert',
});
}
await runner.query(insertHistorySql, {
articleId: fatherId,
articleId: parentId,
companyuserId,
text: `Variante "${childName}" (${sku}) hinzugefügt`,
});
@@ -134,7 +134,7 @@ export default function setFatherBySku(provider) {
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: `Artikel "${sku}" als Variante von "${fatherSku}" zugewiesen`,
text: `Artikel "${sku}" als Variante von "${parentSku}" zugewiesen`,
});
const article = await load(articleId, languageId, runner);

View File

@@ -86,9 +86,9 @@ export default function setPricesBySku(provider) {
throw new Error('POS customer group required');
}
const artCheck = await runner.query('SELECT is_father FROM article WHERE id = @articleId', { articleId });
if (artCheck.rows[0]?.is_father) {
throw new Error('A father article cannot have prices');
const artCheck = await runner.query('SELECT is_parent FROM article WHERE id = @articleId', { articleId });
if (artCheck.rows[0]?.is_parent) {
throw new Error('A parent article cannot have prices');
}
// Read old prices for history

View File

@@ -10,7 +10,7 @@ import resolveArticleBySku from './resolveArticleBySku.js';
import getBySku from './getBySku.js';
import makeRunner from '../runner.js';
export default function toggleFather(provider) {
export default function toggleParent(provider) {
const resolve = resolveArticleBySku(provider);
const load = getBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
@@ -21,39 +21,39 @@ export default function toggleFather(provider) {
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Toggles an article into or out of father mode.
* Toggles an article into or out of parent mode.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {object} options
* @param {boolean} options.isFather - Whether the article should be a father article.
* @param {number} [options.attributeNameId] - The attribute name id required when enabling father mode.
* @param {number} [options.fatherAttributeNameId] - Alias for attributeNameId.
* @param {boolean} options.isParent - Whether the article should be a parent article.
* @param {number} [options.attributeNameId] - The attribute name id required when enabling parent mode.
* @param {number} [options.parentAttributeNameId] - Alias for attributeNameId.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<object>} The updated article detail.
*/
return async (googleAuthId, sku, { isFather, attributeNameId, fatherAttributeNameId } = {}, tx) => {
return async (googleAuthId, sku, { isParent, attributeNameId, parentAttributeNameId } = {}, tx) => {
const run = async (runner) => {
const { articleId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
const targetIsFather = Boolean(isFather);
const targetAttrId = attributeNameId ?? fatherAttributeNameId;
const targetIsParent = Boolean(isParent);
const targetAttrId = attributeNameId ?? parentAttributeNameId;
if (targetIsFather) {
if (targetIsParent) {
if (!targetAttrId || Number(targetAttrId) <= 0) {
throw new Error('attributeNameId is required to become a father article');
throw new Error('attributeNameId is required to become a parent article');
}
const currentRes = await runner.query(
'SELECT fatherarticle_id, articlelist_id, optionslist_id FROM article WHERE id = @articleId',
'SELECT parentarticle_id, articlelist_id, optionslist_id FROM article WHERE id = @articleId',
{ articleId },
);
const currentArt = currentRes.rows[0];
if (currentArt?.articlelist_id != null) {
throw new Error('A Stückliste article cannot become a father');
throw new Error('A Stückliste article cannot become a parent');
}
if (currentArt?.optionslist_id != null) {
throw new Error('An article with options cannot become a father');
throw new Error('An article with options cannot become a parent');
}
const stockRes = await runner.query(
@@ -70,7 +70,7 @@ export default function toggleFather(provider) {
);
const totalStock = Number(stockRes.rows[0]?.totalStock ?? 0);
if (totalStock !== 0) {
throw new Error('An article with existing stock cannot become a father article');
throw new Error('An article with existing stock cannot become a parent article');
}
const supRes = await runner.query(
@@ -78,7 +78,7 @@ export default function toggleFather(provider) {
{ articleId },
);
if ((supRes.rows[0]?.count ?? 0) > 0) {
throw new Error('An article with assigned suppliers cannot become a father article');
throw new Error('An article with assigned suppliers cannot become a parent article');
}
// Fetch existing prices before deletion to record in articlehistory
@@ -96,22 +96,22 @@ export default function toggleFather(provider) {
await runner.query('DELETE FROM price WHERE article_id = @articleId', { articleId });
await runner.query('UPDATE article SET track_stock = 0 WHERE id = @articleId', { articleId });
// If article is a child, verify attribute is not used by father or any father up the chain
let currentFatherId = currentArt?.fatherarticle_id;
// If article is a child, verify attribute is not used by parent or any parent up the chain
let currentParentId = currentArt?.parentarticle_id;
const visited = new Set();
while (currentFatherId != null && !visited.has(currentFatherId)) {
visited.add(currentFatherId);
while (currentParentId != null && !visited.has(currentParentId)) {
visited.add(currentParentId);
const fRes = await runner.query(
'SELECT a.fatherarticle_id, a.fatherattributename_id, an.name AS fatherAttributeName FROM article a LEFT JOIN attributename an ON an.id = a.fatherattributename_id WHERE a.id = @currentFatherId',
{ currentFatherId },
'SELECT a.parentarticle_id, a.parentattributename_id, an.name AS parentAttributeName FROM article a LEFT JOIN attributename an ON an.id = a.parentattributename_id WHERE a.id = @currentParentId',
{ currentParentId },
);
const fRow = fRes.rows[0];
if (!fRow) break;
if (fRow.fatherattributename_id != null && Number(fRow.fatherattributename_id) === Number(targetAttrId)) {
const attrName = fRow.fatherAttributeName || 'ancestor';
throw new Error(`Attribute "${attrName}" is already used by an ancestor father article`);
if (fRow.parentattributename_id != null && Number(fRow.parentattributename_id) === Number(targetAttrId)) {
const attrName = fRow.parentAttributeName || 'ancestor';
throw new Error(`Attribute "${attrName}" is already used by an ancestor parent article`);
}
currentFatherId = fRow.fatherarticle_id;
currentParentId = fRow.parentarticle_id;
}
// Verify attribute exists and belongs to company
@@ -132,16 +132,16 @@ export default function toggleFather(provider) {
{ articleId, attributeId: Number(targetAttrId) },
);
if (existingValRes.rows.length > 0) {
throw new Error('A father article cannot have attribute values for its variant attribute');
throw new Error('A parent article cannot have attribute values for its variant attribute');
}
await runner.query(queries.TOGGLE_FATHER, {
await runner.query(queries.TOGGLE_PARENT, {
articleId,
isFather: 1,
fatherAttributeNameId: Number(targetAttrId),
isParent: 1,
parentAttributeNameId: Number(targetAttrId),
});
// Add history entry recording father conversion and deleted prices
// Add history entry recording parent conversion and deleted prices
const historyLines = [
`Artikel zu Vaterartikel konvertiert (Varianten-Attribut: "${attributeName}")`,
];
@@ -170,17 +170,17 @@ export default function toggleFather(provider) {
text: historyLines.join('\n'),
});
} else {
// Disabling father: verify zero children
// Disabling parent: verify zero children
const childCountRes = await runner.query(getChildCountSql, { articleId });
const childCount = childCountRes.rows[0]?.count ?? 0;
if (childCount > 0) {
throw new Error('Cannot disable father mode on an article that still has children');
throw new Error('Cannot disable parent mode on an article that still has children');
}
await runner.query(queries.TOGGLE_FATHER, {
await runner.query(queries.TOGGLE_PARENT, {
articleId,
isFather: 0,
fatherAttributeNameId: null,
isParent: 0,
parentAttributeNameId: null,
});
await runner.query(insertHistorySql, {