Compare commits

...

2 Commits

Author SHA1 Message Date
Mihir Kandoi
98df30d1da test(accounts): cover rejected material value on a stock updating invoice 2026-09-22 10:59:40 +05:30
Mihir Kandoi
6c3046121d fix(stock): stop valuing rejected material on a stock updating invoice
A Purchase Receipt books rejected material against Stock Received But Not
Billed, so the supplier still owes an invoice for it and the value has a
source. A Purchase Invoice that updates stock bills the accepted quantity
alone, yet Set Valuation Rate For Rejected Materials gave its rejected
material the invoice rate as well. The rejected warehouse then received
stock value that no GL entry backed: ten units at 100 with four rejected
moved 1000 into stock and booked 600, leaving the ledgers 400 apart.

Read the setting through is_rejected_material_valued, which excludes the
invoice, from both the plain rows in update_stock_ledger and the tracked
rows in the bundle. Internal transfers are unaffected: their inward rate is
anchored to the delivery note in stock_ledger.process_sle.
2026-09-22 10:59:39 +05:30
4 changed files with 64 additions and 4 deletions

View File

@@ -2624,6 +2624,51 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
return_pi.submit()
self.assertEqual(return_pi.docstatus, 1)
def test_rejected_material_is_not_valued_on_a_stock_updating_invoice(self):
"""An invoice bills the accepted quantity alone, so its rejected material has no cost and the
stock it moves must match the entries it books."""
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
company = "_Test Company with perpetual inventory"
item = make_item(properties={"is_stock_item": 1, "valuation_method": "FIFO"}).name
rejected_warehouse = create_warehouse("_Test Invoice Rejected Warehouse", company=company)
frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 1)
self.addCleanup(
frappe.db.set_single_value, "Buying Settings", "set_valuation_rate_for_rejected_materials", 0
)
pi = make_purchase_invoice(
item_code=item,
company=company,
warehouse="Stores - TCP1",
rejected_warehouse=rejected_warehouse,
cost_center="Main - TCP1",
supplier_warehouse="Work In Progress - TCP1",
expense_account="_Test Account Cost for Goods Sold - TCP1",
update_stock=1,
received_qty=10,
qty=6,
rejected_qty=4,
rate=100,
)
stock_value = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_no": pi.name, "is_cancelled": 0},
fields=["warehouse", "stock_value_difference"],
)
by_warehouse = {d.warehouse: d.stock_value_difference for d in stock_value}
self.assertEqual(by_warehouse["Stores - TCP1"], 600)
self.assertEqual(by_warehouse[rejected_warehouse], 0)
booked = frappe.get_all(
"GL Entry", filters={"voucher_no": pi.name, "is_cancelled": 0}, fields=["debit"]
)
self.assertEqual(sum(flt(d.debit) for d in booked), sum(by_warehouse.values()))
def test_purchase_invoice_with_use_serial_batch_field_for_rejected_qty(self):
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse

View File

@@ -77,3 +77,14 @@ class BuyingSettings(Document):
def check_maintain_same_rate(self):
if self.maintain_same_rate:
self.set_landed_cost_based_on_purchase_invoice_rate = 0
def is_rejected_material_valued(voucher_type: str) -> bool:
"""Rejected material carries stock value only when something is going to pay for it. A Purchase
Receipt books it against Stock Received But Not Billed, so the supplier still owes an invoice for
it. A stock updating Purchase Invoice bills the accepted quantity alone, so its rejected material
has no cost to carry."""
if voucher_type == "Purchase Invoice":
return False
return bool(frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials"))

View File

@@ -14,6 +14,9 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.party import _get_party_details
from erpnext.buying.doctype.buying_settings.buying_settings import (
is_rejected_material_valued,
)
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
@@ -871,7 +874,7 @@ class BuyingController(SubcontractingController):
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"):
if is_rejected_material_valued(self.doctype):
valuation_rate_for_rejected_item = d.valuation_rate
sl_entries.append(

View File

@@ -27,6 +27,9 @@ from frappe.utils import (
)
from frappe.utils.csvutils import build_csv_response
from erpnext.buying.doctype.buying_settings.buying_settings import (
is_rejected_material_valued,
)
from erpnext.stock.doctype.purchase_receipt_item.purchase_receipt_item import PurchaseReceiptItem
from erpnext.stock.serial_batch_bundle import (
BatchNoValuation,
@@ -899,9 +902,7 @@ class SerialandBatchBundle(Document):
if batches and valuation_method == "FIFO":
stock_queue = parse_json(prev_sle.stock_queue)
set_valuation_rate_for_rejected_materials = frappe.db.get_single_value(
"Buying Settings", "set_valuation_rate_for_rejected_materials"
)
set_valuation_rate_for_rejected_materials = is_rejected_material_valued(self.voucher_type)
precision = frappe.get_precision("Serial and Batch Entry", "incoming_rate")
for d in self.entries: