GWM : depreciation report
This commit is contained in:
parent
0a52e1202c
commit
44c920d0c5
20
scripts/patch-reports-module.sql
Normal file
20
scripts/patch-reports-module.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
-- Reports module — RBAC for read-only report APIs
|
||||||
|
|
||||||
|
INSERT INTO modules (code, name, sort_order, is_active)
|
||||||
|
VALUES ('REPORTS', 'Reports', 90, true)
|
||||||
|
ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name, sort_order = EXCLUDED.sort_order, is_active = true;
|
||||||
|
|
||||||
|
INSERT INTO permissions (module_id, action, is_active)
|
||||||
|
SELECT m.id, a.action, true
|
||||||
|
FROM modules m
|
||||||
|
CROSS JOIN (VALUES ('view'), ('export')) AS a(action)
|
||||||
|
WHERE m.code = 'REPORTS'
|
||||||
|
ON CONFLICT (module_id, action) DO UPDATE SET is_active = true;
|
||||||
|
|
||||||
|
INSERT INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM roles r
|
||||||
|
JOIN permissions p ON p.action IN ('view', 'export')
|
||||||
|
JOIN modules m ON m.id = p.module_id AND m.code = 'REPORTS'
|
||||||
|
WHERE r.name = 'Super Admin' AND r.deleted_at IS NULL
|
||||||
|
ON CONFLICT (role_id, permission_id) DO NOTHING;
|
||||||
245
src/docs/reports-routes.yaml
Normal file
245
src/docs/reports-routes.yaml
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
tags:
|
||||||
|
- name: Reports
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
DepreciationSummary:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
asset_count: { type: integer, example: 42 }
|
||||||
|
total_purchase_cost: { type: number, example: 1250000 }
|
||||||
|
total_accumulated_depreciation: { type: number, example: 320000 }
|
||||||
|
total_book_value: { type: number, example: 930000 }
|
||||||
|
total_annual_depreciation: { type: number, example: 85000 }
|
||||||
|
AssetDepreciationReportItem:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id: { type: string, example: "15" }
|
||||||
|
asset_code: { type: string, example: IT-LAP-00012 }
|
||||||
|
asset_name: { type: string, example: Dell Latitude 5540 }
|
||||||
|
status: { type: string, example: IN_USE }
|
||||||
|
purchase_date: { type: string, format: date, nullable: true }
|
||||||
|
purchase_cost: { type: number, example: 75000 }
|
||||||
|
salvage_value: { type: number, example: 5000 }
|
||||||
|
useful_life_years: { type: integer, nullable: true, example: 5 }
|
||||||
|
depreciation_method: { type: string, nullable: true, example: SLM }
|
||||||
|
depreciation_rate: { type: number, nullable: true, example: 17.3333 }
|
||||||
|
item_category:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
properties:
|
||||||
|
id: { type: string }
|
||||||
|
code: { type: string }
|
||||||
|
name: { type: string }
|
||||||
|
item_subcategory:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
properties:
|
||||||
|
id: { type: string }
|
||||||
|
code: { type: string }
|
||||||
|
name: { type: string }
|
||||||
|
plant:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
properties:
|
||||||
|
id: { type: string }
|
||||||
|
code: { type: string }
|
||||||
|
name: { type: string }
|
||||||
|
department:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
properties:
|
||||||
|
id: { type: string }
|
||||||
|
name: { type: string }
|
||||||
|
depreciation:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
depreciation_method: { type: string, nullable: true }
|
||||||
|
depreciation_rate: { type: number, nullable: true }
|
||||||
|
annual_depreciation: { type: number }
|
||||||
|
accumulated_depreciation: { type: number }
|
||||||
|
book_value: { type: number }
|
||||||
|
years_elapsed: { type: number }
|
||||||
|
purchase_cost: { type: number }
|
||||||
|
salvage_value: { type: number }
|
||||||
|
useful_life_years: { type: integer, nullable: true }
|
||||||
|
DepreciationReportFilterOptions:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
plants:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id: { type: string }
|
||||||
|
code: { type: string }
|
||||||
|
name: { type: string }
|
||||||
|
departments:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id: { type: string }
|
||||||
|
name: { type: string }
|
||||||
|
item_categories:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id: { type: string }
|
||||||
|
code: { type: string }
|
||||||
|
name: { type: string }
|
||||||
|
item_subcategories:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id: { type: string }
|
||||||
|
code: { type: string }
|
||||||
|
name: { type: string }
|
||||||
|
item_category_id: { type: string }
|
||||||
|
depreciation_methods:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
value: { type: string }
|
||||||
|
label: { type: string }
|
||||||
|
statuses:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
value: { type: string }
|
||||||
|
label: { type: string }
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/reports/assets/depreciation/filters:
|
||||||
|
get:
|
||||||
|
tags: [Reports]
|
||||||
|
summary: Filter dropdowns for asset depreciation report
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Filter options fetched
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/ApiResponse"
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data: { $ref: "#/components/schemas/DepreciationReportFilterOptions" }
|
||||||
|
/reports/assets/depreciation/export:
|
||||||
|
get:
|
||||||
|
tags: [Reports]
|
||||||
|
summary: Export asset depreciation report as CSV
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: search
|
||||||
|
schema: { type: string }
|
||||||
|
- in: query
|
||||||
|
name: status
|
||||||
|
schema: { type: string }
|
||||||
|
- in: query
|
||||||
|
name: depreciation_method
|
||||||
|
schema: { type: string, enum: [SLM, WDV, OTHER] }
|
||||||
|
- in: query
|
||||||
|
name: item_category_id
|
||||||
|
schema: { type: integer }
|
||||||
|
- in: query
|
||||||
|
name: item_subcategory_id
|
||||||
|
schema: { type: integer }
|
||||||
|
- in: query
|
||||||
|
name: plant_id
|
||||||
|
schema: { type: integer }
|
||||||
|
- in: query
|
||||||
|
name: department_id
|
||||||
|
schema: { type: integer }
|
||||||
|
- in: query
|
||||||
|
name: is_active
|
||||||
|
schema: { type: boolean }
|
||||||
|
- in: query
|
||||||
|
name: as_of_date
|
||||||
|
schema: { type: string, format: date }
|
||||||
|
description: Depreciation calculation date (defaults to today)
|
||||||
|
- in: query
|
||||||
|
name: purchase_date_from
|
||||||
|
schema: { type: string, format: date }
|
||||||
|
- in: query
|
||||||
|
name: purchase_date_to
|
||||||
|
schema: { type: string, format: date }
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: CSV export
|
||||||
|
content:
|
||||||
|
text/csv:
|
||||||
|
schema: { type: string }
|
||||||
|
/reports/assets/depreciation:
|
||||||
|
get:
|
||||||
|
tags: [Reports]
|
||||||
|
summary: Paginated asset depreciation report
|
||||||
|
description: |
|
||||||
|
Lists assets with computed depreciation (annual, accumulated, book value).
|
||||||
|
`meta.summary` aggregates totals across all rows matching the current filters.
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: page
|
||||||
|
schema: { type: integer, default: 1 }
|
||||||
|
- in: query
|
||||||
|
name: limit
|
||||||
|
schema: { type: integer, default: 20, maximum: 100 }
|
||||||
|
- in: query
|
||||||
|
name: search
|
||||||
|
schema: { type: string }
|
||||||
|
description: Search asset code, name, or serial number
|
||||||
|
- in: query
|
||||||
|
name: status
|
||||||
|
schema: { type: string }
|
||||||
|
- in: query
|
||||||
|
name: depreciation_method
|
||||||
|
schema: { type: string, enum: [SLM, WDV, OTHER] }
|
||||||
|
- in: query
|
||||||
|
name: item_category_id
|
||||||
|
schema: { type: integer }
|
||||||
|
- in: query
|
||||||
|
name: item_subcategory_id
|
||||||
|
schema: { type: integer }
|
||||||
|
- in: query
|
||||||
|
name: plant_id
|
||||||
|
schema: { type: integer }
|
||||||
|
- in: query
|
||||||
|
name: department_id
|
||||||
|
schema: { type: integer }
|
||||||
|
- in: query
|
||||||
|
name: is_active
|
||||||
|
schema: { type: boolean }
|
||||||
|
- in: query
|
||||||
|
name: as_of_date
|
||||||
|
schema: { type: string, format: date }
|
||||||
|
- in: query
|
||||||
|
name: purchase_date_from
|
||||||
|
schema: { type: string, format: date }
|
||||||
|
- in: query
|
||||||
|
name: purchase_date_to
|
||||||
|
schema: { type: string, format: date }
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Asset depreciation report
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: "#/components/schemas/ApiResponse"
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items: { $ref: "#/components/schemas/AssetDepreciationReportItem" }
|
||||||
|
meta:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
page: { type: integer }
|
||||||
|
limit: { type: integer }
|
||||||
|
total: { type: integer }
|
||||||
|
as_of_date: { type: string, format: date-time }
|
||||||
|
summary: { $ref: "#/components/schemas/DepreciationSummary" }
|
||||||
256
src/modules/reports/reports-assets-depreciation.service.js
Normal file
256
src/modules/reports/reports-assets-depreciation.service.js
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
const prisma = require('../../config/prisma');
|
||||||
|
const { getPagination } = require('../../utils/pagination');
|
||||||
|
const { calculateDepreciation, round4 } = require('../assets/assets.depreciation');
|
||||||
|
|
||||||
|
const depreciationInclude = {
|
||||||
|
item_categories: { select: { id: true, code: true, name: true } },
|
||||||
|
item_subcategories: { select: { id: true, code: true, name: true } },
|
||||||
|
plant: { select: { id: true, code: true, name: true } },
|
||||||
|
departments: { select: { id: true, name: true } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildDepreciationWhere = (query) => {
|
||||||
|
const where = {
|
||||||
|
deleted_at: null,
|
||||||
|
...(query.status ? { status: query.status } : {}),
|
||||||
|
...(query.depreciation_method ? { depreciation_method: query.depreciation_method } : {}),
|
||||||
|
...(query.item_category_id ? { item_category_id: BigInt(query.item_category_id) } : {}),
|
||||||
|
...(query.item_subcategory_id
|
||||||
|
? { item_subcategory_id: BigInt(query.item_subcategory_id) }
|
||||||
|
: {}),
|
||||||
|
...(query.plant_id ? { plant_id: BigInt(query.plant_id) } : {}),
|
||||||
|
...(query.department_id ? { department_id: BigInt(query.department_id) } : {}),
|
||||||
|
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||||
|
...(query.search
|
||||||
|
? {
|
||||||
|
OR: [
|
||||||
|
{ asset_code: { contains: query.search, mode: 'insensitive' } },
|
||||||
|
{ asset_name: { contains: query.search, mode: 'insensitive' } },
|
||||||
|
{ serial_number: { contains: query.search, mode: 'insensitive' } },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (query.purchase_date_from || query.purchase_date_to) {
|
||||||
|
where.purchase_date = {};
|
||||||
|
if (query.purchase_date_from) where.purchase_date.gte = new Date(query.purchase_date_from);
|
||||||
|
if (query.purchase_date_to) where.purchase_date.lte = new Date(query.purchase_date_to);
|
||||||
|
}
|
||||||
|
|
||||||
|
return where;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toNumber = (value) =>
|
||||||
|
value !== null && value !== undefined ? Number(value) : value === null ? null : 0;
|
||||||
|
|
||||||
|
const sanitizeDepreciationRow = (asset, asOfDate) => {
|
||||||
|
const purchaseCost = toNumber(asset.purchase_cost);
|
||||||
|
const salvageValue = toNumber(asset.salvage_value);
|
||||||
|
const depreciationRate = toNumber(asset.depreciation_rate);
|
||||||
|
|
||||||
|
const depreciation = calculateDepreciation({
|
||||||
|
depreciation_method: asset.depreciation_method,
|
||||||
|
depreciation_rate: depreciationRate,
|
||||||
|
purchase_cost: purchaseCost,
|
||||||
|
salvage_value: salvageValue,
|
||||||
|
useful_life_years: asset.useful_life_years,
|
||||||
|
purchase_date: asset.purchase_date,
|
||||||
|
as_of_date: asOfDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: asset.id,
|
||||||
|
asset_code: asset.asset_code,
|
||||||
|
asset_name: asset.asset_name,
|
||||||
|
status: asset.status,
|
||||||
|
purchase_date: asset.purchase_date,
|
||||||
|
purchase_cost: purchaseCost,
|
||||||
|
salvage_value: salvageValue,
|
||||||
|
useful_life_years: asset.useful_life_years,
|
||||||
|
depreciation_method: asset.depreciation_method,
|
||||||
|
depreciation_rate: depreciationRate,
|
||||||
|
item_category: asset.item_categories || null,
|
||||||
|
item_subcategory: asset.item_subcategories || null,
|
||||||
|
plant: asset.plant || null,
|
||||||
|
department: asset.departments || null,
|
||||||
|
depreciation,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const computeSummary = (rows, asOfDate) => {
|
||||||
|
let totalPurchaseCost = 0;
|
||||||
|
let totalAccumulatedDepreciation = 0;
|
||||||
|
let totalBookValue = 0;
|
||||||
|
let totalAnnualDepreciation = 0;
|
||||||
|
|
||||||
|
for (const asset of rows) {
|
||||||
|
const item = sanitizeDepreciationRow(asset, asOfDate);
|
||||||
|
totalPurchaseCost = round4(totalPurchaseCost + item.purchase_cost);
|
||||||
|
totalAccumulatedDepreciation = round4(
|
||||||
|
totalAccumulatedDepreciation + item.depreciation.accumulated_depreciation
|
||||||
|
);
|
||||||
|
totalBookValue = round4(totalBookValue + item.depreciation.book_value);
|
||||||
|
totalAnnualDepreciation = round4(
|
||||||
|
totalAnnualDepreciation + item.depreciation.annual_depreciation
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
asset_count: rows.length,
|
||||||
|
total_purchase_cost: totalPurchaseCost,
|
||||||
|
total_accumulated_depreciation: totalAccumulatedDepreciation,
|
||||||
|
total_book_value: totalBookValue,
|
||||||
|
total_annual_depreciation: totalAnnualDepreciation,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const listAssetDepreciation = async (query) => {
|
||||||
|
const { page, limit, skip } = getPagination(query);
|
||||||
|
const asOfDate = query.as_of_date ? new Date(query.as_of_date) : new Date();
|
||||||
|
const where = buildDepreciationWhere(query);
|
||||||
|
|
||||||
|
const [rows, total, summaryRows] = await Promise.all([
|
||||||
|
prisma.assets.findMany({
|
||||||
|
where,
|
||||||
|
include: depreciationInclude,
|
||||||
|
orderBy: [{ asset_code: 'asc' }],
|
||||||
|
skip,
|
||||||
|
take: limit,
|
||||||
|
}),
|
||||||
|
prisma.assets.count({ where }),
|
||||||
|
prisma.assets.findMany({
|
||||||
|
where,
|
||||||
|
include: depreciationInclude,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const summary = computeSummary(summaryRows, asOfDate);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: rows.map((row) => sanitizeDepreciationRow(row, asOfDate)),
|
||||||
|
meta: {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
total,
|
||||||
|
as_of_date: asOfDate.toISOString(),
|
||||||
|
summary,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDepreciationFilterOptions = async () => {
|
||||||
|
const [plants, departments, categories, subcategories] = await Promise.all([
|
||||||
|
prisma.locations.findMany({
|
||||||
|
where: { deleted_at: null, is_active: true, type: 'plant' },
|
||||||
|
select: { id: true, code: true, name: true },
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
}),
|
||||||
|
prisma.departments.findMany({
|
||||||
|
where: { deleted_at: null, is_active: true },
|
||||||
|
select: { id: true, name: true },
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
}),
|
||||||
|
prisma.item_categories.findMany({
|
||||||
|
where: { deleted_at: null, is_active: true },
|
||||||
|
select: { id: true, code: true, name: true },
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
}),
|
||||||
|
prisma.item_subcategories.findMany({
|
||||||
|
where: { deleted_at: null, is_active: true },
|
||||||
|
select: { id: true, code: true, name: true, item_category_id: true },
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
plants,
|
||||||
|
departments,
|
||||||
|
item_categories: categories,
|
||||||
|
item_subcategories: subcategories,
|
||||||
|
depreciation_methods: [
|
||||||
|
{ value: 'SLM', label: 'Straight Line Method (SLM)' },
|
||||||
|
{ value: 'WDV', label: 'Written Down Value (WDV)' },
|
||||||
|
{ value: 'OTHER', label: 'Other' },
|
||||||
|
],
|
||||||
|
statuses: [
|
||||||
|
{ value: 'IN_USE', label: 'In Use' },
|
||||||
|
{ value: 'IDLE', label: 'Idle' },
|
||||||
|
{ value: 'UNDER_MAINTENANCE', label: 'Under Maintenance' },
|
||||||
|
{ value: 'DISPOSED', label: 'Disposed' },
|
||||||
|
{ value: 'SCRAPPED', label: 'Scrapped' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const escapeCsv = (value) => {
|
||||||
|
const text = String(value ?? '');
|
||||||
|
if (/[",\n]/.test(text)) return `"${text.replace(/"/g, '""')}"`;
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportAssetDepreciation = async (query) => {
|
||||||
|
const asOfDate = query.as_of_date ? new Date(query.as_of_date) : new Date();
|
||||||
|
const where = buildDepreciationWhere(query);
|
||||||
|
|
||||||
|
const rows = await prisma.assets.findMany({
|
||||||
|
where,
|
||||||
|
include: depreciationInclude,
|
||||||
|
orderBy: [{ asset_code: 'asc' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const header = [
|
||||||
|
'Asset Code',
|
||||||
|
'Asset Name',
|
||||||
|
'Status',
|
||||||
|
'Category',
|
||||||
|
'Subcategory',
|
||||||
|
'Plant',
|
||||||
|
'Department',
|
||||||
|
'Purchase Date',
|
||||||
|
'Purchase Cost',
|
||||||
|
'Salvage Value',
|
||||||
|
'Useful Life (Years)',
|
||||||
|
'Depreciation Method',
|
||||||
|
'Depreciation Rate (%)',
|
||||||
|
'Annual Depreciation',
|
||||||
|
'Accumulated Depreciation',
|
||||||
|
'Book Value',
|
||||||
|
'Years Elapsed',
|
||||||
|
'As Of Date',
|
||||||
|
];
|
||||||
|
|
||||||
|
const lines = rows.map((row) => {
|
||||||
|
const item = sanitizeDepreciationRow(row, asOfDate);
|
||||||
|
return [
|
||||||
|
item.asset_code,
|
||||||
|
item.asset_name,
|
||||||
|
item.status,
|
||||||
|
item.item_category?.name || '',
|
||||||
|
item.item_subcategory?.name || '',
|
||||||
|
item.plant?.name || '',
|
||||||
|
item.department?.name || '',
|
||||||
|
item.purchase_date ? new Date(item.purchase_date).toISOString().slice(0, 10) : '',
|
||||||
|
item.purchase_cost,
|
||||||
|
item.salvage_value,
|
||||||
|
item.useful_life_years ?? '',
|
||||||
|
item.depreciation_method || '',
|
||||||
|
item.depreciation_rate ?? '',
|
||||||
|
item.depreciation.annual_depreciation,
|
||||||
|
item.depreciation.accumulated_depreciation,
|
||||||
|
item.depreciation.book_value,
|
||||||
|
item.depreciation.years_elapsed,
|
||||||
|
asOfDate.toISOString().slice(0, 10),
|
||||||
|
]
|
||||||
|
.map(escapeCsv)
|
||||||
|
.join(',');
|
||||||
|
});
|
||||||
|
|
||||||
|
return [header.join(','), ...lines].join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
listAssetDepreciation,
|
||||||
|
getDepreciationFilterOptions,
|
||||||
|
exportAssetDepreciation,
|
||||||
|
};
|
||||||
29
src/modules/reports/reports.controller.js
Normal file
29
src/modules/reports/reports.controller.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
const asyncHandler = require('../../utils/asyncHandler');
|
||||||
|
const ApiResponse = require('../../utils/ApiResponse');
|
||||||
|
const depreciationService = require('./reports-assets-depreciation.service');
|
||||||
|
|
||||||
|
const listAssetDepreciation = asyncHandler(async (req, res) => {
|
||||||
|
const result = await depreciationService.listAssetDepreciation(req.query);
|
||||||
|
res.json(new ApiResponse(200, result.data, 'Asset depreciation report fetched', result.meta));
|
||||||
|
});
|
||||||
|
|
||||||
|
const depreciationFilters = asyncHandler(async (_req, res) => {
|
||||||
|
const data = await depreciationService.getDepreciationFilterOptions();
|
||||||
|
res.json(new ApiResponse(200, data, 'Asset depreciation filter options fetched'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const exportAssetDepreciation = asyncHandler(async (req, res) => {
|
||||||
|
const csv = await depreciationService.exportAssetDepreciation(req.query);
|
||||||
|
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
||||||
|
res.setHeader(
|
||||||
|
'Content-Disposition',
|
||||||
|
'attachment; filename="asset-depreciation-report.csv"'
|
||||||
|
);
|
||||||
|
res.send(csv);
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
listAssetDepreciation,
|
||||||
|
depreciationFilters,
|
||||||
|
exportAssetDepreciation,
|
||||||
|
};
|
||||||
33
src/modules/reports/reports.routes.js
Normal file
33
src/modules/reports/reports.routes.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const authenticate = require('../../middlewares/auth.middleware');
|
||||||
|
const authorize = require('../../middlewares/rbac.middleware');
|
||||||
|
const validate = require('../../middlewares/validate.middleware');
|
||||||
|
const controller = require('./reports.controller');
|
||||||
|
const {
|
||||||
|
depreciationReportQuerySchema,
|
||||||
|
exportDepreciationReportQuerySchema,
|
||||||
|
} = require('./reports.validation');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.use(authenticate);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/assets/depreciation/filters',
|
||||||
|
authorize('REPORTS', 'view'),
|
||||||
|
controller.depreciationFilters
|
||||||
|
);
|
||||||
|
router.get(
|
||||||
|
'/assets/depreciation/export',
|
||||||
|
authorize('REPORTS', 'export'),
|
||||||
|
validate(exportDepreciationReportQuerySchema, 'query'),
|
||||||
|
controller.exportAssetDepreciation
|
||||||
|
);
|
||||||
|
router.get(
|
||||||
|
'/assets/depreciation',
|
||||||
|
authorize('REPORTS', 'view'),
|
||||||
|
validate(depreciationReportQuerySchema, 'query'),
|
||||||
|
controller.listAssetDepreciation
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
39
src/modules/reports/reports.validation.js
Normal file
39
src/modules/reports/reports.validation.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
const Joi = require('joi');
|
||||||
|
const { ASSET_STATUSES } = require('../assets/assets.constants');
|
||||||
|
|
||||||
|
const depreciationReportQuerySchema = Joi.object({
|
||||||
|
page: Joi.number().integer().min(1).default(1),
|
||||||
|
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||||
|
search: Joi.string().max(100).trim().allow('').optional(),
|
||||||
|
status: Joi.string()
|
||||||
|
.valid(...ASSET_STATUSES)
|
||||||
|
.optional(),
|
||||||
|
depreciation_method: Joi.string().valid('SLM', 'WDV', 'OTHER').optional(),
|
||||||
|
item_category_id: Joi.number().integer().positive().optional(),
|
||||||
|
item_subcategory_id: Joi.number().integer().positive().optional(),
|
||||||
|
plant_id: Joi.number().integer().positive().optional(),
|
||||||
|
department_id: Joi.number().integer().positive().optional(),
|
||||||
|
is_active: Joi.boolean().optional(),
|
||||||
|
as_of_date: Joi.date().iso().optional(),
|
||||||
|
purchase_date_from: Joi.date().iso().optional(),
|
||||||
|
purchase_date_to: Joi.date().iso().optional(),
|
||||||
|
}).custom((value, helpers) => {
|
||||||
|
if (
|
||||||
|
value.purchase_date_from &&
|
||||||
|
value.purchase_date_to &&
|
||||||
|
new Date(value.purchase_date_from) > new Date(value.purchase_date_to)
|
||||||
|
) {
|
||||||
|
return helpers.message('purchase_date_from must be before or equal to purchase_date_to');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const exportDepreciationReportQuerySchema = depreciationReportQuerySchema.keys({
|
||||||
|
page: Joi.strip(),
|
||||||
|
limit: Joi.strip(),
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
depreciationReportQuerySchema,
|
||||||
|
exportDepreciationReportQuerySchema,
|
||||||
|
};
|
||||||
@ -11,6 +11,7 @@ router.use('/grn', require('../../modules/grn/grn.routes'));
|
|||||||
router.use('/assets', require('../../modules/assets/assets.routes'));
|
router.use('/assets', require('../../modules/assets/assets.routes'));
|
||||||
router.use('/settings', require('../../modules/settings/settings.routes'));
|
router.use('/settings', require('../../modules/settings/settings.routes'));
|
||||||
router.use('/audit-logs', require('../../modules/audit-logs/audit-logs.routes'));
|
router.use('/audit-logs', require('../../modules/audit-logs/audit-logs.routes'));
|
||||||
|
router.use('/reports', require('../../modules/reports/reports.routes'));
|
||||||
router.use('/masters', require('../../modules/masters'));
|
router.use('/masters', require('../../modules/masters'));
|
||||||
|
|
||||||
router.get('/healthz', (req, res) => {
|
router.get('/healthz', (req, res) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user