Compare commits

...

10 Commits

Author SHA1 Message Date
b8460d1ae6 Merge branch 'main' of bitbucket.org:bharatconsumer/erp_be 2026-07-28 16:06:22 +05:30
2a3bac2d69 File name changes 2026-07-28 16:05:13 +05:30
VENBA_NEW_SERVER
e05bfbb537 Merge branch 'main' of bitbucket.org:bharatconsumer/erp_be 2026-07-28 07:18:50 +00:00
VENBA_NEW_SERVER
f4001a742b Check 2026-07-28 07:17:56 +00:00
668a214b64 Test commit : GWM 2026-07-28 12:44:35 +05:30
8b84928473 Test commit : GWM 2026-07-28 12:23:05 +05:30
36b5170c53 salvage_percentage new column added 2026-07-24 12:24:16 +05:30
b4141b9b5b assets list api changes 2026-07-24 11:48:07 +05:30
7516354ce5 Reject reason added for PO 2026-07-22 18:39:00 +05:30
70deacb969 location export fixed 2026-07-22 17:29:42 +05:30
25 changed files with 457 additions and 82 deletions

View File

@ -239,7 +239,7 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL
| [x] | DELETE | `/purchase-orders/:id` | delete | Soft delete |
| [x] | POST | `/purchase-orders/:id/submit` | edit | Submit for approval |
| [x] | POST | `/purchase-orders/:id/approve` | approve | Approve PO |
| [x] | POST | `/purchase-orders/:id/reject` | approve | Reject PO |
| [x] | POST | `/purchase-orders/:id/reject` | approve | Reject PO (`reject_reason` required; stored on PO) |
| [x] | POST | `/purchase-orders/:id/amend` | edit | Amend PO |
| [x] | POST | `/purchase-orders/:id/cancel` | edit | Cancel PO |
| [x] | GET | `/purchase-orders/:id/pdf` | view | PDF export |

View File

