GWM : plant , warehouse removed shippment, billing added & igst,cgst,sgst added in po module
This commit is contained in:
parent
e0e3077019
commit
a108c9c925
@ -329,8 +329,8 @@ flowchart TD
|
||||
| `asset_code` | `document_series` via `ASSET_{category.code}` |
|
||||
| `asset_name` | Item name (+ `#N` if qty > 1) |
|
||||
| `item_category_id` / `item_subcategory_id` | From item master (optional GRN line override) |
|
||||
| `plant_id` | From PO |
|
||||
| `warehouse_id`, `vendor_id`, `po_id`, `grn_id`, `grn_item_id` | From GRN |
|
||||
| `location_id` | From PO `shipping_id` (plant or warehouse) |
|
||||
| `vendor_id`, `po_id`, `grn_id`, `grn_item_id` | From GRN |
|
||||
| `purchase_date` | `grn.grn_date` |
|
||||
| `purchase_cost` | `grn_items.rate` |
|
||||
| `useful_life_years`, `depreciation_method` | From `item_categories` defaults |
|
||||
@ -350,7 +350,7 @@ flowchart TB
|
||||
|
||||
subgraph Validate["normalizeAssetPayload validations"]
|
||||
V1[item_category + subcategory match]
|
||||
V2[plant / dept / warehouse / user refs]
|
||||
V2[location / dept / user refs]
|
||||
V3[vendor / PO / GRN / grn_item refs]
|
||||
V4[disposal_date required if DISPOSED/SCRAPPED]
|
||||
V5[depreciation_rate required if method=OTHER]
|
||||
@ -364,7 +364,7 @@ flowchart TB
|
||||
|
||||
SAVE --> TRANSFER["POST /assets/:id/transfer"]
|
||||
TRANSFER --> TH[(asset_transfers)]
|
||||
TRANSFER --> UPDATE_LOC[Update plant/dept/user/warehouse on asset]
|
||||
TRANSFER --> UPDATE_LOC[Update location/dept/user on asset]
|
||||
|
||||
SAVE --> UPDATE["PUT /assets/:id"]
|
||||
SAVE --> DELETE["DELETE /assets/:id soft delete"]
|
||||
@ -656,7 +656,7 @@ erDiagram
|
||||
|
||||
asset_amc_contracts ||--o{ asset_service_visits : covers
|
||||
|
||||
locations ||--o{ assets : plant_warehouse
|
||||
locations ||--o{ assets : location
|
||||
departments ||--o{ assets : assigned_dept
|
||||
users ||--o{ assets : assigned_user
|
||||
|
||||
|
||||
@ -140,26 +140,22 @@ model asset_transfers {
|
||||
id BigInt @id @default(autoincrement())
|
||||
asset_id BigInt
|
||||
transfer_date DateTime @db.Date
|
||||
from_plant_id BigInt?
|
||||
to_plant_id BigInt?
|
||||
from_location_id BigInt?
|
||||
to_location_id BigInt?
|
||||
from_department_id BigInt?
|
||||
to_department_id BigInt?
|
||||
from_user_id BigInt?
|
||||
to_user_id BigInt?
|
||||
from_warehouse_id BigInt?
|
||||
to_warehouse_id BigInt?
|
||||
reason String?
|
||||
transferred_by BigInt?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
assets assets @relation(fields: [asset_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
departments_asset_transfers_from_department_idTodepartments departments? @relation("asset_transfers_from_department_idTodepartments", fields: [from_department_id], references: [id], onUpdate: NoAction)
|
||||
from_plant locations? @relation("TransferFromPlant", fields: [from_plant_id], references: [id], onUpdate: NoAction)
|
||||
from_location locations? @relation("TransferFromLocation", fields: [from_location_id], references: [id], onUpdate: NoAction)
|
||||
users_asset_transfers_from_user_idTousers users? @relation("asset_transfers_from_user_idTousers", fields: [from_user_id], references: [id], onUpdate: NoAction)
|
||||
from_warehouse locations? @relation("TransferFromWarehouse", fields: [from_warehouse_id], references: [id], onUpdate: NoAction)
|
||||
departments_asset_transfers_to_department_idTodepartments departments? @relation("asset_transfers_to_department_idTodepartments", fields: [to_department_id], references: [id], onUpdate: NoAction)
|
||||
to_plant locations? @relation("TransferToPlant", fields: [to_plant_id], references: [id], onUpdate: NoAction)
|
||||
to_location locations? @relation("TransferToLocation", fields: [to_location_id], references: [id], onUpdate: NoAction)
|
||||
users_asset_transfers_to_user_idTousers users? @relation("asset_transfers_to_user_idTousers", fields: [to_user_id], references: [id], onUpdate: NoAction)
|
||||
to_warehouse locations? @relation("TransferToWarehouse", fields: [to_warehouse_id], references: [id], onUpdate: NoAction)
|
||||
users_asset_transfers_transferred_byTousers users? @relation("asset_transfers_transferred_byTousers", fields: [transferred_by], references: [id], onUpdate: NoAction)
|
||||
|
||||
@@index([asset_id], map: "idx_asset_transfers_asset_id")
|
||||
@ -176,9 +172,8 @@ model assets {
|
||||
manufacturer String? @db.VarChar(200)
|
||||
serial_number String? @db.VarChar(100)
|
||||
part_number String? @db.VarChar(100)
|
||||
plant_id BigInt
|
||||
location_id BigInt
|
||||
department_id BigInt?
|
||||
warehouse_id BigInt?
|
||||
location_detail String? @db.VarChar(200)
|
||||
assigned_to_user_id BigInt?
|
||||
vendor_id BigInt?
|
||||
@ -217,16 +212,15 @@ model assets {
|
||||
departments departments? @relation(fields: [department_id], references: [id], onUpdate: NoAction)
|
||||
grn grn? @relation(fields: [grn_id], references: [id], onUpdate: NoAction)
|
||||
grn_items grn_items? @relation(fields: [grn_item_id], references: [id], onUpdate: NoAction)
|
||||
plant locations @relation("AssetPlantLocation", fields: [plant_id], references: [id], onUpdate: NoAction)
|
||||
location locations @relation("AssetLocation", fields: [location_id], references: [id], onUpdate: NoAction)
|
||||
purchase_orders purchase_orders? @relation(fields: [po_id], references: [id], onUpdate: NoAction)
|
||||
users_assets_updated_byTousers users? @relation("assets_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
vendors_assets_vendor_idTovendors vendors? @relation("assets_vendor_idTovendors", fields: [vendor_id], references: [id], onUpdate: NoAction)
|
||||
warehouse locations? @relation("AssetWarehouseLocation", fields: [warehouse_id], references: [id], onUpdate: NoAction)
|
||||
|
||||
@@index([item_category_id], map: "idx_assets_item_category_id")
|
||||
@@index([item_subcategory_id], map: "idx_assets_item_subcategory_id")
|
||||
@@index([department_id], map: "idx_assets_dept_id")
|
||||
@@index([plant_id], map: "idx_assets_plant_id")
|
||||
@@index([location_id], map: "idx_assets_location_id")
|
||||
}
|
||||
|
||||
model audit_logs {
|
||||
@ -568,17 +562,14 @@ model locations {
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
deleted_at DateTime? @db.Timestamptz(6)
|
||||
transfers_from_plant asset_transfers[] @relation("TransferFromPlant")
|
||||
transfers_to_plant asset_transfers[] @relation("TransferToPlant")
|
||||
transfers_from_wh asset_transfers[] @relation("TransferFromWarehouse")
|
||||
transfers_to_wh asset_transfers[] @relation("TransferToWarehouse")
|
||||
assets_plant assets[] @relation("AssetPlantLocation")
|
||||
assets_warehouse assets[] @relation("AssetWarehouseLocation")
|
||||
transfers_from asset_transfers[] @relation("TransferFromLocation")
|
||||
transfers_to asset_transfers[] @relation("TransferToLocation")
|
||||
assets_location assets[] @relation("AssetLocation")
|
||||
grn_warehouse grn[] @relation("GRNWarehouseLocation")
|
||||
parent locations? @relation("LocationHierarchy", fields: [parent_id], references: [id], onUpdate: NoAction)
|
||||
children locations[] @relation("LocationHierarchy")
|
||||
po_plant purchase_orders[] @relation("POPlantLocation")
|
||||
po_warehouse purchase_orders[] @relation("POWarehouseLocation")
|
||||
po_billing purchase_orders[] @relation("POBillingLocation")
|
||||
po_shipping purchase_orders[] @relation("POShippingLocation")
|
||||
users_plant users[] @relation("UserPlantLocation")
|
||||
users_created users? @relation("locations_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction, map: "fk_locations_created_by")
|
||||
users_updated users? @relation("locations_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction, map: "fk_locations_updated_by")
|
||||
@ -656,14 +647,17 @@ model purchase_orders {
|
||||
po_number String @unique @db.VarChar(30)
|
||||
po_date DateTime @db.Date
|
||||
vendor_id BigInt
|
||||
plant_id BigInt
|
||||
warehouse_id BigInt?
|
||||
billing_id BigInt
|
||||
shipping_id BigInt
|
||||
payment_term_id BigInt?
|
||||
delivery_term_id BigInt?
|
||||
expected_delivery_date DateTime? @db.Date
|
||||
sub_total Decimal @default(0) @db.Decimal(15, 4)
|
||||
discount_amount Decimal @default(0) @db.Decimal(15, 4)
|
||||
tax_total Decimal @default(0) @db.Decimal(15, 4)
|
||||
cgst Decimal @default(0) @db.Decimal(15, 4)
|
||||
sgst Decimal @default(0) @db.Decimal(15, 4)
|
||||
igst 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)
|
||||
@ -692,10 +686,10 @@ model purchase_orders {
|
||||
purchase_orders purchase_orders? @relation("purchase_ordersTopurchase_orders", fields: [parent_po_id], references: [id], onUpdate: NoAction)
|
||||
other_purchase_orders purchase_orders[] @relation("purchase_ordersTopurchase_orders")
|
||||
payment_terms payment_terms? @relation(fields: [payment_term_id], references: [id], onUpdate: NoAction)
|
||||
plant locations @relation("POPlantLocation", fields: [plant_id], references: [id], onUpdate: NoAction)
|
||||
billing_location locations @relation("POBillingLocation", fields: [billing_id], references: [id], onUpdate: NoAction)
|
||||
shipping_location locations @relation("POShippingLocation", fields: [shipping_id], references: [id], onUpdate: NoAction)
|
||||
users_purchase_orders_updated_byTousers users? @relation("purchase_orders_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||
vendors vendors @relation(fields: [vendor_id], references: [id], onUpdate: NoAction)
|
||||
warehouse locations? @relation("POWarehouseLocation", fields: [warehouse_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
|
||||
|
||||
@@index([created_at], map: "idx_po_created_at")
|
||||
@@index([po_date], map: "idx_po_po_date")
|
||||
|
||||
45
scripts/patch-asset-transfers-location.sql
Normal file
45
scripts/patch-asset-transfers-location.sql
Normal file
@ -0,0 +1,45 @@
|
||||
-- Asset transfers: replace from/to plant + warehouse columns with
|
||||
-- from_location_id / to_location_id (either plant or warehouse).
|
||||
-- Idempotent: safe to re-run.
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE asset_transfers ADD COLUMN IF NOT EXISTS from_location_id BIGINT;
|
||||
ALTER TABLE asset_transfers ADD COLUMN IF NOT EXISTS to_location_id BIGINT;
|
||||
|
||||
-- Backfill from old columns (prefer warehouse, else plant) when present
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'asset_transfers' AND column_name = 'from_plant_id'
|
||||
) THEN
|
||||
UPDATE asset_transfers
|
||||
SET from_location_id = COALESCE(from_warehouse_id, from_plant_id)
|
||||
WHERE from_location_id IS NULL;
|
||||
UPDATE asset_transfers
|
||||
SET to_location_id = COALESCE(to_warehouse_id, to_plant_id)
|
||||
WHERE to_location_id IS NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Drop old columns (FK constraints drop with them)
|
||||
ALTER TABLE asset_transfers DROP COLUMN IF EXISTS from_plant_id;
|
||||
ALTER TABLE asset_transfers DROP COLUMN IF EXISTS to_plant_id;
|
||||
ALTER TABLE asset_transfers DROP COLUMN IF EXISTS from_warehouse_id;
|
||||
ALTER TABLE asset_transfers DROP COLUMN IF EXISTS to_warehouse_id;
|
||||
|
||||
-- Foreign keys for the new columns
|
||||
ALTER TABLE asset_transfers DROP CONSTRAINT IF EXISTS fk_at_from_location;
|
||||
ALTER TABLE asset_transfers
|
||||
ADD CONSTRAINT fk_at_from_location FOREIGN KEY (from_location_id) REFERENCES locations(id);
|
||||
ALTER TABLE asset_transfers DROP CONSTRAINT IF EXISTS fk_at_to_location;
|
||||
ALTER TABLE asset_transfers
|
||||
ADD CONSTRAINT fk_at_to_location FOREIGN KEY (to_location_id) REFERENCES locations(id);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Verify:
|
||||
-- SELECT column_name FROM information_schema.columns
|
||||
-- WHERE table_name = 'asset_transfers'
|
||||
-- AND column_name IN ('from_location_id','to_location_id','from_plant_id','to_plant_id');
|
||||
206
scripts/patch-assets-location.sql
Normal file
206
scripts/patch-assets-location.sql
Normal file
@ -0,0 +1,206 @@
|
||||
-- Assets: replace plant_id + warehouse_id with a single location_id (plant OR
|
||||
-- warehouse). For GRN auto-created assets, location_id comes from the PO
|
||||
-- shipping_id. Recreates the asset views to use location_id.
|
||||
-- Idempotent: safe to re-run.
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1. New unified location column
|
||||
ALTER TABLE assets ADD COLUMN IF NOT EXISTS location_id BIGINT;
|
||||
|
||||
-- 2. Backfill from old columns (prefer warehouse, else plant) when present
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'assets' AND column_name = 'plant_id'
|
||||
) THEN
|
||||
UPDATE assets
|
||||
SET location_id = COALESCE(warehouse_id, plant_id)
|
||||
WHERE location_id IS NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
ALTER TABLE assets ALTER COLUMN location_id SET NOT NULL;
|
||||
|
||||
-- 3. Drop dependent views before removing old columns
|
||||
DROP VIEW IF EXISTS v_assets CASCADE;
|
||||
DROP VIEW IF EXISTS v_asset_expiry_alerts CASCADE;
|
||||
DROP VIEW IF EXISTS v_asset_next_service CASCADE;
|
||||
|
||||
-- 4. Drop old columns (FK constraints/indexes drop with them)
|
||||
DROP INDEX IF EXISTS idx_assets_plant_id;
|
||||
ALTER TABLE assets DROP COLUMN IF EXISTS plant_id;
|
||||
ALTER TABLE assets DROP COLUMN IF EXISTS warehouse_id;
|
||||
|
||||
-- 5. FK + index for the new column
|
||||
ALTER TABLE assets DROP CONSTRAINT IF EXISTS fk_assets_location;
|
||||
ALTER TABLE assets
|
||||
ADD CONSTRAINT fk_assets_location FOREIGN KEY (location_id) REFERENCES locations(id);
|
||||
CREATE INDEX IF NOT EXISTS idx_assets_location_id ON assets(location_id);
|
||||
|
||||
-- 6. Recreate views using location_id (join locations regardless of type)
|
||||
CREATE VIEW v_asset_expiry_alerts AS
|
||||
SELECT
|
||||
a.id AS asset_id,
|
||||
a.asset_code,
|
||||
a.asset_name,
|
||||
ac_cat.name AS category_name,
|
||||
loc.name AS location_name,
|
||||
d.name AS department_name,
|
||||
'AMC'::text AS alert_type,
|
||||
amc.id AS reference_id,
|
||||
amc.contract_no AS reference_no,
|
||||
v.vendor_name AS party_name,
|
||||
amc.end_date AS expiry_date,
|
||||
(amc.end_date - CURRENT_DATE) AS days_remaining,
|
||||
CASE
|
||||
WHEN (amc.end_date - CURRENT_DATE) <= 0 THEN 'EXPIRED'
|
||||
WHEN (amc.end_date - CURRENT_DATE) <= 30 THEN 'CRITICAL'
|
||||
WHEN (amc.end_date - CURRENT_DATE) <= 60 THEN 'WARNING'
|
||||
WHEN (amc.end_date - CURRENT_DATE) <= 90 THEN 'INFO'
|
||||
END AS alert_level
|
||||
FROM asset_amc_contracts amc
|
||||
JOIN assets a ON a.id = amc.asset_id
|
||||
JOIN item_categories ac_cat ON ac_cat.id = a.item_category_id
|
||||
JOIN locations loc ON loc.id = a.location_id
|
||||
LEFT JOIN departments d ON d.id = a.department_id
|
||||
LEFT JOIN vendors v ON v.id = amc.vendor_id
|
||||
WHERE amc.is_active = TRUE AND amc.deleted_at IS NULL AND a.deleted_at IS NULL
|
||||
AND (amc.end_date - CURRENT_DATE) <= 90
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
a.id, a.asset_code, a.asset_name, ac_cat.name, loc.name, d.name,
|
||||
'INSURANCE'::text, ins.id, ins.policy_no, ins.insurer_name,
|
||||
ins.policy_end_date, (ins.policy_end_date - CURRENT_DATE),
|
||||
CASE
|
||||
WHEN (ins.policy_end_date - CURRENT_DATE) <= 0 THEN 'EXPIRED'
|
||||
WHEN (ins.policy_end_date - CURRENT_DATE) <= 30 THEN 'CRITICAL'
|
||||
WHEN (ins.policy_end_date - CURRENT_DATE) <= 60 THEN 'WARNING'
|
||||
WHEN (ins.policy_end_date - CURRENT_DATE) <= 90 THEN 'INFO'
|
||||
END
|
||||
FROM asset_insurance_policies ins
|
||||
JOIN assets a ON a.id = ins.asset_id
|
||||
JOIN item_categories ac_cat ON ac_cat.id = a.item_category_id
|
||||
JOIN locations loc ON loc.id = a.location_id
|
||||
LEFT JOIN departments d ON d.id = a.department_id
|
||||
WHERE ins.is_active = TRUE AND ins.deleted_at IS NULL AND a.deleted_at IS NULL
|
||||
AND (ins.policy_end_date - CURRENT_DATE) <= 90
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
a.id, a.asset_code, a.asset_name, ac_cat.name, loc.name, d.name,
|
||||
'WARRANTY'::text, a.id, a.asset_code, 'Manufacturer Warranty'::varchar,
|
||||
a.warranty_expiry_date, (a.warranty_expiry_date - CURRENT_DATE),
|
||||
CASE
|
||||
WHEN (a.warranty_expiry_date - CURRENT_DATE) <= 0 THEN 'EXPIRED'
|
||||
WHEN (a.warranty_expiry_date - CURRENT_DATE) <= 30 THEN 'CRITICAL'
|
||||
WHEN (a.warranty_expiry_date - CURRENT_DATE) <= 60 THEN 'WARNING'
|
||||
WHEN (a.warranty_expiry_date - CURRENT_DATE) <= 90 THEN 'INFO'
|
||||
END
|
||||
FROM assets a
|
||||
JOIN item_categories ac_cat ON ac_cat.id = a.item_category_id
|
||||
JOIN locations loc ON loc.id = a.location_id
|
||||
LEFT JOIN departments d ON d.id = a.department_id
|
||||
WHERE a.warranty_expiry_date IS NOT NULL AND a.deleted_at IS NULL
|
||||
AND (a.warranty_expiry_date - CURRENT_DATE) <= 90;
|
||||
|
||||
CREATE VIEW v_asset_next_service AS
|
||||
SELECT
|
||||
a.id AS asset_id,
|
||||
a.asset_code,
|
||||
a.asset_name,
|
||||
ac_cat.name AS category_name,
|
||||
loc.name AS location_name,
|
||||
d.name AS department_name,
|
||||
sv.id AS last_visit_id,
|
||||
sv.visit_date AS last_service_date,
|
||||
sv.visit_type AS last_visit_type,
|
||||
sv.next_service_date,
|
||||
(sv.next_service_date - CURRENT_DATE) AS days_to_next_service,
|
||||
CASE
|
||||
WHEN sv.next_service_date < CURRENT_DATE THEN 'OVERDUE'
|
||||
WHEN (sv.next_service_date - CURRENT_DATE) <= 7 THEN 'DUE_THIS_WEEK'
|
||||
WHEN (sv.next_service_date - CURRENT_DATE) <= 30 THEN 'DUE_THIS_MONTH'
|
||||
ELSE 'UPCOMING'
|
||||
END AS service_status,
|
||||
v.vendor_name AS service_vendor,
|
||||
amc.contract_no AS amc_contract_no,
|
||||
amc.end_date AS amc_end_date
|
||||
FROM assets a
|
||||
JOIN item_categories ac_cat ON ac_cat.id = a.item_category_id
|
||||
JOIN locations loc ON loc.id = a.location_id
|
||||
LEFT JOIN departments d ON d.id = a.department_id
|
||||
JOIN LATERAL (
|
||||
SELECT * FROM asset_service_visits
|
||||
WHERE asset_id = a.id AND deleted_at IS NULL AND next_service_date IS NOT NULL
|
||||
ORDER BY visit_date DESC
|
||||
LIMIT 1
|
||||
) sv ON TRUE
|
||||
LEFT JOIN vendors v ON v.id = sv.vendor_id
|
||||
LEFT JOIN asset_amc_contracts amc
|
||||
ON amc.asset_id = a.id AND amc.is_active = TRUE AND amc.deleted_at IS NULL
|
||||
WHERE a.deleted_at IS NULL AND a.status NOT IN ('DISPOSED','SCRAPPED');
|
||||
|
||||
CREATE VIEW v_assets AS
|
||||
SELECT
|
||||
a.id,
|
||||
a.asset_code,
|
||||
a.asset_name,
|
||||
ac.name AS category_name,
|
||||
a.brand_model,
|
||||
a.serial_number,
|
||||
a.status,
|
||||
a.condition,
|
||||
a.purchase_date,
|
||||
a.purchase_cost,
|
||||
a.warranty_expiry_date,
|
||||
loc.name AS location_name,
|
||||
loc.type AS location_type,
|
||||
d.name AS department_name,
|
||||
a.location_detail,
|
||||
u.full_name AS assigned_to,
|
||||
v.vendor_name AS supplier,
|
||||
a.qr_code_value,
|
||||
amc.contract_no AS amc_contract_no,
|
||||
amc.end_date AS amc_expiry_date,
|
||||
amc.contract_type AS amc_type,
|
||||
amcv.vendor_name AS amc_vendor,
|
||||
(amc.end_date - CURRENT_DATE) AS amc_days_remaining,
|
||||
ins.policy_no AS insurance_policy_no,
|
||||
ins.policy_end_date AS insurance_expiry_date,
|
||||
ins.insurer_name,
|
||||
ins.sum_insured,
|
||||
(ins.policy_end_date - CURRENT_DATE) AS insurance_days_remaining,
|
||||
lsv.visit_date AS last_service_date,
|
||||
lsv.next_service_date,
|
||||
(lsv.next_service_date - CURRENT_DATE) AS service_due_in_days,
|
||||
a.created_at
|
||||
FROM assets a
|
||||
JOIN item_categories ac ON ac.id = a.item_category_id
|
||||
JOIN locations loc ON loc.id = a.location_id
|
||||
LEFT JOIN departments d ON d.id = a.department_id
|
||||
LEFT JOIN users u ON u.id = a.assigned_to_user_id
|
||||
LEFT JOIN vendors v ON v.id = a.vendor_id
|
||||
LEFT JOIN asset_amc_contracts amc
|
||||
ON amc.asset_id = a.id AND amc.is_active = TRUE AND amc.deleted_at IS NULL
|
||||
LEFT JOIN vendors amcv ON amcv.id = amc.vendor_id
|
||||
LEFT JOIN asset_insurance_policies ins
|
||||
ON ins.asset_id = a.id AND ins.is_active = TRUE AND ins.deleted_at IS NULL
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT visit_date, next_service_date
|
||||
FROM asset_service_visits
|
||||
WHERE asset_id = a.id AND deleted_at IS NULL
|
||||
ORDER BY visit_date DESC LIMIT 1
|
||||
) lsv ON TRUE
|
||||
WHERE a.deleted_at IS NULL;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Verify:
|
||||
-- SELECT column_name FROM information_schema.columns
|
||||
-- WHERE table_name = 'assets'
|
||||
-- AND column_name IN ('location_id','plant_id','warehouse_id');
|
||||
75
scripts/patch-po-billing-shipping-gst.sql
Normal file
75
scripts/patch-po-billing-shipping-gst.sql
Normal file
@ -0,0 +1,75 @@
|
||||
-- Purchase orders: replace plant_id/warehouse_id with billing_id/shipping_id
|
||||
-- (both reference locations, plant OR warehouse) and add cgst/sgst/igst columns
|
||||
-- (tax_total is retained). Idempotent: safe to re-run.
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1. New GST split columns (tax_total kept as the combined total)
|
||||
ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS cgst NUMERIC(15, 4) NOT NULL DEFAULT 0;
|
||||
ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS sgst NUMERIC(15, 4) NOT NULL DEFAULT 0;
|
||||
ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS igst NUMERIC(15, 4) NOT NULL DEFAULT 0;
|
||||
|
||||
-- 2. New billing/shipping location columns
|
||||
ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS billing_id BIGINT;
|
||||
ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS shipping_id BIGINT;
|
||||
|
||||
-- 3. Backfill from the old columns when they still exist
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'purchase_orders' AND column_name = 'plant_id'
|
||||
) THEN
|
||||
UPDATE purchase_orders SET billing_id = plant_id WHERE billing_id IS NULL;
|
||||
UPDATE purchase_orders
|
||||
SET shipping_id = COALESCE(warehouse_id, plant_id)
|
||||
WHERE shipping_id IS NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 4. Enforce NOT NULL now that data is backfilled
|
||||
ALTER TABLE purchase_orders ALTER COLUMN billing_id SET NOT NULL;
|
||||
ALTER TABLE purchase_orders ALTER COLUMN shipping_id SET NOT NULL;
|
||||
|
||||
-- 5. The view selects po.* so it must be dropped before dropping old columns
|
||||
DROP VIEW IF EXISTS v_purchase_orders CASCADE;
|
||||
|
||||
-- 6. Drop old location columns (their FK constraints/indexes drop with them)
|
||||
ALTER TABLE purchase_orders DROP COLUMN IF EXISTS plant_id;
|
||||
ALTER TABLE purchase_orders DROP COLUMN IF EXISTS warehouse_id;
|
||||
|
||||
-- 7. Foreign keys + indexes for the new columns
|
||||
ALTER TABLE purchase_orders DROP CONSTRAINT IF EXISTS fk_po_billing_location;
|
||||
ALTER TABLE purchase_orders
|
||||
ADD CONSTRAINT fk_po_billing_location FOREIGN KEY (billing_id) REFERENCES locations(id);
|
||||
ALTER TABLE purchase_orders DROP CONSTRAINT IF EXISTS fk_po_shipping_location;
|
||||
ALTER TABLE purchase_orders
|
||||
ADD CONSTRAINT fk_po_shipping_location FOREIGN KEY (shipping_id) REFERENCES locations(id);
|
||||
CREATE INDEX IF NOT EXISTS idx_po_billing_id ON purchase_orders(billing_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_po_shipping_id ON purchase_orders(shipping_id);
|
||||
|
||||
-- 8. Recreate the reporting view against billing/shipping locations
|
||||
CREATE VIEW v_purchase_orders AS
|
||||
SELECT
|
||||
po.*,
|
||||
bl.code AS billing_code,
|
||||
bl.name AS billing_name,
|
||||
bl.type AS billing_type,
|
||||
sl.code AS shipping_code,
|
||||
sl.name AS shipping_name,
|
||||
sl.type AS shipping_type,
|
||||
v.vendor_code,
|
||||
v.vendor_name,
|
||||
v.vendor_type
|
||||
FROM purchase_orders po
|
||||
JOIN locations bl ON bl.id = po.billing_id
|
||||
JOIN locations sl ON sl.id = po.shipping_id
|
||||
JOIN vendors v ON v.id = po.vendor_id
|
||||
WHERE po.deleted_at IS NULL;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Verify:
|
||||
-- SELECT column_name FROM information_schema.columns
|
||||
-- WHERE table_name = 'purchase_orders'
|
||||
-- AND column_name IN ('billing_id','shipping_id','cgst','sgst','igst','plant_id','warehouse_id');
|
||||
@ -5,18 +5,17 @@ components:
|
||||
schemas:
|
||||
AssetsCreateBody:
|
||||
type: object
|
||||
required: [asset_name, item_category_id, item_subcategory_id, plant_id]
|
||||
required: [asset_name, item_category_id, item_subcategory_id, location_id]
|
||||
properties:
|
||||
asset_name: { type: string, example: 'CNC Lathe Machine' }
|
||||
item_category_id: { type: integer, example: 1 }
|
||||
item_subcategory_id: { type: integer, example: 1 }
|
||||
plant_id: { type: integer, example: 1 }
|
||||
location_id: { type: integer, example: 1, description: 'Location id (plant or warehouse)' }
|
||||
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 }
|
||||
@ -45,13 +44,12 @@ components:
|
||||
asset_name: { type: string }
|
||||
item_category_id: { type: integer }
|
||||
item_subcategory_id: { type: integer }
|
||||
plant_id: { type: integer }
|
||||
location_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 }
|
||||
@ -89,12 +87,11 @@ components:
|
||||
required: [transfer_date]
|
||||
properties:
|
||||
transfer_date: { type: string, format: date }
|
||||
to_plant_id: { type: integer, nullable: true }
|
||||
to_location_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
|
||||
description: At least one of to_location_id, to_department_id, to_user_id is required
|
||||
AmcContractBody:
|
||||
type: object
|
||||
required: [vendor_id, start_date, end_date]
|
||||
@ -436,7 +433,7 @@ paths:
|
||||
- { name: condition, in: query, schema: { type: string } }
|
||||
- { name: item_category_id, in: query, schema: { type: integer } }
|
||||
- { name: item_subcategory_id, in: query, schema: { type: integer } }
|
||||
- { name: plant_id, in: query, schema: { type: integer } }
|
||||
- { name: location_id, in: query, schema: { type: integer } }
|
||||
- { name: department_id, in: query, schema: { type: integer } }
|
||||
- { name: is_active, in: query, schema: { type: boolean } }
|
||||
responses:
|
||||
@ -457,7 +454,7 @@ paths:
|
||||
- { name: condition, in: query, schema: { type: string } }
|
||||
- { name: item_category_id, in: query, schema: { type: integer } }
|
||||
- { name: item_subcategory_id, in: query, schema: { type: integer } }
|
||||
- { name: plant_id, in: query, schema: { type: integer } }
|
||||
- { name: location_id, in: query, schema: { type: integer } }
|
||||
- { name: department_id, in: query, schema: { type: integer } }
|
||||
- { name: is_active, in: query, schema: { type: boolean } }
|
||||
responses:
|
||||
|
||||
@ -19,12 +19,12 @@ components:
|
||||
remarks: { type: string, example: 'Urgent line' }
|
||||
PurchaseOrdersCreateBody:
|
||||
type: object
|
||||
required: [po_date, vendor_id, plant_id, items]
|
||||
required: [po_date, vendor_id, billing_id, shipping_id, items]
|
||||
properties:
|
||||
po_date: { type: string, format: date, example: '2026-06-18' }
|
||||
vendor_id: { type: integer, example: 1 }
|
||||
plant_id: { type: integer, example: 1 }
|
||||
warehouse_id: { type: integer, nullable: true, example: 1 }
|
||||
billing_id: { type: integer, example: 1, description: 'Location id (plant or warehouse) used for billing / place of supply' }
|
||||
shipping_id: { type: integer, example: 2, description: 'Location id (plant or warehouse) used for shipping / delivery' }
|
||||
payment_term_id: { type: integer, nullable: true, example: 1 }
|
||||
delivery_term_id: { type: integer, nullable: true, example: 1 }
|
||||
expected_delivery_date: { type: string, format: date, nullable: true, example: '2026-07-01' }
|
||||
@ -45,8 +45,8 @@ components:
|
||||
properties:
|
||||
po_date: { type: string, format: date, example: '2026-06-18' }
|
||||
vendor_id: { type: integer, example: 1 }
|
||||
plant_id: { type: integer, example: 1 }
|
||||
warehouse_id: { type: integer, nullable: true }
|
||||
billing_id: { type: integer, example: 1 }
|
||||
shipping_id: { type: integer, example: 2 }
|
||||
payment_term_id: { type: integer, nullable: true }
|
||||
delivery_term_id: { type: integer, nullable: true }
|
||||
expected_delivery_date: { type: string, format: date, nullable: true }
|
||||
@ -100,7 +100,8 @@ paths:
|
||||
- { name: status, in: query, schema: { type: string, example: DRAFT } }
|
||||
- { name: vendor_type, in: query, schema: { type: string, example: RAW_MATERIAL } }
|
||||
- { name: vendor_id, in: query, schema: { type: integer, example: 1 } }
|
||||
- { name: plant_id, in: query, schema: { type: integer, example: 1 } }
|
||||
- { name: billing_id, in: query, schema: { type: integer, example: 1 } }
|
||||
- { name: shipping_id, in: query, schema: { type: integer, example: 2 } }
|
||||
- { name: date_from, in: query, schema: { type: string, format: date } }
|
||||
- { name: date_to, in: query, schema: { type: string, format: date } }
|
||||
responses:
|
||||
@ -120,7 +121,8 @@ paths:
|
||||
- { name: status, in: query, schema: { type: string, example: DRAFT } }
|
||||
- { name: vendor_type, in: query, schema: { type: string, example: RAW_MATERIAL } }
|
||||
- { name: vendor_id, in: query, schema: { type: integer, example: 1 } }
|
||||
- { name: plant_id, in: query, schema: { type: integer, example: 1 } }
|
||||
- { name: billing_id, in: query, schema: { type: integer, example: 1 } }
|
||||
- { name: shipping_id, in: query, schema: { type: integer, example: 2 } }
|
||||
- { name: date_from, in: query, schema: { type: string, format: date } }
|
||||
- { name: date_to, in: query, schema: { type: string, format: date } }
|
||||
responses:
|
||||
|
||||
@ -150,7 +150,7 @@ paths:
|
||||
name: item_subcategory_id
|
||||
schema: { type: integer }
|
||||
- in: query
|
||||
name: plant_id
|
||||
name: location_id
|
||||
schema: { type: integer }
|
||||
- in: query
|
||||
name: department_id
|
||||
@ -205,7 +205,7 @@ paths:
|
||||
name: item_subcategory_id
|
||||
schema: { type: integer }
|
||||
- in: query
|
||||
name: plant_id
|
||||
name: location_id
|
||||
schema: { type: integer }
|
||||
- in: query
|
||||
name: department_id
|
||||
|
||||
@ -6,14 +6,12 @@ const transferAsset = async ({ assetId, transfer, updates, userId }) =>
|
||||
data: {
|
||||
asset_id: BigInt(assetId),
|
||||
transfer_date: transfer.transfer_date,
|
||||
from_plant_id: transfer.from_plant_id,
|
||||
to_plant_id: transfer.to_plant_id,
|
||||
from_location_id: transfer.from_location_id,
|
||||
to_location_id: transfer.to_location_id,
|
||||
from_department_id: transfer.from_department_id,
|
||||
to_department_id: transfer.to_department_id,
|
||||
from_user_id: transfer.from_user_id,
|
||||
to_user_id: transfer.to_user_id,
|
||||
from_warehouse_id: transfer.from_warehouse_id,
|
||||
to_warehouse_id: transfer.to_warehouse_id,
|
||||
reason: transfer.reason,
|
||||
transferred_by: userId ? BigInt(userId) : null,
|
||||
},
|
||||
|
||||
@ -5,7 +5,7 @@ const { getPagination } = require('../../utils/pagination');
|
||||
const { nextDocumentNumber } = require('../../utils/generateCode');
|
||||
const { rowsToCsv } = require('../../utils/csv');
|
||||
const { DISPOSAL_STATUSES, getAssetDropdownOptions } = require('./assets.constants');
|
||||
const { assertPlant, assertWarehouse } = require('../../utils/locations');
|
||||
const { assertAnyLocation } = require('../../utils/locations');
|
||||
const repository = require('./assets.repository');
|
||||
const {
|
||||
DEPRECIATION_METHOD_OPTIONS,
|
||||
@ -27,9 +27,8 @@ const assetInclude = {
|
||||
},
|
||||
},
|
||||
item_subcategories: { select: { id: true, code: true, name: true, item_category_id: true } },
|
||||
plant: { select: { id: true, code: true, name: true } },
|
||||
location: { select: { id: true, code: true, name: true, type: true } },
|
||||
departments: { select: { id: true, name: true } },
|
||||
warehouse: { select: { id: true, code: true, name: true } },
|
||||
users_assets_assigned_to_user_idTousers: {
|
||||
select: { id: true, full_name: true, employee_code: true },
|
||||
},
|
||||
@ -51,14 +50,12 @@ const assetDetailInclude = {
|
||||
};
|
||||
|
||||
const transferInclude = {
|
||||
from_plant: { select: { id: true, code: true, name: true } },
|
||||
to_plant: { select: { id: true, code: true, name: true } },
|
||||
from_location: { select: { id: true, code: true, name: true, type: true } },
|
||||
to_location: { select: { id: true, code: true, name: true, type: true } },
|
||||
departments_asset_transfers_from_department_idTodepartments: { select: { id: true, name: true } },
|
||||
departments_asset_transfers_to_department_idTodepartments: { select: { id: true, name: true } },
|
||||
users_asset_transfers_from_user_idTousers: { select: { id: true, full_name: true } },
|
||||
users_asset_transfers_to_user_idTousers: { select: { id: true, full_name: true } },
|
||||
from_warehouse: { select: { id: true, code: true, name: true } },
|
||||
to_warehouse: { select: { id: true, code: true, name: true } },
|
||||
users_asset_transfers_transferred_byTousers: { select: { id: true, full_name: true } },
|
||||
};
|
||||
|
||||
@ -69,9 +66,8 @@ const sanitizeAsset = (asset) => {
|
||||
const {
|
||||
item_categories,
|
||||
item_subcategories,
|
||||
plant,
|
||||
location,
|
||||
departments,
|
||||
warehouse,
|
||||
users_assets_assigned_to_user_idTousers,
|
||||
vendors_assets_vendor_idTovendors,
|
||||
purchase_orders,
|
||||
@ -102,9 +98,8 @@ const sanitizeAsset = (asset) => {
|
||||
depreciation_rate: depreciationRate,
|
||||
item_category: item_categories || null,
|
||||
item_subcategory: item_subcategories || null,
|
||||
plant: plant || null,
|
||||
location: location || null,
|
||||
department: departments || null,
|
||||
warehouse: warehouse || null,
|
||||
assigned_to_user: users_assets_assigned_to_user_idTousers || null,
|
||||
vendor: vendors_assets_vendor_idTovendors || null,
|
||||
purchase_order: purchase_orders || null,
|
||||
@ -135,28 +130,24 @@ const sanitizeAsset = (asset) => {
|
||||
const sanitizeTransfer = (row) => {
|
||||
if (!row) return null;
|
||||
const {
|
||||
from_plant,
|
||||
to_plant,
|
||||
from_location,
|
||||
to_location,
|
||||
departments_asset_transfers_from_department_idTodepartments,
|
||||
departments_asset_transfers_to_department_idTodepartments,
|
||||
users_asset_transfers_from_user_idTousers,
|
||||
users_asset_transfers_to_user_idTousers,
|
||||
from_warehouse,
|
||||
to_warehouse,
|
||||
users_asset_transfers_transferred_byTousers,
|
||||
...rest
|
||||
} = row;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
from_plant: from_plant || null,
|
||||
to_plant: to_plant || null,
|
||||
from_location: from_location || null,
|
||||
to_location: to_location || null,
|
||||
from_department: departments_asset_transfers_from_department_idTodepartments || null,
|
||||
to_department: departments_asset_transfers_to_department_idTodepartments || null,
|
||||
from_user: users_asset_transfers_from_user_idTousers || null,
|
||||
to_user: users_asset_transfers_to_user_idTousers || null,
|
||||
from_warehouse: from_warehouse || null,
|
||||
to_warehouse: to_warehouse || null,
|
||||
transferred_by_user: users_asset_transfers_transferred_byTousers || null,
|
||||
};
|
||||
};
|
||||
@ -191,10 +182,9 @@ const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
|
||||
if (payload.item_subcategory_id) {
|
||||
await assertItemSubcategory(payload.item_subcategory_id, payload.item_category_id);
|
||||
}
|
||||
await assertPlant(payload.plant_id, 'plant_id');
|
||||
await assertAnyLocation(payload.location_id, 'location_id');
|
||||
if (payload.department_id)
|
||||
await assertReference('departments', payload.department_id, 'department_id');
|
||||
if (payload.warehouse_id) await assertWarehouse(payload.warehouse_id, 'warehouse_id');
|
||||
if (payload.assigned_to_user_id)
|
||||
await assertReference('users', payload.assigned_to_user_id, 'assigned_to_user_id');
|
||||
if (payload.vendor_id) await assertReference('vendors', payload.vendor_id, 'vendor_id');
|
||||
@ -240,9 +230,8 @@ const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
|
||||
manufacturer: payload.manufacturer || null,
|
||||
serial_number: payload.serial_number || null,
|
||||
part_number: payload.part_number || null,
|
||||
plant_id: BigInt(payload.plant_id),
|
||||
location_id: BigInt(payload.location_id),
|
||||
department_id: payload.department_id ? BigInt(payload.department_id) : null,
|
||||
warehouse_id: payload.warehouse_id ? BigInt(payload.warehouse_id) : null,
|
||||
location_detail: payload.location_detail || null,
|
||||
assigned_to_user_id: payload.assigned_to_user_id ? BigInt(payload.assigned_to_user_id) : null,
|
||||
vendor_id: payload.vendor_id ? BigInt(payload.vendor_id) : null,
|
||||
@ -316,7 +305,7 @@ const buildAssetsWhere = (query) => ({
|
||||
...(query.item_subcategory_id
|
||||
? { item_subcategory_id: BigInt(query.item_subcategory_id) }
|
||||
: {}),
|
||||
...(query.plant_id ? { plant_id: BigInt(query.plant_id) } : {}),
|
||||
...(query.location_id ? { location_id: BigInt(query.location_id) } : {}),
|
||||
...(query.department_id ? { department_id: BigInt(query.department_id) } : {}),
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.search
|
||||
@ -365,9 +354,8 @@ const exportAssets = async (query) => {
|
||||
{ key: 'serial_number', header: 'Serial Number' },
|
||||
{ key: 'condition', header: 'Condition' },
|
||||
{ key: 'status', header: 'Status' },
|
||||
{ key: (row) => row.plant?.name || '', header: 'Plant' },
|
||||
{ key: (row) => row.location?.name || '', header: 'Location' },
|
||||
{ key: (row) => row.department?.name || '', header: 'Department' },
|
||||
{ key: (row) => row.warehouse?.name || '', header: 'Warehouse' },
|
||||
{ key: (row) => row.assigned_to_user?.full_name || '', header: 'Assigned To' },
|
||||
{ key: (row) => row.vendor?.vendor_name || '', header: 'Vendor' },
|
||||
{ key: (row) => row.purchase_order?.po_number || '', header: 'PO Number' },
|
||||
@ -398,10 +386,9 @@ const updateAsset = async (id, payload, userId, requestId) => {
|
||||
serial_number:
|
||||
payload.serial_number !== undefined ? payload.serial_number : existing.serial_number,
|
||||
part_number: payload.part_number !== undefined ? payload.part_number : existing.part_number,
|
||||
plant_id: payload.plant_id ?? existing.plant_id,
|
||||
location_id: payload.location_id ?? existing.location_id,
|
||||
department_id:
|
||||
payload.department_id !== undefined ? payload.department_id : existing.department_id,
|
||||
warehouse_id: payload.warehouse_id !== undefined ? payload.warehouse_id : existing.warehouse_id,
|
||||
location_detail:
|
||||
payload.location_detail !== undefined ? payload.location_detail : existing.location_detail,
|
||||
assigned_to_user_id:
|
||||
@ -506,37 +493,31 @@ const transferAsset = async (id, payload, userId, requestId) => {
|
||||
throw new ApiError(409, 'Cannot transfer disposed or scrapped assets');
|
||||
}
|
||||
|
||||
if (payload.to_plant_id) await assertPlant(payload.to_plant_id, 'to_plant_id');
|
||||
if (payload.to_location_id) await assertAnyLocation(payload.to_location_id, 'to_location_id');
|
||||
if (payload.to_department_id) {
|
||||
await assertReference('departments', payload.to_department_id, 'to_department_id');
|
||||
}
|
||||
if (payload.to_user_id) await assertReference('users', payload.to_user_id, 'to_user_id');
|
||||
if (payload.to_warehouse_id) await assertWarehouse(payload.to_warehouse_id, 'to_warehouse_id');
|
||||
|
||||
const transfer = {
|
||||
transfer_date: toDateOnly(payload.transfer_date),
|
||||
from_plant_id: existing.plant_id,
|
||||
to_plant_id: payload.to_plant_id ? BigInt(payload.to_plant_id) : null,
|
||||
from_location_id: existing.location_id,
|
||||
to_location_id: payload.to_location_id ? BigInt(payload.to_location_id) : null,
|
||||
from_department_id: existing.department_id,
|
||||
to_department_id: payload.to_department_id ? BigInt(payload.to_department_id) : null,
|
||||
from_user_id: existing.assigned_to_user_id,
|
||||
to_user_id: payload.to_user_id ? BigInt(payload.to_user_id) : null,
|
||||
from_warehouse_id: existing.warehouse_id,
|
||||
to_warehouse_id: payload.to_warehouse_id ? BigInt(payload.to_warehouse_id) : null,
|
||||
reason: payload.reason || null,
|
||||
};
|
||||
|
||||
const updates = {
|
||||
...(payload.to_plant_id ? { plant_id: BigInt(payload.to_plant_id) } : {}),
|
||||
...(payload.to_location_id ? { location_id: BigInt(payload.to_location_id) } : {}),
|
||||
...(payload.to_department_id !== undefined
|
||||
? { department_id: payload.to_department_id ? BigInt(payload.to_department_id) : null }
|
||||
: {}),
|
||||
...(payload.to_user_id !== undefined
|
||||
? { assigned_to_user_id: payload.to_user_id ? BigInt(payload.to_user_id) : null }
|
||||
: {}),
|
||||
...(payload.to_warehouse_id !== undefined
|
||||
? { warehouse_id: payload.to_warehouse_id ? BigInt(payload.to_warehouse_id) : null }
|
||||
: {}),
|
||||
};
|
||||
|
||||
const { transferRow } = await repository.transferAsset({
|
||||
|
||||
@ -22,9 +22,8 @@ const assetFields = {
|
||||
manufacturer: Joi.string().max(200).allow(null, '').optional(),
|
||||
serial_number: Joi.string().max(100).allow(null, '').optional(),
|
||||
part_number: Joi.string().max(100).allow(null, '').optional(),
|
||||
plant_id: Joi.number().integer().positive().required(),
|
||||
location_id: Joi.number().integer().positive().required(),
|
||||
department_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
warehouse_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
location_detail: Joi.string().max(200).allow(null, '').optional(),
|
||||
assigned_to_user_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
vendor_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
@ -65,9 +64,8 @@ const updateAssetSchema = Joi.object({
|
||||
manufacturer: assetFields.manufacturer,
|
||||
serial_number: assetFields.serial_number,
|
||||
part_number: assetFields.part_number,
|
||||
plant_id: assetFields.plant_id.optional(),
|
||||
location_id: assetFields.location_id.optional(),
|
||||
department_id: assetFields.department_id,
|
||||
warehouse_id: assetFields.warehouse_id,
|
||||
location_detail: assetFields.location_detail,
|
||||
assigned_to_user_id: assetFields.assigned_to_user_id,
|
||||
vendor_id: assetFields.vendor_id,
|
||||
@ -103,7 +101,7 @@ const listAssetsQuerySchema = Joi.object({
|
||||
.optional(),
|
||||
item_category_id: Joi.number().integer().positive().optional(),
|
||||
item_subcategory_id: Joi.number().integer().positive().optional(),
|
||||
plant_id: Joi.number().integer().positive().optional(),
|
||||
location_id: Joi.number().integer().positive().optional(),
|
||||
department_id: Joi.number().integer().positive().optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
@ -115,12 +113,11 @@ const exportAssetsQuerySchema = listAssetsQuerySchema.keys({
|
||||
|
||||
const transferAssetSchema = Joi.object({
|
||||
transfer_date: Joi.date().iso().required(),
|
||||
to_plant_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
to_location_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
to_department_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
to_user_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
to_warehouse_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
reason: Joi.string().allow(null, '').optional(),
|
||||
}).or('to_plant_id', 'to_department_id', 'to_user_id', 'to_warehouse_id');
|
||||
}).or('to_location_id', 'to_department_id', 'to_user_id');
|
||||
|
||||
const amcContractSchema = Joi.object({
|
||||
vendor_id: Joi.number().integer().positive().required(),
|
||||
|
||||
@ -4,7 +4,7 @@ const { recalculatePoStatus } = require('./grn.poStatus');
|
||||
|
||||
const createAssetsForLine = async (
|
||||
tx,
|
||||
{ grn, grnItem, item, itemCategory, itemSubcategory, plantId, assetCodes, userId }
|
||||
{ grn, grnItem, item, itemCategory, itemSubcategory, locationId, assetCodes, userId }
|
||||
) => {
|
||||
const createdAssets = [];
|
||||
|
||||
@ -15,8 +15,7 @@ const createAssetsForLine = async (
|
||||
asset_name: assetCodes.length > 1 ? `${item.item_name} #${index + 1}` : item.item_name,
|
||||
item_category_id: itemCategory.id,
|
||||
item_subcategory_id: itemSubcategory.id,
|
||||
plant_id: BigInt(plantId),
|
||||
warehouse_id: grn.warehouse_id,
|
||||
location_id: BigInt(locationId),
|
||||
vendor_id: grn.vendor_id,
|
||||
po_id: grn.po_id,
|
||||
grn_id: grn.id,
|
||||
@ -69,7 +68,7 @@ const createGrnWithReceipt = async ({ grnNumber, header, items, assetPlans, user
|
||||
item: plan.item,
|
||||
itemCategory: plan.itemCategory,
|
||||
itemSubcategory: plan.itemSubcategory,
|
||||
plantId: plan.plantId,
|
||||
locationId: plan.locationId,
|
||||
assetCodes: plan.assetCodes,
|
||||
userId,
|
||||
});
|
||||
|
||||
@ -356,7 +356,7 @@ const validateAndBuildItems = async (po, payloadItems) => {
|
||||
item,
|
||||
itemCategory,
|
||||
itemSubcategory,
|
||||
plantId: po.plant_id,
|
||||
locationId: po.shipping_id,
|
||||
assetCodes,
|
||||
});
|
||||
}
|
||||
|
||||
@ -60,4 +60,19 @@ const computeHeaderTotals = (
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = { computeLineAmounts, computeHeaderTotals, round4, toNum };
|
||||
/**
|
||||
* Split a total GST amount into CGST/SGST (intra-state) or IGST (inter-state).
|
||||
* For intra-state, CGST and SGST each take half of the tax; SGST absorbs any
|
||||
* rounding remainder so cgst + sgst === taxTotal.
|
||||
*/
|
||||
const splitGst = (taxTotal, isInterState) => {
|
||||
const total = round4(taxTotal);
|
||||
if (isInterState) {
|
||||
return { cgst: 0, sgst: 0, igst: total };
|
||||
}
|
||||
const cgst = round4(total / 2);
|
||||
const sgst = round4(total - cgst);
|
||||
return { cgst, sgst, igst: 0 };
|
||||
};
|
||||
|
||||
module.exports = { computeLineAmounts, computeHeaderTotals, splitGst, round4, toNum };
|
||||
|
||||
@ -16,15 +16,22 @@ const {
|
||||
AMENDABLE_STATUSES,
|
||||
DELETABLE_STATUSES,
|
||||
} = require('./purchase-orders.constants');
|
||||
const { computeLineAmounts, computeHeaderTotals, toNum } = require('./purchase-orders.calculations');
|
||||
const {
|
||||
computeLineAmounts,
|
||||
computeHeaderTotals,
|
||||
splitGst,
|
||||
toNum,
|
||||
} = require('./purchase-orders.calculations');
|
||||
const repository = require('./purchase-orders.repository');
|
||||
const { assertPlant, assertWarehouse } = require('../../utils/locations');
|
||||
const { assertAnyLocation } = require('../../utils/locations');
|
||||
const { sanitizeAttachment } = require('./po.attachments.service');
|
||||
|
||||
const locationSummarySelect = { id: true, code: true, name: true, type: true };
|
||||
|
||||
const poListInclude = {
|
||||
vendors: { select: { id: true, vendor_code: true, vendor_name: true, vendor_type: true } },
|
||||
plant: { select: { id: true, code: true, name: true } },
|
||||
warehouse: { select: { id: true, code: true, name: true } },
|
||||
billing_location: { select: locationSummarySelect },
|
||||
shipping_location: { select: locationSummarySelect },
|
||||
users_purchase_orders_created_byTousers: { select: { id: true, full_name: true } },
|
||||
};
|
||||
|
||||
@ -83,11 +90,26 @@ const poPdfInclude = {
|
||||
},
|
||||
},
|
||||
},
|
||||
plant: {
|
||||
billing_location: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
type: true,
|
||||
gstin: true,
|
||||
address: true,
|
||||
city: true,
|
||||
state: true,
|
||||
pincode: true,
|
||||
},
|
||||
},
|
||||
shipping_location: {
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
type: true,
|
||||
gstin: true,
|
||||
address: true,
|
||||
city: true,
|
||||
state: true,
|
||||
@ -106,8 +128,8 @@ const sanitizePo = (po) => {
|
||||
if (!po) return null;
|
||||
const {
|
||||
vendors,
|
||||
plant,
|
||||
warehouse,
|
||||
billing_location,
|
||||
shipping_location,
|
||||
payment_terms,
|
||||
delivery_terms,
|
||||
users_purchase_orders_created_byTousers,
|
||||
@ -122,8 +144,8 @@ const sanitizePo = (po) => {
|
||||
return {
|
||||
...rest,
|
||||
vendor: vendors || null,
|
||||
plant: plant || null,
|
||||
warehouse: warehouse || null,
|
||||
billing: billing_location || null,
|
||||
shipping: shipping_location || null,
|
||||
payment_term: payment_terms || null,
|
||||
delivery_term: delivery_terms || null,
|
||||
created_by_user: users_purchase_orders_created_byTousers || null,
|
||||
@ -151,8 +173,8 @@ const sanitizePo = (po) => {
|
||||
po_approvals: undefined,
|
||||
po_attachments: undefined,
|
||||
vendors: undefined,
|
||||
plants: undefined,
|
||||
warehouses: undefined,
|
||||
billing_location: undefined,
|
||||
shipping_location: undefined,
|
||||
payment_terms: undefined,
|
||||
delivery_terms: undefined,
|
||||
users_purchase_orders_created_byTousers: undefined,
|
||||
@ -252,10 +274,25 @@ const validateAndBuildItems = async (items) => {
|
||||
return builtItems;
|
||||
};
|
||||
|
||||
const normalizeState = (value) =>
|
||||
value === null || value === undefined ? '' : String(value).trim().toLowerCase();
|
||||
|
||||
/**
|
||||
* Inter-state (IGST) when the place of supply (billing location state) differs
|
||||
* from the vendor's source of supply. When either state is unknown we default
|
||||
* to intra-state (CGST/SGST).
|
||||
*/
|
||||
const isInterStateSupply = (billingLocation, vendor) => {
|
||||
const placeOfSupply = normalizeState(billingLocation?.state);
|
||||
const vendorState = normalizeState(vendor?.source_of_supply);
|
||||
if (!placeOfSupply || !vendorState) return false;
|
||||
return placeOfSupply !== vendorState;
|
||||
};
|
||||
|
||||
const buildHeaderData = async (payload, builtItems, userId) => {
|
||||
await assertReference('vendors', payload.vendor_id, 'vendor_id');
|
||||
await assertPlant(payload.plant_id, 'plant_id');
|
||||
if (payload.warehouse_id) await assertWarehouse(payload.warehouse_id, 'warehouse_id');
|
||||
const vendor = await assertReference('vendors', payload.vendor_id, 'vendor_id');
|
||||
const billing = await assertAnyLocation(payload.billing_id, 'billing_id');
|
||||
await assertAnyLocation(payload.shipping_id, 'shipping_id');
|
||||
if (payload.payment_term_id)
|
||||
await assertReference('payment_terms', payload.payment_term_id, 'payment_term_id');
|
||||
if (payload.delivery_term_id)
|
||||
@ -276,11 +313,13 @@ const buildHeaderData = async (payload, builtItems, userId) => {
|
||||
}
|
||||
);
|
||||
|
||||
const gstSplit = splitGst(totals.tax_total, isInterStateSupply(billing, vendor));
|
||||
|
||||
return {
|
||||
po_date: toDateOnly(payload.po_date),
|
||||
vendor_id: BigInt(payload.vendor_id),
|
||||
plant_id: BigInt(payload.plant_id),
|
||||
warehouse_id: payload.warehouse_id ? BigInt(payload.warehouse_id) : null,
|
||||
billing_id: BigInt(payload.billing_id),
|
||||
shipping_id: BigInt(payload.shipping_id),
|
||||
payment_term_id: payload.payment_term_id ? BigInt(payload.payment_term_id) : null,
|
||||
delivery_term_id: payload.delivery_term_id ? BigInt(payload.delivery_term_id) : null,
|
||||
expected_delivery_date: payload.expected_delivery_date
|
||||
@ -291,6 +330,7 @@ const buildHeaderData = async (payload, builtItems, userId) => {
|
||||
tds_applicable: tdsApplicable,
|
||||
tds_section_pct: tdsApplicable ? tdsSectionPct : null,
|
||||
...totals,
|
||||
...gstSplit,
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
};
|
||||
};
|
||||
@ -357,14 +397,22 @@ const buildPoPdfPayload = (po, company) => {
|
||||
state: vendorAddress?.state || '',
|
||||
pincode: vendorAddress?.pincode || '',
|
||||
},
|
||||
ship_to: {
|
||||
name: po.plant?.name || '-',
|
||||
address: po.plant?.address || '',
|
||||
city: po.plant?.city || '',
|
||||
state: po.plant?.state || '',
|
||||
pincode: po.plant?.pincode || '',
|
||||
bill_to: {
|
||||
name: po.billing?.name || '-',
|
||||
gstin: po.billing?.gstin || '',
|
||||
address: po.billing?.address || '',
|
||||
city: po.billing?.city || '',
|
||||
state: po.billing?.state || '',
|
||||
pincode: po.billing?.pincode || '',
|
||||
},
|
||||
ship_to: {
|
||||
name: po.shipping?.name || '-',
|
||||
gstin: po.shipping?.gstin || '',
|
||||
address: po.shipping?.address || '',
|
||||
city: po.shipping?.city || '',
|
||||
state: po.shipping?.state || '',
|
||||
pincode: po.shipping?.pincode || '',
|
||||
},
|
||||
warehouse: { name: po.warehouse?.name || '-' },
|
||||
items: (po.items || []).map((line) => {
|
||||
const orderedQty = toNum(line.ordered_qty);
|
||||
const rate = toNum(line.rate);
|
||||
@ -384,6 +432,9 @@ const buildPoPdfPayload = (po, company) => {
|
||||
totals: {
|
||||
sub_total: toNum(po.sub_total),
|
||||
tax_total: toNum(po.tax_total),
|
||||
cgst: toNum(po.cgst),
|
||||
sgst: toNum(po.sgst),
|
||||
igst: toNum(po.igst),
|
||||
freight_charges: toNum(po.freight_charges),
|
||||
other_charges: toNum(po.other_charges),
|
||||
discount_amount: toNum(po.discount_amount),
|
||||
@ -427,7 +478,8 @@ const buildPurchaseOrdersWhere = (query) => ({
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(query.vendor_id ? { vendor_id: BigInt(query.vendor_id) } : {}),
|
||||
...(query.vendor_type ? { vendors: { vendor_type: query.vendor_type } } : {}),
|
||||
...(query.plant_id ? { plant_id: BigInt(query.plant_id) } : {}),
|
||||
...(query.billing_id ? { billing_id: BigInt(query.billing_id) } : {}),
|
||||
...(query.shipping_id ? { shipping_id: BigInt(query.shipping_id) } : {}),
|
||||
...(query.search ? { po_number: { contains: query.search, mode: 'insensitive' } } : {}),
|
||||
...(query.date_from || query.date_to
|
||||
? {
|
||||
@ -473,11 +525,14 @@ const exportPurchaseOrders = async (query) => {
|
||||
{ key: 'revision_no', header: 'Revision' },
|
||||
{ key: (row) => row.vendor?.vendor_code || '', header: 'Vendor Code' },
|
||||
{ key: (row) => row.vendor?.vendor_name || '', header: 'Vendor Name' },
|
||||
{ key: (row) => row.plant?.name || '', header: 'Plant' },
|
||||
{ key: (row) => row.warehouse?.name || '', header: 'Warehouse' },
|
||||
{ key: (row) => row.billing?.name || '', header: 'Billing Location' },
|
||||
{ key: (row) => row.shipping?.name || '', header: 'Shipping Location' },
|
||||
{ key: 'expected_delivery_date', header: 'Expected Delivery', type: 'date' },
|
||||
{ key: 'sub_total', header: 'Sub Total' },
|
||||
{ key: 'tax_total', header: 'Tax Total' },
|
||||
{ key: 'cgst', header: 'CGST' },
|
||||
{ key: 'sgst', header: 'SGST' },
|
||||
{ key: 'igst', header: 'IGST' },
|
||||
{ key: 'grand_total', header: 'Grand Total' },
|
||||
{ key: (row) => row.created_by_user?.full_name || '', header: 'Created By' },
|
||||
{ key: 'created_at', header: 'Created At', type: 'datetime' },
|
||||
@ -496,8 +551,8 @@ const updatePurchaseOrder = async (id, payload, userId, requestId) => {
|
||||
const merged = {
|
||||
po_date: payload.po_date ?? existing.po_date,
|
||||
vendor_id: payload.vendor_id ?? existing.vendor_id,
|
||||
plant_id: payload.plant_id ?? existing.plant_id,
|
||||
warehouse_id: payload.warehouse_id !== undefined ? payload.warehouse_id : existing.warehouse_id,
|
||||
billing_id: payload.billing_id ?? existing.billing_id,
|
||||
shipping_id: payload.shipping_id ?? existing.shipping_id,
|
||||
payment_term_id:
|
||||
payload.payment_term_id !== undefined ? payload.payment_term_id : existing.payment_term_id,
|
||||
delivery_term_id:
|
||||
@ -765,8 +820,8 @@ const amendPurchaseOrder = async (id, payload, userId, requestId) => {
|
||||
const merged = {
|
||||
po_date: payload.po_date ?? existing.po_date,
|
||||
vendor_id: payload.vendor_id ?? existing.vendor_id,
|
||||
plant_id: payload.plant_id ?? existing.plant_id,
|
||||
warehouse_id: payload.warehouse_id !== undefined ? payload.warehouse_id : existing.warehouse_id,
|
||||
billing_id: payload.billing_id ?? existing.billing_id,
|
||||
shipping_id: payload.shipping_id ?? existing.shipping_id,
|
||||
payment_term_id:
|
||||
payload.payment_term_id !== undefined ? payload.payment_term_id : existing.payment_term_id,
|
||||
delivery_term_id:
|
||||
|
||||
@ -17,8 +17,8 @@ const poItemSchema = Joi.object({
|
||||
const poHeaderFields = {
|
||||
po_date: Joi.date().iso().required(),
|
||||
vendor_id: Joi.number().integer().positive().required(),
|
||||
plant_id: Joi.number().integer().positive().required(),
|
||||
warehouse_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
billing_id: Joi.number().integer().positive().required(),
|
||||
shipping_id: Joi.number().integer().positive().required(),
|
||||
payment_term_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
delivery_term_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
expected_delivery_date: Joi.date().iso().allow(null).optional(),
|
||||
@ -48,8 +48,8 @@ const createPurchaseOrderSchema = Joi.object({
|
||||
const updatePurchaseOrderSchema = Joi.object({
|
||||
po_date: poHeaderFields.po_date.optional(),
|
||||
vendor_id: poHeaderFields.vendor_id.optional(),
|
||||
plant_id: poHeaderFields.plant_id.optional(),
|
||||
warehouse_id: poHeaderFields.warehouse_id.optional(),
|
||||
billing_id: poHeaderFields.billing_id.optional(),
|
||||
shipping_id: poHeaderFields.shipping_id.optional(),
|
||||
payment_term_id: poHeaderFields.payment_term_id.optional(),
|
||||
delivery_term_id: poHeaderFields.delivery_term_id.optional(),
|
||||
expected_delivery_date: poHeaderFields.expected_delivery_date.optional(),
|
||||
@ -67,8 +67,8 @@ const updatePurchaseOrderSchema = Joi.object({
|
||||
const amendPurchaseOrderSchema = Joi.object({
|
||||
po_date: poHeaderFields.po_date.optional(),
|
||||
vendor_id: poHeaderFields.vendor_id.optional(),
|
||||
plant_id: poHeaderFields.plant_id.optional(),
|
||||
warehouse_id: poHeaderFields.warehouse_id.optional(),
|
||||
billing_id: poHeaderFields.billing_id.optional(),
|
||||
shipping_id: poHeaderFields.shipping_id.optional(),
|
||||
payment_term_id: poHeaderFields.payment_term_id.optional(),
|
||||
delivery_term_id: poHeaderFields.delivery_term_id.optional(),
|
||||
expected_delivery_date: poHeaderFields.expected_delivery_date.optional(),
|
||||
@ -94,7 +94,8 @@ const listPurchaseOrdersQuerySchema = Joi.object({
|
||||
.valid(...VENDOR_TYPES)
|
||||
.optional(),
|
||||
vendor_id: Joi.number().integer().positive().optional(),
|
||||
plant_id: Joi.number().integer().positive().optional(),
|
||||
billing_id: Joi.number().integer().positive().optional(),
|
||||
shipping_id: Joi.number().integer().positive().optional(),
|
||||
date_from: Joi.date().iso().optional(),
|
||||
date_to: Joi.date().iso().optional(),
|
||||
});
|
||||
|
||||
@ -6,7 +6,7 @@ const { calculateDepreciation, round4 } = require('../assets/assets.depreciation
|
||||
const depreciationInclude = {
|
||||
item_categories: { select: { id: true, code: true, name: true } },
|
||||
item_subcategories: { select: { id: true, code: true, name: true } },
|
||||
plant: { select: { id: true, code: true, name: true } },
|
||||
location: { select: { id: true, code: true, name: true, type: true } },
|
||||
departments: { select: { id: true, name: true } },
|
||||
};
|
||||
|
||||
@ -19,7 +19,7 @@ const buildDepreciationWhere = (query) => {
|
||||
...(query.item_subcategory_id
|
||||
? { item_subcategory_id: BigInt(query.item_subcategory_id) }
|
||||
: {}),
|
||||
...(query.plant_id ? { plant_id: BigInt(query.plant_id) } : {}),
|
||||
...(query.location_id ? { location_id: BigInt(query.location_id) } : {}),
|
||||
...(query.department_id ? { department_id: BigInt(query.department_id) } : {}),
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.search
|
||||
@ -73,7 +73,7 @@ const sanitizeDepreciationRow = (asset, asOfDate) => {
|
||||
depreciation_rate: depreciationRate,
|
||||
item_category: asset.item_categories || null,
|
||||
item_subcategory: asset.item_subcategories || null,
|
||||
plant: asset.plant || null,
|
||||
location: asset.location || null,
|
||||
department: asset.departments || null,
|
||||
depreciation,
|
||||
};
|
||||
@ -141,10 +141,10 @@ const listAssetDepreciation = async (query) => {
|
||||
};
|
||||
|
||||
const getDepreciationFilterOptions = async () => {
|
||||
const [plants, departments, categories, subcategories] = await Promise.all([
|
||||
const [locations, departments, categories, subcategories] = await Promise.all([
|
||||
prisma.locations.findMany({
|
||||
where: { deleted_at: null, is_active: true, type: 'plant' },
|
||||
select: { id: true, code: true, name: true },
|
||||
where: { deleted_at: null, is_active: true },
|
||||
select: { id: true, code: true, name: true, type: true },
|
||||
orderBy: { name: 'asc' },
|
||||
}),
|
||||
prisma.departments.findMany({
|
||||
@ -165,7 +165,7 @@ const getDepreciationFilterOptions = async () => {
|
||||
]);
|
||||
|
||||
return {
|
||||
plants,
|
||||
locations,
|
||||
departments,
|
||||
item_categories: categories,
|
||||
item_subcategories: subcategories,
|
||||
@ -201,7 +201,7 @@ const exportAssetDepreciation = async (query) => {
|
||||
{ key: 'status', header: 'Status' },
|
||||
{ key: (row) => row.item_category?.name || '', header: 'Category' },
|
||||
{ key: (row) => row.item_subcategory?.name || '', header: 'Subcategory' },
|
||||
{ key: (row) => row.plant?.name || '', header: 'Plant' },
|
||||
{ key: (row) => row.location?.name || '', header: 'Location' },
|
||||
{ key: (row) => row.department?.name || '', header: 'Department' },
|
||||
{ key: 'purchase_date', header: 'Purchase Date', type: 'date' },
|
||||
{ key: 'purchase_cost', header: 'Purchase Cost' },
|
||||
|
||||
@ -11,7 +11,7 @@ const depreciationReportQuerySchema = Joi.object({
|
||||
depreciation_method: Joi.string().valid('SLM', 'WDV', 'OTHER').optional(),
|
||||
item_category_id: Joi.number().integer().positive().optional(),
|
||||
item_subcategory_id: Joi.number().integer().positive().optional(),
|
||||
plant_id: Joi.number().integer().positive().optional(),
|
||||
location_id: Joi.number().integer().positive().optional(),
|
||||
department_id: Joi.number().integer().positive().optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
as_of_date: Joi.date().iso().optional(),
|
||||
|
||||
@ -31,6 +31,20 @@ const assertPlant = (id, label = 'plant_id') =>
|
||||
const assertWarehouse = (id, label = 'warehouse_id') =>
|
||||
assertLocation(id, LOCATION_TYPES.WAREHOUSE, label);
|
||||
|
||||
/** Assert a location of any type (plant or warehouse). Returns the row (with parent). */
|
||||
const assertAnyLocation = async (id, label, { requireActive = true } = {}) => {
|
||||
if (!id) return null;
|
||||
|
||||
const row = await prisma.locations.findFirst({
|
||||
where: { id: BigInt(id), deleted_at: null },
|
||||
include: { parent: { select: { id: true, code: true, name: true } } },
|
||||
});
|
||||
|
||||
if (!row) throw new ApiError(422, `Invalid ${label}`);
|
||||
if (requireActive && !row.is_active) throw new ApiError(422, `${label} is inactive`);
|
||||
return row;
|
||||
};
|
||||
|
||||
const locationListSelect = { id: true, code: true, name: true, type: true };
|
||||
|
||||
const plantListSelect = { id: true, code: true, name: true };
|
||||
@ -85,6 +99,7 @@ const toLocationResponse = (row) => {
|
||||
module.exports = {
|
||||
LOCATION_TYPES,
|
||||
assertLocation,
|
||||
assertAnyLocation,
|
||||
assertPlant,
|
||||
assertWarehouse,
|
||||
locationListSelect,
|
||||
|
||||
@ -33,17 +33,14 @@ const getDummyAssetData = () => ({
|
||||
code: 'PLT',
|
||||
name: 'Plant & Machinery',
|
||||
},
|
||||
plant: {
|
||||
code: 'PLT-NOI-01',
|
||||
name: 'Noida Manufacturing Plant',
|
||||
location: {
|
||||
code: 'WH-NOI-01',
|
||||
name: 'Noida Main Warehouse',
|
||||
type: 'warehouse',
|
||||
},
|
||||
department: {
|
||||
name: 'Production Engineering',
|
||||
},
|
||||
warehouse: {
|
||||
code: 'WH-NOI-01',
|
||||
name: 'Noida Main Warehouse',
|
||||
},
|
||||
assigned_to: {
|
||||
full_name: 'Vikram Singh',
|
||||
employee_code: 'EMP-1042',
|
||||
@ -124,9 +121,8 @@ const generateAssetHtml = (inputData = getDummyAssetData()) => {
|
||||
<div class="card-title">Location & Assignment</div>
|
||||
<div class="card-body">
|
||||
${renderInfoRows([
|
||||
['Plant', `${escapeHtml(data.plant.code)} - ${escapeHtml(data.plant.name)}`],
|
||||
['Location', `${escapeHtml(data.location.code)} - ${escapeHtml(data.location.name)}`],
|
||||
['Department', escapeHtml(data.department.name)],
|
||||
['Warehouse', `${escapeHtml(data.warehouse.code)} - ${escapeHtml(data.warehouse.name)}`],
|
||||
['Location Detail', escapeHtml(data.asset.location_detail)],
|
||||
[
|
||||
'Assigned To',
|
||||
|
||||
@ -34,14 +34,22 @@ const getDummyPoData = () => ({
|
||||
state: 'Jharkhand',
|
||||
pincode: '831001',
|
||||
},
|
||||
ship_to: {
|
||||
bill_to: {
|
||||
name: 'Chennai manufacturing plant',
|
||||
gstin: '33AAACT1234A1Z5',
|
||||
address: 'Guindy Industrial Estate',
|
||||
city: 'Chennai',
|
||||
state: 'Tamil Nadu',
|
||||
pincode: '600032',
|
||||
},
|
||||
warehouse: { name: 'Main warehouse' },
|
||||
ship_to: {
|
||||
name: 'Main warehouse',
|
||||
gstin: '33AAACT1234A1Z5',
|
||||
address: 'Ambattur Industrial Estate',
|
||||
city: 'Chennai',
|
||||
state: 'Tamil Nadu',
|
||||
pincode: '600058',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
line_no: 1,
|
||||
@ -81,6 +89,9 @@ const getDummyPoData = () => ({
|
||||
totals: {
|
||||
sub_total: 119775,
|
||||
tax_total: 19768.75,
|
||||
cgst: 9884.38,
|
||||
sgst: 9884.37,
|
||||
igst: 0,
|
||||
freight_charges: 750,
|
||||
other_charges: 150,
|
||||
discount_amount: 500,
|
||||
@ -120,8 +131,8 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
|
||||
const company = data.company || {};
|
||||
const po = data.po || {};
|
||||
const vendor = data.vendor || {};
|
||||
const billTo = data.bill_to || {};
|
||||
const shipTo = data.ship_to || {};
|
||||
const warehouse = data.warehouse || {};
|
||||
const totals = data.totals || {};
|
||||
|
||||
const itemsRows = (data.items || [])
|
||||
@ -155,7 +166,28 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
|
||||
const discountValue = Number(totals.discount_amount || 0);
|
||||
const discountFormatted = discountValue > 0 ? `-${formatCurrency(discountValue)}` : formatCurrency(0);
|
||||
|
||||
const cgst = Number(totals.cgst || 0);
|
||||
const sgst = Number(totals.sgst || 0);
|
||||
const igst = Number(totals.igst || 0);
|
||||
const taxRowsHtml =
|
||||
igst > 0
|
||||
? `
|
||||
<div class="summary-row">
|
||||
<span>IGST</span>
|
||||
<span class="amount">${formatCurrency(igst)}</span>
|
||||
</div>`
|
||||
: `
|
||||
<div class="summary-row">
|
||||
<span>CGST</span>
|
||||
<span class="amount">${formatCurrency(cgst)}</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span>SGST</span>
|
||||
<span class="amount">${formatCurrency(sgst)}</span>
|
||||
</div>`;
|
||||
|
||||
const vendorAddress = joinParts(vendor.address, vendor.city, vendor.state, vendor.pincode);
|
||||
const billAddress = joinParts(billTo.address, billTo.city, billTo.state, billTo.pincode);
|
||||
const shipAddress = joinParts(shipTo.address, shipTo.city, shipTo.state, shipTo.pincode);
|
||||
const generatedAt = data.generated_at || formatDate(new Date(), { style: 'datetime' });
|
||||
|
||||
@ -453,10 +485,26 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">Bill to</div>
|
||||
<div class="party-name">${escapeHtml(billTo.name || '-')}</div>
|
||||
<div class="party-body">
|
||||
${escapeHtml(billAddress || '-')}<br />
|
||||
${billTo.gstin ? `GSTIN: ${escapeHtml(billTo.gstin)}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="divider-light" />
|
||||
|
||||
<div class="party-row">
|
||||
<div>
|
||||
<div class="label">Ship to</div>
|
||||
<div class="party-name">${escapeHtml(shipTo.name || '-')}</div>
|
||||
<div class="party-body">${escapeHtml(shipAddress || '-')}</div>
|
||||
<div class="party-body">
|
||||
${escapeHtml(shipAddress || '-')}<br />
|
||||
${shipTo.gstin ? `GSTIN: ${escapeHtml(shipTo.gstin)}` : ''}
|
||||
</div>
|
||||
${
|
||||
po.delivery_term
|
||||
? `<div class="party-term">Delivery term: ${escapeHtml(po.delivery_term)}</div>`
|
||||
@ -472,10 +520,6 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
|
||||
<div class="label">Vendor type</div>
|
||||
<div class="value">${escapeHtml(po.vendor_type || '-')}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">Warehouse</div>
|
||||
<div class="value">${escapeHtml(warehouse.name || '-')}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">Revision</div>
|
||||
<div class="value">${escapeHtml(String(po.revision_no ?? 0))}</div>
|
||||
@ -513,6 +557,7 @@ const generatePoHtml = (inputData = getDummyPoData()) => {
|
||||
<span>Tax (GST)</span>
|
||||
<span class="amount">${formatCurrency(totals.tax_total)}</span>
|
||||
</div>
|
||||
${taxRowsHtml}
|
||||
<div class="summary-row">
|
||||
<span>Freight charges</span>
|
||||
<span class="amount">${formatCurrency(totals.freight_charges)}</span>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user