add import file product
All checks were successful
Spell Check / Spell Check with Typos (push) Successful in 7s

This commit is contained in:
Kanjana 2025-04-18 15:39:02 +07:00
parent fd7833a592
commit 05d16f22de
3 changed files with 236 additions and 0 deletions

View file

@ -24,6 +24,7 @@
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/morgan": "^1.9.9", "@types/morgan": "^1.9.9",
"@types/multer": "^1.4.12",
"@types/node": "^20.17.10", "@types/node": "^20.17.10",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",
@ -46,12 +47,14 @@
"dayjs-plugin-utc": "^0.1.2", "dayjs-plugin-utc": "^0.1.2",
"docx-templates": "^4.13.0", "docx-templates": "^4.13.0",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"exceljs": "^4.4.0",
"express": "^4.21.2", "express": "^4.21.2",
"fast-jwt": "^5.0.5", "fast-jwt": "^5.0.5",
"json-2-csv": "^5.5.8", "json-2-csv": "^5.5.8",
"kysely": "^0.27.5", "kysely": "^0.27.5",
"minio": "^8.0.2", "minio": "^8.0.2",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^1.4.5-lts.2",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
"prisma-extension-kysely": "^3.0.0", "prisma-extension-kysely": "^3.0.0",
"promise.any": "^2.0.6", "promise.any": "^2.0.6",

View file

@ -11,6 +11,7 @@ import {
Security, Security,
Tags, Tags,
Query, Query,
UploadedFile,
} from "tsoa"; } from "tsoa";
import { Prisma, Product, Status } from "@prisma/client"; import { Prisma, Product, Status } from "@prisma/client";
@ -28,6 +29,7 @@ import { filterStatus } from "../services/prisma";
import { deleteFile, deleteFolder, fileLocation, getFile, listFile, setFile } from "../utils/minio"; import { deleteFile, deleteFolder, fileLocation, getFile, listFile, setFile } from "../utils/minio";
import { isUsedError, notFoundError, relationError } from "../utils/error"; import { isUsedError, notFoundError, relationError } from "../utils/error";
import { queryOrNot, whereDateQuery } from "../utils/relation"; import { queryOrNot, whereDateQuery } from "../utils/relation";
import spreadsheet from "../utils/spreadsheet";
const MANAGE_ROLES = [ const MANAGE_ROLES = [
"system", "system",
@ -447,6 +449,132 @@ export class ProductController extends Controller {
where: { id: productId }, where: { id: productId },
}); });
} }
@Post("uploadedFile")
@Security("keycloak", MANAGE_ROLES)
async importProduct(
@Request() req: RequestWithUser,
@UploadedFile() file: Express.Multer.File,
@Query() productGroupId: string,
) {
if (!file?.buffer) throw notFoundError("File");
const buffer = new Uint8Array(file.buffer).buffer;
const dataFile = await spreadsheet.readExcel(buffer, {
header: true,
worksheet: "Sheet1",
});
let dataName: string[] = [];
const data = await dataFile.map((item: any) => {
dataName.push(item.name);
return {
...item,
expenseType:
item.expenseType === "ค่าธรรมเนียม"
? "fee"
: item.expenseType === "ค่าบริการ"
? "serviceFee"
: "processingFee",
shared: item.shared === "ใช่" ? true : false,
calcVat: item.calcVat === "ใช่" ? true : false,
vatIncluded: item.vatIncluded === "รวม" ? true : false,
agentPriceCalcVat: item.agentPriceCalcVat === "ใช่" ? true : false,
agentPriceVatIncluded: item.agentPriceVatIncluded === "รวม" ? true : false,
serviceChargeCalcVat: item.serviceChargeCalcVat === "ใช่" ? true : false,
serviceChargeVatIncluded: item.serviceChargeVatIncluded === "รวม" ? true : false,
};
});
const [productGroup, productSameName] = await prisma.$transaction([
prisma.productGroup.findFirst({
include: {
registeredBranch: {
include: branchRelationPermInclude(req.user),
},
createdBy: true,
updatedBy: true,
},
where: { id: productGroupId },
}),
prisma.product.findMany({
where: {
productGroup: {
registeredBranch: {
OR: permissionCondCompany(req.user),
},
},
name: { in: dataName },
},
}),
]);
if (!productGroup) throw relationError("Product Group");
await permissionCheck(req.user, productGroup.registeredBranch);
let dataProduct: ProductCreate[] = [];
const record = await prisma.$transaction(
async (tx) => {
const branch = productGroup.registeredBranch;
const company = (branch.headOffice || branch).code;
console.log(branch, company);
for (const item of data) {
const dataDuplicate = productSameName.some(
(v) => v.code.slice(0, -3) === item.code.toUpperCase() && v.name === item.name,
);
if (!dataDuplicate) {
const last = await tx.runningNo.upsert({
where: {
key: `PRODUCT_${company}_${item.code.toLocaleUpperCase()}`,
},
create: {
key: `PRODUCT_${company}_${item.code.toLocaleUpperCase()}`,
value: 1,
},
update: { value: { increment: 1 } },
});
dataProduct.push({
...item,
code: `${item.code.toLocaleUpperCase()}${last.value.toString().padStart(3, "0")}`,
createdByUserId: req.user.sub,
updatedByUserId: req.user.sub,
productGroupId: productGroupId,
});
}
}
console.log("dataProduct", dataProduct);
return await prisma.product.createManyAndReturn({
data: dataProduct,
include: {
createdBy: true,
updatedBy: true,
},
});
},
{
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
},
);
if (productGroup.status === "CREATED") {
await prisma.productGroup.update({
include: {
createdBy: true,
updatedBy: true,
},
where: { id: productGroupId },
data: { status: Status.ACTIVE },
});
}
this.setStatus(HttpStatus.CREATED);
return record;
}
} }
@Route("api/v1/product/{productId}") @Route("api/v1/product/{productId}")

