ERP

ERPNext Strict User Permissions Hides Every Purchase Invoice (And Asset and Shipment Are Still Broken)

Strict User Permissions plus a Company User Permission set to Apply to All Document Types empties the Purchase Invoice list with no error. ERPNext fixed it with a one-line JSON flag in v15.116.0 and v16.27.0 — which means Asset and Shipment still carry the identical bug.

MManojAugust 5, 202614 min read
#erpnext#permissions#multi-company#frappe
Share

A user with Read permission on Purchase Invoice opens the list and sees "No Purchase Invoice found." Purchase Order works. Sales Invoice works. Delivery Note works. Only Purchase Invoice is empty, and ERPNext shows no error, no warning, and no hint that a filter was applied. The cause is Apply Strict User Permissions in System Settings combined with a Company User Permission left on its default "Apply to All Document Types" — the permission engine ANDs a condition onto represents_company, a field that is NULL on every invoice from an ordinary external supplier. ERPNext closed this in issue #57072 with PR #57073: a single JSON flag on one field, shipped in v15.116.0 and v16.27.0. That is the part worth pausing on. Because the fix was a per-field flag rather than a change to the permission engine, the same bug is still live on Asset and on Shipment — and upgrading will not save you from either.

Running more than one company in one instance?

This failure mode only exists because you are scoping users to a Company. If you are still designing that structure, start with the ERPNext multi-company setup guide — it covers inter-company transactions and consolidated reporting, which is where represents_company came from in the first place.

I am Manoj, ERP implementation lead at Mith Tech in Bengaluru. We enable Strict User Permissions on most of the ERPNext implementations we run for multi-company groups, because the alternative — blank link fields quietly counting as "allowed" — is not something an auditor accepts. That makes this class of bug our problem rather than a curiosity, and it is why we went and audited every doctype in the codebase rather than stopping at the one that got patched.

What actually happens in the query

The permission engine builds list-view filters in DatabaseQuery.add_user_permissions(), in frappe/model/db_query.py. Three properties of that function combine into the bug, and all three are working as designed:

  1. It iterates every Link field on the doctype, via self.doctype_meta.get_link_fields() — not just the field you think of as the scoping one. A User Permission on Company with Apply to All Document Types populates user_permissions["Company"], so every Link-to-Company field on the doctype produces a condition.
  2. It joins those conditions with AND. Every non-exempt Link-to-Company field must match. There is no "at least one" mode.
  3. Strict mode deletes the escape hatch. With strict off, each condition is prefixed with ifnull(field, '') = '' or, so a blank field passes. With strict on, that disjunct is simply not emitted.

Only ignore_user_permissions on the field itself makes the loop skip a field. That single flag is the whole surface area of the fix.

InteractiveMatch Condition Visualiserlink

Toggle Apply Strict User Permissions and Apply to All Document Types and watch the WHERE clause ERPNext generates for a pre-fix Purchase Invoice list. The ifnull escape appears and disappears in real time, and the verdict panel says whether the user sees rows or an empty list.

Generated WHERE clause — Purchase Invoice, pre-v15.116.0
((
  (`tabPurchase Invoice`.`company` in ('XYZ Ltd'))
  and
  (`tabPurchase Invoice`.`represents_company` in ('XYZ Ltd'))
))
Blank-field escape
removed
Clauses joined with
AND
Rows returned
zero
“No Purchase Invoice found”

represents_company is NULL on every invoice from an ordinary external supplier, and NULL in ('XYZ Ltd') is never true. The AND collapses the whole list to zero rows — with no error message anywhere in the UI.

Generated by DatabaseQuery.add_user_permissions() in frappe/model/db_query.py, which iterates every Link field of the doctype, skips only those flagged ignore_user_permissions, and joins the surviving conditions with AND.

With strict on and a permission of Allow = Company, For Value = 'XYZ Ltd', Apply to All Document Types = 1, the list query gets:

((
  (`tabPurchase Invoice`.`company` in ('XYZ Ltd'))
  and
  (`tabPurchase Invoice`.`represents_company` in ('XYZ Ltd'))
))

represents_company is NULL on every purchase invoice from an ordinary external supplier, and NULL in ('XYZ Ltd') is never true. Zero rows. The form view is slightly more honest: open a specific invoice by URL and has_user_permission() produces "You are not allowed to access this Purchase Invoice record because it is linked to Company 'empty' in field Represents Company." That message is the only place the system tells you what happened — and nobody reaches it, because the list view gave them nothing to click.

Why represents_company is blank

