The "Get Items From" dropdown on a Purchase Invoice, Delivery Note or Sales Order sometimes shows draft source documents that shouldn't be selectable — the user picks one, hits Confirm, and ERPNext throws a message reading only Cannot map because following condition fails: docstatus=1. It looks like a bug in the picker. It isn't. The picker's list filter and the backend mapper's validator live in two different files and drift independently. This post pins the mismatch to the exact ERPNext v16.28.0 source paths, ships a copy-paste client-script gate that eliminates it for every source DocType, and gives you a server-side belt-and-braces for custom apps that call the mapper directly.
The picker and the mapper are two different guards. When they disagree, users see phantom bugs. I am Manoj, ERPNext and Frappe implementation lead at MithTech in Bengaluru — this is one of the most frequently misfiled tickets we inherit from teams migrating off Tally.
The two guards that must agree
The picker and the mapper are enforced in two entirely different files, by two entirely different mechanisms, and they can drift independently. That's the whole bug.
Three columns: UI list query, backend validator, and the mismatch that users experience as a bug.
get_query_filters: {
docstatus: 1,
status: ["not in", ["Closed"]],
company: me.frm.doc.company,
}"Purchase Receipt": {
"doctype": "Purchase Invoice",
"validation": {
"docstatus": ["=", 1]
},
}Cannot map because
following condition
fails: docstatus=1get_query_filters hides drafts from the picker; the server-side validation block is the real guarantee. Fix them in this order — UI first, backend never.The UI list query lives in the source form's JS
Every ERPNext form that offers a "Get Items From" button calls erpnext.utils.map_current_doc with a get_query_filters dict. That dict is handed to a MultiSelectDialog which renders the picker. If docstatus: 1 is present in the dict, drafts never reach the list.
You can see this in erpnext/public/js/utils.js around line 1095, where the utility hands get_query_filters to the dialog's get_query:
let query_args = {};
if (opts.get_query_filters) {
query_args.filters = opts.get_query_filters;
}
if (opts.get_query_method) {
query_args.query = opts.get_query_method;
}
if (query_args.filters || query_args.query) {
opts.get_query = () => JSON.parse(JSON.stringify(query_args));
}
Every caller — purchase_invoice.js, purchase_order.js, sales_order.js, quotation.js, supplier_quotation.js — is expected to populate get_query_filters with docstatus: 1 themselves. Most do. Some don't. The ones that don't produce the bug.
For example, the Purchase Invoice caller in erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js around line 191 gets it right:
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_invoice",
source_doctype: "Purchase Receipt",
target: me.frm,
get_query_filters: {
docstatus: 1,
status: ["not in", ["Closed", "Completed", "Return Issued"]],
company: me.frm.doc.company,
is_return: 0,
},
// ...
});
But this is a per-call convention, not a framework guarantee. A single caller (custom or upstream) that omits docstatus: 1 and the picker leaks drafts.
The backend validator lives in the mapper
Meanwhile, frappe.model.mapper.get_mapped_doc reads a per-map-config validation dict and throws before mapping if the source's field values don't match. From frappe/model/mapper.py:170:
def map_doc(source_doc, target_doc, table_map, source_parent=None):
if table_map.get("validation"):
for key, condition in table_map["validation"].items():
if condition[0] == "=" and source_doc.get(key) != condition[1]:
frappe.throw(
_("Cannot map because following condition fails:") + f" {key}={cstr(condition[1])}"
)
And every mapper in ERPNext core does declare it. Take erpnext/stock/doctype/purchase_receipt/purchase_receipt.py:1572, the make_purchase_invoice mapper:
doclist = get_mapped_doc(
"Purchase Receipt",
source_name,
{
"Purchase Receipt": {
"doctype": "Purchase Invoice",
"field_map": { ... },
"validation": {
"docstatus": ["=", 1],
},
},
# ...
},
target_doc,
set_missing_values,
)
The validation block is doing exactly what it should: rejecting a draft source before mapping. The problem is only that the rejection message reaches the user after the picker has offered them the draft — which is precisely when it feels like a bug.
Same shape in erpnext/selling/doctype/sales_order/sales_order.py:1171 for the make_delivery_note mapper, at line 1091 for make_material_request, at line 1455 for make_sales_invoice. All of them enforce docstatus=1. All of them can still be reached from a picker that offered a draft, because the two layers don't know about each other.
The mismatch, in one sentence
The picker is populated per-caller in JS; the validator is populated per-mapper in Python. When a caller omits docstatus: 1 from the picker's filter, the validator still holds — but only after the user commits — and the resulting error looks phantom.
Vanilla ERPNext picker showing drafts vs the same picker after the client-script gate is installed.
Draft docs appear in the picker
- · User selects PR-2026-0041 (draft)
- · Confirm → opaque backend throw
- · Support ticket lodged as an ERPNext bug
Only submitted docs show up
- · Drafts hidden at the client layer
- · Backend validator remains as fallback
- · Zero-code buying-team training
The fix — a client-side gate on the target form
You could hunt down every JS caller and add docstatus: 1 to its get_query_filters — for upstream files that means a PR against frappe/erpnext, for custom apps a scattered audit, and neither survives the next bench update. Don't. Do this instead: install a client script on each target DocType that patches erpnext.utils.map_current_doc once on refresh, injecting docstatus: 1 into every picker's filters.
Pick a source DocType; get a copy-paste Client Script for the corresponding target form that pre-filters the mapper's list query to submitted docs.
// Custom Script → DocType: Purchase Invoice
// Pre-filters the "Get Items From → Purchase Receipt" picker to submitted docs only.
frappe.ui.form.on('Purchase Invoice', {
refresh(frm) {
if (frm.doc.docstatus !== 0) return;
// Wait for erpnext.utils.map_current_doc to register buttons, then
// override the picker's get_query so drafts never reach the UI list.
const orig = erpnext.utils.map_current_doc;
if (orig && !orig.__mithtech_patched) {
erpnext.utils.map_current_doc = function (opts) {
if (opts.source_doctype === 'Purchase Receipt') {
opts.get_query_filters = Object.assign({}, opts.get_query_filters, {
docstatus: 1,
status: ["not in", ["Closed", "Completed", "Return Issued"]],
is_return: 0
});
}
return orig.call(this, opts);
};
erpnext.utils.map_current_doc.__mithtech_patched = true;
}
}
});Pre-flight: check the target's docstatus is 0 before patching
The generated script bails out with if (frm.doc.docstatus !== 0) return;. This matters. On a submitted or cancelled target document, the "Get Items From" button is hidden anyway — patching erpnext.utils.map_current_doc on those states is a no-op at best and, if the user amends the doc, a stale patch waiting to fire. Keep the docstatus guard.
Also: patch idempotently. The generator uses a __mithtech_patched marker so a second form refresh doesn't wrap the utility twice. Without it, a busy accountant tabbing between drafts stacks patches on top of themselves — first submit is fine, subsequent picks trigger the filter override N times, and browser DevTools logs a warning.
Fix guide, step by step
Identify the target DocType
The gate is installed on the target form — the one you're creating from source docs. If drafts appear when you click "Get Items From → Purchase Receipt" on a Purchase Invoice, the target is Purchase Invoice. Not Purchase Receipt.
Generate the client script from the widget above
Pick your source DocType in the DocStatus Filter Generator. Copy the snippet. It scopes the filter to a specific source (so unrelated pickers keep their own filters), adds docstatus: 1, and preserves the standard status filters ERPNext already uses for that source.
Create a Client Script
In your Frappe desk, go to Client Script → New. Set DocType to the target (e.g. Purchase Invoice), Apply To = Form, Enabled on. Paste the generated code. Save. No bench restart needed.
Test with a real draft
Open a new Purchase Invoice, click Get Items From → Purchase Receipt, filter search to a supplier known to have both submitted and draft PRs. Only submitted rows should appear. Cross-check by opening the raw source list in another tab — the drafts still exist, they're just hidden from the picker.
Roll out to every target-source pair you use
The most common target DocTypes that need this are Purchase Invoice, Delivery Note, Sales Invoice, Sales Order, Purchase Order, and Purchase Receipt. One Client Script per target, listing every source pair inside a single refresh handler if you have several.
What if buying owns POs and accounts owns invoicing?
The above assumes one team owns both source and target creation. In many SMEs they don't — buying issues Purchase Orders, stores books Purchase Receipts, accounts books Purchase Invoices from those PRs. That split is the whole reason drafts leak in the first place.
Buying / stores keep drafting freely
Draft POs and draft PRs are legitimate work-in-progress and shouldn't be hidden anywhere in the buying/stores workspaces. Do NOT install this gate on the source-side forms.
Accounts installs the gate on the target form (Purchase Invoice)
The Client Script is scoped to Purchase Invoice. It has no effect anywhere else. Accounts sees only submitted PRs in the picker; stores continues to see drafts in their own list views and the "Amend" flow.
Add a saved report — 'Draft Purchase Receipts awaiting submit'
Buying / stores manager runs this as a daily worklist. Filter Purchase Receipt by docstatus=0 and creation < today - 2 days — this is the queue of stale drafts that the accounts team can't invoice against yet. Segregation of duties intact; no chasing on Slack.
Add a workflow if approvals get formal
If your finance-controls team wants an explicit approval gate on PR submission — not just a docstatus flip — wire a Frappe Workflow on Purchase Receipt with a Reviewed state before Submitted. Our ERPNext approval workflow setup guide walks the DocType JSON.
The critical thing this split gets right: the client-script gate is a UX correction on the target form, not a permission override on the source. Stores keeps its scope; accounts stops filing phantom bug tickets against ERPNext.
Gates and automation — the permanent fix
The client-script snippet is the minimum. If you run a custom app already, package the same guard into it so it applies across sites without per-site Client Script maintenance.
Three copy-paste automations that ensure draft source docs never leak into a Get Items From picker or into a whitelisted mapper call: client script, server script, and custom-app hooks.py.
Patch erpnext.utils.map_current_doc on the target form to inject docstatus=1 into every Get Items From picker.
// Custom Script → DocType: Purchase Invoice
// Patches ERPNext's shared map_current_doc so every source DocType's
// picker is pre-filtered to submitted docs.
frappe.ui.form.on('Purchase Invoice', {
refresh(frm) {
if (frm.doc.docstatus !== 0) return;
const orig = erpnext.utils.map_current_doc;
if (!orig || orig.__mithtech_patched) return;
erpnext.utils.map_current_doc = function (opts) {
opts.get_query_filters = Object.assign({}, opts.get_query_filters, {
docstatus: 1,
});
return orig.call(this, opts);
};
erpnext.utils.map_current_doc.__mithtech_patched = true;
}
});What each recipe actually enforces
Client Script
Patches erpnext.utils.map_current_doc once on target-form refresh. Every picker opened from that form gets docstatus: 1 injected into its filters. No effect on server-side mapper calls.
Server Script (DocType Event)
Runs on before_insert of the target DocType. Walks the items table, looks up the source docstatus for every purchase_receipt, purchase_order or against_sales_order link, and throws with a readable message if any is not 1. Catches API and script-based mapper calls the client script can't reach.
Custom app hooks.py
Wires the same server guard as a doc_events handler in a packaged app, plus a bundled JS file (app_include_js) that installs the client-side patch site-wide. One deploy, every target form covered, upgrade-safe.
Deploy order that actually works
- Ship the Client Script first — the visible UX fix. Users stop filing bug tickets.
- Add the Server Script as a safety net after two weeks, once you've confirmed no legitimate flow relies on mapping a draft (there shouldn't be any).
- Repackage into a custom app only when the same gate is deployed on more than two production sites. Below that threshold, two Client Scripts and a Server Script per site is less overhead than maintaining an internal app.
When is any of this actually material?
A concrete threshold you can lift into your SOP
If your accounts team is going to run this gate as part of routine site hygiene, they need a written policy on which target DocTypes get the client script by default:
Mapper Gate Policy (draft — for engineering-lead review): Install the
docstatus: 1picker gate on any target DocType whose Get Items From button pulls from a source DocType that supports drafts (docstatus 0). Default coverage: Purchase Invoice, Purchase Order, Purchase Receipt, Delivery Note, Sales Invoice, Sales Order. Optional coverage: Quotation, Supplier Quotation, Material Request. The server-sidebefore_insertreject-drafts guard is mandatory on the same list if any custom app or external integration calls the mapper directly via REST orfrappe.call. Review quarterly; retire the gate on a target DocType only after a documented decision that mapping from drafts is a legitimate business flow.
Two things to notice about writing this down rather than leaving it to ad-hoc fixes:
- The gate is cheap to install and cheap to remove. There's no reason to leave a target DocType exposed unless there's an explicit business case.
- A written list survives staff churn. The next accounts hire who hits the picker in a fresh state won't wonder whether it's meant to show drafts — they'll check the SOP.
Related issues you may also hit
- ERPNext Landed Cost & FX Gone Wrong — the Get Items From → Purchase Receipt flow is the same pattern; if the picker offers a draft PR the FX gate is defeated too.
- ERPNext Landed Cost Voucher primer — LCVs are also created via
frappe.model.mapperfrom Purchase Receipts; the same docstatus mismatch shows up in the LCV picker. - ERPNext approval workflow setup — if you need docstatus-plus-workflow-state gating on the source form, the two mechanisms combine cleanly.
- ERPNext bugs, real fixes from GitHub and the forum — general index of picker/mapper mismatches we track.
FAQ
+Why does the 'Get Items From' picker show draft documents in the first place?
Because the picker's list filter and the backend mapper's validator are declared in two different files, by different maintainers, at different times. If the JS caller for a specific target→source pair forgets to set docstatus: 1 in its get_query_filters, the list query is unfiltered — even though the Python mapper still throws on Confirm.
+How do I hide draft docs from Get Items From in ERPNext without editing core?
Install a Client Script on the target DocType that patches erpnext.utils.map_current_doc once on form refresh. The script wraps every subsequent picker call and injects docstatus: 1 into get_query_filters. Use the DocStatus Filter Generator in this post to produce it for your source DocType.
+Does this apply to ERPNext v14 and v15, or just v16?
The exact source lines cited are from v16.28.0 (erpnext/public/js/utils.js:1095, frappe/model/mapper.py:170), but the same two-layer architecture — per-caller get_query_filters in JS, per-mapper validation in Python — has existed since v13. The Client Script works unchanged across v14, v15 and v16.
+What happens if I skip this and let users keep picking drafts?
Every picker that omits docstatus: 1 will surface drafts. Users will hit the Cannot map because following condition fails: docstatus=1 error, misfile it as an ERPNext bug, and lose 30 seconds of context each time. On a busy accounts desk that's cumulatively significant, and the phantom-bug tickets pollute your GitHub triage. The books stay correct — the backend validator holds — but the UX bleeds.
+Can I just remove the backend get_mapped_doc validation block instead?
No. The validation block is your last line of defence when a custom app, a REST integration, or a frappe.call from an external script tries to map a draft. The client script hides drafts in the picker; the server guard rejects them at the API layer. Both stay.
+Why does the fix live on the target form and not the source form?
Because drafts on the source form are legitimate — stores may have half-filled a Purchase Receipt while waiting for a serial-number entry, or accounts may have started a draft PI. Hiding drafts on the source form breaks the person creating them. The picker on the target form is the only place drafts should never appear.
+Will the Client Script slow down the target form?
No. The patch runs once per refresh (idempotently, via the __mithtech_patched marker), and each picker open injects a single object property. Sub-millisecond. frappe.db.get_value calls happen only inside the server-side guard, which runs at before_insert — not on picker open.
+Which target DocTypes should I install this on first?
Purchase Invoice, Delivery Note, and Sales Invoice cover 90% of reported cases — they're the DocTypes accounts users hit daily. Purchase Order and Purchase Receipt come next for buying and stores teams. Quotation and Supplier Quotation are usually low-frequency and can be handled reactively.
Closing
The "Get Items From" picker isn't buggy; it's under-specified. The picker is populated per-caller in JavaScript, the mapper is validated per-mapper in Python, and when the two disagree the resulting error message reads like an ERPNext bug even though both layers are behaving exactly as coded. Install the client-script gate on your target DocTypes, keep the server-side validator intact, and the entire class of ticket disappears.
Custom app maintenance eating your team's Fridays?
We run a one-week engagement for self-hosted ERPNext sites — audit your custom scripts, package repeat gates like this one into your app, and hand back a maintainable deploy.