Compare commits

..

269 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
Frappe PR Bot
9af618d6bf chore(release): Bumped to Version 16.20.1
## [16.20.1](https://github.com/frappe/erpnext/compare/v16.20.0...v16.20.1) (2026-06-01)

### Bug Fixes

* billing address does not belongs to the company error ([d6b7791](d6b7791f18))
2026-06-01 06:08:21 +00:00
rohitwaghchaure
3c3cde4362 Merge pull request #55475 from frappe/mergify/bp/version-16/pr-55425
fix: billing address does not belongs to the company error (backport #55417) (backport #55425)
2026-06-01 11:36:44 +05:30
Rohit Waghchaure
d6b7791f18 fix: billing address does not belongs to the company error
(cherry picked from commit 9df07b367a)
(cherry picked from commit e1f29de078)
2026-06-01 06:02:05 +00:00
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
Frappe PR Bot
ff46d20b25 chore(release): Bumped to Version 16.20.0
# [16.20.0](https://github.com/frappe/erpnext/compare/v16.19.1...v16.20.0) (2026-05-27)

### Bug Fixes

* consider batchwise valuation in stock ageing report (backport [#54919](https://github.com/frappe/erpnext/issues/54919)) ([#55230](https://github.com/frappe/erpnext/issues/55230)) ([a1457c7](a1457c759d))
* consumed operation cost calculation (backport [#54858](https://github.com/frappe/erpnext/issues/54858)) ([#55133](https://github.com/frappe/erpnext/issues/55133)) ([dfc9144](dfc91441b4))
* correct description for Is Rate Adjustment Entry (Debit Note) checkbox ([a39733d](a39733ddd0))
* correct remarks for foreign currency payment entries ([f3f8f32](f3f8f327df))
* corrected the pricing rule taking the wrong value (backport [#54894](https://github.com/frappe/erpnext/issues/54894)) ([#55124](https://github.com/frappe/erpnext/issues/55124)) ([1a75c14](1a75c14308))
* default use_for_shopping_cart to 0 in set_taxes ([54be4ee](54be4ee275))
* don't reset net_purchase_amount for Composite Asset if already set ([99642b9](99642b9636))
* edit stock uom qty for purchase documents (backport [#55135](https://github.com/frappe/erpnext/issues/55135)) ([#55179](https://github.com/frappe/erpnext/issues/55179)) ([123b4ad](123b4ad563))
* **employee:** js error if user does not have write permission for date field (backport [#55312](https://github.com/frappe/erpnext/issues/55312)) ([#55314](https://github.com/frappe/erpnext/issues/55314)) ([4dff5a7](4dff5a7820))
* faster range calculation on process period closing voucher ([e56ee38](e56ee383bc))
* fg valuation rate in repack entry when multiple FGs ([7b6adce](7b6adce89a))
* inclusive tax amount not considered while setting LCV from purchase invoice ([bd4c244](bd4c24493c))
* incorrect error message string in sales order (backport [#55090](https://github.com/frappe/erpnext/issues/55090)) ([#55095](https://github.com/frappe/erpnext/issues/55095)) ([17bc2b6](17bc2b691f))
* invalid filter on item_group (backport [#55186](https://github.com/frappe/erpnext/issues/55186)) ([#55188](https://github.com/frappe/erpnext/issues/55188)) ([ea86347](ea863477a4))
* item price with party condition (backport [#55100](https://github.com/frappe/erpnext/issues/55100)) ([#55107](https://github.com/frappe/erpnext/issues/55107)) ([cc438a4](cc438a4600))
* job card buttons color (backport [#55252](https://github.com/frappe/erpnext/issues/55252)) ([#55261](https://github.com/frappe/erpnext/issues/55261)) ([69c6ed3](69c6ed3cd9))
* **manufacturing:** fetch from_bom name in production plan (backport [#55085](https://github.com/frappe/erpnext/issues/55085)) ([#55092](https://github.com/frappe/erpnext/issues/55092)) ([36aca51](36aca51fbb))
* **manufacturing:** remove forecast_qty and adjust_qty fields from sa… (backport [#55129](https://github.com/frappe/erpnext/issues/55129)) ([#55136](https://github.com/frappe/erpnext/issues/55136)) ([bde7f16](bde7f1660e))
* **payment_entry:** sync paid/received amounts for cross-currency entries (backport [#55270](https://github.com/frappe/erpnext/issues/55270)) ([#55272](https://github.com/frappe/erpnext/issues/55272)) ([705814f](705814f066))
* pos profile form cleanup (backport [#52436](https://github.com/frappe/erpnext/issues/52436)) ([#55285](https://github.com/frappe/erpnext/issues/55285)) ([1f14ef2](1f14ef2344))
* prevent AttributeError in batch query filters (backport [#55257](https://github.com/frappe/erpnext/issues/55257)) ([#55279](https://github.com/frappe/erpnext/issues/55279)) ([bfd37dc](bfd37dcc21))
* **project:** update customer and sales order as no copy ([1e61ca1](1e61ca162f))
* removed redundant code ([a7eb3ac](a7eb3acd1a))
* **sales_invoice:** skip stock update for POS invoices linked to Delivery Note (backport [#55311](https://github.com/frappe/erpnext/issues/55311)) ([#55313](https://github.com/frappe/erpnext/issues/55313)) ([cd7e1bb](cd7e1bbff1))
* set bin details when adding item using update items (backport [#55096](https://github.com/frappe/erpnext/issues/55096)) ([#55098](https://github.com/frappe/erpnext/issues/55098)) ([bb87ffc](bb87ffc90a))
* single variant creation error ([82b0372](82b0372d5b))
* slow query ([66c9170](66c9170465))
* **stock:** apply posting datetime filters while fetching available batches (backport [#54976](https://github.com/frappe/erpnext/issues/54976)) ([#55185](https://github.com/frappe/erpnext/issues/55185)) ([edf6bea](edf6bea2ee))
* **stock:** remove precision for valuation rate while creating sle (backport [#55249](https://github.com/frappe/erpnext/issues/55249)) ([#55260](https://github.com/frappe/erpnext/issues/55260)) ([9600ecd](9600ecd61c))
* **stock:** remove recalculate current qty function (backport [#54774](https://github.com/frappe/erpnext/issues/54774)) ([#55075](https://github.com/frappe/erpnext/issues/55075)) ([56a9b37](56a9b37fac))
* use passed posting date in make_reverse_gl_entries ([3ce9cf2](3ce9cf2bd8))
* valuation rate missing for standalone credit notes for moving av… (backport [#55102](https://github.com/frappe/erpnext/issues/55102)) ([#55104](https://github.com/frappe/erpnext/issues/55104)) ([b11365b](b11365b8c2))

### Features

* add get_parent_supplier_groups using query builder ([82793cb](82793cbd4d))
* add party groups functionality to party specific item (backport [#54988](https://github.com/frappe/erpnext/issues/54988)) ([#55245](https://github.com/frappe/erpnext/issues/55245)) ([a618f4c](a618f4cca4))
* allow creation of any number of variants in multiple item variant creation dialog ([27db98d](27db98d222))
* pending qty in job card ([b372e6f](b372e6f118))

### Performance Improvements

* skip delink_original_entry during cancellation when Immutable Ledger is enabled ([#55130](https://github.com/frappe/erpnext/issues/55130)) ([8a4cb28](8a4cb28d90))
* skip delink_original_entry during cancellation when Immutable Ledger is enabled (backport [#55130](https://github.com/frappe/erpnext/issues/55130)) ([#55166](https://github.com/frappe/erpnext/issues/55166)) ([92689e0](92689e05da))
2026-05-27 01:22:47 +00:00
mergify[bot]
d215fa7623 chore: remove frappe-semgrep-rules submodule (backport #55083) (#55319)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2026-05-27 01:21:08 +00:00
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
Diptanil Saha
bf8f7ba883 Merge pull request #55307 from frappe/version-16-hotfix
chore: release v16
2026-05-27 05:33:15 +05:30
mergify[bot]
cd7e1bbff1 fix(sales_invoice): skip stock update for POS invoices linked to Delivery Note (backport #55311) (#55313)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix(sales_invoice): skip stock update for POS invoices linked to Delivery Note (#55311)
2026-05-26 20:32:53 +00:00
mergify[bot]
4dff5a7820 fix(employee): js error if user does not have write permission for date field (backport #55312) (#55314)
Co-authored-by: Himanshu Jain <118419692+trufurs@users.noreply.github.com>
fix(employee): js error if user does not have write permission for date field (#55312)
2026-05-27 01:58:49 +05:30
rohitwaghchaure
1781893c3c Merge pull request #55297 from frappe/mergify/bp/version-16-hotfix/pr-55290
fix: inclusive tax amount not considered while setting LCV from purchase invoice (backport #55290)
2026-05-26 16:13:51 +05:30
Nihantra C. Patel
304de47b48 Merge pull request #55295 from frappe/mergify/bp/version-16-hotfix/pr-55268
fix: use passed posting date for period closing validation in reverse GL entries (backport #55268)
2026-05-26 16:06:56 +05:30
Rohit Waghchaure
67c922cdf3 fix: stock reco for legacy serial nos
(cherry picked from commit 9d5fd11bcd)
2026-05-26 10:29:25 +00:00
Rohit Waghchaure
bd4c24493c fix: inclusive tax amount not considered while setting LCV from purchase invoice
(cherry picked from commit 048ddfc265)
2026-05-26 10:15:26 +00:00
Nihantra Patel
6925b6b645 test: update testcase
(cherry picked from commit 9c39b01f1c)
2026-05-26 10:14:00 +00:00
Nihantra Patel
3ce9cf2bd8 fix: use passed posting date in make_reverse_gl_entries
(cherry picked from commit f040bdf165)
2026-05-26 10:14:00 +00:00
Mihir Kandoi
0ede7759df Merge pull request #55289 from frappe/mergify/bp/version-16-hotfix/pr-55286
fix: single variant creation error (backport #55286)
2026-05-26 13:57:01 +05:30
diptanilsaha
1f14ef2344 fix: pos profile form cleanup (backport #52436) (#55285)
fix: pos profile form cleanup (#52436)
2026-05-26 13:49:39 +05:30
Mihir Kandoi
27db98d222 feat: allow creation of any number of variants in multiple item variant creation dialog
(cherry picked from commit 090c25d848)
2026-05-26 08:05:44 +00:00
Mihir Kandoi
82b0372d5b fix: single variant creation error
(cherry picked from commit bda75135c3)
2026-05-26 08:05:43 +00:00
mergify[bot]
69c6ed3cd9 fix: job card buttons color (backport #55252) (#55261)
Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2026-05-26 13:16:41 +05:30
mergify[bot]
ca6bcb57d3 refactor: remove unused customer field in Item DocType (backport #55277) (#55283)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-26 05:47:16 +00:00
ruthra kumar
03d96c7b85 Merge pull request #55281 from frappe/mergify/bp/version-16-hotfix/pr-55256
refactor: handle processes stuck in running state in process pcv (backport #55256)
2026-05-26 10:56:24 +05:30
mergify[bot]
bfd37dcc21 fix: prevent AttributeError in batch query filters (backport #55257) (#55279)
Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
fix: prevent AttributeError in batch query filters (#55257)
2026-05-26 05:20:04 +00:00
mergify[bot]
9600ecd61c fix(stock): remove precision for valuation rate while creating sle (backport #55249) (#55260)
Co-authored-by: Sudharsanan11 <sudharsananashok1975@gmail.com>
2026-05-26 10:29:39 +05:30
ruthra kumar
5dc37b1130 refactor: atomic summarization step for process pcv
(cherry picked from commit 6cb7971342)
2026-05-26 04:58:22 +00:00
ruthra kumar
a19611a2e9 refactor: handle processes stuck in running state in process pcv
(cherry picked from commit f414778486)
2026-05-26 04:58:22 +00:00
ruthra kumar
6755101654 refactor: summarize in background
(cherry picked from commit 1c3a9f7dd9)
2026-05-26 04:58:21 +00:00
mergify[bot]
705814f066 fix(payment_entry): sync paid/received amounts for cross-currency entries (backport #55270) (#55272)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
fix(payment_entry): sync paid/received amounts for cross-currency entries (#55270)
2026-05-25 23:22:02 +05:30
mergify[bot]
1a75c14308 fix: corrected the pricing rule taking the wrong value (backport #54894) (#55124)
fix: corrected the pricing rule taking the wrong value (#54894)

(cherry picked from commit 06477119d1)

Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
2026-05-25 16:02:26 +05:30
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
rohitwaghchaure
56d99152f9 Merge pull request #55244 from frappe/mergify/bp/version-16-hotfix/pr-55216
fix: fg valuation rate in repack entry when multiple FGs (backport #55216)
2026-05-25 15:34:16 +05:30
mergify[bot]
a618f4cca4 feat: add party groups functionality to party specific item (backport #54988) (#55245)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-25 12:38:44 +05:30
Rohit Waghchaure
7b6adce89a fix: fg valuation rate in repack entry when multiple FGs
(cherry picked from commit a47e4c04f7)
2026-05-25 06:15:29 +00:00
mergify[bot]
7752f703d2 refactor: stock ageing report (backport #55231) (#55237)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-24 18:30:45 +05:30
MochaMind
7159b13ee2 chore: update POT file (#55234) 2026-05-24 14:48:11 +02:00
mergify[bot]
ea60efd91a refactor: use frappe.db.bulk_update instead of Case queries in subcon… (backport #55232) (#55233)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-24 09:56:53 +00:00
mergify[bot]
a1457c759d fix: consider batchwise valuation in stock ageing report (backport #54919) (#55230)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: consider batchwise valuation in stock ageing report (#54919)
2026-05-24 07:18:07 +00:00
rohitwaghchaure
ab9e30b487 Merge pull request #55183 from frappe/mergify/bp/version-16-hotfix/pr-55091
feat: pending qty in job card (backport #55091)
2026-05-24 09:30:01 +05:30
Nishka Gosalia
9596e6e1e9 Merge pull request #55196 from frappe/mergify/bp/version-16-hotfix/pr-55189
fix(project): update customer and sales order as no copy (backport #55189)
2026-05-23 15:32:06 +05:30
nareshkannasln
1e61ca162f fix(project): update customer and sales order as no copy
(cherry picked from commit 9d8f3863f2)
2026-05-22 12:21:34 +00:00
mergify[bot]
edf6bea2ee fix(stock): apply posting datetime filters while fetching available batches (backport #54976) (#55185)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(stock): apply posting datetime filters while fetching available batches (#54976)
2026-05-22 11:23:24 +00:00
mergify[bot]
ea863477a4 fix: invalid filter on item_group (backport #55186) (#55188)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: invalid filter on item_group (#55186)
2026-05-22 16:49:23 +05:30
rohitwaghchaure
171cd41928 chore: fix conflicts 2026-05-22 15:25:34 +05:30
Rohit Waghchaure
d2a793b03b refactor: better timer and complete button
(cherry picked from commit 1be92f6d05)

# Conflicts:
#	erpnext/manufacturing/doctype/job_card/job_card.js
2026-05-22 09:52:50 +00:00
Rohit Waghchaure
3081368aad refactor: job_card.js code for better readability
(cherry picked from commit 0a215b0717)

# Conflicts:
#	erpnext/manufacturing/doctype/job_card/job_card.js
2026-05-22 09:52:49 +00:00
Rohit Waghchaure
b372e6f118 feat: pending qty in job card
(cherry picked from commit db64f451c1)
2026-05-22 09:52:49 +00:00
rohitwaghchaure
e2558b6e51 Merge pull request #55182 from frappe/mergify/bp/version-16-hotfix/pr-55159
fix: slow query (backport #55159)
2026-05-22 15:22:18 +05:30
Rohit Waghchaure
66c9170465 fix: slow query
(cherry picked from commit d44f574581)
2026-05-22 09:18:04 +00:00
mergify[bot]
123b4ad563 fix: edit stock uom qty for purchase documents (backport #55135) (#55179)
Co-authored-by: Nishka Gosalia <58264710+nishkagosalia@users.noreply.github.com>
fix: edit stock uom qty for purchase documents (#55135)
2026-05-22 09:14:32 +00:00
ruthra kumar
92689e05da perf: skip delink_original_entry during cancellation when Immutable Ledger is enabled (backport #55130) (#55166)
perf: skip delink_original_entry during cancellation when Immutable Ledger is enabled (#55130)

* perf: get payment ledger and remove update from delink when immutable ledger is enabled

* revert: changes of get_payment_ledger_entries

* perf: skip delink_original_entry during cancellation when Immutable Ledger is enabled

* test: for immutable ledger

* test: add posting_date in create_sales_invoice

* fix: link validation err with immutable ledger on

* test: update testcase of the immutable ledger

* refactor(test): simpler test for immutable invariants

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
(cherry picked from commit 9eeccecd30)

# Conflicts:
#	erpnext/accounts/general_ledger.py

Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com>
2026-05-22 14:25:32 +05:30
mergify[bot]
bde7f1660e fix(manufacturing): remove forecast_qty and adjust_qty fields from sa… (backport #55129) (#55136)
Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
fix(manufacturing): remove forecast_qty and adjust_qty fields from sa… (#55129)
2026-05-22 14:23:51 +05:30
Nihantra C. Patel
8a4cb28d90 perf: skip delink_original_entry during cancellation when Immutable Ledger is enabled (#55130)
* perf: get payment ledger and remove update from delink when immutable ledger is enabled

* revert: changes of get_payment_ledger_entries

* perf: skip delink_original_entry during cancellation when Immutable Ledger is enabled

* test: for immutable ledger

* test: add posting_date in create_sales_invoice

* fix: link validation err with immutable ledger on

* test: update testcase of the immutable ledger

* refactor(test): simpler test for immutable invariants

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
(cherry picked from commit 9eeccecd30)

# Conflicts:
#	erpnext/accounts/general_ledger.py
2026-05-22 12:49:43 +05:30
diptanilsaha
498bd2fb4b Merge pull request #55144 from frappe/mergify/bp/version-16-hotfix/pr-55127
refactor: migrate get_tax_template to query builder with hierarchical group matching (backport #55127)
2026-05-22 01:48:45 +05:30
Khushi Rawat
bfb7a0e941 Merge pull request #55152 from frappe/mergify/bp/version-16-hotfix/pr-55146
fix: correct remarks for foreign currency payment entries (backport #55146)
2026-05-21 20:33:30 +05:30
khushi8112
f3f8f327df fix: correct remarks for foreign currency payment entries
(cherry picked from commit c6cde700b5)
2026-05-21 14:42:44 +00:00
Khushi Rawat
0d816010dd Merge pull request #55148 from frappe/mergify/bp/version-16-hotfix/pr-55147
fix: correct description for Is Rate Adjustment Entry (Debit Note) checkbox (backport #55147)
2026-05-21 20:08:20 +05:30
khushi8112
a39733ddd0 fix: correct description for Is Rate Adjustment Entry (Debit Note) checkbox
(cherry picked from commit 92c969478e)
2026-05-21 12:38:23 +00:00
Khushi Rawat
5c9149c5a5 Merge pull request #55145 from frappe/mergify/bp/version-16-hotfix/pr-55142
fix: don't reset net_purchase_amount for Composite Asset if already set (backport #55142)
2026-05-21 17:11:24 +05:30
khushi8112
99642b9636 fix: don't reset net_purchase_amount for Composite Asset if already set
(cherry picked from commit 98dae6e43a)
2026-05-21 11:38:28 +00:00
diptanilsaha
a63b344a0a test: add tests for supplier group hierarchy and use_for_shopping_cart filter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 8c43118725)
2026-05-21 11:36:30 +00:00
diptanilsaha
54be4ee275 fix: default use_for_shopping_cart to 0 in set_taxes
Ensures regular transactions only match tax rules where
use_for_shopping_cart = 0, preventing webshop-specific rules
from applying to standard documents.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 4d43c74f5f)
2026-05-21 11:36:30 +00:00
diptanilsaha
c45d2a3487 refactor: rewrite get_tax_template using query builder
Migrates from raw frappe.db.sql with string interpolation to frappe.qb.
Adds hierarchical supplier_group matching (mirrors customer_group behaviour).
Removes unused get_customer_group_condition helper.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit f98975f51a)
2026-05-21 11:36:30 +00:00
diptanilsaha
82793cbd4d feat: add get_parent_supplier_groups using query builder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit cb610b79d2)
2026-05-21 11:36:29 +00:00
diptanilsaha
620161c526 refactor: migrate get_parent_customer_groups to query builder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
(cherry picked from commit 91a2a7b0a0)
2026-05-21 11:36:29 +00:00
mergify[bot]
dfc91441b4 fix: consumed operation cost calculation (backport #54858) (#55133)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: consumed operation cost calculation (#54858)
2026-05-21 10:33:43 +00:00
rohitwaghchaure
e630ab64eb Merge pull request #55139 from frappe/mergify/bp/version-16-hotfix/pr-55134
fix: removed redundant code (backport #55134)
2026-05-21 15:56:28 +05:30
Rohit Waghchaure
a7eb3acd1a fix: removed redundant code
(cherry picked from commit 14b17cd8a6)
2026-05-21 09:55:48 +00:00
Mihir Kandoi
02f4d9a4d6 chore: add whitelist (#55113) 2026-05-20 16:33:00 +00:00
mergify[bot]
6d8bbc5b6f chore: migrate Address/Contact custom fields from JSON fixtures to install (backport #55084) (#55088)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
fixtures to install (#55084)
2026-05-20 21:24:40 +05:30
mergify[bot]
cc438a4600 fix: item price with party condition (backport #55100) (#55107)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: item price with party condition (#55100)
2026-05-20 20:24:56 +05:30
mergify[bot]
b11365b8c2 fix: valuation rate missing for standalone credit notes for moving av… (backport #55102) (#55104)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: valuation rate missing for standalone credit notes for moving av… (#55102)
2026-05-20 11:49:54 +00:00
mergify[bot]
bb87ffc90a fix: set bin details when adding item using update items (backport #55096) (#55098)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
fix: set bin details when adding item using update items (#55096)
2026-05-20 16:21:10 +05:30
mergify[bot]
17bc2b691f fix: incorrect error message string in sales order (backport #55090) (#55095)
Co-authored-by: Shllokkk <140623894+Shllokkk@users.noreply.github.com>
fix: incorrect error message string in sales order (#55090)
2026-05-20 09:33:40 +00:00
mergify[bot]
36aca51fbb fix(manufacturing): fetch from_bom name in production plan (backport #55085) (#55092)
Co-authored-by: Pandiyan P <pandiyanpalani37@gmail.com>
fix(manufacturing): fetch from_bom name in production plan (#55085)
2026-05-20 09:19:31 +00:00
mergify[bot]
56a9b37fac fix(stock): remove recalculate current qty function (backport #54774) (#55075)
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
fix(stock): remove recalculate current qty function (#54774)
2026-05-20 14:41:55 +05:30
ruthra kumar
05f641d3bc Merge pull request #55078 from frappe/mergify/bp/version-16-hotfix/pr-55072
perf: faster opening balance range calculation in process period closing voucher (backport #55072)
2026-05-20 12:06:32 +05:30
ruthra kumar
830d035459 refactor: ppcv select with for update and skip locked
(cherry picked from commit eba58b2837)
2026-05-20 06:19:03 +00:00
ruthra kumar
e56ee383bc fix: faster range calculation on process period closing voucher
(cherry picked from commit ee33574a6d)
2026-05-20 06:19:02 +00:00
281 changed files with 471469 additions and 392794 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.19.1"
__version__ = "16.22.0"
def get_default_company(user=None):

View File

@@ -1,126 +0,0 @@
{
"custom_fields": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2018-12-28 22:29:21.828090",
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "Address",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "tax_category",
"fieldtype": "Link",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 15,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "fax",
"label": "Tax Category",
"length": 0,
"mandatory_depends_on": null,
"modified": "2018-12-28 22:29:21.828090",
"modified_by": "Administrator",
"name": "Address-tax_category",
"no_copy": 0,
"options": "Tax Category",
"owner": "Administrator",
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2020-10-14 17:41:40.878179",
"default": "0",
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "Address",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "is_your_company_address",
"fieldtype": "Check",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 20,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "linked_with",
"label": "Is Your Company Address",
"length": 0,
"mandatory_depends_on": null,
"modified": "2020-10-14 17:41:40.878179",
"modified_by": "Administrator",
"name": "Address-is_your_company_address",
"no_copy": 0,
"options": null,
"owner": "Administrator",
"parent": null,
"parentfield": null,
"parenttype": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"translatable": 0,
"unique": 0,
"width": null
}
],
"custom_perms": [],
"doctype": "Address",
"property_setters": [],
"sync_on_migrate": 1
}

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

@@ -807,11 +807,14 @@ frappe.ui.form.on("Payment Entry", {
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (!frm.doc.received_amount) {
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("received_amount", frm.doc.paid_amount);
} else if (company_currency == frm.doc.paid_to_account_currency) {
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
if (company_currency == frm.doc.paid_to_account_currency) {
frm.set_value("received_amount", frm.doc.base_paid_amount);
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
} else if (frm.doc.target_exchange_rate) {
frm.set_value(
"received_amount",
flt(frm.doc.base_paid_amount) / flt(frm.doc.target_exchange_rate)
);
}
}
frm.trigger("reset_received_amount");
@@ -828,15 +831,14 @@ frappe.ui.form.on("Payment Entry", {
);
if (!frm.doc.paid_amount) {
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
frm.set_value("paid_amount", frm.doc.received_amount);
if (frm.doc.target_exchange_rate) {
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
}
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
} else if (company_currency == frm.doc.paid_from_account_currency) {
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
if (company_currency == frm.doc.paid_from_account_currency) {
frm.set_value("paid_amount", frm.doc.base_received_amount);
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
} else if (frm.doc.source_exchange_rate) {
frm.set_value(
"paid_amount",
flt(frm.doc.base_received_amount) / flt(frm.doc.source_exchange_rate)
);
}
}
@@ -1724,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 (
@@ -1238,9 +1253,9 @@ class PaymentEntry(AccountsController):
else:
remarks = [
_("Amount {0} {1} {2} {3}").format(
_(self.paid_to_account_currency)
_(self.paid_from_account_currency)
if self.payment_type == "Receive"
else _(self.paid_from_account_currency),
else _(self.paid_to_account_currency),
self.paid_amount if self.payment_type == "Receive" else self.received_amount,
_("received from") if self.payment_type == "Receive" else _("paid to"),
self.party,
@@ -1256,7 +1271,7 @@ class PaymentEntry(AccountsController):
for d in self.get("references"):
if d.allocated_amount:
remarks.append(
_("Amount {0} {1} against {2} {3}").format(
_("Amount {0} {1} adjusted against {2} {3}").format(
_(self.party_account_currency),
d.allocated_amount,
d.reference_doctype,
@@ -1267,7 +1282,7 @@ class PaymentEntry(AccountsController):
for d in self.get("deductions"):
if d.amount:
remarks.append(
_("Amount {0} {1} deducted against {2}").format(
_("Amount {0} {1} as adjustment to {2}").format(
_(self.company_currency), d.amount, d.account
)
)
@@ -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,8 @@
import frappe
from frappe import qb
from frappe.utils import nowdate
from frappe.query_builder.functions import Count, Sum
from frappe.utils import add_days, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
@@ -90,6 +91,7 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
posting_date = nowdate()
sinv = create_sales_invoice(
posting_date=posting_date,
qty=qty,
rate=rate,
company=self.company,
@@ -531,3 +533,82 @@ class TestPaymentLedgerEntry(ERPNextTestSuite):
# with references removed, deletion should be possible
so.delete()
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, so.doctype, so.name)
@ERPNextTestSuite.change_settings(
"Accounts Settings",
{"enable_immutable_ledger": 1},
)
def test_reverse_entries_on_cancel_for_immutable_ledger(self):
invoice_posting_date = add_days(nowdate(), -5)
gle = qb.DocType("GL Entry")
ple = qb.DocType("Payment Ledger Entry")
si = self.create_sales_invoice(qty=1, rate=100, posting_date=invoice_posting_date)
gles_before = (
qb.from_(gle)
.select(
Count(gle.name),
)
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
.run()[0][0]
)
ples_before = (
qb.from_(ple)
.select(
Count(ple.name),
)
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked.eq(0)))
.run()[0][0]
)
si.cancel()
gles_after = (
qb.from_(gle)
.select(Count(gle.account))
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
.run()[0][0]
)
self.assertEqual(gles_after, gles_before * 2)
ples_after = (
qb.from_(ple)
.select(
Count(ple.name),
)
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked.eq(0)))
.run()[0][0]
)
self.assertEqual(ples_after, ples_before * 2)
# assert debit/credit are reversed
gl_entries = (
qb.from_(gle)
.select(gle.account, Sum(gle.debit).as_("total_debit"), Sum(gle.credit).as_("total_credit"))
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
.groupby(gle.account)
.run(as_dict=True)
)
for gl in gl_entries:
with self.subTest(gl=gl):
self.assertEqual(gl.total_debit, gl.total_credit)
# assert amounts are reversed
pl_entries = (
qb.from_(ple)
.select(ple.account, Sum(ple.amount).as_("total_amount"))
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked == 0))
.groupby(ple.account)
.run(as_dict=True)
)
for pl in pl_entries:
with self.subTest(pl=pl):
self.assertEqual(pl.total_amount, 0)
self.assertFalse(
frappe.db.exists(
"Payment Ledger Entry",
{"voucher_type": si.doctype, "voucher_no": si.name, "delinked": 1},
)
)

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

@@ -8,6 +8,7 @@ from frappe.utils import today
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.general_ledger import make_reverse_gl_entries
from erpnext.accounts.utils import get_fiscal_year
from erpnext.tests.utils import ERPNextTestSuite
@@ -334,6 +335,48 @@ class TestPeriodClosingVoucher(ERPNextTestSuite):
return pcv
@ERPNextTestSuite.change_settings(
"Accounts Settings",
{"enable_immutable_ledger": 1},
)
def test_immutable_ledger_reverse_entry_uses_passed_posting_date_after_pcv(self):
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
jv = make_journal_entry(
posting_date="2021-03-15",
amount=400,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
company=company,
save=False,
)
jv.company = company
jv.save()
jv.submit()
self.make_period_closing_voucher(posting_date="2021-03-31")
# Passed posting_date is after PCV end date, so cancellation should not fail.
make_reverse_gl_entries(
voucher_type="Journal Entry",
voucher_no=jv.name,
posting_date="2022-01-01",
)
totals_after_cancel = frappe.db.sql(
"""
select sum(debit) as total_debit, sum(credit) as total_credit
from `tabGL Entry`
where voucher_type=%s and voucher_no=%s and is_cancelled=0
""",
("Journal Entry", jv.name),
as_dict=True,
)[0]
self.assertEqual(totals_after_cancel.total_debit, totals_after_cancel.total_credit)
def create_company():
company = 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

@@ -13,52 +13,69 @@
"column_break_9",
"warehouse",
"company_address",
"section_break_15",
"applicable_for_users",
"accounting_tab",
"section_break_11",
"payments",
"set_grand_total_to_default_mop",
"price_list_and_currency_section",
"currency",
"column_break_bptt",
"selling_price_list",
"write_off_section",
"write_off_account",
"column_break_ukpz",
"write_off_cost_center",
"column_break_pkca",
"write_off_limit",
"income_and_expense_account",
"income_account",
"column_break_byzk",
"expense_account",
"taxes_section",
"taxes_and_charges",
"column_break_cjpp",
"tax_category",
"section_break_19",
"account_for_change_amount",
"disable_rounded_total",
"column_break_23",
"apply_discount_on",
"allow_partial_payment",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"pos_configurations_tab",
"section_break_14",
"hide_images",
"hide_unavailable_items",
"auto_add_item_to_cart",
"validate_stock_on_save",
"print_receipt_on_order_complete",
"action_on_new_invoice",
"validate_stock_on_save",
"column_break_16",
"update_stock",
"ignore_pricing_rule",
"print_receipt_on_order_complete",
"pos_item_selector_section",
"hide_images",
"column_break_rpny",
"hide_unavailable_items",
"column_break_stcl",
"auto_add_item_to_cart",
"pos_item_details_section",
"allow_rate_change",
"column_break_hwfg",
"allow_discount_change",
"set_grand_total_to_default_mop",
"allow_partial_payment",
"section_break_15",
"applicable_for_users",
"section_break_23",
"item_groups",
"column_break_25",
"customer_groups",
"more_info_tab",
"section_break_16",
"print_format",
"letter_head",
"column_break0",
"tc_name",
"select_print_heading",
"section_break_19",
"selling_price_list",
"currency",
"write_off_account",
"write_off_cost_center",
"write_off_limit",
"account_for_change_amount",
"disable_rounded_total",
"column_break_23",
"income_account",
"expense_account",
"taxes_and_charges",
"tax_category",
"apply_discount_on",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"utm_analytics_section",
"utm_source",
"column_break_tvls",
@@ -133,8 +150,7 @@
},
{
"fieldname": "section_break_14",
"fieldtype": "Section Break",
"label": "Configuration"
"fieldtype": "Section Break"
},
{
"description": "Only show Items from these Item Groups",
@@ -155,6 +171,7 @@
"options": "POS Customer Group"
},
{
"collapsible": 1,
"fieldname": "section_break_16",
"fieldtype": "Section Break",
"label": "Print Settings"
@@ -194,7 +211,7 @@
{
"fieldname": "section_break_19",
"fieldtype": "Section Break",
"label": "Accounting"
"label": "Miscellaneous"
},
{
"fieldname": "selling_price_list",
@@ -430,6 +447,7 @@
},
{
"default": "0",
"description": "Applicable on POS Invoice",
"fieldname": "allow_partial_payment",
"fieldtype": "Check",
"label": "Allow Partial Payment"
@@ -447,6 +465,83 @@
"fieldname": "utm_analytics_section",
"fieldtype": "Section Break",
"label": "Campaign"
},
{
"fieldname": "accounting_tab",
"fieldtype": "Tab Break",
"label": "Accounting"
},
{
"fieldname": "more_info_tab",
"fieldtype": "Tab Break",
"label": "More Info"
},
{
"fieldname": "pos_configurations_tab",
"fieldtype": "Tab Break",
"label": "POS Configurations"
},
{
"fieldname": "price_list_and_currency_section",
"fieldtype": "Section Break",
"label": "Price List & Currency"
},
{
"fieldname": "column_break_bptt",
"fieldtype": "Column Break"
},
{
"fieldname": "write_off_section",
"fieldtype": "Section Break",
"label": "Write Off"
},
{
"fieldname": "column_break_ukpz",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_pkca",
"fieldtype": "Column Break"
},
{
"fieldname": "income_and_expense_account",
"fieldtype": "Section Break",
"label": "Income and Expense"
},
{
"fieldname": "column_break_byzk",
"fieldtype": "Column Break"
},
{
"fieldname": "taxes_section",
"fieldtype": "Section Break",
"label": "Taxes"
},
{
"fieldname": "column_break_cjpp",
"fieldtype": "Column Break"
},
{
"fieldname": "pos_item_selector_section",
"fieldtype": "Section Break",
"label": "POS Item Selector"
},
{
"fieldname": "column_break_rpny",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_stcl",
"fieldtype": "Column Break"
},
{
"fieldname": "pos_item_details_section",
"fieldtype": "Section Break",
"label": "POS Item Details"
},
{
"fieldname": "column_break_hwfg",
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
@@ -475,7 +570,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2026-02-10 14:24:48.597412",
"modified": "2026-05-26 12:07:48.597412",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

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")
@@ -137,9 +138,10 @@ def pause_pcv_processing(docname: str):
ppcv = qb.DocType("Process Period Closing Voucher")
qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run()
# If a date is stuck in 'Running' state, this will allow it to procced.
if queued_dates := frappe.db.get_all(
"Process Period Closing Voucher Detail",
filters={"parent": docname, "status": "Queued"},
filters={"parent": docname, "status": ["in", ["Queued", "Running"]]},
pluck="name",
):
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
@@ -173,6 +175,9 @@ def resume_pcv_processing(docname: str):
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
qb.update(ppcvd).set(ppcvd.status, "Queued").where(ppcvd.name.isin(paused_dates)).run()
start_pcv_processing(docname)
else:
# If a parent doc is stuck in 'Running' state, will allow it to proceed.
schedule_next_date(docname)
def update_default_dimensions(dimension_fields, gl_entry, dimension_values):
@@ -288,7 +293,21 @@ def schedule_next_date(docname: str):
)
# Ensure both normal and opening balances are processed for all dates
if total_no_of_dates == completed:
summarize_and_post_ledger_entries(docname)
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
is_job_running,
)
job_name = f"summarize_{docname}"
if not is_job_running(job_name):
frappe.enqueue(
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.summarize_and_post_ledger_entries",
queue="long",
timeout="3600",
is_async=True,
job_name=job_name,
enqueue_after_commit=True,
docname=docname,
)
def make_dict_json_compliant(dimension_wise_balance) -> dict:
@@ -544,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",
@@ -1921,7 +1920,7 @@
{
"default": "0",
"depends_on": "eval: !doc.is_return",
"description": "Issue a debit note with 0 qty against an existing Sales Invoice",
"description": "Issue a debit note against an existing Sales Invoice to adjust the rate. The quantity will be retained from the original invoice.",
"fieldname": "is_debit_note",
"fieldtype": "Check",
"label": "Is Rate Adjustment Entry (Debit Note)"
@@ -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-01 02:37:29.742764",
"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:
@@ -968,9 +1009,6 @@ class SalesInvoice(SellingController):
if selling_price_list:
self.set("selling_price_list", selling_price_list)
if not for_validate:
self.update_stock = cint(pos.get("update_stock"))
# set pos values in items
for item in self.get("items"):
if item.get("item_code"):
@@ -981,6 +1019,10 @@ class SalesInvoice(SellingController):
if (not for_validate) or (for_validate and not item.get(fname)):
item.set(fname, val)
if not for_validate:
dn_flag = any(d.get("dn_detail") for d in self.get("items"))
self.update_stock = 0 if dn_flag else cint(pos.get("update_stock"))
# fetch terms
if self.tc_name and not self.terms:
self.terms = frappe.db.get_value("Terms and Conditions", self.tc_name, "terms")
@@ -2023,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,
@@ -2742,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"]:
@@ -2775,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,
},
@@ -2783,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"
@@ -2799,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
@@ -3035,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

@@ -14,6 +14,7 @@ from frappe.utils import cstr
from frappe.utils.nestedset import get_root_of
from erpnext.setup.doctype.customer_group.customer_group import get_parent_customer_groups
from erpnext.setup.doctype.supplier_group.supplier_group import get_parent_supplier_groups
class IncorrectCustomerGroup(frappe.ValidationError):
@@ -176,38 +177,44 @@ def get_party_details(party, party_type, args=None):
def get_tax_template(posting_date, args):
"""Get matching tax rule"""
args = frappe._dict(args)
conditions = []
TaxRule = DocType("Tax Rule")
query = frappe.qb.from_(TaxRule).select("*")
if posting_date:
conditions.append(
f"""(from_date is null or from_date <= '{posting_date}')
and (to_date is null or to_date >= '{posting_date}')"""
query = query.where(
(TaxRule.from_date.isnull() | (TaxRule.from_date <= posting_date))
& (TaxRule.to_date.isnull() | (TaxRule.to_date >= posting_date))
)
else:
conditions.append("(from_date is null) and (to_date is null)")
query = query.where(TaxRule.from_date.isnull() & TaxRule.to_date.isnull())
conditions.append(
"ifnull(tax_category, '') = {}".format(frappe.db.escape(cstr(args.get("tax_category")), False))
)
if "tax_category" in args.keys():
del args["tax_category"]
def get_group_ancestors(doctype, get_parents, value):
if not value:
value = get_root_of(doctype)
return [""] + [d.name for d in get_parents(value)]
group_fields = {
"customer_group": ("Customer Group", get_parent_customer_groups),
"supplier_group": ("Supplier Group", get_parent_supplier_groups),
}
args.setdefault("tax_category", "")
for key, value in args.items():
if key == "use_for_shopping_cart":
conditions.append(f"use_for_shopping_cart = {1 if value else 0}")
elif key == "customer_group":
if not value:
value = get_root_of("Customer Group")
customer_group_condition = get_customer_group_condition(value)
conditions.append(f"ifnull({key}, '') in ('', {customer_group_condition})")
query = query.where(TaxRule.use_for_shopping_cart == value)
elif key == "tax_category":
query = query.where(IfNull(TaxRule.tax_category, "") == (value or ""))
elif key in group_fields:
doctype, get_parents = group_fields[key]
query = query.where(
IfNull(TaxRule[key], "").isin(get_group_ancestors(doctype, get_parents, value))
)
else:
conditions.append(f"ifnull({key}, '') in ('', {frappe.db.escape(cstr(value))})")
query = query.where(IfNull(TaxRule[key], "").isin(["", value or ""]))
tax_rule = frappe.db.sql(
"""select * from `tabTax Rule`
where {}""".format(" and ".join(conditions)),
as_dict=True,
)
tax_rule = query.run(as_dict=True)
if not tax_rule:
return None
@@ -236,11 +243,3 @@ def get_tax_template(posting_date, args):
return None
return tax_template
def get_customer_group_condition(customer_group):
condition = ""
customer_groups = ["%s" % (frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group)]
if customer_groups:
condition = ",".join(["%s"] * len(customer_groups)) % (tuple(customer_groups))
return condition

View File

@@ -62,6 +62,117 @@ class TestTaxRule(ERPNextTestSuite):
"_Test Sales Taxes and Charges Template - _TC",
)
def test_for_parent_supplier_group(self):
purchase_template = "_Test Purchase Taxes and Charges Template - _TC"
if not frappe.db.exists("Purchase Taxes and Charges Template", purchase_template):
frappe.get_doc(
{
"doctype": "Purchase Taxes and Charges Template",
"title": "_Test Purchase Taxes and Charges Template",
"company": "_Test Company",
"taxes": [
{
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"description": "VAT",
"doctype": "Purchase Taxes and Charges",
"cost_center": "Main - _TC",
"rate": 6,
}
],
}
).insert()
make_tax_rule(
supplier_group="All Supplier Groups",
tax_type="Purchase",
purchase_tax_template=purchase_template,
priority=1,
use_for_shopping_cart=0,
from_date="2015-01-01",
save=1,
)
# "_Test Supplier Group" has "All Supplier Groups" as its parent — should match hierarchically
self.assertEqual(
get_tax_template(
"2015-01-01",
{
"supplier_group": "_Test Supplier Group",
"tax_type": "Purchase",
"use_for_shopping_cart": 0,
},
),
purchase_template,
)
def test_use_for_shopping_cart_filter(self):
city = "Test Cart City"
# higher priority ensures this rule wins when use_for_shopping_cart is not filtered
make_tax_rule(
customer="_Test Customer",
billing_city=city,
sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
use_for_shopping_cart=0,
priority=2,
save=1,
)
make_tax_rule(
customer="_Test Customer",
billing_city=city,
sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC",
use_for_shopping_cart=1,
priority=1,
save=1,
)
# Cart request (use_for_shopping_cart=1) filters to cart rules only
self.assertEqual(
get_tax_template(
"2015-01-01",
{"customer": "_Test Customer", "billing_city": city, "use_for_shopping_cart": 1},
),
"_Test Sales Taxes and Charges Template 1 - _TC",
)
# Non-cart request omits use_for_shopping_cart — no filter is applied, both rules
# are candidates; non-cart rule wins by higher priority
self.assertEqual(
get_tax_template(
"2015-01-01",
{"customer": "_Test Customer", "billing_city": city},
),
"_Test Sales Taxes and Charges Template - _TC",
)
def test_use_for_shopping_cart_default(self):
city = "Test Default Cart City"
# use_for_shopping_cart not set — Check field defaults to 0
make_tax_rule(
customer="_Test Customer",
billing_city=city,
sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
use_for_shopping_cart=0, # Default is set to 1.
save=1,
)
# Non-cart request (no use_for_shopping_cart in args) matches the rule
self.assertEqual(
get_tax_template(
"2015-01-01",
{"customer": "_Test Customer", "billing_city": city},
),
"_Test Sales Taxes and Charges Template - _TC",
)
# Cart request (use_for_shopping_cart=1) does not match — rule has default 0
self.assertIsNone(
get_tax_template(
"2015-01-01",
{"customer": "_Test Customer", "billing_city": city, "use_for_shopping_cart": 1},
)
)
def test_conflict_with_overlapping_dates(self):
tax_rule1 = make_tax_rule(
customer="_Test Customer",

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

@@ -431,6 +431,8 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.flags.adv_adj = adv_adj
gle.flags.update_outstanding = update_outstanding or "Yes"
gle.flags.notify_update = False
if gle.is_cancelled or is_immutable_ledger_enabled():
gle.flags.ignore_links = True
gle.submit()
if (
@@ -717,7 +719,12 @@ def make_reverse_gl_entries(
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"])
# For reverse entries, use the posting_date parameter if provided and valid
# Otherwise fall back to original posting_date
validation_date = posting_date if posting_date else gl_entries[0]["posting_date"]
validate_against_pcv(is_opening, validation_date, gl_entries[0]["company"])
if partial_cancel:
# Partial cancel is only used by `Advance` in separate account feature.
# Only cancel GL entries for unlinked reference using `voucher_detail_no`

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
@@ -750,7 +769,7 @@ def set_taxes(
args.update({"tax_type": "Purchase"})
if use_for_shopping_cart:
args.update({"use_for_shopping_cart": use_for_shopping_cart})
args.update({"use_for_shopping_cart": cint(use_for_shopping_cart)})
return get_tax_template(posting_date, args)

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

@@ -40,9 +40,11 @@ import erpnext
from erpnext.accounts.doctype.account.account import get_account_currency
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.utils import get_stock_value_on
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:
@@ -1752,31 +1753,31 @@ def sort_stock_vouchers_by_posting_date(
def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None):
values = []
condition = ""
posting_datetime = get_combine_datetime(posting_date, posting_time)
SLE = DocType("Stock Ledger Entry")
query = (
frappe.qb.from_(SLE)
.select(SLE.voucher_type, SLE.voucher_no)
.distinct()
.where(SLE.posting_datetime >= posting_datetime)
.where(SLE.is_cancelled == 0)
.orderby(SLE.posting_datetime)
.orderby(SLE.creation)
.for_update()
)
if for_items:
condition += " and item_code in ({})".format(", ".join(["%s"] * len(for_items)))
values += for_items
query = query.where(SLE.item_code.isin(for_items))
if for_warehouses:
condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses)))
values += for_warehouses
query = query.where(SLE.warehouse.isin(for_warehouses))
if company:
condition += " and company = %s"
values.append(company)
query = query.where(SLE.company == company)
future_stock_vouchers = frappe.db.sql(
f"""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle
where
timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s)
and is_cancelled = 0
{condition}
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""",
tuple([posting_date, posting_time, *values]),
as_dict=True,
)
future_stock_vouchers = query.run(as_dict=True)
return [(d.voucher_type, d.voucher_no) for d in future_stock_vouchers]
@@ -2130,8 +2131,9 @@ def create_payment_ledger_entry(
ple = frappe.get_doc(entry)
if cancel:
delink_original_entry(ple, partial_cancel=partial_cancel)
if is_immutable_ledger_enabled():
if not is_immutable_ledger_enabled():
delink_original_entry(ple, partial_cancel=partial_cancel)
else:
ple.delinked = 0
ple.posting_date = frappe.form_dict.get("posting_date") or getdate()
@@ -2220,6 +2222,7 @@ def delink_original_entry(pl_entry, partial_cancel=False):
qb.update(ple)
.set(ple.modified, now())
.set(ple.modified_by, frappe.session.user)
.set(ple.delinked, True)
.where(
(ple.company == pl_entry.company)
& (ple.account_type == pl_entry.account_type)
@@ -2236,9 +2239,6 @@ def delink_original_entry(pl_entry, partial_cancel=False):
if partial_cancel:
query = query.where(ple.voucher_detail_no == pl_entry.voucher_detail_no)
if not is_immutable_ledger_enabled():
query = query.set(ple.delinked, True)
query.run()
@@ -2711,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

@@ -551,7 +551,9 @@ frappe.ui.form.on("Asset", {
asset_type: function (frm) {
if (frm.doc.docstatus == 0) {
if (frm.doc.asset_type == "Composite Asset") {
frm.set_value("net_purchase_amount", 0);
if (!frm.doc.net_purchase_amount) {
frm.set_value("net_purchase_amount", 0);
}
} else {
frm.set_df_property("net_purchase_amount", "read_only", 0);
}

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

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