feat: add Purchase Person reports mirroring Sales Person reports

Three new Script Reports in the Buying module:
- Purchase Person-wise Transaction Summary (mirrors Sales Person-wise)
- Purchase Person Commission Summary (mirrors Sales Person Commission)
- Purchase Person Target Variance Based On Item Group (mirrors Sales Person Target)

The shared `item_group_wise_sales_target_variance.get_actual_data` helper
gains a `purchase_person` branch that joins `Purchase Team` the same way
the existing `sales_person` branch joins `Sales Team`.

All three reports are added to the Buying workspace under a Purchase Person card.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dipen Gala
2026-06-17 13:12:54 +05:30
parent c8cb70dbd2
commit 1138effd7c
14 changed files with 771 additions and 20 deletions

View File

@@ -0,0 +1,51 @@
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.query_reports["Purchase Person Commission Summary"] = {
filters: [
{
fieldname: "purchase_person",
label: __("Purchase Person"),
fieldtype: "Link",
options: "Purchase Person",
},
{
fieldname: "doc_type",
label: __("Document Type"),
fieldtype: "Select",
options: "Purchase Order\nPurchase Receipt\nPurchase Invoice",
default: "Purchase Order",
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.datetime.get_today(),
},
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
},
{
fieldname: "supplier",
label: __("Supplier"),
fieldtype: "Link",
options: "Supplier",
},
{
fieldname: "territory",
label: __("Territory"),
fieldtype: "Link",
options: "Territory",
},
],
};

View File

@@ -0,0 +1,26 @@
{
"add_total_row": 1,
"creation": "2026-06-17 00:00:00",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"modified": "2026-06-17 00:00:00",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Person Commission Summary",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Order",
"report_name": "Purchase Person Commission Summary",
"report_type": "Script Report",
"roles": [
{
"role": "Purchase Manager"
},
{
"role": "Accounts User"
}
]
}

View File

@@ -0,0 +1,143 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from frappe import _, msgprint, qb
from frappe.query_builder import Criterion
def execute(filters=None):
if not filters:
filters = {}
columns = get_columns(filters)
entries = get_entries(filters)
data = []
for d in entries:
data.append(
[
d.name,
d.supplier,
d.territory,
d.posting_date,
d.base_net_amount,
d.purchase_person,
d.allocated_percentage,
d.commission_rate,
d.allocated_amount,
d.incentives,
]
)
if data:
total_row = [""] * len(data[0])
data.append(total_row)
return columns, data
def get_columns(filters):
if not filters.get("doc_type"):
msgprint(_("Please select the document type first"), raise_exception=1)
return [
{
"label": _(filters["doc_type"]),
"options": filters["doc_type"],
"fieldname": filters["doc_type"],
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Supplier"),
"options": "Supplier",
"fieldname": "supplier",
"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": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
{
"label": _("Purchase Person"),
"options": "Purchase Person",
"fieldname": "purchase_person",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Contribution %"),
"fieldname": "contribution_percentage",
"fieldtype": "Data",
"width": 110,
},
{
"label": _("Commission Rate %"),
"fieldname": "commission_rate",
"fieldtype": "Data",
"width": 100,
},
{
"label": _("Contribution Amount"),
"fieldname": "contribution_amount",
"fieldtype": "Currency",
"width": 120,
},
{"label": _("Incentives"), "fieldname": "incentives", "fieldtype": "Currency", "width": 120},
]
def get_entries(filters):
dt = qb.DocType(filters["doc_type"])
pt = qb.DocType("Purchase Team")
date_field = dt["transaction_date"] if filters["doc_type"] == "Purchase Order" else dt["posting_date"]
conditions = get_conditions(dt, pt, filters, date_field)
return (
qb.from_(dt)
.join(pt)
.on(pt.parent.eq(dt.name) & pt.parenttype.eq(filters["doc_type"]))
.select(
dt.name,
dt.supplier,
dt.territory,
date_field.as_("posting_date"),
dt.base_net_total.as_("base_net_amount"),
pt.commission_rate,
pt.purchase_person,
pt.allocated_percentage,
pt.allocated_amount,
pt.incentives,
)
.where(Criterion.all(conditions))
.orderby(dt.name, pt.purchase_person)
.run(as_dict=True)
)
def get_conditions(dt, pt, filters, date_field):
conditions = [dt.docstatus.eq(1)]
from_dt = filters.get("from_date")
to_dt = filters.get("to_date")
if from_dt and to_dt:
conditions.append(date_field.between(from_dt, to_dt))
elif from_dt:
conditions.append(date_field.gte(from_dt))
elif to_dt:
conditions.append(date_field.lte(to_dt))
for field in ["company", "supplier", "territory"]:
if filters.get(field):
conditions.append(dt[field].eq(filters.get(field)))
if filters.get("purchase_person"):
conditions.append(pt["purchase_person"].eq(filters.get("purchase_person")))
return conditions

View File

@@ -0,0 +1,60 @@
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.query_reports["Purchase Person Target Variance Based On Item Group"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
},
{
fieldname: "fiscal_year",
label: __("Fiscal Year"),
fieldtype: "Link",
options: "Fiscal Year",
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
},
{
fieldname: "doctype",
label: __("Document Type"),
fieldtype: "Select",
options: "Purchase Order\nPurchase Receipt\nPurchase Invoice",
default: "Purchase Order",
},
{
fieldname: "period",
label: __("Period"),
fieldtype: "Select",
options: [
{ value: "Monthly", label: __("Monthly") },
{ value: "Quarterly", label: __("Quarterly") },
{ value: "Half-Yearly", label: __("Half-Yearly") },
{ value: "Yearly", label: __("Yearly") },
],
default: "Monthly",
},
{
fieldname: "target_on",
label: __("Target On"),
fieldtype: "Select",
options: "Quantity\nAmount",
default: "Quantity",
},
],
formatter: function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname.includes("variance")) {
if (data[column.fieldname] < 0) {
value = "<span style='color:red'>" + value + "</span>";
} else if (data[column.fieldname] > 0) {
value = "<span style='color:green'>" + value + "</span>";
}
}
return value;
},
};

