trạng thái
This commit is contained in:
@@ -407,6 +407,10 @@ function parseNullableDate(value) {
|
|||||||
function normalizeAssetStatus(value) {
|
function normalizeAssetStatus(value) {
|
||||||
const normalized = String(value || '').trim().toLowerCase();
|
const normalized = String(value || '').trim().toLowerCase();
|
||||||
|
|
||||||
|
if (['exported', 'da xuat'].includes(normalized)) {
|
||||||
|
return 'exported';
|
||||||
|
}
|
||||||
|
|
||||||
if (['in_use', 'in use', 'dang su dung', 'active'].includes(normalized)) {
|
if (['in_use', 'in use', 'dang su dung', 'active'].includes(normalized)) {
|
||||||
return 'in_use';
|
return 'in_use';
|
||||||
}
|
}
|
||||||
@@ -426,12 +430,61 @@ function normalizeAssetStatus(value) {
|
|||||||
return 'in_use';
|
return 'in_use';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveAssetStatusFromStock(endingBalance, borrowingQuantity) {
|
||||||
|
const ending = parseNonNegativeIntegerOrFallback(endingBalance, 0);
|
||||||
|
const borrowing = parseNonNegativeIntegerOrFallback(borrowingQuantity, 0);
|
||||||
|
|
||||||
|
if (ending <= 0) {
|
||||||
|
return 'exported';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (borrowing > 0) {
|
||||||
|
return 'in_use';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'in_stock';
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeAssetStockBuckets(endingBalance, proposedNewQuantity, proposedUsedQuantity) {
|
||||||
|
const ending = parseNonNegativeIntegerOrFallback(endingBalance, 0);
|
||||||
|
let newQuantity = parseNonNegativeIntegerOrFallback(proposedNewQuantity, ending);
|
||||||
|
let usedQuantity = parseNonNegativeIntegerOrFallback(proposedUsedQuantity, 0);
|
||||||
|
|
||||||
|
const currentTotal = newQuantity + usedQuantity;
|
||||||
|
if (currentTotal < ending) {
|
||||||
|
newQuantity += (ending - currentTotal);
|
||||||
|
} else if (currentTotal > ending) {
|
||||||
|
let overflow = currentTotal - ending;
|
||||||
|
const reduceFromNew = Math.min(newQuantity, overflow);
|
||||||
|
newQuantity -= reduceFromNew;
|
||||||
|
overflow -= reduceFromNew;
|
||||||
|
if (overflow > 0) {
|
||||||
|
usedQuantity = Math.max(usedQuantity - overflow, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
newQuantity: Math.max(newQuantity, 0),
|
||||||
|
usedQuantity: Math.max(usedQuantity, 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeAssetPayload(payload = {}) {
|
function normalizeAssetPayload(payload = {}) {
|
||||||
const quantity = parseNonNegativeIntegerOrFallback(payload.quantity, 0);
|
const quantity = parseNonNegativeIntegerOrFallback(payload.quantity, 0);
|
||||||
const importInPeriod = parseNonNegativeIntegerOrFallback(payload.importInPeriod, 0);
|
const importInPeriod = parseNonNegativeIntegerOrFallback(payload.importInPeriod, 0);
|
||||||
const exportInPeriod = parseNonNegativeIntegerOrFallback(payload.exportInPeriod, 0);
|
const exportInPeriod = parseNonNegativeIntegerOrFallback(payload.exportInPeriod, 0);
|
||||||
const providedEndingBalance = parseOptionalNonNegativeInteger(payload.endingBalance);
|
const providedEndingBalance = parseOptionalNonNegativeInteger(payload.endingBalance);
|
||||||
const endingBalance = providedEndingBalance !== null ? providedEndingBalance : 0;
|
const endingBalance = providedEndingBalance !== null
|
||||||
|
? providedEndingBalance
|
||||||
|
: Math.max(quantity + importInPeriod - exportInPeriod, 0);
|
||||||
|
const providedNewQuantity = parseOptionalNonNegativeInteger(payload.newQuantity);
|
||||||
|
const providedUsedQuantity = parseOptionalNonNegativeInteger(payload.usedQuantity);
|
||||||
|
const stockBuckets = normalizeAssetStockBuckets(
|
||||||
|
endingBalance,
|
||||||
|
providedNewQuantity !== null ? providedNewQuantity : endingBalance,
|
||||||
|
providedUsedQuantity !== null ? providedUsedQuantity : 0
|
||||||
|
);
|
||||||
|
const status = resolveAssetStatusFromStock(endingBalance, exportInPeriod);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assetCode: String(payload.assetCode || '').trim(),
|
assetCode: String(payload.assetCode || '').trim(),
|
||||||
@@ -445,12 +498,14 @@ function normalizeAssetPayload(payload = {}) {
|
|||||||
importInPeriod,
|
importInPeriod,
|
||||||
exportInPeriod,
|
exportInPeriod,
|
||||||
endingBalance,
|
endingBalance,
|
||||||
|
newQuantity: stockBuckets.newQuantity,
|
||||||
|
usedQuantity: stockBuckets.usedQuantity,
|
||||||
location: String(payload.location || '').trim() || null,
|
location: String(payload.location || '').trim() || null,
|
||||||
custodian: String(payload.custodian || '').trim() || null,
|
custodian: String(payload.custodian || '').trim() || null,
|
||||||
borrower: String(payload.borrower || '').trim() || null,
|
borrower: String(payload.borrower || '').trim() || null,
|
||||||
purchaseDate: parseNullableDate(payload.purchaseDate),
|
purchaseDate: parseNullableDate(payload.purchaseDate),
|
||||||
purchasePrice: parseNullableDecimal(payload.purchasePrice),
|
purchasePrice: parseNullableDecimal(payload.purchasePrice),
|
||||||
status: normalizeAssetStatus(payload.status),
|
status,
|
||||||
notes: String(payload.notes || '').trim() || null
|
notes: String(payload.notes || '').trim() || null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1659,6 +1714,8 @@ async function createTables() {
|
|||||||
ImportInPeriod INT NOT NULL DEFAULT 0,
|
ImportInPeriod INT NOT NULL DEFAULT 0,
|
||||||
ExportInPeriod INT NOT NULL DEFAULT 0,
|
ExportInPeriod INT NOT NULL DEFAULT 0,
|
||||||
EndingBalance INT NOT NULL DEFAULT 0,
|
EndingBalance INT NOT NULL DEFAULT 0,
|
||||||
|
NewQuantity INT NOT NULL DEFAULT 0,
|
||||||
|
UsedQuantity INT NOT NULL DEFAULT 0,
|
||||||
Unit NVARCHAR(50),
|
Unit NVARCHAR(50),
|
||||||
Department NVARCHAR(100),
|
Department NVARCHAR(100),
|
||||||
Project NVARCHAR(150),
|
Project NVARCHAR(150),
|
||||||
@@ -1826,6 +1883,8 @@ async function createTables() {
|
|||||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ImportInPeriod') IS NULL ALTER TABLE AssetInventory ADD ImportInPeriod INT NOT NULL CONSTRAINT DF_AssetInventory_ImportInPeriod DEFAULT(0);`);
|
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ImportInPeriod') IS NULL ALTER TABLE AssetInventory ADD ImportInPeriod INT NOT NULL CONSTRAINT DF_AssetInventory_ImportInPeriod DEFAULT(0);`);
|
||||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ExportInPeriod') IS NULL ALTER TABLE AssetInventory ADD ExportInPeriod INT NOT NULL CONSTRAINT DF_AssetInventory_ExportInPeriod DEFAULT(0);`);
|
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ExportInPeriod') IS NULL ALTER TABLE AssetInventory ADD ExportInPeriod INT NOT NULL CONSTRAINT DF_AssetInventory_ExportInPeriod DEFAULT(0);`);
|
||||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','EndingBalance') IS NULL ALTER TABLE AssetInventory ADD EndingBalance INT NOT NULL CONSTRAINT DF_AssetInventory_EndingBalance DEFAULT(0);`);
|
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','EndingBalance') IS NULL ALTER TABLE AssetInventory ADD EndingBalance INT NOT NULL CONSTRAINT DF_AssetInventory_EndingBalance DEFAULT(0);`);
|
||||||
|
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','NewQuantity') IS NULL ALTER TABLE AssetInventory ADD NewQuantity INT NOT NULL CONSTRAINT DF_AssetInventory_NewQuantity DEFAULT(0);`);
|
||||||
|
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','UsedQuantity') IS NULL ALTER TABLE AssetInventory ADD UsedQuantity INT NOT NULL CONSTRAINT DF_AssetInventory_UsedQuantity DEFAULT(0);`);
|
||||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','Project') IS NULL ALTER TABLE AssetInventory ADD Project NVARCHAR(150) NULL;`);
|
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','Project') IS NULL ALTER TABLE AssetInventory ADD Project NVARCHAR(150) NULL;`);
|
||||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','Borrower') IS NULL ALTER TABLE AssetInventory ADD Borrower NVARCHAR(255) NULL;`);
|
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','Borrower') IS NULL ALTER TABLE AssetInventory ADD Borrower NVARCHAR(255) NULL;`);
|
||||||
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ExportedBy') IS NULL ALTER TABLE AssetInventory ADD ExportedBy NVARCHAR(100) NULL;`);
|
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ExportedBy') IS NULL ALTER TABLE AssetInventory ADD ExportedBy NVARCHAR(100) NULL;`);
|
||||||
@@ -1881,6 +1940,34 @@ async function createTables() {
|
|||||||
await pool.request().query(`UPDATE AssetBorrowRequests SET RequestStatus = ISNULL(NULLIF(LTRIM(RTRIM(RequestStatus)), ''), 'approved');`);
|
await pool.request().query(`UPDATE AssetBorrowRequests SET RequestStatus = ISNULL(NULLIF(LTRIM(RTRIM(RequestStatus)), ''), 'approved');`);
|
||||||
await pool.request().query(`UPDATE AssetInventory SET EndingBalance = ISNULL(EndingBalance, ISNULL(Quantity, 0));`);
|
await pool.request().query(`UPDATE AssetInventory SET EndingBalance = ISNULL(EndingBalance, ISNULL(Quantity, 0));`);
|
||||||
await pool.request().query(`UPDATE AssetInventory SET Quantity = ISNULL(NULLIF(Quantity, 0), EndingBalance);`);
|
await pool.request().query(`UPDATE AssetInventory SET Quantity = ISNULL(NULLIF(Quantity, 0), EndingBalance);`);
|
||||||
|
await pool.request().query(`UPDATE AssetInventory SET UsedQuantity = CASE WHEN UsedQuantity < 0 THEN 0 ELSE ISNULL(UsedQuantity, 0) END;`);
|
||||||
|
await pool.request().query(`
|
||||||
|
UPDATE AssetInventory
|
||||||
|
SET NewQuantity = CASE
|
||||||
|
WHEN ISNULL(NewQuantity, 0) < 0 THEN 0
|
||||||
|
ELSE ISNULL(NewQuantity, 0)
|
||||||
|
END;
|
||||||
|
`);
|
||||||
|
await pool.request().query(`
|
||||||
|
UPDATE AssetInventory
|
||||||
|
SET NewQuantity = CASE
|
||||||
|
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) < ISNULL(EndingBalance, 0)
|
||||||
|
THEN ISNULL(NewQuantity, 0) + (ISNULL(EndingBalance, 0) - (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)))
|
||||||
|
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) > ISNULL(EndingBalance, 0)
|
||||||
|
THEN CASE
|
||||||
|
WHEN ISNULL(NewQuantity, 0) >= ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
|
||||||
|
THEN ISNULL(NewQuantity, 0) - ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
ELSE ISNULL(NewQuantity, 0)
|
||||||
|
END,
|
||||||
|
UsedQuantity = CASE
|
||||||
|
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) > ISNULL(EndingBalance, 0)
|
||||||
|
AND ISNULL(NewQuantity, 0) < ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
|
||||||
|
THEN ISNULL(EndingBalance, 0)
|
||||||
|
ELSE ISNULL(UsedQuantity, 0)
|
||||||
|
END;
|
||||||
|
`);
|
||||||
await pool.request().query(`
|
await pool.request().query(`
|
||||||
UPDATE ai
|
UPDATE ai
|
||||||
SET ai.ExportedBy = COALESCE(NULLIF(LTRIM(RTRIM(u.FullName)), ''), NULLIF(LTRIM(RTRIM(u.Username)), ''))
|
SET ai.ExportedBy = COALESCE(NULLIF(LTRIM(RTRIM(u.FullName)), ''), NULLIF(LTRIM(RTRIM(u.Username)), ''))
|
||||||
@@ -3708,6 +3795,11 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
|||||||
ai.AssetName,
|
ai.AssetName,
|
||||||
ai.Quantity,
|
ai.Quantity,
|
||||||
ai.ImportInPeriod,
|
ai.ImportInPeriod,
|
||||||
|
ai.ExportInPeriod,
|
||||||
|
ai.EndingBalance,
|
||||||
|
ai.NewQuantity,
|
||||||
|
ai.UsedQuantity,
|
||||||
|
ai.Status,
|
||||||
ai.Borrower,
|
ai.Borrower,
|
||||||
ai.Unit AS AssetUnit
|
ai.Unit AS AssetUnit
|
||||||
FROM AssetBorrowRequests br
|
FROM AssetBorrowRequests br
|
||||||
@@ -3736,12 +3828,21 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
|||||||
const currentBorrowed = parseBorrowerEntries(targetRequest.Borrower).reduce((sum, entry) => (
|
const currentBorrowed = parseBorrowerEntries(targetRequest.Borrower).reduce((sum, entry) => (
|
||||||
sum + parseNonNegativeInteger(entry?.quantity, 0)
|
sum + parseNonNegativeInteger(entry?.quantity, 0)
|
||||||
), 0);
|
), 0);
|
||||||
const endingBalance = Math.max(
|
const derivedEndingBalance = Math.max(
|
||||||
parseNonNegativeInteger(targetRequest.Quantity, 0)
|
parseNonNegativeInteger(targetRequest.Quantity, 0)
|
||||||
+ parseNonNegativeInteger(targetRequest.ImportInPeriod, 0)
|
+ parseNonNegativeInteger(targetRequest.ImportInPeriod, 0)
|
||||||
- currentBorrowed,
|
- currentBorrowed,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
const baseEndingBalance = parseOptionalNonNegativeInteger(targetRequest.EndingBalance);
|
||||||
|
const endingBalance = baseEndingBalance !== null ? baseEndingBalance : derivedEndingBalance;
|
||||||
|
const baseNewQuantity = parseOptionalNonNegativeInteger(targetRequest.NewQuantity);
|
||||||
|
const baseUsedQuantity = parseOptionalNonNegativeInteger(targetRequest.UsedQuantity);
|
||||||
|
const stockBuckets = normalizeAssetStockBuckets(
|
||||||
|
endingBalance,
|
||||||
|
baseNewQuantity !== null ? baseNewQuantity : endingBalance,
|
||||||
|
baseUsedQuantity !== null ? baseUsedQuantity : 0
|
||||||
|
);
|
||||||
|
|
||||||
if (requestQuantity > endingBalance) {
|
if (requestQuantity > endingBalance) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
@@ -3765,22 +3866,31 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const borrowFromNew = Math.min(stockBuckets.newQuantity, requestQuantity);
|
||||||
|
const borrowFromUsed = Math.max(requestQuantity - borrowFromNew, 0);
|
||||||
|
const nextEndingBalance = Math.max(endingBalance - requestQuantity, 0);
|
||||||
|
const nextNewQuantity = Math.max(stockBuckets.newQuantity - borrowFromNew, 0);
|
||||||
|
const nextUsedQuantity = Math.max(stockBuckets.usedQuantity - borrowFromUsed, 0);
|
||||||
|
const nextBorrowingQuantity = currentBorrowed + requestQuantity;
|
||||||
|
const nextStatus = resolveAssetStatusFromStock(nextEndingBalance, nextBorrowingQuantity);
|
||||||
|
|
||||||
await new sql.Request(transaction)
|
await new sql.Request(transaction)
|
||||||
.input('assetId', sql.Int, targetRequest.AssetId)
|
.input('assetId', sql.Int, targetRequest.AssetId)
|
||||||
.input('borrower', sql.NVarChar, mergedBorrowerSummary)
|
.input('borrower', sql.NVarChar, mergedBorrowerSummary)
|
||||||
.input('exportInPeriod', sql.Int, currentBorrowed + requestQuantity)
|
.input('exportInPeriod', sql.Int, nextBorrowingQuantity)
|
||||||
.input('endingBalance', sql.Int, Math.max(
|
.input('endingBalance', sql.Int, nextEndingBalance)
|
||||||
parseNonNegativeInteger(targetRequest.Quantity, 0)
|
.input('newQuantity', sql.Int, nextNewQuantity)
|
||||||
+ parseNonNegativeInteger(targetRequest.ImportInPeriod, 0)
|
.input('usedQuantity', sql.Int, nextUsedQuantity)
|
||||||
- (currentBorrowed + requestQuantity),
|
.input('status', sql.NVarChar, nextStatus)
|
||||||
0
|
|
||||||
))
|
|
||||||
.input('exportedBy', sql.NVarChar, processorName || null)
|
.input('exportedBy', sql.NVarChar, processorName || null)
|
||||||
.query(`
|
.query(`
|
||||||
UPDATE AssetInventory
|
UPDATE AssetInventory
|
||||||
SET Borrower = @borrower,
|
SET Borrower = @borrower,
|
||||||
ExportInPeriod = @exportInPeriod,
|
ExportInPeriod = @exportInPeriod,
|
||||||
EndingBalance = @endingBalance,
|
EndingBalance = @endingBalance,
|
||||||
|
NewQuantity = @newQuantity,
|
||||||
|
UsedQuantity = @usedQuantity,
|
||||||
|
Status = @status,
|
||||||
ExportedBy = @exportedBy,
|
ExportedBy = @exportedBy,
|
||||||
UpdatedDate = GETDATE()
|
UpdatedDate = GETDATE()
|
||||||
WHERE AssetId = @assetId
|
WHERE AssetId = @assetId
|
||||||
@@ -3806,17 +3916,40 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
|||||||
), 0);
|
), 0);
|
||||||
const quantity = parseNonNegativeInteger(targetRequest.Quantity, 0);
|
const quantity = parseNonNegativeInteger(targetRequest.Quantity, 0);
|
||||||
const importInPeriod = parseNonNegativeInteger(targetRequest.ImportInPeriod, 0);
|
const importInPeriod = parseNonNegativeInteger(targetRequest.ImportInPeriod, 0);
|
||||||
|
const derivedEndingBalance = Math.max(quantity + importInPeriod - parseNonNegativeInteger(targetRequest.ExportInPeriod, 0), 0);
|
||||||
|
const baseEndingBalance = parseOptionalNonNegativeInteger(targetRequest.EndingBalance);
|
||||||
|
const currentEndingBalance = baseEndingBalance !== null ? baseEndingBalance : derivedEndingBalance;
|
||||||
|
const baseNewQuantity = parseOptionalNonNegativeInteger(targetRequest.NewQuantity);
|
||||||
|
const baseUsedQuantity = parseOptionalNonNegativeInteger(targetRequest.UsedQuantity);
|
||||||
|
const stockBuckets = normalizeAssetStockBuckets(
|
||||||
|
currentEndingBalance,
|
||||||
|
baseNewQuantity !== null ? baseNewQuantity : currentEndingBalance,
|
||||||
|
baseUsedQuantity !== null ? baseUsedQuantity : 0
|
||||||
|
);
|
||||||
|
const nextEndingBalance = Math.max(quantity + importInPeriod - remainingBorrowed, 0);
|
||||||
|
const nextBuckets = normalizeAssetStockBuckets(
|
||||||
|
nextEndingBalance,
|
||||||
|
stockBuckets.newQuantity,
|
||||||
|
stockBuckets.usedQuantity + requestQuantity
|
||||||
|
);
|
||||||
|
const nextStatus = resolveAssetStatusFromStock(nextEndingBalance, remainingBorrowed);
|
||||||
await new sql.Request(transaction)
|
await new sql.Request(transaction)
|
||||||
.input('assetId', sql.Int, targetRequest.AssetId)
|
.input('assetId', sql.Int, targetRequest.AssetId)
|
||||||
.input('borrower', sql.NVarChar, borrowerSummary)
|
.input('borrower', sql.NVarChar, borrowerSummary)
|
||||||
.input('exportInPeriod', sql.Int, remainingBorrowed)
|
.input('exportInPeriod', sql.Int, remainingBorrowed)
|
||||||
.input('endingBalance', sql.Int, Math.max(quantity + importInPeriod - remainingBorrowed, 0))
|
.input('endingBalance', sql.Int, nextEndingBalance)
|
||||||
|
.input('newQuantity', sql.Int, nextBuckets.newQuantity)
|
||||||
|
.input('usedQuantity', sql.Int, nextBuckets.usedQuantity)
|
||||||
|
.input('status', sql.NVarChar, nextStatus)
|
||||||
.input('exportedBy', sql.NVarChar, processorName || null)
|
.input('exportedBy', sql.NVarChar, processorName || null)
|
||||||
.query(`
|
.query(`
|
||||||
UPDATE AssetInventory
|
UPDATE AssetInventory
|
||||||
SET Borrower = @borrower,
|
SET Borrower = @borrower,
|
||||||
ExportInPeriod = @exportInPeriod,
|
ExportInPeriod = @exportInPeriod,
|
||||||
EndingBalance = @endingBalance,
|
EndingBalance = @endingBalance,
|
||||||
|
NewQuantity = @newQuantity,
|
||||||
|
UsedQuantity = @usedQuantity,
|
||||||
|
Status = @status,
|
||||||
ExportedBy = CASE WHEN @borrower IS NULL THEN NULL ELSE @exportedBy END,
|
ExportedBy = CASE WHEN @borrower IS NULL THEN NULL ELSE @exportedBy END,
|
||||||
UpdatedDate = GETDATE()
|
UpdatedDate = GETDATE()
|
||||||
WHERE AssetId = @assetId
|
WHERE AssetId = @assetId
|
||||||
@@ -3931,8 +4064,15 @@ app.get('/api/assets', async (req, res) => {
|
|||||||
const result = await pool.request().query(`
|
const result = await pool.request().query(`
|
||||||
SELECT AssetId, AssetCode, AssetName, Model, SerialNumber,
|
SELECT AssetId, AssetCode, AssetName, Model, SerialNumber,
|
||||||
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance,
|
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance,
|
||||||
|
NewQuantity, UsedQuantity,
|
||||||
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
||||||
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy, CreatedDate, UpdatedDate
|
PurchaseDate, PurchasePrice,
|
||||||
|
CASE
|
||||||
|
WHEN ISNULL(EndingBalance, 0) <= 0 THEN 'exported'
|
||||||
|
WHEN ISNULL(ExportInPeriod, 0) > 0 THEN 'in_use'
|
||||||
|
ELSE 'in_stock'
|
||||||
|
END AS Status,
|
||||||
|
Notes, CreatedBy, CreatedDate, UpdatedDate
|
||||||
FROM AssetInventory
|
FROM AssetInventory
|
||||||
ORDER BY UpdatedDate DESC, AssetName ASC
|
ORDER BY UpdatedDate DESC, AssetName ASC
|
||||||
`);
|
`);
|
||||||
@@ -4016,8 +4156,15 @@ app.get('/api/assets/:id', async (req, res) => {
|
|||||||
.query(`
|
.query(`
|
||||||
SELECT AssetId, AssetCode, AssetName, Model, SerialNumber,
|
SELECT AssetId, AssetCode, AssetName, Model, SerialNumber,
|
||||||
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance,
|
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance,
|
||||||
|
NewQuantity, UsedQuantity,
|
||||||
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
||||||
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy, CreatedDate, UpdatedDate
|
PurchaseDate, PurchasePrice,
|
||||||
|
CASE
|
||||||
|
WHEN ISNULL(EndingBalance, 0) <= 0 THEN 'exported'
|
||||||
|
WHEN ISNULL(ExportInPeriod, 0) > 0 THEN 'in_use'
|
||||||
|
ELSE 'in_stock'
|
||||||
|
END AS Status,
|
||||||
|
Notes, CreatedBy, CreatedDate, UpdatedDate
|
||||||
FROM AssetInventory
|
FROM AssetInventory
|
||||||
WHERE AssetId = @assetId
|
WHERE AssetId = @assetId
|
||||||
`);
|
`);
|
||||||
@@ -4104,6 +4251,8 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
|
|||||||
ImportInPeriod,
|
ImportInPeriod,
|
||||||
ExportInPeriod,
|
ExportInPeriod,
|
||||||
EndingBalance,
|
EndingBalance,
|
||||||
|
NewQuantity,
|
||||||
|
UsedQuantity,
|
||||||
Custodian,
|
Custodian,
|
||||||
Borrower
|
Borrower
|
||||||
FROM AssetInventory WITH (UPDLOCK, ROWLOCK)
|
FROM AssetInventory WITH (UPDLOCK, ROWLOCK)
|
||||||
@@ -4129,6 +4278,13 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
|
|||||||
const baseEndingBalance = storedEndingBalance !== null
|
const baseEndingBalance = storedEndingBalance !== null
|
||||||
? storedEndingBalance
|
? storedEndingBalance
|
||||||
: Math.max(quantity + importInPeriod - baseExportInPeriod, 0);
|
: Math.max(quantity + importInPeriod - baseExportInPeriod, 0);
|
||||||
|
const baseNewQuantity = parseOptionalNonNegativeInteger(asset.NewQuantity);
|
||||||
|
const baseUsedQuantity = parseOptionalNonNegativeInteger(asset.UsedQuantity);
|
||||||
|
const stockBuckets = normalizeAssetStockBuckets(
|
||||||
|
baseEndingBalance,
|
||||||
|
baseNewQuantity !== null ? baseNewQuantity : baseEndingBalance,
|
||||||
|
baseUsedQuantity !== null ? baseUsedQuantity : 0
|
||||||
|
);
|
||||||
|
|
||||||
if (baseEndingBalance <= 0) {
|
if (baseEndingBalance <= 0) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
@@ -4150,6 +4306,11 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
|
|||||||
const exportDelta = nextBorrowerExport - previousBorrowerExport;
|
const exportDelta = nextBorrowerExport - previousBorrowerExport;
|
||||||
const nextExportInPeriod = Math.max(baseExportInPeriod + exportDelta, 0);
|
const nextExportInPeriod = Math.max(baseExportInPeriod + exportDelta, 0);
|
||||||
const nextEndingBalance = Math.max(baseEndingBalance - exportDelta, 0);
|
const nextEndingBalance = Math.max(baseEndingBalance - exportDelta, 0);
|
||||||
|
const borrowFromNew = Math.min(stockBuckets.newQuantity, exportDelta);
|
||||||
|
const borrowFromUsed = Math.max(exportDelta - borrowFromNew, 0);
|
||||||
|
const nextNewQuantity = Math.max(stockBuckets.newQuantity - borrowFromNew, 0);
|
||||||
|
const nextUsedQuantity = Math.max(stockBuckets.usedQuantity - borrowFromUsed, 0);
|
||||||
|
const nextStatus = resolveAssetStatusFromStock(nextEndingBalance, nextExportInPeriod);
|
||||||
|
|
||||||
await new sql.Request(transaction)
|
await new sql.Request(transaction)
|
||||||
.input('assetId', sql.Int, assetId)
|
.input('assetId', sql.Int, assetId)
|
||||||
@@ -4157,6 +4318,9 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
|
|||||||
.input('borrower', sql.NVarChar, borrowerSummary)
|
.input('borrower', sql.NVarChar, borrowerSummary)
|
||||||
.input('exportInPeriod', sql.Int, nextExportInPeriod)
|
.input('exportInPeriod', sql.Int, nextExportInPeriod)
|
||||||
.input('endingBalance', sql.Int, nextEndingBalance)
|
.input('endingBalance', sql.Int, nextEndingBalance)
|
||||||
|
.input('newQuantity', sql.Int, nextNewQuantity)
|
||||||
|
.input('usedQuantity', sql.Int, nextUsedQuantity)
|
||||||
|
.input('status', sql.NVarChar, nextStatus)
|
||||||
.input('exportedBy', sql.NVarChar, exportedByName)
|
.input('exportedBy', sql.NVarChar, exportedByName)
|
||||||
.query(`
|
.query(`
|
||||||
UPDATE AssetInventory
|
UPDATE AssetInventory
|
||||||
@@ -4164,6 +4328,9 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
|
|||||||
Borrower = @borrower,
|
Borrower = @borrower,
|
||||||
ExportInPeriod = @exportInPeriod,
|
ExportInPeriod = @exportInPeriod,
|
||||||
EndingBalance = @endingBalance,
|
EndingBalance = @endingBalance,
|
||||||
|
NewQuantity = @newQuantity,
|
||||||
|
UsedQuantity = @usedQuantity,
|
||||||
|
Status = @status,
|
||||||
ExportedBy = @exportedBy,
|
ExportedBy = @exportedBy,
|
||||||
UpdatedDate = GETDATE()
|
UpdatedDate = GETDATE()
|
||||||
WHERE AssetId = @assetId
|
WHERE AssetId = @assetId
|
||||||
@@ -4266,6 +4433,8 @@ app.post('/api/assets', requireAssetOrAdmin, async (req, res) => {
|
|||||||
.input('importInPeriod', sql.Int, payload.importInPeriod)
|
.input('importInPeriod', sql.Int, payload.importInPeriod)
|
||||||
.input('exportInPeriod', sql.Int, payload.exportInPeriod)
|
.input('exportInPeriod', sql.Int, payload.exportInPeriod)
|
||||||
.input('endingBalance', sql.Int, payload.endingBalance)
|
.input('endingBalance', sql.Int, payload.endingBalance)
|
||||||
|
.input('newQuantity', sql.Int, payload.newQuantity)
|
||||||
|
.input('usedQuantity', sql.Int, payload.usedQuantity)
|
||||||
.input('unit', sql.NVarChar, payload.unit)
|
.input('unit', sql.NVarChar, payload.unit)
|
||||||
.input('department', sql.NVarChar, payload.department)
|
.input('department', sql.NVarChar, payload.department)
|
||||||
.input('project', sql.NVarChar, payload.project)
|
.input('project', sql.NVarChar, payload.project)
|
||||||
@@ -4281,12 +4450,12 @@ app.post('/api/assets', requireAssetOrAdmin, async (req, res) => {
|
|||||||
.query(`
|
.query(`
|
||||||
INSERT INTO AssetInventory (
|
INSERT INTO AssetInventory (
|
||||||
AssetCode, AssetName, Model, SerialNumber,
|
AssetCode, AssetName, Model, SerialNumber,
|
||||||
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance,
|
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance, NewQuantity, UsedQuantity,
|
||||||
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
||||||
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy
|
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy
|
||||||
) VALUES (
|
) VALUES (
|
||||||
@assetCode, @assetName, @model, @serialNumber,
|
@assetCode, @assetName, @model, @serialNumber,
|
||||||
@quantity, @importInPeriod, @exportInPeriod, @endingBalance,
|
@quantity, @importInPeriod, @exportInPeriod, @endingBalance, @newQuantity, @usedQuantity,
|
||||||
@unit, @department, @project, @location, @custodian, @borrower, @exportedBy,
|
@unit, @department, @project, @location, @custodian, @borrower, @exportedBy,
|
||||||
@purchaseDate, @purchasePrice, @status, @notes, @createdBy
|
@purchaseDate, @purchasePrice, @status, @notes, @createdBy
|
||||||
);
|
);
|
||||||
@@ -4325,6 +4494,8 @@ app.put('/api/assets/:id', requireAssetOrAdmin, async (req, res) => {
|
|||||||
.input('importInPeriod', sql.Int, payload.importInPeriod)
|
.input('importInPeriod', sql.Int, payload.importInPeriod)
|
||||||
.input('exportInPeriod', sql.Int, payload.exportInPeriod)
|
.input('exportInPeriod', sql.Int, payload.exportInPeriod)
|
||||||
.input('endingBalance', sql.Int, payload.endingBalance)
|
.input('endingBalance', sql.Int, payload.endingBalance)
|
||||||
|
.input('newQuantity', sql.Int, payload.newQuantity)
|
||||||
|
.input('usedQuantity', sql.Int, payload.usedQuantity)
|
||||||
.input('unit', sql.NVarChar, payload.unit)
|
.input('unit', sql.NVarChar, payload.unit)
|
||||||
.input('department', sql.NVarChar, payload.department)
|
.input('department', sql.NVarChar, payload.department)
|
||||||
.input('project', sql.NVarChar, payload.project)
|
.input('project', sql.NVarChar, payload.project)
|
||||||
@@ -4346,6 +4517,8 @@ app.put('/api/assets/:id', requireAssetOrAdmin, async (req, res) => {
|
|||||||
ImportInPeriod = @importInPeriod,
|
ImportInPeriod = @importInPeriod,
|
||||||
ExportInPeriod = @exportInPeriod,
|
ExportInPeriod = @exportInPeriod,
|
||||||
EndingBalance = @endingBalance,
|
EndingBalance = @endingBalance,
|
||||||
|
NewQuantity = @newQuantity,
|
||||||
|
UsedQuantity = @usedQuantity,
|
||||||
Unit = @unit,
|
Unit = @unit,
|
||||||
Department = @department,
|
Department = @department,
|
||||||
Project = @project,
|
Project = @project,
|
||||||
@@ -4461,6 +4634,8 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
|||||||
.input('importInPeriod', sql.Int, row.importInPeriod)
|
.input('importInPeriod', sql.Int, row.importInPeriod)
|
||||||
.input('exportInPeriod', sql.Int, row.exportInPeriod)
|
.input('exportInPeriod', sql.Int, row.exportInPeriod)
|
||||||
.input('endingBalance', sql.Int, row.endingBalance)
|
.input('endingBalance', sql.Int, row.endingBalance)
|
||||||
|
.input('newQuantity', sql.Int, row.newQuantity)
|
||||||
|
.input('usedQuantity', sql.Int, row.usedQuantity)
|
||||||
.input('unit', sql.NVarChar, row.unit)
|
.input('unit', sql.NVarChar, row.unit)
|
||||||
.input('department', sql.NVarChar, row.department)
|
.input('department', sql.NVarChar, row.department)
|
||||||
.input('project', sql.NVarChar, row.project)
|
.input('project', sql.NVarChar, row.project)
|
||||||
@@ -4476,13 +4651,13 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
|||||||
.query(`
|
.query(`
|
||||||
INSERT INTO AssetInventory (
|
INSERT INTO AssetInventory (
|
||||||
AssetCode, AssetName, Model, SerialNumber,
|
AssetCode, AssetName, Model, SerialNumber,
|
||||||
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance,
|
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance, NewQuantity, UsedQuantity,
|
||||||
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
||||||
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy
|
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@assetCode, @assetName, @model, @serialNumber,
|
@assetCode, @assetName, @model, @serialNumber,
|
||||||
@quantity, @importInPeriod, @exportInPeriod, @endingBalance,
|
@quantity, @importInPeriod, @exportInPeriod, @endingBalance, @newQuantity, @usedQuantity,
|
||||||
@unit, @department, @project, @location, @custodian, @borrower, @exportedBy,
|
@unit, @department, @project, @location, @custodian, @borrower, @exportedBy,
|
||||||
@purchaseDate, @purchasePrice, @status, @notes, @createdBy
|
@purchaseDate, @purchasePrice, @status, @notes, @createdBy
|
||||||
);
|
);
|
||||||
@@ -4500,6 +4675,8 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
|||||||
.input('importInPeriod', sql.Int, row.importInPeriod)
|
.input('importInPeriod', sql.Int, row.importInPeriod)
|
||||||
.input('exportInPeriod', sql.Int, row.exportInPeriod)
|
.input('exportInPeriod', sql.Int, row.exportInPeriod)
|
||||||
.input('endingBalance', sql.Int, row.endingBalance)
|
.input('endingBalance', sql.Int, row.endingBalance)
|
||||||
|
.input('newQuantity', sql.Int, row.newQuantity)
|
||||||
|
.input('usedQuantity', sql.Int, row.usedQuantity)
|
||||||
.input('unit', sql.NVarChar, row.unit)
|
.input('unit', sql.NVarChar, row.unit)
|
||||||
.input('department', sql.NVarChar, row.department)
|
.input('department', sql.NVarChar, row.department)
|
||||||
.input('project', sql.NVarChar, row.project)
|
.input('project', sql.NVarChar, row.project)
|
||||||
@@ -4525,6 +4702,8 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
|||||||
ImportInPeriod = @importInPeriod,
|
ImportInPeriod = @importInPeriod,
|
||||||
ExportInPeriod = @exportInPeriod,
|
ExportInPeriod = @exportInPeriod,
|
||||||
EndingBalance = @endingBalance,
|
EndingBalance = @endingBalance,
|
||||||
|
NewQuantity = @newQuantity,
|
||||||
|
UsedQuantity = @usedQuantity,
|
||||||
Unit = @unit,
|
Unit = @unit,
|
||||||
Department = @department,
|
Department = @department,
|
||||||
Project = @project,
|
Project = @project,
|
||||||
@@ -4540,13 +4719,13 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
|||||||
WHEN NOT MATCHED THEN
|
WHEN NOT MATCHED THEN
|
||||||
INSERT (
|
INSERT (
|
||||||
AssetCode, AssetName, Model, SerialNumber,
|
AssetCode, AssetName, Model, SerialNumber,
|
||||||
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance,
|
Quantity, ImportInPeriod, ExportInPeriod, EndingBalance, NewQuantity, UsedQuantity,
|
||||||
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
Unit, Department, Project, Location, Custodian, Borrower, ExportedBy,
|
||||||
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy
|
PurchaseDate, PurchasePrice, Status, Notes, CreatedBy
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
@assetCode, @assetName, @model, @serialNumber,
|
@assetCode, @assetName, @model, @serialNumber,
|
||||||
@quantity, @importInPeriod, @exportInPeriod, @endingBalance,
|
@quantity, @importInPeriod, @exportInPeriod, @endingBalance, @newQuantity, @usedQuantity,
|
||||||
@unit, @department, @project, @location, @custodian, @borrower, @exportedBy,
|
@unit, @department, @project, @location, @custodian, @borrower, @exportedBy,
|
||||||
@purchaseDate, @purchasePrice, @status, @notes, @createdBy
|
@purchaseDate, @purchasePrice, @status, @notes, @createdBy
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ BEGIN
|
|||||||
ImportInPeriod INT NOT NULL DEFAULT 0,
|
ImportInPeriod INT NOT NULL DEFAULT 0,
|
||||||
ExportInPeriod INT NOT NULL DEFAULT 0,
|
ExportInPeriod INT NOT NULL DEFAULT 0,
|
||||||
EndingBalance INT NOT NULL DEFAULT 0,
|
EndingBalance INT NOT NULL DEFAULT 0,
|
||||||
|
NewQuantity INT NOT NULL DEFAULT 0,
|
||||||
|
UsedQuantity INT NOT NULL DEFAULT 0,
|
||||||
Unit NVARCHAR(50),
|
Unit NVARCHAR(50),
|
||||||
Department NVARCHAR(100),
|
Department NVARCHAR(100),
|
||||||
Project NVARCHAR(150),
|
Project NVARCHAR(150),
|
||||||
@@ -124,6 +126,47 @@ BEGIN
|
|||||||
ALTER TABLE AssetInventory ADD ExportedBy NVARCHAR(100) NULL;
|
ALTER TABLE AssetInventory ADD ExportedBy NVARCHAR(100) NULL;
|
||||||
END
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.AssetInventory', 'NewQuantity') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE AssetInventory ADD NewQuantity INT NOT NULL CONSTRAINT DF_AssetInventory_NewQuantity DEFAULT(0);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF COL_LENGTH('dbo.AssetInventory', 'UsedQuantity') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE AssetInventory ADD UsedQuantity INT NOT NULL CONSTRAINT DF_AssetInventory_UsedQuantity DEFAULT(0);
|
||||||
|
END
|
||||||
|
|
||||||
|
UPDATE AssetInventory
|
||||||
|
SET EndingBalance = ISNULL(EndingBalance, ISNULL(Quantity, 0));
|
||||||
|
|
||||||
|
UPDATE AssetInventory
|
||||||
|
SET UsedQuantity = CASE WHEN ISNULL(UsedQuantity, 0) < 0 THEN 0 ELSE ISNULL(UsedQuantity, 0) END;
|
||||||
|
|
||||||
|
UPDATE AssetInventory
|
||||||
|
SET NewQuantity = CASE
|
||||||
|
WHEN ISNULL(NewQuantity, 0) < 0 THEN 0
|
||||||
|
ELSE ISNULL(NewQuantity, 0)
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE AssetInventory
|
||||||
|
SET NewQuantity = CASE
|
||||||
|
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) < ISNULL(EndingBalance, 0)
|
||||||
|
THEN ISNULL(NewQuantity, 0) + (ISNULL(EndingBalance, 0) - (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)))
|
||||||
|
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) > ISNULL(EndingBalance, 0)
|
||||||
|
THEN CASE
|
||||||
|
WHEN ISNULL(NewQuantity, 0) >= ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
|
||||||
|
THEN ISNULL(NewQuantity, 0) - ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
ELSE ISNULL(NewQuantity, 0)
|
||||||
|
END,
|
||||||
|
UsedQuantity = CASE
|
||||||
|
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) > ISNULL(EndingBalance, 0)
|
||||||
|
AND ISNULL(NewQuantity, 0) < ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
|
||||||
|
THEN ISNULL(EndingBalance, 0)
|
||||||
|
ELSE ISNULL(UsedQuantity, 0)
|
||||||
|
END;
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 5. CREATE ASSET DEPARTMENTS TABLE
|
-- 5. CREATE ASSET DEPARTMENTS TABLE
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
|
|||||||
165
public/js/app.js
165
public/js/app.js
@@ -56,6 +56,7 @@ class AccountManager {
|
|||||||
this.initPromise = this.init();
|
this.initPromise = this.init();
|
||||||
this.pendingAccountAppId = undefined;
|
this.pendingAccountAppId = undefined;
|
||||||
this.editingAssetBorrowerEntries = [];
|
this.editingAssetBorrowerEntries = [];
|
||||||
|
this.editingAssetStockSnapshot = null;
|
||||||
this.pendingBorrowAssetId = undefined;
|
this.pendingBorrowAssetId = undefined;
|
||||||
this.editingAssetDepartmentId = undefined;
|
this.editingAssetDepartmentId = undefined;
|
||||||
this.pendingDeleteAssetDepartmentId = undefined;
|
this.pendingDeleteAssetDepartmentId = undefined;
|
||||||
@@ -1326,6 +1327,8 @@ class AccountManager {
|
|||||||
asset.ImportInPeriod,
|
asset.ImportInPeriod,
|
||||||
asset.ExportInPeriod,
|
asset.ExportInPeriod,
|
||||||
asset.EndingBalance,
|
asset.EndingBalance,
|
||||||
|
asset.NewQuantity,
|
||||||
|
asset.UsedQuantity,
|
||||||
asset.Department,
|
asset.Department,
|
||||||
asset.Project,
|
asset.Project,
|
||||||
asset.Location,
|
asset.Location,
|
||||||
@@ -1700,15 +1703,12 @@ class AccountManager {
|
|||||||
|
|
||||||
getAssetStatusMeta(status) {
|
getAssetStatusMeta(status) {
|
||||||
const normalized = String(status || '').toLowerCase();
|
const normalized = String(status || '').toLowerCase();
|
||||||
|
if (normalized === 'exported') {
|
||||||
|
return { label: 'Đã xuất', className: 'bg-rose-100 text-rose-700' };
|
||||||
|
}
|
||||||
if (normalized === 'in_stock') {
|
if (normalized === 'in_stock') {
|
||||||
return { label: 'Trong kho', className: 'bg-emerald-100 text-emerald-700' };
|
return { label: 'Trong kho', className: 'bg-emerald-100 text-emerald-700' };
|
||||||
}
|
}
|
||||||
if (normalized === 'maintenance') {
|
|
||||||
return { label: 'Bảo trì', className: 'bg-amber-100 text-amber-700' };
|
|
||||||
}
|
|
||||||
if (normalized === 'disposed') {
|
|
||||||
return { label: 'Thanh lý', className: 'bg-rose-100 text-rose-700' };
|
|
||||||
}
|
|
||||||
return { label: 'Đang sử dụng', className: 'bg-blue-100 text-blue-700' };
|
return { label: 'Đang sử dụng', className: 'bg-blue-100 text-blue-700' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1977,16 +1977,21 @@ class AccountManager {
|
|||||||
buildAssetQuantityMetrics(asset, borrowerEntriesOverride = null) {
|
buildAssetQuantityMetrics(asset, borrowerEntriesOverride = null) {
|
||||||
const quantity = this.parseNonNegativeInteger(asset?.Quantity ?? asset?.quantity, 0);
|
const quantity = this.parseNonNegativeInteger(asset?.Quantity ?? asset?.quantity, 0);
|
||||||
const importInPeriod = this.parseNonNegativeInteger(asset?.ImportInPeriod ?? asset?.importInPeriod, 0);
|
const importInPeriod = this.parseNonNegativeInteger(asset?.ImportInPeriod ?? asset?.importInPeriod, 0);
|
||||||
|
const storedExportInPeriod = this.parseOptionalNonNegativeInteger(asset?.ExportInPeriod ?? asset?.exportInPeriod);
|
||||||
|
const storedEndingBalance = this.parseOptionalNonNegativeInteger(asset?.EndingBalance ?? asset?.endingBalance);
|
||||||
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 borrowerExportInPeriod = borrowerEntries.reduce((sum, entry) => (
|
const borrowerExportInPeriod = borrowerEntries.reduce((sum, entry) => (
|
||||||
sum + this.parseNonNegativeInteger(entry?.quantity, 0)
|
sum + this.parseNonNegativeInteger(entry?.quantity, 0)
|
||||||
), 0);
|
), 0);
|
||||||
// Borrower entries are the source of truth for exported quantity.
|
// Prefer stored stock numbers from DB/file to avoid overriding imported balances.
|
||||||
// This keeps UI consistent even when legacy rows have stale stored balances.
|
const exportInPeriod = storedExportInPeriod !== null
|
||||||
const exportInPeriod = borrowerExportInPeriod;
|
? storedExportInPeriod
|
||||||
const endingBalance = Math.max(quantity + importInPeriod - exportInPeriod, 0);
|
: borrowerExportInPeriod;
|
||||||
|
const endingBalance = storedEndingBalance !== null
|
||||||
|
? storedEndingBalance
|
||||||
|
: Math.max(quantity + importInPeriod - exportInPeriod, 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
quantity,
|
quantity,
|
||||||
@@ -1998,18 +2003,67 @@ class AccountManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
computeAssetStatusCode(endingBalance, borrowingQuantity) {
|
||||||
|
const ending = this.parseNonNegativeInteger(endingBalance, 0);
|
||||||
|
const borrowing = this.parseNonNegativeInteger(borrowingQuantity, 0);
|
||||||
|
|
||||||
|
if (ending <= 0) {
|
||||||
|
return 'exported';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (borrowing > 0) {
|
||||||
|
return 'in_use';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'in_stock';
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeAssetStockSplit(endingBalance, newQuantityValue, usedQuantityValue) {
|
||||||
|
const ending = this.parseNonNegativeInteger(endingBalance, 0);
|
||||||
|
let newQuantity = this.parseNonNegativeInteger(newQuantityValue, ending);
|
||||||
|
let usedQuantity = this.parseNonNegativeInteger(usedQuantityValue, 0);
|
||||||
|
|
||||||
|
const total = newQuantity + usedQuantity;
|
||||||
|
if (total < ending) {
|
||||||
|
newQuantity += (ending - total);
|
||||||
|
} else if (total > ending) {
|
||||||
|
let overflow = total - ending;
|
||||||
|
const takeFromNew = Math.min(newQuantity, overflow);
|
||||||
|
newQuantity -= takeFromNew;
|
||||||
|
overflow -= takeFromNew;
|
||||||
|
if (overflow > 0) {
|
||||||
|
usedQuantity = Math.max(usedQuantity - overflow, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
newQuantity: Math.max(newQuantity, 0),
|
||||||
|
usedQuantity: Math.max(usedQuantity, 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
normalizeAssetComputedFields(asset) {
|
normalizeAssetComputedFields(asset) {
|
||||||
if (!asset || typeof asset !== 'object') {
|
if (!asset || typeof asset !== 'object') {
|
||||||
return asset;
|
return asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
const metrics = this.buildAssetQuantityMetrics(asset);
|
const metrics = this.buildAssetQuantityMetrics(asset);
|
||||||
|
const stockSplit = this.normalizeAssetStockSplit(
|
||||||
|
metrics.endingBalance,
|
||||||
|
asset?.NewQuantity ?? asset?.newQuantity,
|
||||||
|
asset?.UsedQuantity ?? asset?.usedQuantity
|
||||||
|
);
|
||||||
|
const status = this.computeAssetStatusCode(metrics.endingBalance, metrics.exportInPeriod);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...asset,
|
...asset,
|
||||||
Quantity: metrics.quantity,
|
Quantity: metrics.quantity,
|
||||||
ImportInPeriod: metrics.importInPeriod,
|
ImportInPeriod: metrics.importInPeriod,
|
||||||
ExportInPeriod: metrics.exportInPeriod,
|
ExportInPeriod: metrics.exportInPeriod,
|
||||||
EndingBalance: metrics.endingBalance,
|
EndingBalance: metrics.endingBalance,
|
||||||
|
NewQuantity: stockSplit.newQuantity,
|
||||||
|
UsedQuantity: stockSplit.usedQuantity,
|
||||||
|
Status: status,
|
||||||
Borrower: this.formatBorrowerEntries(metrics.borrowerEntries, '; ') || null
|
Borrower: this.formatBorrowerEntries(metrics.borrowerEntries, '; ') || null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2019,6 +2073,7 @@ class AccountManager {
|
|||||||
const importInput = document.getElementById('assetImportInPeriodInput');
|
const importInput = document.getElementById('assetImportInPeriodInput');
|
||||||
const exportInput = document.getElementById('assetExportInPeriodInput');
|
const exportInput = document.getElementById('assetExportInPeriodInput');
|
||||||
const endingInput = document.getElementById('assetEndingBalanceInput');
|
const endingInput = document.getElementById('assetEndingBalanceInput');
|
||||||
|
const statusInput = document.getElementById('assetStatusInput');
|
||||||
|
|
||||||
if (!quantityInput || !importInput) {
|
if (!quantityInput || !importInput) {
|
||||||
return;
|
return;
|
||||||
@@ -2035,6 +2090,12 @@ class AccountManager {
|
|||||||
if (endingInput) {
|
if (endingInput) {
|
||||||
endingInput.value = String(endingBalance);
|
endingInput.value = String(endingBalance);
|
||||||
}
|
}
|
||||||
|
if (statusInput) {
|
||||||
|
const statusCode = this.computeAssetStatusCode(endingBalance, exportInPeriod);
|
||||||
|
const statusMeta = this.getAssetStatusMeta(statusCode);
|
||||||
|
statusInput.value = statusMeta.label;
|
||||||
|
statusInput.dataset.statusCode = statusCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAssetStockListeners() {
|
setupAssetStockListeners() {
|
||||||
@@ -3510,8 +3571,7 @@ class AccountManager {
|
|||||||
<option value="">Tất cả</option>
|
<option value="">Tất cả</option>
|
||||||
<option value="in_use">Đang sử dụng</option>
|
<option value="in_use">Đang sử dụng</option>
|
||||||
<option value="in_stock">Trong kho</option>
|
<option value="in_stock">Trong kho</option>
|
||||||
<option value="maintenance">Bảo trì</option>
|
<option value="exported">Đã xuất</option>
|
||||||
<option value="disposed">Thanh lý</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1.5 flex-1">
|
<div class="flex items-center gap-1.5 flex-1">
|
||||||
@@ -3527,7 +3587,7 @@ class AccountManager {
|
|||||||
<div class="flex-1 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden min-h-0">
|
<div class="flex-1 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden min-h-0">
|
||||||
${pageInfo.data.length > 0 ? `
|
${pageInfo.data.length > 0 ? `
|
||||||
<div class="table-wrap overflow-y-auto overflow-x-auto flex-1">
|
<div class="table-wrap overflow-y-auto overflow-x-auto flex-1">
|
||||||
<table class="w-full text-left border-collapse" style="min-width: 2400px; border-collapse: separate; border-spacing: 0;">
|
<table class="w-full text-left border-collapse" style="min-width: 2720px; border-collapse: separate; border-spacing: 0;">
|
||||||
<thead class="sticky top-0 z-50 bg-slate-50 border-b border-slate-200">
|
<thead class="sticky top-0 z-50 bg-slate-50 border-b border-slate-200">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="px-4 py-2.5 text-center">
|
<th class="px-4 py-2.5 text-center">
|
||||||
@@ -3547,6 +3607,9 @@ class AccountManager {
|
|||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Dự án</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Dự án</th>
|
||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Người phụ trách</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Người phụ trách</th>
|
||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500" style="width: 132px; min-width: 132px; white-space: nowrap;">Trạng thái</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500" style="width: 132px; min-width: 132px; white-space: nowrap;">Trạng thái</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">SL hàng mới</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">SL đã qua sử dụng</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">SL đang mượn</th>
|
||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Vị trí</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Vị trí</th>
|
||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ngày mua</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ngày mua</th>
|
||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500" style="width: 240px; min-width: 240px;">Người mượn</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500" style="width: 240px; min-width: 240px;">Người mượn</th>
|
||||||
@@ -3583,6 +3646,9 @@ class AccountManager {
|
|||||||
<td class="px-4 py-3 text-sm" style="width: 132px; min-width: 132px; white-space: nowrap;">
|
<td class="px-4 py-3 text-sm" style="width: 132px; min-width: 132px; white-space: nowrap;">
|
||||||
<span class="inline-block px-2 py-1 rounded text-xs font-semibold ${statusMeta.className}" style="display: inline-block; white-space: nowrap; word-break: keep-all; overflow-wrap: normal;">${statusMeta.label}</span>
|
<span class="inline-block px-2 py-1 rounded text-xs font-semibold ${statusMeta.className}" style="display: inline-block; white-space: nowrap; word-break: keep-all; overflow-wrap: normal;">${statusMeta.label}</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600">${asset.NewQuantity ?? 0}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600">${asset.UsedQuantity ?? 0}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600">${asset.ExportInPeriod ?? 0}</td>
|
||||||
<td class="px-4 py-3 text-sm text-slate-600">${asset.Location || '-'}</td>
|
<td class="px-4 py-3 text-sm text-slate-600">${asset.Location || '-'}</td>
|
||||||
<td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">${this.formatDateOnly(asset.PurchaseDate)}</td>
|
<td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">${this.formatDateOnly(asset.PurchaseDate)}</td>
|
||||||
<td class="px-4 py-3 text-sm text-slate-600 whitespace-normal" style="width: 240px; min-width: 240px;">${this.formatBorrowerTableHtml(asset.Borrower)}</td>
|
<td class="px-4 py-3 text-sm text-slate-600 whitespace-normal" style="width: 240px; min-width: 240px;">${this.formatBorrowerTableHtml(asset.Borrower)}</td>
|
||||||
@@ -3667,6 +3733,9 @@ class AccountManager {
|
|||||||
<td class="px-4 py-3 text-sm" style="width: 132px; min-width: 132px; white-space: nowrap;">
|
<td class="px-4 py-3 text-sm" style="width: 132px; min-width: 132px; white-space: nowrap;">
|
||||||
<span class="inline-block px-2 py-1 rounded text-xs font-semibold ${statusMeta.className}" style="display: inline-block; white-space: nowrap; word-break: keep-all; overflow-wrap: normal;">${statusMeta.label}</span>
|
<span class="inline-block px-2 py-1 rounded text-xs font-semibold ${statusMeta.className}" style="display: inline-block; white-space: nowrap; word-break: keep-all; overflow-wrap: normal;">${statusMeta.label}</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600">${asset.NewQuantity ?? 0}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600">${asset.UsedQuantity ?? 0}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600">${asset.ExportInPeriod ?? 0}</td>
|
||||||
<td class="px-4 py-3 text-sm text-slate-600">${asset.Location || '-'}</td>
|
<td class="px-4 py-3 text-sm text-slate-600">${asset.Location || '-'}</td>
|
||||||
<td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">${this.formatDateOnly(asset.PurchaseDate)}</td>
|
<td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">${this.formatDateOnly(asset.PurchaseDate)}</td>
|
||||||
<td class="px-4 py-3 text-sm text-slate-600 whitespace-normal" style="width: 240px; min-width: 240px;">${this.formatBorrowerTableHtml(asset.Borrower)}</td>
|
<td class="px-4 py-3 text-sm text-slate-600 whitespace-normal" style="width: 240px; min-width: 240px;">${this.formatBorrowerTableHtml(asset.Borrower)}</td>
|
||||||
@@ -3895,6 +3964,9 @@ class AccountManager {
|
|||||||
['Nhập trong kỳ', asset?.ImportInPeriod ?? 0],
|
['Nhập trong kỳ', asset?.ImportInPeriod ?? 0],
|
||||||
['Xuất trong kỳ', asset?.ExportInPeriod ?? 0],
|
['Xuất trong kỳ', asset?.ExportInPeriod ?? 0],
|
||||||
['Tồn cuối kỳ', asset?.EndingBalance ?? 0],
|
['Tồn cuối kỳ', asset?.EndingBalance ?? 0],
|
||||||
|
['SL hàng mới', asset?.NewQuantity ?? 0],
|
||||||
|
['SL đã qua sử dụng', asset?.UsedQuantity ?? 0],
|
||||||
|
['SL đang mượn', asset?.ExportInPeriod ?? 0],
|
||||||
['Phòng ban', asset?.Department],
|
['Phòng ban', asset?.Department],
|
||||||
['Dự án', asset?.Project],
|
['Dự án', asset?.Project],
|
||||||
['Vị trí', asset?.Location],
|
['Vị trí', asset?.Location],
|
||||||
@@ -3922,16 +3994,36 @@ class AccountManager {
|
|||||||
populateAssetForm(asset) {
|
populateAssetForm(asset) {
|
||||||
const sourceAsset = asset || {};
|
const sourceAsset = asset || {};
|
||||||
const borrowerEntries = this.parseBorrowerEntries(sourceAsset?.Borrower);
|
const borrowerEntries = this.parseBorrowerEntries(sourceAsset?.Borrower);
|
||||||
|
const metrics = this.buildAssetQuantityMetrics(sourceAsset, borrowerEntries);
|
||||||
|
const stockSplit = this.normalizeAssetStockSplit(
|
||||||
|
metrics.endingBalance,
|
||||||
|
sourceAsset?.NewQuantity ?? metrics.endingBalance,
|
||||||
|
sourceAsset?.UsedQuantity ?? 0
|
||||||
|
);
|
||||||
|
const statusCode = sourceAsset?.AssetId
|
||||||
|
? this.computeAssetStatusCode(metrics.endingBalance, metrics.exportInPeriod)
|
||||||
|
: 'in_stock';
|
||||||
|
const statusMeta = this.getAssetStatusMeta(statusCode);
|
||||||
|
|
||||||
this.editingAssetBorrowerEntries = borrowerEntries;
|
this.editingAssetBorrowerEntries = borrowerEntries;
|
||||||
|
this.editingAssetStockSnapshot = {
|
||||||
|
endingBalance: metrics.endingBalance,
|
||||||
|
newQuantity: stockSplit.newQuantity,
|
||||||
|
usedQuantity: stockSplit.usedQuantity
|
||||||
|
};
|
||||||
this.clearAssetFormValidation();
|
this.clearAssetFormValidation();
|
||||||
|
|
||||||
document.getElementById('assetCodeInput').value = sourceAsset?.AssetCode || '';
|
document.getElementById('assetCodeInput').value = sourceAsset?.AssetCode || '';
|
||||||
document.getElementById('assetNameInput').value = sourceAsset?.AssetName || '';
|
document.getElementById('assetNameInput').value = sourceAsset?.AssetName || '';
|
||||||
document.getElementById('assetStatusInput').value = String(sourceAsset?.Status || 'in_use').toLowerCase();
|
const statusInput = document.getElementById('assetStatusInput');
|
||||||
|
if (statusInput) {
|
||||||
|
statusInput.value = statusMeta.label;
|
||||||
|
statusInput.dataset.statusCode = statusCode;
|
||||||
|
}
|
||||||
document.getElementById('assetModelInput').value = sourceAsset?.Model || '';
|
document.getElementById('assetModelInput').value = sourceAsset?.Model || '';
|
||||||
document.getElementById('assetSerialInput').value = sourceAsset?.SerialNumber || '';
|
document.getElementById('assetSerialInput').value = sourceAsset?.SerialNumber || '';
|
||||||
document.getElementById('assetQuantityInput').value = this.parseNonNegativeInteger(sourceAsset?.Quantity, 0);
|
document.getElementById('assetQuantityInput').value = metrics.quantity;
|
||||||
document.getElementById('assetImportInPeriodInput').value = this.parseNonNegativeInteger(sourceAsset?.ImportInPeriod, 0);
|
document.getElementById('assetImportInPeriodInput').value = metrics.importInPeriod;
|
||||||
document.getElementById('assetUnitInput').value = sourceAsset?.Unit || '';
|
document.getElementById('assetUnitInput').value = sourceAsset?.Unit || '';
|
||||||
this.refreshAssetDepartmentOptions(sourceAsset?.Department || '');
|
this.refreshAssetDepartmentOptions(sourceAsset?.Department || '');
|
||||||
this.refreshAssetProjectOptions(sourceAsset?.Project || '');
|
this.refreshAssetProjectOptions(sourceAsset?.Project || '');
|
||||||
@@ -3962,6 +4054,7 @@ class AccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.editingAssetId === undefined) {
|
if (this.editingAssetId === undefined) {
|
||||||
|
this.editingAssetStockSnapshot = null;
|
||||||
this.populateAssetForm(null);
|
this.populateAssetForm(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4087,19 +4180,36 @@ class AccountManager {
|
|||||||
const borrower = this.formatBorrowerEntries(borrowerEntries, '; ') || null;
|
const borrower = this.formatBorrowerEntries(borrowerEntries, '; ') || null;
|
||||||
const computedEndingBalance = Math.max(quantity + importInPeriod - exportInPeriod, 0);
|
const computedEndingBalance = Math.max(quantity + importInPeriod - exportInPeriod, 0);
|
||||||
const endingBalance = endingBalanceInput !== null ? endingBalanceInput : computedEndingBalance;
|
const endingBalance = endingBalanceInput !== null ? endingBalanceInput : computedEndingBalance;
|
||||||
|
const status = this.computeAssetStatusCode(endingBalance, exportInPeriod);
|
||||||
|
|
||||||
|
const previousSnapshot = this.editingAssetStockSnapshot;
|
||||||
|
let nextNewQuantity = endingBalance;
|
||||||
|
let nextUsedQuantity = 0;
|
||||||
|
if (previousSnapshot) {
|
||||||
|
const previousEnding = this.parseNonNegativeInteger(previousSnapshot.endingBalance, 0);
|
||||||
|
const previousNew = this.parseNonNegativeInteger(previousSnapshot.newQuantity, previousEnding);
|
||||||
|
const previousUsed = this.parseNonNegativeInteger(previousSnapshot.usedQuantity, 0);
|
||||||
|
const deltaEnding = endingBalance - previousEnding;
|
||||||
|
const tentativeNew = Math.max(previousNew + deltaEnding, 0);
|
||||||
|
const normalized = this.normalizeAssetStockSplit(endingBalance, tentativeNew, previousUsed);
|
||||||
|
nextNewQuantity = normalized.newQuantity;
|
||||||
|
nextUsedQuantity = normalized.usedQuantity;
|
||||||
|
}
|
||||||
|
|
||||||
const purchasePrice = String(document.getElementById('assetPriceInput')?.value ?? '').trim();
|
const purchasePrice = String(document.getElementById('assetPriceInput')?.value ?? '').trim();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assetCode: document.getElementById('assetCodeInput')?.value?.trim() || '',
|
assetCode: document.getElementById('assetCodeInput')?.value?.trim() || '',
|
||||||
assetName: document.getElementById('assetNameInput')?.value?.trim() || '',
|
assetName: document.getElementById('assetNameInput')?.value?.trim() || '',
|
||||||
status: document.getElementById('assetStatusInput')?.value || 'in_use',
|
status,
|
||||||
model: document.getElementById('assetModelInput')?.value?.trim() || '',
|
model: document.getElementById('assetModelInput')?.value?.trim() || '',
|
||||||
serialNumber: document.getElementById('assetSerialInput')?.value?.trim() || '',
|
serialNumber: document.getElementById('assetSerialInput')?.value?.trim() || '',
|
||||||
quantity,
|
quantity,
|
||||||
importInPeriod,
|
importInPeriod,
|
||||||
exportInPeriod,
|
exportInPeriod,
|
||||||
endingBalance,
|
endingBalance,
|
||||||
|
newQuantity: nextNewQuantity,
|
||||||
|
usedQuantity: nextUsedQuantity,
|
||||||
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() || '',
|
||||||
@@ -4124,6 +4234,13 @@ class AccountManager {
|
|||||||
const resolvedBaseEndingBalance = baseEndingBalance !== null
|
const resolvedBaseEndingBalance = baseEndingBalance !== null
|
||||||
? baseEndingBalance
|
? baseEndingBalance
|
||||||
: Math.max(quantity + importInPeriod - baseExportInPeriod, 0);
|
: Math.max(quantity + importInPeriod - baseExportInPeriod, 0);
|
||||||
|
const baseNewQuantity = this.parseOptionalNonNegativeInteger(asset?.NewQuantity);
|
||||||
|
const baseUsedQuantity = this.parseOptionalNonNegativeInteger(asset?.UsedQuantity);
|
||||||
|
const resolvedStockSplit = this.normalizeAssetStockSplit(
|
||||||
|
resolvedBaseEndingBalance,
|
||||||
|
baseNewQuantity !== null ? baseNewQuantity : resolvedBaseEndingBalance,
|
||||||
|
baseUsedQuantity !== null ? baseUsedQuantity : 0
|
||||||
|
);
|
||||||
const borrowerEntries = Array.isArray(borrowerEntriesOverride)
|
const borrowerEntries = Array.isArray(borrowerEntriesOverride)
|
||||||
? borrowerEntriesOverride
|
? borrowerEntriesOverride
|
||||||
: this.parseBorrowerEntries(asset?.Borrower);
|
: this.parseBorrowerEntries(asset?.Borrower);
|
||||||
@@ -4131,6 +4248,8 @@ class AccountManager {
|
|||||||
|
|
||||||
let exportInPeriod = baseExportInPeriod;
|
let exportInPeriod = baseExportInPeriod;
|
||||||
let endingBalance = resolvedBaseEndingBalance;
|
let endingBalance = resolvedBaseEndingBalance;
|
||||||
|
let newQuantity = resolvedStockSplit.newQuantity;
|
||||||
|
let usedQuantity = resolvedStockSplit.usedQuantity;
|
||||||
if (Array.isArray(borrowerEntriesOverride)) {
|
if (Array.isArray(borrowerEntriesOverride)) {
|
||||||
const existingBorrowerEntries = this.parseBorrowerEntries(asset?.Borrower);
|
const existingBorrowerEntries = this.parseBorrowerEntries(asset?.Borrower);
|
||||||
const previousBorrowerExport = existingBorrowerEntries.reduce((sum, entry) => (
|
const previousBorrowerExport = existingBorrowerEntries.reduce((sum, entry) => (
|
||||||
@@ -4142,6 +4261,13 @@ class AccountManager {
|
|||||||
const exportDelta = nextBorrowerExport - previousBorrowerExport;
|
const exportDelta = nextBorrowerExport - previousBorrowerExport;
|
||||||
exportInPeriod = Math.max(baseExportInPeriod + exportDelta, 0);
|
exportInPeriod = Math.max(baseExportInPeriod + exportDelta, 0);
|
||||||
endingBalance = Math.max(resolvedBaseEndingBalance - exportDelta, 0);
|
endingBalance = Math.max(resolvedBaseEndingBalance - exportDelta, 0);
|
||||||
|
const borrowFromNew = Math.min(newQuantity, Math.max(exportDelta, 0));
|
||||||
|
const borrowFromUsed = Math.max(Math.max(exportDelta, 0) - borrowFromNew, 0);
|
||||||
|
newQuantity = Math.max(newQuantity - borrowFromNew, 0);
|
||||||
|
usedQuantity = Math.max(usedQuantity - borrowFromUsed, 0);
|
||||||
|
const normalizedSplit = this.normalizeAssetStockSplit(endingBalance, newQuantity, usedQuantity);
|
||||||
|
newQuantity = normalizedSplit.newQuantity;
|
||||||
|
usedQuantity = normalizedSplit.usedQuantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawPrice = asset?.PurchasePrice;
|
const rawPrice = asset?.PurchasePrice;
|
||||||
@@ -4152,13 +4278,15 @@ class AccountManager {
|
|||||||
return {
|
return {
|
||||||
assetCode: String(asset?.AssetCode || '').trim(),
|
assetCode: String(asset?.AssetCode || '').trim(),
|
||||||
assetName: String(asset?.AssetName || '').trim(),
|
assetName: String(asset?.AssetName || '').trim(),
|
||||||
status: String(asset?.Status || 'in_use'),
|
status: this.computeAssetStatusCode(endingBalance, exportInPeriod),
|
||||||
model: String(asset?.Model || '').trim(),
|
model: String(asset?.Model || '').trim(),
|
||||||
serialNumber: String(asset?.SerialNumber || '').trim(),
|
serialNumber: String(asset?.SerialNumber || '').trim(),
|
||||||
quantity,
|
quantity,
|
||||||
importInPeriod,
|
importInPeriod,
|
||||||
exportInPeriod,
|
exportInPeriod,
|
||||||
endingBalance,
|
endingBalance,
|
||||||
|
newQuantity,
|
||||||
|
usedQuantity,
|
||||||
unit: String(asset?.Unit || '').trim(),
|
unit: String(asset?.Unit || '').trim(),
|
||||||
department: String(asset?.Department || '').trim(),
|
department: String(asset?.Department || '').trim(),
|
||||||
project: String(fieldOverrides?.project ?? asset?.Project ?? '').trim(),
|
project: String(fieldOverrides?.project ?? asset?.Project ?? '').trim(),
|
||||||
@@ -4398,6 +4526,7 @@ class AccountManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.editingAssetId = undefined;
|
this.editingAssetId = undefined;
|
||||||
|
this.editingAssetStockSnapshot = null;
|
||||||
this.notifySuccess(isEdit ? 'Cập nhật tài sản thành công' : 'Thêm tài sản thành công');
|
this.notifySuccess(isEdit ? 'Cập nhật tài sản thành công' : 'Thêm tài sản thành công');
|
||||||
this.closeModals();
|
this.closeModals();
|
||||||
await this.refreshAssetsUI();
|
await this.refreshAssetsUI();
|
||||||
|
|||||||
@@ -222,12 +222,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Trạng thái</label>
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Trạng thái</label>
|
||||||
<select id="assetStatusInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
|
<input type="text" id="assetStatusInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly value="Trong kho">
|
||||||
<option value="in_use">Đang sử dụng</option>
|
|
||||||
<option value="in_stock">Trong kho</option>
|
|
||||||
<option value="maintenance">Bảo trì</option>
|
|
||||||
<option value="disposed">Thanh lý</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Model</label>
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Model</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user