@ -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,11 +888,11 @@ 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]
CREATE --> STORE[(assets: method, rate, cost, salvage, life, purchase_date)]
CREATE --> STORE[(assets: method, rate, cost, salvage, life, commencement/purchase date)]
STORE --> READ["GET /assets/:id"]
READ --> CALC
@ -900,22 +900,96 @@ flowchart TD
SLM --> CALC
WDV --> CALC
OTHER --> CALC
CUSTOM --> CALC
</pre>
</div>
<h3>Input fields used in calculation</h3>
<table>
<thead><tr><th>Method</th><th>Rate auto-calc?</th><th>Formula concept</th></tr></thead>
<thead><tr><th>Field</th><th>Meaning</th><th>Role in formula</th></tr></thead>
<tbody>
<tr><td><strong>SLM</strong></td><td>Yes (if rate omitted)</td><td>Equal yearly depreciation on original cost</td></tr>
<tr><td><strong>WDV</strong></td><td>Yes (if rate omitted)</td><td>Depreciation on reducing book value year-by-year</td></tr>
<tr><td><strong>OTHER</strong></td><td>No — user must send <code>depreciation_rate</code></td><td>Custom % on cost</td></tr>
<tr><td><code>purchase_cost</code></td><td>Asset cost / amount</td><td>Starting book value; base for rate &amp; annual dep</td></tr>
<tr><td><code>salvage_value</code></td><td>Residual value at end of life (amount)</td><td>Floor for book value; used in rate auto-calc</td></tr>
<tr><td><code>salvage_percentage</code></td><td>Residual as % of purchase_cost (0100)</td><td>Synced with amount; FE may send either</td></tr>
<tr><td><code>useful_life_years</code></td><td>Expected life in years</td><td>Rate auto-calc + cap on years elapsed</td></tr>
<tr><td><code>commencement_date</code></td><td>Usage / put-to-use date</td><td><strong>Primary</strong> start date for years elapsed</td></tr>
<tr><td><code>purchase_date</code></td><td>Purchase date</td><td>Fallback start date if commencement is null</td></tr>
<tr><td><code>depreciation_method</code></td><td><code>SLM</code> / <code>WDV</code> / <code>CUSTOM</code></td><td>Which formula path to run</td></tr>
<tr><td><code>depreciation_rate</code></td><td>% per year</td><td>Optional for SLM/WDV (auto); required for CUSTOM</td></tr>
<tr><td><code>as_of_date</code></td><td>Calculate as of (preview only)</td><td>Defaults to today</td></tr>
</tbody>
</table>
<h3>Date → years elapsed</h3>
<pre><code>depreciation_start_date = commencement_date || purchase_date
years_elapsed = (as_of_date depreciation_start_date) / 365.25 days
capped_years = min(years_elapsed, useful_life_years) // if life is set
</code></pre>
<p>Accumulated depreciation uses <code>capped_years</code> so it never exceeds useful life.</p>
<h3>Method 1 — SLM (Straight Line)</h3>
<p>Equal depreciation every year on <strong>original cost</strong>, never below salvage.</p>
<p><strong>Auto rate</strong> (when <code>depreciation_rate</code> omitted):</p>
<pre><code>depreciable_amount = max(purchase_cost salvage_value, 0)
depreciation_rate = (depreciable_amount / purchase_cost / useful_life_years) × 100
</code></pre>
<p><strong>Amounts:</strong></p>
<pre><code>annual_depreciation = purchase_cost × rate / 100
accumulated_depreciation = min(annual_depreciation × capped_years, purchase_cost salvage_value)
book_value = max(purchase_cost accumulated_depreciation, salvage_value)
</code></pre>
<p><strong>Example:</strong> cost = ₹1,00,000 · salvage = ₹10,000 · life = 10 years · start = 1 year ago</p>
<ul>
<li>Rate = ((10000010000)/100000/10)×100 = <strong>9%</strong></li>
<li>Annual = 100000 × 9% = <strong>₹9,000</strong></li>
<li>Accumulated (1 yr) = <strong>₹9,000</strong></li>
<li>Book value = <strong>₹91,000</strong></li>
</ul>
<h3>Method 2 — WDV (Written Down Value)</h3>
<p>Depreciation each year on the <strong>reducing book value</strong> (not original cost). Book value never goes below salvage.</p>
<p><strong>Auto rate</strong> (when <code>depreciation_rate</code> omitted; needs salvage &gt; 0 and &lt; cost):</p>
<pre><code>depreciation_rate = (1 (salvage_value / purchase_cost)^(1 / useful_life_years)) × 100
</code></pre>
<p><strong>Amounts (year-by-year):</strong></p>
<pre><code>book_value = purchase_cost
for each full year in capped_years:
year_dep = book_value × rate / 100
book_value = max(book_value year_dep, salvage_value)
if fractional year remains:
year_dep = (book_value × rate / 100) × fraction
book_value = max(book_value year_dep, salvage_value)
accumulated_depreciation = purchase_cost book_value
annual_depreciation = book_value × rate / 100 // next year's dep on current WDV
</code></pre>
<p><strong>Example:</strong> cost = ₹1,00,000 · salvage = ₹10,000 · life = 10 years · start = 1 year ago</p>
<ul>
<li>Rate ≈ (1 (10000/100000)^(1/10)) × 100 ≈ <strong>20.57%</strong></li>
<li>Year 1 dep ≈ 100000 × 20.57% ≈ <strong>₹20,570</strong></li>
<li>Book value ≈ <strong>₹79,430</strong></li>
<li>Accumulated ≈ <strong>₹20,570</strong></li>
<li>Next annual (on WDV) ≈ 79430 × 20.57% ≈ <strong>₹16,340</strong></li>
</ul>
<h3>CUSTOM method</h3>
<p>Same accumulation style as SLM, but <code>depreciation_rate</code> is <strong>mandatory</strong> (no auto-calc).</p>
<table>
<thead><tr><th>Method</th><th>Rate auto-calc?</th><th>Depreciates on</th><th>Salvage role</th></tr></thead>
<tbody>
<tr><td><strong>SLM</strong></td><td>Yes (if rate omitted)</td><td>Original <code>purchase_cost</code> every year</td><td>In rate formula + book-value floor</td></tr>
<tr><td><strong>WDV</strong></td><td>Yes (if rate omitted)</td><td>Reducing book value each year</td><td>In rate formula + book-value floor</td></tr>
<tr><td><strong>CUSTOM</strong></td><td>No — send <code>depreciation_rate</code></td><td>Original cost × rate (like SLM)</td><td>Book-value floor only</td></tr>
</tbody>
</table>
<p><strong>FE usage:</strong></p>
<ul>
<li><strong>Live preview</strong><code>POST /assets/depreciation/calculate</code> (no save)</li>
<li><strong>Saved asset view</strong><code>GET /assets/:id</code><code>data.depreciation</code> object</li>
<li><strong>Method dropdown</strong><code>GET /assets/depreciation-methods</code></li>
<li>Prefer sending <code>commencement_date</code>; backend falls back to <code>purchase_date</code></li>
</ul>
<h2 id="s12">12. Alerts flow (cross-asset dashboards)</h2>

