Compare commits

...

467 Commits

Author SHA1 Message Date
Frappe PR Bot
6e7de0ac47 chore(release): Bumped to Version 15.92.3
## [15.92.3](https://github.com/frappe/erpnext/compare/v15.92.2...v15.92.3) (2025-12-19)

### Bug Fixes

* **stock:** ignore reserved stock while calculating batch qty ([35478bb](35478bbf91))
2025-12-19 13:25:33 +00:00
rohitwaghchaure
2277b1aff5 Merge pull request #51221 from frappe/mergify/bp/version-15/pr-51220
fix(stock): ignore reserved stock while calculating batch qty (backport #51214) (backport #51220)
2025-12-19 18:54:08 +05:30
rohitwaghchaure
58c793f14e chore: fix conflicts
Removed logic for handling reserved stock when calculating batch quantity.

(cherry picked from commit 9ade0725e8)
2025-12-19 12:55:11 +00:00
Sudharsanan11
08cd08adcd test(stock): add test for ignore reserve stock
(cherry picked from commit 4d8ec5f54c)
(cherry picked from commit b20405dbf2)
2025-12-19 12:55:11 +00:00
Sudharsanan11
35478bbf91 fix(stock): ignore reserved stock while calculating batch qty
(cherry picked from commit b23c6e2687)

# Conflicts:
#	erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py
(cherry picked from commit ac2402dd2a)
2025-12-19 12:55:11 +00:00
Frappe PR Bot
54ed428225 chore(release): Bumped to Version 15.92.2
## [15.92.2](https://github.com/frappe/erpnext/compare/v15.92.1...v15.92.2) (2025-12-18)

### Bug Fixes

* **pegged currencies:** skip adding currencies_to_add items on  pegged_currency_item if source_currency or pegged_against currency doc does not exist (backport [#51188](https://github.com/frappe/erpnext/issues/51188)) ([#51203](https://github.com/frappe/erpnext/issues/51203)) ([195f902](195f90232d))
2025-12-18 12:38:51 +00:00
Diptanil Saha
c046dad2c3 Merge pull request #51206 from frappe/mergify/bp/version-15/pr-51203
fix(pegged currencies): skip adding currencies_to_add items on  pegged_currency_item if source_currency or pegged_against currency doc does not exist (backport #51188)
2025-12-18 18:07:29 +05:30
mergify[bot]
195f90232d fix(pegged currencies): skip adding currencies_to_add items on pegged_currency_item if source_currency or pegged_against currency doc does not exist (backport #51188) (#51203)
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
fix(pegged currencies): skip adding currencies_to_add items on  pegged_currency_item if source_currency or pegged_against currency doc does not exist (#51188)

(cherry picked from commit 8ef09c0dc0)
2025-12-18 12:18:08 +00:00
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
Frappe PR Bot
dab17c194c chore(release): Bumped to Version 15.91.0
# [15.91.0](https://github.com/frappe/erpnext/compare/v15.90.1...v15.91.0) (2025-12-02)

### Bug Fixes

* add validation for cancelled reposting entries ([085d685](085d685488))
* add validation for company linked address fields ([0aed8c0](0aed8c04c6))
* **barcode_scanner:** set serial and batch before item to prevent FIFO override ([7d7f929](7d7f929cfc))
* conflicts ([199e25e](199e25ec06))
* correct field name for subcontracted items in material request ([4b49080](4b49080bc4))
* do not override source document in serial no ([69c6b2f](69c6b2f463))
* **email campaign:** send emails using bcc ([b660b90](b660b90adc))
* **Employee:** add/delete user permission (backport [#47016](https://github.com/frappe/erpnext/issues/47016)) ([#50761](https://github.com/frappe/erpnext/issues/50761)) ([821f3f5](821f3f5884))
* enhance SalesOrderController setup method to call super.setup ([7805ccf](7805ccf176))
* exclude is_group records ([a444325](a444325bd1))
* include accounting dimensions in stock entries created during asset repair. ([26872c3](26872c3c25))
* incorrect positional param for `get_field_precision` util (backport [#50764](https://github.com/frappe/erpnext/issues/50764)) ([#50795](https://github.com/frappe/erpnext/issues/50795)) ([ff1ca9d](ff1ca9d480))
* item price not considering based on valid_upto ([dfda8e6](dfda8e6241))
* **Job Card:** avoid Type Error when completed_qty is None ([#50447](https://github.com/frappe/erpnext/issues/50447)) ([cac9eed](cac9eed306))
* label for warehouse based on material request type ([8ee7c47](8ee7c47fdf))
* mandatory depends on for the rejected inventory dimension field ([8c62080](8c620802f0))
* negative batch in subcontracting receipt ([5def006](5def006033))
* **payment reconciliation:** added a hint that posting date can be changed on exchange gain/loss reconcile dialog ([0e03607](0e0360781e))
* **payment-recon:** add validation for outstanding of dr_cr ([70feb50](70feb500f6))
* **pos:** add negative stock validation for product bundle ([46a49a1](46a49a134d))
* remove unused translation files (<100 lines) ([7f7c5f2](7f7c5f2381))
* resolve conflict ([bd795f5](bd795f5546))
* **stock entry:** use fg item expense account for direct manufacturing entry ([4ca5e9e](4ca5e9eef8))
* two primary buttons ([1d2fccf](1d2fccfc0b))
* use asset in against_voucher while posting gl entries for capitalized asset repairs ([80642ed](80642edf4f))
* use posting_date instead of bill_date from purchase invoice ([c12a560](c12a560c63))

### Features

* add stock uom read only field to stock reconciliation item doctype ([5711225](57112258e6))
2025-12-02 16:31:39 +00:00
Rohit Waghchaure
8a3148eee6 fix: cost center not reset
(cherry picked from commit 29f2ecbd6f)
2025-12-02 16:30:41 +00:00
Diptanil Saha
1f79242366 Merge pull request #50868 from frappe/version-15-hotfix 2025-12-02 22:00:12 +05:30
Diptanil Saha
3f673a6848 Merge pull request #50886 from frappe/mergify/bp/version-15-hotfix/pr-50882
fix: mandatory depends on for the rejected inventory dimension field (backport #50882)
2025-12-02 21:26:26 +05:30
Diptanil Saha
293f114c9d Merge pull request #50847 from barredterra/rm-unused-translations 2025-12-02 21:19:55 +05:30
Diptanil Saha
252cc89ec7 Merge pull request #50871 from frappe/mergify/bp/version-15-hotfix/pr-50846 2025-12-02 21:19:40 +05:30
Diptanil Saha
3eaccfe201 Merge pull request #50873 from frappe/mergify/bp/version-15-hotfix/pr-50773
fix: add validation for cancelled reposting entries (backport #50773)
2025-12-02 21:15:56 +05:30
Diptanil Saha
653bb1072f Merge pull request #50885 from frappe/mergify/bp/version-15-hotfix/pr-50864
fix: exclude is_group records (backport #50864)
2025-12-02 21:14:35 +05:30
Diptanil Saha
0a64e43e92 chore: resolve linter issue 2025-12-02 21:04:00 +05:30
diptanilsaha
020db922b7 chore: resolve conflicts 2025-12-02 20:56:54 +05:30
rohitwaghchaure
a67a11e933 Merge pull request #50884 from rohitwaghchaure/fixed-donot-override-source
fix: do not override source document in serial no
2025-12-02 20:55:22 +05:30
Rohit Waghchaure
8c620802f0 fix: mandatory depends on for the rejected inventory dimension field
(cherry picked from commit 5daa625fe8)
2025-12-02 15:10:40 +00:00
0xD0M1M0
a31fb2ac6c fix: Adjust asset purchase amounts based on docstatus
allows cancelation
2025-12-02 16:09:56 +01:00
ravibharathi656
a444325bd1 fix: exclude is_group records
(cherry picked from commit e08805128b)

# Conflicts:
#	erpnext/setup/doctype/customer_group/customer_group.json
#	erpnext/setup/doctype/item_group/item_group.json
#	erpnext/setup/doctype/supplier_group/supplier_group.json
#	erpnext/setup/doctype/territory/territory.json
2025-12-02 15:07:39 +00:00
rohitwaghchaure
2f4b1341d2 Merge pull request #50878 from frappe/mergify/bp/version-15-hotfix/pr-50808
fix(stock entry): use fg item expense account for direct manufacturing entry (backport #50808)
2025-12-02 20:37:39 +05:30
Diptanil Saha
7640944bb9 Merge pull request #50881 from frappe/mergify/bp/version-15-hotfix/pr-50372
fix: add validation for company linked address field (backport #50372)
2025-12-02 20:36:22 +05:30
Rohit Waghchaure
69c6b2f463 fix: do not override source document in serial no 2025-12-02 20:34:55 +05:30
Pugazhendhi Velu
ef6f2389a0 test: add minimal test case
(cherry picked from commit e64b6db2eb)
2025-12-02 14:45:03 +00:00
Pugazhendhi Velu
1d4b97c619 test: add test for company linked address fields
(cherry picked from commit e10007c646)
2025-12-02 14:45:02 +00:00
Pugazhendhi Velu
0aed8c04c6 fix: add validation for company linked address fields
(cherry picked from commit 800a44a65f)
2025-12-02 14:45:02 +00:00
rohitwaghchaure
b1b6953aed chore: fix conflicts 2025-12-02 17:44:17 +05:30
rohitwaghchaure
b2ea5620b2 chore: fix conflicts
Removed the expense account assignment for subcontracting delivery.
2025-12-02 17:42:18 +05:30
rohitwaghchaure
743b179b08 Merge pull request #50877 from frappe/mergify/bp/version-15-hotfix/pr-50850
fix(barcode_scanner): set serial and batch before item to prevent FIFO override (backport #50850)
2025-12-02 17:41:31 +05:30
Pugazhendhi Velu
4553d04c38 test: add test for fg item expense account in direct manufacturing
(cherry picked from commit ba2411b4ee)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/test_stock_entry.py
2025-12-02 12:03:37 +00:00
Pugazhendhi Velu
4ca5e9eef8 fix(stock entry): use fg item expense account for direct manufacturing entry
(cherry picked from commit ce1312764f)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2025-12-02 12:03:37 +00:00
Pugazhendhi Velu
7d7f929cfc fix(barcode_scanner): set serial and batch before item to prevent FIFO override
(cherry picked from commit 92ec633a5c)
2025-12-02 12:02:36 +00:00
l0gesh29
085d685488 fix: add validation for cancelled reposting entries
(cherry picked from commit d8fc369e38)
2025-12-02 11:47:36 +00:00
Diptanil Saha
0458c548ec chore: resolve conflict 2025-12-02 17:14:00 +05:30
Sudharsanan11
e5457f8bb7 test(pos): add test for product bundle negative stock validation
(cherry picked from commit 2612152456)

# Conflicts:
#	erpnext/accounts/doctype/pos_invoice/pos_invoice.py
2025-12-02 11:39:09 +00:00
Sudharsanan11
46a49a134d fix(pos): add negative stock validation for product bundle
(cherry picked from commit 38b4536300)
2025-12-02 11:39:08 +00:00
Khushi Rawat
25a9327b14 Merge pull request #50862 from frappe/mergify/bp/version-15-hotfix/pr-50794
fix: use asset in against_voucher while posting gl entries for capitalised asset repairs (backport #50794)
2025-12-02 12:45:20 +05:30
Mihir Kandoi
d0d38214c5 Merge pull request #50790 from Abdeali099/fix-incorrect-fieldname 2025-12-02 12:30:48 +05:30
Khushi Rawat
991c46d058 Merge pull request #50858 from frappe/mergify/bp/version-15-hotfix/pr-50793
fix: include accounting dimensions in stock entries created during asset repair. (backport #50793)
2025-12-02 12:23:10 +05:30
rohitwaghchaure
ca9bd8b499 Merge pull request #50845 from frappe/mergify/bp/version-15-hotfix/pr-50844
fix: label for warehouse based on material request type (backport #50844)
2025-12-02 12:22:18 +05:30
Khushi Rawat
199e25ec06 fix: conflicts 2025-12-02 12:20:48 +05:30
Navin-S-R
f38fb68d62 chore: reload asset doc before assertEqual
(cherry picked from commit 8c35a6ecdd)
2025-12-02 06:37:54 +00:00
Navin-S-R
3a22d29d7b test: add unit test to validate capitalized asset repair gl entries being booked against the asset
(cherry picked from commit bcf6deec9a)

# Conflicts:
#	erpnext/assets/doctype/asset_repair/test_asset_repair.py
2025-12-02 06:37:54 +00:00
Navin S R
80642edf4f fix: use asset in against_voucher while posting gl entries for capitalized asset repairs
(cherry picked from commit a7e43eddad)
2025-12-02 06:37:54 +00:00
ljain112
9a3e1058f6 refactor: show_general ledger for consistency with other doctyoes
(cherry picked from commit cdbe8b909b)
2025-12-02 06:27:21 +00:00
ljain112
26872c3c25 fix: include accounting dimensions in stock entries created during asset repair.
(cherry picked from commit 147a5ee953)
2025-12-02 06:27:21 +00:00
rohitwaghchaure
dba3f3d335 chore: fix conflicts 2025-12-02 11:04:27 +05:30
barredterra
7f7c5f2381 fix: remove unused translation files (<100 lines)
These translate <=1% of available strings, so cannot be deemed useful.
2025-12-01 17:38:13 +01:00
Rohit Waghchaure
8ee7c47fdf fix: label for warehouse based on material request type
(cherry picked from commit 699e9b4452)

# Conflicts:
#	erpnext/stock/doctype/material_request/material_request.js
2025-12-01 15:58:39 +00:00
Raffael Meyer
441a2bcf38 chore: backport translations from develop (#50842) 2025-12-01 14:50:04 +00:00
mergify[bot]
821f3f5884 fix(Employee): add/delete user permission (backport #47016) (#50761)
Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
2025-12-01 12:49:37 +01:00
Diptanil Saha
ca70e8e9a6 Merge pull request #50825 from frappe/mergify/bp/version-15-hotfix/pr-50797
fix(payment-recon): add validation for outstanding of dr_cr (backport #50797)
2025-12-01 13:27:15 +05:30
l0gesh29
70feb500f6 fix(payment-recon): add validation for outstanding of dr_cr
(cherry picked from commit 765f9a9bbf)
2025-12-01 07:41:28 +00:00
rohitwaghchaure
bf8b3d0546 Merge pull request #50818 from frappe/mergify/bp/version-15-hotfix/pr-50799
fix: negative batch in subcontracting receipt (backport #50799)
2025-12-01 12:50:42 +05:30
Diptanil Saha
07d8bc7852 Merge pull request #50819 from frappe/mergify/bp/version-15-hotfix/pr-50814
fix(email campaign): send emails using bcc (backport #50814)
2025-12-01 12:30:55 +05:30
diptanilsaha
b660b90adc fix(email campaign): send emails using bcc
(cherry picked from commit 7e8d19b0c8)
2025-12-01 06:17:15 +00:00
Rohit Waghchaure
5def006033 fix: negative batch in subcontracting receipt
(cherry picked from commit 71e46b3ef5)
2025-12-01 06:15:37 +00:00
Khushi Rawat
ade6acccfb Merge pull request #50800 from frappe/mergify/bp/version-15-hotfix/pr-50772
fix: use posting_date instead of bill_date from purchase invoice (backport #50772)
2025-11-29 00:28:11 +05:30
Khushi Rawat
bd795f5546 fix: resolve conflict 2025-11-28 17:26:33 +05:30
Navin S R
c12a560c63 fix: use posting_date instead of bill_date from purchase invoice
(cherry picked from commit 145d40dec8)

# Conflicts:
#	erpnext/assets/doctype/asset/asset.py
2025-11-28 11:49:33 +00:00
mergify[bot]
ff1ca9d480 fix: incorrect positional param for get_field_precision util (backport #50764) (#50795) 2025-11-28 08:50:03 +00:00
Abdeali Chharchhoda
4b49080bc4 fix: correct field name for subcontracted items in material request 2025-11-28 12:33:38 +05:30
Diptanil Saha
64e6b36d04 Merge pull request #50789 from frappe/mergify/bp/version-15-hotfix/pr-50642
fix(payment reconciliation): added a hint that posting date can be changed on exchange gain/loss reconcile dialog (backport #50642)
2025-11-28 11:54:32 +05:30
Jatin3128
0e0360781e fix(payment reconciliation): added a hint that posting date can be changed on exchange gain/loss reconcile dialog
(cherry picked from commit 4b612c64a8)
2025-11-28 06:22:00 +00:00
Mihir Kandoi
c62be10620 Merge pull request #50778 from frappe/mergify/bp/version-15-hotfix/pr-50777 2025-11-27 17:28:07 +05:30
Mihir Kandoi
fa541a2604 chore: make unnecessary field read only and show only when required
(cherry picked from commit aab7cd1ae6)
2025-11-27 11:41:54 +00:00
Diptanil Saha
5590b8d40b Merge pull request #50558 from efeone/pos_rate_issue 2025-11-27 16:33:50 +05:30
Mihir Kandoi
a32165016d Merge pull request #50774 from mihir-kandoi/gh50218 2025-11-27 14:56:44 +05:30
Mihir Kandoi
57112258e6 feat: add stock uom read only field to stock reconciliation item doctype 2025-11-27 14:39:19 +05:30
rohitwaghchaure
0bec404e69 Merge pull request #50770 from frappe/mergify/bp/version-15-hotfix/pr-50769
fix: two primary buttons (backport #50769)
2025-11-27 12:06:10 +05:30
Rohit Waghchaure
1d2fccfc0b fix: two primary buttons
(cherry picked from commit f68515210b)
2025-11-27 06:27:09 +00:00
Raffael Meyer
cac9eed306 fix(Job Card): avoid Type Error when completed_qty is None (#50447) 2025-11-26 13:16:47 +01:00
Frappe PR Bot
2bf12a6683 chore(release): Bumped to Version 15.90.1
## [15.90.1](https://github.com/frappe/erpnext/compare/v15.90.0...v15.90.1) (2025-11-26)

### Bug Fixes

* enhance SalesOrderController setup method to call super.setup ([38c4453](38c44533b3))
2025-11-26 09:12:23 +00:00
Diptanil Saha
6205be5e73 Merge pull request #50755 from frappe/mergify/bp/version-15/pr-50754
fix: enhance SalesOrderController setup method to call super.setup (backport #50752)
2025-11-26 14:40:58 +05:30
ljain112
38c44533b3 fix: enhance SalesOrderController setup method to call super.setup
(cherry picked from commit 563c2998ca)
(cherry picked from commit 7805ccf176)
2025-11-26 09:08:27 +00:00
Diptanil Saha
a6713b176b Merge pull request #50754 from frappe/mergify/bp/version-15-hotfix/pr-50752
fix: enhance SalesOrderController setup method to call super.setup (backport #50752)
2025-11-26 14:35:21 +05:30
ljain112
7805ccf176 fix: enhance SalesOrderController setup method to call super.setup
(cherry picked from commit 563c2998ca)
2025-11-26 09:02:56 +00:00
Frappe PR Bot
a66ce02520 chore(release): Bumped to Version 15.90.0
# [15.90.0](https://github.com/frappe/erpnext/compare/v15.89.2...v15.90.0) (2025-11-25)

### Bug Fixes

* add filter company and status to job card employee ([015f946](015f946a14))
* add missing translate function ([475eada](475eada727))
* add return status for purchase receipt ([8ccb9a5](8ccb9a5ad2))
* add validation for FG Items as per BOM qty (backport [#50579](https://github.com/frappe/erpnext/issues/50579)) ([#50715](https://github.com/frappe/erpnext/issues/50715)) ([1995291](1995291194))
* apply precision for scrap items amount ([5b60fbb](5b60fbbd30))
* **customer:** link contact and addresses if created from lead/opportunity/prospect ([b1d40de](b1d40de87e))
* ignore reserved batches from total available batches ([673b893](673b893942))
* incorrect query filter when selecting primary customer adr ([#50727](https://github.com/frappe/erpnext/issues/50727)) ([e8e09cf](e8e09cf8ea))
* **ledger-summary-report:** show party group and territory ([56f03ae](56f03aee02))
* **manufacturing:** apply precision for bom amount and rm_cost_per_qty ([2678694](2678694c5f))
* pick list status doesn't update when DN created from it and PL was created from SO ([2809c46](2809c46a6e))
* prevent pi status from changing on asset repair ([3f2081b](3f2081b440))
* pricing rule was ignoring time validity ([f62e5e6](f62e5e69b8))
* **product bundle:** fields reset if doc is new ([4ba4da0](4ba4da090d))
* **purchase_receipt:** add internal_and_external_links field to show purchase invoice connection count ([89fcdbf](89fcdbf56b))
* redundant message on bom save ([5b16740](5b1674018b))
* remove disabled warehouse in get_warehouses_based_on_account ([aa94c91](aa94c91c12))
* serial batch selector shown only once ([25cd230](25cd230471))
* show current company warehouse only in get material from bom MR ([1d6e3e4](1d6e3e4e7d))
* tests ([45bc218](45bc218acb))
* unhide zero val checkbox ([a247337](a24733791d))
* unknown column error ([2e9a0cb](2e9a0cb01c))
* use current_tax_amount value for base_total_taxes_and_charges ([7ed3c6d](7ed3c6d18a))
* validate sabb autocreation when disabled ([85c0c16](85c0c16964))
* validation for SABB deletion ([0bc98b6](0bc98b609f))

### Features

* **accounting-dimension:** add dynamic triggers for custom accounting dimensions ([#50621](https://github.com/frappe/erpnext/issues/50621)) ([2b7d586](2b7d58602d))
* modify accounting dimension as multiselect field ([6b6e017](6b6e017e36))
* **reports:** preserve accounting dimension filters while navigating between reports ([02a1f81](02a1f815da))
2025-11-25 15:02:30 +00:00
ruthra kumar
3c43b42a01 Merge pull request #50741 from frappe/version-15-hotfix
chore: release v15
2025-11-25 20:31:01 +05:30
ruthra kumar
8ed3a0ec65 Merge branch 'version-15' into version-15-hotfix 2025-11-25 20:14:37 +05:30
mergify[bot]
488d635dc9 chore: switched frankfurter domain from frankfurter.app to frankfurter.dev (backport #50734) (#50740)
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2025-11-25 15:49:53 +05:30
ruthra kumar
08d230b3e3 Merge pull request #50731 from frappe/mergify/bp/version-15-hotfix/pr-50561
fix(ledger-summary-report): show party group and territory (backport #50561)
2025-11-25 12:33:49 +05:30
Mihir Kandoi
f383fafb15 Merge pull request #50728 from frappe/mergify/bp/version-15-hotfix/pr-50727
fix: incorrect query filter when selecting primary customer adr (backport #50727)
2025-11-25 12:14:17 +05:30
ruthra kumar
38124a7616 chore: resolve conflicts 2025-11-25 12:01:37 +05:30
l0gesh29
56cf5382f0 test: add party_group, territory in json
(cherry picked from commit 8f91919933)

# Conflicts:
#	erpnext/accounts/report/customer_ledger_summary/test_customer_ledger_summary.py
2025-11-25 05:38:57 +00:00
l0gesh29
56f03aee02 fix(ledger-summary-report): show party group and territory
(cherry picked from commit 231479a6e2)

# Conflicts:
#	erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
2025-11-25 05:38:57 +00:00
Mihir Kandoi
841f5c24ad chore: resolve conflicts 2025-11-25 10:51:39 +05:30
ruthra kumar
e8051ba180 Merge pull request #50729 from frappe/mergify/bp/version-15-hotfix/pr-50621
feat(accounting-dimension): add dynamic triggers for custom accounting dimensions (backport #50621)
2025-11-25 10:26:47 +05:30
Logesh Periyasamy
2b7d58602d feat(accounting-dimension): add dynamic triggers for custom accounting dimensions (#50621)
* feat: add dynamic triggers for custom accounting dimensions

* feat: add accounting dimension trigger call in setup event

* chore: ignore cur_frm semgrep rules

* chore: move function to transaction.js

(cherry picked from commit 5e58e344b2)
2025-11-25 04:51:32 +00:00
Mihir Kandoi
e8e09cf8ea fix: incorrect query filter when selecting primary customer adr (#50727)
(cherry picked from commit c2b8b97d7d)

# Conflicts:
#	erpnext/selling/doctype/customer/customer.json
2025-11-25 04:49:20 +00:00
Khushi Rawat
188c633d6e Merge pull request #50544 from aerele/asset-repair-pi-status
fix: prevent pi status from changing on asset repair
2025-11-24 23:28:33 +05:30
mergify[bot]
1995291194 fix: add validation for FG Items as per BOM qty (backport #50579) (#50715)
Co-authored-by: Kavin <78342682+kavin0411@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
Co-authored-by: Kavin <78342682+kavin-114@users.noreply.github.com>
fix: add validation for FG Items as per BOM qty (#50579)
2025-11-24 11:18:18 +00:00
Mihir Kandoi
bb0d75eb78 Merge pull request #50717 from frappe/mergify/bp/version-15-hotfix/pr-50716
fix: add missing translate function (backport #50716)
2025-11-24 13:53:48 +05:30
El-Shafei H.
475eada727 fix: add missing translate function
(cherry picked from commit 56def01240)
2025-11-24 07:39:09 +00:00
mergify[bot]
6cffba9a71 Merge pull request #50713 from frappe/mergify/bp/version-15-hotfix/pr-50712 2025-11-24 07:17:43 +00:00
Mihir Kandoi
35f9f9f330 Merge pull request #50711 from frappe/mergify/bp/version-15-hotfix/pr-50661
fix(manufacturing): apply precision for bom amount and rm_cost_per_qty (backport #50661)
2025-11-24 11:02:50 +05:30
Pugazhendhi Velu
5b60fbbd30 fix: apply precision for scrap items amount
(cherry picked from commit 9194e6350a)
2025-11-24 05:08:42 +00:00
Pugazhendhi Velu
2678694c5f fix(manufacturing): apply precision for bom amount and rm_cost_per_qty
(cherry picked from commit 57f9353d90)
2025-11-24 05:08:42 +00:00
Mihir Kandoi
b6a80da457 Merge pull request #50709 from frappe/mergify/bp/version-15-hotfix/pr-50707
fix: unknown column error (backport #50707)
2025-11-23 19:54:21 +05:30
Mihir Kandoi
2e9a0cb01c fix: unknown column error
(cherry picked from commit 3b7d7aed4c)
2025-11-23 14:08:48 +00:00
Frappe PR Bot
38c1867ade chore(release): Bumped to Version 15.89.2
## [15.89.2](https://github.com/frappe/erpnext/compare/v15.89.1...v15.89.2) (2025-11-21)

### Bug Fixes

* use current_tax_amount value for base_total_taxes_and_charges ([c082eda](c082edabf4))
2025-11-21 17:34:52 +00:00
Diptanil Saha
a48b999af9 Merge pull request #50691 from frappe/mergify/bp/version-15/pr-50690
fix: use current_tax_amount value for base_total_taxes_and_charges (backport #50476) (backport #50690)
2025-11-21 23:03:28 +05:30
Pugazhendhi Velu
c082edabf4 fix: use current_tax_amount value for base_total_taxes_and_charges
(cherry picked from commit 5a3fcbedb5)
(cherry picked from commit 7ed3c6d18a)
2025-11-21 17:16:54 +00:00
Diptanil Saha
60ec7d0fb8 Merge pull request #50690 from frappe/mergify/bp/version-15-hotfix/pr-50476
fix: use current_tax_amount value for base_total_taxes_and_charges (backport #50476)
2025-11-21 22:31:13 +05:30
Pugazhendhi Velu
7ed3c6d18a fix: use current_tax_amount value for base_total_taxes_and_charges
(cherry picked from commit 5a3fcbedb5)
2025-11-21 16:41:52 +00:00
Frappe PR Bot
b7e4fb9d83 chore(release): Bumped to Version 15.89.1
## [15.89.1](https://github.com/frappe/erpnext/compare/v15.89.0...v15.89.1) (2025-11-21)

### Bug Fixes

* ignore reserved batches from total available batches ([64950d3](64950d39b5))
2025-11-21 10:36:37 +00:00
rohitwaghchaure
e49e7b621d Merge pull request #50668 from frappe/mergify/bp/version-15/pr-50612
fix: ignore reserved batches from total available batches (backport #50612)
2025-11-21 16:05:05 +05:30
Mihir Kandoi
0a67d20ff8 Merge pull request #50671 from frappe/mergify/bp/version-15-hotfix/pr-50667
fix: pricing rule was ignoring time validity (backport #50667)
2025-11-21 13:08:25 +05:30
Mihir Kandoi
83c6d861eb Merge pull request #50670 from frappe/mergify/bp/version-15-hotfix/pr-50655
fix: pick list status doesn't update when DN created from it and PL w... (backport #50655)
2025-11-21 13:00:31 +05:30
Mihir Kandoi
f62e5e69b8 fix: pricing rule was ignoring time validity
(cherry picked from commit ffae7c4175)
2025-11-21 07:22:07 +00:00
Mihir Kandoi
45bc218acb fix: tests
(cherry picked from commit d26f8aa629)
2025-11-21 07:14:00 +00:00
Mihir Kandoi
2809c46a6e fix: pick list status doesn't update when DN created from it and PL was created from SO
(cherry picked from commit f7b3253683)
2025-11-21 07:14:00 +00:00
Kavin
15c41178d0 test: add unit test for reserved stock validation
(cherry picked from commit 55f2f1c515)
2025-11-21 06:46:17 +00:00
Kavin
64950d39b5 fix: ignore reserved batches from total available batches
(cherry picked from commit 673b893942)
2025-11-21 06:46:17 +00:00
Diptanil Saha
ff1b83025a Merge pull request #50666 from frappe/mergify/bp/version-15-hotfix/pr-50665
fix(customer): link contact and addresses if created from lead/opportunity/prospect (backport #50665)
2025-11-21 07:03:57 +05:30
diptanilsaha
b1d40de87e fix(customer): link contact and addresses if created from lead/opportunity/prospect
(cherry picked from commit 310099f4cd)
2025-11-21 01:18:18 +00: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
Mihir Kandoi
532031c21d Merge pull request #50650 from frappe/mergify/bp/version-15-hotfix/pr-50385
fix: remove disabled warehouse in get_warehouses_based_on_account (backport #50385)
2025-11-20 17:44:02 +05:30
Mihir Kandoi
cb70efb8ed Merge pull request #50653 from frappe/mergify/bp/version-15-hotfix/pr-50639
fix(product bundle): fields reset if doc is new (backport #50639)
2025-11-20 17:43:43 +05:30
Mihir Kandoi
ca0b4696ba Merge pull request #50652 from frappe/mergify/bp/version-15-hotfix/pr-50649
fix: unhide zero val checkbox in stock reco (backport #50649)
2025-11-20 17:43:26 +05:30
Mihir Kandoi
4ba4da090d fix(product bundle): fields reset if doc is new
(cherry picked from commit 7faee7edc2)
2025-11-20 10:42:53 +00:00
rohitwaghchaure
e3b2cc24b2 chore: fix conflicts 2025-11-20 16:12:41 +05:30
Mihir Kandoi
a24733791d fix: unhide zero val checkbox
(cherry picked from commit 20e0313a8c)

# Conflicts:
#	erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
2025-11-20 10:41:55 +00:00
Mihir Kandoi
aa94c91c12 fix: remove disabled warehouse in get_warehouses_based_on_account
(cherry picked from commit ff2d9bf4cb)
2025-11-20 10:33:54 +00:00
Mihir Kandoi
87ffbdf129 Merge pull request #50641 from frappe/mergify/bp/version-15-hotfix/pr-50502
fix(purchase_receipt): add internal_and_external_links field to show … (backport #50502)
2025-11-20 15:45:43 +05:30
rohitwaghchaure
5ef7b8c526 Merge pull request #50648 from frappe/mergify/bp/version-15-hotfix/pr-50646
fix: serial batch selector shown only once (backport #50646)
2025-11-20 15:12:51 +05:30
Mihir Kandoi
25cd230471 fix: serial batch selector shown only once
(cherry picked from commit aa6f09e9a9)
2025-11-20 09:38:37 +00:00
rohitwaghchaure
f05933e814 Merge pull request #50645 from frappe/mergify/bp/version-15-hotfix/pr-50644
fix: validation for SABB deletion (backport #50644)
2025-11-20 14:33:05 +05:30
Diptanil Saha
8df2612694 Merge pull request #50643 from frappe/mergify/bp/version-15-hotfix/pr-50289 2025-11-20 13:58:58 +05:30
Rohit Waghchaure
0bc98b609f fix: validation for SABB deletion
(cherry picked from commit dd4bef0706)
2025-11-20 08:19:48 +00:00
l0gesh29
02a1f815da feat(reports): preserve accounting dimension filters while navigating between reports
(cherry picked from commit fcfcaa76c6)
2025-11-20 07:46:55 +00:00
l0gesh29
6b6e017e36 feat: modify accounting dimension as multiselect field
(cherry picked from commit 3fcd8d84ac)
2025-11-20 07:46:55 +00:00
Karuppasamy B
89fcdbf56b fix(purchase_receipt): add internal_and_external_links field to show purchase invoice connection count
(cherry picked from commit 6c1620ab8c)
2025-11-20 07:03:55 +00:00
rohitwaghchaure
eb2571492f Merge pull request #50612 from aerele/fix/validate-reserved-stock
fix: ignore reserved batches from total available batches
2025-11-19 22:16:00 +05:30
Kavin
55f2f1c515 test: add unit test for reserved stock validation 2025-11-19 17:51:10 +05:30
Kavin
673b893942 fix: ignore reserved batches from total available batches 2025-11-19 17:51:10 +05:30
mergify[bot]
c85ce55f27 Merge pull request #50631 from frappe/mergify/bp/version-15-hotfix/pr-50629
fix: process loss % can be negative (backport #50629)
2025-11-19 11:30:39 +00:00
Mihir Kandoi
a19252e3b3 Merge pull request #50628 from frappe/mergify/bp/version-15-hotfix/pr-50627
fix: show current company warehouse only in get material from bom MR (backport #50627)
2025-11-19 16:29:17 +05:30
Mihir Kandoi
7ece6fd558 Merge pull request #50626 from frappe/mergify/bp/version-15-hotfix/pr-50625
fix: add filter company and status to job card employee (backport #50625)
2025-11-19 16:24:08 +05:30
Mihir Kandoi
1d6e3e4e7d fix: show current company warehouse only in get material from bom MR
(cherry picked from commit 3271eaaf0e)
2025-11-19 10:46:47 +00:00
Mihir Kandoi
015f946a14 fix: add filter company and status to job card employee
(cherry picked from commit 3ca3a6d9bb)
2025-11-19 10:42:32 +00:00
rohitwaghchaure
533a2dbc32 Merge pull request #50607 from frappe/mergify/bp/version-15-hotfix/pr-50512
fix: add return status for purchase receipt (backport #50512)
2025-11-19 14:18:12 +05:30
rohitwaghchaure
99aeb8ecd1 Merge pull request #50606 from frappe/mergify/bp/version-15-hotfix/pr-50486
fix: validate sabb autocreation when disabled (backport #50486)
2025-11-19 14:18:00 +05:30
Mihir Kandoi
371030f8d4 Merge pull request #50615 from frappe/mergify/bp/version-15-hotfix/pr-50614
fix: redundant message on bom save (backport #50614)
2025-11-19 12:53:50 +05:30
Mihir Kandoi
2550b44db8 chore: resolve conflicts 2025-11-19 12:38:47 +05:30
Mihir Kandoi
5b1674018b fix: redundant message on bom save
(cherry picked from commit 074f07694f)

# Conflicts:
#	erpnext/manufacturing/doctype/bom/bom.py
2025-11-19 07:06:11 +00:00
Frappe PR Bot
d8dab986fa chore(release): Bumped to Version 15.89.0
# [15.89.0](https://github.com/frappe/erpnext/compare/v15.88.1...v15.89.0) (2025-11-19)

### Bug Fixes

* add cancelled option in status field ([623a0a9](623a0a932e))
* add condition for allow negative stock in pos (backport [#50369](https://github.com/frappe/erpnext/issues/50369)) ([#50600](https://github.com/frappe/erpnext/issues/50600)) ([2d6640a](2d6640ac61))
* add doctype parameter to lead details for correct company details ([f0eac47](f0eac47037))
* **asset repair:** validate pi status ([2db91ee](2db91ee67e))
* back calcalute total amount from rate and tax_amount in tax withholding details report ([5728299](57282999ad))
* construct batch_nos and serial_nos to avoid NoneType error ([0a0177c](0a0177cb9e))
* correct profit after tax calculation by reducing expenses from income ([627b34a](627b34a120))
* current qty in stock reco ([b4b8459](b4b8459f2c))
* enable allow_negative_stock settings ([2a5c9b4](2a5c9b469c))
* **financial reports:** set fiscal year associated with the default company ([ac40b59](ac40b59665))
* first and last name in supplier quick entry (backport [#50510](https://github.com/frappe/erpnext/issues/50510)) ([#50514](https://github.com/frappe/erpnext/issues/50514)) ([3b636d5](3b636d5db7))
* **general_ledger:** add translation for accounting dimension ([799119a](799119ad3e))
* handle NoneType object error for product bundle ([2b7abfb](2b7abfb34b))
* improve precision in tax amount calculations in tax withholding details report ([c150e57](c150e5795e))
* on changes of paid from/to account fetch company bank account ([3d8a344](3d8a344173))
* **period closing voucher:** add title to error log ([#50498](https://github.com/frappe/erpnext/issues/50498)) ([33962ac](33962ac995))
* prevent pos opening entry creation for disabled pos profile ([68747b5](68747b5818))
* **stock-entry:** prevent default warehouse from overriding parent warehouse ([a5ec0e4](a5ec0e4f50))
* unintended backported depends_on expression ([#50529](https://github.com/frappe/erpnext/issues/50529)) ([81a1628](81a16286a1))
* use dynamic account type to get average ratio balance ([a2c82b4](a2c82b4dc3))

### Features

* Add first and last name fields to quick entry customer creation (backport [#46281](https://github.com/frappe/erpnext/issues/46281)) ([#50522](https://github.com/frappe/erpnext/issues/50522)) ([8c98f16](8c98f1692a))
* **Company:** allow setting default sales contact, fetch into sales transaction (backport [#50159](https://github.com/frappe/erpnext/issues/50159)) ([#50599](https://github.com/frappe/erpnext/issues/50599)) ([f8294f1](f8294f1754))
* **Item Price:** validate UOM ([376da8d](376da8df0a))
* **pos:** prevent disabling POS Profile when open POS sessions exist ([87e8305](87e8305753))
2025-11-19 02:58:17 +00:00
Diptanil Saha
f25e2295d0 Merge pull request #50595 from frappe/version-15-hotfix 2025-11-19 08:26:48 +05:30
ruthra kumar
b4fd4812cd Merge pull request #50527 from aerele/default-report-fiscal-year-v15
fix(financial reports): set fiscal year associated with the default company
2025-11-18 18:12:21 +05:30
rohitwaghchaure
876dec5077 chore: fix conflicts 2025-11-18 18:05:08 +05:30
rohitwaghchaure
36e9aae9d0 chore: fix conflicts 2025-11-18 18:04:29 +05:30
Pugazhendhi Velu
8ccb9a5ad2 fix: add return status for purchase receipt
(cherry picked from commit 3a0e1e8ef9)
2025-11-18 12:33:18 +00:00
Kavin
85c0c16964 fix: validate sabb autocreation when disabled
(cherry picked from commit 3ca1940881)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2025-11-18 12:29:01 +00:00
ruthra kumar
6a46045804 Merge pull request #50603 from frappe/mergify/bp/version-15-hotfix/pr-50524
fix: use dynamic account type to get average ratio balance (backport #50524)
2025-11-18 17:44:31 +05:30
ruthra kumar
befa4bef0d Merge pull request #50601 from frappe/mergify/bp/version-15-hotfix/pr-50516
fix(general_ledger): add translation for accounting dimension (backport #50516)
2025-11-18 17:13:34 +05:30
mergify[bot]
f8294f1754 feat(Company): allow setting default sales contact, fetch into sales transaction (backport #50159) (#50599)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-11-18 17:05:58 +05:30
mergify[bot]
2d6640ac61 fix: add condition for allow negative stock in pos (backport #50369) (#50600)
Co-authored-by: Logesh Periyasamy <logeshperiyasamy24@gmail.com>
fix: add condition for allow negative stock in pos (#50369)
2025-11-18 17:05:47 +05:30
Navin-S-R
627b34a120 fix: correct profit after tax calculation by reducing expenses from income
(cherry picked from commit f420371a7e)
2025-11-18 11:22:38 +00:00
Navin-S-R
a2c82b4dc3 fix: use dynamic account type to get average ratio balance
(cherry picked from commit 9118f08e7b)
2025-11-18 11:22:38 +00:00
Logesh Periyasamy
799119ad3e fix(general_ledger): add translation for accounting dimension
(cherry picked from commit 113ff17c71)
2025-11-18 11:15:37 +00:00
ruthra kumar
a4a0a2a0fb Merge pull request #50554 from frappe/mergify/bp/version-15-hotfix/pr-50540
fix(stock-entry): prevent default warehouse from overriding parent warehouse (backport #50540)
2025-11-18 15:09:08 +05:30
ruthra kumar
35fb2b8ede Merge pull request #50573 from frappe/mergify/bp/version-15-hotfix/pr-50496
fix: back calcalute total amount from rate and tax_amount in tax withholding details report (backport #50496)
2025-11-18 15:08:26 +05:30
ruthra kumar
c844bf5547 Merge pull request #50574 from frappe/mergify/bp/version-15-hotfix/pr-50439
fix: add doctype parameter to lead details for correct company details (backport #50439)
2025-11-18 15:07:15 +05:30
Kavin
81a16286a1 fix: unintended backported depends_on expression (#50529)
Co-authored-by: Kavin <78342682+kavin0411@users.noreply.github.com>
2025-11-18 10:07:26 +02:00
Khushi Rawat
3c8534c4cc Merge pull request #50571 from aerele/fix/support-52685
fix: add cancelled option in status field
2025-11-18 11:15:53 +05:30
ravibharathi656
ac40b59665 fix(financial reports): set fiscal year associated with the default company 2025-11-18 11:14:40 +05:30
ravibharathi656
3f2081b440 fix: prevent pi status from changing on asset repair 2025-11-18 11:09:25 +05:30
Pugazhendhi Velu
2db91ee67e fix(asset repair): validate pi status 2025-11-17 14:06:26 +00:00
ljain112
f0eac47037 fix: add doctype parameter to lead details for correct company details
(cherry picked from commit 0b91338771)
2025-11-17 13:39:09 +00:00
ljain112
4df80c5b53 chore: typo in comment
(cherry picked from commit e056c0327d)
2025-11-17 13:36:08 +00:00
ljain112
c150e5795e fix: improve precision in tax amount calculations in tax withholding details report
(cherry picked from commit 7c5f5405cc)
2025-11-17 13:36:08 +00:00
ljain112
57282999ad fix: back calcalute total amount from rate and tax_amount in tax withholding details report
(cherry picked from commit d3751d9bb4)
2025-11-17 13:36:08 +00:00
Pugazhendhi Velu
623a0a932e fix: add cancelled option in status field 2025-11-17 13:09:38 +00:00
rohitwaghchaure
9b06eaab78 Merge pull request #50535 from aerele/support-53360
fix: construct batch_nos and serial_nos to avoid NoneType error
2025-11-17 16:24:20 +05:30
Sherin KR
dfda8e6241 fix: item price not considering based on valid_upto 2025-11-17 14:34:26 +05:30
Kavin
2a5c9b469c fix: enable allow_negative_stock settings 2025-11-17 13:53:55 +05:30
Kavin
0a0177cb9e fix: construct batch_nos and serial_nos to avoid NoneType error 2025-11-17 12:35:16 +05:30
Mihir Kandoi
eed144d7c9 Merge pull request #50231 from frappe/mergify/bp/version-15-hotfix/pr-40586 2025-11-17 10:26:43 +05:30
Pugazhendhi Velu
a5ec0e4f50 fix(stock-entry): prevent default warehouse from overriding parent warehouse
(cherry picked from commit 8b38578914)
2025-11-17 04:41:27 +00: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
mergify[bot]
8c98f1692a feat: Add first and last name fields to quick entry customer creation (backport #46281) (#50522)
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
Co-authored-by: maasanto <73234812+maasanto@users.noreply.github.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-11-13 21:58:34 +05:30
Diptanil Saha
182e84e94c Merge pull request #50521 from frappe/mergify/bp/version-15-hotfix/pr-50498
fix(period closing voucher): add title to error log (backport #50498)
2025-11-13 21:50:21 +05:30
PUGAZHENDHI V
33962ac995 fix(period closing voucher): add title to error log (#50498)
(cherry picked from commit 4f720b3969)
2025-11-13 15:56:37 +00:00
mergify[bot]
3b636d5db7 fix: first and last name in supplier quick entry (backport #50510) (#50514)
Co-authored-by: ljain112 <ljain112@gmail.com>
Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-11-13 15:37:50 +05:30
Frappe PR Bot
70b8b3bb9e chore(release): Bumped to Version 15.88.1
## [15.88.1](https://github.com/frappe/erpnext/compare/v15.88.0...v15.88.1) (2025-11-12)

### Bug Fixes

* handle NoneType object error for product bundle ([eab6d69](eab6d69ec9))
2025-11-12 18:26:04 +00:00
rohitwaghchaure
c42954bec7 Merge pull request #50500 from frappe/mergify/bp/version-15/pr-50488
fix: handle NoneType object error for packed_items (backport #50488)
2025-11-12 23:54:34 +05:30
Kavin
eab6d69ec9 fix: handle NoneType object error for product bundle
(cherry picked from commit 2b7abfb34b)
2025-11-12 17:21:25 +00:00
rohitwaghchaure
3f0bea2d9f Merge pull request #50488 from aerele/support-53076
fix: handle NoneType object error for packed_items
2025-11-12 22:50:37 +05:30
rohitwaghchaure
34ed9b455f Merge pull request #50491 from frappe/mergify/bp/version-15-hotfix/pr-50487
fix: current qty in stock reconciliation  (backport #50487)
2025-11-12 22:49:44 +05:30
Kavin
2b7abfb34b fix: handle NoneType object error for product bundle 2025-11-12 18:26:19 +05:30
Diptanil Saha
2ba1731d3f Merge pull request #50484 from frappe/mergify/bp/version-15-hotfix/pr-50409 2025-11-12 15:14:53 +05:30
Rohit Waghchaure
b4b8459f2c fix: current qty in stock reco
(cherry picked from commit 58315bc963)
2025-11-12 08:26:01 +00:00
Diptanil Saha
d5160c4c86 test: delete outdated pos opening entry 2025-11-12 12:26:26 +05:30
Diptanil Saha
af19b81343 chore: resolve conflict 2025-11-12 11:35:02 +05:30
Diptanil Saha
650d2f74ba chore: resolve conflict 2025-11-12 11:33:56 +05:30
diptanilsaha
68747b5818 fix: prevent pos opening entry creation for disabled pos profile
(cherry picked from commit e35e8968f0)

# Conflicts:
#	erpnext/accounts/doctype/pos_opening_entry/test_pos_opening_entry.py
2025-11-12 05:44:01 +00:00
diptanilsaha
38848ff43b test: added test to validate disabled pos profile
(cherry picked from commit 69016a284f)

# Conflicts:
#	erpnext/accounts/doctype/pos_profile/test_pos_profile.py
2025-11-12 05:44:00 +00:00
diptanilsaha
87e8305753 feat(pos): prevent disabling POS Profile when open POS sessions exist
(cherry picked from commit c5219278fb)
2025-11-12 05:44:00 +00:00
Diptanil Saha
a2345d467e Merge pull request #50483 from frappe/mergify/bp/version-15-hotfix/pr-50323
fix(payment entry): on changes of paid from/to account fetch company bank account (backport #50323)
2025-11-12 11:11:37 +05:30
Abdeali Chharchhoda
3d8a344173 fix: on changes of paid from/to account fetch company bank account
(cherry picked from commit 4901dc2531)
2025-11-12 05:38:56 +00:00
Frappe PR Bot
e112290728 chore(release): Bumped to Version 15.88.0
# [15.88.0](https://github.com/frappe/erpnext/compare/v15.87.2...v15.88.0) (2025-11-11)

### Bug Fixes

* add company filter for default warehouse for sales return ([32d3fbf](32d3fbf1e8))
* add is_group filter in task for timesheet ([0f00581](0f00581f83))
* add missing stock entry UOM filtering based on item master ([#50135](https://github.com/frappe/erpnext/issues/50135)) ([8f2002d](8f2002d419))
* add validation to reject empty readings ([ac0375f](ac0375fc2e))
* apply company,is_group filter for cost center ([b181686](b1816864de))
* automatically append taxes if taxes_and_charges is set in Buying controller ([25f5fb7](25f5fb7637))
* **buying:** fetch Cost Center from Project ([e8e26a9](e8e26a91bb))
* change fieldtype from link to data for document_type in producti… (backport [#50443](https://github.com/frappe/erpnext/issues/50443)) ([#50455](https://github.com/frappe/erpnext/issues/50455)) ([a3ddc95](a3ddc9533a))
* change fieldtype from link to data for document_type in production plan summary ([9012a72](9012a72185))
* check warehouse account before accessing ([79b3af6](79b3af6d3e))
* ensure that additional discount amount is not mapped repeatedly ([44539f0](44539f0944))
* handle partial dn against reserved stock ([9d979e3](9d979e34ab))
* handle returns as well ([9ed40cc](9ed40cc17d))
* hide total row in general ledger report ([56bb88d](56bb88d281))
* ignore Department doctype ([32182d7](32182d7cc7))
* include cost_center and project upon accounting dimension fetch ([3f490f1](3f490f11d5))
* material request item quantity validation against sales order with over-receipt allowance ([f2fef54](f2fef54b83))
* **material request:** set default buying price list if not exists ([670c6dc](670c6dcdd7))
* Nonetype error if reserved stock is not present ([b8ec3ae](b8ec3ae23a))
* Pass stock_qty and picked_qty in transfer entry ([95ea9ca](95ea9ca66b))
* removed the validation ([080e9a3](080e9a3d73))
* reset billing and shipping address when company changes ([73b8a29](73b8a294cf))
* resolve conflict ([7d593dd](7d593dd3db))
* set company before creating asset movement to avoid permission error ([3fad90e](3fad90ebb9))
* show only stock items in delivered items to be billed and received items to be billed reports ([3dbc90a](3dbc90a0b4))
* state_to_state_province for translation ([#50244](https://github.com/frappe/erpnext/issues/50244)) ([d4f6ca3](d4f6ca3564))
* **stock:** ignore current voucher in reserved stock validation ([0e7f971](0e7f9711e1))
* **Timesheet:** don't use billing_hours for costing amount ([#50394](https://github.com/frappe/erpnext/issues/50394)) ([29c976e](29c976e9ae))
* trends report total mismatch with group filters ([7e3f30b](7e3f30baad))
* Update pick list locations quantity ([ce7ab8d](ce7ab8df9a))
* update uom when item changes ([2a2ae9a](2a2ae9a20c))
* validate is_group for parent task ([3380dea](3380deab02))
* validate that discount amount cannot exceed total before discount ([e559faf](e559fafa83))

### Features

* **account settings:** add checkbox to show balances in payment entry ([90500f0](90500f0ffc))
* add asset name column ([c486471](c48647100f))
* make material transfer warehouse validation optional (backport [#50461](https://github.com/frappe/erpnext/issues/50461)) ([#50462](https://github.com/frappe/erpnext/issues/50462)) ([1747e83](1747e83cb1))
* process period closing voucher ([c8e3da0](c8e3da0a71))

### Performance Improvements

* serial no creation ([6ba2491](6ba24912c3))
2025-11-11 14:49:50 +00:00
Diptanil Saha
3149785960 Merge pull request #50473 from frappe/resolve-payment-entry-settings-conflict 2025-11-11 20:18:23 +05:30
Diptanil Saha
736cff84f2 Merge branch 'version-15' into resolve-payment-entry-settings-conflict 2025-11-11 20:04:20 +05:30
rohitwaghchaure
0023476500 Merge pull request #50470 from frappe/mergify/bp/version-15-hotfix/pr-50187
fix: Update pick list locations quantity (backport #50187)
2025-11-11 16:11:03 +05:30
ruthra kumar
0c4295fb6f Merge pull request #50472 from frappe/mergify/bp/version-15-hotfix/pr-50469
fix: automatically append taxes if taxes_and_charges is set in Buying controller (backport #50469)
2025-11-11 15:57:52 +05:30
Sagar Vora
13371275db Merge pull request #50471 from frappe/mergify/bp/version-15-hotfix/pr-50155
fix: ensure that additional discount amount is not mapped repeatedly (backport #50155)
2025-11-11 15:13:48 +05:30
Sagar Vora
313e6af528 chore: resolve conflicts 2025-11-11 15:11:52 +05:30
ljain112
e2a0d6e5f6 test: add automatic tax addition for buying controller
(cherry picked from commit 3d0a668c50)
2025-11-11 09:34:16 +00:00
ljain112
25f5fb7637 fix: automatically append taxes if taxes_and_charges is set in Buying controller
(cherry picked from commit d171dc7328)
2025-11-11 09:34:16 +00:00
Sagar Vora
22848eb4da chore: remove unused import
(cherry picked from commit 81ab15351e)

# Conflicts:
#	erpnext/controllers/taxes_and_totals.py
2025-11-11 09:32:59 +00:00
Sagar Vora
b4df87e545 refactor: simplify logic
(cherry picked from commit 95f604457d)
2025-11-11 09:32:56 +00:00
Sagar Vora
9ed40cc17d fix: handle returns as well
(cherry picked from commit 0e026b9ccd)
2025-11-11 09:32:56 +00:00
Sagar Vora
22b6760164 test: some tests to ensure correct discount mapping
(cherry picked from commit 0968f435d2)
2025-11-11 09:32:56 +00:00
Sagar Vora
e559fafa83 fix: validate that discount amount cannot exceed total before discount
(cherry picked from commit f4f79d99e4)

# Conflicts:
#	erpnext/controllers/taxes_and_totals.py
2025-11-11 09:32:56 +00:00
Sagar Vora
44539f0944 fix: ensure that additional discount amount is not mapped repeatedly
(cherry picked from commit feb62102d9)
2025-11-11 09:32:55 +00:00
Kavin
a0d94c38c1 test: add test for pending qty calculation in Pick List
(cherry picked from commit 3f7a60d56c)
2025-11-11 09:24:34 +00:00
Kavin
95ea9ca66b fix: Pass stock_qty and picked_qty in transfer entry
(cherry picked from commit 6db605c443)
2025-11-11 09:24:34 +00:00
Kavin
ce7ab8df9a fix: Update pick list locations quantity
(cherry picked from commit bd9e240ca5)
2025-11-11 09:24:34 +00:00
rohitwaghchaure
798858bd4f Merge pull request #50467 from aerele/fix/update-uom-value
fix: update uom when item changes
2025-11-11 13:43:01 +05:30
ruthra kumar
c6774e35f2 Merge pull request #50465 from frappe/mergify/bp/version-15-hotfix/pr-50366
refactor: enqueue exchange rate revaluation per company (backport #50366)
2025-11-11 13:29:25 +05:30
Kavin
2a2ae9a20c fix: update uom when item changes 2025-11-11 13:17:21 +05:30
ravibharathi656
f039bfe35a refactor: enqueue exchange rate revaluation per company
(cherry picked from commit b10e7bf7b5)
2025-11-11 07:34:57 +00:00
mergify[bot]
1747e83cb1 feat: make material transfer warehouse validation optional (backport #50461) (#50462)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2025-11-11 07:18:41 +00:00
rohitwaghchaure
de14c0838c Merge pull request #50445 from frappe/mergify/bp/version-15-hotfix/pr-50436
perf: serial no creation (backport #50436)
2025-11-11 11:45:59 +05:30
ruthra kumar
3788d0f4f0 Merge pull request #50396 from aerele/show-party-and-account-balances
feat(account settings): add checkbox to show balances in payment entry
2025-11-11 11:14:25 +05:30
Mihir Kandoi
ba98a00c6c Merge pull request #50459 from frappe/mergify/bp/version-15-hotfix/pr-50418
fix: show only stock items in delivered items to be billed and received items to be billed reports (backport #50418)
2025-11-11 10:57:58 +05:30
Mihir Kandoi
2c4510ed1e Merge pull request #50458 from frappe/mergify/bp/version-15-hotfix/pr-50244
fix: state_to_state_province for translation (backport #50244)
2025-11-11 10:52:26 +05:30
rohitwaghchaure
87f3ba5794 chore: fix linters issue 2025-11-11 10:51:19 +05:30
Mihir Kandoi
197d09b90a Merge pull request #50460 from frappe/mergify/bp/version-15-hotfix/pr-50446
fix: add company filter for default warehouse for sales return (backport #50446)
2025-11-11 10:49:48 +05:30
Pugazhendhi Velu
32d3fbf1e8 fix: add company filter for default warehouse for sales return
(cherry picked from commit 0b614007bb)
2025-11-11 05:17:53 +00:00
rohitwaghchaure
4ec25ac82e chore: fix conflicts
Removed unused voucher_no and posting_date retrieval.
2025-11-11 10:47:06 +05:30
ravibharathi656
3dbc90a0b4 fix: show only stock items in delivered items to be billed and received items to be billed reports
(cherry picked from commit 1b2e5c9706)
2025-11-11 05:12:50 +00:00
Mihir Kandoi
f00236e669 Merge pull request #50456 from frappe/mergify/bp/version-15-hotfix/pr-50322
fix typo "show_disables_items" to "show_disabled_items" (backport #50322)
2025-11-11 10:41:20 +05:30
mahsem
d4f6ca3564 fix: state_to_state_province for translation (#50244)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
(cherry picked from commit e148a38353)
2025-11-11 05:08:42 +00:00
Mihir Kandoi
10e7ae4dd3 Merge pull request #50457 from frappe/mergify/bp/version-15-hotfix/pr-50320
fix: add is_group filter in task for timesheet (backport #50320)
2025-11-11 10:37:44 +05:30
Pugazhendhi Velu
0f00581f83 fix: add is_group filter in task for timesheet
(cherry picked from commit 5bac896329)
2025-11-11 04:58:38 +00:00
Mihir Kandoi
a3ddc9533a fix: change fieldtype from link to data for document_type in producti… (backport #50443) (#50455)
Co-authored-by: Pugazhendhi Velu <pugazhendhi720@gmail.com>
2025-11-11 10:26:47 +05:30
rethik
2b766bca97 chore: fix typo "show_disables_items" to "show_disabled_items"
(cherry picked from commit d26c598daa)
2025-11-11 04:56:33 +00:00
Mihir Kandoi
38c2633594 Merge pull request #50454 from frappe/mergify/bp/version-15-hotfix/pr-50399
fix: mr item quantity validation against so over-receipt allowance (backport #50399)
2025-11-11 10:25:45 +05:30
Pugazhendhi Velu
9012a72185 fix: change fieldtype from link to data for document_type in production plan summary
(cherry picked from commit 462deb3755)
2025-11-11 04:41:26 +00:00
Pugazhendhi Velu
249d14b072 test: add test for validate mr item qty against so with over-receipt allowance
(cherry picked from commit 55f531bad6)
2025-11-11 04:40:25 +00:00
Pugazhendhi Velu
f2fef54b83 fix: material request item quantity validation against sales order with over-receipt allowance
(cherry picked from commit 8d7e31e3f2)
2025-11-11 04:40:25 +00:00
Mihir Kandoi
1b1e4e4688 Merge pull request #50453 from frappe/mergify/bp/version-15-hotfix/pr-50135
fix: add missing stock entry UOM filtering based on item master (backport #50135)
2025-11-11 10:05:05 +05:30
Rehan Ansari
8f2002d419 fix: add missing stock entry UOM filtering based on item master (#50135)
Co-authored-by: rehansari26 <rehan.ansari@cloverinfotech.com>
(cherry picked from commit 7baf6ec3d6)
2025-11-11 04:29:34 +00:00
Assem Bahnasy
5b643433e5 Fix: Product Bundle Purchase Order Creation Logic (#49831)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2025-11-11 09:53:25 +05:30
Diptanil Saha
e02aaa9f1b Merge pull request #50449 from frappe/mergify/bp/version-15-hotfix/pr-50339
fix: include cost_center and project upon accounting dimension fetch (backport #50339)
2025-11-11 00:10:52 +05:30
l0gesh29
b1816864de fix: apply company,is_group filter for cost center
(cherry picked from commit dcdc1c6a89)
2025-11-10 17:47:08 +00:00
l0gesh29
3f490f11d5 fix: include cost_center and project upon accounting dimension fetch
(cherry picked from commit 4680295303)
2025-11-10 17:47:07 +00:00
Diptanil Saha
b545b69e4b Merge pull request #50444 from frappe/mergify/bp/version-15-hotfix/pr-50361
fix: reset billing and shipping address when company changes (backport #50361)
2025-11-10 20:59:05 +05:30
Rohit Waghchaure
6ba24912c3 perf: serial no creation
(cherry picked from commit 19a9497273)

# Conflicts:
#	erpnext/stock/serial_batch_bundle.py
2025-11-10 15:11:27 +00:00
Pugazhendhi Velu
73b8a294cf fix: reset billing and shipping address when company changes
(cherry picked from commit 0510f7e13f)
2025-11-10 15:08:13 +00:00
ruthra kumar
b5485dc909 Merge pull request #50393 from frappe/mergify/bp/version-15-hotfix/pr-50340
fix: ignore Department doctype (backport #50340)
2025-11-10 16:37:43 +05:30
Frappe PR Bot
292f71bcef chore(release): Bumped to Version 15.87.2
## [15.87.2](https://github.com/frappe/erpnext/compare/v15.87.1...v15.87.2) (2025-11-10)

### Bug Fixes

* Nonetype error if reserved stock is not present ([cf66b5a](cf66b5aa34))
* **stock:** ignore current voucher in reserved stock validation ([9d2c456](9d2c456668))
2025-11-10 10:47:57 +00:00
rohitwaghchaure
1e4acb3703 Merge pull request #50434 from frappe/mergify/bp/version-15/pr-50431
fix(stock): ignore current voucher in reserved stock validation (backport #50431)
2025-11-10 16:16:31 +05:30
Kavin
cf66b5aa34 fix: Nonetype error if reserved stock is not present
(cherry picked from commit b8ec3ae23a)
2025-11-10 10:29:12 +00:00
Kavin
9d2c456668 fix(stock): ignore current voucher in reserved stock validation
(cherry picked from commit 0e7f9711e1)
2025-11-10 10:29:12 +00:00
rohitwaghchaure
5ed4fea3e3 Merge pull request #50431 from aerele/fix/reserved-stock-negative
fix(stock): ignore current voucher in reserved stock validation
2025-11-10 15:57:37 +05:30
Kavin
b8ec3ae23a fix: Nonetype error if reserved stock is not present 2025-11-10 14:51:35 +05:30
Kavin
0e7f9711e1 fix(stock): ignore current voucher in reserved stock validation 2025-11-10 14:29:04 +05:30
rohitwaghchaure
3cad1304a0 Merge pull request #50427 from frappe/mergify/bp/version-15-hotfix/pr-50374
fix: add validation to reject empty readings (backport #50374)
2025-11-10 14:04:23 +05:30
ruthra kumar
9dc1f5f649 Merge pull request #50405 from frappe/mergify/bp/version-15-hotfix/pr-50326
fix: validate is_group for parent task (backport #50326)
2025-11-10 12:58:34 +05:30
ruthra kumar
df6ca3af57 chore: resolve conflicts 2025-11-10 12:43:05 +05:30
ruthra kumar
95db1677e2 Merge pull request #50408 from aerele/support-52573
fix: check warehouse account before accessing
2025-11-10 12:39:14 +05:30
ruthra kumar
ca9f1b6c7b Merge pull request #50429 from frappe/mergify/bp/version-15-hotfix/pr-50423
fix(buying): fetch Cost Center from Project (backport #50423)
2025-11-10 12:35:29 +05:30
barredterra
e8e26a91bb fix(buying): fetch Cost Center from Project
(cherry picked from commit bdabcb081a)
2025-11-10 07:03:04 +00:00
Pugazhendhi Velu
dc88f7b30b refactor: add default reading value when creating a quality inspection
(cherry picked from commit 63fb9f55e7)
2025-11-10 05:38:42 +00:00
Pugazhendhi Velu
ac0375fc2e fix: add validation to reject empty readings
(cherry picked from commit 405d901514)
2025-11-10 05:38:42 +00:00
rohitwaghchaure
5bb2e8a8ff Merge pull request #50401 from aerele/support-52103
fix: handle partial dn against reserved stock
2025-11-07 20:53:43 +05:30
rohitwaghchaure
3989a7ede6 Merge pull request #50412 from frappe/mergify/bp/version-15-hotfix/pr-50411
fix: removed the validation (backport #50411)
2025-11-07 19:16:37 +05:30
Kavin
ef719fe729 test: add test for partial dn against reserved stock 2025-11-07 19:09:23 +05:30
Kavin
9d979e34ab fix: handle partial dn against reserved stock 2025-11-07 19:09:11 +05:30
Rohit Waghchaure
080e9a3d73 fix: removed the validation
(cherry picked from commit 10131333b2)
2025-11-07 12:33:11 +00:00
Kavin
79b3af6d3e fix: check warehouse account before accessing 2025-11-07 15:49:59 +05:30
Patrick Eißler
29c976e9ae fix(Timesheet): don't use billing_hours for costing amount (#50394) 2025-11-07 13:26:44 +05:30
Pugazhendhi Velu
635fe427fe refactor(task): use get_link_to_form for validation error messages
(cherry picked from commit 4cf02b4d78)
2025-11-07 07:28:59 +00:00
Pugazhendhi Velu
dc5b8367c5 test: add test for parent task is_group validation
(cherry picked from commit 291f0c7161)

# Conflicts:
#	erpnext/projects/doctype/task/test_task.py
2025-11-07 07:28:59 +00:00
Pugazhendhi Velu
3380deab02 fix: validate is_group for parent task
(cherry picked from commit ed1a1099cb)

# Conflicts:
#	erpnext/projects/doctype/task/task.py
2025-11-07 07:28:59 +00:00
Diptanil Saha
8e199af118 Merge pull request #50403 from frappe/mergify/bp/version-15-hotfix/pr-50402
fix: trends report total mismatch with group by filters (backport #50402)
2025-11-07 12:50:49 +05:30
diptanilsaha
7e3f30baad fix: trends report total mismatch with group filters
(cherry picked from commit f7d09f8760)
2025-11-07 07:03:51 +00:00
ravibharathi656
90500f0ffc feat(account settings): add checkbox to show balances in payment entry 2025-11-06 18:35:56 +05:30
rethik
32182d7cc7 fix: ignore Department doctype
(cherry picked from commit fff6f1fb23)
2025-11-06 11:14:55 +00:00
ruthra kumar
4f02677d6f Merge pull request #50387 from frappe/mergify/bp/version-15-hotfix/pr-50364
fix: hide total row in general ledger report (backport #50364)
2025-11-06 16:02:53 +05:30
Pugazhendhi Velu
56bb88d281 fix: hide total row in general ledger report
(cherry picked from commit ef38b26a73)
2025-11-06 10:11:46 +00:00
Khushi Rawat
124a0fe45d Merge pull request #50379 from frappe/mergify/bp/version-15-hotfix/pr-50367
fix: set company before creating asset movement to avoid permission error (backport #50367)
2025-11-06 10:03:20 +05:30
rehansari26
3fad90ebb9 fix: set company before creating asset movement to avoid permission error
(cherry picked from commit 8c49c9e500)
2025-11-05 21:50:22 +00:00
Khushi Rawat
99b6dc508e Merge pull request #50377 from frappe/mergify/bp/version-15-hotfix/pr-50342
feat: add asset name column (backport #50342)
2025-11-06 01:29:57 +05:30
Khushi Rawat
7d593dd3db fix: resolve conflict 2025-11-06 00:51:41 +05:30
Rehan Ansari
c48647100f feat: add asset name column
(cherry picked from commit f3eda02972)

# Conflicts:
#	erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
2025-11-05 18:52:50 +00:00
ruthra kumar
c54d995354 Merge pull request #50368 from frappe/mergify/bp/version-15-hotfix/pr-50144
refactor: period closing voucher to handle large data volumes (backport #50144)
2025-11-05 17:42:52 +05:30
ruthra kumar
5c6cc1ea2a chore: resolve conflicts 2025-11-05 17:27:14 +05:30
ruthra kumar
e09ee63d32 refactor: add paused to select option
(cherry picked from commit fca7abf4d6)
2025-11-05 11:28:44 +00:00
ruthra kumar
d8e5075424 refactor: minor changes on status
1. set to 'In Progress' on start of both legacy and new controller
2. force delete to avoid permission issues
3. default to 1hr timeout

(cherry picked from commit 9c13edc0b9)
2025-11-05 11:28:43 +00:00
ruthra kumar
d5a36fe8aa refactor: enable legacy controller by default for pcv
(cherry picked from commit fe39ce03bb)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py
2025-11-05 11:28:43 +00:00
ruthra kumar
10df192275 chore: progress bar
(cherry picked from commit 0b88f98a86)
2025-11-05 11:28:43 +00:00
ruthra kumar
4ebddc591f chore: remove scaffolding
(cherry picked from commit 191c0e65a1)
2025-11-05 11:28:42 +00:00
ruthra kumar
5cc6a1771d refactor: abort processing of all tasks upon cancellation
(cherry picked from commit 090e155fd0)
2025-11-05 11:28:42 +00:00
ruthra kumar
29e8801b7f refactor: more changes
1. 'Accounts Manager' has access to submit, cancel and delete
2. cancel and delete operation of PCV is linked with Proces PCV

(cherry picked from commit cae1237859)
2025-11-05 11:28:42 +00:00
ruthra kumar
d94000fecf refactor: smaller methods
(cherry picked from commit fa3bd6f5a7)
2025-11-05 11:28:42 +00:00
ruthra kumar
5071cad161 refactor: utility to consolidate results from all dates
(cherry picked from commit 7406d83260)
2025-11-05 11:28:41 +00:00
ruthra kumar
0151f5f191 refactor: utility to convert tuple key to str
(cherry picked from commit 5b464ae4c1)
2025-11-05 11:28:41 +00:00
ruthra kumar
d139db296a refactor: cleanup and for better readability
(cherry picked from commit 653ae84b3e)
2025-11-05 11:28:41 +00:00
ruthra kumar
3406e44b03 refactor: make Accounts Closing Balance as well
(cherry picked from commit 6e32769e37)
2025-11-05 11:28:41 +00:00
ruthra kumar
449fa05d7d refactor: store closing balance for Balnace sheet accounts
(cherry picked from commit 09e37bc98c)
2025-11-05 11:28:40 +00:00
ruthra kumar
55222468f9 refactor: calculate both balances from single queue
(cherry picked from commit 643e1fdce8)
2025-11-05 11:28:40 +00:00
ruthra kumar
f381b99b14 refactor: populate opening balances calculation table
(cherry picked from commit 86edacb781)
2025-11-05 11:28:40 +00:00
ruthra kumar
2c880dd609 chore: rename closing balance field
(cherry picked from commit cef879bb3b)
2025-11-05 11:28:39 +00:00
ruthra kumar
87e297e899 refactor: balances for both P&L and Balance sheet accounts
(cherry picked from commit 324bebfd44)
2025-11-05 11:28:39 +00:00
ruthra kumar
6c94ca664f refactor: maintain report type on each date
(cherry picked from commit 186d540502)
2025-11-05 11:28:39 +00:00
ruthra kumar
d911e1dab2 refactor: more stable pause and resume
(cherry picked from commit 9e93298f12)
2025-11-05 11:28:39 +00:00
ruthra kumar
908f8ed462 refactor: process on submit
(cherry picked from commit c738b6d356)
2025-11-05 11:28:38 +00:00
ruthra kumar
633ccef2ff refactor: for better readability
(cherry picked from commit 8ba199016a)
2025-11-05 11:28:38 +00:00
ruthra kumar
e8f8abd685 refactor: store results as is and convert at the end
(cherry picked from commit f25ee3c53f)
2025-11-05 11:28:38 +00:00
ruthra kumar
b6b5524228 refactor: build and post gl entries
(cherry picked from commit e88074ddec)
2025-11-05 11:28:38 +00:00
ruthra kumar
76bdf7944c refactor: store daily balances based on dimensions key
dimensions key is manually converted to string

(cherry picked from commit 1846de0d49)
2025-11-05 11:28:37 +00:00
ruthra kumar
1999de0b75 refactor: store closing balance as JSON
(cherry picked from commit 1a31825409)
2025-11-05 11:28:37 +00:00
ruthra kumar
43acfdff82 refactor: stable start, pause, resume and completion stages
(cherry picked from commit c839ebf593)
2025-11-05 11:28:37 +00:00
ruthra kumar
19b911120c refactor: barebones functions
(cherry picked from commit 1c92b01542)
2025-11-05 11:28:37 +00:00
ruthra kumar
bc07de8c12 refactor: temporarily save balances in JSON
(cherry picked from commit f44c908a8d)
2025-11-05 11:28:36 +00:00
ruthra kumar
b484db3ffd refactor: child table in process pcv
(cherry picked from commit 0d09d21d2e)
2025-11-05 11:28:36 +00:00
ruthra kumar
902ce45a36 refactor: more data structure changes
(cherry picked from commit a15578f8f4)
2025-11-05 11:28:36 +00:00
ruthra kumar
bfc0044d23 refactor: checkbox for pcv controller
(cherry picked from commit 4888461be2)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.py
2025-11-05 11:28:36 +00:00
ruthra kumar
c8e3da0a71 feat: process period closing voucher
(cherry picked from commit 7a93630629)
2025-11-05 11:28:35 +00:00
rohitwaghchaure
2eed8ee343 Merge pull request #50362 from frappe/mergify/bp/version-15-hotfix/pr-50335
fix(material request): set default buying price list if not exists (backport #50335)
2025-11-05 14:44:21 +05:30
ravibharathi656
670c6dcdd7 fix(material request): set default buying price list if not exists
(cherry picked from commit 9c0ff14060)
2025-11-05 08:42:09 +00:00
Raffael Meyer
7d607b82f1 chore: resolve conflicts 2025-10-29 15:37:04 +01:00
barredterra
376da8df0a feat(Item Price): validate UOM
(cherry picked from commit 69824eff80)

# Conflicts:
#	erpnext/stock/doctype/item_price/item_price.py
2025-10-27 06:32:01 +00:00
235 changed files with 30849 additions and 1560 deletions

View File

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

View File

@@ -309,8 +309,8 @@ def get_dimensions(with_cost_center_and_project=False):
if with_cost_center_and_project:
dimension_filters.extend(
[
{"fieldname": "cost_center", "document_type": "Cost Center"},
{"fieldname": "project", "document_type": "Project"},
frappe._dict({"fieldname": "cost_center", "document_type": "Cost Center"}),
frappe._dict({"fieldname": "project", "document_type": "Project"}),
]
)

View File

@@ -56,6 +56,9 @@
"reconciliation_queue_size",
"column_break_resa",
"exchange_gain_loss_posting_date",
"payment_entry_settings",
"show_account_balance",
"show_party_balance",
"invoicing_settings_tab",
"accounts_transactions_settings_section",
"over_billing_allowance",
@@ -95,7 +98,8 @@
"legacy_section",
"ignore_is_opening_check_for_reporting",
"payment_request_settings",
"create_pr_in_draft_status"
"create_pr_in_draft_status",
"column_break_xrnd"
],
"fields": [
{
@@ -636,6 +640,23 @@
"fieldname": "use_legacy_controller_for_pcv",
"fieldtype": "Check",
"label": "Use Legacy Controller For Period Closing Voucher"
},
{
"fieldname": "payment_entry_settings",
"fieldtype": "Section Break",
"label": "Payment Entry Settings"
},
{
"default": "0",
"fieldname": "show_account_balance",
"fieldtype": "Check",
"label": "Show Account Balance"
},
{
"default": "0",
"fieldname": "show_party_balance",
"fieldtype": "Check",
"label": "Show Party Balance"
}
],
"icon": "icon-cog",
@@ -643,7 +664,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-10-20 14:06:08.870427",
"modified": "2025-11-06 17:48:07.682837",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@@ -668,8 +689,9 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -65,8 +65,10 @@ class AccountsSettings(Document):
role_allowed_to_over_bill: DF.Link | None
role_to_override_stop_action: DF.Link | None
round_row_wise_tax: DF.Check
show_account_balance: DF.Check
show_balance_in_coa: DF.Check
show_inclusive_tax_in_print: DF.Check
show_party_balance: DF.Check
show_payment_schedule_in_print: DF.Check
show_taxes_as_table_in_print: DF.Check
stale_days: DF.Int
@@ -105,6 +107,7 @@ class AccountsSettings(Document):
frappe.clear_cache()
self.validate_and_sync_auto_reconcile_config()
self.hide_or_show_party_and_account_balance()
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
@@ -112,6 +115,18 @@ class AccountsSettings(Document):
_("Stale Days should start from 1."), title="Error", indicator="red", raise_exception=1
)
def hide_or_show_party_and_account_balance(self):
def set_property(fieldname, value):
make_property_setter("Payment Entry", fieldname, "hidden", value, "Check")
if self.has_value_changed("show_party_balance"):
set_property("party_balance", not self.show_party_balance)
if self.has_value_changed("show_account_balance"):
account_fields = ["paid_from_account_balance", "paid_to_account_balance"]
for field in account_fields:
set_property(field, not self.show_account_balance)
def enable_payment_schedule_in_print(self):
show_in_print = cint(self.show_payment_schedule_in_print)
for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"):

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.app") {
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
let result = ["rates", "{to_currency}"];
let params = {
base: "{from_currency}",

View File

@@ -78,7 +78,7 @@
"fieldname": "service_provider",
"fieldtype": "Select",
"label": "Service Provider",
"options": "frankfurter.app\nexchangerate.host\nCustom",
"options": "frankfurter.dev\nexchangerate.host\nCustom",
"reqd": 1
},
{
@@ -104,7 +104,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-03-18 08:32:26.895076",
"modified": "2025-11-25 13:03:41.896424",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",
@@ -141,8 +141,9 @@
"write": 1
}
],
"sort_field": "modified",
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
disabled: DF.Check
req_params: DF.Table[CurrencyExchangeSettingsDetails]
result_key: DF.Table[CurrencyExchangeSettingsResult]
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"]
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
url: DF.Data | None
use_http: DF.Check
# end: auto-generated types
@@ -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.app":
elif self.service_provider in ("frankfurter.dev", "frankfurter.app"):
self.set("result_key", [])
self.set("req_params", [])
@@ -105,11 +105,13 @@ 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.app"]:
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}"
protocol = "https://"
if use_http:

View File

@@ -252,7 +252,7 @@ class ExchangeRateRevaluation(Document):
company_currency = erpnext.get_company_currency(company)
precision = get_field_precision(
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
company_currency,
currency=company_currency,
)
if account_details:
@@ -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

@@ -420,7 +420,7 @@ def update_against_account(voucher_type, voucher_no):
if not entries:
return
company_currency = erpnext.get_company_currency(entries[0].company)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency)
accounts_debited, accounts_credited = [], []
for d in entries:

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

@@ -592,6 +592,8 @@ frappe.ui.form.on("Payment Entry", {
paid_from: function (frm) {
if (frm.set_party_account_based_on_party) return;
frm.events.set_company_bank_account(frm);
frm.events.set_account_currency_and_balance(
frm,
frm.doc.paid_from,
@@ -609,6 +611,8 @@ frappe.ui.form.on("Payment Entry", {
paid_to: function (frm) {
if (frm.set_party_account_based_on_party) return;
frm.events.set_company_bank_account(frm);
frm.events.set_account_currency_and_balance(
frm,
frm.doc.paid_to,
@@ -1298,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;
}
@@ -1350,6 +1353,8 @@ frappe.ui.form.on("Payment Entry", {
},
bank_account: function (frm) {
if (frm.set_company_bank_account_based_on_coa) return;
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
if (frm.doc.bank_account && ["Pay", "Receive"].includes(frm.doc.payment_type)) {
frappe.call({
@@ -1388,6 +1393,34 @@ frappe.ui.form.on("Payment Entry", {
}
},
set_company_bank_account: function (frm) {
if (!["Pay", "Receive"].includes(frm.doc.payment_type)) return;
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
if (!frm.doc.company || !frm.doc[field]) return;
frm.set_company_bank_account_based_on_coa = true;
frappe.call({
method: "frappe.client.get_value",
args: {
doctype: "Bank Account",
filters: {
company: frm.doc.company,
account: frm.doc[field],
disabled: 0,
},
fieldname: ["name"],
},
callback: async function (r) {
if (r.message) await frm.set_value("bank_account", r.message.name);
frm.set_company_bank_account_based_on_coa = false;
},
});
},
sales_taxes_and_charges_template: function (frm) {
frm.trigger("fetch_taxes_from_template");
},

View File

@@ -449,7 +449,7 @@ class PaymentEntry(AccountsController):
self.contact_person = get_default_contact(self.party_type, self.party)
complete_contact_details(self)
if not self.party_balance:
if not self.party_balance and frappe.get_single_value("Accounts Settings", "show_party_balance"):
self.party_balance = get_balance_on(
party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company
)
@@ -1800,7 +1800,7 @@ class PaymentEntry(AccountsController):
else:
self.total_taxes_and_charges += current_tax_amount
self.base_total_taxes_and_charges += tax.base_tax_amount
self.base_total_taxes_and_charges += current_tax_amount
if self.get("taxes"):
self.paid_amount_after_tax = self.get("taxes")[-1].base_total
@@ -2684,11 +2684,17 @@ def get_party_details(company, party_type, party, date, cost_center=None):
party_account = get_party_account(party_type, party, company)
account_currency = get_account_currency(party_account)
account_balance = get_balance_on(party_account, date, cost_center=cost_center)
account_balance = (
get_balance_on(party_account, date, cost_center=cost_center)
if frappe.get_single_value("Accounts Settings", "show_account_balance")
else 0
)
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
party_name = frappe.db.get_value(party_type, party, _party_name)
party_balance = get_balance_on(
party_type=party_type, party=party, company=company, cost_center=cost_center
party_balance = (
get_balance_on(party_type=party_type, party=party, company=company, cost_center=cost_center)
if frappe.get_single_value("Accounts Settings", "show_party_balance")
else 0
)
if party_type in ["Customer", "Supplier"]:
party_bank_account = get_party_bank_account(party_type, party)
@@ -2717,7 +2723,11 @@ def get_account_details(account, date, cost_center=None):
if not account_list:
frappe.throw(_("Account: {0} is not permitted under Payment Entry").format(account))
account_balance = get_balance_on(account, date, cost_center=cost_center, ignore_account_permission=True)
account_balance = (
get_balance_on(account, date, cost_center=cost_center, ignore_account_permission=True)
if frappe.get_single_value("Accounts Settings", "show_account_balance")
else 0
)
return frappe._dict(
{
@@ -3529,11 +3539,18 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
def get_party_and_account_balance(
company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None
):
show_account_balance = frappe.get_single_value("Accounts Settings", "show_account_balance")
return frappe._dict(
{
"party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center),
"paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center),
"paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center),
"party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center)
if frappe.get_single_value("Accounts Settings", "show_party_balance")
else 0,
"paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center)
if show_account_balance
else 0,
"paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center)
if show_account_balance
else 0,
}
)

View File

@@ -61,6 +61,22 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
},
};
});
this.frm.set_query("cost_center", "payments", () => {
return {
filters: {
company: this.frm.doc.company,
is_group: 0,
},
};
});
this.frm.set_query("cost_center", "allocation", () => {
return {
filters: {
company: this.frm.doc.company,
is_group: 0,
},
};
});
}
refresh() {
@@ -318,7 +334,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
},
{
fieldtype: "HTML",
options: "<b> New Journal Entry will be posted for the difference amount </b>",
options: __(
"New Journal Entry will be posted for the difference amount. The Posting Date can be modified."
).bold(),
},
],
primary_action: () => {

View File

@@ -72,7 +72,7 @@ class PaymentReconciliation(Document):
self.common_filter_conditions = []
self.accounting_dimension_filter_conditions = []
self.ple_posting_date_filter = []
self.dimensions = get_dimensions()[0]
self.dimensions = get_dimensions(with_cost_center_and_project=True)[0]
def load_from_db(self):
# 'modified' attribute is required for `run_doc_method` to work properly.
@@ -765,6 +765,14 @@ class PaymentReconciliation(Document):
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
for inv in dr_cr_notes:
if (
abs(frappe.db.get_value(inv.voucher_type, inv.voucher_no, "outstanding_amount"))
< inv.allocated_amount
):
frappe.throw(
_("{0} has been modified after you pulled it. Please pull it again.").format(inv.voucher_type)
)
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
reconcile_dr_or_cr = (

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

@@ -475,8 +475,15 @@ def process_gl_and_closing_entries(doc):
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Failed")
frappe.log_error(title=_("Period Closing Voucher {0} GL Entry Processing Failed").format(doc.name))
frappe.db.set_value(
doc.doctype,
doc.name,
{
"error_message": str(e),
"gle_processing_status": "Failed",
},
)
def process_cancellation(voucher_type, voucher_no):
@@ -488,8 +495,17 @@ def process_cancellation(voucher_type, voucher_no):
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")
frappe.log_error(
title=_("Period Closing Voucher {0} GL Entry Cancellation Failed").format(voucher_no)
)
frappe.db.set_value(
voucher_type,
voucher_no,
{
"error_message": str(e),
"gle_processing_status": "Failed",
},
)
def delete_closing_entries(voucher_no):

View File

@@ -18,12 +18,17 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.controllers.queries import item_query as _item_query
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.stock_ledger import is_negative_stock_allowed
class PartialPaymentValidationError(frappe.ValidationError):
pass
class ProductBundleStockValidationError(frappe.ValidationError):
pass
class POSInvoice(SalesInvoice):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -189,6 +194,9 @@ class POSInvoice(SalesInvoice):
super().__init__(*args, **kwargs)
def validate(self):
if not self.customer:
frappe.throw(_("Please select Customer first"))
if not cint(self.is_pos):
frappe.throw(
_("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment")))
@@ -345,34 +353,69 @@ class POSInvoice(SalesInvoice):
):
return
from erpnext.stock.stock_ledger import is_negative_stock_allowed
for d in self.get("items"):
if not d.serial_and_batch_bundle:
if is_negative_stock_allowed(item_code=d.item_code):
return
if frappe.db.exists("Product Bundle", d.item_code):
(
availability,
is_stock_item,
is_negative_stock_allowed,
) = get_product_bundle_stock_availability(d.item_code, d.warehouse, d.stock_qty)
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
else:
availability, is_stock_item, is_negative_stock_allowed = get_stock_availability(
d.item_code, d.warehouse
)
item_code, warehouse, _qty = (
frappe.bold(d.item_code),
frappe.bold(d.warehouse),
frappe.bold(d.qty),
)
if is_stock_item and flt(available_stock) <= 0:
frappe.throw(
_("Row #{}: Item Code: {} is not available under warehouse {}.").format(
d.idx, item_code, warehouse
),
title=_("Item Unavailable"),
)
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
frappe.throw(
_("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
d.idx, item_code, warehouse
),
title=_("Item Unavailable"),
)
if is_negative_stock_allowed:
continue
if isinstance(availability, list):
error_msgs = []
for item in availability:
if flt(item["available"]) < flt(item["required"]):
error_msgs.append(
_("<li>Packed Item {0}: Required {1}, Available {2}</li>").format(
frappe.bold(item["item_code"]),
frappe.bold(flt(item["required"], 2)),
frappe.bold(flt(item["available"], 2)),
)
)
if error_msgs:
frappe.throw(
_(
"<b>Row #{0}:</b> Bundle {1} in warehouse {2} has insufficient packed items:<br><div style='margin-top: 15px;'><ul style='line-height: 0.8;'>{3}</ul></div>"
).format(
d.idx,
frappe.bold(d.item_code),
frappe.bold(d.warehouse),
"<br>".join(error_msgs),
),
title=_("Insufficient Stock for Product Bundle Items"),
exc=ProductBundleStockValidationError,
)
else:
item_code, warehouse = frappe.bold(d.item_code), frappe.bold(d.warehouse)
if is_stock_item and flt(availability) <= 0:
frappe.throw(
_("Row #{0}: Item {1} has no stock in warehouse {2}.").format(
d.idx, item_code, warehouse
),
title=_("Item Out of Stock"),
)
elif is_stock_item and flt(availability) < flt(d.stock_qty):
frappe.throw(
_("Row #{0}: Item {1} in warehouse {2}: Available {3}, Needed {4}.").format(
d.idx,
item_code,
warehouse,
frappe.bold(flt(availability, 2)),
frappe.bold(flt(d.stock_qty, 2)),
),
title=_("Insufficient Stock"),
)
def validate_serialised_or_batched_item(self):
error_msg = []
@@ -765,15 +808,35 @@ def get_stock_availability(item_code, warehouse):
bin_qty = get_bin_qty(item_code, warehouse)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty, is_stock_item
return bin_qty - pos_sales_qty, is_stock_item, is_negative_stock_allowed(item_code=item_code)
else:
is_stock_item = True
if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
return get_bundle_availability(item_code, warehouse), is_stock_item
return get_bundle_availability(item_code, warehouse), is_stock_item, False
else:
is_stock_item = False
# Is a service item or non_stock item
return 0, is_stock_item
return 0, is_stock_item, False
def get_product_bundle_stock_availability(item_code, warehouse, item_qty):
is_stock_item = True
bundle = frappe.get_doc("Product Bundle", item_code)
availabilities = []
for bundle_item in bundle.items:
if frappe.get_value("Item", bundle_item.item_code, "is_stock_item"):
bin_qty = get_bin_qty(bundle_item.item_code, warehouse)
reserved_qty = get_pos_reserved_qty(bundle_item.item_code, warehouse)
available = bin_qty - reserved_qty
availabilities.append(
{
"item_code": bundle_item.item_code,
"required": bundle_item.qty * item_qty,
"available": available,
}
)
return availabilities, is_stock_item, is_negative_stock_allowed(item_code=item_code)
def get_bundle_availability(bundle_item_code, warehouse):

View File

@@ -964,6 +964,84 @@ class TestPOSInvoice(unittest.TestCase):
frappe.db.rollback(save_point="before_test_delivered_serial_no_case")
frappe.set_user("Administrator")
def test_bundle_stock_availability_validation(self):
from erpnext.accounts.doctype.pos_invoice.pos_invoice import ProductBundleStockValidationError
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
init_user_and_profile,
)
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.item.test_item import create_item
init_user_and_profile()
frappe.set_user("Administrator")
warehouse = "_Test Warehouse - _TC"
company = "_Test Company"
# Create stock sub-items
sub_item_a = "_Test Bundle SubA"
if not frappe.db.exists("Item", sub_item_a):
create_item(
item_code=sub_item_a,
is_stock_item=1,
)
sub_item_b = "_Test Bundle SubB"
if not frappe.db.exists("Item", sub_item_b):
create_item(
item_code=sub_item_b,
is_stock_item=1,
)
# Add initial stock: SubA=5, SubB=2
make_stock_entry(item_code=sub_item_a, target=warehouse, qty=5, company=company)
make_stock_entry(item_code=sub_item_b, target=warehouse, qty=2, company=company)
# Create Product Bundle: Test Bundle (SubA x2 + SubB x1)
bundle_item = "_Test Bundle"
if not frappe.db.exists("Item", bundle_item):
create_item(
item_code=bundle_item,
is_stock_item=0,
)
if not frappe.db.exists("Product Bundle", bundle_item):
make_product_bundle(parent=bundle_item, items=[sub_item_a, sub_item_b])
# Test Case 1: Sufficient stock (bundle qty=1: requires SubA=2 (<=5), SubB=1 (<=2)) -> No error
pos_inv_sufficient = create_pos_invoice(
item=bundle_item,
qty=1,
rate=100,
warehouse=warehouse,
pos_profile=self.pos_profile.name,
do_not_save=1,
)
pos_inv_sufficient.append("payments", {"mode_of_payment": "Cash", "amount": 100, "default": 1})
pos_inv_sufficient.insert()
pos_inv_sufficient.submit()
pos_inv_sufficient.cancel()
pos_inv_sufficient.delete()
# Test Case 2: Insufficient stock (reduce SubB to 1, bundle qty=2: requires SubB=2 >1) -> Error with details
make_stock_entry(item_code=sub_item_b, from_warehouse=warehouse, qty=1, company=company)
pos_inv_insufficient = create_pos_invoice(
item=bundle_item,
qty=2,
rate=100,
warehouse=warehouse,
pos_profile=self.pos_profile.name,
do_not_save=1,
)
pos_inv_insufficient.append("payments", {"mode_of_payment": "Cash", "amount": 200, "default": 1})
pos_inv_insufficient.save()
self.assertRaises(ProductBundleStockValidationError, pos_inv_insufficient.submit)
frappe.set_user("test@example.com")
def create_pos_invoice(**args):
args = frappe._dict(args)

View File

@@ -41,9 +41,19 @@ class POSOpeningEntry(StatusUpdater):
self.set_status()
def validate_pos_profile_and_cashier(self):
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
if not frappe.db.exists("POS Profile", self.pos_profile):
frappe.throw(_("POS Profile {} does not exist.").format(self.pos_profile))
pos_profile_company, pos_profile_disabled = frappe.db.get_value(
"POS Profile", self.pos_profile, ["company", "disabled"]
)
if pos_profile_disabled:
frappe.throw(_("POS Profile {} is disabled.").format(frappe.bold(self.pos_profile)))
if self.company != pos_profile_company:
frappe.throw(
_("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company)
_("POS Profile {} does not belong to company {}").format(self.pos_profile, self.company)
)
if not cint(frappe.db.get_value("User", self.user, "enabled")):

View File

@@ -70,6 +70,7 @@ class POSProfile(Document):
# end: auto-generated types
def validate(self):
self.validate_disabled()
self.validate_default_profile()
self.validate_all_link_fields()
self.validate_duplicate_groups()
@@ -94,6 +95,21 @@ class POSProfile(Document):
title=_("Mandatory Accounting Dimension"),
)
def validate_disabled(self):
old_doc = self.get_doc_before_save()
if (
old_doc
and self.disabled
and old_doc.disabled != self.disabled
and frappe.db.exists("POS Opening Entry", {"pos_profile": self.name, "status": "Open"})
):
frappe.throw(
_("POS Profile {0} cannot be disabled as there are ongoing POS sessions.").format(
frappe.bold(self.name)
)
)
def validate_default_profile(self):
for row in self.applicable_for_users:
res = frappe.db.sql(

View File

@@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.utils import cint
from erpnext.accounts.doctype.pos_profile.pos_profile import (
get_child_nodes,
@@ -38,6 +39,51 @@ class TestPOSProfile(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`")
def test_disabled_pos_profile_creation(self):
make_pos_profile(name="_Test POS Profile 001", disabled=1)
pos_profile = frappe.get_doc("POS Profile", "_Test POS Profile 001")
if pos_profile:
self.assertEqual(pos_profile.disabled, 1)
def test_disabled_pos_profile_after_opening(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
test_user, pos_profile = init_user_and_profile()
if pos_profile:
create_opening_entry(pos_profile, test_user.name)
self.assertEqual(pos_profile.disabled, 0)
pos_profile.disabled = 1
self.assertRaises(frappe.ValidationError, pos_profile.save)
def test_disabled_pos_profile_after_completing_session(self):
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
make_closing_entry_from_opening,
)
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import (
create_opening_entry,
)
test_user, pos_profile = init_user_and_profile()
frappe.db.delete("POS Opening Entry", {"pos_profile": pos_profile.name})
if pos_profile:
opening_entry = create_opening_entry(pos_profile, test_user.name)
closing_entry = make_closing_entry_from_opening(opening_entry)
closing_entry.submit()
pos_profile.disabled = 1
pos_profile.save()
pos_profile.reload()
self.assertEqual(pos_profile.disabled, 1)
def get_customers_list(pos_profile=None):
if pos_profile is None:
@@ -117,6 +163,7 @@ def make_pos_profile(**args):
"write_off_account": args.write_off_account or "_Test Write Off - _TC",
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC",
"location": "Block 1" if not args.do_not_set_accounting_dimension else None,
"disabled": cint(args.disabled) or 0,
}
)

View File

@@ -243,10 +243,13 @@ def get_other_conditions(conditions, values, args):
if group_condition:
conditions += " and " + group_condition
if args.get("transaction_date"):
date = args.get("transaction_date") or frappe.get_value(
args.get("doctype"), args.get("name"), "posting_date", ignore=True
)
if date:
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')"""
values["transaction_date"] = args.get("transaction_date")
values["transaction_date"] = date
if args.get("doctype") in [
"Quotation",

View File

@@ -12,6 +12,7 @@ erpnext.buying.setup_buying_controller();
erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController {
setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check();
super.setup(doc);
@@ -125,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();
@@ -574,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

@@ -14,6 +14,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
erpnext.selling.SellingController
) {
setup(doc) {
this.setup_accounting_dimension_triggers();
this.setup_posting_date_time_check();
super.setup(doc);
this.frm.make_methods = {
@@ -647,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)
@@ -289,7 +290,9 @@ def merge_similar_entries(gl_map, precision=None):
company_currency = erpnext.get_company_currency(company)
if not precision:
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
precision = get_field_precision(
frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency
)
# filter zero debit and credit entries
merged_gl_map = filter(
@@ -412,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

@@ -26,16 +26,13 @@ frappe.query_reports["Accounts Payable"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: () => {
var company = frappe.query_report.get_filter_value("company");
return {
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "party_account",

View File

@@ -45,16 +45,13 @@ frappe.query_reports["Accounts Payable Summary"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: () => {
var company = frappe.query_report.get_filter_value("company");
return {
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "party_type",

View File

@@ -28,16 +28,13 @@ frappe.query_reports["Accounts Receivable"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: () => {
var company = frappe.query_report.get_filter_value("company");
return {
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "party_type",

View File

@@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_dimension_with_children,
)
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
from erpnext.accounts.utils import (
build_qb_match_conditions,
get_advance_payment_doctypes,
@@ -994,11 +995,7 @@ class ReceivablePayableReport:
self.add_accounting_dimensions_filters()
def get_cost_center_conditions(self):
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
cost_center_list = [
center.name
for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)})
]
cost_center_list = get_cost_centers_with_children(self.filters.cost_center)
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
def add_common_filters(self):

View File

@@ -45,16 +45,13 @@ frappe.query_reports["Accounts Receivable Summary"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: () => {
var company = frappe.query_report.get_filter_value("company");
return {
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "party_type",

View File

@@ -119,6 +119,7 @@ def get_assets_details(assets):
fields = [
"name as asset",
"asset_name",
"gross_purchase_amount",
"opening_accumulated_depreciation",
"asset_category",
@@ -143,6 +144,12 @@ def get_columns():
"options": "Asset",
"width": 120,
},
{
"label": _("Asset Name"),
"fieldname": "asset_name",
"fieldtype": "Data",
"width": 140,
},
{
"label": _("Depreciation Date"),
"fieldname": "depreciation_date",

View File

@@ -69,12 +69,18 @@ class PartyLedgerSummaryReport:
party_type = self.filters.party_type
doctype = qb.DocType(party_type)
party_details_fields = [
doctype.name.as_("party"),
f"{scrub(party_type)}_name",
f"{scrub(party_type)}_group",
]
if party_type == "Customer":
party_details_fields.append(doctype.territory)
conditions = self.get_party_conditions(doctype)
query = (
qb.from_(doctype)
.select(doctype.name.as_("party"), f"{scrub(party_type)}_name")
.where(Criterion.all(conditions))
)
query = qb.from_(doctype).select(*party_details_fields).where(Criterion.all(conditions))
from frappe.desk.reportview import build_match_conditions
@@ -153,6 +159,31 @@ class PartyLedgerSummaryReport:
credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note"
if self.filters.party_type == "Customer":
columns += [
{
"label": _("Customer Group"),
"fieldname": "customer_group",
"fieldtype": "Link",
"options": "Customer Group",
},
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
},
]
else:
columns += [
{
"label": _("Supplier Group"),
"fieldname": "supplier_group",
"fieldtype": "Link",
"options": "Supplier Group",
}
]
columns += [
{
"label": _("Opening Balance"),
@@ -213,35 +244,6 @@ class PartyLedgerSummaryReport:
},
]
# Hidden columns for handling 'User Permissions'
if self.filters.party_type == "Customer":
columns += [
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
"hidden": 1,
},
{
"label": _("Customer Group"),
"fieldname": "customer_group",
"fieldtype": "Link",
"options": "Customer Group",
"hidden": 1,
},
]
else:
columns += [
{
"label": _("Supplier Group"),
"fieldname": "supplier_group",
"fieldtype": "Link",
"options": "Supplier Group",
"hidden": 1,
}
]
return columns
def get_data(self):

View File

@@ -174,7 +174,7 @@ def add_solvency_ratios(
return_on_equity_ratio = {"ratio": _("Return on Equity Ratio")}
for year in years:
profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
profit_after_tax = flt(total_income.get(year)) - flt(total_expense.get(year))
share_holder_fund = flt(total_asset.get(year)) - flt(total_liability.get(year))
debt_equity_ratio[year] = calculate_ratio(total_liability.get(year), share_holder_fund, precision)
@@ -199,7 +199,7 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale
avg_data = {}
for d in ["Receivable", "Payable", "Stock"]:
avg_data[frappe.scrub(d)] = avg_ratio_balance("Receivable", period_list, precision, filters)
avg_data[frappe.scrub(d)] = avg_ratio_balance(d, period_list, precision, filters)
avg_debtors, avg_creditors, avg_stock = (
avg_data.get("receivable"),

View File

@@ -1,5 +1,5 @@
{
"add_total_row": 1,
"add_total_row": 0,
"add_translate_data": 0,
"columns": [],
"creation": "2013-12-06 13:22:23",
@@ -10,7 +10,7 @@
"idx": 3,
"is_standard": "Yes",
"letterhead": null,
"modified": "2025-08-13 12:47:27.645023",
"modified": "2025-11-05 15:47:59.597853",
"modified_by": "Administrator",
"module": "Accounts",
"name": "General Ledger",

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:
@@ -566,6 +570,13 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
else:
update_value_in_dict(consolidated_gle, key, gle)
if filters.get("include_dimensions"):
dimensions = [*accounting_dimensions, "cost_center", "project"]
for dimension in dimensions:
if val := gle.get(dimension):
gle[dimension] = _(val)
for value in consolidated_gle.values():
update_value_in_dict(totals, "total", value)
update_value_in_dict(totals, "closing", value)

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

@@ -21,6 +21,7 @@ def get_ordered_to_be_billed_data(args, filters=None):
doctype = frappe.qb.DocType(doctype)
child_doctype = frappe.qb.DocType(child_tab)
item = frappe.qb.DocType("Item")
docname = filters.get(args.get("reference_field"), None)
project_field = get_project_field(doctype, child_doctype, party)
@@ -29,6 +30,8 @@ def get_ordered_to_be_billed_data(args, filters=None):
frappe.qb.from_(doctype)
.inner_join(child_doctype)
.on(doctype.name == child_doctype.parent)
.join(item)
.on(item.name == child_doctype.item_code)
.select(
doctype.name,
doctype[args.get("date")].as_("date"),
@@ -54,6 +57,7 @@ def get_ordered_to_be_billed_data(args, filters=None):
& (doctype.company == filters.get("company"))
& (doctype.posting_date <= filters.get("posting_date"))
& (child_doctype.amount > 0)
& (item.is_stock_item == 1)
& (
child_doctype.base_amount
- Round(child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1), precision)

View File

@@ -1,32 +1,37 @@
{
"add_total_row": 1,
"apply_user_permissions": 1,
"creation": "2013-06-13 18:46:55",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"modified": "2018-02-21 01:28:31.261299",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Trends",
"owner": "Administrator",
"ref_doctype": "Purchase Invoice",
"report_name": "Purchase Invoice Trends",
"report_type": "Script Report",
"add_total_row": 0,
"add_translate_data": 0,
"columns": [],
"creation": "2013-06-13 18:46:55",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"letterhead": null,
"modified": "2025-11-05 11:55:49.950442",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Trends",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Invoice",
"report_name": "Purchase Invoice Trends",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
},
{
"role": "Purchase User"
},
},
{
"role": "Accounts Manager"
},
},
{
"role": "Auditor"
}
]
}
],
"timeout": 0
}

View File

@@ -1,26 +1,31 @@
{
"add_total_row": 1,
"apply_user_permissions": 1,
"creation": "2013-06-13 18:44:21",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"modified": "2018-02-21 01:28:03.622485",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Trends",
"owner": "Administrator",
"ref_doctype": "Sales Invoice",
"report_name": "Sales Invoice Trends",
"report_type": "Script Report",
"add_total_row": 0,
"add_translate_data": 0,
"columns": [],
"creation": "2013-06-13 18:44:21",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"letterhead": null,
"modified": "2025-11-05 11:55:50.070651",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Trends",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Sales Invoice",
"report_name": "Sales Invoice Trends",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts Manager"
},
},
{
"role": "Accounts User"
}
]
}
],
"timeout": 0
}

View File

@@ -4,7 +4,9 @@
import frappe
from frappe import _
from frappe.utils import getdate
from frappe.utils import flt, getdate
from erpnext.accounts.utils import get_currency_precision
def execute(filters=None):
@@ -43,6 +45,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
party_map = get_party_pan_map(filters.get("party_type"))
tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(tds_docs)
precision = get_currency_precision()
out = []
entries = {}
@@ -72,17 +75,28 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
if net_total_map.get((voucher_type, name)):
values = net_total_map.get((voucher_type, name))
if values:
if voucher_type == "Journal Entry" and tax_amount and rate:
# back calcalute total amount from rate and tax_amount
base_total = min(tax_amount / (rate / 100), net_total_map.get((voucher_type, name))[0])
# back calculate total amount from rate and tax_amount
base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0])
total_amount = grand_total = base_total
elif voucher_type == "Purchase Invoice":
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(
(voucher_type, name)
)
else:
total_amount, grand_total, base_total = net_total_map.get((voucher_type, name))
if tax_amount and rate:
# back calculate total amount from rate and tax_amount
total_amount = flt((tax_amount * 100) / rate, precision=precision)
else:
total_amount = values[0]
grand_total = values[1]
base_total = values[2]
if voucher_type == "Purchase Invoice":
bill_no = values[3]
bill_date = values[4]
else:
total_amount += entry.credit

View File

@@ -47,22 +47,23 @@ frappe.query_reports["Trial Balance"] = {
{
fieldname: "cost_center",
label: __("Cost Center"),
fieldtype: "Link",
options: "Cost Center",
get_query: function () {
var company = frappe.query_report.get_filter_value("company");
return {
doctype: "Cost Center",
filters: {
company: company,
},
};
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Cost Center", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Cost Center",
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "Link",
fieldtype: "MultiSelectList",
get_data: function (txt) {
return frappe.db.get_link_options("Project", txt, {
company: frappe.query_report.get_filter_value("company"),
});
},
options: "Project",
},
{

View File

@@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
from erpnext.accounts.report.financial_statements import (
filter_accounts,
filter_out_zero_value_rows,
get_cost_centers_with_children,
set_gl_entries_by_account,
)
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
@@ -103,10 +104,6 @@ def get_data(filters):
opening_balances = get_opening_balances(filters, ignore_is_opening)
# add filter inside list so that the query in financial_statements.py doesn't break
if filters.project:
filters.project = [filters.project]
set_gl_entries_by_account(
filters.company,
filters.from_date,
@@ -270,18 +267,12 @@ def get_opening_balance(
opening_balance = opening_balance.where(closing_balance.voucher_type != "Period Closing Voucher")
if filters.cost_center:
lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"])
cost_center = frappe.qb.DocType("Cost Center")
opening_balance = opening_balance.where(
closing_balance.cost_center.isin(
frappe.qb.from_(cost_center)
.select("name")
.where((cost_center.lft >= lft) & (cost_center.rgt <= rgt))
)
closing_balance.cost_center.isin(get_cost_centers_with_children(filters.get("cost_center")))
)
if filters.project:
opening_balance = opening_balance.where(closing_balance.project == filters.project)
opening_balance = opening_balance.where(closing_balance.project.isin(filters.project))
if frappe.db.count("Finance Book"):
if filters.get("include_default_book_entries"):
@@ -408,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

@@ -1755,24 +1755,22 @@ def check_and_delete_linked_reports(report):
frappe.delete_doc("Desktop Icon", icon)
def create_err_and_its_journals(companies: list | None = None) -> None:
if companies:
for company in companies:
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = company.name
err.posting_date = nowdate()
err.rounding_loss_allowance = 0.0
def create_err_and_its_journals(company: dict) -> None:
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = company.name
err.posting_date = nowdate()
err.rounding_loss_allowance = 0.0
err.fetch_and_calculate_accounts_data()
if err.accounts:
err.save().submit()
response = err.make_jv_entries()
err.fetch_and_calculate_accounts_data()
if err.accounts:
err.save().submit()
response = err.make_jv_entries()
if company.submit_err_jv:
jv = response.get("revaluation_jv", None)
jv and frappe.get_doc("Journal Entry", jv).submit()
jv = response.get("zero_balance_jv", None)
jv and frappe.get_doc("Journal Entry", jv).submit()
if company.submit_err_jv:
jv = response.get("revaluation_jv", None)
jv and frappe.get_doc("Journal Entry", jv).submit()
jv = response.get("zero_balance_jv", None)
jv and frappe.get_doc("Journal Entry", jv).submit()
def _auto_create_exchange_rate_revaluation_for(frequency: str) -> None:
@@ -1785,7 +1783,14 @@ def _auto_create_exchange_rate_revaluation_for(frequency: str) -> None:
filters={"auto_exchange_rate_revaluation": 1, "auto_err_frequency": frequency},
fields=["name", "submit_err_jv"],
)
create_err_and_its_journals(companies)
if companies:
for company in companies:
frappe.enqueue(
"erpnext.accounts.utils.create_err_and_its_journals",
company=company,
queue="long",
)
def auto_create_exchange_rate_revaluation_daily() -> None:

View File

@@ -202,7 +202,7 @@ frappe.ui.form.on("Asset", {
callback: function (r) {
if (!r.message) {
$(".primary-action").prop("hidden", true);
$(".form-message").text("Capitalize this asset to confirm");
$(".form-message").text(__("Capitalize this asset to confirm"));
frm.add_custom_button(__("Capitalize Asset"), function () {
frm.trigger("create_asset_capitalization");

View File

@@ -371,7 +371,6 @@
"label": "Other Details"
},
{
"allow_on_submit": 1,
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
@@ -379,7 +378,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
"options": "Draft\nSubmitted\nCancelled\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
"read_only": 1
},
{
@@ -597,7 +596,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2025-10-23 22:43:33.634452",
"modified": "2025-11-17 18:01:51.417942",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -103,6 +103,7 @@ class Asset(AccountsController):
status: DF.Literal[
"Draft",
"Submitted",
"Cancelled",
"Partially Depreciated",
"Fully Depreciated",
"Sold",
@@ -458,6 +459,7 @@ class Asset(AccountsController):
"asset_name": self.asset_name,
"target_location": self.location,
"to_employee": self.custodian,
"company": self.company,
}
]
asset_movement = frappe.get_doc(
@@ -1204,7 +1206,7 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
return {
"company": purchase_doc.company,
"purchase_date": purchase_doc.get("bill_date") or purchase_doc.get("posting_date"),
"purchase_date": purchase_doc.get("posting_date"),
"gross_purchase_amount": flt(first_item.base_net_amount),
"asset_quantity": first_item.qty,
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),

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

@@ -63,14 +63,7 @@ frappe.ui.form.on("Asset Repair", {
},
refresh: function (frm) {
if (frm.doc.docstatus) {
frm.add_custom_button(__("View General Ledger"), function () {
frappe.route_options = {
voucher_no: frm.doc.name,
};
frappe.set_route("query-report", "General Ledger");
});
}
frm.events.show_general_ledger(frm);
let sbb_field = frm.get_docfield("stock_items", "serial_and_batch_bundle");
if (sbb_field) {
@@ -134,6 +127,26 @@ frappe.ui.form.on("Asset Repair", {
frm.set_value("repair_cost", 0);
}
},
show_general_ledger: (frm) => {
if (frm.doc.docstatus > 0) {
frm.add_custom_button(
__("Accounting Ledger"),
function () {
frappe.route_options = {
voucher_no: frm.doc.name,
from_date: frm.doc.posting_date,
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
company: frm.doc.company,
categorize_by: "",
show_cancelled_entries: frm.doc.docstatus === 2,
};
frappe.set_route("query-report", "General Ledger");
},
__("View")
);
}
},
});
frappe.ui.form.on("Asset Repair Consumed Item", {

View File

@@ -139,7 +139,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Asset",
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",null]]]",
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\",null]]]",
"options": "Asset",
"reqd": 1
},
@@ -250,7 +250,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2025-07-29 15:14:34.044564",
"modified": "2025-11-17 18:35:54.575265",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",

View File

@@ -6,6 +6,9 @@ from frappe import _
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
@@ -60,6 +63,17 @@ class AssetRepair(AccountsController):
if self.get("stock_items"):
self.set_stock_items_cost()
self.calculate_total_repair_cost()
self.validate_purchase_invoice_status()
def validate_purchase_invoice_status(self):
if self.purchase_invoice:
docstatus = frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "docstatus")
if docstatus == 0:
frappe.throw(
_("{0} is still in Draft. Please submit it before saving the Asset Repair.").format(
get_link_to_form("Purchase Invoice", self.purchase_invoice)
)
)
def validate_asset(self):
if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"):
@@ -235,6 +249,12 @@ class AssetRepair(AccountsController):
)
stock_entry.asset_repair = self.name
accounting_dimensions = {
"cost_center": self.cost_center,
"project": self.project,
**{dimension: self.get(dimension) for dimension in get_accounting_dimensions()},
}
for stock_item in self.get("stock_items"):
self.validate_serial_no(stock_item)
@@ -246,8 +266,7 @@ class AssetRepair(AccountsController):
"qty": stock_item.consumed_quantity,
"basic_rate": stock_item.valuation_rate,
"serial_and_batch_bundle": stock_item.serial_and_batch_bundle,
"cost_center": self.cost_center,
"project": self.project,
**accounting_dimensions,
},
)
@@ -304,8 +323,8 @@ class AssetRepair(AccountsController):
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": self.completion_date,
"against_voucher_type": "Purchase Invoice",
"against_voucher": self.purchase_invoice,
"against_voucher_type": "Asset",
"against_voucher": self.asset,
"company": self.company,
},
item=self,

View File

@@ -4,6 +4,8 @@
import unittest
import frappe
from frappe import qb
from frappe.query_builder.functions import Sum
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
from erpnext.assets.doctype.asset.asset import (
@@ -294,6 +296,31 @@ class TestAssetRepair(unittest.TestCase):
stock_entry = frappe.get_last_doc("Stock Entry")
self.assertEqual(stock_entry.asset_repair, asset_repair.name)
def test_gl_entries_with_capitalized_asset_repair(self):
asset = create_asset(is_existing_asset=1, calculate_depreciation=1, submit=1)
asset_repair = create_asset_repair(
asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
)
asset.reload()
GLEntry = qb.DocType("GL Entry")
res = (
qb.from_(GLEntry)
.select(Sum(GLEntry.debit_in_account_currency).as_("total_debit"))
.where(
(GLEntry.voucher_type == "Asset Repair")
& (GLEntry.voucher_no == asset_repair.name)
& (GLEntry.against_voucher_type == "Asset")
& (GLEntry.against_voucher == asset.name)
& (GLEntry.company == asset.company)
& (GLEntry.is_cancelled == 0)
)
).run(as_dict=True)
booked_value = res[0].total_debit if res else 0
self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost)
self.assertEqual(booked_value, asset_repair.repair_cost)
def num_of_depreciations(asset):
return asset.finance_books[0].total_number_of_depreciations

View File

@@ -36,6 +36,7 @@
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
"over_transfer_allowance",
"validate_consumed_qty",
"section_break_xcug",
"auto_create_subcontracting_order",
"column_break_izrr",
@@ -270,6 +271,14 @@
"label": "Fixed Outgoing Email Account",
"link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]",
"options": "Email Account"
},
{
"default": "0",
"depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"Material Transferred for Subcontract\"",
"description": "Raw materials consumed qty will be validated based on FG BOM required qty",
"fieldname": "validate_consumed_qty",
"fieldtype": "Check",
"label": "Validate Consumed Qty (as per BOM)"
}
],
"grid_page_length": 50,
@@ -278,7 +287,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-08-20 22:13:38.506889",
"modified": "2025-11-20 12:59:09.925862",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -44,6 +44,7 @@ class BuyingSettings(Document):
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
supplier_group: DF.Link | None
use_transaction_date_exchange_rate: DF.Check
validate_consumed_qty: DF.Check
# end: auto-generated types
def validate(self):

View File

@@ -303,6 +303,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
erpnext.buying.BuyingController
) {
setup() {
this.setup_accounting_dimension_triggers();
this.frm.custom_make_buttons = {
"Purchase Receipt": "Purchase Receipt",
"Purchase Invoice": "Purchase Invoice",

View File

@@ -76,6 +76,46 @@ class TestRequestforQuotation(FrappeTestCase):
self.assertEqual(sq1.get("items")[0].item_code, "_Test Item")
self.assertEqual(sq1.get("items")[0].qty, 5)
def test_make_supplier_quotation_with_taxes(self):
"""Test automatic tax addition when supplier quotation is created from RFQ taxes_and_charges are set"""
# Create a Purchase Taxes and Charges Template for testing
tax_template = frappe.new_doc("Purchase Taxes and Charges Template")
tax_template.doctype = "Purchase Taxes and Charges Template"
tax_template.title = "_Test Purchase Taxes Template for RFQ"
tax_template.company = "_Test Company"
tax_template.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"description": "VAT",
"rate": 10,
},
)
tax_template.save()
rfq = make_request_for_quotation()
supplier = rfq.get("suppliers")[0].supplier
tax_rule = frappe.new_doc("Tax Rule")
tax_rule.company = "_Test Company"
tax_rule.tax_type = "Purchase"
tax_rule.supplier = supplier
tax_rule.purchase_tax_template = tax_template.name
tax_rule.save()
sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=supplier)
# Verify that taxes_and_charges is set from get_party_details
self.assertEqual(sq.taxes_and_charges, tax_template.name)
# Verify that taxes are automatically added
self.assertGreaterEqual(len(sq.get("taxes")), 1)
tax_rule.delete()
tax_template.delete()
def test_make_supplier_quotation_with_special_characters(self):
frappe.delete_doc_if_exists("Supplier", "_Test Supplier '1", force=1)
supplier = frappe.new_doc("Supplier")

View File

@@ -41,18 +41,20 @@ frappe.ui.form.on("Supplier", {
frm.set_query("supplier_primary_contact", function (doc) {
return {
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact",
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
filters: {
supplier: doc.name,
type: "Contact",
},
};
});
frm.set_query("supplier_primary_address", function (doc) {
return {
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
filters: {
link_doctype: "Supplier",
link_name: doc.name,
supplier: doc.name,
type: "Address",
},
};
});
@@ -137,6 +139,14 @@ frappe.ui.form.on("Supplier", {
// indicators
erpnext.utils.set_party_dashboard_indicators(frm);
}
frm.set_query("supplier_group", () => {
return {
filters: {
is_group: 0,
},
};
});
},
get_supplier_group_details: function (frm) {
frappe.call({

View File

@@ -215,19 +215,25 @@ class Supplier(TransactionBase):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
def get_supplier_primary(doctype, txt, searchfield, start, page_len, filters):
supplier = filters.get("supplier")
contact = frappe.qb.DocType("Contact")
type = filters.get("type")
type_doctype = frappe.qb.DocType(type)
dynamic_link = frappe.qb.DocType("Dynamic Link")
return (
frappe.qb.from_(contact)
query = (
frappe.qb.from_(type_doctype)
.join(dynamic_link)
.on(contact.name == dynamic_link.parent)
.select(contact.name, contact.email_id)
.on(type_doctype.name == dynamic_link.parent)
.select(type_doctype.name)
.where(
(dynamic_link.link_name == supplier)
& (dynamic_link.link_doctype == "Supplier")
& (contact.name.like(f"%{txt}%"))
& (type_doctype.name.like(f"%{txt}%"))
)
).run(as_dict=False)
)
if type == "Contact":
query = query.select(type_doctype.email_id)
return query.run()

View File

@@ -1,29 +1,34 @@
{
"add_total_row": 1,
"apply_user_permissions": 1,
"creation": "2013-06-13 18:45:01",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"modified": "2018-02-21 01:28:37.416562",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Trends",
"owner": "Administrator",
"ref_doctype": "Purchase Order",
"report_name": "Purchase Order Trends",
"report_type": "Script Report",
"add_total_row": 0,
"add_translate_data": 0,
"columns": [],
"creation": "2013-06-13 18:45:01",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"letterhead": null,
"modified": "2025-11-05 11:55:50.058154",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Trends",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Order",
"report_name": "Purchase Order Trends",
"report_type": "Script Report",
"roles": [
{
"role": "Stock User"
},
},
{
"role": "Purchase Manager"
},
},
{
"role": "Purchase User"
}
]
}
],
"timeout": 0
}

View File

@@ -39,6 +39,8 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
)
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
from erpnext.accounts.party import (
PURCHASE_TRANSACTION_TYPES,
SALES_TRANSACTION_TYPES,
get_party_account,
get_party_account_currency,
get_party_gle_currency,
@@ -306,6 +308,52 @@ class AccountsController(TransactionBase):
self.set_default_letter_head()
self.validate_company_in_accounting_dimension()
self.validate_party_address_and_contact()
self.validate_company_linked_addresses()
def validate_company_linked_addresses(self):
address_fields = []
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_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",
{
"parent": address,
"parenttype": "Address",
"link_doctype": "Company",
"link_name": self.company,
},
):
frappe.throw(
_("{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:
@@ -360,6 +408,24 @@ class AccountsController(TransactionBase):
for _doctype in repost_doctypes:
dt = frappe.qb.DocType(_doctype)
cancelled_entries = (
frappe.qb.from_(dt)
.select(dt.parent, dt.parenttype)
.where((dt.voucher_type == self.doctype) & (dt.voucher_no == self.name) & (dt.docstatus == 2))
.run(as_dict=True)
)
if cancelled_entries:
entries = "<br>".join([get_link_to_form(d.parenttype, d.parent) for d in cancelled_entries])
frappe.throw(
_(
"The following cancelled repost entries exist for <b>{0}</b>:<br><br>{1}<br><br>"
"Kindly delete these entries before continuing."
).format(self.name, entries)
)
rows = (
frappe.qb.from_(dt)
.select(dt.name, dt.parent, dt.parenttype)
@@ -2918,6 +2984,104 @@ class AccountsController(TransactionBase):
x["transaction_currency"] = self.currency
x["transaction_exchange_rate"] = self.get("conversion_rate") or 1
def after_mapping(self, source_doc):
self.set_discount_amount_after_mapping(source_doc)
def set_discount_amount_after_mapping(self, source_doc):
"""
Ensures that Additional Discount Amount is not copied repeatedly
for multiple mappings of a single source transaction.
"""
# source and target doctypes should both be buying / selling
for transaction_types in (PURCHASE_TRANSACTION_TYPES, SALES_TRANSACTION_TYPES):
if self.doctype in transaction_types and source_doc.doctype in transaction_types:
break
else:
return
# ensure both doctypes have discount_amount field
if not self.meta.get_field("discount_amount") or not source_doc.meta.get_field("discount_amount"):
return
# ensure discount_amount is set in source doc
if not source_doc.discount_amount:
return
# ensure additional_discount_percentage is not set in the source doc
if source_doc.get("additional_discount_percentage"):
return
item_doctype = self.meta.get_field("items").options
doctype_table = frappe.qb.DocType(self.doctype)
item_table = frappe.qb.DocType(item_doctype)
is_same_doctype = self.doctype == source_doc.doctype
is_return = self.get("is_return") and is_same_doctype
if is_same_doctype and not is_return:
# should never happen
# you don't map to the same doctype without it being a return
return
query = (
frappe.qb.from_(doctype_table)
.where(doctype_table.docstatus == 1)
.where(doctype_table.discount_amount != 0)
.select(Sum(doctype_table.discount_amount))
)
if is_return:
query = query.where(doctype_table.is_return == 1).where(
doctype_table.return_against == source_doc.name
)
else:
item_meta = frappe.get_meta(item_doctype)
reference_fieldname = next(
(
row.fieldname
for row in item_meta.fields
if row.fieldtype == "Link"
and row.options == source_doc.doctype
and not row.get("is_custom_field")
),
None,
)
if not reference_fieldname:
return
query = query.where(
doctype_table.name.isin(
frappe.qb.from_(item_table)
.select(item_table.parent)
.where(item_table[reference_fieldname] == source_doc.name)
.distinct()
)
)
result = query.run()
if not result:
return
discount_already_applied = result[0][0]
if not discount_already_applied:
return
if is_return:
# returns have negative discount
discount_already_applied *= -1
discount_amount = max(source_doc.discount_amount - discount_already_applied, 0)
if discount_amount and is_return:
discount_amount *= -1
self.discount_amount = flt(discount_amount, self.precision("discount_amount"))
self.calculate_taxes_and_totals()
@frappe.whitelist()
def get_tax_rate(account_head):

View File

@@ -13,6 +13,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.party import get_party_details
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.get_item_details import get_conversion_factor
@@ -180,6 +181,12 @@ class BuyingController(SubcontractingController):
self.set_missing_item_details(for_validate)
if self.meta.get_field("taxes"):
if self.get("taxes_and_charges") and not self.get("taxes") and not for_validate:
taxes = get_taxes_and_charges("Purchase Taxes and Charges Template", self.taxes_and_charges)
for tax in taxes:
self.append("taxes", tax)
def set_supplier_from_item_default(self):
if self.meta.get_field("supplier") and not self.supplier:
for d in self.get("items"):

View File

@@ -185,7 +185,7 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
frappe.get_meta(doc.doctype + " Item").get_field(
"stock_qty" if doc.get("update_stock", "") else "qty"
),
company_currency,
currency=company_currency,
)
for column in fields:

View File

@@ -95,6 +95,7 @@ class SellingController(StockController):
# set contact and address details for customer, if they are not mentioned
self.set_missing_lead_customer_details(for_validate=for_validate)
self.set_price_list_and_item_details(for_validate=for_validate)
self.set_company_contact_person()
def set_missing_lead_customer_details(self, for_validate=False):
customer, lead = None, None
@@ -137,6 +138,7 @@ class SellingController(StockController):
lead,
posting_date=self.get("transaction_date") or self.get("posting_date"),
company=self.company,
doctype=self.doctype,
)
)
@@ -149,6 +151,13 @@ class SellingController(StockController):
self.set_price_list_currency("Selling")
self.set_missing_item_details(for_validate=for_validate)
def set_company_contact_person(self):
"""Set the Company's Default Sales Contact as Company Contact Person."""
if self.company and self.meta.has_field("company_contact_person") and not self.company_contact_person:
self.company_contact_person = frappe.get_cached_value(
"Company", self.company, "default_sales_contact"
)
def remove_shipping_charge(self):
if self.shipping_rule:
shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule)

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"],
],
@@ -93,6 +94,7 @@ status_map = {
["Draft", None],
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"],
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
[
"Completed",

View File

@@ -11,6 +11,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, flt, get_link_to_form
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.batch.batch import get_batch_qty
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
get_auto_batch_nos,
get_available_serial_nos,
@@ -505,7 +506,7 @@ class SubcontractingController(StockController):
if item.get("serial_and_batch_bundle"):
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
def _get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
@@ -686,7 +687,11 @@ class SubcontractingController(StockController):
serial_nos = get_filtered_serial_nos(serial_nos, self, "supplied_items")
row.serial_no = "\n".join(serial_nos)
elif item_details.has_batch_no and not row.serial_and_batch_bundle and not row.batch_no:
elif (
item_details.has_batch_no
and not row.serial_and_batch_bundle
and (not row.batch_no or self.batch_has_not_available(row.batch_no, row.consumed_qty))
):
batches = get_auto_batch_nos(kwargs)
if batches:
consumed_qty = row.consumed_qty
@@ -711,6 +716,11 @@ class SubcontractingController(StockController):
)
consumed_qty -= d.get("qty")
def batch_has_not_available(self, batch_no, qty_required):
batch_qty = get_batch_qty(batch_no, self.supplier_warehouse, consider_negative_batches=True)
return batch_qty < qty_required
def update_rate_for_supplied_items(self):
if self.doctype != "Subcontracting Receipt":
return
@@ -849,7 +859,7 @@ class SubcontractingController(StockController):
if self.doctype == self.subcontract_data.order_doctype or (
self.backflush_based_on == "BOM" or self.is_return
):
for bom_item in self.__get_materials_from_bom(
for bom_item in self._get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items")
):
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor

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
@@ -682,6 +686,41 @@ class calculate_taxes_and_totals:
self.doc.precision("discount_amount"),
)
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)
or (grand_total < 0 and discount_amount < grand_total) # returns
):
frappe.throw(
_(
"Additional Discount Amount ({discount_amount}) cannot exceed "
"the total before such discount ({total_before_discount})"
).format(
discount_amount=self.doc.get_formatted("discount_amount"),
total_before_discount=self.doc.get_formatted("grand_total"),
),
title=_("Invalid Discount Amount"),
)
def apply_discount_amount(self):
if self.doc.discount_amount:
if not self.doc.apply_discount_on:
@@ -814,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

@@ -16,7 +16,10 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_purchase_order,
prepare_data_for_internal_transfer,
)
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item
@@ -2258,3 +2261,218 @@ class TestAccountsController(FrappeTestCase):
self.assertRaises(frappe.ValidationError, si.save)
si.contact_person = customer_contact.name
si.save()
def test_discount_amount_not_mapped_repeatedly_for_sales_transactions(self):
"""
Test that additional discount amount is not copied repeatedly
when creating multiple delivery notes from a single sales order with discount_amount set
"""
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
# Create a sales order with discount amount
so = make_sales_order(qty=10, rate=100, do_not_submit=True)
so.apply_discount_on = "Net Total"
so.discount_amount = 100
so.save()
so.submit()
# Create first delivery note from sales order (partial qty)
dn1 = make_delivery_note(so.name)
dn1.items[0].qty = 5
dn1.save()
dn1.submit()
# First delivery note should have full discount amount
self.assertEqual(dn1.discount_amount, 100)
self.assertEqual(dn1.grand_total, 400)
# Create second delivery note from the same sales order (remaining qty)
dn2 = make_delivery_note(so.name)
dn2.items[0].qty = 5
dn2.save()
dn2.submit()
# Second delivery note should have discount_amount set to 0
# because discount was already fully applied in first delivery note
self.assertEqual(dn2.discount_amount, 0)
self.assertEqual(dn2.grand_total, 500)
def test_discount_amount_not_mapped_repeatedly_for_purchase_transactions(self):
"""
Test that additional discount amount is not copied repeatedly
when creating multiple purchase receipts from a single purchase order with discount_amount set
"""
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
# Create a purchase order with discount amount
po = create_purchase_order(qty=10, rate=100, do_not_submit=True)
po.apply_discount_on = "Net Total"
po.discount_amount = 100
po.save()
po.submit()
# Create first purchase receipt from purchase order (partial qty)
pr1 = make_purchase_receipt(po.name)
pr1.items[0].qty = 5
pr1.save()
pr1.submit()
# First purchase receipt should have full discount amount
self.assertEqual(pr1.discount_amount, 100)
self.assertEqual(pr1.grand_total, 400)
# Create second purchase receipt from the same purchase order (remaining qty)
pr2 = make_purchase_receipt(po.name)
pr2.items[0].qty = 5
pr2.save()
pr2.submit()
# Second purchase receipt should have discount_amount set to 0
# because discount was already fully applied in first purchase receipt
self.assertEqual(pr2.discount_amount, 0)
self.assertEqual(pr2.grand_total, 500)
def test_discount_amount_partial_application_in_mapped_transactions(self):
"""
Test that discount amount is partially applied when some discount
has already been used in previous mapped transactions
"""
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
# Create a sales order with discount amount
so = make_sales_order(qty=10, rate=100, do_not_submit=True)
so.apply_discount_on = "Net Total"
so.discount_amount = 200
so.save()
so.submit()
self.assertEqual(so.discount_amount, 200)
self.assertEqual(so.grand_total, 800)
# Create first invoice with partial discount (manually set lower discount)
si1 = make_sales_invoice(so.name)
si1.items[0].qty = 5
si1.discount_amount = 50 # Partial discount application
si1.save()
si1.submit()
self.assertEqual(si1.discount_amount, 50)
self.assertEqual(si1.grand_total, 450)
# Create second invoice from the same sales order
si2 = make_sales_invoice(so.name)
si2.items[0].qty = 5
si2.save()
si2.submit()
# Second invoice should have remaining discount (200 - 50 = 150)
self.assertEqual(si2.discount_amount, 150)
self.assertEqual(si2.grand_total, 350)
def test_discount_amount_not_mapped_when_percentage_is_set(self):
"""
Test that discount amount is not adjusted when additional_discount_percentage
is set in the source document (as it will be recalculated based on percentage)
"""
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
# Create a sales order with discount percentage instead of amount
so = make_sales_order(qty=10, rate=100, do_not_submit=True)
so.apply_discount_on = "Net Total"
so.additional_discount_percentage = 10 # 10% discount
so.save()
so.submit()
self.assertEqual(so.discount_amount, 100) # 10% of 1000
self.assertEqual(so.grand_total, 900)
# Create delivery note from sales order
dn = make_delivery_note(so.name)
dn.items[0].qty = 5
dn.save()
# Delivery note should have discount amount recalculated based on percentage
# and not affected by the repeated mapping logic
self.assertEqual(dn.additional_discount_percentage, 10)
self.assertEqual(dn.discount_amount, 50) # 10% of 500
def test_discount_amount_for_multiple_returns(self):
"""
Test that discount amount is correctly adjusted when multiple return invoices
are created against the same original invoice to prevent over-returning discount
"""
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
# Create original sales invoice with discount
si = create_sales_invoice(qty=10, rate=100, do_not_submit=True)
si.apply_discount_on = "Net Total"
si.discount_amount = 100
si.save()
si.submit()
# Create first return - Frappe will copy full discount by default, we need to adjust it
return_si_1 = make_sales_return(si.name)
return_si_1.items[0].qty = -6 # Return 6 out of 10 items
# Manually set discount to match the proportion (60% of discount)
return_si_1.discount_amount = -60
return_si_1.save()
return_si_1.submit()
self.assertEqual(return_si_1.discount_amount, -60)
# Create second return for remaining items
return_si_2 = make_sales_return(si.name)
return_si_2.items[0].qty = -4 # Return remaining 4 out of 10 items
return_si_2.save()
# Second return should only get remaining discount (100 - 60 = 40)
self.assertEqual(return_si_2.discount_amount, -40)
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"
)
company_address.append("links", {"link_doctype": "Company", "link_name": "_Test Company"})
company_address.save()
customer_shipping = make_address(
address_title="Customer", address_type="Shipping", address_line1="10"
)
customer_shipping.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
customer_shipping.save()
supplier_billing = make_address(address_title="Supplier", address_line1="2", city="Ahmedabad")
supplier_billing.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"})
supplier_billing.save()
po = create_purchase_order(do_not_save=True)
po.shipping_address = customer_shipping.name
self.assertRaises(frappe.ValidationError, po.save)
po.shipping_address = company_address.name
po.save()
po.billing_address = supplier_billing.name
self.assertRaises(frappe.ValidationError, po.save)
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

@@ -191,6 +191,9 @@ def get_data(filters, conditions):
des[j + inc] = row1[0][j]
data.append(des)
total_row = calculate_total_row(data1, conditions["columns"])
data.append(total_row)
else:
data = frappe.db.sql(
""" select {} from `tab{}` t1, `tab{} Item` t2 {}
@@ -214,9 +217,32 @@ def get_data(filters, conditions):
as_list=1,
)
total_row = calculate_total_row(data, conditions["columns"])
data.append(total_row)
return data
def calculate_total_row(data, columns):
def wrap_in_quotes(label):
return f"'{label}'"
total_values = {}
for i, col in enumerate(columns):
if "Float" in col or "Currency/currency" in col:
total_values[i] = 0
for row in data:
for i in total_values.keys():
total_values[i] += row[i] if row[i] is not None else 0
total_row = [wrap_in_quotes(_("Total"))]
for i in range(1, len(columns)):
total_row.append(total_values.get(i, None))
return total_row
def get_mon(dt):
return getdate(dt).strftime("%b")

View File

@@ -123,7 +123,7 @@ def send_mail(entry, email_campaign):
subject=frappe.render_template(email_template.get("subject"), context),
content=frappe.render_template(email_template.response_, context),
sender=sender,
recipients=recipient_list,
bcc=recipient_list,
communication_medium="Email",
sent_or_received="Sent",
send_email=True,

View File

@@ -432,7 +432,7 @@ def _set_missing_values(source, target):
@frappe.whitelist()
def get_lead_details(lead, posting_date=None, company=None):
def get_lead_details(lead, posting_date=None, company=None, doctype=None):
if not lead:
return {}
@@ -454,7 +454,7 @@ def get_lead_details(lead, posting_date=None, company=None):
}
)
set_address_details(out, lead, "Lead", company=company)
set_address_details(out, lead, "Lead", doctype=doctype, company=company)
taxes_and_charges = set_taxes(
None,

View File

@@ -340,10 +340,7 @@ doc_events = {
"User": {
"after_insert": "frappe.contacts.doctype.contact.contact.update_contact",
"validate": "erpnext.setup.doctype.employee.employee.validate_employee_role",
"on_update": [
"erpnext.setup.doctype.employee.employee.update_user_permissions",
"erpnext.portal.utils.set_default_role",
],
"on_update": "erpnext.portal.utils.set_default_role",
},
"Communication": {
"on_update": [
@@ -415,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",
@@ -461,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

@@ -548,12 +548,14 @@
{
"fieldname": "process_loss_percentage",
"fieldtype": "Percent",
"label": "% Process Loss"
"label": "% Process Loss",
"non_negative": 1
},
{
"fieldname": "process_loss_qty",
"fieldtype": "Float",
"label": "Process Loss Qty",
"non_negative": 1,
"read_only": 1
},
{
@@ -591,7 +593,6 @@
},
{
"default": "0",
"depends_on": "eval:doc.track_semi_finished_goods === 0",
"fieldname": "fg_based_operating_cost",
"fieldtype": "Check",
"label": "Finished Goods based Operating Cost"
@@ -640,7 +641,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2025-10-29 17:43:12.966753",
"modified": "2025-11-19 16:17:15.925156",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",

View File

@@ -465,7 +465,7 @@ class BOM(WebsiteGenerator):
)
)
def get_rm_rate(self, arg):
def get_rm_rate(self, arg, notify=True):
"""Get raw material rate as per selected method, if bom exists takes bom cost"""
rate = 0
if not self.rm_cost_as_per:
@@ -491,7 +491,7 @@ class BOM(WebsiteGenerator):
),
alert=True,
)
else:
elif notify:
frappe.msgprint(
_("{0} not found for item {1}").format(self.rm_cost_as_per, arg["item_code"]),
alert=True,
@@ -796,11 +796,14 @@ class BOM(WebsiteGenerator):
"stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor,
"sourced_by_supplier": d.sourced_by_supplier,
}
},
notify=False,
)
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
d.amount = flt(
flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")), d.precision("amount")
)
d.base_amount = d.amount * flt(self.conversion_rate)
d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) / flt(
self.quantity, self.precision("quantity")
@@ -823,7 +826,10 @@ class BOM(WebsiteGenerator):
d.base_rate = flt(d.rate, d.precision("rate")) * flt(
self.conversion_rate, self.precision("conversion_rate")
)
d.amount = flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty"))
d.amount = flt(
flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")),
d.precision("amount"),
)
d.base_amount = flt(d.amount, d.precision("amount")) * flt(
self.conversion_rate, self.precision("conversion_rate")
)

View File

@@ -38,6 +38,15 @@ frappe.ui.form.on("Job Card", {
return doc.status === "Complete" ? "green" : "orange";
}
});
frm.set_query("employee", () => {
return {
filters: {
company: frm.doc.company,
status: "Active",
},
};
});
},
refresh: function (frm) {

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:
@@ -605,7 +600,7 @@ class JobCard(Document):
op_row.employee.append(time_log.employee)
if time_log.time_in_mins:
op_row.completed_time += time_log.time_in_mins
op_row.completed_qty += time_log.completed_qty
op_row.completed_qty += flt(time_log.completed_qty)
for row in self.sub_operations:
operation_deatils = operation_wise_completed_time.get(row.sub_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

@@ -1627,7 +1627,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
"min_order_qty": item_master.min_order_qty,
"default_material_request_type": item_master.default_material_request_type,
"qty": planned_qty or 1,
"is_sub_contracted": item_master.is_subcontracted_item,
"is_sub_contracted": item_master.is_sub_contracted_item,
"item_code": item_master.name,
"description": item_master.description,
"stock_uom": item_master.stock_uom,

View File

@@ -209,7 +209,7 @@ frappe.ui.form.on("Work Order", {
if (not_completed && not_completed.length) {
frm.add_custom_button(__("Create Job Card"), () => {
frm.trigger("make_job_card");
}).addClass("btn-primary");
});
}
}
}
@@ -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"),
@@ -253,7 +254,7 @@ frappe.ui.form.on("Work Order", {
if (non_consumed_items && non_consumed_items.length) {
frm.add_custom_button(__("Return Components"), function () {
frm.trigger("create_stock_return_entry");
}).addClass("btn-primary");
});
}
}
},
@@ -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

@@ -151,10 +151,9 @@ def get_column(filters):
},
{
"label": _("Document Type"),
"fieldtype": "Link",
"fieldtype": "Data",
"fieldname": "document_type",
"width": 150,
"options": "DocType",
},
{
"label": _("Document Name"),

View File

@@ -318,7 +318,7 @@
"type": "Link"
}
],
"modified": "2024-10-21 14:13:38.777556",
"modified": "2025-11-24 11:11:28.343568",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
@@ -336,7 +336,7 @@
"doc_view": "List",
"label": "Learn Manufacturing",
"type": "URL",
"url": "https://school.frappe.io/lms/courses/manufacturing?utm_source=in_app"
"url": "https://school.frappe.io/lms/courses/production-planning-and-execution"
},
{
"color": "Grey",

View File

@@ -424,3 +424,6 @@ erpnext.patches.v15_0.update_uae_zero_rated_fetch
erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter
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 #2025-12-11

View File

@@ -0,0 +1,17 @@
import frappe
def execute():
settings_meta = frappe.get_meta("Currency Exchange Settings")
settings = frappe.get_doc("Currency Exchange Settings")
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"
settings.set_parameters_and_result()
settings.flags.ignore_validate = True
settings.save()

View File

@@ -17,6 +17,10 @@ class CircularReferenceError(frappe.ValidationError):
pass
class ParentIsGroupError(frappe.ValidationError):
pass
class Task(NestedSet):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -83,6 +87,7 @@ class Task(NestedSet):
self.update_depends_on()
self.validate_dependencies_for_template_task()
self.validate_completed_on()
self.validate_parent_is_group()
def validate_dates(self):
self.validate_from_to_dates("exp_start_date", "exp_end_date")
@@ -153,20 +158,36 @@ class Task(NestedSet):
def validate_parent_template_task(self):
if self.parent_task:
if not frappe.db.get_value("Task", self.parent_task, "is_template"):
parent_task_format = f"""<a href="/app/task/{self.parent_task}">{self.parent_task}</a>"""
frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
frappe.throw(
_("Parent Task {0} is not a Template Task").format(
get_link_to_form("Task", self.parent_task)
)
)
def validate_depends_on_tasks(self):
if self.depends_on:
for task in self.depends_on:
if not frappe.db.get_value("Task", task.task, "is_template"):
dependent_task_format = f"""<a href="/app/task/{task.task}">{task.task}</a>"""
frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
frappe.throw(
_("Dependent Task {0} is not a Template Task").format(
get_link_to_form("Task", task.task)
)
)
def validate_completed_on(self):
if self.completed_on and getdate(self.completed_on) > getdate():
frappe.throw(_("Completed On cannot be greater than Today"))
def validate_parent_is_group(self):
if self.parent_task:
if not frappe.db.get_value("Task", self.parent_task, "is_group"):
frappe.throw(
_("Parent Task {0} must be a Group Task").format(
get_link_to_form("Task", self.parent_task)
),
ParentIsGroupError,
)
def update_depends_on(self):
depends_on_tasks = ""
for d in self.depends_on:

View File

@@ -6,7 +6,7 @@ import unittest
import frappe
from frappe.utils import add_days, getdate, nowdate
from erpnext.projects.doctype.task.task import CircularReferenceError
from erpnext.projects.doctype.task.task import CircularReferenceError, ParentIsGroupError
class TestTask(unittest.TestCase):
@@ -109,6 +109,20 @@ class TestTask(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue")
def test_parent_task_must_be_group(self):
parent_task = create_task(
subject="_Test Parent Task Non Group",
is_group=0,
)
child_task = create_task(
subject="_Test Child Task",
parent_task=parent_task.name,
save=False,
)
self.assertRaises(ParentIsGroupError, child_task.save)
def create_task(
subject,

View File

@@ -21,6 +21,7 @@ frappe.ui.form.on("Timesheet", {
filters: {
project: child.project,
status: ["!=", "Cancelled"],
is_group: 0,
},
};
};

View File

@@ -285,7 +285,7 @@ class Timesheet(Document):
if data.activity_type or data.is_billable:
rate = get_activity_cost(self.employee, data.activity_type)
hours = data.billing_hours or 0
costing_hours = data.billing_hours or data.hours or 0
costing_hours = data.hours or 0
if rate:
data.billing_rate = (
flt(rate.get("billing_rate")) if flt(data.billing_rate) == 0 else data.billing_rate

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

@@ -2,7 +2,6 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.buying");
// cur_frm.add_fetch('project', 'cost_center', 'cost_center');
erpnext.buying = {
setup_buying_controller: function() {
@@ -11,6 +10,7 @@ erpnext.buying = {
super.setup();
this.toggle_enable_for_stock_uom("allow_to_edit_stock_uom_qty_for_purchase");
this.frm.email_field = "contact_email";
this.frm.add_fetch("project", "cost_center", "cost_center");
}
onload(doc, cdt, cdn) {
@@ -171,15 +171,13 @@ erpnext.buying = {
shipping_address: this.frm.doc.shipping_address
},
callback: (r) => {
if (!this.frm.doc.billing_address)
this.frm.set_value("billing_address", r.message.primary_address || "");
if (!r.message) return;
if (
!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") ||
this.frm.doc.shipping_address
)
return;
this.frm.set_value("shipping_address", r.message.shipping_address || "");
this.frm.set_value("billing_address", r.message.primary_address || "");
if (frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) {
this.frm.set_value("shipping_address", r.message.shipping_address || "");
}
},
});
erpnext.utils.set_letter_head(this.frm)

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