represents_company exists for inter-company transactions: when one company in your group buys from or sells to another company in the same instance. On the Supplier master it is gated behind is_internal_supplier, and — notably — Supplier's copy already carried ignore_user_permissions: 1. On Purchase Invoice it is a fetched mirror ("fetch_from": "supplier.represents_company"), typed in Python as DF.Link | None.

So for every ordinary external supplier the field is legitimately NULL, and in a typical install roughly all purchase invoices have it blank. It is an identity field, not a scoping field. The scoping field is company. That distinction is precisely what ignore_user_permissions is for, which is why the one-line fix is correct rather than a hack — and why the accompanying "read_only": 1 makes sense for a value nobody should type by hand.

Our finding: the fix does not generalise

Because the maintainers chose a per-field flag over a framework change, the correctness of the whole system now depends on every doctype having the flag set correctly on every optional link. So we checked. We pulled the full ERPNext v15.116.0 source tarball, parsed all 659 non-child DocType JSON files, and listed every doctype with more than one Link → Company field along with each field's ignore_user_permissions value. Then we re-verified the interesting ones against v15.119.0, the latest release at the time of writing.

Eleven doctypes have more than one Link to Company. Nine are fine. Two are not.

InteractiveField Exposure Auditorlink

Every ERPNext doctype carrying more than one Link-to-Company field, with each field's ignore_user_permissions flag, whether it is usually populated on a real record, the match condition strict mode generates, and a verdict on whether the list view survives.

Doctypes with more than one Link to Company
companyLink → Companyignore_user_permissions: 0 usually filled

Mandatory. This is the real scoping field — never exempt it.

represents_companyLink → Companyignore_user_permissions: 1 usually NULL

Fetched from supplier.represents_company. NULL unless the supplier is flagged Is Internal Supplier. Given ignore_user_permissions: 1 (and read_only: 1) by PR #57073.

Match condition generated in strict mode
((
  (`tabPurchase Invoice`.`company` in ('XYZ Ltd'))
))
Link-to-Company fields
2
Enforced (not exempt)
1
Enforced but usually NULL
0
List view works

Fixed in v15.116.0 / v16.27.0. Before those releases the list view silently returned zero rows.

Issue #57072, PR #57073 + backports #57074 (v15) / #57075 (v16), merged 2026-07-12/13.

Asset. asset_owner_company is still ignore_user_permissions: 0. It only appears when asset_owner == "Company", so for any asset owned by a supplier or customer — third-party, leased, consignment — it is NULL. In strict mode with an Apply-to-All Company permission, those assets vanish from the Asset list exactly the way purchase invoices did. This is not speculation: the reporter of #57072 hit it in production and said so in the thread on 2026-05-06, and the comment that proposed the ignore_user_permissions approach explicitly named asset_owner_company alongside represents_company. Only the Purchase Invoice half shipped. As of August 2026 there is no released fix.

Shipment. pickup_company and delivery_company are both conditional (pickup_from_type == 'Company' and delivery_to_type == 'Company') and both ignore_user_permissions: 0. On a normal outbound shipment delivery_company is blank; on an inbound one pickup_company is blank. The engine's behaviour is verified and the field metadata is verified, so the Shipment list should go empty for restricted users in both directions.

Be clear about what we did and did not test

The Shipment case is a code-level inference. We read the field flags directly from the v15.119.0 source and we know how add_user_permissions() behaves, but we did not reproduce it on a live strict-mode instance and there is no public bug report for it. Treat it as a strong reason to check your own instance with the Verify recipe below — not as a confirmed defect.

The generalisable rule is the real takeaway, and it is not about Company at all: Strict User Permissions with Apply to All Document Types is only safe on a doctype where every non-exempt Link field pointing at the restricted doctype is always populated. Any optional or conditional second link to the same target silently ANDs the list to zero. Warehouse, Cost Center, Project, Territory and Branch have the identical failure mode wherever a doctype carries two links to them.

There is precedent for this being handled one field at a time rather than structurally: v15.116.0 and v16.27.0 also shipped "ignore user permissions for link fields having link to Account and Cost Center" on the Company doctype. Same disease, different link target, separate patch.

Diagnose, fix, verify

Code recipeDiagnose / Fix / Verify Recipelink

Three copyable tabs: a bench console script that lists every at-risk doctype on your instance, three fix routes including the Property Setter that reproduces the upstream patch, and a verification script that prints the actual match condition the permission engine generates for a restricted user.

