diff --git a/.env.development b/.env.development index b33890f..c330ac4 100644 --- a/.env.development +++ b/.env.development @@ -32,4 +32,5 @@ SMTP_HOST = smtp.163.com SMTP_PORT = 465 SMTP_USER = nest_admin@163.com SMTP_PASS = VIPLLOIPMETTROYU + \ No newline at end of file diff --git a/ecosystem.config.js b/ecosystem.config.js index 4b329d9..a52b8ef 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -10,7 +10,7 @@ module.exports = { autorestart: true, exec_mode: 'cluster', watch: false, - instances: cpuLen, + instances: cpuLen, max_memory_restart: '520M', args: '', env: { diff --git a/src/modules/materials_inventory/materials_inventory.controller.ts b/src/modules/materials_inventory/materials_inventory.controller.ts index c615a32..5e483a5 100644 --- a/src/modules/materials_inventory/materials_inventory.controller.ts +++ b/src/modules/materials_inventory/materials_inventory.controller.ts @@ -33,9 +33,9 @@ export class MaterialsInventoryController { @ApiOperation({ summary: '导出原材料盘点表' }) @Perm(permissions.EXPORT) async exportMaterialsInventoryCheck( - @Param() dto: MaterialsInventoryExportDto, + @Query() dto: MaterialsInventoryExportDto, @Res() res: FastifyReply - ): Promise { + ): Promise { await this.miService.exportMaterialsInventoryCheck(dto, res); } diff --git a/src/modules/materials_inventory/materials_inventory.dto.ts b/src/modules/materials_inventory/materials_inventory.dto.ts index ad49c69..20bfd68 100644 --- a/src/modules/materials_inventory/materials_inventory.dto.ts +++ b/src/modules/materials_inventory/materials_inventory.dto.ts @@ -29,9 +29,10 @@ export class MaterialsInventoryExportDto { @IsOptional() @IsNumber() projectId: number; - + @ApiProperty({ description: '导出时间YYYY-MM-DD' }) @IsOptional() + @IsArray() @Transform(params => { // 开始和结束时间用的是一月的开始和一月的结束的时分秒 const date = params.value; diff --git a/src/modules/materials_inventory/materials_inventory.module.ts b/src/modules/materials_inventory/materials_inventory.module.ts index f3b42ea..4e9aee3 100644 --- a/src/modules/materials_inventory/materials_inventory.module.ts +++ b/src/modules/materials_inventory/materials_inventory.module.ts @@ -8,12 +8,15 @@ import { MaterialsInOutController } from './in_out/materials_in_out.controller'; import { MaterialsInOutService } from './in_out/materials_in_out.service'; import { MaterialsInOutEntity } from './in_out/materials_in_out.entity'; import { ParamConfigModule } from '../system/param-config/param-config.module'; +import { ProjectModule } from '../project/project.module'; +import { ProjectEntity } from '../project/project.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([MaterialsInventoryEntity, MaterialsInOutEntity]), + TypeOrmModule.forFeature([MaterialsInventoryEntity, MaterialsInOutEntity,ProjectEntity]), ParamConfigModule, - StorageModule + StorageModule, + ProjectModule ], controllers: [MaterialsInventoryController, MaterialsInOutController], providers: [MaterialsInventoryService, MaterialsInOutService] diff --git a/src/modules/materials_inventory/materials_inventory.service.ts b/src/modules/materials_inventory/materials_inventory.service.ts index d33d5d1..73dd3de 100644 --- a/src/modules/materials_inventory/materials_inventory.service.ts +++ b/src/modules/materials_inventory/materials_inventory.service.ts @@ -12,48 +12,234 @@ import { Pagination } from '~/helper/paginate/pagination'; import { FastifyReply } from 'fastify'; import { paginate } from '~/helper/paginate'; import * as ExcelJS from 'exceljs'; +import dayjs from 'dayjs'; +import { MaterialsInOutEntity } from './in_out/materials_in_out.entity'; +import { fieldSearch } from '~/shared/database/field-search'; +import { groupBy, uniqBy } from 'lodash'; +import { MaterialsInOrOutEnum } from '~/constants/enum'; +import { ProjectEntity } from '../project/project.entity'; @Injectable() export class MaterialsInventoryService { + const; constructor( @InjectEntityManager() private entityManager: EntityManager, @InjectRepository(MaterialsInventoryEntity) - private materialsInventoryRepository: Repository + private materialsInventoryRepository: Repository, + @InjectRepository(MaterialsInOutEntity) + private materialsInOutRepository: Repository, + @InjectRepository(ProjectEntity) + private projectRepository: Repository ) {} /** * 导出原材料盘点表 */ - async exportMaterialsInventoryCheck(dto: MaterialsInventoryExportDto, res: FastifyReply) { + async exportMaterialsInventoryCheck( + { time, projectId }: MaterialsInventoryExportDto, + res: FastifyReply + ): Promise { + const ROW_HEIGHT = 20; + const HEADER_FONT_SIZE = 18; const workbook = new ExcelJS.Workbook(); - // 创建一个工作表 - const sheet = workbook.addWorksheet('CDKEY'); - // 设置表头 - sheet.columns = [ - { header: '名称', key: 'name', width: 32 }, - { header: '创建人', key: 'create_user', width: 32 }, - { header: '创建时间', key: 'create_time', width: 32 } - ]; - const data = [ - { - name: '名称', - create_user: 'admin', - create_time: '2023-05-18 12:00:00' - } - ]; - data.forEach(item => { - sheet.addRow({ - name: item.name, - create_user: item.create_user, - create_time: item.create_time - }); - }); + let projects: ProjectEntity[] = []; + if (projectId) { + projects = [await this.projectRepository.findOneBy({ id: projectId })]; + } + // 生成数据 + const sqb = this.materialsInOutRepository + .createQueryBuilder('mio') + .leftJoin('mio.project', 'project') + .leftJoin('mio.product', 'product') + .leftJoin('product.unit', 'unit') + .leftJoin('product.company', 'company') + .addSelect(['project.id','project.name', 'product.name', 'unit.label', 'company.name']) + .where(fieldSearch({ time })) + .andWhere('mio.isDelete = 0'); + if (projectId) { + sqb.andWhere('project.id = :projectId', { projectId }); + } + const data = await sqb.addOrderBy('mio.time', 'DESC').getMany(); + if (!projectId) { + projects = uniqBy( + data.map(item => item.project), + 'id' + ); + } + + for (const project of projects) { + const currentProjectData = data.filter(item => item.projectId === project.id); + const sheet = workbook.addWorksheet(project.name); + sheet.mergeCells('A1:T1'); + // 设置标题 + sheet.getCell('A1').value = '山东矿机华信智能科技有限公司原材料盘点表'; + + // 设置日期 + sheet.mergeCells('A2:B2'); + sheet.getCell('A2').value = `日期:${dayjs(time[0]).format('YYYY年M月')}`; + // 设置表头 + const headers = [ + '库存编号', + '公司名称', + '产品名称', + '单位', + '库存数量', + '单价', + '金额', + '', + '', + '', + '', + '', + '', + '', + '', + '结存数量', + '单价', + '金额', + '经办人', + '领料单号', + '备注' + ]; + sheet.addRow(headers); + sheet.addRow([ + '', + '', + '', + '', + '', + '', + '', + '入库时间', + '数量', + '单价', + '金额', + '出库时间', + '数量', + '单价', + '金额', + '', + '', + '', + '', + '', + '' + ]); + for (let i = 1; i <= 7; i++) { + sheet.mergeCells(`${String.fromCharCode(64 + i)}3:${String.fromCharCode(64 + i)}4`); + } + // 入库 + sheet.mergeCells('H3:K3'); + sheet.getCell('H3').value = '入库'; + + // 出库 + sheet.mergeCells('L3:O3'); + sheet.getCell('L3').value = '出库'; + + for (let i = 8; i <= 15; i++) { + sheet.getCell(`${String.fromCharCode(64 + i)}4`).style.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFFFC000' } + }; + } + + for (let i = 16; i <= 21; i++) { + sheet.mergeCells(`${String.fromCharCode(64 + i)}3:${String.fromCharCode(64 + i)}4`); + } + + // 固定信息样式设定 + sheet.eachRow((row, index) => { + row.alignment = { vertical: 'middle', horizontal: 'center' }; + row.font = { bold: true }; + row.height = ROW_HEIGHT; + if (index >= 3) { + row.eachCell(cell => { + cell.border = { + top: { style: 'thin' }, + left: { style: 'thin' }, + bottom: { style: 'thin' }, + right: { style: 'thin' } + }; + }); + } + }); + + const groupedData = groupBy(currentProjectData, 'inventoryNumber'); + let number = 0; + for (const key in groupedData) { + // 目前暂定逻辑出库只有一次或者没有出库。不会对一个入库的记录多次出库,故而用find。 + const inRecord = groupedData[key].find(item => item.inOrOut === MaterialsInOrOutEnum.In); + const outRecord = groupedData[key].find(item => item.inOrOut === MaterialsInOrOutEnum.Out); + number++; + sheet.addRow([ + `${inRecord.inventoryNumber}`, + inRecord.product.company.name, + inRecord.product.name, + inRecord.product.unit.label, + '0', + '0', + '0', + inRecord.time, + inRecord.quantity, + parseFloat(`${inRecord.unitPrice || 0}`), + parseFloat(`${inRecord.amount || 0}`), + outRecord?.time || '', + outRecord?.quantity || '', + parseFloat(`${outRecord.unitPrice || 0}`), + parseFloat(`${outRecord.amount || 0}`), + '0', + '0', + '0', + outRecord?.agent, + outRecord?.issuanceNumber, + outRecord?.remark + ]); + } + sheet.getCell('A1').font = { size: HEADER_FONT_SIZE }; + + // 固定信息样式设定 + sheet.eachRow((row, index) => { + if (index >= 5) { + 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' } + }; + }); + } + }); + + sheet.columns.forEach((column, index: number) => { + let maxColumnLength = 0; + const autoWidth = ['B', 'C', 'U']; + if (String.fromCharCode(65 + index) === 'B') maxColumnLength = 20; + if (autoWidth.includes(String.fromCharCode(65 + index))) { + column.eachCell({ includeEmpty: true }, (cell, rowIndex) => { + if (rowIndex >= 5) { + const columnLength = `${cell.value || ''}`.length; + if (columnLength > maxColumnLength) { + maxColumnLength = columnLength; + } + } + }); + column.width = maxColumnLength < 12 ? 12 : maxColumnLength; // Minimum width of 10 + } else { + column.width = 12; + } + }); + } + //读取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() + '.xlsx')}"` + `attachment; filename="${encodeURIComponent('导出_excel' + new Date().getTime() + '.xls')}"` ) .send(buffer); }