# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt


import json
from collections import defaultdict
from contextlib import contextmanager

import frappe
from frappe import _, _dict, bold, qb, throw
from frappe.automation.doctype.auto_repeat.auto_repeat import month_map
from frappe.contacts.doctype.address.address import get_address_display
from frappe.model.document import Document
from frappe.model.workflow import get_workflow_name
from frappe.query_builder import Criterion, DocType
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Abs, IfNull, Sum
from frappe.utils import (
	add_days,
	add_months,
	cint,
	comma_and,
	cstr,
	date_diff,
	flt,
	fmt_money,
	formatdate,
	get_last_day,
	get_link_to_form,
	getdate,
	parse_json,
	today,
)

import erpnext
from erpnext import _refuse, require_permission
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
	get_accounting_dimensions,
	get_dimensions,
)
from erpnext.accounts.doctype.pricing_rule.utils import (
	apply_pricing_rule_for_free_items,
	apply_pricing_rule_on_transaction,
	get_applied_pricing_rules,
)
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
from erpnext.accounts.party import (
	PURCHASE_TRANSACTION_TYPES,
	SALES_TRANSACTION_TYPES,
	get_party_account,
	get_party_account_currency,
	get_party_gle_currency,
	validate_party_frozen_disabled,
)
from erpnext.accounts.utils import (
	create_gain_loss_journal,
	get_account_currency,
	get_currency_precision,
	get_fiscal_year,
	get_fiscal_years,
	validate_fiscal_year,
)
from erpnext.accounts.utils import get_advance_payment_doctypes as _get_advance_payment_doctypes
from erpnext.buying.utils import update_last_purchase_rate
from erpnext.controllers.print_settings import (
	set_print_templates_for_item_table,
	set_print_templates_for_taxes,
)
from erpnext.controllers.sales_and_purchase_return import validate_return
from erpnext.exceptions import InvalidCurrency
from erpnext.selling.doctype.party_specific_item.party_specific_item import get_restricted_items_condition
from erpnext.setup.utils import get_exchange_rate
from erpnext.stock.doctype.item.item import get_uom_conv_factor
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
from erpnext.stock.get_item_details import (
	NOT_APPLICABLE_TAX,
	ItemDetailsCtx,
	_get_item_details,
	_get_item_tax_template,
	_get_item_tax_template_from_item_group,
	get_bin_details,
	get_conversion_factor,
	get_item_tax_map,
	get_item_warehouse_,
)
from erpnext.stock.utils import (
	is_group_warehouse,
	validate_disabled_warehouse,
	validate_warehouse_company,
)
from erpnext.utilities.regional import temporary_flag
from erpnext.utilities.transaction_base import TransactionBase


class AccountMissingError(frappe.ValidationError):
	pass


class InvalidQtyError(frappe.ValidationError):
	pass


force_item_fields = (
	"item_group",
	"brand",
	"stock_uom",
	"is_fixed_asset",
	"pricing_rules",
	"weight_per_unit",
	"weight_uom",
	"total_weight",
	"valuation_rate",
)


COUNTERPART_REFERENCE = {
	"Sales Invoice": ("inter_company_invoice_reference", "Purchase Invoice"),
	"Purchase Invoice": ("inter_company_invoice_reference", "Sales Invoice"),
	"Sales Order": ("inter_company_order_reference", "Purchase Order"),
	"Purchase Order": ("inter_company_order_reference", "Sales Order"),
	"Delivery Note": ("inter_company_reference", "Purchase Receipt"),
	"Purchase Receipt": ("inter_company_reference", "Delivery Note"),
}


ADDITIONAL_DISCOUNT_FIELDS = ("apply_discount_on", "additional_discount_percentage", "discount_amount")


