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 4afba94d1c)

# Conflicts:
#	erpnext/selling/doctype/sales_order/test_sales_order.py
This commit is contained in:
pandiyan
2026-08-09 11:25:24 +05:30
committed by Mergify
parent 262fbc52d8
commit 1a89609044

View File

@@ -2646,6 +2646,282 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
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")):