This commit is contained in:
seb
2026-10-15 20:14:51 +02:00
parent 7c8c24eb4d
commit 2179b2e241
23 changed files with 1242 additions and 105 deletions

View File

@@ -0,0 +1,123 @@
import {
ENSURE_TYPE as MSSQL_ENSURE_TYPE,
GET_FOR_UPDATE as MSSQL_GET_FOR_UPDATE,
UPDATE_BY_COMPANY_AND_ID as MSSQL_UPDATE_BY_COMPANY_AND_ID,
} from '../../providers/mssql/queries/numbers.js';
import {
ENSURE_TYPE as SQLITE_ENSURE_TYPE,
GET_FOR_UPDATE as SQLITE_GET_FOR_UPDATE,
UPDATE_BY_COMPANY_AND_ID as SQLITE_UPDATE_BY_COMPANY_AND_ID,
} from '../../providers/sqlite3/queries/numbers.js';
/**
* Ensures a number sequence row exists for a company and type,
* inferring the company's prefix style, padding, and next counter if not yet created or unconfigured.
*
* @param {object} runner - The database runner.
* @param {number} companyId - The id of the company.
* @param {string} type - The number type (e.g. 'order', 'quote', 'sku').
* @returns {Promise<{id: number, type: string, prefix: string, padding: number, next: number}>} The sequence row.
*/
export async function ensureNumberType(runner, companyId, type) {
const isSqlite = runner.dialect === 'sqlite3';
const getSql = isSqlite ? SQLITE_GET_FOR_UPDATE : MSSQL_GET_FOR_UPDATE;
const ensureSql = isSqlite ? SQLITE_ENSURE_TYPE : MSSQL_ENSURE_TYPE;
const updateSql = isSqlite ? SQLITE_UPDATE_BY_COMPANY_AND_ID : MSSQL_UPDATE_BY_COMPANY_AND_ID;
let res = await runner.query(getSql, { companyId, type });
let row = res.rows[0];
// If already exists and is configured (has prefix or padding > 0, or not order/quote), return row
if (row && (row.prefix || row.padding > 0 || (type !== 'order' && type !== 'quote'))) {
return row;
}
// Find reference sequence for company to match prefix and padding style
const listSql = isSqlite
? 'SELECT "type", prefix, padding, "next" FROM numbers WHERE company_id = @companyId'
: 'SELECT [type], prefix, padding, [next] FROM numbers WHERE company_id = @companyId';
const existingSeqs = (await runner.query(listSql, { companyId })).rows;
const skuSeq = existingSeqs.find((r) => r.type === 'sku');
const deliverySeq = existingSeqs.find((r) => r.type === 'delivery');
const entitySeq = existingSeqs.find((r) => r.type === 'entity');
const refSeq = skuSeq || deliverySeq || entitySeq;
const pad = refSeq ? Math.max(0, Number(refSeq.padding) || 0) : 6;
const prefixStyle = refSeq?.prefix ?? '';
let prefix = '';
if (type === 'shipment') {
prefix = '-L-';
} else if (type === 'quote') {
if (prefixStyle.startsWith('SKU-') || prefixStyle.startsWith('DEL-') || prefixStyle.startsWith('C-')) {
prefix = 'Q-';
} else if (prefixStyle.startsWith('A-') || prefixStyle.startsWith('L-') || prefixStyle.startsWith('K-')) {
prefix = 'AG-';
} else {
prefix = 'ANG-';
}
} else if (type === 'order') {
if (prefixStyle.startsWith('SKU-') || prefixStyle.startsWith('DEL-') || prefixStyle.startsWith('C-')) {
prefix = 'ORD-';
} else if (prefixStyle.startsWith('A-') || prefixStyle.startsWith('L-') || prefixStyle.startsWith('K-')) {
prefix = 'AU-';
} else {
prefix = 'AUF-';
}
} else if (type === 'adjustment') {
prefix = prefixStyle.endsWith('-') ? 'KORR-' : '';
}
const padding = (type === 'order' || type === 'quote') ? (pad || 6) : (type === 'adjustment' ? 5 : (refSeq?.padding ?? 0));
// Determine next counter by scanning manifest for existing documents
let next = 1;
if (type === 'order' || type === 'quote') {
const manifestSql = isSqlite
? 'SELECT "number" FROM manifest WHERE company_id = @companyId AND "type" = @type'
: 'SELECT [number] FROM manifest WHERE company_id = @companyId AND [type] = @type';
const manifests = (await runner.query(manifestSql, { companyId, type })).rows;
let maxFound = 0;
for (const m of manifests) {
const numStr = String(m.number ?? '').trim();
if (prefix && numStr.startsWith(prefix)) {
const val = parseInt(numStr.slice(prefix.length), 10);
if (!Number.isNaN(val) && val > maxFound) maxFound = val;
} else if (!prefix) {
const val = parseInt(numStr, 10);
if (!Number.isNaN(val) && val > maxFound) maxFound = val;
}
}
if (maxFound > 0) {
next = maxFound + 1;
}
}
if (row) {
// Row existed but was empty unconfigured row (e.g. prefix: '', padding: 0)
const effectiveNext = Math.max(next, row.next ?? 1);
await runner.query(updateSql, {
id: row.id,
companyId,
prefix,
padding,
next: effectiveNext,
});
row.prefix = prefix;
row.padding = padding;
row.next = effectiveNext;
} else {
await runner.query(ensureSql, {
companyId,
type,
prefix,
padding,
next,
});
res = await runner.query(getSql, { companyId, type });
row = res.rows[0];
}
return row;
}