Run in bench console as Administrator. Confirms the strict-mode setting, lists the Company user permissions that apply to all document types, and prints every doctype whose Link-to-Company fields are not all exempt — including Asset and Shipment.

python
# bench --site yoursite console
# Which doctypes will strict mode silently empty?

import frappe

print("apply_strict_user_permissions =",
      frappe.get_system_settings("apply_strict_user_permissions"))

# 1. User Permissions on Company that apply to all doctypes
rows = frappe.get_all(
    "User Permission",
    filters={"allow": "Company"},
    fields=["user", "for_value", "applicable_for",
            "apply_to_all_doctypes"],
)
for r in rows:
    scope = r.applicable_for or "ALL DOCTYPES"
    print(f"{r.user:<32} {r.for_value:<20} -> {scope}")

# 2. Every doctype with >1 non-exempt Link to Company
for dt in frappe.get_all("DocType",
                         filters={"istable": 0, "issingle": 0},
                         pluck="name"):
    try:
        meta = frappe.get_meta(dt)
    except Exception:
        continue

    links = [f for f in meta.get_link_fields()
             if f.options == "Company"]
    enforced = [f.fieldname for f in links
                if not f.ignore_user_permissions]

    if len(enforced) > 1:
        print(f"AT RISK  {dt:<30} {enforced}")

# 3. How many rows actually have the second field blank?
print(frappe.db.sql("""
    select count(*) from `tabPurchase Invoice`
    where docstatus < 2
      and ifnull(represents_company, '') = ''
"""))

Isolate it with the strict toggle

As Administrator, untick Apply Strict User Permissions in System Settings and reload the Purchase Invoice list as the restricted user. If it fills up, you have confirmed the cause in about ten seconds. Re-tick it — you are not done, and leaving it off is the one "fix" that quietly discards the control you wanted.

Read the match condition, do not guess it

Run the Verify tab. DatabaseQuery("Purchase Invoice").build_match_conditions() executed as the restricted user prints the exact WHERE fragment. Two ANDed clauses means you are on a pre-fix build; one clause on company alone means the flag is in place. This beats eyeballing a list, because an empty list has several possible causes and only one of them is this bug.

Apply the flag through Customize Form

Customize Form → Enter Form Type = Purchase Invoice → expand the represents_company row → tick Ignore User Permissions → Update. Thirty seconds, no restart, no code. It writes a Property Setter, so it survives bench migrate. This is byte-for-byte the same effect as the upstream patch.

Run the audit before you trust the upgrade

The Diagnose tab enumerates every doctype on your instance with more than one non-exempt Link to Company, including any custom doctypes and any custom fields you have added. Custom fields matter here: a Link-to-Company custom field on a transactional doctype reproduces this bug on a fully patched ERPNext.

Decide on Asset and Shipment deliberately

If you use Assets with third-party owners, or Shipments in either direction, apply the same Property Setter to asset_owner_company, pickup_company and delivery_company — or accept that those lists are filtered to zero for restricted users. Do not leave it undecided, because the failure is silent and nobody will report it as a bug.

Upgrade, then re-verify

Move to v15.116.0+ or v16.27.0+ on a schedule, in staging first. Your Property Setters remain afterwards and hold the same values as the standard JSON — redundant on Purchase Invoice, still load-bearing on Asset and Shipment. Delete the redundant one when you next clean up your customisation inventory.

Choosing between the four options

A fifth suggestion appears in the issue thread and is worth heading off: a custom permission_query_conditions hook. It reads like a fix and is not one. Conditions returned by that hook are ANDed onto the user-permission match conditions, so a hook can only narrow the result set, never widen it. It cannot rescue an empty list on its own.

Readiness checklist

InteractiveStrict Permissions Readiness Checklistlink

Eight items to settle before enabling Apply Strict User Permissions on a multi-company instance — version, doctype audit, the Asset and Shipment decisions, and testing as a genuinely restricted user.

Eight things to settle before you tick Apply Strict User Permissions.
0 / 8

Nothing here is saved — this is a checklist for your own rollout, not a scan of your ERPNext instance.

+Why can't my user see any Purchase Invoices in ERPNext even though they have read permission?

Almost certainly Apply Strict User Permissions combined with a Company User Permission left on Apply to All Document Types. The permission engine ANDs a condition on represents_company, which is NULL on every invoice from an ordinary external supplier, so the list resolves to zero rows with no error. Test it in ten seconds: untick Apply Strict User Permissions in System Settings. If the list fills up, that is your cause.

+What does Apply Strict User Permissions actually do in ERPNext System Settings?

