This commit is contained in:
seb
2026-10-15 15:54:59 +02:00
parent f38d1f11a6
commit 7c8c24eb4d
5 changed files with 338 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
import {
CREATE_MANIFEST as MSSQL_CREATE_MANIFEST,
ORDER_TYPE as MSSQL_ORDER_TYPE,
QUOTE_TYPE as MSSQL_QUOTE_TYPE,
} from '../../providers/mssql/queries/manifestDoc.js';
import {
CREATE_MANIFEST as SQLITE_CREATE_MANIFEST,
ORDER_TYPE as SQLITE_ORDER_TYPE,
QUOTE_TYPE as SQLITE_QUOTE_TYPE,
} from '../../providers/sqlite3/queries/manifestDoc.js';
import {
FIND_ARTICLES_FOR_HISTORY as MSSQL_FIND_ARTICLES,
} from '../../providers/mssql/queries/ledger.js';
import {
FIND_ARTICLES_FOR_HISTORY as SQLITE_FIND_ARTICLES,
} from '../../providers/sqlite3/queries/ledger.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import nextNumber from '../numbers/nextNumber.js';
import taxEnsure from '../tax/ensure.js';
import { insertLines, normalizeLines, parseDate } from '../manifestLines.js';
import { assertCustomerEntity, loadDoc } from '../manifestDoc/helpers.js';
import { recordOrderHistory } from '../articlehistory/record.js';
import { syncOrderSummary } from './syncSummary.js';
import makeRunner from '../runner.js';
export default function createFromQuote(provider) {
const access = resolveSelectedCompanyAccess(provider);
const ensureTax = taxEnsure(provider);
const allocateNumber = nextNumber(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const createManifestSql = isSqlite ? SQLITE_CREATE_MANIFEST : MSSQL_CREATE_MANIFEST;
const orderType = isSqlite ? SQLITE_ORDER_TYPE : MSSQL_ORDER_TYPE;
const quoteType = isSqlite ? SQLITE_QUOTE_TYPE : MSSQL_QUOTE_TYPE;
const findArticlesSql = isSqlite ? SQLITE_FIND_ARTICLES : MSSQL_FIND_ARTICLES;
/**
* Creates a new sales order from an existing quote for the user's selected company.
* Copies lines, entity, and external number from the quote unless explicitly overridden.
* If order number is omitted, it is auto-allocated from the 'order' number sequence.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} quoteId - The id of the source quote.
* @param {object} [body] - Optional overrides for the order.
* @param {string} [body.number] - The order number; auto-allocated from sequence if omitted.
* @param {string} [body.date] - The order date; defaults to current date if omitted.
* @param {string} [body.createdAt] - Creation timestamp; defaults to current time if omitted.
* @param {number} [body.entityId] - Customer entity id override; defaults to quote's entityId.
* @param {string} [body.externalNumber] - Customer reference override; defaults to quote's externalNumber.
* @param {Array<{sku?: string, name?: string, pu?: number, amount: number, net: number, rebate?: number, taxrate: number, comment?: string|null}>} [body.lines] - Lines override; defaults to quote's lines.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, number: string, type: string, entityId: number|null, externalNumber: string|null, date: string, lines: Array<{id: number, sku: string|null, name: string|null, pu: number, amount: number, net: number, rebate: number, taxrate: number, comment: string|null}>}>} The created order.
* @throws {Error} If the quote is not found, or the order is invalid.
*
* @example
* const order = await store.order.createFromQuote(42, 10);
* // => { id, number, type: 'order', entityId, externalNumber, date, lines: [...] }
*/
return async (googleAuthId, quoteId, body = {}, tx) => {
if (!Number.isFinite(quoteId)) {
throw new Error('Quote not found');
}
const runCreate = async (runner) => {
const { companyId, companyuserId } = await access(googleAuthId, runner);
const quoteDoc = await loadDoc(runner, {
type: quoteType,
companyId,
id: quoteId,
});
if (!quoteDoc) {
throw new Error('Quote not found');
}
const date = body.date !== undefined ? parseDate(body.date) : new Date();
if (!date) {
throw new Error('Invalid order');
}
const createdAt = body.createdAt !== undefined ? parseDate(body.createdAt) : new Date();
if (!createdAt) {
throw new Error('Invalid order');
}
let number = body.number?.trim?.() ? body.number.trim() : null;
if (!number) {
number = await allocateNumber(companyId, orderType, runner);
}
const targetEntityId = body.entityId !== undefined ? body.entityId : quoteDoc.entityId;
const entityId = await assertCustomerEntity(
runner,
companyId,
targetEntityId ?? null,
);
const externalNumber = body.externalNumber !== undefined
? (body.externalNumber?.trim?.() ? body.externalNumber.trim() : null)
: (quoteDoc.externalNumber ?? null);
const rawLines = body.lines !== undefined ? body.lines : (quoteDoc.lines ?? []);
const lines = normalizeLines(rawLines);
const created = await runner.query(createManifestSql, {
companyId,
number,
type: orderType,
entityId,
externalNumber,
date,
createdAt,
});
const manifestId = created.rows[0]?.id ?? created.insertId;
await insertLines(runner, {
manifestId,
lines,
ensureTax,
});
// Record article history for all order items
let customerName = null;
if (entityId) {
const entRes = await runner.query('SELECT name FROM entity WHERE id = @entityId', { entityId });
customerName = entRes.rows[0]?.name ?? null;
}
const historyDate = body.createdAt ? createdAt : date;
for (const line of lines) {
const sku = line.sku ? String(line.sku).trim() : '';
if (!sku) continue;
const articleMatches = await runner.query(findArticlesSql, {
sku,
companyId,
});
for (const match of articleMatches.rows) {
await recordOrderHistory(runner, {
articleId: match.articleId,
companyuserId,
date: historyDate,
orderId: manifestId,
number,
customerName,
amount: line.amount,
net: line.net,
rebate: line.rebate,
taxrate: line.taxrate,
});
}
}
const doc = await loadDoc(runner, {
type: orderType,
companyId,
id: manifestId,
});
await syncOrderSummary(runner, { companyId, manifestId });
return doc;
};
if (tx) {
return runCreate(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(runCreate);
}
return runCreate(makeRunner(provider));
};
}