Manage supplier relationships with a portal for purchase orders, dropship routing, delivery tracking, and vendor performance scorecards
Vendor management covers creating and sending purchase orders to suppliers, tracking goods receipts, running vendor performance scorecards (on-time rate, fill rate, defect rate), and maintaining a supplier portal where vendors can acknowledge POs and provide tracking numbers. For most merchants, QuickBooks/Xero handles POs and vendor tracking, while a supplier portal can be as simple as an email-based workflow for smaller operations.
| Store Stage | Recommended Tool | Why |
|---|---|---|
| Small (< $1M revenue) | QuickBooks Online or Xero + email-based POs | QuickBooks handles POs, vendor payments, and basic receipt tracking; most small merchants don't need a dedicated vendor portal |
| Mid-market ($1M–$20M) | DEAR Inventory (now Cin7 Core) or Unleashed | Both handle POs, goods receipts, and vendor scorecards with Shopify/WooCommerce/BigCommerce integrations |
| Enterprise | NetSuite or Coupa | Full procurement suite with approval workflows, supplier portals, and compliance tracking |
| Dropshipping focus | DSers, AutoDS, or Spocket | See @dropshipping-integration skill for supplier-specific dropshipping tools |
| Custom | Build a PO system + supplier portal | For unique workflows that existing tools don't support |
Using Cin7 Core (formerly DEAR Inventory):
Using QuickBooks Online for POs:
Using ATUM Inventory Management (PO module):
Using Cin7 Core:
Using Unleashed:
Using Cin7 Core:
A good PO includes: your PO number, item descriptions with your SKU and the supplier's SKU, quantities, unit costs, expected delivery date, and delivery address.
Subject: Purchase Order PO-202603-001
Dear [Supplier Contact],
Please find our purchase order details below:
PO Number: PO-202603-001
Order Date: March 12, 2026
Expected Delivery: March 26, 2026
Items:
| SKU | Description | Qty | Unit Cost | Total |
|-----|-------------|-----|-----------|-------|
| YS-1001 | Cotton T-Shirt, White, S | 100 | $8.50 | $850.00 |
| YS-1002 | Cotton T-Shirt, White, M | 150 | $8.50 | $1,275.00 |
Total: $2,125.00
Payment Terms: Net 30
Please confirm receipt of this order and your expected ship date.
Ship to:
[Your warehouse address]
For suppliers who work via email, a simple acknowledgment email workflow is sufficient. For higher-volume supplier relationships, a portal improves visibility.
Simple email-based workflow:
ATUM Supplier Portal (WooCommerce):
Cin7 Core Supplier Portal:
When a shipment arrives from a supplier:
In QuickBooks:
In Cin7 Core / ATUM:
Run vendor scorecards quarterly to identify underperforming suppliers and support negotiation.
Key metrics:
| Metric | Definition | Target |
|---|---|---|
| On-time delivery rate | % of POs where actual receipt date ≤ expected delivery date | > 90% |
| Fill rate | Total units received / total units ordered across all POs | > 95% |
| Defect rate | Damaged/incorrect units received / total units received | < 2% |
| Lead time accuracy | Actual lead time vs. quoted lead time | ± 2 days |
Calculate from your PO system:
In QuickBooks, export PO data to a spreadsheet and compute these metrics. In Cin7 Core and ATUM, built-in vendor reports provide these metrics automatically.
async function calculateVendorScorecard(params: {
vendorId: string;
periodDays: number;
}): Promise<{
onTimeRatePct: number;
fillRatePct: number;
defectRatePct: number;
overallScore: number; // 0–100
}> {
const since = new Date(Date.now() - params.periodDays * 86400000);
const pos = await db.purchaseOrders.findAll({
vendor_id: params.vendorId,
status: { in: ['received', 'partial'] },
created_at: { gte: since },
});
if (pos.length === 0) return { onTimeRatePct: 0, fillRatePct: 0, defectRatePct: 0, overallScore: 0 };
// On-time: PO received on or before expected delivery date
const onTimeCount = pos.filter(po => po.received_at && po.received_at <= new Date(`${po.expected_delivery}T23:59:59Z`)).length;
// Fill rate: total received / total ordered
const allLines = await db.poLines.findByPoIds(pos.map(p => p.id));
const totalOrdered = allLines.reduce((s, l) => s + l.quantity_ordered, 0);
const totalReceived = allLines.reduce((s, l) => s + l.quantity_received, 0);
// Defect rate: damaged units / total received
const totalDamaged = await db.damagedReceipts.sumByVendorAndPeriod(params.vendorId, since);
const onTimeRatePct = (onTimeCount / pos.length) * 100;
const fillRatePct = totalOrdered > 0 ? (totalReceived / totalOrdered) * 100 : 0;
const defectRatePct = totalReceived > 0 ? (totalDamaged / totalReceived) * 100 : 0;
// Weighted score: on-time 40%, fill rate 40%, defect-free 20%
const overallScore = Math.round(
(onTimeRatePct * 0.4) + (fillRatePct * 0.4) + ((100 - defectRatePct) * 0.2)
);
return { onTimeRatePct, fillRatePct, defectRatePct, overallScore };
}
| Problem | Solution |
|---|---|
| PO total doesn't match the supplier's invoice | Store unit costs at the PO line level and never update them retroactively; price discrepancies become AP exceptions flagged for review |
| Supplier ships to the wrong address | Include the warehouse address on every PO and in the portal confirmation screen; verify the ship-to address has been acknowledged |
| Inventory incremented before damaged goods are removed from the count | Only increment inventory for receipts with condition = 'good'; damaged goods go to a quarantine count, not sellable stock |
| Scorecard shows high on-time rate but stock still runs out | On-time rate measures delivery vs. expected date, not vs. actual demand need date; also track "stockout events attributed to late delivery" as a separate metric |