Compare commits

..

10 Commits

Author SHA1 Message Date
Dipen Gala
87b65d09df fix: use transaction_date for Purchase Order in target variance helper
The shared get_data helper defaulted to posting_date for any doctype
other than Sales Order. Purchase Order also uses transaction_date, so
add it to the in-check to prevent the Unknown column SQL error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:40:38 +05:30
Dipen Gala
1631985726 fix: remove territory from all Purchase Partner reports
Purchase Order, Purchase Invoice, and Purchase Receipt have no
territory field. Removed it from the base query SELECT, the common
filters loop, the column definitions, and the JS filter inputs in
Purchase Partner Commission Summary and Transaction Summary reports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 14:38:33 +05:30
Dipen Gala
5284b8c7a8 feat: add Purchase Partner Target Variance Based On Item Group report
Mirrors the Sales Partner Target Variance Based On Item Group report
for the purchase flow. Calls the shared get_data_column helper with
"Purchase Partner" so it reads Target Details with parenttype=Purchase
Partner and matches against the purchase_partner field on PO/PI/PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 13:39:05 +05:30
Dipen Gala
a674d9216a fix: filter purchase_person dropdown to non-group enabled records only
Adds a set_query on purchase_person in the purchase_team child table
so the dropdown excludes group nodes (like "Purchase Team") and
disabled records, matching the same filter used for sales_person in
Sales Team.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 13:22:34 +05:30
Dipen Gala
9dfb60f826 fix: remove territory from Purchase Person reports
Purchase Order, Purchase Invoice, and Purchase Receipt do not have a
territory field (unlike their selling counterparts), causing an
Unknown column SQL error. Removed territory from columns, SELECT, and
filter conditions in both Commission Summary and Transaction Summary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 13:17:33 +05:30
Dipen Gala
1138effd7c feat: add Purchase Person reports mirroring Sales Person reports
Three new Script Reports in the Buying module:
- Purchase Person-wise Transaction Summary (mirrors Sales Person-wise)
- Purchase Person Commission Summary (mirrors Sales Person Commission)
- Purchase Person Target Variance Based On Item Group (mirrors Sales Person Target)

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 13:12:54 +05:30
Dipen Gala
c8cb70dbd2 feat: add Purchase Team root node as default fixture on install
Mirrors the Sales Person/Sales Team fixture so fresh installs get a
root "Purchase Team" group node for the Purchase Person tree.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 13:07:04 +05:30
Dipen Gala
5249274bcd 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>
2026-06-17 12:54:44 +05:30
Dipen Gala
56ed9e8e43 fix: address review comments on purchase partner commission PR
- Fix division-by-zero on PostgreSQL in purchase_partners_commission report
  by wrapping sum(amount_eligible_for_commission) with NULLIF(..., 0)
- Replace lazy `from frappe import throw` with `frappe.throw()` in
  buying_controller.calculate_commission to match selling controller pattern
- Fix indentation of purchase_partner() event handler in buying.js
- Mirror server-side validation in JS: block commission_rate < 0 as well
  as > 100, with consistent error message "must be between 0 and 100"
- Remove unused IntegrationTestCase import from test_purchase_partner.py
- Add Purchase Partner Type fixtures (same types as Sales Partner Type)
  installed via setup wizard so generic records exist out of the box

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 15:19:53 +05:30
Dipen Gala
0147312951 feat: add Purchase Partner and commission functionality
Mirrors the existing Sales Partner / Sales Commission feature for the
purchase side, as requested in issue #52298.

**New DocTypes:**
- Purchase Partner (Setup module) — master for purchase agents/brokers
  with commission_rate, territory, address & contacts, and targets
- Purchase Partner Type (Buying module) — classification for partners

**Commission fields added to:**
- Purchase Order, Purchase Invoice, Purchase Receipt — commission_section,
  purchase_partner (Link), commission_rate (fetch_from partner),
  amount_eligible_for_commission, total_commission
- Purchase Order Item, Purchase Invoice Item, Purchase Receipt Item —
  grant_commission (fetched from Item master, default 0)

**Commission calculation:**
- Python: BuyingController.calculate_commission() mirrors
  SellingController logic; triggered via accounts_controller on validate
