import { Provide } from '@midwayjs/core'; import { Between, Like, Repository, LessThan, MoreThan } from 'typeorm'; import { Stock } from '../entity/stock.entity'; import { StockRecord } from '../entity/stock_record.entity'; import { paginate } from '../utils/paginate.util'; import { Product } from '../entity/product.entty'; import { CreatePurchaseOrderDTO, CreateStockPointDTO, QueryPointDTO, QueryPurchaseOrderDTO, QueryStockDTO, QueryStockRecordDTO, UpdatePurchaseOrderDTO, UpdateStockDTO, UpdateStockPointDTO, } from '../dto/stock.dto'; import { StockPoint } from '../entity/stock_point.entity'; import { PurchaseOrder } from '../entity/purchase_order.entity'; import { PurchaseOrderItem } from '../entity/purchase_order_item.entity'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { PurchaseOrderStatus, StockRecordOperationType, } from '../enums/base.enum'; import { User } from '../entity/user.entity'; import dayjs = require('dayjs'); import { Transfer } from '../entity/transfer.entity'; import { TransferItem } from '../entity/transfer_item.entity'; @Provide() export class StockService { @InjectEntityModel(StockPoint) stockPointModel: Repository; @InjectEntityModel(Stock) stockModel: Repository; @InjectEntityModel(StockRecord) stockRecordModel: Repository; @InjectEntityModel(PurchaseOrder) purchaseOrderModel: Repository; @InjectEntityModel(PurchaseOrderItem) purchaseOrderItemModel: Repository; @InjectEntityModel(Transfer) transferModel: Repository; @InjectEntityModel(TransferItem) transferItemModel: Repository; async createStockPoint(data: CreateStockPointDTO) { const { name, location, contactPerson, contactPhone } = data; const stockPoint = new StockPoint(); stockPoint.name = name; stockPoint.location = location; stockPoint.contactPerson = contactPerson; stockPoint.contactPhone = contactPhone; await this.stockPointModel.save(stockPoint); } async updateStockPoint(id: number, data: UpdateStockPointDTO) { // 确认产品是否存在 const point = await this.stockPointModel.findOneBy({ id }); if (!point) { throw new Error(`产品 ID ${id} 不存在`); } // 更新产品 await this.stockPointModel.update(id, data); } async getStockPoints(query: QueryPointDTO) { const { current = 1, pageSize = 10 } = query; return await paginate(this.stockPointModel, { pagination: { current, pageSize }, }); } async getAllStockPoints(): Promise { return await this.stockPointModel.find(); } async delStockPoints(id: number) { const point = await this.stockPointModel.findOneBy({ id }); if (!point) { throw new Error(`库存点 ID ${id} 不存在`); } await this.stockRecordModel.delete({ stockPointId: id }); await this.stockPointModel.softDelete(id); } async createPurchaseOrder(data: CreatePurchaseOrderDTO) { const { stockPointId, expectedArrivalTime, status, items, note } = data; const now = dayjs().format('YYYY-MM-DD'); const count = await this.purchaseOrderModel.count({ where: { createdAt: Between( dayjs(`${now} 00:00:00`).toDate(), dayjs(`${now} 23:59:59`).toDate() ), }, }); const orderNumber = `${now.replace(/-/g, '')}P0${count + 1}`; const purchaseOrder = await this.purchaseOrderModel.save({ stockPointId, orderNumber, expectedArrivalTime, status, note, }); items.forEach(item => (item.purchaseOrderId = purchaseOrder.id)); await this.purchaseOrderItemModel.save(items); } async updatePurchaseOrder(id: number, data: UpdatePurchaseOrderDTO) { const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id }); if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`); if (purchaseOrder.status === 'received') throw new Error(`采购订单 ID ${id} 已到达,无法修改`); const { stockPointId, expectedArrivalTime, status, items, note } = data; purchaseOrder.stockPointId = stockPointId; purchaseOrder.expectedArrivalTime = expectedArrivalTime; purchaseOrder.status = status; purchaseOrder.note = note; this.purchaseOrderModel.save(purchaseOrder); const dbItems = await this.purchaseOrderItemModel.find({ where: { purchaseOrderId: id }, }); const ids = new Set(items.map(v => String(v.id))); const toDelete = dbItems.filter( dbVariation => !ids.has(String(dbVariation.id)) ); if (toDelete.length > 0) { const idsToDelete = toDelete.map(v => v.id); await this.purchaseOrderItemModel.delete(idsToDelete); } for (const item of items) { if (item.id) { await this.purchaseOrderItemModel.update(item.id, item); } else { item.purchaseOrderId = id; await this.purchaseOrderItemModel.save(item); } } } async getPurchaseOrders(query: QueryPurchaseOrderDTO) { const { current = 1, pageSize = 10, orderNumber, stockPointId } = query; const where: any = {}; if (orderNumber) where.orderNumber = Like(`%${orderNumber}%`); if (stockPointId) where.stockPointId = Like(`%${stockPointId}%`); return await paginate( this.purchaseOrderModel .createQueryBuilder('purchase_order') .leftJoinAndSelect( StockPoint, 'stock_point', 'purchase_order.stockPointId = stock_point.id' ) .leftJoin( qb => qb .select([ 'poi.purchaseOrderId AS purchaseOrderId', "JSON_ARRAYAGG(JSON_OBJECT('id', poi.id, 'productName', poi.productName,'productSku', poi.productSku, 'quantity', poi.quantity, 'price', poi.price)) AS items", ]) .from(PurchaseOrderItem, 'poi') .groupBy('poi.purchaseOrderId'), 'items', 'items.purchaseOrderId = purchase_order.id' ) .select([ 'purchase_order.*', 'stock_point.name as stockPointName', 'items.items', ]) .where(where) .orderBy('createdAt', 'DESC'), { pagination: { current, pageSize }, } ); } async delPurchaseOrder(id: number) { const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id }); if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`); if (purchaseOrder.status === 'received') throw new Error(`采购订单 ID ${id} 已到达,无法删除`); await this.purchaseOrderItemModel.delete({ purchaseOrderId: id }); await this.purchaseOrderModel.delete({ id }); } async receivePurchaseOrder(id: number, userId: number) { const purchaseOrder = await this.purchaseOrderModel.findOneBy({ id }); if (!purchaseOrder) throw new Error(`采购订单 ID ${id} 不存在`); if (purchaseOrder.status === 'received') throw new Error(`采购订单 ID ${id} 已到达,不要重复操作`); const items = await this.purchaseOrderItemModel.find({ where: { purchaseOrderId: id }, }); for (const item of items) { const updateStock = new UpdateStockDTO(); updateStock.stockPointId = purchaseOrder.stockPointId; updateStock.productSku = item.productSku; updateStock.quantityChange = item.quantity; updateStock.operationType = StockRecordOperationType.IN; updateStock.operatorId = userId; updateStock.note = '采购入库'; await this.updateStock(updateStock); } purchaseOrder.status = PurchaseOrderStatus.RECEIVED; await this.purchaseOrderModel.save(purchaseOrder); } async getPurchaseOrder(orderNumber: string) { const sql = ` SELECT poi.* FROM purchase_order po left join purchase_order_item poi on po.id = poi.purchaseOrderId WHERE po.orderNumber = '${orderNumber}' ` const data = await this.stockModel.query(sql); return data; } // 获取库存列表 async getStocks(query: QueryStockDTO) { const { current = 1, pageSize = 10, productName } = query; const nameKeywords = productName ? productName.split(' ').filter(Boolean) : []; let queryBuilder = this.stockModel .createQueryBuilder('stock') .select([ // 'stock.id as id', 'stock.productSku as productSku', 'product.name as productName', 'product.nameCn as productNameCn', 'JSON_ARRAYAGG(JSON_OBJECT("id", stock.stockPointId, "quantity", stock.quantity)) as stockPoint', 'MIN(stock.updatedAt) as updatedAt', 'MAX(stock.createdAt) as createdAt', ]) .leftJoin(Product, 'product', 'product.sku = stock.productSku') .groupBy('stock.productSku') .addGroupBy('product.name') .addGroupBy('product.nameCn'); let totalQueryBuilder = this.stockModel .createQueryBuilder('stock') .select('COUNT(DISTINCT stock.productSku)', 'count') .leftJoin(Product, 'product', 'product.sku = stock.productSku'); if (nameKeywords.length) { nameKeywords.forEach((name, index) => { queryBuilder.andWhere( `EXISTS ( SELECT 1 FROM product p WHERE p.sku = stock.productSku AND p.name LIKE :name${index} )`, { [`name${index}`]: `%${name}%` } ); totalQueryBuilder.andWhere( `EXISTS ( SELECT 1 FROM product p WHERE p.sku = stock.productSku AND p.name LIKE :name${index} )`, { [`name${index}`]: `%${name}%` } ); }); } const items = await queryBuilder.getRawMany(); const total = await totalQueryBuilder.getRawOne(); const transfer = await this.transferModel .createQueryBuilder('t') .select(['ti.productSku as productSku', 'SUM(ti.quantity) as quantity']) .leftJoin(TransferItem, 'ti', 'ti.transferId = t.id') .where('!t.isArrived and !t.isCancel and !t.isLost') .groupBy('ti.productSku') .getRawMany(); for (const item of items) { item.inTransitQuantity = transfer.find(t => t.productSku === item.productSku)?.quantity || 0; } return { items, total, current, pageSize, }; } // 更新库存 async updateStock(data: UpdateStockDTO) { const { stockPointId, productSku, quantityChange, operationType, operatorId, note, } = data; const stock = await this.stockModel.findOneBy({ stockPointId, productSku, }); if (!stock) { // 如果库存不存在,则直接新增 const newStock = this.stockModel.create({ stockPointId, productSku, quantity: operationType === 'in' ? quantityChange : -quantityChange, }); await this.stockModel.save(newStock); } else { // 更新库存 stock.quantity += operationType === 'in' ? quantityChange : -quantityChange; // if (stock.quantity < 0) { // throw new Error('库存不足,无法完成操作'); // } await this.stockModel.save(stock); } // 记录库存变更日志 const stockRecord = this.stockRecordModel.create({ stockPointId, productSku, operationType, quantityChange, operatorId, note, }); await this.stockRecordModel.save(stockRecord); } // 获取库存记录 async getStockRecords(query: QueryStockRecordDTO) { const { current = 1, pageSize = 10, stockPointId, productSku, productName, operationType, startDate, endDate, } = query; const where: any = {}; if (stockPointId) where.stockPointId = stockPointId; if (productSku) where.productSku = productSku; if (operationType) where.operationType = operationType; if (startDate) where.createdAt = MoreThan(startDate); if (endDate) where.createdAt = LessThan(endDate); if (startDate && endDate) where.createdAt = Between(startDate, endDate); const queryBuilder = this.stockRecordModel .createQueryBuilder('stock_record') .leftJoin(Product, 'product', 'product.sku = stock_record.productSku') .leftJoin(User, 'user', 'stock_record.operatorId = user.id') .leftJoin(StockPoint, 'sp', 'sp.id = stock_record.stockPointId') .select([ 'stock_record.*', 'product.name as productName', 'user.username as operatorName', 'sp.name as stockPointName', ]) .where(where); if (productName) queryBuilder.andWhere('product.name LIKE :name', { name: `%${productName}%`, }); const items = await queryBuilder .orderBy('stock_record.createdAt', 'DESC') .skip((current - 1) * pageSize) .take(pageSize) .getRawMany(); const total = await queryBuilder.getCount(); return { items, total, current, pageSize, }; } async createTransfer(data: Record, userId: number) { const { sourceStockPointId, destStockPointId, sendAt, items, note } = data; // for (const item of items) { // const stock = await this.stockModel.findOneBy({ // stockPointId: sourceStockPointId, // productSku: item.productSku, // }); // if (!stock || stock.quantity < item.quantity) // throw new Error(`${item.productName} 库存不足`); // } const now = dayjs().format('YYYY-MM-DD'); const count = await this.transferModel.count({ where: { createdAt: Between( dayjs(`${now} 00:00:00`).toDate(), dayjs(`${now} 23:59:59`).toDate() ), }, }); const orderNumber = `${now.replace(/-/g, '')}P0${count + 1}`; const transfer = await this.transferModel.save({ sourceStockPointId, destStockPointId, orderNumber, sendAt, note, }); for (const item of items) { item.transferId = transfer.id; const updateStock = new UpdateStockDTO(); updateStock.stockPointId = sourceStockPointId; updateStock.productSku = item.productSku; updateStock.quantityChange = item.quantity; updateStock.operationType = StockRecordOperationType.OUT; updateStock.operatorId = userId; updateStock.note = `调拨${transfer.orderNumber} 出库`; await this.updateStock(updateStock); } await this.transferItemModel.save(items); } async getTransfers(query: Record) { const { current = 1, pageSize = 10, orderNumber, sourceStockPointId, destStockPointId, } = query; const where: any = {}; if (orderNumber) where.orderNumber = Like(`%${orderNumber}%`); if (sourceStockPointId) where.sourceStockPointId = sourceStockPointId; if (destStockPointId) where.destStockPointId = destStockPointId; return await paginate( this.transferModel .createQueryBuilder('t') .leftJoinAndSelect(StockPoint, 'sp', 't.sourceStockPointId = sp.id') .leftJoinAndSelect(StockPoint, 'sp1', 't.destStockPointId = sp1.id') .leftJoin( qb => qb .select([ 'ti.transferId AS transferId', "JSON_ARRAYAGG(JSON_OBJECT('id', ti.id, 'productName', ti.productName,'productSku', ti.productSku, 'quantity', ti.quantity)) AS items", ]) .from(TransferItem, 'ti') .groupBy('ti.transferId'), 'items', 'items.transferId = t.id' ) .select([ 't.*', 'sp.name as sourceStockPointName', 'sp1.name as destStockPointName', 'items.items', ]) .where(where) .orderBy('createdAt', 'DESC'), { pagination: { current, pageSize }, } ); } async cancelTransfer(id: number, userId: number) { const transfer = await this.transferModel.findOneBy({ id }); if (!transfer) throw new Error(`调拨 ID ${id} 不存在`); if (transfer.isArrived) throw new Error(`调拨 ID ${id} 已到达,无法取消`); const items = await this.transferItemModel.find({ where: { transferId: id }, }); for (const item of items) { const updateStock = new UpdateStockDTO(); updateStock.stockPointId = transfer.sourceStockPointId; updateStock.productSku = item.productSku; updateStock.quantityChange = item.quantity; updateStock.operationType = StockRecordOperationType.IN; updateStock.operatorId = userId; updateStock.note = `取消调拨${transfer.orderNumber} 入库`; await this.updateStock(updateStock); } transfer.isCancel = true; await this.transferModel.save(transfer); } async receiveTransfer(id: number, userId: number) { const transfer = await this.transferModel.findOneBy({ id }); if (!transfer) throw new Error(`调拨 ID ${id} 不存在`); if (transfer.isCancel) throw new Error(`调拨 ID ${id} 已取消`); if (transfer.isArrived) throw new Error(`调拨 ID ${id} 已到达,不要重复操作`); const items = await this.transferItemModel.find({ where: { transferId: id }, }); for (const item of items) { const updateStock = new UpdateStockDTO(); updateStock.stockPointId = transfer.destStockPointId; updateStock.productSku = item.productSku; updateStock.quantityChange = item.quantity; updateStock.operationType = StockRecordOperationType.IN; updateStock.operatorId = userId; updateStock.note = `调拨${transfer.orderNumber} 入库`; await this.updateStock(updateStock); } transfer.isArrived = true; transfer.arriveAt = new Date(); await this.transferModel.save(transfer); } async lostTransfer(id: number) { const transfer = await this.transferModel.findOneBy({ id }); if (!transfer) throw new Error(`调拨 ID ${id} 不存在`); if (transfer.isCancel) throw new Error(`调拨 ID ${id} 已取消`); if (transfer.isArrived) throw new Error(`调拨 ID ${id} 已到达,不要重复操作`); transfer.isLost = true; await this.transferModel.save(transfer); } async updateTransfer(id: number, data: Record, userId: number) { const { sourceStockPointId, destStockPointId, items, note } = data; const transfer = await this.transferModel.findOneBy({ id }); if (!transfer) throw new Error(`调拨单 ID ${id} 不存在`); if (transfer.isCancel) throw new Error(`调拨单 ID ${id} 已取消`); if (transfer.isArrived) throw new Error(`调拨单 ID ${id} 已到达`); const dbItems = await this.transferItemModel.find({ where: { transferId: id }, }); for (const item of dbItems) { item.transferId = transfer.id; const updateStock = new UpdateStockDTO(); updateStock.stockPointId = sourceStockPointId; updateStock.productSku = item.productSku; updateStock.quantityChange = item.quantity; updateStock.operationType = StockRecordOperationType.IN; updateStock.operatorId = userId; updateStock.note = `调拨调整 ${transfer.orderNumber} 入库`; await this.updateStock(updateStock); } await this.transferItemModel.delete(dbItems.map(v => v.id)); for (const item of items) { item.transferId = transfer.id; const updateStock = new UpdateStockDTO(); updateStock.stockPointId = sourceStockPointId; updateStock.productSku = item.productSku; updateStock.quantityChange = item.quantity; updateStock.operationType = StockRecordOperationType.OUT; updateStock.operatorId = userId; updateStock.note = `调拨调整${transfer.orderNumber} 出库`; await this.updateStock(updateStock); await this.transferItemModel.save(item); } transfer.sourceStockPointId = sourceStockPointId; transfer.destStockPointId = destStockPointId; transfer.note = note; await this.transferModel.save(transfer); } }