class AccountsController(TransactionBase):
	def get_print_settings(self):
		print_setting_fields = []
		items_field = self.meta.get_field("items")

		if items_field and items_field.fieldtype == "Table":
			print_setting_fields += ["compact_item_print", "print_uom_after_quantity"]

		taxes_field = self.meta.get_field("taxes")
		if taxes_field and taxes_field.fieldtype == "Table":
			print_setting_fields += ["print_taxes_with_zero_amount"]

		return print_setting_fields

	@property
	def company_currency(self):
		if not hasattr(self, "__company_currency"):
			self.__company_currency = erpnext.get_company_currency(self.company)

		return self.__company_currency

	def onload(self):
		self.set_onload(
			"make_payment_via_journal_entry",
			frappe.client_cache.get_doc("Accounts Settings").make_payment_via_journal_entry,
		)

		if self.is_new():
			relevant_docs = (
				"Quotation",
				"Purchase Order",
				"Sales Order",
				"Purchase Invoice",
				"Sales Invoice",
			)
			if self.doctype in relevant_docs:
				self.set_payment_schedule()

	def before_insert(self):
		self.clear_clearance_date_on_amend()

	def clear_clearance_date_on_amend(self):
		"""Drop the bank reconciliation clearance date copied over while amending.

		The framework copies `no_copy` fields when amending, so a reconciled
		voucher would carry a stale clearance date into its amendment even though
		the linked bank transaction gets unreconciled on cancellation.
		"""
		if not self.get("amended_from"):
			return

		if self.meta.has_field("clearance_date"):
			self.clearance_date = None

		for payment in self.get("payments") or []:
			if payment.meta.has_field("clearance_date"):
				payment.clearance_date = None

	def on_update(self):
		from erpnext.controllers.taxes_and_totals import process_item_wise_tax_details

		process_item_wise_tax_details(self)

	def remove_bundle_for_non_stock_invoices(self):
		has_sabb = False
		if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.update_stock:
			for item in self.get("items"):
				if item.serial_and_batch_bundle:
					item.serial_and_batch_bundle = None
					has_sabb = True

		if has_sabb:
			self.remove_serial_and_batch_bundle()

	def ensure_supplier_is_not_blocked(self):
		if self.meta.has_field("is_return") and self.is_return:
			return

		hold_type, suppliers = self.get_supplier_hold_scope()
		for supplier in {supplier for supplier in suppliers if supplier}:
			if frappe.get_lazy_doc("Supplier", supplier).is_blocked_for(hold_type):
				frappe.msgprint(
					_("{0} is blocked so this transaction cannot proceed").format(supplier),
					raise_exception=1,
				)

	def get_supplier_hold_scope(self) -> tuple[str | None, list[str]]:
		"""Return the Hold Type that blocks this document and the suppliers it applies to."""
		if self.doctype in ("Purchase Order", "Purchase Invoice"):
			return "Invoices", [self.supplier]
		if self.doctype in ("Purchase Receipt", "Supplier Quotation"):
			return "All", [self.supplier]
		if self.doctype == "Request for Quotation":
			return "All", [row.supplier for row in self.suppliers]
		if self.doctype == "Payment Entry" and self.party_type == "Supplier":
			return "Payments", [self.party]
		if self.doctype == "Journal Entry" and any(
			flt(row.credit_in_account_currency) - flt(row.debit_in_account_currency) > 0
			and frappe.get_cached_value("Account", row.account, "account_type") in ("Bank", "Cash")
			for row in self.accounts
		):
			return "Payments", [
				row.party
				for row in self.accounts
				if row.party_type == "Supplier"
				and flt(row.debit_in_account_currency) - flt(row.credit_in_account_currency) > 0
			]
		return None, []

	def validate_against_voucher_outstanding(self):
		from frappe.model.meta import get_meta

		if not get_meta(self.doctype).has_field("outstanding_amount"):
			return

		if self.get("is_return") and self.return_against and not (self.get("is_pos") or self.get("is_paid")):
			against_voucher_outstanding = frappe.get_value(
				self.doctype, self.return_against, "outstanding_amount"
			)
			document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"

			msg = ""
			if self.get("update_outstanding_for_self"):
				msg = _(
					"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck the '{2}' checkbox."
				).format(
					frappe.bold(document_type),
					get_link_to_form(self.doctype, self.get("return_against")),
					frappe.bold(_("Update Outstanding for Self")),
				)

			elif not self.update_outstanding_for_self and (
				abs(flt(self.rounded_total) or flt(self.grand_total)) > flt(against_voucher_outstanding)
			):
				self.update_outstanding_for_self = 1
				msg = _(
					"The outstanding amount {0} in {1} is lesser than {2}. Updating the outstanding to this invoice."
				).format(
					against_voucher_outstanding,
					get_link_to_form(self.doctype, self.get("return_against")),
					flt(abs(self.outstanding_amount)),
				)

			if msg:
				msg += "<br><br>" + _("You can use {0} to reconcile against {1} later.").format(
					get_link_to_form("Payment Reconciliation"),
					get_link_to_form(self.doctype, self.get("return_against")),
				)
				frappe.msgprint(msg)

	def is_negative_grand_total_allowed(self) -> bool:
		"""Return True if this document may save with a negative grand total.

		Sales Order and Purchase Order never post to the GL, so a negative
		total is safe there whenever the user has explicitly opted into
		negative rates via Selling/Buying Settings. Every other
		AccountsController doctype (invoices, delivery notes, receipts,
		quotations, ...) keeps relying on the `is_return` escape hatch only.
		"""
		if self.doctype == "Sales Order":
			return bool(frappe.get_single_value("Selling Settings", "allow_negative_rates_for_items"))

		if self.doctype == "Purchase Order":
			return bool(frappe.get_single_value("Buying Settings", "allow_negative_rates_for_items"))

		return False

	def validate(self):
		self.sync_mapped_discount_with_header()

		if not self.get("is_return") and not self.get("is_debit_note"):
			self.validate_qty_is_not_zero()

		if (
			self.doctype in ["Sales Invoice", "Purchase Invoice", "POS Invoice"]
			and self.get("is_return")
			and self.get("update_stock")
		):
			self.validate_zero_qty_for_return_invoices_with_stock()

		if self.get("_action") and self._action != "update_after_submit":
			self.set_missing_values(for_validate=True)

		self.validate_price_list()

		if self.get("_action") == "submit":
			self.remove_bundle_for_non_stock_invoices()

		self.ensure_supplier_is_not_blocked()

		self.validate_date_with_fiscal_year()
		self.validate_party_accounts()
		if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
			if self.is_return:
				self.validate_qty()
			else:
				self.clear_stale_deferred_fields()
				self.validate_deferred_start_and_end_date()

		self.validate_inter_company_reference()
		self.validate_inter_company_currency()
		# validate inter  company transaction rate
		self.validate_internal_transaction()

		self.disable_pricing_rule_on_internal_transfer()
		self.disable_tax_included_prices_for_internal_transfer()
		self.set_incoming_rate()
		self.init_internal_values()
		self.validate_against_voucher_outstanding()

		# Need to set taxes based on taxes_and_charges template
		# before calculating taxes and totals
		if self.meta.get_field("taxes_and_charges"):
			self.validate_enabled_taxes_and_charges()
			self.validate_tax_account_company()

		self.set_taxes_and_charges()

		if self.meta.get_field("currency"):
			self.calculate_taxes_and_totals()

			if not self.meta.get_field("is_return") or not self.is_return:
				if not self.is_negative_grand_total_allowed():
					self.validate_value("base_grand_total", ">=", 0)

			validate_return(self)

		self.validate_all_documents_schedule()

		self.validate_party()
		self.validate_party_specific_items()
		self.validate_currency()
		self.validate_party_account_currency()
		self.validate_return_against_account()

		if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
			if invalid_advances := [x for x in self.advances if not x.reference_type or not x.reference_name]:
				frappe.throw(
					_(
						"Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry."
					).format(
						frappe.bold(comma_and([x.idx for x in invalid_advances])),
						frappe.bold(_("Advance Payments")),
					)
				)

			pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
			if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
				self.set_advances()

			self.set_advance_gain_or_loss()

			self.validate_deferred_income_expense_account()
			self.set_inter_company_account()

		if self.doctype == "Purchase Invoice":
			self.calculate_paid_amount()

		with temporary_flag("company", self.company):
			validate_regional(self)
			validate_einvoice_fields(self)

		if self.doctype != "Material Request" and not self.ignore_pricing_rule:
			apply_pricing_rule_on_transaction(self)

		self.set_total_in_words()
		self.set_default_letter_head()
		self.validate_company_in_accounting_dimension()
		self.validate_party_address_and_contact()
		self.validate_company_linked_addresses()

	def validate_company_linked_addresses(self):
		address_fields = []
		sales_doctypes = ("Quotation", "Sales Order", "Delivery Note", "Sales Invoice")
		purchase_doctypes = ("Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation")

		if self.doctype in sales_doctypes:
			address_fields = ["dispatch_address_name", "company_address"]
		elif self.doctype in purchase_doctypes:
			address_fields = ["billing_address", "shipping_address"]

		if not address_fields:
			return

		# Determine if drop ship applies
		is_drop_ship = self.doctype in {
			"Purchase Order",
			"Purchase Invoice",
			"Sales Order",
			"Sales Invoice",
		} and self.is_drop_ship(self.items)

		for field in address_fields:
			address = self.get(field)

			if (field in ["dispatch_address_name", "shipping_address"]) and is_drop_ship:
				continue

			if address and not frappe.db.exists(
				"Dynamic Link",
				{
					"parent": address,
					"parenttype": "Address",
					"link_doctype": "Company",
					"link_name": self.company,
				},
			):
				frappe.throw(
					_("{0} does not belong to the Company {1}.").format(
						_(self.meta.get_label(field)), bold(self.company)
					)
				)

	@staticmethod
	def is_drop_ship(items):
		return any(item.delivered_by_supplier for item in items)

	def validate_price_list(self):
		if self.get("selling_price_list"):
			price_list_field, transaction_side = "selling_price_list", "selling"
		else:
			price_list_field, transaction_side = "buying_price_list", "buying"

		price_list = self.get(price_list_field)
		if not price_list:
			return

		details = (
			frappe.db.get_value("Price List", price_list, ["enabled", transaction_side], as_dict=True)
			or frappe._dict()
		)

		# An internal transfer carries the price list of the outward document into the inward one.
		fits_transaction = details.get(transaction_side) or self.is_internal_transfer()
		if details.enabled and fits_transaction:
			return

		# Returns retain a submitted voucher's pricing even if its price list no longer fits.
		if (
			self.get("is_return")
			and self.get("return_against")
			and price_list
			== frappe.db.get_value(
				self.doctype, {"name": self.return_against, "docstatus": 1}, price_list_field
			)
		):
			return

		if not details.enabled:
			frappe.throw(
				_("Price List {0} is disabled").format(get_link_to_form("Price List", price_list)),
				title=_("Disabled Price List"),
			)

		if transaction_side == "selling":
			message = _("Price List {0} cannot be used on a selling transaction")
		else:
			message = _("Price List {0} cannot be used on a buying transaction")

		frappe.throw(
			message.format(get_link_to_form("Price List", price_list)),
			title=_("Invalid Price List"),
		)

	def set_default_letter_head(self):
		if hasattr(self, "letter_head") and not self.letter_head:
			self.letter_head = frappe.db.get_value("Company", self.company, "default_letter_head")

	def init_internal_values(self):
		# init all the internal values as 0 on sa
		if self.docstatus.is_draft():
			# TODO: Add all such pending values here
			fields = ["billed_amt", "delivered_qty"]
			for item in self.get("items"):
				for field in fields:
					if hasattr(item, field):
						item.set(field, 0)

	def before_cancel(self):
		validate_einvoice_fields(self)

	def _remove_references_in_unreconcile(self):
		upe = frappe.qb.DocType("Unreconcile Payment Entries")
		rows = (
			frappe.qb.from_(upe)
			.select(upe.name, upe.parent)
			.where((upe.reference_doctype == self.doctype) & (upe.reference_name == self.name))
			.run(as_dict=True)
		)

		if rows:
			references_map = frappe._dict()
			for x in rows:
				references_map.setdefault(x.parent, []).append(x.name)

			for doc, rows in references_map.items():
				unreconcile_doc = frappe.get_doc("Unreconcile Payment", doc)
				for row in rows:
					unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0])

				unreconcile_doc.flags.ignore_validate_update_after_submit = True
				unreconcile_doc.flags.ignore_links = True
				unreconcile_doc.save(ignore_permissions=True)

		# delete docs upon parent doc deletion
		unreconcile_docs = frappe.db.get_all("Unreconcile Payment", filters={"voucher_no": self.name})
		for x in unreconcile_docs:
			_doc = frappe.get_doc("Unreconcile Payment", x.name)
			if _doc.docstatus == 1:
				_doc.cancel()
			_doc.delete()

	def _remove_references_in_repost_doctypes(self):
		repost_doctypes = ["Repost Payment Ledger Items", "Repost Accounting Ledger Items"]

		for _doctype in repost_doctypes:
			dt = frappe.qb.DocType(_doctype)

			cancelled_entries = (
				frappe.qb.from_(dt)
				.select(dt.parent, dt.parenttype)
				.where((dt.voucher_type == self.doctype) & (dt.voucher_no == self.name) & (dt.docstatus == 2))
				.run(as_dict=True)
			)

			if cancelled_entries:
				entries = "<br>".join([get_link_to_form(d.parenttype, d.parent) for d in cancelled_entries])

				frappe.throw(
					_(
						"The following cancelled repost entries exist for <b>{0}</b>:<br><br>{1}<br><br>"
						"Kindly delete these entries before continuing."
					).format(self.name, entries)
				)

			rows = (
				frappe.qb.from_(dt)
				.select(dt.name, dt.parent, dt.parenttype)
				.where((dt.voucher_type == self.doctype) & (dt.voucher_no == self.name))
				.run(as_dict=True)
			)

			if rows:
				references_map = frappe._dict()
				for x in rows:
					references_map.setdefault((x.parenttype, x.parent), []).append(x.name)

				for doc, rows in references_map.items():
					repost_doc = frappe.get_doc(doc[0], doc[1])

					for row in rows:
						if _doctype == "Repost Payment Ledger Items":
							repost_doc.remove(repost_doc.get("repost_vouchers", {"name": row})[0])
						else:
							repost_doc.remove(repost_doc.get("vouchers", {"name": row})[0])

					repost_doc.flags.ignore_validate_update_after_submit = True
					repost_doc.flags.ignore_links = True
					repost_doc.save(ignore_permissions=True)

	def _remove_advance_payment_ledger_entries(self):
		adv = qb.DocType("Advance Payment Ledger Entry")
		qb.from_(adv).delete().where(adv.voucher_type.eq(self.doctype) & adv.voucher_no.eq(self.name)).run()

		if self.doctype in self.get_advance_payment_doctypes():
			qb.from_(adv).delete().where(
				adv.against_voucher_type.eq(self.doctype) & adv.against_voucher_no.eq(self.name)
			).run()

	def on_trash(self):
		from erpnext.accounts.utils import delete_exchange_gain_loss_journal, is_immutable_ledger_enabled

		if self.docstatus == 2 and is_immutable_ledger_enabled():
			frappe.throw(
				_("Cancelled transactions cannot be deleted when Immutable Ledger is enabled."),
				title=_("Immutable Ledger Enabled"),
			)

		self._remove_references_in_repost_doctypes()
		self._remove_references_in_unreconcile()
		self.remove_serial_and_batch_bundle()

		# delete sl and gl entries on deletion of transaction
		if frappe.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
			# delete linked exchange gain/loss journal
			delete_exchange_gain_loss_journal(self)

			ple = frappe.qb.DocType("Payment Ledger Entry")
			frappe.qb.from_(ple).delete().where(
				(ple.voucher_type == self.doctype) & (ple.voucher_no == self.name)
				| (
					(ple.against_voucher_type == self.doctype)
					& (ple.against_voucher_no == self.name)
					& ple.delinked
					== 1
				)
			).run()
			gle = frappe.qb.DocType("GL Entry")
			frappe.qb.from_(gle).delete().where(
				(gle.voucher_type == self.doctype) & (gle.voucher_no == self.name)
			).run()
			sle = frappe.qb.DocType("Stock Ledger Entry")
			frappe.qb.from_(sle).delete().where(
				(sle.voucher_type == self.doctype) & (sle.voucher_no == self.name)
			).run()

			self._remove_advance_payment_ledger_entries()

	def remove_serial_and_batch_bundle(self):
		bundles = frappe.get_all(
			"Serial and Batch Bundle",
			filters={"voucher_type": self.doctype, "voucher_no": self.name, "docstatus": ("!=", 1)},
		)

		for bundle in bundles:
			frappe.delete_doc("Serial and Batch Bundle", bundle.name)

		batches = frappe.get_all(
			"Batch", filters={"reference_doctype": self.doctype, "reference_name": self.name}
		)
		for row in batches:
			frappe.delete_doc("Batch", row.name)

	def validate_company_in_accounting_dimension(self):
		doc_field = DocType("DocField")
		accounting_dimension = DocType("Accounting Dimension")
		dimension_list = (
			frappe.qb.from_(accounting_dimension)
			.select(accounting_dimension.document_type)
			.join(doc_field)
			.on(doc_field.parent == accounting_dimension.document_type)
			.where(doc_field.fieldname == "company")
		).run(as_list=True)

		dimension_list = sum(dimension_list, ["Project", "Cost Center"])
		self.validate_company(dimension_list)

		for child in self.get_all_children() or []:
			self.validate_company(dimension_list, child)

	def validate_company(self, dimension_list, child=None):
		for dimension in dimension_list:
			if not child:
				dimension_value = self.get(frappe.scrub(dimension))
			else:
				dimension_value = child.get(frappe.scrub(dimension))

			if dimension_value:
				company = frappe.get_cached_value(dimension, dimension_value, "company")
				if company and company != self.company:
					frappe.throw(
						_("{0}: {1} does not belong to the Company: {2}").format(
							dimension, frappe.bold(dimension_value), self.company
						)
					)

	def validate_party_address_and_contact(self):
		party_type, party = self.get_party()

		if not (party_type and party):
			return

		if party_type == "Customer":
			billing_address, shipping_address = (
				self.get("customer_address"),
				self.get("shipping_address_name"),
			)
			self.validate_party_address(party, party_type, billing_address, shipping_address)
		elif party_type == "Supplier":
			billing_address = self.get("supplier_address")
			self.validate_party_address(party, party_type, billing_address)

		self.validate_party_contact(party, party_type)

	def validate_party_address(self, party, party_type, billing_address, shipping_address=None):
		if billing_address or shipping_address:
			party_address = frappe.get_all(
				"Dynamic Link",
				{"link_doctype": party_type, "link_name": party, "parenttype": "Address"},
				pluck="parent",
			)
			if billing_address and billing_address not in party_address:
				frappe.throw(_("Billing Address does not belong to the {0}").format(party))
			elif shipping_address and shipping_address not in party_address:
				frappe.throw(_("Shipping Address does not belong to the {0}").format(party))

	def validate_party_contact(self, party, party_type):
		if self.get("contact_person"):
			contact = frappe.get_all(
				"Dynamic Link",
				{"link_doctype": party_type, "link_name": party, "parenttype": "Contact"},
				pluck="parent",
			)
			if self.contact_person and self.contact_person not in contact:
				frappe.throw(_("Contact Person does not belong to the {0}").format(party))

	def validate_return_against_account(self):
		if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against:
			cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to"
			original_account = frappe.get_value(self.doctype, self.return_against, cr_dr_account_field)
			if original_account != self.get(cr_dr_account_field):
				frappe.throw(
					_(
						"Please set {0} to {1}, the same account that was used in the original invoice {2}."
					).format(
						frappe.bold(_(self.meta.get_label(cr_dr_account_field), context=self.doctype)),
						frappe.bold(original_account),
						frappe.bold(self.return_against),
					)
				)

	def validate_deferred_income_expense_account(self):
		field_map = {
			"Sales Invoice": "deferred_revenue_account",
			"Purchase Invoice": "deferred_expense_account",
		}

		for item in self.get("items"):
			if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
				if not item.get(field_map.get(self.doctype)):
					default_deferred_account = frappe.get_cached_value(
						"Company", self.company, "default_" + field_map.get(self.doctype)
					)
					if not default_deferred_account:
						frappe.throw(
							_(
								"Row #{0}: Please update deferred revenue/expense account in item row or default account in company master"
							).format(item.idx)
						)
					else:
						item.set(field_map.get(self.doctype), default_deferred_account)

	def validate_auto_repeat_subscription_dates(self):
		if self.get("from_date") and self.get("to_date") and getdate(self.from_date) > getdate(self.to_date):
			frappe.throw(_("To Date cannot be before From Date"), title=_("Invalid Auto Repeat Date"))

	def clear_stale_deferred_fields(self):
		field_map = {
			"Sales Invoice": "deferred_revenue_account",
			"Purchase Invoice": "deferred_expense_account",
		}
		account_field = field_map.get(self.doctype)

		for item in self.get("items"):
			if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
				continue

			item.service_start_date = None
			item.service_end_date = None
			item.service_stop_date = None
			if account_field:
				item.set(account_field, None)

	def validate_deferred_start_and_end_date(self):
		for d in self.items:
			if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"):
				if not (d.service_start_date and d.service_end_date):
					frappe.throw(
						_("Row #{0}: Service Start and End Date is required for deferred accounting").format(
							d.idx
						)
					)
				elif getdate(d.service_start_date) > getdate(d.service_end_date):
					frappe.throw(
						_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(
							d.idx
						)
					)
				elif getdate(self.posting_date) > getdate(d.service_end_date):
					frappe.throw(
						_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx)
					)

	def validate_invoice_documents_schedule(self):
		if (
			self.is_return
			or (self.doctype == "Purchase Invoice" and self.is_paid)
			or (self.doctype == "Sales Invoice" and self.is_pos)
			or self.get("is_opening") == "Yes"
		):
			self.payment_terms_template = ""
			self.payment_schedule = []

		if self.is_return:
			return

		self.validate_payment_schedule_dates()
		self.set_due_date()
		self.set_payment_schedule()
		if not self.get("ignore_default_payment_terms_template"):
			self.validate_payment_schedule_amount()
			self.validate_due_date()
		self.validate_advance_entries()

	def validate_non_invoice_documents_schedule(self):
		self.set_payment_schedule()
		self.validate_payment_schedule_dates()
		self.validate_payment_schedule_amount()

	def validate_all_documents_schedule(self):
		if self.doctype in ("Sales Invoice", "Purchase Invoice"):
			self.validate_invoice_documents_schedule()
		elif self.doctype in ("Quotation", "Purchase Order", "Sales Order"):
			self.validate_non_invoice_documents_schedule()

	def shift_service_dates(self, reference_doc, auto_repeat_doc):
		"""Move item service dates into the new invoice period (used by Auto Repeat)."""
		if not (self.from_date and self.to_date and reference_doc.from_date and reference_doc.to_date):
			return

		from_date = getdate(self.from_date)
		reference_from_date = getdate(reference_doc.from_date)
		months = (
			(from_date.year - reference_from_date.year) * 12 + from_date.month - reference_from_date.month
		)
		days = date_diff(from_date, reference_from_date)
		shift_by_months = auto_repeat_doc.frequency in month_map

		reference_to_date = getdate(reference_doc.to_date)
		to_date = getdate(self.to_date)

		def shift(date):
			# Keep the period end aligned, e.g. 1-28 Feb becomes 1-31 Mar.
			if getdate(date) == reference_to_date:
				return to_date
			if not shift_by_months:
				return add_days(date, days)
			# Whole months never reverse a period, e.g. 29-31 Jan becomes 28-28 Feb.
			shifted_date = getdate(add_months(date, months))
			# Month ends stay month ends, e.g. 1-28 Feb becomes 1-31 Mar.
			if getdate(date) == get_last_day(date):
				shifted_date = get_last_day(shifted_date)
			# Dates inside the reference period stay inside the new period, which can end earlier in the month.
			if getdate(date) < reference_to_date:
				return min(shifted_date, to_date)
			return shifted_date

		for item, reference_item in zip(self.items, reference_doc.items, strict=True):
			if reference_item.service_start_date:
				item.service_start_date = shift(reference_item.service_start_date)
			if reference_item.service_end_date:
				item.service_end_date = shift(reference_item.service_end_date)
			# The new period can end later in the month, e.g. 30 Jan-26 Feb becomes 27 Feb-29 Mar.
			# A start date moved to the period end can then pass the end date, so move the end date after it.
			if (
				item.service_start_date
				and item.service_end_date
				and getdate(item.service_end_date) < getdate(item.service_start_date)
			):
				item.service_end_date = add_days(to_date, 1)

	def before_print(self, settings=None):
		self.set_missing_terms()

		if self.doctype in [
			"Purchase Order",
			"Sales Order",
			"Sales Invoice",
			"Purchase Invoice",
			"Supplier Quotation",
			"Purchase Receipt",
			"Delivery Note",
			"Quotation",
		]:
			if self.get("group_same_items"):
				self.group_similar_items()

			df = self.meta.get_field("discount_amount")
			if self.get("discount_amount") and hasattr(self, "taxes") and not len(self.taxes):
				df.set("print_hide", 0)
				self.discount_amount = -self.discount_amount
			else:
				df.set("print_hide", 1)

		set_print_templates_for_item_table(self, settings)
		set_print_templates_for_taxes(self, settings)

	def set_missing_terms(self):
		if not self.get("tc_name") or self.get("terms"):
			return

		from erpnext.setup.doctype.terms_and_conditions.terms_and_conditions import (
			get_terms_and_conditions,
		)

		self.terms = get_terms_and_conditions(self.tc_name, self.as_dict())

	def calculate_paid_amount(self):
		if hasattr(self, "is_pos") or hasattr(self, "is_paid"):
			is_paid = self.get("is_pos") or self.get("is_paid")

			if is_paid:
				if not self.cash_bank_account:
					# show message that the amount is not paid
					frappe.throw(
						_(
							"Note: Payment Entry will not be created since 'Cash or Bank Account' was not specified"
						)
					)

				if cint(self.is_return) and self.grand_total > self.paid_amount:
					self.paid_amount = flt(flt(self.grand_total), self.precision("paid_amount"))

				elif not flt(self.paid_amount) and flt(self.outstanding_amount) > 0:
					self.paid_amount = flt(flt(self.outstanding_amount), self.precision("paid_amount"))

				self.base_paid_amount = flt(
					self.paid_amount * self.conversion_rate, self.precision("base_paid_amount")
				)
			else:
				self.paid_amount = 0
				self.base_paid_amount = 0

	def set_missing_values(self, for_validate=False):
		if frappe.in_test:
			for fieldname in ["posting_date", "transaction_date"]:
				if self.meta.get_field(fieldname) and not self.get(fieldname):
					self.set(fieldname, today())
					break

	def calculate_taxes_and_totals(self):
		from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals

		calculate_taxes_and_totals(self)

		if self.doctype in (
			"Sales Order",
			"Delivery Note",
			"Sales Invoice",
			"POS Invoice",
		):
			self.calculate_commission()
			self.calculate_contribution()

	def validate_date_with_fiscal_year(self):
		date_field = None
		if self.meta.get_field("posting_date"):
			date_field = "posting_date"
		elif self.meta.get_field("transaction_date"):
			date_field = "transaction_date"

		if not date_field or not self.get(date_field):
			return

		if self.meta.get_field("fiscal_year"):
			validate_fiscal_year(
				self.get(date_field),
				self.fiscal_year,
				self.company,
				self.meta.get_label(date_field),
				self,
			)
		else:
			get_fiscal_year(
				self.get(date_field),
				company=self.company,
				label=self.meta.get_label(date_field),
			)

	def validate_party_accounts(self):
		if self.doctype not in ("Sales Invoice", "Purchase Invoice"):
			return

		if self.doctype == "Sales Invoice":
			party_account_field = "debit_to"
			item_field = "income_account"
		else:
			party_account_field = "credit_to"
			item_field = "expense_account"

		for item in self.get("items"):
			if item.get(item_field) == self.get(party_account_field):
				frappe.throw(
					_("Row {0}: {1} {2} cannot be same as {3} (Party Account) {4}").format(
						item.idx,
						frappe.bold(frappe.unscrub(item_field)),
						item.get(item_field),
						frappe.bold(frappe.unscrub(party_account_field)),
						self.get(party_account_field),
					)
				)

	def validate_inter_company_reference(self):
		if self.get("is_return"):
			return

		if self.doctype not in ("Purchase Invoice", "Purchase Receipt"):
			return

		if self.is_internal_transfer():
			if not (
				self.get("inter_company_reference")
				or self.get("inter_company_invoice_reference")
				or self.get("inter_company_order_reference")
			) and not self.get("is_return"):
				msg = _("Internal Sale or Delivery Reference missing.")
				msg += _("Please create purchase from internal sale or delivery document itself")
				frappe.throw(msg, title=_("Internal Sales Reference Missing"))

			label = "Delivery Note Item" if self.doctype == "Purchase Receipt" else "Sales Invoice Item"

			field = frappe.scrub(label)

			for row in self.get("items"):
				if not row.get(field) and not row.get("delivery_note_packed_item"):
					msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer"
					frappe.throw(_(msg), title=_("Internal Transfer Reference Missing"))

	def validate_inter_company_currency(self):
		if self.doctype not in COUNTERPART_REFERENCE:
			return

		reference_field, counterpart_doctype = COUNTERPART_REFERENCE[self.doctype]
		counterpart = self.get(reference_field)
		if not counterpart:
			return

		currency = frappe.db.get_value(counterpart_doctype, counterpart, "currency")
		if currency and self.currency != currency:
			frappe.throw(
				_("Currency must be {0} to match {1} {2}").format(
					bold(currency), counterpart_doctype, bold(counterpart)
				)
			)

	def validate_internal_transaction(self):
		if not cint(frappe.get_single_value("Accounts Settings", "maintain_same_internal_transaction_rate")):
			return

		doctypes_list = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]

		if self.doctype in doctypes_list and (
			self.get("is_internal_customer") or self.get("is_internal_supplier")
		):
			self.validate_internal_transaction_based_on_voucher_type()

	def validate_internal_transaction_based_on_voucher_type(self):
		order = ["Sales Order", "Purchase Order"]
		invoice = ["Sales Invoice", "Purchase Invoice"]

		if self.doctype in order and self.get("inter_company_order_reference"):
			# Fetch the linked order
			linked_doctype = "Sales Order" if self.doctype == "Purchase Order" else "Purchase Order"
			self.validate_line_items(
				linked_doctype,
				"sales_order" if linked_doctype == "Sales Order" else "purchase_order",
				"sales_order_item" if linked_doctype == "Sales Order" else "purchase_order_item",
			)
		elif self.doctype in invoice and self.get("inter_company_invoice_reference"):
			# Fetch the linked invoice
			linked_doctype = "Sales Invoice" if self.doctype == "Purchase Invoice" else "Purchase Invoice"
			self.validate_line_items(
				linked_doctype,
				"sales_invoice" if linked_doctype == "Sales Invoice" else "purchase_invoice",
				"sales_invoice_item" if linked_doctype == "Sales Invoice" else "purchase_invoice_item",
			)

	def validate_line_items(self, ref_dt, ref_dn_field, ref_link_field):
		action, role_allowed_to_override = frappe.get_cached_value(
			"Accounts Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
		)

		reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
		reference_details = self.get_reference_details(reference_names, ref_dt + " Item")

		stop_actions = []

		for d in self.get("items"):
			if d.get(ref_link_field):
				ref_rate = reference_details.get(d.get(ref_link_field))
				if ref_rate is not None and abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
					if action == "Stop":
						user_roles = [
							r["role"]
							for r in frappe.get_all(
								"Has Role", filters={"parent": frappe.session.user}, fields=["role"]
							)
						]
						if role_allowed_to_override not in user_roles:
							stop_actions.append(
								_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
									d.idx,
									ref_dt,
									self.inter_company_invoice_reference
									if d.parenttype in ("Sales Invoice", "Purchase Invoice")
									else d.get(ref_dn_field),
									d.rate,
									ref_rate,
								)
							)
					else:
						frappe.msgprint(
							_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
								d.idx,
								ref_dt,
								self.inter_company_invoice_reference
								if d.parenttype in ("Sales Invoice", "Purchase Invoice")
								else d.get(ref_dn_field),
								d.rate,
								ref_rate,
							),
							title=_("Warning"),
							indicator="orange",
						)

		if stop_actions:
			frappe.throw(stop_actions, as_list=True)

	def disable_pricing_rule_on_internal_transfer(self):
		if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
			self.ignore_pricing_rule = 1
			frappe.msgprint(
				_("Disabled pricing rules since this {} is an internal transfer").format(self.doctype),
				alert=1,
			)

	def disable_tax_included_prices_for_internal_transfer(self):
		if self.is_internal_transfer():
			tax_updated = False
			for tax in self.get("taxes"):
				if tax.get("included_in_print_rate"):
					tax.included_in_print_rate = 0
					tax_updated = True

			if tax_updated:
				frappe.msgprint(
					_("Disabled tax included prices since this {} is an internal transfer").format(
						self.doctype
					),
					alert=1,
				)

	def validate_due_date(self):
		if self.get("is_pos") or self.doctype not in ["Sales Invoice", "Purchase Invoice"]:
			return

		from erpnext.accounts.party import validate_due_date

		posting_date = (
			self.posting_date if self.doctype == "Sales Invoice" else (self.bill_date or self.posting_date)
		)

		# skip due date validation for records via Data Import
		if frappe.flags.in_import and getdate(self.due_date) < getdate(posting_date):
			self.due_date = posting_date

		elif self.doctype in ["Sales Invoice", "Purchase Invoice"]:
			bill_date = self.bill_date if self.doctype == "Purchase Invoice" else None

			validate_due_date(
				posting_date=posting_date,
				due_date=self.due_date,
				bill_date=bill_date,
				template_name=self.payment_terms_template,
				doctype=self.doctype,
			)

	def set_price_list_currency(self, buying_or_selling):
		if self.meta.get_field("posting_date"):
			transaction_date = self.posting_date
		else:
			transaction_date = self.transaction_date

		if self.meta.get_field("currency"):
			# price list part
			if buying_or_selling.lower() == "selling":
				fieldname = "selling_price_list"
				args = "for_selling"
			else:
				fieldname = "buying_price_list"
				args = "for_buying"

			if self.meta.get_field(fieldname) and self.get(fieldname):
				previous_price_list_currency = self.price_list_currency
				self.price_list_currency = frappe.db.get_value("Price List", self.get(fieldname), "currency")

				if self.price_list_currency == self.company_currency:
					self.plc_conversion_rate = 1.0

				elif not self.plc_conversion_rate or (
					previous_price_list_currency and previous_price_list_currency != self.price_list_currency
				):
					self.plc_conversion_rate = get_exchange_rate(
						self.price_list_currency, self.company_currency, transaction_date, args
					)

			# currency
			if not self.currency:
				self.currency = self.price_list_currency
				self.conversion_rate = self.plc_conversion_rate
			elif self.currency == self.company_currency:
				self.conversion_rate = 1.0
			elif not self.conversion_rate:
				self.conversion_rate = get_exchange_rate(
					self.currency, self.company_currency, transaction_date, args
				)

	def on_recurring(self, reference_doc, auto_repeat_doc):
		self.refresh_exchange_rates()

	def refresh_exchange_rates(self):
		if not self.meta.get_field("conversion_rate"):
			return

		transaction_date = self.get("posting_date") or self.get("transaction_date")
		args = "for_buying" if self.meta.get_field("buying_price_list") else "for_selling"

		for currency_field, rate_field in (
			("currency", "conversion_rate"),
			("price_list_currency", "plc_conversion_rate"),
		):
			currency = self.get(currency_field)
			if not currency or currency == self.company_currency:
				continue

			exchange_rate = get_exchange_rate(currency, self.company_currency, transaction_date, args)
			if not exchange_rate:
				frappe.throw(
					_(
						"Cannot create recurring document: no exchange rate found for {0} to {1} on {2}. Please create a Currency Exchange record."
					).format(currency, self.company_currency, transaction_date),
					title=_("Exchange Rate Not Found"),
				)
			self.set(rate_field, exchange_rate)

	def set_missing_item_details(self, for_validate=False):
		"""set missing item values"""
		from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos

		if hasattr(self, "items"):
			parent_dict = {}
			for fieldname in self.meta.get_valid_columns():
				parent_dict[fieldname] = self.get(fieldname)

			if self.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]:
				document_type = f"{self.doctype} Item"
				parent_dict.update({"document_type": document_type})

			# party_name field used for customer in quotation
			if (
				self.doctype == "Quotation"
				and self.quotation_to == "Customer"
				and parent_dict.get("party_name")
			):
				parent_dict.update({"customer": parent_dict.get("party_name")})

			self.pricing_rules = []

			for item in self.get("items"):
				if item.get("item_code"):
					ctx: ItemDetailsCtx = ItemDetailsCtx(parent_dict.copy())
					ctx.update(item.as_dict())

					ctx.update(
						{
							"doctype": self.doctype,
							"name": self.name,
							"child_doctype": item.doctype,
							"child_docname": item.name,
							"ignore_pricing_rule": (
								self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0
							),
						}
					)

					if not ctx.transaction_date:
						ctx.transaction_date = ctx.posting_date

					if self.get("is_subcontracted"):
						ctx.is_subcontracted = self.is_subcontracted

					ret = _get_item_details(ctx, self, for_validate=for_validate, overwrite_warehouse=False)
					for fieldname, value in ret.items():
						if item.meta.get_field(fieldname) and value is not None:
							if (
								item.get(fieldname) is None
								or fieldname in force_item_fields
								or (
									fieldname in ["serial_no", "batch_no"]
									and item.get("use_serial_batch_fields")
								)
							):
								item.set(fieldname, value)

								if fieldname == "batch_no" and item.batch_no and not item.is_free_item:
									if ret.get("rate"):
										item.set("rate", ret.get("rate"))

									if not item.get("price_list_rate") and ret.get("price_list_rate"):
										item.set("price_list_rate", ret.get("price_list_rate"))

							elif fieldname in ["cost_center", "conversion_factor"] and not item.get(
								fieldname
							):
								item.set(fieldname, value)
							elif fieldname == "item_tax_rate" and not (
								self.get("is_return") and self.get("return_against")
							):
								item.set(fieldname, value)
							elif fieldname == "serial_no":
								# Ensure that serial numbers are matched against Stock UOM
								item_conversion_factor = item.get("conversion_factor") or 1.0
								item_qty = abs(item.get("qty")) * item_conversion_factor

								if item_qty != len(get_serial_nos(item.get("serial_no"))):
									item.set(fieldname, value)

							elif (
								ret.get("pricing_rule_removed")
								and value is not None
								and fieldname
								in [
									"discount_percentage",
									"discount_amount",
									"rate",
									"margin_rate_or_amount",
									"margin_type",
									"remove_free_item",
								]
							):
								# reset pricing rule fields if pricing_rule_removed
								item.set(fieldname, value)

							elif fieldname == "expense_account" and not item.get("expense_account"):
								item.expense_account = value

					if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field(
						"is_fixed_asset"
					):
						item.set("is_fixed_asset", ret.get("is_fixed_asset", 0))

					if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field(
						"tax_withholding_category",
					):
						if not item.get("tax_withholding_category") and ret.get("tax_withholding_category"):
							item.set("tax_withholding_category", ret.get("tax_withholding_category"))

					# Double check for cost center
					# Items add via promotional scheme may not have cost center set
					if hasattr(item, "cost_center") and not item.get("cost_center"):
						item.set(
							"cost_center",
							self.get("cost_center") or erpnext.get_default_cost_center(self.company),
						)

					if ret.get("pricing_rules"):
						self.apply_pricing_rule_on_items(item, ret)
						self.set_pricing_rule_details(item, ret)
				else:
					# Transactions line item without item code

					uom = item.get("uom")
					stock_uom = item.get("stock_uom")
					if bool(uom) != bool(stock_uom):  # xor
						item.stock_uom = item.uom = uom or stock_uom

					# UOM cannot be zero so substitute as 1
					item.conversion_factor = (
						get_uom_conv_factor(item.get("uom"), item.get("stock_uom"))
						or item.get("conversion_factor")
						or 1
					)

			if self.doctype == "Purchase Invoice":
				self.set_expense_account(for_validate)

	def apply_pricing_rule_on_items(self, item, pricing_rule_args):
		if not pricing_rule_args.get("validate_applied_rule", 0):
			# if user changed the discount percentage then set user's discount percentage ?
			if pricing_rule_args.get("price_or_product_discount") == "Price":
				item.set("pricing_rules", pricing_rule_args.get("pricing_rules"))
				if pricing_rule_args.get("apply_rule_on_other_items"):
					other_items = json.loads(pricing_rule_args.get("apply_rule_on_other_items"))
					if other_items and item.item_code not in other_items:
						return

				item.set("discount_percentage", pricing_rule_args.get("discount_percentage"))
				item.set("discount_amount", pricing_rule_args.get("discount_amount"))
				if (
					pricing_rule_args.get("has_pricing_rule_for_rate")
					or pricing_rule_args.get("pricing_rule_for") == "Rate"
				):
					item.set("price_list_rate", pricing_rule_args.get("price_list_rate"))

				if item.get("price_list_rate"):
					item.rate = flt(
						item.price_list_rate * (1.0 - (flt(item.discount_percentage) / 100.0)),
						item.precision("rate"),
					)

					if item.get("discount_amount"):
						item.rate = item.price_list_rate - item.discount_amount

				if item.get("apply_discount_on_discounted_rate") and pricing_rule_args.get("rate"):
					item.rate = pricing_rule_args.get("rate")

			elif pricing_rule_args.get("free_item_data"):
				apply_pricing_rule_for_free_items(self, pricing_rule_args.get("free_item_data"))

		elif pricing_rule_args.get("validate_applied_rule"):
			for pricing_rule in get_applied_pricing_rules(item.get("pricing_rules")):
				pricing_rule_doc = frappe.get_cached_doc("Pricing Rule", pricing_rule)
				for field in ["discount_percentage", "discount_amount", "rate"]:
					if item.get(field) < pricing_rule_doc.get(field):
						title = get_link_to_form("Pricing Rule", pricing_rule)

						frappe.msgprint(
							_("Row {0}: user has not applied the rule {1} on the item {2}").format(
								item.idx, frappe.bold(title), frappe.bold(item.item_code)
							)
						)

	def set_pricing_rule_details(self, item_row, args):
		pricing_rules = get_applied_pricing_rules(args.get("pricing_rules"))
		if not pricing_rules:
			return

		for pricing_rule in pricing_rules:
			self.append(
				"pricing_rules",
				{
					"pricing_rule": pricing_rule,
					"item_code": item_row.item_code,
					"child_docname": item_row.name,
					"rule_applied": True,
				},
			)

	def set_taxes(self):
		if not self.meta.get_field("taxes"):
			return

		tax_master_doctype = self.meta.get_field("taxes_and_charges").options

		if (self.is_new() or self.is_pos_profile_changed()) and not self.get("taxes"):
			if self.company and not self.get("taxes_and_charges"):
				# get the default tax master
				self.taxes_and_charges = frappe.db.get_value(
					tax_master_doctype, {"is_default": 1, "company": self.company}
				)

			self.append_taxes_from_master(tax_master_doctype)

	def is_pos_profile_changed(self):
		if (
			self.doctype == "Sales Invoice"
			and self.is_pos
			and self.pos_profile != frappe.db.get_value("Sales Invoice", self.name, "pos_profile")
		):
			return True

	def set_taxes_and_charges(self):
		if self.doctype == "Material Request":
			# Material Request does not have taxes
			return

		if self.get("taxes") or self.get("is_pos"):
			return

		# set by the Opening Invoice Creation Tool, where the outstanding amount
		# entered against a party is already inclusive of tax
		if self.flags.dont_auto_add_taxes:
			return

		if frappe.get_single_value(
			"Accounts Settings", "add_taxes_from_taxes_and_charges_template"
		) and hasattr(self, "taxes_and_charges"):
			if tax_master_doctype := self.meta.get_field("taxes_and_charges").options:
				self.append_taxes_from_master(tax_master_doctype)

		if frappe.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
			self.append_taxes_from_item_tax_template()

	def append_taxes_from_master(self, tax_master_doctype=None):
		if self.get("taxes_and_charges"):
			if not tax_master_doctype:
				tax_master_doctype = self.meta.get_field("taxes_and_charges").options
			self.extend("taxes", _get_taxes_and_charges(tax_master_doctype, self.get("taxes_and_charges")))

	def append_taxes_from_item_tax_template(self):
		if not frappe.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
			return

		for row in self.items:
			item_tax_rate = row.get("item_tax_rate")
			if not item_tax_rate:
				continue

			if isinstance(item_tax_rate, str):
				item_tax_rate = parse_json(item_tax_rate)

			for account_head, _rate in item_tax_rate.items():
				row = self.get_tax_row(account_head)

				if not row:
					self.append(
						"taxes",
						{
							"charge_type": "On Net Total",
							"account_head": account_head,
							"rate": 0,
							"description": account_head,
							"set_by_item_tax_template": 1,
							"category": "Total",
							"add_deduct_tax": "Add",
						},
					)

	def get_tax_row(self, account_head):
		for row in self.taxes:
			if row.account_head == account_head:
				return row

	def set_other_charges(self):
		self.set("taxes", [])
		self.set_taxes()

	def validate_enabled_taxes_and_charges(self):
		taxes_and_charges_doctype = self.meta.get_options("taxes_and_charges")
		if self.taxes_and_charges and frappe.get_cached_value(
			taxes_and_charges_doctype, self.taxes_and_charges, "disabled"
		):
			frappe.throw(_("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges))

	def validate_tax_account_company(self):
		for d in self.get("taxes"):
			if d.account_head:
				tax_account_company = frappe.get_cached_value("Account", d.account_head, "company")
				if tax_account_company != self.company:
					frappe.throw(
						_("Row #{0}: Account {1} does not belong to company {2}").format(
							d.idx, d.account_head, self.company
						)
					)

	def get_gl_dict(self, args, account_currency=None, item=None):
		"""this method populates the common properties of a gl entry record"""

		posting_date = args.get("posting_date") or self.get("posting_date")
		fiscal_years = get_fiscal_years(posting_date, company=self.company)
		if len(fiscal_years) > 1:
			frappe.throw(
				_("Multiple fiscal years exist for the date {0}. Please set company in Fiscal Year").format(
					formatdate(posting_date)
				)
			)
		else:
			fiscal_year = fiscal_years[0][0]

		gl_dict = frappe._dict(
			{
				"company": self.company,
				"posting_date": posting_date,
				"fiscal_year": fiscal_year,
				"voucher_type": self.doctype,
				"voucher_no": self.name,
				"remarks": self.get("remarks") or self.get("remark"),
				"debit": 0,
				"credit": 0,
				"debit_in_account_currency": 0,
				"credit_in_account_currency": 0,
				"is_opening": self.get("is_opening") or "No",
				"party_type": None,
				"party": None,
				"project": self.get("project"),
				"post_net_value": args.get("post_net_value"),
				"voucher_detail_no": args.get("voucher_detail_no"),
				"voucher_subtype": self.get_voucher_subtype(),
			}
		)

		with temporary_flag("company", self.company):
			update_gl_dict_with_regional_fields(self, gl_dict)

		update_gl_dict_with_app_based_fields(self, gl_dict)

		accounting_dimensions = get_accounting_dimensions()
		dimension_dict = frappe._dict()

		for dimension in accounting_dimensions:
			value = self.get(dimension)
			if item and item.get(dimension):
				value = item.get(dimension)
			if isinstance(value, list | dict):
				continue
			dimension_dict[dimension] = value

		gl_dict.update(dimension_dict)
		gl_dict.update(args)

		if not account_currency:
			account_currency = get_account_currency(gl_dict.account)

		if gl_dict.account and self.doctype not in [
			"Journal Entry",
			"Period Closing Voucher",
			"Payment Entry",
			"Purchase Receipt",
			"Purchase Invoice",
			"Stock Entry",
		]:
			self.validate_account_currency(gl_dict.account, account_currency)

		if gl_dict.account and self.doctype not in [
			"Journal Entry",
			"Period Closing Voucher",
			"Payment Entry",
		]:
			set_balance_in_account_currency(
				gl_dict,
				account_currency,
				args.get("transaction_exchange_rate") or self.get("conversion_rate"),
				self.company_currency,
			)

		# Update details in transaction currency
		if self.doctype not in ["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"]:
			gl_dict.update(
				{
					"transaction_currency": self.get("currency") or self.company_currency,
					"transaction_exchange_rate": args.get("transaction_exchange_rate")
					or self.get("conversion_rate", 1),
					"debit_in_transaction_currency": self.get_value_in_transaction_currency(
						account_currency, gl_dict, "debit"
					),
					"credit_in_transaction_currency": self.get_value_in_transaction_currency(
						account_currency, gl_dict, "credit"
					),
				}
			)

		if not args.get("against_voucher_type") and self.get("against_voucher_type"):
			gl_dict.update({"against_voucher_type": self.get("against_voucher_type")})

		if not args.get("against_voucher") and self.get("against_voucher"):
			gl_dict.update({"against_voucher": self.get("against_voucher")})

		return gl_dict

	def get_voucher_subtype(self):
		voucher_subtypes = {
			"Journal Entry": "voucher_type",
			"Payment Entry": "payment_type",
			"Stock Entry": "stock_entry_type",
			"Asset Capitalization": "entry_type",
		}

		for method_name in frappe.get_hooks("voucher_subtypes"):
			voucher_subtype = frappe.get_attr(method_name)(self)

			if voucher_subtype:
				return voucher_subtype

		if self.doctype in voucher_subtypes:
			return self.get(voucher_subtypes[self.doctype])
		elif self.doctype == "Purchase Receipt" and self.is_return:
			return "Purchase Return"
		elif self.doctype == "Delivery Note" and self.is_return:
			return "Sales Return"
		elif self.doctype == "Sales Invoice" and self.is_return:
			return "Credit Note"
		elif self.doctype == "Sales Invoice" and self.is_debit_note:
			return "Debit Note"
		elif self.doctype == "Purchase Invoice" and self.is_return:
			return "Debit Note"

		return self.doctype

	def get_value_in_transaction_currency(self, account_currency, gl_dict, field):
		if account_currency == self.get("currency"):
			return gl_dict.get(field + "_in_account_currency")
		else:
			return flt(gl_dict.get(field, 0) / self.get("conversion_rate", 1))

	def validate_zero_qty_for_return_invoices_with_stock(self):
		rows = []
		for item in self.items:
			if not flt(item.qty):
				rows.append(item)
		if rows:
			frappe.throw(
				_(
					"For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}"
				).format(frappe.bold(comma_and(["#" + str(x.idx) for x in rows])))
			)

	def validate_qty_is_not_zero(self):
		if self.flags.allow_zero_qty:
			return

		for item in self.items:
			if self.doctype == "Purchase Receipt" and item.rejected_qty:
				continue

			if not flt(item.qty):
				frappe.throw(
					msg=_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
						item.idx, frappe.bold(item.item_code)
					),
					title=_("Invalid Quantity"),
					exc=InvalidQtyError,
				)

	def validate_account_currency(self, account, account_currency=None):
		valid_currency = [self.company_currency]
		if self.get("currency") and self.currency != self.company_currency:
			valid_currency.append(self.currency)

		if account_currency not in valid_currency:
			frappe.throw(
				_("Account {0} is invalid. Account Currency must be {1}").format(
					account, (" " + _("or") + " ").join(valid_currency)
				)
			)

	def clear_unallocated_advances(self, childtype, parentfield):
		self.set(parentfield, self.get(parentfield, {"allocated_amount": ["not in", [0, None, ""]]}))

		doctype = frappe.qb.DocType(childtype)
		frappe.qb.from_(doctype).delete().where(
			(doctype.parentfield == parentfield)
			& (doctype.parent == self.name)
			& (doctype.allocated_amount == 0)
		).run()

	@frappe.whitelist()
	def apply_shipping_rule(self):
		if self.shipping_rule:
			shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule)
			shipping_rule.apply(self)
			self.calculate_taxes_and_totals()

	def get_shipping_address(self):
		"""Returns Address object from shipping address fields if present"""

		# shipping address fields can be `shipping_address_name` or `shipping_address`
		# try getting value from both

		for fieldname in ("shipping_address_name", "shipping_address"):
			shipping_field = self.meta.get_field(fieldname)
			if shipping_field and shipping_field.fieldtype == "Link":
				if self.get(fieldname):
					return frappe.get_doc("Address", self.get(fieldname))

		return {}

	@frappe.whitelist()
	def set_advances(self):
		"""Returns list of advances against Account, Party, Reference"""

		res = self.get_advance_entries(
			include_unallocated=not cint(self.get("only_include_allocated_payments"))
		)

		self.set("advances", [])
		advance_allocated = 0
		for d in res:
			if self.get("party_account_currency") == self.company_currency:
				amount = self.get("base_rounded_total") or self.base_grand_total
			else:
				amount = self.get("rounded_total") or self.grand_total
			allocated_amount = min(amount - advance_allocated, d.amount)
			advance_allocated += flt(allocated_amount)

			advance_row = {
				"doctype": self.doctype + " Advance",
				"reference_type": d.reference_type,
				"reference_name": d.reference_name,
				"reference_row": d.reference_row,
				"remarks": d.remarks,
				"advance_amount": flt(d.amount),
				"allocated_amount": allocated_amount,
				"ref_exchange_rate": flt(d.exchange_rate),  # exchange_rate of advance entry
				"difference_posting_date": self.posting_date,
			}
			if d.get("paid_from"):
				advance_row["account"] = d.paid_from
			if d.get("paid_to"):
				advance_row["account"] = d.paid_to

			self.append("advances", advance_row)

	def get_advance_entries(self, include_unallocated=True):
		party_account = []
		default_advance_account = None

		if self.doctype in ["Sales Invoice", "POS Invoice"]:
			party_type = "Customer"
			party = self.customer
			amount_field = "credit_in_account_currency"
			order_field = "sales_order"
			order_doctype = "Sales Order"
			party_account.append(self.debit_to)
		else:
			party_type = "Supplier"
			party = self.supplier
			amount_field = "debit_in_account_currency"
			order_field = "purchase_order"
			order_doctype = "Purchase Order"
			party_account.append(self.credit_to)

		party_accounts = get_party_account(
			party_type, party=party, company=self.company, include_advance=True
		)

		if party_accounts:
			party_account.append(party_accounts[0])
			default_advance_account = party_accounts[1] if len(party_accounts) == 2 else None

		order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))

		journal_entries = get_advance_journal_entries(
			party_type, party, party_account, amount_field, order_doctype, order_list, include_unallocated
		)

		payment_entries = get_advance_payment_entries_for_regional(
			party_type,
			party,
			party_account,
			order_doctype,
			order_list,
			default_advance_account,
			include_unallocated,
		)

		res = journal_entries + payment_entries

		return res

	def is_inclusive_tax(self):
		is_inclusive = cint(frappe.get_single_value("Accounts Settings", "show_inclusive_tax_in_print"))

		if is_inclusive:
			is_inclusive = 0
			if self.get("taxes", filters={"included_in_print_rate": 1}):
				is_inclusive = 1

		return is_inclusive

	def should_show_taxes_as_table_in_print(self):
		return cint(frappe.get_single_value("Accounts Settings", "show_taxes_as_table_in_print"))

	def validate_advance_entries(self):
		order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order"
		order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))

		if not order_list:
			return

		advance_entries = self.get_advance_entries(include_unallocated=False)

		if advance_entries:
			advance_entries_against_si = [d.reference_name for d in self.get("advances")]
			for d in advance_entries:
				if not advance_entries_against_si or d.reference_name not in advance_entries_against_si:
					frappe.msgprint(
						_(
							"Payment Entry {0} is linked against Order {1}, check if it should be pulled as advance in this invoice."
						).format(d.reference_name, d.against_order)
					)

	def set_advance_gain_or_loss(self):
		if self.get("conversion_rate") == 1 or not self.get("advances"):
			return

		is_purchase_invoice = self.doctype == "Purchase Invoice"
		party_account = self.credit_to if is_purchase_invoice else self.debit_to
		if get_account_currency(party_account) != self.currency:
			return

		for d in self.get("advances"):
			advance_exchange_rate = d.ref_exchange_rate
			if d.allocated_amount and self.conversion_rate != advance_exchange_rate:
				base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount
				base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount
				difference = base_allocated_amount_in_ref_rate - base_allocated_amount_in_inv_rate

				d.exchange_gain_loss = difference

	def make_precision_loss_gl_entry(self, gl_entries):
		(
			round_off_account,
			round_off_cost_center,
			round_off_for_opening,
		) = get_round_off_account_and_cost_center(
			self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
		)

		precision_loss = self.get("base_net_total") - flt(
			self.get("net_total") * self.conversion_rate, self.precision("net_total")
		)

		credit_or_debit = "credit" if self.doctype == "Purchase Invoice" else "debit"
		against = self.supplier if self.doctype == "Purchase Invoice" else self.customer

		if precision_loss:
			gl_entries.append(
				self.get_gl_dict(
					{
						"account": round_off_account,
						"against": against,
						credit_or_debit: precision_loss,
						"cost_center": round_off_cost_center
						if self.use_company_roundoff_cost_center
						else self.cost_center or round_off_cost_center,
						"remarks": _("Net total calculation precision loss"),
					}
				)
			)

	def gain_loss_journal_already_booked(
		self,
		gain_loss_account,
		exc_gain_loss,
		ref2_dt,
		ref2_dn,
		ref2_detail_no,
	) -> bool:
		"""
		Check if gain/loss is booked
		"""
		if res := frappe.db.get_all(
			"Journal Entry Account",
			filters={
				"docstatus": 1,
				"account": gain_loss_account,
				"reference_type": ref2_dt,  # this will be Journal Entry
				"reference_name": ref2_dn,
				"reference_detail_no": ref2_detail_no,
			},
			pluck="parent",
		):
			# deduplicate
			res = list({x for x in res})
			if exc_vouchers := frappe.db.get_all(
				"Journal Entry",
				filters={"name": ["in", res], "voucher_type": "Exchange Gain Or Loss"},
				fields=["voucher_type", "total_debit", "total_credit"],
			):
				booked_voucher = exc_vouchers[0]
				if (
					booked_voucher.total_debit == exc_gain_loss
					and booked_voucher.total_credit == exc_gain_loss
					and booked_voucher.voucher_type == "Exchange Gain Or Loss"
				):
					return True
		return False

	def make_exchange_gain_loss_journal(
		self, args: dict | None = None, dimensions_dict: dict | None = None
	) -> None:
		"""
		Make Exchange Gain/Loss journal for Invoices and Payments
		"""
		# Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event.
		# see accounts/utils.py:cancel_exchange_gain_loss_journal()
		if self.docstatus == 1:
			if dimensions_dict is None:
				dimensions_dict = frappe._dict()
				active_dimensions = get_dimensions()[0]
				for dim in active_dimensions:
					dimensions_dict[dim.fieldname] = self.get(dim.fieldname)

			if self.get("doctype") == "Journal Entry":
				# 'args' is populated with exchange gain/loss account and the amount to be booked.
				# These are generated by Sales/Purchase Invoice during reconciliation and advance allocation.
				# and below logic is only for such scenarios
				if args:
					precision = get_currency_precision()
					for arg in args:
						# Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount`
						if (
							flt(arg.get("difference_amount", 0), precision) != 0
							or flt(arg.get("exchange_gain_loss", 0), precision) != 0
						) and arg.get("difference_account"):
							party_account = arg.get("account")
							gain_loss_account = arg.get("difference_account")
							difference_amount = arg.get("difference_amount") or arg.get("exchange_gain_loss")
							if difference_amount > 0:
								dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit"
							else:
								dr_or_cr = "credit" if arg.get("party_type") == "Customer" else "debit"

							reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"

							if not self.gain_loss_journal_already_booked(
								gain_loss_account,
								difference_amount,
								self.doctype,
								self.name,
								arg.get("referenced_row"),
							):
								posting_date = arg.get("difference_posting_date") or frappe.db.get_value(
									arg.voucher_type, arg.voucher_no, "posting_date"
								)
								je = create_gain_loss_journal(
									self.company,
									posting_date,
									arg.get("party_type"),
									arg.get("party"),
									party_account,
									gain_loss_account,
									difference_amount,
									dr_or_cr,
									reverse_dr_or_cr,
									arg.get("against_voucher_type"),
									arg.get("against_voucher"),
									arg.get("idx"),
									self.doctype,
									self.name,
									arg.get("referenced_row"),
									arg.get("cost_center"),
									dimensions_dict,
									arg.get("project"),
								)
								frappe.msgprint(
									_("Exchange Gain/Loss amount has been booked through {0}").format(
										get_link_to_form("Journal Entry", je)
									)
								)

			if self.get("doctype") == "Payment Entry":
				# For Payment Entry, exchange_gain_loss field in the `references` table is the trigger for journal creation
				gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0]
				booked = []
				if gain_loss_to_book:
					[x.reference_doctype for x in gain_loss_to_book]
					[x.reference_name for x in gain_loss_to_book]
					je = qb.DocType("Journal Entry")
					jea = qb.DocType("Journal Entry Account")
					parents = (
						qb.from_(jea)
						.select(jea.parent)
						.where(
							(jea.reference_type == "Payment Entry")
							& (jea.reference_name == self.name)
							& (jea.docstatus == 1)
						)
						.run()
					)

					booked = []
					if parents:
						booked = (
							qb.from_(je)
							.inner_join(jea)
							.on(je.name == jea.parent)
							.select(jea.reference_type, jea.reference_name, jea.reference_detail_no)
							.where(
								(je.docstatus == 1)
								& (je.name.isin(parents))
								& (je.voucher_type == "Exchange Gain or Loss")
							)
							.run()
						)

				for d in gain_loss_to_book:
					# Filter out References for which Gain/Loss is already booked
					if d.exchange_gain_loss and (
						(d.reference_doctype, d.reference_name, str(d.idx)) not in booked
					):
						if self.book_advance_payments_in_separate_party_account:
							party_account = d.account
						else:
							if self.payment_type == "Receive":
								party_account = self.paid_from
							elif self.payment_type == "Pay":
								party_account = self.paid_to

						dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"

						# Inverse debit/credit for payable accounts
						if self.is_payable_account(d.reference_doctype, party_account):
							dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"

						reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"

						gain_loss_account = frappe.get_cached_value(
							"Company", self.company, "exchange_gain_loss_account"
						)
						je = create_gain_loss_journal(
							self.company,
							args.get("difference_posting_date") if args else self.posting_date,
							self.party_type,
							self.party,
							party_account,
							gain_loss_account,
							d.exchange_gain_loss,
							dr_or_cr,
							reverse_dr_or_cr,
							d.reference_doctype,
							d.reference_name,
							d.idx,
							self.doctype,
							self.name,
							d.idx,
							self.cost_center,
							dimensions_dict,
							self.project,
						)
						frappe.msgprint(
							_("Exchange Gain/Loss amount has been booked through {0}").format(
								get_link_to_form("Journal Entry", je)
							)
						)

	def is_payable_account(self, reference_doctype, account):
		if reference_doctype == "Purchase Invoice" or (
			reference_doctype in ("Journal Entry", "Payment Entry")
			and frappe.get_cached_value("Account", account, "account_type") == "Payable"
		):
			return True
		return False

	def update_against_document_in_jv(self):
		"""
		Links invoice and advance voucher:
		        1. cancel advance voucher
		        2. split into multiple rows if partially adjusted, assign against voucher
		        3. submit advance voucher
		"""

		if self.doctype == "Sales Invoice":
			party_type = "Customer"
			party = self.customer
			party_account = self.debit_to
			dr_or_cr = "credit_in_account_currency"
		else:
			party_type = "Supplier"
			party = self.supplier
			party_account = self.credit_to
			dr_or_cr = "debit_in_account_currency"

		lst = []
		for d in self.get("advances"):
			if flt(d.allocated_amount) > 0:
				args = frappe._dict(
					{
						"voucher_type": d.reference_type,
						"voucher_no": d.reference_name,
						"voucher_detail_no": d.reference_row,
						"against_voucher_type": self.doctype,
						"against_voucher": self.name,
						"account": party_account,
						"party_type": party_type,
						"party": party,
						"is_advance": "Yes",
						"dr_or_cr": dr_or_cr,
						"unadjusted_amount": flt(d.advance_amount),
						"allocated_amount": flt(d.allocated_amount),
						"precision": d.precision("advance_amount"),
						"exchange_rate": (
							self.conversion_rate
							if self.party_account_currency != self.company_currency
							else 1
						),
						"grand_total": (
							self.base_grand_total
							if self.party_account_currency == self.company_currency
							else self.grand_total
						),
						"outstanding_amount": self.outstanding_amount,
						"difference_account": frappe.get_cached_value(
							"Company", self.company, "exchange_gain_loss_account"
						),
						"exchange_gain_loss": flt(d.get("exchange_gain_loss")),
						"difference_posting_date": d.get("difference_posting_date"),
					}
				)
				lst.append(args)

		if lst:
			from erpnext.accounts.utils import reconcile_against_document

			# pass dimension values to utility method
			active_dimensions = get_dimensions()[0]
			for x in lst:
				for dim in active_dimensions:
					if self.get(dim.fieldname):
						x.update({dim.fieldname: self.get(dim.fieldname)})
			reconcile_against_document(lst, active_dimensions=active_dimensions)

	def cancel_system_generated_credit_debit_notes(self):
		# Cancel 'Credit/Debit' Note Journal Entries, if found.
		if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
			voucher_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
			journals = frappe.db.get_all(
				"Journal Entry",
				filters={
					"is_system_generated": 1,
					"reference_type": self.doctype,
					"reference_name": self.name,
					"voucher_type": voucher_type,
					"docstatus": 1,
				},
				pluck="name",
			)
			for x in journals:
				frappe.get_doc("Journal Entry", x).cancel()

	def on_cancel(self):
		from erpnext.accounts.doctype.bank_transaction.bank_transaction import (
			remove_from_bank_transaction,
		)
		from erpnext.accounts.utils import (
			cancel_common_party_journal,
			cancel_exchange_gain_loss_journal,
			unlink_ref_doc_from_payment_entries,
		)

		remove_from_bank_transaction(self.doctype, self.name)

		if self.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
			self.cancel_system_generated_credit_debit_notes()

			# Cancel Exchange Gain/Loss Journal before unlinking
			cancel_exchange_gain_loss_journal(self)
			cancel_common_party_journal(self)

			if frappe.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"):
				unlink_ref_doc_from_payment_entries(self)

		elif self.doctype in ["Sales Order", "Purchase Order"]:
			if frappe.get_single_value("Accounts Settings", "unlink_advance_payment_on_cancelation_of_order"):
				unlink_ref_doc_from_payment_entries(self)

			if self.doctype == "Sales Order":
				self.unlink_ref_doc_from_po()

	def unlink_ref_doc_from_po(self):
		so_items = []
		for item in self.items:
			so_items.append(item.name)

		linked_po = list(
			set(
				frappe.get_all(
					"Purchase Order Item",
					filters={
						"sales_order": self.name,
						"sales_order_item": ["in", so_items],
						"docstatus": ["<", 2],
					},
					pluck="parent",
				)
			)
		)

		if linked_po:
			frappe.db.set_value(
				"Purchase Order Item",
				{"sales_order": self.name, "sales_order_item": ["in", so_items], "docstatus": ["<", 2]},
				{"sales_order": None, "sales_order_item": None},
			)

			frappe.msgprint(_("Purchase Orders {0} are un-linked").format("\n".join(linked_po)))

	def get_tax_map(self):
		tax_map = {}
		for tax in self.get("taxes"):
			tax_map.setdefault(tax.account_head, 0.0)
			tax_map[tax.account_head] += tax.tax_amount

		return tax_map

	def get_amount_and_base_amount(self, item, enable_discount_accounting):
		amount = item.net_amount
		base_amount = item.base_net_amount

		if (
			enable_discount_accounting
			and self.get("discount_amount")
			and self.get("additional_discount_account")
		):
			# cases where distributed_discount_amount is not patched
			if not hasattr(self, "__has_distributed_discount_set"):
				self.__has_distributed_discount_set = any(
					i.distributed_discount_amount for i in self.get("items")
				)

			if not self.__has_distributed_discount_set:
				return item.amount, item.base_amount

			amount += item.distributed_discount_amount
			base_amount += flt(
				item.distributed_discount_amount * self.get("conversion_rate"),
				item.precision("distributed_discount_amount"),
			)

		return amount, base_amount

	def get_tax_amounts(self, tax, enable_discount_accounting):
		amount = tax.tax_amount_after_discount_amount
		base_amount = tax.base_tax_amount_after_discount_amount

		if (
			enable_discount_accounting
			and self.get("discount_amount")
			and self.get("additional_discount_account")
			and self.get("apply_discount_on") == "Grand Total"
		):
			amount = tax.tax_amount
			base_amount = tax.base_tax_amount

		return amount, base_amount

	def make_discount_gl_entries(self, gl_entries):
		enable_discount_accounting = cint(
			frappe.get_single_value("Selling Settings", "enable_discount_accounting")
		)

		if enable_discount_accounting:
			for item in self.get("items"):
				if item.get("discount_amount") and item.get("discount_account"):
					discount_amount = item.discount_amount * item.qty
					income_account = (
						item.income_account
						if (not item.enable_deferred_revenue or self.is_return)
						else item.deferred_revenue_account
					)

					account_currency = get_account_currency(item.discount_account)
					gl_entries.append(
						self.get_gl_dict(
							{
								"account": item.discount_account,
								"against": self.customer,
								"debit": flt(
									discount_amount * self.get("conversion_rate"),
									item.precision("discount_amount"),
								),
								"debit_in_transaction_currency": flt(
									discount_amount, item.precision("discount_amount")
								),
								"cost_center": item.cost_center,
								"project": item.project,
							},
							account_currency,
							item=item,
						)
					)

					account_currency = get_account_currency(income_account)
					gl_entries.append(
						self.get_gl_dict(
							{
								"account": income_account,
								"against": self.customer,
								"credit": flt(
									discount_amount * self.get("conversion_rate"),
									item.precision("discount_amount"),
								),
								"credit_in_transaction_currency": flt(
									discount_amount, item.precision("discount_amount")
								),
								"cost_center": item.cost_center,
								"project": item.project or self.project,
							},
							account_currency,
							item=item,
						)
					)

		if (
			(enable_discount_accounting or self.get("is_cash_or_non_trade_discount"))
			and self.get("additional_discount_account")
			and self.get("discount_amount")
		):
			gl_entries.append(
				self.get_gl_dict(
					{
						"account": self.additional_discount_account,
						"against": self.customer,
						"debit": self.base_discount_amount,
						"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company),
					},
					item=self,
				)
			)

	def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on):
		from erpnext.controllers.status_updater import get_allowance_for

		ref_wise_billed_amount = self.get_reference_wise_billed_amt(ref_dt, item_ref_dn, based_on)

		if not ref_wise_billed_amount:
			return

		total_overbilled_amt = 0.0
		overbilled_items = []
		precision = self.precision(based_on, "items")
		precision_allowance = 1 / (10**precision)

		role_allowed_to_overbill = frappe.get_single_value("Accounts Settings", "role_allowed_to_over_bill")
		is_overbilling_allowed = role_allowed_to_overbill in frappe.get_roles()

		for row in ref_wise_billed_amount.values():
			total_billed_amt = row.billed_amt
			allowance = get_allowance_for(row.item_code, {}, None, None, "amount")[0]

			max_allowed_amt = flt(row.ref_amt * (100 + allowance) / 100)

			if total_billed_amt < 0 and max_allowed_amt < 0:
				# while making debit note against purchase return entry(purchase receipt) getting overbill error
				total_billed_amt, max_allowed_amt = abs(total_billed_amt), abs(max_allowed_amt)

			overbill_amt = total_billed_amt - max_allowed_amt
			row["max_allowed_amt"] = max_allowed_amt
			total_overbilled_amt += overbill_amt

			if overbill_amt > precision_allowance and not is_overbilling_allowed:
				if self.doctype != "Purchase Invoice" or not cint(
					frappe.db.get_single_value(
						"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
					)
				):
					overbilled_items.append(row)

		if overbilled_items:
			self.throw_overbill_exception(overbilled_items, precision)

		if is_overbilling_allowed and total_overbilled_amt > 0.1:
			frappe.msgprint(
				_("Overbilling of {} ignored because you have {} role.").format(
					total_overbilled_amt, role_allowed_to_overbill
				),
				indicator="orange",
				alert=True,
			)

	def get_billing_reference_details(self, reference_names, reference_doctype, based_on):
		return frappe._dict(
			frappe.get_all(
				reference_doctype,
				filters={"name": ("in", reference_names)},
				fields=["name", based_on],
				as_list=1,
			)
		)

	def get_reference_wise_billed_amt(self, ref_dt, item_ref_dn, based_on):
		"""
		Returns Sum of Amount of
		Sales/Purchase Invoice Items
		that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`)
		that are submitted OR not submitted but are under current invoice
		"""
		reference_names = [d.get(item_ref_dn) for d in self.items if d.get(item_ref_dn)]

		if not reference_names:
			return

		ref_wise_billed_amount = {}
		precision = self.precision(based_on, "items")
		reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
		already_billed = self.get_already_billed_amount(reference_names, item_ref_dn, based_on)

		for item in self.items:
			key = item.get(item_ref_dn)
			if not key:
				continue

			ref_amt = flt(reference_details.get(key), precision)
			current_amount = flt(item.get(based_on), precision)

			if not ref_amt:
				if current_amount:  # Skip warning for free items
					frappe.msgprint(
						_(
							"System will not check over billing since amount for Item {0} in {1} is zero"
						).format(item.item_code, ref_dt),
						title=_("Warning"),
						indicator="orange",
					)
				continue

			ref_wise_billed_amount.setdefault(
				key,
				frappe._dict(item_code=item.item_code, billed_amt=0.0, ref_amt=ref_amt, rows=[]),
			)

			ref_wise_billed_amount[key]["rows"].append(item.idx)
			ref_wise_billed_amount[key]["ref_amt"] = ref_amt
			ref_wise_billed_amount[key]["billed_amt"] += current_amount
			if key in already_billed:
				ref_wise_billed_amount[key]["billed_amt"] += flt(already_billed.pop(key, 0), precision)

		return ref_wise_billed_amount

	def get_already_billed_amount(self, reference_names, item_ref_dn, based_on):
		item_doctype = frappe.qb.DocType(self.items[0].doctype)
		based_on_field = frappe.qb.Field(based_on)
		join_field = frappe.qb.Field(item_ref_dn)

		return frappe._dict(
			(
				frappe.qb.from_(item_doctype)
				.select(join_field, Sum(based_on_field))
				.where(join_field.isin(reference_names))
				.where((item_doctype.docstatus == 1) & (item_doctype.parent != self.name))
				.groupby(join_field)
			).run()
		)

	def throw_overbill_exception(self, overbilled_items, precision):
		message = (
			_("<p>Cannot overbill for the following Items:</p>")
			+ "<ul>"
			+ "".join(
				_("<li>Item {0} in row(s) {1} billed more than {2}</li>").format(
					frappe.bold(item.item_code),
					", ".join(str(x) for x in item.rows),
					frappe.bold(fmt_money(item.max_allowed_amt, precision=precision, currency=self.currency)),
				)
				for item in overbilled_items
			)
			+ "</ul>"
		)
		message += _("<p>To allow over-billing, please set allowance in Accounts Settings.</p>")

		frappe.throw(_(message))

	def get_company_default(self, fieldname, ignore_validation=False):
		from erpnext.accounts.utils import get_company_default

		return get_company_default(self.company, fieldname, ignore_validation=ignore_validation)

	def get_stock_items(self):
		stock_items = []
		item_codes = list(set(item.item_code for item in self.get("items")))
		if item_codes:
			stock_items = frappe.db.get_values(
				"Item", {"name": ["in", item_codes], "is_stock_item": 1}, pluck="name", cache=True
			)

		return stock_items

	def get_asset_items(self):
		asset_items = []
		item_codes = list(set(item.item_code for item in self.get("items")))
		if item_codes:
			asset_items = frappe.db.get_values(
				"Item", {"name": ["in", item_codes], "is_fixed_asset": 1}, pluck="name", cache=True
			)

		return asset_items

	def calculate_total_advance_from_ledger(self):
		adv = frappe.qb.DocType("Advance Payment Ledger Entry")
		return (
			qb.from_(adv)
			.select(Abs(Sum(adv.amount)).as_("amount"), adv.currency.as_("account_currency"))
			.where(adv.company == self.company)
			.where(adv.delinked == 0)
			.where(adv.against_voucher_type == self.doctype)
			.where(adv.against_voucher_no == self.name)
			.run(as_dict=True)
		)

	def set_total_advance_paid(self):
		advance = self.calculate_total_advance_from_ledger()
		advance_paid = 0

		if advance:
			advance = advance[0]

			advance_paid = flt(advance.amount, self.precision("advance_paid"))
			if advance.account_currency:
				frappe.db.set_value(
					self.doctype, self.name, "party_account_currency", advance.account_currency
				)

		self.db_set("advance_paid", advance_paid)
		self.set_advance_payment_status()

	def set_advance_payment_status(self):
		new_status = None

		PaymentRequest = frappe.qb.DocType("Payment Request")
		paid_amount = frappe.get_value(
			doctype="Payment Request",
			filters={
				"reference_doctype": self.doctype,
				"reference_name": self.name,
				"docstatus": 1,
			},
			fieldname=Sum(PaymentRequest.grand_total - PaymentRequest.outstanding_amount),
		)

		if not paid_amount:
			if self.doctype in self.get_advance_payment_doctypes(payment_type="receivable"):
				new_status = "Not Requested" if paid_amount is None else "Requested"
			elif self.doctype in self.get_advance_payment_doctypes(payment_type="payable"):
				new_status = "Not Initiated" if paid_amount is None else "Initiated"
		else:
			total_amount = self.get("rounded_total") or self.get("grand_total")
			new_status = "Fully Paid" if paid_amount == total_amount else "Partially Paid"

		if new_status == self.advance_payment_status:
			return

		self.db_set("advance_payment_status", new_status, update_modified=False)
		self.set_status(update=True)
		self.notify_update()

	@property
	def company_abbr(self):
		if not hasattr(self, "_abbr"):
			self._abbr = frappe.get_cached_value("Company", self.company, "abbr")

		return self._abbr

	def raise_missing_debit_credit_account_error(self, party_type, party):
		"""Raise an error if debit to/credit to account does not exist."""
		db_or_cr = (
			frappe.bold(_("Debit To")) if self.doctype == "Sales Invoice" else frappe.bold(_("Credit To"))
		)
		rec_or_pay = "Receivable" if self.doctype == "Sales Invoice" else "Payable"

		link_to_party = frappe.utils.get_link_to_form(party_type, party)
		link_to_company = frappe.utils.get_link_to_form("Company", self.company)

		message = _("{0} Account not found against Customer {1}.").format(db_or_cr, frappe.bold(party) or "")
		message += "<br>" + _("Please set one of the following:") + "<br>"
		message += (
			"<br><ul><li>"
			+ _("'Account' in the Accounting section of Customer {0}").format(link_to_party)
			+ "</li>"
		)
		message += (
			"<li>"
			+ _("'Default {0} Account' in Company {1}").format(rec_or_pay, link_to_company)
			+ "</li></ul>"
		)

		frappe.throw(message, title=_("Account Missing"), exc=AccountMissingError)

	def validate_party(self):
		party_type, party = self.get_party()
		validate_party_frozen_disabled(self.company, party_type, party)

	def validate_party_specific_items(self):
		party_type, party = self.get_party()
		if self.get("quotation_to") == "Customer":
			party = self.party_name
		if not party:
			return

		restricted_items_condition = get_restricted_items_condition(party_type, party)
		rows = self.get_rows_for_item_restrictions() if restricted_items_condition is not None else []
		if not rows:
			return

		item = frappe.qb.DocType("Item")
		restricted_items = (
			frappe.qb.from_(item)
			.select(item.name)
			.where(item.name.isin(list({row.item_code for row in rows})))
			.where(restricted_items_condition)
			.run(pluck=True)
		)
		for row in rows:
			if row.item_code in restricted_items:
				frappe.throw(
					_("Row {0}: Item {1} is not allowed for {2} {3}.").format(
						row.idx, frappe.bold(row.item_code), _(party_type), frappe.bold(party)
					),
					title=_("Item Restricted for Party"),
				)

	def get_rows_for_item_restrictions(self):
		"""Skip return rows that reverse a submitted row of the original document."""
		rows = [row for row in self.get("items") if row.item_code]
		if not (self.get("is_return") and self.get("return_against")):
			return rows

		reference_field = (
			"dn_detail" if self.doctype == "Delivery Note" else frappe.scrub(self.doctype) + "_item"
		)
		original_items = dict(
			frappe.get_all(
				f"{self.doctype} Item",
				filters={"parent": self.return_against, "docstatus": 1},
				fields=["name", "item_code"],
				as_list=True,
			)
		)
		return [
			row
			for row in rows
			if flt(row.qty) > 0 or original_items.get(row.get(reference_field)) != row.item_code
		]

	def get_party(self):
		party_type = None
		if self.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
			party_type = "Customer"

		elif self.doctype in (
			"Supplier Quotation",
			"Purchase Order",
			"Purchase Receipt",
			"Purchase Invoice",
		):
			party_type = "Supplier"

		elif self.meta.get_field("customer"):
			party_type = "Customer"

		elif self.meta.get_field("supplier"):
			party_type = "Supplier"

		party = self.get(party_type.lower()) if party_type else None

		return party_type, party

	def validate_currency(self):
		if self.get("currency"):
			party_type, party = self.get_party()
			if party_type and party:
				party_account_currency = get_party_account_currency(party_type, party, self.company)

				if (
					party_account_currency
					and party_account_currency != self.company_currency
					and self.currency != party_account_currency
				):
					frappe.throw(
						_("Accounting Entry for {0}: {1} can only be made in currency: {2}").format(
							party_type, party, party_account_currency
						),
						InvalidCurrency,
					)

				# Note: not validating with gle account because we don't have the account
				# at quotation / sales order level and we shouldn't stop someone
				# from creating a sales invoice if sales order is already created

	def validate_party_account_currency(self):
		if self.doctype not in ("Sales Invoice", "Purchase Invoice"):
			return

		if self.is_opening == "Yes":
			return

		party_type, party = self.get_party()
		party_gle_currency = get_party_gle_currency(party_type, party, self.company)
		party_account = self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to")
		party_account_currency = get_account_currency(party_account)
		allow_multi_currency_invoices_against_single_party_account = frappe.db.get_singles_value(
			"Accounts Settings", "allow_multi_currency_invoices_against_single_party_account"
		)

		if (
			not party_gle_currency
			and (party_account_currency != self.currency)
			and not allow_multi_currency_invoices_against_single_party_account
		):
			frappe.throw(
				_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
					frappe.bold(party_account), party_account_currency, self.currency
				)
			)

	def delink_advance_entries(self, linked_doc_name):
		total_allocated_amount = 0
		for adv in self.advances:
			consider_for_total_advance = True
			if adv.reference_name == linked_doc_name:
				doctype = frappe.qb.DocType(self.doctype + " Advance")
				frappe.qb.from_(doctype).delete().where(doctype.name == adv.name).run()

				consider_for_total_advance = False

			if consider_for_total_advance:
				total_allocated_amount += flt(adv.allocated_amount, adv.precision("allocated_amount"))

		frappe.db.set_value(
			self.doctype, self.name, "total_advance", total_allocated_amount, update_modified=False
		)

	def group_similar_items(self):
		grouped_items = {}
		# to update serial number in print
		count = 0

		fields_to_group = frappe.get_hooks("fields_for_group_similar_items")
		fields_to_group = set(fields_to_group)

		for item in self.items:
			item_values = grouped_items.setdefault(item.item_code, defaultdict(int))

			for field in fields_to_group:
				item_values[field] += item.get(field, 0)

		duplicate_list = []
		for item in self.items:
			if item.item_code in grouped_items:
				count += 1

				for field in fields_to_group:
					item.set(field, grouped_items[item.item_code][field])

				if item.qty:
					item.rate = flt(flt(item.amount) / flt(item.qty), item.precision("rate"))
				else:
					item.rate = 0

				item.idx = count
				del grouped_items[item.item_code]
			else:
				duplicate_list.append(item)
		for item in duplicate_list:
			self.remove(item)

	def set_payment_schedule(self):
		if (self.doctype == "Sales Invoice" and self.is_pos) or self.get("is_opening") == "Yes":
			self.payment_terms_template = ""
			return

		party_account_currency = self.get("party_account_currency")
		if not party_account_currency:
			party_type, party = self.get_party()

			if party_type and party:
				party_account_currency = get_party_account_currency(party_type, party, self.company)

		posting_date = self.get("bill_date") or self.get("posting_date") or self.get("transaction_date")
		date = self.get("due_date")
		due_date = date or posting_date

		base_grand_total = flt(self.get("base_rounded_total") or self.base_grand_total)
		grand_total = flt(self.get("rounded_total") or self.grand_total)
		automatically_fetch_payment_terms = 0

		if self.doctype in ("Sales Invoice", "Purchase Invoice", "Sales Order"):
			po_or_so, doctype, fieldname = self.get_order_details()
			automatically_fetch_payment_terms = cint(
				frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
			)
			if self.doctype != "Sales Order":
				base_grand_total = base_grand_total - flt(self.base_write_off_amount)
				grand_total = grand_total - flt(self.write_off_amount)

		if self.get("total_advance"):
			if party_account_currency == self.company_currency:
				base_grand_total -= self.get("total_advance")
				grand_total = flt(
					base_grand_total / self.get("conversion_rate"), self.precision("grand_total")
				)
			else:
				grand_total -= self.get("total_advance")
				base_grand_total = flt(
					grand_total * self.get("conversion_rate"), self.precision("base_grand_total")
				)

		if not self.get("payment_schedule"):
			if (
				self.doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order"]
				and automatically_fetch_payment_terms
				and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
			):
				self.fetch_payment_terms_from_order(
					po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
				)
				if self.get("payment_terms_template"):
					self.ignore_default_payment_terms_template = 1
			elif self.get("payment_terms_template"):
				data = _get_payment_terms(
					self.payment_terms_template, posting_date, grand_total, base_grand_total
				)
				for item in data:
					self.append("payment_schedule", item)
			elif self.doctype not in ["Purchase Receipt"]:
				data = dict(
					due_date=due_date,
					invoice_portion=100,
					payment_amount=grand_total,
					base_payment_amount=base_grand_total,
				)
				self.append("payment_schedule", data)

		allocate_payment_based_on_payment_terms = frappe.db.get_value(
			"Payment Terms Template", self.payment_terms_template, "allocate_payment_based_on_payment_terms"
		)

		if not (
			automatically_fetch_payment_terms
			and allocate_payment_based_on_payment_terms
			and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
		):
			for d in self.get("payment_schedule"):
				if d.invoice_portion:
					d.payment_amount = flt(
						grand_total * flt(d.invoice_portion) / 100, d.precision("payment_amount")
					)
					d.base_payment_amount = flt(
						base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
					)
					d.outstanding = d.payment_amount
					d.base_outstanding = d.base_payment_amount
				elif not d.invoice_portion:
					d.base_payment_amount = flt(
						d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
					)
					d.base_outstanding = d.base_payment_amount
		else:
			self.fetch_payment_terms_from_order(
				po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
			)
			self.ignore_default_payment_terms_template = 1

	def get_order_details(self):
		if not self.get("items"):
			return None, None, None
		if self.doctype == "Sales Invoice":
			prev_doc = self.get("items")[0].get("sales_order")
			prev_doctype = "Sales Order"
			prev_doctype_name = "sales_order"
		elif self.doctype == "Purchase Invoice":
			prev_doc = self.get("items")[0].get("purchase_order")
			prev_doctype = "Purchase Order"
			prev_doctype_name = "purchase_order"
		else:
			prev_doc = self.get("items")[0].get("prevdoc_docname")
			prev_doctype = "Quotation"
			prev_doctype_name = "prevdoc_docname"
		return prev_doc, prev_doctype, prev_doctype_name

	def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype):
		if po_or_so and self.all_items_have_same_po_or_so(po_or_so, fieldname):
			if self.linked_order_has_payment_terms_template(po_or_so, doctype):
				return True
			elif self.linked_order_has_payment_schedule(po_or_so):
				return True

		return False

	def all_items_have_same_po_or_so(self, po_or_so, fieldname):
		for item in self.get("items"):
			if item.get(fieldname) != po_or_so:
				return False

		return True

	def linked_order_has_payment_terms_template(self, po_or_so, doctype):
		return frappe.get_value(doctype, po_or_so, "payment_terms_template")

	def linked_order_has_payment_schedule(self, po_or_so):
		return frappe.get_all("Payment Schedule", filters={"parent": po_or_so})

	def fetch_payment_terms_from_order(
		self, po_or_so, po_or_so_doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
	):
		"""
		Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice.
		"""
		po_or_so = frappe.get_cached_doc(po_or_so_doctype, po_or_so)

		self.payment_schedule = []
		self.payment_terms_template = po_or_so.payment_terms_template
		posting_date = self.get("bill_date") or self.get("posting_date") or self.get("transaction_date")

		for schedule in po_or_so.payment_schedule:
			payment_schedule = {
				"payment_term": schedule.payment_term,
				"due_date": schedule.due_date,
				"invoice_portion": schedule.invoice_portion,
				"mode_of_payment": schedule.mode_of_payment,
				"description": schedule.description,
				"paid_amount": schedule.paid_amount,
			}

			if automatically_fetch_payment_terms:
				if schedule.due_date_based_on:
					payment_schedule["due_date"] = get_due_date(schedule, posting_date)
					payment_schedule["due_date_based_on"] = schedule.due_date_based_on
					payment_schedule["credit_days"] = cint(schedule.credit_days)
					payment_schedule["credit_months"] = cint(schedule.credit_months)

				if schedule.discount_validity_based_on and flt(schedule.discount):
					payment_schedule["discount_date"] = get_discount_date(schedule, posting_date)
					payment_schedule["discount_validity_based_on"] = schedule.discount_validity_based_on
					payment_schedule["discount_validity"] = cint(schedule.discount_validity)

				payment_schedule["payment_amount"] = flt(
					grand_total * flt(payment_schedule["invoice_portion"]) / 100,
					schedule.precision("payment_amount"),
				)
				payment_schedule["base_payment_amount"] = flt(
					base_grand_total * flt(payment_schedule["invoice_portion"]) / 100,
					schedule.precision("base_payment_amount"),
				)
				payment_schedule["outstanding"] = payment_schedule["payment_amount"]
			else:
				payment_schedule["base_payment_amount"] = flt(
					schedule.base_payment_amount * self.get("conversion_rate"),
					schedule.precision("base_payment_amount"),
				)

			if schedule.discount_type == "Percentage":
				payment_schedule["discount_type"] = schedule.discount_type
				payment_schedule["discount"] = schedule.discount

			if not schedule.invoice_portion:
				payment_schedule["payment_amount"] = schedule.payment_amount

			self.append("payment_schedule", payment_schedule)

	def set_due_date(self):
		due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]
		if due_dates:
			self.due_date = max(due_dates)

	def validate_payment_schedule_dates(self):
		dates = []
		li = []

		if self.doctype == "Sales Invoice" and self.is_pos:
			return

		for d in self.get("payment_schedule"):
			if not flt(d.discount):
				d.discount_date = None
			d.validate_from_to_dates("discount_date", "due_date")
			if self.doctype in ["Sales Order", "Quotation"] and getdate(d.due_date) < getdate(
				self.transaction_date
			):
				frappe.throw(
					_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(
						d.idx
					)
				)
			elif d.due_date in dates:
				li.append(_("{0} in row {1}").format(d.due_date, d.idx))
			dates.append(d.due_date)

		if li:
			duplicates = "<br>" + "<br>".join(li)
			frappe.throw(
				_("Rows with duplicate due dates in other rows were found: {0}").format(duplicates),
				title=_("Payment Schedule"),
			)

	def validate_payment_schedule_amount(self):
		if (self.doctype == "Sales Invoice" and self.is_pos) or self.get("is_opening") == "Yes":
			return

		party_account_currency = self.get("party_account_currency")
		if not party_account_currency:
			party_type, party = self.get_party()

			if party_type and party:
				party_account_currency = get_party_account_currency(party_type, party, self.company)

		if self.get("payment_schedule"):
			total = 0
			base_total = 0
			for d in self.get("payment_schedule"):
				total += flt(d.payment_amount, d.precision("payment_amount"))
				base_total += flt(d.base_payment_amount, d.precision("base_payment_amount"))

			base_grand_total = flt(self.get("base_rounded_total") or self.base_grand_total)
			grand_total = flt(self.get("rounded_total") or self.grand_total)

			if self.doctype in ("Sales Invoice", "Purchase Invoice"):
				base_grand_total = base_grand_total - flt(self.base_write_off_amount)
				grand_total = grand_total - flt(self.write_off_amount)

			if self.get("total_advance"):
				if party_account_currency == self.company_currency:
					base_grand_total -= self.get("total_advance")
					grand_total = flt(
						base_grand_total / self.get("conversion_rate"), self.precision("grand_total")
					)
				else:
					grand_total -= self.get("total_advance")
					base_grand_total = flt(
						grand_total * self.get("conversion_rate"), self.precision("base_grand_total")
					)

			if (
				abs(
					flt(total, self.precision("grand_total"))
					- flt(grand_total, self.precision("grand_total"))
				)
				> 0.1
				or abs(
					flt(base_total, self.precision("base_grand_total"))
					- flt(base_grand_total, self.precision("base_grand_total"))
				)
				> 0.1
			):
				frappe.throw(
					_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total")
				)

	def is_rounded_total_disabled(self):
		if self.meta.get_field("disable_rounded_total"):
			return self.disable_rounded_total
		else:
			return frappe.db.get_single_value("Global Defaults", "disable_rounded_total")

	def set_inter_company_account(self):
		"""
		Set intercompany account for inter warehouse transactions
		This account will be used in case billing company and internal customer's
		representation company is same
		"""

		if self.is_internal_transfer() and not self.unrealized_profit_loss_account:
			unrealized_profit_loss_account = frappe.get_cached_value(
				"Company", self.company, "unrealized_profit_loss_account"
			)

			if not unrealized_profit_loss_account:
				msg = _(
					"Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}"
				).format(frappe.bold(self.company))
				frappe.throw(msg)

			self.unrealized_profit_loss_account = unrealized_profit_loss_account

	def is_internal_transfer(self):
		"""
		It will an internal transfer if its an internal customer and representation
		company is same as billing company
		"""
		if self.doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
			internal_party_field = "is_internal_customer"
		elif self.doctype in ("Purchase Invoice", "Purchase Receipt", "Purchase Order"):
			internal_party_field = "is_internal_supplier"
		else:
			return False

		if self.get(internal_party_field) and (self.represents_company == self.company):
			return True

		return False

	def process_common_party_accounting(self):
		is_invoice = self.doctype in ["Sales Invoice", "Purchase Invoice"]
		if not is_invoice:
			return

		if frappe.get_single_value("Accounts Settings", "enable_common_party_accounting"):
			party_link = self.get_common_party_link()
			if party_link and self.outstanding_amount:
				self.create_advance_and_reconcile(party_link)

	def get_common_party_link(self):
		party_type, party = self.get_party()
		return frappe.db.get_value(
			doctype="Party Link",
			filters={"secondary_role": party_type, "secondary_party": party},
			fieldname=["primary_role", "primary_party"],
			as_dict=True,
		)

	def create_advance_and_reconcile(self, party_link):
		secondary_party_type, secondary_party = self.get_party()
		primary_party_type, primary_party = party_link.primary_role, party_link.primary_party

		primary_account = get_party_account(primary_party_type, primary_party, self.company)
		secondary_account = get_party_account(secondary_party_type, secondary_party, self.company)
		primary_account_currency = get_account_currency(primary_account)
		secondary_account_currency = get_account_currency(secondary_account)
		default_currency = erpnext.get_company_currency(self.company)

		# Determine if multi-currency journal entry is needed
		multi_currency = (
			primary_account_currency != default_currency or secondary_account_currency != default_currency
		)

		jv = frappe.new_doc("Journal Entry")
		jv.voucher_type = "Journal Entry"
		jv.posting_date = self.posting_date
		jv.company = self.company
		jv.remark = f"Adjustment for {self.doctype} {self.name}"
		jv.is_system_generated = True

		reconcilation_entry = frappe._dict()
		advance_entry = frappe._dict()

		reconcilation_entry.account = secondary_account
		reconcilation_entry.party_type = secondary_party_type
		reconcilation_entry.party = secondary_party
		reconcilation_entry.reference_type = self.doctype
		reconcilation_entry.reference_name = self.name
		reconcilation_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)

		advance_entry.account = primary_account
		advance_entry.party_type = primary_party_type
		advance_entry.party = primary_party
		advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
		# For returns the direction is reversed, so this entry cannot be an advance
		# (JE validation: Supplier advance must be debit, Customer advance must be credit)
		advance_entry.is_advance = "No" if self.is_return else "Yes"

		# Update dimensions
		dimensions_dict = frappe._dict()
		active_dimensions = get_dimensions()[0]
		for dim in active_dimensions:
			dimensions_dict[dim.fieldname] = self.get(dim.fieldname)

		reconcilation_entry.update(dimensions_dict)
		advance_entry.update(dimensions_dict)

		# Calculate exchange rates if necessary
		if multi_currency:
			# Exchange rates for primary and secondary accounts
			exc_rate_primary_to_default = (
				1
				if primary_account_currency == default_currency
				else get_exchange_rate(primary_account_currency, default_currency, self.posting_date)
			)
			exc_rate_secondary_to_default = (
				1
				if secondary_account_currency == default_currency
				else get_exchange_rate(secondary_account_currency, default_currency, self.posting_date)
			)
			exc_rate_secondary_to_primary = (
				1
				if secondary_account_currency == primary_account_currency
				else get_exchange_rate(
					secondary_account_currency, primary_account_currency, self.posting_date
				)
			)

			outstanding_amount = abs(self.outstanding_amount)
			os_in_default_currency = outstanding_amount * exc_rate_secondary_to_default
			os_in_primary_currency = outstanding_amount * exc_rate_secondary_to_primary

			# SI normal and PI return → reconciliation is credit; SI return and PI normal → debit
			reconciliation_is_credit = (self.doctype == "Sales Invoice") != bool(self.is_return)
			_set_je_amounts(
				reconcilation_entry, outstanding_amount, os_in_default_currency, reconciliation_is_credit
			)
			_set_je_amounts(
				advance_entry, os_in_primary_currency, os_in_default_currency, not reconciliation_is_credit
			)

			reconcilation_entry.exchange_rate = exc_rate_secondary_to_default
			advance_entry.exchange_rate = exc_rate_primary_to_default
		else:
			outstanding_amount = abs(self.outstanding_amount)
			reconciliation_is_credit = (self.doctype == "Sales Invoice") != bool(self.is_return)
			_set_je_amounts(reconcilation_entry, outstanding_amount, is_credit=reconciliation_is_credit)
			_set_je_amounts(advance_entry, outstanding_amount, is_credit=not reconciliation_is_credit)

		jv.multi_currency = multi_currency
		jv.append("accounts", reconcilation_entry)
		jv.append("accounts", advance_entry)

		jv.save()
		jv.submit()

	def check_conversion_rate(self):
		default_currency = erpnext.get_company_currency(self.company)
		if not default_currency:
			throw(_("Please enter default currency in Company Master"))

		if not self.conversion_rate:
			throw(_("Conversion rate cannot be 0"))

		if self.currency == default_currency and flt(self.conversion_rate) != 1.00:
			throw(_("Conversion rate must be 1.00 if document currency is same as company currency"))

		if self.currency != default_currency and flt(self.conversion_rate) == 1.00:
			frappe.msgprint(
				_("Conversion rate is 1.00, but document currency is different from company currency")
			)

	def check_finance_books(self, item, asset):
		if (
			len(asset.finance_books) > 1
			and not item.get("finance_book")
			and not self.get("finance_book")
			and asset.finance_books[0].finance_book
		):
			frappe.throw(
				_("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx)
			)

	def check_if_fields_updated(self, fields_to_check, child_tables):
		# Check if any field affecting accounting entry is altered
		doc_before_update = self.get_doc_before_save()
		accounting_dimensions = [*get_accounting_dimensions(), "cost_center", "project"]

		# Parent Level Accounts excluding party account
		fields_to_check += accounting_dimensions
		for field in fields_to_check:
			if doc_before_update.get(field) != self.get(field):
				return True

		# Check for child tables
		for table in child_tables:
			if check_if_child_table_updated(
				doc_before_update.get(table), self.get(table), child_tables[table]
			):
				return True

		return False

	def repost_accounting_entries(self):
		repost_ledger = frappe.new_doc("Repost Accounting Ledger")
		repost_ledger.company = self.company
		repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
		repost_ledger.flags.ignore_permissions = True
		repost_ledger.insert()
		repost_ledger.submit()

	def get_advance_payment_doctypes(self, payment_type=None) -> list:
		return _get_advance_payment_doctypes(payment_type=payment_type)

	def set_transaction_currency_and_rate_in_gl_map(self, gl_entries):
		for x in gl_entries:
			x["transaction_currency"] = self.currency
			x["transaction_exchange_rate"] = self.get("conversion_rate") or 1

	@property
	def has_mapped_discount(self):
		return any(item.get("mapped_additional_discount_amount") for item in self.get("items"))

	@property
	def has_cash_discount(self):
		return bool(self.get("is_cash_or_non_trade_discount") and self.get("discount_amount"))

	def drop_mapped_discount(self, item):
		item.mapped_additional_discount_amount = 0
		self.clear_stale_mapped_discount_total()

	def clear_stale_mapped_discount_total(self):
		if not self.has_mapped_discount:
			self.discount_amount = 0

	@property
	def is_discount_header_derived(self):
		precision = self.precision("discount_amount")
		return (
			self.get("apply_discount_on") == "Net Total"
			and not flt(self.get("additional_discount_percentage"))
			and flt(self.get("discount_amount"), precision)
			== flt(sum(flt(item.distributed_discount_amount) for item in self.items), precision)
		)

	def sync_mapped_discount_with_header(self):
		previous = self.get_doc_before_save()
		if not previous:
			return

		header_changed = any(self.has_value_changed(field) for field in ADDITIONAL_DISCOUNT_FIELDS)
		if self.has_mapped_discount and header_changed and not self.is_discount_header_derived:
			for item in self.items:
				item.mapped_additional_discount_amount = 0
		elif previous.has_mapped_discount and not header_changed:
			self.clear_stale_mapped_discount_total()

	def is_same_transaction_side(self, source_doc):
		return any(
			self.doctype in transaction_types and source_doc.doctype in transaction_types
			for transaction_types in (PURCHASE_TRANSACTION_TYPES, SALES_TRANSACTION_TYPES)
		)

	def before_mapping(self, source_doc, table_maps):
		if not self.get("items") or not self.is_same_transaction_side(source_doc):
			return

		if self.has_cash_discount:
			self.keep_cash_discount(source_doc)
		elif self.has_different_discount(source_doc):
			self.calculate_taxes_and_totals()
			self.move_additional_discount_to_items()
			self.flags.mapped_discount_item_count = len(self.items)

	def after_mapping(self, source_doc):
		cash_discount = self.flags.pop("mapped_cash_discount", None)
		existing_item_count = self.flags.pop("mapped_discount_item_count", None)
		if cash_discount:
			self.update(cash_discount)
			self.calculate_taxes_and_totals()
		elif existing_item_count is None:
			self.set_discount_amount_after_mapping(source_doc)
		else:
			self.apply_source_discount_to_mapped_items(source_doc, existing_item_count)

	def keep_cash_discount(self, source_doc):
		if source_doc.get("discount_amount") or source_doc.has_mapped_discount:
			frappe.throw(
				_("The cash or non-trade discount cannot be combined with the discount of {0}.").format(
					get_link_to_form(source_doc.doctype, source_doc.name)
				),
				title=_("Discounts Cannot Be Combined"),
			)

		self.flags.mapped_cash_discount = {field: self.get(field) for field in ADDITIONAL_DISCOUNT_FIELDS}

	def has_different_discount(self, source_doc):
		if not flt(self.get("discount_amount")) and not flt(source_doc.get("discount_amount")):
			return False

		discount_percentage = flt(self.get("additional_discount_percentage"))
		return not (
			discount_percentage
			and discount_percentage == flt(source_doc.get("additional_discount_percentage"))
			and self.get("apply_discount_on") == source_doc.get("apply_discount_on")
		)

	def apply_source_discount_to_mapped_items(self, source_doc, existing_item_count):
		if len(self.items) == existing_item_count:
			self.update({"additional_discount_percentage": 0, "discount_amount": 0})
		else:
			with self.mapped_items_only(existing_item_count):
				self.update({field: source_doc.get(field) for field in ADDITIONAL_DISCOUNT_FIELDS})
				self.set_discount_amount_after_mapping(source_doc)
				self.calculate_taxes_and_totals()
				self.move_additional_discount_to_items()

		self.calculate_taxes_and_totals()

	@contextmanager
	def mapped_items_only(self, existing_item_count):
		existing_items = self.items[:existing_item_count]
		self.set("items", self.items[existing_item_count:])
		try:
			yield
		finally:
			self.set("items", existing_items + self.items)

	def move_additional_discount_to_items(self):
		if not self.discount_amount or self.has_mapped_discount:
			return

		for item in self.items:
			stock_qty = get_qty_in_stock_uom(item)
			if stock_qty:
				item.mapped_additional_discount_amount = flt(
					flt(item.distributed_discount_amount) / stock_qty,
					item.precision("mapped_additional_discount_amount"),
				)

	def set_discount_amount_after_mapping(self, source_doc):
		"""
		Ensures that Additional Discount Amount is not copied repeatedly
		for multiple mappings of a single source transaction.
		"""

		if not self.is_same_transaction_side(source_doc):
			return

		# ensure both doctypes have discount_amount field
		if not self.meta.get_field("discount_amount") or not source_doc.meta.get_field("discount_amount"):
			return

		# ensure discount_amount is set in source doc
		if not source_doc.discount_amount:
			return

		if source_doc.has_mapped_discount:
			self.clear_stale_mapped_discount_total()
			self.calculate_taxes_and_totals()
			return

		# ensure additional_discount_percentage is not set in the source doc
		if source_doc.get("additional_discount_percentage"):
			return

		item_doctype = self.meta.get_field("items").options
		doctype_table = frappe.qb.DocType(self.doctype)
		item_table = frappe.qb.DocType(item_doctype)

		is_same_doctype = self.doctype == source_doc.doctype
		is_return = self.get("is_return") and is_same_doctype

		if is_same_doctype and not is_return:
			# should never happen
			# you don't map to the same doctype without it being a return
			return

		query = (
			frappe.qb.from_(doctype_table)
			.where(doctype_table.docstatus == 1)
			.where(doctype_table.discount_amount != 0)
			.select(Sum(doctype_table.discount_amount))
		)

		if is_return:
			query = query.where(doctype_table.is_return == 1).where(
				doctype_table.return_against == source_doc.name
			)

		else:
			item_meta = frappe.get_meta(item_doctype)
			reference_fieldname = next(
				(
					row.fieldname
					for row in item_meta.fields
					if row.fieldtype == "Link"
					and row.options == source_doc.doctype
					and not row.get("is_custom_field")
				),
				None,
			)

			if not reference_fieldname:
				return

			linked_documents = (
				frappe.qb.from_(item_table)
				.select(item_table.parent)
				.where(item_table[reference_fieldname] == source_doc.name)
				.distinct()
			)
			documents_with_mapped_discount = (
				frappe.qb.from_(item_table)
				.select(item_table.parent)
				.where(item_table.parent.isin(linked_documents))
				.where(item_table.mapped_additional_discount_amount != 0)
			)
			query = query.where(doctype_table.name.isin(linked_documents)).where(
				doctype_table.name.notin(documents_with_mapped_discount)
			)

		result = query.run()
		if not result:
			return

		discount_already_applied = flt(result[0][0])
		if not is_return:
			discount_already_applied += self.get_mapped_discount_applied(source_doc, reference_fieldname)
		if not discount_already_applied:
			return

		if is_return:
			# returns have negative discount
			discount_already_applied *= -1

		discount_amount = max(source_doc.discount_amount - discount_already_applied, 0)
		if discount_amount and is_return:
			discount_amount *= -1

		self.discount_amount = flt(discount_amount, self.precision("discount_amount"))

		self.calculate_taxes_and_totals()

	def get_mapped_discount_applied(self, source_doc, reference_fieldname):
		item_table = frappe.qb.DocType(self.meta.get_field("items").options)
		applied = flt(
			frappe.qb.from_(item_table)
			.select(
				Sum(
					item_table.mapped_additional_discount_amount
					* item_table.qty
					* item_table.conversion_factor
				)
			)
			.where(item_table.docstatus == 1)
			.where(item_table[reference_fieldname] == source_doc.name)
			.where(item_table.mapped_additional_discount_amount != 0)
			.run()[0][0]
		)

		distributed_discount = sum(flt(item.distributed_discount_amount) for item in source_doc.items)
		if not distributed_discount:
			return applied

		return applied * source_doc.discount_amount / distributed_discount


@frappe.whitelist()
def get_tax_rate(account_head):
	return frappe.get_cached_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True)


