From aaba8dc173a120608bc5fed802ccc78883c13cae Mon Sep 17 00:00:00 2001 From: louis <869322496@qq.com> Date: Mon, 15 Apr 2024 13:50:13 +0800 Subject: [PATCH] feat: sale_quotation --- src/constants/error-code.constant.ts | 4 +- .../group/sale_quotation_group.entity.ts | 2 + .../sale_quotation.controller.ts | 24 ++++ .../sale_quotation/sale_quotation.module.ts | 11 +- .../sale_quotation/sale_quotation.service.ts | 111 ++++++++++++++++++ .../sale_quotation_template.module.ts | 2 +- .../sale_quotation_template.service.ts | 17 ++- 7 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 src/modules/sale_quotation/sale_quotation.controller.ts create mode 100644 src/modules/sale_quotation/sale_quotation.service.ts diff --git a/src/constants/error-code.constant.ts b/src/constants/error-code.constant.ts index fd27d49..27b8e11 100644 --- a/src/constants/error-code.constant.ts +++ b/src/constants/error-code.constant.ts @@ -64,5 +64,7 @@ export enum ErrorEnum { MATERIALS_IN_OUT_UNIT_PRICE_MUST_ZERO_WHEN_MODIFIED = '1411:只能修改初始单价为0的入库记录。 若有疑问,请联系管理员', // SaleQuotation - SALE_QUOTATION_COMPONENT_DUPLICATED = '1412:存在名称,价格,规格都相同的配件,请检查是否重复录入' + SALE_QUOTATION_COMPONENT_DUPLICATED = '1412:存在名称,价格,规格都相同的配件,请检查是否重复录入', + SALE_QUOTATION_TEMPLATE_NAME_DUPLICATE = '1413:模板名已存在' + } diff --git a/src/modules/sale_quotation/group/sale_quotation_group.entity.ts b/src/modules/sale_quotation/group/sale_quotation_group.entity.ts index 4f4038e..6d8276b 100644 --- a/src/modules/sale_quotation/group/sale_quotation_group.entity.ts +++ b/src/modules/sale_quotation/group/sale_quotation_group.entity.ts @@ -23,6 +23,8 @@ export class SaleQuotationGroupEntity extends CommonEntity { @ApiProperty({ description: '删除状态:0未删除,1已删除' }) isDelete: number; + items:any[]; + @ManyToMany(() => SaleQuotationComponentEntity, component => component.groups) @JoinTable({ name: 'sale_quotation_group_component', diff --git a/src/modules/sale_quotation/sale_quotation.controller.ts b/src/modules/sale_quotation/sale_quotation.controller.ts new file mode 100644 index 0000000..9e16122 --- /dev/null +++ b/src/modules/sale_quotation/sale_quotation.controller.ts @@ -0,0 +1,24 @@ +import { Controller, Get, Query, Res } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { FastifyReply } from 'fastify'; +import { SaleQuotationService } from './sale_quotation.service'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +export const permissions = definePermission('sale_quotation:sale_quotation', { + EXPORT: 'export' +} as const); + +@ApiTags('SaleQuotation - 报价模块') +@ApiSecurityAuth() +@Controller('sale_quotation') +export class SaleQuotationController { + constructor(private saleQuotationService: SaleQuotationService) {} + + @Get('export/:id') + @ApiOperation({ summary: '导出报价配置明细' }) + @Perm(permissions.EXPORT) + async export(@IdParam() id: number, @Res() res: FastifyReply): Promise { + await this.saleQuotationService.export(id, res); + } +} diff --git a/src/modules/sale_quotation/sale_quotation.module.ts b/src/modules/sale_quotation/sale_quotation.module.ts index a928e8e..f997cac 100644 --- a/src/modules/sale_quotation/sale_quotation.module.ts +++ b/src/modules/sale_quotation/sale_quotation.module.ts @@ -3,6 +3,12 @@ import { SaleQuotationGroupModule } from './group/sale_quotation_group.module'; import { SaleQuotationTemplateModule } from './template/sale_quotation_template.module'; import { SaleQuotationComponentModule } from './component/sale_quotation_component.module'; import { RouterModule } from '@nestjs/core'; +import { SaleQuotationController } from './sale_quotation.controller'; +import { SaleQuotationService } from './sale_quotation.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SaleQuotationTemplateEntity } from './template/sale_quotation_template.entity'; +import { SaleQuotationGroupEntity } from './group/sale_quotation_group.entity'; +import { SaleQuotationComponentEntity } from './component/sale_quotation_component.entity'; const modules = [ SaleQuotationComponentModule, SaleQuotationGroupModule, @@ -11,6 +17,7 @@ const modules = [ @Module({ imports: [ ...modules, + TypeOrmModule.forFeature([SaleQuotationTemplateEntity, SaleQuotationGroupEntity,SaleQuotationComponentEntity]), RouterModule.register([ { path: 'sale_quotation', @@ -19,7 +26,7 @@ const modules = [ } ]) ], - controllers: [], - providers: [] + controllers: [SaleQuotationController], + providers: [SaleQuotationService] }) export class SaleQuotationModule {} diff --git a/src/modules/sale_quotation/sale_quotation.service.ts b/src/modules/sale_quotation/sale_quotation.service.ts new file mode 100644 index 0000000..d9aca58 --- /dev/null +++ b/src/modules/sale_quotation/sale_quotation.service.ts @@ -0,0 +1,111 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, Repository } from 'typeorm'; +import { SaleQuotationTemplateEntity } from './template/sale_quotation_template.entity'; +import { FastifyReply } from 'fastify'; +import * as ExcelJS from 'exceljs'; +import { SaleQuotationGroupEntity } from './group/sale_quotation_group.entity'; +@Injectable() +export class SaleQuotationService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(SaleQuotationTemplateEntity) + private saleQuotationTemplateRepository: Repository + ) {} + + /** + * 导出报价配置明细 + */ + async export(templateId: number, res: FastifyReply): Promise { + const ROW_HEIGHT = 20; + const HEADER_FONT_SIZE = 18; + const workbook = new ExcelJS.Workbook(); + let data = await this.saleQuotationTemplateRepository.findOneBy({ id: templateId }); + const template: JSON = data.template; + const sheet = workbook.addWorksheet(data.name); + if (template != null) { + sheet.mergeCells('A1:H1'); + // 设置标题 + sheet.getCell('A1').value = '电液控部分配置明细'; + // 设置副标题 + sheet.mergeCells('A2:F2'); + sheet.getCell('A2').value = '支架电液控系统配置明细(中间131架+过渡架4架)'; + sheet.getCell(`G2`).value = ''; + sheet.getCell(`H2`).value = ''; + const groups = template['data'] as SaleQuotationGroupEntity[]; + const headers = [ + '过渡', + '名称', + '规格、型号及说明', + '单位', + '数量', + '备 注', + '单价', + '总价' + ]; + + sheet.addRow(headers); + for (let i = 1; i <= 8; i++) { + sheet.getCell(`${String.fromCharCode(64 + i)}1`).style.font = { bold: true }; + sheet.getCell(`${String.fromCharCode(64 + i)}2`).style.font = { bold: true }; + sheet.getCell(`${String.fromCharCode(64 + i)}3`).style.font = { bold: true }; + } + + let rowIndex = 3; + for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) { + rowIndex++; + const group = groups[groupIndex]; + sheet.mergeCells(`A${rowIndex}:F${rowIndex}`); + sheet.getCell(`A${rowIndex}`).value = group.name; + sheet.getCell(`A${rowIndex}`).style.font = { bold: true }; + sheet.getCell(`G${rowIndex}`).value = ''; + sheet.getCell(`H${rowIndex}`).value = ''; + for (let componentIndex = 0; componentIndex < group.items.length; componentIndex++) { + const item = group.items[componentIndex]; + rowIndex++; + sheet.addRow([ + `${componentIndex + 1}`, + item.name ?? '', + item.componentSpecification ?? '', + item.unit ?? '', + item.quantity ?? '', + item.remark ?? '', + item.unitPrice ?? '', + item.amount ?? '' + ]); + } + } + sheet.getCell(`I${rowIndex - 1}`).value = '总价'; + sheet.getCell(`I${rowIndex - 1}`).style.font = { bold: true }; + sheet.getCell(`I${rowIndex}`).value = template['totalPrice']; + } + sheet.eachRow((row, index) => { + if (index >= 0) { + row.alignment = { vertical: 'middle', horizontal: 'center' }; + row.height = ROW_HEIGHT; + row.eachCell(cell => { + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + }; + }); + } + }); + const columnWidthMap = { A: 8, B: 30, C: 25, F: 20 }; + sheet.columns.forEach((column, index: number) => { + column.width = columnWidthMap[String.fromCharCode(65 + index)] ?? 10; // Minimum width of 10 + }); + + //读取buffer进行传输 + const buffer = await workbook.xlsx.writeBuffer(); + res + .header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + .header( + 'Content-Disposition', + `attachment; filename="${encodeURIComponent('导出_excel' + new Date().getTime() + '.xls')}"` + ) + .send(buffer); + } +} diff --git a/src/modules/sale_quotation/template/sale_quotation_template.module.ts b/src/modules/sale_quotation/template/sale_quotation_template.module.ts index 2759ef7..01c4c03 100644 --- a/src/modules/sale_quotation/template/sale_quotation_template.module.ts +++ b/src/modules/sale_quotation/template/sale_quotation_template.module.ts @@ -9,6 +9,6 @@ import { SaleQuotationTemplateEntity } from './sale_quotation_template.entity'; @Module({ imports: [TypeOrmModule.forFeature([SaleQuotationTemplateEntity]), DatabaseModule], controllers: [SaleQuotationTemplateController], - providers: [SaleQuotationTemplateService] + providers: [SaleQuotationTemplateService], }) export class SaleQuotationTemplateModule {} diff --git a/src/modules/sale_quotation/template/sale_quotation_template.service.ts b/src/modules/sale_quotation/template/sale_quotation_template.service.ts index 5ff5633..dd7a83f 100644 --- a/src/modules/sale_quotation/template/sale_quotation_template.service.ts +++ b/src/modules/sale_quotation/template/sale_quotation_template.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; -import { EntityManager, Repository } from 'typeorm'; +import { EntityManager, Not, Repository } from 'typeorm'; import { Pagination } from '~/helper/paginate/pagination'; import { paginate } from '~/helper/paginate'; import { BusinessException } from '~/common/exceptions/biz.exception'; @@ -33,7 +33,8 @@ export class SaleQuotationTemplateService { const queryBuilder = this.saleQuotationTemplateRepository .createQueryBuilder('saleQuotationTemplate') .where(fieldSearch(fields)) - .andWhere('saleQuotationTemplate.isDelete = 0').addOrderBy('saleQuotationTemplate.createdAt', 'DESC'); + .andWhere('saleQuotationTemplate.isDelete = 0') + .addOrderBy('saleQuotationTemplate.createdAt', 'DESC'); return paginate(queryBuilder, { page, @@ -45,6 +46,12 @@ export class SaleQuotationTemplateService { * 新增 */ async create(dto: SaleQuotationTemplateDto): Promise { + const isDuplicated = await this.saleQuotationTemplateRepository.exist({ + where: { name: dto.name } + }); + if (isDuplicated) { + throw new BusinessException(ErrorEnum.SALE_QUOTATION_TEMPLATE_NAME_DUPLICATE); + } await this.saleQuotationTemplateRepository.insert(dto); } @@ -53,6 +60,12 @@ export class SaleQuotationTemplateService { */ async update(id: number, data: Partial): Promise { await this.entityManager.transaction(async manager => { + const isDuplicated = await this.saleQuotationTemplateRepository.exist({ + where: { name: data.name, id: Not(id) } + }); + if (isDuplicated) { + throw new BusinessException(ErrorEnum.SALE_QUOTATION_TEMPLATE_NAME_DUPLICATE); + } await manager.update(SaleQuotationTemplateEntity, id, { ...data });