View File

@@ -1,20 +1,21 @@
import {
ENSURE_TYPE as MSSQL_ENSURE_TYPE,
LIST_BY_COMPANY as MSSQL_LIST_BY_COMPANY,
} from '../../providers/mssql/queries/numbers.js';
import {
ENSURE_TYPE as SQLITE_ENSURE_TYPE,
LIST_BY_COMPANY as SQLITE_LIST_BY_COMPANY,
} from '../../providers/sqlite3/queries/numbers.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import { ensureNumberType } from './ensureType.js';
import makeRunner from '../runner.js';
const DEFAULT_TYPES = [
{ type: 'sku', prefix: '' },
{ type: 'entity', prefix: '' },
{ type: 'delivery', prefix: '' },
{ type: 'adjustment', prefix: '' },
{ type: 'shipment', prefix: '-L-' },
'sku',
'entity',
'delivery',
'adjustment',
'shipment',
'quote',
'order',
];
function shape(row) {
@@ -30,13 +31,12 @@ function shape(row) {
export default function list(provider) {
const access = resolveSelectedCompanyAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const ensureSql = isSqlite ? SQLITE_ENSURE_TYPE : MSSQL_ENSURE_TYPE;
const sql =
isSqlite ? SQLITE_LIST_BY_COMPANY : MSSQL_LIST_BY_COMPANY;
/**
* Lists the number sequences of the user's selected company.
* Ensures that default sequences ('sku', 'entity', 'delivery', 'adjustment', 'shipment') exist.
* Ensures that default sequences ('sku', 'entity', 'delivery', 'adjustment', 'shipment', 'quote', 'order') exist.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {object} [tx] - Optional transaction to run within.
@@ -52,8 +52,7 @@ export default function list(provider) {
for (const item of DEFAULT_TYPES) {
const type = typeof item === 'string' ? item : item.type;
const prefix = typeof item === 'string' ? (type === 'shipment' ? '-L-' : '') : item.prefix;
await runner.query(ensureSql, { companyId, type, prefix });
await ensureNumberType(runner, companyId, type);
}
const result = await runner.query(sql, { companyId });

View File

@@ -1,13 +1,10 @@
import {
ENSURE_TYPE as MSSQL_ENSURE_TYPE,
GET_FOR_UPDATE as MSSQL_GET_FOR_UPDATE,
INCREMENT as MSSQL_INCREMENT,
} from '../../providers/mssql/queries/numbers.js';
import {
ENSURE_TYPE as SQLITE_ENSURE_TYPE,
GET_FOR_UPDATE as SQLITE_GET_FOR_UPDATE,
INCREMENT as SQLITE_INCREMENT,
} from '../../providers/sqlite3/queries/numbers.js';
import { ensureNumberType } from './ensureType.js';
import makeRunner from '../runner.js';
function formatNumber(prefix, padding, num) {
@@ -19,8 +16,6 @@ function formatNumber(prefix, padding, num) {
export default function nextNumber(provider) {
const isSqlite = provider?.dialect === 'sqlite3';
const ensureSql = isSqlite ? SQLITE_ENSURE_TYPE : MSSQL_ENSURE_TYPE;
const getSql = isSqlite ? SQLITE_GET_FOR_UPDATE : MSSQL_GET_FOR_UPDATE;
const incSql = isSqlite ? SQLITE_INCREMENT : MSSQL_INCREMENT;
/**
@@ -37,10 +32,7 @@ export default function nextNumber(provider) {
*/
return async (companyId, type, tx) => {
const doAlloc = async (runner) => {
const prefix = type === 'shipment' ? '-L-' : '';
await runner.query(ensureSql, { companyId, type, prefix });
const res = await runner.query(getSql, { companyId, type });
const row = res.rows[0];
const row = await ensureNumberType(runner, companyId, type);
if (!row) {
throw new Error('Number type not found for company');
}

View File

@@ -1,11 +1,4 @@
import {
ENSURE_TYPE as MSSQL_ENSURE_TYPE,
GET_FOR_UPDATE as MSSQL_GET_FOR_UPDATE,
} from '../../providers/mssql/queries/numbers.js';
import {
ENSURE_TYPE as SQLITE_ENSURE_TYPE,
GET_FOR_UPDATE as SQLITE_GET_FOR_UPDATE,
} from '../../providers/sqlite3/queries/numbers.js';
import { ensureNumberType } from './ensureType.js';
import makeRunner from '../runner.js';
function formatNumber(prefix, padding, num) {
@@ -16,10 +9,6 @@ function formatNumber(prefix, padding, num) {
}
export default function peekNumber(provider) {
const isSqlite = provider?.dialect === 'sqlite3';
const ensureSql = isSqlite ? SQLITE_ENSURE_TYPE : MSSQL_ENSURE_TYPE;
const getSql = isSqlite ? SQLITE_GET_FOR_UPDATE : MSSQL_GET_FOR_UPDATE;
/**
* Returns the next number for a company and type without consuming it.
*
@@ -34,10 +23,7 @@ export default function peekNumber(provider) {
*/
return async (companyId, type, tx) => {
const doPeek = async (runner) => {
const prefix = type === 'shipment' ? '-L-' : '';
await runner.query(ensureSql, { companyId, type, prefix });
const res = await runner.query(getSql, { companyId, type });
const row = res.rows[0];
const row = await ensureNumberType(runner, companyId, type);
if (!row) {
throw new Error('Number type not found for company');
}