From c815e5d561b67c1cc9850fb78b4c1f0ac9483b5b Mon Sep 17 00:00:00 2001 From: louis <869322496@qq.com> Date: Fri, 12 Apr 2024 09:55:02 +0800 Subject: [PATCH] feat: sale_quotation --- src/app.module.ts | 6 +- src/constants/error-code.constant.ts | 5 +- src/modules/project/project.service.ts | 5 - .../sale_quotation_component.controller.ts | 69 ++++++++ .../component/sale_quotation_component.dto.ts | 59 +++++++ .../sale_quotation_component.entity.ts | 85 +++++++++ .../sale_quotation_component.module.ts | 14 ++ .../sale_quotation_component.service.ts | 161 ++++++++++++++++++ .../group/sale_quotation_group.controller.ts | 63 +++++++ .../group/sale_quotation_group.dto.ts | 27 +++ .../group/sale_quotation_group.entity.ts | 33 ++++ .../group/sale_quotation_group.module.ts | 14 ++ .../group/sale_quotation_group.service.ts | 83 +++++++++ .../sale_quotation/sale_quotation.module.ts | 25 +++ .../sale_quotation_template.controller.ts | 63 +++++++ .../template/sale_quotation_template.dto.ts | 29 ++++ .../sale_quotation_template.entity.ts | 32 ++++ .../sale_quotation_template.module.ts | 14 ++ .../sale_quotation_template.service.ts | 83 +++++++++ src/modules/tools/storage/storage.entity.ts | 6 + 20 files changed, 869 insertions(+), 7 deletions(-) create mode 100644 src/modules/sale_quotation/component/sale_quotation_component.controller.ts create mode 100644 src/modules/sale_quotation/component/sale_quotation_component.dto.ts create mode 100644 src/modules/sale_quotation/component/sale_quotation_component.entity.ts create mode 100644 src/modules/sale_quotation/component/sale_quotation_component.module.ts create mode 100644 src/modules/sale_quotation/component/sale_quotation_component.service.ts create mode 100644 src/modules/sale_quotation/group/sale_quotation_group.controller.ts create mode 100644 src/modules/sale_quotation/group/sale_quotation_group.dto.ts create mode 100644 src/modules/sale_quotation/group/sale_quotation_group.entity.ts create mode 100644 src/modules/sale_quotation/group/sale_quotation_group.module.ts create mode 100644 src/modules/sale_quotation/group/sale_quotation_group.service.ts create mode 100644 src/modules/sale_quotation/sale_quotation.module.ts create mode 100644 src/modules/sale_quotation/template/sale_quotation_template.controller.ts create mode 100644 src/modules/sale_quotation/template/sale_quotation_template.dto.ts create mode 100644 src/modules/sale_quotation/template/sale_quotation_template.entity.ts create mode 100644 src/modules/sale_quotation/template/sale_quotation_template.module.ts create mode 100644 src/modules/sale_quotation/template/sale_quotation_template.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 28b5c6d..992ae61 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -30,6 +30,7 @@ import { CompanyModule } from './modules/company/company.module'; import { ProductModule } from './modules/product/product.module'; import { ProjectModule } from './modules/project/project.module'; import { VehicleUsageModule } from './modules/vehicle_usage/vehicle_usage.module'; +import { SaleQuotationModule } from './modules/sale_quotation/sale_quotation.module'; @Module({ imports: [ @@ -73,7 +74,10 @@ import { VehicleUsageModule } from './modules/vehicle_usage/vehicle_usage.module ProjectModule, // 车辆管理 - VehicleUsageModule + VehicleUsageModule, + + //报价管理 + SaleQuotationModule ], providers: [ { provide: APP_FILTER, useClass: AllExceptionsFilter }, diff --git a/src/constants/error-code.constant.ts b/src/constants/error-code.constant.ts index 40cacd8..fd27d49 100644 --- a/src/constants/error-code.constant.ts +++ b/src/constants/error-code.constant.ts @@ -61,5 +61,8 @@ export enum ErrorEnum { INVENTORY_INSUFFICIENT = '1408:库存数量不足。请检查库存或重新操作', MATERIALS_IN_OUT_NOT_FOUND = '1409:出入库信息不存在', MATERIALS_IN_OUT_UNIT_PRICE_CANNOT_BE_MODIFIED = '1410:该价格的产品已经出库,单价不允许修改。若有疑问,请联系管理员', - MATERIALS_IN_OUT_UNIT_PRICE_MUST_ZERO_WHEN_MODIFIED = '1411:只能修改初始单价为0的入库记录。 若有疑问,请联系管理员' + MATERIALS_IN_OUT_UNIT_PRICE_MUST_ZERO_WHEN_MODIFIED = '1411:只能修改初始单价为0的入库记录。 若有疑问,请联系管理员', + + // SaleQuotation + SALE_QUOTATION_COMPONENT_DUPLICATED = '1412:存在名称,价格,规格都相同的配件,请检查是否重复录入' } diff --git a/src/modules/project/project.service.ts b/src/modules/project/project.service.ts index fcd4dd0..4b56820 100644 --- a/src/modules/project/project.service.ts +++ b/src/modules/project/project.service.ts @@ -56,11 +56,6 @@ export class ProjectService { await manager.update(ProjectEntity, id, { ...data }); - const project = await this.projectRepository - .createQueryBuilder('project') - .leftJoinAndSelect('project.files', 'files') - .where('project.id = :id', { id }) - .getOne(); if (fileIds?.length) { const count = await this.storageRepository .createQueryBuilder('storage') diff --git a/src/modules/sale_quotation/component/sale_quotation_component.controller.ts b/src/modules/sale_quotation/component/sale_quotation_component.controller.ts new file mode 100644 index 0000000..76d1fa5 --- /dev/null +++ b/src/modules/sale_quotation/component/sale_quotation_component.controller.ts @@ -0,0 +1,69 @@ +import { Body, Controller, Get, Query, Put, Delete, Post } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { SaleQuotationComponentDto, SaleQuotationComponentQueryDto, SaleQuotationComponentUpdateDto } from './sale_quotation_component.dto'; +import { SaleQuotationComponentService } from './sale_quotation_component.service'; +export const permissions = definePermission('sale_quotation:sale_quotation_component', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('SaleQuotationComponent - 报价配件') +@ApiSecurityAuth() +@Controller('sale_quotation_component') +export class SaleQuotationComponentController { + constructor(private saleQuotationComponentService: SaleQuotationComponentService) {} + + @Get() + @ApiOperation({ summary: '分页获取报价配件列表' }) + @ApiResult({ type: [SaleQuotationComponentDto], isPage: true }) + @Perm(permissions.LIST) + async list(@Query() dto: SaleQuotationComponentQueryDto) { + return this.saleQuotationComponentService.findAll(dto); + } + + @Get(':id') + @ApiOperation({ summary: '获取报价配件信息' }) + @ApiResult({ type: SaleQuotationComponentDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.saleQuotationComponentService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增报价配件' }) + @Perm(permissions.CREATE) + async create(@Body() dto: SaleQuotationComponentDto): Promise { + await this.saleQuotationComponentService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新报价配件' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: SaleQuotationComponentUpdateDto): Promise { + await this.saleQuotationComponentService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除报价配件' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.saleQuotationComponentService.delete(id); + } + + @Put('unlink-attachments/:id') + @ApiOperation({ summary: '附件解除关联' }) + @Perm(permissions.UPDATE) + async unlinkAttachments( + @IdParam() id: number, + @Body() { fileIds }: SaleQuotationComponentUpdateDto + ): Promise { + await this.saleQuotationComponentService.unlinkAttachments(id, fileIds); + } +} diff --git a/src/modules/sale_quotation/component/sale_quotation_component.dto.ts b/src/modules/sale_quotation/component/sale_quotation_component.dto.ts new file mode 100644 index 0000000..f0c6e94 --- /dev/null +++ b/src/modules/sale_quotation/component/sale_quotation_component.dto.ts @@ -0,0 +1,59 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { IsUnique } from '~/shared/database/constraints/unique.constraint'; +import { SaleQuotationComponentEntity } from './sale_quotation_component.entity'; + +export class SaleQuotationComponentDto { + @ApiProperty({ description: '报价配件名称' }) + @IsString() + name: string; + + @ApiProperty({ description: '配件规格' }) + @IsOptional() + @IsString() + componentSpecification: string; + + @ApiProperty({ description: '配件备注' }) + @IsOptional() + @IsString() + remark: string; + + @ApiProperty({ description: '单位(字典)' }) + @IsOptional() + @IsNumber() + unitId: number; + + @ApiProperty({ description: '单价' }) + @IsOptional() + @IsNumber() + unitPrice: number; + + @ApiProperty({ description: '附件' }) + files: Storage[]; +} + +export class SaleQuotationComponentUpdateDto extends PartialType(SaleQuotationComponentDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} + +export class ComapnyCreateDto extends PartialType(SaleQuotationComponentDto) { + @ApiProperty({ description: '附件' }) + @IsOptional() + @IsArray() + fileIds: number[]; +} + +export class SaleQuotationComponentQueryDto extends IntersectionType( + PagerDto, + PartialType(SaleQuotationComponentDto) +) { + @ApiProperty({ description: '报价配件名称' }) + @IsOptional() + @IsString() + name: string; +} diff --git a/src/modules/sale_quotation/component/sale_quotation_component.entity.ts b/src/modules/sale_quotation/component/sale_quotation_component.entity.ts new file mode 100644 index 0000000..5834376 --- /dev/null +++ b/src/modules/sale_quotation/component/sale_quotation_component.entity.ts @@ -0,0 +1,85 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { + Column, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, + Relation +} from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { SaleQuotationGroupEntity } from '../group/sale_quotation_group.entity'; +import { DictItemEntity } from '~/modules/system/dict-item/dict-item.entity'; + +/** + * 报价配件实体类 + */ +@Entity({ name: 'sale_quotation_component' }) +export class SaleQuotationComponentEntity extends CommonEntity { + @Column({ + name: 'name', + type: 'varchar', + length: 255, + comment: '报价配件名称' + }) + @ApiProperty({ description: '报价配件名称' }) + name: string; + + @Column({ + name: 'component_specification', + type: 'varchar', + nullable: true, + length: 255, + comment: '产品规格' + }) + @ApiProperty({ description: '产品规格', nullable: true }) + componentSpecification?: string; + + @Column({ name: 'unit_id', type: 'int', comment: '单位(字典)', nullable: true }) + @ApiProperty({ description: '单位(字典)' }) + unitId: number; + + @ManyToOne(() => DictItemEntity) + @JoinColumn({ name: 'unit_id' }) + unit: DictItemEntity; + + @Column({ + name: 'unit_price', + type: 'decimal', + precision: 15, + default: 0, + scale: 10, + comment: '单价' + }) + @ApiProperty({ description: '单价' }) + unitPrice: number; + + @Column({ + name: 'remark', + type: 'varchar', + length: 255, + nullable: true, + comment: '备注' + }) + @ApiProperty({ description: '产品备注' }) + remark: string; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; + + @ManyToMany(() => Storage, storage => storage.saleQuotationComponents) + @JoinTable({ + name: 'sale_quotation_component_storage', + joinColumn: { name: 'sale_quotation_component_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } + }) + files: Relation; + + @ApiHideProperty() + @ManyToMany(() => SaleQuotationGroupEntity, group => group.components) + groups: Relation; +} diff --git a/src/modules/sale_quotation/component/sale_quotation_component.module.ts b/src/modules/sale_quotation/component/sale_quotation_component.module.ts new file mode 100644 index 0000000..5954eac --- /dev/null +++ b/src/modules/sale_quotation/component/sale_quotation_component.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StorageModule } from '~/modules/tools/storage/storage.module'; +import { DatabaseModule } from '~/shared/database/database.module'; +import { SaleQuotationComponentService } from './sale_quotation_component.service'; +import { SaleQuotationComponentController } from './sale_quotation_component.controller'; +import { SaleQuotationComponentEntity } from './sale_quotation_component.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([SaleQuotationComponentEntity]), StorageModule, DatabaseModule], + controllers: [SaleQuotationComponentController], + providers: [SaleQuotationComponentService] +}) +export class SaleQuotationComponentModule {} diff --git a/src/modules/sale_quotation/component/sale_quotation_component.service.ts b/src/modules/sale_quotation/component/sale_quotation_component.service.ts new file mode 100644 index 0000000..4fd80a9 --- /dev/null +++ b/src/modules/sale_quotation/component/sale_quotation_component.service.ts @@ -0,0 +1,161 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/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'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { fieldSearch } from '~/shared/database/field-search'; +import { SaleQuotationComponentEntity } from './sale_quotation_component.entity'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { + SaleQuotationComponentDto, + SaleQuotationComponentQueryDto, + SaleQuotationComponentUpdateDto +} from './sale_quotation_component.dto'; + +@Injectable() +export class SaleQuotationComponentService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(SaleQuotationComponentEntity) + private saleQuotationComponentRepository: Repository, + @InjectRepository(Storage) + private storageRepository: Repository + ) {} + + buildSearchQuery() { + return this.saleQuotationComponentRepository + .createQueryBuilder('saleQuotationComponent') + .leftJoin('saleQuotationComponent.unit', 'unit') + .leftJoin('saleQuotationComponent.files', 'files') + .addSelect(['files.id', 'files.path', 'unit.id', 'unit.label']); + } + + /** + * 分页查询所有 + */ + async findAll({ + page, + pageSize, + ...fields + }: SaleQuotationComponentQueryDto): Promise> { + const queryBuilder = this.buildSearchQuery() + .where(fieldSearch(fields)) + .andWhere('saleQuotationComponent.isDelete = 0'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create(dto: SaleQuotationComponentDto): Promise { + const { unitPrice, name, componentSpecification } = dto; + const isExist = await this.saleQuotationComponentRepository.exist({ + where: { + unitPrice, + name, + componentSpecification + } + }); + if (isExist) { + throw new BusinessException(ErrorEnum.SALE_QUOTATION_COMPONENT_DUPLICATED); + } + await this.saleQuotationComponentRepository.insert(dto); + } + + /** + * 更新 + */ + async update( + id: number, + { fileIds, ...data }: Partial + ): Promise { + await this.entityManager.transaction(async manager => { + const beUpdateEntity = await manager.findOne(SaleQuotationComponentEntity, { + where: { + id + } + }); + const { unitPrice, name, componentSpecification } = beUpdateEntity; + const isExist = await this.saleQuotationComponentRepository.exist({ + where: { + id: Not(id), + unitPrice, + name, + componentSpecification + } + }); + if (isExist) { + throw new BusinessException(ErrorEnum.SALE_QUOTATION_COMPONENT_DUPLICATED); + } + await manager.update(SaleQuotationComponentEntity, id, { + ...data + }); + if (fileIds?.length) { + const count = await this.storageRepository + .createQueryBuilder('storage') + .where('storage.id in(:fileIds)', { fileIds }) + .getCount(); + if (count !== fileIds?.length) { + throw new BusinessException(ErrorEnum.STORAGE_NOT_FOUND); + } + // 附件要批量插入 + await manager + .createQueryBuilder() + .relation(SaleQuotationComponentEntity, 'files') + .of(id) + .add(fileIds); + } + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + // 比较重要,做逻辑删除 + await this.saleQuotationComponentRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个信息 + */ + async info(id: number) { + const info = await this.buildSearchQuery() + .where({ + id + }) + .andWhere('saleQuotationComponent.isDelete = 0') + .getOne(); + return info; + } + + /** + * 解除附件关联 + * @param id 实体ID + * @param fileIds 附件ID + */ + async unlinkAttachments(id: number, fileIds: number[]) { + await this.entityManager.transaction(async manager => { + const saleQuotationComponent = await this.saleQuotationComponentRepository + .createQueryBuilder('saleQuotationComponent') + .leftJoinAndSelect('saleQuotationComponent.files', 'files') + .where('saleQuotationComponent.id = :id', { id }) + .getOne(); + const linkedFiles = saleQuotationComponent.files + .map(item => item.id) + .filter(item => !fileIds.includes(item)); + // 附件要批量更新 + await manager + .createQueryBuilder() + .relation(SaleQuotationComponentEntity, 'files') + .of(id) + .addAndRemove(linkedFiles, saleQuotationComponent.files); + }); + } +} diff --git a/src/modules/sale_quotation/group/sale_quotation_group.controller.ts b/src/modules/sale_quotation/group/sale_quotation_group.controller.ts new file mode 100644 index 0000000..ebf1e2b --- /dev/null +++ b/src/modules/sale_quotation/group/sale_quotation_group.controller.ts @@ -0,0 +1,63 @@ +import { Body, Controller, Get, Query, Put, Delete, Post } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { + SaleQuotationGroupDto, + SaleQuotationGroupQueryDto, + SaleQuotationGroupUpdateDto +} from './sale_quotation_group.dto'; +import { SaleQuotationGroupService } from './sale_quotation_group.service'; +export const permissions = definePermission('sale_quotation:sale_quotation_group', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('SaleQuotationGroup - 报价分组') +@ApiSecurityAuth() +@Controller('sale_quotation_group') +export class SaleQuotationGroupController { + constructor(private saleQuotationGroupService: SaleQuotationGroupService) {} + + @Get() + @ApiOperation({ summary: '分页获取报价分组列表' }) + @ApiResult({ type: [SaleQuotationGroupDto], isPage: true }) + @Perm(permissions.LIST) + async list(@Query() dto: SaleQuotationGroupQueryDto) { + return this.saleQuotationGroupService.findAll(dto); + } + + @Get(':id') + @ApiOperation({ summary: '获取报价分组信息' }) + @ApiResult({ type: SaleQuotationGroupDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.saleQuotationGroupService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增报价分组' }) + @Perm(permissions.CREATE) + async create(@Body() dto: SaleQuotationGroupDto): Promise { + await this.saleQuotationGroupService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新报价分组' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: SaleQuotationGroupUpdateDto): Promise { + await this.saleQuotationGroupService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除报价分组' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.saleQuotationGroupService.delete(id); + } +} diff --git a/src/modules/sale_quotation/group/sale_quotation_group.dto.ts b/src/modules/sale_quotation/group/sale_quotation_group.dto.ts new file mode 100644 index 0000000..9b5d6f8 --- /dev/null +++ b/src/modules/sale_quotation/group/sale_quotation_group.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { IsUnique } from '~/shared/database/constraints/unique.constraint'; +import { SaleQuotationGroupEntity } from './sale_quotation_group.entity'; + +export class SaleQuotationGroupDto { + @ApiProperty({ description: '报价分组名称' }) + @IsUnique(SaleQuotationGroupEntity, { message: '已存在同名报价分组' }) + @IsString() + name: string; +} + +export class SaleQuotationGroupUpdateDto extends PartialType(SaleQuotationGroupDto) {} + +export class ComapnyCreateDto extends PartialType(SaleQuotationGroupDto) {} + +export class SaleQuotationGroupQueryDto extends IntersectionType( + PagerDto, + PartialType(SaleQuotationGroupDto) +) { + @ApiProperty({ description: '报价分组名称' }) + @IsOptional() + @IsString() + name: string; +} diff --git a/src/modules/sale_quotation/group/sale_quotation_group.entity.ts b/src/modules/sale_quotation/group/sale_quotation_group.entity.ts new file mode 100644 index 0000000..4f4038e --- /dev/null +++ b/src/modules/sale_quotation/group/sale_quotation_group.entity.ts @@ -0,0 +1,33 @@ +import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'; +import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, Relation } from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { SaleQuotationComponentEntity } from '../component/sale_quotation_component.entity'; + +/** + * 报价分组实体类 + */ +@Entity({ name: 'sale_quotation_group' }) +export class SaleQuotationGroupEntity extends CommonEntity { + @Column({ + name: 'name', + type: 'varchar', + unique: true, + length: 255, + comment: '报价分组名称' + }) + @ApiProperty({ description: '报价分组名称' }) + name: string; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; + + @ManyToMany(() => SaleQuotationComponentEntity, component => component.groups) + @JoinTable({ + name: 'sale_quotation_group_component', + joinColumn: { name: 'group_id', referencedColumnName: 'id' }, + inverseJoinColumn: { name: 'component_id', referencedColumnName: 'id' } + }) + components: Relation; +} diff --git a/src/modules/sale_quotation/group/sale_quotation_group.module.ts b/src/modules/sale_quotation/group/sale_quotation_group.module.ts new file mode 100644 index 0000000..a5cc146 --- /dev/null +++ b/src/modules/sale_quotation/group/sale_quotation_group.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StorageModule } from '~/modules/tools/storage/storage.module'; +import { DatabaseModule } from '~/shared/database/database.module'; +import { SaleQuotationGroupService } from './sale_quotation_group.service'; +import { SaleQuotationGroupController } from './sale_quotation_group.controller'; +import { SaleQuotationGroupEntity } from './sale_quotation_group.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([SaleQuotationGroupEntity]), DatabaseModule], + controllers: [SaleQuotationGroupController], + providers: [SaleQuotationGroupService] +}) +export class SaleQuotationGroupModule {} diff --git a/src/modules/sale_quotation/group/sale_quotation_group.service.ts b/src/modules/sale_quotation/group/sale_quotation_group.service.ts new file mode 100644 index 0000000..25fc1af --- /dev/null +++ b/src/modules/sale_quotation/group/sale_quotation_group.service.ts @@ -0,0 +1,83 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, Repository } from 'typeorm'; +import { Pagination } from '~/helper/paginate/pagination'; +import { paginate } from '~/helper/paginate'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { fieldSearch } from '~/shared/database/field-search'; +import { SaleQuotationGroupEntity } from './sale_quotation_group.entity'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { + SaleQuotationGroupDto, + SaleQuotationGroupQueryDto, + SaleQuotationGroupUpdateDto +} from './sale_quotation_group.dto'; + +@Injectable() +export class SaleQuotationGroupService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(SaleQuotationGroupEntity) + private saleQuotationGroupRepository: Repository + ) {} + + /** + * 分页查询所有 + */ + async findAll({ + page, + pageSize, + ...fields + }: SaleQuotationGroupQueryDto): Promise> { + const queryBuilder = this.saleQuotationGroupRepository + .createQueryBuilder('saleQuotationGroup') + .where(fieldSearch(fields)) + .andWhere('saleQuotationGroup.isDelete = 0'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create(dto: SaleQuotationGroupDto): Promise { + await this.saleQuotationGroupRepository.insert(dto); + } + + /** + * 更新 + */ + async update(id: number, data: Partial): Promise { + await this.entityManager.transaction(async manager => { + await manager.update(SaleQuotationGroupEntity, id, { + ...data + }); + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + // 比较重要,做逻辑删除 + await this.saleQuotationGroupRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个信息 + */ + async info(id: number) { + const info = await this.saleQuotationGroupRepository + .createQueryBuilder('saleQuotationGroup') + .where({ + id + }) + .andWhere('saleQuotationGroup.isDelete = 0') + .getOne(); + return info; + } +} diff --git a/src/modules/sale_quotation/sale_quotation.module.ts b/src/modules/sale_quotation/sale_quotation.module.ts new file mode 100644 index 0000000..a928e8e --- /dev/null +++ b/src/modules/sale_quotation/sale_quotation.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +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'; +const modules = [ + SaleQuotationComponentModule, + SaleQuotationGroupModule, + SaleQuotationTemplateModule +]; +@Module({ + imports: [ + ...modules, + RouterModule.register([ + { + path: 'sale_quotation', + module: SaleQuotationModule, + children: [...modules] + } + ]) + ], + controllers: [], + providers: [] +}) +export class SaleQuotationModule {} diff --git a/src/modules/sale_quotation/template/sale_quotation_template.controller.ts b/src/modules/sale_quotation/template/sale_quotation_template.controller.ts new file mode 100644 index 0000000..18a9d1a --- /dev/null +++ b/src/modules/sale_quotation/template/sale_quotation_template.controller.ts @@ -0,0 +1,63 @@ +import { Body, Controller, Get, Query, Put, Delete, Post } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'; +import { ApiResult } from '~/common/decorators/api-result.decorator'; +import { IdParam } from '~/common/decorators/id-param.decorator'; +import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'; +import { + SaleQuotationTemplateDto, + SaleQuotationTemplateQueryDto, + SaleQuotationTemplateUpdateDto +} from './sale_quotation_template.dto'; +import { SaleQuotationTemplateService } from './sale_quotation_template.service'; +export const permissions = definePermission('sale_quotation:sale_quotation_template', { + LIST: 'list', + CREATE: 'create', + READ: 'read', + UPDATE: 'update', + DELETE: 'delete' +} as const); + +@ApiTags('SaleQuotationTemplate - 报价模板') +@ApiSecurityAuth() +@Controller('sale_quotation_template') +export class SaleQuotationTemplateController { + constructor(private saleQuotationTemplateService: SaleQuotationTemplateService) {} + + @Get() + @ApiOperation({ summary: '分页获取报价模板列表' }) + @ApiResult({ type: [SaleQuotationTemplateDto], isPage: true }) + @Perm(permissions.LIST) + async list(@Query() dto: SaleQuotationTemplateQueryDto) { + return this.saleQuotationTemplateService.findAll(dto); + } + + @Get(':id') + @ApiOperation({ summary: '获取报价模板信息' }) + @ApiResult({ type: SaleQuotationTemplateDto }) + @Perm(permissions.READ) + async info(@IdParam() id: number) { + return this.saleQuotationTemplateService.info(id); + } + + @Post() + @ApiOperation({ summary: '新增报价模板' }) + @Perm(permissions.CREATE) + async create(@Body() dto: SaleQuotationTemplateDto): Promise { + await this.saleQuotationTemplateService.create(dto); + } + + @Put(':id') + @ApiOperation({ summary: '更新报价模板' }) + @Perm(permissions.UPDATE) + async update(@IdParam() id: number, @Body() dto: SaleQuotationTemplateUpdateDto): Promise { + await this.saleQuotationTemplateService.update(id, dto); + } + + @Delete(':id') + @ApiOperation({ summary: '删除报价模板' }) + @Perm(permissions.DELETE) + async delete(@IdParam() id: number): Promise { + await this.saleQuotationTemplateService.delete(id); + } +} diff --git a/src/modules/sale_quotation/template/sale_quotation_template.dto.ts b/src/modules/sale_quotation/template/sale_quotation_template.dto.ts new file mode 100644 index 0000000..b916fd7 --- /dev/null +++ b/src/modules/sale_quotation/template/sale_quotation_template.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty, IntersectionType, PartialType } from '@nestjs/swagger'; +import { IsOptional, IsString } from 'class-validator'; +import { PagerDto } from '~/common/dto/pager.dto'; +import { IsUnique } from '~/shared/database/constraints/unique.constraint'; +import { SaleQuotationTemplateEntity } from './sale_quotation_template.entity'; + +export class SaleQuotationTemplateDto { + @ApiProperty({ description: '报价模板名称' }) + @IsString() + name: string; + + @ApiProperty({ description: '报价模板' }) + @IsOptional() + template: JSON; +} + +export class SaleQuotationTemplateUpdateDto extends PartialType(SaleQuotationTemplateDto) {} + +export class ComapnyCreateDto extends PartialType(SaleQuotationTemplateDto) {} + +export class SaleQuotationTemplateQueryDto extends IntersectionType( + PagerDto, + PartialType(SaleQuotationTemplateDto) +) { + @ApiProperty({ description: '报价模板名称' }) + @IsOptional() + @IsString() + name: string; +} diff --git a/src/modules/sale_quotation/template/sale_quotation_template.entity.ts b/src/modules/sale_quotation/template/sale_quotation_template.entity.ts new file mode 100644 index 0000000..9e2aabe --- /dev/null +++ b/src/modules/sale_quotation/template/sale_quotation_template.entity.ts @@ -0,0 +1,32 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Column, Entity } from 'typeorm'; +import { CommonEntity } from '~/common/entity/common.entity'; + +/** + * 报价模板实体类 + */ +@Entity({ name: 'sale_quotation_template' }) +export class SaleQuotationTemplateEntity extends CommonEntity { + @Column({ + name: 'name', + type: 'varchar', + unique: true, + length: 255, + comment: '报价模板名称' + }) + @ApiProperty({ description: '报价模板名称' }) + name: string; + + @Column({ + name: 'template', + type: 'json', + comment: '报价模板', + nullable: true + }) + @ApiProperty({ description: '报价模板(JSON)' }) + template: JSON; + + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) + @ApiProperty({ description: '删除状态:0未删除,1已删除' }) + isDelete: number; +} diff --git a/src/modules/sale_quotation/template/sale_quotation_template.module.ts b/src/modules/sale_quotation/template/sale_quotation_template.module.ts new file mode 100644 index 0000000..2759ef7 --- /dev/null +++ b/src/modules/sale_quotation/template/sale_quotation_template.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { StorageModule } from '~/modules/tools/storage/storage.module'; +import { DatabaseModule } from '~/shared/database/database.module'; +import { SaleQuotationTemplateService } from './sale_quotation_template.service'; +import { SaleQuotationTemplateController } from './sale_quotation_template.controller'; +import { SaleQuotationTemplateEntity } from './sale_quotation_template.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([SaleQuotationTemplateEntity]), DatabaseModule], + controllers: [SaleQuotationTemplateController], + 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 new file mode 100644 index 0000000..5ff5633 --- /dev/null +++ b/src/modules/sale_quotation/template/sale_quotation_template.service.ts @@ -0,0 +1,83 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, Repository } from 'typeorm'; +import { Pagination } from '~/helper/paginate/pagination'; +import { paginate } from '~/helper/paginate'; +import { BusinessException } from '~/common/exceptions/biz.exception'; +import { ErrorEnum } from '~/constants/error-code.constant'; +import { fieldSearch } from '~/shared/database/field-search'; +import { SaleQuotationTemplateEntity } from './sale_quotation_template.entity'; +import { Storage } from '~/modules/tools/storage/storage.entity'; +import { + SaleQuotationTemplateDto, + SaleQuotationTemplateQueryDto, + SaleQuotationTemplateUpdateDto +} from './sale_quotation_template.dto'; + +@Injectable() +export class SaleQuotationTemplateService { + constructor( + @InjectEntityManager() private entityManager: EntityManager, + @InjectRepository(SaleQuotationTemplateEntity) + private saleQuotationTemplateRepository: Repository + ) {} + + /** + * 分页查询所有 + */ + async findAll({ + page, + pageSize, + ...fields + }: SaleQuotationTemplateQueryDto): Promise> { + const queryBuilder = this.saleQuotationTemplateRepository + .createQueryBuilder('saleQuotationTemplate') + .where(fieldSearch(fields)) + .andWhere('saleQuotationTemplate.isDelete = 0').addOrderBy('saleQuotationTemplate.createdAt', 'DESC'); + + return paginate(queryBuilder, { + page, + pageSize + }); + } + + /** + * 新增 + */ + async create(dto: SaleQuotationTemplateDto): Promise { + await this.saleQuotationTemplateRepository.insert(dto); + } + + /** + * 更新 + */ + async update(id: number, data: Partial): Promise { + await this.entityManager.transaction(async manager => { + await manager.update(SaleQuotationTemplateEntity, id, { + ...data + }); + }); + } + + /** + * 删除 + */ + async delete(id: number): Promise { + // 比较重要,做逻辑删除 + await this.saleQuotationTemplateRepository.update(id, { isDelete: 1 }); + } + + /** + * 获取单个信息 + */ + async info(id: number) { + const info = await this.saleQuotationTemplateRepository + .createQueryBuilder('saleQuotationTemplate') + .where({ + id + }) + .andWhere('saleQuotationTemplate.isDelete = 0') + .getOne(); + return info; + } +} diff --git a/src/modules/tools/storage/storage.entity.ts b/src/modules/tools/storage/storage.entity.ts index 6929c0e..ecdbad9 100644 --- a/src/modules/tools/storage/storage.entity.ts +++ b/src/modules/tools/storage/storage.entity.ts @@ -8,6 +8,7 @@ import { MaterialsInOutEntity } from '~/modules/materials_inventory/in_out/mater import { MaterialsInventoryEntity } from '~/modules/materials_inventory/materials_inventory.entity'; import { ProductEntity } from '~/modules/product/product.entity'; import { ProjectEntity } from '~/modules/project/project.entity'; +import { SaleQuotationComponentEntity } from '~/modules/sale_quotation/component/sale_quotation_component.entity'; import { VehicleUsageEntity } from '~/modules/vehicle_usage/vehicle_usage.entity'; @Entity({ name: 'tool_storage' }) @@ -77,4 +78,9 @@ export class Storage extends CommonEntity { @ApiHideProperty() @ManyToMany(() => VehicleUsageEntity, vu => vu.files) vehicleUsage: Relation; + + @ApiHideProperty() + @ManyToMany(() => SaleQuotationComponentEntity, component => component.files) + saleQuotationComponents: Relation; + }