feat: add Purchase Person tree DocType and Purchase Team child table

Mirrors Sales Person/Sales Team functionality for the purchase flow.
Purchase Person is a tree DocType (Setup module) and Purchase Team is
a child table (Buying module). Both are added to Purchase Order,
Purchase Invoice, and Purchase Receipt. The BuyingController gains
calculate_contribution() and validate_purchase_team() methods, and
accounts_controller wires it into the calculate_totals flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dipen Gala
2026-06-17 12:54:44 +05:30
parent 56ed9e8e43
commit 5249274bcd
17 changed files with 853 additions and 53 deletions

View File

@@ -173,6 +173,8 @@
"column_break_commission",
"commission_rate",
"total_commission",
"purchase_team_section",
"purchase_team",
"more_info_tab",
"status_section",
"status",
@@ -1733,6 +1735,22 @@
"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,
@@ -1805,4 +1823,4 @@
"timeline_field": "supplier",
"title_field": "supplier_name",
"track_changes": 1
}
}

View File

@@ -139,6 +139,8 @@
"column_break_commission",
"commission_rate",
"total_commission",
"purchase_team_section",
"purchase_team",
"more_info_tab",
"tracking_section",
"status",
@@ -1341,6 +1343,22 @@
"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,
@@ -1404,4 +1422,4 @@
"timeline_field": "supplier",
"title_field": "supplier_name",
"track_changes": 1
}
}

View 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
}

View 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

View File

@@ -415,6 +415,25 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Purchase Person",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Purchase Person",
"link_count": 0,
"link_to": "Purchase Person",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Purchase Partner",
"hidden": 0,
@@ -603,4 +622,4 @@
"shortcuts": [],
"title": "Buying",
"type": "Workspace"
}
}

View File

@@ -645,6 +645,7 @@ class AccountsController(TransactionBase):
"Purchase Invoice",
):
self.calculate_commission()
self.calculate_contribution()
def validate_date_with_fiscal_year(self):
if self.meta.get_field("fiscal_year"):

View File

@@ -407,6 +407,48 @@ class BuyingController(SubcontractingController):
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

View File

@@ -477,64 +477,89 @@ erpnext.buying = {
this.calculate_purchase_commission();
}
commission_rate() {
if (
["Purchase Order", "Purchase Receipt", "Purchase Invoice"].includes(
this.frm.doc.doctype
)
) {
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
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)
);
}
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")}`
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)
);
}
this.frm.doc.amount_eligible_for_commission = (this.frm.doc.items || []).reduce(
(sum, item) => (item.grant_commission ? sum + item.base_net_amount : sum),
0
);
purchase_team_add(doc, cdt, cdn) {
this.calculate_purchase_contribution();
}
this.frm.doc.total_commission = flt(
(this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate) / 100.0,
precision("total_commission")
);
purchase_team_remove() {
this.calculate_purchase_contribution();
}
refresh_field(["amount_eligible_for_commission", "total_commission"]);
}
calculate_purchase_contribution() {
if (!this.frm.fields_dict.purchase_team || this.frm.doc.docstatus === 1) return;
add_serial_batch_for_rejected_qty(doc, cdt, cdn) {
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;
let fields = ["has_batch_no", "has_serial_no"];

View File

@@ -90,6 +90,50 @@ class TestPurchasePartner(ERPNextTestSuite):
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)
@@ -102,3 +146,20 @@ def make_purchase_partner(**kwargs):
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

View 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" };
};

View 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
}

View 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

View File

@@ -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"),
},
],
};

View File

@@ -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"})

View File

@@ -132,6 +132,8 @@
"column_break_commission",
"commission_rate",
"total_commission",
"purchase_team_section",
"purchase_team",
"more_info_tab",
"status_section",
"status",
@@ -1336,6 +1338,22 @@
"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,
@@ -1413,4 +1431,4 @@
"timeline_field": "supplier",
"title_field": "supplier_name",
"track_changes": 1
}
}