from collections import defaultdict

import frappe
from frappe import _, bold
from frappe.query_builder import Case, Order
from frappe.utils import flt, get_link_to_form

from erpnext.stock.serial_batch_bundle import get_serial_batch_list_from_item


class SubcontractingInwardController:
	def validate_subcontracting_inward(self):
		self.validate_inward_order()
		self.set_allow_zero_valuation_rate()
		self.validate_warehouse_()
		self.validate_serial_batch_for_return_or_delivery()
		self.validate_delivery()
		self.update_customer_provided_item_cost()

	def on_submit_subcontracting_inward(self):
		self.update_inward_order_item()
		self.update_inward_order_received_items()
		self.update_inward_order_secondary_items()
		self.create_stock_reservation_entries_for_inward()
		self.update_inward_order_status()

	def on_cancel_subcontracting_inward(self):
		self.update_inward_order_item()
		self.validate_manufacture_entry_cancel()
		self.validate_delivery()
		self.validate_receive_from_customer_cancel()
		self.update_inward_order_received_items()
		self.update_inward_order_secondary_items()
		self.remove_reference_for_additional_items()
		self.update_inward_order_status()

	def validate_purpose(self):
		if self.subcontracting_inward_order and self.purpose not in [
			"Receive from Customer",
			"Return Raw Material to Customer",
			"Manufacture",
			"Subcontracting Delivery",
			"Subcontracting Return",
			"Material Transfer for Manufacture",
		]:
			self.subcontracting_inward_order = None

	def validate_inward_order(self):
		if self.subcontracting_inward_order:
			match self.purpose:
				case "Receive from Customer":
					self.validate_material_receipt()
				case purpose if purpose in ["Return Raw Material to Customer", "Subcontracting Return"]:
					self.validate_returns()
				case "Material Transfer for Manufacture":
					self.validate_material_transfer()
				case "Manufacture":
					self.validate_manufacture()

	def validate_material_receipt(self):
		self.set_scio_detail_for_received_items()
		rm_item_fg_combo = []
		scio_details = []
		for item in self.items:
			if not frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item"):
				frappe.throw(
					_("Row #{0}: Item {1} is not a Customer Provided Item.").format(
						item.idx,
						get_link_to_form("Item", item.item_code),
					)
				)

			if item.scio_detail in scio_details:
				frappe.throw(
					_(
						"Row #{0}: Customer Provided Item {1} cannot be added multiple times in the Subcontracting Inward process."
					).format(item.idx, get_link_to_form("Item", item.item_code))
				)
			elif item.scio_detail:
				scio_details.append(item.scio_detail)

			if (
				item.scio_detail
				and frappe.get_cached_value(
					"Subcontracting Inward Order Received Item", item.scio_detail, "rm_item_code"
				)
				!= item.item_code
			):
				frappe.throw(
					_(
						"Row #{0}: Item {1} mismatch. Changing of item code is not permitted, add another row instead."
					).format(item.idx, get_link_to_form("Item", item.item_code))
				)

			if not item.scio_detail:  # item is additional
				if item.against_fg:
					if (item.item_code, item.against_fg) not in rm_item_fg_combo:
						rm_item_fg_combo.append((item.item_code, item.against_fg))
					else:
						frappe.throw(
							_(
								"Row #{0}: Customer Provided Item {1} against Subcontracting Inward Order Item {2} ({3}) cannot be added multiple times."
							).format(
								item.idx,
								get_link_to_form("Item", item.item_code),
								bold(item.against_fg),
								get_link_to_form(
									"Item",
									frappe.get_cached_value(
										"Subcontracting Inward Order Item", item.against_fg, "item_code"
									),
								),
							)
						)
				else:
					frappe.throw(
						_(
							"Row #{0}: Please select the Finished Good Item against which this Customer Provided Item will be used."
						).format(item.idx)
					)

	def set_scio_detail_for_received_items(self):
		"""Receive an extra row on the order's existing row for the same item and finished good."""
		items = [item for item in self.items if not item.scio_detail]
		if not items:
			return

		received_items = frappe.get_all(
			"Subcontracting Inward Order Received Item",
			filters={
				"parent": self.subcontracting_inward_order,
				"docstatus": 1,
				"reference_name": ["in", [item.against_fg for item in items]],
			},
			fields=["name", "rm_item_code", "reference_name"],
		)
		row_names = {(row.rm_item_code, row.reference_name): row.name for row in received_items}
		for item in items:
			item.scio_detail = row_names.get((item.item_code, item.against_fg))

	def validate_returns(self):
		for item in self.items:
			if not item.scio_detail:
				frappe.throw(
					_("Row #{0}: Item {1} is not a part of Subcontracting Inward Order {2}").format(
						item.idx,
						get_link_to_form("Item", item.item_code),
						get_link_to_form("Subcontracting Inward Order", self.subcontracting_inward_order),
					)
				)
			elif item.item_code != (
				frappe.get_cached_value(
					"Subcontracting Inward Order Received Item", item.scio_detail, "rm_item_code"
				)
				or frappe.get_cached_value("Subcontracting Inward Order Item", item.scio_detail, "item_code")
			):
				frappe.throw(
					_("Row #{0}: Item {1} mismatch. Changing of item code is not permitted.").format(
						item.idx, get_link_to_form("Item", item.item_code)
					)
				)

			precision = item.precision("transfer_qty")
			if self.purpose == "Return Raw Material to Customer":
				data = frappe.get_value(
					"Subcontracting Inward Order Received Item",
					item.scio_detail,
					["received_qty", "returned_qty", "work_order_qty"],
					as_dict=True,
				)
				if flt(data.returned_qty + item.transfer_qty, precision) > flt(
					data.received_qty - data.work_order_qty, precision
				):
					frappe.throw(
						_(
							"Row #{0}: Returned quantity cannot be greater than available quantity for Item {1}"
						).format(item.idx, get_link_to_form("Item", item.item_code))
					)
			else:
				data = frappe.get_value(
					"Subcontracting Inward Order Item",
					item.scio_detail,
					["returned_qty", "delivered_qty"],
					as_dict=True,
				)
				if flt(item.transfer_qty, precision) > flt(data.delivered_qty - data.returned_qty, precision):
					frappe.throw(
						_(
							"Row #{0}: Returned quantity cannot be greater than available quantity to return for Item {1}"
						).format(item.idx, get_link_to_form("Item", item.item_code))
					)

	def validate_material_transfer(self):
		customer_warehouse = frappe.get_cached_value(
			"Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse"
		)
		item_codes = []
		for item in self.items:
			if not frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item"):
				continue
			elif item.s_warehouse != customer_warehouse:
				frappe.throw(
					_("Row #{0}: For Customer Provided Item {1}, Source Warehouse must be {2}").format(
						item.idx,
						get_link_to_form("Item", item.item_code),
						get_link_to_form("Warehouse", customer_warehouse),
					)
				)
			elif item.item_code in item_codes:
				frappe.throw(
					_(
						"Row #{0}: Customer Provided Item {1} cannot be added multiple times in the Subcontracting Inward process."
					).format(
						item.idx,
						get_link_to_form("Item", item.item_code),
					)
				)
			else:
				work_order_items = frappe.get_all(
					"Work Order Item",
					{"parent": self.work_order, "docstatus": 1, "is_customer_provided_item": 1},
					["item_code", "transferred_qty", "required_qty", "stock_reserved_qty"],
				)
				wo_item_dict = frappe._dict(
					{
						wo_item.item_code: frappe._dict(
							{
								"transferred_qty": wo_item.transferred_qty,
								"required_qty": wo_item.required_qty,
								"stock_reserved_qty": wo_item.stock_reserved_qty,
							}
						)
						for wo_item in work_order_items
					}
				)
				if wo_item := wo_item_dict.get(item.item_code):
					if wo_item.transferred_qty + item.transfer_qty > max(
						wo_item.required_qty, wo_item.stock_reserved_qty
					):
						frappe.throw(
							_(
								"Row #{0}: Overconsumption of Customer Provided Item {1} against Work Order {2} is not allowed in the Subcontracting Inward process."
							).format(
								item.idx,
								get_link_to_form("Item", item.item_code),
								get_link_to_form("Work Order", self.work_order),
							)
						)
					else:
						item_codes.append(item.item_code)
				else:
					frappe.throw(
						_("Row #{0}: Customer Provided Item {1} is not a part of Work Order {2}").format(
							item.idx,
							get_link_to_form("Item", item.item_code),
							get_link_to_form("Work Order", self.work_order),
						)
					)

	def validate_manufacture(self):
		if next(item for item in self.items if item.is_finished_item).t_warehouse != (
			fg_warehouse := frappe.get_cached_value("Work Order", self.work_order, "fg_warehouse")
		):
			frappe.throw(
				_(
					"Target Warehouse for Finished Good must be same as Finished Good Warehouse {1} in Work Order {2} linked to the Subcontracting Inward Order."
				).format(
					get_link_to_form("Warehouse", fg_warehouse),
					get_link_to_form("Work Order", self.work_order),
				)
			)

		items = [
			item
			for item in self.get("items")
			if not item.is_finished_item
			and not item.secondary_item_type
			and not item.valuation_type
			and frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
		]

		customer_warehouse = frappe.get_cached_value(
			"Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse"
		)
		if frappe.get_cached_value("Work Order", self.work_order, "skip_transfer"):
			table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
			query = (
				frappe.qb.from_(table)
				.select(
					table.rm_item_code,
					(table.received_qty - table.returned_qty).as_("total_qty"),
					table.consumed_qty,
					table.name,
				)
				.where(
					(table.docstatus == 1)
					& (table.parent == self.subcontracting_inward_order)
					& (
						table.reference_name
						== frappe.get_cached_value(
							"Work Order", self.work_order, "subcontracting_inward_order_item"
						)
					)
					& (table.rm_item_code.isin([item.item_code for item in items]))
				)
			)
			rm_item_dict = frappe._dict(
				{
					d.rm_item_code: frappe._dict(
						{"name": d.name, "total_qty": d.total_qty, "qty": d.consumed_qty}
					)
					for d in query.run(as_dict=True)
				}
			)

			item_codes = []
			for item in items:
				if rm := rm_item_dict.get(item.item_code):
					if rm.qty + item.transfer_qty > rm.total_qty:
						frappe.throw(
							_(
								"Row #{0}: Customer Provided Item {1} exceeds quantity available through Subcontracting Inward Order"
							).format(item.idx, get_link_to_form("Item", item.item_code), item.transfer_qty)
						)
					elif item.s_warehouse != customer_warehouse:
						frappe.throw(
							_(
								"Row #{0}: For Customer Provided Item {1}, Source Warehouse must be {2}"
							).format(
								item.idx,
								get_link_to_form("Item", item.item_code),
								get_link_to_form("Warehouse", customer_warehouse),
							)
						)
					elif item.item_code in item_codes:
						frappe.throw(
							_(
								"Row #{0}: Customer Provided Item {1} cannot be added multiple times in the Subcontracting Inward process."
							).format(
								item.idx,
								get_link_to_form("Item", item.item_code),
							)
						)
					else:
						item_codes.append(item.item_code)
				else:
					frappe.throw(
						_(
							"Row #{0}: Customer Provided Item {1} is not a part of Subcontracting Inward Order {2}"
						).format(
							item.idx,
							get_link_to_form("Item", item.item_code),
							get_link_to_form("Subcontracting Inward Order", self.subcontracting_inward_order),
						)
					)
		else:
			work_order_items = frappe.get_all(
				"Work Order Item",
				{"parent": self.work_order, "docstatus": 1, "is_customer_provided_item": 1},
				["item_code", "transferred_qty", "consumed_qty"],
			)
			wo_item_dict = frappe._dict(
				{
					wo_item.item_code: frappe._dict(
						{"transferred_qty": wo_item.transferred_qty, "consumed_qty": wo_item.consumed_qty}
					)
					for wo_item in work_order_items
				}
			)
			item_codes = []
			for item in items:
				if wo_item := wo_item_dict.get(item.item_code):
					if wo_item.consumed_qty + item.transfer_qty > wo_item.transferred_qty:
						frappe.throw(
							_(
								"Row #{0}: Overconsumption of Customer Provided Item {1} against Work Order {2} is not allowed in the Subcontracting Inward process."
							).format(
								item.idx,
								get_link_to_form("Item", item.item_code),
								get_link_to_form("Work Order", self.work_order),
							)
						)
					elif item.item_code in item_codes:
						frappe.throw(
							_(
								"Row #{0}: Customer Provided Item {1} cannot be added multiple times in the Subcontracting Inward process."
							).format(
								item.idx,
								get_link_to_form("Item", item.item_code),
							)
						)
					else:
						item_codes.append(item.item_code)
				else:
					frappe.throw(
						_("Row #{0}: Customer Provided Item {1} is not a part of Work Order {2}").format(
							item.idx,
							get_link_to_form("Item", item.item_code),
							get_link_to_form("Work Order", self.work_order),
						)
					)

	def set_allow_zero_valuation_rate(self):
		if self.subcontracting_inward_order:
			if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]:
				for item in self.items:
					if (
						item.is_finished_item or item.secondary_item_type or item.valuation_type
					) and item.valuation_rate == 0:
						item.allow_zero_valuation_rate = 1

	def validate_warehouse_(self):
		if self.subcontracting_inward_order and self.purpose in [
			"Receive from Customer",
			"Return Raw Material to Customer",
			"Material Transfer for Manufacture",
		]:
			customer_warehouse = frappe.get_cached_value(
				"Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse"
			)
			for item in self.items:
				if self.purpose == "Material Transfer for Manufacture" and not frappe.get_cached_value(
					"Item", item.item_code, "is_customer_provided_item"
				):
					continue

				if (item.s_warehouse or item.t_warehouse) != customer_warehouse:
					if item.t_warehouse:
						frappe.throw(
							_(
								"Row #{0}: Target Warehouse must be same as Customer Warehouse {1} from the linked Subcontracting Inward Order"
							).format(item.idx, get_link_to_form("Warehouse", customer_warehouse))
						)
					else:
						frappe.throw(
							_(
								"Row #{0}: Source Warehouse must be same as Customer Warehouse {1} from the linked Subcontracting Inward Order"
							).format(item.idx, get_link_to_form("Warehouse", customer_warehouse))
						)

	def validate_serial_batch_for_return_or_delivery(self):
		if self.subcontracting_inward_order and self.purpose in [
			"Return Raw Material to Customer",
			"Subcontracting Delivery",
			"Subcontracting Return",
		]:
			for item in self.items:
				serial_nos, batch_nos = self.get_serial_nos_and_batches_from_sres(
					item.scio_detail, only_pending=self.purpose != "Subcontracting Return"
				)
				serial_list, batch_list = get_serial_batch_list_from_item(item)

				if serial_list and (
					incorrect_serial_nos := [sn for sn in serial_list if sn not in serial_nos]
				):
					frappe.throw(
						_(
							"Row #{0}: Serial No(s) {1} are not a part of the linked Subcontracting Inward Order. Please select valid Serial No(s)."
						).format(
							item.idx,
							", ".join([get_link_to_form("Serial No", sn) for sn in incorrect_serial_nos]),
						)
					)
				if batch_list and (
					incorrect_batch_nos := [bn for bn in batch_list if bn not in list(batch_nos.keys())]
				):
					frappe.throw(
						_(
							"Row #{0}: Batch No(s) {1} is not a part of the linked Subcontracting Inward Order. Please select valid Batch No(s)."
						).format(
							item.idx,
							", ".join([get_link_to_form("Batch No", bn) for bn in incorrect_batch_nos]),
						)
					)

	def get_serial_nos_and_batches_from_sres(self, scio_detail, only_pending=True):
		serial_nos, batch_nos = [], frappe._dict()

		table = frappe.qb.DocType("Stock Reservation Entry")
		child_table = frappe.qb.DocType("Serial and Batch Entry")
		query = (
			frappe.qb.from_(table)
			.join(child_table)
			.on(table.name == child_table.parent)
			.select(child_table.serial_no, child_table.batch_no, child_table.qty, child_table.delivered_qty)
			.where((table.docstatus == 1) & (table.voucher_detail_no == scio_detail))
		)

		if only_pending:
			query = query.where(child_table.qty != child_table.delivered_qty)
		else:
			query = query.where(child_table.delivered_qty > 0)

		for d in query.run(as_dict=True):
			if d.serial_no and d.serial_no not in serial_nos:
				serial_nos.append(d.serial_no)
			if d.batch_no:
				qty = d.qty - d.delivered_qty if only_pending else d.qty
				batch_nos[d.batch_no] = batch_nos.get(d.batch_no, 0) + qty

		return serial_nos, batch_nos

	def validate_delivery(self):
		if self.purpose == "Subcontracting Delivery":
			if self._action in ["save", "submit"]:
				self.validate_delivery_on_save()
			else:
				for item in self.items:
					if not item.secondary_item_type and not item.valuation_type:
						delivered_qty, returned_qty = frappe.get_value(
							"Subcontracting Inward Order Item",
							item.scio_detail,
							["delivered_qty", "returned_qty"],
						)
						if returned_qty > delivered_qty:
							frappe.throw(
								_(
									"Row #{0}: Cannot cancel this Stock Entry as returned quantity cannot be greater than delivered quantity for Item {1} in the linked Subcontracting Inward Order"
								).format(item.idx, get_link_to_form("Item", item.item_code))
							)

	def validate_delivery_on_save(self):
		allow_delivery_of_overproduced_qty = frappe.get_single_value(
			"Selling Settings", "allow_delivery_of_overproduced_qty"
		)

		for item in self.items:
			if not item.scio_detail:
				frappe.throw(
					_("Row #{0}: Item {1} is not a part of Subcontracting Inward Order {2}").format(
						item.idx,
						get_link_to_form("Item", item.item_code),
						get_link_to_form("Subcontracting Inward Order", self.subcontracting_inward_order),
					)
				)

			from pypika.terms import ValueWrapper

			table = frappe.qb.DocType("Subcontracting Inward Order Item")
			query = (
				frappe.qb.from_(table)
				.select(
					(
						Case()
						.when(
							(table.produced_qty < table.qty)
							| ValueWrapper(allow_delivery_of_overproduced_qty),
							table.produced_qty,
						)
						.else_(table.qty)
						- table.delivered_qty
					).as_("max_allowed_qty")
				)
				.where((table.name == item.scio_detail) & (table.docstatus == 1))
			)
			max_allowed_qty = query.run(pluck="max_allowed_qty")

			if max_allowed_qty:
				max_allowed_qty = max_allowed_qty[0]
			else:
				table = frappe.qb.DocType("Subcontracting Inward Order Secondary Item")
				query = (
					frappe.qb.from_(table)
					.select((table.produced_qty - table.delivered_qty).as_("max_allowed_qty"))
					.where((table.name == item.scio_detail) & (table.docstatus == 1))
				)
				max_allowed_qty = query.run(pluck="max_allowed_qty")[0]

			if item.transfer_qty > max_allowed_qty:
				frappe.throw(
					_(
						"Row #{0}: Quantity of Item {1} cannot be more than {2} {3} against Subcontracting Inward Order {4}"
					).format(
						item.idx,
						get_link_to_form("Item", item.item_code),
						bold(max_allowed_qty),
						bold(
							frappe.get_cached_value(
								"Subcontracting Inward Order Item"
								if not item.secondary_item_type and not item.valuation_type
								else "Subcontracting Inward Order Secondary Item",
								item.scio_detail,
								"stock_uom",
							)
						),
						get_link_to_form("Subcontracting Inward Order", self.subcontracting_inward_order),
					)
				)

	def update_customer_provided_item_cost(self):
		if self.purpose == "Receive from Customer":
			for item in self.items:
				item.valuation_rate = 0
				item.customer_provided_item_cost = flt(
					item.basic_rate + (item.additional_cost / item.transfer_qty), item.precision("basic_rate")
				)

	def validate_receive_from_customer_cancel(self):
		if self.purpose == "Receive from Customer":
			for item in self.items:
				scio_rm_item = frappe.get_value(
					"Subcontracting Inward Order Received Item",
					item.scio_detail,
					["received_qty", "returned_qty", "work_order_qty"],
					as_dict=True,
				)
				if (
					scio_rm_item.received_qty - scio_rm_item.returned_qty - item.transfer_qty
				) < scio_rm_item.work_order_qty:
					frappe.throw(
						_("Row #{0}: Work Order exists against full or partial quantity of Item {1}").format(
							item.idx, get_link_to_form("Item", item.item_code)
						)
					)

	def validate_manufacture_entry_cancel(self):
		if self.subcontracting_inward_order and self.purpose == "Manufacture":
			fg_item_name = frappe.get_cached_value(
				"Work Order", self.work_order, "subcontracting_inward_order_item"
			)
			produced_qty, delivered_qty = frappe.get_value(
				"Subcontracting Inward Order Item", fg_item_name, ["produced_qty", "delivered_qty"]
			)
			if produced_qty < delivered_qty:
				frappe.throw(
					_(
						"Cannot cancel this Manufacturing Stock Entry as quantity of Finished Good produced cannot be less than quantity delivered in the linked Subcontracting Inward Order."
					)
				)

			for item in [item for item in self.items if not item.is_finished_item]:
				if item.secondary_item_type or item.valuation_type:
					scio_secondary_item = frappe.get_value(
						"Subcontracting Inward Order Secondary Item",
						{
							"docstatus": 1,
							"item_code": item.item_code,
							"warehouse": item.t_warehouse,
							"reference_name": fg_item_name,
						},
						["produced_qty", "delivered_qty"],
						as_dict=True,
					)
					if (
						scio_secondary_item
						and scio_secondary_item.delivered_qty
						> scio_secondary_item.produced_qty - item.transfer_qty
					):
						frappe.throw(
							_(
								"Row #{0}: Cannot cancel this Manufacturing Stock Entry as quantity of Secondary Item {1} produced cannot be less than quantity delivered."
							).format(item.idx, get_link_to_form("Item", item.item_code))
						)
				else:
					scio_rm_item = frappe.get_value(
						"Subcontracting Inward Order Received Item",
						{
							"docstatus": 1,
							"rm_item_code": item.item_code,
							"warehouse": item.s_warehouse,
							"is_customer_provided_item": 0,
							"is_additional_item": 1,
						},
						["consumed_qty", "billed_qty", "returned_qty"],
						as_dict=True,
					)
					if scio_rm_item and (scio_rm_item.billed_qty - scio_rm_item.returned_qty) > (
						scio_rm_item.consumed_qty - item.transfer_qty
					):
						frappe.throw(
							_(
								"Row #{0}: Cannot cancel this Manufacturing Stock Entry as billed quantity of Item {1} cannot be greater than consumed quantity."
							).format(item.idx, get_link_to_form("Item", item.item_code))
						)

	def update_inward_order_item(self):
		if self.purpose == "Manufacture" and (
			scio_item_name := frappe.get_cached_value(
				"Work Order", self.work_order, "subcontracting_inward_order_item"
			)
		):
			if scio_item_name:
				frappe.get_doc(
					"Subcontracting Inward Order Item", scio_item_name
				).update_manufacturing_qty_fields()
		elif self.purpose in ["Subcontracting Delivery", "Subcontracting Return"]:
			fieldname = "delivered_qty" if self.purpose == "Subcontracting Delivery" else "returned_qty"
			qty_map = defaultdict(lambda: defaultdict(float))
			for item in self.items:
				doctype = (
					"Subcontracting Inward Order Item"
					if not item.secondary_item_type and not item.valuation_type
					else "Subcontracting Inward Order Secondary Item"
				)
				qty_map[doctype][item.scio_detail] += (
					item.transfer_qty if self._action == "submit" else -item.transfer_qty
				)

			for doctype, item_qty_map in qty_map.items():
				table = frappe.qb.DocType(doctype)
				field = table[fieldname]
				doc_updates = {
					scio_detail: {fieldname: field + qty} for scio_detail, qty in item_qty_map.items()
				}
				frappe.db.bulk_update(doctype, doc_updates, chunk_size=len(doc_updates))

	def update_inward_order_received_items(self):
		if self.subcontracting_inward_order:
			match self.purpose:
				case "Receive from Customer":
					self.update_inward_order_received_items_for_raw_materials_receipt()
				case "Manufacture":
					self.update_inward_order_received_items_for_manufacture()
				case "Return Raw Material to Customer":
					scio_rm_names = {
						item.scio_detail: item.transfer_qty
						if self._action == "submit"
						else -item.transfer_qty
						for item in self.items
					}
					table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
					doc_updates = {
						scio_rm_name: {"returned_qty": table.returned_qty + qty}
						for scio_rm_name, qty in scio_rm_names.items()
					}
					if doc_updates:
						frappe.db.bulk_update(
							"Subcontracting Inward Order Received Item",
							doc_updates,
							chunk_size=len(doc_updates),
							update_modified=False,
						)

	def update_inward_order_received_items_for_raw_materials_receipt(self):
		data = frappe._dict()
		for item in self.items:
			if item.scio_detail:
				data[item.scio_detail] = frappe._dict(
					{"transfer_qty": item.transfer_qty, "rate": item.customer_provided_item_cost}
				)
			else:
				scio_rm = frappe.new_doc(
					"Subcontracting Inward Order Received Item",
					parent=self.subcontracting_inward_order,
					parenttype="Subcontracting Inward Order",
					parentfield="received_items",
					idx=frappe.db.count(
						"Subcontracting Inward Order Received Item",
						{"parent": self.subcontracting_inward_order},
					)
					+ 1,
					rm_item_code=item.item_code,
					stock_uom=item.stock_uom,
					warehouse=item.t_warehouse,
					received_qty=item.transfer_qty,
					consumed_qty=0,
					work_order_qty=0,
					returned_qty=0,
					rate=item.customer_provided_item_cost,
					is_customer_provided_item=True,
					is_additional_item=True,
					reference_name=item.against_fg,
					main_item_code=frappe.get_cached_value(
						"Subcontracting Inward Order Item", item.against_fg, "item_code"
					),
				)
				scio_rm.insert(ignore_permissions=True)
				scio_rm.submit()
				item.db_set("scio_detail", scio_rm.name)

		if data:
			precision = self.precision("customer_provided_item_cost", "items")
			result = frappe.get_all(
				"Subcontracting Inward Order Received Item",
				filters={
					"parent": self.subcontracting_inward_order,
					"name": ["in", list(data.keys())],
					"docstatus": 1,
				},
				fields=[
					"rate",
					"name",
					"required_qty",
					"received_qty",
					"returned_qty",
					"consumed_qty",
				],
			)

			doc_updates = {}
			for d in result:
				current_qty = flt(data[d.name].transfer_qty) * (1 if self._action == "submit" else -1)
				current_rate = flt(data[d.name].rate)

				# Weighted average rate must be computed on the on-hand balance
				balance_qty = d.received_qty - d.returned_qty - d.consumed_qty
				old_total = d.rate * balance_qty
				current_total = current_rate * current_qty

				new_balance_qty = balance_qty + current_qty
				d.received_qty = d.received_qty + current_qty
				d.rate = (
					flt((old_total + current_total) / new_balance_qty, precision)
					if new_balance_qty > 0
					else 0.0
				)

				if not d.required_qty and not d.received_qty:
					frappe.delete_doc(
						"Subcontracting Inward Order Received Item", d.name, ignore_permissions=True
					)
				else:
					doc_updates[d.name] = {"received_qty": d.received_qty, "rate": d.rate}

			if doc_updates:
				frappe.db.bulk_update(
					"Subcontracting Inward Order Received Item",
					doc_updates,
					chunk_size=len(doc_updates),
					update_modified=False,
				)

	def update_inward_order_received_items_for_manufacture(self):
		customer_warehouse = frappe.get_cached_value(
			"Subcontracting Inward Order", self.subcontracting_inward_order, "customer_warehouse"
		)
		items = [
			item
			for item in self.items
			if not item.is_finished_item and not item.secondary_item_type and not item.valuation_type
		]
		item_code_wh = frappe._dict(
			{
				(
					item.item_code,
					customer_warehouse
					if frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
					else item.s_warehouse,
				): item.transfer_qty if self._action == "submit" else -item.transfer_qty
				for item in items
			}
		)
		item_codes, warehouses = zip(*list(item_code_wh.keys()), strict=True)

		table = frappe.qb.DocType("Subcontracting Inward Order Received Item")
		data = (
			frappe.qb.from_(table)
			.select(
				table.name,
				table.rm_item_code,
				table.is_customer_provided_item,
				table.consumed_qty,
				table.warehouse,
				table.is_additional_item,
			)
			.where(
				(table.docstatus == 1)
				& (table.rm_item_code.isin(list(set(item_codes))))
				& (
					(table.warehouse.isin(list(set(warehouses)))) | (table.warehouse.isnull())
				)  # warehouse will always be null for non additional self procured raw materials
				& (table.parent == self.subcontracting_inward_order)
				& (
					table.reference_name
					== frappe.get_cached_value(
						"Work Order", self.work_order, "subcontracting_inward_order_item"
					)
				)
			)
		)

		if data := data.run(as_dict=True):
			used_item_wh = []
			doc_updates = {}
			for d in data:
				if not d.warehouse:
					d.warehouse = next(
						key[1]
						for key in item_code_wh.keys()
						if key[0] == d.rm_item_code and key not in used_item_wh
					)
					used_item_wh.append((d.rm_item_code, d.warehouse))

				qty = d.consumed_qty + item_code_wh[(d.rm_item_code, d.warehouse)]
				if qty or d.is_customer_provided_item or not d.is_additional_item:
					doc_updates[d.name] = {"consumed_qty": qty}
				else:
					frappe.delete_doc(
						"Subcontracting Inward Order Received Item", d.name, ignore_permissions=True
					)

			if doc_updates:
				frappe.db.bulk_update(
					"Subcontracting Inward Order Received Item",
					doc_updates,
					chunk_size=len(doc_updates),
					update_modified=False,
				)

			main_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code
			for extra_item in [
				item
				for item in items
				if not frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
				and (item.item_code, item.s_warehouse)
				not in [(d.rm_item_code, d.warehouse) for d in data if not d.is_customer_provided_item]
			]:
				doc = frappe.new_doc(
					"Subcontracting Inward Order Received Item",
					parent=self.subcontracting_inward_order,
					parenttype="Subcontracting Inward Order",
					parentfield="received_items",
					idx=frappe.db.count(
						"Subcontracting Inward Order Received Item",
						{"parent": self.subcontracting_inward_order},
					)
					+ 1,
					main_item_code=main_item_code,
					rm_item_code=extra_item.item_code,
					stock_uom=extra_item.stock_uom,
					reference_name=frappe.get_cached_value(
						"Work Order", self.work_order, "subcontracting_inward_order_item"
					),
					required_qty=0,
					consumed_qty=extra_item.transfer_qty,
					warehouse=extra_item.s_warehouse,
					is_additional_item=True,
				)
				doc.insert(ignore_permissions=True)
				doc.submit()

	def update_inward_order_secondary_items(self):
		if (scio := self.subcontracting_inward_order) and self.purpose == "Manufacture":
			secondary_items_list = [
				item for item in self.items if item.secondary_item_type or item.valuation_type
			]

			secondary_items = defaultdict(float)
			for item in secondary_items_list:
				secondary_items[(item.item_code, item.t_warehouse)] += (
					item.transfer_qty if self._action == "submit" else -item.transfer_qty
				)
			secondary_items = frappe._dict(secondary_items)

			if secondary_items:
				item_codes, warehouses = zip(*list(secondary_items.keys()), strict=True)
				item_codes = list(item_codes)
				warehouses = list(warehouses)

				result = frappe.get_all(
					"Subcontracting Inward Order Secondary Item",
					filters={
						"item_code": ["in", item_codes],
						"warehouse": ["in", warehouses],
						"reference_name": frappe.get_cached_value(
							"Work Order", self.work_order, "subcontracting_inward_order_item"
						),
						"docstatus": 1,
					},
					fields=["name", "item_code", "warehouse", "produced_qty"],
				)

				if result:
					secondary_items_dict = frappe._dict(
						{
							(d.item_code, d.warehouse): frappe._dict(
								{"name": d.name, "produced_qty": d.produced_qty}
							)
							for d in result
						}
					)
					doc_updates = {}
					for key, value in secondary_items_dict.items():
						if (
							self._action == "cancel"
							and value.produced_qty - abs(secondary_items.get(key)) == 0
						):
							frappe.delete_doc(
								"Subcontracting Inward Order Secondary Item",
								value.name,
								ignore_permissions=True,
							)
						else:
							doc_updates[value.name] = {
								"produced_qty": value.produced_qty + secondary_items.get(key)
							}

					if doc_updates:
						frappe.db.bulk_update(
							"Subcontracting Inward Order Secondary Item",
							doc_updates,
							chunk_size=len(doc_updates),
							update_modified=False,
						)

				fg_item_code = next(fg for fg in self.items if fg.is_finished_item).item_code
				for secondary_item in [
					item
					for item in secondary_items_list
					if (item.item_code, item.t_warehouse) not in [(d.item_code, d.warehouse) for d in result]
				]:
					doc = frappe.new_doc(
						"Subcontracting Inward Order Secondary Item",
						parent=scio,
						parenttype="Subcontracting Inward Order",
						parentfield="secondary_items",
						idx=frappe.db.count("Subcontracting Inward Order Secondary Item", {"parent": scio})
						+ 1,
						item_code=secondary_item.item_code,
						fg_item_code=fg_item_code,
						stock_uom=secondary_item.stock_uom,
						warehouse=secondary_item.t_warehouse,
						produced_qty=secondary_item.transfer_qty,
						secondary_item_type=secondary_item.secondary_item_type,
						delivered_qty=0,
						reference_name=frappe.get_value(
							"Work Order", self.work_order, "subcontracting_inward_order_item"
						),
					)
					doc.insert(ignore_permissions=True)
					doc.submit()

	def cancel_stock_reservation_entries_for_inward(self):
		if self.purpose == "Receive from Customer":
			self.adjust_inward_reservations(release=True)

	def remove_reference_for_additional_items(self):
		if self.subcontracting_inward_order:
			items = [
				item
				for item in self.items
				if item.scio_detail
				and (
					not frappe.db.exists("Subcontracting Inward Order Received Item", item.scio_detail)
					and not frappe.db.exists("Subcontracting Inward Order Item", item.scio_detail)
					and not frappe.db.exists("Subcontracting Inward Order Secondary Item", item.scio_detail)
				)
			]
			for item in items:
				item.db_set("scio_detail", None)

	def create_stock_reservation_entries_for_inward(self):
		if self.purpose == "Receive from Customer":
			for item in self.items:
				item.reload()
				sre = frappe.new_doc("Stock Reservation Entry")
				sre.company = self.company
				sre.voucher_type = "Subcontracting Inward Order"
				sre.voucher_qty = sre.reserved_qty = sre.available_qty = item.transfer_qty
				sre.voucher_no = self.subcontracting_inward_order
				sre.voucher_detail_no = item.scio_detail
				sre.item_code = item.item_code
				sre.stock_uom = item.stock_uom
				sre.warehouse = item.t_warehouse or item.s_warehouse
				sre.has_serial_no = frappe.get_cached_value("Item", item.item_code, "has_serial_no")
				sre.has_batch_no = frappe.get_cached_value("Item", item.item_code, "has_batch_no")
				sre.reservation_based_on = "Qty" if not item.serial_and_batch_bundle else "Serial and Batch"
				if item.serial_and_batch_bundle:
					sabb = frappe.get_doc("Serial and Batch Bundle", item.serial_and_batch_bundle)
					for entry in sabb.entries:
						sre.append(
							"sb_entries",
							{
								"serial_no": entry.serial_no,
								"batch_no": entry.batch_no,
								"qty": entry.qty,
								"warehouse": entry.warehouse,
							},
						)
				sre.submit()
			frappe.msgprint(_("Stock Reservation Entries Created"), alert=True, indicator="green")

	def validate_no_later_work_order_reservation(self):
		"""Cancel gives batch qty back newest reservation first, which is wrong once a later Work Order took from them."""
		rows = defaultdict(list)
		for item in self.items:
			if not item.scio_detail or frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
				continue
			if batches := get_serial_batch_list_from_item(item)[1]:
				rows[item.scio_detail].append((item, batches))
		if not rows:
			return

		for reservation in self.get_later_work_order_reservations(list(rows)):
			for item, batches in rows[reservation.from_voucher_detail_no]:
				if reservation.batch_no and reservation.batch_no not in batches:
					continue

				frappe.throw(
					_(
						"Row #{0}: Work Order {1} reserved Item {2} after this return. Cancel the Work Order first."
					).format(
						item.idx,
						get_link_to_form("Work Order", reservation.voucher_no),
						bold(item.item_code),
					)
				)

	def get_later_work_order_reservations(self, scio_details: list[str]) -> list:
		"""Work Order reservations taken from these inward order rows after this entry was submitted; a cancelled one counts for the batches it still holds."""
		submitted_on = frappe.get_all(
			"Stock Ledger Entry",
			filters={"voucher_type": self.doctype, "voucher_no": self.name},
			pluck="creation",
			order_by="creation asc",
			limit=1,
		)
		reservation = frappe.qb.DocType("Stock Reservation Entry")
		entry = frappe.qb.DocType("Serial and Batch Entry")
		return (
			frappe.qb.from_(reservation)
			.left_join(entry)
			.on((entry.parent == reservation.name) & (entry.parenttype == "Stock Reservation Entry"))
			.select(reservation.voucher_no, reservation.from_voucher_detail_no, entry.batch_no)
			.where(
				(
					(reservation.docstatus == 1)
					| (
						(reservation.docstatus == 2)
						& (
							reservation.delivered_qty + reservation.transferred_qty + reservation.consumed_qty
							> 0
						)
					)
				)
				& ((reservation.docstatus == 1) | (entry.delivered_qty > 0))
				& (reservation.voucher_type == "Work Order")
				& reservation.from_voucher_detail_no.isin(scio_details)
				& (reservation.creation > submitted_on[0])
			)
		).run(as_dict=True)

	def adjust_stock_reservation_entries_for_return(self):
		if self.purpose == "Return Raw Material to Customer":
			if self._action == "cancel":
				self.validate_no_later_work_order_reservation()
			self.adjust_inward_reservations(release=self._action == "submit")

	def adjust_inward_reservations(self, release):
		for item in self.items:
			serial_list, batch_list = get_serial_batch_list_from_item(item)

			if serial_list or batch_list:
				table = frappe.qb.DocType("Stock Reservation Entry")
				child_table = frappe.qb.DocType("Serial and Batch Entry")
				query = (
					frappe.qb.from_(table)
					.join(child_table)
					.on(table.name == child_table.parent)
					.select(
						table.name.as_("sre_name"),
						child_table.name.as_("sbe_name"),
						child_table.batch_no,
						child_table.qty,
						child_table.delivered_qty,
					)
					.where((table.docstatus == 1) & (table.voucher_detail_no == item.scio_detail))
					.orderby(table.creation, order=Order.asc if release else Order.desc)
				)
				if serial_list:
					query = query.where(child_table.serial_no.isin(serial_list))
				if batch_list:
					query = query.where(child_table.batch_no.isin(batch_list))
				result = query.run(as_dict=True)

				qty_to_deliver = {row.sre_name: 0 for row in result}
				consumed_qty = {batch: 0 for batch in batch_list}
				for row in result:
					if serial_list:
						frappe.get_doc("Serial and Batch Entry", row.sbe_name).db_set(
							"delivered_qty", 1 if release else 0
						)
						qty_to_deliver[row.sre_name] += row.qty
					elif batch_list and not serial_list:
						sabe_qty = abs(
							frappe.get_value(
								"Serial and Batch Entry",
								{"parent": item.serial_and_batch_bundle, "batch_no": row.batch_no},
								"qty",
							)
						)

						open_qty = row.qty - row.delivered_qty if release else row.delivered_qty
						qty = min(open_qty, sabe_qty - consumed_qty[row.batch_no])
						sbe_doc = frappe.get_doc("Serial and Batch Entry", row.sbe_name)
						sbe_doc.db_set(
							"delivered_qty",
							sbe_doc.delivered_qty + (qty if release else -qty),
						)
						qty_to_deliver[row.sre_name] += qty
						consumed_qty[row.batch_no] += qty

				for sre_name, qty in qty_to_deliver.items():
					self.update_inward_reserved_qty(sre_name, -qty if release else qty)
			else:
				table = frappe.qb.DocType("Stock Reservation Entry")
				open_qty = (
					table.reserved_qty - table.delivered_qty
					if release
					else table.voucher_qty - table.reserved_qty
				)
				sre_list = (
					frappe.qb.from_(table)
					.select(table.name, open_qty.as_("qty"))
					.where(
						(table.docstatus == 1)
						& (table.voucher_detail_no == item.scio_detail)
						& (open_qty > 0)
					)
					.orderby(table.creation)
				).run(as_dict=True)

				voucher_qty = item.transfer_qty
				for sre in sre_list:
					qty = min(sre.qty, voucher_qty)
					self.update_inward_reserved_qty(sre.name, -qty if release else qty)
					voucher_qty -= qty
					if voucher_qty <= 0:
						break

	def update_inward_reserved_qty(self, sre_name, qty_change):
		"""Change reserved qty, not delivered qty, which Work Order reservations own."""
		sre_doc = frappe.get_doc("Stock Reservation Entry", sre_name)
		sre_doc.db_set("reserved_qty", sre_doc.reserved_qty + qty_change)
		sre_doc.update_status()
		sre_doc.update_reserved_stock_in_bin()

	def update_inward_order_status(self):
		if self.subcontracting_inward_order:
			from erpnext.subcontracting.doctype.subcontracting_inward_order.subcontracting_inward_order import (
				set_subcontracting_inward_order_status,
			)

			set_subcontracting_inward_order_status(self.subcontracting_inward_order)


@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_fg_reference_names(doctype, txt, searchfield, start, page_len, filters):
	return frappe.get_all(
		"Subcontracting Inward Order Item",
		limit_start=start,
		limit_page_length=page_len,
		filters={"parent": filters.get("parent"), "docstatus": 1},
		or_filters=[
			["name", "like", "%%%s%%" % txt],
			["item_code", "like", "%%%s%%" % txt],
		],
		fields=["name", "item_code", "delivery_warehouse"],
		as_list=True,
		order_by="idx",
	)