# the only doctypes a `taxes_and_charges` Link points at; `master_doctype` is caller-supplied and
# reaches get_doc()
TAX_MASTER_DOCTYPES = ("Sales Taxes and Charges Template", "Purchase Taxes and Charges Template")


def validate_tax_master(master_doctype, master_name=None):
	"""Reject a caller-supplied doctype that is not a tax template.

	`master_name` is accepted so the call sites read the same as on develop, where it also narrows
	the caller to their permitted companies. There is no Company Restriction on this branch.
	"""
	if master_doctype not in TAX_MASTER_DOCTYPES:
		frappe.throw(_("Invalid tax master doctype"), frappe.PermissionError)


@frappe.whitelist()
def get_default_taxes_and_charges(master_doctype, tax_template=None, company=None):
	default = _get_default_taxes_and_charges(master_doctype, tax_template, company)
	if default and default.get("taxes_and_charges"):
		require_permission(master_doctype, default["taxes_and_charges"], "select")
	return default


def _get_default_taxes_and_charges(master_doctype, tax_template=None, company=None):
	if not company:
		return {}

	validate_tax_master(master_doctype, tax_template)

	if tax_template and company:
		tax_template_company = frappe.get_cached_value(master_doctype, tax_template, "company")
		if tax_template_company == company:
			return

	default_tax = frappe.db.get_value(master_doctype, {"is_default": 1, "company": company})

	return {
		"taxes_and_charges": default_tax,
		"taxes": _get_taxes_and_charges(master_doctype, default_tax),
	}


