mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-04 18:23:05 +00:00
Compare commits
10 Commits
fix-je-par
...
feat-purch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87b65d09df | ||
|
|
1631985726 | ||
|
|
5284b8c7a8 | ||
|
|
a674d9216a | ||
|
|
9dfb60f826 | ||
|
|
1138effd7c | ||
|
|
c8cb70dbd2 | ||
|
|
5249274bcd | ||
|
|
56ed9e8e43 | ||
|
|
0147312951 |
51
.github/helper/install.sh
vendored
51
.github/helper/install.sh
vendored
@@ -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=$!
|
||||
|
||||
21
.github/workflows/server-tests-mariadb.yml
vendored
21
.github/workflows/server-tests-mariadb.yml
vendored
@@ -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
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"disabledLabels": [
|
||||
"conflicts"
|
||||
],
|
||||
"context": {
|
||||
"repos": [
|
||||
"frappe/frappe"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -17,7 +17,6 @@ frappe.ui.form.on("Budget", {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: frm.doc.company,
|
||||
root_type: ["in", ["Income", "Expense"]],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
"production_plan",
|
||||
"production_plan_item",
|
||||
"production_plan_sub_assembly_item",
|
||||
"grant_commission",
|
||||
"page_break",
|
||||
"column_break_pjyo",
|
||||
"job_card"
|
||||
@@ -934,6 +935,15 @@
|
||||
"non_negative": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.grant_commission",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -955,4 +965,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "field:purchase_partner_type",
|
||||
"creation": "2026-06-15 00:00:00.000000",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"purchase_partner_type"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "purchase_partner_type",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Purchase Partner Type",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2026-06-15 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Partner Type",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"translated_doctype": 1
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PurchasePartnerType(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
purchase_partner_type: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestPurchasePartnerType(ERPNextTestSuite):
|
||||
def test_purchase_partner_type_creation(self):
|
||||
if not frappe.db.exists("Purchase Partner Type", "_Test Purchase Partner Type"):
|
||||
ppt = frappe.new_doc("Purchase Partner Type")
|
||||
ppt.purchase_partner_type = "_Test Purchase Partner Type"
|
||||
ppt.insert(ignore_permissions=True)
|
||||
|
||||
self.assertTrue(frappe.db.exists("Purchase Partner Type", "_Test Purchase Partner Type"))
|
||||
frappe.delete_doc("Purchase Partner Type", "_Test Purchase Partner Type", force=True)
|
||||
0
erpnext/buying/doctype/purchase_team/__init__.py
Normal file
0
erpnext/buying/doctype/purchase_team/__init__.py
Normal file
83
erpnext/buying/doctype/purchase_team/purchase_team.json
Normal file
83
erpnext/buying/doctype/purchase_team/purchase_team.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2026-06-17 00:00:00",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"purchase_person",
|
||||
"contact_no",
|
||||
"allocated_percentage",
|
||||
"allocated_amount",
|
||||
"commission_rate",
|
||||
"incentives"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "purchase_person",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Purchase Person",
|
||||
"options": "Purchase Person",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "contact_no",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Contact No."
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "allocated_percentage",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Contribution (%)"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Contribution to Net Total",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "purchase_person.commission_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Commission Rate",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "incentives",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Incentives",
|
||||
"options": "Company:company:default_currency"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-17 00:00:00",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Team",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
27
erpnext/buying/doctype/purchase_team/purchase_team.py
Normal file
27
erpnext/buying/doctype/purchase_team/purchase_team.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class PurchaseTeam(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
allocated_amount: DF.Currency
|
||||
allocated_percentage: DF.Float
|
||||
commission_rate: DF.Data | None
|
||||
contact_no: DF.Data | None
|
||||
incentives: DF.Currency
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
purchase_person: DF.Link
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Purchase Partner Commission Summary"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "purchase_partner",
|
||||
label: __("Purchase Partner"),
|
||||
fieldtype: "Link",
|
||||
options: "Purchase Partner",
|
||||
},
|
||||
{
|
||||
fieldname: "doctype",
|
||||
label: __("Document Type"),
|
||||
fieldtype: "Select",
|
||||
options: "Purchase Order\nPurchase Receipt\nPurchase Invoice",
|
||||
default: "Purchase Order",
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "supplier",
|
||||
label: __("Supplier"),
|
||||
fieldtype: "Link",
|
||||
options: "Supplier",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"creation": "2026-06-15 00:00:00.000000",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-06-15 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Partner Commission Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Purchase Partner Commission Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import DocType, Field, Order
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.utils import QueryBuilder
|
||||
from frappe.utils.data import comma_or
|
||||
|
||||
PURCHASE_TRANSACTION_DOCTYPES = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
return PurchasePartnerCommissionSummaryReport(filters).run()
|
||||
|
||||
|
||||
class PurchasePartnerSummaryReport:
|
||||
"""Base class for Purchase Partner Summary related Reports."""
|
||||
|
||||
dt: DocType
|
||||
date_field: str
|
||||
date_label: str
|
||||
columns: list
|
||||
data: list
|
||||
query: QueryBuilder
|
||||
filters: dict
|
||||
|
||||
def __init__(self, filters: dict):
|
||||
self.filters = filters
|
||||
self.columns = []
|
||||
|
||||
def run(self):
|
||||
self.validate_filters()
|
||||
self.prepare_columns()
|
||||
self.get_data()
|
||||
return self.columns, self.data
|
||||
|
||||
def validate_filters(self):
|
||||
if not self.filters.get("doctype"):
|
||||
frappe.throw(_("Please select the document type first."))
|
||||
|
||||
if self.filters.get("doctype") not in PURCHASE_TRANSACTION_DOCTYPES:
|
||||
frappe.throw(_("DocType can be one of them {0}").format(comma_or(PURCHASE_TRANSACTION_DOCTYPES)))
|
||||
|
||||
if not self.filters.get("company"):
|
||||
frappe.throw(_("Please select a company."))
|
||||
|
||||
if (
|
||||
self.filters.get("from_date")
|
||||
and self.filters.get("to_date")
|
||||
and self.filters.get("from_date") > self.filters.get("to_date")
|
||||
):
|
||||
frappe.throw(_("From Date cannot be greater than To Date."))
|
||||
|
||||
self._set_date_field_and_label()
|
||||
|
||||
def _set_date_field_and_label(self):
|
||||
self.date_field = (
|
||||
"transaction_date" if self.filters.get("doctype") == "Purchase Order" else "posting_date"
|
||||
)
|
||||
self.date_label = _("Order Date") if self.date_field == "transaction_date" else _("Posting Date")
|
||||
|
||||
def prepare_columns(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_data(self):
|
||||
self.build_report_query()
|
||||
self.data = self.query.run(as_dict=1)
|
||||
|
||||
def build_report_query(self):
|
||||
self._build_report_base_query()
|
||||
self.extend_report_query()
|
||||
self._apply_common_filters()
|
||||
self.apply_filters()
|
||||
|
||||
def _build_report_base_query(self):
|
||||
self.dt = DocType(self.filters.get("doctype"))
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", self.filters.get("company"), "default_currency")
|
||||
|
||||
self.query = (
|
||||
frappe.qb.from_(self.dt)
|
||||
.select(
|
||||
self.dt.name,
|
||||
self.dt.supplier,
|
||||
Field(self.date_field, "posting_date", table=self.dt),
|
||||
self.dt.purchase_partner,
|
||||
self.dt.commission_rate,
|
||||
ConstantColumn(company_currency).as_("currency"),
|
||||
)
|
||||
.where(
|
||||
(self.dt.docstatus == 1)
|
||||
& (self.dt.purchase_partner.notnull())
|
||||
& (self.dt.purchase_partner != "")
|
||||
)
|
||||
.orderby(self.dt.name, order=Order.desc)
|
||||
.orderby(self.dt.purchase_partner)
|
||||
)
|
||||
|
||||
def extend_report_query(self):
|
||||
pass
|
||||
|
||||
def _apply_common_filters(self):
|
||||
for field in ["company", "supplier", "purchase_partner"]:
|
||||
if self.filters.get(field):
|
||||
self.query = self.query.where(Field(field, table=self.dt) == self.filters.get(field))
|
||||
|
||||
if self.filters.get("from_date"):
|
||||
self.query = self.query.where(
|
||||
Field(self.date_field, table=self.dt) >= self.filters.get("from_date")
|
||||
)
|
||||
|
||||
if self.filters.get("to_date"):
|
||||
self.query = self.query.where(
|
||||
Field(self.date_field, table=self.dt) <= self.filters.get("to_date")
|
||||
)
|
||||
|
||||
def apply_filters(self):
|
||||
pass
|
||||
|
||||
def make_column(
|
||||
self,
|
||||
label: str,
|
||||
fieldname: str,
|
||||
fieldtype: str,
|
||||
width: int = 140,
|
||||
options: str = "",
|
||||
hidden: int = 0,
|
||||
):
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=label,
|
||||
fieldname=fieldname,
|
||||
fieldtype=fieldtype,
|
||||
options=options,
|
||||
width=width,
|
||||
hidden=hidden,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class PurchasePartnerCommissionSummaryReport(PurchasePartnerSummaryReport):
|
||||
def prepare_columns(self):
|
||||
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
|
||||
self.make_column(_("Supplier"), "supplier", "Link", options="Supplier")
|
||||
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
|
||||
self.make_column(self.date_label, "posting_date", "Date")
|
||||
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
|
||||
self.make_column(_("Purchase Partner"), "purchase_partner", "Link", options="Purchase Partner")
|
||||
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
|
||||
self.make_column(_("Total Commission"), "total_commission", "Currency", 120, "currency")
|
||||
|
||||
def extend_report_query(self):
|
||||
self.query = self.query.select(
|
||||
self.dt.base_net_total.as_("amount"),
|
||||
self.dt.total_commission,
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.query_reports["Purchase Partner Target Variance Based On Item Group"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
{
|
||||
fieldname: "fiscal_year",
|
||||
label: __("Fiscal Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||
},
|
||||
{
|
||||
fieldname: "doctype",
|
||||
label: __("Document Type"),
|
||||
fieldtype: "Select",
|
||||
options: "Purchase Order\nPurchase Receipt\nPurchase Invoice",
|
||||
default: "Purchase Order",
|
||||
},
|
||||
{
|
||||
fieldname: "period",
|
||||
label: __("Period"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Monthly", label: __("Monthly") },
|
||||
{ value: "Quarterly", label: __("Quarterly") },
|
||||
{ value: "Half-Yearly", label: __("Half-Yearly") },
|
||||
{ value: "Yearly", label: __("Yearly") },
|
||||
],
|
||||
default: "Monthly",
|
||||
},
|
||||
{
|
||||
fieldname: "target_on",
|
||||
label: __("Target On"),
|
||||
fieldtype: "Select",
|
||||
options: "Quantity\nAmount",
|
||||
default: "Quantity",
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (column.fieldname.includes("variance")) {
|
||||
if (data[column.fieldname] < 0) {
|
||||
value = "<span style='color:red'>" + value + "</span>";
|
||||
} else if (data[column.fieldname] > 0) {
|
||||
value = "<span style='color:green'>" + value + "</span>";
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"creation": "2026-06-17 00:00:00",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-06-17 00:00:00",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Partner Target Variance Based On Item Group",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Purchase Partner Target Variance Based On Item Group",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.item_group_wise_sales_target_variance import (
|
||||
get_data_column,
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
return get_data_column(filters, "Purchase Partner")
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Purchase Partner Transaction Summary"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "purchase_partner",
|
||||
label: __("Purchase Partner"),
|
||||
fieldtype: "Link",
|
||||
options: "Purchase Partner",
|
||||
},
|
||||
{
|
||||
fieldname: "doctype",
|
||||
label: __("Document Type"),
|
||||
fieldtype: "Select",
|
||||
options: "Purchase Order\nPurchase Receipt\nPurchase Invoice",
|
||||
default: "Purchase Order",
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "supplier",
|
||||
label: __("Supplier"),
|
||||
fieldtype: "Link",
|
||||
options: "Supplier",
|
||||
},
|
||||
{
|
||||
fieldname: "item_group",
|
||||
label: __("Item Group"),
|
||||
fieldtype: "Link",
|
||||
options: "Item Group",
|
||||
},
|
||||
{
|
||||
fieldname: "brand",
|
||||
label: __("Brand"),
|
||||
fieldtype: "Link",
|
||||
options: "Brand",
|
||||
},
|
||||
{
|
||||
fieldname: "show_return_entries",
|
||||
label: __("Show Return Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"creation": "2026-06-15 00:00:00.000000",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-06-15 00:00:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Partner Transaction Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Purchase Partner Transaction Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Case
|
||||
|
||||
from erpnext.buying.report.purchase_partner_commission_summary.purchase_partner_commission_summary import (
|
||||
PurchasePartnerSummaryReport,
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
return PurchasePartnerTransactionSummaryReport(filters=filters).run()
|
||||
|
||||
|
||||
class PurchasePartnerTransactionSummaryReport(PurchasePartnerSummaryReport):
|
||||
def prepare_columns(self):
|
||||
self.make_column(_(self.filters.get("doctype")), "name", "Link", options=self.filters.get("doctype"))
|
||||
self.make_column(_("Supplier"), "supplier", "Link", options="Supplier")
|
||||
self.make_column(_("Currency"), "currency", "Data", 80, hidden=1)
|
||||
self.make_column(self.date_label, "posting_date", "Date")
|
||||
self.make_column(_("Item Code"), "item_code", "Link", 100, "Item")
|
||||
self.make_column(_("Item Group"), "item_group", "Link", 100, "Item Group")
|
||||
self.make_column(_("Brand"), "brand", "Link", 100, "Brand")
|
||||
self.make_column(_("Quantity"), "qty", "Float", 120)
|
||||
self.make_column(_("Rate"), "rate", "Currency", 120, "currency")
|
||||
self.make_column(_("Amount"), "amount", "Currency", 120, "currency")
|
||||
self.make_column(_("Purchase Partner"), "purchase_partner", "Link", options="Purchase Partner")
|
||||
self.make_column(_("Commission Rate %"), "commission_rate", "Data", 100)
|
||||
self.make_column(_("Commission"), "commission", "Currency", 120, "currency")
|
||||
|
||||
def extend_report_query(self):
|
||||
self.dt_item = frappe.qb.DocType(f"{self.filters['doctype']} Item")
|
||||
|
||||
self.query = (
|
||||
self.query.join(self.dt_item)
|
||||
.on(self.dt.name == self.dt_item.parent)
|
||||
.select(
|
||||
self.dt_item.base_net_rate.as_("rate"),
|
||||
self.dt_item.qty,
|
||||
self.dt_item.base_net_amount.as_("amount"),
|
||||
Case()
|
||||
.when(
|
||||
self.dt_item.grant_commission.eq(1),
|
||||
((self.dt_item.base_net_amount * self.dt.commission_rate) / 100),
|
||||
)
|
||||
.else_(0)
|
||||
.as_("commission"),
|
||||
self.dt_item.brand,
|
||||
self.dt_item.item_group,
|
||||
self.dt_item.item_code,
|
||||
)
|
||||
)
|
||||
|
||||
def apply_filters(self):
|
||||
if not self.filters.get("show_return_entries"):
|
||||
self.query = self.query.where(self.dt_item.qty > 0.0)
|
||||
|
||||
if self.filters.get("brand"):
|
||||
self.query = self.query.where(self.dt_item.brand == self.filters.get("brand"))
|
||||
|
||||
if self.filters.get("item_group"):
|
||||
lft, rgt = frappe.get_cached_value("Item Group", self.filters.get("item_group"), ["lft", "rgt"])
|
||||
if item_groups := frappe.get_all(
|
||||
"Item Group", filters=[["lft", ">=", lft], ["rgt", "<=", rgt]], pluck="name"
|
||||
):
|
||||
self.query = self.query.where(self.dt_item.item_group.isin(item_groups))
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.query_reports["Purchase Person Commission Summary"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "purchase_person",
|
||||
label: __("Purchase Person"),
|
||||
fieldtype: "Link",
|
||||
options: "Purchase Person",
|
||||
},
|
||||
{
|
||||
fieldname: "doc_type",
|
||||
label: __("Document Type"),
|
||||
fieldtype: "Select",
|
||||
options: "Purchase Order\nPurchase Receipt\nPurchase Invoice",
|
||||
default: "Purchase Order",
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
{
|
||||
fieldname: "supplier",
|
||||
label: __("Supplier"),
|
||||
fieldtype: "Link",
|
||||
options: "Supplier",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"creation": "2026-06-17 00:00:00",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-06-17 00:00:00",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Person Commission Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Purchase Person Commission Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.query_builder import Criterion
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
columns = get_columns(filters)
|
||||
entries = get_entries(filters)
|
||||
data = []
|
||||
|
||||
for d in entries:
|
||||
data.append(
|
||||
[
|
||||
d.name,
|
||||
d.supplier,
|
||||
d.posting_date,
|
||||
d.base_net_amount,
|
||||
d.purchase_person,
|
||||
d.allocated_percentage,
|
||||
d.commission_rate,
|
||||
d.allocated_amount,
|
||||
d.incentives,
|
||||
]
|
||||
)
|
||||
|
||||
if data:
|
||||
total_row = [""] * len(data[0])
|
||||
data.append(total_row)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
if not filters.get("doc_type"):
|
||||
msgprint(_("Please select the document type first"), raise_exception=1)
|
||||
|
||||
return [
|
||||
{
|
||||
"label": _(filters["doc_type"]),
|
||||
"options": filters["doc_type"],
|
||||
"fieldname": filters["doc_type"],
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Supplier"),
|
||||
"options": "Supplier",
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
|
||||
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
|
||||
{
|
||||
"label": _("Purchase Person"),
|
||||
"options": "Purchase Person",
|
||||
"fieldname": "purchase_person",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Contribution %"),
|
||||
"fieldname": "contribution_percentage",
|
||||
"fieldtype": "Data",
|
||||
"width": 110,
|
||||
},
|
||||
{
|
||||
"label": _("Commission Rate %"),
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Contribution Amount"),
|
||||
"fieldname": "contribution_amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 120,
|
||||
},
|
||||
{"label": _("Incentives"), "fieldname": "incentives", "fieldtype": "Currency", "width": 120},
|
||||
]
|
||||
|
||||
|
||||
def get_entries(filters):
|
||||
dt = qb.DocType(filters["doc_type"])
|
||||
pt = qb.DocType("Purchase Team")
|
||||
date_field = dt["transaction_date"] if filters["doc_type"] == "Purchase Order" else dt["posting_date"]
|
||||
|
||||
conditions = get_conditions(dt, pt, filters, date_field)
|
||||
return (
|
||||
qb.from_(dt)
|
||||
.join(pt)
|
||||
.on(pt.parent.eq(dt.name) & pt.parenttype.eq(filters["doc_type"]))
|
||||
.select(
|
||||
dt.name,
|
||||
dt.supplier,
|
||||
date_field.as_("posting_date"),
|
||||
dt.base_net_total.as_("base_net_amount"),
|
||||
pt.commission_rate,
|
||||
pt.purchase_person,
|
||||
pt.allocated_percentage,
|
||||
pt.allocated_amount,
|
||||
pt.incentives,
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.orderby(dt.name, pt.purchase_person)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
|
||||
def get_conditions(dt, pt, filters, date_field):
|
||||
conditions = [dt.docstatus.eq(1)]
|
||||
|
||||
from_dt = filters.get("from_date")
|
||||
to_dt = filters.get("to_date")
|
||||
if from_dt and to_dt:
|
||||
conditions.append(date_field.between(from_dt, to_dt))
|
||||
elif from_dt:
|
||||
conditions.append(date_field.gte(from_dt))
|
||||
elif to_dt:
|
||||
conditions.append(date_field.lte(to_dt))
|
||||
|
||||
for field in ["company", "supplier"]:
|
||||
if filters.get(field):
|
||||
conditions.append(dt[field].eq(filters.get(field)))
|
||||
|
||||
if filters.get("purchase_person"):
|
||||
conditions.append(pt["purchase_person"].eq(filters.get("purchase_person")))
|
||||
|
||||
return conditions
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.query_reports["Purchase Person Target Variance Based On Item Group"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
{
|
||||
fieldname: "fiscal_year",
|
||||
label: __("Fiscal Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||
},
|
||||
{
|
||||
fieldname: "doctype",
|
||||
label: __("Document Type"),
|
||||
fieldtype: "Select",
|
||||
options: "Purchase Order\nPurchase Receipt\nPurchase Invoice",
|
||||
default: "Purchase Order",
|
||||
},
|
||||
{
|
||||
fieldname: "period",
|
||||
label: __("Period"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Monthly", label: __("Monthly") },
|
||||
{ value: "Quarterly", label: __("Quarterly") },
|
||||
{ value: "Half-Yearly", label: __("Half-Yearly") },
|
||||
{ value: "Yearly", label: __("Yearly") },
|
||||
],
|
||||
default: "Monthly",
|
||||
},
|
||||
{
|
||||
fieldname: "target_on",
|
||||
label: __("Target On"),
|
||||
fieldtype: "Select",
|
||||
options: "Quantity\nAmount",
|
||||
default: "Quantity",
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (column.fieldname.includes("variance")) {
|
||||
if (data[column.fieldname] < 0) {
|
||||
value = "<span style='color:red'>" + value + "</span>";
|
||||
} else if (data[column.fieldname] > 0) {
|
||||
value = "<span style='color:green'>" + value + "</span>";
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"creation": "2026-06-17 00:00:00",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-06-17 00:00:00",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Person Target Variance Based On Item Group",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Purchase Person Target Variance Based On Item Group",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.item_group_wise_sales_target_variance import (
|
||||
get_data_column,
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
return get_data_column(filters, "Purchase Person")
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.query_reports["Purchase Person-wise Transaction Summary"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "purchase_person",
|
||||
label: __("Purchase Person"),
|
||||
fieldtype: "Link",
|
||||
options: "Purchase Person",
|
||||
},
|
||||
{
|
||||
fieldname: "doc_type",
|
||||
label: __("Document Type"),
|
||||
fieldtype: "Select",
|
||||
options: "Purchase Order\nPurchase Receipt\nPurchase Invoice",
|
||||
default: "Purchase Order",
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("From Date"),
|
||||
fieldtype: "Date",
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1],
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
label: __("To Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "item_group",
|
||||
label: __("Item Group"),
|
||||
fieldtype: "Link",
|
||||
options: "Item Group",
|
||||
},
|
||||
{
|
||||
fieldname: "brand",
|
||||
label: __("Brand"),
|
||||
fieldtype: "Link",
|
||||
options: "Brand",
|
||||
},
|
||||
{
|
||||
fieldname: "supplier",
|
||||
label: __("Supplier"),
|
||||
fieldtype: "Link",
|
||||
options: "Supplier",
|
||||
},
|
||||
{
|
||||
fieldname: "show_return_entries",
|
||||
label: __("Show Return Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"creation": "2026-06-17 00:00:00",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2026-06-17 00:00:00",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Person-wise Transaction Summary",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Purchase Person-wise Transaction Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.query_builder import Case, Criterion
|
||||
|
||||
from erpnext import get_company_currency
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
entries = get_entries(filters)
|
||||
item_details = get_item_details()
|
||||
data = []
|
||||
|
||||
company_currency = get_company_currency(filters.get("company"))
|
||||
|
||||
for d in entries:
|
||||
if d.stock_qty > 0 or filters.get("show_return_entries", 0):
|
||||
data.append(
|
||||
[
|
||||
d.name,
|
||||
d.supplier,
|
||||
d.warehouse,
|
||||
d.posting_date,
|
||||
d.item_code,
|
||||
item_details.get(d.item_code, {}).get("item_group"),
|
||||
item_details.get(d.item_code, {}).get("brand"),
|
||||
d.stock_qty,
|
||||
d.base_net_amount,
|
||||
d.purchase_person,
|
||||
d.allocated_percentage,
|
||||
(d.stock_qty * d.allocated_percentage / 100),
|
||||
d.contribution_amt,
|
||||
company_currency,
|
||||
]
|
||||
)
|
||||
|
||||
if data:
|
||||
total_row = [""] * len(data[0])
|
||||
data.append(total_row)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def validate_filters(filters):
|
||||
ALLOWED_DOCTYPES = ["Purchase Order", "Purchase Invoice", "Purchase Receipt"]
|
||||
|
||||
if not filters.get("doc_type"):
|
||||
msgprint(_("Please select the document type first"), raise_exception=1)
|
||||
|
||||
if filters.get("doc_type") not in ALLOWED_DOCTYPES:
|
||||
frappe.throw(_("{0}, {1} or {2} are the only allowed options.").format(*ALLOWED_DOCTYPES))
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
return [
|
||||
{
|
||||
"label": _(filters["doc_type"]),
|
||||
"options": filters["doc_type"],
|
||||
"fieldname": frappe.scrub(filters["doc_type"]),
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Supplier"),
|
||||
"options": "Supplier",
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Warehouse"),
|
||||
"options": "Warehouse",
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 140},
|
||||
{
|
||||
"label": _("Item Code"),
|
||||
"options": "Item",
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Item Group"),
|
||||
"options": "Item Group",
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Brand"),
|
||||
"options": "Brand",
|
||||
"fieldname": "brand",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{"label": _("Total Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 140},
|
||||
{
|
||||
"label": _("Amount"),
|
||||
"options": "currency",
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Purchase Person"),
|
||||
"options": "Purchase Person",
|
||||
"fieldname": "purchase_person",
|
||||
"fieldtype": "Link",
|
||||
"width": 140,
|
||||
},
|
||||
{"label": _("Contribution %"), "fieldname": "contribution", "fieldtype": "Float", "width": 140},
|
||||
{
|
||||
"label": _("Contribution Qty"),
|
||||
"fieldname": "contribution_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Contribution Amount"),
|
||||
"options": "currency",
|
||||
"fieldname": "contribution_amt",
|
||||
"fieldtype": "Currency",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Currency"),
|
||||
"options": "Currency",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_entries(filters):
|
||||
doc_type = filters["doc_type"]
|
||||
|
||||
date_field = "transaction_date" if doc_type == "Purchase Order" else "posting_date"
|
||||
qty_field = "received_qty" if doc_type == "Purchase Order" else "qty"
|
||||
|
||||
dt = frappe.qb.DocType(doc_type)
|
||||
dt_item = frappe.qb.DocType(f"{doc_type} Item")
|
||||
pt = frappe.qb.DocType("Purchase Team")
|
||||
|
||||
calc_qty = dt_item[qty_field] * dt_item.conversion_factor
|
||||
calc_net_amount = dt_item.base_net_rate * calc_qty
|
||||
|
||||
stock_qty_case = Case().when(dt.status == "Closed", calc_qty).else_(dt_item.stock_qty).as_("stock_qty")
|
||||
|
||||
base_net_amount_case = (
|
||||
Case()
|
||||
.when(dt.status == "Closed", calc_net_amount)
|
||||
.else_(dt_item.base_net_amount)
|
||||
.as_("base_net_amount")
|
||||
)
|
||||
|
||||
contribution_amt_case = (
|
||||
Case()
|
||||
.when(dt.status == "Closed", (calc_net_amount * pt.allocated_percentage / 100))
|
||||
.else_(dt_item.base_net_amount * pt.allocated_percentage / 100)
|
||||
.as_("contribution_amt")
|
||||
)
|
||||
|
||||
conditions = get_conditions(dt, pt, filters, date_field)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(dt)
|
||||
.join(dt_item)
|
||||
.on(dt.name == dt_item.parent)
|
||||
.join(pt)
|
||||
.on(dt.name == pt.parent)
|
||||
.select(
|
||||
dt.name,
|
||||
dt.supplier,
|
||||
dt[date_field].as_("posting_date"),
|
||||
dt_item.item_code,
|
||||
pt.purchase_person,
|
||||
pt.allocated_percentage,
|
||||
dt_item.warehouse,
|
||||
stock_qty_case,
|
||||
base_net_amount_case,
|
||||
contribution_amt_case,
|
||||
)
|
||||
.where(pt.parenttype == doc_type)
|
||||
.where(dt.docstatus == 1)
|
||||
.where(Criterion.all(conditions))
|
||||
.orderby(pt.purchase_person)
|
||||
.orderby(dt.name, order=frappe.qb.desc)
|
||||
)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_conditions(dt, pt, filters, date_field):
|
||||
conditions = []
|
||||
|
||||
for field in ["company", "supplier"]:
|
||||
if filters.get(field):
|
||||
conditions.append(dt[field].eq(filters[field]))
|
||||
|
||||
if filters.get("purchase_person"):
|
||||
lft, rgt = frappe.get_value("Purchase Person", filters.get("purchase_person"), ["lft", "rgt"])
|
||||
purchase_person_tbl = frappe.qb.DocType("Purchase Person")
|
||||
subquery = (
|
||||
frappe.qb.from_(purchase_person_tbl)
|
||||
.select(purchase_person_tbl.name)
|
||||
.where(purchase_person_tbl.lft >= lft)
|
||||
.where(purchase_person_tbl.rgt <= rgt)
|
||||
)
|
||||
conditions.append(pt.purchase_person.isin(subquery))
|
||||
|
||||
if filters.get("from_date"):
|
||||
conditions.append(dt[date_field].gte(filters["from_date"]))
|
||||
|
||||
if filters.get("to_date"):
|
||||
conditions.append(dt[date_field].lte(filters["to_date"]))
|
||||
|
||||
items = get_items(filters)
|
||||
if items:
|
||||
conditions.append(
|
||||
frappe.qb.DocType(f"{filters['doc_type']} Item").item_code.isin([i[0] for i in items])
|
||||
)
|
||||
elif filters.get("item_group") or filters.get("brand"):
|
||||
conditions.append(frappe.qb.terms.ValueWrapper(0).eq(1))
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def get_items(filters):
|
||||
item = qb.DocType("Item")
|
||||
|
||||
item_query_conditions = []
|
||||
if filters.get("item_group"):
|
||||
item_group = qb.DocType("Item Group")
|
||||
lft, rgt = frappe.db.get_all(
|
||||
"Item Group", filters={"name": filters.get("item_group")}, fields=["lft", "rgt"], as_list=True
|
||||
)[0]
|
||||
item_group_query = (
|
||||
qb.from_(item_group)
|
||||
.select(item_group.name)
|
||||
.where((item_group.lft >= lft) & (item_group.rgt <= rgt))
|
||||
)
|
||||
item_query_conditions.append(item.item_group.isin(item_group_query))
|
||||
if filters.get("brand"):
|
||||
item_query_conditions.append(item.brand == filters.get("brand"))
|
||||
|
||||
if not item_query_conditions:
|
||||
return []
|
||||
|
||||
return qb.from_(item).select(item.name).where(Criterion.all(item_query_conditions)).run()
|
||||
|
||||
|
||||
def get_item_details():
|
||||
items = frappe.get_all("Item", fields=["name", "item_group", "brand"])
|
||||
return {d.name: d for d in items}
|
||||
@@ -23,6 +23,7 @@
|
||||
"is_query_report": 0,
|
||||
"label": "Buying",
|
||||
"link_count": 0,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
@@ -86,6 +87,7 @@
|
||||
"is_query_report": 0,
|
||||
"label": "Items & Pricing",
|
||||
"link_count": 0,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
@@ -171,6 +173,7 @@
|
||||
"is_query_report": 0,
|
||||
"label": "Settings",
|
||||
"link_count": 0,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
@@ -212,6 +215,7 @@
|
||||
"is_query_report": 0,
|
||||
"label": "Supplier",
|
||||
"link_count": 0,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
@@ -264,6 +268,7 @@
|
||||
"is_query_report": 0,
|
||||
"label": "Supplier Scorecard",
|
||||
"link_count": 0,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
@@ -316,6 +321,7 @@
|
||||
"is_query_report": 0,
|
||||
"label": "Key Reports",
|
||||
"link_count": 0,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
@@ -385,11 +391,140 @@
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Purchase Partner",
|
||||
"link_count": 0,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Purchase Partner",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Partner",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Purchase Partner Type",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Partner Type",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Purchase Partner",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Partners Commission",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Partners Commission",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Purchase Partner",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Partner Commission Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Partner Commission Summary",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Purchase Partner",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Partner Transaction Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Partner Transaction Summary",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Purchase Partner",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Partner Target Variance Based On Item Group",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Partner Target Variance Based On Item Group",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Purchase Person",
|
||||
"link_count": 0,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Purchase Person",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Person",
|
||||
"link_type": "DocType",
|
||||
"onboard": 1,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Purchase Person",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Person-wise Transaction Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Person-wise Transaction Summary",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Purchase Person",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Person Commission Summary",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Person Commission Summary",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Purchase Person",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Purchase Person Target Variance Based On Item Group",
|
||||
"link_count": 0,
|
||||
"link_to": "Purchase Person Target Variance Based On Item Group",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Other Reports",
|
||||
"link_count": 0,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
@@ -497,6 +632,7 @@
|
||||
"is_query_report": 0,
|
||||
"label": "Regional",
|
||||
"link_count": 0,
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
@@ -512,7 +648,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-01-02 14:55:59.078773",
|
||||
"modified": "2026-06-17 13:13:38.489837",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying",
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
2239
erpnext/locale/ar.po
2239
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
2239
erpnext/locale/bs.po
2239
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
2235
erpnext/locale/cs.po
2235
erpnext/locale/cs.po
File diff suppressed because it is too large
Load Diff
2235
erpnext/locale/da.po
2235
erpnext/locale/da.po
File diff suppressed because it is too large
Load Diff
2239
erpnext/locale/de.po
2239
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
2239
erpnext/locale/eo.po
2239
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
2239
erpnext/locale/es.po
2239
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
2239
erpnext/locale/fa.po
2239
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
2239
erpnext/locale/fr.po
2239
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
62782
erpnext/locale/hi.po
62782
erpnext/locale/hi.po
File diff suppressed because it is too large
Load Diff
2239
erpnext/locale/hr.po
2239
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
2237
erpnext/locale/hu.po
2237
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
2235
erpnext/locale/id.po
2235
erpnext/locale/id.po
File diff suppressed because it is too large
Load Diff
2235
erpnext/locale/it.po
2235
erpnext/locale/it.po
File diff suppressed because it is too large
Load Diff
2239
erpnext/locale/ko.po
2239
erpnext/locale/ko.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2235
erpnext/locale/my.po
2235
erpnext/locale/my.po
File diff suppressed because it is too large
Load Diff
2235
erpnext/locale/nb.po
2235
erpnext/locale/nb.po
File diff suppressed because it is too large
Load Diff
2239
erpnext/locale/nl.po
2239
erpnext/locale/nl.po
File diff suppressed because it is too large
Load Diff
2239
erpnext/locale/pl.po
2239
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
2239
erpnext/locale/pt.po
2239
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2239
erpnext/locale/ru.po
2239
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
2235
erpnext/locale/sl.po
2235
erpnext/locale/sl.po
File diff suppressed because it is too large
Load Diff
2239
erpnext/locale/sr.po
2239
erpnext/locale/sr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2347
erpnext/locale/sv.po
2347
erpnext/locale/sv.po
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
Reference in New Issue
Block a user