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 <smitvora203@gmail.com>
This commit is contained in:
Ninad Parikh
2024-07-26 18:22:40 +05:30
committed by GitHub
parent 096ec2db6a
commit 723ac0ffc4
9 changed files with 154 additions and 61 deletions

View File

@@ -57,6 +57,7 @@
"base_net_rate", "base_net_rate",
"base_net_amount", "base_net_amount",
"valuation_rate", "valuation_rate",
"sales_incoming_rate",
"item_tax_amount", "item_tax_amount",
"landed_cost_voucher_amount", "landed_cost_voucher_amount",
"rm_supp_cost", "rm_supp_cost",
@@ -958,12 +959,22 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
"search_index": 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, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-06-14 11:57:07.171700", "modified": "2024-07-19 12:12:42.449298",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@@ -79,6 +79,7 @@ class PurchaseInvoiceItem(Document):
rejected_serial_no: DF.Text | None rejected_serial_no: DF.Text | None
rejected_warehouse: DF.Link | None rejected_warehouse: DF.Link | None
rm_supp_cost: DF.Currency rm_supp_cost: DF.Currency
sales_incoming_rate: DF.Currency
sales_invoice_item: DF.Data | None sales_invoice_item: DF.Data | None
serial_and_batch_bundle: DF.Link | None serial_and_batch_bundle: DF.Link | None
serial_no: DF.Text | None serial_no: DF.Text | None

View File