@frappe.whitelist()
def get_taxes_and_charges(master_doctype, master_name):
	if master_name:
		require_permission(master_doctype, master_name, "select")
	return _get_taxes_and_charges(master_doctype, master_name)


def _get_taxes_and_charges(master_doctype, master_name):
	if not master_name:
		return

	validate_tax_master(master_doctype, master_name)

	from frappe.model import child_table_fields, default_fields

	tax_master = frappe.get_doc(master_doctype, master_name)

	taxes_and_charges = []
	for _i, tax in enumerate(tax_master.get("taxes")):
		tax = tax.as_dict()

		for fieldname in default_fields + child_table_fields:
			if fieldname in tax:
				del tax[fieldname]

		taxes_and_charges.append(tax)

	return taxes_and_charges


def validate_conversion_rate(currency, conversion_rate, conversion_rate_label, company):
	"""common validation for currency and price list currency"""

	company_currency = frappe.get_cached_value("Company", company, "default_currency")

	if not conversion_rate:
		throw(
			_("{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}.").format(
				conversion_rate_label, currency, company_currency
			)
		)


def validate_taxes_and_charges(tax):
	if tax.charge_type in ["Actual", "On Net Total", "On Paid Amount"] and tax.row_id:
		frappe.throw(
			_("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'")
		)
	elif tax.charge_type in ["On Previous Row Amount", "On Previous Row Total"]:
		if cint(tax.idx) == 1:
			frappe.throw(
				_(
					"Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"
				)
			)
		elif not tax.row_id:
			frappe.throw(
				_("Please specify a valid Row ID for row {0} in table {1}").format(tax.idx, _(tax.doctype))
			)
		elif tax.row_id and cint(tax.row_id) >= cint(tax.idx):
			frappe.throw(
				_("Cannot refer row number greater than or equal to current row number for this Charge type")
			)

	if tax.charge_type == "Actual":
		tax.rate = None


