diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a6833e46895..27a1f47d90b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2965,29 +2965,46 @@ class AccountsController(TransactionBase): return item_doctype = self.meta.get_field("items").options - 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 - doctype_table = frappe.qb.DocType(self.doctype) item_table = frappe.qb.DocType(item_doctype) - discount_already_applied = ( + + 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) - .where( + .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 + + query = query.where( doctype_table.name.isin( frappe.qb.from_(item_table) .select(item_table.parent) @@ -2995,20 +3012,29 @@ class AccountsController(TransactionBase): .distinct() ) ) - .select(Sum(doctype_table.discount_amount)) - ).run() + result = query.run() + if not result: + return + + discount_already_applied = result[0][0] if not discount_already_applied: return - discount_already_applied = flt(discount_already_applied[0][0], self.precision("discount_amount")) + if is_return: + # returns have negative discount + discount_already_applied *= -1 + if (source_doc.discount_amount * (discount_already_applied - source_doc.discount_amount)) >= 0: # full discount already applied or exceeded self.discount_amount = 0 else: - self.discount_amount = flt( - self.discount_amount - discount_already_applied, self.precision("discount_amount") - ) + discount_amount = source_doc.discount_amount - discount_already_applied + if is_return: + # returns have negative discount + discount_amount *= -1 + + self.discount_amount = flt(discount_amount, self.precision("discount_amount")) self.calculate_taxes_and_totals() diff --git a/erpnext/controllers/tests/test_accounts_controller.py b/erpnext/controllers/tests/test_accounts_controller.py index c745d61ca5c..b001bdb351b 100644 --- a/erpnext/controllers/tests/test_accounts_controller.py +++ b/erpnext/controllers/tests/test_accounts_controller.py @@ -2400,3 +2400,35 @@ class TestAccountsController(IntegrationTestCase): # and not affected by the repeated mapping logic self.assertEqual(dn.additional_discount_percentage, 10) self.assertEqual(dn.discount_amount, 50) # 10% of 500 + + def test_discount_amount_for_multiple_returns(self): + """ + Test that discount amount is correctly adjusted when multiple return invoices + are created against the same original invoice to prevent over-returning discount + """ + from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return + + # Create original sales invoice with discount + si = create_sales_invoice(qty=10, rate=100, do_not_submit=True) + si.apply_discount_on = "Net Total" + si.discount_amount = 100 + si.save() + si.submit() + + # Create first return - Frappe will copy full discount by default, we need to adjust it + return_si_1 = make_sales_return(si.name) + return_si_1.items[0].qty = -6 # Return 6 out of 10 items + # Manually set discount to match the proportion (60% of discount) + return_si_1.discount_amount = -60 + return_si_1.save() + return_si_1.submit() + + self.assertEqual(return_si_1.discount_amount, -60) + + # Create second return for remaining items + return_si_2 = make_sales_return(si.name) + return_si_2.items[0].qty = -4 # Return remaining 4 out of 10 items + return_si_2.save() + + # Second return should only get remaining discount (100 - 60 = 40) + self.assertEqual(return_si_2.discount_amount, -40)