It removes the "blank counts as allowed" escape. Normally each restricted link field is checked as ifnull(field,'')='' OR field IN (allowed values). With strict mode on, the ifnull half is not emitted, so a document with a blank restricted link field is hidden rather than shown. That is the documented behaviour — the bug was never that strict mode misbehaved, it was that ERPNext shipped a doctype with a Link-to-Company field blank on nearly every real record.

+Which ERPNext version fixes the Purchase Invoice not visible with Company user permission bug?

ERPNext v15.116.0 on the version-15 line and v16.27.0 on the version-16 line, both released on 2026-07-13. The fix is issue #57072, PR #57073, with backports #57074 and #57075. If you are on v15.115.0 or earlier, or v16.26.x or earlier, you still have it. Note that these releases fix Purchase Invoice only — Asset and Shipment carry the same pattern and are not covered.

+Hey Google, why are my ERPNext purchase invoices not showing up for a user?

Because a Company user permission set to apply to all document types is also being matched against the Represents Company field, which is empty on ordinary invoices, and strict user permissions removes the rule that lets empty fields pass. Fix it by ticking Ignore User Permissions on the Represents Company field in Customize Form, or by upgrading to ERPNext 15.116 or 16.27.

+How do I set ignore user permissions on a field in ERPNext without touching code?

Open Customize Form, set Enter Form Type to the doctype, expand the field's row in the Fields table, tick Ignore User Permissions, and click Update. It writes a Property Setter, so it survives bench migrate. Apply it to identity fields like represents_company or asset_owner_company — never to the company field itself, which is the field actually enforcing your isolation.

+What is Represents Company on a Purchase Invoice in ERPNext and why is it blank?

It identifies which sister company a counterparty represents in inter-company transactions, so ERPNext can pair a Purchase Invoice in one company with the corresponding Sales Invoice in another. It fetches from supplier.represents_company, which only exists when the Supplier is flagged Is Internal Supplier. For every ordinary external supplier it is legitimately NULL — and since the fix it is read-only as well.

+What happens if we don't fix this and just leave strict user permissions on?

Restricted users keep seeing an empty Purchase Invoice list with no error, and on any version they will keep seeing empty Asset and Shipment lists too. Nothing logs it. People work around it by escalating roles or by asking someone with System Manager to pull the data, which erodes exactly the isolation you enabled strict mode to enforce. The failure is silent, so the cost shows up as workarounds rather than as a ticket.

+Alexa, is it safe to turn on strict user permissions in ERPNext?

Only after you check your doctypes. Strict mode requires every link field pointing at the restricted record to be filled in. Assets with a blank asset owner company, and shipments with a blank pickup or delivery company, will disappear from list views the same way purchase invoices did — and unlike the purchase invoice bug, those two are still unfixed as of August 2026.

One last thing worth saying plainly: an empty list is not always a permission problem. If your accounts team is staring at a Purchase Invoice list that looks wrong rather than empty, the cause is more often reconciliation state than row-level security — see the payment reconciliation automation guide for that side of it. Rule out strict mode first, because it is the one cause that produces zero rows and zero explanation.

The Purchase Invoice half of this is closed, and if you are on a current release you already have it. The part that is not closed is the assumption underneath it: that a permission model enforced by per-field flags stays correct as doctypes and custom fields accumulate. Asset and Shipment are the two live counter-examples in the standard app today, and the next one will be a Link-to-Company custom field somebody adds to a transactional doctype on your own instance. Audit before you tick the box, not after someone reports an empty list.

Multi-company permissions behaving strangely?

We design and audit ERPNext permission models for multi-company groups — user permissions, role scoping, and the field-level flags that decide whether a list view returns anything at all. If your users are seeing empty lists nobody can explain, send us the setup.

Free · By email

Get practical ERPNext & automation guides

New implementation guides, cost breakdowns and open-source tips for Indian businesses — occasionally, straight to your inbox. No spam.

M

Written by

Manoj

Founder of Mith Tech, an open-source ERP & automation studio. Hands-on ERPNext/Frappe implementation across multi-branch, multi-warehouse Indian operations — GST/TDS/PT compliance, branch-level permissions, and custom Frappe apps that give management real-time visibility.

Keep reading

See what this looks like for your business

Book a free 30-minute audit. We'll map your workflows, find where time and money leak, and design an open-source stack you actually own — no per-user licence fees.

Book a consultation
0
Published on 5 August 2026

Manoj

Comments & ratings

No comments yet. Start a new discussion.