- JS: BuyingController.calculate_purchase_commission() in buying.js;
  triggered from taxes_and_totals.js after totals recalculate
- Event handlers: purchase_partner / commission_rate / total_commission

**New Reports:**
- Purchase Partner Commission Summary (Buying) — per-document summary
- Purchase Partner Transaction Summary (Buying) — item-level breakdown
- Purchase Partners Commission (Accounts) — aggregated query report

**Workspace:** Purchase Partner card added to Buying workspace

**Tests:** test_purchase_partner.py covers commission calculation,
grant_commission exclusion, rate validation, and report execution

Fixes #52298

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 17:39:59 +05:30
160 changed files with 33057 additions and 100774 deletions

View File

@@ -4,46 +4,24 @@ set -e
cd ~ || exit
sudo apt update
sudo apt remove mysql-server mysql-client
sudo apt install libcups2-dev redis-server mariadb-client libmariadb-dev
pip install frappe-bench
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"}
frappecommitish=${FRAPPE_BRANCH:-$githubbranch}
# ---------------------------------------------------------------------------
# Phase 1 — parallelise the three slow, independent setup steps:
# a) system packages b) frappe-bench pip install c) frappe git fetch
# ---------------------------------------------------------------------------
sudo apt update
# apt remove/install must run sequentially but can overlap with pip and git.
sudo apt remove mysql-server mysql-client
sudo apt install libcups2-dev redis-server mariadb-client libmariadb-dev &
apt_pid=$!
pip install frappe-bench &
pip_pid=$!
mkdir frappe
(
cd frappe
git init
git remote add origin "https://github.com/${frappeuser}/frappe"
git fetch origin "${frappecommitish}" --depth 1
) &
clone_pid=$!
wait $apt_pid
wait $pip_pid
wait $clone_pid
pushd frappe
git init
git remote add origin "https://github.com/${frappeuser}/frappe"
git fetch origin "${frappecommitish}" --depth 1
git checkout FETCH_HEAD
popd
# ---------------------------------------------------------------------------
# Phase 2 — bench init and site setup
# ---------------------------------------------------------------------------
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
mkdir ~/frappe-bench/sites/test_site
@@ -59,11 +37,6 @@ if [ "$DB" == "mariadb" ];then
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
# Belt-and-suspenders: also set performance variables at runtime in case
# MARIADB_EXTRA_FLAGS was not honoured by the container image.
mariadb --host 127.0.0.1 --port 3306 -u root -proot \
-e "SET GLOBAL innodb_flush_log_at_trx_commit=0; SET GLOBAL sync_binlog=0;"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
@@ -78,11 +51,9 @@ fi
install_whktml() {
# Re-use the .deb if the wkhtmltopdf cache step already restored it.
if [ ! -f /tmp/wkhtmltox.deb ]; then
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
fi
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
sudo apt install /tmp/wkhtmltox.deb
}
install_whktml &
wkpid=$!

View File

@@ -59,10 +59,6 @@ jobs:
env:
TZ: 'Asia/Kolkata'
MARIADB_ROOT_PASSWORD: 'root'
# Disable durability guarantees that are unnecessary in a throwaway CI container.
# innodb_flush_log_at_trx_commit=0 avoids an fsync on every commit (biggest win).
# sync_binlog=0 skips binary-log syncs; innodb_doublewrite=0 skips the doublewrite buffer.
MARIADB_EXTRA_FLAGS: --innodb-flush-log-at-trx-commit=0 --sync-binlog=0 --innodb-doublewrite=0
ports:
- 3306:3306
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
@@ -126,12 +122,6 @@ jobs:
restore-keys: |
${{ runner.os }}-yarn-
- name: Cache wkhtmltopdf
uses: actions/cache@v4
with:
path: /tmp/wkhtmltox.deb
key: wkhtmltox-0.12.6.1-2-jammy-amd64
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
@@ -141,14 +131,7 @@ jobs:
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
- name: Run Tests
run: |
cd ~/frappe-bench/
coverage_flag=""
if [ "$WITH_COVERAGE" = "true" ]; then coverage_flag="--with-coverage"; fi
bench --site test_site run-parallel-tests --lightmode --app erpnext \
--total-builds ${{ strategy.job-total }} \
--build-number ${{ matrix.container }} \
$coverage_flag
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --lightmode --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }} --with-coverage'
env:
TYPE: server
@@ -158,7 +141,6 @@ jobs:
run: cat ~/frappe-bench/bench_start.log || true
- name: Upload coverage data
if: ${{ env.WITH_COVERAGE == 'true' }}
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.container }}
@@ -167,7 +149,6 @@ jobs:
coverage:
name: Coverage Wrap Up
needs: test
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
steps:
- name: Clone

