GWM : added some of missing columns
This commit is contained in:
parent
6562c49957
commit
ab3e0f0e1b
@ -62,13 +62,14 @@ Build in this order. Each module: `*.routes.js` → `*.controller.js` → `*.ser
|
||||
| # | Module | Status | Notes |
|
||||
|---|--------|--------|-------|
|
||||
| 8 | **auth** | [x] Done | login, refresh, logout, me; rotating hashed refresh tokens; account lockout |
|
||||
| 9 | **masters** | [x] Done | 14 sub-masters under `masters/index.js` (UOM template replicated) |
|
||||
| 9 | **masters** | [x] Done | 15 sub-masters under `masters/index.js` (UOM template replicated) |
|
||||
| — | **users** | [x] Done | CRUD + summary/filters/export; RBAC: `USERS` |
|
||||
| — | **roles** | [x] Done | CRUD + permission assignment + matrix; RBAC: `ROLES` |
|
||||
| 10 | **vendors** | [x] Done | CRUD + addresses/contacts/bank-details; encrypt bank accounts |
|
||||
| 11 | **purchase-orders** | [x] Done | Header + line items; submit/approve/reject/amend/cancel; PDF export |
|
||||
| 12 | **grn** | [x] Done | Receipt + PO status recalc + asset auto-creation |
|
||||
| 13 | **assets** | [x] Done | CRUD + transfer + AMC/service visits/insurance + expiry alerts |
|
||||
| 14 | **settings** | [x] Done | Company profile + SMTP email settings; RBAC: `SETTINGS` |
|
||||
|
||||
Current progress:
|
||||
|
||||
@ -169,6 +170,7 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL
|
||||
| [x] | Payment Terms | `/masters/payment-terms` |
|
||||
| [x] | Delivery Terms | `/masters/delivery-terms` |
|
||||
| [x] | Asset Categories | `/masters/asset-categories` |
|
||||
| [x] | Asset Subcategories | `/masters/asset-subcategories` | Filter: `asset_category_id` |
|
||||
| [x] | Departments | `/masters/departments` |
|
||||
| [x] | Designations | `/masters/designations` |
|
||||
| [x] | Locations | `/masters/locations` | Unified plants + warehouses (`type`: `plant` \| `warehouse`) |
|
||||
@ -185,8 +187,10 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL
|
||||
| Status | Method | Endpoint | RBAC | Notes |
|
||||
|--------|--------|----------|------|-------|
|
||||
| [x] | GET | `/vendors` | view | List (`search`, `status`, `vendor_type`) |
|
||||
| [x] | GET | `/vendors/gst-treatments` | view | GST treatment dropdown options |
|
||||
| [x] | GET | `/vendors/source-of-supply` | view | Indian states/UTs dropdown options |
|
||||
| [x] | POST | `/vendors` | create | Auto `vendor_code` via VENDOR series |
|
||||
| [x] | GET | `/vendors/:id` | view | Detail + addresses, contacts, bank details |
|
||||
| [x] | GET | `/vendors/:id` | view | Detail + addresses, contacts, bank details, item mappings |
|
||||
| [x] | PUT | `/vendors/:id` | edit | Update vendor |
|
||||
| [x] | PATCH | `/vendors/:id/status` | edit | `active` / `inactive` / `blacklisted` |
|
||||
| [x] | DELETE | `/vendors/:id` | delete | Soft delete |
|
||||
@ -205,6 +209,11 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL
|
||||
| [x] | GET | `/vendors/:vendorId/bank-details/:bankDetailId` | view | Get bank detail |
|
||||
| [x] | PUT | `/vendors/:vendorId/bank-details/:bankDetailId` | edit | Update bank detail |
|
||||
| [x] | DELETE | `/vendors/:vendorId/bank-details/:bankDetailId` | delete | Deactivate bank detail |
|
||||
| [x] | GET | `/vendors/:vendorId/items` | view | List vendor–item mappings |
|
||||
| [x] | POST | `/vendors/:vendorId/items` | create | Map item to vendor |
|
||||
| [x] | GET | `/vendors/:vendorId/items/:mappingId` | view | Get item mapping |
|
||||
| [x] | PUT | `/vendors/:vendorId/items/:mappingId` | edit | Update rate / preferred flag |
|
||||
| [x] | DELETE | `/vendors/:vendorId/items/:mappingId` | delete | Deactivate item mapping |
|
||||
|
||||
---
|
||||
|
||||
@ -288,6 +297,22 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL
|
||||
| [x] | GET | `/assets/alerts/expiry` | view | AMC/Insurance/Warranty expiry (`?days=30/60/90&type=`) |
|
||||
| [x] | GET | `/assets/alerts/service` | view | Overdue/upcoming service (`?status=OVERDUE`) |
|
||||
|
||||
---
|
||||
|
||||
### Settings (`/settings`) — module: `SETTINGS`
|
||||
|
||||
| Status | Method | Endpoint | RBAC | Notes |
|
||||
|--------|--------|----------|------|-------|
|
||||
| [x] | GET | `/settings/company` | view | Company profile (org name, contact, address, logo URL) |
|
||||
| [x] | PUT | `/settings/company` | edit | Update company profile |
|
||||
| [x] | POST | `/settings/company/logo` | edit | Upload logo (`multipart/form-data`, field `logo`) |
|
||||
| [x] | GET | `/settings/email` | view | SMTP settings (`has_smtp_password` flag; password never returned) |
|
||||
| [x] | PUT | `/settings/email` | edit | Update SMTP settings (password encrypted at rest) |
|
||||
|
||||
**DB patch:** `scripts/patch-company-email-settings.sql` — creates `company` + `email_settings` singleton tables and `SETTINGS` module permissions for Super Admin.
|
||||
|
||||
---
|
||||
|
||||
**DB patch:** run `scripts/patch-assets-amc-insurance.sql` then `scripts/patch-assets-views.sql` on deployed DB.
|
||||
|
||||
---
|
||||
@ -301,11 +326,12 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL
|
||||
| Users | 8 | 8 | [x] Done |
|
||||
| Roles | 10 | 10 | [x] Done |
|
||||
| Masters | 70 | 70 | [x] Done |
|
||||
| Vendors | 20 | 20 | [x] Done |
|
||||
| Vendors | 27 | 27 | [x] Done |
|
||||
| Purchase Orders | 11 | 11 | [x] Done |
|
||||
| GRN | 6 | 6 | [x] Done |
|
||||
| Assets | 24 | 24 | [x] Done |
|
||||
| **Total** | **156** | **156** | **[x] Phase 1 APIs + Asset extensions** |
|
||||
| Settings | 5 | 5 | [x] Done |
|
||||
| **Total** | **168** | **168** | **[x] Phase 1 APIs + Asset extensions** |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -151,9 +151,29 @@ model asset_categories {
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
users_asset_categories_created_byTousers users? @relation("asset_categories_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
users_asset_categories_updated_byTousers users? @relation("asset_categories_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
asset_subcategories asset_subcategories[]
|
||||
assets assets[]
|
||||
}
|
||||
|
||||
model asset_subcategories {
|
||||
id BigInt @id @default(autoincrement())
|
||||
asset_category_id BigInt
|
||||
code String @unique @db.VarChar(30)
|
||||
name String @db.VarChar(150)
|
||||
is_active Boolean @default(true)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
users_asset_subcategories_created_byTousers users? @relation("asset_subcategories_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
asset_categories asset_categories @relation(fields: [asset_category_id], references: [id], onUpdate: NoAction)
|
||||
users_asset_subcategories_updated_byTousers users? @relation("asset_subcategories_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
assets assets[]
|
||||
|
||||
@@index([asset_category_id], map: "idx_asset_subcategories_category_id")
|
||||
}
|
||||
|
||||
model asset_transfers {
|
||||
id BigInt @id @default(autoincrement())
|
||||
asset_id BigInt
|
||||
@ -189,6 +209,7 @@ model assets {
|
||||
asset_code String @unique @db.VarChar(30)
|
||||
asset_name String @db.VarChar(200)
|
||||
asset_category_id BigInt
|
||||
asset_subcategory_id BigInt?
|
||||
brand_model String? @db.VarChar(200)
|
||||
manufacturer String? @db.VarChar(200)
|
||||
serial_number String? @db.VarChar(100)
|
||||
@ -205,7 +226,8 @@ model assets {
|
||||
purchase_date DateTime? @db.Date
|
||||
purchase_cost Decimal @default(0) @db.Decimal(15, 4)
|
||||
useful_life_years Int?
|
||||
depreciation_method String? @db.VarChar(10)
|
||||
depreciation_method String? @db.VarChar(20)
|
||||
depreciation_rate Decimal? @db.Decimal(8, 4)
|
||||
salvage_value Decimal? @default(0) @db.Decimal(15, 4)
|
||||
warranty_expiry_date DateTime? @db.Date
|
||||
condition String @default("NEW") @db.VarChar(20)
|
||||
@ -227,6 +249,7 @@ model assets {
|
||||
asset_service_visits asset_service_visits[]
|
||||
asset_transfers asset_transfers[]
|
||||
asset_categories asset_categories @relation(fields: [asset_category_id], references: [id], onUpdate: NoAction)
|
||||
asset_subcategories asset_subcategories? @relation(fields: [asset_subcategory_id], references: [id], onUpdate: NoAction)
|
||||
users_assets_assigned_to_user_idTousers users? @relation("assets_assigned_to_user_idTousers", fields: [assigned_to_user_id], references: [id], onUpdate: NoAction)
|
||||
users_assets_created_byTousers users? @relation("assets_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
departments departments? @relation(fields: [department_id], references: [id], onUpdate: NoAction)
|
||||
@ -239,6 +262,7 @@ model assets {
|
||||
warehouse locations? @relation("AssetWarehouseLocation", fields: [warehouse_id], references: [id], onUpdate: NoAction)
|
||||
|
||||
@@index([asset_category_id], map: "idx_assets_category_id")
|
||||
@@index([asset_subcategory_id], map: "idx_assets_subcategory_id")
|
||||
@@index([department_id], map: "idx_assets_dept_id")
|
||||
@@index([plant_id], map: "idx_assets_plant_id")
|
||||
}
|
||||
@ -695,6 +719,10 @@ model purchase_orders {
|
||||
tax_total Decimal @default(0) @db.Decimal(15, 4)
|
||||
freight_charges Decimal @default(0) @db.Decimal(15, 4)
|
||||
other_charges Decimal @default(0) @db.Decimal(15, 4)
|
||||
tds_applicable Boolean @default(false)
|
||||
tds_section_pct Decimal? @db.Decimal(5, 2)
|
||||
tds_amount Decimal @default(0) @db.Decimal(15, 4)
|
||||
adjustment Decimal @default(0) @db.Decimal(15, 4)
|
||||
grand_total Decimal @default(0) @db.Decimal(15, 4)
|
||||
status String @default("DRAFT") @db.VarChar(30)
|
||||
revision_no Int @default(0)
|
||||
@ -830,6 +858,8 @@ model users {
|
||||
asset_service_visits_asset_service_visits_updated_byTousers asset_service_visits[] @relation("asset_service_visits_updated_byTousers")
|
||||
asset_categories_asset_categories_created_byTousers asset_categories[] @relation("asset_categories_created_byTousers")
|
||||
asset_categories_asset_categories_updated_byTousers asset_categories[] @relation("asset_categories_updated_byTousers")
|
||||
asset_subcategories_asset_subcategories_created_byTousers asset_subcategories[] @relation("asset_subcategories_created_byTousers")
|
||||
asset_subcategories_asset_subcategories_updated_byTousers asset_subcategories[] @relation("asset_subcategories_updated_byTousers")
|
||||
asset_transfers_asset_transfers_from_user_idTousers asset_transfers[] @relation("asset_transfers_from_user_idTousers")
|
||||
asset_transfers_asset_transfers_to_user_idTousers asset_transfers[] @relation("asset_transfers_to_user_idTousers")
|
||||
asset_transfers_asset_transfers_transferred_byTousers asset_transfers[] @relation("asset_transfers_transferred_byTousers")
|
||||
@ -978,6 +1008,8 @@ model vendors {
|
||||
vendor_code String @unique @db.VarChar(30)
|
||||
vendor_name String @db.VarChar(200)
|
||||
vendor_type String @db.VarChar(30)
|
||||
gst_treatment String? @db.VarChar(30)
|
||||
source_of_supply String? @db.VarChar(100)
|
||||
gstin String? @db.VarChar(15)
|
||||
pan String? @db.VarChar(10)
|
||||
payment_term_id BigInt?
|
||||
@ -1005,3 +1037,34 @@ model vendors {
|
||||
|
||||
@@index([vendor_name(ops: raw("gin_trgm_ops"))], map: "idx_vendors_name_trgm", type: Gin)
|
||||
}
|
||||
|
||||
model company {
|
||||
id BigInt @id @default(1)
|
||||
org_name String? @db.VarChar(200)
|
||||
mobile String? @db.VarChar(20)
|
||||
email String? @db.VarChar(200)
|
||||
website String? @db.VarChar(255)
|
||||
logo_path String? @db.VarChar(500)
|
||||
address String?
|
||||
city String? @db.VarChar(100)
|
||||
state String? @db.VarChar(100)
|
||||
pincode String? @db.VarChar(10)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
}
|
||||
|
||||
model email_settings {
|
||||
id BigInt @id @default(1)
|
||||
smtp_host String? @db.VarChar(255)
|
||||
smtp_port Int @default(587)
|
||||
smtp_username String? @db.VarChar(200)
|
||||
smtp_password String? @db.VarChar(500)
|
||||
sender_email String? @db.VarChar(200)
|
||||
sender_name String? @db.VarChar(200)
|
||||
created_by BigInt?
|
||||
updated_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
}
|
||||
|
||||
40
scripts/patch-asset-subcategories.sql
Normal file
40
scripts/patch-asset-subcategories.sql
Normal file
@ -0,0 +1,40 @@
|
||||
-- Asset Sub-Categories master + assets.asset_subcategory_id
|
||||
|
||||
CREATE TABLE IF NOT EXISTS asset_subcategories (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
asset_category_id BIGINT NOT NULL REFERENCES asset_categories (id),
|
||||
code VARCHAR(30) NOT NULL UNIQUE,
|
||||
name VARCHAR(150) NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
created_by BIGINT,
|
||||
updated_by BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_asset_subcategories_category_id
|
||||
ON asset_subcategories (asset_category_id);
|
||||
|
||||
ALTER TABLE assets
|
||||
ADD COLUMN IF NOT EXISTS asset_subcategory_id BIGINT;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'assets_asset_subcategory_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE assets
|
||||
ADD CONSTRAINT assets_asset_subcategory_id_fkey
|
||||
FOREIGN KEY (asset_subcategory_id)
|
||||
REFERENCES asset_subcategories (id);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_assets_subcategory_id
|
||||
ON assets (asset_subcategory_id);
|
||||
|
||||
COMMENT ON TABLE asset_subcategories IS 'Asset sub-categories under asset_categories';
|
||||
COMMENT ON COLUMN assets.asset_subcategory_id IS 'Required for new assets via API; nullable for legacy rows';
|
||||
10
scripts/patch-assets-depreciation-rate.sql
Normal file
10
scripts/patch-assets-depreciation-rate.sql
Normal file
@ -0,0 +1,10 @@
|
||||
-- Depreciation rate on assets + widen depreciation_method for OTHER
|
||||
|
||||
ALTER TABLE assets
|
||||
ADD COLUMN IF NOT EXISTS depreciation_rate NUMERIC(8, 4);
|
||||
|
||||
ALTER TABLE assets
|
||||
ALTER COLUMN depreciation_method TYPE VARCHAR(20);
|
||||
|
||||
COMMENT ON COLUMN assets.depreciation_method IS 'SLM, WDV, OTHER';
|
||||
COMMENT ON COLUMN assets.depreciation_rate IS 'Annual depreciation rate percentage';
|
||||
59
scripts/patch-company-email-settings.sql
Normal file
59
scripts/patch-company-email-settings.sql
Normal file
@ -0,0 +1,59 @@
|
||||
-- Company profile + email (SMTP) settings + SETTINGS RBAC module
|
||||
|
||||
CREATE TABLE IF NOT EXISTS company (
|
||||
id BIGINT PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
||||
org_name VARCHAR(200),
|
||||
mobile VARCHAR(20),
|
||||
email VARCHAR(200),
|
||||
website VARCHAR(255),
|
||||
logo_path VARCHAR(500),
|
||||
address TEXT,
|
||||
city VARCHAR(100),
|
||||
state VARCHAR(100),
|
||||
pincode VARCHAR(10),
|
||||
created_by BIGINT,
|
||||
updated_by BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS email_settings (
|
||||
id BIGINT PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
||||
smtp_host VARCHAR(255),
|
||||
smtp_port INTEGER NOT NULL DEFAULT 587,
|
||||
smtp_username VARCHAR(200),
|
||||
smtp_password VARCHAR(500),
|
||||
sender_email VARCHAR(200),
|
||||
sender_name VARCHAR(200),
|
||||
created_by BIGINT,
|
||||
updated_by BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
INSERT INTO company (id, org_name)
|
||||
VALUES (1, '')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO email_settings (id, smtp_port)
|
||||
VALUES (1, 587)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO modules (code, name, sort_order, is_active)
|
||||
VALUES ('SETTINGS', 'Settings', 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'), ('edit')) AS a(action)
|
||||
WHERE m.code = 'SETTINGS'
|
||||
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', 'edit')
|
||||
JOIN modules m ON m.id = p.module_id AND m.code = 'SETTINGS'
|
||||
WHERE r.name = 'Super Admin' AND r.deleted_at IS NULL
|
||||
ON CONFLICT (role_id, permission_id) DO NOTHING;
|
||||
12
scripts/patch-po-tds-adjustment.sql
Normal file
12
scripts/patch-po-tds-adjustment.sql
Normal file
@ -0,0 +1,12 @@
|
||||
-- Add TDS and adjustment fields to purchase_orders
|
||||
|
||||
ALTER TABLE purchase_orders
|
||||
ADD COLUMN IF NOT EXISTS tds_applicable BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS tds_section_pct NUMERIC(5, 2),
|
||||
ADD COLUMN IF NOT EXISTS tds_amount NUMERIC(15, 4) NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS adjustment NUMERIC(15, 4) NOT NULL DEFAULT 0;
|
||||
|
||||
COMMENT ON COLUMN purchase_orders.tds_applicable IS 'Whether TDS is applicable on this PO';
|
||||
COMMENT ON COLUMN purchase_orders.tds_section_pct IS 'TDS percentage when tds_applicable is true';
|
||||
COMMENT ON COLUMN purchase_orders.tds_amount IS 'Computed TDS amount deducted from grand total';
|
||||
COMMENT ON COLUMN purchase_orders.adjustment IS 'Manual +/- amount adjustment applied to grand total';
|
||||
8
scripts/patch-vendors-gst-source.sql
Normal file
8
scripts/patch-vendors-gst-source.sql
Normal file
@ -0,0 +1,8 @@
|
||||
-- Add GST Treatment and Source of Supply columns to vendors
|
||||
|
||||
ALTER TABLE vendors
|
||||
ADD COLUMN IF NOT EXISTS gst_treatment VARCHAR(30),
|
||||
ADD COLUMN IF NOT EXISTS source_of_supply VARCHAR(100);
|
||||
|
||||
COMMENT ON COLUMN vendors.gst_treatment IS 'REGULAR, COMPOSITION, UNREGISTERED, CONSUMER, SEZ, OVERSEAS, DEEMED_EXPORT, OTHER';
|
||||
COMMENT ON COLUMN vendors.source_of_supply IS 'Indian state or union territory name';
|
||||
@ -1,3 +1,4 @@
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
@ -25,6 +26,8 @@ app.use(express.urlencoded({ extended: true }));
|
||||
app.use(requestId);
|
||||
app.use(morganMiddleware);
|
||||
|
||||
app.use('/uploads', express.static(path.resolve(env.UPLOAD_DIR)));
|
||||
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ success: true, message: 'OK', uptime: process.uptime() });
|
||||
});
|
||||
|
||||
@ -5,13 +5,96 @@ components:
|
||||
schemas:
|
||||
AssetsCreateBody:
|
||||
type: object
|
||||
required: [asset_name, asset_category_id, plant_id]
|
||||
required: [asset_name, asset_category_id, asset_subcategory_id, plant_id]
|
||||
properties:
|
||||
asset_name: { type: string, example: 'CNC Lathe Machine' }
|
||||
asset_category_id: { type: integer, example: 1 }
|
||||
asset_subcategory_id: { type: integer, example: 1 }
|
||||
plant_id: { type: integer, example: 1 }
|
||||
warranty_expiry_date: { type: string, format: date, nullable: true }
|
||||
brand_model: { type: string }
|
||||
manufacturer: { type: string }
|
||||
serial_number: { type: string }
|
||||
part_number: { type: string }
|
||||
department_id: { type: integer, nullable: true }
|
||||
warehouse_id: { type: integer, nullable: true }
|
||||
location_detail: { type: string }
|
||||
assigned_to_user_id: { type: integer, nullable: true }
|
||||
vendor_id: { type: integer, nullable: true }
|
||||
po_id: { type: integer, nullable: true }
|
||||
grn_id: { type: integer, nullable: true }
|
||||
grn_item_id: { type: integer, nullable: true }
|
||||
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 }
|
||||
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] }
|
||||
qr_code_value: { type: string }
|
||||
disposal_date: { type: string, format: date, nullable: true }
|
||||
disposal_reason: { type: string }
|
||||
disposal_value: { type: number }
|
||||
remarks: { type: string }
|
||||
is_active: { type: boolean }
|
||||
AssetsUpdateBody:
|
||||
type: object
|
||||
minProperties: 1
|
||||
properties:
|
||||
asset_name: { type: string }
|
||||
asset_category_id: { type: integer }
|
||||
asset_subcategory_id: { type: integer }
|
||||
plant_id: { type: integer }
|
||||
brand_model: { type: string }
|
||||
manufacturer: { type: string }
|
||||
serial_number: { type: string }
|
||||
part_number: { type: string }
|
||||
department_id: { type: integer, nullable: true }
|
||||
warehouse_id: { type: integer, nullable: true }
|
||||
location_detail: { type: string }
|
||||
assigned_to_user_id: { type: integer, nullable: true }
|
||||
vendor_id: { type: integer, nullable: true }
|
||||
po_id: { type: integer, nullable: true }
|
||||
grn_id: { type: integer, nullable: true }
|
||||
grn_item_id: { type: integer, nullable: true }
|
||||
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_rate: { type: number, nullable: true }
|
||||
salvage_value: { type: number }
|
||||
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] }
|
||||
qr_code_value: { type: string }
|
||||
disposal_date: { type: string, format: date, nullable: true }
|
||||
disposal_reason: { type: string }
|
||||
disposal_value: { type: number }
|
||||
remarks: { type: string }
|
||||
is_active: { type: boolean }
|
||||
DepreciationCalculateBody:
|
||||
type: object
|
||||
required: [depreciation_method]
|
||||
properties:
|
||||
depreciation_method: { type: string, enum: [SLM, WDV, OTHER] }
|
||||
depreciation_rate: { type: number, nullable: true }
|
||||
purchase_cost: { type: number, example: 850000 }
|
||||
salvage_value: { type: number, example: 50000 }
|
||||
useful_life_years: { type: integer, example: 10 }
|
||||
purchase_date: { type: string, format: date }
|
||||
as_of_date: { type: string, format: date }
|
||||
TransferAssetBody:
|
||||
type: object
|
||||
required: [transfer_date]
|
||||
properties:
|
||||
transfer_date: { type: string, format: date }
|
||||
to_plant_id: { type: integer, nullable: true }
|
||||
to_department_id: { type: integer, nullable: true }
|
||||
to_user_id: { type: integer, nullable: true }
|
||||
to_warehouse_id: { type: integer, nullable: true }
|
||||
reason: { type: string }
|
||||
description: At least one of to_plant_id, to_department_id, to_user_id, to_warehouse_id is required
|
||||
AmcContractBody:
|
||||
type: object
|
||||
required: [vendor_id, start_date, end_date]
|
||||
@ -21,29 +104,196 @@ components:
|
||||
contract_type: { type: string, enum: [COMPREHENSIVE, LABOUR_ONLY, PARTS_ONLY, PREVENTIVE_ONLY] }
|
||||
start_date: { type: string, format: date }
|
||||
end_date: { type: string, format: date }
|
||||
renewal_date: { type: string, format: date, nullable: true }
|
||||
annual_cost: { type: number, example: 50000 }
|
||||
payment_frequency: { type: string, enum: [MONTHLY, QUARTERLY, HALF_YEARLY, ANNUAL] }
|
||||
service_frequency: { type: string, enum: [MONTHLY, QUARTERLY, HALF_YEARLY, ANNUAL], nullable: true }
|
||||
visits_per_year: { type: integer, nullable: true, example: 4 }
|
||||
contact_person: { type: string }
|
||||
contact_phone: { type: string }
|
||||
contact_email: { type: string, format: email }
|
||||
scope_of_work: { type: string }
|
||||
exclusions: { type: string }
|
||||
remarks: { type: string }
|
||||
is_active: { type: boolean }
|
||||
AmcContractUpdateBody:
|
||||
type: object
|
||||
minProperties: 1
|
||||
properties:
|
||||
vendor_id: { type: integer }
|
||||
contract_no: { type: string }
|
||||
contract_type: { type: string, enum: [COMPREHENSIVE, LABOUR_ONLY, PARTS_ONLY, PREVENTIVE_ONLY] }
|
||||
start_date: { type: string, format: date }
|
||||
end_date: { type: string, format: date }
|
||||
renewal_date: { type: string, format: date, nullable: true }
|
||||
annual_cost: { type: number }
|
||||
payment_frequency: { type: string, enum: [MONTHLY, QUARTERLY, HALF_YEARLY, ANNUAL] }
|
||||
service_frequency: { type: string, enum: [MONTHLY, QUARTERLY, HALF_YEARLY, ANNUAL], nullable: true }
|
||||
visits_per_year: { type: integer, nullable: true }
|
||||
contact_person: { type: string }
|
||||
contact_phone: { type: string }
|
||||
contact_email: { type: string, format: email }
|
||||
scope_of_work: { type: string }
|
||||
exclusions: { type: string }
|
||||
remarks: { type: string }
|
||||
is_active: { type: boolean }
|
||||
AmcRenewBody:
|
||||
type: object
|
||||
required: [start_date, end_date]
|
||||
properties:
|
||||
start_date: { type: string, format: date }
|
||||
end_date: { type: string, format: date }
|
||||
vendor_id: { type: integer }
|
||||
contract_no: { type: string }
|
||||
contract_type: { type: string, enum: [COMPREHENSIVE, LABOUR_ONLY, PARTS_ONLY, PREVENTIVE_ONLY] }
|
||||
renewal_date: { type: string, format: date, nullable: true }
|
||||
annual_cost: { type: number }
|
||||
payment_frequency: { type: string, enum: [MONTHLY, QUARTERLY, HALF_YEARLY, ANNUAL] }
|
||||
service_frequency: { type: string, enum: [MONTHLY, QUARTERLY, HALF_YEARLY, ANNUAL], nullable: true }
|
||||
visits_per_year: { type: integer, nullable: true }
|
||||
contact_person: { type: string }
|
||||
contact_phone: { type: string }
|
||||
contact_email: { type: string, format: email }
|
||||
scope_of_work: { type: string }
|
||||
exclusions: { type: string }
|
||||
remarks: { type: string }
|
||||
ServiceVisitBody:
|
||||
type: object
|
||||
required: [visit_type, visit_date]
|
||||
properties:
|
||||
visit_type: { type: string, enum: [PREVENTIVE, BREAKDOWN, INSPECTION, INSTALLATION, CALIBRATION, OTHER] }
|
||||
visit_date: { type: string, format: date }
|
||||
amc_contract_id: { type: integer, nullable: true }
|
||||
visit_number: { type: integer, nullable: true }
|
||||
complaint_no: { type: string, example: CMP-001 }
|
||||
complaint_date: { type: string, format: date, nullable: true }
|
||||
complaint_desc: { type: string }
|
||||
engineer_name: { type: string }
|
||||
engineer_phone: { type: string }
|
||||
vendor_id: { type: integer, nullable: true }
|
||||
work_done: { type: string }
|
||||
parts_replaced: { type: string }
|
||||
next_service_date: { type: string, format: date, nullable: true }
|
||||
status: { type: string, enum: [SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED, PENDING_PARTS] }
|
||||
downtime_hours: { type: number, example: 2.5 }
|
||||
service_cost: { type: number, example: 1500 }
|
||||
is_under_amc: { type: boolean }
|
||||
asset_condition_after: { type: string, enum: [GOOD, FAIR, POOR, NEEDS_REPLACEMENT], nullable: true }
|
||||
remarks: { type: string }
|
||||
ServiceVisitUpdateBody:
|
||||
type: object
|
||||
minProperties: 1
|
||||
properties:
|
||||
visit_type: { type: string, enum: [PREVENTIVE, BREAKDOWN, INSPECTION, INSTALLATION, CALIBRATION, OTHER] }
|
||||
visit_date: { type: string, format: date }
|
||||
amc_contract_id: { type: integer, nullable: true }
|
||||
visit_number: { type: integer, nullable: true }
|
||||
complaint_no: { type: string }
|
||||
complaint_date: { type: string, format: date, nullable: true }
|
||||
complaint_desc: { type: string }
|
||||
engineer_name: { type: string }
|
||||
engineer_phone: { type: string }
|
||||
vendor_id: { type: integer, nullable: true }
|
||||
work_done: { type: string }
|
||||
parts_replaced: { type: string }
|
||||
next_service_date: { type: string, format: date, nullable: true }
|
||||
status: { type: string, enum: [SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED, PENDING_PARTS] }
|
||||
downtime_hours: { type: number }
|
||||
service_cost: { type: number }
|
||||
is_under_amc: { type: boolean }
|
||||
asset_condition_after: { type: string, enum: [GOOD, FAIR, POOR, NEEDS_REPLACEMENT], nullable: true }
|
||||
remarks: { type: string }
|
||||
ServiceVisitStatusBody:
|
||||
type: object
|
||||
required: [status]
|
||||
properties:
|
||||
status: { type: string, enum: [SCHEDULED, IN_PROGRESS, COMPLETED, CANCELLED, PENDING_PARTS] }
|
||||
remarks: { type: string }
|
||||
InsurancePolicyBody:
|
||||
type: object
|
||||
required: [policy_no, insurer_name, policy_start_date, policy_end_date]
|
||||
properties:
|
||||
policy_no: { type: string, example: POL-2026-001 }
|
||||
insurer_name: { type: string, example: 'ICICI Lombard' }
|
||||
insurer_branch: { type: string }
|
||||
insurer_contact: { type: string }
|
||||
insurer_phone: { type: string }
|
||||
insurer_email: { type: string, format: email }
|
||||
policy_type: { type: string, enum: [FIRE_AND_ALLIED, MACHINERY_BREAKDOWN, COMPREHENSIVE, THIRD_PARTY, VEHICLE, OTHER] }
|
||||
sum_insured: { type: number, example: 1000000 }
|
||||
annual_premium: { type: number, example: 25000 }
|
||||
policy_start_date: { type: string, format: date }
|
||||
policy_end_date: { type: string, format: date }
|
||||
renewal_date: { type: string, format: date, nullable: true }
|
||||
is_auto_renewal: { type: boolean }
|
||||
premium_paid: { type: boolean }
|
||||
premium_paid_date: { type: string, format: date, nullable: true }
|
||||
remarks: { type: string }
|
||||
is_active: { type: boolean }
|
||||
InsurancePolicyUpdateBody:
|
||||
type: object
|
||||
minProperties: 1
|
||||
properties:
|
||||
policy_no: { type: string }
|
||||
insurer_name: { type: string }
|
||||
insurer_branch: { type: string }
|
||||
insurer_contact: { type: string }
|
||||
insurer_phone: { type: string }
|
||||
insurer_email: { type: string, format: email }
|
||||
policy_type: { type: string, enum: [FIRE_AND_ALLIED, MACHINERY_BREAKDOWN, COMPREHENSIVE, THIRD_PARTY, VEHICLE, OTHER] }
|
||||
sum_insured: { type: number }
|
||||
annual_premium: { type: number }
|
||||
policy_start_date: { type: string, format: date }
|
||||
policy_end_date: { type: string, format: date }
|
||||
renewal_date: { type: string, format: date, nullable: true }
|
||||
is_auto_renewal: { type: boolean }
|
||||
premium_paid: { type: boolean }
|
||||
premium_paid_date: { type: string, format: date, nullable: true }
|
||||
remarks: { type: string }
|
||||
is_active: { type: boolean }
|
||||
InsuranceRenewBody:
|
||||
type: object
|
||||
required: [policy_start_date, policy_end_date]
|
||||
properties:
|
||||
policy_start_date: { type: string, format: date }
|
||||
policy_end_date: { type: string, format: date }
|
||||
policy_no: { type: string }
|
||||
insurer_name: { type: string }
|
||||
insurer_branch: { type: string }
|
||||
insurer_contact: { type: string }
|
||||
insurer_phone: { type: string }
|
||||
insurer_email: { type: string, format: email }
|
||||
policy_type: { type: string, enum: [FIRE_AND_ALLIED, MACHINERY_BREAKDOWN, COMPREHENSIVE, THIRD_PARTY, VEHICLE, OTHER] }
|
||||
sum_insured: { type: number }
|
||||
annual_premium: { type: number }
|
||||
renewal_date: { type: string, format: date, nullable: true }
|
||||
is_auto_renewal: { type: boolean }
|
||||
premium_paid: { type: boolean }
|
||||
premium_paid_date: { type: string, format: date, nullable: true }
|
||||
remarks: { type: string }
|
||||
|
||||
paths:
|
||||
/assets/depreciation-methods:
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: List depreciation method dropdown options (SLM, WDV, OTHER)
|
||||
responses:
|
||||
'200':
|
||||
description: Depreciation methods fetched
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/depreciation/calculate:
|
||||
post:
|
||||
tags: [Assets]
|
||||
summary: Preview depreciation calculation for selected method/rate
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/DepreciationCalculateBody' }
|
||||
responses:
|
||||
'200':
|
||||
description: Depreciation calculated
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/alerts/expiry:
|
||||
get:
|
||||
tags: [Assets]
|
||||
@ -56,38 +306,117 @@ paths:
|
||||
responses:
|
||||
'200':
|
||||
description: Expiry alerts fetched
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ApiResponse' }
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/alerts/service:
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: Service due/overdue alerts
|
||||
parameters:
|
||||
- { name: status, in: query, schema: { type: string, enum: [OVERDUE, DUE_THIS_WEEK, DUE_THIS_MONTH, UPCOMING] } }
|
||||
- { name: page, in: query, schema: { type: integer } }
|
||||
- { name: limit, in: query, schema: { type: integer } }
|
||||
responses:
|
||||
'200':
|
||||
description: Service alerts fetched
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ApiResponse' }
|
||||
/assets/{id}/amc:
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets:
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: List AMC contracts for asset
|
||||
summary: List assets
|
||||
parameters:
|
||||
- { name: page, in: query, schema: { type: integer } }
|
||||
- { name: limit, in: query, schema: { type: integer } }
|
||||
- { name: search, in: query, schema: { type: string } }
|
||||
- { name: status, in: query, schema: { type: string } }
|
||||
- { name: condition, in: query, schema: { type: string } }
|
||||
- { name: asset_category_id, in: query, schema: { type: integer } }
|
||||
- { name: asset_subcategory_id, in: query, schema: { type: integer } }
|
||||
- { name: plant_id, in: query, schema: { type: integer } }
|
||||
- { name: department_id, in: query, schema: { type: integer } }
|
||||
- { name: is_active, in: query, schema: { type: boolean } }
|
||||
responses:
|
||||
'200':
|
||||
description: Assets fetched
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
post:
|
||||
tags: [Assets]
|
||||
summary: Create asset
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/AssetsCreateBody' }
|
||||
responses:
|
||||
'201':
|
||||
description: Asset created
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/{id}:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: Get asset (includes depreciation summary)
|
||||
responses:
|
||||
'200':
|
||||
description: Asset fetched
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
put:
|
||||
tags: [Assets]
|
||||
summary: Update asset
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/AssetsUpdateBody' }
|
||||
responses:
|
||||
'200':
|
||||
description: Asset updated
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
delete:
|
||||
tags: [Assets]
|
||||
summary: Soft delete asset
|
||||
responses:
|
||||
'200':
|
||||
description: Asset deleted
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/{id}/transfer:
|
||||
post:
|
||||
tags: [Assets]
|
||||
summary: Transfer asset
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/TransferAssetBody' }
|
||||
responses:
|
||||
'200':
|
||||
description: Asset transferred
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/{id}/transfer-history:
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: Asset transfer history
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200':
|
||||
description: Transfer history fetched
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/{id}/amc:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: List AMC contracts for asset
|
||||
responses:
|
||||
'200':
|
||||
description: AMC contracts fetched
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ApiResponse' }
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
post:
|
||||
tags: [Assets]
|
||||
summary: Create AMC contract
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@ -96,6 +425,30 @@ paths:
|
||||
responses:
|
||||
'201':
|
||||
description: AMC contract created
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/{id}/amc/{contractId}:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
- { name: contractId, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: Get AMC contract
|
||||
responses:
|
||||
'200':
|
||||
description: AMC contract fetched
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
put:
|
||||
tags: [Assets]
|
||||
summary: Update AMC contract
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/AmcContractUpdateBody' }
|
||||
responses:
|
||||
'200':
|
||||
description: AMC contract updated
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/{id}/amc/{contractId}/renew:
|
||||
patch:
|
||||
tags: [Assets]
|
||||
@ -107,29 +460,27 @@ paths:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [start_date, end_date]
|
||||
properties:
|
||||
start_date: { type: string, format: date }
|
||||
end_date: { type: string, format: date }
|
||||
schema: { $ref: '#/components/schemas/AmcRenewBody' }
|
||||
responses:
|
||||
'201':
|
||||
'200':
|
||||
description: AMC renewed
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/{id}/service-visits:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: List service visits
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
- { name: status, in: query, schema: { type: string } }
|
||||
- { name: visit_type, in: query, schema: { type: string } }
|
||||
responses:
|
||||
'200':
|
||||
description: Service visits fetched
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
post:
|
||||
tags: [Assets]
|
||||
summary: Log service visit
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@ -138,20 +489,59 @@ paths:
|
||||
responses:
|
||||
'201':
|
||||
description: Service visit created
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/{id}/service-visits/{visitId}:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
- { name: visitId, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: Get service visit
|
||||
responses:
|
||||
'200':
|
||||
description: Service visit fetched
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
put:
|
||||
tags: [Assets]
|
||||
summary: Update service visit
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ServiceVisitUpdateBody' }
|
||||
responses:
|
||||
'200':
|
||||
description: Service visit updated
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/{id}/service-visits/{visitId}/status:
|
||||
patch:
|
||||
tags: [Assets]
|
||||
summary: Update service visit status
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
- { name: visitId, in: path, required: true, schema: { type: string } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ServiceVisitStatusBody' }
|
||||
responses:
|
||||
'200':
|
||||
description: Status updated
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/{id}/insurance:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: List insurance policies
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200':
|
||||
description: Insurance policies fetched
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
post:
|
||||
tags: [Assets]
|
||||
summary: Create insurance policy
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@ -160,6 +550,30 @@ paths:
|
||||
responses:
|
||||
'201':
|
||||
description: Insurance policy created
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/{id}/insurance/{policyId}:
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
- { name: policyId, in: path, required: true, schema: { type: string } }
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: Get insurance policy
|
||||
responses:
|
||||
'200':
|
||||
description: Insurance policy fetched
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
put:
|
||||
tags: [Assets]
|
||||
summary: Update insurance policy
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/InsurancePolicyUpdateBody' }
|
||||
responses:
|
||||
'200':
|
||||
description: Insurance policy updated
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/{id}/insurance/{policyId}/renew:
|
||||
patch:
|
||||
tags: [Assets]
|
||||
@ -171,63 +585,8 @@ paths:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [policy_start_date, policy_end_date]
|
||||
properties:
|
||||
policy_start_date: { type: string, format: date }
|
||||
policy_end_date: { type: string, format: date }
|
||||
schema: { $ref: '#/components/schemas/InsuranceRenewBody' }
|
||||
responses:
|
||||
'201':
|
||||
'200':
|
||||
description: Insurance renewed
|
||||
/assets:
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: List assets
|
||||
responses:
|
||||
'200':
|
||||
description: Assets fetched
|
||||
post:
|
||||
tags: [Assets]
|
||||
summary: Create asset
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/AssetsCreateBody' }
|
||||
responses:
|
||||
'201':
|
||||
description: Asset created
|
||||
/assets/{id}:
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: Get asset
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200':
|
||||
description: Asset fetched
|
||||
put:
|
||||
tags: [Assets]
|
||||
summary: Update asset
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200':
|
||||
description: Asset updated
|
||||
delete:
|
||||
tags: [Assets]
|
||||
summary: Soft delete asset
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200':
|
||||
description: Asset deleted
|
||||
/assets/{id}/transfer:
|
||||
post:
|
||||
tags: [Assets]
|
||||
summary: Transfer asset
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200':
|
||||
description: Asset transferred
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
|
||||
@ -3,6 +3,7 @@ tags:
|
||||
- name: UOM
|
||||
- name: Item Categories
|
||||
- name: Item Subcategories
|
||||
- name: Asset Subcategories
|
||||
- name: Items
|
||||
- name: Brands
|
||||
- name: GST Rates
|
||||
@ -11,6 +12,7 @@ tags:
|
||||
- name: Asset Categories
|
||||
- name: Departments
|
||||
- name: Designations
|
||||
- name: Locations
|
||||
- name: Warehouses
|
||||
- name: Plants
|
||||
- name: Document Series
|
||||
@ -149,7 +151,7 @@ components:
|
||||
properties:
|
||||
code: { type: string, example: 'OWN01' }
|
||||
name: { type: string, example: 'Bharat Clean' }
|
||||
brand_type: { type: string, example: 'OWN' }
|
||||
brand_type: { type: string, enum: [OWN, OEM, THIRD_PARTY], example: OWN }
|
||||
contact_person: { type: string, example: 'Ravi' }
|
||||
phone: { type: string, example: '9876543210' }
|
||||
email: { type: string, example: 'ops@bharaterp.com' }
|
||||
@ -160,7 +162,7 @@ components:
|
||||
properties:
|
||||
code: { type: string, example: 'OWN01' }
|
||||
name: { type: string, example: 'Bharat Clean' }
|
||||
brand_type: { type: string, example: 'OWN' }
|
||||
brand_type: { type: string, enum: [OWN, OEM, THIRD_PARTY], example: OWN }
|
||||
contact_person: { type: string, example: 'Ravi' }
|
||||
phone: { type: string, example: '9876543210' }
|
||||
email: { type: string, example: 'ops@bharaterp.com' }
|
||||
@ -221,7 +223,7 @@ components:
|
||||
name: { type: string, example: 'Plant & Machinery' }
|
||||
code_prefix: { type: string, example: 'MCH' }
|
||||
default_useful_life_years: { type: integer, example: 15 }
|
||||
default_depreciation_method: { type: string, example: 'WDV' }
|
||||
default_depreciation_method: { type: string, enum: [SLM, WDV, OTHER], example: WDV }
|
||||
is_active: { type: boolean, example: true }
|
||||
AssetCategoriesUpdateBody:
|
||||
type: object
|
||||
@ -231,7 +233,23 @@ components:
|
||||
name: { type: string, example: 'Plant & Machinery' }
|
||||
code_prefix: { type: string, example: 'MCH' }
|
||||
default_useful_life_years: { type: integer, example: 15 }
|
||||
default_depreciation_method: { type: string, example: 'WDV' }
|
||||
default_depreciation_method: { type: string, enum: [SLM, WDV, OTHER], example: WDV }
|
||||
is_active: { type: boolean, example: true }
|
||||
AssetSubcategoriesCreateBody:
|
||||
type: object
|
||||
required: [asset_category_id, code, name]
|
||||
properties:
|
||||
asset_category_id: { type: integer, example: 1 }
|
||||
code: { type: string, example: 'CNC' }
|
||||
name: { type: string, example: 'CNC Machines' }
|
||||
is_active: { type: boolean, example: true }
|
||||
AssetSubcategoriesUpdateBody:
|
||||
type: object
|
||||
minProperties: 1
|
||||
properties:
|
||||
asset_category_id: { type: integer, example: 1 }
|
||||
code: { type: string, example: 'CNC' }
|
||||
name: { type: string, example: 'CNC Machines' }
|
||||
is_active: { type: boolean, example: true }
|
||||
DepartmentsCreateBody:
|
||||
type: object
|
||||
@ -257,6 +275,37 @@ components:
|
||||
properties:
|
||||
name: { type: string, example: 'Store Manager' }
|
||||
is_active: { type: boolean, example: true }
|
||||
LocationsCreateBody:
|
||||
type: object
|
||||
required: [type, code, name]
|
||||
properties:
|
||||
type: { type: string, enum: [plant, warehouse], example: plant }
|
||||
code: { type: string, example: 'PLT01' }
|
||||
name: { type: string, example: 'Bharat Plant 1' }
|
||||
parent_id: { type: integer, nullable: true, example: 1, description: Required when type is warehouse (plant id) }
|
||||
gstin: { type: string, example: '27ABCDE1234F1Z5' }
|
||||
address: { type: string, example: 'MIDC, Nashik' }
|
||||
city: { type: string, example: 'Nashik' }
|
||||
state: { type: string, example: 'Maharashtra' }
|
||||
pincode: { type: string, example: '422001' }
|
||||
phone: { type: string, example: '0253-1234567' }
|
||||
location: { type: string, example: 'Ground Floor', description: Used for warehouse }
|
||||
is_active: { type: boolean, example: true }
|
||||
LocationsUpdateBody:
|
||||
type: object
|
||||
minProperties: 1
|
||||
properties:
|
||||
code: { type: string, example: 'PLT01' }
|
||||
name: { type: string, example: 'Bharat Plant 1' }
|
||||
parent_id: { type: integer, nullable: true, example: 1 }
|
||||
gstin: { type: string, example: '27ABCDE1234F1Z5' }
|
||||
address: { type: string, example: 'MIDC, Nashik' }
|
||||
city: { type: string, example: 'Nashik' }
|
||||
state: { type: string, example: 'Maharashtra' }
|
||||
pincode: { type: string, example: '422001' }
|
||||
phone: { type: string, example: '0253-1234567' }
|
||||
location: { type: string, example: 'Ground Floor' }
|
||||
is_active: { type: boolean, example: true }
|
||||
WarehousesCreateBody:
|
||||
type: object
|
||||
required: [code, name, plant_id]
|
||||
@ -1099,6 +1148,79 @@ paths:
|
||||
description: Deleted
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"404": { description: Not found }
|
||||
/masters/asset-subcategories:
|
||||
get:
|
||||
tags: [Asset Subcategories]
|
||||
summary: List Asset Subcategories
|
||||
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 }
|
||||
- in: query
|
||||
name: is_active
|
||||
schema: { type: boolean }
|
||||
- in: query
|
||||
name: asset_category_id
|
||||
schema: { type: integer }
|
||||
description: Filter by parent asset category
|
||||
responses:
|
||||
"200":
|
||||
description: List fetched
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
post:
|
||||
tags: [Asset Subcategories]
|
||||
summary: Create Asset Subcategory
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/AssetSubcategoriesCreateBody" }
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"409": { description: Duplicate key }
|
||||
/masters/asset-subcategories/{id}:
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: string }
|
||||
get:
|
||||
tags: [Asset Subcategories]
|
||||
summary: Get Asset Subcategory by id
|
||||
responses:
|
||||
"200":
|
||||
description: Fetched
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"404": { description: Not found }
|
||||
put:
|
||||
tags: [Asset Subcategories]
|
||||
summary: Update Asset Subcategory
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/AssetSubcategoriesUpdateBody" }
|
||||
responses:
|
||||
"200":
|
||||
description: Updated
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"404": { description: Not found }
|
||||
delete:
|
||||
tags: [Asset Subcategories]
|
||||
summary: Delete Asset Subcategory
|
||||
responses:
|
||||
"200":
|
||||
description: Deleted
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"404": { description: Not found }
|
||||
/masters/departments:
|
||||
get:
|
||||
tags: [Departments]
|
||||
@ -1237,6 +1359,82 @@ paths:
|
||||
description: Deleted
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"404": { description: Not found }
|
||||
/masters/locations:
|
||||
get:
|
||||
tags: [Locations]
|
||||
summary: List locations (plants and warehouses)
|
||||
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 }
|
||||
- in: query
|
||||
name: is_active
|
||||
schema: { type: boolean }
|
||||
- in: query
|
||||
name: type
|
||||
schema: { type: string, enum: [plant, warehouse] }
|
||||
- in: query
|
||||
name: parent_id
|
||||
schema: { type: integer }
|
||||
description: Filter warehouses by plant id
|
||||
responses:
|
||||
"200":
|
||||
description: List fetched
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
post:
|
||||
tags: [Locations]
|
||||
summary: Create location (plant or warehouse)
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/LocationsCreateBody" }
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"409": { description: Duplicate key }
|
||||
/masters/locations/{id}:
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: string }
|
||||
get:
|
||||
tags: [Locations]
|
||||
summary: Get location by id
|
||||
responses:
|
||||
"200":
|
||||
description: Fetched
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"404": { description: Not found }
|
||||
put:
|
||||
tags: [Locations]
|
||||
summary: Update location
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/LocationsUpdateBody" }
|
||||
responses:
|
||||
"200":
|
||||
description: Updated
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"404": { description: Not found }
|
||||
delete:
|
||||
tags: [Locations]
|
||||
summary: Delete location
|
||||
responses:
|
||||
"200":
|
||||
description: Deleted
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"404": { description: Not found }
|
||||
/masters/warehouses:
|
||||
get:
|
||||
tags: [Warehouses]
|
||||
|
||||
@ -20,6 +20,7 @@ components:
|
||||
storage_location: { type: string, example: 'Rack A-12' }
|
||||
remarks: { type: string, example: 'Partial receipt' }
|
||||
asset_category_id: { type: integer, nullable: true, example: 1, description: Required when PO item is an asset item }
|
||||
asset_subcategory_id: { type: integer, nullable: true, example: 1, description: Required when PO item is an asset item }
|
||||
GrnCreateBody:
|
||||
type: object
|
||||
required: [grn_date, po_id, warehouse_id, items]
|
||||
|
||||
@ -36,6 +36,9 @@ components:
|
||||
discount_amount: { type: number, example: 0 }
|
||||
freight_charges: { type: number, example: 0 }
|
||||
other_charges: { type: number, example: 0 }
|
||||
tds_applicable: { type: boolean, example: false }
|
||||
tds_section_pct: { type: number, nullable: true, example: 2 }
|
||||
adjustment: { type: number, example: -10 }
|
||||
terms_and_conditions: { type: string, example: 'Standard terms apply' }
|
||||
remarks: { type: string, example: 'Monthly RM order' }
|
||||
items:
|
||||
@ -59,6 +62,9 @@ components:
|
||||
discount_amount: { type: number }
|
||||
freight_charges: { type: number }
|
||||
other_charges: { type: number }
|
||||
tds_applicable: { type: boolean }
|
||||
tds_section_pct: { type: number, nullable: true }
|
||||
adjustment: { type: number }
|
||||
terms_and_conditions: { type: string }
|
||||
remarks: { type: string }
|
||||
items:
|
||||
|
||||
88
src/docs/settings-routes.yaml
Normal file
88
src/docs/settings-routes.yaml
Normal file
@ -0,0 +1,88 @@
|
||||
tags:
|
||||
- name: Settings
|
||||
|
||||
components:
|
||||
schemas:
|
||||
CompanySettingsBody:
|
||||
type: object
|
||||
minProperties: 1
|
||||
properties:
|
||||
org_name: { type: string, example: 'Bharat Consumer Products' }
|
||||
mobile: { type: string, example: '9876543210' }
|
||||
email: { type: string, format: email, example: 'info@company.com' }
|
||||
website: { type: string, example: 'https://www.company.com' }
|
||||
address: { type: string, example: '3rd Floor, Business Tower' }
|
||||
city: { type: string, example: 'Noida' }
|
||||
state: { type: string, example: 'Uttar Pradesh' }
|
||||
pincode: { type: string, example: '201301' }
|
||||
EmailSettingsBody:
|
||||
type: object
|
||||
minProperties: 1
|
||||
properties:
|
||||
smtp_host: { type: string, example: 'smtp.gmail.com' }
|
||||
smtp_port: { type: integer, example: 587 }
|
||||
smtp_username: { type: string, example: 'smtp-user@company.com' }
|
||||
smtp_password: { type: string, example: 'app-password' }
|
||||
sender_email: { type: string, format: email, example: 'noreply@company.com' }
|
||||
sender_name: { type: string, example: 'Bharat ERP' }
|
||||
|
||||
paths:
|
||||
/settings/company:
|
||||
get:
|
||||
tags: [Settings]
|
||||
summary: Get company profile settings
|
||||
responses:
|
||||
"200":
|
||||
description: Company settings fetched
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
put:
|
||||
tags: [Settings]
|
||||
summary: Update company profile settings
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/CompanySettingsBody" }
|
||||
responses:
|
||||
"200":
|
||||
description: Company settings updated
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
/settings/company/logo:
|
||||
post:
|
||||
tags: [Settings]
|
||||
summary: Upload company logo
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required: [logo]
|
||||
properties:
|
||||
logo:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
"200":
|
||||
description: Logo updated
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
/settings/email:
|
||||
get:
|
||||
tags: [Settings]
|
||||
summary: Get SMTP email settings
|
||||
responses:
|
||||
"200":
|
||||
description: Email settings fetched
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
put:
|
||||
tags: [Settings]
|
||||
summary: Update SMTP email settings
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/EmailSettingsBody" }
|
||||
responses:
|
||||
"200":
|
||||
description: Email settings updated
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
@ -12,6 +12,11 @@ components:
|
||||
type: string
|
||||
enum: [RAW_MATERIAL, PACKING_MATERIAL, ASSET_CAPITAL, SERVICE, GENERAL]
|
||||
example: RAW_MATERIAL
|
||||
gst_treatment:
|
||||
type: string
|
||||
enum: [REGULAR, COMPOSITION, UNREGISTERED, CONSUMER, SEZ, OVERSEAS, DEEMED_EXPORT, OTHER]
|
||||
example: REGULAR
|
||||
source_of_supply: { type: string, example: 'Maharashtra' }
|
||||
gstin: { type: string, example: '27ABCDE1234F1Z5' }
|
||||
pan: { type: string, example: 'ABCDE1234F' }
|
||||
payment_term_id: { type: integer, example: 1 }
|
||||
@ -26,6 +31,10 @@ components:
|
||||
vendor_type:
|
||||
type: string
|
||||
enum: [RAW_MATERIAL, PACKING_MATERIAL, ASSET_CAPITAL, SERVICE, GENERAL]
|
||||
gst_treatment:
|
||||
type: string
|
||||
enum: [REGULAR, COMPOSITION, UNREGISTERED, CONSUMER, SEZ, OVERSEAS, DEEMED_EXPORT, OTHER]
|
||||
source_of_supply: { type: string, example: 'Maharashtra' }
|
||||
gstin: { type: string, example: '27ABCDE1234F1Z5' }
|
||||
pan: { type: string, example: 'ABCDE1234F' }
|
||||
payment_term_id: { type: integer, example: 1 }
|
||||
@ -120,6 +129,21 @@ components:
|
||||
enum: [CURRENT, SAVINGS, OVERDRAFT]
|
||||
is_primary: { type: boolean, example: true }
|
||||
is_active: { type: boolean, example: true }
|
||||
VendorItemMappingsCreateBody:
|
||||
type: object
|
||||
required: [item_id]
|
||||
properties:
|
||||
item_id: { type: integer, example: 12 }
|
||||
last_purchase_rate: { type: number, example: 1250.5 }
|
||||
is_preferred: { type: boolean, example: false }
|
||||
is_active: { type: boolean, example: true }
|
||||
VendorItemMappingsUpdateBody:
|
||||
type: object
|
||||
minProperties: 1
|
||||
properties:
|
||||
last_purchase_rate: { type: number, example: 1300 }
|
||||
is_preferred: { type: boolean, example: true }
|
||||
is_active: { type: boolean, example: true }
|
||||
|
||||
paths:
|
||||
/vendors:
|
||||
@ -163,6 +187,22 @@ paths:
|
||||
"201":
|
||||
description: Created
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
/vendors/gst-treatments:
|
||||
get:
|
||||
tags: [Vendors]
|
||||
summary: List GST treatment dropdown options
|
||||
responses:
|
||||
"200":
|
||||
description: GST treatment options fetched
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
/vendors/source-of-supply:
|
||||
get:
|
||||
tags: [Vendors]
|
||||
summary: List source of supply dropdown options (Indian states and UTs)
|
||||
responses:
|
||||
"200":
|
||||
description: Source of supply options fetched
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
/vendors/{id}:
|
||||
parameters:
|
||||
- in: path
|
||||
@ -409,3 +449,68 @@ paths:
|
||||
description: Deleted
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"404": { description: Not found }
|
||||
/vendors/{vendorId}/items:
|
||||
parameters:
|
||||
- in: path
|
||||
name: vendorId
|
||||
required: true
|
||||
schema: { type: string }
|
||||
get:
|
||||
tags: [Vendors]
|
||||
summary: List vendor item mappings
|
||||
responses:
|
||||
"200":
|
||||
description: Fetched
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
post:
|
||||
tags: [Vendors]
|
||||
summary: Map item to vendor
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/VendorItemMappingsCreateBody" }
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"409": { description: Item already mapped }
|
||||
/vendors/{vendorId}/items/{mappingId}:
|
||||
parameters:
|
||||
- in: path
|
||||
name: vendorId
|
||||
required: true
|
||||
schema: { type: string }
|
||||
- in: path
|
||||
name: mappingId
|
||||
required: true
|
||||
schema: { type: string }
|
||||
get:
|
||||
tags: [Vendors]
|
||||
summary: Get vendor item mapping
|
||||
responses:
|
||||
"200":
|
||||
description: Fetched
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"404": { description: Not found }
|
||||
put:
|
||||
tags: [Vendors]
|
||||
summary: Update vendor item mapping
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/VendorItemMappingsUpdateBody" }
|
||||
responses:
|
||||
"200":
|
||||
description: Updated
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"404": { description: Not found }
|
||||
delete:
|
||||
tags: [Vendors]
|
||||
summary: Delete vendor item mapping
|
||||
responses:
|
||||
"200":
|
||||
description: Deleted
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"404": { description: Not found }
|
||||
|
||||
@ -15,7 +15,11 @@ const amcInclude = {
|
||||
const sanitizeAmc = (row) => {
|
||||
if (!row) return null;
|
||||
const { vendors, ...rest } = row;
|
||||
return { ...rest, vendor: vendors || null };
|
||||
return {
|
||||
...rest,
|
||||
annual_cost: rest.annual_cost !== null && rest.annual_cost !== undefined ? Number(rest.annual_cost) : 0,
|
||||
vendor: vendors || null,
|
||||
};
|
||||
};
|
||||
|
||||
const buildAmcData = async (payload, assetId) => {
|
||||
|
||||
@ -3,7 +3,15 @@ const ApiError = require('../../utils/ApiError');
|
||||
const auditLog = require('../../utils/auditLog');
|
||||
const { toDateOnly, getAssetOrThrow, deactivateOtherActive } = require('./assets.helpers');
|
||||
|
||||
const sanitizeInsurance = (row) => (row ? { ...row } : null);
|
||||
const sanitizeInsurance = (row) => {
|
||||
if (!row) return null;
|
||||
return {
|
||||
...row,
|
||||
sum_insured: row.sum_insured !== null && row.sum_insured !== undefined ? Number(row.sum_insured) : 0,
|
||||
annual_premium:
|
||||
row.annual_premium !== null && row.annual_premium !== undefined ? Number(row.annual_premium) : 0,
|
||||
};
|
||||
};
|
||||
|
||||
const buildInsuranceData = async (payload, assetId) => ({
|
||||
asset_id: BigInt(assetId),
|
||||
|
||||
@ -13,6 +13,12 @@ const sanitizeVisit = (row) => {
|
||||
const { vendors, asset_amc_contracts, ...rest } = row;
|
||||
return {
|
||||
...rest,
|
||||
downtime_hours:
|
||||
rest.downtime_hours !== null && rest.downtime_hours !== undefined
|
||||
? Number(rest.downtime_hours)
|
||||
: 0,
|
||||
service_cost:
|
||||
rest.service_cost !== null && rest.service_cost !== undefined ? Number(rest.service_cost) : 0,
|
||||
vendor: vendors || null,
|
||||
amc_contract: asset_amc_contracts || null,
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
const ASSET_CONDITIONS = ['NEW', 'GOOD', 'FAIR', 'POOR'];
|
||||
const ASSET_STATUSES = ['IN_USE', 'IDLE', 'UNDER_MAINTENANCE', 'DISPOSED', 'SCRAPPED'];
|
||||
const DEPRECIATION_METHODS = ['SLM', 'WDV'];
|
||||
const DEPRECIATION_METHODS = ['SLM', 'WDV', 'OTHER'];
|
||||
const DISPOSAL_STATUSES = ['DISPOSED', 'SCRAPPED'];
|
||||
|
||||
const AMC_CONTRACT_TYPES = ['COMPREHENSIVE', 'LABOUR_ONLY', 'PARTS_ONLY', 'PREVENTIVE_ONLY'];
|
||||
|
||||
@ -172,6 +172,16 @@ const serviceAlerts = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, result.data, 'Service alerts fetched', result.meta));
|
||||
});
|
||||
|
||||
const listDepreciationMethods = asyncHandler(async (_req, res) => {
|
||||
const data = service.listDepreciationMethods();
|
||||
res.json(new ApiResponse(200, data, 'Depreciation methods fetched'));
|
||||
});
|
||||
|
||||
const calculateDepreciation = asyncHandler(async (req, res) => {
|
||||
const data = service.previewDepreciation(req.body);
|
||||
res.json(new ApiResponse(200, data, 'Depreciation calculated'));
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
list,
|
||||
@ -180,6 +190,8 @@ module.exports = {
|
||||
remove,
|
||||
transfer,
|
||||
transferHistory,
|
||||
listDepreciationMethods,
|
||||
calculateDepreciation,
|
||||
listAmc,
|
||||
createAmc,
|
||||
getAmc,
|
||||
|
||||
141
src/modules/assets/assets.depreciation.js
Normal file
141
src/modules/assets/assets.depreciation.js
Normal file
@ -0,0 +1,141 @@
|
||||
const round4 = (value) => Math.round(Number(value || 0) * 10000) / 10000;
|
||||
|
||||
const DEPRECIATION_METHOD_OPTIONS = [
|
||||
{
|
||||
value: 'SLM',
|
||||
label: 'Straight Line Method (SLM)',
|
||||
description:
|
||||
'Equal depreciation each year on original cost. Rate auto-calculated from useful life when not provided.',
|
||||
},
|
||||
{
|
||||
value: 'WDV',
|
||||
label: 'Written Down Value (WDV)',
|
||||
description:
|
||||
'Depreciation on reducing book value. Rate auto-calculated from cost, salvage and useful life when not provided.',
|
||||
},
|
||||
{
|
||||
value: 'OTHER',
|
||||
label: 'Other',
|
||||
description: 'Custom method. Depreciation rate must be entered manually.',
|
||||
},
|
||||
];
|
||||
|
||||
const yearsElapsed = (purchaseDate, asOfDate = new Date()) => {
|
||||
if (!purchaseDate) return 0;
|
||||
const start = new Date(purchaseDate);
|
||||
const end = new Date(asOfDate);
|
||||
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || end < start) return 0;
|
||||
const ms = end.getTime() - start.getTime();
|
||||
return round4(ms / (365.25 * 24 * 60 * 60 * 1000));
|
||||
};
|
||||
|
||||
const resolveDepreciationRate = ({
|
||||
method,
|
||||
depreciation_rate: rate,
|
||||
purchase_cost: cost,
|
||||
salvage_value: salvage,
|
||||
useful_life_years: lifeYears,
|
||||
}) => {
|
||||
if (rate !== undefined && rate !== null && rate !== '') {
|
||||
return round4(rate);
|
||||
}
|
||||
|
||||
const purchaseCost = Number(cost || 0);
|
||||
const salvageValue = Number(salvage || 0);
|
||||
const years = Number(lifeYears || 0);
|
||||
|
||||
if (!method || method === 'OTHER' || years <= 0) return null;
|
||||
|
||||
if (method === '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 (purchaseCost <= 0 || salvageValue <= 0 || salvageValue >= purchaseCost) return null;
|
||||
return round4((1 - (salvageValue / purchaseCost) ** (1 / years)) * 100);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const calculateDepreciation = ({
|
||||
depreciation_method: method,
|
||||
depreciation_rate: rateInput,
|
||||
purchase_cost: cost,
|
||||
salvage_value: salvage,
|
||||
useful_life_years: lifeYears,
|
||||
purchase_date: purchaseDate,
|
||||
as_of_date: asOfDate,
|
||||
} = {}) => {
|
||||
const purchaseCost = Number(cost || 0);
|
||||
const salvageValue = Number(salvage || 0);
|
||||
const rate = resolveDepreciationRate({
|
||||
method,
|
||||
depreciation_rate: rateInput,
|
||||
purchase_cost: purchaseCost,
|
||||
salvage_value: salvageValue,
|
||||
useful_life_years: lifeYears,
|
||||
});
|
||||
|
||||
const elapsed = yearsElapsed(purchaseDate, asOfDate || new Date());
|
||||
const cappedYears =
|
||||
lifeYears && Number(lifeYears) > 0 ? Math.min(elapsed, Number(lifeYears)) : elapsed;
|
||||
|
||||
let annualDepreciation = 0;
|
||||
let accumulatedDepreciation = 0;
|
||||
let bookValue = purchaseCost;
|
||||
|
||||
if (method === '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) {
|
||||
annualDepreciation = round4((purchaseCost * rate) / 100);
|
||||
bookValue = purchaseCost;
|
||||
const fullYears = Math.floor(cappedYears);
|
||||
const fraction = cappedYears - fullYears;
|
||||
|
||||
for (let i = 0; i < fullYears; i += 1) {
|
||||
const yearDep = round4((bookValue * rate) / 100);
|
||||
bookValue = round4(Math.max(bookValue - yearDep, salvageValue));
|
||||
accumulatedDepreciation = round4(purchaseCost - bookValue);
|
||||
if (bookValue <= salvageValue) break;
|
||||
}
|
||||
|
||||
if (fraction > 0 && bookValue > salvageValue) {
|
||||
const yearDep = round4(((bookValue * rate) / 100) * fraction);
|
||||
bookValue = round4(Math.max(bookValue - yearDep, salvageValue));
|
||||
accumulatedDepreciation = round4(purchaseCost - bookValue);
|
||||
}
|
||||
|
||||
annualDepreciation = round4((bookValue * rate) / 100);
|
||||
} else if (method === 'OTHER' && 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));
|
||||
}
|
||||
|
||||
return {
|
||||
depreciation_method: method || null,
|
||||
depreciation_rate: rate,
|
||||
annual_depreciation: annualDepreciation,
|
||||
accumulated_depreciation: accumulatedDepreciation,
|
||||
book_value: bookValue,
|
||||
years_elapsed: elapsed,
|
||||
purchase_cost: purchaseCost,
|
||||
salvage_value: salvageValue,
|
||||
useful_life_years: lifeYears ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
DEPRECIATION_METHOD_OPTIONS,
|
||||
resolveDepreciationRate,
|
||||
calculateDepreciation,
|
||||
yearsElapsed,
|
||||
round4,
|
||||
};
|
||||
@ -10,6 +10,7 @@ const SOFT_DELETE_TABLES = new Set([
|
||||
'grn',
|
||||
'grn_items',
|
||||
'asset_categories',
|
||||
'asset_subcategories',
|
||||
]);
|
||||
|
||||
const toDateOnly = (value) => {
|
||||
|
||||
@ -18,6 +18,7 @@ const {
|
||||
insurancePolicySchema,
|
||||
updateInsurancePolicySchema,
|
||||
renewInsurancePolicySchema,
|
||||
depreciationCalculateSchema,
|
||||
expiryAlertsQuerySchema,
|
||||
serviceAlertsQuerySchema,
|
||||
} = require('./assets.validation');
|
||||
@ -47,6 +48,14 @@ router.get(
|
||||
controller.serviceAlerts
|
||||
);
|
||||
|
||||
router.get('/depreciation-methods', authorize('ASSET', 'view'), controller.listDepreciationMethods);
|
||||
router.post(
|
||||
'/depreciation/calculate',
|
||||
authorize('ASSET', 'view'),
|
||||
validate(depreciationCalculateSchema),
|
||||
controller.calculateDepreciation
|
||||
);
|
||||
|
||||
router.get('/:id/amc', authorize('ASSET', 'view'), controller.listAmc);
|
||||
router.post(
|
||||
'/:id/amc',
|
||||
|
||||
@ -6,6 +6,11 @@ const { nextDocumentNumber } = require('../../utils/generateCode');
|
||||
const { DISPOSAL_STATUSES } = require('./assets.constants');
|
||||
const { assertPlant, assertWarehouse } = require('../../utils/locations');
|
||||
const repository = require('./assets.repository');
|
||||
const {
|
||||
DEPRECIATION_METHOD_OPTIONS,
|
||||
resolveDepreciationRate,
|
||||
calculateDepreciation,
|
||||
} = require('./assets.depreciation');
|
||||
|
||||
const SOFT_DELETE_TABLES = new Set([
|
||||
'vendors',
|
||||
@ -16,10 +21,12 @@ const SOFT_DELETE_TABLES = new Set([
|
||||
'grn',
|
||||
'grn_items',
|
||||
'asset_categories',
|
||||
'asset_subcategories',
|
||||
]);
|
||||
|
||||
const assetInclude = {
|
||||
asset_categories: { select: { id: true, code: true, name: true, code_prefix: true } },
|
||||
asset_subcategories: { select: { id: true, code: true, name: true, asset_category_id: true } },
|
||||
plant: { select: { id: true, code: true, name: true } },
|
||||
departments: { select: { id: true, name: true } },
|
||||
warehouse: { select: { id: true, code: true, name: true } },
|
||||
@ -61,6 +68,7 @@ const sanitizeAsset = (asset) => {
|
||||
if (!asset) return null;
|
||||
const {
|
||||
asset_categories,
|
||||
asset_subcategories,
|
||||
plant,
|
||||
departments,
|
||||
warehouse,
|
||||
@ -73,9 +81,26 @@ const sanitizeAsset = (asset) => {
|
||||
...rest
|
||||
} = asset;
|
||||
|
||||
const purchaseCost =
|
||||
rest.purchase_cost !== null && rest.purchase_cost !== undefined
|
||||
? Number(rest.purchase_cost)
|
||||
: 0;
|
||||
const salvageValue =
|
||||
rest.salvage_value !== null && rest.salvage_value !== undefined
|
||||
? Number(rest.salvage_value)
|
||||
: 0;
|
||||
const depreciationRate =
|
||||
rest.depreciation_rate !== null && rest.depreciation_rate !== undefined
|
||||
? Number(rest.depreciation_rate)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
purchase_cost: purchaseCost,
|
||||
salvage_value: salvageValue,
|
||||
depreciation_rate: depreciationRate,
|
||||
asset_category: asset_categories || null,
|
||||
asset_subcategory: asset_subcategories || null,
|
||||
plant: plant || null,
|
||||
department: departments || null,
|
||||
warehouse: warehouse || null,
|
||||
@ -85,7 +110,16 @@ const sanitizeAsset = (asset) => {
|
||||
grn: grn || null,
|
||||
created_by_user: users_assets_created_byTousers || null,
|
||||
updated_by_user: users_assets_updated_byTousers || null,
|
||||
depreciation: calculateDepreciation({
|
||||
depreciation_method: rest.depreciation_method,
|
||||
depreciation_rate: depreciationRate,
|
||||
purchase_cost: purchaseCost,
|
||||
salvage_value: salvageValue,
|
||||
useful_life_years: rest.useful_life_years,
|
||||
purchase_date: rest.purchase_date,
|
||||
}),
|
||||
asset_categories: undefined,
|
||||
asset_subcategories: undefined,
|
||||
departments: undefined,
|
||||
users_assets_assigned_to_user_idTousers: undefined,
|
||||
vendors_assets_vendor_idTovendors: undefined,
|
||||
@ -143,12 +177,30 @@ const assertDisposalFields = (status, disposalDate) => {
|
||||
}
|
||||
};
|
||||
|
||||
const assertAssetSubcategory = async (subcategoryId, categoryId) => {
|
||||
const subcategory = await assertReference(
|
||||
'asset_subcategories',
|
||||
subcategoryId,
|
||||
'asset_subcategory_id'
|
||||
);
|
||||
if (subcategory.asset_category_id.toString() !== BigInt(categoryId).toString()) {
|
||||
throw new ApiError(422, 'asset_subcategory_id does not belong to the selected asset_category_id');
|
||||
}
|
||||
return subcategory;
|
||||
};
|
||||
|
||||
const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
|
||||
const category = await assertReference(
|
||||
'asset_categories',
|
||||
payload.asset_category_id,
|
||||
'asset_category_id'
|
||||
);
|
||||
if (isCreate && !payload.asset_subcategory_id) {
|
||||
throw new ApiError(422, 'asset_subcategory_id is required');
|
||||
}
|
||||
if (payload.asset_subcategory_id) {
|
||||
await assertAssetSubcategory(payload.asset_subcategory_id, payload.asset_category_id);
|
||||
}
|
||||
await assertPlant(payload.plant_id, 'plant_id');
|
||||
if (payload.department_id)
|
||||
await assertReference('departments', payload.department_id, 'department_id');
|
||||
@ -167,9 +219,33 @@ const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
|
||||
const disposalDate = payload.disposal_date ? toDateOnly(payload.disposal_date) : null;
|
||||
assertDisposalFields(status, disposalDate);
|
||||
|
||||
const purchaseCost = payload.purchase_cost ?? 0;
|
||||
const salvageValue = payload.salvage_value ?? 0;
|
||||
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;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
const depreciationRate = resolveDepreciationRate({
|
||||
method: depreciationMethod,
|
||||
depreciation_rate: payload.depreciation_rate,
|
||||
purchase_cost: purchaseCost,
|
||||
salvage_value: salvageValue,
|
||||
useful_life_years: usefulLifeYears,
|
||||
});
|
||||
|
||||
return {
|
||||
asset_name: payload.asset_name,
|
||||
asset_category_id: BigInt(payload.asset_category_id),
|
||||
asset_subcategory_id: payload.asset_subcategory_id
|
||||
? BigInt(payload.asset_subcategory_id)
|
||||
: null,
|
||||
brand_model: payload.brand_model || null,
|
||||
manufacturer: payload.manufacturer || null,
|
||||
serial_number: payload.serial_number || null,
|
||||
@ -184,14 +260,11 @@ const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
|
||||
grn_id: payload.grn_id ? BigInt(payload.grn_id) : null,
|
||||
grn_item_id: payload.grn_item_id ? BigInt(payload.grn_item_id) : null,
|
||||
purchase_date: payload.purchase_date ? toDateOnly(payload.purchase_date) : null,
|
||||
purchase_cost: payload.purchase_cost ?? 0,
|
||||
useful_life_years:
|
||||
payload.useful_life_years !== undefined && payload.useful_life_years !== null
|
||||
? payload.useful_life_years
|
||||
: category.default_useful_life_years,
|
||||
depreciation_method:
|
||||
payload.depreciation_method || category.default_depreciation_method || null,
|
||||
salvage_value: payload.salvage_value ?? 0,
|
||||
purchase_cost: purchaseCost,
|
||||
useful_life_years: usefulLifeYears,
|
||||
depreciation_method: depreciationMethod,
|
||||
depreciation_rate: depreciationRate,
|
||||
salvage_value: salvageValue,
|
||||
warranty_expiry_date: payload.warranty_expiry_date
|
||||
? toDateOnly(payload.warranty_expiry_date)
|
||||
: null,
|
||||
@ -252,6 +325,9 @@ const listAssets = async (query) => {
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(query.condition ? { condition: query.condition } : {}),
|
||||
...(query.asset_category_id ? { asset_category_id: BigInt(query.asset_category_id) } : {}),
|
||||
...(query.asset_subcategory_id
|
||||
? { asset_subcategory_id: BigInt(query.asset_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 } : {}),
|
||||
@ -288,6 +364,10 @@ const updateAsset = async (id, payload, userId, requestId) => {
|
||||
const merged = {
|
||||
asset_name: payload.asset_name ?? existing.asset_name,
|
||||
asset_category_id: payload.asset_category_id ?? existing.asset_category_id,
|
||||
asset_subcategory_id:
|
||||
payload.asset_subcategory_id !== undefined
|
||||
? payload.asset_subcategory_id
|
||||
: existing.asset_subcategory_id,
|
||||
brand_model: payload.brand_model !== undefined ? payload.brand_model : existing.brand_model,
|
||||
manufacturer: payload.manufacturer !== undefined ? payload.manufacturer : existing.manufacturer,
|
||||
serial_number:
|
||||
@ -318,6 +398,12 @@ const updateAsset = async (id, payload, userId, requestId) => {
|
||||
payload.depreciation_method !== undefined
|
||||
? payload.depreciation_method
|
||||
: existing.depreciation_method,
|
||||
depreciation_rate:
|
||||
payload.depreciation_rate !== undefined
|
||||
? payload.depreciation_rate
|
||||
: existing.depreciation_rate !== null && existing.depreciation_rate !== undefined
|
||||
? Number(existing.depreciation_rate)
|
||||
: null,
|
||||
salvage_value: payload.salvage_value ?? Number(existing.salvage_value ?? 0),
|
||||
warranty_expiry_date:
|
||||
payload.warranty_expiry_date !== undefined
|
||||
@ -468,6 +554,15 @@ const getTransferHistory = async (id) => {
|
||||
return rows.map(sanitizeTransfer);
|
||||
};
|
||||
|
||||
const listDepreciationMethods = () => DEPRECIATION_METHOD_OPTIONS;
|
||||
|
||||
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');
|
||||
}
|
||||
return calculateDepreciation(payload);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createAsset,
|
||||
listAssets,
|
||||
@ -476,4 +571,6 @@ module.exports = {
|
||||
deleteAsset,
|
||||
transferAsset,
|
||||
getTransferHistory,
|
||||
listDepreciationMethods,
|
||||
previewDepreciation,
|
||||
};
|
||||
|
||||
@ -17,6 +17,7 @@ const {
|
||||
const assetFields = {
|
||||
asset_name: Joi.string().max(200).required(),
|
||||
asset_category_id: Joi.number().integer().positive().required(),
|
||||
asset_subcategory_id: Joi.number().integer().positive().required(),
|
||||
brand_model: Joi.string().max(200).allow(null, '').optional(),
|
||||
manufacturer: Joi.string().max(200).allow(null, '').optional(),
|
||||
serial_number: Joi.string().max(100).allow(null, '').optional(),
|
||||
@ -37,6 +38,7 @@ const assetFields = {
|
||||
.valid(...DEPRECIATION_METHODS)
|
||||
.allow(null)
|
||||
.optional(),
|
||||
depreciation_rate: Joi.number().min(0).max(100).allow(null).optional(),
|
||||
salvage_value: Joi.number().min(0).allow(null).optional(),
|
||||
warranty_expiry_date: Joi.date().iso().allow(null).optional(),
|
||||
condition: Joi.string()
|
||||
@ -58,6 +60,7 @@ const createAssetSchema = Joi.object(assetFields);
|
||||
const updateAssetSchema = Joi.object({
|
||||
asset_name: assetFields.asset_name.optional(),
|
||||
asset_category_id: assetFields.asset_category_id.optional(),
|
||||
asset_subcategory_id: assetFields.asset_subcategory_id.optional(),
|
||||
brand_model: assetFields.brand_model,
|
||||
manufacturer: assetFields.manufacturer,
|
||||
serial_number: assetFields.serial_number,
|
||||
@ -75,6 +78,7 @@ const updateAssetSchema = Joi.object({
|
||||
purchase_cost: assetFields.purchase_cost.optional(),
|
||||
useful_life_years: assetFields.useful_life_years,
|
||||
depreciation_method: assetFields.depreciation_method,
|
||||
depreciation_rate: assetFields.depreciation_rate,
|
||||
salvage_value: assetFields.salvage_value,
|
||||
warranty_expiry_date: assetFields.warranty_expiry_date,
|
||||
condition: assetFields.condition.optional(),
|
||||
@ -98,6 +102,7 @@ const listAssetsQuerySchema = Joi.object({
|
||||
.valid(...ASSET_CONDITIONS)
|
||||
.optional(),
|
||||
asset_category_id: Joi.number().integer().positive().optional(),
|
||||
asset_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(),
|
||||
@ -267,6 +272,18 @@ const renewInsurancePolicySchema = Joi.object({
|
||||
remarks: Joi.string().allow(null, '').optional(),
|
||||
});
|
||||
|
||||
const depreciationCalculateSchema = Joi.object({
|
||||
depreciation_method: Joi.string()
|
||||
.valid(...DEPRECIATION_METHODS)
|
||||
.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),
|
||||
useful_life_years: Joi.number().integer().min(0).allow(null).optional(),
|
||||
purchase_date: Joi.date().iso().allow(null).optional(),
|
||||
as_of_date: Joi.date().iso().allow(null).optional(),
|
||||
});
|
||||
|
||||
const expiryAlertsQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
@ -299,6 +316,7 @@ module.exports = {
|
||||
insurancePolicySchema,
|
||||
updateInsurancePolicySchema,
|
||||
renewInsurancePolicySchema,
|
||||
depreciationCalculateSchema,
|
||||
expiryAlertsQuerySchema,
|
||||
serviceAlertsQuerySchema,
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@ const { recalculatePoStatus } = require('./grn.poStatus');
|
||||
|
||||
const createAssetsForLine = async (
|
||||
tx,
|
||||
{ grn, grnItem, item, assetCategory, plantId, assetCodes, userId }
|
||||
{ grn, grnItem, item, assetCategory, assetSubcategory, plantId, assetCodes, userId }
|
||||
) => {
|
||||
const createdAssets = [];
|
||||
|
||||
@ -14,6 +14,7 @@ const createAssetsForLine = async (
|
||||
asset_code: assetCodes[index],
|
||||
asset_name: assetCodes.length > 1 ? `${item.item_name} #${index + 1}` : item.item_name,
|
||||
asset_category_id: assetCategory.id,
|
||||
asset_subcategory_id: assetSubcategory.id,
|
||||
plant_id: BigInt(plantId),
|
||||
warehouse_id: grn.warehouse_id,
|
||||
vendor_id: grn.vendor_id,
|
||||
@ -67,6 +68,7 @@ const createGrnWithReceipt = async ({ grnNumber, header, items, assetPlans, user
|
||||
grnItem,
|
||||
item: plan.item,
|
||||
assetCategory: plan.assetCategory,
|
||||
assetSubcategory: plan.assetSubcategory,
|
||||
plantId: plan.plantId,
|
||||
assetCodes: plan.assetCodes,
|
||||
userId,
|
||||
|
||||
@ -181,12 +181,31 @@ const validateAndBuildItems = async (po, payloadItems) => {
|
||||
`Line ${row.line_no}: asset_category_id is required for asset items`
|
||||
);
|
||||
}
|
||||
if (!row.asset_subcategory_id) {
|
||||
throw new ApiError(
|
||||
422,
|
||||
`Line ${row.line_no}: asset_subcategory_id is required for asset items`
|
||||
);
|
||||
}
|
||||
|
||||
const assetCategory = await prisma.asset_categories.findFirst({
|
||||
where: { id: BigInt(row.asset_category_id), deleted_at: null, is_active: true },
|
||||
});
|
||||
if (!assetCategory) throw new ApiError(422, `Line ${row.line_no}: invalid asset_category_id`);
|
||||
|
||||
const assetSubcategory = await prisma.asset_subcategories.findFirst({
|
||||
where: { id: BigInt(row.asset_subcategory_id), deleted_at: null, is_active: true },
|
||||
});
|
||||
if (!assetSubcategory) {
|
||||
throw new ApiError(422, `Line ${row.line_no}: invalid asset_subcategory_id`);
|
||||
}
|
||||
if (assetSubcategory.asset_category_id.toString() !== assetCategory.id.toString()) {
|
||||
throw new ApiError(
|
||||
422,
|
||||
`Line ${row.line_no}: asset_subcategory_id does not belong to asset_category_id`
|
||||
);
|
||||
}
|
||||
|
||||
const units = Math.floor(acceptedQty);
|
||||
const assetCodes = [];
|
||||
for (let unit = 0; unit < units; unit += 1) {
|
||||
@ -197,6 +216,7 @@ const validateAndBuildItems = async (po, payloadItems) => {
|
||||
po_item_id: poItem.id,
|
||||
item,
|
||||
assetCategory,
|
||||
assetSubcategory,
|
||||
plantId: po.plant_id,
|
||||
assetCodes,
|
||||
});
|
||||
|
||||
@ -15,6 +15,7 @@ const grnItemSchema = Joi.object({
|
||||
storage_location: Joi.string().max(100).allow(null, '').optional(),
|
||||
remarks: Joi.string().allow(null, '').optional(),
|
||||
asset_category_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
asset_subcategory_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
});
|
||||
|
||||
const createGrnSchema = Joi.object({
|
||||
|
||||
@ -12,7 +12,7 @@ const normalizePayload = (payload, fields) => {
|
||||
if (f.uppercase) out[f.name] = out[f.name].toUpperCase();
|
||||
}
|
||||
if (f.type === 'int' && out[f.name] !== null && out[f.name] !== undefined) {
|
||||
out[f.name] = Number(out[f.name]);
|
||||
out[f.name] = f.asBigInt ? BigInt(out[f.name]) : Number(out[f.name]);
|
||||
}
|
||||
if (f.type === 'decimal' && out[f.name] !== null && out[f.name] !== undefined) {
|
||||
out[f.name] = Number(out[f.name]);
|
||||
@ -78,6 +78,13 @@ const buildMasterService = ({
|
||||
: {}),
|
||||
};
|
||||
|
||||
for (const f of fields) {
|
||||
if (!f.filterable || query[f.name] === undefined || query[f.name] === null || query[f.name] === '') {
|
||||
continue;
|
||||
}
|
||||
where[f.name] = f.type === 'int' ? BigInt(query[f.name]) : query[f.name];
|
||||
}
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
model.findMany({ where, orderBy: { created_at: 'desc' }, skip, take: limit }),
|
||||
model.count({ where }),
|
||||
|
||||
80
src/modules/masters/_shared/masters.validation.js
Normal file
80
src/modules/masters/_shared/masters.validation.js
Normal file
@ -0,0 +1,80 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const MASTER_CODE_REGEX = /^[A-Za-z0-9\-_/]+$/;
|
||||
const MASTER_NAME_REGEX = /^[A-Za-z0-9 \-_/&.()]+$/;
|
||||
|
||||
const MASTER_CODE_MESSAGE = 'Allowed characters: A-Z, a-z, 0-9, -, _, /';
|
||||
const MASTER_NAME_MESSAGE = 'Allowed characters: A-Z, a-z, 0-9, space, -, _, /, &, ., (, )';
|
||||
|
||||
const validateMasterCode = (value, helpers) => {
|
||||
if (value === undefined || value === null) return value;
|
||||
const str = String(value).trim();
|
||||
if (!str) return helpers.error('string.masterCodeEmpty');
|
||||
if (!MASTER_CODE_REGEX.test(str)) return helpers.error('string.masterCode');
|
||||
return str;
|
||||
};
|
||||
|
||||
const validateMasterName = (value, helpers) => {
|
||||
if (value === undefined || value === null) return value;
|
||||
const str = String(value).trim();
|
||||
if (!str) return helpers.error('string.masterNameEmpty');
|
||||
if (!MASTER_NAME_REGEX.test(str)) return helpers.error('string.masterName');
|
||||
return str;
|
||||
};
|
||||
|
||||
const validateMasterCodeOptional = (value, helpers) => {
|
||||
if (value === undefined || value === null || value === '') return value === '' ? '' : value;
|
||||
const str = String(value).trim();
|
||||
if (!str) return '';
|
||||
if (!MASTER_CODE_REGEX.test(str)) return helpers.error('string.masterCode');
|
||||
return str;
|
||||
};
|
||||
|
||||
const validateMasterNameOptional = (value, helpers) => {
|
||||
if (value === undefined || value === null || value === '') return value === '' ? '' : value;
|
||||
const str = String(value).trim();
|
||||
if (!str) return '';
|
||||
if (!MASTER_NAME_REGEX.test(str)) return helpers.error('string.masterName');
|
||||
return str;
|
||||
};
|
||||
|
||||
const masterCodeMessages = {
|
||||
'string.masterCode': MASTER_CODE_MESSAGE,
|
||||
'string.masterCodeEmpty': 'code is required',
|
||||
'any.required': 'code is required',
|
||||
};
|
||||
|
||||
const masterNameMessages = {
|
||||
'string.masterName': MASTER_NAME_MESSAGE,
|
||||
'string.masterNameEmpty': 'name is required',
|
||||
'any.required': 'name is required',
|
||||
};
|
||||
|
||||
const masterCode = ({ max = 30, required = false } = {}) => {
|
||||
const validator = required ? validateMasterCode : validateMasterCodeOptional;
|
||||
let schema = Joi.string().max(max).custom(validator).messages(masterCodeMessages);
|
||||
return required ? schema.required() : schema.optional();
|
||||
};
|
||||
|
||||
const masterName = ({ max = 150, required = false } = {}) => {
|
||||
const validator = required ? validateMasterName : validateMasterNameOptional;
|
||||
let schema = Joi.string().max(max).custom(validator).messages(masterNameMessages);
|
||||
return required ? schema.required() : schema.optional();
|
||||
};
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
MASTER_CODE_REGEX,
|
||||
MASTER_NAME_REGEX,
|
||||
MASTER_CODE_MESSAGE,
|
||||
MASTER_NAME_MESSAGE,
|
||||
masterCode,
|
||||
masterName,
|
||||
listQuerySchema,
|
||||
};
|
||||
@ -1,21 +1,29 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
const { DEPRECIATION_METHODS } = require('../../assets/assets.constants');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
code_prefix: Joi.string().allow('').optional(),
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
code_prefix: masterCode({ max: 20 }),
|
||||
default_useful_life_years: Joi.number().integer().optional(),
|
||||
default_depreciation_method: Joi.string().allow('').optional(),
|
||||
default_depreciation_method: Joi.string()
|
||||
.valid(...DEPRECIATION_METHODS)
|
||||
.allow(null, '')
|
||||
.optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
const updateSchema = Joi.object({
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
code_prefix: masterCode({ max: 20 }),
|
||||
default_useful_life_years: Joi.number().integer().optional(),
|
||||
default_depreciation_method: Joi.string()
|
||||
.valid(...DEPRECIATION_METHODS)
|
||||
.allow(null, '')
|
||||
.optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
const asyncHandler = require('../../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../../utils/ApiResponse');
|
||||
const service = require('./asset-subcategories.service');
|
||||
|
||||
const create = asyncHandler(async (req, res) => {
|
||||
const data = await service.createAssetSubcategories(req.body, req.user?.id, req.id);
|
||||
res.status(201).json(new ApiResponse(201, data, 'asset_subcategories created successfully'));
|
||||
});
|
||||
|
||||
const list = asyncHandler(async (req, res) => {
|
||||
const result = await service.listAssetSubcategories(req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'asset_subcategories list fetched', result.meta));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getAssetSubcategoriesById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'asset_subcategories fetched'));
|
||||
});
|
||||
|
||||
const update = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateAssetSubcategories(
|
||||
req.params.id,
|
||||
req.body,
|
||||
req.user?.id,
|
||||
req.id
|
||||
);
|
||||
res.json(new ApiResponse(200, data, 'asset_subcategories updated successfully'));
|
||||
});
|
||||
|
||||
const remove = asyncHandler(async (req, res) => {
|
||||
await service.deleteAssetSubcategories(req.params.id, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, null, 'asset_subcategories deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, getOne, update, remove };
|
||||
@ -0,0 +1,17 @@
|
||||
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('./asset-subcategories.controller');
|
||||
const { createSchema, updateSchema, listQuerySchema } = require('./asset-subcategories.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
||||
|
||||
module.exports = router;
|
||||
@ -0,0 +1,81 @@
|
||||
const prisma = require('../../../config/prisma');
|
||||
const ApiError = require('../../../utils/ApiError');
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const config = {
|
||||
modelName: 'asset_subcategories',
|
||||
tableName: 'asset_subcategories',
|
||||
uniqueField: 'code',
|
||||
softDelete: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'asset_category_id',
|
||||
type: 'int',
|
||||
asBigInt: true,
|
||||
filterable: true,
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'code',
|
||||
type: 'string',
|
||||
uppercase: true,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
uppercase: false,
|
||||
searchable: true,
|
||||
},
|
||||
{
|
||||
name: 'is_active',
|
||||
type: 'boolean',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'created_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
name: 'updated_by',
|
||||
type: 'int',
|
||||
uppercase: false,
|
||||
searchable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const service = buildMasterService(config);
|
||||
|
||||
const assertAssetCategory = async (assetCategoryId) => {
|
||||
const category = await prisma.asset_categories.findFirst({
|
||||
where: { id: BigInt(assetCategoryId), deleted_at: null },
|
||||
});
|
||||
if (!category) throw new ApiError(422, 'Invalid asset_category_id');
|
||||
if (category.is_active === false) throw new ApiError(422, 'asset_category_id is inactive');
|
||||
return category;
|
||||
};
|
||||
|
||||
const createAssetSubcategories = async (payload, userId, requestId) => {
|
||||
await assertAssetCategory(payload.asset_category_id);
|
||||
return service.createOne(payload, userId, requestId);
|
||||
};
|
||||
|
||||
const updateAssetSubcategories = async (id, payload, userId, requestId) => {
|
||||
if (payload.asset_category_id !== undefined) {
|
||||
await assertAssetCategory(payload.asset_category_id);
|
||||
}
|
||||
return service.updateOne(id, payload, userId, requestId);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createAssetSubcategories,
|
||||
listAssetSubcategories: service.list,
|
||||
getAssetSubcategoriesById: service.getOne,
|
||||
updateAssetSubcategories,
|
||||
deleteAssetSubcategories: service.removeOne,
|
||||
};
|
||||
@ -0,0 +1,26 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
asset_category_id: Joi.number().integer().positive().required(),
|
||||
code: masterCode({ max: 30, required: true }),
|
||||
name: masterName({ max: 150, required: true }),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = Joi.object({
|
||||
asset_category_id: Joi.number().integer().positive().optional(),
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
const listAssetSubcategoriesQuerySchema = listQuerySchema.keys({
|
||||
asset_category_id: Joi.number().integer().positive().optional(),
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
createSchema,
|
||||
updateSchema,
|
||||
listQuerySchema: listAssetSubcategoriesQuerySchema,
|
||||
};
|
||||
@ -1,28 +1,28 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const BRAND_TYPES = ['OWN', 'OEM', 'THIRD_PARTY'];
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().max(30).required(),
|
||||
name: Joi.string().max(150).required(),
|
||||
code: masterCode({ max: 30, required: true }),
|
||||
name: masterName({ max: 150, required: true }),
|
||||
brand_type: Joi.string()
|
||||
.valid(...BRAND_TYPES)
|
||||
.default('OWN'),
|
||||
contact_person: Joi.string().max(100).allow(null, '').optional(),
|
||||
contact_person: masterName({ max: 100 }).allow(null),
|
||||
phone: Joi.string().max(20).allow(null, '').optional(),
|
||||
email: Joi.string().email().max(200).allow(null, '').optional(),
|
||||
is_active: Joi.boolean().default(true),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema
|
||||
.fork(['code', 'name', 'brand_type'], (schema) => schema.optional())
|
||||
.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
const updateSchema = Joi.object({
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
brand_type: Joi.string().valid(...BRAND_TYPES).optional(),
|
||||
contact_person: masterName({ max: 100 }).allow(null),
|
||||
phone: Joi.string().max(20).allow(null, '').optional(),
|
||||
email: Joi.string().email().max(200).allow(null, '').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema, BRAND_TYPES };
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
description: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
const updateSchema = Joi.object({
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
description: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
|
||||
@ -1,17 +1,14 @@
|
||||
const Joi = require('joi');
|
||||
const { masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
name: Joi.string().allow('').optional(),
|
||||
name: masterName({ max: 150 }),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
const updateSchema = Joi.object({
|
||||
name: masterName({ max: 150 }),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
|
||||
@ -1,17 +1,14 @@
|
||||
const Joi = require('joi');
|
||||
const { masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
name: Joi.string().allow('').optional(),
|
||||
name: masterName({ max: 150 }),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
const updateSchema = Joi.object({
|
||||
name: masterName({ max: 150 }),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
|
||||
@ -1,21 +1,22 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, listQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
prefix: Joi.string().allow('').optional(),
|
||||
code: masterCode({ max: 30 }),
|
||||
prefix: masterCode({ max: 30 }),
|
||||
current_number: Joi.number().integer().optional(),
|
||||
padding: Joi.number().integer().optional(),
|
||||
description: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
const updateSchema = Joi.object({
|
||||
code: masterCode({ max: 30 }),
|
||||
prefix: masterCode({ max: 30 }),
|
||||
current_number: Joi.number().integer().optional(),
|
||||
padding: Joi.number().integer().optional(),
|
||||
description: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
|
||||
@ -9,6 +9,7 @@ router.use('/gst-rates', require('./gst-rates/gst-rates.routes'));
|
||||
router.use('/payment-terms', require('./payment-terms/payment-terms.routes'));
|
||||
router.use('/delivery-terms', require('./delivery-terms/delivery-terms.routes'));
|
||||
router.use('/asset-categories', require('./asset-categories/asset-categories.routes'));
|
||||
router.use('/asset-subcategories', require('./asset-subcategories/asset-subcategories.routes'));
|
||||
router.use('/departments', require('./departments/departments.routes'));
|
||||
router.use('/designations', require('./designations/designations.routes'));
|
||||
router.use('/document-series', require('./document-series/document-series.routes'));
|
||||
|
||||
@ -1,18 +1,16 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
const updateSchema = Joi.object({
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
item_category_id: Joi.number().integer().optional(),
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
const updateSchema = Joi.object({
|
||||
item_category_id: Joi.number().integer().optional(),
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
item_code: Joi.string().allow('').optional(),
|
||||
item_name: Joi.string().allow('').optional(),
|
||||
item_code: masterCode({ max: 30 }),
|
||||
item_name: masterName({ max: 200 }),
|
||||
item_category_id: Joi.number().integer().optional(),
|
||||
item_subcategory_id: Joi.number().integer().optional(),
|
||||
uom_id: Joi.number().integer().optional(),
|
||||
@ -17,13 +18,21 @@ const createSchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
const updateSchema = Joi.object({
|
||||
item_code: masterCode({ max: 30 }),
|
||||
item_name: masterName({ max: 200 }),
|
||||
item_category_id: Joi.number().integer().optional(),
|
||||
item_subcategory_id: Joi.number().integer().optional(),
|
||||
uom_id: Joi.number().integer().optional(),
|
||||
hsn_code_id: Joi.number().integer().optional(),
|
||||
gst_rate_id: Joi.number().integer().optional(),
|
||||
brand_id: Joi.number().integer().optional(),
|
||||
is_asset_item: Joi.boolean().optional(),
|
||||
description: Joi.string().allow('').optional(),
|
||||
specification: Joi.string().allow('').optional(),
|
||||
min_order_qty: Joi.number().precision(4).optional(),
|
||||
reorder_level: Joi.number().precision(4).optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
|
||||
@ -100,10 +100,11 @@ const createLocation = async (type, payload, userId, requestId) => {
|
||||
|
||||
const listLocations = async (query, type = null) => {
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
const effectiveType = type || query.type || null;
|
||||
|
||||
const where = {
|
||||
deleted_at: null,
|
||||
...(type ? { type } : {}),
|
||||
...(effectiveType ? { type: effectiveType } : {}),
|
||||
...(query.parent_id ? { parent_id: BigInt(query.parent_id) } : {}),
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.search
|
||||
@ -119,7 +120,10 @@ const listLocations = async (query, type = null) => {
|
||||
const [rows, total] = await Promise.all([
|
||||
prisma.locations.findMany({
|
||||
where,
|
||||
include: type === LOCATION_TYPES.WAREHOUSE ? warehouseInclude : undefined,
|
||||
include:
|
||||
effectiveType === LOCATION_TYPES.WAREHOUSE || !effectiveType
|
||||
? warehouseInclude
|
||||
: undefined,
|
||||
orderBy: { created_at: 'desc' },
|
||||
skip,
|
||||
take: limit,
|
||||
@ -128,8 +132,8 @@ const listLocations = async (query, type = null) => {
|
||||
]);
|
||||
|
||||
const mapRow = (row) => {
|
||||
if (type === LOCATION_TYPES.PLANT) return toPlantResponse(row);
|
||||
if (type === LOCATION_TYPES.WAREHOUSE) return toWarehouseResponse(row);
|
||||
if (effectiveType === LOCATION_TYPES.PLANT) return toPlantResponse(row);
|
||||
if (effectiveType === LOCATION_TYPES.WAREHOUSE) return toWarehouseResponse(row);
|
||||
return toLocationResponse(row);
|
||||
};
|
||||
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const locationFields = {
|
||||
code: Joi.string().max(30).optional(),
|
||||
name: Joi.string().max(150).optional(),
|
||||
gstin: Joi.string().max(15).allow(null, '').optional(),
|
||||
address: Joi.string().allow(null, '').optional(),
|
||||
city: Joi.string().max(100).allow(null, '').optional(),
|
||||
@ -16,8 +15,8 @@ const locationFields = {
|
||||
};
|
||||
|
||||
const createPlantSchema = Joi.object({
|
||||
code: Joi.string().max(30).required(),
|
||||
name: Joi.string().max(150).required(),
|
||||
code: masterCode({ max: 30, required: true }),
|
||||
name: masterName({ max: 150, required: true }),
|
||||
gstin: locationFields.gstin,
|
||||
address: locationFields.address,
|
||||
city: locationFields.city,
|
||||
@ -27,24 +26,38 @@ const createPlantSchema = Joi.object({
|
||||
is_active: Joi.boolean().default(true),
|
||||
});
|
||||
|
||||
const updatePlantSchema = createPlantSchema.fork(['code', 'name'], (schema) => schema.optional()).min(1);
|
||||
const updatePlantSchema = Joi.object({
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
gstin: locationFields.gstin,
|
||||
address: locationFields.address,
|
||||
city: locationFields.city,
|
||||
state: locationFields.state,
|
||||
pincode: locationFields.pincode,
|
||||
phone: locationFields.phone,
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
const createWarehouseSchema = Joi.object({
|
||||
code: Joi.string().max(30).required(),
|
||||
name: Joi.string().max(150).required(),
|
||||
code: masterCode({ max: 30, required: true }),
|
||||
name: masterName({ max: 150, required: true }),
|
||||
plant_id: Joi.number().integer().positive().required(),
|
||||
location: locationFields.location,
|
||||
is_active: Joi.boolean().default(true),
|
||||
});
|
||||
|
||||
const updateWarehouseSchema = createWarehouseSchema
|
||||
.fork(['code', 'name', 'plant_id'], (schema) => schema.optional())
|
||||
.min(1);
|
||||
const updateWarehouseSchema = Joi.object({
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
plant_id: Joi.number().integer().positive().optional(),
|
||||
location: locationFields.location,
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
const createLocationSchema = Joi.object({
|
||||
type: Joi.string().valid('plant', 'warehouse').required(),
|
||||
code: Joi.string().max(30).required(),
|
||||
name: Joi.string().max(150).required(),
|
||||
code: masterCode({ max: 30, required: true }),
|
||||
name: masterName({ max: 150, required: true }),
|
||||
parent_id: Joi.when('type', {
|
||||
is: 'warehouse',
|
||||
then: Joi.number().integer().positive().required(),
|
||||
@ -61,8 +74,8 @@ const createLocationSchema = Joi.object({
|
||||
});
|
||||
|
||||
const updateLocationSchema = Joi.object({
|
||||
code: Joi.string().max(30).optional(),
|
||||
name: Joi.string().max(150).optional(),
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
parent_id: Joi.number().integer().positive().optional(),
|
||||
gstin: locationFields.gstin,
|
||||
address: locationFields.address,
|
||||
@ -74,11 +87,7 @@ const updateLocationSchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
const listLocationsQuerySchema = listQuerySchema.keys({
|
||||
type: Joi.string().valid('plant', 'warehouse').optional(),
|
||||
parent_id: Joi.number().integer().positive().optional(),
|
||||
});
|
||||
@ -90,5 +99,5 @@ module.exports = {
|
||||
updateWarehouseSchema,
|
||||
createLocationSchema,
|
||||
updateLocationSchema,
|
||||
listQuerySchema,
|
||||
listQuerySchema: listLocationsQuerySchema,
|
||||
};
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
credit_days: Joi.number().integer().optional(),
|
||||
description: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
const updateSchema = Joi.object({
|
||||
code: masterCode({ max: 30 }),
|
||||
name: masterName({ max: 150 }),
|
||||
credit_days: Joi.number().integer().optional(),
|
||||
description: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
}).min(1);
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
|
||||
@ -1,24 +1,7 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
gstin: Joi.string().allow('').optional(),
|
||||
address: Joi.string().allow('').optional(),
|
||||
city: Joi.string().allow('').optional(),
|
||||
state: Joi.string().allow('').optional(),
|
||||
pincode: Joi.string().allow('').optional(),
|
||||
phone: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
const {
|
||||
createPlantSchema: createSchema,
|
||||
updatePlantSchema: updateSchema,
|
||||
listQuerySchema,
|
||||
} = require('../locations/locations.validation');
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
|
||||
@ -1,22 +1,16 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||
|
||||
const createUomSchema = Joi.object({
|
||||
code: Joi.string().max(20).required(),
|
||||
name: Joi.string().max(100).required(),
|
||||
code: masterCode({ max: 20, required: true }),
|
||||
name: masterName({ max: 100, required: true }),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateUomSchema = Joi.object({
|
||||
code: Joi.string().max(20).optional(),
|
||||
name: Joi.string().max(100).optional(),
|
||||
code: masterCode({ max: 20 }),
|
||||
name: masterName({ max: 100 }),
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
const listUomQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
module.exports = { createUomSchema, updateUomSchema, listUomQuerySchema };
|
||||
module.exports = { createUomSchema, updateUomSchema, listUomQuerySchema: listQuerySchema };
|
||||
|
||||
@ -1,20 +1,7 @@
|
||||
const Joi = require('joi');
|
||||
|
||||
const createSchema = Joi.object({
|
||||
code: Joi.string().allow('').optional(),
|
||||
name: Joi.string().allow('').optional(),
|
||||
plant_id: Joi.number().integer().optional(),
|
||||
location: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const updateSchema = createSchema.min(1);
|
||||
|
||||
const listQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
const {
|
||||
createWarehouseSchema: createSchema,
|
||||
updateWarehouseSchema: updateSchema,
|
||||
listQuerySchema,
|
||||
} = require('../locations/locations.validation');
|
||||
|
||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
||||
|
||||
@ -24,14 +24,28 @@ const computeLineAmounts = (line, gstRatePct = 0) => {
|
||||
};
|
||||
};
|
||||
|
||||
const computeHeaderTotals = (lines, headerDiscount = 0, freight = 0, other = 0) => {
|
||||
const computeHeaderTotals = (
|
||||
lines,
|
||||
headerDiscount = 0,
|
||||
freight = 0,
|
||||
other = 0,
|
||||
{ tdsApplicable = false, tdsSectionPct = 0, adjustment = 0 } = {}
|
||||
) => {
|
||||
const subTotal = round4(lines.reduce((sum, line) => sum + toNum(line.taxable_amount), 0));
|
||||
const taxTotal = round4(lines.reduce((sum, line) => sum + toNum(line.tax_amount), 0));
|
||||
const headerDiscountAmount = round4(headerDiscount);
|
||||
const freightCharges = round4(freight);
|
||||
const otherCharges = round4(other);
|
||||
const tdsAmount = tdsApplicable ? round4((subTotal * toNum(tdsSectionPct)) / 100) : 0;
|
||||
const adjustmentAmount = round4(adjustment);
|
||||
const grandTotal = round4(
|
||||
subTotal + taxTotal + freightCharges + otherCharges - headerDiscountAmount
|
||||
subTotal +
|
||||
taxTotal +
|
||||
freightCharges +
|
||||
otherCharges -
|
||||
headerDiscountAmount -
|
||||
tdsAmount +
|
||||
adjustmentAmount
|
||||
);
|
||||
|
||||
return {
|
||||
@ -40,6 +54,8 @@ const computeHeaderTotals = (lines, headerDiscount = 0, freight = 0, other = 0)
|
||||
discount_amount: headerDiscountAmount,
|
||||
freight_charges: freightCharges,
|
||||
other_charges: otherCharges,
|
||||
tds_amount: tdsAmount,
|
||||
adjustment: adjustmentAmount,
|
||||
grand_total: grandTotal,
|
||||
};
|
||||
};
|
||||
|
||||
@ -12,7 +12,7 @@ const {
|
||||
AMENDABLE_STATUSES,
|
||||
DELETABLE_STATUSES,
|
||||
} = require('./purchase-orders.constants');
|
||||
const { computeLineAmounts, computeHeaderTotals } = require('./purchase-orders.calculations');
|
||||
const { computeLineAmounts, computeHeaderTotals, toNum } = require('./purchase-orders.calculations');
|
||||
const repository = require('./purchase-orders.repository');
|
||||
const { assertPlant, assertWarehouse } = require('../../utils/locations');
|
||||
|
||||
@ -215,11 +215,19 @@ const buildHeaderData = async (payload, builtItems, userId) => {
|
||||
if (payload.delivery_term_id)
|
||||
await assertReference('delivery_terms', payload.delivery_term_id, 'delivery_term_id');
|
||||
|
||||
const tdsApplicable = Boolean(payload.tds_applicable);
|
||||
const tdsSectionPct = tdsApplicable ? toNum(payload.tds_section_pct) : 0;
|
||||
|
||||
const totals = computeHeaderTotals(
|
||||
builtItems,
|
||||
payload.discount_amount,
|
||||
payload.freight_charges,
|
||||
payload.other_charges
|
||||
payload.other_charges,
|
||||
{
|
||||
tdsApplicable,
|
||||
tdsSectionPct,
|
||||
adjustment: payload.adjustment,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
@ -236,6 +244,8 @@ const buildHeaderData = async (payload, builtItems, userId) => {
|
||||
: null,
|
||||
terms_and_conditions: payload.terms_and_conditions || null,
|
||||
remarks: payload.remarks || null,
|
||||
tds_applicable: tdsApplicable,
|
||||
tds_section_pct: tdsApplicable ? tdsSectionPct : null,
|
||||
...totals,
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
};
|
||||
@ -337,6 +347,16 @@ const updatePurchaseOrder = async (id, payload, userId, requestId) => {
|
||||
discount_amount: payload.discount_amount ?? Number(existing.discount_amount),
|
||||
freight_charges: payload.freight_charges ?? Number(existing.freight_charges),
|
||||
other_charges: payload.other_charges ?? Number(existing.other_charges),
|
||||
tds_applicable:
|
||||
payload.tds_applicable !== undefined ? payload.tds_applicable : existing.tds_applicable,
|
||||
tds_section_pct:
|
||||
payload.tds_section_pct !== undefined
|
||||
? payload.tds_section_pct
|
||||
: existing.tds_section_pct !== null && existing.tds_section_pct !== undefined
|
||||
? Number(existing.tds_section_pct)
|
||||
: null,
|
||||
adjustment:
|
||||
payload.adjustment !== undefined ? payload.adjustment : Number(existing.adjustment ?? 0),
|
||||
terms_and_conditions:
|
||||
payload.terms_and_conditions !== undefined
|
||||
? payload.terms_and_conditions
|
||||
@ -598,6 +618,16 @@ const amendPurchaseOrder = async (id, payload, userId, requestId) => {
|
||||
discount_amount: payload.discount_amount ?? Number(existing.discount_amount),
|
||||
freight_charges: payload.freight_charges ?? Number(existing.freight_charges),
|
||||
other_charges: payload.other_charges ?? Number(existing.other_charges),
|
||||
tds_applicable:
|
||||
payload.tds_applicable !== undefined ? payload.tds_applicable : existing.tds_applicable,
|
||||
tds_section_pct:
|
||||
payload.tds_section_pct !== undefined
|
||||
? payload.tds_section_pct
|
||||
: existing.tds_section_pct !== null && existing.tds_section_pct !== undefined
|
||||
? Number(existing.tds_section_pct)
|
||||
: null,
|
||||
adjustment:
|
||||
payload.adjustment !== undefined ? payload.adjustment : Number(existing.adjustment ?? 0),
|
||||
terms_and_conditions:
|
||||
payload.terms_and_conditions !== undefined
|
||||
? payload.terms_and_conditions
|
||||
|
||||
@ -29,6 +29,17 @@ const poHeaderFields = {
|
||||
discount_amount: Joi.number().min(0).default(0),
|
||||
freight_charges: Joi.number().min(0).default(0),
|
||||
other_charges: Joi.number().min(0).default(0),
|
||||
tds_applicable: Joi.boolean().default(false),
|
||||
tds_section_pct: Joi.number()
|
||||
.min(0)
|
||||
.max(100)
|
||||
.allow(null)
|
||||
.when('tds_applicable', {
|
||||
is: true,
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.valid(null).optional(),
|
||||
}),
|
||||
adjustment: Joi.number().precision(4).default(0),
|
||||
terms_and_conditions: Joi.string().allow(null, '').optional(),
|
||||
remarks: Joi.string().allow(null, '').optional(),
|
||||
};
|
||||
@ -51,6 +62,9 @@ const updatePurchaseOrderSchema = Joi.object({
|
||||
discount_amount: poHeaderFields.discount_amount.optional(),
|
||||
freight_charges: poHeaderFields.freight_charges.optional(),
|
||||
other_charges: poHeaderFields.other_charges.optional(),
|
||||
tds_applicable: poHeaderFields.tds_applicable.optional(),
|
||||
tds_section_pct: poHeaderFields.tds_section_pct.optional(),
|
||||
adjustment: poHeaderFields.adjustment.optional(),
|
||||
terms_and_conditions: poHeaderFields.terms_and_conditions.optional(),
|
||||
remarks: poHeaderFields.remarks.optional(),
|
||||
items: Joi.array().items(poItemSchema).min(1).optional(),
|
||||
@ -69,6 +83,9 @@ const amendPurchaseOrderSchema = Joi.object({
|
||||
discount_amount: poHeaderFields.discount_amount.optional(),
|
||||
freight_charges: poHeaderFields.freight_charges.optional(),
|
||||
other_charges: poHeaderFields.other_charges.optional(),
|
||||
tds_applicable: poHeaderFields.tds_applicable.optional(),
|
||||
tds_section_pct: poHeaderFields.tds_section_pct.optional(),
|
||||
adjustment: poHeaderFields.adjustment.optional(),
|
||||
terms_and_conditions: poHeaderFields.terms_and_conditions.optional(),
|
||||
remarks: poHeaderFields.remarks.optional(),
|
||||
items: Joi.array().items(poItemSchema).min(1).optional(),
|
||||
|
||||
36
src/modules/settings/settings.controller.js
Normal file
36
src/modules/settings/settings.controller.js
Normal file
@ -0,0 +1,36 @@
|
||||
const asyncHandler = require('../../utils/asyncHandler');
|
||||
const ApiResponse = require('../../utils/ApiResponse');
|
||||
const service = require('./settings.service');
|
||||
|
||||
const getCompany = asyncHandler(async (_req, res) => {
|
||||
const data = await service.getCompany();
|
||||
res.json(new ApiResponse(200, data, 'Company settings fetched'));
|
||||
});
|
||||
|
||||
const updateCompany = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateCompany(req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'Company settings updated successfully'));
|
||||
});
|
||||
|
||||
const uploadCompanyLogo = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateCompanyLogo(req.file, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'Company logo updated successfully'));
|
||||
});
|
||||
|
||||
const getEmailSettings = asyncHandler(async (_req, res) => {
|
||||
const data = await service.getEmailSettings();
|
||||
res.json(new ApiResponse(200, data, 'Email settings fetched'));
|
||||
});
|
||||
|
||||
const updateEmailSettings = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateEmailSettings(req.body, req.user?.id, req.id);
|
||||
res.json(new ApiResponse(200, data, 'Email settings updated successfully'));
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getCompany,
|
||||
updateCompany,
|
||||
uploadCompanyLogo,
|
||||
getEmailSettings,
|
||||
updateEmailSettings,
|
||||
};
|
||||
25
src/modules/settings/settings.routes.js
Normal file
25
src/modules/settings/settings.routes.js
Normal file
@ -0,0 +1,25 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../../middlewares/auth.middleware');
|
||||
const authorize = require('../../middlewares/rbac.middleware');
|
||||
const validate = require('../../middlewares/validate.middleware');
|
||||
const upload = require('../../middlewares/upload.middleware');
|
||||
const controller = require('./settings.controller');
|
||||
const { companyUpdateSchema, emailUpdateSchema } = require('./settings.validation');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
router.get('/company', authorize('SETTINGS', 'view'), controller.getCompany);
|
||||
router.put('/company', authorize('SETTINGS', 'edit'), validate(companyUpdateSchema), controller.updateCompany);
|
||||
router.post(
|
||||
'/company/logo',
|
||||
authorize('SETTINGS', 'edit'),
|
||||
upload.single('logo'),
|
||||
controller.uploadCompanyLogo
|
||||
);
|
||||
|
||||
router.get('/email', authorize('SETTINGS', 'view'), controller.getEmailSettings);
|
||||
router.put('/email', authorize('SETTINGS', 'edit'), validate(emailUpdateSchema), controller.updateEmailSettings);
|
||||
|
||||
module.exports = router;
|
||||
211
src/modules/settings/settings.service.js
Normal file
211
src/modules/settings/settings.service.js
Normal file
@ -0,0 +1,211 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const prisma = require('../../config/prisma');
|
||||
const env = require('../../config/env');
|
||||
const ApiError = require('../../utils/ApiError');
|
||||
const auditLog = require('../../utils/auditLog');
|
||||
const { encrypt, decrypt } = require('../../utils/encryption');
|
||||
|
||||
const COMPANY_ID = BigInt(1);
|
||||
const EMAIL_SETTINGS_ID = BigInt(1);
|
||||
|
||||
const buildLogoUrl = (logoPath) => {
|
||||
if (!logoPath) return null;
|
||||
const normalized = logoPath.replace(/\\/g, '/');
|
||||
return normalized.startsWith('/') ? normalized : `/${normalized}`;
|
||||
};
|
||||
|
||||
const sanitizeCompany = (row) => {
|
||||
if (!row) return null;
|
||||
return {
|
||||
id: row.id,
|
||||
org_name: row.org_name,
|
||||
mobile: row.mobile,
|
||||
email: row.email,
|
||||
website: row.website,
|
||||
logo_path: row.logo_path,
|
||||
logo_url: buildLogoUrl(row.logo_path),
|
||||
address: row.address,
|
||||
city: row.city,
|
||||
state: row.state,
|
||||
pincode: row.pincode,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
const sanitizeEmailSettings = (row) => {
|
||||
if (!row) return null;
|
||||
return {
|
||||
id: row.id,
|
||||
smtp_host: row.smtp_host,
|
||||
smtp_port: row.smtp_port,
|
||||
smtp_username: row.smtp_username,
|
||||
sender_email: row.sender_email,
|
||||
sender_name: row.sender_name,
|
||||
has_smtp_password: Boolean(row.smtp_password),
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
};
|
||||
};
|
||||
|
||||
const getOrCreateCompany = async () => {
|
||||
return prisma.company.upsert({
|
||||
where: { id: COMPANY_ID },
|
||||
create: { id: COMPANY_ID, org_name: '' },
|
||||
update: {},
|
||||
});
|
||||
};
|
||||
|
||||
const getOrCreateEmailSettings = async () => {
|
||||
return prisma.email_settings.upsert({
|
||||
where: { id: EMAIL_SETTINGS_ID },
|
||||
create: { id: EMAIL_SETTINGS_ID, smtp_port: 587 },
|
||||
update: {},
|
||||
});
|
||||
};
|
||||
|
||||
const getCompany = async () => sanitizeCompany(await getOrCreateCompany());
|
||||
|
||||
const updateCompany = async (payload, userId, requestId) => {
|
||||
const existing = await getOrCreateCompany();
|
||||
|
||||
const data = {
|
||||
...(payload.org_name !== undefined ? { org_name: payload.org_name || null } : {}),
|
||||
...(payload.mobile !== undefined ? { mobile: payload.mobile || null } : {}),
|
||||
...(payload.email !== undefined ? { email: payload.email || null } : {}),
|
||||
...(payload.website !== undefined ? { website: payload.website || null } : {}),
|
||||
...(payload.address !== undefined ? { address: payload.address || null } : {}),
|
||||
...(payload.city !== undefined ? { city: payload.city || null } : {}),
|
||||
...(payload.state !== undefined ? { state: payload.state || null } : {}),
|
||||
...(payload.pincode !== undefined ? { pincode: payload.pincode || null } : {}),
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
};
|
||||
|
||||
const updated = await prisma.company.update({
|
||||
where: { id: COMPANY_ID },
|
||||
data,
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'company',
|
||||
recordId: COMPANY_ID,
|
||||
action: 'UPDATE',
|
||||
oldValue: sanitizeCompany(existing),
|
||||
newValue: sanitizeCompany(updated),
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return sanitizeCompany(updated);
|
||||
};
|
||||
|
||||
const updateCompanyLogo = async (file, userId, requestId) => {
|
||||
if (!file) throw new ApiError(400, 'Logo file is required');
|
||||
|
||||
const existing = await getOrCreateCompany();
|
||||
|
||||
if (existing.logo_path) {
|
||||
const oldPath = path.resolve(process.cwd(), existing.logo_path);
|
||||
if (fs.existsSync(oldPath)) {
|
||||
fs.unlinkSync(oldPath);
|
||||
}
|
||||
}
|
||||
|
||||
const logoPath = path.join(env.UPLOAD_DIR, file.filename).replace(/\\/g, '/');
|
||||
|
||||
const updated = await prisma.company.update({
|
||||
where: { id: COMPANY_ID },
|
||||
data: {
|
||||
logo_path: logoPath,
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
},
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'company',
|
||||
recordId: COMPANY_ID,
|
||||
action: 'UPDATE',
|
||||
oldValue: { logo_path: existing.logo_path },
|
||||
newValue: { logo_path: updated.logo_path },
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return sanitizeCompany(updated);
|
||||
};
|
||||
|
||||
const getEmailSettings = async () => sanitizeEmailSettings(await getOrCreateEmailSettings());
|
||||
|
||||
const updateEmailSettings = async (payload, userId, requestId) => {
|
||||
const existing = await getOrCreateEmailSettings();
|
||||
|
||||
const data = {
|
||||
...(payload.smtp_host !== undefined ? { smtp_host: payload.smtp_host || null } : {}),
|
||||
...(payload.smtp_port !== undefined ? { smtp_port: payload.smtp_port } : {}),
|
||||
...(payload.smtp_username !== undefined ? { smtp_username: payload.smtp_username || null } : {}),
|
||||
...(payload.sender_email !== undefined ? { sender_email: payload.sender_email || null } : {}),
|
||||
...(payload.sender_name !== undefined ? { sender_name: payload.sender_name || null } : {}),
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
};
|
||||
|
||||
if (payload.smtp_password !== undefined && payload.smtp_password !== '') {
|
||||
data.smtp_password = encrypt(payload.smtp_password);
|
||||
}
|
||||
|
||||
const updated = await prisma.email_settings.update({
|
||||
where: { id: EMAIL_SETTINGS_ID },
|
||||
data,
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'email_settings',
|
||||
recordId: EMAIL_SETTINGS_ID,
|
||||
action: 'UPDATE',
|
||||
oldValue: sanitizeEmailSettings(existing),
|
||||
newValue: sanitizeEmailSettings(updated),
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return sanitizeEmailSettings(updated);
|
||||
};
|
||||
|
||||
const getCompanyForDocuments = async () => {
|
||||
const row = await getOrCreateCompany();
|
||||
return {
|
||||
name: row.org_name || 'Company',
|
||||
address: row.address || '',
|
||||
city: row.city || '',
|
||||
state: row.state || '',
|
||||
pincode: row.pincode || '',
|
||||
gstin: '',
|
||||
phone: row.mobile || '',
|
||||
email: row.email || '',
|
||||
website: row.website || '',
|
||||
logo_url: buildLogoUrl(row.logo_path),
|
||||
};
|
||||
};
|
||||
|
||||
const getSmtpConfig = async () => {
|
||||
const row = await getOrCreateEmailSettings();
|
||||
if (!row.smtp_host) return null;
|
||||
return {
|
||||
host: row.smtp_host,
|
||||
port: row.smtp_port,
|
||||
username: row.smtp_username,
|
||||
password: row.smtp_password ? decrypt(row.smtp_password) : null,
|
||||
fromEmail: row.sender_email,
|
||||
fromName: row.sender_name,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getCompany,
|
||||
updateCompany,
|
||||
updateCompanyLogo,
|
||||
getEmailSettings,
|
||||
updateEmailSettings,
|
||||
getCompanyForDocuments,
|
||||
getSmtpConfig,
|
||||
};
|
||||
27
src/modules/settings/settings.validation.js
Normal file
27
src/modules/settings/settings.validation.js
Normal file
@ -0,0 +1,27 @@
|
||||
const Joi = require('joi');
|
||||
const { masterName } = require('../masters/_shared/masters.validation');
|
||||
|
||||
const companyUpdateSchema = Joi.object({
|
||||
org_name: masterName({ max: 200 }),
|
||||
mobile: Joi.string().max(20).allow(null, '').optional(),
|
||||
email: Joi.string().email().max(200).allow(null, '').optional(),
|
||||
website: Joi.string().max(255).allow(null, '').optional(),
|
||||
address: Joi.string().allow(null, '').optional(),
|
||||
city: Joi.string().max(100).allow(null, '').optional(),
|
||||
state: Joi.string().max(100).allow(null, '').optional(),
|
||||
pincode: Joi.string().max(10).allow(null, '').optional(),
|
||||
}).min(1);
|
||||
|
||||
const emailUpdateSchema = Joi.object({
|
||||
smtp_host: Joi.string().max(255).allow(null, '').optional(),
|
||||
smtp_port: Joi.number().integer().min(1).max(65535).optional(),
|
||||
smtp_username: Joi.string().max(200).allow(null, '').optional(),
|
||||
smtp_password: Joi.string().max(200).allow(null, '').optional(),
|
||||
sender_email: Joi.string().email().max(200).allow(null, '').optional(),
|
||||
sender_name: masterName({ max: 200 }),
|
||||
}).min(1);
|
||||
|
||||
module.exports = {
|
||||
companyUpdateSchema,
|
||||
emailUpdateSchema,
|
||||
};
|
||||
59
src/modules/vendors/vendors.constants.js
vendored
Normal file
59
src/modules/vendors/vendors.constants.js
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
const GST_TREATMENTS = [
|
||||
{ value: 'REGULAR', label: 'Regular' },
|
||||
{ value: 'COMPOSITION', label: 'Composition' },
|
||||
{ value: 'UNREGISTERED', label: 'Unregistered' },
|
||||
{ value: 'CONSUMER', label: 'Consumer' },
|
||||
{ value: 'SEZ', label: 'SEZ' },
|
||||
{ value: 'OVERSEAS', label: 'Overseas' },
|
||||
{ value: 'DEEMED_EXPORT', label: 'Deemed Export' },
|
||||
{ value: 'OTHER', label: 'Other' },
|
||||
];
|
||||
|
||||
const SOURCE_OF_SUPPLY_OPTIONS = [
|
||||
{ value: 'Andhra Pradesh', label: 'Andhra Pradesh' },
|
||||
{ value: 'Arunachal Pradesh', label: 'Arunachal Pradesh' },
|
||||
{ value: 'Assam', label: 'Assam' },
|
||||
{ value: 'Bihar', label: 'Bihar' },
|
||||
{ value: 'Chhattisgarh', label: 'Chhattisgarh' },
|
||||
{ value: 'Goa', label: 'Goa' },
|
||||
{ value: 'Gujarat', label: 'Gujarat' },
|
||||
{ value: 'Haryana', label: 'Haryana' },
|
||||
{ value: 'Himachal Pradesh', label: 'Himachal Pradesh' },
|
||||
{ value: 'Jharkhand', label: 'Jharkhand' },
|
||||
{ value: 'Karnataka', label: 'Karnataka' },
|
||||
{ value: 'Kerala', label: 'Kerala' },
|
||||
{ value: 'Madhya Pradesh', label: 'Madhya Pradesh' },
|
||||
{ value: 'Maharashtra', label: 'Maharashtra' },
|
||||
{ value: 'Manipur', label: 'Manipur' },
|
||||
{ value: 'Meghalaya', label: 'Meghalaya' },
|
||||
{ value: 'Mizoram', label: 'Mizoram' },
|
||||
{ value: 'Nagaland', label: 'Nagaland' },
|
||||
{ value: 'Odisha', label: 'Odisha' },
|
||||
{ value: 'Punjab', label: 'Punjab' },
|
||||
{ value: 'Rajasthan', label: 'Rajasthan' },
|
||||
{ value: 'Sikkim', label: 'Sikkim' },
|
||||
{ value: 'Tamil Nadu', label: 'Tamil Nadu' },
|
||||
{ value: 'Telangana', label: 'Telangana' },
|
||||
{ value: 'Tripura', label: 'Tripura' },
|
||||
{ value: 'Uttar Pradesh', label: 'Uttar Pradesh' },
|
||||
{ value: 'Uttarakhand', label: 'Uttarakhand' },
|
||||
{ value: 'West Bengal', label: 'West Bengal' },
|
||||
{ value: 'Andaman and Nicobar Islands', label: 'Andaman and Nicobar Islands' },
|
||||
{ value: 'Chandigarh', label: 'Chandigarh' },
|
||||
{ value: 'Dadra and Nagar Haveli and Daman and Diu', label: 'Dadra and Nagar Haveli and Daman and Diu' },
|
||||
{ value: 'Delhi', label: 'Delhi' },
|
||||
{ value: 'Jammu and Kashmir', label: 'Jammu and Kashmir' },
|
||||
{ value: 'Ladakh', label: 'Ladakh' },
|
||||
{ value: 'Lakshadweep', label: 'Lakshadweep' },
|
||||
{ value: 'Puducherry', label: 'Puducherry' },
|
||||
];
|
||||
|
||||
const GST_TREATMENT_VALUES = GST_TREATMENTS.map((item) => item.value);
|
||||
const SOURCE_OF_SUPPLY_VALUES = SOURCE_OF_SUPPLY_OPTIONS.map((item) => item.value);
|
||||
|
||||
module.exports = {
|
||||
GST_TREATMENTS,
|
||||
SOURCE_OF_SUPPLY_OPTIONS,
|
||||
GST_TREATMENT_VALUES,
|
||||
SOURCE_OF_SUPPLY_VALUES,
|
||||
};
|
||||
58
src/modules/vendors/vendors.controller.js
vendored
58
src/modules/vendors/vendors.controller.js
vendored
@ -12,6 +12,16 @@ const list = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, result.data, 'Vendors fetched', result.meta));
|
||||
});
|
||||
|
||||
const listGstTreatments = asyncHandler(async (_req, res) => {
|
||||
const data = service.listGstTreatments();
|
||||
res.json(new ApiResponse(200, data, 'GST treatment options fetched'));
|
||||
});
|
||||
|
||||
const listSourceOfSupplyOptions = asyncHandler(async (_req, res) => {
|
||||
const data = service.listSourceOfSupplyOptions();
|
||||
res.json(new ApiResponse(200, data, 'Source of supply options fetched'));
|
||||
});
|
||||
|
||||
const getOne = asyncHandler(async (req, res) => {
|
||||
const data = await service.getVendorById(req.params.id);
|
||||
res.json(new ApiResponse(200, data, 'Vendor fetched'));
|
||||
@ -130,9 +140,52 @@ const removeBankDetail = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'Vendor bank detail deleted successfully'));
|
||||
});
|
||||
|
||||
const listItemMappings = asyncHandler(async (req, res) => {
|
||||
const data = await service.listItemMappings(req.params.vendorId);
|
||||
res.json(new ApiResponse(200, data, 'Vendor item mappings fetched'));
|
||||
});
|
||||
|
||||
const createItemMapping = asyncHandler(async (req, res) => {
|
||||
const data = await service.createItemMapping(
|
||||
req.params.vendorId,
|
||||
req.body,
|
||||
req.user?.id,
|
||||
req.id
|
||||
);
|
||||
res.status(201).json(new ApiResponse(201, data, 'Vendor item mapping created successfully'));
|
||||
});
|
||||
|
||||
const getItemMapping = asyncHandler(async (req, res) => {
|
||||
const data = await service.getItemMapping(req.params.vendorId, req.params.mappingId);
|
||||
res.json(new ApiResponse(200, data, 'Vendor item mapping fetched'));
|
||||
});
|
||||
|
||||
const updateItemMapping = asyncHandler(async (req, res) => {
|
||||
const data = await service.updateItemMapping(
|
||||
req.params.vendorId,
|
||||
req.params.mappingId,
|
||||
req.body,
|
||||
req.user?.id,
|
||||
req.id
|
||||
);
|
||||
res.json(new ApiResponse(200, data, 'Vendor item mapping updated successfully'));
|
||||
});
|
||||
|
||||
const removeItemMapping = asyncHandler(async (req, res) => {
|
||||
await service.deleteItemMapping(
|
||||
req.params.vendorId,
|
||||
req.params.mappingId,
|
||||
req.user?.id,
|
||||
req.id
|
||||
);
|
||||
res.json(new ApiResponse(200, null, 'Vendor item mapping deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
list,
|
||||
listGstTreatments,
|
||||
listSourceOfSupplyOptions,
|
||||
getOne,
|
||||
update,
|
||||
changeStatus,
|
||||
@ -152,4 +205,9 @@ module.exports = {
|
||||
getBankDetail,
|
||||
updateBankDetail,
|
||||
removeBankDetail,
|
||||
listItemMappings,
|
||||
createItemMapping,
|
||||
getItemMapping,
|
||||
updateItemMapping,
|
||||
removeItemMapping,
|
||||
};
|
||||
|
||||
29
src/modules/vendors/vendors.routes.js
vendored
29
src/modules/vendors/vendors.routes.js
vendored
@ -14,6 +14,8 @@ const {
|
||||
updateContactSchema,
|
||||
createBankDetailSchema,
|
||||
updateBankDetailSchema,
|
||||
createItemMappingSchema,
|
||||
updateItemMappingSchema,
|
||||
} = require('./vendors.validation');
|
||||
|
||||
const router = express.Router();
|
||||
@ -28,6 +30,9 @@ router.get(
|
||||
);
|
||||
router.post('/', authorize('VENDOR', 'create'), validate(createVendorSchema), controller.create);
|
||||
|
||||
router.get('/gst-treatments', authorize('VENDOR', 'view'), controller.listGstTreatments);
|
||||
router.get('/source-of-supply', authorize('VENDOR', 'view'), controller.listSourceOfSupplyOptions);
|
||||
|
||||
router.get('/:vendorId/addresses', authorize('VENDOR', 'view'), controller.listAddresses);
|
||||
router.post(
|
||||
'/:vendorId/addresses',
|
||||
@ -92,6 +97,30 @@ router.delete(
|
||||
controller.removeBankDetail
|
||||
);
|
||||
|
||||
router.get('/:vendorId/items', authorize('VENDOR', 'view'), controller.listItemMappings);
|
||||
router.post(
|
||||
'/:vendorId/items',
|
||||
authorize('VENDOR', 'create'),
|
||||
validate(createItemMappingSchema),
|
||||
controller.createItemMapping
|
||||
);
|
||||
router.get(
|
||||
'/:vendorId/items/:mappingId',
|
||||
authorize('VENDOR', 'view'),
|
||||
controller.getItemMapping
|
||||
);
|
||||
router.put(
|
||||
'/:vendorId/items/:mappingId',
|
||||
authorize('VENDOR', 'edit'),
|
||||
validate(updateItemMappingSchema),
|
||||
controller.updateItemMapping
|
||||
);
|
||||
router.delete(
|
||||
'/:vendorId/items/:mappingId',
|
||||
authorize('VENDOR', 'delete'),
|
||||
controller.removeItemMapping
|
||||
);
|
||||
|
||||
router.get('/:id', authorize('VENDOR', 'view'), controller.getOne);
|
||||
router.put('/:id', authorize('VENDOR', 'edit'), validate(updateVendorSchema), controller.update);
|
||||
router.patch(
|
||||
|
||||
192
src/modules/vendors/vendors.service.js
vendored
192
src/modules/vendors/vendors.service.js
vendored
@ -4,6 +4,10 @@ const auditLog = require('../../utils/auditLog');
|
||||
const { getPagination } = require('../../utils/pagination');
|
||||
const { nextDocumentNumber } = require('../../utils/generateCode');
|
||||
const { encrypt, decrypt, blindIndex } = require('../../utils/encryption');
|
||||
const {
|
||||
GST_TREATMENTS,
|
||||
SOURCE_OF_SUPPLY_OPTIONS,
|
||||
} = require('./vendors.constants');
|
||||
|
||||
const vendorInclude = {
|
||||
payment_terms: { select: { id: true, code: true, name: true } },
|
||||
@ -14,6 +18,17 @@ const vendorDetailInclude = {
|
||||
vendor_addresses: { where: { is_active: true }, orderBy: { created_at: 'asc' } },
|
||||
vendor_contacts: { where: { is_active: true }, orderBy: { created_at: 'asc' } },
|
||||
vendor_bank_details: { where: { is_active: true }, orderBy: { created_at: 'asc' } },
|
||||
vendor_item_mapping: {
|
||||
where: { is_active: true },
|
||||
orderBy: { created_at: 'asc' },
|
||||
include: {
|
||||
items: { select: { id: true, item_code: true, item_name: true, is_active: true } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const itemMappingInclude = {
|
||||
items: { select: { id: true, item_code: true, item_name: true, is_active: true } },
|
||||
};
|
||||
|
||||
const sanitizeBankDetail = (row) => {
|
||||
@ -26,6 +41,19 @@ const sanitizeBankDetail = (row) => {
|
||||
};
|
||||
};
|
||||
|
||||
const sanitizeItemMapping = (row) => {
|
||||
if (!row) return null;
|
||||
const { items, ...rest } = row;
|
||||
return {
|
||||
...rest,
|
||||
last_purchase_rate:
|
||||
rest.last_purchase_rate !== null && rest.last_purchase_rate !== undefined
|
||||
? Number(rest.last_purchase_rate)
|
||||
: null,
|
||||
item: items || null,
|
||||
};
|
||||
};
|
||||
|
||||
const sanitizeVendor = (vendor, { includeChildren = false } = {}) => {
|
||||
if (!vendor) return null;
|
||||
const result = { ...vendor };
|
||||
@ -33,9 +61,11 @@ const sanitizeVendor = (vendor, { includeChildren = false } = {}) => {
|
||||
result.addresses = (vendor.vendor_addresses || []).map((row) => ({ ...row }));
|
||||
result.contacts = (vendor.vendor_contacts || []).map((row) => ({ ...row }));
|
||||
result.bank_details = (vendor.vendor_bank_details || []).map(sanitizeBankDetail);
|
||||
result.item_mappings = (vendor.vendor_item_mapping || []).map(sanitizeItemMapping);
|
||||
delete result.vendor_addresses;
|
||||
delete result.vendor_contacts;
|
||||
delete result.vendor_bank_details;
|
||||
delete result.vendor_item_mapping;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
@ -57,10 +87,20 @@ const assertPaymentTerm = async (paymentTermId) => {
|
||||
if (!term) throw new ApiError(422, 'Invalid payment_term_id');
|
||||
};
|
||||
|
||||
const assertItem = async (itemId) => {
|
||||
const item = await prisma.items.findFirst({
|
||||
where: { id: BigInt(itemId), deleted_at: null },
|
||||
});
|
||||
if (!item) throw new ApiError(422, 'Invalid item_id');
|
||||
return item;
|
||||
};
|
||||
|
||||
const normalizeVendorPayload = (payload) => {
|
||||
const data = { ...payload };
|
||||
if (data.gstin !== undefined) data.gstin = data.gstin || null;
|
||||
if (data.pan !== undefined) data.pan = data.pan || null;
|
||||
if (data.gst_treatment !== undefined) data.gst_treatment = data.gst_treatment || null;
|
||||
if (data.source_of_supply !== undefined) data.source_of_supply = data.source_of_supply || null;
|
||||
if (data.remarks !== undefined) data.remarks = data.remarks || null;
|
||||
if (data.payment_term_id !== undefined && data.payment_term_id !== null) {
|
||||
data.payment_term_id = BigInt(data.payment_term_id);
|
||||
@ -126,6 +166,10 @@ const listVendors = async (query) => {
|
||||
return { data: rows.map((row) => sanitizeVendor(row)), meta: { page, limit, total } };
|
||||
};
|
||||
|
||||
const listGstTreatments = () => GST_TREATMENTS;
|
||||
|
||||
const listSourceOfSupplyOptions = () => SOURCE_OF_SUPPLY_OPTIONS;
|
||||
|
||||
const getVendorById = async (id) => {
|
||||
const vendor = await getVendorOrThrow(id, { includeChildren: true });
|
||||
return sanitizeVendor(vendor, { includeChildren: true });
|
||||
@ -469,9 +513,152 @@ const deleteBankDetail = async (vendorId, bankDetailId, userId, requestId) => {
|
||||
});
|
||||
};
|
||||
|
||||
const listItemMappings = async (vendorId) => {
|
||||
await getVendorOrThrow(vendorId);
|
||||
const rows = await prisma.vendor_item_mapping.findMany({
|
||||
where: { vendor_id: BigInt(vendorId), is_active: true },
|
||||
include: itemMappingInclude,
|
||||
orderBy: { created_at: 'asc' },
|
||||
});
|
||||
return rows.map(sanitizeItemMapping);
|
||||
};
|
||||
|
||||
const createItemMapping = async (vendorId, payload, userId, requestId) => {
|
||||
await getVendorOrThrow(vendorId);
|
||||
await assertItem(payload.item_id);
|
||||
|
||||
const vendor_id = BigInt(vendorId);
|
||||
const item_id = BigInt(payload.item_id);
|
||||
|
||||
const existing = await prisma.vendor_item_mapping.findUnique({
|
||||
where: { vendor_id_item_id: { vendor_id, item_id } },
|
||||
include: itemMappingInclude,
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
if (existing.is_active) {
|
||||
throw new ApiError(409, 'This item is already mapped to the vendor');
|
||||
}
|
||||
|
||||
const reactivated = await prisma.vendor_item_mapping.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
last_purchase_rate: payload.last_purchase_rate ?? null,
|
||||
is_preferred: payload.is_preferred ?? false,
|
||||
is_active: true,
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
},
|
||||
include: itemMappingInclude,
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'vendor_item_mapping',
|
||||
recordId: reactivated.id,
|
||||
action: 'UPDATE',
|
||||
oldValue: sanitizeItemMapping(existing),
|
||||
newValue: sanitizeItemMapping(reactivated),
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return sanitizeItemMapping(reactivated);
|
||||
}
|
||||
|
||||
const created = await prisma.vendor_item_mapping.create({
|
||||
data: {
|
||||
vendor_id,
|
||||
item_id,
|
||||
last_purchase_rate: payload.last_purchase_rate ?? null,
|
||||
is_preferred: payload.is_preferred ?? false,
|
||||
is_active: payload.is_active ?? true,
|
||||
created_by: userId ? BigInt(userId) : null,
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
},
|
||||
include: itemMappingInclude,
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'vendor_item_mapping',
|
||||
recordId: created.id,
|
||||
action: 'CREATE',
|
||||
oldValue: null,
|
||||
newValue: sanitizeItemMapping(created),
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return sanitizeItemMapping(created);
|
||||
};
|
||||
|
||||
const getItemMapping = async (vendorId, mappingId) => {
|
||||
await getVendorOrThrow(vendorId);
|
||||
const row = await prisma.vendor_item_mapping.findFirst({
|
||||
where: { id: BigInt(mappingId), vendor_id: BigInt(vendorId), is_active: true },
|
||||
include: itemMappingInclude,
|
||||
});
|
||||
if (!row) throw new ApiError(404, 'Vendor item mapping not found');
|
||||
return sanitizeItemMapping(row);
|
||||
};
|
||||
|
||||
const updateItemMapping = async (vendorId, mappingId, payload, userId, requestId) => {
|
||||
const existing = await prisma.vendor_item_mapping.findFirst({
|
||||
where: { id: BigInt(mappingId), vendor_id: BigInt(vendorId), is_active: true },
|
||||
include: itemMappingInclude,
|
||||
});
|
||||
if (!existing) throw new ApiError(404, 'Vendor item mapping not found');
|
||||
|
||||
const data = { ...payload, updated_by: userId ? BigInt(userId) : null };
|
||||
if (payload.last_purchase_rate === null) {
|
||||
data.last_purchase_rate = null;
|
||||
}
|
||||
|
||||
const updated = await prisma.vendor_item_mapping.update({
|
||||
where: { id: BigInt(mappingId) },
|
||||
data,
|
||||
include: itemMappingInclude,
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'vendor_item_mapping',
|
||||
recordId: mappingId,
|
||||
action: 'UPDATE',
|
||||
oldValue: sanitizeItemMapping(existing),
|
||||
newValue: sanitizeItemMapping(updated),
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return sanitizeItemMapping(updated);
|
||||
};
|
||||
|
||||
const deleteItemMapping = async (vendorId, mappingId, userId, requestId) => {
|
||||
const existing = await prisma.vendor_item_mapping.findFirst({
|
||||
where: { id: BigInt(mappingId), vendor_id: BigInt(vendorId), is_active: true },
|
||||
include: itemMappingInclude,
|
||||
});
|
||||
if (!existing) throw new ApiError(404, 'Vendor item mapping not found');
|
||||
|
||||
await prisma.vendor_item_mapping.update({
|
||||
where: { id: BigInt(mappingId) },
|
||||
data: { is_active: false, updated_by: userId ? BigInt(userId) : null },
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'vendor_item_mapping',
|
||||
recordId: mappingId,
|
||||
action: 'DELETE',
|
||||
oldValue: sanitizeItemMapping(existing),
|
||||
newValue: { is_active: false },
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createVendor,
|
||||
listVendors,
|
||||
listGstTreatments,
|
||||
listSourceOfSupplyOptions,
|
||||
getVendorById,
|
||||
updateVendor,
|
||||
setVendorStatus,
|
||||
@ -491,4 +678,9 @@ module.exports = {
|
||||
getBankDetail,
|
||||
updateBankDetail,
|
||||
deleteBankDetail,
|
||||
listItemMappings,
|
||||
createItemMapping,
|
||||
getItemMapping,
|
||||
updateItemMapping,
|
||||
deleteItemMapping,
|
||||
};
|
||||
|
||||
35
src/modules/vendors/vendors.validation.js
vendored
35
src/modules/vendors/vendors.validation.js
vendored
@ -1,4 +1,8 @@
|
||||
const Joi = require('joi');
|
||||
const {
|
||||
GST_TREATMENT_VALUES,
|
||||
SOURCE_OF_SUPPLY_VALUES,
|
||||
} = require('./vendors.constants');
|
||||
|
||||
const vendorTypes = ['RAW_MATERIAL', 'PACKING_MATERIAL', 'ASSET_CAPITAL', 'SERVICE', 'GENERAL'];
|
||||
const vendorStatuses = ['active', 'inactive', 'blacklisted'];
|
||||
@ -10,6 +14,14 @@ const createVendorSchema = Joi.object({
|
||||
vendor_type: Joi.string()
|
||||
.valid(...vendorTypes)
|
||||
.required(),
|
||||
gst_treatment: Joi.string()
|
||||
.valid(...GST_TREATMENT_VALUES)
|
||||
.allow(null, '')
|
||||
.optional(),
|
||||
source_of_supply: Joi.string()
|
||||
.valid(...SOURCE_OF_SUPPLY_VALUES)
|
||||
.allow(null, '')
|
||||
.optional(),
|
||||
gstin: Joi.string().length(15).allow(null, '').optional(),
|
||||
pan: Joi.string().length(10).allow(null, '').optional(),
|
||||
payment_term_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
@ -23,6 +35,14 @@ const updateVendorSchema = Joi.object({
|
||||
vendor_type: Joi.string()
|
||||
.valid(...vendorTypes)
|
||||
.optional(),
|
||||
gst_treatment: Joi.string()
|
||||
.valid(...GST_TREATMENT_VALUES)
|
||||
.allow(null, '')
|
||||
.optional(),
|
||||
source_of_supply: Joi.string()
|
||||
.valid(...SOURCE_OF_SUPPLY_VALUES)
|
||||
.allow(null, '')
|
||||
.optional(),
|
||||
gstin: Joi.string().length(15).allow(null, '').optional(),
|
||||
pan: Joi.string().length(10).allow(null, '').optional(),
|
||||
payment_term_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
@ -107,6 +127,19 @@ const updateBankDetailSchema = Joi.object({
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
const createItemMappingSchema = Joi.object({
|
||||
item_id: Joi.number().integer().positive().required(),
|
||||
last_purchase_rate: Joi.number().precision(4).min(0).allow(null).optional(),
|
||||
is_preferred: Joi.boolean().default(false),
|
||||
is_active: Joi.boolean().default(true),
|
||||
});
|
||||
|
||||
const updateItemMappingSchema = Joi.object({
|
||||
last_purchase_rate: Joi.number().precision(4).min(0).allow(null).optional(),
|
||||
is_preferred: Joi.boolean().optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
}).min(1);
|
||||
|
||||
module.exports = {
|
||||
createVendorSchema,
|
||||
updateVendorSchema,
|
||||
@ -118,4 +151,6 @@ module.exports = {
|
||||
updateContactSchema,
|
||||
createBankDetailSchema,
|
||||
updateBankDetailSchema,
|
||||
createItemMappingSchema,
|
||||
updateItemMappingSchema,
|
||||
};
|
||||
|
||||
@ -9,6 +9,7 @@ router.use('/vendors', require('../../modules/vendors/vendors.routes'));
|
||||
router.use('/purchase-orders', require('../../modules/purchase-orders/purchase-orders.routes'));
|
||||
router.use('/grn', require('../../modules/grn/grn.routes'));
|
||||
router.use('/assets', require('../../modules/assets/assets.routes'));
|
||||
router.use('/settings', require('../../modules/settings/settings.routes'));
|
||||
router.use('/masters', require('../../modules/masters'));
|
||||
|
||||
router.get('/healthz', (req, res) => {
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
const app = require('./app');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const env = require('./config/env');
|
||||
const logger = require('./config/logger');
|
||||
const prisma = require('./config/prisma');
|
||||
@ -7,6 +9,8 @@ BigInt.prototype.toJSON = function toJSON() {
|
||||
return this.toString();
|
||||
};
|
||||
|
||||
fs.mkdirSync(path.resolve(env.UPLOAD_DIR), { recursive: true });
|
||||
|
||||
const server = app.listen(env.PORT, () => {
|
||||
logger.info(`Server running on port ${env.PORT} [${env.NODE_ENV}]`);
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user