feat: 盘点表计算模块

This commit is contained in:
louis 2024-03-08 17:23:33 +08:00
parent 3610d8fdc6
commit 5d9ce4cef2
9 changed files with 204 additions and 179 deletions

View File

@ -88,6 +88,7 @@
"helmet": "^7.1.0",
"ioredis": "^5.3.2",
"lodash": "^4.17.21",
"mathjs": "^12.4.0",
"minio": "^7.1.3",
"mysql2": "^3.9.1",
"nanoid": "^3.3.7",

View File

@ -131,6 +131,9 @@ dependencies:
lodash:
specifier: ^4.17.21
version: 4.17.21
mathjs:
specifier: ^12.4.0
version: 12.4.0
minio:
specifier: ^7.1.3
version: 7.1.3
@ -4881,6 +4884,10 @@ packages:
dot-prop: 5.3.0
dev: true
/complex.js@2.1.1:
resolution: {integrity: sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==}
dev: false
/component-emitter@1.3.1:
resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==}
dev: true
@ -5429,6 +5436,10 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/decimal.js@10.4.3:
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
dev: false
/decode-uri-component@0.2.2:
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
engines: {node: '>=0.10'}
@ -5912,6 +5923,10 @@ packages:
/escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
/escape-latex@1.2.0:
resolution: {integrity: sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==}
dev: false
/escape-string-applescript@1.0.0:
resolution: {integrity: sha512-4/hFwoYaC6TkpDn9A3pTC52zQPArFeXuIfhUtCGYdauTzXVP9H3BDr3oO/QzQehMpLDC7srvYgfwvImPFGfvBA==}
engines: {node: '>=0.10.0'}
@ -6662,6 +6677,10 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/fraction.js@4.3.4:
resolution: {integrity: sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q==}
dev: false
/fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
@ -7671,6 +7690,10 @@ packages:
dev: false
optional: true
/javascript-natural-sort@0.7.1:
resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==}
dev: false
/jest-changed-files@29.7.0:
resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -8709,6 +8732,22 @@ packages:
hasBin: true
dev: true
/mathjs@12.4.0:
resolution: {integrity: sha512-4Moy0RNjwMSajEkGGxNUyMMC/CZAcl87WBopvNsJWB4E4EFebpTedr+0/rhqmnOSTH3Wu/3WfiWiw6mqiaHxVw==}
engines: {node: '>= 18'}
hasBin: true
dependencies:
'@babel/runtime': 7.23.9
complex.js: 2.1.1
decimal.js: 10.4.3
escape-latex: 1.2.0
fraction.js: 4.3.4
javascript-natural-sort: 0.7.1
seedrandom: 3.0.5
tiny-emitter: 2.1.0
typed-function: 4.1.1
dev: false
/memfs@3.5.3:
resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==}
engines: {node: '>= 4.0.0'}
@ -10861,6 +10900,10 @@ packages:
resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
dev: false
/seedrandom@3.0.5:
resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==}
dev: false
/segmentit@2.0.3:
resolution: {integrity: sha512-7mn2XL3OdTUQ+AhHz7SbgyxLTaQRzTWQNVwiK+UlTO8aePGbSwvKUzTwE4238+OUY9MoR6ksAg35zl8sfTunQQ==}
requiresBuild: true
@ -11635,6 +11678,10 @@ packages:
/through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
/tiny-emitter@2.1.0:
resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
dev: false
/tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
@ -11893,6 +11940,11 @@ packages:
resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==}
dev: true
/typed-function@4.1.1:
resolution: {integrity: sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ==}
engines: {node: '>= 14'}
dev: false
/typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
dev: true

View File

@ -55,5 +55,8 @@ export enum ErrorEnum {
PRODUCT_EXIST = '1406:产品已存在',
// Contract
CONTRACT_NUMBER_EXIST = '1407:存在相同的合同编号'
CONTRACT_NUMBER_EXIST = '1407:存在相同的合同编号',
// Inventory 库存不足
INVENTORY_INSUFFICIENT = '1408:库存不足'
}

View File

@ -0,0 +1,11 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class BaseService {
generateInventoryNumber(): string {
// Generate a random inventory number
return Math.floor(Math.random() * 1000000).toString();
}
// Add more common methods here
}

View File

