mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-26 18:18:30 +00:00
refactor: Sales Partner Commission Summary and Sales Partner Transaction Summary report (#54268)
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -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"),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user