From 22377fa86d370ab4182e6a62babc025d541f0f2b Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Tue, 25 Aug 2026 17:04:52 +0530 Subject: [PATCH 1/8] fix(stock): keep pick list links when refetching stock entry items (cherry picked from commit 01e0844d401ad2d3fa1468ce6f3455f0ca5ef05d) # Conflicts: # erpnext/stock/doctype/stock_entry/stock_entry.js --- erpnext/stock/doctype/pick_list/pick_list.py | 1 + .../stock/doctype/pick_list/test_pick_list.py | 52 +++++++++++++++++++ .../stock/doctype/stock_entry/stock_entry.js | 36 +++++++++++++ .../stock/doctype/stock_entry/stock_entry.py | 3 ++ 4 files changed, 92 insertions(+) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 7a2dd32291a..144e486d94e 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -1508,6 +1508,7 @@ def add_product_bundles_to_delivery_note( @frappe.whitelist() def create_stock_entry(pick_list: str | dict): pick_list = frappe.get_doc(frappe.parse_json(pick_list)) + pick_list.check_permission("read") validate_item_locations(pick_list) stock_entry = frappe.new_doc("Stock Entry") diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 8ed07e61381..d777e0862e7 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -1078,6 +1078,58 @@ class TestPickList(FrappeTestCase): self.assertEqual(pick_list.locations[0].transferred_qty, 4) self.assertEqual(pick_list.status, "Partially Transferred") + def test_get_items_keeps_pick_list_rows_on_stock_entry(self): + """Entering fg_completed_qty on a Stock Entry mapped from a Pick List triggers get_items(); + it must not refetch from the BOM, or the pick_list_item links transferred_qty rides on are + lost and the Pick List stays Open with every row offered again.""" + from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom + from erpnext.manufacturing.doctype.work_order.work_order import ( + create_pick_list as pick_list_for_wo, + ) + from erpnext.manufacturing.doctype.work_order.work_order import make_work_order + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + source_warehouse = create_warehouse("_Test Partial Transfer Source") + wip_warehouse = create_warehouse("_Test Partial Transfer WIP", company="_Test Company") + fg_warehouse = create_warehouse("_Test Partial Transfer FG", company="_Test Company") + fg_item = make_item(properties={"is_stock_item": 1}).name + rm_item = make_item(properties={"is_stock_item": 1}).name + bom = make_bom(item=fg_item, rate=100, raw_materials=[rm_item]) + make_stock_entry(item=rm_item, to_warehouse=source_warehouse, qty=100) + + wo = make_work_order(item=fg_item, qty=10, bom_no=bom.name, company="_Test Company") + wo.required_items[0].source_warehouse = source_warehouse + wo.wip_warehouse = wip_warehouse + wo.fg_warehouse = fg_warehouse + wo.submit() + + pick_list = pick_list_for_wo(wo.name, for_qty=wo.qty) + pick_list.save().submit() + self.assertEqual(pick_list.status, "Open") + + se = frappe.get_doc(create_stock_entry(pick_list.as_dict())) + self.assertTrue(all(row.pick_list_item for row in se.items)) + self.assertEqual(se.fg_completed_qty, 0) + + se.fg_completed_qty = 4 + se.get_items() + self.assertEqual(len(se.items), len(pick_list.locations)) + self.assertTrue(all(row.pick_list_item for row in se.items)) + se.fg_completed_qty = 0 + + for row in se.items: + row.qty = 4 + se.save().submit() + self.assertEqual(se.fg_completed_qty, 0) + + pick_list.reload() + self.assertEqual(pick_list.locations[0].transferred_qty, 4) + self.assertEqual(pick_list.status, "Partially Transferred") + + next_se = frappe.get_doc(create_stock_entry(pick_list.as_dict())) + self.assertEqual(len(next_se.items), 1) + self.assertEqual(next_se.items[0].qty, 6) + def test_create_second_delivery_note_with_fully_delivered_location(self): # When one pick list item is fully delivered by the first Delivery Note # and another item is still pending, creating a second Delivery Note from diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index e2db1c7c98a..413c25987f4 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -181,6 +181,32 @@ frappe.ui.form.on("Stock Entry", { if (!check_should_not_attach_bom_items(frm.doc.bom_no)) { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); } +<<<<<<< HEAD +======= + + if (frm.doc.purpose == "Receive from Customer") { + frm.set_query("against_fg", "items", function () { + return { + query: "erpnext.controllers.subcontracting_inward_controller.get_fg_reference_names", + filters: { + parent: frm.doc.subcontracting_inward_order, + }, + }; + }); + } + + if (frm.doc.job_card && frm.doc.purpose === "Manufacture") { + frm.set_df_property("fg_completed_qty", "read_only", 1); + frm.set_df_property("get_items", "hidden", 1); + } + + if (frm.doc.pick_list) { + frm.set_df_property("get_items", "hidden", 1); + if (!frm.doc.job_card) { + frm.set_df_property("fg_completed_qty", "read_only", 1); + } + } +>>>>>>> 01e0844 (fix(stock): keep pick list links when refetching stock entry items) }, setup_quality_inspection: function (frm) { @@ -1356,10 +1382,20 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle ) { frappe.model.remove_from_locals("Work Order", this.frm.doc.work_order); } + + if (this.frm.doc.pick_list) { + frappe.model.remove_from_locals("Pick List", this.frm.doc.pick_list); + } } fg_completed_qty() { +<<<<<<< HEAD this.get_items(); +======= + if (!this.frm.doc.job_card && !this.frm.doc.pick_list) { + this.get_items(); + } +>>>>>>> 01e0844 (fix(stock): keep pick list links when refetching stock entry items) } get_items() { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 0541193184c..62b27bf1059 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -2576,6 +2576,9 @@ class StockEntry(StockController): @frappe.whitelist() def get_items(self): + if self.pick_list: + return + self.set("items", []) self.validate_work_order() From e126c4597b9b2a0760f704e91441ec33c71312a6 Mon Sep 17 00:00:00 2001 From: Sudharsanan11 Date: Wed, 26 Aug 2026 13:08:35 +0530 Subject: [PATCH 2/8] fix: resolve stock entry backport conflicts --- .../stock/doctype/stock_entry/stock_entry.js | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 413c25987f4..dcbcd292647 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -181,24 +181,6 @@ frappe.ui.form.on("Stock Entry", { if (!check_should_not_attach_bom_items(frm.doc.bom_no)) { erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); } -<<<<<<< HEAD -======= - - if (frm.doc.purpose == "Receive from Customer") { - frm.set_query("against_fg", "items", function () { - return { - query: "erpnext.controllers.subcontracting_inward_controller.get_fg_reference_names", - filters: { - parent: frm.doc.subcontracting_inward_order, - }, - }; - }); - } - - if (frm.doc.job_card && frm.doc.purpose === "Manufacture") { - frm.set_df_property("fg_completed_qty", "read_only", 1); - frm.set_df_property("get_items", "hidden", 1); - } if (frm.doc.pick_list) { frm.set_df_property("get_items", "hidden", 1); @@ -206,7 +188,6 @@ frappe.ui.form.on("Stock Entry", { frm.set_df_property("fg_completed_qty", "read_only", 1); } } ->>>>>>> 01e0844 (fix(stock): keep pick list links when refetching stock entry items) }, setup_quality_inspection: function (frm) { @@ -1389,13 +1370,9 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle } fg_completed_qty() { -<<<<<<< HEAD - this.get_items(); -======= - if (!this.frm.doc.job_card && !this.frm.doc.pick_list) { + if (!this.frm.doc.pick_list) { this.get_items(); } ->>>>>>> 01e0844 (fix(stock): keep pick list links when refetching stock entry items) } get_items() { From 228ab2d97e1e63a5da77da9d2bf92f631b7cb36c Mon Sep 17 00:00:00 2001 From: Jatin3128 <140256508+Jatin3128@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:14:25 +0530 Subject: [PATCH 3/8] fix: keep source rate on re-fetch when maintain same rate is enabled (backport #57479) (#58332) * fix: keep source rate on re-fetch when maintain same rate is enabled (backport #57479) With "maintain same rate" on, re-fetching item details on a row mapped from a source document (e.g. a Purchase Order) pulled the latest Item Price, giving a rate the document can never be saved with. Skip the price list fetch for such rows and keep the source rate, both for a single-row re-fetch and the bulk apply_price_list path (price list / party / conversion rate change). The rate is read from the linked source row in the database (not the mutable target row) and permission-checked against the source document, so an unsaved edit can't lock in a different rate and a crafted request can't disclose another document's pricing. Fixes frappe/erpnext#57436 * fix: resolve linter findings in get_item_details Add missing type hints on the whitelisted get_item_details signature and rename maintain_same_rate_enabled's sole "args" parameter, both flagged by the semgrep security/code-quality rules. Also drops an extra blank line that ruff-format rejected. * fix: widen get_item_details doc type hint to include Document accounts_controller.py calls get_item_details(args, self, ...) during validate, passing the transaction Document itself, not a dict/JSON string. The narrower hint tripped Frappe's runtime argument type validation on every whitelisted call with a live Document, failing test-record creation across the suite. --- erpnext/stock/get_item_details.py | 111 +++++++-- erpnext/stock/tests/test_get_item_details.py | 245 +++++++++++++++++++ 2 files changed, 341 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index c9fa5fe6e16..6c1d658481a 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -7,6 +7,7 @@ import json import frappe from frappe import _, throw from frappe.model import child_table_fields, default_fields +from frappe.model.document import Document from frappe.model.meta import get_field_precision from frappe.model.utils import get_fetch_values from frappe.query_builder.functions import IfNull, Sum @@ -34,11 +35,34 @@ purchase_doctypes = [ "Purchase Invoice", ] +maintain_same_rate_source_fields = { + "Purchase Order": {"supplier_quotation_item": "Supplier Quotation Item"}, + "Purchase Receipt": {"purchase_order_item": "Purchase Order Item"}, + "Purchase Invoice": {"po_detail": "Purchase Order Item", "pr_detail": "Purchase Receipt Item"}, + "Sales Order": {"quotation_item": "Quotation Item"}, + "Delivery Note": {"so_detail": "Sales Order Item", "si_detail": "Sales Invoice Item"}, + "Sales Invoice": {"so_detail": "Sales Order Item", "dn_detail": "Delivery Note Item"}, +} + +LOCKED_RATE_FIELDS = [ + "price_list_rate", + "rate", + "discount_percentage", + "discount_amount", + "margin_type", + "margin_rate_or_amount", +] + NOT_APPLICABLE_TAX = "N/A" @frappe.whitelist() -def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True): +def get_item_details( + args: dict | str, + doc: Document | dict | str | None = None, + for_validate: bool | str = False, + overwrite_warehouse: bool | str = True, +): """ args = { "item_code": "", @@ -100,16 +124,20 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if args.get("doctype") in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]: args.customer = None - out.update(get_price_list_rate(args, item)) + source_row = get_rate_locked_source_row(args, doc) + if source_row: + lock_source_rate(out, source_row) + else: + out.update(get_price_list_rate(args, item)) - if ( - not out.price_list_rate - and args.transaction_type == "selling" - and frappe.get_single_value("Selling Settings", "fallback_to_default_price_list") - ): - fallback_args = args.copy() - fallback_args.price_list = frappe.get_single_value("Selling Settings", "selling_price_list") - out.update(get_price_list_rate(fallback_args, item)) + if ( + not out.price_list_rate + and args.transaction_type == "selling" + and frappe.get_single_value("Selling Settings", "fallback_to_default_price_list") + ): + fallback_args = args.copy() + fallback_args.price_list = frappe.get_single_value("Selling Settings", "selling_price_list") + out.update(get_price_list_rate(fallback_args, item)) args.customer = current_customer @@ -124,9 +152,8 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru if args.get(key) is None: args[key] = value - data = get_pricing_rule_for_item(args, doc=doc, for_validate=for_validate) - - out.update(data) + if not source_row: + out.update(get_pricing_rule_for_item(args, doc=doc, for_validate=for_validate)) if ( frappe.db.get_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward") @@ -156,6 +183,52 @@ def remove_standard_fields(details): return details +def get_rate_locked_source_row(args, doc): + """Reads the source row from the DB, not the mutable target row, so an unsaved edit can't override the locked rate.""" + if isinstance(doc, str): + doc = json.loads(doc) + + source_fields = maintain_same_rate_source_fields.get(args.parenttype or args.doctype) + if not source_fields or not doc or args.get("is_return") or not maintain_same_rate_enabled(args): + return None + + row = next((d for d in doc.get("items") or [] if d.get("name") == args.child_docname), None) + if not row: + return None + + for link_field, source_doctype in source_fields.items(): + if source_name := row.get(link_field): + # don't leak another document's pricing to a caller without read access + source = frappe.db.get_value( + source_doctype, source_name, [*LOCKED_RATE_FIELDS, "parent", "parenttype"], as_dict=True + ) + if source and frappe.has_permission(source.parenttype, doc=source.parent): + return source + return None + return None + + +def maintain_same_rate_enabled(transaction_args): + if (transaction_args.parenttype or transaction_args.doctype) in purchase_doctypes: + if transaction_args.get("is_internal_supplier"): + return False + return bool(cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))) + + if transaction_args.get("is_internal_customer"): + return False + return bool(cint(frappe.get_cached_value("Selling Settings", "None", "maintain_same_sales_rate"))) + + +def lock_source_rate(out, source_row): + """Copies the full pricing block so a manual discount or margin on the source row survives.""" + out.price_list_rate = flt(source_row.get("price_list_rate")) or flt(source_row.get("rate")) + out.rate = flt(source_row.get("rate")) + out.discount_percentage = flt(source_row.get("discount_percentage")) + out.discount_amount = flt(source_row.get("discount_amount")) + out.margin_type = source_row.get("margin_type") + out.margin_rate_or_amount = flt(source_row.get("margin_rate_or_amount")) + + def set_valuation_rate(out, args): if frappe.db.exists("Product Bundle", {"name": args.item_code, "disabled": 0}, cache=True): valuation_rate = 0.0 @@ -1522,13 +1595,21 @@ def apply_price_list(args, as_doc=False, doc=None): def apply_price_list_on_item(args, doc=None): item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1) - item_details = get_price_list_rate(args, item_doc) + + source_row = get_rate_locked_source_row(args, doc) + if source_row: + item_details = frappe._dict() + lock_source_rate(item_details, source_row) + else: + item_details = get_price_list_rate(args, item_doc) args.conversion_factor = flt(args.conversion_factor) or get_conversion_factor( args.item_code, args.uom ).get("conversion_factor", 1) args.stock_qty = flt(args.qty) * flt(args.conversion_factor) - item_details.update(get_pricing_rule_for_item(args, doc=doc)) + + if not source_row: + item_details.update(get_pricing_rule_for_item(args, doc=doc)) return item_details diff --git a/erpnext/stock/tests/test_get_item_details.py b/erpnext/stock/tests/test_get_item_details.py index d0b725b9179..398d7e603b5 100644 --- a/erpnext/stock/tests/test_get_item_details.py +++ b/erpnext/stock/tests/test_get_item_details.py @@ -177,3 +177,248 @@ class TestGetItemDetail(FrappeTestCase): dn.save() self.assertEqual(dn.items[0].batch_no, "BATCH01") self.assertEqual(dn.items[0].rate, 50) + + def test_maintain_same_rate_keeps_source_rate_on_refetch(self): + """#57436: with "maintain same rate" on, re-fetching a PR row mapped from a + PO must keep the PO rate instead of pulling a newer, higher Item Price. + + The rate is validated on save, so it can never persist changed; assert the + fetched rate directly to prove the newer Item Price is never picked up. + """ + from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + from erpnext.stock.doctype.item.test_item import make_item + + def set_maintain_same_rate(value): + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", value) + frappe.clear_cache(doctype="Buying Settings") + + set_maintain_same_rate(1) + + item_code = make_item(properties={"is_stock_item": 1}).name + po = create_purchase_order(item_code=item_code, qty=1, rate=100) + + # The PO may auto-insert an Item Price at 100; bump it to the newer, higher rate. + item_price = frappe.db.get_value( + "Item Price", {"item_code": item_code, "price_list": "Standard Buying"} + ) + if item_price: + frappe.db.set_value("Item Price", item_price, "price_list_rate", 120) + else: + frappe.get_doc( + { + "doctype": "Item Price", + "price_list": "Standard Buying", + "item_code": item_code, + "price_list_rate": 120, + } + ).insert() + + pr = make_purchase_receipt(po.name) + pr.insert() + + def fetch_price_list_rate(): + args = frappe._dict( + { + "item_code": item_code, + "doctype": "Purchase Receipt", + "name": pr.name, + "company": pr.company, + "supplier": pr.supplier, + "currency": pr.currency, + "conversion_rate": 1.0, + "price_list": "Standard Buying", + "price_list_currency": pr.currency, + "plc_conversion_rate": 1.0, + "warehouse": pr.items[0].warehouse, + "uom": pr.items[0].uom, + "stock_uom": pr.items[0].stock_uom, + "qty": pr.items[0].qty, + "child_doctype": pr.items[0].doctype, + "child_docname": pr.items[0].name, + "is_return": 0, + "is_internal_supplier": 0, + "ignore_pricing_rule": 1, + } + ) + return get_item_details(args, pr).get("price_list_rate") + + # Rate stays at the PO rate; the newer Item Price (120) is not fetched. + self.assertEqual(fetch_price_list_rate(), 100) + + # Control: without the setting the newer Item Price would be fetched. + set_maintain_same_rate(0) + self.assertEqual(fetch_price_list_rate(), 120) + + def test_apply_price_list_keeps_source_rate_when_maintain_same_rate(self): + """#57436: the bulk apply_price_list path (price list / party / conversion rate + change) must also keep the source rate on mapped rows, not just re-fetch of a + single row. Here a PR row carries its PO rate (175) while the current price list + rate is 100; the bulk apply must keep 175. + """ + from frappe.utils import flt, nowdate + + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + from erpnext.stock.get_item_details import apply_price_list + + item_code = "_Test Item" + price_list = "_Test Buying Price List" + + original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate") + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + frappe.clear_cache(doctype="Buying Settings") + + try: + po = create_purchase_order(item_code=item_code, rate=175, qty=1) + + row_name = "pr-row-1" + pr_doc = { + "doctype": "Purchase Receipt", + "items": [ + { + "name": row_name, + "item_code": item_code, + "purchase_order_item": po.items[0].name, + "price_list_rate": 175, + "rate": 175, + } + ], + } + args = frappe._dict( + doctype="Purchase Receipt", + supplier=po.supplier, + company=po.company, + currency=po.currency, + conversion_rate=1.0, + price_list=price_list, + plc_conversion_rate=1.0, + transaction_date=nowdate(), + items=[ + frappe._dict( + doctype="Purchase Receipt Item", + parenttype="Purchase Receipt", + item_code=item_code, + child_docname=row_name, + qty=1, + uom=po.items[0].uom, + stock_uom=po.items[0].stock_uom, + conversion_factor=1.0, + ) + ], + ) + + result = apply_price_list(args, doc=pr_doc) + self.assertEqual(flt(result["children"][0].get("price_list_rate")), 175) + finally: + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original) + frappe.clear_cache(doctype="Buying Settings") + + def test_maintain_same_rate_keeps_source_discount_on_refetch(self): + """A mapped source row with a discount has rate != price_list_rate. Re-fetch must + return the source's rate and discount, not just the pre-discount price, or the + recomputed rate diverges from the reference and fails maintain-same-rate on save. + """ + from frappe.utils import flt + + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + + item_code = "_Test Item" + price_list = "_Test Buying Price List" + + original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate") + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + frappe.clear_cache(doctype="Buying Settings") + + try: + # source PO carries the discount: list rate 100, 10% off, effective rate 90 + frappe.flags.dont_fetch_price_list_rate = True + po = create_purchase_order(item_code=item_code, qty=1, do_not_save=True) + po.buying_price_list = price_list + po.items[0].price_list_rate = 100 + po.items[0].discount_percentage = 10 + po.items[0].rate = 90 + po.insert() + po.submit() + frappe.flags.dont_fetch_price_list_rate = False + + row_name = "pr-row-1" + pr_doc = { + "doctype": "Purchase Receipt", + "items": [ + {"name": row_name, "item_code": item_code, "purchase_order_item": po.items[0].name} + ], + } + args = frappe._dict( + item_code=item_code, + doctype="Purchase Receipt", + company=po.company, + supplier=po.supplier, + currency=po.currency, + conversion_rate=1.0, + price_list=price_list, + price_list_currency=po.currency, + plc_conversion_rate=1.0, + warehouse="_Test Warehouse - _TC", + uom=po.items[0].uom, + stock_uom=po.items[0].stock_uom, + qty=1, + child_docname=row_name, + is_return=0, + is_internal_supplier=0, + ignore_pricing_rule=1, + ) + + out = get_item_details(args, pr_doc) + self.assertEqual(flt(out.get("price_list_rate")), 100) + self.assertEqual(flt(out.get("rate")), 90) + self.assertEqual(flt(out.get("discount_percentage")), 10) + finally: + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original) + frappe.clear_cache(doctype="Buying Settings") + frappe.flags.dont_fetch_price_list_rate = False + + def test_rate_lock_source_lookup_checks_permission(self): + """The lock reads source pricing via a direct DB read, so it must not disclose a + source document's pricing to a caller who cannot read that document. + """ + from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order + from erpnext.stock.get_item_details import get_rate_locked_source_row + + original = frappe.db.get_single_value("Buying Settings", "maintain_same_rate") + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1) + frappe.clear_cache(doctype="Buying Settings") + + role, email = "_Test Role Without PO Access", "_test_rate_lock_probe@example.com" + try: + po = create_purchase_order(item_code="_Test Item", qty=1, rate=90) + pr_doc = { + "doctype": "Purchase Receipt", + "items": [{"name": "r1", "item_code": "_Test Item", "purchase_order_item": po.items[0].name}], + } + args = frappe._dict(doctype="Purchase Receipt", child_docname="r1") + + # an authorized caller receives the source row + self.assertIsNotNone(get_rate_locked_source_row(args.copy(), dict(pr_doc))) + + if not frappe.db.exists("Role", role): + frappe.get_doc({"doctype": "Role", "role_name": role, "desk_access": 1}).insert( + ignore_permissions=True + ) + if not frappe.db.exists("User", email): + frappe.get_doc( + { + "doctype": "User", + "email": email, + "first_name": "Probe", + "send_welcome_email": 0, + "roles": [{"role": role}], + } + ).insert(ignore_permissions=True) + + frappe.set_user(email) + # a caller who cannot read the Purchase Order gets nothing + self.assertIsNone(get_rate_locked_source_row(args.copy(), dict(pr_doc))) + finally: + frappe.set_user("Administrator") + frappe.db.set_single_value("Buying Settings", "maintain_same_rate", original) + frappe.clear_cache(doctype="Buying Settings") From ccdaa92de8f717e9fb473321abed8a75206e9c45 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Wed, 26 Aug 2026 14:50:32 +0530 Subject: [PATCH 4/8] fix: reset hardcoded letter head on Incorrect Serial and Batch Bundle report --- .../incorrect_serial_and_batch_bundle.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.json b/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.json index 11e6e4ea3d4..be5c06d8b59 100644 --- a/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.json +++ b/erpnext/stock/report/incorrect_serial_and_batch_bundle/incorrect_serial_and_batch_bundle.json @@ -9,9 +9,9 @@ "idx": 0, "is_standard": "Yes", "json": "{}", - "letter_head": "Test", + "letter_head": null, "letterhead": null, - "modified": "2025-02-03 15:39:47.613040", + "modified": "2026-08-26 14:39:19.102191", "modified_by": "Administrator", "module": "Stock", "name": "Incorrect Serial and Batch Bundle", From 32d44c6e91b56111db427091ea3b53cc3350e521 Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Thu, 27 Aug 2026 12:48:45 +0530 Subject: [PATCH 5/8] fix: validate items against source Sales Order in Material Request (cherry picked from commit 65466464df0fdc6de32be5bc7ad25db326081c69) --- .../material_request/material_request.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index eaf28dd4b96..e4738473c87 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -94,6 +94,22 @@ class MaterialRequest(BuyingController): def check_if_already_pulled(self): pass + def validate_with_previous_doc(self): + super().validate_with_previous_doc( + { + "Sales Order": { + "ref_dn_field": "sales_order", + "compare_fields": [["company", "="]], + }, + "Sales Order Item": { + "ref_dn_field": "sales_order_item", + "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + } + ) + def validate_qty_against_so(self): so_items = {} # Format --> {'SO/00001': {'Item/001': 120, 'Item/002': 24}} for d in self.get("items"): @@ -136,6 +152,7 @@ class MaterialRequest(BuyingController): self.validate_schedule_date() self.check_for_on_hold_or_closed_status("Sales Order", "sales_order") + self.validate_with_previous_doc() self.validate_uom_is_integer("uom", "qty") self.validate_material_request_type() From 59a9819dae736947b3ba748ef411fde3d557702a Mon Sep 17 00:00:00 2001 From: Shllokkk Date: Thu, 27 Aug 2026 12:48:45 +0530 Subject: [PATCH 6/8] test: block item change on Sales Order-sourced Material Request row (cherry picked from commit 07f8250e407080e43fefa9408e2e3e058978bbb4) # Conflicts: # erpnext/stock/doctype/material_request/test_material_request.py --- .../material_request/test_material_request.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py index 6ae289625bc..4c753c867a2 100644 --- a/erpnext/stock/doctype/material_request/test_material_request.py +++ b/erpnext/stock/doctype/material_request/test_material_request.py @@ -919,6 +919,18 @@ class TestMaterialRequest(FrappeTestCase): self.assertRaises(OverAllowanceError, mr.submit) + def test_item_change_on_sales_order_row_is_blocked(self): + from erpnext.selling.doctype.sales_order.sales_order import make_material_request + from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order + + other_item = create_item("_Test MR Item Swap").name + so = make_sales_order() + mr = make_material_request(so.name) + mr.material_request_type = "Purchase" + # swapping the fetched item would leave a stale link to the SO row + mr.items[0].item_code = other_item + self.assertRaises(frappe.ValidationError, mr.insert) + def test_pending_qty_in_pick_list(self): """Test for pick list mapped doc qty from partially received Material Request Transfer""" import json From 045d7086d10034ef3bad83acf301a6abec39cff9 Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Thu, 27 Aug 2026 14:16:01 +0530 Subject: [PATCH 7/8] fix(accounts): validate frozen accounts in period closing voucher (#58477) --- .../doctype/period_closing_voucher/period_closing_voucher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 8671213c3cc..89dd73af457 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -54,7 +54,7 @@ class PeriodClosingVoucher(AccountsController): if for_cancellation and is_immutable_ledger_enabled(): posting_date = getdate() - check_freezing_date(posting_date, self.company) + check_freezing_date(posting_date) def validate_start_and_end_date(self): self.fy_start_date, self.fy_end_date = frappe.db.get_value( From d62f6e715f2fa3fd30dac1fc52199bc8698f7cf8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 09:46:30 +0000 Subject: [PATCH 8/8] fix: mark item tax templates as not applicable (backport #54673) (backport #54677) (#58462) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- .../setup_wizard/data/country_wise_tax.json | 102 +++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/setup_wizard/data/country_wise_tax.json b/erpnext/setup/setup_wizard/data/country_wise_tax.json index 7ee3a79b4a0..000c115d6bc 100644 --- a/erpnext/setup/setup_wizard/data/country_wise_tax.json +++ b/erpnext/setup/setup_wizard/data/country_wise_tax.json @@ -1395,6 +1395,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1413,6 +1414,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1431,6 +1433,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1449,6 +1452,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1467,6 +1471,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1485,6 +1490,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1507,6 +1513,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1525,6 +1532,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1543,6 +1551,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1561,6 +1570,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1579,6 +1589,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1597,6 +1608,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1628,6 +1640,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1637,6 +1650,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1646,6 +1660,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1655,6 +1670,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1664,6 +1680,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1673,6 +1690,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1682,6 +1700,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1691,6 +1710,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1700,6 +1720,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1709,6 +1730,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1718,6 +1740,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1727,6 +1750,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -1735,6 +1759,7 @@ "account_number": "1433", "root_type": "Asset" }, + "not_applicable": 1, "tax_rate": 0.00 } ] @@ -2158,6 +2183,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2176,6 +2202,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2194,6 +2221,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2212,6 +2240,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2230,6 +2259,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2248,6 +2278,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2270,6 +2301,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2288,6 +2320,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2306,6 +2339,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2324,6 +2358,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2342,6 +2377,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2360,6 +2396,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2391,6 +2428,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2400,6 +2438,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2409,6 +2448,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2418,6 +2458,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2427,6 +2468,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2436,6 +2478,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2445,6 +2488,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2454,6 +2498,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2463,6 +2508,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2472,6 +2518,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2481,6 +2528,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2490,6 +2538,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2498,6 +2547,7 @@ "account_number": "1588", "root_type": "Asset" }, + "not_applicable": 1, "tax_rate": 0.00 } ] @@ -2921,6 +2971,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2939,6 +2990,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2957,6 +3009,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2975,6 +3028,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -2993,6 +3047,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3011,6 +3066,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3033,6 +3089,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3051,6 +3108,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3069,7 +3127,8 @@ "root_type": "Liability", "tax_rate": 19.00 }, - "tax_rate": 19.00 + "not_applicable": 1, + "tax_rate": 0.00 }, { "tax_type": { @@ -3087,6 +3146,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3105,6 +3165,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3123,6 +3184,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3154,6 +3216,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3163,6 +3226,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3172,6 +3236,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3181,6 +3246,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3190,6 +3256,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3199,6 +3266,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3208,6 +3276,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3217,6 +3286,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3226,6 +3296,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3235,6 +3306,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3244,6 +3316,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3253,6 +3326,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3261,6 +3335,7 @@ "account_number": "1550", "root_type": "Asset" }, + "not_applicable": 1, "tax_rate": 0.00 } ] @@ -3653,6 +3728,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3669,6 +3745,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3685,6 +3762,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3701,6 +3779,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3717,6 +3796,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3733,6 +3813,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3753,6 +3834,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3769,6 +3851,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3785,6 +3868,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3801,6 +3885,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3817,6 +3902,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3833,6 +3919,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3861,6 +3948,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3869,6 +3957,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3877,6 +3966,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3885,6 +3975,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3893,6 +3984,7 @@ "root_type": "Liability", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3901,6 +3993,7 @@ "root_type": "Liability", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3909,6 +4002,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3917,6 +4011,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3925,6 +4020,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3933,6 +4029,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3941,6 +4038,7 @@ "root_type": "Asset", "tax_rate": 19.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3949,6 +4047,7 @@ "root_type": "Asset", "tax_rate": 7.00 }, + "not_applicable": 1, "tax_rate": 0.00 }, { @@ -3956,6 +4055,7 @@ "account_name": "Entstandene Einfuhrumsatzsteuer", "root_type": "Asset" }, + "not_applicable": 1, "tax_rate": 0.00 } ]