diff --git a/docs/MODULE_DATA_FLOW.html b/docs/MODULE_DATA_FLOW.html
index 8197f48..dee5fbb 100644
--- a/docs/MODULE_DATA_FLOW.html
+++ b/docs/MODULE_DATA_FLOW.html
@@ -747,7 +747,7 @@ flowchart TB
V2[plant / dept / warehouse / user refs]
V3[vendor / PO / GRN / grn_item refs]
V4[disposal_date required if DISPOSED/SCRAPPED]
- V5[depreciation_rate required if method=OTHER]
+ V5[depreciation_rate required if method=CUSTOM]
V6[resolve rate from category defaults]
end
@@ -888,7 +888,7 @@ flowchart TD
subgraph Methods["depreciation_method"]
SLM[SLM Straight Line]
WDV[WDV Written Down Value]
- OTHER[OTHER manual rate required]
+ CUSTOM[CUSTOM manual rate required]
end
PREVIEW --> CALC[calculateDepreciation]
@@ -900,7 +900,7 @@ flowchart TD
SLM --> CALC
WDV --> CALC
- OTHER --> CALC
+ CUSTOM --> CALC
@@ -909,12 +909,13 @@ flowchart TD
| Field | Meaning | Role in formula |
purchase_cost | Asset cost / amount | Starting book value; base for rate & annual dep |
- salvage_value | Residual value at end of life | Floor for book value; used in rate auto-calc |
+ salvage_value | Residual value at end of life (amount) | Floor for book value; used in rate auto-calc |
+ salvage_percentage | Residual as % of purchase_cost (0–100) | Synced with amount; FE may send either |
useful_life_years | Expected life in years | Rate auto-calc + cap on years elapsed |
commencement_date | Usage / put-to-use date | Primary start date for years elapsed |
purchase_date | Purchase date | Fallback start date if commencement is null |
- depreciation_method | SLM / WDV / OTHER | Which formula path to run |
- depreciation_rate | % per year | Optional for SLM/WDV (auto); required for OTHER |
+ depreciation_method | SLM / WDV / CUSTOM | Which formula path to run |
+ depreciation_rate | % per year | Optional for SLM/WDV (auto); required for CUSTOM |
as_of_date | Calculate as of (preview only) | Defaults to today |
@@ -971,7 +972,7 @@ annual_depreciation = book_value × rate / 100 // next year's dep on curr
Next annual (on WDV) ≈ 79430 × 20.57% ≈ ₹16,340
- OTHER method
+ CUSTOM method
Same accumulation style as SLM, but depreciation_rate is mandatory (no auto-calc).
@@ -979,7 +980,7 @@ annual_depreciation = book_value × rate / 100 // next year's dep on curr
| SLM | Yes (if rate omitted) | Original purchase_cost every year | In rate formula + book-value floor |
| WDV | Yes (if rate omitted) | Reducing book value each year | In rate formula + book-value floor |
- | OTHER | No — send depreciation_rate | Original cost × rate (like SLM) | Book-value floor only |
+ | CUSTOM | No — send depreciation_rate | Original cost × rate (like SLM) | Book-value floor only |
diff --git a/docs/MODULE_DATA_FLOW.md b/docs/MODULE_DATA_FLOW.md
index 6b4a66b..6b67c53 100644
--- a/docs/MODULE_DATA_FLOW.md
+++ b/docs/MODULE_DATA_FLOW.md
@@ -353,7 +353,7 @@ flowchart TB
V2[location / dept / user refs]
V3[vendor / PO / GRN / grn_item refs]
V4[disposal_date required if DISPOSED/SCRAPPED]
- V5[depreciation_rate required if method=OTHER]
+ V5[depreciation_rate required if method=CUSTOM]
V6[resolve rate from category defaults]
end
@@ -497,7 +497,7 @@ flowchart TD
subgraph Methods["depreciation_method"]
SLM[SLM Straight Line]
WDV[WDV Written Down Value]
- OTHER[OTHER manual rate required]
+ CUSTOM[CUSTOM manual rate required]
end
PREVIEW --> CALC[calculateDepreciation]
@@ -509,7 +509,7 @@ flowchart TD
SLM --> CALC
WDV --> CALC
- OTHER --> CALC
+ CUSTOM --> CALC
```
### Input fields used in calculation
@@ -517,12 +517,13 @@ flowchart TD
| Field | Meaning | Role in formula |
|---|---|---|
| `purchase_cost` | Asset cost / amount | Starting book value; base for rate & annual dep |
-| `salvage_value` | Residual value at end of life | Floor for book value; used in rate auto-calc |
+| `salvage_value` | Residual value at end of life (amount) | Floor for book value; used in rate auto-calc |
+| `salvage_percentage` | Residual as % of purchase_cost (0–100) | Synced with amount; FE may send either |
| `useful_life_years` | Expected life in years | Rate auto-calc + cap on years elapsed |
| `commencement_date` | Usage / put-to-use date | **Primary** start date for years elapsed |
| `purchase_date` | Purchase date | Fallback start date if commencement is null |
-| `depreciation_method` | `SLM` / `WDV` / `OTHER` | Which formula path to run |
-| `depreciation_rate` | % per year | Optional for SLM/WDV (auto); required for OTHER |
+| `depreciation_method` | `SLM` / `WDV` / `CUSTOM` | Which formula path to run |
+| `depreciation_rate` | % per year | Optional for SLM/WDV (auto); required for CUSTOM |
| `as_of_date` | Calculate as of (preview only) | Defaults to today |
### Date → years elapsed
@@ -594,7 +595,7 @@ annual_depreciation = book_value × rate / 100 // next year's dep on curr
- Accumulated ≈ **₹20,570**
- Next annual (on WDV) ≈ 79430 × 20.57% ≈ **₹16,340**
-### OTHER method
+### CUSTOM method
Same accumulation style as SLM, but `depreciation_rate` is **mandatory** (no auto-calc).
@@ -602,7 +603,7 @@ Same accumulation style as SLM, but `depreciation_rate` is **mandatory** (no aut
|---|---|---|---|
| **SLM** | Yes (if rate omitted) | Original `purchase_cost` every year | In rate formula + book-value floor |
| **WDV** | Yes (if rate omitted) | Reducing book value each year | In rate formula + book-value floor |
-| **OTHER** | No — send `depreciation_rate` | Original cost × rate (like SLM) | Book-value floor only |
+| **CUSTOM** | No — send `depreciation_rate` | Original cost × rate (like SLM) | Book-value floor only |
**FE usage:**
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 6eb71b1..47b3ab6 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -189,6 +189,7 @@ model assets {
depreciation_method String? @db.VarChar(20)
depreciation_rate Decimal? @db.Decimal(8, 4)
salvage_value Decimal? @default(0) @db.Decimal(15, 4)
+ salvage_percentage Decimal? @default(0) @db.Decimal(8, 4)
warranty_expiry_date DateTime? @db.Date
condition String @default("NEW") @db.VarChar(20)
status String @default("IN_USE") @db.VarChar(30)
diff --git a/scripts/patch-assets-salvage-percentage.sql b/scripts/patch-assets-salvage-percentage.sql
new file mode 100644
index 0000000..73899fe
--- /dev/null
+++ b/scripts/patch-assets-salvage-percentage.sql
@@ -0,0 +1,27 @@
+-- Rename depreciation method OTHER → CUSTOM; add salvage_percentage
+-- Run on each environment after deploy.
+
+ALTER TABLE assets
+ ADD COLUMN IF NOT EXISTS salvage_percentage DECIMAL(8, 4) DEFAULT 0;
+
+COMMENT ON COLUMN assets.salvage_percentage IS 'Salvage as % of purchase_cost (0–100). Synced with salvage_value.';
+COMMENT ON COLUMN assets.depreciation_method IS 'SLM, WDV, CUSTOM';
+
+UPDATE assets
+SET depreciation_method = 'CUSTOM'
+WHERE depreciation_method = 'OTHER'
+ AND deleted_at IS NULL;
+
+UPDATE assets
+SET salvage_percentage = ROUND(
+ (COALESCE(salvage_value, 0) / purchase_cost) * 100,
+ 4
+)
+WHERE purchase_cost > 0
+ AND (salvage_percentage IS NULL OR salvage_percentage = 0)
+ AND COALESCE(salvage_value, 0) > 0
+ AND deleted_at IS NULL;
+
+UPDATE item_categories
+SET default_depreciation_method = 'CUSTOM'
+WHERE default_depreciation_method = 'OTHER';
diff --git a/src/docs/assets-routes.yaml b/src/docs/assets-routes.yaml
index 8ed260d..99c69fe 100644
--- a/src/docs/assets-routes.yaml
+++ b/src/docs/assets-routes.yaml
@@ -38,9 +38,10 @@ components:
purchase_date: { type: string, format: date, nullable: true }
purchase_cost: { type: number, example: 850000 }
useful_life_years: { type: integer, example: 10 }
- depreciation_method: { type: string, enum: [SLM, WDV, OTHER], example: SLM }
- depreciation_rate: { type: number, nullable: true, example: 10, description: 'Required for OTHER; auto-calculated for SLM/WDV when omitted' }
- salvage_value: { type: number, example: 50000 }
+ depreciation_method: { type: string, enum: [SLM, WDV, CUSTOM], example: SLM }
+ depreciation_rate: { type: number, nullable: true, example: 10, description: 'Required for CUSTOM; auto-calculated for SLM/WDV when omitted' }
+ salvage_value: { type: number, example: 50000, description: 'Salvage amount. Send this OR salvage_percentage (not both required).' }
+ salvage_percentage: { type: number, example: 5, description: 'Salvage as % of purchase_cost (0–100). If both sent, percentage wins and amount is derived.' }
warranty_expiry_date: { type: string, format: date, nullable: true }
condition: { type: string, enum: [NEW, GOOD, FAIR, POOR] }
status: { type: string, enum: [IN_USE, IDLE, UNDER_MAINTENANCE, DISPOSED, SCRAPPED] }
@@ -84,9 +85,10 @@ components:
purchase_date: { type: string, format: date, nullable: true }
purchase_cost: { type: number }
useful_life_years: { type: integer }
- depreciation_method: { type: string, enum: [SLM, WDV, OTHER] }
+ depreciation_method: { type: string, enum: [SLM, WDV, CUSTOM] }
depreciation_rate: { type: number, nullable: true }
salvage_value: { type: number }
+ salvage_percentage: { type: number, description: 'Salvage as % of purchase_cost (0–100)' }
warranty_expiry_date: { type: string, format: date, nullable: true }
condition: { type: string, enum: [NEW, GOOD, FAIR, POOR] }
status: { type: string, enum: [IN_USE, IDLE, UNDER_MAINTENANCE, DISPOSED, SCRAPPED] }
@@ -100,10 +102,11 @@ components:
type: object
required: [depreciation_method]
properties:
- depreciation_method: { type: string, enum: [SLM, WDV, OTHER] }
+ depreciation_method: { type: string, enum: [SLM, WDV, CUSTOM] }
depreciation_rate: { type: number, nullable: true }
purchase_cost: { type: number, example: 850000 }
salvage_value: { type: number, example: 50000 }
+ salvage_percentage: { type: number, example: 5, description: 'Optional; if sent with/without salvage_value, % wins when both present' }
useful_life_years: { type: integer, example: 10 }
commencement_date: { type: string, format: date, description: 'Preferred depreciation start date' }
purchase_date: { type: string, format: date }
@@ -418,7 +421,7 @@ paths:
/assets/depreciation-methods:
get:
tags: [Assets]
- summary: List depreciation method dropdown options (SLM, WDV, OTHER)
+ summary: List depreciation method dropdown options (SLM, WDV, CUSTOM)
responses:
'200':
description: Depreciation methods fetched
diff --git a/src/docs/completed-routes.yaml b/src/docs/completed-routes.yaml
index 5074136..7dfefe2 100644
--- a/src/docs/completed-routes.yaml
+++ b/src/docs/completed-routes.yaml
@@ -116,7 +116,7 @@ components:
category_type: { type: string, enum: [STOCK, ASSET], example: STOCK, description: 'STOCK categories are shown in the Items module; ASSET categories in the Assets module' }
code_prefix: { type: string, nullable: true, example: 'MCH', description: Optional; used for asset code series when category is used for assets }
default_useful_life_years: { type: integer, nullable: true, example: 15 }
- default_depreciation_method: { type: string, nullable: true, enum: [SLM, WDV, OTHER], example: SLM }
+ default_depreciation_method: { type: string, nullable: true, enum: [SLM, WDV, CUSTOM], example: SLM }
is_active: { type: boolean, example: true }
ItemCategoriesUpdateBody:
type: object
@@ -127,7 +127,7 @@ components:
category_type: { type: string, enum: [STOCK, ASSET], example: STOCK }
code_prefix: { type: string, nullable: true, example: 'MCH' }
default_useful_life_years: { type: integer, nullable: true, example: 15 }
- default_depreciation_method: { type: string, nullable: true, enum: [SLM, WDV, OTHER], example: SLM }
+ default_depreciation_method: { type: string, nullable: true, enum: [SLM, WDV, CUSTOM], example: SLM }
is_active: { type: boolean, example: true }
ItemSubcategoriesCreateBody:
type: object
diff --git a/src/docs/reports-routes.yaml b/src/docs/reports-routes.yaml
index 4cb2f06..00821d2 100644
--- a/src/docs/reports-routes.yaml
+++ b/src/docs/reports-routes.yaml
@@ -142,7 +142,7 @@ paths:
schema: { type: string }
- in: query
name: depreciation_method
- schema: { type: string, enum: [SLM, WDV, OTHER] }
+ schema: { type: string, enum: [SLM, WDV, CUSTOM] }
- in: query
name: item_category_id
schema: { type: integer }
@@ -197,7 +197,7 @@ paths:
schema: { type: string }
- in: query
name: depreciation_method
- schema: { type: string, enum: [SLM, WDV, OTHER] }
+ schema: { type: string, enum: [SLM, WDV, CUSTOM] }
- in: query
name: item_category_id
schema: { type: integer }
diff --git a/src/modules/assets/assets.constants.js b/src/modules/assets/assets.constants.js
index 9c48714..bdca53e 100644
--- a/src/modules/assets/assets.constants.js
+++ b/src/modules/assets/assets.constants.js
@@ -16,7 +16,7 @@ const ASSET_STATUS_OPTIONS = toOptions([
['SCRAPPED', 'Scrapped'],
]);
-const DEPRECIATION_METHOD_VALUES = ['SLM', 'WDV', 'OTHER'];
+const DEPRECIATION_METHOD_VALUES = ['SLM', 'WDV', 'CUSTOM'];
const AMC_CONTRACT_TYPE_OPTIONS = toOptions([
['COMPREHENSIVE', 'Comprehensive'],
diff --git a/src/modules/assets/assets.depreciation.js b/src/modules/assets/assets.depreciation.js
index ab08bfa..64871d5 100644
--- a/src/modules/assets/assets.depreciation.js
+++ b/src/modules/assets/assets.depreciation.js
@@ -14,12 +14,57 @@ const DEPRECIATION_METHOD_OPTIONS = [
'Depreciation on reducing book value. Rate auto-calculated from cost, salvage and useful life when not provided.',
},
{
- value: 'OTHER',
- label: 'Other',
+ value: 'CUSTOM',
+ label: 'Custom',
description: 'Custom method. Depreciation rate must be entered manually.',
},
];
+/** Map legacy OTHER → CUSTOM for reads/writes. */
+const normalizeDepreciationMethod = (method) => {
+ if (method === 'OTHER') return 'CUSTOM';
+ return method || null;
+};
+
+/**
+ * Resolve salvage amount and % from either input.
+ * - If only salvage_percentage → derive salvage_value from purchase_cost
+ * - If only salvage_value → derive salvage_percentage
+ * - If both → prefer salvage_percentage and recompute salvage_value
+ * - If neither → both 0
+ */
+const resolveSalvageFields = ({
+ purchase_cost: cost,
+ salvage_value: salvageAmount,
+ salvage_percentage: salvagePct,
+} = {}) => {
+ const purchaseCost = Number(cost || 0);
+ const hasPct = salvagePct !== undefined && salvagePct !== null && salvagePct !== '';
+ const hasAmount =
+ salvageAmount !== undefined && salvageAmount !== null && salvageAmount !== '';
+
+ let salvagePercentage = 0;
+ let salvageValue = 0;
+
+ if (hasPct && !hasAmount) {
+ salvagePercentage = round4(Math.min(Math.max(Number(salvagePct), 0), 100));
+ salvageValue = purchaseCost > 0 ? round4((purchaseCost * salvagePercentage) / 100) : 0;
+ } else if (hasAmount && !hasPct) {
+ salvageValue = round4(Math.max(Number(salvageAmount), 0));
+ salvagePercentage =
+ purchaseCost > 0 ? round4(Math.min((salvageValue / purchaseCost) * 100, 100)) : 0;
+ } else if (hasPct && hasAmount) {
+ // Both sent: percentage is source of truth (FE amount/% toggle)
+ salvagePercentage = round4(Math.min(Math.max(Number(salvagePct), 0), 100));
+ salvageValue = purchaseCost > 0 ? round4((purchaseCost * salvagePercentage) / 100) : 0;
+ }
+
+ return {
+ salvage_value: salvageValue,
+ salvage_percentage: salvagePercentage,
+ };
+};
+
const yearsElapsed = (purchaseDate, asOfDate = new Date()) => {
if (!purchaseDate) return 0;
const start = new Date(purchaseDate);
@@ -43,16 +88,17 @@ const resolveDepreciationRate = ({
const purchaseCost = Number(cost || 0);
const salvageValue = Number(salvage || 0);
const years = Number(lifeYears || 0);
+ const normalizedMethod = normalizeDepreciationMethod(method);
- if (!method || method === 'OTHER' || years <= 0) return null;
+ if (!normalizedMethod || normalizedMethod === 'CUSTOM' || years <= 0) return null;
- if (method === 'SLM') {
+ if (normalizedMethod === 'SLM') {
if (purchaseCost <= 0) return round4(100 / years);
const depreciable = Math.max(purchaseCost - salvageValue, 0);
return round4((depreciable / purchaseCost / years) * 100);
}
- if (method === 'WDV') {
+ if (normalizedMethod === 'WDV') {
if (purchaseCost <= 0 || salvageValue <= 0 || salvageValue >= purchaseCost) return null;
return round4((1 - (salvageValue / purchaseCost) ** (1 / years)) * 100);
}
@@ -65,15 +111,23 @@ const calculateDepreciation = ({
depreciation_rate: rateInput,
purchase_cost: cost,
salvage_value: salvage,
+ salvage_percentage: salvagePct,
useful_life_years: lifeYears,
commencement_date: commencementDate,
purchase_date: purchaseDate,
as_of_date: asOfDate,
} = {}) => {
const purchaseCost = Number(cost || 0);
- const salvageValue = Number(salvage || 0);
+ const { salvage_value: salvageValue, salvage_percentage: salvagePercentage } =
+ resolveSalvageFields({
+ purchase_cost: purchaseCost,
+ salvage_value: salvage,
+ salvage_percentage: salvagePct,
+ });
+
+ const normalizedMethod = normalizeDepreciationMethod(method);
const rate = resolveDepreciationRate({
- method,
+ method: normalizedMethod,
depreciation_rate: rateInput,
purchase_cost: purchaseCost,
salvage_value: salvageValue,
@@ -91,12 +145,12 @@ const calculateDepreciation = ({
let accumulatedDepreciation = 0;
let bookValue = purchaseCost;
- if (method === 'SLM' && rate !== null) {
+ if (normalizedMethod === 'SLM' && rate !== null) {
annualDepreciation = round4((purchaseCost * rate) / 100);
const maxDepreciable = Math.max(purchaseCost - salvageValue, 0);
accumulatedDepreciation = round4(Math.min(annualDepreciation * cappedYears, maxDepreciable));
bookValue = round4(Math.max(purchaseCost - accumulatedDepreciation, salvageValue));
- } else if (method === 'WDV' && rate !== null) {
+ } else if (normalizedMethod === 'WDV' && rate !== null) {
annualDepreciation = round4((purchaseCost * rate) / 100);
bookValue = purchaseCost;
const fullYears = Math.floor(cappedYears);
@@ -116,7 +170,7 @@ const calculateDepreciation = ({
}
annualDepreciation = round4((bookValue * rate) / 100);
- } else if (method === 'OTHER' && rate !== null) {
+ } else if (normalizedMethod === 'CUSTOM' && rate !== null) {
annualDepreciation = round4((purchaseCost * rate) / 100);
const maxDepreciable = Math.max(purchaseCost - salvageValue, 0);
accumulatedDepreciation = round4(Math.min(annualDepreciation * cappedYears, maxDepreciable));
@@ -124,7 +178,7 @@ const calculateDepreciation = ({
}
return {
- depreciation_method: method || null,
+ depreciation_method: normalizedMethod,
depreciation_rate: rate,
annual_depreciation: annualDepreciation,
accumulated_depreciation: accumulatedDepreciation,
@@ -132,6 +186,7 @@ const calculateDepreciation = ({
years_elapsed: elapsed,
purchase_cost: purchaseCost,
salvage_value: salvageValue,
+ salvage_percentage: salvagePercentage,
useful_life_years: lifeYears ?? null,
depreciation_start_date: depreciationStartDate || null,
};
@@ -140,6 +195,8 @@ const calculateDepreciation = ({
module.exports = {
DEPRECIATION_METHOD_OPTIONS,
resolveDepreciationRate,
+ resolveSalvageFields,
+ normalizeDepreciationMethod,
calculateDepreciation,
yearsElapsed,
round4,
diff --git a/src/modules/assets/assets.service.js b/src/modules/assets/assets.service.js
index e97d60c..e7a2940 100644
--- a/src/modules/assets/assets.service.js
+++ b/src/modules/assets/assets.service.js
@@ -10,6 +10,8 @@ const repository = require('./assets.repository');
const {
DEPRECIATION_METHOD_OPTIONS,
resolveDepreciationRate,
+ resolveSalvageFields,
+ normalizeDepreciationMethod,
calculateDepreciation,
} = require('./assets.depreciation');
const { assertReference, toDateOnly, normalizeChecklistTemplate, presentChecklistTemplate } = require('./assets.helpers');
@@ -143,13 +145,24 @@ const sanitizeAsset = (asset) => {
rest.salvage_value !== null && rest.salvage_value !== undefined
? Number(rest.salvage_value)
: 0;
+ let salvagePercentage =
+ rest.salvage_percentage !== null && rest.salvage_percentage !== undefined
+ ? Number(rest.salvage_percentage)
+ : null;
+ if (salvagePercentage === null) {
+ salvagePercentage =
+ purchaseCost > 0 ? Math.round((salvageValue / purchaseCost) * 10000) / 10000 : 0;
+ }
const depreciationRate =
rest.depreciation_rate !== null && rest.depreciation_rate !== undefined
? Number(rest.depreciation_rate)
: null;
+ const depreciationMethod = normalizeDepreciationMethod(rest.depreciation_method);
+
+ // Use stored salvage_value for calc (do not re-derive from % on read)
const depreciation = calculateDepreciation({
- depreciation_method: rest.depreciation_method,
+ depreciation_method: depreciationMethod,
depreciation_rate: depreciationRate,
purchase_cost: purchaseCost,
salvage_value: salvageValue,
@@ -163,6 +176,8 @@ const sanitizeAsset = (asset) => {
maintenance_checklist_json: presentChecklistTemplate(rest.maintenance_checklist_json),
purchase_cost: purchaseCost,
salvage_value: salvageValue,
+ salvage_percentage: salvagePercentage,
+ depreciation_method: depreciationMethod,
depreciation_rate: depreciationRate,
// Current written-down / book value after depreciation (same as depreciation.book_value)
current_value: depreciation.book_value,
@@ -286,16 +301,27 @@ const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
assertDisposalFields(status, disposalDate);
const purchaseCost = payload.purchase_cost ?? 0;
- const salvageValue = payload.salvage_value ?? 0;
+ const { salvage_value: salvageValue, salvage_percentage: salvagePercentage } =
+ resolveSalvageFields({
+ purchase_cost: purchaseCost,
+ salvage_value: payload.salvage_value,
+ salvage_percentage: payload.salvage_percentage,
+ });
const usefulLifeYears =
payload.useful_life_years !== undefined && payload.useful_life_years !== null
? payload.useful_life_years
: category.default_useful_life_years;
- const depreciationMethod =
- payload.depreciation_method || category.default_depreciation_method || null;
+ const depreciationMethod = normalizeDepreciationMethod(
+ payload.depreciation_method || category.default_depreciation_method || null
+ );
- if (depreciationMethod === 'OTHER' && (payload.depreciation_rate === undefined || payload.depreciation_rate === null || payload.depreciation_rate === '')) {
- throw new ApiError(422, 'depreciation_rate is required when depreciation_method is OTHER');
+ if (
+ depreciationMethod === 'CUSTOM' &&
+ (payload.depreciation_rate === undefined ||
+ payload.depreciation_rate === null ||
+ payload.depreciation_rate === '')
+ ) {
+ throw new ApiError(422, 'depreciation_rate is required when depreciation_method is CUSTOM');
}
const depreciationRate = resolveDepreciationRate({
@@ -342,6 +368,7 @@ const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
depreciation_method: depreciationMethod,
depreciation_rate: depreciationRate,
salvage_value: salvageValue,
+ salvage_percentage: salvagePercentage,
warranty_expiry_date: payload.warranty_expiry_date
? toDateOnly(payload.warranty_expiry_date)
: null,
@@ -498,6 +525,8 @@ const exportAssets = async (query) => {
{ key: (row) => row.depreciation?.annual_depreciation ?? '', header: 'Annual Depreciation' },
{ key: (row) => row.depreciation?.depreciation_method || '', header: 'Depreciation Method' },
{ key: (row) => row.depreciation?.depreciation_rate ?? '', header: 'Depreciation Rate %' },
+ { key: 'salvage_value', header: 'Salvage Value' },
+ { key: 'salvage_percentage', header: 'Salvage %' },
{ key: 'warranty_expiry_date', header: 'Warranty Expiry', type: 'date' },
{
key: (row) => row.maintenance_incharge_user?.full_name || '',
@@ -574,7 +603,20 @@ const updateAsset = async (id, payload, userId, requestId) => {
: existing.depreciation_rate !== null && existing.depreciation_rate !== undefined
? Number(existing.depreciation_rate)
: null,
- salvage_value: payload.salvage_value ?? Number(existing.salvage_value ?? 0),
+ salvage_value:
+ payload.salvage_value !== undefined
+ ? payload.salvage_value
+ : payload.salvage_percentage !== undefined
+ ? undefined
+ : Number(existing.salvage_value ?? 0),
+ salvage_percentage:
+ payload.salvage_percentage !== undefined
+ ? payload.salvage_percentage
+ : payload.salvage_value !== undefined
+ ? undefined
+ : existing.salvage_percentage !== null && existing.salvage_percentage !== undefined
+ ? Number(existing.salvage_percentage)
+ : undefined,
warranty_expiry_date:
payload.warranty_expiry_date !== undefined
? payload.warranty_expiry_date
@@ -732,10 +774,14 @@ const listVisitConditionsAfter = () => getAssetDropdownOptions().visit_condition
const listAssetOptions = () => getAssetDropdownOptions();
const previewDepreciation = (payload) => {
- if (payload.depreciation_method === 'OTHER' && (payload.depreciation_rate === undefined || payload.depreciation_rate === null)) {
- throw new ApiError(422, 'depreciation_rate is required when depreciation_method is OTHER');
+ const method = normalizeDepreciationMethod(payload.depreciation_method);
+ if (
+ method === 'CUSTOM' &&
+ (payload.depreciation_rate === undefined || payload.depreciation_rate === null)
+ ) {
+ throw new ApiError(422, 'depreciation_rate is required when depreciation_method is CUSTOM');
}
- return calculateDepreciation(payload);
+ return calculateDepreciation({ ...payload, depreciation_method: method });
};
module.exports = {
diff --git a/src/modules/assets/assets.validation.js b/src/modules/assets/assets.validation.js
index f7700af..7b75c45 100644
--- a/src/modules/assets/assets.validation.js
+++ b/src/modules/assets/assets.validation.js
@@ -57,11 +57,12 @@ const assetFields = {
purchase_cost: Joi.number().min(0).default(0),
useful_life_years: Joi.number().integer().min(0).allow(null).optional(),
depreciation_method: Joi.string()
- .valid(...DEPRECIATION_METHODS)
+ .valid(...DEPRECIATION_METHODS, 'OTHER') // OTHER accepted as alias → stored as CUSTOM
.allow(null)
.optional(),
depreciation_rate: Joi.number().min(0).max(100).allow(null).optional(),
salvage_value: Joi.number().min(0).allow(null).optional(),
+ salvage_percentage: Joi.number().min(0).max(100).allow(null).optional(),
warranty_expiry_date: Joi.date().iso().allow(null).optional(),
condition: Joi.string()
.valid(...ASSET_CONDITIONS)
@@ -105,6 +106,7 @@ const updateAssetSchema = Joi.object({
depreciation_method: assetFields.depreciation_method,
depreciation_rate: assetFields.depreciation_rate,
salvage_value: assetFields.salvage_value,
+ salvage_percentage: assetFields.salvage_percentage,
warranty_expiry_date: assetFields.warranty_expiry_date,
condition: assetFields.condition.optional(),
status: assetFields.status.optional(),
@@ -304,11 +306,12 @@ const renewInsurancePolicySchema = Joi.object({
const depreciationCalculateSchema = Joi.object({
depreciation_method: Joi.string()
- .valid(...DEPRECIATION_METHODS)
+ .valid(...DEPRECIATION_METHODS, 'OTHER')
.required(),
depreciation_rate: Joi.number().min(0).max(100).allow(null).optional(),
purchase_cost: Joi.number().min(0).default(0),
- salvage_value: Joi.number().min(0).default(0),
+ salvage_value: Joi.number().min(0).allow(null).optional(),
+ salvage_percentage: Joi.number().min(0).max(100).allow(null).optional(),
useful_life_years: Joi.number().integer().min(0).allow(null).optional(),
commencement_date: Joi.date().iso().allow(null).optional(),
purchase_date: Joi.date().iso().allow(null).optional(),
diff --git a/src/modules/masters/item-categories/item-categories.service.js b/src/modules/masters/item-categories/item-categories.service.js
index 1225951..8e71566 100644
--- a/src/modules/masters/item-categories/item-categories.service.js
+++ b/src/modules/masters/item-categories/item-categories.service.js
@@ -1,4 +1,5 @@
const { buildMasterService } = require('../_shared/master.factory');
+const { normalizeDepreciationMethod } = require('../../assets/assets.depreciation');
const emptyToNull = (value) => (value === '' ? null : value);
@@ -75,7 +76,9 @@ const normalizeAssetDefaults = (payload) => {
out.code_prefix = emptyToNull(out.code_prefix);
}
if (Object.prototype.hasOwnProperty.call(out, 'default_depreciation_method')) {
- out.default_depreciation_method = emptyToNull(out.default_depreciation_method);
+ out.default_depreciation_method = normalizeDepreciationMethod(
+ emptyToNull(out.default_depreciation_method)
+ );
}
return out;
};
diff --git a/src/modules/masters/item-categories/item-categories.validation.js b/src/modules/masters/item-categories/item-categories.validation.js
index 228ec19..1c8431f 100644
--- a/src/modules/masters/item-categories/item-categories.validation.js
+++ b/src/modules/masters/item-categories/item-categories.validation.js
@@ -13,7 +13,7 @@ const createSchema = Joi.object({
code_prefix: masterCode({ max: 10 }).allow(null, ''),
default_useful_life_years: Joi.number().integer().min(0).allow(null).optional(),
default_depreciation_method: Joi.string()
- .valid(...DEPRECIATION_METHODS)
+ .valid(...DEPRECIATION_METHODS, 'OTHER')
.allow(null, '')
.optional(),
is_active: Joi.boolean().optional(),
@@ -29,7 +29,7 @@ const updateSchema = Joi.object({
code_prefix: masterCode({ max: 10 }).allow(null, ''),
default_useful_life_years: Joi.number().integer().min(0).allow(null).optional(),
default_depreciation_method: Joi.string()
- .valid(...DEPRECIATION_METHODS)
+ .valid(...DEPRECIATION_METHODS, 'OTHER')
.allow(null, '')
.optional(),
is_active: Joi.boolean().optional(),
diff --git a/src/modules/reports/reports-assets-depreciation.service.js b/src/modules/reports/reports-assets-depreciation.service.js
index 18d3878..270febd 100644
--- a/src/modules/reports/reports-assets-depreciation.service.js
+++ b/src/modules/reports/reports-assets-depreciation.service.js
@@ -11,10 +11,16 @@ const depreciationInclude = {
};
const buildDepreciationWhere = (query) => {
+ const methodFilter = query.depreciation_method
+ ? query.depreciation_method === 'CUSTOM' || query.depreciation_method === 'OTHER'
+ ? { depreciation_method: { in: ['CUSTOM', 'OTHER'] } }
+ : { depreciation_method: query.depreciation_method }
+ : {};
+
const where = {
deleted_at: null,
...(query.status ? { status: query.status } : {}),
- ...(query.depreciation_method ? { depreciation_method: query.depreciation_method } : {}),
+ ...methodFilter,
...(query.item_category_id ? { item_category_id: BigInt(query.item_category_id) } : {}),
...(query.item_subcategory_id
? { item_subcategory_id: BigInt(query.item_subcategory_id) }
@@ -70,8 +76,14 @@ const sanitizeDepreciationRow = (asset, asOfDate) => {
purchase_date: asset.purchase_date,
purchase_cost: purchaseCost,
salvage_value: salvageValue,
+ salvage_percentage:
+ asset.salvage_percentage !== null && asset.salvage_percentage !== undefined
+ ? toNumber(asset.salvage_percentage)
+ : purchaseCost > 0
+ ? round4((salvageValue / purchaseCost) * 100)
+ : 0,
useful_life_years: asset.useful_life_years,
- depreciation_method: asset.depreciation_method,
+ depreciation_method: asset.depreciation_method === 'OTHER' ? 'CUSTOM' : asset.depreciation_method,
depreciation_rate: depreciationRate,
item_category: asset.item_categories || null,
item_subcategory: asset.item_subcategories || null,
@@ -174,7 +186,7 @@ const getDepreciationFilterOptions = async () => {
depreciation_methods: [
{ value: 'SLM', label: 'Straight Line Method (SLM)' },
{ value: 'WDV', label: 'Written Down Value (WDV)' },
- { value: 'OTHER', label: 'Other' },
+ { value: 'CUSTOM', label: 'Custom' },
],
statuses: [
{ value: 'IN_USE', label: 'In Use' },
@@ -209,6 +221,7 @@ const exportAssetDepreciation = async (query) => {
{ key: 'purchase_date', header: 'Purchase Date', type: 'date' },
{ key: 'purchase_cost', header: 'Purchase Cost' },
{ key: 'salvage_value', header: 'Salvage Value' },
+ { key: 'salvage_percentage', header: 'Salvage %' },
{ key: (row) => row.useful_life_years ?? '', header: 'Useful Life (Years)' },
{ key: (row) => row.depreciation_method || '', header: 'Depreciation Method' },
{ key: (row) => row.depreciation_rate ?? '', header: 'Depreciation Rate (%)' },
diff --git a/src/modules/reports/reports.validation.js b/src/modules/reports/reports.validation.js
index d21d189..9a7f1f0 100644
--- a/src/modules/reports/reports.validation.js
+++ b/src/modules/reports/reports.validation.js
@@ -9,7 +9,7 @@ const depreciationReportQuerySchema = Joi.object({
status: Joi.string()
.valid(...ASSET_STATUSES)
.optional(),
- depreciation_method: Joi.string().valid('SLM', 'WDV', 'OTHER').optional(),
+ depreciation_method: Joi.string().valid('SLM', 'WDV', 'CUSTOM', 'OTHER').optional(),
item_category_id: Joi.number().integer().positive().optional(),
item_subcategory_id: Joi.number().integer().positive().optional(),
location_id: Joi.number().integer().positive().optional(),