Skip to content

Commit 1d461f6

Browse files
authored
Merge pull request #3989 from Northeastern-Electric-Racing/feature/bom-improvements
Feature/bom improvements
2 parents f56c047 + f4ef024 commit 1d461f6

34 files changed

Lines changed: 3590 additions & 1381 deletions

src/backend/src/controllers/projects.controllers.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -216,17 +216,17 @@ export default class ProjectsController {
216216
name,
217217
status,
218218
materialTypeName,
219+
linkUrl,
220+
wbsNum,
221+
req.organization,
219222
manufacturerName,
220223
manufacturerPartNumber,
221224
quantity,
222225
price,
223226
subtotal,
224-
linkUrl,
225-
wbsNum,
226-
req.organization,
227227
notes,
228228
assemblyId,
229-
pdmFileName === '' ? undefined : pdmFileName,
229+
pdmFileName,
230230
unitName,
231231
reimbursementRequestId
232232
);
@@ -236,6 +236,22 @@ export default class ProjectsController {
236236
}
237237
}
238238

239+
static async copyMaterialsToProject(req: Request, res: Response, next: NextFunction) {
240+
try {
241+
const { materialIds, destinationWbsNum } = req.body;
242+
243+
const newMaterialIds = await BillOfMaterialsService.copyMaterialsToProject(
244+
req.currentUser,
245+
materialIds,
246+
destinationWbsNum,
247+
req.organization
248+
);
249+
res.status(200).json(newMaterialIds);
250+
} catch (error: unknown) {
251+
next(error);
252+
}
253+
}
254+
239255
static async createManufacturer(req: Request, res: Response, next: NextFunction) {
240256
try {
241257
const { name } = req.body;
@@ -379,17 +395,17 @@ export default class ProjectsController {
379395
name,
380396
status,
381397
materialTypeName,
398+
linkUrl,
399+
req.organization,
382400
manufacturerName,
383401
manufacturerPartNumber,
384402
quantity,
385403
price,
386404
subtotal,
387-
linkUrl,
388-
req.organization,
389405
notes,
390406
unitName,
391407
assemblyId,
392-
pdmFileName === '' ? undefined : pdmFileName,
408+
pdmFileName,
393409
reimbursementRequestId
394410
);
395411
res.status(200).json(updatedMaterial);

src/backend/src/prisma-query-args/bom.query-args.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const getMaterialQueryArgs = (organizationId: string) =>
2525
materialType: true,
2626
unit: true,
2727
manufacturer: true,
28+
reimbursementProducts: false,
2829
reimbursementRequest: getReimbursementRequestQueryArgs(organizationId)
2930
}
3031
});
@@ -37,6 +38,7 @@ export const getMaterialPreviewQueryArgs = (organizationId: string) =>
3738
unit: true,
3839
manufacturer: true,
3940
materialType: true,
41+
reimbursementProducts: false,
4042
reimbursementRequest: getReimbursementRequestQueryArgs(organizationId)
4143
}
4244
});

