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:
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,27 +340,61 @@ 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 cint(self.get("is_return")):
|
|
||||||
# Get outgoing rate based on original item cost based on valuation method
|
|
||||||
|
|
||||||
if not d.get(frappe.scrub(ref_doctype)):
|
if not d.get(frappe.scrub(ref_doctype)):
|
||||||
posting_time = self.get("posting_time")
|
posting_time = self.get("posting_time")
|
||||||
if not posting_time and self.doctype == "Purchase Order":
|
if not posting_time:
|
||||||
posting_time = nowtime()
|
posting_time = nowtime()
|
||||||
|
|
||||||
outgoing_rate = get_incoming_rate(
|
outgoing_rate = get_incoming_rate(
|
||||||
@@ -376,33 +414,15 @@ class BuyingController(SubcontractingController):
|
|||||||
raise_error_if_no_rate=False,
|
raise_error_if_no_rate=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
d.sales_incoming_rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
||||||
else:
|
else:
|
||||||
field = (
|
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
|
||||||
"incoming_rate"
|
d.sales_incoming_rate = flt(
|
||||||
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)
|
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||||
* (d.conversion_factor or 1),
|
* (d.conversion_factor or 1),
|
||||||
d.precision("rate"),
|
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"):
|
||||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse:
|
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse:
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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\"",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user