View File

@ -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,11 +497,11 @@ 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]
CREATE --> STORE[(assets: method, rate, cost, salvage, life, purchase_date)]
CREATE --> STORE[(assets: method, rate, cost, salvage, life, commencement/purchase date)]
STORE --> READ["GET /assets/:id"]
READ --> CALC
@ -509,20 +509,108 @@ flowchart TD
SLM --> CALC
WDV --> CALC
OTHER --> CALC
CUSTOM --> CALC
```
| Method | Rate auto-calc? | Formula concept |
### Input fields used in calculation
| Field | Meaning | Role in formula |
|---|---|---|
| **SLM** | Yes (if rate omitted) | Equal yearly depreciation on original cost |
| **WDV** | Yes (if rate omitted) | Depreciation on reducing book value year-by-year |
| **OTHER** | No — user must send `depreciation_rate` | Custom % on cost |
| `purchase_cost` | Asset cost / amount | Starting book value; base for rate & annual dep |
| `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 (0100) | 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` / `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
```
depreciation_start_date = commencement_date || purchase_date
years_elapsed = (as_of_date depreciation_start_date) / 365.25 days
capped_years = min(years_elapsed, useful_life_years) // if life is set
```
Accumulated depreciation uses `capped_years` so it never exceeds useful life.
### Method 1 — SLM (Straight Line)
Equal depreciation every year on **original cost**, never below salvage.
**Auto rate** (when `depreciation_rate` omitted):
```
depreciable_amount = max(purchase_cost salvage_value, 0)
depreciation_rate = (depreciable_amount / purchase_cost / useful_life_years) × 100
```
**Amounts:**
```
annual_depreciation = purchase_cost × rate / 100
accumulated_depreciation = min(annual_depreciation × capped_years, purchase_cost salvage_value)
book_value = max(purchase_cost accumulated_depreciation, salvage_value)
```
**Example:** cost = ₹1,00,000 · salvage = ₹10,000 · life = 10 years · start = 1 year ago
- Rate = ((10000010000)/100000/10)×100 = **9%**
- Annual = 100000 × 9% = **₹9,000**
- Accumulated (1 yr) = **₹9,000**
- Book value = **₹91,000**
### Method 2 — WDV (Written Down Value)
Depreciation each year on the **reducing book value**. Book value never goes below salvage.
**Auto rate** (when `depreciation_rate` omitted; needs salvage &gt; 0 and &lt; cost):
```
depreciation_rate = (1 (salvage_value / purchase_cost)^(1 / useful_life_years)) × 100
```
**Amounts (year-by-year):**
```
book_value = purchase_cost
for each full year in capped_years:
year_dep = book_value × rate / 100
book_value = max(book_value year_dep, salvage_value)
if fractional year remains:
year_dep = (book_value × rate / 100) × fraction
book_value = max(book_value year_dep, salvage_value)
accumulated_depreciation = purchase_cost book_value
annual_depreciation = book_value × rate / 100 // next year's dep on current WDV
```
**Example:** cost = ₹1,00,000 · salvage = ₹10,000 · life = 10 years · start = 1 year ago
- Rate ≈ (1 (10000/100000)^(1/10)) × 100 ≈ **20.57%**
- Year 1 dep ≈ 100000 × 20.57% ≈ **₹20,570**
- Book value ≈ **₹79,430**
- Accumulated ≈ **₹20,570**
- Next annual (on WDV) ≈ 79430 × 20.57% ≈ **₹16,340**
### CUSTOM method
Same accumulation style as SLM, but `depreciation_rate` is **mandatory** (no auto-calc).
| Method | Rate auto-calc? | Depreciates on | Salvage role |
|---|---|---|---|
| **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 |
| **CUSTOM** | No — send `depreciation_rate` | Original cost × rate (like SLM) | Book-value floor only |
**FE usage:**
- **Live preview**`POST /assets/depreciation/calculate` (no save)
- **Saved asset view**`GET /assets/:id``data.depreciation` object
- **Method dropdown**`GET /assets/depreciation-methods`
- Prefer sending `commencement_date`; backend falls back to `purchase_date`
---

