136 lines
5.5 KiB
JavaScript
136 lines
5.5 KiB
JavaScript
// services/rmOvgSettingsStore.js
|
||
// Admin-configured Raw Material "Ovg%" override — maps an item code and/or
|
||
// its SAP item group to a default overage percentage. Used by work-order.html
|
||
// to pre-fill a raw material row's Ovg. cell and to recompute "Qty Req./100 ml"
|
||
// as (BOM value) − (BOM value × Ovg%/100) instead of the plain BOM figure,
|
||
// for any item this resolves a match for. See [[work-order-qty-req-two-way-sync]]
|
||
// for the existing Raw Material calc model this feature layers on top of.
|
||
const sql = require('mssql');
|
||
|
||
const TABLE = `[dbo].[ZRM_OVG_SETTINGS]`;
|
||
|
||
let _conn = null;
|
||
async function getConn() {
|
||
if (_conn) return _conn;
|
||
_conn = await sql.connect({
|
||
server: process.env.APP_SQL_HOST,
|
||
port: parseInt(process.env.APP_SQL_PORT),
|
||
user: process.env.APP_SQL_USER,
|
||
password: process.env.APP_SQL_PASSWORD,
|
||
database: process.env.APP_SQL_DATABASE,
|
||
options: { encrypt: true, trustServerCertificate: true },
|
||
});
|
||
return _conn;
|
||
}
|
||
async function exec(sqlQuery, params = []) {
|
||
const conn = await getConn();
|
||
const request = conn.request();
|
||
params.forEach((param, index) => { request.input(`param${index}`, param); });
|
||
const replacedSql = sqlQuery.replace(/\?/g, (m, offset, string) => {
|
||
const i = (string.slice(0, offset).match(/\?/g) || []).length;
|
||
return `@param${i}`;
|
||
});
|
||
const result = await request.query(replacedSql);
|
||
return result.recordset || [];
|
||
}
|
||
function isAlreadyExists(e) {
|
||
const m = (e.message || '').toLowerCase();
|
||
return m.includes('already exists') || m.includes('duplicate') || m.includes('existing object') || m.includes('there is already an object');
|
||
}
|
||
|
||
async function bootstrap() {
|
||
console.log('[RM-OVG-SETTINGS] Checking table', TABLE, '...');
|
||
await exec(`
|
||
CREATE TABLE ${TABLE} (
|
||
ID INT IDENTITY(1,1) PRIMARY KEY,
|
||
ITEM_CODE NVARCHAR(60) NULL,
|
||
ITEM_GROUP INT NULL,
|
||
OVG_PERCENT FLOAT NOT NULL,
|
||
CREATED_BY NVARCHAR(50),
|
||
CREATED_AT DATETIME2,
|
||
UPDATED_BY NVARCHAR(50),
|
||
UPDATED_AT DATETIME2
|
||
)
|
||
`).catch(e => {
|
||
if (isAlreadyExists(e)) { console.log('[RM-OVG-SETTINGS] Table exists — OK'); }
|
||
else throw e;
|
||
});
|
||
console.log('[RM-OVG-SETTINGS] ✅ Ready');
|
||
}
|
||
|
||
function fromRow(row) {
|
||
return {
|
||
id: row.ID,
|
||
itemCode: row.ITEM_CODE || '',
|
||
itemGroup: row.ITEM_GROUP != null ? row.ITEM_GROUP : null,
|
||
ovgPercent: row.OVG_PERCENT,
|
||
createdBy: row.CREATED_BY || '',
|
||
createdAt: row.CREATED_AT ? new Date(row.CREATED_AT).toISOString() : null,
|
||
updatedBy: row.UPDATED_BY || '',
|
||
updatedAt: row.UPDATED_AT ? new Date(row.UPDATED_AT).toISOString() : null,
|
||
};
|
||
}
|
||
|
||
async function listAll() {
|
||
const rows = await exec(`SELECT * FROM ${TABLE} ORDER BY ITEM_CODE, ITEM_GROUP`);
|
||
return rows.map(fromRow);
|
||
}
|
||
|
||
async function create({ itemCode, itemGroup, ovgPercent, by }) {
|
||
const code = (itemCode || '').trim().toUpperCase() || null;
|
||
const group = itemGroup != null && itemGroup !== '' ? parseInt(itemGroup) : null;
|
||
if (!code && group == null) throw new Error('Provide an Item Code and/or Item Group');
|
||
const pct = parseFloat(ovgPercent);
|
||
if (isNaN(pct)) throw new Error('Ovg% must be a number');
|
||
const now = new Date().toISOString();
|
||
const idRows = await exec(`
|
||
INSERT INTO ${TABLE} (ITEM_CODE, ITEM_GROUP, OVG_PERCENT, CREATED_BY, CREATED_AT, UPDATED_BY, UPDATED_AT)
|
||
VALUES (?,?,?,?,?,?,?);
|
||
SELECT SCOPE_IDENTITY() AS ID;
|
||
`, [code, group, pct, by || '', now.replace('T', ' ').substring(0, 23), by || '', now.replace('T', ' ').substring(0, 23)]);
|
||
const rows = await exec(`SELECT * FROM ${TABLE} WHERE ID = ?`, [idRows[0].ID]);
|
||
return fromRow(rows[0]);
|
||
}
|
||
|
||
async function update(id, { itemCode, itemGroup, ovgPercent, by }) {
|
||
const code = (itemCode || '').trim().toUpperCase() || null;
|
||
const group = itemGroup != null && itemGroup !== '' ? parseInt(itemGroup) : null;
|
||
if (!code && group == null) throw new Error('Provide an Item Code and/or Item Group');
|
||
const pct = parseFloat(ovgPercent);
|
||
if (isNaN(pct)) throw new Error('Ovg% must be a number');
|
||
const now = new Date().toISOString().replace('T', ' ').substring(0, 23);
|
||
await exec(`UPDATE ${TABLE} SET ITEM_CODE=?, ITEM_GROUP=?, OVG_PERCENT=?, UPDATED_BY=?, UPDATED_AT=? WHERE ID=?`,
|
||
[code, group, pct, by || '', now, parseInt(id)]);
|
||
const rows = await exec(`SELECT * FROM ${TABLE} WHERE ID = ?`, [parseInt(id)]);
|
||
if (!rows.length) throw new Error('Not found');
|
||
return fromRow(rows[0]);
|
||
}
|
||
|
||
async function remove(id) {
|
||
await exec(`DELETE FROM ${TABLE} WHERE ID = ?`, [parseInt(id)]);
|
||
}
|
||
|
||
// Resolves an Ovg% for each item code, given that item's own SAP item group
|
||
// (caller looks the group up from OITM — this store has no SAP connection of
|
||
// its own). Item-code-specific rows win over item-group rows for the same
|
||
// item. Returns { itemCode: percent } — items with no match are simply
|
||
// absent from the result (never a 0 entry, since 0 IS a valid configured %).
|
||
async function resolveForItems(itemCodesUpper, groupByItemCode) {
|
||
const rows = await listAll();
|
||
const byCode = {};
|
||
const byGroup = {};
|
||
rows.forEach(r => {
|
||
if (r.itemCode) byCode[r.itemCode] = r.ovgPercent;
|
||
else if (r.itemGroup != null) byGroup[r.itemGroup] = r.ovgPercent;
|
||
});
|
||
const out = {};
|
||
itemCodesUpper.forEach(code => {
|
||
if (byCode[code] != null) { out[code] = byCode[code]; return; }
|
||
const grp = groupByItemCode[code];
|
||
if (grp != null && byGroup[grp] != null) out[code] = byGroup[grp];
|
||
});
|
||
return out;
|
||
}
|
||
|
||
module.exports = { bootstrap, listAll, create, update, remove, resolveForItems };
|