577 lines
20 KiB
TypeScript
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);
|
|
}
|
|
}
|