@@ -314,18 +314,22 @@ class BuyingController(SubcontractingController):
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0 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) qty_in_stock_uom = flt(item.qty * item.conversion_factor)
if self.get("is_old_subcontracting_flow"): if self.get("is_old_subcontracting_flow"):
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate) item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
item.valuation_rate = ( item.valuation_rate = (
item.base_net_amount net_rate
+ item.item_tax_amount + item.item_tax_amount
+ item.rm_supp_cost + item.rm_supp_cost
+ flt(item.landed_cost_voucher_amount) + flt(item.landed_cost_voucher_amount)
) / qty_in_stock_uom ) / qty_in_stock_uom
else: else:
item.valuation_rate = ( item.valuation_rate = (
item.base_net_amount net_rate
+ item.item_tax_amount + item.item_tax_amount
+ flt(item.landed_cost_voucher_amount) + flt(item.landed_cost_voucher_amount)
+ flt(item.get("rate_difference_with_purchase_invoice")) + flt(item.get("rate_difference_with_purchase_invoice"))
@@ -336,72 +340,88 @@ class BuyingController(SubcontractingController):
update_regional_item_valuation_rate(self) update_regional_item_valuation_rate(self)
def set_incoming_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 return
if not self.is_internal_transfer(): if not self.is_internal_transfer():
return 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 = { ref_doctype_map = {
"Purchase Order": "Sales Order Item",
"Purchase Receipt": "Delivery Note Item", "Purchase Receipt": "Delivery Note Item",
"Purchase Invoice": "Sales Invoice Item", "Purchase Invoice": "Sales Invoice Item",
} }
ref_doctype = ref_doctype_map.get(self.doctype) ref_doctype = ref_doctype_map.get(self.doctype)
items = self.get("items") for d in self.get("items"):
for d in items: if not d.get(frappe.scrub(ref_doctype)):
if not cint(self.get("is_return")): posting_time = self.get("posting_time")
# Get outgoing rate based on original item cost based on valuation method if not posting_time:
posting_time = nowtime()
if not d.get(frappe.scrub(ref_doctype)): outgoing_rate = get_incoming_rate(
posting_time = self.get("posting_time") {
if not posting_time and self.doctype == "Purchase Order": "item_code": d.item_code,
posting_time = nowtime() "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( d.sales_incoming_rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
{ else:
"item_code": d.item_code, field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
"warehouse": d.get("from_warehouse"), d.sales_incoming_rate = flt(
"posting_date": self.get("posting_date") or self.get("transaction_date"), frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
"posting_time": posting_time, * (d.conversion_factor or 1),
"qty": -1 * flt(d.get("stock_qty")), d.precision("rate"),
"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
def validate_for_subcontracting(self): def validate_for_subcontracting(self):
if self.is_subcontracted and self.get("is_old_subcontracting_flow"): if self.is_subcontracted and self.get("is_old_subcontracting_flow"):
@@ -566,11 +586,9 @@ class BuyingController(SubcontractingController):
if d.from_warehouse: if d.from_warehouse:
sle.dependant_sle_voucher_detail_no = d.name sle.dependant_sle_voucher_detail_no = d.name
else: 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( sle.update(
{ {
"incoming_rate": incoming_rate, "incoming_rate": d.valuation_rate,
"recalculate_rate": 1 "recalculate_rate": 1
if (self.is_subcontracted and (d.bom or d.get("fg_item"))) or d.from_warehouse if (self.is_subcontracted and (d.bom or d.get("fg_item"))) or d.from_warehouse
else 0, else 0,

View File

@@ -432,6 +432,9 @@ class SellingController(StockController):
if self.doctype not in ("Delivery Note", "Sales Invoice"): if self.doctype not in ("Delivery Note", "Sales Invoice"):
return 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 []) items = self.get("items") + (self.get("packed_items") or [])
for d in items: for d in items:
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"): 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: if d.incoming_rate != incoming_rate:
d.incoming_rate = incoming_rate d.incoming_rate = incoming_rate
else: else:
if allow_at_arms_length_price:
continue
rate = flt( rate = flt(
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor, flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
d.precision("rate"), d.precision("rate"),

View File

@@ -5,7 +5,7 @@
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.query_builder.functions import Sum 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 frappe.utils import add_days, getdate, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry 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.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.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account 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 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_si, [])
self.assertEqual(exc_je_for_pe, []) 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): def test_20_journal_against_sales_invoice(self):
# Invoice in Foreign Currency # Invoice in Foreign Currency
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)

View File

@@ -67,6 +67,7 @@
"base_net_rate", "base_net_rate",
"base_net_amount", "base_net_amount",
"valuation_rate", "valuation_rate",
"sales_incoming_rate",
"item_tax_amount", "item_tax_amount",
"rm_supp_cost", "rm_supp_cost",
"landed_cost_voucher_amount", "landed_cost_voucher_amount",
@@ -1124,12 +1125,22 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Return Qty from Rejected Warehouse", "label": "Return Qty from Rejected Warehouse",
"read_only": 1 "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, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2024-05-28 09:48:24.448815", "modified": "2024-07-19 12:14:21.521466",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt Item", "name": "Purchase Receipt Item",

View File

@@ -88,6 +88,7 @@ class PurchaseReceiptItem(Document):
return_qty_from_rejected_warehouse: DF.Check return_qty_from_rejected_warehouse: DF.Check
returned_qty: DF.Float returned_qty: DF.Float
rm_supp_cost: DF.Currency rm_supp_cost: DF.Currency
sales_incoming_rate: DF.Currency
sales_order: DF.Link | None sales_order: DF.Link | None
sales_order_item: DF.Data | None sales_order_item: DF.Data | None
sample_quantity: DF.Int sample_quantity: DF.Int

View File

@@ -32,6 +32,7 @@
"allow_negative_stock", "allow_negative_stock",
"show_barcode_field", "show_barcode_field",
"clean_description_html", "clean_description_html",
"allow_internal_transfer_at_arms_length_price",
"quality_inspection_settings_section", "quality_inspection_settings_section",
"action_if_quality_inspection_is_not_submitted", "action_if_quality_inspection_is_not_submitted",
"column_break_23", "column_break_23",
@@ -440,6 +441,13 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Do Not Update Serial / Batch on Creation of Auto Bundle" "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", "default": "0",
"depends_on": "eval:doc.valuation_method === \"Moving Average\"", "depends_on": "eval:doc.valuation_method === \"Moving Average\"",

View File

@@ -27,6 +27,7 @@ class StockSettings(Document):
action_if_quality_inspection_is_rejected: DF.Literal["Stop", "Warn"] action_if_quality_inspection_is_rejected: DF.Literal["Stop", "Warn"]
allow_from_dn: DF.Check allow_from_dn: DF.Check
allow_from_pr: DF.Check allow_from_pr: DF.Check
allow_internal_transfer_at_arms_length_price: DF.Check
allow_negative_stock: DF.Check allow_negative_stock: DF.Check
allow_partial_reservation: DF.Check allow_partial_reservation: DF.Check
allow_to_edit_stock_uom_qty_for_purchase: DF.Check allow_to_edit_stock_uom_qty_for_purchase: DF.Check