diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 097d4f1ad03..7597eec0e57 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -179,12 +179,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 5408ebd283a..a7967de15a2 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -2793,7 +2793,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"]: @@ -2834,10 +2834,19 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): 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" @@ -2850,20 +2859,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 1be06706100..8ae7ff0c518 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3000,6 +3000,67 @@ class TestSalesInvoice(ERPNextTestSuite): frappe.local.enable_perpetual_inventory["_Test Company 1"] = old_perpetual_inventory frappe.db.set_single_value("Stock Settings", "allow_negative_stock", old_negative_stock) + 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_sle_for_target_warehouse(self): se = make_stock_entry( item_code="138-CMS Shoe",