Compare commits

..

120 Commits

Author SHA1 Message Date
Frappe PR Bot
6e479f918d chore(release): Bumped to Version 14.57.0
# [14.57.0](https://github.com/frappe/erpnext/compare/v14.56.0...v14.57.0) (2023-12-27)

### Bug Fixes

* allow to set rate manually for service item in BOM ([#38880](https://github.com/frappe/erpnext/issues/38880)) ([c2f692a](c2f692a4e4))
* display all item rate stop messages ([226d0e0](226d0e0196))
* do not reset the basic rate for the material receipt stock entry ([#38896](https://github.com/frappe/erpnext/issues/38896)) ([98bfcc4](98bfcc4c75))
* first name error on customer creation ([#38927](https://github.com/frappe/erpnext/issues/38927)) ([28f052d](28f052d586))
* german translations for Lost Quotations ([#38435](https://github.com/frappe/erpnext/issues/38435)) ([81b5e6c](81b5e6c5f1))
* groups for current accounts in German CoAs ([39aa36e](39aa36e44b))
* incorrect price list in customer-wise item price report ([7577706](7577706a9d))
* min order qty optional in production plan (backport [#38956](https://github.com/frappe/erpnext/issues/38956)) ([#38957](https://github.com/frappe/erpnext/issues/38957)) ([f6f2712](f6f2712db0))
* remove rows with zero consumed qty ([f2f4100](f2f410093a))
* reposting not fixing valuation rate for sales return using movin… ([#38895](https://github.com/frappe/erpnext/issues/38895)) ([3a668bb](3a668bbe96))
* reset the incoming rate on changing of the warehouse ([#38909](https://github.com/frappe/erpnext/issues/38909)) ([2770ca1](2770ca1b65))
* skip jvs against bank accounts ([98198d3](98198d35ea))
* **test:** expect account currency when party account is specified. ([30cb218](30cb218638))
* unset discount amount from coupon discounts ([87e8dd0](87e8dd0d63))
* use party account currency when party account is specified ([d9e0d55](d9e0d55b88))
* **ux:** make PR and PI Item rate field readonly based on `Maintain Same Rate` (backport [#38942](https://github.com/frappe/erpnext/issues/38942)) ([#38943](https://github.com/frappe/erpnext/issues/38943)) ([1eee203](1eee203f7f))

### Features

* in_party_currency option for AR/AP reports ([3c717db](3c717db3fd))
* total_asset_cost field (backport [#38879](https://github.com/frappe/erpnext/issues/38879)) ([#38886](https://github.com/frappe/erpnext/issues/38886)) ([8169c7d](8169c7de0d))

### Performance Improvements

* Drop unused/duplicate/sub-optimal indexes (backport [#38884](https://github.com/frappe/erpnext/issues/38884)) ([#38912](https://github.com/frappe/erpnext/issues/38912)) ([b254a72](b254a72d41))
* use estimated rows instead of actual rows (backport [#38830](https://github.com/frappe/erpnext/issues/38830)) ([#38875](https://github.com/frappe/erpnext/issues/38875)) ([4d1ccd9](4d1ccd9e27))
2023-12-27 08:40:26 +00:00
rohitwaghchaure
8e8d0ad9aa Merge pull request #38953 from frappe/version-14-hotfix
chore: release v14
2023-12-27 14:09:17 +05:30
rohitwaghchaure
400c78c600 Merge branch 'version-14' into version-14-hotfix 2023-12-27 10:24:01 +05:30
mergify[bot]
f6f2712db0 fix: min order qty optional in production plan (backport #38956) (#38957)
* fix: min order qty optional in production plan (#38956)

* fix: min order qty optional in production plan

* fix: test cases

(cherry picked from commit b09c9354fb)

# Conflicts:
#	erpnext/manufacturing/doctype/bom/bom.json
#	erpnext/manufacturing/doctype/production_plan/production_plan.py

* chore: fix conflicts

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-26 21:59:09 +05:30
mergify[bot]
1eee203f7f fix(ux): make PR and PI Item rate field readonly based on Maintain Same Rate (backport #38942) (#38943)
* fix(ux): make PI Item rate field editable

(cherry picked from commit eb5bb9f9a9)

* fix(ux): make PI Item rate field readonly based on `Maintain Same Rate`

(cherry picked from commit cb9114442b)

* fix(ux): make PR Item rate field readonly based on `Maintain Same Rate`

(cherry picked from commit b1ba210332)

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-12-26 14:45:54 +05:30
ruthra kumar
ec337a1c9f Merge pull request #38945 from frappe/mergify/bp/version-14-hotfix/pr-38891
fix: incorrect price list in customer-wise item price report (backport #38891)
2023-12-26 12:16:52 +05:30
ruthra kumar
7577706a9d fix: incorrect price list in customer-wise item price report
(cherry picked from commit 9a00edb031)
2023-12-26 06:17:45 +00:00
mergify[bot]
81b5e6c5f1 fix: german translations for Lost Quotations (#38435)
* fix: german translations for Lost Quotations (#38435)

(cherry picked from commit 5952cfa673)

# Conflicts:
#	erpnext/translations/de.csv

* chore: resolve conflicts

---------

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-12-24 16:24:31 +05:30
ruthra kumar
28f052d586 fix: first name error on customer creation (#38927)
fix: error on customer creation
2023-12-24 16:12:13 +05:30
s-aga-r
2eac618bba Merge pull request #38892 from s-aga-r/FIX-6565
fix: remove rows with zero consumed qty
2023-12-22 15:58:09 +05:30
Deepesh Garg
6f6de13813 Merge pull request #38815 from frappe/mergify/bp/version-14-hotfix/pr-38803
fix: groups for current accounts in German CoAs (#38803)
2023-12-22 08:40:28 +05:30
Deepesh Garg
be1a269f9f Merge pull request #38857 from frappe/revert-38422-mergify/bp/version-14-hotfix/pr-38250
Revert "fix: unset discount amount based on coupon code (backport #38250)"
2023-12-22 08:40:07 +05:30
Deepesh Garg
0d70be56d2 Merge pull request #38370 from pps190/backport/version-14-hotfix/38289
fix: display all item rate stop messages (#38289)
2023-12-22 08:38:19 +05:30
Deepesh Garg
50655821c1 Merge pull request #38770 from frappe/mergify/bp/version-14-hotfix/pr-38767
fix: skip JVs against bank accounts in tax report (#38767)
2023-12-22 08:37:41 +05:30
mergify[bot]
b254a72d41 perf: Drop unused/duplicate/sub-optimal indexes (backport #38884) (#38912)
* perf: Drop unused/duplicate/sub-optimal indexes (#38884)

* ci: enable more checks

* perf: Drop unused/duplicate indexes

(cherry picked from commit 787333896c)

# Conflicts:
#	erpnext/accounts/doctype/gl_entry/gl_entry.json
#	erpnext/patches.txt

* chore: conflicts

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-12-21 21:40:59 +05:30
rohitwaghchaure
2770ca1b65 fix: reset the incoming rate on changing of the warehouse (#38909) 2023-12-21 18:49:36 +05:30
mergify[bot]
0d7cb1a9be chore: additional_asset_cost field (backport #38904) (backport #38905) (#38908)
chore: additional_asset_cost field (backport #38904) (#38905)

* chore: additional_asset_cost field (#38904)

(cherry picked from commit 283763dfb2)

# Conflicts:
#	erpnext/assets/doctype/asset/asset.py

* chore: resolve conflicts in asset.py

---------

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
(cherry picked from commit 3b9feffc00)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2023-12-21 17:12:01 +05:30
Frappe PR Bot
3abb91b2c4 chore(release): Bumped to Version 14.56.0
# [14.56.0](https://github.com/frappe/erpnext/compare/v14.55.2...v14.56.0) (2023-12-21)

### Features

* total_asset_cost field (backport [#38879](https://github.com/frappe/erpnext/issues/38879)) (backport [#38886](https://github.com/frappe/erpnext/issues/38886)) ([#38907](https://github.com/frappe/erpnext/issues/38907)) ([a63b8df](a63b8df867))
2023-12-21 11:41:51 +00:00
mergify[bot]
a63b8df867 feat: total_asset_cost field (backport #38879) (backport #38886) (#38907)
feat: total_asset_cost field (backport #38879) (#38886)

* feat: total_asset_cost field (#38879)

(cherry picked from commit d370c60a6c)

# Conflicts:
#	erpnext/assets/doctype/asset/asset.json
#	erpnext/assets/doctype/asset/asset.py
#	erpnext/patches.txt

* chore: resolve conflicts in asset.json

* chore: resolve conflicts in asset.py

* chore: resolve conflicts in patches.txt

---------

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
(cherry picked from commit 8169c7de0d)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2023-12-21 17:10:04 +05:30
mergify[bot]
3b9feffc00 chore: additional_asset_cost field (backport #38904) (#38905)
* chore: additional_asset_cost field (#38904)

(cherry picked from commit 283763dfb2)

# Conflicts:
#	erpnext/assets/doctype/asset/asset.py

* chore: resolve conflicts in asset.py

---------

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-12-21 16:56:32 +05:30
Frappe PR Bot
0c13cb829c chore(release): Bumped to Version 14.55.2
## [14.55.2](https://github.com/frappe/erpnext/compare/v14.55.1...v14.55.2) (2023-12-21)

### Bug Fixes

* reposting not fixing valuation rate for sales return using movin… (backport [#38895](https://github.com/frappe/erpnext/issues/38895)) ([#38900](https://github.com/frappe/erpnext/issues/38900)) ([8aba707](8aba707b6a))
2023-12-21 10:05:58 +00:00
mergify[bot]
8aba707b6a fix: reposting not fixing valuation rate for sales return using movin… (backport #38895) (#38900)
fix: reposting not fixing valuation rate for sales return using movin… (#38895)

fix: reposting not fixing valuation rate for sales return using moving average method
(cherry picked from commit 3a668bbe96)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-21 15:34:49 +05:30
rohitwaghchaure
98bfcc4c75 fix: do not reset the basic rate for the material receipt stock entry (#38896) 2023-12-21 14:13:55 +05:30
rohitwaghchaure
3a668bbe96 fix: reposting not fixing valuation rate for sales return using movin… (#38895)
fix: reposting not fixing valuation rate for sales return using moving average method
2023-12-21 14:13:34 +05:30
s-aga-r
f2f410093a fix: remove rows with zero consumed qty 2023-12-21 11:03:18 +05:30
mergify[bot]
8169c7de0d feat: total_asset_cost field (backport #38879) (#38886)
* feat: total_asset_cost field (#38879)

(cherry picked from commit d370c60a6c)

# Conflicts:
#	erpnext/assets/doctype/asset/asset.json
#	erpnext/assets/doctype/asset/asset.py
#	erpnext/patches.txt

* chore: resolve conflicts in asset.json

* chore: resolve conflicts in asset.py

* chore: resolve conflicts in patches.txt

---------

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-12-20 22:57:37 +05:30
Frappe PR Bot
eefac19cc8 chore(release): Bumped to Version 14.55.1
## [14.55.1](https://github.com/frappe/erpnext/compare/v14.55.0...v14.55.1) (2023-12-20)

### Bug Fixes

* allow to set rate manually for service item in BOM (backport [#38880](https://github.com/frappe/erpnext/issues/38880)) ([#38881](https://github.com/frappe/erpnext/issues/38881)) ([afc0491](afc049154d))
2023-12-20 13:06:21 +00:00
mergify[bot]
afc049154d fix: allow to set rate manually for service item in BOM (backport #38880) (#38881)
fix: allow to set rate manually for service item in BOM (#38880)

(cherry picked from commit c2f692a4e4)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-20 18:34:31 +05:30
mergify[bot]
4d1ccd9e27 perf: use estimated rows instead of actual rows (backport #38830) (#38875)
* perf: use estimated rows instead of actual rows (#38830)

(cherry picked from commit 9983283f95)

# Conflicts:
#	erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py

* chore: conflicts

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-12-20 18:17:41 +05:30
Deepesh Garg
c48c8b8ddb Merge pull request #38858 from GursheenK/unset-discount-amt-coupon-code
fix: unset discount amount for removed coupons
2023-12-20 18:17:20 +05:30
rohitwaghchaure
c2f692a4e4 fix: allow to set rate manually for service item in BOM (#38880) 2023-12-20 17:53:48 +05:30
ruthra kumar
d94b5a318d Merge pull request #38872 from frappe/mergify/bp/version-14-hotfix/pr-38838
fix: use party account currency when party account is specified (backport #38838)
2023-12-20 13:09:17 +05:30
Devin Slauenwhite
30cb218638 fix(test): expect account currency when party account is specified.
(cherry picked from commit a09241e3c7)
2023-12-20 07:05:27 +00:00
Devin Slauenwhite
d9e0d55b88 fix: use party account currency when party account is specified
(cherry picked from commit c7b961ffa2)
2023-12-20 07:05:26 +00:00
ruthra kumar
1e9b4e2403 Merge pull request #38800 from pps190/backport/version-14-hotfix/37746
feat: in_party_currency option for AR/AP reports (backport #37746)
2023-12-20 12:25:35 +05:30
Frappe PR Bot
8657ba374a chore(release): Bumped to Version 14.55.0
# [14.55.0](https://github.com/frappe/erpnext/compare/v14.54.2...v14.55.0) (2023-12-20)

### Bug Fixes

* close PO on SCO close (backport [#38667](https://github.com/frappe/erpnext/issues/38667)) ([#38680](https://github.com/frappe/erpnext/issues/38680)) ([22b442d](22b442d593))
* **customer:** contact creation for companies ([#38055](https://github.com/frappe/erpnext/issues/38055)) ([ed9b38b](ed9b38bc8d))
* **customer:** quick form and integration fixes ([#37386](https://github.com/frappe/erpnext/issues/37386)) ([6ed7c66](6ed7c6610a))
* error while filtering on name on reconciliation tool ([30b6321](30b6321fd5))
* fetch exc rate of multi currency journals ([bf585de](bf585de558))
* fetch item_tax_template values if fields with fetch_from exisit ([24ae45c](24ae45ce84))
* get customers for leaderboard ([9dfe342](9dfe342838))
* get items for leaderboard ([812b301](812b301aab))
* get sales partner for leaderboard ([c63f873](c63f873a6f))
* get sales person for leaderboard ([df93447](df93447d30))
* get suppliers for leaderboard ([7477ff3](7477ff3b5c))
* if not budget then don't validate ([#38861](https://github.com/frappe/erpnext/issues/38861)) ([d375164](d375164100))
* incoming rate for sales return with Moving Average valuation method ([#38849](https://github.com/frappe/erpnext/issues/38849)) ([7fdac62](7fdac62393))
* incorrect limit ([#38818](https://github.com/frappe/erpnext/issues/38818)) ([e18dc5c](e18dc5cea3))
* Init internal child table values ([d593f81](d593f81a16))
* item variant with manufacturer ([#38845](https://github.com/frappe/erpnext/issues/38845)) ([e0c8ff1](e0c8ff10da))
* not able to cancel SCR with Batch ([#38817](https://github.com/frappe/erpnext/issues/38817)) ([fb5090f](fb5090fd3f))
* not able to make inter-company po from so ([#38826](https://github.com/frappe/erpnext/issues/38826)) ([23042df](23042dfc3c))
* on closed unreserved the production plan qty ([#38848](https://github.com/frappe/erpnext/issues/38848)) ([2184e8e](2184e8ef58))
* Reset SLA on issue doesn't work (backport [#38789](https://github.com/frappe/erpnext/issues/38789)) ([#38790](https://github.com/frappe/erpnext/issues/38790)) ([942f34a](942f34a734))
* supplier removed on selection of item (backport [#38712](https://github.com/frappe/erpnext/issues/38712)) ([#38732](https://github.com/frappe/erpnext/issues/38732)) ([dda95ea](dda95ea892))
* timezone aware SLA banner (backport [#38745](https://github.com/frappe/erpnext/issues/38745)) ([#38746](https://github.com/frappe/erpnext/issues/38746)) ([73d525e](73d525e84d))
* **ux:** don't override Item Name and Description in MR (backport [#38720](https://github.com/frappe/erpnext/issues/38720)) ([#38762](https://github.com/frappe/erpnext/issues/38762)) ([f7bfbd8](f7bfbd82dd))
* validation error on reconciling PE to Journals as Invoice ([9836205](9836205725))
* wrong currency in Stock Balance report (backport [#38778](https://github.com/frappe/erpnext/issues/38778)) ([#38779](https://github.com/frappe/erpnext/issues/38779)) ([e05b23c](e05b23c388))
* wrong paid and cn amount on pos invoice ([77da0da](77da0da945))

### Features

* **RFQ:** special properties in print preview (backport [#38725](https://github.com/frappe/erpnext/issues/38725)) ([#38726](https://github.com/frappe/erpnext/issues/38726)) ([2290750](229075048b))
* set lead name from email ([1469ca7](1469ca7d0b))

### Performance Improvements

* index `return_against` on delivery note (backport [#38827](https://github.com/frappe/erpnext/issues/38827)) ([#38831](https://github.com/frappe/erpnext/issues/38831)) ([4114760](4114760efd))
2023-12-20 04:46:08 +00:00
Deepesh Garg
3a8b7e9b96 Merge pull request #38852 from frappe/version-14-hotfix
chore: release v14
2023-12-20 10:14:59 +05:30
mergify[bot]
22b442d593 fix: close PO on SCO close (backport #38667) (#38680)
fix: close PO on SCO close

(cherry picked from commit b023e5d6b3)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-12-20 09:14:54 +05:30
mergify[bot]
f7bfbd82dd fix(ux): don't override Item Name and Description in MR (backport #38720) (#38762)
fix(ux): don't override Item Name and Description in MR

(cherry picked from commit 726ac6bda1)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-12-20 09:14:03 +05:30
rohitwaghchaure
d375164100 fix: if not budget then don't validate (#38861) 2023-12-19 18:19:20 +05:30
rohitwaghchaure
7fdac62393 fix: incoming rate for sales return with Moving Average valuation method (#38849) 2023-12-19 18:01:50 +05:30
rohitwaghchaure
2184e8ef58 fix: on closed unreserved the production plan qty (#38848) 2023-12-19 17:20:41 +05:30
Gursheen Anand
87e8dd0d63 fix: unset discount amount from coupon discounts 2023-12-19 15:56:59 +05:30
rohitwaghchaure
857843bc29 chore: fix test case (#38856) 2023-12-19 15:54:29 +05:30
Gursheen Kaur Anand
37a18299d0 Revert "fix: unset discount amount based on coupon code (backport #38250)" 2023-12-19 15:35:35 +05:30
rohitwaghchaure
e0c8ff10da fix: item variant with manufacturer (#38845) 2023-12-19 12:45:08 +05:30
ruthra kumar
61b02ec75c Merge pull request #38843 from frappe/mergify/bp/version-14-hotfix/pr-38797
fix: wrong paid and cn amount on pos invoice (backport #38797)
2023-12-19 11:33:25 +05:30
Dany Robert
939b7f7cbf test: partial payment for pos invoice
(cherry picked from commit 8772628912)
2023-12-19 05:33:10 +00:00
Dany Robert
77da0da945 fix: wrong paid and cn amount on pos invoice
(cherry picked from commit 5cb5e09dbb)
2023-12-19 05:33:09 +00:00
Devin Slauenwhite
226d0e0196 fix: display all item rate stop messages 2023-12-18 14:14:29 -05:00
mergify[bot]
4114760efd perf: index return_against on delivery note (backport #38827) (#38831)
* perf: index `return_against` on delivery note (#38827)

There's a multi-column index but that's useful IFF all parts of column
are part of query.

return against on it's own is VERY unique because it's a primary key, we
don't need a multi-column index here.

(cherry picked from commit 8d79365e0d)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/purchase_receipt.json

* chore: conflict

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-12-18 18:10:54 +05:30
rohitwaghchaure
23042dfc3c fix: not able to make inter-company po from so (#38826) 2023-12-18 17:44:17 +05:30
ruthra kumar
9216a852d4 Merge pull request #38824 from frappe/mergify/bp/version-14-hotfix/pr-38820
refactor: ignore ERR journals in Statment of Accounts (backport #38820)
2023-12-18 16:24:57 +05:30
ruthra kumar
6478c80359 refactor: ignore ERR journals in Statment of Accounts
(cherry picked from commit 39ef75e2d0)
2023-12-18 15:59:59 +05:30
rohitwaghchaure
e18dc5cea3 fix: incorrect limit (#38818) 2023-12-18 15:59:56 +05:30
rohitwaghchaure
fb5090fd3f fix: not able to cancel SCR with Batch (#38817) 2023-12-18 14:11:36 +05:30
barredterra
39aa36e44b fix: groups for current accounts in German CoAs
(cherry picked from commit 259f313af7)
2023-12-17 18:24:10 +00:00
Deepesh Garg
20a764a4a3 Merge pull request #38809 from vorasmit/fetch-item-taxes-v14
fix: fetch item_tax_template values if fields with fetch_from exisit (#38284)
2023-12-17 18:51:22 +05:30
Smit Vora
24ae45ce84 fix: fetch item_tax_template values if fields with fetch_from exisit 2023-12-17 14:03:37 +05:30
Deepesh Garg
a14e7f0e84 Merge pull request #38751 from frappe/mergify/bp/version-14-hotfix/pr-38505
feat: set lead name from email (#38505)
2023-12-16 22:29:43 +05:30
Devin Slauenwhite
7eee573fcb fixup! feat: in_party_currency option for AR/AP reports
fix: merge conflict typo
2023-12-16 16:29:01 +00:00
Richard Case
0894022695 chore: update tests 2023-12-16 16:15:00 +00:00
Richard Case
3c717db3fd feat: in_party_currency option for AR/AP reports 2023-12-16 16:14:44 +00:00
mergify[bot]
942f34a734 fix: Reset SLA on issue doesn't work (backport #38789) (#38790)
fix: Reset SLA on issue doesn't work (#38789)

This was broken since last refactor where it was spun off to work with
all types of doctypes but client side code was never adapted.

(cherry picked from commit fa1c7b663c)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-12-15 22:02:29 +05:30
Raffael Meyer
4821919695 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-38505 2023-12-15 15:15:37 +01:00
Deepesh Garg
34549b62bd Merge pull request #38782 from frappe/mergify/bp/version-14-hotfix/pr-38691
fix: Init internal child table values (#38691)
2023-12-15 18:48:10 +05:30
Deepesh Garg
d593f81a16 fix: Init internal child table values
(cherry picked from commit 2588970d55)
2023-12-15 12:45:51 +00:00
mergify[bot]
e05b23c388 fix: wrong currency in Stock Balance report (backport #38778) (#38779)
fix: wrong currency in Stock Balance report

(cherry picked from commit 5a83a16e60)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-12-15 15:32:50 +05:30
ruthra kumar
d4c131b153 Merge pull request #38774 from frappe/mergify/bp/version-14-hotfix/pr-38766
fix: validation error on reconciling PE to Journals as Invoice (backport #38766)
2023-12-15 14:41:43 +05:30
ruthra kumar
9836205725 fix: validation error on reconciling PE to Journals as Invoice
With the same exchange rate on Journal Entry and Payment Entry,
reconcilition should not post exc gain/loss journal and should not
throw validation error

(cherry picked from commit 5eeb650dfd)
2023-12-15 13:20:38 +05:30
Gursheen Anand
98198d35ea fix: skip jvs against bank accounts
(cherry picked from commit f7b2380ec1)
2023-12-15 07:29:14 +00:00
Deepesh Garg
12cd7a7083 Merge pull request #38674 from frappe/mergify/bp/version-14-hotfix/pr-38672
fix: get data for leaderboard (#38672)
2023-12-15 10:15:41 +05:30
Deepesh Garg
e539152c6b Merge pull request #38753 from barredterra/contact-from-customer-creation
fix: address and contact from customer creation (backport #38055, #37386, #38060)
2023-12-15 10:14:58 +05:30
David Arnold
6ed7c6610a fix(customer): quick form and integration fixes (#37386) 2023-12-14 16:50:20 +01:00
barredterra
d6d717c315 test: parse full name 2023-12-14 16:47:46 +01:00
barredterra
935e23449f refactor: parse full name 2023-12-14 16:47:36 +01:00
David Arnold
ed9b38bc8d fix(customer): contact creation for companies (#38055) 2023-12-14 16:47:15 +01:00
Raffael Meyer
2c52f2ce13 chore: resolve conflicts 2023-12-14 13:51:47 +01:00
barredterra
1469ca7d0b feat: set lead name from email
(cherry picked from commit ceeb724acc)

# Conflicts:
#	erpnext/crm/doctype/lead/lead.json
2023-12-14 12:49:40 +00:00
mergify[bot]
73d525e84d fix: timezone aware SLA banner (backport #38745) (#38746)
fix: timezone aware SLA banner (#38745)

(cherry picked from commit eaf86a6461)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-12-14 15:59:19 +05:30
ruthra kumar
f6c611d026 Merge pull request #38744 from ruthra-kumar/filter_error_on_reconciliation_tool
fix: error while filtering on name on reconciliation tool
2023-12-14 15:10:12 +05:30
ruthra kumar
30b6321fd5 fix: error while filtering on name on reconciliation tool 2023-12-14 14:28:20 +05:30
Frappe PR Bot
20a26c3dd4 chore(release): Bumped to Version 14.54.2
## [14.54.2](https://github.com/frappe/erpnext/compare/v14.54.1...v14.54.2) (2023-12-14)

### Bug Fixes

* fetch exc rate of multi currency journals ([cdef0bd](cdef0bd503))
2023-12-14 06:20:30 +00:00
ruthra kumar
d975eccaab Merge pull request #38741 from frappe/mergify/bp/version-14/pr-38717
fix: fetch exc rate of multi currency journals on reconciliation (backport #38717)
2023-12-14 11:49:20 +05:30
ruthra kumar
cdef0bd503 fix: fetch exc rate of multi currency journals
(cherry picked from commit 1b3ba25220)
2023-12-14 06:00:23 +00:00
ruthra kumar
a714114cd9 Merge pull request #38736 from frappe/mergify/bp/version-14-hotfix/pr-38717
fix: fetch exc rate of multi currency journals on reconciliation (backport #38717)
2023-12-14 09:59:44 +05:30
ruthra kumar
bf585de558 fix: fetch exc rate of multi currency journals
(cherry picked from commit 1b3ba25220)
2023-12-14 04:06:43 +00:00
Frappe PR Bot
b7fd24074d chore(release): Bumped to Version 14.54.1
## [14.54.1](https://github.com/frappe/erpnext/compare/v14.54.0...v14.54.1) (2023-12-13)

### Bug Fixes

* supplier removed on selection of item (backport [#38712](https://github.com/frappe/erpnext/issues/38712)) (backport [#38732](https://github.com/frappe/erpnext/issues/38732)) ([#38735](https://github.com/frappe/erpnext/issues/38735)) ([d53a9f6](d53a9f6530))
2023-12-13 19:09:56 +00:00
mergify[bot]
d53a9f6530 fix: supplier removed on selection of item (backport #38712) (backport #38732) (#38735)
fix: supplier removed on selection of item (backport #38712) (#38732)

fix: supplier removed on selection of item (#38712)

(cherry picked from commit db24e24882)

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

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2023-12-14 00:38:47 +05:30
mergify[bot]
dda95ea892 fix: supplier removed on selection of item (backport #38712) (#38732)
fix: supplier removed on selection of item (#38712)

(cherry picked from commit db24e24882)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-13 19:05:52 +00:00
mergify[bot]
4908d03a88 Revert "fix(ux): don't update qty blindly" (backport #38728) (backport #38729) (#38733)
Revert "fix(ux): don't update qty blindly" (backport #38728) (#38729)

Revert "fix(ux): don't update qty blindly" (#38728)

(cherry picked from commit 6851c5042f)

Co-authored-by: Ankush Menat <ankush@frappe.io>
(cherry picked from commit 0d49dd3648)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2023-12-14 00:30:36 +05:30
mergify[bot]
229075048b feat(RFQ): special properties in print preview (backport #38725) (#38726)
feat: RFQ print preview

(cherry picked from commit 27f05145ae)

Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com>
2023-12-14 00:10:58 +05:30
mergify[bot]
0d49dd3648 Revert "fix(ux): don't update qty blindly" (backport #38728) (#38729)
Revert "fix(ux): don't update qty blindly" (#38728)

(cherry picked from commit 6851c5042f)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-12-13 23:16:26 +05:30
Frappe PR Bot
214861d68f chore(release): Bumped to Version 14.54.0
# [14.54.0](https://github.com/frappe/erpnext/compare/v14.53.0...v14.54.0) (2023-12-12)

### Bug Fixes

* 1st row depr. sch. value of asset put to less than 180 days acc. to I.T. S. 32 ([#38697](https://github.com/frappe/erpnext/issues/38697)) ([753a1b5](753a1b511d))
* calc monthly_repayment_amount correctly in regenerate_repayment_schedule ([#38636](https://github.com/frappe/erpnext/issues/38636)) ([008400d](008400d287))
* format only if searched text contain link value text ([b28deaa](b28deaae2c))
* limit end date to current date ([cbe15e1](cbe15e159a))
* typo in unittest ([#38673](https://github.com/frappe/erpnext/issues/38673)) ([c87a3c7](c87a3c7793))
* **ux:** don't update qty blindly (backport [#38608](https://github.com/frappe/erpnext/issues/38608)) ([#38638](https://github.com/frappe/erpnext/issues/38638)) ([29cd474](29cd474f44))

### Features

* add employee number to client user bootinfo (backport [#38477](https://github.com/frappe/erpnext/issues/38477)) ([#38602](https://github.com/frappe/erpnext/issues/38602)) ([c2a137f](c2a137f0fb))

### Reverts

* Revert "refactor: bank transaction (backport #38182)" (#38653) ([7813efa](7813efad35)), closes [#38182](https://github.com/frappe/erpnext/issues/38182) [#38653](https://github.com/frappe/erpnext/issues/38653) [#38182](https://github.com/frappe/erpnext/issues/38182)
2023-12-12 16:12:27 +00:00
Deepesh Garg
e7b96209cd Merge pull request #38689 from frappe/version-14-hotfix
chore: release v14
2023-12-12 21:40:16 +05:30
Anand Baburajan
753a1b511d fix: 1st row depr. sch. value of asset put to less than 180 days acc. to I.T. S. 32 (#38697)
fix: 1st row value of asset put to less than 180 days acc. to IT S. 32
2023-12-12 20:53:44 +05:30
ruthra kumar
6715dd6167 Merge pull request #38677 from frappe/mergify/bp/version-14-hotfix/pr-38673
fix: typo in unittest (backport #38673)
2023-12-12 17:35:16 +05:30
ruthra kumar
a061798a6f Merge pull request #38686 from frappe/mergify/bp/version-14-hotfix/pr-38685
refactor: add `get_list` for virtual child doctypes (backport #38685)
2023-12-12 15:24:56 +05:30
ruthra kumar
fa79a7cdab refactor: add get_list for virtual child doctypes
(cherry picked from commit 15c90551b6)
2023-12-12 14:51:41 +05:30
ruthra kumar
6884aac2a0 Merge pull request #38683 from frappe/mergify/bp/version-14-hotfix/pr-38679
refactor(test): repost utility deletion flag test (backport #38679)
2023-12-12 13:33:17 +05:30
ruthra kumar
e61a12b4cb refactor(test): update repost settings before test case
(cherry picked from commit acb6e8e120)
2023-12-12 07:16:49 +00:00
ruthra kumar
4542a12787 refactor: increase limit and remove explicit call to start_repost
(cherry picked from commit ccff588563)
2023-12-12 07:16:49 +00:00
ruthra kumar
f0408c89ce refactor: remove explicit commit on repost
(cherry picked from commit a97b3db749)
2023-12-12 07:16:48 +00:00
ruthra kumar
7c080bd60c refactor(test): repost utility deletion flag test
(cherry picked from commit cc15f695b4)
2023-12-12 07:16:48 +00:00
Raffael Meyer
c87a3c7793 fix: typo in unittest (#38673)
(cherry picked from commit 6ad298adfc)
2023-12-12 04:26:10 +00:00
barredterra
038a30feb0 chore: deprecate unused method
(cherry picked from commit 956c3c50a0)
2023-12-12 04:24:39 +00:00
barredterra
c63f873a6f fix: get sales partner for leaderboard
(cherry picked from commit 40c1acc961)
2023-12-12 04:24:39 +00:00
barredterra
df93447d30 fix: get sales person for leaderboard
(cherry picked from commit 7babfd4ac4)
2023-12-12 04:24:39 +00:00
barredterra
7477ff3b5c fix: get suppliers for leaderboard
(cherry picked from commit 65df4b6aa8)
2023-12-12 04:24:38 +00:00
barredterra
812b301aab fix: get items for leaderboard
(cherry picked from commit 2721ee3a8d)
2023-12-12 04:24:38 +00:00
barredterra
9dfe342838 fix: get customers for leaderboard
(cherry picked from commit 137b5a6108)
2023-12-12 04:24:38 +00:00
Deepesh Garg
7813efad35 Revert "refactor: bank transaction (backport #38182)" (#38653)
Revert "refactor: bank transaction (#38182)"

This reverts commit a4b5fc8100.
2023-12-10 16:29:27 +05:30
mergify[bot]
29cd474f44 fix(ux): don't update qty blindly (backport #38608) (#38638)
fix(ux): don't update qty blindly

(cherry picked from commit 0156339f34)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-12-08 17:59:46 +05:30
Anand Baburajan
008400d287 fix: calc monthly_repayment_amount correctly in regenerate_repayment_schedule (#38636) 2023-12-08 16:34:29 +05:30
Shariq Ansari
4b9d9c0df4 Merge pull request #38626 from frappe/mergify/bp/version-14-hotfix/pr-38623
fix: format only if searched text contain link value text (backport #38623)
2023-12-07 20:51:16 +05:30
Shariq Ansari
b28deaae2c fix: format only if searched text contain link value text
(cherry picked from commit 08ed3cd313)
2023-12-07 15:17:48 +00:00
Ankush Menat
4e6b8ead8f chore: remove unused cache=True
(cherry picked from commit 6a47a2ceaf)
2023-12-06 20:55:35 +05:30
mergify[bot]
c2a137f0fb feat: add employee number to client user bootinfo (backport #38477) (#38602)
feat: add employee number to client user bootinfo (#38477)

(cherry picked from commit 525f656cc1)

Co-authored-by: Richard Case <110036763+casesolved-co-uk@users.noreply.github.com>
2023-12-06 20:55:06 +05:30
Deepesh Garg
f6fcb7ee26 Merge pull request #38571 from frappe/mergify/bp/version-14-hotfix/pr-38556
fix(minor): financial statements period end date (#38556)
2023-12-06 13:45:45 +05:30
Gursheen Anand
cbe15e159a fix: limit end date to current date
(cherry picked from commit ab6e92aae1)
2023-12-05 05:54:12 +00:00
92 changed files with 1213 additions and 822 deletions

View File

@@ -5,7 +5,7 @@ fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
rev: v4.3.0
hooks:
- id: trailing-whitespace
files: "erpnext.*"
@@ -15,6 +15,10 @@ repos:
args: ['--branch', 'develop']
- id: check-merge-conflict
- id: check-ast
- id: check-json
- id: check-toml
- id: check-yaml
- id: debug-statements
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4

View File

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

View File

@@ -53,8 +53,13 @@
},
"II. Forderungen und sonstige Vermögensgegenstände": {
"is_group": 1,
"Ford. a. Lieferungen und Leistungen": {
"Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
"account_number": "1400",
"account_type": "Receivable",
"is_group": 1
},
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
"account_number": "1410",
"account_type": "Receivable"
},
"Durchlaufende Posten": {
@@ -180,8 +185,13 @@
},
"IV. Verbindlichkeiten aus Lieferungen und Leistungen": {
"is_group": 1,
"Verbindlichkeiten aus Lieferungen u. Leistungen": {
"Verbindlichkeiten aus Lieferungen und Leistungen mit Kontokorrent": {
"account_number": "1600",
"account_type": "Payable",
"is_group": 1
},
"Verbindlichkeiten aus Lieferungen und Leistungen ohne Kontokorrent": {
"account_number": "1610",
"account_type": "Payable"
}
},

View File

@@ -407,13 +407,10 @@
"Bewertungskorrektur zu Forderungen aus Lieferungen und Leistungen": {
"account_number": "9960"
},
"Debitoren": {
"is_group": 1,
"account_number": "10000"
},
"Forderungen aus Lieferungen und Leistungen": {
"Forderungen aus Lieferungen und Leistungen mit Kontokorrent": {
"account_number": "1200",
"account_type": "Receivable"
"account_type": "Receivable",
"is_group": 1
},
"Forderungen aus Lieferungen und Leistungen ohne Kontokorrent": {
"account_number": "1210"
@@ -1138,18 +1135,15 @@
"Bewertungskorrektur zu Verb. aus Lieferungen und Leistungen": {
"account_number": "9964"
},
"Kreditoren": {
"account_number": "70000",
"Verb. aus Lieferungen und Leistungen mit Kontokorrent": {
"account_number": "3300",
"account_type": "Payable",
"is_group": 1,
"Wareneingangs-­Verrechnungskonto" : {
"Wareneingangs-Verrechnungskonto" : {
"account_number": "70001",
"account_type": "Stock Received But Not Billed"
}
},
"Verb. aus Lieferungen und Leistungen": {
"account_number": "3300",
"account_type": "Payable"
},
"Verb. aus Lieferungen und Leistungen ohne Kontokorrent": {
"account_number": "3310"
},

View File

@@ -423,9 +423,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
transaction.add_payment_entries(vouchers)
transaction.save()
return transaction
return frappe.get_doc("Bank Transaction", bank_transaction_name)
@frappe.whitelist()

View File

@@ -13,7 +13,6 @@
"status",
"bank_account",
"company",
"amended_from",
"section_break_4",
"deposit",
"withdrawal",
@@ -26,10 +25,10 @@
"transaction_id",
"transaction_type",
"section_break_14",
"column_break_oufv",
"payment_entries",
"section_break_18",
"allocated_amount",
"amended_from",
"column_break_17",
"unallocated_amount",
"party_section",
@@ -139,12 +138,10 @@
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"label": "Allocated Amount",
"options": "currency",
"read_only": 1
"options": "currency"
},
{
"fieldname": "amended_from",
@@ -160,12 +157,10 @@
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "unallocated_amount",
"fieldtype": "Currency",
"label": "Unallocated Amount",
"options": "currency",
"read_only": 1
"options": "currency"
},
{
"fieldname": "party_section",
@@ -230,15 +225,11 @@
"fieldname": "bank_party_account_number",
"fieldtype": "Data",
"label": "Party Account No. (Bank Statement)"
},
{
"fieldname": "column_break_oufv",
"fieldtype": "Column Break"
}
],
"is_submittable": 1,
"links": [],
"modified": "2023-11-18 18:32:47.203694",
"modified": "2023-06-06 13:58:12.821411",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",

View File

@@ -2,73 +2,78 @@
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import flt
from erpnext.controllers.status_updater import StatusUpdater
class BankTransaction(StatusUpdater):
def before_validate(self):
self.update_allocated_amount()
def after_insert(self):
self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit))
def validate(self):
self.validate_duplicate_references()
def validate_duplicate_references(self):
"""Make sure the same voucher is not allocated twice within the same Bank Transaction"""
if not self.payment_entries:
return
pe = []
for row in self.payment_entries:
reference = (row.payment_document, row.payment_entry)
if reference in pe:
frappe.throw(
_("{0} {1} is allocated twice in this Bank Transaction").format(
row.payment_document, row.payment_entry
)
)
pe.append(reference)
def update_allocated_amount(self):
self.allocated_amount = (
sum(p.allocated_amount for p in self.payment_entries) if self.payment_entries else 0.0
)
self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - self.allocated_amount
def before_submit(self):
self.allocate_payment_entries()
def on_submit(self):
self.clear_linked_payment_entries()
self.set_status()
if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
self.auto_set_party()
def before_update_after_submit(self):
self.validate_duplicate_references()
self.allocate_payment_entries()
self.update_allocated_amount()
_saving_flag = False
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
def on_update_after_submit(self):
"Run on save(). Avoid recursion caused by multiple saves"
if not self._saving_flag:
self._saving_flag = True
self.clear_linked_payment_entries()
self.update_allocations()
self._saving_flag = False
def on_cancel(self):
for payment_entry in self.payment_entries:
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
self.clear_linked_payment_entries(for_cancel=True)
self.set_status(update=True)
def update_allocations(self):
"The doctype does not allow modifications after submission, so write to the db direct"
if self.payment_entries:
allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
else:
allocated_amount = 0.0
amount = abs(flt(self.withdrawal) - flt(self.deposit))
self.db_set("allocated_amount", flt(allocated_amount))
self.db_set("unallocated_amount", amount - flt(allocated_amount))
self.reload()
self.set_status(update=True)
def add_payment_entries(self, vouchers):
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
if 0.0 >= self.unallocated_amount:
frappe.throw(_("Bank Transaction {0} is already fully reconciled").format(self.name))
frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
added = False
for voucher in vouchers:
self.append(
"payment_entries",
{
# Can't add same voucher twice
found = False
for pe in self.payment_entries:
if (
pe.payment_document == voucher["payment_doctype"]
and pe.payment_entry == voucher["payment_name"]
):
found = True
if not found:
pe = {
"payment_document": voucher["payment_doctype"],
"payment_entry": voucher["payment_name"],
"allocated_amount": 0.0, # Temporary
},
)
}
child = self.append("payment_entries", pe)
added = True
# runs on_update_after_submit
if added:
self.save()
def allocate_payment_entries(self):
"""Refactored from bank reconciliation tool.
@@ -85,7 +90,6 @@ class BankTransaction(StatusUpdater):
- clear means: set the latest transaction date as clearance date
"""
remaining_amount = self.unallocated_amount
to_remove = []
for payment_entry in self.payment_entries:
if payment_entry.allocated_amount == 0.0:
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
@@ -95,39 +99,49 @@ class BankTransaction(StatusUpdater):
if 0.0 == unallocated_amount:
if should_clear:
latest_transaction.clear_linked_payment_entry(payment_entry)
to_remove.append(payment_entry)
self.db_delete_payment_entry(payment_entry)
elif remaining_amount <= 0.0:
to_remove.append(payment_entry)
self.db_delete_payment_entry(payment_entry)
elif 0.0 < unallocated_amount <= remaining_amount:
payment_entry.allocated_amount = unallocated_amount
elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount:
payment_entry.db_set("allocated_amount", unallocated_amount)
remaining_amount -= unallocated_amount
if should_clear:
latest_transaction.clear_linked_payment_entry(payment_entry)
elif 0.0 < unallocated_amount:
payment_entry.allocated_amount = remaining_amount
elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount:
payment_entry.db_set("allocated_amount", remaining_amount)
remaining_amount = 0.0
elif 0.0 > unallocated_amount:
frappe.throw(_("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
self.db_delete_payment_entry(payment_entry)
frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
for payment_entry in to_remove:
self.remove(to_remove)
self.reload()
def db_delete_payment_entry(self, payment_entry):
frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name})
@frappe.whitelist()
def remove_payment_entries(self):
for payment_entry in self.payment_entries:
self.remove_payment_entry(payment_entry)
self.save() # runs before_update_after_submit
# runs on_update_after_submit
self.save()
def remove_payment_entry(self, payment_entry):
"Clear payment entry and clearance"
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
self.remove(payment_entry)
def clear_linked_payment_entries(self, for_cancel=False):
if for_cancel:
for payment_entry in self.payment_entries:
self.clear_linked_payment_entry(payment_entry, for_cancel)
else:
self.allocate_payment_entries()
def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
clearance_date = None if for_cancel else self.date
set_voucher_clearance(
@@ -148,10 +162,11 @@ class BankTransaction(StatusUpdater):
deposit=self.deposit,
).match()
if not result:
return
self.party_type, self.party = result
if result:
party_type, party = result
frappe.db.set_value(
"Bank Transaction", self.name, field={"party_type": party_type, "party": party}
)
@frappe.whitelist()
@@ -183,7 +198,9 @@ def get_clearance_details(transaction, payment_entry):
if gle["gl_account"] == gl_bank_account:
if gle["amount"] <= 0.0:
frappe.throw(
_("Voucher {0} value is broken: {1}").format(payment_entry.payment_entry, gle["amount"])
frappe._("Voucher {0} value is broken: {1}").format(
payment_entry.payment_entry, gle["amount"]
)
)
unmatched_gles -= 1
@@ -204,7 +221,7 @@ def get_clearance_details(transaction, payment_entry):
def get_related_bank_gl_entries(doctype, docname):
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
return frappe.db.sql(
result = frappe.db.sql(
"""
SELECT
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
@@ -222,6 +239,7 @@ def get_related_bank_gl_entries(doctype, docname):
dict(doctype=doctype, docname=docname),
as_dict=True,
)
return result
def get_total_allocated_amount(doctype, docname):
@@ -354,7 +372,6 @@ def set_voucher_clearance(doctype, docname, clearance_date, self):
if clearance_date:
vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
bt.add_payment_entries(vouchers)
bt.save()
else:
for pe in bt.payment_entries:
if pe.payment_document == self.doctype and pe.payment_entry == self.name:

View File

@@ -138,8 +138,7 @@
"label": "Against Voucher Type",
"oldfieldname": "against_voucher_type",
"oldfieldtype": "Data",
"options": "DocType",
"search_index": 1
"options": "DocType"
},
{
"fieldname": "against_voucher",
@@ -158,8 +157,7 @@
"label": "Voucher Type",
"oldfieldname": "voucher_type",
"oldfieldtype": "Select",
"options": "DocType",
"search_index": 1
"options": "DocType"
},
{
"fieldname": "voucher_no",
@@ -291,4 +289,4 @@
"search_fields": "voucher_no,account,posting_date,against_voucher",
"sort_field": "modified",
"sort_order": "DESC"
}
}

View File

@@ -111,8 +111,6 @@ class PaymentReconciliation(Document):
def get_payment_entries(self):
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
condition = self.get_conditions(get_payments=True)
if self.payment_name:
condition += "name like '%%{0}%%'".format(self.payment_name)
payment_entries = get_advance_payment_entries_for_regional(
self.party_type,
@@ -122,6 +120,7 @@ class PaymentReconciliation(Document):
against_all_orders=True,
limit=self.payment_limit,
condition=condition,
payment_name=self.payment_name,
)
return payment_entries
@@ -539,6 +538,27 @@ class PaymentReconciliation(Document):
invoice_exchange_map.update(purchase_invoice_map)
journals = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry"
]
journals.extend(
[d.get("reference_name") for d in payments if d.get("reference_type") == "Journal Entry"]
)
if journals:
journals = list(set(journals))
journals_map = frappe._dict(
frappe.db.get_all(
"Journal Entry Account",
filters={"parent": ("in", journals), "account": ("in", [self.receivable_payable_account])},
fields=[
"parent as `name`",
"exchange_rate",
],
as_list=1,
)
)
invoice_exchange_map.update(journals_map)
return invoice_exchange_map
def validate_allocation(self):

View File

@@ -6,4 +6,6 @@ from frappe.model.document import Document
class PaymentReconciliationAllocation(Document):
pass
@staticmethod
def get_list(args):
pass

View File

@@ -6,4 +6,6 @@ from frappe.model.document import Document
class PaymentReconciliationInvoice(Document):
pass
@staticmethod
def get_list(args):
pass

View File

@@ -6,4 +6,6 @@ from frappe.model.document import Document
class PaymentReconciliationPayment(Document):
pass
@staticmethod
def get_list(args):
pass

View File

@@ -15,6 +15,7 @@
"group_by",
"cost_center",
"territory",
"ignore_exchange_rate_revaluation_journals",
"column_break_14",
"to_date",
"finance_book",
@@ -374,10 +375,16 @@
"fieldname": "pdf_name",
"fieldtype": "Data",
"label": "PDF Name"
},
{
"default": "0",
"fieldname": "ignore_exchange_rate_revaluation_journals",
"fieldtype": "Check",
"label": "Ignore Exchange Rate Revaluation Journals"
}
],
"links": [],
"modified": "2023-08-28 12:59:53.071334",
"modified": "2023-12-18 12:20:08.965120",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -64,6 +64,18 @@ def get_statement_dict(doc, get_statement_dict=False):
statement_dict = {}
ageing = ""
err_journals = None
if doc.report == "General Ledger" and doc.ignore_exchange_rate_revaluation_journals:
err_journals = frappe.db.get_all(
"Journal Entry",
filters={
"company": doc.company,
"docstatus": 1,
"voucher_type": ("in", ["Exchange Rate Revaluation", "Exchange Gain Or Loss"]),
},
as_list=True,
)
for entry in doc.customers:
if doc.include_ageing:
ageing = set_ageing(doc, entry)
@@ -76,6 +88,8 @@ def get_statement_dict(doc, get_statement_dict=False):
)
filters = get_common_filters(doc)
if err_journals:
filters.update({"voucher_no_not_in": [x[0] for x in err_journals]})
if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))

View File

@@ -164,6 +164,18 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
})
}, __("Get Items From"));
if (!this.frm.doc.is_return) {
frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => {
if (value) {
this.frm.doc.items.forEach((item) => {
this.frm.fields_dict.items.grid.update_docfield_property(
"rate", "read_only", (item.purchase_receipt && item.pr_detail)
);
});
}
});
}
}
this.frm.toggle_reqd("supplier_warehouse", this.frm.doc.is_subcontracted);

View File

@@ -1660,10 +1660,6 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
return make_inter_company_transaction("Purchase Invoice", source_name, target_doc)
def on_doctype_update():
frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"])
@frappe.whitelist()
def make_purchase_receipt(source_name, target_doc=None):
def update_item(obj, target, source_parent):

View File

@@ -286,7 +286,6 @@
"oldfieldname": "import_rate",
"oldfieldtype": "Currency",
"options": "currency",
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_receipt && doc.pr_detail)",
"reqd": 1
},
{
@@ -894,7 +893,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-11-30 16:26:05.629780",
"modified": "2023-12-25 22:00:28.043555",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -108,7 +108,7 @@ class RepostAccountingLedger(Document):
return rendered_page
def on_submit(self):
if len(self.vouchers) > 1:
if len(self.vouchers) > 5:
job_name = "repost_accounting_ledger_" + self.name
frappe.enqueue(
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
@@ -152,8 +152,6 @@ def start_repost(account_repost_doc=str) -> None:
doc.make_gl_entries(1)
doc.make_gl_entries()
frappe.db.commit()
def get_allowed_types_from_settings():
return [

View File

@@ -20,18 +20,11 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
self.create_company()
self.create_customer()
self.create_item()
self.update_repost_settings()
update_repost_settings()
def teadDown(self):
def tearDown(self):
frappe.db.rollback()
def update_repost_settings(self):
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
for x in allowed_types:
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
repost_settings.save()
def test_01_basic_functions(self):
si = create_sales_invoice(
item=self.item,
@@ -90,9 +83,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
# Submit repost document
ral.save().submit()
# background jobs don't run on test cases. Manually triggering repost function.
start_repost(ral.name)
res = (
qb.from_(gl)
.select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit"))
@@ -177,26 +167,6 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
pe = get_payment_entry(si.doctype, si.name)
pe.save().submit()
# without deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.delete_cancelled_entries = False
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save()
# assert preview data is generated
preview = ral.generate_preview()
self.assertIsNotNone(preview)
ral.save().submit()
# background jobs don't run on test cases. Manually triggering repost function.
start_repost(ral.name)
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
# with deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
@@ -205,6 +175,38 @@ class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase):
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save().submit()
start_repost(ral.name)
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
def test_05_without_deletion_flag(self):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
)
pe = get_payment_entry(si.doctype, si.name)
pe.save().submit()
# without deletion flag set
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = self.company
ral.delete_cancelled_entries = False
ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name})
ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name})
ral.save().submit()
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1}))
self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1}))
def update_repost_settings():
allowed_types = ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]
repost_settings = frappe.get_doc("Repost Accounting Ledger Settings")
for x in allowed_types:
repost_settings.append("allowed_types", {"document_type": x, "allowed": True})
repost_settings.save()

View File

@@ -2195,9 +2195,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
def get_received_items(reference_name, doctype, reference_fieldname):
reference_field = "inter_company_invoice_reference"
if doctype == "Purchase Order":
reference_field = "inter_company_order_reference"
filters = {
reference_field: reference_name,
"docstatus": 1,
}
target_doctypes = frappe.get_all(
doctype,
filters={"inter_company_invoice_reference": reference_name, "docstatus": 1},
filters=filters,
as_list=True,
)
@@ -2379,10 +2388,6 @@ def get_loyalty_programs(customer):
return lp_details
def on_doctype_update():
frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"])
@frappe.whitelist()
def create_invoice_discounting(source_name, target_doc=None):
invoice = frappe.get_doc("Sales Invoice", source_name)

View File

@@ -2791,6 +2791,12 @@ class TestSalesInvoice(FrappeTestCase):
@change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self):
from erpnext.accounts.doctype.repost_accounting_ledger.test_repost_accounting_ledger import (
update_repost_settings,
)
update_repost_settings()
additional_discount_account = create_account(
account_name="Discount Account",
parent_account="Indirect Expenses - _TC",

View File

@@ -149,11 +149,16 @@ frappe.query_reports["Accounts Payable"] = {
"label": __("Revaluation Journals"),
"fieldtype": "Check",
},
{
"fieldname": "in_party_currency",
"label": __("In Party Currency"),
"fieldtype": "Check",
},
{
"fieldname": "ignore_accounts",
"label": __("Group by Voucher"),
"fieldtype": "Check",
}
},
],

View File

@@ -40,6 +40,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"range2": 60,
"range3": 90,
"range4": 120,
"in_party_currency": 1,
}
data = execute(filters)

View File

@@ -181,13 +181,17 @@ frappe.query_reports["Accounts Receivable"] = {
"label": __("Revaluation Journals"),
"fieldtype": "Check",
},
{
"fieldname": "in_party_currency",
"label": __("In Party Currency"),
"fieldtype": "Check",
},
{
"fieldname": "ignore_accounts",
"label": __("Group by Voucher"),
"fieldtype": "Check",
}
],
"formatter": function(value, row, column, data, default_formatter) {

View File

@@ -28,8 +28,8 @@ from erpnext.accounts.utils import get_currency_precision
# 6. Configurable Ageing Groups (0-30, 30-60 etc) can be set via filters
# 7. For overpayment against an invoice with payment terms, there will be an additional row
# 8. Invoice details like Sales Persons, Delivery Notes are also fetched comma separated
# 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party
# 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable"
# 9. Report amounts are in party currency if in_party_currency is selected, otherwise company currency
# 10. This report is based on Payment Ledger Entries
def execute(filters=None):
@@ -84,6 +84,9 @@ class ReceivablePayableReport(object):
self.total_row_map = {}
self.skip_total_row = 1
if self.filters.get("in_party_currency"):
self.skip_total_row = 1
def get_data(self):
self.get_ple_entries()
self.get_sales_invoices_or_customers_based_on_sales_person()
@@ -145,7 +148,7 @@ class ReceivablePayableReport(object):
if self.filters.get("group_by_party"):
self.init_subtotal_row(ple.party)
if self.filters.get("group_by_party"):
if self.filters.get("group_by_party") and not self.filters.get("in_party_currency"):
self.init_subtotal_row("Total")
def get_invoices(self, ple):
@@ -224,8 +227,7 @@ class ReceivablePayableReport(object):
if not row:
return
# amount in "Party Currency", if its supplied. If not, amount in company currency
if self.filters.get("party_type") and self.filters.get("party"):
if self.filters.get("in_party_currency") or self.filters.get("party_account"):
amount = ple.amount_in_account_currency
else:
amount = ple.amount
@@ -244,8 +246,12 @@ class ReceivablePayableReport(object):
row.invoiced_in_account_currency += amount_in_account_currency
else:
if self.is_invoice(ple):
row.credit_note -= amount
row.credit_note_in_account_currency -= amount_in_account_currency
if row.voucher_no == ple.voucher_no == ple.against_voucher_no:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
else:
row.credit_note -= amount
row.credit_note_in_account_currency -= amount_in_account_currency
else:
row.paid -= amount
row.paid_in_account_currency -= amount_in_account_currency
@@ -256,8 +262,10 @@ class ReceivablePayableReport(object):
def update_sub_total_row(self, row, party):
total_row = self.total_row_map.get(party)
for field in self.get_currency_fields():
total_row[field] += row.get(field, 0.0)
if total_row:
for field in self.get_currency_fields():
total_row[field] += row.get(field, 0.0)
total_row["currency"] = row.get("currency", "")
def append_subtotal_row(self, party):
sub_total_row = self.total_row_map.get(party)
@@ -318,7 +326,7 @@ class ReceivablePayableReport(object):
if self.filters.get("group_by_party"):
self.append_subtotal_row(self.previous_party)
if self.data:
self.data.append(self.total_row_map.get("Total"))
self.data.append(self.total_row_map.get("Total", {}))
def append_row(self, row):
self.allocate_future_payments(row)
@@ -449,7 +457,7 @@ class ReceivablePayableReport(object):
party_details = self.get_party_details(row.party) or {}
row.update(party_details)
if self.filters.get("party_type") and self.filters.get("party"):
if self.filters.get("in_party_currency") or self.filters.get("party_account"):
row.currency = row.account_currency
else:
row.currency = self.company_currency

View File

@@ -76,6 +76,41 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
return credit_note
def test_pos_receivable(self):
filters = {
"company": self.company,
"party_type": "Customer",
"party": [self.customer],
"report_date": add_days(today(), 2),
"based_on_payment_terms": 0,
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_remarks": False,
}
pos_inv = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
pos_inv.posting_date = add_days(today(), 2)
pos_inv.is_pos = 1
pos_inv.append(
"payments",
frappe._dict(
mode_of_payment="Cash",
amount=flt(pos_inv.grand_total / 2),
),
)
pos_inv.disable_rounded_total = 1
pos_inv.save()
pos_inv.submit()
report = execute(filters)
expected_data = [[pos_inv.grand_total, pos_inv.paid_amount, 0]]
row = report[1][-1]
self.assertEqual(expected_data[0], [row.invoiced, row.paid, row.credit_note])
pos_inv.cancel()
def test_accounts_receivable(self):
filters = {
"company": self.company,
@@ -544,7 +579,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters.update({"party_account": self.debtors_usd})
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency]
expected_data = [100.0, 100.0, self.debtors_usd, si2.currency]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
@@ -581,6 +616,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
"range2": 60,
"range3": 90,
"range4": 120,
"in_party_currency": 1,
}
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)

View File

@@ -8,7 +8,17 @@ import re
import frappe
from frappe import _
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
from frappe.utils import (
add_days,
add_months,
cint,
cstr,
flt,
formatdate,
get_first_day,
getdate,
today,
)
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
@@ -43,6 +53,8 @@ def get_period_list(
year_start_date = getdate(period_start_date)
year_end_date = getdate(period_end_date)
year_end_date = getdate(today()) if year_end_date > getdate(today()) else year_end_date
months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity]
period_list = []

View File

@@ -231,6 +231,9 @@ def get_conditions(filters):
if filters.get("voucher_no"):
conditions.append("voucher_no=%(voucher_no)s")
if filters.get("voucher_no_not_in"):
conditions.append("voucher_no not in %(voucher_no_not_in)s")
if filters.get("group_by") == "Group by Party" and not filters.get("party_type"):
conditions.append("party_type in ('Customer', 'Supplier')")

View File

@@ -345,21 +345,16 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
if filters.get("party"):
party = [filters.get("party")]
query = query.where(
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
| ((gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party")))
| gle.party.isin(party)
jv_condition = gle.against.isin(party) | (
(gle.voucher_type == "Journal Entry") & (gle.party == filters.get("party"))
)
else:
party = frappe.get_all(filters.get("party_type"), pluck="name")
query = query.where(
((gle.account.isin(tds_accounts) & gle.against.isin(party)))
| (
(gle.voucher_type == "Journal Entry")
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
)
| gle.party.isin(party)
jv_condition = gle.against.isin(party) | (
(gle.voucher_type == "Journal Entry")
& ((gle.party_type == filters.get("party_type")) | (gle.party_type == ""))
)
query = query.where((gle.account.isin(tds_accounts) & jv_condition) | gle.party.isin(party))
return query

View File

@@ -626,8 +626,10 @@ def update_reference_in_payment_entry(
"total_amount": d.grand_total,
"outstanding_amount": d.outstanding_amount,
"allocated_amount": d.allocated_amount,
"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(),
"exchange_gain_loss": d.exchange_gain_loss,
"exchange_rate": d.exchange_rate
if d.difference_amount is not None
else payment_entry.get_exchange_rate(),
"exchange_gain_loss": d.difference_amount,
}
if d.voucher_detail_no:

View File

@@ -35,6 +35,8 @@
"purchase_receipt",
"purchase_invoice",
"available_for_use_date",
"total_asset_cost",
"additional_asset_cost",
"column_break_23",
"gross_purchase_amount",
"asset_quantity",
@@ -532,6 +534,22 @@
"label": "Capitalized In",
"options": "Asset Capitalization",
"read_only": 1
},
{
"depends_on": "eval:doc.docstatus > 0",
"fieldname": "total_asset_cost",
"fieldtype": "Currency",
"label": "Total Asset Cost",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"depends_on": "eval:doc.docstatus > 0",
"fieldname": "additional_asset_cost",
"fieldtype": "Currency",
"label": "Additional Asset Cost",
"options": "Company:company:default_currency",
"read_only": 1
}
],
"idx": 72,
@@ -565,7 +583,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2023-11-20 21:05:45.216899",
"modified": "2023-12-21 16:46:20.732869",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -609,4 +627,4 @@
"states": [],
"title_field": "asset_name",
"track_changes": 1
}
}

View File

@@ -48,6 +48,7 @@ class Asset(AccountsController):
if self.get("schedules"):
self.validate_expected_value_after_useful_life()
self.total_asset_cost = self.gross_purchase_amount
self.status = self.get_status()
def on_submit(self):
@@ -427,6 +428,10 @@ class Asset(AccountsController):
n == 0
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
and not self.opening_accumulated_depreciation
and get_updated_rate_of_depreciation_for_wdv_and_dd(
self, value_after_depreciation, finance_book, False
)
== finance_book.rate_of_depreciation
):
from_date = add_days(
self.available_for_use_date, -1
@@ -1387,7 +1392,9 @@ def get_depreciation_amount(
@erpnext.allow_regional
def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb_row):
def get_updated_rate_of_depreciation_for_wdv_and_dd(
asset, depreciable_value, fb_row, show_msg=True
):
return fb_row.rate_of_depreciation

View File

@@ -46,6 +46,10 @@ class AssetRepair(AccountsController):
self.increase_asset_value()
if self.capitalize_repair_cost:
self.asset_doc.total_asset_cost += self.repair_cost
self.asset_doc.additional_asset_cost += self.repair_cost
if self.get("stock_consumption"):
self.check_for_stock_items_and_warehouse()
self.decrease_stock_quantity()
@@ -68,6 +72,10 @@ class AssetRepair(AccountsController):
self.decrease_asset_value()
if self.capitalize_repair_cost:
self.asset_doc.total_asset_cost -= self.repair_cost
self.asset_doc.additional_asset_cost -= self.repair_cost
if self.get("stock_consumption"):
self.increase_stock_quantity()
if self.get("capitalize_repair_cost"):

View File

@@ -123,8 +123,7 @@
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
"reqd": 1,
"search_index": 1
"reqd": 1
},
{
"fieldname": "supplier_part_no",

View File

@@ -80,6 +80,15 @@ class RequestforQuotation(BuyingController):
supplier.quote_status = "Pending"
self.send_to_supplier()
def before_print(self, settings=None):
"""Use the first suppliers data to render the print preview."""
if self.vendor or not self.suppliers:
# If a specific supplier is already set, via Tools > Download PDF,
# we don't want to override it.
return
self.update_supplier_part_no(self.suppliers[0].supplier)
def on_cancel(self):
self.db_set("status", "Cancelled")

View File

@@ -165,6 +165,7 @@ class AccountsController(TransactionBase):
self.disable_pricing_rule_on_internal_transfer()
self.disable_tax_included_prices_for_internal_transfer()
self.set_incoming_rate()
self.init_internal_values()
if self.meta.get_field("currency"):
self.calculate_taxes_and_totals()
@@ -224,6 +225,16 @@ class AccountsController(TransactionBase):
self.set_total_in_words()
def init_internal_values(self):
# init all the internal values as 0 on sa
if self.docstatus.is_draft():
# TODO: Add all such pending values here
fields = ["billed_amt", "delivered_qty"]
for item in self.get("items"):
for field in fields:
if hasattr(item, field):
item.set(field, 0)
def before_cancel(self):
validate_einvoice_fields(self)
@@ -629,6 +640,7 @@ class AccountsController(TransactionBase):
args["doctype"] = self.doctype
args["name"] = self.name
args["child_doctype"] = item.doctype
args["child_docname"] = item.name
args["ignore_pricing_rule"] = (
self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0
@@ -2504,6 +2516,7 @@ def get_advance_payment_entries(
against_all_orders=False,
limit=None,
condition=None,
payment_name=None,
):
party_account_field = "paid_from" if party_type == "Customer" else "paid_to"
currency_field = (
@@ -2526,6 +2539,10 @@ def get_advance_payment_entries(
reference_condition = ""
order_list = []
payment_name_filter = ""
if payment_name:
payment_name_filter = " and t1.name like '%%{0}%%'".format(payment_name)
if not condition:
condition = ""
@@ -2540,7 +2557,7 @@ def get_advance_payment_entries(
where
t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s
and t1.party_type = %s and t1.party = %s and t1.docstatus = 1
and t2.reference_doctype = %s {2} {3}
and t2.reference_doctype = %s {2} {3} {6}
order by t1.posting_date {4}
""".format(
currency_field,
@@ -2549,12 +2566,17 @@ def get_advance_payment_entries(
condition,
limit_cond,
exchange_rate_field,
payment_name_filter,
),
[party_account, payment_type, party_type, party, order_doctype] + order_list,
as_dict=1,
)
if include_unallocated:
payment_name_filter = ""
if payment_name:
payment_name_filter = " and name like '%%{0}%%'".format(payment_name)
unallocated_payment_entries = frappe.db.sql(
"""
select 'Payment Entry' as reference_type, name as reference_name, posting_date,
@@ -2562,10 +2584,15 @@ def get_advance_payment_entries(
from `tabPayment Entry`
where
{0} = %s and party_type = %s and party = %s and payment_type = %s
and docstatus = 1 and unallocated_amount > 0 {condition}
and docstatus = 1 and unallocated_amount > 0 {condition} {4}
order by posting_date {1}
""".format(
party_account_field, limit_cond, exchange_rate_field, currency_field, condition=condition or ""
party_account_field,
limit_cond,
exchange_rate_field,
currency_field,
payment_name_filter,
condition=condition or "",
),
(party_account, party_type, party, payment_type),
as_dict=1,

View File

@@ -354,7 +354,11 @@ class BuyingController(SubcontractingController):
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
else:
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
field = (
"incoming_rate"
if self.get("is_internal_supplier") and not self.doctype == "Purchase Order"
else "rate"
)
rate = flt(
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
* (d.conversion_factor or 1),

View File

@@ -52,10 +52,24 @@ def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part
copy_attributes_to_variant(template, variant)
variant.manufacturer = manufacturer
variant.manufacturer_part_no = manufacturer_part_no
variant.item_code = append_number_if_name_exists("Item", template.name)
variant.flags.ignore_mandatory = True
variant.save()
if not frappe.db.exists(
"Item Manufacturer", {"item_code": variant.name, "manufacturer": manufacturer}
):
manufacturer_doc = frappe.new_doc("Item Manufacturer")
manufacturer_doc.update(
{
"item_code": variant.name,
"manufacturer": manufacturer,
"manufacturer_part_no": manufacturer_part_no,
}
)
manufacturer_doc.flags.ignore_mandatory = True
manufacturer_doc.save(ignore_permissions=True)
return variant

View File

@@ -12,7 +12,7 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.item.item import set_item_default
from erpnext.stock.get_item_details import get_bin_details, get_conversion_factor
from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
class SellingController(StockController):
@@ -422,11 +422,15 @@ class SellingController(StockController):
items = self.get("items") + (self.get("packed_items") or [])
for d in items:
if not self.get("return_against"):
if not self.get("return_against") or (
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
):
# Get incoming rate based on original item cost based on valuation method
qty = flt(d.get("stock_qty") or d.get("actual_qty"))
if not (self.get("is_return") and d.incoming_rate):
if not d.incoming_rate or (
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
):
d.incoming_rate = get_incoming_rate(
{
"item_code": d.item_code,

View File

@@ -433,8 +433,11 @@ class SubcontractingController(StockController):
self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
self.available_materials[key]["batch_no"][batch_no] = 0
if abs(qty) > 0 and not new_rm_obj:
if new_rm_obj:
self.remove(rm_obj)
elif abs(qty) > 0:
self.__set_consumed_qty(rm_obj, qty)
else:
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
self.__set_serial_nos(item_row, rm_obj)
@@ -525,6 +528,10 @@ class SubcontractingController(StockController):
(row.item_code, row.get(self.subcontract_data.order_field))
] -= row.qty
def __reset_idx(self):
for idx, item in enumerate(self.get(self.raw_material_table)):
item.idx = idx + 1
def __prepare_supplied_items(self):
self.initialized_fields()
self.__get_subcontract_orders()
@@ -532,6 +539,7 @@ class SubcontractingController(StockController):
self.get_available_materials()
self.__remove_changed_rows()
self.__set_supplied_items()
self.__reset_idx()
def __validate_batch_no(self, row, key):
if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get(

View File

@@ -514,7 +514,7 @@
"idx": 5,
"image_field": "image",
"links": [],
"modified": "2022-10-13 12:42:04.277879",
"modified": "2023-12-01 18:46:49.468526",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
@@ -576,10 +576,11 @@
],
"search_fields": "lead_name,lead_owner,status",
"sender_field": "email_id",
"sender_name_field": "lead_name",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"subject_field": "title",
"title_field": "title"
}
}

View File

@@ -14,6 +14,7 @@ from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_ema
from erpnext.accounts.party import set_taxes
from erpnext.controllers.selling_controller import SellingController
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
from erpnext.selling.doctype.customer.customer import parse_full_name
class Lead(SellingController, CRMNote):
@@ -48,6 +49,10 @@ class Lead(SellingController, CRMNote):
return
self.contact_doc = self.create_contact()
# leads created by email inbox only have the full name set
if self.lead_name and not any([self.first_name, self.middle_name, self.last_name]):
self.first_name, self.middle_name, self.last_name = parse_full_name(self.lead_name)
def after_insert(self):
self.link_to_contact()

View File

@@ -638,4 +638,5 @@ additional_timeline_content = {
extend_bootinfo = [
"erpnext.support.doctype.service_level_agreement.service_level_agreement.add_sla_doctypes",
"erpnext.startup.boot.bootinfo",
]

View File

@@ -599,6 +599,8 @@ def regenerate_repayment_schedule(loan, cancel=0):
last_repayment_amount = None
last_balance_amount = None
original_repayment_schedule_len = len(loan_doc.get("repayment_schedule"))
for term in reversed(loan_doc.get("repayment_schedule")):
if not term.is_accrued:
next_accrual_date = term.payment_date
@@ -616,7 +618,7 @@ def regenerate_repayment_schedule(loan, cancel=0):
if loan_doc.repayment_method == "Repay Fixed Amount per Period":
monthly_repayment_amount = flt(
balance_amount / len(loan_doc.get("repayment_schedule")) - accrued_entries
balance_amount / (original_repayment_schedule_len - accrued_entries)
)
else:
repayment_period = loan_doc.repayment_periods - accrued_entries

View File

@@ -214,6 +214,7 @@
"options": "\nWork Order\nJob Card"
},
{
"default": "1",
"fieldname": "conversion_rate",
"fieldtype": "Float",
"label": "Conversion Rate",
@@ -606,7 +607,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2023-04-06 12:47:58.514795",
"modified": "2023-12-26 19:34:08.159312",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
@@ -645,4 +646,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -661,6 +661,9 @@ class BOM(WebsiteGenerator):
base_total_rm_cost = 0
for d in self.get("items"):
if not d.is_stock_item and self.rm_cost_as_per == "Valuation Rate":
continue
old_rate = d.rate
d.rate = self.get_rm_rate(
{

View File

@@ -698,6 +698,35 @@ class TestBOM(FrappeTestCase):
bom.update_cost()
self.assertFalse(bom.flags.cost_updated)
def test_bom_with_service_item_cost(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 1000.0}).name
service_item = make_item(properties={"is_stock_item": 0}).name
fg_item = make_item(properties={"is_stock_item": 1}).name
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
bom = make_bom(item=fg_item, raw_materials=[rm_item, service_item], do_not_save=True)
bom.rm_cost_as_per = "Valuation Rate"
for row in bom.items:
if row.item_code == service_item:
row.rate = 566.00
else:
row.rate = 800.00
bom.save()
for row in bom.items:
if row.item_code == service_item:
self.assertEqual(row.is_stock_item, 0)
self.assertEqual(row.rate, 566.00)
else:
self.assertEqual(row.is_stock_item, 1)
def test_do_not_include_manufacturing_and_fixed_items(self):
from erpnext.manufacturing.doctype.bom.bom import item_query

View File

@@ -14,6 +14,7 @@
"bom_no",
"source_warehouse",
"allow_alternative_item",
"is_stock_item",
"section_break_5",
"description",
"col_break1",
@@ -185,7 +186,7 @@
"in_list_view": 1,
"label": "Rate",
"options": "currency",
"read_only": 1,
"read_only_depends_on": "eval:doc.is_stock_item == 1",
"reqd": 1
},
{
@@ -284,13 +285,21 @@
"fieldname": "do_not_explode",
"fieldtype": "Check",
"label": "Do Not Explode"
},
{
"default": "0",
"fetch_from": "item_code.is_stock_item",
"fieldname": "is_stock_item",
"fieldtype": "Check",
"label": "Is Stock Item",
"read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-11-14 18:35:51.378513",
"modified": "2023-12-20 16:21:55.477883",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Item",

View File

@@ -305,6 +305,8 @@ frappe.ui.form.on('Production Plan', {
frappe.throw(__("Select the Warehouse"));
}
frm.set_value("consider_minimum_order_qty", 0);
if (frm.doc.ignore_existing_ordered_qty) {
frm.events.get_items_for_material_requests(frm);
} else {

View File

@@ -48,6 +48,7 @@
"material_request_planning",
"include_non_stock_items",
"include_subcontracted_items",
"consider_minimum_order_qty",
"include_safety_stock",
"ignore_existing_ordered_qty",
"column_break_25",
@@ -423,13 +424,19 @@
"fieldtype": "Link",
"label": "Sub Assembly Warehouse",
"options": "Warehouse"
},
{
"default": "0",
"fieldname": "consider_minimum_order_qty",
"fieldtype": "Check",
"label": "Consider Minimum Order Qty"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-11-03 14:08:11.928027",
"modified": "2023-12-26 16:31:13.740777",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@@ -508,6 +508,7 @@ class ProductionPlan(Document):
if close:
self.db_set("status", "Closed")
self.update_bin_qty()
return
if self.total_produced_qty > 0:
@@ -522,6 +523,9 @@ class ProductionPlan(Document):
if close is not None:
self.db_set("status", self.status)
if self.docstatus == 1 and self.status != "Completed":
self.update_bin_qty()
def update_ordered_status(self):
update_status = False
for d in self.po_items:
@@ -1131,7 +1135,14 @@ def get_subitems(
def get_material_request_items(
row, sales_order, company, ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict
doc,
row,
sales_order,
company,
ignore_existing_ordered_qty,
include_safety_stock,
warehouse,
bin_dict,
):
total_qty = row["qty"]
@@ -1140,8 +1151,14 @@ def get_material_request_items(
required_qty = total_qty
elif total_qty > bin_dict.get("projected_qty", 0):
required_qty = total_qty - bin_dict.get("projected_qty", 0)
if required_qty > 0 and required_qty < row["min_order_qty"]:
if (
doc.get("consider_minimum_order_qty")
and required_qty > 0
and required_qty < row["min_order_qty"]
):
required_qty = row["min_order_qty"]
item_group_defaults = get_item_group_defaults(row.item_code, company)
if not row["purchase_uom"]:
@@ -1479,6 +1496,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
if details.qty > 0:
items = get_material_request_items(
doc,
details,
sales_order,
company,

View File

@@ -1447,6 +1447,70 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(row.get("uom"), "Nos")
self.assertEqual(row.get("conversion_factor"), 10.0)
def test_unreserve_qty_on_closing_of_pp(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_or_make_bin
fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
rm_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
store_warehouse = create_warehouse("Store Warehouse", company="_Test Company")
rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company")
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC")
pln = create_production_plan(
item_code=fg_item, planned_qty=10, stock_uom="_Test UOM 1", do_not_submit=1
)
pln.for_warehouse = rm_warehouse
mr_items = get_items_for_material_requests(pln.as_dict())
for d in mr_items:
pln.append("mr_items", d)
pln.save()
pln.submit()
bin_name = get_or_make_bin(rm_item, rm_warehouse)
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
pln.reload()
pln.set_status(close=True)
bin_name = get_or_make_bin(rm_item, rm_warehouse)
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertAlmostEqual(after_qty, before_qty - 10)
pln.reload()
pln.set_status(close=False)
bin_name = get_or_make_bin(rm_item, rm_warehouse)
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertAlmostEqual(after_qty, before_qty)
def test_min_order_qty_in_pp(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_or_make_bin
fg_item = make_item(properties={"is_stock_item": 1}).name
rm_item = make_item(properties={"is_stock_item": 1, "min_order_qty": 1000}).name
rm_warehouse = create_warehouse("RM Warehouse", company="_Test Company")
make_bom(item=fg_item, raw_materials=[rm_item], source_warehouse="_Test Warehouse - _TC")
pln = create_production_plan(item_code=fg_item, planned_qty=10, do_not_submit=1)
pln.for_warehouse = rm_warehouse
mr_items = get_items_for_material_requests(pln.as_dict())
for d in mr_items:
self.assertEqual(d.get("quantity"), 10.0)
pln.consider_minimum_order_qty = 1
mr_items = get_items_for_material_requests(pln.as_dict())
for d in mr_items:
self.assertEqual(d.get("quantity"), 1000.0)
def create_production_plan(**args):
"""

View File

@@ -352,5 +352,8 @@ erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation
erpnext.patches.v14_0.update_zero_asset_quantity_field
execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
erpnext.patches.v14_0.clear_reconciliation_values_from_singles
erpnext.patches.v14_0.update_total_asset_cost_field
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
erpnext.patches.v14_0.set_maintain_stock_for_bom_item

View File

@@ -0,0 +1,19 @@
import frappe
def execute():
if not frappe.db.exists("BOM", {"docstatus": 1}):
return
# Added is_stock_item to handle Read Only based on condition for the rate field
frappe.db.sql(
"""
UPDATE
`tabBOM Item` boi,
`tabItem` i
SET
boi.is_stock_item = i.is_stock_item
WHERE
boi.item_code = i.name
"""
)

View File

@@ -0,0 +1,17 @@
import frappe
def execute():
asset = frappe.qb.DocType("Asset")
frappe.qb.update(asset).set(asset.total_asset_cost, asset.gross_purchase_amount).run()
asset_repair_list = frappe.db.get_all(
"Asset Repair",
filters={"docstatus": 1, "repair_status": "Completed", "capitalize_repair_cost": 1},
fields=["asset", "repair_cost"],
)
for asset_repair in asset_repair_list:
frappe.qb.update(asset).set(
asset.total_asset_cost, asset.total_asset_cost + asset_repair.repair_cost
).where(asset.name == asset_repair.asset).run()

View File

@@ -482,6 +482,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
cost_center: item.cost_center,
tax_category: me.frm.doc.tax_category,
item_tax_template: item.item_tax_template,
child_doctype: item.doctype,
child_docname: item.name,
is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow,
}

View File

@@ -2,10 +2,16 @@ frappe.provide("erpnext.financial_statements");
erpnext.financial_statements = {
"filters": get_filters(),
"formatter": function(value, row, column, data, default_formatter) {
"formatter": function(value, row, column, data, default_formatter, filter) {
if (data && column.fieldname=="account") {
value = data.account_name || value;
if (filter && filter?.text && filter?.type == "contains") {
if (!value.toLowerCase().includes(filter.text)) {
return value;
}
}
if (data.account) {
column.link_onclick =
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";

View File

@@ -971,7 +971,7 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
}
function get_time_left(timestamp, agreement_status) {
const diff = moment(timestamp).diff(moment());
const diff = moment(timestamp).diff(frappe.datetime.system_datetime(true));
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : 'Failed';
let indicator = (diff_display == 'Failed' && agreement_status != 'Fulfilled') ? 'red' : 'green';
return {'diff_display': diff_display, 'indicator': indicator};

View File

@@ -1,431 +0,0 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.selling");
erpnext.sales_common = {
setup_selling_controller:function() {
erpnext.selling.SellingController = class SellingController extends erpnext.TransactionController {
setup() {
super.setup();
this.toggle_enable_for_stock_uom("allow_to_edit_stock_uom_qty_for_sales");
this.frm.email_field = "contact_email";
}
onload() {
super.onload();
this.setup_queries();
this.frm.set_query('shipping_rule', function() {
return {
filters: {
"shipping_rule_type": "Selling"
}
};
});
}
setup_queries() {
var me = this;
$.each([["customer", "customer"],
["lead", "lead"]],
function(i, opts) {
if(me.frm.fields_dict[opts[0]])
me.frm.set_query(opts[0], erpnext.queries[opts[1]]);
});
me.frm.set_query('contact_person', erpnext.queries.contact_query);
me.frm.set_query('customer_address', erpnext.queries.address_query);
me.frm.set_query('shipping_address_name', erpnext.queries.address_query);
me.frm.set_query('dispatch_address_name', erpnext.queries.dispatch_address_query);
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
if(this.frm.fields_dict.selling_price_list) {
this.frm.set_query("selling_price_list", function() {
return { filters: { selling: 1 } };
});
}
if(this.frm.fields_dict.tc_name) {
this.frm.set_query("tc_name", function() {
return { filters: { selling: 1 } };
});
}
if(!this.frm.fields_dict["items"]) {
return;
}
if(this.frm.fields_dict["items"].grid.get_field('item_code')) {
this.frm.set_query("item_code", "items", function() {
return {
query: "erpnext.controllers.queries.item_query",
filters: {'is_sales_item': 1, 'customer': me.frm.doc.customer, 'has_variants': 0}
}
});
}
if(this.frm.fields_dict["packed_items"] &&
this.frm.fields_dict["packed_items"].grid.get_field('batch_no')) {
this.frm.set_query("batch_no", "packed_items", function(doc, cdt, cdn) {
return me.set_query_for_batch(doc, cdt, cdn)
});
}
if(this.frm.fields_dict["items"].grid.get_field('item_code')) {
this.frm.set_query("item_tax_template", "items", function(doc, cdt, cdn) {
return me.set_query_for_item_tax_template(doc, cdt, cdn)
});
}
}
refresh() {
super.refresh();
frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'}
this.frm.toggle_display("customer_name",
(this.frm.doc.customer_name && this.frm.doc.customer_name!==this.frm.doc.customer));
this.toggle_editable_price_list_rate();
}
customer() {
var me = this;
erpnext.utils.get_party_details(this.frm, null, null, function() {
me.apply_price_list();
});
}
customer_address() {
erpnext.utils.get_address_display(this.frm, "customer_address");
erpnext.utils.set_taxes_from_address(this.frm, "customer_address", "customer_address", "shipping_address_name");
}
shipping_address_name() {
erpnext.utils.get_address_display(this.frm, "shipping_address_name", "shipping_address");
erpnext.utils.set_taxes_from_address(this.frm, "shipping_address_name", "customer_address", "shipping_address_name");
}
dispatch_address_name() {
erpnext.utils.get_address_display(this.frm, "dispatch_address_name", "dispatch_address");
}
sales_partner() {
this.apply_pricing_rule();
}
campaign() {
this.apply_pricing_rule();
}
selling_price_list() {
this.apply_price_list();
this.set_dynamic_labels();
}
discount_percentage(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
item.discount_amount = 0.0;
this.apply_discount_on_item(doc, cdt, cdn, 'discount_percentage');
}
discount_amount(doc, cdt, cdn) {
if(doc.name === cdn) {
return;
}
var item = frappe.get_doc(cdt, cdn);
item.discount_percentage = 0.0;
this.apply_discount_on_item(doc, cdt, cdn, 'discount_amount');
}
commission_rate() {
this.calculate_commission();
}
total_commission() {
frappe.model.round_floats_in(this.frm.doc, ["amount_eligible_for_commission", "total_commission"]);
const { amount_eligible_for_commission } = this.frm.doc;
if(!amount_eligible_for_commission) return;
this.frm.set_value(
"commission_rate", flt(
this.frm.doc.total_commission * 100.0 / amount_eligible_for_commission
)
);
}
allocated_percentage(doc, cdt, cdn) {
var sales_person = frappe.get_doc(cdt, cdn);
if(sales_person.allocated_percentage) {
sales_person.allocated_percentage = flt(sales_person.allocated_percentage,
precision("allocated_percentage", sales_person));
sales_person.allocated_amount = flt(this.frm.doc.amount_eligible_for_commission *
sales_person.allocated_percentage / 100.0,
precision("allocated_amount", sales_person));
refresh_field(["allocated_amount"], sales_person);
this.calculate_incentive(sales_person);
refresh_field(["allocated_percentage", "allocated_amount", "commission_rate","incentives"], sales_person.name,
sales_person.parentfield);
}
}
sales_person(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.calculate_incentive(row);
refresh_field("incentives",row.name,row.parentfield);
}
toggle_editable_price_list_rate() {
var df = frappe.meta.get_docfield(this.frm.doc.doctype + " Item", "price_list_rate", this.frm.doc.name);
var editable_price_list_rate = cint(frappe.defaults.get_default("editable_price_list_rate"));
if(df && editable_price_list_rate) {
const parent_field = frappe.meta.get_parentfield(this.frm.doc.doctype, this.frm.doc.doctype + " Item");
if (!this.frm.fields_dict[parent_field]) return;
this.frm.fields_dict[parent_field].grid.update_docfield_property(
'price_list_rate', 'read_only', 0
);
}
}
calculate_commission() {
if(!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return;
if(this.frm.doc.commission_rate > 100) {
this.frm.set_value("commission_rate", 100);
frappe.throw(`${__(frappe.meta.get_label(
this.frm.doc.doctype, "commission_rate", this.frm.doc.name
))} ${__("cannot be greater than 100")}`);
}
this.frm.doc.amount_eligible_for_commission = this.frm.doc.items.reduce(
(sum, item) => item.grant_commission ? sum + item.base_net_amount : sum, 0
)
this.frm.doc.total_commission = flt(
this.frm.doc.amount_eligible_for_commission * this.frm.doc.commission_rate / 100.0,
precision("total_commission")
);
refresh_field(["amount_eligible_for_commission", "total_commission"]);
}
calculate_contribution() {
var me = this;
$.each(this.frm.doc.doctype.sales_team || [], function(i, sales_person) {
frappe.model.round_floats_in(sales_person);
if (!sales_person.allocated_percentage) return;
sales_person.allocated_amount = flt(
me.frm.doc.amount_eligible_for_commission
* sales_person.allocated_percentage
/ 100.0,
precision("allocated_amount", sales_person)
);
});
}
calculate_incentive(row) {
if(row.allocated_amount)
{
row.incentives = flt(
row.allocated_amount * row.commission_rate / 100.0,
precision("incentives", row));
}
}
set_dynamic_labels() {
super.set_dynamic_labels();
this.set_product_bundle_help(this.frm.doc);
}
set_product_bundle_help(doc) {
if(!this.frm.fields_dict.packing_list) return;
if ((doc.packed_items || []).length) {
$(this.frm.fields_dict.packing_list.row.wrapper).toggle(true);
if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
var help_msg = "<div class='alert alert-warning'>" +
__("For 'Product Bundle' items, Warehouse, Serial No and Batch No will be considered from the 'Packing List' table. If Warehouse and Batch No are same for all packing items for any 'Product Bundle' item, those values can be entered in the main Item table, values will be copied to 'Packing List' table.")+
"</div>";
frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = help_msg;
}
} else {
$(this.frm.fields_dict.packing_list.row.wrapper).toggle(false);
if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = '';
}
}
refresh_field('product_bundle_help');
}
company_address() {
var me = this;
if(this.frm.doc.company_address) {
frappe.call({
method: "frappe.contacts.doctype.address.address.get_address_display",
args: {"address_dict": this.frm.doc.company_address },
callback: function(r) {
if(r.message) {
me.frm.set_value("company_address_display", r.message)
}
}
})
} else {
this.frm.set_value("company_address_display", "");
}
}
conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate) {
super.conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate);
}
qty(doc, cdt, cdn) {
super.qty(doc, cdt, cdn);
}
pick_serial_and_batch(doc, cdt, cdn) {
let item = locals[cdt][cdn];
let me = this;
let path = "assets/erpnext/js/utils/serial_no_batch_selector.js";
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
item.has_serial_no = r.message.has_serial_no;
item.has_batch_no = r.message.has_batch_no;
item.type_of_transaction = item.qty > 0 ? "Outward":"Inward";
item.title = item.has_serial_no ?
__("Select Serial No") : __("Select Batch No");
if (item.has_serial_no && item.has_batch_no) {
item.title = __("Select Serial and Batch");
}
frappe.require(path, function() {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
frappe.model.set_value(item.doctype, item.name, {
"serial_and_batch_bundle": r.name,
"qty": Math.abs(r.total_qty)
});
}
}
);
});
}
});
}
update_auto_repeat_reference(doc) {
if (doc.auto_repeat) {
frappe.call({
method:"frappe.automation.doctype.auto_repeat.auto_repeat.update_reference",
args:{
docname: doc.auto_repeat,
reference:doc.name
},
callback: function(r){
if (r.message=="success") {
frappe.show_alert({message:__("Auto repeat document updated"), indicator:'green'});
} else {
frappe.show_alert({message:__("An error occurred during the update process"), indicator:'red'});
}
}
})
}
}
project() {
let me = this;
if(in_list(["Delivery Note", "Sales Invoice", "Sales Order"], this.frm.doc.doctype)) {
if(this.frm.doc.project) {
frappe.call({
method:'erpnext.projects.doctype.project.project.get_cost_center_name' ,
args: {project: this.frm.doc.project},
callback: function(r, rt) {
if(!r.exc) {
$.each(me.frm.doc["items"] || [], function(i, row) {
if(r.message) {
frappe.model.set_value(row.doctype, row.name, "cost_center", r.message);
frappe.msgprint(__("Cost Center For Item with Item Code {0} has been Changed to {1}", [row.item_name, r.message]));
}
})
}
}
})
}
}
}
coupon_code() {
this.frm.set_value("discount_amount", 0);
this.frm.set_value("additional_discount_percentage", 0);
}
};
}
}
erpnext.pre_sales = {
set_as_lost: function(doctype) {
frappe.ui.form.on(doctype, {
set_as_lost_dialog: function(frm) {
var dialog = new frappe.ui.Dialog({
title: __("Set as Lost"),
fields: [
{
"fieldtype": "Table MultiSelect",
"label": __("Lost Reasons"),
"fieldname": "lost_reason",
"options": frm.doctype === 'Opportunity' ? 'Opportunity Lost Reason Detail': 'Quotation Lost Reason Detail',
"reqd": 1
},
{
"fieldtype": "Table MultiSelect",
"label": __("Competitors"),
"fieldname": "competitors",
"options": "Competitor Detail"
},
{
"fieldtype": "Small Text",
"label": __("Detailed Reason"),
"fieldname": "detailed_reason"
},
],
primary_action: function() {
let values = dialog.get_values();
frm.call({
doc: frm.doc,
method: 'declare_enquiry_lost',
args: {
'lost_reasons_list': values.lost_reason,
'competitors': values.competitors ? values.competitors : [],
'detailed_reason': values.detailed_reason
},
callback: function(r) {
dialog.hide();
frm.reload_doc();
},
});
},
primary_action_label: __('Declare Lost')
});
dialog.show();
}
});
}
}

View File

@@ -307,18 +307,19 @@ class Customer(TransactionBase):
def create_contact(contact, party_type, party, email):
"""Create contact based on given contact name"""
contact = contact.split(" ")
contact = frappe.get_doc(
first, middle, last = parse_full_name(contact)
doc = frappe.get_doc(
{
"doctype": "Contact",
"first_name": contact[0],
"last_name": len(contact) > 1 and contact[1] or "",
"first_name": first,
"middle_name": middle,
"last_name": last,
"is_primary_contact": 1,
}
)
contact.append("email_ids", dict(email_id=email, is_primary=1))
contact.append("links", dict(link_doctype=party_type, link_name=party))
contact.insert()
doc.append("email_ids", dict(email_id=email, is_primary=1))
doc.append("links", dict(link_doctype=party_type, link_name=party))
return doc.insert()
@frappe.whitelist()
@@ -684,24 +685,43 @@ def get_credit_limit(customer, company):
def make_contact(args, is_primary_contact=1):
contact = frappe.get_doc(
{
"doctype": "Contact",
"first_name": args.get("name"),
"is_primary_contact": is_primary_contact,
"links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}],
}
)
values = {
"doctype": "Contact",
"is_primary_contact": is_primary_contact,
"links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}],
}
if args.customer_type == "Individual":
first, middle, last = parse_full_name(args.get("customer_name"))
values.update(
{
"first_name": first,
"middle_name": middle,
"last_name": last,
}
)
else:
values.update(
{
"first_name": args.get("customer_name"),
"company_name": args.get("customer_name"),
}
)
contact = frappe.get_doc(values)
if args.get("email_id"):
contact.add_email(args.get("email_id"), is_primary=True)
if args.get("mobile_no"):
contact.add_phone(args.get("mobile_no"), is_primary_mobile_no=True)
contact.insert()
if flags := args.get("flags"):
contact.insert(ignore_permissions=flags.get("ignore_permissions"))
else:
contact.insert()
return contact
def make_address(args, is_primary_address=1):
def make_address(args, is_primary_address=1, is_shipping_address=1):
reqd_fields = []
for field in ["city", "country"]:
if not args.get(field):
@@ -717,16 +737,23 @@ def make_address(args, is_primary_address=1):
address = frappe.get_doc(
{
"doctype": "Address",
"address_title": args.get("name"),
"address_title": args.get("customer_name"),
"address_line1": args.get("address_line1"),
"address_line2": args.get("address_line2"),
"city": args.get("city"),
"state": args.get("state"),
"pincode": args.get("pincode"),
"country": args.get("country"),
"is_primary_address": is_primary_address,
"is_shipping_address": is_shipping_address,
"links": [{"link_doctype": args.get("doctype"), "link_name": args.get("name")}],
}
).insert()
)
if flags := args.get("flags"):
address.insert(ignore_permissions=flags.get("ignore_permissions"))
else:
address.insert()
return address
@@ -747,3 +774,13 @@ def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, fil
.where((dlink.link_name == customer) & (con.name.like(f"%{txt}%")))
.run()
)
def parse_full_name(full_name: str) -> tuple[str, str | None, str | None]:
"""Parse full name into first name, middle name and last name"""
names = full_name.split()
first_name = names[0]
middle_name = " ".join(names[1:-1]) if len(names) > 2 else None
last_name = names[-1] if len(names) > 1 else None
return first_name, middle_name, last_name

View File

@@ -10,7 +10,11 @@ from frappe.utils import flt
from erpnext.accounts.party import get_due_date
from erpnext.exceptions import PartyDisabled, PartyFrozen
from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding
from erpnext.selling.doctype.customer.customer import (
get_credit_limit,
get_customer_outstanding,
parse_full_name,
)
from erpnext.tests.utils import create_test_contact_and_address
test_ignore = ["Price List"]
@@ -373,6 +377,22 @@ class TestCustomer(FrappeTestCase):
frappe.db.set_value("Selling Settings", None, "cust_master_name", "Customer Name")
def test_parse_full_name(self):
first, middle, last = parse_full_name("John")
self.assertEqual(first, "John")
self.assertEqual(middle, None)
self.assertEqual(last, None)
first, middle, last = parse_full_name("John Doe")
self.assertEqual(first, "John")
self.assertEqual(middle, None)
self.assertEqual(last, "Doe")
first, middle, last = parse_full_name("John Michael Doe")
self.assertEqual(first, "John")
self.assertEqual(middle, "Michael")
self.assertEqual(last, "Doe")
def get_customer_dict(customer_name):
return {

View File

@@ -3,11 +3,11 @@
import frappe
from frappe import _
from frappe import _, qb
from frappe.query_builder import Criterion
from erpnext import get_default_company
from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_price_list_rate_for
def execute(filters=None):
@@ -50,6 +50,42 @@ def get_columns(filters=None):
]
def fetch_item_prices(
customer: str = None, price_list: str = None, selling_price_list: str = None, items: list = None
):
price_list_map = frappe._dict()
ip = qb.DocType("Item Price")
and_conditions = []
or_conditions = []
if items:
and_conditions.append(ip.item_code.isin([x.item_code for x in items]))
and_conditions.append(ip.selling == True)
or_conditions.append(ip.customer == None)
or_conditions.append(ip.price_list == None)
if customer:
or_conditions.append(ip.customer == customer)
if price_list:
or_conditions.append(ip.price_list == price_list)
if selling_price_list:
or_conditions.append(ip.price_list == selling_price_list)
res = (
qb.from_(ip)
.select(ip.item_code, ip.price_list, ip.price_list_rate)
.where(Criterion.all(and_conditions))
.where(Criterion.any(or_conditions))
.run(as_dict=True)
)
for x in res:
price_list_map.update({(x.item_code, x.price_list): x.price_list_rate})
return price_list_map
def get_data(filters=None):
data = []
customer_details = get_customer_details(filters)
@@ -59,9 +95,17 @@ def get_data(filters=None):
"Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code"
)
item_stock_map = {item.item_code: item.available for item in item_stock_map}
price_list_map = fetch_item_prices(
customer_details.customer,
customer_details.price_list,
customer_details.selling_price_list,
items,
)
for item in items:
price_list_rate = get_price_list_rate_for(customer_details, item.item_code) or 0.0
price_list_rate = price_list_map.get(
(item.item_code, customer_details.price_list or customer_details.selling_price_list), 0.0
)
available_stock = item_stock_map.get(item.item_code)
data.append(

View File

@@ -200,6 +200,10 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
item.serial_no = null;
}
if (doc.docstatus === 0 && doc.is_return && !doc.return_against) {
item.incoming_rate = 0.0;
}
var has_batch_no;
frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_batch_no', (r) => {
has_batch_no = r && r.has_batch_no;
@@ -428,6 +432,11 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
})
}
}
coupon_code() {
this.frm.set_value("discount_amount", 0);
this.frm.set_value("additional_discount_percentage", 0);
}
};
frappe.ui.form.on(cur_frm.doctype,"project", function(frm) {

View File

@@ -73,3 +73,11 @@ def update_page_info(bootinfo):
"Sales Person Tree": {"title": "Sales Person Tree", "route": "Tree/Sales Person"},
}
)
def bootinfo(bootinfo):
if bootinfo.get("user") and bootinfo["user"].get("name"):
bootinfo["user"]["employee"] = ""
employee = frappe.db.get_value("Employee", {"user_id": bootinfo["user"]["name"]}, "name")
if employee:
bootinfo["user"]["employee"] = employee

View File

@@ -1,5 +1,5 @@
import frappe
from frappe.utils import cint
from frappe.utils.deprecations import deprecated
def get_leaderboards():
@@ -54,12 +54,13 @@ def get_leaderboards():
@frappe.whitelist()
def get_all_customers(date_range, company, field, limit=None):
filters = [["docstatus", "=", "1"], ["company", "=", company]]
from_date, to_date = parse_date_range(date_range)
if field == "outstanding_amount":
filters = [["docstatus", "=", "1"], ["company", "=", company]]
if date_range:
date_range = frappe.parse_json(date_range)
filters.append(["posting_date", ">=", "between", [date_range[0], date_range[1]]])
return frappe.db.get_all(
if from_date and to_date:
filters.append(["posting_date", "between", [from_date, to_date]])
return frappe.get_list(
"Sales Invoice",
fields=["customer as name", "sum(outstanding_amount) as value"],
filters=filters,
@@ -69,26 +70,20 @@ def get_all_customers(date_range, company, field, limit=None):
)
else:
if field == "total_sales_amount":
select_field = "sum(so_item.base_net_amount)"
select_field = "base_net_total"
elif field == "total_qty_sold":
select_field = "sum(so_item.stock_qty)"
select_field = "total_qty"
date_condition = get_date_condition(date_range, "so.transaction_date")
if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.db.sql(
"""
select so.customer as name, {0} as value
FROM `tabSales Order` as so JOIN `tabSales Order Item` as so_item
ON so.name = so_item.parent
where so.docstatus = 1 {1} and so.company = %s
group by so.customer
order by value DESC
limit %s
""".format(
select_field, date_condition
),
(company, cint(limit)),
as_dict=1,
return frappe.get_list(
"Sales Order",
fields=["customer as name", f"sum({select_field}) as value"],
filters=filters,
group_by="customer",
order_by="value desc",
limit=limit,
)
@@ -96,55 +91,58 @@ def get_all_customers(date_range, company, field, limit=None):
def get_all_items(date_range, company, field, limit=None):
if field in ("available_stock_qty", "available_stock_value"):
select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)"
return frappe.db.get_all(
results = frappe.db.get_all(
"Bin",
fields=["item_code as name", "{0} as value".format(select_field)],
group_by="item_code",
order_by="value desc",
limit=limit,
)
readable_active_items = set(frappe.get_list("Item", filters={"disabled": 0}, pluck="name"))
return [item for item in results if item["name"] in readable_active_items]
else:
if field == "total_sales_amount":
select_field = "sum(order_item.base_net_amount)"
select_field = "base_net_amount"
select_doctype = "Sales Order"
elif field == "total_purchase_amount":
select_field = "sum(order_item.base_net_amount)"
select_field = "base_net_amount"
select_doctype = "Purchase Order"
elif field == "total_qty_sold":
select_field = "sum(order_item.stock_qty)"
select_field = "stock_qty"
select_doctype = "Sales Order"
elif field == "total_qty_purchased":
select_field = "sum(order_item.stock_qty)"
select_field = "stock_qty"
select_doctype = "Purchase Order"
date_condition = get_date_condition(date_range, "sales_order.transaction_date")
filters = [["docstatus", "=", "1"], ["company", "=", company]]
from_date, to_date = parse_date_range(date_range)
if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.db.sql(
"""
select order_item.item_code as name, {0} as value
from `tab{1}` sales_order join `tab{1} Item` as order_item
on sales_order.name = order_item.parent
where sales_order.docstatus = 1
and sales_order.company = %s {2}
group by order_item.item_code
order by value desc
limit %s
""".format(
select_field, select_doctype, date_condition
),
(company, cint(limit)),
as_dict=1,
) # nosec
child_doctype = f"{select_doctype} Item"
return frappe.get_list(
select_doctype,
fields=[
f"`tab{child_doctype}`.item_code as name",
f"sum(`tab{child_doctype}`.{select_field}) as value",
],
filters=filters,
order_by="value desc",
group_by=f"`tab{child_doctype}`.item_code",
limit=limit,
)
@frappe.whitelist()
def get_all_suppliers(date_range, company, field, limit=None):
filters = [["docstatus", "=", "1"], ["company", "=", company]]
from_date, to_date = parse_date_range(date_range)
if field == "outstanding_amount":
filters = [["docstatus", "=", "1"], ["company", "=", company]]
if date_range:
date_range = frappe.parse_json(date_range)
filters.append(["posting_date", "between", [date_range[0], date_range[1]]])
return frappe.db.get_all(
if from_date and to_date:
filters.append(["posting_date", "between", [from_date, to_date]])
return frappe.get_list(
"Purchase Invoice",
fields=["supplier as name", "sum(outstanding_amount) as value"],
filters=filters,
@@ -154,48 +152,40 @@ def get_all_suppliers(date_range, company, field, limit=None):
)
else:
if field == "total_purchase_amount":
select_field = "sum(purchase_order_item.base_net_amount)"
select_field = "base_net_total"
elif field == "total_qty_purchased":
select_field = "sum(purchase_order_item.stock_qty)"
select_field = "total_qty"
date_condition = get_date_condition(date_range, "purchase_order.modified")
if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.db.sql(
"""
select purchase_order.supplier as name, {0} as value
FROM `tabPurchase Order` as purchase_order LEFT JOIN `tabPurchase Order Item`
as purchase_order_item ON purchase_order.name = purchase_order_item.parent
where
purchase_order.docstatus = 1
{1}
and purchase_order.company = %s
group by purchase_order.supplier
order by value DESC
limit %s""".format(
select_field, date_condition
),
(company, cint(limit)),
as_dict=1,
) # nosec
return frappe.get_list(
"Purchase Order",
fields=["supplier as name", f"sum({select_field}) as value"],
filters=filters,
group_by="supplier",
order_by="value desc",
limit=limit,
)
@frappe.whitelist()
def get_all_sales_partner(date_range, company, field, limit=None):
if field == "total_sales_amount":
select_field = "sum(`base_net_total`)"
select_field = "base_net_total"
elif field == "total_commission":
select_field = "sum(`total_commission`)"
select_field = "total_commission"
filters = {"sales_partner": ["!=", ""], "docstatus": 1, "company": company}
if date_range:
date_range = frappe.parse_json(date_range)
filters["transaction_date"] = ["between", [date_range[0], date_range[1]]]
filters = [["docstatus", "=", "1"], ["company", "=", company], ["sales_partner", "is", "set"]]
from_date, to_date = parse_date_range(date_range)
if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.get_list(
"Sales Order",
fields=[
"`sales_partner` as name",
"{} as value".format(select_field),
"sales_partner as name",
f"sum({select_field}) as value",
],
filters=filters,
group_by="sales_partner",
@@ -206,27 +196,29 @@ def get_all_sales_partner(date_range, company, field, limit=None):
@frappe.whitelist()
def get_all_sales_person(date_range, company, field=None, limit=0):
date_condition = get_date_condition(date_range, "sales_order.transaction_date")
filters = [
["docstatus", "=", "1"],
["company", "=", company],
["Sales Team", "sales_person", "is", "set"],
]
from_date, to_date = parse_date_range(date_range)
if from_date and to_date:
filters.append(["transaction_date", "between", [from_date, to_date]])
return frappe.db.sql(
"""
select sales_team.sales_person as name, sum(sales_order.base_net_total) as value
from `tabSales Order` as sales_order join `tabSales Team` as sales_team
on sales_order.name = sales_team.parent and sales_team.parenttype = 'Sales Order'
where sales_order.docstatus = 1
and sales_order.company = %s
{date_condition}
group by sales_team.sales_person
order by value DESC
limit %s
""".format(
date_condition=date_condition
),
(company, cint(limit)),
as_dict=1,
return frappe.get_list(
"Sales Order",
fields=[
"`tabSales Team`.sales_person as name",
"sum(`tabSales Team`.allocated_amount) as value",
],
filters=filters,
group_by="`tabSales Team`.sales_person",
order_by="value desc",
limit=limit,
)
@deprecated
def get_date_condition(date_range, field):
date_condition = ""
if date_range:
@@ -236,3 +228,11 @@ def get_date_condition(date_range, field):
field, frappe.db.escape(from_date), frappe.db.escape(to_date)
)
return date_condition
def parse_date_range(date_range):
if date_range:
date_range = frappe.parse_json(date_range)
return date_range[0], date_range[1]
return None, None

View File

@@ -51,8 +51,7 @@
"oldfieldtype": "Link",
"options": "Item",
"read_only": 1,
"reqd": 1,
"search_index": 1
"reqd": 1
},
{
"default": "0.00",

View File

@@ -301,7 +301,8 @@
"no_copy": 1,
"options": "Delivery Note",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"collapsible": 1,
@@ -1400,7 +1401,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2023-09-04 14:15:28.363184",
"modified": "2023-12-18 17:19:39.368239",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@@ -997,7 +997,3 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
)
return doclist
def on_doctype_update():
frappe.db.add_index("Delivery Note", ["customer", "is_return", "return_against"])

View File

@@ -0,0 +1,27 @@
import click
import frappe
UNUSED_INDEXES = [
("Delivery Note", ["customer", "is_return", "return_against"]),
("Sales Invoice", ["customer", "is_return", "return_against"]),
("Purchase Invoice", ["supplier", "is_return", "return_against"]),
("Purchase Receipt", ["supplier", "is_return", "return_against"]),
]
def execute():
for doctype, index_fields in UNUSED_INDEXES:
table = f"tab{doctype}"
index_name = frappe.db.get_index_name(index_fields)
drop_index_if_exists(table, index_name)
def drop_index_if_exists(table: str, index: str):
if not frappe.db.has_index(table, index):
return
try:
frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`")
click.echo(f"✓ dropped {index} index from {table}")
except Exception:
frappe.log_error("Failed to drop index")

View File

@@ -1332,6 +1332,109 @@ class TestDeliveryNote(FrappeTestCase):
dn.reload()
self.assertFalse(dn.items[0].target_warehouse)
def test_sales_return_valuation_for_moving_average(self):
item_code = make_item(
"_Test Item Sales Return with MA", {"is_stock_item": 1, "valuation_method": "Moving Average"}
).name
make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=5,
basic_rate=100.0,
posting_date=add_days(nowdate(), -5),
)
dn = create_delivery_note(
item_code=item_code, qty=5, rate=500, posting_date=add_days(nowdate(), -4)
)
self.assertEqual(dn.items[0].incoming_rate, 100.0)
make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=5,
basic_rate=200.0,
posting_date=add_days(nowdate(), -3),
)
make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=5,
basic_rate=300.0,
posting_date=add_days(nowdate(), -2),
)
dn1 = create_delivery_note(
is_return=1,
item_code=item_code,
return_against=dn.name,
qty=-5,
rate=500,
company=dn.company,
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
do_not_submit=1,
posting_date=add_days(nowdate(), -1),
)
# (300 * 5) + (200 * 5) = 2500
# 2500 / 10 = 250
self.assertAlmostEqual(dn1.items[0].incoming_rate, 250.0)
def test_sales_return_valuation_for_moving_average_case2(self):
# Make DN return
# Make Bakcdated Purchase Receipt and check DN return valuation rate
# The rate should be recalculate based on the backdated purchase receipt
frappe.flags.print_debug_messages = False
item_code = make_item(
"_Test Item Sales Return with MA Case2",
{"is_stock_item": 1, "valuation_method": "Moving Average", "stock_uom": "Nos"},
).name
make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=5,
basic_rate=100.0,
posting_date=add_days(nowdate(), -5),
)
dn = create_delivery_note(
item_code=item_code,
warehouse="_Test Warehouse - _TC",
qty=5,
rate=500,
posting_date=add_days(nowdate(), -4),
)
returned_dn = create_delivery_note(
is_return=1,
item_code=item_code,
return_against=dn.name,
qty=-5,
rate=500,
company=dn.company,
warehouse="_Test Warehouse - _TC",
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
posting_date=add_days(nowdate(), -1),
)
self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 100.0)
# Make backdated purchase receipt
make_stock_entry(
item_code=item_code,
target="_Test Warehouse - _TC",
qty=5,
basic_rate=200.0,
posting_date=add_days(nowdate(), -3),
)
returned_dn.reload()
self.assertAlmostEqual(returned_dn.items[0].incoming_rate, 200.0)
def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note")

View File

@@ -522,39 +522,25 @@ class TestItem(FrappeTestCase):
self.assertEqual(factor, 1.0)
def test_item_variant_by_manufacturer(self):
fields = [{"field_name": "description"}, {"field_name": "variant_based_on"}]
set_item_variant_settings(fields)
template = make_item(
"_Test Item Variant By Manufacturer", {"has_variants": 1, "variant_based_on": "Manufacturer"}
).name
if frappe.db.exists("Item", "_Test Variant Mfg"):
frappe.delete_doc("Item", "_Test Variant Mfg")
if frappe.db.exists("Item", "_Test Variant Mfg-1"):
frappe.delete_doc("Item", "_Test Variant Mfg-1")
if frappe.db.exists("Manufacturer", "MSG1"):
frappe.delete_doc("Manufacturer", "MSG1")
for manufacturer in ["DFSS", "DASA", "ASAAS"]:
if not frappe.db.exists("Manufacturer", manufacturer):
m_doc = frappe.new_doc("Manufacturer")
m_doc.short_name = manufacturer
m_doc.insert()
template = frappe.get_doc(
dict(
doctype="Item",
item_code="_Test Variant Mfg",
has_variant=1,
item_group="Products",
variant_based_on="Manufacturer",
)
).insert()
self.assertFalse(frappe.db.exists("Item Manufacturer", {"manufacturer": "DFSS"}))
variant = get_variant(template, manufacturer="DFSS", manufacturer_part_no="DFSS-123")
manufacturer = frappe.get_doc(dict(doctype="Manufacturer", short_name="MSG1")).insert()
item_manufacturer = frappe.db.exists(
"Item Manufacturer", {"manufacturer": "DFSS", "item_code": variant.name}
)
self.assertTrue(item_manufacturer)
variant = get_variant(template.name, manufacturer=manufacturer.name)
self.assertEqual(variant.item_code, "_Test Variant Mfg-1")
self.assertEqual(variant.description, "_Test Variant Mfg")
self.assertEqual(variant.manufacturer, "MSG1")
variant.insert()
variant = get_variant(template.name, manufacturer=manufacturer.name, manufacturer_part_no="007")
self.assertEqual(variant.item_code, "_Test Variant Mfg-2")
self.assertEqual(variant.description, "_Test Variant Mfg")
self.assertEqual(variant.manufacturer, "MSG1")
self.assertEqual(variant.manufacturer_part_no, "007")
frappe.delete_doc("Item Manufacturer", item_manufacturer)
def test_stock_exists_against_template_item(self):
stock_item = frappe.get_all("Stock Ledger Entry", fields=["item_code"], limit=1)

View File

@@ -199,9 +199,8 @@ frappe.ui.form.on('Material Request', {
get_item_data: function(frm, item, overwrite_warehouse=false) {
if (item && !item.item_code) { return; }
frm.call({
frappe.call({
method: "erpnext.stock.get_item_details.get_item_details",
child: item,
args: {
args: {
item_code: item.item_code,

View File

@@ -123,7 +123,9 @@ class MaterialRequest(BuyingController):
def on_submit(self):
self.update_requested_qty_in_production_plan()
self.update_requested_qty()
if self.material_request_type == "Purchase":
if self.material_request_type == "Purchase" and frappe.db.exists(
"Budget", {"applicable_on_material_request": 1, "docstatus": 1}
):
self.validate_budget()
def before_save(self):

View File

@@ -13,7 +13,7 @@ from frappe.model.mapper import map_child_doc
from frappe.query_builder import Case
from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Coalesce, IfNull, Locate, Replace, Sum
from frappe.utils import cint, floor, flt, today
from frappe.utils import ceil, cint, floor, flt, today
from frappe.utils.nestedset import get_descendants_of
from erpnext.selling.doctype.sales_order.sales_order import (
@@ -605,7 +605,7 @@ def get_available_item_locations_for_serialized_item(
.select(sn.name, sn.warehouse)
.where((sn.item_code == item_code) & (sn.company == company))
.orderby(sn.purchase_date)
.limit(cint(required_qty + total_picked_qty))
.limit(ceil(required_qty + total_picked_qty))
)
if from_warehouses:
@@ -647,7 +647,7 @@ def get_available_item_locations_for_batched_item(
.groupby(sle.warehouse, sle.batch_no, sle.item_code)
.having(Sum(sle.actual_qty) > 0)
.orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse)
.limit(cint(required_qty + total_picked_qty))
.limit(ceil(required_qty + total_picked_qty))
)
if from_warehouses:
@@ -680,7 +680,7 @@ def get_available_item_locations_for_serial_and_batched_item(
(conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse)
)
.orderby(sn.purchase_date)
.limit(cint(location.qty + total_picked_qty))
.limit(ceil(location.qty + total_picked_qty))
).run(as_dict=True)
serial_nos = [sn.name for sn in serial_nos]
@@ -699,7 +699,7 @@ def get_available_item_locations_for_other_item(
.select(bin.warehouse, bin.actual_qty.as_("qty"))
.where((bin.item_code == item_code) & (bin.actual_qty > 0))
.orderby(bin.creation)
.limit(cint(required_qty + total_picked_qty))
.limit(ceil(required_qty + total_picked_qty))
)
if from_warehouses:

View File

@@ -78,6 +78,20 @@ frappe.ui.form.on("Purchase Receipt", {
}, __('Create'));
}
if (frm.doc.docstatus === 0) {
if (!frm.doc.is_return) {
frappe.db.get_single_value("Buying Settings", "maintain_same_rate").then((value) => {
if (value) {
frm.doc.items.forEach((item) => {
frm.fields_dict.items.grid.update_docfield_property(
"rate", "read_only", (item.purchase_order && item.purchase_order_item)
);
});
}
});
}
}
frm.events.add_custom_buttons(frm);
},

View File

@@ -288,7 +288,8 @@
"no_copy": 1,
"options": "Purchase Receipt",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "section_addresses",
@@ -1241,7 +1242,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
"modified": "2023-10-01 21:00:44.556816",
"modified": "2023-12-18 17:26:41.279663",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",

View File

@@ -1199,10 +1199,6 @@ def get_item_account_wise_additional_cost(purchase_document):
return item_account_wise_cost
def on_doctype_update():
frappe.db.add_index("Purchase Receipt", ["supplier", "is_return", "return_against"])
@erpnext.allow_regional
def update_regional_gl_entries(gl_list, doc):
return

View File

@@ -352,7 +352,6 @@
"oldfieldtype": "Currency",
"options": "currency",
"print_width": "100px",
"read_only_depends_on": "eval: (!parent.is_return && doc.purchase_order && doc.purchase_order_item)",
"width": "100px"
},
{
@@ -1055,7 +1054,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-11-30 16:12:02.364608",
"modified": "2023-12-25 22:32:09.801965",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@@ -517,7 +517,12 @@ frappe.ui.form.on('Stock Entry', {
},
callback: function(r) {
if (!r.exc) {
["actual_qty", "basic_rate"].forEach((field) => {
let fields = ["actual_qty", "basic_rate"];
if (frm.doc.purpose == "Material Receipt") {
fields = ["actual_qty"];
}
fields.forEach((field) => {
frappe.model.set_value(cdt, cdn, field, (r.message[field] || 0.0));
});
frm.events.calculate_basic_amount(frm, child);

View File

@@ -8,6 +8,7 @@ import frappe
from frappe import _, throw
from frappe.model import child_table_fields, default_fields
from frappe.model.meta import get_field_precision
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import CombineDatetime, IfNull, Sum
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
@@ -391,7 +392,6 @@ def get_basic_details(args, item, overwrite_warehouse=True):
"net_amount": 0.0,
"discount_percentage": 0.0,
"discount_amount": flt(args.discount_amount) or 0.0,
"supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults),
"update_stock": args.get("update_stock")
if args.get("doctype") in ["Sales Invoice", "Purchase Invoice"]
else 0,
@@ -411,6 +411,10 @@ def get_basic_details(args, item, overwrite_warehouse=True):
}
)
default_supplier = get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults)
if default_supplier:
out.supplier = default_supplier
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
out.update(calculate_service_end_date(args, item))
@@ -605,6 +609,9 @@ def get_item_tax_template(args, item, out):
item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
item_group = item_group_doc.parent_item_group
if args.get("child_doctype") and item_tax_template:
out.update(get_fetch_values(args.get("child_doctype"), "item_tax_template", item_tax_template))
def _get_item_tax_template(args, taxes, out=None, for_validate=False):
if out is None:

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import _
from frappe.utils import cint, flt, getdate
from frappe.utils import cint, flt, get_table_name, getdate
from pypika import functions as fn
from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
@@ -12,11 +12,22 @@ from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter
SLE_COUNT_LIMIT = 10_000
def _estimate_table_row_count(doctype: str):
table = get_table_name(doctype)
return cint(
frappe.db.sql(
f"""select table_rows
from information_schema.tables
where table_name = '{table}' ;"""
)[0][0]
)
def execute(filters=None):
if not filters:
filters = {}
sle_count = frappe.db.count("Stock Ledger Entry")
sle_count = _estimate_table_row_count("Stock Ledger Entry")
if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))

View File

@@ -397,7 +397,7 @@ class StockBalanceReport(object):
"fieldname": "bal_val",
"fieldtype": "Currency",
"width": 100,
"options": "currency",
"options": "Company:company:default_currency",
},
{
"label": _("Opening Qty"),
@@ -411,7 +411,7 @@ class StockBalanceReport(object):
"fieldname": "opening_val",
"fieldtype": "Currency",
"width": 110,
"options": "currency",
"options": "Company:company:default_currency",
},
{
"label": _("In Qty"),

View File

@@ -16,6 +16,7 @@ from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions
from erpnext.stock.utils import (
get_incoming_outgoing_rate_for_cancel,
get_incoming_rate,
get_or_make_bin,
get_valuation_method,
)
@@ -701,7 +702,23 @@ class update_entries_after(object):
)
if self.valuation_method == "Moving Average":
rate = flt(self.data[self.args.warehouse].previous_sle.valuation_rate)
rate = get_incoming_rate(
{
"item_code": sle.item_code,
"warehouse": sle.warehouse,
"posting_date": sle.posting_date,
"posting_time": sle.posting_time,
"qty": sle.actual_qty,
"serial_no": sle.get("serial_no"),
"batch_no": sle.get("batch_no"),
"company": sle.company,
"voucher_type": sle.voucher_type,
"voucher_no": sle.voucher_no,
"allow_zero_valuation": self.allow_zero_rate,
"sle": sle.name,
}
)
else:
rate = get_rate_for_return(
sle.voucher_type,

View File

@@ -7,6 +7,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt
from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
from erpnext.buying.doctype.purchase_order.purchase_order import update_status as update_po_status
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.stock_balance import update_bin_qty
from erpnext.stock.utils import get_bin
@@ -234,6 +235,9 @@ class SubcontractingOrder(SubcontractingController):
"Subcontracting Order", self.name, "status", status, update_modified=update_modified
)
if status == "Closed":
update_po_status("Closed", self.purchase_order)
@frappe.whitelist()
def make_subcontracting_receipt(source_name, target_doc=None):

View File

@@ -111,13 +111,13 @@ class SubcontractingReceipt(SubcontractingController):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.update_status_updater_args()
self.update_prevdoc_status()
self.delete_auto_created_batches()
self.set_consumed_qty_in_subcontract_order()
self.set_subcontracting_order_status()
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
self.update_status()
self.delete_auto_created_batches()
@frappe.whitelist()
def set_missing_values(self):

View File

@@ -655,6 +655,79 @@ class TestSubcontractingReceipt(FrappeTestCase):
self.assertEqual(rm_item.rate, 100)
self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate)
def test_subcontracting_receipt_cancel_with_batch(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
# Step - 1: Set Backflush Based On as "BOM"
set_backflush_based_on("BOM")
# Step - 2: Create FG and RM Items
fg_item = make_item(
properties={"is_stock_item": 1, "is_sub_contracted_item": 1, "has_batch_no": 1}
).name
rm_item1 = make_item(properties={"is_stock_item": 1}).name
rm_item2 = make_item(properties={"is_stock_item": 1}).name
make_item("Subcontracted Service Item Test For Batch 1", {"is_stock_item": 0})
# Step - 3: Create BOM for FG Item
bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2])
for rm_item in bom.items:
self.assertEqual(rm_item.rate, 0)
self.assertEqual(rm_item.amount, 0)
bom = bom.name
# Step - 4: Create PO and SCO
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item Test For Batch 1",
"qty": 100,
"rate": 100,
"fg_item": fg_item,
"fg_item_qty": 100,
},
]
sco = get_subcontracting_order(service_items=service_items)
for rm_item in sco.supplied_items:
self.assertEqual(rm_item.rate, 0)
self.assertEqual(rm_item.amount, 0)
# Step - 5: Inward Raw Materials
rm_items = get_rm_items(sco.supplied_items)
for rm_item in rm_items:
rm_item["rate"] = 100
itemwise_details = make_stock_in_entry(rm_items=rm_items)
# Step - 6: Transfer RM's to Subcontractor
se = make_stock_transfer_entry(
sco_no=sco.name,
rm_items=rm_items,
itemwise_details=copy.deepcopy(itemwise_details),
)
for item in se.items:
self.assertEqual(item.qty, 100)
self.assertEqual(item.basic_rate, 100)
self.assertEqual(item.amount, item.qty * item.basic_rate)
batch_doc = frappe.get_doc(
{
"doctype": "Batch",
"item": fg_item,
"batch_id": frappe.generate_hash(length=10),
}
).insert(ignore_permissions=True)
# Step - 7: Create Subcontracting Receipt
scr = make_subcontracting_receipt(sco.name)
scr.items[0].batch_no = batch_doc.batch_id
scr.save()
scr.submit()
scr.load_from_db()
# Step - 8: Cancel Subcontracting Receipt
scr.cancel()
self.assertTrue(scr.docstatus == 2)
def make_return_subcontracting_receipt(**args):
args = frappe._dict(args)

View File

@@ -58,7 +58,9 @@ frappe.ui.form.on("Issue", {
frappe.call("erpnext.support.doctype.service_level_agreement.service_level_agreement.reset_service_level_agreement", {
reason: values.reason,
user: frappe.session.user_email
user: frappe.session.user_email,
doctype: frm.doc.doctype,
docname: frm.doc.name,
}, () => {
reset_sla.enable_primary_action();
frm.refresh();

View File

@@ -739,10 +739,12 @@ def get_response_and_resolution_duration(doc):
return priority
def reset_service_level_agreement(doc, reason, user):
@frappe.whitelist()
def reset_service_level_agreement(doctype: str, docname: str, reason, user):
if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"):
frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings."))
doc = frappe.get_doc(doctype, docname)
frappe.get_doc(
{
"doctype": "Comment",

View File

@@ -0,0 +1,24 @@
import frappe
from frappe.tests.utils import FrappeTestCase
INDEXED_FIELDS = {
"Bin": ["item_code"],
"GL Entry": ["voucher_type", "against_voucher_type"],
"Purchase Order Item": ["item_code"],
"Stock Ledger Entry": ["warehouse"],
}
class TestPerformance(FrappeTestCase):
def test_ensure_indexes(self):
# These fields are not explicitly indexed BUT they are prefix in some
# other composite index. If those are removed this test should be
# updated accordingly.
for doctype, fields in INDEXED_FIELDS.items():
for field in fields:
self.assertTrue(
frappe.db.sql(
f"""SHOW INDEX FROM `tab{doctype}`
WHERE Column_name = "{field}" AND Seq_in_index = 1"""
)
)

View File

@@ -9104,3 +9104,7 @@ Select an item from each set to be used in the Sales Order.,"Wählen Sie aus den
Is Alternative,Ist Alternative,
Alternative Items,Alternativpositionen,
Component Type,Komponententyp,
Lost Quotations,Verlorene Angebote,
Lost Quotations %,Verlorene Angebote %,
Lost Value,Verlorener Wert,
Lost Value %,Verlorener Wert %,
Can't render this file because it is too large.

View File

@@ -98,6 +98,7 @@ class TransactionBase(StatusUpdater):
"Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
)
stop_actions = []
for ref_dt, ref_dn_field, ref_link_field in ref_details:
reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
reference_details = self.get_reference_details(reference_names, ref_dt + " Item")
@@ -108,7 +109,7 @@ class TransactionBase(StatusUpdater):
if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
if action == "Stop":
if role_allowed_to_override not in frappe.get_roles():
frappe.throw(
stop_actions.append(
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate
)
@@ -121,6 +122,8 @@ class TransactionBase(StatusUpdater):
title=_("Warning"),
indicator="orange",
)
if stop_actions:
frappe.throw(stop_actions, as_list=True)
def get_reference_details(self, reference_names, reference_doctype):
return frappe._dict(