Merge pull request #32563 from rohitwaghchaure/fixed-source-stock-reposting-for-purchase-flow
fix: consider sales rate as incoming rate for transit warehouse in purchase flow
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 2b633cb..5dbe7eb 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -705,6 +705,10 @@
)
)
+ credit_amount = item.base_net_amount
+ if self.is_internal_supplier and item.valuation_rate:
+ credit_amount = flt(item.valuation_rate * item.stock_qty)
+
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
gl_entries.append(
self.get_gl_dict(
@@ -714,7 +718,7 @@
"cost_center": item.cost_center,
"project": item.project or self.project,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")),
+ "debit": -1 * flt(credit_amount, item.precision("base_net_amount")),
},
warehouse_account[item.from_warehouse]["account_currency"],
item=item,
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index ce44ae3..52e221c 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -3306,6 +3306,7 @@
"item_name": args.item_name or "_Test Item",
"description": args.description or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "target_warehouse": args.target_warehouse,
"qty": args.qty or 1,
"uom": args.uom or "Nos",
"stock_uom": args.uom or "Nos",
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 6269724..1f807b8 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -2,6 +2,9 @@
# License: GNU General Public License v3. See license.txt
+from cmath import pi
+from turtle import update
+
import frappe
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cint, cstr, flt, today
@@ -14,6 +17,7 @@
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+from erpnext.stock.get_item_details import update_stock
from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction
@@ -1199,6 +1203,8 @@
self.assertEqual(pr1.items[0].rate, 100)
pr1.submit()
+ self.assertEqual(pr1.is_internal_supplier, 1)
+
# Backdated purchase receipt entry, the valuation rate should be updated for DN1 and PR1
make_purchase_receipt(
item_code=item_doc.name,
@@ -1241,6 +1247,234 @@
self.assertEqual(query[0].value, 0)
+ def test_backdated_transaction_for_internal_transfer_in_trasit_warehouse_for_purchase_receipt(
+ self,
+ ):
+ 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 Internal From Warehouse New", company=company)
+ to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company)
+ item_doc = create_item("Test Internal Transfer Item")
+
+ target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company)
+
+ make_purchase_receipt(
+ item_code=item_doc.name,
+ company=company,
+ posting_date=add_days(today(), -1),
+ warehouse=from_warehouse,
+ qty=1,
+ rate=100,
+ )
+
+ # Keep stock in advance and make sure that systen won't pick this stock while reposting backdated transaction
+ for i in range(1, 4):
+ make_purchase_receipt(
+ item_code=item_doc.name,
+ company=company,
+ posting_date=add_days(today(), -1 * i),
+ warehouse=target_warehouse,
+ qty=1,
+ rate=320 * i,
+ )
+
+ dn1 = create_delivery_note(
+ item_code=item_doc.name,
+ company=company,
+ customer=customer,
+ cost_center="Main - TCP1",
+ expense_account="Cost of Goods Sold - TCP1",
+ qty=1,
+ rate=500,
+ warehouse=from_warehouse,
+ target_warehouse=target_warehouse,
+ )
+
+ self.assertEqual(dn1.items[0].rate, 100)
+
+ pr1 = make_inter_company_purchase_receipt(dn1.name)
+ pr1.items[0].warehouse = to_warehouse
+ self.assertEqual(pr1.items[0].rate, 100)
+ pr1.submit()
+
+ stk_ledger = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "warehouse": target_warehouse},
+ ["stock_value_difference", "outgoing_rate"],
+ as_dict=True,
+ )
+
+ self.assertEqual(abs(stk_ledger.stock_value_difference), 100)
+ self.assertEqual(stk_ledger.outgoing_rate, 100)
+
+ # Backdated purchase receipt entry, the valuation rate should be updated for DN1 and PR1
+ make_purchase_receipt(
+ item_code=item_doc.name,
+ company=company,
+ posting_date=add_days(today(), -2),
+ warehouse=from_warehouse,
+ qty=1,
+ rate=200,
+ )
+
+ dn_value = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": dn1.name, "warehouse": target_warehouse},
+ "stock_value_difference",
+ )
+
+ self.assertEqual(abs(dn_value), 200.00)
+
+ pr_value = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "warehouse": to_warehouse},
+ "stock_value_difference",
+ )
+
+ self.assertEqual(abs(pr_value), 200.00)
+ pr1.load_from_db()
+
+ self.assertEqual(pr1.items[0].valuation_rate, 200)
+ self.assertEqual(pr1.items[0].rate, 100)
+
+ Gl = frappe.qb.DocType("GL Entry")
+
+ query = (
+ frappe.qb.from_(Gl)
+ .select(
+ (fn.Sum(Gl.debit) - fn.Sum(Gl.credit)).as_("value"),
+ )
+ .where((Gl.voucher_type == pr1.doctype) & (Gl.voucher_no == pr1.name))
+ ).run(as_dict=True)
+
+ self.assertEqual(query[0].value, 0)
+
+ def test_backdated_transaction_for_internal_transfer_in_trasit_warehouse_for_purchase_invoice(
+ self,
+ ):
+ from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
+ make_purchase_invoice as make_purchase_invoice_for_si,
+ )
+ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
+ make_inter_company_purchase_invoice,
+ )
+ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+
+ prepare_data_for_internal_transfer()
+ customer = "_Test Internal Customer 2"
+ company = "_Test Company with perpetual inventory"
+
+ from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company)
+ to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company)
+ item_doc = create_item("Test Internal Transfer Item")
+
+ target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company)
+
+ make_purchase_invoice_for_si(
+ item_code=item_doc.name,
+ company=company,
+ posting_date=add_days(today(), -1),
+ warehouse=from_warehouse,
+ qty=1,
+ update_stock=1,
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ rate=100,
+ )
+
+ # Keep stock in advance and make sure that systen won't pick this stock while reposting backdated transaction
+ for i in range(1, 4):
+ make_purchase_invoice_for_si(
+ item_code=item_doc.name,
+ company=company,
+ posting_date=add_days(today(), -1 * i),
+ warehouse=target_warehouse,
+ update_stock=1,
+ qty=1,
+ expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1",
+ rate=320 * i,
+ )
+
+ si1 = create_sales_invoice(
+ item_code=item_doc.name,
+ company=company,
+ customer=customer,
+ cost_center="Main - TCP1",
+ income_account="Sales - TCP1",
+ qty=1,
+ rate=500,
+ update_stock=1,
+ warehouse=from_warehouse,
+ target_warehouse=target_warehouse,
+ )
+
+ self.assertEqual(si1.items[0].rate, 100)
+
+ pi1 = make_inter_company_purchase_invoice(si1.name)
+ pi1.items[0].warehouse = to_warehouse
+ self.assertEqual(pi1.items[0].rate, 100)
+ pi1.update_stock = 1
+ pi1.save()
+ pi1.submit()
+
+ stk_ledger = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": pi1.doctype, "voucher_no": pi1.name, "warehouse": target_warehouse},
+ ["stock_value_difference", "outgoing_rate"],
+ as_dict=True,
+ )
+
+ self.assertEqual(abs(stk_ledger.stock_value_difference), 100)
+ self.assertEqual(stk_ledger.outgoing_rate, 100)
+
+ # Backdated purchase receipt entry, the valuation rate should be updated for si1 and pi1
+ make_purchase_receipt(
+ item_code=item_doc.name,
+ company=company,
+ posting_date=add_days(today(), -2),
+ warehouse=from_warehouse,
+ qty=1,
+ rate=200,
+ )
+
+ si_value = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": si1.doctype, "voucher_no": si1.name, "warehouse": target_warehouse},
+ "stock_value_difference",
+ )
+
+ self.assertEqual(abs(si_value), 200.00)
+
+ pi_value = frappe.db.get_value(
+ "Stock Ledger Entry",
+ {"voucher_type": pi1.doctype, "voucher_no": pi1.name, "warehouse": to_warehouse},
+ "stock_value_difference",
+ )
+
+ self.assertEqual(abs(pi_value), 200.00)
+ pi1.load_from_db()
+
+ self.assertEqual(pi1.items[0].valuation_rate, 200)
+ self.assertEqual(pi1.items[0].rate, 100)
+
+ Gl = frappe.qb.DocType("GL Entry")
+
+ query = (
+ frappe.qb.from_(Gl)
+ .select(
+ (fn.Sum(Gl.debit) - fn.Sum(Gl.credit)).as_("value"),
+ )
+ .where((Gl.voucher_type == pi1.doctype) & (Gl.voucher_no == pi1.name))
+ ).run(as_dict=True)
+
+ self.assertEqual(query[0].value, 0)
+
def test_batch_expiry_for_purchase_receipt(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 9ca40c3..cdf6e89 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -542,6 +542,14 @@
if not self.args.get("sle_id"):
self.get_dynamic_incoming_outgoing_rate(sle)
+ if (
+ sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
+ and sle.voucher_detail_no
+ and sle.actual_qty < 0
+ and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier")
+ ):
+ sle.outgoing_rate = get_incoming_rate_for_inter_company_transfer(sle)
+
if get_serial_nos(sle.serial_no):
self.get_serialized_values(sle)
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
@@ -589,6 +597,7 @@
sle.stock_queue = json.dumps(self.wh_data.stock_queue)
sle.stock_value_difference = stock_value_difference
sle.doctype = "Stock Ledger Entry"
+
frappe.get_doc(sle).db_update()
if not self.args.get("sle_id"):
@@ -652,22 +661,7 @@
and sle.voucher_detail_no
and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier")
):
- field = (
- "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
- )
- doctype = (
- "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
- )
- refernce_name = frappe.get_cached_value(
- sle.voucher_type + " Item", sle.voucher_detail_no, field
- )
-
- if refernce_name:
- rate = frappe.get_cached_value(
- doctype,
- refernce_name,
- "incoming_rate",
- )
+ rate = get_incoming_rate_for_inter_company_transfer(sle)
else:
if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
rate_field = "valuation_rate"
@@ -748,14 +742,12 @@
def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
- frappe.db.set_value(
- sle.voucher_type + " Item",
- sle.voucher_detail_no,
- {
- "base_net_rate": outgoing_rate,
- "valuation_rate": outgoing_rate,
- },
- )
+ if sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"] and frappe.get_cached_value(
+ sle.voucher_type, sle.voucher_no, "is_internal_supplier"
+ ):
+ frappe.db.set_value(
+ f"{sle.voucher_type} Item", sle.voucher_detail_no, "valuation_rate", sle.outgoing_rate
+ )
else:
frappe.db.set_value(
"Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate
@@ -1546,3 +1538,25 @@
if item_code and cint(frappe.db.get_value("Item", item_code, "allow_negative_stock", cache=True)):
return True
return False
+
+
+def get_incoming_rate_for_inter_company_transfer(sle) -> float:
+ """
+ For inter company transfer, incoming rate is the average of the outgoing rate
+ """
+ rate = 0.0
+
+ field = "delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
+
+ doctype = "Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
+
+ reference_name = frappe.get_cached_value(sle.voucher_type + " Item", sle.voucher_detail_no, field)
+
+ if reference_name:
+ rate = frappe.get_cached_value(
+ doctype,
+ reference_name,
+ "incoming_rate",
+ )
+
+ return rate