def validate_account_head(idx: int, account: str, company: str, context: str | None = None) -> None:
	"""Throw a ValidationError if the account belongs to a different company or is a group account."""
	if company != frappe.get_cached_value("Account", account, "company"):
		frappe.throw(
			_("Row {0}: The {3} Account {1} does not belong to the company {2}").format(
				idx, frappe.bold(account), frappe.bold(company), context or ""
			),
			title=_("Invalid Account"),
		)

	if frappe.get_cached_value("Account", account, "is_group"):
		frappe.throw(
			_(
				"You selected the account group {1} as {2} Account in row {0}. Please select a single account."
			).format(idx, frappe.bold(account), context or ""),
			title=_("Invalid Account"),
		)


def validate_cost_center(tax, doc):
	if not tax.cost_center:
		return

	company = frappe.get_cached_value("Cost Center", tax.cost_center, "company")

	if company != doc.company:
		frappe.throw(
			_("Row {0}: Cost Center {1} does not belong to Company {2}").format(
				tax.idx, frappe.bold(tax.cost_center), frappe.bold(doc.company)
			),
			title=_("Invalid Cost Center"),
		)


def validate_inclusive_tax(tax, doc):
	def _on_previous_row_error(row_range):
		throw(
			_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(
				tax.idx, row_range
			)
		)

	if cint(getattr(tax, "included_in_print_rate", None)):
		if tax.charge_type == "Actual":
			# inclusive tax cannot be of type Actual
			throw(
				_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(
					tax.idx
				)
			)
		elif tax.charge_type == "On Previous Row Amount" and not cint(
			doc.get("taxes")[cint(tax.row_id) - 1].included_in_print_rate
		):
			# referred row should also be inclusive
			_on_previous_row_error(tax.row_id)
		elif tax.charge_type == "On Previous Row Total" and not all(
			[cint(t.included_in_print_rate) for t in doc.get("taxes")[: cint(tax.row_id)]]
		):
			# all rows about the referred tax should be inclusive
			_on_previous_row_error("1 - %d" % (tax.row_id,))
		elif tax.get("category") == "Valuation":
			frappe.throw(_("Valuation type charges can not be marked as Inclusive"))


