mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-24 13:57:05 +00:00
fix(stock): deduct rejected qty from the in-transit warehouse (#59299)
This commit is contained in:
@@ -467,7 +467,7 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
net_rate = self.get_internal_transfer_qty(item) * item.sales_incoming_rate
|
||||
|
||||
if (
|
||||
not net_rate
|
||||
@@ -803,6 +803,26 @@ class BuyingController(SubcontractingController):
|
||||
)
|
||||
)
|
||||
|
||||
def get_internal_transfer_qty(self, row) -> float:
|
||||
if flt(row.qty) or not self.is_internal_receipt():
|
||||
return flt(row.qty)
|
||||
|
||||
return flt(row.rejected_qty)
|
||||
|
||||
def is_internal_receipt(self) -> bool:
|
||||
return self.doctype == "Purchase Receipt" and self.is_internal_transfer()
|
||||
|
||||
def get_source_warehouse_qty(self, row, accepted_qty):
|
||||
if not (self.is_internal_receipt() and flt(row.rejected_qty)):
|
||||
return accepted_qty
|
||||
|
||||
if row.get("serial_and_batch_bundle") or row.get("rejected_serial_and_batch_bundle"):
|
||||
return accepted_qty
|
||||
|
||||
rejected_qty = flt(flt(row.rejected_qty) * flt(row.conversion_factor), row.precision("stock_qty"))
|
||||
|
||||
return flt(accepted_qty + rejected_qty, row.precision("stock_qty"))
|
||||
|
||||
def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
self.update_ordered_and_reserved_qty()
|
||||
|
||||
@@ -815,8 +835,9 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
if d.warehouse:
|
||||
pr_qty = flt(flt(d.qty) * flt(d.conversion_factor), d.precision("stock_qty"))
|
||||
source_qty = self.get_source_warehouse_qty(d, pr_qty)
|
||||
|
||||
if pr_qty:
|
||||
if pr_qty or source_qty:
|
||||
if d.from_warehouse and (
|
||||
(not cint(self.is_return) and self.docstatus == 1)
|
||||
or (cint(self.is_return) and self.docstatus == 2)
|
||||
@@ -832,7 +853,7 @@ class BuyingController(SubcontractingController):
|
||||
from_warehouse_sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
"actual_qty": -1 * pr_qty,
|
||||
"actual_qty": -1 * source_qty,
|
||||
"warehouse": d.from_warehouse,
|
||||
"outgoing_rate": d.rate,
|
||||
"recalculate_rate": 1,
|
||||
@@ -907,7 +928,7 @@ class BuyingController(SubcontractingController):
|
||||
from_warehouse_sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
"actual_qty": -1 * pr_qty,
|
||||
"actual_qty": -1 * source_qty,
|
||||
"warehouse": d.from_warehouse,
|
||||
"recalculate_rate": 1,
|
||||
"serial_and_batch_bundle": (
|
||||
|
||||
@@ -478,6 +478,14 @@ class PurchaseReceipt(BuyingController):
|
||||
|
||||
return process_gl_map(gl_entries, from_repost=frappe.flags.through_repost_item_valuation)
|
||||
|
||||
def is_rejected_material_valued(self) -> bool:
|
||||
if self.is_internal_transfer():
|
||||
return True
|
||||
|
||||
return bool(
|
||||
frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials")
|
||||
)
|
||||
|
||||
def make_item_gl_entries(self, gl_entries, inventory_account_map=None):
|
||||
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import (
|
||||
get_purchase_document_details,
|
||||
@@ -544,8 +552,10 @@ class PurchaseReceipt(BuyingController):
|
||||
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"
|
||||
if (
|
||||
item.get("rejected_qty")
|
||||
and not self.is_internal_transfer()
|
||||
and frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials")
|
||||
):
|
||||
outgoing_amount += get_stock_value_difference(self.name, item.name, item.rejected_warehouse)
|
||||
credit_amount = outgoing_amount
|
||||
@@ -701,9 +711,7 @@ class PurchaseReceipt(BuyingController):
|
||||
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"
|
||||
):
|
||||
if item.get("rejected_qty") and self.is_rejected_material_valued():
|
||||
rejected_item_cost = get_stock_value_difference(self.name, item.name, item.rejected_warehouse)
|
||||
divisional_loss -= rejected_item_cost
|
||||
|
||||
@@ -813,9 +821,7 @@ class PurchaseReceipt(BuyingController):
|
||||
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"
|
||||
):
|
||||
if d.rejected_qty and self.is_rejected_material_valued():
|
||||
stock_asset_rbnb = (
|
||||
self.get_company_default("asset_received_but_not_billed")
|
||||
if d.is_fixed_asset
|
||||
|
||||
@@ -2219,6 +2219,140 @@ class TestPurchaseReceipt(ERPNextTestSuite):
|
||||
|
||||
self.assertEqual(query[0].value, 0)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Buying Settings", {"set_valuation_rate_for_rejected_materials": 0})
|
||||
def test_internal_transfer_pr_rejected_qty_leaves_in_transit_warehouse(self):
|
||||
"""Rejected material of an internal transfer leaves the in-transit warehouse along with the
|
||||
accepted material, and is booked into the rejected warehouse, whatever Buying Settings says
|
||||
about valuing rejected material."""
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
customer = "_Test Internal Customer 2"
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test Rejected Transfer From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test Rejected Transfer Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test Rejected Transfer To", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test Rejected Transfer Rejected", company=company)
|
||||
|
||||
item_doc = create_item("Test Rejected Internal Transfer Item")
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
warehouse=from_warehouse,
|
||||
qty=10,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
customer=customer,
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=10,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
target_warehouse=transit_warehouse,
|
||||
)
|
||||
|
||||
pr = make_inter_company_purchase_receipt(dn.name)
|
||||
pr.items[0].warehouse = to_warehouse
|
||||
pr.items[0].qty = 7
|
||||
pr.items[0].rejected_qty = 3
|
||||
pr.items[0].received_qty = 10
|
||||
pr.items[0].rejected_warehouse = rejected_warehouse
|
||||
pr.submit()
|
||||
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "actual_qty", "stock_value_difference"],
|
||||
)
|
||||
|
||||
stock_qty = {d.warehouse: d.actual_qty for d in sl_entries}
|
||||
stock_value = {d.warehouse: d.stock_value_difference for d in sl_entries}
|
||||
|
||||
self.assertEqual(stock_qty[transit_warehouse], -10)
|
||||
self.assertEqual(stock_qty[to_warehouse], 7)
|
||||
self.assertEqual(stock_qty[rejected_warehouse], 3)
|
||||
|
||||
self.assertEqual(stock_value[transit_warehouse], -1000)
|
||||
self.assertEqual(stock_value[to_warehouse], 700)
|
||||
self.assertEqual(stock_value[rejected_warehouse], 300)
|
||||
|
||||
gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True)
|
||||
booked_value = {d.account: flt(d.debit) - flt(d.credit) for d in gl_entries}
|
||||
|
||||
self.assertEqual(sum(booked_value.values()), 0)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, transit_warehouse)], -1000)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, to_warehouse)], 700)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, rejected_warehouse)], 300)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Buying Settings", {"set_valuation_rate_for_rejected_materials": 0})
|
||||
def test_internal_transfer_pr_with_every_unit_rejected(self):
|
||||
"""A receipt that rejects the whole transfer still empties the in-transit warehouse."""
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test All Rejected From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test All Rejected Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test All Rejected To", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test All Rejected Rejected", company=company)
|
||||
|
||||
item_doc = create_item("Test All Rejected Internal Transfer Item")
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item_doc.name, company=company, warehouse=from_warehouse, qty=10, rate=100
|
||||
)
|
||||
|
||||
dn = create_delivery_note(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
customer="_Test Internal Customer 2",
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=10,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
target_warehouse=transit_warehouse,
|
||||
)
|
||||
|
||||
pr = make_inter_company_purchase_receipt(dn.name)
|
||||
pr.items[0].warehouse = to_warehouse
|
||||
pr.items[0].qty = 0
|
||||
pr.items[0].rejected_qty = 10
|
||||
pr.items[0].received_qty = 10
|
||||
pr.items[0].rejected_warehouse = rejected_warehouse
|
||||
pr.submit()
|
||||
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "actual_qty", "stock_value_difference"],
|
||||
)
|
||||
|
||||
stock_qty = {d.warehouse: d.actual_qty for d in sl_entries}
|
||||
stock_value = {d.warehouse: d.stock_value_difference for d in sl_entries}
|
||||
|
||||
self.assertNotIn(to_warehouse, stock_qty)
|
||||
self.assertEqual(stock_qty[transit_warehouse], -10)
|
||||
self.assertEqual(stock_qty[rejected_warehouse], 10)
|
||||
self.assertEqual(stock_value[transit_warehouse], -1000)
|
||||
self.assertEqual(stock_value[rejected_warehouse], 1000)
|
||||
|
||||
gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True)
|
||||
booked_value = {d.account: flt(d.debit) - flt(d.credit) for d in gl_entries}
|
||||
|
||||
self.assertEqual(sum(booked_value.values()), 0)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, transit_warehouse)], -1000)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, rejected_warehouse)], 1000)
|
||||
|
||||
def test_internal_transfer_pr_incoming_sle_anchored_to_dn_rate(self):
|
||||
"""Internal-transfer PR's inward SLE must use DN.incoming_rate even when
|
||||
PR.item.valuation_rate was wrong at submit, so divisional_loss does not
|
||||
|
||||
Reference in New Issue
Block a user