Compare commits

...

128 Commits

Author SHA1 Message Date
Frappe PR Bot
30238e3063 chore(release): Bumped to Version 16.1.0
# [16.1.0](https://github.com/frappe/erpnext/compare/v16.0.1...v16.1.0) (2026-01-20)

### Bug Fixes

* **accounts_controller:** make return message translatable ([621243c](621243c1d3))
* **accounts:** add missing accounting dimensions in advance taxes and charges ([673635e](673635e2c3))
* add below-0 column in ar/ap report (backport [#51673](https://github.com/frappe/erpnext/issues/51673)) ([#51780](https://github.com/frappe/erpnext/issues/51780)) ([5c93bf5](5c93bf5798))
* add company filters for warehouse ([ccab91b](ccab91b9ed))
* add other charges in total ([68c8dfb](68c8dfb24c))
* add uom js error ([a660ed0](a660ed061b))
* add validation for amount and hours ([ce421bb](ce421bb1d4))
* add validation for direct return ([bfd6375](bfd6375508))
* add validation for duplication ([84a749e](84a749e3d0))
* add validation for return against ([6dade11](6dade11d8f))
* allow creation of DN in SI for items not having DN reference ([fef6df7](fef6df709d))
* allow disassemble stock entry without work order (backport [#51761](https://github.com/frappe/erpnext/issues/51761)) ([#51836](https://github.com/frappe/erpnext/issues/51836)) ([c830bf6](c830bf6fc7))
* **bank_account:** validation for is_company_account ([5d5d208](5d5d208a49))
* **bom:** pass company warehouse filter ([3c533d0](3c533d04f5))
* **budget variance report:** check budget dimensions ([a3d860e](a3d860eabf))
* bugs ([accce1f](accce1fe59))
* calculate net profit amount from root node accounts ([89b44c4](89b44c41a2))
* change docfield type to render html format (backport [#51795](https://github.com/frappe/erpnext/issues/51795)) ([#51804](https://github.com/frappe/erpnext/issues/51804)) ([fcea760](fcea7603a8))
* common_party_path ([#51826](https://github.com/frappe/erpnext/issues/51826)) ([aeb2b60](aeb2b60450))
* continuous raw material consumption with bom validation (backport [#51914](https://github.com/frappe/erpnext/issues/51914)) ([#51919](https://github.com/frappe/erpnext/issues/51919)) ([c9d7c6c](c9d7c6cd42))
* docs_path ([86d5939](86d5939d91))
* dont show certain fields based on permissions ([d3dfed9](d3dfed909e))
* handle return cancellation ([65a1c70](65a1c7086b))
* include total hours validation in depends on ([cbfc137](cbfc13728b))
* **manufacturing:** consider process loss qty while validating the work order ([7b3f746](7b3f74609a))
* no attribute error on LCV ([fe59ace](fe59ace285))
* no attribute error on subcontracting receipt ([2131c7a](2131c7aadb))
* overproduction % not considered when making WO from SO ([fb669eb](fb669eb6f4))
* **pos:** reapply set warehouse during cart update ([6869115](686911546f))
* **postgres:** compute current month sales without DATE_FORMAT ([49760e4](49760e4542))
* prevent UOM from updating incorrectly while scanning barcode ([9d5a0e5](9d5a0e56a0))
* qty with serial no count ([ae6b3af](ae6b3af013))
* remove already transferred batch ([f1e41f4](f1e41f4a4f))
* setting process loss qty causes fg item qty to be incorrect ([cb2d455](cb2d4550af))
* Show non-SLE vouchers with GL entries in Stock vs Account Value Comparison report ([e64ae9a](e64ae9a8a9))
* **stock:** resolve quantity issue when adding items via barcode scan ([ab482ca](ab482caac9))
* **transaction.js:** use flt instead of cint for plc_conversion_rate ([8ba4701](8ba470160d))
* validation message in stock reco row idx ([176096b](176096bc5b))
* valuation rate for non batchwise valuation ([768c131](768c131073))

### Features

* add list_view status for partial billing ([9b88275](9b88275312))
* add new 2025 Charts of Accounts for France ([9cc9fa5](9cc9fa59be))
* Adding Item name in update item dialog box ([1da8ed2](1da8ed202b))
* modify field properties ([e49add2](e49add20b7))
* remove old French chart of accounts with code as nex 2025 is provided ([3bdaab1](3bdaab149b))
* support for serial item ([c4c2d35](c4c2d35565))
* **timesheet:** handle partial billing in sales invoice ([332673f](332673f260))

### Performance Improvements

* prevent duplicate reposting for the same item ([3ac431b](3ac431bd50))
2026-01-20 16:47:59 +00:00
ruthra kumar
35b3045b72 Merge pull request #51911 from frappe/version-16-hotfix
chore: release v16
2026-01-20 22:16:27 +05:30
Mihir Kandoi
cf130ff865 Merge pull request #51936 from frappe/mergify/bp/version-16-hotfix/pr-51934
fix: validation message in stock reco row idx (backport #51934)
2026-01-20 21:05:40 +05:30
Mihir Kandoi
176096bc5b fix: validation message in stock reco row idx
(cherry picked from commit 3960c01798)
2026-01-20 15:17:53 +00:00
rohitwaghchaure
e854eafc0b Merge pull request #51933 from frappe/mergify/bp/version-16-hotfix/pr-51930
Revert "perf: prevent duplicate reposting for the same item" (backport #51930)
2026-01-20 20:05:41 +05:30
rohitwaghchaure
72cdddbeda Revert "perf: prevent duplicate reposting for the same item"
(cherry picked from commit 6e4b90055f)
2026-01-20 14:19:47 +00:00
mergify[bot]
c9d7c6cd42 fix: continuous raw material consumption with bom validation (backport #51914) (#51919)
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-01-20 12:56:27 +00:00
Diptanil Saha
8393c3c32d Merge pull request #51922 from frappe/mergify/bp/version-16-hotfix/pr-51887
fix(bank_account): `is_company_account` related validations (backport #51887)
2026-01-20 18:09:01 +05:30
Mihir Kandoi
5752d2e0a1 Merge pull request #51926 from frappe/mergify/bp/version-16-hotfix/pr-51909
fix: allow creation of DN in SI for items not having DN reference (backport #51909)
2026-01-20 18:03:35 +05:30
rohitwaghchaure
f25558b4d7 Merge pull request #51924 from frappe/mergify/bp/version-16-hotfix/pr-51920
perf: prevent duplicate reposting for the same item (backport #51920)
2026-01-20 18:01:21 +05:30
Mihir Kandoi
fef6df709d fix: allow creation of DN in SI for items not having DN reference
(cherry picked from commit b691de0147)
2026-01-20 12:14:59 +00:00
Rohit Waghchaure
3ac431bd50 perf: prevent duplicate reposting for the same item
(cherry picked from commit 7535931571)
2026-01-20 12:09:03 +00:00
diptanilsaha
5d5d208a49 fix(bank_account): validation for is_company_account
(cherry picked from commit 7532ab01d6)
2026-01-20 11:53:16 +00:00
ruthra kumar
9535f3d583 Merge pull request #51916 from frappe/mergify/bp/version-16-hotfix/pr-51671
fix(accounts): add missing accounting dimensions in advance taxes and charges (backport #51671)
2026-01-20 17:19:35 +05:30
Nikhil Kothari
673635e2c3 fix(accounts): add missing accounting dimensions in advance taxes and charges
(cherry picked from commit 22e9cb4cf4)

# Conflicts:
#	erpnext/patches.txt
2026-01-20 17:04:51 +05:30
Mihir Kandoi
37e241ba15 Merge pull request #51897 from frappe/mergify/bp/version-16-hotfix/pr-51895
fix: overproduction % not considered when making WO from SO (backport #51895)
2026-01-20 13:25:01 +05:30
Mihir Kandoi
fb669eb6f4 fix: overproduction % not considered when making WO from SO
(cherry picked from commit edba9efb5e)
2026-01-20 07:34:24 +00:00
ruthra kumar
232225d753 Merge pull request #51891 from frappe/mergify/bp/version-16-hotfix/pr-51561
fix: delete advance ledger entries  while reconciling payment entry (backport #51561)
2026-01-20 08:17:44 +05:30
ruthra kumar
80cbd851d1 Merge pull request #51893 from frappe/mergify/bp/version-16-hotfix/pr-51886
fix(accounts_controller): make return message translatable (backport #51886)
2026-01-20 08:13:12 +05:30
ruthra kumar
5474ac298d Merge pull request #51884 from frappe/mergify/bp/version-16-hotfix/pr-51830
fix(manufacturing): consider process loss qty while validating the work order (backport #51830)
2026-01-20 08:08:29 +05:30
ruthra kumar
7a9b10a05e Merge pull request #51861 from frappe/mergify/bp/version-16-hotfix/pr-51822
fix(budget variance report): check budget dimensions (backport #51822)
2026-01-20 07:56:49 +05:30
barredterra
621243c1d3 fix(accounts_controller): make return message translatable
(cherry picked from commit 0209f0fe29)
2026-01-20 02:26:48 +00:00
Lakshit Jain
efa5173964 Merge pull request #51561 from ljain112/fic-adv-ple-po
fix: delete advance ledger entries  while reconciling payment entry
(cherry picked from commit aea70c5ec1)
2026-01-20 02:21:34 +00:00
Sudharsanan11
7b3f74609a fix(manufacturing): consider process loss qty while validating the work order
(cherry picked from commit e6366e830c)
2026-01-19 16:18:36 +00:00
Mihir Kandoi
775f6d07b1 Merge pull request #51882 from frappe/mergify/bp/version-16-hotfix/pr-51880
fix: no attribute error on LCV (backport #51880)
2026-01-19 20:30:03 +05:30
Mihir Kandoi
e80ed14456 Merge pull request #51881 from frappe/mergify/bp/version-16-hotfix/pr-51879
fix: no attribute error on subcontracting receipt (backport #51879)
2026-01-19 20:15:55 +05:30
Mihir Kandoi
fe59ace285 fix: no attribute error on LCV
(cherry picked from commit ad11914fca)
2026-01-19 14:35:22 +00:00
Mihir Kandoi
2131c7aadb fix: no attribute error on subcontracting receipt
(cherry picked from commit fbac8b032e)
2026-01-19 14:30:08 +00:00
Diptanil Saha
b7284c7717 Merge pull request #51877 from frappe/mergify/bp/version-16-hotfix/pr-51595 2026-01-19 18:12:07 +05:30
Florian HENRY
6b9107c05c chore: re add older template
(cherry picked from commit b3efb3084f)
2026-01-19 12:23:31 +00:00
Florian HENRY
1ed8857d31 chore: fix bank account type
(cherry picked from commit 4fe1b214c1)
2026-01-19 12:23:31 +00:00
Florian HENRY
a195690bc8 chore: fix CASH acount type
(cherry picked from commit 6a876de838)
2026-01-19 12:23:31 +00:00
Florian HENRY
0c546c9e5a chore: fix bank acount type
(cherry picked from commit 765487a087)
2026-01-19 12:23:30 +00:00
Florian HENRY
11d9fd3dee chore: add Expenses Included In Valuation account
(cherry picked from commit c519cd0268)
2026-01-19 12:23:30 +00:00
Florian HENRY
3bdaab149b feat: remove old French chart of accounts with code as nex 2025 is provided
(cherry picked from commit bf430fce09)
2026-01-19 12:23:30 +00:00
Florian HENRY
ad4ac4e53c chore: Review PR #51595
(cherry picked from commit 6bdaeb983d)
2026-01-19 12:23:30 +00:00
Florian HENRY
9cc9fa59be feat: add new 2025 Charts of Accounts for France
(cherry picked from commit c81dee137f)
2026-01-19 12:23:30 +00:00
rohitwaghchaure
ab2aedd9a2 Merge pull request #51866 from frappe/mergify/bp/version-16-hotfix/pr-51769
fix(pos): reapply set warehouse during cart update (backport #51769)
2026-01-19 15:44:38 +05:30
ravibharathi656
686911546f fix(pos): reapply set warehouse during cart update
(cherry picked from commit 5a53c45321)
2026-01-19 10:07:50 +00:00
rohitwaghchaure
4f3078ab1a Merge pull request #51863 from frappe/mergify/bp/version-16-hotfix/pr-51690
feat: Adding Item name in update item dialog box (backport #51690)
2026-01-19 15:34:47 +05:30
rohitwaghchaure
a950adab79 Merge pull request #51864 from frappe/mergify/bp/version-16-hotfix/pr-51856
fix: qty with serial no count (backport #51856)
2026-01-19 15:34:23 +05:30
Rohit Waghchaure
ae6b3af013 fix: qty with serial no count
(cherry picked from commit 56e58ef301)
2026-01-19 10:00:35 +00:00
Nishka Gosalia
1da8ed202b feat: Adding Item name in update item dialog box
(cherry picked from commit e6133ad6d4)
2026-01-19 10:00:34 +00:00
ervishnucs
a3d860eabf fix(budget variance report): check budget dimensions
(cherry picked from commit cb696a8880)
2026-01-19 09:55:17 +00:00
rohitwaghchaure
adc9dc82ca Merge pull request #51848 from frappe/mergify/bp/version-16-hotfix/pr-51644
Refactor batch bundle get snos sle (backport #51644)
2026-01-19 15:15:20 +05:30
Rohit Waghchaure
79e04ea1fe chore: fix semantic commit message
(cherry picked from commit dfcbee9cc0)
2026-01-19 09:22:45 +00:00
krupalvora
0981b894dd refactor: Batch & Bundle get sle for snos - Added docstring
(cherry picked from commit 22dee50348)
2026-01-19 09:22:45 +00:00
krupalvora
380564a677 refactor: Batch & Bundle get Stock ledger for snos - added posting date in select
(cherry picked from commit 1ccc7365a7)
2026-01-19 09:22:45 +00:00
krupalvora
75deb180fb refactor: Batch & Bundle get Stock ledger for snos v2
(cherry picked from commit a074d81754)
2026-01-19 09:22:45 +00:00
krupalvora
839315752b refactor: Batch & Bundle get Stock ledger for snos
(cherry picked from commit c0149925ad)
2026-01-19 09:22:44 +00:00
Mihir Kandoi
2b4a547e23 Merge pull request #51847 from frappe/mergify/bp/version-16-hotfix/pr-51845
fix(bom): pass company warehouse filter (backport #51845)
2026-01-19 14:18:39 +05:30
22-poojashree
3c533d04f5 fix(bom): pass company warehouse filter
(cherry picked from commit 73bcfc4710)
2026-01-19 08:34:16 +00:00
ruthra kumar
1c214eec98 Merge pull request #51844 from frappe/mergify/bp/version-16-hotfix/pr-51826
fix: common_party_path (backport #51826)
2026-01-19 13:21:14 +05:30
mahsem
aeb2b60450 fix: common_party_path (#51826)
* fix: common_pary_path

* chore: remove non-existent anchor

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
(cherry picked from commit 0c0f43f7f7)
2026-01-19 07:50:30 +00:00
ruthra kumar
1b3f5e1c96 Merge pull request #51841 from frappe/mergify/bp/version-16-hotfix/pr-51513
fix: calculate net profit amount from root node accounts (backport #51513)
2026-01-19 13:05:56 +05:30
mergify[bot]
c830bf6fc7 fix: allow disassemble stock entry without work order (backport #51761) (#51836)
fix: allow disassemble stock entry without work order (#51761)

* fix: allow disassemble stock entry without work order

* fix: use existing functionality to load fg item

* chore: better dict update

(cherry picked from commit 83919119f8)

Co-authored-by: Smit Vora <smitvora203@gmail.com>
2026-01-19 12:54:06 +05:30
ruthra kumar
e2b95da24d Merge pull request #51839 from frappe/mergify/bp/version-16-hotfix/pr-51787
fix: recalculate taxes when item tax template changes after discount (backport #51787)
2026-01-19 12:50:34 +05:30
Navin-S-R
89b44c41a2 fix: calculate net profit amount from root node accounts
(cherry picked from commit c84986d00e)
2026-01-19 07:15:50 +00:00
Lakshit Jain
181141b56a Merge pull request #51787 from ljain112/fix-taxes-disc
fix: recalculate taxes when item tax template changes after discount
(cherry picked from commit f00aeec9b4)
2026-01-19 07:01:52 +00:00
ruthra kumar
5742a5d86a Merge pull request #51834 from frappe/mergify/bp/version-16-hotfix/pr-51742
fix: add other charges in total (backport #51742)
2026-01-19 11:34:22 +05:30
SowmyaArunachalam
68c8dfb24c fix: add other charges in total
(cherry picked from commit 9406c07c42)
2026-01-19 05:45:20 +00:00
Mihir Kandoi
7f54de7926 Merge pull request #51829 from frappe/mergify/bp/version-16-hotfix/pr-51824
fix: setting process loss qty causes fg item qty to be incorrect (backport #51824)
2026-01-18 22:51:58 +05:30
Mihir Kandoi
cb2d4550af fix: setting process loss qty causes fg item qty to be incorrect
(cherry picked from commit 56f5df6847)
2026-01-18 17:21:03 +00:00
Mihir Kandoi
c22d7e16d1 Merge pull request #51821 from frappe/mergify/bp/version-16-hotfix/pr-51817
fix: prevent UOM from updating incorrectly while scanning barcode (backport #51817)
2026-01-18 15:10:56 +05:30
Pandiyan5273
9d5a0e56a0 fix: prevent UOM from updating incorrectly while scanning barcode
(cherry picked from commit 30263b26a5)
2026-01-18 09:36:44 +00:00
mergify[bot]
fcea7603a8 fix: change docfield type to render html format (backport #51795) (#51804)
fix: change docfield type to render html format (#51795)

(cherry picked from commit 3fe5b5c80d)

Co-authored-by: Sowmya <106989392+SowmyaArunachalam@users.noreply.github.com>
2026-01-17 15:12:57 +05:30
ruthra kumar
42ebb7446a Merge pull request #51797 from frappe/mergify/bp/version-16-hotfix/pr-51555
fix(postgres): compute current month sales without DATE_FORMAT (backport #51555)
2026-01-16 17:15:48 +05:30
Matt Howard
49760e4542 fix(postgres): compute current month sales without DATE_FORMAT
(cherry picked from commit 64f391adf7)
2026-01-16 11:29:08 +00:00
Mihir Kandoi
6e1f4d84b6 Merge pull request #51794 from frappe/mergify/bp/version-16-hotfix/pr-51790
fix(stock): resolve quantity issue when adding items via barcode scan (backport #51790)
2026-01-16 16:20:37 +05:30
Pandiyan5273
ab482caac9 fix(stock): resolve quantity issue when adding items via barcode scan
(cherry picked from commit f959b2c59a)
2026-01-16 10:49:36 +00:00
Mihir Kandoi
d8506fb2c0 Merge pull request #51792 from frappe/mergify/bp/version-16-hotfix/pr-51791
fix: dont show certain fields based on permissions (backport #51791)
2026-01-16 16:02:37 +05:30
Mihir Kandoi
d3dfed909e fix: dont show certain fields based on permissions
(cherry picked from commit b3db2981de)
2026-01-16 10:31:39 +00:00
Mihir Kandoi
ac31c5ca19 Merge pull request #51789 from frappe/mergify/bp/version-16-hotfix/pr-51784
fix: add company filters for warehouse (backport #51784)
2026-01-16 15:16:05 +05:30
SowmyaArunachalam
ccab91b9ed fix: add company filters for warehouse
(cherry picked from commit f952b92d71)
2026-01-16 09:44:42 +00:00
Mihir Kandoi
541a8b135a Merge pull request #51785 from frappe/mergify/bp/version-16-hotfix/pr-51693 2026-01-16 14:09:49 +05:30
Mihir Kandoi
c0a30a5302 chore: typo
(cherry picked from commit 8fd1d6aec8)
2026-01-16 08:25:03 +00:00
Mihir Kandoi
accce1fe59 fix: bugs
(cherry picked from commit 19ae405742)
2026-01-16 08:25:03 +00:00
Mihir Kandoi
f04221417e test: add test case
(cherry picked from commit b567184dd7)
2026-01-16 08:25:03 +00:00
Mihir Kandoi
c4c2d35565 feat: support for serial item
(cherry picked from commit 3d0f649411)
2026-01-16 08:25:02 +00:00
Mihir Kandoi
f1e41f4a4f fix: remove already transferred batch
(cherry picked from commit b54067e04d)
2026-01-16 08:25:02 +00:00
Mihir Kandoi
d9326d80de refactor: sample retention stock entry
(cherry picked from commit 8d188cd32b)
2026-01-16 08:25:02 +00:00
ruthra kumar
5c93bf5798 fix: add below-0 column in ar/ap report (backport #51673) (#51780)
Merge pull request #51673 from Jatin3128/ar/ap-future-range-fix

fix: add below-0 column in ar/ap report
(cherry picked from commit c5b0787de6)

Co-authored-by: Jatin3128 <140256508+Jatin3128@users.noreply.github.com>
2026-01-16 12:46:26 +05:30
Jatin3128
f62ad83d6f Merge pull request #51673 from Jatin3128/ar/ap-future-range-fix
fix: add below-0 column in ar/ap report
(cherry picked from commit c5b0787de6)
2026-01-16 06:37:34 +00:00
Ankush Menat
876e2d4e6e build: Update Frappe dependency (#51779) 2026-01-16 11:24:43 +05:30
rohitwaghchaure
4977e06c50 Merge pull request #51772 from frappe/mergify/bp/version-16-hotfix/pr-51768
fix: Show non-SLE vouchers with GL entries in Stock vs Account Value … (backport #51768)
2026-01-15 19:26:45 +05:30
Rohit Waghchaure
e64ae9a8a9 fix: Show non-SLE vouchers with GL entries in Stock vs Account Value Comparison report
(cherry picked from commit 1db9ce205f)
2026-01-15 12:20:36 +00:00
rohitwaghchaure
6ddf4eee15 Merge pull request #51752 from frappe/mergify/bp/version-16-hotfix/pr-51729
fix: valuation rate for non batchwise valuation (backport #51729)
2026-01-15 17:00:32 +05:30
Mihir Kandoi
d27a09cb9f Merge pull request #51755 from frappe/mergify/bp/version-16-hotfix/pr-51753
fix: docs_path (backport #51753)
2026-01-14 21:31:31 +05:30
mahsem
86d5939d91 fix: docs_path
(cherry picked from commit 7ef8c81caf)
2026-01-14 16:00:03 +00:00
Rohit Waghchaure
768c131073 fix: valuation rate for non batchwise valuation
(cherry picked from commit b6312bca9c)
2026-01-14 14:06:50 +00:00
Diptanil Saha
8f77223057 Merge pull request #51748 from frappe/mergify/bp/version-16-hotfix/pr-51730
fix(transaction.js): use flt instead of cint for plc_conversion_rate (backport #51730)
2026-01-14 15:55:13 +05:30
diptanilsaha
8ba470160d fix(transaction.js): use flt instead of cint for plc_conversion_rate
(cherry picked from commit 8b445e04e5)
2026-01-14 10:22:21 +00:00
Mihir Kandoi
1a6264d831 Merge pull request #51737 from frappe/mergify/bp/version-16-hotfix/pr-51684 2026-01-14 11:37:00 +05:30
Mihir Kandoi
19a90c0980 Merge pull request #51736 from frappe/mergify/bp/version-16-hotfix/pr-51295 2026-01-14 11:18:57 +05:30
Pandiyan5273
d57fc49896 test(stock-entry): manufacture entry without work order
(cherry picked from commit 784e338be4)
2026-01-14 05:34:40 +00:00
l0gesh29
fe0431a6d0 chore: modify error msg
(cherry picked from commit f7004aa8c3)
2026-01-14 05:33:39 +00:00
l0gesh29
bfd6375508 fix: add validation for direct return
(cherry picked from commit 8379b39aaf)
2026-01-14 05:33:38 +00:00
l0gesh29
6dade11d8f fix: add validation for return against
(cherry picked from commit ff9b936634)
2026-01-14 05:33:38 +00:00
Logesh Periyasamy
ce421bb1d4 fix: add validation for amount and hours
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
(cherry picked from commit 43d1d685c6)
2026-01-14 05:33:38 +00:00
l0gesh29
84a749e3d0 fix: add validation for duplication
(cherry picked from commit cda8a97f4a)
2026-01-14 05:33:37 +00:00
l0gesh29
65a1c7086b fix: handle return cancellation
(cherry picked from commit 50f73a5072)
2026-01-14 05:33:37 +00:00
l0gesh29
a04da71182 test: add test for partial billing and return
(cherry picked from commit ae594e81f9)
2026-01-14 05:33:37 +00:00
l0gesh29
cbfc13728b fix: include total hours validation in depends on
(cherry picked from commit 57d34ab146)
2026-01-14 05:33:37 +00:00
l0gesh29
9b88275312 feat: add list_view status for partial billing
(cherry picked from commit ff0b37055b)
2026-01-14 05:33:36 +00:00
l0gesh29
332673f260 feat(timesheet): handle partial billing in sales invoice
(cherry picked from commit c87b5d3132)
2026-01-14 05:33:36 +00:00
l0gesh29
e49add20b7 feat: modify field properties
(cherry picked from commit 38a4642479)
2026-01-14 05:33:36 +00:00
Mihir Kandoi
c3b0633eda Merge pull request #51734 from frappe/mergify/bp/version-16-hotfix/pr-51733
fix: add uom js error (backport #51733)
2026-01-14 10:28:24 +05:30
Mihir Kandoi
a660ed061b fix: add uom js error
(cherry picked from commit 6d3f6d73d0)
2026-01-14 04:55:22 +00:00
Frappe PR Bot
0b1c0c36b5 chore(release): Bumped to Version 16.0.1
## [16.0.1](https://github.com/frappe/erpnext/compare/v16.0.0...v16.0.1) (2026-01-13)

### Bug Fixes

* **asset value adjustment:** skip cancelling revaluation journal entry if already cancelled (backport [#51666](https://github.com/frappe/erpnext/issues/51666)) ([#51716](https://github.com/frappe/erpnext/issues/51716)) ([4b85d51](4b85d51257))
* Redirect to Desktop after signup ([#51696](https://github.com/frappe/erpnext/issues/51696)) ([0363b01](0363b01ab7))
* Redirect to Desktop after signup ([#51696](https://github.com/frappe/erpnext/issues/51696)) ([#51697](https://github.com/frappe/erpnext/issues/51697)) ([294fb27](294fb27dc8))
* Redirect to Desktop after signup (backport [#51696](https://github.com/frappe/erpnext/issues/51696)) ([#51714](https://github.com/frappe/erpnext/issues/51714)) ([2118321](211832104c))
* stock module not opened when no warehouses ([3420e21](3420e21d45))
* **tds:** correct tax logic for customer ([50ce61a](50ce61ae02))
2026-01-13 16:20:34 +00:00
Mihir Kandoi
d316ef2306 Merge pull request #51728 from frappe/trigger-release-v16 2026-01-13 21:44:48 +05:30
Mihir Kandoi
af3a7903b3 chore: trigger release 2026-01-13 21:43:30 +05:30
Mihir Kandoi
a66e114a71 Merge pull request #51727 from frappe/change-release-branch 2026-01-13 21:34:26 +05:30
Mihir Kandoi
631b9d3bb0 chore: update release branch from version-13 to version-16 2026-01-13 20:32:48 +05:30
Mihir Kandoi
eb03781718 Merge pull request #51713 from frappe/version-16-hotfix 2026-01-13 20:17:06 +05:30
rohitwaghchaure
eb7cebac91 Merge pull request #51720 from frappe/mergify/bp/version-16-hotfix/pr-51719
fix: stock module not opened when no warehouses (backport #51719)
2026-01-13 17:38:25 +05:30
Rohit Waghchaure
3420e21d45 fix: stock module not opened when no warehouses
(cherry picked from commit 9de3b07223)
2026-01-13 11:49:56 +00:00
Mihir Kandoi
211832104c fix: Redirect to Desktop after signup (backport #51696) (#51714)
Co-authored-by: Nabin Hait <nabinhait@gmail.com>
fix: Redirect to Desktop after signup (#51696)
2026-01-13 16:07:20 +05:30
mergify[bot]
4b85d51257 fix(asset value adjustment): skip cancelling revaluation journal entry if already cancelled (backport #51666) (#51716)
Co-authored-by: Navin-S-R <navin@aerele.in>
2026-01-13 16:05:14 +05:30
Nabin Hait
0363b01ab7 fix: Redirect to Desktop after signup (#51696)
(cherry picked from commit 3bc58fb46f)
2026-01-13 09:46:21 +00:00
Mihir Kandoi
fc517f7fa2 Merge pull request #51707 from mihir-kandoi/ci-patch-test-2 2026-01-13 15:11:37 +05:30
ruthra kumar
18451b69e6 Merge pull request #51703 from frappe/mergify/bp/version-16-hotfix/pr-51412
fix(tds): correct tax logic for customer (backport #51412)
2026-01-13 11:41:28 +05:30
ljain112
50ce61ae02 fix(tds): correct tax logic for customer
(cherry picked from commit 86b0f67dbc)
2026-01-13 05:37:32 +00:00
Nabin Hait
294fb27dc8 fix: Redirect to Desktop after signup (#51696) (#51697) 2026-01-12 19:22:24 +05:30
Rohit Waghchaure
c3fdb191b9 Merge branch 'develop' into version-16 2026-01-12 16:20:48 +05:30
Rohit Waghchaure
93db2ebd6f Merge branch 'develop' into version-16 2026-01-12 15:07:27 +05:30
rohitwaghchaure
2925f9a04e chore: weekly release for v16 2026-01-12 14:29:14 +05:30
rohitwaghchaure
c7bf103c0c chore: fix version 2026-01-12 14:04:45 +05:30
rohitwaghchaure
6dead8fd85 chore: fix version 2026-01-12 12:19:58 +05:30
71 changed files with 6772 additions and 234 deletions

View File

@@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: false
matrix:
version: ["14", "15"]
version: ["14", "15", "16"]
steps:
- uses: octokit/request-action@v2.x

View File

@@ -113,8 +113,8 @@ jobs:
jq 'del(.install_apps)' ~/frappe-bench/sites/test_site/site_config.json > tmp.json
mv tmp.json ~/frappe-bench/sites/test_site/site_config.json
wget https://erpnext.com/files/v13-erpnext.sql.gz
bench --site test_site --force restore ~/frappe-bench/v13-erpnext.sql.gz
wget https://frappe.io/files/erpnext-v14.sql.gz
bench --site test_site --force restore ~/frappe-bench/erpnext-v14.sql.gz
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
@@ -142,7 +142,6 @@ jobs:
bench --site test_site migrate
}
update_to_version 14 3.11
update_to_version 15 3.13
echo "Updating to latest version"

View File

@@ -2,7 +2,7 @@ name: Generate Semantic Release
on:
push:
branches:
- version-13
- version-16
permissions:
contents: read

View File

@@ -1,5 +1,5 @@
{
"branches": ["version-13"],
"branches": ["version-16"],
"plugins": [
"@semantic-release/commit-analyzer", {
"preset": "angular",
@@ -21,4 +21,4 @@
],
"@semantic-release/github"
]
}
}

View File

@@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document
from frappe.utils.user import is_website_user
__version__ = "16.0.0-dev"
__version__ = "16.1.0"
def get_default_company(user=None):

View File

@@ -281,7 +281,7 @@
},
{
"default": "0",
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
"description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\">Common Party</a>",
"fieldname": "enable_common_party_accounting",
"fieldtype": "Check",
"label": "Enable Common Party Accounting"

View File

@@ -42,8 +42,4 @@ frappe.ui.form.on("Bank Account", {
});
}
},
is_company_account: function (frm) {
frm.set_df_property("account", "reqd", frm.doc.is_company_account);
},
});

View File

@@ -52,6 +52,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company Account",
"mandatory_depends_on": "is_company_account",
"options": "Account"
},
{
@@ -98,6 +99,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"mandatory_depends_on": "is_company_account",
"options": "Company"
},
{
@@ -252,7 +254,7 @@
"link_fieldname": "default_bank_account"
}
],
"modified": "2025-08-29 12:32:01.081687",
"modified": "2026-01-20 00:46:16.633364",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",

View File

@@ -51,25 +51,29 @@ class BankAccount(Document):
delete_contact_and_address("Bank Account", self.name)
def validate(self):
self.validate_company()
self.validate_account()
self.validate_is_company_account()
self.update_default_bank_account()
def validate_account(self):
if self.account:
if accounts := frappe.db.get_all(
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
):
frappe.throw(
_("'{0}' account is already used by {1}. Use another account.").format(
frappe.bold(self.account),
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
)
)
def validate_is_company_account(self):
if self.is_company_account:
if not self.company:
frappe.throw(_("Company is mandatory for company account"))
def validate_company(self):
if self.is_company_account and not self.company:
frappe.throw(_("Company is mandatory for company account"))
if not self.account:
frappe.throw(_("Company Account is mandatory"))
self.validate_account()
def validate_account(self):
if accounts := frappe.db.get_all(
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
):
frappe.throw(
_("'{0}' account is already used by {1}. Use another account.").format(
frappe.bold(self.account),
frappe.bold(comma_and([get_link_to_form(self.doctype, x[0]) for x in accounts])),
)
)
def update_default_bank_account(self):
if self.is_default and not self.disabled:

View File

@@ -184,6 +184,9 @@ class JournalEntry(AccountsController):
else:
return self._submit()
def before_cancel(self):
self.has_asset_adjustment_entry()
def cancel(self):
if len(self.accounts) > 100:
queue_submission(self, "_cancel")
@@ -554,12 +557,27 @@ class JournalEntry(AccountsController):
)
frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "")
def unlink_asset_adjustment_entry(self):
frappe.db.sql(
""" update `tabAsset Value Adjustment`
set journal_entry = null where journal_entry = %s""",
self.name,
def has_asset_adjustment_entry(self):
if self.flags.get("via_asset_value_adjustment"):
return
asset_value_adjustment = frappe.db.get_value(
"Asset Value Adjustment", {"docstatus": 1, "journal_entry": self.name}, "name"
)
if asset_value_adjustment:
frappe.throw(
_(
"Cannot cancel this document as it is linked with the submitted Asset Value Adjustment <b>{0}</b>. Please cancel the Asset Value Adjustment to continue."
).format(frappe.utils.get_link_to_form("Asset Value Adjustment", asset_value_adjustment))
)
def unlink_asset_adjustment_entry(self):
AssetValueAdjustment = frappe.qb.DocType("Asset Value Adjustment")
(
frappe.qb.update(AssetValueAdjustment)
.set(AssetValueAdjustment.journal_entry, None)
.where(AssetValueAdjustment.journal_entry == self.name)
).run()
def validate_party(self):
for d in self.get("accounts"):

View File

@@ -115,15 +115,9 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
}
if (cint(doc.update_stock) != 1) {
// show Make Delivery Note button only if Sales Invoice is not created from Delivery Note
var from_delivery_note = false;
from_delivery_note = this.frm.doc.items.some(function (item) {
return item.delivery_note ? true : false;
});
if (!from_delivery_note && !is_delivered_by_supplier) {
if (!is_delivered_by_supplier) {
this.frm.add_custom_button(
__("Delivery"),
__("Delivery Note"),
this.frm.cscript["Make Delivery Note"],
__("Create")
);

View File

@@ -778,8 +778,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.total_billing_amount > 0",
"depends_on": "eval:!doc.is_return",
"collapsible_depends_on": "eval:doc.total_billing_amount > 0 || doc.total_billing_hours > 0",
"fieldname": "time_sheet_list",
"fieldtype": "Section Break",
"hide_border": 1,
@@ -793,7 +792,6 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Time Sheets",
"no_copy": 1,
"options": "Sales Invoice Timesheet",
"print_hide": 1
},
@@ -2092,7 +2090,7 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:(!doc.is_return && doc.total_billing_amount > 0)",
"depends_on": "eval:doc.total_billing_amount > 0 || doc.total_billing_hours > 0",
"fieldname": "section_break_104",
"fieldtype": "Section Break"
},
@@ -2306,7 +2304,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2025-10-09 14:48:59.472826",
"modified": "2025-12-24 18:29:50.242618",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -352,10 +352,22 @@ class SalesInvoice(SellingController):
self.is_opening = "No"
self.set_against_income_account()
self.validate_time_sheets_are_submitted()
if self.is_return and not self.return_against and self.timesheets:
frappe.throw(_("Direct return is not allowed for Timesheet."))
if not self.is_return:
self.validate_time_sheets_are_submitted()
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
if self.is_return:
self.timesheets = []
if self.is_return and self.return_against:
for row in self.timesheets:
if row.billing_hours:
row.billing_hours = -abs(row.billing_hours)
if row.billing_amount:
row.billing_amount = -abs(row.billing_amount)
self.update_packing_list()
self.set_billing_hours_and_amount()
self.update_timesheet_billing_for_project()
@@ -484,7 +496,7 @@ class SalesInvoice(SellingController):
if cint(self.is_pos) != 1 and not self.is_return:
self.update_against_document_in_jv()
self.update_time_sheet(self.name)
self.update_time_sheet(None if (self.is_return and self.return_against) else self.name)
if frappe.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction":
update_company_current_month_sales(self.company)
@@ -564,7 +576,7 @@ class SalesInvoice(SellingController):
self.check_if_consolidated_invoice()
super().before_cancel()
self.update_time_sheet(None)
self.update_time_sheet(self.return_against if (self.is_return and self.return_against) else None)
def on_cancel(self):
check_if_return_invoice_linked_with_payment_entry(self)
@@ -804,8 +816,20 @@ class SalesInvoice(SellingController):
for data in timesheet.time_logs:
if (
(self.project and args.timesheet_detail == data.name)
or (not self.project and not data.sales_invoice)
or (not sales_invoice and data.sales_invoice == self.name)
or (not self.project and not data.sales_invoice and args.timesheet_detail == data.name)
or (
not sales_invoice
and data.sales_invoice == self.name
and args.timesheet_detail == data.name
)
or (
self.is_return
and self.return_against
and data.sales_invoice
and data.sales_invoice == self.return_against
and not sales_invoice
and args.timesheet_detail == data.name
)
):
data.sales_invoice = sales_invoice
@@ -845,11 +869,26 @@ class SalesInvoice(SellingController):
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
def validate_time_sheets_are_submitted(self):
# Note: This validation is skipped for return invoices
# to allow returns to reference already-billed timesheet details
for data in self.timesheets:
# Handle invoice duplication
if data.time_sheet and data.timesheet_detail:
if sales_invoice := frappe.db.get_value(
"Timesheet Detail", data.timesheet_detail, "sales_invoice"
):
frappe.throw(
_("Row {0}: Sales Invoice {1} is already created for {2}").format(
data.idx, frappe.bold(sales_invoice), frappe.bold(data.time_sheet)
)
)
if data.time_sheet:
status = frappe.db.get_value("Timesheet", data.time_sheet, "status")
if status not in ["Submitted", "Payslip"]:
frappe.throw(_("Timesheet {0} is already completed or cancelled").format(data.time_sheet))
if status not in ["Submitted", "Payslip", "Partially Billed"]:
frappe.throw(
_("Timesheet {0} cannot be invoiced in its current state").format(data.time_sheet)
)
def set_pos_fields(self, for_validate=False):
"""Set retail related fields from POS Profiles"""
@@ -1283,7 +1322,12 @@ class SalesInvoice(SellingController):
timesheet.billing_amount = ts_doc.total_billable_amount
def update_timesheet_billing_for_project(self):
if not self.timesheets and self.project and self.is_auto_fetch_timesheet_enabled():
if (
not self.is_return
and not self.timesheets
and self.project
and self.is_auto_fetch_timesheet_enabled()
):
self.add_timesheet_data()
else:
self.calculate_billing_amount_for_timesheet()
@@ -2378,7 +2422,9 @@ def make_delivery_note(source_name, target_doc=None):
"cost_center": "cost_center",
},
"postprocess": update_item,
"condition": lambda doc: doc.delivered_by_supplier != 1 and not doc.scio_detail,
"condition": lambda doc: doc.delivered_by_supplier != 1
and not doc.scio_detail
and not doc.dn_detail,
},
"Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "reset_value": True},
"Sales Team": {

View File

@@ -2951,6 +2951,60 @@ class TestSalesInvoice(ERPNextTestSuite):
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
self.assertEqual(sales_invoice.items[0].item_tax_rate, item_tax_map)
def test_item_tax_template_change_with_grand_total_discount(self):
"""
Test that when item tax template changes due to discount on Grand Total,
the tax calculations are consistent.
"""
item = create_item("Test Item With Multiple Tax Templates")
item.set("taxes", [])
item.append(
"taxes",
{
"item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"minimum_net_rate": 0,
"maximum_net_rate": 500,
},
)
item.append(
"taxes",
{
"item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"minimum_net_rate": 501,
"maximum_net_rate": 1000,
},
)
item.save()
si = create_sales_invoice(item=item.name, rate=700, do_not_save=True)
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Excise Duty - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Excise Duty",
"rate": 0,
},
)
si.insert()
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
si.apply_discount_on = "Grand Total"
si.discount_amount = 300
si.save()
# Verify template changed to 10%
self.assertEqual(si.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
self.assertEqual(si.taxes[0].tax_amount, 70) # 10% of 700
self.assertEqual(si.grand_total, 470) # 700 + 70 - 300
si.submit()
@IntegrationTestCase.change_settings("Selling Settings", {"enable_discount_accounting": 1})
def test_sales_invoice_with_discount_accounting_enabled(self):
discount_account = create_account(

View File

@@ -52,7 +52,6 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Timesheet Detail",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
@@ -117,15 +116,16 @@
],
"istable": 1,
"links": [],
"modified": "2024-03-27 13:10:36.562795",
"modified": "2025-12-23 13:54:17.677187",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Timesheet",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -415,7 +415,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
"cost_center": "Main - _TC",
"tax_amount": 500,
"description": "Test",
"add_deduct_tax": "Add",
},
)
pi.save()
@@ -506,7 +505,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
"cost_center": "Main - _TC",
"tax_amount": 200,
"description": "Test Gross Tax",
"add_deduct_tax": "Add",
},
)
si.save()
@@ -541,10 +539,10 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
"cost_center": "Main - _TC",
"tax_amount": 400,
"description": "Test Gross Tax",
"add_deduct_tax": "Add",
},
)
si.save()
si.reload()
si.submit()
invoices.append(si)
# For amount before threshold (first 8000 + VAT): TCS entry with amount zero
@@ -594,7 +592,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
"cost_center": "Main - _TC",
"tax_amount": 500,
"description": "VAT added to test TDS calculation on gross amount",
"add_deduct_tax": "Add",
},
)
si.save()
@@ -1024,7 +1021,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
"cost_center": "Main - _TC",
"tax_amount": 1000,
"description": "VAT added to test TDS calculation on gross amount",
"add_deduct_tax": "Add",
},
)
pi.save()
@@ -1162,7 +1158,6 @@ class TestTaxWithholdingCategory(IntegrationTestCase):
"cost_center": "Main - _TC",
"tax_amount": 8000,
"description": "Test",
"add_deduct_tax": "Add",
},
)

View File

@@ -708,6 +708,10 @@ class TaxWithholdingController:
existing_taxes = {row.account_head: row for row in self.doc.taxes if row.is_tax_withholding_account}
precision = self.doc.precision("tax_amount", "taxes")
conversion_rate = self.get_conversion_rate()
add_deduct_tax = "Deduct"
if self.party_type == "Customer":
add_deduct_tax = "Add"
for account_head, base_amount in account_amount_map.items():
tax_amount = flt(base_amount / conversion_rate, precision)
@@ -724,6 +728,7 @@ class TaxWithholdingController:
tax_row = self._create_tax_row(account_head, tax_amount)
for_update = False
tax_row.add_deduct_tax = add_deduct_tax
# Set item-wise tax breakup for this tax row
self._set_item_wise_tax_for_tds(
tax_row, account_head, category_withholding_map, for_update=for_update
@@ -743,7 +748,6 @@ class TaxWithholdingController:
"account_head": account_head,
"description": account_head,
"cost_center": cost_center,
"add_deduct_tax": "Deduct",
"tax_amount": tax_amount,
"dont_recompute_tax": 1,
},
@@ -807,12 +811,14 @@ class TaxWithholdingController:
else:
item_tax_amount = 0
multiplier = -1 if tax_row.add_deduct_tax == "Deduct" else 1
self.doc._item_wise_tax_details.append(
frappe._dict(
item=item,
tax=tax_row,
rate=category.tax_rate,
amount=item_tax_amount * -1, # Negative because it's a deduction
amount=item_tax_amount * multiplier,
taxable_amount=item_base_taxable,
)
)

View File

@@ -877,11 +877,15 @@ class ReceivablePayableReport:
else:
entry_date = row.posting_date
row.range0 = 0.0
self.get_ageing_data(entry_date, row)
# ageing buckets should not have amounts if due date is not reached
if getdate(entry_date) > getdate(self.age_as_on):
row.range0 = row.outstanding
[setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
row.total_due = 0
return
row.total_due = sum(row[f"range{i}"] for i in self.range_numbers)
@@ -1281,6 +1285,8 @@ class ReceivablePayableReport:
ranges = [*self.ranges, _("Above")]
prev_range_value = 0
self.add_column(label=_("<0"), fieldname="range0", fieldtype="Currency")
self.ageing_column_labels.append(_("<0"))
for idx, curr_range_value in enumerate(ranges):
label = f"{prev_range_value}-{curr_range_value}"
self.add_column(label=label, fieldname="range" + str(idx + 1))
@@ -1296,7 +1302,9 @@ class ReceivablePayableReport:
for row in self.data:
row = frappe._dict(row)
if not cint(row.bold):
values = [flt(row.get(f"range{i}", None), precision) for i in self.range_numbers]
values = [flt(row.get("range0", 0), precision)] + [
flt(row.get(f"range{i}", 0), precision) for i in self.range_numbers
]
rows.append({"values": values})
self.chart = {

View File

@@ -18,6 +18,8 @@ def execute(filters=None):
dimensions = filters.get("budget_against_filter")
else:
dimensions = get_budget_dimensions(filters)
if not dimensions:
return columns, [], None, None
budget_records = get_budget_records(filters, dimensions)
budget_map = build_budget_map(budget_records, filters)

View File

@@ -219,13 +219,18 @@ def get_net_profit(
has_value = False
gross_income_roots = [row for row in (gross_income or []) if not flt(row.get("indent"))]
non_gross_income_roots = [row for row in (non_gross_income or []) if not flt(row.get("indent"))]
gross_expense_roots = [row for row in (gross_expense or []) if not flt(row.get("indent"))]
non_gross_expense_roots = [row for row in (non_gross_expense or []) if not flt(row.get("indent"))]
for period in period_list:
key = period if consolidated else period.key
gross_income_for_period = flt(gross_income[0].get(key, 0)) if gross_income else 0
non_gross_income_for_period = flt(non_gross_income[0].get(key, 0)) if non_gross_income else 0
gross_expense_for_period = flt(gross_expense[0].get(key, 0)) if gross_expense else 0
non_gross_expense_for_period = flt(non_gross_expense[0].get(key, 0)) if non_gross_expense else 0
gross_income_for_period = sum(flt(row.get(key, 0)) for row in gross_income_roots)
non_gross_income_for_period = sum(flt(row.get(key, 0)) for row in non_gross_income_roots)
gross_expense_for_period = sum(flt(row.get(key, 0)) for row in gross_expense_roots)
non_gross_expense_for_period = sum(flt(row.get(key, 0)) for row in non_gross_expense_roots)
total_income = gross_income_for_period + non_gross_income_for_period
total_expense = gross_expense_for_period + non_gross_expense_for_period

View File

@@ -105,7 +105,7 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
{
"total_tax": total_tax,
"total_other_charges": total_other_charges,
"total": d.base_net_amount + total_tax,
"total": d.base_net_amount + total_tax + total_other_charges,
"currency": company_currency,
}
)

View File

@@ -547,6 +547,7 @@ def reconcile_against_document(
doc.make_advance_gl_entries(entry=row)
else:
_delete_pl_entries(voucher_type, voucher_no)
_delete_adv_pl_entries(voucher_type, voucher_no)
gl_map = doc.build_gl_map()
# Make sure there is no overallocation
from erpnext.accounts.general_ledger import process_debit_credit_difference

View File

@@ -74,7 +74,7 @@ class AssetValueAdjustment(Document):
)
def on_cancel(self):
frappe.get_doc("Journal Entry", self.journal_entry).cancel()
self.cancel_asset_revaluation_entry()
self.update_asset()
add_asset_activity(
self.asset,
@@ -167,6 +167,17 @@ class AssetValueAdjustment(Document):
if dimension.get("mandatory_for_pl"):
debit_entry.update({dimension["fieldname"]: dimension_value})
def cancel_asset_revaluation_entry(self):
if not self.journal_entry:
return
revaluation_entry = frappe.get_doc("Journal Entry", self.journal_entry)
if revaluation_entry.docstatus == 1:
# Ignore permissions to match Journal Entry submission behavior
revaluation_entry.flags.ignore_permissions = True
revaluation_entry.flags.via_asset_value_adjustment = True
revaluation_entry.cancel()
def update_asset(self):
asset = self.update_asset_value_after_depreciation()
note = self.get_adjustment_note()

View File

@@ -1330,6 +1330,55 @@ class TestPurchaseOrder(IntegrationTestCase):
pi = make_pi_from_po(po.name)
self.assertEqual(pi.items[0].qty, 50)
def test_multiple_advances_against_purchase_order_are_allocated_across_partial_purchase_invoices(self):
# step - 1: create PO
po = create_purchase_order(qty=10, rate=10)
# step - 2: create first partial advance payment
pe1 = get_payment_entry("Purchase Order", po.name, bank_account="_Test Bank - _TC")
pe1.reference_no = "1"
pe1.reference_date = nowdate()
pe1.paid_amount = 50
pe1.references[0].allocated_amount = 50
pe1.save(ignore_permissions=True).submit()
# check first advance paid against PO
po.reload()
self.assertEqual(po.advance_paid, 50)
# step - 3: create first PI for partial qty and allocate first advance
pi_1 = make_pi_from_po(po.name)
pi_1.update_stock = 1
pi_1.allocate_advances_automatically = 1
pi_1.items[0].qty = 5
pi_1.save(ignore_permissions=True).submit()
# step - 4: create second advance payment for remaining
pe2 = get_payment_entry("Purchase Order", po.name, bank_account="_Test Bank - _TC")
pe2.reference_no = "2"
pe2.reference_date = nowdate()
pe2.paid_amount = 50
pe2.references[0].allocated_amount = 50
pe2.save(ignore_permissions=True).submit()
# check second advance paid against PO
po.reload()
self.assertEqual(po.advance_paid, 100)
# step - 5: create second PI for remaining qty and allocate second advance
pi_2 = make_pi_from_po(po.name)
pi_2.update_stock = 1
pi_2.allocate_advances_automatically = 1
pi_2.save(ignore_permissions=True).submit()
# check PO and PI status
po.reload()
pi_1.reload()
pi_2.reload()
self.assertEqual(pi_1.status, "Paid")
self.assertEqual(pi_2.status, "Paid")
self.assertEqual(po.status, "Completed")
def create_po_for_sc_testing():
from erpnext.controllers.tests.test_subcontracting_controller import (

View File

@@ -383,7 +383,7 @@
},
{
"fieldname": "primary_address",
"fieldtype": "Text",
"fieldtype": "Text Editor",
"label": "Primary Address",
"read_only": 1
},
@@ -500,7 +500,7 @@
"link_fieldname": "party"
}
],
"modified": "2025-06-29 05:30:50.398653",
"modified": "2026-01-16 15:56:31.139206",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@@ -62,7 +62,7 @@ class Supplier(TransactionBase):
portal_users: DF.Table[PortalUser]
prevent_pos: DF.Check
prevent_rfqs: DF.Check
primary_address: DF.Text | None
primary_address: DF.TextEditor | None
release_date: DF.Date | None
represents_company: DF.Link | None
supplier_details: DF.Text | None

View File

@@ -16,6 +16,14 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
return !doc.qty && me.frm.doc.has_unit_price_items ? "yellow" : "";
});
this.frm.set_query("warehouse", "items", (doc, cdt, cdn) => {
return {
filters: {
company: doc.company,
is_group: 0,
},
};
});
super.setup();
}

View File

@@ -187,9 +187,8 @@ class AccountsController(TransactionBase):
msg = ""
if self.get("update_outstanding_for_self"):
msg = (
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, "
"uncheck '{2}' checkbox. <br><br>Or"
msg = _(
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck the '{2}' checkbox."
).format(
frappe.bold(document_type),
get_link_to_form(self.doctype, self.get("return_against")),
@@ -200,8 +199,8 @@ class AccountsController(TransactionBase):
abs(flt(self.rounded_total) or flt(self.grand_total)) > flt(against_voucher_outstanding)
):
self.update_outstanding_for_self = 1
msg = (
"The outstanding amount {} in {} is lesser than {}. Updating the outstanding to this invoice. <br><br>And"
msg = _(
"The outstanding amount {0} in {1} is lesser than {2}. Updating the outstanding to this invoice."
).format(
against_voucher_outstanding,
get_link_to_form(self.doctype, self.get("return_against")),
@@ -209,11 +208,11 @@ class AccountsController(TransactionBase):
)
if msg:
msg += " you can use {} tool to reconcile against {} later.".format(
msg += "<br><br>" + _("You can use {0} to reconcile against {1} later.").format(
get_link_to_form("Payment Reconciliation"),
get_link_to_form(self.doctype, self.get("return_against")),
)
frappe.msgprint(_(msg))
frappe.msgprint(msg)
def validate(self):
if not self.get("is_return") and not self.get("is_debit_note"):

View File

@@ -610,7 +610,9 @@ class SubcontractingController(StockController):
and self.doctype != "Subcontracting Inward Order"
):
row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse
elif frappe.get_cached_value("Item", row.rm_item_code, "is_customer_provided_item"):
elif frappe.get_cached_value("Item", row.rm_item_code, "is_customer_provided_item") and self.get(
"customer_warehouse"
):
row.warehouse = self.customer_warehouse
def __set_alternative_item(self, bom_item):

View File

@@ -39,17 +39,23 @@ class calculate_taxes_and_totals:
items = list(filter(lambda item: not item.get("is_alternative"), self.doc.get("items")))
return items
def calculate(self):
def calculate(self, ignore_tax_template_validation=False):
if not len(self.doc.items):
return
self.discount_amount_applied = False
self.need_recomputation = False
self.ignore_tax_template_validation = ignore_tax_template_validation
self._calculate()
if self.doc.meta.get_field("discount_amount"):
self.set_discount_amount()
self.apply_discount_amount()
if not ignore_tax_template_validation and self.need_recomputation:
return self.calculate(ignore_tax_template_validation=True)
# Update grand total as per cash and non trade discount
if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"):
self.doc.grand_total -= self.doc.discount_amount
@@ -79,6 +85,9 @@ class calculate_taxes_and_totals:
self.calculate_total_net_weight()
def validate_item_tax_template(self):
if self.ignore_tax_template_validation:
return
if self.doc.get("is_return") and self.doc.get("return_against"):
return
@@ -122,6 +131,10 @@ class calculate_taxes_and_totals:
)
)
# For correct tax_amount calculation re-computation is required
if self.discount_amount_applied and self.doc.apply_discount_on == "Grand Total":
self.need_recomputation = True
def update_item_tax_map(self):
for item in self.doc.items:
item.item_tax_rate = get_item_tax_map(

View File

@@ -8,7 +8,7 @@ app_email = "hello@frappe.io"
app_license = "GNU General Public License (v3)"
source_link = "https://github.com/frappe/erpnext"
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
app_home = "/app/home"
app_home = "/desk"
add_to_apps_screen = [
{
@@ -569,6 +569,7 @@ accounting_dimension_doctypes = [
"Payment Request",
"Asset Movement Item",
"Asset Depreciation Schedule",
"Advance Taxes and Charges",
]
get_matching_queries = (

View File

@@ -9,7 +9,7 @@ from erpnext.setup.doctype.employee.employee import get_holiday_list_for_employe
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.utilities.transaction_base import TransactionBase, delete_events
#
class MaintenanceSchedule(TransactionBase):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.

View File

@@ -92,6 +92,10 @@ frappe.ui.form.on("BOM", {
};
});
frm.events.set_company_filters(frm, "project");
frm.events.set_company_filters(frm, "default_source_warehouse");
frm.events.set_company_filters(frm, "default_target_warehouse");
frm.trigger("toggle_fields_for_semi_finished_goods");
},
@@ -104,6 +108,16 @@ frappe.ui.form.on("BOM", {
}
},
set_company_filters: function (frm, fieldname) {
frm.set_query(fieldname, () => {
return {
filters: {
company: frm.doc.company,
},
};
});
},
track_semi_finished_goods(frm) {
frm.trigger("toggle_fields_for_semi_finished_goods");
},

View File

@@ -829,7 +829,7 @@ erpnext.work_order = {
}
}
if (counter > 0) {
var consumption_btn = frm.add_custom_button(
frm.add_custom_button(
__("Material Consumption"),
function () {
const backflush_raw_materials_based_on =

View File

@@ -502,8 +502,8 @@ class WorkOrder(Document):
def validate_work_order_against_so(self):
# already ordered qty
ordered_qty_against_so = frappe.db.sql(
"""select sum(qty) from `tabWork Order`
where production_item = %s and sales_order = %s and docstatus < 2 and status != 'Closed' and name != %s""",
"""select sum(qty - process_loss_qty) from `tabWork Order`
where production_item = %s and sales_order = %s and docstatus = 1 and status != 'Closed' and name != %s""",
(self.production_item, self.sales_order, self.name),
)[0][0]
@@ -512,13 +512,13 @@ class WorkOrder(Document):
# get qty from Sales Order Item table
so_item_qty = frappe.db.sql(
"""select sum(stock_qty) from `tabSales Order Item`
where parent = %s and item_code = %s""",
where parent = %s and item_code = %s and docstatus = 1""",
(self.sales_order, self.production_item),
)[0][0]
# get qty from Packing Item table
dnpi_qty = frappe.db.sql(
"""select sum(qty) from `tabPacked Item`
where parent = %s and parenttype = 'Sales Order' and item_code = %s""",
where parent = %s and parenttype = 'Sales Order' and item_code = %s and docstatus = 1""",
(self.sales_order, self.production_item),
)[0][0]
# total qty in SO
@@ -530,8 +530,10 @@ class WorkOrder(Document):
if total_qty > so_qty + (allowance_percentage / 100 * so_qty):
frappe.throw(
_("Cannot produce more Item {0} than Sales Order quantity {1}").format(
self.production_item, so_qty
_("Cannot produce more Item {0} than Sales Order quantity {1} {2}").format(
get_link_to_form("Item", self.production_item),
frappe.bold(so_qty),
frappe.bold(frappe.get_value("Item", self.production_item, "stock_uom")),
),
OverProductionError,
)

View File

@@ -458,3 +458,4 @@ erpnext.patches.v16_0.update_corrected_cancelled_status
erpnext.patches.v16_0.fix_barcode_typo
erpnext.patches.v16_0.set_post_change_gl_entries_on_pos_settings
execute:frappe.delete_doc_if_exists("Workspace Sidebar", "Opening & Closing")
erpnext.patches.v15_0.create_accounting_dimensions_in_advance_taxes_and_charges

View File

@@ -0,0 +1,7 @@
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
create_accounting_dimensions_for_doctype,
)
def execute():
create_accounting_dimensions_for_doctype(doctype="Advance Taxes and Charges")

View File

@@ -7,6 +7,7 @@ import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils import add_to_date, now_datetime, nowdate
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.projects.doctype.task.test_task import create_task
from erpnext.projects.doctype.timesheet.timesheet import OverlapError, make_sales_invoice
@@ -272,6 +273,60 @@ class TestTimesheet(ERPNextTestSuite):
ts.calculate_percentage_billed()
self.assertEqual(ts.per_billed, 100)
def test_partial_billing_and_return(self):
"""
Test Timesheet status transitions during partial billing, full billing,
sales return, and return cancellation.
Scenario:
1. Create a Timesheet with two billable time logs.
2. Create a Sales Invoice billing only one time log → Timesheet becomes Partially Billed.
3. Create another Sales Invoice billing the remaining time log → Timesheet becomes Billed.
4. Create a Sales Return against the second invoice → Timesheet reverts to Partially Billed.
5. Cancel the Sales Return → Timesheet returns to Billed status.
This test ensures Timesheet status is recalculated correctly
across billing and return lifecycle events.
"""
emp = make_employee("test_employee_6@salary.com")
timesheet = make_timesheet(emp, simulate=True, is_billable=1, do_not_submit=True)
timesheet_detail = timesheet.append("time_logs", {})
timesheet_detail.is_billable = 1
timesheet_detail.activity_type = "_Test Activity Type"
timesheet_detail.from_time = timesheet.time_logs[0].to_time + datetime.timedelta(minutes=1)
timesheet_detail.hours = 2
timesheet_detail.to_time = timesheet_detail.from_time + datetime.timedelta(
hours=timesheet_detail.hours
)
timesheet.save().submit()
sales_invoice = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer", currency="INR")
sales_invoice.due_date = nowdate()
sales_invoice.timesheets.pop()
sales_invoice.submit()
timesheet_status = frappe.get_value("Timesheet", timesheet.name, "status")
self.assertEqual(timesheet_status, "Partially Billed")
sales_invoice2 = make_sales_invoice(timesheet.name, "_Test Item", "_Test Customer", currency="INR")
sales_invoice2.due_date = nowdate()
sales_invoice2.submit()
timesheet_status = frappe.get_value("Timesheet", timesheet.name, "status")
self.assertEqual(timesheet_status, "Billed")
sales_return = make_sales_return(sales_invoice2.name).submit()
timesheet_status = frappe.get_value("Timesheet", timesheet.name, "status")
self.assertEqual(timesheet_status, "Partially Billed")
sales_return.load_from_db()
sales_return.cancel()
timesheet.load_from_db()
self.assertEqual(timesheet.time_logs[1].sales_invoice, sales_invoice2.name)
self.assertEqual(timesheet.status, "Billed")
def make_timesheet(
employee,
@@ -283,6 +338,7 @@ def make_timesheet(
company=None,
currency=None,
exchange_rate=None,
do_not_submit=False,
):
update_activity_type(activity_type)
timesheet = frappe.new_doc("Timesheet")
@@ -311,7 +367,8 @@ def make_timesheet(
else:
timesheet.save(ignore_permissions=True)
timesheet.submit()
if not do_not_submit:
timesheet.submit()
return timesheet

View File

@@ -91,7 +91,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nSubmitted\nBilled\nPayslip\nCompleted\nCancelled",
"options": "Draft\nSubmitted\nPartially Billed\nBilled\nPayslip\nCompleted\nCancelled",
"print_hide": 1,
"read_only": 1
},
@@ -310,7 +310,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:10:53.551907",
"modified": "2025-12-19 13:48:23.453636",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet",
@@ -386,8 +386,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "ASC",
"states": [],
"title_field": "title"
}
}

View File

@@ -51,7 +51,9 @@ class Timesheet(Document):
per_billed: DF.Percent
sales_invoice: DF.Link | None
start_date: DF.Date | None
status: DF.Literal["Draft", "Submitted", "Billed", "Payslip", "Completed", "Cancelled"]
status: DF.Literal[
"Draft", "Submitted", "Partially Billed", "Billed", "Payslip", "Completed", "Cancelled"
]
time_logs: DF.Table[TimesheetDetail]
title: DF.Data | None
total_billable_amount: DF.Currency
@@ -128,6 +130,9 @@ class Timesheet(Document):
if flt(self.per_billed, self.precision("per_billed")) >= 100.0:
self.status = "Billed"
if 0.0 < flt(self.per_billed, self.precision("per_billed")) < 100.0:
self.status = "Partially Billed"
if self.sales_invoice:
self.status = "Completed"
@@ -433,7 +438,7 @@ def make_sales_invoice(source_name, item_code=None, customer=None, currency=None
target.append("items", {"item_code": item_code, "qty": hours, "rate": billing_rate})
for time_log in timesheet.time_logs:
if time_log.is_billable:
if time_log.is_billable and not time_log.sales_invoice:
target.append(
"timesheets",
{

View File

@@ -1,6 +1,10 @@
frappe.listview_settings["Timesheet"] = {
add_fields: ["status", "total_hours", "start_date", "end_date"],
get_indicator: function (doc) {
if (doc.status == "Partially Billed") {
return [__("Partially Billed"), "orange", "status,=," + "Partially Billed"];
}
if (doc.status == "Billed") {
return [__("Billed"), "green", "status,=," + "Billed"];
}

View File

@@ -518,7 +518,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
barcode(doc, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.barcode) {
if (row.barcode && !frappe.flags.trigger_from_barcode_scanner) {
erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => {
frappe.model.set_value(cdt, cdn, {
item_code: r.message.item_code,
@@ -945,11 +945,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
// Replace all occurences of comma with line feed
item.serial_no = item.serial_no.replace(/,/g, "\n");
item.conversion_factor = item.conversion_factor || 1;
refresh_field("serial_no", item.name, item.parentfield);
if (!doc.is_return) {
setTimeout(() => {
me.update_qty(cdt, cdn);
}, 3000);
}, 300);
}
}
}
@@ -1530,8 +1529,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} else if (
this.frm.doc.price_list_currency === this.frm.doc.currency &&
this.frm.doc.plc_conversion_rate &&
cint(this.frm.doc.plc_conversion_rate) != 1 &&
cint(this.frm.doc.plc_conversion_rate) != cint(this.frm.doc.conversion_rate)
flt(this.frm.doc.plc_conversion_rate) != 1 &&
flt(this.frm.doc.plc_conversion_rate) != flt(this.frm.doc.conversion_rate)
) {
this.frm.set_value("conversion_rate", this.frm.doc.plc_conversion_rate);
}

View File

@@ -735,6 +735,7 @@ erpnext.utils.update_child_items = function (opts) {
fieldname: "item_name",
label: __("Item Name"),
read_only: 1,
in_list_view: 1,
},
{
fieldtype: "Link",
@@ -792,7 +793,7 @@ erpnext.utils.update_child_items = function (opts) {
];
if (frm.doc.doctype == "Sales Order" || frm.doc.doctype == "Purchase Order") {
fields.splice(2, 0, {
fields.splice(3, 0, {
fieldtype: "Date",
fieldname: frm.doc.doctype == "Sales Order" ? "delivery_date" : "schedule_date",
in_list_view: 1,
@@ -800,7 +801,7 @@ erpnext.utils.update_child_items = function (opts) {
default: frm.doc.doctype == "Sales Order" ? frm.doc.delivery_date : frm.doc.schedule_date,
reqd: 1,
});
fields.splice(3, 0, {
fields.splice(4, 0, {
fieldtype: "Float",
fieldname: "conversion_factor",
label: __("Conversion Factor"),

View File

@@ -138,7 +138,6 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
frappe.run_serially([
() => this.set_selector_trigger_flag(data),
() => 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),
@@ -148,6 +147,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
this.show_scan_message(row.idx, !is_new_row, qty);
}),
() => this.clean_up(),
() => this.set_barcode_uom(row, uom),
() => this.revert_selector_flag(),
() => resolve(row),
]);

View File

@@ -157,7 +157,7 @@ erpnext.utils.get_address_display = function (frm, address_field, display_field,
args: { address_dict: frm.doc[address_field] },
callback: function (r) {
if (r.message) {
frm.set_value(display_field, frappe.utils.html2text(r.message));
frm.set_value(display_field, r.message);
}
},
});

View File

@@ -335,7 +335,7 @@
},
{
"fieldname": "primary_address",
"fieldtype": "Text",
"fieldtype": "Text Editor",
"label": "Primary Address",
"read_only": 1
},
@@ -625,7 +625,7 @@
"link_fieldname": "party"
}
],
"modified": "2025-11-25 09:35:56.772949",
"modified": "2026-01-16 15:56:05.967663",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",

View File

@@ -83,7 +83,7 @@ class Customer(TransactionBase):
opportunity_name: DF.Link | None
payment_terms: DF.Link | None
portal_users: DF.Table[PortalUser]
primary_address: DF.Text | None
primary_address: DF.TextEditor | None
prospect_name: DF.Link | None
represents_company: DF.Link | None
sales_team: DF.Table[SalesTeam]

View File

@@ -36,6 +36,15 @@ frappe.ui.form.on("Quotation", {
};
});
frm.set_query("warehouse", "items", (doc, cdt, cdn) => {
return {
filters: {
company: doc.company,
is_group: 0,
},
};
});
frm.set_indicator_formatter("item_code", function (doc) {
return !doc.qty && frm.doc.has_unit_price_items ? "yellow" : "";
});

View File

@@ -1220,10 +1220,12 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
},
freeze: true,
callback: function (r) {
if (!r.message) {
if (r.message.length === 0) {
frappe.msgprint({
title: __("Work Order not created"),
message: __("No Items with Bill of Materials to Manufacture"),
message: __(
"No Items with Bill of Materials to Manufacture or all items already manufactured"
),
indicator: "orange",
});
return;
@@ -1233,19 +1235,24 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
label: __("Items"),
fieldtype: "Table",
fieldname: "items",
cannot_add_rows: true,
description: __("Select BOM and Qty for Production"),
fields: [
{
fieldtype: "Read Only",
fieldtype: "Link",
fieldname: "item_code",
options: "Item",
label: __("Item Code"),
in_list_view: 1,
read_only: 1,
},
{
fieldtype: "Read Only",
fieldtype: "Data",
fieldname: "item_name",
label: __("Item Name"),
in_list_view: 1,
read_only: 1,
fetch_from: "item_code.item_name",
},
{
fieldtype: "Link",
@@ -1271,6 +1278,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
reqd: 1,
label: __("Sales Order Item"),
hidden: 1,
read_only: 1,
},
],
data: r.message,

View File

@@ -1981,6 +1981,10 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
)
]
overproduction_percentage_for_sales_order = (
frappe.get_single_value("Manufacturing Settings", "overproduction_percentage_for_sales_order")
/ 100
)
for table in [so.items, so.packed_items]:
for i in table:
bom = get_default_bom(i.item_code)
@@ -1989,12 +1993,12 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
if not for_raw_material_request:
total_work_order_qty = flt(
qb.from_(wo)
.select(Sum(wo.qty))
.select(Sum(wo.qty - wo.process_loss_qty))
.where(
(wo.production_item == i.item_code)
& (wo.sales_order == so.name)
& (wo.sales_order_item == i.name)
& (wo.docstatus.lt(2))
& (wo.docstatus == 1)
& (wo.status != "Closed")
)
.run()[0][0]
@@ -2003,7 +2007,10 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
else:
pending_qty = stock_qty
if pending_qty and i.item_code not in product_bundle_parents:
if not pending_qty:
pending_qty = stock_qty * overproduction_percentage_for_sales_order
if pending_qty > 0 and i.item_code not in product_bundle_parents:
items.append(
dict(
name=i.name,

View File

@@ -651,6 +651,9 @@ erpnext.PointOfSale.Controller = class {
async on_cart_update(args) {
frappe.dom.freeze();
if (this.frm.doc.set_warehouse !== this.settings.warehouse) {
this.frm.set_value("set_warehouse", this.settings.warehouse);
}
let item_row = undefined;
try {
let { field, value, item } = args;

View File

@@ -11,7 +11,16 @@ from frappe.cache_manager import clear_defaults_cache
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.desk.page.setup_wizard.setup_wizard import make_records
from frappe.utils import add_months, cint, formatdate, get_first_day, get_link_to_form, get_timestamp, today
from frappe.utils import (
add_months,
cint,
formatdate,
get_first_day,
get_last_day,
get_link_to_form,
get_timestamp,
today,
)
from frappe.utils.nestedset import NestedSet, rebuild_tree
from erpnext.accounts.doctype.account.account import get_account_currency
@@ -866,31 +875,41 @@ def install_country_fixtures(company, country):
def update_company_current_month_sales(company):
from_date = get_first_day(today())
to_date = get_first_day(add_months(from_date, 1))
"""Update Company's Total Monthly Sales.
results = frappe.db.sql(
"""
SELECT
SUM(base_grand_total) AS total,
DATE_FORMAT(posting_date, '%%m-%%Y') AS month_year
FROM
`tabSales Invoice`
WHERE
posting_date >= %s
AND posting_date < %s
AND docstatus = 1
AND company = %s
GROUP BY
month_year
""",
(from_date, to_date, company),
as_dict=True,
Postgres compatibility:
- Avoid MariaDB-only DATE_FORMAT().
- Use a date range for the current month instead (portable + index-friendly).
"""
# Local imports so you don't have to touch file-level imports
from frappe.query_builder.functions import Sum
start_date = get_first_day(today())
end_date = get_last_day(today())
si = frappe.qb.DocType("Sales Invoice")
total_monthly_sales = (
frappe.qb.from_(si)
.select(Sum(si.base_grand_total))
.where(
(si.docstatus == 1)
& (si.company == company)
& (si.posting_date >= start_date)
& (si.posting_date <= end_date)
)
).run(pluck=True)[0] or 0
# Fieldname in standard ERPNext is `total_monthly_sales`
frappe.db.set_value(
"Company",
company,
"total_monthly_sales",
total_monthly_sales,
update_modified=False,
)
monthly_total = results[0]["total"] if len(results) > 0 else 0
frappe.db.set_value("Company", company, "total_monthly_sales", monthly_total)
def update_company_monthly_sales(company):
"""Cache past year monthly sales of every company based on sales invoices"""

View File

@@ -1,7 +1,2 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
//--------- ONLOAD -------------
cur_frm.cscript.onload = function (doc, cdt, cdn) {};
cur_frm.cscript.refresh = function (doc, cdt, cdn) {};

View File

@@ -53,12 +53,14 @@ def get_stock_value_by_item_group(company):
.inner_join(item_doctype)
.on(doctype.item_code == item_doctype.name)
.select(item_doctype.item_group, stock_value.as_("stock_value"))
.where(doctype.warehouse.isin(warehouses))
.groupby(item_doctype.item_group)
.orderby(stock_value, order=frappe.qb.desc)
.limit(10)
)
if warehouses:
query = query.where(doctype.warehouse.isin(warehouses))
results = query.run(as_dict=True)
labels = []

View File

@@ -116,6 +116,11 @@ frappe.ui.form.on("Item", {
},
__("View")
);
frm.toggle_display(
["opening_stock"],
frappe.model.can_create("Stock Entry") && frappe.model.can_write("Stock Entry")
);
}
if (frm.doc.is_fixed_asset) {
@@ -239,6 +244,8 @@ frappe.ui.form.on("Item", {
},
};
});
frm.toggle_display(["standard_rate"], frappe.model.can_create("Item Price"));
},
validate: function (frm) {
@@ -1063,7 +1070,7 @@ frappe.tour["Item"] = [
fieldname: "valuation_rate",
title: "Valuation Rate",
description: __(
"There are two options to maintain valuation of stock. FIFO (first in - first out) and Moving Average. To understand this topic in detail please visit <a href='https://docs.erpnext.com/docs/v13/user/manual/en/stock/articles/item-valuation-fifo-and-moving-average' target='_blank'>Item Valuation, FIFO and Moving Average.</a>"
"There are two options to maintain valuation of stock. FIFO (first in - first out) and Moving Average. To understand this topic in detail please visit <a href='https://docs.frappe.io/erpnext/user/manual/en/calculation-of-valuation-rate-in-fifo-and-moving-average' target='_blank'>Item Valuation, FIFO and Moving Average.</a>"
),
},
{

View File

@@ -437,7 +437,7 @@ def get_vendor_invoices(doctype, txt, searchfield, start, page_len, filters):
query = get_vendor_invoice_query(filters)
if txt:
query = query.where(doctype.name.like(f"%{txt}%"))
query = query.where(frappe.qb.DocType(doctype).name.like(f"%{txt}%"))
if start:
query = query.limit(page_len).offset(start)

View File

@@ -266,7 +266,7 @@ erpnext.stock.PurchaseReceiptController = class PurchaseReceiptController extend
);
}
cur_frm.add_custom_button(
__("Retention Stock Entry"),
__("Sample Retention Stock Entry"),
this.make_retention_stock_entry,
__("Create")
);

View File

@@ -4849,6 +4849,154 @@ class TestPurchaseReceipt(IntegrationTestCase):
self.assertEqual(return_entry.items[0].qty, -2)
self.assertEqual(return_entry.items[0].rejected_qty, 0) # 3-3=0
def test_do_not_use_batchwise_valuation_with_fifo(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item_code = make_item(
"Test Item Do Not Use Batchwise Valuation with FIFO",
{
"is_stock_item": 1,
"has_batch_no": 1,
"batch_number_series": "BN-TESTDNUBVWF-.#####",
"valuation_method": "FIFO",
},
).name
doc = frappe.new_doc("Batch")
doc.update(
{
"batch_id": "BN-TESTDNUBVWF-00001",
"item": item_code,
}
).insert()
doc.db_set("use_batchwise_valuation", 0)
doc.reload()
self.assertTrue(doc.use_batchwise_valuation == 0)
doc = frappe.new_doc("Batch")
doc.update(
{
"batch_id": "BN-TESTDNUBVWF-00002",
"item": item_code,
}
).insert()
self.assertTrue(doc.use_batchwise_valuation == 1)
warehouse = "_Test Warehouse - _TC"
make_stock_entry(
item_code=item_code,
qty=10,
rate=100,
target=warehouse,
batch_no="BN-TESTDNUBVWF-00001",
use_serial_batch_fields=1,
)
se1 = make_stock_entry(
item_code=item_code,
qty=10,
rate=200,
target=warehouse,
batch_no="BN-TESTDNUBVWF-00001",
use_serial_batch_fields=1,
)
stock_queue = frappe.db.get_value(
"Stock Ledger Entry",
{
"item_code": item_code,
"warehouse": warehouse,
"is_cancelled": 0,
"voucher_type": "Stock Entry",
"voucher_no": se1.name,
},
"stock_queue",
)
stock_queue = frappe.parse_json(stock_queue)
self.assertEqual(stock_queue, [[10, 100.0], [10, 200.0]])
se2 = make_stock_entry(
item_code=item_code,
qty=10,
rate=2,
target=warehouse,
batch_no="BN-TESTDNUBVWF-00002",
use_serial_batch_fields=1,
)
stock_queue = frappe.db.get_value(
"Stock Ledger Entry",
{
"item_code": item_code,
"warehouse": warehouse,
"is_cancelled": 0,
"voucher_type": "Stock Entry",
"voucher_no": se2.name,
},
"stock_queue",
)
stock_queue = frappe.parse_json(stock_queue)
self.assertEqual(stock_queue, [[10, 100.0], [10, 200.0]])
se3 = make_stock_entry(
item_code=item_code,
qty=20,
source=warehouse,
batch_no="BN-TESTDNUBVWF-00001",
use_serial_batch_fields=1,
)
ste_details = frappe.db.get_value(
"Stock Ledger Entry",
{
"item_code": item_code,
"warehouse": warehouse,
"is_cancelled": 0,
"voucher_type": "Stock Entry",
"voucher_no": se3.name,
},
["stock_queue", "stock_value_difference"],
as_dict=1,
)
stock_queue = frappe.parse_json(ste_details.stock_queue)
self.assertEqual(stock_queue, [])
self.assertEqual(ste_details.stock_value_difference, 3000 * -1)
se4 = make_stock_entry(
item_code=item_code,
qty=20,
rate=0,
target=warehouse,
batch_no="BN-TESTDNUBVWF-00001",
use_serial_batch_fields=1,
do_not_submit=1,
)
se4.items[0].basic_rate = 0.0
se4.items[0].allow_zero_valuation_rate = 1
se4.submit()
stock_queue = frappe.db.get_value(
"Stock Ledger Entry",
{
"item_code": item_code,
"warehouse": warehouse,
"is_cancelled": 0,
"voucher_type": "Stock Entry",
"voucher_no": se4.name,
},
"stock_queue",
)
self.assertEqual(frappe.parse_json(stock_queue), [[20, 0.0]])
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -12,7 +12,7 @@ import frappe.query_builder.functions
from frappe import _, _dict, bold
from frappe.model.document import Document
from frappe.model.naming import make_autoname
from frappe.query_builder.functions import Sum
from frappe.query_builder.functions import Concat_ws, Locate, Sum
from frappe.utils import (
cint,
cstr,
@@ -712,17 +712,16 @@ class SerialandBatchBundle(Document):
is_packed_item = True
stock_queue = []
batches = []
if prev_sle and prev_sle.stock_queue:
batches = frappe.get_all(
"Batch",
filters={
"name": ("in", [d.batch_no for d in self.entries if d.batch_no]),
"use_batchwise_valuation": 0,
},
pluck="name",
)
batches = frappe.get_all(
"Batch",
filters={
"name": ("in", [d.batch_no for d in self.entries if d.batch_no]),
"use_batchwise_valuation": 0,
},
pluck="name",
)
if prev_sle and prev_sle.stock_queue and parse_json(prev_sle.stock_queue):
if batches and valuation_method == "FIFO":
stock_queue = parse_json(prev_sle.stock_queue)
@@ -749,7 +748,7 @@ class SerialandBatchBundle(Document):
if d.qty:
d.stock_value_difference = flt(d.qty) * d.incoming_rate
if stock_queue and valuation_method == "FIFO" and d.batch_no in batches:
if valuation_method == "FIFO" and d.batch_no in batches and d.incoming_rate is not None:
stock_queue.append([d.qty, d.incoming_rate])
d.stock_queue = json.dumps(stock_queue)
@@ -2986,7 +2985,15 @@ def get_ledgers_from_serial_batch_bundle(**kwargs) -> list[frappe._dict]:
def get_stock_ledgers_for_serial_nos(kwargs):
"""
Fetch stock ledger entries based on various filters.
:param kwargs: Filters including posting_datetime, creation, warehouse, item_code, serial_nos, ignore_voucher_detail_no, voucher_no. Joins with Serial and Batch Entry table to filter based on serial numbers.
:return: List of stock ledger entries as dictionaries.
:rtype: list[dict]
"""
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
serial_batch_entry = frappe.qb.DocType("Serial and Batch Entry")
query = (
frappe.qb.from_(stock_ledger_entry)
@@ -3013,7 +3020,7 @@ def get_stock_ledgers_for_serial_nos(kwargs):
query = query.where(timestamp_condition)
for field in ["warehouse", "item_code", "serial_no"]:
for field in ["warehouse", "item_code"]:
if not kwargs.get(field):
continue
@@ -3022,6 +3029,27 @@ def get_stock_ledgers_for_serial_nos(kwargs):
else:
query = query.where(stock_ledger_entry[field] == kwargs.get(field))
serial_nos = kwargs.get("serial_nos") or kwargs.get("serial_no")
if serial_nos and not isinstance(serial_nos, list):
serial_nos = [serial_nos]
if serial_nos:
query = (
query.left_join(serial_batch_entry)
.on(stock_ledger_entry.serial_and_batch_bundle == serial_batch_entry.parent)
.distinct()
)
bundle_match = serial_batch_entry.serial_no.isin(serial_nos)
padded_serial_no = Concat_ws("", "\n", stock_ledger_entry.serial_no, "\n")
direct_match = None
for sn in serial_nos:
cond = Locate(f"\n{sn}\n", padded_serial_no) > 0
direct_match = cond if direct_match is None else (direct_match | cond)
query = query.where(bundle_match | direct_match)
if kwargs.ignore_voucher_detail_no:
query = query.where(stock_ledger_entry.voucher_detail_no != kwargs.ignore_voucher_detail_no)

View File

@@ -115,7 +115,7 @@
},
{
"fieldname": "pickup_address",
"fieldtype": "Small Text",
"fieldtype": "Text Editor",
"read_only": 1
},
{
@@ -135,7 +135,7 @@
},
{
"fieldname": "pickup_contact",
"fieldtype": "Small Text",
"fieldtype": "Text Editor",
"read_only": 1
},
{
@@ -193,7 +193,7 @@
},
{
"fieldname": "delivery_address",
"fieldtype": "Small Text",
"fieldtype": "Text Editor",
"read_only": 1
},
{
@@ -214,7 +214,7 @@
{
"depends_on": "eval:doc.delivery_contact_name",
"fieldname": "delivery_contact",
"fieldtype": "Small Text",
"fieldtype": "Text Editor",
"read_only": 1
},
{
@@ -441,11 +441,11 @@
],
"is_submittable": 1,
"links": [],
"modified": "2026-01-07 19:24:23.566312",
"modified": "2026-01-16 14:59:28.547953",
"modified_by": "Administrator",
"module": "Stock",
"name": "Shipment",
"naming_rule": "Expression (old style)",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
@@ -477,8 +477,9 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -27,10 +27,10 @@ class Shipment(Document):
awb_number: DF.Data | None
carrier: DF.Data | None
carrier_service: DF.Data | None
delivery_address: DF.SmallText | None
delivery_address: DF.TextEditor | None
delivery_address_name: DF.Link
delivery_company: DF.Link | None
delivery_contact: DF.SmallText | None
delivery_contact: DF.TextEditor | None
delivery_contact_email: DF.Data | None
delivery_contact_name: DF.Link | None
delivery_customer: DF.Link | None
@@ -42,10 +42,10 @@ class Shipment(Document):
pallets: DF.Literal["No", "Yes"]
parcel_template: DF.Link | None
pickup: DF.Data | None
pickup_address: DF.SmallText | None
pickup_address: DF.TextEditor | None
pickup_address_name: DF.Link
pickup_company: DF.Link | None
pickup_contact: DF.SmallText | None
pickup_contact: DF.TextEditor | None
pickup_contact_email: DF.Data | None
pickup_contact_name: DF.Link | None
pickup_contact_person: DF.Link | None

View File

@@ -440,12 +440,16 @@ frappe.ui.form.on("Stock Entry", {
if (
frm.doc.docstatus == 1 &&
frm.doc.purpose == "Material Receipt" &&
["Material Receipt", "Manufacture"].includes(frm.doc.purpose) &&
frm.get_sum("items", "sample_quantity")
) {
frm.add_custom_button(__("Create Sample Retention Stock Entry"), function () {
frm.trigger("make_retention_stock_entry");
});
frm.add_custom_button(
__("Sample Retention Stock Entry"),
function () {
frm.trigger("make_retention_stock_entry");
},
__("Create")
);
}
frm.trigger("setup_quality_inspection");
@@ -568,10 +572,6 @@ frappe.ui.form.on("Stock Entry", {
if (r.message) {
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
} else {
frappe.msgprint(
__("Retention Stock Entry already created or Sample Quantity not provided")
);
}
},
});
@@ -885,12 +885,11 @@ frappe.ui.form.on("Stock Entry", {
frm.doc.items.forEach((item) => {
if (item.is_finished_item) {
fg_completed_qty += flt(item.transfer_qty);
fg_completed_qty += flt(item.transfer_qty + frm.doc.process_loss_qty);
}
});
frm.doc.fg_completed_qty = fg_completed_qty;
frm.refresh_field("fg_completed_qty");
frm.set_value("fg_completed_qty", fg_completed_qty);
},
});
@@ -1054,7 +1053,7 @@ frappe.ui.form.on("Stock Entry Detail", {
var validate_sample_quantity = function (frm, cdt, cdn) {
var d = locals[cdt][cdn];
if (d.sample_quantity && frm.doc.purpose == "Material Receipt") {
if (d.sample_quantity && d.transfer_qty && frm.doc.purpose == "Material Receipt") {
frappe.call({
method: "erpnext.stock.doctype.stock_entry.stock_entry.validate_sample_quantity",
args: {

View File

@@ -724,7 +724,7 @@ class StockEntry(StockController, SubcontractingInwardController):
"Subcontracting Return",
]
validate_for_manufacture = any([d.bom_no for d in self.get("items")])
has_bom = any([d.bom_no for d in self.get("items")])
if self.purpose in source_mandatory and self.purpose not in target_mandatory:
self.to_warehouse = None
@@ -753,7 +753,7 @@ class StockEntry(StockController, SubcontractingInwardController):
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
if self.purpose == "Manufacture":
if validate_for_manufacture:
if has_bom:
if d.is_finished_item or d.is_scrap_item:
d.s_warehouse = None
if not d.t_warehouse:
@@ -763,6 +763,17 @@ class StockEntry(StockController, SubcontractingInwardController):
if not d.s_warehouse:
frappe.throw(_("Source warehouse is mandatory for row {0}").format(d.idx))
if self.purpose == "Disassemble":
if has_bom:
if d.is_finished_item:
d.t_warehouse = None
if not d.s_warehouse:
frappe.throw(_("Source warehouse is mandatory for row {0}").format(d.idx))
else:
d.s_warehouse = None
if not d.t_warehouse:
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
if cstr(d.s_warehouse) == cstr(d.t_warehouse) and self.purpose not in [
"Material Transfer for Manufacture",
"Material Transfer",
@@ -927,7 +938,9 @@ class StockEntry(StockController, SubcontractingInwardController):
if matched_item := self.get_matched_items(item_code):
if flt(details.get("qty"), precision) != flt(matched_item.qty, precision):
frappe.throw(
_("For the item {0}, the quantity should be {1} according to the BOM {2}.").format(
_(
"For the item {0}, the consumed quantity should be {1} according to the BOM {2}."
).format(
frappe.bold(item_code),
flt(details.get("qty")),
get_link_to_form("BOM", self.bom_no),
@@ -992,12 +1005,37 @@ class StockEntry(StockController, SubcontractingInwardController):
)
def get_matched_items(self, item_code):
for row in self.items:
items = [item for item in self.items if item.s_warehouse]
for row in items or self.get_consumed_items():
if row.item_code == item_code or row.original_item == item_code:
return row
return {}
def get_consumed_items(self):
"""Get all raw materials consumed through consumption entries"""
parent = frappe.qb.DocType("Stock Entry")
child = frappe.qb.DocType("Stock Entry Detail")
query = (
frappe.qb.from_(parent)
.join(child)
.on(parent.name == child.parent)
.select(
child.item_code,
Sum(child.qty).as_("qty"),
child.original_item,
)
.where(
(parent.docstatus == 1)
& (parent.purpose == "Material Consumption for Manufacture")
& (parent.work_order == self.work_order)
)
.groupby(child.item_code, child.original_item)
)
return query.run(as_dict=True)
@frappe.whitelist()
def get_stock_and_rate(self):
"""
@@ -2162,9 +2200,12 @@ class StockEntry(StockController, SubcontractingInwardController):
def get_items_for_disassembly(self):
"""Get items for Disassembly Order"""
if not self.work_order:
frappe.throw(_("The Work Order is mandatory for Disassembly Order"))
if self.work_order:
return self._add_items_for_disassembly_from_work_order()
return self._add_items_for_disassembly_from_bom()
def _add_items_for_disassembly_from_work_order(self):
items = self.get_items_from_manufacture_entry()
s_warehouse = frappe.db.get_value("Work Order", self.work_order, "fg_warehouse")
@@ -2196,6 +2237,23 @@ class StockEntry(StockController, SubcontractingInwardController):
child_row.t_warehouse = row.s_warehouse
child_row.is_finished_item = 0 if row.is_finished_item else 1
def _add_items_for_disassembly_from_bom(self):
if not self.bom_no or not self.fg_completed_qty:
frappe.throw(_("BOM and Finished Good Quantity is mandatory for Disassembly"))
# Raw Materials
item_dict = self.get_bom_raw_materials(self.fg_completed_qty)
for item_row in item_dict.values():
item_row["to_warehouse"] = self.to_warehouse
item_row["from_warehouse"] = ""
item_row["is_finished_item"] = 0
self.add_to_stock_entry_detail(item_dict)
# Finished goods
self.load_items_from_bom()
def get_items_from_manufacture_entry(self):
return frappe.get_all(
"Stock Entry",
@@ -2560,6 +2618,7 @@ class StockEntry(StockController, SubcontractingInwardController):
expense_account = item.get("expense_account")
if not expense_account:
expense_account = frappe.get_cached_value("Company", self.company, "stock_adjustment_account")
args = {
"to_warehouse": to_warehouse,
"from_warehouse": "",
@@ -2570,8 +2629,18 @@ class StockEntry(StockController, SubcontractingInwardController):
"expense_account": expense_account,
"cost_center": item.get("buying_cost_center"),
"is_finished_item": 1,
"sample_quantity": item.get("sample_quantity"),
}
if self.purpose == "Disassemble":
args.update(
{
"from_warehouse": self.from_warehouse,
"to_warehouse": "",
"qty": flt(self.fg_completed_qty),
}
)
if (
self.work_order
and self.pro_doc.has_batch_no
@@ -3103,6 +3172,7 @@ class StockEntry(StockController, SubcontractingInwardController):
se_child.po_detail = item_row.get("po_detail")
se_child.sco_rm_detail = item_row.get("sco_rm_detail")
se_child.scio_detail = item_row.get("scio_detail")
se_child.sample_quantity = item_row.get("sample_quantity", 0)
for field in [
self.subcontract_data.rm_detail_field,
@@ -3408,13 +3478,14 @@ class StockEntry(StockController, SubcontractingInwardController):
@frappe.whitelist()
def move_sample_to_retention_warehouse(company, items):
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
get_batch_from_bundle,
from erpnext.stock.serial_batch_bundle import (
SerialBatchCreation,
get_batch_nos,
)
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
if isinstance(items, str):
items = json.loads(items)
retention_warehouse = frappe.get_single_value("Stock Settings", "sample_retention_warehouse")
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.company = company
@@ -3422,38 +3493,64 @@ def move_sample_to_retention_warehouse(company, items):
stock_entry.set_stock_entry_type()
for item in items:
if item.get("sample_quantity") and item.get("serial_and_batch_bundle"):
batch_no = get_batch_from_bundle(item.get("serial_and_batch_bundle"))
sample_quantity = validate_sample_quantity(
item.get("item_code"),
item.get("sample_quantity"),
item.get("transfer_qty") or item.get("qty"),
batch_no,
warehouse = item.get("t_warehouse") or item.get("warehouse")
total_qty = 0
cls_obj = SerialBatchCreation(
{
"type_of_transaction": "Outward",
"serial_and_batch_bundle": item.get("serial_and_batch_bundle"),
"item_code": item.get("item_code"),
"warehouse": warehouse,
"do_not_save": True,
}
)
if sample_quantity:
cls_obj = SerialBatchCreation(
{
"type_of_transaction": "Outward",
"serial_and_batch_bundle": item.get("serial_and_batch_bundle"),
"item_code": item.get("item_code"),
"warehouse": item.get("t_warehouse"),
}
sabb = cls_obj.duplicate_package()
batches = get_batch_nos(item.get("serial_and_batch_bundle"))
sabe_list = []
for batch_no in batches.keys():
sample_quantity = validate_sample_quantity(
item.get("item_code"),
item.get("sample_quantity"),
item.get("transfer_qty") or item.get("qty"),
batch_no,
)
cls_obj.duplicate_package()
sabe = next(item for item in sabb.entries if item.batch_no == batch_no)
if sample_quantity:
if sabb.has_serial_no:
new_sabe = [
entry
for entry in sabb.entries
if entry.batch_no == batch_no
and frappe.db.exists(
"Serial No", {"name": entry.serial_no, "warehouse": warehouse}
)
][: int(sample_quantity)]
sabe_list.extend(new_sabe)
total_qty += len(new_sabe)
else:
total_qty += sample_quantity
sabe.qty = sample_quantity
else:
sabb.entries.remove(sabe)
if total_qty:
if sabe_list:
sabb.entries = sabe_list
sabb.save()
stock_entry.append(
"items",
{
"item_code": item.get("item_code"),
"s_warehouse": item.get("t_warehouse"),
"s_warehouse": warehouse,
"t_warehouse": retention_warehouse,
"qty": item.get("sample_quantity"),
"qty": total_qty,
"basic_rate": item.get("valuation_rate"),
"uom": item.get("uom"),
"stock_uom": item.get("stock_uom"),
"conversion_factor": item.get("conversion_factor") or 1.0,
"serial_and_batch_bundle": cls_obj.serial_and_batch_bundle,
"serial_and_batch_bundle": sabb.name,
},
)
if stock_entry.get("items"):

View File

@@ -190,6 +190,7 @@ def make_stock_entry(**args):
"cost_center": args.cost_center,
"expense_account": args.expense_account,
"use_serial_batch_fields": args.use_serial_batch_fields,
"sample_quantity": frappe.get_value("Item", args.item, "sample_quantity") or 0,
},
)

View File

@@ -2250,6 +2250,145 @@ class TestStockEntry(IntegrationTestCase):
material_request.reload()
self.assertEqual(material_request.transfer_status, "Completed")
def test_manufacture_entry_without_wo(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
fg_item = make_item("_Mobiles", properties={"is_stock_item": 1}).name
rm_item1 = make_item("_Temper Glass", properties={"is_stock_item": 1}).name
rm_item2 = make_item("_Battery", properties={"is_stock_item": 1}).name
warehouse = "_Test Warehouse - _TC"
make_stock_entry(item_code=rm_item1, target=warehouse, qty=5, purpose="Material Receipt")
make_stock_entry(item_code=rm_item2, target=warehouse, qty=5, purpose="Material Receipt")
bom_no = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]).name
se = make_stock_entry(item_code=fg_item, qty=1, purpose="Manufacture", do_not_save=True)
se.from_bom = 1
se.use_multi_level_bom = 1
se.bom_no = bom_no
se.fg_completed_qty = 1
se.from_warehouse = warehouse
se.to_warehouse = warehouse
se.get_items()
rm_items = {d.item_code: d.qty for d in se.items if d.item_code != fg_item}
self.assertEqual(rm_items[rm_item1], 1)
self.assertEqual(rm_items[rm_item2], 1)
se.calculate_rate_and_amount()
se.save()
se.submit()
def test_disassemble_entry_without_wo(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
fg_item = make_item("_Disassemble Mobile", properties={"is_stock_item": 1}).name
rm_item1 = make_item("_Disassemble Temper Glass", properties={"is_stock_item": 1}).name
rm_item2 = make_item("_Disassemble Battery", properties={"is_stock_item": 1}).name
warehouse = "_Test Warehouse - _TC"
# Stock up the FG item (what we'll disassemble)
make_stock_entry(item_code=fg_item, target=warehouse, qty=5, purpose="Material Receipt")
bom_no = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2]).name
se = make_stock_entry(item_code=fg_item, qty=1, purpose="Disassemble", do_not_save=True)
se.from_bom = 1
se.use_multi_level_bom = 1
se.bom_no = bom_no
se.fg_completed_qty = 1
se.from_warehouse = warehouse
se.to_warehouse = warehouse
se.get_items()
# Verify FG as source (being consumed)
fg_items = [d for d in se.items if d.is_finished_item]
self.assertEqual(len(fg_items), 1)
self.assertEqual(fg_items[0].item_code, fg_item)
self.assertEqual(fg_items[0].qty, 1)
self.assertEqual(fg_items[0].s_warehouse, warehouse)
self.assertFalse(fg_items[0].t_warehouse)
# Verify RM as target (being received)
rm_items = {d.item_code: d for d in se.items if not d.is_finished_item}
self.assertEqual(len(rm_items), 2)
self.assertIn(rm_item1, rm_items)
self.assertIn(rm_item2, rm_items)
self.assertEqual(rm_items[rm_item1].qty, 1)
self.assertEqual(rm_items[rm_item2].qty, 1)
self.assertEqual(rm_items[rm_item1].t_warehouse, warehouse)
self.assertFalse(rm_items[rm_item1].s_warehouse)
se.calculate_rate_and_amount()
se.save()
se.submit()
@IntegrationTestCase.change_settings(
"Stock Settings", {"sample_retention_warehouse": "_Test Warehouse 1 - _TC"}
)
def test_sample_retention_stock_entry(self):
from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse
warehouse = "_Test Warehouse - _TC"
retain_sample_item = make_item(
"Retain Sample Item",
properties={
"is_stock_item": 1,
"retain_sample": 1,
"sample_quantity": 2,
"has_batch_no": 1,
"has_serial_no": 1,
"create_new_batch": 1,
"batch_number_series": "SAMPLE-RET-.#####",
"serial_no_series": "SAMPLE-RET-SN-.#####",
},
)
material_receipt = make_stock_entry(
item_code=retain_sample_item.item_code, target=warehouse, qty=10, purpose="Material Receipt"
)
source_sabb = frappe.get_doc(
"Serial and Batch Bundle", material_receipt.items[0].serial_and_batch_bundle
)
batch = source_sabb.entries[0].batch_no
serial_nos = [entry.serial_no for entry in source_sabb.entries]
sample_entry = frappe.get_doc(
move_sample_to_retention_warehouse(material_receipt.company, material_receipt.items)
)
sample_entry.submit()
target_sabb = frappe.get_doc("Serial and Batch Bundle", sample_entry.items[0].serial_and_batch_bundle)
self.assertEqual(sample_entry.items[0].transfer_qty, 2)
self.assertEqual(target_sabb.entries[0].batch_no, batch)
self.assertEqual([entry.serial_no for entry in target_sabb.entries], serial_nos[:2])
@IntegrationTestCase.change_settings(
"Manufacturing Settings",
{
"material_consumption": 1,
"backflush_raw_materials_based_on": "BOM",
"validate_components_quantities_per_bom": 1,
},
)
def test_validation_as_per_bom_with_continuous_raw_material_consumption(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry as _make_stock_entry
from erpnext.manufacturing.doctype.work_order.work_order import make_work_order
fg_item = make_item("_Mobiles", properties={"is_stock_item": 1}).name
rm_item1 = make_item("_Battery", properties={"is_stock_item": 1}).name
warehouse = "Stores - WP"
bom_no = make_bom(item=fg_item, raw_materials=[rm_item1]).name
make_stock_entry(item_code=rm_item1, target=warehouse, qty=5, rate=10, purpose="Material Receipt")
work_order = make_work_order(bom_no, fg_item, 5)
work_order.skip_transfer = 1
work_order.fg_warehouse = warehouse
work_order.submit()
frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 5)).submit()
frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 5)).submit()
def make_serialized_item(self, **args):
args = frappe._dict(args)

View File

@@ -75,6 +75,7 @@ class StockReconciliation(StockController):
self.validate_duplicate_serial_and_batch_bundle("items")
self.remove_items_with_no_change()
self.validate_data()
self.change_row_indexes()
self.validate_expense_account()
self.validate_customer_provided_item()
self.set_zero_value_for_customer_provided_items()
@@ -556,8 +557,7 @@ class StockReconciliation(StockController):
elif len(items) != len(self.items):
self.items = items
for i, item in enumerate(self.items):
item.idx = i + 1
self.change_idx = True
frappe.msgprint(_("Removed items with no change in quantity or value."))
def calculate_difference_amount(self, item, item_dict):
@@ -574,14 +574,14 @@ class StockReconciliation(StockController):
def validate_data(self):
def _get_msg(row_num, msg):
return _("Row # {0}:").format(row_num + 1) + " " + msg
return _("Row #{0}:").format(row_num) + " " + msg
self.validation_messages = []
item_warehouse_combinations = []
default_currency = frappe.db.get_default("currency")
for row_num, row in enumerate(self.items):
for row in self.items:
# find duplicates
key = [row.item_code, row.warehouse]
for field in ["serial_no", "batch_no"]:
@@ -594,7 +594,7 @@ class StockReconciliation(StockController):
if key in item_warehouse_combinations:
self.validation_messages.append(
_get_msg(row_num, _("Same item and warehouse combination already entered."))
_get_msg(row.idx, _("Same item and warehouse combination already entered."))
)
else:
item_warehouse_combinations.append(key)
@@ -604,7 +604,7 @@ class StockReconciliation(StockController):
if row.serial_no and not row.qty:
self.validation_messages.append(
_get_msg(
row_num,
row.idx,
f"Quantity should not be zero for the {bold(row.item_code)} since serial nos are specified",
)
)
@@ -612,17 +612,17 @@ class StockReconciliation(StockController):
# if both not specified
if row.qty in ["", None] and row.valuation_rate in ["", None]:
self.validation_messages.append(
_get_msg(row_num, _("Please specify either Quantity or Valuation Rate or both"))
_get_msg(row.idx, _("Please specify either Quantity or Valuation Rate or both"))
)
# do not allow negative quantity
if flt(row.qty) < 0:
self.validation_messages.append(_get_msg(row_num, _("Negative Quantity is not allowed")))
self.validation_messages.append(_get_msg(row.idx, _("Negative Quantity is not allowed")))
# do not allow negative valuation
if flt(row.valuation_rate) < 0:
self.validation_messages.append(
_get_msg(row_num, _("Negative Valuation Rate is not allowed"))
_get_msg(row.idx, _("Negative Valuation Rate is not allowed"))
)
if row.qty and row.valuation_rate in ["", None]:
@@ -654,6 +654,11 @@ class StockReconciliation(StockController):
raise frappe.ValidationError(self.validation_messages)
def change_row_indexes(self):
if getattr(self, "change_idx", False):
for i, item in enumerate(self.items):
item.idx = i + 1
def validate_item(self, item_code, row):
from erpnext.stock.doctype.item.item import (
validate_cancelled_item,

View File

@@ -42,9 +42,37 @@ def get_data(report_filters):
gl_data = voucher_wise_gl_data.get(key) or {}
d.account_value = gl_data.get("account_value", 0)
d.difference_value = d.stock_value - d.account_value
d.ledger_type = "Stock Ledger Entry"
if abs(d.difference_value) > 0.1:
data.append(d)
if key in voucher_wise_gl_data:
del voucher_wise_gl_data[key]
if voucher_wise_gl_data:
data += get_gl_ledgers_with_no_stock_ledger_entries(voucher_wise_gl_data)
return data
def get_gl_ledgers_with_no_stock_ledger_entries(voucher_wise_gl_data):
data = []
for key in voucher_wise_gl_data:
gl_data = voucher_wise_gl_data.get(key) or {}
data.append(
{
"name": gl_data.get("name"),
"ledger_type": "GL Entry",
"voucher_type": gl_data.get("voucher_type"),
"voucher_no": gl_data.get("voucher_no"),
"posting_date": gl_data.get("posting_date"),
"stock_value": 0,
"account_value": gl_data.get("account_value", 0),
"difference_value": gl_data.get("account_value", 0) * -1,
}
)
return data
@@ -88,6 +116,7 @@ def get_gl_data(report_filters, filters):
"name",
"voucher_type",
"voucher_no",
"posting_date",
{
"SUB": [{"SUM": "debit_in_account_currency"}, {"SUM": "credit_in_account_currency"}],
"as": "account_value",
@@ -109,10 +138,15 @@ def get_columns(filters):
{
"label": _("Stock Ledger ID"),
"fieldname": "name",
"fieldtype": "Link",
"options": "Stock Ledger Entry",
"fieldtype": "Dynamic Link",
"options": "ledger_type",
"width": "80",
},
{
"label": _("Ledger Type"),
"fieldname": "ledger_type",
"fieldtype": "Data",
},
{"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date"},
{"label": _("Posting Time"), "fieldname": "posting_time", "fieldtype": "Time"},
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": "110"},

View File

@@ -989,9 +989,10 @@ def get_batch_nos(serial_and_batch_bundle):
entries = frappe.get_all(
"Serial and Batch Entry",
fields=["batch_no", "qty", "name"],
fields=["batch_no", {"SUM": "qty", "as": "qty"}],
filters={"parent": serial_and_batch_bundle, "batch_no": ("is", "set")},
order_by="idx",
group_by="batch_no",
)
if not entries:
@@ -1116,7 +1117,7 @@ class SerialBatchCreation:
id = self.serial_and_batch_bundle
package = frappe.get_doc("Serial and Batch Bundle", id)
new_package = frappe.copy_doc(package)
new_package = frappe.copy_doc(package, ignore_no_copy=False)
if self.get("returned_serial_nos"):
self.remove_returned_serial_nos(new_package)

View File

@@ -38,7 +38,7 @@ skip_namespaces = [
]
[tool.bench.frappe-dependencies]
frappe = ">=16.0.0-dev,<17.0.0"
frappe = ">=16.0.0,<17.0.0"
[tool.ruff]
line-length = 110