def set_balance_in_account_currency(
	gl_dict, account_currency=None, conversion_rate=None, company_currency=None
):
	if (not conversion_rate) and (account_currency != company_currency):
		frappe.throw(
			_("Account: {0} with currency: {1} can not be selected").format(gl_dict.account, account_currency)
		)

	gl_dict["account_currency"] = account_currency

	# set debit/credit in account currency if not provided
	if flt(gl_dict.debit) and not flt(gl_dict.debit_in_account_currency):
		gl_dict.debit_in_account_currency = (
			gl_dict.debit if account_currency == company_currency else flt(gl_dict.debit / conversion_rate, 2)
		)

	if flt(gl_dict.credit) and not flt(gl_dict.credit_in_account_currency):
		gl_dict.credit_in_account_currency = (
			gl_dict.credit
			if account_currency == company_currency
			else flt(gl_dict.credit / conversion_rate, 2)
		)


def get_advance_journal_entries(
	party_type,
	party,
	party_account,
	amount_field,
	order_doctype,
	order_list,
	include_unallocated=True,
):
	journal_entry = frappe.qb.DocType("Journal Entry")
	journal_acc = frappe.qb.DocType("Journal Entry Account")
	q = (
		frappe.qb.from_(journal_entry)
		.inner_join(journal_acc)
		.on(journal_entry.name == journal_acc.parent)
		.select(
			ConstantColumn("Journal Entry").as_("reference_type"),
			(journal_entry.name).as_("reference_name"),
			(journal_entry.remark).as_("remarks"),
			(journal_acc[amount_field]).as_("amount"),
			(journal_acc.name).as_("reference_row"),
			(journal_acc.reference_name).as_("against_order"),
			(journal_acc.exchange_rate),
		)
		.where(
			journal_acc.account.isin(party_account)
			& (journal_acc.party_type == party_type)
			& (journal_acc.party == party)
			& (journal_acc.is_advance == "Yes")
			& (journal_entry.docstatus == 1)
		)
	)
	if party_type == "Customer":
		q = q.where(journal_acc.credit_in_account_currency > 0)

	else:
		q = q.where(journal_acc.debit_in_account_currency > 0)

	reference_or_condition = []

	if include_unallocated:
		reference_or_condition.append(journal_acc.reference_name.isnull())
		reference_or_condition.append(journal_acc.reference_name == "")

	if order_list:
		reference_or_condition.append(
			(journal_acc.reference_type == order_doctype) & ((journal_acc.reference_name).isin(order_list))
		)

	if reference_or_condition:
		q = q.where(Criterion.any(reference_or_condition))

	q = q.orderby(journal_entry.posting_date)

	journal_entries = q.run(as_dict=True)
	return list(journal_entries)


@erpnext.allow_regional
def get_advance_payment_entries_for_regional(*args, **kwargs):
	return get_advance_payment_entries(*args, **kwargs)


def get_advance_payment_entries(
	party_type,
	party,
	party_account,
	order_doctype,
	order_list=None,
	default_advance_account=None,
	include_unallocated=True,
	against_all_orders=False,
	limit=None,
	condition=None,
):
	payment_entries = []
	payment_entry = frappe.qb.DocType("Payment Entry")

	if order_list or against_all_orders:
		q = get_common_query(
			party_type,
			party,
			party_account,
			default_advance_account,
			limit,
			condition,
		)
		payment_ref = frappe.qb.DocType("Payment Entry Reference")

		q = q.inner_join(payment_ref).on(payment_entry.name == payment_ref.parent)
		q = q.select(
			(payment_ref.allocated_amount).as_("amount"),
			(payment_ref.name).as_("reference_row"),
			(payment_ref.reference_name).as_("against_order"),
			(payment_entry.book_advance_payments_in_separate_party_account),
		)

		q = q.where(payment_ref.reference_doctype == order_doctype)
		if order_list:
			q = q.where(payment_ref.reference_name.isin(order_list))

		allocated = list(q.run(as_dict=True))
		payment_entries += allocated
	if include_unallocated:
		q = get_common_query(
			party_type,
			party,
			party_account,
			default_advance_account,
			limit,
			condition,
		)
		q = q.select((payment_entry.unallocated_amount).as_("amount"))
		q = q.where(payment_entry.unallocated_amount > 0)

		unallocated = list(q.run(as_dict=True))
		payment_entries += unallocated
	return payment_entries


