Compare commits

..

194 Commits

Author SHA1 Message Date
Frappe PR Bot
054b20a2ae chore(release): Bumped to Version 16.22.0
# [16.22.0](https://github.com/frappe/erpnext/compare/v16.21.1...v16.22.0) (2026-06-10)

### Bug Fixes

* **accounts:** include asset items in purchase receipt validation ([#55150](https://github.com/frappe/erpnext/issues/55150)) ([0984c86](0984c86583))
* Add authorization checks on internal functions (backport [#55709](https://github.com/frappe/erpnext/issues/55709)) ([#55726](https://github.com/frappe/erpnext/issues/55726)) ([2ae6451](2ae6451f10))
* add company filter to Budget Against dimension options ([bfc6f44](bfc6f44fb0))
* add custom dimensions filters in Gross and Net profit report (backport [#55110](https://github.com/frappe/erpnext/issues/55110)) ([#55584](https://github.com/frappe/erpnext/issues/55584)) ([5d7e69d](5d7e69d8cf))
* Add likely missing escapes (backport [#55574](https://github.com/frappe/erpnext/issues/55574)) ([#55581](https://github.com/frappe/erpnext/issues/55581)) ([6a503f8](6a503f834c))
* aggregate child cost center data in Budget Variance Report ([4f2611c](4f2611cbe8))
* allow specific methods to run ([c9a5b00](c9a5b0026e))
* bypass project permission check when updating consumed material … (backport [#55645](https://github.com/frappe/erpnext/issues/55645)) ([#55707](https://github.com/frappe/erpnext/issues/55707)) ([4471666](4471666c8c))
* **cheque_print_template:** print format creation from cheque print template requires system manager (backport [#55708](https://github.com/frappe/erpnext/issues/55708)) ([#55712](https://github.com/frappe/erpnext/issues/55712)) ([38dd298](38dd2982f3))
* disallow BOM finished good item in secondary items table (backport [#55710](https://github.com/frappe/erpnext/issues/55710)) ([#55719](https://github.com/frappe/erpnext/issues/55719)) ([ecd3a19](ecd3a19912))
* do not allow to make changes in SABB after submit ([a03e3bf](a03e3bfe9f))
* don't allow to submit job card with hold status ([b4c850d](b4c850da1c))
* drop ignore_permissions handling from add_ac ([c0cf9aa](c0cf9aa1a7))
* duplicating a Customer/Supplier shouldn't inherit the source's primary contact and address (backport [#55421](https://github.com/frappe/erpnext/issues/55421)) ([#55609](https://github.com/frappe/erpnext/issues/55609)) ([808e51d](808e51db19))
* handle multi-select stock ageing filters ([#55775](https://github.com/frappe/erpnext/issues/55775)) ([e48ffe6](e48ffe6ef0))
* handle separator rows in financial statement formatter ([d8afc00](d8afc00ab5))
* include CRM Deal in `quotation to` filters ([f7e6542](f7e6542bcd))
* item report view ([7dfae51](7dfae51044))
* **item:** format integer numeric variant attributes without decimals (backport [#55561](https://github.com/frappe/erpnext/issues/55561)) ([#55564](https://github.com/frappe/erpnext/issues/55564)) ([fffba84](fffba84868))
* linter issue ([dd83705](dd837052ef))
* minor fixes in report print formats ([#55151](https://github.com/frappe/erpnext/issues/55151)) ([80f7aff](80f7aff3f9))
* move Company filter at the start ([3fb6437](3fb6437d26))
* naming series issue ([6eecf07](6eecf0701e))
* prevent double rounding in inclusive tax calculations (backport [#52512](https://github.com/frappe/erpnext/issues/52512)) ([#55570](https://github.com/frappe/erpnext/issues/55570)) ([37b61f0](37b61f06ae))
* prevent leakage of party-derived fields in cross doctype transactions (backport [#55336](https://github.com/frappe/erpnext/issues/55336)) ([#55579](https://github.com/frappe/erpnext/issues/55579)) ([7904385](7904385b90))
* prevent negative amounts in common party JE on return invoices (backport [#55034](https://github.com/frappe/erpnext/issues/55034)) ([#55064](https://github.com/frappe/erpnext/issues/55064)) ([9eb0e3c](9eb0e3c82e))
* prevent selling items from sample retention warehouse (backport [#55613](https://github.com/frappe/erpnext/issues/55613)) ([#55634](https://github.com/frappe/erpnext/issues/55634)) ([84d205f](84d205f553))
* **process statement of accounts:** validate pdf_name and validate permission before triggering send_auto_email (backport [#55781](https://github.com/frappe/erpnext/issues/55781)) ([#55783](https://github.com/frappe/erpnext/issues/55783)) ([e15879a](e15879acd1))
* **profit-and-loss-statement-report:** margin calculation the report showing null% for empty cell ([04fe76b](04fe76bf83))
* **profit-and-loss-statement:** margin calculation the report showing null% for empty cell ([b3d3f13](b3d3f13fc5))
* **profit-loss-report:** handle zero base values and prevent null% display ([90ac065](90ac065930))
* remove item name from update items dialog item code column (backport [#55718](https://github.com/frappe/erpnext/issues/55718)) ([#55723](https://github.com/frappe/erpnext/issues/55723)) ([497c3a5](497c3a5e83))
* restrict already invoiced qty in intercompany purchase invoice ([#55768](https://github.com/frappe/erpnext/issues/55768)) ([e90a6ec](e90a6ecf1c))
* **selling:** consider delivered qty (backport [#55597](https://github.com/frappe/erpnext/issues/55597)) ([#55607](https://github.com/frappe/erpnext/issues/55607)) ([142ab3c](142ab3ce2a))
* simplify New Zealand sales accounts ([93de14c](93de14c421))
* skip empty spacer rows in compute_growth_view_data (P&L growth view) ([18afcf0](18afcf0c01))
* spelling of Payment Reconciliation in sidebar (backport [#55599](https://github.com/frappe/erpnext/issues/55599)) ([#55602](https://github.com/frappe/erpnext/issues/55602)) ([40cf77e](40cf77e7f0))
* sql injection ([02a29a8](02a29a85a7))
* Stock Reservation blocks Subcontracting operation within the same Work Order ([1c0dace](1c0dace3d6))
* **stock:** add validation for work order seial nos and batch nos (backport [#55604](https://github.com/frappe/erpnext/issues/55604)) ([#55605](https://github.com/frappe/erpnext/issues/55605)) ([7de77a8](7de77a8916))
* **stock:** change valuation rate column label in stock ledger entry/report (backport [#55323](https://github.com/frappe/erpnext/issues/55323)) ([#55394](https://github.com/frappe/erpnext/issues/55394)) ([c6560be](c6560be58d))
* **stock:** set stock received but not billed account for purchase ([#55149](https://github.com/frappe/erpnext/issues/55149)) ([90667b2](90667b2de2))
* **subscription:** bill on creation and keep status in sync with invoices (backport [#55615](https://github.com/frappe/erpnext/issues/55615)) ([#55701](https://github.com/frappe/erpnext/issues/55701)) ([0f069e1](0f069e13da))
* **subscription:** correct billing/deferred bugs and tighten guards (backport [#55554](https://github.com/frappe/erpnext/issues/55554)) ([#55610](https://github.com/frappe/erpnext/issues/55610)) ([dee7bd8](dee7bd8d64))
* **taxes:** add category and add_deduct_tax fields to tax entries (backport [#55753](https://github.com/frappe/erpnext/issues/55753)) ([#55773](https://github.com/frappe/erpnext/issues/55773)) ([9a6fae9](9a6fae9fdd))
* update add_total_row_account to control blank row addition ([e8a6933](e8a6933ff3))
* update formatter to handle blank rows in financial statements ([f657503](f657503ea3))
* update items respects workflow "Only Allow Edit For" role ([#55667](https://github.com/frappe/erpnext/issues/55667)) ([76b9b6a](76b9b6a34e))
* use new_doc with field allowlist in CRM integration endpoints ([d941ccf](d941ccfe3c))
* **UX:** Accounts settings cleanup (backport [#55470](https://github.com/frappe/erpnext/issues/55470)) ([#55603](https://github.com/frappe/erpnext/issues/55603)) ([3917415](3917415368))
* **UX:** stock settings form cleanup ([f6f542f](f6f542fadc))
* validate fg and materials qty in the disassemble entry ([4453c10](4453c1072a))
* work order status should be in process if material transfer is skipped (backport [#55641](https://github.com/frappe/erpnext/issues/55641) to version-16-hotfix) ([#55642](https://github.com/frappe/erpnext/issues/55642)) ([32011c3](32011c3364))

### Features

* add item where used report ([#55714](https://github.com/frappe/erpnext/issues/55714)) ([8f85cce](8f85cce4cf))
* add New Zealand chart of accounts ([107a446](107a446d98))
* added cost of goods sold (backport [#54974](https://github.com/frappe/erpnext/issues/54974)) ([#55552](https://github.com/frappe/erpnext/issues/55552)) ([20af709](20af7093ac))
* create sales invoice from pick list (backport [#55594](https://github.com/frappe/erpnext/issues/55594)) ([#55635](https://github.com/frappe/erpnext/issues/55635)) ([743afc9](743afc972d))
* item prices list view ([#54853](https://github.com/frappe/erpnext/issues/54853)) ([12c1940](12c1940e0b))
* show non stock items and secondary items in work order (backport [#55631](https://github.com/frappe/erpnext/issues/55631)) ([#55636](https://github.com/frappe/erpnext/issues/55636)) ([3f983c9](3f983c9e4d))

### Performance Improvements

* batch status check for on-hold/closed documents, remove N+1 queries (backport [#54798](https://github.com/frappe/erpnext/issues/54798)) ([#55573](https://github.com/frappe/erpnext/issues/55573)) ([0274afe](0274afe560))
* **transaction:** exit early before backend query (backport [#55556](https://github.com/frappe/erpnext/issues/55556)) ([#55558](https://github.com/frappe/erpnext/issues/55558)) ([6a1c384](6a1c384f9b))
2026-06-10 00:25:19 +00:00
Mihir Kandoi
03f6b7a50e Merge pull request #55764 from frappe/version-16-hotfix 2026-06-10 05:53:43 +05:30
mergify[bot]
e15879acd1 fix(process statement of accounts): validate pdf_name and validate permission before triggering send_auto_email (backport #55781) (#55783)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix(process statement of accounts): validate pdf_name and validate permission before triggering send_auto_email (#55781)
2026-06-09 19:11:44 +00:00
rohitwaghchaure
01e90830f1 Merge pull request #55766 from rohitwaghchaure/fixed-github-55621-v16
fix: Stock Reservation blocks Subcontracting operation within the same work order
2026-06-09 20:00:20 +05:30
Rohit Waghchaure
1c0dace3d6 fix: Stock Reservation blocks Subcontracting operation within the same Work Order 2026-06-09 19:40:45 +05:30
Mihir Kandoi
e48ffe6ef0 fix: handle multi-select stock ageing filters (#55775) 2026-06-09 13:52:32 +00:00
MochaMind
8ff6fc5fea chore: sync translations to version-16-hotfix (#55739) 2026-06-09 13:19:55 +00:00
Pandiyan P
e90a6ecf1c fix: restrict already invoiced qty in intercompany purchase invoice (#55768) 2026-06-09 13:16:22 +00:00
mergify[bot]
9a6fae9fdd fix(taxes): add category and add_deduct_tax fields to tax entries (backport #55753) (#55773)
Co-authored-by: Lakshit Jain <ljain112@gmail.com>
fix(taxes): add category and add_deduct_tax fields to tax entries (#55753)
2026-06-09 12:59:13 +00:00
mergify[bot]
7904385b90 fix: prevent leakage of party-derived fields in cross doctype transactions (backport #55336) (#55579)
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
Co-authored-by: Lakshit Jain <ljain112@gmail.com>
fix: prevent leakage of party-derived fields in cross doctype transactions (#55336)
2026-06-09 18:03:26 +05:30
rohitwaghchaure
f0e51b35a6 Merge pull request #55762 from frappe/mergify/bp/version-16-hotfix/pr-55760
fix: don't allow to submit job card with hold status (backport #55760)
2026-06-09 15:15:39 +05:30
rohitwaghchaure
60a270acd4 chore: fix conflicts
Removed validation for completed quantity matching in job card.
2026-06-09 14:56:39 +05:30
Rohit Waghchaure
b4c850da1c fix: don't allow to submit job card with hold status
(cherry picked from commit 9c23229cbf)

# Conflicts:
#	erpnext/manufacturing/doctype/job_card/job_card.py
2026-06-09 09:01:09 +00:00
ruthra kumar
9b1cdd0e8f Merge pull request #55729 from frappe/mergify/bp/version-16-hotfix/pr-55486
Validations in CRM-api endpoints (backport #55486)
2026-06-09 10:31:49 +05:30
rohitwaghchaure
26a19bf372 Merge pull request #55752 from frappe/mergify/bp/version-16-hotfix/pr-55748
fix: sql injection (backport #55748)
2026-06-09 09:49:00 +05:30
Rohit Waghchaure
02a29a85a7 fix: sql injection
(cherry picked from commit bd0acf4413)
2026-06-09 03:58:34 +00:00
rohitwaghchaure
6e7a96eae9 Merge pull request #55744 from frappe/mergify/bp/version-16-hotfix/pr-55737
fix: allow specific methods to run (backport #55737)
2026-06-09 09:27:46 +05:30
Rohit Waghchaure
dd837052ef fix: linter issue 2026-06-08 23:17:29 +05:30
rohitwaghchaure
95818ec71d chore: fix conflicts
Removed redundant quantity assignments and completion time logging from job card processing.
2026-06-08 20:08:16 +05:30
rohitwaghchaure
3d1327cfe5 chore: fix conflicts 2026-06-08 20:06:44 +05:30
rohitwaghchaure
b617f1302d chore: fix conflicts 2026-06-08 20:05:46 +05:30
Rohit Waghchaure
c9a5b0026e fix: allow specific methods to run
(cherry picked from commit 8db1eb0d27)

# Conflicts:
#	erpnext/manufacturing/doctype/job_card/job_card.py
2026-06-08 14:18:26 +00:00
rohitwaghchaure
c5434e39d8 Merge pull request #55740 from rohitwaghchaure/fixed-support-67770-3-v16
fix: validate fg and materials qty in the disassemble entry
2026-06-08 19:30:34 +05:30
Rohit Waghchaure
4453c1072a fix: validate fg and materials qty in the disassemble entry 2026-06-08 17:42:04 +05:30
MochaMind
8e930d0ef1 chore: update POT file (#55691) 2026-06-08 11:15:28 +00:00
rohitwaghchaure
e6f6f2c173 Merge pull request #55735 from frappe/mergify/bp/version-16-hotfix/pr-55716
fix: do not allow to make changes in SABB after submit (backport #55716)
2026-06-08 16:02:51 +05:30
mergify[bot]
2ae6451f10 fix: Add authorization checks on internal functions (backport #55709) (#55726)
* fix: Add authorization checks on internal functions (#55709)

(cherry picked from commit ba936eefab)

# Conflicts:
#	erpnext/accounts/doctype/pos_profile/pos_profile.py
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
#	erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py
#	erpnext/support/doctype/issue/issue.py

* chore: conflicts

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
2026-06-08 10:16:12 +00:00
mergify[bot]
497c3a5e83 fix: remove item name from update items dialog item code column (backport #55718) (#55723)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
Co-authored-by: Abdullah <frappe@LAPTOP-4E788RM4.localdomain>
fix: remove item name from update items dialog item code column (#55718)
2026-06-08 15:42:38 +05:30
rohitwaghchaure
5efeb2af18 chore: fix conflicts
Removed unused import for StockReconciliationItem.
2026-06-08 15:30:48 +05:30
Rohit Waghchaure
a03e3bfe9f fix: do not allow to make changes in SABB after submit
(cherry picked from commit e36426e235)

# Conflicts:
#	erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
2026-06-08 09:56:18 +00:00
Shllokkk
d941ccfe3c fix: use new_doc with field allowlist in CRM integration endpoints
(cherry picked from commit e460e83516)
2026-06-08 09:41:26 +00:00
mergify[bot]
ecd3a19912 fix: disallow BOM finished good item in secondary items table (backport #55710) (#55719)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
fix: disallow BOM finished good item in secondary items table (#55710)
2026-06-08 13:40:59 +05:30
Mihir Kandoi
8f85cce4cf feat: add item where used report (#55714) 2026-06-08 07:52:23 +00:00
mergify[bot]
38dd2982f3 fix(cheque_print_template): print format creation from cheque print template requires system manager (backport #55708) (#55712)
* fix(cheque_print_template): print format creation from cheque print template requires system manager (#55708)

(cherry picked from commit faf92b1368)

# Conflicts:
#	erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py

* chore: resolved conflicts

---------

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-06-08 07:48:24 +00:00
mergify[bot]
4471666c8c fix: bypass project permission check when updating consumed material … (backport #55645) (#55707)
* fix: bypass project permission check when updating consumed material cost

(cherry picked from commit 4b0b7adeee)

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

* chore: resolve conflicts

---------

Co-authored-by: pandiyan <pandiyanpalani37@gmail.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-06-08 07:11:04 +00:00
ruthra kumar
c1b47bd561 Merge pull request #55704 from frappe/mergify/bp/version-16-hotfix/pr-55665
fix: drop ignore_permissions handling from add_ac (backport #55665)
2026-06-08 12:09:44 +05:30
mergify[bot]
c6560be58d fix(stock): change valuation rate column label in stock ledger entry/report (backport #55323) (#55394)
Co-authored-by: Sudharsanan11 <sudharsananashok1975@gmail.com>
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
2026-06-08 11:54:00 +05:30
kaulith
76b9b6a34e fix: update items respects workflow "Only Allow Edit For" role (#55667) 2026-06-08 11:52:59 +05:30
Shllokkk
c0cf9aa1a7 fix: drop ignore_permissions handling from add_ac
(cherry picked from commit 37d2adc74b)
2026-06-08 06:15:15 +00:00
mergify[bot]
0f069e13da fix(subscription): bill on creation and keep status in sync with invoices (backport #55615) (#55701)
* fix(subscription): bill on creation and keep status in sync with invoices (#55615)

(cherry picked from commit bb36e956ac)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.py
#	erpnext/accounts/utils.py

* fix(subscription): resolve cherry-pick conflicts for version-16-hotfix backport

---------

Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
Co-authored-by: Jatin3128 <jatinsarna8@gmail.com>
2026-06-08 07:33:08 +05:30
mergify[bot]
a761a98e3a ci: add review comments on gettext files (backport #55699) (#55700)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-06-07 23:27:29 +00:00
mergify[bot]
a9926871d6 chore: remove unused whitelisted method from project (backport #55648) (#55673)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-06-07 23:16:02 +05:30
Nabin Hait
0785fa0257 Merge pull request #55698 from frappe/mergify/bp/version-16-hotfix/pr-55101
fix: handle blank rows in financial statement formatter (backport #55101)
2026-06-07 22:55:44 +05:30
Nabin Hait
86ee17614c Merge pull request #55697 from frappe/mergify/bp/version-16-hotfix/pr-54684
fix(profit-loss-report): handle zero base values and prevent null% display (backport #54684)
2026-06-07 22:55:34 +05:30
Abdeali Chharchhoda
f657503ea3 fix: update formatter to handle blank rows in financial statements
(cherry picked from commit 814c11200a)
2026-06-07 16:58:49 +00:00
Abdeali Chharchhoda
e8a6933ff3 fix: update add_total_row_account to control blank row addition
(cherry picked from commit f7c744350c)
2026-06-07 16:58:49 +00:00
Abdeali Chharchhoda
d8afc00ab5 fix: handle separator rows in financial statement formatter
(cherry picked from commit cf597361f6)
2026-06-07 16:58:49 +00:00
Ahmed Reda Abukhatwa
18afcf0c01 fix: skip empty spacer rows in compute_growth_view_data (P&L growth view)
(cherry picked from commit 3592c3086d)
2026-06-07 16:56:54 +00:00
Ahmed Reda Abukhatwa
90ac065930 fix(profit-loss-report): handle zero base values and prevent null% display
(cherry picked from commit 7335011814)
2026-06-07 16:56:54 +00:00
Ahmed Reda Abukhatwa
b3d3f13fc5 fix(profit-and-loss-statement): margin calculation the report showing null% for empty cell
(cherry picked from commit 671555edbc)
2026-06-07 16:56:54 +00:00
Ahmed Reda Abukhatwa
04fe76bf83 fix(profit-and-loss-statement-report): margin calculation the report showing null% for empty cell
(cherry picked from commit df6fd782b7)
2026-06-07 16:56:53 +00:00
rohitwaghchaure
a403e85918 Merge pull request #55664 from frappe/mergify/bp/version-16-hotfix/pr-55661
fix: naming series issue (backport #55661)
2026-06-05 21:06:00 +05:30
rohitwaghchaure
7c46298fb6 chore: fix conflicts
Removed unused import of DateTimeLikeObject from frappe.utils.data.
2026-06-05 20:46:59 +05:30
Rohit Waghchaure
6eecf0701e fix: naming series issue
(cherry picked from commit 3a50056968)

# Conflicts:
#	erpnext/stock/doctype/batch/batch.py
2026-06-05 15:14:22 +00:00
Mihir Kandoi
cfe5d07b4c [codex] Show in-transit status for add-to-transit Stock Entries (backport #55644 to version-16-hotfix) (#55658)
[codex] Show in-transit status for add-to-transit Stock Entries (#55644)

(cherry picked from commit df03524b19)
2026-06-05 11:32:21 +00:00
Khushi Rawat
e7a4d6451e Merge pull request #55653 from frappe/mergify/bp/version-16-hotfix/pr-54853
feat: item prices list view (backport #54853)
2026-06-05 16:26:50 +05:30
Khushi Rawat
12c1940e0b feat: item prices list view (#54853)
* feat: add item prices tab to Item doctype

* feat: item form pricing tab

* fix: remove action button for edit item price

* fix: prevent stale item price rendering after form navigation

* fix: remove stale call to deleted edit_prices_button function

* fix: item price list fixes

* fix: show filtered price list

* fix: show filtered price list

(cherry picked from commit 5873f55cf0)
2026-06-05 10:12:58 +00:00
Mihir Kandoi
32011c3364 fix: work order status should be in process if material transfer is skipped (backport #55641 to version-16-hotfix) (#55642)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 05:37:45 +00:00
mergify[bot]
3f983c9e4d feat: show non stock items and secondary items in work order (backport #55631) (#55636)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-06-05 05:06:50 +00:00
shahzeelahmed
bda915a201 Merge pull request #55619 from frappe/mergify/bp/version-16-hotfix/pr-55536
fix: allow CRM Deal as Quotation To for CRM integration (backport #55536)
2026-06-05 00:01:31 +05:30
Mihir Kandoi
743afc972d feat: create sales invoice from pick list (backport #55594) (#55635) 2026-06-04 17:17:26 +00:00
mergify[bot]
84d205f553 fix: prevent selling items from sample retention warehouse (backport #55613) (#55634)
Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: prevent selling items from sample retention warehouse (#55613)
2026-06-04 16:50:34 +00:00
mergify[bot]
1823fbea37 refactor: convert rfq_transaction_list to query builder (backport #55497) (#55630)
Co-authored-by: Shllokkk <140623894+Shllokkk@users.noreply.github.com>
2026-06-04 14:26:37 +00:00
shahzeelahmed
f7e6542bcd fix: include CRM Deal in quotation to filters
(cherry picked from commit 519dc0b958)
2026-06-04 06:22:26 +00:00
mergify[bot]
3917415368 fix(UX): Accounts settings cleanup (backport #55470) (#55603)
fix(UX): Accounts settings cleanup (backport #55470)
2026-06-04 11:38:58 +05:30
mergify[bot]
dee7bd8d64 fix(subscription): correct billing/deferred bugs and tighten guards (backport #55554) (#55610)
fix(subscription): correct billing/deferred bugs and tighten guards (#55554)

(cherry picked from commit d54db2e0ca)

Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
2026-06-04 05:22:02 +05:30
Khushi Rawat
e112a1933a Merge pull request #55611 from frappe/mergify/bp/version-16-hotfix/pr-55151
fix: minor fixes in report print formats (backport #55151)
2026-06-03 23:24:47 +05:30
Shllokkk
80f7aff3f9 fix: minor fixes in report print formats (#55151)
(cherry picked from commit a75693a81f)
2026-06-03 16:56:11 +00:00
mergify[bot]
808e51db19 fix: duplicating a Customer/Supplier shouldn't inherit the source's primary contact and address (backport #55421) (#55609)
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
Co-authored-by: Antoine Maas <antoine.maas@okte.io>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: duplicating a Customer/Supplier shouldn't inherit the source's primary contact and address (#55421)
2026-06-03 16:03:59 +00:00
mergify[bot]
0274afe560 perf: batch status check for on-hold/closed documents, remove N+1 queries (backport #54798) (#55573)
* perf: batch status check for on-hold/closed documents, remove N+1 queries (#54798)

(cherry picked from commit 5074597d00)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/purchase_order.py

* chore: resolve conflicts

---------

Co-authored-by: Shubh Doshi <124681920+shubhdoshi21@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-06-03 16:00:54 +00:00
mergify[bot]
6a503f834c fix: Add likely missing escapes (backport #55574) (#55581)
Co-authored-by: Ankush Menat <ankush@frappe.io>
fix: Add likely missing escaps (#55574)
2026-06-03 15:53:43 +00:00
mergify[bot]
7de77a8916 fix(stock): add validation for work order seial nos and batch nos (backport #55604) (#55605)
* fix(stock): add validation for work order seial nos and batch nos

(cherry picked from commit 6d3f9d3c6f)

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

* chore: resolve conflicts

* chore: resolve conflicts

---------

Co-authored-by: pandiyan <pandiyanpalani37@gmail.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-06-03 15:50:50 +00:00
mergify[bot]
142ab3ce2a fix(selling): consider delivered qty (backport #55597) (#55607)
Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
fix(selling): consider delivered qty (#55597)
2026-06-03 15:37:29 +00:00
mergify[bot]
9a2549dc32 refactor: minor problems in production plan (backport #55577) (#55595)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-06-03 21:02:31 +05:30
mergify[bot]
40cf77e7f0 fix: spelling of Payment Reconciliation in sidebar (backport #55599) (#55602)
fix: spelling of Payment Reconciliation in sidebar (#55599)

(cherry picked from commit 371b5c7593)

Co-authored-by: Nikhil Kothari <nik.kothari22@live.com>
2026-06-03 19:48:34 +05:30
Nishka Gosalia
6ff9685881 Merge pull request #55598 from frappe/mergify/bp/version-16-hotfix/pr-55309
fix(UX):stock settings form cleanup (backport #55309)
2026-06-03 16:45:52 +05:30
nishkagosalia
f6f542fadc fix(UX): stock settings form cleanup
(cherry picked from commit 0b4e20ae98)
2026-06-03 10:34:43 +00:00
Frappe PR Bot
6bcab7cfc8 chore(release): Bumped to Version 16.21.1
## [16.21.1](https://github.com/frappe/erpnext/compare/v16.21.0...v16.21.1) (2026-06-03)

### Bug Fixes

* item report view ([656d1bd](656d1bd6e3))
2026-06-03 09:43:23 +00:00
Nishka Gosalia
39c8161011 Merge pull request #55593 from frappe/mergify/bp/version-16/pr-55592
fix: item report view (backport #55591) (backport #55592)
2026-06-03 15:11:44 +05:30
nishkagosalia
656d1bd6e3 fix: item report view
(cherry picked from commit bca917380d)
(cherry picked from commit 7dfae51044)
2026-06-03 09:39:24 +00:00
Nishka Gosalia
19c47abb79 Merge pull request #55592 from frappe/mergify/bp/version-16-hotfix/pr-55591
fix: item report view (backport #55591)
2026-06-03 15:08:59 +05:30
nishkagosalia
7dfae51044 fix: item report view
(cherry picked from commit bca917380d)
2026-06-03 09:35:48 +00:00
mergify[bot]
a563d01425 regional(setup): add 0% and 6% VAT rates for Belgium (backport #54719) (#55583)
Co-authored-by: Antoine Maas <antoine.maas@okte.io>
2026-06-03 14:20:44 +05:30
mergify[bot]
5d7e69d8cf fix: add custom dimensions filters in Gross and Net profit report (backport #55110) (#55584)
Co-authored-by: Abdeali Chharchhodawala <99460106+Abdeali099@users.noreply.github.com>
fix: add custom dimensions filters in Gross and Net profit report (#55110)
2026-06-03 14:20:19 +05:30
Khushi Rawat
6c05625e65 Merge pull request #55586 from frappe/mergify/bp/version-16-hotfix/pr-55149
fix(stock): set stock received but not billed account for purchase (backport #55149)
2026-06-03 14:19:07 +05:30
Khushi Rawat
eaadb15bd5 Merge pull request #55588 from frappe/mergify/bp/version-16-hotfix/pr-55150
fix(accounts): include asset items in purchase receipt validation (backport #55150)
2026-06-03 14:18:43 +05:30
mergify[bot]
8f9db3c72d Avoid status updation for purchase invoice from paid to unpaid by issuing a paid debit note against it (backport #54382) (#55576)
Co-authored-by: Shllokkk <140623894+Shllokkk@users.noreply.github.com>
2026-06-03 14:11:25 +05:30
mergify[bot]
37b61f06ae fix: prevent double rounding in inclusive tax calculations (backport #52512) (#55570)
Co-authored-by: Luis Mendoza <mendozal@gmail.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix: prevent double rounding in inclusive tax calculations (#52512)
2026-06-03 14:10:32 +05:30
mergify[bot]
201e62195f fix payment schedule discount date when no discount is applied (backport #55462) (#55568)
Co-authored-by: Vishnu Priya Baskaran <145791817+ervishnucs@users.noreply.github.com>
fix payment schedule discount date when no discount is applied (#55462)
2026-06-03 14:10:12 +05:30
mergify[bot]
20af7093ac feat: added cost of goods sold (backport #54974) (#55552)
Co-authored-by: soham7117 <sohampawar626@gmail.com>
Signed-off-by: soham7117 <sohampawar626@gmail.com>
2026-06-03 14:09:37 +05:30
mergify[bot]
44adfbea33 refactor(sales_invoice): replace sql with qb in update_billing_status… (backport #55380) (#55411)
Co-authored-by: Loic Oberle <loic@dokos.io>
2026-06-03 14:09:15 +05:30
mergify[bot]
9eb0e3c82e fix: prevent negative amounts in common party JE on return invoices (backport #55034) (#55064)
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
fix: prevent negative amounts in common party JE on return invoices (#55034)
2026-06-03 14:08:49 +05:30
Pandiyan P
0984c86583 fix(accounts): include asset items in purchase receipt validation (#55150)
(cherry picked from commit d0d9411700)
2026-06-03 07:43:31 +00:00
Pandiyan P
90667b2de2 fix(stock): set stock received but not billed account for purchase (#55149)
(cherry picked from commit c4d28a2612)
2026-06-03 07:41:56 +00:00
ruthra kumar
f6791d5bcf Merge pull request #55572 from frappe/mergify/bp/version-16-hotfix/pr-55478
feat: add New Zealand chart of accounts (backport #55478)
2026-06-03 13:01:30 +05:30
Imesha Sudasingha
93de14c421 fix: simplify New Zealand sales accounts
(cherry picked from commit eebb37f9fd)
2026-06-03 05:48:42 +00:00
Imesha Sudasingha
107a446d98 feat: add New Zealand chart of accounts
(cherry picked from commit f8a123e79d)
2026-06-03 05:48:42 +00:00
Khushi Rawat
af97849c7b Merge pull request #55566 from frappe/mergify/bp/version-16-hotfix/pr-55562
fix: aggregate child cost center data in Budget Variance Report (backport #55562)
2026-06-03 03:37:27 +05:30
khushi8112
a7d42a4edd refactor: replace db.sql with frappe.qb
(cherry picked from commit 41884cfd2a)
2026-06-02 21:49:17 +00:00
khushi8112
4f2611cbe8 fix: aggregate child cost center data in Budget Variance Report
(cherry picked from commit cd7fa56ec4)
2026-06-02 21:49:17 +00:00
Khushi Rawat
91a64692f9 Merge pull request #55565 from frappe/mergify/bp/version-16-hotfix/pr-54840
fix: add company filter to Budget Against dimension options (backport #54840)
2026-06-03 02:50:01 +05:30
khushi8112
3fb6437d26 fix: move Company filter at the start
(cherry picked from commit c34eeee096)
2026-06-02 21:01:53 +00:00
HemilSangani
bfc6f44fb0 fix: add company filter to Budget Against dimension options
(cherry picked from commit bdf0136fc5)
2026-06-02 21:01:53 +00:00
mergify[bot]
fffba84868 fix(item): format integer numeric variant attributes without decimals (backport #55561) (#55564)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix(item): format integer numeric variant attributes without decimals (#55561)
2026-06-02 23:00:04 +02:00
mergify[bot]
6a1c384f9b perf(transaction): exit early before backend query (backport #55556) (#55558)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-06-02 20:51:01 +02:00
Frappe PR Bot
ecb572de92 chore(release): Bumped to Version 16.21.0
# [16.21.0](https://github.com/frappe/erpnext/compare/v16.20.1...v16.21.0) (2026-06-02)

### Bug Fixes

* asset scrap flow related changes ([e3f03a2](e3f03a21c3))
* billing address does not belongs to the company error ([e1f29de](e1f29de078))
* **book_appointment:** when scheduling is disabled, block API endpoints (backport [#55455](https://github.com/frappe/erpnext/issues/55455)) ([#55457](https://github.com/frappe/erpnext/issues/55457)) ([aa5dfde](aa5dfde23b))
* changes as per review ([8b1d981](8b1d9817a6))
* check perm for account (backport [#55479](https://github.com/frappe/erpnext/issues/55479)) ([#55483](https://github.com/frappe/erpnext/issues/55483)) ([0c946f2](0c946f2420))
* **custom_financial_template:** sum account closing balances across dimensions ([3359e20](3359e20d06))
* import DateTimeLikeObject ([d82e03e](d82e03edb6))
* **issue:** check permission before issue status modification (backport [#55458](https://github.com/frappe/erpnext/issues/55458)) ([#55460](https://github.com/frappe/erpnext/issues/55460)) ([7c5d617](7c5d617049))
* item master list view UI cleanup ([2d554c0](2d554c05d6))
* **je:** preserve account on duplicate row when party row exists (backport [#55180](https://github.com/frappe/erpnext/issues/55180)) ([#55512](https://github.com/frappe/erpnext/issues/55512)) ([fe585dc](fe585dc225))
* Make Distributed Discount Amount field read only ([29441b7](29441b7249))
* **manufacturing:** allow to edit batch size while creating a work order (backport [#55058](https://github.com/frappe/erpnext/issues/55058)) ([#55326](https://github.com/frappe/erpnext/issues/55326)) ([ae92a82](ae92a82930))
* material transfer in transit issue ([356bb78](356bb7878f))
* merge conflicts ([b74e365](b74e365421))
* new bom version should not recalculate operations through routing (backport [#55370](https://github.com/frappe/erpnext/issues/55370)) ([#55372](https://github.com/frappe/erpnext/issues/55372)) ([933ac01](933ac0108c))
* over order allowance setting fix ([41b2de3](41b2de35a9))
* pick correct name when creating user from RFQ (backport [#55468](https://github.com/frappe/erpnext/issues/55468)) ([#55472](https://github.com/frappe/erpnext/issues/55472)) ([fc842fb](fc842fb45f))
* **pos:** escape html output in pos page templates (backport [#55527](https://github.com/frappe/erpnext/issues/55527)) ([#55529](https://github.com/frappe/erpnext/issues/55529)) ([224426e](224426e06b))
* **pos:** escape item data on pos item selector (backport [#55503](https://github.com/frappe/erpnext/issues/55503)) ([#55508](https://github.com/frappe/erpnext/issues/55508)) ([5393c93](5393c93675))
* **pos:** preserve contacts and enforce permissions in set_customer_info (backport [#55463](https://github.com/frappe/erpnext/issues/55463)) ([#55466](https://github.com/frappe/erpnext/issues/55466)) ([ef2700b](ef2700bec6))
* **ppr:** make default_advance_account optional ([7a7cc31](7a7cc31523))
* **quotation:** made customer contact column visible (backport [#55433](https://github.com/frappe/erpnext/issues/55433)) ([#55435](https://github.com/frappe/erpnext/issues/55435)) ([2feb8eb](2feb8eb370))
* **regional:** Japanese CT Rate (backport [#54998](https://github.com/frappe/erpnext/issues/54998)) ([#55438](https://github.com/frappe/erpnext/issues/55438)) ([7426aaf](7426aaf1e2))
* resolve conflict ([abe19e1](abe19e1212))
* **selling:** handle None values while grouping opportunities by utm … (backport [#55300](https://github.com/frappe/erpnext/issues/55300)) ([#55328](https://github.com/frappe/erpnext/issues/55328)) ([198970c](198970cdee))
* set a fallback value if no fiscal year set ([1521410](1521410125))
* stock reco for legacy serial nos ([67c922c](67c922cdf3))
* **stock:** add warning message to notify the user to configure the inspection ([1679680](1679680d8e))
* **stock:** allow to create quality inspection after purchase/delivery ([51a140a](51a140a2bd))
* **stock:** change qb to qb get_query to fix filter issues (backport [#55443](https://github.com/frappe/erpnext/issues/55443)) ([#55445](https://github.com/frappe/erpnext/issues/55445)) ([277a072](277a0723ef))
* **stock:** get_actual_qty during cancellations (backport [#55388](https://github.com/frappe/erpnext/issues/55388)) ([#55392](https://github.com/frappe/erpnext/issues/55392)) ([faa1573](faa15731cb))
* **tds:** treat NULL and empty-string tax_withholding_group as equivalent ([82e12d2](82e12d2d52))
* unable to submit subcontracted job card (backport [#55537](https://github.com/frappe/erpnext/issues/55537)) ([#55540](https://github.com/frappe/erpnext/issues/55540)) ([ceb1042](ceb10422ae))
* update default_advance_account type ([0bbc493](0bbc493213))
* use fiscal year instead of calendar year in accounting dashboard number cards ([81d10d3](81d10d32f2))
* use get_query instead of get_all for data fetching ([7cbef15](7cbef15596))
* **UX:** Move title field to More Info ([20592fc](20592fc25d))

### Features

* build and upload assets to GitHub Releases ([4c05ebc](4c05ebc21e))
* over order allowance setting ([08eaaa5](08eaaa5b83))
* **payment-entry:** warn user before cancelling reconciled payment entry ([61d6d2f](61d6d2f344))
2026-06-02 16:55:08 +00:00
Mihir Kandoi
23181b3962 Merge pull request #55547 from frappe/version-16-hotfix 2026-06-02 22:23:38 +05:30
Khushi Rawat
d57e86362a Merge pull request #55549 from frappe/mergify/bp/version-16-hotfix/pr-55484
fix: use fiscal year instead of calendar year in accounting dashboard number cards (backport #55484)
2026-06-02 17:55:57 +05:30
khushi8112
1521410125 fix: set a fallback value if no fiscal year set
(cherry picked from commit c68918bc18)
2026-06-02 11:26:28 +00:00
khushi8112
81d10d32f2 fix: use fiscal year instead of calendar year in accounting dashboard number cards
(cherry picked from commit e8fff2fdad)
2026-06-02 11:26:28 +00:00
ruthra kumar
2213c1ffad Merge pull request #55545 from frappe/mergify/bp/version-16-hotfix/pr-54979
fix(ppr): make default_advance_account optional (backport #54979)
2026-06-02 16:48:36 +05:30
Diptanil Saha
37814bf6cd ci: fix for pull request finding 'CodeQL / Workflow does not contain permissions'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-02 16:36:50 +05:30
mergify[bot]
ceb10422ae fix: unable to submit subcontracted job card (backport #55537) (#55540)
* fix: unable to submit subcontracted job card (#55537)

(cherry picked from commit 0a49403838)

# Conflicts:
#	erpnext/controllers/subcontracting_controller.py

* chore: resolve conflicts

---------

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-06-02 10:47:30 +00:00
Khushi Rawat
072a853fad Merge pull request #55543 from frappe/mergify/bp/version-16-hotfix/pr-55539
feat(payment-entry): warn user before cancelling reconciled payment entry (backport #55539)
2026-06-02 15:54:59 +05:30
Dany Robert
0bbc493213 fix: update default_advance_account type
(cherry picked from commit 30b9e11303)
2026-06-02 10:07:02 +00:00
Dany Robert
7a7cc31523 fix(ppr): make default_advance_account optional
(cherry picked from commit 4b1d369ac6)
2026-06-02 10:07:02 +00:00
khushi8112
61d6d2f344 feat(payment-entry): warn user before cancelling reconciled payment entry
(cherry picked from commit f0ba54d957)
2026-06-02 10:00:28 +00:00
rohitwaghchaure
b89a34970b Merge pull request #55519 from frappe/mergify/bp/version-16-hotfix/pr-55415
fix(stock): allow to create quality inspection after purchase/delivery (backport #55415)
2026-06-02 11:51:45 +05:30
Rushabh Mehta
0fe44e1a67 Merge pull request #55534 from rmehta/feat/build-and-upload-assets-v16
feat: build and upload assets to GitHub Releases
2026-06-02 07:38:15 +05:30
Rushabh Mehta
4c05ebc21e feat: build and upload assets to GitHub Releases 2026-06-02 06:45:25 +05:30
mergify[bot]
224426e06b fix(pos): escape html output in pos page templates (backport #55527) (#55529)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(pos): escape html output in pos page templates (#55527)
2026-06-02 00:46:54 +05:30
rohitwaghchaure
980cefa169 chore: fixed conflicts 2026-06-01 22:10:06 +05:30
rohitwaghchaure
8d12a89558 chore: fixed conflicts 2026-06-01 22:07:42 +05:30
mergify[bot]
d1b2425b2b chore(serial_and_batch_bundle): remove update_serial_or_batch method (backport #55481) (#55516)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-06-01 16:15:54 +00:00
Sudharsanan11
1679680d8e fix(stock): add warning message to notify the user to configure the inspection
(cherry picked from commit e003fe4de0)
2026-06-01 16:14:11 +00:00
Sudharsanan11
51a140a2bd fix(stock): allow to create quality inspection after purchase/delivery
(cherry picked from commit c6a88ab1d2)

# Conflicts:
#	erpnext/controllers/stock_controller.py
2026-06-01 16:14:11 +00:00
rohitwaghchaure
13e1159c41 Merge pull request #55412 from frappe/mergify/bp/version-16-hotfix/pr-55377
refactor(sales_invoice): replace sql with qb in get_all_mode_of_payments (backport #55377)
2026-06-01 21:41:40 +05:30
MochaMind
056f622634 chore: sync translations to version-16-hotfix (#55426) 2026-06-01 21:38:27 +05:30
mergify[bot]
fe585dc225 fix(je): preserve account on duplicate row when party row exists (backport #55180) (#55512)
fix(je): preserve account on duplicate row when party row exists (#55180)

(cherry picked from commit 57dbac712f)

Co-authored-by: Gajendra Nishad <75714258+gajjug004@users.noreply.github.com>
2026-06-01 18:46:07 +05:30
mergify[bot]
5393c93675 fix(pos): escape item data on pos item selector (backport #55503) (#55508)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix(pos): escape item data on pos item selector (#55503)
2026-06-01 18:27:30 +05:30
ruthra kumar
4af265c48f Merge pull request #55502 from frappe/mergify/bp/version-16/pr-55495
fix: opening bal double counting in Process Period Closing Voucher (backport #55495)
2026-06-01 16:00:41 +05:30
ruthra kumar
fdf5439ece Merge pull request #55499 from frappe/mergify/bp/version-16-hotfix/pr-55495
fix: opening bal double counting in Process Period Closing Voucher (backport #55495)
2026-06-01 15:51:29 +05:30
ruthra kumar
ce97a74c5f test: prevent double counting of opening balances
(cherry picked from commit 7f2af123ee)
2026-06-01 09:36:47 +00:00
ruthra kumar
e955b4a3b9 refactor: color coded status in list view
(cherry picked from commit cfeffbb354)
2026-06-01 09:36:46 +00:00
ruthra kumar
de42a9e86e refactor: tabbed view for process period closing voucher
(cherry picked from commit 1960c81619)
2026-06-01 09:36:46 +00:00
ruthra kumar
5206b279b6 refactor: only consider non-opening balance for Balance sheet accounts
(cherry picked from commit a2b8334046)
2026-06-01 09:36:46 +00:00
ruthra kumar
de1256467f test: prevent double counting of opening balances
(cherry picked from commit 7f2af123ee)
2026-06-01 09:33:09 +00:00
ruthra kumar
1029ef46a7 refactor: color coded status in list view
(cherry picked from commit cfeffbb354)
2026-06-01 09:33:08 +00:00
ruthra kumar
c59dc684fe refactor: tabbed view for process period closing voucher
(cherry picked from commit 1960c81619)
2026-06-01 09:33:08 +00:00
ruthra kumar
d2bbf6d32b refactor: only consider non-opening balance for Balance sheet accounts
(cherry picked from commit a2b8334046)
2026-06-01 09:33:08 +00:00
mergify[bot]
0c946f2420 fix: check perm for account (backport #55479) (#55483)
fix: check perm for account (#55479)

(cherry picked from commit dd1d2925d5)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2026-06-01 14:29:16 +05:30
mergify[bot]
fc842fb45f fix: pick correct name when creating user from RFQ (backport #55468) (#55472)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: pick correct name when creating user from RFQ (#55468)
2026-06-01 05:58:12 +00:00
mergify[bot]
ef2700bec6 fix(pos): preserve contacts and enforce permissions in set_customer_info (backport #55463) (#55466)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix(pos): preserve contacts and enforce permissions in set_customer_info (#55463)
2026-06-01 05:12:48 +05:30
mergify[bot]
7c5d617049 fix(issue): check permission before issue status modification (backport #55458) (#55460)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix(issue): check permission before issue status modification (#55458)
2026-06-01 00:57:21 +05:30
mergify[bot]
2f51c48fd8 refactor: task_info portal pages (backport #55448) (#55454)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-05-31 22:06:47 +05:30
mergify[bot]
aa5dfde23b fix(book_appointment): when scheduling is disabled, block API endpoints (backport #55455) (#55457)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix(book_appointment): when scheduling is disabled, block API endpoints (#55455)
2026-05-31 16:10:20 +00:00
MochaMind
ad267ec295 chore: update POT file (#55451) 2026-05-31 15:05:47 +02:00
mergify[bot]
5c51145984 refactor(pos_profile): migrating raw sql to qb in set_defaults (backport #55447) (#55450)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2026-05-31 09:45:28 +00:00
mergify[bot]
277a0723ef fix(stock): change qb to qb get_query to fix filter issues (backport #55443) (#55445)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(stock): change qb to qb get_query to fix filter issues (#55443)
2026-05-31 07:09:40 +00:00
mergify[bot]
36a54ca40b chore: mark as out of beta (backport #55439) (#55441)
* chore: mark as out of beta (#55439)

(cherry picked from commit aed957e7d1)

* chore: remove newer property allow_bulk_edit

not supported on v16

* chore(Bank Statement Import): mark as out of beta

---------

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2026-05-30 19:28:31 +00:00
mergify[bot]
7426aaf1e2 fix(regional): Japanese CT Rate (backport #54998) (#55438)
Co-authored-by: mh35 <mh35jp@gmail.com>
fix(regional): Japanese CT Rate (#54998)
2026-05-30 22:00:04 +05:30
mergify[bot]
2feb8eb370 fix(quotation): made customer contact column visible (backport #55433) (#55435)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix(quotation): made customer contact column visible (#55433)
2026-05-30 19:33:01 +05:30
rohitwaghchaure
22c26f42e9 Merge pull request #55425 from frappe/mergify/bp/version-16-hotfix/pr-55417
fix: billing address does not belongs to the company error (backport #55417)
2026-05-30 12:43:06 +05:30
MochaMind
75465035ae chore: sync translations to version-16-hotfix (#55420) 2026-05-29 23:04:48 +05:30
Rohit Waghchaure
e1f29de078 fix: billing address does not belongs to the company error
(cherry picked from commit 9df07b367a)
2026-05-29 17:24:13 +00:00
mergify[bot]
26d94c5594 ci: configure upstream fetch refspec so git fetch creates tracking refs (backport #55422) (#55423)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 17:01:32 +00:00
mergify[bot]
542aff2677 ci: split sync into orchestrator + per-branch runners, generalise for any app (backport #55414) (#55418)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 20:37:33 +05:30
Loic Oberle
db4e459a5a refactor(sales_invoice): replace sql with qb in get_all_mode_of_payments (#55377)
(cherry picked from commit 618045ec98)
2026-05-29 12:05:16 +00:00
Khushi Rawat
e42c3961a9 Merge pull request #55409 from frappe/mergify/bp/version-16-hotfix/pr-55397
fix: item master list view UI cleanup (backport #55397)
2026-05-29 16:30:05 +05:30
Khushi Rawat
63aff0098c Merge pull request #55254 from frappe/mergify/bp/version-16-hotfix/pr-55126
fix: asset scrap flow related changes (backport #55126)
2026-05-29 15:48:03 +05:30
Khushi Rawat
6d747420a3 Merge pull request #55359 from frappe/mergify/bp/version-16-hotfix/pr-55137
fix: use get_query instead of get_all for data fetching (backport #55137)
2026-05-29 15:36:28 +05:30
Khushi Rawat
d82e03edb6 fix: import DateTimeLikeObject 2026-05-29 15:21:58 +05:30
khushi8112
2d554c05d6 fix: item master list view UI cleanup
(cherry picked from commit 69ee7e93d8)
2026-05-29 09:50:18 +00:00
mergify[bot]
876995a35f ci: fix branch base and per-language commits in sync-hotfix-translations (backport #55405) (#55407)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
fix branch base and per-language commits in sync-hotfix-translations (#55405)
2026-05-29 15:10:40 +05:30
Khushi Rawat
abe19e1212 fix: resolve conflict 2026-05-29 15:05:41 +05:30
Nishka Gosalia
bfc8683e52 Merge pull request #55403 from frappe/mergify/bp/version-16-hotfix/pr-55400
fix: Make Distributed Discount Amount field read only (backport #55400)
2026-05-29 14:09:18 +05:30
Nishka Gosalia
84ce7c720e Merge pull request #55402 from frappe/mergify/bp/version-16-hotfix/pr-55399
fix: over order allowance setting fix (backport #55399)
2026-05-29 13:00:37 +05:30
nishkagosalia
29441b7249 fix: Make Distributed Discount Amount field read only
(cherry picked from commit 512c95529e)
2026-05-29 07:30:01 +00:00
nishkagosalia
41b2de35a9 fix: over order allowance setting fix
(cherry picked from commit 30011963bc)
2026-05-29 07:07:59 +00:00
mergify[bot]
faa15731cb fix(stock): get_actual_qty during cancellations (backport #55388) (#55392)
Co-authored-by: archielister <archie.lister@lush.co.uk>
fix(stock): get_actual_qty during cancellations (#55388)
2026-05-28 22:40:24 +05:30
Nishka Gosalia
0ce697b170 Merge pull request #55373 from frappe/mergify/bp/version-16-hotfix/pr-55367
fix(UX): Move title field to More Info (backport #55367)
2026-05-28 16:24:35 +05:30
mergify[bot]
933ac0108c fix: new bom version should not recalculate operations through routing (backport #55370) (#55372)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: new bom version should not recalculate operations through routing (#55370)
2026-05-28 16:01:52 +05:30
nishkagosalia
b74e365421 fix: merge conflicts 2026-05-28 15:35:10 +05:30
nishkagosalia
20592fc25d fix(UX): Move title field to More Info
(cherry picked from commit 34c24b86fa)

# Conflicts:
#	erpnext/accounts/doctype/pos_invoice/pos_invoice.json
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.json
#	erpnext/buying/doctype/purchase_order/purchase_order.json
#	erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
#	erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
#	erpnext/selling/doctype/quotation/quotation.json
#	erpnext/selling/doctype/sales_order/sales_order.json
#	erpnext/stock/doctype/delivery_note/delivery_note.json
#	erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
2026-05-28 09:39:37 +00:00
Nishka Gosalia
4ac003a804 Merge pull request #55369 from frappe/mergify/bp/version-16-hotfix/pr-55340
feat: over order allowance setting (backport #55340)
2026-05-28 14:38:34 +05:30
nishkagosalia
08eaaa5b83 feat: over order allowance setting
(cherry picked from commit 355d71dbd2)
2026-05-28 08:49:11 +00:00
khushi8112
7cbef15596 fix: use get_query instead of get_all for data fetching
(cherry picked from commit 1fd99337b3)
2026-05-27 19:17:29 +00:00
mergify[bot]
3a7af0e59d ci: add node setup on sync translations to version 16 hotfix (backport #55355) (#55356)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix (#55355)
2026-05-27 18:23:23 +00:00
mergify[bot]
65b6da552e ci: sync translations from develop to version-16-hotfix (backport #55348) (#55353)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
fix (#55348)
2026-05-27 16:30:59 +00:00
MochaMind
91dcb96307 chore: update POT file (#55351) 2026-05-27 21:12:08 +05:30
Lakshit Jain
aa6c45bae1 Merge pull request #55343 from frappe/mergify/bp/version-16-hotfix/pr-55330
fix(tds): treat NULL and empty-string tax_withholding_group as equivalent (backport #55330)
2026-05-27 17:22:29 +05:30
Lakshit Jain
8c4d5d343b Merge pull request #55342 from frappe/mergify/bp/version-16-hotfix/pr-55333
fix(custom_financial_template): sum account closing balances across dimensions (backport #55333)
2026-05-27 17:21:24 +05:30
ljain112
8b1d9817a6 fix: changes as per review
(cherry picked from commit 251e7b623c)
2026-05-27 11:24:51 +00:00
ljain112
82e12d2d52 fix(tds): treat NULL and empty-string tax_withholding_group as equivalent
(cherry picked from commit a85f8a64b1)
2026-05-27 11:24:51 +00:00
ljain112
3359e20d06 fix(custom_financial_template): sum account closing balances across dimensions
(cherry picked from commit 4a49a205b3)
2026-05-27 11:24:13 +00:00
mergify[bot]
198970cdee fix(selling): handle None values while grouping opportunities by utm … (backport #55300) (#55328)
Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
fix(selling): handle None values while grouping opportunities by utm … (#55300)
2026-05-27 07:17:24 +00:00
rohitwaghchaure
9919b3d75c Merge pull request #55299 from frappe/mergify/bp/version-16-hotfix/pr-55242
fix: stock reco for legacy serial nos (backport #55242)
2026-05-27 12:16:07 +05:30
mergify[bot]
ae92a82930 fix(manufacturing): allow to edit batch size while creating a work order (backport #55058) (#55326)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(manufacturing): allow to edit batch size while creating a work order (#55058)
2026-05-27 06:22:59 +00:00
mergify[bot]
8b4ad229e7 Merge branch 'version-16-hotfix' into mergify/bp/version-16-hotfix/pr-55242 2026-05-27 06:18:17 +00:00
rohitwaghchaure
64ee4b8d99 Merge pull request #55320 from rohitwaghchaure/fixed-support-69397
fix: material transfer in transit issue
2026-05-27 11:23:21 +05:30
Rohit Waghchaure
356bb7878f fix: material transfer in transit issue 2026-05-27 10:29:46 +05:30
mergify[bot]
8925a6527b chore: remove frappe-semgrep-rules submodule (backport #55083) (#55318)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-05-27 01:00:48 +00:00
Rohit Waghchaure
67c922cdf3 fix: stock reco for legacy serial nos
(cherry picked from commit 9d5fd11bcd)
2026-05-26 10:29:25 +00:00
khushi8112
e3f03a21c3 fix: asset scrap flow related changes
(cherry picked from commit 21bb8fe979)

# Conflicts:
#	erpnext/assets/doctype/asset/depreciation.py
2026-05-25 10:16:25 +00:00
243 changed files with 468123 additions and 391212 deletions

52
.github/helper/merge_po_files.py vendored Normal file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Overlay develop's .po translations onto hotfix's .po files.
Called by sync_hotfix_translations.sh before `bench update-po-files`.
Merge rules:
a. msgid absent from develop → keep hotfix's existing msgstr
b. language not yet in hotfix → copy file as-is (bench will filter to main.pot)
c. msgid present in both → use develop's msgstr
"""
from datetime import datetime, timezone
from pathlib import Path
from babel.messages.pofile import read_po, write_po
DEVELOP = Path("/tmp/develop-po/erpnext/locale/")
LOCALE = Path("./apps/erpnext/erpnext/locale/")
added = updated = 0
for src in sorted(DEVELOP.glob("*.po")):
dst = LOCALE / src.name
with src.open("rb") as f:
dev = read_po(f)
if not dst.exists():
dev.revision_date = datetime.now(timezone.utc)
with dst.open("wb") as f:
write_po(f, dev)
added += 1
print(f" [new] {src.name}")
continue
with dst.open("rb") as f:
hf = read_po(f)
changes = 0
for msg in hf:
if msg.id and msg.id in dev and dev[msg.id].string and dev[msg.id].string != msg.string:
msg.string = dev[msg.id].string
changes += 1
if changes:
hf.revision_date = datetime.now(timezone.utc)
with dst.open("wb") as f:
write_po(f, hf)
updated += 1
print(f" [updated] {src.name} ({changes} msgstr(s) from develop)")
else:
print(f" [no-op] {src.name}")
print(f"\n{added} new language(s), {updated} updated.")

View File

@@ -0,0 +1,121 @@
#!/bin/bash
# Syncs Crowdin translations from develop to a hotfix branch.
# Merge logic: see merge_po_files.py.
# Env: GH_TOKEN, PR_REVIEWER, GITHUB_WORKSPACE, APP_NAME, GITHUB_REPOSITORY
# (all set by Actions).
set -e
HOTFIX_BRANCH="${HOTFIX_BRANCH:?HOTFIX_BRANCH env var is required}"
APP_NAME="${APP_NAME:?APP_NAME env var is required}"
cd ~ || exit
echo "=== Setting up bench ==="
pip install frappe-bench
bench -v init frappe-bench --skip-assets --skip-redis-config-generation --python "$(which python)"
cd ./frappe-bench || exit
bench get-app --skip-assets "${APP_NAME}" "${GITHUB_WORKSPACE}"
echo "=== Setting up sync_translations_${HOTFIX_BRANCH} branch ==="
cd "./apps/${APP_NAME}" || exit
git config user.email "developers@erpnext.com"
git config user.name "frappe-pr-bot"
git remote set-url upstream "https://github.com/${GITHUB_REPOSITORY}.git"
git config remote.upstream.fetch "+refs/heads/*:refs/remotes/upstream/*"
gh auth setup-git
git fetch upstream "${HOTFIX_BRANCH}"
if git ls-remote --exit-code --heads upstream "sync_translations_${HOTFIX_BRANCH}" >/dev/null 2>&1; then
git fetch upstream "sync_translations_${HOTFIX_BRANCH}"
git checkout -b "sync_translations_${HOTFIX_BRANCH}" "upstream/sync_translations_${HOTFIX_BRANCH}"
git merge -X theirs "upstream/${HOTFIX_BRANCH}" --no-edit
else
git checkout -b "sync_translations_${HOTFIX_BRANCH}" "upstream/${HOTFIX_BRANCH}"
fi
cd ../.. || exit
echo "=== Fetching develop's .po files ==="
mkdir -p /tmp/develop-po
git -C "${GITHUB_WORKSPACE}" fetch origin develop
git -C "${GITHUB_WORKSPACE}" archive origin/develop "${APP_NAME}/locale/" \
| tar -xf - -C /tmp/develop-po/
po_count=$(find "/tmp/develop-po/${APP_NAME}/locale" -name "*.po" | wc -l)
if [ "${po_count}" -eq 0 ]; then
echo "ERROR: No .po files found in develop's archive. Aborting." >&2
exit 1
fi
echo "Extracted ${po_count} .po file(s) from develop."
echo "=== Merging and reconciling ==="
env/bin/python "${GITHUB_WORKSPACE}/.github/helper/merge_po_files.py"
bench update-po-files --app "${APP_NAME}"
cd "./apps/${APP_NAME}" || exit
if git diff --quiet "${APP_NAME}/locale/" && [ -z "$(git ls-files --others --exclude-standard "${APP_NAME}/locale/")" ]; then
echo "Translations are already up to date. No PR needed."
exit 0
fi
echo "Changed files:"
git diff --name-only "${APP_NAME}/locale/"
git ls-files --others --exclude-standard "${APP_NAME}/locale/"
echo "=== Committing ==="
while IFS= read -r file; do
git add "${file}"
lang=$(basename "${file}" .po)
git commit -m "chore: add ${lang} translation to ${HOTFIX_BRANCH}"
done < <(git ls-files --others --exclude-standard "${APP_NAME}/locale/" | grep '\.po$' | sort)
while IFS= read -r file; do
git add "${file}"
if ! git diff --staged --quiet -- "${file}"; then
lang=$(basename "${file}" .po)
git commit -m "chore: sync ${lang} translation to ${HOTFIX_BRANCH}"
else
git restore --staged -- "${file}"
fi
done < <(git diff --name-only "${APP_NAME}/locale/" | grep '\.po$' | sort)
if git ls-remote --exit-code --heads upstream "sync_translations_${HOTFIX_BRANCH}" >/dev/null 2>&1; then
git fetch upstream "sync_translations_${HOTFIX_BRANCH}"
git merge -X ours "upstream/sync_translations_${HOTFIX_BRANCH}" --no-edit
fi
git push -u upstream sync_translations_${HOTFIX_BRANCH}
echo "=== Opening PR (if not already open) ==="
existing_pr=$(gh pr list \
--base "${HOTFIX_BRANCH}" \
--head "sync_translations_${HOTFIX_BRANCH}" \
--state open \
--json number \
--jq 'length' \
-R "${GITHUB_REPOSITORY}")
if [ "${existing_pr}" -gt 0 ]; then
echo "PR already open — branch updated in place. No new PR needed."
exit 0
fi
gh pr create \
--base "${HOTFIX_BRANCH}" \
--head "sync_translations_${HOTFIX_BRANCH}" \
--title "chore: sync translations to ${HOTFIX_BRANCH}" \
--body "Automated sync of Crowdin translations from \`develop\` to \`${HOTFIX_BRANCH}\`.
A 3-way merge is performed per language, then \`bench update-po-files\` reconciles each \`.po\` against hotfix's \`main.pot\`:
| Case | Condition | Result |
|------|-----------|--------|
| **a** | \`msgid\` in hotfix's \`main.pot\`, **not** in develop's \`.po\` | Hotfix's existing \`msgstr\` is **preserved** (string removed from develop but still needed in hotfix) |
| **b** | \`msgid\` **not** in hotfix's \`main.pot\` | **Dropped** from hotfix's \`.po\` |
| **c** | \`msgid\` in both hotfix's \`main.pot\` and develop's \`.po\` | Develop's \`msgstr\` is used (Crowdin translation wins) |
Generated by the \`sync-hotfix-translations\` workflow." \
--label "translation" \
--label "skip-release-notes" \
--reviewer "${PR_REVIEWER}" \
-R "${GITHUB_REPOSITORY}"

View File

@@ -0,0 +1,70 @@
name: Build and Upload Assets
on:
push:
branches:
- develop
- 'version-*'
concurrency:
group: build-assets-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
build-assets:
name: Build JS/CSS and upload to release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
repository: frappe/frappe
path: apps/frappe
ref: ${{ github.ref_name }}
- uses: actions/checkout@v4
with:
path: apps/erpnext
- name: Create bench structure
run: |
mkdir -p sites
printf "frappe\nerpnext\n" > sites/apps.txt
- uses: actions/setup-node@v4
with:
node-version: 24
cache: yarn
cache-dependency-path: apps/frappe/yarn.lock
- name: Install frappe JS dependencies
working-directory: apps/frappe
run: yarn install --frozen-lockfile
- name: Install erpnext JS dependencies
working-directory: apps/erpnext
run: yarn install --frozen-lockfile --ignore-scripts
- name: Link node_modules into public/
working-directory: apps/frappe
run: ln -s "$PWD/node_modules" frappe/public/node_modules
- name: Build assets (production)
working-directory: apps/frappe
run: yarn run production
- name: Package assets
working-directory: apps/erpnext
run: tar czf erpnext-assets.tar.gz -C ../../sites/assets/erpnext dist
- name: Upload to rolling release
working-directory: apps/erpnext
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="assets-${GITHUB_REF_NAME//\//-}"
gh release create "$TAG" --prerelease --title "Assets: $GITHUB_REF_NAME" --notes "" 2>/dev/null || true
gh release upload "$TAG" erpnext-assets.tar.gz --clobber

View File

@@ -0,0 +1,25 @@
name: Review translation PRs
description: "Posts review comments with relevant translation changes that are hard to inspect in the diff view."
on:
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
paths:
- "**/*.po"
- "**/*.pot"
concurrency:
group: po-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
review-po-pr:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
pull-requests: write
steps:
- uses: alyf-de/po-review-action@v1.0.0

View File

@@ -0,0 +1,52 @@
# Runner — maintain this file on each hotfix branch, not on develop.
#
# Fires when main.pot changes on this branch (i.e. after a POT update PR
# merges), or when dispatched by the orchestrator on develop (weekly schedule).
#
# Uses github.ref_name so the file is identical across all hotfix branches
# with no branch-specific edits required.
name: Run hotfix translation sync
on:
workflow_dispatch:
# One run at a time per branch. cancel-in-progress: false to avoid leaving
# an orphaned remote branch from a mid-flight git push + gh pr create.
concurrency:
group: sync-hotfix-translations-${{ github.ref_name }}
cancel-in-progress: false
jobs:
sync-translations:
name: Sync translations from develop into ${{ github.ref_name }}
runs-on: ubuntu-latest
permissions:
contents: write
env:
HOTFIX_BRANCH: ${{ github.ref_name }}
APP_NAME: ${{ github.event.repository.name }}
steps:
- name: Checkout ${{ env.HOTFIX_BRANCH }}
uses: actions/checkout@v6
with:
ref: ${{ env.HOTFIX_BRANCH }}
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.14"
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
- name: Run sync script
run: |
bash "${GITHUB_WORKSPACE}/.github/helper/sync_hotfix_translations.sh"
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
PR_REVIEWER: diptanilsaha

View File

@@ -0,0 +1,39 @@
# Orchestrator — lives on develop only.
#
# Triggers on the weekly schedule and dispatches the runner workflow on each
# hotfix branch listed in the matrix. To add or remove a branch, edit the
# matrix below.
#
# POT-change triggers are handled by the runner on each hotfix branch
# (run-hotfix-translation-sync.yml), since GitHub only evaluates a workflow
# from the branch that receives the push.
name: Sync translations to hotfix branches
on:
schedule:
# 10:00 UTC Monday
- cron: "0 10 * * 1"
workflow_dispatch:
permissions:
contents: read
jobs:
trigger-runners:
name: Trigger sync → ${{ matrix.hotfix_branch }}
runs-on: ubuntu-latest
strategy:
matrix:
hotfix_branch:
- version-16-hotfix
fail-fast: false
steps:
- name: Dispatch runner on ${{ matrix.hotfix_branch }}
run: |
gh workflow run run-hotfix-translation-sync.yml \
--repo "${{ github.repository }}" \
--ref "${{ matrix.hotfix_branch }}"
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}

View File

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

View File

@@ -518,6 +518,7 @@ def get_account_autoname(account_number, account_name, company):
def update_account_number(name, account_name, account_number=None, from_descendant=False):
_ensure_idle_system()
account = frappe.get_cached_doc("Account", name)
account.check_permission("write")
if not account:
return
@@ -579,10 +580,12 @@ def update_account_number(name, account_name, account_number=None, from_descenda
@frappe.whitelist()
def merge_account(old, new):
_ensure_idle_system()
# Validate properties before merging
new_account = frappe.get_cached_doc("Account", new)
old_account = frappe.get_cached_doc("Account", old)
new_account.check_permission("write")
old_account.check_permission("write")
if not new_account:
throw(_("Account {0} does not exist").format(new))

View File

@@ -0,0 +1,449 @@
{
"country_code": "nz",
"name": "New Zealand - Chart of Accounts with Account Numbers",
"disabled": "No",
"tree": {
"Application of Funds (Assets)": {
"Current Assets": {
"Bank Accounts": {
"Business Transaction Account": {
"account_number": "11011",
"account_type": "Bank"
},
"Business Savings Account": {
"account_number": "11012",
"account_type": "Bank"
},
"account_number": "11010",
"is_group": 1
},
"Cash on Hand": {
"account_number": "11020",
"account_type": "Cash"
},
"Accounts Receivable": {
"Debtors": {
"account_number": "11210",
"account_type": "Receivable"
},
"Provision for Doubtful Debts": {
"account_number": "11220"
},
"account_number": "11200",
"is_group": 1
},
"Inventory": {
"Stock on Hand": {
"account_number": "11311",
"account_type": "Stock"
},
"Work In Progress": {
"account_number": "11312",
"account_type": "Stock"
},
"account_number": "11310",
"account_type": "Stock",
"is_group": 1
},
"Prepayments": {
"Prepayments": {
"account_number": "11411"
},
"Supplier Advances": {
"account_number": "11412"
},
"Deferred Expense": {
"account_number": "11413"
},
"account_number": "11410",
"is_group": 1
},
"GST Receivable": {
"account_number": "11510",
"account_type": "Tax"
},
"Income Tax Receivable": {
"account_number": "11520",
"account_type": "Tax"
},
"account_number": "11000",
"is_group": 1
},
"Fixed Assets": {
"Plant & Equipment": {
"Plant & Equipment": {
"account_number": "16011",
"account_type": "Fixed Asset"
},
"Accumulated Depreciation - Plant & Equipment": {
"account_number": "16012",
"account_type": "Accumulated Depreciation"
},
"account_number": "16010",
"is_group": 1
},
"Motor Vehicles": {
"Motor Vehicles": {
"account_number": "16021",
"account_type": "Fixed Asset"
},
"Accumulated Depreciation - Motor Vehicles": {
"account_number": "16022",
"account_type": "Accumulated Depreciation"
},
"account_number": "16020",
"is_group": 1
},
"Office Equipment": {
"Office Equipment": {
"account_number": "16031",
"account_type": "Fixed Asset"
},
"Accumulated Depreciation - Office Equipment": {
"account_number": "16032",
"account_type": "Accumulated Depreciation"
},
"account_number": "16030",
"is_group": 1
},
"Buildings": {
"Buildings": {
"account_number": "16041",
"account_type": "Fixed Asset"
},
"Accumulated Depreciation - Buildings": {
"account_number": "16042",
"account_type": "Accumulated Depreciation"
},
"account_number": "16040",
"is_group": 1
},
"Computer Equipment": {
"Computer Equipment": {
"account_number": "16051",
"account_type": "Fixed Asset"
},
"Accumulated Depreciation - Computer Equipment": {
"account_number": "16052",
"account_type": "Accumulated Depreciation"
},
"account_number": "16050",
"is_group": 1
},
"Capital Work in Progress": {
"account_number": "16090",
"account_type": "Capital Work in Progress"
},
"account_number": "16000",
"is_group": 1
},
"account_number": "10000",
"root_type": "Asset"
},
"Source of Funds (Liabilities)": {
"Current Liabilities": {
"Accounts Payable": {
"Creditors": {
"account_number": "21010",
"account_type": "Payable"
},
"account_number": "21000",
"is_group": 1
},
"Goods Received Not Invoiced": {
"account_number": "21100",
"account_type": "Stock Received But Not Billed"
},
"Asset Received Not Invoiced": {
"account_number": "21110",
"account_type": "Asset Received But Not Billed"
},
"Service Received Not Invoiced": {
"account_number": "21120",
"account_type": "Service Received But Not Billed"
},
"Accrued Expenses": {
"account_number": "21200"
},
"Wages Payable": {
"account_number": "21300"
},
"PAYE Payable": {
"account_number": "22010"
},
"KiwiSaver Payable": {
"account_number": "22020"
},
"ACC Payable": {
"account_number": "22030"
},
"Credit Cards": {
"Business Credit Card": {
"account_number": "22110"
},
"account_number": "22100",
"is_group": 1
},
"Customer Advances": {
"account_number": "22200"
},
"Deferred Revenue": {
"account_number": "22210"
},
"Provisional Account": {
"account_number": "22220"
},
"Tax Liabilities": {
"GST Payable": {
"account_number": "22310",
"account_type": "Tax"
},
"GST Suspense": {
"account_number": "22320",
"account_type": "Tax"
},
"FBT Payable": {
"account_number": "22330",
"account_type": "Tax"
},
"Income Tax Payable": {
"account_number": "22340",
"account_type": "Tax"
},
"account_number": "22300",
"is_group": 1
},
"account_number": "21500",
"is_group": 1
},
"Non-Current Liabilities": {
"Bank Loans": {
"Bank Loan": {
"account_number": "25011"
},
"account_number": "25010",
"is_group": 1
},
"Lease Liabilities": {
"Lease Liability": {
"account_number": "25021"
},
"account_number": "25020",
"is_group": 1
},
"Shareholder Loans": {
"Shareholder Loan": {
"account_number": "25031"
},
"account_number": "25030",
"is_group": 1
},
"account_number": "25000",
"is_group": 1
},
"account_number": "20000",
"root_type": "Liability"
},
"Equity": {
"Share Capital": {
"account_number": "31010",
"account_type": "Equity"
},
"Drawings": {
"account_number": "31020",
"account_type": "Equity"
},
"Current Year Earnings": {
"account_number": "35010",
"account_type": "Equity"
},
"Retained Earnings": {
"account_number": "35020",
"account_type": "Equity"
},
"account_number": "30000",
"root_type": "Equity"
},
"Income": {
"Sales": {
"account_number": "41010",
"account_type": "Income Account"
},
"Other Income": {
"Interest Income": {
"account_number": "47010",
"account_type": "Income Account"
},
"Rounding Gain/Loss": {
"account_number": "47020",
"account_type": "Income Account"
},
"Foreign Exchange Gain": {
"account_number": "47030",
"account_type": "Income Account"
},
"account_number": "47000",
"is_group": 1
},
"account_number": "40000",
"root_type": "Income"
},
"Expenses": {
"Cost of Goods Sold": {
"Purchases": {
"account_number": "51010",
"account_type": "Cost of Goods Sold"
},
"Freight Inwards": {
"account_number": "51020",
"account_type": "Expenses Included In Valuation"
},
"Duty and Landing Costs": {
"account_number": "51030",
"account_type": "Expenses Included In Valuation"
},
"Stock Adjustment": {
"account_number": "51040",
"account_type": "Stock Adjustment"
},
"Stock Write Off": {
"account_number": "51050",
"account_type": "Stock Adjustment"
},
"account_number": "51000",
"account_type": "Cost of Goods Sold",
"is_group": 1
},
"Operating Expenses": {
"Wages & Salaries": {
"account_number": "61010",
"account_type": "Expense Account"
},
"KiwiSaver Employer Contribution": {
"account_number": "61020",
"account_type": "Expense Account"
},
"ACC Levies": {
"account_number": "61030",
"account_type": "Expense Account"
},
"Rent": {
"account_number": "65010",
"account_type": "Expense Account"
},
"Power": {
"account_number": "65020",
"account_type": "Expense Account"
},
"Telephone": {
"account_number": "66010",
"account_type": "Expense Account"
},
"Insurance": {
"account_number": "64010",
"account_type": "Expense Account"
},
"Accounting Fees": {
"account_number": "64020",
"account_type": "Expense Account"
},
"Legal Fees": {
"account_number": "64030",
"account_type": "Expense Account"
},
"Advertising and Marketing": {
"account_number": "65030",
"account_type": "Expense Account"
},
"Repairs and Maintenance": {
"account_number": "65040",
"account_type": "Expense Account"
},
"Freight and Courier": {
"account_number": "65050",
"account_type": "Expense Account"
},
"Operating Costs": {
"account_number": "65060",
"account_type": "Expense Account"
},
"account_number": "60000",
"is_group": 1
},
"Depreciation and Amortisation": {
"Depreciation - Plant & Equipment": {
"account_number": "62010",
"account_type": "Depreciation"
},
"Depreciation - Motor Vehicles": {
"account_number": "62020",
"account_type": "Depreciation"
},
"Depreciation - Office Equipment": {
"account_number": "62030",
"account_type": "Depreciation"
},
"Depreciation - Computer Equipment": {
"account_number": "62040",
"account_type": "Depreciation"
},
"account_number": "62000",
"is_group": 1
},
"Finance Costs": {
"Bank Charges": {
"account_number": "67010",
"account_type": "Expense Account"
},
"Interest Expense": {
"account_number": "67020",
"account_type": "Expense Account"
},
"Rounding Off": {
"account_number": "67030",
"account_type": "Round Off"
},
"Payment Discounts": {
"account_number": "67040",
"account_type": "Expense Account"
},
"account_number": "67000",
"is_group": 1
},
"Income Tax Expense": {
"account_number": "81010",
"account_type": "Expense Account"
},
"Foreign Exchange": {
"Exchange Gain/Loss": {
"account_number": "82010",
"account_type": "Expense Account"
},
"Unrealized Exchange Gain/Loss": {
"account_number": "82020",
"account_type": "Expense Account"
},
"account_number": "82000",
"is_group": 1
},
"Bad Debts": {
"account_number": "83010",
"account_type": "Expense Account"
},
"Write Off": {
"account_number": "83020",
"account_type": "Expense Account"
},
"Gain/Loss on Asset Disposal": {
"account_number": "83030",
"account_type": "Expense Account"
},
"Expenses Included In Asset Valuation": {
"account_number": "84010",
"account_type": "Expenses Included In Asset Valuation"
},
"account_number": "50000",
"root_type": "Expense"
}
}
}

View File

@@ -570,6 +570,17 @@
"account_number": "5000",
"is_group": 1,
"root_type": "Expense",
"Cost of Goods Sold": {
"account_number": "5001",
"is_group": 1,
"root_type": "Expense",
"Cost of Goods Sold": {
"account_number": "5010",
"is_group": 0,
"root_type": "Expense",
"account_type": "Cost of Goods Sold"
}
},
"Operating Expenses": {
"account_number": "5100",
"is_group": 1,

View File

@@ -10,6 +10,9 @@ frappe.ui.form.on("Accounts Settings", {
},
};
});
if (!frm.naming_controller) frm.naming_controller = new erpnext.NamingSeriesController(frm);
frm.naming_controller.render_table("transaction_naming_html", get_transactions(frm));
},
enable_immutable_ledger: function (frm) {
if (!frm.doc.enable_immutable_ledger) {
@@ -49,3 +52,16 @@ function toggle_tax_settings(frm, field_name) {
frm.set_value(other_field, 0);
}
}
function get_transactions(frm) {
const transactions = [
{ label: __("Journal Entry"), doctype: "Journal Entry" },
{ label: __("Payment Entry"), doctype: "Payment Entry" },
{ label: __("Purchase Invoice"), doctype: "Purchase Invoice" },
{ label: __("Purchase Order"), doctype: "Purchase Order" },
{ label: __("Purchase Receipt"), doctype: "Purchase Receipt" },
{ label: __("Sales Invoice"), doctype: "Sales Invoice" },
];
return transactions;
}

View File

@@ -22,9 +22,9 @@
"allow_multi_currency_invoices_against_single_party_account",
"confirm_before_resetting_posting_date",
"analytics_section",
"enable_discounts_and_margin",
"enable_accounting_dimensions",
"column_break_vtnr",
"enable_discounts_and_margin",
"journals_section",
"merge_similar_account_heads",
"deferred_accounting_settings_section",
@@ -43,7 +43,6 @@
"print_settings",
"show_inclusive_tax_in_print",
"show_taxes_as_table_in_print",
"column_break_12",
"show_payment_schedule_in_print",
"item_price_settings_section",
"maintain_same_internal_transaction_rate",
@@ -59,29 +58,30 @@
"payments_tab",
"section_break_jpd0",
"auto_reconcile_payments",
"exchange_gain_loss_posting_date",
"auto_reconciliation_job_trigger",
"reconciliation_queue_size",
"column_break_resa",
"exchange_gain_loss_posting_date",
"repost_section",
"column_break_mfor",
"repost_allowed_types",
"payment_options_section",
"fetch_payment_schedule_in_payment_request",
"enable_loyalty_point_program",
"column_break_ctam",
"fetch_payment_schedule_in_payment_request",
"invoicing_settings_tab",
"accounts_transactions_settings_section",
"over_billing_allowance",
"column_break_11",
"role_allowed_to_over_bill",
"credit_controller",
"make_payment_via_journal_entry",
"over_billing_allowance",
"credit_controller",
"role_allowed_to_over_bill",
"column_break_11",
"assets_tab",
"asset_settings_section",
"calculate_depr_using_total_days",
"column_break_gjcc",
"book_asset_depreciation_entry_automatically",
"calculate_depr_using_total_days",
"role_to_notify_on_depreciation_failure",
"column_break_gjcc",
"closing_settings_tab",
"period_closing_settings_section",
"ignore_account_closing_balance",
@@ -90,8 +90,8 @@
"reports_tab",
"remarks_section",
"general_ledger_remarks_length",
"column_break_lvjk",
"receivable_payable_remarks_length",
"column_break_lvjk",
"accounts_receivable_payable_tuning_section",
"receivable_payable_fetch_method",
"default_ageing_range",
@@ -103,11 +103,14 @@
"show_balance_in_coa",
"banking_section",
"enable_party_matching",
"automatically_run_rules_on_unreconciled_transactions",
"enable_fuzzy_matching",
"payment_request_section",
"create_pr_in_draft_status",
"budget_section",
"use_legacy_budget_controller"
"use_legacy_budget_controller",
"document_naming_tab",
"transaction_naming_html"
],
"fields": [
{
@@ -115,14 +118,14 @@
"description": "Address used to determine Tax Category in transactions",
"fieldname": "determine_address_tax_category_from",
"fieldtype": "Select",
"label": "Determine Address Tax Category From",
"label": "Determine Address Tax Category from",
"options": "Billing Address\nShipping Address"
},
{
"fieldname": "credit_controller",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Role allowed to bypass Credit Limit",
"label": "Role allowed to bypass credit limit",
"options": "Role"
},
{
@@ -130,7 +133,7 @@
"description": "Enabling this ensures each Purchase Invoice has a unique value in Supplier Invoice No. field within a particular fiscal year",
"fieldname": "check_supplier_invoice_uniqueness",
"fieldtype": "Check",
"label": "Check Supplier Invoice Number Uniqueness"
"label": "Check Supplier invoice number uniqueness"
},
{
"default": "0",
@@ -141,27 +144,29 @@
},
{
"default": "1",
"documentation_url": "https://docs.frappe.io/erpnext/accounts-settings#4-unlink-payment-on-cancellation-of-invoice",
"fieldname": "unlink_payment_on_cancellation_of_invoice",
"fieldtype": "Check",
"label": "Unlink Payment on Cancellation of Invoice"
"label": "Unlink Payment on cancellation of invoice"
},
{
"default": "1",
"documentation_url": "https://docs.frappe.io/erpnext/accounts-settings#8-unlink-advance-payment-on-cancellation-of-order",
"fieldname": "unlink_advance_payment_on_cancelation_of_order",
"fieldtype": "Check",
"label": "Unlink Advance Payment on Cancellation of Order"
"label": "Unlink Advance Payment on cancellation of order"
},
{
"default": "1",
"fieldname": "book_asset_depreciation_entry_automatically",
"fieldtype": "Check",
"label": "Book Asset Depreciation Entry Automatically"
"label": "Book Asset Depreciation entry automatically"
},
{
"default": "1",
"fieldname": "add_taxes_from_item_tax_template",
"fieldtype": "Check",
"label": "Automatically Add Taxes and Charges from Item Tax Template"
"label": "Automatically add Taxes and Charges from Item Tax Template"
},
{
"fieldname": "print_settings",
@@ -172,17 +177,13 @@
"default": "0",
"fieldname": "show_inclusive_tax_in_print",
"fieldtype": "Check",
"label": "Show Inclusive Tax in Print"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
"label": "Show inclusive tax in print"
},
{
"default": "0",
"fieldname": "show_payment_schedule_in_print",
"fieldtype": "Check",
"label": "Show Payment Schedule in Print"
"label": "Show Payment Schedule in print"
},
{
"fieldname": "currency_exchange_section",
@@ -208,7 +209,7 @@
"description": "Payment Terms from orders will be fetched into the invoices as is",
"fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check",
"label": "Automatically Fetch Payment Terms from Order/Quotation"
"label": "Automatically fetch Payment Terms from Order/Quotation"
},
{
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
@@ -220,7 +221,7 @@
"default": "1",
"fieldname": "automatically_process_deferred_accounting_entry",
"fieldtype": "Check",
"label": "Automatically Process Deferred Accounting Entry"
"label": "Automatically process deferred Accounting entry"
},
{
"fieldname": "deferred_accounting_settings_section",
@@ -236,7 +237,7 @@
"description": "If this is unchecked, direct GL entries will be created to book deferred revenue or expense",
"fieldname": "book_deferred_entries_via_journal_entry",
"fieldtype": "Check",
"label": "Book Deferred Entries Via Journal Entry"
"label": "Book deferred entries via Journal Entry"
},
{
"default": "0",
@@ -244,38 +245,37 @@
"description": "If this is unchecked Journal Entries will be saved in a Draft state and will have to be submitted manually",
"fieldname": "submit_journal_entries",
"fieldtype": "Check",
"label": "Submit Journal Entries"
"label": "Submit Journal entries"
},
{
"default": "Days",
"description": "If \"Months\" is selected, a fixed amount will be booked as deferred revenue or expense for each month irrespective of the number of days in a month. It will be prorated if deferred revenue or expense is not booked for an entire month",
"fieldname": "book_deferred_entries_based_on",
"fieldtype": "Select",
"label": "Book Deferred Entries Based On",
"label": "Book Deferred entries based on",
"options": "Days\nMonths"
},
{
"default": "0",
"fieldname": "delete_linked_ledger_entries",
"fieldtype": "Check",
"label": "Delete Accounting and Stock Ledger Entries on deletion of Transaction"
"label": "Delete Accounting and Stock Ledger entries on deletion of transaction"
},
{
"depends_on": "eval: doc.over_billing_allowance > 0",
"description": "Users with this role are allowed to over bill above the allowance percentage",
"fieldname": "role_allowed_to_over_bill",
"fieldtype": "Link",
"label": "Role Allowed to Over Bill ",
"label": "Role Allowed to over bill ",
"options": "Role"
},
{
"fieldname": "period_closing_settings_section",
"fieldtype": "Section Break",
"label": "Period Closing Settings"
"fieldtype": "Section Break"
},
{
"fieldname": "accounts_transactions_settings_section",
"fieldtype": "Section Break",
"label": "Credit Limit Settings"
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_11",
@@ -360,14 +360,14 @@
"default": "1",
"fieldname": "show_balance_in_coa",
"fieldtype": "Check",
"label": "Show Balances in Chart Of Accounts"
"label": "Show balances in Chart of Accounts"
},
{
"default": "0",
"description": "Split Early Payment Discount Loss into Income and Tax Loss",
"fieldname": "book_tax_discount_loss",
"fieldtype": "Check",
"label": "Book Tax Loss on Early Payment Discount"
"label": "Book tax loss on early payment discount"
},
{
"fieldname": "journals_section",
@@ -379,7 +379,7 @@
"description": "Rows with Same Account heads will be merged on Ledger",
"fieldname": "merge_similar_account_heads",
"fieldtype": "Check",
"label": "Merge Similar Account Heads"
"label": "Merge similar Account Heads"
},
{
"fieldname": "section_break_jpd0",
@@ -390,13 +390,13 @@
"default": "0",
"fieldname": "auto_reconcile_payments",
"fieldtype": "Check",
"label": "Auto Reconcile Payments"
"label": "Auto reconcile Payments"
},
{
"default": "0",
"fieldname": "show_taxes_as_table_in_print",
"fieldtype": "Check",
"label": "Show Taxes as Table in Print"
"label": "Show taxes as table in print"
},
{
"default": "0",
@@ -418,14 +418,14 @@
"description": "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ",
"fieldname": "ignore_account_closing_balance",
"fieldtype": "Check",
"label": "Ignore Account Closing Balance"
"label": "Ignore Account closing balance"
},
{
"default": "0",
"description": "Tax Amount will be rounded on a row(items) level",
"fieldname": "round_row_wise_tax",
"fieldtype": "Check",
"label": "Round Tax Amount Row-wise"
"label": "Round tax amount row-wise"
},
{
"fieldname": "reports_tab",
@@ -437,14 +437,14 @@
"description": "Truncates 'Remarks' column to set character length",
"fieldname": "general_ledger_remarks_length",
"fieldtype": "Int",
"label": "General Ledger"
"label": "General Ledger remarks length"
},
{
"default": "0",
"description": "Truncates 'Remarks' column to set character length",
"fieldname": "receivable_payable_remarks_length",
"fieldtype": "Int",
"label": "Accounts Receivable/Payable"
"label": "Accounts Receivable / Payable remarks length"
},
{
"fieldname": "column_break_lvjk",
@@ -478,7 +478,7 @@
"description": "Payment Requests made from Sales / Purchase Invoice will be put in Draft explicitly",
"fieldname": "create_pr_in_draft_status",
"fieldtype": "Check",
"label": "Create in Draft Status"
"label": "Create payment requests in Draft status"
},
{
"fieldname": "column_break_yuug",
@@ -493,14 +493,14 @@
"description": "Interval should be between 1 to 59 MInutes",
"fieldname": "auto_reconciliation_job_trigger",
"fieldtype": "Int",
"label": "Auto Reconciliation Job Trigger"
"label": "Auto Reconciliation job trigger"
},
{
"default": "5",
"description": "Documents Processed on each trigger. Queue Size should be between 5 and 100",
"fieldname": "reconciliation_queue_size",
"fieldtype": "Int",
"label": "Reconciliation Queue Size"
"label": "Reconciliation queue size"
},
{
"default": "0",
@@ -514,14 +514,14 @@
"description": "Only applies for Normal Payments",
"fieldname": "exchange_gain_loss_posting_date",
"fieldtype": "Select",
"label": "Posting Date Inheritance for Exchange Gain / Loss",
"label": "Posting Date inheritance for exchange gain / loss",
"options": "Invoice\nPayment\nReconciliation Date"
},
{
"default": "Buffered Cursor",
"fieldname": "receivable_payable_fetch_method",
"fieldtype": "Select",
"label": "Data Fetch Method",
"label": "Data fetch method",
"options": "Buffered Cursor\nUnBuffered Cursor"
},
{
@@ -538,14 +538,14 @@
"default": "0",
"fieldname": "maintain_same_internal_transaction_rate",
"fieldtype": "Check",
"label": "Maintain Same Rate Throughout Internal Transaction"
"label": "Maintain same rate throughout internal Transaction"
},
{
"default": "Stop",
"depends_on": "maintain_same_internal_transaction_rate",
"fieldname": "maintain_same_rate_action",
"fieldtype": "Select",
"label": "Action if Same Rate is Not Maintained Throughout Internal Transaction",
"label": "Action if same rate is not maintained throughout internal transaction",
"mandatory_depends_on": "maintain_same_internal_transaction_rate",
"options": "Stop\nWarn"
},
@@ -553,7 +553,7 @@
"depends_on": "eval: doc.maintain_same_internal_transaction_rate && doc.maintain_same_rate_action == 'Stop'",
"fieldname": "role_to_override_stop_action",
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"label": "Role allowed to override stop action",
"options": "Role"
},
{
@@ -585,7 +585,7 @@
"description": "If no taxes are set, and Taxes and Charges Template is selected, the system will automatically apply the taxes from the chosen template.",
"fieldname": "add_taxes_from_taxes_and_charges_template",
"fieldtype": "Check",
"label": "Automatically Add Taxes from Taxes and Charges Template"
"label": "Automatically add taxes from Taxes and Charges Template"
},
{
"fieldname": "column_break_ntmi",
@@ -595,19 +595,20 @@
"default": "0",
"fieldname": "fetch_valuation_rate_for_internal_transaction",
"fieldtype": "Check",
"label": "Fetch Valuation Rate for Internal Transaction"
"label": "Fetch valuation rate for internal Transaction"
},
{
"default": "0",
"description": "Enable this if you are experiencing issues with the new budget controller. Uses the older budget validation logic",
"fieldname": "use_legacy_budget_controller",
"fieldtype": "Check",
"label": "Use Legacy Budget Controller"
"label": "Use legacy Budget Controller"
},
{
"default": "1",
"fieldname": "use_legacy_controller_for_pcv",
"fieldtype": "Check",
"label": "Use Legacy Controller For Period Closing Voucher"
"label": "Use legacy controller for Period Closing Voucher"
},
{
"description": "Users with this role will be notified if the asset depreciation gets failed",
@@ -625,7 +626,7 @@
{
"fieldname": "chart_of_accounts_section",
"fieldtype": "Section Break",
"label": "Chart Of Accounts"
"label": "Chart of Accounts"
},
{
"fieldname": "banking_section",
@@ -670,6 +671,7 @@
},
{
"default": "0",
"documentation_url": "https://docs.frappe.io/erpnext/loyalty-program",
"fieldname": "enable_loyalty_point_program",
"fieldtype": "Check",
"label": "Enable Loyalty Point Program"
@@ -696,7 +698,7 @@
"default": "1",
"fieldname": "fetch_payment_schedule_in_payment_request",
"fieldtype": "Check",
"label": "Fetch Payment Schedule In Payment Request"
"label": "Fetch Payment Schedule in Payment Request"
},
{
"fieldname": "repost_section",
@@ -706,8 +708,22 @@
{
"fieldname": "repost_allowed_types",
"fieldtype": "Table",
"label": "Allowed Doctypes",
"label": "Allowed DocTypes",
"options": "Repost Allowed Types"
},
{
"fieldname": "document_naming_tab",
"fieldtype": "Tab Break",
"label": "Document Naming"
},
{
"fieldname": "transaction_naming_html",
"fieldtype": "HTML"
},
{
"description": "Changing the account in any transaction of the DocTypes listed below will trigger a repost. To prevent reposting, remove the relevant DocType from the list.",
"fieldname": "column_break_mfor",
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
@@ -717,7 +733,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-05-18 12:16:33.679345",
"modified": "2026-06-03 13:11:54.721495",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -1,7 +1,6 @@
{
"actions": [],
"autoname": "format:Bank Statement Import on {creation}",
"beta": 1,
"creation": "2019-08-04 14:16:08.318714",
"doctype": "DocType",
"editable_grid": 1,
@@ -226,7 +225,7 @@
],
"hide_toolbar": 1,
"links": [],
"modified": "2025-06-11 02:23:22.159961",
"modified": "2026-05-30 20:51:10.353723",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Statement Import",
@@ -251,4 +250,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -121,7 +121,7 @@ class BisectAccountingStatements(Document):
cur_node.save()
@frappe.whitelist()
@frappe.whitelist(methods=["POST"])
def build_tree(self):
frappe.db.delete("Bisect Nodes")

View File

@@ -707,18 +707,20 @@ def get_ordered_amount(params):
def get_other_condition(params, for_doc):
condition = f"expense_account = '{params.expense_account}'"
condition = f"expense_account = {frappe.db.escape(params.expense_account)}"
budget_against_field = params.get("budget_against_field")
if budget_against_field and params.get(budget_against_field):
condition += f" and child.{budget_against_field} = '{params.get(budget_against_field)}'"
condition += (
f" and child.{budget_against_field} = {frappe.db.escape(params.get(budget_against_field))}"
)
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
start_date = frappe.get_cached_value("Fiscal Year", params.from_fiscal_year, "year_start_date")
end_date = frappe.get_cached_value("Fiscal Year", params.to_fiscal_year, "year_end_date")
condition += f" and parent.{date_field} between '{start_date}' and '{end_date}'"
condition += f" and parent.{date_field} between {frappe.db.escape(str(start_date))} and {frappe.db.escape(str(end_date))}"
return condition

View File

@@ -6,12 +6,14 @@ frappe.provide("erpnext.cheque_print");
frappe.ui.form.on("Cheque Print Template", {
refresh: function (frm) {
if (!frm.doc.__islocal) {
frm.add_custom_button(
frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"),
function () {
erpnext.cheque_print.view_cheque_print(frm);
}
).addClass("btn-primary");
if (frappe.user.has_role("System Manager")) {
frm.add_custom_button(
frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"),
function () {
erpnext.cheque_print.view_cheque_print(frm);
}
).addClass("btn-primary");
}
$(frm.fields_dict.cheque_print_preview.wrapper).empty();

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_bulk_edit": 1,
"autoname": "field:bank_name",
"creation": "2016-05-04 14:35:00.402544",
"doctype": "DocType",
@@ -294,7 +295,7 @@
],
"links": [],
"max_attachments": 1,
"modified": "2024-03-27 13:06:44.654989",
"modified": "2026-06-08 12:10:35.829531",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cheque Print Template",
@@ -325,19 +326,17 @@
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"write": 1
"share": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -48,6 +48,8 @@ class ChequePrintTemplate(Document):
@frappe.whitelist()
def create_or_update_cheque_print_format(template_name):
frappe.only_for("System Manager")
if not frappe.db.exists("Print Format", template_name):
cheque_print = frappe.new_doc("Print Format")
cheque_print.update(

View File

@@ -2,7 +2,6 @@
"actions": [],
"allow_events_in_timeline": 1,
"autoname": "naming_series:",
"beta": 1,
"creation": "2019-07-05 16:34:31.013238",
"doctype": "DocType",
"engine": "InnoDB",
@@ -400,7 +399,7 @@
],
"is_submittable": 1,
"links": [],
"modified": "2024-11-26 13:46:07.760867",
"modified": "2026-05-30 23:18:04.712528",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Dunning",
@@ -449,9 +448,10 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"title_field": "customer_name",
"track_changes": 1
}
}

View File

@@ -1,7 +1,6 @@
{
"actions": [],
"allow_rename": 1,
"beta": 1,
"creation": "2019-12-04 04:59:08.003664",
"doctype": "DocType",
"editable_grid": 1,
@@ -107,7 +106,7 @@
"link_fieldname": "dunning_type"
}
],
"modified": "2024-03-27 13:08:19.584112",
"modified": "2026-05-30 23:18:20.740726",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Dunning Type",
@@ -151,8 +150,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -565,18 +565,19 @@ class FinancialQueryBuilder:
frappe.qb.from_(acb_table)
.select(
acb_table.account,
(acb_table.debit - acb_table.credit).as_("balance"),
Sum(acb_table.debit - acb_table.credit).as_("balance"),
)
.where(acb_table.company == self.company)
.where(acb_table.account.isin(account_names))
.where(acb_table.period_closing_voucher == closing_voucher)
.groupby(acb_table.account)
)
query = self._apply_standard_filters(query, acb_table, "Account Closing Balance")
results = self._execute_with_permissions(query, "Account Closing Balance")
for row in results:
closing_balances[row["account"]] = row["balance"]
closing_balances[row["account"]] = row["balance"] or 0.0
return closing_balances

View File

@@ -19,6 +19,7 @@ from erpnext.accounts.doctype.financial_report_template.test_financial_report_te
)
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.utils import get_currency_precision, get_fiscal_year
from erpnext.tests.utils import change_settings
class TestDependencyResolver(FinancialReportTemplateTestCase):
@@ -1953,6 +1954,104 @@ class TestFinancialQueryBuilder(FinancialReportTemplateTestCase):
jv_2023.cancel()
@change_settings("Accounts Settings", {"use_legacy_controller_for_pcv": 1})
def test_opening_balance_sums_acb_rows_across_dimensions(self):
"""
Account Closing Balance stores one row per (account, cost_center,
project, finance_book). The closing-balance fetch must sum all rows.
"""
company = "_Test Company"
cash_account = "_Test Cash - _TC"
sales_account = "Sales - _TC"
cc_1 = "_Test Cost Center - _TC"
cc_2 = "_Test Cost Center 2 - _TC"
docs = []
try:
jv_2023_cc1 = make_journal_entry(
account1=cash_account,
account2=sales_account,
amount=3000,
posting_date="2023-06-15",
cost_center=cc_1,
company=company,
submit=True,
)
docs.append(jv_2023_cc1)
jv_2023_cc2 = make_journal_entry(
account1=cash_account,
account2=sales_account,
amount=2000,
posting_date="2023-06-15",
cost_center=cc_2,
company=company,
submit=True,
)
docs.append(jv_2023_cc2)
fy_2023 = get_fiscal_year("2023-06-15", company=company)
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": cc_1,
"closing_account_head": "Deferred Revenue - _TC",
"remarks": "Test multi-dim PCV",
}
)
pcv.insert()
pcv.submit()
docs.append(pcv)
jv_2024 = make_journal_entry(
account1=cash_account,
account2=sales_account,
amount=100,
posting_date="2024-01-15",
cost_center=cc_1,
company=company,
submit=True,
)
docs.append(jv_2024)
filters = {
"company": company,
"from_fiscal_year": "2024",
"to_fiscal_year": "2024",
"period_start_date": "2024-01-01",
"period_end_date": "2024-03-31",
"filter_based_on": "Date Range",
"periodicity": "Monthly",
"ignore_closing_entries": True,
}
periods = [
{"key": "2024_jan", "from_date": "2024-01-01", "to_date": "2024-01-31"},
{"key": "2024_feb", "from_date": "2024-02-01", "to_date": "2024-02-29"},
{"key": "2024_mar", "from_date": "2024-03-01", "to_date": "2024-03-31"},
]
query_builder = FinancialQueryBuilder(filters, periods)
accounts = [
frappe._dict({"name": cash_account, "account_name": "Cash", "account_number": "1001"}),
]
balances_data = query_builder.fetch_account_balances(accounts)
cash_data = balances_data.get(cash_account)
self.assertIsNotNone(cash_data, "Cash account must appear in results")
jan_cash = cash_data.get_period("2024_jan")
self.assertEqual(jan_cash.opening, 5000.0)
self.assertEqual(jan_cash.movement, 100.0)
self.assertEqual(jan_cash.closing, 5100.0)
finally:
self.cancel_docs(docs)
def test_opening_entries_roll_into_opening_after_period_closing(self):
"""
Sequence:

View File

@@ -9,6 +9,14 @@ from erpnext.tests.utils import ERPNextTestSuite
class FinancialReportTemplateTestCase(ERPNextTestSuite):
"""Utility class with common setup and helper methods for all test classes"""
def cancel_docs(self, docs):
"""Cancel submitted docs in reverse creation order to avoid dependency issues."""
for doc in reversed(docs):
if doc:
doc.reload()
if doc.docstatus == 1:
doc.cancel()
def setUp(self):
"""Set up test data"""
self.create_test_template()

View File

@@ -433,15 +433,17 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
accounts_add(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
row.exchange_rate = 1;
$.each(doc.accounts, function (i, d) {
if (d.account && d.party && d.party_type) {
row.account = d.account;
row.party = d.party;
row.party_type = d.party_type;
row.exchange_rate = d.exchange_rate;
}
});
if (!row.exchange_rate) row.exchange_rate = 1;
if (!row.account) {
$.each(doc.accounts, function (i, d) {
if (d.account && d.party && d.party_type) {
row.account = d.account;
row.party = d.party;
row.party_type = d.party_type;
row.exchange_rate = d.exchange_rate;
}
});
}
// set difference
if (doc.difference) {

View File

@@ -1291,7 +1291,11 @@ class JournalEntry(AccountsController):
self.validate_total_debit_and_credit()
def get_values(self):
cond = f" and outstanding_amount <= {self.write_off_amount}" if flt(self.write_off_amount) > 0 else ""
cond = (
f" and outstanding_amount <= {flt(self.write_off_amount)}"
if flt(self.write_off_amount) > 0
else ""
)
if self.write_off_based_on == "Accounts Receivable":
return frappe.db.sql(

View File

@@ -1,7 +1,6 @@
{
"actions": [],
"allow_copy": 1,
"beta": 1,
"creation": "2017-08-29 02:22:54.947711",
"doctype": "DocType",
"editable_grid": 1,
@@ -90,7 +89,7 @@
"hide_toolbar": 1,
"issingle": 1,
"links": [],
"modified": "2026-03-31 01:47:20.360352",
"modified": "2026-05-30 23:18:48.691227",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool",

View File

@@ -1726,6 +1726,35 @@ frappe.ui.form.on("Payment Entry", {
},
});
},
before_cancel: function (frm) {
return new Promise((resolve, reject) => {
frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_linked_bank_transactions",
args: { payment_entry: frm.doc.name },
callback: function (r) {
const linked = r.message || [];
if (!linked.length) {
resolve();
return;
}
const bt_links = linked
.map((name) => frappe.utils.get_form_link("Bank Transaction", name, true))
.join(", ");
frappe.confirm(
__(
"This Payment Entry is reconciled with {0}. Cancelling will automatically unreconcile it. Do you want to proceed?",
[bt_links]
),
() => resolve(),
() => reject(),
__("Yes"),
__("No")
);
},
});
});
},
});
frappe.ui.form.on("Payment Entry Reference", {

View File

@@ -208,6 +208,7 @@ class PaymentEntry(AccountsController):
self.make_gl_entries()
self.update_outstanding_amounts()
self.set_status()
self.trigger_invoice_update_for_subscriptions()
def validate_for_repost(self):
validate_docs_for_voucher_types(["Payment Entry"])
@@ -314,6 +315,7 @@ class PaymentEntry(AccountsController):
self.update_outstanding_amounts()
self.delink_advance_entry_references()
self.set_status()
self.trigger_invoice_update_for_subscriptions()
def update_payment_requests(self, cancel=False):
from erpnext.accounts.doctype.payment_request.payment_request import (
@@ -505,6 +507,19 @@ class PaymentEntry(AccountsController):
doc = frappe.get_lazy_doc(reference.reference_doctype, reference.reference_name)
doc.delink_advance_entries(self.name)
def trigger_invoice_update_for_subscriptions(self):
invoice_names = set()
for ref in self.references:
if ref.reference_doctype in ("Sales Invoice", "Purchase Invoice"):
invoice_names.add((ref.reference_doctype, ref.reference_name))
for doctype, name in invoice_names:
try:
doc = frappe.get_doc(doctype, name)
doc.refresh_subscription_status()
except Exception:
frappe.log_error(_("Failed to update subscription status for {0} {1}").format(doctype, name))
def set_missing_values(self):
if self.payment_type == "Internal Transfer":
for field in (
@@ -3568,3 +3583,16 @@ def make_payment_order(source_name, target_doc=None):
@erpnext.allow_regional
def add_regional_gl_entries(gl_entries, doc):
return
@frappe.whitelist()
def get_linked_bank_transactions(payment_entry: str) -> list:
frappe.has_permission("Payment Entry", ptype="read", doc=payment_entry, throw=True)
return frappe.get_all(
"Bank Transaction Payments",
filters={
"payment_document": "Payment Entry",
"payment_entry": payment_entry,
},
pluck="parent",
)

View File

@@ -3,7 +3,9 @@
import unittest
import frappe
from frappe.utils import add_days, getdate
from erpnext.controllers.accounts_controller import get_payment_term_details
from erpnext.tests.utils import ERPNextTestSuite
@@ -56,6 +58,52 @@ class TestPaymentTermsTemplate(ERPNextTestSuite):
self.assertRaises(frappe.ValidationError, template.insert)
def test_no_discount_date_without_discount(self):
posting_date = "2026-05-29"
term = frappe._dict(
{
"payment_term": "_Test No Discount Term",
"invoice_portion": 100.0,
"due_date_based_on": "Day(s) after invoice date",
"credit_days": 0,
"credit_months": 0,
"discount_type": "Percentage",
"discount": 0,
"discount_validity_based_on": "Day(s) after invoice date",
"discount_validity": 0,
}
)
details = get_payment_term_details(
term, posting_date=posting_date, grand_total=100, base_grand_total=100
)
self.assertEqual(getdate(details.due_date), getdate(posting_date))
self.assertIsNone(details.discount_date)
def test_discount_date_generated_with_discount(self):
posting_date = "2026-05-29"
term = frappe._dict(
{
"payment_term": "_Test Discount Term",
"invoice_portion": 100.0,
"due_date_based_on": "Day(s) after invoice date",
"credit_days": 30,
"credit_months": 0,
"discount_type": "Percentage",
"discount": 5,
"discount_validity_based_on": "Day(s) after invoice date",
"discount_validity": 10,
}
)
details = get_payment_term_details(
term, posting_date=posting_date, grand_total=100, base_grand_total=100
)
self.assertEqual(getdate(details.due_date), getdate(add_days(posting_date, 30)))
self.assertEqual(getdate(details.discount_date), getdate(add_days(posting_date, 10)))
def test_duplicate_terms(self):
template = frappe.get_doc(
{

View File

@@ -26,8 +26,6 @@
"due_date",
"amended_from",
"return_against",
"section_break_abck",
"title",
"accounting_dimensions_section",
"project",
"dimension_col_break",
@@ -172,6 +170,7 @@
"is_discounted",
"col_break23",
"status",
"title",
"more_info",
"debit_to",
"party_account_currency",
@@ -1625,10 +1624,6 @@
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "section_break_abck",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
@@ -1641,7 +1636,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2026-05-01 02:37:30.580568",
"modified": "2026-05-28 12:22:50.253090",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

@@ -208,15 +208,14 @@ class POSProfile(Document):
def set_defaults(self, include_current_pos=True):
frappe.defaults.clear_default("is_pos")
if not include_current_pos:
condition = " where pfu.name != '%s' and pfu.default = 1 " % self.name.replace("'", "'")
else:
condition = " where pfu.default = 1 "
pfu = frappe.qb.DocType("POS Profile User")
pos_view_users = frappe.db.sql_list(
f"""select pfu.user
from `tabPOS Profile User` as pfu {condition}"""
)
query = frappe.qb.from_(pfu).select(pfu.user).where(pfu.default == 1)
if not include_current_pos:
query = query.where(pfu.name != self.name)
pos_view_users = query.run(as_list=1, pluck=True)
for user in pos_view_users:
if user:
@@ -315,32 +314,3 @@ def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
)
return pos_profile
@frappe.whitelist()
def set_default_profile(pos_profile, company):
modified = now()
user = frappe.session.user
if pos_profile and company:
frappe.db.sql(
""" update `tabPOS Profile User` pfu, `tabPOS Profile` pf
set
pfu.default = 0, pf.modified = %s, pf.modified_by = %s
where
pfu.user = %s and pf.name = pfu.parent and pf.company = %s
and pfu.default = 1""",
(modified, user, user, company),
auto_commit=1,
)
frappe.db.sql(
""" update `tabPOS Profile User` pfu, `tabPOS Profile` pf
set
pfu.default = 1, pf.modified = %s, pf.modified_by = %s
where
pfu.user = %s and pf.name = pfu.parent and pf.company = %s and pf.name = %s
""",
(modified, user, user, company, pos_profile),
auto_commit=1,
)

View File

@@ -151,13 +151,13 @@
"label": "Default Advance Account",
"mandatory_depends_on": "doc.party_type",
"options": "Account",
"reqd": 1
"reqd": 0
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-01-08 08:22:14.798085",
"modified": "2026-05-16 11:43:12.758685",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Payment Reconciliation",

View File

@@ -23,7 +23,7 @@ class ProcessPaymentReconciliation(Document):
bank_cash_account: DF.Link | None
company: DF.Link
cost_center: DF.Link | None
default_advance_account: DF.Link
default_advance_account: DF.Link | None
error_log: DF.LongText | None
from_invoice_date: DF.Date | None
from_payment_date: DF.Date | None
@@ -131,6 +131,7 @@ def is_job_running(job_name: str) -> bool:
@frappe.whitelist()
def pause_job_for_doc(docname: str | None = None):
if docname:
frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True)
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Paused")
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
if log:
@@ -145,6 +146,8 @@ def trigger_job_for_doc(docname: str | None = None):
if not docname:
return
frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True)
if not frappe.get_single_value("Accounts Settings", "auto_reconcile_payments"):
frappe.throw(
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
@@ -218,10 +221,7 @@ def trigger_reconciliation_for_queued_docs():
fields = ["company", "party_type", "party", "receivable_payable_account", "default_advance_account"]
def get_filters_as_tuple(fields, doc):
filters = ()
for x in fields:
filters += tuple(doc.get(x))
return filters
return tuple(doc.get(x) or "" for x in fields)
for x in all_queued:
doc = frappe.get_doc("Process Payment Reconciliation", x)

View File

@@ -1,5 +1,6 @@
{
"actions": [],
"allow_bulk_edit": 1,
"autoname": "format:Process-PCV-{###}",
"creation": "2025-09-25 15:44:03.534699",
"doctype": "DocType",
@@ -7,11 +8,13 @@
"field_order": [
"parent_pcv",
"status",
"amended_from",
"section_normal_balances",
"p_l_closing_balance",
"normal_balances",
"bs_closing_balance",
"z_opening_balances",
"amended_from"
"normal_balances",
"section_opening_balances",
"z_opening_balances"
],
"fields": [
{
@@ -64,17 +67,27 @@
"fieldname": "bs_closing_balance",
"fieldtype": "JSON",
"label": "Balance Sheet Closing Balance"
},
{
"fieldname": "section_normal_balances",
"fieldtype": "Tab Break",
"label": "Normal Balances"
},
{
"fieldname": "section_opening_balances",
"fieldtype": "Tab Break",
"label": "Opening Balances"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-11-05 11:40:24.996403",
"modified": "2026-06-01 12:16:37.374412",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Period Closing Voucher",
"naming_rule": "Expression",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{

View File

@@ -36,8 +36,8 @@ class ProcessPeriodClosingVoucher(Document):
parent_pcv: DF.Link
status: DF.Literal["Queued", "Running", "Paused", "Completed", "Cancelled"]
z_opening_balances: DF.Table[ProcessPeriodClosingVoucherDetail]
# end: auto-generated types
def on_discard(self):
self.db_set("status", "Cancelled")
@@ -92,6 +92,7 @@ class ProcessPeriodClosingVoucher(Document):
@frappe.whitelist()
def start_pcv_processing(docname: str):
if frappe.db.get_value("Process Period Closing Voucher", docname, "status") in ["Queued", "Running"]:
frappe.has_permission("Process Payment Reconciliation", "write", doc=docname, throw=True)
frappe.db.set_value("Process Period Closing Voucher", docname, "status", "Running")
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
@@ -562,6 +563,9 @@ def process_individual_date(docname: str, date, report_type, parentfield):
if parentfield == "z_opening_balances":
query = query.where(gle.is_opening.eq("Yes"))
else:
# Keep balances aligned with legacy PCV logic (non-opening transactions only)
query = query.where(gle.is_opening.eq("No"))
query = query.groupby(gle.account)
for dim in dimensions:

View File

@@ -0,0 +1,17 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// render
frappe.listview_settings["Process Period Closing Voucher"] = {
add_fields: ["status"],
get_indicator: function (doc) {
const status_colors = {
Queued: "blue",
Running: "orange",
Paused: "gray",
Completed: "green",
Cancelled: "red",
};
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
},
};

View File

@@ -1,4 +1,173 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import frappe
from frappe.utils import today
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher import (
process_individual_date,
)
from erpnext.accounts.utils import get_fiscal_year
from erpnext.tests.utils import ERPNextTestSuite
class TestProcessPeriodClosingVoucher(ERPNextTestSuite):
def setUp(self):
frappe.db.set_single_value("Accounts Settings", "use_legacy_controller_for_pcv", 0)
self.company = "_Test Company"
def make_period_closing_voucher(self, posting_date, submit=True):
fy = get_fiscal_year(posting_date, company="_Test Company")
pcv = frappe.get_doc(
{
"doctype": "Period Closing Voucher",
"transaction_date": posting_date or today(),
"period_start_date": fy[1],
"period_end_date": fy[2],
"company": self.company,
"fiscal_year": fy[0],
"closing_account_head": "Retained Earnings - _TC",
"remarks": "closing",
}
)
pcv.insert()
if submit:
pcv.submit()
return pcv
def make_process_pcv(self):
self.pcv = self.make_period_closing_voucher(posting_date=today(), submit=False)
ppcv = frappe.get_doc(
{
"doctype": "Process Period Closing Voucher",
"parent_pcv": self.pcv.name,
}
)
ppcv.save()
return ppcv
def set_processing_date_status(self, date, ppcv, rpt_type, parentfield, status):
frappe.db.set_value(
"Process Period Closing Voucher Detail",
{"processing_date": date, "parent": ppcv, "report_type": rpt_type, "parentfield": parentfield},
"status",
status,
)
def get_processing_date_closing_balance(self, date, ppcv, rpt_type, parentfield):
return frappe.db.get_value(
"Process Period Closing Voucher Detail",
{"processing_date": date, "parent": ppcv, "report_type": rpt_type, "parentfield": parentfield},
"closing_balance",
)
def test_opening_balance_double_counting(self):
ppcv = self.make_process_pcv()
self.assertEqual(self.pcv.is_first_period_closing_voucher(), True)
opening_jv = make_journal_entry(
posting_date=today(),
amount=10,
account1="Cash - _TC",
account2="Debtors - _TC",
company=self.company,
save=False,
)
opening_jv.accounts[1].party_type = "Customer"
opening_jv.accounts[1].party = "_Test Customer"
opening_jv.is_opening = "Yes"
opening_jv.save()
opening_jv.submit()
jv = make_journal_entry(
posting_date=today(),
amount=120,
account1="Debtors - _TC",
account2="Sales - _TC",
company=self.company,
save=False,
)
jv.accounts[0].party_type = "Customer"
jv.accounts[0].party = "_Test Customer"
jv.save()
jv.submit()
# P&L balance
parentfield = "normal_balances"
rpt_type = "Profit and Loss"
# status has to be set to 'Running' for logic to run
self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running")
process_individual_date(ppcv.name, today(), rpt_type, parentfield)
bal = frappe.parse_json(
self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield)
)
self.assertEqual(len(bal), 1)
expected_pl = {
"account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC",
"debit": 0.0,
"credit": 120.0,
"debit_in_account_currency": 0.0,
"credit_in_account_currency": 120.0,
}
for k in expected_pl.keys():
with self.subTest(k):
self.assertEqual(expected_pl[k], bal[0][k])
# Balance sheet balance
rpt_type = "Balance Sheet"
self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running")
process_individual_date(ppcv.name, today(), rpt_type, parentfield)
bal = frappe.parse_json(
self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield)
)
self.assertEqual(len(bal), 1)
expected_bs = {
"account": "Debtors - _TC",
"cost_center": "_Test Cost Center - _TC",
"debit": 120.0,
"credit": 0.0,
"debit_in_account_currency": 120.0,
"credit_in_account_currency": 0.0,
}
for k in expected_bs.keys():
with self.subTest(k):
self.assertEqual(expected_bs[k], bal[0][k])
# Opening balance
parentfield = "z_opening_balances"
rpt_type = "Balance Sheet"
self.set_processing_date_status(today(), ppcv.name, rpt_type, parentfield, "Running")
process_individual_date(ppcv.name, today(), rpt_type, parentfield)
bal = frappe.parse_json(
self.get_processing_date_closing_balance(today(), ppcv.name, rpt_type, parentfield)
)
self.assertEqual(len(bal), 2)
opening_cash = next(x for x in bal if x["account"] == "Cash - _TC")
expected_opening_cash = {
"account": "Cash - _TC",
"cost_center": "_Test Cost Center - _TC",
"debit": 10.0,
"credit": 0.0,
"debit_in_account_currency": 10.0,
"credit_in_account_currency": 0.0,
"account_currency": "INR",
}
for k in expected_opening_cash.keys():
with self.subTest(k):
self.assertEqual(expected_opening_cash[k], opening_cash[k])
opening_debtors = next(x for x in bal if x["account"] == "Debtors - _TC")
expected_opening_debtors = {
"account": "Debtors - _TC",
"cost_center": "_Test Cost Center - _TC",
"debit": 0.0,
"credit": 10.0,
"debit_in_account_currency": 0.0,
"credit_in_account_currency": 10.0,
"account_currency": "INR",
}
for k in expected_opening_debtors.keys():
with self.subTest(k):
self.assertEqual(expected_opening_debtors[k], opening_debtors[k])

View File

@@ -101,6 +101,7 @@ class ProcessStatementOfAccounts(Document):
validate_template(self.subject)
validate_template(self.body)
validate_template(self.pdf_name)
if not self.customers:
frappe.throw(_("Customers not selected."))
@@ -518,6 +519,7 @@ def download_statements(document_name):
@frappe.whitelist()
def send_emails(document_name, from_scheduler=False, posting_date=None):
doc = frappe.get_doc("Process Statement Of Accounts", document_name)
doc.check_permission()
report = get_report_pdf(doc, consolidated=False)
if report:
@@ -574,6 +576,7 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
@frappe.whitelist()
def send_auto_email():
frappe.has_permission("Process Statement Of Accounts", throw=True)
selected = frappe.get_list(
"Process Statement Of Accounts",
filters={"enable_auto_email": 1},

View File

@@ -591,6 +591,25 @@ frappe.ui.form.on("Purchase Invoice", {
};
});
frm.set_query("write_off_account", function (doc) {
return {
filters: {
report_type: "Profit and Loss",
is_group: 0,
company: doc.company,
},
};
});
frm.set_query("write_off_cost_center", function (doc) {
return {
filters: {
is_group: 0,
company: doc.company,
},
};
});
frm.fields_dict["items"].grid.get_field("deferred_expense_account").get_query = function (doc) {
return {
filters: {

View File

@@ -27,8 +27,6 @@
"update_billed_amount_in_purchase_receipt",
"apply_tds",
"amended_from",
"section_break_hzux",
"title",
"supplier_invoice_details",
"bill_no",
"column_break_15",
@@ -201,6 +199,7 @@
"hold_comment",
"additional_info_section",
"is_internal_supplier",
"title",
"represents_company",
"supplier_group",
"sender",
@@ -1685,10 +1684,6 @@
"fieldtype": "Section Break",
"label": "Automation"
},
{
"fieldname": "section_break_hzux",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
@@ -1703,7 +1698,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2026-04-28 07:15:31.062404",
"modified": "2026-05-28 12:36:55.215363",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -32,10 +32,14 @@ from erpnext.accounts.general_ledger import (
merge_similar_entries,
)
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding
from erpnext.accounts.utils import (
get_account_currency,
get_fiscal_year,
refresh_subscription_status,
update_voucher_outstanding,
)
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.accounts_controller import merge_taxes, validate_account_head
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
@@ -282,7 +286,9 @@ class PurchaseInvoice(BuyingController):
self.check_conversion_rate()
self.validate_credit_to_acc()
self.clear_unallocated_advances("Purchase Invoice Advance", "advances")
self.check_on_hold_or_closed_status()
self.check_for_on_hold_or_closed_status(
"Purchase Order", "purchase_order", exclude_if_field="purchase_receipt"
)
self.validate_with_previous_doc()
self.validate_uom_is_integer("uom", "qty")
self.validate_uom_is_integer("stock_uom", "stock_qty")
@@ -290,6 +296,7 @@ class PurchaseInvoice(BuyingController):
self.validate_expense_account()
self.set_against_expense_account()
self.validate_write_off_account()
self.validate_write_off_cost_center()
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
self.set_status()
self.validate_purchase_receipt_if_update_stock()
@@ -387,14 +394,6 @@ class PurchaseInvoice(BuyingController):
self.party_account_currency = account.account_currency
def check_on_hold_or_closed_status(self):
check_list = []
for d in self.get("items"):
if d.purchase_order and d.purchase_order not in check_list and not d.purchase_receipt:
check_list.append(d.purchase_order)
check_on_hold_or_closed_status("Purchase Order", d.purchase_order)
def validate_with_previous_doc(self):
super().validate_with_previous_doc(
{
@@ -635,15 +634,16 @@ class PurchaseInvoice(BuyingController):
throw(msg, title=_("Mandatory Purchase Order"))
def pr_required(self):
stock_items = self.get_stock_items()
if frappe.db.get_single_value("Buying Settings", "pr_required") == "Yes":
stock_and_asset_items = self.get_stock_items()
stock_and_asset_items.extend(self.get_asset_items())
if frappe.get_value(
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt"
):
return
for d in self.get("items"):
if not d.purchase_receipt and d.item_code in stock_items:
if not d.purchase_receipt and d.item_code in stock_and_asset_items:
msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
msg += "<br><br>"
msg += _(
@@ -659,6 +659,27 @@ class PurchaseInvoice(BuyingController):
if self.write_off_amount and not self.write_off_account:
throw(_("Please enter Write Off Account"))
if not self.write_off_account:
return
doc = frappe.db.get_value(
"Account", self.write_off_account, ["report_type", "is_group", "company"], as_dict=True
)
if not doc or doc.report_type != "Profit and Loss" or doc.is_group or doc.company != self.company:
throw(_("Please enter a valid Write Off Account"))
def validate_write_off_cost_center(self):
if not self.write_off_cost_center:
return
doc = frappe.db.get_value(
"Cost Center", self.write_off_cost_center, ["is_group", "company"], as_dict=True
)
if not doc or doc.is_group or doc.company != self.company:
throw(_("Please enter a valid Write Off Cost Center"))
def check_prev_docstatus(self):
for d in self.get("items"):
if d.purchase_order:
@@ -739,6 +760,7 @@ class PurchaseInvoice(BuyingController):
def validate_for_repost(self):
self.validate_write_off_account()
self.validate_write_off_cost_center()
self.validate_expense_account()
validate_docs_for_voucher_types(["Purchase Invoice"])
validate_docs_for_deferred_accounting([], [self.name])
@@ -806,6 +828,10 @@ class PurchaseInvoice(BuyingController):
self.validate_for_repost()
self.repost_accounting_entries()
def refresh_subscription_status(self):
if self.get("subscription"):
refresh_subscription_status(self.subscription)
def make_gl_entries(self, gl_entries=None, from_repost=False):
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
if self.docstatus == 1:
@@ -852,7 +878,9 @@ class PurchaseInvoice(BuyingController):
if update_outstanding == "No":
update_voucher_outstanding(
voucher_type=self.doctype,
voucher_no=self.return_against if cint(self.is_return) and self.return_against else self.name,
voucher_no=self.return_against
if (cint(self.is_return) and self.return_against)
else self.name,
account=self.credit_to,
party_type="Supplier",
party=self.supplier,
@@ -1546,6 +1574,9 @@ class PurchaseInvoice(BuyingController):
def make_payment_gl_entries(self, gl_entries):
# Make Cash GL Entries
if cint(self.is_paid) and self.cash_bank_account and self.paid_amount:
against_voucher = self.name
if self.is_return and self.return_against and not self.update_outstanding_for_self:
against_voucher = self.return_against
bank_account_currency = get_account_currency(self.cash_bank_account)
# CASH, make payment entries
gl_entries.append(
@@ -1560,9 +1591,7 @@ class PurchaseInvoice(BuyingController):
if self.party_account_currency == self.company_currency
else self.paid_amount,
"debit_in_transaction_currency": self.paid_amount,
"against_voucher": self.return_against
if cint(self.is_return) and self.return_against
else self.name,
"against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
"project": self.project,
@@ -1684,7 +1713,9 @@ class PurchaseInvoice(BuyingController):
super().on_cancel()
PurchaseTaxWithholding(self).on_cancel()
self.check_on_hold_or_closed_status()
self.check_for_on_hold_or_closed_status(
"Purchase Order", "purchase_order", exclude_if_field="purchase_receipt"
)
if self.is_return and not self.update_billed_amount_in_purchase_order:
# NOTE status updating bypassed for is_return
@@ -1977,6 +2008,7 @@ def make_stock_entry(source_name, target_doc=None):
def change_release_date(name, release_date=None):
if frappe.db.exists("Purchase Invoice", name):
pi = frappe.get_lazy_doc("Purchase Invoice", name)
pi.check_permission()
pi.db_set("release_date", release_date)

View File

@@ -2962,6 +2962,52 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
pr = make_purchase_receipt_from_pi(pi.name)
self.assertFalse(pr.items)
@ERPNextTestSuite.change_settings("Accounts Settings", {"enable_common_party_accounting": True})
def test_purchase_invoice_return_common_party_je_has_no_negative_amounts(self):
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
)
from erpnext.accounts.doctype.party_link.party_link import create_party_link
from erpnext.controllers.sales_and_purchase_return import make_return_doc
customer = make_customer(customer="_Test Common Party Return PI")
supplier = create_supplier(supplier_name="_Test Common Party Return PI").name
# Supplier must be secondary so get_common_party_link finds it via the PI's party_type
party_link = create_party_link("Customer", customer, supplier)
pi = make_purchase_invoice(supplier=supplier, parent_cost_center="_Test Cost Center - _TC")
return_pi = make_return_doc(pi.doctype, pi.name)
return_pi.submit()
# JE for the return should credit the supplier (secondary/reconciliation) account
# and debit the customer (primary) account — all positive amounts
jv_accounts = frappe.get_all(
"Journal Entry Account",
filters={"reference_type": return_pi.doctype, "reference_name": return_pi.name, "docstatus": 1},
fields=["debit_in_account_currency", "credit_in_account_currency", "account"],
)
self.assertTrue(jv_accounts, "Expected a Journal Entry for the return invoice")
for row in jv_accounts:
self.assertGreaterEqual(
row.debit_in_account_currency,
0,
f"Negative debit on account {row.account}",
)
self.assertGreaterEqual(
row.credit_in_account_currency,
0,
f"Negative credit on account {row.account}",
)
# Supplier (secondary) account must be credited, not debited
supplier_row = next(r for r in jv_accounts if r.account == pi.credit_to)
self.assertGreater(supplier_row.credit_in_account_currency, 0)
self.assertEqual(supplier_row.debit_in_account_currency, 0)
party_link.delete()
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -179,12 +179,31 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
: "Inter Company Purchase Invoice";
me.frm.add_custom_button(
button_label,
__(button_label),
function () {
me.make_inter_company_invoice();
},
__("Create")
);
frappe.call({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.get_received_items",
args: {
reference_name: me.frm.doc.name,
doctype: "Purchase Invoice",
reference_fieldname: "sales_invoice_item",
},
callback: function (r) {
if (r.exc) return;
const received_items = r.message || {};
const has_pending_qty = me.frm.doc.items.some(
(item) => flt(item.qty) - flt(received_items[item.name] || 0) > 0
);
if (!has_pending_qty) {
me.frm.remove_custom_button(__(button_label), __("Create"));
}
},
});
}
}

View File

@@ -33,8 +33,6 @@
"is_created_using_pos",
"pos_closing_entry",
"has_subcontracted",
"section_break_qllv",
"title",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -234,6 +232,7 @@
"status",
"remarks",
"customer_group",
"title",
"column_break_imbx",
"is_internal_customer",
"represents_company",
@@ -2343,10 +2342,6 @@
"fieldname": "column_break_iaso",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_qllv",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
@@ -2367,7 +2362,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2026-05-21 17:31:11.190958",
"modified": "2026-05-28 12:15:12.486443",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -28,9 +28,15 @@ from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger
)
from erpnext.accounts.doctype.tax_withholding_entry.tax_withholding_entry import SalesTaxWithholding
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
from erpnext.accounts.party import (
CROSS_PARTY_FIELD_NO_MAP,
get_due_date,
get_party_account,
get_party_details,
)
from erpnext.accounts.utils import (
get_account_currency,
refresh_subscription_status,
update_voucher_outstanding,
)
from erpnext.assets.doctype.asset.asset import split_asset
@@ -370,6 +376,8 @@ class SalesInvoice(SellingController):
if row.billing_amount:
row.billing_amount = -abs(row.billing_amount)
self.validate_update_stock_for_pick_list_reference()
self.set_serial_and_batch_bundle_from_pick_list()
self.update_packing_list()
self.set_billing_hours_and_amount()
self.update_timesheet_billing_for_project()
@@ -389,6 +397,18 @@ class SalesInvoice(SellingController):
self.validate_subcontracted_sales_order()
self.validate_scio_self_rm_qty()
def validate_update_stock_for_pick_list_reference(self):
if self.update_stock or self.is_return:
return
for row in self.items:
if row.get("against_pick_list"):
frappe.throw(
_(
"Row {0}: Update Stock must be checked for item {1} because it is against Pick List {2}."
).format(row.idx, frappe.bold(row.item_code), frappe.bold(row.against_pick_list))
)
def validate_accounts(self):
self.validate_write_off_account()
self.validate_account_for_change_amount()
@@ -491,6 +511,7 @@ class SalesInvoice(SellingController):
if self.update_stock == 1:
self.repost_future_sle_and_gle()
self.update_pick_list_status()
if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
@@ -614,6 +635,7 @@ class SalesInvoice(SellingController):
if self.update_stock == 1:
self.update_stock_reservation_entries()
self.repost_future_sle_and_gle()
self.update_pick_list_status()
self.db_set("status", "Cancelled")
@@ -665,26 +687,41 @@ class SalesInvoice(SellingController):
if not cint(self.update_stock):
return
self.status_updater.append(
{
"source_dt": "Sales Invoice Item",
"target_dt": "Sales Order Item",
"target_parent_dt": "Sales Order",
"target_parent_field": "per_delivered",
"target_field": "delivered_qty",
"target_ref_field": "qty",
"source_field": "qty",
"join_field": "so_detail",
"percent_join_field": "sales_order",
"status_field": "delivery_status",
"keyword": "Delivered",
"second_source_dt": "Delivery Note Item",
"second_source_field": "qty",
"second_join_field": "so_detail",
"overflow_type": "delivery",
"extra_cond": """ and exists(select name from `tabSales Invoice`
where name=`tabSales Invoice Item`.parent and update_stock = 1)""",
}
self.status_updater.extend(
[
{
"source_dt": "Sales Invoice Item",
"target_dt": "Sales Order Item",
"target_parent_dt": "Sales Order",
"target_parent_field": "per_delivered",
"target_field": "delivered_qty",
"target_ref_field": "qty",
"source_field": "qty",
"join_field": "so_detail",
"percent_join_field": "sales_order",
"status_field": "delivery_status",
"keyword": "Delivered",
"second_source_dt": "Delivery Note Item",
"second_source_field": "qty",
"second_join_field": "so_detail",
"overflow_type": "delivery",
"extra_cond": """ and exists(select name from `tabSales Invoice`
where name=`tabSales Invoice Item`.parent and update_stock = 1)""",
},
{
"source_dt": "Sales Invoice Item",
"target_dt": "Pick List Item",
"join_field": "pick_list_item",
"target_field": "delivered_qty",
"target_parent_dt": "Pick List",
"target_parent_field": "per_delivered",
"target_ref_field": "picked_qty",
"source_field": "stock_qty",
"percent_join_field": "against_pick_list",
"status_field": "delivery_status",
"keyword": "Delivered",
},
]
)
if not cint(self.is_return):
@@ -777,6 +814,10 @@ class SalesInvoice(SellingController):
"set_default_payment": pos.get("set_grand_total_to_default_mop", 1),
}
def refresh_subscription_status(self):
if self.get("subscription"):
refresh_subscription_status(self.subscription)
@frappe.whitelist()
def reset_mode_of_payments(self):
if self.pos_profile:
@@ -2024,15 +2065,24 @@ class SalesInvoice(SellingController):
def update_billing_status_in_dn(self, update_modified=True):
if self.is_return and not self.update_billed_amount_in_delivery_note:
return
updated_delivery_notes = []
SalesInvoiceItem = frappe.qb.DocType("Sales Invoice Item")
from frappe.query_builder.functions import Coalesce, Sum
for d in self.get("items"):
if d.dn_detail:
billed_amt = frappe.db.sql(
"""select sum(amount) from `tabSales Invoice Item`
where dn_detail=%s and docstatus=1""",
d.dn_detail,
query = (
frappe.qb.from_(SalesInvoiceItem)
.select(Coalesce(Sum(SalesInvoiceItem.amount), 0))
.where(SalesInvoiceItem.dn_detail == d.dn_detail)
.where(SalesInvoiceItem.docstatus == 1)
)
billed_amt = billed_amt and billed_amt[0][0] or 0
res = query.run()
billed_amt = res[0][0] if res else 0
frappe.db.set_value(
"Delivery Note Item",
d.dn_detail,
@@ -2743,7 +2793,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"rate": "rate",
},
"postprocess": update_item,
"condition": lambda doc: doc.qty > 0,
"condition": lambda doc: doc.qty - received_items.get(doc.name, 0.0) > 0,
}
if doctype in ["Sales Invoice", "Sales Order"]:
@@ -2776,7 +2826,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"doctype": target_doctype,
"postprocess": update_details,
"set_target_warehouse": "set_from_warehouse",
"field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address", "cost_center"],
"field_no_map": [*CROSS_PARTY_FIELD_NO_MAP, "set_warehouse", "cost_center"],
},
doctype + " Item": item_field_map,
},
@@ -2784,10 +2834,19 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
set_missing_values,
)
if not doclist.get("items"):
frappe.throw(
_(
"Cannot create Intercompany {0}. All items in the source {1} have already been fully invoiced. "
"Please check the existing linked {2}s."
).format(target_doctype, doctype, target_doctype)
)
return doclist
def get_received_items(reference_name, doctype, reference_fieldname):
@frappe.whitelist()
def get_received_items(reference_name: str, doctype: str, reference_fieldname: str):
reference_field = "inter_company_invoice_reference"
if doctype == "Purchase Order":
reference_field = "inter_company_order_reference"
@@ -2800,20 +2859,19 @@ def get_received_items(reference_name, doctype, reference_fieldname):
target_doctypes = frappe.get_all(
doctype,
filters=filters,
as_list=True,
pluck="name",
)
received_items_map = {}
if target_doctypes:
target_doctypes = list(target_doctypes[0])
received_items_map = frappe._dict(
frappe.get_all(
received_items_data = frappe.get_all(
doctype + " Item",
filters={"parent": ("in", target_doctypes)},
fields=[reference_fieldname, "qty"],
as_list=1,
)
)
for item in received_items_data:
key = item.get(reference_fieldname)
if key:
received_items_map[key] = received_items_map.get(key, 0.0) + flt(item.qty)
return received_items_map
@@ -3036,15 +3094,22 @@ def update_multi_mode_option(doc, pos_profile):
def get_all_mode_of_payments(doc):
return frappe.db.sql(
"""
select mpa.default_account, mpa.parent, mp.type as type
from `tabMode of Payment Account` mpa,`tabMode of Payment` mp
where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
{"company": doc.company},
as_dict=1,
ModeOfPaymentAccount = frappe.qb.DocType("Mode of Payment Account")
ModeOfPayment = frappe.qb.DocType("Mode of Payment")
query = (
frappe.qb.from_(ModeOfPaymentAccount)
.join(ModeOfPayment)
.on(ModeOfPaymentAccount.parent == ModeOfPayment.name)
.select(
ModeOfPaymentAccount.default_account, ModeOfPaymentAccount.parent, ModeOfPayment.type.as_("type")
)
.where(ModeOfPaymentAccount.company == doc.company)
.where(ModeOfPayment.enabled == 1)
)
return query.run(as_dict=1)
def get_mode_of_payments_info(mode_of_payments, company):
data = frappe.db.sql(

View File

@@ -383,6 +383,262 @@ class TestSalesInvoice(ERPNextTestSuite):
self.assertEqual(si.net_total, 3859.65)
self.assertEqual(si.grand_total, 4900.00)
@ERPNextTestSuite.change_settings("System Settings", {"number_format": "#,###", "currency_precision": 0})
def test_inclusive_tax_zero_decimal_currency(self):
"""Tax-included prices in zero-decimal currencies (e.g. JPY) must not produce
net + tax != gross due to double rounding of the net amount."""
si = create_sales_invoice(qty=1, rate=50000, do_not_save=True)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Tax 10%",
"rate": 10,
"included_in_print_rate": 1,
},
)
si.insert()
# With currency_precision=0 (like JPY, KRW):
# 50,000 / 1.10 = 45,454.545... → net rounds to 45,455
# Tax from unrounded net: 0.10 * 45,454.545 = 4,545.4545 → rounds to 4,545
# The fix ensures net + tax = gross without double rounding error
self.assertEqual(si.items[0].net_amount, 45455)
self.assertEqual(si.taxes[0].tax_amount, 4545)
self.assertEqual(si.grand_total, 50000)
def test_inclusive_tax_decimal_value_currency(self):
"""Tax-included prices with decimal currency values must preserve gross total."""
si = create_sales_invoice(qty=1, rate=10000.04, do_not_save=True)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Tax 10%",
"rate": 10,
"included_in_print_rate": 1,
},
)
si.insert()
# 10,000.04 / 1.10 = 9,090.94545... → net rounds to 9,090.95
# Tax from unrounded net: 0.10 * 9,090.94545... = 909.0945... → rounds to 909.09
# If tax were calculated from rounded net instead, it would become 909.10 and grand total 10,000.05.
self.assertEqual(si.items[0].net_amount, 9090.95)
self.assertEqual(si.taxes[0].tax_amount, 909.09)
self.assertEqual(si.grand_total, 10000.04)
@ERPNextTestSuite.change_settings("System Settings", {"number_format": "#,###", "currency_precision": 0})
def test_inclusive_tax_zero_decimal_currency_multiple_items(self):
"""Multiple items with tax-included prices in zero-decimal currency."""
si = create_sales_invoice(qty=1, rate=50000, do_not_save=True)
create_item("_Test Inclusive Tax Item 2")
si.append(
"items",
{
"item_code": "_Test Inclusive Tax Item 2",
"warehouse": "_Test Warehouse - _TC",
"qty": 1,
"rate": 30000,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
},
)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Tax 10%",
"rate": 10,
"included_in_print_rate": 1,
},
)
si.insert()
# With currency_precision=0:
# Item 1: 50,000 / 1.10 = 45,454.545 → net 45,455, tax 4,545
# Item 2: 30,000 / 1.10 = 27,272.727 → net 27,273, tax 2,727
# Per-item: net + tax = gross holds (45455+4545=50000, 27273+2727=30000)
# Accumulated tax rounds separately: flt(7272.72, 0) = 7273
# adjust_grand_total_for_inclusive_tax patches grand_total back to 80000
self.assertEqual(si.items[0].net_amount, 45455)
self.assertEqual(si.items[1].net_amount, 27273)
self.assertEqual(si.net_total, 72728)
self.assertEqual(si.taxes[0].tax_amount, 7273)
self.assertEqual(si.grand_total, 80000)
@ERPNextTestSuite.change_settings("System Settings", {"number_format": "#,###", "currency_precision": 0})
def test_inclusive_tax_zero_decimal_currency_many_items(self):
"""Test with 10 items (mixed 10% and 5% tax) to verify tolerance of 1 is sufficient."""
si = create_sales_invoice(qty=1, rate=50000, do_not_save=True)
# Add 9 more items - mix of amounts and tax rates
# Using similar amounts to maximize same-direction rounding
item_configs = [
("_Test Inclusive Tax Item 2", 50100, None), # 10% (default)
("_Test Inclusive Tax Item 3", 50200, '{"_Test Account Service Tax - _TC": 5}'), # 5%
("_Test Inclusive Tax Item 4", 50300, None), # 10%
("_Test Inclusive Tax Item 5", 50400, '{"_Test Account Service Tax - _TC": 5}'), # 5%
("_Test Inclusive Tax Item 6", 50500, None), # 10%
("_Test Inclusive Tax Item 7", 50600, '{"_Test Account Service Tax - _TC": 5}'), # 5%
("_Test Inclusive Tax Item 8", 50700, None), # 10%
("_Test Inclusive Tax Item 9", 50800, None), # 10%
("_Test Inclusive Tax Item 10", 50900, '{"_Test Account Service Tax - _TC": 5}'), # 5%
]
for item_code, rate, item_tax_rate in item_configs:
create_item(item_code)
item_dict = {
"item_code": item_code,
"warehouse": "_Test Warehouse - _TC",
"qty": 1,
"rate": rate,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
}
if item_tax_rate:
item_dict["item_tax_rate"] = item_tax_rate
si.append("items", item_dict)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Tax 10%",
"rate": 10,
"included_in_print_rate": 1,
},
)
si.insert()
# Verify each item: net + tax = gross (within rounding tolerance)
total_gross = 0
for item in si.items:
total_gross += item.amount
# Grand total should match sum of gross amounts
# This tests that the tolerance of 1 handles mixed tax rates and similar amounts
self.assertEqual(si.grand_total, total_gross)
def test_inclusive_tax_with_decimal_value_on_previous_row_amount(self):
"""Inclusive tax with decimal value and On Previous Row Amount must not double-round net amount."""
si = create_sales_invoice(qty=1, rate=50000.55, do_not_save=True)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Tax 10%",
"rate": 10,
"included_in_print_rate": 1,
},
)
si.append(
"taxes",
{
"charge_type": "On Previous Row Amount",
"account_head": "_Test Account Education Cess - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Cess 5% on Tax 10%",
"rate": 5,
"row_id": 1,
"included_in_print_rate": 1,
},
)
si.insert()
# Tax fractions: 10% + (5% of 10%) = 10.5%
# 50,000.55 / 1.105 = 45,249.3665... → net rounds to 45,249.37
# Taxes are calculated from the unrounded net to keep the inclusive gross stable.
self.assertEqual(si.items[0].net_amount, 45249.37)
self.assertEqual(si.taxes[0].tax_amount, 4524.94)
self.assertEqual(si.taxes[1].tax_amount, 226.25)
self.assertEqual(si.grand_total, 50000.55)
def test_inclusive_tax_with_decimal_value_on_previous_row_amount_non_inclusive(self):
"""Non-inclusive previous-row tax should be added after inclusive tax extraction."""
si = create_sales_invoice(qty=1, rate=10000.04, do_not_save=True)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Tax 10%",
"rate": 10,
"included_in_print_rate": 1,
},
)
si.append(
"taxes",
{
"charge_type": "On Previous Row Amount",
"account_head": "_Test Account Education Cess - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Cess 5% on Tax 10%",
"rate": 5,
"row_id": 1,
"included_in_print_rate": 0,
},
)
si.insert()
# Only the first tax is inclusive:
# 10,000.04 / 1.10 = 9,090.94545... → net rounds to 9,090.95
# Inclusive tax = 909.09, restoring the original gross of 10,000.04
# The non-inclusive previous-row tax is added afterward: 5% of 909.09 = 45.45
self.assertEqual(si.items[0].net_amount, 9090.95)
self.assertEqual(si.taxes[0].tax_amount, 909.09)
self.assertEqual(si.taxes[1].tax_amount, 45.45)
self.assertEqual(si.grand_total, 10045.49)
def test_inclusive_tax_with_decimal_value_on_previous_row_total(self):
"""Inclusive tax with decimal value and On Previous Row Total must not double-round net amount."""
si = create_sales_invoice(qty=1, rate=50000.55, do_not_save=True)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Tax 10%",
"rate": 10,
"included_in_print_rate": 1,
},
)
si.append(
"taxes",
{
"charge_type": "On Previous Row Total",
"account_head": "_Test Account Education Cess - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Cess 5% on Previous Total",
"rate": 5,
"row_id": 1,
"included_in_print_rate": 1,
},
)
si.insert()
# Tax fractions: 10% + (5% of 110%) = 15.5%
# 50,000.55 / 1.155 = 43,290.5195... → net rounds to 43,290.52
# Taxes are calculated from the unrounded net/previous total to keep the inclusive gross stable.
self.assertEqual(si.items[0].net_amount, 43290.52)
self.assertEqual(si.taxes[0].tax_amount, 4329.05)
self.assertEqual(si.taxes[1].tax_amount, 2380.98)
self.assertEqual(si.grand_total, 50000.55)
def test_sales_invoice_discount_amount(self):
si = frappe.copy_doc(self.globalTestRecords["Sales Invoice"][3])
si.discount_amount = 104.94
@@ -2662,6 +2918,34 @@ class TestSalesInvoice(ERPNextTestSuite):
self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
def test_inter_company_transaction_does_not_inherit_party_fields(self):
"""
Party-derived fields on SI (from Customer) must not leak into the mapped PI.
"""
si = create_sales_invoice(
company="Wind Power LLC",
customer="_Test Internal Customer",
debit_to="Debtors - WP",
warehouse="Stores - WP",
income_account="Sales - WP",
expense_account="Cost of Goods Sold - WP",
cost_center="Main - WP",
currency="USD",
do_not_save=1,
)
si.selling_price_list = "_Test Price List Rest of the World"
si.tax_category = "_Test Tax Category 1"
si.language = "ar"
si.payment_terms_template = "_Test Payment Term Template"
si.submit()
pi = make_inter_company_transaction("Sales Invoice", si.name)
supplier = frappe.get_doc("Supplier", "_Test Internal Supplier")
self.assertEqual(pi.tax_category or None, supplier.tax_category or None)
self.assertEqual(pi.language or None, supplier.language or None)
self.assertEqual(pi.payment_terms_template or None, supplier.payment_terms or None)
def test_inter_company_transaction_without_default_warehouse(self):
"Check mapping (expense account) of inter company SI to PI in absence of default warehouse."
# setup
@@ -2716,6 +3000,67 @@ class TestSalesInvoice(ERPNextTestSuite):
frappe.local.enable_perpetual_inventory["_Test Company 1"] = old_perpetual_inventory
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", old_negative_stock)
def test_restrict_inter_company_pi_when_sales_invoice_qty_fully_consumed(self):
item_code_1 = "_Test IC Item 1"
item_code_2 = "_Test IC Item 2"
create_item(item_code_1, is_stock_item=1)
create_item(item_code_2, is_stock_item=1)
si = create_sales_invoice(
company="Wind Power LLC",
customer="_Test Internal Customer",
item_code=item_code_1,
debit_to="Debtors - WP",
warehouse="Stores - WP",
income_account="Sales - WP",
expense_account="Cost of Goods Sold - WP",
cost_center="Main - WP",
currency="USD",
qty=3,
do_not_save=1,
)
si.selling_price_list = "_Test Price List Rest of the World"
si.append(
"items",
{
"item_code": item_code_2,
"item_name": item_code_2,
"description": item_code_2,
"warehouse": "Stores - WP",
"qty": 2,
"uom": "Nos",
"stock_uom": "Nos",
"rate": 100,
"price_list_rate": 100,
"income_account": "Sales - WP",
"expense_account": "Cost of Goods Sold - WP",
"cost_center": "Main - WP",
"conversion_factor": 1,
},
)
si.submit()
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
for item in target_doc.items:
item.update(
{
"expense_account": "Cost of Goods Sold - _TC1",
"cost_center": "Main - _TC1",
}
)
target_doc.submit()
self.assertEqual(len(target_doc.items), 2)
self.assertEqual([item.qty for item in target_doc.items], [3, 2])
with self.assertRaisesRegex(
frappe.ValidationError,
"already been fully invoiced",
):
make_inter_company_transaction("Sales Invoice", si.name)
def test_sle_for_target_warehouse(self):
se = make_stock_entry(
item_code="138-CMS Shoe",
@@ -3319,6 +3664,52 @@ class TestSalesInvoice(ERPNextTestSuite):
party_link.delete()
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
@ERPNextTestSuite.change_settings("Accounts Settings", {"enable_common_party_accounting": True})
def test_sales_invoice_return_common_party_je_has_no_negative_amounts(self):
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
)
from erpnext.accounts.doctype.party_link.party_link import create_party_link
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.sales_and_purchase_return import make_return_doc
customer = make_customer(customer="_Test Common Party Return SI")
supplier = create_supplier(supplier_name="_Test Common Party Return SI").name
party_link = create_party_link("Supplier", supplier, customer)
si = create_sales_invoice(customer=customer, parent_cost_center="_Test Cost Center - _TC")
return_si = make_return_doc(si.doctype, si.name)
return_si.submit()
# JE for the return should credit the supplier (primary/advance) account
# and debit the customer (secondary/reconciliation) account — all positive amounts
jv_accounts = frappe.get_all(
"Journal Entry Account",
filters={"reference_type": return_si.doctype, "reference_name": return_si.name, "docstatus": 1},
fields=["debit_in_account_currency", "credit_in_account_currency", "account"],
)
self.assertTrue(jv_accounts, "Expected a Journal Entry for the return invoice")
for row in jv_accounts:
self.assertGreaterEqual(
row.debit_in_account_currency,
0,
f"Negative debit on account {row.account}",
)
self.assertGreaterEqual(
row.credit_in_account_currency,
0,
f"Negative credit on account {row.account}",
)
# Customer (secondary) account must be debited, not credited
customer_row = next(r for r in jv_accounts if r.account == return_si.debit_to)
self.assertGreater(customer_row.debit_in_account_currency, 0)
self.assertEqual(customer_row.credit_in_account_currency, 0)
party_link.delete()
def test_payment_statuses(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry

View File

@@ -104,6 +104,7 @@
"sales_order",
"so_detail",
"sales_invoice_item",
"pick_list_item",
"column_break_74",
"delivery_note",
"dn_detail",
@@ -112,6 +113,7 @@
"pos_invoice",
"pos_invoice_item",
"scio_detail",
"against_pick_list",
"internal_transfer_section",
"purchase_order",
"column_break_92",
@@ -855,8 +857,8 @@
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
"no_copy": 1,
"print_hide": 1,
"options": "currency",
"print_hide": 1,
"read_only": 1
},
{
@@ -947,7 +949,8 @@
"fieldtype": "Currency",
"label": "Distributed Discount Amount",
"options": "currency",
"print_hide": 1
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "available_quantity_section",
@@ -1010,13 +1013,30 @@
"label": "Consider for Tax Withholding",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "against_pick_list",
"fieldtype": "Link",
"hidden": 1,
"label": "Against Pick List",
"no_copy": 1,
"options": "Pick List",
"read_only": 1
},
{
"fieldname": "pick_list_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Pick List Item",
"no_copy": 1,
"read_only": 1
}
],
"grid_page_length": 50,
"idx": 1,
"istable": 1,
"links": [],
"modified": "2026-02-24 14:37:16.853941",
"modified": "2026-06-03 13:17:36.145788",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -22,6 +22,7 @@ class SalesInvoiceItem(Document):
actual_batch_qty: DF.Float
actual_qty: DF.Float
against_pick_list: DF.Link | None
allow_zero_valuation_rate: DF.Check
amount: DF.Currency
apply_tds: DF.Check
@@ -72,6 +73,7 @@ class SalesInvoiceItem(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
pick_list_item: DF.Data | None
pos_invoice: DF.Link | None
pos_invoice_item: DF.Data | None
price_list_rate: DF.Currency

View File

@@ -86,6 +86,39 @@ class Subscription(Document):
# update start just before the subscription doc is created
self.update_subscription_period(self.start_date)
def after_insert(self) -> None:
if frappe.flags.in_import or frappe.flags.in_migrate:
return
if getdate(self.start_date) > getdate(nowdate()):
return
self.generate_invoices_till_date()
def generate_invoices_till_date(self) -> None:
"""
Catch up a freshly created subscription by billing every elapsed period
from the start date up to today, then advancing the status (e.g. cancelling
if the end date has been crossed). Stops early when no further invoice is due
or an outstanding invoice blocks billing (per `generate_new_invoices_past_due_date`).
"""
while getdate(self._next_invoice_trigger_date()) <= getdate(nowdate()):
period_start = self.current_invoice_start
self.process(posting_date=self._next_invoice_trigger_date())
if self.status == "Cancelled" or getdate(self.current_invoice_start) == getdate(period_start):
break
if not self.generate_new_invoices_past_due_date:
break
def _next_invoice_trigger_date(self) -> DateTimeLikeObject:
if self.generate_invoice_at == "Beginning of the current subscription period":
return self.current_invoice_start
if self.generate_invoice_at == "Days before the current subscription period":
return add_days(self.current_invoice_start, -self.number_of_days)
return self.current_invoice_end
def update_subscription_period(self, date: DateTimeLikeObject | None = None):
"""
Subscription period is the period to be billed. This method updates the
@@ -269,7 +302,7 @@ class Subscription(Document):
Returns `True` if the grace period for the `Subscription` has passed
"""
if not self.current_invoice_is_past_due():
return
return False
grace_period = cint(frappe.get_value("Subscription Settings", None, "grace_period"))
return getdate(posting_date) >= getdate(add_days(self.current_invoice.due_date, grace_period))
@@ -281,6 +314,9 @@ class Subscription(Document):
if not self.current_invoice or self.is_paid(self.current_invoice):
return False
if not self.current_invoice.due_date:
return False
return getdate(posting_date) >= getdate(self.current_invoice.due_date)
@property
@@ -345,7 +381,13 @@ class Subscription(Document):
frappe.throw(_("Trial Period Start date cannot be after Subscription Start Date"))
def validate_end_date(self) -> None:
if not self.plans:
return
billing_cycle_info = self.get_billing_cycle_data()
if not billing_cycle_info:
return
end_date = add_to_date(self.start_date, **billing_cycle_info)
if self.end_date and getdate(self.end_date) <= getdate(end_date):
@@ -514,7 +556,7 @@ class Subscription(Document):
item_code = plan_doc.item
if self.party == "Customer":
if self.party_type == "Customer":
deferred_field = "enable_deferred_revenue"
else:
deferred_field = "enable_deferred_expense"
@@ -598,19 +640,22 @@ class Subscription(Document):
if self.has_outstanding_invoice() and not self.generate_new_invoices_past_due_date:
return False
if self.generate_invoice_at == "Beginning of the current subscription period" and (
getdate(posting_date) == getdate(self.current_invoice_start)
):
return True
elif self.generate_invoice_at == "Days before the current subscription period" and (
getdate(posting_date) == getdate(add_days(self.current_invoice_start, -1 * self.number_of_days))
):
return True
elif getdate(posting_date) == getdate(self.current_invoice_end):
return True
else:
posting = getdate(posting_date)
trigger = getdate(self._next_invoice_trigger_date())
if posting < trigger:
return False
# Cap the late-fire window at one billing cycle past the period end so a
# multi-year gap doesn't retroactively bill cycle after cycle in one call.
billing_cycle_info = self.get_billing_cycle_data()
if billing_cycle_info:
upper = getdate(add_to_date(self.current_invoice_end, **billing_cycle_info))
else:
upper = getdate(self.current_invoice_end)
return posting <= upper
def is_current_invoice_generated(
self,
_current_start_date: DateTimeLikeObject | None = None,
@@ -650,13 +695,6 @@ class Subscription(Document):
if invoice:
return frappe.get_doc(self.invoice_document_type, invoice[0])
def cancel_subscription_at_period_end(self) -> None:
"""
Called when `Subscription.cancel_at_period_end` is truthy
"""
self.status = "Cancelled"
self.cancelation_date = nowdate()
@property
def invoices(self) -> list[dict]:
return frappe.get_all(
@@ -703,7 +741,7 @@ class Subscription(Document):
self.status = "Cancelled"
self.cancelation_date = nowdate()
if to_generate_invoice and self.cancelation_date >= self.current_invoice_start:
if to_generate_invoice and getdate(self.cancelation_date) >= getdate(self.current_invoice_start):
self.generate_invoice(self.current_invoice_start, self.cancelation_date)
self.save()
@@ -731,7 +769,7 @@ class Subscription(Document):
"""
# Don't process future subscriptions
if nowdate() < self.current_invoice_start:
if getdate(nowdate()) < getdate(self.current_invoice_start):
frappe.msgprint(_("Subscription for Future dates cannot be processed."))
return
@@ -770,10 +808,10 @@ def process_all(subscription: list, posting_date: DateTimeLikeObject | None = No
for subscription_name in subscription:
try:
subscription = frappe.get_doc("Subscription", subscription_name)
subscription.process(posting_date)
sub = frappe.get_doc("Subscription", subscription_name)
sub.process(posting_date)
if not frappe.in_test:
frappe.db.commit()
except frappe.ValidationError:
frappe.db.rollback()
subscription.log_error("Subscription failed")
sub.log_error("Subscription failed")

View File

@@ -17,7 +17,8 @@ from frappe.utils.data import (
)
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor
from erpnext.accounts.doctype.subscription.subscription import Subscription, get_prorata_factor, process_all
from erpnext.accounts.utils import update_subscription_on_invoice_update
from erpnext.tests.utils import ERPNextTestSuite
@@ -61,16 +62,13 @@ class TestSubscription(ERPNextTestSuite):
self.assertRaises(frappe.ValidationError, subscription.save)
def test_invoice_is_generated_at_end_of_billing_period(self):
# Back-dated postpaid period has already ended, so catch-up bills it on creation
# and advances to the next period.
subscription = create_subscription(start_date="2018-01-01")
self.assertEqual(subscription.status, "Active")
self.assertEqual(subscription.current_invoice_start, "2018-01-01")
self.assertEqual(subscription.current_invoice_end, "2018-01-31")
subscription.process(posting_date="2018-01-31")
self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(subscription.current_invoice_start, "2018-02-01")
self.assertEqual(subscription.current_invoice_end, "2018-02-28")
self.assertEqual(subscription.status, "Unpaid")
self.assertEqual(getdate(subscription.current_invoice_start), getdate("2018-02-01"))
self.assertEqual(getdate(subscription.current_invoice_end), getdate("2018-02-28"))
def test_status_goes_back_to_active_after_invoice_is_paid(self):
subscription = create_subscription(
@@ -100,12 +98,10 @@ class TestSubscription(ERPNextTestSuite):
settings.cancel_after_grace = 1
settings.save()
# Back-dated unpaid invoice is already past its (zero) grace period, so catch-up
# cancels the subscription on creation.
subscription = create_subscription(start_date="2018-01-01")
self.assertEqual(subscription.status, "Active")
subscription.process(posting_date="2018-01-31") # generate first invoice
# This should change status to Cancelled since grace period is 0
# And is backdated subscription so subscription will be cancelled after processing
self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(subscription.status, "Cancelled")
def test_subscription_unpaid_after_grace_period(self):
@@ -257,18 +253,12 @@ class TestSubscription(ERPNextTestSuite):
settings.cancel_after_grace = 1
settings.save()
# Back-dated unpaid invoice past grace -> cancelled with one invoice on creation.
subscription = create_subscription(start_date="2018-01-01")
subscription.process() # generate first invoice
# Generate an invoice for the cancelled period
subscription.cancel_subscription()
self.assertEqual(subscription.status, "Cancelled")
self.assertEqual(len(subscription.invoices), 1)
subscription.process()
self.assertEqual(subscription.status, "Cancelled")
self.assertEqual(len(subscription.invoices), 1)
# Re-processing a cancelled subscription is a no-op.
subscription.process()
self.assertEqual(subscription.status, "Cancelled")
self.assertEqual(len(subscription.invoices), 1)
@@ -407,13 +397,21 @@ class TestSubscription(ERPNextTestSuite):
subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
subscription.save()
# even though subscription starts at "2018-01-15" and Billing interval is Month and count 3
# First invoice will end at "2018-03-31" instead of "2018-04-14"
self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31")
# The first (prepaid) period is billed on creation. Even though the subscription
# starts at "2018-01-15" with a 3-month interval, follow_calendar_months ends the
# first invoice at "2018-03-31" instead of "2018-04-14".
self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(
getdate(frappe.db.get_value("Purchase Invoice", subscription.invoices[0].name, "to_date")),
getdate("2018-03-31"),
)
def test_subscription_generate_invoice_past_due(self):
# With `generate_new_invoices_past_due_date` enabled, catch-up bills every elapsed
# 3-month period up to the end date on creation, even while previous ones are unpaid.
subscription = create_subscription(
start_date="2018-01-01",
end_date="2018-12-31",
party_type="Supplier",
party="_Test Supplier",
generate_invoice_at="Beginning of the current subscription period",
@@ -421,18 +419,9 @@ class TestSubscription(ERPNextTestSuite):
plans=[{"plan": "_Test Plan Name 4", "qty": 1}],
)
# Process subscription and create first invoice
# Subscription status will be unpaid since due date has already passed
subscription.process(posting_date="2018-01-01")
self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(len(subscription.invoices), 4)
self.assertEqual(subscription.status, "Unpaid")
# Now the Subscription is unpaid
# Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in
# subscription and the interval between the subscriptions is 3 months
subscription.process(posting_date="2018-04-01")
self.assertEqual(len(subscription.invoices), 2)
def test_subscription_without_generate_invoice_past_due(self):
subscription = create_subscription(
start_date="2018-01-01",
@@ -493,16 +482,13 @@ class TestSubscription(ERPNextTestSuite):
"""Test if Subscription recovers when start/end date run out of sync with created invoices."""
subscription = create_subscription(
start_date="2021-01-01",
end_date="2021-02-28",
submit_invoice=0,
generate_new_invoices_past_due_date=1,
party="_Test Subscription Customer John Doe",
)
# create invoices for the first two moths
subscription.process(posting_date="2021-01-31")
subscription.process(posting_date="2021-02-28")
# Catch-up bills both elapsed months on creation.
self.assertEqual(len(subscription.invoices), 2)
self.assertEqual(
getdate(frappe.db.get_value("Sales Invoice", subscription.invoices[0].name, "from_date")),
@@ -513,7 +499,7 @@ class TestSubscription(ERPNextTestSuite):
getdate("2021-02-01"),
)
# recreate most recent invoice
# Re-processing much later must not duplicate the already-billed periods.
subscription.process(posting_date="2022-01-31")
self.assertEqual(len(subscription.invoices), 2)
@@ -527,17 +513,16 @@ class TestSubscription(ERPNextTestSuite):
)
def test_subscription_invoice_generation_before_days(self):
# "Days before" trigger fires 10 days ahead of each period; catch-up bills both
# elapsed periods (within the end date) on creation.
subscription = create_subscription(
start_date="2023-01-01",
end_date="2023-02-28",
generate_invoice_at="Days before the current subscription period",
number_of_days=10,
generate_new_invoices_past_due_date=1,
)
subscription.process(posting_date="2022-12-22")
self.assertEqual(len(subscription.invoices), 1)
subscription.process(posting_date="2023-01-22")
self.assertEqual(len(subscription.invoices), 2)
def test_future_subscription(self):
@@ -596,13 +581,7 @@ class TestSubscription(ERPNextTestSuite):
generate_invoice_at="Beginning of the current subscription period",
plans=[{"plan": "_Test plan name 10", "qty": 1}],
)
subscription.process(posting_date=add_days(start_date, 2))
self.assertEqual(len(subscription.invoices), 1)
subscription.process(posting_date=add_days(start_date, 5))
self.assertEqual(len(subscription.invoices), 2)
subscription.process(posting_date=add_days(start_date, 8))
# Catch-up billing on creation generates every elapsed period and cancels at end
self.assertEqual(len(subscription.invoices), 3)
self.assertEqual(subscription.status, "Cancelled")
@@ -624,20 +603,71 @@ class TestSubscription(ERPNextTestSuite):
plans=[{"plan": "_Test plan name 10", "qty": 1}],
)
subscription.process(posting_date=add_days(start_date, 2))
self.assertEqual(len(subscription.invoices), 1)
subscription.process(posting_date=add_days(start_date, 5))
self.assertEqual(len(subscription.invoices), 2)
# partial last cycle invoice
subscription.process(posting_date=add_days(start_date, 6))
# Catch-up billing on creation incl. the partial last cycle, then cancels at end
self.assertEqual(len(subscription.invoices), 3)
self.assertEqual(subscription.status, "Cancelled")
self.assertRaises(frappe.ValidationError, subscription.process, posting_date=add_days(start_date, 7))
def test_invoice_generated_when_scheduler_runs_one_day_late(self):
# The trigger date (period end) is long past, yet catch-up still bills the period
# on creation (Bug 1: the check is `>= trigger`, not `== trigger`).
subscription = create_subscription(start_date="2018-01-01")
self.assertEqual(len(subscription.invoices), 1)
def test_deferred_revenue_applied_for_customer_subscription(self):
item_code = "_Test Non Stock Item"
frappe.db.set_value("Item", item_code, "enable_deferred_revenue", 1)
try:
# Build the period without saving, so on-create billing doesn't try to post an
# invoice (the deferred item has no account configured). This only exercises the
# item-mapping helper.
subscription = create_subscription(start_date="2018-01-01", do_not_save=True)
subscription.update_subscription_period("2018-01-01")
items = subscription.get_items_from_plans(subscription.plans)
self.assertEqual(items[0].get("enable_deferred_revenue"), 1)
self.assertEqual(getdate(items[0]["service_start_date"]), getdate("2018-01-01"))
self.assertEqual(getdate(items[0]["service_end_date"]), getdate("2018-01-31"))
finally:
frappe.db.set_value("Item", item_code, "enable_deferred_revenue", 0)
def test_validate_end_date_with_no_plans_does_not_crash(self):
sub = frappe.new_doc("Subscription")
sub.party_type = "Customer"
sub.party = "_Test Customer"
sub.company = "_Test Company"
sub.start_date = "2018-01-01"
sub.end_date = "2018-03-01"
try:
sub.validate_end_date()
except TypeError as e:
self.fail(f"validate_end_date crashed with no plans: {e}")
def test_process_all_logs_error_when_first_subscription_fails(self):
sub1 = create_subscription(start_date="2018-01-01")
sub2 = create_subscription(start_date="2018-01-02")
processed = []
original_process = Subscription.process
original_rollback = frappe.db.rollback
def patched(self, posting_date=None):
processed.append(self.name)
if self.name == sub1.name:
raise frappe.ValidationError("forced failure")
Subscription.process = patched
# process_all calls frappe.db.rollback() on error which would otherwise wipe
# the test transaction; stub it so we can observe the iteration in isolation.
frappe.db.rollback = lambda *a, **kw: None
try:
process_all([sub1.name, sub2.name])
finally:
Subscription.process = original_process
frappe.db.rollback = original_rollback
self.assertEqual(processed, [sub1.name, sub2.name])
def test_subscription_auto_completion(self):
create_plan(
plan_name="_Test Plan 3 Day",
@@ -674,10 +704,106 @@ class TestSubscription(ERPNextTestSuite):
for invoice in invoices:
pi = get_payment_entry("Sales Invoice", invoice.name)
pi.submit()
# Paying the invoices refreshes the subscription via the Payment Entry hook, so
# reload before processing the stale in-memory copy.
subscription.reload()
# After processing through all days, subscription should be completed
subscription.process(posting_date=add_days(end_date, 1))
self.assertEqual(subscription.status, "Completed")
def test_status_updates_immediately_when_invoice_paid(self):
subscription = create_subscription(
start_date=nowdate(),
generate_invoice_at="Beginning of the current subscription period",
submit_invoice=1,
)
subscription.process(posting_date=nowdate())
self.assertEqual(subscription.status, "Unpaid")
invoice = subscription.get_current_invoice()
payment = get_payment_entry("Sales Invoice", invoice.name)
payment.submit()
subscription.reload()
self.assertEqual(subscription.status, "Active")
def test_invoice_update_hook_refreshes_subscription_status(self):
subscription = create_subscription(
start_date=nowdate(),
generate_invoice_at="Beginning of the current subscription period",
submit_invoice=1,
)
subscription.process(posting_date=nowdate())
self.assertEqual(subscription.status, "Unpaid")
invoice = subscription.get_current_invoice()
invoice.db_set("outstanding_amount", 0)
invoice.db_set("status", "Paid")
update_subscription_on_invoice_update(invoice)
subscription.reload()
self.assertEqual(subscription.status, "Active")
def test_payment_entry_triggers_subscription_status_update(self):
# Test that payment entry → invoice → subscription status update chain works
subscription = create_subscription(
start_date=nowdate(),
generate_invoice_at="Beginning of the current subscription period",
submit_invoice=1,
)
subscription.process(posting_date=nowdate())
self.assertEqual(subscription.status, "Unpaid")
invoice = subscription.get_current_invoice()
self.assertIsNotNone(invoice)
self.assertGreater(invoice.outstanding_amount, 0)
# Create and submit payment entry
payment_entry = get_payment_entry(invoice.doctype, invoice.name, bank_account="_Test Bank - _TC")
payment_entry.reference_no = "12345"
payment_entry.reference_date = nowdate()
payment_entry.submit()
# Subscription status should now be Active (via on_update_after_submit hook)
subscription.reload()
self.assertEqual(subscription.status, "Active")
def test_first_invoice_generated_on_create_for_prepaid(self):
subscription = create_subscription(
start_date=nowdate(),
generate_invoice_at="Beginning of the current subscription period",
)
self.assertEqual(len(subscription.invoices), 1)
def test_first_invoice_not_generated_on_create_during_trial(self):
subscription = create_subscription(
start_date=nowdate(),
trial_period_start=nowdate(),
trial_period_end=add_days(nowdate(), 30),
generate_invoice_at="Beginning of the current subscription period",
)
self.assertEqual(len(subscription.invoices), 0)
self.assertEqual(subscription.status, "Trialing")
def test_first_invoice_not_generated_during_bulk_import(self):
frappe.flags.in_import = True
try:
subscription = create_subscription(
start_date=nowdate(),
generate_invoice_at="Beginning of the current subscription period",
)
self.assertEqual(len(subscription.invoices), 0)
finally:
frappe.flags.in_import = False
def test_first_invoice_not_generated_for_future_dated_subscription(self):
subscription = create_subscription(
start_date=add_days(nowdate(), 10),
generate_invoice_at="Beginning of the current subscription period",
)
self.assertEqual(len(subscription.invoices), 0)
def make_plans():
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")

View File

@@ -7,7 +7,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import getdate
from frappe.utils import cstr, getdate
from erpnext import allow_regional
from erpnext.controllers.accounts_controller import validate_account_head
@@ -48,7 +48,7 @@ class TaxWithholdingCategory(Document):
for d in self.get("rates"):
if getdate(d.from_date) >= getdate(d.to_date):
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
group_rates[d.tax_withholding_group].append(d)
group_rates[cstr(d.tax_withholding_group)].append(d)
# Validate overlapping dates within each group
for group, rates in group_rates.items():
@@ -92,10 +92,9 @@ class TaxWithholdingCategory(Document):
def get_applicable_tax_row(self, posting_date, tax_withholding_group):
for row in self.rates:
if (
getdate(row.from_date) <= getdate(posting_date) <= getdate(row.to_date)
and row.tax_withholding_group == tax_withholding_group
):
if getdate(row.from_date) <= getdate(posting_date) <= getdate(row.to_date) and cstr(
row.tax_withholding_group
) == cstr(tax_withholding_group):
return row
frappe.throw(_("No Tax Withholding data found for the current posting date."))
@@ -116,7 +115,7 @@ class TaxWithholdingDetails:
def __init__(
self,
tax_withholding_categories: list[str],
tax_withholding_group: str,
tax_withholding_group: str | None,
posting_date: str,
party_type: str,
party: str,

View File

@@ -999,6 +999,47 @@ class TestTaxWithholdingCategory(ERPNextTestSuite):
self.cleanup_invoices(invoices)
def test_null_and_empty_tax_withholding_group_are_equivalent(self):
"""
NULL and empty-string `tax_withholding_group` must be treated as the
same value.
"""
category = frappe.get_doc("Tax Withholding Category", "Cumulative Threshold TDS")
original_row = category.rates[0]
original_row.tax_withholding_group = None
# Part 1: validate_dates must detect overlap between NULL-group and
# empty-string-group rows covering the same date range.
category.append(
"rates",
{
"from_date": original_row.from_date,
"to_date": original_row.to_date,
"tax_withholding_group": "",
"tax_withholding_rate": original_row.tax_withholding_rate,
},
)
with self.assertRaises(frappe.ValidationError):
category.validate_dates()
category.rates.pop()
# Part 2: get_applicable_tax_row must match NULL <-> "" in either direction.
posting_date = original_row.from_date
row = category.get_applicable_tax_row(posting_date=posting_date, tax_withholding_group="")
self.assertEqual(row.name, original_row.name)
row = category.get_applicable_tax_row(posting_date=posting_date, tax_withholding_group=None)
self.assertEqual(row.name, original_row.name)
original_row.tax_withholding_group = ""
row = category.get_applicable_tax_row(posting_date=posting_date, tax_withholding_group=None)
self.assertEqual(row.name, original_row.name)
original_row.tax_withholding_group = None
with self.assertRaises(frappe.ValidationError):
category.get_applicable_tax_row(posting_date=posting_date, tax_withholding_group="194R")
def test_tds_calculation_on_net_total(self):
self.setup_party_with_category("Supplier", "Test TDS Supplier4", "Cumulative Threshold TDS")
invoices = []

View File

@@ -4,14 +4,14 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Purchase Invoice",
"dynamic_filters_json": "[[\"Purchase Invoice\",\"company\",\"=\",\" frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\"],[\"Purchase Invoice\",\"posting_date\",\"Timespan\",\"this year\"]]",
"dynamic_filters_json": "[[\"Purchase Invoice\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Purchase Invoice\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
"filters_json": "[[\"Purchase Invoice\",\"docstatus\",\"=\",\"1\"]]",
"function": "Sum",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"label": "Total Incoming Bills",
"modified": "2024-12-05 12:00:00.000000",
"modified": "2026-06-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Total Incoming Bills",

View File

@@ -4,14 +4,14 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Payment Entry",
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\"]]",
"dynamic_filters_json": "[[\"Payment Entry\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Receive\"]]",
"function": "Sum",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"label": "Total Incoming Payment",
"modified": "2024-12-05 12:00:00.000000",
"modified": "2026-06-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Total Incoming Payment",

View File

@@ -4,14 +4,14 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Sales Invoice",
"dynamic_filters_json": "[[\"Sales Invoice\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\"],[\"Sales Invoice\",\"posting_date\",\"Timespan\",\"this year\"]]",
"dynamic_filters_json": "[[\"Sales Invoice\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Sales Invoice\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
"filters_json": "[[\"Sales Invoice\",\"docstatus\",\"=\",\"1\"]]",
"function": "Sum",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"label": "Total Outgoing Bills",
"modified": "2024-12-05 12:00:00.000000",
"modified": "2026-06-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Total Outgoing Bills",

View File

@@ -4,14 +4,14 @@
"docstatus": 0,
"doctype": "Number Card",
"document_type": "Payment Entry",
"dynamic_filters_json": "[[\"Payment Entry\",\"company\",\"=\",\"frappe.defaults.get_user_default(\\\"Company\\\")\"]]",
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"posting_date\",\"Timespan\",\"this year\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\"]]",
"dynamic_filters_json": "[[\"Payment Entry\", \"company\", \"=\", \"frappe.defaults.get_user_default(\\\"Company\\\")\"], [\"Payment Entry\", \"posting_date\", \"Between\", \"(frappe.boot.current_fiscal_year || [null, `${frappe.datetime.get_today().slice(0,4)}-01-01`, `${frappe.datetime.get_today().slice(0,4)}-12-31`]).slice(1)\"]]",
"filters_json": "[[\"Payment Entry\",\"docstatus\",\"=\",\"1\"],[\"Payment Entry\",\"payment_type\",\"=\",\"Pay\"]]",
"function": "Sum",
"idx": 0,
"is_public": 1,
"is_standard": 1,
"label": "Total Outgoing Payment",
"modified": "2024-12-05 12:00:00.000000",
"modified": "2026-06-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Total Outgoing Payment",

View File

@@ -48,6 +48,25 @@ SALES_TRANSACTION_TYPES = {
}
TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES
# Party-derived fields that must NOT be auto-copied by `get_mapped_doc` when the
# source and target documents belong to different parties (e.g. Sales Order →
# Purchase Order or inter-company Sales Invoice → Purchase Invoice).
CROSS_PARTY_FIELD_NO_MAP = [
"tax_category",
"tax_id",
"tax_withholding_category",
"taxes_and_charges",
"address_display",
"contact_display",
"contact_mobile",
"contact_email",
"contact_person",
"shipping_address",
"dispatch_address",
"payment_terms_template",
"language",
]
class DuplicatePartyAccountError(frappe.ValidationError):
pass

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,32 +1,37 @@
{
"add_total_row": 1,
"apply_user_permissions": 1,
"creation": "2013-04-22 16:16:03",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"modified": "2017-02-24 20:09:46.150861",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Payable",
"owner": "Administrator",
"ref_doctype": "Purchase Invoice",
"report_name": "Accounts Payable",
"report_type": "Script Report",
"add_total_row": 1,
"add_translate_data": 0,
"columns": [],
"creation": "2013-04-22 16:16:03",
"default_print_format": "Accounts Payable Standard",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"modified": "2026-05-22 14:35:14.716933",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Payable",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Invoice",
"report_name": "Accounts Payable",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
},
{
"role": "Purchase User"
},
},
{
"role": "Accounts Manager"
},
},
{
"role": "Auditor"
}
]
}
],
"timeout": 0
}

View File

@@ -1,32 +1,37 @@
{
"add_total_row": 1,
"apply_user_permissions": 1,
"creation": "2014-11-04 12:09:59.672379",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 20:11:35.655834",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Payable Summary",
"owner": "Administrator",
"ref_doctype": "Purchase Invoice",
"report_name": "Accounts Payable Summary",
"report_type": "Script Report",
"add_total_row": 1,
"add_translate_data": 0,
"columns": [],
"creation": "2014-11-04 12:09:59.672379",
"default_print_format": "Accounts Payable Summary Standard",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 2,
"is_standard": "Yes",
"modified": "2026-05-22 14:35:19.179799",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Payable Summary",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Invoice",
"report_name": "Accounts Payable Summary",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
},
{
"role": "Purchase User"
},
},
{
"role": "Accounts Manager"
},
},
{
"role": "Auditor"
}
]
}
],
"timeout": 0
}

View File

@@ -1,26 +1,31 @@
{
"add_total_row": 1,
"apply_user_permissions": 1,
"creation": "2013-04-16 11:31:13",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"modified": "2017-03-06 05:52:06.235584",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Receivable",
"owner": "Administrator",
"ref_doctype": "Sales Invoice",
"report_name": "Accounts Receivable",
"report_type": "Script Report",
"add_total_row": 1,
"add_translate_data": 0,
"columns": [],
"creation": "2013-04-16 11:31:13",
"default_print_format": "Accounts Receivable Standard",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 5,
"is_standard": "Yes",
"modified": "2026-05-22 14:34:57.666402",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Receivable",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Sales Invoice",
"report_name": "Accounts Receivable",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts Manager"
},
},
{
"role": "Accounts User"
}
]
}
],
"timeout": 0
}

View File

@@ -1,26 +1,31 @@
{
"add_total_row": 1,
"apply_user_permissions": 1,
"creation": "2014-10-17 15:45:00.694265",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-03-06 05:52:23.751082",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Receivable Summary",
"owner": "Administrator",
"ref_doctype": "Sales Invoice",
"report_name": "Accounts Receivable Summary",
"report_type": "Script Report",
"add_total_row": 1,
"add_translate_data": 0,
"columns": [],
"creation": "2014-10-17 15:45:00.694265",
"default_print_format": "Accounts Receivable Summary Standard",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 2,
"is_standard": "Yes",
"modified": "2026-05-22 14:35:10.656797",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Receivable Summary",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Sales Invoice",
"report_name": "Accounts Receivable Summary",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts Manager"
},
},
{
"role": "Accounts User"
}
]
}
],
"timeout": 0
}

View File

@@ -1,29 +1,34 @@
{
"add_total_row": 0,
"creation": "2014-07-14 05:24:20.385279",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2018-09-07 12:18:21.850851",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Balance Sheet",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "Balance Sheet",
"report_type": "Script Report",
"add_total_row": 0,
"add_translate_data": 0,
"columns": [],
"creation": "2014-07-14 05:24:20.385279",
"default_print_format": "Balance Sheet Standard",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"modified": "2026-05-22 14:35:28.187799",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Balance Sheet",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "Balance Sheet",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
},
{
"role": "Accounts Manager"
},
},
{
"role": "Auditor"
}
]
}
],
"timeout": 0
}

View File

@@ -38,6 +38,14 @@ function get_filters() {
let budget_against_options = get_dimensions();
let filters = [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
},
{
fieldname: "from_fiscal_year",
label: __("From Fiscal Year"),
@@ -67,14 +75,6 @@ function get_filters() {
default: "Yearly",
reqd: 1,
},
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1,
},
{
fieldname: "budget_against",
label: __("Budget Against"),
@@ -96,9 +96,12 @@ function get_filters() {
if (!frappe.query_report.filters) return;
let budget_against = frappe.query_report.get_filter_value("budget_against");
let company = frappe.query_report.get_filter_value("company");
if (!budget_against) return;
return frappe.db.get_link_options(budget_against, txt);
const filters = budget_against !== "Branch" && company ? { company: company } : {};
return frappe.db.get_link_options(budget_against, txt, filters);
},
},
{

View File

@@ -3,6 +3,7 @@
import frappe
from frappe import _
from frappe.query_builder import CustomFunction
from frappe.utils import add_months, flt, formatdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
@@ -19,6 +20,8 @@ def execute(filters=None):
columns = get_columns(filters)
if filters.get("budget_against_filter"):
dimensions = filters.get("budget_against_filter")
if filters.get("budget_against") == "Cost Center":
dimensions = get_cost_center_with_children(dimensions)
else:
dimensions = get_budget_dimensions(filters)
if not dimensions:
@@ -40,39 +43,29 @@ def validate_filters(filters):
def get_budget_records(filters, dimensions):
budget_against_field = frappe.scrub(filters["budget_against"])
budget = frappe.qb.DocType("Budget")
return frappe.db.sql(
f"""
SELECT
b.name,
b.account,
b.{budget_against_field} AS dimension,
b.budget_amount,
b.from_fiscal_year,
b.to_fiscal_year,
b.budget_start_date,
b.budget_end_date
FROM
`tabBudget` b
WHERE
b.company = %s
AND b.docstatus = 1
AND b.budget_against = %s
AND b.{budget_against_field} IN ({", ".join(["%s"] * len(dimensions))})
AND (
b.from_fiscal_year <= %s
AND b.to_fiscal_year >= %s
)
""",
(
filters.company,
filters.budget_against,
*dimensions,
filters.to_fiscal_year,
filters.from_fiscal_year,
),
as_dict=True,
)
return (
frappe.qb.from_(budget)
.select(
budget.name,
budget.account,
budget[budget_against_field].as_("dimension"),
budget.budget_amount,
budget.from_fiscal_year,
budget.to_fiscal_year,
budget.budget_start_date,
budget.budget_end_date,
)
.where(
(budget.company == filters.company)
& (budget.docstatus == 1)
& (budget.budget_against == filters.budget_against)
& (budget[budget_against_field].isin(dimensions))
& (budget.from_fiscal_year <= filters.to_fiscal_year)
& (budget.to_fiscal_year >= filters.from_fiscal_year)
)
).run(as_dict=True)
def build_budget_map(budget_records, filters):
@@ -120,50 +113,41 @@ def build_budget_map(budget_records, filters):
def get_actual_transactions(dimension_name, filters):
budget_against = frappe.scrub(filters.get("budget_against"))
cost_center_filter = ""
monthname = CustomFunction("MONTHNAME", ["date"])
gle = frappe.qb.DocType("GL Entry")
budget = frappe.qb.DocType("Budget")
query = (
frappe.qb.from_(gle)
.from_(budget)
.select(
gle.account,
gle.debit,
gle.credit,
gle.fiscal_year,
monthname(gle.posting_date).as_("month_name"),
budget[budget_against].as_("budget_against"),
)
.where(
(budget.docstatus == 1)
& (budget.account == gle.account)
& (gle.fiscal_year >= filters.from_fiscal_year)
& (gle.fiscal_year <= filters.to_fiscal_year)
& (gle.is_cancelled == 0)
& (budget[budget_against] == dimension_name)
)
.groupby(gle.name)
.orderby(gle.fiscal_year)
)
if filters.get("budget_against") == "Cost Center" and dimension_name:
cc_lft, cc_rgt = frappe.db.get_value("Cost Center", dimension_name, ["lft", "rgt"])
cost_center_filter = f"""
and lft >= "{cc_lft}"
and rgt <= "{cc_rgt}"
"""
cost_centers = get_cost_center_with_children([dimension_name])
query = query.where(gle.cost_center.isin(cost_centers))
else:
query = query.where(budget[budget_against] == gle[budget_against])
actual_transactions = frappe.db.sql(
f"""
select
gl.account,
gl.debit,
gl.credit,
gl.fiscal_year,
MONTHNAME(gl.posting_date) as month_name,
b.{budget_against} as budget_against
from
`tabGL Entry` gl,
`tabBudget` b
where
b.docstatus = 1
and b.account=gl.account
and b.{budget_against} = gl.{budget_against}
and gl.fiscal_year between %s and %s
and gl.is_cancelled = 0
and b.{budget_against} = %s
and exists(
select
name
from
`tab{filters.budget_against}`
where
name = gl.{budget_against}
{cost_center_filter}
)
group by
gl.name
order by gl.fiscal_year
""",
(filters.from_fiscal_year, filters.to_fiscal_year, dimension_name),
as_dict=1,
)
actual_transactions = query.run(as_dict=True)
actual_transactions_map = {}
for transaction in actual_transactions:
@@ -382,33 +366,37 @@ def get_fiscal_years(filters):
return fiscal_year
def get_budget_dimensions(filters):
order_by = ""
if filters.get("budget_against") == "Cost Center":
order_by = "order by lft"
if filters.get("budget_against") in ["Cost Center", "Project"]:
return frappe.db.sql_list(
"""
select
name
from
`tab{tab}`
where
company = %s
{order_by}
""".format(tab=filters.get("budget_against"), order_by=order_by),
filters.get("company"),
def get_cost_center_with_children(cost_centers):
"""Expand each cost center to include itself and all its descendants."""
cc = frappe.qb.DocType("Cost Center")
all_cost_centers = set()
for cost_center in cost_centers:
result = frappe.db.get_value("Cost Center", cost_center, ["lft", "rgt"])
if not result:
continue
lft, rgt = result
children = (
frappe.qb.from_(cc).select(cc.name).where((cc.lft >= lft) & (cc.rgt <= rgt)).run(pluck="name")
)
all_cost_centers.update(children)
return list(all_cost_centers)
def get_budget_dimensions(filters):
budget_against = filters.get("budget_against")
dimension = frappe.qb.DocType(budget_against)
if budget_against in ["Cost Center", "Project"]:
query = (
frappe.qb.from_(dimension)
.select(dimension.name)
.where(dimension.company == filters.get("company"))
)
if budget_against == "Cost Center":
query = query.orderby(dimension.lft)
return query.run(pluck="name")
else:
return frappe.db.sql_list(
"""
select
name
from
`tab{tab}`
""".format(tab=filters.get("budget_against"))
) # nosec
return frappe.qb.from_(dimension).select(dimension.name).run(pluck="name")
def validate_budget_dimensions(filters):

View File

@@ -1,29 +1,34 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2015-12-12 10:22:45.383203",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 20:09:19.748690",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cash Flow",
"owner": "Administrator",
"ref_doctype": "GL Entry",
"report_name": "Cash Flow",
"report_type": "Script Report",
"add_total_row": 0,
"add_translate_data": 0,
"columns": [],
"creation": "2015-12-12 10:22:45.383203",
"default_print_format": "Cash Flow Statement Standard",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"modified": "2026-05-22 14:35:34.353508",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cash Flow",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "Cash Flow",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
},
{
"role": "Accounts Manager"
},
},
{
"role": "Auditor"
}
]
}
],
"timeout": 0
}

View File

@@ -132,7 +132,14 @@ def execute(filters=None):
)
net_change_in_cash = add_total_row_account(
data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters
data,
data,
_("Net Change in Cash"),
period_list,
company_currency,
summary_data,
filters,
add_blank_row=False,
)
if filters.show_opening_and_closing_balance:
@@ -250,7 +257,17 @@ def get_start_date(period, accumulated_values, company):
return start_date
def add_total_row_account(out, data, label, period_list, currency, summary_data, filters, consolidated=False):
def add_total_row_account(
out,
data,
label,
period_list,
currency,
summary_data,
filters,
consolidated=False,
add_blank_row=True,
):
total_row = {
"section_name": "'" + _("{0}").format(label) + "'",
"section": "'" + _("{0}").format(label) + "'",
@@ -275,7 +292,9 @@ def add_total_row_account(out, data, label, period_list, currency, summary_data,
total_row["total"] += row["total"]
out.append(total_row)
out.append({})
if add_blank_row:
out.append({})
return total_row

View File

@@ -737,6 +737,9 @@ def compute_growth_view_data(data, columns):
data_copy = copy.deepcopy(data)
for row_idx in range(len(data_copy)):
if not data_copy[row_idx]:
continue
for column_idx in range(1, len(columns)):
previous_period_key = columns[column_idx - 1].get("key")
current_period_key = columns[column_idx].get("key")
@@ -785,13 +788,21 @@ def compute_margin_view_data(data, columns, accumulated_values):
for column in columns:
curr_period = column.get("key")
base_value = base_row[curr_period]
curr_value = row[curr_period]
if curr_value is None or base_value <= 0:
base_value = base_row.get(curr_period)
curr_value = row.get(curr_period)
if base_value is None or curr_value is None:
data[row_idx][curr_period] = None
continue
if base_value == 0:
if curr_value == 0:
data[row_idx][curr_period] = 0
else:
data[row_idx][curr_period] = None
continue
margin_percent = round((curr_value / base_value) * 100, 2)
data[row_idx][curr_period] = margin_percent

View File

@@ -3,14 +3,14 @@
"add_translate_data": 0,
"columns": [],
"creation": "2013-12-06 13:22:23",
"default_print_format": "General Ledger Standard",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"idx": 4,
"is_standard": "Yes",
"letterhead": null,
"modified": "2025-11-05 15:47:59.597853",
"modified": "2026-05-22 14:34:35.246000",
"modified_by": "Administrator",
"module": "Accounts",
"name": "General Ledger",

View File

@@ -1,9 +1,13 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.query_reports["Gross and Net Profit Report"] = $.extend({}, erpnext.financial_statements);
const GNP_REPORT = "Gross and Net Profit Report";
frappe.query_reports["Gross and Net Profit Report"]["filters"].push({
frappe.query_reports[GNP_REPORT] = $.extend({}, erpnext.financial_statements);
erpnext.utils.add_dimensions(GNP_REPORT, 10);
frappe.query_reports[GNP_REPORT]["filters"].push({
fieldname: "accumulated_values",
label: __("Accumulated Values"),
fieldtype: "Check",

View File

@@ -4,6 +4,7 @@
import frappe
from frappe import _
from frappe.query_builder import CustomFunction
from frappe.utils import cint
@@ -94,19 +95,35 @@ def get_data(filters):
def get_sales_details(filters):
item_details_map = {}
date_field = "s.transaction_date" if filters["based_on"] == "Sales Order" else "s.posting_date"
if filters["based_on"] not in ("Sales Order", "Sales Invoice"):
frappe.throw(_("Invalid value {0} for 'Based On'").format(filters["based_on"]))
sales_data = frappe.db.sql(
"""
select s.territory, s.customer, si.item_group, si.item_code, si.qty, {date_field} as last_order_date,
DATEDIFF(CURRENT_DATE, {date_field}) as days_since_last_order
from `tab{doctype}` s, `tab{doctype} Item` si
where s.name = si.parent and s.docstatus = 1
order by days_since_last_order """.format( # nosec
date_field=date_field, doctype=filters["based_on"]
),
as_dict=1,
)
parent = frappe.qb.DocType(filters["based_on"])
child_doctype = "Sales Order Item" if filters["based_on"] == "Sales Order" else "Sales Invoice Item"
child = frappe.qb.DocType(child_doctype)
date_diff = CustomFunction("DATEDIFF", ["d1", "d2"])
current_date = CustomFunction("CURRENT_DATE", [])
date_col = parent.transaction_date if filters["based_on"] == "Sales Order" else parent.posting_date
days_since_last_order = date_diff(current_date(), date_col)
sales_data = (
frappe.qb.from_(parent)
.inner_join(child)
.on(parent.name == child.parent)
.select(
parent.territory,
parent.customer,
child.item_group,
child.item_code,
child.qty,
date_col.as_("last_order_date"),
days_since_last_order.as_("days_since_last_order"),
)
.where(parent.docstatus == 1)
.orderby(days_since_last_order)
).run(as_dict=True)
for d in sales_data:
item_details_map.setdefault((d.territory, d.item_code), d)

View File

@@ -1,29 +1,34 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2014-07-18 11:43:33.173207",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 20:12:40.282376",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Profit and Loss Statement",
"owner": "Administrator",
"ref_doctype": "GL Entry",
"report_name": "Profit and Loss Statement",
"report_type": "Script Report",
"add_total_row": 0,
"add_translate_data": 0,
"columns": [],
"creation": "2014-07-18 11:43:33.173207",
"default_print_format": "P&L Statement Standard",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 2,
"is_standard": "Yes",
"modified": "2026-05-22 14:36:04.544347",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Profit and Loss Statement",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "Profit and Loss Statement",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
},
{
"role": "Accounts Manager"
},
},
{
"role": "Auditor"
}
]
}
],
"timeout": 0
}

View File

@@ -1,19 +1,23 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2014-07-22 11:41:23.743564",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 20:12:33.520866",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Trial Balance",
"owner": "Administrator",
"ref_doctype": "GL Entry",
"report_name": "Trial Balance",
"add_total_row": 0,
"add_translate_data": 0,
"columns": [],
"creation": "2014-07-22 11:41:23.743564",
"default_print_format": "Trial Balance Standard",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 2,
"is_standard": "Yes",
"modified": "2026-05-22 14:35:44.889062",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Trial Balance",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "Trial Balance",
"report_type": "Script Report",
"roles": [
{
@@ -25,5 +29,6 @@
{
"role": "Auditor"
}
]
}
],
"timeout": 0
}

View File

@@ -43,6 +43,8 @@ from erpnext.stock import get_warehouse_account_map
from erpnext.stock.utils import get_combine_datetime, get_stock_value_on
if TYPE_CHECKING:
from frappe.model.document import Document
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import RepostItemValuation
@@ -443,15 +445,13 @@ def add_ac(args=None):
if not args:
args = frappe.local.form_dict
args.pop("ignore_permissions", None)
frappe.has_permission("Account", "create", throw=True)
args.doctype = "Account"
args = make_tree_args(**args)
ac = frappe.new_doc("Account")
if args.get("ignore_permissions"):
ac.flags.ignore_permissions = True
args.pop("ignore_permissions")
ac.update(args)
if not ac.parent_account:
@@ -1540,6 +1540,7 @@ def update_cost_center(docname, cost_center_name, cost_center_number, company, m
Renames the document by adding the number as a prefix to the current name and updates
all transaction where it was present.
"""
frappe.has_permission("Cost Center", "write", doc=docname, throw=True)
validate_field_number("Cost Center", docname, cost_center_number, company, "cost_center_number")
if cost_center_number:
@@ -2710,3 +2711,14 @@ def build_qb_match_conditions(doctype, user=None) -> list:
def is_immutable_ledger_enabled():
return frappe.get_single_value("Accounts Settings", "enable_immutable_ledger")
def update_subscription_on_invoice_update(doc: "Document", method: str | None = None) -> None:
if doc.get("subscription"):
refresh_subscription_status(doc.subscription)
def refresh_subscription_status(name: str) -> None:
subscription = frappe.get_doc("Subscription", name)
subscription.set_subscription_status()
subscription.save(ignore_permissions=True)

View File

@@ -7,6 +7,7 @@ from frappe import _
from frappe.query_builder import Order
from frappe.query_builder.functions import Max, Min
from frappe.utils import (
DateTimeLikeObject,
add_months,
cint,
flt,
@@ -359,7 +360,8 @@ def get_message_for_depr_entry_posting_error(asset_links, error_log_links):
@frappe.whitelist()
def scrap_asset(asset_name, scrap_date=None):
def scrap_asset(asset_name: str, scrap_date: DateTimeLikeObject | None = None):
frappe.has_permission("Asset", "write", asset_name, throw=True)
asset = frappe.get_doc("Asset", asset_name)
scrap_date = getdate(scrap_date) or getdate(today())
asset.db_set("disposal_date", scrap_date)
@@ -448,7 +450,8 @@ def create_journal_entry_for_scrap(asset, scrap_date):
@frappe.whitelist()
def restore_asset(asset_name):
def restore_asset(asset_name: str):
frappe.has_permission("Asset", "write", asset_name, throw=True)
asset = frappe.get_doc("Asset", asset_name)
reverse_depreciation_entry_made_on_disposal(asset)
reset_depreciation_schedule(asset, get_note_for_restore(asset))

View File

@@ -31,7 +31,8 @@ class BulkTransactionLog(Document):
log_detail = qb.DocType("Bulk Transaction Log Detail")
has_records = frappe.db.sql(
f"select exists (select * from `tabBulk Transaction Log Detail` where date = '{self.name}');"
"select exists (select * from `tabBulk Transaction Log Detail` where date = %s);",
(self.name,),
)[0][0]
if not has_records:
raise frappe.DoesNotExistError

View File

@@ -17,6 +17,7 @@
"section_break_vwgg",
"maintain_same_rate",
"column_break_lwxs",
"set_landed_cost_based_on_purchase_invoice_rate",
"maintain_same_rate_action",
"role_to_override_stop_action",
"transaction_settings_section",
@@ -24,7 +25,8 @@
"po_required",
"pr_required",
"project_update_frequency",
"column_break_12",
"over_order_allowance",
"column_break_kdcm",
"allow_multiple_items",
"allow_negative_rates_for_items",
"set_valuation_rate_for_rejected_materials",
@@ -33,7 +35,6 @@
"purchase_invoice_settings_section",
"bill_for_rejected_quantity_in_purchase_invoice",
"use_transaction_date_exchange_rate",
"set_landed_cost_based_on_purchase_invoice_rate",
"zero_quantity_line_items_section",
"allow_zero_qty_in_supplier_quotation",
"allow_zero_qty_in_request_for_quotation",
@@ -156,10 +157,6 @@
"fieldtype": "Tab Break",
"label": "Transaction Settings"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Prevents the system from automatically using the rate from the last purchase transaction when creating new purchase orders or transactions.",
@@ -335,6 +332,16 @@
"hidden": 1,
"is_virtual": 1,
"label": "Naming Series options"
},
{
"description": "The percentage by which you are allowed to order more on a Purchase Order than the quantity requested on the originating Material Request. For example, if the Material Request has 100 units and the allowance is 10%, you can order up to 110 units",
"fieldname": "over_order_allowance",
"fieldtype": "Float",
"label": "Over Order Allowance (%)"
},
{
"fieldname": "column_break_kdcm",
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
@@ -343,7 +350,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-05-05 16:30:37.184607",
"modified": "2026-05-27 23:04:00.842393",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -34,6 +34,7 @@ class BuyingSettings(Document):
fixed_email: DF.Link | None
maintain_same_rate: DF.Check
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
over_order_allowance: DF.Float
over_transfer_allowance: DF.Float
po_required: DF.Literal["No", "Yes"]
pr_required: DF.Literal["No", "Yes"]

View File

@@ -23,8 +23,6 @@
"is_subcontracted",
"has_unit_price_items",
"supplier_warehouse",
"section_break_ahub",
"title",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -56,9 +54,7 @@
"net_total",
"section_break_48",
"pricing_rules",
"raw_material_details",
"set_reserve_warehouse",
"supplied_items",
"taxes_section",
"tax_category",
"taxes_and_charges",
@@ -156,6 +152,7 @@
"auto_repeat",
"update_auto_repeat_reference",
"additional_info_section",
"title",
"party_account_currency",
"represents_company",
"ref_sq",
@@ -1312,10 +1309,6 @@
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "section_break_ahub",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
@@ -1330,7 +1323,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2026-04-28 06:11:46.904768",
"modified": "2026-05-28 12:34:19.659621",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -16,7 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
validate_inter_company_party,
)
from erpnext.accounts.party import get_party_account, get_party_account_currency
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
from erpnext.buying.utils import validate_for_items
from erpnext.controllers.buying_controller import BuyingController
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
validate_against_blanket_order,
@@ -182,6 +182,9 @@ class PurchaseOrder(BuyingController):
"target_ref_field": "stock_qty",
"source_field": "stock_qty",
"percent_join_field": "material_request",
"global_allowance_field": "over_order_allowance",
"global_allowance_doctype": "Buying Settings",
"item_allowance_field": "over_order_allowance",
}
]
@@ -203,7 +206,7 @@ class PurchaseOrder(BuyingController):
self.validate_supplier()
self.validate_schedule_date()
validate_for_items(self)
self.check_on_hold_or_closed_status()
self.check_for_on_hold_or_closed_status("Material Request", "material_request")
self.validate_uom_is_integer("uom", "qty")
self.validate_uom_is_integer("stock_uom", "stock_qty")
@@ -398,18 +401,6 @@ class PurchaseOrder(BuyingController):
d.base_rate
) = d.price_list_rate = d.rate = d.last_purchase_rate = item_last_purchase_rate
# Check for Closed status
def check_on_hold_or_closed_status(self):
check_list = []
for d in self.get("items"):
if (
d.meta.get_field("material_request")
and d.material_request
and d.material_request not in check_list
):
check_list.append(d.material_request)
check_on_hold_or_closed_status("Material Request", d.material_request)
def update_ordered_qty(self, po_item_rows=None):
"""update requested qty (before ordered_qty is updated)"""
item_wh_list = []
@@ -495,7 +486,7 @@ class PurchaseOrder(BuyingController):
self.update_receiving_percentage()
self.update_reserved_qty_for_subcontract()
self.check_on_hold_or_closed_status()
self.check_for_on_hold_or_closed_status("Material Request", "material_request")
self.db_set("status", "Cancelled")

View File

@@ -128,6 +128,44 @@ class TestPurchaseOrder(ERPNextTestSuite):
frappe.db.set_value("Item", "_Test Item", "over_billing_allowance", 0)
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 0)
def test_over_order_allowance_against_material_request(self) -> None:
"""Over Order Allowance in Buying Settings must govern PO qty vs MR qty independently
from Over Delivery/Receipt Allowance which governs receipt/delivery against a PO."""
mr = make_material_request(qty=100)
po = make_purchase_order(mr.name)
po.supplier = "_Test Supplier"
po.items[0].qty = 110 # 10% over the MR qty
# Without any allowance, submitting should raise an OverAllowanceError
from erpnext.controllers.status_updater import OverAllowanceError
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 0)
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
self.assertRaises(OverAllowanceError, po.submit)
# Granting 10% in Over Order Allowance (Buying Settings) must allow the submit
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10)
po.reload()
po.items[0].qty = 110
po.submit()
self.assertEqual(po.docstatus, 1)
po.cancel()
# Over Delivery/Receipt Allowance must remain independent — changing it must not
# affect the MR → PO validation when Over Order Allowance is 0.
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 0)
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50)
mr2 = make_material_request(qty=100)
po2 = make_purchase_order(mr2.name)
po2.supplier = "_Test Supplier"
po2.items[0].qty = 110
self.assertRaises(OverAllowanceError, po2.submit)
# cleanup
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 0)
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
def test_update_remove_child_linked_to_mr(self):
"""Test impact on linked PO and MR on deleting/updating row."""
mr = make_material_request(qty=10)

View File

@@ -16,8 +16,6 @@
"status",
"has_unit_price_items",
"amended_from",
"section_break_mhyw",
"title",
"suppliers_section",
"suppliers",
"items_section",
@@ -44,6 +42,7 @@
"letter_head",
"more_info",
"opportunity",
"title",
"address_and_contact_tab",
"billing_address",
"billing_address_display",
@@ -374,10 +373,6 @@
"label": "Shipping Address Details",
"read_only": 1
},
{
"fieldname": "section_break_mhyw",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "title",
@@ -392,7 +387,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-04-28 06:18:05.661710",
"modified": "2026-05-28 12:28:46.606963",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",

View File

@@ -6,6 +6,7 @@ import json
import frappe
from frappe import _
from frappe.contacts.doctype.contact.contact import get_full_name
from frappe.core.doctype.communication.email import make
from frappe.desk.form.load import get_attachments
from frappe.model.mapper import get_mapped_doc
@@ -275,12 +276,20 @@ class RequestforQuotation(BuyingController):
supplier_doc.save()
def create_user(self, rfq_supplier, link):
contact_name = None
if rfq_supplier.contact:
name_fields = frappe.get_value(
"Contact", rfq_supplier.contact, ["first_name", "middle_name", "last_name"]
)
if name_fields:
contact_name = get_full_name(*name_fields)
user = frappe.get_doc(
{
"doctype": "User",
"send_welcome_email": 0,
"email": rfq_supplier.email_id,
"first_name": rfq_supplier.supplier_name or rfq_supplier.supplier,
"first_name": contact_name or rfq_supplier.supplier_name or rfq_supplier.supplier,
"user_type": "Website User",
"redirect_url": link,
}

View File

@@ -368,19 +368,22 @@
"fieldname": "supplier_primary_contact",
"fieldtype": "Link",
"label": "Supplier Primary Contact",
"no_copy": 1,
"options": "Contact"
},
{
"fetch_from": "supplier_primary_contact.mobile_no",
"fieldname": "mobile_no",
"fieldtype": "Read Only",
"label": "Mobile No"
"label": "Mobile No",
"no_copy": 1
},
{
"fetch_from": "supplier_primary_contact.email_id",
"fieldname": "email_id",
"fieldtype": "Read Only",
"label": "Email Id"
"label": "Email Id",
"no_copy": 1
},
{
"fieldname": "column_break_44",
@@ -390,6 +393,7 @@
"fieldname": "primary_address",
"fieldtype": "Text Editor",
"label": "Primary Address",
"no_copy": 1,
"read_only": 1
},
{
@@ -397,6 +401,7 @@
"fieldname": "supplier_primary_address",
"fieldtype": "Link",
"label": "Supplier Primary Address",
"no_copy": 1,
"options": "Address"
},
{
@@ -517,7 +522,7 @@
"link_fieldname": "party"
}
],
"modified": "2026-03-09 17:15:25.465759",
"modified": "2026-05-29 16:52:59.441272",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@@ -20,8 +20,6 @@
"quotation_number",
"has_unit_price_items",
"amended_from",
"section_break_kumc",
"title",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
@@ -118,6 +116,7 @@
"more_info",
"is_subcontracted",
"column_break_57",
"title",
"opportunity",
"connections_tab"
],
@@ -940,10 +939,6 @@
"fieldname": "auto_repeat_section",
"fieldtype": "Section Break",
"label": "Auto Repeat"
},
{
"fieldname": "section_break_kumc",
"fieldtype": "Section Break"
}
],
"grid_page_length": 50,
@@ -952,7 +947,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-04-28 06:23:52.813948",
"modified": "2026-05-28 12:29:37.509487",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",

View File

@@ -201,6 +201,7 @@ def refresh_scorecards():
def make_all_scorecards(docname):
sc = frappe.get_doc("Supplier Scorecard", docname)
supplier = frappe.get_doc("Supplier", sc.supplier)
supplier.check_permission("write")
start_date = getdate(supplier.creation)
end_date = get_scorecard_date(sc.period, start_date)

View File

@@ -297,7 +297,8 @@ def get_message():
@frappe.whitelist()
def set_default_supplier(item_code, supplier, company):
def set_default_supplier(item_code: str, supplier: str, company: str):
frappe.has_permission("Item", "write", doc=item_code, throw=True)
frappe.db.set_value(
"Item Default",
{"parent": item_code, "company": company},

View File

@@ -113,7 +113,14 @@ def check_on_hold_or_closed_status(doctype, docname) -> None:
status = frappe.db.get_value(doctype, docname, "status")
if status in ("Closed", "On Hold"):
frappe.throw(_("{0} {1} status is {2}").format(doctype, docname, status), frappe.InvalidStatusError)
frappe.throw(
_("{0} {1} status is {2}.").format(
frappe.bold(_(doctype)),
frappe.bold(docname),
frappe.bold(_(status)),
),
frappe.InvalidStatusError,
)
@frappe.whitelist()

View File

@@ -8,7 +8,7 @@ from collections import defaultdict
import frappe
from frappe import _, bold, qb, throw
from frappe.contacts.doctype.address.address import get_address_display
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
from frappe.model.workflow import get_workflow_name
from frappe.query_builder import Criterion, DocType
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Abs, Sum
@@ -182,7 +182,7 @@ class AccountsController(TransactionBase):
if not get_meta(self.doctype).has_field("outstanding_amount"):
return
if self.get("is_return") and self.return_against and not self.get("is_pos"):
if self.get("is_return") and self.return_against and not (self.get("is_pos") or self.get("is_paid")):
against_voucher_outstanding = frappe.get_value(
self.doctype, self.return_against, "outstanding_amount"
)
@@ -2686,7 +2686,7 @@ class AccountsController(TransactionBase):
payment_schedule["credit_days"] = cint(schedule.credit_days)
payment_schedule["credit_months"] = cint(schedule.credit_months)
if schedule.discount_validity_based_on:
if schedule.discount_validity_based_on and flt(schedule.discount):
payment_schedule["discount_date"] = get_discount_date(schedule, posting_date)
payment_schedule["discount_validity_based_on"] = schedule.discount_validity_based_on
payment_schedule["discount_validity"] = cint(schedule.discount_validity)
@@ -2728,6 +2728,8 @@ class AccountsController(TransactionBase):
return
for d in self.get("payment_schedule"):
if not flt(d.discount):
d.discount_date = None
d.validate_from_to_dates("discount_date", "due_date")
if self.doctype in ["Sales Order", "Quotation"] and getdate(d.due_date) < getdate(
self.transaction_date
@@ -2899,7 +2901,9 @@ class AccountsController(TransactionBase):
advance_entry.party_type = primary_party_type
advance_entry.party = primary_party
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
advance_entry.is_advance = "Yes"
# For returns the direction is reversed, so this entry cannot be an advance
# (JE validation: Supplier advance must be debit, Customer advance must be credit)
advance_entry.is_advance = "No" if self.is_return else "Yes"
# Update dimensions
dimensions_dict = frappe._dict()
@@ -2931,35 +2935,26 @@ class AccountsController(TransactionBase):
)
)
# Convert outstanding amount from secondary to primary account currency, if needed
outstanding_amount = abs(self.outstanding_amount)
os_in_default_currency = outstanding_amount * exc_rate_secondary_to_default
os_in_primary_currency = outstanding_amount * exc_rate_secondary_to_primary
os_in_default_currency = self.outstanding_amount * exc_rate_secondary_to_default
os_in_primary_currency = self.outstanding_amount * exc_rate_secondary_to_primary
# SI normal and PI return → reconciliation is credit; SI return and PI normal → debit
reconciliation_is_credit = (self.doctype == "Sales Invoice") != bool(self.is_return)
_set_je_amounts(
reconcilation_entry, outstanding_amount, os_in_default_currency, reconciliation_is_credit
)
_set_je_amounts(
advance_entry, os_in_primary_currency, os_in_default_currency, not reconciliation_is_credit
)
if self.doctype == "Sales Invoice":
# Calculate credit and debit values for reconciliation and advance entries
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
reconcilation_entry.credit = os_in_default_currency
advance_entry.debit_in_account_currency = os_in_primary_currency
advance_entry.debit = os_in_default_currency
else:
advance_entry.credit_in_account_currency = os_in_primary_currency
advance_entry.credit = os_in_default_currency
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
reconcilation_entry.debit = os_in_default_currency
# Set exchange rates for entries
reconcilation_entry.exchange_rate = exc_rate_secondary_to_default
advance_entry.exchange_rate = exc_rate_primary_to_default
else:
if self.doctype == "Sales Invoice":
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
advance_entry.debit_in_account_currency = self.outstanding_amount
else:
advance_entry.credit_in_account_currency = self.outstanding_amount
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
outstanding_amount = abs(self.outstanding_amount)
reconciliation_is_credit = (self.doctype == "Sales Invoice") != bool(self.is_return)
_set_je_amounts(reconcilation_entry, outstanding_amount, is_credit=reconciliation_is_credit)
_set_je_amounts(advance_entry, outstanding_amount, is_credit=not reconciliation_is_credit)
jv.multi_currency = multi_currency
jv.append("accounts", reconcilation_entry)
@@ -3613,12 +3608,11 @@ def get_payment_term_details(
term_details.outstanding = term_details.payment_amount
term_details.base_outstanding = term_details.base_payment_amount
if bill_date:
term_details.due_date = get_due_date(term, bill_date)
term_details.discount_date = get_discount_date(term, bill_date)
elif posting_date:
term_details.due_date = get_due_date(term, posting_date)
term_details.discount_date = get_discount_date(term, posting_date)
has_discount = flt(term.get("discount"))
date = bill_date or posting_date
if date:
term_details.due_date = get_due_date(term, date)
term_details.discount_date = get_discount_date(term, date) if has_discount else None
if getdate(term_details.due_date) < getdate(posting_date):
term_details.due_date = posting_date
@@ -3688,6 +3682,17 @@ def set_child_tax_template_and_map(item, child_item, parent_doc):
)
def _set_je_amounts(entry, amount, default_amount=None, is_credit=True):
if is_credit:
entry.credit_in_account_currency = amount
if default_amount is not None:
entry.credit = default_amount
else:
entry.debit_in_account_currency = amount
if default_amount is not None:
entry.debit = default_amount
def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True):
add_taxes_from_item_tax_template = frappe.get_single_value(
"Accounts Settings", "add_taxes_from_item_tax_template"
@@ -3848,7 +3853,9 @@ def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
@frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
def update_child_qty_rate(
parent_doctype: str, trans_items: str, parent_doctype_name: str, child_docname: str = "items"
):
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items
from erpnext.selling.doctype.quotation.quotation import get_ordered_items
@@ -3874,14 +3881,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
current_state = doc.get(workflow_doc.workflow_state_field)
roles = frappe.get_roles()
transitions = []
for transition in workflow_doc.transitions:
if transition.next_state == current_state and transition.allowed in roles:
if not is_transition_condition_satisfied(transition, doc):
continue
transitions.append(transition.as_dict())
allowed = any(
state.state == current_state and (not state.allow_edit or state.allow_edit in roles)
for state in workflow_doc.states
)
if not transitions:
if not allowed:
frappe.throw(
_("You are not allowed to update as per the conditions set in {} Workflow.").format(
get_link_to_form("Workflow", workflow)

View File

@@ -727,19 +727,6 @@ class BuyingController(SubcontractingController):
)
)
def check_for_on_hold_or_closed_status(self, ref_doctype, ref_fieldname):
for d in self.get("items"):
if d.get(ref_fieldname):
status = frappe.db.get_value(ref_doctype, d.get(ref_fieldname), "status")
if status in ("Closed", "On Hold"):
frappe.throw(
_("{ref_doctype} {ref_name} is {status}.").format(
ref_doctype=frappe.bold(_(ref_doctype)),
ref_name=frappe.bold(d.get(ref_fieldname)),
status=frappe.bold(_(status)),
)
)
def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False):
self.update_ordered_and_reserved_qty()

View File

@@ -390,10 +390,15 @@ def get_delivery_notes_to_be_billed(
.where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
)
query = frappe.qb.get_query(
"Delivery Note",
fields=fields,
filters=filters,
ignore_permissions=False,
)
query = (
frappe.qb.from_(DeliveryNote)
.select(*[DeliveryNote[f] for f in fields])
.where(
query.where(
(DeliveryNote.docstatus == 1)
& (DeliveryNote.status.notin(["Stopped", "Closed"]))
& (DeliveryNote[searchfield].like(f"%{txt}%"))
@@ -407,12 +412,11 @@ def get_delivery_notes_to_be_billed(
)
)
)
.orderby(DeliveryNote[searchfield], order=Order.asc)
.limit(page_len)
.offset(start)
)
if filters and isinstance(filters, dict):
for key, value in filters.items():
query = query.where(DeliveryNote[key] == value)
query = query.orderby(DeliveryNote[searchfield], order=Order.asc).limit(page_len).offset(start)
return query.run(as_dict=as_dict)

View File

@@ -70,6 +70,7 @@ class SellingController(StockController):
self.validate_for_duplicate_items()
self.validate_target_warehouse()
self.validate_auto_repeat_subscription_dates()
self.validate_sample_retention_warehouse()
for table_field in ["items", "packed_items"]:
if self.get(table_field):
self.set_serial_and_batch_bundle(table_field)
@@ -469,11 +470,9 @@ class SellingController(StockController):
return so_qty, so_warehouse
def check_sales_order_on_hold_or_close(self, ref_fieldname):
for d in self.get("items"):
if d.get(ref_fieldname):
status = frappe.db.get_value("Sales Order", d.get(ref_fieldname), "status")
if status in ("Closed", "On Hold") and not self.is_return:
frappe.throw(_("Sales Order {0} is {1}").format(d.get(ref_fieldname), status))
if self.is_return:
return
self.check_for_on_hold_or_closed_status("Sales Order", ref_fieldname)
def update_reserved_qty(self):
so_map = {}
@@ -893,6 +892,26 @@ class SellingController(StockController):
validate_item_type(self, "is_sales_item", "sales")
def validate_sample_retention_warehouse(self):
if self.get("is_return"):
return
sample_retention_warehouse = frappe.db.get_single_value(
"Stock Settings", "sample_retention_warehouse"
)
if not sample_retention_warehouse:
return
items = self.get("items") + (self.get("packed_items"))
for item in items:
if item.get("warehouse") == sample_retention_warehouse:
frappe.throw(
_("Row {0}: Cannot sell item {1} from Sample Retention Warehouse {2}").format(
item.idx, frappe.bold(item.item_code), frappe.bold(sample_retention_warehouse)
),
title=_("Not Allowed"),
)
def update_stock_reservation_entries(self) -> None:
"""Updates Delivered Qty in Stock Reservation Entries."""
@@ -1041,6 +1060,44 @@ class SellingController(StockController):
qty_to_undelivered -= qty_can_be_undelivered
def set_serial_and_batch_bundle_from_pick_list(self):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
for item in self.items:
if item.use_serial_batch_fields or not item.against_pick_list or not self.get("update_stock", 1):
continue
if item.pick_list_item and not item.serial_and_batch_bundle:
filters = {
"item_code": item.item_code,
"voucher_type": "Pick List",
"voucher_no": item.against_pick_list,
"voucher_detail_no": item.pick_list_item,
}
bundle_id = frappe.db.get_value("Serial and Batch Bundle", filters, "name")
if bundle_id:
cls_obj = SerialBatchCreation(
{
"type_of_transaction": "Outward",
"serial_and_batch_bundle": bundle_id,
"item_code": item.get("item_code"),
"warehouse": item.get("warehouse"),
}
)
cls_obj.duplicate_package()
item.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
def update_pick_list_status(self):
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
pick_lists = {row.against_pick_list for row in self.items if row.against_pick_list}
for pick_list in pick_lists:
update_pick_list_status(pick_list)
def set_default_income_account_for_item(obj):
"""Set income account as default for items in the transaction.

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