diff --git a/src/constants/error-code.constant.ts b/src/constants/error-code.constant.ts index 688571a..beb178b 100644 --- a/src/constants/error-code.constant.ts +++ b/src/constants/error-code.constant.ts @@ -57,6 +57,7 @@ export enum ErrorEnum { // Contract CONTRACT_NUMBER_EXIST = '1407:存在相同的合同编号', - // Inventory 库存不足 - INVENTORY_INSUFFICIENT = '1408:库存不足' + // Inventory + INVENTORY_INSUFFICIENT = '1408:库存不足', + MATERIALS_IN_OUT_NOT_FOUND = '1409:出入库信息不存在', } diff --git a/src/modules/materials_inventory/in_out/materials_in_out.service.ts b/src/modules/materials_inventory/in_out/materials_in_out.service.ts index 7bff520..aea3e81 100644 --- a/src/modules/materials_inventory/in_out/materials_in_out.service.ts +++ b/src/modules/materials_inventory/in_out/materials_in_out.service.ts @@ -18,6 +18,7 @@ import { ParamConfigEntity } from '~/modules/system/param-config/param-config.en import { MaterialsInOrOutEnum, ParamConfigEnum } from '~/constants/enum'; import { MaterialsInventoryEntity } from '../materials_inventory.entity'; import { MaterialsInventoryService } from '../materials_inventory.service'; +import { isDefined } from 'class-validator'; @Injectable() export class MaterialsInOutService { @@ -83,8 +84,8 @@ export class MaterialsInOutService { * 新增 */ async create(dto: MaterialsInOutDto): Promise { - let { inOrOut, inventoryNumber } = dto; - if (inOrOut === MaterialsInOrOutEnum.In) { + let { inOrOut, inventoryNumber, projectId } = dto; + if (Object.is(inOrOut, MaterialsInOrOutEnum.In)) { // 入库 inventoryNumber = await this.generateInventoryNumber(); } else { @@ -100,12 +101,16 @@ export class MaterialsInOutService { await this.entityManager.transaction(async manager => { // 1.生成出入库记录 - const { productId, quantity } = await manager.create(MaterialsInOutEntity, { + const { productId, quantity, unitPrice } = await manager.save(MaterialsInOutEntity, { ...this.materialsInOutRepository.create(dto), inventoryNumber }); - // 2.更新库存 - await this.materialsInventoryService.inInventory({ productId, inQuantity: quantity }); + // 2.更新增减库存 + await ( + Object.is(inOrOut, MaterialsInOrOutEnum.In) + ? this.materialsInventoryService.inInventory + : this.materialsInventoryService.outInventory + )({ productId, quantity, unitPrice, projectId }, manager); }); } @@ -114,9 +119,52 @@ export class MaterialsInOutService { */ async update(id: number, { fileIds, ...data }: Partial): Promise { await this.entityManager.transaction(async manager => { + const entity = await manager.findOne(MaterialsInOutEntity, { + where: { + id + }, + lock: { mode: 'pessimistic_write' } + }); await manager.update(MaterialsInOutEntity, id, { ...data }); + let changedQuantity = 0; + if (isDefined(data.quantity) && entity.quantity !== data.quantity) { + if (entity.inOrOut === MaterialsInOrOutEnum.In) { + // 入库减少等于出库 + if (data.quantity - entity.quantity < 0) { + data.inOrOut = MaterialsInOrOutEnum.Out; + } else { + // 入库增多等于入库 + data.inOrOut = MaterialsInOrOutEnum.In; + } + } else { + // 出库减少等于入库 + if (data.quantity - entity.quantity < 0) { + data.inOrOut = MaterialsInOrOutEnum.In; + } else { + // 出库增多等于出库 + data.inOrOut = MaterialsInOrOutEnum.Out; + } + } + changedQuantity = Math.abs(data.quantity - entity.quantity); + } + // 2.更新增减库存 + if (changedQuantity !== 0) { + await ( + Object.is(data.inOrOut, MaterialsInOrOutEnum.In) + ? this.materialsInventoryService.inInventory + : this.materialsInventoryService.outInventory + )( + { + productId: entity.productId, + quantity: Math.abs(changedQuantity), + unitPrice: undefined, + projectId: entity.projectId + }, + manager + ); + } if (fileIds?.length) { const count = await this.storageRepository .createQueryBuilder('storage') @@ -139,6 +187,34 @@ export class MaterialsInOutService { * 删除 */ async delete(id: number): Promise { + await this.entityManager.transaction(async manager => { + const entity = await manager.findOne(MaterialsInOutEntity, { + where: { + id, + isDelete: 0 + }, + lock: { mode: 'pessimistic_write' } + }); + if (!entity) { + throw new BusinessException(ErrorEnum.MATERIALS_IN_OUT_NOT_FOUND); + } + + // 更新库存 + await ( + Object.is(entity.inOrOut, MaterialsInOrOutEnum.In) + ? this.materialsInventoryService.outInventory + : this.materialsInventoryService.inInventory + )( + { + productId: entity.productId, + quantity: entity.quantity, + unitPrice: undefined, + projectId: entity.projectId + }, + manager + ); + }); + // 出入库比较重要,做逻辑删除 await this.materialsInOutRepository.update(id, { isDelete: 1 }); } diff --git a/src/modules/materials_inventory/materials_inventory.entity.ts b/src/modules/materials_inventory/materials_inventory.entity.ts index 0d7fc29..b8c509a 100644 --- a/src/modules/materials_inventory/materials_inventory.entity.ts +++ b/src/modules/materials_inventory/materials_inventory.entity.ts @@ -1,9 +1,19 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Column, Entity, JoinTable, ManyToMany, Relation } from 'typeorm'; +import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, Relation } from 'typeorm'; import { CommonEntity } from '~/common/entity/common.entity'; +import { ProductEntity } from '../product/product.entity'; +import { ProjectEntity } from '../project/project.entity'; @Entity({ name: 'materials_inventory' }) export class MaterialsInventoryEntity extends CommonEntity { + @Column({ + name: 'project_id', + type: 'int', + comment: '项目' + }) + @ApiProperty({ description: '项目' }) + projectId: number; + @Column({ name: 'product_id', type: 'int', @@ -24,9 +34,9 @@ export class MaterialsInventoryEntity extends CommonEntity { @Column({ name: 'unit_price', type: 'decimal', - precision: 10, + precision: 15, default: 0, - scale: 2, + scale: 10, comment: '库存产品单价' }) @ApiProperty({ description: '库存产品单价' }) @@ -39,4 +49,12 @@ export class MaterialsInventoryEntity extends CommonEntity { @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) @ApiProperty({ description: '删除状态:0未删除,1已删除' }) isDelete: number; + + @ManyToOne(() => ProjectEntity) + @JoinColumn({ name: 'project_id' }) + project: ProjectEntity; + + @ManyToOne(() => ProductEntity) + @JoinColumn({ name: 'product_id' }) + product: ProductEntity; } diff --git a/src/modules/materials_inventory/materials_inventory.service.ts b/src/modules/materials_inventory/materials_inventory.service.ts index 4033086..c305d57 100644 --- a/src/modules/materials_inventory/materials_inventory.service.ts +++ b/src/modules/materials_inventory/materials_inventory.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; import { MaterialsInventoryEntity } from './materials_inventory.entity'; -import { EntityManager, Repository } from 'typeorm'; +import { EntityManager, In, MoreThan, Repository } from 'typeorm'; import { MaterialsInventoryDto, MaterialsInventoryExportDto, @@ -15,7 +15,7 @@ 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 { groupBy, sum, uniqBy } from 'lodash'; import { MaterialsInOrOutEnum } from '~/constants/enum'; import { ProjectEntity } from '../project/project.entity'; import { calcNumber } from '~/utils'; @@ -48,6 +48,13 @@ export class MaterialsInventoryService { if (projectId) { projects = [await this.projectRepository.findOneBy({ id: projectId })]; } + // 查询出项目产品所属的当前库存 + const inventoriesInProjects = await this.materialsInventoryRepository.find({ + where: { + ...(projects?.length ? { projectId: In(projects.map(item => item.id)) } : null) + } + }); + // 生成数据 const sqb = this.materialsInOutRepository .createQueryBuilder('mio') @@ -56,12 +63,15 @@ export class MaterialsInventoryService { .leftJoin('product.unit', 'unit') .leftJoin('product.company', 'company') .addSelect(['project.id', 'project.name', 'product.name', 'unit.label', 'company.name']) - .where(fieldSearch({ time })) + .where({ + time: MoreThan(time[0]) + }) .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( @@ -71,12 +81,19 @@ export class MaterialsInventoryService { } for (const project of projects) { + const currentProjectInventories = inventoriesInProjects.filter(({ projectId }) => + Object.is(projectId, project.id) + ); const currentProjectData = data.filter(item => item.projectId === project.id); + const currentMonthProjectData = currentProjectData.filter(item => { + return ( + dayjs(item.time).isAfter(dayjs(time[0])) && dayjs(item.time).isBefore(dayjs(time[1])) + ); + }); 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月')}`; @@ -168,21 +185,53 @@ export class MaterialsInventoryService { } }); - const groupedData = groupBy(currentProjectData, 'inventoryNumber'); + const groupedData = groupBy(currentMonthProjectData, 'inventoryNumber'); let number = 0; + const groupedInventories = groupBy( + currentProjectInventories, + item => `${item.projectId}_${item.productId}` + ); 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); + const currInventories = + groupedInventories[`${inRecord.projectId}_${inRecord.productId}`]?.shift(); + const allDataFromMonth = data.filter( + res => res.projectId === inRecord.projectId && res.productId === inRecord.productId + ); + let currentQuantity = 0; + let balanceQuantity = 0; + // 月初库存数量 + if (currInventories) { + const sumIn = sum( + allDataFromMonth + .filter(res => Object.is(res.inOrOut, MaterialsInOrOutEnum.In)) + .map(item => item.quantity) + ); + const sumOut = sum( + allDataFromMonth + .filter(res => Object.is(res.inOrOut, MaterialsInOrOutEnum.Out)) + .map(item => item.quantity) + ); + const sumDistance = calcNumber(sumIn, sumOut, 'subtract'); + currentQuantity = calcNumber(currInventories.quantity, sumDistance, 'subtract'); + } + // 结存库存数量 + balanceQuantity = calcNumber( + currentQuantity, + calcNumber(inRecord.quantity, outRecord.quantity, 'subtract'), + 'add' + ); number++; sheet.addRow([ - `${inRecord.inventoryNumber}`, - inRecord.product.company.name, - inRecord.product.name, - inRecord.product.unit.label, - '0', - '0', - '0', + `${inRecord.inventoryNumber || ''}`, + inRecord.product.company.name || '', + inRecord.product.name || '', + inRecord.product.unit.label || '', + currentQuantity, + parseFloat(`${inRecord.unitPrice || 0}`), + calcNumber(currentQuantity, inRecord.unitPrice || 0, 'multiply'), inRecord.time, inRecord.quantity, parseFloat(`${inRecord.unitPrice || 0}`), @@ -191,12 +240,12 @@ export class MaterialsInventoryService { outRecord?.quantity || '', parseFloat(`${outRecord.unitPrice || 0}`), parseFloat(`${outRecord.amount || 0}`), - '0', - '0', - '0', - outRecord?.agent, - outRecord?.issuanceNumber, - outRecord?.remark + balanceQuantity, + parseFloat(`${outRecord.unitPrice || 0}`), + calcNumber(balanceQuantity, outRecord.unitPrice || 0, 'multiply'), + outRecord?.agent || '', + outRecord?.issuanceNumber || '', + outRecord?.remark || '' ]); } sheet.getCell('A1').font = { size: HEADER_FONT_SIZE }; @@ -256,8 +305,12 @@ export class MaterialsInventoryService { }: MaterialsInventoryQueryDto): Promise> { const queryBuilder = this.materialsInventoryRepository .createQueryBuilder('materialsInventory') + .leftJoin('materialsInventory.project', 'project') + .leftJoin('materialsInventory.product', 'product') + .leftJoin('product.unit', 'unit') + .leftJoin('product.company', 'company') + .addSelect(['project.name', 'product.name', 'unit.label', 'company.name']) .where('materialsInventory.isDelete = 0'); - return paginate(queryBuilder, { page, pageSize @@ -283,78 +336,82 @@ export class MaterialsInventoryService { } /** - * 产品入库 + * 产品入库后计算最新库存 + * @param data 传入项目ID,产品ID和入库数量和单价 + * @param manager 传入事务对象防止开启多重事务 */ - async inInventory(data: { - productId: number; - inQuantity: number; - unitPrice?: number; - }): Promise { - const { productId, inQuantity, unitPrice } = data; + async inInventory( + data: { + projectId: number; + productId: number; + quantity: number; + unitPrice?: number; + }, + manager: EntityManager + ): Promise { + const { projectId, productId, quantity: inQuantity, unitPrice } = data; - await this.entityManager.transaction(async manager => { - const exsitedInventory = await this.materialsInventoryRepository.findOne({ - where: { productId }, - lock: { mode: 'pessimistic_write' } // 开启悲观行锁,防止脏读和修改 - }); - - // 若不存在库存,直接新增库存 - if (!exsitedInventory) { - await this.entityManager.transaction(async manager => { - if (exsitedInventory) { - await manager.insert(MaterialsInventoryEntity, { - productId, - unitPrice, - quantity: inQuantity - }); - } - }); - return; - } - // 若存在库存,则库存增加 - let { quantity, id } = exsitedInventory; - const newQuantity = calcNumber(quantity || 0, inQuantity || 0, 'add'); - if (isNaN(newQuantity)) { - throw new Error('库存数量不合法'); - } - await manager.update(MaterialsInventoryEntity, id, { - quantity: newQuantity - }); + const exsitedInventory = await manager.findOne(MaterialsInventoryEntity, { + where: { projectId, productId }, // 查出某个项目的某个产品的库存情况 + lock: { mode: 'pessimistic_write' } // 开启悲观行锁,防止脏读和修改 }); - } - /** - * 产品入库 - */ - async outInventory(data: { productId: number; outQuantity: number }): Promise { - const { productId, outQuantity } = data; - - await this.entityManager.transaction(async manager => { - // 开启悲观行锁,防止脏读和修改 - const inventory = await this.materialsInventoryRepository.findOne({ - where: { productId }, - lock: { mode: 'pessimistic_write' } - }); - // 检查库存剩余 - if (inventory.quantity < outQuantity) { - throw new BusinessException(ErrorEnum.INVENTORY_INSUFFICIENT); - } - // 库存充足,可以出库 - let { quantity, id } = inventory; - const newQuantity = calcNumber(quantity || 0, outQuantity || 0, 'subtract'); - if (isNaN(newQuantity)) { - throw new Error('库存数量不合法'); - } - await manager.update(MaterialsInventoryEntity, id, { - quantity: newQuantity + // 若不存在库存,直接新增库存 + if (!exsitedInventory) { + await manager.insert(MaterialsInventoryEntity, { + projectId, + productId, + unitPrice, + quantity: inQuantity }); + return; + } + // 若该项目存在库存,则该项目该产品的库存增加 + let { quantity, id } = exsitedInventory; + const newQuantity = calcNumber(quantity || 0, inQuantity || 0, 'add'); + if (isNaN(newQuantity)) { + throw new Error('库存数量不合法'); + } + await manager.update(MaterialsInventoryEntity, id, { + quantity: newQuantity }); } /** * 产品出库 + * @param data 传入产品id和入库数量和单价 + * @param manager 传入事务对象防止开启多重事务 */ + async outInventory( + data: { + projectId: number; + productId: number; + quantity: number; + unitPrice?: number; + }, + manager: EntityManager + ): Promise { + const { projectId, productId, quantity: outQuantity } = data; + // 开启悲观行锁,防止脏读和修改 + const inventory = await manager.findOne(MaterialsInventoryEntity, { + where: { projectId, productId }, + lock: { mode: 'pessimistic_write' } + }); + // 检查库存剩余 + if (inventory.quantity < outQuantity) { + throw new BusinessException(ErrorEnum.INVENTORY_INSUFFICIENT); + } + // 若该项目的该产品库存充足,则该项目该产品的库存减少 + let { quantity, id } = inventory; + const newQuantity = calcNumber(quantity || 0, outQuantity || 0, 'subtract'); + if (isNaN(newQuantity)) { + throw new Error('库存数量不合法'); + } + await manager.update(MaterialsInventoryEntity, id, { + quantity: newQuantity + }); + } /** * 删除 */ diff --git a/src/modules/product/product.dto.ts b/src/modules/product/product.dto.ts index 4824c24..e8b4b08 100644 --- a/src/modules/product/product.dto.ts +++ b/src/modules/product/product.dto.ts @@ -8,6 +8,11 @@ export class ProductDto { @IsString() name: string; + @ApiProperty({ description: '产品备注' }) + @IsOptional() + @IsString() + remark: string; + @ApiProperty({ description: '单位(字典)' }) @IsOptional() @IsNumber() diff --git a/src/modules/product/product.entity.ts b/src/modules/product/product.entity.ts index 0c6aa58..6b0ea0b 100644 --- a/src/modules/product/product.entity.ts +++ b/src/modules/product/product.entity.ts @@ -26,6 +26,15 @@ export class ProductEntity extends CommonEntity { @ApiProperty({ description: '产品名称' }) name: string; + @Column({ + name: 'remark', + type: 'varchar', + length: 255, + comment: '备注' + }) + @ApiProperty({ description: '产品备注' }) + remark: string; + @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) @ApiProperty({ description: '删除状态:0未删除,1已删除' }) isDelete: number; diff --git a/src/utils/tool.util.ts b/src/utils/tool.util.ts index 9320ca9..5f8afbd 100644 --- a/src/utils/tool.util.ts +++ b/src/utils/tool.util.ts @@ -67,10 +67,10 @@ export function calcNumber( return add(bignumber(firstNumber), bignumber(secondNumber)).toNumber(); case 'subtract': return subtract(bignumber(firstNumber), bignumber(secondNumber)).toNumber(); - // case 'multiply': - // return multiply(bignumber(firstNumber), bignumber(secondNumber)); - // case 'divide': - // return divide(bignumber(firstNumber), bignumber(secondNumber)); + case 'multiply': + return (multiply(bignumber(firstNumber), bignumber(secondNumber)) as BigNumber).toNumber(); + case 'divide': + return (divide(bignumber(firstNumber), bignumber(secondNumber)) as BigNumber).toNumber(); default: return 0; }