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 resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
import ensureByPath from '../category/ensureByPath.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 makeRunner from '../runner.js';
|
||||||
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
|
||||||
|
|
||||||
// Normalizes a numeric value that may arrive as a string using German decimal
|
// 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.
|
// 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) {
|
export default function importRows(provider) {
|
||||||
const companyAccess = resolveSelectedCompanyAccess(provider);
|
const companyAccess = resolveSelectedCompanyAccess(provider);
|
||||||
const ensureUnit = ensureDefaultUnit(provider);
|
const getArticleBySku = getBySku(provider);
|
||||||
|
const createArticleUnderParent = createUnderParent(provider);
|
||||||
const ensureCategoryByPath = ensureByPath(provider);
|
const ensureCategoryByPath = ensureByPath(provider);
|
||||||
const resolveSlug = resolveUniqueSlug(provider);
|
const upsertBrand = brandUpsert(provider);
|
||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const setBrandId = setBrandIdBySku(provider);
|
||||||
|
const setPrices = setPricesBySku(provider);
|
||||||
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
const setEans = setEansBySku(provider);
|
||||||
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
|
const setParent = setParentBySku(provider);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports article rows, creating articles by their SKU and returning per-row
|
* 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) => {
|
return async ({ googleAuthId, rows }, tx) => {
|
||||||
const baseRunner = makeRunner(provider, tx);
|
const baseRunner = makeRunner(provider, tx);
|
||||||
const {
|
const { rootCategoryId } = await companyAccess(googleAuthId, baseRunner);
|
||||||
companyId,
|
|
||||||
languageId,
|
|
||||||
rootCategoryId,
|
|
||||||
posCustomerGroupId,
|
|
||||||
companyuserId,
|
|
||||||
} = await companyAccess(googleAuthId, baseRunner);
|
|
||||||
|
|
||||||
if (!Array.isArray(rows)) {
|
if (!Array.isArray(rows)) {
|
||||||
throw new Error('Invalid rows');
|
throw new Error('Invalid rows');
|
||||||
@@ -141,6 +82,7 @@ export default function importRows(provider) {
|
|||||||
const skipped = [];
|
const skipped = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
const usedEans = new Set();
|
const usedEans = new Set();
|
||||||
|
const seenSkus = new Set();
|
||||||
|
|
||||||
const runInTx = async (fn) => {
|
const runInTx = async (fn) => {
|
||||||
if (tx) {
|
if (tx) {
|
||||||
@@ -162,38 +104,25 @@ export default function importRows(provider) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existing = await baseRunner.query(articleQueries.GET_ID_BY_SKU, { sku, companyId });
|
if (seenSkus.has(sku)) {
|
||||||
if (existing.rows[0]) {
|
|
||||||
skipped.push({ sku, name, reason: 'duplicate sku' });
|
skipped.push({ sku, name, reason: 'duplicate sku' });
|
||||||
continue;
|
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 {
|
try {
|
||||||
await runInTx(async (runner) => {
|
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;
|
let parentId = row.parentId;
|
||||||
if (parentId == null && row.parentCategory) {
|
if (parentId == null && row.parentCategory) {
|
||||||
parentId = await ensureCategoryByPath(
|
parentId = await ensureCategoryByPath(
|
||||||
@@ -205,81 +134,53 @@ export default function importRows(provider) {
|
|||||||
throw new Error('Parent category required');
|
throw new Error('Parent category required');
|
||||||
}
|
}
|
||||||
|
|
||||||
await runner.query(addCategoryArticleSql, {
|
const rawTrack = row.track_stock ?? row.trackStock;
|
||||||
categoryId: parentId,
|
const created = await createArticleUnderParent({
|
||||||
articleId,
|
googleAuthId,
|
||||||
});
|
parentId,
|
||||||
|
|
||||||
await runner.query(createArticleTextSql, {
|
|
||||||
articleId,
|
|
||||||
languageId,
|
|
||||||
name,
|
name,
|
||||||
slug,
|
sku,
|
||||||
desc: row.description ?? row.desc ?? null,
|
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() : '';
|
const brandName = row.brand != null ? String(row.brand).trim() : '';
|
||||||
if (brandName) {
|
if (brandName) {
|
||||||
const brandId = await ensureBrand(runner, companyId, brandName);
|
const brand = await upsertBrand(googleAuthId, { name: brandName }, runner);
|
||||||
await runner.query(articleQueries.UPDATE_BRAND, {
|
await setBrandId(googleAuthId, sku, brand.id, runner);
|
||||||
articleId,
|
|
||||||
brandId,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POS price (net/tax).
|
|
||||||
const net = toNumber(row.net);
|
const net = toNumber(row.net);
|
||||||
const taxRate = toNumber(row.tax) ?? 0;
|
const taxRate = toNumber(row.tax) ?? 0;
|
||||||
if (net != null) {
|
if (net != null) {
|
||||||
const taxResult = await runner.query(getTaxByRateSql, {
|
await setPrices(googleAuthId, sku, [{ net, taxRate }], runner);
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EANs (array, or a single value; skip duplicates within the import).
|
|
||||||
const rawEans = row.eans;
|
const rawEans = row.eans;
|
||||||
const eans = Array.isArray(rawEans)
|
const eans = Array.isArray(rawEans)
|
||||||
? rawEans
|
? rawEans
|
||||||
: rawEans == null || rawEans === ''
|
: rawEans == null || rawEans === ''
|
||||||
? []
|
? []
|
||||||
: [rawEans];
|
: [rawEans];
|
||||||
|
|
||||||
|
const validEans = [];
|
||||||
for (const ean of eans) {
|
for (const ean of eans) {
|
||||||
const clean = String(ean ?? '').trim();
|
const clean = String(ean ?? '').trim();
|
||||||
if (!clean || usedEans.has(clean)) continue;
|
if (!clean || usedEans.has(clean)) continue;
|
||||||
const used = await runner.query(articleQueries.EAN_EXISTS, {
|
try {
|
||||||
ean: clean,
|
await setEans(googleAuthId, sku, [...validEans, clean], runner);
|
||||||
});
|
validEans.push(clean);
|
||||||
if (used.rows[0]) continue;
|
usedEans.add(clean);
|
||||||
await runner.query(articleQueries.ADD_EAN, {
|
} catch (err) {
|
||||||
articleId,
|
if (err.message?.includes('in use') || err.message?.includes('unique')) {
|
||||||
ean: clean,
|
continue;
|
||||||
});
|
}
|
||||||
usedEans.add(clean);
|
throw err;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
imported.push({ id: articleId, sku, name });
|
seenSkus.add(sku);
|
||||||
|
imported.push({ id: created.id, sku, name });
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errors.push({ sku, name, error: err.message ?? String(err) });
|
errors.push({ sku, name, error: err.message ?? String(err) });
|
||||||
@@ -295,59 +196,7 @@ export default function importRows(provider) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await runInTx(async (runner) => {
|
await runInTx(async (runner) => {
|
||||||
const child = await runner.query(articleQueries.GET_ID_BY_SKU, { sku, companyId });
|
await setParent(googleAuthId, sku, parentSku, runner);
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// Non-fatal: article was already imported in pass 1.
|
// Non-fatal: article was already imported in pass 1.
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ export default function updateBySku(provider) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasTrackStock) {
|
if (hasTrackStock) {
|
||||||
await setTrackStock(googleAuthId, sku, Boolean(rawTrackStock), activeTx);
|
await setTrackStock(googleAuthId, sku, Boolean(rawTrackStock), runner);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (historyLines.length) {
|
if (historyLines.length) {
|
||||||
|
|||||||
Reference in New Issue
Block a user