u
This commit is contained in:
@@ -97,7 +97,7 @@ Creates a new article under a given parent category, allocating a SKU and a defa
|
|||||||
| `input.name` | `string` | The article name. |
|
| `input.name` | `string` | The article name. |
|
||||||
| `[input.desc]` | `string` | The article description text/HTML. |
|
| `[input.desc]` | `string` | The article description text/HTML. |
|
||||||
| `[input.seo]` | `string` | The article SEO text. |
|
| `[input.seo]` | `string` | The article SEO text. |
|
||||||
| `[input.track_stock=true]` | `boolean|number` | Whether inventory stock is tracked. |
|
| `[input.track_stock=false]` | `boolean|number` | Whether inventory stock is tracked. |
|
||||||
| `[tx]` | `object` | Optional transaction to run within. |
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
@@ -251,7 +251,7 @@ Imports article rows, creating articles by their SKU and returning per-row impor
|
|||||||
| `[input.rows[].net]` | `number|string` | The POS net price (accepts German decimals). |
|
| `[input.rows[].net]` | `number|string` | The POS net price (accepts German decimals). |
|
||||||
| `[input.rows[].tax]` | `number|string` | The tax rate in percent (e.g. `19`). |
|
| `[input.rows[].tax]` | `number|string` | The tax rate in percent (e.g. `19`). |
|
||||||
| `[input.rows[].eans]` | `Array<string>|string` | The EANs to set. |
|
| `[input.rows[].eans]` | `Array<string>|string` | The EANs to set. |
|
||||||
| `[input.rows[].track_stock=true]` | `boolean|number` | Whether inventory stock is tracked. |
|
| `[input.rows[].track_stock=false]` | `boolean|number` | Whether inventory stock is tracked. |
|
||||||
| `[input.rows[].parentSku]` | `string` | The parent article SKU for a V/K compound link. |
|
| `[input.rows[].parentSku]` | `string` | The parent article SKU for a V/K compound link. |
|
||||||
| `[input.rows[].deposit]` | `number|string` | The deposit (Pfand) value; currently ignored. |
|
| `[input.rows[].deposit]` | `number|string` | The deposit (Pfand) value; currently ignored. |
|
||||||
| `[tx]` | `object` | Optional transaction. |
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
@@ -591,7 +591,7 @@ Sets the track_stock flag of an article by its SKU.
|
|||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
`Error` — If trackStock is invalid or the article is not found.
|
`Error` — If trackStock is invalid, article not found, or no warehouse exists when enabling.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
|
|||||||
|
|
||||||
*Source: `src/store/warehouse/delete.js`*
|
*Source: `src/store/warehouse/delete.js`*
|
||||||
|
|
||||||
Deletes a warehouse for the user's selected company, unless referenced in the ledger.
|
Deletes a warehouse for the user's selected company, unless referenced in the ledger or it is the last warehouse while articles have stock tracking enabled.
|
||||||
|
|
||||||
**Parameters**
|
**Parameters**
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ Deletes a warehouse for the user's selected company, unless referenced in the le
|
|||||||
|
|
||||||
**Throws**
|
**Throws**
|
||||||
|
|
||||||
`Error` — If warehouse not found or is in use in the ledger.
|
`Error` — If warehouse not found, in use in the ledger, or is the last warehouse with track_stock articles.
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ async function run() {
|
|||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Ledger Linked Article',
|
name: 'Ledger Linked Article',
|
||||||
});
|
});
|
||||||
|
assert.equal(article.track_stock, false, 'Newly created article must have track_stock disabled');
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
async () => store.article.setTrackStockBySku(googleAuthId, article.sku, true),
|
||||||
|
/At least one warehouse is required to enable stock tracking/,
|
||||||
|
'Should reject enabling stock tracking when no warehouse exists',
|
||||||
|
);
|
||||||
|
|
||||||
await store.articlesupplier.create(googleAuthId, article.sku, {
|
await store.articlesupplier.create(googleAuthId, article.sku, {
|
||||||
entityId: supplier.id,
|
entityId: supplier.id,
|
||||||
sku: 'SKU-ABC',
|
sku: 'SKU-ABC',
|
||||||
@@ -278,6 +286,9 @@ async function run() {
|
|||||||
assert.equal(articleDetail.article.warehouses[1].id, wh1.id);
|
assert.equal(articleDetail.article.warehouses[1].id, wh1.id);
|
||||||
assert.equal(articleDetail.article.warehouses[1].sort, 1);
|
assert.equal(articleDetail.article.warehouses[1].sort, 1);
|
||||||
|
|
||||||
|
// Enable stock tracking now that warehouses exist
|
||||||
|
await store.article.setTrackStockBySku(googleAuthId, article.sku, true);
|
||||||
|
|
||||||
// Add ledger row with warehouse
|
// Add ledger row with warehouse
|
||||||
const rowWithWh = await store.ledger.add(googleAuthId, {
|
const rowWithWh = await store.ledger.add(googleAuthId, {
|
||||||
manifestarticle_id: manifestarticleId1,
|
manifestarticle_id: manifestarticleId1,
|
||||||
@@ -318,6 +329,38 @@ async function run() {
|
|||||||
const whListAfterDelete = await store.warehouse.list(googleAuthId);
|
const whListAfterDelete = await store.warehouse.list(googleAuthId);
|
||||||
assert.equal(whListAfterDelete.length, 1);
|
assert.equal(whListAfterDelete.length, 1);
|
||||||
assert.equal(whListAfterDelete[0].id, wh1.id);
|
assert.equal(whListAfterDelete[0].id, wh1.id);
|
||||||
|
|
||||||
|
// Test last warehouse guard with track_stock articles
|
||||||
|
const freshCompany = await store.company.create({
|
||||||
|
name: 'Warehouse Test Co',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, freshCompany.id);
|
||||||
|
const testWh = await store.warehouse.create(googleAuthId, { name: 'Only Warehouse' });
|
||||||
|
const testArt = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: freshCompany.rootCategoryId,
|
||||||
|
name: 'Tracked Article',
|
||||||
|
track_stock: true,
|
||||||
|
});
|
||||||
|
assert.equal(testArt.track_stock, true);
|
||||||
|
await assert.rejects(
|
||||||
|
async () => store.warehouse.delete(googleAuthId, testWh.id),
|
||||||
|
/Cannot delete the last warehouse while articles have stock tracking enabled/,
|
||||||
|
'Should reject deleting last warehouse when articles have track_stock enabled',
|
||||||
|
);
|
||||||
|
// Disable track_stock on the article, then deleting the last warehouse must succeed
|
||||||
|
await store.article.setTrackStockBySku(googleAuthId, testArt.sku, false);
|
||||||
|
await store.warehouse.delete(googleAuthId, testWh.id);
|
||||||
|
const remainingWh = await store.warehouse.list(googleAuthId);
|
||||||
|
assert.equal(remainingWh.length, 0);
|
||||||
|
|
||||||
|
// Switch back to original company
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
console.log('✓ Warehouse CRUD and ledger enforcement passed');
|
console.log('✓ Warehouse CRUD and ledger enforcement passed');
|
||||||
|
|
||||||
console.log('✓ All ledger tests passed successfully!');
|
console.log('✓ All ledger tests passed successfully!');
|
||||||
|
|||||||
@@ -47,12 +47,14 @@ async function run() {
|
|||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Item A',
|
name: 'Item A',
|
||||||
sku: 'ITEM-A',
|
sku: 'ITEM-A',
|
||||||
|
track_stock: true,
|
||||||
});
|
});
|
||||||
const itemB = await store.article.createUnderParent({
|
const itemB = await store.article.createUnderParent({
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Item B',
|
name: 'Item B',
|
||||||
sku: 'ITEM-B',
|
sku: 'ITEM-B',
|
||||||
|
track_stock: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stock up Item A (20) and Item B (20)
|
// Stock up Item A (20) and Item B (20)
|
||||||
|
|||||||
@@ -157,14 +157,27 @@ async function runTests() {
|
|||||||
parentId: cat.id,
|
parentId: cat.id,
|
||||||
name: 'Test Article',
|
name: 'Test Article',
|
||||||
});
|
});
|
||||||
assert.equal(article.track_stock, true, 'Default track_stock must be true');
|
assert.equal(article.track_stock, false, 'Default track_stock must be false');
|
||||||
|
|
||||||
const fetchedArt = await store.article.getBySku(googleAuthId, article.sku);
|
const fetchedArt = await store.article.getBySku(googleAuthId, article.sku);
|
||||||
assert.equal(fetchedArt.article.track_stock, true, 'getBySku must return track_stock: true');
|
assert.equal(fetchedArt.article.track_stock, false, 'getBySku must return track_stock: false');
|
||||||
|
|
||||||
await store.article.setTrackStockBySku(googleAuthId, article.sku, false);
|
// Enabling track_stock without a warehouse must fail
|
||||||
|
await assert.rejects(
|
||||||
|
async () => store.article.setTrackStockBySku(googleAuthId, article.sku, true),
|
||||||
|
/At least one warehouse is required to enable stock tracking/,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create warehouse to allow enabling track_stock
|
||||||
|
await store.warehouse.create(googleAuthId, { name: 'Storage WH' });
|
||||||
|
|
||||||
|
await store.article.setTrackStockBySku(googleAuthId, article.sku, true);
|
||||||
const afterSet = await store.article.getBySku(googleAuthId, article.sku);
|
const afterSet = await store.article.getBySku(googleAuthId, article.sku);
|
||||||
assert.equal(afterSet.article.track_stock, false, 'setTrackStockBySku false must update track_stock');
|
assert.equal(afterSet.article.track_stock, true, 'setTrackStockBySku true must update track_stock');
|
||||||
|
|
||||||
|
await store.article.updateBySku(googleAuthId, article.sku, { track_stock: false });
|
||||||
|
const afterDisable = await store.article.getBySku(googleAuthId, article.sku);
|
||||||
|
assert.equal(afterDisable.article.track_stock, false, 'updateBySku track_stock: false must update track_stock');
|
||||||
|
|
||||||
await store.article.updateBySku(googleAuthId, article.sku, { track_stock: true });
|
await store.article.updateBySku(googleAuthId, article.sku, { track_stock: true });
|
||||||
const afterUpdate = await store.article.getBySku(googleAuthId, article.sku);
|
const afterUpdate = await store.article.getBySku(googleAuthId, article.sku);
|
||||||
|
|||||||
@@ -40,11 +40,13 @@ async function run() {
|
|||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Component Article 1',
|
name: 'Component Article 1',
|
||||||
|
track_stock: true,
|
||||||
});
|
});
|
||||||
const comp2 = await store.article.createUnderParent({
|
const comp2 = await store.article.createUnderParent({
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
parentId: company.rootCategoryId,
|
parentId: company.rootCategoryId,
|
||||||
name: 'Component Article 2',
|
name: 'Component Article 2',
|
||||||
|
track_stock: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a bundle article
|
// Create a bundle article
|
||||||
|
|||||||
@@ -652,3 +652,9 @@ SELECT
|
|||||||
) AS articleJson
|
) AS articleJson
|
||||||
FROM auth
|
FROM auth
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const CHECK_HAS_WAREHOUSE = `
|
||||||
|
SELECT TOP 1 id
|
||||||
|
FROM warehouse
|
||||||
|
WHERE company_id = @companyId;
|
||||||
|
`;
|
||||||
|
|||||||
@@ -67,3 +67,16 @@ WHERE id = @id AND company_id = @companyId;
|
|||||||
export const CHECK_LEDGER_USAGE = `
|
export const CHECK_LEDGER_USAGE = `
|
||||||
SELECT TOP 1 id FROM ledger WHERE warehouse_id = @warehouseId;
|
SELECT TOP 1 id FROM ledger WHERE warehouse_id = @warehouseId;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const COUNT_BY_COMPANY = `
|
||||||
|
SELECT COUNT(1) AS count
|
||||||
|
FROM warehouse
|
||||||
|
WHERE company_id = @companyId;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CHECK_TRACK_STOCK_ARTICLES = `
|
||||||
|
SELECT TOP 1 id
|
||||||
|
FROM article
|
||||||
|
WHERE company_id = @companyId AND track_stock = 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ BEGIN
|
|||||||
unit_id INT NOT NULL,
|
unit_id INT NOT NULL,
|
||||||
unitamount FLOAT NOT NULL,
|
unitamount FLOAT NOT NULL,
|
||||||
weight FLOAT NOT NULL,
|
weight FLOAT NOT NULL,
|
||||||
track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 1,
|
track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 0,
|
||||||
brand_id INT NULL,
|
brand_id INT NULL,
|
||||||
articlelist_id INT NULL,
|
articlelist_id INT NULL,
|
||||||
optionslist_id INT NULL,
|
optionslist_id INT NULL,
|
||||||
@@ -358,7 +358,7 @@ BEGIN
|
|||||||
|
|
||||||
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'track_stock')
|
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'track_stock')
|
||||||
BEGIN
|
BEGIN
|
||||||
ALTER TABLE article ADD track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 1;
|
ALTER TABLE article ADD track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 0;
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -456,7 +456,7 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
|
|||||||
const articleCols = this.db.pragma('table_info("article")').map((c) => c.name);
|
const articleCols = this.db.pragma('table_info("article")').map((c) => c.name);
|
||||||
if (articleCols.length > 0 && !articleCols.includes('track_stock')) {
|
if (articleCols.length > 0 && !articleCols.includes('track_stock')) {
|
||||||
this.db.exec(
|
this.db.exec(
|
||||||
`ALTER TABLE article ADD COLUMN track_stock INTEGER NOT NULL DEFAULT 1`,
|
`ALTER TABLE article ADD COLUMN track_stock INTEGER NOT NULL DEFAULT 0`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (articleCols.length > 0 && !articleCols.includes('parentattributename_id')) {
|
if (articleCols.length > 0 && !articleCols.includes('parentattributename_id')) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { translateQueries } from './translate.js';
|
|||||||
|
|
||||||
const queries = translateQueries(source, 'article');
|
const queries = translateQueries(source, 'article');
|
||||||
export const {
|
export const {
|
||||||
ADD_CATEGORY, ADD_EAN, ARTICLE_LIST_COLUMNS, CREATE, CREATE_CHILD,
|
ADD_CATEGORY, ADD_EAN, ARTICLE_LIST_COLUMNS, CHECK_HAS_WAREHOUSE, CREATE, CREATE_CHILD,
|
||||||
DELETE_CATEGORIES_BY_ARTICLE, DELETE_EANS_BY_ARTICLE, EAN_EXISTS, EAN_EXISTS_FOR_OTHER_ARTICLE,
|
DELETE_CATEGORIES_BY_ARTICLE, DELETE_EANS_BY_ARTICLE, EAN_EXISTS, EAN_EXISTS_FOR_OTHER_ARTICLE,
|
||||||
GET_BRAND_BY_COMPANY_AND_ID,
|
GET_BRAND_BY_COMPANY_AND_ID,
|
||||||
GET_CATEGORY_ID_BY_SKU, GET_CATEGORY_TREE_FLAT, GET_ID_BY_SKU,
|
GET_CATEGORY_ID_BY_SKU, GET_CATEGORY_TREE_FLAT, GET_ID_BY_SKU,
|
||||||
|
|||||||
@@ -67,3 +67,17 @@ WHERE id = @id AND company_id = @companyId;
|
|||||||
export const CHECK_LEDGER_USAGE = `
|
export const CHECK_LEDGER_USAGE = `
|
||||||
SELECT id FROM ledger WHERE warehouse_id = @warehouseId LIMIT 1;
|
SELECT id FROM ledger WHERE warehouse_id = @warehouseId LIMIT 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const COUNT_BY_COMPANY = `
|
||||||
|
SELECT COUNT(1) AS count
|
||||||
|
FROM warehouse
|
||||||
|
WHERE company_id = @companyId;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CHECK_TRACK_STOCK_ARTICLES = `
|
||||||
|
SELECT id
|
||||||
|
FROM article
|
||||||
|
WHERE company_id = @companyId AND track_stock = 1
|
||||||
|
LIMIT 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ CREATE TABLE IF NOT EXISTS article (
|
|||||||
unit_id INTEGER NOT NULL,
|
unit_id INTEGER NOT NULL,
|
||||||
unitamount REAL NOT NULL,
|
unitamount REAL NOT NULL,
|
||||||
weight REAL NOT NULL,
|
weight REAL NOT NULL,
|
||||||
track_stock INTEGER NOT NULL DEFAULT 1,
|
track_stock INTEGER NOT NULL DEFAULT 0,
|
||||||
brand_id INTEGER,
|
brand_id INTEGER,
|
||||||
articlelist_id INTEGER,
|
articlelist_id INTEGER,
|
||||||
optionslist_id INTEGER,
|
optionslist_id INTEGER,
|
||||||
|
|||||||
@@ -177,7 +177,13 @@ export default function addChild(provider) {
|
|||||||
|
|
||||||
// Auto-create child article
|
// Auto-create child article
|
||||||
const rawTrack = options.track_stock ?? options.trackStock;
|
const rawTrack = options.track_stock ?? options.trackStock;
|
||||||
const finalTrackStock = rawTrack == null ? parent.track_stock : (rawTrack ? 1 : 0);
|
const finalTrackStock = rawTrack == null ? (parent.track_stock ? 1 : 0) : (rawTrack ? 1 : 0);
|
||||||
|
if (finalTrackStock === 1) {
|
||||||
|
const whRes = await runner.query(queries.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 childRes = await runner.query(queries.CREATE_CHILD, {
|
const childRes = await runner.query(queries.CREATE_CHILD, {
|
||||||
companyId,
|
companyId,
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { CREATE as MSSQL_CREATE_ARTICLE } from '../../providers/mssql/queries/article.js';
|
import {
|
||||||
import { CREATE as SQLITE_CREATE_ARTICLE } from '../../providers/sqlite3/queries/article.js';
|
CREATE as MSSQL_CREATE_ARTICLE,
|
||||||
|
CHECK_HAS_WAREHOUSE as MSSQL_CHECK_HAS_WAREHOUSE,
|
||||||
|
} from '../../providers/mssql/queries/article.js';
|
||||||
|
import {
|
||||||
|
CREATE as SQLITE_CREATE_ARTICLE,
|
||||||
|
CHECK_HAS_WAREHOUSE as SQLITE_CHECK_HAS_WAREHOUSE,
|
||||||
|
} from '../../providers/sqlite3/queries/article.js';
|
||||||
import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/categoryarticle.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 { 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 MSSQL_CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
|
||||||
@@ -20,6 +26,7 @@ export default function createUnderParent(provider) {
|
|||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
const createArticleSql = isSqlite ? SQLITE_CREATE_ARTICLE : MSSQL_CREATE_ARTICLE;
|
const createArticleSql = isSqlite ? SQLITE_CREATE_ARTICLE : MSSQL_CREATE_ARTICLE;
|
||||||
|
const checkHasWarehouseSql = isSqlite ? SQLITE_CHECK_HAS_WAREHOUSE : MSSQL_CHECK_HAS_WAREHOUSE;
|
||||||
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
|
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
|
||||||
const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT;
|
const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT;
|
||||||
|
|
||||||
@@ -33,7 +40,7 @@ export default function createUnderParent(provider) {
|
|||||||
* @param {string} input.name - The article name.
|
* @param {string} input.name - The article name.
|
||||||
* @param {string} [input.desc] - The article description text/HTML.
|
* @param {string} [input.desc] - The article description text/HTML.
|
||||||
* @param {string} [input.seo] - The article SEO text.
|
* @param {string} [input.seo] - The article SEO text.
|
||||||
* @param {boolean|number} [input.track_stock=true] - Whether inventory stock is tracked.
|
* @param {boolean|number} [input.track_stock=false] - Whether inventory stock is tracked.
|
||||||
* @param {object} [tx] - Optional transaction to run within.
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>} The created article.
|
* @returns {Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>} The created article.
|
||||||
*
|
*
|
||||||
@@ -43,7 +50,7 @@ export default function createUnderParent(provider) {
|
|||||||
*/
|
*/
|
||||||
return async ({ googleAuthId, parentId, name, desc, description, seo, track_stock, trackStock, sku: inputSku }, tx) => {
|
return async ({ googleAuthId, parentId, name, desc, description, seo, track_stock, trackStock, sku: inputSku }, tx) => {
|
||||||
const rawTrack = track_stock ?? trackStock;
|
const rawTrack = track_stock ?? trackStock;
|
||||||
const finalTrackStock = rawTrack == null ? 1 : (rawTrack ? 1 : 0);
|
const finalTrackStock = rawTrack == null ? 0 : (rawTrack ? 1 : 0);
|
||||||
|
|
||||||
const runCreate = async (runner) => {
|
const runCreate = async (runner) => {
|
||||||
const { companyId, languageId, companyuserId } = await access(
|
const { companyId, languageId, companyuserId } = await access(
|
||||||
@@ -52,6 +59,13 @@ export default function createUnderParent(provider) {
|
|||||||
runner,
|
runner,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (finalTrackStock === 1) {
|
||||||
|
const whRes = await runner.query(checkHasWarehouseSql, { companyId });
|
||||||
|
if (!whRes.rows || whRes.rows.length === 0) {
|
||||||
|
throw new Error('At least one warehouse is required to enable stock tracking');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const unitId = await ensureUnit(runner, languageId, companyId);
|
const unitId = await ensureUnit(runner, languageId, companyId);
|
||||||
const sku = (typeof inputSku === 'string' && inputSku.trim()) ? inputSku.trim() : await allocateSku(companyId, runner);
|
const sku = (typeof inputSku === 'string' && inputSku.trim()) ? inputSku.trim() : await allocateSku(companyId, runner);
|
||||||
const slug = await resolveSlug(runner, { companyId, languageId, name });
|
const slug = await resolveSlug(runner, { companyId, languageId, name });
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function getBySku(provider) {
|
|||||||
seo: a.seo ?? null,
|
seo: a.seo ?? null,
|
||||||
unitamount: a.unitamount,
|
unitamount: a.unitamount,
|
||||||
weight: a.weight,
|
weight: a.weight,
|
||||||
track_stock: Boolean(a.track_stock ?? 1),
|
track_stock: Boolean(a.track_stock ?? 0),
|
||||||
unit: {
|
unit: {
|
||||||
si: a.unitSi,
|
si: a.unitSi,
|
||||||
basesi: a.unitBasesi,
|
basesi: a.unitBasesi,
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default function importRows(provider) {
|
|||||||
* @param {number|string} [input.rows[].net] - The POS net price (accepts German decimals).
|
* @param {number|string} [input.rows[].net] - The POS net price (accepts German decimals).
|
||||||
* @param {number|string} [input.rows[].tax] - The tax rate in percent (e.g. `19`).
|
* @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 {Array<string>|string} [input.rows[].eans] - The EANs to set.
|
||||||
* @param {boolean|number} [input.rows[].track_stock=true] - Whether inventory stock is tracked.
|
* @param {boolean|number} [input.rows[].track_stock=false] - Whether inventory stock is tracked.
|
||||||
* @param {string} [input.rows[].parentSku] - The parent 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 {number|string} [input.rows[].deposit] - The deposit (Pfand) value; currently ignored.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
@@ -174,7 +174,13 @@ export default function importRows(provider) {
|
|||||||
const slug = await resolveSlug(runner, { companyId, languageId, name });
|
const slug = await resolveSlug(runner, { companyId, languageId, name });
|
||||||
|
|
||||||
const rawTrack = row.track_stock ?? row.trackStock;
|
const rawTrack = row.track_stock ?? row.trackStock;
|
||||||
const trackStock = rawTrack == null ? 1 : (rawTrack ? 1 : 0);
|
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, {
|
const article = await runner.query(articleQueries.CREATE, {
|
||||||
companyId,
|
companyId,
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export default function listExport(provider) {
|
|||||||
unitSi: row.unitSi,
|
unitSi: row.unitSi,
|
||||||
unitSymbol: row.unitSymbol,
|
unitSymbol: row.unitSymbol,
|
||||||
unitamount: row.unitamount,
|
unitamount: row.unitamount,
|
||||||
track_stock: Boolean(row.track_stock ?? 1),
|
track_stock: Boolean(row.track_stock ?? 0),
|
||||||
brandName: row.brandName,
|
brandName: row.brandName,
|
||||||
net: row.net,
|
net: row.net,
|
||||||
taxRate: row.taxRate,
|
taxRate: row.taxRate,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function shapeArticleDetail(row, categoryIds, photos, eans, prices) {
|
|||||||
slug: row.slug,
|
slug: row.slug,
|
||||||
unitamount: row.unitamount,
|
unitamount: row.unitamount,
|
||||||
weight: row.weight,
|
weight: row.weight,
|
||||||
track_stock: Boolean(row.track_stock ?? 1),
|
track_stock: Boolean(row.track_stock ?? 0),
|
||||||
unit: {
|
unit: {
|
||||||
si: row.unitSi,
|
si: row.unitSi,
|
||||||
basesi: row.unitBasesi,
|
basesi: row.unitBasesi,
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { UPDATE_TRACK_STOCK as MSSQL_UPDATE_TRACK_STOCK } from '../../providers/mssql/queries/article.js';
|
import {
|
||||||
import { UPDATE_TRACK_STOCK as SQLITE_UPDATE_TRACK_STOCK } from '../../providers/sqlite3/queries/article.js';
|
UPDATE_TRACK_STOCK as MSSQL_UPDATE_TRACK_STOCK,
|
||||||
|
CHECK_HAS_WAREHOUSE as MSSQL_CHECK_HAS_WAREHOUSE,
|
||||||
|
} from '../../providers/mssql/queries/article.js';
|
||||||
|
import {
|
||||||
|
UPDATE_TRACK_STOCK as SQLITE_UPDATE_TRACK_STOCK,
|
||||||
|
CHECK_HAS_WAREHOUSE as SQLITE_CHECK_HAS_WAREHOUSE,
|
||||||
|
} from '../../providers/sqlite3/queries/article.js';
|
||||||
import queryBySku from './queryBySku.js';
|
import queryBySku from './queryBySku.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
import { recordTrackStockChangeHistory } from '../articlehistory/record.js';
|
import { recordTrackStockChangeHistory } from '../articlehistory/record.js';
|
||||||
@@ -8,10 +14,9 @@ import makeRunner from '../runner.js';
|
|||||||
export default function setTrackStockBySku(provider) {
|
export default function setTrackStockBySku(provider) {
|
||||||
const resolve = resolveArticleBySku(provider);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const load = queryBySku(provider);
|
const load = queryBySku(provider);
|
||||||
const sql =
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
provider?.dialect === 'sqlite3'
|
const sql = isSqlite ? SQLITE_UPDATE_TRACK_STOCK : MSSQL_UPDATE_TRACK_STOCK;
|
||||||
? SQLITE_UPDATE_TRACK_STOCK
|
const checkHasWarehouseSql = isSqlite ? SQLITE_CHECK_HAS_WAREHOUSE : MSSQL_CHECK_HAS_WAREHOUSE;
|
||||||
: MSSQL_UPDATE_TRACK_STOCK;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the track_stock flag of an article by its SKU.
|
* Sets the track_stock flag of an article by its SKU.
|
||||||
@@ -21,7 +26,7 @@ export default function setTrackStockBySku(provider) {
|
|||||||
* @param {boolean|number} trackStock - Whether stock tracking is enabled.
|
* @param {boolean|number} trackStock - Whether stock tracking is enabled.
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<object>} The updated article.
|
* @returns {Promise<object>} The updated article.
|
||||||
* @throws {Error} If trackStock is invalid or the article is not found.
|
* @throws {Error} If trackStock is invalid, article not found, or no warehouse exists when enabling.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const article = await store.article.setTrackStockBySku(42, 'SKU-1', true);
|
* const article = await store.article.setTrackStockBySku(42, 'SKU-1', true);
|
||||||
@@ -33,15 +38,22 @@ export default function setTrackStockBySku(provider) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const runner = makeRunner(provider, tx);
|
const runner = makeRunner(provider, tx);
|
||||||
const { articleId, languageId, companyuserId } = await resolve(googleAuthId, sku, runner);
|
const { articleId, languageId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
|
const newTrack = Boolean(trackStock);
|
||||||
|
if (newTrack) {
|
||||||
|
const whRes = await runner.query(checkHasWarehouseSql, { companyId });
|
||||||
|
if (!whRes.rows || whRes.rows.length === 0) {
|
||||||
|
throw new Error('At least one warehouse is required to enable stock tracking');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Read old track_stock for history
|
// Read old track_stock for history
|
||||||
const oldRes = await runner.query(
|
const oldRes = await runner.query(
|
||||||
'SELECT track_stock FROM article WHERE id = @articleId',
|
'SELECT track_stock FROM article WHERE id = @articleId',
|
||||||
{ articleId },
|
{ articleId },
|
||||||
);
|
);
|
||||||
const oldTrack = Boolean(oldRes.rows[0]?.track_stock ?? 1);
|
const oldTrack = Boolean(oldRes.rows[0]?.track_stock ?? 0);
|
||||||
const newTrack = Boolean(trackStock);
|
|
||||||
|
|
||||||
const result = await runner.query(sql, {
|
const result = await runner.query(sql, {
|
||||||
articleId,
|
articleId,
|
||||||
|
|||||||
@@ -98,6 +98,13 @@ export default function updateBySku(provider) {
|
|||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasTrackStock && Boolean(rawTrackStock)) {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let unitId;
|
let unitId;
|
||||||
let newUnitSymbol = current.unitSymbol ?? current.unitSi ?? '';
|
let newUnitSymbol = current.unitSymbol ?? current.unitSi ?? '';
|
||||||
if (hasUnitSi) {
|
if (hasUnitSi) {
|
||||||
@@ -139,7 +146,7 @@ export default function updateBySku(provider) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasTrackStock) {
|
if (hasTrackStock) {
|
||||||
const oldTrack = Boolean(current.track_stock ?? 1);
|
const oldTrack = Boolean(current.track_stock ?? 0);
|
||||||
const newTrack = Boolean(rawTrackStock);
|
const newTrack = Boolean(rawTrackStock);
|
||||||
if (oldTrack !== newTrack) {
|
if (oldTrack !== newTrack) {
|
||||||
historyLines.push(
|
historyLines.push(
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function queryCategoryArticles(provider) {
|
|||||||
} = row;
|
} = row;
|
||||||
return {
|
return {
|
||||||
...rest,
|
...rest,
|
||||||
track_stock: Boolean(rest.track_stock ?? 1),
|
track_stock: Boolean(rest.track_stock ?? 0),
|
||||||
// MSSQL returns BIT as boolean, SQLite as 0/1 integer. Normalize so both
|
// MSSQL returns BIT as boolean, SQLite as 0/1 integer. Normalize so both
|
||||||
// dialects expose isParent as a real boolean, matching article/getBySku.
|
// dialects expose isParent as a real boolean, matching article/getBySku.
|
||||||
isParent: Boolean(rest.isParent),
|
isParent: Boolean(rest.isParent),
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE,
|
DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE,
|
||||||
CHECK_LEDGER_USAGE as MSSQL_CHECK_USAGE,
|
CHECK_LEDGER_USAGE as MSSQL_CHECK_USAGE,
|
||||||
|
COUNT_BY_COMPANY as MSSQL_COUNT,
|
||||||
|
CHECK_TRACK_STOCK_ARTICLES as MSSQL_CHECK_TRACK_STOCK,
|
||||||
} from '../../providers/mssql/queries/warehouse.js';
|
} from '../../providers/mssql/queries/warehouse.js';
|
||||||
import {
|
import {
|
||||||
DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE,
|
DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE,
|
||||||
CHECK_LEDGER_USAGE as SQLITE_CHECK_USAGE,
|
CHECK_LEDGER_USAGE as SQLITE_CHECK_USAGE,
|
||||||
|
COUNT_BY_COMPANY as SQLITE_COUNT,
|
||||||
|
CHECK_TRACK_STOCK_ARTICLES as SQLITE_CHECK_TRACK_STOCK,
|
||||||
} from '../../providers/sqlite3/queries/warehouse.js';
|
} from '../../providers/sqlite3/queries/warehouse.js';
|
||||||
import { isForeignKeyConstraintError } from '../../providers/provider.js';
|
import { isForeignKeyConstraintError } from '../../providers/provider.js';
|
||||||
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
|
||||||
@@ -15,15 +19,18 @@ export default function del(provider) {
|
|||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
const checkUsageSql = isSqlite ? SQLITE_CHECK_USAGE : MSSQL_CHECK_USAGE;
|
const checkUsageSql = isSqlite ? SQLITE_CHECK_USAGE : MSSQL_CHECK_USAGE;
|
||||||
|
const countSql = isSqlite ? SQLITE_COUNT : MSSQL_COUNT;
|
||||||
|
const checkTrackStockSql = isSqlite ? SQLITE_CHECK_TRACK_STOCK : MSSQL_CHECK_TRACK_STOCK;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a warehouse for the user's selected company, unless referenced in the ledger.
|
* Deletes a warehouse for the user's selected company, unless referenced in the ledger
|
||||||
|
* or it is the last warehouse while articles have stock tracking enabled.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {number} id - The warehouse ID to delete.
|
* @param {number} id - The warehouse ID to delete.
|
||||||
* @param {object} [tx] - Optional transaction to run within.
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
* @throws {Error} If warehouse not found or is in use in the ledger.
|
* @throws {Error} If warehouse not found, in use in the ledger, or is the last warehouse with track_stock articles.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* await store.warehouse.delete(googleAuthId, 1);
|
* await store.warehouse.delete(googleAuthId, 1);
|
||||||
@@ -43,6 +50,16 @@ export default function del(provider) {
|
|||||||
throw new Error('Warehouse in use in ledger');
|
throw new Error('Warehouse in use in ledger');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cannot delete the last warehouse if there are articles with track_stock enabled
|
||||||
|
const countRes = await runner.query(countSql, { companyId });
|
||||||
|
const count = Number(countRes.rows[0]?.count ?? 0);
|
||||||
|
if (count <= 1) {
|
||||||
|
const trackRes = await runner.query(checkTrackStockSql, { companyId });
|
||||||
|
if (trackRes.rows && trackRes.rows.length > 0) {
|
||||||
|
throw new Error('Cannot delete the last warehouse while articles have stock tracking enabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await runner.query(deleteSql, {
|
const result = await runner.query(deleteSql, {
|
||||||
id: numericId,
|
id: numericId,
|
||||||
|
|||||||
Reference in New Issue
Block a user