View File

@@ -1,10 +0,0 @@
{
"disabledLabels": [
"conflicts"
],
"context": {
"repos": [
"frappe/frappe"
]
}
}

View File

@@ -94,7 +94,6 @@ class BankClearance(Document):
invalid_document = []
invalid_cheque_date = []
entries_to_update = []
self.check_permission("write")
def validate_entry(d):
is_valid = True

View File

@@ -518,7 +518,6 @@ def create_internal_transfer(
"""
bank_transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
bank_transaction.check_permission("write")
bank_account = frappe.get_cached_value("Bank Account", bank_transaction.bank_account, "account")
company = frappe.get_cached_value("Account", bank_account, "company")
@@ -779,6 +778,7 @@ def create_bulk_payment_entry_and_reconcile(
"""
Create a payment entry and reconcile it with the bank transaction
"""
output = []
for bank_transaction_name in bank_transaction_names:

View File

@@ -374,7 +374,6 @@ def unreconcile_transaction(transaction_name: str | int):
Else, cancel the individual entries
"""
transaction = frappe.get_doc("Bank Transaction", transaction_name)
transaction.check_permission("write")
vouchers_to_cancel = []
@@ -402,7 +401,6 @@ def unreconcile_transaction_entry(bank_transaction_id: str | int, voucher_type:
"""
bank_transaction = frappe.get_doc("Bank Transaction", bank_transaction_id)
bank_transaction.check_permission("write")
# Find the voucher in the bank transaction and depending on the action, either remove it or cancel the voucher
for entry in bank_transaction.payment_entries:

View File

@@ -17,7 +17,6 @@ frappe.ui.form.on("Budget", {
filters: {
is_group: 0,
company: frm.doc.company,
root_type: ["in", ["Income", "Expense"]],
},
};
});

View File

@@ -11,28 +11,22 @@ frappe.ui.form.on("Currency Exchange Settings", {
},
callback: function (r) {
if (r && r.message) {
let result = [],
params = {};
if (frm.doc.service_provider == "exchangerate.host") {
result = ["result"];
params = {
let result = ["result"];
let params = {
date: "{transaction_date}",
from: "{from_currency}",
to: "{to_currency}",
};
add_param(frm, r.message, params, result);
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
result = ["rates", "{to_currency}"];
params = {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",
symbols: "{to_currency}",
};
} else if (frm.doc.service_provider == "frankfurter.dev - v2") {
result = ["rate"];
params = {
date: "{transaction_date}",
};
add_param(frm, r.message, params, result);
}
add_param(frm, r.message, params, result);
}
},
});

View File

@@ -78,7 +78,7 @@
"fieldname": "service_provider",
"fieldtype": "Select",
"label": "Service Provider",
"options": "frankfurter.dev\nexchangerate.host\nfrankfurter.dev - v2\nCustom",
"options": "frankfurter.dev\nexchangerate.host\nCustom",
"reqd": 1
},
{
@@ -101,10 +101,11 @@
"label": "Use HTTP Protocol"
}
],
"hide_toolbar": 0,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-06-15 11:25:55.873110",
"modified": "2026-03-16 13:28:21.075743",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",
@@ -121,11 +122,24 @@
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts User",
"share": 1
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",

View File

@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
disabled: DF.Check
req_params: DF.Table[CurrencyExchangeSettingsDetails]
result_key: DF.Table[CurrencyExchangeSettingsResult]
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "frankfurter.dev - v2", "Custom"]
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
url: DF.Data | None
use_http: DF.Check
# end: auto-generated types
@@ -70,14 +70,6 @@ class CurrencyExchangeSettings(Document):
self.append("req_params", {"key": "base", "value": "{from_currency}"})
self.append("req_params", {"key": "symbols", "value": "{to_currency}"})
elif self.service_provider == "frankfurter.dev - v2":
self.set("result_key", [])
self.set("req_params", [])
self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
self.append("result_key", {"key": "rate"})
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
def validate_parameters(self):
params = {}
for row in self.req_params:
@@ -113,20 +105,13 @@ class CurrencyExchangeSettings(Document):
@frappe.whitelist()
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
if service_provider and service_provider in [
"exchangerate.host",
"frankfurter.dev",
"frankfurter.app",
"frankfurter.dev - v2",
]:
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "api.frankfurter.app/{transaction_date}"
elif service_provider == "frankfurter.dev":
api = "api.frankfurter.dev/v1/{transaction_date}"
elif service_provider == "frankfurter.dev - v2":
api = "api.frankfurter.dev/v2/rate/{from_currency}/{to_currency}"
protocol = "https://"
if use_http:

View File

@@ -484,12 +484,6 @@ class JournalEntry(AccountsController):
d.idx, d.account, d.party_type
)
)
elif d.party_type or d.party:
frappe.throw(
_(
"Row {0}: Party Type or Party can only be set for Receivable / Payable account, but account {1} is of type {2}"
).format(d.idx, d.account, account_type or _("None"))
)
def check_credit_limit(self):
customers = list(

View File

@@ -662,13 +662,6 @@ class TestJournalEntry(ERPNextTestSuite):
jv.save()
self.assertRaises(frappe.ValidationError, jv.submit)
def test_party_not_allowed_for_non_receivable_payable_account(self):
customer = make_customer("_Test New Customer")
jv = make_journal_entry(account1="_Test Cash - _TC", account2="_Test Bank - _TC", amount=100, save=False)
jv.accounts[0].party_type = "Customer"
jv.accounts[0].party = customer
self.assertRaises(frappe.ValidationError, jv.save)
def test_validate_reference_doc_debit_against_sales_order_throws(self):
"""Characterize: a debit entry linked to a Sales Order is rejected."""
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order

View File

@@ -2780,7 +2780,7 @@ def get_payment_entry(
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
)
pe.set_exchange_rate()
pe.set_exchange_rate(ref_doc=doc)
pe.set_amounts()
# If PE is created from PR directly, then no need to find open PRs for the references

View File

@@ -532,8 +532,6 @@ class TestPaymentEntry(ERPNextTestSuite):
si.submit()
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700)
pe.source_exchange_rate = 50
pe.set_amounts()
pe.reference_no = si.name
pe.reference_date = nowdate()
@@ -609,8 +607,6 @@ class TestPaymentEntry(ERPNextTestSuite):
pe = get_payment_entry(
"Sales Invoice", si.name, party_amount=20, bank_account="_Test Bank - _TC", bank_amount=900
)
pe.source_exchange_rate = 50
pe.set_amounts()
pe.reference_no = "1"
pe.reference_date = "2016-01-01"

View File

@@ -11,12 +11,11 @@ from erpnext import get_company_currency
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.bank_account.bank_account import get_party_bank_account
from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_payment_entry,
)
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
from erpnext.accounts.party import get_party_account
from erpnext.accounts.party import get_party_account, get_party_bank_account
from erpnext.accounts.utils import get_account_currency, get_advance_payment_doctypes, get_currency_precision
from erpnext.utilities import payment_app_import_guard

View File

@@ -332,12 +332,7 @@ class TestPaymentRequest(ERPNextTestSuite):
return_doc=1,
)
pe = pr.create_payment_entry(submit=False)
pe.source_exchange_rate = 50
pe.target_exchange_rate = 50
pe.set_amounts()
pe.insert(ignore_permissions=True)
pe.submit()
pe = pr.set_as_paid()
expected_gle = dict(
(d[0], d)
@@ -423,12 +418,7 @@ class TestPaymentRequest(ERPNextTestSuite):
pr = make_payment_request(dt=po_doc.doctype, dn=po_doc.name, recipient_id="nabin@erpnext.com")
pr = frappe.get_doc(pr).save().submit()
pe = pr.create_payment_entry(submit=False)
pe.target_exchange_rate = 80
pe.paid_amount = 800
pe.set_amounts()
pe.insert(ignore_permissions=True)
pe.submit()
pe = pr.create_payment_entry()
self.assertEqual(pe.base_paid_amount, 800)
self.assertEqual(pe.paid_amount, 800)
self.assertEqual(pe.base_received_amount, 800)

View File

@@ -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",
@@ -614,12 +622,10 @@
{
"default": "0",
"depends_on": "eval:doc.items.every((item) => !item.pr_detail)",
"description": "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Purchase Receipt is created separately.",
"fieldname": "update_stock",
"fieldtype": "Check",
"label": "Update Stock",
"print_hide": 1,
"show_description_on_click": 1
"print_hide": 1
},
{
"fieldname": "scan_barcode",
@@ -1685,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,
@@ -1692,7 +1758,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-06-13 18:36:46.704623",
"modified": "2026-05-28 12:36:55.215363",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -51,6 +51,16 @@ class ExpenseAccountService:
if doc.update_stock and item.warehouse and (not item.from_warehouse):
_inv_dict = doc.get_inventory_account_dict(item, inventory_account_map)
if for_validate and item.expense_account and item.expense_account != _inv_dict["account"]:
msg = _(
"Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account"
).format(
item.idx,
frappe.bold(_inv_dict["account"]),
frappe.bold(item.expense_account),
frappe.bold(item.warehouse),
)
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = _inv_dict["account"]
else:
# check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not

View File

@@ -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": []
}
}

View File

@@ -158,7 +158,6 @@ def start_repost(account_repost_doc: str | None = None) -> None:
frappe.flags.through_repost_accounting_ledger = True
if account_repost_doc:
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
repost_doc.check_permission("write")
if repost_doc.docstatus == 1:
# Prevent repost on invoices with deferred accounting

View File

@@ -715,7 +715,6 @@
{
"default": "0",
"depends_on": "eval:doc.items.every((item) => !item.dn_detail)",
"description": "If checked, updates inventory; stock and accounting entries are created together. Leave unchecked if a Delivery Note is created separately.",
"fieldname": "update_stock",
"fieldtype": "Check",
"hide_days": 1,
@@ -723,8 +722,7 @@
"label": "Update Stock",
"oldfieldname": "update_stock",
"oldfieldtype": "Check",
"print_hide": 1,
"show_description_on_click": 1
"print_hide": 1
},
{
"fieldname": "scan_barcode",

View File

@@ -412,8 +412,8 @@ class SalesInvoice(SellingController):
validate_account_head(item.idx, item.income_account, self.company, _("Income"))
def before_save(self):
POSService(self).update_paid_amount()
POSService(self).set_account_for_mode_of_payment()
POSService(self).set_paid_amount()
def before_submit(self):
self.add_remarks()

View File

@@ -114,17 +114,10 @@ class POSService:
return pos
def update_paid_amount(self) -> None:
def set_paid_amount(self) -> None:
doc = self.doc
paid_amount = 0.0
base_paid_amount = 0.0
if not cint(doc.is_pos) and doc.is_return:
doc.set("payments", [])
doc.paid_amount = paid_amount
doc.base_paid_amount = base_paid_amount
return
for data in doc.payments:
data.base_amount = flt(data.amount * doc.conversion_rate, doc.precision("base_paid_amount"))
paid_amount += data.amount

View File

@@ -509,6 +509,11 @@ def get_party_advance_account(party_type, party, company):
return account
@frappe.whitelist()
def get_party_bank_account(party_type: str, party: str):
return frappe.db.get_value("Bank Account", {"party_type": party_type, "party": party, "is_default": 1})
def get_party_account_currency(party_type, party, company):
def generator():
party_account = get_party_account(party_type, party, company)

View File

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

View File

@@ -80,8 +80,6 @@ class TestUtils(ERPNextTestSuite):
purchase_invoice.submit()
payment_entry = get_payment_entry(purchase_invoice.doctype, purchase_invoice.name)
payment_entry.target_exchange_rate = 82.32
payment_entry.set_amounts()
payment_entry.paid_amount = 15725
payment_entry.deductions = []
payment_entry.save()

View File

@@ -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,

View File

@@ -86,7 +86,7 @@ class SubcontractingService:
def update_subcontracting_order_status(self) -> None:
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
set_subcontracting_order_status as update_sco_status,
update_subcontracting_order_status as update_sco_status,
)
doc = self.doc

View File

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

View File

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

View File

@@ -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

View File

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

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

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

View File

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

View File

@@ -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,
)

View File

@@ -0,0 +1,60 @@
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.query_reports["Purchase 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;
},
};

View File

@@ -0,0 +1,33 @@
{
"add_total_row": 0,
"creation": "2026-06-17 00:00:00",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"modified": "2026-06-17 00:00:00",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase 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"
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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,
},
],
};

View File

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

View File

@@ -0,0 +1,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}

View File

@@ -23,6 +23,7 @@
"is_query_report": 0,
"label": "Buying",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -86,6 +87,7 @@
"is_query_report": 0,
"label": "Items & Pricing",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -171,6 +173,7 @@
"is_query_report": 0,
"label": "Settings",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -212,6 +215,7 @@
"is_query_report": 0,
"label": "Supplier",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -264,6 +268,7 @@
"is_query_report": 0,
"label": "Supplier Scorecard",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -316,6 +321,7 @@
"is_query_report": 0,
"label": "Key Reports",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -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",

View File

@@ -38,7 +38,7 @@ from erpnext.accounts.party import (
from erpnext.accounts.utils import (
get_advance_payment_doctypes as _get_advance_payment_doctypes,
)
from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year
from erpnext.accounts.utils import validate_fiscal_year
from erpnext.controllers.print_settings import (
set_print_templates_for_item_table,
set_print_templates_for_taxes,
@@ -639,30 +639,30 @@ 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):
date_field = None
if self.meta.get_field("posting_date"):
date_field = "posting_date"
elif self.meta.get_field("transaction_date"):
date_field = "transaction_date"
if not date_field or not self.get(date_field):
return
if self.meta.get_field("fiscal_year"):
validate_fiscal_year(
self.get(date_field),
self.fiscal_year,
self.company,
self.meta.get_label(date_field),
self,
)
else:
get_fiscal_year(
self.get(date_field),
company=self.company,
label=self.meta.get_label(date_field),
)
date_field = None
if self.meta.get_field("posting_date"):
date_field = "posting_date"
elif self.meta.get_field("transaction_date"):
date_field = "transaction_date"
if date_field and self.get(date_field):
validate_fiscal_year(
self.get(date_field),
self.fiscal_year,
self.company,
self.meta.get_label(date_field),
self,
)
def validate_due_date(self):
if self.get("is_pos") or self.doctype not in ["Sales Invoice", "Purchase Invoice"]:

View File

@@ -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

View File

@@ -598,7 +598,6 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
target_doc.so_detail = source_doc.so_detail
target_doc.expense_account = source_doc.expense_account
target_doc.dn_detail = source_doc.name
target_doc.cost_center = source_doc.cost_center
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
elif doctype == "Sales Invoice" or doctype == "POS Invoice":

View File

@@ -1124,10 +1124,10 @@ class SubcontractingInwardController:
def update_inward_order_status(self):
if self.subcontracting_inward_order:
from erpnext.subcontracting.doctype.subcontracting_inward_order.subcontracting_inward_order import (
set_subcontracting_inward_order_status,
update_subcontracting_inward_order_status,
)
set_subcontracting_inward_order_status(self.subcontracting_inward_order)
update_subcontracting_inward_order_status(self.subcontracting_inward_order)
@frappe.whitelist()

View File

@@ -16,13 +16,12 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestItemWiseInventoryAccount(ERPNextTestSuite):
def setUp(self):
self.company = "_Test Company with perpetual inventory"
self.company_abbr = "TCP1"
self.company = make_company()
self.company_abbr = frappe.db.get_value("Company", self.company, "abbr")
self.default_warehouse = frappe.db.get_value(
"Warehouse",
{"company": self.company, "is_group": 0, "warehouse_name": ("like", "%Stores%")},
)
frappe.db.set_value("Company", self.company, "enable_item_wise_inventory_account", 1)
def test_item_account_for_purchase_receipt_entry(self):
items = {
@@ -578,3 +577,23 @@ class TestItemWiseInventoryAccount(ERPNextTestSuite):
gl_value = gl_value * -1
self.assertEqual(sle_value, gl_value, f"GL Entry not created for {item_code} correctly")
def make_company():
company = "_Test Company for Item Wise Inventory Account"
if frappe.db.exists("Company", company):
return company
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": "_Test Company for Item Wise Inventory Account",
"abbr": "_TCIWIA",
"default_currency": "INR",
"country": "India",
"enable_perpetual_inventory": 1,
"enable_item_wise_inventory_account": 1,
}
).insert()
return company.name

View File

@@ -14,7 +14,6 @@
"opportunity_section",
"close_opportunity_after_days",
"column_break_9",
"enable_opportunity_creation_from_contact_us",
"quotation_section",
"default_valid_till",
"section_break_13",
@@ -99,20 +98,15 @@
"fieldname": "update_timestamp_on_new_communication",
"fieldtype": "Check",
"label": "Update timestamp on new communication"
},
{
"default": "0",
"fieldname": "enable_opportunity_creation_from_contact_us",
"fieldtype": "Check",
"label": "Enable Opportunity Creation from Contact Us"
}
],
"grid_page_length": 50,
"hide_toolbar": 0,
"icon": "fa fa-cog",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-06-11 23:09:49.750381",
"modified": "2026-03-16 13:28:19.573964",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM Settings",

View File

@@ -2,7 +2,6 @@
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
@@ -21,20 +20,8 @@ class CRMSettings(Document):
carry_forward_communication_and_comments: DF.Check
close_opportunity_after_days: DF.Int
default_valid_till: DF.Data | None
enable_opportunity_creation_from_contact_us: DF.Check
update_timestamp_on_new_communication: DF.Check
# end: auto-generated types
def validate(self):
frappe.db.set_default("campaign_naming_by", self.get("campaign_naming_by", ""))
self.validate_enable_opportunity_creation_from_contact_us()
def validate_enable_opportunity_creation_from_contact_us(self):
contact_disabled = frappe.get_single_value("Contact Us Settings", "is_disabled")
if self.enable_opportunity_creation_from_contact_us and contact_disabled:
frappe.throw(
_(
"Cannot enable Opportunity creation from Contact Us because the Contact Us form is disabled."
)
)

View File

@@ -8,7 +8,7 @@ from frappe.contacts.address_and_contact import (
load_address_and_contact,
)
from frappe.model.document import Document
from frappe.utils import comma_and, get_link_to_form, validate_email_address
from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address
from frappe.utils.data import DateTimeLikeObject
from erpnext.accounts.party import set_taxes
@@ -173,6 +173,9 @@ class Lead(SellingController, CRMNote):
if self.email_id == self.lead_owner:
frappe.throw(_("Lead Owner cannot be same as the Lead Email Address"))
if self.is_new() or not self.image:
self.image = has_gravatar(self.email_id)
def link_to_contact(self):
# update contact links
if self.contact_doc:

View File

@@ -130,6 +130,7 @@ def make_lead_from_communication(communication: str, ignore_communication_links:
}
)
lead.flags.ignore_mandatory = True
lead.flags.ignore_permissions = True
lead.insert()
lead_name = lead.name

View File

@@ -145,7 +145,7 @@ def make_opportunity_from_communication(
"opportunity_from": opportunity_from,
"party_name": lead,
}
).insert()
).insert(ignore_permissions=True)
link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links)

View File

@@ -5,11 +5,6 @@ from frappe.utils import cstr, now, today
from pypika import functions
def disable_opportunity_creation_on_contact_us_disabled(doc, method):
if doc.is_disabled:
frappe.db.set_single_value("CRM Settings", "enable_opportunity_creation_from_contact_us", 0)
def update_lead_phone_numbers(contact, method):
if contact.phone_nos:
contact_lead = contact.get_link_for("Lead")

View File

@@ -383,9 +383,6 @@ doc_events = {
"Event": {
"after_insert": "erpnext.crm.utils.link_events_with_prospect",
},
"Contact Us Settings": {
"on_update": "erpnext.crm.utils.disable_opportunity_creation_on_contact_us_disabled",
},
"Sales Invoice": {
"on_submit": [
"erpnext.regional.italy.utils.sales_invoice_on_submit",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More