refactor: Sales Partner Commission Summary and Sales Partner Transaction Summary report (#54268)

This commit is contained in:
diptanilsaha
2026-04-21 08:42:22 +05:30
committed by GitHub
parent 0edee23e53
commit 11fc3e5495
5 changed files with 794 additions and 222 deletions

View File

@@ -1,122 +1,176 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, msgprint
from frappe import _
from frappe.query_builder import DocType, Field, Order
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.utils import QueryBuilder
from frappe.utils.data import comma_or
SALES_TRANSACTION_DOCTYPES = ["Sales Order", "Sales Invoice", "Delivery Note", "POS Invoice"]
def execute(filters=None):
if not filters:
filters = {}
columns = get_columns(filters)
data = get_entries(filters)
return columns, data
return SalesPartnerCommissionSummaryReport(filters).run()
def get_columns(filters):
if not filters.get("doctype"):
msgprint(_("Please select the document type first"), raise_exception=1)
class SalesPartnerSummaryReport:
"""
Base class to generate Sales Partner Summary related Reports.
"""
columns = [
{
"label": _(filters["doctype"]),
"options": filters["doctype"],
"fieldname": "name",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Customer"),
"options": "Customer",
"fieldname": "customer",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Currency"),
"fieldname": "currency",
"fieldtype": "Data",
"width": 80,
},
{
"label": _("Territory"),
"options": "Territory",
"fieldname": "territory",
"fieldtype": "Link",
"width": 100,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{
"label": _("Amount"),
"fieldname": "amount",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{
"label": _("Sales Partner"),
"options": "Sales Partner",
"fieldname": "sales_partner",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Commission Rate %"),
"fieldname": "commission_rate",
"fieldtype": "Data",
"width": 100,
},
{
"label": _("Total Commission"),
"fieldname": "total_commission",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
]
dt: DocType
date_field: str
date_label: str
columns: list
data: list
query: QueryBuilder
filters: dict
return columns
def __init__(self, filters: dict):
self.filters = filters
self.columns = []
def run(self):
self.validate_filters()
self.prepare_columns()
self.get_data()
def get_entries(filters):
date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date"
company_currency = frappe.db.get_value("Company", filters.get("company"), "default_currency")
conditions = get_conditions(filters, date_field)
entries = frappe.db.sql(
return self.columns, self.data
def validate_filters(self):
if not self.filters.get("doctype"):
frappe.throw(_("Please select the document type first."))
if self.filters.get("doctype") not in SALES_TRANSACTION_DOCTYPES:
frappe.throw(_("DocType can be one of them {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)))
if not self.filters.get("company"):
frappe.throw(_("Please select a company."))
if (
self.filters.get("from_date")
and self.filters.get("to_date")
and self.filters.get("from_date") > self.filters.get("to_date")
):
frappe.throw(_("From Date cannot be greater than To Date."))
self._set_date_field_and_label()
def _set_date_field_and_label(self):
self.date_field = (
"transaction_date" if self.filters.get("doctype") == "Sales Order" else "posting_date"
)
self.date_label = _("Order Date") if self.date_field == "transaction_date" else _("Posting Date")
def prepare_columns(self):
"""
SELECT
name, customer, territory, {} as posting_date, base_net_total as amount,
sales_partner, commission_rate, total_commission, '{}' as currency
FROM
`tab{}`
WHERE
{} and docstatus = 1 and sales_partner is not null
and sales_partner != '' order by name desc, sales_partner
""".format(date_field, company_currency, filters.get("doctype"), conditions),
filters,
as_dict=1,
)
Extend this method to add columns on the report. Use `make_column` to add more columns.
"""
raise NotImplementedError
return entries
def get_data(self):
self.build_report_query()
self.data = self.query.run(as_dict=1)
def build_report_query(self):
self._build_report_base_query()
self.extend_report_query()
self._apply_common_filters()
self.apply_filters()
def _build_report_base_query(self):
self.dt = DocType(self.filters.get("doctype"))
company_currency = frappe.get_cached_value("Company", self.filters.get("company"), "default_currency")
self.query = (
frappe.qb.from_(self.dt)
.select(
self.dt.name,
self.dt.customer,
self.dt.territory,
Field(self.date_field, "posting_date", table=self.dt),
self.dt.sales_partner,
self.dt.commission_rate,
ConstantColumn(company_currency).as_("currency"),
)
.where(
(self.dt.docstatus == 1) & (self.dt.sales_partner.notnull()) & (self.dt.sales_partner != "")
)
.orderby(self.dt.name, order=Order.desc)
.orderby(self.dt.sales_partner)
)
def extend_report_query(self):
"""
Extend this method to select more columns on the query.
"""
pass
def _apply_common_filters(self):
for field in ["company", "customer", "territory", "sales_partner"]:
if self.filters.get(field):
self.query = self.query.where(Field(field, table=self.dt) == self.filters.get(field))
if self.filters.get("from_date"):
self.query = self.query.where(
Field(self.date_field, table=self.dt) >= self.filters.get("from_date")
)
if self.filters.get("to_date"):
self.query = self.query.where(
Field(self.date_field, table=self.dt) <= self.filters.get("to_date")
)
def apply_filters(self):
"""
Extend this method to add more conditions on the query.
"""
pass
def make_column(
self, label: str, fieldname: str, fieldtype: str, width: int = 140, options: str = "", hidden: int = 0
):
self.columns.append(
dict(
label=label,
fieldname=fieldname,
fieldtype=fieldtype,
options=options,
width=width,
hidden=hidden,
)
)
def get_conditions(filters, date_field):
conditions = "1=1"
class SalesPartnerCommissionSummaryReport(SalesPartnerSummaryReport):
def prepare_columns(self):
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
for field in ["company", "customer", "territory"]:
if filters.get(field):
conditions += f" and {field} = %({field})s"
self.make_column(_("Customer"), "customer", "Link", options="Customer")
if filters.get("sales_partner"):
conditions += " and sales_partner = %(sales_partner)s"
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
if filters.get("from_date"):
conditions += f" and {date_field} >= %(from_date)s"
self.make_column(_("Territory"), "territory", "Link", 100, "Territory")
if filters.get("to_date"):
conditions += f" and {date_field} <= %(to_date)s"
self.make_column(self.date_label, "posting_date", "Date")
return conditions
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
self.make_column(_("Sales Partner"), "sales_partner", "Link", options="Sales Partner")
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
self.make_column(_("Total Commission"), "total_commission", "Currency", 120, "currency")
def extend_report_query(self):
self.query = self.query.select(
self.dt.base_net_total.as_("amount"),
self.dt.total_commission,
)

View File

@@ -0,0 +1,395 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.desk.query_report import run
from frappe.utils.data import comma_or
from erpnext.selling.report.sales_partner_commission_summary.sales_partner_commission_summary import (
SALES_TRANSACTION_DOCTYPES,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.tests.utils import ERPNextTestSuite
class SalesPartnerSummaryReportTestMixin(ERPNextTestSuite):
def assert_doctype_filters(self):
self.filters["doctype"] = "Purchase Invoice"
with self.assertRaisesRegex(
frappe.ValidationError,
_("DocType can be one of them {0}").format(comma_or(SALES_TRANSACTION_DOCTYPES)),
):
run(self.report_name, self.filters)
def assert_posting_date_label(self):
data = run(self.report_name, self.filters)
posting_date_column = next(
(column for column in data.get("columns") if column.fieldname == "posting_date"), None
)
self.assertNotEqual(posting_date_column.get("label"), "Posting Date")
self.assertEqual(posting_date_column.get("label"), "Order Date")
self.filters["doctype"] = "Sales Invoice"
data = run(self.report_name, self.filters)
posting_date_column = next(
(column for column in data.get("columns") if column.fieldname == "posting_date"), None
)
self.assertEqual(posting_date_column.get("label"), "Posting Date")
self.assertNotEqual(posting_date_column.get("label"), "Order Date")
def create_transactions(self, doctype):
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import (
POSInvoiceTestMixin,
create_pos_invoice,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
make_transaction_funcs = {
"Sales Order": make_sales_order,
"Sales Invoice": create_sales_invoice,
"Delivery Note": create_delivery_note,
"POS Invoice": create_pos_invoice,
}
self.date_field = "transaction_date" if doctype == "Sales Order" else "posting_date"
self.make_transaction_func = make_transaction_funcs[doctype]
make_stock_entry(
item_code="_Test Item 2",
qty=10,
company="_Test Company",
to_warehouse="_Test Warehouse - _TC",
purpose="Material Receipt",
posting_date="2026-01-01",
)
if doctype == "POS Invoice":
POSInvoiceTestMixin.setUp(self)
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
pos_opening_entry = create_opening_entry(self.pos_profile, self.test_user.name, get_obj=1)
self.transaction_doc_with_7pc_commision()
self.transaction_doc_with_5pc_commission()
self.transaction_doc_with_no_sales_partner()
self.transaction_doc_date_out_of_range_of_filters()
self.transaction_doc_with_revoked_commission()
self.transaction_doc_not_submitted()
self.transaction_doc_cancelled()
if doctype == "Sales Order":
return
self.transaction_doc_returned()
if doctype == "POS Invoice":
pos_opening_entry.cancel()
def transaction_doc_with_7pc_commision(self):
args = {"rate": 100, "qty": 10, self.date_field: "2026-01-14", "do_not_save": 1}
self.seven_pc_doc = self.make_transaction_func(**args)
self.seven_pc_doc.sales_partner = "_Test Sales Partner India - 1"
if self.seven_pc_doc.doctype == "POS Invoice":
self.seven_pc_doc.append("payments", {"mode_of_payment": "Cash", "amount": 1000, "default": 1})
self.seven_pc_doc.save()
self.seven_pc_doc.submit()
def transaction_doc_with_5pc_commission(self):
args = {"rate": 20, "qty": 6, self.date_field: "2026-01-15", "do_not_save": 1}
self.five_pc_doc = self.make_transaction_func(**args)
self.five_pc_doc.sales_partner = "_Test Sales Partner India - 2"
self.five_pc_doc.append(
"items",
{
"item_code": "_Test Item 2",
"qty": 4,
"rate": 30,
},
)
if self.five_pc_doc.doctype == "POS Invoice":
self.five_pc_doc.append("payments", {"mode_of_payment": "Cash", "amount": 500, "default": 1})
self.five_pc_doc.save()
self.five_pc_doc.submit()
def transaction_doc_with_no_sales_partner(self):
args = {
"item_code": "_Test Item",
"rate": 50,
"qty": 10,
self.date_field: "2026-01-19",
"do_not_save": 1,
}
self.no_sp_doc = self.make_transaction_func(**args)
if self.no_sp_doc.doctype == "POS Invoice":
self.no_sp_doc.append("payments", {"mode_of_payment": "Cash", "amount": 500, "default": 1})
self.no_sp_doc.save()
self.no_sp_doc.submit()
def transaction_doc_date_out_of_range_of_filters(self):
args = {
"item_code": "_Test Item",
"rate": 60,
"qty": 10,
self.date_field: "2026-02-04",
"do_not_save": 1,
}
self.date_out_of_range_doc = self.make_transaction_func(**args)
self.date_out_of_range_doc.sales_partner = "_Test Sales Partner India - 1"
if self.date_out_of_range_doc.doctype == "POS Invoice":
self.date_out_of_range_doc.append(
"payments", {"mode_of_payment": "Cash", "amount": 600, "default": 1}
)
self.date_out_of_range_doc.save()
self.date_out_of_range_doc.submit()
def transaction_doc_with_revoked_commission(self):
try:
frappe.db.set_value("Item", "_Test Item", "grant_commission", 0)
args = {
"item_code": "_Test Item",
"rate": 80,
"qty": 10,
self.date_field: "2026-01-26",
"do_not_save": 1,
}
self.revoked_comm_doc = self.make_transaction_func(**args)
self.revoked_comm_doc.sales_partner = "_Test Sales Partner India - 1"
if self.revoked_comm_doc.doctype == "POS Invoice":
self.revoked_comm_doc.append(
"payments", {"mode_of_payment": "Cash", "amount": 800, "default": 1}
)
self.revoked_comm_doc.save()
self.revoked_comm_doc.submit()
finally:
frappe.db.set_value("Item", "_Test Item", "grant_commission", 1)
def transaction_doc_not_submitted(self):
args = {
"item_code": "_Test Item",
"rate": 80,
"qty": 10,
self.date_field: "2026-01-26",
"do_not_save": 1,
}
self.doc_not_submitted = self.make_transaction_func(**args)
self.doc_not_submitted.set(self.date_field, "2026-01-26")
self.doc_not_submitted.sales_partner = "_Test Sales Partner India - 1"
if self.doc_not_submitted.doctype == "POS Invoice":
self.doc_not_submitted.append(
"payments", {"mode_of_payment": "Cash", "amount": 800, "default": 1}
)
self.doc_not_submitted.save()
def transaction_doc_cancelled(self):
args = {
"item_code": "_Test Item",
"rate": 80,
"qty": 10,
self.date_field: "2026-01-26",
"do_not_save": 1,
}
self.cancelled_doc = self.make_transaction_func(**args)
self.cancelled_doc.sales_partner = "_Test Sales Partner India - 1"
if self.cancelled_doc.doctype == "POS Invoice":
self.cancelled_doc.append("payments", {"mode_of_payment": "Cash", "amount": 800, "default": 1})
self.cancelled_doc.save()
self.cancelled_doc.submit()
self.cancelled_doc.cancel()
def transaction_doc_returned(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
args = {
"item_code": "_Test Item",
"rate": 90,
"qty": 10,
self.date_field: "2026-01-18",
"do_not_save": 1,
}
self.to_be_returned_doc = self.make_transaction_func(**args)
self.to_be_returned_doc.sales_partner = "_Test Sales Partner India - 2"
if self.to_be_returned_doc.doctype == "POS Invoice":
self.to_be_returned_doc.append(
"payments", {"mode_of_payment": "Cash", "amount": 900, "default": 1}
)
self.to_be_returned_doc.save()
self.to_be_returned_doc.submit()
self.returned_doc = make_return_doc(self.to_be_returned_doc.doctype, self.to_be_returned_doc.name)
self.returned_doc.posting_date = "2026-01-19"
if self.returned_doc.doctype == "POS Invoice":
self.returned_doc.payments = []
self.returned_doc.append("payments", {"mode_of_payment": "Cash", "amount": -900, "default": 1})
self.returned_doc.save()
self.returned_doc.submit()
class TestSalesPartnerCommissionSummary(SalesPartnerSummaryReportTestMixin):
def setUp(self):
self.filters = {
"company": "_Test Company",
"doctype": "Sales Order",
"from_date": "2026-01-01",
"to_date": "2026-01-31",
}
self.report_name = "Sales Partner Commission Summary"
def test_doctype_filters(self):
self.assert_doctype_filters()
def test_posting_date_column_label(self):
self.assert_posting_date_label()
def test_sales_order_sp_commission_summary(self):
self.filters["doctype"] = "Sales Order"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_commission_summary_report()
def test_sales_invoice_sp_commission_summary(self):
self.filters["doctype"] = "Sales Invoice"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_commission_summary_report()
def test_delivery_note_sp_commission_summary(self):
self.filters["doctype"] = "Delivery Note"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_commission_summary_report()
def test_pos_invoice_sp_commission_summary(self):
self.filters["doctype"] = "POS Invoice"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_commission_summary_report()
def assert_sales_partner_commission_summary_report(self):
report_data = run(self.report_name, self.filters)
self.report_result = report_data.get("result")
self.report_result_without_total_row = self.report_result[:-1]
self.assertIsNotNone(self.report_result_without_total_row)
self.assert_7pc_commission()
self.assert_5pc_commission_with_multiple_items()
self.assert_doc_with_no_sp()
self.assert_doc_with_posting_date_out_of_range()
self.assert_doc_with_revoked_commission()
self.assert_doc_not_submitted()
self.assert_doc_cancelled()
self.assert_total_commission()
if self.filters["doctype"] != "Sales Order":
self.assert_returned_doc()
def assert_7pc_commission(self):
doc_name = self.seven_pc_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNotNone(row)
self.assertEqual(row["amount"], 1000)
self.assertEqual(row["commission_rate"], 7)
self.assertEqual(row["total_commission"], 70)
def assert_5pc_commission_with_multiple_items(self):
doc_name = self.five_pc_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNotNone(row)
self.assertEqual(row["amount"], 240)
self.assertEqual(row["commission_rate"], 5)
self.assertEqual(row["total_commission"], 12)
def assert_doc_with_no_sp(self):
doc_name = self.no_sp_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_doc_with_posting_date_out_of_range(self):
doc_name = self.date_out_of_range_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_doc_with_revoked_commission(self):
doc_name = self.revoked_comm_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNotNone(row)
self.assertEqual(row["amount"], 800)
self.assertEqual(row["commission_rate"], 7)
self.assertEqual(row["total_commission"], 0)
def assert_doc_not_submitted(self):
doc_name = self.doc_not_submitted.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_doc_cancelled(self):
doc_name = self.cancelled_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_total_commission(self):
total_row = self.report_result[-1]
# Total Amount
self.assertEqual(total_row[-4], 2040)
# Total Commission
self.assertEqual(total_row[-1], 82)
def assert_returned_doc(self):
doc_name = self.to_be_returned_doc.name
returned_doc_name = self.returned_doc.name
outward_row = next(
(row for row in self.report_result_without_total_row if row.get("name") == doc_name), None
)
inward_row = next(
(row for row in self.report_result_without_total_row if row.get("name") == returned_doc_name),
None,
)
self.assertIsNotNone(outward_row)
self.assertIsNotNone(inward_row)
self.assertEqual(outward_row["amount"], 900)
self.assertEqual(outward_row["total_commission"], 45)
self.assertEqual(inward_row["amount"], -900)
self.assertEqual(inward_row["total_commission"], -45)

View File

@@ -3,6 +3,14 @@
frappe.query_reports["Sales Partner Transaction Summary"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
},
{
fieldname: "sales_partner",
label: __("Sales Partner"),
@@ -28,14 +36,6 @@ frappe.query_reports["Sales Partner Transaction Summary"] = {
fieldtype: "Date",
default: frappe.datetime.get_today(),
},
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
},
{
fieldname: "item_group",
label: __("Item Group"),

View File

@@ -3,144 +3,84 @@
import frappe
from frappe import _, msgprint
from frappe import _
from frappe.query_builder import Case
from erpnext.selling.report.sales_partner_commission_summary.sales_partner_commission_summary import (
SalesPartnerSummaryReport,
)
def execute(filters=None):
if not filters:
filters = {}
columns = get_columns(filters)
data = get_entries(filters)
return columns, data
return SalesPartnerTransactionSummaryReport(filters=filters).run()
def get_columns(filters):
if not filters.get("doctype"):
msgprint(_("Please select the document type first"), raise_exception=1)
class SalesPartnerTransactionSummaryReport(SalesPartnerSummaryReport):
def prepare_columns(self):
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
columns = [
{
"label": _(filters["doctype"]),
"options": filters["doctype"],
"fieldname": "name",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Customer"),
"options": "Customer",
"fieldname": "customer",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Territory"),
"options": "Territory",
"fieldname": "territory",
"fieldtype": "Link",
"width": 100,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
{
"label": _("Item Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 100,
},
{
"label": _("Item Group"),
"fieldname": "item_group",
"fieldtype": "Link",
"options": "Item Group",
"width": 100,
},
{
"label": _("Brand"),
"fieldname": "brand",
"fieldtype": "Link",
"options": "Brand",
"width": 100,
},
{"label": _("Quantity"), "fieldname": "qty", "fieldtype": "Float", "width": 120},
{"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 120},
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
{
"label": _("Sales Partner"),
"options": "Sales Partner",
"fieldname": "sales_partner",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Commission Rate %"),
"fieldname": "commission_rate",
"fieldtype": "Data",
"width": 100,
},
{"label": _("Commission"), "fieldname": "commission", "fieldtype": "Currency", "width": 120},
{
"label": _("Currency"),
"fieldname": "currency",
"fieldtype": "Link",
"options": "Currency",
"width": 120,
},
]
self.make_column(_("Customer"), "customer", "Link", options="Customer")
return columns
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
self.make_column(_("Territory"), "territory", "Link", 100, "Territory")
def get_entries(filters):
date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date"
self.make_column(self.date_label, "posting_date", "Date")
conditions = get_conditions(filters, date_field)
entries = frappe.db.sql(
"""
SELECT
dt.name, dt.customer, dt.territory, dt.{date_field} as posting_date, dt.currency,
dt_item.base_net_rate as rate, dt_item.qty, dt_item.base_net_amount as amount,
((dt_item.base_net_amount * dt.commission_rate) / 100) as commission,
dt_item.brand, dt.sales_partner, dt.commission_rate, dt_item.item_group, dt_item.item_code
FROM
`tab{doctype}` dt, `tab{doctype} Item` dt_item
WHERE
{cond} and dt.name = dt_item.parent and dt.docstatus = 1
and dt.sales_partner is not null and dt.sales_partner != ''
order by dt.name desc, dt.sales_partner
""".format(date_field=date_field, doctype=filters.get("doctype"), cond=conditions),
filters,
as_dict=1,
)
self.make_column(_("Item Code"), "item_code", "Link", 100, "Item")
return entries
self.make_column(_("Item Group"), "item_group", "Link", 100, "Item Group")
self.make_column(_("Brand"), "brand", "Link", 100, "Brand")
def get_conditions(filters, date_field):
conditions = "1=1"
self.make_column(_("Quantity"), "qty", "Float", 120)
for field in ["company", "customer", "territory", "sales_partner"]:
if filters.get(field):
conditions += f" and dt.{field} = %({field})s"
self.make_column(_("Rate"), "rate", "Currency", 120, "currency")
if filters.get("from_date"):
conditions += f" and dt.{date_field} >= %(from_date)s"
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
if filters.get("to_date"):
conditions += f" and dt.{date_field} <= %(to_date)s"
self.make_column(_("Sales Partner"), "sales_partner", "Link", options="Sales Partner")
if not filters.get("show_return_entries"):
conditions += " and dt_item.qty > 0.0"
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
if filters.get("brand"):
conditions += " and dt_item.brand = %(brand)s"
self.make_column(_("Commission"), "commission", "Currency", 120, "currency")
if filters.get("item_group"):
lft, rgt = frappe.get_cached_value("Item Group", filters.get("item_group"), ["lft", "rgt"])
def extend_report_query(self):
self.dt_item = frappe.qb.DocType(f"{self.filters['doctype']} Item")
conditions += f""" and dt_item.item_group in (select name from
`tabItem Group` where lft >= {lft} and rgt <= {rgt})"""
self.query = (
self.query.join(self.dt_item)
.on(self.dt.name == self.dt_item.parent)
.select(
self.dt_item.base_net_rate.as_("rate"),
self.dt_item.qty,
self.dt_item.base_net_amount.as_("amount"),
Case()
.when(
self.dt_item.grant_commission.eq(1),
((self.dt_item.base_net_amount * self.dt.commission_rate) / 100),
)
.else_(0)
.as_("commission"),
self.dt_item.brand,
self.dt_item.item_group,
self.dt_item.item_code,
)
)
return conditions
def apply_filters(self):
if not self.filters.get("show_return_entries"):
self.query = self.query.where(self.dt_item.qty > 0.0)
if self.filters.get("brand"):
self.query = self.query.where(self.dt_item.brand == self.filters.get("brand"))
if self.filters.get("item_group"):
lft, rgt = frappe.get_cached_value("Item Group", self.filters.get("item_group"), ["lft", "rgt"])
if item_groups := frappe.get_all(
"Item Group", filters=[["lft", ">=", lft], ["rgt", "<=", rgt]], pluck="name"
):
self.query = self.query.where(self.dt_item.item_group.isin(item_groups))

View File

@@ -0,0 +1,183 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.desk.query_report import run
from erpnext.selling.report.sales_partner_commission_summary.test_sales_partner_commission_summary import (
SalesPartnerSummaryReportTestMixin,
)
class TestSalesPartnerTransactionSummary(SalesPartnerSummaryReportTestMixin):
def setUp(self):
self.filters = {
"company": "_Test Company",
"doctype": "Sales Order",
"from_date": "2026-01-01",
"to_date": "2026-01-31",
"show_return_entries": 1,
}
self.report_name = "Sales Partner Transaction Summary"
def test_doctype_filters(self):
self.assert_doctype_filters()
def test_posting_date_column_label(self):
self.assert_posting_date_label()
def test_sales_order_sp_transaction_summary(self):
self.filters["doctype"] = "Sales Order"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_transaction_summary_report()
def test_sales_invoice_sp_transaction_summary(self):
self.filters["doctype"] = "Sales Invoice"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_transaction_summary_report()
def test_delivery_note_sp_transaction_summary(self):
self.filters["doctype"] = "Delivery Note"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_transaction_summary_report()
def test_pos_invoice_sp_transaction_summary(self):
self.filters["doctype"] = "POS Invoice"
self.create_transactions(self.filters["doctype"])
self.assert_sales_partner_transaction_summary_report()
def assert_sales_partner_transaction_summary_report(self):
report_data = run(self.report_name, self.filters)
self.report_result = report_data.get("result")
self.report_result_without_total_row = self.report_result[:-1]
self.assertIsNotNone(self.report_result_without_total_row)
self.assert_7pc_commission()
self.assert_5pc_commission_with_multiple_items()
self.assert_doc_with_no_sp()
self.assert_doc_with_posting_date_out_of_range()
self.assert_doc_with_revoked_commission()
self.assert_doc_not_submitted()
self.assert_doc_cancelled()
self.assert_commission()
if self.filters["doctype"] != "Sales Order":
self.assert_returned_doc()
def assert_7pc_commission(self):
doc_name = self.seven_pc_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNotNone(row)
self.assertEqual(row["customer"], "_Test Customer")
self.assertEqual(row["item_code"], "_Test Item")
self.assertEqual(row["item_group"], "_Test Item Group")
self.assertEqual(row["amount"], 1000)
self.assertEqual(row["commission_rate"], 7)
self.assertEqual(row["commission"], 70)
def assert_5pc_commission_with_multiple_items(self):
doc_name = self.five_pc_doc.name
row1 = next(
(
row
for row in self.report_result_without_total_row
if row.get("name") == doc_name and row.get("item_code") == "_Test Item"
),
None,
)
self.assertIsNotNone(row1)
row2 = next(
(
row
for row in self.report_result_without_total_row
if row.get("name") == doc_name and row.get("item_code") == "_Test Item 2"
),
None,
)
self.assertIsNotNone(row2)
self.assertEqual(row1["amount"], 120)
self.assertEqual(row1["commission_rate"], 5)
self.assertEqual(row1["commission"], 6)
self.assertEqual(row2["amount"], 120)
self.assertEqual(row2["commission_rate"], 5)
self.assertEqual(row2["commission"], 6)
def assert_doc_with_no_sp(self):
doc_name = self.no_sp_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_doc_with_posting_date_out_of_range(self):
doc_name = self.date_out_of_range_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_doc_with_revoked_commission(self):
doc_name = self.revoked_comm_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNotNone(row)
self.assertEqual(row["amount"], 800)
self.assertEqual(row["commission_rate"], 7)
self.assertEqual(row["commission"], 0)
def assert_doc_not_submitted(self):
doc_name = self.doc_not_submitted.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_doc_cancelled(self):
doc_name = self.cancelled_doc.name
row = next((row for row in self.report_result_without_total_row if row.get("name") == doc_name), None)
self.assertIsNone(row)
def assert_commission(self):
total_row = self.report_result[-1]
# Total Amount
self.assertEqual(total_row[-4], 2040)
# Total Commission
self.assertEqual(total_row[-1], 82)
def assert_returned_doc(self):
doc_name = self.to_be_returned_doc.name
returned_doc_name = self.returned_doc.name
outward_row = next(
(row for row in self.report_result_without_total_row if row.get("name") == doc_name), None
)
inward_row = next(
(row for row in self.report_result_without_total_row if row.get("name") == returned_doc_name),
None,
)
self.assertIsNotNone(outward_row)
self.assertIsNotNone(inward_row)
self.assertEqual(outward_row["amount"], 900)
self.assertEqual(outward_row["commission"], 45)
self.assertEqual(inward_row["amount"], -900)
self.assertEqual(inward_row["commission"], -45)