def get_common_query(
	party_type,
	party,
	party_account,
	default_advance_account,
	limit,
	condition,
):
	account_type = frappe.db.get_value("Party Type", party_type, "account_type")
	payment_type = "Receive" if account_type == "Receivable" else "Pay"
	payment_entry = frappe.qb.DocType("Payment Entry")

	q = (
		frappe.qb.from_(payment_entry)
		.select(
			ConstantColumn("Payment Entry").as_("reference_type"),
			(payment_entry.name).as_("reference_name"),
			payment_entry.posting_date,
			(payment_entry.remarks).as_("remarks"),
			(payment_entry.book_advance_payments_in_separate_party_account),
		)
		.where(payment_entry.payment_type == payment_type)
		.where(payment_entry.party_type == party_type)
		.where(payment_entry.party == party)
		.where(payment_entry.docstatus == 1)
	)

	field = "paid_from" if payment_type == "Receive" else "paid_to"

	q = q.select((payment_entry[f"{field}_account_currency"]).as_("currency"))
	q = q.select(payment_entry[field])
	account_condition = payment_entry[field].isin(party_account)
	if default_advance_account:
		q = q.where(
			account_condition
			| (
				(payment_entry[field] == default_advance_account)
				& (payment_entry.book_advance_payments_in_separate_party_account == 1)
			)
		)

	else:
		q = q.where(account_condition)

	if payment_type == "Receive":
		q = q.select((payment_entry.source_exchange_rate).as_("exchange_rate"))
	else:
		q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate"))

	if condition:
		# conditions should be built as an array and passed as Criterion
		common_filter_conditions = []

		common_filter_conditions.append(payment_entry.company == condition["company"])
		if condition.get("name", None):
			common_filter_conditions.append(payment_entry.name.like(f"%{condition.get('name')}%"))

		if condition.get("from_payment_date"):
			common_filter_conditions.append(payment_entry.posting_date.gte(condition["from_payment_date"]))

		if condition.get("to_payment_date"):
			common_filter_conditions.append(payment_entry.posting_date.lte(condition["to_payment_date"]))

		if condition.get("get_payments") is True:
			if condition.get("cost_center"):
				common_filter_conditions.append(payment_entry.cost_center == condition["cost_center"])

			if condition.get("accounting_dimensions"):
				apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions")
				for field, val in condition.get("accounting_dimensions").items():
					if isinstance(val, list | tuple | set):
						value_condition = payment_entry[field].isin(val)
						if apply_strict_user_permissions:
							common_filter_conditions.append(value_condition)
						else:
							common_filter_conditions.append(
								(IfNull(payment_entry[field], "") == "") | value_condition
							)
					else:
						common_filter_conditions.append(payment_entry[field] == val)

			if condition.get("minimum_payment_amount"):
				common_filter_conditions.append(
					payment_entry.unallocated_amount.gte(condition["minimum_payment_amount"])
				)

			if condition.get("maximum_payment_amount"):
				common_filter_conditions.append(
					payment_entry.unallocated_amount.lte(condition["maximum_payment_amount"])
				)
		q = q.where(Criterion.all(common_filter_conditions))

	q = q.orderby(payment_entry.posting_date)
	q = q.limit(limit) if limit else q

	return q


def update_invoice_status():
	"""Updates status as Overdue for applicable invoices. Runs daily."""
	today = getdate()
	payment_schedule = frappe.qb.DocType("Payment Schedule")
	for doctype in ("Sales Invoice", "Purchase Invoice"):
		invoice = frappe.qb.DocType(doctype)

		consider_base_amount = invoice.party_account_currency != invoice.currency
		payment_amount = (
			frappe.qb.terms.Case()
			.when(consider_base_amount, payment_schedule.base_payment_amount)
			.else_(payment_schedule.payment_amount)
		)

		payable_amount = (
			frappe.qb.from_(payment_schedule)
			.select(Sum(payment_amount))
			.where((payment_schedule.parent == invoice.name) & (payment_schedule.due_date < today))
		)

		total = (
			frappe.qb.terms.Case()
			.when(invoice.disable_rounded_total, invoice.grand_total)
			.else_(invoice.rounded_total)
		)

		base_total = (
			frappe.qb.terms.Case()
			.when(invoice.disable_rounded_total, invoice.base_grand_total)
			.else_(invoice.base_rounded_total)
		)

		total_amount = frappe.qb.terms.Case().when(consider_base_amount, base_total).else_(total)

		is_overdue = total_amount - invoice.outstanding_amount < payable_amount

		conditions = (
			(invoice.docstatus == 1)
			& (invoice.outstanding_amount > 0)
			& (invoice.status.like("Unpaid%") | invoice.status.like("Partly Paid%"))
			& (
				((invoice.is_pos & invoice.due_date < today) | is_overdue)
				if doctype == "Sales Invoice"
				else is_overdue
			)
		)

		status = (
			frappe.qb.terms.Case()
			.when(invoice.status.like("%Discounted"), "Overdue and Discounted")
			.else_("Overdue")
		)

		frappe.qb.update(invoice).set("status", status).where(conditions).run()


@frappe.whitelist()
def get_payment_terms(
	terms_template: str | None,
	posting_date: str | None = None,
	grand_total: float | int | str | None = None,
	base_grand_total: float | int | str | None = None,
	bill_date: str | None = None,
):
	if not terms_template:
		return

	terms_template = cstr(terms_template)
	if not frappe.has_permission("Payment Terms Template", "read", doc=terms_template):
		_refuse()

	return _get_payment_terms(terms_template, posting_date, grand_total, base_grand_total, bill_date)


def _get_payment_terms(
	terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None
):
	if not terms_template:
		return

	terms_doc = frappe.get_doc("Payment Terms Template", terms_template)

	schedule = []
	for d in terms_doc.get("terms"):
		term_details = get_payment_term_details(d, posting_date, grand_total, base_grand_total, bill_date)
		schedule.append(term_details)

	return schedule


@frappe.whitelist()
def get_payment_term_details(
	term: str | _dict | Document,
	posting_date: str | None = None,
	grand_total: float | int | str | None = None,
	base_grand_total: float | int | str | None = None,
	bill_date: str | None = None,
):
	term_details = frappe._dict()
	if isinstance(term, str):
		if not term or not frappe.has_permission("Payment Term", "select", doc=term):
			_refuse()
		term = frappe.get_doc("Payment Term", term)
	elif not hasattr(term, "payment_term"):
		_refuse()
	else:
		term_details.payment_term = term.payment_term

	fields_to_copy = [
		"description",
		"invoice_portion",
		"discount_type",
		"discount",
		"mode_of_payment",
		"due_date_based_on",
		"credit_days",
		"credit_months",
		"discount_validity_based_on",
		"discount_validity",
	]

	for field in fields_to_copy:
		term_details[field] = term.get(field)

	term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
	term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100
	term_details.outstanding = term_details.payment_amount
	term_details.base_outstanding = term_details.base_payment_amount

	has_discount = flt(term.get("discount"))
	date = bill_date or posting_date
	if date:
		term_details.due_date = get_due_date(term, date)
		term_details.discount_date = get_discount_date(term, date) if has_discount else None

	if getdate(term_details.due_date) < getdate(posting_date):
		term_details.due_date = posting_date

	return term_details


def get_due_date(term, posting_date=None, bill_date=None):
	due_date = None
	date = bill_date or posting_date
	if term.due_date_based_on == "Day(s) after invoice date":
		due_date = add_days(date, cint(term.credit_days))
	elif term.due_date_based_on == "Day(s) after the end of the invoice month":
		due_date = add_days(get_last_day(date), cint(term.credit_days))
	elif term.due_date_based_on == "Month(s) after the end of the invoice month":
		due_date = get_last_day(add_months(date, cint(term.credit_months)))
	return due_date


def get_discount_date(term, posting_date=None, bill_date=None):
	discount_validity = None
	date = bill_date or posting_date
	if term.discount_validity_based_on == "Day(s) after invoice date":
		discount_validity = add_days(date, cint(term.discount_validity))
	elif term.discount_validity_based_on == "Day(s) after the end of the invoice month":
		discount_validity = add_days(get_last_day(date), cint(term.discount_validity))
	elif term.discount_validity_based_on == "Month(s) after the end of the invoice month":
		discount_validity = get_last_day(add_months(date, cint(term.discount_validity)))
	return discount_validity


def get_supplier_block_status(party_name):
	"""
	Returns a dict containing the values of `on_hold`, `release_date` and `hold_type` of
	a `Supplier`
	"""
	supplier = frappe.get_doc("Supplier", party_name)
	info = {
		"on_hold": supplier.on_hold,
		"release_date": supplier.release_date,
		"hold_type": supplier.hold_type,
	}
	return info


def set_child_tax_template_and_map(item, child_item, parent_doc):
	ctx = ItemDetailsCtx(
		{
			"item_code": item.item_code,
			"posting_date": parent_doc.transaction_date,
			"tax_category": parent_doc.get("tax_category"),
			"company": parent_doc.get("company"),
			"base_net_rate": item.get("base_net_rate"),
		}
	)

	item_tax_template = _get_item_tax_template(ctx, item.taxes)

	if not item_tax_template:
		item_tax_template = _get_item_tax_template_from_item_group(ctx, item.item_group)

	child_item.item_tax_template = item_tax_template
	child_item.item_tax_rate = get_item_tax_map(
		doc=parent_doc,
		tax_template=child_item.item_tax_template,
		as_json=True,
	)


def _set_je_amounts(entry, amount, default_amount=None, is_credit=True):
	if is_credit:
		entry.credit_in_account_currency = amount
		if default_amount is not None:
			entry.credit = default_amount
	else:
		entry.debit_in_account_currency = amount
		if default_amount is not None:
			entry.debit = default_amount


def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True):
	add_taxes_from_item_tax_template = frappe.get_single_value(
		"Accounts Settings", "add_taxes_from_item_tax_template"
	)

	if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
		tax_map = json.loads(child_item.get("item_tax_rate"))
		for tax_type, tax_rate in tax_map.items():
			if tax_rate == NOT_APPLICABLE_TAX:
				continue

			tax_rate = flt(tax_rate)
			taxes = parent_doc.get("taxes") or []
			# add new row for tax head only if missing
			found = any(tax.account_head == tax_type for tax in taxes)
			if not found:
				tax_row = parent_doc.append("taxes", {})
				tax_row.update(
					{
						"description": str(tax_type).split(" - ")[0],
						"charge_type": "On Net Total",
						"account_head": tax_type,
						"rate": tax_rate,
						"set_by_item_tax_template": 1,
					}
				)
				if parent_doc.doctype == "Purchase Order":
					tax_row.update({"category": "Total", "add_deduct_tax": "Add"})
				if db_insert:
					tax_row.db_insert()


def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, trans_item):
	"""
	Returns a Sales/Purchase Order Item child item containing the default values
	"""
	p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
	child_item = frappe.new_doc(child_doctype, parent_doc=p_doc, parentfield=child_docname)
	item = frappe.get_doc("Item", trans_item.get("item_code"))

	for field in ("item_code", "item_name", "description", "item_group"):
		child_item.update({field: item.get(field)})

	date_fieldname = "delivery_date" if child_doctype == "Sales Order Item" else "schedule_date"
	child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)})
	child_item.stock_uom = item.stock_uom
	child_item.uom = trans_item.get("uom") or item.stock_uom
	child_item.warehouse = get_new_child_item_warehouse(p_doc, item, trans_item, child_doctype)
	conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
	child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
	child_item.update(get_bin_details(child_item.item_code, child_item.warehouse, p_doc.get("company")))

	if child_doctype in ["Purchase Order Item", "Supplier Quotation Item"]:
		# Initialized value will update in parent validation
		child_item.base_rate = 1
		child_item.base_amount = 1

	set_child_tax_template_and_map(item, child_item, p_doc)
	add_taxes_from_tax_template(child_item, p_doc)
	return child_item


def get_new_child_item_warehouse(p_doc, item, trans_item: dict, child_doctype: str) -> str | None:
	"""Return the warehouse picked in the Update Items dialog, else the configured default.

	Validates whichever warehouse was resolved, since a submitted parent skips validate().
	"""
	warehouse = trans_item.get("warehouse") or get_item_warehouse_(p_doc, item, overwrite_warehouse=True)

	if not warehouse:
		if is_warehouse_required_for_new_child_item(child_doctype, item, trans_item):
			frappe.throw(
				_(
					"Cannot find a default warehouse for item {0}. Please select one in the Update Items dialog, or set a default in the Item Master or in the Company."
				).format(frappe.bold(item.item_code))
			)
		return None

	validate_warehouse_company(warehouse, p_doc.company)
	validate_disabled_warehouse(warehouse)
	is_group_warehouse(warehouse)
	return warehouse


def is_warehouse_required_for_new_child_item(child_doctype: str, item, trans_item: dict) -> bool:
	"""Sales Order always needs one; buying documents only for stock rows, as in validate_stock_item_warehouse."""
	if child_doctype == "Sales Order Item":
		return True

	if child_doctype in ("Purchase Order Item", "Supplier Quotation Item"):
		return bool(item.is_stock_item and flt(trans_item.get("qty")) and not item.delivered_by_supplier)

	return False


def get_rows_with_kept_reservation(parent):
	"""Rows of a picked Sales Order whose stock stays reserved, including Packed Items."""
	if parent.doctype != "Sales Order" or not flt(parent.per_picked):
		return set()

	from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
		get_sre_reserved_qty_details_for_voucher,
	)

	bundle_rows = {d.name: d.parent_detail_docname for d in parent.get("packed_items")}
	reserved = get_sre_reserved_qty_details_for_voucher(parent.doctype, parent.name)
	return {bundle_rows.get(row, row) for row in reserved}


def validate_child_on_delete(row, parent, ordered_item=None):
	"""Check if partially transacted item (row) is being deleted."""
	if parent.doctype == "Sales Order":
		if flt(row.delivered_qty):
			frappe.throw(
				_("Row #{0}: Cannot delete item {1} which has already been delivered").format(
					row.idx, row.item_code
				)
			)
		if flt(row.work_order_qty):
			frappe.throw(
				_("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format(
					row.idx, row.item_code
				)
			)
		if flt(row.ordered_qty):
			frappe.throw(
				_(
					"Row #{0}: Cannot delete item {1} which is already ordered against this Sales Order."
				).format(row.idx, row.item_code)
			)

	if parent.doctype == "Purchase Order" and flt(row.received_qty):
		frappe.throw(
			_("Row #{0}: Cannot delete item {1} which has already been received").format(
				row.idx, row.item_code
			)
		)
	if parent.doctype in ["Purchase Order", "Sales Order"]:
		if flt(row.billed_amt):
			frappe.throw(
				_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
					row.idx, row.item_code
				)
			)

	if parent.doctype == "Quotation":
		if ordered_item.get(row.name):
			frappe.throw(_("Cannot delete an item which has been ordered"))


def update_bin_on_delete(row, doctype):
	"""Update bin for deleted item (row)."""
	from erpnext.stock.stock_balance import (
		get_indented_qty,
		get_ordered_qty,
		get_reserved_qty,
		update_bin_qty,
	)

	qty_dict = {}

	if doctype == "Sales Order":
		qty_dict["reserved_qty"] = get_reserved_qty(row.item_code, row.warehouse)
	else:
		if row.material_request_item:
			qty_dict["indented_qty"] = get_indented_qty(row.item_code, row.warehouse)

		qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse)

	if row.warehouse:
		update_bin_qty(row.item_code, row.warehouse, qty_dict)


def validate_blanket_order_changes(parent, data) -> None:
	if parent.doctype not in ("Sales Order", "Purchase Order"):
		return

	new_rows = {d.get("docname"): d for d in data if d.get("docname")}
	precision = frappe.get_precision(parent.doctype + " Item", "stock_qty")
	changes = defaultdict(float)
	for row in parent.items:
		if not (row.blanket_order and row.against_blanket_order):
			continue

		new_row = new_rows.get(row.name)
		if new_row and not new_row.get("item_code"):
			continue

		new_stock_qty = 0.0
		if new_row:
			conversion_factor = flt(new_row.get("conversion_factor")) or flt(row.conversion_factor) or 1
			new_stock_qty = flt(flt(new_row.get("qty")) * conversion_factor, precision)

		changes[(row.blanket_order, row.item_code)] += new_stock_qty - flt(row.stock_qty)

	for (blanket_order, item_code), change in sorted(changes.items()):
		if change > 0:
			frappe.get_doc("Blanket Order", blanket_order, for_update=True).validate_qty_can_be_ordered(
				{item_code: change}, parent.doctype
			)


def validate_and_delete_children(parent, data, ordered_item=None, rows_with_kept_reservation=None) -> bool:
	deleted_children = []
	updated_item_names = [d.get("docname") for d in data]
	for item in parent.items:
		if item.name not in updated_item_names:
			deleted_children.append(item)

	if deleted_children:
		if rows_with_kept_reservation is None:
			rows_with_kept_reservation = get_rows_with_kept_reservation(parent)
		for row in deleted_children:
			if row.name in rows_with_kept_reservation:
				frappe.throw(
					_("Row #{0}: Unreserve stock before deleting item {1}.").format(
						row.idx, frappe.bold(row.item_code)
					),
					title=_("Stock Reserved"),
				)

	for d in deleted_children:
		validate_child_on_delete(d, parent, ordered_item)
		d.flags.ignore_permissions = True
		d.cancel()
		d.delete()

	if parent.doctype == "Purchase Order":
		parent.update_ordered_qty_in_so_for_removed_items(deleted_children)

	# need to update ordered qty in Material Request first
	# bin uses Material Request Items to recalculate & update
	if parent.doctype not in ["Quotation", "Supplier Quotation"]:
		parent.update_prevdoc_status()
		for d in deleted_children:
			update_bin_on_delete(d, parent.doctype)

	return bool(deleted_children)


