Transparent supplier quotation comparison for structured procurement decisions.
rfqdiff compares supplier quotations across price, lead time and payment terms while keeping the scoring model explicit, deterministic and machine-readable.
Mixed-currency quotations should first be normalized with currency-normalizer. Technical compliance is deliberately kept outside this tool and is supplied by bidlint at the portfolio decision layer.
Supplier quotations are often compared in spreadsheets where assumptions, weights and recommendation logic become difficult to audit. rfqdiff keeps the commercial comparison small and inspectable: every score is produced from explicit inputs and explicit weights.
The goal is not to automate procurement judgment. The goal is to make a commercial comparison reproducible enough that a reviewer can understand how the recommendation was produced, see which criteria contributed to the result, and trace each loaded quotation back to its input file.
rfqdiff is responsible for commercial quotation comparison.
It does:
- compare price, lead time and payment terms;
- import quotations from JSON, CSV and Excel (
.xlsx); - use explicit default or user-supplied commercial scoring weights;
- produce deterministic supplier scores and criterion-level score contributions;
- identify the runner-up and score margin;
- return machine-readable decision explanations for downstream systems;
- export ranked comparison reports as CSV or Excel;
- attach source-file provenance to loaded quotations;
- preserve upstream normalization metadata when present in JSON inputs.
It intentionally does not:
- fetch or infer FX rates;
- determine technical compliance;
- score operational supplier risk;
- make contractual acceptance decisions;
- generate opaque AI rationale for a recommendation;
- accept hidden or unsupported scoring criteria;
- treat file hashes as digital signatures or proof of authenticity.
Those responsibilities remain separated across the engineering procurement toolchain.
Requirements: Python 3.11+.
git clone https://github.com/yigitcan-ozturk/rfqdiff.git
cd rfqdiff
python -m pip install .The installed command is:
rfqdiff --helpThe original python main.py ... source-checkout workflow remains supported for backward compatibility.
Compare separate JSON quotations:
rfqdiff samples/supplier_a.json samples/supplier_b.jsonCompare multiple suppliers from one CSV file:
rfqdiff samples/quotations.csvExcel workbooks use the same columns and can be passed directly:
rfqdiff quotations.xlsxMachine-readable output:
rfqdiff samples/quotations.csv --jsonWrite the same integration payload to a file:
rfqdiff samples/quotations.csv --output rfq.jsonExport a ranked comparison report:
rfqdiff samples/quotations.csv --report comparison.csv
rfqdiff samples/quotations.csv --report comparison.xlsxJSON, CSV and XLSX inputs can also be combined in one command as long as every quotation uses the same currency.
The default commercial model is:
| Criterion | Weight | Better score |
|---|---|---|
| Price | 50% | Lower |
| Lead time | 30% | Lower |
| Payment terms | 20% | Longer |
For a different procurement strategy, pass a JSON weights profile with --weights:
{
"price": 0.40,
"lead_time": 0.40,
"payment_terms": 0.20
}rfqdiff samples/quotations.csv --weights samples/weights-balanced.jsonWeight profiles are deliberately strict:
- all three supported criteria must be present;
- no additional criteria are accepted;
- each value must be between
0and1; - values must sum to exactly
1.0within floating-point tolerance.
The effective profile is returned in the output payload under weights, keeping each recommendation auditable.
Every scored supplier contains a score_breakdown showing the weighted contribution of each supported criterion to the final score:
{
"name": "Supplier A",
"score": 97.1,
"score_breakdown": {
"price": 47.1,
"lead_time": 30.0,
"payment_terms": 20.0
}
}The result also identifies the runner-up and the score margin between the top two suppliers. decision_explanation then exposes the winning supplier's component scores and the supplier leading each raw commercial criterion:
"decision_explanation": {
"winner": "Supplier A",
"runner_up": "Supplier B",
"score_margin": 30.0,
"winner_score_breakdown": {
"price": 47.1,
"lead_time": 30.0,
"payment_terms": 20.0
},
"criterion_leaders": {
"price": "Supplier B",
"lead_time": "Supplier A",
"payment_terms": "Supplier A"
}
}This is a deterministic explanation layer, not generated prose. Reviewers can reproduce every contribution from the quotation values and the effective weights rather than accepting an opaque narrative.
Every quotation loaded from a file receives an rfqdiff_source object. It records the input artifact used for that specific supplier row without storing the caller's full local path.
A JSON quotation receives:
"rfqdiff_source": {
"file": "supplier_a.json",
"format": "json",
"sha256": "..."
}CSV quotations additionally record the source row. Excel quotations record both the source row and active worksheet:
"rfqdiff_source": {
"file": "quotations.xlsx",
"format": "xlsx",
"sha256": "...",
"row": 2,
"sheet": "Commercial Quotes"
}The SHA-256 value is an integrity and traceability fingerprint for the input file. It helps reviewers verify that two results came from the same source bytes; it is not a digital signature and does not establish who created or approved the quotation.
rfqdiff_source is reserved by the tool and cannot be supplied in input data. This prevents imported quotations from spoofing provenance generated by rfqdiff itself.
The provenance object stays with each supplier through scoring and JSON output. CSV/XLSX comparison exports also include source file, format, hash, row and sheet columns where applicable.
--report produces reviewer-friendly exports without replacing the machine-readable JSON contract.
CSV reports contain one ranked supplier per row with:
- rank and recommended flag;
- supplier name, currency and price;
- lead time and payment terms;
- final score;
- price, lead-time and payment-term score contributions;
- source provenance fields.
Excel reports contain two worksheets:
Comparison— the ranked supplier table, component scores and source provenance;Summary— recommended supplier, runner-up, score margin, lowest price, fastest lead time, best payment terms and effective scoring weights.
The report uses the same scored payload as the JSON output, so the human-facing export and downstream integration result remain aligned.
A result includes the recommendation, ranked suppliers, decision summary, explanation, effective weights and any quotation provenance:
{
"tool": "rfqdiff",
"version": "0.2",
"currency": "EUR",
"recommended_supplier": "Supplier A",
"suppliers": [
{
"name": "Supplier A",
"score": 97.1,
"score_breakdown": {
"price": 47.1,
"lead_time": 30.0,
"payment_terms": 20.0
},
"rfqdiff_source": {
"file": "quotations.csv",
"format": "csv",
"sha256": "...",
"row": 2
}
}
],
"decision_summary": {
"recommended_supplier": {"name": "Supplier A", "score": 97.1},
"runner_up": {"name": "Supplier B", "score": 67.1},
"score_margin": 30.0
},
"weights": {
"price": 0.5,
"lead_time": 0.3,
"payment_terms": 0.2
}
}The full decision_summary also includes lowest price, fastest lead time and best payment terms. The full supplier objects include commercial values and any upstream normalization metadata.
import rfqdiff
scored = rfqdiff.score_quotes([
{"name": "A", "currency": "EUR", "price": 100, "lead_time_weeks": 4, "payment_days": 30},
{"name": "B", "currency": "EUR", "price": 120, "lead_time_weeks": 5, "payment_days": 0},
])Custom weights can be supplied through the public API:
weights = {"price": 0.4, "lead_time": 0.4, "payment_terms": 0.2}
scored = rfqdiff.score_quotes(quotes, weights)Tabular files and weight profiles can be loaded through the public API as well:
from pathlib import Path
import rfqdiff
quotes = rfqdiff.load_quotes(Path("quotations.xlsx"))
weights = rfqdiff.load_weights(Path("weights.json"))Comparison reports can be exported from a built result:
payload = rfqdiff.build_result(scored, "EUR", weights)
rfqdiff.write_report(payload, Path("comparison.xlsx"))A JSON quotation contains one supplier:
{
"name": "Supplier A",
"currency": "EUR",
"price": 84200,
"lead_time_weeks": 8,
"payment_days": 30
}CSV and Excel files use one supplier per row with these required columns:
| Column | Meaning |
|---|---|
name |
Supplier name |
currency |
ISO-style currency code used by the quotation |
price |
Commercial quotation value |
lead_time_weeks |
Lead time in weeks |
payment_days |
Payment term length in days |
Example CSV:
name,currency,price,lead_time_weeks,payment_days
Supplier A,EUR,84200,8,30
Supplier B,EUR,79400,14,0All quotations must use the same currency. For mixed currencies, normalize them first with currency-normalizer, then pass the normalized values to rfqdiff.
currency-normalizer ──> rfqdiff ───────────────────────┐
│
payment-terms-parser ──────────────────────────────────┼──> supplier-scorecard
│
vendor-risk-engine ────────────────────────────────────┤
│
bidlint ──> technical compliance ──────────────────────┘
supplier-scorecard reads the supplier score from the rfqdiff JSON payload as its quotation score. Engineering compliance remains an independent input from bidlint, keeping commercial and technical decisions separately auditable.
GitHub Actions validates:
- unit tests on Python 3.11, 3.12 and 3.13;
- JSON, CSV and Excel quotation loading;
- source-file provenance, row/sheet traceability and SHA-256 fingerprints;
- rejection of caller-supplied reserved provenance fields;
- default and configurable scoring profiles;
- criterion-level score breakdowns, runner-up selection and score margins;
- rejection of incomplete or unsupported weight profiles;
- CSV and Excel comparison report exports;
- wheel and source-distribution builds;
- package metadata with
twine check; - installation of the built wheel and runtime dependencies;
- the installed
rfqdiffconsole command and public package namespace.
- Explicit scoring — weights, criterion contributions and final scores remain visible.
- Deterministic explanations — recommendation rationale is computed from the same scoring model, not generated as opaque prose.
- Traceable inputs — loaded quotations retain source artifact fingerprints and tabular locations.
- Deterministic results — identical supported inputs produce identical comparison results.
- Separation of concerns — FX, technical compliance and supplier risk stay outside quotation scoring.
- Machine-readable handoff — downstream tools consume a stable structured result instead of scraping presentation text.
- Review before authority — a recommendation supports procurement judgment; it does not replace approval.
| Tool | Role |
|---|---|
currency-normalizer |
Normalize quotation values across currencies |
rfqdiff |
Compare and explain normalized commercial quotations |
payment-terms-parser |
Convert payment terms into commercial-risk signals |
vendor-risk-engine |
Score operational, quality, compliance and dependency risk |
bidlint |
Produce evidence-backed technical-compliance findings and scores |
supplier-scorecard |
Combine commercial, risk and technical signals into one supplier recommendation |
Early-stage project, currently at v0.2. The v0.2 development line now provides JSON/CSV/XLSX quotation ingestion, auditable configurable weights, criterion-level scoring explanations, source provenance, CSV/XLSX report export, an installable Python package and console CLI, and a stable machine-readable integration contract.
The original v0.2 product roadmap is implemented. The next milestone is release/pilot hardening rather than adding more scoring criteria to this tool.
Released under the MIT License.