Canvass Business Rules
This document outlines the business rules for the canvassing process in the PRS system.
Canvass Entity
A canvass is the process of collecting and comparing supplier quotations for items in a requisition.
Canvass Status Flow
| Text Only |
|---|
| DRAFT → FOR_CS_APPROVAL → CS_APPROVED;
CS_REJECTED;
CS_CANCELLED
|
- A canvass can be REJECTED at the approval stage.
Canvass Fields
| Field |
Description |
Validation Rules |
| csNumber |
Canvass Sheet Number |
Auto-generated |
| csLetter |
Canvass Sheet Letter |
Auto-generated |
| requisitionId |
Associated Requisition ID |
Required |
| status |
Current status |
Must be one of the defined statuses |
| createdBy |
User ID of creator |
Required |
| assignedTo |
User ID of assignee |
Required |
| notes |
Additional notes |
Optional, max 100 characters |
Business Rules
1. Canvass Creation
- Only the user assigned to the requisition can create a canvass
- User can create multiple canvass sheet
- A canvass can be created as a draft or submitted immediately
- Canvass Sheet Number (CS Number) is auto-generated
- Canvass Sheet Letter (CS Letter) is auto-generated
- Creator is set to the current assigned purchasing staff
- Status is set to DRAFT based on isDraft flag and items included
- RS Status will be set to RS_IN_PROGRESS once canvass is submitted
| JavaScript |
|---|
| // Example code for CS Number generation
async function generateCSNumberCode(isDraft, canvassId) {
const csLetter = 'A'; // Default letter
if (isDraft) {
return { csNumber: null, csLetter };
}
if (canvassId) {
const existingCanvass = await canvassRepository.findOne({
where: { id: canvassId },
});
if (existingCanvass.csNumber) {
return { csNumber: existingCanvass.csNumber, csLetter: existingCanvass.csLetter };
}
}
const latestCS = await canvassRepository.findOne({
where: {
csNumber: { [Sequelize.Op.not]: null },
},
order: [['csNumber', 'DESC']],
});
let csNumber = '00001';
if (latestCS && latestCS.csNumber) {
const latestNumber = parseInt(latestCS.csNumber, 10);
csNumber = (latestNumber + 1).toString().padStart(5, '0');
}
return { csNumber, csLetter };
}
|
2. Canvass Validation
- Requisition must exist and be in ASSIGNED status
- Assigned user will be the creator of the canvass
- Items must be valid and belong to the requisition
- Item requested qty must not be fully canvassed
- Suppliers must be provided for each item with price information
| JavaScript |
|---|
| // Example validation logic
async function canvassCreateValidation(payload) {
const { canvassId, requisitionId, creatorId, isDraft = true } = payload;
const { CANVASS_STATUS } = constants.canvass;
const existingRequisition = await requisitionRepository.findOne({
where: { id: requisitionId },
});
if (!existingRequisition) {
throw new Error('Requisition not found. Cannot create canvass without a valid requisition');
}
if (!existingRequisition.assignedTo) {
throw new Error('Cannot create canvass. No assigned purchasing staff.');
}
if (existingRequisition.assignedTo !== creatorId) {
throw new Error('Cannot create canvass. Requisition is assigned to another user');
}
return existingRequisition;
}
|
3. Canvass Items
- Items can be added from the requisition as long as the requested quantity is not yet fully canvassed
- For OFM, canvass quantity should not exceed requested quantity
- For Non-OFM, allowed but will highlight exceeded quantity
- Each item must have at least one supplier - up to 4 suppliers
- Each supplier must have a price
- Items can be partially canvassed (not all requisition items included)
- Steelbars are shown in a separate tab
4. Canvass Suppliers
For Non-TOM Request:
- Each canvass item can have 1 up to 4 suppliers
- Suppliers must have valid information (name, contact, etc.)
- Suppliers must provide a price for the item
- Suppliers can offer discounts (percentage or fixed amount)
For TOM Request:
- Each canvass item can have 1 supplier only, cannot add more supplier
- Supplier drop-down would only contain list of COMPANIES and PROJECTS
- Suppliers must provide a price for the item - for confirmation
- Suppliers can offer discounts (percentage or fixed amount)
| JavaScript |
|---|
| // Example supplier processing logic
async function processSuppliers({
isDraft,
suppliers,
assignedTo,
transaction,
canvassItemId,
existingSuppliers,
isUpdateItems = false,
userFromToken,
canvassId,
}) {
if (isUpdateItems) {
const newSupplierIds = suppliers
.filter((supplier) => supplier.id)
.map((supplier) => supplier.id);
const suppliersToDelete = existingSuppliers
.filter((supplier) => !newSupplierIds.includes(supplier.id))
.map((supplier) => supplier.id);
if (suppliersToDelete.length > 0) {
await canvassItemSupplierRepository.destroy(
{
id: {
[Sequelize.Op.in]: suppliersToDelete,
},
},
{ transaction },
);
}
}
for (const supplier of suppliers) {
const { id: supplierId, attachments = [], ...supplierData } = supplier;
let canvassSupplier;
if (supplierId) {
canvassSupplier = await canvassItemSupplierRepository.update(
{ id: supplierId },
{
...supplierData,
assignedTo,
},
{ transaction, returning: true },
);
canvassSupplier = canvassSupplier[1][0];
} else {
canvassSupplier = await canvassItemSupplierRepository.create(
{
...supplierData,
canvassItemId,
assignedTo,
},
{ transaction },
);
}
// Process attachments
// ...
}
}
|
5. Canvass Approval
- Only FOR_CS_APPROVAL canvasses can be approved
- User must be an assigned approver for the canvass
- Approvers are processed in order by level
- If all approvers approve, status changes to CS_APPROVED
- Status will remain as FOR_CS_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
- During Purchasing Head approval, atleast 1 (one) or more supplier per item should be chosen
- Final Management approvals has 2 fixed approvers that can approve interchangeably
- Upon final approval, Purchase Orders are automatically created, grouped by supplier and terms
| JavaScript |
|---|
| // Example approval logic
async function approveCanvass({
existingCanvass,
approver,
canvassSuppliers,
transaction,
}) {
const { USER_TYPES } = constants.user;
const { CANVASS_STATUS, CANVASS_APPROVER_STATUS, CANVASS_ITEM_STATUS } = constants.canvass;
const forApprovalStatuses = [
CANVASS_STATUS.FOR_APPROVAL,
CANVASS_STATUS.REJECTED,
];
const isReadyForApproval = !forApprovalStatuses.includes(
existingCanvass.status,
);
if (isReadyForApproval) {
throw new Error('Canvass sheet is not for approval');
}
const approvers = await canvassApproverRepository.findAll({
where: { canvassRequisitionId: existingCanvass.id },
paginate: false,
order: [
['level', 'ASC'],
['isAdhoc', 'ASC'],
],
});
// Process approvers
// ...
// Process selected suppliers
if (canvassSuppliers.length > 0) {
for (const supplier of canvassSuppliers) {
await canvassItemSupplierRepository.update(
{ id: supplier.id },
{ isSelected: supplier.isSelected },
{ transaction },
);
}
}
// Check if all approvers have approved
const pendingApprovers = approvers.filter(
(a) => a.status === CANVASS_APPROVER_STATUS.PENDING,
);
if (pendingApprovers.length === 0) {
await canvassRequisitionRepository.update(
{ id: existingCanvass.id },
{ status: CANVASS_STATUS.APPROVED },
{ transaction },
);
await canvassItemRepository.update(
{ canvassRequisitionId: existingCanvass.id },
{ status: CANVASS_ITEM_STATUS.APPROVED },
{ transaction },
);
return true; // All approved
}
return false; // Not all approved yet
}
|
6. Canvass Rejection
- Only FOR_CS_APPROVAL canvasses can be rejected
- User must be an assigned approver for the canvass
- Rejection requires a reason/comment
- Upon rejection, status changes to CS_REJECTED
- Assigned Purchasing staff should be able to edit REJECTED canvass sheet
- Can resubmit and will undergo approval process again with status FOR_CS_APPROVAL
Validation Rules
Detailed validation rules are defined in the Zod schema:
| JavaScript |
|---|
| const createCanvassSchema = z
.object({
id: createNumberSchema('Canvass ID').optional(),
isDraft: z.boolean({
invalid_type_error: 'Invalid draft type',
required_error: 'Draft type is required',
}),
requisitionId: createNumberSchema('Requisition ID'),
addItems: z
.array(addItemsSchema, {
invalid_type_error: 'Invalid Canvass Items',
})
.optional(),
updateItems: z
.array(updateItemsSchema, {
invalid_type_error: 'Invalid Canvass Items',
})
.optional(),
deleteItems: z
.array(deleteItemsSchema, {
invalid_type_error: 'Invalid Canvass Item ID',
})
.optional(),
notes: z
.string(stringFieldError('Canvass note'))
.trim()
.max(100, 'Notes must not exceed 100 characters')
.regex(
/^[A-Za-z0-9'\s.-Ññ]+$/,
'Notes can only contain letters, numbers, spaces and specific special characters: -\'."',
)
.optional(),
})
.strict();
|