diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 990f7e89d6f..93ddefefe85 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2321,11 +2321,14 @@ class TestSalesInvoice(ERPNextTestSuite): 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 index 7a8ff400a65..3ea050a53b4 100644 --- a/erpnext/accounts/services/child_item_update.py +++ b/erpnext/accounts/services/child_item_update.py @@ -534,6 +534,7 @@ def update_child_item_rate_and_discount( 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, @@ -541,14 +542,11 @@ def update_child_item_rate_and_discount( ) child_item.rate_with_margin = child_item.rate else: - child_item.discount_percentage = flt( - (1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0, - child_item.precision("discount_percentage"), - ) - child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate) child_item.margin_type = "" child_item.margin_rate_or_amount = 0 - child_item.rate_with_margin = 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: diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index b598db625a4..e98eb2cdcde 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -164,83 +164,85 @@ 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) - if item.discount_percentage == 100: - item.rate = 0.0 - elif item.price_list_rate: - if not item.rate or (item.pricing_rules and item.discount_percentage > 0): - item.rate = flt( - item.price_list_rate * (1.0 - (item.discount_percentage / 100.0)), - item.precision("rate"), - ) - - item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0) - - elif item.discount_amount and item.pricing_rules: - item.rate = item.price_list_rate - item.discount_amount - - if item.doctype in [ - "Quotation Item", - "Sales Order Item", - "Delivery Note Item", - "Sales Invoice Item", - "POS Invoice Item", - "Purchase Invoice Item", - "Purchase Order Item", - "Purchase Receipt Item", - ]: - item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item) - if flt(item.rate_with_margin) > 0: - item.rate = flt( - item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), - item.precision("rate"), - ) - - if item.discount_amount and not item.discount_percentage: - item.rate = item.rate_with_margin - item.discount_amount - else: - item.discount_amount = flt( - item.rate_with_margin - item.rate, item.precision("discount_amount") - ) - - elif flt(item.price_list_rate) > 0: - item.discount_amount = flt( - item.price_list_rate - item.rate, item.precision("discount_amount") - ) - elif flt(item.price_list_rate) > 0 and not item.discount_amount: - item.discount_amount = flt( - item.price_list_rate - item.rate, item.precision("discount_amount") + 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 ) - - 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")) + 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""" @@ -1134,48 +1136,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) @@ -1210,6 +1170,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 dc4f1e3bd61..acbffa95d7d 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -10,29 +10,30 @@ 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") { - 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); } 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)); - item_rate = flt(item.rate_with_margin, precision("rate", item)); - - if (item.discount_percentage && !item.discount_amount) { - item.discount_amount = (flt(item.rate_with_margin) * flt(item.discount_percentage)) / 100; + if (item.discount_percentage) { + item.discount_amount = flt( + (item.rate_with_margin * item.discount_percentage) / 100, + precision("discount_amount", item) + ); } - 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); + let item_rate = item.rate_with_margin; + if (item.discount_amount) { + item_rate = item.rate_with_margin - item.discount_amount; } - + 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 1c6af4fc978..1843cb02b47 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -13,39 +13,58 @@ 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); - var has_margin_field = frappe.meta.has_field(cdt, "margin_type"); - 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", + ]); if (item.price_list_rate && !item.blanket_order_rate) { - if (item.rate > item.price_list_rate && has_margin_field) { + 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 - // or set discount - item.discount_percentage = 0; - 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; - } else { - item.discount_percentage = flt( - (1 - item.rate / item.price_list_rate) * 100.0, - precision("discount_percentage", item) - ); - item.discount_amount = flt(item.price_list_rate) - flt(item.rate); - item.margin_type = ""; - item.margin_rate_or_amount = 0; - item.rate_with_margin = 0; + // 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.discount_percentage = 0.0; item.margin_type = ""; 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); @@ -3440,3 +3459,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 fd2b40d3a18..b3f9273e0b1 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -403,9 +403,9 @@ class TestQuotation(ERPNextTestSuite): 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) @@ -425,11 +425,11 @@ class TestQuotation(ERPNextTestSuite): 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)), @@ -465,11 +465,13 @@ class TestQuotation(ERPNextTestSuite): rate_with_margin = flt((1500 * 18.75) / 100 + 1500) - test_record = dict(self.globalTestRecords["Quotation"][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 + 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) quotation.transaction_date = nowdate() diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 5ba55d304c3..052ee574b72 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -1484,6 +1484,8 @@ class TestSalesOrder(ERPNextTestSuite): 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 bd7bbcdd34b..85f2e83f8d9 100644 --- a/erpnext/utilities/transaction_base.py +++ b/erpnext/utilities/transaction_base.py @@ -513,18 +513,15 @@ class TransactionBase(StatusUpdater): 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 and not item_obj.discount_amount: + 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 and item_obj.discount_amount > 0: + if item_obj.discount_amount: item_rate = flt( (item_obj.rate_with_margin) - (item_obj.discount_amount), item_obj.precision("rate") ) - item_obj.discount_percentage = ( - 100 * flt(item_obj.discount_amount) / flt(item_obj.rate_with_margin) - ) item_obj.rate = item_rate