Skip to content

feat: allow to set valuation rate for Rejected Materials #47582

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions erpnext/buying/doctype/buying_settings/buying_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand All @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions erpnext/buying/doctype/buying_settings/buying_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()

Expand Down
6 changes: 5 additions & 1 deletion erpnext/controllers/buying_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
},
)
Expand Down
28 changes: 27 additions & 1 deletion erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down
120 changes: 120 additions & 0 deletions erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading