mirror of
https://github.com/frappe/erpnext.git
synced 2026-10-29 22:57:42 +00:00
fix(selling): validate targets and count achievements once
(cherry picked from commit eec3bcf900)
This commit is contained in:
committed by
Mergify
parent
7550e0570f
commit
6ed19302f7
+22
-33
@@ -169,43 +169,32 @@ def prepare_data(
|
||||
item_group_parent_child_map = get_item_group_parent_child_map()
|
||||
|
||||
for d in sales_users_data:
|
||||
key = (d.parent, d.item_group)
|
||||
key = (d.parent, d.item_group or None)
|
||||
dist_data = get_periodwise_distribution_data(d.distribution_id, period_list, filters.get("period"))
|
||||
details = rows.setdefault(key, {"total_target": 0, "total_achieved": 0})
|
||||
for period in period_list:
|
||||
target_key = f"target_{period.key}"
|
||||
target = (d.get(target_qty_amt_field) * dist_data.get(period.key)) / 100
|
||||
details[target_key] = details.get(target_key, 0) + target
|
||||
details["total_target"] += target
|
||||
|
||||
if key not in rows:
|
||||
rows.setdefault(key, {"total_target": 0, "total_achieved": 0, "total_variance": 0})
|
||||
|
||||
details = rows[key]
|
||||
for (parent, item_group), details in rows.items():
|
||||
for period in period_list:
|
||||
p_key = period.key
|
||||
if p_key not in details:
|
||||
details[p_key] = 0
|
||||
|
||||
target_key = f"target_{p_key}"
|
||||
variance_key = f"variance_{p_key}"
|
||||
details[target_key] = (d.get(target_qty_amt_field) * dist_data.get(p_key)) / 100
|
||||
details[variance_key] = 0
|
||||
details["total_target"] += details[target_key]
|
||||
|
||||
for r in actual_data:
|
||||
if (
|
||||
r.get(sales_field) == d.parent
|
||||
and period.from_date <= r.get(date_field)
|
||||
and r.get(date_field) <= period.to_date
|
||||
and (
|
||||
not d.item_group
|
||||
or not sales_user_wise_item_groups.get(d.parent)
|
||||
or r.item_group == d.item_group
|
||||
or r.item_group in item_group_parent_child_map.get(d.item_group, [])
|
||||
)
|
||||
):
|
||||
details[p_key] += r.get(qty_or_amount_field, 0)
|
||||
details[variance_key] = details.get(p_key) - details.get(target_key)
|
||||
else:
|
||||
details[variance_key] = details.get(p_key) - details.get(target_key)
|
||||
|
||||
details["total_achieved"] += details.get(p_key)
|
||||
details["total_variance"] = details.get("total_achieved") - details.get("total_target")
|
||||
details[p_key] = sum(
|
||||
r.get(qty_or_amount_field, 0)
|
||||
for r in actual_data
|
||||
if r.get(sales_field) == parent
|
||||
and period.from_date <= r.get(date_field) <= period.to_date
|
||||
and (
|
||||
not item_group
|
||||
or r.item_group == item_group
|
||||
or r.item_group in item_group_parent_child_map.get(item_group, [])
|
||||
)
|
||||
)
|
||||
details[f"variance_{p_key}"] = details[p_key] - details[f"target_{p_key}"]
|
||||
details["total_achieved"] += details[p_key]
|
||||
details["total_variance"] = details["total_achieved"] - details["total_target"]
|
||||
|
||||
return rows
|
||||
|
||||
|
||||
+7
@@ -62,3 +62,10 @@ class TestSalesPartnerTargetVarianceBasedOnItemGroup(FrappeTestCase):
|
||||
)
|
||||
|
||||
check_company_filter(self, "Sales Partner", "partner_name", execute)
|
||||
|
||||
def test_duplicate_target_inputs_count_actuals_once(self):
|
||||
from erpnext.selling.report.territory_target_variance_based_on_item_group.test_territory_target_variance_based_on_item_group import (
|
||||
check_duplicate_targets,
|
||||
)
|
||||
|
||||
check_duplicate_targets(self, "Sales Partner", "partner_name", execute)
|
||||
|
||||
+7
@@ -120,6 +120,13 @@ class TestSalesPersonTargetVarianceBasedOnItemGroup(FrappeTestCase):
|
||||
|
||||
check_company_filter(self, "Sales Person", "sales_person_name", execute)
|
||||
|
||||
def test_duplicate_target_inputs_count_actuals_once(self):
|
||||
from erpnext.selling.report.territory_target_variance_based_on_item_group.test_territory_target_variance_based_on_item_group import (
|
||||
check_duplicate_targets,
|
||||
)
|
||||
|
||||
check_duplicate_targets(self, "Sales Person", "sales_person_name", execute)
|
||||
|
||||
|
||||
def create_target_distribution(fiscal_year):
|
||||
if frappe.db.exists("Monthly Distribution", "Target Report Distribution"):
|
||||
|
||||
+85
@@ -157,6 +157,13 @@ class TestTerritoryTargetVarianceBasedOnItemGroup(FrappeTestCase):
|
||||
|
||||
check_company_filter(self, "Territory", "territory_name", execute)
|
||||
|
||||
def test_duplicate_target_inputs_count_actuals_once(self):
|
||||
from erpnext.selling.report.territory_target_variance_based_on_item_group.test_territory_target_variance_based_on_item_group import (
|
||||
check_duplicate_targets,
|
||||
)
|
||||
|
||||
check_duplicate_targets(self, "Territory", "territory_name", execute)
|
||||
|
||||
|
||||
def create_territory_with_target(name, fiscal_year, distribution_id, target_qty=50):
|
||||
doc = frappe.new_doc("Territory")
|
||||
@@ -211,3 +218,81 @@ def check_company_filter(test, doctype, name_field, execute_report):
|
||||
test.assertEqual(flt(achieved(), 2), flt(order.base_net_total, 2))
|
||||
filters.company = "_Test Company"
|
||||
test.assertEqual(flt(achieved(), 2), 0)
|
||||
|
||||
|
||||
def check_duplicate_targets(test, doctype, name_field, execute_report):
|
||||
from frappe.utils import add_months, getdate
|
||||
|
||||
from erpnext.selling.report.sales_person_target_variance_based_on_item_group.test_sales_person_target_variance_based_on_item_group import (
|
||||
create_sales_target_doc,
|
||||
)
|
||||
|
||||
distribution = create_target_distribution(test.fiscal_year)
|
||||
month = getdate(nowdate()).strftime("%B")
|
||||
for percentage in distribution.percentages:
|
||||
percentage.percentage_allocation = 100 if percentage.month == month else 0
|
||||
distribution.save()
|
||||
other_distribution = frappe.copy_doc(distribution)
|
||||
other_distribution.distribution_id = frappe.generate_hash(length=10)
|
||||
other_month = getdate(add_months(nowdate(), 1)).strftime("%B")
|
||||
for percentage in other_distribution.percentages:
|
||||
percentage.percentage_allocation = 100 if percentage.month == other_month else 0
|
||||
other_distribution.insert()
|
||||
holder = create_sales_target_doc(
|
||||
doctype, name_field, frappe.generate_hash(length=10), test.fiscal_year, distribution.name
|
||||
)
|
||||
holder.targets[0].target_amount = 1000
|
||||
holder.save()
|
||||
holder.append(
|
||||
"targets", dict(holder.targets[0].as_dict(), name=None, distribution_id=other_distribution.name)
|
||||
)
|
||||
order = make_sales_order(rate=600, qty=1, do_not_submit=True)
|
||||
field = frappe.scrub(doctype)
|
||||
if doctype == "Sales Person":
|
||||
order.set("sales_team", [{"sales_person": holder.name, "allocated_percentage": 100}])
|
||||
else:
|
||||
order.set(field, holder.name)
|
||||
if doctype == "Sales Partner":
|
||||
order.commission_rate = 5
|
||||
order.submit()
|
||||
filters = frappe._dict(
|
||||
company=order.company,
|
||||
fiscal_year=test.fiscal_year,
|
||||
doctype="Sales Order",
|
||||
period="Yearly",
|
||||
target_on="Amount",
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import get_period_list
|
||||
from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.item_group_wise_sales_target_variance import (
|
||||
get_actual_data,
|
||||
prepare_data,
|
||||
)
|
||||
|
||||
def report_row():
|
||||
periods = get_period_list(
|
||||
test.fiscal_year, test.fiscal_year, "", "", "Fiscal Year", filters.period, company=order.company
|
||||
)
|
||||
rows = prepare_data(
|
||||
filters,
|
||||
holder.targets,
|
||||
{holder.name: []},
|
||||
get_actual_data(filters, [holder.name], "transaction_date", field),
|
||||
"transaction_date",
|
||||
periods,
|
||||
field,
|
||||
)
|
||||
return rows[(holder.name, None)]
|
||||
|
||||
# Feed historical duplicate inputs to the report without writing invalid records.
|
||||
row = report_row()
|
||||
test.assertEqual(
|
||||
[flt(row[k], 2) for k in ("total_target", "total_achieved", "total_variance")], [2000, 600, -1400]
|
||||
)
|
||||
filters.period = "Monthly"
|
||||
row = report_row()
|
||||
test.assertEqual(
|
||||
[flt(row[k], 2) for k in ("total_target", "total_achieved", "total_variance")], [2000, 600, -1400]
|
||||
)
|
||||
test.assertEqual(
|
||||
sorted(flt(v, 2) for k, v in row.items() if k.startswith("target_")), [0] * 10 + [1000, 1000]
|
||||
)
|
||||
|
||||
@@ -7,6 +7,8 @@ from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.utils import cstr, filter_strip_join
|
||||
from frappe.website.website_generator import WebsiteGenerator
|
||||
|
||||
from erpnext.setup.doctype.target_detail.target_detail import validate_duplicate_targets
|
||||
|
||||
|
||||
class SalesPartner(WebsiteGenerator):
|
||||
# begin: auto-generated types
|
||||
@@ -47,6 +49,7 @@ class SalesPartner(WebsiteGenerator):
|
||||
self.name = self.partner_name
|
||||
|
||||
def validate(self):
|
||||
validate_duplicate_targets(self.get("targets"))
|
||||
if not self.route:
|
||||
self.route = "partners/" + self.scrub(self.partner_name)
|
||||
super().validate()
|
||||
|
||||
@@ -14,6 +14,7 @@ from frappe.utils.data import get_url_to_list
|
||||
from frappe.utils.nestedset import NestedSet, get_root_of
|
||||
|
||||
from erpnext import get_default_currency
|
||||
from erpnext.setup.doctype.target_detail.target_detail import validate_duplicate_targets
|
||||
|
||||
|
||||
class SalesPerson(NestedSet):
|
||||
@@ -43,6 +44,7 @@ class SalesPerson(NestedSet):
|
||||
nsm_parent_field = "parent_sales_person"
|
||||
|
||||
def validate(self):
|
||||
validate_duplicate_targets(self.get("targets"))
|
||||
if not self.enabled:
|
||||
self.validate_sales_person()
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
@@ -25,3 +27,14 @@ class TargetDetail(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def validate_duplicate_targets(targets):
|
||||
seen = set()
|
||||
for target in targets or []:
|
||||
key = (target.fiscal_year, target.item_group or None)
|
||||
if key in seen:
|
||||
frappe.throw(
|
||||
_("Row {0}: A target already exists for this fiscal year and item group.").format(target.idx)
|
||||
)
|
||||
seen.add(key)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.selling.report.sales_person_target_variance_based_on_item_group.test_sales_person_target_variance_based_on_item_group import (
|
||||
create_sales_target_doc,
|
||||
create_target_distribution,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestTargetDetail(ERPNextTestSuite):
|
||||
def test_duplicate_target_rows_are_rejected(self):
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
distribution = create_target_distribution(fiscal_year)
|
||||
for doctype, name_field in (
|
||||
("Territory", "territory_name"),
|
||||
("Sales Person", "sales_person_name"),
|
||||
("Sales Partner", "partner_name"),
|
||||
):
|
||||
holder = create_sales_target_doc(
|
||||
doctype, name_field, frappe.generate_hash(length=10), fiscal_year, distribution.name
|
||||
)
|
||||
for item_group in (None, "_Test Item Group A"):
|
||||
with self.subTest(doctype=doctype, item_group=item_group):
|
||||
holder.reload()
|
||||
holder.targets[0].item_group = item_group
|
||||
holder.append(
|
||||
"targets", dict(holder.targets[0].as_dict(), name=None, item_group=item_group or "")
|
||||
)
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "target already exists"):
|
||||
holder.save()
|
||||
|
||||
# Overall and specific item groups are separate targets.
|
||||
holder.reload()
|
||||
holder.targets[0].item_group = None
|
||||
holder.append(
|
||||
"targets", dict(holder.targets[0].as_dict(), name=None, item_group="_Test Item Group A")
|
||||
)
|
||||
holder.save()
|
||||
@@ -7,6 +7,8 @@ from frappe import _
|
||||
from frappe.utils import flt
|
||||
from frappe.utils.nestedset import NestedSet, get_root_of
|
||||
|
||||
from erpnext.setup.doctype.target_detail.target_detail import validate_duplicate_targets
|
||||
|
||||
|
||||
class Territory(NestedSet):
|
||||
# begin: auto-generated types
|
||||
@@ -32,6 +34,7 @@ class Territory(NestedSet):
|
||||
nsm_parent_field = "parent_territory"
|
||||
|
||||
def validate(self):
|
||||
validate_duplicate_targets(self.get("targets"))
|
||||
if not self.parent_territory:
|
||||
self.parent_territory = get_root_of("Territory")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user