feat: 重做出入库逻辑

This commit is contained in:
louis 2024-03-22 16:47:26 +08:00
parent 77247c140c
commit cab022af0a
12 changed files with 202 additions and 60 deletions

View File

@ -13,7 +13,7 @@ SWAGGER_PATH = api-docs
SWAGGER_VERSION = 1.0
# db
DB_HOST = 127.0.0.1
DB_HOST = 192.168.60.39
DB_PORT = 13307
DB_DATABASE = hxoa
DB_USERNAME = root
@ -23,7 +23,7 @@ DB_LOGGING = "all"
# redis
REDIS_PORT = 6379
REDIS_HOST = 127.0.0.1
REDIS_HOST = 192.168.60.39
REDIS_PASSWORD = 123456
REDIS_DB = 0

View File

@ -21,7 +21,9 @@ export enum MaterialsInOrOutEnum {
// 系统参数key
export enum ParamConfigEnum {
MaterialsInOutPrefix = 'materials_in_out_prefix'
InventoryNumberPrefixIn = 'inventory_number_prefix_in',
InventoryNumberPrefixOut = 'inventory_number_prefix_out',
ProductNumberPrefix = 'product_number_prefix'
}
// 合同审核状态

View File

@ -60,4 +60,5 @@ export enum ErrorEnum {
// Inventory
INVENTORY_INSUFFICIENT = '1408:库存不足',
MATERIALS_IN_OUT_NOT_FOUND = '1409:出入库信息不存在',
MATERIALS_IN_OUT_UNIT_PRICE_CANNOT_BE_MODIFIED = '1410:该价格的产品已经出库,单价不允许修改。若有疑问,请联系管理员'
}

View File

@ -46,11 +46,6 @@ export class AuthController {
@ApiSecurityAuth()
@ApiOperation({ summary: '屏幕解锁使用密码和token' })
@ApiResult({ type: LoginToken })
async unlock(@Body() dto: LoginDto, @AuthUser() user: IAuthUser): Promise<Boolean> {
await this.authService.unlock(user.uid, dto.password);
return true;

View File

@ -13,7 +13,8 @@ import {
IsString,
Matches,
MinLength,
ValidateIf
ValidateIf,
isNumber
} from 'class-validator';
import dayjs from 'dayjs';
import { PagerDto } from '~/common/dto/pager.dto';
@ -37,6 +38,11 @@ export class MaterialsInOutDto {
@IsString()
inventoryNumber: string;
@ApiProperty({ description: '库存id(产品和单价双主键决定一条库存)' })
@IsOptional()
@IsNumber()
inventoryId: number;
@ApiProperty({ description: '单位(字典)' })
@IsNumber()
@IsOptional()

View File

@ -39,7 +39,7 @@ export class MaterialsInOutEntity extends CommonEntity {
productId: number;
@Column({
name: 'inOrOut',
name: 'in_or_out',
type: 'tinyint',
comment: '入库或出库'
})

View File

@ -49,12 +49,17 @@ export class MaterialsInOutService {
.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'
])
@ -84,21 +89,8 @@ export class MaterialsInOutService {
*
*/
async create(dto: MaterialsInOutDto): Promise<void> {
let { inOrOut, inventoryNumber, projectId } = dto;
if (Object.is(inOrOut, MaterialsInOrOutEnum.In)) {
// 入库
inventoryNumber = await this.generateInventoryNumber();
} else {
// 出库
const inRecord = await this.materialsInOutRepository.findOne({
where: {
inventoryNumber
}
});
const { productId } = inRecord;
dto.productId = productId;
}
let { inOrOut, inventoryNumber, projectId, inventoryId } = dto;
inventoryNumber = await this.generateInventoryNumber(inOrOut);
await this.entityManager.transaction(async manager => {
// 1.生成出入库记录
const { productId, quantity, unitPrice } = await manager.save(MaterialsInOutEntity, {
@ -110,7 +102,7 @@ export class MaterialsInOutService {
Object.is(inOrOut, MaterialsInOrOutEnum.In)
? this.materialsInventoryService.inInventory
: this.materialsInventoryService.outInventory
)({ productId, quantity, unitPrice, projectId }, manager);
)({ productId, quantity, unitPrice, projectId, inventoryId }, manager);
});
}
@ -125,9 +117,41 @@ export class MaterialsInOutService {
},
lock: { mode: 'pessimistic_write' }
});
await manager.update(MaterialsInOutEntity, id, {
...data
});
// 修改入库记录的价格,会直接更改库存实际价格.
// 1.如果有了出库记录,不允许修改入库价格。
if (
Object.is(data.inOrOut, MaterialsInOrOutEnum.In) &&
isDefined(data.unitPrice) &&
Math.abs(Number(data.unitPrice) - Number(entity.unitPrice)) !== 0
) {
const outEntity = await manager.findOne(MaterialsInOutEntity, {
where: {
unitPrice: entity.unitPrice,
inOrOut: MaterialsInOrOutEnum.Out,
projectId: entity.projectId,
productId: entity.productId
}
});
if (isDefined(outEntity)) {
throw new BusinessException(ErrorEnum.MATERIALS_IN_OUT_UNIT_PRICE_CANNOT_BE_MODIFIED);
}
await (
Object.is(data.inOrOut, MaterialsInOrOutEnum.In)
? this.materialsInventoryService.inInventory
: this.materialsInventoryService.outInventory
)(
{
productId: entity.productId,
quantity: 0,
unitPrice: entity.unitPrice,
projectId: entity.projectId,
changedUnitPrice: data.unitPrice
},
manager
);
}
let changedQuantity = 0;
if (isDefined(data.quantity) && entity.quantity !== data.quantity) {
if (entity.inOrOut === MaterialsInOrOutEnum.In) {
@ -165,6 +189,12 @@ export class MaterialsInOutService {
manager
);
}
// 完成所有业务逻辑后,更新出入库记录
await manager.update(MaterialsInOutEntity, id, {
...data
});
if (fileIds?.length) {
const count = await this.storageRepository
.createQueryBuilder('storage')
@ -208,7 +238,7 @@ export class MaterialsInOutService {
{
productId: entity.productId,
quantity: entity.quantity,
unitPrice: undefined,
unitPrice: entity.unitPrice,
projectId: entity.projectId
},
manager
@ -261,12 +291,14 @@ export class MaterialsInOutService {
*
* @returns
*/
async generateInventoryNumber() {
async generateInventoryNumber(inOrOut: MaterialsInOrOutEnum = MaterialsInOrOutEnum.In) {
const prefix =
(
await this.paramConfigRepository.findOne({
where: {
key: ParamConfigEnum.MaterialsInOutPrefix
key: inOrOut
? ParamConfigEnum.InventoryNumberPrefixOut
: ParamConfigEnum.InventoryNumberPrefixIn
}
})
)?.value || '';
@ -276,6 +308,7 @@ export class MaterialsInOutService {
`MAX(CAST(REPLACE(materialsInOut.inventoryNumber, '${prefix}', '') AS UNSIGNED))`,
'maxInventoryNumber'
)
.where('materialsInOut.inOrOut = :inOrOut', { inOrOut })
.getRawOne();
const lastNumber = lastMaterial.maxInventoryNumber
? parseInt(lastMaterial.maxInventoryNumber.replace(prefix, ''))

View File

@ -62,7 +62,15 @@ export class MaterialsInventoryService {
.leftJoin('mio.product', 'product')
.leftJoin('product.unit', 'unit')
.leftJoin('product.company', 'company')
.addSelect(['project.id', 'project.name', 'product.name', 'unit.label', 'company.name'])
.addSelect([
'project.id',
'project.name',
'unit.label',
'company.name',
'product.name',
'product.productSpecification',
'product.productNumber'
])
.where({
time: MoreThan(time[0])
})
@ -99,7 +107,7 @@ export class MaterialsInventoryService {
sheet.getCell('A2').value = `日期:${dayjs(time[0]).format('YYYY年M月')}`;
// 设置表头
const headers = [
'库存编号',
'出入库单号',
'公司名称',
'产品名称',
'单位',
@ -185,7 +193,10 @@ export class MaterialsInventoryService {
}
});
const groupedData = groupBy<MaterialsInOutEntity>(currentMonthProjectData, 'inventoryNumber');
const groupedData = groupBy<MaterialsInOutEntity>(
currentMonthProjectData,
record => `${record.productId}-${record.unitPrice}`
);
let number = 0;
const groupedInventories = groupBy(
currentProjectInventories,
@ -309,7 +320,14 @@ export class MaterialsInventoryService {
.leftJoin('materialsInventory.product', 'product')
.leftJoin('product.unit', 'unit')
.leftJoin('product.company', 'company')
.addSelect(['project.name', 'product.name', 'unit.label', 'company.name'])
.addSelect([
'project.name',
'unit.label',
'company.name',
'product.name',
'product.productSpecification',
'product.productNumber'
])
.where('materialsInventory.isDelete = 0');
return paginate<MaterialsInventoryEntity>(queryBuilder, {
page,
@ -337,6 +355,7 @@ export class MaterialsInventoryService {
/**
*
* id和价格双主键存储
* @param data ID,ID和入库数量和单价
* @param manager
*/
@ -346,13 +365,14 @@ export class MaterialsInventoryService {
productId: number;
quantity: number;
unitPrice?: number;
changedUnitPrice?: number;
},
manager: EntityManager
): Promise<void> {
const { projectId, productId, quantity: inQuantity, unitPrice } = data;
const { projectId, productId, quantity: inQuantity, unitPrice, changedUnitPrice } = data;
const exsitedInventory = await manager.findOne(MaterialsInventoryEntity, {
where: { projectId, productId }, // 查出某个项目的某个产品的库存情况
where: { projectId, productId, unitPrice }, // 根据项目,产品,价格查出之前的实时库存情况
lock: { mode: 'pessimistic_write' } // 开启悲观行锁,防止脏读和修改
});
@ -373,29 +393,28 @@ export class MaterialsInventoryService {
throw new Error('库存数量不合法');
}
await manager.update(MaterialsInventoryEntity, id, {
quantity: newQuantity
quantity: newQuantity,
unitPrice: changedUnitPrice || undefined
});
}
/**
*
* @param data id和入库数量和单价
* @param data ID()
* @param manager
*/
async outInventory(
data: {
projectId: number;
productId: number;
quantity: number;
unitPrice?: number;
inventoryId?: number;
},
manager: EntityManager
): Promise<void> {
const { projectId, productId, quantity: outQuantity } = data;
const { quantity: outQuantity, inventoryId } = data;
// 开启悲观行锁,防止脏读和修改
const inventory = await manager.findOne(MaterialsInventoryEntity, {
where: { projectId, productId },
where: { id: inventoryId },
lock: { mode: 'pessimistic_write' }
});
// 检查库存剩余
@ -406,26 +425,41 @@ export class MaterialsInventoryService {
let { quantity, id } = inventory;
const newQuantity = calcNumber(quantity || 0, outQuantity || 0, 'subtract');
if (isNaN(newQuantity)) {
throw new Error('库存数量不合法');
throw new Error('库存数量不足。请检查库存或重新操作。');
}
await manager.update(MaterialsInventoryEntity, id, {
quantity: newQuantity
});
}
/**
*
*/
async delete(id: number): Promise<void> {
// 合同比较重要,做逻辑删除
// 比较重要,做逻辑删除
await this.materialsInventoryRepository.update(id, { isDelete: 1 });
}
/**
*
*
*/
async info(id: number) {
const info = await this.materialsInventoryRepository
.createQueryBuilder('materialsInventory')
.leftJoin('materialsInventory.project', 'project')
.leftJoin('materialsInventory.product', 'product')
.leftJoin('product.unit', 'unit')
.leftJoin('product.company', 'company')
.addSelect([
'project.name',
'project.id',
'product.id',
'product.name',
'unit.label',
'company.name',
'product.productSpecification',
'product.productNumber'
])
.where({
id
})

View File

@ -8,6 +8,12 @@ export class ProductDto {
@IsString()
name: string;
@ApiProperty({ description: '产品规格' })
@IsOptional()
@IsString()
productSpecification: string;
@ApiProperty({ description: '产品备注' })
@IsOptional()
@IsString()
@ -47,4 +53,10 @@ export class ProductQueryDto extends IntersectionType(
@IsOptional()
@IsString()
name?: string;
@ApiProperty({ description: '关键字(名字/编号/规格)' })
@IsOptional()
@IsString()
keyword?: string;
}

View File

@ -17,6 +17,16 @@ 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',
length: 255,
comment: '产品编号'
})
@ApiProperty({ description: '产品编号' })
productNumber: string;
@Column({
name: 'name',
type: 'varchar',
@ -26,6 +36,15 @@ export class ProductEntity extends CommonEntity {
@ApiProperty({ description: '产品名称' })
name: string;
@Column({
name: 'product_specification',
type: 'varchar',
nullable: true,
length: 255,
comment: '产品规格'
})
@ApiProperty({ description: '产品规格', nullable: true })
productSpecification?: string;
@Column({
name: 'remark',
type: 'varchar',

View File

@ -4,9 +4,10 @@ import { ProductService } from './product.service';
import { ProductEntity } from './product.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { StorageModule } from '../tools/storage/storage.module';
import { ParamConfigModule } from '../system/param-config/param-config.module';
@Module({
imports: [TypeOrmModule.forFeature([ProductEntity]), StorageModule],
imports: [TypeOrmModule.forFeature([ProductEntity]), StorageModule, ParamConfigModule],
controllers: [ProductController],
providers: [ProductService]
})

View File

@ -9,6 +9,8 @@ import { Storage } from '../tools/storage/storage.entity';
import { BusinessException } from '~/common/exceptions/biz.exception';
import { ErrorEnum } from '~/constants/error-code.constant';
import { fieldSearch } from '~/shared/database/field-search';
import { ParamConfigEnum } from '~/constants/enum';
import { ParamConfigEntity } from '../system/param-config/param-config.entity';
@Injectable()
export class ProductService {
@ -17,7 +19,9 @@ export class ProductService {
@InjectRepository(ProductEntity)
private productRepository: Repository<ProductEntity>,
@InjectRepository(Storage)
private storageRepository: Repository<Storage>
private storageRepository: Repository<Storage>,
@InjectRepository(ParamConfigEntity)
private paramConfigRepository: Repository<ParamConfigEntity>
) {}
/**
@ -28,13 +32,13 @@ export class ProductService {
pageSize,
...fields
}: ProductQueryDto): Promise<Pagination<ProductEntity>> {
const { company: companyName, ...ext } = fields;
const { company: companyName, keyword, ...ext } = fields;
const sqb = this.productRepository
.createQueryBuilder('product')
.leftJoin('product.files', 'files')
.leftJoin('product.company', 'company')
.leftJoin('product.unit', 'unit')
.addSelect(['files.id', 'files.path', 'company.name', 'company.id','unit.id','unit.label'])
.addSelect(['files.id', 'files.path', 'company.name', 'company.id', 'unit.id', 'unit.label'])
.where(fieldSearch(ext))
.andWhere('product.isDelete = 0')
.addOrderBy('product.namePinyin', 'ASC');
@ -45,6 +49,16 @@ export class ProductService {
}
});
}
if (keyword) {
//关键字模糊查询product的name,productNumber,productSpecification
sqb.andWhere(
'(product.name like :keyword or product.productNumber like :keyword or product.productSpecification like :keyword)',
{
keyword: `%${keyword}%`
}
);
}
return paginate<ProductEntity>(sqb, {
page,
pageSize
@ -62,7 +76,9 @@ export class ProductService {
if (isExsit) {
throw new BusinessException(ErrorEnum.PRODUCT_EXIST);
}
await this.productRepository.insert(this.productRepository.create(dto));
await this.productRepository.insert(
this.productRepository.create({ ...dto, productNumber: await this.generateProductNumber() })
);
}
/**
@ -91,11 +107,7 @@ export class ProductService {
throw new BusinessException(ErrorEnum.STORAGE_NOT_FOUND);
}
// 附件要批量插入
await manager
.createQueryBuilder()
.relation(ProductEntity, 'files')
.of(id)
.add(fileIds);
await manager.createQueryBuilder().relation(ProductEntity, 'files').of(id).add(fileIds);
}
});
}
@ -146,4 +158,31 @@ export class ProductService {
.addAndRemove(linkedFiles, product.files);
});
}
/**
*
* @returns
*/
async generateProductNumber(): Promise<string> {
const prefix =
(
await this.paramConfigRepository.findOne({
where: {
key: ParamConfigEnum.ProductNumberPrefix
}
})
)?.value || '';
const lastProduct = await this.productRepository
.createQueryBuilder('product')
.select(
`MAX(CAST(REPLACE(COALESCE(product.product_number, ''), '${prefix}', '') AS UNSIGNED))`,
'productNumber'
)
.getRawOne();
const lastNumber = lastProduct.productNumber
? parseInt(lastProduct.productNumber.replace(prefix, ''))
: 0;
const newNumber = lastNumber + 1 < 1000 ? 1000 : lastNumber + 1;
return `${prefix}${newNumber}`;
}
}