From c37aae0a36728fcc1a7cef80d6177c8cd6dd3162 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Thu, 25 Jun 2026 14:49:11 +0530 Subject: [PATCH] fix: rewrite item rate calculation (#56315) Co-authored-by: Harsh Patadia Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com> (cherry picked from commit cb0689bd1eb982ba275377531a459bbf4c2fecf5) # Conflicts: # erpnext/accounts/services/child_item_update.py # erpnext/controllers/taxes_and_totals.py # erpnext/public/js/controllers/taxes_and_totals.js # erpnext/public/js/controllers/transaction.js # erpnext/selling/doctype/quotation/test_quotation.py # erpnext/utilities/transaction_base.py --- .../sales_invoice/test_sales_invoice.py | 3 + .../accounts/services/child_item_update.py | 588 ++++++++++++++++++ erpnext/controllers/taxes_and_totals.py | 155 +++-- .../public/js/controllers/taxes_and_totals.js | 24 +- erpnext/public/js/controllers/transaction.js | 66 +- .../doctype/quotation/test_quotation.py | 20 +- .../doctype/sales_order/test_sales_order.py | 2 + erpnext/utilities/transaction_base.py | 285 +++++++++ 8 files changed, 1075 insertions(+), 68 deletions(-) create mode 100644 erpnext/accounts/services/child_item_update.py diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 6f51e27f532..cd397b5bb26 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1922,11 +1922,14 @@ class TestSalesInvoice(FrappeTestCase): def test_create_so_with_margin(self): si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True) price_list_rate = flt(100) * flt(si.plc_conversion_rate) + si.items[0].price_list_rate = price_list_rate si.items[0].margin_type = "Percentage" si.items[0].margin_rate_or_amount = 25 si.items[0].discount_amount = 0.0 si.items[0].discount_percentage = 0.0 + # set rate to zero, so that it is recalculated on save + si.items[0].rate = 0 si.save() self.assertEqual(si.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate)) diff --git a/erpnext/accounts/services/child_item_update.py b/erpnext/accounts/services/child_item_update.py new file mode 100644 index 00000000000..3ea050a53b4 --- /dev/null +++ b/erpnext/accounts/services/child_item_update.py @@ -0,0 +1,588 @@ +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +"""Child item update service: ChildItemUpdater class and helpers for the update_child_qty_rate API.""" + +import frappe +from frappe import _ +from frappe.model.workflow import get_workflow_name +from frappe.utils import flt, get_link_to_form, getdate + +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions +from erpnext.buying.utils import update_last_purchase_rate +from erpnext.stock.doctype.packed_item.packed_item import make_packing_list +from erpnext.stock.get_item_details import ( + get_bin_details, + get_conversion_factor, + get_item_warehouse_, +) + + +class ChildItemUpdater: + """Validates and applies item-level edits on submitted orders and quotations.""" + + def __init__(self, parent_doctype: str, parent_doctype_name: str, child_docname: str = "items"): + self.parent_doctype = parent_doctype + self.parent_doctype_name = parent_doctype_name + self.child_docname = child_docname + self.parent = frappe.get_doc(parent_doctype, parent_doctype_name) + self.allow_zero_qty = get_allow_zero_qty(parent_doctype) + self._ordered_items: dict | None = None + self._purchased_items: dict | None = None + + def update(self, trans_items: str | list) -> None: + """Process item additions, edits, and deletions from trans_items JSON.""" + from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items + from erpnext.selling.doctype.quotation.mapper import get_ordered_items + + data = frappe.parse_json(trans_items) + any_qty_changed = False + items_added_or_removed = False + any_conversion_factor_changed = False + + self._check_permissions("write") + + if self.parent_doctype == "Quotation": + self._ordered_items = get_ordered_items(self.parent.name) + items_added_or_removed |= validate_and_delete_children(self.parent, data, self._ordered_items) + elif self.parent_doctype == "Supplier Quotation": + self._purchased_items = get_purchased_items(self.parent.name) + items_added_or_removed |= validate_and_delete_children(self.parent, data, self._purchased_items) + else: + items_added_or_removed |= validate_and_delete_children(self.parent, data) + + for d in data: + new_child_flag = False + rate_unchanged = None + + if not d.get("item_code"): + continue + + if not d.get("docname"): + new_child_flag = True + items_added_or_removed = True + self._check_permissions("create") + child_item = self._get_new_child_item(d) + else: + self._check_permissions("write") + child_item = frappe.get_doc(self.parent_doctype + " Item", d.get("docname")) + + change_state = get_child_item_change_state(self.parent_doctype, child_item, d) + rate_unchanged = change_state.rate_unchanged + any_conversion_factor_changed |= not change_state.conversion_factor_unchanged + if is_child_item_unchanged(change_state): + continue + + self._validate_quantity_and_rate(child_item, d, rate_unchanged) + + if flt(child_item.get("qty")) != flt(d.get("qty")): + any_qty_changed = True + + if self.parent.doctype in ("Sales Order", "Purchase Order") and self.parent.is_subcontracted: + self._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") + update_child_item_rate_and_discount( + self.parent_doctype, child_item, d, self.allow_zero_qty, rate_unchanged=rate_unchanged + ) + update_child_item_uom_and_weight(child_item, d) + + if d.get("delivery_date") and self.parent_doctype == "Sales Order": + child_item.delivery_date = d.get("delivery_date") + + if d.get("schedule_date") and self.parent_doctype == "Purchase Order": + child_item.schedule_date = d.get("schedule_date") + + if d.get("bom_no") and self.parent_doctype == "Sales Order": + child_item.bom_no = d.get("bom_no") + + child_item.flags.ignore_validate_update_after_submit = True + if new_child_flag: + self.parent.load_from_db() + child_item.idx = len(self.parent.items) + 1 + child_item.insert() + else: + child_item.save(ignore_permissions=True) + + self._post_update(any_qty_changed, items_added_or_removed, any_conversion_factor_changed) + + def _post_update( + self, any_qty_changed: bool, items_added_or_removed: bool, any_conversion_factor_changed: bool + ) -> None: + parent = self.parent + parent.reload() + 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 self.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 self.parent_doctype != "Supplier Quotation": + from erpnext.accounts.services.payment_schedule import PaymentScheduleService + + PaymentScheduleService(parent).set_payment_schedule() + + if self.parent_doctype == "Purchase Order": + parent.validate_minimum_order_qty() + parent.validate_budget() + if parent.is_against_so(): + parent.update_status_updater() + elif self.parent_doctype == "Sales Order": + parent.check_credit_limit() + + for idx, row in enumerate(parent.get(self.child_docname), start=1): + row.idx = idx + + parent.save() + + if self.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 and 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 self.parent_doctype == "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() + + parent.reload() + self._validate_workflow() + + if self.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") + + if self.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 has_reserved_stock(parent.doctype, parent.name): + cancel_stock_reservation_entries(parent.doctype, parent.name) + if parent.per_picked == 0: + parent.create_stock_reservation_entries() + + def _check_permissions(self, perm_type: str = "create") -> None: + try: + self.parent.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], self.parent_doctype + ), + title=_("Insufficient Permissions"), + ) + + def _validate_workflow(self) -> None: + workflow = get_workflow_name(self.parent.doctype) + if not workflow: + return + + workflow_doc = frappe.get_doc("Workflow", workflow) + current_state = self.parent.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(self, item_row) -> "frappe.model.document.Document": + child_doctype = self.parent_doctype + " Item" + return set_order_defaults( + self.parent_doctype, + self.parent_doctype_name, + child_doctype, + self.child_docname, + item_row, + ) + + def _validate_quantity_and_rate(self, child_item, new_data: dict, rate_unchanged: bool | None) -> None: + if not flt(new_data.get("qty")) and not self.allow_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 self.parent_doctype in qty_limits: + qty_field, error_message = qty_limits[self.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, + title=_("Invalid Qty"), + ) + + if self.parent_doctype not in ("Quotation", "Supplier Quotation"): + return + + items_map = self._ordered_items if self.parent_doctype == "Quotation" else self._purchased_items + if not items_map: + return + + qty_to_check = items_map.get(child_item.name) + if not qty_to_check: + return + + 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_fg_item_for_subcontracting(self, new_data: dict, is_new: bool) -> None: + 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"] + ) + ) + + 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"])) + + +@frappe.whitelist() +def update_child_qty_rate( + parent_doctype: str, trans_items: str, parent_doctype_name: str, child_docname: str = "items" +) -> None: + ChildItemUpdater(parent_doctype, parent_doctype_name, child_docname).update(trans_items) + + +def set_order_defaults( + parent_doctype: str, + parent_doctype_name: str, + child_doctype: str, + child_docname: str, + trans_item: dict, +) -> "frappe.model.document.Document": + """Return a new child item populated with item master defaults.""" + from erpnext.accounts.services.taxes import add_taxes_from_tax_template, set_child_tax_template_and_map + + 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", "weight_per_unit", "weight_uom"): + 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_item_warehouse_(p_doc, item, overwrite_warehouse=True) + 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"): + child_item.base_rate = 1 + child_item.base_amount = 1 + + if child_doctype == "Sales Order Item": + child_item.warehouse = get_item_warehouse_(p_doc, item, overwrite_warehouse=True) + if not child_item.warehouse: + frappe.throw( + _( + "Cannot find a default warehouse for item {0}. Please set one in the Item Master or in Stock Settings." + ).format(frappe.bold(item.item_code)) + ) + + set_child_tax_template_and_map(item, child_item, p_doc) + add_taxes_from_tax_template(child_item, p_doc) + return child_item + + +def validate_child_on_delete(row, parent, ordered_item=None) -> None: + """Raise if a partially transacted child item 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") and 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" and ordered_item and ordered_item.get(row.name): + frappe.throw(_("Cannot delete an item which has been ordered")) + + +def update_bin_on_delete(row, doctype: str) -> None: + """Update bin quantities after a child item row is deleted.""" + 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_and_delete_children(parent, data, ordered_item=None) -> bool: + """Delete child rows not present in data; return True if any were removed.""" + updated_item_names = [d.get("docname") for d in data] + deleted_children = [item for item in parent.items if item.name not in updated_item_names] + + for d in deleted_children: + validate_child_on_delete(d, parent, ordered_item) + d.cancel() + d.delete() + + if parent.doctype == "Purchase Order": + parent.update_ordered_qty_in_so_for_removed_items(deleted_children) + + 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) + + +def get_allow_zero_qty(parent_doctype: str) -> bool: + if parent_doctype == "Sales Order": + return frappe.db.get_single_value("Selling Settings", "allow_zero_qty_in_sales_order") or False + if parent_doctype == "Purchase Order": + return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False + return False + + +def get_child_item_change_state(parent_doctype: str, child_item, new_data) -> frappe._dict: + prev_rate, new_rate = flt(child_item.get("rate")), flt(new_data.get("rate")) + prev_qty, new_qty = flt(child_item.get("qty")), flt(new_data.get("qty")) + prev_fg_qty, new_fg_qty = flt(child_item.get("fg_item_qty")), flt(new_data.get("fg_item_qty")) + prev_con_fac = flt(child_item.get("conversion_factor")) + new_con_fac = flt(new_data.get("conversion_factor")) + + if parent_doctype == "Sales Order": + prev_date, new_date = child_item.get("delivery_date"), new_data.get("delivery_date") + elif parent_doctype == "Purchase Order": + prev_date, new_date = child_item.get("schedule_date"), new_data.get("schedule_date") + else: + prev_date, new_date = None, None + + if parent_doctype in ("Quotation", "Supplier Quotation"): + date_unchanged = False + else: + prev_date = getdate(prev_date) if prev_date else None + new_date = getdate(new_date) if new_date else None + date_unchanged = prev_date == new_date + + return frappe._dict( + rate_unchanged=prev_rate == new_rate, + qty_unchanged=prev_qty == new_qty, + fg_qty_unchanged=prev_fg_qty == new_fg_qty, + uom_unchanged=child_item.get("uom") == new_data.get("uom"), + conversion_factor_unchanged=prev_con_fac == new_con_fac, + date_unchanged=date_unchanged, + description_unchanged=child_item.get("description") == new_data.get("description"), + ) + + +def is_child_item_unchanged(change_state: frappe._dict) -> bool: + return ( + change_state.rate_unchanged + and change_state.qty_unchanged + and change_state.fg_qty_unchanged + and change_state.conversion_factor_unchanged + and change_state.uom_unchanged + and change_state.date_unchanged + and change_state.description_unchanged + ) + + +def update_child_item_rate_and_discount( + parent_doctype: str, + child_item, + new_data, + allow_zero_qty: bool, + rate_unchanged: bool | None = None, +) -> None: + rate_precision = child_item.precision("rate") or 2 + qty_precision = child_item.precision("qty") or 2 + + if rate_unchanged is None: + rate_unchanged = flt(child_item.get("rate")) == flt(new_data.get("rate")) + + if not rate_unchanged and not child_item.get("qty") and allow_zero_qty: + frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price")))) + + row_rate = flt(new_data.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(new_data.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) + ) + + child_item.rate = row_rate + + if parent_doctype not in ("Sales Order", "Purchase Order") or not flt(child_item.price_list_rate): + return + + if flt(child_item.rate) > flt(child_item.price_list_rate): + 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) + + +def update_child_item_uom_and_weight(child_item, new_data) -> None: + conv_fac_precision = child_item.precision("conversion_factor") or 2 + + if new_data.get("conversion_factor"): + if child_item.stock_uom == child_item.uom: + child_item.conversion_factor = 1 + else: + child_item.conversion_factor = flt(new_data.get("conversion_factor"), conv_fac_precision) + + if new_data.get("uom"): + child_item.uom = new_data.get("uom") + conversion_factor = flt( + get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor") + ) + child_item.conversion_factor = ( + flt(new_data.get("conversion_factor"), conv_fac_precision) or conversion_factor + ) + + if 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"), + ) + + +def check_if_child_table_updated( + child_table_before_update, child_table_after_update, fields_to_check +) -> bool: + """Return True if any accounting-relevant field changed in a child table.""" + fields_to_check = list(fields_to_check) + get_accounting_dimensions() + ["cost_center", "project"] + + 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 diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index e218e9a44cb..edef7358671 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -180,16 +180,21 @@ class calculate_taxes_and_totals: self.doc.conversion_rate = flt(self.doc.conversion_rate) - def calculate_item_values(self): - if self.doc.get("is_consolidated"): + def calculate_item_rate(self, item): + if not item.price_list_rate: + remove_margin(item) + remove_discount(item) + item.rate_with_margin = 0 return - if not self.discount_amount_applied: - do_not_round_fields = ["valuation_rate", "incoming_rate", "sales_incoming_rate"] + has_pricing_rules = item.pricing_rules and not self.doc.ignore_pricing_rule + if has_pricing_rules: + remove_margin(item) - for item in self.doc.items: - self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields) + for d in get_applied_pricing_rules(item.pricing_rules): + pricing_rule = frappe.get_cached_doc("Pricing Rule", d) +<<<<<<< HEAD if item.discount_percentage == 100: item.rate = 0.0 elif item.price_list_rate: @@ -237,20 +242,73 @@ class calculate_taxes_and_totals: not item.qty and self.doc.get("is_return") and self.doc.get("doctype") != "Purchase Receipt" +======= + if not ( + pricing_rule.margin_type + and pricing_rule.margin_rate_or_amount + and ( + pricing_rule.margin_type == "Percentage" or pricing_rule.currency == self.doc.currency + ) +>>>>>>> cb0689bd1e (fix: rewrite item rate calculation (#56315)) ): - item.amount = flt(-1 * item.rate, item.precision("amount")) - elif not item.qty and self.doc.get("is_debit_note"): - item.amount = flt(item.rate, item.precision("amount")) - else: - item.amount = flt(item.rate * item.qty, item.precision("amount")) + continue - item.net_amount = item.amount + item.margin_type = pricing_rule.margin_type + item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount - self._set_in_company_currency( - item, ["price_list_rate", "rate", "net_rate", "amount", "net_amount"] - ) + item.rate_with_margin = get_rate_with_margin(item) + if item.discount_percentage > 0: + item.discount_amount = flt( + item.rate_with_margin * item.discount_percentage / 100.0, item.precision("discount_amount") + ) - item.item_tax_amount = 0.0 + calculated_rate = flt(item.rate_with_margin - item.discount_amount, item.precision("rate")) + + # if rate is 0 or pricing rules are applicable, calculated rate is preferred + if has_pricing_rules or not item.rate: + item.rate = calculated_rate + return + + # discount and margin are correct, exit early + if item.rate == calculated_rate: + return + + # item rate does not match calculated rate. prefer item rate, reset margin / discount + if item.rate > item.price_list_rate: + item.margin_type = "Amount" + item.margin_rate_or_amount = flt( + item.rate - item.price_list_rate, item.precision("margin_rate_or_amount") + ) + item.rate_with_margin = item.rate + remove_discount(item) + return + + item.rate_with_margin = item.price_list_rate + item.discount_amount = flt(item.rate_with_margin - item.rate, item.precision("discount_amount")) + item.discount_percentage = 0 + remove_margin(item) + + def calculate_item_values(self): + if self.doc.get("is_consolidated") or self.discount_amount_applied: + return + + do_not_round_fields = ["valuation_rate", "incoming_rate", "sales_incoming_rate"] + for item in self.doc.items: + self.doc.round_floats_in(item, do_not_round_fields=do_not_round_fields) + self.calculate_item_rate(item) + + item.net_rate = item.rate + if not item.qty and self.doc.get("is_return") and self.doc.get("doctype") != "Purchase Receipt": + item.amount = flt(-1 * item.rate, item.precision("amount")) + elif not item.qty and self.doc.get("is_debit_note"): + item.amount = flt(item.rate, item.precision("amount")) + else: + item.amount = flt(item.rate * item.qty, item.precision("amount")) + item.net_amount = item.amount + self._set_in_company_currency( + item, ["price_list_rate", "rate_with_margin", "rate", "net_rate", "amount", "net_amount"] + ) + item.item_tax_amount = 0.0 def _set_in_company_currency(self, doc, fields): """set values in base currency""" @@ -1024,48 +1082,6 @@ class calculate_taxes_and_totals: self.calculate_outstanding_amount() - def calculate_margin(self, item): - rate_with_margin = 0.0 - base_rate_with_margin = 0.0 - if item.price_list_rate: - if item.pricing_rules and not self.doc.ignore_pricing_rule: - has_margin = False - for d in get_applied_pricing_rules(item.pricing_rules): - pricing_rule = frappe.get_cached_doc("Pricing Rule", d) - - if pricing_rule.margin_rate_or_amount and ( - ( - pricing_rule.currency == self.doc.currency - and pricing_rule.margin_type in ["Amount", "Percentage"] - ) - or pricing_rule.margin_type == "Percentage" - ): - item.margin_type = pricing_rule.margin_type - item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount - has_margin = True - - if not has_margin: - item.margin_type = None - item.margin_rate_or_amount = 0.0 - - if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate): - item.margin_type = "Amount" - item.margin_rate_or_amount = flt( - item.rate - item.price_list_rate, item.precision("margin_rate_or_amount") - ) - item.rate_with_margin = item.rate - - elif item.margin_type and item.margin_rate_or_amount: - margin_value = ( - item.margin_rate_or_amount - if item.margin_type == "Amount" - else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100 - ) - rate_with_margin = flt(item.price_list_rate) + flt(margin_value) - base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate) - - return rate_with_margin, base_rate_with_margin - def set_item_wise_tax_breakup(self): self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc) @@ -1100,6 +1116,29 @@ class calculate_taxes_and_totals: ) +def remove_discount(item): + item.discount_percentage = 0.0 + item.discount_amount = 0.0 + + +def remove_margin(item): + item.margin_type = None + item.margin_rate_or_amount = 0.0 + + +def get_rate_with_margin(item): + if not item.margin_type: + return item.price_list_rate + + if item.margin_type == "Percentage": + return flt( + item.price_list_rate * (1 + (item.margin_rate_or_amount / 100.0)), + item.precision("rate_with_margin"), + ) + + return flt(item.price_list_rate + item.margin_rate_or_amount, item.precision("rate_with_margin")) + + def get_itemised_tax_breakup_html(doc): if not doc.taxes: return diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 0df4cabfb4f..95163af9c33 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -8,18 +8,24 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { apply_pricing_rule_on_item(item) { let effective_item_rate = item.price_list_rate; - let item_rate = item.rate; if (["Sales Order", "Quotation"].includes(item.parenttype) && item.blanket_order_rate) { effective_item_rate = item.blanket_order_rate; } + + let rate_with_margin; if (item.margin_type == "Percentage") { +<<<<<<< HEAD item.rate_with_margin = flt(effective_item_rate) + flt(effective_item_rate) * ( flt(item.margin_rate_or_amount) / 100); +======= + rate_with_margin = effective_item_rate * (1 + item.margin_rate_or_amount / 100); +>>>>>>> cb0689bd1e (fix: rewrite item rate calculation (#56315)) } else { - item.rate_with_margin = flt(effective_item_rate) + flt(item.margin_rate_or_amount); + rate_with_margin = effective_item_rate + item.margin_rate_or_amount; } - item.base_rate_with_margin = flt(item.rate_with_margin) * flt(this.frm.doc.conversion_rate); + item.rate_with_margin = flt(rate_with_margin, precision("rate_with_margin", item)); +<<<<<<< HEAD item_rate = flt(item.rate_with_margin , precision("rate", item)); if (item.discount_percentage && !item.discount_amount) { @@ -29,8 +35,20 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { if (item.discount_amount > 0) { item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item)); item.discount_percentage = 100 * flt(item.discount_amount) / flt(item.rate_with_margin); +======= + if (item.discount_percentage) { + item.discount_amount = flt( + (item.rate_with_margin * item.discount_percentage) / 100, + precision("discount_amount", item) + ); } + let item_rate = item.rate_with_margin; + if (item.discount_amount) { + item_rate = item.rate_with_margin - item.discount_amount; +>>>>>>> cb0689bd1e (fix: rewrite item rate calculation (#56315)) + } + item_rate = flt(item_rate, precision("rate", item)); frappe.model.set_value(item.doctype, item.name, "rate", item_rate); } diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 5fce7783c38..a1d2d14f118 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -14,10 +14,20 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe frappe.flags.hide_serial_batch_dialog = true; frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) { var item = frappe.get_doc(cdt, cdn); +<<<<<<< HEAD var has_margin_field = frappe.meta.has_field(cdt, 'margin_type'); +======= +>>>>>>> cb0689bd1e (fix: rewrite item rate calculation (#56315)) - frappe.model.round_floats_in(item, ["rate", "price_list_rate"]); + frappe.model.round_floats_in(item, [ + "rate", + "price_list_rate", + "margin_rate_or_amount", + "discount_amount", + "discount_percentage", + ]); +<<<<<<< HEAD if(item.price_list_rate && !item.blanket_order_rate) { if(item.rate > item.price_list_rate && has_margin_field) { // if rate is greater than price_list_rate, set margin @@ -38,11 +48,51 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } else { item.discount_percentage = 0.0; item.margin_type = ''; +======= + if (item.price_list_rate && !item.blanket_order_rate) { + const rate_with_margin = get_rate_with_margin(item); + + if (item.discount_percentage) { + item.discount_amount = flt( + (rate_with_margin * item.discount_percentage) / 100.0, + precision("discount_amount", item) + ); + } + + const calculated_rate = flt(rate_with_margin - item.discount_amount, precision("rate", item)); + + if (calculated_rate !== item.rate) { + // if rate is greater than price_list_rate, set margin + // otherwise, set discount + if (item.rate > item.price_list_rate) { + item.margin_type = "Amount"; + item.margin_rate_or_amount = flt( + item.rate - item.price_list_rate, + precision("margin_rate_or_amount", item) + ); + item.rate_with_margin = item.rate; + item.discount_amount = 0; + item.discount_percentage = 0; + } else { + item.margin_type = ""; + item.margin_rate_or_amount = 0; + item.rate_with_margin = item.price_list_rate; + item.discount_percentage = 0; + item.discount_amount = flt( + item.rate_with_margin - item.rate, + precision("discount_amount", item) + ); + } + } + } else { + item.margin_type = ""; +>>>>>>> cb0689bd1e (fix: rewrite item rate calculation (#56315)) item.margin_rate_or_amount = 0; item.rate_with_margin = 0; + item.discount_amount = 0; + item.discount_percentage = 0.0; } - item.base_rate_with_margin = item.rate_with_margin * flt(frm.doc.conversion_rate); - + me.set_in_company_currency(item, ["rate_with_margin"]); cur_frm.cscript.set_gross_profit(item); cur_frm.cscript.calculate_taxes_and_totals(); cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn); @@ -2937,3 +2987,13 @@ erpnext.set_unit_price_items_note = (frm) => { ); } }; + +function get_rate_with_margin(item) { + if (!item.margin_type) return item.price_list_rate; + + if (item.margin_type === "Percentage") { + return flt(item.price_list_rate * (1 + item.margin_rate_or_amount / 100), precision("rate", item)); + } + + return flt(item.price_list_rate + item.margin_rate_or_amount, precision("rate", item)); +} diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index d9b0b322056..51621412f38 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -395,9 +395,9 @@ class TestQuotation(FrappeTestCase): quotation.save() quotation.submit() - self.assertEqual(quotation.payment_schedule[0].payment_amount, 8906.00) + self.assertEqual(quotation.payment_schedule[0].payment_amount, 500.00) self.assertEqual(quotation.payment_schedule[0].due_date, quotation.transaction_date) - self.assertEqual(quotation.payment_schedule[1].payment_amount, 8906.00) + self.assertEqual(quotation.payment_schedule[1].payment_amount, 500.00) self.assertEqual(quotation.payment_schedule[1].due_date, add_days(quotation.transaction_date, 30)) sales_order = make_sales_order(quotation.name) @@ -417,11 +417,11 @@ class TestQuotation(FrappeTestCase): sales_order.set("taxes", []) sales_order.save() - self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00) + self.assertEqual(sales_order.payment_schedule[0].payment_amount, 500.00) self.assertEqual( getdate(sales_order.payment_schedule[0].due_date), getdate(quotation.transaction_date) ) - self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00) + self.assertEqual(sales_order.payment_schedule[1].payment_amount, 500.00) self.assertEqual( getdate(sales_order.payment_schedule[1].due_date), getdate(add_days(quotation.transaction_date, 30)), @@ -457,11 +457,23 @@ class TestQuotation(FrappeTestCase): rate_with_margin = flt((1500 * 18.75) / 100 + 1500) +<<<<<<< HEAD test_records[0]["items"][0]["price_list_rate"] = 1500 test_records[0]["items"][0]["margin_type"] = "Percentage" test_records[0]["items"][0]["margin_rate_or_amount"] = 18.75 quotation = frappe.copy_doc(test_records[0]) +======= + test_record = frappe.copy_doc(self.globalTestRecords["Quotation"][0]) + + test_record.items[0].price_list_rate = 1500 + test_record.items[0].margin_type = "Percentage" + test_record.items[0].margin_rate_or_amount = 18.75 + # set rate to zero, so that it is recalculated on save + test_record.items[0].rate = 0 + + quotation = frappe.copy_doc(test_record) +>>>>>>> cb0689bd1e (fix: rewrite item rate calculation (#56315)) quotation.transaction_date = nowdate() quotation.valid_till = add_months(quotation.transaction_date, 1) quotation.insert() diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 6f8befeb876..b5aa71931f8 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1451,6 +1451,8 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase): so.items[0].price_list_rate = price_list_rate = 100 so.items[0].margin_type = "Percentage" so.items[0].margin_rate_or_amount = 25 + # set rate to zero, so that it is recalculated on save + so.items[0].rate = 0 so.save() new_so = frappe.copy_doc(so) diff --git a/erpnext/utilities/transaction_base.py b/erpnext/utilities/transaction_base.py index 0fa68f52c16..340c6b93fdd 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -292,6 +292,291 @@ class TransactionBase(StatusUpdater): ) ) +<<<<<<< HEAD +======= + def fetch_item_details(self, item: dict) -> dict: + return get_item_details( + frappe._dict( + { + "item_code": item.get("item_code"), + "barcode": item.get("barcode"), + "serial_no": item.get("serial_no"), + "batch_no": item.get("batch_no"), + "set_warehouse": self.get("set_warehouse"), + "warehouse": item.get("warehouse"), + "customer": self.get("customer") or self.get("party_name"), + "quotation_to": self.get("quotation_to"), + "supplier": self.get("supplier"), + "currency": self.get("currency"), + "is_internal_supplier": self.get("is_internal_supplier"), + "is_internal_customer": self.get("is_internal_customer"), + "update_stock": self.update_stock + if self.doctype in ["Purchase Invoice", "Sales Invoice"] + else False, + "conversion_rate": self.get("conversion_rate"), + "price_list": self.get("selling_price_list") or self.get("buying_price_list"), + "price_list_currency": self.get("price_list_currency"), + "plc_conversion_rate": self.get("plc_conversion_rate"), + "company": self.get("company"), + "order_type": self.get("order_type"), + "is_pos": cint(self.get("is_pos")), + "is_return": cint(self.get("is_return")), + "is_subcontracted": self.get("is_subcontracted"), + "ignore_pricing_rule": self.get("ignore_pricing_rule"), + "doctype": self.get("doctype"), + "name": self.get("name"), + "project": item.get("project") or self.get("project"), + "qty": item.get("qty") or 1, + "net_rate": item.get("rate"), + "base_net_rate": item.get("base_net_rate"), + "stock_qty": item.get("stock_qty"), + "conversion_factor": item.get("conversion_factor"), + "weight_per_unit": item.get("weight_per_unit"), + "uom": item.get("uom"), + "weight_uom": item.get("weight_uom"), + "manufacturer": item.get("manufacturer"), + "stock_uom": item.get("stock_uom"), + "pos_profile": self.get("pos_profile") if cint(self.get("is_pos")) else "", + "cost_center": item.get("cost_center"), + "tax_category": self.get("tax_category"), + "item_tax_template": item.get("item_tax_template"), + "child_doctype": item.get("doctype"), + "child_docname": item.get("name"), + } + ), + self, + ) + + @frappe.whitelist() + def process_item_selection(self, item_idx: int): + # Server side 'item' doc. Update this to reflect in UI + item_obj = self.get("items", {"idx": item_idx})[0] + + if not item_obj.item_code: + return + + # 'item_details' has latest item related values + item_details = self.fetch_item_details(item_obj) + + self.set_fetched_values(item_obj, item_details) + + if self.doctype == "Request for Quotation": + return + + self.set_item_rate_and_discounts(item_obj, item_details) + self.add_taxes_from_item_template(item_obj, item_details) + self.add_free_item(item_obj, item_details) + self.handle_internal_parties(item_obj, item_details) + self.conversion_factor(item_obj, item_details) + self.calculate_taxes_and_totals() + + def set_fetched_values(self, item_obj: object, item_details: dict) -> None: + for k, v in item_details.items(): + if hasattr(item_obj, k): + setattr(item_obj, k, v) + + def handle_internal_parties(self, item_obj: object, item_details: dict) -> None: + fetch_valuation_rate_for_internal_transaction = cint( + frappe.get_single_value("Accounts Settings", "fetch_valuation_rate_for_internal_transaction") + ) + if ( + self.get("is_internal_customer") or self.get("is_internal_supplier") + ) and fetch_valuation_rate_for_internal_transaction: + args = frappe._dict( + { + "item_code": item_obj.item_code, + "warehouse": item_obj.from_warehouse + if self.doctype in ["Purchase Receipt", "Purchase Invoice"] + else item_obj.warehouse, + "qty": item_obj.qty * item_obj.conversion_factor, + "voucher_type": self.doctype, + "company": self.company, + } + ) + + if self.doctype in ["Purchase Order", "Sales Order"]: + args.update( + { + "posting_date": self.transaction_date, + "posting_time": self.transaction_time, + } + ) + else: + args.update( + { + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "serial_no": item_obj.serial_no, + "batch_no": item_obj.batch_no, + "allow_zero_valuation_rate": item_obj.allow_zero_valuation_rate, + } + ) + + rate = get_incoming_rate(args=args) + item_obj.rate = rate * item_obj.conversion_factor + else: + self.set_rate_based_on_price_list(item_obj, item_details) + + def add_taxes_from_item_template(self, item_obj: object, item_details: dict) -> None: + if item_details.item_tax_rate and frappe.get_single_value( + "Accounts Settings", "add_taxes_from_item_tax_template" + ): + item_tax_template = frappe.json.loads(item_details.item_tax_rate) + for tax_head, _rate in item_tax_template.items(): + if _rate == NOT_APPLICABLE_TAX: + continue + + found = [x for x in self.taxes if x.account_head == tax_head] + if not found: + child_doctype = self.get_table_field_doctype("taxes") + child = frappe.new_doc(child_doctype, parent_doc=self, parentfield="taxes") + child.charge_type = "On Net Total" + child.account_head = tax_head + child.rate = 0 + self.append("taxes", child) + + def set_rate_based_on_price_list(self, item_obj: object, item_details: dict) -> None: + if item_obj.price_list_rate and item_obj.discount_percentage: + item_obj.rate = flt( + item_obj.price_list_rate * (1 - item_obj.discount_percentage / 100.0), + item_obj.precision("rate"), + ) + + def copy_from_first_row(self, row, fields): + if self.items and row: + fields.extend([x.get("fieldname") for x in get_dimensions(True)[0]]) + first_row = self.items[0] + [setattr(row, k, first_row.get(k)) for k in fields if hasattr(first_row, k)] + + def add_free_item(self, item_obj: object, item_details: dict) -> None: + free_items = item_details.get("free_item_data") + if free_items and len(free_items): + existing_free_items = [x for x in self.items if x.is_free_item] + for free_item in free_items: + _matches = [ + x + for x in existing_free_items + if x.item_code == free_item.get("item_code") + and x.pricing_rules == free_item.get("pricing_rules") + ] + if _matches: + row_to_modify = _matches[0] + else: + row_to_modify = self.append("items") + + for k, _v in free_item.items(): + setattr(row_to_modify, k, free_item.get(k)) + + self.copy_from_first_row(row_to_modify, ["expense_account", "income_account"]) + + def conversion_factor(self, item_obj: object, item_details: dict) -> None: + if frappe.get_meta(item_obj.doctype).has_field("stock_qty"): + item_obj.stock_qty = flt( + item_obj.qty * item_obj.conversion_factor, item_obj.precision("stock_qty") + ) + + if self.doctype != "Material Request": + item_obj.total_weight = flt(item_obj.stock_qty * item_obj.weight_per_unit) + self.calculate_net_weight() + + # TODO: for handling customization not to fetch price list rate + if frappe.flags.dont_fetch_price_list_rate: + return + + if not frappe.flags.dont_fetch_price_list_rate and frappe.get_meta(self.doctype).has_field( + "price_list_currency" + ): + self._apply_price_list(item_obj, True) + self.calculate_stock_uom_rate(item_obj) + + def calculate_stock_uom_rate(self, item_obj: object) -> None: + if item_obj.rate: + item_obj.stock_uom_rate = flt(item_obj.rate) / flt(item_obj.conversion_factor) + + def set_item_rate_and_discounts(self, item_obj: object, item_details: dict) -> None: + effective_item_rate = item_details.price_list_rate + item_rate = item_details.rate + + # Field order precedance + # blanket_order_rate -> margin_type -> discount_percentage -> discount_amount + if item_obj.parenttype in ["Sales Order", "Quotation"] and item_obj.blanket_order_rate: + effective_item_rate = item_obj.blanket_order_rate + + if item_obj.margin_type == "Percentage": + item_obj.rate_with_margin = flt(effective_item_rate) + flt(effective_item_rate) * ( + flt(item_obj.margin_rate_or_amount) / 100 + ) + else: + item_obj.rate_with_margin = flt(effective_item_rate) + flt(item_obj.margin_rate_or_amount) + + item_obj.base_rate_with_margin = flt(item_obj.rate_with_margin) * flt(self.conversion_rate) + item_rate = flt(item_obj.rate_with_margin, item_obj.precision("rate")) + + if item_obj.discount_percentage: + item_obj.discount_amount = ( + flt(item_obj.rate_with_margin) * flt(item_obj.discount_percentage) / 100 + ) + + if item_obj.discount_amount: + item_rate = flt( + (item_obj.rate_with_margin) - (item_obj.discount_amount), item_obj.precision("rate") + ) + + item_obj.rate = item_rate + + def calculate_net_weight(self): + self.total_net_weight = sum([x.get("total_weight") or 0 for x in self.items]) + self.apply_shipping_rule() + + def _apply_price_list(self, item_obj: object, reset_plc_conversion: bool) -> None: + if self.doctype == "Material Request": + return + + if not reset_plc_conversion: + self.plc_conversion_rate = "" + + if not self.items or not (item_obj.get("selling_price_list") or item_obj.get("buying_price_list")): + return + + if self.get("in_apply_price_list"): + return + + self.in_apply_price_list = True + + from erpnext.stock.get_item_details import apply_price_list + + args = { + "items": [x.as_dict() for x in self.items], + "customer": self.customer or self.party_name, + "quotation_to": self.quotation_to, + "customer_group": self.customer_group, + "territory": self.territory, + "supplier": self.supplier, + "supplier_group": self.supplier_group, + "currency": self.currency, + "conversion_rate": self.conversion_rate, + "price_list": self.selling_price_list or self.buying_price_list, + "price_list_currency": self.price_list_currency, + "plc_conversion_rate": self.plc_conversion_rate, + "company": self.company, + "transaction_date": self.transaction_date or self.posting_date, + "campaign": self.campaign, + "sales_partner": self.sales_partner, + "ignore_pricing_rule": self.ignore_pricing_rule, + "doctype": self.doctype, + "name": self.name, + "is_return": self.is_return, + "update_stock": self.update_stock if self.doctype in ["Sales Invoice", "Purchase Invoice"] else 0, + "conversion_factor": self.conversion_factor, + "pos_profile": self.pos_profile if self.doctype == "Sales Invoice" else "", + "coupon_code": self.coupon_code, + "is_internal_supplier": self.is_internal_supplier, + "is_internal_customer": self.is_internal_customer, + } + # TODO: test method call impact on document + apply_price_list(cts=args, as_doc=True, doc=self) + +>>>>>>> cb0689bd1e (fix: rewrite item rate calculation (#56315)) def delete_events(ref_type, ref_name): events = (