27
package-lock.json generated
View File

@ -23,6 +23,7 @@
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.1",
"multer": "^1.4.5-lts.2",
"nodemailer": "^6.10.1",
"puppeteer": "^25.2.0",
"swagger-jsdoc": "6.2.8",
"swagger-ui-express": "^5.0.1",
@ -1323,13 +1324,13 @@
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
"integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==",
"dev": true
"devOptional": true
},
"node_modules/@prisma/engines": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz",
"integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==",
"dev": true,
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/debug": "5.22.0",
@ -1342,13 +1343,13 @@
"version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz",
"integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==",
"dev": true
"devOptional": true
},
"node_modules/@prisma/fetch-engine": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz",
"integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==",
"dev": true,
"devOptional": true,
"dependencies": {
"@prisma/debug": "5.22.0",
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
@ -1359,7 +1360,7 @@
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz",
"integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==",
"dev": true,
"devOptional": true,
"dependencies": {
"@prisma/debug": "5.22.0"
}
@ -5712,6 +5713,14 @@
"node": ">=18"
}
},
"node_modules/nodemailer": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nodemon": {
"version": "3.1.14",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
@ -5944,6 +5953,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"peer": true
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -6244,7 +6259,7 @@
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz",
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
"dev": true,
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/engines": "5.22.0"

View File

@ -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)
@ -711,6 +712,7 @@ model purchase_orders {
parent_po_id BigInt?
terms_and_conditions String?
remarks String?
reject_reason String?
is_active Boolean @default(true)
created_by BigInt?
updated_by BigInt?

View File

@ -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 (0100). 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';

View File

@ -0,0 +1,4 @@
-- Persist PO rejection reason on purchase_orders header
ALTER TABLE purchase_orders
ADD COLUMN IF NOT EXISTS reject_reason TEXT;

View File

@ -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 (0100). 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 (0100)' }
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
@ -509,6 +512,9 @@ paths:
get:
tags: [Assets]
summary: List assets
description: >
Each item includes purchase_cost, current_value (book value after depreciation),
and a depreciation summary object (method, rate, annual, accumulated, book_value, years_elapsed).
parameters:
- { name: page, in: query, schema: { type: integer } }
- { name: limit, in: query, schema: { type: integer } }

View File

@ -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

View File

@ -67,9 +67,10 @@ components:
remarks: { type: string, example: 'Approved for procurement' }
PurchaseOrdersRejectBody:
type: object
required: [remarks]
required: [reject_reason]
properties:
remarks: { type: string, example: 'Rates not competitive' }
reject_reason: { type: string, example: 'Rates not competitive' }
remarks: { type: string, example: 'Rates not competitive', description: 'Legacy alias for reject_reason' }
PoAttachmentResponse:
type: object
properties:
@ -247,6 +248,7 @@ paths:
post:
tags: [Purchase Orders]
summary: Reject purchase order
description: Requires reject_reason. Stored on purchase_orders.reject_reason and cleared on resubmit/approve.
parameters:
- { name: id, in: path, required: true, schema: { type: string, example: '1' } }
requestBody:

View File

@ -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 }

