From 723ac0ffc4d25dc901c8c1b33a7b1b220682e642 Mon Sep 17 00:00:00 2001 From: Ninad Parikh <109862100+Ninad1306@users.noreply.github.com> Date: Fri, 26 Jul 2024 18:22:40 +0530 Subject: [PATCH] fix: Update Rate as per Valuation Rate for Internal Transfers only if Setting is Enabled (#42050) * fix: update rate for internal transfers only if settings enabled * fix: better naming * fix: create field for storing incoming rate in purchase doctypes * fix: use qty instead of qty_in_stock_uom * fix: add description, refactor for readablility * test: test case to validate internal transfers at arm's length price * fix: minor fix * fix: deletion of code not required --------- Co-authored-by: Smit Vora --- .../purchase_invoice_item.json | 13 +- .../purchase_invoice_item.py | 1 + erpnext/controllers/buying_controller.py | 134 ++++++++++-------- erpnext/controllers/selling_controller.py | 6 + .../tests/test_accounts_controller.py | 38 ++++- .../purchase_receipt_item.json | 13 +- .../purchase_receipt_item.py | 1 + .../stock_settings/stock_settings.json | 8 ++ .../doctype/stock_settings/stock_settings.py | 1 + 9 files changed, 154 insertions(+), 61 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index ff9269fbf2d..6e0dc694b3b 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -57,6 +57,7 @@ "base_net_rate", "base_net_amount", "valuation_rate", + "sales_incoming_rate", "item_tax_amount", "landed_cost_voucher_amount", "rm_supp_cost", @@ -958,12 +959,22 @@ "print_hide": 1, "read_only": 1, "search_index": 1 + }, + { + "description": "Valuation rate for the item as per Sales Invoice (Only for Internal Transfers)", + "fieldname": "sales_incoming_rate", + "fieldtype": "Currency", + "hidden": 1, + "label": "Sales Incoming Rate", + "no_copy": 1, + "options": "Company:company:default_currency", + "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-06-14 11:57:07.171700", + "modified": "2024-07-19 12:12:42.449298", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py index baeece4815c..a8f844c6c1c 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py @@ -79,6 +79,7 @@ class PurchaseInvoiceItem(Document): rejected_serial_no: DF.Text | None rejected_warehouse: DF.Link | None rm_supp_cost: DF.Currency + sales_incoming_rate: DF.Currency sales_invoice_item: DF.Data | None serial_and_batch_bundle: DF.Link | None serial_no: DF.Text | None diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 4dc1189d0fa..6cce2f40ad3 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -314,18 +314,22 @@ class BuyingController(SubcontractingController): get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0 ) + net_rate = item.base_net_amount + if item.sales_incoming_rate: # for internal transfer + net_rate = item.qty * item.sales_incoming_rate + qty_in_stock_uom = flt(item.qty * item.conversion_factor) if self.get("is_old_subcontracting_flow"): item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate) item.valuation_rate = ( - item.base_net_amount + net_rate + item.item_tax_amount + item.rm_supp_cost + flt(item.landed_cost_voucher_amount) ) / qty_in_stock_uom else: item.valuation_rate = ( - item.base_net_amount + net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount) + flt(item.get("rate_difference_with_purchase_invoice")) @@ -336,72 +340,88 @@ class BuyingController(SubcontractingController): update_regional_item_valuation_rate(self) def set_incoming_rate(self): - if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"): + """ + Override item rate with incoming rate for internal stock transfer + """ + if self.doctype not in ("Purchase Receipt", "Purchase Invoice"): + return + + if not (self.doctype == "Purchase Receipt" or self.get("update_stock")): + return + + if cint(self.get("is_return")): + # Get outgoing rate based on original item cost based on valuation method return if not self.is_internal_transfer(): return + allow_at_arms_length_price = frappe.get_cached_value( + "Stock Settings", None, "allow_internal_transfer_at_arms_length_price" + ) + if allow_at_arms_length_price: + return + + self.set_sales_incoming_rate_for_internal_transfer() + + for d in self.get("items"): + d.discount_percentage = 0.0 + d.discount_amount = 0.0 + d.margin_rate_or_amount = 0.0 + + if d.rate == d.sales_incoming_rate: + continue + + d.rate = d.sales_incoming_rate + frappe.msgprint( + _( + "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" + ).format(d.idx), + alert=1, + ) + + def set_sales_incoming_rate_for_internal_transfer(self): + """ + Set incoming rate from the sales transaction against which the + purchase is made (internal transfer) + """ ref_doctype_map = { - "Purchase Order": "Sales Order Item", "Purchase Receipt": "Delivery Note Item", "Purchase Invoice": "Sales Invoice Item", } ref_doctype = ref_doctype_map.get(self.doctype) - items = self.get("items") - for d in items: - if not cint(self.get("is_return")): - # Get outgoing rate based on original item cost based on valuation method + for d in self.get("items"): + if not d.get(frappe.scrub(ref_doctype)): + posting_time = self.get("posting_time") + if not posting_time: + posting_time = nowtime() - if not d.get(frappe.scrub(ref_doctype)): - posting_time = self.get("posting_time") - if not posting_time and self.doctype == "Purchase Order": - posting_time = nowtime() + outgoing_rate = get_incoming_rate( + { + "item_code": d.item_code, + "warehouse": d.get("from_warehouse"), + "posting_date": self.get("posting_date") or self.get("transaction_date"), + "posting_time": posting_time, + "qty": -1 * flt(d.get("stock_qty")), + "serial_and_batch_bundle": d.get("serial_and_batch_bundle"), + "company": self.company, + "voucher_type": self.doctype, + "voucher_no": self.name, + "allow_zero_valuation": d.get("allow_zero_valuation"), + "voucher_detail_no": d.name, + }, + raise_error_if_no_rate=False, + ) - outgoing_rate = get_incoming_rate( - { - "item_code": d.item_code, - "warehouse": d.get("from_warehouse"), - "posting_date": self.get("posting_date") or self.get("transaction_date"), - "posting_time": posting_time, - "qty": -1 * flt(d.get("stock_qty")), - "serial_and_batch_bundle": d.get("serial_and_batch_bundle"), - "company": self.company, - "voucher_type": self.doctype, - "voucher_no": self.name, - "allow_zero_valuation": d.get("allow_zero_valuation"), - "voucher_detail_no": d.name, - }, - raise_error_if_no_rate=False, - ) - - rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate")) - else: - field = ( - "incoming_rate" - if self.get("is_internal_supplier") and not self.doctype == "Purchase Order" - else "rate" - ) - rate = flt( - frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field) - * (d.conversion_factor or 1), - d.precision("rate"), - ) - - if self.is_internal_transfer(): - if self.doctype == "Purchase Receipt" or self.get("update_stock"): - if rate != d.rate: - d.rate = rate - frappe.msgprint( - _( - "Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer" - ).format(d.idx), - alert=1, - ) - d.discount_percentage = 0.0 - d.discount_amount = 0.0 - d.margin_rate_or_amount = 0.0 + d.sales_incoming_rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate")) + else: + field = "incoming_rate" if self.get("is_internal_supplier") else "rate" + d.sales_incoming_rate = flt( + frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field) + * (d.conversion_factor or 1), + d.precision("rate"), + ) def validate_for_subcontracting(self): if self.is_subcontracted and self.get("is_old_subcontracting_flow"): @@ -566,11 +586,9 @@ class BuyingController(SubcontractingController): if d.from_warehouse: sle.dependant_sle_voucher_detail_no = d.name else: - val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9 - incoming_rate = flt(d.valuation_rate, val_rate_db_precision) sle.update( { - "incoming_rate": incoming_rate, + "incoming_rate": d.valuation_rate, "recalculate_rate": 1 if (self.is_subcontracted and (d.bom or d.get("fg_item"))) or d.from_warehouse else 0, diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index d926395f7c4..55d01da17d1 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -432,6 +432,9 @@ class SellingController(StockController): if self.doctype not in ("Delivery Note", "Sales Invoice"): return + allow_at_arms_length_price = frappe.get_cached_value( + "Stock Settings", None, "allow_internal_transfer_at_arms_length_price" + ) items = self.get("items") + (self.get("packed_items") or []) for d in items: if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"): @@ -478,6 +481,9 @@ class SellingController(StockController): if d.incoming_rate != incoming_rate: d.incoming_rate = incoming_rate else: + if allow_at_arms_length_price: + continue + rate = flt( flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor, d.precision("rate"), diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index 3f6830c2021..b2f8fce3d31 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -5,7 +5,7 @@ import frappe from frappe import qb from frappe.query_builder.functions import Sum -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, change_settings from frappe.utils import add_days, getdate, nowdate from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry @@ -13,6 +13,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.party import get_party_account +from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer from erpnext.stock.doctype.item.test_item import create_item @@ -804,6 +805,41 @@ class TestAccountsController(FrappeTestCase): self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_pe, []) + @change_settings("Stock Settings", {"allow_internal_transfer_at_arms_length_price": 1}) + def test_16_internal_transfer_at_arms_length_price(self): + from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse + + prepare_data_for_internal_transfer() + company = "_Test Company with perpetual inventory" + target_warehouse = create_warehouse("_Test Internal Warehouse New 1", company=company) + warehouse = create_warehouse("_Test Internal Warehouse New 2", company=company) + arms_length_price = 40 + + si = create_sales_invoice( + company=company, + customer="_Test Internal Customer 2", + debit_to="Debtors - TCP1", + target_warehouse=target_warehouse, + warehouse=warehouse, + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + update_stock=True, + do_not_save=True, + do_not_submit=True, + ) + + si.items[0].rate = arms_length_price + si.save() + # rate should not reset to incoming rate + self.assertEqual(si.items[0].rate, arms_length_price) + + frappe.db.set_single_value("Stock Settings", "allow_internal_transfer_at_arms_length_price", 0) + si.items[0].rate = arms_length_price + si.save() + # rate should reset to incoming rate + self.assertEqual(si.items[0].rate, 100) + def test_20_journal_against_sales_invoice(self): # Invoice in Foreign Currency si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 9604c55450e..0a5e8c60403 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -67,6 +67,7 @@ "base_net_rate", "base_net_amount", "valuation_rate", + "sales_incoming_rate", "item_tax_amount", "rm_supp_cost", "landed_cost_voucher_amount", @@ -1124,12 +1125,22 @@ "fieldtype": "Check", "label": "Return Qty from Rejected Warehouse", "read_only": 1 + }, + { + "description": "Valuation rate for the item as per Sales Invoice (Only for Internal Transfers)", + "fieldname": "sales_incoming_rate", + "fieldtype": "Currency", + "hidden": 1, + "label": "Sales Incoming Rate", + "no_copy": 1, + "options": "Company:company:default_currency", + "print_hide": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2024-05-28 09:48:24.448815", + "modified": "2024-07-19 12:14:21.521466", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py index 393b6a25691..2154007771d 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py @@ -88,6 +88,7 @@ class PurchaseReceiptItem(Document): return_qty_from_rejected_warehouse: DF.Check returned_qty: DF.Float rm_supp_cost: DF.Currency + sales_incoming_rate: DF.Currency sales_order: DF.Link | None sales_order_item: DF.Data | None sample_quantity: DF.Int diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json index 64c524738d2..7f3a3e68d21 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.json +++ b/erpnext/stock/doctype/stock_settings/stock_settings.json @@ -32,6 +32,7 @@ "allow_negative_stock", "show_barcode_field", "clean_description_html", + "allow_internal_transfer_at_arms_length_price", "quality_inspection_settings_section", "action_if_quality_inspection_is_not_submitted", "column_break_23", @@ -440,6 +441,13 @@ "fieldtype": "Check", "label": "Do Not Update Serial / Batch on Creation of Auto Bundle" }, + { + "default": "0", + "description": "If enabled, the item rate won't adjust to the valuation rate during internal transfers, but accounting will still use the valuation rate.", + "fieldname": "allow_internal_transfer_at_arms_length_price", + "fieldtype": "Check", + "label": "Allow Internal Transfers at Arm's Length Price" + }, { "default": "0", "depends_on": "eval:doc.valuation_method === \"Moving Average\"", diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py index fae75f49777..c029b7bd1fb 100644 --- a/erpnext/stock/doctype/stock_settings/stock_settings.py +++ b/erpnext/stock/doctype/stock_settings/stock_settings.py @@ -27,6 +27,7 @@ class StockSettings(Document): action_if_quality_inspection_is_rejected: DF.Literal["Stop", "Warn"] allow_from_dn: DF.Check allow_from_pr: DF.Check + allow_internal_transfer_at_arms_length_price: DF.Check allow_negative_stock: DF.Check allow_partial_reservation: DF.Check allow_to_edit_stock_uom_qty_for_purchase: DF.Check