Compare commits

..

97 Commits

Author SHA1 Message Date
Frappe PR Bot
2a5164d170 chore(release): Bumped to Version 15.7.0
# [15.7.0](https://github.com/frappe/erpnext/compare/v15.6.1...v15.7.0) (2023-12-20)

### Bug Fixes

* asset patch failure due to missing shift_based column (backport [#38776](https://github.com/frappe/erpnext/issues/38776)) ([#38777](https://github.com/frappe/erpnext/issues/38777)) ([6e92c78](6e92c78cbd))
* barcode scanning for the stock entry (backport [#38716](https://github.com/frappe/erpnext/issues/38716)) ([#38718](https://github.com/frappe/erpnext/issues/38718)) ([1dcb065](1dcb065c64))
* **demo:** Demo setup for canadian COA ([7d84441](7d844411fb))
* fetch exc rate of multi currency journals ([f55b561](f55b561ff9))
* fetch item_tax_template values if fields with fetch_from exisit ([fba28d6](fba28d6941))
* groups for current accounts in German CoAs ([58de991](58de9913b9))
* homepage not working (backport [#38755](https://github.com/frappe/erpnext/issues/38755)) ([#38756](https://github.com/frappe/erpnext/issues/38756)) ([ad3a5b5](ad3a5b58e4))
* if not budget then don't validate (backport [#38861](https://github.com/frappe/erpnext/issues/38861)) ([#38865](https://github.com/frappe/erpnext/issues/38865)) ([5ec75fb](5ec75fb6df))
* incoming rate for sales return with Moving Average valuation method (backport [#38849](https://github.com/frappe/erpnext/issues/38849)) (backport [#38863](https://github.com/frappe/erpnext/issues/38863)) ([#38866](https://github.com/frappe/erpnext/issues/38866)) ([d65be69](d65be69c4c))
* incorrect available qty for backdated stock reco with batch (backport [#37858](https://github.com/frappe/erpnext/issues/37858)) ([#38811](https://github.com/frappe/erpnext/issues/38811)) ([18bd330](18bd330a59))
* Init internal child table values ([7802f6c](7802f6c528))
* item variant with manufacturer (backport [#38845](https://github.com/frappe/erpnext/issues/38845)) (backport [#38847](https://github.com/frappe/erpnext/issues/38847)) ([#38851](https://github.com/frappe/erpnext/issues/38851)) ([7320440](7320440b61))
* not able to cancel SCR with Batch (backport [#38817](https://github.com/frappe/erpnext/issues/38817)) (backport [#38821](https://github.com/frappe/erpnext/issues/38821)) ([#38829](https://github.com/frappe/erpnext/issues/38829)) ([4bd1a5f](4bd1a5f955))
* not able to make inter-company po from so (backport [#38826](https://github.com/frappe/erpnext/issues/38826)) (backport [#38828](https://github.com/frappe/erpnext/issues/38828)) ([#38833](https://github.com/frappe/erpnext/issues/38833)) ([02ceee6](02ceee6669))
* on closed unreserved the production plan qty (backport [#38848](https://github.com/frappe/erpnext/issues/38848)) (backport [#38859](https://github.com/frappe/erpnext/issues/38859)) ([#38862](https://github.com/frappe/erpnext/issues/38862)) ([cbbc6af](cbbc6af128))
* **pe:** show split alert only on splitting ([77dba48](77dba4834c))
* Reset SLA on issue doesn't work (backport [#38789](https://github.com/frappe/erpnext/issues/38789)) ([#38791](https://github.com/frappe/erpnext/issues/38791)) ([2045306](2045306283))
* serial and batch bundle return not working (backport [#38754](https://github.com/frappe/erpnext/issues/38754)) ([#38806](https://github.com/frappe/erpnext/issues/38806)) ([8990c48](8990c48e7b))
* set `fg-itm-qty` based on `qty` instead of the other way round in Subcontracting POs (backport [#38842](https://github.com/frappe/erpnext/issues/38842)) ([#38855](https://github.com/frappe/erpnext/issues/38855)) ([4e27174](4e27174c85))
* show bill_date and bill_no in Purchase Register ([4ed86db](4ed86dbff2))
* skip jvs against bank accounts ([bf8a2d0](bf8a2d0e3a))
* supplier removed on selection of item (backport [#38712](https://github.com/frappe/erpnext/issues/38712)) ([#38713](https://github.com/frappe/erpnext/issues/38713)) ([feb452b](feb452b740))
* timezone aware SLA banner (backport [#38745](https://github.com/frappe/erpnext/issues/38745)) ([#38747](https://github.com/frappe/erpnext/issues/38747)) ([a551660](a551660d2a))
* **ux:** don't override Item Name and Description in MR (backport [#38720](https://github.com/frappe/erpnext/issues/38720)) ([#38763](https://github.com/frappe/erpnext/issues/38763)) ([703be50](703be50bc7))
* validation error on reconciling PE to Journals as Invoice ([aa5e16e](aa5e16e681))
* wrong currency in Stock Balance report (backport [#38778](https://github.com/frappe/erpnext/issues/38778)) ([#38780](https://github.com/frappe/erpnext/issues/38780)) ([7fc8150](7fc8150617))
* wrong paid and cn amount on pos invoice ([cff9e47](cff9e47162))

### Features

* RFQ print preview ([f30bede](f30bede2e0))
* set lead name from email ([f770621](f7706211ea))

### Performance Improvements

* index `return_against` on delivery note (backport [#38827](https://github.com/frappe/erpnext/issues/38827)) ([#38832](https://github.com/frappe/erpnext/issues/38832)) ([6ad75e7](6ad75e72e6))
2023-12-20 04:47:03 +00:00
Deepesh Garg
028228ee12 Merge pull request #38853 from frappe/version-15-hotfix
chore: release v15
2023-12-20 10:15:54 +05:30
mergify[bot]
4e27174c85 fix: set fg-itm-qty based on qty instead of the other way round in Subcontracting POs (backport #38842) (#38855)
fix: set `fg-itm-qty` based on `qty` instead of the other way round

(cherry picked from commit a99d0a65b0)

Co-authored-by: Gughan Ravikumar <gughanrk@gmail.com>
2023-12-20 09:12:20 +05:30
mergify[bot]
d65be69c4c fix: incoming rate for sales return with Moving Average valuation method (backport #38849) (backport #38863) (#38866)
fix: incoming rate for sales return with Moving Average valuation method (backport #38849) (#38863)

* fix: incoming rate for sales return with Moving Average valuation method (#38849)

(cherry picked from commit 7fdac62393)

# Conflicts:
#	erpnext/stock/doctype/delivery_note/test_delivery_note.py

* chore: fix conflicts

---------

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

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2023-12-19 19:13:08 +05:30
mergify[bot]
5ec75fb6df fix: if not budget then don't validate (backport #38861) (#38865)
fix: if not budget then don't validate (#38861)

(cherry picked from commit d375164100)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-19 18:30:36 +05:30
mergify[bot]
cbbc6af128 fix: on closed unreserved the production plan qty (backport #38848) (backport #38859) (#38862)
fix: on closed unreserved the production plan qty (backport #38848) (#38859)

fix: on closed unreserved the production plan qty (#38848)

(cherry picked from commit 2184e8ef58)

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

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2023-12-19 18:18:40 +05:30
mergify[bot]
7320440b61 fix: item variant with manufacturer (backport #38845) (backport #38847) (#38851)
fix: item variant with manufacturer (backport #38845) (#38847)

* fix: item variant with manufacturer (#38845)

(cherry picked from commit e0c8ff10da)

* chore: fix test case

---------

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

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2023-12-19 15:17:54 +05:30
ruthra kumar
637343f751 Merge pull request #38844 from frappe/mergify/bp/version-15-hotfix/pr-38797
fix: wrong paid and cn amount on pos invoice (backport #38797)
2023-12-19 11:33:23 +05:30
Dany Robert
ffb6d65910 test: partial payment for pos invoice
(cherry picked from commit 8772628912)
2023-12-19 05:33:15 +00:00
Dany Robert
cff9e47162 fix: wrong paid and cn amount on pos invoice
(cherry picked from commit 5cb5e09dbb)
2023-12-19 05:33:15 +00:00
mergify[bot]
02ceee6669 fix: not able to make inter-company po from so (backport #38826) (backport #38828) (#38833)
fix: not able to make inter-company po from so (backport #38826) (#38828)

fix: not able to make inter-company po from so (#38826)

(cherry picked from commit 23042dfc3c)

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

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2023-12-18 18:31:28 +05:30
mergify[bot]
4bd1a5f955 fix: not able to cancel SCR with Batch (backport #38817) (backport #38821) (#38829)
fix: not able to cancel SCR with Batch (backport #38817) (#38821)

* fix: not able to cancel SCR with Batch (#38817)

(cherry picked from commit fb5090fd3f)

# Conflicts:
#	erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py

* chore: fix test case

* chore: fix test case

* chore: fix test case

* chore: fix test case

---------

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

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2023-12-18 18:13:15 +05:30
mergify[bot]
6ad75e72e6 perf: index return_against on delivery note (backport #38827) (#38832)
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)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-12-18 18:09:42 +05:30
ruthra kumar
c696370cff Merge pull request #38823 from frappe/mergify/bp/version-15-hotfix/pr-38820
refactor: ignore ERR journals in Statment of Accounts (backport #38820)
2023-12-18 16:24:48 +05:30
ruthra kumar
5a807af505 refactor: ignore ERR journals in Statment of Accounts
(cherry picked from commit 39ef75e2d0)
2023-12-18 10:26:30 +00:00
Raffael Meyer
4fe7988249 Merge pull request #38814 from frappe/mergify/bp/version-15-hotfix/pr-38803
fix: groups for current accounts in German CoAs (backport #38803)
2023-12-17 19:52:24 +01:00
barredterra
58de9913b9 fix: groups for current accounts in German CoAs
(cherry picked from commit 259f313af7)
2023-12-17 18:24:07 +00:00
Deepesh Garg
b52ceceeb7 Merge pull request #38808 from vorasmit/fetch-item-taxes-v15
fix: fetch item_tax_template values if fields with fetch_from exisit (#38284)
2023-12-17 18:51:05 +05:30
mergify[bot]
8990c48e7b fix: serial and batch bundle return not working (backport #38754) (#38806)
fix: serial and batch bundle return not working (#38754)

* fix: serial and batch bundle return not working

* test: added test case for delivery note return against denormalized serial no

(cherry picked from commit 0743289925)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-17 18:02:31 +05:30
mergify[bot]
18bd330a59 fix: incorrect available qty for backdated stock reco with batch (backport #37858) (#38811)
fix: incorrect available qty for backdated stock reco with batch (#37858)

* fix: incorrect available qty for backdated stock reco with batch

* test: added test case

(cherry picked from commit d4c0dbfacc)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-17 17:41:51 +05:30
Deepesh Garg
b02604396d Merge pull request #38810 from frappe/mergify/bp/version-15-hotfix/pr-38807
fix(demo): Demo setup for Canadian COA (#38807)
2023-12-17 17:13:54 +05:30
Deepesh Garg
7d844411fb fix(demo): Demo setup for canadian COA
(cherry picked from commit c9fd182268)
2023-12-17 08:35:53 +00:00
Smit Vora
fba28d6941 fix: fetch item_tax_template values if fields with fetch_from exisit 2023-12-17 13:59:22 +05:30
Deepesh Garg
57f7933a4b Merge pull request #38771 from frappe/mergify/bp/version-15-hotfix/pr-38767
fix: skip JVs against bank accounts in tax report (#38767)
2023-12-16 22:28:55 +05:30
mergify[bot]
2045306283 fix: Reset SLA on issue doesn't work (backport #38789) (#38791)
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:33 +05:30
Raffael Meyer
4fb479e2f5 Merge pull request #38788 from barredterra/bp-38440
fix(pe): show split alert only on splitting (backport #38440)
2023-12-15 16:26:33 +01:00
Dany Robert
77dba4834c fix(pe): show split alert only on splitting 2023-12-15 16:08:48 +01:00
Raffael Meyer
a70be4299e Merge pull request #38787 from frappe/mergify/bp/version-15-hotfix/pr-38742
fix(Purchase Register): show bill_date and bill_no (backport #38742)
2023-12-15 15:39:45 +01:00
ljain112
4ed86dbff2 fix: show bill_date and bill_no in Purchase Register
(cherry picked from commit f53ba178a8)
2023-12-15 14:25:03 +00:00
Deepesh Garg
63f6970b45 Merge pull request #38783 from frappe/mergify/bp/version-15-hotfix/pr-38691
fix: Init internal child table values (#38691)
2023-12-15 18:48:24 +05:30
Deepesh Garg
7802f6c528 fix: Init internal child table values
(cherry picked from commit 2588970d55)
2023-12-15 12:45:56 +00:00
mergify[bot]
7fc8150617 fix: wrong currency in Stock Balance report (backport #38778) (#38780)
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:59 +05:30
mergify[bot]
6e92c78cbd fix: asset patch failure due to missing shift_based column (backport #38776) (#38777)
fix: asset patch failure due to missing shift_based column (#38776)

* fix: add missing daily_prorata_based in get_asset_finance_books_map

* fix: reload Asset Finance Book doctype

(cherry picked from commit 1704180f38)

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-12-15 14:47:24 +05:30
ruthra kumar
5beea361fe Merge pull request #38775 from frappe/mergify/bp/version-15-hotfix/pr-38766
fix: validation error on reconciling PE to Journals as Invoice (backport #38766)
2023-12-15 14:41:55 +05:30
ruthra kumar
aa5e16e681 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 07:44:08 +00:00
ruthra kumar
27f454d07e Merge pull request #38768 from frappe/mergify/bp/version-15/pr-37625
refactor: set exchange rate on foreign currency JE from Bank Reconciliation (backport #37625)
2023-12-15 13:08:50 +05:30
Gursheen Anand
bf8a2d0e3a fix: skip jvs against bank accounts
(cherry picked from commit f7b2380ec1)
2023-12-15 07:29:30 +00:00
ruthra kumar
d890391531 refactor: handle bank transaction in foreign currency
(cherry picked from commit 74a0d6408a)
2023-12-15 07:06:48 +00:00
ruthra kumar
6e1d9a3dbc refactor: exc rate on foreign currency JE from Bank Reconciliation
(cherry picked from commit 89f484282a)
2023-12-15 07:06:48 +00:00
mergify[bot]
703be50bc7 fix(ux): don't override Item Name and Description in MR (backport #38720) (#38763)
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-15 11:58:20 +05:30
ruthra kumar
013d0ff99c Merge pull request #38764 from frappe/mergify/bp/version-15-hotfix/pr-37625
refactor: set exchange rate on foreign currency JE from Bank Reconciliation (backport #37625)
2023-12-15 11:41:04 +05:30
ruthra kumar
b1b157aa19 refactor: handle bank transaction in foreign currency
(cherry picked from commit 74a0d6408a)
2023-12-15 05:51:44 +00:00
ruthra kumar
e7e5727015 refactor: exc rate on foreign currency JE from Bank Reconciliation
(cherry picked from commit 89f484282a)
2023-12-15 05:51:43 +00:00
mergify[bot]
ad3a5b58e4 fix: homepage not working (backport #38755) (#38756)
fix: homepage not working (#38755)

(cherry picked from commit d6201ce5c7)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-14 23:02:25 +05:30
Raffael Meyer
f501c8b336 Merge pull request #38752 from frappe/mergify/bp/version-15-hotfix/pr-38505 2023-12-14 16:38:29 +01:00
barredterra
f7706211ea feat: set lead name from email
(cherry picked from commit ceeb724acc)
2023-12-14 12:49:41 +00:00
mergify[bot]
a551660d2a fix: timezone aware SLA banner (backport #38745) (#38747)
fix: timezone aware SLA banner (#38745)

(cherry picked from commit eaf86a6461)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-12-14 15:59:21 +05:30
ruthra kumar
5cbe170117 Merge pull request #38737 from frappe/mergify/bp/version-15-hotfix/pr-38717
fix: fetch exc rate of multi currency journals on reconciliation (backport #38717)
2023-12-14 09:59:47 +05:30
ruthra kumar
f55b561ff9 fix: fetch exc rate of multi currency journals
(cherry picked from commit 1b3ba25220)
2023-12-14 04:06:48 +00:00
mergify[bot]
753c1aa406 Revert "fix(ux): don't update qty blindly" (backport #38728) (backport #38730) (#38734)
Revert "fix(ux): don't update qty blindly" (backport #38728) (#38730)

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 3fabca1051)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2023-12-14 00:30:18 +05:30
Frappe PR Bot
ff85f2a940 chore(release): Bumped to Version 15.6.1
## [15.6.1](https://github.com/frappe/erpnext/compare/v15.6.0...v15.6.1) (2023-12-13)

### Bug Fixes

* supplier removed on selection of item (backport [#38712](https://github.com/frappe/erpnext/issues/38712)) (backport [#38713](https://github.com/frappe/erpnext/issues/38713)) ([#38731](https://github.com/frappe/erpnext/issues/38731)) ([04c605d](04c605d76b))
2023-12-13 18:49:38 +00:00
mergify[bot]
04c605d76b fix: supplier removed on selection of item (backport #38712) (backport #38713) (#38731)
fix: supplier removed on selection of item (backport #38712) (#38713)

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

(cherry picked from commit db24e24882)

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

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
2023-12-14 00:17:57 +05:30
mergify[bot]
3fabca1051 Revert "fix(ux): don't update qty blindly" (backport #38728) (#38730)
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:15 +05:30
Raffael Meyer
32ec73dd96 Merge pull request #38727 from frappe/mergify/bp/version-15-hotfix/pr-38725 2023-12-13 17:48:11 +01:00
barredterra
f30bede2e0 feat: RFQ print preview
(cherry picked from commit 27f05145ae)
2023-12-13 16:32:30 +00:00
mergify[bot]
1dcb065c64 fix: barcode scanning for the stock entry (backport #38716) (#38718)
fix: barcode scanning for the stock entry (#38716)

(cherry picked from commit 13cba5068b)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-13 18:32:48 +05:30
mergify[bot]
feb452b740 fix: supplier removed on selection of item (backport #38712) (#38713)
fix: supplier removed on selection of item (#38712)

(cherry picked from commit db24e24882)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-13 14:39:00 +05:30
Frappe PR Bot
86b276a3ec chore(release): Bumped to Version 15.6.0
# [15.6.0](https://github.com/frappe/erpnext/compare/v15.5.0...v15.6.0) (2023-12-12)

### Bug Fixes

* `split_invoices_based_on_payment_terms` (backport [#37859](https://github.com/frappe/erpnext/issues/37859)) ([#38488](https://github.com/frappe/erpnext/issues/38488)) ([4b76cc4](4b76cc46a1))
* 1st row depr. sch. value of asset put to less than 180 days acc. to I.T. S. 32 (backport [#38696](https://github.com/frappe/erpnext/issues/38696)) ([#38703](https://github.com/frappe/erpnext/issues/38703)) ([2bd9671](2bd96713db))
* auto delete draft serial and batch bundle (backport [#38637](https://github.com/frappe/erpnext/issues/38637)) ([#38654](https://github.com/frappe/erpnext/issues/38654)) ([b0675f6](b0675f6490))
* close PO on SCO close (backport [#38667](https://github.com/frappe/erpnext/issues/38667)) ([#38681](https://github.com/frappe/erpnext/issues/38681)) ([4055543](4055543f5d))
* format only if searched text contain link value text ([d79e6e3](d79e6e353e))
* get customers for leaderboard ([9d5c79d](9d5c79d6b2))
* get items for leaderboard ([7cb38a8](7cb38a8f22))
* get sales partner for leaderboard ([3845d42](3845d4294c))
* get sales person for leaderboard ([2fcfebe](2fcfebe1d3))
* get suppliers for leaderboard ([e205772](e205772482))
* ignore non-existing regional customizations (backport [#38621](https://github.com/frappe/erpnext/issues/38621)) ([#38624](https://github.com/frappe/erpnext/issues/38624)) ([c70e6f2](c70e6f23df))
* limit end date to current date ([dbdb971](dbdb971e10))
* negative batch issue (backport [#38688](https://github.com/frappe/erpnext/issues/38688)) ([#38694](https://github.com/frappe/erpnext/issues/38694)) ([a75081b](a75081b8c0))
* not able to make serial and batch using csv import (backport [#38659](https://github.com/frappe/erpnext/issues/38659)) ([#38662](https://github.com/frappe/erpnext/issues/38662)) ([dd07eca](dd07ecad45))
* only highest eligible coupon applied ([#38416](https://github.com/frappe/erpnext/issues/38416)) ([aa66ee6](aa66ee64ec))
* serial and batch bundle permission (backport [#38618](https://github.com/frappe/erpnext/issues/38618)) ([#38619](https://github.com/frappe/erpnext/issues/38619)) ([ce2bd15](ce2bd15872))
* serial no filter in the Serial No Ledger report (backport [#38669](https://github.com/frappe/erpnext/issues/38669)) ([#38682](https://github.com/frappe/erpnext/issues/38682)) ([d188c8e](d188c8ec0e))
* Shipping Address Link Showing in Buying (backport [#38634](https://github.com/frappe/erpnext/issues/38634)) ([#38646](https://github.com/frappe/erpnext/issues/38646)) ([4150ed9](4150ed9b3b))
* show stock qty in popup (backport [#38698](https://github.com/frappe/erpnext/issues/38698)) ([#38699](https://github.com/frappe/erpnext/issues/38699)) ([6e2cde4](6e2cde4a21))
* typeerror on new sites ([#38692](https://github.com/frappe/erpnext/issues/38692)) ([9239e73](9239e735ad))
* typo in unittest ([#38673](https://github.com/frappe/erpnext/issues/38673)) ([14ee13c](14ee13c77e))
* **ux:** don't update qty blindly (backport [#38608](https://github.com/frappe/erpnext/issues/38608)) ([#38639](https://github.com/frappe/erpnext/issues/38639)) ([0b2e2a2](0b2e2a2ab5))

### Features

* add employee number to client user bootinfo (backport [#38477](https://github.com/frappe/erpnext/issues/38477)) ([#38603](https://github.com/frappe/erpnext/issues/38603)) ([c7dbcbc](c7dbcbcd17))
2023-12-12 16:11:51 +00:00
Deepesh Garg
0d8a52f63b Merge pull request #38690 from frappe/version-15-hotfix
chore: release v15
2023-12-12 21:40:42 +05:30
mergify[bot]
2bd96713db fix: 1st row depr. sch. value of asset put to less than 180 days acc. to I.T. S. 32 (backport #38696) (#38703)
fix: 1st row depr. sch. value of asset put to less than 180 days acc. to I.T. S. 32 (#38696)

fix: 1st row value of asset put to less than 180 days acc. to IT S. 32
(cherry picked from commit e7984b3ef9)

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-12-12 21:09:38 +05:30
Raffael Meyer
1d2edec550 Merge pull request #38675 from frappe/mergify/bp/version-15-hotfix/pr-38672
fix: get data for leaderboard (backport #38672)
2023-12-12 13:44:04 +01:00
ruthra kumar
965126df83 Merge pull request #38678 from frappe/mergify/bp/version-15-hotfix/pr-38673
fix: typo in unittest (backport #38673)
2023-12-12 17:35:22 +05:30
mergify[bot]
6e2cde4a21 fix: show stock qty in popup (backport #38698) (#38699)
fix: show stock qty in popup (#38698)

(cherry picked from commit b562b4cf99)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-12 17:04:57 +05:30
mergify[bot]
a75081b8c0 fix: negative batch issue (backport #38688) (#38694)
fix: negative batch issue (#38688)

(cherry picked from commit 69d7a640ee)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-12 16:41:26 +05:30
ruthra kumar
b68e1f6ea6 Merge pull request #38693 from frappe/mergify/bp/version-15-hotfix/pr-38692
fix: typeerror on new sites (backport #38692)
2023-12-12 16:03:08 +05:30
ruthra kumar
9239e735ad fix: typeerror on new sites (#38692)
(cherry picked from commit fa2d33cb50)
2023-12-12 10:14:39 +00:00
ruthra kumar
576ff49943 Merge pull request #38687 from frappe/mergify/bp/version-15-hotfix/pr-38685
refactor: add `get_list` for virtual child doctypes (backport #38685)
2023-12-12 15:24:37 +05:30
ruthra kumar
92b5c80a4a refactor: add get_list for virtual child doctypes
(cherry picked from commit 15c90551b6)
2023-12-12 09:12:39 +00:00
mergify[bot]
d188c8ec0e fix: serial no filter in the Serial No Ledger report (backport #38669) (#38682)
fix: serial no filter in the Serial No Ledger report (#38669)

(cherry picked from commit 780c4278e6)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-12 14:33:11 +05:30
mergify[bot]
4055543f5d fix: close PO on SCO close (backport #38667) (#38681)
fix: close PO on SCO close

(cherry picked from commit b023e5d6b3)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-12-12 13:40:30 +05:30
ruthra kumar
fc79c6bf54 Merge pull request #38684 from frappe/mergify/bp/version-15-hotfix/pr-38679
refactor(test): repost utility deletion flag test (backport #38679)
2023-12-12 13:33:24 +05:30
ruthra kumar
522113ba98 refactor(test): update repost settings before test case
(cherry picked from commit acb6e8e120)
2023-12-12 07:16:51 +00:00
ruthra kumar
6469526c26 refactor: increase limit and remove explicit call to start_repost
(cherry picked from commit ccff588563)
2023-12-12 07:16:50 +00:00
ruthra kumar
122e6902ed refactor: remove explicit commit on repost
(cherry picked from commit a97b3db749)
2023-12-12 07:16:50 +00:00
ruthra kumar
64d93cec66 refactor(test): repost utility deletion flag test
(cherry picked from commit cc15f695b4)
2023-12-12 07:16:50 +00:00
Raffael Meyer
14ee13c77e fix: typo in unittest (#38673)
(cherry picked from commit 6ad298adfc)
2023-12-12 04:26:54 +00:00
barredterra
b17178bba9 chore: deprecate unused method
(cherry picked from commit 956c3c50a0)
2023-12-12 04:24:39 +00:00
barredterra
3845d4294c fix: get sales partner for leaderboard
(cherry picked from commit 40c1acc961)
2023-12-12 04:24:39 +00:00
barredterra
2fcfebe1d3 fix: get sales person for leaderboard
(cherry picked from commit 7babfd4ac4)
2023-12-12 04:24:39 +00:00
barredterra
e205772482 fix: get suppliers for leaderboard
(cherry picked from commit 65df4b6aa8)
2023-12-12 04:24:39 +00:00
barredterra
7cb38a8f22 fix: get items for leaderboard
(cherry picked from commit 2721ee3a8d)
2023-12-12 04:24:38 +00:00
barredterra
9d5c79d6b2 fix: get customers for leaderboard
(cherry picked from commit 137b5a6108)
2023-12-12 04:24:38 +00:00
mergify[bot]
dd07ecad45 fix: not able to make serial and batch using csv import (backport #38659) (#38662)
fix: not able to make serial and batch using csv import (#38659)

(cherry picked from commit 89a0e9c245)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-11 11:29:25 +05:30
mergify[bot]
b0675f6490 fix: auto delete draft serial and batch bundle (backport #38637) (#38654)
fix: auto delete draft serial and batch bundle (#38637)

(cherry picked from commit 89326bd657)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-10 16:45:03 +05:30
mergify[bot]
4150ed9b3b fix: Shipping Address Link Showing in Buying (backport #38634) (#38646)
fix(ux): `Shipping Address Link`

(cherry picked from commit ca0c3eb184)

Co-authored-by: creative-paramu <pparameshwari@thirvusoft.in>
2023-12-08 19:04:24 +05:30
mergify[bot]
0b2e2a2ab5 fix(ux): don't update qty blindly (backport #38608) (#38639)
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:32 +05:30
mergify[bot]
c70e6f23df fix: ignore non-existing regional customizations (backport #38621) (#38624)
fix: ignore non-existing regional customizations (#38621)

(cherry picked from commit 9611e9bd7f)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-12-07 21:43:46 +05:30
Shariq Ansari
5cf0c896bb Merge pull request #38627 from frappe/mergify/bp/version-15-hotfix/pr-38623
fix: format only if searched text contain link value text (backport #38623)
2023-12-07 20:51:03 +05:30
Shariq Ansari
d79e6e353e fix: format only if searched text contain link value text
(cherry picked from commit 08ed3cd313)
2023-12-07 15:18:52 +00:00
mergify[bot]
ce2bd15872 fix: serial and batch bundle permission (backport #38618) (#38619)
fix: serial and batch bundle permission (#38618)

(cherry picked from commit 231ab83562)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-07 18:36:31 +05:30
mergify[bot]
deef6ea66a chore: minor code cleanup (backport #38615) (#38616)
chore: minor code cleanup (#38615)

(cherry picked from commit f45dd740c5)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-12-07 15:36:21 +05:30
mergify[bot]
aa66ee64ec fix: only highest eligible coupon applied (#38416)
fix: only highest eligible coupon applied (#38416)

* fix: application of pricing rule when coupon is used

(cherry picked from commit d34787cf6d)

Co-authored-by: sandratridz <102575830+sandratridz@users.noreply.github.com>
2023-12-07 10:34:11 +05:30
Ankush Menat
eeb0567a73 chore: remove unused cache=True
(cherry picked from commit 6a47a2ceaf)
2023-12-06 20:54:43 +05:30
mergify[bot]
c7dbcbcd17 feat: add employee number to client user bootinfo (backport #38477) (#38603)
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:52:50 +05:30
mergify[bot]
4b76cc46a1 fix: split_invoices_based_on_payment_terms (backport #37859) (#38488)
* refactor: `split_invoices_based_on_payment_terms`

- Invoices were in the wrong order due to the logic. The invoices with payment terms were added first and the rest after.
- Overly long function with unnecessary loops (reduced to one main loop) and complexity
- The split row as per payment terms was not ordered. So the second installment was allocated first

(cherry picked from commit 6bd56d2d5f)

* test: `get_outstanding_reference_documents` (triggered via UI)

(cherry picked from commit 162c0497d1)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/test_payment_entry.py

* fix: Alert message and make sure invoice due dates are different for effective test

- Make invoice due dates are different so that the invoice with the earliest due date is allocated first in the test
- Translate voucher type, simplify alert message. The invoice could be "split" into 1 row, no. of rows in the message seems unnecessary.

(cherry picked from commit 56ac3424d2)

* style: Remove spaces introduced via merge conflict

(cherry picked from commit 4b4b176fcf)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/test_payment_entry.py

* fix: Re-add no.of rows split in alert message

(cherry picked from commit 1fc5844025)

* fix: Merge conflicts in tests

---------

Co-authored-by: marination <maricadsouza221197@gmail.com>
2023-12-06 18:01:42 +05:30
Gursheen Kaur Anand
bfe2b923e1 Merge pull request #38572 from frappe/mergify/bp/version-15-hotfix/pr-38556
fix(minor): financial statements period end date (backport #38556)
2023-12-05 22:02:27 +05:30
Gursheen Anand
dbdb971e10 fix: limit end date to current date
(cherry picked from commit ab6e92aae1)
2023-12-05 05:54:49 +00:00
90 changed files with 1863 additions and 581 deletions

View File

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

View File

@@ -33,7 +33,9 @@
},
"Stocks": {
"Mati\u00e8res premi\u00e8res": {},
"Stock de produits fini": {},
"Stock de produits fini": {
"account_type": "Stock"
},
"Stock exp\u00e9di\u00e9 non-factur\u00e9": {},
"Travaux en cours": {},
"account_type": "Stock"
@@ -395,9 +397,11 @@
},
"Produits": {
"Revenus de ventes": {
" Escomptes de volume sur ventes": {},
"Escomptes de volume sur ventes": {},
"Autres produits d'exploitation": {},
"Ventes": {},
"Ventes": {
"account_type": "Income Account"
},
"Ventes avec des provinces harmonis\u00e9es": {},
"Ventes avec des provinces non-harmonis\u00e9es": {},
"Ventes \u00e0 l'\u00e9tranger": {}

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

@@ -18,6 +18,7 @@ from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_s
get_entries,
)
from erpnext.accounts.utils import get_account_currency, get_balance_on
from erpnext.setup.utils import get_exchange_rate
class BankReconciliationTool(Document):
@@ -150,7 +151,7 @@ def create_journal_entry_bts(
bank_transaction = frappe.db.get_values(
"Bank Transaction",
bank_transaction_name,
fieldname=["name", "deposit", "withdrawal", "bank_account"],
fieldname=["name", "deposit", "withdrawal", "bank_account", "currency"],
as_dict=True,
)[0]
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
@@ -164,29 +165,94 @@ def create_journal_entry_bts(
)
company = frappe.get_value("Account", company_account, "company")
company_default_currency = frappe.get_cached_value("Company", company, "default_currency")
company_account_currency = frappe.get_cached_value("Account", company_account, "account_currency")
second_account_currency = frappe.get_cached_value("Account", second_account, "account_currency")
# determine if multi-currency Journal or not
is_multi_currency = (
True
if company_default_currency != company_account_currency
or company_default_currency != second_account_currency
or company_default_currency != bank_transaction.currency
else False
)
accounts = []
# Multi Currency?
accounts.append(
{
"account": second_account,
"credit_in_account_currency": bank_transaction.deposit,
"debit_in_account_currency": bank_transaction.withdrawal,
"party_type": party_type,
"party": party,
"cost_center": get_default_cost_center(company),
}
)
second_account_dict = {
"account": second_account,
"account_currency": second_account_currency,
"credit_in_account_currency": bank_transaction.deposit,
"debit_in_account_currency": bank_transaction.withdrawal,
"party_type": party_type,
"party": party,
"cost_center": get_default_cost_center(company),
}
accounts.append(
{
"account": company_account,
"bank_account": bank_transaction.bank_account,
"credit_in_account_currency": bank_transaction.withdrawal,
"debit_in_account_currency": bank_transaction.deposit,
"cost_center": get_default_cost_center(company),
}
)
company_account_dict = {
"account": company_account,
"account_currency": company_account_currency,
"bank_account": bank_transaction.bank_account,
"credit_in_account_currency": bank_transaction.withdrawal,
"debit_in_account_currency": bank_transaction.deposit,
"cost_center": get_default_cost_center(company),
}
# convert transaction amount to company currency
if is_multi_currency:
exc_rate = get_exchange_rate(bank_transaction.currency, company_default_currency, posting_date)
withdrawal_in_company_currency = flt(exc_rate * abs(bank_transaction.withdrawal))
deposit_in_company_currency = flt(exc_rate * abs(bank_transaction.deposit))
else:
withdrawal_in_company_currency = bank_transaction.withdrawal
deposit_in_company_currency = bank_transaction.deposit
# if second account is of foreign currency, convert and set debit and credit fields.
if second_account_currency != company_default_currency:
exc_rate = get_exchange_rate(second_account_currency, company_default_currency, posting_date)
second_account_dict.update(
{
"exchange_rate": exc_rate,
"credit": deposit_in_company_currency,
"debit": withdrawal_in_company_currency,
"credit_in_account_currency": flt(deposit_in_company_currency / exc_rate) or 0,
"debit_in_account_currency": flt(withdrawal_in_company_currency / exc_rate) or 0,
}
)
else:
second_account_dict.update(
{
"exchange_rate": 1,
"credit": deposit_in_company_currency,
"debit": withdrawal_in_company_currency,
"credit_in_account_currency": deposit_in_company_currency,
"debit_in_account_currency": withdrawal_in_company_currency,
}
)
# if company account is of foreign currency, convert and set debit and credit fields.
if company_account_currency != company_default_currency:
exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date)
company_account_dict.update(
{
"exchange_rate": exc_rate,
"credit": withdrawal_in_company_currency,
"debit": deposit_in_company_currency,
}
)
else:
company_account_dict.update(
{
"exchange_rate": 1,
"credit": withdrawal_in_company_currency,
"debit": deposit_in_company_currency,
"credit_in_account_currency": withdrawal_in_company_currency,
"debit_in_account_currency": deposit_in_company_currency,
}
)
accounts.append(second_account_dict)
accounts.append(company_account_dict)
journal_entry_dict = {
"voucher_type": entry_type,
@@ -196,6 +262,9 @@ def create_journal_entry_bts(
"cheque_no": reference_number,
"mode_of_payment": mode_of_payment,
}
if is_multi_currency:
journal_entry_dict.update({"multi_currency": True})
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.update(journal_entry_dict)
journal_entry.set("accounts", accounts)

View File

@@ -1689,13 +1689,43 @@ def get_outstanding_reference_documents(args, validate=False):
return data
def split_invoices_based_on_payment_terms(outstanding_invoices, company):
invoice_ref_based_on_payment_terms = {}
def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list:
"""Split a list of invoices based on their payment terms."""
exc_rates = get_currency_data(outstanding_invoices, company)
outstanding_invoices_after_split = []
for entry in outstanding_invoices:
if entry.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
if payment_term_template := frappe.db.get_value(
entry.voucher_type, entry.voucher_no, "payment_terms_template"
):
split_rows = get_split_invoice_rows(entry, payment_term_template, exc_rates)
if not split_rows:
continue
if len(split_rows) > 1:
frappe.msgprint(
_("Splitting {0} {1} into {2} rows as per Payment Terms").format(
_(entry.voucher_type), frappe.bold(entry.voucher_no), len(split_rows)
),
alert=True,
)
outstanding_invoices_after_split += split_rows
continue
# If not an invoice or no payment terms template, add as it is
outstanding_invoices_after_split.append(entry)
return outstanding_invoices_after_split
def get_currency_data(outstanding_invoices: list, company: str = None) -> dict:
"""Get currency and conversion data for a list of invoices."""
exc_rates = frappe._dict()
company_currency = (
frappe.db.get_value("Company", company, "default_currency") if company else None
)
exc_rates = frappe._dict()
for doctype in ["Sales Invoice", "Purchase Invoice"]:
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
for x in frappe.db.get_all(
@@ -1710,72 +1740,54 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company):
company_currency=company_currency,
)
for idx, d in enumerate(outstanding_invoices):
if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
payment_term_template = frappe.db.get_value(
d.voucher_type, d.voucher_no, "payment_terms_template"
return exc_rates
def get_split_invoice_rows(invoice: dict, payment_term_template: str, exc_rates: dict) -> list:
"""Split invoice based on its payment schedule table."""
split_rows = []
allocate_payment_based_on_payment_terms = frappe.db.get_value(
"Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
)
if not allocate_payment_based_on_payment_terms:
return [invoice]
payment_schedule = frappe.get_all(
"Payment Schedule", filters={"parent": invoice.voucher_no}, fields=["*"], order_by="due_date"
)
for payment_term in payment_schedule:
if not payment_term.outstanding > 0.1:
continue
doc_details = exc_rates.get(payment_term.parent, None)
is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
doc_details.party_account_currency != doc_details.company_currency
)
payment_term_outstanding = flt(payment_term.outstanding)
if not is_multi_currency_acc:
payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
split_rows.append(
frappe._dict(
{
"due_date": invoice.due_date,
"currency": invoice.currency,
"voucher_no": invoice.voucher_no,
"voucher_type": invoice.voucher_type,
"posting_date": invoice.posting_date,
"invoice_amount": flt(invoice.invoice_amount),
"outstanding_amount": payment_term_outstanding
if payment_term_outstanding
else invoice.outstanding_amount,
"payment_term_outstanding": payment_term_outstanding,
"payment_amount": payment_term.payment_amount,
"payment_term": payment_term.payment_term,
}
)
if payment_term_template:
allocate_payment_based_on_payment_terms = frappe.get_cached_value(
"Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
)
if allocate_payment_based_on_payment_terms:
payment_schedule = frappe.get_all(
"Payment Schedule", filters={"parent": d.voucher_no}, fields=["*"]
)
)
for payment_term in payment_schedule:
if payment_term.outstanding > 0.1:
doc_details = exc_rates.get(payment_term.parent, None)
is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
doc_details.party_account_currency != doc_details.company_currency
)
payment_term_outstanding = flt(payment_term.outstanding)
if not is_multi_currency_acc:
payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
invoice_ref_based_on_payment_terms.setdefault(idx, [])
invoice_ref_based_on_payment_terms[idx].append(
frappe._dict(
{
"due_date": d.due_date,
"currency": d.currency,
"voucher_no": d.voucher_no,
"voucher_type": d.voucher_type,
"posting_date": d.posting_date,
"invoice_amount": flt(d.invoice_amount),
"outstanding_amount": payment_term_outstanding
if payment_term_outstanding
else d.outstanding_amount,
"payment_term_outstanding": payment_term_outstanding,
"payment_amount": payment_term.payment_amount,
"payment_term": payment_term.payment_term,
"account": d.account,
}
)
)
outstanding_invoices_after_split = []
if invoice_ref_based_on_payment_terms:
for idx, ref in invoice_ref_based_on_payment_terms.items():
voucher_no = ref[0]["voucher_no"]
voucher_type = ref[0]["voucher_type"]
frappe.msgprint(
_("Spliting {} {} into {} row(s) as per Payment Terms").format(
voucher_type, voucher_no, len(ref)
),
alert=True,
)
outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx]
existing_row = list(filter(lambda x: x.get("voucher_no") == voucher_no, outstanding_invoices))
index = outstanding_invoices.index(existing_row[0])
outstanding_invoices.pop(index)
outstanding_invoices_after_split += outstanding_invoices
return outstanding_invoices_after_split
return split_rows
def get_orders_to_be_billed(

View File

@@ -6,11 +6,12 @@ import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import flt, nowdate
from frappe.utils import add_days, flt, nowdate
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.payment_entry.payment_entry import (
InvalidPaymentEntry,
get_outstanding_reference_documents,
get_payment_entry,
get_reference_details,
)
@@ -1471,6 +1472,45 @@ class TestPaymentEntry(FrappeTestCase):
for field in ["account", "debit", "credit"]:
self.assertEqual(self.expected_gle[row][field], gl_entries[row][field])
def test_outstanding_invoices_api(self):
"""
Test if `get_outstanding_reference_documents` fetches invoices in the right order.
"""
customer = create_customer("Max Mustermann", "INR")
create_payment_terms_template()
# SI has an earlier due date and SI2 has a later due date
si = create_sales_invoice(
qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4)
)
si2 = create_sales_invoice(do_not_save=1, qty=1, rate=100, customer=customer)
si2.payment_terms_template = "Test Receivable Template"
si2.submit()
args = {
"posting_date": nowdate(),
"company": "_Test Company",
"party_type": "Customer",
"payment_type": "Pay",
"party": customer,
"party_account": "Debtors - _TC",
}
args.update(
{
"get_outstanding_invoices": True,
"from_posting_date": add_days(nowdate(), -4),
"to_posting_date": add_days(nowdate(), 2),
}
)
references = get_outstanding_reference_documents(args)
self.assertEqual(len(references), 3)
self.assertEqual(references[0].voucher_no, si.name)
self.assertEqual(references[1].voucher_no, si2.name)
self.assertEqual(references[2].voucher_no, si2.name)
self.assertEqual(references[1].payment_term, "Basic Amount Receivable")
self.assertEqual(references[2].payment_term, "Tax Receivable")
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")
@@ -1531,6 +1571,9 @@ def create_payment_terms_template():
def create_payment_terms_template_with_discount(
name=None, discount_type=None, discount=None, template_name=None
):
"""
Create a Payment Terms Template with % or amount discount.
"""
create_payment_term(name or "30 Credit Days with 10% Discount")
template_name = template_name or "Test Discount Template"

View File

@@ -592,6 +592,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

@@ -34,4 +34,6 @@ class PaymentReconciliationAllocation(Document):
unreconciled_amount: DF.Currency
# end: auto-generated types
pass
@staticmethod
def get_list(args):
pass

View File

@@ -26,4 +26,6 @@ class PaymentReconciliationInvoice(Document):
parenttype: DF.Data
# end: auto-generated types
pass
@staticmethod
def get_list(args):
pass

View File

@@ -30,4 +30,6 @@ class PaymentReconciliationPayment(Document):
remark: DF.SmallText | None
# end: auto-generated types
pass
@staticmethod
def get_list(args):
pass

View File

@@ -581,6 +581,8 @@ def apply_pricing_rule_on_transaction(doc):
if d.price_or_product_discount == "Price":
if d.apply_discount_on:
doc.set("apply_discount_on", d.apply_discount_on)
# Variable to track whether the condition has been met
condition_met = False
for field in ["additional_discount_percentage", "discount_amount"]:
pr_field = "discount_percentage" if field == "additional_discount_percentage" else field
@@ -603,6 +605,11 @@ def apply_pricing_rule_on_transaction(doc):
if coupon_code_pricing_rule == d.name:
# if selected coupon code is linked with pricing rule
doc.set(field, d.get(pr_field))
# Set the condition_met variable to True and break out of the loop
condition_met = True
break
else:
# reset discount if not linked
doc.set(field, 0)
@@ -611,6 +618,10 @@ def apply_pricing_rule_on_transaction(doc):
doc.set(field, 0)
doc.calculate_taxes_and_totals()
# Break out of the main loop if the condition is met
if condition_met:
break
elif d.price_or_product_discount == "Product":
item_details = frappe._dict({"parenttype": doc.doctype, "free_item_data": []})
get_product_discount_rule(d, item_details, doc=doc)

View File

@@ -15,6 +15,7 @@
"group_by",
"cost_center",
"territory",
"ignore_exchange_rate_revaluation_journals",
"column_break_14",
"to_date",
"finance_book",
@@ -376,10 +377,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

@@ -56,6 +56,7 @@ class ProcessStatementOfAccounts(Document):
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
from_date: DF.Date | None
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
ignore_exchange_rate_revaluation_journals: DF.Check
include_ageing: DF.Check
include_break: DF.Check
letter_head: DF.Link | None
@@ -119,6 +120,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)
@@ -131,6 +144,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

@@ -126,7 +126,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",
@@ -170,8 +170,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

@@ -2356,9 +2356,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,
)

View File

@@ -2799,6 +2799,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

@@ -242,8 +242,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

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,

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

@@ -238,6 +238,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

@@ -89,6 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
"payable_account": inv.credit_to,
"mode_of_payment": inv.mode_of_payment,
"project": ", ".join(project) if inv.doctype == "Purchase Invoice" else inv.project,
"bill_no": inv.bill_no,
"bill_date": inv.bill_date,
"remarks": inv.remarks,
"purchase_order": ", ".join(purchase_order),
"purchase_receipt": ", ".join(purchase_receipt),

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

@@ -657,8 +657,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,
"account": d.account,
}

View File

@@ -340,6 +340,10 @@ class AssetDepreciationSchedule(Document):
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(
asset_doc, value_after_depreciation, row, False
)
== row.rate_of_depreciation
):
from_date = add_days(
asset_doc.available_for_use_date, -1
@@ -605,7 +609,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

@@ -214,7 +214,7 @@ frappe.ui.form.on("Purchase Order Item", {
}
},
fg_item_qty: async function(frm, cdt, cdn) {
qty: async function (frm, cdt, cdn) {
if (frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) {
var row = locals[cdt][cdn];
@@ -222,7 +222,7 @@ frappe.ui.form.on("Purchase Order Item", {
var result = await frm.events.get_subcontracting_boms_for_finished_goods(row.fg_item)
if (result.message && row.item_code == result.message.service_item && row.uom == result.message.service_item_uom) {
frappe.model.set_value(cdt, cdn, "qty", flt(row.fg_item_qty) * flt(result.message.conversion_factor));
frappe.model.set_value(cdt, cdn, "fg_item_qty", flt(row.qty) / flt(result.message.conversion_factor));
}
}
}

View File

@@ -119,6 +119,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

@@ -166,6 +166,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()
@@ -225,6 +226,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)
@@ -292,6 +303,7 @@ class AccountsController(TransactionBase):
def on_trash(self):
self._remove_references_in_repost_doctypes()
self._remove_references_in_unreconcile()
self.remove_serial_and_batch_bundle()
# delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
@@ -307,6 +319,15 @@ class AccountsController(TransactionBase):
(self.doctype, self.name),
)
def remove_serial_and_batch_bundle(self):
bundles = frappe.get_all(
"Serial and Batch Bundle",
filters={"voucher_type": self.doctype, "voucher_no": self.name, "docstatus": ("!=", 1)},
)
for bundle in bundles:
frappe.delete_doc("Serial and Batch Bundle", bundle.name)
def validate_deferred_income_expense_account(self):
field_map = {
"Sales Invoice": "deferred_revenue_account",
@@ -629,6 +650,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

View File

@@ -381,7 +381,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

@@ -56,10 +56,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

@@ -8,6 +8,8 @@ from frappe.model.meta import get_field_precision
from frappe.utils import flt, format_datetime, get_datetime
import erpnext
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
from erpnext.stock.utils import get_incoming_rate
@@ -69,8 +71,6 @@ def validate_return_against(doc):
def validate_returned_items(doc):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
valid_items = frappe._dict()
select_fields = "item_code, qty, stock_qty, rate, parenttype, conversion_factor"
@@ -123,26 +123,6 @@ def validate_returned_items(doc):
)
)
elif ref.batch_no and d.batch_no not in ref.batch_no:
frappe.throw(
_("Row # {0}: Batch No must be same as {1} {2}").format(
d.idx, doc.doctype, doc.return_against
)
)
elif ref.serial_no:
if d.qty and not d.serial_no:
frappe.throw(_("Row # {0}: Serial No is mandatory").format(d.idx))
else:
serial_nos = get_serial_nos(d.serial_no)
for s in serial_nos:
if s not in ref.serial_no:
frappe.throw(
_("Row # {0}: Serial No {1} does not match with {2} {3}").format(
d.idx, s, doc.doctype, doc.return_against
)
)
if (
warehouse_mandatory
and not d.get("warehouse")
@@ -397,71 +377,92 @@ def make_return_doc(
else:
doc.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent):
def update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
target_doc.qty = -1 * source_doc.qty
item_details = frappe.get_cached_value(
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
)
returned_serial_nos = []
if source_doc.get("serial_and_batch_bundle"):
if item_details.has_serial_no:
returned_serial_nos = get_returned_serial_nos(source_doc, source_parent)
returned_batches = frappe._dict()
serial_and_batch_field = (
"serial_and_batch_bundle" if qty_field == "stock_qty" else "rejected_serial_and_batch_bundle"
)
old_serial_no_field = "serial_no" if qty_field == "stock_qty" else "rejected_serial_no"
old_batch_no_field = "batch_no"
type_of_transaction = "Inward"
if (
frappe.db.get_value(
"Serial and Batch Bundle", source_doc.serial_and_batch_bundle, "type_of_transaction"
)
== "Inward"
):
type_of_transaction = "Outward"
cls_obj = SerialBatchCreation(
{
"type_of_transaction": type_of_transaction,
"serial_and_batch_bundle": source_doc.serial_and_batch_bundle,
"returned_against": source_doc.name,
"item_code": source_doc.item_code,
"returned_serial_nos": returned_serial_nos,
}
)
cls_obj.duplicate_package()
if cls_obj.serial_and_batch_bundle:
target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
if source_doc.get("rejected_serial_and_batch_bundle"):
if (
source_doc.get(serial_and_batch_field)
or source_doc.get(old_serial_no_field)
or source_doc.get(old_batch_no_field)
):
if item_details.has_serial_no:
returned_serial_nos = get_returned_serial_nos(
source_doc, source_parent, serial_no_field="rejected_serial_and_batch_bundle"
source_doc, source_parent, serial_no_field=serial_and_batch_field
)
else:
returned_batches = get_returned_batches(
source_doc, source_parent, batch_no_field=serial_and_batch_field
)
type_of_transaction = "Inward"
if (
if source_doc.get(serial_and_batch_field) and (
frappe.db.get_value(
"Serial and Batch Bundle", source_doc.rejected_serial_and_batch_bundle, "type_of_transaction"
"Serial and Batch Bundle", source_doc.get(serial_and_batch_field), "type_of_transaction"
)
== "Inward"
):
type_of_transaction = "Outward"
elif source_parent.doctype in [
"Purchase Invoice",
"Purchase Receipt",
"Subcontracting Receipt",
]:
type_of_transaction = "Outward"
cls_obj = SerialBatchCreation(
{
"type_of_transaction": type_of_transaction,
"serial_and_batch_bundle": source_doc.rejected_serial_and_batch_bundle,
"serial_and_batch_bundle": source_doc.get(serial_and_batch_field),
"returned_against": source_doc.name,
"item_code": source_doc.item_code,
"returned_serial_nos": returned_serial_nos,
"voucher_type": source_parent.doctype,
"do_not_submit": True,
"warehouse": source_doc.warehouse,
"has_serial_no": item_details.has_serial_no,
"has_batch_no": item_details.has_batch_no,
}
)
cls_obj.duplicate_package()
if cls_obj.serial_and_batch_bundle:
target_doc.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
serial_nos = []
batches = frappe._dict()
if source_doc.get(old_batch_no_field):
batches = frappe._dict({source_doc.batch_no: source_doc.get(qty_field)})
elif source_doc.get(old_serial_no_field):
serial_nos = get_serial_nos(source_doc.get(old_serial_no_field))
elif source_doc.get(serial_and_batch_field):
if item_details.has_serial_no:
serial_nos = get_serial_nos_from_bundle(source_doc.get(serial_and_batch_field))
else:
batches = get_batches_from_bundle(source_doc.get(serial_and_batch_field))
if serial_nos:
cls_obj.serial_nos = sorted(list(set(serial_nos) - set(returned_serial_nos)))
elif batches:
for batch in batches:
if batch in returned_batches:
batches[batch] -= flt(returned_batches.get(batch))
cls_obj.batches = batches
if source_doc.get(serial_and_batch_field):
cls_obj.duplicate_package()
if cls_obj.serial_and_batch_bundle:
target_doc.set(serial_and_batch_field, cls_obj.serial_and_batch_bundle)
else:
target_doc.set(serial_and_batch_field, cls_obj.make_serial_and_batch_bundle().name)
def update_item(source_doc, target_doc, source_parent):
target_doc.qty = -1 * source_doc.qty
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype
@@ -561,6 +562,17 @@ def make_return_doc(
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
item_details = frappe.get_cached_value(
"Item", source_doc.item_code, ["has_batch_no", "has_serial_no"], as_dict=1
)
if not item_details.has_batch_no and not item_details.has_serial_no:
return
for qty_field in ["stock_qty", "rejected_qty"]:
if target_doc.get(qty_field):
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
def update_terms(source_doc, target_doc, source_parent):
target_doc.payment_amount = -source_doc.payment_amount
@@ -716,6 +728,9 @@ def get_returned_serial_nos(
[parent_doc.doctype, "docstatus", "=", 1],
]
if serial_no_field == "rejected_serial_and_batch_bundle":
filters.append([child_doc.doctype, "rejected_qty", ">", 0])
# Required for POS Invoice
if ignore_voucher_detail_no:
filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no])
@@ -723,9 +738,57 @@ def get_returned_serial_nos(
ids = []
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
ids.append(row.get("serial_and_batch_bundle"))
if row.get(old_field):
if row.get(old_field) and not row.get(serial_no_field):
serial_nos.extend(get_serial_nos_from_serial_no(row.get(old_field)))
serial_nos.extend(get_serial_nos(ids))
if ids:
serial_nos.extend(get_serial_nos(ids))
return serial_nos
def get_returned_batches(
child_doc, parent_doc, batch_no_field=None, ignore_voucher_detail_no=None
):
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
batches = frappe._dict()
old_field = "batch_no"
if not batch_no_field:
batch_no_field = "serial_and_batch_bundle"
return_ref_field = frappe.scrub(child_doc.doctype)
if child_doc.doctype == "Delivery Note Item":
return_ref_field = "dn_detail"
fields = [
f"`{'tab' + child_doc.doctype}`.`{batch_no_field}`",
f"`{'tab' + child_doc.doctype}`.`batch_no`",
f"`{'tab' + child_doc.doctype}`.`stock_qty`",
]
filters = [
[parent_doc.doctype, "return_against", "=", parent_doc.name],
[parent_doc.doctype, "is_return", "=", 1],
[child_doc.doctype, return_ref_field, "=", child_doc.name],
[parent_doc.doctype, "docstatus", "=", 1],
]
if batch_no_field == "rejected_serial_and_batch_bundle":
filters.append([child_doc.doctype, "rejected_qty", ">", 0])
# Required for POS Invoice
if ignore_voucher_detail_no:
filters.append([child_doc.doctype, "name", "!=", ignore_voucher_detail_no])
ids = []
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
ids.append(row.get("serial_and_batch_bundle"))
if row.get(old_field) and not row.get(batch_no_field):
batches.setdefault(row.get(old_field), row.get("stock_qty"))
if ids:
batches.update(get_batches_from_bundle(ids))
return batches

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):
@@ -308,6 +308,8 @@ class SellingController(StockController):
"warehouse": p.warehouse or d.warehouse,
"item_code": p.item_code,
"qty": flt(p.qty),
"serial_no": p.serial_no if self.docstatus == 2 else None,
"batch_no": p.batch_no if self.docstatus == 2 else None,
"uom": p.uom,
"serial_and_batch_bundle": p.serial_and_batch_bundle
or get_serial_and_batch_bundle(p, self),
@@ -330,6 +332,8 @@ class SellingController(StockController):
"warehouse": d.warehouse,
"item_code": d.item_code,
"qty": d.stock_qty,
"serial_no": d.serial_no if self.docstatus == 2 else None,
"batch_no": d.batch_no if self.docstatus == 2 else None,
"uom": d.uom,
"stock_uom": d.stock_uom,
"conversion_factor": d.conversion_factor,
@@ -428,11 +432,13 @@ 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:
d.incoming_rate = get_incoming_rate(
{
"item_code": d.item_code,

View File

@@ -455,6 +455,12 @@ class StockController(AccountsController):
sl_dict.update(args)
self.update_inventory_dimensions(d, sl_dict)
if self.docstatus == 2:
# To handle denormalized serial no records, will br deprecated in v16
for field in ["serial_no", "batch_no"]:
if d.get(field):
sl_dict[field] = d.get(field)
return sl_dict
def update_inventory_dimensions(self, row, sl_dict) -> None:

View File

@@ -516,7 +516,7 @@
"idx": 5,
"image_field": "image",
"links": [],
"modified": "2023-08-28 22:28:00.104413",
"modified": "2023-12-01 18:46:49.468526",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
@@ -577,6 +577,7 @@
],
"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",

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):
@@ -111,6 +112,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

@@ -637,6 +637,7 @@ additional_timeline_content = {
extend_bootinfo = [
"erpnext.support.doctype.service_level_agreement.service_level_agreement.add_sla_doctypes",
"erpnext.startup.boot.bootinfo",
]

View File

@@ -583,6 +583,7 @@ class ProductionPlan(Document):
if close:
self.db_set("status", "Closed")
self.update_bin_qty()
return
if self.total_produced_qty > 0:
@@ -597,6 +598,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:

View File

@@ -1458,6 +1458,47 @@ 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 create_production_plan(**args):
"""

View File

@@ -352,3 +352,4 @@ erpnext.patches.v14_0.update_zero_asset_quantity_field
execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
# 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

View File

@@ -3,6 +3,7 @@ import frappe
def execute():
frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule")
frappe.reload_doc("assets", "doctype", "Asset Finance Book")
assets = get_details_of_draft_or_submitted_depreciable_assets()
@@ -86,6 +87,7 @@ def get_asset_finance_books_map():
afb.frequency_of_depreciation,
afb.rate_of_depreciation,
afb.expected_value_after_useful_life,
afb.daily_prorata_based,
afb.shift_based,
)
.where(asset.docstatus < 2)

View File

@@ -2,14 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on('Homepage', {
setup: function(frm) {
frm.fields_dict["products"].grid.get_field("item").get_query = function() {
return {
filters: {'published': 1}
}
}
},
refresh: function(frm) {
frm.add_custom_button(__('Set Meta Tags'), () => {
frappe.utils.set_meta_tag('home');

View File

@@ -36,14 +36,14 @@ erpnext.buying = {
// no idea where me is coming from
if(this.frm.get_field('shipping_address')) {
this.frm.set_query("shipping_address", function() {
if(me.frm.doc.customer) {
this.frm.set_query("shipping_address", () => {
if(this.frm.doc.customer) {
return {
query: 'frappe.contacts.doctype.address.address.address_query',
filters: { link_doctype: 'Customer', link_name: me.frm.doc.customer }
filters: { link_doctype: 'Customer', link_name: this.frm.doc.customer }
};
} else
return erpnext.queries.company_address_query(me.frm.doc)
return erpnext.queries.company_address_query(this.frm.doc)
});
}
}
@@ -361,9 +361,14 @@ erpnext.buying = {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}
let update_values = {
"serial_and_batch_bundle": r.name,
"qty": Math.abs(r.total_qty)
"qty": qty
}
if (r.warehouse) {
@@ -396,9 +401,14 @@ erpnext.buying = {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}
let update_values = {
"serial_and_batch_bundle": r.name,
"rejected_qty": Math.abs(r.total_qty)
"rejected_qty": qty
}
if (r.warehouse) {

View File

@@ -380,6 +380,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
scan_barcode() {
frappe.flags.dialog_set = false;
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
barcode_scanner.process_scan();
}
@@ -512,6 +513,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

@@ -8,7 +8,7 @@ $.extend(erpnext, {
if(!company && cur_frm)
company = cur_frm.doc.company;
if(company)
return frappe.get_doc(":Company", company).default_currency || frappe.boot.sysdefaults.currency;
return frappe.get_doc(":Company", company)?.default_currency || frappe.boot.sysdefaults.currency;
else
return frappe.boot.sysdefaults.currency;
},
@@ -1077,7 +1077,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

@@ -114,13 +114,13 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.run_serially([
() => this.set_selector_trigger_flag(data),
() => this.set_serial_no(row, serial_no),
() => this.set_batch_no(row, batch_no),
() => this.set_barcode(row, barcode),
() => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
this.show_scan_message(row.idx, row.item_code, qty);
}),
() => this.set_barcode_uom(row, uom),
() => this.set_serial_no(row, serial_no),
() => this.set_batch_no(row, batch_no),
() => this.set_barcode(row, barcode),
() => this.clean_up(),
() => this.revert_selector_flag(),
() => resolve(row)
@@ -131,10 +131,10 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
// batch and serial selector is reduandant when all info can be added by scan
// this flag on item row is used by transaction.js to avoid triggering selector
set_selector_trigger_flag(data) {
const {batch_no, serial_no, has_batch_no, has_serial_no} = data;
const {has_batch_no, has_serial_no} = data;
const require_selecting_batch = has_batch_no && !batch_no;
const require_selecting_serial = has_serial_no && !serial_no;
const require_selecting_batch = has_batch_no;
const require_selecting_serial = has_serial_no;
if (!(require_selecting_batch || require_selecting_serial)) {
frappe.flags.hide_serial_batch_dialog = true;

View File

@@ -317,9 +317,14 @@ erpnext.sales_common = {
new erpnext.SerialBatchPackageSelector(
me.frm, item, (r) => {
if (r) {
let qty = Math.abs(r.total_qty);
if (doc.is_return) {
qty = qty * -1;
}
frappe.model.set_value(item.doctype, item.name, {
"serial_and_batch_bundle": r.name,
"qty": Math.abs(r.total_qty)
"qty": qty
});
}
}

View File

@@ -31,19 +31,40 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
secondary_action: () => this.edit_full_form(),
});
this.dialog.set_value("qty", this.item.qty).then(() => {
if (this.item.serial_no) {
this.dialog.set_value("scan_serial_no", this.item.serial_no);
frappe.model.set_value(this.item.doctype, this.item.name, 'serial_no', '');
} else if (this.item.batch_no) {
this.dialog.set_value("scan_batch_no", this.item.batch_no);
frappe.model.set_value(this.item.doctype, this.item.name, 'batch_no', '');
}
});
this.dialog.show();
this.$scan_btn = this.dialog.$wrapper.find(".link-btn");
this.$scan_btn.css("display", "inline");
let qty = this.item.stock_qty || this.item.transfer_qty || this.item.qty;
if (this.item?.is_rejected) {
qty = this.item.rejected_qty;
}
qty = Math.abs(qty);
if (qty > 0) {
this.dialog.set_value("qty", qty).then(() => {
if (this.item.serial_no && !this.item.serial_and_batch_bundle) {
let serial_nos = this.item.serial_no.split('\n');
if (serial_nos.length > 1) {
serial_nos.forEach(serial_no => {
this.dialog.fields_dict.entries.df.data.push({
serial_no: serial_no,
batch_no: this.item.batch_no
});
});
} else {
this.dialog.set_value("scan_serial_no", this.item.serial_no);
}
frappe.model.set_value(this.item.doctype, this.item.name, 'serial_no', '');
} else if (this.item.batch_no && !this.item.serial_and_batch_bundle) {
this.dialog.set_value("scan_batch_no", this.item.batch_no);
frappe.model.set_value(this.item.doctype, this.item.name, 'batch_no', '');
}
this.dialog.fields_dict.entries.grid.refresh();
});
}
}
get_serial_no_filters() {
@@ -463,13 +484,13 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
}
render_data() {
if (!this.frm.is_new() && this.bundle) {
if (this.bundle) {
frappe.call({
method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.get_serial_batch_ledgers',
args: {
item_code: this.item.item_code,
name: this.bundle,
voucher_no: this.item.parent,
voucher_no: !this.frm.is_new() ? this.item.parent : "",
}
}).then(r => {
if (r.message) {

View File

@@ -112,9 +112,9 @@ def create_transaction(doctype, company, start_date):
warehouse = get_warehouse(company)
if document_type == "Purchase Order":
posting_date = get_random_date(start_date, 1, 30)
posting_date = get_random_date(start_date, 1, 25)
else:
posting_date = get_random_date(start_date, 31, 364)
posting_date = get_random_date(start_date, 31, 350)
doctype.update(
{

View File

@@ -2,8 +2,8 @@
# License: GNU General Public License v3. See license.txt
import os
import json
import os
import frappe
from frappe import _
@@ -114,10 +114,11 @@ def update_regional_tax_settings(country, company):
frappe.scrub(country)
)
frappe.get_attr(module_name)(country, company)
except Exception as e:
except (ImportError, AttributeError):
pass
except Exception:
# Log error and ignore if failed to setup regional tax settings
frappe.log_error("Unable to setup regional tax settings")
pass
def make_taxes_and_charges_template(company_name, doctype, template):

View File

@@ -75,3 +75,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

@@ -301,7 +301,8 @@
"no_copy": 1,
"options": "Delivery Note",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"collapsible": 1,
@@ -1401,7 +1402,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

@@ -1294,7 +1294,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,15 @@
import frappe
def execute():
"""Drop unused return_against index"""
try:
frappe.db.sql_ddl(
"ALTER TABLE `tabDelivery Note` DROP INDEX `customer_is_return_return_against_index`"
)
frappe.db.sql_ddl(
"ALTER TABLE `tabPurchase Receipt` DROP INDEX `supplier_is_return_return_against_index`"
)
except Exception:
frappe.log_error("Failed to drop unused index")

View File

@@ -174,6 +174,115 @@ class TestDeliveryNote(FrappeTestCase):
for field, value in field_values.items():
self.assertEqual(cstr(serial_no.get(field)), value)
def test_delivery_note_return_against_denormalized_serial_no(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
frappe.flags.ignore_serial_batch_bundle_validation = True
sn_item = "Old Serial NO Item Return Test - 1"
make_item(
sn_item,
{
"has_serial_no": 1,
"serial_no_series": "OSN-.####",
"is_stock_item": 1,
},
)
frappe.flags.ignore_serial_batch_bundle_validation = True
serial_nos = [
"OSN-1",
"OSN-2",
"OSN-3",
"OSN-4",
"OSN-5",
"OSN-6",
"OSN-7",
"OSN-8",
"OSN-9",
"OSN-10",
"OSN-11",
"OSN-12",
]
for sn in serial_nos:
if not frappe.db.exists("Serial No", sn):
sn_doc = frappe.get_doc(
{
"doctype": "Serial No",
"item_code": sn_item,
"serial_no": sn,
}
)
sn_doc.insert()
warehouse = "_Test Warehouse - _TC"
company = frappe.db.get_value("Warehouse", warehouse, "company")
se_doc = make_stock_entry(
item_code=sn_item,
company=company,
target="_Test Warehouse - _TC",
qty=12,
basic_rate=100,
do_not_submit=1,
)
se_doc.items[0].serial_no = "\n".join(serial_nos)
se_doc.submit()
self.assertEqual(sorted(get_serial_nos(se_doc.items[0].serial_no)), sorted(serial_nos))
dn = create_delivery_note(
item_code=sn_item,
qty=12,
rate=500,
warehouse=warehouse,
company=company,
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
do_not_submit=1,
)
dn.items[0].serial_no = "\n".join(serial_nos)
dn.submit()
dn.reload()
self.assertTrue(dn.items[0].serial_no)
frappe.flags.ignore_serial_batch_bundle_validation = False
# return entry
dn1 = make_sales_return(dn.name)
dn1.items[0].qty = -2
bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn1.items[0].serial_and_batch_bundle)
bundle_doc.set("entries", bundle_doc.entries[:2])
bundle_doc.save()
dn1.save()
dn1.submit()
returned_serial_nos1 = get_serial_nos_from_bundle(dn1.items[0].serial_and_batch_bundle)
for serial_no in returned_serial_nos1:
self.assertTrue(serial_no in serial_nos)
dn2 = make_sales_return(dn.name)
dn2.items[0].qty = -2
bundle_doc = frappe.get_doc("Serial and Batch Bundle", dn2.items[0].serial_and_batch_bundle)
bundle_doc.set("entries", bundle_doc.entries[:2])
bundle_doc.save()
dn2.save()
dn2.submit()
returned_serial_nos2 = get_serial_nos_from_bundle(dn2.items[0].serial_and_batch_bundle)
for serial_no in returned_serial_nos2:
self.assertTrue(serial_no in serial_nos)
self.assertFalse(serial_no in returned_serial_nos1)
def test_sales_return_for_non_bundled_items_partial(self):
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
@@ -1266,6 +1375,56 @@ class TestDeliveryNote(FrappeTestCase):
dn.reload()
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
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 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

@@ -169,7 +169,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

@@ -289,7 +289,8 @@
"no_copy": 1,
"options": "Purchase Receipt",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "section_addresses",
@@ -1251,7 +1252,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
"modified": "2023-11-28 13:14:15.243474",
"modified": "2023-12-18 17:26:41.279663",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",

View File

@@ -1357,10 +1357,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

@@ -121,7 +121,7 @@ frappe.ui.form.on('Serial and Batch Bundle', {
frappe.throw(__("Please attach CSV file"));
}
if (frm.doc.has_serial_no && !prompt_data.using_csv_file && !prompt_data.serial_nos) {
if (frm.doc.has_serial_no && !prompt_data.csv_file && !prompt_data.serial_nos) {
frappe.throw(__("Please enter serial nos"));
}
},

View File

@@ -1,7 +1,7 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2022-09-29 14:56:38.338267",
"creation": "2023-08-11 17:22:12.907518",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
@@ -250,7 +250,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-07-28 12:56:03.072224",
"modified": "2023-12-07 17:56:55.528563",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Bundle",
@@ -270,6 +270,118 @@
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase User",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock User",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Delivery User",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Delivery Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Manufacturing User",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Manufacturing Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",

View File

@@ -23,7 +23,11 @@ from frappe.utils import (
)
from frappe.utils.csvutils import build_csv_response
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
from erpnext.stock.serial_batch_bundle import (
BatchNoValuation,
SerialNoValuation,
get_batches_from_bundle,
)
from erpnext.stock.serial_batch_bundle import get_serial_nos as get_serial_nos_from_bundle
@@ -123,6 +127,11 @@ class SerialandBatchBundle(Document):
)
def validate_serial_nos_duplicate(self):
# Don't inward same serial number multiple times
if not self.warehouse:
return
if self.voucher_type in ["Stock Reconciliation", "Stock Entry"] and self.docstatus != 1:
return
@@ -146,7 +155,6 @@ class SerialandBatchBundle(Document):
kwargs["voucher_no"] = self.voucher_no
available_serial_nos = get_available_serial_nos(kwargs)
for data in available_serial_nos:
if data.serial_no in serial_nos:
self.throw_error_message(
@@ -157,7 +165,7 @@ class SerialandBatchBundle(Document):
def throw_error_message(self, message, exception=frappe.ValidationError):
frappe.throw(_(message), exception, title=_("Error"))
def set_incoming_rate(self, row=None, save=False):
def set_incoming_rate(self, row=None, save=False, allow_negative_stock=False):
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
"Installation Note",
"Job Card",
@@ -167,7 +175,9 @@ class SerialandBatchBundle(Document):
return
if self.type_of_transaction == "Outward":
self.set_incoming_rate_for_outward_transaction(row, save)
self.set_incoming_rate_for_outward_transaction(
row, save, allow_negative_stock=allow_negative_stock
)
else:
self.set_incoming_rate_for_inward_transaction(row, save)
@@ -188,7 +198,9 @@ class SerialandBatchBundle(Document):
def get_serial_nos(self):
return [d.serial_no for d in self.entries if d.serial_no]
def set_incoming_rate_for_outward_transaction(self, row=None, save=False):
def set_incoming_rate_for_outward_transaction(
self, row=None, save=False, allow_negative_stock=False
):
sle = self.get_sle_for_outward_transaction()
if self.has_serial_no:
@@ -217,7 +229,8 @@ class SerialandBatchBundle(Document):
if self.docstatus == 1:
available_qty += flt(d.qty)
self.validate_negative_batch(d.batch_no, available_qty)
if not allow_negative_stock:
self.validate_negative_batch(d.batch_no, available_qty)
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
@@ -322,6 +335,19 @@ class SerialandBatchBundle(Document):
):
values_to_set["posting_time"] = parent.posting_time
if parent.doctype in [
"Delivery Note",
"Purchase Receipt",
"Purchase Invoice",
"Sales Invoice",
] and parent.get("is_return"):
return_ref_field = frappe.scrub(parent.doctype) + "_item"
if parent.doctype == "Delivery Note":
return_ref_field = "dn_detail"
if row.get(return_ref_field):
values_to_set["returned_against"] = row.get(return_ref_field)
if values_to_set:
self.db_set(values_to_set)
@@ -433,7 +459,7 @@ class SerialandBatchBundle(Document):
qty_field = "qty"
precision = row.precision
if self.voucher_type in ["Subcontracting Receipt"]:
if row.get("doctype") in ["Subcontracting Receipt Supplied Item"]:
qty_field = "consumed_qty"
if abs(abs(flt(self.total_qty, precision)) - abs(flt(row.get(qty_field), precision))) > 0.01:
@@ -504,8 +530,23 @@ class SerialandBatchBundle(Document):
batch_nos = []
serial_batches = {}
for row in self.entries:
if self.has_serial_no and not row.serial_no:
frappe.throw(
_("At row {0}: Serial No is mandatory for Item {1}").format(
bold(row.idx), bold(self.item_code)
),
title=_("Serial No is mandatory"),
)
if self.has_batch_no and not row.batch_no:
frappe.throw(
_("At row {0}: Batch No is mandatory for Item {1}").format(
bold(row.idx), bold(self.item_code)
),
title=_("Batch No is mandatory"),
)
if row.serial_no:
serial_nos.append(row.serial_no)
@@ -569,6 +610,67 @@ class SerialandBatchBundle(Document):
f"Batch Nos {bold(incorrect_batch_nos)} does not belong to Item {bold(self.item_code)}"
)
def validate_serial_and_batch_no_for_returned(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
if not self.returned_against:
return
if self.voucher_type not in [
"Purchase Receipt",
"Purchase Invoice",
"Sales Invoice",
"Delivery Note",
]:
return
data = self.get_orignal_document_data()
if not data:
return
serial_nos, batches = [], []
current_serial_nos = [d.serial_no for d in self.entries if d.serial_no]
current_batches = [d.batch_no for d in self.entries if d.batch_no]
for d in data:
if self.has_serial_no:
if d.serial_and_batch_bundle:
serial_nos = get_serial_nos_from_bundle(d.serial_and_batch_bundle)
else:
serial_nos = get_serial_nos(d.serial_no)
elif self.has_batch_no:
if d.serial_and_batch_bundle:
batches = get_batches_from_bundle(d.serial_and_batch_bundle)
else:
batches = frappe._dict({d.batch_no: d.stock_qty})
if batches:
batches = [d for d in batches if batches[d] > 0]
if serial_nos:
if not set(current_serial_nos).issubset(set(serial_nos)):
self.throw_error_message(
f"Serial Nos {bold(', '.join(serial_nos))} are not part of the original document."
)
if batches:
if not set(current_batches).issubset(set(batches)):
self.throw_error_message(
f"Batch Nos {bold(', '.join(batches))} are not part of the original document."
)
def get_orignal_document_data(self):
fields = ["serial_and_batch_bundle", "stock_qty"]
if self.has_serial_no:
fields.append("serial_no")
elif self.has_batch_no:
fields.append("batch_no")
child_doc = self.voucher_type + " Item"
return frappe.get_all(child_doc, fields=fields, filters={"name": self.returned_against})
def validate_duplicate_serial_and_batch_no(self):
serial_nos = []
batch_nos = []
@@ -667,9 +769,29 @@ class SerialandBatchBundle(Document):
for batch in batches:
frappe.db.set_value("Batch", batch.name, {"reference_name": None, "reference_doctype": None})
def before_submit(self):
self.validate_serial_and_batch_no_for_returned()
self.set_purchase_document_no()
def on_submit(self):
self.validate_serial_nos_inventory()
def set_purchase_document_no(self):
if not self.has_serial_no:
return
if self.total_qty > 0:
serial_nos = [d.serial_no for d in self.entries if d.serial_no]
sn_table = frappe.qb.DocType("Serial No")
(
frappe.qb.update(sn_table)
.set(
sn_table.purchase_document_no,
self.voucher_no if not sn_table.purchase_document_no else self.voucher_no,
)
.where(sn_table.name.isin(serial_nos))
).run()
def validate_serial_and_batch_inventory(self):
self.check_future_entries_exists()
self.validate_batch_inventory()
@@ -688,6 +810,7 @@ class SerialandBatchBundle(Document):
"item_code": self.item_code,
"warehouse": self.warehouse,
"batch_no": batches,
"consider_negative_batches": True,
}
)
)
@@ -698,6 +821,9 @@ class SerialandBatchBundle(Document):
available_batches = get_available_batches_qty(available_batches)
for batch_no in batches:
if batch_no not in available_batches or available_batches[batch_no] < 0:
if flt(available_batches.get(batch_no)) < 0:
self.validate_negative_batch(batch_no, available_batches[batch_no])
self.throw_error_message(
f"Batch {bold(batch_no)} is not available in the selected warehouse {self.warehouse}"
)
@@ -789,6 +915,9 @@ def parse_csv_file_to_get_serial_batch(reader):
if index == 0:
has_serial_no = row[0] == "Serial No"
has_batch_no = row[0] == "Batch No"
if not has_batch_no:
has_batch_no = row[1] == "Batch No"
continue
if not row[0]:
@@ -805,6 +934,13 @@ def parse_csv_file_to_get_serial_batch(reader):
}
)
batch_nos.append(
{
"batch_no": row[1],
"qty": row[2],
}
)
serial_nos.append(_dict)
elif has_batch_no:
batch_nos.append(
@@ -840,6 +976,9 @@ def make_serial_nos(item_code, serial_nos):
serial_nos_details = []
user = frappe.session.user
for serial_no in serial_nos:
if frappe.db.exists("Serial No", serial_no):
continue
serial_nos_details.append(
(
serial_no,
@@ -870,7 +1009,7 @@ def make_serial_nos(item_code, serial_nos):
frappe.db.bulk_insert("Serial No", fields=fields, values=set(serial_nos_details))
frappe.msgprint(_("Serial Nos are created successfully"))
frappe.msgprint(_("Serial Nos are created successfully"), alert=True)
def make_batch_nos(item_code, batch_nos):
@@ -881,6 +1020,9 @@ def make_batch_nos(item_code, batch_nos):
batch_nos_details = []
user = frappe.session.user
for batch_no in batch_nos:
if frappe.db.exists("Batch", batch_no):
continue
batch_nos_details.append(
(batch_no, batch_no, now(), now(), user, user, item.item_code, item.item_name, item.description)
)
@@ -899,7 +1041,7 @@ def make_batch_nos(item_code, batch_nos):
frappe.db.bulk_insert("Batch", fields=fields, values=set(batch_nos_details))
frappe.msgprint(_("Batch Nos are created successfully"))
frappe.msgprint(_("Batch Nos are created successfully"), alert=True)
def parse_serial_nos(data):
@@ -1454,7 +1596,8 @@ def get_auto_batch_nos(kwargs):
available_batches, stock_ledgers_batches, pos_invoice_batches, sre_reserved_batches
)
available_batches = list(filter(lambda x: x.qty > 0, available_batches))
if not kwargs.consider_negative_batches:
available_batches = list(filter(lambda x: x.qty > 0, available_batches))
if not qty:
return available_batches

View File

@@ -368,6 +368,58 @@ class TestSerialandBatchBundle(FrappeTestCase):
# Batch does not belong to serial no
self.assertRaises(frappe.exceptions.ValidationError, doc.save)
def test_auto_delete_draft_serial_and_batch_bundle(self):
serial_and_batch_code = "New Serial No Auto Delete 1"
make_item(
serial_and_batch_code,
{
"has_serial_no": 1,
"serial_no_series": "TEST-SER-VALL-.#####",
"is_stock_item": 1,
},
)
ste = make_stock_entry(
item_code=serial_and_batch_code,
target="_Test Warehouse - _TC",
qty=1,
rate=500,
do_not_submit=True,
)
serial_no = "SN-TEST-AUTO-DEL"
if not frappe.db.exists("Serial No", serial_no):
frappe.get_doc(
{
"doctype": "Serial No",
"serial_no": serial_no,
"item_code": serial_and_batch_code,
"company": "_Test Company",
}
).insert(ignore_permissions=True)
bundle_doc = make_serial_batch_bundle(
{
"item_code": serial_and_batch_code,
"warehouse": "_Test Warehouse - _TC",
"voucher_type": "Stock Entry",
"posting_date": ste.posting_date,
"posting_time": ste.posting_time,
"qty": 1,
"serial_nos": [serial_no],
"type_of_transaction": "Inward",
"do_not_submit": True,
}
)
bundle_doc.reload()
ste.items[0].serial_and_batch_bundle = bundle_doc.name
ste.save()
ste.reload()
ste.delete()
self.assertFalse(frappe.db.exists("Serial and Batch Bundle", bundle_doc.name))
def get_batch_from_bundle(bundle):
from erpnext.stock.serial_batch_bundle import get_batch_nos

View File

@@ -27,7 +27,6 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Serial No",
"mandatory_depends_on": "eval:parent.has_serial_no == 1",
"options": "Serial No",
"search_index": 1
},
@@ -38,7 +37,6 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Batch No",
"mandatory_depends_on": "eval:parent.has_batch_no == 1",
"options": "Batch",
"search_index": 1
},
@@ -122,7 +120,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-07-03 15:29:50.199075",
"modified": "2023-12-10 19:47:48.227772",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial and Batch Entry",

View File

@@ -27,8 +27,6 @@
"column_break_24",
"location",
"employee",
"delivery_details",
"delivery_document_type",
"warranty_amc_details",
"column_break6",
"warranty_expiry_date",
@@ -39,7 +37,8 @@
"more_info",
"company",
"column_break_2cmm",
"work_order"
"work_order",
"purchase_document_no"
],
"fields": [
{
@@ -153,20 +152,6 @@
"options": "Employee",
"read_only": 1
},
{
"fieldname": "delivery_details",
"fieldtype": "Section Break",
"label": "Delivery Details",
"oldfieldtype": "Column Break"
},
{
"fieldname": "delivery_document_type",
"fieldtype": "Link",
"label": "Delivery Document Type",
"no_copy": 1,
"options": "DocType",
"read_only": 1
},
{
"fieldname": "warranty_amc_details",
"fieldtype": "Section Break",
@@ -275,12 +260,19 @@
{
"fieldname": "column_break_2cmm",
"fieldtype": "Column Break"
},
{
"fieldname": "purchase_document_no",
"fieldtype": "Data",
"label": "Creation Document No",
"no_copy": 1,
"read_only": 1
}
],
"icon": "fa fa-barcode",
"idx": 1,
"links": [],
"modified": "2023-11-28 15:37:59.489945",
"modified": "2023-12-17 10:52:55.767839",
"modified_by": "Administrator",
"module": "Stock",
"name": "Serial No",

View File

@@ -41,7 +41,6 @@ class SerialNo(StockController):
batch_no: DF.Link | None
brand: DF.Link | None
company: DF.Link
delivery_document_type: DF.Link | None
description: DF.Text | None
employee: DF.Link | None
item_code: DF.Link
@@ -51,6 +50,7 @@ class SerialNo(StockController):
maintenance_status: DF.Literal[
"", "Under Warranty", "Out of Warranty", "Under AMC", "Out of AMC"
]
purchase_document_no: DF.Data | None
purchase_rate: DF.Float
serial_no: DF.Data
status: DF.Literal["", "Active", "Inactive", "Delivered", "Expired"]
@@ -231,26 +231,6 @@ def auto_fetch_serial_number(
return sorted([d.get("name") for d in serial_numbers])
def get_delivered_serial_nos(serial_nos):
"""
Returns serial numbers that delivered from the list of serial numbers
"""
from frappe.query_builder.functions import Coalesce
SerialNo = frappe.qb.DocType("Serial No")
serial_nos = get_serial_nos(serial_nos)
query = (
frappe.qb.select(SerialNo.name)
.from_(SerialNo)
.where((SerialNo.name.isin(serial_nos)) & (Coalesce(SerialNo.delivery_document_type, "") != ""))
)
result = query.run()
if result and len(result) > 0:
delivered_serial_nos = [row[0] for row in result]
return delivered_serial_nos
@frappe.whitelist()
def get_pos_reserved_serial_nos(filters):
if isinstance(filters, str):

View File

@@ -781,10 +781,9 @@ frappe.ui.form.on('Stock Entry Detail', {
});
refresh_field("items");
let no_batch_serial_number_value = !d.serial_no;
if (d.has_batch_no && !d.has_serial_no) {
// check only batch_no for batched item
no_batch_serial_number_value = !d.batch_no;
let no_batch_serial_number_value = false;
if (d.has_serial_no || d.has_batch_no) {
no_batch_serial_number_value = true;
}
if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog && !frappe.flags.dialog_set) {
@@ -941,6 +940,7 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
}
scan_barcode() {
frappe.flags.dialog_set = false;
const barcode_scanner = new erpnext.utils.BarcodeScanner({frm:this.frm});
barcode_scanner.process_scan();
}

View File

@@ -1737,6 +1737,45 @@ class TestStockEntry(FrappeTestCase):
self.assertFalse(doc.is_enqueue_action())
frappe.flags.in_test = True
def test_negative_batch(self):
item_code = "Test Negative Batch Item - 001"
make_item(
item_code,
{"has_batch_no": 1, "create_new_batch": 1, "batch_naming_series": "Test-BCH-NNS.#####"},
)
se1 = make_stock_entry(
item_code=item_code,
purpose="Material Receipt",
qty=100,
target="_Test Warehouse - _TC",
)
se1.reload()
batch_no = get_batch_from_bundle(se1.items[0].serial_and_batch_bundle)
se2 = make_stock_entry(
item_code=item_code,
purpose="Material Issue",
batch_no=batch_no,
qty=10,
source="_Test Warehouse - _TC",
)
se2.reload()
se3 = make_stock_entry(
item_code=item_code,
purpose="Material Receipt",
qty=100,
target="_Test Warehouse - _TC",
)
se3.reload()
self.assertRaises(frappe.ValidationError, se1.cancel)
def make_serialized_item(**args):
args = frappe._dict(args)

View File

@@ -181,6 +181,9 @@ class StockLedgerEntry(Document):
frappe.throw(_("Actual Qty is mandatory"))
def validate_serial_batch_no_bundle(self):
if self.is_cancelled == 1:
return
item_detail = frappe.get_cached_value(
"Item",
self.item_code,

View File

@@ -209,7 +209,7 @@ frappe.ui.form.on("Stock Reconciliation", {
set_amount_quantity: function(doc, cdt, cdn) {
var d = frappe.model.get_doc(cdt, cdn);
if (d.qty & d.valuation_rate) {
if (d.qty && d.valuation_rate) {
frappe.model.set_value(cdt, cdn, "amount", flt(d.qty) * flt(d.valuation_rate));
frappe.model.set_value(cdt, cdn, "quantity_difference", flt(d.qty) - flt(d.current_qty));
frappe.model.set_value(cdt, cdn, "amount_difference", flt(d.amount) - flt(d.current_amount));

View File

@@ -6,7 +6,7 @@ from typing import Optional
import frappe
from frappe import _, bold, msgprint
from frappe.query_builder.functions import CombineDatetime, Sum
from frappe.utils import cint, cstr, flt
from frappe.utils import add_to_date, cint, cstr, flt
import erpnext
from erpnext.accounts.utils import get_company_default
@@ -116,9 +116,12 @@ class StockReconciliation(StockController):
self.repost_future_sle_and_gle()
self.delete_auto_created_batches()
def set_current_serial_and_batch_bundle(self):
def set_current_serial_and_batch_bundle(self, voucher_detail_no=None, save=False) -> None:
"""Set Serial and Batch Bundle for each item"""
for item in self.items:
if voucher_detail_no and voucher_detail_no != item.name:
continue
item_details = frappe.get_cached_value(
"Item", item.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
)
@@ -176,6 +179,7 @@ class StockReconciliation(StockController):
"warehouse": item.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"ignore_voucher_nos": [self.name],
}
)
)
@@ -191,11 +195,36 @@ class StockReconciliation(StockController):
)
if not serial_and_batch_bundle.entries:
if voucher_detail_no:
return
continue
item.current_serial_and_batch_bundle = serial_and_batch_bundle.save().name
serial_and_batch_bundle.save()
item.current_serial_and_batch_bundle = serial_and_batch_bundle.name
item.current_qty = abs(serial_and_batch_bundle.total_qty)
item.current_valuation_rate = abs(serial_and_batch_bundle.avg_rate)
if save:
sle_creation = frappe.db.get_value(
"Serial and Batch Bundle", item.serial_and_batch_bundle, "creation"
)
creation = add_to_date(sle_creation, seconds=-1)
item.db_set(
{
"current_serial_and_batch_bundle": item.current_serial_and_batch_bundle,
"current_qty": item.current_qty,
"current_valuation_rate": item.current_valuation_rate,
"creation": creation,
}
)
serial_and_batch_bundle.db_set(
{
"creation": creation,
"voucher_no": self.name,
"voucher_detail_no": voucher_detail_no,
}
)
def set_new_serial_and_batch_bundle(self):
for item in self.items:
@@ -737,56 +766,84 @@ class StockReconciliation(StockController):
else:
self._cancel()
def recalculate_current_qty(self, item_code, batch_no):
def recalculate_current_qty(self, voucher_detail_no, sle_creation, add_new_sle=False):
from erpnext.stock.stock_ledger import get_valuation_rate
sl_entries = []
for row in self.items:
if (
not (row.item_code == item_code and row.batch_no == batch_no)
and not row.serial_and_batch_bundle
):
if voucher_detail_no != row.name:
continue
current_qty = 0.0
if row.current_serial_and_batch_bundle:
self.recalculate_qty_for_serial_and_batch_bundle(row)
continue
current_qty = get_batch_qty_for_stock_reco(
item_code, row.warehouse, batch_no, self.posting_date, self.posting_time, self.name
)
current_qty = self.get_qty_for_serial_and_batch_bundle(row)
elif row.batch_no:
current_qty = get_batch_qty_for_stock_reco(
row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
)
precesion = row.precision("current_qty")
if flt(current_qty, precesion) == flt(row.current_qty, precesion):
continue
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
val_rate = get_valuation_rate(
row.item_code,
row.warehouse,
self.doctype,
self.name,
company=self.company,
batch_no=row.batch_no,
serial_and_batch_bundle=row.current_serial_and_batch_bundle,
)
val_rate = get_valuation_rate(
item_code, row.warehouse, self.doctype, self.name, company=self.company, batch_no=batch_no
)
row.current_valuation_rate = val_rate
row.current_qty = current_qty
row.db_set(
{
"current_qty": row.current_qty,
"current_valuation_rate": row.current_valuation_rate,
"current_amount": flt(row.current_qty * row.current_valuation_rate),
}
)
row.current_valuation_rate = val_rate
if not row.current_qty and current_qty:
sle = self.get_sle_for_items(row)
sle.actual_qty = current_qty * -1
sle.valuation_rate = val_rate
sl_entries.append(sle)
if (
add_new_sle
and not frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_detail_no": row.name, "actual_qty": ("<", 0), "is_cancelled": 0},
"name",
)
and (not row.current_serial_and_batch_bundle and not row.batch_no)
):
self.set_current_serial_and_batch_bundle(voucher_detail_no, save=True)
row.reload()
row.current_qty = current_qty
row.db_set(
{
"current_qty": row.current_qty,
"current_valuation_rate": row.current_valuation_rate,
"current_amount": flt(row.current_qty * row.current_valuation_rate),
}
)
if row.current_qty > 0 and row.current_serial_and_batch_bundle:
new_sle = self.get_sle_for_items(row)
new_sle.actual_qty = row.current_qty * -1
new_sle.valuation_rate = row.current_valuation_rate
new_sle.creation_time = add_to_date(sle_creation, seconds=-1)
new_sle.serial_and_batch_bundle = row.current_serial_and_batch_bundle
new_sle.qty_after_transaction = 0.0
sl_entries.append(new_sle)
if sl_entries:
self.make_sl_entries(sl_entries, allow_negative_stock=True)
self.make_sl_entries(sl_entries, allow_negative_stock=self.has_negative_stock_allowed())
if not frappe.db.exists("Repost Item Valuation", {"voucher_no": self.name, "status": "Queued"}):
self.repost_future_sle_and_gle(force=True)
def recalculate_qty_for_serial_and_batch_bundle(self, row):
def has_negative_stock_allowed(self):
allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
if all(d.serial_and_batch_bundle and flt(d.qty) == flt(d.current_qty) for d in self.items):
allow_negative_stock = True
return allow_negative_stock
def get_qty_for_serial_and_batch_bundle(self, row):
doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
precision = doc.entries[0].precision("qty")
current_qty = 0
for d in doc.entries:
qty = (
get_batch_qty(
@@ -799,10 +856,12 @@ class StockReconciliation(StockController):
or 0
) * -1
if flt(d.qty, precision) == flt(qty, precision):
continue
if flt(d.qty, precision) != flt(qty, precision):
d.db_set("qty", qty)
d.db_set("qty", qty)
current_qty += qty
return abs(current_qty)
def get_batch_qty_for_stock_reco(

View File

@@ -742,13 +742,6 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
se2.cancel()
self.assertTrue(frappe.db.exists("Repost Item Valuation", {"voucher_no": stock_reco.name}))
self.assertEqual(
frappe.db.get_value("Repost Item Valuation", {"voucher_no": stock_reco.name}, "status"),
"Completed",
)
sle = frappe.get_all(
"Stock Ledger Entry",
filters={"item_code": item_code, "warehouse": warehouse, "is_cancelled": 0},
@@ -766,6 +759,68 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
self.assertEqual(flt(sle[0].actual_qty), flt(-100.0))
def test_backdated_stock_reco_entry_with_batch(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item_code = self.make_item(
"Test New Batch Item ABCVSD",
{
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "BNS9.####",
"create_new_batch": 1,
},
).name
warehouse = "_Test Warehouse - _TC"
# Stock Reco for 100, Balace Qty 100
stock_reco = create_stock_reconciliation(
item_code=item_code,
posting_date=nowdate(),
posting_time="11:00:00",
warehouse=warehouse,
qty=100,
rate=100,
)
sles = frappe.get_all(
"Stock Ledger Entry",
fields=["actual_qty"],
filters={"voucher_no": stock_reco.name, "is_cancelled": 0},
)
self.assertEqual(len(sles), 1)
stock_reco.reload()
batch_no = get_batch_from_bundle(stock_reco.items[0].serial_and_batch_bundle)
# Stock Reco for 100, Balace Qty 100
stock_reco1 = create_stock_reconciliation(
item_code=item_code,
posting_date=add_days(nowdate(), -1),
posting_time="11:00:00",
batch_no=batch_no,
warehouse=warehouse,
qty=60,
rate=100,
)
sles = frappe.get_all(
"Stock Ledger Entry",
fields=["actual_qty"],
filters={"voucher_no": stock_reco.name, "is_cancelled": 0},
)
stock_reco1.reload()
new_batch_no = get_batch_from_bundle(stock_reco1.items[0].serial_and_batch_bundle)
self.assertEqual(len(sles), 2)
for row in sles:
if row.actual_qty < 0:
self.assertEqual(row.actual_qty, -60)
def test_update_stock_reconciliation_while_reposting(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry

View File

@@ -205,6 +205,7 @@
"fieldname": "current_serial_and_batch_bundle",
"fieldtype": "Link",
"label": "Current Serial / Batch Bundle",
"no_copy": 1,
"options": "Serial and Batch Bundle",
"read_only": 1
},
@@ -216,7 +217,7 @@
],
"istable": 1,
"links": [],
"modified": "2023-07-26 12:54:34.011915",
"modified": "2023-11-02 15:47:07.929550",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",

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 IfNull, Sum
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
@@ -357,7 +358,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,
@@ -377,6 +377,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))
@@ -571,6 +575,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

@@ -1,9 +1,12 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import copy
import frappe
from frappe import _
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_serial_nos_from_sle
from erpnext.stock.stock_ledger import get_stock_ledger_entries
@@ -15,8 +18,8 @@ def execute(filters=None):
def get_columns(filters):
columns = [
{"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date"},
{"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time"},
{"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 120},
{"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90},
{
"label": _("Voucher Type"),
"fieldtype": "Link",
@@ -29,7 +32,7 @@ def get_columns(filters):
"fieldtype": "Dynamic Link",
"fieldname": "voucher_no",
"options": "voucher_type",
"width": 180,
"width": 230,
},
{
"label": _("Company"),
@@ -49,7 +52,7 @@ def get_columns(filters):
"label": _("Status"),
"fieldtype": "Data",
"fieldname": "status",
"width": 120,
"width": 90,
},
{
"label": _("Serial No"),
@@ -62,7 +65,7 @@ def get_columns(filters):
"label": _("Valuation Rate"),
"fieldtype": "Float",
"fieldname": "valuation_rate",
"width": 150,
"width": 130,
},
{
"label": _("Qty"),
@@ -102,15 +105,29 @@ def get_data(filters):
}
)
serial_nos = [{"serial_no": row.serial_no, "valuation_rate": row.valuation_rate}]
serial_nos = []
if row.serial_no:
parsed_serial_nos = get_serial_nos_from_sle(row.serial_no)
for serial_no in parsed_serial_nos:
if filters.get("serial_no") and filters.get("serial_no") != serial_no:
continue
serial_nos.append(
{
"serial_no": serial_no,
"valuation_rate": abs(row.stock_value_difference / row.actual_qty),
}
)
if row.serial_and_batch_bundle:
serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, [])
serial_nos.extend(bundle_wise_serial_nos.get(row.serial_and_batch_bundle, []))
for index, bundle_data in enumerate(serial_nos):
if index == 0:
args.serial_no = bundle_data.get("serial_no")
args.valuation_rate = bundle_data.get("valuation_rate")
data.append(args)
new_args = copy.deepcopy(args)
new_args.serial_no = bundle_data.get("serial_no")
new_args.valuation_rate = bundle_data.get("valuation_rate")
data.append(new_args)
else:
data.append(
{

View File

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

View File

@@ -249,6 +249,13 @@ def get_columns(filters):
"options": "Serial No",
"width": 100,
},
{
"label": _("Serial and Batch Bundle"),
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"options": "Serial and Batch Bundle",
"width": 100,
},
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100},
{
"label": _("Project"),
@@ -287,6 +294,7 @@ def get_stock_ledger_entries(filters, items):
sle.voucher_type,
sle.qty_after_transaction,
sle.stock_value_difference,
sle.serial_and_batch_bundle,
sle.voucher_no,
sle.stock_value,
sle.batch_no,

View File

@@ -24,6 +24,7 @@ SLE_FIELDS = (
"stock_value_difference",
"valuation_rate",
"voucher_detail_no",
"serial_and_batch_bundle",
)
@@ -64,7 +65,11 @@ def add_invariant_check_fields(sles):
balance_qty += sle.actual_qty
balance_stock_value += sle.stock_value_difference
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
if (
sle.voucher_type == "Stock Reconciliation"
and not sle.batch_no
and not sle.serial_and_batch_bundle
):
balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty")
if balance_qty is None:
balance_qty = sle.qty_after_transaction
@@ -143,6 +148,12 @@ def get_columns():
"label": _("Batch"),
"options": "Batch",
},
{
"fieldname": "serial_and_batch_bundle",
"fieldtype": "Link",
"label": _("Serial and Batch Bundle"),
"options": "Serial and Batch Bundle",
},
{
"fieldname": "use_batchwise_valuation",
"fieldtype": "Check",

View File

@@ -218,15 +218,16 @@ class SerialBatchBundle:
).validate_serial_and_batch_inventory()
def post_process(self):
if not self.sle.serial_and_batch_bundle:
if not self.sle.serial_and_batch_bundle and not self.sle.serial_no and not self.sle.batch_no:
return
docstatus = frappe.get_cached_value(
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
)
if self.sle.serial_and_batch_bundle:
docstatus = frappe.get_cached_value(
"Serial and Batch Bundle", self.sle.serial_and_batch_bundle, "docstatus"
)
if docstatus != 1:
self.submit_serial_and_batch_bundle()
if docstatus != 1:
self.submit_serial_and_batch_bundle()
if self.item_details.has_serial_no == 1:
self.set_warehouse_and_status_in_serial_nos()
@@ -249,7 +250,12 @@ class SerialBatchBundle:
doc.submit()
def set_warehouse_and_status_in_serial_nos(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos as get_parsed_serial_nos
serial_nos = get_serial_nos(self.sle.serial_and_batch_bundle)
if not self.sle.serial_and_batch_bundle and self.sle.serial_no:
serial_nos = get_parsed_serial_nos(self.sle.serial_no)
warehouse = self.warehouse if self.sle.actual_qty > 0 else None
if not serial_nos:
@@ -263,7 +269,14 @@ class SerialBatchBundle:
(
frappe.qb.update(sn_table)
.set(sn_table.warehouse, warehouse)
.set(sn_table.status, "Active" if warehouse else status)
.set(
sn_table.status,
"Active"
if warehouse
else status
if (sn_table.purchase_document_no != self.sle.voucher_no and self.sle.is_cancelled != 1)
else "Inactive",
)
.where(sn_table.name.isin(serial_nos))
).run()
@@ -290,6 +303,8 @@ class SerialBatchBundle:
from erpnext.stock.doctype.batch.batch import get_available_batches
batches = get_batch_nos(self.sle.serial_and_batch_bundle)
if not self.sle.serial_and_batch_bundle and self.sle.batch_no:
batches = frappe._dict({self.sle.batch_no: self.sle.actual_qty})
batches_qty = get_available_batches(
frappe._dict(
@@ -312,13 +327,35 @@ def get_serial_nos(serial_and_batch_bundle, serial_nos=None):
if serial_nos:
filters["serial_no"] = ("in", serial_nos)
entries = frappe.get_all("Serial and Batch Entry", fields=["serial_no"], filters=filters)
entries = frappe.get_all(
"Serial and Batch Entry", fields=["serial_no"], filters=filters, order_by="idx"
)
if not entries:
return []
return [d.serial_no for d in entries if d.serial_no]
def get_batches_from_bundle(serial_and_batch_bundle, batches=None):
if not serial_and_batch_bundle:
return []
filters = {"parent": serial_and_batch_bundle, "batch_no": ("is", "set")}
if isinstance(serial_and_batch_bundle, list):
filters = {"parent": ("in", serial_and_batch_bundle)}
if batches:
filters["batch_no"] = ("in", batches)
entries = frappe.get_all(
"Serial and Batch Entry", fields=["batch_no", "qty"], filters=filters, order_by="idx", as_list=1
)
if not entries:
return frappe._dict({})
return frappe._dict(entries)
def get_serial_nos_from_bundle(serial_and_batch_bundle, serial_nos=None):
return get_serial_nos(serial_and_batch_bundle, serial_nos=serial_nos)

View File

@@ -210,6 +210,11 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
sle.allow_negative_stock = allow_negative_stock
sle.via_landed_cost_voucher = via_landed_cost_voucher
sle.submit()
# Added to handle the case when the stock ledger entry is created from the repostig
if args.get("creation_time") and args.get("voucher_type") == "Stock Reconciliation":
sle.db_set("creation", args.get("creation_time"))
return sle
@@ -696,9 +701,11 @@ class update_entries_after(object):
if (
sle.voucher_type == "Stock Reconciliation"
and (sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle))
and (
sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
)
and sle.voucher_detail_no
and sle.actual_qty < 0
and not self.args.get("sle_id")
):
self.reset_actual_qty_for_stock_reco(sle)
@@ -765,27 +772,22 @@ class update_entries_after(object):
self.update_outgoing_rate_on_transaction(sle)
def reset_actual_qty_for_stock_reco(self, sle):
if sle.serial_and_batch_bundle:
current_qty = frappe.get_cached_value(
"Serial and Batch Bundle", sle.serial_and_batch_bundle, "total_qty"
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
if sle.actual_qty < 0:
sle.actual_qty = (
flt(frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"))
* -1
)
if current_qty is not None:
current_qty = abs(current_qty)
else:
current_qty = frappe.get_cached_value(
"Stock Reconciliation Item", sle.voucher_detail_no, "current_qty"
)
if current_qty:
sle.actual_qty = current_qty * -1
elif current_qty == 0:
sle.is_cancelled = 1
if abs(sle.actual_qty) == 0.0:
sle.is_cancelled = 1
def calculate_valuation_for_serial_batch_bundle(self, sle):
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
doc.set_incoming_rate(save=True)
doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
doc.calculate_qty_and_amount(save=True)
self.wh_data.stock_value = round_off_if_near_zero(self.wh_data.stock_value + doc.total_amount)
@@ -1472,6 +1474,7 @@ def get_valuation_rate(
currency=None,
company=None,
raise_error_if_no_rate=True,
batch_no=None,
serial_and_batch_bundle=None,
):
@@ -1480,6 +1483,25 @@ def get_valuation_rate(
if not company:
company = frappe.get_cached_value("Warehouse", warehouse, "company")
if warehouse and batch_no and frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"):
table = frappe.qb.DocType("Stock Ledger Entry")
query = (
frappe.qb.from_(table)
.select(Sum(table.stock_value_difference) / Sum(table.actual_qty))
.where(
(table.item_code == item_code)
& (table.warehouse == warehouse)
& (table.batch_no == batch_no)
& (table.is_cancelled == 0)
& (table.voucher_no != voucher_no)
& (table.voucher_type != voucher_type)
)
)
last_valuation_rate = query.run()
if last_valuation_rate:
return flt(last_valuation_rate[0][0])
# Get moving average rate of a specific batch number
if warehouse and serial_and_batch_bundle:
batch_obj = BatchNoValuation(
@@ -1574,8 +1596,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
next_stock_reco_detail = get_next_stock_reco(args)
if next_stock_reco_detail:
detail = next_stock_reco_detail[0]
if detail.batch_no or (detail.serial_and_batch_bundle and detail.has_batch_no):
regenerate_sle_for_batch_stock_reco(detail)
# add condition to update SLEs before this date & time
datetime_limit_condition = get_datetime_limit_condition(detail)
@@ -1604,16 +1624,6 @@ def update_qty_in_future_sle(args, allow_negative_stock=False):
validate_negative_qty_in_future_sle(args, allow_negative_stock)
def regenerate_sle_for_batch_stock_reco(detail):
doc = frappe.get_cached_doc("Stock Reconciliation", detail.voucher_no)
doc.recalculate_current_qty(detail.item_code, detail.batch_no)
if not frappe.db.exists(
"Repost Item Valuation", {"voucher_no": doc.name, "status": "Queued", "docstatus": "1"}
):
doc.repost_future_sle_and_gle(force=True)
def get_stock_reco_qty_shift(args):
stock_reco_qty_shift = 0
if args.get("is_cancelled"):

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
@@ -308,6 +309,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

@@ -167,13 +167,13 @@ class SubcontractingReceipt(SubcontractingController):
)
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()
def validate_items_qty(self):
for item in self.items:

View File

@@ -953,6 +953,91 @@ class TestSubcontractingReceipt(FrappeTestCase):
scr.submit()
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)
serial_batch_bundle = frappe.get_doc(
{
"doctype": "Serial and Batch Bundle",
"item_code": fg_item,
"warehouse": sco.items[0].warehouse,
"has_batch_no": 1,
"type_of_transaction": "Inward",
"voucher_type": "Subcontracting Receipt",
"entries": [{"batch_no": batch_doc.name, "qty": 100}],
}
).insert(ignore_permissions=True)
# Step - 7: Create Subcontracting Receipt
scr = make_subcontracting_receipt(sco.name)
scr.items[0].serial_and_batch_bundle = serial_batch_bundle.name
scr.save()
scr.submit()
scr.load_from_db()
# Step - 8: Cancel Subcontracting Receipt
scr.cancel()
self.assertTrue(scr.docstatus == 2)
@change_settings("Buying Settings", {"auto_create_purchase_receipt": 1})
def test_auto_create_purchase_receipt(self):
fg_item = "Subcontracted Item SA1"

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

@@ -774,10 +774,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

@@ -26,6 +26,26 @@
{{ render_homepage_section(homepage.hero_section_doc) }}
{% endif %}
{% if homepage.products %}
<section class="container section-products my-5">
<h3>{{ _('Products') }}</h3>
<div class="row">
{% for item in homepage.products %}
<div class="col-md-4 mb-4">
<div class="card h-100 justify-content-between">
<img class="card-img-top website-image-extra-large" src="{{ item.image }}" loading="lazy" alt="{{ item.item_name }}"></img>
<div class="card-body flex-grow-0">
<h5 class="card-title">{{ item.item_name }}</h5>
<a href="{{ item.route }}" class="card-link">{{ _('More details') }}</a>
</div>
</div>
</div>
{% endfor %}
</div>
</section>
{% endif %}
{% if blogs %}
<section class="container my-5">
<h3>{{ _('Publications') }}</h3>