@frappe.whitelist()
def update_child_qty_rate(
	parent_doctype: str, trans_items: str, parent_doctype_name: str, child_docname: str = "items"
):
	from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items
	from erpnext.selling.doctype.quotation.quotation import get_ordered_items

	def check_doc_permissions(doc, perm_type="create"):
		try:
			doc.check_permission(perm_type)
		except frappe.PermissionError:
			actions = {"create": "add", "write": "update"}

			frappe.throw(
				_("You do not have permissions to {} items in a {}.").format(
					actions[perm_type], parent_doctype
				),
				title=_("Insufficient Permissions"),
			)

	def validate_parent_status(doc):
		if doc.doctype == "Purchase Order" and doc.status in ("Closed", "On Hold", "Delivered"):
			frappe.throw(
				_("Cannot update items of Purchase Order {0} because it is {1}.").format(
					doc.name, _(doc.status)
				)
			)

	def validate_workflow_conditions(doc):
		workflow = get_workflow_name(doc.doctype)
		if not workflow:
			return

		workflow_doc = frappe.get_doc("Workflow", workflow)
		current_state = doc.get(workflow_doc.workflow_state_field)
		roles = frappe.get_roles()

		allowed = any(
			state.state == current_state and (not state.allow_edit or state.allow_edit in roles)
			for state in workflow_doc.states
		)

		if not allowed:
			frappe.throw(
				_("You are not allowed to update as per the conditions set in {} Workflow.").format(
					get_link_to_form("Workflow", workflow)
				),
				title=_("Insufficient Permissions"),
			)

	def get_new_child_item(item_row):
		child_doctype = parent_doctype + " Item"
		return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)

	def is_allowed_zero_qty():
		if parent_doctype == "Sales Order":
			return frappe.db.get_single_value("Selling Settings", "allow_zero_qty_in_sales_order") or False
		elif parent_doctype == "Purchase Order":
			return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False
		return False

	def validate_quantity_and_rate(child_item, new_data):
		if not flt(new_data.get("qty")) and not is_allowed_zero_qty():
			frappe.throw(
				_("Row #{0}:Quantity for Item {1} cannot be zero.").format(
					new_data.get("idx"), frappe.bold(new_data.get("item_code"))
				),
				title=_("Invalid Qty"),
			)

		qty_limits = {
			"Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity.")),
			"Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity.")),
		}

		if parent_doctype in qty_limits:
			qty_field, error_message = qty_limits[parent_doctype]
			if flt(new_data.get("qty")) < flt(child_item.get(qty_field)):
				frappe.throw(
					_("Row #{0}:").format(new_data.get("idx"))
					+ error_message.format(frappe.bold(new_data.get("item_code"))),
					title=_("Invalid Qty"),
				)

		if child_item.name in rows_with_kept_reservation:
			new_stock_qty = flt(new_data.get("qty")) * (
				flt(new_data.get("conversion_factor")) or flt(child_item.conversion_factor)
			)
			min_stock_qty = flt(child_item.delivered_qty) * flt(child_item.conversion_factor) + flt(
				child_item.stock_reserved_qty
			)
			# Packed Items reserve in component units, so a reserved bundle keeps its qty.
			if not flt(child_item.stock_reserved_qty):
				min_stock_qty = flt(child_item.stock_qty)
			if flt(new_stock_qty - min_stock_qty, child_item.precision("stock_qty")) < 0:
				frappe.throw(
					_(
						"Row #{0}: Cannot set quantity less than delivered and reserved quantity. Unreserve stock before reducing the quantity."
					).format(new_data.get("idx") or child_item.idx),
					title=_("Invalid Qty"),
				)

		if parent_doctype in ["Quotation", "Supplier Quotation"]:
			if (parent_doctype == "Quotation" and not ordered_items) or (
				parent_doctype == "Supplier Quotation" and not purchased_items
			):
				return

			qty_to_check = (
				ordered_items.get(child_item.name)
				if parent_doctype == "Quotation"
				else purchased_items.get(child_item.name)
			)

			if qty_to_check:
				if not rate_unchanged:
					frappe.throw(
						_(
							"Cannot update rate as item {0} is already ordered or purchased against this quotation"
						).format(frappe.bold(new_data.get("item_code")))
					)

				if flt(new_data.get("qty")) < qty_to_check:
					frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))

	def validate_uom_unchanged_after_fulfilment(child_item, new_data):
		uom_changed = new_data.get("uom") not in (None, child_item.uom)
		factor_changed = flt(new_data.get("conversion_factor")) not in (0, flt(child_item.conversion_factor))
		if not uom_changed and not factor_changed:
			return

		if child_item.name in rows_with_kept_reservation:
			frappe.throw(
				_(
					"Row #{0}: Unreserve stock before changing the UOM or conversion factor of Item {1}."
				).format(new_data.get("idx") or child_item.idx, frappe.bold(child_item.item_code)),
				title=_("Stock Reserved"),
			)

		fulfilled_qty_field = {"Sales Order": "delivered_qty", "Purchase Order": "received_qty"}.get(
			parent_doctype
		)
		if not fulfilled_qty_field or not flt(child_item.get(fulfilled_qty_field)):
			return

		frappe.throw(
			_(
				"Row #{0}: Cannot change the UOM or conversion factor of Item {1} after part of it was received or delivered."
			).format(new_data.get("idx") or child_item.idx, frappe.bold(child_item.item_code)),
			title=_("Invalid UOM"),
		)

	def should_update_supplied_items(doc) -> bool:
		"""Subcontracted PO can allow following changes *after submit*:

		1. Change rate of subcontracting - regardless of other changes.
		2. Change qty and/or add new items and/or remove items
		        Exception: Transfer/Consumption is already made, qty change not allowed.
		"""

		supplied_items_processed = any(
			item.supplied_qty or item.consumed_qty or item.returned_qty for item in doc.supplied_items
		)

		update_supplied_items = any_qty_changed or items_added_or_removed or any_conversion_factor_changed
		if update_supplied_items and supplied_items_processed:
			frappe.throw(_("Item qty can not be updated as raw materials are already processed."))

		return update_supplied_items

	def validate_fg_item_for_subcontracting(new_data, is_new):
		if is_new:
			if not new_data.get("fg_item"):
				frappe.throw(
					_("Finished Good Item is not specified for service item {0}").format(
						new_data["item_code"]
					)
				)
			else:
				is_sub_contracted_item, default_bom = frappe.db.get_value(
					"Item", new_data["fg_item"], ["is_sub_contracted_item", "default_bom"]
				)

				if not is_sub_contracted_item:
					frappe.throw(
						_("Finished Good Item {0} must be a sub-contracted item").format(new_data["fg_item"])
					)
				elif not default_bom:
					frappe.throw(_("Default BOM not found for FG Item {0}").format(new_data["fg_item"]))

		if not new_data.get("fg_item_qty"):
			frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"]))

	data = json.loads(trans_items)
	any_qty_changed = False  # updated to true if any item's qty changes
	items_added_or_removed = False  # updated to true if any new item is added or removed
	any_conversion_factor_changed = False

	parent = frappe.get_doc(parent_doctype, parent_doctype_name)
	had_mapped_discount = parent.has_mapped_discount
	rfqs_before_update = set()

	check_doc_permissions(parent, "write")
	rows_with_kept_reservation = get_rows_with_kept_reservation(parent)
	validate_parent_status(parent)
	validate_blanket_order_changes(parent, data)

	if parent_doctype == "Quotation":
		ordered_items = get_ordered_items(parent.name)
		_removed_items = validate_and_delete_children(parent, data, ordered_items)
	elif parent_doctype == "Supplier Quotation":
		rfqs_before_update = parent.get_request_for_quotations()
		purchased_items = get_purchased_items(parent.name)
		_removed_items = validate_and_delete_children(parent, data, purchased_items)
	else:
		_removed_items = validate_and_delete_children(
			parent, data, rows_with_kept_reservation=rows_with_kept_reservation
		)

	items_added_or_removed |= _removed_items

	for d in data:
		new_child_flag = False

		if not d.get("item_code"):
			# ignore empty rows
			continue

		if not d.get("docname"):
			new_child_flag = True
			items_added_or_removed = True
			check_doc_permissions(parent, "create")
			child_item = get_new_child_item(d)
		else:
			check_doc_permissions(parent, "write")
			child_item = frappe.get_doc(parent_doctype + " Item", d.get("docname"))

			prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate"))
			prev_qty, new_qty = flt(child_item.get("qty")), flt(d.get("qty"))
			prev_fg_qty, new_fg_qty = flt(child_item.get("fg_item_qty")), flt(d.get("fg_item_qty"))
			prev_con_fac, new_con_fac = (
				flt(child_item.get("conversion_factor")),
				flt(d.get("conversion_factor")),
			)
			prev_uom, new_uom = child_item.get("uom"), d.get("uom")

			if parent_doctype == "Sales Order":
				prev_date, new_date = child_item.get("delivery_date"), d.get("delivery_date")
			elif parent_doctype == "Purchase Order":
				prev_date, new_date = child_item.get("schedule_date"), d.get("schedule_date")

			prev_description, new_description = (child_item.get("description"), d.get("description"))
			description_unchanged = prev_description == new_description
			rate_unchanged = prev_rate == new_rate
			qty_unchanged = prev_qty == new_qty
			fg_qty_unchanged = prev_fg_qty == new_fg_qty
			uom_unchanged = prev_uom == new_uom
			conversion_factor_unchanged = prev_con_fac == new_con_fac
			any_conversion_factor_changed |= not conversion_factor_unchanged
			date_unchanged = (
				(prev_date == getdate(new_date) if prev_date and new_date else False)
				if parent_doctype not in ["Quotation", "Supplier Quotation"]
				else None
			)  # in case of delivery note etc
			if (
				rate_unchanged
				and qty_unchanged
				and fg_qty_unchanged
				and conversion_factor_unchanged
				and uom_unchanged
				and date_unchanged
				and description_unchanged
			):
				continue

		validate_quantity_and_rate(child_item, d)
		if not new_child_flag:
			validate_uom_unchanged_after_fulfilment(child_item, d)

		if flt(child_item.get("qty")) != flt(d.get("qty")):
			any_qty_changed = True

		if (
			parent.doctype in ["Sales Order", "Purchase Order"]
			and parent.is_subcontracted
			and not parent.get("is_old_subcontracting_flow")
		):
			validate_fg_item_for_subcontracting(d, new_child_flag)
			child_item.fg_item_qty = flt(d["fg_item_qty"])

			if new_child_flag:
				child_item.fg_item = d["fg_item"]

		child_item.qty = flt(d.get("qty"))
		child_item.description = d.get("description")
		rate_precision = child_item.precision("rate") or 2
		conv_fac_precision = child_item.precision("conversion_factor") or 2
		qty_precision = child_item.precision("qty") or 2

		prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate"))
		rate_unchanged = prev_rate == new_rate
		if not rate_unchanged and not child_item.get("qty") and is_allowed_zero_qty():
			frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price"))))
		# Amount cannot be lesser than billed amount, except for negative amounts
		row_rate = flt(d.get("rate"), rate_precision)

		if parent_doctype in ["Purchase Order", "Sales Order"]:
			amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
				row_rate * flt(d.get("qty"), qty_precision), rate_precision
			)
			if amount_below_billed_amt and row_rate > 0.0:
				frappe.throw(
					_(
						"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
					).format(child_item.idx, child_item.item_code)
				)
			else:
				child_item.rate = row_rate
		else:
			child_item.rate = row_rate

		if d.get("conversion_factor"):
			if child_item.stock_uom == child_item.uom:
				child_item.conversion_factor = 1
			else:
				child_item.conversion_factor = flt(d.get("conversion_factor"), conv_fac_precision)

		if d.get("uom"):
			child_item.uom = d.get("uom")
			conversion_factor = flt(
				get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor")
			)
			child_item.conversion_factor = (
				flt(d.get("conversion_factor"), conv_fac_precision) or conversion_factor
			)

		if child_item.get("total_weight") and child_item.get("weight_per_unit"):
			child_item.total_weight = flt(
				child_item.weight_per_unit * child_item.qty * child_item.conversion_factor,
				child_item.precision("total_weight"),
			)

		if d.get("delivery_date") and parent_doctype == "Sales Order":
			child_item.delivery_date = d.get("delivery_date")

		if d.get("schedule_date") and parent_doctype == "Purchase Order":
			child_item.schedule_date = d.get("schedule_date")

		if d.get("bom_no") and parent_doctype == "Sales Order":
			child_item.bom_no = d.get("bom_no")

		if parent_doctype in ["Sales Order", "Purchase Order"]:
			if flt(child_item.price_list_rate):
				if flt(child_item.rate) > flt(child_item.price_list_rate):
					#  if rate is greater than price_list_rate, set margin
					#  or set discount
					child_item.discount_percentage = 0
					child_item.discount_amount = 0
					child_item.margin_type = "Amount"
					child_item.margin_rate_or_amount = flt(
						child_item.rate - child_item.price_list_rate,
						child_item.precision("margin_rate_or_amount"),
					)
					child_item.rate_with_margin = child_item.rate
				else:
					child_item.margin_type = ""
					child_item.margin_rate_or_amount = 0
					child_item.rate_with_margin = child_item.price_list_rate
					child_item.discount_percentage = 0
					child_item.discount_amount = flt(child_item.rate_with_margin) - flt(child_item.rate)

		child_item.flags.ignore_validate_update_after_submit = True
		if new_child_flag:
			parent.load_from_db()
			child_item.idx = len(parent.items) + 1
			child_item.insert()
		else:
			child_item.save(ignore_permissions=True)

	parent.reload()
	if had_mapped_discount:
		parent.clear_stale_mapped_discount_total()

	parent.flags.ignore_validate_update_after_submit = True
	parent.set_qty_as_per_stock_uom()
	parent.calculate_taxes_and_totals()
	parent.set_total_in_words()
	if parent_doctype == "Sales Order" and not parent.is_subcontracted:
		make_packing_list(parent)
		parent.set_gross_profit()
	frappe.get_cached_doc("Authorization Control").validate_approving_authority(
		parent.doctype, parent.company, parent.base_grand_total
	)

	if parent_doctype != "Supplier Quotation":
		parent.set_payment_schedule()
	if parent_doctype == "Purchase Order":
		parent.validate_minimum_order_qty()
		parent.validate_budget()
		if parent.is_against_so():
			parent.update_status_updater()
	elif parent_doctype == "Sales Order":
		parent.check_credit_limit()

	# reset index of child table
	for idx, row in enumerate(parent.get(child_docname), start=1):
		row.idx = idx

	parent.save()

	if parent_doctype == "Purchase Order":
		update_last_purchase_rate(parent, is_submit=1)

		if any_qty_changed or items_added_or_removed or any_conversion_factor_changed:
			parent.update_prevdoc_status()

		parent.update_requested_qty()
		parent.update_ordered_qty()
		parent.update_ordered_and_reserved_qty()
		parent.update_receiving_percentage()

		if parent.is_subcontracted:
			if parent.is_old_subcontracting_flow:
				if should_update_supplied_items(parent):
					parent.update_reserved_qty_for_subcontract()
					parent.create_raw_materials_supplied_or_received()
				parent.save()
			else:
				if not parent.can_update_items():
					frappe.throw(
						_(
							"Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}."
						).format(frappe.bold(parent.name))
					)
	elif parent_doctype == "Sales Order":  # Sales Order
		if parent.is_subcontracted and not parent.can_update_items():
			frappe.throw(
				_(
					"Items cannot be updated as Subcontracting Inward Order(s) exist against this Subcontracted Sales Order."
				)
			)
		parent.validate_selling_price()
		parent.validate_for_duplicate_items()
		parent.validate_warehouse()
		parent.update_reserved_qty()
		parent.update_project()
		parent.update_prevdoc_status("submit")
		parent.update_delivery_status()

	elif parent_doctype == "Supplier Quotation":
		parent.update_rfq_supplier_status(parent.docstatus.is_submitted(), rfqs_before_update)

	parent.reload()
	validate_workflow_conditions(parent)

	if parent_doctype in ["Purchase Order", "Sales Order"]:
		parent.update_blanket_order()
		parent.update_billing_percentage()
		parent.set_status()

	parent.validate_uom_is_integer("uom", "qty")
	parent.validate_uom_is_integer("stock_uom", "stock_qty")

	# Cancel and Recreate Stock Reservation Entries.
	if parent_doctype == "Sales Order" and not parent.is_subcontracted:
		from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import (
			cancel_stock_reservation_entries,
			has_reserved_stock,
		)

		if parent.per_picked == 0 and has_reserved_stock(parent.doctype, parent.name):
			cancel_stock_reservation_entries(parent.doctype, parent.name)
			parent._create_stock_reservation_entries()


def check_if_child_table_updated(child_table_before_update, child_table_after_update, fields_to_check):
	fields_to_check = list(fields_to_check) + get_accounting_dimensions() + ["cost_center", "project"]

	# Check if any field affecting accounting entry is altered
	for index, item in enumerate(child_table_before_update):
		for field in fields_to_check:
			if child_table_after_update[index].get(field) != item.get(field):
				return True

	return False


def merge_taxes(source_doc, target_doc):
	tax_map = {}
	for tax in source_doc.get("taxes") or []:
		found = False
		for t in target_doc.get("taxes") or []:
			if t.account_head == tax.account_head and t.cost_center == tax.cost_center:
				t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount)
				t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount)
				tax_map[tax.name] = t
				found = True

		if not found:
			tax.charge_type = "Actual"
			tax.included_in_print_rate = 0
			tax.dont_recompute_tax = 1
			tax.row_id = None
			tax.idx = None
			tax.tax_amount = tax.tax_amount_after_discount_amount
			tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
			tax_map[tax.name] = target_doc.append("taxes", tax)

	item_map = {d._old_name: d for d in target_doc.get("items") if d.get("_old_name")}

	item_tax_details = target_doc.get("_item_wise_tax_details") or []
	for row in source_doc.get("item_wise_tax_details"):
		item = item_map.get(row.item_row)
		tax = tax_map.get(row.tax_row)
		if not (item and tax):
			continue

		item_tax_details.append(
			frappe._dict(
				item=item,
				tax=tax,
				amount=row.amount,
				rate=row.rate,
				taxable_amount=row.taxable_amount,
			)
		)

	target_doc._item_wise_tax_details = item_tax_details


@erpnext.allow_regional
def validate_regional(doc):
	pass


@erpnext.allow_regional
def validate_einvoice_fields(doc):
	pass


@erpnext.allow_regional
def update_gl_dict_with_regional_fields(doc, gl_dict):
	pass


def update_gl_dict_with_app_based_fields(doc, gl_dict):
	for method in frappe.get_hooks("update_gl_dict_with_app_based_fields", default=[]):
		frappe.get_attr(method)(doc, gl_dict)


@frappe.whitelist()
def get_missing_company_details(doctype: str, docname: str):
	from frappe.contacts.doctype.address.address import get_address_display_list

	company = frappe.db.get_value(doctype, docname, "company")
	if doctype in ["Purchase Order", "Purchase Invoice", "Request for Quotation"]:
		company_address = frappe.db.get_value(doctype, docname, "billing_address")
	else:
		company_address = frappe.db.get_value(doctype, docname, "company_address")

	company_details = frappe.get_value(
		"Company", company, ["company_logo", "website", "phone_no", "email"], as_dict=True
	)

	required_fields = [
		company_details.get("company_logo"),
		company_details.get("phone_no"),
		company_details.get("email"),
	]

	if not all(required_fields) and not frappe.has_permission("Company", "write", throw=False):
		frappe.msgprint(
			_(
				"Some required Company details are missing. You don't have permission to update them. Please contact your System Manager."
			)
		)
		return

	if not company_address and not frappe.has_permission(doctype, "write", throw=False):
		frappe.msgprint(
			_(
				"Company Address is missing. You don't have permission to update it. Please contact your System Manager."
			)
		)
		return

	address_display_list = get_address_display_list("Company", company)
	address_line = address_display_list[0].get("address_line1") if address_display_list else ""
	needs_new_company_address = not address_line

	if needs_new_company_address and not frappe.has_permission("Address", "create", throw=False):
		frappe.msgprint(
			_(
				"Company Address is missing. You don't have permission to create an Address. Please contact your System Manager."
			)
		)
		return

	required_fields.append(company_address)
	required_fields.append(address_line)

	if all(required_fields):
		return False
	return {
		"company_logo": company_details.get("company_logo"),
		"website": company_details.get("website"),
		"phone_no": company_details.get("phone_no"),
		"email": company_details.get("email"),
		"address_line": address_line,
		"company": company,
		"company_address": company_address,
		"name": docname,
	}


@frappe.whitelist()
def update_company_master_and_address(current_doctype, name, company, details):
	from frappe.utils import validate_email_address

	if not frappe.has_permission(current_doctype, "write", doc=name, throw=False):
		frappe.throw(
			_("You don't have permission to update this document. Please contact your System Manager."),
			title=_("Insufficient Permissions"),
		)

	if not frappe.has_permission("Company", "write", doc=company, throw=False):
		frappe.throw(
			_("You don't have permission to update Company details. Please contact your System Manager."),
			title=_("Insufficient Permissions"),
		)

	if isinstance(details, str):
		details = frappe.parse_json(details)

	if details.get("email"):
		validate_email_address(details.get("email"), throw=True)

	company_fields = ["company_logo", "website", "phone_no", "email"]
	company_fields_to_update = {field: details.get(field) for field in company_fields if details.get(field)}

	if company_fields_to_update:
		frappe.db.set_value("Company", company, company_fields_to_update)

	company_address = details.get("company_address")
	if details.get("address_line1"):
		if not frappe.has_permission("Address", "create", throw=False):
			frappe.throw(
				_(
					"You don't have permission to create a Company Address. Please contact your System Manager."
				),
				title=_("Insufficient Permissions"),
			)
		address_doc = frappe.get_doc(
			{
				"doctype": "Address",
				"address_title": details.get("address_title"),
				"address_type": details.get("address_type"),
				"address_line1": details.get("address_line1"),
				"address_line2": details.get("address_line2"),
				"city": details.get("city"),
				"state": details.get("state"),
				"pincode": details.get("pincode"),
				"country": details.get("country"),
				"is_your_company_address": 1,
				"links": [{"link_doctype": "Company", "link_name": company}],
			}
		)
		address_doc.insert()
		company_address = address_doc.name

	update_doc_company_address(current_doctype, name, company_address, details)


def update_doc_company_address(current_doctype, docname, company_address, details):
	if not company_address:
		return

	address_field_map = {
		"Purchase Order": ("billing_address", "billing_address_display"),
		"Purchase Invoice": ("billing_address", "billing_address_display"),
		"Sales Order": ("company_address", "company_address_display"),
		"Sales Invoice": ("company_address", "company_address_display"),
		"Delivery Note": ("company_address", "company_address_display"),
		"POS Invoice": ("company_address", "company_address_display"),
		"Quotation": ("company_address", "company_address_display"),
		"Request for Quotation": ("billing_address", "billing_address_display"),
	}

	address_field, display_field = address_field_map.get(
		current_doctype, ("company_address", "company_address_display")
	)

	current_display = frappe.db.get_value(current_doctype, docname, display_field)

	if current_display and not details.get("address_line1"):
		return

	from frappe.query_builder import DocType

	DocType = DocType(current_doctype)

	(
		frappe.qb.update(DocType)
		.set(getattr(DocType, address_field), company_address)
		.set(getattr(DocType, display_field), get_address_display(company_address))
		.where(DocType.name == docname)
	).run()


def get_qty_in_stock_uom(item) -> float:
	return flt(item.qty) * (flt(item.get("conversion_factor")) or 1)
