This commit is contained in:
seb
2026-10-16 22:34:48 +02:00
parent 7635821dad
commit c7d709de1f
2 changed files with 55 additions and 206 deletions

View File

@@ -1,40 +1,13 @@
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/categoryarticle.js';
import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js';
import { CREATE as MSSQL_CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
import { CREATE as SQLITE_CREATE_ARTICLETEXT } from '../../providers/sqlite3/queries/articletext.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 {
recordParentConvertedHistory,
recordVariantAddedOnParentHistory,
recordChildAssignedToParentHistory,
recordArticleImportedHistory,
} from '../articlehistory/record.js';
import {
CREATE as MSSQL_CREATE_BRAND,
GET_BY_COMPANY_AND_NAME as MSSQL_GET_BRAND_BY_NAME,
} from '../../providers/mssql/queries/brand.js';
import {
CREATE as SQLITE_CREATE_BRAND,
GET_BY_COMPANY_AND_NAME as SQLITE_GET_BRAND_BY_NAME,
} from '../../providers/sqlite3/queries/brand.js';
import { CREATE as MSSQL_CREATE_PRICE } from '../../providers/mssql/queries/price.js';
import { CREATE as SQLITE_CREATE_PRICE } from '../../providers/sqlite3/queries/price.js';
import {
CREATE as MSSQL_CREATE_TAX,
GET_BY_RATE as MSSQL_GET_TAX_BY_RATE,
} from '../../providers/mssql/queries/tax.js';
import {
CREATE as SQLITE_CREATE_TAX,
GET_BY_RATE as SQLITE_GET_TAX_BY_RATE,
} from '../../providers/sqlite3/queries/tax.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import ensureByPath from '../category/ensureByPath.js';
import ensureDefaultUnit from '../unit/ensureDefault.js';
import createUnderParent from './createUnderParent.js';
import getBySku from './getBySku.js';
import setBrandIdBySku from './setBrandIdBySku.js';
import setPricesBySku from './setPricesBySku.js';
import setEansBySku from './setEansBySku.js';
import setParentBySku from './setParentBySku.js';
import brandUpsert from '../brand/upsert.js';
import makeRunner from '../runner.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.
@@ -51,40 +24,14 @@ function toNumber(value) {
export default function importRows(provider) {
const companyAccess = resolveSelectedCompanyAccess(provider);
const ensureUnit = ensureDefaultUnit(provider);
const getArticleBySku = getBySku(provider);
const createArticleUnderParent = createUnderParent(provider);
const ensureCategoryByPath = ensureByPath(provider);
const resolveSlug = resolveUniqueSlug(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT;
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
const getBrandByNameSql = isSqlite ? SQLITE_GET_BRAND_BY_NAME : MSSQL_GET_BRAND_BY_NAME;
const createBrandSql = isSqlite ? SQLITE_CREATE_BRAND : MSSQL_CREATE_BRAND;
const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE;
const getTaxByRateSql = isSqlite ? SQLITE_GET_TAX_BY_RATE : MSSQL_GET_TAX_BY_RATE;
const createTaxSql = isSqlite ? SQLITE_CREATE_TAX : MSSQL_CREATE_TAX;
// Find or create a brand for a company by name (within the current tx).
async function ensureBrand(runner, companyId, name) {
const existing = await runner.query(getBrandByNameSql, {
companyId,
name,
});
if (existing.rows[0]) {
return existing.rows[0].id;
}
const created = await runner.query(createBrandSql, {
companyId,
name,
gsprpinfo: null,
});
return created.rows[0]?.id ?? created.insertId;
}
const upsertBrand = brandUpsert(provider);
const setBrandId = setBrandIdBySku(provider);
const setPrices = setPricesBySku(provider);
const setEans = setEansBySku(provider);
const setParent = setParentBySku(provider);
/**
* Imports article rows, creating articles by their SKU and returning per-row
@@ -125,13 +72,7 @@ export default function importRows(provider) {
*/
return async ({ googleAuthId, rows }, tx) => {
const baseRunner = makeRunner(provider, tx);
const {
companyId,
languageId,
rootCategoryId,
posCustomerGroupId,
companyuserId,
} = await companyAccess(googleAuthId, baseRunner);
const { rootCategoryId } = await companyAccess(googleAuthId, baseRunner);
if (!Array.isArray(rows)) {
throw new Error('Invalid rows');
@@ -141,6 +82,7 @@ export default function importRows(provider) {
const skipped = [];
const errors = [];
const usedEans = new Set();
const seenSkus = new Set();
const runInTx = async (fn) => {
if (tx) {
@@ -162,38 +104,25 @@ export default function importRows(provider) {
continue;
}
const existing = await baseRunner.query(articleQueries.GET_ID_BY_SKU, { sku, companyId });
if (existing.rows[0]) {
if (seenSkus.has(sku)) {
skipped.push({ sku, name, reason: 'duplicate sku' });
continue;
}
try {
await getArticleBySku(googleAuthId, sku, baseRunner);
seenSkus.add(sku);
skipped.push({ sku, name, reason: 'duplicate sku' });
continue;
} catch (err) {
if (!err.message?.includes('not found')) {
errors.push({ sku, name, error: err.message ?? String(err) });
continue;
}
}
try {
await runInTx(async (runner) => {
const unitId = await ensureUnit(runner, companyId, languageId);
const slug = await resolveSlug(runner, { companyId, languageId, name });
const rawTrack = row.track_stock ?? row.trackStock;
const trackStock = rawTrack == null ? 0 : (rawTrack ? 1 : 0);
if (trackStock === 1) {
const whRes = await runner.query(articleQueries.CHECK_HAS_WAREHOUSE, { companyId });
if (!whRes.rows || whRes.rows.length === 0) {
throw new Error('At least one warehouse is required to enable stock tracking');
}
}
const article = await runner.query(articleQueries.CREATE, {
companyId,
sku,
unitId,
unitamount: 1,
weight: 0,
trackStock,
});
const articleId = article.rows[0]?.id ?? article.insertId;
// Resolve the parent category id (explicit id or breadcrumb path).
let parentId = row.parentId;
if (parentId == null && row.parentCategory) {
parentId = await ensureCategoryByPath(
@@ -205,81 +134,53 @@ export default function importRows(provider) {
throw new Error('Parent category required');
}
await runner.query(addCategoryArticleSql, {
categoryId: parentId,
articleId,
});
await runner.query(createArticleTextSql, {
articleId,
languageId,
const rawTrack = row.track_stock ?? row.trackStock;
const created = await createArticleUnderParent({
googleAuthId,
parentId,
name,
slug,
sku,
desc: row.description ?? row.desc ?? null,
seo: null,
});
track_stock: rawTrack,
}, runner);
await recordArticleImportedHistory(runner, {
articleId,
companyuserId,
name,
});
// Brand (upsert by name).
const brandName = row.brand != null ? String(row.brand).trim() : '';
if (brandName) {
const brandId = await ensureBrand(runner, companyId, brandName);
await runner.query(articleQueries.UPDATE_BRAND, {
articleId,
brandId,
});
const brand = await upsertBrand(googleAuthId, { name: brandName }, runner);
await setBrandId(googleAuthId, sku, brand.id, runner);
}
// POS price (net/tax).
const net = toNumber(row.net);
const taxRate = toNumber(row.tax) ?? 0;
if (net != null) {
const taxResult = await runner.query(getTaxByRateSql, {
rate: taxRate,
});
let taxId = taxResult.rows[0]?.id;
if (!taxId) {
const created = await runner.query(createTaxSql, {
rate: taxRate,
});
taxId = created.rows[0]?.id ?? created.insertId;
}
await runner.query(createPriceSql, {
taxId,
articleId,
minmultiplier: 1,
customerGroupId: posCustomerGroupId,
net,
});
await setPrices(googleAuthId, sku, [{ net, taxRate }], runner);
}
// EANs (array, or a single value; skip duplicates within the import).
const rawEans = row.eans;
const eans = Array.isArray(rawEans)
? rawEans
: rawEans == null || rawEans === ''
? []
: [rawEans];
const validEans = [];
for (const ean of eans) {
const clean = String(ean ?? '').trim();
if (!clean || usedEans.has(clean)) continue;
const used = await runner.query(articleQueries.EAN_EXISTS, {
ean: clean,
});
if (used.rows[0]) continue;
await runner.query(articleQueries.ADD_EAN, {
articleId,
ean: clean,
});
usedEans.add(clean);
try {
await setEans(googleAuthId, sku, [...validEans, clean], runner);
validEans.push(clean);
usedEans.add(clean);
} catch (err) {
if (err.message?.includes('in use') || err.message?.includes('unique')) {
continue;
}
throw err;
}
}
imported.push({ id: articleId, sku, name });
seenSkus.add(sku);
imported.push({ id: created.id, sku, name });
});
} catch (err) {
errors.push({ sku, name, error: err.message ?? String(err) });
@@ -295,59 +196,7 @@ export default function importRows(provider) {
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 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 parentRow = parent.rows[0];
const parentId = parentRow?.id;
if (parentId && parentId !== childId) {
const wasParent = Boolean(parentRow.is_parent);
const existingSortsRes = await runner.query(
'SELECT parentsort FROM article WHERE parentarticle_id = @parentId ORDER BY parentsort ASC',
{ parentId },
);
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.parentsort)));
let parentsort = 0;
while (usedSorts.has(parentsort)) {
parentsort++;
}
await runner.query(articleQueries.UPDATE_PARENT_BY_SKU, {
sku,
companyId,
parentArticleId: parentId,
parentsort,
});
await runner.query(articleQueries.SET_IS_PARENT, {
articleId: parentId,
});
if (!wasParent) {
await recordParentConvertedHistory(runner, {
articleId: parentId,
companyuserId,
});
}
const childName = String(row?.name ?? sku).trim();
await recordVariantAddedOnParentHistory(runner, {
parentId,
companyuserId,
childName,
childSku: sku,
});
await recordChildAssignedToParentHistory(runner, {
childId,
companyuserId,
childName,
childSku: sku,
parentSku,
});
}
}
await setParent(googleAuthId, sku, parentSku, runner);
});
} catch {
// Non-fatal: article was already imported in pass 1.

View File

@@ -221,7 +221,7 @@ export default function updateBySku(provider) {
}
if (hasTrackStock) {
await setTrackStock(googleAuthId, sku, Boolean(rawTrackStock), activeTx);
await setTrackStock(googleAuthId, sku, Boolean(rawTrackStock), runner);
}
if (historyLines.length) {