Skip to content

Purchase Order Business Rules

This document outlines the business rules for purchase orders in the PRS system.

Purchase Order Entity

A purchase order (PO) is an official document issued to suppliers to order goods or services.

Purchase Order Status Flow

Text Only
1
2
3
FOR_PO_REVIEW → FOR_PO_APPROVAL → FOR SENDING → FOR_DELIVERY → CLOSED_PO;
PO_REJECTED;
PO_CANCELLED;
  • PO can be REJECTED during FOR_PO_APPROVAL status
  • PO can be CANCELLED if PO items are NOT yet FULLY DELIVERED
  • PO status will be CLOSED_PO if all PO items delivered (based on DR) and fully paid (based on PR)

Purchase Order Fields

Field Description Validation Rules
poNumber Purchase Order Number Auto-generated
poLetter Purchase Order Letter Auto-generated
requisitionId Associated Requisition ID Required
supplierId Supplier ID Required
supplierName Supplier Name Required
status Current status Must be one of the defined statuses
createdBy User ID of creator Required
warranty Warranty information Optional
deliveryAddress Delivery address Required
totalAmount Total amount Calculated from items

Business Rules

1. Purchase Order Creation

  • Purchase orders are created automatically after canvass approval
  • Purchase orders are grouped by supplier and terms
  • PO Number is auto-generated
  • PO Letter is auto-generated
  • Status is set to FOR_PO_REVIEW
  • Includes all selected items from the canvass for each supplier
JavaScript
// Example code for PO creation
async function createPurchaseOrder({ transaction, existingCanvass }) {
  const canvassItems = await canvassItemRepository.findAll({
    where: { canvassRequisitionId: existingCanvass.id },
    paginate: false,
  });

  const canvassItemIds = canvassItems.map((item) => item.id);

  const selectedSuppliers = await canvassItemSupplierRepository.getSelectedSupplierByCanvassId(
    canvassItemIds,
  );

  // Group by supplier
  const supplierGroups = {};
  for (const supplier of selectedSuppliers) {
    if (!supplierGroups[supplier.supplierId]) {
      supplierGroups[supplier.supplierId] = {
        supplierId: supplier.supplierId,
        supplierName: supplier.supplierName,
        items: [],
      };
    }

    supplierGroups[supplier.supplierId].items.push({
      canvassItemId: supplier.canvassItemId,
      canvassItemSupplierId: supplier.id,
      price: supplier.price,
      quantity: supplier.canvassItem.quantity,
      unit: supplier.canvassItem.unit,
      description: supplier.canvassItem.description,
    });
  }

  // Create purchase orders for each supplier
  for (const supplierId in supplierGroups) {
    const supplierGroup = supplierGroups[supplierId];

    // Generate PO number
    const { poNumber, poLetter } = await generatePONumberCode();

    // Create purchase order
    const purchaseOrder = await purchaseOrderRepository.create(
      {
        poNumber,
        poLetter,
        requisitionId: existingCanvass.requisitionId,
        supplierId: supplierGroup.supplierId,
        supplierName: supplierGroup.supplierName,
        status: 'FOR_PO_REVIEW',
        createdBy: existingCanvass.createdBy,
        totalAmount: calculateTotalAmount(supplierGroup.items),
      },
      { transaction },
    );

    // Create purchase order items
    for (const item of supplierGroup.items) {
      await purchaseOrderItemRepository.create(
        {
          purchaseOrderId: purchaseOrder.id,
          canvassItemId: item.canvassItemId,
          canvassItemSupplierId: item.canvassItemSupplierId,
          price: item.price,
          quantity: item.quantity,
          unit: item.unit,
          description: item.description,
        },
        { transaction },
      );
    }

    // Create purchase order approvers
    await createPurchaseOrderApprovers(purchaseOrder.id, existingCanvass.createdBy, transaction);
  }
}

2. Purchase Order Review

  • Purchase order details can be updated during review
  • Warranty and delivery address can be specified
  • Discount, fixed or percentage, can be added
  • Status changes to FOR_PO_APPROVAL when submitted for approval

