From a5c23a3d16dedcb05bf05e924b8630e1dec44e41 Mon Sep 17 00:00:00 2001 From: Pandiyan P Date: Tue, 9 Jun 2026 13:13:23 +0530 Subject: [PATCH] fix: restrict already invoiced qty in intercompany purchase invoice (#55754) --- .../doctype/sales_invoice/sales_invoice.js | 21 ++++++- .../doctype/sales_invoice/sales_invoice.py | 28 +++++---- .../sales_invoice/test_sales_invoice.py | 61 +++++++++++++++++++ 3 files changed, 98 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 61b9cf1fc06..259cd970941 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -180,12 +180,31 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends ( : "Inter Company Purchase Invoice"; me.frm.add_custom_button( - button_label, + __(button_label), function () { me.make_inter_company_invoice(); }, __("Create") ); + + frappe.call({ + method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.get_received_items", + args: { + reference_name: me.frm.doc.name, + doctype: "Purchase Invoice", + reference_fieldname: "sales_invoice_item", + }, + callback: function (r) { + if (r.exc) return; + const received_items = r.message || {}; + const has_pending_qty = me.frm.doc.items.some( + (item) => flt(item.qty) - flt(received_items[item.name] || 0) > 0 + ); + if (!has_pending_qty) { + me.frm.remove_custom_button(__(button_label), __("Create")); + } + }, + }); } } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3e16c503e69..df76ef6831e 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2526,7 +2526,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): "rate": "rate", }, "postprocess": update_item, - "condition": lambda doc: doc.qty > 0, + "condition": lambda doc: doc.qty - received_items.get(doc.name, 0.0) > 0, } if doctype in ["Sales Invoice", "Sales Order"]: @@ -2566,11 +2566,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target_doc, set_missing_values, ) - + if not doclist.get("items"): + frappe.throw( + _( + "Cannot create Intercompany {0}. All items in the source {1} have already been fully invoiced. " + "Please check the existing linked {2}s." + ).format(target_doctype, doctype, target_doctype) + ) return doclist -def get_received_items(reference_name, doctype, reference_fieldname): +@frappe.whitelist() +def get_received_items(reference_name: str, doctype: str, reference_fieldname: str): reference_field = "inter_company_invoice_reference" if doctype == "Purchase Order": reference_field = "inter_company_order_reference" @@ -2583,20 +2590,19 @@ def get_received_items(reference_name, doctype, reference_fieldname): target_doctypes = frappe.get_all( doctype, filters=filters, - as_list=True, + pluck="name", ) - + received_items_map = {} if target_doctypes: - target_doctypes = list(target_doctypes[0]) - - received_items_map = frappe._dict( - frappe.get_all( + received_items_data = frappe.get_all( doctype + " Item", filters={"parent": ("in", target_doctypes)}, fields=[reference_fieldname, "qty"], - as_list=1, ) - ) + for item in received_items_data: + key = item.get(reference_fieldname) + if key: + received_items_map[key] = received_items_map.get(key, 0.0) + flt(item.qty) return received_items_map diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 147bb76064d..ef7c25c86a9 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2690,6 +2690,67 @@ class TestSalesInvoice(FrappeTestCase): self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.supplier, "_Test Internal Supplier") + def test_restrict_inter_company_pi_when_sales_invoice_qty_fully_consumed(self): + item_code_1 = "_Test IC Item 1" + item_code_2 = "_Test IC Item 2" + + create_item(item_code_1, is_stock_item=1) + create_item(item_code_2, is_stock_item=1) + + si = create_sales_invoice( + company="Wind Power LLC", + customer="_Test Internal Customer", + item_code=item_code_1, + debit_to="Debtors - WP", + warehouse="Stores - WP", + income_account="Sales - WP", + expense_account="Cost of Goods Sold - WP", + cost_center="Main - WP", + currency="USD", + qty=3, + do_not_save=1, + ) + si.selling_price_list = "_Test Price List Rest of the World" + si.append( + "items", + { + "item_code": item_code_2, + "item_name": item_code_2, + "description": item_code_2, + "warehouse": "Stores - WP", + "qty": 2, + "uom": "Nos", + "stock_uom": "Nos", + "rate": 100, + "price_list_rate": 100, + "income_account": "Sales - WP", + "expense_account": "Cost of Goods Sold - WP", + "cost_center": "Main - WP", + "conversion_factor": 1, + }, + ) + + si.submit() + + target_doc = make_inter_company_transaction("Sales Invoice", si.name) + + for item in target_doc.items: + item.update( + { + "expense_account": "Cost of Goods Sold - _TC1", + "cost_center": "Main - _TC1", + } + ) + + target_doc.submit() + self.assertEqual(len(target_doc.items), 2) + self.assertEqual([item.qty for item in target_doc.items], [3, 2]) + with self.assertRaisesRegex( + frappe.ValidationError, + "already been fully invoiced", + ): + make_inter_company_transaction("Sales Invoice", si.name) + def test_inter_company_transaction_without_default_warehouse(self): "Check mapping (expense account) of inter company SI to PI in absence of default warehouse." # setup