feat: inventory

This commit is contained in:
louis 2024-03-27 14:11:11 +08:00
parent cab022af0a
commit ce54da72c9
12 changed files with 263 additions and 88 deletions

View File

@ -26,7 +26,7 @@ export class BusinessException extends HttpException {
code,
message
}),
HttpStatus.OK
HttpStatus.BAD_REQUEST
);
this.errorCode = Number(code);

View File

@ -21,8 +21,9 @@ export enum MaterialsInOrOutEnum {
// 系统参数key
export enum ParamConfigEnum {
InventoryNumberPrefixIn = 'inventory_number_prefix_in',
InventoryNumberPrefixOut = 'inventory_number_prefix_out',
InventoryNumberPrefix = 'inventory_number_prefix',
InventoryInOutNumberPrefixIn = 'inventory_inout_number_prefix_in',
InventoryInOutNumberPrefixOut = 'inventory_inout_number_prefix_out',
ProductNumberPrefix = 'product_number_prefix'
}
@ -32,3 +33,10 @@ export enum ContractStatusEnum {
Approved = 1, // 已通过
Rejected = 2 // 已拒绝
}
// 库存查询剩余咋黄台
export enum HasInventoryStatusEnum {
All = 0, // 全部
Yes = 1, // 有库存
No = 2 // 无库存
}

View File

@ -1,4 +1,4 @@
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { ExecutionContext, HttpException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { FastifyRequest } from 'fastify';
@ -72,7 +72,7 @@ export class JwtAuthGuard extends AuthGuard(AuthStrategy.JWT) {
const pv = await this.authService.getPasswordVersionByUid(request.user.uid);
if (pv !== `${request.user.pv}`) {
// 密码版本不一致,登录期间已更改过密码
throw new BusinessException(ErrorEnum.INVALID_LOGIN);
throw new HttpException(ErrorEnum.INVALID_LOGIN,HttpStatus.UNAUTHORIZED);
}
// 不允许多端登录

View File

@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
@Injectable()
export class BaseService {
generateInventoryNumber(): string {
generateInventoryInOutNumber(): string {
// Generate a random inventory number
return Math.floor(Math.random() * 1000000).toString();
}

View File

@ -6,7 +6,11 @@ import { MaterialsInOutService } from './materials_in_out.service';
import { MaterialsInOutEntity } from './materials_in_out.entity';
import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator';
import { definePermission, Perm } from '~/modules/auth/decorators/permission.decorator';
import { MaterialsInOutQueryDto, MaterialsInOutDto, MaterialsInOutUpdateDto } from './materials_in_out.dto';
import {
MaterialsInOutQueryDto,
MaterialsInOutDto,
MaterialsInOutUpdateDto
} from './materials_in_out.dto';
export const permissions = definePermission('materials_inventory:history_in_out', {
LIST: 'list',
@ -40,8 +44,8 @@ export class MaterialsInOutController {
@Post()
@ApiOperation({ summary: '新增原材料出入库记录' })
@Perm(permissions.CREATE)
async create(@Body() dto: MaterialsInOutDto): Promise<void> {
await this.materialsInOutService.create(dto);
async create(@Body() dto: MaterialsInOutDto): Promise<number> {
return this.materialsInOutService.create(dto);
}
@Put(':id')

View File

@ -29,14 +29,14 @@ export class MaterialsInOutDto {
projectId?: number;
@ApiProperty({ description: '产品Id' })
@ValidateIf(o => !o.inventoryNumber)
@ValidateIf(o => !o.inventoryInOutNumber)
@IsNumber()
productId: number;
@ApiProperty({ description: '原材料库存编号' })
@IsOptional()
@IsString()
inventoryNumber: string;
inventoryInOutNumber: string;
@ApiProperty({ description: '库存id(产品和单价双主键决定一条库存)' })
@IsOptional()
@ -80,6 +80,11 @@ export class MaterialsInOutDto {
@IsString()
issuanceNumber: string;
@ApiProperty({ description: '库存位置' })
@IsOptional()
@IsString()
position: string;
@IsOptional()
@IsString()
@ApiProperty({ description: '备注' })
@ -98,11 +103,12 @@ export class MaterialsInOutQueryDto extends PagerDto<MaterialsInOutQueryDto> {
// @IsString()
@Transform(params => {
// 开始和结束时间用的是一天的开始和一天的结束的时分秒
const date = params.value;
return [
date ? `${formatToDate(dayjs(date).startOf('month'))} 00:00:00` : null,
date ? `${formatToDate(dayjs(date).endOf('month'))} 23:59:59` : null
];
return params.value
? [
params.value[0] ? `${formatToDate(params.value[0], 'YYYY-MM-DD')} 00:00:00` : null,
params.value[1] ? `${formatToDate(params.value[1], 'YYYY-MM-DD')} 23:59:59` : null
]
: [];
})
time?: string[];
@ -129,7 +135,7 @@ export class MaterialsInOutQueryDto extends PagerDto<MaterialsInOutQueryDto> {
@ApiProperty({ description: '原材料库存编号' })
@IsOptional()
@IsString()
inventoryNumber?: string;
inventoryInOutNumber?: string;
@IsOptional()
@IsString()

View File

@ -19,16 +19,17 @@ import { ProductEntity } from '~/modules/product/product.entity';
import { ProjectEntity } from '~/modules/project/project.entity';
import { ParamConfigEntity } from '~/modules/system/param-config/param-config.entity';
import { Storage } from '~/modules/tools/storage/storage.entity';
import { MaterialsInventoryEntity } from '../materials_inventory.entity';
@Entity({ name: 'materials_in_out' })
export class MaterialsInOutEntity extends CommonEntity {
@Column({
name: 'inventory_number',
name: 'inventory_inout_number',
type: 'varchar',
length: 50,
comment: '原材料编号'
comment: '原材料出入库编号'
})
@ApiProperty({ description: '原材料编号' })
inventoryNumber: string;
@ApiProperty({ description: '原材料出入库编号' })
inventoryInOutNumber: string;
@Column({
name: 'product_id',
@ -38,6 +39,14 @@ export class MaterialsInOutEntity extends CommonEntity {
@ApiProperty({ description: '产品' })
productId: number;
@Column({
name: 'inventory_id',
type: 'int',
comment: '库存'
})
@ApiProperty({ description: '库存' })
inventoryId: number;
@Column({
name: 'in_or_out',
type: 'tinyint',
@ -127,4 +136,8 @@ export class MaterialsInOutEntity extends CommonEntity {
inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' }
})
files: Relation<Storage[]>;
@ManyToOne(() => MaterialsInventoryEntity)
@JoinColumn({ name: 'inventory_id' })
inventory: MaterialsInventoryEntity;
}

View File

@ -43,26 +43,7 @@ export class MaterialsInOutService {
isCreateOut,
...ext
}: MaterialsInOutQueryDto): Promise<Pagination<MaterialsInOutEntity>> {
const sqb = this.materialsInOutRepository
.createQueryBuilder('materialsInOut')
.leftJoin('materialsInOut.files', 'files')
.leftJoin('materialsInOut.project', 'project')
.leftJoin('materialsInOut.product', 'product')
.leftJoin('product.unit', 'unit')
.leftJoin('product.files', 'productFiles')
.leftJoin('product.company', 'company')
.addSelect([
'files.id',
'files.path',
'project.name',
'product.name',
'product.productSpecification',
'product.productNumber',
'productFiles.id',
'productFiles.path',
'unit.label',
'company.name'
])
const sqb = this.buildSearchQuery()
.where(fieldSearch(ext))
.andWhere('materialsInOut.isDelete = 0')
.addOrderBy('materialsInOut.createdAt', 'DESC');
@ -85,25 +66,63 @@ export class MaterialsInOutService {
return pageData;
}
buildSearchQuery() {
return this.materialsInOutRepository
.createQueryBuilder('materialsInOut')
.leftJoin('materialsInOut.files', 'files')
.leftJoin('materialsInOut.project', 'project')
.leftJoin('materialsInOut.product', 'product')
.leftJoin('materialsInOut.inventory', 'inventory')
.leftJoin('product.unit', 'unit')
.leftJoin('product.files', 'productFiles')
.leftJoin('product.company', 'company')
.addSelect([
'inventory.id',
'inventory.position',
'files.id',
'files.path',
'project.name',
'product.name',
'product.productSpecification',
'product.productNumber',
'productFiles.id',
'productFiles.path',
'unit.label',
'company.name'
]);
}
/**
*
*/
async create(dto: MaterialsInOutDto): Promise<void> {
let { inOrOut, inventoryNumber, projectId, inventoryId } = dto;
inventoryNumber = await this.generateInventoryNumber(inOrOut);
async create(dto: MaterialsInOutDto): Promise<number> {
let {
inOrOut,
inventoryInOutNumber,
projectId,
inventoryId,
position,
unitPrice,
quantity,
productId
} = dto;
inventoryInOutNumber = await this.generateInventoryInOutNumber(inOrOut);
let newRecordId;
await this.entityManager.transaction(async manager => {
// 1.生成出入库记录
const { productId, quantity, unitPrice } = await manager.save(MaterialsInOutEntity, {
...this.materialsInOutRepository.create(dto),
inventoryNumber
});
// 2.更新增减库存
await (
delete dto.position;
// 1.更新增减库存
const inventoryEntity = await (
Object.is(inOrOut, MaterialsInOrOutEnum.In)
? this.materialsInventoryService.inInventory
: this.materialsInventoryService.outInventory
)({ productId, quantity, unitPrice, projectId, inventoryId }, manager);
? this.materialsInventoryService.inInventory.bind(this.materialsInventoryService)
: this.materialsInventoryService.outInventory.bind(this.materialsInventoryService)
)({ productId, quantity, unitPrice, projectId, inventoryId, position }, manager);
// 2.生成出入库记录
const { id } = await manager.save(MaterialsInOutEntity, {
...this.materialsInOutRepository.create({ ...dto, inventoryId: inventoryEntity?.id }),
inventoryInOutNumber
});
newRecordId = id;
});
return newRecordId;
}
/**
@ -138,8 +157,8 @@ export class MaterialsInOutService {
}
await (
Object.is(data.inOrOut, MaterialsInOrOutEnum.In)
? this.materialsInventoryService.inInventory
: this.materialsInventoryService.outInventory
? this.materialsInventoryService.inInventory.bind(this.materialsInventoryService)
: this.materialsInventoryService.outInventory.bind(this.materialsInventoryService)
)(
{
productId: entity.productId,
@ -189,7 +208,6 @@ export class MaterialsInOutService {
manager
);
}
// 完成所有业务逻辑后,更新出入库记录
await manager.update(MaterialsInOutEntity, id, {
...data
@ -232,8 +250,8 @@ export class MaterialsInOutService {
// 更新库存
await (
Object.is(entity.inOrOut, MaterialsInOrOutEnum.In)
? this.materialsInventoryService.outInventory
: this.materialsInventoryService.inInventory
? this.materialsInventoryService.outInventory.bind(this.materialsInventoryService)
: this.materialsInventoryService.inInventory.bind(this.materialsInventoryService)
)(
{
productId: entity.productId,
@ -253,8 +271,7 @@ export class MaterialsInOutService {
*
*/
async info(id: number) {
const info = await this.materialsInOutRepository
.createQueryBuilder('materialsInOut')
const info = await this.buildSearchQuery()
.where({
id
})
@ -288,30 +305,30 @@ export class MaterialsInOutService {
}
/**
*
* @returns
*
* @returns
*/
async generateInventoryNumber(inOrOut: MaterialsInOrOutEnum = MaterialsInOrOutEnum.In) {
async generateInventoryInOutNumber(inOrOut: MaterialsInOrOutEnum = MaterialsInOrOutEnum.In) {
const prefix =
(
await this.paramConfigRepository.findOne({
where: {
key: inOrOut
? ParamConfigEnum.InventoryNumberPrefixOut
: ParamConfigEnum.InventoryNumberPrefixIn
? ParamConfigEnum.InventoryInOutNumberPrefixOut
: ParamConfigEnum.InventoryInOutNumberPrefixIn
}
})
)?.value || '';
const lastMaterial = await this.materialsInOutRepository
.createQueryBuilder('materialsInOut')
.select(
`MAX(CAST(REPLACE(materialsInOut.inventoryNumber, '${prefix}', '') AS UNSIGNED))`,
'maxInventoryNumber'
`MAX(CAST(REPLACE(materialsInOut.inventoryInOutNumber, '${prefix}', '') AS UNSIGNED))`,
'maxInventoryInOutNumber'
)
.where('materialsInOut.inOrOut = :inOrOut', { inOrOut })
.getRawOne();
const lastNumber = lastMaterial.maxInventoryNumber
? parseInt(lastMaterial.maxInventoryNumber.replace(prefix, ''))
const lastNumber = lastMaterial.maxInventoryInOutNumber
? parseInt(lastMaterial.maxInventoryInOutNumber.replace(prefix, ''))
: 0;
const newNumber = lastNumber + 1 < 1000 ? 1000 : lastNumber + 1;
return `${prefix}${newNumber}`;

View File

@ -3,6 +3,7 @@ import {
IsArray,
IsDate,
IsDateString,
IsEnum,
IsIn,
IsInt,
IsNumber,
@ -16,6 +17,7 @@ import { Storage } from '../tools/storage/storage.entity';
import { Transform } from 'class-transformer';
import dayjs from 'dayjs';
import { formatToDate } from '~/utils';
import { HasInventoryStatusEnum } from '~/constants/enum';
export class MaterialsInventoryDto {}
@ -23,7 +25,22 @@ export class MaterialsInventoryUpdateDto extends PartialType(MaterialsInventoryD
export class MaterialsInventoryQueryDto extends IntersectionType(
PagerDto<MaterialsInventoryDto>,
PartialType(MaterialsInventoryDto)
) {}
) {
@ApiProperty({ description: '产品名' })
@IsOptional()
@IsString()
product: string;
@ApiProperty({ description: '关键字' })
@IsOptional()
@IsString()
keyword: string;
@ApiProperty({ description: '产品名' })
@IsOptional()
@IsEnum(HasInventoryStatusEnum)
isHasInventory: HasInventoryStatusEnum;
}
export class MaterialsInventoryExportDto {
@ApiProperty({ description: '项目' })
@IsOptional()

View File

@ -1,8 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, Relation } from 'typeorm';
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 { ProductEntity } from '../product/product.entity';
import { ProjectEntity } from '../project/project.entity';
import { MaterialsInOutEntity } from './in_out/materials_in_out.entity';
@Entity({ name: 'materials_inventory' })
export class MaterialsInventoryEntity extends CommonEntity {
@ -22,6 +32,16 @@ export class MaterialsInventoryEntity extends CommonEntity {
@ApiProperty({ description: '产品' })
productId: number;
@Column({
name: 'position',
type: 'varchar',
length: 255,
nullable: true,
comment: '库存位置'
})
@ApiProperty({ description: '库存位置' })
position: string;
@Column({
name: 'quantity',
type: 'int',
@ -57,4 +77,17 @@ export class MaterialsInventoryEntity extends CommonEntity {
@ManyToOne(() => ProductEntity)
@JoinColumn({ name: 'product_id' })
product: ProductEntity;
@Column({
name: 'inventory_number',
type: 'varchar',
length: 50,
comment: '库存编号'
})
@ApiProperty({ description: '库存编号' })
inventoryNumber: string;
@ApiHideProperty()
@OneToMany(() => MaterialsInOutEntity, inout => inout.inventory)
materialsInOuts: Relation<MaterialsInOutEntity[]>;
}

View File

@ -16,14 +16,15 @@ import dayjs from 'dayjs';
import { MaterialsInOutEntity } from './in_out/materials_in_out.entity';
import { fieldSearch } from '~/shared/database/field-search';
import { groupBy, sum, uniqBy } from 'lodash';
import { MaterialsInOrOutEnum } from '~/constants/enum';
import { HasInventoryStatusEnum, MaterialsInOrOutEnum, ParamConfigEnum } from '~/constants/enum';
import { ProjectEntity } from '../project/project.entity';
import { calcNumber } from '~/utils';
import { BusinessException } from '~/common/exceptions/biz.exception';
import { ErrorEnum } from '~/constants/error-code.constant';
import { ParamConfigEntity } from '../system/param-config/param-config.entity';
import { isDefined } from 'class-validator';
@Injectable()
export class MaterialsInventoryService {
const;
constructor(
@InjectEntityManager() private entityManager: EntityManager,
@InjectRepository(MaterialsInventoryEntity)
@ -31,7 +32,9 @@ export class MaterialsInventoryService {
@InjectRepository(MaterialsInOutEntity)
private materialsInOutRepository: Repository<MaterialsInOutEntity>,
@InjectRepository(ProjectEntity)
private projectRepository: Repository<ProjectEntity>
private projectRepository: Repository<ProjectEntity>,
@InjectRepository(ParamConfigEntity)
private paramConfigRepository: Repository<ParamConfigEntity>
) {}
/**
@ -236,7 +239,7 @@ export class MaterialsInventoryService {
);
number++;
sheet.addRow([
`${inRecord.inventoryNumber || ''}`,
`${inRecord.inventoryInOutNumber || ''}`,
inRecord.product.company.name || '',
inRecord.product.name || '',
inRecord.product.unit.label || '',
@ -312,7 +315,10 @@ export class MaterialsInventoryService {
*/
async findAll({
page,
pageSize
pageSize,
product,
keyword,
isHasInventory
}: MaterialsInventoryQueryDto): Promise<Pagination<MaterialsInventoryEntity>> {
const queryBuilder = this.materialsInventoryRepository
.createQueryBuilder('materialsInventory')
@ -322,13 +328,34 @@ export class MaterialsInventoryService {
.leftJoin('product.company', 'company')
.addSelect([
'project.name',
'project.id',
'unit.id',
'unit.label',
'company.id',
'company.name',
'product.id',
'product.name',
'product.productSpecification',
'product.productNumber'
])
.where('materialsInventory.isDelete = 0');
if (product) {
queryBuilder.andWhere('product.name like :product', { product: `%${product}%` });
}
if (keyword) {
queryBuilder.andWhere(
'(materialsInventory.inventoryNumber like :keyword or product.name like :keyword or product.productNumber like :keyword or product.productSpecification like :keyword)',
{
keyword: `%${keyword}%`
}
);
}
if (isHasInventory == HasInventoryStatusEnum.Yes) {
queryBuilder.andWhere('materialsInventory.quantity > 0');
}
if (isHasInventory == HasInventoryStatusEnum.No) {
queryBuilder.andWhere('materialsInventory.quantity = 0');
}
return paginate<MaterialsInventoryEntity>(queryBuilder, {
page,
pageSize
@ -361,6 +388,7 @@ export class MaterialsInventoryService {
*/
async inInventory(
data: {
position?: string;
projectId: number;
productId: number;
quantity: number;
@ -368,8 +396,15 @@ export class MaterialsInventoryService {
changedUnitPrice?: number;
},
manager: EntityManager
): Promise<void> {
const { projectId, productId, quantity: inQuantity, unitPrice, changedUnitPrice } = data;
): Promise<MaterialsInventoryEntity> {
const {
projectId,
productId,
quantity: inQuantity,
unitPrice,
changedUnitPrice,
position
} = data;
const exsitedInventory = await manager.findOne(MaterialsInventoryEntity, {
where: { projectId, productId, unitPrice }, // 根据项目,产品,价格查出之前的实时库存情况
@ -378,13 +413,16 @@ export class MaterialsInventoryService {
// 若不存在库存,直接新增库存
if (!exsitedInventory) {
await manager.insert(MaterialsInventoryEntity, {
const inventoryNumber = await this.generateInventoryNumber();
const { raw } = await manager.insert(MaterialsInventoryEntity, {
projectId,
productId,
unitPrice,
inventoryNumber,
position,
quantity: inQuantity
});
return;
return manager.findOne(MaterialsInventoryEntity, { where: { id: raw.insertId } });
}
// 若该项目存在库存,则该项目该产品的库存增加
let { quantity, id } = exsitedInventory;
@ -396,6 +434,8 @@ export class MaterialsInventoryService {
quantity: newQuantity,
unitPrice: changedUnitPrice || undefined
});
return manager.findOne(MaterialsInventoryEntity, { where: { id } });
}
/**
@ -407,14 +447,22 @@ export class MaterialsInventoryService {
data: {
quantity: number;
inventoryId?: number;
productId: number;
unitPrice?: number;
},
manager: EntityManager
): Promise<void> {
const { quantity: outQuantity, inventoryId } = data;
): Promise<MaterialsInventoryEntity> {
const { quantity: outQuantity, inventoryId, productId, unitPrice } = data;
let searchPayload: any = {};
if (inventoryId) {
searchPayload.id = inventoryId;
} else {
// 删除出入库记录时需要根据产品ID和价格查找库存
searchPayload = { productId, unitPrice };
}
// 开启悲观行锁,防止脏读和修改
const inventory = await manager.findOne(MaterialsInventoryEntity, {
where: { id: inventoryId },
where: searchPayload,
lock: { mode: 'pessimistic_write' }
});
// 检查库存剩余
@ -430,6 +478,8 @@ export class MaterialsInventoryService {
await manager.update(MaterialsInventoryEntity, id, {
quantity: newQuantity
});
return manager.findOne(MaterialsInventoryEntity, { where: { id } });
}
/**
@ -467,4 +517,31 @@ export class MaterialsInventoryService {
.getOne();
return info;
}
/**
*
* @returns
*/
async generateInventoryNumber() {
const prefix =
(
await this.paramConfigRepository.findOne({
where: {
key: ParamConfigEnum.InventoryNumberPrefix
}
})
)?.value || '';
const lastInventory = await this.materialsInventoryRepository
.createQueryBuilder('materials_inventory')
.select(
`MAX(CAST(REPLACE(materials_inventory.inventoryNumber, '${prefix}', '') AS UNSIGNED))`,
'maxInventoryNumber'
)
.getRawOne();
const lastNumber = lastInventory.maxInventoryNumber
? parseInt(lastInventory.maxInventoryNumber.replace(prefix, ''))
: 0;
const newNumber = lastNumber + 1 < 1000 ? 1000 : lastNumber + 1;
return `${prefix}${newNumber}`;
}
}

View File

@ -17,7 +17,6 @@ import pinyin from 'pinyin';
import { DictItemEntity } from '../system/dict-item/dict-item.entity';
@Entity({ name: 'product' })
export class ProductEntity extends CommonEntity {
@Column({
name: 'product_number',
type: 'varchar',
@ -35,7 +34,7 @@ export class ProductEntity extends CommonEntity {
})
@ApiProperty({ description: '产品名称' })
name: string;
@Column({
name: 'product_specification',
type: 'varchar',
@ -45,6 +44,7 @@ export class ProductEntity extends CommonEntity {
})
@ApiProperty({ description: '产品规格', nullable: true })
productSpecification?: string;
@Column({
name: 'remark',
type: 'varchar',