View File

@@ -0,0 +1,33 @@
{
"add_total_row": 0,
"creation": "2026-06-17 00:00:00",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"modified": "2026-06-17 00:00:00",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Person Target Variance Based On Item Group",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Order",
"report_name": "Purchase Person Target Variance Based On Item Group",
"report_type": "Script Report",
"roles": [
{
"role": "Purchase User"
},
{
"role": "Purchase Manager"
},
{
"role": "Accounts User"
},
{
"role": "Stock User"
}
]
}

View File

@@ -0,0 +1,11 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.item_group_wise_sales_target_variance import (
get_data_column,
)
def execute(filters=None):
return get_data_column(filters, "Purchase Person")

View File

@@ -0,0 +1,70 @@
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.query_reports["Purchase Person-wise Transaction Summary"] = {
filters: [
{
fieldname: "purchase_person",
label: __("Purchase Person"),
fieldtype: "Link",
options: "Purchase Person",
},
{
fieldname: "doc_type",
label: __("Document Type"),
fieldtype: "Select",
options: "Purchase Order\nPurchase Receipt\nPurchase Invoice",
default: "Purchase Order",
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
},
{
fieldname: "to_date",
label: __("To Date"),
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"),
fieldtype: "Link",
options: "Item Group",
},
{
fieldname: "brand",
label: __("Brand"),
fieldtype: "Link",
options: "Brand",
},
{
fieldname: "supplier",
label: __("Supplier"),
fieldtype: "Link",
options: "Supplier",
},
{
fieldname: "territory",
label: __("Territory"),
fieldtype: "Link",
options: "Territory",
},
{
fieldname: "show_return_entries",
label: __("Show Return Entries"),
fieldtype: "Check",
default: 0,
},
],
};

View File

@@ -0,0 +1,31 @@
{
"add_total_row": 1,
"creation": "2026-06-17 00:00:00",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"modified": "2026-06-17 00:00:00",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Person-wise Transaction Summary",
"owner": "Administrator",
"ref_doctype": "Purchase Order",
"report_name": "Purchase Person-wise Transaction Summary",
"report_type": "Script Report",
"roles": [
{
"role": "Purchase User"
},
{
"role": "Purchase Manager"
},
{
"role": "Accounts User"
},
{
"role": "Stock User"
}
]
}

View File

