mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-19 04:42:40 +00:00
Compare commits
10 Commits
l10n_devel
...
feat-purch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87b65d09df | ||
|
|
1631985726 | ||
|
|
5284b8c7a8 | ||
|
|
a674d9216a | ||
|
|
9dfb60f826 | ||
|
|
1138effd7c | ||
|
|
c8cb70dbd2 | ||
|
|
5249274bcd | ||
|
|
56ed9e8e43 | ||
|
|
0147312951 |
@@ -167,6 +167,14 @@
|
||||
"terms_section_break",
|
||||
"tc_name",
|
||||
"terms",
|
||||
"commission_section",
|
||||
"purchase_partner",
|
||||
"amount_eligible_for_commission",
|
||||
"column_break_commission",
|
||||
"commission_rate",
|
||||
"total_commission",
|
||||
"purchase_team_section",
|
||||
"purchase_team",
|
||||
"more_info_tab",
|
||||
"status_section",
|
||||
"status",
|
||||
@@ -1683,6 +1691,66 @@
|
||||
"fieldname": "automation_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Automation"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "purchase_partner",
|
||||
"fieldname": "commission_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Commission",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_partner",
|
||||
"fieldtype": "Link",
|
||||
"label": "Purchase Partner",
|
||||
"options": "Purchase Partner",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_commission",
|
||||
"fieldtype": "Column Break",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "purchase_partner.commission_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Commission Rate (%)",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Commission",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "purchase_team",
|
||||
"fieldname": "purchase_team_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Purchase Team",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "purchase_team",
|
||||
"fieldtype": "Table",
|
||||
"label": "Purchase Contributions and Incentives",
|
||||
"options": "Purchase Team",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
||||
@@ -121,6 +121,7 @@
|
||||
"dimension_col_break",
|
||||
"cost_center",
|
||||
"section_break_82",
|
||||
"grant_commission",
|
||||
"page_break"
|
||||
],
|
||||
"fields": [
|
||||
@@ -1004,6 +1005,15 @@
|
||||
"label": "Delivered by Supplier",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.grant_commission",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -1021,4 +1031,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2026-06-15 00:00:00.000000",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-06-15 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Partners Commission",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"query": "SELECT\n purchase_partner as \"Purchase Partner:Link/Purchase Partner:220\",\n sum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n sum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n sum(total_commission) as \"Total Commission:Currency:170\",\n sum(total_commission)*100 / NULLIF(sum(amount_eligible_for_commission), 0) as \"Average Commission Rate:Percent:220\"\nFROM\n `tabPurchase Invoice`\nWHERE\n docstatus = 1\n AND IFNULL(base_net_total, 0) > 0\n AND IFNULL(total_commission, 0) > 0\nGROUP BY\n purchase_partner\nORDER BY\n sum(total_commission) DESC",
|
||||
"ref_doctype": "Purchase Invoice",
|
||||
"report_name": "Purchase Partners Commission",
|
||||
"report_type": "Query Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
@@ -133,6 +133,14 @@
|
||||
"terms_section_break",
|
||||
"tc_name",
|
||||
"terms",
|
||||
"commission_section",
|
||||
"purchase_partner",
|
||||
"amount_eligible_for_commission",
|
||||
"column_break_commission",
|
||||
"commission_rate",
|
||||
"total_commission",
|
||||
"purchase_team_section",
|
||||
"purchase_team",
|
||||
"more_info_tab",
|
||||
"tracking_section",
|
||||
"status",
|
||||
@@ -1291,6 +1299,66 @@
|
||||
"fieldname": "auto_repeat_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Auto Repeat"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "purchase_partner",
|
||||
"fieldname": "commission_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Commission",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_partner",
|
||||
"fieldtype": "Link",
|
||||
"label": "Purchase Partner",
|
||||
"options": "Purchase Partner",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_commission",
|
||||
"fieldtype": "Column Break",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "purchase_partner.commission_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Commission Rate (%)",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Commission",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "purchase_team",
|
||||
"fieldname": "purchase_team_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Purchase Team",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "purchase_team",
|
||||
"fieldtype": "Table",
|
||||
"label": "Purchase Contributions and Incentives",
|
||||
"options": "Purchase Team",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
"production_plan",
|
||||
"production_plan_item",
|
||||
"production_plan_sub_assembly_item",
|
||||
"grant_commission",
|
||||
"page_break",
|
||||
"column_break_pjyo",
|
||||
"job_card"
|
||||
@@ -934,6 +935,15 @@
|
||||
"non_negative": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.grant_commission",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -955,4 +965,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "field:purchase_partner_type",
|
||||
"creation": "2026-06-15 00:00:00.000000",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"purchase_partner_type"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "purchase_partner_type",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Purchase Partner Type",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-06-15 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Partner Type",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"translated_doctype": 1
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PurchasePartnerType(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
purchase_partner_type: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestPurchasePartnerType(ERPNextTestSuite):
|
||||
def test_purchase_partner_type_creation(self):
|
||||
if not frappe.db.exists("Purchase Partner Type", "_Test Purchase Partner Type"):
|
||||
ppt = frappe.new_doc("Purchase Partner Type")
|
||||
ppt.purchase_partner_type = "_Test Purchase Partner Type"
|
||||
ppt.insert(ignore_permissions=True)
|
||||
|
||||
self.assertTrue(frappe.db.exists("Purchase Partner Type", "_Test Purchase Partner Type"))
|
||||
frappe.delete_doc("Purchase Partner Type", "_Test Purchase Partner Type", force=True)
|
||||
0
erpnext/buying/doctype/purchase_team/__init__.py
Normal file
0
erpnext/buying/doctype/purchase_team/__init__.py
Normal file
83
erpnext/buying/doctype/purchase_team/purchase_team.json
Normal file
83
erpnext/buying/doctype/purchase_team/purchase_team.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2026-06-17 00:00:00",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"purchase_person",
|
||||
"contact_no",
|
||||
"allocated_percentage",
|
||||
"allocated_amount",
|
||||
"commission_rate",
|
||||
"incentives"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "purchase_person",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Purchase Person",
|
||||
"options": "Purchase Person",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "contact_no",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Contact No."
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "allocated_percentage",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Contribution (%)"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Contribution to Net Total",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "purchase_person.commission_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Commission Rate",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "incentives",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Incentives",
|
||||
"options": "Company:company:default_currency"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-17 00:00:00",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Team",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
27
erpnext/buying/doctype/purchase_team/purchase_team.py
Normal file
27
erpnext/buying/doctype/purchase_team/purchase_team.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PurchaseTeam(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
allocated_amount: DF.Currency
|
||||
allocated_percentage: DF.Float
|
||||
commission_rate: DF.Data | None
|
||||
contact_no: DF.Data | None
|
||||
incentives: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
purchase_person: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Purchase Partner Commission Summary"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "purchase_partner",
|
||||
label: __("Purchase Partner"),
|
||||
fieldtype: "Link",
|
||||
options: "Purchase Partner",
|
||||
},
|
||||
{
|
||||
fieldname: "doctype",
|
||||
label: __("Document Type"),
|
||||
fieldtype: "Select",
|
||||
options: "Purchase Order\nPurchase Receipt\nPurchase Invoice",
|
||||
default: "Purchase Order",
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "supplier",
|
||||
label: __("Supplier"),
|
||||
fieldtype: "Link",
|
||||
options: "Supplier",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"creation": "2026-06-15 00:00:00.000000",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-06-15 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Partner Commission Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Purchase Partner Commission Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
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
|
||||
|
||||
PURCHASE_TRANSACTION_DOCTYPES = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
return PurchasePartnerCommissionSummaryReport(filters).run()
|
||||
|
||||
|
||||
class PurchasePartnerSummaryReport:
|
||||
"""Base class for Purchase Partner Summary related Reports."""
|
||||
|
||||
dt: DocType
|
||||
date_field: str
|
||||
date_label: str
|
||||
columns: list
|
||||
data: list
|
||||
query: QueryBuilder
|
||||
filters: dict
|
||||
|
||||
def __init__(self, filters: dict):
|
||||
self.filters = filters
|
||||
self.columns = []
|
||||
|
||||
def run(self):
|
||||
self.validate_filters()
|
||||
self.prepare_columns()
|
||||
self.get_data()
|
||||
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 PURCHASE_TRANSACTION_DOCTYPES:
|
||||
frappe.throw(_("DocType can be one of them {0}").format(comma_or(PURCHASE_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") == "Purchase Order" else "posting_date"
|
||||
)
|
||||
self.date_label = _("Order Date") if self.date_field == "transaction_date" else _("Posting Date")
|
||||
|
||||
def prepare_columns(self):
|
||||
raise NotImplementedError
|
||||
|
||||
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.supplier,
|
||||
Field(self.date_field, "posting_date", table=self.dt),
|
||||
self.dt.purchase_partner,
|
||||
self.dt.commission_rate,
|
||||
ConstantColumn(company_currency).as_("currency"),
|
||||
)
|
||||
.where(
|
||||
(self.dt.docstatus == 1)
|
||||
& (self.dt.purchase_partner.notnull())
|
||||
& (self.dt.purchase_partner != "")
|
||||
)
|
||||
.orderby(self.dt.name, order=Order.desc)
|
||||
.orderby(self.dt.purchase_partner)
|
||||
)
|
||||
|
||||
def extend_report_query(self):
|
||||
pass
|
||||
|
||||
def _apply_common_filters(self):
|
||||
for field in ["company", "supplier", "purchase_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):
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class PurchasePartnerCommissionSummaryReport(PurchasePartnerSummaryReport):
|
||||
def prepare_columns(self):
|
||||
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
|
||||
self.make_column(_("Supplier"), "supplier", "Link", options="Supplier")
|
||||
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
|
||||
self.make_column(self.date_label, "posting_date", "Date")
|
||||
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
|
||||
self.make_column(_("Purchase Partner"), "purchase_partner", "Link", options="Purchase 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,60 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.query_reports["Purchase Partner 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;
|
||||
},
|
||||
};
|
||||
@@ -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 Partner Target Variance Based On Item Group",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Purchase Partner Target Variance Based On Item Group",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 Partner")
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Purchase Partner Transaction Summary"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "purchase_partner",
|
||||
label: __("Purchase Partner"),
|
||||
fieldtype: "Link",
|
||||
options: "Purchase Partner",
|
||||
},
|
||||
{
|
||||
fieldname: "doctype",
|
||||
label: __("Document Type"),
|
||||
fieldtype: "Select",
|
||||
options: "Purchase Order\nPurchase Receipt\nPurchase Invoice",
|
||||
default: "Purchase Order",
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "supplier",
|
||||
label: __("Supplier"),
|
||||
fieldtype: "Link",
|
||||
options: "Supplier",
|
||||
},
|
||||
{
|
||||
fieldname: "item_group",
|
||||
label: __("Item Group"),
|
||||
fieldtype: "Link",
|
||||
options: "Item Group",
|
||||
},
|
||||
{
|
||||
fieldname: "brand",
|
||||
label: __("Brand"),
|
||||
fieldtype: "Link",
|
||||
options: "Brand",
|
||||
},
|
||||
{
|
||||
fieldname: "show_return_entries",
|
||||
label: __("Show Return Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"creation": "2026-06-15 00:00:00.000000",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-06-15 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Partner Transaction Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Purchase Partner Transaction Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Case
|
||||
|
||||
from erpnext.buying.report.purchase_partner_commission_summary.purchase_partner_commission_summary import (
|
||||
PurchasePartnerSummaryReport,
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
return PurchasePartnerTransactionSummaryReport(filters=filters).run()
|
||||
|
||||
|
||||
class PurchasePartnerTransactionSummaryReport(PurchasePartnerSummaryReport):
|
||||
def prepare_columns(self):
|
||||
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
|
||||
self.make_column(_("Supplier"), "supplier", "Link", options="Supplier")
|
||||
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
|
||||
self.make_column(self.date_label, "posting_date", "Date")
|
||||
self.make_column(_("Item Code"), "item_code", "Link", 100, "Item")
|
||||
self.make_column(_("Item Group"), "item_group", "Link", 100, "Item Group")
|
||||
self.make_column(_("Brand"), "brand", "Link", 100, "Brand")
|
||||
self.make_column(_("Quantity"), "qty", "Float", 120)
|
||||
self.make_column(_("Rate"), "rate", "Currency", 120, "currency")
|
||||
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
|
||||
self.make_column(_("Purchase Partner"), "purchase_partner", "Link", options="Purchase Partner")
|
||||
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
|
||||
self.make_column(_("Commission"), "commission", "Currency", 120, "currency")
|
||||
|
||||
def extend_report_query(self):
|
||||
self.dt_item = frappe.qb.DocType(f"{self.filters['doctype']} Item")
|
||||
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
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,45 @@
|
||||
// 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",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
# 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.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": _("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,
|
||||
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"]:
|
||||
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
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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")
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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: "show_return_entries",
|
||||
label: __("Show Return Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
# 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.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": _("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[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"]:
|
||||
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}
|
||||
@@ -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"
|
||||
},
|
||||
@@ -385,11 +391,140 @@
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Purchase Partner",
|
||||
"link_count": 0,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Purchase Partner",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Partner",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Purchase Partner Type",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Partner Type",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Purchase Partner",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Partners Commission",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Partners Commission",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Purchase Partner",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Partner Commission Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Partner Commission Summary",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Purchase Partner",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Partner Transaction Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Partner Transaction Summary",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Purchase Partner",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Partner Target Variance Based On Item Group",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Partner Target Variance Based On Item Group",
|
||||
"link_type": "Report",
|
||||
"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"
|
||||
},
|
||||
@@ -497,6 +632,7 @@
|
||||
"is_query_report": 0,
|
||||
"label": "Regional",
|
||||
"link_count": 0,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
@@ -512,7 +648,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-01-02 14:55:59.078773",
|
||||
"modified": "2026-06-17 13:13:38.489837",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying",
|
||||
|
||||
@@ -639,6 +639,14 @@ class AccountsController(TransactionBase):
|
||||
self.calculate_commission()
|
||||
self.calculate_contribution()
|
||||
|
||||
if self.doctype in (
|
||||
"Purchase Order",
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
):
|
||||
self.calculate_commission()
|
||||
self.calculate_contribution()
|
||||
|
||||
def validate_date_with_fiscal_year(self):
|
||||
if self.meta.get_field("fiscal_year"):
|
||||
date_field = None
|
||||
|
||||
@@ -384,6 +384,71 @@ class BuyingController(SubcontractingController):
|
||||
item=row,
|
||||
)
|
||||
|
||||
def calculate_commission(self):
|
||||
if not self.meta.get_field("commission_rate"):
|
||||
return
|
||||
|
||||
self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate"))
|
||||
|
||||
if not (0 <= self.commission_rate <= 100.0):
|
||||
frappe.throw(
|
||||
"{} {}".format(
|
||||
_(self.meta.get_label("commission_rate")),
|
||||
_("must be between 0 and 100"),
|
||||
)
|
||||
)
|
||||
|
||||
self.amount_eligible_for_commission = sum(
|
||||
item.base_net_amount for item in self.items if item.grant_commission
|
||||
)
|
||||
|
||||
self.total_commission = flt(
|
||||
self.amount_eligible_for_commission * self.commission_rate / 100.0,
|
||||
self.precision("total_commission"),
|
||||
)
|
||||
|
||||
def calculate_contribution(self):
|
||||
if not self.meta.get_field("purchase_team"):
|
||||
return
|
||||
|
||||
total = 0.0
|
||||
purchase_team = self.get("purchase_team")
|
||||
|
||||
self.validate_purchase_team(purchase_team)
|
||||
|
||||
for purchase_person in purchase_team:
|
||||
self.round_floats_in(purchase_person)
|
||||
|
||||
purchase_person.allocated_amount = flt(
|
||||
flt(self.amount_eligible_for_commission) * purchase_person.allocated_percentage / 100.0,
|
||||
self.precision("allocated_amount", purchase_person),
|
||||
)
|
||||
|
||||
if purchase_person.commission_rate:
|
||||
purchase_person.incentives = flt(
|
||||
purchase_person.allocated_amount * flt(purchase_person.commission_rate) / 100.0,
|
||||
self.precision("incentives", purchase_person),
|
||||
)
|
||||
|
||||
total += purchase_person.allocated_percentage
|
||||
|
||||
if purchase_team and total != 100.0:
|
||||
frappe.throw(_("Total allocated percentage for purchase team should be 100"))
|
||||
|
||||
def validate_purchase_team(self, purchase_team):
|
||||
purchase_persons = [d.purchase_person for d in purchase_team]
|
||||
|
||||
if not purchase_persons:
|
||||
return
|
||||
|
||||
purchase_person_status = frappe.db.get_all(
|
||||
"Purchase Person", filters={"name": ["in", purchase_persons]}, fields=["name", "enabled"]
|
||||
)
|
||||
|
||||
for row in purchase_person_status:
|
||||
if not row.enabled:
|
||||
frappe.throw(_("Purchase Person <b>{0}</b> is disabled.").format(row.name))
|
||||
|
||||
def set_total_in_words(self):
|
||||
from frappe.utils import money_in_words
|
||||
|
||||
|
||||
@@ -87,6 +87,15 @@ erpnext.buying = {
|
||||
me.frm.set_query("supplier_address", erpnext.queries.address_query);
|
||||
|
||||
me.frm.set_query("billing_address", erpnext.queries.company_address_query);
|
||||
|
||||
me.frm.set_query("purchase_person", "purchase_team", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
enabled: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
|
||||
|
||||
this.frm.set_query("item_code", "items", function () {
|
||||
@@ -473,6 +482,92 @@ erpnext.buying = {
|
||||
});
|
||||
}
|
||||
|
||||
purchase_partner() {
|
||||
this.calculate_purchase_commission();
|
||||
}
|
||||
|
||||
commission_rate() {
|
||||
if (
|
||||
["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype)
|
||||
) {
|
||||
this.calculate_purchase_commission();
|
||||
}
|
||||
}
|
||||
|
||||
total_commission() {
|
||||
if (
|
||||
!["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype)
|
||||
)
|
||||
return;
|
||||
frappe.model.round_floats_in(this.frm.doc, [
|
||||
"amount_eligible_for_commission",
|
||||
"total_commission",
|
||||
]);
|
||||
const { amount_eligible_for_commission } = this.frm.doc;
|
||||
if (!amount_eligible_for_commission) return;
|
||||
this.frm.set_value(
|
||||
"commission_rate",
|
||||
flt((this.frm.doc.total_commission * 100.0) / amount_eligible_for_commission)
|
||||
);
|
||||
}
|
||||
|
||||
purchase_team_add(doc, cdt, cdn) {
|
||||
this.calculate_purchase_contribution();
|
||||
}
|
||||
|
||||
purchase_team_remove() {
|
||||
this.calculate_purchase_contribution();
|
||||
}
|
||||
|
||||
calculate_purchase_contribution() {
|
||||
if (!this.frm.fields_dict.purchase_team || this.frm.doc.docstatus === 1) return;
|
||||
|
||||
const purchaseTeam = this.frm.doc.purchase_team || [];
|
||||
let total = 0.0;
|
||||
|
||||
purchaseTeam.forEach((row) => {
|
||||
row.allocated_amount = flt(
|
||||
(flt(this.frm.doc.amount_eligible_for_commission) * row.allocated_percentage) / 100.0
|
||||
);
|
||||
|
||||
if (row.commission_rate) {
|
||||
row.incentives = flt((row.allocated_amount * flt(row.commission_rate)) / 100.0);
|
||||
}
|
||||
|
||||
total += flt(row.allocated_percentage);
|
||||
});
|
||||
|
||||
if (purchaseTeam.length && total !== 100.0) {
|
||||
frappe.msgprint(__("Total allocated percentage for purchase team should be 100"));
|
||||
}
|
||||
|
||||
refresh_field("purchase_team");
|
||||
}
|
||||
|
||||
calculate_purchase_commission() {
|
||||
if (!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return;
|
||||
|
||||
if (this.frm.doc.commission_rate < 0 || this.frm.doc.commission_rate > 100) {
|
||||
frappe.throw(
|
||||
`${__(
|
||||
frappe.meta.get_label(this.frm.doc.doctype, "commission_rate", this.frm.doc.name)
|
||||
)} ${__("must be between 0 and 100")}`
|
||||
);
|
||||
}
|
||||
|
||||
this.frm.doc.amount_eligible_for_commission = (this.frm.doc.items || []).reduce(
|
||||
(sum, item) => (item.grant_commission ? sum + item.base_net_amount : sum),
|
||||
0
|
||||
);
|
||||
|
||||
this.frm.doc.total_commission = flt(
|
||||
(this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate) / 100.0,
|
||||
precision("total_commission")
|
||||
);
|
||||
|
||||
refresh_field(["amount_eligible_for_commission", "total_commission"]);
|
||||
}
|
||||
|
||||
add_serial_batch_for_rejected_qty(doc, cdt, cdn) {
|
||||
let item = locals[cdt][cdn];
|
||||
let me = this;
|
||||
|
||||
@@ -76,6 +76,11 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
this.calculate_contribution();
|
||||
}
|
||||
|
||||
// Purchase partner commission
|
||||
if (["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype)) {
|
||||
this.calculate_purchase_commission();
|
||||
}
|
||||
|
||||
// Update paid amount on return/debit note creation
|
||||
if (
|
||||
this.frm.doc.doctype === "Purchase Invoice" &&
|
||||
|
||||
@@ -55,7 +55,9 @@ def get_data(filters, period_list, partner_doctype):
|
||||
if d.item_group:
|
||||
sales_user_wise_item_groups[d.parent].append(d.item_group)
|
||||
|
||||
date_field = "transaction_date" if filters.get("doctype") == "Sales Order" else "posting_date"
|
||||
date_field = (
|
||||
"transaction_date" if filters.get("doctype") in ("Sales Order", "Purchase Order") else "posting_date"
|
||||
)
|
||||
|
||||
actual_data = get_actual_data(filters, sales_users, date_field, sales_field)
|
||||
|
||||
@@ -243,6 +245,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
|
||||
|
||||
0
erpnext/setup/doctype/purchase_partner/__init__.py
Normal file
0
erpnext/setup/doctype/purchase_partner/__init__.py
Normal file
25
erpnext/setup/doctype/purchase_partner/purchase_partner.js
Normal file
25
erpnext/setup/doctype/purchase_partner/purchase_partner.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.ui.form.on("Purchase Partner", {
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.__islocal) {
|
||||
hide_field(["address_html", "contact_html", "address_contacts"]);
|
||||
frappe.contacts.clear_address_and_contact(frm);
|
||||
} else {
|
||||
unhide_field(["address_html", "contact_html", "address_contacts"]);
|
||||
frappe.contacts.render_address_and_contact(frm);
|
||||
}
|
||||
},
|
||||
|
||||
setup: function (frm) {
|
||||
frm.fields_dict["targets"].grid.get_field("distribution_id").get_query = function (doc, cdt, cdn) {
|
||||
var row = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
fiscal_year: row.fiscal_year,
|
||||
},
|
||||
};
|
||||
};
|
||||
},
|
||||
});
|
||||
145
erpnext/setup/doctype/purchase_partner/purchase_partner.json
Normal file
145
erpnext/setup/doctype/purchase_partner/purchase_partner.json
Normal file
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:partner_name",
|
||||
"creation": "2026-06-15 00:00:00",
|
||||
"description": "A third party agent / broker / commission agent who facilitates purchases for a commission.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"partner_name",
|
||||
"partner_type",
|
||||
"territory",
|
||||
"column_break0",
|
||||
"commission_rate",
|
||||
"address_contacts",
|
||||
"address_desc",
|
||||
"address_html",
|
||||
"column_break1",
|
||||
"contact_desc",
|
||||
"contact_html",
|
||||
"partner_target_details_section_break",
|
||||
"targets"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "partner_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Purchase Partner Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "partner_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Partner Type",
|
||||
"options": "Purchase Partner Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Territory",
|
||||
"options": "Territory",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break0",
|
||||
"fieldtype": "Column Break",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Commission Rate",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "address_contacts",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address & Contacts"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.__islocal",
|
||||
"fieldname": "address_desc",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Address Desc"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Address HTML",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break1",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.__islocal",
|
||||
"fieldname": "contact_desc",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Contact Desc"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Contact HTML",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "partner_target_details_section_break",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Purchase Partner Target"
|
||||
},
|
||||
{
|
||||
"fieldname": "targets",
|
||||
"fieldtype": "Table",
|
||||
"label": "Targets",
|
||||
"options": "Target Detail"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-15 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Purchase Partner",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase Master Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
28
erpnext/setup/doctype/purchase_partner/purchase_partner.py
Normal file
28
erpnext/setup/doctype/purchase_partner/purchase_partner.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PurchasePartner(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.setup.doctype.target_detail.target_detail import TargetDetail
|
||||
|
||||
commission_rate: DF.Float
|
||||
partner_name: DF.Data
|
||||
partner_type: DF.Link | None
|
||||
targets: DF.Table[TargetDetail]
|
||||
territory: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
def onload(self):
|
||||
load_address_and_contact(self)
|
||||
165
erpnext/setup/doctype/purchase_partner/test_purchase_partner.py
Normal file
165
erpnext/setup/doctype/purchase_partner/test_purchase_partner.py
Normal file
@@ -0,0 +1,165 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestPurchasePartner(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.partner = make_purchase_partner()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.delete_doc("Purchase Partner", self.partner.name, force=True)
|
||||
|
||||
def test_purchase_partner_creation(self):
|
||||
self.assertEqual(self.partner.commission_rate, 10.0)
|
||||
self.assertEqual(self.partner.territory, "_Test Territory")
|
||||
|
||||
def test_commission_calculated_on_purchase_order(self):
|
||||
po = create_purchase_order(do_not_submit=True)
|
||||
po.purchase_partner = self.partner.name
|
||||
po.commission_rate = self.partner.commission_rate
|
||||
# grant_commission defaults to 1 fetched from item, ensure it's set
|
||||
for item in po.items:
|
||||
item.grant_commission = 1
|
||||
po.save()
|
||||
|
||||
self.assertEqual(po.commission_rate, 10.0)
|
||||
expected_commission = po.base_net_total * 10.0 / 100.0
|
||||
self.assertAlmostEqual(po.total_commission, expected_commission, places=2)
|
||||
self.assertAlmostEqual(po.amount_eligible_for_commission, po.base_net_total, places=2)
|
||||
|
||||
def test_commission_zero_when_grant_commission_false(self):
|
||||
po = create_purchase_order(do_not_submit=True)
|
||||
po.purchase_partner = self.partner.name
|
||||
po.commission_rate = 10.0
|
||||
for item in po.items:
|
||||
item.grant_commission = 0
|
||||
po.save()
|
||||
|
||||
self.assertEqual(po.total_commission, 0)
|
||||
self.assertEqual(po.amount_eligible_for_commission, 0)
|
||||
|
||||
def test_commission_rate_validation(self):
|
||||
po = create_purchase_order(do_not_submit=True)
|
||||
po.purchase_partner = self.partner.name
|
||||
po.commission_rate = 110.0
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
po.save()
|
||||
|
||||
def test_commission_on_purchase_invoice(self):
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
|
||||
pi = make_purchase_invoice(do_not_save=True)
|
||||
pi.purchase_partner = self.partner.name
|
||||
pi.commission_rate = self.partner.commission_rate
|
||||
for item in pi.items:
|
||||
item.grant_commission = 1
|
||||
pi.save()
|
||||
|
||||
self.assertEqual(pi.commission_rate, 10.0)
|
||||
expected_commission = pi.base_net_total * 10.0 / 100.0
|
||||
self.assertAlmostEqual(pi.total_commission, expected_commission, places=2)
|
||||
|
||||
def test_purchase_partner_commission_summary_report(self):
|
||||
from erpnext.buying.report.purchase_partner_commission_summary.purchase_partner_commission_summary import (
|
||||
execute,
|
||||
)
|
||||
|
||||
po = create_purchase_order(do_not_submit=True)
|
||||
po.purchase_partner = self.partner.name
|
||||
po.commission_rate = 10.0
|
||||
for item in po.items:
|
||||
item.grant_commission = 1
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
columns, data = execute(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"doctype": "Purchase Order",
|
||||
"purchase_partner": self.partner.name,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertTrue(len(columns) > 0)
|
||||
self.assertTrue(any(row.get("purchase_partner") == self.partner.name for row in data))
|
||||
|
||||
po.cancel()
|
||||
|
||||
def test_purchase_team_contribution_on_purchase_order(self):
|
||||
purchase_person = make_purchase_person()
|
||||
|
||||
po = create_purchase_order(do_not_submit=True)
|
||||
for item in po.items:
|
||||
item.grant_commission = 1
|
||||
po.save()
|
||||
|
||||
po.append(
|
||||
"purchase_team",
|
||||
{
|
||||
"purchase_person": purchase_person.purchase_person_name,
|
||||
"allocated_percentage": 100.0,
|
||||
},
|
||||
)
|
||||
po.save()
|
||||
|
||||
self.assertEqual(len(po.purchase_team), 1)
|
||||
self.assertAlmostEqual(
|
||||
po.purchase_team[0].allocated_amount, po.amount_eligible_for_commission, places=2
|
||||
)
|
||||
|
||||
frappe.delete_doc("Purchase Person", purchase_person.name, force=True)
|
||||
|
||||
def test_purchase_team_total_percentage_validation(self):
|
||||
purchase_person = make_purchase_person()
|
||||
|
||||
po = create_purchase_order(do_not_submit=True)
|
||||
for item in po.items:
|
||||
item.grant_commission = 1
|
||||
po.save()
|
||||
|
||||
po.append(
|
||||
"purchase_team",
|
||||
{
|
||||
"purchase_person": purchase_person.purchase_person_name,
|
||||
"allocated_percentage": 60.0,
|
||||
},
|
||||
)
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
po.save()
|
||||
|
||||
frappe.delete_doc("Purchase Person", purchase_person.name, force=True)
|
||||
|
||||
|
||||
def make_purchase_partner(**kwargs):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
partner = frappe.new_doc("Purchase Partner")
|
||||
partner.partner_name = kwargs.partner_name or "_Test Purchase Partner"
|
||||
partner.territory = kwargs.territory or "_Test Territory"
|
||||
partner.commission_rate = kwargs.commission_rate or 10.0
|
||||
if not frappe.db.exists("Purchase Partner", partner.partner_name):
|
||||
partner.insert(ignore_permissions=True)
|
||||
else:
|
||||
partner = frappe.get_doc("Purchase Partner", partner.partner_name)
|
||||
return partner
|
||||
|
||||
|
||||
def make_purchase_person(**kwargs):
|
||||
kwargs = frappe._dict(kwargs)
|
||||
name = kwargs.purchase_person_name or "_Test Purchase Person"
|
||||
if frappe.db.exists("Purchase Person", name):
|
||||
frappe.delete_doc("Purchase Person", name, force=True)
|
||||
person = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Purchase Person",
|
||||
"purchase_person_name": name,
|
||||
"commission_rate": kwargs.commission_rate or "10",
|
||||
"enabled": 1,
|
||||
}
|
||||
)
|
||||
person.insert(ignore_permissions=True)
|
||||
return person
|
||||
0
erpnext/setup/doctype/purchase_person/__init__.py
Normal file
0
erpnext/setup/doctype/purchase_person/__init__.py
Normal file
64
erpnext/setup/doctype/purchase_person/purchase_person.js
Normal file
64
erpnext/setup/doctype/purchase_person/purchase_person.js
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.ui.form.on("Purchase Person", {
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.__onload && frm.doc.__onload.dashboard_info) {
|
||||
let info = frm.doc.__onload.dashboard_info;
|
||||
frm.dashboard.add_indicator(
|
||||
__("Total Contribution Amount Against Orders: {0}", [
|
||||
format_currency(info.allocated_amount_against_order, info.currency),
|
||||
]),
|
||||
"blue"
|
||||
);
|
||||
|
||||
frm.dashboard.add_indicator(
|
||||
__("Total Contribution Amount Against Invoices: {0}", [
|
||||
format_currency(info.allocated_amount_against_invoice, info.currency),
|
||||
]),
|
||||
"blue"
|
||||
);
|
||||
}
|
||||
frm.trigger("set_root_readonly");
|
||||
},
|
||||
|
||||
setup: function (frm) {
|
||||
frm.fields_dict["targets"].grid.get_field("distribution_id").get_query = function (doc, cdt, cdn) {
|
||||
var row = locals[cdt][cdn];
|
||||
return {
|
||||
filters: {
|
||||
fiscal_year: row.fiscal_year,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
frm.make_methods = {
|
||||
"Purchase Order": () =>
|
||||
frappe
|
||||
.new_doc("Purchase Order")
|
||||
.then(() => frm.add_child("purchase_team", { purchase_person: frm.doc.name })),
|
||||
};
|
||||
},
|
||||
|
||||
set_root_readonly: function (frm) {
|
||||
if (!frm.doc.parent_purchase_person && !frm.doc.__islocal) {
|
||||
frm.set_read_only();
|
||||
frm.set_intro(__("This is a root purchase person and cannot be edited."));
|
||||
} else {
|
||||
frm.set_intro(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
cur_frm.fields_dict["parent_purchase_person"].get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: [
|
||||
["Purchase Person", "is_group", "=", 1],
|
||||
["Purchase Person", "name", "!=", doc.purchase_person_name],
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
cur_frm.fields_dict.employee.get_query = function (doc, cdt, cdn) {
|
||||
return { query: "erpnext.controllers.queries.employee_query" };
|
||||
};
|
||||
181
erpnext/setup/doctype/purchase_person/purchase_person.json
Normal file
181
erpnext/setup/doctype/purchase_person/purchase_person.json
Normal file
@@ -0,0 +1,181 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:purchase_person_name",
|
||||
"creation": "2026-06-17 00:00:00",
|
||||
"description": "All Purchase Transactions can be tagged against multiple Purchase Persons so that you can set and monitor targets.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"name_and_employee_id",
|
||||
"purchase_person_name",
|
||||
"parent_purchase_person",
|
||||
"commission_rate",
|
||||
"is_group",
|
||||
"enabled",
|
||||
"cb0",
|
||||
"employee",
|
||||
"department",
|
||||
"lft",
|
||||
"rgt",
|
||||
"old_parent",
|
||||
"target_details_section_break",
|
||||
"targets"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "name_and_employee_id",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Name and Employee ID",
|
||||
"options": "icon-user"
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_person_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Purchase Person Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"description": "Select company name first.",
|
||||
"fieldname": "parent_purchase_person",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Parent Purchase Person",
|
||||
"options": "Purchase Person"
|
||||
},
|
||||
{
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Data",
|
||||
"label": "Commission Rate",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_group",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Group",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "cb0",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "employee",
|
||||
"fieldtype": "Link",
|
||||
"label": "Employee",
|
||||
"options": "Employee"
|
||||
},
|
||||
{
|
||||
"fetch_from": "employee.department",
|
||||
"fieldname": "department",
|
||||
"fieldtype": "Link",
|
||||
"label": "Department",
|
||||
"options": "Department",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "lft",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"label": "lft",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "rgt",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"label": "rgt",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "old_parent",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Old Parent",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "target_details_section_break",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Target Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "targets",
|
||||
"fieldtype": "Table",
|
||||
"label": "Target Details",
|
||||
"options": "Target Detail"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"max_attachments": 0,
|
||||
"modified": "2026-06-17 00:00:00",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Purchase Person",
|
||||
"nsm_parent_field": "parent_purchase_person",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase User",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
148
erpnext/setup/doctype/purchase_person/purchase_person.py
Normal file
148
erpnext/setup/doctype/purchase_person/purchase_person.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from collections import defaultdict
|
||||
from itertools import chain
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Interval
|
||||
from frappe.query_builder.functions import Count, CurDate, UnixTimestamp
|
||||
from frappe.utils import flt
|
||||
from frappe.utils.nestedset import NestedSet, get_root_of
|
||||
|
||||
from erpnext import get_default_currency
|
||||
|
||||
|
||||
class PurchasePerson(NestedSet):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.setup.doctype.target_detail.target_detail import TargetDetail
|
||||
|
||||
commission_rate: DF.Data | None
|
||||
department: DF.Link | None
|
||||
employee: DF.Link | None
|
||||
enabled: DF.Check
|
||||
is_group: DF.Check
|
||||
lft: DF.Int
|
||||
old_parent: DF.Data | None
|
||||
parent_purchase_person: DF.Link | None
|
||||
purchase_person_name: DF.Data
|
||||
rgt: DF.Int
|
||||
targets: DF.Table[TargetDetail]
|
||||
# end: auto-generated types
|
||||
|
||||
nsm_parent_field = "parent_purchase_person"
|
||||
|
||||
def validate(self):
|
||||
if not self.enabled:
|
||||
self.validate_purchase_person()
|
||||
|
||||
if not self.parent_purchase_person:
|
||||
self.parent_purchase_person = get_root_of("Purchase Person")
|
||||
|
||||
for d in self.get("targets") or []:
|
||||
if not flt(d.target_qty) and not flt(d.target_amount):
|
||||
frappe.throw(_("Either target qty or target amount is mandatory."))
|
||||
self.validate_employee_id()
|
||||
|
||||
def onload(self):
|
||||
self.load_dashboard_info()
|
||||
|
||||
def load_dashboard_info(self):
|
||||
company_default_currency = get_default_currency()
|
||||
|
||||
allocated_amount_against_order = flt(
|
||||
frappe.db.get_value(
|
||||
"Purchase Team",
|
||||
{
|
||||
"docstatus": 1,
|
||||
"parenttype": "Purchase Order",
|
||||
"purchase_person": self.purchase_person_name,
|
||||
},
|
||||
[{"SUM": "allocated_amount"}],
|
||||
)
|
||||
)
|
||||
|
||||
allocated_amount_against_invoice = flt(
|
||||
frappe.db.get_value(
|
||||
"Purchase Team",
|
||||
{
|
||||
"docstatus": 1,
|
||||
"parenttype": "Purchase Invoice",
|
||||
"purchase_person": self.purchase_person_name,
|
||||
},
|
||||
[{"SUM": "allocated_amount"}],
|
||||
)
|
||||
)
|
||||
|
||||
info = {}
|
||||
info["allocated_amount_against_order"] = allocated_amount_against_order
|
||||
info["allocated_amount_against_invoice"] = allocated_amount_against_invoice
|
||||
info["currency"] = company_default_currency
|
||||
|
||||
self.set_onload("dashboard_info", info)
|
||||
|
||||
def on_update(self):
|
||||
super().on_update()
|
||||
self.validate_one_root()
|
||||
|
||||
def validate_purchase_person(self):
|
||||
purchase_team = frappe.qb.DocType("Purchase Team")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(purchase_team)
|
||||
.select(purchase_team.purchase_person)
|
||||
.where(purchase_team.purchase_person == self.name)
|
||||
.groupby(purchase_team.purchase_person)
|
||||
).run(as_dict=True)
|
||||
|
||||
if query:
|
||||
frappe.throw(_("The Purchase Person {0} is linked with existing transactions.").format(self.name))
|
||||
|
||||
def validate_employee_id(self):
|
||||
if self.employee:
|
||||
purchase_person = frappe.db.get_value("Purchase Person", {"employee": self.employee})
|
||||
|
||||
if purchase_person and purchase_person != self.name:
|
||||
frappe.throw(
|
||||
_("Another Purchase Person {0} exists with the same Employee id").format(purchase_person)
|
||||
)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Purchase Person", ["lft", "rgt"])
|
||||
|
||||
|
||||
def get_timeline_data(doctype: str, name: str) -> dict[int, int]:
|
||||
def _fetch_activity(doctype: str, date_field: str):
|
||||
purchase_team = frappe.qb.DocType("Purchase Team")
|
||||
transaction = frappe.qb.DocType(doctype)
|
||||
|
||||
return dict(
|
||||
frappe.qb.from_(transaction)
|
||||
.join(purchase_team)
|
||||
.on(transaction.name == purchase_team.parent)
|
||||
.select(UnixTimestamp(transaction[date_field]), Count("*"))
|
||||
.where(purchase_team.purchase_person == name)
|
||||
.where(transaction[date_field] > CurDate() - Interval(years=1))
|
||||
.groupby(transaction[date_field])
|
||||
.run()
|
||||
)
|
||||
|
||||
purchase_order_activity = _fetch_activity("Purchase Order", "transaction_date")
|
||||
purchase_invoice_activity = _fetch_activity("Purchase Invoice", "posting_date")
|
||||
|
||||
merged_activities = defaultdict(int)
|
||||
|
||||
for ts, count in chain(purchase_order_activity.items(), purchase_invoice_activity.items()):
|
||||
merged_activities[ts] += count
|
||||
|
||||
return merged_activities
|
||||
@@ -0,0 +1,23 @@
|
||||
frappe.treeview_settings["Purchase Person"] = {
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "purchase_person_name",
|
||||
label: __("New Purchase Person Name"),
|
||||
reqd: true,
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
fieldname: "employee",
|
||||
label: __("Employee"),
|
||||
options: "Employee",
|
||||
description: __("Please enter Employee Id of this purchase person"),
|
||||
},
|
||||
{
|
||||
fieldtype: "Check",
|
||||
fieldname: "is_group",
|
||||
label: __("Group Node"),
|
||||
description: __("Further nodes can be only created under 'Group' type nodes"),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestPurchasePerson(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
frappe.db.delete("Purchase Person", {"purchase_person_name": "_Test Purchase Person"})
|
||||
frappe.db.delete("Purchase Person", {"purchase_person_name": "_Test Purchase Person Group"})
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.delete("Purchase Person", {"purchase_person_name": "_Test Purchase Person"})
|
||||
frappe.db.delete("Purchase Person", {"purchase_person_name": "_Test Purchase Person Group"})
|
||||
|
||||
def test_create_purchase_person(self):
|
||||
purchase_person = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Purchase Person",
|
||||
"purchase_person_name": "_Test Purchase Person",
|
||||
"commission_rate": "10",
|
||||
"enabled": 1,
|
||||
}
|
||||
)
|
||||
purchase_person.insert(ignore_permissions=True)
|
||||
|
||||
self.assertEqual(purchase_person.purchase_person_name, "_Test Purchase Person")
|
||||
self.assertTrue(purchase_person.enabled)
|
||||
|
||||
def test_create_purchase_person_group(self):
|
||||
group = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Purchase Person",
|
||||
"purchase_person_name": "_Test Purchase Person Group",
|
||||
"is_group": 1,
|
||||
"enabled": 1,
|
||||
}
|
||||
)
|
||||
group.insert(ignore_permissions=True)
|
||||
|
||||
self.assertTrue(group.is_group)
|
||||
self.assertTrue(group.lft)
|
||||
self.assertTrue(group.rgt)
|
||||
|
||||
def test_duplicate_employee_raises_error(self):
|
||||
employee = frappe.db.get_value("Employee", {"status": "Active"}, "name")
|
||||
if not employee:
|
||||
return
|
||||
|
||||
pp1 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Purchase Person",
|
||||
"purchase_person_name": "_Test Purchase Person",
|
||||
"employee": employee,
|
||||
"enabled": 1,
|
||||
}
|
||||
)
|
||||
pp1.insert(ignore_permissions=True)
|
||||
|
||||
pp2 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Purchase Person",
|
||||
"purchase_person_name": "_Test Purchase Person 2",
|
||||
"employee": employee,
|
||||
"enabled": 1,
|
||||
}
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, pp2.insert)
|
||||
|
||||
frappe.db.delete("Purchase Person", {"purchase_person_name": "_Test Purchase Person 2"})
|
||||
@@ -0,0 +1,7 @@
|
||||
Channel Partner
|
||||
Distributor
|
||||
Dealer
|
||||
Agent
|
||||
Retailer
|
||||
Implementation Partner
|
||||
Reseller
|
||||
@@ -248,6 +248,13 @@ def get_preset_records(country=None):
|
||||
"is_group": 1,
|
||||
"parent_sales_person": "",
|
||||
},
|
||||
# Purchase Person
|
||||
{
|
||||
"doctype": "Purchase Person",
|
||||
"purchase_person_name": _("Purchase Team"),
|
||||
"is_group": 1,
|
||||
"parent_purchase_person": "",
|
||||
},
|
||||
# Mode of Payment
|
||||
{
|
||||
"doctype": "Mode of Payment",
|
||||
@@ -328,6 +335,7 @@ def install(country=None):
|
||||
("Industry Type", "industry", "industry_type.txt"),
|
||||
("UTM Source", "name", "marketing_source.txt"),
|
||||
("Sales Partner Type", "sales_partner_type", "sales_partner_type.txt"),
|
||||
("Purchase Partner Type", "purchase_partner_type", "purchase_partner_type.txt"),
|
||||
):
|
||||
records += [{"doctype": doctype, title_field: title} for title in read_lines(filename)]
|
||||
|
||||
|
||||
@@ -126,6 +126,14 @@
|
||||
"terms_tab",
|
||||
"tc_name",
|
||||
"terms",
|
||||
"commission_section",
|
||||
"purchase_partner",
|
||||
"amount_eligible_for_commission",
|
||||
"column_break_commission",
|
||||
"commission_rate",
|
||||
"total_commission",
|
||||
"purchase_team_section",
|
||||
"purchase_team",
|
||||
"more_info_tab",
|
||||
"status_section",
|
||||
"status",
|
||||
@@ -1286,6 +1294,66 @@
|
||||
{
|
||||
"fieldname": "column_break_ugyv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "purchase_partner",
|
||||
"fieldname": "commission_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Commission",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_partner",
|
||||
"fieldtype": "Link",
|
||||
"label": "Purchase Partner",
|
||||
"options": "Purchase Partner",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_commission",
|
||||
"fieldtype": "Column Break",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "purchase_partner.commission_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Commission Rate (%)",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Commission",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "purchase_team",
|
||||
"fieldname": "purchase_team_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Purchase Team",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "purchase_team",
|
||||
"fieldtype": "Table",
|
||||
"label": "Purchase Contributions and Incentives",
|
||||
"options": "Purchase Team",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
"dimension_col_break",
|
||||
"cost_center",
|
||||
"section_break_80",
|
||||
"grant_commission",
|
||||
"page_break",
|
||||
"sales_order",
|
||||
"sales_order_item",
|
||||
@@ -1117,6 +1118,15 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.grant_commission",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
@@ -1134,4 +1144,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user