import fix

This commit is contained in:
2026-05-06 11:14:10 +07:00
parent 197186eac8
commit 8b2a9d7afe
4 changed files with 264 additions and 57 deletions

View File

@@ -329,8 +329,29 @@ function parsePositiveInteger(value, fallback = 1) {
} }
function parseNonNegativeInteger(value, fallback = 0) { function parseNonNegativeInteger(value, fallback = 0) {
const parsed = Number(value); const parsed = parseAssetImportNumericValue(value, Number.NaN);
if (Number.isInteger(parsed) && parsed >= 0) { if (Number.isFinite(parsed) && parsed >= 0) {
return Math.floor(parsed);
}
return fallback;
}
function parseOptionalNonNegativeInteger(value) {
if (value === undefined || value === null || String(value).trim() === '') {
return null;
}
const parsed = parseAssetImportNumericValue(value, Number.NaN);
if (Number.isFinite(parsed) && parsed >= 0) {
return Math.floor(parsed);
}
return null;
}
function parseNonNegativeIntegerOrFallback(value, fallback = 0) {
const parsed = parseOptionalNonNegativeInteger(value);
if (parsed !== null) {
return parsed; return parsed;
} }
return fallback; return fallback;
@@ -406,8 +427,11 @@ function normalizeAssetStatus(value) {
} }
function normalizeAssetPayload(payload = {}) { function normalizeAssetPayload(payload = {}) {
const quantity = parseNonNegativeInteger(payload.quantity, 0); const quantity = parseNonNegativeIntegerOrFallback(payload.quantity, 0);
const endingBalance = parseNonNegativeInteger(payload.endingBalance, quantity); const importInPeriod = parseNonNegativeIntegerOrFallback(payload.importInPeriod, 0);
const exportInPeriod = parseNonNegativeIntegerOrFallback(payload.exportInPeriod, 0);
const providedEndingBalance = parseOptionalNonNegativeInteger(payload.endingBalance);
const endingBalance = providedEndingBalance !== null ? providedEndingBalance : 0;
return { return {
assetCode: String(payload.assetCode || '').trim(), assetCode: String(payload.assetCode || '').trim(),
@@ -418,8 +442,8 @@ function normalizeAssetPayload(payload = {}) {
unit: String(payload.unit || '').trim() || null, unit: String(payload.unit || '').trim() || null,
department: String(payload.department || '').trim() || null, department: String(payload.department || '').trim() || null,
project: String(payload.project || '').trim() || null, project: String(payload.project || '').trim() || null,
importInPeriod: parseNonNegativeInteger(payload.importInPeriod, 0), importInPeriod,
exportInPeriod: parseNonNegativeInteger(payload.exportInPeriod, 0), exportInPeriod,
endingBalance, endingBalance,
location: String(payload.location || '').trim() || null, location: String(payload.location || '').trim() || null,
custodian: String(payload.custodian || '').trim() || null, custodian: String(payload.custodian || '').trim() || null,
@@ -648,6 +672,28 @@ function isHeaderLikeAssetImportRow(row = {}) {
return headerLikeCount >= 2; return headerLikeCount >= 2;
} }
function isMeaningfulImportedAssetRow(row = {}) {
const assetName = String(row.assetName || '').trim();
if (!assetName) {
return false;
}
return [
row.assetCode,
row.assetName,
row.model,
row.unit,
row.location,
row.department,
row.project,
row.notes,
row.quantity,
row.importInPeriod,
row.exportInPeriod,
row.endingBalance
].some(value => String(value ?? '').trim() !== '');
}
const ASSET_IMPORT_ALIASES = { const ASSET_IMPORT_ALIASES = {
stt: ['STT', 'So thu tu'], stt: ['STT', 'So thu tu'],
assetCode: ['Asset Code', 'Ma tai san', 'Ma TS', 'Ma TSCD', 'Ma vat tu', 'Ma VT', 'Ma linh kien', 'Code', 'SKU', 'Part Number', 'PN', 'So the', 'So hieu', 'Ma tai san/CCDC'], assetCode: ['Asset Code', 'Ma tai san', 'Ma TS', 'Ma TSCD', 'Ma vat tu', 'Ma VT', 'Ma linh kien', 'Code', 'SKU', 'Part Number', 'PN', 'So the', 'So hieu', 'Ma tai san/CCDC'],
@@ -815,6 +861,7 @@ function parseAssetImportRowsByHeaderMap(matrixRows) {
); );
const mapped = { const mapped = {
sourceStt: parseAssetImportSttNumber(pick(row, bestFieldMap.stt)),
assetCode: String(pick(row, bestFieldMap.assetCode)).trim(), assetCode: String(pick(row, bestFieldMap.assetCode)).trim(),
assetName: String(pick(row, bestFieldMap.assetName)).trim(), assetName: String(pick(row, bestFieldMap.assetName)).trim(),
model: String(pick(row, bestFieldMap.model)).trim(), model: String(pick(row, bestFieldMap.model)).trim(),
@@ -844,7 +891,7 @@ function parseAssetImportRowsByHeaderMap(matrixRows) {
}) })
.filter(Boolean) .filter(Boolean)
.filter(row => !isHeaderLikeAssetImportRow(row)) .filter(row => !isHeaderLikeAssetImportRow(row))
.filter(row => row.assetCode && row.assetName); .filter(row => isMeaningfulImportedAssetRow(row));
return parsed; return parsed;
} }
@@ -880,12 +927,46 @@ function inferAssetImportColumnIndex(headerRow, aliases = []) {
return -1; return -1;
} }
function parseAssetImportNumericValue(value, fallback = 1) { function parseAssetImportNumericValue(value, fallback = 0) {
if (value === undefined || value === null || value === '') { if (value === undefined || value === null || value === '') {
return fallback; return fallback;
} }
const normalized = String(value).trim().replace(/,/g, ''); const raw = String(value).trim();
if (!raw) {
return fallback;
}
const compact = raw.replace(/\s+/g, '');
let normalized = compact;
const hasDot = compact.includes('.');
const hasComma = compact.includes(',');
// vi-VN style: 1.234,56 or 1.234
if (hasDot && hasComma) {
if (/^-?\d{1,3}(\.\d{3})+(,\d+)?$/.test(compact)) {
normalized = compact.replace(/\./g, '').replace(',', '.');
} else if (/^-?\d{1,3}(,\d{3})+(\.\d+)?$/.test(compact)) {
// en-US style: 1,234.56
normalized = compact.replace(/,/g, '');
} else {
normalized = compact.replace(/,/g, '');
}
} else if (hasDot) {
if (/^-?\d{1,3}(\.\d{3})+$/.test(compact)) {
normalized = compact.replace(/\./g, '');
}
} else if (hasComma) {
if (/^-?\d{1,3}(,\d{3})+$/.test(compact)) {
normalized = compact.replace(/,/g, '');
} else if (/^-?\d+,\d+$/.test(compact)) {
normalized = compact.replace(',', '.');
} else {
normalized = compact.replace(/,/g, '');
}
}
if (!normalized) { if (!normalized) {
return fallback; return fallback;
} }
@@ -929,7 +1010,9 @@ function generateImportAssetCodeFromRow(mapped, rowNumber = 0) {
const fromSerial = sanitizeAssetCodeToken(mapped.serialNumber); const fromSerial = sanitizeAssetCodeToken(mapped.serialNumber);
const fromName = sanitizeAssetCodeToken(mapped.assetName); const fromName = sanitizeAssetCodeToken(mapped.assetName);
const base = fromModel || fromSerial || fromName || 'ASSET'; const base = fromModel || fromSerial || fromName || 'ASSET';
const suffix = String(rowNumber || 0).padStart(4, '0'); const sttNumber = parseAssetImportSttNumber(mapped?.sourceStt);
const suffixSeed = sttNumber || rowNumber || 0;
const suffix = String(suffixSeed).padStart(4, '0');
return `IMP-${base}-${suffix}`; return `IMP-${base}-${suffix}`;
} }
@@ -1058,6 +1141,7 @@ function mapAssetImportMatrixRowsByIndex(matrixRows, headerRowIndex) {
); );
const mapped = { const mapped = {
sourceStt: sttValue,
assetCode: String(pick(row, indexMap.assetCode)).trim(), assetCode: String(pick(row, indexMap.assetCode)).trim(),
assetName: String(pick(row, indexMap.assetName)).trim(), assetName: String(pick(row, indexMap.assetName)).trim(),
model: String(pick(row, indexMap.model)).trim(), model: String(pick(row, indexMap.model)).trim(),
@@ -1081,7 +1165,7 @@ function mapAssetImportMatrixRowsByIndex(matrixRows, headerRowIndex) {
}) })
.filter(Boolean) .filter(Boolean)
.filter(row => !isHeaderLikeAssetImportRow(row)) .filter(row => !isHeaderLikeAssetImportRow(row))
.filter(row => row.assetCode && row.assetName); .filter(row => isMeaningfulImportedAssetRow(row));
} }
function parseAssetImportRowsFromMatrix(matrixRows) { function parseAssetImportRowsFromMatrix(matrixRows) {
@@ -1149,8 +1233,10 @@ function parseAssetImportRowsFromMatrix(matrixRows) {
return sttRows return sttRows
.map((row, idx) => { .map((row, idx) => {
const sttValue = parseAssetImportSttNumber(row[detectedSttCol]);
const endingBalance = parseAssetImportNumericValue(row[detectedSttCol + 9] ?? '', 0); const endingBalance = parseAssetImportNumericValue(row[detectedSttCol + 9] ?? '', 0);
const mapped = { const mapped = {
sourceStt: sttValue,
assetCode: String(row[detectedSttCol + 2] ?? '').trim() || String(row[detectedSttCol + 4] ?? '').trim(), assetCode: String(row[detectedSttCol + 2] ?? '').trim() || String(row[detectedSttCol + 4] ?? '').trim(),
assetName: String(row[detectedSttCol + 3] ?? '').trim() || String(row[detectedSttCol + 2] ?? '').trim() || String(row[detectedSttCol + 4] ?? '').trim(), assetName: String(row[detectedSttCol + 3] ?? '').trim() || String(row[detectedSttCol + 2] ?? '').trim() || String(row[detectedSttCol + 4] ?? '').trim(),
model: String(row[detectedSttCol + 4] ?? '').trim(), model: String(row[detectedSttCol + 4] ?? '').trim(),
@@ -1173,7 +1259,7 @@ function parseAssetImportRowsFromMatrix(matrixRows) {
return finalizeImportedAssetPayload(mapped, idx + 2); return finalizeImportedAssetPayload(mapped, idx + 2);
}) })
.filter(row => !isHeaderLikeAssetImportRow(row)) .filter(row => !isHeaderLikeAssetImportRow(row))
.filter(row => row.assetCode && row.assetName); .filter(row => isMeaningfulImportedAssetRow(row));
} }
function detectLikelySttColumn(matrixRows) { function detectLikelySttColumn(matrixRows) {
@@ -1248,8 +1334,10 @@ function parseAssetImportRowsLoose(matrixRows) {
return dataRows return dataRows
.map((row, idx) => { .map((row, idx) => {
const sttValue = parseAssetImportSttNumber(row[sttCol]);
const endingBalance = parseAssetImportNumericValue(row[sttCol + 9] ?? '', 0); const endingBalance = parseAssetImportNumericValue(row[sttCol + 9] ?? '', 0);
const mapped = { const mapped = {
sourceStt: sttValue,
assetCode: String(row[sttCol + 2] ?? '').trim() || String(row[sttCol + 4] ?? '').trim(), assetCode: String(row[sttCol + 2] ?? '').trim() || String(row[sttCol + 4] ?? '').trim(),
assetName: String(row[sttCol + 3] ?? '').trim() || String(row[sttCol + 2] ?? '').trim() || String(row[sttCol + 4] ?? '').trim(), assetName: String(row[sttCol + 3] ?? '').trim() || String(row[sttCol + 2] ?? '').trim() || String(row[sttCol + 4] ?? '').trim(),
model: String(row[sttCol + 4] ?? '').trim(), model: String(row[sttCol + 4] ?? '').trim(),
@@ -1272,7 +1360,7 @@ function parseAssetImportRowsLoose(matrixRows) {
return finalizeImportedAssetPayload(mapped, idx + 2); return finalizeImportedAssetPayload(mapped, idx + 2);
}) })
.filter(row => !isHeaderLikeAssetImportRow(row)) .filter(row => !isHeaderLikeAssetImportRow(row))
.filter(row => row.assetCode && row.assetName); .filter(row => isMeaningfulImportedAssetRow(row));
} }
function parseAssetImportRows(matrixRows) { function parseAssetImportRows(matrixRows) {
@@ -1295,11 +1383,53 @@ function countNonEmptyMatrixRows(matrixRows) {
).length; ).length;
} }
async function ensureUniqueImportAssetCode(transaction, initialCode, maxAttempts = 12) {
let candidate = String(initialCode || '').trim();
if (!candidate) {
candidate = generateManualAssetCodeFromPayload({});
}
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const existed = await new sql.Request(transaction)
.input('assetCode', sql.NVarChar, candidate)
.query(`
SELECT TOP 1 AssetId
FROM AssetInventory
WHERE AssetCode = @assetCode
`);
if (existed.recordset.length === 0) {
return candidate;
}
const suffix = String(attempt + 1).padStart(2, '0');
candidate = `${String(initialCode || 'IMP-ASSET').slice(0, 90)}-${suffix}`;
}
return generateManualAssetCodeFromPayload({ assetName: initialCode || 'IMPORT' });
}
function scoreAssetImportSheetName(sheetName = '') {
const token = normalizeImportToken(sheetName);
if (!token) {
return 0;
}
let score = 0;
if (token.includes('xuatnhapton')) score += 120;
if (token.includes('kho')) score += 60;
if (token.includes('robotics')) score += 40;
if (token.includes('inventory')) score += 40;
if (token.includes('asset')) score += 30;
return score;
}
function parseAssetImportRowsFromWorkbook(workbook) { function parseAssetImportRowsFromWorkbook(workbook) {
const sheetNames = Array.isArray(workbook?.SheetNames) ? workbook.SheetNames : []; const sheetNames = Array.isArray(workbook?.SheetNames) ? workbook.SheetNames : [];
let bestRows = []; let bestRows = [];
let bestSheetName = ''; let bestSheetName = '';
let bestNonEmptyRows = 0; let bestNonEmptyRows = 0;
let bestSheetPriority = 0;
const diagnostics = []; const diagnostics = [];
for (const sheetName of sheetNames) { for (const sheetName of sheetNames) {
@@ -1316,16 +1446,23 @@ function parseAssetImportRowsFromWorkbook(workbook) {
const parsedRows = parseAssetImportRows(matrixRows); const parsedRows = parseAssetImportRows(matrixRows);
const nonEmptyRows = countNonEmptyMatrixRows(matrixRows); const nonEmptyRows = countNonEmptyMatrixRows(matrixRows);
const sheetPriority = scoreAssetImportSheetName(sheetName);
diagnostics.push({ diagnostics.push({
sheetName, sheetName,
parsedRows: parsedRows.length, parsedRows: parsedRows.length,
nonEmptyRows nonEmptyRows,
sheetPriority
}); });
if (parsedRows.length > bestRows.length || (parsedRows.length === bestRows.length && nonEmptyRows > bestNonEmptyRows)) { if (
(sheetPriority > bestSheetPriority && parsedRows.length > 0)
|| (sheetPriority === bestSheetPriority && parsedRows.length > bestRows.length)
|| (sheetPriority === bestSheetPriority && parsedRows.length === bestRows.length && nonEmptyRows > bestNonEmptyRows)
) {
bestRows = parsedRows; bestRows = parsedRows;
bestSheetName = sheetName; bestSheetName = sheetName;
bestNonEmptyRows = nonEmptyRows; bestNonEmptyRows = nonEmptyRows;
bestSheetPriority = sheetPriority;
} }
} }
@@ -4006,16 +4143,18 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
const normalizedRows = incomingRows const normalizedRows = incomingRows
.map((row, rowIndex) => { .map((row, rowIndex) => {
const normalized = normalizeAssetPayload(row); const normalized = normalizeAssetPayload(row);
if (!normalized.assetCode && normalized.assetName) { const hasOriginalAssetCode = String(normalized.assetCode || '').trim() !== '';
if (!hasOriginalAssetCode && normalized.assetName) {
normalized.assetCode = generateImportAssetCodeFromRow(normalized, rowIndex + 1); normalized.assetCode = generateImportAssetCodeFromRow(normalized, rowIndex + 1);
} }
normalized.__hasOriginalAssetCode = hasOriginalAssetCode;
return normalized; return normalized;
}) })
.filter(row => !isHeaderLikeAssetImportRow(row)) .filter(row => !isHeaderLikeAssetImportRow(row))
.filter(row => row.assetCode && row.assetName); .filter(row => isMeaningfulImportedAssetRow(row));
if (!normalizedRows.length) { if (!normalizedRows.length) {
return res.status(400).json({ success: false, message: 'No valid rows found. Asset code and name are required.' }); return res.status(400).json({ success: false, message: 'No valid rows found. Asset name or model is required.' });
} }
const transaction = new sql.Transaction(pool); const transaction = new sql.Transaction(pool);
@@ -4026,6 +4165,48 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
await transaction.begin(); await transaction.begin();
for (const row of normalizedRows) { for (const row of normalizedRows) {
if (!row.__hasOriginalAssetCode) {
const uniqueImportCode = await ensureUniqueImportAssetCode(transaction, row.assetCode);
row.assetCode = uniqueImportCode;
await new sql.Request(transaction)
.input('assetCode', sql.NVarChar, row.assetCode)
.input('assetName', sql.NVarChar, row.assetName)
.input('model', sql.NVarChar, row.model)
.input('serialNumber', sql.NVarChar, row.serialNumber)
.input('quantity', sql.Int, row.quantity)
.input('importInPeriod', sql.Int, row.importInPeriod)
.input('exportInPeriod', sql.Int, row.exportInPeriod)
.input('endingBalance', sql.Int, row.endingBalance)
.input('unit', sql.NVarChar, row.unit)
.input('department', sql.NVarChar, row.department)
.input('project', sql.NVarChar, row.project)
.input('location', sql.NVarChar, row.location)
.input('custodian', sql.NVarChar, row.custodian)
.input('borrower', sql.NVarChar, row.borrower)
.input('exportedBy', sql.NVarChar, exportedBy)
.input('purchaseDate', sql.Date, row.purchaseDate)
.input('purchasePrice', sql.Decimal(18, 2), row.purchasePrice)
.input('status', sql.NVarChar, row.status)
.input('notes', sql.NVarChar, row.notes)
.input('createdBy', sql.Int, createdBy)
.query(`
INSERT INTO AssetInventory (
AssetCode, AssetName, Model, SerialNumber,
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance,
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy
)
VALUES (
@assetCode, @assetName, @model, @serialNumber,
@quantity, @importInPeriod, @exportInPeriod, @endingBalance,
@unit, @department, @project, @location, @custodian, @borrower, @exportedBy,
@purchaseDate, @purchasePrice, @status, @notes, @createdBy
);
`);
inserted += 1;
continue;
}
const mergeResult = await new sql.Request(transaction) const mergeResult = await new sql.Request(transaction)
.input('assetCode', sql.NVarChar, row.assetCode) .input('assetCode', sql.NVarChar, row.assetCode)
.input('assetName', sql.NVarChar, row.assetName) .input('assetName', sql.NVarChar, row.assetName)
@@ -4089,11 +4270,8 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
`); `);
const mergeAction = String(mergeResult.recordset?.[0]?.MergeAction || '').toUpperCase(); const mergeAction = String(mergeResult.recordset?.[0]?.MergeAction || '').toUpperCase();
if (mergeAction === 'INSERT') { if (mergeAction === 'INSERT') inserted += 1;
inserted += 1; if (mergeAction === 'UPDATE') updated += 1;
} else if (mergeAction === 'UPDATE') {
updated += 1;
}
} }
await transaction.commit(); await transaction.commit();

View File

@@ -1637,6 +1637,19 @@ class AccountManager {
return parsed; return parsed;
} }
parseOptionalNonNegativeInteger(value) {
if (value === null || value === undefined || String(value).trim() === '') {
return null;
}
const parsed = Number.parseInt(String(value).replace(/,/g, '').trim(), 10);
if (!Number.isFinite(parsed) || parsed < 0) {
return null;
}
return parsed;
}
escapeHtml(value) { escapeHtml(value) {
return String(value || '') return String(value || '')
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
@@ -1850,17 +1863,26 @@ class AccountManager {
const borrowerEntries = Array.isArray(borrowerEntriesOverride) const borrowerEntries = Array.isArray(borrowerEntriesOverride)
? this.parseBorrowerEntries(borrowerEntriesOverride) ? this.parseBorrowerEntries(borrowerEntriesOverride)
: this.parseBorrowerEntries(asset?.Borrower ?? asset?.borrower); : this.parseBorrowerEntries(asset?.Borrower ?? asset?.borrower);
const exportInPeriod = borrowerEntries.reduce((sum, entry) => ( const borrowerExportInPeriod = borrowerEntries.reduce((sum, entry) => (
sum + this.parseNonNegativeInteger(entry?.quantity, 0) sum + this.parseNonNegativeInteger(entry?.quantity, 0)
), 0); ), 0);
const endingBalance = Math.max(quantity + importInPeriod - exportInPeriod, 0); const storedExportInPeriod = this.parseOptionalNonNegativeInteger(asset?.ExportInPeriod ?? asset?.exportInPeriod);
const exportInPeriod = storedExportInPeriod !== null
? storedExportInPeriod
: borrowerExportInPeriod;
const computedEndingBalance = Math.max(quantity + importInPeriod - exportInPeriod, 0);
const storedEndingBalance = this.parseOptionalNonNegativeInteger(asset?.EndingBalance ?? asset?.endingBalance);
const endingBalance = storedEndingBalance !== null
? storedEndingBalance
: computedEndingBalance;
return { return {
quantity, quantity,
importInPeriod, importInPeriod,
exportInPeriod, exportInPeriod,
endingBalance, endingBalance,
borrowerEntries borrowerEntries,
borrowerExportInPeriod
}; };
} }
@@ -1890,19 +1912,16 @@ class AccountManager {
return; return;
} }
const metrics = this.buildAssetQuantityMetrics( const quantity = this.parseNonNegativeInteger(quantityInput.value, 0);
{ const importInPeriod = this.parseNonNegativeInteger(importInput.value, 0);
Quantity: quantityInput.value, const exportInPeriod = this.parseNonNegativeInteger(exportInput?.value ?? 0, 0);
ImportInPeriod: importInput.value const endingBalance = Math.max(quantity + importInPeriod - exportInPeriod, 0);
},
this.editingAssetBorrowerEntries
);
if (exportInput) { if (exportInput) {
exportInput.value = String(metrics.exportInPeriod); exportInput.value = String(exportInPeriod);
} }
if (endingInput) { if (endingInput) {
endingInput.value = String(metrics.endingBalance); endingInput.value = String(endingBalance);
} }
} }
@@ -3944,18 +3963,14 @@ class AccountManager {
collectAssetFormPayload() { collectAssetFormPayload() {
const quantity = this.parseNonNegativeInteger(document.getElementById('assetQuantityInput')?.value ?? 0, 0); const quantity = this.parseNonNegativeInteger(document.getElementById('assetQuantityInput')?.value ?? 0, 0);
const importInPeriod = this.parseNonNegativeInteger(document.getElementById('assetImportInPeriodInput')?.value ?? 0, 0); const importInPeriod = this.parseNonNegativeInteger(document.getElementById('assetImportInPeriodInput')?.value ?? 0, 0);
const exportInPeriod = this.parseNonNegativeInteger(document.getElementById('assetExportInPeriodInput')?.value ?? 0, 0);
const endingBalanceInput = this.parseOptionalNonNegativeInteger(document.getElementById('assetEndingBalanceInput')?.value ?? '');
const borrowerEntries = Array.isArray(this.editingAssetBorrowerEntries) const borrowerEntries = Array.isArray(this.editingAssetBorrowerEntries)
? this.editingAssetBorrowerEntries ? this.editingAssetBorrowerEntries
: []; : [];
const borrower = this.formatBorrowerEntries(borrowerEntries, '; ') || null; const borrower = this.formatBorrowerEntries(borrowerEntries, '; ') || null;
const metrics = this.buildAssetQuantityMetrics( const computedEndingBalance = Math.max(quantity + importInPeriod - exportInPeriod, 0);
{ const endingBalance = endingBalanceInput !== null ? endingBalanceInput : computedEndingBalance;
Quantity: quantity,
ImportInPeriod: importInPeriod,
Borrower: borrower
},
borrowerEntries
);
const purchasePrice = String(document.getElementById('assetPriceInput')?.value ?? '').trim(); const purchasePrice = String(document.getElementById('assetPriceInput')?.value ?? '').trim();
@@ -3967,8 +3982,8 @@ class AccountManager {
serialNumber: document.getElementById('assetSerialInput')?.value?.trim() || '', serialNumber: document.getElementById('assetSerialInput')?.value?.trim() || '',
quantity, quantity,
importInPeriod, importInPeriod,
exportInPeriod: metrics.exportInPeriod, exportInPeriod,
endingBalance: metrics.endingBalance, endingBalance,
unit: document.getElementById('assetUnitInput')?.value?.trim() || '', unit: document.getElementById('assetUnitInput')?.value?.trim() || '',
department: document.getElementById('assetDepartmentInput')?.value?.trim() || '', department: document.getElementById('assetDepartmentInput')?.value?.trim() || '',
project: document.getElementById('assetProjectInput')?.value?.trim() || '', project: document.getElementById('assetProjectInput')?.value?.trim() || '',
@@ -3986,18 +4001,32 @@ class AccountManager {
return null; return null;
} }
const quantity = this.parseNonNegativeInteger(asset?.Quantity, 0);
const importInPeriod = this.parseNonNegativeInteger(asset?.ImportInPeriod, 0);
const baseExportInPeriod = this.parseNonNegativeInteger(asset?.ExportInPeriod, 0);
const baseEndingBalance = this.parseOptionalNonNegativeInteger(asset?.EndingBalance);
const resolvedBaseEndingBalance = baseEndingBalance !== null
? baseEndingBalance
: Math.max(quantity + importInPeriod - baseExportInPeriod, 0);
const borrowerEntries = Array.isArray(borrowerEntriesOverride) const borrowerEntries = Array.isArray(borrowerEntriesOverride)
? borrowerEntriesOverride ? borrowerEntriesOverride
: this.parseBorrowerEntries(asset?.Borrower); : this.parseBorrowerEntries(asset?.Borrower);
const borrower = this.formatBorrowerEntries(borrowerEntries, '; ') || null; const borrower = this.formatBorrowerEntries(borrowerEntries, '; ') || null;
const metrics = this.buildAssetQuantityMetrics(
{ let exportInPeriod = baseExportInPeriod;
Quantity: asset?.Quantity, let endingBalance = resolvedBaseEndingBalance;
ImportInPeriod: asset?.ImportInPeriod, if (Array.isArray(borrowerEntriesOverride)) {
Borrower: borrower const existingBorrowerEntries = this.parseBorrowerEntries(asset?.Borrower);
}, const previousBorrowerExport = existingBorrowerEntries.reduce((sum, entry) => (
borrowerEntries sum + this.parseNonNegativeInteger(entry?.quantity, 0)
); ), 0);
const nextBorrowerExport = this.parseBorrowerEntries(borrowerEntriesOverride).reduce((sum, entry) => (
sum + this.parseNonNegativeInteger(entry?.quantity, 0)
), 0);
const exportDelta = nextBorrowerExport - previousBorrowerExport;
exportInPeriod = Math.max(baseExportInPeriod + exportDelta, 0);
endingBalance = Math.max(resolvedBaseEndingBalance - exportDelta, 0);
}
const rawPrice = asset?.PurchasePrice; const rawPrice = asset?.PurchasePrice;
const normalizedPrice = rawPrice === undefined || rawPrice === null || String(rawPrice).trim() === '' const normalizedPrice = rawPrice === undefined || rawPrice === null || String(rawPrice).trim() === ''
@@ -4010,10 +4039,10 @@ class AccountManager {
status: String(asset?.Status || 'in_use'), status: String(asset?.Status || 'in_use'),
model: String(asset?.Model || '').trim(), model: String(asset?.Model || '').trim(),
serialNumber: String(asset?.SerialNumber || '').trim(), serialNumber: String(asset?.SerialNumber || '').trim(),
quantity: metrics.quantity, quantity,
importInPeriod: metrics.importInPeriod, importInPeriod,
exportInPeriod: metrics.exportInPeriod, exportInPeriod,
endingBalance: metrics.endingBalance, endingBalance,
unit: String(asset?.Unit || '').trim(), unit: String(asset?.Unit || '').trim(),
department: String(asset?.Department || '').trim(), department: String(asset?.Department || '').trim(),
project: String(asset?.Project || '').trim(), project: String(asset?.Project || '').trim(),

BIN
tmp-import-source.xls Normal file

Binary file not shown.

BIN
tmp-user-import.xls Normal file

Binary file not shown.