feat: 产品根据项目进行库存数量维护完成

This commit is contained in:
louis 2024-03-11 13:41:41 +08:00
parent 5d9ce4cef2
commit b07434e536
7 changed files with 259 additions and 93 deletions

View File

@ -57,6 +57,7 @@ export enum ErrorEnum {
// Contract // Contract
CONTRACT_NUMBER_EXIST = '1407:存在相同的合同编号', CONTRACT_NUMBER_EXIST = '1407:存在相同的合同编号',
// Inventory 库存不足 // Inventory
INVENTORY_INSUFFICIENT = '1408:库存不足' INVENTORY_INSUFFICIENT = '1408:库存不足',
MATERIALS_IN_OUT_NOT_FOUND = '1409:出入库信息不存在',
} }

View File

@ -18,6 +18,7 @@ import { ParamConfigEntity } from '~/modules/system/param-config/param-config.en
import { MaterialsInOrOutEnum, ParamConfigEnum } from '~/constants/enum'; import { MaterialsInOrOutEnum, ParamConfigEnum } from '~/constants/enum';
import { MaterialsInventoryEntity } from '../materials_inventory.entity'; import { MaterialsInventoryEntity } from '../materials_inventory.entity';
import { MaterialsInventoryService } from '../materials_inventory.service'; import { MaterialsInventoryService } from '../materials_inventory.service';
import { isDefined } from 'class-validator';
@Injectable() @Injectable()
export class MaterialsInOutService { export class MaterialsInOutService {
@ -83,8 +84,8 @@ export class MaterialsInOutService {
* *
*/ */
async create(dto: MaterialsInOutDto): Promise<void> { async create(dto: MaterialsInOutDto): Promise<void> {
let { inOrOut, inventoryNumber } = dto; let { inOrOut, inventoryNumber, projectId } = dto;
if (inOrOut === MaterialsInOrOutEnum.In) { if (Object.is(inOrOut, MaterialsInOrOutEnum.In)) {
// 入库 // 入库
inventoryNumber = await this.generateInventoryNumber(); inventoryNumber = await this.generateInventoryNumber();
} else { } else {
@ -100,12 +101,16 @@ export class MaterialsInOutService {
await this.entityManager.transaction(async manager => { await this.entityManager.transaction(async manager => {
// 1.生成出入库记录 // 1.生成出入库记录
const { productId, quantity } = await manager.create(MaterialsInOutEntity, { const { productId, quantity, unitPrice } = await manager.save(MaterialsInOutEntity, {
...this.materialsInOutRepository.create(dto), ...this.materialsInOutRepository.create(dto),
inventoryNumber inventoryNumber
}); });
// 2.更新库存 // 2.更新增减库存
await this.materialsInventoryService.inInventory({ productId, inQuantity: quantity }); 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<MaterialsInOutUpdateDto>): Promise<void> { async update(id: number, { fileIds, ...data }: Partial<MaterialsInOutUpdateDto>): Promise<void> {
await this.entityManager.transaction(async manager => { await this.entityManager.transaction(async manager => {
const entity = await manager.findOne(MaterialsInOutEntity, {
where: {
id
},
lock: { mode: 'pessimistic_write' }
});
await manager.update(MaterialsInOutEntity, id, { await manager.update(MaterialsInOutEntity, id, {
...data ...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) { if (fileIds?.length) {
const count = await this.storageRepository const count = await this.storageRepository
.createQueryBuilder('storage') .createQueryBuilder('storage')
@ -139,6 +187,34 @@ export class MaterialsInOutService {
* *
*/ */
async delete(id: number): Promise<void> { async delete(id: number): Promise<void> {
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 }); await this.materialsInOutRepository.update(id, { isDelete: 1 });
} }

View File

@ -1,9 +1,19 @@
import { ApiProperty } from '@nestjs/swagger'; 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 { CommonEntity } from '~/common/entity/common.entity';
import { ProductEntity } from '../product/product.entity';
import { ProjectEntity } from '../project/project.entity';
@Entity({ name: 'materials_inventory' }) @Entity({ name: 'materials_inventory' })
export class MaterialsInventoryEntity extends CommonEntity { export class MaterialsInventoryEntity extends CommonEntity {
@Column({
name: 'project_id',
type: 'int',
comment: '项目'
})
@ApiProperty({ description: '项目' })
projectId: number;
@Column({ @Column({
name: 'product_id', name: 'product_id',
type: 'int', type: 'int',
@ -24,9 +34,9 @@ export class MaterialsInventoryEntity extends CommonEntity {
@Column({ @Column({
name: 'unit_price', name: 'unit_price',
type: 'decimal', type: 'decimal',
precision: 10, precision: 15,
default: 0, default: 0,
scale: 2, scale: 10,
comment: '库存产品单价' comment: '库存产品单价'
}) })
@ApiProperty({ description: '库存产品单价' }) @ApiProperty({ description: '库存产品单价' })
@ -39,4 +49,12 @@ export class MaterialsInventoryEntity extends CommonEntity {
@Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' })
@ApiProperty({ description: '删除状态0未删除1已删除' }) @ApiProperty({ description: '删除状态0未删除1已删除' })
isDelete: number; isDelete: number;
@ManyToOne(() => ProjectEntity)
@JoinColumn({ name: 'project_id' })
project: ProjectEntity;
@ManyToOne(() => ProductEntity)
@JoinColumn({ name: 'product_id' })
product: ProductEntity;
} }

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm';
import { MaterialsInventoryEntity } from './materials_inventory.entity'; import { MaterialsInventoryEntity } from './materials_inventory.entity';
import { EntityManager, Repository } from 'typeorm'; import { EntityManager, In, MoreThan, Repository } from 'typeorm';
import { import {
MaterialsInventoryDto, MaterialsInventoryDto,
MaterialsInventoryExportDto, MaterialsInventoryExportDto,
@ -15,7 +15,7 @@ import * as ExcelJS from 'exceljs';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { MaterialsInOutEntity } from './in_out/materials_in_out.entity'; import { MaterialsInOutEntity } from './in_out/materials_in_out.entity';
import { fieldSearch } from '~/shared/database/field-search'; import { fieldSearch } from '~/shared/database/field-search';
import { groupBy, uniqBy } from 'lodash'; import { groupBy, sum, uniqBy } from 'lodash';
import { MaterialsInOrOutEnum } from '~/constants/enum'; import { MaterialsInOrOutEnum } from '~/constants/enum';
import { ProjectEntity } from '../project/project.entity'; import { ProjectEntity } from '../project/project.entity';
import { calcNumber } from '~/utils'; import { calcNumber } from '~/utils';
@ -48,6 +48,13 @@ export class MaterialsInventoryService {
if (projectId) { if (projectId) {
projects = [await this.projectRepository.findOneBy({ id: 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 const sqb = this.materialsInOutRepository
.createQueryBuilder('mio') .createQueryBuilder('mio')
@ -56,12 +63,15 @@ export class MaterialsInventoryService {
.leftJoin('product.unit', 'unit') .leftJoin('product.unit', 'unit')
.leftJoin('product.company', 'company') .leftJoin('product.company', 'company')
.addSelect(['project.id', 'project.name', 'product.name', 'unit.label', 'company.name']) .addSelect(['project.id', 'project.name', 'product.name', 'unit.label', 'company.name'])
.where(fieldSearch({ time })) .where({
time: MoreThan(time[0])
})
.andWhere('mio.isDelete = 0'); .andWhere('mio.isDelete = 0');
if (projectId) { if (projectId) {
sqb.andWhere('project.id = :projectId', { projectId }); sqb.andWhere('project.id = :projectId', { projectId });
} }
const data = await sqb.addOrderBy('mio.time', 'DESC').getMany(); const data = await sqb.addOrderBy('mio.time', 'DESC').getMany();
if (!projectId) { if (!projectId) {
projects = uniqBy( projects = uniqBy(
@ -71,12 +81,19 @@ export class MaterialsInventoryService {
} }
for (const project of projects) { 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 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); const sheet = workbook.addWorksheet(project.name);
sheet.mergeCells('A1:T1'); sheet.mergeCells('A1:T1');
// 设置标题 // 设置标题
sheet.getCell('A1').value = '山东矿机华信智能科技有限公司原材料盘点表'; sheet.getCell('A1').value = '山东矿机华信智能科技有限公司原材料盘点表';
// 设置日期 // 设置日期
sheet.mergeCells('A2:B2'); sheet.mergeCells('A2:B2');
sheet.getCell('A2').value = `日期:${dayjs(time[0]).format('YYYY年M月')}`; sheet.getCell('A2').value = `日期:${dayjs(time[0]).format('YYYY年M月')}`;
@ -168,21 +185,53 @@ export class MaterialsInventoryService {
} }
}); });
const groupedData = groupBy<MaterialsInOutEntity>(currentProjectData, 'inventoryNumber'); const groupedData = groupBy<MaterialsInOutEntity>(currentMonthProjectData, 'inventoryNumber');
let number = 0; let number = 0;
const groupedInventories = groupBy(
currentProjectInventories,
item => `${item.projectId}_${item.productId}`
);
for (const key in groupedData) { for (const key in groupedData) {
// 目前暂定逻辑出库只有一次或者没有出库。不会对一个入库的记录多次出库,故而用find。 // 目前暂定逻辑出库只有一次或者没有出库。不会对一个入库的记录多次出库,故而用find。
const inRecord = groupedData[key].find(item => item.inOrOut === MaterialsInOrOutEnum.In); const inRecord = groupedData[key].find(item => item.inOrOut === MaterialsInOrOutEnum.In);
const outRecord = groupedData[key].find(item => item.inOrOut === MaterialsInOrOutEnum.Out); 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++; number++;
sheet.addRow([ sheet.addRow([
`${inRecord.inventoryNumber}`, `${inRecord.inventoryNumber || ''}`,
inRecord.product.company.name, inRecord.product.company.name || '',
inRecord.product.name, inRecord.product.name || '',
inRecord.product.unit.label, inRecord.product.unit.label || '',
'0', currentQuantity,
'0', parseFloat(`${inRecord.unitPrice || 0}`),
'0', calcNumber(currentQuantity, inRecord.unitPrice || 0, 'multiply'),
inRecord.time, inRecord.time,
inRecord.quantity, inRecord.quantity,
parseFloat(`${inRecord.unitPrice || 0}`), parseFloat(`${inRecord.unitPrice || 0}`),
@ -191,12 +240,12 @@ export class MaterialsInventoryService {
outRecord?.quantity || '', outRecord?.quantity || '',
parseFloat(`${outRecord.unitPrice || 0}`), parseFloat(`${outRecord.unitPrice || 0}`),
parseFloat(`${outRecord.amount || 0}`), parseFloat(`${outRecord.amount || 0}`),
'0', balanceQuantity,
'0', parseFloat(`${outRecord.unitPrice || 0}`),
'0', calcNumber(balanceQuantity, outRecord.unitPrice || 0, 'multiply'),
outRecord?.agent, outRecord?.agent || '',
outRecord?.issuanceNumber, outRecord?.issuanceNumber || '',
outRecord?.remark outRecord?.remark || ''
]); ]);
} }
sheet.getCell('A1').font = { size: HEADER_FONT_SIZE }; sheet.getCell('A1').font = { size: HEADER_FONT_SIZE };
@ -256,8 +305,12 @@ export class MaterialsInventoryService {
}: MaterialsInventoryQueryDto): Promise<Pagination<MaterialsInventoryEntity>> { }: MaterialsInventoryQueryDto): Promise<Pagination<MaterialsInventoryEntity>> {
const queryBuilder = this.materialsInventoryRepository const queryBuilder = this.materialsInventoryRepository
.createQueryBuilder('materialsInventory') .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'); .where('materialsInventory.isDelete = 0');
return paginate<MaterialsInventoryEntity>(queryBuilder, { return paginate<MaterialsInventoryEntity>(queryBuilder, {
page, page,
pageSize pageSize
@ -283,78 +336,82 @@ export class MaterialsInventoryService {
} }
/** /**
* *
* @param data ID,ID和入库数量和单价
* @param manager
*/ */
async inInventory(data: { async inInventory(
productId: number; data: {
inQuantity: number; projectId: number;
unitPrice?: number; productId: number;
}): Promise<void> { quantity: number;
const { productId, inQuantity, unitPrice } = data; unitPrice?: number;
},
manager: EntityManager
): Promise<void> {
const { projectId, productId, quantity: inQuantity, unitPrice } = data;
await this.entityManager.transaction(async manager => { const exsitedInventory = await manager.findOne(MaterialsInventoryEntity, {
const exsitedInventory = await this.materialsInventoryRepository.findOne({ where: { projectId, productId }, // 查出某个项目的某个产品的库存情况
where: { productId }, lock: { mode: 'pessimistic_write' } // 开启悲观行锁,防止脏读和修改
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
});
}); });
}
/** // 若不存在库存,直接新增库存
* if (!exsitedInventory) {
*/ await manager.insert(MaterialsInventoryEntity, {
async outInventory(data: { productId: number; outQuantity: number }): Promise<void> { projectId,
const { productId, outQuantity } = data; productId,
unitPrice,
await this.entityManager.transaction(async manager => { quantity: inQuantity
// 开启悲观行锁,防止脏读和修改
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
}); });
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<void> {
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
});
}
/** /**
* *
*/ */

View File

@ -8,6 +8,11 @@ export class ProductDto {
@IsString() @IsString()
name: string; name: string;
@ApiProperty({ description: '产品备注' })
@IsOptional()
@IsString()
remark: string;
@ApiProperty({ description: '单位(字典)' }) @ApiProperty({ description: '单位(字典)' })
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()

View File

@ -26,6 +26,15 @@ export class ProductEntity extends CommonEntity {
@ApiProperty({ description: '产品名称' }) @ApiProperty({ description: '产品名称' })
name: string; name: string;
@Column({
name: 'remark',
type: 'varchar',
length: 255,
comment: '备注'
})
@ApiProperty({ description: '产品备注' })
remark: string;
@Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' }) @Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' })
@ApiProperty({ description: '删除状态0未删除1已删除' }) @ApiProperty({ description: '删除状态0未删除1已删除' })
isDelete: number; isDelete: number;

View File

@ -67,10 +67,10 @@ export function calcNumber(
return add(bignumber(firstNumber), bignumber(secondNumber)).toNumber(); return add(bignumber(firstNumber), bignumber(secondNumber)).toNumber();
case 'subtract': case 'subtract':
return subtract(bignumber(firstNumber), bignumber(secondNumber)).toNumber(); return subtract(bignumber(firstNumber), bignumber(secondNumber)).toNumber();
// case 'multiply': case 'multiply':
// return multiply(bignumber(firstNumber), bignumber(secondNumber)); return (multiply(bignumber(firstNumber), bignumber(secondNumber)) as BigNumber).toNumber();
// case 'divide': case 'divide':
// return divide(bignumber(firstNumber), bignumber(secondNumber)); return (divide(bignumber(firstNumber), bignumber(secondNumber)) as BigNumber).toNumber();
default: default:
return 0; return 0;
} }