3. Purchase Order Approval

  • Only FOR_PO_APPROVAL purchase orders can be approved
  • User must be an assigned approver for the purchase order
  • Approvers are processed in order by level
  • Status will remain as FOR APPROVAL until fully approved
  • Additional approvers can be added during the approval process
  • Approvers can input a note during approval
  • Approvers can edit item quantity or delete an item from the table
  • If all approvers approve, status changes to FOR_SENDING
JavaScript
// Example approval logic
async function approvePurchaseOrder({
  purchaseOrderId,
  approverId,
  transaction,
}) {
  const purchaseOrder = await purchaseOrderRepository.findOne({
    where: { id: purchaseOrderId },
    include: [{ association: 'purchaseOrderApprovers' }],
    transaction,
  });

  if (purchaseOrder.status !== 'FOR_PO_APPROVAL' && purchaseOrder.status !== 'REJECT_PO') {
    throw new Error('Purchase order cannot be approved');
  }

  const approver = purchaseOrder.purchaseOrderApprovers.find(a => a.userId === approverId);

  if (!approver) {
    throw new Error('User is not an approver for this purchase order');
  }

  if (approver.status === 'APPROVED') {
    throw new Error('User has already approved this purchase order');
  }

  await approver.update({ status: 'APPROVED' }, { transaction });

  const allApprovers = purchaseOrder.purchaseOrderApprovers;
  const pendingApprovers = allApprovers.filter(a => a.status !== 'APPROVED');

  if (pendingApprovers.length === 0) {
    await purchaseOrder.update({ status: 'FOR_DELIVERY' }, { transaction });

    // Update requisition status
    await requisitionRepository.update(
      { id: purchaseOrder.requisitionId },
      { status: 'FOR_DELIVERY' },
      { transaction },
    );

    return true; // All approved
  }

  return false; // Not all approved yet
}

4. Purchase Order Sending

  • Purchasing Staff will send the PO to supplier manually (outside of the system)
  • After sending, assigned Purchasing Staff can change status from FOR_SENDING to FOR_DELIVERY

5. Purchase Order Rejection

  • Only FOR_PO_APPROVAL purchase orders can be rejected
  • User must be an assigned approver for the purchase order
  • Rejection requires a reason/comment
  • Upon rejection, status changes to PO_REJECTED
  • Assigned Purchasing Staff should be able to edit rejected PO
  • Can resubmit and will undergo approval process again with status FOR_PO_APPROVAL

6. Purchase Order Cancellation

  • Purchase orders can be CANCELLED if PO items are NOT yet FULLY DELIVERED or
  • Purchase orders can be cancelled if the supplier is suspended
  • Upon cancellation, status changes to CANCELLED_PO
  • Should allow cancelled PO items to be canvassed again (add undelivered PO Qty to Remaining Qty to be canvassed)
JavaScript
// Example cancellation logic
async function cancelPurchaseOrder(purchaseOrderId, reason, transaction) {
  const purchaseOrder = await purchaseOrderRepository.update(
    { id: purchaseOrderId },
    { 
      status: 'CANCELLED_PO',
      cancellationReason: reason,
    },
    { transaction },
  );

  // Remove approvers
  await purchaseOrderApproverRepository.destroy(
    {
      purchaseOrderId,
    },
    { transaction },
  );

  return purchaseOrder;
}

7. Purchase Order Items

  • Items are created from the selected suppliers in the canvass
  • Each item includes price, quantity, unit, and description
  • Total amount is calculated from all items

Validation Rules

Detailed validation rules are defined in the Zod schema:

JavaScript
const updatePurchaseOrderSchema = z
  .object({
    warranty: z
      .string(stringFieldError('Warranty'))
      .trim()
      .max(100, 'Warranty must not exceed 100 characters')
      .regex(
        /^[A-Za-z0-9'\s.-Ññ]+$/,
        'Warranty can only contain letters, numbers, spaces and specific special characters: -\'."',
      )
      .optional(),
    deliveryAddress: z
      .string(stringFieldError('Delivery Address'))
      .trim()
      .min(1, 'Delivery Address is required')
      .max(100, 'Delivery Address must not exceed 100 characters')
      .regex(
        /^[A-Za-z0-9'\s.-Ññ]+$/,
        'Delivery Address can only contain letters, numbers, spaces and specific special characters: -\'."',
      ),
  })
  .strict();