diff --git a/package.json b/package.json index 7c66bd9..5b84432 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71071f8..94fdde3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/constants/error-code.constant.ts b/src/constants/error-code.constant.ts index 2dd82b4..688571a 100644 --- a/src/constants/error-code.constant.ts +++ b/src/constants/error-code.constant.ts @@ -55,5 +55,8 @@ export enum ErrorEnum { PRODUCT_EXIST = '1406:产品已存在', // Contract - CONTRACT_NUMBER_EXIST = '1407:存在相同的合同编号' + CONTRACT_NUMBER_EXIST = '1407:存在相同的合同编号', + + // Inventory 库存不足 + INVENTORY_INSUFFICIENT = '1408:库存不足' } diff --git a/src/modules/common/base.service.ts b/src/modules/common/base.service.ts new file mode 100644 index 0000000..7b47117 --- /dev/null +++ b/src/modules/common/base.service.ts @@ -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 +} 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 3d2dd77..7bff520 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 @@ -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, @InjectRepository(ParamConfigEntity) - private paramConfigRepository: Repository + private paramConfigRepository: Repository, + private materialsInventoryService: MaterialsInventoryService ) {} /** * 查询所有出入库记录 @@ -57,7 +60,7 @@ export class MaterialsInOutService { .where(fieldSearch(ext)) .andWhere('materialsInOut.isDelete = 0') .addOrderBy('materialsInOut.createdAt', 'DESC'); - + if (productName) { sqb.andWhere('product.name like :productName', { productName: `%${productName}%` }); } @@ -82,8 +85,10 @@ export class MaterialsInOutService { async create(dto: MaterialsInOutDto): Promise { let { inOrOut, inventoryNumber } = dto; if (inOrOut === MaterialsInOrOutEnum.In) { + // 入库 inventoryNumber = await this.generateInventoryNumber(); } else { + // 出库 const inRecord = await this.materialsInOutRepository.findOne({ where: { inventoryNumber @@ -92,9 +97,15 @@ export class MaterialsInOutService { const { productId } = inRecord; dto.productId = productId; } - await this.materialsInOutRepository.insert({ - ...this.materialsInOutRepository.create(dto), - inventoryNumber + + 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 }); }); } diff --git a/src/modules/materials_inventory/materials_inventory.controller.ts b/src/modules/materials_inventory/materials_inventory.controller.ts index 5e483a5..da479d1 100644 --- a/src/modules/materials_inventory/materials_inventory.controller.ts +++ b/src/modules/materials_inventory/materials_inventory.controller.ts @@ -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 { await this.miService.create(dto); } @Put(':id') - @ApiOperation({ summary: '更新原材料盘点' }) + @ApiOperation({ summary: '更新原材料库存' }) @Perm(permissions.UPDATE) async update(@IdParam() id: number, @Body() dto: MaterialsInventoryUpdateDto): Promise { await this.miService.update(id, dto); } @Delete(':id') - @ApiOperation({ summary: '删除原材料盘点' }) + @ApiOperation({ summary: '删除原材料库存' }) @Perm(permissions.DELETE) async delete(@IdParam() id: number): Promise { await this.miService.delete(id); diff --git a/src/modules/materials_inventory/materials_inventory.entity.ts b/src/modules/materials_inventory/materials_inventory.entity.ts index 3023003..0d7fc29 100644 --- a/src/modules/materials_inventory/materials_inventory.entity.ts +++ b/src/modules/materials_inventory/materials_inventory.entity.ts @@ -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; diff --git a/src/modules/materials_inventory/materials_inventory.service.ts b/src/modules/materials_inventory/materials_inventory.service.ts index 73dd3de..4033086 100644 --- a/src/modules/materials_inventory/materials_inventory.service.ts +++ b/src/modules/materials_inventory/materials_inventory.service.ts @@ -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 { await this.materialsInventoryRepository.insert(dto); } /** - * 更新 + * 更新库存 */ async update(id: number, data: Partial): Promise { await this.entityManager.transaction(async manager => { @@ -279,6 +282,79 @@ export class MaterialsInventoryService { }); } + /** + * 产品入库 + */ + async inInventory(data: { + productId: number; + inQuantity: number; + unitPrice?: number; + }): Promise { + 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 { + 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 + }); + }); + } + + /** + * 产品出库 + */ + /** * 删除 */ diff --git a/src/utils/tool.util.ts b/src/utils/tool.util.ts index 4bd86d8..9320ca9 100644 --- a/src/utils/tool.util.ts +++ b/src/utils/tool.util.ts @@ -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';