View File

@ -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'],

View File

@ -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,

View File

@ -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,17 +145,42 @@ 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: depreciationMethod,
depreciation_rate: depreciationRate,
purchase_cost: purchaseCost,
salvage_value: salvageValue,
useful_life_years: rest.useful_life_years,
commencement_date: rest.commencement_date,
purchase_date: rest.purchase_date,
});
return {
...rest,
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,
item_category: item_categories || null,
item_subcategory: item_subcategories || null,
location: location || null,
@ -167,15 +194,10 @@ const sanitizeAsset = (asset) => {
created_by_user: users_assets_created_byTousers || null,
updated_by_user: users_assets_updated_byTousers || null,
attachments: (asset_attachments || []).map(sanitizeAttachment),
depreciation: calculateDepreciation({
depreciation_method: rest.depreciation_method,
depreciation_rate: depreciationRate,
purchase_cost: purchaseCost,
salvage_value: salvageValue,
useful_life_years: rest.useful_life_years,
commencement_date: rest.commencement_date,
purchase_date: rest.purchase_date,
}),
depreciation: {
...depreciation,
current_value: depreciation.book_value,
},
item_categories: undefined,
item_subcategories: undefined,
departments: undefined,
@ -279,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({
@ -335,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,
@ -486,6 +520,13 @@ const exportAssets = async (query) => {
{ key: 'commencement_date', header: 'Commencement Date', type: 'date' },
{ key: 'purchase_date', header: 'Purchase Date', type: 'date' },
{ key: 'purchase_cost', header: 'Purchase Cost' },
{ key: 'current_value', header: 'Current Value' },
{ key: (row) => row.depreciation?.accumulated_depreciation ?? '', header: 'Accumulated Depreciation' },
{ 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 || '',
@ -562,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
@ -720,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 = {

View File

@ -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(),

View File

@ -17,7 +17,7 @@ const list = asyncHandler(async (req, res) => {
const exportCsv = asyncHandler(async (req, res) => {
const csv = await service.exportGrns(req.query);
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename="grn-export.csv"');
res.setHeader('Content-Disposition', 'attachment; filename="pr-export.csv"');
res.send(csv);
});

View File

@ -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;
};

View File

@ -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(),

View File

@ -166,8 +166,11 @@ const exportLocations = async (query, type = null) => {
{ key: 'code', header: 'Code' },
{ key: 'name', header: 'Name' },
{ key: 'gstin', header: 'GSTIN' },
{ key: 'address', header: 'Address' },
{ key: 'city', header: 'City' },
{ key: 'state', header: 'State' },
{ key: 'pincode', header: 'Pincode' },
{ key: 'phone', header: 'Phone' },
{ key: 'is_active', header: 'Is Active' },
{ key: 'created_at', header: 'Created At', type: 'datetime' },
]
@ -175,6 +178,13 @@ const exportLocations = async (query, type = null) => {
{ key: 'type', header: 'Type' },
{ key: 'code', header: 'Code' },
{ key: 'name', header: 'Name' },
{ key: 'gstin', header: 'GSTIN' },
{ key: 'address', header: 'Address' },
{ key: 'city', header: 'City' },
{ key: 'state', header: 'State' },
{ key: 'pincode', header: 'Pincode' },
{ key: 'phone', header: 'Phone' },
{ key: 'location', header: 'Location' },
{ key: 'is_active', header: 'Is Active' },
{ key: 'created_at', header: 'Created At', type: 'datetime' },
];

View File

@ -3,10 +3,12 @@ const TEMPLATE_CODES = {
FORGOT_PASSWORD: 'FORGOT_PASSWORD',
};
const CHANNELS = {
EMAIL: 'EMAIL',
};
module.exports = {
TEMPLATE_CODES,
CHANNELS,

View File

@ -697,6 +697,7 @@ const submitPurchaseOrder = async (id, payload, userId, requestId) => {
where: { id: BigInt(id) },
data: {
status: 'PENDING_APPROVAL',
reject_reason: null,
remarks: payload.remarks ?? existing.remarks,
updated_by: userId ? BigInt(userId) : null,
},
@ -739,6 +740,7 @@ const approvePurchaseOrder = async (id, payload, userId, requestId) => {
where: { id: BigInt(id) },
data: {
status: 'APPROVED',
reject_reason: null,
updated_by: userId ? BigInt(userId) : null,
},
include: poDetailInclude,
@ -765,12 +767,15 @@ const rejectPurchaseOrder = async (id, payload, userId, requestId) => {
const pendingApproval = existing.po_approvals.find((row) => row.status === 'PENDING');
if (!pendingApproval) throw new ApiError(409, 'No pending approval step found');
const rejectReason = String(payload.reject_reason || payload.remarks || '').trim();
if (!rejectReason) throw new ApiError(422, 'reject_reason is required');
const updated = await prisma.$transaction(async (tx) => {
await tx.po_approvals.update({
where: { id: pendingApproval.id },
data: {
status: 'REJECTED',
remarks: payload.remarks,
remarks: rejectReason,
approver_user_id: userId ? BigInt(userId) : null,
acted_at: new Date(),
},
@ -780,6 +785,7 @@ const rejectPurchaseOrder = async (id, payload, userId, requestId) => {
where: { id: BigInt(id) },
data: {
status: 'REJECTED',
reject_reason: rejectReason,
updated_by: userId ? BigInt(userId) : null,
},
include: poDetailInclude,

View File

@ -111,7 +111,12 @@ const workflowRemarksSchema = Joi.object({
});
const rejectPurchaseOrderSchema = Joi.object({
remarks: Joi.string().trim().min(1).required(),
reject_reason: Joi.string().trim().min(1).optional(),
remarks: Joi.string().trim().min(1).optional(), // legacy alias for reject_reason
})
.or('reject_reason', 'remarks')
.messages({
'object.missing': 'reject_reason is required',
});
module.exports = {

View File

@ -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 (%)' },

View File

@ -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(),

View File

@ -403,7 +403,7 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => {
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Goods Receipt Note ${escapeHtml(grn.grn_number || '')}</title>
<title>Purchase Receipt ${escapeHtml(grn.grn_number || '')}</title>
<style>${sharedDocumentStyles()}</style>
</head>
<body>
@ -424,7 +424,7 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => {
</div>
</div>
<div class="title-block">
<h2 class="doc-title">Goods Receipt Note</h2>
<h2 class="doc-title">Purchase Receipt</h2>
<div class="doc-number">${escapeHtml(grn.grn_number || '-')}</div>
</div>
</div>
@ -533,7 +533,7 @@ const generateGrnHtml = (inputData = getDummyGrnData()) => {
</div>
<div class="footer">
Generated ${escapeHtml(generatedAt)} · This is a system-generated goods receipt note and does not require a physical stamp.
Generated ${escapeHtml(generatedAt)} · This is a system-generated purchase receipt and does not require a physical stamp.
</div>
</div>
</body>