Automated UOM shot name
This commit is contained in:
parent
9d3c006343
commit
3692b7bf4c
@ -75,15 +75,10 @@ exports.deleteUnit = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
exports.uploadUnitMasterFromCSV = async (req, res) => {
|
exports.uploadUnitMasterFromCSV = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!req.file) {
|
if (!req.file)
|
||||||
return res.status(400).send({
|
return res.status(400).send({ status: "failed", message: "No file uploaded." });
|
||||||
status: "failed",
|
|
||||||
message: "No file uploaded.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = req.file.path;
|
const filePath = req.file.path;
|
||||||
|
|
||||||
@ -96,8 +91,8 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate created_by
|
// Validate user
|
||||||
if (!req.user.id || isNaN(req.user.id)) {
|
if (!req.user?.id || isNaN(req.user.id)) {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
status: "failed",
|
status: "failed",
|
||||||
@ -105,7 +100,7 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if file is empty
|
// Validate file not empty
|
||||||
const stats = fs.statSync(filePath);
|
const stats = fs.statSync(filePath);
|
||||||
if (stats.size === 0) {
|
if (stats.size === 0) {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
@ -116,6 +111,7 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const results = [];
|
const results = [];
|
||||||
|
|
||||||
fs.createReadStream(filePath)
|
fs.createReadStream(filePath)
|
||||||
.pipe(csv())
|
.pipe(csv())
|
||||||
.on("data", (row) => results.push(row))
|
.on("data", (row) => results.push(row))
|
||||||
@ -129,10 +125,9 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const requiredCols = ["Display Name", "Unit Name", "Description"];
|
//Expected columns
|
||||||
|
const requiredCols = ["Unit Name", "Description"];
|
||||||
const headers = Object.keys(results[0] || {});
|
const headers = Object.keys(results[0] || {});
|
||||||
|
|
||||||
//Check for missing or extra columns
|
|
||||||
const missingCols = requiredCols.filter((col) => !headers.includes(col));
|
const missingCols = requiredCols.filter((col) => !headers.includes(col));
|
||||||
const extraCols = headers.filter((col) => !requiredCols.includes(col));
|
const extraCols = headers.filter((col) => !requiredCols.includes(col));
|
||||||
|
|
||||||
@ -142,135 +137,170 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
if (missingCols.length > 0)
|
if (missingCols.length > 0)
|
||||||
msg += `Missing required columns: ${missingCols.join(", ")}. `;
|
msg += `Missing required columns: ${missingCols.join(", ")}. `;
|
||||||
if (extraCols.length > 0)
|
if (extraCols.length > 0)
|
||||||
msg += `Unexpected columns found: ${extraCols.join(", ")}. Only 'Display Name', 'Unit Name', and 'Description' are allowed.`;
|
msg += `Unexpected columns found: ${extraCols.join(
|
||||||
|
", "
|
||||||
|
)}. Only 'Unit Name' and 'Description' are allowed.`;
|
||||||
return res.status(400).send({ status: "failed", message: msg.trim() });
|
return res.status(400).send({ status: "failed", message: msg.trim() });
|
||||||
}
|
}
|
||||||
|
|
||||||
const inserted = [];
|
const inserted = [];
|
||||||
const duplicates = []; // DB duplicates
|
const duplicates = [];
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
//Detect duplicates within uploaded file
|
|
||||||
const seenShortNames = new Set();
|
|
||||||
const seenUnitNames = new Set();
|
const seenUnitNames = new Set();
|
||||||
|
const seenShortNames = new Set();
|
||||||
const fileDuplicates = [];
|
const fileDuplicates = [];
|
||||||
|
|
||||||
|
//generate smart short name (letters + numbers)
|
||||||
|
const generateShortName = (unitName, existingShorts = new Set()) => {
|
||||||
|
const cleaned = unitName.replace(/[^a-zA-Z0-9]/g, "").toUpperCase();
|
||||||
|
|
||||||
|
const abbreviationMap = {
|
||||||
|
METER: "MT",
|
||||||
|
METRE: "MT",
|
||||||
|
KILOGRAM: "KG",
|
||||||
|
GRAM: "GM",
|
||||||
|
LITER: "LTR",
|
||||||
|
LITRE: "LTR",
|
||||||
|
CENTIMETER: "CM",
|
||||||
|
MILLIMETER: "MM",
|
||||||
|
SECOND: "SEC",
|
||||||
|
MINUTE: "MIN",
|
||||||
|
HOUR: "HR",
|
||||||
|
DAY: "DY",
|
||||||
|
PIECE: "PC",
|
||||||
|
BOX: "BX",
|
||||||
|
USER: "USR",
|
||||||
|
ITEM: "ITM",
|
||||||
|
UNIT: "UNT",
|
||||||
|
};
|
||||||
|
|
||||||
|
let shortName = abbreviationMap[cleaned] || cleaned.substring(0, 3);
|
||||||
|
if (shortName.length < 2) shortName = shortName.padEnd(2, "X");
|
||||||
|
|
||||||
|
let finalShort = `${shortName}${Math.floor(Math.random() * 100)}`;
|
||||||
|
while (existingShorts.has(finalShort.toLowerCase())) {
|
||||||
|
finalShort = `${shortName}${Math.floor(Math.random() * 100)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalShort.substring(0, 5);
|
||||||
|
};
|
||||||
|
|
||||||
|
//Validate & prepare records
|
||||||
for (const [index, row] of results.entries()) {
|
for (const [index, row] of results.entries()) {
|
||||||
const uomShort = row["Display Name"]?.trim().toUpperCase();
|
|
||||||
const uom = row["Unit Name"]?.trim();
|
const uom = row["Unit Name"]?.trim();
|
||||||
const description = row["Description"]?.trim();
|
const description = row["Description"]?.trim() || null;
|
||||||
|
|
||||||
if (!uomShort || !uom) {
|
if (!uom) {
|
||||||
errors.push({ row: index + 1, reason: "Missing required fields (Display Name or Unit Name)." });
|
errors.push({ row: index + 1, reason: "Missing 'Unit Name'." });
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!/^[A-Z]+$/i.test(uomShort)) {
|
|
||||||
errors.push({ row: index + 1, reason: "Display Name must contain only alphabetic characters (A–Z)." });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uomShort.length > 5) {
|
|
||||||
errors.push({ row: index + 1, reason: "Display Name exceeds 5 characters." });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shortKey = uomShort.toLowerCase();
|
|
||||||
const uomKey = uom.toLowerCase();
|
const uomKey = uom.toLowerCase();
|
||||||
|
if (seenUnitNames.has(uomKey)) {
|
||||||
if (seenShortNames.has(shortKey) || seenUnitNames.has(uomKey)) {
|
fileDuplicates.push({
|
||||||
fileDuplicates.push({ row: index + 1, uom_short_name: uomShort, uom });
|
row: index + 1,
|
||||||
|
reason: "Duplicate Unit Name in file.",
|
||||||
|
uom,
|
||||||
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
seenShortNames.add(shortKey);
|
let uomShort = generateShortName(uom, seenShortNames);
|
||||||
|
|
||||||
seenUnitNames.add(uomKey);
|
seenUnitNames.add(uomKey);
|
||||||
|
seenShortNames.add(uomShort.toLowerCase());
|
||||||
|
results[index]._generatedShort = uomShort;
|
||||||
|
results[index]._description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop if file has duplicate rows
|
// Stop if file has internal duplicates
|
||||||
if (fileDuplicates.length > 0) {
|
if (fileDuplicates.length > 0) {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
status: "failed",
|
status: "failed",
|
||||||
message: "Duplicate entries found within uploaded file.",
|
message: "Duplicate Unit Names found within uploaded file.",
|
||||||
duplicate_rows: fileDuplicates,
|
duplicate_rows: fileDuplicates,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each row and check against DB
|
//Insert into DB with safe duplicate handling
|
||||||
for (const [index, row] of results.entries()) {
|
for (const [index, row] of results.entries()) {
|
||||||
try {
|
try {
|
||||||
const uomShort = row["Display Name"]?.trim().toUpperCase();
|
|
||||||
const uom = row["Unit Name"]?.trim();
|
const uom = row["Unit Name"]?.trim();
|
||||||
const description = row["Description"]?.trim();
|
let uomShort = row._generatedShort;
|
||||||
|
const description = row._description;
|
||||||
|
|
||||||
if (!uomShort || !uom) {
|
|
||||||
errors.push({ row: index + 1, reason: "Missing required fields (Display Name or Unit Name)." });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uomShort.length > 5) {
|
|
||||||
errors.push({ row: index + 1, reason: "Display Name exceeds 5 characters." });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Check for existing record (duplicate) in DB
|
|
||||||
const existing = await UnitMaster.findOne({
|
const existing = await UnitMaster.findOne({
|
||||||
where: {
|
where: {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
Sequelize.where(
|
|
||||||
Sequelize.fn("LOWER", Sequelize.col("uom_short_name")),
|
|
||||||
uomShort.toLowerCase()
|
|
||||||
),
|
|
||||||
Sequelize.where(
|
Sequelize.where(
|
||||||
Sequelize.fn("LOWER", Sequelize.col("uom")),
|
Sequelize.fn("LOWER", Sequelize.col("uom")),
|
||||||
uom.toLowerCase()
|
uom.toLowerCase()
|
||||||
),
|
),
|
||||||
|
Sequelize.where(
|
||||||
|
Sequelize.fn("LOWER", Sequelize.col("uom_short_name")),
|
||||||
|
uomShort.toLowerCase()
|
||||||
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
duplicates.push({
|
// Skip if same Unit Name already exists
|
||||||
id: existing.id,
|
if (existing.uom.toLowerCase() === uom.toLowerCase()) {
|
||||||
uom_short_name: existing.uom_short_name,
|
duplicates.push({
|
||||||
uom: existing.uom,
|
id: existing.id,
|
||||||
});
|
uom: existing.uom,
|
||||||
continue;
|
uom_short_name: existing.uom_short_name,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Safe regeneration using ternary inside while loop
|
||||||
|
let newShort = uomShort;
|
||||||
|
let existsInDb;
|
||||||
|
|
||||||
|
do {
|
||||||
|
existsInDb = await UnitMaster.findOne({
|
||||||
|
where: Sequelize.where(
|
||||||
|
Sequelize.fn("LOWER", Sequelize.col("uom_short_name")),
|
||||||
|
newShort.toLowerCase()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
newShort = existsInDb
|
||||||
|
? `${uomShort}${Math.floor(Math.random() * 1000)}`.substring(0, 5)
|
||||||
|
: newShort;
|
||||||
|
} while (existsInDb);
|
||||||
|
|
||||||
|
uomShort = newShort;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert record
|
|
||||||
const newUnit = await UnitMaster.create({
|
const newUnit = await UnitMaster.create({
|
||||||
uom_short_name: uomShort,
|
uom_short_name: uomShort,
|
||||||
uom: uom,
|
uom,
|
||||||
description: description || null,
|
description,
|
||||||
created_by: parseInt(req.user.id),
|
created_by: parseInt(req.user.id),
|
||||||
created_at: new Date(),
|
created_at: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
inserted.push(newUnit);
|
inserted.push(newUnit);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errors.push({
|
errors.push({ row: index + 1, reason: err.message });
|
||||||
row: index + 1,
|
|
||||||
reason: err.message,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup file
|
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
|
|
||||||
// Final response summary
|
|
||||||
let finalStatus = "success";
|
let finalStatus = "success";
|
||||||
let message = `${inserted.length} units inserted successfully.`;
|
let message = `${inserted.length} units inserted successfully.`;
|
||||||
|
|
||||||
if (errors.length > 0 || duplicates.length > 0) {
|
if (errors.length > 0 || duplicates.length > 0) {
|
||||||
finalStatus = inserted.length > 0 ? "partial_success" : "failed";
|
finalStatus = inserted.length > 0 ? "partial_success" : "failed";
|
||||||
if (finalStatus === "partial_success") {
|
message =
|
||||||
message = `${inserted.length} units inserted, ${duplicates.length} duplicates skipped, ${errors.length} validation errors found.`;
|
finalStatus === "partial_success"
|
||||||
} else {
|
? `${inserted.length} inserted, ${duplicates.length} duplicates skipped, ${errors.length} errors found.`
|
||||||
message = `No units imported. ${duplicates.length} duplicates and ${errors.length} validation errors found.`;
|
: `No units imported. ${duplicates.length} duplicates and ${errors.length} errors found.`;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
@ -282,7 +312,7 @@ exports.uploadUnitMasterFromCSV = async (req, res) => {
|
|||||||
skipped: duplicates.length,
|
skipped: duplicates.length,
|
||||||
errors: errors.length,
|
errors: errors.length,
|
||||||
},
|
},
|
||||||
duplicates,
|
duplicates,
|
||||||
error_details: errors,
|
error_details: errors,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user