Merge branch 'develop'
This commit is contained in:
commit
f1d4584d02
17 changed files with 569 additions and 76 deletions
|
|
@ -0,0 +1,5 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "CreditNoteStatus" AS ENUM ('Pending', 'Success');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "CreditNote" ADD COLUMN "creditNoteStatus" "CreditNoteStatus";
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- Made the column `creditNoteStatus` on table `CreditNote` required. This step will fail if there are existing NULL values in that column.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "CreditNote" ALTER COLUMN "creditNoteStatus" SET NOT NULL,
|
||||
ALTER COLUMN "creditNoteStatus" SET DEFAULT 'Pending';
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "CreditNote" ADD COLUMN "value" DOUBLE PRECISION NOT NULL DEFAULT 0;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `code` to the `CreditNote` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "CreditNote" ADD COLUMN "code" TEXT NOT NULL;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "CreditNote" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ADD COLUMN "createdByUserId" TEXT;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CreditNote" ADD CONSTRAINT "CreditNote_createdByUserId_fkey" FOREIGN KEY ("createdByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterEnum
|
||||
ALTER TYPE "TaskStatus" ADD VALUE 'Restart';
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterEnum
|
||||
ALTER TYPE "UserTaskStatus" ADD VALUE 'Restart';
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "UserTask" ADD COLUMN "acceptedAt" TIMESTAMP(3),
|
||||
ADD COLUMN "submittedAt" TIMESTAMP(3);
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "CreditNotePaybackType" AS ENUM ('Cash', 'BankTransfer');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "CreditNote" ADD COLUMN "detail" TEXT,
|
||||
ADD COLUMN "paybackAccount" TEXT,
|
||||
ADD COLUMN "paybackAccountName" TEXT,
|
||||
ADD COLUMN "paybackBank" TEXT,
|
||||
ADD COLUMN "paybackType" "CreditNotePaybackType",
|
||||
ADD COLUMN "reason" TEXT;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "PaybackStatus" AS ENUM ('Pending', 'Verify', 'Done');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "CreditNote" ADD COLUMN "paybackStatus" "PaybackStatus" NOT NULL DEFAULT 'Pending';
|
||||
|
|
@ -482,6 +482,7 @@ model User {
|
|||
notificationReceive Notification[] @relation("NotificationReceiver")
|
||||
notificationRead Notification[]
|
||||
taskOrderCreated TaskOrder[] @relation("TaskOrderCreatedByUser")
|
||||
creditNoteCreated CreditNote[] @relation("CreditNoteCreatedByUser")
|
||||
|
||||
requestWorkStepStatus RequestWorkStepStatus[]
|
||||
userTask UserTask[]
|
||||
|
|
@ -1497,6 +1498,7 @@ enum TaskStatus {
|
|||
|
||||
Success
|
||||
Failed
|
||||
Restart
|
||||
Redo
|
||||
|
||||
Validate
|
||||
|
|
@ -1559,6 +1561,7 @@ model TaskOrder {
|
|||
|
||||
enum UserTaskStatus {
|
||||
Pending // Should not be use but define here for type
|
||||
Restart
|
||||
Accept
|
||||
Submit
|
||||
}
|
||||
|
|
@ -1572,16 +1575,53 @@ model UserTask {
|
|||
taskOrderId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
|
||||
acceptedAt DateTime?
|
||||
submittedAt DateTime?
|
||||
}
|
||||
|
||||
enum CreditNoteStatus {
|
||||
Pending
|
||||
Success
|
||||
}
|
||||
|
||||
enum CreditNotePaybackType {
|
||||
Cash
|
||||
BankTransfer
|
||||
}
|
||||
|
||||
enum PaybackStatus {
|
||||
Pending
|
||||
Verify
|
||||
Done
|
||||
}
|
||||
|
||||
model CreditNote {
|
||||
id String @id @default(cuid())
|
||||
|
||||
code String
|
||||
|
||||
creditNoteStatus CreditNoteStatus @default(Pending)
|
||||
|
||||
value Float @default(0)
|
||||
reason String?
|
||||
detail String?
|
||||
|
||||
paybackType CreditNotePaybackType?
|
||||
paybackBank String?
|
||||
paybackAccount String?
|
||||
paybackAccountName String?
|
||||
paybackStatus PaybackStatus @default(Pending)
|
||||
|
||||
quotation Quotation @relation(fields: [quotationId], references: [id], onDelete: Cascade)
|
||||
quotationId String
|
||||
|
||||
// NOTE: only status cancel
|
||||
requestWork RequestWork[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
createdBy User? @relation(name: "CreditNoteCreatedByUser", fields: [createdByUserId], references: [id])
|
||||
createdByUserId String?
|
||||
}
|
||||
|
||||
model DebitNote {
|
||||
|
|
|
|||
|
|
@ -642,11 +642,13 @@ export class UserController extends Controller {
|
|||
}
|
||||
}
|
||||
|
||||
if (body.username) {
|
||||
if (body.username || body.email || body.firstName || body.lastName) {
|
||||
await editUser(userId, {
|
||||
firstName: body.firstName,
|
||||
lastName: body.lastName,
|
||||
username: body.username,
|
||||
email: body.email,
|
||||
enabled: body.status !== "INACTIVE",
|
||||
enabled: body.status ? body.status !== "INACTIVE" : undefined,
|
||||
});
|
||||
} else if (body.status) {
|
||||
await editUser(userId, { enabled: body.status !== "INACTIVE" });
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { queryOrNot } from "../utils/relation";
|
|||
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatus from "../interfaces/http-status";
|
||||
import { RequestWorkStatus } from "../generated/kysely/types";
|
||||
|
||||
type QuotationCreate = {
|
||||
registeredBranchId: string;
|
||||
|
|
@ -190,6 +191,8 @@ export class QuotationController extends Controller {
|
|||
@Query() payCondition?: PayCondition,
|
||||
@Query() status?: QuotationStatus,
|
||||
@Query() urgentFirst?: boolean,
|
||||
@Query() includeRegisteredBranch?: boolean,
|
||||
@Query() hasCancel?: boolean,
|
||||
@Query() code?: string,
|
||||
@Query() query = "",
|
||||
) {
|
||||
|
|
@ -214,6 +217,18 @@ export class QuotationController extends Controller {
|
|||
payCondition,
|
||||
registeredBranch: isSystem(req.user) ? undefined : { OR: permissionCond(req.user) },
|
||||
quotationStatus: status,
|
||||
requestData: hasCancel
|
||||
? {
|
||||
some: {
|
||||
requestWork: {
|
||||
some: {
|
||||
creditNoteId: null,
|
||||
stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
} satisfies Prisma.QuotationWhereInput;
|
||||
|
||||
const [result, total] = await prisma.$transaction([
|
||||
|
|
@ -223,6 +238,7 @@ export class QuotationController extends Controller {
|
|||
_count: {
|
||||
select: { worker: true },
|
||||
},
|
||||
registeredBranch: includeRegisteredBranch,
|
||||
customerBranch: {
|
||||
include: {
|
||||
customer: {
|
||||
|
|
@ -246,6 +262,46 @@ export class QuotationController extends Controller {
|
|||
prisma.quotation.count({ where }),
|
||||
]);
|
||||
|
||||
if (hasCancel) {
|
||||
const canceled = await prisma.requestData.findMany({
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
requestWork: {
|
||||
where: {
|
||||
creditNoteId: null,
|
||||
stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
requestWork: {
|
||||
some: {
|
||||
creditNoteId: null,
|
||||
stepStatus: { some: { workStatus: RequestWorkStatus.Canceled } },
|
||||
},
|
||||
},
|
||||
quotationId: { in: result.map((v) => v.id) },
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
result: result.map((v) => {
|
||||
const canceledCount = canceled
|
||||
.filter((item) => item.quotationId === v.id)
|
||||
.reduce((a, c) => a + c._count.requestWork, 0);
|
||||
return Object.assign(v, {
|
||||
_count: { ...v._count, canceledWork: canceledCount },
|
||||
});
|
||||
}),
|
||||
page,
|
||||
pageSize,
|
||||
total,
|
||||
};
|
||||
}
|
||||
|
||||
return { result: result, page, pageSize, total };
|
||||
}
|
||||
|
||||
|
|
@ -419,11 +475,11 @@ export class QuotationController extends Controller {
|
|||
const list = body.productServiceList.map((v, i) => {
|
||||
const p = product.find((p) => p.id === v.productId)!;
|
||||
const price = body.agentPrice ? p.agentPrice : p.price;
|
||||
const pricePerUnit = p.vatIncluded ? precisionRound(price / (1 + VAT_DEFAULT)) : price;
|
||||
const pricePerUnit = p.vatIncluded ? price / (1 + VAT_DEFAULT) : price;
|
||||
const vat = p.calcVat
|
||||
? precisionRound(
|
||||
(pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) * VAT_DEFAULT,
|
||||
) * (!v.discount ? v.amount : 1)
|
||||
? (pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) *
|
||||
VAT_DEFAULT *
|
||||
(!v.discount ? v.amount : 1)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
|
|
@ -684,11 +740,11 @@ export class QuotationController extends Controller {
|
|||
const list = body.productServiceList?.map((v, i) => {
|
||||
const p = product.find((p) => p.id === v.productId)!;
|
||||
const price = record.agentPrice ? p.agentPrice : p.price;
|
||||
const pricePerUnit = p.vatIncluded ? precisionRound(price / (1 + VAT_DEFAULT)) : price;
|
||||
const pricePerUnit = p.vatIncluded ? price / (1 + VAT_DEFAULT) : price;
|
||||
const vat = p.calcVat
|
||||
? precisionRound(
|
||||
(pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) * VAT_DEFAULT,
|
||||
) * (!v.discount ? v.amount : 1)
|
||||
? (pricePerUnit * (v.discount ? v.amount : 1) - (v.discount || 0)) *
|
||||
VAT_DEFAULT *
|
||||
(!v.discount ? v.amount : 1)
|
||||
: 0;
|
||||
return {
|
||||
order: i + 1,
|
||||
|
|
@ -980,7 +1036,7 @@ export class QuotationActionController extends Controller {
|
|||
@Route("api/v1/quotation/{quotationId}/attachment")
|
||||
@Tags("Quotation")
|
||||
export class QuotationFileController extends Controller {
|
||||
private async checkPermission(user: RequestWithUser["user"], id: string) {
|
||||
async #checkPermission(user: RequestWithUser["user"], id: string) {
|
||||
const data = await prisma.quotation.findUnique({
|
||||
include: {
|
||||
registeredBranch: {
|
||||
|
|
@ -989,14 +1045,14 @@ export class QuotationFileController extends Controller {
|
|||
},
|
||||
where: { id },
|
||||
});
|
||||
if (!data) throw notFoundError("Payment");
|
||||
if (!data) throw notFoundError("Quotation");
|
||||
await permissionCheck(user, data.registeredBranch);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Security("keycloak")
|
||||
async listAttachment(@Request() req: RequestWithUser, @Path() quotationId: string) {
|
||||
await this.checkPermission(req.user, quotationId);
|
||||
await this.#checkPermission(req.user, quotationId);
|
||||
return await listFile(fileLocation.quotation.attachment(quotationId));
|
||||
}
|
||||
|
||||
|
|
@ -1018,7 +1074,7 @@ export class QuotationFileController extends Controller {
|
|||
@Path() quotationId: string,
|
||||
@Path() name: string,
|
||||
) {
|
||||
await this.checkPermission(req.user, quotationId);
|
||||
await this.#checkPermission(req.user, quotationId);
|
||||
return await getFile(fileLocation.quotation.attachment(quotationId, name));
|
||||
}
|
||||
|
||||
|
|
@ -1029,7 +1085,7 @@ export class QuotationFileController extends Controller {
|
|||
@Path() quotationId: string,
|
||||
@Path() name: string,
|
||||
) {
|
||||
await this.checkPermission(req.user, quotationId);
|
||||
await this.#checkPermission(req.user, quotationId);
|
||||
return await setFile(fileLocation.quotation.attachment(quotationId, name));
|
||||
}
|
||||
|
||||
|
|
@ -1040,7 +1096,7 @@ export class QuotationFileController extends Controller {
|
|||
@Path() quotationId: string,
|
||||
@Path() name: string,
|
||||
) {
|
||||
await this.checkPermission(req.user, quotationId);
|
||||
await this.#checkPermission(req.user, quotationId);
|
||||
return await deleteFile(fileLocation.quotation.attachment(quotationId, name));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -292,6 +292,8 @@ export class RequestListController extends Controller {
|
|||
@Query() requestDataId?: string,
|
||||
@Query() workStatus?: RequestWorkStatus,
|
||||
@Query() readyToTask?: boolean,
|
||||
@Query() cancelOnly?: boolean,
|
||||
@Query() quotationId?: string,
|
||||
) {
|
||||
let statusCondition: Prisma.RequestWorkWhereInput["stepStatus"] = {};
|
||||
|
||||
|
|
@ -313,16 +315,29 @@ export class RequestListController extends Controller {
|
|||
],
|
||||
},
|
||||
};
|
||||
} else {
|
||||
}
|
||||
|
||||
if (cancelOnly) {
|
||||
statusCondition = {
|
||||
some: { workStatus: RequestWorkStatus.Canceled },
|
||||
};
|
||||
}
|
||||
|
||||
if (workStatus && !readyToTask && !cancelOnly) {
|
||||
statusCondition = {
|
||||
some: { workStatus },
|
||||
};
|
||||
}
|
||||
|
||||
const where = {
|
||||
stepStatus: readyToTask || workStatus ? statusCondition : undefined,
|
||||
stepStatus: readyToTask || cancelOnly || workStatus ? statusCondition : undefined,
|
||||
creditNote: cancelOnly ? null : undefined,
|
||||
request: {
|
||||
id: requestDataId,
|
||||
requestDataStatus: readyToTask
|
||||
? { notIn: [RequestDataStatus.Canceled, RequestDataStatus.Completed] }
|
||||
: undefined,
|
||||
quotationId,
|
||||
quotation: {
|
||||
registeredBranch: { OR: permissionCond(req.user) },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ export class TaskController extends Controller {
|
|||
userTask: true,
|
||||
taskList: true,
|
||||
institution: true,
|
||||
registeredBranch: true,
|
||||
createdBy: true,
|
||||
},
|
||||
}),
|
||||
|
|
@ -150,6 +151,7 @@ export class TaskController extends Controller {
|
|||
where: {
|
||||
requestWorkStep: { responsibleUserId: taskAssignedUserId },
|
||||
},
|
||||
orderBy: { id: "asc" },
|
||||
include: {
|
||||
requestWorkStep: {
|
||||
include: {
|
||||
|
|
@ -451,6 +453,7 @@ export class TaskActionController extends Controller {
|
|||
return await prisma.$transaction(async (tx) => {
|
||||
const promises = body.map(async (v) => {
|
||||
const record = await tx.task.findFirst({
|
||||
include: { requestWorkStep: true },
|
||||
where: {
|
||||
step: v.step,
|
||||
requestWorkId: v.requestWorkId,
|
||||
|
|
@ -458,6 +461,16 @@ export class TaskActionController extends Controller {
|
|||
},
|
||||
});
|
||||
if (!record) throw notFoundError("Task List");
|
||||
|
||||
if (v.taskStatus === TaskStatus.Restart && record.requestWorkStep.responsibleUserId) {
|
||||
await tx.userTask.updateMany({
|
||||
where: {
|
||||
taskOrderId: record.taskOrderId,
|
||||
userId: record.requestWorkStep.responsibleUserId,
|
||||
},
|
||||
data: { userTaskStatus: UserTaskStatus.Restart },
|
||||
});
|
||||
}
|
||||
return await tx.task.update({
|
||||
where: { id: record.id },
|
||||
data: {
|
||||
|
|
@ -512,20 +525,14 @@ export class TaskActionController extends Controller {
|
|||
taskOrderId: taskOrderId,
|
||||
userId: submitUserId,
|
||||
},
|
||||
data: { userTaskStatus: UserTaskStatus.Submit },
|
||||
data: { userTaskStatus: UserTaskStatus.Submit, submittedAt: new Date() },
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
@Post("complete")
|
||||
@Security("keycloak")
|
||||
async completeTaskOrder(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() taskOrderId: string,
|
||||
@Query() submitUserId?: string,
|
||||
) {
|
||||
submitUserId = submitUserId ?? req.user.sub;
|
||||
|
||||
async completeTaskOrder(@Request() req: RequestWithUser, @Path() taskOrderId: string) {
|
||||
const record = await prisma.taskOrder.findFirst({ where: { id: taskOrderId } });
|
||||
|
||||
if (!record) throw notFoundError("Task Order");
|
||||
|
|
@ -534,21 +541,54 @@ export class TaskActionController extends Controller {
|
|||
await Promise.all([
|
||||
tx.taskOrder.update({
|
||||
where: { id: taskOrderId },
|
||||
data: { taskOrderStatus: TaskOrderStatus.Complete },
|
||||
data: {
|
||||
taskOrderStatus: TaskOrderStatus.Complete,
|
||||
userTask: {
|
||||
updateMany: {
|
||||
where: { taskOrderId },
|
||||
data: {
|
||||
userTaskStatus: UserTaskStatus.Submit,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
tx.requestWorkStepStatus.updateMany({
|
||||
where: {
|
||||
task: {
|
||||
some: { taskOrderId, taskStatus: TaskStatus.Redo },
|
||||
some: {
|
||||
taskOrderId,
|
||||
taskStatus: {
|
||||
notIn: [
|
||||
TaskStatus.Canceled,
|
||||
TaskStatus.Success,
|
||||
TaskStatus.Validate,
|
||||
TaskStatus.Complete,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
data: { workStatus: RequestWorkStatus.Ready },
|
||||
}),
|
||||
tx.task.updateMany({
|
||||
where: {
|
||||
taskOrderId: taskOrderId,
|
||||
taskStatus: {
|
||||
notIn: [
|
||||
TaskStatus.Canceled,
|
||||
TaskStatus.Success,
|
||||
TaskStatus.Validate,
|
||||
TaskStatus.Complete,
|
||||
],
|
||||
},
|
||||
},
|
||||
data: { taskStatus: TaskStatus.Redo },
|
||||
}),
|
||||
tx.task.updateMany({
|
||||
where: {
|
||||
taskOrderId: taskOrderId,
|
||||
taskStatus: TaskStatus.Validate,
|
||||
requestWorkStep: { responsibleUserId: submitUserId },
|
||||
},
|
||||
data: { taskStatus: TaskStatus.Complete },
|
||||
}),
|
||||
|
|
@ -708,17 +748,38 @@ export class UserTaskController extends Controller {
|
|||
requestWorkStep: { responsibleUserId: req.user.sub },
|
||||
},
|
||||
},
|
||||
userTask: userTaskStatus
|
||||
? {
|
||||
some:
|
||||
userTaskStatus !== UserTaskStatus.Pending
|
||||
? {
|
||||
userTaskStatus,
|
||||
userId: req.user.sub,
|
||||
}
|
||||
: undefined,
|
||||
none: userTaskStatus === UserTaskStatus.Pending ? { userId: req.user.sub } : undefined,
|
||||
}
|
||||
AND: userTaskStatus
|
||||
? [
|
||||
{
|
||||
OR:
|
||||
userTaskStatus === UserTaskStatus.Pending
|
||||
? [
|
||||
{
|
||||
userTask: {
|
||||
some: {
|
||||
userTaskStatus: {
|
||||
in: [UserTaskStatus.Pending, UserTaskStatus.Restart],
|
||||
},
|
||||
userId: req.user.sub,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
userTask: { none: { userId: req.user.sub } },
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
userTask:
|
||||
userTaskStatus !== UserTaskStatus.Pending
|
||||
? {
|
||||
some: {
|
||||
userTaskStatus,
|
||||
userId: req.user.sub,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
OR: queryOrNot(query, [
|
||||
{ code: { contains: query, mode: "insensitive" } },
|
||||
|
|
@ -735,6 +796,7 @@ export class UserTaskController extends Controller {
|
|||
userTask: {
|
||||
where: { userId: req.user.sub },
|
||||
},
|
||||
registeredBranch: true,
|
||||
institution: true,
|
||||
createdBy: true,
|
||||
},
|
||||
|
|
@ -783,9 +845,11 @@ export class UserTaskController extends Controller {
|
|||
data: {
|
||||
taskOrderStatus: TaskOrderStatus.InProgress,
|
||||
userTask: {
|
||||
deleteMany: { userId: req.user.sub },
|
||||
create: {
|
||||
userId: req.user.sub,
|
||||
userTaskStatus: UserTaskStatus.Accept,
|
||||
acceptedAt: new Date(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -793,7 +857,7 @@ export class UserTaskController extends Controller {
|
|||
tx.task.updateMany({
|
||||
where: {
|
||||
taskOrderId: taskOrderId,
|
||||
taskStatus: TaskStatus.Pending,
|
||||
taskStatus: { in: [TaskStatus.Pending, TaskStatus.Restart] },
|
||||
requestWorkStep: { responsibleUserId: req.user.sub },
|
||||
},
|
||||
data: {
|
||||
|
|
@ -812,13 +876,6 @@ export class UserTaskController extends Controller {
|
|||
},
|
||||
data: { requestDataStatus: RequestDataStatus.InProgress },
|
||||
}),
|
||||
tx.userTask.create({
|
||||
data: {
|
||||
userId: req.user.sub,
|
||||
taskOrderId: taskOrderId,
|
||||
userTaskStatus: UserTaskStatus.Accept,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Head,
|
||||
Path,
|
||||
Post,
|
||||
Put,
|
||||
|
|
@ -13,7 +14,6 @@ import {
|
|||
Tags,
|
||||
} from "tsoa";
|
||||
|
||||
// import { Prisma } from "@prisma/client";
|
||||
import prisma from "../db";
|
||||
|
||||
import { RequestWithUser } from "../interfaces/user";
|
||||
|
|
@ -24,10 +24,11 @@ import {
|
|||
} from "../services/permission";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatus from "../interfaces/http-status";
|
||||
import { deleteFile, fileLocation, getFile, getPresigned, listFile, setFile } from "../utils/minio";
|
||||
import { notFoundError } from "../utils/error";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { CreditNotePaybackType, CreditNoteStatus, Prisma } from "@prisma/client";
|
||||
import { queryOrNot } from "../utils/relation";
|
||||
import { RequestWorkStatus } from "../generated/kysely/types";
|
||||
import { PaybackStatus, RequestWorkStatus } from "../generated/kysely/types";
|
||||
|
||||
const MANAGE_ROLES = [
|
||||
"system",
|
||||
|
|
@ -53,10 +54,22 @@ const permissionCheckCompany = createPermCheck((_) => true);
|
|||
type CreditNoteCreate = {
|
||||
requestWorkId: string[];
|
||||
quotationId: string;
|
||||
reason?: string;
|
||||
detail?: string;
|
||||
paybackType?: CreditNotePaybackType;
|
||||
paybackBank?: string;
|
||||
paybackAccount?: string;
|
||||
paybackAccountName?: string;
|
||||
};
|
||||
type CreditNoteUpdate = {
|
||||
requestWorkId: string[];
|
||||
quotationId: string;
|
||||
reason?: string;
|
||||
detail?: string;
|
||||
paybackType?: CreditNotePaybackType;
|
||||
paybackBank?: string;
|
||||
paybackAccount?: string;
|
||||
paybackAccountName?: string;
|
||||
};
|
||||
|
||||
@Route("api/v1/credit-note")
|
||||
|
|
@ -64,7 +77,7 @@ type CreditNoteUpdate = {
|
|||
export class CreditNoteController extends Controller {
|
||||
@Get("stats")
|
||||
@Security("keycloak")
|
||||
async getCreditNoteStats(@Request() req: RequestWithUser, @Query() quotationId: string) {
|
||||
async getCreditNoteStats(@Request() req: RequestWithUser, @Query() quotationId?: string) {
|
||||
const where = {
|
||||
requestWork: {
|
||||
some: {
|
||||
|
|
@ -88,8 +101,16 @@ export class CreditNoteController extends Controller {
|
|||
@Query() pageSize: number = 30,
|
||||
@Query() query: string = "",
|
||||
@Query() quotationId?: string,
|
||||
@Query() creditNoteStatus?: CreditNoteStatus,
|
||||
) {
|
||||
return await this.getCreditNoteListByCriteria(req, page, pageSize, query, quotationId);
|
||||
return await this.getCreditNoteListByCriteria(
|
||||
req,
|
||||
page,
|
||||
pageSize,
|
||||
query,
|
||||
quotationId,
|
||||
creditNoteStatus,
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: only when needed or else remove this and implement in getCreditNoteList
|
||||
|
|
@ -101,6 +122,7 @@ export class CreditNoteController extends Controller {
|
|||
@Query() pageSize: number = 30,
|
||||
@Query() query: string = "",
|
||||
@Query() quotationId?: string,
|
||||
@Query() creditNoteStatus?: CreditNoteStatus,
|
||||
@Body() body?: {},
|
||||
) {
|
||||
const where = {
|
||||
|
|
@ -146,6 +168,7 @@ export class CreditNoteController extends Controller {
|
|||
},
|
||||
},
|
||||
]),
|
||||
creditNoteStatus,
|
||||
requestWork: {
|
||||
some: {
|
||||
request: {
|
||||
|
|
@ -162,6 +185,16 @@ export class CreditNoteController extends Controller {
|
|||
prisma.creditNote.findMany({
|
||||
where,
|
||||
include: {
|
||||
quotation: {
|
||||
include: {
|
||||
registeredBranch: true,
|
||||
customerBranch: {
|
||||
include: {
|
||||
customer: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestWork: {
|
||||
include: { request: true },
|
||||
},
|
||||
|
|
@ -191,8 +224,33 @@ export class CreditNoteController extends Controller {
|
|||
return prisma.creditNote.findFirst({
|
||||
where,
|
||||
include: {
|
||||
quotation: {
|
||||
include: {
|
||||
registeredBranch: true,
|
||||
customerBranch: {
|
||||
include: {
|
||||
customer: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
requestWork: {
|
||||
include: { request: true },
|
||||
include: {
|
||||
request: {
|
||||
include: { employee: true },
|
||||
},
|
||||
productService: {
|
||||
include: {
|
||||
service: true,
|
||||
work: {
|
||||
include: { productOnWork: true },
|
||||
},
|
||||
product: {
|
||||
include: { document: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -203,6 +261,7 @@ export class CreditNoteController extends Controller {
|
|||
async createCreditNote(@Request() req: RequestWithUser, @Body() body: CreditNoteCreate) {
|
||||
const requestWork = await prisma.requestWork.findMany({
|
||||
where: {
|
||||
creditNote: null,
|
||||
request: {
|
||||
quotation: {
|
||||
id: body.quotationId,
|
||||
|
|
@ -216,6 +275,15 @@ export class CreditNoteController extends Controller {
|
|||
id: { in: body.requestWorkId },
|
||||
},
|
||||
include: {
|
||||
stepStatus: true,
|
||||
productService: {
|
||||
include: {
|
||||
product: true,
|
||||
work: {
|
||||
include: { productOnWork: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
request: {
|
||||
include: {
|
||||
quotation: {
|
||||
|
|
@ -236,28 +304,74 @@ export class CreditNoteController extends Controller {
|
|||
requestWork.map((item) => permissionCheck(req.user, item.request.quotation.registeredBranch)),
|
||||
);
|
||||
|
||||
const record = await prisma.creditNote.create({
|
||||
include: {
|
||||
requestWork: {
|
||||
include: {
|
||||
request: true,
|
||||
},
|
||||
},
|
||||
quotation: true,
|
||||
},
|
||||
data: {
|
||||
requestWork: {
|
||||
connect: body.requestWorkId.map((v) => ({
|
||||
id: v,
|
||||
})),
|
||||
},
|
||||
quotationId: body.quotationId,
|
||||
},
|
||||
});
|
||||
const value = requestWork.reduce((a, c) => {
|
||||
const serviceChargeStepCount = c.productService.work?.productOnWork.find(
|
||||
(v) => v.productId === c.productService.productId,
|
||||
)?.stepCount;
|
||||
|
||||
const successCount = c.stepStatus.filter(
|
||||
(v) => v.workStatus === RequestWorkStatus.Completed,
|
||||
).length;
|
||||
|
||||
const price = c.request.quotation.agentPrice ? "price" : "agentPrice";
|
||||
|
||||
if (serviceChargeStepCount && successCount) {
|
||||
return (
|
||||
a +
|
||||
c.productService.product[price] -
|
||||
c.productService.product.serviceCharge * successCount
|
||||
);
|
||||
}
|
||||
return a + c.productService.product.price;
|
||||
}, 0);
|
||||
|
||||
this.setStatus(HttpStatus.CREATED);
|
||||
|
||||
return record;
|
||||
return await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const currentMonth = new Date().getMonth() + 1;
|
||||
|
||||
const last = await tx.runningNo.upsert({
|
||||
where: {
|
||||
key: `CREDIT_NOTE_${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}`,
|
||||
},
|
||||
create: {
|
||||
key: `CREDIT_NOTE_${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}`,
|
||||
value: 1,
|
||||
},
|
||||
update: { value: { increment: 1 } },
|
||||
});
|
||||
|
||||
return await prisma.creditNote.create({
|
||||
include: {
|
||||
requestWork: {
|
||||
include: {
|
||||
request: true,
|
||||
},
|
||||
},
|
||||
quotation: true,
|
||||
},
|
||||
data: {
|
||||
reason: body.reason,
|
||||
detail: body.detail,
|
||||
paybackType: body.paybackType,
|
||||
paybackBank: body.paybackBank,
|
||||
paybackAccount: body.paybackAccount,
|
||||
paybackAccountName: body.paybackAccountName,
|
||||
code: `CN${currentYear.toString().padStart(2, "0")}${currentMonth.toString().padStart(2, "0")}${last.value.toString().padStart(6, "0")}`,
|
||||
value,
|
||||
requestWork: {
|
||||
connect: body.requestWorkId.map((v) => ({
|
||||
id: v,
|
||||
})),
|
||||
},
|
||||
quotationId: body.quotationId,
|
||||
},
|
||||
});
|
||||
},
|
||||
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable },
|
||||
);
|
||||
}
|
||||
|
||||
@Put("{creditNoteId}")
|
||||
|
|
@ -284,6 +398,7 @@ export class CreditNoteController extends Controller {
|
|||
|
||||
const requestWork = await prisma.requestWork.findMany({
|
||||
where: {
|
||||
OR: [{ creditNote: null }, { creditNoteId }],
|
||||
request: {
|
||||
quotation: {
|
||||
id: body.quotationId,
|
||||
|
|
@ -296,16 +411,55 @@ export class CreditNoteController extends Controller {
|
|||
},
|
||||
id: { in: body.requestWorkId },
|
||||
},
|
||||
include: {
|
||||
stepStatus: true,
|
||||
productService: {
|
||||
include: {
|
||||
product: true,
|
||||
work: {
|
||||
include: { productOnWork: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
request: {
|
||||
include: {
|
||||
quotation: {
|
||||
include: {
|
||||
registeredBranch: { include: branchRelationPermInclude(req.user) },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (requestWork.length !== body.requestWorkId.length) {
|
||||
throw new HttpError(HttpStatus.BAD_REQUEST, "Not Match", "reqNotMet");
|
||||
}
|
||||
|
||||
const value = requestWork.reduce((a, c) => {
|
||||
const serviceChargeStepCount = c.productService.work?.productOnWork.find(
|
||||
(v) => v.productId === c.productService.productId,
|
||||
)?.stepCount;
|
||||
|
||||
const successCount = c.stepStatus.filter(
|
||||
(v) => v.workStatus === RequestWorkStatus.Completed,
|
||||
).length;
|
||||
|
||||
const price = c.request.quotation.agentPrice ? "price" : "agentPrice";
|
||||
|
||||
if (serviceChargeStepCount && successCount) {
|
||||
return (
|
||||
a +
|
||||
c.productService.product[price] -
|
||||
c.productService.product.serviceCharge * successCount
|
||||
);
|
||||
}
|
||||
return a + c.productService.product.price;
|
||||
}, 0);
|
||||
|
||||
const record = await prisma.creditNote.update({
|
||||
where: {
|
||||
id: creditNoteId,
|
||||
},
|
||||
where: { id: creditNoteId },
|
||||
include: {
|
||||
requestWork: {
|
||||
include: {
|
||||
|
|
@ -315,6 +469,13 @@ export class CreditNoteController extends Controller {
|
|||
quotation: true,
|
||||
},
|
||||
data: {
|
||||
reason: body.reason,
|
||||
detail: body.detail,
|
||||
paybackType: body.paybackType,
|
||||
paybackBank: body.paybackBank,
|
||||
paybackAccount: body.paybackAccount,
|
||||
paybackAccountName: body.paybackAccountName,
|
||||
value,
|
||||
requestWork: {
|
||||
disconnect: creditNoteData.requestWork
|
||||
.map((item) => ({
|
||||
|
|
@ -353,3 +514,110 @@ export class CreditNoteController extends Controller {
|
|||
return await prisma.creditNote.delete({ where: { id: creditNoteId } });
|
||||
}
|
||||
}
|
||||
|
||||
@Route("api/v1/credit-note/{creditNoteId}")
|
||||
@Tags("Credit Note")
|
||||
export class CreditNoteActionController extends Controller {
|
||||
async #checkPermission(user: RequestWithUser["user"], id: string) {
|
||||
const creditNoteData = await prisma.creditNote.findFirst({
|
||||
where: { id },
|
||||
include: {
|
||||
requestWork: true,
|
||||
quotation: {
|
||||
include: {
|
||||
registeredBranch: { include: branchRelationPermInclude(user) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!creditNoteData) throw notFoundError("Credit Note");
|
||||
await permissionCheck(user, creditNoteData.quotation.registeredBranch);
|
||||
return creditNoteData;
|
||||
}
|
||||
|
||||
@Post("payback-status")
|
||||
@Security("keycloak", MANAGE_ROLES)
|
||||
async updateStatus(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() creditNoteId: string,
|
||||
@Body() body: PaybackStatus,
|
||||
) {
|
||||
await this.#checkPermission(req.user, creditNoteId);
|
||||
return await prisma.creditNote.update({
|
||||
where: { id: creditNoteId },
|
||||
include: {
|
||||
requestWork: {
|
||||
include: {
|
||||
request: true,
|
||||
},
|
||||
},
|
||||
quotation: true,
|
||||
},
|
||||
data: {
|
||||
creditNoteStatus: body === PaybackStatus.Done ? CreditNoteStatus.Success : undefined,
|
||||
paybackStatus: body,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Route("api/v1/credit-note/{creditNoteId}")
|
||||
@Tags("Credit Note")
|
||||
export class CreditNoteAttachmentController extends Controller {
|
||||
async #checkPermission(user: RequestWithUser["user"], id: string) {
|
||||
const creditNoteData = await prisma.creditNote.findFirst({
|
||||
where: { id },
|
||||
include: {
|
||||
requestWork: true,
|
||||
quotation: {
|
||||
include: {
|
||||
registeredBranch: { include: branchRelationPermInclude(user) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!creditNoteData) throw notFoundError("Credit Note");
|
||||
await permissionCheck(user, creditNoteData.quotation.registeredBranch);
|
||||
return creditNoteData;
|
||||
}
|
||||
|
||||
@Get("attachment")
|
||||
@Security("keycloak")
|
||||
async listAttachment(@Request() req: RequestWithUser, @Path() creditNoteId: string) {
|
||||
await this.#checkPermission(req.user, creditNoteId);
|
||||
return await listFile(fileLocation.creditNote.attachment(creditNoteId));
|
||||
}
|
||||
|
||||
@Get("attachment/{name}")
|
||||
@Security("keycloak")
|
||||
async getAttachment(@Path() creditNoteId: string, @Path() name: string) {
|
||||
return await getFile(fileLocation.creditNote.attachment(creditNoteId, name));
|
||||
}
|
||||
|
||||
@Head("attachment/{name}")
|
||||
async headAttachment(@Path() creditNoteId: string, @Path() name: string) {
|
||||
return await getPresigned("head", fileLocation.creditNote.attachment(creditNoteId, name));
|
||||
}
|
||||
|
||||
@Put("attachment/{name}")
|
||||
@Security("keycloak")
|
||||
async putAttachment(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() creditNoteId: string,
|
||||
@Path() name: string,
|
||||
) {
|
||||
await this.#checkPermission(req.user, creditNoteId);
|
||||
return await setFile(fileLocation.creditNote.attachment(creditNoteId, name));
|
||||
}
|
||||
|
||||
@Delete("attachment/{name}")
|
||||
@Security("keycloak")
|
||||
async delAttachment(
|
||||
@Request() req: RequestWithUser,
|
||||
@Path() creditNoteId: string,
|
||||
@Path() name: string,
|
||||
) {
|
||||
await this.#checkPermission(req.user, creditNoteId);
|
||||
return await deleteFile(fileLocation.creditNote.attachment(creditNoteId, name));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,4 +112,7 @@ export const fileLocation = {
|
|||
task: {
|
||||
attachment: (taskId: string, name?: string) => `task/attachment-${taskId}/${name || ""}`,
|
||||
},
|
||||
creditNote: {
|
||||
attachment: (taskId: string, name?: string) => `credit-note/attachment-${taskId}/${name || ""}`,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue