refactor: update quotation structure and endpoint
This commit is contained in:
parent
4f4b8df9a3
commit
71ffd895f6
3 changed files with 262 additions and 363 deletions
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the `QuotationService` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `QuotationServiceWork` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `QuotationServiceWorkProduct` table. If the table is not empty, all the data it contains will be lost.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "QuotationService" DROP CONSTRAINT "QuotationService_quotationId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "QuotationService" DROP CONSTRAINT "QuotationService_refServiceId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "QuotationServiceWork" DROP CONSTRAINT "QuotationServiceWork_serviceId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "QuotationServiceWorkProduct" DROP CONSTRAINT "QuotationServiceWorkProduct_productId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "QuotationServiceWorkProduct" DROP CONSTRAINT "QuotationServiceWorkProduct_workId_fkey";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "QuotationService";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "QuotationServiceWork";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "QuotationServiceWorkProduct";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "QuotationProductServiceList" (
|
||||
"id" TEXT NOT NULL,
|
||||
"quotationId" TEXT NOT NULL,
|
||||
"order" INTEGER NOT NULL,
|
||||
"vat" DOUBLE PRECISION NOT NULL,
|
||||
"amount" INTEGER NOT NULL,
|
||||
"discount" DOUBLE PRECISION NOT NULL,
|
||||
"pricePerUnit" DOUBLE PRECISION NOT NULL,
|
||||
"productId" TEXT NOT NULL,
|
||||
"workId" TEXT,
|
||||
"serviceId" TEXT,
|
||||
|
||||
CONSTRAINT "QuotationProductServiceList_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "QuotationProductServiceList" ADD CONSTRAINT "QuotationProductServiceList_quotationId_fkey" FOREIGN KEY ("quotationId") REFERENCES "Quotation"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "QuotationProductServiceList" ADD CONSTRAINT "QuotationProductServiceList_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "QuotationProductServiceList" ADD CONSTRAINT "QuotationProductServiceList_workId_fkey" FOREIGN KEY ("workId") REFERENCES "Work"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "QuotationProductServiceList" ADD CONSTRAINT "QuotationProductServiceList_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
|
@ -931,7 +931,7 @@ model Product {
|
|||
productGroupId String
|
||||
|
||||
workProduct WorkProduct[]
|
||||
quotationServiceWorkProduct QuotationServiceWorkProduct[]
|
||||
quotationProductServiceList QuotationProductServiceList[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "ProductCreatedByUser", fields: [createdByUserId], references: [id], onDelete: SetNull)
|
||||
|
|
@ -955,8 +955,8 @@ model Service {
|
|||
shared Boolean @default(false)
|
||||
selectedImage String?
|
||||
|
||||
work Work[]
|
||||
quotationService QuotationService[]
|
||||
work Work[]
|
||||
quotationProductServiceList QuotationProductServiceList[]
|
||||
|
||||
productGroup ProductGroup @relation(fields: [productGroupId], references: [id], onDelete: Cascade)
|
||||
productGroupId String
|
||||
|
|
@ -989,7 +989,8 @@ model Work {
|
|||
updatedBy User? @relation(name: "WorkUpdatedByUser", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
||||
updatedByUserId String?
|
||||
|
||||
productOnWork WorkProduct[]
|
||||
productOnWork WorkProduct[]
|
||||
quotationProductServiceList QuotationProductServiceList[]
|
||||
}
|
||||
|
||||
model WorkProduct {
|
||||
|
|
@ -1045,10 +1046,10 @@ model Quotation {
|
|||
workerCount Int
|
||||
worker QuotationWorker[]
|
||||
|
||||
service QuotationService[]
|
||||
|
||||
urgent Boolean @default(false)
|
||||
|
||||
productServiceList QuotationProductServiceList[]
|
||||
|
||||
totalPrice Float
|
||||
totalDiscount Float
|
||||
vat Float
|
||||
|
|
@ -1084,47 +1085,24 @@ model QuotationWorker {
|
|||
quotationId String
|
||||
}
|
||||
|
||||
model QuotationService {
|
||||
id String @id @default(cuid())
|
||||
|
||||
code String
|
||||
name String
|
||||
detail String
|
||||
attributes Json?
|
||||
|
||||
work QuotationServiceWork[]
|
||||
|
||||
refServiceId String
|
||||
refService Service @relation(fields: [refServiceId], references: [id])
|
||||
|
||||
quotation Quotation @relation(fields: [quotationId], references: [id], onDelete: Cascade)
|
||||
model QuotationProductServiceList {
|
||||
id String @id @default(cuid())
|
||||
quotationId String
|
||||
}
|
||||
quotation Quotation @relation(fields: [quotationId], references: [id])
|
||||
|
||||
model QuotationServiceWork {
|
||||
id String @id @default(cuid())
|
||||
|
||||
order Int
|
||||
name String
|
||||
attributes Json?
|
||||
|
||||
service QuotationService @relation(fields: [serviceId], references: [id], onDelete: Cascade)
|
||||
serviceId String
|
||||
|
||||
productOnWork QuotationServiceWorkProduct[]
|
||||
}
|
||||
|
||||
model QuotationServiceWorkProduct {
|
||||
order Int
|
||||
work QuotationServiceWork @relation(fields: [workId], references: [id], onDelete: Cascade)
|
||||
workId String
|
||||
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
||||
productId String
|
||||
order Int
|
||||
|
||||
vat Float
|
||||
amount Int
|
||||
discount Float
|
||||
pricePerUnit Float
|
||||
|
||||
@@id([workId, productId])
|
||||
productId String
|
||||
product Product @relation(fields: [productId], references: [id])
|
||||
|
||||
workId String?
|
||||
work Work? @relation(fields: [workId], references: [id])
|
||||
|
||||
serviceId String?
|
||||
service Service? @relation(fields: [serviceId], references: [id])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,28 +64,22 @@ type QuotationCreate = {
|
|||
|
||||
urgent?: boolean;
|
||||
|
||||
service: {
|
||||
id: string;
|
||||
// Other fields will come from original data
|
||||
work: {
|
||||
id: string;
|
||||
// Name field will come from original data
|
||||
excluded?: boolean;
|
||||
product: {
|
||||
id: string;
|
||||
amount: number;
|
||||
/**
|
||||
* @maximum 1
|
||||
* @minimum 0
|
||||
*/
|
||||
discount: number;
|
||||
/**
|
||||
* @maximum 1
|
||||
* @minimum 0
|
||||
*/
|
||||
vat?: number;
|
||||
}[];
|
||||
}[];
|
||||
productServiceList: {
|
||||
serviceId?: string;
|
||||
workId?: string;
|
||||
productId: string;
|
||||
amount: number;
|
||||
/**
|
||||
* @maximum 1
|
||||
* @minimum 0
|
||||
*/
|
||||
discount: number;
|
||||
pricePerUnit?: number;
|
||||
/**
|
||||
* @maximum 1
|
||||
* @minimum 0
|
||||
*/
|
||||
vat?: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
|
|
@ -129,31 +123,22 @@ type QuotationUpdate = {
|
|||
|
||||
urgent?: boolean;
|
||||
|
||||
service?: {
|
||||
id: string;
|
||||
// Other fields will come from original data
|
||||
work: {
|
||||
id: string;
|
||||
excluded?: boolean;
|
||||
// Name field will come from original data
|
||||
product: {
|
||||
id: string;
|
||||
/**
|
||||
* @isInt
|
||||
*/
|
||||
amount: number;
|
||||
/**
|
||||
* @maximum 1
|
||||
* @minimum 0
|
||||
*/
|
||||
discount: number;
|
||||
/**
|
||||
* @maximum 1
|
||||
* @minimum 0
|
||||
*/
|
||||
vat: number;
|
||||
}[];
|
||||
}[];
|
||||
productServiceList?: {
|
||||
serviceId?: string;
|
||||
workId?: string;
|
||||
productId: string;
|
||||
amount: number;
|
||||
/**
|
||||
* @maximum 1
|
||||
* @minimum 0
|
||||
*/
|
||||
discount: number;
|
||||
pricePerUnit?: number;
|
||||
/**
|
||||
* @maximum 1
|
||||
* @minimum 0
|
||||
*/
|
||||
vat?: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
|
|
@ -202,14 +187,11 @@ export class QuotationController extends Controller {
|
|||
include: {
|
||||
customerBranch: true,
|
||||
worker: true,
|
||||
service: {
|
||||
productServiceList: {
|
||||
include: {
|
||||
_count: { select: { work: true } },
|
||||
work: {
|
||||
include: {
|
||||
_count: { select: { productOnWork: true } },
|
||||
},
|
||||
},
|
||||
product: true,
|
||||
work: true,
|
||||
service: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -231,17 +213,11 @@ export class QuotationController extends Controller {
|
|||
worker: {
|
||||
include: { employee: true },
|
||||
},
|
||||
service: {
|
||||
productServiceList: {
|
||||
include: {
|
||||
_count: { select: { work: true } },
|
||||
work: {
|
||||
include: {
|
||||
_count: { select: { productOnWork: true } },
|
||||
productOnWork: {
|
||||
include: { product: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
product: true,
|
||||
work: true,
|
||||
service: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -256,45 +232,49 @@ export class QuotationController extends Controller {
|
|||
@Post()
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async createQuotation(@Request() req: RequestWithUser, @Body() body: QuotationCreate) {
|
||||
const existingEmployee = body.worker.filter((v) => typeof v === "string");
|
||||
const serviceIdList = body.service.map((v) => v.id);
|
||||
const productIdList = body.service.flatMap((a) =>
|
||||
a.work.flatMap((b) => b.product.map((c) => c.id)),
|
||||
);
|
||||
const ids = {
|
||||
employee: body.worker.filter((v) => typeof v === "string"),
|
||||
product: body.productServiceList
|
||||
.map((v) => v.productId)
|
||||
.filter((v, i, a) => a.findIndex((c) => c === v) === i),
|
||||
work: body.productServiceList.map((v) => v.workId || []).flat(),
|
||||
service: body.productServiceList.map((v) => v.serviceId || []).flat(),
|
||||
};
|
||||
|
||||
const [customerBranch, employee, service, product] = await prisma.$transaction([
|
||||
prisma.customerBranch.findUnique({
|
||||
include: {
|
||||
customer: {
|
||||
const [customerBranch, employee, product, work, service] = await prisma.$transaction(
|
||||
async (tx) =>
|
||||
await Promise.all([
|
||||
tx.customerBranch.findUnique({
|
||||
include: {
|
||||
registeredBranch: {
|
||||
include: branchRelationPermInclude(req.user),
|
||||
customer: {
|
||||
include: {
|
||||
registeredBranch: {
|
||||
include: branchRelationPermInclude(req.user),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: { id: body.customerBranchId },
|
||||
}),
|
||||
prisma.employee.findMany({
|
||||
where: { id: { in: existingEmployee } },
|
||||
}),
|
||||
prisma.service.findMany({
|
||||
include: { work: true },
|
||||
where: { id: { in: serviceIdList } },
|
||||
}),
|
||||
prisma.product.findMany({
|
||||
where: { id: { in: productIdList } },
|
||||
}),
|
||||
]);
|
||||
where: { id: body.customerBranchId },
|
||||
}),
|
||||
tx.employee.findMany({ where: { id: { in: ids.employee } } }),
|
||||
tx.product.findMany({ where: { id: { in: ids.product } } }),
|
||||
ids.work.length ? tx.work.findMany({ where: { id: { in: ids.work } } }) : null,
|
||||
ids.service.length ? tx.service.findMany({ where: { id: { in: ids.service } } }) : null,
|
||||
]),
|
||||
);
|
||||
|
||||
if (serviceIdList.length !== service.length) throw relationError("Service");
|
||||
if (productIdList.length !== product.length) throw relationError("Product");
|
||||
if (existingEmployee.length !== employee.length) throw relationError("Worker");
|
||||
if (!customerBranch) throw relationError("Customer Branch");
|
||||
|
||||
if (ids.employee.length !== employee.length) throw relationError("Worker");
|
||||
if (ids.product.length !== product.length) throw relationError("Product");
|
||||
if (ids.work.length && ids.work.length !== work?.length) throw relationError("Work");
|
||||
if (ids.service.length && ids.service.length !== service?.length) {
|
||||
throw relationError("Service");
|
||||
}
|
||||
|
||||
await permissionCheck(req.user, customerBranch.customer.registeredBranch);
|
||||
|
||||
const { service: _service, worker: _worker, ...rest } = body;
|
||||
const { productServiceList: _productServiceList, worker: _worker, ...rest } = body;
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const nonExistEmployee = body.worker.filter((v) => typeof v !== "string");
|
||||
|
|
@ -330,58 +310,6 @@ export class QuotationController extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
const price = { totalPrice: 0, totalDiscount: 0, totalVat: 0 };
|
||||
|
||||
const restructureService = body.service.flatMap((a) => {
|
||||
const currentService = service.find((b) => b.id === a.id);
|
||||
|
||||
if (!currentService) return []; // should not possible
|
||||
|
||||
return {
|
||||
id: currentService.id,
|
||||
name: currentService.name,
|
||||
code: currentService.code,
|
||||
detail: currentService.detail,
|
||||
attributes: currentService.attributes as Prisma.JsonObject,
|
||||
work: a.work.flatMap((c) => {
|
||||
if (c.excluded) return [];
|
||||
|
||||
const currentWork = currentService.work.find((d) => d.id === c.id);
|
||||
|
||||
if (!currentWork) return []; // additional will get stripped
|
||||
|
||||
return {
|
||||
id: currentWork.id,
|
||||
order: currentWork.order,
|
||||
name: currentWork.name,
|
||||
attributes: currentWork.attributes as Prisma.JsonObject,
|
||||
product: c.product.flatMap((e) => {
|
||||
const currentProduct = product.find((f) => f.id === e.id);
|
||||
|
||||
if (!currentProduct) return []; // should not possible
|
||||
|
||||
price.totalPrice += currentProduct.price * e.amount;
|
||||
price.totalDiscount +=
|
||||
Math.round(currentProduct.price * e.amount * e.discount * 100) / 100;
|
||||
price.totalVat +=
|
||||
Math.round(
|
||||
(currentProduct.price * e.amount -
|
||||
currentProduct.price * e.amount * e.discount) *
|
||||
(e.vat === undefined ? VAT_DEFAULT : e.vat) *
|
||||
100,
|
||||
) / 100;
|
||||
|
||||
return {
|
||||
...e,
|
||||
vat: e.vat === undefined ? VAT_DEFAULT : e.vat,
|
||||
pricePerUnit: currentProduct.price,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const currentMonth = new Date().getMonth() + 1;
|
||||
const currentDate = new Date().getDate();
|
||||
|
|
@ -397,17 +325,46 @@ export class QuotationController extends Controller {
|
|||
update: { value: { increment: 1 } },
|
||||
});
|
||||
|
||||
const list = body.productServiceList.map((v, i) => ({
|
||||
order: i + 1,
|
||||
productId: v.productId,
|
||||
workId: v.workId,
|
||||
serviceId: v.serviceId,
|
||||
pricePerUnit: v.pricePerUnit || product.find((p) => p.id === v.productId)?.price || 0,
|
||||
amount: v.amount,
|
||||
discount: v.discount,
|
||||
vat: v.vat || VAT_DEFAULT,
|
||||
}));
|
||||
|
||||
const price = list.reduce(
|
||||
(a, c) => {
|
||||
const price = c.pricePerUnit * c.amount;
|
||||
const discount = price * c.discount;
|
||||
const vat = (price - discount) * c.vat;
|
||||
|
||||
a.totalPrice += price;
|
||||
a.totalDiscount += discount;
|
||||
a.vat += vat;
|
||||
a.finalPrice += price - discount + vat;
|
||||
|
||||
return a;
|
||||
},
|
||||
{
|
||||
totalPrice: 0,
|
||||
totalDiscount: 0,
|
||||
vat: 0,
|
||||
vatExcluded: 0,
|
||||
finalPrice: 0,
|
||||
},
|
||||
);
|
||||
|
||||
return await tx.quotation.create({
|
||||
include: {
|
||||
service: {
|
||||
productServiceList: {
|
||||
include: {
|
||||
work: {
|
||||
include: {
|
||||
productOnWork: {
|
||||
include: { product: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
product: true,
|
||||
work: true,
|
||||
service: true,
|
||||
},
|
||||
},
|
||||
paySplit: true,
|
||||
|
|
@ -416,12 +373,13 @@ export class QuotationController extends Controller {
|
|||
include: { customer: true },
|
||||
},
|
||||
_count: {
|
||||
select: { service: true },
|
||||
select: { productServiceList: true },
|
||||
},
|
||||
},
|
||||
|
||||
data: {
|
||||
...rest,
|
||||
...price,
|
||||
statusOrder: +(rest.status === "INACTIVE"),
|
||||
code: `${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${currentDate.toString().padStart(2, "0")}${lastQuotation.value.toString().padStart(4, "0")}`,
|
||||
worker: {
|
||||
|
|
@ -433,11 +391,6 @@ export class QuotationController extends Controller {
|
|||
})),
|
||||
},
|
||||
},
|
||||
totalPrice: price.totalPrice,
|
||||
totalDiscount: price.totalDiscount,
|
||||
vat: price.totalVat,
|
||||
vatExcluded: 0,
|
||||
finalPrice: price.totalPrice - price.totalDiscount,
|
||||
paySplit: {
|
||||
createMany: {
|
||||
data: (rest.paySplit || []).map((v, i) => ({
|
||||
|
|
@ -446,34 +399,7 @@ export class QuotationController extends Controller {
|
|||
})),
|
||||
},
|
||||
},
|
||||
service: {
|
||||
create: restructureService.map((a) => ({
|
||||
code: a.code,
|
||||
name: a.name,
|
||||
detail: a.detail,
|
||||
attributes: a.attributes,
|
||||
refServiceId: a.id,
|
||||
work: {
|
||||
create: a.work.map((b) => ({
|
||||
order: b.order,
|
||||
name: b.name,
|
||||
attributes: b.attributes,
|
||||
productOnWork: {
|
||||
createMany: {
|
||||
data: b.product.map((v, i) => ({
|
||||
productId: v.id,
|
||||
order: i + 1,
|
||||
vat: v.vat,
|
||||
amount: v.amount,
|
||||
discount: v.discount,
|
||||
pricePerUnit: v.pricePerUnit,
|
||||
})),
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
})),
|
||||
},
|
||||
productServiceList: { create: list },
|
||||
createdByUserId: req.user.sub,
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
|
|
@ -505,57 +431,53 @@ export class QuotationController extends Controller {
|
|||
|
||||
if (!record) throw notFoundError("Quotation");
|
||||
|
||||
const existingEmployee = body.worker?.filter((v) => typeof v === "string");
|
||||
const serviceIdList = body.service?.map((v) => v.id);
|
||||
const productIdList = body.service?.flatMap((a) =>
|
||||
a.work.flatMap((b) => b.product.map((c) => c.id)),
|
||||
);
|
||||
const ids = {
|
||||
employee: body.worker?.filter((v) => typeof v === "string"),
|
||||
product: body.productServiceList
|
||||
?.map((v) => v.productId)
|
||||
.filter((v, i, a) => a.findIndex((c) => c === v) === i),
|
||||
work: body.productServiceList?.map((v) => v.workId || []).flat(),
|
||||
service: body.productServiceList?.map((v) => v.serviceId || []).flat(),
|
||||
};
|
||||
|
||||
const [customerBranch, employee, service, product] = await prisma.$transaction(
|
||||
const [customerBranch, employee, product, work, service] = await prisma.$transaction(
|
||||
async (tx) =>
|
||||
await Promise.all([
|
||||
body.customerBranchId
|
||||
? tx.customerBranch.findFirst({
|
||||
tx.customerBranch.findUnique({
|
||||
include: {
|
||||
customer: {
|
||||
include: {
|
||||
customer: {
|
||||
include: {
|
||||
registeredBranch: { include: branchRelationPermInclude(req.user) },
|
||||
},
|
||||
registeredBranch: {
|
||||
include: branchRelationPermInclude(req.user),
|
||||
},
|
||||
},
|
||||
where: { id: body.customerBranchId },
|
||||
})
|
||||
: null,
|
||||
body.worker
|
||||
? tx.employee.findMany({
|
||||
where: { id: { in: existingEmployee } },
|
||||
})
|
||||
: null,
|
||||
body.service
|
||||
? tx.service.findMany({
|
||||
include: { work: true },
|
||||
where: { id: { in: serviceIdList } },
|
||||
})
|
||||
: null,
|
||||
body.service
|
||||
? tx.product.findMany({
|
||||
where: { id: { in: productIdList } },
|
||||
})
|
||||
: null,
|
||||
},
|
||||
},
|
||||
where: { id: body.customerBranchId },
|
||||
}),
|
||||
tx.employee.findMany({ where: { id: { in: ids.employee } } }),
|
||||
tx.product.findMany({ where: { id: { in: ids.product } } }),
|
||||
ids.work?.length ? tx.work.findMany({ where: { id: { in: ids.work } } }) : null,
|
||||
ids.service?.length ? tx.service.findMany({ where: { id: { in: ids.service } } }) : null,
|
||||
]),
|
||||
);
|
||||
|
||||
if (serviceIdList?.length !== service?.length) throw relationError("Service");
|
||||
if (productIdList?.length !== product?.length) throw relationError("Product");
|
||||
if (existingEmployee?.length !== employee?.length) throw relationError("Worker");
|
||||
if (body.customerBranchId && !customerBranch) throw relationError("Customer Branch");
|
||||
if (ids.employee && ids.employee.length !== employee.length) throw relationError("Worker");
|
||||
if (ids.product && ids.product.length !== product.length) throw relationError("Product");
|
||||
if (ids.work && ids.work.length && ids.work.length !== work?.length) {
|
||||
throw relationError("Work");
|
||||
}
|
||||
if (ids.service && ids.service.length && ids.service.length !== service?.length) {
|
||||
throw relationError("Service");
|
||||
}
|
||||
|
||||
await permissionCheck(req.user, record.customerBranch.customer.registeredBranch);
|
||||
if (customerBranch && record.customerBranchId !== body.customerBranchId) {
|
||||
await permissionCheck(req.user, customerBranch.customer.registeredBranch);
|
||||
}
|
||||
|
||||
const { service: _service, worker: _worker, ...rest } = body;
|
||||
const { productServiceList: _productServiceList, worker: _worker, ...rest } = body;
|
||||
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const sortedEmployeeId: string[] = [];
|
||||
|
|
@ -597,69 +519,46 @@ export class QuotationController extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
const price = { totalPrice: 0, totalDiscount: 0, totalVat: 0 };
|
||||
const list = body.productServiceList?.map((v, i) => ({
|
||||
order: i + 1,
|
||||
productId: v.productId,
|
||||
workId: v.workId,
|
||||
serviceId: v.serviceId,
|
||||
pricePerUnit: v.pricePerUnit || product.find((p) => p.id === v.productId)?.price || 0,
|
||||
amount: v.amount,
|
||||
discount: v.discount,
|
||||
vat: v.vat || VAT_DEFAULT,
|
||||
}));
|
||||
|
||||
const restructureService = body.service?.flatMap((a) => {
|
||||
const currentService = service?.find((b) => b.id === a.id);
|
||||
const price = list?.reduce(
|
||||
(a, c) => {
|
||||
const price = c.pricePerUnit * c.amount;
|
||||
const discount = price * c.discount;
|
||||
const vat = price - discount * c.vat;
|
||||
|
||||
if (!currentService) return []; // should not possible
|
||||
a.totalPrice += price;
|
||||
a.totalDiscount += discount;
|
||||
a.vat += vat;
|
||||
a.finalPrice += price - discount + vat;
|
||||
|
||||
return {
|
||||
id: currentService.id,
|
||||
name: currentService.name,
|
||||
code: currentService.code,
|
||||
detail: currentService.detail,
|
||||
attributes: currentService.attributes as Prisma.JsonObject,
|
||||
work: a.work.flatMap((c) => {
|
||||
if (c.excluded) return [];
|
||||
|
||||
const currentWork = currentService.work.find((d) => d.id === c.id);
|
||||
|
||||
if (!currentWork) return []; // additional will get stripped
|
||||
|
||||
return {
|
||||
id: currentWork.id,
|
||||
order: currentWork.order,
|
||||
name: currentWork.name,
|
||||
attributes: currentWork.attributes as Prisma.JsonObject,
|
||||
product: c.product.flatMap((e) => {
|
||||
const currentProduct = product?.find((f) => f.id === e.id);
|
||||
|
||||
if (!currentProduct) return []; // should not possible
|
||||
|
||||
price.totalPrice += currentProduct.price * e.amount;
|
||||
price.totalDiscount +=
|
||||
Math.round(currentProduct.price * e.amount * e.discount * 100) / 100;
|
||||
price.totalVat +=
|
||||
Math.round(
|
||||
(currentProduct.price * e.amount -
|
||||
currentProduct.price * e.amount * e.discount) *
|
||||
(e.vat === undefined ? VAT_DEFAULT : e.vat) *
|
||||
100,
|
||||
) / 100;
|
||||
|
||||
return {
|
||||
...e,
|
||||
vat: e.vat === undefined ? VAT_DEFAULT : e.vat,
|
||||
pricePerUnit: currentProduct.price,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
return a;
|
||||
},
|
||||
{
|
||||
totalPrice: 0,
|
||||
totalDiscount: 0,
|
||||
vat: 0,
|
||||
vatExcluded: 0,
|
||||
finalPrice: 0,
|
||||
},
|
||||
);
|
||||
|
||||
return await tx.quotation.update({
|
||||
include: {
|
||||
service: {
|
||||
productServiceList: {
|
||||
include: {
|
||||
work: {
|
||||
include: {
|
||||
productOnWork: {
|
||||
include: { product: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
product: true,
|
||||
work: true,
|
||||
service: true,
|
||||
},
|
||||
},
|
||||
paySplit: true,
|
||||
|
|
@ -668,12 +567,13 @@ export class QuotationController extends Controller {
|
|||
include: { customer: true },
|
||||
},
|
||||
_count: {
|
||||
select: { service: true },
|
||||
select: { productServiceList: true },
|
||||
},
|
||||
},
|
||||
where: { id: quotationId },
|
||||
data: {
|
||||
...rest,
|
||||
...price,
|
||||
statusOrder: +(rest.status === "INACTIVE"),
|
||||
worker:
|
||||
sortedEmployeeId.length > 0
|
||||
|
|
@ -689,14 +589,6 @@ export class QuotationController extends Controller {
|
|||
},
|
||||
}
|
||||
: undefined,
|
||||
totalPrice: body.service ? price.totalPrice : undefined,
|
||||
totalDiscount: body.service ? price.totalDiscount : undefined,
|
||||
|
||||
vat: body.service ? price.totalVat : undefined,
|
||||
vatExcluded: body.service ? 0 : undefined,
|
||||
|
||||
finalPrice: body.service ? price.totalPrice - price.totalDiscount : undefined,
|
||||
|
||||
paySplit: rest.paySplit
|
||||
? {
|
||||
deleteMany: {},
|
||||
|
|
@ -708,40 +600,10 @@ export class QuotationController extends Controller {
|
|||
},
|
||||
}
|
||||
: undefined,
|
||||
|
||||
service:
|
||||
body.service && restructureService
|
||||
? {
|
||||
deleteMany: {},
|
||||
create: restructureService.map((a) => ({
|
||||
code: a.code,
|
||||
name: a.name,
|
||||
detail: a.detail,
|
||||
attributes: a.attributes,
|
||||
refServiceId: a.id,
|
||||
work: {
|
||||
create: a.work.map((b) => ({
|
||||
order: b.order,
|
||||
name: b.name,
|
||||
attributes: b.attributes,
|
||||
productOnWork: {
|
||||
createMany: {
|
||||
data: b.product.map((v, i) => ({
|
||||
productId: v.id,
|
||||
order: i + 1,
|
||||
vat: v.vat,
|
||||
amount: v.amount,
|
||||
discount: v.discount,
|
||||
pricePerUnit: v.pricePerUnit,
|
||||
})),
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
|
||||
productServiceList: {
|
||||
deleteMany: {},
|
||||
create: list,
|
||||
},
|
||||
updatedByUserId: req.user.sub,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue