---
title: "Sending RFQs"
space: "Wiki"
url: "https://jmtradelink.com/docs/vendor-rates-combinations/sending-rfqs"
updated: "2026-06-22"
---

# Sending RFQs

Section: Vendor Rates & Combinations

---

## Overview

Once an Export Costing has been built with its charge heads, the next step is to get vendor rates. The app generates **one Request for Quotation (RFQ) per cost category** rather than one combined sheet. This is deliberate: a rice mill should never see freight costs, and a freight forwarder should never see your purchase price. Each supplier receives only the rows that belong to their trade.

Vendors can then quote without an ERPNext login — via a tokenized guest link or the email that RFQ submission fires — and the result is a submitted Supplier Quotation that flows directly into the combination engine.

---

## What Can Be Put on an RFQ

Not every charge head is quotable. The constant `RFQ_QTY_BASES` in `export_costing_actions.py` defines the allowed unit types:

| Qty Basis | Can be RFQ'd? | Reason |
|---|---|---|
| Per Net MT | Yes | Vendor can name a unit price |
| Per Gross MT | Yes | Vendor can name a unit price |
| Per Kg | Yes | Vendor can name a unit price |
| Per Bag | Yes | Vendor can name a unit price |
| Per Container | Yes | Vendor can name a unit price |
| Per BL | Yes | Vendor can name a unit price |
| Lumpsum | Yes | One total — still a direct vendor quote |
| Percent of Subtotal | **No** | Derived from other costs; no vendor sets this |
| Manual | **No** | Entered directly by the team |

> **Why this matters:** A `Percent of Subtotal` row (e.g. insurance calculated as a percentage of the FOB value) is purely a formula — there is no vendor to ask. Sending it on an RFQ would make no sense and the function throws `"No quotable charge head selected — derived bases (percent/manual) cannot be RFQ'd."` if the selection collapses to zero valid rows.

---

## Creating RFQs: `make_requests_for_quotation`

**Entry point:** `Create ▸ Requests for Quotation` on the Export Costing form.

**Function:** `export_management.export_management.doctype.export_costing.export_costing_actions.make_requests_for_quotation`

**Parameters:**

| Parameter | Type | Description |
|---|---|---|
| `export_costing` | `str` | Name of the Export Costing document |
| `charge_items` | `str \| list \| None` | Optional filter — only create RFQs for these charge heads. If omitted, all quotable heads get an RFQ. |

### What the Function Does, Step by Step

**1. Permission check.** The function calls `costing.check_permission("write")` — a user who can only read the costing cannot generate RFQs.

**2. Filter to quotable rows.** All rows in `costing.charges` whose `qty_basis` is in `RFQ_QTY_BASES` are collected. If the caller passed a `charge_items` list, it narrows further to those heads.

**3. Resolve item metadata in bulk.** To avoid N+1 queries, a single `frappe.get_all` fetches every relevant `Export Charge Item` (to get `is_goods` and `service_item`), and a second `frappe.get_all` fetches every relevant `Item` (to get `stock_uom`, `item_name`, `description`).

**4. One RFQ per charge row.** For each quotable charge row the function creates a new `Request for Quotation` document and appends exactly one item row. The item placed on the RFQ depends on the head type:

| Head type (`is_goods`) | Item put on the RFQ | `applies_to_item` |
|---|---|---|
| **Goods** (e.g. Procurement Cost) | The real product `Item` from the costing row (`charge_row.item_code`) | The product item code — scopes the rate to this commodity |
| **Service** (e.g. Sea Freight, CFS Handling) | The `Export Charge Item`'s synthetic `service_item` | `None` |

This means a rice procurement RFQ contains the actual rice `Item` — so the eventual Purchase Order buys the commodity rather than an abstract service. A freight RFQ contains the synthetic freight service item. For goods heads, `applies_to_item` is set to `charge_row.item_code`; for service heads it is left `None`.

**5. Route fields stamped on the RFQ.** Three fields from the costing are copied to the header of every RFQ so the supplier and the Pull Rates step know the trade route:

| Field on RFQ | Source on Costing |
|---|---|
| `em_port_of_loading` | `costing.port_of_loading` |
| `em_port_of_discharge` | `costing.port_of_discharge` |
| `em_container_type` | `costing.container_type` |

These custom fields carry the `em_` prefix to prevent collisions with ERPNext's own fields. The standard RFQ → Supplier Quotation mapper copies them automatically to the SQ header.

**6. Item row fields.** Beyond the standard ERPNext item row fields, each row carries:

| Custom field | Value | Purpose |
|---|---|---|
| `charge_item` | The charge head name (e.g. `"Sea Freight"`) | Links the SQ row back to the right charge head during Pull Rates |
| `em_qty_basis` | The row's `qty_basis` (e.g. `"Per Container"`) | Tells Pull Rates how to apply the rate |
| `applies_to_item` | The product item code (goods heads only; `None` for service heads) | Scopes the rate to one commodity when a costing has multiple products |

**7. Email template.** Every RFQ gets `email_template = "Vendor Rate Quote Request"` (the constant `RFQ_EMAIL_TEMPLATE_NAME`). This is the branded template shipped with the app (`vendor_rate_quote_request.html`). The template is created once on install/migrate and is intentionally never overwritten by later migrations so desk edits survive.

**8. Inserted as Draft with `ignore_mandatory`.** ERPNext's RFQ doctype requires at least one supplier before it can save. But the supplier list is the user's next step on the RFQ form — they know their vendors, not the code. The function inserts each RFQ with `ignore_mandatory=True` so the draft lands in the system immediately. When the user submits the RFQ after adding suppliers, ERPNext re-runs all mandatory validation, meaning an RFQ with no suppliers can never transition to the submitted state that triggers email and guest links.

The function returns the list of RFQ names it created.

---

## After Creation: Add Suppliers and Submit

On each generated RFQ (linked from the Export Costing's RFQ list):

1. Open the RFQ.
2. Add the relevant suppliers to the `suppliers` table — mills for goods heads, forwarders for service heads, etc.
3. Set `Send Email = Yes` and confirm the `email_id` for each supplier row you want to notify.
4. **Submit** the RFQ.

On submit, the `before_submit` hook in `doc_events/request_for_quotation.py` mints a unique 32-character random token (`em_guest_token`) for every supplier row that does not already have one. These tokens are stored on the `Request for Quotation Supplier` child row with `allow_on_submit = 1` (so they can be written to a submitted document).

> **Why tokens are minted before submit, not during email render:** Frappe's Jinja sandbox forbids database writes mid-render. The email template calls `get_guest_quote_url`, which must only read the existing token. Pre-minting in `before_submit` keeps that path read-only.

---

## Two Ways Vendors Quote

Both paths produce a **submitted Supplier Quotation**. This is intentional — the combination engine only consumes submitted SQs, so vendor rates flow into the costing without a manual review or submission step.

### Path 1: No-Login Guest Link (`/quote/<token>`)

**For vendors who will not or cannot use ERPNext.**

After the RFQ is submitted, go to **Guest Quote Links** on the RFQ form. This calls `get_guest_quote_links`, which:

- Verifies the RFQ is submitted (throws if not).
- Iterates the RFQ's supplier rows.
- Returns each supplier's `em_guest_token` (minting it lazily if somehow absent).
- Returns a list of `{supplier, link}` dicts where `link` is the full URL `/quote/<token>`.

Send the link to the vendor by any channel (WhatsApp, manual email, etc.).

**What the vendor sees at `/quote/<token>`:**

The page resolves the token via `resolve_token`, which looks up the `Request for Quotation Supplier` row where `em_guest_token` matches and `docstatus = 1` (i.e. the RFQ is submitted). If the token does not exist or the RFQ is not submitted, the page shows an "invalid or withdrawn" error.

The vendor sees the RFQ's item rows and can enter rates. Crucially, **vendors can quote multiple port variants for the same head**: one rate with a blank port (ex-works — buyer arranges pickup from the mill) and additional rates keyed to specific ports of loading (delivered-at-port). Each port variant becomes a separate row on the resulting Supplier Quotation item table, with `em_port_of_loading` set accordingly. A blank port means "any port / ex-works wildcard."

When the vendor submits, `submit_guest_quote` runs server-side. The server trusts only the rate values from the guest — everything else (supplier, company, item codes, route fields, `charge_item`, `em_qty_basis`, `applies_to_item`) is reconstructed from the RFQ via the token. This means a guest can only ever set prices, never forge metadata.

The resulting Supplier Quotation is immediately **submitted** (`quotation.submit()`). Optional notes the vendor types are saved as a Comment on the SQ.

One submission per link: if a Supplier Quotation already exists (draft or submitted) for this RFQ + supplier pair, the endpoint throws `"A quotation for this link was already submitted — thank you."` The endpoint is also rate-limited to 10 calls per 60 seconds per token.

**Revoking a guest link:** Clear the `em_guest_token` field on the RFQ supplier row. The field is `hidden = 1` but the desk shows it via the Guest Quote Links helper. Once cleared, the old URL returns "invalid or withdrawn."

### Path 2: Email

**For vendors comfortable with email.**

When the RFQ is submitted, ERPNext's standard submit flow sends the configured `email_template` to suppliers with `send_email = Yes`. The "Vendor Rate Quote Request" template renders a branded email with the team's identity and a **"Submit your Quotation" button** that links to `/quote/<token>` — the same guest page as Path 1.

There is also a **Resend** button on the RFQ (calling `send_guest_rfq_emails`) that re-sends the branded email to all supplier rows that have `send_email = Yes` and a valid `email_id`. Already-sent rows are re-sent; this is intentional (a manual desk action).

> **One page, two entry channels.** Both paths land on `/quote/<token>`. There is no separate "email portal" vs "direct link" portal — only one no-login quote page. This simplifies support: if a vendor claims they cannot submit, the diagnosis is always the same page.

---

## Field Reference: Custom Fields Added by This App

### On `Request for Quotation` (header)

| Fieldname | Type | Description |
|---|---|---|
| `export_costing` | Link → Export Costing | The costing this RFQ was raised from |
| `em_port_of_loading` | Link → Port | Trade route: origin port |
| `em_port_of_discharge` | Link → Port | Trade route: destination port |
| `em_container_type` | Select | Container specification (20' FCL, 40' FCL, 40' HC) |

### On `Request for Quotation Item` (item rows)

| Fieldname | Type | Description |
|---|---|---|
| `charge_item` | Link → Export Charge Item | Which cost category this row prices |
| `em_qty_basis` | Select | Unit basis (Per Net MT, Per Container, Lumpsum, etc.) |
| `applies_to_item` | Link → Item | For goods heads: which product this rate is for; `None` for service heads |
| `em_port_of_loading` | Link → Port | Per-row port: blank = ex-works/any-port wildcard; set = rate landed at this port |

### On `Request for Quotation Supplier` (supplier rows)

| Fieldname | Type | Description |
|---|---|---|
| `em_guest_token` | Data | 32-char random token backing `/quote/<token>`; hidden, read-only, `allow_on_submit`; clear to revoke |

The same route fields (`export_costing`, `em_port_of_loading`, `em_port_of_discharge`, `em_container_type`) and item row fields (`charge_item`, `em_qty_basis`, `applies_to_item`, `em_port_of_loading`) are added to `Supplier Quotation` and `Supplier Quotation Item` respectively, so the standard ERPNext RFQ → SQ mapper copies them without any custom logic.

---

## Demo Walk-Through

In the demo seed (`export.localhost:8010`):

- The seed inserts submitted Supplier Quotations directly (bypassing the RFQ flow) so the demo costing already has rates and a winning combination applied.
- To exercise the full RFQ path from scratch, create a new Export Costing from any Opportunity, apply a costing template, then use **Create ▸ Requests for Quotation**. Tick "Procurement Cost" and "Sea Freight" to generate two RFQs — one will show the rice item, the other the freight service item. Add yourself as a supplier with a real email and submit.

---

## Common Mistakes

> **Submitting without suppliers:** The draft insert with `ignore_mandatory=True` means the RFQ saves even if the suppliers table is empty. ERPNext will refuse to submit in that state. Always add at least one supplier before clicking Submit.

> **Sending the guest link before submitting the RFQ:** `get_guest_quote_links` throws `"Submit the RFQ first — guest links are for sent requests."` Guest tokens are only meaningful on submitted RFQs.

> **Trying to RFQ a Percent-of-Subtotal head:** The function silently drops non-quotable rows. If ALL selected heads are non-quotable, it throws. If only some are non-quotable, the others proceed without warning — this is expected behavior, not a bug.

> **Vendor submits twice:** The one-submission guard on `submit_guest_quote` blocks a second submission on the same token. If the vendor made an error, a team member must cancel the first SQ and clear the token to issue a fresh link (or mint a new token by saving the supplier row).
