From 39f15bb3e9ca020586e99e0c52d249db48653573 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Sun, 9 Aug 2026 11:25:12 +0530 Subject: [PATCH 1/3] fix: tolerate floating-point drift in sales team allocated percentage the total of allocated_percentage was compared to 100 with exact float equality, so a correct allocation could be rejected when the sum drifts in binary floating point (10.0 + 58.02 + 31.98 -> 100.00000000000001). round the total to the field precision before comparing, in both SellingController.calculate_contribution and Customer.validate. (cherry picked from commit f7b277582940cbb5f7a427ef8250b18db773004b) --- erpnext/controllers/selling_controller.py | 2 +- erpnext/selling/doctype/customer/customer.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 6cdcb7090d2..51c821df93f 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -253,7 +253,7 @@ class SellingController(StockController): total += sales_person.allocated_percentage - if sales_team and total != 100.0: + if sales_team and flt(total, self.precision("allocated_percentage", "sales_team")) != 100.0: throw(_("Total allocated percentage for sales team should be 100")) def validate_sales_team(self, sales_team): diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 126145792dd..76495a94959 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -194,7 +194,8 @@ class Customer(TransactionBase): self.loyalty_program_tier = customer.loyalty_program_tier if self.sales_team: - if sum(member.allocated_percentage or 0 for member in self.sales_team) != 100: + total = sum(flt(member.allocated_percentage) for member in self.sales_team) + if flt(total, self.precision("allocated_percentage", "sales_team")) != 100: frappe.throw(_("Total contribution percentage should be equal to 100")) @frappe.whitelist() From ee9026d62d956049a738a63abe055906100ff977 Mon Sep 17 00:00:00 2001 From: pandiyan Date: Sun, 9 Aug 2026 11:25:24 +0530 Subject: [PATCH 2/3] test: sales team allocation totalling 100 in floating point covers the case where the percentages are correct but the accumulated sum is 100.00000000000001. two rows can never drift, since the second reconstructs exactly as 100 - first, so the case needs three rows. (cherry picked from commit 4afba94d1c3dffec6cb789f69f537883b88dc32d) # Conflicts: # erpnext/selling/doctype/sales_order/test_sales_order.py --- .../doctype/sales_order/test_sales_order.py | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index f45632573b0..36fcf226059 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2890,6 +2890,282 @@ class TestSalesOrder(ERPNextTestSuite): so = make_sales_order(item_code=fg_item, qty=10, rate=50, warehouse=fg_warehouse, do_not_save=1) self.assertRaises(frappe.ValidationError, so.save) +<<<<<<< HEAD +======= + @ERPNextTestSuite.change_settings( + "Stock Settings", {"enable_stock_reservation": 1, "use_serial_batch_fields": 0} + ) + def test_product_bundle_reservation(self): + pb_item = make_item("Product Bundle Item", {"is_stock_item": 0}) + simple_item = make_item("Simple Item", {"is_stock_item": 1}) + sb_item = make_item( + "Serial Batch Item", + { + "is_stock_item": 1, + "has_serial_no": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "BAT-TSBIFRM-.#####", + "serial_no_series": "SN-TSBIFRM-.#####", + }, + ) + make_product_bundle(pb_item.name, [simple_item.name, sb_item.name]) + + warehouse = "_Test Warehouse - _TC" + + make_stock_entry( + item_code=simple_item.name, + target=warehouse, + qty=10, + ) + + # two different stock entries on purpose to get two batches + make_stock_entry( + item_code=sb_item.name, + target=warehouse, + qty=5, + ) + make_stock_entry( + item_code=sb_item.name, + target=warehouse, + qty=5, + ) + + so = make_sales_order(item_code=pb_item.name, do_not_submit=1) + so.reserve_stock = 1 + for item in so.packed_items: + item.reserve_stock = 1 + so.submit() + + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + get_sre_reserved_batch_nos_details, + get_sre_reserved_qty_for_voucher_detail_no, + get_sre_reserved_serial_nos_details, + ) + + for item in so.packed_items: + self.assertEqual( + get_sre_reserved_qty_for_voucher_detail_no(item.item_code, "Sales Order", so.name, item.name), + item.qty, + ) + + sre_serial_nos = list(get_sre_reserved_serial_nos_details(sb_item.name, warehouse).keys()) + sre_batch_nos = list(get_sre_reserved_batch_nos_details(sb_item.name, warehouse).keys()) + + dn = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) + dn.save() + + self.assertTrue(dn.packed_items[1].serial_and_batch_bundle) + + from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos + + serial_nos_in_bundle = get_serial_nos(dn.packed_items[1].serial_and_batch_bundle) + batches_in_bundle = list(get_batches_from_bundle(dn.packed_items[1].serial_and_batch_bundle).keys()) + + self.assertEqual(sre_serial_nos, serial_nos_in_bundle) + self.assertEqual(sre_batch_nos, batches_in_bundle) + + dn.items[0].qty = 5 + dn.save() + sabb_doc = frappe.get_doc("Serial and Batch Bundle", dn.packed_items[1].serial_and_batch_bundle) + sabb_doc.entries = sabb_doc.entries[:5] + sabb_doc.company = dn.company + sabb_doc.save() + dn.submit() + + serial_nos = set(sre_serial_nos) - set(get_serial_nos(sabb_doc.name)) + batch_nos = set(sre_batch_nos) - set(get_batches_from_bundle(sabb_doc.name).keys()) + + dn1 = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) + dn1.save() + + self.assertTrue(dn1.packed_items[1].serial_and_batch_bundle) + + from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos + + serial_nos_in_bundle = set(get_serial_nos(dn1.packed_items[1].serial_and_batch_bundle)) + batches_in_bundle = set(get_batches_from_bundle(dn1.packed_items[1].serial_and_batch_bundle).keys()) + + self.assertEqual(serial_nos, serial_nos_in_bundle) + self.assertEqual(batch_nos, batches_in_bundle) + + dn.cancel() + + # test the same thing with sales invoice as well + + si = make_sales_invoice(so.name) + si.update_stock = 1 + si.save() + + self.assertTrue(si.packed_items[1].serial_and_batch_bundle) + + from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos + + serial_nos_in_bundle = get_serial_nos(si.packed_items[1].serial_and_batch_bundle) + batches_in_bundle = list(get_batches_from_bundle(si.packed_items[1].serial_and_batch_bundle).keys()) + + self.assertEqual(sre_serial_nos, serial_nos_in_bundle) + self.assertEqual(sre_batch_nos, batches_in_bundle) + + si.items[0].qty = 5 + si.save() + sabb_doc = frappe.get_doc("Serial and Batch Bundle", si.packed_items[1].serial_and_batch_bundle) + sabb_doc.entries = sabb_doc.entries[:5] + sabb_doc.company = si.company + sabb_doc.save() + si.submit() + + serial_nos = set(sre_serial_nos) - set(get_serial_nos(sabb_doc.name)) + batch_nos = set(sre_batch_nos) - set(get_batches_from_bundle(sabb_doc.name).keys()) + + si1 = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) + si1.save() + + self.assertTrue(si1.packed_items[1].serial_and_batch_bundle) + + from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos + + serial_nos_in_bundle = set(get_serial_nos(si1.packed_items[1].serial_and_batch_bundle)) + batches_in_bundle = set(get_batches_from_bundle(si1.packed_items[1].serial_and_batch_bundle).keys()) + + self.assertEqual(serial_nos, serial_nos_in_bundle) + self.assertEqual(batch_nos, batches_in_bundle) + + def test_sales_team_contribution_follows_grant_commission(self): + """Sales-person allocation tracks the grant-commission-eligible amount, not the gross total. + + The Item "Grant Commission" flag includes an item in both Sales Partner and Sales Person + commission, so each sales person's allocated_amount is a share of + amount_eligible_for_commission rather than net_total. + """ + frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) + frappe.db.set_value("Item", "_Test FG Item", "grant_commission", 0) + try: + so = make_sales_order( + do_not_save=True, + item_list=[ + {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC", "qty": 10, "rate": 100}, + { + "item_code": "_Test FG Item", + "warehouse": "_Test Warehouse - _TC", + "qty": 10, + "rate": 100, + }, + ], + ) + so.append( + "sales_team", + {"sales_person": "_Test Sales Person 1", "allocated_percentage": 60, "commission_rate": 10}, + ) + so.append( + "sales_team", + {"sales_person": "_Test Sales Person 2", "allocated_percentage": 40, "commission_rate": 0}, + ) + so.save() + + self.assertEqual(so.net_total, 2000) + self.assertEqual(so.amount_eligible_for_commission, 1000) # only the grant_commission item + + first, second = so.sales_team + # allocation follows the eligible amount (1000), not net_total (2000) + self.assertEqual(first.allocated_amount, 600) + self.assertEqual(first.incentives, 60) # 600 * 10% + self.assertEqual(second.allocated_amount, 400) + self.assertEqual(second.incentives, 0) + finally: + # grant_commission defaults to 1 for both items; restore + frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) + frappe.db.set_value("Item", "_Test FG Item", "grant_commission", 1) + + def test_sales_team_allocated_percentage_must_total_100(self): + with self.subTest("partial allocation is rejected"): + so = make_sales_order(do_not_save=True) + so.append("sales_team", {"sales_person": "_Test Sales Person 1", "allocated_percentage": 60}) + self.assertRaises(frappe.ValidationError, so.save) + + with self.subTest("allocation totalling 100 is accepted"): + so = make_sales_order(do_not_save=True) + so.append("sales_team", {"sales_person": "_Test Sales Person 1", "allocated_percentage": 60}) + so.append("sales_team", {"sales_person": "_Test Sales Person 2", "allocated_percentage": 40}) + so.save() + self.assertEqual(sum(d.allocated_percentage for d in so.sales_team), 100) + + with self.subTest("floating-point drift in the total is tolerated"): + # 10.0 + 58.02 + 31.98 accumulates to 100.00000000000001 in binary floating point + so = make_sales_order(do_not_save=True) + for sales_person, percentage in ( + ("_Test Sales Person", 10.0), + ("_Test Sales Person 1", 58.02), + ("_Test Sales Person 2", 31.98), + ): + so.append("sales_team", {"sales_person": sales_person, "allocated_percentage": percentage}) + so.save() + + def test_sales_team_disabled_sales_person_rejected(self): + frappe.db.set_value("Sales Person", "_Test Sales Person 2", "enabled", 0) + try: + so = make_sales_order(do_not_save=True) + so.append("sales_team", {"sales_person": "_Test Sales Person 2", "allocated_percentage": 100}) + self.assertRaises(frappe.ValidationError, so.save) + finally: + frappe.db.set_value("Sales Person", "_Test Sales Person 2", "enabled", 1) + + def test_sales_partner_commission(self): + """Sales Partner commission: total_commission = amount_eligible_for_commission * rate / 100.""" + frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) + try: + so = make_sales_order(qty=10, rate=100, do_not_save=True) + so.sales_partner = "_Test Sales Partner India - 1" + so.commission_rate = 7 + so.save() + + self.assertEqual(so.amount_eligible_for_commission, 1000) + self.assertEqual(so.total_commission, 70) # 1000 * 7% + + with self.subTest("commission rate above 100 is rejected"): + so.commission_rate = 101 + self.assertRaises(frappe.ValidationError, so.save) + finally: + frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) + + def test_commission_fields_not_copied_on_duplicate(self): + """Commission rate/amount fields are no_copy; only the sales partner carries to a copy.""" + frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) + try: + so = make_sales_order(qty=10, rate=100, do_not_save=True) + so.sales_partner = "_Test Sales Partner India - 1" + so.commission_rate = 7 + so.save() + self.assertEqual(so.total_commission, 70) + + # ignore_no_copy=False mirrors UI "Duplicate"/amend, which honour no_copy + duplicate = frappe.copy_doc(so, ignore_no_copy=False) + self.assertEqual(duplicate.sales_partner, "_Test Sales Partner India - 1") + self.assertFalse(duplicate.commission_rate) + self.assertFalse(duplicate.total_commission) + self.assertFalse(duplicate.amount_eligible_for_commission) + finally: + frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) + + def test_commission_rate_carried_through_mapper(self): + """commission_rate is no_copy, but Make Delivery Note / Sales Invoice still carries it.""" + from erpnext.selling.doctype.sales_order.mapper import make_delivery_note, make_sales_invoice + + original = frappe.db.get_value("Item", "_Test Item", "grant_commission") + frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) + try: + so = make_sales_order(qty=10, rate=100, do_not_save=True) + so.sales_partner = "_Test Sales Partner India - 1" + so.commission_rate = 7 + so.submit() + + # carried to the mapped (unsaved) documents even though the field is no_copy + self.assertEqual(make_delivery_note(so.name).commission_rate, 7) + self.assertEqual(make_sales_invoice(so.name).commission_rate, 7) + finally: + frappe.db.set_value("Item", "_Test Item", "grant_commission", original) + +>>>>>>> 4afba94d1c (test: sales team allocation totalling 100 in floating point) def compare_payment_schedules(doc, doc1, doc2): for index, schedule in enumerate(doc1.get("payment_schedule")): From c507d5f09bcd5558aaa5239dd6eb9412f664c98f Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Sun, 9 Aug 2026 12:49:07 +0530 Subject: [PATCH 3/3] chore: resolve conflict --- .../doctype/sales_order/test_sales_order.py | 285 +----------------- 1 file changed, 10 insertions(+), 275 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 36fcf226059..b5e6568c82b 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -2890,282 +2890,17 @@ class TestSalesOrder(ERPNextTestSuite): so = make_sales_order(item_code=fg_item, qty=10, rate=50, warehouse=fg_warehouse, do_not_save=1) self.assertRaises(frappe.ValidationError, so.save) -<<<<<<< HEAD -======= - @ERPNextTestSuite.change_settings( - "Stock Settings", {"enable_stock_reservation": 1, "use_serial_batch_fields": 0} - ) - def test_product_bundle_reservation(self): - pb_item = make_item("Product Bundle Item", {"is_stock_item": 0}) - simple_item = make_item("Simple Item", {"is_stock_item": 1}) - sb_item = make_item( - "Serial Batch Item", - { - "is_stock_item": 1, - "has_serial_no": 1, - "has_batch_no": 1, - "create_new_batch": 1, - "batch_number_series": "BAT-TSBIFRM-.#####", - "serial_no_series": "SN-TSBIFRM-.#####", - }, - ) - make_product_bundle(pb_item.name, [simple_item.name, sb_item.name]) + def test_sales_team_allocated_percentage_tolerates_floating_point_drift(self): + # 10.0 + 58.02 + 31.98 accumulates to 100.00000000000001 in binary floating point + so = make_sales_order(do_not_save=True) + for sales_person, percentage in ( + ("_Test Sales Person", 10.0), + ("_Test Sales Person 1", 58.02), + ("_Test Sales Person 2", 31.98), + ): + so.append("sales_team", {"sales_person": sales_person, "allocated_percentage": percentage}) + so.save() - warehouse = "_Test Warehouse - _TC" - - make_stock_entry( - item_code=simple_item.name, - target=warehouse, - qty=10, - ) - - # two different stock entries on purpose to get two batches - make_stock_entry( - item_code=sb_item.name, - target=warehouse, - qty=5, - ) - make_stock_entry( - item_code=sb_item.name, - target=warehouse, - qty=5, - ) - - so = make_sales_order(item_code=pb_item.name, do_not_submit=1) - so.reserve_stock = 1 - for item in so.packed_items: - item.reserve_stock = 1 - so.submit() - - from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - get_sre_reserved_batch_nos_details, - get_sre_reserved_qty_for_voucher_detail_no, - get_sre_reserved_serial_nos_details, - ) - - for item in so.packed_items: - self.assertEqual( - get_sre_reserved_qty_for_voucher_detail_no(item.item_code, "Sales Order", so.name, item.name), - item.qty, - ) - - sre_serial_nos = list(get_sre_reserved_serial_nos_details(sb_item.name, warehouse).keys()) - sre_batch_nos = list(get_sre_reserved_batch_nos_details(sb_item.name, warehouse).keys()) - - dn = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) - dn.save() - - self.assertTrue(dn.packed_items[1].serial_and_batch_bundle) - - from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos - - serial_nos_in_bundle = get_serial_nos(dn.packed_items[1].serial_and_batch_bundle) - batches_in_bundle = list(get_batches_from_bundle(dn.packed_items[1].serial_and_batch_bundle).keys()) - - self.assertEqual(sre_serial_nos, serial_nos_in_bundle) - self.assertEqual(sre_batch_nos, batches_in_bundle) - - dn.items[0].qty = 5 - dn.save() - sabb_doc = frappe.get_doc("Serial and Batch Bundle", dn.packed_items[1].serial_and_batch_bundle) - sabb_doc.entries = sabb_doc.entries[:5] - sabb_doc.company = dn.company - sabb_doc.save() - dn.submit() - - serial_nos = set(sre_serial_nos) - set(get_serial_nos(sabb_doc.name)) - batch_nos = set(sre_batch_nos) - set(get_batches_from_bundle(sabb_doc.name).keys()) - - dn1 = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) - dn1.save() - - self.assertTrue(dn1.packed_items[1].serial_and_batch_bundle) - - from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos - - serial_nos_in_bundle = set(get_serial_nos(dn1.packed_items[1].serial_and_batch_bundle)) - batches_in_bundle = set(get_batches_from_bundle(dn1.packed_items[1].serial_and_batch_bundle).keys()) - - self.assertEqual(serial_nos, serial_nos_in_bundle) - self.assertEqual(batch_nos, batches_in_bundle) - - dn.cancel() - - # test the same thing with sales invoice as well - - si = make_sales_invoice(so.name) - si.update_stock = 1 - si.save() - - self.assertTrue(si.packed_items[1].serial_and_batch_bundle) - - from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos - - serial_nos_in_bundle = get_serial_nos(si.packed_items[1].serial_and_batch_bundle) - batches_in_bundle = list(get_batches_from_bundle(si.packed_items[1].serial_and_batch_bundle).keys()) - - self.assertEqual(sre_serial_nos, serial_nos_in_bundle) - self.assertEqual(sre_batch_nos, batches_in_bundle) - - si.items[0].qty = 5 - si.save() - sabb_doc = frappe.get_doc("Serial and Batch Bundle", si.packed_items[1].serial_and_batch_bundle) - sabb_doc.entries = sabb_doc.entries[:5] - sabb_doc.company = si.company - sabb_doc.save() - si.submit() - - serial_nos = set(sre_serial_nos) - set(get_serial_nos(sabb_doc.name)) - batch_nos = set(sre_batch_nos) - set(get_batches_from_bundle(sabb_doc.name).keys()) - - si1 = make_delivery_note(so.name, kwargs={"for_reserved_stock": True}) - si1.save() - - self.assertTrue(si1.packed_items[1].serial_and_batch_bundle) - - from erpnext.stock.serial_batch_bundle import get_batches_from_bundle, get_serial_nos - - serial_nos_in_bundle = set(get_serial_nos(si1.packed_items[1].serial_and_batch_bundle)) - batches_in_bundle = set(get_batches_from_bundle(si1.packed_items[1].serial_and_batch_bundle).keys()) - - self.assertEqual(serial_nos, serial_nos_in_bundle) - self.assertEqual(batch_nos, batches_in_bundle) - - def test_sales_team_contribution_follows_grant_commission(self): - """Sales-person allocation tracks the grant-commission-eligible amount, not the gross total. - - The Item "Grant Commission" flag includes an item in both Sales Partner and Sales Person - commission, so each sales person's allocated_amount is a share of - amount_eligible_for_commission rather than net_total. - """ - frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) - frappe.db.set_value("Item", "_Test FG Item", "grant_commission", 0) - try: - so = make_sales_order( - do_not_save=True, - item_list=[ - {"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC", "qty": 10, "rate": 100}, - { - "item_code": "_Test FG Item", - "warehouse": "_Test Warehouse - _TC", - "qty": 10, - "rate": 100, - }, - ], - ) - so.append( - "sales_team", - {"sales_person": "_Test Sales Person 1", "allocated_percentage": 60, "commission_rate": 10}, - ) - so.append( - "sales_team", - {"sales_person": "_Test Sales Person 2", "allocated_percentage": 40, "commission_rate": 0}, - ) - so.save() - - self.assertEqual(so.net_total, 2000) - self.assertEqual(so.amount_eligible_for_commission, 1000) # only the grant_commission item - - first, second = so.sales_team - # allocation follows the eligible amount (1000), not net_total (2000) - self.assertEqual(first.allocated_amount, 600) - self.assertEqual(first.incentives, 60) # 600 * 10% - self.assertEqual(second.allocated_amount, 400) - self.assertEqual(second.incentives, 0) - finally: - # grant_commission defaults to 1 for both items; restore - frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) - frappe.db.set_value("Item", "_Test FG Item", "grant_commission", 1) - - def test_sales_team_allocated_percentage_must_total_100(self): - with self.subTest("partial allocation is rejected"): - so = make_sales_order(do_not_save=True) - so.append("sales_team", {"sales_person": "_Test Sales Person 1", "allocated_percentage": 60}) - self.assertRaises(frappe.ValidationError, so.save) - - with self.subTest("allocation totalling 100 is accepted"): - so = make_sales_order(do_not_save=True) - so.append("sales_team", {"sales_person": "_Test Sales Person 1", "allocated_percentage": 60}) - so.append("sales_team", {"sales_person": "_Test Sales Person 2", "allocated_percentage": 40}) - so.save() - self.assertEqual(sum(d.allocated_percentage for d in so.sales_team), 100) - - with self.subTest("floating-point drift in the total is tolerated"): - # 10.0 + 58.02 + 31.98 accumulates to 100.00000000000001 in binary floating point - so = make_sales_order(do_not_save=True) - for sales_person, percentage in ( - ("_Test Sales Person", 10.0), - ("_Test Sales Person 1", 58.02), - ("_Test Sales Person 2", 31.98), - ): - so.append("sales_team", {"sales_person": sales_person, "allocated_percentage": percentage}) - so.save() - - def test_sales_team_disabled_sales_person_rejected(self): - frappe.db.set_value("Sales Person", "_Test Sales Person 2", "enabled", 0) - try: - so = make_sales_order(do_not_save=True) - so.append("sales_team", {"sales_person": "_Test Sales Person 2", "allocated_percentage": 100}) - self.assertRaises(frappe.ValidationError, so.save) - finally: - frappe.db.set_value("Sales Person", "_Test Sales Person 2", "enabled", 1) - - def test_sales_partner_commission(self): - """Sales Partner commission: total_commission = amount_eligible_for_commission * rate / 100.""" - frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) - try: - so = make_sales_order(qty=10, rate=100, do_not_save=True) - so.sales_partner = "_Test Sales Partner India - 1" - so.commission_rate = 7 - so.save() - - self.assertEqual(so.amount_eligible_for_commission, 1000) - self.assertEqual(so.total_commission, 70) # 1000 * 7% - - with self.subTest("commission rate above 100 is rejected"): - so.commission_rate = 101 - self.assertRaises(frappe.ValidationError, so.save) - finally: - frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) - - def test_commission_fields_not_copied_on_duplicate(self): - """Commission rate/amount fields are no_copy; only the sales partner carries to a copy.""" - frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) - try: - so = make_sales_order(qty=10, rate=100, do_not_save=True) - so.sales_partner = "_Test Sales Partner India - 1" - so.commission_rate = 7 - so.save() - self.assertEqual(so.total_commission, 70) - - # ignore_no_copy=False mirrors UI "Duplicate"/amend, which honour no_copy - duplicate = frappe.copy_doc(so, ignore_no_copy=False) - self.assertEqual(duplicate.sales_partner, "_Test Sales Partner India - 1") - self.assertFalse(duplicate.commission_rate) - self.assertFalse(duplicate.total_commission) - self.assertFalse(duplicate.amount_eligible_for_commission) - finally: - frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) - - def test_commission_rate_carried_through_mapper(self): - """commission_rate is no_copy, but Make Delivery Note / Sales Invoice still carries it.""" - from erpnext.selling.doctype.sales_order.mapper import make_delivery_note, make_sales_invoice - - original = frappe.db.get_value("Item", "_Test Item", "grant_commission") - frappe.db.set_value("Item", "_Test Item", "grant_commission", 1) - try: - so = make_sales_order(qty=10, rate=100, do_not_save=True) - so.sales_partner = "_Test Sales Partner India - 1" - so.commission_rate = 7 - so.submit() - - # carried to the mapped (unsaved) documents even though the field is no_copy - self.assertEqual(make_delivery_note(so.name).commission_rate, 7) - self.assertEqual(make_sales_invoice(so.name).commission_rate, 7) - finally: - frappe.db.set_value("Item", "_Test Item", "grant_commission", original) - ->>>>>>> 4afba94d1c (test: sales team allocation totalling 100 in floating point) def compare_payment_schedules(doc, doc1, doc2): for index, schedule in enumerate(doc1.get("payment_schedule")):