Compare commits

..

54 Commits

Author SHA1 Message Date
Mohd Haris
a3f262b415 fix: restrict Party Type/Party to Receivable/Payable accounts in Journal Entry
Journal Entry validation only checked party details when an account was
of type Receivable/Payable, but never restricted setting a Party Type or
Party against accounts of other types. This regressed v14 behavior where
party info could only be captured for Receivable/Payable accounts.

Add a branch to validate_party() that throws when a Party Type or Party
is set on an account that is not Receivable/Payable, with a clear message.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:21:51 +05:30
ruthra kumar
7e602d5389 Merge pull request #53152 from aerele/fix_payment_entry
fix: prevent exchange rate flow from transaction to payment
2026-06-16 14:02:55 +05:30
rohitwaghchaure
529f8dc7cd Merge pull request #55826 from rohitwaghchaure/moved-files-to-services
refactor: moved files from stock_entry_handler to services
2026-06-16 13:57:08 +05:30
Shllokkk
52b406f5f1 fix(budget): add root_type filter on account field (#55934) 2026-06-16 11:43:52 +05:30
MochaMind
3dda2005d8 fix: sync translations from crowdin (#55900) 2026-06-16 11:13:36 +05:30
Jatin3128
322d4dff25 fix: clear stale payment rows on non-POS returns so they don't surface in bank reconciliation (#55903) 2026-06-16 10:42:36 +05:30
ruthra kumar
01a10fb5b0 Merge pull request #55949 from ruthra-kumar/speed_up_item_wise_inventory_account_tests
refactor(test): speed up item wise inventory test
2026-06-16 08:47:00 +05:30
ruthra kumar
4c084f7eff Merge pull request #55948 from ruthra-kumar/transaction_deletion_record_test_speed_up
refactor(test): faster transaction deletion record tests
2026-06-16 08:34:03 +05:30
ruthra kumar
627f2058b5 refactor(test): speed up item wise inventory test 2026-06-16 08:23:45 +05:30
ruthra kumar
8db4d2705a refactor(test): dont create master data in setUp 2026-06-16 08:03:47 +05:30
ruthra kumar
4ca7bc8ccf Merge pull request #55942 from ruthra-kumar/speed_up_delivery_note_tests
refactor(test): dont create company in setUp of Deliv Note
2026-06-15 20:16:16 +05:30
rohitwaghchaure
ca5cc4afdc Merge pull request #55928 from aerele/fix/support-#70854
fix(stock): update stock value calculation in stock balance report
2026-06-15 18:10:23 +05:30
Jatin3128
380b005659 fix: fiscal year check on validation (#55930) 2026-06-15 18:08:05 +05:30
ruthra kumar
df0ad93262 refactor(test): dont create company in setUp of Deliv Note 2026-06-15 17:51:02 +05:30
ruthra kumar
f503614cc0 Merge pull request #55929 from ruthra-kumar/parallelize_and_optimize_install_helper
ci: optimize install helper setup
2026-06-15 17:39:24 +05:30
Rohit Waghchaure
6d9beea56b refactor: moved files from stock_entry_handler to services 2026-06-15 17:28:50 +05:30
rohitwaghchaure
560d8bb674 Merge pull request #55926 from rohitwaghchaure/fixed-recalculate-rate-for-purchase-doc
fix: recalculate incoming rate in SLE for purchase documents during repost
2026-06-15 17:03:56 +05:30
Mihir Kandoi
a3e3e1b32c ci: optimize install helper setup
Cc: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 16:50:50 +05:30
Sudharsanan11
2492dfa558 fix(stock): update stock value calculation in stock balance report 2026-06-15 16:27:18 +05:30
ervishnucs
3b5a203d61 test: resolve failed testcases for exchage rate 2026-06-15 16:20:04 +05:30
ervishnucs
934abe5c6d fix: prevent exchange rate flow from transaction to payment 2026-06-15 16:20:04 +05:30
Rohit Waghchaure
867ee484b9 fix: recalculate incoming rate in SLE for purchase documents during repost 2026-06-15 16:09:03 +05:30
Diptanil Saha
2652082475 Merge pull request #55755 from diptanilsaha/feat/ces_frankfurter_v2
feat(currency exchange settings): frankfurter v2 support
2026-06-15 14:22:49 +05:30
diptanilsaha
abb579e2db fix(get_exchange_rate): using get_single_value to fetch disabled value from currency_exchange_settings 2026-06-15 13:52:34 +05:30
diptanilsaha
0c2d5488a6 fix: restricting currency_exchange_settings write permission only to system manager 2026-06-15 13:52:34 +05:30
diptanilsaha
138f683a68 test: fixed currency exchange test for frankfurter v2 api 2026-06-15 13:52:27 +05:30
diptanilsaha
479f9f63c9 fix: use frankfurter v2 by default for new install 2026-06-15 13:52:06 +05:30
diptanilsaha
56bfe6b6a6 feat(currency exchange settings): frankfurter v2 support 2026-06-15 13:52:06 +05:30
rohitwaghchaure
acae34c8e1 Merge pull request #55901 from rohitwaghchaure/fixed-regression-security-fixes
fix: regression issues related to security fixes
2026-06-15 12:20:38 +05:30
Mihir Kandoi
dcbe4a6d55 Merge pull request #55906 from raghavisruia/develop
fix: show company name in delete transactions confirmation dialog
2026-06-15 09:49:59 +05:30
Raghav Ruia
87d26a2d67 fix: show company name in delete transactions confirmation dialog
Display the actual company name in bold within the confirmation dialog
label so users immediately know which company they must type to confirm,
reducing the risk of accidental data loss.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 09:45:55 +05:30
Rohit Waghchaure
e1d8d06966 refactor: consolidate duplicate get_party_bank_account into bank_account.py 2026-06-14 23:53:35 +05:30
Rohit Waghchaure
8c88cecc1f fix: regression issues related to security fixes 2026-06-14 23:42:48 +05:30
MochaMind
9aeafb8140 fix: sync translations from crowdin (#55784) 2026-06-14 17:37:37 +00:00
MochaMind
c24e9796ae chore: update POT file (#55894) 2026-06-14 13:11:21 +02:00
rohitwaghchaure
c7d42e161b Merge pull request #55877 from rohitwaghchaure/feat-allow_to_edit_stock_uom_qty_for_stock_entry
feat: Allow to edit stock UOM qty for Stock Entry
2026-06-14 09:54:24 +05:30
Raffael Meyer
701896692a ci: set disabledLabels and context for greptile (#55883) 2026-06-13 18:46:03 +00:00
Raffael Meyer
93d6be2ed7 fix(Lead): stop storing Gravatar image URLs for Leads (#55880) 2026-06-13 19:03:29 +02:00
Rohit Waghchaure
b0e9ad198f feat: Allow to edit stock UOM qty for Stock Entry 2026-06-13 21:41:05 +05:30
Dipen Gala
a9029f83c7 feat(invoices): add tooltip description to Update Stock checkbox (#55868)
* feat(invoices): add tooltip description to Update Stock checkbox

Adds a description below the Update Stock checkbox on both Sales Invoice
and Purchase Invoice so users understand when to use the field without
consulting documentation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(invoices): replace Update Stock description with hover info tooltip

Removes the inline description text and adds an ℹ icon next to the
Update Stock checkbox label on both Sales Invoice and Purchase Invoice.
Hovering the icon shows the contextual tooltip via Bootstrap tooltip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(invoices): use Frappe native tooltip-content class for Update Stock icon

Replace Bootstrap .tooltip() (pure black bg) with Frappe's own
.tooltip-content CSS class so the hover tooltip matches the rest of
the ERPNext UI — uses var(--bg-dark-gray) and var(--text-dark).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(invoices): use frappe.ui.SidebarCard for Update Stock info tooltip

Replace custom CSS tooltip with the same SidebarCard + Popper approach
Frappe's InfoCard uses for field description tooltips — gives the native
ERPNext card appearance (white card, border, shadow) on hover.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(invoices): use built-in field description for Update Stock tooltip

Replace custom SidebarCard JS tooltip with Frappe's native
description + show_description_on_click field property on the
update_stock field in Sales Invoice and Purchase Invoice.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove duplicate description in purchase_invoice update_stock field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* revert: restore custom tooltip in purchase_invoice.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* revert: remove all changes from purchase_invoice.js

Keep purchase_invoice.js identical to upstream develop.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 20:48:03 +05:30
rohitwaghchaure
31e4da562d Merge pull request #55874 from rohitwaghchaure/fixed-permission-for-bom-comparison-tool
fix: permission in bom compare tool
2026-06-13 19:10:38 +05:30
Rohit Waghchaure
e6fdb3702a fix: permission in bom compare tool 2026-06-13 19:09:04 +05:30
rohitwaghchaure
bd60a9be90 Merge pull request #55849 from rohitwaghchaure/fixed-permissions-for-whitelist-functions
fix: permission for whitelist functions
2026-06-13 18:36:46 +05:30
Rohit Waghchaure
a64466561f fix: permission for whitelist functions 2026-06-13 17:45:37 +05:30
Mihir Kandoi
f7ff25d9a8 Merge pull request #55835 from mihir-kandoi/codex/develop-user-disable-audit-fix
fix: sync employee user status after save
2026-06-13 14:49:34 +05:30
Diptanil Saha
c933e34914 fix: opportunity creation from contact us page (#55841) 2026-06-13 04:47:45 +00:00
Mihir Kandoi
87092961e7 Merge pull request #55853 from SandraFrappe/fix/cost-center
fix: pass source cost center to target cost center
2026-06-12 20:57:04 +05:30
rohitwaghchaure
3f436985ed Merge pull request #55844 from rohitwaghchaure/fixed-job-card-permissions
fix: permissions in workstation file
2026-06-12 16:05:24 +05:30
Rohit Waghchaure
cf127e8900 fix: permissions in workstation file 2026-06-12 15:37:46 +05:30
SandraFrappe
9ea766fc10 fix: pass source cost center to target cost center 2026-06-12 14:44:22 +05:30
rohitwaghchaure
53180fde93 Merge pull request #55845 from frappe/fix-update-stock-expense-head-warning
fix: remove unnecessary expense head warning for purchase invoices with update stock
2026-06-12 13:29:34 +05:30
Dipen Gala
224dff32df fix: remove unnecessary expense head warning for purchase invoices with update stock
When a Purchase Invoice is created with `update_stock = 1`, the system
automatically replaces the item's expense account with the correct
inventory account for perpetual inventory. This is expected behaviour,
but a `frappe.msgprint` warning was being shown to the user:

  "Expense Head changed to Stock In Hand because account Cost of Goods
   Sold is not linked to warehouse Stores or it is not the default
   inventory account."

The message is purely informational, provides no actionable guidance,
and confuses users who deliberately enable Update Stock. The underlying
account substitution logic is unchanged; only the popup is suppressed.

The two other `msgprint` calls (for the Purchase-Receipt-linked and
no-Purchase-Receipt flows) are intentionally preserved — those surface
a genuine change in behaviour that users may not expect.

Fixes: https://github.com/frappe/erpnext/issues/...
2026-06-12 12:57:58 +05:30
Mihir Kandoi
64175bdb3e fix: skip unchanged employee user status sync 2026-06-11 21:34:43 +05:30
Mihir Kandoi
4fed04c6c7 fix: sync employee user status after save 2026-06-11 20:58:35 +05:30
160 changed files with 100774 additions and 33057 deletions

View File

@@ -4,24 +4,46 @@ 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
@@ -37,6 +59,11 @@ 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'"
@@ -51,9 +78,11 @@ fi
install_whktml() {
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
# 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
sudo apt install /tmp/wkhtmltox.deb
}
install_whktml &
wkpid=$!

View File

@@ -59,6 +59,10 @@ 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
@@ -122,6 +126,12 @@ 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:
@@ -131,7 +141,14 @@ jobs:
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
- name: Run Tests
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'
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
env:
TYPE: server
@@ -141,6 +158,7 @@ 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 }}
@@ -149,6 +167,7 @@ jobs:
coverage:
name: Coverage Wrap Up
needs: test
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
steps:
- name: Clone

10
.greptile/config.json Normal file
View File

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

View File

@@ -94,6 +94,7 @@ 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,6 +518,7 @@ 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")
@@ -778,7 +779,6 @@ 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,6 +374,7 @@ 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 = []
@@ -401,6 +402,7 @@ 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,6 +17,7 @@ frappe.ui.form.on("Budget", {
filters: {
is_group: 0,
company: frm.doc.company,
root_type: ["in", ["Income", "Expense"]],
},
};
});

View File

@@ -11,22 +11,28 @@ frappe.ui.form.on("Currency Exchange Settings", {
},
callback: function (r) {
if (r && r.message) {
let result = [],
params = {};
if (frm.doc.service_provider == "exchangerate.host") {
let result = ["result"];
let params = {
result = ["result"];
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)) {
let result = ["rates", "{to_currency}"];
let params = {
result = ["rates", "{to_currency}"];
params = {
base: "{from_currency}",
symbols: "{to_currency}",
};
add_param(frm, r.message, params, result);
} else if (frm.doc.service_provider == "frankfurter.dev - v2") {
result = ["rate"];
params = {
date: "{transaction_date}",
};
}
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\nCustom",
"options": "frankfurter.dev\nexchangerate.host\nfrankfurter.dev - v2\nCustom",
"reqd": 1
},
{
@@ -101,11 +101,10 @@
"label": "Use HTTP Protocol"
}
],
"hide_toolbar": 0,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-03-16 13:28:21.075743",
"modified": "2026-06-15 11:25:55.873110",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",
@@ -122,24 +121,11 @@
"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,
"write": 1
"share": 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", "Custom"]
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "frankfurter.dev - v2", "Custom"]
url: DF.Data | None
use_http: DF.Check
# end: auto-generated types
@@ -70,6 +70,14 @@ 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:
@@ -105,13 +113,20 @@ 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"]:
if service_provider and service_provider in [
"exchangerate.host",
"frankfurter.dev",
"frankfurter.app",
"frankfurter.dev - v2",
]:
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,6 +484,12 @@ 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,6 +662,13 @@ 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(ref_doc=doc)
pe.set_exchange_rate()
pe.set_amounts()
# If PE is created from PR directly, then no need to find open PRs for the references

View File

@@ -532,6 +532,8 @@ 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()
@@ -607,6 +609,8 @@ 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,11 +11,12 @@ 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, get_party_bank_account
from erpnext.accounts.party import get_party_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,7 +332,12 @@ class TestPaymentRequest(ERPNextTestSuite):
return_doc=1,
)
pe = pr.set_as_paid()
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()
expected_gle = dict(
(d[0], d)
@@ -418,7 +423,12 @@ 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()
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()
self.assertEqual(pe.base_paid_amount, 800)
self.assertEqual(pe.paid_amount, 800)
self.assertEqual(pe.base_received_amount, 800)

View File

@@ -167,14 +167,6 @@
"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",
@@ -622,10 +614,12 @@
{
"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
"print_hide": 1,
"show_description_on_click": 1
},
{
"fieldname": "scan_barcode",
@@ -1691,66 +1685,6 @@
"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,
@@ -1758,7 +1692,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-05-28 12:36:55.215363",
"modified": "2026-06-13 18:36:46.704623",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -51,16 +51,6 @@ 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,7 +121,6 @@
"dimension_col_break",
"cost_center",
"section_break_82",
"grant_commission",
"page_break"
],
"fields": [
@@ -1005,15 +1004,6 @@
"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,
@@ -1031,4 +1021,4 @@
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -158,6 +158,7 @@ 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,6 +715,7 @@
{
"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,
@@ -722,7 +723,8 @@
"label": "Update Stock",
"oldfieldname": "update_stock",
"oldfieldtype": "Check",
"print_hide": 1
"print_hide": 1,
"show_description_on_click": 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,10 +114,17 @@ class POSService:
return pos
def set_paid_amount(self) -> None:
def update_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,11 +509,6 @@ 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

@@ -1,31 +0,0 @@
{
"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,6 +80,8 @@ 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,14 +133,6 @@
"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",
@@ -1299,66 +1291,6 @@
"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 (
update_subcontracting_order_status as update_sco_status,
set_subcontracting_order_status as update_sco_status,
)
doc = self.doc

View File

@@ -111,7 +111,6 @@
"production_plan",
"production_plan_item",
"production_plan_sub_assembly_item",
"grant_commission",
"page_break",
"column_break_pjyo",
"job_card"
@@ -935,15 +934,6 @@
"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,
@@ -965,4 +955,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -1,267 +0,0 @@
# 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,7 +23,6 @@
"is_query_report": 0,
"label": "Buying",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -87,7 +86,6 @@
"is_query_report": 0,
"label": "Items & Pricing",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -173,7 +171,6 @@
"is_query_report": 0,
"label": "Settings",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -215,7 +212,6 @@
"is_query_report": 0,
"label": "Supplier",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -268,7 +264,6 @@
"is_query_report": 0,
"label": "Supplier Scorecard",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -321,7 +316,6 @@
"is_query_report": 0,
"label": "Key Reports",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -391,140 +385,11 @@
"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"
},
@@ -632,7 +497,6 @@
"is_query_report": 0,
"label": "Regional",
"link_count": 0,
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
@@ -648,7 +512,7 @@
"type": "Link"
}
],
"modified": "2026-06-17 13:13:38.489837",
"modified": "2026-01-02 14:55:59.078773",
"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 validate_fiscal_year
from erpnext.accounts.utils import get_fiscal_year, 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):
if self.meta.get_field("fiscal_year"):
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"
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,
)
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),
)
def validate_due_date(self):
if self.get("is_pos") or self.doctype not in ["Sales Invoice", "Purchase Invoice"]:

View File

@@ -384,71 +384,6 @@ 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,6 +598,7 @@ 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 (
update_subcontracting_inward_order_status,
set_subcontracting_inward_order_status,
)
update_subcontracting_inward_order_status(self.subcontracting_inward_order)
set_subcontracting_inward_order_status(self.subcontracting_inward_order)
@frappe.whitelist()

View File

@@ -16,12 +16,13 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestItemWiseInventoryAccount(ERPNextTestSuite):
def setUp(self):
self.company = make_company()
self.company_abbr = frappe.db.get_value("Company", self.company, "abbr")
self.company = "_Test Company with perpetual inventory"
self.company_abbr = "TCP1"
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 = {
@@ -577,23 +578,3 @@ 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,6 +14,7 @@
"opportunity_section",
"close_opportunity_after_days",
"column_break_9",
"enable_opportunity_creation_from_contact_us",
"quotation_section",
"default_valid_till",
"section_break_13",
@@ -98,15 +99,20 @@
"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-03-16 13:28:19.573964",
"modified": "2026-06-11 23:09:49.750381",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM Settings",

View File

@@ -2,6 +2,7 @@
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
@@ -20,8 +21,20 @@ 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, has_gravatar, validate_email_address
from frappe.utils import comma_and, get_link_to_form, validate_email_address
from frappe.utils.data import DateTimeLikeObject
from erpnext.accounts.party import set_taxes
@@ -173,9 +173,6 @@ 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,7 +130,6 @@ 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(ignore_permissions=True)
).insert()
link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links)

View File

@@ -5,6 +5,11 @@ 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,6 +383,9 @@ 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

62782
erpnext/locale/hi.po Normal file

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