feat: Repost item costing (#24183)

* Repost item valuation (#24031)

* feat: Reposting logic for future finished/transferred item

* feat: added fields to identify needs to recalculate rate while reposting

* refactor: Set rate for outgoing and finished items

* refactor: Arranged fields in Stock Entry item table and added fields to identify finished and scrap item

* refactor: Arranged fields in Stock Entry item table and added fields to identify finished and scrap item

* refactor: Get outgoing rate for purchase return

* refactor: Get incoming rate for sales return

* test: Added tests for reposting valuation of transferred/finished/returned items

* feat: added incoming rate field in DN, SI and Packed Item table

* feat: get incoming rate for returned item

* fix: no error while getting valuation rate in stock entry

* fix: update stock ledger for DN and SI

* feat: update item valuation rate in PR and PI based on supplied items cost

* feat: SLE reposting logic for sales return and subcontracted item with test cases

* feat: update qty in future sle

* feat: repost future sle and gle via Repost Item Valuation

* fix: Skip unwanted function calling while reposting

* fix: repost sle for specific item and warehouse

* test: Modified tests for backdated stock reco

* fix: ignore cancelled sle in few methods

* feat: role allowed to do backdated entry

* feat: Show reposting status on stock valuation related reports

* fix: minor fixes

* fix: fixed sider issues

* fix: serial no fix related to immutable ledger

* fix: Test cases fixes related to perpetual inventory

* fix: Test cases fixed

* fix: Fixed reposting on cancel and test cases

* feat: Restart reposting item valuation

* refactor: Code cleanup using small functions and test case fixes

* fix: minor fixes

* fix: Raise on error while reposting item valuation

* fix: minor fix

* fix: Tests fixed

* fix: skip some validation ig gle made from reposting

* fix: test fixes

* fix: debugging stock and account validation

* fix: debugging stock and account validation

* fix: debugging travis for stock and account sync validation

* fix: debugging travis

* fix: debugging travis

* fix: debugging travis

* fix: removed duplicate field from pos profile
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 286c4f4..dc61870 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -16,6 +16,8 @@
 
 from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
 from erpnext.controllers.stock_controller import StockController
+from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
+from erpnext.stock.utils import get_incoming_rate
 
 class BuyingController(StockController):
 	def __setup__(self):
@@ -63,7 +65,7 @@
 			self.set_landed_cost_voucher_amount()
 
 		if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
-			self.update_valuation_rate("items")
+			self.update_valuation_rate()
 
 	def set_missing_values(self, for_validate=False):
 		super(BuyingController, self).set_missing_values(for_validate)
@@ -177,7 +179,7 @@
 			self.in_words = money_in_words(amount, self.currency)
 
 	# update valuation rate
-	def update_valuation_rate(self, parentfield):
+	def update_valuation_rate(self, reset_outgoing_rate=True):
 		"""
 			item_tax_amount is the total tax amount applied on that item
 			stored for valuation
@@ -188,7 +190,7 @@
 
 		stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
 		last_item_idx = 1
-		for d in self.get(parentfield):
+		for d in self.get("items"):
 			if d.item_code and d.item_code in stock_and_asset_items:
 				stock_and_asset_items_qty += flt(d.qty)
 				stock_and_asset_items_amount += flt(d.base_net_amount)
@@ -198,7 +200,7 @@
 			if d.category in ["Valuation", "Valuation and Total"]])
 
 		valuation_amount_adjustment = total_valuation_amount
-		for i, item in enumerate(self.get(parentfield)):
+		for i, item in enumerate(self.get("items")):
 			if item.item_code and item.qty and item.item_code in stock_and_asset_items:
 				item_proportion = flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount \
 					else flt(item.qty) / stock_and_asset_items_qty
@@ -216,16 +218,34 @@
 					item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
 
 				qty_in_stock_uom = flt(item.qty * item.conversion_factor)
-				rm_supp_cost = flt(item.rm_supp_cost) if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0
-
-				landed_cost_voucher_amount = flt(item.landed_cost_voucher_amount) \
-					if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0
-
-				item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + rm_supp_cost
-					 + landed_cost_voucher_amount) / qty_in_stock_uom)
+				item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
+				item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + item.rm_supp_cost
+					 + flt(item.landed_cost_voucher_amount)) / qty_in_stock_uom)
 			else:
 				item.valuation_rate = 0.0
 
+	def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
+		supplied_items_cost = 0.0
+		for d in self.get("supplied_items"):
+			if d.reference_name == item_row_id:
+				if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'):
+					rate = get_incoming_rate({
+						"item_code": d.rm_item_code,
+						"warehouse": self.supplier_warehouse,
+						"posting_date": self.posting_date,
+						"posting_time": self.posting_time,
+						"qty": -1 * d.consumed_qty,
+						"serial_no": d.serial_no
+					})
+
+					if rate > 0:
+						d.rate = rate
+
+				d.amount = flt(d.consumed_qty) * flt(d.rate)
+				supplied_items_cost += flt(d.amount)
+		
+		return supplied_items_cost
+
 	def validate_for_subcontracting(self):
 		if not self.is_subcontracted and self.sub_contracted_items:
 			frappe.throw(_("Please enter 'Is Subcontracted' as Yes or No"))
@@ -352,35 +372,17 @@
 				else:
 					self.append_raw_material_to_be_backflushed(item, raw_material, qty)
 
-	def append_raw_material_to_be_backflushed(self, fg_item_doc, raw_material_data, qty):
+	def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
 		rm = self.append('supplied_items', {})
 		rm.update(raw_material_data)
 
 		if not rm.main_item_code:
-			rm.main_item_code = fg_item_doc.item_code
+			rm.main_item_code = fg_item_row.item_code
 
-		rm.reference_name = fg_item_doc.name
+		rm.reference_name = fg_item_row.name
 		rm.required_qty = qty
 		rm.consumed_qty = qty
 
-		if not raw_material_data.get('non_stock_item'):
-			from erpnext.stock.utils import get_incoming_rate
-			rm.rate = get_incoming_rate({
-				"item_code": raw_material_data.rm_item_code,
-				"warehouse": self.supplier_warehouse,
-				"posting_date": self.posting_date,
-				"posting_time": self.posting_time,
-				"qty": -1 * qty,
-				"serial_no": rm.serial_no
-			})
-
-			if not rm.rate:
-				rm.rate = get_valuation_rate(raw_material_data.rm_item_code, self.supplier_warehouse,
-					self.doctype, self.name, currency=self.company_currency, company=self.company)
-
-		rm.amount = qty * flt(rm.rate)
-		fg_item_doc.rm_supp_cost += rm.amount
-
 	def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
 		exploded_item = 1
 		if hasattr(item, 'include_exploded_items'):
@@ -389,7 +391,7 @@
 		bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
 
 		used_alternative_items = []
-		if self.doctype == 'Purchase Receipt' and item.purchase_order:
+		if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
 			used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
 
 		raw_materials_cost = 0
@@ -406,7 +408,7 @@
 					reserve_warehouse = None
 
 			conversion_factor = item.conversion_factor
-			if (self.doctype == 'Purchase Receipt' and item.purchase_order and
+			if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
 				bom_item.item_code in used_alternative_items):
 				alternative_item_data = used_alternative_items.get(bom_item.item_code)
 				bom_item.item_code = alternative_item_data.item_code
@@ -434,9 +436,7 @@
 			rm.rm_item_code = bom_item.item_code
 			rm.stock_uom = bom_item.stock_uom
 			rm.required_qty = required_qty
-			if self.doctype == "Purchase Order" and not rm.reserve_warehouse:
-				rm.reserve_warehouse = reserve_warehouse
-
+			rm.rate = bom_item.rate
 			rm.conversion_factor = conversion_factor
 
 			if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
@@ -444,29 +444,8 @@
 				rm.description = bom_item.description
 				if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
 					rm.batch_no = item.batch_no
-
-			# get raw materials rate
-			if self.doctype == "Purchase Receipt":
-				from erpnext.stock.utils import get_incoming_rate
-				rm.rate = get_incoming_rate({
-					"item_code": bom_item.item_code,
-					"warehouse": self.supplier_warehouse,
-					"posting_date": self.posting_date,
-					"posting_time": self.posting_time,
-					"qty": -1 * required_qty,
-					"serial_no": rm.serial_no
-				})
-				if not rm.rate:
-					rm.rate = get_valuation_rate(bom_item.item_code, self.supplier_warehouse,
-						self.doctype, self.name, currency=self.company_currency, company = self.company)
-			else:
-				rm.rate = bom_item.rate
-
-			rm.amount = required_qty * flt(rm.rate)
-			raw_materials_cost += flt(rm.amount)
-
-		if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
-			item.rm_supp_cost = raw_materials_cost
+			elif not rm.reserve_warehouse:
+				rm.reserve_warehouse = reserve_warehouse
 
 	def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
 		"""Remove all those child items which are no longer present in main item table"""
@@ -579,7 +558,8 @@
 						or (cint(self.is_return) and self.docstatus==2)):
 						from_warehouse_sle = self.get_sl_entries(d, {
 							"actual_qty": -1 * pr_qty,
-							"warehouse": d.from_warehouse
+							"warehouse": d.from_warehouse,
+							"dependant_sle_voucher_detail_no": d.name
 						})
 
 						sl_entries.append(from_warehouse_sle)
@@ -589,28 +569,20 @@
 						"serial_no": cstr(d.serial_no).strip()
 					})
 					if self.is_return:
-						filters = {
-							"voucher_type": self.doctype,
-							"voucher_no": self.return_against,
-							"item_code": d.item_code
-						}
-
-						if (self.doctype == "Purchase Invoice" and self.update_stock
-							and d.get("purchase_invoice_item")):
-							filters["voucher_detail_no"] = d.purchase_invoice_item
-						elif self.doctype == "Purchase Receipt" and d.get("purchase_receipt_item"):
-							filters["voucher_detail_no"] = d.purchase_receipt_item
-
-						original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", filters, "incoming_rate")
+						outgoing_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d)
 
 						sle.update({
-							"outgoing_rate": original_incoming_rate
+							"outgoing_rate": outgoing_rate,
+							"recalculate_rate": 1
 						})
+						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": incoming_rate,
+							"recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0
 						})
 					sl_entries.append(sle)
 
@@ -618,7 +590,8 @@
 						or (cint(self.is_return) and self.docstatus==1)):
 						from_warehouse_sle = self.get_sl_entries(d, {
 							"actual_qty": -1 * pr_qty,
-							"warehouse": d.from_warehouse
+							"warehouse": d.from_warehouse,
+							"recalculate_rate": 1
 						})
 
 						sl_entries.append(from_warehouse_sle)
@@ -666,6 +639,7 @@
 					"item_code": d.rm_item_code,
 					"warehouse": self.supplier_warehouse,
 					"actual_qty": -1*flt(d.consumed_qty),
+					"dependant_sle_voucher_detail_no": d.reference_name
 				}))
 
 	def on_submit(self):
@@ -857,6 +831,7 @@
 		else:
 			validate_item_type(self, "is_purchase_item", "purchase")
 
+
 def get_items_from_bom(item_code, bom, exploded_item=1):
 	doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"