diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json index 7aada160b806..d89d8d48bf21 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.json +++ b/erpnext/buying/doctype/buying_settings/buying_settings.json @@ -12,21 +12,25 @@ "column_break_4", "maintain_same_rate_action", "role_to_override_stop_action", - "transaction_settings_section", + "section_break_xmlt", "po_required", - "pr_required", "blanket_order_allowance", + "column_break_sbwq", + "pr_required", "project_update_frequency", + "transaction_settings_section", + "column_break_fcyl", + "set_landed_cost_based_on_purchase_invoice_rate", + "allow_zero_qty_in_supplier_quotation", + "use_transaction_date_exchange_rate", + "allow_zero_qty_in_request_for_quotation", "column_break_12", "maintain_same_rate", - "set_landed_cost_based_on_purchase_invoice_rate", "allow_multiple_items", "bill_for_rejected_quantity_in_purchase_invoice", + "set_valuation_rate_for_rejected_materials", "disable_last_purchase_rate", "show_pay_button", - "use_transaction_date_exchange_rate", - "allow_zero_qty_in_request_for_quotation", - "allow_zero_qty_in_supplier_quotation", "allow_zero_qty_in_purchase_order", "subcontract", "backflush_raw_materials_of_subcontract_based_on", @@ -231,6 +235,26 @@ "fieldname": "allow_zero_qty_in_supplier_quotation", "fieldtype": "Check", "label": "Allow Supplier Quotation with Zero Quantity" + }, + { + "fieldname": "section_break_xmlt", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_sbwq", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_fcyl", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "bill_for_rejected_quantity_in_purchase_invoice", + "description": "If enabled, the system will generate an accounting entry for materials rejected in the Purchase Receipt.", + "fieldname": "set_valuation_rate_for_rejected_materials", + "fieldtype": "Check", + "label": "Set Valuation Rate for Rejected Materials" } ], "grid_page_length": 50, @@ -239,7 +263,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-05-06 15:21:49.639642", + "modified": "2025-05-16 15:56:38.321369", "modified_by": "Administrator", "module": "Buying", "name": "Buying Settings", diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.py b/erpnext/buying/doctype/buying_settings/buying_settings.py index 4dde7c8dabf3..f3b3dbefff40 100644 --- a/erpnext/buying/doctype/buying_settings/buying_settings.py +++ b/erpnext/buying/doctype/buying_settings/buying_settings.py @@ -38,6 +38,7 @@ class BuyingSettings(Document): project_update_frequency: DF.Literal["Each Transaction", "Manual"] role_to_override_stop_action: DF.Link | None set_landed_cost_based_on_purchase_invoice_rate: DF.Check + set_valuation_rate_for_rejected_materials: DF.Check show_pay_button: DF.Check supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"] supplier_group: DF.Link | None @@ -57,6 +58,9 @@ def validate(self): hide_name_field=False, ) + if not self.bill_for_rejected_quantity_in_purchase_invoice: + self.set_valuation_rate_for_rejected_materials = 0 + def before_save(self): self.check_maintain_same_rate() diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 602d6c955b33..4915b710658c 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -681,6 +681,10 @@ def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_vouche sl_entries.append(from_warehouse_sle) if flt(d.rejected_qty) != 0: + valuation_rate_for_rejected_item = 0.0 + if frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials"): + valuation_rate_for_rejected_item = d.valuation_rate + sl_entries.append( self.get_sl_entries( d, @@ -689,7 +693,7 @@ def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_vouche "actual_qty": flt( flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty") ), - "incoming_rate": 0.0, + "incoming_rate": valuation_rate_for_rejected_item, "serial_and_batch_bundle": d.rejected_serial_and_batch_bundle, }, ) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index b6d87b7525c0..d495e7ab88fc 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -531,6 +531,14 @@ def make_stock_received_but_not_billed_entry(item): outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse)) credit_amount = outgoing_amount + if item.get("rejected_qty") and frappe.db.get_single_value( + "Buying Settings", "set_valuation_rate_for_rejected_materials" + ): + outgoing_amount += abs( + get_stock_value_difference(self.name, item.name, item.rejected_warehouse) + ) + credit_amount = outgoing_amount + if credit_amount: if not account: validate_account("Stock or Asset Received But Not Billed") @@ -664,6 +672,14 @@ def make_divisional_loss_gl_entry(item, outgoing_amount): valuation_amount_as_per_doc - flt(stock_value_diff), item.precision("base_net_amount") ) + if item.get("rejected_qty") and frappe.db.get_single_value( + "Buying Settings", "set_valuation_rate_for_rejected_materials" + ): + rejected_item_cost = abs( + get_stock_value_difference(self.name, item.name, item.rejected_warehouse) + ) + divisional_loss -= rejected_item_cost + if divisional_loss: loss_account = ( self.get_company_default("default_expense_account", ignore_validation=True) @@ -751,13 +767,23 @@ def make_divisional_loss_gl_entry(item, outgoing_amount): make_sub_contracting_gl_entries(d) make_divisional_loss_gl_entry(d, outgoing_amount) elif (d.warehouse and d.warehouse not in warehouse_with_no_account) or ( - d.rejected_warehouse and d.rejected_warehouse not in warehouse_with_no_account + not frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials") + and d.rejected_warehouse + and d.rejected_warehouse not in warehouse_with_no_account ): warehouse_with_no_account.append(d.warehouse or d.rejected_warehouse) if d.is_fixed_asset and d.landed_cost_voucher_amount: self.update_assets(d, d.valuation_rate) + if d.rejected_qty and frappe.db.get_single_value( + "Buying Settings", "set_valuation_rate_for_rejected_materials" + ): + stock_value_diff = get_stock_value_difference(self.name, d.name, d.rejected_warehouse) + stock_asset_account_name = warehouse_account[d.rejected_warehouse]["account"] + + make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name) + if warehouse_with_no_account: frappe.msgprint( _("No accounting entries for the following warehouses") diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 5e0663ea89d6..42f53a6abc88 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -4233,6 +4233,126 @@ def get_sabb_qty(sabb): # Test 3 - OverAllowanceError should be thrown as qty is greater than qty in DN self.assertRaises(erpnext.controllers.status_updater.OverAllowanceError, pr.submit) + def test_valuation_rate_for_rejected_materials(self): + item = make_item("Test Item with Rej Material Valuation", {"is_stock_item": 1}) + company = "_Test Company with perpetual inventory" + + warehouse = create_warehouse( + "_Test In-ward Warehouse", + company="_Test Company with perpetual inventory", + ) + + rej_warehouse = create_warehouse( + "_Test Warehouse - Rejected Material", + company="_Test Company with perpetual inventory", + ) + + frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 1) + + frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 1) + + pr = make_purchase_receipt( + item_code=item.name, + qty=10, + rate=100, + company=company, + warehouse=warehouse, + rejected_qty=5, + rejected_warehouse=rej_warehouse, + ) + + stock_received_but_not_billed_account = frappe.get_value( + "Company", + company, + "stock_received_but_not_billed", + ) + + rejected_item_cost = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "warehouse": rej_warehouse, + }, + "stock_value_difference", + ) + + self.assertEqual(rejected_item_cost, 500) + + srbnb_cost = frappe.db.get_value( + "GL Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "account": stock_received_but_not_billed_account, + }, + "credit", + ) + + self.assertEqual(srbnb_cost, 1500) + + frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0) + + frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 0) + + def test_no_valuation_rate_for_rejected_materials(self): + item = make_item("Test Item with Rej Material No Valuation", {"is_stock_item": 1}) + company = "_Test Company with perpetual inventory" + + warehouse = create_warehouse( + "_Test In-ward Warehouse", + company="_Test Company with perpetual inventory", + ) + + rej_warehouse = create_warehouse( + "_Test Warehouse - Rejected Material", + company="_Test Company with perpetual inventory", + ) + + frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0) + + frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 0) + + pr = make_purchase_receipt( + item_code=item.name, + qty=10, + rate=100, + company=company, + warehouse=warehouse, + rejected_qty=5, + rejected_warehouse=rej_warehouse, + ) + + stock_received_but_not_billed_account = frappe.get_value( + "Company", + company, + "stock_received_but_not_billed", + ) + + rejected_item_cost = frappe.db.get_value( + "Stock Ledger Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "warehouse": rej_warehouse, + }, + "stock_value_difference", + ) + + self.assertEqual(rejected_item_cost, 0.0) + + srbnb_cost = frappe.db.get_value( + "GL Entry", + { + "voucher_type": "Purchase Receipt", + "voucher_no": pr.name, + "account": stock_received_but_not_billed_account, + }, + "credit", + ) + + self.assertEqual(srbnb_cost, 1000) + def prepare_data_for_internal_transfer(): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier