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")):