Compare commits

..

143 Commits

Author SHA1 Message Date
Frappe PR Bot
1e52738150 chore(release): Bumped to Version 15.92.1
## [15.92.1](https://github.com/frappe/erpnext/compare/v15.92.0...v15.92.1) (2025-12-17)

### Bug Fixes

* incorrect current qty in stock reco (backport [#51152](https://github.com/frappe/erpnext/issues/51152)) ([#51158](https://github.com/frappe/erpnext/issues/51158)) ([552c5b5](552c5b5911))
2025-12-17 09:34:54 +00:00
rohitwaghchaure
cbd0a76645 Merge pull request #51161 from frappe/mergify/bp/version-15/pr-51158
fix: incorrect current qty in stock reco (backport #51152) (backport #51158)
2025-12-17 15:03:25 +05:30
mergify[bot]
552c5b5911 fix: incorrect current qty in stock reco (backport #51152) (#51158)
* fix: incorrect current qty in stock reco (#51152)

(cherry picked from commit dec474ef3a)

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
(cherry picked from commit 89d6a8f02e)
2025-12-17 08:17:20 +00:00
Frappe PR Bot
66b2b89bcd chore(release): Bumped to Version 15.92.0
# [15.92.0](https://github.com/frappe/erpnext/compare/v15.91.3...v15.92.0) (2025-12-16)

### Bug Fixes

* **accounts:** handle drop ship in company linked address validation ([b340d7d](b340d7d4f4))
* add link filters for item group in quickentry ([981c9c7](981c9c76c1))
* add missing query key in 'Reports To' field filter ([e1dc80b](e1dc80b6d8))
* add validation for transferred qty and handle MR transfer status for in-transit entry. (backport [#50683](https://github.com/frappe/erpnext/issues/50683)) ([#51134](https://github.com/frappe/erpnext/issues/51134)) ([6a6398a](6a6398a392))
* **currency exchange settings:** added backward compatibility for frankfurter api ([8d32ba9](8d32ba9a2e))
* delayed tasks summary chart color ([325fc61](325fc619dc))
* ensure fresh `grand_total_diff` is used for each calculation ([2d198e6](2d198e698a))
* ensure type on method parameter ([16c8b74](16c8b74d52))
* incorrect invoice qty ([ebbecdb](ebbecdba23))
* **manufacturing:** add validation for disassemble qty ([cdc0429](cdc04292f2))
* **manufacturing:** get items for disassembly order ([279cf6f](279cf6fe00))
* mark navbar item as translatable ([ec3a226](ec3a226a83))
* only show net gl balance as opening in general ledger ([0d5e45b](0d5e45bb7c))
* **payment entry:** fetch gain loss account from company boot ([c01e40d](c01e40da3c))
* precision issue on job card submission ([4ee4a57](4ee4a57f72))
* preserve user-entered exchange rates in ERR journal entries ([fa04e36](fa04e368d3))
* prevent dispatch address copying on drop ship ([5d5dff9](5d5dff9103))
* prevent self in "Reports To" dropdown (UI-level check) ([9e8bb9b](9e8bb9b235))
* putaway rule not applying on serial nos ([df820ae](df820aece6))
* re-calculate outstanding / write-off amount during submission ([5bfdc01](5bfdc010f3))
* **Rename Tool:** use "Link" field instead of "Select" ([2aff169](2aff16928c))
* **Rename Tool:** use "Link" field instead of "Select" (backport [#50995](https://github.com/frappe/erpnext/issues/50995)) ([#51138](https://github.com/frappe/erpnext/issues/51138)) ([53bb2cf](53bb2cf7c0))
* Serial/Batches not fetching when creating Material Transfer from Purchase Receipt ([f3c70a6](f3c70a66b5))
* **share balance:** use currency field instead of int for rate and amount ([a8ed281](a8ed2815a4))
* Short circuit guest perm checks ([dab8ac7](dab8ac7b1d))
* stock ageing report ([d098572](d09857294c))
* **stock:** remove total bar in chart view ([918f8ca](918f8ca79b))
* **subcontract:** ignore BOM qty validation for alternative items (backport [#51122](https://github.com/frappe/erpnext/issues/51122)) ([#51135](https://github.com/frappe/erpnext/issues/51135)) ([2c9c6c3](2c9c6c3798))
* **trial_balance:** remove hardcoded precision for currency values ([99b69c1](99b69c121e))
* use dummy translations for custom field labels ([#49875](https://github.com/frappe/erpnext/issues/49875)) ([088bbac](088bbac543))
* validate available stock with multiple dimensions (backport [#50937](https://github.com/frappe/erpnext/issues/50937)) ([#50983](https://github.com/frappe/erpnext/issues/50983)) ([98eeff8](98eeff8775))
* validate budget after cost center allocation ([a2b6e4a](a2b6e4a1c5))

### Features

* introduce extended bank transaction fields (backport [#50021](https://github.com/frappe/erpnext/issues/50021)) ([#51112](https://github.com/frappe/erpnext/issues/51112)) ([a61890e](a61890ec2b))

### Performance Improvements

* move all hourly/daily jobs to maintenance queue (backport [#47504](https://github.com/frappe/erpnext/issues/47504)) ([#51005](https://github.com/frappe/erpnext/issues/51005)) ([46ca347](46ca347578))
* sabb validate serial no ([#51132](https://github.com/frappe/erpnext/issues/51132)) ([3a9888a](3a9888aad9))

### Reverts

* changes to install_fixtures ([19dc26e](19dc26ea16))
2025-12-16 15:33:15 +00:00
ruthra kumar
d804d43ed0 Merge pull request #51124 from frappe/version-15-hotfix
chore: release v15
2025-12-16 21:01:44 +05:30
ruthra kumar
53bb2cf7c0 fix(Rename Tool): use "Link" field instead of "Select" (backport #50995) (#51138)
fix(Rename Tool): use "Link" field instead of "Select"

(cherry picked from commit ba9bbed038)

# Conflicts:
#	erpnext/utilities/doctype/rename_tool/rename_tool.json
#	erpnext/utilities/doctype/rename_tool/rename_tool.py

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2025-12-16 20:40:10 +05:30
Raffael Meyer
2aff16928c fix(Rename Tool): use "Link" field instead of "Select"
(cherry picked from commit ba9bbed038)

# Conflicts:
#	erpnext/utilities/doctype/rename_tool/rename_tool.json
#	erpnext/utilities/doctype/rename_tool/rename_tool.py
2025-12-16 20:23:15 +05:30
Diptanil Saha
630dcf072f Merge pull request #51144 from frappe/mergify/bp/version-15-hotfix/pr-51120
fix: add link filters for item group in quickentry (backport #51120)
2025-12-16 18:59:24 +05:30
Mihir Kandoi
b73eb47a43 Merge pull request #51140 from frappe/mergify/bp/version-15-hotfix/pr-51137 2025-12-16 18:31:52 +05:30
Afsal Syed
981c9c76c1 fix: add link filters for item group in quickentry
(cherry picked from commit 3bef6bf5ef)
2025-12-16 12:57:51 +00:00
mergify[bot]
6a6398a392 fix: add validation for transferred qty and handle MR transfer status for in-transit entry. (backport #50683) (#51134)
fix: add validation for transferred qty and handle MR transfer status for in-transit entry. (#50683)

* fix: add validation for transferred qty

* fix: modify if statement

* test: add unit test for mr transfer status in-transit entry

(cherry picked from commit 890316a793)

Co-authored-by: Logesh Periyasamy <logeshperiyasamy24@gmail.com>
2025-12-16 18:22:15 +05:30
Mihir Kandoi
325fc619dc fix: delayed tasks summary chart color
(cherry picked from commit 38affb0562)
2025-12-16 12:44:30 +00:00
mergify[bot]
2c9c6c3798 fix(subcontract): ignore BOM qty validation for alternative items (backport #51122) (#51135)
fix(subcontract): ignore BOM qty validation for alternative items (#51122)

(cherry picked from commit 2f19244660)

Co-authored-by: Kavin <78342682+kavin-114@users.noreply.github.com>
2025-12-16 18:11:44 +05:30
rohitwaghchaure
3a9888aad9 perf: sabb validate serial no (#51132) 2025-12-16 17:37:39 +05:30
ruthra kumar
695ca39d84 Merge pull request #51129 from frappe/mergify/bp/version-15-hotfix/pr-51048
fix(payment entry): fetch gain loss account from company boot (backport #51048)
2025-12-16 17:00:49 +05:30
ruthra kumar
9032d4c3d6 Merge pull request #51110 from one-highflyer/fix/err-preserve-exchange-rate
fix: preserve user-entered exchange rates in ERR journal entries
2025-12-16 16:47:12 +05:30
ravibharathi656
c01e40da3c fix(payment entry): fetch gain loss account from company boot
(cherry picked from commit 8e54be7808)
2025-12-16 10:58:21 +00:00
ruthra kumar
552cb5c528 Merge pull request #51127 from frappe/mergify/bp/version-15-hotfix/pr-51123
fix: ensure type on method parameter (backport #51123)
2025-12-16 15:59:49 +05:30
ruthra kumar
e6ccb00d4c Merge pull request #50539 from frappe/mergify/bp/version-15-hotfix/pr-49875
fix: use dummy translations for custom field labels (backport #49875)
2025-12-16 15:54:27 +05:30
ruthra kumar
5b481d9235 Merge pull request #51071 from frappe/mergify/bp/version-15-hotfix/pr-51041
fix(trial_balance): remove hardcoded precision for currency values (backport #51041)
2025-12-16 15:40:02 +05:30
ruthra kumar
6b27b659e3 Merge pull request #51017 from frappe/mergify/bp/version-15-hotfix/pr-50948
fix(stock): remove total bar in chart view (backport #50948)
2025-12-16 15:36:04 +05:30
ruthra kumar
16c8b74d52 fix: ensure type on method parameter
(cherry picked from commit c055e86e51)
2025-12-16 10:03:53 +00:00
Mihir Kandoi
8dcb2f39c2 Merge pull request #51088 from aerele/v15-drop-ship-retain-address 2025-12-16 13:28:02 +05:30
ravibharathi656
5d5dff9103 fix: prevent dispatch address copying on drop ship 2025-12-16 13:10:38 +05:30
ruthra kumar
b529a6d00c Merge pull request #51116 from frappe/mergify/bp/version-15-hotfix/pr-51077
refactor: standardize cost_center updation across transactions (backport #51077)
2025-12-16 11:07:51 +05:30
Navin-S-R
41659a875b refactor: standardize cost_center updation across transactions
(cherry picked from commit c28f6f1856)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.js
2025-12-16 11:04:41 +05:30
mergify[bot]
a61890ec2b feat: introduce extended bank transaction fields (backport #50021) (#51112)
Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
Co-authored-by: 0xD0M1M0 <76812428+0xD0M1M0@users.noreply.github.com>
2025-12-15 23:04:16 +01:00
Imesha Sudasingha
fa04e368d3 fix: preserve user-entered exchange rates in ERR journal entries
The JE creation was overriding exchange_rate=1 with the system rate.
Set ignore_exchange_rate flag to preserve user values.
2025-12-15 20:23:06 +05:30
Venkatesh
98eeff8775 fix: validate available stock with multiple dimensions (backport #50937) (#50983)
* fix: validate available stock with multiple dimensions

* test: validate negative stock with multiple inventory dimensions

* chore: reset document_wise_inventory_dimensions
2025-12-15 19:07:06 +05:30
Smit Vora
64acf179db Merge pull request #51105 from frappe/mergify/bp/version-15-hotfix/pr-50782
fix: only show net balance as opening in general ledger (backport #50782)
2025-12-15 15:50:49 +05:30
Smit Vora
0d5e45bb7c fix: only show net gl balance as opening in general ledger
(cherry picked from commit b7c7e0746e)
2025-12-15 10:00:49 +00:00
Mihir Kandoi
fc86784eb1 Merge pull request #51096 from frappe/mergify/bp/version-15-hotfix/pr-49139 2025-12-15 03:50:07 +05:30
Anjali Patel
e1dc80b6d8 fix: add missing query key in 'Reports To' field filter
(cherry picked from commit cbfb14a654)
2025-12-14 20:26:21 +00:00
Anjali Patel
9e8bb9b235 fix: prevent self in "Reports To" dropdown (UI-level check)
Ensures employee cannot select themselves in the "Reports To" field via UI.
This complements server-side validation by improving UX.

(cherry picked from commit 608d38a172)
2025-12-14 20:26:20 +00:00
mergify[bot]
ae90ee3f17 Merge pull request #51087 from frappe/mergify/bp/version-15-hotfix/pr-51063
fix(transaction-deletion): Add virtual doctypes to the list of ignored doctypes (backport #51063)
2025-12-14 14:30:11 +05:30
Ankush Menat
dab8ac7b1d fix: Short circuit guest perm checks 2025-12-14 12:11:55 +05:30
rohitwaghchaure
ce769d3a2f Merge pull request #51082 from frappe/mergify/bp/version-15-hotfix/pr-51079
fix: stock ageing report (backport #51079)
2025-12-13 07:39:04 +05:30
Rohit Waghchaure
d09857294c fix: stock ageing report
(cherry picked from commit cb84ffd972)
2025-12-12 15:13:41 +00:00
Frappe PR Bot
3a74968ced chore(release): Bumped to Version 15.91.3
## [15.91.3](https://github.com/frappe/erpnext/compare/v15.91.2...v15.91.3) (2025-12-12)

### Bug Fixes

* **accounts:** handle drop ship in company linked address validation ([5d01cad](5d01cad1d5))
2025-12-12 08:57:31 +00:00
rohitwaghchaure
26725f4e53 Merge pull request #51075 from frappe/mergify/bp/version-15/pr-51072
fix(accounts): handle drop ship in company linked address validation (backport #51034) (backport #51072)
2025-12-12 14:26:09 +05:30
Khushi Rawat
030ce6d6a0 Merge pull request #51074 from frappe/mergify/bp/version-15-hotfix/pr-51070
fix: validate budget after cost center allocation (backport #51070)
2025-12-12 14:15:45 +05:30
Sudharsanan11
b46d93c709 test(accounts): add validation test for dispatch address with drop ship enabled
(cherry picked from commit f6a96e5563)
(cherry picked from commit 2263f9a477)
2025-12-12 08:37:56 +00:00
Sudharsanan11
5d01cad1d5 fix(accounts): handle drop ship in company linked address validation
(cherry picked from commit 2ec119e561)
(cherry picked from commit b340d7d4f4)
2025-12-12 08:37:56 +00:00
rohitwaghchaure
97b253740b Merge pull request #51072 from frappe/mergify/bp/version-15-hotfix/pr-51034
fix(accounts): handle drop ship in company linked address validation (backport #51034)
2025-12-12 14:07:15 +05:30
rohitwaghchaure
94c3d66f2f Merge pull request #51073 from frappe/mergify/bp/version-15-hotfix/pr-51069
fix: incorrect invoice qty (backport #51069)
2025-12-12 13:58:24 +05:30
khushi8112
a2b6e4a1c5 fix: validate budget after cost center allocation
(cherry picked from commit f9be8a46fb)
2025-12-12 08:22:40 +00:00
Rohit Waghchaure
ebbecdba23 fix: incorrect invoice qty
(cherry picked from commit 96cdb7d54f)
2025-12-12 08:07:11 +00:00
Sudharsanan11
2263f9a477 test(accounts): add validation test for dispatch address with drop ship enabled
(cherry picked from commit f6a96e5563)
2025-12-12 08:05:59 +00:00
Sudharsanan11
b340d7d4f4 fix(accounts): handle drop ship in company linked address validation
(cherry picked from commit 2ec119e561)
2025-12-12 08:05:58 +00:00
Navin-S-R
99b69c121e fix(trial_balance): remove hardcoded precision for currency values
(cherry picked from commit a8af04f6fc)
2025-12-12 07:40:41 +00:00
rohitwaghchaure
860486bb34 Merge pull request #51068 from frappe/mergify/bp/version-15-hotfix/pr-51047
fix(manufacturing): get items for disassembly order (backport #51047)
2025-12-12 13:10:36 +05:30
rohitwaghchaure
1693e3ef3f chore: fix conflicts 2025-12-12 11:53:18 +05:30
Sudharsanan11
279cf6fe00 fix(manufacturing): get items for disassembly order
(cherry picked from commit 99148a2aba)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/work_order.py
2025-12-12 06:15:12 +00:00
Sudharsanan11
cdc04292f2 fix(manufacturing): add validation for disassemble qty
(cherry picked from commit 86d6facab3)
2025-12-12 06:15:11 +00:00
Sagar Vora
6788b58d1c Merge pull request #51059 from frappe/mergify/bp/version-15-hotfix/pr-51057
fix: re-calculate outstanding / write-off amount during submission (backport #51057)
2025-12-11 23:26:06 +05:30
Sagar Vora
5bfdc010f3 fix: re-calculate outstanding / write-off amount during submission
(cherry picked from commit 09c9ac1b66)
2025-12-11 17:35:25 +00:00
Sagar Vora
0e7efd75cd Merge pull request #51053 from frappe/mergify/bp/version-15-hotfix/pr-51051
fix: ensure fresh `grand_total_diff` is used for each calculation (backport #51051)
2025-12-11 18:07:37 +05:30
Sagar Vora
2d198e698a fix: ensure fresh grand_total_diff is used for each calculation
(cherry picked from commit b3fdef8d19)
2025-12-11 12:35:29 +00:00
Diptanil Saha
857ab70f4e Merge pull request #51045 from frappe/mergify/bp/version-15-hotfix/pr-51037
fix(currency exchange settings): added backward compatibility for frankfurter api (backport #51037)
2025-12-11 16:28:03 +05:30
Frappe PR Bot
ca21f16db2 chore(release): Bumped to Version 15.91.2
## [15.91.2](https://github.com/frappe/erpnext/compare/v15.91.1...v15.91.2) (2025-12-11)

### Bug Fixes

* putaway rule not applying on serial nos ([23c82d4](23c82d410b))
* Serial/Batches not fetching when creating Material Transfer from Purchase Receipt ([1e0532f](1e0532f387))
2025-12-11 10:48:40 +00:00
rohitwaghchaure
5324000e2e Merge pull request #51043 from frappe/mergify/bp/version-15/pr-51036
fix: put-away rule not applying on serial nos (backport #51035) (backport #51036)
2025-12-11 16:17:10 +05:30
rohitwaghchaure
9d2055c620 Merge pull request #51042 from frappe/mergify/bp/version-15/pr-51029
fix: Serial/Batches not fetching when creating Material Transfer from Purchase Receipt (backport #51027) (backport #51029)
2025-12-11 16:16:29 +05:30
Diptanil Saha
113da4f512 chore: resolve conflict 2025-12-11 15:59:57 +05:30
diptanilsaha
8d32ba9a2e fix(currency exchange settings): added backward compatibility for frankfurter api
(cherry picked from commit 5c2bb66028)

# Conflicts:
#	erpnext/patches.txt
2025-12-11 10:26:45 +00:00
Rohit Waghchaure
23c82d410b fix: putaway rule not applying on serial nos
(cherry picked from commit 6bb0bdcdca)
(cherry picked from commit df820aece6)
2025-12-11 10:15:31 +00:00
rohitwaghchaure
580e825ec2 chore: fix conflicts
(cherry picked from commit c8565c47a2)
2025-12-11 10:15:29 +00:00
Rohit Waghchaure
1e0532f387 fix: Serial/Batches not fetching when creating Material Transfer from Purchase Receipt
(cherry picked from commit d16c50486a)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
(cherry picked from commit f3c70a66b5)
2025-12-11 10:15:29 +00:00
rohitwaghchaure
8f569d9711 Merge pull request #51036 from frappe/mergify/bp/version-15-hotfix/pr-51035
fix: put-away rule not applying on serial nos (backport #51035)
2025-12-11 15:43:51 +05:30
Rohit Waghchaure
df820aece6 fix: putaway rule not applying on serial nos
(cherry picked from commit 6bb0bdcdca)
2025-12-11 09:10:12 +00:00
rohitwaghchaure
6530cfe84b Merge pull request #51029 from frappe/mergify/bp/version-15-hotfix/pr-51027
fix: Serial/Batches not fetching when creating Material Transfer from Purchase Receipt (backport #51027)
2025-12-11 13:14:46 +05:30
rohitwaghchaure
c8565c47a2 chore: fix conflicts 2025-12-11 11:36:31 +05:30
Rohit Waghchaure
f3c70a66b5 fix: Serial/Batches not fetching when creating Material Transfer from Purchase Receipt
(cherry picked from commit d16c50486a)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
2025-12-11 05:29:24 +00:00
barredterra
115fd48bbf Merge remote-tracking branch 'upstream/version-15-hotfix' into mergify/bp/version-15-hotfix/pr-49875 2025-12-10 13:28:30 +01:00
Sudharsanan11
918f8ca79b fix(stock): remove total bar in chart view
(cherry picked from commit 198eb372e3)
2025-12-10 09:42:17 +00:00
mergify[bot]
46ca347578 perf: move all hourly/daily jobs to maintenance queue (backport #47504) (#51005)
perf: move all hourly/daily jobs to maintenance queue (#47504)

None of them need to strictly happen at 00:00 or *:00, so moving them all to maintenance queue which executes with same frequency but spaced out.

(cherry picked from commit a50251401f)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2025-12-10 06:19:50 +00:00
Diptanil Saha
8226502956 Merge pull request #51003 from frappe/mergify/bp/version-15-hotfix/pr-51001
fix(share balance): use currency field instead of int for rate and amount (backport #51001)
2025-12-10 10:38:42 +05:30
Mihir Kandoi
71e537b030 Merge pull request #51004 from frappe/mergify/bp/version-15-hotfix/pr-50952
fix: precision issue on job card submission (backport #50952)
2025-12-10 10:08:00 +05:30
Diptanil Saha
8fd3e8e22e chore: resolve conflict 2025-12-10 09:59:04 +05:30
Dany Robert
4ee4a57f72 fix: precision issue on job card submission
(cherry picked from commit 80730908c9)
2025-12-10 04:22:20 +00:00
diptanilsaha
a8ed2815a4 fix(share balance): use currency field instead of int for rate and amount
(cherry picked from commit 2fe5fad884)

# Conflicts:
#	erpnext/accounts/doctype/share_balance/share_balance.json
2025-12-10 04:21:07 +00:00
Frappe PR Bot
a2b676b340 chore(release): Bumped to Version 15.91.1
## [15.91.1](https://github.com/frappe/erpnext/compare/v15.91.0...v15.91.1) (2025-12-09)

### Bug Fixes

* add return status for delivery note ([ebb6296](ebb62966d3))
* Adjust asset purchase amounts based on docstatus ([a31fb2a](a31fb2ac6c))
* change is_return value in filter from Yes to 1 ([52e26b6](52e26b6da8))
* conflicts ([bd00a48](bd00a484ea))
* conflicts ([1427b4a](1427b4ac3f))
* cost center not reset ([8a3148e](8a3148eee6))
* ensure payment request button only shows for submitted invoices ([b4053ee](b4053ee0d8))
* fg qty uom in manufacture entry ([70d5726](70d57260d6))
* handle duplicate description in item-wise report ([1a278e7](1a278e7ca0))
* include return invoice discount in discount validation ([bf1c606](bf1c606610))
* incorrect condition ([d9e9f35](d9e9f35230))
* inward same serial / batches in disassembly which were used ([cfbd716](cfbd71693b))
* LCV is not changing the valuation of the repacked item ([8b22d9d](8b22d9d95e))
* missing attribute error when restoring asset ([bde209b](bde209b077))
* performance of the reposting ([8d734df](8d734df63b))
* **picklist:** calculate picked qty excluding the delivered qty ([3785ffe](3785ffe5c9))
* quality inspection showing Not Saved ([abe599a](abe599a49d))
* remove comment ([da88196](da88196a89))
* remove set_only_once from is_fixed_asset ([fd6e42e](fd6e42e15e))
* **sales invoice:** 100% additional discount gl issue with discount accounting ([bd6210a](bd6210a212))
* tds for customer and supplier in Journal Entry (backport [#49963](https://github.com/frappe/erpnext/issues/49963)) ([#50985](https://github.com/frappe/erpnext/issues/50985)) ([f2c556a](f2c556a6cc))
* untranslated string in job card ([b2f6d07](b2f6d07c25))
* variant items not fetched while making BOM for Variant Item ([176ce0d](176ce0d4d6))
2025-12-09 17:00:25 +00:00
Diptanil Saha
691db5b877 Merge pull request #50981 from frappe/version-15-hotfix 2025-12-09 22:28:48 +05:30
Diptanil Saha
7bec3d19ac Merge pull request #50977 from ljain112/fix-item-wise-sales-register
fix: handle duplicate description in item-wise report (backport #50979)
2025-12-09 21:47:16 +05:30
Diptanil Saha
3f85aa3aea Merge pull request #50997 from frappe/mergify/bp/version-15-hotfix/pr-50944
fix: include return invoice discount in discount validation (backport #50944)
2025-12-09 21:39:17 +05:30
Diptanil Saha
9ccf4900fe chore: resolve conflict 2025-12-09 20:52:33 +05:30
ravibharathi656
bf1c606610 fix: include return invoice discount in discount validation
(cherry picked from commit fab1ef5d76)

# Conflicts:
#	erpnext/controllers/taxes_and_totals.py
2025-12-09 15:18:08 +00:00
Mihir Kandoi
6c53d31f2d Merge pull request #50994 from frappe/mergify/bp/version-15-hotfix/pr-50912
fix: add return status for delivery note (backport #50912)
2025-12-09 20:07:22 +05:30
Mihir Kandoi
4de1af498b chore: resolve conflicts 2025-12-09 19:51:26 +05:30
Mihir Kandoi
c65409c348 Merge pull request #50993 from frappe/mergify/bp/version-15-hotfix/pr-50910
fix: validate picklist partial reserved qty (backport #50910)
2025-12-09 18:50:37 +05:30
Pugazhendhi Velu
422aec12cb test: add test for return status in delivery note
(cherry picked from commit 445a255a7f)
2025-12-09 13:06:52 +00:00
Pugazhendhi Velu
52e26b6da8 fix: change is_return value in filter from Yes to 1
(cherry picked from commit af212f520d)
2025-12-09 13:06:52 +00:00
Pugazhendhi Velu
ebb62966d3 fix: add return status for delivery note
(cherry picked from commit dec67eecad)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/delivery_note.py
2025-12-09 13:06:51 +00:00
Sudharsanan11
b05e2910d8 test(picklist): add test for reserved qty after partial delivery
(cherry picked from commit 758553b9fc)
2025-12-09 13:04:46 +00:00
Sudharsanan11
3785ffe5c9 fix(picklist): calculate picked qty excluding the delivered qty
(cherry picked from commit f5b75b27d7)
2025-12-09 13:04:45 +00:00
Diptanil Saha
a4ab198042 Merge pull request #50991 from frappe/mergify/bp/version-15-hotfix/pr-50970
fix: ensure payment request button only shows for submitted invoices (backport #50970)
2025-12-09 17:21:53 +05:30
Diptanil Saha
67c5249b38 Merge pull request #50988 from frappe/mergify/bp/version-15-hotfix/pr-50968 2025-12-09 17:18:10 +05:30
Diptanil Saha
af067d1c00 chore: resolve conflict 2025-12-09 17:17:27 +05:30
Abdeali Chharchhoda
b4053ee0d8 fix: ensure payment request button only shows for submitted invoices
(cherry picked from commit f26ee9e546)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
2025-12-09 11:42:22 +00:00
rohitwaghchaure
9f846e2636 Merge pull request #50990 from frappe/mergify/bp/version-15-hotfix/pr-50978
fix: performance of the reposting (backport #50978)
2025-12-09 16:54:21 +05:30
Rohit Waghchaure
8d734df63b fix: performance of the reposting
(cherry picked from commit 1bcfad8eb1)
2025-12-09 11:06:16 +00:00
Abdeali Chharchhoda
0998123e52 refactor: payment request status updates with bulk database operation
(cherry picked from commit 5154fa8259)
2025-12-09 10:59:11 +00:00
mergify[bot]
f2c556a6cc fix: tds for customer and supplier in Journal Entry (backport #49963) (#50985)
Co-authored-by: ljain112 <ljain112@gmail.com>
Co-authored-by: Smit Vora <smitvora203@gmail.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-12-09 16:26:36 +05:30
Khushi Rawat
b41612bea8 Merge pull request #50982 from khushi8112/missing-attribute-issue
fix: Missing attribute error
2025-12-09 15:31:08 +05:30
khushi8112
da88196a89 fix: remove comment 2025-12-09 15:13:40 +05:30
khushi8112
bde209b077 fix: missing attribute error when restoring asset 2025-12-09 15:10:16 +05:30
ljain112
1a278e7ca0 fix: handle duplicate description in item-wise report 2025-12-09 12:03:50 +05:30
rohitwaghchaure
1637cb4168 Merge pull request #50973 from frappe/mergify/bp/version-15-hotfix/pr-50972
fix: incorrect condition (backport #50972)
2025-12-08 20:27:10 +05:30
Rohit Waghchaure
d9e9f35230 fix: incorrect condition
(cherry picked from commit 264baf34f6)
2025-12-08 14:39:16 +00:00
rohitwaghchaure
cbc73148d3 Merge pull request #50969 from frappe/mergify/bp/version-15-hotfix/pr-50742
fix: inward same serial / batches in disassembly which were used (backport #50742)
2025-12-08 19:53:36 +05:30
rohitwaghchaure
60a18247e1 chore: fix conflicts 2025-12-08 19:09:52 +05:30
rohitwaghchaure
7cc0436083 chore: fix conflicts 2025-12-08 19:08:45 +05:30
rohitwaghchaure
f8eb48472e chore: fix conflicts 2025-12-08 19:07:13 +05:30
rohitwaghchaure
8074d396d0 chore: fix conflicts
Removed posting_datetime and type_of_transaction from the query.
2025-12-08 19:05:51 +05:30
Rohit Waghchaure
cfbd71693b fix: inward same serial / batches in disassembly which were used
(cherry picked from commit 95e6c72539)

# Conflicts:
#	erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2025-12-08 12:57:58 +00:00
Diptanil Saha
3a2d7d18a3 Merge pull request #50946 from frappe/mergify/bp/version-15-hotfix/pr-50931
fix(bulk transaction process): skip records creation if original records are marked 'On Hold' or 'Closed' (backport #50931)
2025-12-05 16:56:52 +05:30
Diptanil Saha
b55cefc54f Merge pull request #50945 from frappe/mergify/bp/version-15-hotfix/pr-50943
fix(sales invoice): 100% additional discount gl issue with discount accounting (backport #50943)
2025-12-05 16:48:44 +05:30
Diptanil Saha
05778bb81a chore: resolve conflict 2025-12-05 16:40:57 +05:30
Diptanil Saha
a70296e9b5 Merge pull request #50931 from diptanilsaha/gh-49357
(cherry picked from commit 31d55248e4)

# Conflicts:
#	erpnext/utilities/bulk_transaction.py
2025-12-05 11:03:16 +00:00
diptanilsaha
bd6210a212 fix(sales invoice): 100% additional discount gl issue with discount accounting
(cherry picked from commit d6bdbfe266)
2025-12-05 11:02:16 +00:00
Khushi Rawat
944c9ad0b3 Merge pull request #50924 from frappe/mergify/bp/version-15-hotfix/pr-50879
fix: remove set_only_once from is_fixed_asset field (backport #50879)
2025-12-04 13:04:50 +05:30
Khushi Rawat
bd00a484ea fix: conflicts 2025-12-04 12:48:50 +05:30
Khushi Rawat
1427b4ac3f fix: conflicts 2025-12-04 12:48:07 +05:30
ravibharathi656
fd6e42e15e fix: remove set_only_once from is_fixed_asset
(cherry picked from commit 70521fb9bf)

# Conflicts:
#	erpnext/stock/doctype/item/item.json
#	erpnext/stock/doctype/item/item.py
2025-12-04 06:44:46 +00:00
rohitwaghchaure
8b071c0d22 Merge pull request #50922 from frappe/mergify/bp/version-15-hotfix/pr-50913
fix: variant items not fetched while making BOM for Variant Item (backport #50913)
2025-12-04 11:43:57 +05:30
Rohit Waghchaure
176ce0d4d6 fix: variant items not fetched while making BOM for Variant Item
(cherry picked from commit a0256bd798)
2025-12-04 04:38:17 +00:00
rohitwaghchaure
dd888fc30a Merge pull request #50909 from frappe/mergify/bp/version-15-hotfix/pr-50905
fix: LCV is not changing the valuation of the repacked item (backport #50905)
2025-12-04 10:06:31 +05:30
Mihir Kandoi
789adaeabe Merge pull request #50908 from frappe/mergify/bp/version-15-hotfix/pr-50906
fix: untranslated string in job card (backport #50906)
2025-12-03 20:05:11 +05:30
rohitwaghchaure
2342f8d710 chore: fix conflicts
Removed test for purchase expense account and repost GL entries.
2025-12-03 18:38:40 +05:30
Rohit Waghchaure
8b22d9d95e fix: LCV is not changing the valuation of the repacked item
(cherry picked from commit ccbbc60585)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
2025-12-03 12:58:22 +00:00
rohitwaghchaure
626c799b60 Merge pull request #50907 from frappe/mergify/bp/version-15-hotfix/pr-50902
fix: fg qty uom in manufacture entry (backport #50902)
2025-12-03 18:22:15 +05:30
Mihir Kandoi
b2f6d07c25 fix: untranslated string in job card
(cherry picked from commit ec06f4a71b)
2025-12-03 12:40:35 +00:00
Mihir Kandoi
70d57260d6 fix: fg qty uom in manufacture entry
(cherry picked from commit d9a377108c)
2025-12-03 12:37:19 +00:00
Khushi Rawat
9062b90237 Merge pull request #50887 from 0xD0M1M0/patch-1
fix: Reduce asset value on asset capitalization cancelation
2025-12-03 15:01:41 +05:30
rohitwaghchaure
40467bc26c Merge pull request #50901 from frappe/mergify/bp/version-15-hotfix/pr-50896
fix: quality inspection showing Not Saved (backport #50896)
2025-12-03 14:52:51 +05:30
Rohit Waghchaure
abe599a49d fix: quality inspection showing Not Saved
(cherry picked from commit 3f78d6afed)
2025-12-03 08:08:37 +00:00
rohitwaghchaure
9975f5fe69 Merge pull request #50889 from frappe/mergify/bp/version-15-hotfix/pr-50888
fix: cost center not reset (backport #50888)
2025-12-02 22:37:08 +05:30
Rohit Waghchaure
8a3148eee6 fix: cost center not reset
(cherry picked from commit 29f2ecbd6f)
2025-12-02 16:30:41 +00:00
0xD0M1M0
a31fb2ac6c fix: Adjust asset purchase amounts based on docstatus
allows cancelation
2025-12-02 16:09:56 +01:00
barredterra
2c13c4746b Merge remote-tracking branch 'upstream/version-15-hotfix' into mergify/bp/version-15-hotfix/pr-49875 2025-11-21 00:46:43 +01:00
barredterra
ec3a226a83 fix: mark navbar item as translatable 2025-11-15 19:55:59 +01:00
barredterra
19dc26ea16 revert: changes to install_fixtures
I think this would be too breaking. Custom apps might expect the translated data to exist.
2025-11-15 19:51:05 +01:00
barredterra
e29a384f90 chore: resolve conflicts 2025-11-15 19:39:40 +01:00
Raffael Meyer
088bbac543 fix: use dummy translations for custom field labels (#49875)
(cherry picked from commit 9a989a84fb)

# Conflicts:
#	erpnext/setup/install.py
#	erpnext/setup/setup_wizard/operations/install_fixtures.py
2025-11-15 18:34:57 +00:00
78 changed files with 1525 additions and 448 deletions

View File

@@ -4,7 +4,7 @@ import inspect
import frappe
from frappe.utils.user import is_website_user
__version__ = "15.91.0"
__version__ = "15.92.1"
def get_default_company(user=None):

View File

@@ -38,7 +38,10 @@
"column_break_3czf",
"bank_party_name",
"bank_party_account_number",
"bank_party_iban"
"bank_party_iban",
"extended_bank_statement_section",
"included_fee",
"excluded_fee"
],
"fields": [
{
@@ -233,12 +236,32 @@
{
"fieldname": "column_break_oufv",
"fieldtype": "Column Break"
},
{
"fieldname": "extended_bank_statement_section",
"fieldtype": "Section Break",
"label": "Extended Bank Statement"
},
{
"fieldname": "included_fee",
"fieldtype": "Currency",
"label": "Included Fee",
"non_negative": 1,
"options": "currency"
},
{
"description": "On save, the Excluded Fee will be converted to an Included Fee.",
"fieldname": "excluded_fee",
"fieldtype": "Currency",
"label": "Excluded Fee",
"non_negative": 1,
"options": "currency"
}
],
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2025-10-23 17:32:58.514807",
"modified": "2025-12-07 20:49:18.600757",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -32,6 +32,8 @@ class BankTransaction(Document):
date: DF.Date | None
deposit: DF.Currency
description: DF.SmallText | None
excluded_fee: DF.Currency
included_fee: DF.Currency
naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
party: DF.DynamicLink | None
party_type: DF.Link | None
@@ -45,9 +47,11 @@ class BankTransaction(Document):
# end: auto-generated types
def before_validate(self):
self.handle_excluded_fee()
self.update_allocated_amount()
def validate(self):
self.validate_included_fee()
self.validate_duplicate_references()
self.validate_currency()
@@ -307,6 +311,40 @@ class BankTransaction(Document):
self.party_type, self.party = result
def validate_included_fee(self):
"""
The included_fee is only handled for withdrawals. An included_fee for a deposit, is not credited to the account and is
therefore outside of the deposit value and can be larger than the deposit itself.
"""
if self.included_fee and self.withdrawal:
if self.included_fee > self.withdrawal:
frappe.throw(_("Included fee is bigger than the withdrawal itself."))
def handle_excluded_fee(self):
# Include the excluded fee on validate to handle all further processing the same
excluded_fee = flt(self.excluded_fee)
if excluded_fee <= 0:
return
# Suppress a negative deposit (aka withdrawal), likely not intendend
if flt(self.deposit) > 0 and (flt(self.deposit) - excluded_fee) < 0:
frappe.throw(_("The Excluded Fee is bigger than the Deposit it is deducted from."))
# Enforce directionality
if flt(self.deposit) > 0 and flt(self.withdrawal) > 0:
frappe.throw(
_("Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee.")
)
if flt(self.deposit) > 0:
self.deposit = flt(self.deposit) - excluded_fee
# A fee applied to deposit and withdrawal equal 0 become a withdrawal
elif flt(self.withdrawal) >= 0:
self.withdrawal = flt(self.withdrawal) + excluded_fee
self.included_fee = flt(self.included_fee) + excluded_fee
self.excluded_fee = 0
@frappe.whitelist()
def get_doctypes_for_bank_reconciliation():

View File

@@ -0,0 +1,133 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
class TestBankTransactionFees(FrappeTestCase):
def test_included_fee_throws(self):
"""A fee that's part of a withdrawal cannot be bigger than the
withdrawal itself."""
bt = frappe.new_doc("Bank Transaction")
bt.withdrawal = 100
bt.included_fee = 101
self.assertRaises(frappe.ValidationError, bt.validate_included_fee)
def test_included_fee_allows_equal(self):
"""A fee that's part of a withdrawal may be equal to the withdrawal
amount (only the fee was deducted from the account)."""
bt = frappe.new_doc("Bank Transaction")
bt.withdrawal = 100
bt.included_fee = 100
bt.validate_included_fee()
def test_included_fee_allows_for_deposit(self):
"""For deposits, a fee may be recorded separately without limiting the
received amount."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.included_fee = 999
bt.validate_included_fee()
def test_excluded_fee_noop_when_zero(self):
"""When there is no excluded fee to apply, the amounts should remain
unchanged."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 100
bt.withdrawal = 0
bt.included_fee = 5
bt.excluded_fee = 0
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 100)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_throws_when_exceeds_deposit(self):
"""A fee deducted from an incoming payment must not exceed the incoming
amount (else it would be a withdrawal, a conversion we don't support)."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.excluded_fee = 11
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
def test_excluded_fee_throws_when_both_deposit_and_withdrawal_are_set(self):
"""A transaction must be either incoming or outgoing when applying a
fee, not both."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 10
bt.withdrawal = 10
bt.excluded_fee = 1
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
def test_excluded_fee_deducts_from_deposit(self):
"""When a fee is deducted from an incoming payment, the net received
amount decreases and the fee is tracked as included."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 100
bt.withdrawal = 0
bt.included_fee = 2
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 95)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 7)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_can_reduce_an_incoming_payment_to_zero(self):
"""A separately-deducted fee may reduce an incoming payment to zero,
while still tracking the fee."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 5
bt.withdrawal = 0
bt.included_fee = 0
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 0)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_increases_outgoing_payment(self):
"""When a separately-deducted fee is provided for an outgoing payment,
the total money leaving increases and the fee is tracked."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 0
bt.withdrawal = 100
bt.included_fee = 2
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 105)
self.assertEqual(bt.included_fee, 7)
self.assertEqual(bt.excluded_fee, 0)
def test_excluded_fee_turns_zero_amount_into_withdrawal(self):
"""If only an excluded fee is provided, it should be treated as an
outgoing payment and the fee is then tracked as included."""
bt = frappe.new_doc("Bank Transaction")
bt.deposit = 0
bt.withdrawal = 0
bt.included_fee = 0
bt.excluded_fee = 5
bt.handle_excluded_fee()
self.assertEqual(bt.deposit, 0)
self.assertEqual(bt.withdrawal, 5)
self.assertEqual(bt.included_fee, 5)
self.assertEqual(bt.excluded_fee, 0)

View File

@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
to: "{to_currency}",
};
add_param(frm, r.message, params, result);
} else if (frm.doc.service_provider == "frankfurter.dev") {
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",

View File

@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
self.append("req_params", {"key": "from", "value": "{from_currency}"})
self.append("req_params", {"key": "to", "value": "{to_currency}"})
elif self.service_provider == "frankfurter.dev":
elif self.service_provider in ("frankfurter.dev", "frankfurter.app"):
self.set("result_key", [])
self.set("req_params", [])
@@ -105,9 +105,11 @@ class CurrencyExchangeSettings(Document):
@frappe.whitelist()
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev"]:
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "api.frankfurter.app/{transaction_date}"
elif service_provider == "frankfurter.dev":
api = "api.frankfurter.dev/v1/{transaction_date}"

View File

@@ -486,6 +486,9 @@ class ExchangeRateRevaluation(Document):
journal_entry.posting_date = self.posting_date
journal_entry.multi_currency = 1
# Prevent JE from overriding user-entered exchange rates (e.g., rate of 1)
journal_entry.flags.ignore_exchange_rate = True
journal_entry_accounts = []
for d in accounts:
if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):

View File

@@ -33,6 +33,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
get_depr_schedule,
)
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
class StockAccountInvalidTransaction(frappe.ValidationError):
@@ -273,93 +274,7 @@ class JournalEntry(AccountsController):
)
def apply_tax_withholding(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
return
parties = [d.party for d in self.get("accounts") if d.party]
parties = list(set(parties))
if len(parties) > 1:
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
account_type_map = get_account_type_map(self.company)
party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
debit_or_credit = (
"debit_in_account_currency"
if self.voucher_type == "Credit Note"
else "credit_in_account_currency"
)
rev_debit_or_credit = (
"credit_in_account_currency"
if debit_or_credit == "debit_in_account_currency"
else "debit_in_account_currency"
)
party_account = get_party_account(party_type.title(), parties[0], self.company)
net_total = sum(
d.get(debit_or_credit)
for d in self.get("accounts")
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
)
party_amount = sum(
d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
)
inv = frappe._dict(
{
party_type: parties[0],
"doctype": doctype,
"company": self.company,
"posting_date": self.posting_date,
"net_total": net_total,
}
)
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
inv, self.tax_withholding_category
)
if not tax_withholding_details:
return
accounts = []
for d in self.get("accounts"):
if d.get("account") == tax_withholding_details.get("account_head"):
d.update(
{
"account": tax_withholding_details.get("account_head"),
debit_or_credit: tax_withholding_details.get("tax_amount"),
}
)
accounts.append(d.get("account"))
if d.get("account") == party_account:
d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
if not accounts or tax_withholding_details.get("account_head") not in accounts:
self.append(
"accounts",
{
"account": tax_withholding_details.get("account_head"),
rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
"against_account": parties[0],
},
)
to_remove = [
d
for d in self.get("accounts")
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
]
for d in to_remove:
self.remove(d)
JournalEntryTaxWithholding(self).apply()
def update_asset_value(self):
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
@@ -1281,6 +1196,230 @@ class JournalEntry(AccountsController):
frappe.throw(_("Accounts table cannot be blank."))
class JournalEntryTaxWithholding:
def __init__(self, journal_entry):
self.doc: JournalEntry = journal_entry
self.party = None
self.party_type = None
self.party_account = None
self.party_row = None
self.existing_tds_rows = []
self.precision = None
self.has_multiple_parties = False
# Direction fields based on party type
self.party_field = None # "credit" for Supplier, "debit" for Customer
self.reverse_field = None # opposite of party_field
def apply(self):
if not self._set_party_info():
return
self._setup_direction_fields()
self._reset_existing_tds()
if not self._should_apply_tds():
self._cleanup_duplicate_tds_rows(None)
return
if self.has_multiple_parties:
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
net_total = self._calculate_net_total()
if net_total <= 0:
return
tds_details = self._get_tds_details(net_total)
if not tds_details or not tds_details.get("tax_amount"):
return
self._create_or_update_tds_row(tds_details)
self._update_party_amount(tds_details.get("tax_amount"), is_reversal=False)
self._recalculate_totals()
def _should_apply_tds(self):
return self.doc.apply_tds and self.doc.voucher_type in ("Debit Note", "Credit Note")
def _set_party_info(self):
for row in self.doc.get("accounts"):
if row.party_type in ("Customer", "Supplier") and row.party:
if self.party and row.party != self.party:
self.has_multiple_parties = True
if not self.party:
self.party = row.party
self.party_type = row.party_type
self.party_account = row.account
self.party_row = row
if row.get("is_tax_withholding_account"):
self.existing_tds_rows.append(row)
return bool(self.party)
def _setup_direction_fields(self):
"""
For Supplier (TDS): party has credit, TDS reduces credit
For Customer (TCS): party has debit, TCS increases debit
"""
if self.party_type == "Supplier":
self.party_field = "credit"
self.reverse_field = "debit"
else: # Customer
self.party_field = "debit"
self.reverse_field = "credit"
self.precision = self.doc.precision(self.party_field, self.party_row)
def _reset_existing_tds(self):
for row in self.existing_tds_rows:
# TDS amount is always in credit (liability to government)
tds_amount = flt(row.get("credit") - row.get("debit"), self.precision)
if not tds_amount:
continue
self._update_party_amount(tds_amount, is_reversal=True)
# zero_out_tds_row
row.update(
{
"credit": 0,
"credit_in_account_currency": 0,
"debit": 0,
"debit_in_account_currency": 0,
}
)
def _update_party_amount(self, amount, is_reversal=False):
amount = flt(amount, self.precision)
amount_in_party_currency = flt(amount / self.party_row.get("exchange_rate", 1), self.precision)
# Determine which field the party amount is in
active_field = self.party_field if self.party_row.get(self.party_field) else self.reverse_field
# If amount is in reverse field, flip the signs
if active_field == self.reverse_field:
amount = -amount
amount_in_party_currency = -amount_in_party_currency
# Direction multiplier based on party type:
# Customer (TCS): +1 (add to debit)
# Supplier (TDS): -1 (subtract from credit)
direction = 1 if self.party_type == "Customer" else -1
# Reversal inverts the direction
if is_reversal:
direction = -direction
adjustment = amount * direction
adjustment_in_party_currency = amount_in_party_currency * direction
active_field_account_currency = f"{active_field}_in_account_currency"
self.party_row.update(
{
active_field: flt(self.party_row.get(active_field) + adjustment, self.precision),
active_field_account_currency: flt(
self.party_row.get(active_field_account_currency) + adjustment_in_party_currency,
self.precision,
),
}
)
def _calculate_net_total(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
account_type_map = get_account_type_map(self.doc.company)
return flt(
sum(
d.get(self.reverse_field) - d.get(self.party_field)
for d in self.doc.get("accounts")
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
and d.account != self.party_account
and not d.get("is_tax_withholding_account")
),
self.precision,
)
def _get_tds_details(self, net_total):
return get_party_tax_withholding_details(
frappe._dict(
{
"party_type": self.party_type,
"party": self.party,
"doctype": self.doc.doctype,
"company": self.doc.company,
"posting_date": self.doc.posting_date,
"tax_withholding_net_total": net_total,
"base_tax_withholding_net_total": net_total,
"grand_total": net_total,
}
),
self.doc.tax_withholding_category,
)
def _create_or_update_tds_row(self, tds_details):
tax_account = tds_details.get("account_head")
account_currency = get_account_currency(tax_account)
company_currency = frappe.get_cached_value("Company", self.doc.company, "default_currency")
exchange_rate = _get_exchange_rate(account_currency, company_currency, self.doc.posting_date)
tax_amount = flt(tds_details.get("tax_amount"), self.precision)
tax_amount_in_account_currency = flt(tax_amount / exchange_rate, self.precision)
# Find existing TDS row for this account
tax_row = None
for row in self.doc.get("accounts"):
if row.account == tax_account and row.get("is_tax_withholding_account"):
tax_row = row
break
if not tax_row:
tax_row = self.doc.append(
"accounts",
{
"account": tax_account,
"account_currency": account_currency,
"exchange_rate": exchange_rate,
"cost_center": tds_details.get("cost_center"),
"credit": 0,
"credit_in_account_currency": 0,
"debit": 0,
"debit_in_account_currency": 0,
"is_tax_withholding_account": 1,
},
)
# TDS/TCS is always credited (liability to government)
tax_row.update(
{
"credit": tax_amount,
"credit_in_account_currency": tax_amount_in_account_currency,
"debit": 0,
"debit_in_account_currency": 0,
}
)
self._cleanup_duplicate_tds_rows(tax_row)
def _cleanup_duplicate_tds_rows(self, current_tax_row):
rows_to_remove = [
row
for row in self.doc.get("accounts")
if row.get("is_tax_withholding_account") and row != current_tax_row
]
for row in rows_to_remove:
self.doc.remove(row)
def _recalculate_totals(self):
self.doc.set_amounts_in_company_currency()
self.doc.set_total_debit_credit()
self.doc.set_against_account()
@frappe.whitelist()
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
@@ -1649,8 +1788,6 @@ def get_exchange_rate(
credit=None,
exchange_rate=None,
):
from erpnext.setup.utils import get_exchange_rate
account_details = frappe.get_cached_value(
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
)
@@ -1672,8 +1809,8 @@ def get_exchange_rate(
# The date used to retreive the exchange rate here is the date passed
# in as an argument to this function.
elif (not exchange_rate or flt(exchange_rate) == 1) and account_currency and posting_date:
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
else:
exchange_rate = 1

View File

@@ -34,6 +34,7 @@
"reference_detail_no",
"advance_voucher_type",
"advance_voucher_no",
"is_tax_withholding_account",
"col_break3",
"is_advance",
"user_remark",
@@ -282,12 +283,19 @@
"options": "advance_voucher_type",
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"fieldname": "is_tax_withholding_account",
"fieldtype": "Check",
"label": "Is Tax Withholding Account",
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2025-10-27 13:48:32.805100",
"modified": "2025-11-27 12:23:33.157655",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",

View File

@@ -28,6 +28,7 @@ class JournalEntryAccount(Document):
debit_in_account_currency: DF.Currency
exchange_rate: DF.Float
is_advance: DF.Literal["No", "Yes"]
is_tax_withholding_account: DF.Check
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data

View File

@@ -1302,15 +1302,14 @@ frappe.ui.form.on("Payment Entry", {
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
if (!row) {
const response = await get_company_defaults(frm.doc.company);
const company_defaults = frappe.get_doc(":Company", frm.doc.company);
const account =
response.message?.[account_fieldname] ||
company_defaults?.[account_fieldname] ||
(await prompt_for_missing_account(frm, account_fieldname));
row = frm.add_child("deductions");
row.account = account;
row.cost_center = response.message?.cost_center;
row.cost_center = company_defaults?.cost_center;
row.is_exchange_gain_loss = 1;
}

View File

@@ -545,6 +545,9 @@ def make_payment_request(**args):
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
if args.dn and not isinstance(args.dn, str):
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
if not args.get("company"):
args.company = ref_doc.company
@@ -850,6 +853,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
)
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
doc_updates = {}
for ref in references:
if not ref.payment_request:
@@ -875,7 +879,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
title=_("Invalid Allocated Amount"),
)
# update status
# determine status
if new_outstanding_amount == payment_request["grand_total"]:
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
elif new_outstanding_amount == 0:
@@ -883,12 +887,15 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
elif new_outstanding_amount > 0:
status = "Partially Paid"
# update database
frappe.db.set_value(
"Payment Request",
ref.payment_request,
{"outstanding_amount": new_outstanding_amount, "status": status},
)
# prepare bulk update data
doc_updates[ref.payment_request] = {
"outstanding_amount": new_outstanding_amount,
"status": status,
}
# bulk update all payment requests
if doc_updates:
frappe.db.bulk_update("Payment Request", doc_updates)
def get_dummy_message(doc):

View File

@@ -126,8 +126,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
}
if (doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
cur_frm.add_custom_button(
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
this.frm.add_custom_button(
__("Payment Request"),
function () {
me.make_payment_request();
@@ -575,17 +575,6 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
};
};
cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
var d = locals[cdt][cdn];
if (d.cost_center) {
var cl = doc.items || [];
for (var i = 0; i < cl.length; i++) {
if (!cl[i].cost_center) cl[i].cost_center = d.cost_center;
}
}
refresh_field("items");
};
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
return {
filters: [["Project", "status", "not in", "Completed, Cancelled"]],

View File

@@ -648,10 +648,6 @@ cur_frm.cscript.expense_account = function (doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "expense_account");
};
cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "cost_center");
};
cur_frm.set_query("debit_to", function (doc) {
return {
filters: {

View File

@@ -1349,7 +1349,11 @@ class SalesInvoice(SellingController):
)
for item in self.get("items"):
if flt(item.base_net_amount, item.precision("base_net_amount")) or item.is_fixed_asset:
if (
flt(item.base_net_amount, item.precision("base_net_amount"))
or item.is_fixed_asset
or enable_discount_accounting
):
# Do not book income for transfer within same company
if self.is_internal_transfer():
continue

View File

@@ -80,7 +80,7 @@
"collapsible": 0,
"columns": 0,
"fieldname": "rate",
"fieldtype": "Int",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -102,7 +102,7 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -199,7 +199,7 @@
"collapsible": 0,
"columns": 0,
"fieldname": "amount",
"fieldtype": "Int",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
@@ -221,7 +221,7 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
@@ -324,7 +324,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-01-10 18:32:36.201124",
"modified": "2025-12-10 08:06:40.611761",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Share Balance",
@@ -339,4 +339,4 @@
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}
}

View File

@@ -14,7 +14,7 @@ class ShareBalance(Document):
if TYPE_CHECKING:
from frappe.types import DF
amount: DF.Int
amount: DF.Currency
current_state: DF.Literal["", "Issued", "Purchased"]
from_no: DF.Int
is_company: DF.Check
@@ -22,7 +22,7 @@ class ShareBalance(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
rate: DF.Int
rate: DF.Currency
share_type: DF.Link
to_no: DF.Int
# end: auto-generated types

View File

@@ -85,6 +85,9 @@ def get_party_details(inv):
if inv.doctype == "Sales Invoice":
party_type = "Customer"
party = inv.customer
elif inv.doctype == "Journal Entry":
party_type = inv.party_type
party = inv.party
else:
party_type = "Supplier"
party = inv.supplier
@@ -155,7 +158,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
party_type, parties, inv, tax_details, posting_date, pan_no
)
if party_type == "Supplier":
if party_type == "Supplier" or inv.doctype == "Journal Entry":
tax_row = get_tax_row_for_tds(tax_details, tax_amount)
else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
@@ -346,7 +349,10 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
elif party_type == "Customer":
if tax_deducted:
# if already TCS is charged, then amount will be calculated based on 'Previous Row Total'
tax_amount = 0
if inv.doctype == "Sales Invoice":
tax_amount = 0
else:
tax_amount = inv.base_tax_withholding_net_total * tax_details.rate / 100
else:
# if no TCS has been charged in FY,
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
@@ -718,7 +724,7 @@ def get_advance_adjusted_in_invoice(inv):
def get_invoice_total_without_tcs(inv, tax_details):
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
tcs_tax_row = [d for d in inv.get("taxes") or [] if d.account_head == tax_details.account_head]
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
return inv.grand_total - tcs_tax_row_amount

View File

@@ -848,6 +848,90 @@ class TestTaxWithholdingCategory(FrappeTestCase):
self.assertEqual(payment.taxes[0].tax_amount, 6000)
self.assertEqual(payment.taxes[0].allocated_amount, 6000)
def test_tds_on_journal_entry_for_supplier(self):
"""Test TDS deduction for Supplier in Debit Note"""
frappe.db.set_value(
"Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS"
)
jv = make_journal_entry_with_tax_withholding(
party_type="Supplier",
party="Test TDS Supplier",
voucher_type="Debit Note",
amount=50000,
save=False,
)
jv.apply_tds = 1
jv.tax_withholding_category = "Cumulative Threshold TDS"
jv.save()
# Again saving should not change tds amount
jv.user_remark = "Test TDS on Journal Entry for Supplier"
jv.save()
jv.submit()
# TDS = 50000 * 10% = 5000
self.assertEqual(len(jv.accounts), 3)
# Find TDS account row
tds_row = None
supplier_row = None
for row in jv.accounts:
if row.account == "TDS - _TC":
tds_row = row
elif row.party == "Test TDS Supplier":
supplier_row = row
self.assertEqual(tds_row.credit, 5000)
self.assertEqual(tds_row.debit, 0)
# Supplier amount should be reduced by TDS
self.assertEqual(supplier_row.credit, 45000)
jv.cancel()
def test_tcs_on_journal_entry_for_customer(self):
"""Test TCS collection for Customer in Credit Note"""
frappe.db.set_value(
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
)
# Create Credit Note with amount exceeding threshold
jv = make_journal_entry_with_tax_withholding(
party_type="Customer",
party="Test TCS Customer",
voucher_type="Credit Note",
amount=50000,
save=False,
)
jv.apply_tds = 1
jv.tax_withholding_category = "Cumulative Threshold TCS"
jv.save()
# Again saving should not change tds amount
jv.user_remark = "Test TCS on Journal Entry for Customer"
jv.save()
jv.submit()
# Assert TCS calculation (10% on amount above threshold of 30000)
self.assertEqual(len(jv.accounts), 3)
# Find TCS account row
tcs_row = None
customer_row = None
for row in jv.accounts:
if row.account == "TCS - _TC":
tcs_row = row
elif row.party == "Test TCS Customer":
customer_row = row
# TCS should be credited (liability to government)
self.assertEqual(tcs_row.credit, 2000) # above threshold 20000*10%
self.assertEqual(tcs_row.debit, 0)
# Customer amount should be increased by TCS
self.assertEqual(customer_row.debit, 52000)
jv.cancel()
def cancel_invoices():
purchase_invoices = frappe.get_all(
@@ -996,6 +1080,88 @@ def create_payment_entry(**args):
return pe
def make_journal_entry_with_tax_withholding(
party_type,
party,
voucher_type,
amount,
cost_center=None,
posting_date=None,
save=True,
submit=False,
):
"""Helper function to create Journal Entry for tax withholding"""
if not cost_center:
cost_center = "_Test Cost Center - _TC"
jv = frappe.new_doc("Journal Entry")
jv.posting_date = posting_date or today()
jv.company = "_Test Company"
jv.voucher_type = voucher_type
jv.multi_currency = 0
if party_type == "Supplier":
# Debit Note: Expense Dr, Supplier Cr
expense_account = "Stock Received But Not Billed - _TC"
party_account = "Creditors - _TC"
jv.append(
"accounts",
{
"account": expense_account,
"cost_center": cost_center,
"debit_in_account_currency": amount,
"exchange_rate": 1,
},
)
jv.append(
"accounts",
{
"account": party_account,
"party_type": party_type,
"party": party,
"cost_center": cost_center,
"credit_in_account_currency": amount,
"exchange_rate": 1,
},
)
else: # Customer
# Credit Note: Customer Dr, Income Cr
party_account = "Debtors - _TC"
income_account = "Sales - _TC"
jv.append(
"accounts",
{
"account": party_account,
"party_type": party_type,
"party": party,
"cost_center": cost_center,
"debit_in_account_currency": amount,
"exchange_rate": 1,
},
)
jv.append(
"accounts",
{
"account": income_account,
"cost_center": cost_center,
"credit_in_account_currency": amount,
"exchange_rate": 1,
},
)
if save or submit:
jv.insert()
if submit:
jv.submit()
return jv
def create_records():
# create a new suppliers
for name in [

View File

@@ -199,19 +199,20 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_r
for d in gl_map:
cost_center = d.get("cost_center")
cost_center_allocation = get_cost_center_allocation_data(
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
)
if not cost_center_allocation:
new_gl_map.append(d)
continue
# Validate budget against main cost center
if not from_repost:
validate_expense_against_budget(
d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)
)
cost_center_allocation = get_cost_center_allocation_data(
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
)
if not cost_center_allocation:
new_gl_map.append(d)
continue
if d.account == round_off_account:
d.cost_center = cost_center_allocation[0][0]
new_gl_map.append(d)
@@ -414,7 +415,11 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.flags.notify_update = False
gle.submit()
if not from_repost and gle.voucher_type != "Period Closing Voucher":
if (
not from_repost
and gle.voucher_type != "Period Closing Voucher"
and (gle.is_cancelled == 0 or gle.voucher_type == "Journal Entry")
):
validate_expense_against_budget(args)

View File

@@ -482,7 +482,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
immutable_ledger = frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger")
def update_value_in_dict(data, key, gle):
def update_value_in_dict(data, key, gle, show_net_values=False):
data[key].debit += gle.debit
data[key].credit += gle.credit
@@ -493,10 +493,14 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
data[key].debit_in_transaction_currency += gle.debit_in_transaction_currency
data[key].credit_in_transaction_currency += gle.credit_in_transaction_currency
if filters.get("show_net_values_in_party_account") and account_type_map.get(data[key].account) in (
"Receivable",
"Payable",
):
if (
filters.get("show_net_values_in_party_account")
and account_type_map.get(data[key].account)
in (
"Receivable",
"Payable",
)
) or show_net_values:
net_value = data[key].debit - data[key].credit
net_value_in_account_currency = (
data[key].debit_in_account_currency - data[key].credit_in_account_currency
@@ -526,11 +530,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries):
if not group_by_voucher_consolidated:
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle)
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle)
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle, True)
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle, True)
update_value_in_dict(totals, "opening", gle)
update_value_in_dict(totals, "closing", gle)
update_value_in_dict(totals, "opening", gle, True)
update_value_in_dict(totals, "closing", gle, True)
elif gle.posting_date <= to_date or (cstr(gle.is_opening) == "Yes" and show_opening_entries):
if not group_by_voucher_consolidated:

View File

@@ -5,7 +5,6 @@
import frappe
from frappe import _
from frappe.utils import flt
from pypika import Order
import erpnext
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
@@ -16,7 +15,7 @@ from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register i
get_group_by_and_display_fields,
get_tax_accounts,
)
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
from erpnext.accounts.report.utils import get_values_for_columns
def execute(filters=None):
@@ -41,16 +40,6 @@ def _execute(filters=None, additional_table_columns=None):
tax_doctype="Purchase Taxes and Charges",
)
scrubbed_tax_fields = {}
for tax in tax_columns:
scrubbed_tax_fields.update(
{
tax + " Rate": frappe.scrub(tax + " Rate"),
tax + " Amount": frappe.scrub(tax + " Amount"),
}
)
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
data = []
@@ -100,8 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update(
{
scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
f"{tax}_rate": item_tax.get("tax_rate", 0),
f"{tax}_amount": item_tax.get("tax_amount", 0),
}
)
total_tax += flt(item_tax.get("tax_amount"))

View File

@@ -6,7 +6,7 @@ import frappe
from frappe import _
from frappe.model.meta import get_field_precision
from frappe.query_builder import functions as fn
from frappe.utils import cstr, flt
from frappe.utils import flt
from frappe.utils.nestedset import get_descendants_of
from frappe.utils.xlsxutils import handle_html
@@ -32,16 +32,6 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
scrubbed_tax_fields = {}
for tax in tax_columns:
scrubbed_tax_fields.update(
{
tax + " Rate": frappe.scrub(tax + " Rate"),
tax + " Amount": frappe.scrub(tax + " Amount"),
}
)
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
so_dn_map = get_delivery_notes_against_sales_order(item_list)
@@ -102,8 +92,8 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update(
{
scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
f"{tax}_rate": item_tax.get("tax_rate", 0),
f"{tax}_amount": item_tax.get("tax_amount", 0),
}
)
if item_tax.get("is_other_charges"):
@@ -546,9 +536,10 @@ def get_tax_accounts(
import json
item_row_map = {}
tax_columns = []
tax_columns = {}
invoice_item_row = {}
itemised_tax = {}
scrubbed_description_map = {}
add_deduct_tax = "charge_type"
tax_amount_precision = (
@@ -605,9 +596,14 @@ def get_tax_accounts(
tax_amount,
) in tax_details:
description = handle_html(description)
if description not in tax_columns and tax_amount:
scrubbed_description = scrubbed_description_map.get(description)
if not scrubbed_description:
scrubbed_description = frappe.scrub(description)
scrubbed_description_map[description] = scrubbed_description
if scrubbed_description not in tax_columns and tax_amount:
# as description is text editor earlier and markup can break the column convention in reports
tax_columns.append(description)
tax_columns[scrubbed_description] = description
if item_wise_tax_detail:
try:
@@ -641,7 +637,7 @@ def get_tax_accounts(
else tax_value
)
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
itemised_tax.setdefault(d.name, {})[scrubbed_description] = frappe._dict(
{
"tax_rate": tax_rate,
"tax_amount": tax_value,
@@ -653,7 +649,7 @@ def get_tax_accounts(
continue
elif charge_type == "Actual" and tax_amount:
for d in invoice_item_row.get(parent, []):
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
itemised_tax.setdefault(d.name, {})[scrubbed_description] = frappe._dict(
{
"tax_rate": "NA",
"tax_amount": flt(
@@ -662,12 +658,14 @@ def get_tax_accounts(
}
)
tax_columns.sort()
for desc in tax_columns:
tax_columns_list = list(tax_columns.keys())
tax_columns_list.sort()
for scrubbed_desc in tax_columns_list:
desc = tax_columns[scrubbed_desc]
columns.append(
{
"label": _(desc + " Rate"),
"fieldname": frappe.scrub(desc + " Rate"),
"fieldname": f"{scrubbed_desc}_rate",
"fieldtype": "Float",
"width": 100,
}
@@ -676,7 +674,7 @@ def get_tax_accounts(
columns.append(
{
"label": _(desc + " Amount"),
"fieldname": frappe.scrub(desc + " Amount"),
"fieldname": f"{scrubbed_desc}_amount",
"fieldtype": "Currency",
"options": "currency",
"width": 100,
@@ -714,7 +712,7 @@ def get_tax_accounts(
},
]
return itemised_tax, tax_columns
return itemised_tax, tax_columns_list
def add_total_row(
@@ -807,5 +805,5 @@ def add_sub_total_row(item, total_row_map, group_by_value, tax_columns):
total_row["percent_gt"] += item["percent_gt"]
for tax in tax_columns:
total_row.setdefault(frappe.scrub(tax + " Amount"), 0.0)
total_row[frappe.scrub(tax + " Amount")] += flt(item[frappe.scrub(tax + " Amount")])
total_row.setdefault(f"{tax}_amount", 0.0)
total_row[f"{tax}_amount"] += flt(item[f"{tax}_amount"])

View File

@@ -399,7 +399,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
}
for key in value_fields:
row[key] = flt(d.get(key, 0.0), 3)
row[key] = flt(d.get(key, 0.0))
if abs(row[key]) >= get_zero_cutoff(company_currency):
# ignore zero values

View File

@@ -537,6 +537,7 @@ def modify_depreciation_schedule_for_asset_repairs(asset, notes):
for repair in asset_repairs:
if repair.increase_in_asset_life:
asset_repair = frappe.get_doc("Asset Repair", repair.name)
asset_repair.asset_doc = asset
asset_repair.modify_depreciation_schedule()
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes)

View File

@@ -139,6 +139,7 @@ class AssetCapitalization(StockController):
self.make_gl_entries()
self.repost_future_sle_and_gle()
self.restore_consumed_asset_items()
self.update_target_asset()
def set_title(self):
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
@@ -607,8 +608,12 @@ class AssetCapitalization(StockController):
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
asset_doc = frappe.get_doc("Asset", self.target_asset)
asset_doc.gross_purchase_amount += total_target_asset_value
asset_doc.purchase_amount += total_target_asset_value
if self.docstatus == 2:
asset_doc.gross_purchase_amount -= total_target_asset_value
asset_doc.purchase_amount -= total_target_asset_value
else:
asset_doc.gross_purchase_amount += total_target_asset_value
asset_doc.purchase_amount += total_target_asset_value
asset_doc.set_status("Work In Progress")
asset_doc.flags.ignore_validate = True
asset_doc.save()

View File

@@ -312,13 +312,30 @@ class AccountsController(TransactionBase):
def validate_company_linked_addresses(self):
address_fields = []
if self.doctype in ("Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
sales_doctypes = ("Quotation", "Sales Order", "Delivery Note", "Sales Invoice")
purchase_doctypes = ("Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation")
if self.doctype in sales_doctypes:
address_fields = ["dispatch_address_name", "company_address"]
elif self.doctype in ("Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation"):
elif self.doctype in purchase_doctypes:
address_fields = ["billing_address", "shipping_address"]
if not address_fields:
return
# Determine if drop ship applies
is_drop_ship = self.doctype in {
"Purchase Order",
"Sales Order",
"Sales Invoice",
} and self.is_drop_ship(self.items)
for field in address_fields:
address = self.get(field)
if (field in ["dispatch_address_name", "shipping_address"]) and is_drop_ship:
continue
if address and not frappe.db.exists(
"Dynamic Link",
{
@@ -329,11 +346,15 @@ class AccountsController(TransactionBase):
},
):
frappe.throw(
_("{0} does not belong to the {1}.").format(
_("{0} does not belong to the Company {1}.").format(
_(self.meta.get_label(field)), bold(self.company)
)
)
@staticmethod
def is_drop_ship(items):
return any(item.delivered_by_supplier for item in items)
def set_default_letter_head(self):
if hasattr(self, "letter_head") and not self.letter_head:
self.letter_head = frappe.db.get_value("Company", self.company, "default_letter_head")

View File

@@ -86,6 +86,7 @@ status_map = {
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
],

View File

@@ -7,6 +7,7 @@ import json
import frappe
from frappe import _, scrub
from frappe.model.document import Document
from frappe.query_builder import functions
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
from frappe.utils.deprecations import deprecated
@@ -377,6 +378,9 @@ class calculate_taxes_and_totals:
self._calculate()
def calculate_taxes(self):
# reset value from earlier calculations
self.grand_total_diff = 0
doc = self.doc
if not doc.get("taxes"):
return
@@ -586,7 +590,7 @@ class calculate_taxes_and_totals:
self.grand_total_diff = 0
def calculate_totals(self):
grand_total_diff = getattr(self, "grand_total_diff", 0)
grand_total_diff = self.grand_total_diff
if self.doc.get("taxes"):
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + grand_total_diff
@@ -685,6 +689,22 @@ class calculate_taxes_and_totals:
discount_amount = self.doc.discount_amount or 0
grand_total = self.doc.grand_total
if self.doc.get("is_return") and self.doc.get("return_against"):
doctype = frappe.qb.DocType(self.doc.doctype)
result = (
frappe.qb.from_(doctype)
.select(functions.Sum(doctype.discount_amount).as_("total_return_discount"))
.where(
(doctype.return_against == self.doc.return_against)
& (doctype.is_return == 1)
& (doctype.docstatus == 1)
)
).run(as_dict=True)
total_return_discount = abs(result[0].get("total_return_discount") or 0)
discount_amount += total_return_discount
# validate that discount amount cannot exceed the total before discount
if (
(grand_total >= 0 and discount_amount > grand_total)
@@ -833,12 +853,11 @@ class calculate_taxes_and_totals:
)
)
if self.doc.docstatus.is_draft():
if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0
if self.doc.get("write_off_outstanding_amount_automatically"):
self.doc.write_off_amount = 0
self.calculate_outstanding_amount()
self.calculate_write_off_amount()
self.calculate_outstanding_amount()
self.calculate_write_off_amount()
def is_internal_invoice(self):
"""

View File

@@ -2434,6 +2434,7 @@ class TestAccountsController(FrappeTestCase):
def test_company_linked_address(self):
from erpnext.crm.doctype.prospect.test_prospect import make_address
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
company_address = make_address(
address_title="Company", address_type="Shipping", address_line1="100", city="Mumbai"
@@ -2462,3 +2463,16 @@ class TestAccountsController(FrappeTestCase):
po.billing_address = company_address.name
po.reload()
po.save()
si = make_sales_order(do_not_save=1, do_not_submit=1)
si.dispatch_address_name = supplier_billing.name
self.assertRaises(frappe.ValidationError, si.save)
si.items[0].delivered_by_supplier = 1
si.items[0].supplier = "_Test Supplier"
si.save()
po = create_purchase_order(do_not_save=True)
po.shipping_address = customer_shipping.name
self.assertRaises(frappe.ValidationError, po.save)
po.items[0].delivered_by_supplier = 1
po.save()

View File

@@ -412,29 +412,29 @@ scheduler_events = {
"0/15 * * * *": [
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
],
"0/30 * * * *": [
"erpnext.utilities.doctype.video.video.update_youtube_data",
],
"0/30 * * * *": [],
# Hourly but offset by 30 minutes
"30 * * * *": [
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
],
# Daily but offset by 45 minutes
"45 0 * * *": [
"erpnext.stock.reorder_item.reorder_item",
],
"45 0 * * *": [],
},
"hourly": [
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
],
"hourly_long": [
"hourly_long": [],
"hourly_maintenance": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
"erpnext.utilities.bulk_transaction.retry",
"erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.projects.doctype.project.project.project_status_update_reminder",
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.utilities.doctype.video.video.update_youtube_data",
],
"daily": [
"daily": [],
"daily_long": [],
"daily_maintenance": [
"erpnext.support.doctype.issue.issue.auto_close_tickets",
"erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity",
"erpnext.controllers.accounts_controller.update_invoice_status",
@@ -458,17 +458,16 @@ scheduler_events = {
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily",
"erpnext.accounts.utils.run_ledger_health_checks",
"erpnext.assets.doctype.asset_maintenance_log.asset_maintenance_log.update_asset_maintenance_log_status",
],
"weekly": [
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
],
"daily_long": [
"erpnext.stock.reorder_item.reorder_item",
"erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
],
"weekly": [
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_monthly",

View File

@@ -389,10 +389,12 @@ frappe.ui.form.on("BOM", {
);
has_template_rm.forEach((d) => {
let bom_qty = dialog.fields_dict.qty?.value || 1;
dialog.fields_dict.items.df.data.push({
item_code: d.item_code,
variant_item_code: "",
qty: (d.qty / frm.doc.quantity) * (dialog.fields_dict.qty.value || 1),
qty: flt(d.qty / frm.doc.quantity) * flt(bom_qty),
source_warehouse: d.source_warehouse,
operation: d.operation,
});

View File

@@ -178,17 +178,12 @@ class JobCard(Document):
if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
form_link = get_link_to_form("Manufacturing Settings", "Manufacturing Settings")
msg = f"""
Qty To Manufacture in the job card
cannot be greater than Qty To Manufacture in the
work order for the operation {bold(self.operation)}.
<br><br><b>Solution: </b> Either you can reduce the
Qty To Manufacture in the job card or set the
'Overproduction Percentage For Work Order'
in the {form_link}."""
frappe.throw(_(msg), title=_("Extra Job Card Quantity"))
frappe.throw(
_(
"Qty To Manufacture in the job card cannot be greater than Qty To Manufacture in the work order for the operation {0}. <br><br><b>Solution: </b> Either you can reduce the Qty To Manufacture in the job card or set the 'Overproduction Percentage For Work Order' in the {1}."
).format(bold(self.operation), form_link),
title=_("Extra Job Card Quantity"),
)
def set_sub_operations(self):
if not self.sub_operations and self.operation:
@@ -1064,14 +1059,16 @@ class JobCard(Document):
)
if row.completed_qty < current_operation_qty:
msg = f"""The completed quantity {bold(current_operation_qty)}
of an operation {bold(self.operation)} cannot be greater
than the completed quantity {bold(row.completed_qty)}
of a previous operation
{bold(row.operation)}.
"""
frappe.throw(_(msg))
frappe.throw(
_(
"The completed quantity {0} of an operation {1} cannot be greater than the completed quantity {2} of a previous operation {3}."
).format(
bold(current_operation_qty),
bold(self.operation),
bold(row.completed_qty),
bold(row.operation),
)
)
def validate_work_order(self):
if self.is_work_order_closed():

View File

@@ -229,7 +229,8 @@ frappe.ui.form.on("Work Order", {
if (
frm.doc.docstatus === 1 &&
["Closed", "Completed"].includes(frm.doc.status) &&
frm.doc.produced_qty > 0
frm.doc.produced_qty > 0 &&
frm.doc.produced_qty > frm.doc.disassembled_qty
) {
frm.add_custom_button(
__("Disassemble Order"),
@@ -402,11 +403,14 @@ frappe.ui.form.on("Work Order", {
erpnext.work_order
.show_prompt_for_qty_input(frm, "Disassemble")
.then((data) => {
if (flt(data.qty) <= 0) {
frappe.msgprint(__("Disassemble Qty cannot be less than or equal to <b>0</b>."));
return;
}
return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
work_order_id: frm.doc.name,
purpose: "Disassemble",
qty: data.qty,
target_warehouse: data.target_warehouse,
});
})
.then((stock_entry) => {
@@ -863,24 +867,6 @@ erpnext.work_order = {
},
];
if (purpose === "Disassemble") {
fields.push({
fieldtype: "Link",
options: "Warehouse",
fieldname: "target_warehouse",
label: __("Target Warehouse"),
default: frm.doc.source_warehouse || frm.doc.wip_warehouse,
get_query() {
return {
filters: {
company: frm.doc.company,
is_group: 0,
},
};
},
});
}
return new Promise((resolve, reject) => {
frm.qty_prompt = frappe.prompt(
fields,

View File

@@ -979,14 +979,14 @@ class WorkOrder(Document):
for d in self.get("operations"):
precision = d.precision("completed_qty")
qty = flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision)
qty = flt(flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision), precision)
if not qty:
d.status = "Pending"
elif flt(qty) < flt(self.qty):
elif qty < flt(self.qty, precision):
d.status = "Work in Progress"
elif flt(qty) == flt(self.qty):
elif qty == flt(self.qty, precision):
d.status = "Completed"
elif flt(qty) <= max_allowed_qty_for_wo:
elif qty <= flt(max_allowed_qty_for_wo, precision):
d.status = "Completed"
else:
frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'"))
@@ -1373,6 +1373,13 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_m
item_details = get_item_details(item, project)
if frappe.db.get_value("Item", item, "variant_of"):
if variant_bom := frappe.db.get_value(
"BOM",
{"item": item, "is_default": 1, "docstatus": 1},
):
bom_no = variant_bom
wo_doc = frappe.new_doc("Work Order")
wo_doc.production_item = item
wo_doc.update(item_details)
@@ -1502,7 +1509,7 @@ def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
stock_entry.set_stock_entry_type()
stock_entry.get_items(qty, work_order.production_item)
stock_entry.get_items()
if purpose != "Disassemble":
stock_entry.set_serial_no_batch_for_finished_good()

View File

@@ -426,4 +426,4 @@ erpnext.patches.v15_0.set_asset_status_if_not_already_set
erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing
execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1)
execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1)
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11

View File

@@ -2,8 +2,13 @@ import frappe
def execute():
settings_meta = frappe.get_meta("Currency Exchange Settings")
settings = frappe.get_doc("Currency Exchange Settings")
if settings.service_provider != "frankfurter.app":
if (
"frankfurter.dev" not in settings_meta.get_options("service_provider").split("\n")
or settings.service_provider != "frankfurter.app"
):
return
settings.service_provider = "frankfurter.dev"

View File

@@ -75,13 +75,27 @@ def get_chart_data(data):
delay = delay + 1
else:
on_track = on_track + 1
labels = []
datasets = []
colors = []
if on_track:
labels.append(_("On Track"))
datasets.append(on_track)
colors.append("#84D5BA")
if delay:
labels.append(_("Delayed"))
datasets.append(delay)
colors.append("#CB4B5F")
charts = {
"data": {
"labels": [_("On Track"), _("Delayed")],
"datasets": [{"name": _("Delayed"), "values": [on_track, delay]}],
"labels": labels,
"datasets": [{"name": _("Delayed"), "values": datasets}],
},
"type": "percentage",
"colors": ["#84D5BA", "#CB4B5F"],
"colors": colors,
}
return charts

View File

@@ -343,6 +343,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
}
calculate_taxes() {
// reset value from earlier calculations
this.grand_total_diff = 0;
const doc = this.frm.doc;
if (!doc.taxes?.length) return;
@@ -578,6 +581,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) {
me.grand_total_diff = diff;
} else {
me.grand_total_diff = 0;
}
}
}
@@ -587,7 +592,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
// Changing sequence can cause rounding_adjustmentng issue and on-screen discrepency
const me = this;
const tax_count = this.frm.doc.taxes?.length;
const grand_total_diff = this.grand_total_diff || 0;
const grand_total_diff = this.grand_total_diff;
this.frm.doc.grand_total = flt(tax_count
? this.frm.doc["taxes"][tax_count - 1].total + grand_total_diff

View File

@@ -1093,6 +1093,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
this.frm.refresh_field("payment_schedule");
}
cost_center(doc, cdt, cdn) {
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "cost_center");
}
due_date(doc, cdt, cdn) {
// due_date is to be changed, payment terms template and/or payment schedule must
// be removed as due_date is automatically changed based on payment terms

View File

@@ -1408,7 +1408,6 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
{
"Sales Order": {
"doctype": "Purchase Order",
"field_map": {"dispatch_address_name": "dispatch_address"},
"field_no_map": [
"address_display",
"contact_display",
@@ -1417,6 +1416,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
"contact_person",
"taxes_and_charges",
"shipping_address",
"dispatch_address",
],
"validation": {"docstatus": ["=", 1]},
},
@@ -1549,7 +1549,6 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
{
"Sales Order": {
"doctype": "Purchase Order",
"field_map": {"dispatch_address_name": "dispatch_address"},
"field_no_map": [
"address_display",
"contact_display",
@@ -1558,6 +1557,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
"contact_person",
"taxes_and_charges",
"shipping_address",
"dispatch_address",
],
"validation": {"docstatus": ["=", 1]},
},

View File

@@ -11,7 +11,13 @@ erpnext.setup.EmployeeController = class EmployeeController extends frappe.ui.fo
};
};
this.frm.fields_dict.reports_to.get_query = function (doc, cdt, cdn) {
return { query: "erpnext.controllers.queries.employee_query" };
return {
query: "erpnext.controllers.queries.employee_query",
filters: [
["status", "=", "Active"],
["name", "!=", doc.name],
],
};
};
}

View File

@@ -347,8 +347,9 @@ class TransactionDeletionRecord(Document):
self.db_set("error_log", None)
def get_doctypes_to_be_ignored_list(self):
singles = frappe.get_all("DocType", filters={"issingle": 1}, pluck="name")
doctypes_to_be_ignored_list = singles
doctypes_to_be_ignored_list = frappe.get_all(
"DocType", or_filters=[["issingle", "=", 1], ["is_virtual", "=", 1]], pluck="name"
)
for doctype in self.doctypes_to_be_ignored:
doctypes_to_be_ignored_list.append(doctype.doctype_name)

View File

@@ -4,14 +4,13 @@
import click
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
from frappe.utils import cint
import erpnext
from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules
from erpnext.setup.doctype.incoterm.incoterm import create_incoterms
from erpnext.setup.utils import identity as _
from .default_success_action import get_default_success_action
@@ -193,28 +192,27 @@ def add_company_to_session_defaults():
def add_standard_navbar_items():
navbar_settings = frappe.get_single("Navbar Settings")
erpnext_navbar_items = [
{
"item_label": "Documentation",
"item_label": _("Documentation"),
"item_type": "Route",
"route": "https://docs.erpnext.com/",
"is_standard": 1,
},
{
"item_label": "User Forum",
"item_label": _("User Forum"),
"item_type": "Route",
"route": "https://discuss.frappe.io",
"is_standard": 1,
},
{
"item_label": "Frappe School",
"item_label": _("Frappe School"),
"item_type": "Route",
"route": "https://frappe.io/school?utm_source=in_app",
"is_standard": 1,
},
{
"item_label": "Report an Issue",
"item_label": _("Report an Issue"),
"item_type": "Route",
"route": "https://github.com/frappe/erpnext/issues",
"is_standard": 1,

View File

@@ -234,3 +234,15 @@ def welcome_email():
site_name = get_default_company() or "ERPNext"
title = _("Welcome to {0}").format(site_name)
return title
def identity(x, *args, **kwargs):
"""Used for redefining the translation function to return the string as is.
We want to create english records but still mark the strings as translatable.
E.g. when the respective DocTypes have 'Translate Link Fields' enabled or
we're creating custom fields.
Use like this: `from erpnext.setup.utils import identity as _`
"""
return x

View File

@@ -50,7 +50,7 @@ def boot_session(bootinfo):
bootinfo.docs += frappe.db.sql(
"""select name, default_currency, cost_center, default_selling_terms, default_buying_terms,
default_letter_head, default_bank_account, enable_perpetual_inventory, country from `tabCompany`""",
default_letter_head, default_bank_account, enable_perpetual_inventory, country, exchange_gain_loss_account from `tabCompany`""",
as_dict=1,
update={"doctype": ":Company"},
)

View File

@@ -166,7 +166,9 @@ class Batch(Document):
for row in batches:
batch_qty += row.get("qty")
self.db_set("batch_qty", batch_qty)
if self.batch_qty != batch_qty:
self.db_set("batch_qty", batch_qty)
frappe.msgprint(_("Batch Qty updated to {0}").format(batch_qty), alert=True)
def set_batchwise_valuation(self):

View File

@@ -130,10 +130,6 @@ frappe.ui.form.on("Delivery Note Item", {
var d = locals[dt][dn];
frm.update_in_all_rows("items", "expense_account", d.expense_account);
},
cost_center: function (frm, dt, dn) {
var d = locals[dt][dn];
frm.update_in_all_rows("items", "cost_center", d.cost_center);
},
});
erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (

View File

@@ -1091,7 +1091,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "\nDraft\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed",
"options": "\nDraft\nTo Bill\nCompleted\nReturn\nReturn Issued\nCancelled\nClosed",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
@@ -1404,7 +1404,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2025-08-04 19:20:47.724218",
"modified": "2025-12-02 23:55:25.415443",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@@ -126,7 +126,9 @@ class DeliveryNote(SellingController):
shipping_address_name: DF.Link | None
shipping_rule: DF.Link | None
source: DF.Link | None
status: DF.Literal["", "Draft", "To Bill", "Completed", "Return Issued", "Cancelled", "Closed"]
status: DF.Literal[
"", "Draft", "To Bill", "Completed", "Return", "Return Issued", "Cancelled", "Closed"
]
tax_category: DF.Link | None
tax_id: DF.Data | None
taxes: DF.Table[SalesTaxesandCharges]

View File

@@ -12,8 +12,8 @@ frappe.listview_settings["Delivery Note"] = {
"currency",
],
get_indicator: function (doc) {
if (cint(doc.is_return) == 1) {
return [__("Return"), "gray", "is_return,=,Yes"];
if (cint(doc.is_return) == 1 && doc.status == "Return") {
return [__("Return"), "gray", "is_return,=,1"];
} else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"];
} else if (doc.status === "Return Issued") {

View File

@@ -2581,6 +2581,7 @@ class TestDeliveryNote(FrappeTestCase):
dn = make_delivery_note(so.name)
dn.submit()
self.assertEqual(dn.per_billed, 0)
self.assertEqual(dn.status, "To Bill")
si = make_sales_invoice(dn.name)
si.location = "Test Location"
@@ -2595,6 +2596,7 @@ class TestDeliveryNote(FrappeTestCase):
dn.load_from_db()
self.assertEqual(dn.per_billed, 100)
self.assertEqual(dn.per_returned, 100)
self.assertEqual(returned.status, "Return")
def test_sales_return_for_product_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle

View File

@@ -504,6 +504,61 @@ class TestInventoryDimension(FrappeTestCase):
self.assertEqual(site_name, "Site 1")
def test_validate_negative_stock_with_multiple_dimension(self):
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 0)
item_code = "Test Negative Multi Inventory Dimension Item"
create_item(item_code)
inv_dimension_1 = create_inventory_dimension(
apply_to_all_doctypes=1,
dimension_name="Inv Site",
reference_document="Inv Site",
document_type="Inv Site",
validate_negative_stock=1,
)
inv_dimension_1.db_set("validate_negative_stock", 1)
inv_dimension_2 = create_inventory_dimension(
apply_to_all_doctypes=1,
dimension_name="Rack",
reference_document="Rack",
document_type="Rack",
validate_negative_stock=1,
)
inv_dimension_2.db_set("validate_negative_stock", 1)
frappe.local.inventory_dimensions = {}
frappe.local.document_wise_inventory_dimensions = {}
pr_doc = make_purchase_receipt(item_code=item_code, qty=30, do_not_submit=True)
pr_doc.items[0].inv_site = "Site 1"
pr_doc.items[0].rack = "Rack 1"
pr_doc.save()
pr_doc.submit()
pr_doc = make_purchase_receipt(item_code=item_code, qty=15, do_not_submit=True)
pr_doc.items[0].inv_site = "Site 1"
pr_doc.items[0].rack = "Rack 2"
pr_doc.save()
pr_doc.submit()
pr_doc = make_purchase_receipt(item_code=item_code, qty=30, do_not_submit=True)
pr_doc.items[0].inv_site = "Site 2"
pr_doc.items[0].rack = "Rack 1"
pr_doc.save()
pr_doc.submit()
pr_doc = make_purchase_receipt(item_code=item_code, qty=25, do_not_submit=True)
pr_doc.items[0].inv_site = "Site 2"
pr_doc.items[0].rack = "Rack 2"
pr_doc.save()
pr_doc.submit()
dn_doc = create_delivery_note(item_code=item_code, qty=35, do_not_submit=True)
dn_doc.items[0].inv_site = "Site 2"
dn_doc.items[0].rack = "Rack 1"
dn_doc.save()
self.assertRaises(InventoryDimensionNegativeStockError, dn_doc.submit)
def get_voucher_sl_entries(voucher_no, fields):
return frappe.get_all(
@@ -593,7 +648,7 @@ def prepare_test_data():
}
).insert(ignore_permissions=True)
for rack in ["Rack 1"]:
for rack in ["Rack 1", "Rack 2"]:
if not frappe.db.exists("Rack", rack):
frappe.get_doc({"doctype": "Rack", "rack_name": rack}).insert(ignore_permissions=True)

View File

@@ -224,7 +224,7 @@ frappe.ui.form.on("Item", {
["is_stock_item", "has_serial_no", "has_batch_no", "has_variants"].forEach((fieldname) => {
frm.set_df_property(fieldname, "read_only", stock_exists);
});
frm.set_df_property("is_fixed_asset", "read_only", frm.doc.__onload?.asset_exists ? 1 : 0);
frm.toggle_reqd("customer", frm.doc.is_customer_provided_item ? 1 : 0);
frm.set_query("item_group", () => {
return {

View File

@@ -167,6 +167,7 @@
"in_preview": 1,
"in_standard_filter": 1,
"label": "Item Group",
"link_filters": "[[\"Item Group\",\"is_group\",\"=\",0]]",
"oldfieldname": "item_group",
"oldfieldtype": "Link",
"options": "Item Group",
@@ -243,8 +244,7 @@
"default": "0",
"fieldname": "is_fixed_asset",
"fieldtype": "Check",
"label": "Is Fixed Asset",
"set_only_once": 1
"label": "Is Fixed Asset"
},
{
"allow_in_quick_entry": 1,
@@ -895,7 +895,7 @@
"image_field": "image",
"links": [],
"make_attachments_public": 1,
"modified": "2025-08-08 14:58:48.674193",
"modified": "2025-12-15 20:08:35.634046",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",

View File

@@ -155,6 +155,7 @@ class Item(Document):
self.set_onload("stock_exists", self.stock_ledger_created())
self.set_onload("asset_naming_series", get_asset_naming_series())
self.set_onload("current_valuation_method", get_valuation_method(self.name))
self.set_onload("asset_exists", self.has_submitted_assets())
def autoname(self):
if frappe.db.get_default("item_naming_by") == "Naming Series":
@@ -306,9 +307,8 @@ class Item(Document):
if self.stock_ledger_created():
frappe.throw(_("Cannot be a fixed asset item as Stock Ledger is created."))
if not self.is_fixed_asset:
asset = frappe.db.get_all("Asset", filters={"item_code": self.name, "docstatus": 1}, limit=1)
if asset:
if not self.is_fixed_asset and not self.is_new():
if self.has_submitted_assets():
frappe.throw(
_('"Is Fixed Asset" cannot be unchecked, as Asset record exists against the item')
)
@@ -525,6 +525,9 @@ class Item(Document):
)
return self._stock_ledger_created
def has_submitted_assets(self):
return bool(frappe.db.exists("Asset", {"item_code": self.name, "docstatus": 1}))
def update_item_price(self):
frappe.db.sql(
"""

View File

@@ -19,6 +19,7 @@ from erpnext.stock.doctype.material_request.material_request import (
make_supplier_quotation,
raise_work_orders,
)
from erpnext.stock.doctype.stock_entry.stock_entry import make_stock_in_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
@@ -926,6 +927,49 @@ class TestMaterialRequest(FrappeTestCase):
pl_for_pending = create_pick_list(mr.name)
self.assertEqual(pl_for_pending.locations[0].qty, 5)
def test_mr_status_with_partial_and_excess_end_transit(self):
material_request = make_material_request(
material_request_type="Material Transfer",
item_code="_Test Item Home Desktop 100",
)
in_transit_wh = get_in_transit_warehouse(material_request.company)
# Make sure stock is available in source warehouse
self._insert_stock_entry(20.0, 20.0)
# Stock Entry (Transfer to In-Transit)
stock_entry_1 = make_in_transit_stock_entry(material_request.name, in_transit_wh)
stock_entry_1.items[0].update(
{
"qty": 5,
"s_warehouse": "_Test Warehouse 1 - _TC",
}
)
stock_entry_1.save().submit()
stock_entry_2 = make_in_transit_stock_entry(material_request.name, in_transit_wh)
stock_entry_2.items[0].update(
{
"qty": 5,
"s_warehouse": "_Test Warehouse 1 - _TC",
}
)
stock_entry_2.save().submit()
end_transit_1 = make_stock_in_entry(stock_entry_1.name)
end_transit_1.save().submit()
# Material Request Transfer Status should still be In Transit
material_request.load_from_db()
self.assertEqual(material_request.transfer_status, "In Transit")
end_transit_2 = make_stock_in_entry(stock_entry_2.name)
end_transit_2.items[0].update({"qty": 6}) # More than transferred
end_transit_2.save()
self.assertRaises(frappe.ValidationError, end_transit_2.submit)
def get_in_transit_warehouse(company):
if not frappe.db.exists("Warehouse Type", "Transit"):

View File

@@ -743,7 +743,10 @@ class PickList(TransactionBase):
pi_item.serial_no,
(
Case()
.when((pi_item.picked_qty > 0) & (pi_item.docstatus == 1), pi_item.picked_qty)
.when(
(pi_item.picked_qty > 0) & (pi_item.docstatus == 1),
pi_item.picked_qty - pi_item.delivered_qty,
)
.else_(pi_item.stock_qty)
).as_("picked_qty"),
)

View File

@@ -645,6 +645,46 @@ class TestPickList(FrappeTestCase):
if dn_item.item_code == "_Test Item 2":
self.assertEqual(dn_item.qty, 2)
def test_picklist_reserved_qty_validation(self):
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
warehouse = "_Test Warehouse - _TC"
test_stock_item = "_Test Stock Item"
# Ensure stock item exists
if not frappe.db.exists("Item", test_stock_item):
create_item(
item_code=test_stock_item,
is_stock_item=1,
)
# Add initial stock qty
make_stock_entry(item_code=test_stock_item, to_warehouse=warehouse, qty=15)
# Create SO for 10 qty
sales_order_1 = make_sales_order(item_code=test_stock_item, warehouse=warehouse, qty=10)
# Create and Submit picklist for SO
picklist_1 = create_pick_list(sales_order_1.name)
picklist_1.submit()
# Create DN for 5 qty
dn = create_delivery_note(picklist_1.name)
dn.items[0].qty = 5
dn.save()
dn.submit()
# Verify partly delivered state
picklist_1.reload()
self.assertEqual(picklist_1.status, "Partly Delivered")
# Create another SO (10 qty)
sales_order_2 = make_sales_order(item_code=test_stock_item, warehouse=warehouse, qty=10)
# Expected pick qty = 5
picklist_2 = create_pick_list(sales_order_2.name)
self.assertEqual(picklist_2.locations[0].qty, 5)
def test_picklist_with_multi_uom(self):
warehouse = "_Test Warehouse - _TC"
item = make_item(properties={"uoms": [dict(uom="Box", conversion_factor=24)]}).name

View File

@@ -8,7 +8,7 @@ import frappe
from frappe import _, throw
from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import CombineDatetime
from frappe.query_builder.functions import Abs, CombineDatetime, Sum
from frappe.utils import cint, flt, get_datetime, getdate, nowdate
from pypika import functions as fn
@@ -19,6 +19,11 @@ from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.accounts_controller import merge_taxes
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction
from erpnext.stock.serial_batch_bundle import (
SerialBatchCreation,
get_batches_from_bundle,
get_serial_nos_from_bundle,
)
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -1367,21 +1372,26 @@ def get_invoiced_qty_map(purchase_receipt):
def get_returned_qty_map(purchase_receipt):
"""returns a map: {so_detail: returned_qty}"""
returned_qty_map = frappe._dict(
frappe.db.sql(
"""select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty
from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
where pr.name = pr_item.parent
and pr.docstatus = 1
and pr.is_return = 1
and pr.return_against = %s
""",
purchase_receipt,
)
)
"""returns a map: {pr_detail: returned_qty}"""
return returned_qty_map
pr = frappe.qb.DocType("Purchase Receipt")
pr_item = frappe.qb.DocType("Purchase Receipt Item")
query = (
frappe.qb.from_(pr)
.inner_join(pr_item)
.on(pr.name == pr_item.parent)
.select(pr_item.purchase_receipt_item, Sum(Abs(pr_item.qty)).as_("qty"))
.where(
(pr.docstatus == 1)
& (pr.is_return == 1)
& (pr.return_against == purchase_receipt)
& (pr_item.purchase_receipt_item.isnotnull())
)
.groupby(pr_item.purchase_receipt_item)
).run(as_list=1)
return frappe._dict(query) if query else frappe._dict()
@frappe.whitelist()
@@ -1411,6 +1421,35 @@ def make_stock_entry(source_name, target_doc=None):
target.purpose = "Material Transfer"
target.set_missing_values()
def update_item(source_doc, target_doc, source_parent):
if source_doc.serial_and_batch_bundle:
serial_nos = get_serial_nos_from_bundle(source_doc.serial_and_batch_bundle)
if serial_nos:
serial_nos = "\n".join(serial_nos)
batches = get_batches_from_bundle(source_doc.serial_and_batch_bundle)
if batches:
if len(batches) == 1:
target_doc.use_serial_batch_fields = 1
target_doc.batch_no = next(iter(batches))
elif not serial_nos:
cls_obj = SerialBatchCreation(
{
"type_of_transaction": "Outward",
"serial_and_batch_bundle": source_doc.serial_and_batch_bundle,
"item_code": source_doc.item_code,
"warehouse": source_doc.warehouse,
}
)
cls_obj.duplicate_package()
target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
if serial_nos:
target_doc.use_serial_batch_fields = 1
target_doc.serial_no = serial_nos
doclist = get_mapped_doc(
"Purchase Receipt",
source_name,
@@ -1425,6 +1464,7 @@ def make_stock_entry(source_name, target_doc=None):
"parent": "reference_purchase_receipt",
"batch_no": "batch_no",
},
"postprocess": update_item,
},
},
target_doc,

View File

@@ -12,7 +12,7 @@ frappe.listview_settings["Purchase Receipt"] = {
],
get_indicator: function (doc) {
if (cint(doc.is_return) == 1 && doc.status == "Return") {
return [__("Return"), "gray", "is_return,=,Yes"];
return [__("Return"), "gray", "is_return,=,1"];
} else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"];
} else if (flt(doc.per_returned, 2) === 100) {

View File

@@ -4448,6 +4448,87 @@ class TestPurchaseReceipt(FrappeTestCase):
self.assertEqual(srbnb_cost, 1000)
def test_lcv_for_repack_entry(self):
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
create_landed_cost_voucher,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
for item in [
"Potatoes Raw Material Item",
"Fries Finished Goods Item",
]:
create_item(item)
pr = make_purchase_receipt(
item_code="Potatoes Raw Material Item",
warehouse="_Test Warehouse - _TC",
qty=100,
rate=50,
)
wh1 = create_warehouse("WH A1", company=pr.company)
wh2 = create_warehouse("WH A2", company=pr.company)
ste = make_stock_entry(
purpose="Repack",
source="_Test Warehouse - _TC",
item_code="Potatoes Raw Material Item",
qty=100,
company=pr.company,
do_not_save=1,
)
ste.append(
"items",
{
"item_code": "Fries Finished Goods Item",
"qty": 50,
"t_warehouse": wh1,
},
)
ste.append(
"items",
{
"item_code": "Fries Finished Goods Item",
"qty": 50,
"t_warehouse": wh2,
},
)
ste.insert()
ste.submit()
ste.reload()
for row in ste.items:
if row.t_warehouse:
self.assertEqual(row.valuation_rate, 50)
sles = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_type": ste.doctype, "voucher_no": ste.name, "actual_qty": (">", 0)},
pluck="stock_value_difference",
)
self.assertEqual(sles, [2500.0, 2500.0])
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=2000 * -1)
ste.reload()
for row in ste.items:
if row.t_warehouse:
self.assertEqual(row.valuation_rate, 30)
sles = frappe.get_all(
"Stock Ledger Entry",
filters={"voucher_type": ste.doctype, "voucher_no": ste.name, "actual_qty": (">", 0)},
pluck="stock_value_difference",
)
self.assertEqual(sles, [1500.0, 1500.0])
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -11,6 +11,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, cstr, floor, flt, nowdate
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_stock_balance
@@ -119,13 +120,19 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
item = frappe._dict(item)
source_warehouse = item.get("s_warehouse")
serial_nos = []
if item.get("serial_no"):
serial_nos = get_serial_nos(item.get("serial_no"))
item.conversion_factor = flt(item.conversion_factor) or 1.0
pending_qty, item_code = flt(item.qty), item.item_code
pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty)
uom_must_be_whole_number = frappe.db.get_value("UOM", item.uom, "must_be_whole_number")
if not pending_qty or not item_code:
updated_table = add_row(item, pending_qty, source_warehouse or item.warehouse, updated_table)
updated_table = add_row(
item, pending_qty, source_warehouse or item.warehouse, updated_table, serial_nos=serial_nos
)
continue
at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse)
@@ -141,7 +148,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
# rules available, but no free space
items_not_accomodated.append([item_code, pending_qty])
else:
updated_table = add_row(item, pending_qty, warehouse, updated_table)
updated_table = add_row(item, pending_qty, warehouse, updated_table, serial_nos=serial_nos)
continue
# maintain item/item-warehouse wise rules, to handle if item is entered twice
@@ -167,7 +174,9 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
if not qty_to_allocate:
break
updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table, rule.name)
updated_table = add_row(
item, qty_to_allocate, rule.warehouse, updated_table, rule.name, serial_nos=serial_nos
)
pending_stock_qty -= stock_qty_to_allocate
pending_qty -= qty_to_allocate
@@ -265,7 +274,7 @@ def get_ordered_putaway_rules(item_code, company, source_warehouse=None):
return False, vacant_rules
def add_row(item, to_allocate, warehouse, updated_table, rule=None):
def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None):
new_updated_table_row = copy.deepcopy(item)
new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1
new_updated_table_row.name = None
@@ -283,6 +292,9 @@ def add_row(item, to_allocate, warehouse, updated_table, rule=None):
if rule:
new_updated_table_row.putaway_rule = rule
if serial_nos:
new_updated_table_row.serial_no = get_serial_nos_to_allocate(serial_nos, to_allocate)
new_updated_table_row.serial_and_batch_bundle = ""
updated_table.append(new_updated_table_row)
@@ -311,3 +323,12 @@ def show_unassigned_items_message(items_not_accomodated):
""".format(_("Item"), _("Unassigned Qty"), formatted_item_rows)
frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True)
def get_serial_nos_to_allocate(serial_nos, to_allocate):
if serial_nos:
allocated_serial_nos = serial_nos[0 : cint(to_allocate)]
serial_nos[:] = serial_nos[cint(to_allocate) :] # pop out allocated serial nos and modify list
return "\n".join(allocated_serial_nos) if allocated_serial_nos else ""
else:
return ""

View File

@@ -9,7 +9,7 @@ frappe.ui.form.on("Quality Inspection", {
},
set_default_company(frm) {
if (!frm.doc.company) {
if (frm.doc.docstatus === 0 && !frm.doc.company) {
frm.set_value("company", frappe.defaults.get_default("company"));
}
},

View File

@@ -2008,6 +2008,8 @@ def get_available_serial_nos(kwargs):
filters["name"] = ("in", time_based_serial_nos)
elif ignore_serial_nos:
filters["name"] = ("not in", ignore_serial_nos)
elif kwargs.get("serial_nos"):
filters["name"] = ("in", kwargs.get("serial_nos"))
if kwargs.get("batches"):
batches = get_non_expired_batches(kwargs.get("batches"))
@@ -2532,6 +2534,9 @@ def get_voucher_wise_serial_batch_from_bundle(**kwargs) -> dict[str, dict]:
child_row = group_by_voucher[key]
if row.serial_no:
child_row["serial_nos"].append(row.serial_no)
child_row["item_row"].qty = len(child_row["serial_nos"]) * (
-1 if row.type_of_transaction == "Outward" else 1
)
if row.batch_no:
child_row["batch_nos"][row.batch_no] += row.qty

View File

@@ -571,6 +571,14 @@ frappe.ui.form.on("Stock Entry", {
}
},
set_rate_and_fg_qty: function (frm, cdt, cdn) {
frm.events.set_basic_rate(frm, cdt, cdn);
let item = frappe.get_doc(cdt, cdn);
if (item.is_finished_item) {
frm.events.set_fg_completed_qty(frm);
}
},
get_warehouse_details: function (frm, cdt, cdn) {
var child = locals[cdt][cdn];
if (!child.bom_no) {
@@ -833,7 +841,7 @@ frappe.ui.form.on("Stock Entry", {
frm.doc.items.forEach((item) => {
if (item.is_finished_item) {
fg_completed_qty += flt(item.qty);
fg_completed_qty += flt(item.transfer_qty);
}
});
@@ -859,15 +867,11 @@ frappe.ui.form.on("Stock Entry Detail", {
},
qty(frm, cdt, cdn) {
frm.events.set_basic_rate(frm, cdt, cdn);
let item = frappe.get_doc(cdt, cdn);
if (item.is_finished_item) {
frm.events.set_fg_completed_qty(frm);
}
frm.events.set_rate_and_fg_qty(frm, cdt, cdn);
},
conversion_factor(frm, cdt, cdn) {
frm.events.set_basic_rate(frm, cdt, cdn);
frm.events.set_rate_and_fg_qty(frm, cdt, cdn);
},
s_warehouse(frm, cdt, cdn) {

View File

@@ -8,6 +8,7 @@ from collections import defaultdict
import frappe
from frappe import _, bold
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import DocType
from frappe.query_builder.functions import Sum
from frappe.utils import (
cint,
@@ -243,7 +244,66 @@ class StockEntry(StockController):
self.validate_same_source_target_warehouse_during_material_transfer()
def set_serial_batch_for_disassembly(self):
if self.purpose != "Disassemble":
return
available_materials = get_available_materials(self.work_order, self)
for row in self.items:
warehouse = row.s_warehouse or row.t_warehouse
materials = available_materials.get((row.item_code, warehouse))
if not materials:
continue
batches = defaultdict(float)
serial_nos = []
qty = row.transfer_qty
for batch_no, batch_qty in materials.batch_details.items():
if qty <= 0:
break
batch_qty = abs(batch_qty)
if batch_qty <= qty:
batches[batch_no] = batch_qty
qty -= batch_qty
else:
batches[batch_no] = qty
qty = 0
if materials.serial_nos:
serial_nos = materials.serial_nos[: int(row.transfer_qty)]
if not serial_nos and not batches:
continue
bundle_doc = SerialBatchCreation(
{
"item_code": row.item_code,
"warehouse": warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"voucher_type": self.doctype,
"voucher_no": self.name,
"voucher_detail_no": row.name,
"qty": row.transfer_qty,
"type_of_transaction": "Inward" if row.t_warehouse else "Outward",
"company": self.company,
"do_not_submit": True,
}
).make_serial_and_batch_bundle(serial_nos=serial_nos, batch_nos=batches)
row.serial_and_batch_bundle = bundle_doc.name
row.use_serial_batch_fields = 0
row.db_set(
{
"serial_and_batch_bundle": bundle_doc.name,
"use_serial_batch_fields": 0,
}
)
def on_submit(self):
self.set_serial_batch_for_disassembly()
self.validate_closed_subcontracting_order()
self.make_bundle_using_old_serial_batch_fields()
self.update_disassembled_order()
@@ -1846,7 +1906,7 @@ class StockEntry(StockController):
},
)
def get_items_for_disassembly(self, disassemble_qty, production_item):
def get_items_for_disassembly(self):
"""Get items for Disassembly Order"""
if not self.work_order:
@@ -1856,7 +1916,13 @@ class StockEntry(StockController):
s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
items_dict = get_bom_items_as_dict(self.bom_no, self.company, disassemble_qty)
items_dict = get_bom_items_as_dict(
self.bom_no,
self.company,
self.fg_completed_qty,
fetch_exploded=self.use_multi_level_bom,
fetch_qty_in_stock_uom=False,
)
for row in items:
child_row = self.append("items", {})
@@ -1870,11 +1936,11 @@ class StockEntry(StockController):
child_row.qty = bom_items.get("qty", child_row.qty)
child_row.amount = bom_items.get("amount", child_row.amount)
if row.item_code == production_item:
child_row.qty = disassemble_qty
if row.is_finished_item:
child_row.qty = self.fg_completed_qty
child_row.s_warehouse = (self.from_warehouse or s_warehouse) if row.is_finished_item else ""
child_row.t_warehouse = self.to_warehouse if not row.is_finished_item else ""
child_row.t_warehouse = row.s_warehouse
child_row.is_finished_item = 0 if row.is_finished_item else 1
def get_items_from_manufacture_entry(self):
@@ -1893,6 +1959,8 @@ class StockEntry(StockController):
"`tabStock Entry Detail`.`is_finished_item`",
"`tabStock Entry Detail`.`batch_no`",
"`tabStock Entry Detail`.`serial_no`",
"`tabStock Entry Detail`.`s_warehouse`",
"`tabStock Entry Detail`.`t_warehouse`",
"`tabStock Entry Detail`.`use_serial_batch_fields`",
],
filters=[
@@ -1905,12 +1973,12 @@ class StockEntry(StockController):
)
@frappe.whitelist()
def get_items(self, qty=None, production_item=None):
def get_items(self):
self.set("items", [])
self.validate_work_order()
if self.purpose == "Disassemble" and qty is not None:
return self.get_items_for_disassembly(qty, production_item)
if self.purpose == "Disassemble":
return self.get_items_for_disassembly()
if not self.posting_date or not self.posting_time:
frappe.throw(_("Posting date and posting time is mandatory"))
@@ -2748,6 +2816,17 @@ class StockEntry(StockController):
},
)
if d.docstatus == 1:
transfer_qty = frappe.get_value("Stock Entry Detail", d.ste_detail, "transfer_qty")
if transferred_qty and transferred_qty[0]:
if transferred_qty[0].qty > transfer_qty:
frappe.throw(
_(
"Row {0}: Transferred quantity cannot be greater than the requested quantity."
).format(d.idx)
)
stock_entries[(d.against_stock_entry, d.ste_detail)] = (
transferred_qty[0].qty if transferred_qty and transferred_qty[0] else 0.0
) or 0.0
@@ -2811,7 +2890,7 @@ class StockEntry(StockController):
parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, "add_to_transit")
for item in self.items:
material_request = item.material_request or None
material_request = item.get("material_request")
if self.purpose == "Material Transfer" and material_request not in material_requests:
if self.outgoing_stock_entry and parent_se:
material_request = frappe.get_value(
@@ -2820,6 +2899,11 @@ class StockEntry(StockController):
if material_request and material_request not in material_requests:
material_requests.append(material_request)
if status == "Completed":
qty = get_transferred_qty(material_request)
if qty.get("transfer_qty") > qty.get("transferred_qty"):
status = "In Transit"
frappe.db.set_value("Material Request", material_request, "transfer_status", status)
def set_serial_no_batch_for_finished_good(self):
@@ -3259,8 +3343,8 @@ def get_items_from_subcontract_order(source_name, target_doc=None):
return target_doc
def get_available_materials(work_order) -> dict:
data = get_stock_entry_data(work_order)
def get_available_materials(work_order, stock_entry_doc=None) -> dict:
data = get_stock_entry_data(work_order, stock_entry_doc=stock_entry_doc)
available_materials = {}
for row in data:
@@ -3268,6 +3352,9 @@ def get_available_materials(work_order) -> dict:
if row.purpose != "Material Transfer for Manufacture":
key = (row.item_code, row.s_warehouse)
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
key = (row.item_code, row.s_warehouse or row.warehouse)
if key not in available_materials:
available_materials.setdefault(
key,
@@ -3278,7 +3365,9 @@ def get_available_materials(work_order) -> dict:
item_data = available_materials[key]
if row.purpose == "Material Transfer for Manufacture":
if row.purpose == "Material Transfer for Manufacture" or (
stock_entry_doc and stock_entry_doc.purpose == "Disassemble" and row.purpose == "Manufacture"
):
item_data.qty += row.qty
if row.batch_no:
item_data.batch_details[row.batch_no] += row.qty
@@ -3318,7 +3407,7 @@ def get_available_materials(work_order) -> dict:
return available_materials
def get_stock_entry_data(work_order):
def get_stock_entry_data(work_order, stock_entry_doc=None):
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_voucher_wise_serial_batch_from_bundle,
)
@@ -3350,19 +3439,35 @@ def get_stock_entry_data(work_order):
(stock_entry.name == stock_entry_detail.parent)
& (stock_entry.work_order == work_order)
& (stock_entry.docstatus == 1)
& (stock_entry_detail.s_warehouse.isnotnull())
& (
stock_entry.purpose.isin(
[
"Manufacture",
"Material Consumption for Manufacture",
"Material Transfer for Manufacture",
]
)
)
)
.orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx)
).run(as_dict=1)
)
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
data = data.where(
stock_entry.purpose.isin(
[
"Disassemble",
"Manufacture",
]
)
)
data = data.where(stock_entry.name != stock_entry_doc.name)
else:
data = data.where(
stock_entry.purpose.isin(
[
"Manufacture",
"Material Consumption for Manufacture",
"Material Transfer for Manufacture",
]
)
)
data = data.where(stock_entry_detail.s_warehouse.isnotnull())
data = data.run(as_dict=1)
if not data:
return []
@@ -3375,6 +3480,9 @@ def get_stock_entry_data(work_order):
if row.purpose != "Material Transfer for Manufacture":
key = (row.item_code, row.s_warehouse, row.name)
if stock_entry_doc and stock_entry_doc.purpose == "Disassemble":
key = (row.item_code, row.s_warehouse or row.warehouse, row.name)
if bundle_data.get(key):
row.update(bundle_data.get(key))
@@ -3454,3 +3562,18 @@ def get_batchwise_serial_nos(item_code, row):
batchwise_serial_nos[batch_no] = sorted([serial_no.name for serial_no in serial_nos])
return batchwise_serial_nos
def get_transferred_qty(material_request):
sed = DocType("Stock Entry Detail")
query = (
frappe.qb.from_(sed)
.select(
Sum(sed.transfer_qty).as_("transfer_qty"),
Sum(sed.transferred_qty).as_("transferred_qty"),
)
.where((sed.material_request == material_request) & (sed.docstatus == 1))
).run(as_dict=True)
return query[0]

View File

@@ -113,17 +113,15 @@ class StockLedgerEntry(Document):
return
flt_precision = cint(frappe.db.get_default("float_precision")) or 2
for dimension, values in dimensions.items():
dimension_value = values.get("value")
available_qty = self.get_available_qty_after_prev_transaction(dimension, dimension_value)
available_qty = self.get_available_qty_after_prev_transaction(dimensions)
diff = flt(available_qty + flt(self.actual_qty), flt_precision) # qty after current transaction
if diff < 0 and abs(diff) > 0.0001:
self.throw_validation_error(diff, dimension, dimension_value)
diff = flt(available_qty + flt(self.actual_qty), flt_precision) # qty after current transaction
if diff < 0 and abs(diff) > 0.0001:
self.throw_validation_error(diff, dimensions)
def get_available_qty_after_prev_transaction(self, dimension, dimension_value):
def get_available_qty_after_prev_transaction(self, dimensions):
sle = frappe.qb.DocType("Stock Ledger Entry")
available_qty = (
available_qty_query = (
frappe.qb.from_(sle)
.select(Sum(sle.actual_qty))
.where(
@@ -132,21 +130,27 @@ class StockLedgerEntry(Document):
& (sle.posting_datetime < self.posting_datetime)
& (sle.company == self.company)
& (sle.is_cancelled == 0)
& (sle[dimension] == dimension_value)
)
).run()
)
for dimension, values in dimensions.items():
dimension_value = values.get("value")
available_qty_query = available_qty_query.where(sle[dimension] == dimension_value)
available_qty = available_qty_query.run()
return available_qty[0][0] or 0
def throw_validation_error(self, diff, dimension, dimension_value):
def throw_validation_error(self, diff, dimensions):
msg = _(
"{0} units of {1} are required in {2} with the inventory dimension: {3} ({4}) on {5} {6} for {7} to complete the transaction."
"{0} units of {1} are required in {2} with the inventory dimension: {3} on {4} {5} for {6} to complete the transaction."
).format(
abs(diff),
frappe.get_desk_link("Item", self.item_code),
frappe.get_desk_link("Warehouse", self.warehouse),
frappe.bold(dimension),
frappe.bold(dimension_value),
frappe.bold(
", ".join([f"{dimension}: {values.get('value')}" for dimension, values in dimensions.items()])
),
self.posting_date,
self.posting_time,
frappe.get_desk_link(self.voucher_type, self.voucher_no),

View File

@@ -1227,32 +1227,23 @@ class StockReconciliation(StockController):
def get_batch_qty_for_stock_reco(
item_code, warehouse, batch_no, posting_date, posting_time, voucher_no, sle_creation
):
ledger = frappe.qb.DocType("Stock Ledger Entry")
posting_datetime = get_combine_datetime(posting_date, posting_time)
query = (
frappe.qb.from_(ledger)
.select(
Sum(ledger.actual_qty).as_("batch_qty"),
qty = (
get_batch_qty(
batch_no,
warehouse,
item_code,
creation=sle_creation,
posting_date=posting_date,
posting_time=posting_time,
ignore_voucher_nos=[voucher_no],
for_stock_levels=True,
consider_negative_batches=True,
do_not_check_future_batches=True,
)
.where(
(ledger.item_code == item_code)
& (ledger.warehouse == warehouse)
& (ledger.docstatus == 1)
& (ledger.is_cancelled == 0)
& (ledger.batch_no == batch_no)
& (ledger.voucher_no != voucher_no)
& (
(ledger.posting_datetime < posting_datetime)
| ((ledger.posting_datetime == posting_datetime) & (ledger.creation < sle_creation))
)
)
.groupby(ledger.batch_no)
or 0
)
sle = query.run(as_dict=True)
return flt(sle[0].batch_qty) if sle else 0
return flt(qty)
@frappe.whitelist()

View File

@@ -20,6 +20,9 @@ def execute(filters=None):
def get_chart_data(data, filters):
def wrap_in_quotes(label):
return f"'{label}'"
if not data:
return []
@@ -36,6 +39,9 @@ def get_chart_data(data, filters):
data = data[:10]
for row in data:
if row[0] == wrap_in_quotes(_("Total")):
continue
labels.append(row[0])
datapoints.append(row[-1])

View File

@@ -273,6 +273,7 @@ class FIFOSlots:
else:
serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle) or []
serial_nos = self.uppercase_serial_nos(serial_nos)
if d.actual_qty > 0:
self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
else:
@@ -289,6 +290,10 @@ class FIFOSlots:
return self.item_details
def uppercase_serial_nos(self, serial_nos):
"Convert serial nos to uppercase for uniformity."
return [sn.upper() for sn in serial_nos]
def __init_key_stores(self, row: dict) -> tuple:
"Initialise keys and FIFO Queue."

View File

@@ -647,10 +647,32 @@ class update_entries_after:
if sle.dependant_sle_voucher_detail_no:
entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
if sle.voucher_type == "Stock Entry" and is_repack_entry(sle.voucher_no):
# for repack entries, we need to repost both source and target warehouses
self.update_distinct_item_warehouses_for_repack(sle)
if self.exceptions:
self.raise_exceptions()
def update_distinct_item_warehouses_for_repack(self, sle):
sles = (
frappe.get_all(
"Stock Ledger Entry",
filters={
"voucher_type": "Stock Entry",
"voucher_no": sle.voucher_no,
"actual_qty": (">", 0),
"is_cancelled": 0,
"voucher_detail_no": ("!=", sle.dependant_sle_voucher_detail_no),
},
fields=["*"],
)
or []
)
for dependant_sle in sles:
self.update_distinct_item_warehouses(dependant_sle)
def has_stock_reco_with_serial_batch(self, sle):
if (
sle.voucher_type == "Stock Reconciliation"
@@ -696,6 +718,13 @@ class update_entries_after:
{"item_code": self.item_code, "warehouse": self.args.warehouse}
)
key = (self.item_code, self.args.warehouse)
if key in self.distinct_item_warehouses and self.distinct_item_warehouses[key].get(
"transfer_entry_to_repost"
):
# only repost stock entries
args["filter_voucher_type"] = "Stock Entry"
return list(self.get_sle_after_datetime(args))
def get_dependent_entries_to_fix(self, entries_to_fix, sle):
@@ -729,8 +758,10 @@ class update_entries_after:
if getdate(existing_sle.get("posting_date")) > getdate(dependant_sle.posting_date):
self.distinct_item_warehouses[key] = val
self.new_items_found = True
elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry(
dependant_sle.voucher_no
elif (
dependant_sle.actual_qty > 0
and dependant_sle.voucher_type == "Stock Entry"
and is_transfer_stock_entry(dependant_sle.voucher_no)
):
if self.distinct_item_warehouses[key].get("transfer_entry_to_repost"):
return
@@ -1156,7 +1187,11 @@ class update_entries_after:
def get_dynamic_incoming_outgoing_rate(self, sle):
# Get updated incoming/outgoing rate from transaction
if sle.recalculate_rate or self.has_landed_cost_based_on_pi(sle):
if (
sle.recalculate_rate
or self.has_landed_cost_based_on_pi(sle)
or (sle.voucher_type == "Stock Entry" and sle.actual_qty > 0 and is_repack_entry(sle.voucher_no))
):
rate = self.get_incoming_outgoing_rate_from_transaction(sle)
if flt(sle.actual_qty) >= 0:
@@ -1813,6 +1848,9 @@ def get_stock_ledger_entries(
if operator in (">", "<=") and previous_sle.get("name"):
conditions += " and name!=%(name)s"
if previous_sle.get("filter_voucher_type"):
conditions += " and voucher_type = %(filter_voucher_type)s"
if extra_cond:
conditions += f"{extra_cond}"
@@ -1908,8 +1946,7 @@ def get_valuation_rate(
& (table.warehouse == warehouse)
& (table.batch_no == batch_no)
& (table.is_cancelled == 0)
& (table.voucher_no != voucher_no)
& (table.voucher_type != voucher_type)
& ((table.voucher_no != voucher_no) | (table.voucher_type != voucher_type))
)
)
@@ -2454,3 +2491,8 @@ def get_incoming_rate_for_serial_and_batch(item_code, row, sn_obj):
incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(row.batch_no)))
return incoming_rate
@frappe.request_cache
def is_repack_entry(stock_entry_id):
return frappe.get_cached_value("Stock Entry", stock_entry_id, "purpose") == "Repack"

View File

@@ -543,6 +543,12 @@ class SubcontractingReceipt(SubcontractingController):
for row in self.items:
precision = row.precision("qty")
# if allow alternative item, ignore the validation as per BOM required qty
is_allow_alternative_item = frappe.db.get_value("BOM", row.bom, "allow_alternative_item")
if is_allow_alternative_item:
continue
for bom_item in self._get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items")
):

View File

@@ -199,7 +199,7 @@ def get_issue_list(doctype, txt, filters, limit_start, limit_page_length=20, ord
customer = contact_doc.get_link_for("Customer")
ignore_permissions = False
if is_website_user():
if is_website_user() and user != "Guest":
if not filters:
filters = {}

View File

@@ -16,9 +16,30 @@ def transaction_processing(data, from_doctype, to_doctype):
else:
deserialized_data = data
skipped_records = [d for d in deserialized_data if d.get("status") in ("On Hold", "Closed")]
deserialized_data = [d for d in deserialized_data if d.get("status") not in ("On Hold", "Closed")]
length_of_data = len(deserialized_data)
frappe.msgprint(_("Started a background job to create {1} {0}").format(to_doctype, length_of_data))
skipped_msg = ""
if skipped_records:
skipped_msg = _("{0} creation for the following records will be skipped.").format(to_doctype)
skipped_msg += (
"<br><br><ul>"
+ "".join(_("<li>{}</li>").format(frappe.bold(row.get("name"))) for row in skipped_records)
+ "</ul>"
)
if not length_of_data:
frappe.msgprint(skipped_msg)
return
frappe.msgprint(
_("Started a background job to create {1} {0}. {2}").format(to_doctype, length_of_data, skipped_msg)
)
frappe.enqueue(
job,
deserialized_data=deserialized_data,

View File

@@ -2,14 +2,6 @@
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Rename Tool", {
onload: function (frm) {
return frappe.call({
method: "erpnext.utilities.doctype.rename_tool.rename_tool.get_doctypes",
callback: function (r) {
frm.set_df_property("select_doctype", "options", r.message);
},
});
},
refresh: function (frm) {
frm.disable_save();

View File

@@ -8,27 +8,13 @@
"doctype": "DocType",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "Type of document to rename.",
"fieldname": "select_doctype",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Select DocType",
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"description": "Type of document to rename.",
"fieldname": "select_doctype",
"fieldtype": "Link",
"label": "Select DocType",
"link_filters": "[[\"DocType\",\"allow_rename\",\"=\",1],[\"DocType\",\"module\",\"!=\",\"Core\"]]",
"options": "DocType"
},
{
"allow_on_submit": 0,
"bold": 0,
@@ -72,22 +58,18 @@
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 1,
"icon": "fa fa-magic",
"idx": 1,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 1,
"modified": "2015-10-19 03:04:49.097140",
"modified_by": "Administrator",
"module": "Utilities",
"name": "Rename Tool",
"owner": "Administrator",
],
"hide_toolbar": 1,
"icon": "fa fa-magic",
"idx": 1,
"issingle": 1,
"links": [],
"max_attachments": 1,
"modified": "2025-12-09 14:18:33.838623",
"modified_by": "Administrator",
"module": "Utilities",
"name": "Rename Tool",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
@@ -112,4 +94,4 @@
],
"read_only": 0,
"read_only_onload": 0
}
}

View File

@@ -7,6 +7,7 @@
import frappe
from frappe.model.document import Document
from frappe.model.rename_doc import bulk_rename
from frappe.utils.deprecations import deprecated
class RenameTool(Document):
@@ -19,13 +20,14 @@ class RenameTool(Document):
from frappe.types import DF
file_to_rename: DF.Attach | None
select_doctype: DF.Literal
select_doctype: DF.Link | None
# end: auto-generated types
pass
@frappe.whitelist()
@deprecated
def get_doctypes():
return frappe.db.sql_list(
"""select name from tabDocType