Added Multi-Company Support
This commit is contained in:
@@ -44,9 +44,12 @@ const assetColumns = [
|
||||
const defaultBooks = ['GAAP', 'FEDERAL', 'STATE', 'AMT', 'ACE', 'USER'];
|
||||
|
||||
// Depreciation books currently enabled in the registry (excludes the maintenance/PM book).
|
||||
function enabledBookCodes() {
|
||||
function enabledBookCodes(companyId) {
|
||||
try {
|
||||
const rows = all("SELECT code FROM books WHERE enabled = 1 AND book_type != 'maintenance' ORDER BY sort_order, id");
|
||||
const rows = all(
|
||||
"SELECT code FROM books WHERE enabled = 1 AND book_type != 'maintenance' AND (? IS NULL OR entity_id = ?) ORDER BY sort_order, id",
|
||||
[companyId || null, companyId || null]
|
||||
);
|
||||
if (rows.length) return rows.map((row) => row.code);
|
||||
} catch {
|
||||
// books table may not exist on a very old database
|
||||
@@ -121,17 +124,18 @@ function hydrateAsset(id) {
|
||||
return asset;
|
||||
}
|
||||
|
||||
function lookupAssetByCode(code) {
|
||||
function lookupAssetByCode(code, companyId) {
|
||||
const value = String(code || '').trim();
|
||||
if (!value) return null;
|
||||
const row = one(
|
||||
`SELECT id FROM assets
|
||||
WHERE barcode_value = ? COLLATE NOCASE
|
||||
WHERE (? IS NULL OR entity_id = ?)
|
||||
AND (barcode_value = ? COLLATE NOCASE
|
||||
OR asset_id = ? COLLATE NOCASE
|
||||
OR serial_number = ? COLLATE NOCASE
|
||||
OR serial_number = ? COLLATE NOCASE)
|
||||
ORDER BY (barcode_value = ? COLLATE NOCASE) DESC, id
|
||||
LIMIT 1`,
|
||||
[value, value, value, value]
|
||||
[companyId || null, companyId || null, value, value, value, value]
|
||||
);
|
||||
return row ? hydrateAsset(row.id) : null;
|
||||
}
|
||||
@@ -212,8 +216,8 @@ function normalizeAssetInput(body) {
|
||||
return input;
|
||||
}
|
||||
|
||||
function createBooks(assetId, asset, books = []) {
|
||||
const selectedBooks = books.length ? books : enabledBookCodes().map((book) => ({ book_type: book }));
|
||||
function createBooks(assetId, asset, books = [], companyId) {
|
||||
const selectedBooks = books.length ? books : enabledBookCodes(companyId).map((book) => ({ book_type: book }));
|
||||
for (const book of selectedBooks) {
|
||||
run(
|
||||
`INSERT OR REPLACE INTO asset_books (
|
||||
@@ -262,9 +266,10 @@ function listAssets(query = {}) {
|
||||
]).map(assetFromRow);
|
||||
}
|
||||
|
||||
function createAsset(body, userId) {
|
||||
function createAsset(body, userId, companyId) {
|
||||
const created = now();
|
||||
const input = normalizeAssetInput(body);
|
||||
if (companyId) input.entity_id = companyId; // new assets belong to the active company
|
||||
const result = tx(() => {
|
||||
input.asset_id = resolveNewAssetId(input); // category ID template, else default sequence
|
||||
const insert = run(
|
||||
@@ -272,7 +277,7 @@ function createAsset(body, userId) {
|
||||
VALUES (${assetColumns.map(() => '?').join(', ')}, ?, ?, ?)`,
|
||||
[...assetColumns.map((column) => input[column] ?? null), userId, created, created]
|
||||
);
|
||||
createBooks(insert.lastInsertRowid, input, body.books || []);
|
||||
createBooks(insert.lastInsertRowid, input, body.books || [], input.entity_id);
|
||||
run('UPDATE assets SET asset_class_number = ? WHERE id = ?', [assetClassNumberForCategory(input.category), insert.lastInsertRowid]);
|
||||
return insert;
|
||||
});
|
||||
@@ -281,11 +286,13 @@ function createAsset(body, userId) {
|
||||
return asset;
|
||||
}
|
||||
|
||||
function updateAsset(id, body, userId) {
|
||||
function updateAsset(id, body, userId, companyId) {
|
||||
const before = hydrateAsset(id);
|
||||
if (!before) return null;
|
||||
if (companyId && before.entity_id !== companyId) return null; // belongs to another company
|
||||
|
||||
const input = normalizeAssetInput({ ...before, ...body });
|
||||
if (companyId) input.entity_id = companyId; // assets stay within their company in phase 1
|
||||
// When depreciation-critical fields change, the caller chooses when the change applies.
|
||||
// 'placed_in_service' rebuilds the whole schedule by clearing prior depreciation;
|
||||
// 'going_forward' keeps depreciation already recorded (the stored prior depreciation).
|
||||
@@ -296,7 +303,7 @@ function updateAsset(id, body, userId) {
|
||||
`UPDATE assets SET ${assetColumns.map((column) => `${column} = ?`).join(', ')}, updated_at = ? WHERE id = ?`,
|
||||
[...assetColumns.map((column) => input[column] ?? null), updated, id]
|
||||
);
|
||||
if (Array.isArray(body.books)) createBooks(id, input, body.books);
|
||||
if (Array.isArray(body.books)) createBooks(id, input, body.books, input.entity_id);
|
||||
run('UPDATE assets SET asset_class_number = ? WHERE id = ?', [assetClassNumberForCategory(input.category), id]);
|
||||
});
|
||||
|
||||
@@ -305,17 +312,23 @@ function updateAsset(id, body, userId) {
|
||||
return asset;
|
||||
}
|
||||
|
||||
function deleteAsset(id, userId) {
|
||||
function deleteAsset(id, userId, companyId) {
|
||||
const before = hydrateAsset(id);
|
||||
if (!before) return false;
|
||||
if (companyId && before.entity_id !== companyId) return false; // belongs to another company
|
||||
run('DELETE FROM assets WHERE id = ?', [id]);
|
||||
audit(userId, 'delete', 'asset', id, before, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
function massUpdateAssets(ids, changes, userId) {
|
||||
function massUpdateAssets(ids, changes, userId, companyId) {
|
||||
const allowed = ['status', 'location', 'department', 'group_name', 'in_service_date', 'useful_life_months', 'condition', 'disposal_date'];
|
||||
const columns = allowed.filter((column) => Object.prototype.hasOwnProperty.call(changes, column));
|
||||
// Restrict to assets in the active company so a forged id list can't touch another company's data.
|
||||
if (companyId && ids.length) {
|
||||
const placeholders = ids.map(() => '?').join(',');
|
||||
ids = all(`SELECT id FROM assets WHERE entity_id = ? AND id IN (${placeholders})`, [companyId, ...ids]).map((r) => r.id);
|
||||
}
|
||||
if (!ids.length || !columns.length) return 0;
|
||||
|
||||
tx(() => {
|
||||
@@ -330,26 +343,29 @@ function massUpdateAssets(ids, changes, userId) {
|
||||
return ids.length;
|
||||
}
|
||||
|
||||
function assetExportRows() {
|
||||
function assetExportRows(companyId) {
|
||||
return all(`
|
||||
SELECT a.*, e.name AS entity_name
|
||||
FROM assets a
|
||||
LEFT JOIN entities e ON e.id = a.entity_id
|
||||
WHERE (? IS NULL OR a.entity_id = ?)
|
||||
ORDER BY a.asset_id
|
||||
`).map((asset) => ({
|
||||
`, [companyId || null, companyId || null]).map((asset) => ({
|
||||
...asset,
|
||||
listed_property: Boolean(asset.listed_property),
|
||||
custom_fields: parseJson(asset.custom_fields, {})
|
||||
}));
|
||||
}
|
||||
|
||||
function upsertImportedAssets(rows, userId) {
|
||||
function upsertImportedAssets(rows, userId, companyId) {
|
||||
let imported = 0;
|
||||
tx(() => {
|
||||
for (const row of rows) {
|
||||
const input = normalizeAssetInput(row);
|
||||
if (companyId) input.entity_id = companyId; // imports land in the active company
|
||||
if (!input.asset_id) input.asset_id = resolveNewAssetId(input);
|
||||
const existing = one('SELECT id FROM assets WHERE asset_id = ?', [input.asset_id]);
|
||||
// Match within the active company so imports don't collide with another company's asset ids.
|
||||
const existing = one('SELECT id FROM assets WHERE asset_id = ? AND (? IS NULL OR entity_id = ?)', [input.asset_id, companyId || null, companyId || null]);
|
||||
const classNumber = assetClassNumberForCategory(input.category);
|
||||
if (existing) {
|
||||
run(
|
||||
@@ -362,7 +378,7 @@ function upsertImportedAssets(rows, userId) {
|
||||
VALUES (${assetColumns.map(() => '?').join(', ')}, ?, ?, ?)`,
|
||||
[...assetColumns.map((column) => input[column] ?? null), userId, now(), now()]
|
||||
);
|
||||
createBooks(result.lastInsertRowid, input, []);
|
||||
createBooks(result.lastInsertRowid, input, [], input.entity_id);
|
||||
run('UPDATE assets SET asset_class_number = ? WHERE id = ?', [classNumber, result.lastInsertRowid]);
|
||||
}
|
||||
imported += 1;
|
||||
|
||||
Reference in New Issue
Block a user