This commit is contained in:
seb
2026-09-26 23:06:05 +02:00
parent e491ecfa47
commit e7f92ebef8
41 changed files with 3632 additions and 1751 deletions

190
src/store/stock/adjust.js Normal file
View File

@@ -0,0 +1,190 @@
import * as mssqlQueries from '../../providers/mssql/queries/manifestDoc.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/manifestDoc.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import nextNumber from '../numbers/nextNumber.js';
import ledgerAdd from '../ledger/add.js';
import makeRunner from '../runner.js';
import { parseDate } from '../manifestLines.js';
const ADJUSTMENT_TYPE = 'adjustment';
const GET_ARTICLE_NAME_SQL = `
SELECT at.name
FROM article a
LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId
WHERE a.id = @articleId;
`;
export default function adjust(provider) {
const access = resolveSelectedCompanyAccess(provider);
const resolveArticle = resolveArticleBySku(provider);
const allocateNumber = nextNumber(provider);
const addLedger = ledgerAdd(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
/**
* Adjusts article stock manually by creating an implicit manifest of type "adjustment",
* allocating a number from the "adjustment" number scheme, and appending to the ledger.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {object} input - Adjustment payload.
* @param {string} [input.sku] - Article SKU (if single-item adjustment).
* @param {number} [input.warehouseId] - Warehouse ID (can also pass warehouse_id).
* @param {number} [input.amount] - Signed adjustment amount (+ to add, - to remove).
* @param {string} [input.text] - Optional adjustment reason/memo.
* @param {string} [input.externalNumber] - Optional external reference.
* @param {string|Date} [input.date] - Optional date (defaults to now).
* @param {Array<object>} [input.lines] - Optional array of items to adjust in one manifest.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array<object>, ledger: Array<object>}>}
*/
return async (googleAuthId, input = {}, tx) => {
const rawLines = Array.isArray(input.lines)
? input.lines
: [
{
sku: input.sku,
warehouseId: input.warehouseId ?? input.warehouse_id,
amount: input.amount,
text: input.text ?? input.transactionText ?? input.transaction_text,
},
];
if (rawLines.length === 0) {
throw new Error('No items to adjust');
}
const defaultWarehouseId = input.warehouseId ?? input.warehouse_id;
const defaultText = input.text ?? input.transactionText ?? input.transaction_text;
const externalNumber = input.externalNumber?.trim?.() ? input.externalNumber.trim() : null;
const date = input.date ? parseDate(input.date) ?? new Date() : new Date();
const createdAt = new Date();
const normalizedLines = rawLines.map((line, idx) => {
const sku = typeof line.sku === 'string' ? line.sku.trim() : '';
if (!sku) {
throw new Error(`SKU required for item ${idx + 1}`);
}
const rawWhId = line.warehouseId ?? line.warehouse_id ?? defaultWarehouseId;
const warehouseId = Number(rawWhId);
if (!Number.isInteger(warehouseId) || warehouseId <= 0) {
throw new Error(`Warehouse required for item ${idx + 1} (${sku})`);
}
const amount = Number(line.amount);
if (!Number.isFinite(amount) || amount === 0) {
throw new Error(`Invalid amount for item ${idx + 1} (${sku}): must be non-zero number`);
}
const text = (line.text ?? line.transactionText ?? line.transaction_text ?? defaultText)?.trim?.() ||
(amount > 0 ? `Manual stock addition (+${amount})` : `Manual stock removal (${amount})`);
return {
sku,
warehouseId,
amount,
text,
};
});
const run = async (runner, activeTx) => {
const { companyId } = await access(googleAuthId, runner);
// Validate all articles exist and belong to the user's company
const resolvedArticles = [];
for (const item of normalizedLines) {
const resolved = await resolveArticle(googleAuthId, item.sku, activeTx);
const nameRes = await runner.query(GET_ARTICLE_NAME_SQL, {
articleId: resolved.articleId,
languageId: resolved.languageId,
});
const articleName = nameRes.rows[0]?.name ?? item.sku;
resolvedArticles.push({ ...item, ...resolved, articleName });
}
// Allocate unique adjustment number using the dedicated adjustment number scheme
const number = await allocateNumber(companyId, ADJUSTMENT_TYPE, activeTx);
// Create the implicit adjustment manifest
const manifestRes = await runner.query(queries.CREATE_MANIFEST, {
companyId,
number,
type: ADJUSTMENT_TYPE,
entityId: null,
externalNumber,
date,
createdAt,
});
const manifestId = manifestRes.rows[0]?.id ?? manifestRes.insertId;
const createdLines = [];
const createdLedgerEntries = [];
for (const item of resolvedArticles) {
// Create manifestarticle row
const lineRes = await runner.query(queries.CREATE_LINE, {
manifestId,
sku: item.sku,
name: item.articleName,
pu: 1,
amount: Math.abs(item.amount),
net: 0,
comment: item.text,
rebate: 0,
taxrate: 0,
});
const manifestarticleId = lineRes.rows[0]?.id ?? lineRes.insertId;
// Append to ledger
const ledgerRow = await addLedger(
googleAuthId,
{
manifestarticle_id: manifestarticleId,
warehouse_id: item.warehouseId,
transaction_type: ADJUSTMENT_TYPE,
amount: item.amount,
transaction_text: item.text,
},
activeTx,
);
createdLines.push({
manifestarticle_id: manifestarticleId,
sku: item.sku,
name: item.articleName,
warehouse_id: item.warehouseId,
amount: item.amount,
text: item.text,
});
createdLedgerEntries.push(ledgerRow);
}
return {
manifest: {
id: manifestId,
number,
type: ADJUSTMENT_TYPE,
date: date.toISOString(),
createdAt: createdAt.toISOString(),
externalNumber,
},
lines: createdLines,
ledger: createdLedgerEntries,
};
};
if (tx) {
return run(makeRunner(provider, tx), tx);
}
if (typeof provider?.transaction === 'function') {
return provider.transaction((runner, activeTx) => run(runner, activeTx));
}
return run(makeRunner(provider));
};
}