105
src/utils/spreadsheet.ts Normal file
View file

@ -0,0 +1,105 @@
import Excel from "exceljs";
export default class spreadsheet {
static async readCsv() {
// TODO: read csv
}
/**
* This function read data from excel file.
*
* @param buffer - Excel file.
* @param opts.header - Interprets the first row as the names of the fields.
* @param opts.worksheet - Specifies the worksheet to read. Can be the worksheet's name or its 1-based index.
*
* @returns
*/
static async readExcel<T extends unknown>(
buffer: Excel.Buffer,
opts?: { header?: boolean; worksheet?: number | string },
): Promise<T[]> {
const workbook = new Excel.Workbook();
await workbook.xlsx.load(buffer);
const worksheet = workbook.getWorksheet(opts?.worksheet ?? 1);
if (!worksheet) return [];
const header: Record<number, string | number> = {};
const values: any[] = [];
worksheet.eachRow((row, rowId) => {
if (rowId === 1 && opts?.header !== false) {
row.eachCell((cell, cellId) => {
if (typeof cell.value === "string") {
header[cellId] = nameValue(cell.value);
} else {
header[cellId] = cellId.toString();
}
});
} else {
const data: Record<string | number, Excel.CellValue> = {};
row.eachCell((cell, cellId) => {
data[opts?.header !== false ? header[cellId] : cellId - 1] = cell.value;
});
values.push(opts?.header !== false ? data : Object.values(data));
}
});
return values;
}
}
function nameValue(value: string) {
let code: string;
switch (value) {
case "ชื่อสินค้าและบริการ":
code = "name";
break;
case "ระยะเวลาดำเนินการ":
code = "process";
break;
case "ประเภทค่าใช้จ่าย":
code = "expenseType";
break;
case "รายละเอียด":
code = "detail";
break;
case "หมายเหตุ":
code = "remark";
break;
case "ใช้งานร่วมกัน":
code = "shared";
break;
case "คำนวณภาษีราคาขาย":
code = "calcVat";
break;
case "รวม VAT ราคาขาย":
code = "vatIncluded";
break;
case "ราคาต่อหน่วย (บาท) ราคาขาย":
code = "price";
break;
case "คำนวณภาษีราคาตัวแทน":
code = "agentPriceCalcVat";
break;
case "รวม VAT ราคาตัวแทน":
code = "agentPriceVatIncluded";
break;
case "ราคาต่อหน่วย (บาท) ราคาตัวแทน":
code = "agentPrice";
break;
case "คำนวณภาษีราคาดำเนินการ":
code = "serviceChargeCalcVat";
break;
case "รวม VAT ราคาดำเนินการ":
code = "serviceChargeVatIncluded";
break;
case "ราคาต่อหน่วย (บาท) ราคาดำเนินการ":
code = "serviceCharge";
break;
default:
code = "code";
break;
}
return code;
}