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