API/src/service/stock.service.ts

667 lines
22 KiB
TypeScript

import { Provide } from '@midwayjs/core';
import { Between, Like, Repository, LessThan, MoreThan, In } 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.entity';
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';
import { Area } from '../entity/area.entity';
@Provide()
export class StockService {
@InjectEntityModel(StockPoint)
stockPointModel: Repository<StockPoint>;
@InjectEntityModel(Stock)
stockModel: Repository<Stock>;
@InjectEntityModel(StockRecord)
stockRecordModel: Repository<StockRecord>;
@InjectEntityModel(PurchaseOrder)
purchaseOrderModel: Repository<PurchaseOrder>;
@InjectEntityModel(PurchaseOrderItem)
purchaseOrderItemModel: Repository<PurchaseOrderItem>;
@InjectEntityModel(Transfer)
transferModel: Repository<Transfer>;
@InjectEntityModel(TransferItem)
transferItemModel: Repository<TransferItem>;
@InjectEntityModel(Area)
areaModel: Repository<Area>;
async createStockPoint(data: CreateStockPointDTO) {
const { areas: areaCodes, ...restData } = data;
const stockPoint = new StockPoint();
Object.assign(stockPoint, restData);
if (areaCodes && areaCodes.length > 0) {
const areas = await this.areaModel.findBy({ code: In(areaCodes) });
stockPoint.areas = areas;
} else {
stockPoint.areas = [];
}
await this.stockPointModel.save(stockPoint);
}
async updateStockPoint(id: number, data: UpdateStockPointDTO) {
const { areas: areaCodes, ...restData } = data;
const pointToUpdate = await this.stockPointModel.findOneBy({ id });
if (!pointToUpdate) {
throw new Error(`仓库点 ID ${id} 不存在`);
}
Object.assign(pointToUpdate, restData);
if (areaCodes !== undefined) {
if (areaCodes.length > 0) {
const areas = await this.areaModel.findBy({ code: In(areaCodes) });
pointToUpdate.areas = areas;
} else {
pointToUpdate.areas = [];
}
}
await this.stockPointModel.save(pointToUpdate);
}
async getStockPoints(query: QueryPointDTO) {
const { current = 1, pageSize = 10 } = query;
return await paginate(this.stockPointModel, {
pagination: { current, pageSize },
relations: ['areas'],
});
}
async getAllStockPoints(): Promise<StockPoint[]> {
return await this.stockPointModel.find({ relations: ['areas'] });
}
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, 'name', poi.name,'sku', poi.sku, '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 },
}
);
}
// 检查指定 SKU 是否在任一仓库有库存(数量大于 0)
async hasStockBySku(sku: string): Promise<boolean> {
const count = await this.stockModel
.createQueryBuilder('stock')
.where('stock.sku = :sku', { sku })
.andWhere('stock.quantity > 0')
.getCount();
return count > 0;
}
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.sku = item.sku;
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, name, sku } = query;
const nameKeywords = name
? name.split(' ').filter(Boolean)
: [];
let queryBuilder = this.stockModel
.createQueryBuilder('stock')
.select([
// 'stock.id as id',
'stock.sku as sku',
'product.name as name',
'product.nameCn as nameCn',
'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.sku')
.groupBy('stock.sku')
.addGroupBy('product.name')
.addGroupBy('product.nameCn');
let totalQueryBuilder = this.stockModel
.createQueryBuilder('stock')
.select('COUNT(DISTINCT stock.sku)', 'count')
.leftJoin(Product, 'product', 'product.sku = stock.sku');
if (sku || nameKeywords.length) {
const conditions = [];
if (sku) {
conditions.push(`stock.sku LIKE :sku`);
}
if (nameKeywords.length) {
nameKeywords.forEach((name, index) => {
conditions.push(`product.name LIKE :name${index}`);
});
}
const whereClause = conditions.join(' OR ');
queryBuilder.andWhere(`(${whereClause})`, {
sku: `%${sku}%`,
...nameKeywords.reduce((acc, name, index) => ({ ...acc, [`name${index}`]: `%${name}%` }), {}),
});
totalQueryBuilder.andWhere(`(${whereClause})`, {
sku: `%${sku}%`,
...nameKeywords.reduce((acc, name, index) => ({ ...acc, [`name${index}`]: `%${name}%` }), {}),
});
}
if (query.order) {
const sortFieldMap: Record<string, string> = {
name: 'product.name',
sku: 'stock.sku',
updatedAt: 'updatedAt',
createdAt: 'createdAt',
};
let isFirstSort = true;
Object.entries(query.order).forEach(([field, direction]) => {
const orderDirection = direction === 'asc' ? 'ASC' : 'DESC';
if (field.startsWith('point_')) {
const pointId = field.split('_')[1];
const sortExpr = `SUM(CASE WHEN stock.stockPointId = :pointId THEN stock.quantity ELSE 0 END)`;
const sortAlias = `pointSort_${pointId}`;
queryBuilder
.addSelect(sortExpr, sortAlias)
.setParameter('pointId', Number(pointId));
if (isFirstSort) {
queryBuilder.orderBy(sortAlias, orderDirection);
isFirstSort = false;
} else {
queryBuilder.addOrderBy(sortAlias, orderDirection);
}
} else {
const actualSortField = sortFieldMap[field] || field;
if (isFirstSort) {
queryBuilder.orderBy(actualSortField, orderDirection);
isFirstSort = false;
} else {
queryBuilder.addOrderBy(actualSortField, orderDirection);
}
}
});
} else {
// 默认按产品名称排序
queryBuilder.orderBy('product.name', 'ASC');
}
const items = await queryBuilder
.offset((current - 1) * pageSize)
.limit(pageSize)
.getRawMany();
const totalResult = await totalQueryBuilder.getRawOne();
const total = parseInt(totalResult.count, 10);
const transfer = await this.transferModel
.createQueryBuilder('t')
.select(['ti.sku as sku', 'SUM(ti.quantity) as quantity'])
.leftJoin(TransferItem, 'ti', 'ti.transferId = t.id')
.where('!t.isArrived and !t.isCancel and !t.isLost')
.groupBy('ti.sku')
.getRawMany();
for (const item of items) {
item.inTransitQuantity =
transfer.find(t => t.sku === item.sku)?.quantity || 0;
}
return {
items,
total,
current,
pageSize,
};
}
async getStocksBySkus(skus: string[]) {
if (!skus || skus.length === 0) {
return [];
}
const stocks = await this.stockModel
.createQueryBuilder('stock')
.select('stock.sku', 'sku')
.addSelect('SUM(stock.quantity)', 'totalQuantity')
.where('stock.sku IN (:...skus)', { skus })
.groupBy('stock.sku')
.getRawMany();
return stocks;
}
// 更新库存
async updateStock(data: UpdateStockDTO) {
const {
stockPointId,
sku,
quantityChange,
operationType,
operatorId,
note,
} = data;
const stock = await this.stockModel.findOneBy({
stockPointId,
sku,
});
if (!stock) {
// 如果库存不存在,则直接新增
const newStock = this.stockModel.create({
stockPointId,
sku,
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,
sku,
operationType,
quantityChange,
operatorId,
note,
});
await this.stockRecordModel.save(stockRecord);
}
// 获取库存记录
async getStockRecords(query: QueryStockRecordDTO) {
const {
current = 1,
pageSize = 10,
stockPointId,
sku,
name,
operationType,
startDate,
endDate,
} = query;
const where: any = {};
if (stockPointId) where.stockPointId = stockPointId;
if (sku) where.sku = sku;
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.sku')
.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 (name)
queryBuilder.andWhere('product.name LIKE :name', {
name: `%${name}%`,
});
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<string, any>, userId: number) {
const { sourceStockPointId, destStockPointId, sendAt, items, note } = data;
// for (const item of items) {
// const stock = await this.stockModel.findOneBy({
// stockPointId: sourceStockPointId,
// sku: item.sku,
// });
// 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.sku = item.sku;
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<string, any>) {
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,'sku', ti.sku, '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.sku = item.sku;
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.sku = item.sku;
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<string, any>, 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.sku = item.sku;
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.sku = item.sku;
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);
}
}