Compare commits

..

181 Commits

Author SHA1 Message Date
Frappe PR Bot
7b494dc9e8 chore(release): Bumped to Version 16.17.0
# [16.17.0](https://github.com/frappe/erpnext/compare/v16.16.0...v16.17.0) (2026-05-05)

### Bug Fixes

* accounts and account types in German CoA "SKR 03" (backport [#54711](https://github.com/frappe/erpnext/issues/54711)) ([#54713](https://github.com/frappe/erpnext/issues/54713)) ([982810a](982810a700))
* add missing fields in set_currency_labels (backport [#54689](https://github.com/frappe/erpnext/issues/54689)) ([#54690](https://github.com/frappe/erpnext/issues/54690)) ([bca893a](bca893a508))
* Backfill `not_applicable` on Item Tax Template Details for German companies (backport [#54682](https://github.com/frappe/erpnext/issues/54682)) ([#54686](https://github.com/frappe/erpnext/issues/54686)) ([a22d773](a22d773341))
* copy project from first row to new rows (backport [#53295](https://github.com/frappe/erpnext/issues/53295)) ([#54620](https://github.com/frappe/erpnext/issues/54620)) ([e24ab72](e24ab72c0d))
* correct project filter in buying doctypes (backport [#54644](https://github.com/frappe/erpnext/issues/54644)) ([#54652](https://github.com/frappe/erpnext/issues/54652)) ([86cf256](86cf256358))
* correct titles set to {customer_name} or {supplier_name} text strings (backport [#54656](https://github.com/frappe/erpnext/issues/54656)) ([#54669](https://github.com/frappe/erpnext/issues/54669)) ([38cfeb1](38cfeb1bb7))
* dont show serial/batch button when PR is submitted (backport [#54642](https://github.com/frappe/erpnext/issues/54642)) ([#54646](https://github.com/frappe/erpnext/issues/54646)) ([6dbc17d](6dbc17d71a))
* error when creating quotation from CRM (backport [#54722](https://github.com/frappe/erpnext/issues/54722)) ([#54725](https://github.com/frappe/erpnext/issues/54725)) ([2cd4c1a](2cd4c1a052))
* hide payment and payment request buttons based on permissions in invoices and orders (backport [#53920](https://github.com/frappe/erpnext/issues/53920)) ([#54736](https://github.com/frappe/erpnext/issues/54736)) ([e60490d](e60490dceb))
* incorrect expense account book in purchase return (backport [#54681](https://github.com/frappe/erpnext/issues/54681)) ([#54693](https://github.com/frappe/erpnext/issues/54693)) ([0dade2c](0dade2c38c))
* mark item tax templates as not applicable (backport [#54673](https://github.com/frappe/erpnext/issues/54673)) ([#54677](https://github.com/frappe/erpnext/issues/54677)) ([126e13b](126e13be25))
* **payment_entry:** convert the date args to string type before escaping in `get_outstanding_reference_documents` (backport [#54639](https://github.com/frappe/erpnext/issues/54639)) ([#54648](https://github.com/frappe/erpnext/issues/54648)) ([19a8ebe](19a8ebe8a5))
* **project:** use user.email for invitations and skip disabled users. (backport [#54561](https://github.com/frappe/erpnext/issues/54561)) ([#54667](https://github.com/frappe/erpnext/issues/54667)) ([288cdf3](288cdf3bf0))
* py error on sales forecast doctype (backport [#54641](https://github.com/frappe/erpnext/issues/54641)) ([#54643](https://github.com/frappe/erpnext/issues/54643)) ([7bd360a](7bd360aa29))
* Remove bom stock report link from manufacturing workspace ([0f27881](0f27881fed))
* **selling:** blanket order ordered qty recalculation on sales order status change (backport [#54593](https://github.com/frappe/erpnext/issues/54593)) ([#54623](https://github.com/frappe/erpnext/issues/54623)) ([9db03bc](9db03bc520))
* set valid_from in created Item Price (backport [#54696](https://github.com/frappe/erpnext/issues/54696)) ([#54700](https://github.com/frappe/erpnext/issues/54700)) ([bbb4e79](bbb4e79d0a))
* show correct status in Serial No Ledger (backport [#54567](https://github.com/frappe/erpnext/issues/54567)) ([#54626](https://github.com/frappe/erpnext/issues/54626)) ([d6f2ff6](d6f2ff6b87))
* show in and out qty in the stock ledger report for stock recos ([d27cf48](d27cf48b19))
* skip depreciation rescheduling when asset is fully depreciated on sale ([d3c893d](d3c893d08b))
* skip rescheduling only for asset being disposed ([07a957c](07a957c164))
* use RecoverableErrors isinstance check for repost timeout status (backport [#54543](https://github.com/frappe/erpnext/issues/54543)) ([#54649](https://github.com/frappe/erpnext/issues/54649)) ([b300159](b3001595ab))

### Features

* copy terms attachments to transactions (backport [#53403](https://github.com/frappe/erpnext/issues/53403)) ([#54661](https://github.com/frappe/erpnext/issues/54661)) ([bd932da](bd932da08b))
* **ux:** Naming series dialog ([#54554](https://github.com/frappe/erpnext/issues/54554)) ([48ebb4c](48ebb4ca61))

### Performance Improvements

* max recursion depth error in serial no (backport [#54629](https://github.com/frappe/erpnext/issues/54629)) ([#54631](https://github.com/frappe/erpnext/issues/54631)) ([808214f](808214fd95))
2026-05-05 16:32:20 +00:00
diptanilsaha
ed69dafbe8 Merge pull request #54740 from frappe/version-16-hotfix 2026-05-05 22:00:39 +05:30
Nishka Gosalia
c985f94009 Merge pull request #54743 from frappe/mergify/bp/version-16-hotfix/pr-54732
fix: Remove bom stock report link from manufacturing workspace (backport #54732)
2026-05-05 16:44:55 +05:30
nishkagosalia
0f27881fed fix: Remove bom stock report link from manufacturing workspace
(cherry picked from commit f86568b078)
2026-05-05 10:51:13 +00:00
mergify[bot]
e60490dceb fix: hide payment and payment request buttons based on permissions in invoices and orders (backport #53920) (#54736)
Co-authored-by: Sakthivel Murugan S <129778327+ssakthivelmurugan@users.noreply.github.com>
Co-authored-by: ravibharathi656 <ravibharathi656@gmail.com>
fix: hide payment and payment request buttons based on permissions in invoices and orders (#53920)
2026-05-05 12:17:57 +05:30
mergify[bot]
2cd4c1a052 fix: error when creating quotation from CRM (backport #54722) (#54725)
fix: error when creating quotation from CRM (#54722)

(cherry picked from commit 2d3190effb)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-04 16:04:03 +00:00
mergify[bot]
982810a700 fix: accounts and account types in German CoA "SKR 03" (backport #54711) (#54713)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: accounts and account types in German CoA "SKR 03" (#54711)
2026-05-03 17:49:02 +00:00
MochaMind
18006b978f chore: update POT file (#54710) 2026-05-03 14:24:28 +02:00
mergify[bot]
bbb4e79d0a fix: set valid_from in created Item Price (backport #54696) (#54700)
* fix: set valid_from in created Item Price (#54696)

Co-authored-by: Kaajal-Chhattani <kaajal.chhattani@aurigait.com>
(cherry picked from commit 6246a9aa6e)

# Conflicts:
#	erpnext/stock/get_item_details.py

* chore: resolve conflicts

---------

Co-authored-by: Kaajalchhattani <89331214+Kaajalchhattani@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-02 16:45:52 +00:00
mergify[bot]
bca893a508 fix: add missing fields in set_currency_labels (backport #54689) (#54690)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: add missing fields in set_currency_labels (#54689)
2026-05-01 14:39:39 +02:00
mergify[bot]
0dade2c38c fix: incorrect expense account book in purchase return (backport #54681) (#54693)
fix: incorrect expense account book in purchase return

(cherry picked from commit 2a720e7008)

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2026-05-01 12:47:05 +05:30
mergify[bot]
a22d773341 fix: Backfill not_applicable on Item Tax Template Details for German companies (backport #54682) (#54686)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: Backfill `not_applicable` on Item Tax Template Details for German companies (#54682)
2026-05-01 04:29:06 +02:00
mergify[bot]
126e13be25 fix: mark item tax templates as not applicable (backport #54673) (#54677)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: mark item tax templates as not applicable (#54673)
2026-04-30 17:52:24 +02:00
mergify[bot]
288cdf3bf0 fix(project): use user.email for invitations and skip disabled users. (backport #54561) (#54667)
fix(project): use user.email for invitations and skip disabled users. (#54561)

* fix(project): use user.email for invitations and skip disabled users.

* Update erpnext/projects/doctype/project/project.py



* fix(project): remove duplicate loop causing indentation error

* fix(project): resolve pre-commit hook failure

---------


(cherry picked from commit 231dd1856f)

Co-authored-by: Hemil-Sangani <hemil@sanskartechnolab.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-04-30 14:35:12 +05:30
rohitwaghchaure
2422237c1a Merge pull request #54671 from frappe/mergify/bp/version-16-hotfix/pr-54664
fix: show in and out qty in the stock ledger report for stock recos (backport #54664)
2026-04-30 14:34:23 +05:30
mergify[bot]
38cfeb1bb7 fix: correct titles set to {customer_name} or {supplier_name} text strings (backport #54656) (#54669)
Co-authored-by: Trusted Computer <75872475+trustedcomputer@users.noreply.github.com>
Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
fix: correct titles set to {customer_name} or {supplier_name} text strings (#54656)
2026-04-30 08:52:23 +00:00
Rohit Waghchaure
d27cf48b19 fix: show in and out qty in the stock ledger report for stock recos
(cherry picked from commit da081254a6)
2026-04-30 08:44:26 +00:00
Khushi Rawat
c232f1f450 Merge pull request #54659 from frappe/mergify/bp/version-16-hotfix/pr-54658
fix: skip depreciation rescheduling when asset is fully depreciated on sale (backport #54658)
2026-04-30 11:31:15 +05:30
mergify[bot]
bd932da08b feat: copy terms attachments to transactions (backport #53403) (#54661)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-04-29 23:43:28 +02:00
khushi8112
07a957c164 fix: skip rescheduling only for asset being disposed
(cherry picked from commit 88b82383f5)
2026-04-29 21:05:17 +00:00
khushi8112
d3c893d08b fix: skip depreciation rescheduling when asset is fully depreciated on sale
(cherry picked from commit c4155b6c81)
2026-04-29 21:05:17 +00:00
mergify[bot]
b3001595ab fix: use RecoverableErrors isinstance check for repost timeout status (backport #54543) (#54649)
fix: use RecoverableErrors isinstance check for repost timeout status

When a Repost Item Valuation job is killed by an RQ worker timeout
(JobTimeoutException raised via SIGALRM), the existing status detection
relied solely on traceback string matching for 'timeout' or 'Deadlock'.

This is unreliable because SIGALRM can interrupt a C-extension call
(e.g. inside pypika's copy.copy()) before Python records the exception
in the traceback. In that case the traceback shows only the interrupted
frame -- not JobTimeoutException -- so the job is permanently marked
'Failed' instead of 'In Progress', preventing the scheduler from
automatically retrying it.

RecoverableErrors = (JobTimeoutException, QueryDeadlockError,
QueryTimeoutError) is already defined at the top of this file and is
already used further down in the same except block to suppress email
notifications. Extend its use to also guard the status decision.

The traceback string fallback is kept as a secondary check for
forward compatibility with other timeout signals.

Fixes: jobs permanently stuck as 'Failed' after RQ worker timeout,
requiring manual re-queue to resume reposting.

(cherry picked from commit a49e2de866)

Co-authored-by: Assem Bahnasy <bahnasyassem@gmail.com>
2026-04-29 12:02:04 +00:00
mergify[bot]
86cf256358 fix: correct project filter in buying doctypes (backport #54644) (#54652)
fix: correct project filter in buying doctypes (#54644)

(cherry picked from commit a04c028522)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-29 17:28:12 +05:30
mergify[bot]
19a8ebe8a5 fix(payment_entry): convert the date args to string type before escaping in get_outstanding_reference_documents (backport #54639) (#54648)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(payment_entry): convert the date args to string type before escaping in `get_outstanding_reference_documents` (#54639)
2026-04-29 11:45:24 +00:00
mergify[bot]
6dbc17d71a fix: dont show serial/batch button when PR is submitted (backport #54642) (#54646)
fix: dont show serial/batch button when PR is submitted (#54642)

(cherry picked from commit 060defcc2b)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-29 11:32:37 +00:00
mergify[bot]
7bd360aa29 fix: py error on sales forecast doctype (backport #54641) (#54643)
fix: py error on sales forecast doctype (#54641)

fix: py error on sales forecase doctype
(cherry picked from commit d0d8cff48f)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-29 11:13:18 +00:00
Nishka Gosalia
2e438011da Merge pull request #54635 from frappe/mergify/bp/version-16-hotfix/pr-54554 2026-04-29 15:21:22 +05:30
Nishka Gosalia
48ebb4ca61 feat(ux): Naming series dialog (#54554)
(cherry picked from commit 844f3dbc0b)
2026-04-29 09:15:45 +00:00
mergify[bot]
808214fd95 perf: max recursion depth error in serial no (backport #54629) (#54631)
perf: max recursion depth error in serial no (#54629)

(cherry picked from commit 503b5bf140)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-29 08:53:07 +00:00
mergify[bot]
d6f2ff6b87 fix: show correct status in Serial No Ledger (backport #54567) (#54626)
* refactor: extract SN status logic

(cherry picked from commit cb2e6e1e2e)

* fix: show correct status in Serial No Ledger

(cherry picked from commit 2b3e047143)

---------

Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
2026-04-29 13:55:18 +05:30
mergify[bot]
9db03bc520 fix(selling): blanket order ordered qty recalculation on sales order status change (backport #54593) (#54623)
fix(selling): blanket order ordered qty recalculation on sales order status change (#54593)

(cherry picked from commit d68801e73a)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-04-29 06:47:55 +00:00
mergify[bot]
e24ab72c0d fix: copy project from first row to new rows (backport #53295) (#54620)
fix: copy project to new item row from parent

(cherry picked from commit 68cc518497)

Co-authored-by: ravibharathi656 <ravibharathi656@gmail.com>
2026-04-29 11:55:46 +05:30
Frappe PR Bot
4d5c665e22 chore(release): Bumped to Version 16.16.0
# [16.16.0](https://github.com/frappe/erpnext/compare/v16.15.1...v16.16.0) (2026-04-28)

### Bug Fixes

* **`get_stock_balance`:** validate inventory dimension fieldnames (backport [#54587](https://github.com/frappe/erpnext/issues/54587)) ([#54589](https://github.com/frappe/erpnext/issues/54589)) ([9f04fcc](9f04fcc190))
* add filter labels and required filters for financial report validation ([e6f0bb6](e6f0bb66e2))
* add party type for dynamic link support ([c6d4802](c6d4802857))
* always exclude pcv entries except for closing account head ([446c111](446c111653))
* avoid double reduction of pe reference outstanding (backport [#54193](https://github.com/frappe/erpnext/issues/54193)) ([#54613](https://github.com/frappe/erpnext/issues/54613)) ([5de4b01](5de4b013ea))
* correct display depends on condition ([#54556](https://github.com/frappe/erpnext/issues/54556)) ([0df38a8](0df38a841e))
* debit credit not equal in purchase transactions for multi currency (backport [#54456](https://github.com/frappe/erpnext/issues/54456)) ([#54564](https://github.com/frappe/erpnext/issues/54564)) ([d9a9a5b](d9a9a5bcde))
* delivery schedule in the sales order ([386f499](386f49978e))
* duplicate entries being shown in batch exists in future transact… (backport [#54604](https://github.com/frappe/erpnext/issues/54604)) ([#54606](https://github.com/frappe/erpnext/issues/54606)) ([1111771](11117710d3))
* **edi:** restrict Code List imports to files and trusted backend URLs (backport [#54137](https://github.com/frappe/erpnext/issues/54137)) ([#54266](https://github.com/frappe/erpnext/issues/54266)) ([2a244d1](2a244d162b)), closes [#54488](https://github.com/frappe/erpnext/issues/54488)
* ensure fiscal year is checked before validating date filters in financial statements ([fba7871](fba78711cc))
* ensure tax withholding entries respect date range of category ([719d982](719d982a07))
* filter opening entries in first year in custom financial statement ([6bd6e62](6bd6e62c8c))
* filter overdue purchase order items by company (backport [#54099](https://github.com/frappe/erpnext/issues/54099)) ([#54611](https://github.com/frappe/erpnext/issues/54611)) ([8f8bf13](8f8bf13b41))
* hide feature flag controlled fields on install ([45dc2c4](45dc2c40fd))
* make inv dimen reqd only in delivery note (backport [#54546](https://github.com/frappe/erpnext/issues/54546)) ([#54552](https://github.com/frappe/erpnext/issues/54552)) ([d56df96](d56df96f73))
* **manufacturing:** remove conversion factor for stock qty (backport [#54525](https://github.com/frappe/erpnext/issues/54525)) ([#54573](https://github.com/frappe/erpnext/issues/54573)) ([f14751d](f14751d538))
* negative quantity check in validate_item_qty (backport [#54559](https://github.com/frappe/erpnext/issues/54559)) ([#54572](https://github.com/frappe/erpnext/issues/54572)) ([f7fa394](f7fa394aea))
* **payment_entry:** escape arguments on invoice and order fetching sql queries (backport [#54582](https://github.com/frappe/erpnext/issues/54582)) ([#54586](https://github.com/frappe/erpnext/issues/54586)) ([5289aa0](5289aa0ab3))
* **PCV:** set correct filters of `from_date` and `to_date` on General Ledger Report on clicking `Ledger` button (backport [#54522](https://github.com/frappe/erpnext/issues/54522)) ([#54524](https://github.com/frappe/erpnext/issues/54524)) ([f3996fb](f3996fb971))
* preserve inventory dimensions when raw materials are reset (backport [#54440](https://github.com/frappe/erpnext/issues/54440)) ([#54493](https://github.com/frappe/erpnext/issues/54493)) ([456e99b](456e99b352))
* py error on stock ageing report (backport [#54467](https://github.com/frappe/erpnext/issues/54467)) ([#54469](https://github.com/frappe/erpnext/issues/54469)) ([090aab3](090aab33fb))
* skip BudgetValidation when cancelling GL entries ([1b14673](1b146738c4))
* **stock:** add stock entry in batch master connection ([62bbe28](62bbe28a72))
* **stock:** remove validation for transfer_qty field (backport [#54542](https://github.com/frappe/erpnext/issues/54542)) ([#54545](https://github.com/frappe/erpnext/issues/54545)) ([cc85370](cc85370d54))
* **stock:** set incoming rate as zero for outward sle (backport [#54514](https://github.com/frappe/erpnext/issues/54514)) ([#54533](https://github.com/frappe/erpnext/issues/54533)) ([cabea2f](cabea2f288))
* **stock:** show available qty in warehouse link field (backport [#54474](https://github.com/frappe/erpnext/issues/54474)) ([#54484](https://github.com/frappe/erpnext/issues/54484)) ([f7b87ed](f7b87ed0e3))
* **stock:** show item code in serial and batch selector dialog ([85d1eb8](85d1eb8379))
* summing of values could be zero even if values exist ([d51ce66](d51ce66cb2))
* update account identification to avoid using name_field in financial statements ([7b60ec8](7b60ec8457))
* update fiscal year filter to use mandatory_depends_on instead of reqd ([6570796](6570796fba))
* update status of quotation in patch (backport [#54577](https://github.com/frappe/erpnext/issues/54577)) ([#54580](https://github.com/frappe/erpnext/issues/54580)) ([134e4b7](134e4b7446))

### Features

* add setting to hide Subscription references across doctypes ([#54576](https://github.com/frappe/erpnext/issues/54576)) ([15b6633](15b6633fc3))
* Add XLSX styling support to custom financial report templates ([#52612](https://github.com/frappe/erpnext/issues/52612)) ([055ff56](055ff56ce4))
* Add XLSX styling support to custom financial report templates (backport [#52612](https://github.com/frappe/erpnext/issues/52612)) ([#54485](https://github.com/frappe/erpnext/issues/54485)) ([df3fbed](df3fbeded2))
* danish_bosnian_address_template (backport [#54093](https://github.com/frappe/erpnext/issues/54093)) ([#54516](https://github.com/frappe/erpnext/issues/54516)) ([5c0d2cb](5c0d2cb474))
* enhance account category with root type ([#53190](https://github.com/frappe/erpnext/issues/53190)) ([96bab08](96bab08ae0))
2026-04-28 21:03:29 +00:00
diptanilsaha
e09487d140 Merge pull request #54583 from frappe/version-16-hotfix 2026-04-29 02:31:54 +05:30
mergify[bot]
5de4b013ea fix: avoid double reduction of pe reference outstanding (backport #54193) (#54613)
* fix: avoid double reduction of pe reference outstanding (#54193)

Co-authored-by: diptanilsaha <diptanil@frappe.io>
(cherry picked from commit d1a80d40c4)

# Conflicts:
#	erpnext/accounts/utils.py

* chore: remove type hints for function parameters

---------

Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com>
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-28 20:41:42 +00:00
mergify[bot]
8f8bf13b41 fix: filter overdue purchase order items by company (backport #54099) (#54611)
Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com>
fix: filter overdue purchase order items by company (#54099)
2026-04-29 01:25:31 +05:30
mergify[bot]
11117710d3 fix: duplicate entries being shown in batch exists in future transact… (backport #54604) (#54606)
fix: duplicate entries being shown in batch exists in future transact… (#54604)

fix: duplicate entries being shown in batch exists in future transactions msg
(cherry picked from commit 54f20de7e3)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-28 22:23:13 +05:30
diptanilsaha
90b07b3db5 Merge branch 'version-16' into version-16-hotfix 2026-04-28 21:49:53 +05:30
Trusted Computer
0d498baa10 refactor: bring back titles on sales transactions and make them optional and visible on purchase transactions (backport #52633) (#54601)
* refactor: bring back titles on sales transactions and make them optional and visible on purchase transactions

* fix: update timestamp in json to UTC
2026-04-28 21:12:41 +05:30
mergify[bot]
deef1696d6 refactor(sms_center): replaced raw SQL queries with Query Builder (backport #54600) (#54603)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-28 15:41:59 +00:00
Lakshit Jain
c6ee18b4d4 Merge pull request #54599 from frappe/mergify/bp/version-16-hotfix/pr-54362
fix: filter opening entries after closing voucher (backport #54362)
2026-04-28 19:04:45 +05:30
Lakshit Jain
2f88fa6731 Merge pull request #54598 from frappe/mergify/bp/version-16-hotfix/pr-54517
fix: always exclude pcv entries except for closing account head (backport #54517)
2026-04-28 19:03:09 +05:30
Lakshit Jain
9c5c87b354 Merge pull request #54594 from frappe/mergify/bp/version-16-hotfix/pr-54479
fix:  Handle mandantory filters for financial statements report (backport #54479)
2026-04-28 18:45:05 +05:30
Smit Vora
64a724baea test: include both accounts to test sum = 0
(cherry picked from commit 590f2ffe28)
2026-04-28 13:13:46 +00:00
Smit Vora
7f32c3aca7 test: opening entries after period closing
(cherry picked from commit 5fc3ca1d4b)
2026-04-28 13:13:46 +00:00
Smit Vora
4675921077 chore: comment
(cherry picked from commit c94b8c41f3)
2026-04-28 13:13:46 +00:00
Smit Vora
d51ce66cb2 fix: summing of values could be zero even if values exist
(cherry picked from commit 7ae91cac01)
2026-04-28 13:13:45 +00:00
vorasmit
6bd6e62c8c fix: filter opening entries in first year in custom financial statement
(cherry picked from commit 3c8a066484)
2026-04-28 13:13:45 +00:00
Smit Vora
fa901946ce test: pcv is excluded from PL accounts
(cherry picked from commit 84aa54c540)
2026-04-28 13:13:18 +00:00
Smit Vora
446c111653 fix: always exclude pcv entries except for closing account head
(cherry picked from commit 0349e7a0b8)
2026-04-28 13:13:18 +00:00
Abdeali Chharchhoda
859b24dd95 chore: minor fix
(cherry picked from commit 3854d2cbf6)
2026-04-28 12:01:21 +00:00
Abdeali Chharchhoda
7b60ec8457 fix: update account identification to avoid using name_field in financial statements
(cherry picked from commit 1fd6c3ba1a)
2026-04-28 12:01:21 +00:00
Abdeali Chharchhoda
e6f0bb66e2 fix: add filter labels and required filters for financial report validation
(cherry picked from commit 4274c2aba3)
2026-04-28 12:01:21 +00:00
Abdeali Chharchhoda
6570796fba fix: update fiscal year filter to use mandatory_depends_on instead of reqd
(cherry picked from commit 79d6a51e1e)
2026-04-28 12:01:20 +00:00
Abdeali Chharchhoda
fba78711cc fix: ensure fiscal year is checked before validating date filters in financial statements
(cherry picked from commit 5a915cb45e)
2026-04-28 12:01:20 +00:00
mergify[bot]
9f04fcc190 fix(get_stock_balance): validate inventory dimension fieldnames (backport #54587) (#54589)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(`get_stock_balance`): validate inventory dimension fieldnames (#54587)
2026-04-28 11:35:16 +00:00
mergify[bot]
5289aa0ab3 fix(payment_entry): escape arguments on invoice and order fetching sql queries (backport #54582) (#54586)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(payment_entry): escape arguments on invoice and order fetching sql queries (#54582)
2026-04-28 10:48:41 +00:00
ruthra kumar
185ef4e273 Merge pull request #54553 from frappe/mergify/bp/version-16-hotfix/pr-54509
fix: hide feature flag controlled fields on install (backport #54509)
2026-04-28 16:04:42 +05:30
ruthra kumar
45dc2c40fd fix: hide feature flag controlled fields on install
(cherry picked from commit 889fdf2f11)
2026-04-28 15:44:28 +05:30
mergify[bot]
386a373c9b chore(sidebar): moved Inactive Customers from CRM to Selling Workspace Sidebar (backport #54578) (#54581)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-28 15:41:36 +05:30
mergify[bot]
134e4b7446 fix: update status of quotation in patch (backport #54577) (#54580)
fix: update status of quotation in patch (#54577)

(cherry picked from commit 2088a01c19)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-28 09:44:16 +00:00
Jatin3128
15b6633fc3 feat: add setting to hide Subscription references across doctypes (#54576) 2026-04-28 13:09:50 +05:30
mergify[bot]
f14751d538 fix(manufacturing): remove conversion factor for stock qty (backport #54525) (#54573)
fix(manufacturing): remove conversion factor for stock qty (#54525)

(cherry picked from commit 6f9089dd5b)

Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
2026-04-28 05:38:29 +00:00
mergify[bot]
f7fa394aea fix: negative quantity check in validate_item_qty (backport #54559) (#54572)
fix: negative quantity check in validate_item_qty (#54559)

Fix negative quantity check in validate_item_qty

When saving a Blanket Order with a blank qty field in the items table, the following error is raised:

TypeError: '<' not supported between instances of 'NoneType' and 'int'

Root cause: The validate_item_qty method compares d.qty < 0 directly. When the qty field is left empty, its value is None, and Python cannot compare None with an integer.

Fix
Wrap d.qty with flt(), which safely converts None (and any non-numeric value) to 0.0 before the comparison.

# Before
if d.qty < 0:

# After
if flt(d.qty) < 0:

(cherry picked from commit 63edd5ddc6)

Co-authored-by: Vinay Mishra <39999379+vinaymishraofficial@users.noreply.github.com>
2026-04-28 05:33:55 +00:00
mergify[bot]
d9a9a5bcde fix: debit credit not equal in purchase transactions for multi currency (backport #54456) (#54564)
fix: debit credit not equal in purchase transactions for multi currency (#54456)

(cherry picked from commit 601581d6f8)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-27 15:23:05 +00:00
Mihir Kandoi
0df38a841e fix: correct display depends on condition (#54556) 2026-04-27 10:08:47 +00:00
mergify[bot]
d56df96f73 fix: make inv dimen reqd only in delivery note (backport #54546) (#54552)
fix: make inv dimen reqd only in delivery note (#54546)

(cherry picked from commit 0aadd1e3a5)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-27 15:10:44 +05:30
mergify[bot]
cc85370d54 fix(stock): remove validation for transfer_qty field (backport #54542) (#54545)
fix(stock): remove validation for transfer_qty field (#54542)

(cherry picked from commit 60a6b38c31)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-04-27 07:20:16 +00:00
mergify[bot]
ac9aa7f154 refactor: quality inspection item query (backport #54511) (#54540)
* refactor: quality inspection item query (#54511)

(cherry picked from commit be2a4b7b2a)

# Conflicts:
#	erpnext/stock/doctype/quality_inspection/quality_inspection.py

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-27 05:50:37 +00:00
MochaMind
23cac0df83 chore: update POT file (#54535) 2026-04-26 18:55:31 +02:00
mergify[bot]
5c0d2cb474 feat: danish_bosnian_address_template (backport #54093) (#54516)
feat: danish_bosnian_address_template (#54093)

(cherry picked from commit e517eeaaa2)

Co-authored-by: mahsem <137205921+mahsem@users.noreply.github.com>
2026-04-26 21:06:52 +05:30
mergify[bot]
cabea2f288 fix(stock): set incoming rate as zero for outward sle (backport #54514) (#54533)
fix(stock): set incoming rate as zero for outward sle

(cherry picked from commit ce37530e70)

Co-authored-by: Sudharsanan11 <sudharsananashok1975@gmail.com>
2026-04-26 20:24:52 +05:30
rohitwaghchaure
4c95daaca5 Merge pull request #54531 from frappe/mergify/bp/version-16-hotfix/pr-54530
fix(stock): show item code in serial and batch selector dialog (backport #54530)
2026-04-26 10:25:51 +05:30
Sudharsanan11
62bbe28a72 fix(stock): add stock entry in batch master connection
(cherry picked from commit fee5bcadb2)
2026-04-26 04:35:42 +00:00
Sudharsanan11
85d1eb8379 fix(stock): show item code in serial and batch selector dialog
(cherry picked from commit f572bc51e1)
2026-04-26 04:35:42 +00:00
mergify[bot]
8de9ac4e34 refactor(UX): selling settings form (backport #54412) (#54527) 2026-04-25 18:01:07 +05:30
mergify[bot]
f3996fb971 fix(PCV): set correct filters of from_date and to_date on General Ledger Report on clicking Ledger button (backport #54522) (#54524)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(PCV): set correct filters of `from_date` and `to_date` on General Ledger Report on clicking `Ledger` button (#54522)
2026-04-25 00:08:27 +05:30
mergify[bot]
764c775e19 refactor: tax witholding report (backport #54449) (backport #54477) (#54519)
* refactor: use consistent report column names

(cherry picked from commit 6dca96b423)
(cherry picked from commit 9276cd7343)

* refactor: how data is built

(cherry picked from commit c3e7f7f02f)
(cherry picked from commit be0e58fb23)

* refactor: better label for entity type

(cherry picked from commit 53666974a3)
(cherry picked from commit fffaf834fd)

* refactor: updated key for withholding_date

(cherry picked from commit 07b023a934)
(cherry picked from commit e6cfdb8e4d)

* test: None is better than zero, as no values exist

(cherry picked from commit b5550f747e)
(cherry picked from commit 40466be9ef)

* refactor: make report extensible by regional apps

(cherry picked from commit f0ea20e579)
(cherry picked from commit 6392126ca5)

* fix: add party type for dynamic link support

(cherry picked from commit b925469c4d)
(cherry picked from commit c6d4802857)

---------

Co-authored-by: Smit Vora <smitvora203@gmail.com>
2026-04-24 14:39:42 +00:00
Frappe PR Bot
66ec6a4d20 chore(release): Bumped to Version 16.15.1
## [16.15.1](https://github.com/frappe/erpnext/compare/v16.15.0...v16.15.1) (2026-04-24)

### Bug Fixes

* preserve inventory dimensions when raw materials are reset (backport [#54440](https://github.com/frappe/erpnext/issues/54440)) (backport [#54493](https://github.com/frappe/erpnext/issues/54493)) ([#54513](https://github.com/frappe/erpnext/issues/54513)) ([610735d](610735d1c5))
2026-04-24 12:35:07 +00:00
mergify[bot]
610735d1c5 fix: preserve inventory dimensions when raw materials are reset (backport #54440) (backport #54493) (#54513)
* fix: preserve inventory dimensions when raw materials are reset (backport #54440) (#54493)

fix: preserve inventory dimensions when raw materials are reset (#54440)

* fix: preserve inventory dimensions when raw materials are reset

* test: add test case

(cherry picked from commit 0e20e35842)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
(cherry picked from commit 456e99b352)

# Conflicts:
#	erpnext/patches.txt

* chore: resolve conflicts

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-24 12:33:34 +00:00
Khushi Rawat
8a5fa64e1d Merge pull request #54504 from frappe/mergify/bp/version-16-hotfix/pr-53314
fix: skip budget validation when cancelling GL entries (backport #53314)
2026-04-24 17:39:05 +05:30
Smit Vora
aecf2c1c0e Merge pull request #54505 from frappe/mergify/bp/version-16-hotfix/pr-54476
fix: ensure tax withholding entries respect date range of category (backport #54476)
2026-04-24 13:39:54 +05:30
ljain112
719d982a07 fix: ensure tax withholding entries respect date range of category
(cherry picked from commit 9ead8d4e3f)
2026-04-24 07:48:53 +00:00
nareshkannasln
1b146738c4 fix: skip BudgetValidation when cancelling GL entries
(cherry picked from commit fa34ebea94)
2026-04-24 06:45:16 +00:00
mergify[bot]
c4010b0581 ci: fix timezone for python mariadb tests (backport #54464) (#54465)
ci: fix timezone for python mariadb tests (#54464)

(cherry picked from commit 0d2da6d86c)

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-23 22:31:16 +00:00
mergify[bot]
456e99b352 fix: preserve inventory dimensions when raw materials are reset (backport #54440) (#54493)
fix: preserve inventory dimensions when raw materials are reset (#54440)

* fix: preserve inventory dimensions when raw materials are reset

* test: add test case

(cherry picked from commit 0e20e35842)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-23 17:37:04 +00:00
mergify[bot]
2a244d162b fix(edi): restrict Code List imports to files and trusted backend URLs (backport #54137) (#54266)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix(edi): restrict Code List imports to files and trusted backend URLs (#54137)
fix(edi): hardcode "Code List" DocType in importer (#54488)
2026-04-23 15:36:35 +00:00
mergify[bot]
ddc9ea16cc ci: test correctness pattern (backport #54186) (#54473)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-04-23 13:29:15 +00:00
mergify[bot]
f7b87ed0e3 fix(stock): show available qty in warehouse link field (backport #54474) (#54484)
fix(stock): show available qty in warehouse link field (#54474)

(cherry picked from commit ab19b16fe2)

Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
2026-04-23 18:19:28 +05:30
Smit Vora
01b22254e7 Merge pull request #54486 from frappe/mergify/bp/version-16-hotfix/pr-53190 2026-04-23 17:54:28 +05:30
Smit Vora
df3fbeded2 feat: Add XLSX styling support to custom financial report templates (backport #52612) (#54485)
Co-authored-by: Abdeali Chharchhodawala <99460106+Abdeali099@users.noreply.github.com>
2026-04-23 17:50:52 +05:30
Abdeali Chharchhodawala
96bab08ae0 feat: enhance account category with root type (#53190)
(cherry picked from commit f6639db0e9)
2026-04-23 12:05:07 +00:00
Abdeali Chharchhodawala
055ff56ce4 feat: Add XLSX styling support to custom financial report templates (#52612)
(cherry picked from commit c35221852a)
2026-04-23 11:46:01 +00:00
Smit Vora
4f8184ec70 Merge pull request #54477 from frappe/mergify/bp/version-16-hotfix/pr-54449 2026-04-23 15:18:46 +05:30
Smit Vora
c6d4802857 fix: add party type for dynamic link support
(cherry picked from commit b925469c4d)
2026-04-23 09:27:53 +00:00
Smit Vora
6392126ca5 refactor: make report extensible by regional apps
(cherry picked from commit f0ea20e579)
2026-04-23 09:27:53 +00:00
Smit Vora
40466be9ef test: None is better than zero, as no values exist
(cherry picked from commit b5550f747e)
2026-04-23 09:27:53 +00:00
Smit Vora
e6cfdb8e4d refactor: updated key for withholding_date
(cherry picked from commit 07b023a934)
2026-04-23 09:27:53 +00:00
Smit Vora
fffaf834fd refactor: better label for entity type
(cherry picked from commit 53666974a3)
2026-04-23 09:27:52 +00:00
Smit Vora
be0e58fb23 refactor: how data is built
(cherry picked from commit c3e7f7f02f)
2026-04-23 09:27:52 +00:00
Smit Vora
9276cd7343 refactor: use consistent report column names
(cherry picked from commit 6dca96b423)
2026-04-23 09:27:52 +00:00
rohitwaghchaure
dd2763aabc Merge pull request #54472 from frappe/mergify/bp/version-16-hotfix/pr-54471
fix: delivery schedule in the sales order (backport #54471)
2026-04-22 22:11:01 +05:30
Rohit Waghchaure
386f49978e fix: delivery schedule in the sales order
(cherry picked from commit 435db260ee)
2026-04-22 16:33:12 +00:00
mergify[bot]
090aab33fb fix: py error on stock ageing report (backport #54467) (#54469)
fix: py error on stock ageing report (#54467)

(cherry picked from commit f5357c233d)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-22 14:41:13 +00:00
ruthra kumar
99bc2c174b Merge pull request #54463 from frappe/mergify/bp/version-16-hotfix/pr-54447
refactor(test): remove explicit sql delete calls (backport #54447)
2026-04-22 11:34:10 +05:30
ruthra kumar
91a748d9bf refactor(test): remove explicit sql delete calls
(cherry picked from commit b16dd3f2dd)
2026-04-22 05:43:48 +00:00
Frappe PR Bot
c99b9e1b64 chore(release): Bumped to Version 16.15.0
# [16.15.0](https://github.com/frappe/erpnext/compare/v16.14.0...v16.15.0) (2026-04-22)

### Bug Fixes

* **accounts:** fetch project name from payment entry to journal entry (backport [#54307](https://github.com/frappe/erpnext/issues/54307)) ([#54453](https://github.com/frappe/erpnext/issues/54453)) ([62a9a76](62a9a761b7))
* add portal user ownership check to supplier quotation (backport [#54298](https://github.com/frappe/erpnext/issues/54298)) ([#54300](https://github.com/frappe/erpnext/issues/54300)) ([d7da5b0](d7da5b047d))
* add project filter to accounts payable and receivable reports (backport [#54344](https://github.com/frappe/erpnext/issues/54344)) ([#54442](https://github.com/frappe/erpnext/issues/54442)) ([57cd2a0](57cd2a06e8))
* append row level user remarks in gl map ([aa359ad](aa359aded4))
* changed qty validation from qty field to stock_qty (backport [#54352](https://github.com/frappe/erpnext/issues/54352)) ([#54357](https://github.com/frappe/erpnext/issues/54357)) ([fa76e8a](fa76e8ac7f))
* clear conditions table when calculate_based_on is set to Fixed ([7849733](78497336c7))
* clear shipping rule conditions for fixed shipping rule ([319d769](319d769c6f))
* **dashboard-trends:** set default fiscal year and company before val… (backport [#54339](https://github.com/frappe/erpnext/issues/54339)) ([#54400](https://github.com/frappe/erpnext/issues/54400)) ([b1825c0](b1825c0cbe))
* default company perms for HR manager ([47abaf7](47abaf70b2))
* default perm for HR manager & HR user ([95213fb](95213fb9b8))
* default perm for HR manager & HR user ([a7b1fec](a7b1fec21d))
* default permission for HR manager role ([534891a](534891aac4))
* default permission for HR User role ([0d6d64f](0d6d64ff05))
* Disallow negative rates in Purchase invoice (backport [#54254](https://github.com/frappe/erpnext/issues/54254)) ([#54393](https://github.com/frappe/erpnext/issues/54393)) ([cac9073](cac907383b))
* dropship logic should come above non stock logic in gross profit… (backport [#54383](https://github.com/frappe/erpnext/issues/54383)) ([#54385](https://github.com/frappe/erpnext/issues/54385)) ([78aaf6c](78aaf6c7e8))
* fetch item tax template from item group when creating item (backport [#54258](https://github.com/frappe/erpnext/issues/54258)) ([#54368](https://github.com/frappe/erpnext/issues/54368)) ([3914d5d](3914d5d1b7))
* hide operations field in bom creator if phantom (backport [#54336](https://github.com/frappe/erpnext/issues/54336)) ([#54337](https://github.com/frappe/erpnext/issues/54337)) ([b252ad4](b252ad49b7))
* make Target Warehouse mandatory on UI ([46f5de0](46f5de0b1c))
* **manufacturing:** handle empty list in query builder ([d2cc549](d2cc549696))
* move make_dimension_in_accounting_doctypes from after_insert to on_update ([f287edd](f287edd8c2))
* negative batch report showing same batch-warehouse multiple times ([493f36b](493f36b3ce))
* non-collapsible in customer quick entry ([101f68c](101f68c8e8))
* **pos_invoice_item:** fetch `grant_commission` from `item_code` (backport [#54413](https://github.com/frappe/erpnext/issues/54413)) ([#54418](https://github.com/frappe/erpnext/issues/54418)) ([dd6d4d1](dd6d4d1910))
* **purchase_register:** filter tax rows by parenttype in invoice tax map query (backport [#54272](https://github.com/frappe/erpnext/issues/54272)) ([#54444](https://github.com/frappe/erpnext/issues/54444)) ([01aff64](01aff6492c))
* recalculate operating costs if workstation type is changed (backport [#54390](https://github.com/frappe/erpnext/issues/54390)) ([#54398](https://github.com/frappe/erpnext/issues/54398)) ([cfcba1f](cfcba1fcf2))
* remove unwanted perm for HR user role ([4940aeb](4940aeb712))
* reset base_rounded_total when rounded_total resets (backport [#54241](https://github.com/frappe/erpnext/issues/54241)) ([#54304](https://github.com/frappe/erpnext/issues/54304)) ([45052ce](45052ce8a7))
* resolve conflict ([9e6300b](9e6300bf76))
* sales order is not valid when creating WO from MR from PP (backport [#54435](https://github.com/frappe/erpnext/issues/54435)) ([#54436](https://github.com/frappe/erpnext/issues/54436)) ([5397b7d](5397b7da25))
* Table row in dialog should not have delete row option ([5916e57](5916e570af))
* **taxes_and_totals:** apply conversion_rate to taxable_amount in get_itemised_tax ([d506e57](d506e574d2))
* **test:** missing repost allowed defaults ([d49c343](d49c34389b))
* use qty instead of stock qty dropship gross profit report (backport [#54389](https://github.com/frappe/erpnext/issues/54389)) ([#54391](https://github.com/frappe/erpnext/issues/54391)) ([7556550](7556550158))
* validate south africa company in vat audit report (backport [#54030](https://github.com/frappe/erpnext/issues/54030)) ([#54394](https://github.com/frappe/erpnext/issues/54394)) ([aa2cba9](aa2cba9780))
* zero valuation rate popup on SI (backport [#54376](https://github.com/frappe/erpnext/issues/54376)) ([#54377](https://github.com/frappe/erpnext/issues/54377)) ([104eac2](104eac21e8))

### Features

* add option to create production plan from sales order (backport [#53662](https://github.com/frappe/erpnext/issues/53662)) ([#54323](https://github.com/frappe/erpnext/issues/54323)) ([b487f69](b487f69b59))
* add support for 'not applicable' tax in item tax templates ([#50898](https://github.com/frappe/erpnext/issues/50898)) ([52a4ca9](52a4ca9c41))
* backflush based on in BOM ([2c73e37](2c73e37f80))
* make fg phantom-able in bom creator (backport [#54332](https://github.com/frappe/erpnext/issues/54332)) ([#54333](https://github.com/frappe/erpnext/issues/54333)) ([10dbfd3](10dbfd310f))
* use single remark field with custom remark toggle ([27c5dab](27c5dab7e4))
2026-04-22 00:21:02 +00:00
diptanilsaha
a9747213f5 Merge pull request #54437 from frappe/version-16-hotfix 2026-04-22 05:49:31 +05:30
mergify[bot]
5923618df3 refactor(test): move contact and address creation to bootstrap (backport #54406) (#54410)
Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2026-04-22 00:24:36 +05:30
MochaMind
193a44f298 chore: update POT file (#54401) 2026-04-22 00:16:52 +05:30
Khushi Rawat
d9731d7c72 Merge pull request #54427 from frappe/mergify/bp/version-16-hotfix/pr-54131
feat: use single remark field with custom remark toggle (backport #54131)
2026-04-22 00:05:24 +05:30
mergify[bot]
457adcee95 refactor: fix test cases in tax withholding details report (backport #54422) (#54445)
Co-authored-by: ljain112 <ljain112@gmail.com>
2026-04-21 23:58:57 +05:30
mergify[bot]
01aff6492c fix(purchase_register): filter tax rows by parenttype in invoice tax map query (backport #54272) (#54444)
fix(purchase_register): filter tax rows by parenttype in invoice tax map query

(cherry picked from commit 3aeb7d6b01)

Co-authored-by: ljain112 <ljain112@gmail.com>
2026-04-21 23:58:25 +05:30
mergify[bot]
57cd2a06e8 fix: add project filter to accounts payable and receivable reports (backport #54344) (#54442)
Co-authored-by: ljain112 <ljain112@gmail.com>
2026-04-21 23:56:35 +05:30
mergify[bot]
62a9a761b7 fix(accounts): fetch project name from payment entry to journal entry (backport #54307) (#54453)
Co-authored-by: sarathibalamurugan <sarathigreen4@gmail.com>
2026-04-21 23:54:56 +05:30
Khushi Rawat
9e6300bf76 fix: resolve conflict 2026-04-21 16:15:21 +05:30
mergify[bot]
5397b7da25 fix: sales order is not valid when creating WO from MR from PP (backport #54435) (#54436)
fix: sales order is not valid when creating WO from MR from PP (#54435)

(cherry picked from commit e65b9fc2ae)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-21 15:44:26 +05:30
mergify[bot]
37d080bdb4 refactor: Sales Partner Commission Summary and Sales Partner Transaction Summary report (backport #54268) (#54431)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-21 09:21:10 +05:30
khushi8112
aa359aded4 fix: append row level user remarks in gl map
(cherry picked from commit 84e5272f5d)
2026-04-20 19:16:59 +00:00
khushi8112
27c5dab7e4 feat: use single remark field with custom remark toggle
(cherry picked from commit 697f521e14)

# Conflicts:
#	erpnext/accounts/doctype/journal_entry/journal_entry.json
2026-04-20 19:16:58 +00:00
Ravibharathi
4e05277695 Merge pull request #54424 from frappe/mergify/bp/version-16-hotfix/pr-54415
fix: clear conditions table when calculate_based_on is set to Fixed (backport #54415)
2026-04-20 19:44:19 +05:30
ravibharathi656
319d769c6f fix: clear shipping rule conditions for fixed shipping rule
(cherry picked from commit d6bb0ae093)
2026-04-20 13:53:25 +00:00
sarathibalamurugan
78497336c7 fix: clear conditions table when calculate_based_on is set to Fixed
(cherry picked from commit d73920be12)
2026-04-20 13:53:25 +00:00
rohitwaghchaure
d5ea039e07 Merge pull request #54414 from frappe/mergify/bp/version-16-hotfix/pr-54350
feat: backflush based on in BOM (backport #54350)
2026-04-20 17:57:33 +05:30
mergify[bot]
4bb30a9157 test(BootStrapTestData): create sales_partner test data while bootstrapping (backport #54416) (#54421)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-04-20 12:03:23 +00:00
mergify[bot]
dd6d4d1910 fix(pos_invoice_item): fetch grant_commission from item_code (backport #54413) (#54418)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(pos_invoice_item): fetch `grant_commission` from `item_code` (#54413)
2026-04-20 11:47:15 +00:00
Rohit Waghchaure
2c73e37f80 feat: backflush based on in BOM
(cherry picked from commit 877d99c5a5)
2026-04-20 11:07:19 +00:00
Raheel Khan
3bee79b90d Merge pull request #54407 from frappe/mergify/bp/version-16-hotfix/pr-53756
fix(hrms): default permission for HR roles (backport #53756)
2026-04-20 12:21:31 +05:30
iamkhanraheel
47abaf70b2 fix: default company perms for HR manager
(cherry picked from commit 2018a90ad8)
2026-04-20 05:50:43 +00:00
iamkhanraheel
4940aeb712 fix: remove unwanted perm for HR user role
(cherry picked from commit d26cd69fe5)
2026-04-20 05:50:42 +00:00
iamkhanraheel
95213fb9b8 fix: default perm for HR manager & HR user
(cherry picked from commit 41103a0622)
2026-04-20 05:50:42 +00:00
iamkhanraheel
a7b1fec21d fix: default perm for HR manager & HR user
(cherry picked from commit f02b3b6166)
2026-04-20 05:50:42 +00:00
iamkhanraheel
534891aac4 fix: default permission for HR manager role
(cherry picked from commit 5ec66169a7)
2026-04-20 05:50:42 +00:00
iamkhanraheel
0d6d64ff05 fix: default permission for HR User role
(cherry picked from commit 7b0bfe76cc)
2026-04-20 05:50:41 +00:00
mergify[bot]
fa76e8ac7f fix: changed qty validation from qty field to stock_qty (backport #54352) (#54357)
fix: changed qty validation from qty field to stock_qty (#54352)

(cherry picked from commit ba01d66c24)

Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
2026-04-20 10:54:04 +05:30
ruthra kumar
abed348121 Merge pull request #54366 from frappe/mergify/bp/version-16-hotfix/pr-50898
feat: add support for 'not applicable' tax in item tax templates (backport #50898)
2026-04-20 10:17:00 +05:30
mergify[bot]
b1825c0cbe fix(dashboard-trends): set default fiscal year and company before val… (backport #54339) (#54400)
* fix(dashboard-trends): set default fiscal year and company before val… (#54339)

* fix(dashboard-trends): set default fiscal year and company before validating filters Ensure  and  are populated with default values

* fix(dashboard-trends): ensure fiscal_year and company are properly set before validation to avoid empty filter issues

* Update erpnext/controllers/trends.py

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
(cherry picked from commit d61b5fd5f6)

# Conflicts:
#	erpnext/controllers/trends.py

* chore: fix conflicts

---------

Co-authored-by: Ahmed AbuKhatwa <82771130+AhmedAbokhatwa@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-19 09:52:45 +00:00
mergify[bot]
cfcba1fcf2 fix: recalculate operating costs if workstation type is changed (backport #54390) (#54398)
fix: recalculate operating costs if workstation type is changed (#54390)

* fix: recalculate operating costs if workstation type is changed

* fix: do not overwrite op costs on every save

(cherry picked from commit 28f3429a54)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-19 08:06:09 +00:00
mergify[bot]
d542a72da5 Fix : None handling in pricing rule free item quantity calculation (backport #54375) (#54396)
Fix : None handling in pricing rule free item quantity calculation (#54375)

* fix(pricing_rule): handle None qty in transaction_qty calculation

* Update erpnext/accounts/doctype/pricing_rule/utils.py

---------



(cherry picked from commit 82438d6c72)

Co-authored-by: Jaganath-Tridots <jaganath@tridotstech.com>
Co-authored-by: Jagan <jagan@DESKTOP-HPDMQ06.localdomain>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-19 08:02:03 +00:00
mergify[bot]
aa2cba9780 fix: validate south africa company in vat audit report (backport #54030) (#54394)
fix: validate south africa company in vat audit report (#54030)

* fix: validate south africa company in vat audit report

* fix: use qb to get invoice data

* fix: validate company region in south africa vat settings

(cherry picked from commit 1c65cc1088)

Co-authored-by: Ravibharathi <131471282+ravibharathi656@users.noreply.github.com>
2026-04-19 07:57:13 +00:00
mergify[bot]
cac907383b fix: Disallow negative rates in Purchase invoice (backport #54254) (#54393)
fix: Disallow negative rates in Purchase invoice (#54254)

(cherry picked from commit 23768ae0a5)

Co-authored-by: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com>
2026-04-19 07:46:21 +00:00
mergify[bot]
7556550158 fix: use qty instead of stock qty dropship gross profit report (backport #54389) (#54391)
fix: use qty instead of stock qty dropship gross profit report (#54389)

(cherry picked from commit d6b379b936)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-19 12:52:15 +05:30
mergify[bot]
78aaf6c7e8 fix: dropship logic should come above non stock logic in gross profit… (backport #54383) (#54385)
fix: dropship logic should come above non stock logic in gross profit… (#54383)

fix: dropship logic should come above non stock logic in gross profit report
(cherry picked from commit 40bcaa7bc3)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-18 16:37:43 +00:00
mergify[bot]
104eac21e8 fix: zero valuation rate popup on SI (backport #54376) (#54377)
fix: zero valuation rate popup on SI (#54376)

(cherry picked from commit 3ef6c24f07)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-18 12:05:36 +00:00
mergify[bot]
3914d5d1b7 fix: fetch item tax template from item group when creating item (backport #54258) (#54368)
fix: fetch item tax template from item group when creating item (#54258)

(cherry picked from commit b93f2350ee)

Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
2026-04-18 06:49:44 +00:00
Lakshit Jain
52a4ca9c41 feat: add support for 'not applicable' tax in item tax templates (#50898)
* feat: add support for 'not applicable' tax in item tax templates

* refactor: remove unused imports

* fix: import NOT_APPLICABLE_TAX in get_item_tax_map function

* fix: add item wise tax details for not applicable taxes

* test: added test case for `not_applicable`

* fix: do not create item wise tax details for not applicable tax

* fix: ensure tax rate is set to 0 for not applicable tax rows

* refactor: changes as per review

* test: update selling settings

* test: correct settings

* fix: return both net and current tax amounts for not applicable tax

(cherry picked from commit 453fe376ab)
2026-04-18 06:05:36 +00:00
rohitwaghchaure
de66fd0c58 Merge pull request #54361 from frappe/mergify/bp/version-16-hotfix/pr-54355
fix(manufacturing): handle empty list in query builder (backport #54355)
2026-04-17 21:43:14 +05:30
rohitwaghchaure
450b4c2f5f Merge pull request #54360 from frappe/mergify/bp/version-16-hotfix/pr-54354
fix: negative batch report showing same batch-warehouse multiple times (backport #54354)
2026-04-17 21:32:28 +05:30
Pandiyan37
d2cc549696 fix(manufacturing): handle empty list in query builder
(cherry picked from commit 9e5d94c1e6)
2026-04-17 15:52:18 +00:00
Rohit Waghchaure
493f36b3ce fix: negative batch report showing same batch-warehouse multiple times
(cherry picked from commit 700572980d)
2026-04-17 15:42:03 +00:00
Nishka Gosalia
cd605d35c5 Merge pull request #54348 from frappe/mergify/bp/version-16-hotfix/pr-54074
fix: Table row in dialog should not have delete row option (backport #54074)
2026-04-17 17:07:40 +05:30
nishkagosalia
5916e570af fix: Table row in dialog should not have delete row option
(cherry picked from commit eb89903dec)
2026-04-17 10:40:26 +00:00
Nishka Gosalia
4d300f7d34 Merge pull request #54347 from frappe/mergify/bp/version-16-hotfix/pr-54345 2026-04-17 16:08:41 +05:30
nishkagosalia
36cc39ddc6 refactor(UX): Batch Form Cleanup
(cherry picked from commit de747fe625)
2026-04-17 09:44:19 +00:00
rohitwaghchaure
afd25508d6 Merge pull request #54343 from frappe/mergify/bp/version-16-hotfix/pr-54342
fix: make Target Warehouse mandatory on UI for WO (backport #54342)
2026-04-17 13:27:35 +05:30
Rohit Waghchaure
46f5de0b1c fix: make Target Warehouse mandatory on UI
(cherry picked from commit 2a8267e10a)
2026-04-17 07:54:10 +00:00
mergify[bot]
b252ad49b7 fix: hide operations field in bom creator if phantom (backport #54336) (#54337) 2026-04-16 16:11:45 +00:00
mergify[bot]
10dbfd310f feat: make fg phantom-able in bom creator (backport #54332) (#54333) 2026-04-16 19:17:51 +05:30
ruthra kumar
07bcaab33b Merge pull request #54328 from frappe/mergify/bp/version-16-hotfix/pr-54327
fix(test): missing repost allowed defaults (backport #54327)
2026-04-16 17:20:04 +05:30
ruthra kumar
9d969d5af5 Merge pull request #54321 from frappe/mergify/bp/version-16-hotfix/pr-54301
refactor(ux): merge repost settings to accounts settings (backport #54301)
2026-04-16 17:19:48 +05:30
ruthra kumar
2e7c4776d4 refactor: delete redundent repost setting
(cherry picked from commit 6a04c159ca)
2026-04-16 16:55:38 +05:30
ruthra kumar
5a2933df8f refactor: limit reposting to only supported doctypes
(cherry picked from commit 940d3cfe0a)
2026-04-16 16:55:38 +05:30
ruthra kumar
fa5e4dee17 refactor: remove redundant field from filter
(cherry picked from commit ece85c770f)
2026-04-16 16:55:38 +05:30
ruthra kumar
3ba400a02b refactor(ux): better error message
(cherry picked from commit 3093409933)
2026-04-16 16:55:38 +05:30
ruthra kumar
5c064331cb refactor(test): use new source for repost setting
(cherry picked from commit b8207d5ed1)
2026-04-16 16:55:38 +05:30
ruthra kumar
151864079b refactor: move allowed doctypes to accounts settings
- dropped 'allowed' field

(cherry picked from commit d5c58277cb)
2026-04-16 16:55:37 +05:30
ruthra kumar
f785f36ad6 refactor: merge reposting settings to accounts settings
(cherry picked from commit 89ebf48544)
2026-04-16 16:55:26 +05:30
ruthra kumar
d49c34389b fix(test): missing repost allowed defaults
(cherry picked from commit 257865deb2)
2026-04-16 10:33:04 +00:00
mergify[bot]
ead7744f81 refactor: add category field to uom (backport #54290) (#54325)
* refactor: add category field to uom (#54290)

(cherry picked from commit e04a2e6da2)

# Conflicts:
#	erpnext/patches.txt

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-16 10:16:28 +00:00
mergify[bot]
b487f69b59 feat: add option to create production plan from sales order (backport #53662) (#54323)
Co-authored-by: sudarsan2001 <frankel9675@gmail.com>
Co-authored-by: Venkatesh <47534423+venkat102@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-04-16 15:16:34 +05:30
ruthra kumar
d74e632934 Merge pull request #54318 from frappe/mergify/bp/version-16-hotfix/pr-54172
fix: move make_dimension_in_accounting_doctypes from after_insert to on_update (backport #54172)
2026-04-16 11:29:50 +05:30
ruthra kumar
29ba701432 Merge pull request #54316 from frappe/mergify/bp/version-16-hotfix/pr-52923
fix(taxes_and_totals): apply conversion_rate to taxable_amount in get_itemised_tax (backport #52923)
2026-04-16 11:12:15 +05:30
Shllokkk
f287edd8c2 fix: move make_dimension_in_accounting_doctypes from after_insert to on_update
(cherry picked from commit ee067e6015)
2026-04-16 05:37:36 +00:00
Dharanidharan2813
d506e574d2 fix(taxes_and_totals): apply conversion_rate to taxable_amount in get_itemised_tax
(cherry picked from commit 2e577ed25b)
2026-04-16 05:22:40 +00:00
mergify[bot]
45052ce8a7 fix: reset base_rounded_total when rounded_total resets (backport #54241) (#54304)
* fix: reset base_rounded_total when rounded_total resets

(cherry picked from commit f8d278b733)

# Conflicts:
#	erpnext/controllers/tests/test_taxes_and_totals.py

* chore: spelling mistake

(cherry picked from commit e2ac476587)

* chore: resolve conflicts

---------

Co-authored-by: ljain112 <ljain112@gmail.com>
2026-04-16 10:39:17 +05:30
NaviN
4f9f90738a Merge pull request #54309 from frappe/mergify/bp/version-16-hotfix/pr-54306
fix: non-collapsible in customer quick entry (backport #54306)
2026-04-15 17:32:56 +05:30
PKSowmiya05
101f68c8e8 fix: non-collapsible in customer quick entry
(cherry picked from commit 53e120269d)
2026-04-15 11:56:54 +00:00
ruthra kumar
ffebb86846 refactor(company): don't force set service expense account on save (backport #54275) (#54305)
* refactor(company): don't force set service expense account on save

(cherry picked from commit 927f40b296)

* refactor(test): set dependant value in company master

(cherry picked from commit 299e141cee)

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2026-04-15 15:55:53 +05:30
ruthra kumar
6cc560a579 refactor(test): set dependant value in company master
(cherry picked from commit 299e141cee)
2026-04-15 10:04:00 +00:00
ruthra kumar
47e78bd4b9 refactor(company): don't force set service expense account on save
(cherry picked from commit 927f40b296)
2026-04-15 10:03:59 +00:00
mergify[bot]
d7da5b047d fix: add portal user ownership check to supplier quotation (backport #54298) (#54300)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: add portal user ownership check to supplier quotation (#54298)
2026-04-15 06:13:11 +00:00
241 changed files with 8785 additions and 4217 deletions

View File

@@ -41,6 +41,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
env:
TZ: 'Asia/Kolkata'
NODE_ENV: "production"
WITH_COVERAGE: ${{ github.event_name != 'pull_request' }}
@@ -56,6 +57,7 @@ jobs:
mysql:
image: mariadb:10.6
env:
TZ: 'Asia/Kolkata'
MARIADB_ROOT_PASSWORD: 'root'
ports:
- 3306:3306

View File

@@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document
from frappe.utils.user import is_website_user
__version__ = "16.14.0"
__version__ = "16.17.0"
def get_default_company(user=None):

View File

@@ -5,8 +5,7 @@ frappe.ui.form.on("Account", {
setup: function (frm) {
frm.add_fetch("parent_account", "report_type", "report_type");
frm.add_fetch("parent_account", "root_type", "root_type");
},
onload: function (frm) {
frm.set_query("parent_account", function (doc) {
return {
filters: {
@@ -15,7 +14,18 @@ frappe.ui.form.on("Account", {
},
};
});
frm.set_query("account_category", function () {
if (!frm.doc.root_type) return;
return {
filters: {
root_type: ["in", [frm.doc.root_type, ""]],
},
};
});
},
refresh: function (frm) {
frm.toggle_display("account_name", frm.is_new());
@@ -58,12 +68,20 @@ frappe.ui.form.on("Account", {
}
}
},
account_type: function (frm) {
if (frm.doc.is_group == 0) {
frm.toggle_display(["tax_rate"], frm.doc.account_type == "Tax");
frm.toggle_display("warehouse", frm.doc.account_type == "Stock");
}
},
root_type: function (frm) {
if (frm.doc.account_category) {
frm.set_value("account_category", "");
}
},
add_toolbar_buttons: function (frm) {
frm.add_custom_button(
__("Chart of Accounts"),

View File

@@ -203,7 +203,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2025-08-02 06:26:44.657146",
"modified": "2026-04-14 18:14:42.202065",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account",
@@ -256,6 +256,14 @@
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"role": "HR User",
"select": 1
},
{
"role": "HR Manager",
"select": 1
}
],
"row_format": "Dynamic",

View File

@@ -34,6 +34,13 @@
"account_number": "0430",
"account_type": "Fixed Asset"
},
"Anlagen im Bau": {
"is_group": 1,
"Andere Anlagen, Betriebs- und Geschäftsausstattung im Bau": {
"account_number": "0498",
"account_type": "Capital Work in Progress"
}
},
"Accumulated Depreciation": {
"account_type": "Accumulated Depreciation"
}
@@ -317,13 +324,21 @@
"account_number": "3800",
"account_type": "Expenses Included In Asset Valuation"
},
"Bestandsveränderungen Roh-, Hilfs- und Betriebsstoffe sowie bezogene Waren": {
"account_number": "3960",
"account_type": "Stock Adjustment"
},
"Herstellungskosten": {
"account_number": "4996",
"account_type": "Cost of Goods Sold"
},
"Anlagenabgänge Sachanlagen (Restbuchwert bei Buchverlust)": {
"account_number": "2310",
"account_type": "Expense Account"
},
"Verluste aus dem Abgang von Gegenständen des Anlagevermögens": {
"account_number": "2320",
"account_type": "Stock Adjustment"
"account_type": "Expense Account"
},
"Verwaltungskosten": {
"account_number": "4997",
@@ -340,7 +355,7 @@
"is_group": 1,
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
"account_number": "4830",
"account_type": "Accumulated Depreciation"
"account_type": "Depreciation"
},
"Abschreibungen auf Gebäude": {
"account_number": "4831",

View File

@@ -321,72 +321,6 @@ class TestAccount(ERPNextTestSuite):
self.assertEqual(balance, 0)
def _make_test_records(verbose=None):
from frappe.tests.utils import make_test_objects
accounts = [
# [account_name, parent_account, is_group]
["_Test Bank", "Bank Accounts", 0, "Bank", None],
["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"],
["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"],
["_Test Cash", "Cash In Hand", 0, "Cash", None],
["_Test Account Stock Expenses", "Direct Expenses", 1, None, None],
["_Test Account Shipping Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
["_Test Account Customs Duty", "_Test Account Stock Expenses", 0, "Tax", None],
["_Test Account Insurance Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
["_Test Account Stock Adjustment", "_Test Account Stock Expenses", 0, "Stock Adjustment", None],
["_Test Employee Advance", "Current Liabilities", 0, None, None],
["_Test Account Tax Assets", "Current Assets", 1, None, None],
["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Service Tax", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Reserves and Surplus", "Current Liabilities", 0, None, None],
["_Test Account Cost for Goods Sold", "Expenses", 0, None, None],
["_Test Account Excise Duty", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account S&H Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account CST", "Direct Expenses", 0, "Tax", None],
["_Test Account Discount", "Direct Expenses", 0, None, None],
["_Test Write Off", "Indirect Expenses", 0, None, None],
["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None],
["_Test Account Sales", "Direct Income", 0, None, None],
# related to Account Inventory Integration
["_Test Account Stock In Hand", "Current Assets", 0, None, None],
# fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, "Depreciation", None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
# Receivable / Payable Account
["_Test Receivable", "Current Assets", 0, "Receivable", None],
["_Test Payable", "Current Liabilities", 0, "Payable", None],
["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"],
["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"],
]
for company, abbr in [
["_Test Company", "_TC"],
["_Test Company 1", "_TC1"],
["_Test Company with perpetual inventory", "TCP1"],
]:
test_objects = make_test_objects(
"Account",
[
{
"doctype": "Account",
"account_name": account_name,
"parent_account": parent_account + " - " + abbr,
"company": company,
"is_group": is_group,
"account_type": account_type,
"account_currency": currency,
}
for account_name, parent_account, is_group, account_type, currency in accounts
],
)
return test_objects
def get_inventory_account(company, warehouse=None):
account = None
if warehouse:

View File

@@ -7,6 +7,8 @@
"engine": "InnoDB",
"field_order": [
"account_category_name",
"root_type",
"column_break_qluu",
"description"
],
"fields": [
@@ -14,6 +16,7 @@
"fieldname": "account_category_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Account Category Name",
"reqd": 1,
"unique": 1
@@ -22,6 +25,18 @@
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "column_break_qluu",
"fieldtype": "Column Break"
},
{
"fieldname": "root_type",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Root Type",
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity"
}
],
"grid_page_length": 50,
@@ -32,7 +47,7 @@
"link_fieldname": "account_category"
}
],
"modified": "2026-02-23 01:19:49.589393",
"modified": "2026-03-05 06:49:34.430723",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account Category",
@@ -69,7 +84,7 @@
}
],
"row_format": "Dynamic",
"search_fields": "account_category_name, description",
"search_fields": "account_category_name, root_type",
"sort_field": "creation",
"sort_order": "DESC",
"states": []

View File

@@ -21,6 +21,7 @@ class AccountCategory(Document):
account_category_name: DF.Data
description: DF.SmallText | None
root_type: DF.Literal["", "Asset", "Liability", "Income", "Expense", "Equity"]
# end: auto-generated types
def after_rename(self, old_name, new_name, merge):

View File

@@ -82,7 +82,7 @@ class AccountingDimension(Document):
else:
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
def after_insert(self):
def on_update(self):
if frappe.in_test:
make_dimension_in_accounting_doctypes(doc=self)
else:

View File

@@ -2,7 +2,15 @@
// For license information, please see license.txt
frappe.ui.form.on("Accounts Settings", {
refresh: function (frm) {},
refresh: function (frm) {
frm.set_query("document_type", "repost_allowed_types", function (doc, cdt, cdn) {
return {
filters: {
name: ["in", frappe.boot.sysdefaults.repost_allowed_doctypes],
},
};
});
},
enable_immutable_ledger: function (frm) {
if (!frm.doc.enable_immutable_ledger) {
return;

View File

@@ -16,6 +16,7 @@
"invoicing_features_section",
"check_supplier_invoice_uniqueness",
"automatically_fetch_payment_terms",
"enable_subscription",
"column_break_17",
"enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account",
@@ -62,6 +63,8 @@
"reconciliation_queue_size",
"column_break_resa",
"exchange_gain_loss_posting_date",
"repost_section",
"repost_allowed_types",
"payment_options_section",
"enable_loyalty_point_program",
"column_break_ctam",
@@ -702,6 +705,17 @@
"fieldname": "fetch_payment_schedule_in_payment_request",
"fieldtype": "Check",
"label": "Fetch Payment Schedule In Payment Request"
},
{
"fieldname": "repost_section",
"fieldtype": "Section Break",
"label": "Repost"
},
{
"fieldname": "repost_allowed_types",
"fieldtype": "Table",
"label": "Allowed Doctypes",
"options": "Repost Allowed Types"
}
],
"grid_page_length": 50,
@@ -711,7 +725,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-03-30 07:32:58.182018",
"modified": "2026-04-13 15:30:28.729627",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -10,6 +10,9 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
from frappe.model.document import Document
from frappe.utils import cint
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.utils import sync_auto_reconcile_config
SELLING_DOCTYPES = [
@@ -44,6 +47,8 @@ class AccountsSettings(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes
add_taxes_from_item_tax_template: DF.Check
add_taxes_from_taxes_and_charges_template: DF.Check
allow_multi_currency_invoices_against_single_party_account: DF.Check
@@ -72,6 +77,7 @@ class AccountsSettings(Document):
enable_immutable_ledger: DF.Check
enable_loyalty_point_program: DF.Check
enable_party_matching: DF.Check
enable_subscription: DF.Check
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
fetch_payment_schedule_in_payment_request: DF.Check
fetch_valuation_rate_for_internal_transaction: DF.Check
@@ -86,6 +92,7 @@ class AccountsSettings(Document):
receivable_payable_fetch_method: DF.Literal["Buffered Cursor", "UnBuffered Cursor", "Raw SQL"]
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
repost_allowed_types: DF.Table[RepostAllowedTypes]
role_allowed_to_over_bill: DF.Link | None
role_to_notify_on_depreciation_failure: DF.Link | None
role_to_override_stop_action: DF.Link | None
@@ -136,10 +143,15 @@ class AccountsSettings(Document):
toggle_loyalty_point_program_section(not self.enable_loyalty_point_program)
clear_cache = True
if old_doc.enable_subscription != self.enable_subscription:
toggle_subscription_sections(not self.enable_subscription)
clear_cache = True
if clear_cache:
frappe.clear_cache()
self.validate_and_sync_auto_reconcile_config()
self.update_property_for_accounting_dimension()
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
@@ -186,6 +198,17 @@ class AccountsSettings(Document):
title=_("Auto Tax Settings Error"),
)
def update_property_for_accounting_dimension(self):
doctypes = [entry.document_type for entry in self.repost_allowed_types]
if not doctypes:
return
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import get_child_docs
doctypes += get_child_docs(doctypes)
set_allow_on_submit_for_dimension_fields(doctypes)
@frappe.whitelist()
def drop_ar_sql_procedures(self):
from erpnext.accounts.report.accounts_receivable.accounts_receivable import InitSQLProceduresForAR
@@ -216,6 +239,12 @@ def toggle_loyalty_point_program_section(hide):
create_property_setter_for_hiding_field(doctype, "loyalty_points_redemption", hide)
def toggle_subscription_sections(hide):
subscription_doctypes = frappe.get_hooks("subscription_doctypes")
for doctype in subscription_doctypes:
create_property_setter_for_hiding_field(doctype, "subscription_section", hide)
def create_property_setter_for_hiding_field(doctype, field_name, hide):
make_property_setter(
doctype,
@@ -225,3 +254,12 @@ def create_property_setter_for_hiding_field(doctype, field_name, hide):
"Check",
validate_fields_for_doctype=False,
)
def set_allow_on_submit_for_dimension_fields(doctypes):
for dt in doctypes:
meta = frappe.get_meta(dt)
for dimension in get_accounting_dimensions():
df = meta.get_field(dimension)
if df and not df.allow_on_submit:
frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1)

View File

@@ -126,7 +126,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2025-01-22 10:46:42.904001",
"modified": "2026-04-14 18:15:27.367298",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cost Center",
@@ -173,11 +173,20 @@
"role": "Employee",
"select": 1,
"share": 1
},
{
"role": "HR User",
"select": 1
},
{
"role": "HR Manager",
"select": 1
}
],
"row_format": "Dynamic",
"search_fields": "parent_cost_center, is_group",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "ASC",
"states": []
}
}

View File

@@ -6,7 +6,7 @@ import json
import math
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from functools import reduce
from functools import cache, reduce
from typing import Any, Union
import frappe
@@ -15,6 +15,7 @@ from frappe.database.operator_map import OPERATOR_MAP
from frappe.query_builder import Case
from frappe.query_builder.functions import Sum
from frappe.utils import cstr, date_diff, flt, getdate
from frappe.utils.xlsxutils import XLSXMetadata, XLSXStyleBuilder
from pypika.terms import Bracket, LiteralValue
from erpnext import get_company_currency
@@ -38,6 +39,9 @@ from erpnext.accounts.report.financial_statements import (
)
from erpnext.accounts.utils import get_children, get_currency_precision
DEFAULT_BULLET_PREFIX = ""
SEGMENT_PREFIX = "seg_"
# ============================================================================
# DATA MODELS
# ============================================================================
@@ -141,7 +145,7 @@ class SegmentData:
@property
def id(self) -> str:
return f"seg_{self.index}"
return f"{SEGMENT_PREFIX}{self.index}"
@dataclass
@@ -222,14 +226,38 @@ class FinancialReportEngine:
return context.get_result()
def _validate_filters(self, filters: dict[str, Any]) -> None:
required_filters = ["report_template", "period_start_date", "period_end_date"]
filter_labels = {
"report_template": _("Report Template"),
"filter_based_on": _("Filter Based On"),
"period_start_date": _("Start Date"),
"period_end_date": _("End Date"),
"from_fiscal_year": _("Start Year"),
"to_fiscal_year": _("End Year"),
}
required_filters_by_basis = {
"Date Range": ("period_start_date", "period_end_date"),
"Fiscal Year": ("from_fiscal_year", "to_fiscal_year"),
}
required_filters = ["report_template", "filter_based_on"]
required_filters.extend(required_filters_by_basis.get(filters.get("filter_based_on"), ()))
for filter_key in required_filters:
if not filters.get(filter_key):
frappe.throw(_("Missing required filter: {0}").format(filter_key))
frappe.throw(
title=_("Missing Required Filter"),
msg=_("Missing required filter: {0}").format(
frappe.bold(filter_labels.get(filter_key, filter_key))
),
)
if filters.get("presentation_currency"):
frappe.msgprint(_("Currency filters are currently unsupported in Custom Financial Report."))
frappe.msgprint(
title=_("Unsupported Feature"),
msg=_("Currency filters are currently unsupported in Custom Financial Report."),
indicator="orange",
)
# Margin view is dependent on first row being an income account. Hence not supported.
# Way to implement this would be using calculated rows with formulas.
@@ -464,6 +492,7 @@ class FinancialQueryBuilder:
self.periods = periods
self.company = filters.get("company")
self.account_meta = {} # {name: {account_name, account_number}}
self.ignore_opening_entries = False
def fetch_account_balances(self, accounts: list[dict]) -> dict[str, AccountData]:
"""
@@ -501,6 +530,8 @@ class FinancialQueryBuilder:
"""
Return opening balances for *all accounts* defaulting to zero.
"""
self.ignore_opening_entries = False
if frappe.get_single_value("Accounts Settings", "ignore_account_closing_balance"):
return self._get_opening_balances_from_gl(accounts)
@@ -520,9 +551,9 @@ class FinancialQueryBuilder:
if last_closing_voucher:
closing_voucher = last_closing_voucher[0]
closing_data = self._get_closing_balances(accounts, closing_voucher.name)
self.ignore_opening_entries = True # Else it will double count
if sum(closing_data.values()) != 0.0:
return self._rebase_closing_balances(closing_data, closing_voucher.period_end_date)
return self._rebase_closing_balances(closing_data, closing_voucher.period_end_date)
return self._get_opening_balances_from_gl(accounts)
@@ -616,7 +647,12 @@ class FinancialQueryBuilder:
.groupby(gl_table.account)
)
if not frappe.get_single_value("Accounts Settings", "ignore_is_opening_check_for_reporting"):
ignore_is_opening = frappe.get_single_value(
"Accounts Settings", "ignore_is_opening_check_for_reporting"
)
if self.ignore_opening_entries and not ignore_is_opening:
# This filter here applies to all accounts (BS & PL)
# However, in legacy query, this filter only applies to BS accounts
query = query.where(gl_table.is_opening == "No")
# Add period-specific columns
@@ -680,11 +716,18 @@ class FinancialQueryBuilder:
account_data.unaccumulate_values()
def _apply_standard_filters(self, query, table, doctype: str = "GL Entry"):
if self.filters.get("ignore_closing_entries"):
if doctype == "GL Entry":
query = query.where(table.voucher_type != "Period Closing Voucher")
else:
query = query.where(table.is_period_closing_voucher_entry == 0)
# Exclude PCV-generated entries except those posted to a closing-account-head
# so BS retained earnings survive while P&L reversal entries are filtered out
pcv = frappe.qb.DocType("Period Closing Voucher")
closing_heads = frappe.qb.from_(pcv).select(pcv.closing_account_head).where(pcv.docstatus == 1)
if doctype == "GL Entry":
is_pcv = table.voucher_type == "Period Closing Voucher"
else:
# Account Closing Balance
is_pcv = table.is_period_closing_voucher_entry == 1
query = query.where(~is_pcv | table.account.isin(closing_heads))
if self.filters.get("project"):
projects = self.filters.get("project")
@@ -1392,7 +1435,8 @@ class FormattingEngine:
condition=lambda rd: getattr(rd.row, "italic_text", False), format_properties={"italic": True}
),
FormattingRule(
condition=lambda rd: rd.is_detail_row, format_properties={"is_detail": True, "prefix": ""}
condition=lambda rd: rd.is_detail_row,
format_properties={"is_detail": True, "prefix": DEFAULT_BULLET_PREFIX},
),
FormattingRule(
condition=lambda rd: getattr(rd.row, "warn_if_negative", False),
@@ -1838,3 +1882,124 @@ class GrowthViewTransformer:
return 0.0
else:
return flt(((current_value - previous_value) / abs(previous_value)) * 100, 2)
# ============================================================================
# XLSX EXPORT STYLING
# ============================================================================
def get_xlsx_styles(metadata: XLSXMetadata) -> dict | None:
"""
Generate XLSX styles for financial report templates.
NOTE: Currently only custom report generated with "Report Template" filter will have styles applied.
"""
# skip styling
if not metadata.filters.get("report_template"):
return
builder = XLSXStyleBuilder(metadata, default_styling=False)
builder.apply_default_styles(currency_formatting=False)
# currency is fixed for all columns (only if report template filter is applied)
currency = get_company_currency(metadata.filters.get("company"))
styles = {
"bold": builder.register_style({"bold": True}),
"italic": builder.register_style({"italic": True}),
"warning": builder.register_style({"font_color": "#dc3545"}), # text-danger
}
fieldtype_formats = {
"Int": builder.register_style({"num_format": "General"}),
"Float": builder.register_style({"num_format": builder.get_number_format("Float")}),
"Percent": builder.register_style({"num_format": builder.get_number_format("Percent")}),
"Currency": builder.register_style({"num_format": builder.get_number_format("Currency", currency)}),
}
# quick access for hot loop
style_cell = builder.style_cell
@cache
def get_color_style(color: str) -> int:
return builder.register_style({"font_color": color})
@cache
def get_prefix_style(prefix: str) -> int:
prefix = f"{prefix or DEFAULT_BULLET_PREFIX}@"
return builder.register_style({"num_format": prefix})
@cache
def get_indent_style(indent: int) -> int:
return builder.register_style({"align": "left", "indent": indent})
# column level styling of currency columns
for col_idx, col in metadata.column_map.items():
if col.get("fieldtype") != "Currency":
continue
builder.style_column(col_idx, fieldtype_formats["Currency"])
# cell level styling
for row_idx, row in metadata.row_map.items():
# skip total row
if metadata.has_total_row and row_idx == builder.last_row_index:
continue
is_segmented = (row.get("_segment_info", {}).get("total_segments", 1) or 1) > 1
segment_values = row.get("segment_values", {}) or {}
for col_idx, col in metadata.column_map.items():
fieldname = col.get("fieldname")
is_account = fieldname == "account"
# determine formatting bucket
if is_segmented and fieldname.startswith(SEGMENT_PREFIX):
formatting = row.copy()
_, seg_idx, seg_fieldname = fieldname.split("_", 2)
is_account = seg_fieldname == "account"
formatting.update(segment_values.get(f"{SEGMENT_PREFIX}{seg_idx}", {}) or {})
else:
formatting = row # default formatting bucket.
if not is_account and formatting.get("is_blank_line"):
continue
col_fieldtype = col.get("fieldtype")
cell_fieldtype = formatting.get("fieldtype") or col_fieldtype
cell_value = row.get(fieldname)
if cell_value in (None, ""):
continue
# account column and other fieldtype styling
if is_account:
if formatting.get("is_detail") or (prefix := formatting.get("prefix")):
style_cell(row_idx, col_idx, get_prefix_style(prefix))
# custom indentation (different segment might have different indentation levels)
if is_segmented and (indent := formatting.get("indent")) and indent > 0:
style_cell(row_idx, col_idx, get_indent_style(indent))
else:
if col_fieldtype != cell_fieldtype and cell_fieldtype in fieldtype_formats:
style_cell(row_idx, col_idx, fieldtype_formats[cell_fieldtype])
# text styles
for style_key in ("bold", "italic"):
if formatting.get(style_key):
style_cell(row_idx, col_idx, styles[style_key])
# color styles
if (
formatting.get("warn_if_negative")
and cell_fieldtype in frappe.model.numeric_fieldtypes
and flt(cell_value) < 0
):
style_cell(row_idx, col_idx, styles["warning"])
elif color := formatting.get("color"):
style_cell(row_idx, col_idx, get_color_style(color))
return builder.result

View File

@@ -10,6 +10,7 @@ from erpnext.accounts.doctype.financial_report_template.financial_report_engine
DependencyResolver,
FilterExpressionParser,
FinancialQueryBuilder,
FinancialReportEngine,
FormulaCalculator,
PeriodValue,
)
@@ -1952,6 +1953,159 @@ class TestFinancialQueryBuilder(FinancialReportTemplateTestCase):
jv_2023.cancel()
def test_opening_entries_roll_into_opening_after_period_closing(self):
"""
Sequence:
1. is_opening JV of 3000 in current year (FY 2024)
2. is_opening JV of 5000 in next year (FY 2025)
3. Period Closing Voucher for previous year (FY 2023)
Expected (BS report for FY 2024):
opening of FY 2024 = 3000 + 5000 = 8000
(all is_opening entries roll into opening irrespective of fiscal year,
on top of the PCV carry-forward — here PCV closing for cash is 0).
"""
company = "_Test Company"
cash_account = "_Test Cash - _TC"
# Opening JVs cannot post against P&L accounts; use a Balance Sheet offset.
opening_offset_account = "Temporary Opening - _TC"
pcv = None
jv_current_year = None
jv_next_year = None
original_pcv_setting = frappe.db.get_single_value(
"Accounts Settings", "use_legacy_controller_for_pcv"
)
try:
# Step 1: opening JV in current year (FY 2024) — must be posted before PCV
# exists, else `validate_against_pcv` rejects it.
jv_current_year = make_journal_entry(
account1=cash_account,
account2=opening_offset_account,
amount=3000,
posting_date="2024-06-15",
company=company,
save=False,
)
jv_current_year.is_opening = "Yes"
jv_current_year.insert()
jv_current_year.submit()
# Step 2: opening JV in next year (FY 2025)
jv_next_year = make_journal_entry(
account1=cash_account,
account2=opening_offset_account,
amount=5000,
posting_date="2025-06-15",
company=company,
save=False,
)
jv_next_year.is_opening = "Yes"
jv_next_year.insert()
jv_next_year.submit()
# Step 3: book Period Closing Voucher for previous year (FY 2023)
closing_account = frappe.db.get_value(
"Account",
{
"company": company,
"root_type": "Liability",
"is_group": 0,
"account_type": ["not in", ["Payable", "Receivable"]],
},
"name",
)
fy_2023 = get_fiscal_year("2023-06-15", company=company)
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
pcv = frappe.get_doc(
{
"doctype": "Period Closing Voucher",
"transaction_date": "2023-12-31",
"period_start_date": fy_2023[1],
"period_end_date": fy_2023[2],
"company": company,
"fiscal_year": fy_2023[0],
"cost_center": "_Test Cost Center - _TC",
"closing_account_head": closing_account,
"remarks": "Test Period Closing",
}
)
pcv.insert()
pcv.submit()
pcv.reload()
# Run BS report for FY 2024
filters = {
"company": company,
"from_fiscal_year": "2024",
"to_fiscal_year": "2024",
"period_start_date": "2024-01-01",
"period_end_date": "2024-12-31",
"filter_based_on": "Date Range",
"periodicity": "Yearly",
"ignore_closing_entries": True,
}
periods = [{"key": "2024", "from_date": "2024-01-01", "to_date": "2024-12-31"}]
query_builder = FinancialQueryBuilder(filters, periods)
accounts = [
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
frappe._dict(
{
"name": opening_offset_account,
"account_name": "Temporary Opening",
"account_number": "1900",
}
),
]
balances_data = query_builder.fetch_account_balances(accounts)
cash_data = balances_data.get(cash_account)
offset_data = balances_data.get(opening_offset_account)
self.assertIsNotNone(cash_data, "Cash account should exist in results")
self.assertIsNotNone(offset_data, "Offset account should exist in results")
year_2024_cash = cash_data.get_period("2024")
year_2024_offset = offset_data.get_period("2024")
self.assertIsNotNone(year_2024_cash, "FY 2024 period should exist for cash")
self.assertIsNotNone(year_2024_offset, "FY 2024 period should exist for offset")
# All is_opening JVs (current + next year) roll into FY 2024 opening
self.assertEqual(
year_2024_cash.opening,
8000.0,
"FY 2024 cash opening must combine is_opening JVs from current and next year",
)
self.assertEqual(
year_2024_offset.opening,
-8000.0,
"FY 2024 offset opening must combine is_opening JVs from current and next year",
)
self.assertEqual(
year_2024_cash.movement, 0.0, "Opening JVs must not be counted as period movement"
)
self.assertEqual(year_2024_cash.closing, 8000.0, "Closing = opening when no non-opening movement")
finally:
frappe.db.set_single_value(
"Accounts Settings", "use_legacy_controller_for_pcv", original_pcv_setting or 0
)
if pcv:
pcv.reload()
if pcv.docstatus == 1:
pcv.cancel()
if jv_next_year and jv_next_year.docstatus == 1:
jv_next_year.cancel()
if jv_current_year and jv_current_year.docstatus == 1:
jv_current_year.cancel()
def test_account_with_gl_entries_but_no_prior_closing_balance(self):
company = "_Test Company"
cash_account = "_Test Cash - _TC"
@@ -2025,3 +2179,210 @@ class TestFinancialQueryBuilder(FinancialReportTemplateTestCase):
finally:
jv.cancel()
def test_pl_pcv_exclusion_and_growth_view_year_over_year(self):
"""
Sequence:
1. Expense JV 2000 in FY 2024, PCV for FY 2024
→ assert FY 2024 movement = 2000 via FinancialQueryBuilder
2. Expense JV 3000 in FY 2025, PCV for FY 2025
3. Run FinancialReportEngine with selected_view="Growth"
→ assert col_2024 = 2000 (raw), col_2025 = 50.0 (% growth)
"""
company = "_Test Company"
expense_account = "Administrative Expenses - _TC"
bank_account = "_Test Bank - _TC"
template = None
pcv_2024 = None
pcv_2025 = None
jv_2024 = None
jv_2025 = None
original_pcv_setting = frappe.db.get_single_value(
"Accounts Settings", "use_legacy_controller_for_pcv"
)
try:
closing_account = frappe.db.get_value(
"Account",
{
"company": company,
"root_type": "Liability",
"is_group": 0,
"account_type": ["not in", ["Payable", "Receivable"]],
},
"name",
)
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
accounts = [
frappe._dict(
{
"name": expense_account,
"account_name": "Administrative Expenses",
"account_number": "5001",
}
),
]
# --- Step 1: FY 2024 expense + PCV, assert PCV reversal excluded ---
jv_2024 = make_journal_entry(
account1=expense_account,
account2=bank_account,
amount=2000,
posting_date="2024-06-15",
company=company,
submit=True,
)
fy_2024 = get_fiscal_year("2024-06-15", company=company)
pcv_2024 = frappe.get_doc(
{
"doctype": "Period Closing Voucher",
"transaction_date": "2024-12-31",
"period_start_date": fy_2024[1],
"period_end_date": fy_2024[2],
"company": company,
"fiscal_year": fy_2024[0],
"cost_center": "_Test Cost Center - _TC",
"closing_account_head": closing_account,
"remarks": "Test PCV FY 2024",
}
)
pcv_2024.insert()
pcv_2024.submit()
pcv_2024.reload()
builder_2024 = FinancialQueryBuilder(
{
"company": company,
"from_fiscal_year": "2024",
"to_fiscal_year": "2024",
"period_start_date": "2024-01-01",
"period_end_date": "2024-12-31",
"filter_based_on": "Date Range",
"periodicity": "Yearly",
},
[{"key": "2024", "from_date": "2024-01-01", "to_date": "2024-12-31"}],
)
data_2024 = builder_2024.fetch_account_balances(accounts)
expense_2024 = data_2024.get(expense_account)
self.assertIsNotNone(expense_2024, "Expense account must appear in FY 2024 results")
year_2024 = expense_2024.get_period("2024")
self.assertEqual(
year_2024.movement,
2000.0,
"FY 2024 expense movement must equal real expense (PCV reversal excluded)",
)
# --- Step 2: FY 2025 expense + PCV ---
jv_2025 = make_journal_entry(
account1=expense_account,
account2=bank_account,
amount=3000,
posting_date="2025-06-15",
company=company,
submit=True,
)
fy_2025 = get_fiscal_year("2025-06-15", company=company)
pcv_2025 = frappe.get_doc(
{
"doctype": "Period Closing Voucher",
"transaction_date": "2025-12-31",
"period_start_date": fy_2025[1],
"period_end_date": fy_2025[2],
"company": company,
"fiscal_year": fy_2025[0],
"cost_center": "_Test Cost Center - _TC",
"closing_account_head": closing_account,
"remarks": "Test PCV FY 2025",
}
)
pcv_2025.insert()
pcv_2025.submit()
pcv_2025.reload()
# --- Step 3: full pipeline with Growth view across both years ---
template_name = f"Test Growth Template {frappe.generate_hash()[:8]}"
template = frappe.get_doc(
{
"doctype": "Financial Report Template",
"template_name": template_name,
"report_type": "Profit and Loss Statement",
"rows": [
{
"reference_code": "EXP_ADMIN",
"display_name": "Administrative Expenses",
"indentation_level": 0,
"data_source": "Account Data",
"balance_type": "Closing Balance",
"calculation_formula": f'["name", "=", "{expense_account}"]',
},
],
}
)
template.insert()
filters = frappe._dict(
{
"company": company,
"report_template": template_name,
"from_fiscal_year": fy_2024[0],
"to_fiscal_year": fy_2025[0],
"period_start_date": "2024-01-01",
"period_end_date": "2025-12-31",
"filter_based_on": "Date Range",
"periodicity": "Yearly",
"accumulated_values": 0,
"selected_view": "Growth",
}
)
_columns, formatted_data, _msg, _chart = FinancialReportEngine().execute(filters)
expense_row = next(
(row for row in formatted_data if row.get("account_name") == "Administrative Expenses"),
None,
)
self.assertIsNotNone(expense_row, "Administrative Expenses row must appear in growth view")
period_keys = expense_row.get("_segment_info", {}).get("period_keys", [])
self.assertEqual(len(period_keys), 2, "Yearly view must yield exactly two periods")
first_period_key, second_period_key = period_keys
# First column: raw absolute value (FY 2024 expense)
self.assertEqual(
flt(expense_row[first_period_key]),
2000.0,
"First column in growth view must keep raw FY 2024 expense value",
)
# Second column: ((3000 - 2000) / 2000) * 100 = 50.0
self.assertEqual(
flt(expense_row[second_period_key]),
50.0,
"Second column must be % growth FY 2024 → FY 2025",
)
finally:
frappe.db.set_single_value(
"Accounts Settings", "use_legacy_controller_for_pcv", original_pcv_setting or 0
)
if pcv_2025:
pcv_2025.reload()
if pcv_2025.docstatus == 1:
pcv_2025.cancel()
if jv_2025 and jv_2025.docstatus == 1:
jv_2025.cancel()
if pcv_2024:
pcv_2024.reload()
if pcv_2024.docstatus == 1:
pcv_2024.cancel()
if jv_2024 and jv_2024.docstatus == 1:
jv_2024.cancel()
if template and frappe.db.exists("Financial Report Template", template.name):
frappe.delete_doc("Financial Report Template", template.name, force=1)

View File

@@ -47,3 +47,12 @@ frappe.ui.form.on("Item Tax Template", {
});
},
});
frappe.ui.form.on("Item Tax Template Detail", {
not_applicable: function (frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.not_applicable) {
frappe.model.set_value(cdt, cdn, "tax_rate", 0);
}
},
});

View File

@@ -27,8 +27,15 @@ class ItemTaxTemplate(Document):
# end: auto-generated types
def validate(self):
self.set_zero_rate_for_not_applicable_tax()
self.validate_tax_accounts()
def set_zero_rate_for_not_applicable_tax(self):
"""Ensure tax_rate is 0 for any row marked as not applicable."""
for row in self.get("taxes"):
if row.not_applicable:
row.tax_rate = 0
def autoname(self):
if self.company and self.title:
abbr = frappe.get_cached_value("Company", self.company, "abbr")

View File

@@ -6,7 +6,8 @@
"engine": "InnoDB",
"field_order": [
"tax_type",
"tax_rate"
"tax_rate",
"not_applicable"
],
"fields": [
{
@@ -21,20 +22,30 @@
"fieldname": "tax_rate",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Tax Rate"
"label": "Tax Rate",
"read_only_depends_on": "eval:doc.not_applicable"
},
{
"default": "0",
"description": "Check if this tax is not applicable to items (distinct from 0% rate)",
"fieldname": "not_applicable",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Not Applicable"
}
],
"istable": 1,
"links": [],
"modified": "2024-03-27 13:09:55.735360",
"modified": "2025-12-26 17:19:18.791891",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template Detail",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -14,6 +14,7 @@ class ItemTaxTemplateDetail(Document):
if TYPE_CHECKING:
from frappe.types import DF
not_applicable: DF.Check
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -648,7 +648,7 @@ $.extend(erpnext.journal_entry, {
reqd: 1,
default: frm.doc.posting_date,
},
{ fieldtype: "Small Text", fieldname: "user_remark", label: __("User Remark") },
{ fieldtype: "Small Text", fieldname: "remark", label: __("Remark") },
{
fieldtype: "Select",
fieldname: "naming_series",
@@ -665,8 +665,14 @@ $.extend(erpnext.journal_entry, {
var values = dialog.get_values();
frm.set_value("posting_date", values.posting_date);
frm.set_value("user_remark", values.user_remark);
frm.set_value("naming_series", values.naming_series);
if (values.remark) {
frm.set_value("custom_remark", 1);
frm.set_value("remark", values.remark);
} else {
frm.set_value("custom_remark", 0);
frm.set_value("remark", "");
}
// clear table is used because there might've been an error while adding child
// and cleanup didn't happen

View File

@@ -39,7 +39,7 @@
"clearance_date",
"column_break_oizh",
"user_remark",
"subscription_section",
"auto_repeat_section",
"auto_repeat",
"tax_withholding_tab",
"section_tax_withholding_entry",
@@ -78,6 +78,7 @@
"from_template",
"title",
"column_break3",
"custom_remark",
"remark",
"mode_of_payment",
"party_not_required"
@@ -202,6 +203,7 @@
{
"fieldname": "user_remark",
"fieldtype": "Small Text",
"hidden": 1,
"label": "User Remark",
"no_copy": 1,
"oldfieldname": "user_remark",
@@ -315,7 +317,7 @@
"no_copy": 1,
"oldfieldname": "remark",
"oldfieldtype": "Small Text",
"read_only": 1
"read_only_depends_on": "eval: !doc.custom_remark"
},
{
"depends_on": "eval:doc.voucher_type== \"Inter Company Journal Entry\"",
@@ -475,11 +477,6 @@
"options": "Stock Entry",
"read_only": 1
},
{
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Subscription"
},
{
"allow_on_submit": 1,
"fieldname": "auto_repeat",
@@ -651,6 +648,17 @@
"fieldname": "tax_withholding_tab",
"fieldtype": "Tab Break",
"label": "Tax Withholding"
},
{
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"default": "0",
"fieldname": "custom_remark",
"fieldtype": "Check",
"label": "Custom Remark"
}
],
"icon": "fa fa-file-text",
@@ -665,7 +673,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2026-03-09 17:15:26.569327",
"modified": "2026-04-08 14:19:30.870894",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -61,6 +61,7 @@ class JournalEntry(AccountsController):
cheque_no: DF.Data | None
clearance_date: DF.Date | None
company: DF.Link
custom_remark: DF.Check
difference: DF.Currency
due_date: DF.Date | None
finance_book: DF.Link | None
@@ -1026,8 +1027,8 @@ class JournalEntry(AccountsController):
if self.flags.skip_remarks_creation:
return
if self.user_remark:
r.append(_("Note: {0}").format(self.user_remark))
if self.get("custom_remark"):
return
if self.cheque_no:
if self.cheque_date:
@@ -1548,7 +1549,7 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
frappe.qb.from_(JournalEntry)
.join(JournalEntryAccount)
.on(JournalEntryAccount.parent == JournalEntry.name)
.select(JournalEntry.name, JournalEntry.posting_date, JournalEntry.user_remark)
.select(JournalEntry.name, JournalEntry.posting_date, JournalEntry.remark)
.where(JournalEntryAccount.account == filters.get("account"))
.where(JournalEntryAccount.reference_type.isnull() | (JournalEntryAccount.reference_type == ""))
.where(JournalEntry.docstatus == 1)

View File

@@ -1,5 +1,5 @@
frappe.listview_settings["Journal Entry"] = {
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "user_remark"],
add_fields: ["voucher_type", "posting_date", "total_debit", "company", "remark"],
get_indicator: function (doc) {
if (doc.docstatus === 1) {
return [__(doc.voucher_type), "blue", `voucher_type,=,${doc.voucher_type}`];

View File

@@ -413,9 +413,9 @@ class TestJournalEntry(ERPNextTestSuite):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
# Configure Repost Accounting Ledger for JVs
settings = frappe.get_doc("Repost Accounting Ledger Settings")
if not [x for x in settings.allowed_types if x.document_type == "Journal Entry"]:
settings.append("allowed_types", {"document_type": "Journal Entry", "allowed": True})
settings = frappe.get_doc("Accounts Settings")
if "Journal Entry" not in [x.document_type for x in settings.repost_allowed_types]:
settings.append("repost_allowed_types", {"document_type": "Journal Entry"})
settings.save()
# Create JV with defaut cost center - _Test Cost Center
@@ -523,7 +523,7 @@ class TestJournalEntry(ERPNextTestSuite):
jv = frappe.new_doc("Journal Entry")
jv.posting_date = nowdate()
jv.company = "_Test Company"
jv.user_remark = "test"
jv.remark = "test"
jv.extend(
"accounts",
[
@@ -592,6 +592,14 @@ class TestJournalEntry(ERPNextTestSuite):
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
def test_custom_remark(self):
# When custom_remark is enabled, remark should not be auto-overwritten on save
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
jv.custom_remark = 1
jv.remark = "My custom remark text"
jv.insert()
self.assertEqual(jv.remark, "My custom remark text")
def test_credit_limit_for_customer(self):
customer = make_customer("_Test New Customer")
set_credit_limit("_Test New Customer", "_Test Company", 50)
@@ -620,7 +628,7 @@ def make_journal_entry(
jv = frappe.new_doc("Journal Entry")
jv.posting_date = posting_date or nowdate()
jv.company = company or "_Test Company"
jv.user_remark = "test"
jv.remark = "test"
jv.multi_currency = 1
jv.set(
"accounts",

View File

@@ -48,7 +48,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-08-16 19:22:42.942264",
"modified": "2026-04-14 18:16:47.795986",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Mode of Payment",
@@ -68,12 +68,21 @@
"read": 1,
"report": 1,
"role": "Accounts User"
},
{
"role": "HR User",
"select": 1
},
{
"role": "HR Manager",
"select": 1
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"translated_doctype": 1
}
}

View File

@@ -89,6 +89,7 @@
"remarks",
"base_in_words",
"is_opening",
"title",
"column_break_16",
"letter_head",
"print_heading",
@@ -96,10 +97,9 @@
"bank_account_no",
"payment_order",
"in_words",
"subscription_section",
"auto_repeat",
"amended_from",
"title"
"auto_repeat_section",
"auto_repeat"
],
"fields": [
{
@@ -503,11 +503,6 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Subscription Section"
},
{
"allow_on_submit": 1,
"fieldname": "auto_repeat",
@@ -781,6 +776,11 @@
"fieldname": "override_tax_withholding_entries",
"fieldtype": "Check",
"label": "Edit Tax Withholding Entries"
},
{
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
}
],
"grid_page_length": 50,

View File

@@ -2306,22 +2306,20 @@ def get_outstanding_reference_documents(args, validate=False):
# Get positive outstanding sales /purchase invoices
condition = ""
if args.get("voucher_type") and args.get("voucher_no"):
condition = " and voucher_type={} and voucher_no={}".format(
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
)
condition = f" and voucher_type={frappe.db.escape(args['voucher_type'])} and voucher_no={frappe.db.escape(args['voucher_no'])}"
common_filter.append(ple.voucher_type == args["voucher_type"])
common_filter.append(ple.voucher_no == args["voucher_no"])
# Add cost center condition
if args.get("cost_center"):
condition += " and cost_center='%s'" % args.get("cost_center")
condition += f" and cost_center={frappe.db.escape(args.get('cost_center'))}"
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
# dynamic dimension filters
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
if args.get(dim.fieldname):
condition += f" and {dim.fieldname}='{args.get(dim.fieldname)}'"
condition += f" and {dim.fieldname}={frappe.db.escape(args.get(dim.fieldname))}"
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
date_fields_dict = {
@@ -2330,18 +2328,19 @@ def get_outstanding_reference_documents(args, validate=False):
}
for fieldname, date_fields in date_fields_dict.items():
from_date = frappe.db.escape(str(args.get(date_fields[0]))) if args.get(date_fields[0]) else None
to_date = frappe.db.escape(str(args.get(date_fields[1]))) if args.get(date_fields[1]) else None
if args.get(date_fields[0]) and args.get(date_fields[1]):
condition += " and {} between '{}' and '{}'".format(
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
)
condition += f" and {fieldname} between {from_date} and {to_date}"
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
elif args.get(date_fields[0]):
# if only from date is supplied
condition += f" and {fieldname} >= '{args.get(date_fields[0])}'"
condition += f" and {fieldname} >= {from_date}"
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
elif args.get(date_fields[1]):
# if only to date is supplied
condition += f" and {fieldname} <= '{args.get(date_fields[1])}'"
condition += f" and {fieldname} <= {to_date}"
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
if args.get("company"):
@@ -2561,7 +2560,7 @@ def get_orders_to_be_billed(
active_dimensions = get_dimensions(True)[0]
for dim in active_dimensions:
if filters.get(dim.fieldname):
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
condition += f" and {dim.fieldname}={frappe.db.escape(filters.get(dim.fieldname))}"
if party_account_currency == company_currency:
grand_total_field = "base_grand_total"

View File

@@ -195,6 +195,30 @@ class TestPaymentEntry(ERPNextTestSuite):
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 100)
def test_reference_outstanding_amount_on_advance_pull(self):
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
so = make_sales_order(qty=1, rate=1000)
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
pe.paid_amount = pe.received_amount = 500
pe.references[0].allocated_amount = 500
pe.insert()
pe.submit()
so.reload()
self.assertEqual(so.advance_paid, 500)
si = make_sales_invoice(so.name)
si.allocate_advances_automatically = 1
si.save()
self.assertEqual(si.get("advances")[0].allocated_amount, 500)
self.assertEqual(si.get("advances")[0].reference_name, pe.name)
si.submit()
pe.load_from_db()
self.assertEqual(pe.references[0].reference_name, si.name)
self.assertEqual(pe.references[0].outstanding_amount, si.outstanding_amount)
def test_payment_entry_against_pi(self):
pi = make_purchase_invoice(
supplier="_Test Supplier USD",
@@ -2105,6 +2129,37 @@ class TestPaymentEntry(ERPNextTestSuite):
self.assertEqual(ref.voucher_no, so.name)
self.assertIsNotNone(ref.payment_term)
def test_project_name_in_exchange_gain_loss_entry(self):
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
do_not_submit=True,
)
from erpnext.projects.doctype.project.test_project import make_project
si.project = make_project({"project_name": "_Test Project for Exchange Gain Loss Entry"}).name
si.submit()
pe = get_payment_entry("Sales Invoice", si.name)
pe.source_exchange_rate = 100
pe.insert()
pe.submit()
rows = frappe.get_all(
"Journal Entry Account",
or_filters=[{"reference_name": pe.name}, {"reference_name": si.name}],
fields=["project"],
)
self.assertEqual(len(rows), 2)
self.assertEqual(rows[0].project, si.project)
self.assertEqual(rows[1].project, si.project)
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@@ -183,7 +183,7 @@
"depends_on": "eval:doc.is_a_subscription",
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Subscription Section"
"label": "Subscription"
},
{
"fieldname": "subscription_plans",
@@ -478,7 +478,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-01-13 12:53:00.963274",
"modified": "2026-02-27 19:11:03.308896",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",

View File

@@ -46,8 +46,8 @@ frappe.ui.form.on("Period Closing Voucher", {
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
from_date: frm.doc.period_start_date,
to_date: frm.doc.period_end_date,
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,

View File

@@ -18,9 +18,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 1)
def test_closing_entry(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
@@ -70,9 +67,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertEqual(pcv_gle, expected_gle)
def test_cost_center_wise_posting(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
surplus_account = create_account()
@@ -136,9 +130,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
)
def test_period_closing_with_finance_book_entries(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
@@ -190,9 +181,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertSequenceEqual(pcv_gle, expected_gle)
def test_gl_entries_restrictions(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
@@ -213,10 +201,6 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
self.assertRaises(frappe.ValidationError, jv1.submit)
def test_closing_balance_with_dimensions_and_test_reposting_entry(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
frappe.db.sql("delete from `tabAccount Closing Balance` where company='Test PCV Company'")
company = create_company()
cost_center1 = create_cost_center("Test Cost Center 1")
cost_center2 = create_cost_center("Test Cost Center 2")

View File

@@ -201,7 +201,6 @@ class TestPOSClosingEntry(ERPNextTestSuite):
)
from erpnext.stock.doctype.batch.batch import get_batch_qty
frappe.db.sql("delete from `tabPOS Invoice`")
item_doc = make_item(
"_Test Item With Batch FOR POS Merge Test",
properties={

View File

@@ -26,6 +26,8 @@
"due_date",
"amended_from",
"return_against",
"section_break_abck",
"title",
"accounting_dimensions_section",
"project",
"dimension_col_break",
@@ -187,7 +189,7 @@
"subscription_section",
"from_date",
"to_date",
"column_break_140",
"auto_repeat_section",
"auto_repeat",
"update_auto_repeat_reference",
"against_income_account"
@@ -662,6 +664,7 @@
"fieldname": "total_billing_amount",
"fieldtype": "Currency",
"label": "Total Billing Amount",
"options": "currency",
"print_hide": 1,
"read_only": 1
},
@@ -1462,7 +1465,7 @@
{
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Subscription Section"
"label": "Subscription"
},
{
"allow_on_submit": 1,
@@ -1480,10 +1483,6 @@
"no_copy": 1,
"print_hide": 1
},
{
"fieldname": "column_break_140",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "auto_repeat",
@@ -1533,6 +1532,7 @@
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"options": "Company:company:default_currency",
"read_only": 1
},
{
@@ -1619,12 +1619,29 @@
{
"fieldname": "column_break_bhao",
"fieldtype": "Column Break"
},
{
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "section_break_abck",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2026-02-10 14:23:07.181782",
"modified": "2026-05-01 02:37:30.580568",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

@@ -172,6 +172,7 @@ class POSInvoice(SalesInvoice):
terms: DF.TextEditor | None
territory: DF.Link | None
timesheets: DF.Table[SalesInvoiceTimesheet]
title: DF.Data | None
to_date: DF.Date | None
total: DF.Currency
total_advance: DF.Currency

View File

@@ -37,7 +37,6 @@ class POSInvoiceTestMixin(ERPNextTestSuite):
frappe.db.set_single_value("Selling Settings", "validate_selling_price", 0)
frappe.db.set_single_value("POS Settings", "invoice_type", "POS Invoice")
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=800, basic_rate=100)
frappe.db.sql("delete from `tabTax Rule`")
mode_of_payment = frappe.get_doc("Mode of Payment", "Bank Draft")
set_default_account_for_mode_of_payment(mode_of_payment, "_Test Company", "_Test Bank - _TC")

View File

@@ -34,7 +34,6 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
consolidate_pos_invoices,
)
frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 270})
@@ -64,7 +63,6 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
consolidate_pos_invoices,
)
frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})
@@ -123,7 +121,7 @@ class TestPOSInvoiceMerging(POSInvoiceTestMixin):
item = "Test Selling Price Validation"
make_item(item, {"is_stock_item": 1})
make_purchase_receipt(item_code=item, warehouse="_Test Warehouse - _TC", qty=1, rate=300)
frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile()
pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1)
pos_inv.append("payments", {"mode_of_payment": "Cash", "amount": 300})

View File

@@ -811,6 +811,7 @@
},
{
"default": "0",
"fetch_from": "item_code.grant_commission",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
@@ -857,7 +858,7 @@
],
"istable": 1,
"links": [],
"modified": "2025-11-12 18:11:11.818015",
"modified": "2026-04-20 16:16:12.322024",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",

View File

@@ -17,7 +17,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestPricingRule(ERPNextTestSuite):
def setUp(self):
delete_existing_pricing_rules()
setup_pricing_rule_data()
self.enterClassContext(self.change_settings("Selling Settings", validate_selling_price=0))
@@ -1586,16 +1585,6 @@ def setup_pricing_rule_data():
).insert()
def delete_existing_pricing_rules():
for doctype in [
"Pricing Rule",
"Pricing Rule Item Code",
"Pricing Rule Item Group",
"Pricing Rule Brand",
]:
frappe.db.sql(f"delete from `tab{doctype}`")
def make_item_price(item, price_list_name, item_price):
frappe.get_doc(
{

View File

@@ -662,7 +662,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
if pricing_rule.is_recursive:
transaction_qty = sum(
[
row.qty
flt(row.qty)
for row in doc.items
if not row.is_free_item
and row.item_code == args.item_code

View File

@@ -115,7 +115,12 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
if (
doc.docstatus == 1 &&
doc.outstanding_amount != 0 &&
!doc.on_hold &&
frappe.model.can_create("Payment Entry")
) {
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -130,7 +135,13 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
if (
doc.docstatus == 1 &&
doc.outstanding_amount > 0 &&
!cint(doc.is_return) &&
!doc.on_hold &&
frappe.boot.user.in_create.includes("Payment Request")
) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -443,13 +454,14 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"expense_account",
"discount_account",
"cost_center",
"project",
]);
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["expense_account", "discount_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
on_submit() {
@@ -558,12 +570,6 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
};
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};
frappe.ui.form.on("Purchase Invoice", {
setup: function (frm) {
frm.custom_make_buttons = {

View File

@@ -27,6 +27,8 @@
"update_billed_amount_in_purchase_receipt",
"apply_tds",
"amended_from",
"section_break_hzux",
"title",
"supplier_invoice_details",
"bill_no",
"column_break_15",
@@ -180,11 +182,12 @@
"unrealized_profit_loss_account",
"subscription_section",
"subscription",
"auto_repeat",
"update_auto_repeat_reference",
"column_break_114",
"from_date",
"to_date",
"automation_section",
"auto_repeat",
"update_auto_repeat_reference",
"printing_settings",
"letter_head",
"group_same_items",
@@ -1675,6 +1678,24 @@
"fieldname": "totals_section",
"fieldtype": "Section Break",
"label": "Totals"
},
{
"collapsible": 1,
"fieldname": "automation_section",
"fieldtype": "Section Break",
"label": "Automation"
},
{
"fieldname": "section_break_hzux",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"grid_page_length": 50,
@@ -1682,7 +1703,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-03-25 11:45:38.696888",
"modified": "2026-04-28 07:15:31.062404",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -203,6 +203,7 @@ class PurchaseInvoice(BuyingController):
taxes_and_charges_deducted: DF.Currency
tc_name: DF.Link | None
terms: DF.TextEditor | None
title: DF.Data | None
to_date: DF.Date | None
total: DF.Currency
total_advance: DF.Currency

View File

@@ -2256,9 +2256,9 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
def test_repost_accounting_entries(self):
# update repost settings
settings = frappe.get_doc("Repost Accounting Ledger Settings")
if not [x for x in settings.allowed_types if x.document_type == "Purchase Invoice"]:
settings.append("allowed_types", {"document_type": "Purchase Invoice", "allowed": True})
settings = frappe.get_doc("Accounts Settings")
if "Purchase Invoice" not in [x.document_type for x in settings.repost_allowed_types]:
settings.append("repost_allowed_types", {"document_type": "Purchase Invoice"})
settings.save()
pi = make_purchase_invoice(

View File

@@ -219,7 +219,6 @@ def get_allowed_types_from_settings(child_doc: bool = False):
x.document_type
for x in frappe.db.get_all(
"Repost Allowed Types",
filters={"allowed": True},
fields=["document_type"],
distinct=True,
)
@@ -274,14 +273,13 @@ def validate_docs_for_voucher_types(doc_voucher_types):
if disallowed_types := voucher_types.difference(allowed_types):
message = "are" if len(disallowed_types) > 1 else "is"
frappe.throw(
_("{0} {1} not allowed to be reposted. Modify {2} to enable reposting.").format(
_(
"{0} {1} not allowed to be reposted. You can enable it by adding it '{2}' table in {3}."
).format(
frappe.bold(comma_and(list(disallowed_types))),
message,
frappe.bold(
frappe.utils.get_link_to_form(
"Repost Accounting Ledger Settings", "Repost Accounting Ledger Settings"
)
),
frappe.bold("Allowed Doctype"),
frappe.utils.get_link_to_form("Accounts Settings"),
)
)
@@ -289,8 +287,6 @@ def validate_docs_for_voucher_types(doc_voucher_types):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters):
filters = {"allowed": True}
if txt:
filters.update({"document_type": ("like", f"%{txt}%")})

View File

@@ -203,6 +203,11 @@ class TestRepostAccountingLedger(ERPNextTestSuite):
def test_06_repost_purchase_receipt(self):
from erpnext.accounts.doctype.account.test_account import create_account
if not frappe.db.set_value("Company", "_Test Company", "service_expense_account"):
frappe.db.set_value(
"Company", "_Test Company", "service_expense_account", "Marketing Expenses - _TC"
)
provisional_account = create_account(
account_name="Provision Account",
parent_account="Current Liabilities - _TC",
@@ -275,7 +280,8 @@ def update_repost_settings():
"Journal Entry",
"Purchase Receipt",
]
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
for x in allowed_types:
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
repost_settings.save()
settings = frappe.get_doc("Accounts Settings")
for _type in allowed_types:
if _type not in [x.document_type for x in settings.repost_allowed_types]:
settings.append("repost_allowed_types", {"document_type": _type})
settings.save()

View File

@@ -1,8 +0,0 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Repost Accounting Ledger Settings", {
// refresh(frm) {
// },
// });

View File

@@ -1,53 +0,0 @@
{
"actions": [],
"creation": "2023-11-07 09:57:20.619939",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"allowed_types"
],
"fields": [
{
"fieldname": "allowed_types",
"fieldtype": "Table",
"label": "Allowed Doctypes",
"options": "Repost Allowed Types"
}
],
"grid_page_length": 50,
"hide_toolbar": 0,
"in_create": 1,
"issingle": 1,
"links": [],
"modified": "2026-03-16 13:28:21.312607",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Accounting Ledger Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Administrator",
"select": 1,
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"read": 1,
"role": "System Manager",
"select": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,45 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import get_child_docs
class RepostAccountingLedgerSettings(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.repost_allowed_types.repost_allowed_types import RepostAllowedTypes
allowed_types: DF.Table[RepostAllowedTypes]
# end: auto-generated types
def validate(self):
self.update_property_for_accounting_dimension()
def update_property_for_accounting_dimension(self):
doctypes = [entry.document_type for entry in self.allowed_types if entry.allowed]
if not doctypes:
return
doctypes += get_child_docs(doctypes)
set_allow_on_submit_for_dimension_fields(doctypes)
def set_allow_on_submit_for_dimension_fields(doctypes):
for dt in doctypes:
meta = frappe.get_meta(dt)
for dimension in get_accounting_dimensions():
df = meta.get_field(dimension)
if df and not df.allow_on_submit:
frappe.db.set_value("Custom Field", dt + "-" + dimension, "allow_on_submit", 1)

View File

@@ -1,11 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from erpnext.tests.utils import ERPNextTestSuite
class TestRepostAccountingLedgerSettings(ERPNextTestSuite):
pass

View File

@@ -6,9 +6,7 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"column_break_sfzb",
"allowed"
"document_type"
],
"fields": [
{
@@ -17,29 +15,20 @@
"in_list_view": 1,
"label": "Doctype",
"options": "DocType"
},
{
"default": "0",
"fieldname": "allowed",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Allowed"
},
{
"fieldname": "column_break_sfzb",
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:32.415806",
"modified": "2026-04-14 16:53:16.806714",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Allowed Types",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -14,7 +14,6 @@ class RepostAllowedTypes(Document):
if TYPE_CHECKING:
from frappe.types import DF
allowed: DF.Check
document_type: DF.Link | None
parent: DF.Data
parentfield: DF.Data

View File

@@ -94,7 +94,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
}
if (doc.docstatus == 1 && doc.outstanding_amount != 0) {
if (doc.docstatus == 1 && doc.outstanding_amount != 0 && frappe.model.can_create("Payment Entry")) {
this.frm.add_custom_button(__("Payment"), () => this.make_payment_entry(), __("Create"));
this.frm.page.set_inner_btn_group_as_primary(__("Create"));
}
@@ -135,13 +135,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
if (doc.outstanding_amount > 0) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request_with_schedule();
},
__("Create")
);
if (frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request_with_schedule();
},
__("Create")
);
}
this.frm.add_custom_button(
__("Invoice Discounting"),
this.make_invoice_discounting.bind(this),
@@ -552,12 +554,14 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, [
"income_account",
"discount_account",
"cost_center",
]);
const row = frappe.get_doc(cdt, cdn);
const field_copy = ["income_account", "discount_account", "cost_center"];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
field_copy.push("project");
}
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
set_dynamic_labels() {

View File

@@ -33,6 +33,8 @@
"is_created_using_pos",
"pos_closing_entry",
"has_subcontracted",
"section_break_qllv",
"title",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -91,9 +93,9 @@
"column_break_xjag",
"base_rounding_adjustment",
"base_rounded_total",
"section_break_vacb",
"section_break_pxwz",
"total_advance",
"column_break_rdks",
"column_break_iaso",
"outstanding_amount",
"section_tax_withholding_entry",
"tax_withholding_group",
@@ -199,6 +201,7 @@
"unrealized_profit_loss_account",
"against_income_account",
"commission_section",
"column_break_rdiw",
"sales_partner",
"amount_eligible_for_commission",
"column_break10",
@@ -214,12 +217,14 @@
"language",
"subscription_section",
"subscription",
"from_date",
"auto_repeat",
"column_break_140",
"from_date",
"to_date",
"automation_section",
"auto_repeat",
"update_auto_repeat_reference",
"utm_analytics_section",
"column_break_rdke",
"utm_source",
"utm_medium",
"column_break_ixxw",
@@ -1147,6 +1152,7 @@
"hide_seconds": 1,
"label": "Rounding Adjustment",
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -1159,6 +1165,7 @@
"label": "Rounded Total",
"oldfieldname": "rounded_total",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
@@ -2304,14 +2311,6 @@
"options": "fa fa-group",
"print_hide": 1
},
{
"fieldname": "section_break_vacb",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_rdks",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_ixxw",
"fieldtype": "Column Break"
@@ -2321,6 +2320,40 @@
"fieldname": "utm_analytics_section",
"fieldtype": "Section Break",
"label": "UTM Analytics"
},
{
"collapsible": 1,
"fieldname": "automation_section",
"fieldtype": "Section Break",
"label": "Automation"
},
{
"fieldname": "section_break_pxwz",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_rdke",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_rdiw",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_iaso",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_qllv",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"grid_page_length": 50,
@@ -2334,7 +2367,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-03-09 17:15:30.931929",
"modified": "2026-05-01 02:37:29.742764",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -225,6 +225,7 @@ class SalesInvoice(SellingController):
terms: DF.TextEditor | None
territory: DF.Link | None
timesheets: DF.Table[SalesInvoiceTimesheet]
title: DF.Data | None
to_date: DF.Date | None
total: DF.Currency
total_advance: DF.Currency

View File

@@ -2025,10 +2025,6 @@ class TestSalesInvoice(ERPNextTestSuite):
)
def test_multiple_uom_in_selling(self):
frappe.db.sql(
"""delete from `tabItem Price`
where price_list='_Test Price List' and item_code='_Test Item'"""
)
item_price = frappe.new_doc("Item Price")
item_price.price_list = "_Test Price List"
item_price.item_code = "_Test Item"

View File

@@ -10,8 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestShareTransfer(ERPNextTestSuite):
def setUp(self):
frappe.db.sql("delete from `tabShare Transfer`")
frappe.db.sql("delete from `tabShare Balance`")
share_transfers = [
{
"doctype": "Share Transfer",

View File

@@ -25,6 +25,10 @@ frappe.ui.form.on("Shipping Rule", {
},
calculate_based_on: function (frm) {
frm.trigger("toggle_reqd");
if (frm.doc.calculate_based_on === "Fixed") {
frm.clear_table("conditions");
frm.refresh_field("conditions");
}
},
toggle_reqd: function (frm) {
frm.toggle_reqd("shipping_amount", frm.doc.calculate_based_on === "Fixed");

View File

@@ -58,6 +58,11 @@ class ShippingRule(Document):
self.validate_overlapping_shipping_rule_conditions()
def validate_from_to_values(self):
if self.calculate_based_on == "Fixed":
if self.conditions:
self.set("conditions", [])
return
zero_to_values = []
for d in self.get("conditions"):

View File

@@ -5,8 +5,7 @@ import datetime
from unittest.mock import patch
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.utils import add_days, add_months, today
from frappe.utils import add_days, add_months, getdate, today
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.utils import get_fiscal_year
@@ -1922,7 +1921,6 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
def set_previous_fy_and_tax_category(self):
test_company = "_Test Company"
category = "Cumulative Threshold TDS"
def add_company_to_fy(fy, company):
if not [x.company for x in fy.companies if x.company == company]:
@@ -1948,20 +1946,6 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
)
self.prev_fy.save()
# setup tax withholding category for previous fiscal year
cat = frappe.get_doc("Tax Withholding Category", category)
cat.append(
"rates",
{
"from_date": self.prev_fy.year_start_date,
"to_date": self.prev_fy.year_end_date,
"tax_withholding_rate": 10,
"single_threshold": 0,
"cumulative_threshold": 30000,
},
)
cat.save()
def test_tds_across_fiscal_year(self):
"""
Advance TDS on previous fiscal year should be properly allocated on Invoices in upcoming fiscal year
@@ -1972,6 +1956,14 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
supplier = "Test TDS Supplier"
# Cumulative threshold 30000 and tax rate 10%
category = "Cumulative Threshold TDS"
create_tax_withholding_category(
category_name=category,
rate=10,
from_date=self.prev_fy.year_start_date,
to_date=self.prev_fy.year_end_date,
account="TDS - _TC",
cumulative_threshold=30000,
)
frappe.db.set_value(
"Supplier",
supplier,
@@ -2043,6 +2035,158 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
self.assertEqual(pi2.taxes, [])
self.assertEqual(payment.taxes[0].tax_amount, 6000)
def test_threshold_resets_in_new_fiscal_year(self):
"""
Threshold entries from a previous FY must not carry over into the new FY.
"""
self.set_previous_fy_and_tax_category()
invoices = []
supplier = "Test TDS Supplier"
category = "Cumulative Threshold TDS"
create_tax_withholding_category(
category_name=category,
rate=10,
from_date=self.prev_fy.year_start_date,
to_date=self.prev_fy.year_end_date,
account="TDS - _TC",
cumulative_threshold=30000,
)
self.setup_party_with_category("Supplier", supplier, category)
prev_fy_date = add_days(self.prev_fy.year_end_date, -10)
# Previous FY: 3 invoices to cross the 30000 cumulative threshold
for _ in range(3):
pi = create_purchase_invoice(supplier=supplier, posting_date=prev_fy_date, set_posting_time=True)
pi.submit()
invoices.append(pi)
# Third invoice crosses the threshold - 3000 TDS deducted across all three
self.validate_tax_deduction(invoices[-1], 3000)
# Current FY: 10000 invoice - must be Under Withheld, threshold resets
pi_curr = create_purchase_invoice(supplier=supplier)
pi_curr.submit()
invoices.append(pi_curr)
self.validate_tax_deduction(pi_curr, 0)
self.validate_tax_withholding_entries(
"Purchase Invoice",
pi_curr.name,
[
self.get_tax_withholding_entry(
tax_withholding_category=category,
party_type="Supplier",
party=supplier,
taxable_doctype="Purchase Invoice",
taxable_name=pi_curr.name,
tax_rate=10.0,
taxable_amount=10000.0,
withholding_amount=0.0,
status="Under Withheld",
withholding_doctype=None,
withholding_name=None,
under_withheld_reason=None,
)
],
)
self.cleanup_invoices(invoices)
def test_tax_on_excess_threshold_resets_in_new_fiscal_year(self):
"""
For tax-on-excess categories, unused threshold must reset each FY.
"""
self.set_previous_fy_and_tax_category()
invoices = []
supplier = "Test TDS Supplier3"
category = "New TDS Category"
create_tax_withholding_category(
category_name=category,
rate=10,
from_date=self.prev_fy.year_start_date,
to_date=self.prev_fy.year_end_date,
account="TDS - _TC",
cumulative_threshold=30000,
tax_on_excess_amount=1,
round_off_tax_amount=1,
)
self.setup_party_with_category("Supplier", supplier, category)
prev_fy_date = add_days(self.prev_fy.year_end_date, -10)
for _ in range(2):
pi = create_purchase_invoice(supplier=supplier, posting_date=prev_fy_date, set_posting_time=True)
pi.submit()
invoices.append(pi)
pi3 = create_purchase_invoice(
supplier=supplier, rate=20000, posting_date=prev_fy_date, set_posting_time=True
)
pi3.submit()
invoices.append(pi3)
self.validate_tax_deduction(pi3, 1000)
self.validate_tax_withholding_entries(
"Purchase Invoice",
pi3.name,
[
self.get_tax_withholding_entry(
tax_withholding_category=category,
party_type="Supplier",
party=supplier,
taxable_doctype="Purchase Invoice",
taxable_name=pi3.name,
tax_rate=10.0,
taxable_amount=10000.0,
withholding_amount=0.0,
status="Settled",
withholding_doctype="Purchase Invoice",
withholding_name=pi3.name,
under_withheld_reason="Threshold Exemption",
),
self.get_tax_withholding_entry(
tax_withholding_category=category,
party_type="Supplier",
party=supplier,
taxable_doctype="Purchase Invoice",
taxable_name=pi3.name,
tax_rate=10.0,
taxable_amount=10000.0,
withholding_amount=1000.0,
status="Settled",
withholding_doctype="Purchase Invoice",
withholding_name=pi3.name,
under_withheld_reason=None,
),
],
)
# no excess, so no TDS
pi_curr = create_purchase_invoice(supplier=supplier, rate=30000)
pi_curr.submit()
invoices.append(pi_curr)
self.validate_tax_deduction(pi_curr, 0)
self.validate_tax_withholding_entries(
"Purchase Invoice",
pi_curr.name,
[
self.get_tax_withholding_entry(
tax_withholding_category=category,
party_type="Supplier",
party=supplier,
taxable_doctype="Purchase Invoice",
taxable_name=pi_curr.name,
tax_rate=10.0,
taxable_amount=30000.0,
withholding_amount=0.0,
status="Settled",
withholding_doctype="Purchase Invoice",
withholding_name=pi_curr.name,
under_withheld_reason="Threshold Exemption",
),
],
)
self.cleanup_invoices(invoices)
@ERPNextTestSuite.change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
def test_tds_payment_entry_cancellation(self):
"""
@@ -3997,7 +4141,7 @@ def create_tax_withholding_category(
tax_deduction_basis="Net Total",
):
if not frappe.db.exists("Tax Withholding Category", category_name):
frappe.get_doc(
doc = frappe.get_doc(
{
"doctype": "Tax Withholding Category",
"name": category_name,
@@ -4018,6 +4162,22 @@ def create_tax_withholding_category(
"accounts": [{"company": "_Test Company", "account": account}],
}
).insert()
else:
doc = frappe.get_doc("Tax Withholding Category", category_name)
if not any(getdate(r.from_date) == getdate(from_date) for r in doc.rates):
doc.append(
"rates",
{
"from_date": from_date,
"to_date": to_date,
"tax_withholding_rate": rate,
"single_threshold": single_threshold,
"cumulative_threshold": cumulative_threshold,
},
)
doc.save()
return doc
def create_lower_deduction_certificate(

View File

@@ -640,6 +640,7 @@ class TaxWithholdingController:
.where(entry.tax_withholding_category == category.name)
.where(entry.company == self.doc.company)
.where(entry.docstatus == 1)
.where(entry.taxable_date.between(category.from_date, category.to_date))
.groupby(entry.status)
)

View File

@@ -1,118 +1,147 @@
[
{
"account_category_name": "Cash and Cash Equivalents",
"root_type": "Asset",
"description": "Cash on hand, demand deposits, and short-term highly liquid investments readily convertible to cash with original maturities of three months or less. Examples: Cash in hand, bank current accounts, money market funds, treasury bills \u22643 months."
},
{
"account_category_name": "Cost of Goods Sold",
"root_type": "Expense",
"description": "Direct costs attributable to cost of goods sold. Examples: Raw materials, stock in trade."
},
{
"account_category_name": "Current Tax Liabilities",
"root_type": "Liability",
"description": "Income tax obligations for current and prior periods. Examples: Provision for income tax, advance tax paid, tax deducted at source."
},
{
"account_category_name": "Finance Costs",
"root_type": "Expense",
"description": "Interest and financing-related expenses. Examples: Interest on borrowings, bank charges, lease interest, foreign exchange losses."
},
{
"account_category_name": "Intangible Assets",
"root_type": "Asset",
"description": "Identifiable non-monetary assets without physical substance. Examples: Software, patents, trademarks, licenses, development costs."
},
{
"account_category_name": "Investment Income",
"root_type": "Income",
"description": "Returns generated from financial investments and cash management. Examples: Interest income, dividend income, rental income, fair value gains."
},
{
"account_category_name": "Long-term Borrowings",
"root_type": "Liability",
"description": "Interest-bearing debt obligations with maturity beyond one year. Examples: Term loans, bonds, debentures, mortgages."
},
{
"account_category_name": "Long-term Investments",
"root_type": "Asset",
"description": "Investments held for strategic purposes or extended periods. Examples: Equity investments, bonds, associates, joint ventures, deposits."
},
{
"account_category_name": "Long-term Provisions",
"root_type": "Liability",
"description": "Present obligations beyond one year with uncertain timing/amount. Examples: Asset retirement obligations, environmental remediation, legal settlements."
},
{
"account_category_name": "Operating Expenses",
"root_type": "Expense",
"description": "Costs incurred in ordinary business operations excluding direct costs. Examples: Selling expenses, administrative costs, marketing, utilities, rent."
},
{
"account_category_name": "Other Current Assets",
"root_type": "Asset",
"description": "Current assets not classified elsewhere including prepaid expenses and advances. Examples: Prepaid insurance, prepaid rent, advance to suppliers, security deposits recoverable within one year."
},
{
"account_category_name": "Other Current Liabilities",
"root_type": "Liability",
"description": "Short-term obligations not classified elsewhere. Examples: Accrued expenses, statutory liabilities, employee payables."
},
{
"account_category_name": "Other Direct Costs",
"root_type": "Expense",
"description": "Direct costs excluding cost of goods sold. Examples: Direct labor, manufacturing overhead, freight inward."
},
{
"account_category_name": "Other Non-current Assets",
"root_type": "Asset",
"description": "Long-term assets not classified elsewhere. Examples: Security deposits, long-term prepayments, advances for capital goods."
},
{
"account_category_name": "Other Non-current Liabilities",
"root_type": "Liability",
"description": "Long-term obligations not classified elsewhere. Examples: Long-term deposits, deferred income, government grants."
},
{
"account_category_name": "Other Operating Income",
"root_type": "Income",
"description": "Incidental income related to business operations but not core revenue. Examples: Scrap sales, government grants, insurance claims, foreign exchange gains."
},
{
"account_category_name": "Other Payables",
"root_type": "Liability",
"description": "Non-trade payables and obligations to parties other than suppliers. Examples: Employee payables, accrued expenses, customer advances, security deposits received."
},
{
"account_category_name": "Other Receivables",
"root_type": "Asset",
"description": "Non-trade amounts due to the entity excluding financing arrangements. Examples: Employee advances, insurance claims, tax refunds, deposits recoverable."
},
{
"account_category_name": "Reserves and Surplus",
"root_type": "Equity",
"description": "Accumulated profits and other reserves created from profits or share premium. Examples: General reserves, retained earnings, statutory reserves, share premium."
},
{
"account_category_name": "Revenue from Operations",
"root_type": "Income",
"description": "Income from primary business activities in ordinary course. Examples: Sales of goods, service revenue, commission income, royalty income."
},
{
"account_category_name": "Share Capital",
"root_type": "Equity",
"description": "Nominal value of issued and paid-up equity shares. Examples: Common stock, ordinary shares, preference shares."
},
{
"account_category_name": "Short-term Borrowings",
"root_type": "Liability",
"description": "Interest-bearing debt obligations due within one year. Examples: Bank overdrafts, short-term loans, current portion of long-term debt."
},
{
"account_category_name": "Short-term Investments",
"root_type": "Asset",
"description": "Financial instruments held for short-term investment purposes, readily convertible to cash. Examples: Marketable securities, fixed deposits >3 months, mutual funds."
},
{
"account_category_name": "Short-term Provisions",
"root_type": "Liability",
"description": "Present obligations due within one year with uncertain timing or amount. Examples: Warranty provisions, legal claims, restructuring costs."
},
{
"account_category_name": "Stock Assets",
"root_type": "Asset",
"description": "Inventory and stock-related assets including raw materials, work in progress, finished goods, and stock in trade. Examples: Raw materials, finished goods, trading merchandise, consumables."
},
{
"account_category_name": "Tangible Assets",
"root_type": "Asset",
"description": "Physical assets used in business operations including property, plant, and equipment. Examples: Land, buildings, machinery, equipment, vehicles, furniture, capital work in progress."
},
{
"account_category_name": "Tax Expense",
"root_type": "Expense",
"description": "Current and deferred income tax obligations. Examples: Current tax provision, deferred tax expense, withholding taxes."
},
{
"account_category_name": "Trade Payables",
"root_type": "Liability",
"description": "Amounts owed to suppliers. Examples: Supplier invoices, accrued purchases, bills payable."
},
{
"account_category_name": "Trade Receivables",
"root_type": "Asset",
"description": "Amounts due from customers for goods sold or services provided in ordinary course of business. Examples: Accounts receivable, notes receivable from customers, unbilled revenue."
}
]
]

View File

@@ -36,7 +36,8 @@ def make_gl_entries(
):
if gl_map:
if (
not cint(frappe.get_single_value("Accounts Settings", "use_legacy_budget_controller"))
not cancel
and not cint(frappe.get_single_value("Accounts Settings", "use_legacy_budget_controller"))
and gl_map[0].voucher_type != "Period Closing Voucher"
):
bud_val = BudgetValidation(gl_map=gl_map)

View File

@@ -10,9 +10,6 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestAccountBalance(ERPNextTestSuite):
def test_account_balance(self):
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
filters = {
"company": "_Test Company 2",
"report_date": getdate(),

View File

@@ -34,6 +34,17 @@ frappe.query_reports["Accounts Payable"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_account",
label: __("Payable Account"),

View File

@@ -117,3 +117,49 @@ class TestAccountsPayable(ERPNextTestSuite, AccountsTestMixin):
self.assertEqual(len(report[1]), 2)
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
pi.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"project": [project.name],
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
row = report[0]
self.assertEqual(row.project, project.name)
self.assertEqual(row.invoiced, 300.0)
def test_project_on_report_output(self):
"""
Report row must carry the invoice's project.
"""
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
}
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AP Project Output", "company": self.company}
).insert()
pi = self.create_purchase_invoice(do_not_submit=True)
pi.project = project.name
pi.save().submit()
report = execute(filters)
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual([pi.name, project.name, 300], [row.voucher_no, row.project, row.outstanding])

View File

@@ -53,6 +53,17 @@ frappe.query_reports["Accounts Payable Summary"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -36,6 +36,17 @@ frappe.query_reports["Accounts Receivable"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -196,6 +196,7 @@ class ReceivablePayableReport:
and ple.against_voucher_type in self.advance_payment_doctypes
):
self.voucher_balance[key].cost_center = ple.cost_center
self.voucher_balance[key].project = ple.project
self.get_invoices(ple)
@@ -362,6 +363,7 @@ class ReceivablePayableReport:
posting_date,
account_currency,
cost_center,
project,
sum(invoiced) `invoiced`,
sum(paid) `paid`,
sum(credit_note) `credit_note`,
@@ -390,6 +392,7 @@ class ReceivablePayableReport:
"credit_note_in_account_currency",
"outstanding_in_account_currency",
"cost_center",
"project",
]:
_d[field] = x.get(field)
@@ -931,6 +934,7 @@ class ReceivablePayableReport:
ple.against_voucher_no,
ple.party_type,
ple.cost_center,
ple.project,
ple.party,
ple.posting_date,
ple.due_date,
@@ -998,6 +1002,9 @@ class ReceivablePayableReport:
if self.filters.cost_center:
self.get_cost_center_conditions()
if self.filters.project:
self.qb_selection_filter.append(self.ple.project.isin(self.filters.project))
self.add_accounting_dimensions_filters()
def get_cost_center_conditions(self):
@@ -1237,6 +1244,7 @@ class ReceivablePayableReport:
)
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
self.add_column(label=_("Project"), fieldname="project", fieldtype="Link", options="Project")
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")
self.add_column(
label=_("Voucher No"),
@@ -1413,6 +1421,7 @@ class InitSQLProceduresForAR:
posting_date date,
account_currency {_varchar_type},
cost_center {_varchar_type},
project {_varchar_type},
invoiced {_currency_type},
paid {_currency_type},
credit_note {_currency_type},
@@ -1432,6 +1441,7 @@ class InitSQLProceduresForAR:
against_voucher_no {_varchar_type},
party_type {_varchar_type},
cost_center {_varchar_type},
project {_varchar_type},
party {_varchar_type},
posting_date date,
due_date date,
@@ -1447,7 +1457,7 @@ class InitSQLProceduresForAR:
begin
if not exists (select name from `{_voucher_balance_name}` where name = sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)))
then
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, 0, 0, 0, 0, 0, 0);
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)), ple.voucher_type, ple.voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency, ple.cost_center, ple.project, 0, 0, 0, 0, 0, 0);
end if;
end;
"""
@@ -1489,7 +1499,7 @@ class InitSQLProceduresForAR:
end if;
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.voucher_type, ple.voucher_no, ple.party)), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
insert into `{_voucher_balance_name}` values (sha1(concat_ws(',', ple.account, ple.voucher_type, ple.voucher_no, ple.party)), ple.against_voucher_type, ple.against_voucher_no, ple.party, ple.account, ple.posting_date, ple.account_currency,'', '', invoiced, paid, 0, invoiced_in_account_currency, paid_in_account_currency, 0);
end;
"""

View File

@@ -774,22 +774,18 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
def test_party_account_filter(self):
si1 = self.create_sales_invoice()
self.customer2 = (
frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
}
)
.insert()
.submit()
)
jane = frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
}
).insert()
self.customer = jane.name
si2 = self.create_sales_invoice(do_not_submit=True)
si2.posting_date = add_days(today(), -1)
si2.customer = self.customer2.name
si2.currency = "USD"
si2.conversion_rate = 80
si2.debit_to = self.debtors_usd
@@ -997,22 +993,18 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
self.assertEqual(expected_data, report_output)
def test_future_payments_on_foreign_currency(self):
self.customer2 = (
frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
}
)
.insert()
.submit()
)
jane = frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
}
).insert()
self.customer = jane.name
si = self.create_sales_invoice(do_not_submit=True)
si.posting_date = add_days(today(), -1)
si.customer = self.customer2.name
si.currency = "USD"
si.conversion_rate = 80
si.debit_to = self.debtors_usd
@@ -1204,3 +1196,52 @@ class TestAccountsReceivable(ERPNextTestSuite, AccountsTestMixin):
self.assertEqual(len(report[1]), 2)
self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
def test_project_filter(self):
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AR Project", "company": self.company}
).insert()
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.project = project.name
si.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
"project": [project.name],
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
row = report[0]
self.assertEqual(row.project, project.name)
self.assertEqual(row.invoiced, 100.0)
def test_project_on_report_output(self):
"""
Report row must carry the invoice's project even when the payment entry
has no project set.
"""
filters = {
"company": self.company,
"report_date": today(),
"range": "30, 60, 90, 120",
}
project = frappe.get_doc(
{"doctype": "Project", "project_name": "_Test AR Project Output", "company": self.company}
).insert()
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.project = project.name
si.save().submit()
# payment has no project — report row must still show the invoice's project
self.create_payment_entry(si.name)
report = execute(filters)
self.assertEqual(len(report[1]), 1)
row = report[1][0]
self.assertEqual([si.name, project.name, 60], [row.voucher_no, row.project, row.outstanding])

View File

@@ -53,6 +53,17 @@ frappe.query_reports["Accounts Receivable Summary"] = {
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "MultiSelectList",
options: "Project",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
},
{
fieldname: "party_type",
label: __("Party Type"),

View File

@@ -8,6 +8,7 @@ from frappe.utils import cint, flt
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
FinancialReportEngine,
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
)
from erpnext.accounts.report.financial_statements import (
compute_growth_view_data,

View File

@@ -13,9 +13,6 @@ COMPANY_SHORT_NAME = "_TC6"
class TestBalanceSheet(ERPNextTestSuite):
def test_balance_sheet(self):
frappe.db.sql(f"delete from `tabJournal Entry` where company='{COMPANY}'")
frappe.db.sql(f"delete from `tabGL Entry` where company='{COMPANY}'")
create_account("VAT Liabilities", f"Duties and Taxes - {COMPANY_SHORT_NAME}", COMPANY)
create_account("Advance VAT Paid", f"Duties and Taxes - {COMPANY_SHORT_NAME}", COMPANY)
create_account("My Bank", f"Bank Accounts - {COMPANY_SHORT_NAME}", COMPANY)

View File

@@ -12,6 +12,7 @@ from pypika import Order
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
FinancialReportEngine,
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
)
from erpnext.accounts.report.financial_statements import (
get_columns,

View File

@@ -3,6 +3,7 @@
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
FinancialReportEngine,
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
)

View File

@@ -774,7 +774,28 @@ class GrossProfitGenerator:
# IMP NOTE
# stock_ledger_entries should already be filtered by item_code and warehouse and
# sorted by posting_date desc, posting_time desc
if item_code in self.non_stock_items and (row.project or row.cost_center):
if (
row.delivered_by_supplier
and row.so_detail
and (
po_details := frappe.get_all(
"Purchase Order Item",
filters={"sales_order_item": row.so_detail, "docstatus": 1},
pluck="name",
)
)
):
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(Sum(table.qty * table.base_net_rate))
.where((table.po_detail.isin(po_details)) & (table.docstatus == 1))
)
return flt(query.run()[0][0])
elif item_code in self.non_stock_items and (row.project or row.cost_center):
# Issue 6089-Get last purchasing rate for non-stock item
item_rate = self.get_last_purchase_rate(item_code, row)
return flt(row.qty) * item_rate
@@ -804,26 +825,6 @@ class GrossProfitGenerator:
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)
elif (
row.delivered_by_supplier
and row.so_detail
and (
po_details := frappe.get_all(
"Purchase Order Item",
filters={"sales_order_item": row.so_detail, "docstatus": 1},
pluck="name",
)
)
):
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(Sum(table.stock_qty * table.base_net_rate))
.where((table.po_detail.isin(po_details)) & (table.docstatus == 1))
)
return flt(query.run()[0][0])
elif row.sales_order and row.so_detail:
incoming_amount = self.get_buying_amount_from_so_dn(row.sales_order, row.so_detail, item_code)
if incoming_amount:

View File

@@ -8,6 +8,7 @@ from frappe.utils import flt
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
FinancialReportEngine,
get_xlsx_styles, #! DO NOT REMOVE - hook for styling
)
from erpnext.accounts.report.financial_statements import (
compute_growth_view_data,

View File

@@ -499,7 +499,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts, inc
else sum(base_tax_amount_after_discount_amount) * -1 end as tax_amount
from `tabPurchase Taxes and Charges`
where parent in (%s) and category in ('Total', 'Valuation and Total')
and base_tax_amount_after_discount_amount != 0
and base_tax_amount_after_discount_amount != 0 and parenttype='Purchase Invoice'
group by parent, account_head, add_deduct_tax
"""
% ", ".join(["%s"] * len(invoice_list)),

View File

@@ -5,14 +5,12 @@ import frappe
from frappe.utils import add_months, today
from erpnext.accounts.report.purchase_register.purchase_register import execute
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.tests.utils import ERPNextTestSuite
class TestPurchaseRegister(ERPNextTestSuite):
def test_purchase_register(self):
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
filters = frappe._dict(company="_Test Company 6", from_date=add_months(today(), -1), to_date=today())
pi = make_purchase_invoice()
@@ -26,10 +24,50 @@ class TestPurchaseRegister(ERPNextTestSuite):
self.assertEqual(first_row.total_tax, 100)
self.assertEqual(first_row.grand_total, 1100)
def test_purchase_register_ledger_view(self):
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
def test_purchase_register_ignores_tax_rows_from_other_doctype(self):
filters = frappe._dict(company="_Test Company 6", from_date=add_months(today(), -1), to_date=today())
pi = make_purchase_invoice()
# Real workflow setup: create a Purchase Receipt tax row in the same shared child table.
pr = make_purchase_receipt(
company="_Test Company 6",
supplier="_Test Supplier",
item="_Test Item",
warehouse="_Test Warehouse - _TC6",
cost_center="_Test Cost Center - _TC6",
do_not_save=1,
do_not_submit=1,
qty=1,
rate=1000,
)
pr.append(
"taxes",
{
"account_head": "GST - _TC6",
"cost_center": "_Test Cost Center - _TC6",
"add_deduct_tax": "Add",
"category": "Valuation and Total",
"charge_type": "Actual",
"description": "PR Tax",
"tax_amount": 100.0,
"rate": 100,
},
)
pr.insert()
pr.submit()
# Mimic custom naming collision across doctypes (same parent value in shared child table).
frappe.rename_doc("Purchase Receipt", pr.name, pi.name, force=True)
report_results = execute(filters)
first_row = frappe._dict(report_results[1][0])
self.assertEqual(first_row.voucher_no, pi.name)
self.assertEqual(first_row.total_tax, 100)
self.assertEqual(first_row.grand_total, 1100)
def test_purchase_register_ledger_view(self):
filters = frappe._dict(
company="_Test Company 6",
from_date=add_months(today(), -1),

View File

@@ -4,6 +4,7 @@ from frappe.utils import getdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.sales_register.sales_register import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.tests.utils import ERPNextTestSuite
@@ -72,6 +73,43 @@ class TestItemWiseSalesRegister(ERPNextTestSuite, AccountsTestMixin):
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
self.assertDictEqual(report_output, expected_result)
def test_sales_register_ignores_tax_rows_from_other_doctype(self):
si = self.create_sales_invoice(rate=98)
# Real workflow setup: create a Sales Order with taxes in the shared child table.
so = make_sales_order(
item=self.item,
company=self.company,
customer=self.customer,
rate=77,
do_not_save=1,
do_not_submit=1,
)
so.append(
"taxes",
{
"charge_type": "Actual",
"account_head": self.income_account,
"description": "SO Tax",
"tax_amount": 55.0,
},
)
so.insert()
so.submit()
# Mimic custom naming collision across doctypes (same parent value in shared child table).
frappe.rename_doc("Sales Order", so.name, si.name, force=True)
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
report = execute(filters)
self.assertEqual(len(report[1]), 1)
result = frappe._dict(report[1][0])
self.assertEqual(result.voucher_no, si.name)
self.assertEqual(result.net_total, 98.0)
self.assertEqual(result.tax_total, 0)
self.assertEqual(result.grand_total, 98.0)
def test_journal_with_cost_center_filter(self):
je1 = frappe.get_doc(
{

View File

@@ -6,299 +6,282 @@ from frappe import _
from frappe.query_builder.functions import IfNull
def execute(filters=None):
"""Generate Tax Withholding Details report"""
validate_filters(filters)
class TaxWithholdingDetailsReport:
party_types = ("Customer", "Supplier")
document_types = ("Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry")
# Process and format data
data = get_tax_withholding_data(filters)
columns = get_columns(filters)
def __init__(self, filters=None):
self.filters = frappe._dict(filters or {})
self.entries = []
self.doc_info = {}
self.party_details = {}
return columns, data
@classmethod
def execute(cls, filters=None):
return cls(filters).run()
def run(self):
self.validate_filters()
return self.get_columns(), self.get_data()
def validate_filters(filters):
"""Validate report filters"""
filters = frappe._dict(filters or {})
def validate_filters(self):
if not self.filters.from_date or not self.filters.to_date:
frappe.throw(_("From Date and To Date are required"))
if not filters.from_date or not filters.to_date:
frappe.throw(_("From Date and To Date are required"))
if self.filters.from_date > self.filters.to_date:
frappe.throw(_("From Date must be before To Date"))
if filters.from_date > filters.to_date:
frappe.throw(_("From Date must be before To Date"))
def get_data(self):
self.entries = self.get_entries_query().run(as_dict=True)
if not self.entries:
return []
self.doc_info = self.fetch_additional_doc_info()
self.party_details = self.fetch_party_details()
return self.build_rows()
def get_tax_withholding_data(filters):
"""Process entries into final report format"""
data = []
entries = get_tax_withholding_entries(filters)
if not entries:
return data
def build_rows(self):
rows = []
for entry in self.entries:
doc_details = (
self.doc_info.get((entry.transaction_type, entry.ref_no), {}) if entry.ref_no else {}
)
party_info = self.party_details.get((entry.party_type, entry.party), {})
rows.append({**entry, **doc_details, **party_info})
doc_info = get_additional_doc_info(entries)
party_details = get_party_details(entries)
rows.sort(
key=lambda x: (
x["tax_withholding_category"] or "",
x["transaction_date"] or "",
x["withholding_name"] or "",
)
)
return rows
for entry in entries:
doc_details = frappe._dict()
if entry.taxable_name:
doc_details = doc_info.get((entry.taxable_doctype, entry.taxable_name), {})
def get_entries_query(self):
twe = frappe.qb.DocType("Tax Withholding Entry")
query = (
frappe.qb.from_(twe)
.select(
twe.party_type,
twe.party,
IfNull(twe.tax_id, "").as_("tax_id"),
twe.tax_withholding_category,
twe.taxable_amount.as_("total_amount"),
twe.tax_rate.as_("rate"),
twe.withholding_amount.as_("tax_amount"),
IfNull(twe.taxable_doctype, "").as_("transaction_type"),
IfNull(twe.taxable_name, "").as_("ref_no"),
twe.taxable_date,
IfNull(twe.withholding_doctype, "").as_("withholding_doctype"),
IfNull(twe.withholding_name, "").as_("withholding_name"),
twe.withholding_date.as_("transaction_date"),
)
.where(twe.docstatus == 1)
.where(twe.withholding_date >= self.filters.from_date)
.where(twe.withholding_date <= self.filters.to_date)
.where(IfNull(twe.withholding_name, "") != "")
.where(twe.status != "Duplicate")
)
party_info = party_details.get((entry.party_type, entry.party), {})
if self.filters.company:
query = query.where(twe.company == self.filters.company)
if self.filters.party_type:
query = query.where(twe.party_type == self.filters.party_type)
if self.filters.party:
query = query.where(twe.party == self.filters.party)
row = {
"section_code": entry.tax_withholding_category,
"entity_type": party_info.get("entity_type"),
"rate": entry.tax_rate,
"total_amount": entry.taxable_amount,
"grand_total": doc_details.get("grand_total", 0),
"base_total": doc_details.get("base_total", 0),
"tax_amount": entry.withholding_amount,
"transaction_date": entry.withholding_date,
"transaction_type": entry.taxable_doctype,
"ref_no": entry.taxable_name,
"taxable_date": entry.taxable_date,
"supplier_invoice_no": doc_details.get("bill_no"),
"supplier_invoice_date": doc_details.get("bill_date"),
"withholding_doctype": entry.withholding_doctype,
"withholding_name": entry.withholding_name,
"party_name": party_info.get("party_name"),
"tax_id": entry.tax_id,
"party": entry.party,
"party_type": entry.party_type,
}
data.append(row)
return query
# Sort by section code and transaction date
data.sort(key=lambda x: (x["section_code"] or "", x["transaction_date"] or ""))
return data
def fetch_party_details(self):
parties_by_type = {pt: set() for pt in self.party_types}
for entry in self.entries:
if entry.party_type in parties_by_type and entry.party:
parties_by_type[entry.party_type].add(entry.party)
party_map = {}
for party_type, party_set in parties_by_type.items():
if not party_set:
continue
def get_party_details(entries):
"""Fetch party details in batch for all entries"""
party_map = frappe._dict()
parties_by_type = {"Customer": set(), "Supplier": set()}
query = self.get_party_query(party_type, party_set)
if query is None:
continue
# Group parties by type
for entry in entries:
if entry.party_type in parties_by_type and entry.party:
parties_by_type[entry.party_type].add(entry.party)
for row in query.run(as_dict=True):
party_map[(party_type, row.pop("name"))] = row
# Batch fetch for each party type
for party_type, party_set in parties_by_type.items():
if not party_type or not party_set:
continue
return party_map
def get_party_query(self, party_type, party_set):
doctype = frappe.qb.DocType(party_type)
fields = [doctype.name]
if party_type == "Supplier":
fields.extend([doctype.supplier_type.as_("entity_type"), doctype.supplier_name.as_("party_name")])
fields.extend(
[
doctype.supplier_type.as_("party_entity_type"),
doctype.supplier_name.as_("party_name"),
]
)
elif party_type == "Customer":
fields.extend([doctype.customer_type.as_("entity_type"), doctype.customer_name.as_("party_name")])
fields.extend(
[
doctype.customer_type.as_("party_entity_type"),
doctype.customer_name.as_("party_name"),
]
)
else:
return None
query = frappe.qb.from_(doctype).select(*fields).where(doctype.name.isin(party_set))
party_details = query.run(as_dict=True)
return frappe.qb.from_(doctype).select(*fields).where(doctype.name.isin(party_set))
for party in party_details:
party_map[(party_type, party.name)] = party
def fetch_additional_doc_info(self):
docs_by_type = {dt: set() for dt in self.document_types}
for entry in self.entries:
if entry.ref_no and entry.transaction_type in docs_by_type:
docs_by_type[entry.transaction_type].add(entry.ref_no)
return party_map
doc_info = {}
for doctype_name, voucher_set in docs_by_type.items():
if not voucher_set:
continue
query = self.get_doc_info_query(doctype_name, voucher_set)
if query is None:
continue
for row in query.run(as_dict=True):
doc_info[(doctype_name, row.pop("name"))] = row
return doc_info
def get_doc_info_query(self, doctype_name, voucher_set):
if doctype_name == "Purchase Invoice":
get_doc_fields = self.get_purchase_invoice_fields
elif doctype_name == "Sales Invoice":
get_doc_fields = self.get_sales_invoice_fields
elif doctype_name == "Payment Entry":
get_doc_fields = self.get_payment_entry_fields
elif doctype_name == "Journal Entry":
get_doc_fields = self.get_journal_entry_fields
else:
return None
doctype = frappe.qb.DocType(doctype_name)
fields = [doctype.name, *get_doc_fields(doctype)]
return frappe.qb.from_(doctype).select(*fields).where(doctype.name.isin(voucher_set))
def get_purchase_invoice_fields(self, doctype):
return [
doctype.grand_total,
doctype.base_total,
doctype.bill_no.as_("supplier_invoice_no"),
doctype.bill_date.as_("supplier_invoice_date"),
]
def get_sales_invoice_fields(self, doctype):
return [doctype.grand_total, doctype.base_total]
def get_payment_entry_fields(self, doctype):
return [
doctype.paid_amount_after_tax.as_("grand_total"),
doctype.base_paid_amount.as_("base_total"),
]
def get_journal_entry_fields(self, doctype):
return [doctype.total_debit.as_("grand_total"), doctype.total_debit.as_("base_total")]
def get_columns(self):
party_type = self.filters.get("party_type", "Party")
return [
{
"label": _("Tax Withholding Category"),
"options": "Tax Withholding Category",
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"width": 90,
},
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 60},
{
"label": _(f"{party_type} Name"),
"fieldname": "party_name",
"fieldtype": "Data",
"width": 180,
},
{
"label": _(party_type),
"fieldname": "party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 180,
},
{
"label": _(f"{party_type} Type"),
"fieldname": "party_entity_type",
"fieldtype": "Data",
"width": 100,
},
{
"label": _("Supplier Invoice No"),
"fieldname": "supplier_invoice_no",
"fieldtype": "Data",
"width": 120,
},
{
"label": _("Supplier Invoice Date"),
"fieldname": "supplier_invoice_date",
"fieldtype": "Date",
"width": 120,
},
{"label": _("Tax Rate %"), "fieldname": "rate", "fieldtype": "Percent", "width": 60},
{
"label": _("Taxable Amount"),
"fieldname": "total_amount",
"fieldtype": "Currency",
"width": 120,
},
{"label": _("Tax Amount"), "fieldname": "tax_amount", "fieldtype": "Currency", "width": 120},
{
"label": _("Grand Total (Company Currency)"),
"fieldname": "base_total",
"fieldtype": "Currency",
"width": 150,
},
{
"label": _("Grand Total (Transaction Currency)"),
"fieldname": "grand_total",
"fieldtype": "Currency",
"width": 170,
},
{"label": _("Reference Date"), "fieldname": "taxable_date", "fieldtype": "Date", "width": 100},
{
"label": _("Transaction Type"),
"fieldname": "transaction_type",
"fieldtype": "Data",
"width": 130,
},
{
"label": _("Reference No."),
"fieldname": "ref_no",
"fieldtype": "Dynamic Link",
"options": "transaction_type",
"width": 180,
},
{
"label": _("Date of Transaction"),
"fieldname": "transaction_date",
"fieldtype": "Date",
"width": 100,
},
{
"label": _("Withholding Document"),
"fieldname": "withholding_name",
"fieldtype": "Dynamic Link",
"options": "withholding_doctype",
"width": 150,
},
]
def get_columns(filters):
"""Generate report columns based on filters"""
columns = [
{
"label": _("Section Code"),
"options": "Tax Withholding Category",
"fieldname": "section_code",
"fieldtype": "Link",
"width": 90,
},
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 60},
{
"label": _(f"{filters.get('party_type', 'Party')} Name"),
"fieldname": "party_name",
"fieldtype": "Data",
"width": 180,
},
{
"label": _(filters.get("party_type", "Party")),
"fieldname": "party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 180,
},
{
"label": _("Entity Type"),
"fieldname": "entity_type",
"fieldtype": "Data",
"width": 100,
},
{
"label": _("Supplier Invoice No"),
"fieldname": "supplier_invoice_no",
"fieldtype": "Data",
"width": 120,
},
{
"label": _("Supplier Invoice Date"),
"fieldname": "supplier_invoice_date",
"fieldtype": "Date",
"width": 120,
},
{
"label": _("Tax Rate %"),
"fieldname": "rate",
"fieldtype": "Percent",
"width": 60,
},
{
"label": _("Taxable Amount"),
"fieldname": "total_amount",
"fieldtype": "Currency",
"width": 120,
},
{
"label": _("Tax Amount"),
"fieldname": "tax_amount",
"fieldtype": "Currency",
"width": 120,
},
{
"label": _("Grand Total (Company Currency)"),
"fieldname": "base_total",
"fieldtype": "Currency",
"width": 150,
},
{
"label": _("Grand Total (Transaction Currency)"),
"fieldname": "grand_total",
"fieldtype": "Currency",
"width": 170,
},
{
"label": _("Reference Date"),
"fieldname": "taxable_date",
"fieldtype": "Date",
"width": 100,
},
{
"label": _("Transaction Type"),
"fieldname": "transaction_type",
"fieldtype": "Data",
"width": 130,
},
{
"label": _("Reference No."),
"fieldname": "ref_no",
"fieldtype": "Dynamic Link",
"options": "transaction_type",
"width": 180,
},
{
"label": _("Date of Transaction"),
"fieldname": "transaction_date",
"fieldtype": "Date",
"width": 100,
},
{
"label": _("Withholding Document"),
"fieldname": "withholding_name",
"fieldtype": "Dynamic Link",
"options": "withholding_doctype",
"width": 150,
},
]
return columns
def get_tax_withholding_entries(filters):
twe = frappe.qb.DocType("Tax Withholding Entry")
query = (
frappe.qb.from_(twe)
.select(
twe.company,
twe.party_type,
twe.party,
IfNull(twe.tax_id, "").as_("tax_id"),
twe.tax_withholding_category,
IfNull(twe.tax_withholding_group, "").as_("tax_withholding_group"),
twe.taxable_amount,
twe.tax_rate,
twe.withholding_amount,
IfNull(twe.taxable_doctype, "").as_("taxable_doctype"),
IfNull(twe.taxable_name, "").as_("taxable_name"),
twe.taxable_date,
IfNull(twe.under_withheld_reason, "").as_("under_withheld_reason"),
IfNull(twe.lower_deduction_certificate, "").as_("lower_deduction_certificate"),
IfNull(twe.withholding_doctype, "").as_("withholding_doctype"),
IfNull(twe.withholding_name, "").as_("withholding_name"),
twe.withholding_date,
twe.status,
)
.where(twe.docstatus == 1)
.where(twe.withholding_date >= filters.from_date)
.where(twe.withholding_date <= filters.to_date)
.where(IfNull(twe.withholding_name, "") != "")
.where(twe.status != "Duplicate")
)
if filters.get("company"):
query = query.where(twe.company == filters.get("company"))
if filters.get("party_type"):
query = query.where(twe.party_type == filters.get("party_type"))
if filters.get("party"):
query = query.where(twe.party == filters.get("party"))
return query.run(as_dict=True)
def get_additional_doc_info(entries):
"""Fetch additional document information in batch"""
doc_info = {}
docs_by_type = {
"Purchase Invoice": set(),
"Sales Invoice": set(),
"Payment Entry": set(),
"Journal Entry": set(),
}
# Group documents by type
for entry in entries:
if entry.taxable_name and entry.taxable_doctype in docs_by_type:
docs_by_type[entry.taxable_doctype].add(entry.taxable_name)
for doctype_name, voucher_set in docs_by_type.items():
if voucher_set:
_fetch_doc_info(doctype_name, voucher_set, doc_info)
return doc_info
def _fetch_doc_info(doctype_name, voucher_set, doc_info):
doctype = frappe.qb.DocType(doctype_name)
fields = [doctype.name]
# Add doctype-specific fields
if doctype_name == "Purchase Invoice":
fields.extend([doctype.grand_total, doctype.base_total, doctype.bill_no, doctype.bill_date])
elif doctype_name == "Sales Invoice":
fields.extend([doctype.grand_total, doctype.base_total])
elif doctype_name == "Payment Entry":
fields.extend(
[doctype.paid_amount_after_tax.as_("grand_total"), doctype.base_paid_amount.as_("base_total")]
)
elif doctype_name == "Journal Entry":
fields.extend([doctype.total_debit.as_("grand_total"), doctype.total_debit.as_("base_total")])
else:
return
query = frappe.qb.from_(doctype).select(*fields).where(doctype.name.isin(voucher_set))
entries = query.run(as_dict=True)
for entry in entries:
doc_info[(doctype_name, entry.name)] = entry
execute = TaxWithholdingDetailsReport.execute

View File

@@ -5,10 +5,12 @@ import frappe
from frappe.utils import add_to_date, today
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.tax_withholding_category.test_tax_withholding_category import (
create_purchase_invoice,
create_records,
create_sales_invoice,
create_tax_withholding_category,
make_journal_entry_with_tax_withholding,
)
from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
@@ -20,43 +22,45 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
def setUp(self):
self.create_company()
self.clear_old_entries()
create_tax_accounts()
create_records()
def test_tax_withholding_for_customers(self):
create_tax_category(cumulative_threshold=300)
frappe.db.set_value("Customer", "_Test Customer", "tax_withholding_category", "TCS")
si = create_sales_invoice(rate=1000)
pe = create_tcs_payment_entry()
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "TCS")
si = create_sales_invoice(customer="Test TCS Customer", rate=1000)
si.submit()
create_tcs_payment_entry()
jv = create_tcs_journal_entry()
filters = frappe._dict(
company="_Test Company", party_type="Customer", from_date=today(), to_date=today()
)
result = execute(filters)[1]
expected_values = [
# Check for JV totals using back calculation logic
[jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
[si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
[jv.name, "TCS", 0.075, 1000.75, 0.75, 1000.75],
["", "TCS", 0.075, None, 0.75, None],
[si.name, "TCS", 0.075, 1000.0, 0.75, 1000.75],
]
self.check_expected_values(result, expected_values)
def test_single_account_for_multiple_categories(self):
create_tax_category("TDS - 1", rate=10, account="TDS - _TC")
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
inv_1.tax_withholding_category = "TDS - 1"
create_tax_category("TDS - 1", rate=10, account="TDS - _TC", cumulative_threshold=1)
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "TDS - 1")
inv_1 = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
inv_1.submit()
create_tax_category("TDS - 2", rate=20, account="TDS - _TC")
inv_2 = make_purchase_invoice(rate=1000, do_not_submit=True)
inv_2.tax_withholding_category = "TDS - 2"
create_tax_category("TDS - 2", rate=20, account="TDS - _TC", cumulative_threshold=1)
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "TDS - 2")
inv_2 = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000)
inv_2.submit()
result = execute(
frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today())
)[1]
expected_values = [
[inv_1.name, "TDS - 1", 10, 5000, 500, 5500],
[inv_2.name, "TDS - 2", 20, 5000, 1000, 6000],
[inv_1.name, "TDS - 1", 10, 5000, 500, 4500],
[inv_2.name, "TDS - 2", 20, 5000, 1000, 4000],
]
self.check_expected_values(result, expected_values)
@@ -81,20 +85,21 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
tds_doc.save()
inv_1 = make_purchase_invoice(
rate=1000, posting_date=add_to_date(fiscal_year[1], days=1), do_not_save=True, do_not_submit=True
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", tds_doc.name)
inv_1 = create_purchase_invoice(
supplier="Test TDS Supplier",
rate=5000,
posting_date=add_to_date(fiscal_year[1], days=1),
set_posting_time=True,
)
inv_1.set_posting_time = 1
inv_1.apply_tds = 1
inv_1.tax_withholding_category = tds_doc.name
inv_1.save()
inv_1.submit()
inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True)
inv_2.set_posting_time = 1
inv_2.apply_tds = 1
inv_2.tax_withholding_category = tds_doc.name
inv_2.save()
inv_2 = create_purchase_invoice(
supplier="Test TDS Supplier",
rate=5000,
posting_date=from_date,
set_posting_time=True,
)
inv_2.submit()
result = execute(
@@ -113,12 +118,13 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
self.check_expected_values(result, expected_values)
def check_expected_values(self, result, expected_values):
self.assertEqual(len(result), len(expected_values))
for i in range(len(result)):
voucher = frappe._dict(result[i])
voucher_expected_values = expected_values[i]
voucher_actual_values = (
voucher.ref_no,
voucher.section_code,
voucher.tax_withholding_category,
voucher.rate,
voucher.base_total,
voucher.tax_amount,
@@ -127,21 +133,6 @@ class TestTaxWithholdingDetails(ERPNextTestSuite, AccountsTestMixin):
self.assertSequenceEqual(voucher_actual_values, voucher_expected_values)
def create_tax_accounts():
account_names = ["TCS", "TDS"]
for account in account_names:
frappe.get_doc(
{
"doctype": "Account",
"company": "_Test Company",
"account_name": account,
"parent_account": "Duties and Taxes - _TC",
"report_type": "Balance Sheet",
"root_type": "Liability",
}
).insert(ignore_if_duplicate=True)
def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulative_threshold=0):
fiscal_year = get_fiscal_year(today(), company="_Test Company")
from_date = fiscal_year[1]
@@ -157,55 +148,34 @@ def create_tax_category(category="TCS", rate=0.075, account="TCS - _TC", cumulat
)
def create_tcs_payment_entry():
def create_tcs_payment_entry(party="Test TCS Customer", category="TCS", amount=1000):
"""Create a TCS Payment Entry that generates a Tax Withholding Entry (Over Withheld)."""
payment_entry = create_payment_entry(
payment_type="Receive",
party_type="Customer",
party="_Test Customer",
party=party,
paid_from="Debtors - _TC",
paid_to="Cash - _TC",
paid_amount=2550,
)
payment_entry.append(
"taxes",
{
"account_head": "TCS - _TC",
"charge_type": "Actual",
"tax_amount": 0.53,
"add_deduct_tax": "Add",
"description": "Test",
"cost_center": "Main - _TC",
},
paid_amount=amount,
)
payment_entry.apply_tds = 1
payment_entry.tax_withholding_category = category
payment_entry.save()
payment_entry.submit()
return payment_entry
def create_tcs_journal_entry():
jv = frappe.new_doc("Journal Entry")
jv.posting_date = today()
jv.company = "_Test Company"
jv.set(
"accounts",
[
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": "_Test Customer",
"credit_in_account_currency": 10000,
},
{
"account": "Debtors - _TC",
"party_type": "Customer",
"party": "_Test Customer",
"debit_in_account_currency": 9992.5,
},
{
"account": "TCS - _TC",
"debit_in_account_currency": 7.5,
},
],
def create_tcs_journal_entry(party="Test TCS Customer", category="TCS", amount=1000):
"""Create a TCS Credit Note Journal Entry that generates a Tax Withholding Entry."""
jv = make_journal_entry_with_tax_withholding(
party_type="Customer",
party=party,
voucher_type="Credit Note",
amount=amount,
save=False,
)
jv.insert()
return jv.submit()
jv.apply_tds = 1
jv.tax_withholding_category = category
jv.save()
jv.submit()
return jv

View File

@@ -2,121 +2,94 @@ import frappe
from frappe import _
from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import (
get_tax_withholding_data,
TaxWithholdingDetailsReport,
)
from erpnext.accounts.utils import get_fiscal_year
def execute(filters=None):
validate_filters(filters)
class TDSComputationSummaryReport(TaxWithholdingDetailsReport):
GROUP_BY_FIELDS = ("party_type", "party", "tax_withholding_category")
CARRY_OVER_FIELDS = (
"tax_id",
"party",
"party_type",
"party_name",
"tax_withholding_category",
"party_entity_type",
"rate",
)
AGGREGATE_FIELDS = ("total_amount", "tax_amount")
data = get_tax_withholding_data(filters)
columns = get_columns(filters)
def validate_filters(self):
if self.filters.from_date > self.filters.to_date:
frappe.throw(_("From Date must be before To Date"))
final_result = group_by_party_and_category(data, filters)
from_year = get_fiscal_year(self.filters.from_date)[0]
to_year = get_fiscal_year(self.filters.to_date)[0]
if from_year != to_year:
frappe.throw(_("From Date and To Date lie in different Fiscal Year"))
return columns, final_result
self.filters.fiscal_year = from_year
def get_data(self):
return self.group_rows(super().get_data())
def validate_filters(filters):
"""Validate if dates are properly set and lie in the same fiscal year"""
if filters.from_date > filters.to_date:
frappe.throw(_("From Date must be before To Date"))
def group_rows(self, data):
grouped = {}
for row in data:
key = tuple(row.get(f) for f in self.GROUP_BY_FIELDS)
bucket = grouped.setdefault(
key,
{
**{f: row.get(f) for f in self.CARRY_OVER_FIELDS},
**{f: 0.0 for f in self.AGGREGATE_FIELDS},
},
)
from_year = get_fiscal_year(filters.from_date)[0]
to_year = get_fiscal_year(filters.to_date)[0]
if from_year != to_year:
frappe.throw(_("From Date and To Date lie in different Fiscal Year"))
for f in self.AGGREGATE_FIELDS:
bucket[f] += row.get(f) or 0.0
filters["fiscal_year"] = from_year
return list(grouped.values())
def group_by_party_and_category(data, filters):
party_category_wise_map = {}
for row in data:
party_category_wise_map.setdefault(
(row.get("party"), row.get("section_code")),
def get_columns(self):
party_type = self.filters.get("party_type", "Party")
return [
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 90},
{
"tax_id": row.get("tax_id"),
"party": row.get("party"),
"party_name": row.get("party_name"),
"section_code": row.get("section_code"),
"entity_type": row.get("entity_type"),
"rate": row.get("rate"),
"total_amount": 0.0,
"tax_amount": 0.0,
"label": _(party_type),
"fieldname": "party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 180,
},
)
party_category_wise_map.get((row.get("party"), row.get("section_code")))["total_amount"] += row.get(
"total_amount", 0.0
)
party_category_wise_map.get((row.get("party"), row.get("section_code")))["tax_amount"] += row.get(
"tax_amount", 0.0
)
final_result = get_final_result(party_category_wise_map)
return final_result
{
"label": _(f"{party_type} Name"),
"fieldname": "party_name",
"fieldtype": "Data",
"width": 180,
},
{
"label": _("Tax Withholding Category"),
"options": "Tax Withholding Category",
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"width": 180,
},
{
"label": _(f"{party_type} Type"),
"fieldname": "party_entity_type",
"fieldtype": "Data",
"width": 180,
},
{"label": _("Tax Rate %"), "fieldname": "rate", "fieldtype": "Percent", "width": 120},
{
"label": _("Total Taxable Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 120,
},
{"label": _("Tax Amount"), "fieldname": "tax_amount", "fieldtype": "Float", "width": 120},
]
def get_final_result(party_category_wise_map):
out = []
for _key, value in party_category_wise_map.items():
out.append(value)
return out
def get_columns(filters):
columns = [
{"label": _("Tax Id"), "fieldname": "tax_id", "fieldtype": "Data", "width": 90},
{
"label": _(filters.get("party_type")),
"fieldname": "party",
"fieldtype": "Dynamic Link",
"options": "party_type",
"width": 180,
},
{
"label": _(f"{filters.get('party_type', 'Party')} Name"),
"fieldname": "party_name",
"fieldtype": "Data",
"width": 180,
},
{
"label": _("Section Code"),
"options": "Tax Withholding Category",
"fieldname": "section_code",
"fieldtype": "Link",
"width": 180,
},
{
"label": _("Entity Type"),
"fieldname": "entity_type",
"fieldtype": "Data",
"width": 180,
},
{
"label": _("Tax Rate %"),
"fieldname": "rate",
"fieldtype": "Percent",
"width": 120,
},
{
"label": _("Total Taxable Amount"),
"fieldname": "total_amount",
"fieldtype": "Float",
"width": 120,
},
{
"label": _("Tax Amount"),
"fieldname": "tax_amount",
"fieldtype": "Float",
"width": 120,
},
]
return columns
execute = TDSComputationSummaryReport.execute

View File

@@ -42,9 +42,6 @@ class TestTrialBalance(ERPNextTestSuite):
"""
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
frappe.db.sql("delete from `tabSales Invoice` where company='Trial Balance Company'")
frappe.db.sql("delete from `tabGL Entry` where company='Trial Balance Company'")
branch1 = frappe.new_doc("Branch")
branch1.branch = "Location 1"
branch1.insert(ignore_if_duplicate=True)

View File

@@ -546,7 +546,7 @@ def reconcile_against_document(
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
dimensions_dict=dimensions_dict,
)
if referenced_row.get("outstanding_amount"):
if referenced_row.get("outstanding_amount") and entry.get("outstanding_amount") is None:
referenced_row.outstanding_amount -= flt(entry.allocated_amount)
reposting_rows.append(referenced_row)
@@ -2513,6 +2513,7 @@ def create_gain_loss_journal(
ref2_detail_no,
cost_center,
dimensions,
project=None,
) -> str:
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
@@ -2539,6 +2540,7 @@ def create_gain_loss_journal(
"account_currency": party_account_currency,
"exchange_rate": 0,
"cost_center": cost_center or erpnext.get_default_cost_center(company),
"project": project,
"reference_type": ref1_dt,
"reference_name": ref1_dn,
"reference_detail_no": ref1_detail_no,
@@ -2556,6 +2558,7 @@ def create_gain_loss_journal(
"account_currency": gain_loss_account_currency,
"exchange_rate": 1,
"cost_center": cost_center or erpnext.get_default_cost_center(company),
"project": project,
"reference_type": ref2_dt,
"reference_name": ref2_dn,
"reference_detail_no": ref2_detail_no,

View File

@@ -26,7 +26,6 @@ class TestAssetCapitalization(ERPNextTestSuite):
def setUp(self):
set_depreciation_settings_in_company()
create_asset_capitalization_data()
frappe.db.sql("delete from `tabTax Rule`")
def test_capitalization_with_perpetual_inventory(self):
company = "_Test Company with perpetual inventory"

View File

@@ -195,6 +195,9 @@ def reschedule_depreciation(asset_doc, notes, disposal_date=None):
for row in asset_doc.get("finance_books"):
current_schedule = get_asset_depr_schedule_doc(asset_doc.name, None, row.finance_book)
if disposal_date and flt(row.value_after_depreciation) <= flt(row.expected_value_after_useful_life):
continue
if current_schedule:
if current_schedule.docstatus == 1:
new_schedule = frappe.copy_doc(current_schedule)

View File

@@ -24,6 +24,7 @@
"allow_zero_qty_in_supplier_quotation",
"use_transaction_date_exchange_rate",
"allow_zero_qty_in_request_for_quotation",
"allow_negative_rates_for_items",
"column_break_12",
"maintain_same_rate",
"allow_multiple_items",
@@ -279,6 +280,12 @@
"fieldname": "validate_consumed_qty",
"fieldtype": "Check",
"label": "Validate Consumed Qty (as per BOM)"
},
{
"default": "0",
"fieldname": "allow_negative_rates_for_items",
"fieldtype": "Check",
"label": "Allow Negative rates for Items"
}
],
"grid_page_length": 50,
@@ -288,7 +295,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-03-16 13:28:19.432589",
"modified": "2026-04-15 16:07:35.484787",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -18,6 +18,7 @@ class BuyingSettings(Document):
from frappe.types import DF
allow_multiple_items: DF.Check
allow_negative_rates_for_items: DF.Check
allow_zero_qty_in_purchase_order: DF.Check
allow_zero_qty_in_request_for_quotation: DF.Check
allow_zero_qty_in_supplier_quotation: DF.Check

View File

@@ -416,7 +416,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
__("Create")
);
if (flt(doc.per_billed) < 100 && doc.status != "Delivered") {
if (
frappe.model.can_create("Payment Entry") &&
flt(doc.per_billed) < 100 &&
doc.status != "Delivered"
) {
this.frm.add_custom_button(
__("Payment"),
() => this.make_payment_entry(),
@@ -424,7 +428,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
);
}
if (flt(doc.per_billed) < 100) {
if (flt(doc.per_billed) < 100 && frappe.boot.user.in_create.includes("Payment Request")) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
@@ -660,12 +664,20 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
}
items_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
if (doc.schedule_date) {
row.schedule_date = doc.schedule_date;
refresh_field("schedule_date", cdn, "items");
const row = frappe.get_doc(cdt, cdn);
const field_copy = [];
if (doc.project) {
frappe.model.set_value(cdt, cdn, "project", doc.project);
} else {
this.frm.script_manager.copy_from_first_row("items", row, ["schedule_date"]);
field_copy.push("project");
}
if (doc.schedule_date) {
frappe.model.set_value(cdt, cdn, "schedule_date", doc.schedule_date);
} else {
field_copy.push("schedule_date");
}
if (field_copy.length) {
this.frm.script_manager.copy_from_first_row("items", row, field_copy);
}
}
@@ -740,12 +752,6 @@ cur_frm.cscript.update_status = function (label, status) {
});
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};
if (cur_frm.doc.is_old_subcontracting_flow) {
cur_frm.fields_dict["items"].grid.get_field("bom").get_query = function (doc, cdt, cdn) {
var d = locals[cdt][cdn];

View File

@@ -23,6 +23,8 @@
"is_subcontracted",
"has_unit_price_items",
"supplier_warehouse",
"section_break_ahub",
"title",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -147,7 +149,7 @@
"column_break_86",
"select_print_heading",
"language",
"subscription_section",
"auto_repeat_section",
"from_date",
"to_date",
"column_break_97",
@@ -1013,12 +1015,6 @@
"label": "Print Language",
"print_hide": 1
},
{
"collapsible": 1,
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"allow_on_submit": 1,
"fieldname": "from_date",
@@ -1309,6 +1305,24 @@
"fieldtype": "Time",
"label": "Time",
"mandatory_depends_on": "is_internal_supplier"
},
{
"collapsible": 1,
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "section_break_ahub",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"grid_page_length": 50,
@@ -1316,7 +1330,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2026-03-25 11:46:18.748951",
"modified": "2026-04-28 06:11:46.904768",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -159,6 +159,7 @@ class PurchaseOrder(BuyingController):
taxes_and_charges_deducted: DF.Currency
tc_name: DF.Link | None
terms: DF.TextEditor | None
title: DF.Data | None
to_date: DF.Date | None
total: DF.Currency
total_net_weight: DF.Float

View File

@@ -16,6 +16,8 @@
"status",
"has_unit_price_items",
"amended_from",
"section_break_mhyw",
"title",
"suppliers_section",
"suppliers",
"items_section",
@@ -371,6 +373,18 @@
"fieldtype": "Text Editor",
"label": "Shipping Address Details",
"read_only": 1
},
{
"fieldname": "section_break_mhyw",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"print_hide": 1
}
],
"grid_page_length": 50,
@@ -378,7 +392,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-03-19 15:27:56.730649",
"modified": "2026-04-28 06:18:05.661710",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
@@ -448,5 +462,6 @@
"show_name_in_global_search": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": []
"states": [],
"title_field": "company"
}

View File

@@ -63,6 +63,7 @@ class RequestforQuotation(BuyingController):
suppliers: DF.Table[RequestforQuotationSupplier]
tc_name: DF.Link | None
terms: DF.TextEditor | None
title: DF.Data | None
transaction_date: DF.Date
use_html: DF.Check
vendor: DF.Link | None
@@ -477,6 +478,11 @@ def create_supplier_quotation(doc):
if isinstance(doc, str):
doc = json.loads(doc)
if frappe.session.user not in frappe.get_all(
"Portal User", {"parent": doc.get("supplier")}, pluck="user"
):
frappe.throw(_("Not Permitted"), frappe.PermissionError)
try:
sq_doc = frappe.get_doc(
{

View File

@@ -264,6 +264,13 @@ def make_request_for_quotation(**args) -> "RequestforQuotation":
for data in supplier_data:
rfq.append("suppliers", data)
frappe.new_doc(
"Portal User",
user="Administrator",
parent=data.get("supplier"),
parentfield="portal_users",
parenttype="Supplier",
).insert()
rfq.append(
"items",

View File

@@ -126,9 +126,3 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
// for backward compatibility: combine new and previous states
extend_cscript(cur_frm.cscript, new erpnext.buying.SupplierQuotationController({ frm: cur_frm }));
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
};
};

View File

@@ -9,7 +9,6 @@
"engine": "InnoDB",
"field_order": [
"supplier_section",
"title",
"naming_series",
"supplier",
"supplier_name",
@@ -21,6 +20,8 @@
"quotation_number",
"has_unit_price_items",
"amended_from",
"section_break_kumc",
"title",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -111,7 +112,7 @@
"column_break_85",
"select_print_heading",
"language",
"subscription_section",
"auto_repeat_section",
"auto_repeat",
"update_auto_repeat_reference",
"more_info",
@@ -127,10 +128,9 @@
"options": "fa fa-user"
},
{
"default": "{supplier_name}",
"allow_on_submit": 1,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
"no_copy": 1,
"print_hide": 1,
@@ -736,11 +736,6 @@
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "subscription_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "auto_repeat",
"fieldtype": "Link",
@@ -940,6 +935,15 @@
"no_copy": 1,
"options": "Item Wise Tax Detail",
"print_hide": 1
},
{
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "section_break_kumc",
"fieldtype": "Section Break"
}
],
"grid_page_length": 50,
@@ -948,7 +952,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-01-29 21:23:13.778468",
"modified": "2026-04-28 06:23:52.813948",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
@@ -1016,5 +1020,5 @@
"sort_order": "DESC",
"states": [],
"timeline_field": "supplier",
"title_field": "title"
"title_field": "supplier_name"
}

View File

@@ -13,7 +13,6 @@ class TestSupplierScorecard(ERPNextTestSuite):
self.assertEqual(doc.name, valid_scorecard[0].get("supplier"))
def test_criteria_weight(self):
delete_test_scorecards()
my_doc = make_supplier_scorecard()
for d in my_doc.criteria:
d.weight = 0
@@ -33,26 +32,6 @@ def make_supplier_scorecard():
return my_doc
def delete_test_scorecards():
my_doc = make_supplier_scorecard()
if frappe.db.exists("Supplier Scorecard", my_doc.name):
# Delete all the periods, then delete the scorecard
frappe.db.sql(
"""delete from `tabSupplier Scorecard Period` where scorecard = %(scorecard)s""",
{"scorecard": my_doc.name},
)
frappe.db.sql(
"""delete from `tabSupplier Scorecard Scoring Criteria` where parenttype = 'Supplier Scorecard Period'"""
)
frappe.db.sql(
"""delete from `tabSupplier Scorecard Scoring Standing` where parenttype = 'Supplier Scorecard Period'"""
)
frappe.db.sql(
"""delete from `tabSupplier Scorecard Scoring Variable` where parenttype = 'Supplier Scorecard Period'"""
)
frappe.delete_doc(my_doc.doctype, my_doc.name)
valid_scorecard = [
{
"standings": [

View File

@@ -9,41 +9,15 @@ from erpnext.tests.utils import ERPNextTestSuite
class TestSupplierScorecardCriteria(ERPNextTestSuite):
def test_variables_exist(self):
delete_test_scorecards()
for d in test_good_criteria:
frappe.get_doc(d).insert()
self.assertRaises(frappe.ValidationError, frappe.get_doc(test_bad_criteria[0]).insert)
def test_formula_validate(self):
delete_test_scorecards()
self.assertRaises(frappe.ValidationError, frappe.get_doc(test_bad_criteria[1]).insert)
def delete_test_scorecards():
# Delete all the periods so we can delete all the criteria
frappe.db.sql("""delete from `tabSupplier Scorecard Period`""")
frappe.db.sql(
"""delete from `tabSupplier Scorecard Scoring Criteria` where parenttype = 'Supplier Scorecard Period'"""
)
frappe.db.sql(
"""delete from `tabSupplier Scorecard Scoring Standing` where parenttype = 'Supplier Scorecard Period'"""
)
frappe.db.sql(
"""delete from `tabSupplier Scorecard Scoring Variable` where parenttype = 'Supplier Scorecard Period'"""
)
for d in test_good_criteria:
if frappe.db.exists("Supplier Scorecard Criteria", d.get("name")):
# Delete all the periods, then delete the scorecard
frappe.delete_doc(d.get("doctype"), d.get("name"))
for d in test_bad_criteria:
if frappe.db.exists("Supplier Scorecard Criteria", d.get("name")):
# Delete all the periods, then delete the scorecard
frappe.delete_doc(d.get("doctype"), d.get("name"))
test_good_criteria = [
{
"name": "Delivery",

View File

@@ -68,6 +68,7 @@ from erpnext.setup.utils import get_exchange_rate
from erpnext.stock.doctype.item.item import get_uom_conv_factor
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
from erpnext.stock.get_item_details import (
NOT_APPLICABLE_TAX,
ItemDetailsCtx,
_get_item_tax_template,
get_conversion_factor,
@@ -1767,6 +1768,7 @@ class AccountsController(TransactionBase):
arg.get("referenced_row"),
arg.get("cost_center"),
dimensions_dict,
arg.get("project"),
)
frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format(
@@ -1851,6 +1853,7 @@ class AccountsController(TransactionBase):
d.idx,
self.cost_center,
dimensions_dict,
self.project,
)
frappe.msgprint(
_("Exchange Gain/Loss amount has been booked through {0}").format(
@@ -3684,8 +3687,11 @@ def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True):
if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
tax_map = json.loads(child_item.get("item_tax_rate"))
for tax_type in tax_map:
tax_rate = flt(tax_map[tax_type])
for tax_type, tax_rate in tax_map.items():
if tax_rate == NOT_APPLICABLE_TAX:
continue
tax_rate = flt(tax_rate)
taxes = parent_doc.get("taxes") or []
# add new row for tax head only if missing
found = any(tax.account_head == tax_type for tax in taxes)

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