@ -16,6 +16,8 @@ import { MaterialsInOutEntity } from './materials_in_out.entity';
import { fieldSearch } from '~/shared/database/field-search';
import { ParamConfigEntity } from '~/modules/system/param-config/param-config.entity';
import { MaterialsInOrOutEnum, ParamConfigEnum } from '~/constants/enum';
import { MaterialsInventoryEntity } from '../materials_inventory.entity';
import { MaterialsInventoryService } from '../materials_inventory.service';
@Injectable()
export class MaterialsInOutService {
@ -26,7 +28,8 @@ export class MaterialsInOutService {
@InjectRepository(Storage)
private storageRepository: Repository<Storage>,
@InjectRepository(ParamConfigEntity)
private paramConfigRepository: Repository<ParamConfigEntity>
private paramConfigRepository: Repository<ParamConfigEntity>,
private materialsInventoryService: MaterialsInventoryService
) {}
/**
*
@ -82,8 +85,10 @@ export class MaterialsInOutService {
async create(dto: MaterialsInOutDto): Promise<void> {
let { inOrOut, inventoryNumber } = dto;
if (inOrOut === MaterialsInOrOutEnum.In) {
// 入库
inventoryNumber = await this.generateInventoryNumber();
} else {
// 出库
const inRecord = await this.materialsInOutRepository.findOne({
where: {
inventoryNumber
@ -92,10 +97,16 @@ export class MaterialsInOutService {
const { productId } = inRecord;
dto.productId = productId;
}
await this.materialsInOutRepository.insert({
await this.entityManager.transaction(async manager => {
// 1.生成出入库记录
const { productId, quantity } = await manager.create(MaterialsInOutEntity, {
...this.materialsInOutRepository.create(dto),
inventoryNumber
});
// 2.更新库存
await this.materialsInventoryService.inInventory({ productId, inQuantity: quantity });
});
}
/**

View File

@ -23,7 +23,7 @@ export const permissions = definePermission('app:materials_inventory', {
EXPORT: 'export'
} as const);
@ApiTags('MaterialsI Inventory - 原材料盘点')
@ApiTags('MaterialsI Inventory - 原材料库存')
@ApiSecurityAuth()
@Controller('materials-inventory')
export class MaterialsInventoryController {
@ -40,7 +40,7 @@ export class MaterialsInventoryController {
}
@Get()
@ApiOperation({ summary: '获取原材料盘点列表' })
@ApiOperation({ summary: '获取原材料库存列表' })
@ApiResult({ type: [MaterialsInventoryEntity], isPage: true })
@Perm(permissions.LIST)
async list(@Query() dto: MaterialsInventoryQueryDto) {
@ -48,7 +48,7 @@ export class MaterialsInventoryController {
}
@Get(':id')
@ApiOperation({ summary: '获取原材料盘点信息' })
@ApiOperation({ summary: '获取原材料库存信息' })
@ApiResult({ type: MaterialsInventoryDto })
@Perm(permissions.READ)
async info(@IdParam() id: number) {
@ -56,21 +56,21 @@ export class MaterialsInventoryController {
}
@Post()
@ApiOperation({ summary: '新增原材料盘点' })
@ApiOperation({ summary: '新增原材料库存' })
@Perm(permissions.CREATE)
async create(@Body() dto: MaterialsInventoryDto): Promise<void> {
await this.miService.create(dto);
}
@Put(':id')
@ApiOperation({ summary: '更新原材料盘点' })
@ApiOperation({ summary: '更新原材料库存' })
@Perm(permissions.UPDATE)
async update(@IdParam() id: number, @Body() dto: MaterialsInventoryUpdateDto): Promise<void> {
await this.miService.update(id, dto);
}
@Delete(':id')
@ApiOperation({ summary: '删除原材料盘点' })
@ApiOperation({ summary: '删除原材料库存' })
@Perm(permissions.DELETE)
async delete(@IdParam() id: number): Promise<void> {
await this.miService.delete(id);

View File

@ -1,193 +1,41 @@
import { ApiProperty } from '@nestjs/swagger';
import { Column, Entity, JoinTable, ManyToMany, Relation } from 'typeorm';
import { CommonEntity } from '~/common/entity/common.entity';
import { Storage } from '../tools/storage/storage.entity';
@Entity({ name: 'materials_inventory' })
export class MaterialsInventoryEntity extends CommonEntity {
@Column({ name: 'company_name', type: 'varchar', length: 255, comment: '公司名称' })
@ApiProperty({ description: '公司名称' })
companyName: number;
@Column({
name: 'product',
name: 'product_id',
type: 'int',
comment: '产品名称(字典)'
comment: '产品'
})
@ApiProperty({ description: '产品名称(字典)' })
product: number;
@ApiProperty({ description: '产品' })
productId: number;
@Column({
name: 'unit',
type: 'int',
comment: '单位(字典)'
})
@ApiProperty({ description: '单位(字典)' })
unit: number;
@Column({
name: 'previous_inventory_quantity',
name: 'quantity',
type: 'int',
default: 0,
comment: '之前的库存数量'
comment: '库存产品数量'
})
@ApiProperty({ description: '之前的库存数量' })
previousInventoryQuantity: number;
@ApiProperty({ description: '库存产品数量' })
quantity: number;
@Column({
name: 'previous_unit_price',
name: 'unit_price',
type: 'decimal',
precision: 10,
default: 0,
scale: 2,
comment: '之前的单价'
comment: '库存产品单价'
})
@ApiProperty({ description: '之前的单价' })
previousUnitPrice: number;
@Column({
name: 'previous_amount',
type: 'decimal',
precision: 10,
scale: 2,
default: 0,
comment: '之前的金额'
})
@ApiProperty({ description: '之前的金额' })
previousAmount: number;
@Column({
name: 'inventory_time',
type: 'date',
nullable: true,
comment: '入库时间'
})
@ApiProperty({ description: '入库时间' })
inventoryTime: Date;
@Column({
name: 'inventory_quantity',
type: 'int',
default: 0,
comment: '入库数量'
})
@ApiProperty({ description: '入库数量' })
inventoryQuantity: number;
@Column({
name: 'inventory_unit_price',
type: 'decimal',
precision: 10,
default: 0,
scale: 2,
comment: '入库单价'
})
@ApiProperty({ description: '入库单价' })
inventoryUnitPrice: number;
@Column({
name: 'inventory_amount',
type: 'decimal',
precision: 10,
default: 0,
scale: 2,
comment: '入库金额'
})
@ApiProperty({ description: '入库金额' })
inventoryAmount: number;
@Column({
name: 'out_time',
type: 'date',
nullable: true,
comment: '出库时间'
})
@ApiProperty({ description: '出库时间' })
outime: Date;
@Column({
name: 'out_quantity',
type: 'int',
default: 0,
comment: '出库数量'
})
@ApiProperty({ description: '出库数量' })
outQuantity: number;
@Column({
name: 'out_unit_price',
type: 'decimal',
precision: 10,
default: 0,
scale: 2,
comment: '出库单价'
})
@ApiProperty({ description: '出库单价' })
outUnitPrice: number;
@Column({
name: 'out_amount',
type: 'decimal',
precision: 10,
default: 0,
scale: 2,
comment: '出库金额'
})
@ApiProperty({ description: '出库金额' })
outAmount: number;
@Column({
name: 'current_inventory_quantity',
type: 'int',
default: 0,
comment: '现在的结存数量'
})
@ApiProperty({ description: '现在的结存数量' })
currentInventoryQuantity: number;
@Column({
name: 'current_unit_price',
type: 'decimal',
precision: 10,
default: 0,
scale: 2,
comment: '现在的单价'
})
@ApiProperty({ description: '现在的单价' })
currentUnitPrice: number;
@Column({
name: 'current_amount',
type: 'decimal',
precision: 10,
default: 0,
scale: 2,
comment: '现在的金额'
})
@ApiProperty({ description: '现在的金额' })
currentAmount: number;
@Column({ name: 'agent', type: 'varchar', length: 50, comment: '经办人', nullable: true })
@ApiProperty({ description: '经办人' })
agent: string;
@Column({
name: 'issuance_number',
type: 'varchar',
length: 100,
comment: '领料单号'
})
@ApiProperty({ description: '领料单号' })
issuanceNumber: string;
@ApiProperty({ description: '库存产品单价' })
unitPrice: number;
@Column({ name: 'remark', type: 'varchar', length: 255, comment: '备注', nullable: true })
@ApiProperty({ description: '备注' })
remark: string;
@Column({ name: 'project', type: 'varchar', length: 255, comment: '项目', nullable: false })
@ApiProperty({ description: '项目' })
project: string;
@Column({ name: 'is_delete', type: 'tinyint', default: 0, comment: '是否删除' })
@ApiProperty({ description: '删除状态0未删除1已删除' })
isDelete: number;

View File

@ -18,6 +18,9 @@ import { fieldSearch } from '~/shared/database/field-search';
import { groupBy, uniqBy } from 'lodash';
import { MaterialsInOrOutEnum } 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';
@Injectable()
export class MaterialsInventoryService {
const;
@ -52,7 +55,7 @@ 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', 'product.name', 'unit.label', 'company.name'])
.where(fieldSearch({ time }))
.andWhere('mio.isDelete = 0');
@ -262,14 +265,14 @@ export class MaterialsInventoryService {
}
/**
*
*
*/
async create(dto: MaterialsInventoryDto): Promise<void> {
await this.materialsInventoryRepository.insert(dto);
}
/**
*
*
*/
async update(id: number, data: Partial<MaterialsInventoryUpdateDto>): Promise<void> {
await this.entityManager.transaction(async manager => {
@ -279,6 +282,79 @@ export class MaterialsInventoryService {
});
}
/**
*
*/
async inInventory(data: {
productId: number;
inQuantity: number;
unitPrice?: number;
}): Promise<void> {
const { productId, 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
});
});
}
/**
*
*/
async outInventory(data: { productId: number; outQuantity: number }): Promise<void> {
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
});
});
}
/**
*
*/
/**
*
*/

View File

@ -1,6 +1,7 @@
import { customAlphabet, nanoid } from 'nanoid';
import { md5 } from './crypto.util';
import { add, subtract, multiply, divide, bignumber, BigNumber } from 'mathjs';
export function getAvatar(mail: string | undefined) {
if (!mail) return '';
@ -53,3 +54,25 @@ export const hashString = function (str, seed = 0) {
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
};
/**
* 使mathjs进行四则运算
*/
export function calcNumber(
firstNumber: number,
secondNumber: number,
option: CalclateOption
): number {
switch (option) {
case 'add':
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));
default:
return 0;
}
}
type CalclateOption = 'add' | 'subtract' | 'multiply' | 'divide';