PO logic
This commit is contained in:
@@ -75,6 +75,13 @@ type WorkOrderRecord = {
|
||||
name: string;
|
||||
};
|
||||
} | null;
|
||||
salesOrder: {
|
||||
id: string;
|
||||
documentNumber: string;
|
||||
} | null;
|
||||
salesOrderLine: {
|
||||
id: string;
|
||||
} | null;
|
||||
warehouse: {
|
||||
id: string;
|
||||
code: string;
|
||||
@@ -191,6 +198,17 @@ function buildInclude() {
|
||||
},
|
||||
},
|
||||
},
|
||||
salesOrder: {
|
||||
select: {
|
||||
id: true,
|
||||
documentNumber: true,
|
||||
},
|
||||
},
|
||||
salesOrderLine: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
warehouse: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -278,6 +296,9 @@ function mapSummary(record: WorkOrderRecord): WorkOrderSummaryDto {
|
||||
projectId: record.project?.id ?? null,
|
||||
projectNumber: record.project?.projectNumber ?? null,
|
||||
projectName: record.project?.name ?? null,
|
||||
salesOrderId: record.salesOrder?.id ?? null,
|
||||
salesOrderLineId: record.salesOrderLine?.id ?? null,
|
||||
salesOrderNumber: record.salesOrder?.documentNumber ?? null,
|
||||
quantity: record.quantity,
|
||||
completedQuantity: record.completedQuantity,
|
||||
dueDate: record.dueDate ? record.dueDate.toISOString() : null,
|
||||
@@ -293,7 +314,10 @@ function mapSummary(record: WorkOrderRecord): WorkOrderSummaryDto {
|
||||
};
|
||||
}
|
||||
|
||||
function mapDetail(record: WorkOrderRecord): WorkOrderDetailDto {
|
||||
function mapDetail(
|
||||
record: WorkOrderRecord,
|
||||
componentAvailability: Map<string, { onHandQuantity: number; reservedQuantity: number }>
|
||||
): WorkOrderDetailDto {
|
||||
const issuedByComponent = new Map<string, number>();
|
||||
|
||||
for (const issue of record.materialIssues) {
|
||||
@@ -325,6 +349,11 @@ function mapDetail(record: WorkOrderRecord): WorkOrderDetailDto {
|
||||
materialRequirements: record.item.bomLines.map((line) => {
|
||||
const requiredQuantity = line.quantity * record.quantity;
|
||||
const issuedQuantity = issuedByComponent.get(line.componentItem.id) ?? 0;
|
||||
const availability = componentAvailability.get(line.componentItem.id) ?? { onHandQuantity: 0, reservedQuantity: 0 };
|
||||
const onHandQuantity = availability.onHandQuantity;
|
||||
const reservedQuantity = availability.reservedQuantity;
|
||||
const availableQuantity = onHandQuantity - reservedQuantity;
|
||||
const shortageQuantity = Math.max(requiredQuantity - issuedQuantity - Math.max(availableQuantity, 0), 0);
|
||||
|
||||
return {
|
||||
componentItemId: line.componentItem.id,
|
||||
@@ -335,6 +364,10 @@ function mapDetail(record: WorkOrderRecord): WorkOrderDetailDto {
|
||||
requiredQuantity,
|
||||
issuedQuantity,
|
||||
remainingQuantity: Math.max(requiredQuantity - issuedQuantity, 0),
|
||||
onHandQuantity,
|
||||
reservedQuantity,
|
||||
availableQuantity,
|
||||
shortageQuantity,
|
||||
};
|
||||
}),
|
||||
materialIssues: record.materialIssues.map((issue) => ({
|
||||
@@ -531,6 +564,63 @@ async function getItemLocationOnHand(itemId: string, warehouseId: string, locati
|
||||
}, 0);
|
||||
}
|
||||
|
||||
async function getComponentAvailability(workOrder: WorkOrderRecord) {
|
||||
const componentItemIds = [...new Set(workOrder.item.bomLines.map((line) => line.componentItem.id))];
|
||||
if (componentItemIds.length === 0) {
|
||||
return new Map<string, { onHandQuantity: number; reservedQuantity: number }>();
|
||||
}
|
||||
|
||||
const [transactions, reservations] = await Promise.all([
|
||||
prisma.inventoryTransaction.findMany({
|
||||
where: {
|
||||
itemId: { in: componentItemIds },
|
||||
warehouseId: workOrder.warehouse.id,
|
||||
locationId: workOrder.location.id,
|
||||
},
|
||||
select: {
|
||||
itemId: true,
|
||||
transactionType: true,
|
||||
quantity: true,
|
||||
},
|
||||
}),
|
||||
prisma.inventoryReservation.findMany({
|
||||
where: {
|
||||
itemId: { in: componentItemIds },
|
||||
warehouseId: workOrder.warehouse.id,
|
||||
locationId: workOrder.location.id,
|
||||
status: "ACTIVE",
|
||||
},
|
||||
select: {
|
||||
itemId: true,
|
||||
quantity: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const availability = new Map<string, { onHandQuantity: number; reservedQuantity: number }>();
|
||||
for (const itemId of componentItemIds) {
|
||||
availability.set(itemId, { onHandQuantity: 0, reservedQuantity: 0 });
|
||||
}
|
||||
|
||||
for (const transaction of transactions) {
|
||||
const current = availability.get(transaction.itemId);
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
current.onHandQuantity += transaction.transactionType === "RECEIPT" || transaction.transactionType === "ADJUSTMENT_IN" ? transaction.quantity : -transaction.quantity;
|
||||
}
|
||||
|
||||
for (const reservation of reservations) {
|
||||
const current = availability.get(reservation.itemId);
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
current.reservedQuantity += reservation.quantity;
|
||||
}
|
||||
|
||||
return availability;
|
||||
}
|
||||
|
||||
async function validateWorkOrderInput(payload: WorkOrderInput) {
|
||||
const item = await prisma.inventoryItem.findUnique({
|
||||
where: { id: payload.itemId },
|
||||
@@ -573,6 +663,40 @@ async function validateWorkOrderInput(payload: WorkOrderInput) {
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.salesOrderId) {
|
||||
const salesOrder = await prisma.salesOrder.findUnique({
|
||||
where: { id: payload.salesOrderId },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!salesOrder) {
|
||||
return { ok: false as const, reason: "Linked sales order was not found." };
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.salesOrderLineId) {
|
||||
if (!payload.salesOrderId) {
|
||||
return { ok: false as const, reason: "Linked sales-order line requires a linked sales order." };
|
||||
}
|
||||
|
||||
const salesOrderLine = await prisma.salesOrderLine.findUnique({
|
||||
where: { id: payload.salesOrderLineId },
|
||||
select: {
|
||||
id: true,
|
||||
orderId: true,
|
||||
itemId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!salesOrderLine || salesOrderLine.orderId !== payload.salesOrderId) {
|
||||
return { ok: false as const, reason: "Linked sales-order line was not found on the selected sales order." };
|
||||
}
|
||||
|
||||
if (salesOrderLine.itemId !== payload.itemId) {
|
||||
return { ok: false as const, reason: "Linked sales-order line item does not match the selected build item." };
|
||||
}
|
||||
}
|
||||
|
||||
const location = await prisma.warehouseLocation.findUnique({
|
||||
where: { id: payload.locationId },
|
||||
select: {
|
||||
@@ -727,7 +851,11 @@ export async function getWorkOrderById(workOrderId: string) {
|
||||
include: buildInclude(),
|
||||
});
|
||||
|
||||
return workOrder ? mapDetail(workOrder as WorkOrderRecord) : null;
|
||||
if (!workOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return mapDetail(workOrder as WorkOrderRecord, await getComponentAvailability(workOrder as WorkOrderRecord));
|
||||
}
|
||||
|
||||
export async function createWorkOrder(payload: WorkOrderInput, actorId?: string | null) {
|
||||
@@ -742,6 +870,8 @@ export async function createWorkOrder(payload: WorkOrderInput, actorId?: string
|
||||
workOrderNumber,
|
||||
itemId: payload.itemId,
|
||||
projectId: payload.projectId,
|
||||
salesOrderId: payload.salesOrderId,
|
||||
salesOrderLineId: payload.salesOrderLineId,
|
||||
warehouseId: payload.warehouseId,
|
||||
locationId: payload.locationId,
|
||||
status: payload.status,
|
||||
@@ -804,6 +934,8 @@ export async function updateWorkOrder(workOrderId: string, payload: WorkOrderInp
|
||||
data: {
|
||||
itemId: payload.itemId,
|
||||
projectId: payload.projectId,
|
||||
salesOrderId: payload.salesOrderId,
|
||||
salesOrderLineId: payload.salesOrderLineId,
|
||||
warehouseId: payload.warehouseId,
|
||||
locationId: payload.locationId,
|
||||
status: payload.status,
|
||||
@@ -916,7 +1048,7 @@ export async function issueWorkOrderMaterial(workOrderId: string, payload: WorkO
|
||||
return { ok: false as const, reason: "Warehouse location is invalid for the selected warehouse." };
|
||||
}
|
||||
|
||||
const currentDetail = mapDetail(workOrder as WorkOrderRecord);
|
||||
const currentDetail = mapDetail(workOrder as WorkOrderRecord, await getComponentAvailability(workOrder as WorkOrderRecord));
|
||||
const currentRequirement = currentDetail.materialRequirements.find(
|
||||
(requirement: WorkOrderDetailDto["materialRequirements"][number]) => requirement.componentItemId === payload.componentItemId
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user