Compare commits

..

263 Commits

Author SHA1 Message Date
Frappe PR Bot
4723fbfd57 chore(release): Bumped to Version 14.77.2
## [14.77.2](https://github.com/frappe/erpnext/compare/v14.77.1...v14.77.2) (2024-11-22)

### Bug Fixes

* patch ([#44191](https://github.com/frappe/erpnext/issues/44191)) ([d1295d1](d1295d1c79))
2024-11-22 11:28:10 +00:00
rohitwaghchaure
ab49ee2e05 Merge pull request #44289 from frappe/mergify/bp/version-14/pr-44288
fix: patch (backport #44191) (backport #44288)
2024-11-22 16:56:24 +05:30
rohitwaghchaure
d1295d1c79 fix: patch (#44191)
(cherry picked from commit 495528a758)
(cherry picked from commit 3643c60c67)
2024-11-22 10:56:37 +00:00
Frappe PR Bot
46a8500361 chore(release): Bumped to Version 14.77.1
## [14.77.1](https://github.com/frappe/erpnext/compare/v14.77.0...v14.77.1) (2024-11-22)

### Bug Fixes

* include current invoice amount when tax_on_excess_amount is checked ([43f59da](43f59da0a9))
2024-11-22 06:01:07 +00:00
ruthra kumar
88ba24f1cf Merge pull request #44270 from frappe/mergify/bp/version-14/pr-44194
fix: include current invoice amount when tax_on_excess_amount is checked (backport #44194)
2024-11-22 11:29:50 +05:30
venkat102
87bb403159 test: add unit test for tax on excess amount
(cherry picked from commit 4820273595)
2024-11-22 05:37:12 +00:00
venkat102
43f59da0a9 fix: include current invoice amount when tax_on_excess_amount is checked
(cherry picked from commit b74f2896cd)
2024-11-22 05:37:12 +00:00
Frappe PR Bot
2c865bcd49 chore(release): Bumped to Version 14.77.0
# [14.77.0](https://github.com/frappe/erpnext/compare/v14.76.0...v14.77.0) (2024-11-20)

### Bug Fixes

* added test cases ([5fa4fd8](5fa4fd8825))
* apply posting date sorting to invoices in Payment Reconciliation similar to payments ([cc9b22c](cc9b22ce9f))
* backport german translations from develop ([#44046](https://github.com/frappe/erpnext/issues/44046)) ([c23868a](c23868a14d))
* broken apply on other item pricing rule ([f03e301](f03e301250))
* broken UI on currency exchange ([36898f6](36898f6797))
* check if pricing rule matches with coupon code ([#44104](https://github.com/frappe/erpnext/issues/44104)) ([24b5b3c](24b5b3c8e0))
* correctly set 'cannot_add_rows' property on allocations table field ([137ef78](137ef78d96))
* disable conversion to user tz for sales order calender ([356da69](356da69179))
* Get Entries not showing accounts with no gain or loss in Exchange Rate Revaluation issue ([33e835c](33e835c4d3))
* linters ([c3e2ff2](c3e2ff2fa5))
* non group pos warehouse ([ec40131](ec40131d4d))
* payment reco for jv with negative dr or cr amount ([23fb4f3](23fb4f348f))
* remove trailing whitespace ([1019d98](1019d98c5a))
* set conversion factor before applying price list ([e09f101](e09f101336))
* set default party type in Payment Entry ([f3cbbef](f3cbbef346))
* stock ledger variance report filter options (backport [#44137](https://github.com/frappe/erpnext/issues/44137)) ([#44149](https://github.com/frappe/erpnext/issues/44149)) ([f871f08](f871f08f47))
* validate sales team to ensure all sales person are enabled ([bd0f11e](bd0f11ef4f))

### Features

* new DocTypes "Code List" and "Common Code" (backport [#43425](https://github.com/frappe/erpnext/issues/43425)) ([#44172](https://github.com/frappe/erpnext/issues/44172)) ([a7de8c1](a7de8c1143))
2024-11-20 08:37:41 +00:00
ruthra kumar
fc783f5acf Merge pull request #44210 from frappe/version-14-hotfix
chore: release v14
2024-11-20 14:06:14 +05:30
ruthra kumar
24126b03dd Merge pull request #44244 from frappe/mergify/bp/version-14-hotfix/pr-44197
fix: payment reco for jv with negative dr or cr amount (backport #44197)
2024-11-20 13:12:55 +05:30
ljain112
5fa4fd8825 fix: added test cases
(cherry picked from commit 6f9ea6422d)
2024-11-20 07:18:19 +00:00
ljain112
23fb4f348f fix: payment reco for jv with negative dr or cr amount
(cherry picked from commit fee79b9445)
2024-11-20 07:18:19 +00:00
ruthra kumar
227c912ece Merge pull request #44241 from frappe/mergify/bp/version-14-hotfix/pr-44240
fix: non group pos warehouse (backport #44240)
2024-11-20 12:17:33 +05:30
ruthra kumar
5bfbdb805c Merge pull request #44238 from frappe/mergify/bp/version-14-hotfix/pr-44220
refactor: Update `Payment Request` search query in PE's reference (backport #44220)
2024-11-20 12:16:21 +05:30
Nihantra C. Patel
ec40131d4d fix: non group pos warehouse
(cherry picked from commit d526be0394)
2024-11-20 06:41:34 +00:00
ruthra kumar
859672c419 Merge pull request #44232 from frappe/mergify/bp/version-14-hotfix/pr-44207
fix: validate sales team to ensure all sales person are enabled (backport #44207)
2024-11-20 11:51:56 +05:30
Abdeali Chharchhoda
e8c3617628 refactor: Update Payment Request search query in PE's reference
(cherry picked from commit 4ab3499a17)
2024-11-20 06:21:41 +00:00
ruthra kumar
09b21b8cb4 Merge pull request #44234 from frappe/mergify/bp/version-14-hotfix/pr-44203
fix: disable conversion to user tz for sales order calender (backport #44203)
2024-11-20 11:51:21 +05:30
ljain112
356da69179 fix: disable conversion to user tz for sales order calender
(cherry picked from commit cdf098c193)
2024-11-20 05:17:52 +00:00
ljain112
bd0f11ef4f fix: validate sales team to ensure all sales person are enabled
(cherry picked from commit 548dbb33eb)
2024-11-20 05:11:35 +00:00
ruthra kumar
9f3dfb3d18 Merge pull request #44216 from frappe/mergify/bp/version-14-hotfix/pr-44104
fix: check if pricing rule matches with coupon code (backport #44104)
2024-11-19 17:37:33 +05:30
Nikolas Beckel
24b5b3c8e0 fix: check if pricing rule matches with coupon code (#44104)
* fix: check if pricing rule matches with coupon code

* fix: correct linting error

(cherry picked from commit 9d31bf7647)
2024-11-19 11:41:48 +00:00
mergify[bot]
a7de8c1143 feat: new DocTypes "Code List" and "Common Code" (backport #43425) (#44172)
Co-authored-by: David <dgx.arnold@gmail.com>
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2024-11-18 20:59:41 +01:00
ruthra kumar
2fcab327aa Merge pull request #44181 from frappe/mergify/bp/version-14-hotfix/pr-44127
fix: set default Party Type based on Payment Type in Payment Entry (backport #44127)
2024-11-18 10:49:10 +05:30
vishakhdesai
f3cbbef346 fix: set default party type in Payment Entry
(cherry picked from commit 19222690d3)
2024-11-18 10:16:59 +05:30
ruthra kumar
339beff023 Merge pull request #44179 from frappe/mergify/bp/version-14-hotfix/pr-44147
fix: set conversion factor before applying price list (backport #44147)
2024-11-18 10:14:34 +05:30
ruthra kumar
85d398efcc Merge pull request #44177 from frappe/mergify/bp/version-14-hotfix/pr-44157
fix: apply "cannot_add_rows" directly to table field for more efficient solution (backport #44157)
2024-11-18 10:14:12 +05:30
vishakhdesai
e09f101336 fix: set conversion factor before applying price list
(cherry picked from commit 9749fe23cc)
2024-11-18 04:38:32 +00:00
UmakanthKaspa
f9b52b292e refactor: set 'cannot_add_rows' directly in the allocations table field (optimized approach)
(cherry picked from commit 5dd8eafdfc)
2024-11-18 04:36:12 +00:00
Türker Tunalı
8a94d7bea8 fix:Unable to pause job card (#42839)
fix: unable to pause job card

Pause operation creates "Value missing for: Completed Qty" error. That field was mandatory but it's non mandatory in v15.
2024-11-15 17:29:58 +05:30
mergify[bot]
f871f08f47 fix: stock ledger variance report filter options (backport #44137) (#44149)
fix: stock ledger variance report filter options (#44137)

(cherry picked from commit e8bbf6492f)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-11-15 15:05:03 +05:30
ruthra kumar
313ea3983f Merge pull request #44161 from frappe/mergify/bp/version-14-hotfix/pr-43414
fix: Get Entries not showing accounts with no gain or loss in Exchange Rate Revaluation issue fixed (backport #43414)
2024-11-15 13:09:35 +05:30
Vishakh Desai
c3e2ff2fa5 fix: linters
(cherry picked from commit 9cc22b4cac)
2024-11-15 07:18:49 +00:00
Vishakh Desai
33e835c4d3 fix: Get Entries not showing accounts with no gain or loss in Exchange Rate Revaluation issue
(cherry picked from commit 6de6f55b39)
2024-11-15 07:18:49 +00:00
ruthra kumar
c9fa4af4fe Merge pull request #44159 from frappe/mergify/bp/version-14-hotfix/pr-44158
fix: broken UI on currency exchange (backport #44158)
2024-11-15 12:28:18 +05:30
ruthra kumar
36898f6797 fix: broken UI on currency exchange
(cherry picked from commit e91b65e7bd)
2024-11-15 06:52:40 +00:00
ruthra kumar
b9a0f4fed8 Merge pull request #44154 from frappe/mergify/bp/version-14-hotfix/pr-44089
fix: apply posting date sorting to invoices in Payment Reconciliation similar to payments (backport #44089)
2024-11-15 10:54:40 +05:30
UmakanthKaspa
1019d98c5a fix: remove trailing whitespace
(cherry picked from commit d6703eb88b)
2024-11-15 05:00:03 +00:00
UmakanthKaspa
cc9b22ce9f fix: apply posting date sorting to invoices in Payment Reconciliation similar to payments
(cherry picked from commit 0bd83d920d)
2024-11-15 05:00:02 +00:00
ruthra kumar
f5135cd4a4 Merge pull request #44152 from frappe/mergify/bp/version-14-hotfix/pr-44148
Fix: Disable "Add Row" button in allocations table during UnReconcile process (backport #44148)
2024-11-15 10:22:43 +05:30
UmakanthKaspa
137ef78d96 fix: correctly set 'cannot_add_rows' property on allocations table field
(cherry picked from commit 13ca2700f8)
2024-11-15 04:47:46 +00:00
ruthra kumar
5452f8ac4a Merge pull request #44139 from frappe/mergify/bp/version-14-hotfix/pr-43189
fix: broken apply on other item (backport #43189)
2024-11-14 13:45:41 +05:30
ruthra kumar
f03e301250 fix: broken apply on other item pricing rule
(cherry picked from commit e5119a749c)
2024-11-14 07:54:10 +00:00
Raffael Meyer
c23868a14d fix: backport german translations from develop (#44046) 2024-11-13 18:17:50 +01:00
Frappe PR Bot
75d785f6b6 chore(release): Bumped to Version 14.76.0
# [14.76.0](https://github.com/frappe/erpnext/compare/v14.75.2...v14.76.0) (2024-11-13)

### Bug Fixes

* Drop Shipping address based on customer shopping address ([ea38a1f](ea38a1f36a))
* exception on register reports when filtered on cost center ([599581e](599581e09d))
* NoneType while updating ordered_qty in SO for removed items ([1446aa3](1446aa3636))
* populate payment schedule from payment terms (backport [#44082](https://github.com/frappe/erpnext/issues/44082)) ([#44084](https://github.com/frappe/erpnext/issues/44084)) ([6b149f5](6b149f5ddf))
* subcontracting receipt has no reference field for purchase order ([b1e205f](b1e205f4d1))
* task path (backport [#44073](https://github.com/frappe/erpnext/issues/44073)) ([#44077](https://github.com/frappe/erpnext/issues/44077)) ([c12d5f6](c12d5f613a))
* tyeerror while saving pick list ([d73cc83](d73cc83b91))
* update per_billed value in Purchase Receipt while creating Debit Note ([#43977](https://github.com/frappe/erpnext/issues/43977)) ([da6c6dc](da6c6dcfcb))

### Features

* Partly billed status in Purchase Receipt ([#39543](https://github.com/frappe/erpnext/issues/39543)) ([3e2fa16](3e2fa16d84))
2024-11-13 15:00:34 +00:00
ruthra kumar
a63a0e0d37 Merge pull request #44102 from frappe/version-14-hotfix
chore: release v14
2024-11-13 20:29:06 +05:30
ruthra kumar
ddac2c59ab Merge pull request #44114 from frappe/mergify/bp/version-14-hotfix/pr-43977
fix: update per_billed value in Purchase Receipt while creating Debit Note (backport #43977)
2024-11-13 19:01:04 +05:30
rohitwaghchaure
030df0d7e8 chore: fix conflicts 2024-11-13 18:27:41 +05:30
NaviN
da6c6dcfcb fix: update per_billed value in Purchase Receipt while creating Debit Note (#43977)
* fix: update per_billed value in Purchase Receipt while creating Debit Note

* test: add unit test for validating per_billed value for partial Debit Note

(cherry picked from commit 494fd7ceea)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
2024-11-13 18:27:41 +05:30
ruthra kumar
cbe92fef0c Merge pull request #44128 from frappe/mergify/bp/version-14-hotfix/pr-39543
feat: Partly billed status in Purchase Receipt (backport #39543)
2024-11-13 18:04:39 +05:30
Deepesh Garg
3e2fa16d84 feat: Partly billed status in Purchase Receipt (#39543)
(cherry picked from commit a673220feb)
2024-11-13 12:13:37 +00:00
ruthra kumar
44988c8bfa Merge pull request #44126 from frappe/mergify/copy/version-14-hotfix/pr-44124
refactor: 'Partly Billed' status for Purchase Receipt (copy #44124)
2024-11-13 17:22:41 +05:30
ruthra kumar
2acee05a0a refactor: 'Partly Billed' status for Purchase Receipt
(cherry picked from commit c58bbd25f2)
2024-11-13 17:00:20 +05:30
Nihantra C. Patel
d258684b8d Merge pull request #44119 from frappe/mergify/bp/version-14-hotfix/pr-44116
fix: Drop Shipping address based on customer shopping address (backport #44116)
2024-11-13 15:51:55 +05:30
Nihantra Patel
ea38a1f36a fix: Drop Shipping address based on customer shopping address
(cherry picked from commit c7499f3528)
2024-11-13 10:01:42 +00:00
ruthra kumar
7fb5f17100 Merge pull request #44087 from frappe/mergify/bp/version-14-hotfix/pr-43695
fix: exception on register reports when filtered on cost center (backport #43695)
2024-11-11 17:05:32 +05:30
ruthra kumar
fd9fd8f46f chore: filter report output 2024-11-11 16:42:42 +05:30
ruthra kumar
558fac6843 chore: use FrappeTestCase 2024-11-11 15:07:46 +05:30
ruthra kumar
71a1e50314 refactor(test): assertion refactoring and exact decimals
(cherry picked from commit 1d11131afe)
2024-11-11 09:18:08 +00:00
ruthra kumar
4f7d534cab refactor(test): pass all mandatory fields
(cherry picked from commit c53e9637dd)
2024-11-11 09:18:08 +00:00
ruthra kumar
a8a89f971d refactor(test): fix incorrect assertion
(cherry picked from commit d6030e7112)
2024-11-11 09:18:07 +00:00
ruthra kumar
964ac05e74 test: journals with cost center
(cherry picked from commit c255f34eea)
2024-11-11 09:18:07 +00:00
ruthra kumar
a98fe7a72b test: basic report output
(cherry picked from commit 657201b324)
2024-11-11 09:18:07 +00:00
Vishv-silveroak
599581e09d fix: exception on register reports when filtered on cost center
1

(cherry picked from commit f01e1a8e20)
2024-11-11 09:18:07 +00:00
mergify[bot]
6b149f5ddf fix: populate payment schedule from payment terms (backport #44082) (#44084)
fix: populate payment schedule from payment terms (#44082)

(cherry picked from commit c81eb6c824)

Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com>
2024-11-11 13:12:39 +05:30
mergify[bot]
f2aa71cd6c chore: update CODEOWNERS (backport #44074) (#44080)
* chore: update `CODEOWNERS` (#44074)

(cherry picked from commit 9a758ea826)

# Conflicts:
#	CODEOWNERS

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2024-11-11 13:06:26 +05:30
mergify[bot]
c12d5f613a fix: task path (backport #44073) (#44077)
fix: task path (#44073)

(cherry picked from commit 8c99acb1b9)

Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com>
2024-11-11 13:00:43 +05:30
rohitwaghchaure
3df783bfff Merge pull request #44069 from frappe/mergify/bp/version-14-hotfix/pr-44058
fix: type error while saving pick list (backport #44058)
2024-11-11 12:27:00 +05:30
vishnu
d73cc83b91 fix: tyeerror while saving pick list
(cherry picked from commit 22de0ecbdc)
2024-11-11 06:24:53 +00:00
Frappe PR Bot
e46834095b chore(release): Bumped to Version 14.75.2
## [14.75.2](https://github.com/frappe/erpnext/compare/v14.75.1...v14.75.2) (2024-11-08)

### Bug Fixes

* subcontracting receipt has no reference field for purchase order ([1633a31](1633a3176c))
2024-11-08 07:22:01 +00:00
rohitwaghchaure
2f969478e9 Merge pull request #44032 from frappe/mergify/bp/version-14/pr-44014
fix: Subcontracting Receipt has no reference field for Purchase Order (backport #44014)
2024-11-08 12:49:15 +05:30
Ninad1306
1633a3176c fix: subcontracting receipt has no reference field for purchase order
(cherry picked from commit b1e205f4d1)
2024-11-08 06:53:14 +00:00
ruthra kumar
093b4f9845 Merge pull request #44026 from frappe/mergify/bp/version-14-hotfix/pr-43873
fix: add missing fields to field_no_map array (backport #43873)
2024-11-08 11:52:24 +05:30
Ravindu Nethmina
440ada1bdc refactor: add "margin_type" and "margin_rate_or_amount" to no copy
(cherry picked from commit 70f090c1ec)
2024-11-08 06:00:28 +00:00
ruthra kumar
82cd42bbdf Merge pull request #44023 from frappe/mergify/bp/version-14-hotfix/pr-43762
fix: handle NoneType error when updating ordered_qty in SO for remove… (backport #43762)
2024-11-08 11:11:18 +05:30
bhaveshkumar.j
1446aa3636 fix: NoneType while updating ordered_qty in SO for removed items
(cherry picked from commit 442cdd7ce4)
2024-11-08 05:10:39 +00:00
Sagar Vora
0cdb4cb927 Merge pull request #44014 from Ninad1306/dashboard_links 2024-11-07 18:16:06 +05:30
Frappe PR Bot
ea634ec230 chore(release): Bumped to Version 14.75.1
## [14.75.1](https://github.com/frappe/erpnext/compare/v14.75.0...v14.75.1) (2024-11-07)

### Bug Fixes

* ensure list has items ([5c8af85](5c8af85086))
* error when saving POS merge log ([#43989](https://github.com/frappe/erpnext/issues/43989)) ([657bd37](657bd3712b))
* removed single quotes from deferred revenue ([#43985](https://github.com/frappe/erpnext/issues/43985)) ([aee8a23](aee8a23626))
* serial no ledger report ([#44006](https://github.com/frappe/erpnext/issues/44006)) ([9838eaa](9838eaa332))
* task showing limit in customer portal (backport [#44003](https://github.com/frappe/erpnext/issues/44003)) ([#44004](https://github.com/frappe/erpnext/issues/44004)) ([bc9f0f8](bc9f0f8368))
2024-11-07 12:44:44 +00:00
Sagar Vora
33a9c5e88d Merge pull request #44007 from frappe/version-14-hotfix 2024-11-07 18:13:19 +05:30
Ninad1306
b1e205f4d1 fix: subcontracting receipt has no reference field for purchase order 2024-11-07 16:34:14 +05:30
rohitwaghchaure
9838eaa332 fix: serial no ledger report (#44006) 2024-11-07 13:12:46 +05:30
mergify[bot]
bc9f0f8368 fix: task showing limit in customer portal (backport #44003) (#44004)
fix: task showing limit in customer portal (#44003)

(cherry picked from commit 44832c3b5c)

Co-authored-by: Nihantra C. Patel <141945075+Nihantra-Patel@users.noreply.github.com>
2024-11-07 12:49:42 +05:30
Sagar Vora
478b49cc49 Merge pull request #43994 from frappe/mergify/bp/version-14-hotfix/pr-43993
fix: ensure list has items (backport #43993)
2024-11-06 14:08:21 +05:30
Sagar Vora
5c8af85086 fix: ensure list has items
(cherry picked from commit e13e688987)
2024-11-06 08:37:20 +00:00
Sagar Vora
876af6dca5 Merge pull request #43990 from frappe/mergify/bp/version-14-hotfix/pr-43989
fix: error when saving POS merge log (backport #43989)
2024-11-06 13:26:09 +05:30
Sagar Vora
657bd3712b fix: error when saving POS merge log (#43989)
(cherry picked from commit c62596b323)
2024-11-06 07:52:12 +00:00
Sagar Vora
0a166a813a Merge pull request #43988 from frappe/mergify/bp/version-14-hotfix/pr-43985
fix: removed single quotes from deferred revenue (backport #43985)
2024-11-06 12:08:14 +05:30
Nihantra C. Patel
aee8a23626 fix: removed single quotes from deferred revenue (#43985)
(cherry picked from commit 834d18840c)
2024-11-06 06:37:02 +00:00
Frappe PR Bot
710771edf4 chore(release): Bumped to Version 14.75.0
# [14.75.0](https://github.com/frappe/erpnext/compare/v14.74.8...v14.75.0) (2024-11-06)

### Bug Fixes

* SO link on PO and add in missing dashboard references on both ([4f8a8a5](4f8a8a5e99))

### Features

* remove Payroll Entry from Bank Account dashboard (backport [#43931](https://github.com/frappe/erpnext/issues/43931)) ([#43932](https://github.com/frappe/erpnext/issues/43932)) ([42c2b84](42c2b84b28))

### Performance Improvements

* too many writes error during reposting (backport [#43978](https://github.com/frappe/erpnext/issues/43978)) ([#43982](https://github.com/frappe/erpnext/issues/43982)) ([57d35ec](57d35ec20e))
2024-11-06 05:11:31 +00:00
ruthra kumar
e6b97cad89 Merge pull request #43974 from frappe/version-14-hotfix
chore: release v14
2024-11-06 10:40:10 +05:30
mergify[bot]
57d35ec20e perf: too many writes error during reposting (backport #43978) (#43982)
perf: too many writes error during reposting (#43978)

perf: too many writes error
(cherry picked from commit 134c24b9c5)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-11-06 10:05:36 +05:30
ruthra kumar
1e2bea82f9 Merge pull request #43964 from frappe/mergify/bp/version-14-hotfix/pr-43388
fix: SO link on PO and add in missing dashboard references on both (backport #43388)
2024-11-04 15:34:04 +05:30
ruthra kumar
0f4c5dd26c chore: resolve conflicts
Stock Reservation Entry is unavailable in v14
2024-11-04 14:17:04 +05:30
CaseSolved
7906c21bdf chore: linting
(cherry picked from commit be6970c850)
2024-11-04 08:32:22 +00:00
CaseSolved
4f8a8a5e99 fix: SO link on PO and add in missing dashboard references on both
(cherry picked from commit 2017fd80d1)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py
#	erpnext/selling/doctype/sales_order/sales_order_dashboard.py
2024-11-04 08:32:22 +00:00
mergify[bot]
42c2b84b28 feat: remove Payroll Entry from Bank Account dashboard (backport #43931) (#43932)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2024-10-30 15:49:21 +01:00
Frappe PR Bot
80f2f5cd2f chore(release): Bumped to Version 14.74.8
## [14.74.8](https://github.com/frappe/erpnext/compare/v14.74.7...v14.74.8) (2024-10-30)

### Bug Fixes

* add company filter for project ([d3e29fe](d3e29fe828))
* add parenttype clause to invoice tax query in sales_register report ([cbe704c](cbe704c73c))
* added validation for UOM must be whole number (backport [#43710](https://github.com/frappe/erpnext/issues/43710)) ([#43711](https://github.com/frappe/erpnext/issues/43711)) ([aaae81f](aaae81f7e8))
* basic rate not editable in Stock Entry Detail (backport [#43837](https://github.com/frappe/erpnext/issues/43837)) ([#43839](https://github.com/frappe/erpnext/issues/43839)) ([51c1d03](51c1d032ba))
* calculate tds with net amount when invoice exceeds single threshold amount (backport [#43869](https://github.com/frappe/erpnext/issues/43869)) ([#43919](https://github.com/frappe/erpnext/issues/43919)) ([55c5f30](55c5f30d54))
* do not check for payment terms details for return invoices. ([fd63ace](fd63ace6c7))
* do not set payment terms for return invoices ([bd34a67](bd34a67871))
* get stock accounts from the doc instead of db in validate_stock_accounts ([ad04610](ad04610c94))
* hide payment terms for return and paid purchase invoices ([78db8c0](78db8c0029))
* not able to submit stock entry ([#43890](https://github.com/frappe/erpnext/issues/43890)) ([0742e6e](0742e6e193))
* purchase return validation issue ([#43871](https://github.com/frappe/erpnext/issues/43871)) ([a7cc7b2](a7cc7b28c0))
* remarks field in payment reconciliation ([93243b1](93243b1d95))
* **RFQ:** make strings translatable (backport [#43843](https://github.com/frappe/erpnext/issues/43843)) ([#43847](https://github.com/frappe/erpnext/issues/43847)) ([895f1bf](895f1bff01))
* set bill_no before `against_voucher` gets concatenated ([2f3c3d5](2f3c3d5519))
* work order finish button not showing (backport [#43875](https://github.com/frappe/erpnext/issues/43875)) ([#43877](https://github.com/frappe/erpnext/issues/43877)) ([76530de](76530de786))
2024-10-30 09:52:54 +00:00
ruthra kumar
4e85a79e42 Merge pull request #43892 from frappe/version-14-hotfix
chore: release v14
2024-10-30 15:21:35 +05:30
mergify[bot]
55c5f30d54 fix: calculate tds with net amount when invoice exceeds single threshold amount (backport #43869) (#43919)
* fix: calculate tds with net amount when invoice exceeds single threshold amount

(cherry picked from commit ef694a40a1)

* test: add unit test to validate purchase invoice exceeding single threshold value

(cherry picked from commit 94badb464d)

---------

Co-authored-by: venkat102 <venkatesharunachalam659@gmail.com>
2024-10-30 11:26:08 +05:30
ruthra kumar
cc85f5de26 Merge pull request #43916 from frappe/mergify/bp/version-14-hotfix/pr-43886
fix: remarks field in payment reconciliation (backport #43886)
2024-10-30 10:51:39 +05:30
ruthra kumar
15881ea718 chore: resolve conflicts 2024-10-30 10:30:48 +05:30
Ahmed Shareef
93243b1d95 fix: remarks field in payment reconciliation
(cherry picked from commit 2d5b079949)

# Conflicts:
#	erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json
#	erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.py
2024-10-30 04:50:23 +00:00
mergify[bot]
76530de786 fix: work order finish button not showing (backport #43875) (#43877)
* fix: work order finish button not showing (#43875)

(cherry picked from commit 0a70be5b99)

# Conflicts:
#	erpnext/manufacturing/doctype/job_card/job_card.js
#	erpnext/manufacturing/doctype/job_card/job_card.py

* chore: fix conflicts

* chore: fix conflicts

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-29 22:00:36 +05:30
ruthra kumar
d04396b697 Merge pull request #43896 from frappe/mergify/bp/version-14-hotfix/pr-43856
fix: add parenttype clause to invoice tax query in sales_register report (backport #43856)
2024-10-29 16:12:46 +05:30
ruthra kumar
e552df31d6 Merge pull request #43894 from frappe/mergify/bp/version-14-hotfix/pr-43698
fix: Project name instead of ID in chart (backport #43698)
2024-10-29 16:12:32 +05:30
Imesha Sudasingha
cbe704c73c fix: add parenttype clause to invoice tax query in sales_register report
(cherry picked from commit e30ab141f4)
2024-10-29 10:10:37 +00:00
rohitwaghchaure
0742e6e193 fix: not able to submit stock entry (#43890) 2024-10-29 15:35:19 +05:30
lukas.brandhoff
3fa392ebdf refactor: include 'Project Name' in Project summary report
Keep name field for backwards compatibility

(cherry picked from commit 736d1a1105)
2024-10-29 09:54:26 +00:00
ruthra kumar
a3c2121870 Merge pull request #43863 from frappe/mergify/bp/version-14-hotfix/pr-43685
fix: do not check for payment terms details for return invoices. (backport #43685)
2024-10-28 15:53:06 +05:30
mergify[bot]
aaae81f7e8 fix: added validation for UOM must be whole number (backport #43710) (#43711)
* fix: added validation for UOM must be whole number (#43710)

(cherry picked from commit 4fd4a37dc9)

# Conflicts:
#	erpnext/controllers/accounts_controller.py

* chore: fix conflicts

* chore: fix linters issue

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-28 15:12:16 +05:30
rohitwaghchaure
a7cc7b28c0 fix: purchase return validation issue (#43871) 2024-10-28 15:11:07 +05:30
mergify[bot]
51c1d032ba fix: basic rate not editable in Stock Entry Detail (backport #43837) (#43839)
* fix: basic rate not editable in Stock Entry Detail (#43837)

(cherry picked from commit 5a967bc868)

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

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-28 14:02:23 +05:30
ruthra kumar
c695959902 chore: resolve conflicts 2024-10-28 13:32:30 +05:30
ruthra kumar
83f5a24520 Merge pull request #43868 from frappe/mergify/bp/version-14-hotfix/pr-43862
refactor: fix garbage title, desc and name in razorpay (backport #43862)
2024-10-28 12:47:42 +05:30
ruthra kumar
627832fe5b refactor: fix garbage title, desc and name in razorpay
(cherry picked from commit 7332ec8fbd)
2024-10-28 06:55:08 +00:00
ljain112
78db8c0029 fix: hide payment terms for return and paid purchase invoices
(cherry picked from commit 912e1e3f3d)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
2024-10-28 05:53:55 +00:00
ljain112
bd34a67871 fix: do not set payment terms for return invoices
(cherry picked from commit 8b700eadc7)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.js
2024-10-28 05:53:54 +00:00
ljain112
fd63ace6c7 fix: do not check for payment terms details for return invoices.
(cherry picked from commit 6703b7d1ae)
2024-10-28 05:53:54 +00:00
ruthra kumar
36fdd540c0 Merge pull request #43858 from frappe/mergify/bp/version-14-hotfix/pr-43833
refactor: query for expense_account moved to setup hook in purchase invoice (backport #43833)
2024-10-28 09:59:55 +05:30
ruthra kumar
b81dc2bf5e chore: resolve conflict 2024-10-28 09:55:04 +05:30
ljain112
7e7cc807a5 refactor: query for expense_account moved to setup hook in purchase invoice
(cherry picked from commit a9ac0cc223)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
2024-10-28 04:14:14 +00:00
mergify[bot]
895f1bff01 fix(RFQ): make strings translatable (backport #43843) (#43847)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix(RFQ): make strings translatable (#43843)
2024-10-26 19:51:31 +02:00
ruthra kumar
53c4b5f752 Merge pull request #43832 from frappe/mergify/bp/version-14-hotfix/pr-43830
fix: set bill_no before `against_voucher` gets concatenated (backport #43830)
2024-10-25 14:28:44 +05:30
ruthra kumar
2f3c3d5519 fix: set bill_no before against_voucher gets concatenated
(cherry picked from commit 7bade7f1fe)
2024-10-25 14:05:15 +05:30
ruthra kumar
6ddff1b168 Merge pull request #43806 from frappe/mergify/bp/version-14-hotfix/pr-43755
fix: Add Company Filter (backport #43755)
2024-10-23 17:57:42 +05:30
IamSaiyyamChhetri
d3e29fe828 fix: add company filter for project
- In Project dt Sales Order field
- In Sales Order dt Project field

(cherry picked from commit 9909d760a5)
2024-10-23 17:31:44 +05:30
Smit Vora
3ace876c1f Merge pull request #43784 from frappe/mergify/bp/version-14-hotfix/pr-43462
fix: get stock accounts from the doc in `validate_stock_accounts` in Journal Entry (backport #43462)
2024-10-23 10:36:30 +05:30
Frappe PR Bot
8c7ed37afd chore(release): Bumped to Version 14.74.7
## [14.74.7](https://github.com/frappe/erpnext/compare/v14.74.6...v14.74.7) (2024-10-23)

### Bug Fixes

* "show_remarks" checkbox in Process statement of accounts ([0833823](083382399f))
* coupon code validation logic ([c18beae](c18beaea6b))
* **deferred_revenue:** Escape account in query ([3d082c9](3d082c93bc))
* Freeze Screen on load invoices on POS Closing Entry ([725b6be](725b6beff3))
* get period estimate till service end date ([35c257a](35c257a7ae))
* incorrect amount in bank clearance ([98362d9](98362d9183))
* party_balance based on company in payment entry ([5333f33](5333f33c21))
* remove extra space ([719565c](719565c897))
* use correct variable in error message (backport [#43790](https://github.com/frappe/erpnext/issues/43790)) ([#43791](https://github.com/frappe/erpnext/issues/43791)) ([5b0a8fd](5b0a8fd2ce))
2024-10-23 04:45:56 +00:00
ruthra kumar
9da08df3ca Merge pull request #43773 from frappe/version-14-hotfix
chore: release v14
2024-10-23 10:14:40 +05:30
mergify[bot]
5b0a8fd2ce fix: use correct variable in error message (backport #43790) (#43791)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: use correct variable in error message (#43790)
2024-10-22 19:43:13 +02:00
Sagar Vora
f5f6c7609a test: fix test case 2024-10-22 20:27:06 +05:30
Vishakh Desai
ad04610c94 fix: get stock accounts from the doc instead of db in validate_stock_accounts
(cherry picked from commit 30954586d8)
2024-10-22 14:07:06 +00:00
ruthra kumar
1a4a329c1d Merge pull request #43779 from frappe/mergify/bp/version-14-hotfix/pr-43778
refactor: validate_return_against_account (backport #43778)
2024-10-22 17:36:37 +05:30
Raffael Meyer
411f7c3a2c refactor: validate_return_against_account (#43778)
(cherry picked from commit c4faa0e101)
2024-10-22 11:45:49 +00:00
ruthra kumar
c55d473c92 Merge pull request #43776 from frappe/mergify/bp/version-14-hotfix/pr-43775
fix(deferred_revenue): Escape account in query (backport #43775)
2024-10-22 16:40:02 +05:30
Corentin Forler
3d082c93bc fix(deferred_revenue): Escape account in query
(cherry picked from commit c7b3ae41d4)
2024-10-22 10:42:08 +00:00
ruthra kumar
a3e8cc6abe Merge pull request #43767 from frappe/mergify/bp/version-14-hotfix/pr-43766
refactor: allow unreconcile on bank and cash entry type journals (backport #43766)
2024-10-22 12:12:52 +05:30
ruthra kumar
0dcb5a4fbb refactor: allow unreconcile on bank and cash entry type journals
(cherry picked from commit 2c4f37f488)
2024-10-22 06:35:37 +00:00
ruthra kumar
14a589eb2f Merge pull request #43764 from frappe/mergify/bp/version-14-hotfix/pr-43761
fix: coupon code validation logic (backport #43761)
2024-10-22 10:39:10 +05:30
ruthra kumar
2af36383cc chore: resolve conflict 2024-10-22 10:16:57 +05:30
bhaveshkumar.j
719565c897 fix: remove extra space
(cherry picked from commit 1561a9e1bf)
2024-10-22 04:37:48 +00:00
bhaveshkumar.j
c18beaea6b fix: coupon code validation logic
(cherry picked from commit d04257a32d)

# Conflicts:
#	erpnext/accounts/doctype/pricing_rule/utils.py
2024-10-22 04:37:48 +00:00
ruthra kumar
3d5c15df0a Merge pull request #43741 from frappe/mergify/bp/version-14-hotfix/pr-43726
fix: "show_remarks" checkbox in Process statement of accounts (backport #43726)
2024-10-21 13:09:04 +05:30
ruthra kumar
0cf172c0da Merge pull request #43743 from frappe/mergify/bp/version-14-hotfix/pr-43727
fix: get period estimate till service end date (backport #43727)
2024-10-21 13:08:44 +05:30
ruthra kumar
5289022351 Merge pull request #43745 from frappe/mergify/bp/version-14-hotfix/pr-43720
fix: party_balance based on company in payment entry (backport #43720)
2024-10-21 13:08:22 +05:30
ruthra kumar
bbce58524e Merge pull request #43747 from frappe/mergify/bp/version-14-hotfix/pr-42461
fix: Freeze Screen on load invoices on POS Closing Entry (backport #42461)
2024-10-21 13:08:02 +05:30
HarryPaulo
725b6beff3 fix: Freeze Screen on load invoices on POS Closing Entry
(cherry picked from commit 486d396174)
2024-10-21 06:57:51 +00:00
ljain112
5333f33c21 fix: party_balance based on company in payment entry
(cherry picked from commit 97c9adf06b)
2024-10-21 06:53:31 +00:00
ruthra kumar
beb5318446 chore: resolve conflict 2024-10-21 12:21:51 +05:30
venkat102
35c257a7ae fix: get period estimate till service end date
(cherry picked from commit a7ba7e9c28)
2024-10-21 06:50:26 +00:00
ljain112
083382399f fix: "show_remarks" checkbox in Process statement of accounts
(cherry picked from commit f4600df1f7)

# Conflicts:
#	erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
2024-10-21 06:44:35 +00:00
ruthra kumar
a05d6de2be Merge pull request #43716 from frappe/mergify/bp/version-14-hotfix/pr-42898
fix: incorrect amount in bank clearance (backport #42898)
2024-10-18 11:01:11 +05:30
NIYAZ RAZAK
98362d9183 fix: incorrect amount in bank clearance
(cherry picked from commit 9a11df59fc)
2024-10-18 10:37:17 +05:30
Frappe PR Bot
e401ad5b8f chore(release): Bumped to Version 14.74.6
## [14.74.6](https://github.com/frappe/erpnext/compare/v14.74.5...v14.74.6) (2024-10-16)

### Bug Fixes

* added parentheses for correct query formation for logical OR condition ([0f738de](0f738de5e5))
* added string for translation in bank reconciliation statement ([32f6eb7](32f6eb7ee7))
* conversion factor issue (backport [#43645](https://github.com/frappe/erpnext/issues/43645)) ([#43675](https://github.com/frappe/erpnext/issues/43675)) ([1262c32](1262c322d1))
* delete invalid pricing rule on change of applicable_for ([c7c2bdd](c7c2bdd159))
* ignore free item when qty is zero ([ae47fb9](ae47fb9617))
* missing child company accounts in consolidated balance sheet ([bb774ce](bb774cec58))
* removed unused query ([dcbfd4a](dcbfd4ae1d))
* run gl_entries and closing voucher processes in same function ([59c314c](59c314c98a))
* update formatings ([a8ad608](a8ad608fd8))
* update formatings ([699b098](699b09846a))
* update item details with actual quantity. ([c520a7c](c520a7c6c1))
* Use `ref_doc.get()` for `party_account_currency` ([01d6c10](01d6c1081b))
* validation for corrective job card (backport [#43555](https://github.com/frappe/erpnext/issues/43555)) (backport [#43558](https://github.com/frappe/erpnext/issues/43558)) ([#43628](https://github.com/frappe/erpnext/issues/43628)) ([1f09975](1f099752d2))
* zero incoming rate for delivery note return ([#43642](https://github.com/frappe/erpnext/issues/43642)) ([7eac9cc](7eac9cc1db))
2024-10-16 05:02:55 +00:00
ruthra kumar
9106c2fdd8 Merge pull request #43668 from frappe/version-14-hotfix
chore: release v14
2024-10-16 10:31:35 +05:30
ruthra kumar
2e6ead30e0 Merge pull request #43683 from frappe/mergify/bp/version-14-hotfix/pr-43661
fix: missing child company accounts in consolidated balance sheet (backport #43661)
2024-10-15 17:38:11 +05:30
ruthra kumar
70050081ff Merge pull request #43681 from frappe/mergify/bp/version-14-hotfix/pr-43663
fix: run gl_entries and closing voucher processes in same function (backport #43663)
2024-10-15 17:28:04 +05:30
ljain112
bb774cec58 fix: missing child company accounts in consolidated balance sheet
(cherry picked from commit 7fae9d57d2)
2024-10-15 11:37:52 +00:00
ruthra kumar
7c549e60d6 Merge pull request #43679 from frappe/mergify/bp/version-14-hotfix/pr-43602
fix: added parentheses for correct query formation for logical OR condition (backport #43602)
2024-10-15 17:07:17 +05:30
ljain112
59c314c98a fix: run gl_entries and closing voucher processes in same function
(cherry picked from commit af4daa5b0f)
2024-10-15 11:35:55 +00:00
ljain112
0f738de5e5 fix: added parentheses for correct query formation for logical OR condition
(cherry picked from commit c0da8f11f7)
2024-10-15 11:15:40 +00:00
ruthra kumar
8701cb73ef Merge pull request #43677 from frappe/mergify/bp/version-14-hotfix/pr-43600
fix: added string for translation in bank reconciliation statement (backport #43600)
2024-10-15 16:40:53 +05:30
ljain112
32f6eb7ee7 fix: added string for translation in bank reconciliation statement
(cherry picked from commit c99d9f7037)
2024-10-15 11:02:29 +00:00
mergify[bot]
1262c322d1 fix: conversion factor issue (backport #43645) (#43675)
fix: conversion factor issue (#43645)

(cherry picked from commit a52756f1d4)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-10-15 16:08:37 +05:30
ruthra kumar
aa85c3abee Merge pull request #43669 from frappe/mergify/bp/version-14-hotfix/pr-43557
fix: delete invalid pricing rule on change of applicable_for values (backport #43557)
2024-10-15 15:56:29 +05:30
ljain112
5d9474c98c test: added test for change in applicable_for_value in promotional scheme
(cherry picked from commit 2613bdd868)
2024-10-15 09:55:31 +00:00
ljain112
c7c2bdd159 fix: delete invalid pricing rule on change of applicable_for
(cherry picked from commit 42746fc630)
2024-10-15 09:55:31 +00:00
ruthra kumar
1913caf2b0 Merge pull request #43665 from frappe/mergify/bp/version-14-hotfix/pr-43662
fix: removed unused query (backport #43662)
2024-10-15 13:55:52 +05:30
ruthra kumar
5aff4242e3 Merge pull request #43657 from frappe/mergify/bp/version-14-hotfix/pr-43570
fix: update item details with actual quantity (backport #43570)
2024-10-15 13:52:23 +05:30
ruthra kumar
e7f630fc74 chore: resolve conflict 2024-10-15 13:34:37 +05:30
ruthra kumar
9697cb066c Merge pull request #43659 from frappe/mergify/bp/version-14-hotfix/pr-43642
fix: zero incoming rate for delivery note return (backport #43642)
2024-10-15 13:26:03 +05:30
ruthra kumar
be78e17e8c chore: resolve conflict 2024-10-15 13:25:14 +05:30
ljain112
dcbfd4ae1d fix: removed unused query
(cherry picked from commit 5f590ddfa2)

# Conflicts:
#	erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
2024-10-15 07:53:40 +00:00
rohitwaghchaure
a585a7029f chore: fix conflicts 2024-10-15 12:08:24 +05:30
rohitwaghchaure
7eac9cc1db fix: zero incoming rate for delivery note return (#43642)
(cherry picked from commit 6087a57b0c)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/test_delivery_note.py
2024-10-15 06:28:36 +00:00
Bhavan23
a8ad608fd8 fix: update formatings
(cherry picked from commit 5044297321)
2024-10-15 06:27:42 +00:00
Bhavan23
699b09846a fix: update formatings
(cherry picked from commit 5f4a523340)

# Conflicts:
#	erpnext/selling/doctype/sales_order/sales_order.py
2024-10-15 06:27:41 +00:00
Bhavan23
dc2483bb92 test: Validate the actual quantity when creating a material request from the sales order
(cherry picked from commit 17fdd42645)
2024-10-15 06:27:41 +00:00
Bhavan23
c520a7c6c1 fix: update item details with actual quantity.
(cherry picked from commit 9dbdfec9b7)
2024-10-15 06:27:40 +00:00
ruthra kumar
475c9b8450 Merge pull request #43653 from frappe/mergify/bp/version-14-hotfix/pr-43601
refactor: remove 'format:' based naming (backport #43601)
2024-10-15 07:34:49 +05:30
ruthra kumar
2cce2cfdbe Merge pull request #43649 from frappe/mergify/bp/version-14-hotfix/pr-43614
fix: ignore free item when qty is zero (backport #43614)
2024-10-15 06:01:44 +05:30
ruthra kumar
065465626e chore: resolve conflict 2024-10-15 06:00:47 +05:30
venkat102
fc7207bcd3 refactor: remove 'format:' based naming
(cherry picked from commit e8e1ec0e85)

# Conflicts:
#	erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.json
2024-10-14 15:45:57 +00:00
Ninad1306
3b1f0c191e test: test case to validate free item is ignored when qty is zero
(cherry picked from commit a2b41a0c16)
2024-10-14 10:51:00 +00:00
Ninad1306
ae47fb9617 fix: ignore free item when qty is zero
(cherry picked from commit 7ae98f77ee)
2024-10-14 10:51:00 +00:00
Smit Vora
16d9ca72e8 Merge pull request #43640 from frappe/mergify/bp/version-14-hotfix/pr-43638
fix: Use `ref_doc.get()` for `party_account_currency` (backport #43638)
2024-10-14 13:59:05 +05:30
Abdeali Chharchhoda
01d6c1081b fix: Use ref_doc.get() for party_account_currency
(cherry picked from commit b79549422a)
2024-10-14 06:09:08 +00:00
Frappe PR Bot
7404911940 chore(release): Bumped to Version 14.74.5
## [14.74.5](https://github.com/frappe/erpnext/compare/v14.74.4...v14.74.5) (2024-10-12)

### Bug Fixes

* validation for corrective job card (backport [#43555](https://github.com/frappe/erpnext/issues/43555)) (backport [#43558](https://github.com/frappe/erpnext/issues/43558)) ([#43628](https://github.com/frappe/erpnext/issues/43628)) ([ea0f044](ea0f044c48))
2024-10-12 15:51:49 +00:00
rohitwaghchaure
136adcb793 Merge pull request #43629 from frappe/mergify/bp/version-14/pr-43628
fix: validation for corrective job card (backport #43555) (backport #43558) (backport #43628)
2024-10-12 21:20:29 +05:30
mergify[bot]
ea0f044c48 fix: validation for corrective job card (backport #43555) (backport #43558) (#43628)
fix: validation for corrective job card (backport #43555) (#43558)

* fix: validation for corrective job card (#43555)

(cherry picked from commit 7a0a893d08)

# Conflicts:
#	erpnext/manufacturing/doctype/job_card/job_card.py

* chore: fix conflicts

* chore: fix linters issue

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
(cherry picked from commit cf0fa0db7b)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
(cherry picked from commit 1f099752d2)
2024-10-12 13:04:35 +00:00
mergify[bot]
1f099752d2 fix: validation for corrective job card (backport #43555) (backport #43558) (#43628)
fix: validation for corrective job card (backport #43555) (#43558)

* fix: validation for corrective job card (#43555)

(cherry picked from commit 7a0a893d08)

# Conflicts:
#	erpnext/manufacturing/doctype/job_card/job_card.py

* chore: fix conflicts

* chore: fix linters issue

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
(cherry picked from commit cf0fa0db7b)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2024-10-12 18:32:30 +05:30
Frappe PR Bot
e9dbbbe324 chore(release): Bumped to Version 14.74.4
## [14.74.4](https://github.com/frappe/erpnext/compare/v14.74.3...v14.74.4) (2024-10-10)

### Bug Fixes

* add parenttype condition for item table in Purchase Register Report ([4586f03](4586f03bdc))
* Add removed test code `b41f10c1b9` ([8f5c191](8f5c191136))
* create Account Closing Balance even though there are no transaction in period ([2fc776d](2fc776d0f5))
* deduct advances adjusted for threshold check for tcs ([de15728](de15728741))
* do not include advances for tds vouchers ([9d950a9](9d950a9b77))
* get company from ref doc as it's not available in payment request ([078eb46](078eb46eb8))
* multiple issues in Payment Request ([#42427](https://github.com/frappe/erpnext/issues/42427)) ([ef52be2](ef52be2f17))
* Remove `advance_payment_status` uses ([36b4f68](36b4f68566))
* Remove unreference method ([d4421da](d4421dade1))
* Remove unused  field ([3a51cf4](3a51cf4e1a))
* Remove unused function `get_paid_amount_against_order` ([677aadd](677aadde7c))
* Separate `on_submit` and `before_submit` of PR ([d828ea6](d828ea6a1a))
2024-10-10 00:48:02 +00:00
ruthra kumar
4b1e3c96c2 Merge pull request #43562 from frappe/version-14-hotfix
chore: release v14
2024-10-10 06:16:42 +05:30
Sagar Vora
8714ec608b Merge pull request #43571 from frappe/mergify/bp/version-14-hotfix/pr-43271
fix: deduct advances adjusted for threshold check for tcs (backport #43271)
2024-10-09 18:38:07 +05:30
Sagar Vora
e7518414cb Merge pull request #43595 from ljain112/fix-pr-43271 2024-10-09 17:44:37 +05:30
ljain112
d5c82ccbab test: fixed account settings in test case 2024-10-09 17:34:59 +05:30
ljain112
f5a7c8b0b0 test: added test cases for the tcs deduction for advances adjusted.
(cherry picked from commit efe238cefd)
2024-10-08 12:05:48 +00:00
ljain112
de15728741 fix: deduct advances adjusted for threshold check for tcs
(cherry picked from commit 767c8f92be)
2024-10-08 12:05:48 +00:00
Smit Vora
d6f9909ba5 Merge pull request #43552 from frappe/mergify/bp/version-14-hotfix/pr-43397
fix: do not include advances for tds vouchers (backport #43397)
2024-10-08 11:29:48 +05:30
ljain112
9d950a9b77 fix: do not include advances for tds vouchers
(cherry picked from commit 7ef918421e)
2024-10-08 05:09:01 +00:00
Smit Vora
0336e7ac45 Merge pull request #43529 from frappe/mergify/bp/version-14-hotfix/pr-43407
fix: multiple issues in Payment Request (backport #43407)
2024-10-08 10:38:50 +05:30
Smit Vora
078eb46eb8 fix: get company from ref doc as it's not available in payment request 2024-10-07 22:55:58 +05:30
Smit Vora
1d21f428c1 Merge pull request #43526 from frappe/mergify/bp/version-14-hotfix/pr-43391
fix: create Account Closing Balance even though there are no transaction in period (backport #43391)
2024-10-07 22:34:37 +05:30
Smit Vora
71ee88dc79 Merge pull request #43524 from frappe/mergify/bp/version-14-hotfix/pr-43384
fix: add parenttype condition for item table in Purchase Register Report (backport #43384)
2024-10-07 22:34:23 +05:30
Smit Vora
f6b91969b2 chore: resolve conflicts, remove additional new fields 2024-10-07 22:31:49 +05:30
Smit Vora
3f5f37fa73 chore: resolve conflicts visible prima-facie 2024-10-07 22:15:26 +05:30
Smit Vora
d4258fff9d chore: remove unused filed
(cherry picked from commit 0c599c2b6d)
2024-10-07 16:17:06 +00:00
Abdeali Chharchhoda
54608bdff9 test: Change Accounts Settings for multi currency (https://github.com/frappe/erpnext/pull/42427#discussion_r1789859737)
(cherry picked from commit 62cc86114b)
2024-10-07 16:17:05 +00:00
Abdeali Chharchhoda
4d00d2b87b test: Remove Payment Gateway settings from test
(cherry picked from commit 3d9d56ab50)
2024-10-07 16:17:05 +00:00
Abdeali Chharchhoda
d828ea6a1a fix: Separate on_submit and before_submit of PR
(cherry picked from commit dbd7b83204)
2024-10-07 16:17:05 +00:00
Abdeali Chharchhoda
677aadde7c fix: Remove unused function get_paid_amount_against_order
(cherry picked from commit 75916629c8)
2024-10-07 16:17:04 +00:00
Abdeali Chharchhoda
3a51cf4e1a fix: Remove unused field
(cherry picked from commit e785928c0f)

# Conflicts:
#	erpnext/accounts/doctype/payment_request/payment_request.json
2024-10-07 16:17:04 +00:00
Abdeali Chharchhoda
7d0a094515 test: Removed initial PR status assertion
(cherry picked from commit 67bd540135)
2024-10-07 16:17:03 +00:00
Abdeali Chharchhoda
8f5c191136 fix: Add removed test code b41f10c1b9
(cherry picked from commit 30fd11f138)
2024-10-07 16:17:02 +00:00
Abdeali Chharchhoda
d4421dade1 fix: Remove unreference method
(cherry picked from commit 770bc1c293)
2024-10-07 16:17:02 +00:00
Abdeali Chharchhoda
36b4f68566 fix: Remove advance_payment_status uses
(cherry picked from commit 907e3af1b0)
2024-10-07 16:17:02 +00:00
Abdeali Chharchhodawala
ef52be2f17 fix: multiple issues in Payment Request (#42427)
* fix: multiple issues in Payment Request

* chore: minor changes

* fix: remove  bug

* fix: replace `round` with `flt`

* fix: update `set_advance_payment_status()` logic

* fix: removed bug of `set_advance_payment_status`

* fix: changes as per review

* refactor: replace sql query of `matched_payment_requests` to query builder

* fix: replace `locals` with `get_doc` in set_query

* fix: changes during review

* fix: minor review changes

* fix: remove unnecessary code for setting payment entry received amount

* fix: logic for ser payment_request if PE made from transaction

* fix: Use rounded total to make Payment Request from `Sales Invoice` or `Purchase Invoice`

* refactor: enhance logic of `set_open_payment_requests_to_references`

* fix: added one optional arg `created_from_payment_request`

* fix: handle multiple allocation of PR at PE's reference

* fix: logic for PR if outstanding docs fetch

* fix: formatted Link field for `Payment Request` for PE's references

* fix: replace `get_all()` with `get_list()` for getting Payment Request for Link field

* fix: replace `get_all()` with `get_list()` for getting Payment Request for Link field

* chore: format `payment_entry.js` file

* style: Show preview popup of `Payment Request`

* fix: remove minor bug

* fix: add virtual field for Payment Term and Request `outstanding_amount` in PE's reference

* fix: get outstanding amount in PE's reference on realtime

* fix: move allocation of allocated_amount to server side (no change)

* fix: some minor changes to allocation

* fix: Split `Payment Request` if PE is created from PR and there are `Payment Terms`

* fix: minor logic changes

* fix: Allocation of allocated_amount if `paid_amount` is changes

* fix: improve logic of allocation

* fix: set matched payment request if unset

* fix: minor changes

* fix: Allocate single Payment Request if PE created from PR

* fix: improve code logic

* fix: Removed duplication code

* fix: proper message title

* refactor: Rename method of Allocation Amount to References

* refactor: Changing `grand_total` description based on `party_type`

* refactor: update Payment Request

* fix: Remove virtual property of payment_term_oustanding from references

* fix: fetch party account currency for creating payment request

* fix: use transaction currency as base in payment request

* fix: party amount for creating payment entry

* fix: allow for proportional amount paid by bank

* fix: Changed field order in Payment Request

* fix: Minor refactor in Payment Entry Reference table data

* test: Added test cases for allow Payment at `Partially Paid` status for PR

* test: Update partial paid status test case

* test: Update test case for same currency PR

* refactor: Wider the `msgprint` dialog for after save PE

* test: Update PR test cases

* chore: Remove dirty lines

* test: Checking `Advance Payment Status`

* fix: formatting update

* fix: Use `flt` where doing subtraction

* test: PR test case with Payment Term for same currency

* fix: remove redundant `flt`

* test: Add test cases for PR

---------

Co-authored-by: Sagar Vora <sagar@resilient.tech>
(cherry picked from commit ea69ba7cd8)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/payment_entry.js
#	erpnext/accounts/doctype/payment_entry/payment_entry.py
#	erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
#	erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.py
#	erpnext/accounts/doctype/payment_request/payment_request.json
#	erpnext/accounts/doctype/payment_request/payment_request.py
2024-10-07 16:17:01 +00:00
ljain112
2fc776d0f5 fix: create Account Closing Balance even though there are no transaction in period
(cherry picked from commit 43deaea96b)
2024-10-07 16:12:19 +00:00
ljain112
4586f03bdc fix: add parenttype condition for item table in Purchase Register Report
(cherry picked from commit 28abf191fc)
2024-10-07 15:55:42 +00:00
Frappe PR Bot
96cb08dba8 chore(release): Bumped to Version 14.74.3
## [14.74.3](https://github.com/frappe/erpnext/compare/v14.74.2...v14.74.3) (2024-10-04)

### Bug Fixes

* add company filter in Warehouse wise Item Balance Age and Value ([3cb8502](3cb85028d0))
* adjustmen entry for stock reco ([57925a1](57925a1315))
* conflicts ([02ce528](02ce528501))
* **Dunning:** logic for fetching text (backport [#43160](https://github.com/frappe/erpnext/issues/43160)) ([#43489](https://github.com/frappe/erpnext/issues/43489)) ([31ecdb2](31ecdb2104))
* Fix API endpoint for Frankfurter ([1c0843c](1c0843caee))
* patch to update Currency Exchange Settings for `frankfurter.app` ([b4706db](b4706db816))
* set margin fields for purchase documents when updating items ([97edadf](97edadfc4b))
* Stock Ledger Invariant Check report ([4426384](44263842f5))
2024-10-04 03:04:09 +00:00
ruthra kumar
b18ad94071 Merge pull request #43468 from frappe/version-14-hotfix
chore: release v14
2024-10-04 08:32:45 +05:30
mergify[bot]
31ecdb2104 fix(Dunning): logic for fetching text (backport #43160) (#43489)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix(Dunning): logic for fetching text (#43160)
2024-10-03 20:28:59 +01:00
Sagar Vora
3b550ca419 Merge pull request #43484 from frappe/mergify/bp/version-14-hotfix/pr-43475
fix: set margin fields for purchase documents when updating items (backport #43475)
2024-10-02 15:24:03 +05:30
Sagar Vora
97edadfc4b fix: set margin fields for purchase documents when updating items
(cherry picked from commit 7be4d56be2)
2024-10-02 09:51:45 +00:00
Sagar Vora
824ccfb10c Merge pull request #43482 from frappe/mergify/bp/version-14-hotfix/pr-43481
fix: patch to update Currency Exchange Settings for `frankfurter.app` (backport #43481)
2024-10-02 15:14:46 +05:30
Sagar Vora
02ce528501 fix: conflicts 2024-10-02 15:14:16 +05:30
Sagar Vora
b4706db816 fix: patch to update Currency Exchange Settings for frankfurter.app
(cherry picked from commit c5889c5d2e)

# Conflicts:
#	erpnext/patches.txt
2024-10-02 09:43:14 +00:00
Sagar Vora
f608ba55ab Merge pull request #43477 from frappe/mergify/bp/version-14-hotfix/pr-43476
fix: Fix API endpoint for Frankfurter (backport #43476)
2024-10-02 14:43:07 +05:30
Sagar Vora
b18b4c1e35 test: update test for API change
(cherry picked from commit c444de017a)
2024-10-02 09:11:47 +00:00
Corentin Forler
1c0843caee fix: Fix API endpoint for Frankfurter
(cherry picked from commit 33e72111c7)
2024-10-02 09:11:47 +00:00
Nihantra C. Patel
7b158accec Merge pull request #43458 from frappe/mergify/bp/version-14-hotfix/pr-43455
fix: add company filter in Warehouse wise Item Balance Age and Value (backport #43455)
2024-10-01 11:46:54 +05:30
Nihantra Patel
3cb85028d0 fix: add company filter in Warehouse wise Item Balance Age and Value
(cherry picked from commit 75950f86cf)
2024-10-01 05:47:33 +00:00
rohitwaghchaure
619550942f Merge pull request #43439 from frappe/mergify/bp/version-14-hotfix/pr-43437
fix: adjustment entry for stock reco (backport #43437)
2024-09-30 15:38:09 +05:30
Rohit Waghchaure
57925a1315 fix: adjustmen entry for stock reco
(cherry picked from commit 4e463b7d6d)
2024-09-30 08:57:50 +00:00
rohitwaghchaure
a201c56b38 Merge pull request #43432 from frappe/mergify/bp/version-14-hotfix/pr-43420
fix: Stock Ledger Invariant Check report (backport #43420)
2024-09-30 13:39:47 +05:30
Rohit Waghchaure
44263842f5 fix: Stock Ledger Invariant Check report
(cherry picked from commit d7daedc5b2)
2024-09-29 17:07:35 +00:00
Frappe PR Bot
20f9549247 chore(release): Bumped to Version 14.74.2
## [14.74.2](https://github.com/frappe/erpnext/compare/v14.74.1...v14.74.2) (2024-09-25)

### Bug Fixes

* add currency in financial statement ([6becaba](6becaba672))
* AR / AP report to ignore 0.0 outstanding ([79f95ba](79f95ba507))
* **Bank Account:** dashboard connections (backport [#43365](https://github.com/frappe/erpnext/issues/43365)) ([#43366](https://github.com/frappe/erpnext/issues/43366)) ([4146bdf](4146bdf324))
* change dynamic link doctype fieldtype to data ([c0dfda6](c0dfda6ccc))
* incorrect outstanding on non-pos invoice with write_off_account ([79235cd](79235cd688))
* incorrect stock balance for inventory dimension (backport [#43284](https://github.com/frappe/erpnext/issues/43284)) ([#43289](https://github.com/frappe/erpnext/issues/43289)) ([1bdbcc6](1bdbcc6703))
* partial return on POS invoice ([50e0f56](50e0f56d18))
* partial return on POS invoice ([ef7d4fe](ef7d4fe84a))
* Payment Ledger Report currency fieldtype fix ([7ac39dc](7ac39dc233))
* **Payment Reconciliation:** German translations ([32d4f96](32d4f96e02))
* Serial No reserved against historical sales order - unable to sell again ([#43294](https://github.com/frappe/erpnext/issues/43294)) ([8abc1f4](8abc1f4da9))
* show chart tool tip in report currency ([92438f8](92438f8dba))
* translate in js ([e43c357](e43c357c08))
* Translation for button SO to PO ([a8b5bcf](a8b5bcfe02))
* Translation for button SO to PO --conflicts ([b6ce922](b6ce9222aa))
2024-09-25 04:38:46 +00:00
ruthra kumar
644ada8225 Merge pull request #43362 from frappe/version-14-hotfix
chore: release v14
2024-09-25 10:07:32 +05:30
mergify[bot]
4146bdf324 fix(Bank Account): dashboard connections (backport #43365) (#43366)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
fix(Bank Account): dashboard connections (#43365)
2024-09-24 16:20:05 +02:00
ruthra kumar
c36b40ad8c Merge pull request #43363 from frappe/mergify/bp/version-14-hotfix/pr-43356
fix: AR / AP report to ignore 0.0 outstanding (backport #43356)
2024-09-24 16:58:26 +05:30
ruthra kumar
79f95ba507 fix: AR / AP report to ignore 0.0 outstanding
(cherry picked from commit 6e2cf79e2c)
2024-09-24 10:14:49 +00:00
Nihantra C. Patel
5ba9fb73bb Merge pull request #43342 from frappe/mergify/bp/version-14-hotfix/pr-43253
fix: partial return on POS invoice (backport #43253)
2024-09-23 23:36:25 +05:30
ruthra kumar
43ba79f0e5 Merge pull request #43299 from barredterra/de-payment-reco
fix(Payment Reconciliation): German translations
2024-09-23 20:03:02 +05:30
ruthra kumar
5ce4dae204 chore: resolve conflict 2024-09-23 20:02:10 +05:30
Nihantra C. Patel
50e0f56d18 fix: partial return on POS invoice
(cherry picked from commit 18bdd06652)

# Conflicts:
#	erpnext/accounts/doctype/pos_invoice/pos_invoice.py
2024-09-23 14:26:05 +00:00
Nihantra C. Patel
ef7d4fe84a fix: partial return on POS invoice
(cherry picked from commit 76289fa8dc)

# Conflicts:
#	erpnext/accounts/doctype/pos_invoice/pos_invoice.py
2024-09-23 14:26:05 +00:00
ruthra kumar
8f81fdba4f Merge pull request #43335 from frappe/mergify/bp/version-14-hotfix/pr-43316
fix: incorrect outstanding on non-pos invoice with write_off_account (backport #43316)
2024-09-23 18:22:39 +05:30
ruthra kumar
5434d4bb1c Merge pull request #43337 from frappe/mergify/bp/version-14-hotfix/pr-43308
fix: show chart tool tip in report currency (backport #43308)
2024-09-23 18:22:17 +05:30
ruthra kumar
54d0162fef Merge pull request #43339 from frappe/mergify/bp/version-14-hotfix/pr-43307
fix: change dynamic link doctype fieldtype to data (backport #43307)
2024-09-23 18:21:57 +05:30
venkat102
c0dfda6ccc fix: change dynamic link doctype fieldtype to data
(cherry picked from commit 1e46f7344a)
2024-09-23 12:23:01 +00:00
venkat102
6becaba672 fix: add currency in financial statement
(cherry picked from commit 91a27bda84)
2024-09-23 12:11:47 +00:00
venkat102
92438f8dba fix: show chart tool tip in report currency
(cherry picked from commit 827b3f4542)
2024-09-23 12:11:46 +00:00
ruthra kumar
79235cd688 fix: incorrect outstanding on non-pos invoice with write_off_account
(cherry picked from commit d5e2906e59)
2024-09-23 12:09:50 +00:00
barredterra
32d4f96e02 fix(Payment Reconciliation): German translations 2024-09-19 13:57:55 +02:00
mergify[bot]
1bdbcc6703 fix: incorrect stock balance for inventory dimension (backport #43284) (#43289)
* fix: incorrect stock balance for inventory dimension (#43284)

(cherry picked from commit 3e7a7a54bf)

# Conflicts:
#	erpnext/stock/report/stock_balance/stock_balance.js

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2024-09-19 10:20:58 +05:30
rohitwaghchaure
8abc1f4da9 fix: Serial No reserved against historical sales order - unable to sell again (#43294) 2024-09-19 09:37:08 +05:30
ruthra kumar
2e2a4d9c70 Merge pull request #43292 from frappe/mergify/bp/version-14-hotfix/pr-43274
fix: translate in js (backport #43274)
2024-09-19 09:01:04 +05:30
Doğancan
e43c357c08 fix: translate in js
(cherry picked from commit 25faec5662)
2024-09-19 03:27:14 +00:00
ruthra kumar
66349d0877 Merge pull request #43288 from frappe/mergify/bp/version-14-hotfix/pr-43213
fix: Payment Ledger Report currency fieldtype fix (backport #43213)
2024-09-19 08:50:19 +05:30
sameer Chauhan
7ac39dc233 fix: Payment Ledger Report currency fieldtype fix
(cherry picked from commit ad2d6a1625)
2024-09-19 01:36:31 +00:00
Nihantra C. Patel
02f414e0aa Merge pull request #43277 from frappe/mergify/bp/version-14-hotfix/pr-43276
fix: Translation for button SO to PO (backport #43276)
2024-09-18 21:49:31 +05:30
Nihantra C. Patel
b6ce9222aa fix: Translation for button SO to PO --conflicts 2024-09-18 21:47:30 +05:30
Nihantra C. Patel
a8b5bcfe02 fix: Translation for button SO to PO
(cherry picked from commit a5275e9f28)

# Conflicts:
#	erpnext/selling/doctype/sales_order/sales_order.js
2024-09-18 16:14:06 +00:00
123 changed files with 5686 additions and 609 deletions

View File

@@ -4,22 +4,22 @@
# the repo. Unless a later match takes precedence,
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @anandbaburajan @deepeshgarg007
erpnext/assets/ @khushi8112 @deepeshgarg007
erpnext/loan_management/ @deepeshgarg007
erpnext/regional @deepeshgarg007 @ruthra-kumar
erpnext/selling @deepeshgarg007 @ruthra-kumar
erpnext/support/ @deepeshgarg007
pos*
erpnext/buying/ @rohitwaghchaure @s-aga-r
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
erpnext/stock/ @rohitwaghchaure @s-aga-r
erpnext/subcontracting @rohitwaghchaure @s-aga-r
erpnext/buying/ @rohitwaghchaure
erpnext/maintenance/ @rohitwaghchaure
erpnext/manufacturing/ @rohitwaghchaure
erpnext/quality_management/ @rohitwaghchaure
erpnext/stock/ @rohitwaghchaure
erpnext/subcontracting @rohitwaghchaure
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
erpnext/patches/ @deepeshgarg007
.github/ @deepeshgarg007
pyproject.toml @ankush
pyproject.toml @akhilnarang

View File

@@ -3,7 +3,7 @@ import inspect
import frappe
__version__ = "14.74.1"
__version__ = "14.77.2"
def get_default_company(user=None):

View File

@@ -58,7 +58,7 @@ def build_conditions(process_type, account, company):
)
if account:
conditions += f"AND {deferred_account}='{account}'"
conditions += f"AND {deferred_account}={frappe.db.escape(account)}"
elif company:
conditions += f"AND p.company = {frappe.db.escape(company)}"

View File

@@ -208,8 +208,49 @@
"label": "Disabled"
}
],
"links": [],
"modified": "2024-02-02 17:50:09.768835",
"links": [
{
"group": "Transactions",
"link_doctype": "Payment Request",
"link_fieldname": "bank_account"
},
{
"group": "Transactions",
"link_doctype": "Payment Order",
"link_fieldname": "bank_account"
},
{
"group": "Transactions",
"link_doctype": "Bank Guarantee",
"link_fieldname": "bank_account"
},
{
"group": "Transactions",
"link_doctype": "Bank Transaction",
"link_fieldname": "bank_account"
},
{
"group": "Accounting",
"link_doctype": "Payment Entry",
"link_fieldname": "bank_account"
},
{
"group": "Accounting",
"link_doctype": "Journal Entry",
"link_fieldname": "bank_account"
},
{
"group": "Party",
"link_doctype": "Customer",
"link_fieldname": "default_bank_account"
},
{
"group": "Party",
"link_doctype": "Supplier",
"link_fieldname": "default_bank_account"
}
],
"modified": "2024-10-30 09:41:14.113414",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",
@@ -246,4 +287,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -1,20 +0,0 @@
from frappe import _
def get_data():
return {
"fieldname": "bank_account",
"non_standard_fieldnames": {
"Customer": "default_bank_account",
"Supplier": "default_bank_account",
},
"transactions": [
{
"label": _("Payments"),
"items": ["Payment Entry", "Payment Request", "Payment Order", "Payroll Entry"],
},
{"label": _("Party"), "items": ["Customer", "Supplier"]},
{"items": ["Bank Guarantee"]},
{"items": ["Journal Entry"]},
],
}

View File

@@ -55,7 +55,7 @@ class BankClearance(Document):
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
if(paid_from=%(account)s, 0, received_amount) as debit,
if(paid_from=%(account)s, 0, received_amount + total_taxes_and_charges) as debit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
from `tabPayment Entry`

View File

@@ -84,7 +84,7 @@ def get_api_endpoint(service_provider: str | None = None, use_http: bool = False
if service_provider == "exchangerate.host":
api = "api.exchangerate.host/convert"
elif service_provider == "frankfurter.app":
api = "frankfurter.app/{transaction_date}"
api = "api.frankfurter.app/{transaction_date}"
protocol = "https://"
if use_http:

View File

@@ -129,19 +129,31 @@ def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_
@frappe.whitelist()
def get_dunning_letter_text(dunning_type, doc, language=None):
def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str | None = None) -> dict:
DOCTYPE = "Dunning Letter Text"
FIELDS = ["body_text", "closing_text", "language"]
if isinstance(doc, str):
doc = json.loads(doc)
if not language:
language = doc.get("language")
if language:
filters = {"parent": dunning_type, "language": language}
else:
filters = {"parent": dunning_type, "is_default_language": 1}
letter_text = frappe.db.get_value(
"Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1
)
if letter_text:
return {
"body_text": frappe.render_template(letter_text.body_text, doc),
"closing_text": frappe.render_template(letter_text.closing_text, doc),
"language": letter_text.language,
}
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
)
if not letter_text:
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1
)
if not letter_text:
return {}
return {
"body_text": frappe.render_template(letter_text.body_text, doc),
"closing_text": frappe.render_template(letter_text.closing_text, doc),
"language": letter_text.language,
}

View File

@@ -52,6 +52,21 @@ class ExchangeRateRevaluation(Document):
if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries"))
def before_submit(self):
self.remove_accounts_without_gain_loss()
def remove_accounts_without_gain_loss(self):
self.accounts = [account for account in self.accounts if account.gain_loss]
if not self.accounts:
frappe.throw(_("At least one account with exchange gain or loss is required"))
frappe.msgprint(
_("Removing rows without exchange gain or loss"),
alert=True,
indicator="yellow",
)
def on_cancel(self):
self.ignore_linked_doctypes = "GL Entry"
@@ -226,23 +241,23 @@ class ExchangeRateRevaluation(Document):
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date)
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
if gain_loss:
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"zero_balance": d.zero_balance,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
"new_balance_in_account_currency": d.balance_in_account_currency,
"gain_loss": gain_loss,
}
)
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"zero_balance": d.zero_balance,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
"new_balance_in_account_currency": d.balance_in_account_currency,
"gain_loss": gain_loss,
}
)
# Handle Accounts with '0' balance in Account/Base Currency
for d in [x for x in account_details if x.zero_balance]:
@@ -266,23 +281,22 @@ class ExchangeRateRevaluation(Document):
current_exchange_rate * d.balance_in_account_currency
)
if gain_loss:
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"zero_balance": d.zero_balance,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
"new_balance_in_account_currency": new_balance_in_account_currency,
"gain_loss": gain_loss,
}
)
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"zero_balance": d.zero_balance,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
"new_balance_in_account_currency": new_balance_in_account_currency,
"gain_loss": gain_loss,
}
)
return accounts

View File

@@ -153,7 +153,7 @@ class JournalEntry(AccountsController):
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
def validate_stock_accounts(self):
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
for account in stock_accounts:
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
account, self.posting_date, self.company

View File

@@ -146,12 +146,12 @@ class TestJournalEntry(unittest.TestCase):
"credit_in_account_currency": 0 if diff > 0 else abs(diff),
},
)
jv.insert()
if account_bal == stock_bal:
self.assertRaises(StockAccountInvalidTransaction, jv.submit)
self.assertRaises(StockAccountInvalidTransaction, jv.insert)
frappe.db.rollback()
else:
jv.insert()
jv.submit()
jv.cancel()

View File

@@ -15,6 +15,10 @@ frappe.ui.form.on('Payment Entry', {
}
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
if (frm.is_new()) {
set_default_party_type(frm);
}
},
setup: function(frm) {
@@ -154,6 +158,17 @@ frappe.ui.form.on('Payment Entry', {
};
});
frm.set_query("payment_request", "references", function (doc, cdt, cdn) {
const row = frappe.get_doc(cdt, cdn);
return {
query: "erpnext.accounts.doctype.payment_request.payment_request.get_open_payment_requests_query",
filters: {
reference_doctype: row.reference_doctype,
reference_name: row.reference_name,
},
};
});
frm.set_query("sales_taxes_and_charges_template", function () {
return {
filters: {
@@ -171,7 +186,15 @@ frappe.ui.form.on('Payment Entry', {
},
};
});
frm.add_fetch(
"payment_request",
"outstanding_amount",
"payment_request_outstanding",
"Payment Entry Reference"
);
},
refresh: function (frm) {
erpnext.hide_company(frm);
frm.events.hide_unhide_fields(frm);
@@ -184,6 +207,7 @@ frappe.ui.form.on('Payment Entry', {
}
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
frappe.flags.allocate_payment_amount = true;
},
validate_company: (frm) => {
@@ -300,6 +324,7 @@ frappe.ui.form.on('Payment Entry', {
},
payment_type: function(frm) {
set_default_party_type(frm);
if(frm.doc.payment_type == "Internal Transfer") {
$.each(["party", "party_balance", "paid_from", "paid_to",
"references", "total_allocated_amount"], function(i, field) {
@@ -633,10 +658,9 @@ frappe.ui.form.on('Payment Entry', {
frm.set_value("base_received_amount",
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate));
if(frm.doc.payment_type == "Pay")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1);
else
frm.events.set_unallocated_amount(frm);
if (frm.doc.payment_type == "Pay")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, true);
else frm.events.set_unallocated_amount(frm);
frm.set_paid_amount_based_on_received_amount = false;
frm.events.hide_unhide_fields(frm);
@@ -654,10 +678,9 @@ frappe.ui.form.on('Payment Entry', {
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
}
if(frm.doc.payment_type == "Receive")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1);
else
frm.events.set_unallocated_amount(frm);
if (frm.doc.payment_type == "Receive")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, true);
else frm.events.set_unallocated_amount(frm);
},
get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
@@ -801,6 +824,7 @@ frappe.ui.form.on('Payment Entry', {
c.outstanding_amount = d.outstanding_amount;
c.bill_no = d.bill_no;
c.payment_term = d.payment_term;
c.payment_term_outstanding = d.payment_term_outstanding;
c.allocated_amount = d.allocated_amount;
if(!in_list(frm.events.get_order_doctypes(frm), d.voucher_type)) {
@@ -842,10 +866,12 @@ frappe.ui.form.on('Payment Entry', {
}
}
frm.events.allocate_party_amount_against_ref_docs(frm,
(frm.doc.payment_type=="Receive" ? frm.doc.paid_amount : frm.doc.received_amount));
}
frm.events.allocate_party_amount_against_ref_docs(
frm,
frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount,
false
);
},
});
},
@@ -857,76 +883,13 @@ frappe.ui.form.on('Payment Entry', {
return ["Sales Invoice", "Purchase Invoice"];
},
allocate_party_amount_against_ref_docs: function(frm, paid_amount, paid_amount_change) {
var total_positive_outstanding_including_order = 0;
var total_negative_outstanding = 0;
var total_deductions = frappe.utils.sum($.map(frm.doc.deductions || [],
function(d) { return flt(d.amount) }));
allocate_party_amount_against_ref_docs: async function (frm, paid_amount, paid_amount_change) {
await frm.call("allocate_amount_to_references", {
paid_amount: paid_amount,
paid_amount_change: paid_amount_change,
allocate_payment_amount: frappe.flags.allocate_payment_amount ?? false,
});
paid_amount -= total_deductions;
$.each(frm.doc.references || [], function(i, row) {
if(flt(row.outstanding_amount) > 0)
total_positive_outstanding_including_order += flt(row.outstanding_amount);
else
total_negative_outstanding += Math.abs(flt(row.outstanding_amount));
})
var allocated_negative_outstanding = 0;
if (
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee")
) {
if(total_positive_outstanding_including_order > paid_amount) {
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;
allocated_negative_outstanding = total_negative_outstanding < remaining_outstanding ?
total_negative_outstanding : remaining_outstanding;
}
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"))
if(paid_amount > total_negative_outstanding) {
if(total_negative_outstanding == 0) {
frappe.msgprint(
__("Cannot {0} {1} {2} without any negative outstanding invoice", [frm.doc.payment_type,
(frm.doc.party_type=="Customer" ? "to" : "from"), frm.doc.party_type])
);
return false
} else {
frappe.msgprint(
__("Paid Amount cannot be greater than total negative outstanding amount {0}", [total_negative_outstanding])
);
return false;
}
} else {
allocated_positive_outstanding = total_negative_outstanding - paid_amount;
allocated_negative_outstanding = paid_amount +
(total_positive_outstanding_including_order < allocated_positive_outstanding ?
total_positive_outstanding_including_order : allocated_positive_outstanding)
}
}
$.each(frm.doc.references || [], function(i, row) {
if (frappe.flags.allocate_payment_amount == 0) {
//If allocate payment amount checkbox is unchecked, set zero to allocate amount
row.allocated_amount = 0;
} else if (frappe.flags.allocate_payment_amount != 0 && (!row.allocated_amount || paid_amount_change)) {
if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) {
row.allocated_amount = (row.outstanding_amount >= allocated_positive_outstanding) ?
allocated_positive_outstanding : row.outstanding_amount;
allocated_positive_outstanding -= flt(row.allocated_amount);
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
row.allocated_amount = (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) ?
-1*allocated_negative_outstanding : row.outstanding_amount;
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
}
}
})
frm.refresh_fields()
frm.events.set_total_allocated_amount(frm);
},
@@ -1409,6 +1372,62 @@ frappe.ui.form.on('Payment Entry', {
return current_tax_amount;
},
cost_center: function (frm) {
if (frm.doc.posting_date && (frm.doc.paid_from || frm.doc.paid_to)) {
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
args: {
company: frm.doc.company,
date: frm.doc.posting_date,
paid_from: frm.doc.paid_from,
paid_to: frm.doc.paid_to,
ptype: frm.doc.party_type,
pty: frm.doc.party,
cost_center: frm.doc.cost_center,
},
callback: function (r, rt) {
if (r.message) {
frappe.run_serially([
() => {
frm.set_value(
"paid_from_account_balance",
r.message.paid_from_account_balance
);
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
frm.set_value("party_balance", r.message.party_balance);
},
]);
}
},
});
}
},
after_save: function (frm) {
const { matched_payment_requests } = frappe.last_response;
if (!matched_payment_requests) return;
const COLUMN_LABEL = [
[__("Reference DocType"), __("Reference Name"), __("Allocated Amount"), __("Payment Request")],
];
frappe.msgprint({
title: __("Unset Matched Payment Request"),
message: COLUMN_LABEL.concat(matched_payment_requests),
as_table: true,
wide: true,
primary_action: {
label: __("Allocate Payment Request"),
action() {
frappe.hide_msgprint();
frm.call("set_matched_payment_requests", { matched_payment_requests }, () => {
frm.dirty();
});
},
},
});
},
});
@@ -1495,35 +1514,18 @@ frappe.ui.form.on('Payment Entry Deduction', {
deductions_remove: function(frm) {
frm.events.set_unallocated_amount(frm);
}
})
frappe.ui.form.on('Payment Entry', {
cost_center: function(frm){
if (frm.doc.posting_date && (frm.doc.paid_from||frm.doc.paid_to)) {
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_and_account_balance",
args: {
company: frm.doc.company,
date: frm.doc.posting_date,
paid_from: frm.doc.paid_from,
paid_to: frm.doc.paid_to,
ptype: frm.doc.party_type,
pty: frm.doc.party,
cost_center: frm.doc.cost_center
},
callback: function(r, rt) {
if(r.message) {
frappe.run_serially([
() => {
frm.set_value("paid_from_account_balance", r.message.paid_from_account_balance);
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
frm.set_value("party_balance", r.message.party_balance);
}
]);
}
}
});
}
},
})
});
function set_default_party_type(frm) {
if (frm.doc.party) return;
let party_type;
if (frm.doc.payment_type == "Receive") {
party_type = "Customer";
} else if (frm.doc.payment_type == "Pay") {
party_type = "Supplier";
}
if (party_type) frm.set_value("party_type", party_type);
}

View File

@@ -7,9 +7,11 @@ from functools import reduce
import frappe
from frappe import ValidationError, _, qb, scrub, throw
from frappe.query_builder import Tuple
from frappe.query_builder.functions import Count
from frappe.utils import cint, comma_or, flt, getdate, nowdate
from frappe.utils.data import comma_and, fmt_money
from pypika.functions import Sum
from frappe.utils.data import comma_and, fmt_money, get_link_to_form
from pypika.functions import Coalesce, Sum
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
@@ -91,13 +93,17 @@ class PaymentEntry(AccountsController):
self.set_tax_withholding()
self.set_status()
def before_save(self):
self.set_matched_unset_payment_requests_to_response()
def on_submit(self):
if self.difference_amount:
frappe.throw(_("Difference Amount must be zero"))
self.make_gl_entries()
self.update_outstanding_amounts()
self.update_advance_paid()
self.update_payment_schedule()
self.update_payment_requests()
self.update_advance_paid() # advance_paid_status depends on the payment request amount
self.set_status()
def on_cancel(self):
@@ -115,30 +121,34 @@ class PaymentEntry(AccountsController):
super().on_cancel()
self.make_gl_entries(cancel=1)
self.update_outstanding_amounts()
self.update_advance_paid()
self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1)
self.set_payment_req_status()
self.update_payment_requests(cancel=True)
self.update_advance_paid() # advance_paid_status depends on the payment request amount
self.set_status()
def set_payment_req_status(self):
from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status
def update_payment_requests(self, cancel=False):
from erpnext.accounts.doctype.payment_request.payment_request import (
update_payment_requests_as_per_pe_references,
)
update_payment_req_status(self, None)
update_payment_requests_as_per_pe_references(self.references, cancel=cancel)
def update_outstanding_amounts(self):
self.set_missing_ref_details(force=True)
def validate_duplicate_entry(self):
reference_names = []
reference_names = set()
for d in self.get("references"):
if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names:
key = (d.reference_doctype, d.reference_name, d.payment_term, d.payment_request)
if key in reference_names:
frappe.throw(
_("Row #{0}: Duplicate entry in References {1} {2}").format(
d.idx, d.reference_doctype, d.reference_name
)
)
reference_names.append((d.reference_doctype, d.reference_name, d.payment_term))
reference_names.add(key)
def set_bank_account_data(self):
if self.bank_account:
@@ -164,6 +174,8 @@ class PaymentEntry(AccountsController):
if self.payment_type == "Internal Transfer":
return
self.validate_allocated_amount_as_per_payment_request()
if self.party_type in ("Customer", "Supplier"):
self.validate_allocated_amount_with_latest_data()
else:
@@ -176,6 +188,27 @@ class PaymentEntry(AccountsController):
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(d.outstanding_amount):
frappe.throw(fail_message.format(d.idx))
def validate_allocated_amount_as_per_payment_request(self):
"""
Allocated amount should not be greater than the outstanding amount of the Payment Request.
"""
if not self.references:
return
pr_outstanding_amounts = get_payment_request_outstanding_set_in_references(self.references)
if not pr_outstanding_amounts:
return
for ref in self.references:
if ref.payment_request and ref.allocated_amount > pr_outstanding_amounts[ref.payment_request]:
frappe.throw(
msg=_(
"Row #{0}: Allocated Amount cannot be greater than Outstanding Amount of Payment Request {1}"
).format(ref.idx, get_link_to_form("Payment Request", ref.payment_request)),
title=_("Invalid Allocated Amount"),
)
def term_based_allocation_enabled_for_reference(
self, reference_doctype: str, reference_name: str
) -> bool:
@@ -1422,6 +1455,380 @@ class PaymentEntry(AccountsController):
return current_tax_fraction
def set_matched_unset_payment_requests_to_response(self):
"""
Find matched Payment Requests for those references which have no Payment Request set.\n
And set to `frappe.response` to show in the frontend for allocation.
"""
if not self.references:
return
matched_payment_requests = get_matched_payment_request_of_references(
[row for row in self.references if not row.payment_request]
)
if not matched_payment_requests:
return
frappe.response["matched_payment_requests"] = matched_payment_requests
@frappe.whitelist()
def allocate_amount_to_references(self, paid_amount, paid_amount_change, allocate_payment_amount):
"""
Allocate `Allocated Amount` and `Payment Request` against `Reference` based on `Paid Amount` and `Outstanding Amount`.\n
:param paid_amount: Paid Amount / Received Amount.
:param paid_amount_change: Flag to check if `Paid Amount` is changed or not.
:param allocate_payment_amount: Flag to allocate amount or not. (Payment Request is also dependent on this flag)
"""
if not self.references:
return
if not allocate_payment_amount:
for ref in self.references:
ref.allocated_amount = 0
return
# calculating outstanding amounts
precision = self.precision("paid_amount")
total_positive_outstanding_including_order = 0
total_negative_outstanding = 0
paid_amount -= sum(flt(d.amount, precision) for d in self.deductions)
for ref in self.references:
reference_outstanding_amount = ref.outstanding_amount
abs_outstanding_amount = abs(reference_outstanding_amount)
if reference_outstanding_amount > 0:
total_positive_outstanding_including_order += abs_outstanding_amount
else:
total_negative_outstanding += abs_outstanding_amount
# calculating allocated outstanding amounts
allocated_negative_outstanding = 0
allocated_positive_outstanding = 0
# checking party type and payment type
if (self.payment_type == "Receive" and self.party_type == "Customer") or (
self.payment_type == "Pay" and self.party_type in ("Supplier", "Employee")
):
if total_positive_outstanding_including_order > paid_amount:
remaining_outstanding = flt(
total_positive_outstanding_including_order - paid_amount, precision
)
allocated_negative_outstanding = min(remaining_outstanding, total_negative_outstanding)
allocated_positive_outstanding = paid_amount + allocated_negative_outstanding
elif self.party_type in ("Supplier", "Employee"):
if paid_amount > total_negative_outstanding:
if total_negative_outstanding == 0:
frappe.msgprint(
_("Cannot {0} from {2} without any negative outstanding invoice").format(
self.payment_type,
self.party_type,
)
)
else:
frappe.msgprint(
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
total_negative_outstanding
)
)
return
else:
allocated_positive_outstanding = flt(total_negative_outstanding - paid_amount, precision)
allocated_negative_outstanding = paid_amount + min(
total_positive_outstanding_including_order, allocated_positive_outstanding
)
# inner function to set `allocated_amount` to those row which have no PR
def _allocation_to_unset_pr_row(
row, outstanding_amount, allocated_positive_outstanding, allocated_negative_outstanding
):
if outstanding_amount > 0 and allocated_positive_outstanding >= 0:
row.allocated_amount = min(allocated_positive_outstanding, outstanding_amount)
allocated_positive_outstanding = flt(
allocated_positive_outstanding - row.allocated_amount, precision
)
elif outstanding_amount < 0 and allocated_negative_outstanding:
row.allocated_amount = min(allocated_negative_outstanding, abs(outstanding_amount)) * -1
allocated_negative_outstanding = flt(
allocated_negative_outstanding - abs(row.allocated_amount), precision
)
return allocated_positive_outstanding, allocated_negative_outstanding
# allocate amount based on `paid_amount` is changed or not
if not paid_amount_change:
for ref in self.references:
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
ref,
ref.outstanding_amount,
allocated_positive_outstanding,
allocated_negative_outstanding,
)
allocate_open_payment_requests_to_references(self.references, self.precision("paid_amount"))
else:
payment_request_outstanding_amounts = (
get_payment_request_outstanding_set_in_references(self.references) or {}
)
references_outstanding_amounts = get_references_outstanding_amount(self.references) or {}
remaining_references_allocated_amounts = references_outstanding_amounts.copy()
# Re allocate amount to those references which have PR set (Higher priority)
for ref in self.references:
if not ref.payment_request:
continue
# fetch outstanding_amount of `Reference` (Payment Term) and `Payment Request` to allocate new amount
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
reference_outstanding_amount = references_outstanding_amounts[key]
pr_outstanding_amount = payment_request_outstanding_amounts[ref.payment_request]
if reference_outstanding_amount > 0 and allocated_positive_outstanding >= 0:
# allocate amount according to outstanding amounts
outstanding_amounts = (
allocated_positive_outstanding,
reference_outstanding_amount,
pr_outstanding_amount,
)
ref.allocated_amount = min(outstanding_amounts)
# update amounts to track allocation
allocated_amount = ref.allocated_amount
allocated_positive_outstanding = flt(
allocated_positive_outstanding - allocated_amount, precision
)
remaining_references_allocated_amounts[key] = flt(
remaining_references_allocated_amounts[key] - allocated_amount, precision
)
payment_request_outstanding_amounts[ref.payment_request] = flt(
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
)
elif reference_outstanding_amount < 0 and allocated_negative_outstanding:
# allocate amount according to outstanding amounts
outstanding_amounts = (
allocated_negative_outstanding,
abs(reference_outstanding_amount),
pr_outstanding_amount,
)
ref.allocated_amount = min(outstanding_amounts) * -1
# update amounts to track allocation
allocated_amount = abs(ref.allocated_amount)
allocated_negative_outstanding = flt(
allocated_negative_outstanding - allocated_amount, precision
)
remaining_references_allocated_amounts[key] += allocated_amount # negative amount
payment_request_outstanding_amounts[ref.payment_request] = flt(
payment_request_outstanding_amounts[ref.payment_request] - allocated_amount, precision
)
# Re allocate amount to those references which have no PR (Lower priority)
for ref in self.references:
if ref.payment_request:
continue
key = (ref.reference_doctype, ref.reference_name, ref.get("payment_term"))
reference_outstanding_amount = remaining_references_allocated_amounts[key]
allocated_positive_outstanding, allocated_negative_outstanding = _allocation_to_unset_pr_row(
ref,
reference_outstanding_amount,
allocated_positive_outstanding,
allocated_negative_outstanding,
)
@frappe.whitelist()
def set_matched_payment_requests(self, matched_payment_requests):
"""
Set `Payment Request` against `Reference` based on `matched_payment_requests`.\n
:param matched_payment_requests: List of tuple of matched Payment Requests.
---
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
"""
if not self.references or not matched_payment_requests:
return
if isinstance(matched_payment_requests, str):
matched_payment_requests = json.loads(matched_payment_requests)
# modify matched_payment_requests
# like (reference_doctype, reference_name, allocated_amount): payment_request
payment_requests = {}
for row in matched_payment_requests:
key = tuple(row[:3])
payment_requests[key] = row[3]
for ref in self.references:
if ref.payment_request:
continue
key = (ref.reference_doctype, ref.reference_name, ref.allocated_amount)
if key in payment_requests:
ref.payment_request = payment_requests[key]
del payment_requests[key] # to avoid duplicate allocation
def get_matched_payment_request_of_references(references=None):
"""
Get those `Payment Requests` which are matched with `References`.\n
- Amount must be same.
- Only single `Payment Request` available for this amount.
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
"""
if not references:
return
# to fetch matched rows
refs = {
(row.reference_doctype, row.reference_name, row.allocated_amount)
for row in references
if row.reference_doctype and row.reference_name and row.allocated_amount
}
if not refs:
return
PR = frappe.qb.DocType("Payment Request")
# query to group by reference_doctype, reference_name, outstanding_amount
subquery = (
frappe.qb.from_(PR)
.select(
PR.reference_doctype,
PR.reference_name,
PR.outstanding_amount.as_("allocated_amount"),
PR.name.as_("payment_request"),
Count("*").as_("count"),
)
.where(Tuple(PR.reference_doctype, PR.reference_name, PR.outstanding_amount).isin(refs))
.where(PR.status != "Paid")
.where(PR.docstatus == 1)
.groupby(PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
)
# query to fetch matched rows which are single
matched_prs = (
frappe.qb.from_(subquery)
.select(
subquery.reference_doctype,
subquery.reference_name,
subquery.allocated_amount,
subquery.payment_request,
)
.where(subquery.count == 1)
.run()
)
return matched_prs if matched_prs else None
def get_references_outstanding_amount(references=None):
"""
Fetch accurate outstanding amount of `References`.\n
- If `Payment Term` is set, then fetch outstanding amount from `Payment Schedule`.
- If `Payment Term` is not set, then fetch outstanding amount from `References` it self.
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
"""
if not references:
return
refs_with_payment_term = get_outstanding_of_references_with_payment_term(references) or {}
refs_without_payment_term = get_outstanding_of_references_with_no_payment_term(references) or {}
return {**refs_with_payment_term, **refs_without_payment_term}
def get_outstanding_of_references_with_payment_term(references=None):
"""
Fetch outstanding amount of `References` which have `Payment Term` set.\n
Example: {(reference_doctype, reference_name, payment_term): outstanding_amount, ...}
"""
if not references:
return
refs = {
(row.reference_doctype, row.reference_name, row.payment_term)
for row in references
if row.reference_doctype and row.reference_name and row.payment_term
}
if not refs:
return
PS = frappe.qb.DocType("Payment Schedule")
response = (
frappe.qb.from_(PS)
.select(PS.parenttype, PS.parent, PS.payment_term, PS.outstanding)
.where(Tuple(PS.parenttype, PS.parent, PS.payment_term).isin(refs))
).run(as_dict=True)
if not response:
return
return {(row.parenttype, row.parent, row.payment_term): row.outstanding for row in response}
def get_outstanding_of_references_with_no_payment_term(references):
"""
Fetch outstanding amount of `References` which have no `Payment Term` set.\n
- Fetch outstanding amount from `References` it self.
Note: `None` is used for allocation of `Payment Request`
Example: {(reference_doctype, reference_name, None): outstanding_amount, ...}
"""
if not references:
return
outstanding_amounts = {}
for ref in references:
if ref.payment_term:
continue
key = (ref.reference_doctype, ref.reference_name, None)
if key not in outstanding_amounts:
outstanding_amounts[key] = ref.outstanding_amount
return outstanding_amounts
def get_payment_request_outstanding_set_in_references(references=None):
"""
Fetch outstanding amount of `Payment Request` which are set in `References`.\n
Example: {payment_request: outstanding_amount, ...}
"""
if not references:
return
referenced_payment_requests = {row.payment_request for row in references if row.payment_request}
if not referenced_payment_requests:
return
PR = frappe.qb.DocType("Payment Request")
response = (
frappe.qb.from_(PR)
.select(PR.name, PR.outstanding_amount)
.where(PR.name.isin(referenced_payment_requests))
).run()
return dict(response) if response else None
def validate_inclusive_tax(tax, doc):
def _on_previous_row_error(row_range):
@@ -1863,7 +2270,9 @@ def get_party_details(company, party_type, party, date, cost_center=None):
account_balance = get_balance_on(party_account, date, cost_center=cost_center)
_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, cost_center=cost_center)
party_balance = get_balance_on(
party_type=party_type, party=party, company=company, cost_center=cost_center
)
if party_type in ["Customer", "Supplier"]:
bank_account = get_party_bank_account(party_type, party)
@@ -2010,6 +2419,8 @@ def get_payment_entry(
party_type=None,
payment_type=None,
reference_date=None,
ignore_permissions=False,
created_from_payment_request=False,
):
doc = frappe.get_doc(dt, dn)
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
@@ -2160,9 +2571,179 @@ def get_payment_entry(
pe.set_difference_amount()
# If PE is created from PR directly, then no need to find open PRs for the references
if not created_from_payment_request:
allocate_open_payment_requests_to_references(pe.references, pe.precision("paid_amount"))
return pe
def get_open_payment_requests_for_references(references=None):
"""
Fetch all unpaid Payment Requests for the references. \n
- Each reference can have multiple Payment Requests. \n
Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}}
"""
if not references:
return
refs = {
(row.reference_doctype, row.reference_name)
for row in references
if row.reference_doctype and row.reference_name and row.allocated_amount
}
if not refs:
return
PR = frappe.qb.DocType("Payment Request")
response = (
frappe.qb.from_(PR)
.select(PR.name, PR.reference_doctype, PR.reference_name, PR.outstanding_amount)
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
.where(PR.status != "Paid")
.where(PR.docstatus == 1)
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
).run(as_dict=True)
if not response:
return
reference_payment_requests = {}
for row in response:
key = (row.reference_doctype, row.reference_name)
if key not in reference_payment_requests:
reference_payment_requests[key] = {row.name: row.outstanding_amount}
else:
reference_payment_requests[key][row.name] = row.outstanding_amount
return reference_payment_requests
def allocate_open_payment_requests_to_references(references=None, precision=None):
"""
Allocate unpaid Payment Requests to the references. \n
---
- Allocation based on below factors
- Reference Allocated Amount
- Reference Outstanding Amount (With Payment Terms or without Payment Terms)
- Reference Payment Request's outstanding amount
---
- Allocation based on below scenarios
- Reference's Allocated Amount == Payment Request's Outstanding Amount
- Allocate the Payment Request to the reference
- This PR will not be allocated further
- Reference's Allocated Amount < Payment Request's Outstanding Amount
- Allocate the Payment Request to the reference
- Reduce the PR's outstanding amount by the allocated amount
- This PR can be allocated further
- Reference's Allocated Amount > Payment Request's Outstanding Amount
- Allocate the Payment Request to the reference
- Reduce Allocated Amount of the reference by the PR's outstanding amount
- Create a new row for the remaining amount until the Allocated Amount is 0
- Allocate PR if available
---
- Note:
- Priority is given to the first Payment Request of respective references.
- Single Reference can have multiple rows.
- With Payment Terms or without Payment Terms
- With Payment Request or without Payment Request
"""
if not references:
return
# get all unpaid payment requests for the references
references_open_payment_requests = get_open_payment_requests_for_references(references)
if not references_open_payment_requests:
return
if not precision:
precision = references[0].precision("allocated_amount")
# to manage new rows
row_number = 1
MOVE_TO_NEXT_ROW = 1
TO_SKIP_NEW_ROW = 2
while row_number <= len(references):
row = references[row_number - 1]
reference_key = (row.reference_doctype, row.reference_name)
# update the idx to maintain the order
row.idx = row_number
# unpaid payment requests for the reference
reference_payment_requests = references_open_payment_requests.get(reference_key)
if not reference_payment_requests:
row_number += MOVE_TO_NEXT_ROW # to move to next reference row
continue
# get the first payment request and its outstanding amount
payment_request, pr_outstanding_amount = next(iter(reference_payment_requests.items()))
allocated_amount = row.allocated_amount
# allocate the payment request to the reference and PR's outstanding amount
row.payment_request = payment_request
if pr_outstanding_amount == allocated_amount:
del reference_payment_requests[payment_request]
row_number += MOVE_TO_NEXT_ROW
elif pr_outstanding_amount > allocated_amount:
# reduce the outstanding amount of the payment request
reference_payment_requests[payment_request] -= allocated_amount
row_number += MOVE_TO_NEXT_ROW
else:
# split the reference row to allocate the remaining amount
del reference_payment_requests[payment_request]
row.allocated_amount = pr_outstanding_amount
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
# set the remaining amount to the next row
while allocated_amount:
# create a new row for the remaining amount
new_row = frappe.copy_doc(row)
references.insert(row_number, new_row)
# get the first payment request and its outstanding amount
payment_request, pr_outstanding_amount = next(
iter(reference_payment_requests.items()), (None, None)
)
# update new row
new_row.idx = row_number + 1
new_row.payment_request = payment_request
new_row.allocated_amount = min(
pr_outstanding_amount if pr_outstanding_amount else allocated_amount, allocated_amount
)
if not payment_request or not pr_outstanding_amount:
row_number += TO_SKIP_NEW_ROW
break
elif pr_outstanding_amount == allocated_amount:
del reference_payment_requests[payment_request]
row_number += TO_SKIP_NEW_ROW
break
elif pr_outstanding_amount > allocated_amount:
reference_payment_requests[payment_request] -= allocated_amount
row_number += TO_SKIP_NEW_ROW
break
else:
allocated_amount = flt(allocated_amount - pr_outstanding_amount, precision)
del reference_payment_requests[payment_request]
row_number += MOVE_TO_NEXT_ROW
def update_accounting_dimensions(pe, doc):
"""
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document

View File

@@ -10,12 +10,15 @@
"due_date",
"bill_no",
"payment_term",
"payment_term_outstanding",
"column_break_4",
"total_amount",
"outstanding_amount",
"allocated_amount",
"exchange_rate",
"exchange_gain_loss"
"exchange_gain_loss",
"payment_request",
"payment_request_outstanding"
],
"fields": [
{
@@ -101,12 +104,33 @@
"label": "Exchange Gain/Loss",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "payment_request",
"fieldtype": "Link",
"label": "Payment Request",
"options": "Payment Request"
},
{
"depends_on": "eval: doc.payment_term",
"fieldname": "payment_term_outstanding",
"fieldtype": "Float",
"label": "Payment Term Outstanding",
"read_only": 1
},
{
"depends_on": "eval: doc.payment_request && doc.payment_request_outstanding",
"fieldname": "payment_request_outstanding",
"fieldtype": "Float",
"is_virtual": 1,
"label": "Payment Request Outstanding",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-12-12 12:31:44.919895",
"modified": "2024-09-16 18:11:50.019343",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",

View File

@@ -1,9 +1,14 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class PaymentEntryReference(Document):
pass
@property
def payment_request_outstanding(self):
if not self.payment_request:
return
return frappe.db.get_value("Payment Request", self.payment_request, "outstanding_amount")

View File

@@ -144,12 +144,14 @@ class PaymentReconciliation(Document):
if self.get("cost_center"):
conditions.append(jea.cost_center == self.cost_center)
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency"
)
conditions.append(jea[dr_or_cr].gt(0))
account_type = erpnext.get_party_account_type(self.party_type)
if account_type == "Receivable":
dr_or_cr = jea.credit_in_account_currency - jea.debit_in_account_currency
elif account_type == "Payable":
dr_or_cr = jea.debit_in_account_currency - jea.credit_in_account_currency
conditions.append(dr_or_cr.gt(0))
if self.bank_cash_account:
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
@@ -164,7 +166,7 @@ class PaymentReconciliation(Document):
je.posting_date,
je.remark.as_("remarks"),
jea.name.as_("reference_row"),
jea[dr_or_cr].as_("amount"),
dr_or_cr.as_("amount"),
jea.is_advance,
jea.exchange_rate,
jea.account_currency.as_("currency"),
@@ -256,6 +258,7 @@ class PaymentReconciliation(Document):
"posting_date": inv.posting_date,
"currency": inv.currency,
"cost_center": inv.cost_center,
"remarks": inv.remarks,
}
)
)
@@ -298,6 +301,10 @@ class PaymentReconciliation(Document):
if self.invoice_limit:
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
non_reconciled_invoices = sorted(
non_reconciled_invoices, key=lambda k: k["posting_date"] or getdate(nowdate())
)
self.add_invoice_entries(non_reconciled_invoices)
def add_invoice_entries(self, non_reconciled_invoices):

View File

@@ -615,6 +615,42 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
def test_negative_debit_or_credit_journal_against_invoice(self):
transaction_date = nowdate()
amount = 100
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
# credit debtors account to record a payment
je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
je.accounts[1].party_type = "Customer"
je.accounts[1].party = self.customer
je.accounts[1].credit_in_account_currency = 0
je.accounts[1].debit_in_account_currency = -1 * amount
je.save()
je.submit()
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
# assert outstanding
si.reload()
self.assertEqual(si.status, "Paid")
self.assertEqual(si.outstanding_amount, 0)
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
def test_journal_against_journal(self):
transaction_date = nowdate()
sales = "Sales - _PR"
@@ -937,6 +973,100 @@ class TestPaymentReconciliation(FrappeTestCase):
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
)
def test_difference_amount_via_negative_debit_or_credit_journal_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer4
si.currency = "EUR"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.save().submit()
# Make payment using Journal Entry
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
je1.multi_currency = 1
je1.accounts[0].exchange_rate = 1
je1.accounts[0].credit_in_account_currency = -8000
je1.accounts[0].credit = -8000
je1.accounts[0].debit_in_account_currency = 0
je1.accounts[0].debit = 0
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = self.customer4
je1.accounts[1].exchange_rate = 80
je1.accounts[1].credit_in_account_currency = 100
je1.accounts[1].credit = 8000
je1.accounts[1].debit_in_account_currency = 0
je1.accounts[1].debit = 0
je1.save()
je1.submit()
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
je2.multi_currency = 1
je2.accounts[0].exchange_rate = 1
je2.accounts[0].credit_in_account_currency = -16000
je2.accounts[0].credit = -16000
je2.accounts[0].debit_in_account_currency = 0
je2.accounts[0].debit = 0
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer4
je2.accounts[1].exchange_rate = 80
je2.accounts[1].credit_in_account_currency = 200
je1.accounts[1].credit = 16000
je1.accounts[1].debit_in_account_currency = 0
je1.accounts[1].debit = 0
je2.save()
je2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer4
pr.receivable_payable_account = self.debtors_eur
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 2)
# Test exact payment allocation
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[0].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
# Test partial payment allocation (with excess payment entry)
pr.set("allocation", [])
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[1].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
# Check if difference journal entry gets generated for difference amount after reconciliation
pr.reconcile()
total_credit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
"sum(credit) as amount",
group_by="reference_name",
)[0].amount
# total credit includes the exchange gain/loss amount
self.assertEqual(flt(total_credit_amount, 2), 8500)
jea_parent = frappe.db.get_all(
"Journal Entry Account",
filters={"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name, "credit": 500},
fields=["parent"],
)[0]
self.assertEqual(
frappe.db.get_value("Journal Entry", jea_parent.parent, "voucher_type"), "Exchange Gain Or Loss"
)
def test_difference_amount_via_payment_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(

View File

@@ -14,7 +14,7 @@
"amount",
"difference_amount",
"sec_break1",
"remark",
"remarks",
"currency",
"exchange_rate",
"cost_center"
@@ -74,12 +74,6 @@
"fieldname": "sec_break1",
"fieldtype": "Section Break"
},
{
"fieldname": "remark",
"fieldtype": "Small Text",
"label": "Remark",
"read_only": 1
},
{
"fieldname": "currency",
"fieldtype": "Link",
@@ -105,12 +99,18 @@
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "remarks",
"fieldtype": "Small Text",
"label": "Remarks",
"read_only": 1
}
],
"is_virtual": 1,
"istable": 1,
"links": [],
"modified": "2023-11-17 17:33:34.818530",
"modified": "2024-10-29 16:24:43.021230",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Payment",

View File

@@ -48,8 +48,8 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
}
if (
(!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") &&
frm.doc.status == "Initiated"
frm.doc.payment_request_type == "Outward" &&
["Initiated", "Partially Paid"].includes(frm.doc.status)
) {
frm.add_custom_button(__("Create Payment Entry"), function () {
frappe.call({

View File

@@ -18,9 +18,11 @@
"reference_name",
"transaction_details",
"grand_total",
"currency",
"is_a_subscription",
"column_break_18",
"currency",
"outstanding_amount",
"party_account_currency",
"subscription_section",
"subscription_plans",
"bank_account_details",
@@ -68,6 +70,7 @@
{
"fieldname": "transaction_date",
"fieldtype": "Date",
"in_preview": 1,
"label": "Transaction Date"
},
{
@@ -132,7 +135,8 @@
"no_copy": 1,
"options": "reference_doctype",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "transaction_details",
@@ -140,11 +144,14 @@
"label": "Transaction Details"
},
{
"description": "Amount in customer's currency",
"description": "Amount in transaction currency",
"fieldname": "grand_total",
"fieldtype": "Currency",
"in_preview": 1,
"label": "Amount",
"options": "currency"
"non_negative": 1,
"options": "currency",
"reqd": 1
},
{
"default": "0",
@@ -388,13 +395,31 @@
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"depends_on": "eval: doc.docstatus === 1",
"description": "Amount in party's bank account currency",
"fieldname": "outstanding_amount",
"fieldtype": "Currency",
"in_preview": 1,
"label": "Outstanding Amount",
"non_negative": 1,
"options": "party_account_currency",
"read_only": 1
},
{
"fieldname": "party_account_currency",
"fieldtype": "Link",
"label": "Party Account Currency",
"options": "Currency",
"read_only": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-12-21 16:56:40.115737",
"modified": "2024-09-16 17:50:54.440090",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
@@ -429,7 +454,8 @@
"write": 1
}
],
"show_preview_popup": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -7,9 +7,11 @@ import json
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.functions import Sum
from frappe.utils import flt, get_url, nowdate
from frappe.utils.background_jobs import enqueue
from erpnext import get_company_currency
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
@@ -45,6 +47,12 @@ class PaymentRequest(Document):
frappe.throw(_("To create a Payment Request reference document is required"))
def validate_payment_request_amount(self):
if self.grand_total == 0:
frappe.throw(
_("{0} cannot be zero").format(self.get_label_from_fieldname("grand_total")),
title=_("Invalid Amount"),
)
existing_payment_request_amount = flt(
get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
)
@@ -92,6 +100,29 @@ class PaymentRequest(Document):
).format(self.grand_total, amount)
)
def before_submit(self):
company = frappe.get_value(self.reference_doctype, self.reference_name, "company")
if (
self.currency != self.party_account_currency
and self.party_account_currency == get_company_currency(company)
):
# set outstanding amount in party account currency
invoice = frappe.get_value(
self.reference_doctype,
self.reference_name,
["rounded_total", "grand_total", "base_rounded_total", "base_grand_total"],
as_dict=1,
)
grand_total = invoice.get("rounded_total") or invoice.get("grand_total")
base_grand_total = invoice.get("base_rounded_total") or invoice.get("base_grand_total")
self.outstanding_amount = flt(
self.grand_total / grand_total * base_grand_total,
self.precision("outstanding_amount"),
)
else:
self.outstanding_amount = self.grand_total
def on_submit(self):
if self.payment_request_type == "Outward":
self.db_set("status", "Initiated")
@@ -207,12 +238,12 @@ class PaymentRequest(Document):
return controller.get_payment_url(
**{
"amount": flt(self.grand_total, self.precision("grand_total")),
"title": data.company.encode("utf-8"),
"description": self.subject.encode("utf-8"),
"title": data.company,
"description": self.subject,
"reference_doctype": "Payment Request",
"reference_docname": self.name,
"payer_email": self.email_to or frappe.session.user,
"payer_name": frappe.safe_encode(data.customer_name),
"payer_name": data.customer_name,
"order_id": self.name,
"currency": self.currency,
}
@@ -220,7 +251,7 @@ class PaymentRequest(Document):
def set_as_paid(self):
if self.payment_channel == "Phone":
self.db_set("status", "Paid")
self.db_set({"status": "Paid", "outstanding_amount": 0})
else:
payment_entry = self.create_payment_entry()
@@ -241,26 +272,32 @@ class PaymentRequest(Document):
else:
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account)
party_account_currency = (
self.get("party_account_currency")
or ref_doc.get("party_account_currency")
or get_account_currency(party_account)
)
party_amount = bank_amount = self.outstanding_amount
bank_amount = self.grand_total
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
else:
party_amount = self.grand_total
exchange_rate = ref_doc.get("conversion_rate")
bank_amount = flt(self.outstanding_amount / exchange_rate, self.precision("grand_total"))
# outstanding amount is already in Part's account currency
payment_entry = get_payment_entry(
self.reference_doctype,
self.reference_name,
party_amount=party_amount,
bank_account=self.payment_account,
bank_amount=bank_amount,
created_from_payment_request=True,
)
payment_entry.update(
{
"mode_of_payment": self.mode_of_payment,
"reference_no": self.name,
"reference_no": self.name, # to prevent validation error
"reference_date": nowdate(),
"remarks": "Payment Entry against {} {} via Payment Request {}".format(
self.reference_doctype, self.reference_name, self.name
@@ -268,6 +305,9 @@ class PaymentRequest(Document):
}
)
# Allocate payment_request for each reference in payment_entry (Payment Term can splits the row)
self._allocate_payment_request_to_pe_references(references=payment_entry.references)
# Update dimensions
payment_entry.update(
{
@@ -276,14 +316,6 @@ class PaymentRequest(Document):
}
)
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
amount = payment_entry.base_paid_amount
else:
amount = self.grand_total
payment_entry.received_amount = amount
payment_entry.get("references")[0].allocated_amount = amount
# Update 'Paid Amount' on Forex transactions
if self.currency != ref_doc.company_currency:
if (
@@ -397,6 +429,62 @@ class PaymentRequest(Document):
if payment_provider == "stripe":
return create_stripe_subscription(gateway_controller, data)
def _allocate_payment_request_to_pe_references(self, references):
"""
Allocate the Payment Request to the Payment Entry references based on\n
- Allocated Amount.
- Outstanding Amount of Payment Request.\n
Payment Request is doc itself and references are the rows of Payment Entry.
"""
if len(references) == 1:
references[0].payment_request = self.name
return
precision = references[0].precision("allocated_amount")
outstanding_amount = self.outstanding_amount
# to manage rows
row_number = 1
MOVE_TO_NEXT_ROW = 1
TO_SKIP_NEW_ROW = 2
NEW_ROW_ADDED = False
while row_number <= len(references):
row = references[row_number - 1]
# update the idx to maintain the order
row.idx = row_number
if outstanding_amount == 0:
if not NEW_ROW_ADDED:
break
row_number += MOVE_TO_NEXT_ROW
continue
# allocate the payment request to the row
row.payment_request = self.name
if row.allocated_amount <= outstanding_amount:
outstanding_amount = flt(outstanding_amount - row.allocated_amount, precision)
row_number += MOVE_TO_NEXT_ROW
else:
remaining_allocated_amount = flt(row.allocated_amount - outstanding_amount, precision)
row.allocated_amount = outstanding_amount
outstanding_amount = 0
# create a new row without PR for remaining unallocated amount
new_row = frappe.copy_doc(row)
references.insert(row_number, new_row)
# update new row
new_row.idx = row_number + 1
new_row.payment_request = None
new_row.allocated_amount = remaining_allocated_amount
NEW_ROW_ADDED = True
row_number += TO_SKIP_NEW_ROW
@frappe.whitelist(allow_guest=True)
def make_payment_request(**args):
@@ -427,11 +515,15 @@ def make_payment_request(**args):
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
)
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
# fetches existing payment request `grand_total` amount
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
if existing_payment_request_amount:
grand_total -= existing_payment_request_amount
if not grand_total:
frappe.throw(_("Payment Request is already created"))
if draft_payment_request:
frappe.db.set_value(
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
@@ -445,6 +537,13 @@ def make_payment_request(**args):
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
)
party_type = args.get("party_type") or "Customer"
party_account_currency = ref_doc.get("party_account_currency")
if not party_account_currency:
party_account = get_party_account(party_type, ref_doc.get(party_type.lower()), ref_doc.company)
party_account_currency = get_account_currency(party_account)
pr.update(
{
"payment_gateway_account": gateway_account.get("name"),
@@ -453,6 +552,7 @@ def make_payment_request(**args):
"payment_channel": gateway_account.get("payment_channel"),
"payment_request_type": args.get("payment_request_type"),
"currency": ref_doc.currency,
"party_account_currency": party_account_currency,
"grand_total": grand_total,
"mode_of_payment": args.mode_of_payment,
"email_to": args.recipient_id or ref_doc.owner,
@@ -460,7 +560,7 @@ def make_payment_request(**args):
"message": gateway_account.get("message") or get_dummy_message(ref_doc),
"reference_doctype": args.dt,
"reference_name": args.dn,
"party_type": args.get("party_type") or "Customer",
"party_type": party_type,
"party": args.get("party") or ref_doc.get("customer"),
"bank_account": bank_account,
}
@@ -503,9 +603,11 @@ def get_amount(ref_doc, payment_account=None):
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if not ref_doc.get("is_pos"):
if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.grand_total)
grand_total = flt(ref_doc.rounded_total or ref_doc.grand_total)
else:
grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
grand_total = flt(
flt(ref_doc.base_rounded_total or ref_doc.base_grand_total) / ref_doc.conversion_rate
)
elif dt == "Sales Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
@@ -527,24 +629,20 @@ def get_amount(ref_doc, payment_account=None):
def get_existing_payment_request_amount(ref_dt, ref_dn):
"""
Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
and get the summation of existing paid payment request for Phone payment channel.
Return the total amount of Payment Requests against a reference document.
"""
existing_payment_request_amount = frappe.db.sql(
"""
select sum(grand_total)
from `tabPayment Request`
where
reference_doctype = %s
and reference_name = %s
and docstatus = 1
and (status != 'Paid'
or (payment_channel = 'Phone'
and status = 'Paid'))
""",
(ref_dt, ref_dn),
PR = frappe.qb.DocType("Payment Request")
response = (
frappe.qb.from_(PR)
.select(Sum(PR.grand_total))
.where(PR.reference_doctype == ref_dt)
.where(PR.reference_name == ref_dn)
.where(PR.docstatus == 1)
.run()
)
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
return response[0][0] if response[0] else 0
def get_gateway_details(args): # nosemgrep
@@ -592,41 +690,66 @@ def make_payment_entry(docname):
return doc.create_payment_entry(submit=False).as_dict()
def update_payment_req_status(doc, method):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details
def update_payment_requests_as_per_pe_references(references=None, cancel=False):
"""
Update Payment Request's `Status` and `Outstanding Amount` based on Payment Entry Reference's `Allocated Amount`.
"""
if not references:
return
for ref in doc.references:
payment_request_name = frappe.db.get_value(
"Payment Request",
{
"reference_doctype": ref.reference_doctype,
"reference_name": ref.reference_name,
"docstatus": 1,
},
precision = references[0].precision("allocated_amount")
referenced_payment_requests = frappe.get_all(
"Payment Request",
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
fields=[
"name",
"grand_total",
"outstanding_amount",
"payment_request_type",
],
)
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
for ref in references:
if not ref.payment_request:
continue
payment_request = referenced_payment_requests[ref.payment_request]
pr_outstanding = payment_request["outstanding_amount"]
# update outstanding amount
new_outstanding_amount = flt(
pr_outstanding + ref.allocated_amount if cancel else pr_outstanding - ref.allocated_amount,
precision,
)
if payment_request_name:
ref_details = get_reference_details(
ref.reference_doctype,
ref.reference_name,
doc.party_account_currency,
doc.party_type,
doc.party,
# to handle same payment request for the multiple allocations
payment_request["outstanding_amount"] = new_outstanding_amount
if not cancel and new_outstanding_amount < 0:
frappe.throw(
msg=_(
"The allocated amount is greater than the outstanding amount of Payment Request {0}"
).format(ref.payment_request),
title=_("Invalid Allocated Amount"),
)
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
status = pay_req_doc.status
if status != "Paid" and not ref_details.outstanding_amount:
status = "Paid"
elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount:
status = "Partially Paid"
elif ref_details.outstanding_amount == ref_details.total_amount:
if pay_req_doc.payment_request_type == "Outward":
status = "Initiated"
elif pay_req_doc.payment_request_type == "Inward":
status = "Requested"
# update 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:
status = "Paid"
elif new_outstanding_amount > 0:
status = "Partially Paid"
pay_req_doc.db_set("status", status)
# update database
frappe.db.set_value(
"Payment Request",
ref.payment_request,
{"outstanding_amount": new_outstanding_amount, "status": status},
)
def get_dummy_message(doc):
@@ -710,3 +833,36 @@ def validate_payment(doc, method=None):
doc.reference_docname
)
)
@frappe.whitelist()
def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len, filters):
# permission checks in `get_list()`
filters = frappe._dict(filters)
if not filters.reference_doctype or not filters.reference_name:
return []
if txt:
filters.name = ["like", f"%{txt}%"]
open_payment_requests = frappe.get_list(
"Payment Request",
filters={
**filters,
"status": ["!=", "Paid"],
"outstanding_amount": ["!=", 0], # for compatibility with old data
"docstatus": 1,
},
fields=["name", "grand_total", "outstanding_amount"],
order_by="transaction_date ASC,creation ASC",
)
return [
(
pr.name,
_("<strong>Grand Total:</strong> {0}").format(pr.grand_total),
_("<strong>Outstanding Amount:</strong> {0}").format(pr.outstanding_amount),
)
for pr in open_payment_requests
]

View File

@@ -1,11 +1,13 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import re
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests.utils import FrappeTestCase, change_settings
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
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
@@ -15,6 +17,7 @@ from erpnext.setup.utils import get_exchange_rate
test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"]
payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"}
payment_method = [
@@ -278,3 +281,246 @@ class TestPaymentRequest(FrappeTestCase):
self.assertEqual(pe.paid_amount, 800)
self.assertEqual(pe.base_received_amount, 800)
self.assertEqual(pe.received_amount, 10)
def test_multiple_payment_if_partially_paid_for_same_currency(self):
so = make_sales_order(currency="INR", qty=1, rate=1000)
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
self.assertEqual(pr.grand_total, 1000)
self.assertEqual(pr.outstanding_amount, pr.grand_total)
self.assertEqual(pr.party_account_currency, pr.currency) # INR
so.load_from_db()
# to make partial payment
pe = pr.create_payment_entry(submit=False)
pe.paid_amount = 200
pe.references[0].allocated_amount = 200
pe.submit()
self.assertEqual(pe.references[0].payment_request, pr.name)
so.load_from_db()
pr.load_from_db()
self.assertEqual(pr.status, "Partially Paid")
self.assertEqual(pr.outstanding_amount, 800)
self.assertEqual(pr.grand_total, 1000)
# complete payment
pe = pr.create_payment_entry()
self.assertEqual(pe.paid_amount, 800) # paid amount set from pr's outstanding amount
self.assertEqual(pe.references[0].allocated_amount, 800)
self.assertEqual(pe.references[0].outstanding_amount, 800) # for Orders it is not zero
self.assertEqual(pe.references[0].payment_request, pr.name)
so.load_from_db()
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
self.assertEqual(pr.outstanding_amount, 0)
self.assertEqual(pr.grand_total, 1000)
# creating a more payment Request must not allowed
self.assertRaisesRegex(
frappe.exceptions.ValidationError,
re.compile(r"Payment Request is already created"),
make_payment_request,
dt="Sales Order",
dn=so.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
@change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1})
def test_multiple_payment_if_partially_paid_for_multi_currency(self):
pi = make_purchase_invoice(currency="USD", conversion_rate=50, qty=1, rate=100, do_not_save=1)
pi.credit_to = "Creditors - _TC"
pi.submit()
pr = make_payment_request(
dt="Purchase Invoice",
dn=pi.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
# 100 USD -> 5000 INR
self.assertEqual(pr.grand_total, 100)
self.assertEqual(pr.outstanding_amount, 5000)
self.assertEqual(pr.currency, "USD")
self.assertEqual(pr.party_account_currency, "INR")
self.assertEqual(pr.status, "Initiated")
# to make partial payment
pe = pr.create_payment_entry(submit=False)
pe.paid_amount = 2000
pe.references[0].allocated_amount = 2000
pe.submit()
self.assertEqual(pe.references[0].payment_request, pr.name)
pr.load_from_db()
self.assertEqual(pr.status, "Partially Paid")
self.assertEqual(pr.outstanding_amount, 3000)
self.assertEqual(pr.grand_total, 100)
# complete payment
pe = pr.create_payment_entry()
self.assertEqual(pe.paid_amount, 3000) # paid amount set from pr's outstanding amount
self.assertEqual(pe.references[0].allocated_amount, 3000)
self.assertEqual(pe.references[0].outstanding_amount, 0) # for Invoices it will zero
self.assertEqual(pe.references[0].payment_request, pr.name)
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
self.assertEqual(pr.outstanding_amount, 0)
self.assertEqual(pr.grand_total, 100)
# creating a more payment Request must not allowed
self.assertRaisesRegex(
frappe.exceptions.ValidationError,
re.compile(r"Payment Request is already created"),
make_payment_request,
dt="Purchase Invoice",
dn=pi.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
def test_single_payment_with_payment_term_for_same_currency(self):
create_payment_terms_template()
po = create_purchase_order(do_not_save=1, currency="INR", qty=1, rate=20000)
po.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
po.save()
po.submit()
pr = make_payment_request(
dt="Purchase Order",
dn=po.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
self.assertEqual(pr.grand_total, 20000)
self.assertEqual(pr.outstanding_amount, pr.grand_total)
self.assertEqual(pr.party_account_currency, pr.currency) # INR
self.assertEqual(pr.status, "Initiated")
po.load_from_db()
pe = pr.create_payment_entry()
self.assertEqual(len(pe.references), 2)
self.assertEqual(pe.paid_amount, 20000)
# check 1st payment term
self.assertEqual(pe.references[0].allocated_amount, 16949.2)
self.assertEqual(pe.references[0].payment_request, pr.name)
# check 2nd payment term
self.assertEqual(pe.references[1].allocated_amount, 3050.8)
self.assertEqual(pe.references[1].payment_request, pr.name)
po.load_from_db()
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
self.assertEqual(pr.outstanding_amount, 0)
self.assertEqual(pr.grand_total, 20000)
@change_settings("Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1})
def test_single_payment_with_payment_term_for_multi_currency(self):
create_payment_terms_template()
si = create_sales_invoice(
do_not_save=1, currency="USD", debit_to="Debtors - _TC", qty=1, rate=200, conversion_rate=50
)
si.payment_terms_template = "Test Receivable Template" # 84.746 and 15.254
si.save()
si.submit()
pr = make_payment_request(
dt="Sales Invoice",
dn=si.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
# 200 USD -> 10000 INR
self.assertEqual(pr.grand_total, 200)
self.assertEqual(pr.outstanding_amount, 10000)
self.assertEqual(pr.currency, "USD")
self.assertEqual(pr.party_account_currency, "INR")
pe = pr.create_payment_entry()
self.assertEqual(len(pe.references), 2)
self.assertEqual(pe.paid_amount, 10000)
# check 1st payment term
# convert it via dollar and conversion_rate
self.assertEqual(pe.references[0].allocated_amount, 8474.5) # multi currency conversion
self.assertEqual(pe.references[0].payment_request, pr.name)
# check 2nd payment term
self.assertEqual(pe.references[1].allocated_amount, 1525.5) # multi currency conversion
self.assertEqual(pe.references[1].payment_request, pr.name)
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
self.assertEqual(pr.outstanding_amount, 0)
self.assertEqual(pr.grand_total, 200)
def test_payment_cancel_process(self):
so = make_sales_order(currency="INR", qty=1, rate=1000)
pr = make_payment_request(
dt="Sales Order",
dn=so.name,
mute_email=1,
submit_doc=1,
return_doc=1,
)
self.assertEqual(pr.grand_total, 1000)
self.assertEqual(pr.outstanding_amount, pr.grand_total)
so.load_from_db()
pe = pr.create_payment_entry(submit=False)
pe.paid_amount = 800
pe.references[0].allocated_amount = 800
pe.submit()
self.assertEqual(pe.references[0].payment_request, pr.name)
so.load_from_db()
pr.load_from_db()
self.assertEqual(pr.status, "Partially Paid")
self.assertEqual(pr.outstanding_amount, 200)
self.assertEqual(pr.grand_total, 1000)
# cancelling PE
pe.cancel()
pr.load_from_db()
self.assertEqual(pr.status, "Requested")
self.assertEqual(pr.outstanding_amount, 1000)
self.assertEqual(pr.grand_total, 1000)
so.load_from_db()

View File

@@ -40,7 +40,7 @@ class PeriodClosingVoucher(AccountsController):
)
if gle_count > 5000:
frappe.enqueue(
make_reverse_gl_entries,
process_cancellation,
voucher_type="Period Closing Voucher",
voucher_no=self.name,
queue="long",
@@ -51,9 +51,7 @@ class PeriodClosingVoucher(AccountsController):
alert=True,
)
else:
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
self.delete_closing_entries()
process_cancellation(voucher_type="Period Closing Voucher", voucher_no=self.name)
def validate_future_closing_vouchers(self):
if frappe.db.exists(
@@ -66,12 +64,6 @@ class PeriodClosingVoucher(AccountsController):
)
)
def delete_closing_entries(self):
closing_balance = frappe.qb.DocType("Account Closing Balance")
frappe.qb.from_(closing_balance).delete().where(
closing_balance.period_closing_voucher == self.name
).run()
def validate_account_head(self):
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
@@ -136,14 +128,7 @@ class PeriodClosingVoucher(AccountsController):
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
if len(gl_entries + closing_entries) > 3000:
frappe.enqueue(
process_gl_entries,
gl_entries=gl_entries,
voucher_name=self.name,
timeout=3000,
)
frappe.enqueue(
process_closing_entries,
process_gl_and_closing_entries,
gl_entries=gl_entries,
closing_entries=closing_entries,
voucher_name=self.name,
@@ -157,8 +142,9 @@ class PeriodClosingVoucher(AccountsController):
alert=True,
)
else:
process_gl_entries(gl_entries, self.name)
process_closing_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
process_gl_and_closing_entries(
gl_entries, closing_entries, self.name, self.company, self.posting_date
)
def get_grouped_gl_entries(self, get_opening_entries=False):
closing_entries = []
@@ -323,9 +309,10 @@ class PeriodClosingVoucher(AccountsController):
if get_opening_entries:
query = query.where(
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
| gl_entry.is_opening
== "Yes"
( # noqa: UP034
(gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date))
| (gl_entry.is_opening == "Yes")
)
)
else:
query = query.where(
@@ -343,12 +330,16 @@ class PeriodClosingVoucher(AccountsController):
return query.run(as_dict=1)
def process_gl_entries(gl_entries, voucher_name):
def process_gl_and_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
from erpnext.accounts.general_ledger import make_gl_entries
try:
if gl_entries:
make_gl_entries(gl_entries, merge_entries=False)
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
@@ -356,26 +347,21 @@ def process_gl_entries(gl_entries, voucher_name):
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
def process_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
try:
if gl_entries + closing_entries:
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
def make_reverse_gl_entries(voucher_type, voucher_no):
def process_cancellation(voucher_type, voucher_no):
from erpnext.accounts.general_ledger import make_reverse_gl_entries
try:
make_reverse_gl_entries(voucher_type=voucher_type, voucher_no=voucher_no)
delete_closing_entries(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")
def delete_closing_entries(voucher_no):
closing_balance = frappe.qb.DocType("Account Closing Balance")
frappe.qb.from_(closing_balance).delete().where(
closing_balance.period_closing_voucher == voucher_no
).run()

View File

@@ -80,8 +80,10 @@ frappe.ui.form.on("POS Closing Entry", {
) {
reset_values(frm);
frappe.run_serially([
() => frappe.dom.freeze(__("Loading Invoices! Please Wait...")),
() => frm.trigger("set_opening_amounts"),
() => frm.trigger("get_pos_invoices"),
() => frappe.dom.unfreeze(),
]);
}
},

View File

@@ -379,7 +379,7 @@ class POSInvoice(SalesInvoice):
if self.is_return and entry.amount > 0:
frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx))
if self.is_return:
if self.is_return and self.docstatus != 0:
invoice_total = self.rounded_total or self.grand_total
if total_amount_in_payments and total_amount_in_payments < invoice_total:
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))

View File

@@ -389,7 +389,9 @@ def split_invoices(invoices):
if not item.serial_no:
continue
return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against)
return_against_is_added = any(
d for d in _invoices if d and d[0].pos_invoice == pos_invoice.return_against
)
if return_against_is_added:
break

View File

@@ -359,7 +359,20 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
if isinstance(pricing_rule, str):
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
update_pricing_rule_uom(pricing_rule, args)
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or []
fetch_other_item = True if pricing_rule.apply_rule_on_other else False
pricing_rule.apply_rule_on_other_items = (
get_pricing_rule_items(pricing_rule, other_items=fetch_other_item) or []
)
if pricing_rule.coupon_code_based == 1:
if not args.coupon_code:
return item_details
coupon_code = frappe.db.get_value(
doctype="Coupon Code", filters={"pricing_rule": pricing_rule.name}, fieldname="name"
)
if args.coupon_code != coupon_code:
continue
if pricing_rule.get("suggestion"):
continue
@@ -386,9 +399,6 @@ def get_pricing_rule_for_item(args, doc=None, for_validate=False):
pricing_rule.apply_rule_on_other_items
)
if pricing_rule.coupon_code_based == 1 and args.coupon_code is None:
return item_details
if not pricing_rule.validate_applied_rule:
if pricing_rule.price_or_product_discount == "Price":
apply_price_discount_rule(pricing_rule, item_details, args)

View File

@@ -981,6 +981,12 @@ class TestPricingRule(FrappeTestCase):
self.assertEqual(so.items[1].item_code, "_Test Item")
self.assertEqual(so.items[1].qty, 3)
so = make_sales_order(item_code="_Test Item", qty=5, do_not_submit=1)
so.items[0].qty = 1
del so.items[-1]
so.save()
self.assertEqual(len(so.items), 1)
def test_apply_multiple_pricing_rules_for_discount_percentage_and_amount(self):
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")

View File

@@ -644,6 +644,9 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
if pricing_rule.round_free_qty:
qty = math.floor(qty)
if not qty:
return
free_item_data_args = {
"item_code": free_item,
"qty": qty,
@@ -712,14 +715,11 @@ def get_pricing_rule_items(pr_doc, other_items=False) -> list:
def validate_coupon_code(coupon_name):
coupon = frappe.get_doc("Coupon Code", coupon_name)
if coupon.valid_from:
if coupon.valid_from > getdate(today()):
frappe.throw(_("Sorry, this coupon code's validity has not started"))
elif coupon.valid_upto:
if coupon.valid_upto < getdate(today()):
frappe.throw(_("Sorry, this coupon code's validity has expired"))
elif coupon.used >= coupon.maximum_use:
if coupon.valid_from and coupon.valid_from > getdate(today()):
frappe.throw(_("Sorry, this coupon code's validity has not started"))
elif coupon.valid_upto and coupon.valid_upto < getdate(today()):
frappe.throw(_("Sorry, this coupon code's validity has expired"))
elif coupon.maximum_use and coupon.used >= coupon.maximum_use:
frappe.throw(_("Sorry, this coupon code is no longer valid"))

View File

@@ -25,6 +25,7 @@
"payment_terms_template",
"sales_partner",
"sales_person",
"show_remarks",
"based_on_payment_terms",
"section_break_3",
"customer_collection",
@@ -388,10 +389,16 @@
"fieldname": "ignore_cr_dr_notes",
"fieldtype": "Check",
"label": "Ignore System Generated Credit / Debit Notes"
},
{
"default": "0",
"fieldname": "show_remarks",
"fieldtype": "Check",
"label": "Show Remarks"
}
],
"links": [],
"modified": "2024-08-13 10:41:18.381165",
"modified": "2024-10-18 17:51:39.108481",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -132,6 +132,7 @@ def get_common_filters(doc):
"finance_book": doc.finance_book if doc.finance_book else None,
"account": [doc.account] if doc.account else None,
"cost_center": [cc.cost_center_name for cc in doc.cost_center],
"show_remarks": doc.show_remarks,
}
)

View File

@@ -5,6 +5,8 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import IfNull
pricing_rule_fields = [
"apply_on",
@@ -91,22 +93,50 @@ class PromotionalScheme(Document):
if self.is_new():
return
transaction_exists = False
docnames = []
invalid_pricing_rule = self.get_invalid_pricing_rules()
# If user has changed applicable for
if self.get_doc_before_save() and self.get_doc_before_save().applicable_for == self.applicable_for:
if not invalid_pricing_rule:
return
docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name})
if frappe.db.exists(
"Pricing Rule Detail",
{
"pricing_rule": ["in", invalid_pricing_rule],
"docstatus": ["<", 2],
},
):
raise_for_transaction_exists(self.name)
for docname in docnames:
if frappe.db.exists("Pricing Rule Detail", {"pricing_rule": docname.name, "docstatus": ("<", 2)}):
raise_for_transaction_exists(self.name)
for doc in invalid_pricing_rule:
frappe.delete_doc("Pricing Rule", doc)
if docnames and not transaction_exists:
for docname in docnames:
frappe.delete_doc("Pricing Rule", docname.name)
frappe.msgprint(
_("The following invalid Pricing Rules are deleted:")
+ "<br><br><ul><li>"
+ "</li><li>".join(invalid_pricing_rule)
+ "</li></ul>"
)
def get_invalid_pricing_rules(self):
pr = frappe.qb.DocType("Pricing Rule")
conditions = []
conditions.append(pr.promotional_scheme == self.name)
if self.applicable_for:
applicable_for = frappe.scrub(self.applicable_for)
applicable_for_list = [d.get(applicable_for) for d in self.get(applicable_for)]
conditions.append(
(IfNull(pr.applicable_for, "") != self.applicable_for)
| (
(IfNull(pr.applicable_for, "") == self.applicable_for)
& IfNull(pr[applicable_for], "").notin(applicable_for_list)
)
)
else:
conditions.append(IfNull(pr.applicable_for, "") != "")
return frappe.qb.from_(pr).select(pr.name).where(Criterion.all(conditions)).run(pluck=True)
def on_update(self):
self.validate()

View File

@@ -90,6 +90,31 @@ class TestPromotionalScheme(unittest.TestCase):
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
self.assertEqual(price_rules, [])
def test_change_applicable_for_values_in_promotional_scheme(self):
ps = make_promotional_scheme(applicable_for="Customer", customer="_Test Customer")
ps.append("customer", {"customer": "_Test Customer 2"})
ps.save()
price_rules = frappe.get_all(
"Pricing Rule", filters={"promotional_scheme": ps.name, "applicable_for": "Customer"}
)
self.assertTrue(len(price_rules), 2)
ps.set("customer", [])
ps.append("customer", {"customer": "_Test Customer 2"})
ps.save()
price_rules = frappe.get_all(
"Pricing Rule",
filters={
"promotional_scheme": ps.name,
"applicable_for": "Customer",
"customer": "_Test Customer",
},
)
self.assertEqual(price_rules, [])
frappe.delete_doc("Promotional Scheme", ps.name)
def test_min_max_amount_configuration(self):
ps = make_promotional_scheme()
ps.price_discount_slabs[0].min_amount = 10

View File

@@ -25,6 +25,13 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
};
});
this.frm.set_query("expense_account", "items", function () {
return {
query: "erpnext.controllers.queries.get_expense_account",
filters: { company: doc.company },
};
});
}
onload() {
@@ -303,8 +310,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
party_type: "Supplier",
account: this.frm.doc.credit_to,
price_list: this.frm.doc.buying_price_list,
fetch_payment_terms_template: cint(!this.frm.doc.ignore_default_payment_terms_template)
}, function() {
fetch_payment_terms_template: cint(
(this.frm.doc.is_return == 0) & !this.frm.doc.ignore_default_payment_terms_template
),
},
function () {
me.apply_pricing_rule();
me.frm.doc.apply_tds = me.frm.supplier_tds ? 1 : 0;
me.frm.doc.tax_withholding_category = me.frm.supplier_tds;
@@ -469,13 +479,6 @@ cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn)
}
}
cur_frm.set_query("expense_account", "items", function(doc) {
return {
query: "erpnext.controllers.queries.get_expense_account",
filters: {'company': doc.company }
}
});
cur_frm.set_query("wip_composite_asset", "items", function() {
return {
filters: {'is_composite_asset': 1, 'docstatus': 0 }

View File

@@ -1128,12 +1128,14 @@
"label": "Payment Terms"
},
{
"depends_on": "eval:(!doc.is_paid && !doc.is_return)",
"fieldname": "payment_terms_template",
"fieldtype": "Link",
"label": "Payment Terms Template",
"options": "Payment Terms Template"
},
{
"depends_on": "eval:(!doc.is_paid && !doc.is_return)",
"fieldname": "payment_schedule",
"fieldtype": "Table",
"label": "Payment Schedule",
@@ -1611,7 +1613,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2024-09-11 12:59:19.130593",
"modified": "2024-10-25 18:13:01.944477",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -280,8 +280,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
party_type: "Customer",
account: this.frm.doc.debit_to,
price_list: this.frm.doc.selling_price_list,
pos_profile: pos_profile
}, function() {
pos_profile: pos_profile,
fetch_payment_terms_template: cint(
(this.frm.doc.is_return == 0) & !this.frm.doc.ignore_default_payment_terms_template
),
},
function () {
me.apply_pricing_rule();
});

View File

@@ -170,6 +170,7 @@ class SalesInvoice(SellingController):
):
validate_loyalty_points(self, self.loyalty_points)
self.allow_write_off_only_on_pos()
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_accounts(self):
@@ -850,6 +851,10 @@ class SalesInvoice(SellingController):
raise_exception=1,
)
def allow_write_off_only_on_pos(self):
if not self.is_pos and self.write_off_account:
self.write_off_account = None
def validate_write_off_account(self):
if flt(self.write_off_amount) and not self.write_off_account:
self.write_off_account = frappe.get_cached_value("Company", self.company, "write_off_account")

View File

@@ -295,7 +295,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_amount = 0
else:
# if no TCS has been charged in FY,
# then chargeable value is "prev invoices + advances" value which cross the threshold
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
if cint(tax_details.round_off_tax_amount):
@@ -382,6 +382,9 @@ def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, pa
Use Payment Ledger to fetch unallocated Advance Payments
"""
if party_type == "Supplier":
return []
ple = qb.DocType("Payment Ledger Entry")
conditions = []
@@ -479,7 +482,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
payment_entry_filters.pop("apply_tax_withholding_amount", None)
payment_entry_filters.pop("tax_withholding_category", None)
supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
supp_inv_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0
supp_jv_credit_amt = (
frappe.db.get_value(
@@ -503,7 +506,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
group_by="payment_type",
)
supp_credit_amt += supp_jv_credit_amt
supp_credit_amt = supp_jv_credit_amt
supp_credit_amt += inv.tax_withholding_net_total
for type in payment_entry_amounts:
@@ -521,19 +524,19 @@ def get_tds_amount(ldc, parties, inv, tax_details, vouchers):
tax_withholding_net_total = inv.tax_withholding_net_total
if (threshold and tax_withholding_net_total >= threshold) or (
cumulative_threshold and supp_credit_amt >= cumulative_threshold
cumulative_threshold and (supp_credit_amt + supp_inv_credit_amt) >= cumulative_threshold
):
# Get net total again as TDS is calculated on net total
# Grand is used to just check for threshold breach
net_total = (
frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)") or 0.0
)
supp_credit_amt += net_total
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
tax_details.tax_on_excess_amount
):
# Get net total again as TDS is calculated on net total
# Grand is used to just check for threshold breach
net_total = (
frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(tax_withholding_net_total)")
or 0.0
)
net_total += inv.tax_withholding_net_total
supp_credit_amt = net_total - cumulative_threshold
supp_credit_amt = net_total + tax_withholding_net_total - cumulative_threshold
if ldc and is_valid_certificate(ldc, inv.get("posting_date") or inv.get("transaction_date"), 0):
tds_amount = get_lower_deduction_amount(
@@ -575,8 +578,6 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
conditions.append(ple.voucher_no == ple.against_voucher_no)
conditions.append(ple.company == inv.company)
(qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run(as_list=1))
advance_amt = (
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
)
@@ -599,9 +600,12 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
)
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
advance_adjusted = get_advance_adjusted_in_invoice(inv)
current_invoice_total = get_invoice_total_without_tcs(inv, tax_details)
total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt
total_invoiced_amt = (
current_invoice_total + invoiced_amt + advance_amt - credit_note_amt - advance_adjusted
)
if cumulative_threshold and total_invoiced_amt >= cumulative_threshold:
chargeable_amt = total_invoiced_amt - cumulative_threshold
@@ -610,6 +614,14 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
return tcs_amount
def get_advance_adjusted_in_invoice(inv):
advances_adjusted = 0
for row in inv.get("advances", []):
advances_adjusted += row.allocated_amount
return advances_adjusted
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_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0

View File

@@ -121,6 +121,85 @@ class TestTaxWithholdingCategory(FrappeTestCase):
for d in reversed(invoices):
d.cancel()
def test_cumulative_threshold_with_party_ledger_amount_on_net_total(self):
invoices = []
frappe.db.set_value(
"Supplier", "Test TDS Supplier3", "tax_withholding_category", "Advance TDS Category"
)
# Invoice with tax and without exceeding single and cumulative thresholds
for _ in range(2):
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=1000, do_not_save=True)
pi.apply_tds = 1
pi.append(
"taxes",
{
"category": "Total",
"charge_type": "Actual",
"account_head": "_Test Account VAT - _TC",
"cost_center": "Main - _TC",
"tax_amount": 500,
"description": "Test",
"add_deduct_tax": "Add",
},
)
pi.save()
pi.submit()
invoices.append(pi)
# Third Invoice exceeds single threshold and not exceeding cumulative threshold
pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=6000)
pi1.apply_tds = 1
pi1.save()
pi1.submit()
invoices.append(pi1)
# Cumulative threshold is 10,000
# Threshold calculation should be only on the third invoice
self.assertEqual(pi1.taxes[0].tax_amount, 800)
for d in reversed(invoices):
d.cancel()
def test_cumulative_threshold_with_tax_on_excess_amount(self):
invoices = []
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
# Invoice with tax and without exceeding single and cumulative thresholds
for _ in range(2):
pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=10000, do_not_save=True)
pi.apply_tds = 1
pi.append(
"taxes",
{
"category": "Total",
"charge_type": "Actual",
"account_head": "_Test Account VAT - _TC",
"cost_center": "Main - _TC",
"tax_amount": 500,
"description": "Test",
"add_deduct_tax": "Add",
},
)
pi.save()
pi.submit()
invoices.append(pi)
# Third Invoice exceeds single threshold and not exceeding cumulative threshold
pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000)
pi1.apply_tds = 1
pi1.save()
pi1.submit()
invoices.append(pi1)
# Cumulative threshold is 10,000
# Threshold calculation should be only on the third invoice
self.assertTrue(len(pi1.taxes) > 0)
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
for d in reversed(invoices):
d.cancel()
def test_cumulative_threshold_tcs(self):
frappe.db.set_value(
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
@@ -214,6 +293,50 @@ class TestTaxWithholdingCategory(FrappeTestCase):
d.reload()
d.cancel()
@change_settings(
"Accounts Settings",
{"unlink_payment_on_cancellation_of_invoice": 1},
)
def test_tcs_on_allocated_advance_payments(self):
frappe.db.set_value(
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
)
vouchers = []
# create advance payment
pe = create_payment_entry(
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=30000
)
pe.paid_from = "Debtors - _TC"
pe.paid_to = "Cash - _TC"
pe.submit()
vouchers.append(pe)
si = create_sales_invoice(customer="Test TCS Customer", rate=50000)
advances = si.get_advance_entries()
si.append(
"advances",
{
"reference_type": advances[0].reference_type,
"reference_name": advances[0].reference_name,
"advance_amount": advances[0].amount,
"allocated_amount": 30000,
},
)
si.submit()
vouchers.append(si)
# assert tax collection on total invoice ,advance payment adjusted should be excluded.
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == "TCS - _TC"])
# tcs = (inv amt)50000+(adv amt)30000-(adv adj) 30000 - threshold(30000) * rate 10%
self.assertEqual(tcs_charged, 2000)
# cancel invoice and payments to avoid clashing
for d in reversed(vouchers):
d.reload()
d.cancel()
def test_tds_calculation_on_net_total(self):
frappe.db.set_value(
"Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS"

View File

@@ -1,7 +1,5 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:UNREC-{#####}",
"creation": "2023-08-22 10:26:34.421423",
"default_view": "List",
"doctype": "DocType",
@@ -58,11 +56,10 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-08-28 17:42:50.261377",
"modified": "2024-10-10 12:03:50.022444",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Unreconcile Payment",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{

View File

@@ -305,8 +305,8 @@ class ReceivablePayableReport:
must_consider = False
if self.filters.get("for_revaluation_journals"):
if (abs(row.outstanding) >= 0.0 / 10**self.currency_precision) or (
abs(row.outstanding_in_account_currency) >= 0.0 / 10**self.currency_precision
if (abs(row.outstanding) >= 1.0 / 10**self.currency_precision) or (
abs(row.outstanding_in_account_currency) >= 1.0 / 10**self.currency_precision
):
must_consider = True
else:
@@ -500,7 +500,8 @@ class ReceivablePayableReport:
from `tab{row.voucher_type}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and
si.name = %s
si.name = %s and
si.is_return = 0
order by ps.paid_amount desc, due_date
""",
row.voucher_no,

View File

@@ -95,7 +95,7 @@ def execute(filters=None):
filters.periodicity, period_list, filters.accumulated_values, company=filters.company
)
chart = get_chart_data(filters, columns, asset, liability, equity)
chart = get_chart_data(filters, columns, asset, liability, equity, currency)
report_summary, primitive_summary = get_report_summary(
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
@@ -219,7 +219,7 @@ def get_report_summary(
], (net_asset - net_liability + net_equity)
def get_chart_data(filters, columns, asset, liability, equity):
def get_chart_data(filters, columns, asset, liability, equity, currency):
labels = [d.get("label") for d in columns[2:]]
asset_data, liability_data, equity_data = [], [], []
@@ -247,4 +247,8 @@ def get_chart_data(filters, columns, asset, liability, equity):
else:
chart["type"] = "line"
chart["fieldtype"] = "Currency"
chart["options"] = "currency"
chart["currency"] = currency
return chart

View File

@@ -47,7 +47,7 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
},
],
formatter: function (value, row, column, data, default_formatter, filter) {
if (column.fieldname == "payment_entry" && value == "Cheques and Deposits incorrectly cleared") {
if (column.fieldname == "payment_entry" && value == __("Cheques and Deposits incorrectly cleared")) {
column.link_onclick =
"frappe.query_reports['Bank Reconciliation Statement'].open_utility_report()";
}

View File

@@ -116,7 +116,7 @@ def execute(filters=None):
)
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company)
chart = get_chart_data(columns, data)
chart = get_chart_data(columns, data, company_currency)
report_summary = get_report_summary(summary_data, company_currency)
@@ -257,7 +257,7 @@ def get_report_summary(summary_data, currency):
return report_summary
def get_chart_data(columns, data):
def get_chart_data(columns, data, currency):
labels = [d.get("label") for d in columns[2:]]
datasets = [
{
@@ -272,5 +272,7 @@ def get_chart_data(columns, data):
chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"}
chart["fieldtype"] = "Currency"
chart["options"] = "currency"
chart["currency"] = currency
return chart

View File

@@ -120,7 +120,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
True,
)
chart = get_chart_data(filters, columns, asset, liability, equity)
chart = get_chart_data(filters, columns, asset, liability, equity, company_currency)
return data, message, chart, report_summary
@@ -178,7 +178,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
if net_profit_loss:
data.append(net_profit_loss)
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss)
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss, company_currency)
report_summary, primitive_summary = get_pl_summary(
companies, "", income, expense, net_profit_loss, company_currency, filters, True
@@ -474,10 +474,13 @@ def update_parent_account_names(accounts):
for d in accounts:
if d.account_number:
account_name = d.account_number + " - " + d.account_name
account_key = d.account_number + " - " + d.account_name
else:
account_name = d.account_name
name_to_account_map[d.name] = account_name
account_key = d.account_name
d.account_key = account_key
name_to_account_map[d.name] = account_key
for account in accounts:
if account.parent_account:
@@ -510,33 +513,26 @@ def get_subsidiary_companies(company):
def get_accounts(root_type, companies):
accounts = []
added_accounts = []
for company in companies:
for account in frappe.get_all(
"Account",
fields=[
"name",
"is_group",
"company",
"parent_account",
"lft",
"rgt",
"root_type",
"report_type",
"account_name",
"account_number",
],
filters={"company": company, "root_type": root_type},
):
if account.account_number:
account_key = account.account_number + "-" + account.account_name
else:
account_key = account.account_name
if account_key not in added_accounts:
accounts.append(account)
added_accounts.append(account_key)
accounts.extend(
frappe.get_all(
"Account",
fields=[
"name",
"is_group",
"company",
"parent_account",
"lft",
"rgt",
"root_type",
"report_type",
"account_name",
"account_number",
],
filters={"company": company, "root_type": root_type},
)
)
return accounts
@@ -775,15 +771,17 @@ def add_total_row(out, root_type, balance_must_be, companies, company_currency):
def filter_accounts(accounts, depth=10):
parent_children_map = {}
accounts_by_name = {}
for d in accounts:
if d.account_number:
account_name = d.account_number + " - " + d.account_name
else:
account_name = d.account_name
d["company_wise_opening_bal"] = defaultdict(float)
accounts_by_name[account_name] = d
added_accounts = []
parent_children_map.setdefault(d.parent_account or None, []).append(d)
for d in accounts:
if d.account_key in added_accounts:
continue
added_accounts.append(d.account_key)
d["company_wise_opening_bal"] = defaultdict(float)
accounts_by_name[d.account_key] = d
parent_children_map.setdefault(d.parent_account_name or None, []).append(d)
filtered_accounts = []
@@ -795,7 +793,7 @@ def filter_accounts(accounts, depth=10):
for child in children:
child.indent = level
filtered_accounts.append(child)
add_to_list(child.name, level + 1)
add_to_list(child.account_key, level + 1)
add_to_list(None, 0)

View File

@@ -122,21 +122,24 @@ class Deferred_Item:
"""
simulate future posting by creating dummy gl entries. starts from the last posting date.
"""
if self.service_start_date != self.service_end_date:
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
self.estimate_for_period_list = get_period_list(
self.filters.from_fiscal_year,
self.filters.to_fiscal_year,
add_days(self.last_entry_date, 1),
self.period_list[-1].to_date,
"Date Range",
"Monthly",
company=self.filters.company,
)
for period in self.estimate_for_period_list:
amount = self.calculate_amount(period.from_date, period.to_date)
gle = self.make_dummy_gle(period.key, period.to_date, amount)
self.gle_entries.append(gle)
if (
self.service_start_date != self.service_end_date
and add_days(self.last_entry_date, 1) < self.service_end_date
):
self.estimate_for_period_list = get_period_list(
self.filters.from_fiscal_year,
self.filters.to_fiscal_year,
add_days(self.last_entry_date, 1),
self.service_end_date,
"Date Range",
"Monthly",
company=self.filters.company,
)
for period in self.estimate_for_period_list:
amount = self.calculate_amount(period.from_date, period.to_date)
gle = self.make_dummy_gle(period.key, period.to_date, amount)
self.gle_entries.append(gle)
def calculate_item_revenue_expense_for_period(self):
"""

View File

@@ -199,8 +199,7 @@ class General_Payment_Ledger_Comparison:
dict(
label=_("Voucher Type"),
fieldname="voucher_type",
fieldtype="Link",
options="DocType",
fieldtype="Data",
width="100",
)
)
@@ -219,8 +218,7 @@ class General_Payment_Ledger_Comparison:
dict(
label=_("Party Type"),
fieldname="party_type",
fieldtype="Link",
options="DocType",
fieldtype="Data",
width="100",
)
)

View File

@@ -337,9 +337,17 @@ def get_accounts_with_children(accounts):
return list(set(all_accounts)) if all_accounts else None
def set_bill_no(gl_entries):
inv_details = get_supplier_invoice_details()
for gl in gl_entries:
gl["bill_no"] = inv_details.get(gl.get("against_voucher"), "")
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
data = []
set_bill_no(gl_entries)
gle_map = initialize_gle_map(gl_entries, filters)
totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map)
@@ -513,7 +521,6 @@ def get_account_type_map(company):
def get_result_as_list(data, filters):
balance, _balance_in_account_currency = 0, 0
inv_details = get_supplier_invoice_details()
for d in data:
if not d.get("posting_date"):
@@ -523,7 +530,6 @@ def get_result_as_list(data, filters):
d["balance"] = balance
d["account_currency"] = filters.account_currency
d["bill_no"] = inv_details.get(d.get("against_voucher"), "")
return data

View File

@@ -210,7 +210,7 @@ class PaymentLedger:
)
)
self.columns.append(
dict(label=_("Currency"), fieldname="currency", fieldtype="Currency", hidden=True)
dict(label=_("Currency"), fieldname="currency", fieldtype="Link", options="Currency", hidden=True)
)
def run(self):

View File

@@ -59,11 +59,11 @@ def execute(filters=None):
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company)
chart = get_chart_data(filters, columns, income, expense, net_profit_loss)
currency = filters.presentation_currency or frappe.get_cached_value(
"Company", filters.company, "default_currency"
)
chart = get_chart_data(filters, columns, income, expense, net_profit_loss, currency)
report_summary, primitive_summary = get_report_summary(
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
)
@@ -141,7 +141,7 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
return net_profit_loss
def get_chart_data(filters, columns, income, expense, net_profit_loss):
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
labels = [d.get("label") for d in columns[2:]]
income_data, expense_data, net_profit = [], [], []
@@ -170,5 +170,7 @@ def get_chart_data(filters, columns, income, expense, net_profit_loss):
chart["type"] = "line"
chart["fieldtype"] = "Currency"
chart["options"] = "currency"
chart["currency"] = currency
return chart

View File

@@ -311,6 +311,7 @@ def get_account_columns(invoice_list, include_payments):
"""select distinct expense_account
from `tabPurchase Invoice Item` where docstatus = 1
and (expense_account is not null and expense_account != '')
and parenttype='Purchase Invoice'
and parent in (%s) order by expense_account"""
% ", ".join(["%s"] * len(invoice_list)),
tuple([inv.name for inv in invoice_list]),
@@ -443,7 +444,7 @@ def get_invoice_expense_map(invoice_list):
"""
select parent, expense_account, sum(base_net_amount) as amount
from `tabPurchase Invoice Item`
where parent in (%s)
where parent in (%s) and parenttype='Purchase Invoice'
group by parent, expense_account
"""
% ", ".join(["%s"] * len(invoice_list)),
@@ -514,7 +515,7 @@ def get_invoice_po_pr_map(invoice_list):
"""
select parent, purchase_order, purchase_receipt, po_detail, project
from `tabPurchase Invoice Item`
where parent in (%s)
where parent in (%s) and parenttype='Purchase Invoice'
"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),

View File

@@ -522,7 +522,8 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts, inclu
tax_details = frappe.db.sql(
"""select parent, account_head,
sum(base_tax_amount_after_discount_amount) as tax_amount
from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head"""
from `tabSales Taxes and Charges` where parent in (%s) and parenttype = 'Sales Invoice'
group by parent, account_head"""
% ", ".join(["%s"] * len(invoice_list)),
tuple(inv.name for inv in invoice_list),
as_dict=1,

View File

@@ -0,0 +1,179 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import getdate, today
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.sales_register.sales_register import execute
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.create_customer()
self.create_item()
self.create_child_cost_center()
def tearDown(self):
frappe.db.rollback()
def create_child_cost_center(self):
cc_name = "South Wing"
if frappe.db.exists("Cost Center", cc_name):
cc = frappe.get_doc("Cost Center", cc_name)
else:
parent = frappe.db.get_value("Cost Center", self.cost_center, "parent_cost_center")
cc = frappe.get_doc(
{
"doctype": "Cost Center",
"company": self.company,
"is_group": False,
"parent_cost_center": parent,
"cost_center_name": cc_name,
}
)
cc = cc.save()
self.south_cc = cc.name
def create_sales_invoice(self, rate=100, do_not_submit=False):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=rate,
price_list_rate=rate,
do_not_save=1,
)
si = si.save()
if not do_not_submit:
si = si.submit()
return si
def test_basic_report_output(self):
si = self.create_sales_invoice(rate=98)
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
report = execute(filters)
res = [x for x in report[1] if x.get("voucher_no") == si.name]
expected_result = {
"voucher_type": si.doctype,
"voucher_no": si.name,
"posting_date": getdate(),
"customer": self.customer,
"receivable_account": self.debit_to,
"net_total": 98.0,
"grand_total": 98.0,
"debit": 98.0,
}
report_output = {k: v for k, v in res[0].items() if k in expected_result}
self.assertDictEqual(report_output, expected_result)
def test_journal_with_cost_center_filter(self):
je1 = frappe.get_doc(
{
"doctype": "Journal Entry",
"voucher_type": "Journal Entry",
"company": self.company,
"posting_date": getdate(),
"accounts": [
{
"account": self.debit_to,
"party_type": "Customer",
"party": self.customer,
"credit_in_account_currency": 77,
"credit": 77,
"is_advance": "Yes",
"cost_center": self.cost_center,
},
{
"account": self.cash,
"debit_in_account_currency": 77,
"debit": 77,
},
],
}
)
je1.submit()
je2 = frappe.get_doc(
{
"doctype": "Journal Entry",
"voucher_type": "Journal Entry",
"company": self.company,
"posting_date": getdate(),
"accounts": [
{
"account": self.debit_to,
"party_type": "Customer",
"party": self.customer,
"credit_in_account_currency": 98,
"credit": 98,
"is_advance": "Yes",
"cost_center": self.south_cc,
},
{
"account": self.cash,
"debit_in_account_currency": 98,
"debit": 98,
},
],
}
)
je2.submit()
filters = frappe._dict(
{
"from_date": today(),
"to_date": today(),
"company": self.company,
"include_payments": True,
"customer": self.customer,
"cost_center": self.cost_center,
}
)
report_output = execute(filters)[1]
filtered_output = [x for x in report_output if x.get("voucher_no") == je1.name]
self.assertEqual(len(filtered_output), 1)
expected_result = {
"voucher_type": je1.doctype,
"voucher_no": je1.name,
"posting_date": je1.posting_date,
"customer": self.customer,
"receivable_account": self.debit_to,
"net_total": 77.0,
"credit": 77.0,
}
result_fields = {k: v for k, v in filtered_output[0].items() if k in expected_result}
self.assertDictEqual(result_fields, expected_result)
filters = frappe._dict(
{
"from_date": today(),
"to_date": today(),
"company": self.company,
"include_payments": True,
"customer": self.customer,
"cost_center": self.south_cc,
}
)
report_output = execute(filters)[1]
filtered_output = [x for x in report_output if x.get("voucher_no") == je2.name]
self.assertEqual(len(filtered_output), 1)
expected_result = {
"voucher_type": je2.doctype,
"voucher_no": je2.name,
"posting_date": je2.posting_date,
"customer": self.customer,
"receivable_account": self.debit_to,
"net_total": 98.0,
"credit": 98.0,
}
result_output = {k: v for k, v in filtered_output[0].items() if k in expected_result}
self.assertDictEqual(result_output, expected_result)

View File

@@ -255,7 +255,9 @@ def get_journal_entries(filters, args):
)
.orderby(je.posting_date, je.name, order=Order.desc)
)
query = apply_common_conditions(filters, query, doctype="Journal Entry", payments=True)
query = apply_common_conditions(
filters, query, doctype="Journal Entry", child_doctype="Journal Entry Account", payments=True
)
journal_entries = query.run(as_dict=True)
return journal_entries
@@ -306,7 +308,9 @@ def apply_common_conditions(filters, query, doctype, child_doctype=None, payment
query = query.where(parent_doc.posting_date <= filters.to_date)
if payments:
if filters.get("cost_center"):
if doctype == "Journal Entry" and filters.get("cost_center"):
query = query.where(child_doc.cost_center == filters.cost_center)
elif filters.get("cost_center"):
query = query.where(parent_doc.cost_center == filters.cost_center)
else:
if filters.get("cost_center"):
@@ -326,6 +330,7 @@ def apply_common_conditions(filters, query, doctype, child_doctype=None, payment
if join_required:
query = query.inner_join(child_doc).on(parent_doc.name == child_doc.parent)
query = query.where(child_doc.parenttype == doctype)
query = query.distinct()
if parent_doc.get_table_name() != "tabJournal Entry":

View File

@@ -579,6 +579,16 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"):
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
rev_dr_or_cr = (
"debit_in_account_currency"
if d["dr_or_cr"] == "credit_in_account_currency"
else "credit_in_account_currency"
)
if jv_detail.get(rev_dr_or_cr):
d["dr_or_cr"] = rev_dr_or_cr
d["allocated_amount"] = d["allocated_amount"] * -1
d["unadjusted_amount"] = d["unadjusted_amount"] * -1
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
# adjust the unreconciled balance
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
@@ -1524,12 +1534,16 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
return matched
def get_stock_accounts(company, voucher_type=None, voucher_no=None):
def get_stock_accounts(company, voucher_type=None, voucher_no=None, accounts=None):
stock_accounts = [
d.name
for d in frappe.db.get_all("Account", {"account_type": "Stock", "company": company, "is_group": 0})
]
if voucher_type and voucher_no:
if accounts:
stock_accounts = [row.account for row in accounts if row.account in stock_accounts]
elif voucher_type and voucher_no:
if voucher_type == "Journal Entry":
stock_accounts = [
d.account
@@ -1926,6 +1940,7 @@ class QueryPaymentLedger:
ple.cost_center.as_("cost_center"),
Sum(ple.amount).as_("amount"),
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
ple.remarks,
)
.where(ple.delinked == 0)
.where(Criterion.all(filter_on_voucher_no))
@@ -1988,6 +2003,7 @@ class QueryPaymentLedger:
Table("vouchers").due_date,
Table("vouchers").currency,
Table("vouchers").cost_center.as_("cost_center"),
Table("vouchers").remarks,
)
.where(Criterion.all(filter_on_outstanding_amount))
)

View File

@@ -202,7 +202,7 @@ frappe.ui.form.on("Asset", {
<div class="row">
<div class="col-xs-12 col-sm-6">
<span class="indicator whitespace-nowrap red">
<span>Failed to post depreciation entries</span>
<span>${__("Failed to post depreciation entries")}</span>
</span>
</div>
</div>`;

View File

@@ -456,9 +456,11 @@ class PurchaseOrder(BuyingController):
if not self.is_against_so():
return
for item in removed_items:
prev_ordered_qty = frappe.get_cached_value(
"Sales Order Item", item.get("sales_order_item"), "ordered_qty"
prev_ordered_qty = (
frappe.get_cached_value("Sales Order Item", item.get("sales_order_item"), "ordered_qty")
or 0.0
)
frappe.db.set_value(
"Sales Order Item", item.get("sales_order_item"), "ordered_qty", prev_ordered_qty - item.qty
)

View File

@@ -14,15 +14,26 @@ def get_data():
"Material Request": ["items", "material_request"],
"Supplier Quotation": ["items", "supplier_quotation"],
"Project": ["items", "project"],
"Sales Order": ["items", "sales_order"],
"BOM": ["items", "bom"],
"Production Plan": ["items", "production_plan"],
"Blanket Order": ["items", "blanket_order"],
},
"transactions": [
{"label": _("Related"), "items": ["Purchase Receipt", "Purchase Invoice"]},
{"label": _("Related"), "items": ["Purchase Receipt", "Purchase Invoice", "Sales Order"]},
{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry", "Payment Request"]},
{
"label": _("Reference"),
"items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"],
"items": ["Supplier Quotation", "Project", "Auto Repeat"],
},
{
"label": _("Manufacturing"),
"items": ["Material Request", "BOM", "Production Plan", "Blanket Order"],
},
{
"label": _("Sub-contracting"),
"items": ["Subcontracting Order", "Stock Entry"],
},
{"label": _("Sub-contracting"), "items": ["Subcontracting Order", "Stock Entry"]},
{"label": _("Internal"), "items": ["Sales Order"]},
],
}

View File

@@ -137,8 +137,8 @@ frappe.ui.form.on("Request for Quotation",{
return;
}
},
"Download PDF for Supplier",
"Download"
__("Download PDF for Supplier"),
__("Download")
);
},
__("Tools")
@@ -257,8 +257,10 @@ frappe.ui.form.on("Request for Quotation",{
});
};
dialog.fields_dict.note.$wrapper.append(`<p class="small text-muted">This is a preview of the email to be sent. A PDF of the document will
automatically be attached with the email.</p>`);
const msg = __(
"This is a preview of the email to be sent. A PDF of the document will automatically be attached with the email."
);
dialog.fields_dict.note.$wrapper.append(`<p class="small text-muted">${msg}</p>`);
dialog.show();
}

View File

@@ -392,6 +392,7 @@ def create_rfq_items(sq_doc, supplier, data):
"material_request",
"material_request_item",
"stock_qty",
"uom",
]:
args[field] = data.get(field)

View File

@@ -357,12 +357,15 @@ class AccountsController(TransactionBase):
def validate_return_against_account(self):
if self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against:
cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to"
cr_dr_account_label = "Debit To" if self.doctype == "Sales Invoice" else "Credit To"
cr_dr_account = self.get(cr_dr_account_field)
if frappe.get_value(self.doctype, self.return_against, cr_dr_account_field) != cr_dr_account:
original_account = frappe.get_value(self.doctype, self.return_against, cr_dr_account_field)
if original_account != self.get(cr_dr_account_field):
frappe.throw(
_("'{0}' account: '{1}' should match the Return Against Invoice").format(
frappe.bold(cr_dr_account_label), frappe.bold(cr_dr_account)
_(
"Please set {0} to {1}, the same account that was used in the original invoice {2}."
).format(
frappe.bold(_(self.meta.get_label(cr_dr_account_field), context=self.doctype)),
frappe.bold(original_account),
frappe.bold(self.return_against),
)
)
@@ -412,6 +415,11 @@ class AccountsController(TransactionBase):
)
def validate_invoice_documents_schedule(self):
if self.is_return:
self.payment_terms_template = ""
self.payment_schedule = []
return
self.validate_payment_schedule_dates()
self.set_due_date()
self.set_payment_schedule()
@@ -426,7 +434,7 @@ class AccountsController(TransactionBase):
self.validate_payment_schedule_amount()
def validate_all_documents_schedule(self):
if self.doctype in ("Sales Invoice", "Purchase Invoice") and not self.is_return:
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
self.validate_invoice_documents_schedule()
elif self.doctype in ("Quotation", "Purchase Order", "Sales Order"):
self.validate_non_invoice_documents_schedule()
@@ -1844,7 +1852,7 @@ class AccountsController(TransactionBase):
).format(formatted_advance_paid, self.name, formatted_order_total)
)
frappe.db.set_value(self.doctype, self.name, "advance_paid", advance_paid)
self.db_set("advance_paid", advance_paid)
@property
def company_abbr(self):
@@ -3124,7 +3132,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
items_added_or_removed = False # updated to true if any new item is added or removed
any_conversion_factor_changed = False
sales_doctypes = ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"]
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
check_doc_permissions(parent, "write")
@@ -3226,25 +3233,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
# if rate is greater than price_list_rate, set margin
# or set discount
child_item.discount_percentage = 0
if parent_doctype in sales_doctypes:
child_item.margin_type = "Amount"
child_item.margin_rate_or_amount = flt(
child_item.rate - child_item.price_list_rate,
child_item.precision("margin_rate_or_amount"),
)
child_item.rate_with_margin = child_item.rate
child_item.margin_type = "Amount"
child_item.margin_rate_or_amount = flt(
child_item.rate - child_item.price_list_rate,
child_item.precision("margin_rate_or_amount"),
)
child_item.rate_with_margin = child_item.rate
else:
child_item.discount_percentage = flt(
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
child_item.precision("discount_percentage"),
)
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
if parent_doctype in sales_doctypes:
child_item.margin_type = ""
child_item.margin_rate_or_amount = 0
child_item.rate_with_margin = 0
child_item.margin_type = ""
child_item.margin_rate_or_amount = 0
child_item.rate_with_margin = 0
child_item.flags.ignore_validate_update_after_submit = True
if new_child_flag:
@@ -3310,6 +3313,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.update_billing_percentage()
parent.set_status()
parent.validate_uom_is_integer("uom", "qty")
parent.validate_uom_is_integer("stock_uom", "stock_qty")
def check_if_child_table_updated(child_table_before_update, child_table_after_update, fields_to_check):
accounting_dimensions = [*get_accounting_dimensions(), "cost_center", "project"]

View File

@@ -78,6 +78,9 @@ def validate_returned_items(doc):
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
select_fields += ",rejected_qty, received_qty"
if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
select_fields += ",name"
for d in frappe.db.sql(
f"""select {select_fields} from `tab{doc.doctype} Item` where parent = %s""",
doc.return_against,
@@ -103,15 +106,24 @@ def validate_returned_items(doc):
items_returned = False
for d in doc.get("items"):
key = d.item_code
raise_exception = False
if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
field = frappe.scrub(doc.doctype) + "_item"
if d.get(field):
key = (d.item_code, d.get(field))
raise_exception = True
if d.item_code and (flt(d.qty) < 0 or flt(d.get("received_qty")) < 0):
if d.item_code not in valid_items:
frappe.throw(
if key not in valid_items:
frappe.msgprint(
_("Row # {0}: Returned Item {1} does not exist in {2} {3}").format(
d.idx, d.item_code, doc.doctype, doc.return_against
)
),
raise_exception=raise_exception,
)
else:
ref = valid_items.get(d.item_code, frappe._dict())
ref = valid_items.get(key, frappe._dict())
validate_quantity(doc, d, ref, valid_items, already_returned_items)
if (
@@ -206,8 +218,12 @@ def validate_quantity(doc, args, ref, valid_items, already_returned_items):
def get_ref_item_dict(valid_items, ref_item_row):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
key = ref_item_row.item_code
if ref_item_row.get("name"):
key = (ref_item_row.item_code, ref_item_row.name)
valid_items.setdefault(
ref_item_row.item_code,
key,
frappe._dict(
{
"qty": 0,
@@ -221,7 +237,7 @@ def get_ref_item_dict(valid_items, ref_item_row):
}
),
)
item_dict = valid_items[ref_item_row.item_code]
item_dict = valid_items[key]
item_dict["qty"] += ref_item_row.qty
item_dict["stock_qty"] += ref_item_row.get("stock_qty", 0)
if ref_item_row.get("rate", 0) > item_dict["rate"]:

View File

@@ -167,6 +167,9 @@ class SellingController(StockController):
total = 0.0
sales_team = self.get("sales_team")
self.validate_sales_team(sales_team)
for sales_person in sales_team:
self.round_floats_in(sales_person)
@@ -186,6 +189,20 @@ class SellingController(StockController):
if sales_team and total != 100.0:
throw(_("Total allocated percentage for sales team should be 100"))
def validate_sales_team(self, sales_team):
sales_persons = [d.sales_person for d in sales_team]
if not sales_persons:
return
sales_person_status = frappe.db.get_all(
"Sales Person", filters={"name": ["in", sales_persons]}, fields=["name", "enabled"]
)
for row in sales_person_status:
if not row.enabled:
frappe.throw(_("Sales Person <b>{0}</b> is disabled.").format(row.name))
def validate_max_discount(self):
for d in self.get("items"):
if d.item_code:
@@ -456,6 +473,16 @@ class SellingController(StockController):
raise_error_if_no_rate=False,
)
if (
not d.incoming_rate
and self.get("return_against")
and self.get("is_return")
and get_valuation_method(d.item_code) == "Moving Average"
):
d.incoming_rate = get_rate_for_return(
self.doctype, self.name, d.item_code, self.return_against, item_row=d
)
# For internal transfers use incoming rate as the valuation rate
if self.is_internal_transfer():
if self.doctype == "Delivery Note" or self.get("update_stock"):

View File

@@ -91,7 +91,8 @@ status_map = {
],
"Purchase Receipt": [
["Draft", None],
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
["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 Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],

View File

@@ -50,6 +50,21 @@ class StockController(AccountsController):
self.set_rate_of_stock_uom()
self.validate_internal_transfer()
self.validate_putaway_capacity()
self.reset_conversion_factor()
def reset_conversion_factor(self):
for row in self.get("items"):
if row.uom != row.stock_uom:
continue
if row.conversion_factor != 1.0:
row.conversion_factor = 1.0
frappe.msgprint(
_(
"Conversion factor for item {0} has been reset to 1.0 as the uom {1} is same as stock uom {2}."
).format(bold(row.item_code), bold(row.uom), bold(row.stock_uom)),
alert=True,
)
def make_gl_entries(self, gl_entries=None, from_repost=False, via_landed_cost_voucher=False):
if self.docstatus == 2:

0
erpnext/edi/__init__.py Normal file
View File

View File

View File

@@ -0,0 +1,51 @@
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Code List", {
refresh: (frm) => {
if (!frm.doc.__islocal) {
frm.add_custom_button(__("Import Genericode File"), function () {
erpnext.edi.import_genericode(frm);
});
}
},
setup: (frm) => {
frm.savetrash = () => {
frm.validate_form_action("Delete");
frappe.confirm(
__(
"Are you sure you want to delete {0}?<p>This action will also delete all associated Common Code documents.</p>",
[frm.docname.bold()]
),
function () {
return frappe.call({
method: "frappe.client.delete",
args: {
doctype: frm.doctype,
name: frm.docname,
},
freeze: true,
freeze_message: __("Deleting {0} and all associated Common Code documents...", [
frm.docname,
]),
callback: function (r) {
if (!r.exc) {
frappe.utils.play_sound("delete");
frappe.model.clear_doc(frm.doctype, frm.docname);
window.history.back();
}
},
});
}
);
};
frm.set_query("default_common_code", function (doc) {
return {
filters: {
code_list: doc.name,
},
};
});
},
});

View File

@@ -0,0 +1,112 @@
{
"actions": [],
"allow_copy": 1,
"allow_rename": 1,
"autoname": "prompt",
"creation": "2024-09-29 06:55:03.920375",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"title",
"canonical_uri",
"url",
"default_common_code",
"column_break_nkls",
"version",
"publisher",
"publisher_id",
"section_break_npxp",
"description"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title"
},
{
"fieldname": "publisher",
"fieldtype": "Data",
"in_standard_filter": 1,
"label": "Publisher"
},
{
"columns": 1,
"fieldname": "version",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Version"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "canonical_uri",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Canonical URI"
},
{
"fieldname": "column_break_nkls",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_npxp",
"fieldtype": "Section Break"
},
{
"fieldname": "publisher_id",
"fieldtype": "Data",
"in_standard_filter": 1,
"label": "Publisher ID"
},
{
"fieldname": "url",
"fieldtype": "Data",
"label": "URL",
"options": "URL"
},
{
"description": "This value shall be used when no matching Common Code for a record is found.",
"fieldname": "default_common_code",
"fieldtype": "Link",
"label": "Default Common Code",
"options": "Common Code"
}
],
"index_web_pages_for_search": 1,
"links": [
{
"link_doctype": "Common Code",
"link_fieldname": "code_list"
}
],
"modified": "2024-11-16 17:01:40.260293",
"modified_by": "Administrator",
"module": "EDI",
"name": "Code List",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "description",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}

View File

@@ -0,0 +1,125 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from typing import TYPE_CHECKING
import frappe
from frappe.model.document import Document
if TYPE_CHECKING:
from lxml.etree import Element
class CodeList(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
canonical_uri: DF.Data | None
default_common_code: DF.Link | None
description: DF.SmallText | None
publisher: DF.Data | None
publisher_id: DF.Data | None
title: DF.Data | None
url: DF.Data | None
version: DF.Data | None
# end: auto-generated types
def on_trash(self):
if not frappe.flags.in_bulk_delete:
self.__delete_linked_docs()
def __delete_linked_docs(self):
self.db_set("default_common_code", None)
linked_docs = frappe.get_all(
"Common Code",
filters={"code_list": self.name},
fields=["name"],
)
for doc in linked_docs:
frappe.delete_doc("Common Code", doc.name)
def get_codes_for(self, doctype: str, name: str) -> tuple[str]:
"""Get the applicable codes for a doctype and name"""
return get_codes_for(self.name, doctype, name)
def get_docnames_for(self, doctype: str, code: str) -> tuple[str]:
"""Get the mapped docnames for a doctype and code"""
return get_docnames_for(self.name, doctype, code)
def get_default_code(self) -> str | None:
"""Get the default common code for this code list"""
return (
frappe.db.get_value("Common Code", self.default_common_code, "common_code")
if self.default_common_code
else None
)
def from_genericode(self, root: "Element"):
"""Extract Code List details from genericode XML"""
self.title = root.find(".//Identification/ShortName").text
self.version = root.find(".//Identification/Version").text
self.canonical_uri = root.find(".//CanonicalUri").text
# optionals
self.description = getattr(root.find(".//Identification/LongName"), "text", None)
self.publisher = getattr(root.find(".//Identification/Agency/ShortName"), "text", None)
if not self.publisher:
self.publisher = getattr(root.find(".//Identification/Agency/LongName"), "text", None)
self.publisher_id = getattr(root.find(".//Identification/Agency/Identifier"), "text", None)
self.url = getattr(root.find(".//Identification/LocationUri"), "text", None)
def get_codes_for(code_list: str, doctype: str, name: str) -> tuple[str]:
"""Return the common code for a given record"""
CommonCode = frappe.qb.DocType("Common Code")
DynamicLink = frappe.qb.DocType("Dynamic Link")
codes = (
frappe.qb.from_(CommonCode)
.join(DynamicLink)
.on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code"))
.select(CommonCode.common_code)
.where(
(DynamicLink.link_doctype == doctype)
& (DynamicLink.link_name == name)
& (CommonCode.code_list == code_list)
)
.distinct()
.orderby(CommonCode.common_code)
).run()
return tuple(c[0] for c in codes) if codes else ()
def get_docnames_for(code_list: str, doctype: str, code: str) -> tuple[str]:
"""Return the record name for a given common code"""
CommonCode = frappe.qb.DocType("Common Code")
DynamicLink = frappe.qb.DocType("Dynamic Link")
docnames = (
frappe.qb.from_(CommonCode)
.join(DynamicLink)
.on((CommonCode.name == DynamicLink.parent) & (DynamicLink.parenttype == "Common Code"))
.select(DynamicLink.link_name)
.where(
(DynamicLink.link_doctype == doctype)
& (CommonCode.common_code == code)
& (CommonCode.code_list == code_list)
)
.distinct()
.orderby(DynamicLink.idx)
).run()
return tuple(d[0] for d in docnames) if docnames else ()
def get_default_code(code_list: str) -> str | None:
"""Return the default common code for a given code list"""
code_id = frappe.db.get_value("Code List", code_list, "default_common_code")
return frappe.db.get_value("Common Code", code_id, "common_code") if code_id else None

View File

@@ -0,0 +1,218 @@
frappe.provide("erpnext.edi");
erpnext.edi.import_genericode = function (listview_or_form) {
let doctype = "Code List";
let docname = undefined;
if (listview_or_form.doc !== undefined) {
docname = listview_or_form.doc.name;
}
new frappe.ui.FileUploader({
method: "erpnext.edi.doctype.code_list.code_list_import.import_genericode",
doctype: doctype,
docname: docname,
allow_toggle_private: false,
allow_take_photo: false,
on_success: function (_file_doc, r) {
listview_or_form.refresh();
show_column_selection_dialog(r.message);
},
});
};
function show_column_selection_dialog(context) {
let title_description = __("If there is no title column, use the code column for the title.");
let default_title = get_default(context.columns, ["name", "Name", "code-name", "scheme-name"]);
let fields = [
{
fieldtype: "HTML",
fieldname: "code_list_info",
options: `<div class="text-muted">${__(
"You are importing data for the code list:"
)} ${frappe.utils.get_form_link(
"Code List",
context.code_list,
true,
context.code_list_title
)}</div>`,
},
{
fieldtype: "Section Break",
},
{
fieldname: "import_column",
label: __("Import"),
fieldtype: "Column Break",
},
{
fieldname: "title_column",
label: __("as Title"),
fieldtype: "Select",
reqd: 1,
options: context.columns,
default: default_title,
description: default_title ? null : title_description,
},
{
fieldname: "code_column",
label: __("as Code"),
fieldtype: "Select",
options: context.columns,
reqd: 1,
default: get_default(context.columns, ["code", "Code", "value"]),
},
{
fieldname: "filters_column",
label: __("Filter"),
fieldtype: "Column Break",
},
];
if (context.columns.length > 2) {
fields.splice(5, 0, {
fieldname: "description_column",
label: __("as Description"),
fieldtype: "Select",
options: [null].concat(context.columns),
default: get_default(context.columns, [
"description",
"Description",
"remark",
__("description"),
__("Description"),
]),
});
}
// Add filterable columns
for (let column in context.filterable_columns) {
fields.push({
fieldname: `filter_${column}`,
label: __("by {}", [column]),
fieldtype: "Select",
options: [null].concat(context.filterable_columns[column]),
});
}
fields.push(
{
fieldname: "preview_section",
label: __("Preview"),
fieldtype: "Section Break",
},
{
fieldname: "preview_html",
fieldtype: "HTML",
}
);
let d = new frappe.ui.Dialog({
title: __("Select Columns and Filters"),
fields: fields,
primary_action_label: __("Import"),
size: "large", // This will make the modal wider
primary_action(values) {
let filters = {};
for (let field in values) {
if (field.startsWith("filter_") && values[field]) {
filters[field.replace("filter_", "")] = values[field];
}
}
frappe
.xcall("erpnext.edi.doctype.code_list.code_list_import.process_genericode_import", {
code_list_name: context.code_list,
file_name: context.file,
code_column: values.code_column,
title_column: values.title_column,
description_column: values.description_column,
filters: filters,
})
.then((count) => {
frappe.msgprint(__("Import completed. {0} common codes created.", [count]));
});
d.hide();
},
});
d.fields_dict.code_column.df.onchange = () => update_preview(d, context);
d.fields_dict.title_column.df.onchange = (e) => {
let field = d.fields_dict.title_column;
if (!e.target.value) {
field.df.description = title_description;
field.refresh();
} else {
field.df.description = null;
field.refresh();
}
update_preview(d, context);
};
// Add onchange events for filterable columns
for (let column in context.filterable_columns) {
d.fields_dict[`filter_${column}`].df.onchange = () => update_preview(d, context);
}
d.show();
update_preview(d, context);
}
/**
* Return the first key from the keys array that is found in the columns array.
*/
function get_default(columns, keys) {
return keys.find((key) => columns.includes(key));
}
function update_preview(dialog, context) {
let code_column = dialog.get_value("code_column");
let title_column = dialog.get_value("title_column");
let description_column = dialog.get_value("description_column");
let html = '<table class="table table-bordered"><thead><tr>';
if (title_column) html += `<th>${__("Title")}</th>`;
if (code_column) html += `<th>${__("Code")}</th>`;
if (description_column) html += `<th>${__("Description")}</th>`;
// Add headers for filterable columns
for (let column in context.filterable_columns) {
if (dialog.get_value(`filter_${column}`)) {
html += `<th>${__(column)}</th>`;
}
}
html += "</tr></thead><tbody>";
for (let i = 0; i < 3; i++) {
html += "<tr>";
if (title_column) {
let title = context.example_values[title_column][i] || "";
html += `<td title="${title}">${truncate(title)}</td>`;
}
if (code_column) {
let code = context.example_values[code_column][i] || "";
html += `<td title="${code}">${truncate(code)}</td>`;
}
if (description_column) {
let description = context.example_values[description_column][i] || "";
html += `<td title="${description}">${truncate(description)}</td>`;
}
// Add values for filterable columns
for (let column in context.filterable_columns) {
if (dialog.get_value(`filter_${column}`)) {
let value = context.example_values[column][i] || "";
html += `<td title="${value}">${truncate(value)}</td>`;
}
}
html += "</tr>";
}
html += "</tbody></table>";
dialog.fields_dict.preview_html.$wrapper.html(html);
}
function truncate(value, maxLength = 40) {
if (typeof value !== "string") return "";
return value.length > maxLength ? value.substring(0, maxLength - 3) + "..." : value;
}

View File

@@ -0,0 +1,140 @@
import json
import frappe
import requests
from frappe import _
from lxml import etree
URL_PREFIXES = ("http://", "https://")
@frappe.whitelist()
def import_genericode():
doctype = "Code List"
docname = frappe.form_dict.docname
content = frappe.local.uploaded_file
# recover the content, if it's a link
if (file_url := frappe.local.uploaded_file_url) and file_url.startswith(URL_PREFIXES):
try:
# If it's a URL, fetch the content and make it a local file (for durable audit)
response = requests.get(frappe.local.uploaded_file_url)
response.raise_for_status()
frappe.local.uploaded_file = content = response.content
frappe.local.uploaded_filename = frappe.local.uploaded_file_url.split("/")[-1]
frappe.local.uploaded_file_url = None
except Exception as e:
frappe.throw(f"<pre>{e!s}</pre>", title=_("Fetching Error"))
if file_url := frappe.local.uploaded_file_url:
file_path = frappe.utils.file_manager.get_file_path(file_url)
with open(file_path.encode(), mode="rb") as f:
content = f.read()
# Parse the xml content
parser = etree.XMLParser(remove_blank_text=True)
try:
root = etree.fromstring(content, parser=parser)
except Exception as e:
frappe.throw(f"<pre>{e!s}</pre>", title=_("Parsing Error"))
# Extract the name (CanonicalVersionUri) from the parsed XML
name = root.find(".//CanonicalVersionUri").text
docname = docname or name
if frappe.db.exists(doctype, docname):
code_list = frappe.get_doc(doctype, docname)
if code_list.name != name:
frappe.throw(_("The uploaded file does not match the selected Code List."))
else:
# Create a new Code List document with the extracted name
code_list = frappe.new_doc(doctype)
code_list.name = name
code_list.from_genericode(root)
code_list.save()
# Attach the file and provide a recoverable identifier
file_doc = frappe.get_doc(
{
"doctype": "File",
"attached_to_doctype": "Code List",
"attached_to_name": code_list.name,
"folder": "Home/Attachments",
"file_name": frappe.local.uploaded_filename,
"file_url": frappe.local.uploaded_file_url,
"is_private": 1,
"content": content,
}
).save()
# Get available columns and example values
columns, example_values, filterable_columns = get_genericode_columns_and_examples(root)
return {
"code_list": code_list.name,
"code_list_title": code_list.title,
"file": file_doc.name,
"columns": columns,
"example_values": example_values,
"filterable_columns": filterable_columns,
}
@frappe.whitelist()
def process_genericode_import(
code_list_name: str,
file_name: str,
code_column: str,
title_column: str | None = None,
description_column: str | None = None,
filters: str | None = None,
):
from erpnext.edi.doctype.common_code.common_code import import_genericode
column_map = {"code": code_column, "title": title_column, "description": description_column}
return import_genericode(code_list_name, file_name, column_map, json.loads(filters) if filters else None)
def get_genericode_columns_and_examples(root):
columns = []
example_values = {}
filterable_columns = {}
# Get column names
for column in root.findall(".//Column"):
column_id = column.get("Id")
columns.append(column_id)
example_values[column_id] = []
filterable_columns[column_id] = set()
# Get all values and count unique occurrences
for row in root.findall(".//SimpleCodeList/Row"):
for value in row.findall("Value"):
column_id = value.get("ColumnRef")
if column_id not in columns:
# Handle undeclared column
columns.append(column_id)
example_values[column_id] = []
filterable_columns[column_id] = set()
simple_value = value.find("./SimpleValue")
if simple_value is None:
continue
filterable_columns[column_id].add(simple_value.text)
# Get example values (up to 3) and filter columns with cardinality <= 5
for row in root.findall(".//SimpleCodeList/Row")[:3]:
for value in row.findall("Value"):
column_id = value.get("ColumnRef")
simple_value = value.find("./SimpleValue")
if simple_value is None:
continue
example_values[column_id].append(simple_value.text)
filterable_columns = {k: list(v) for k, v in filterable_columns.items() if len(v) <= 5}
return columns, example_values, filterable_columns

View File

@@ -0,0 +1,8 @@
frappe.listview_settings["Code List"] = {
onload: function (listview) {
listview.page.add_inner_button(__("Import Genericode File"), function () {
erpnext.edi.import_genericode(listview);
});
},
hide_name_column: true,
};

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestCodeList(FrappeTestCase):
pass

View File

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

View File

@@ -0,0 +1,103 @@
{
"actions": [],
"autoname": "hash",
"creation": "2024-09-29 07:01:18.133067",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"code_list",
"title",
"common_code",
"description",
"column_break_wxsw",
"additional_data",
"section_break_rhgh",
"applies_to"
],
"fields": [
{
"fieldname": "code_list",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Code List",
"options": "Code List",
"reqd": 1,
"search_index": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Title",
"length": 300,
"reqd": 1
},
{
"fieldname": "column_break_wxsw",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_rhgh",
"fieldtype": "Section Break"
},
{
"fieldname": "applies_to",
"fieldtype": "Table",
"label": "Applies To",
"options": "Dynamic Link"
},
{
"fieldname": "common_code",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Common Code",
"length": 300,
"reqd": 1,
"search_index": 1
},
{
"fieldname": "additional_data",
"fieldtype": "Code",
"label": "Additional Data",
"max_height": "190px",
"read_only": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description",
"max_height": "60px"
}
],
"links": [],
"modified": "2024-11-06 07:46:17.175687",
"modified_by": "Administrator",
"module": "EDI",
"name": "Common Code",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "common_code,description",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}

View File

@@ -0,0 +1,114 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import hashlib
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils.data import get_link_to_form
from lxml import etree
class CommonCode(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.core.doctype.dynamic_link.dynamic_link import DynamicLink
from frappe.types import DF
additional_data: DF.Code | None
applies_to: DF.Table[DynamicLink]
code_list: DF.Link
common_code: DF.Data
description: DF.SmallText | None
title: DF.Data
# end: auto-generated types
def validate(self):
self.validate_distinct_references()
def validate_distinct_references(self):
"""Ensure no two Common Codes of the same Code List are linked to the same document."""
for link in self.applies_to:
existing_links = frappe.get_all(
"Common Code",
filters=[
["name", "!=", self.name],
["code_list", "=", self.code_list],
["Dynamic Link", "link_doctype", "=", link.link_doctype],
["Dynamic Link", "link_name", "=", link.link_name],
],
fields=["name", "common_code"],
)
if existing_links:
existing_link = existing_links[0]
frappe.throw(
_("{0} {1} is already linked to Common Code {2}.").format(
link.link_doctype,
link.link_name,
get_link_to_form("Common Code", existing_link["name"], existing_link["common_code"]),
)
)
def from_genericode(self, column_map: dict, xml_element: "etree.Element"):
"""Populate the Common Code document from a genericode XML element
Args:
column_map (dict): A mapping of column names to XML column references. Keys: code, title, description
code (etree.Element): The XML element representing a code in the genericode file
"""
title_column = column_map.get("title")
code_column = column_map["code"]
description_column = column_map.get("description")
self.common_code = xml_element.find(f"./Value[@ColumnRef='{code_column}']/SimpleValue").text
if title_column:
simple_value_title = xml_element.find(f"./Value[@ColumnRef='{title_column}']/SimpleValue")
self.title = simple_value_title.text if simple_value_title is not None else self.common_code
if description_column:
simple_value_descr = xml_element.find(f"./Value[@ColumnRef='{description_column}']/SimpleValue")
self.description = simple_value_descr.text if simple_value_descr is not None else None
self.additional_data = etree.tostring(xml_element, encoding="unicode", pretty_print=True)
def simple_hash(input_string, length=6):
return hashlib.blake2b(input_string.encode(), digest_size=length // 2).hexdigest()
def import_genericode(code_list: str, file_name: str, column_map: dict, filters: dict | None = None):
"""Import genericode file and create Common Code entries"""
file_path = frappe.utils.file_manager.get_file_path(file_name)
parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse(file_path, parser=parser)
root = tree.getroot()
# Construct the XPath expression
xpath_expr = ".//SimpleCodeList/Row"
filter_conditions = [
f"Value[@ColumnRef='{column_ref}']/SimpleValue='{value}'" for column_ref, value in filters.items()
]
if filter_conditions:
xpath_expr += "[" + " and ".join(filter_conditions) + "]"
elements = root.xpath(xpath_expr)
total_elements = len(elements)
for i, xml_element in enumerate(elements, start=1):
common_code: "CommonCode" = frappe.new_doc("Common Code")
common_code.code_list = code_list
common_code.from_genericode(column_map, xml_element)
common_code.save()
frappe.publish_progress(i / total_elements * 100, title=_("Importing Common Codes"))
return total_elements
def on_doctype_update():
frappe.db.add_index("Common Code", ["code_list", "common_code"])

View File

@@ -0,0 +1,8 @@
frappe.listview_settings["Common Code"] = {
onload: function (listview) {
listview.page.add_inner_button(__("Import Genericode File"), function () {
erpnext.edi.import_genericode(listview);
});
},
hide_name_column: true,
};

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestCommonCode(FrappeTestCase):
pass

View File

@@ -25,6 +25,14 @@ doctype_js = {
"Newsletter": "public/js/newsletter.js",
"Contact": "public/js/contact.js",
}
doctype_list_js = {
"Code List": [
"edi/doctype/code_list/code_list_import.js",
],
"Common Code": [
"edi/doctype/code_list/code_list_import.js",
],
}
override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
@@ -354,7 +362,6 @@ doc_events = {
"Payment Entry": {
"on_submit": [
"erpnext.regional.create_transaction_log",
"erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status",
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
],
"on_trash": "erpnext.regional.check_deletion_permission",

View File

@@ -521,7 +521,7 @@ class JobCard(Document):
self.set_transferred_qty()
def validate_transfer_qty(self):
if self.items and self.transferred_qty < self.for_quantity:
if not self.is_corrective_job_card and self.items and self.transferred_qty < self.for_quantity:
frappe.throw(
_(
"Materials needs to be transferred to the work in progress warehouse for the job card {0}"
@@ -767,26 +767,19 @@ class JobCard(Document):
qty = 0
if self.work_order:
doc = frappe.get_doc("Work Order", self.work_order)
if doc.transfer_material_against == "Job Card" and not doc.skip_transfer:
completed = True
min_qty = []
for d in doc.operations:
if d.status != "Completed":
completed = False
if d.completed_qty:
min_qty.append(d.completed_qty)
else:
min_qty = []
break
if completed:
job_cards = frappe.get_all(
"Job Card",
filters={"work_order": self.work_order, "docstatus": ("!=", 2)},
fields="sum(transferred_qty) as qty",
group_by="operation_id",
)
if min_qty:
qty = min(min_qty)
if job_cards:
qty = min(d.qty for d in job_cards)
doc.db_set("material_transferred_for_manufacturing", qty)
doc.db_set("material_transferred_for_manufacturing", qty)
self.set_status(update_status)

View File

@@ -43,7 +43,7 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Completed Qty",
"reqd": 1
"reqd": 0
},
{
"fieldname": "employee",
@@ -74,4 +74,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}
}

View File

@@ -19,4 +19,5 @@ Loan Management
Telephony
Bulk Transaction
E-commerce
Subcontracting
Subcontracting
EDI

View File

@@ -366,3 +366,4 @@ execute:frappe.db.set_single_value('E Commerce Settings', 'show_actual_qty', 1)
erpnext.patches.v14_0.delete_orphaned_asset_movement_item_records
erpnext.patches.v14_0.remove_cancelled_asset_capitalization_from_asset
erpnext.patches.v14_0.enable_set_priority_for_pricing_rules #1
erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter

View File

@@ -38,7 +38,7 @@ def execute():
data = frappe.db.sql(
"""
SELECT
name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company
name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company, creation
FROM
`tabStock Ledger Entry`
WHERE
@@ -67,6 +67,7 @@ def execute():
"voucher_type": d.voucher_type,
"voucher_no": d.voucher_no,
"sle_id": d.name,
"creation": d.creation,
},
allow_negative_stock=True,
)

View File

@@ -0,0 +1,11 @@
import frappe
def execute():
settings = frappe.get_doc("Currency Exchange Settings")
if settings.service_provider != "frankfurter.app":
return
settings.set_parameters_and_result()
settings.flags.ignore_validate = True
settings.save()

View File

@@ -47,6 +47,7 @@ frappe.ui.form.on("Project", {
frm.set_query("sales_order", function () {
var filters = {
project: ["in", frm.doc.__islocal ? [""] : [frm.doc.name, ""]],
company: frm.doc.company,
};
if (frm.doc.customer) {

View File

@@ -118,14 +118,14 @@ 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="#Form/Task/{self.parent_task}">{self.parent_task}</a>"""
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))
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="#Form/Task/{task.task}">{task.task}</a>"""
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))
def validate_completed_on(self):

View File

@@ -15,6 +15,7 @@ def execute(filters=None):
filters=filters,
fields=[
"name",
"project_name",
"status",
"percent_complete",
"expected_start_date",
@@ -48,6 +49,11 @@ def get_columns():
"options": "Project",
"width": 200,
},
{
"fieldname": "project_name",
"label": _("Project Name"),
"width": 200,
},
{
"fieldname": "project_type",
"label": _("Type"),
@@ -82,7 +88,7 @@ def get_chart_data(data):
overdue = []
for project in data:
labels.append(project.name)
labels.append(project.project_name)
total.append(project.total_tasks)
completed.append(project.completed_tasks)
overdue.append(project.overdue_tasks)

View File

@@ -822,9 +822,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) {
erpnext.utils.get_shipping_address(this.frm, function() {
set_party_account(set_pricing);
});
let is_drop_ship = me.frm.doc.items.some(item => item.delivered_by_supplier);
if (!is_drop_ship) {
console.log('get_shipping_address');
erpnext.utils.get_shipping_address(this.frm, function() {
set_party_account(set_pricing);
});
}
} else {
set_party_account(set_pricing);
@@ -1099,6 +1104,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
callback: function(r) {
if(!r.exc) {
frappe.model.set_value(cdt, cdn, 'conversion_factor', r.message.conversion_factor);
me.apply_price_list(item, true);
}
}
});
@@ -2213,7 +2219,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
payment_terms_template() {
var me = this;
const doc = this.frm.doc;
if(doc.payment_terms_template && doc.doctype !== 'Delivery Note') {
if(doc.payment_terms_template && doc.doctype !== 'Delivery Note' && !doc.is_return) {
var posting_date = doc.posting_date || doc.transaction_date;
frappe.call({
method: "erpnext.controllers.accounts_controller.get_payment_terms",

View File

@@ -4,7 +4,8 @@ erpnext.accounts.unreconcile_payment = {
add_unreconcile_btn(frm) {
if (frm.doc.docstatus == 1) {
if (
(frm.doc.doctype == "Journal Entry" && frm.doc.voucher_type != "Journal Entry") ||
(frm.doc.doctype == "Journal Entry" &&
!["Journal Entry", "Bank Entry", "Cash Entry"].includes(frm.doc.voucher_type)) ||
!["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"].includes(
frm.doc.doctype
)
@@ -99,6 +100,7 @@ erpnext.accounts.unreconcile_payment = {
fieldtype: "Table",
read_only: 1,
fields: child_table_fields,
cannot_add_rows: true,
},
];
@@ -122,7 +124,6 @@ erpnext.accounts.unreconcile_payment = {
title: "UnReconcile Allocations",
fields: unreconcile_dialog_fields,
size: "large",
cannot_add_rows: true,
primary_action_label: "UnReconcile",
primary_action(values) {
let selected_allocations = values.allocations.filter((x) => x.__checked);

View File

@@ -778,8 +778,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
]
}
],
primary_action_label: 'Create Purchase Order',
primary_action (args) {
primary_action_label: __("Create Purchase Order"),
primary_action(args) {
if (!args) return;
let selected_items = dialog.fields_dict.items_for_po.grid.get_selected_children();

View File

@@ -30,7 +30,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
from erpnext.selling.doctype.customer.customer import check_credit_limit
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.get_item_details import get_default_bom, get_price_list_rate
from erpnext.stock.get_item_details import get_bin_details, get_default_bom, get_price_list_rate
from erpnext.stock.stock_balance import get_reserved_qty, update_bin_qty
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -607,6 +607,9 @@ def make_material_request(source_name, target_doc=None):
target.project = source_parent.project
target.qty = get_remaining_qty(source)
target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
target.actual_qty = get_bin_details(
target.item_code, target.warehouse, source_parent.company, True
).get("actual_qty", 0)
args = target.as_dict().copy()
args.update(
@@ -915,7 +918,10 @@ def get_events(start, end, filters=None):
""",
{"start": start, "end": end},
as_dict=True,
update={"allDay": 0},
update={
"allDay": 0,
"convertToUserTz": 0,
},
)
return data
@@ -1032,6 +1038,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
"discount_percentage",
"discount_amount",
"pricing_rules",
"margin_type",
"margin_rate_or_amount",
],
"postprocess": update_item,
"condition": lambda doc: doc.ordered_qty < doc.stock_qty
@@ -1085,9 +1093,17 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
target.payment_schedule = []
if is_drop_ship_order(target):
target.customer = source.customer
target.customer_name = source.customer_name
target.shipping_address = source.shipping_address_name
if source.shipping_address_name:
target.shipping_address = source.shipping_address_name
target.shipping_address_display = source.shipping_address
else:
target.shipping_address = source.customer_address
target.shipping_address_display = source.address_display
target.customer_contact_person = source.contact_person
target.customer_contact_display = source.contact_display
target.customer_contact_mobile = source.contact_mobile
target.customer_contact_email = source.contact_email
else:
target.customer = target.customer_name = target.shipping_address = None

View File

@@ -8,6 +8,7 @@ frappe.views.calendar["Sales Order"] = {
id: "name",
title: "customer_name",
allDay: "allDay",
convertToUserTz: "convertToUserTz",
},
gantt: true,
filters: [

View File

@@ -14,6 +14,8 @@ def get_data():
},
"internal_links": {
"Quotation": ["items", "prevdoc_docname"],
"BOM": ["items", "bom_no"],
"Blanket Order": ["items", "blanket_order"],
},
"transactions": [
{
@@ -22,8 +24,8 @@ def get_data():
},
{"label": _("Purchasing"), "items": ["Material Request", "Purchase Order"]},
{"label": _("Projects"), "items": ["Project"]},
{"label": _("Manufacturing"), "items": ["Work Order"]},
{"label": _("Reference"), "items": ["Quotation", "Auto Repeat"]},
{"label": _("Manufacturing"), "items": ["Work Order", "BOM", "Blanket Order"]},
{"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]},
],
}

View File

@@ -30,6 +30,7 @@ from erpnext.selling.doctype.sales_order.sales_order import (
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.get_item_details import get_bin_details
class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
@@ -99,6 +100,12 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
self.assertEqual(mr.material_request_type, "Purchase")
self.assertEqual(len(mr.get("items")), len(so.get("items")))
for item in mr.get("items"):
actual_qty = get_bin_details(item.item_code, item.warehouse, mr.company, True).get(
"actual_qty", 0
)
self.assertEqual(flt(item.actual_qty), actual_qty)
def test_make_delivery_note(self):
so = make_sales_order(do_not_submit=True)

View File

@@ -277,7 +277,7 @@ erpnext.PointOfSale.ItemDetails = class {
};
this.warehouse_control.df.get_query = () => {
return {
filters: { company: this.events.get_frm().doc.company },
filters: { company: this.events.get_frm().doc.company, is_group: 0 },
};
};
this.warehouse_control.refresh();

View File

@@ -1,30 +1,32 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
extend_cscript(cur_frm.cscript, {
onload: function () {
if (cur_frm.doc.__islocal) {
cur_frm.set_value("to_currency", frappe.defaults.get_global_default("currency"));
frappe.ui.form.on("Currency Exchange", {
onload: function (frm) {
if (frm.doc.__islocal) {
frm.set_value("to_currency", frappe.defaults.get_global_default("currency"));
}
},
refresh: function () {
cur_frm.cscript.set_exchange_rate_label();
refresh: function (frm) {
// Don't trigger on Quick Entry form
if (typeof frm.is_dialog === "undefined") {
frm.trigger("set_exchange_rate_label");
}
},
from_currency: function () {
cur_frm.cscript.set_exchange_rate_label();
from_currency: function (frm) {
frm.trigger("set_exchange_rate_label");
},
to_currency: function () {
cur_frm.cscript.set_exchange_rate_label();
to_currency: function (frm) {
frm.trigger("set_exchange_rate_label");
},
set_exchange_rate_label: function () {
if (cur_frm.doc.from_currency && cur_frm.doc.to_currency) {
var default_label = __(frappe.meta.docfield_map[cur_frm.doctype]["exchange_rate"].label);
cur_frm.fields_dict.exchange_rate.set_label(
default_label + repl(" (1 %(from_currency)s = [?] %(to_currency)s)", cur_frm.doc)
set_exchange_rate_label: function (frm) {
if (frm.doc.from_currency && frm.doc.to_currency) {
var default_label = __(frappe.meta.docfield_map[frm.doctype]["exchange_rate"].label);
frm.fields_dict.exchange_rate.set_label(
default_label + repl(" (1 %(from_currency)s = [?] %(to_currency)s)", frm.doc)
);
}
},

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