src/backend/src/prisma-query-args/reimbursement-products.query-args.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const getReimbursementProductQueryArgs = (organizationId: string) =>
2525
Prisma.validator<Prisma.Reimbursement_ProductDefaultArgs>()({
2626
include: {
2727
refundSources: getRefundSourceQueryArgs(organizationId),
28-
reimbursementProductReason: getReimbursementProductReasonQueryArgs(organizationId)
28+
reimbursementProductReason: getReimbursementProductReasonQueryArgs(organizationId),
29+
material: true
2930
}
3031
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
-- DropForeignKey
2+
ALTER TABLE "Material" DROP CONSTRAINT "Material_manufacturerId_fkey";
3+
4+
-- AlterTable
5+
ALTER TABLE "Material" ALTER COLUMN "manufacturerId" DROP NOT NULL,
6+
ALTER COLUMN "manufacturerPartNumber" DROP NOT NULL,
7+
ALTER COLUMN "quantity" DROP NOT NULL,
8+
ALTER COLUMN "price" DROP NOT NULL,
9+
ALTER COLUMN "subtotal" DROP NOT NULL;
10+
11+
-- AlterTable
12+
ALTER TABLE "Reimbursement_Product" ALTER COLUMN "name" DROP NOT NULL;
13+
14+
-- AlterTable
15+
ALTER TABLE "Reimbursement_Product" ADD COLUMN "materialId" TEXT;
16+
17+
-- CreateIndex
18+
CREATE INDEX "Reimbursement_Product_materialId_idx" ON "Reimbursement_Product"("materialId");
19+
20+
-- AddForeignKey
21+
ALTER TABLE "Reimbursement_Product" ADD CONSTRAINT "Reimbursement_Product_materialId_fkey" FOREIGN KEY ("materialId") REFERENCES "Material"("materialId") ON DELETE SET NULL ON UPDATE CASCADE;
22+
23+
-- AddForeignKey
24+
ALTER TABLE "Material" ADD CONSTRAINT "Material_manufacturerId_fkey" FOREIGN KEY ("manufacturerId") REFERENCES "Manufacturer"("id") ON DELETE SET NULL ON UPDATE CASCADE;
25+
26+
DO $$
27+
DECLARE
28+
material_record RECORD;
29+
new_reason_id TEXT;
30+
BEGIN
31+
-- Loop through all materials that are linked to RRs but don't have products yet
32+
FOR material_record IN
33+
SELECT
34+
m."materialId",
35+
m."name",
36+
m."subtotal",
37+
m."wbsElementId",
38+
m."reimbursementRequestId"
39+
FROM "Material" m
40+
WHERE m."reimbursementRequestId" IS NOT NULL
41+
AND m."dateDeleted" IS NULL
42+
AND EXISTS (
43+
-- Only migrate if the RR isn't deleted
44+
SELECT 1 FROM "Reimbursement_Request" rr
45+
WHERE rr."reimbursementRequestId" = m."reimbursementRequestId"
46+
AND rr."dateDeleted" IS NULL
47+
)
48+
AND EXISTS (
49+
-- Only migrate if the WBS element isn't deleted
50+
SELECT 1 FROM "WBS_Element" wbs
51+
WHERE wbs."wbsElementId" = m."wbsElementId"
52+
AND wbs."dateDeleted" IS NULL
53+
)
54+
AND NOT EXISTS (
55+
-- Skip if a product already links to this material for this RR
56+
SELECT 1 FROM "Reimbursement_Product" rp
57+
WHERE rp."materialId" = m."materialId"
58+
AND rp."reimbursementRequestId" = m."reimbursementRequestId"
59+
)
60+
LOOP
61+
-- Create a new reason for this material (can't reuse due to @unique and one to one relation)
62+
INSERT INTO "Reimbursement_Product_Reason" (
63+
"reimbursementProductReasonId",
64+
"wbsElementId"
65+
) VALUES (
66+
gen_random_uuid(),
67+
material_record."wbsElementId"
68+
)
69+
RETURNING "reimbursementProductReasonId" INTO new_reason_id;
70+
71+
-- Create the product linked to the material
72+
INSERT INTO "Reimbursement_Product" (
73+
"reimbursementProductId",
74+
"name",
75+
"cost",
76+
"materialId",
77+
"reimbursementProductReasonId",
78+
"reimbursementRequestId"
79+
) VALUES (
80+
gen_random_uuid(),
81+
material_record."name",
82+
COALESCE(material_record."subtotal", 0),
83+
material_record."materialId",
84+
new_reason_id,
85+
material_record."reimbursementRequestId"
86+
);
87+
END LOOP;
88+
END $$;

src/backend/src/prisma/schema.prisma

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -767,9 +767,11 @@ model Reimbursement_Product_Reason {
767767

768768
model Reimbursement_Product {
769769
reimbursementProductId String @id @default(uuid())
770-
name String
770+
name String?
771771
dateDeleted DateTime?
772772
cost Int
773+
material Material? @relation(fields: [materialId], references: [materialId])
774+
materialId String?
773775
reimbursementProductReasonId String @unique
774776
reimbursementProductReason Reimbursement_Product_Reason @relation(fields: [reimbursementProductReasonId], references: [reimbursementProductReasonId])
775777
reimbursementRequestId String
@@ -778,6 +780,7 @@ model Reimbursement_Product {
778780
779781
@@index([reimbursementRequestId])
780782
@@index([reimbursementProductReasonId])
783+
@@index([materialId])
781784
}
782785

783786
model Refund_Source {
@@ -970,34 +973,35 @@ model Assembly {
970973
}
971974

972975
model Material {
973-
materialId String @id @default(uuid())
974-
assembly Assembly? @relation(fields: [assemblyId], references: [assemblyId])
976+
materialId String @id @default(uuid())
977+
assembly Assembly? @relation(fields: [assemblyId], references: [assemblyId])
975978
assemblyId String?
976979
name String
977-
wbsElement WBS_Element @relation(fields: [wbsElementId], references: [wbsElementId])
980+
wbsElement WBS_Element @relation(fields: [wbsElementId], references: [wbsElementId])
978981
wbsElementId String
979982
dateDeleted DateTime?
980-
userDeleted User? @relation(fields: [userDeletedId], references: [userId], name: "materialDeleter")
983+
userDeleted User? @relation(fields: [userDeletedId], references: [userId], name: "materialDeleter")
981984
userDeletedId String?
982985
dateCreated DateTime
983-
userCreated User @relation(fields: [userCreatedId], references: [userId], name: "materialCreator")
986+
userCreated User @relation(fields: [userCreatedId], references: [userId], name: "materialCreator")
984987
userCreatedId String
985988
status Material_Status
986-
materialType Material_Type @relation(fields: [materialTypeId], references: [id])
989+
materialType Material_Type @relation(fields: [materialTypeId], references: [id])
987990
materialTypeId String
988-
manufacturer Manufacturer @relation(fields: [manufacturerId], references: [id])
989-
manufacturerId String
990-
manufacturerPartNumber String
991+
manufacturer Manufacturer? @relation(fields: [manufacturerId], references: [id])
992+
manufacturerId String?
993+
manufacturerPartNumber String?
991994
pdmFileName String?
992-
quantity Decimal
993-
unit Unit? @relation(fields: [unitId], references: [id])
995+
quantity Decimal?
996+
unit Unit? @relation(fields: [unitId], references: [id])
994997
unitId String?
995-
price Int
996-
subtotal Int
998+
price Int?
999+
subtotal Int?
9971000
linkUrl String
9981001
notes String?
999-
reimbursementRequest Reimbursement_Request? @relation(fields: [reimbursementRequestId], references: [reimbursementRequestId])
1002+
reimbursementRequest Reimbursement_Request? @relation(fields: [reimbursementRequestId], references: [reimbursementRequestId])
10001003
reimbursementRequestId String?
1004+
reimbursementProducts Reimbursement_Product[]
10011005
10021006
@@index([assemblyId])
10031007
@@index([materialTypeId])

src/backend/src/prisma/seed.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2297,58 +2297,66 @@ const performSeed: () => Promise<void> = async () => {
22972297
'10k Resistor',
22982298
MaterialStatus.Ordered,
22992299
'Resistor',
2300-
'Digikey',
2301-
'abcdef',
2302-
new Decimal(20),
2303-
30,
2304-
600,
23052300
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
23062301
{
23072302
carNumber: 0,
23082303
projectNumber: 1,
23092304
workPackageNumber: 0
23102305
},
23112306
ner,
2312-
'Here are some notes'
2307+
'Digikey',
2308+
'abcdef',
2309+
new Decimal(20),
2310+
30,
2311+
600,
2312+
'Here are some notes',
2313+
assembly1.assemblyId,
2314+
undefined,
2315+
undefined,
2316+
undefined
23132317
);
23142318

23152319
await BillOfMaterialsService.createMaterial(
23162320
thomasEmrax,
23172321
'20k Resistor',
23182322
MaterialStatus.Ordered,
23192323
'Resistor',
2320-
'Digikey',
2321-
'bacfed',
2322-
new Decimal(10),
2323-
7,
2324-
70,
23252324
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
23262325
{
23272326
carNumber: 0,
23282327
projectNumber: 1,
23292328
workPackageNumber: 0
23302329
},
23312330
ner,
2332-
'Here are some more notes'
2331+
'Digikey',
2332+
'bacfed',
2333+
new Decimal(10),
2334+
7,
2335+
70,
2336+
'Here are some more notes',
2337+
undefined,
2338+
undefined,
2339+
undefined,
2340+
undefined
23332341
);
23342342

23352343
await BillOfMaterialsService.createMaterial(
23362344
thomasEmrax,
23372345
'100k Resistor',
23382346
MaterialStatus.ReadyToOrder,
23392347
'Resistor',
2340-
'Digikey',
2341-
'lalsd',
2342-
new Decimal(5),
2343-
10,
2344-
50,
23452348
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
23462349
{
23472350
carNumber: 0,
23482351
projectNumber: 1,
23492352
workPackageNumber: 0
23502353
},
23512354
ner,
2355+
'Digikey',
2356+
'lalsd',
2357+
new Decimal(5),
2358+
10,
2359+
50,
23522360
undefined,
23532361
undefined,
23542362
undefined,

src/backend/src/routes/projects.routes.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
validateInputs,
88
materialValidators
99
} from '../utils/validation.utils.js';
10+
import { validateWBS } from 'shared';
1011
import ProjectsController from '../controllers/projects.controllers.js';
1112

1213
const projectRouter = express.Router();
@@ -93,6 +94,16 @@ projectRouter.post(
9394
);
9495
projectRouter.post('/bom/material/:wbsNum/create', ...materialValidators, validateInputs, ProjectsController.createMaterial);
9596
projectRouter.post('/bom/material/:materialId/edit', ...materialValidators, validateInputs, ProjectsController.editMaterial);
97+
projectRouter.post(
98+
'/bom/material/copy',
99+
body('materialIds').isArray({ min: 1 }),
100+
nonEmptyString(body('materialIds.*')),
101+
body('destinationWbsNum').customSanitizer((value) => {
102+
return validateWBS(value);
103+
}),
104+
validateInputs,
105+
ProjectsController.copyMaterialsToProject
106+
);
96107

97108
projectRouter.post(
98109
'/bom/assembly/:assemblyId/edit',

src/backend/src/routes/reimbursement-requests.routes.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
isOptionalDateOnly,
1212
nonEmptyString,
1313
validateInputs,
14-
validateReimbursementProducts
14+
validateReimbursementProducts,
15+
validateReimbursementProductsForEdit
1516
} from '../utils/validation.utils.js';
1617
import ReimbursementRequestController from '../controllers/reimbursement-requests.controllers.js';
1718
import multer, { memoryStorage } from 'multer';
@@ -139,7 +140,7 @@ reimbursementRequestsRouter.post(
139140
nonEmptyString(body('receiptPictures.*.googleFileId')),
140141
nonEmptyString(body('accountCodeId')),
141142
intMinZero(body('totalCost')),
142-
validateReimbursementProducts(),
143+
validateReimbursementProductsForEdit(),
143144
validateInputs,
144145
ReimbursementRequestController.editReimbursementRequest
145146
);

0 commit comments

Comments
 (0)