Added Multi-Company Support
This commit is contained in:
295
server/db.js
295
server/db.js
@@ -108,6 +108,12 @@ function initialize() {
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_companies (
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
entity_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (user_id, entity_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS entities (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
@@ -115,17 +121,21 @@ function initialize() {
|
||||
tax_id TEXT,
|
||||
fiscal_year_end_month INTEGER NOT NULL DEFAULT 12,
|
||||
base_currency TEXT NOT NULL DEFAULT 'USD',
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
disposed_at TEXT,
|
||||
disposed_reason TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS reference_values (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
entity_id INTEGER REFERENCES entities(id),
|
||||
type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
code TEXT,
|
||||
metadata TEXT,
|
||||
UNIQUE(type, name)
|
||||
UNIQUE(entity_id, type, name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS employees (
|
||||
@@ -207,12 +217,14 @@ function initialize() {
|
||||
|
||||
CREATE TABLE IF NOT EXISTS asset_templates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
entity_id INTEGER REFERENCES entities(id),
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
defaults TEXT NOT NULL,
|
||||
custom_fields TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(entity_id, name)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tax_rule_sets (
|
||||
@@ -328,6 +340,7 @@ function initialize() {
|
||||
|
||||
CREATE TABLE IF NOT EXISTS saved_reports (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
entity_id INTEGER REFERENCES entities(id),
|
||||
name TEXT NOT NULL,
|
||||
report_type TEXT NOT NULL,
|
||||
options_json TEXT NOT NULL,
|
||||
@@ -382,7 +395,8 @@ function initialize() {
|
||||
|
||||
CREATE TABLE IF NOT EXISTS books (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code TEXT NOT NULL UNIQUE,
|
||||
entity_id INTEGER REFERENCES entities(id),
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
book_type TEXT NOT NULL DEFAULT 'tax',
|
||||
@@ -394,7 +408,8 @@ function initialize() {
|
||||
fiscal_year_end_month INTEGER NOT NULL DEFAULT 12,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(entity_id, code)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS alerts (
|
||||
@@ -528,6 +543,7 @@ function initialize() {
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contacts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
entity_id INTEGER REFERENCES entities(id),
|
||||
type TEXT NOT NULL DEFAULT 'other',
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
@@ -571,7 +587,8 @@ function initialize() {
|
||||
|
||||
CREATE TABLE IF NOT EXISTS parts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
part_number TEXT NOT NULL UNIQUE,
|
||||
entity_id INTEGER REFERENCES entities(id),
|
||||
part_number TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category TEXT,
|
||||
@@ -588,7 +605,8 @@ function initialize() {
|
||||
notes TEXT,
|
||||
created_by INTEGER REFERENCES users(id),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(entity_id, part_number)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS part_stock (
|
||||
@@ -759,6 +777,154 @@ function migrate() {
|
||||
ensureColumn('users', 'last_login_at', 'TEXT');
|
||||
ensureColumn('users', 'must_change_password', 'INTEGER NOT NULL DEFAULT 0');
|
||||
dropUsersRoleCheck();
|
||||
// Multi-company scoping: company lifecycle + per-company foreign keys.
|
||||
ensureColumn('entities', 'status', "TEXT NOT NULL DEFAULT 'active'");
|
||||
ensureColumn('entities', 'disposed_at', 'TEXT');
|
||||
ensureColumn('entities', 'disposed_reason', 'TEXT');
|
||||
ensureColumn('pm_plans', 'entity_id', 'INTEGER REFERENCES entities(id)');
|
||||
ensureColumn('alerts', 'entity_id', 'INTEGER REFERENCES entities(id)');
|
||||
rebuildBooksPerCompany();
|
||||
// Phase 2: scope reference data, contacts, templates, saved reports per company.
|
||||
ensureColumn('contacts', 'entity_id', 'INTEGER REFERENCES entities(id)');
|
||||
ensureColumn('saved_reports', 'entity_id', 'INTEGER REFERENCES entities(id)');
|
||||
rebuildReferenceValuesPerCompany();
|
||||
rebuildAssetTemplatesPerCompany();
|
||||
rebuildPartsPerCompany();
|
||||
backfillCompanyScope();
|
||||
}
|
||||
|
||||
// parts became per-company: UNIQUE(part_number) → UNIQUE(entity_id, part_number).
|
||||
function rebuildPartsPerCompany() {
|
||||
const ddl = one("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'parts'");
|
||||
if (!ddl || /entity_id/i.test(ddl.sql)) return; // already scoped
|
||||
db.exec('PRAGMA foreign_keys=OFF');
|
||||
db.exec(`
|
||||
CREATE TABLE parts_rebuild (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
entity_id INTEGER REFERENCES entities(id),
|
||||
part_number TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
category TEXT,
|
||||
manufacturer TEXT,
|
||||
manufacturer_part_number TEXT,
|
||||
barcode TEXT,
|
||||
unit_of_measure TEXT NOT NULL DEFAULT 'each',
|
||||
unit_cost REAL NOT NULL DEFAULT 0,
|
||||
reorder_point REAL NOT NULL DEFAULT 0,
|
||||
reorder_quantity REAL NOT NULL DEFAULT 0,
|
||||
measurements TEXT,
|
||||
supplier_contact_id INTEGER REFERENCES contacts(id),
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
notes TEXT,
|
||||
created_by INTEGER REFERENCES users(id),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(entity_id, part_number)
|
||||
);
|
||||
INSERT INTO parts_rebuild (id, part_number, name, description, category, manufacturer, manufacturer_part_number, barcode, unit_of_measure, unit_cost, reorder_point, reorder_quantity, measurements, supplier_contact_id, status, notes, created_by, created_at, updated_at)
|
||||
SELECT id, part_number, name, description, category, manufacturer, manufacturer_part_number, barcode, unit_of_measure, unit_cost, reorder_point, reorder_quantity, measurements, supplier_contact_id, status, notes, created_by, created_at, updated_at FROM parts;
|
||||
DROP TABLE parts;
|
||||
ALTER TABLE parts_rebuild RENAME TO parts;
|
||||
`);
|
||||
db.exec('PRAGMA foreign_keys=ON');
|
||||
}
|
||||
|
||||
// reference_values became per-company: UNIQUE(type, name) → UNIQUE(entity_id, type, name).
|
||||
function rebuildReferenceValuesPerCompany() {
|
||||
const ddl = one("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'reference_values'");
|
||||
if (!ddl || /entity_id/i.test(ddl.sql)) return; // already scoped
|
||||
db.exec('PRAGMA foreign_keys=OFF');
|
||||
db.exec(`
|
||||
CREATE TABLE reference_values_rebuild (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
entity_id INTEGER REFERENCES entities(id),
|
||||
type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
code TEXT,
|
||||
metadata TEXT,
|
||||
UNIQUE(entity_id, type, name)
|
||||
);
|
||||
INSERT INTO reference_values_rebuild (id, type, name, code, metadata)
|
||||
SELECT id, type, name, code, metadata FROM reference_values;
|
||||
DROP TABLE reference_values;
|
||||
ALTER TABLE reference_values_rebuild RENAME TO reference_values;
|
||||
`);
|
||||
db.exec('PRAGMA foreign_keys=ON');
|
||||
}
|
||||
|
||||
// asset_templates became per-company: UNIQUE(name) → UNIQUE(entity_id, name).
|
||||
function rebuildAssetTemplatesPerCompany() {
|
||||
const ddl = one("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'asset_templates'");
|
||||
if (!ddl || /entity_id/i.test(ddl.sql)) return; // already scoped
|
||||
db.exec('PRAGMA foreign_keys=OFF');
|
||||
db.exec(`
|
||||
CREATE TABLE asset_templates_rebuild (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
entity_id INTEGER REFERENCES entities(id),
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
defaults TEXT NOT NULL,
|
||||
custom_fields TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(entity_id, name)
|
||||
);
|
||||
INSERT INTO asset_templates_rebuild (id, name, description, defaults, custom_fields, created_at, updated_at)
|
||||
SELECT id, name, description, defaults, custom_fields, created_at, updated_at FROM asset_templates;
|
||||
DROP TABLE asset_templates;
|
||||
ALTER TABLE asset_templates_rebuild RENAME TO asset_templates;
|
||||
`);
|
||||
db.exec('PRAGMA foreign_keys=ON');
|
||||
}
|
||||
|
||||
// Books became per-company: the old global UNIQUE(code) must become UNIQUE(entity_id, code).
|
||||
// SQLite can't alter a constraint in place, so rebuild when the legacy column-level UNIQUE is present.
|
||||
function rebuildBooksPerCompany() {
|
||||
const ddl = one("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'books'");
|
||||
if (!ddl || !/code\s+TEXT\s+NOT\s+NULL\s+UNIQUE/i.test(ddl.sql)) return; // already composite
|
||||
db.exec('PRAGMA foreign_keys=OFF');
|
||||
db.exec(`
|
||||
CREATE TABLE books_rebuild (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
entity_id INTEGER REFERENCES entities(id),
|
||||
code TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
book_type TEXT NOT NULL DEFAULT 'tax',
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
is_primary INTEGER NOT NULL DEFAULT 0,
|
||||
tax_rule_set_id INTEGER REFERENCES tax_rule_sets(id),
|
||||
default_method TEXT,
|
||||
default_convention TEXT,
|
||||
fiscal_year_end_month INTEGER NOT NULL DEFAULT 12,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(entity_id, code)
|
||||
);
|
||||
INSERT INTO books_rebuild (id, code, name, description, book_type, enabled, is_primary, tax_rule_set_id, default_method, default_convention, fiscal_year_end_month, sort_order, created_at, updated_at)
|
||||
SELECT id, code, name, description, book_type, enabled, is_primary, tax_rule_set_id, default_method, default_convention, fiscal_year_end_month, sort_order, created_at, updated_at FROM books;
|
||||
DROP TABLE books;
|
||||
ALTER TABLE books_rebuild RENAME TO books;
|
||||
`);
|
||||
db.exec('PRAGMA foreign_keys=ON');
|
||||
}
|
||||
|
||||
// Assign pre-multi-company rows to the default (first active) company. No-op on a fresh DB.
|
||||
function backfillCompanyScope() {
|
||||
const def = one("SELECT id FROM entities WHERE status = 'active' ORDER BY id LIMIT 1") || one('SELECT id FROM entities ORDER BY id LIMIT 1');
|
||||
if (!def) return;
|
||||
// OR IGNORE so a pre-existing duplicate (e.g. two templates with the same name) can't break the
|
||||
// new per-company UNIQUE constraints — the duplicate is left unscoped rather than crashing init.
|
||||
for (const table of ['assets', 'books', 'pm_plans', 'alerts', 'reference_values', 'contacts', 'asset_templates', 'saved_reports', 'parts']) {
|
||||
run(`UPDATE OR IGNORE ${table} SET entity_id = ? WHERE entity_id IS NULL`, [def.id]);
|
||||
}
|
||||
// Preserve "everyone sees all" on upgrade: grant each existing user every existing company,
|
||||
// but only when the mapping is empty (first run) so deliberate later restrictions aren't undone.
|
||||
if (!one('SELECT 1 AS x FROM user_companies LIMIT 1')) {
|
||||
run('INSERT OR IGNORE INTO user_companies (user_id, entity_id) SELECT u.id, e.id FROM users u CROSS JOIN entities e');
|
||||
}
|
||||
}
|
||||
|
||||
// Custom roles require the legacy CHECK(role IN (...)) constraint on `users` to be removed.
|
||||
@@ -787,6 +953,62 @@ function dropUsersRoleCheck() {
|
||||
db.exec('PRAGMA foreign_keys=ON');
|
||||
}
|
||||
|
||||
// Seed the standard book set for a company (idempotent per company). Called during initial seed and
|
||||
// whenever a new company is created so it starts with GAAP/FEDERAL/STATE/AMT/ACE/USER + the PM book.
|
||||
function seedBooksForCompany(entityId) {
|
||||
if (!entityId || one('SELECT id FROM books WHERE entity_id = ? LIMIT 1', [entityId])) return;
|
||||
const createdAt = now();
|
||||
const federalRule = one("SELECT id FROM tax_rule_sets WHERE active = 1 ORDER BY effective_date DESC, id DESC LIMIT 1");
|
||||
const ruleId = federalRule ? federalRule.id : null;
|
||||
const bookDefs = [
|
||||
['GAAP', 'GAAP / Financial', 'Primary book for financial statements', 'financial', 1, 1, 'straight_line', 'half_year'],
|
||||
['FEDERAL', 'Federal Tax', 'US federal income tax depreciation', 'tax', 1, 0, 'macrs_gds_200db_5', 'half_year'],
|
||||
['STATE', 'State Tax', 'State income tax depreciation', 'tax', 1, 0, 'macrs_gds_200db_5', 'half_year'],
|
||||
['AMT', 'Alternative Minimum Tax', 'AMT depreciation adjustments', 'tax', 1, 0, 'macrs_gds_150db_5', 'half_year'],
|
||||
['ACE', 'Adjusted Current Earnings', 'ACE depreciation', 'tax', 1, 0, 'macrs_ads_sl_5', 'half_year'],
|
||||
['USER', 'User-defined', 'Custom internal reporting book', 'internal', 0, 0, 'straight_line', 'half_year']
|
||||
];
|
||||
bookDefs.forEach((def, index) => {
|
||||
run(
|
||||
`INSERT INTO books (entity_id, code, name, description, book_type, enabled, is_primary, tax_rule_set_id, default_method, default_convention, sort_order, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[entityId, def[0], def[1], def[2], def[3], def[4], def[5], ruleId, def[6], def[7], index, createdAt, createdAt]
|
||||
);
|
||||
});
|
||||
// The PM book tracks actual preventative-maintenance spend per asset (not depreciation).
|
||||
run(
|
||||
`INSERT OR IGNORE INTO books (entity_id, code, name, description, book_type, enabled, is_primary, sort_order, created_at, updated_at)
|
||||
VALUES (?, 'PM', 'PM / Maintenance', 'Actual preventative-maintenance spend per asset', 'maintenance', 1, 0, 6, ?, ?)`,
|
||||
[entityId, createdAt, createdAt]
|
||||
);
|
||||
}
|
||||
|
||||
// Seed the standard reference data (categories, locations, departments, part categories) for a company.
|
||||
// Idempotent per company; called during initial seed and when a new company is created.
|
||||
function seedReferenceValuesForCompany(entityId) {
|
||||
if (!entityId) return;
|
||||
const refs = [
|
||||
['location', 'Headquarters', 'HQ'],
|
||||
['location', 'Warehouse', 'WH'],
|
||||
['department', 'Accounting', 'ACCT'],
|
||||
['department', 'Operations', 'OPS'],
|
||||
['category', 'Computer Equipment', 'COMP'],
|
||||
['category', 'Office Furniture', 'FURN'],
|
||||
['category', 'Vehicles', 'AUTO'],
|
||||
['category', 'Leasehold Improvements', 'LHI'],
|
||||
['part_category', 'Electrical', null],
|
||||
['part_category', 'Plumbing', null],
|
||||
['part_category', 'Structural Component', null],
|
||||
['part_category', 'Electronic Component', null],
|
||||
['part_category', 'Fire Safety', null],
|
||||
['part_category', 'Mechanical Safety', null],
|
||||
['part_category', 'Other', null]
|
||||
];
|
||||
for (const item of refs) {
|
||||
run('INSERT OR IGNORE INTO reference_values (entity_id, type, name, code, metadata) VALUES (?, ?, ?, ?, ?)', [entityId, item[0], item[1], item[2], '{}']);
|
||||
}
|
||||
}
|
||||
|
||||
function seed() {
|
||||
const createdAt = now();
|
||||
|
||||
@@ -915,31 +1137,8 @@ function seed() {
|
||||
);
|
||||
}
|
||||
|
||||
const refs = [
|
||||
['location', 'Headquarters', 'HQ'],
|
||||
['location', 'Warehouse', 'WH'],
|
||||
['department', 'Accounting', 'ACCT'],
|
||||
['department', 'Operations', 'OPS'],
|
||||
['category', 'Computer Equipment', 'COMP'],
|
||||
['category', 'Office Furniture', 'FURN'],
|
||||
['category', 'Vehicles', 'AUTO'],
|
||||
['category', 'Leasehold Improvements', 'LHI'],
|
||||
['part_category', 'Electrical', null],
|
||||
['part_category', 'Plumbing', null],
|
||||
['part_category', 'Structural Component', null],
|
||||
['part_category', 'Electronic Component', null],
|
||||
['part_category', 'Fire Safety', null],
|
||||
['part_category', 'Mechanical Safety', null],
|
||||
['part_category', 'Other', null]
|
||||
];
|
||||
for (const item of refs) {
|
||||
run('INSERT OR IGNORE INTO reference_values (type, name, code, metadata) VALUES (?, ?, ?, ?)', [
|
||||
item[0],
|
||||
item[1],
|
||||
item[2],
|
||||
'{}'
|
||||
]);
|
||||
}
|
||||
const refEntity = one('SELECT id FROM entities ORDER BY id LIMIT 1');
|
||||
if (refEntity) seedReferenceValuesForCompany(refEntity.id);
|
||||
|
||||
run(
|
||||
`INSERT OR IGNORE INTO employees (
|
||||
@@ -1005,32 +1204,10 @@ function seed() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!one('SELECT id FROM books LIMIT 1')) {
|
||||
const federalRule = one("SELECT id FROM tax_rule_sets WHERE active = 1 ORDER BY effective_date DESC, id DESC LIMIT 1");
|
||||
const ruleId = federalRule ? federalRule.id : null;
|
||||
const bookDefs = [
|
||||
['GAAP', 'GAAP / Financial', 'Primary book for financial statements', 'financial', 1, 1, 'straight_line', 'half_year'],
|
||||
['FEDERAL', 'Federal Tax', 'US federal income tax depreciation', 'tax', 1, 0, 'macrs_gds_200db_5', 'half_year'],
|
||||
['STATE', 'State Tax', 'State income tax depreciation', 'tax', 1, 0, 'macrs_gds_200db_5', 'half_year'],
|
||||
['AMT', 'Alternative Minimum Tax', 'AMT depreciation adjustments', 'tax', 1, 0, 'macrs_gds_150db_5', 'half_year'],
|
||||
['ACE', 'Adjusted Current Earnings', 'ACE depreciation', 'tax', 1, 0, 'macrs_ads_sl_5', 'half_year'],
|
||||
['USER', 'User-defined', 'Custom internal reporting book', 'internal', 0, 0, 'straight_line', 'half_year']
|
||||
];
|
||||
bookDefs.forEach((def, index) => {
|
||||
run(
|
||||
`INSERT INTO books (code, name, description, book_type, enabled, is_primary, tax_rule_set_id, default_method, default_convention, sort_order, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[def[0], def[1], def[2], def[3], def[4], def[5], ruleId, def[6], def[7], index, createdAt, createdAt]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// The PM book tracks actual preventative-maintenance spend per asset (not depreciation).
|
||||
run(
|
||||
`INSERT OR IGNORE INTO books (code, name, description, book_type, enabled, is_primary, sort_order, created_at, updated_at)
|
||||
VALUES ('PM', 'PM / Maintenance', 'Actual preventative-maintenance spend per asset', 'maintenance', 1, 0, 6, ?, ?)`,
|
||||
[createdAt, createdAt]
|
||||
);
|
||||
// Seed the standard book set for the default company. Each company gets its own set; see
|
||||
// seedBooksForCompany (also called when a new company is created).
|
||||
const defaultEntity = one('SELECT id FROM entities ORDER BY id LIMIT 1');
|
||||
if (defaultEntity) seedBooksForCompany(defaultEntity.id);
|
||||
|
||||
const settings = {
|
||||
server_fqdn: process.env.DEPRECORE_SERVER_FQDN || 'localhost',
|
||||
@@ -1091,5 +1268,7 @@ module.exports = {
|
||||
one,
|
||||
parseJson,
|
||||
run,
|
||||
seedBooksForCompany,
|
||||
seedReferenceValuesForCompany,
|
||||
tx
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user