forked from yoone/API
1
0
Fork 0
API/src/service/stock.service.ts

577 lines
20 KiB
TypeScript

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<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>;
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<StockPoint[]> {
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<string, any>, 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<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,'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<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.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);
}
}