@@ -0,0 +1,276 @@
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe import _, msgprint, qb
from frappe.query_builder import Case, Criterion
from erpnext import get_company_currency
def execute(filters=None):
if not filters:
filters = {}
validate_filters(filters)
columns = get_columns(filters)
entries = get_entries(filters)
item_details = get_item_details()
data = []
company_currency = get_company_currency(filters.get("company"))
for d in entries:
if d.stock_qty > 0 or filters.get("show_return_entries", 0):
data.append(
[
d.name,
d.supplier,
d.territory,
d.warehouse,
d.posting_date,
d.item_code,
item_details.get(d.item_code, {}).get("item_group"),
item_details.get(d.item_code, {}).get("brand"),
d.stock_qty,
d.base_net_amount,
d.purchase_person,
d.allocated_percentage,
(d.stock_qty * d.allocated_percentage / 100),
d.contribution_amt,
company_currency,
]
)
if data:
total_row = [""] * len(data[0])
data.append(total_row)
return columns, data
def validate_filters(filters):
ALLOWED_DOCTYPES = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]
if not filters.get("doc_type"):
msgprint(_("Please select the document type first"), raise_exception=1)
if filters.get("doc_type") not in ALLOWED_DOCTYPES:
frappe.throw(_("{0}, {1} or {2} are the only allowed options.").format(*ALLOWED_DOCTYPES))
def get_columns(filters):
return [
{
"label": _(filters["doc_type"]),
"options": filters["doc_type"],
"fieldname": frappe.scrub(filters["doc_type"]),
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Supplier"),
"options": "Supplier",
"fieldname": "supplier",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Territory"),
"options": "Territory",
"fieldname": "territory",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Warehouse"),
"options": "Warehouse",
"fieldname": "warehouse",
"fieldtype": "Link",
"width": 140,
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 140},
{
"label": _("Item Code"),
"options": "Item",
"fieldname": "item_code",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Item Group"),
"options": "Item Group",
"fieldname": "item_group",
"fieldtype": "Link",
"width": 140,
},
{
"label": _("Brand"),
"options": "Brand",
"fieldname": "brand",
"fieldtype": "Link",
"width": 140,
},
{"label": _("Total Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 140},
{
"label": _("Amount"),
"options": "currency",
"fieldname": "amount",
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Purchase Person"),
"options": "Purchase Person",
"fieldname": "purchase_person",
"fieldtype": "Link",
"width": 140,
},
{"label": _("Contribution %"), "fieldname": "contribution", "fieldtype": "Float", "width": 140},
{
"label": _("Contribution Qty"),
"fieldname": "contribution_qty",
"fieldtype": "Float",
"width": 140,
},
{
"label": _("Contribution Amount"),
"options": "currency",
"fieldname": "contribution_amt",
"fieldtype": "Currency",
"width": 140,
},
{
"label": _("Currency"),
"options": "Currency",
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
},
]
def get_entries(filters):
doc_type = filters["doc_type"]
date_field = "transaction_date" if doc_type == "Purchase Order" else "posting_date"
qty_field = "received_qty" if doc_type == "Purchase Order" else "qty"
dt = frappe.qb.DocType(doc_type)
dt_item = frappe.qb.DocType(f"{doc_type} Item")
pt = frappe.qb.DocType("Purchase Team")
calc_qty = dt_item[qty_field] * dt_item.conversion_factor
calc_net_amount = dt_item.base_net_rate * calc_qty
stock_qty_case = Case().when(dt.status == "Closed", calc_qty).else_(dt_item.stock_qty).as_("stock_qty")
base_net_amount_case = (
Case()
.when(dt.status == "Closed", calc_net_amount)
.else_(dt_item.base_net_amount)
.as_("base_net_amount")
)
contribution_amt_case = (
Case()
.when(dt.status == "Closed", (calc_net_amount * pt.allocated_percentage / 100))
.else_(dt_item.base_net_amount * pt.allocated_percentage / 100)
.as_("contribution_amt")
)
conditions = get_conditions(dt, pt, filters, date_field)
query = (
frappe.qb.from_(dt)
.join(dt_item)
.on(dt.name == dt_item.parent)
.join(pt)
.on(dt.name == pt.parent)
.select(
dt.name,
dt.supplier,
dt.territory,
dt[date_field].as_("posting_date"),
dt_item.item_code,
pt.purchase_person,
pt.allocated_percentage,
dt_item.warehouse,
stock_qty_case,
base_net_amount_case,
contribution_amt_case,
)
.where(pt.parenttype == doc_type)
.where(dt.docstatus == 1)
.where(Criterion.all(conditions))
.orderby(pt.purchase_person)
.orderby(dt.name, order=frappe.qb.desc)
)
return query.run(as_dict=True)
def get_conditions(dt, pt, filters, date_field):
conditions = []
for field in ["company", "supplier", "territory"]:
if filters.get(field):
conditions.append(dt[field].eq(filters[field]))
if filters.get("purchase_person"):
lft, rgt = frappe.get_value("Purchase Person", filters.get("purchase_person"), ["lft", "rgt"])
purchase_person_tbl = frappe.qb.DocType("Purchase Person")
subquery = (
frappe.qb.from_(purchase_person_tbl)
.select(purchase_person_tbl.name)
.where(purchase_person_tbl.lft >= lft)
.where(purchase_person_tbl.rgt <= rgt)
)
conditions.append(pt.purchase_person.isin(subquery))
if filters.get("from_date"):
conditions.append(dt[date_field].gte(filters["from_date"]))
if filters.get("to_date"):
conditions.append(dt[date_field].lte(filters["to_date"]))
items = get_items(filters)
if items:
conditions.append(
frappe.qb.DocType(f"{filters['doc_type']} Item").item_code.isin([i[0] for i in items])
)
elif filters.get("item_group") or filters.get("brand"):
conditions.append(frappe.qb.terms.ValueWrapper(0).eq(1))
return conditions
def get_items(filters):
item = qb.DocType("Item")
item_query_conditions = []
if filters.get("item_group"):
item_group = qb.DocType("Item Group")
lft, rgt = frappe.db.get_all(
"Item Group", filters={"name": filters.get("item_group")}, fields=["lft", "rgt"], as_list=True
)[0]
item_group_query = (
qb.from_(item_group)
.select(item_group.name)
.where((item_group.lft >= lft) & (item_group.rgt <= rgt))
)
item_query_conditions.append(item.item_group.isin(item_group_query))
if filters.get("brand"):
item_query_conditions.append(item.brand == filters.get("brand"))
if not item_query_conditions:
return []
return qb.from_(item).select(item.name).where(Criterion.all(item_query_conditions)).run()
def get_item_details():
items = frappe.get_all("Item", fields=["name", "item_group", "brand"])
return {d.name: d for d in items}

View File

@@ -23,6 +23,7 @@
"is_query_report": 0,
"label": "Buying",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -86,6 +87,7 @@
"is_query_report": 0,
"label": "Items & Pricing",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -171,6 +173,7 @@
"is_query_report": 0,
"label": "Settings",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -212,6 +215,7 @@
"is_query_report": 0,
"label": "Supplier",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -264,6 +268,7 @@
"is_query_report": 0,
"label": "Supplier Scorecard",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -316,6 +321,7 @@
"is_query_report": 0,
"label": "Key Reports",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -390,6 +396,7 @@
"is_query_report": 0,
"label": "Purchase Partner",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -415,25 +422,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Purchase Person",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Purchase Person",
"link_count": 0,
"link_to": "Purchase Person",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Purchase Partner",
"hidden": 0,
@@ -467,11 +455,65 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Purchase Person",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Purchase Person",
"link_count": 0,
"link_to": "Purchase Person",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Purchase Person",
"hidden": 0,
"is_query_report": 1,
"label": "Purchase Person-wise Transaction Summary",
"link_count": 0,
"link_to": "Purchase Person-wise Transaction Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Purchase Person",
"hidden": 0,
"is_query_report": 1,
"label": "Purchase Person Commission Summary",
"link_count": 0,
"link_to": "Purchase Person Commission Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Purchase Person",
"hidden": 0,
"is_query_report": 1,
"label": "Purchase Person Target Variance Based On Item Group",
"link_count": 0,
"link_to": "Purchase Person Target Variance Based On Item Group",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Other Reports",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -579,6 +621,7 @@
"is_query_report": 0,
"label": "Regional",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -594,7 +637,7 @@
"type": "Link"
}
],
"modified": "2026-01-02 14:55:59.078773",
"modified": "2026-06-17 13:01:33.296280",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying",

View File

@@ -243,6 +243,13 @@ def get_actual_data(filters, sales_users_or_territory_data, date_field, sales_fi
sales_field_col = sales_team[sales_field]
query = query.inner_join(sales_team).on(sales_team.parent == parent_doc.name)
elif sales_field == "purchase_person":
purchase_team = frappe.qb.DocType("Purchase Team")
stock_qty = child_doc.stock_qty * purchase_team.allocated_percentage / 100
net_amount = child_doc.base_net_amount * purchase_team.allocated_percentage / 100
sales_field_col = purchase_team[sales_field]
query = query.inner_join(purchase_team).on(purchase_team.parent == parent_doc.name)
else:
stock_qty = child_doc.stock_qty
net_amount = child_doc.base_net_amount