Compare commits

...

265 Commits

Author SHA1 Message Date
Frappe PR Bot
e0ca060475 chore(release): Bumped to Version 15.65.3
## [15.65.3](https://github.com/frappe/erpnext/compare/v15.65.2...v15.65.3) (2025-06-19)

### Bug Fixes

* stock reconciliation validation for serial and batch ([3057f6c](3057f6ce35))
* use set_query on sales_order link field in work order ([6a6130e](6a6130e06c))
2025-06-19 05:30:19 +00:00
rohitwaghchaure
84d348b3d4 Merge pull request #48145 from frappe/mergify/bp/version-15/pr-48107
fix: use set_query on sales_order link field in work order (backport #48077) (backport #48107)
2025-06-19 10:58:59 +05:30
rohitwaghchaure
a63457e5b0 Merge pull request #48144 from frappe/mergify/bp/version-15/pr-48113
fix: stock reconciliation validation for serial nos (backport #47988) (backport #48113)
2025-06-19 10:58:43 +05:30
Mihir Kandoi
6a6130e06c fix: use set_query on sales_order link field in work order
(cherry picked from commit 6def182e1a)
(cherry picked from commit b33bec4dad)
2025-06-19 04:37:39 +00:00
Rohit Waghchaure
3057f6ce35 fix: stock reconciliation validation for serial and batch
(cherry picked from commit 69d54d2e0f)
(cherry picked from commit 89376ddf8d)
2025-06-19 04:36:35 +00:00
Frappe PR Bot
d2ac603fd7 chore(release): Bumped to Version 15.65.2
## [15.65.2](https://github.com/frappe/erpnext/compare/v15.65.1...v15.65.2) (2025-06-17)

### Bug Fixes

* add validation for calculate ageing with filter for summary and other reports ([23db22a](23db22ac0b))
* add validation for exchange gain/loss entries ([97f1304](97f13049ee))
* add validation for party type ([baa08ce](baa08ce496))
* batch page length ([994454b](994454bfc3))
* budget naming series (backport [#48075](https://github.com/frappe/erpnext/issues/48075)) ([#48080](https://github.com/frappe/erpnext/issues/48080)) ([4d18fd0](4d18fd0e80))
* do not allow capitalization from connection tab for submitted asset ([7b5e2a6](7b5e2a6af0))
* do not reset party account for return doc ([ad5421a](ad5421a413))
* float division by zero ([40504b8](40504b8da2))
* incorrect condition for setting party account on change of company ([f2b9e73](f2b9e73819))
* incorrect warehouse set from SO to MR ([e9365d7](e9365d7272))
* pos invoice consolidation row refer issue (backport [#48057](https://github.com/frappe/erpnext/issues/48057)) ([#48058](https://github.com/frappe/erpnext/issues/48058)) ([db9f0b6](db9f0b6f38))
* prevent saving negative quantity in BOM ([1c9032a](1c9032a4c2))
* unpack non-iterable NoneType object error ([dd39d24](dd39d24da0))
* update asset status after making asset value adjustment record ([ee30357](ee30357835))
2025-06-17 14:51:34 +00:00
ruthra kumar
3340b19c5d Merge pull request #48100 from frappe/version-15-hotfix
chore: release v15
2025-06-17 20:20:01 +05:30
Sagar Vora
4d1022182d Merge pull request #48105 from frappe/mergify/bp/version-15-hotfix/pr-48104
fix: incorrect condition for setting party account on change of company (backport #48104)
2025-06-17 11:26:35 +00:00
ljain112
f2b9e73819 fix: incorrect condition for setting party account on change of company
(cherry picked from commit 20565f5f19)
2025-06-17 11:26:13 +00:00
ruthra kumar
31cb78e3f3 Merge pull request #48101 from frappe/mergify/bp/version-15-hotfix/pr-47898
Pegged currency (backport #47898)
2025-06-17 16:00:54 +05:30
ruthra kumar
789cd20d2d Merge pull request #48102 from frappe/mergify/bp/version-15-hotfix/pr-48098
fix: do not reset party account for return doc (backport #48098)
2025-06-17 15:42:32 +05:30
Karuppasamy
cb58d05777 Merge pull request #47898 from aerele/pegged-currency
Pegged currency

(cherry picked from commit cec0ffad06)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json
#	erpnext/patches.txt
#	erpnext/setup/install.py
2025-06-17 15:32:36 +05:30
ljain112
ad5421a413 fix: do not reset party account for return doc
(cherry picked from commit 7e758a9d5b)
2025-06-17 09:59:44 +00:00
ruthra kumar
84f9a1fb2d Merge pull request #48094 from frappe/mergify/bp/version-15-hotfix/pr-48082
fix:add validation for party type (backport #48082)
2025-06-17 15:14:34 +05:30
ruthra kumar
7d88b8bbb9 Merge pull request #48093 from frappe/mergify/bp/version-15-hotfix/pr-47879
fix: add validation for exchange gain/loss entries (backport #47879)
2025-06-17 13:59:08 +05:30
AlcinSnowlina
baa08ce496 fix: add validation for party type
(cherry picked from commit 7c9d6aaae2)
2025-06-17 08:26:07 +00:00
ruthra kumar
7031a5033c Merge pull request #48092 from frappe/mergify/bp/version-15-hotfix/pr-48076
fix: Prevent saving negative quantity in BOM (backport #48076)
2025-06-17 13:47:20 +05:30
l0gesh29
1f1e88d52e test: add test for debit/credit calculations in exchange gain/loss account filter in GL
(cherry picked from commit 6ea32a8762)

# Conflicts:
#	erpnext/accounts/report/general_ledger/test_general_ledger.py
2025-06-17 13:42:02 +05:30
l0gesh29
97f13049ee fix: add validation for exchange gain/loss entries
(cherry picked from commit d992f67658)
2025-06-17 08:04:57 +00:00
iamkhanraheel
1c9032a4c2 fix: prevent saving negative quantity in BOM
(cherry picked from commit e52d83e756)
2025-06-17 07:50:27 +00:00
Khushi Rawat
323a91ddd7 Merge pull request #48090 from khushi8112/update-asset-status-after-revaluation-v15-backport
fix: update asset status after making asset value adjustment record
2025-06-17 13:06:50 +05:30
Khushi Rawat
ee30357835 fix: update asset status after making asset value adjustment record 2025-06-17 12:35:44 +05:30
mergify[bot]
4d18fd0e80 fix: budget naming series (backport #48075) (#48080)
* fix: budget naming series (#48075)

(cherry picked from commit c4bdf2a721)

# Conflicts:
#	erpnext/accounts/doctype/budget/budget.json

* chore: resolve conflict

---------

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-06-17 11:21:54 +05:30
rohitwaghchaure
d054486495 Merge pull request #48065 from frappe/mergify/bp/version-15-hotfix/pr-48061
fix: incorrect warehouse set from SO to MR (backport #48061)
2025-06-16 14:52:44 +05:30
Rohit Waghchaure
e9365d7272 fix: incorrect warehouse set from SO to MR
(cherry picked from commit 0da8d9c869)
2025-06-16 04:42:27 +00:00
ruthra kumar
b3578ecf09 Merge pull request #48063 from frappe/mergify/bp/version-15-hotfix/pr-48036
fix: add validation for calculate ageing with filter for summary and … (backport #48036)
2025-06-16 09:52:37 +05:30
l0gesh29
23db22ac0b fix: add validation for calculate ageing with filter for summary and other reports
(cherry picked from commit c630aa9fe8)
2025-06-16 03:10:11 +00:00
rohitwaghchaure
85099a3a68 Merge pull request #48044 from frappe/mergify/bp/version-15-hotfix/pr-48037
fix: float division by zero (backport #48037)
2025-06-15 18:29:02 +05:30
mergify[bot]
db9f0b6f38 fix: pos invoice consolidation row refer issue (backport #48057) (#48058)
fix: pos invoice consolidation row refer issue (#48057)

(cherry picked from commit 4178d9e2a1)

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-06-13 20:08:22 +05:30
mergify[bot]
56311e6e1e build(deps): bump rapidfuzz (backport #47503) (#48051)
build(deps): bump rapidfuzz (#47503)


(cherry picked from commit c275c55d6c)

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
Co-authored-by: Akhil Narang <me@akhilnarang.dev>
2025-06-13 16:01:08 +05:30
Rohit Waghchaure
40504b8da2 fix: float division by zero
(cherry picked from commit 59cbe85817)
2025-06-13 02:50:28 +00:00
rohitwaghchaure
379def5f0f Merge pull request #48022 from frappe/mergify/bp/version-15-hotfix/pr-48018
fix: batch page length (backport #48018)
2025-06-12 15:47:24 +05:30
Khushi Rawat
ab6a534df2 Merge pull request #48032 from frappe/mergify/bp/version-15-hotfix/pr-48031
fix: do not allow capitalization from connection tab for submitted asset (backport #48031)
2025-06-12 15:22:06 +05:30
Khushi Rawat
7b5e2a6af0 fix: do not allow capitalization from connection tab for submitted asset
(cherry picked from commit 27bec4cde5)
2025-06-12 09:48:10 +00:00
Rohit Waghchaure
994454bfc3 fix: batch page length
(cherry picked from commit 338256b799)
2025-06-11 14:33:13 +00:00
Frappe PR Bot
9febca2981 chore(release): Bumped to Version 15.65.1
## [15.65.1](https://github.com/frappe/erpnext/compare/v15.65.0...v15.65.1) (2025-06-11)

### Bug Fixes

* unpack non-iterable NoneType object error ([f92b5b9](f92b5b9a2e))
2025-06-11 12:51:34 +00:00
Sagar Vora
62efd09f2f Merge pull request #48021 from frappe/mergify/bp/version-15/pr-48019
fix: unpack non-iterable NoneType object error in patch (backport #48019)
2025-06-11 12:50:06 +00:00
Sagar Vora
b8f1c8f6b1 Merge pull request #48020 from frappe/mergify/bp/version-15-hotfix/pr-48019
fix: unpack non-iterable NoneType object error in patch (backport #48019)
2025-06-11 12:49:27 +00:00
priyanshshah2442
f92b5b9a2e fix: unpack non-iterable NoneType object error
(cherry picked from commit 7d940faa4f)
2025-06-11 12:49:17 +00:00
priyanshshah2442
dd39d24da0 fix: unpack non-iterable NoneType object error
(cherry picked from commit 7d940faa4f)
2025-06-11 12:49:05 +00:00
Frappe PR Bot
dc8bb792d7 chore(release): Bumped to Version 15.65.0
# [15.65.0](https://github.com/frappe/erpnext/compare/v15.64.1...v15.65.0) (2025-06-10)

### Bug Fixes

* add .length in list validation ([#47974](https://github.com/frappe/erpnext/issues/47974)) ([66f41d4](66f41d44c4))
* add change log for bug fix in Additional Discount functionality ([f27e591](f27e591d88))
* add draft transactions also in calculated mismatch report ([23b5d2d](23b5d2db2c))
* add user permission while fetching ple ([a2cdd91](a2cdd91a0d))
* **asset:** make purchase date mandatory ([a5e5553](a5e5553520))
* AttributeError due to incorrect object ([43d4e26](43d4e26ac5))
* available qty in BOM Stock Report ([84b2f87](84b2f871ba))
* better description of tab name ([#44697](https://github.com/frappe/erpnext/issues/44697)) ([d05b49b](d05b49b0f8))
* changes in report ([78c6386](78c63869e0))
* changes to report and patch ([5237ff8](5237ff8d94))
* conflicts ([aa29c5d](aa29c5dde2))
* consider expired batches in the stock reco (backport [#47909](https://github.com/frappe/erpnext/issues/47909)) ([#47919](https://github.com/frappe/erpnext/issues/47919)) ([2e78e14](2e78e14c7e))
* consider user permission while populating the data ([617b065](617b0658b8))
* do not create repeat work orders ([795108c](795108c1dd))
* do not remove item which has zero qty and zero valuation ([ef77791](ef77791bd6))
* ensure proper float conversion for discount values ([d24c2c4](d24c2c4cca))
* fetch correct item tax template on item rate update ([#47973](https://github.com/frappe/erpnext/issues/47973)) ([f88e682](f88e68230a))
* fieldtype to Currency for discount amounts ([59dd5fe](59dd5fee26))
* incorrect warehouse in MR ([8156d89](8156d89903))
* key-error for COGS By Item Group report (backport [#47914](https://github.com/frappe/erpnext/issues/47914)) ([#47915](https://github.com/frappe/erpnext/issues/47915)) ([996fb75](996fb7552a))
* patch to set discount percentange in case of mismatch ([039c47e](039c47e3f2))
* pos permission error on strict permission (backport [#47896](https://github.com/frappe/erpnext/issues/47896)) ([#47897](https://github.com/frappe/erpnext/issues/47897)) ([0314a39](0314a39fab))
* Project argument is passed correctly for MR creation ([e98ad4c](e98ad4ce27))
* remove currency col ([35035c2](35035c2a31))
* remove use sales invoice check ([#47908](https://github.com/frappe/erpnext/issues/47908)) ([1b15507](1b1550708d))
* **report:** include descendants when filtering by parent item group ([d21bfa2](d21bfa219d))
* **sales order:** error message on creation of work order from sales order ([129cd7a](129cd7ae8a))
* stock adjustment entry during reposting (backport [#47878](https://github.com/frappe/erpnext/issues/47878)) ([#47883](https://github.com/frappe/erpnext/issues/47883)) ([e5d06f8](e5d06f8c86))
* stock reco qty with inventory dimension (backport [#47918](https://github.com/frappe/erpnext/issues/47918)) ([#47922](https://github.com/frappe/erpnext/issues/47922)) ([6d2c14c](6d2c14c75e))
* test case to verify correct setting of discount amount and percentage ([06ea957](06ea957ae5))
* throw permission error ([#47976](https://github.com/frappe/erpnext/issues/47976)) ([9167d2e](9167d2ef64))
* typo ([8b4824f](8b4824fef5))
* update currency based on transaction ([eaeb18c](eaeb18c651))
* zero division error in purchase receipt ([b99f8fd](b99f8fd021))

### Features

* Add hook to update gl dict by apps ([76c2477](76c2477d23))
* add validation for inter company transactions ([9a47c50](9a47c507c0))
* populate Timer dialog project field from Timesheet parent_project (backport [#47971](https://github.com/frappe/erpnext/issues/47971)) ([#48001](https://github.com/frappe/erpnext/issues/48001)) ([66b0426](66b0426155))
* report to verify discount amount mismatch ([b3eb49d](b3eb49d39d))

### Performance Improvements

* Batch GLE/SLE rename commits (backport [#47950](https://github.com/frappe/erpnext/issues/47950)) ([#47951](https://github.com/frappe/erpnext/issues/47951)) ([f490de9](f490de9285))

### Reverts

* Revert "fix: calculate discount percentage if discount amount is specified (#…" ([5a5449c](5a5449c60c))
2025-06-10 14:33:55 +00:00
ruthra kumar
bd11146f02 Merge pull request #47996 from frappe/version-15-hotfix
chore: release v15
2025-06-10 20:02:19 +05:30
rohitwaghchaure
33f1d7a5fe Merge pull request #48004 from frappe/mergify/bp/version-15-hotfix/pr-47998
fix: incorrect warehouse in MR (backport #47998)
2025-06-10 18:41:17 +05:30
rohitwaghchaure
60de0474a1 chore: fix conflicts 2025-06-10 18:19:27 +05:30
Rohit Waghchaure
8156d89903 fix: incorrect warehouse in MR
(cherry picked from commit 2b9ca79291)

# Conflicts:
#	erpnext/manufacturing/doctype/production_plan/production_plan.py
2025-06-10 12:41:26 +00:00
mergify[bot]
66b0426155 feat: populate Timer dialog project field from Timesheet parent_project (backport #47971) (#48001)
feat: populate Timer dialog project field from Timesheet parent_project (#47971)

* feat: default parent project in timer dialog > project

* chore: fix formatting

* fix: remove unnecessary or condition

---------


(cherry picked from commit bc87609264)

Co-authored-by: Rahul Agrawal <12agrawalrahul@gmail.com>
Co-authored-by: Rahul Agrawal <deathstarconsole@Rahuls-MacBook-Air.local>
2025-06-10 18:07:28 +05:30
rohitwaghchaure
60b12b8319 Merge pull request #47992 from frappe/mergify/bp/version-15-hotfix/pr-47969
fix: do not create repeat work orders (backport #47969)
2025-06-10 16:40:54 +05:30
ruthra kumar
112d40db22 Merge pull request #47994 from frappe/mergify/bp/version-15-hotfix/pr-47981
refactor(Work Order): query_sales_order (backport #47981)
2025-06-10 15:01:09 +05:30
ruthra kumar
0d0b05bf6c Merge pull request #47991 from frappe/mergify/bp/version-15-hotfix/pr-47923
fix: update currency based on transaction (backport #47923)
2025-06-10 14:42:05 +05:30
rohitwaghchaure
c2d7e8c471 chore: fix conflicts 2025-06-10 14:38:18 +05:30
rohitwaghchaure
ccb0f7ac42 chore: fix conflicts 2025-06-10 14:36:31 +05:30
rohitwaghchaure
781b66e252 chore: fix conflicts 2025-06-10 14:35:23 +05:30
barredterra
efd3b1c966 refactor(Work Order): query_sales_order
- Use `get_list` instead of `db.sql_list`

    The method is used for setting link options in the frontend and the Link field doesn't ignore permissions, so get_list should be fine here.

- Added type hints to enable argument validation

(cherry picked from commit 2dbdacf905)
2025-06-10 09:03:18 +00:00
Rohit Waghchaure
795108c1dd fix: do not create repeat work orders
(cherry picked from commit 384f4e120a)

# Conflicts:
#	erpnext/manufacturing/doctype/production_plan/production_plan.js
#	erpnext/manufacturing/doctype/production_plan/test_production_plan.py
#	erpnext/manufacturing/doctype/production_plan_sub_assembly_item/production_plan_sub_assembly_item.json
2025-06-10 09:00:27 +00:00
rohitwaghchaure
7348778220 Merge pull request #47941 from frappe/mergify/bp/version-15-hotfix/pr-47888
fix: do not remove item which has zero qty and zero valuation (backport #47888)
2025-06-10 14:29:54 +05:30
DHINESH00
eaeb18c651 fix: update currency based on transaction
(cherry picked from commit fc4f38eed1)
2025-06-10 08:56:37 +00:00
rohitwaghchaure
78607b5812 Merge pull request #47987 from frappe/mergify/bp/version-15-hotfix/pr-47942
fix: available qty in BOM Stock Report (backport #47942)
2025-06-10 14:02:43 +05:30
Sagar Vora
823cfeaf4f Merge pull request #47978 from frappe/mergify/bp/version-15-hotfix/pr-47976
fix: throw permission error (backport #47976)
2025-06-10 07:37:10 +00:00
Sagar Vora
aa29c5dde2 fix: conflicts 2025-06-10 13:05:37 +05:30
ruthra kumar
713b17c3a5 Merge pull request #47990 from frappe/mergify/bp/version-15-hotfix/pr-47989
fix: Include draft transactions in the `Calculated Discount Mismatch` report (backport #47989)
2025-06-10 12:58:05 +05:30
priyanshshah2442
23b5d2db2c fix: add draft transactions also in calculated mismatch report
(cherry picked from commit 4e1abc1814)
2025-06-10 07:12:03 +00:00
Rohit Waghchaure
84b2f871ba fix: available qty in BOM Stock Report
(cherry picked from commit ea689bbe3f)
2025-06-10 06:50:51 +00:00
Sagar Vora
ac78bfb55b Merge pull request #47985 from frappe/mergify/bp/version-15-hotfix/pr-47946
fix: patch and report for incorrect discount values (backport #47946)
2025-06-10 06:10:23 +00:00
priyanshshah2442
59dd5fee26 fix: fieldtype to Currency for discount amounts
(cherry picked from commit f781a39dbe)
2025-06-10 06:04:09 +00:00
Sagar Vora
35035c2a31 fix: remove currency col
(cherry picked from commit 9bf9b34ac4)
2025-06-10 06:04:08 +00:00
Sagar Vora
78c63869e0 fix: changes in report
(cherry picked from commit 33e793354c)
2025-06-10 06:04:08 +00:00
priyanshshah2442
06ea957ae5 fix: test case to verify correct setting of discount amount and percentage
(cherry picked from commit 3f0c5be5d9)
2025-06-10 06:04:08 +00:00
priyanshshah2442
f27e591d88 fix: add change log for bug fix in Additional Discount functionality
(cherry picked from commit 9120927a65)
2025-06-10 06:04:08 +00:00
priyanshshah2442
d24c2c4cca fix: ensure proper float conversion for discount values
(cherry picked from commit 3dcb801a37)
2025-06-10 06:04:07 +00:00
Sagar Vora
5237ff8d94 fix: changes to report and patch
(cherry picked from commit daad6137f8)
2025-06-10 06:04:07 +00:00
priyanshshah2442
b3eb49d39d feat: report to verify discount amount mismatch
(cherry picked from commit 62dd6df24f)
2025-06-10 06:04:07 +00:00
priyanshshah2442
039c47e3f2 fix: patch to set discount percentange in case of mismatch
(cherry picked from commit f7eda8a156)
2025-06-10 06:04:06 +00:00
ruthra kumar
7ecb4d3d6f Merge pull request #47968 from aerele/validate-intercompany-rate
Add validation for inter company transactions rate
2025-06-10 11:12:26 +05:30
ruthra kumar
76a9e45ff8 Merge pull request #47982 from frappe/mergify/bp/version-15-hotfix/pr-47974
fix: add .length in list validation (backport #47974)
2025-06-10 10:25:21 +05:30
ruthra kumar
c69a0f150d Merge pull request #47934 from thomasantony12/so_bug_on_wo
fix(sales order): error message on creation of work order from sales order
2025-06-10 10:19:33 +05:30
l0gesh29
66f41d44c4 fix: add .length in list validation (#47974)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
(cherry picked from commit c8cec8cedf)
2025-06-09 23:16:36 +00:00
Khushi Rawat
7393a9f470 Merge pull request #47980 from frappe/mergify/bp/version-15-hotfix/pr-47979
fix: AttributeError due to incorrect object (backport #47979)
2025-06-10 00:31:45 +05:30
Khushi Rawat
43d4e26ac5 fix: AttributeError due to incorrect object
(cherry picked from commit 351796bce6)
2025-06-09 18:46:03 +00:00
Aayush Dalal
9167d2ef64 fix: throw permission error (#47976)
Co-authored-by: Sagar Vora <16315650+sagarvora@users.noreply.github.com>
(cherry picked from commit 8b6a8d0c4f)

# Conflicts:
#	erpnext/stock/utils.py
2025-06-09 17:29:58 +00:00
Diptanil Saha
f88e68230a fix: fetch correct item tax template on item rate update (#47973) 2025-06-09 19:23:33 +05:30
ravibharathi656
3aee14176c test: pass sales invoice name instead of doc 2025-06-09 19:12:51 +05:30
ravibharathi656
d6796da464 test: add unit test for inter company transaction rate validation 2025-06-09 19:12:51 +05:30
ravibharathi656
9a47c507c0 feat: add validation for inter company transactions 2025-06-09 19:12:51 +05:30
ruthra kumar
95d1d7047d Merge pull request #47967 from frappe/mergify/bp/version-15-hotfix/pr-47865
fix: consider user permission while populating the data (backport #47865)
2025-06-09 15:41:37 +05:30
l0gesh29
a2cdd91a0d fix: add user permission while fetching ple
(cherry picked from commit 1a4bb30923)
2025-06-09 09:53:13 +00:00
l0gesh29
617b0658b8 fix: consider user permission while populating the data
(cherry picked from commit 074dc6d7dd)
2025-06-09 09:53:13 +00:00
Khushi Rawat
d8e9ed417d Merge pull request #47943 from frappe/mergify/bp/version-15-hotfix/pr-47547
fix(asset): make purchase date mandatory (backport #47547)
2025-06-09 14:48:57 +05:30
ruthra kumar
eb7eadc16f Merge pull request #47590 from FathihMohammed/show_descedants
fix(report): include descendants when filtering by parent item group
2025-06-09 13:31:41 +05:30
FATHIH MOHAMMED
d21bfa219d fix(report): include descendants when filtering by parent item group 2025-06-09 11:54:32 +05:30
ruthra kumar
198089cac1 Merge pull request #47958 from frappe/mergify/bp/version-15-hotfix/pr-44697
fix: better description of tab name (backport #44697)
2025-06-09 10:17:31 +05:30
mahsem
d05b49b0f8 fix: better description of tab name (#44697)
(cherry picked from commit 6119d4384a)
2025-06-08 20:15:11 +00:00
mergify[bot]
f490de9285 perf: Batch GLE/SLE rename commits (backport #47950) (#47951)
perf: Batch GLE/SLE rename commits (#47950)

(cherry picked from commit bb693c0a4f)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2025-06-06 21:00:49 +05:30
RAVIBHARATHI P C
a5e5553520 fix(asset): make purchase date mandatory
(cherry picked from commit e6f47be4b0)
2025-06-06 12:10:43 +00:00
rohitwaghchaure
ea393ef008 chore: fix conflicts 2025-06-06 15:38:10 +05:30
Rohit Waghchaure
ef77791bd6 fix: do not remove item which has zero qty and zero valuation
(cherry picked from commit 86e4a658a5)

# Conflicts:
#	erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
2025-06-06 10:03:14 +00:00
Frappe PR Bot
63d165c48a chore(release): Bumped to Version 15.64.1
## [15.64.1](https://github.com/frappe/erpnext/compare/v15.64.0...v15.64.1) (2025-06-06)

### Reverts

* Revert "fix: calculate discount percentage if discount amount is specified (#…" ([29d7593](29d7593fa7))
2025-06-06 07:07:12 +00:00
thomasantony12
129cd7ae8a fix(sales order): error message on creation of work order from sales order 2025-06-06 12:36:30 +05:30
Sagar Vora
83a57909d3 Merge pull request #47933 from frappe/mergify/bp/version-15/pr-47927
fix: calculate discount percentage if discount amount is specified" (backport #47927)
2025-06-06 07:05:40 +00:00
Sagar Vora
29d7593fa7 Revert "fix: calculate discount percentage if discount amount is specified (#…"
This reverts commit bb474f4f42.

(cherry picked from commit 27dc0f5b70)
2025-06-06 07:05:26 +00:00
Sagar Vora
a8e1c4f6cd Merge pull request #47928 from frappe/mergify/bp/version-15-hotfix/pr-47927
fix: calculate discount percentage if discount amount is specified" (backport #47927)
2025-06-06 06:07:35 +00:00
Sagar Vora
5a5449c60c Revert "fix: calculate discount percentage if discount amount is specified (#…"
This reverts commit bb474f4f42.

(cherry picked from commit 27dc0f5b70)
2025-06-06 06:07:15 +00:00
mergify[bot]
6d2c14c75e fix: stock reco qty with inventory dimension (backport #47918) (#47922)
fix: stock reco qty with inventory dimension (#47918)

(cherry picked from commit 342cebc778)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-06-06 09:51:23 +05:30
mergify[bot]
2e78e14c7e fix: consider expired batches in the stock reco (backport #47909) (#47919)
fix: consider expired batches in the stock reco (#47909)

(cherry picked from commit 8fa3473945)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-06-05 17:35:23 +05:30
mergify[bot]
996fb7552a fix: key-error for COGS By Item Group report (backport #47914) (#47915)
fix: key-error for COGS By Item Group report (#47914)

fix: keyerror for COGS By Item Group report
(cherry picked from commit 997ce4eaa7)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-06-05 17:18:30 +05:30
Deepesh Garg
04349b61bd Merge pull request #47917 from frappe/mergify/bp/version-15-hotfix/pr-47907
feat: Add hook to update gl dict by apps (backport #47907)
2025-06-05 16:51:20 +05:30
Deepesh Garg
d3202068d9 style: Linting issues
(cherry picked from commit c4aecb15ce)
2025-06-05 11:03:52 +00:00
Deepesh Garg
76c2477d23 feat: Add hook to update gl dict by apps
(cherry picked from commit 10ff369ff2)
2025-06-05 11:03:51 +00:00
Diptanil Saha
1b1550708d fix: remove use sales invoice check (#47908) 2025-06-05 14:08:37 +05:30
ruthra kumar
425674e164 Merge pull request #47906 from frappe/mergify/bp/version-15-hotfix/pr-47665
fix: Project argument is not passed correctly for MR creation (backport #47665)
2025-06-05 11:53:22 +05:30
Syed Mujeer Hashmi
e98ad4ce27 fix: Project argument is passed correctly for MR creation
(cherry picked from commit 9eab434ae8)
2025-06-05 06:21:32 +00:00
mergify[bot]
0314a39fab fix: pos permission error on strict permission (backport #47896) (#47897)
fix: pos permission error on strict permission (#47896)

(cherry picked from commit bb903a4bef)

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-06-04 16:11:07 +05:30
mergify[bot]
e5d06f8c86 fix: stock adjustment entry during reposting (backport #47878) (#47883)
fix: stock adjustment entry during reposting (#47878)

fix: stock adjustment entry
(cherry picked from commit cbcd580daa)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-06-04 12:51:10 +05:30
ruthra kumar
fb5d60eeb6 Merge pull request #47873 from frappe/mergify/bp/version-15-hotfix/pr-47872
fix: typo (backport #47872)
2025-06-03 19:49:47 +05:30
Mihir Kandoi
8d7be8a536 Merge pull request #47876 from frappe/mergify/bp/version-15-hotfix/pr-47874 2025-06-03 19:09:52 +05:30
Mihir Kandoi
b99f8fd021 fix: zero division error in purchase receipt
(cherry picked from commit 32229fb646)
2025-06-03 13:09:28 +00:00
Ayush Marhatta
8b4824fef5 fix: typo
(cherry picked from commit a243abb5fd)
2025-06-03 12:09:06 +00:00
Frappe PR Bot
916511ef1a chore(release): Bumped to Version 15.64.0
# [15.64.0](https://github.com/frappe/erpnext/compare/v15.63.0...v15.64.0) (2025-06-03)

### Bug Fixes

* Accounts receivable shouldn't fetch DN for employees ([9f5cfdd](9f5cfdd65b))
* add company filter to cost center and project in process statement of accounts ([5ebf1b9](5ebf1b9cc4))
* add internal link field in Sales Order connections for internal transactions ([3c697e9](3c697e90a3))
* calculate discount percentage if discount amount is specified ([#47806](https://github.com/frappe/erpnext/issues/47806)) ([ba8a316](ba8a316b06))
* cash flow report fixes ([4a1966c](4a1966c680))
* check return_against exists before api call ([8623a56](8623a5650b))
* decimal issue ([#47839](https://github.com/frappe/erpnext/issues/47839)) ([34b62d2](34b62d226c))
* ensure backend response is awaited before saving ([5a23d7c](5a23d7cdca))
* GL entries for rejected returned materials ([#47612](https://github.com/frappe/erpnext/issues/47612)) ([5bac652](5bac652b5f))
* Handle duplicate Items qty in Quotation ([4c1b415](4c1b415b9d))
* improved indexing for SLE queries. (backport [#47194](https://github.com/frappe/erpnext/issues/47194)) ([#47822](https://github.com/frappe/erpnext/issues/47822)) ([3879cbd](3879cbd86d))
* incorrect actual qty in product bundle balance report (backport [#47791](https://github.com/frappe/erpnext/issues/47791)) ([#47814](https://github.com/frappe/erpnext/issues/47814)) ([9df3b9b](9df3b9b059))
* **Timesheet:** Only update to_time if it's more than 1 second off ([#47702](https://github.com/frappe/erpnext/issues/47702)) ([470534a](470534af78))
* use `query.walk() `for escaping special chars in receiable/payable report ([2e3ebec](2e3ebec53c))
* use user default for company instead of global default in purchase order analysis report ([7d828dc](7d828dc17e))

### Features

* add column "Item Name" to "BOM Stock Report" (backport [#47116](https://github.com/frappe/erpnext/issues/47116)) ([#47485](https://github.com/frappe/erpnext/issues/47485)) ([9192913](9192913832))
* allow to set valuation rate for Rejected Materials (backport [#47582](https://github.com/frappe/erpnext/issues/47582)) ([#47869](https://github.com/frappe/erpnext/issues/47869)) ([3582b32](3582b32f03))
* show item name for raw materials in BOM creator ([0c612be](0c612be6fe))
* specify expense account and cost center for raw materials in Su… (backport [#47756](https://github.com/frappe/erpnext/issues/47756)) ([#47861](https://github.com/frappe/erpnext/issues/47861)) ([01dd733](01dd7337a2))
2025-06-03 11:54:42 +00:00
ruthra kumar
c614ff419b Merge pull request #47868 from frappe/version-15-hotfix
chore: release v15
2025-06-03 17:23:19 +05:30
ruthra kumar
d5163ed502 Merge pull request #47870 from frappe/mergify/bp/version-15-hotfix/pr-47612
fix: GL entries for rejected returned materials (backport #47612)
2025-06-03 16:45:02 +05:30
rohitwaghchaure
5bac652b5f fix: GL entries for rejected returned materials (#47612)
(cherry picked from commit 3e098da01f)
2025-06-03 10:57:14 +00:00
mergify[bot]
3582b32f03 feat: allow to set valuation rate for Rejected Materials (backport #47582) (#47869)
feat: allow to set valuation rate for Rejected Materials (#47582)

(cherry picked from commit ca0e53dd78)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-06-03 16:25:53 +05:30
ruthra kumar
2815a0d827 Merge pull request #47864 from frappe/mergify/bp/version-15-hotfix/pr-47854
fix: use user default for company instead of global default in purchase order analysis report (backport #47854)
2025-06-03 13:22:23 +05:30
Ayush Marhatta
7d828dc17e fix: use user default for company instead of global default in purchase order analysis report
(cherry picked from commit 49f23513e0)
2025-06-03 07:48:19 +00:00
mergify[bot]
01dd7337a2 feat: specify expense account and cost center for raw materials in Su… (backport #47756) (#47861) 2025-06-03 12:28:12 +05:30
Marc Ramser
470534af78 fix(Timesheet): Only update to_time if it's more than 1 second off (#47702)
* Fix: Only update to_time if it's more than 1 second off

Before, to_time was updated even when it was almost the same as the expected time (like 17:20 vs 17:19:59.998). This causes problems because of small rounding errors and caused valid times like 17:20 to be reset. Now, to_time is only updated if the difference is greater than 1 second.

To reproduce the current error:
* From Time 09:00:00
* To Time 17:20:00
Save 
To Time is 17:19:59

* Update erpnext/projects/doctype/timesheet/timesheet.py

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>

* Update timesheet.py

---------

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2025-06-03 11:59:13 +05:30
ruthra kumar
a4fe89f65c Merge pull request #47860 from frappe/mergify/bp/version-15-hotfix/pr-47809
fix: cash flow report fixes (backport #47809)
2025-06-03 11:47:50 +05:30
Lakshit Jain
4a1966c680 fix: cash flow report fixes
(cherry picked from commit 20b87512d1)
2025-06-03 05:53:23 +00:00
ruthra kumar
cd7462dd87 Merge pull request #47852 from frappe/mergify/bp/version-15-hotfix/pr-47780
fix: add internal link field in Sales Order connections for internal … (backport #47780)
2025-06-03 09:59:31 +05:30
Karuppasamy923
3c697e90a3 fix: add internal link field in Sales Order connections for internal transactions
(cherry picked from commit e3e6503076)
2025-06-02 11:17:45 +00:00
ruthra kumar
6dde327713 Merge pull request #47849 from frappe/mergify/bp/version-15-hotfix/pr-47502
fix: Handle duplicate Items qty in Quotation (backport #47502)
2025-06-02 15:11:46 +05:30
ruthra kumar
16b10274cf Merge pull request #47840 from frappe/mergify/bp/version-15-hotfix/pr-47839
fix: decimal issue (backport #47839)
2025-06-02 14:51:08 +05:30
Abdeali Chharchhodawala
4c1b415b9d fix: Handle duplicate Items qty in Quotation
fix: Handle duplicate Items qty in Quotation
(cherry picked from commit 39f6d8ffb6)
2025-06-02 09:20:41 +00:00
ruthra kumar
d7124779bf Merge pull request #47842 from frappe/mergify/bp/version-15-hotfix/pr-47821
Accounts receivable show delivery note (backport #47821)
2025-06-02 14:02:05 +05:30
ruthra kumar
053a5b93ca Merge pull request #47844 from frappe/mergify/bp/version-15-hotfix/pr-47781
fix: add company filter to cost center and project in process statement of accounts (backport #47781)
2025-06-02 13:41:21 +05:30
l0gesh29
9f5cfdd65b fix: Accounts receivable shouldn't fetch DN for employees
* fix: reorder function call

* fix: Add condition to fetch return entries for specific party types

(cherry picked from commit c8e052f3c6)
2025-06-02 13:40:20 +05:30
ljain112
5ebf1b9cc4 fix: add company filter to cost center and project in process statement of accounts
(cherry picked from commit 14313b162a)
2025-06-02 08:08:42 +00:00
rohitwaghchaure
34b62d226c fix: decimal issue (#47839)
(cherry picked from commit 0dbd9efc91)
2025-06-02 07:55:28 +00:00
Mihir Kandoi
9bf8904c80 Merge pull request #47832 from frappe/mergify/bp/version-15-hotfix/pr-47806 2025-05-31 21:18:29 +05:30
Mihir Kandoi
ba8a316b06 fix: calculate discount percentage if discount amount is specified (#47806)
(cherry picked from commit bb474f4f42)
2025-05-31 15:23:21 +00:00
mergify[bot]
3879cbd86d fix: improved indexing for SLE queries. (backport #47194) (#47822)
* fix: improved indexing for SLE queries. (#47194)

(cherry picked from commit b49a835b4c)

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

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-05-30 15:36:51 +05:30
mergify[bot]
9df3b9b059 fix: incorrect actual qty in product bundle balance report (backport #47791) (#47814)
fix: incorrect actual qty in product bundle balance report (#47791)

(cherry picked from commit c544c3e018)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-05-30 14:01:32 +05:30
ruthra kumar
0131800df2 Merge pull request #47808 from frappe/mergify/bp/version-15-hotfix/pr-47794
fix: use `query.walk() `for escaping special chars in receiable/payable report (backport #47794)
2025-05-29 14:07:34 +05:30
ljain112
2e3ebec53c fix: use query.walk() for escaping special chars in receiable/payable report
(cherry picked from commit a0a51b5074)
2025-05-29 08:21:18 +00:00
mergify[bot]
ef9ccd7a57 chore: removed orphaned function (backport #47796) (#47804)
chore: removed orphaned function (#47796)

(cherry picked from commit cb9e6f6655)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-05-29 12:25:08 +05:30
Mihir Kandoi
903d9b50fe Merge pull request #47798 from frappe/mergify/bp/version-15-hotfix/pr-47792
feat: show item name for raw materials in BOM creator (backport #47792)
2025-05-28 20:28:51 +05:30
Mihir Kandoi
0c612be6fe feat: show item name for raw materials in BOM creator
(cherry picked from commit 90ba4ad1e1)
2025-05-28 14:23:24 +00:00
ruthra kumar
893a86e173 Merge pull request #47777 from frappe/mergify/bp/version-15-hotfix/pr-47041
fix: Check `return_against` and Await API Call (backport #47041)
2025-05-28 10:57:56 +05:30
Sanket322
5a23d7cdca fix: ensure backend response is awaited before saving
(cherry picked from commit c48db0b7c0)
2025-05-28 03:56:00 +00:00
Sanket322
8623a5650b fix: check return_against exists before api call
(cherry picked from commit 00b6b97197)
2025-05-28 03:56:00 +00:00
mergify[bot]
73bc57f53e Revert "fix: translate_pos_buttons" (backport #47773) (#47774)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: translate_pos_buttons" (#47773)
2025-05-27 21:11:49 +02:00
mergify[bot]
9192913832 feat: add column "Item Name" to "BOM Stock Report" (backport #47116) (#47485)
Co-authored-by: Patrick Eißler <77415730+PatrickDEissler@users.noreply.github.com>
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2025-05-27 19:21:41 +02:00
Frappe PR Bot
f59093c6b7 chore(release): Bumped to Version 15.63.0
# [15.63.0](https://github.com/frappe/erpnext/compare/v15.62.0...v15.63.0) (2025-05-27)

### Bug Fixes

* absence of rounding causing discrepancy in the valuation rate calculation (backport [#47700](https://github.com/frappe/erpnext/issues/47700)) ([#47711](https://github.com/frappe/erpnext/issues/47711)) ([f41bcc6](f41bcc6fec))
* add no_copy for lost reasons ([db97dbd](db97dbd394))
* create Quality Inspection button not showing (backport [#47746](https://github.com/frappe/erpnext/issues/47746)) ([#47750](https://github.com/frappe/erpnext/issues/47750)) ([60dfe36](60dfe36195))
* display stock value in currency format in chart warehouse wise stock value ([ba009f4](ba009f4626))
* do not update same field twice ([63ba27e](63ba27e359))
* exchange rate not being fetched when creating supplier quotation from MR ([2c22615](2c22615b6b))
* filter of item for manufacture type material request (backport [#47712](https://github.com/frappe/erpnext/issues/47712)) ([#47717](https://github.com/frappe/erpnext/issues/47717)) ([2961e59](2961e595c2))
* handle multiselect filters for tree doctypes in Customer Ledger Summary Report ([f783bf6](f783bf60a4))
* Headline rendered twice on first save ([f94a14c](f94a14c06a))
* include rejected amount in PI/PR overbilling validation logic ([#47572](https://github.com/frappe/erpnext/issues/47572)) ([cd1c10a](cd1c10a43f))
* incorrect inventory dimension for material transfer (backport [#47592](https://github.com/frappe/erpnext/issues/47592)) ([#47644](https://github.com/frappe/erpnext/issues/47644)) ([9a78283](9a78283ecb))
* incorrect valuation rate due to positive qty (backport [#47686](https://github.com/frappe/erpnext/issues/47686)) ([#47688](https://github.com/frappe/erpnext/issues/47688)) ([62aa1cd](62aa1cdb33))
* linter ([c44493f](c44493fd7e))
* Linter (due to conflicts resolved on gh) ([37f4cf5](37f4cf5367))
* Linters ([91e167f](91e167fe72))
* made changes specifically for value adjustment entry ([74e29f1](74e29f1218))
* Merge conflicts ([3deb11e](3deb11e5b2))
* only include advances within the tcs period ([a2f5975](a2f5975133))
* party account based on party type's account type ([d3d22f6](d3d22f699e))
* patch to rename group_by filter in custom reports (backport [#47709](https://github.com/frappe/erpnext/issues/47709)) ([#47730](https://github.com/frappe/erpnext/issues/47730)) ([a137944](a137944955))
* patch to set status cancelled for already cancelled pos invoices (backport [#47725](https://github.com/frappe/erpnext/issues/47725)) ([#47759](https://github.com/frappe/erpnext/issues/47759)) ([4fd1af2](4fd1af2118))
* **portal:** User cannot create 0 qty SQ from RFQ ([f95a3f5](f95a3f5b8b))
* pos invoice status not updating on cancel (backport [#47556](https://github.com/frappe/erpnext/issues/47556)) ([#47657](https://github.com/frappe/erpnext/issues/47657)) ([db318a4](db318a4e9b))
* prettier ([0f22646](0f2264658f))
* prettier ([2c8db09](2c8db092a0))
* Relabel unit price settings for more clarity ([8891f46](8891f46a22))
* setting paid amount to 0 when is_paid is unchecked in purchase invoice ([895231a](895231a8a7))
* show general ledger in doc currency in Process Statement Of Accounts ([b3cbbf2](b3cbbf2ce3))
* skip drop ship items (backport [#47670](https://github.com/frappe/erpnext/issues/47670)) ([#47718](https://github.com/frappe/erpnext/issues/47718)) ([e058885](e05888502f))
* skip last purchase rate for free item (backport [#47693](https://github.com/frappe/erpnext/issues/47693)) ([#47696](https://github.com/frappe/erpnext/issues/47696)) ([f17b7b5](f17b7b5ee9))
* space ([fe78bb6](fe78bb60c4))
* space ([194e41a](194e41a2d9))
* translate_pos_buttons ([01b0d10](01b0d1057e))
* Treat rows as Unit Price rows only until the qty is 0 ([d963601](d9636018f5))
* typo in TREE_DOCTYPES list "Terrirtory" should be "Territory" ([3d2d1ba](3d2d1ba072))
* updated value after depreciation after value adjustment ([8ed6e98](8ed6e98565))
* use pypika object `LiteralValue` for adding match conditions ([fb2df77](fb2df77da2))

### Features

* add validation for Item Tax Template on rate change ([92d5e91](92d5e91e1f))
* Unit Price Contract ([33366fc](33366fce6c))
* Unit Price Items in Buying (RFQ, SQ, PO) ([f8fa775](f8fa775af3))
2025-05-27 14:56:33 +00:00
ruthra kumar
7ede5392bd Merge pull request #47758 from frappe/version-15-hotfix
chore: release v15
2025-05-27 20:24:59 +05:30
ruthra kumar
0f5c7d95a0 Merge pull request #47772 from frappe/mergify/bp/version-15-hotfix/pr-47766
fix: handle multiselect filters for tree doctypes in Customer Ledger Summary Report (backport #47766)
2025-05-27 20:09:05 +05:30
ljain112
f783bf60a4 fix: handle multiselect filters for tree doctypes in Customer Ledger Summary Report
(cherry picked from commit 536f7d5ff8)
2025-05-27 14:24:08 +00:00
ruthra kumar
4c49ab19d6 Merge pull request #47769 from frappe/mergify/bp/version-15-hotfix/pr-47765
fix: use pypika object `LiteralValue` for adding match conditions (backport #47765)
2025-05-27 19:52:20 +05:30
ruthra kumar
de1afee75a Merge pull request #47770 from frappe/mergify/bp/version-15-hotfix/pr-47767
fix: add no_copy for lost reasons (backport #47767)
2025-05-27 19:52:04 +05:30
l0gesh29
db97dbd394 fix: add no_copy for lost reasons
(cherry picked from commit 98e889a516)
2025-05-27 12:33:50 +00:00
ljain112
fb2df77da2 fix: use pypika object LiteralValue for adding match conditions
(cherry picked from commit 9093e5e363)
2025-05-27 12:31:16 +00:00
ruthra kumar
260073f14a Merge pull request #47764 from frappe/mergify/bp/version-15-hotfix/pr-47763
feat: add validation for Item Tax Template on rate change (backport #47763)
2025-05-27 17:14:54 +05:30
Karuppasamy923
92d5e91e1f feat: add validation for Item Tax Template on rate change
(cherry picked from commit a9a957edc7)
2025-05-27 11:30:15 +00:00
mergify[bot]
4fd1af2118 fix: patch to set status cancelled for already cancelled pos invoices (backport #47725) (#47759)
* fix: patch to set status cancelled for already cancelled pos invoices (#47725)

(cherry picked from commit 4d1d66e579)

# Conflicts:
#	erpnext/patches.txt

* chore: resolve conflict

---------

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-05-27 15:55:40 +05:30
Khushi Rawat
5997e37454 Merge pull request #47754 from khushi8112/asset-value-adjustment-of-zero-cost
fix: updated value after depreciation after value adjustment
2025-05-27 15:28:11 +05:30
Khushi Rawat
63ba27e359 fix: do not update same field twice 2025-05-27 15:10:51 +05:30
ruthra kumar
fd19f284c4 Merge pull request #47755 from frappe/mergify/bp/version-15-hotfix/pr-47679
fix: setting paid amount to 0 when is_paid is unchecked in purchase invoice (backport #47679)
2025-05-27 14:37:21 +05:30
Khushi Rawat
74e29f1218 fix: made changes specifically for value adjustment entry 2025-05-27 14:14:30 +05:30
ruthra kumar
5dee1d40ac Merge pull request #47753 from frappe/mergify/bp/version-15-hotfix/pr-47736
fix: only include advances within the tcs period (backport #47736)
2025-05-27 14:11:01 +05:30
ljain112
895231a8a7 fix: setting paid amount to 0 when is_paid is unchecked in purchase invoice
(cherry picked from commit e358a9e53f)
2025-05-27 08:32:33 +00:00
Khushi Rawat
8ed6e98565 fix: updated value after depreciation after value adjustment 2025-05-27 13:34:03 +05:30
ruthra kumar
70f9c13f3c Merge pull request #47751 from frappe/mergify/bp/version-15-hotfix/pr-47737
fix: party account based on party type's account type (backport #47737)
2025-05-27 13:31:39 +05:30
ljain112
a2f5975133 fix: only include advances within the tcs period
(cherry picked from commit 477ec9fdcc)
2025-05-27 07:49:19 +00:00
ljain112
d3d22f699e fix: party account based on party type's account type
(cherry picked from commit 19b1650522)
2025-05-27 07:44:13 +00:00
mergify[bot]
60dfe36195 fix: create Quality Inspection button not showing (backport #47746) (#47750)
fix: create Quality Inspection button not showing (#47746)

(cherry picked from commit d8cb073eaf)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-05-27 13:06:32 +05:30
ruthra kumar
b9698366c3 Merge pull request #47747 from frappe/mergify/bp/version-15-hotfix/pr-47654
fix: show general ledger in doc currency in Process Statement Of Accounts (backport #47654)
2025-05-27 12:01:48 +05:30
ruthra kumar
1fb4ac44fe Merge pull request #47748 from frappe/mergify/bp/version-15-hotfix/pr-47659
fix: translate_pos_buttons (backport #47659)
2025-05-27 11:52:27 +05:30
mahsem
0f2264658f fix: prettier
(cherry picked from commit 2839fc9460)
2025-05-27 06:12:19 +00:00
mahsem
fe78bb60c4 fix: space
(cherry picked from commit 50a5b51909)
2025-05-27 06:12:19 +00:00
mahsem
194e41a2d9 fix: space
(cherry picked from commit a442ec4e80)
2025-05-27 06:12:19 +00:00
mahsem
2c8db092a0 fix: prettier
(cherry picked from commit 1953c8489c)
2025-05-27 06:12:19 +00:00
mahsem
c44493fd7e fix: linter
(cherry picked from commit 4a6b5b9993)
2025-05-27 06:12:18 +00:00
mahsem
01b0d1057e fix: translate_pos_buttons
(cherry picked from commit ce45d1664d)
2025-05-27 06:12:18 +00:00
ljain112
9d2f396d75 chore: update test case because currency is auto set to system currency
(cherry picked from commit 22a94d6817)
2025-05-27 06:11:09 +00:00
ljain112
b3cbbf2ce3 fix: show general ledger in doc currency in Process Statement Of Accounts
(cherry picked from commit 998f6a29a4)
2025-05-27 06:11:09 +00:00
ruthra kumar
0cbb7223be Merge pull request #47742 from frappe/mergify/bp/version-15-hotfix/pr-47697
refactor: Fetch party name for contract (backport #47697)
2025-05-26 19:56:31 +05:30
ruthra kumar
c09b258d57 chore: resolve conflicts 2025-05-26 17:48:11 +05:30
ruthra kumar
d5d1a51b92 refactor: patch old contract with full party name
(cherry picked from commit 8e2221178b)

# Conflicts:
#	erpnext/patches.txt
2025-05-26 12:11:31 +00:00
ruthra kumar
9abac4c6df refactor: fetch party name on selection
(cherry picked from commit 752024e222)
2025-05-26 12:11:31 +00:00
ruthra kumar
48f786e493 refactor: full name field in contract
(cherry picked from commit 016924361a)

# Conflicts:
#	erpnext/crm/doctype/contract/contract.json
2025-05-26 12:11:30 +00:00
mergify[bot]
a137944955 fix: patch to rename group_by filter in custom reports (backport #47709) (#47730)
* fix: patch to rename group_by filter in custom reports

(cherry picked from commit 0d19c18c06)

# Conflicts:
#	erpnext/patches.txt

* fix: using python instead of sql query

(cherry picked from commit 48eccb1f73)

* chore: resolve conflict

---------

Co-authored-by: diptanilsaha <diptanil@frappe.io>
2025-05-26 13:29:41 +05:30
ruthra kumar
bb54bebe94 Merge pull request #47726 from frappe/mergify/bp/version-15-hotfix/pr-47253
fix: display stock value in currency format in chart warehouse wise stock value (backport #47253)
2025-05-26 11:35:46 +05:30
Prateek Karamchandani
ba009f4626 fix: display stock value in currency format in chart warehouse wise stock value
(cherry picked from commit 7a5cbc759c)
2025-05-26 05:49:56 +00:00
mergify[bot]
e05888502f fix: skip drop ship items (backport #47670) (#47718)
fix: skip drop ship items (#47670)

(cherry picked from commit 67c86ec028)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-05-26 07:53:55 +05:30
mergify[bot]
2961e595c2 fix: filter of item for manufacture type material request (backport #47712) (#47717)
fix: filter of item for manufacture type material request (#47712)

(cherry picked from commit 874750f9ce)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-05-25 20:51:14 +05:30
mergify[bot]
f41bcc6fec fix: absence of rounding causing discrepancy in the valuation rate calculation (backport #47700) (#47711)
fix: absence of rounding causing discrepancy in the valuation rate calculation (#47700)

(cherry picked from commit 1e8ed22421)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-05-24 17:27:20 +05:30
Marica
a6b1bdc78b Merge pull request #47410 from frappe/mergify/bp/version-15-hotfix/pr-46214
feat: Unit Price Items (backport #46214)
2025-05-23 17:42:33 +02:00
mergify[bot]
f17b7b5ee9 fix: skip last purchase rate for free item (backport #47693) (#47696)
fix: skip last purchase rate for free item (#47693)

(cherry picked from commit c3b17024bd)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-05-23 09:49:51 +05:30
ruthra kumar
5529a17831 Merge pull request #47685 from frappe/mergify/bp/version-15-hotfix/pr-47675
fix: typo in TREE_DOCTYPES list "Terrirtory" should be "Territory" (backport #47675)
2025-05-22 16:25:15 +05:30
mergify[bot]
62aa1cdb33 fix: incorrect valuation rate due to positive qty (backport #47686) (#47688)
fix: incorrect valuation rate due to positive qty (#47686)

(cherry picked from commit 6ed97b5fda)

Co-authored-by: Khushi Rawat <142375893+khushi8112@users.noreply.github.com>
2025-05-22 16:17:18 +05:30
ljain112
3d2d1ba072 fix: typo in TREE_DOCTYPES list "Terrirtory" should be "Territory"
(cherry picked from commit 51162cb1a3)
2025-05-22 10:27:05 +00:00
Mihir Kandoi
f3bc80c89d Merge pull request #47678 from frappe/mergify/bp/version-15-hotfix/pr-47658
fix: exchange rate not being fetched when creating supplier quotation… (backport #47658)
2025-05-22 14:36:37 +05:30
Mihir Kandoi
6892994ef6 Merge pull request #47677 from frappe/mergify/bp/version-15-hotfix/pr-47572
fix: include rejected amount in PI/PR overbilling validation logic (backport #47572)
2025-05-22 14:36:19 +05:30
Mihir Kandoi
2c22615b6b fix: exchange rate not being fetched when creating supplier quotation from MR
(cherry picked from commit 9d12ae071a)
2025-05-22 07:26:50 +00:00
Mihir Kandoi
cd1c10a43f fix: include rejected amount in PI/PR overbilling validation logic (#47572)
* fix: include rejected amount in PI/PR overbilling validation logic

* fix: add check if amount is 0

* fix: unneccessary condition

(cherry picked from commit 8d9888b1b6)
2025-05-22 07:23:46 +00:00
mergify[bot]
db318a4e9b fix: pos invoice status not updating on cancel (backport #47556) (#47657)
fix: pos invoice status not updating on cancel (#47556)

(cherry picked from commit 8c86def018)

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-05-21 15:09:48 +05:30
mergify[bot]
9a78283ecb fix: incorrect inventory dimension for material transfer (backport #47592) (#47644)
fix: incorrect inventory dimension for material transfer (#47592)

(cherry picked from commit 738cb6a0c1)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-05-20 20:46:57 +05:30
Frappe PR Bot
52a5cd9702 chore(release): Bumped to Version 15.62.0
# [15.62.0](https://github.com/frappe/erpnext/compare/v15.61.1...v15.62.0) (2025-05-20)

### Bug Fixes

* alias name and parent to prevent child row mapping issues ([612fa7c](612fa7c672))
* allow FG as RM by default (backport [#47543](https://github.com/frappe/erpnext/issues/47543)) ([#47550](https://github.com/frappe/erpnext/issues/47550)) ([9355782](9355782397))
* asset cancellation issue (backport [#47639](https://github.com/frappe/erpnext/issues/47639)) ([#47641](https://github.com/frappe/erpnext/issues/47641)) ([ce9da48](ce9da48a5e))
* asset image field updation issue (backport [#47615](https://github.com/frappe/erpnext/issues/47615)) ([#47617](https://github.com/frappe/erpnext/issues/47617)) ([35c7af1](35c7af1b9d))
* better validation message with solution for BOM recursion (backport [#47472](https://github.com/frappe/erpnext/issues/47472)) ([#47477](https://github.com/frappe/erpnext/issues/47477)) ([a450f4c](a450f4ce64))
* Broken test + use `super()` appropriately ([5b50d5a](5b50d5abf2))
* Conflicts ([9cede83](9cede83de1))
* correct expense amount in party ledger summary. ([09a46fc](09a46fcf0e))
* date formatting in process_statement_of_accounts accounts_receivable print format ([cf354c0](cf354c0da3))
* include only invoices with update_stock = 0  for billed amt in delivery note. ([70e190d](70e190dbbb))
* incorrect qty during reset (backport [#47593](https://github.com/frappe/erpnext/issues/47593)) ([#47595](https://github.com/frappe/erpnext/issues/47595)) ([72ae80e](72ae80e2e3))
* mapping of dispatch address when creating PO from SO (backport [#47552](https://github.com/frappe/erpnext/issues/47552)) ([#47553](https://github.com/frappe/erpnext/issues/47553)) ([30ec69c](30ec69c977))
* POS Invoice can't use Loyalty Points when Global Rounded Total is Disabled (backport [#47491](https://github.com/frappe/erpnext/issues/47491)) ([#47564](https://github.com/frappe/erpnext/issues/47564)) ([926c0c5](926c0c5cf4))
* pos item group filter fetching wrong items (backport [#47545](https://github.com/frappe/erpnext/issues/47545)) ([#47546](https://github.com/frappe/erpnext/issues/47546)) ([5a3eff0](5a3eff05a1))
* **quotation:** use `Text Editor` field in alternative items dialog for item description ([32eeeda](32eeedac24))
* remove hardcoded doctype in `make_return_doc` ([1a69d81](1a69d8137f))
* removed invalid child param to prevent callback failure ([073d06c](073d06c44f))
* **SalesAnalytics:** Ignore opening entries ([be280a4](be280a408e))
* set no_copy to party_balance field in Payment Entry ([da4ed5c](da4ed5cc18))
* set no_copy to party_balance field in Payment Entry ([52cab02](52cab02a5c))
* validate inter-company transaction address links ([86aa072](86aa072235))
* validation message format (backport [#47542](https://github.com/frappe/erpnext/issues/47542)) ([#47549](https://github.com/frappe/erpnext/issues/47549)) ([f225e19](f225e1986e))

### Features

* add checbox for validating time logs in job card ([80c7661](80c76618ae))
* add option to calculate ageing based on report date or today date ([69337cf](69337cf18b))
2025-05-20 13:54:53 +00:00
ruthra kumar
f3052a446f Merge pull request #47636 from frappe/version-15-hotfix
chore: release v15
2025-05-20 19:23:12 +05:30
mergify[bot]
ce9da48a5e fix: asset cancellation issue (backport #47639) (#47641)
fix: asset cancellation issue (#47639)

(cherry picked from commit 33ab64dec2)

Co-authored-by: Khushi Rawat <142375893+khushi8112@users.noreply.github.com>
2025-05-20 17:21:48 +05:30
mergify[bot]
a450f4ce64 fix: better validation message with solution for BOM recursion (backport #47472) (#47477)
fix: better validation message with solution for BOM recursion

(cherry picked from commit 7103cdd84a)

Co-authored-by: Rohit Waghchaure <rohitw1991@gmail.com>
2025-05-20 16:15:23 +05:30
ruthra kumar
a029f2e8a3 Merge pull request #47633 from frappe/mergify/bp/version-15-hotfix/pr-47632
fix(quotation): use `Text Editor` field in alternative items dialog (backport #47632)
2025-05-20 14:25:10 +05:30
Akhil Narang
32eeedac24 fix(quotation): use Text Editor field in alternative items dialog for item description
`Data` causes text to overflow - the field is originally a `Text Editor` field

Signed-off-by: Akhil Narang <me@akhilnarang.dev>
(cherry picked from commit c7ea91073e)
2025-05-20 08:52:05 +00:00
ruthra kumar
ecb2bab70f Merge pull request #47631 from frappe/mergify/bp/version-15-hotfix/pr-47629
fix: date formatting in process_statement_of_accounts accounts_receivable print format (backport #47629)
2025-05-20 14:06:20 +05:30
ljain112
cf354c0da3 fix: date formatting in process_statement_of_accounts accounts_receivable print format
(cherry picked from commit 67c32ce3c9)

# Conflicts:
#	erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts_accounts_receivable.html
2025-05-20 13:57:47 +05:30
ruthra kumar
73fc8c374f Merge pull request #47628 from frappe/mergify/bp/version-15-hotfix/pr-47580
feat: add option to calculate ageing based on report date or today's date (backport #47580)
2025-05-20 13:53:04 +05:30
ruthra kumar
8c4ce03f44 Merge pull request #47623 from frappe/mergify/bp/version-15-hotfix/pr-47486
fix(SalesAnalytics): Ignore opening entries (backport #47486)
2025-05-20 13:21:36 +05:30
l0gesh29
69337cf18b feat: add option to calculate ageing based on report date or today date
(cherry picked from commit c67ba2d49b)
2025-05-20 07:48:39 +00:00
ruthra kumar
b773b494a0 Merge pull request #47625 from frappe/mergify/bp/version-15-hotfix/pr-47559
fix: include only invoices with update_stock = 0  for billed amt in delivery note. (backport #47559)
2025-05-20 11:25:06 +05:30
ljain112
70e190dbbb fix: include only invoices with update_stock = 0 for billed amt in delivery note.
(cherry picked from commit 6dc459db58)
2025-05-20 05:31:56 +00:00
l0gesh29
be280a408e fix(SalesAnalytics): Ignore opening entries
(cherry picked from commit 6d269b4409)
2025-05-20 05:20:43 +00:00
ruthra kumar
9584c80857 Merge pull request #47622 from frappe/mergify/bp/version-15-hotfix/pr-47614
fix: remove hardcoded doctype in `make_return_doc` (backport #47614)
2025-05-20 10:14:57 +05:30
barredterra
1a69d8137f fix: remove hardcoded doctype in make_return_doc
(cherry picked from commit 45a5c19dd4)
2025-05-20 04:30:11 +00:00
mergify[bot]
35c7af1b9d fix: asset image field updation issue (backport #47615) (#47617)
fix: asset image field updation issue (#47615)

(cherry picked from commit ff2ccf9bce)

Co-authored-by: Khushi Rawat <142375893+khushi8112@users.noreply.github.com>
2025-05-20 01:28:45 +05:30
ruthra kumar
da4ed5cc18 fix: set no_copy to party_balance field in Payment Entry 2025-05-19 16:15:16 +05:30
ruthra kumar
927d0f686f Merge pull request #47600 from frappe/mergify/bp/version-15-hotfix/pr-47505
fix: validate inter company transaction address links (backport #47505)
2025-05-19 13:25:08 +05:30
Bhavan23
8ee9a46d96 test: add test case to validate inter-company transaction address links
(cherry picked from commit 0caa757dd6)
2025-05-19 07:39:50 +00:00
Bhavan23
86aa072235 fix: validate inter-company transaction address links
(cherry picked from commit aed46ad5b9)
2025-05-19 07:39:50 +00:00
mergify[bot]
72ae80e2e3 fix: incorrect qty during reset (backport #47593) (#47595)
fix: incorrect qty during reset (#47593)

(cherry picked from commit a058fe7319)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-05-19 12:02:29 +05:30
Mihir Kandoi
829550cd99 Merge pull request #47577 from frappe/mergify/bp/version-15-hotfix/pr-47570
feat: add checkbox for validating time logs in job card (backport #47570)
2025-05-17 00:34:56 +05:30
Mihir Kandoi
249d18962c chore: resolve conflicts 2025-05-17 00:08:39 +05:30
Khushi Rawat
b9f12ed4c7 Merge pull request #47576 from frappe/mergify/bp/version-15-hotfix/pr-47573
fix: alias 'name' and 'parent' to prevent child row mapping issues (backport #47573)
2025-05-16 15:52:37 +05:30
Mihir Kandoi
80c76618ae feat: add checbox for validating time logs in job card
(cherry picked from commit 2d9a6a4de8)

# Conflicts:
#	erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json
2025-05-16 10:05:52 +00:00
Khushi Rawat
073d06c44f fix: removed invalid child param to prevent callback failure
(cherry picked from commit 1ca51e4f14)
2025-05-16 10:05:51 +00:00
Khushi Rawat
612fa7c672 fix: alias name and parent to prevent child row mapping issues
(cherry picked from commit a418e377f4)
2025-05-16 10:05:51 +00:00
marination
e7dc31191c Merge branch 'version-15-hotfix' into mergify/bp/version-15-hotfix/pr-46214 2025-05-15 18:50:58 +02:00
mergify[bot]
926c0c5cf4 fix: POS Invoice can't use Loyalty Points when Global Rounded Total is Disabled (backport #47491) (#47564)
fix: POS Invoice can't use Loyalty Points when Global Rounded Total is Disabled (#47491)

(cherry picked from commit b541b536c3)

Co-authored-by: Kitti U. @ Ecosoft <kittiu@gmail.com>
2025-05-15 19:35:03 +05:30
ljain112
52cab02a5c fix: set no_copy to party_balance field in Payment Entry 2025-05-15 18:03:25 +05:30
Frappe PR Bot
31fa1c9a58 chore(release): Bumped to Version 15.61.1
## [15.61.1](https://github.com/frappe/erpnext/compare/v15.61.0...v15.61.1) (2025-05-15)

### Bug Fixes

* correct expense amount in party ledger summary. ([67741f1](67741f1a21))
2025-05-15 06:05:18 +00:00
ruthra kumar
631a9bfa7c Merge pull request #47557 from frappe/mergify/bp/version-15/pr-47541
fix: correct expense amount in party ledger summary. (backport #47541)
2025-05-15 11:33:53 +05:30
ljain112
67741f1a21 fix: correct expense amount in party ledger summary.
(cherry picked from commit 09a46fcf0e)
2025-05-15 05:47:32 +00:00
mergify[bot]
f225e1986e fix: validation message format (backport #47542) (#47549)
fix: validation message format (#47542)

(cherry picked from commit a18e1cffa7)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-05-15 10:31:28 +05:30
mergify[bot]
9355782397 fix: allow FG as RM by default (backport #47543) (#47550)
fix: allow FG as RM by default (#47543)

(cherry picked from commit 4241bfd4bc)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2025-05-15 10:31:12 +05:30
mergify[bot]
30ec69c977 fix: mapping of dispatch address when creating PO from SO (backport #47552) (#47553)
fix: mapping of dispatch address when creating PO from SO (#47552)

* fix: mapping of dispatch address when creating PO from SO

* fix: add to default supplier function as well

(cherry picked from commit 82161e9cb5)

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2025-05-14 20:52:04 +05:30
ruthra kumar
b22831bd94 Merge pull request #47541 from ljain112/fix-cls
fix: correct expense amount in party ledger summary.
2025-05-14 17:56:22 +05:30
mergify[bot]
5a3eff05a1 fix: pos item group filter fetching wrong items (backport #47545) (#47546)
fix: pos item group filter fetching wrong items (#47545)

(cherry picked from commit 5c28e01590)

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-05-14 17:41:36 +05:30
ljain112
09a46fcf0e fix: correct expense amount in party ledger summary. 2025-05-14 12:38:38 +05:30
marination
37f4cf5367 fix: Linter (due to conflicts resolved on gh) 2025-05-13 17:18:40 +02:00
marination
f95a3f5b8b fix(portal): User cannot create 0 qty SQ from RFQ
- The portal uses `create_supplier_quotation` for SQ creation which excludes 0 qty items
2025-05-13 17:13:41 +02:00
marination
0286788e97 chore: Relabel according to review changes 2025-05-13 17:13:41 +02:00
marination
8891f46a22 fix: Relabel unit price settings for more clarity 2025-05-13 17:13:41 +02:00
Marica
890ce4a676 Merge branch 'version-15-hotfix' into mergify/bp/version-15-hotfix/pr-46214 2025-05-13 17:07:43 +02:00
Marica
2960d0dce1 Merge pull request #47537 from frappe/mergify/bp/version-15-hotfix/pr-38530
refactor: Consolidate duplicate zero-quantity Items checks for transactions. (backport #38530)
2025-05-13 16:55:54 +02:00
marination
5b50d5abf2 fix: Broken test + use super() appropriately
- test: Remove `test_bom_qty`. It had invalid code. Its been removed from develop. There wasn't a strong case being tested.
2025-05-13 14:42:57 +02:00
marination
9cede83de1 fix: Conflicts 2025-05-13 14:26:54 +02:00
Bernd Oliver Sünderhauf
a8b982dd0a chore: Adapt translations to reworded message.
(cherry picked from commit 3688d9412e)

# Conflicts:
#	erpnext/translations/tr.csv
2025-05-13 12:13:56 +00:00
Bernd Oliver Sünderhauf
cf45ffdabe refactor: Consolidate duplicate zero-quantity transaction Items checks.
(cherry picked from commit 4918aeb4c6)

# Conflicts:
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2025-05-13 12:13:47 +00:00
Bernd Oliver Sünderhauf
e91a0acbb3 test: Add, expand and refine test-cases for zero-quantity transactions.
(cherry picked from commit b2d8a44199)

# Conflicts:
#	erpnext/selling/doctype/sales_order/test_sales_order.py
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2025-05-13 12:13:46 +00:00
marination
3deb11e5b2 fix: Merge conflicts 2025-05-13 13:35:19 +02:00
marination
f94a14c06a fix: Headline rendered twice on first save
- `refresh` gets triggered twice and that renders the note twice
- Remove any existing note before rendering

(cherry picked from commit bf62f9ad57)
2025-05-05 16:59:18 +00:00
marination
d9636018f5 fix: Treat rows as Unit Price rows only until the qty is 0
- The unit price check should depend on the row qty being 0
- Once the row ceases to be 0, it is treated as an ordinary row
- test: PO, SO and Quotation

(cherry picked from commit 0447c7be0a)

# Conflicts:
#	erpnext/selling/doctype/quotation/test_quotation.py
#	erpnext/selling/doctype/sales_order/test_sales_order.py
2025-05-05 16:59:17 +00:00
marination
2d96a62530 test: Sales Order + fix: Mapping of Items from Quotation & SO
(cherry picked from commit 55981c8358)

# Conflicts:
#	erpnext/selling/doctype/sales_order/test_sales_order.py
2025-05-05 16:59:17 +00:00
marination
eba73df88e test: Purchase Order with Unit Price Items
- chore: Fix error message in accounts controller

(cherry picked from commit eea758f5b2)

# Conflicts:
#	erpnext/buying/doctype/purchase_order/test_purchase_order.py
#	erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py
2025-05-05 16:59:16 +00:00
marination
c19065e675 test: Zero Qty in RFQ and Supplier Quotation
(cherry picked from commit 8f96c0b546)

# Conflicts:
#	erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
#	erpnext/buying/doctype/supplier_quotation/test_supplier_quotation.py
2025-05-05 16:59:16 +00:00
marination
f8fa775af3 feat: Unit Price Items in Buying (RFQ, SQ, PO)
- chore: Extract `set_unit_price_items_note` into a util

(cherry picked from commit e403d3f153)

# Conflicts:
#	erpnext/buying/doctype/buying_settings/buying_settings.json
#	erpnext/buying/doctype/purchase_order/purchase_order.json
#	erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
#	erpnext/selling/doctype/quotation/quotation.json
#	erpnext/selling/doctype/selling_settings/selling_settings.json
2025-05-05 16:59:16 +00:00
marination
91e167fe72 fix: Linters
(cherry picked from commit 71f65bab5e)

# Conflicts:
#	erpnext/selling/doctype/sales_order/sales_order.py
2025-05-05 16:59:15 +00:00
marination
33366fce6c feat: Unit Price Contract
(cherry picked from commit c1e4e7af28)

# Conflicts:
#	erpnext/controllers/accounts_controller.py
#	erpnext/selling/doctype/quotation/quotation.json
#	erpnext/selling/doctype/sales_order/sales_order.py
#	erpnext/selling/doctype/selling_settings/selling_settings.json
2025-05-05 16:59:15 +00:00
227 changed files with 5760 additions and 3366 deletions

View File

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

View File

@@ -38,8 +38,14 @@
"show_taxes_as_table_in_print",
"column_break_12",
"show_payment_schedule_in_print",
"item_price_settings_section",
"maintain_same_internal_transaction_rate",
"column_break_feyo",
"maintain_same_rate_action",
"role_to_override_stop_action",
"currency_exchange_section",
"allow_stale",
"allow_pegged_currencies_exchange_rates",
"column_break_yuug",
"stale_days",
"section_break_jpd0",
@@ -540,13 +546,6 @@
"fieldname": "column_break_xrnd",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "If enabled, Sales Invoice will be generated instead of POS Invoice in POS Transactions for real-time update of G/L and Stock Ledger.",
"fieldname": "use_sales_invoice_in_pos",
"fieldtype": "Check",
"label": "Use Sales Invoice"
},
{
"default": "Buffered Cursor",
"fieldname": "receivable_payable_fetch_method",
@@ -563,6 +562,44 @@
"fieldname": "legacy_section",
"fieldtype": "Section Break",
"label": "Legacy Fields"
},
{
"default": "0",
"fieldname": "maintain_same_internal_transaction_rate",
"fieldtype": "Check",
"label": "Maintain Same Rate Throughout Internal Transaction"
},
{
"default": "Stop",
"depends_on": "maintain_same_internal_transaction_rate",
"fieldname": "maintain_same_rate_action",
"fieldtype": "Select",
"label": "Action if Same Rate is Not Maintained Throughout Internal Transaction",
"mandatory_depends_on": "maintain_same_internal_transaction_rate",
"options": "Stop\nWarn"
},
{
"depends_on": "eval: doc.maintain_same_internal_transaction_rate && doc.maintain_same_rate_action == 'Stop'",
"fieldname": "role_to_override_stop_action",
"fieldtype": "Link",
"label": "Role Allowed to Override Stop Action",
"options": "Role"
},
{
"fieldname": "item_price_settings_section",
"fieldtype": "Section Break",
"label": "Item Price Settings"
},
{
"fieldname": "column_break_feyo",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Enable this field to fetch the exchange rates for Pegged Currencies.\n\n",
"fieldname": "allow_pegged_currencies_exchange_rates",
"fieldtype": "Check",
"label": "Allow Pegged Currencies Exchange Rates"
}
],
"icon": "icon-cog",
@@ -570,7 +607,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-05-05 12:29:38.302027",
"modified": "2025-06-16 16:40:54.871486",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@@ -599,4 +636,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
}
}

View File

@@ -26,6 +26,7 @@ class AccountsSettings(Document):
acc_frozen_upto: DF.Date | None
add_taxes_from_item_tax_template: DF.Check
allow_multi_currency_invoices_against_single_party_account: DF.Check
allow_pegged_currencies_exchange_rates: DF.Check
allow_stale: DF.Check
auto_reconcile_payments: DF.Check
auto_reconciliation_job_trigger: DF.Int
@@ -50,6 +51,8 @@ class AccountsSettings(Document):
general_ledger_remarks_length: DF.Int
ignore_account_closing_balance: DF.Check
ignore_is_opening_check_for_reporting: DF.Check
maintain_same_internal_transaction_rate: DF.Check
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
make_payment_via_journal_entry: DF.Check
merge_similar_account_heads: DF.Check
over_billing_allowance: DF.Currency
@@ -58,6 +61,7 @@ class AccountsSettings(Document):
receivable_payable_remarks_length: DF.Int
reconciliation_queue_size: DF.Int
role_allowed_to_over_bill: DF.Link | None
role_to_override_stop_action: DF.Link | None
round_row_wise_tax: DF.Check
show_balance_in_coa: DF.Check
show_inclusive_tax_in_print: DF.Check

View File

@@ -277,7 +277,7 @@ def get_import_status(docname):
@frappe.whitelist()
def get_import_logs(docname: str):
frappe.has_permission("Bank Statement Import")
frappe.has_permission("Bank Statement Import", throw=True)
return frappe.get_all(
"Data Import Log",

View File

@@ -1,6 +1,7 @@
import frappe
from frappe.utils import flt
from rapidfuzz import fuzz, process
from rapidfuzz.utils import default_process
class AutoMatchParty:
@@ -132,6 +133,7 @@ class AutoMatchbyPartyNameDescription:
query=self.get(field),
choices={row.get("name"): row.get("party_name") for row in names},
scorer=fuzz.token_set_ratio,
processor=default_process,
)
party_name, skip = self.process_fuzzy_result(result)

View File

@@ -7,10 +7,10 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"budget_against",
"company",
"cost_center",
"naming_series",
"project",
"fiscal_year",
"column_break_3",
@@ -195,19 +195,19 @@
},
{
"fieldname": "naming_series",
"fieldtype": "Data",
"hidden": 1,
"fieldtype": "Select",
"label": "Series",
"no_copy": 1,
"options": "BUDGET-.YYYY.-",
"print_hide": 1,
"read_only": 1,
"reqd": 1,
"set_only_once": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-10-10 22:14:36.361509",
"modified": "2025-06-16 15:57:13.114981",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget",
@@ -235,4 +235,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -48,7 +48,7 @@ class Budget(Document):
cost_center: DF.Link | None
fiscal_year: DF.Link
monthly_distribution: DF.Link | None
naming_series: DF.Data | None
naming_series: DF.Literal["BUDGET-.YYYY.-"]
project: DF.Link | None
# end: auto-generated types
@@ -136,9 +136,6 @@ class Budget(Document):
):
self.applicable_on_booking_actual_expenses = 1
def before_naming(self):
self.naming_series = f"{{{frappe.scrub(self.budget_against)}}}./.{self.fiscal_year}/.###"
def validate_expense_against_budget(args, expense_amount=0):
args = frappe._dict(args)

View File

@@ -7,7 +7,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.model.naming import set_name_from_naming_options
from frappe.utils import flt, fmt_money, now
from frappe.utils import create_batch, flt, fmt_money, now
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -451,12 +451,15 @@ def rename_gle_sle_docs():
def rename_temporarily_named_docs(doctype):
"""Rename temporarily named docs using autoname options"""
docs_to_rename = frappe.get_all(doctype, {"to_rename": "1"}, order_by="creation", limit=50000)
for doc in docs_to_rename:
oldname = doc.name
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
newname = doc.name
frappe.db.sql(
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
(newname, now(), oldname),
auto_commit=True,
)
autoname = frappe.get_meta(doctype).autoname
for batch in create_batch(docs_to_rename, 100):
for doc in batch:
oldname = doc.name
set_name_from_naming_options(autoname, doc)
newname = doc.name
frappe.db.sql(
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0, modified = %s where name = %s",
(newname, now(), oldname),
)
frappe.db.commit()

View File

@@ -168,8 +168,9 @@ def validate_loyalty_points(ref_doc, points_to_redeem):
loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor)
if loyalty_amount > ref_doc.rounded_total:
frappe.throw(_("You can't redeem Loyalty Points having more value than the Rounded Total."))
total_amount = ref_doc.grand_total if ref_doc.is_rounded_total_disabled() else ref_doc.rounded_total
if loyalty_amount > total_amount:
frappe.throw(_("You can't redeem Loyalty Points having more value than the Total Amount."))
if not ref_doc.loyalty_amount and ref_doc.loyalty_amount != loyalty_amount:
ref_doc.loyalty_amount = loyalty_amount

View File

@@ -228,6 +228,7 @@
"fieldname": "party_balance",
"fieldtype": "Currency",
"label": "Party Balance",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
@@ -799,7 +800,7 @@
"table_fieldname": "payment_entries"
}
],
"modified": "2025-05-08 11:18:10.238085",
"modified": "2025-05-15 18:01:04.013025",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

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

View File

@@ -0,0 +1,47 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-05-30 11:47:03.670913",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"pegged_currencies_item_section",
"pegged_currency_item"
],
"fields": [
{
"fieldname": "pegged_currencies_item_section",
"fieldtype": "Section Break"
},
{
"fieldname": "pegged_currency_item",
"fieldtype": "Table",
"options": "Pegged Currency Details"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-06-02 11:46:31.936714",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pegged Currencies",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,22 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PeggedCurrencies(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.pegged_currencies.pegged_currencies import PeggedCurrencies
pegged_currency_item: DF.Table[PeggedCurrencies]
# end: auto-generated types
pass

View File

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

View File

@@ -0,0 +1,49 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-05-30 11:59:28.219277",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"source_currency",
"pegged_against",
"pegged_exchange_rate"
],
"fields": [
{
"fieldname": "source_currency",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "pegged_exchange_rate",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Exchange Rate"
},
{
"fieldname": "pegged_against",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Pegged Against",
"options": "Currency"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-06-17 14:11:16.521193",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pegged Currency Details",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,25 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PeggedCurrencyDetails(Document):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from frappe.types import DF
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
pegged_against: DF.Link | None
pegged_exchange_rate: DF.Data | None
source_currency: DF.Link | None
# end: auto-generated types
pass

View File

@@ -273,6 +273,8 @@ class POSInvoice(SalesInvoice):
against_psi_doc.delete_loyalty_point_entry()
against_psi_doc.make_loyalty_point_entry()
self.db_set("status", "Cancelled")
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count

View File

@@ -258,6 +258,7 @@ class POSInvoiceMergeLog(Document):
if not found:
tax.charge_type = "Actual"
tax.idx = idx
tax.row_id = None
idx += 1
tax.included_in_print_rate = 0
tax.tax_amount = tax.tax_amount_after_discount_amount

View File

@@ -60,6 +60,20 @@ frappe.ui.form.on("Process Statement Of Accounts", {
},
};
});
frm.set_query("cost_center", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
frm.set_query("project", function () {
return {
filters: {
company: frm.doc.company,
},
};
});
if (frm.doc.__islocal) {
frm.set_value("from_date", frappe.datetime.add_months(frappe.datetime.get_today(), -1));
frm.set_value("to_date", frappe.datetime.get_today());

View File

@@ -129,8 +129,8 @@ def get_statement_dict(doc, get_statement_dict=False):
tax_id = frappe.get_doc("Customer", entry.customer).tax_id
presentation_currency = (
get_party_account_currency("Customer", entry.customer, doc.company)
or doc.currency
doc.currency
or get_party_account_currency("Customer", entry.customer, doc.company)
or get_company_currency(doc.company)
)

View File

@@ -211,7 +211,7 @@
<tr>
{% if(report.report_name == "Accounts Receivable" or report.report_name == "Accounts Payable") %}
{% if(data[i]["party"]) %}
<td>{{ (data[i]["posting_date"]) }}</td>
<td>{{ frappe.format((data[i]["posting_date"]), 'Date') }}</td>
<td style="text-align: right">{{ data[i]["age"] }}</td>
<td>
{% if not(filters.show_future_payments) %}

View File

@@ -97,6 +97,7 @@ def create_process_soa(**args):
company=args.company or "_Test Company",
customers=args.customers or [{"customer": "_Test Customer"}],
enable_auto_email=1 if args.enable_auto_email else 0,
currency=args.currency or "",
frequency=args.frequency or "Weekly",
report=args.report or "General Ledger",
from_date=args.from_date or getdate(today()),

View File

@@ -425,6 +425,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
this.frm.set_value("is_paid", 0);
frappe.msgprint(__("Please specify Company to proceed"));
}
} else {
this.frm.set_value("paid_amount", 0);
}
this.calculate_outstanding_amount();
this.frm.refresh_fields();

View File

@@ -16,7 +16,7 @@ from erpnext.buying.doctype.purchase_order.test_purchase_order import (
create_purchase_order,
)
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.controllers.accounts_controller import InvalidQtyError, get_payment_terms
from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project
@@ -55,6 +55,16 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
def tearDown(self):
frappe.db.rollback()
def test_purchase_invoice_qty(self):
pi = make_purchase_invoice(qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
pi.save()
# No error with qty=1
pi.items[0].qty = 1
pi.save()
self.assertEqual(pi.items[0].qty, 1)
def test_purchase_invoice_received_qty(self):
"""
1. Test if received qty is validated against accepted + rejected
@@ -2796,6 +2806,17 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
# Test 4 - Since this PI is overbilled by 130% and only 120% is allowed, it will fail
self.assertRaises(frappe.ValidationError, pi.submit)
def test_discount_percentage_not_set_when_amount_is_manually_set(self):
pi = make_purchase_invoice(do_not_save=True)
discount_amount = 7
pi.discount_amount = discount_amount
pi.save()
self.assertEqual(pi.additional_discount_percentage, None)
pi.set_posting_time = 1
pi.posting_date = add_days(today(), -1)
pi.save()
self.assertEqual(pi.discount_amount, discount_amount)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(
@@ -2905,7 +2926,7 @@ def make_purchase_invoice(**args):
bundle_id = None
if not args.use_serial_batch_fields and (args.get("batch_no") or args.get("serial_no")):
batches = {}
qty = args.qty or 5
qty = args.qty if args.qty is not None else 5
item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
@@ -2934,7 +2955,7 @@ def make_purchase_invoice(**args):
"item_code": args.item or args.item_code or "_Test Item",
"item_name": args.item_name,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 5,
"qty": args.qty if args.qty is not None else 5,
"received_qty": args.received_qty or 0,
"rejected_qty": args.rejected_qty or 0,
"rate": args.rate or 50,

View File

@@ -2286,6 +2286,18 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
set_purchase_references(target)
def update_details(source_doc, target_doc, source_parent):
def _validate_address_link(address, link_doctype, link_name):
return frappe.db.get_value(
"Dynamic Link",
{
"parent": address,
"parenttype": "Address",
"link_doctype": link_doctype,
"link_name": link_name,
},
"parent",
)
target_doc.inter_company_invoice_reference = source_doc.name
if target_doc.doctype in ["Purchase Invoice", "Purchase Order"]:
currency = frappe.db.get_value("Supplier", details.get("party"), "default_currency")
@@ -2296,16 +2308,34 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
target_doc.buying_price_list = source_doc.selling_price_list
# Invert Addresses
update_address(target_doc, "supplier_address", "address_display", source_doc.company_address)
update_address(
target_doc, "dispatch_address", "dispatch_address_display", source_doc.dispatch_address_name
)
update_address(
target_doc, "shipping_address", "shipping_address_display", source_doc.shipping_address_name
)
update_address(
target_doc, "billing_address", "billing_address_display", source_doc.customer_address
)
if source_doc.company_address and _validate_address_link(
source_doc.company_address, "Supplier", details.get("party")
):
update_address(target_doc, "supplier_address", "address_display", source_doc.company_address)
if source_doc.dispatch_address_name and _validate_address_link(
source_doc.dispatch_address_name, "Company", details.get("company")
):
update_address(
target_doc,
"dispatch_address",
"dispatch_address_display",
source_doc.dispatch_address_name,
)
if source_doc.shipping_address_name and _validate_address_link(
source_doc.shipping_address_name, "Company", details.get("company")
):
update_address(
target_doc,
"shipping_address",
"shipping_address_display",
source_doc.shipping_address_name,
)
if source_doc.customer_address and _validate_address_link(
source_doc.customer_address, "Company", details.get("company")
):
update_address(
target_doc, "billing_address", "billing_address_display", source_doc.customer_address
)
if currency:
target_doc.currency = currency
@@ -2326,13 +2356,22 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
target_doc.customer = details.get("party")
target_doc.selling_price_list = source_doc.buying_price_list
update_address(
target_doc, "company_address", "company_address_display", source_doc.supplier_address
)
update_address(
target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address
)
update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address)
if source_doc.supplier_address and _validate_address_link(
source_doc.supplier_address, "Company", details.get("company")
):
update_address(
target_doc, "company_address", "company_address_display", source_doc.supplier_address
)
if source_doc.shipping_address and _validate_address_link(
source_doc.shipping_address, "Customer", details.get("party")
):
update_address(
target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address
)
if source_doc.shipping_address and _validate_address_link(
source_doc.shipping_address, "Customer", details.get("party")
):
update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address)
if currency:
target_doc.currency = currency

View File

@@ -27,7 +27,7 @@ from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_d
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
)
from erpnext.controllers.accounts_controller import update_invoice_status
from erpnext.controllers.accounts_controller import InvalidQtyError, update_invoice_status
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.selling.doctype.customer.test_customer import get_customer_dict
@@ -64,6 +64,26 @@ class TestSalesInvoice(FrappeTestCase):
)
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
@change_settings(
"Accounts Settings",
{"maintain_same_internal_transaction_rate": 1, "maintain_same_rate_action": "Stop"},
)
def test_invalid_rate_without_override(self):
from frappe import ValidationError
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_purchase_invoice
si = create_sales_invoice(
customer="_Test Internal Customer 3", company="_Test Company", is_internal_customer=1, rate=100
)
pi = make_inter_company_purchase_invoice(si.name)
pi.items[0].rate = 120
with self.assertRaises(ValidationError) as e:
pi.insert()
pi.submit()
self.assertIn("Rate must be same", str(e.exception))
def tearDown(self):
frappe.db.rollback()
@@ -82,6 +102,16 @@ class TestSalesInvoice(FrappeTestCase):
def tearDownClass(self):
unlink_payment_on_cancel_of_invoice(0)
def test_sales_invoice_qty(self):
si = create_sales_invoice(qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
si.save()
# No error with qty=1
si.items[0].qty = 1
si.save()
self.assertEqual(si.items[0].qty, 1)
def test_timestamp_change(self):
w = frappe.copy_doc(test_records[0])
w.docstatus = 0
@@ -2571,6 +2601,62 @@ class TestSalesInvoice(FrappeTestCase):
acc_settings.book_deferred_entries_based_on = "Days"
acc_settings.save()
def test_validate_inter_company_transaction_address_links(self):
def _validate_address_link(address, link_doctype, link_name):
return frappe.db.get_value(
"Dynamic Link",
{
"parent": address,
"parenttype": "Address",
"link_doctype": link_doctype,
"link_name": link_name,
},
"parent",
)
si = create_sales_invoice(
company="Wind Power LLC",
customer="_Test Internal Customer",
debit_to="Debtors - WP",
warehouse="Stores - WP",
income_account="Sales - WP",
expense_account="Cost of Goods Sold - WP",
cost_center="Main - WP",
currency="USD",
do_not_save=1,
)
si.selling_price_list = "_Test Price List Rest of the World"
si.submit()
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
target_doc.items[0].update(
{
"expense_account": "Cost of Goods Sold - _TC1",
"cost_center": "Main - _TC1",
"warehouse": "Stores - _TC1",
}
)
target_doc.save()
if target_doc.doctype in ["Purchase Invoice", "Purchase Order"]:
for details in [
("supplier_address", "Supplier", target_doc.supplier),
("dispatch_address", "Company", target_doc.company),
("shipping_address", "Company", target_doc.company),
("billing_address", "Company", target_doc.company),
]:
if address := target_doc.get(details[0]):
self.assertEqual(address, _validate_address_link(address, details[1], details[2]))
else:
for details in [
("company_address", "Company", target_doc.company),
("shipping_address_name", "Customer", target_doc.customer),
("customer_address", "Customer", target_doc.customer),
]:
if address := target_doc.get(details[0]):
self.assertEqual(address, _validate_address_link(address, details[1], details[2]))
def test_inter_company_transaction(self):
si = create_sales_invoice(
company="Wind Power LLC",
@@ -4375,11 +4461,12 @@ def create_sales_invoice(**args):
si.conversion_rate = args.conversion_rate or 1
si.naming_series = args.naming_series or "T-SINV-"
si.cost_center = args.parent_cost_center
si.is_internal_customer = args.is_internal_customer or 0
bundle_id = None
if si.update_stock and (args.get("batch_no") or args.get("serial_no")):
batches = {}
qty = args.qty or 1
qty = args.qty if args.qty is not None else 1
item_code = args.item or args.item_code or "_Test Item"
if args.get("batch_no"):
batches = frappe._dict({args.batch_no: qty})
@@ -4411,7 +4498,7 @@ def create_sales_invoice(**args):
"description": args.description or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"target_warehouse": args.target_warehouse,
"qty": args.qty or 1,
"qty": args.qty if args.qty is not None else 1,
"uom": args.uom or "Nos",
"stock_uom": args.uom or "Nos",
"rate": args.rate if args.get("rate") is not None else 100,
@@ -4577,6 +4664,12 @@ def create_internal_parties():
allowed_to_interact_with="_Test Company with perpetual inventory",
)
create_internal_supplier(
supplier_name="_Test Internal Supplier 3",
represents_company="_Test Company",
allowed_to_interact_with="_Test Company",
)
def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with):
if not frappe.db.exists("Supplier", supplier_name):

View File

@@ -671,6 +671,7 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
conditions.append(ple.party.isin(parties))
conditions.append(ple.voucher_no == ple.against_voucher_no)
conditions.append(ple.company == inv.company)
conditions.append(ple.posting_date[tax_details.from_date : tax_details.to_date])
advance_amt = (
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0

View File

@@ -288,17 +288,18 @@ class TestTaxWithholdingCategory(FrappeTestCase):
frappe.db.set_value(
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
)
fiscal_year = get_fiscal_year(today(), company="_Test Company")
vouchers = []
# create advance payment
pe = create_payment_entry(
pe1 = create_payment_entry(
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=20000
)
pe.paid_from = "Debtors - _TC"
pe.paid_to = "Cash - _TC"
pe.submit()
vouchers.append(pe)
pe1.paid_from = "Debtors - _TC"
pe1.paid_to = "Cash - _TC"
pe1.submit()
vouchers.append(pe1)
# create invoice
si1 = create_sales_invoice(customer="Test TCS Customer", rate=5000)
@@ -320,6 +321,17 @@ class TestTaxWithholdingCategory(FrappeTestCase):
# make another invoice
# sum of unallocated amount from payment entry and this sales invoice will breach cumulative threashold
# TDS should be calculated
# this payment should not be considered for TCS calculation as it is outside of fiscal year
pe2 = create_payment_entry(
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=10000
)
pe2.paid_from = "Debtors - _TC"
pe2.paid_to = "Cash - _TC"
pe2.posting_date = add_days(fiscal_year[1], -10)
pe2.submit()
vouchers.append(pe2)
si2 = create_sales_invoice(customer="Test TCS Customer", rate=15000)
si2.submit()
vouchers.append(si2)

View File

@@ -424,6 +424,8 @@ def get_party_account(party_type, party=None, company=None, include_advance=Fals
Will first search in party (Customer / Supplier) record, if not found,
will search in group (Customer Group / Supplier Group),
finally will return default."""
if not party_type:
frappe.throw(_("Party Type is mandatory"))
if not company:
frappe.throw(_("Please select a Company"))
@@ -460,6 +462,12 @@ def get_party_account(party_type, party=None, company=None, include_advance=Fals
if (account and account_currency != existing_gle_currency) or not account:
account = get_party_gle_account(party_type, party, company)
# get default account on the basis of party type
if not account:
account_type = frappe.get_cached_value("Party Type", party_type, "account_type")
default_account_name = "default_" + account_type.lower() + "_account"
account = frappe.get_cached_value("Company", company, default_account_name)
if include_advance and party_type in ["Customer", "Supplier", "Student"]:
advance_account = get_party_advance_account(party_type, party, company)
if advance_account:

View File

@@ -60,6 +60,13 @@ frappe.query_reports["Accounts Payable"] = {
options: "Posting Date\nDue Date\nSupplier Invoice Date",
default: "Due Date",
},
{
fieldname: "calculate_ageing_with",
label: __("Calculate Ageing With"),
fieldtype: "Select",
options: "Report Date\nToday Date",
default: "Report Date",
},
{
fieldname: "range",
label: __("Ageing Range"),

View File

@@ -185,7 +185,7 @@
{% if(!filters.show_future_payments) { %}
<td>
{% if(!(filters.party)) { %}
{% if(!filters.party?.length) { %}
{%= data[i]["party"] %}
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
<br> {%= data[i]["customer_name"] %}
@@ -258,7 +258,7 @@
{% if(data[i]["party"]|| "&nbsp;") { %}
{% if(!data[i]["is_total_row"]) { %}
<td>
{% if(!(filters.party)) { %}
{% if(!filters.party?.length) { %}
{%= data[i]["party"] %}
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
<br> {%= data[i]["customer_name"] %}

View File

@@ -89,6 +89,13 @@ frappe.query_reports["Accounts Receivable"] = {
options: "Posting Date\nDue Date",
default: "Due Date",
},
{
fieldname: "calculate_ageing_with",
label: __("Calculate Ageing With"),
fieldtype: "Select",
options: "Report Date\nToday Date",
default: "Report Date",
},
{
fieldname: "range",
label: __("Ageing Range"),

View File

@@ -6,6 +6,7 @@ from collections import OrderedDict
import frappe
from frappe import _, qb, query_builder, scrub
from frappe.desk.reportview import build_match_conditions
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Date, Substring, Sum
from frappe.utils import cint, cstr, flt, getdate, nowdate
@@ -47,7 +48,10 @@ class ReceivablePayableReport:
self.ple = qb.DocType("Payment Ledger Entry")
self.filters.report_date = getdate(self.filters.report_date or nowdate())
self.age_as_on = (
getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date
getdate(nowdate())
if "calculate_ageing_with" not in self.filters
or self.filters.calculate_ageing_with == "Today Date"
else self.filters.report_date
)
if not self.filters.range:
@@ -96,9 +100,6 @@ class ReceivablePayableReport:
def get_data(self):
self.get_sales_invoices_or_customers_based_on_sales_person()
# Build delivery note map against all sales invoices
self.build_delivery_note_map()
# Get invoice details like bill_no, due_date etc for all invoices
self.get_invoice_details()
@@ -106,7 +107,8 @@ class ReceivablePayableReport:
self.get_future_payments()
# Get return entries
self.get_return_entries()
if not self.filters.party_type or self.filters.party_type in ["Customer", "Supplier"]:
self.get_return_entries()
# Get Exchange Rate Revaluations
self.get_exchange_rate_revaluations()
@@ -120,10 +122,14 @@ class ReceivablePayableReport:
elif self.ple_fetch_method == "UnBuffered Cursor":
self.fetch_ple_in_unbuffered_cursor()
# Build delivery note map against all sales invoices
self.build_delivery_note_map()
self.build_data()
def fetch_ple_in_buffered_cursor(self):
self.ple_entries = frappe.db.sql(self.ple_query.get_sql(), as_dict=True)
query, param = self.ple_query
self.ple_entries = frappe.db.sql(query, param, as_dict=True)
for ple in self.ple_entries:
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
@@ -136,8 +142,9 @@ class ReceivablePayableReport:
def fetch_ple_in_unbuffered_cursor(self):
self.ple_entries = []
query, param = self.ple_query
with frappe.db.unbuffered_cursor():
for ple in frappe.db.sql(self.ple_query.get_sql(), as_dict=True, as_iterator=True):
for ple in frappe.db.sql(query, param, as_dict=True, as_iterator=True):
self.init_voucher_balance(ple) # invoiced, paid, credit_note, outstanding
self.ple_entries.append(ple)
@@ -444,16 +451,14 @@ class ReceivablePayableReport:
self.invoice_details = frappe._dict()
if self.account_type == "Receivable":
# nosemgrep
si_list = frappe.db.sql(
"""
select name, due_date, po_no
from `tabSales Invoice`
where posting_date <= %s
and company = %s
and docstatus = 1
""",
(self.filters.report_date, self.filters.company),
as_dict=1,
si_list = frappe.get_list(
"Sales Invoice",
filters={
"posting_date": ("<=", self.filters.report_date),
"company": self.filters.company,
"docstatus": 1,
},
fields=["name", "due_date", "po_no"],
)
for d in si_list:
self.invoice_details.setdefault(d.name, d)
@@ -476,33 +481,29 @@ class ReceivablePayableReport:
if self.account_type == "Payable":
# nosemgrep
for pi in frappe.db.sql(
"""
select name, due_date, bill_no, bill_date
from `tabPurchase Invoice`
where
posting_date <= %s
and company = %s
and docstatus = 1
""",
(self.filters.report_date, self.filters.company),
as_dict=1,
):
invoices = frappe.get_list(
"Purchase Invoice",
filters={
"posting_date": ("<=", self.filters.report_date),
"company": self.filters.company,
"docstatus": 1,
},
fields=["name", "due_date", "bill_no", "bill_date"],
)
for pi in invoices:
self.invoice_details.setdefault(pi.name, pi)
# Invoices booked via Journal Entries
# nosemgrep
journal_entries = frappe.db.sql(
"""
select name, due_date, bill_no, bill_date
from `tabJournal Entry`
where
posting_date <= %s
and company = %s
and docstatus = 1
""",
(self.filters.report_date, self.filters.company),
as_dict=1,
journal_entries = frappe.get_list(
"Journal Entry",
filters={
"posting_date": ("<=", self.filters.report_date),
"company": self.filters.company,
"docstatus": 1,
},
fields=["name", "due_date", "bill_no", "bill_date"],
)
for je in journal_entries:
@@ -784,7 +785,7 @@ class ReceivablePayableReport:
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.filters.report_date):
if getdate(entry_date) > getdate(self.age_as_on):
[setattr(row, f"range{i}", 0.0) for i in self.range_numbers]
row.total_due = sum(row[f"range{i}"] for i in self.range_numbers)
@@ -851,12 +852,18 @@ class ReceivablePayableReport:
else:
query = query.select(ple.remarks)
if self.filters.get("group_by_party"):
query = query.orderby(self.ple.party, self.ple.posting_date)
else:
query = query.orderby(self.ple.posting_date, self.ple.party)
query, param = query.walk()
self.ple_query = query
match_conditions = build_match_conditions("Payment Ledger Entry")
if match_conditions:
query += " AND " + match_conditions
if self.filters.get("group_by_party"):
query += f" ORDER BY `{self.ple.party.name}`, `{self.ple.posting_date.name}`"
else:
query += f" ORDER BY `{self.ple.posting_date.name}`, `{self.ple.party.name}`"
self.ple_query = (query, param)
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):

View File

@@ -0,0 +1,13 @@
// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.query_reports["Calculated Discount Mismatch"] = {
// filters: [
// {
// "fieldname": "my_filter",
// "label": __("My Filter"),
// "fieldtype": "Data",
// "reqd": 1,
// },
// ],
// };

View File

@@ -0,0 +1,38 @@
{
"add_total_row": 0,
"add_translate_data": 0,
"columns": [],
"creation": "2025-06-06 17:09:50.681090",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"letter_head": "",
"letterhead": null,
"modified": "2025-06-06 18:09:18.221911",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Calculated Discount Mismatch",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Version",
"report_name": "Calculated Discount Mismatch",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Administrator"
},
{
"role": "Accounts Manager"
},
{
"role": "Accounts User"
}
],
"timeout": 0
}

View File

@@ -0,0 +1,173 @@
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
import frappe
from frappe import _
from frappe.query_builder import Order, Tuple
from frappe.utils.formatters import format_value
AFFECTED_DOCTYPES = frozenset(
(
"POS Invoice",
"Purchase Invoice",
"Sales Invoice",
"Purchase Order",
"Supplier Quotation",
"Quotation",
"Sales Order",
"Delivery Note",
"Purchase Receipt",
)
)
LAST_MODIFIED_DATE_THRESHOLD = "2025-05-30"
def execute(filters=None):
columns = get_columns()
data = get_data()
return columns, data
def get_columns():
return [
{
"fieldname": "doctype",
"label": _("Transaction Type"),
"fieldtype": "Link",
"options": "DocType",
"width": 120,
},
{
"fieldname": "docname",
"label": _("Transaction Name"),
"fieldtype": "Dynamic Link",
"options": "doctype",
"width": 150,
},
{
"fieldname": "actual_discount_percentage",
"label": _("Discount Percentage in Transaction"),
"fieldtype": "Percent",
"width": 180,
},
{
"fieldname": "actual_discount_amount",
"label": _("Discount Amount in Transaction"),
"fieldtype": "Currency",
"width": 180,
},
{
"fieldname": "suspected_discount_amount",
"label": _("Suspected Discount Amount"),
"fieldtype": "Currency",
"width": 180,
},
]
def get_data():
transactions_with_discount_percentage = {}
for doctype in AFFECTED_DOCTYPES:
transactions = get_transactions_with_discount_percentage(doctype)
for transaction in transactions:
transactions_with_discount_percentage[(doctype, transaction.name)] = transaction
if not transactions_with_discount_percentage:
return []
VERSION = frappe.qb.DocType("Version")
versions = (
frappe.qb.from_(VERSION)
.select(VERSION.ref_doctype, VERSION.docname, VERSION.data)
.where(VERSION.creation > LAST_MODIFIED_DATE_THRESHOLD)
.where(Tuple(VERSION.ref_doctype, VERSION.docname).isin(list(transactions_with_discount_percentage)))
.where(
VERSION.data.like('%"discount\\_amount"%')
| VERSION.data.like('%"additional\\_discount\\_percentage"%')
)
.orderby(VERSION.creation, order=Order.desc)
.run(as_dict=True)
)
if not versions:
return []
version_map = {}
for version in versions:
key = (version.ref_doctype, version.docname)
if key not in version_map:
version_map[key] = []
version_map[key].append(version.data)
data = []
discount_amount_field_map = {
doctype: frappe.get_meta(doctype).get_field("discount_amount") for doctype in AFFECTED_DOCTYPES
}
for doc, versions in version_map.items():
for version_data in versions:
if '"additional_discount_percentage"' in version_data:
# don't consider doc if additional_discount_percentage is changed in newest version
break
version_data = json.loads(version_data)
changed_values = version_data.get("changed")
if not changed_values:
continue
discount_values = next((row for row in changed_values if row[0] == "discount_amount"), None)
if not discount_values:
continue
old = discount_values[1]
new = discount_values[2]
doctype = doc[0]
doc_values = transactions_with_discount_percentage.get(doc)
formatted_discount_amount = format_value(
doc_values.discount_amount,
df=discount_amount_field_map[doctype],
currency=doc_values.currency,
)
if new != formatted_discount_amount:
# if the discount amount in the version is not equal to the current value, skip
break
data.append(
{
"doctype": doctype,
"docname": doc_values.name,
"actual_discount_percentage": doc_values.additional_discount_percentage,
"actual_discount_amount": new,
"suspected_discount_amount": old,
}
)
break
return data
def get_transactions_with_discount_percentage(doctype):
transactions = frappe.get_all(
doctype,
fields=[
"name",
"currency",
"additional_discount_percentage",
"discount_amount",
],
filters={
"docstatus": ["<", 2],
"additional_discount_percentage": [">", 0],
"discount_amount": ["!=", 0],
"modified": [">", LAST_MODIFIED_DATE_THRESHOLD],
},
)
return transactions

View File

@@ -75,7 +75,11 @@ def execute(filters=None):
# add first net income in operations section
if net_profit_loss:
net_profit_loss.update(
{"indent": 1, "parent_section": cash_flow_sections[0]["section_header"]}
{
"indent": 1,
"parent_section": cash_flow_sections[0]["section_header"],
"section": net_profit_loss["account"],
}
)
data.append(net_profit_loss)
section_data.append(net_profit_loss)

View File

@@ -8,6 +8,7 @@ from frappe.query_builder import Criterion, Tuple
from frappe.query_builder.functions import IfNull
from frappe.utils import getdate, nowdate
from frappe.utils.nestedset import get_descendants_of
from pypika.terms import LiteralValue
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
@@ -15,7 +16,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
)
TREE_DOCTYPES = frozenset(
["Customer Group", "Terrirtory", "Supplier Group", "Sales Partner", "Sales Person", "Cost Center"]
["Customer Group", "Territory", "Supplier Group", "Sales Partner", "Sales Person", "Cost Center"]
)
@@ -77,13 +78,12 @@ class PartyLedgerSummaryReport:
from frappe.desk.reportview import build_match_conditions
query, params = query.walk()
match_conditions = build_match_conditions(party_type)
if match_conditions:
query += "and" + match_conditions
query = query.where(LiteralValue(match_conditions))
party_details = frappe.db.sql(query, params, as_dict=True)
party_details = query.run(as_dict=True)
for row in party_details:
self.parties.append(row.party)
@@ -405,7 +405,9 @@ class PartyLedgerSummaryReport:
gl = qb.DocType("GL Entry")
query = (
qb.from_(gl)
.select(gl.voucher_type, gl.voucher_no)
.select(
gl.posting_date, gl.account, gl.party, gl.voucher_type, gl.voucher_no, gl.debit, gl.credit
)
.where(
(gl.docstatus < 2)
& (gl.is_cancelled == 0)
@@ -456,9 +458,16 @@ class PartyLedgerSummaryReport:
def get_children(doctype, value):
children = get_descendants_of(doctype, value)
if not isinstance(value, list):
value = [d.strip() for d in value.strip().split(",") if d]
return [value, *children]
all_children = []
for d in value:
all_children += get_descendants_of(doctype, value)
all_children.append(d)
return list(set(all_children))
def execute(filters=None):

View File

@@ -47,12 +47,12 @@
{% for(let j=0, k=data.length; j<k; j++) { %}
{%
var row = data[j];
var row_class = data[j].parent_account ? "" : "financial-statements-important";
row_class += data[j].account_name ? "" : " financial-statements-blank-row";
var row_class = data[j].parent_account || data[j].parent_section ? "" : "financial-statements-important";
row_class += data[j].account_name || data[j].section ? "" : " financial-statements-blank-row";
%}
<tr class="{%= row_class %}">
<td>
<span style="padding-left: {%= cint(data[j].indent) * 2 %}em">{%= row.account_name %}</span>
<span style="padding-left: {%= cint(data[j].indent) * 2 %}em">{%= row.account_name || row.section %}</span>
</td>
{% for(let i=1, l=report_columns.length; i<l; i++) { %}
<td class="text-right">

View File

@@ -194,7 +194,8 @@ def get_gl_entries(filters, accounting_dimensions):
voucher_type, voucher_subtype, voucher_no, {dimension_fields}
cost_center, project, {transaction_currency_fields}
against_voucher_type, against_voucher, account_currency,
against, is_opening, creation {select_fields}
against, is_opening, creation {select_fields},
transaction_currency
from `tabGL Entry`
where company=%(company)s {get_conditions(filters)}
{order_by_statement}

View File

@@ -3,12 +3,15 @@
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import flt, today
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.general_ledger.general_ledger import execute
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
class TestGeneralLedger(FrappeTestCase):
@@ -168,6 +171,90 @@ class TestGeneralLedger(FrappeTestCase):
self.assertEqual(data[3]["debit"], 100)
self.assertEqual(data[3]["credit"], 100)
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": True})
def test_debit_in_exchange_gain_loss_account(self):
company = "_Test Company"
exchange_gain_loss_account = frappe.db.get_value("Company", "exchange_gain_loss_account")
if not exchange_gain_loss_account:
frappe.db.set_value(
"Company", company, "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
)
account_name = "_Test Receivable USD - _TC"
customer_name = "_Test Customer USD"
sales_invoice = create_sales_invoice(
company=company,
customer=customer_name,
currency="USD",
debit_to=account_name,
conversion_rate=85,
posting_date=today(),
)
payment_entry = create_payment_entry(
company=company,
party_type="Customer",
party=customer_name,
payment_type="Receive",
paid_from=account_name,
paid_from_account_currency="USD",
paid_to="Cash - _TC",
paid_to_account_currency="INR",
paid_amount=10,
do_not_submit=True,
)
payment_entry.base_paid_amount = 800
payment_entry.received_amount = 800
payment_entry.currency = "USD"
payment_entry.source_exchange_rate = 80
payment_entry.append(
"references",
frappe._dict(
{
"reference_doctype": "Sales Invoice",
"reference_name": sales_invoice.name,
"total_amount": 10,
"outstanding_amount": 10,
"exchange_rate": 85,
"allocated_amount": 10,
"exchange_gain_loss": -50,
}
),
)
payment_entry.save()
payment_entry.submit()
journal_entry = frappe.get_all(
"Journal Entry Account", filters={"reference_name": sales_invoice.name}, fields=["parent"]
)
columns, data = execute(
frappe._dict(
{
"company": company,
"from_date": today(),
"to_date": today(),
"include_dimensions": 1,
"include_default_book_entries": 1,
"account": ["_Test Exchange Gain/Loss - _TC"],
"categorize_by": "Categorize by Voucher (Consolidated)",
}
)
)
entry = data[1]
self.assertEqual(entry["debit"], 50)
self.assertEqual(entry["voucher_type"], "Journal Entry")
self.assertEqual(entry["voucher_no"], journal_entry[0]["parent"])
payment_entry.cancel()
payment_entry.delete()
sales_invoice.reload()
sales_invoice.cancel()
sales_invoice.delete()
def test_ignore_exchange_rate_journals_filter(self):
# create a new account with USD currency
account_name = "Test Debtors USD"

View File

@@ -6,6 +6,7 @@ import frappe
from frappe import _
from frappe.model.meta import get_field_precision
from frappe.utils import cstr, flt
from frappe.utils.nestedset import get_descendants_of
from frappe.utils.xlsxutils import handle_html
from pypika import Order
@@ -375,7 +376,12 @@ def apply_conditions(query, si, sii, filters, additional_conditions=None):
query = query.where(sii.item_code == filters.get("item_code"))
if filters.get("item_group"):
query = query.where(sii.item_group == filters.get("item_group"))
if frappe.db.get_value("Item Group", filters.get("item_group"), "is_group"):
item_groups = get_descendants_of("Item Group", filters.get("item_group"))
item_groups.append(filters.get("item_group"))
query = query.where(sii.item_group.isin(item_groups))
else:
query = query.where(sii.item_group == filters.get("item_group"))
if filters.get("income_account"):
query = query.where(

View File

@@ -101,13 +101,18 @@ def convert_to_presentation_currency(gl_entries, currency_info):
account_currencies = list(set(entry["account_currency"] for entry in gl_entries))
for entry in gl_entries:
transaction_currency = entry.get("transaction_currency")
debit = flt(entry["debit"])
credit = flt(entry["credit"])
debit_in_account_currency = flt(entry["debit_in_account_currency"])
credit_in_account_currency = flt(entry["credit_in_account_currency"])
account_currency = entry["account_currency"]
if len(account_currencies) == 1 and account_currency == presentation_currency:
if (
len(account_currencies) == 1
and account_currency == presentation_currency
and (transaction_currency is None or account_currency == transaction_currency)
):
entry["debit"] = debit_in_account_currency
entry["credit"] = credit_in_account_currency
else:

View File

@@ -72,6 +72,12 @@ frappe.ui.form.on("Asset", {
filters: { item_code: doc.item_code },
};
});
if (frm.doc.docstatus == 1) {
frm.custom_make_buttons = {
"Asset Capitalization": "Asset Capitalization",
};
}
},
refresh: function (frm) {

View File

@@ -154,6 +154,7 @@
{
"allow_on_submit": 1,
"fetch_from": "item_code.image",
"fetch_if_empty": 1,
"fieldname": "image",
"fieldtype": "Attach Image",
"hidden": 1,
@@ -204,8 +205,8 @@
"fieldname": "purchase_date",
"fieldtype": "Date",
"label": "Purchase Date",
"mandatory_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
"reqd": 1
},
{
"fieldname": "disposal_date",
@@ -595,7 +596,7 @@
"link_fieldname": "target_asset"
}
],
"modified": "2025-04-24 15:31:47.373274",
"modified": "2025-05-20 00:44:06.229177",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -42,16 +42,15 @@ from erpnext.controllers.accounts_controller import AccountsController
class Asset(AccountsController):
# begin: auto-generated types
# ruff: noqa
# This code is auto-generated. Do not modify anything in this block.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook
from frappe.types import DF
from erpnext.assets.doctype.asset_finance_book.asset_finance_book import AssetFinanceBook
additional_asset_cost: DF.Currency
amended_from: DF.Link | None
asset_category: DF.Link | None
@@ -94,7 +93,7 @@ class Asset(AccountsController):
opening_number_of_booked_depreciations: DF.Int
policy_number: DF.Data | None
purchase_amount: DF.Currency
purchase_date: DF.Date | None
purchase_date: DF.Date
purchase_invoice: DF.Link | None
purchase_invoice_item: DF.Data | None
purchase_receipt: DF.Link | None
@@ -118,7 +117,6 @@ class Asset(AccountsController):
total_asset_cost: DF.Currency
total_number_of_depreciations: DF.Int
value_after_depreciation: DF.Currency
# ruff: noqa
# end: auto-generated types
def validate(self):

View File

@@ -798,7 +798,7 @@ def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_
idx = 1
if finance_book:
for d in asset.finance_books:
for d in asset_doc.finance_books:
if d.finance_book == finance_book:
idx = d.idx
break

View File

@@ -281,7 +281,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
if (me.frm.doc.target_item_code) {
return me.frm.call({
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_item_details",
child: me.frm.doc,
args: {
item_code: me.frm.doc.target_item_code,
company: me.frm.doc.company,
@@ -301,7 +300,6 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
if (me.frm.doc.target_asset) {
return me.frm.call({
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details",
child: me.frm.doc,
args: {
asset: me.frm.doc.target_asset,
company: me.frm.doc.company,
@@ -404,7 +402,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
args: {
item_code: item.item_code,
warehouse: cstr(item.warehouse),
qty: flt(item.stock_qty),
qty: -1 * flt(item.stock_qty),
serial_no: item.serial_no,
posting_date: me.frm.doc.posting_date,
posting_time: me.frm.doc.posting_time,

View File

@@ -875,8 +875,8 @@ def get_items_tagged_to_wip_composite_asset(params):
"valuation_rate",
"amount",
"is_fixed_asset",
"parent",
"name",
"parent as purchase_receipt",
"name as purchase_receipt_item",
]
pr_items = frappe.get_all(
@@ -905,7 +905,7 @@ def process_stock_item(d):
stock_capitalized = frappe.db.exists(
"Asset Capitalization Stock Item",
{
"purchase_receipt_item": d.name,
"purchase_receipt_item": d.purchase_receipt_item,
"parentfield": "stock_items",
"parenttype": "Asset Capitalization",
"docstatus": 1,
@@ -916,7 +916,7 @@ def process_stock_item(d):
return None
stock_item_data = frappe._dict(d)
stock_item_data.purchase_receipt_item = d.name
stock_item_data.purchase_receipt_item = d.purchase_receipt_item
return stock_item_data
@@ -925,7 +925,7 @@ def process_fixed_asset(d):
"Asset",
{
"item_code": d.item_code,
"purchase_receipt": d.parent,
"purchase_receipt": d.purchase_receipt,
"status": ("not in", ["Draft", "Scrapped", "Sold", "Capitalized"]),
},
["name as asset", "asset_name", "company"],

View File

@@ -255,8 +255,10 @@ class AssetDepreciationSchedule(Document):
value_after_depreciation,
):
asset_doc.validate_asset_finance_books(row)
if not value_after_depreciation:
if (
not value_after_depreciation
and not asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment
):
value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row)
row.value_after_depreciation = value_after_depreciation
@@ -1068,8 +1070,6 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
)
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
if asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment and not value_after_depreciation:
value_after_depreciation = row.value_after_depreciation - difference_amount
if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in (
"Written Down Value",

View File

@@ -153,8 +153,6 @@ class AssetMovement(Document):
args,
)
self.validate_movement_cancellation(d, latest_movement_entry)
if latest_movement_entry:
current_location = latest_movement_entry[0][0]
current_employee = latest_movement_entry[0][1]
@@ -182,12 +180,3 @@ class AssetMovement(Document):
d.asset,
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)),
)
def validate_movement_cancellation(self, row, latest_movement_entry):
asset_doc = frappe.get_doc("Asset", row.asset)
if not latest_movement_entry and asset_doc.docstatus == 1:
frappe.throw(
_(
"Asset {0} has only one movement record. Please create another movement before deleting this one to maintain asset tracking."
).format(row.asset)
)

View File

@@ -147,45 +147,6 @@ class TestAssetMovement(unittest.TestCase):
movement1.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location")
def test_last_movement_cancellation_validation(self):
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location")
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
asset = frappe.get_doc("Asset", asset_name)
asset.calculate_depreciation = 1
asset.available_for_use_date = "2020-06-06"
asset.purchase_date = "2020-06-06"
asset.append(
"finance_books",
{
"expected_value_after_useful_life": 10000,
"next_depreciation_date": "2020-12-31",
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 3,
"frequency_of_depreciation": 10,
},
)
if asset.docstatus == 0:
asset.submit()
AssetMovement = frappe.qb.DocType("Asset Movement")
AssetMovementItem = frappe.qb.DocType("Asset Movement Item")
asset_movement = (
frappe.qb.from_(AssetMovement)
.join(AssetMovementItem)
.on(AssetMovementItem.parent == AssetMovement.name)
.select(AssetMovement.name)
.where(
(AssetMovementItem.asset == asset.name)
& (AssetMovement.company == asset.company)
& (AssetMovement.docstatus == 1)
)
).run(as_dict=True)
asset_movement_doc = frappe.get_doc("Asset Movement", asset_movement[0].name)
self.assertRaises(frappe.ValidationError, asset_movement_doc.cancel)
def create_asset_movement(**args):
args = frappe._dict(args)

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import flt, formatdate, get_link_to_form, getdate
from frappe.utils import cstr, flt, formatdate, get_link_to_form, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
@@ -188,15 +188,25 @@ class AssetValueAdjustment(Document):
get_link_to_form(self.get("doctype"), self.get("name")),
)
difference_amount = self.difference_amount if self.docstatus == 1 else -1 * self.difference_amount
if asset.calculate_depreciation:
for row in asset.finance_books:
if cstr(row.finance_book) == cstr(self.finance_book):
row.value_after_depreciation += flt(difference_amount)
row.db_update()
asset.db_update()
make_new_active_asset_depr_schedules_and_cancel_current_ones(
asset,
notes,
value_after_depreciation=asset_value,
ignore_booked_entry=True,
difference_amount=self.difference_amount,
difference_amount=difference_amount,
)
asset.flags.ignore_validate_update_after_submit = True
asset.save()
asset.set_status()
@frappe.whitelist()

View File

@@ -12,19 +12,26 @@
"column_break_4",
"maintain_same_rate_action",
"role_to_override_stop_action",
"transaction_settings_section",
"section_break_xmlt",
"po_required",
"pr_required",
"blanket_order_allowance",
"column_break_sbwq",
"pr_required",
"project_update_frequency",
"transaction_settings_section",
"column_break_fcyl",
"set_landed_cost_based_on_purchase_invoice_rate",
"allow_zero_qty_in_supplier_quotation",
"use_transaction_date_exchange_rate",
"allow_zero_qty_in_request_for_quotation",
"column_break_12",
"maintain_same_rate",
"set_landed_cost_based_on_purchase_invoice_rate",
"allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
"set_valuation_rate_for_rejected_materials",
"disable_last_purchase_rate",
"show_pay_button",
"use_transaction_date_exchange_rate",
"allow_zero_qty_in_purchase_order",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
@@ -207,14 +214,56 @@
"fieldtype": "Select",
"label": "Update frequency of Project",
"options": "Each Transaction\nManual"
},
{
"default": "0",
"description": "Allows users to submit Purchase Orders with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.",
"fieldname": "allow_zero_qty_in_purchase_order",
"fieldtype": "Check",
"label": "Allow Purchase Order with Zero Quantity"
},
{
"default": "0",
"description": "Allows users to submit Request for Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.",
"fieldname": "allow_zero_qty_in_request_for_quotation",
"fieldtype": "Check",
"label": "Allow Request for Quotation with Zero Quantity"
},
{
"default": "0",
"description": "Allows users to submit Supplier Quotations with zero quantity. Useful when rates are fixed but the quantities are not. Eg. Rate Contracts.",
"fieldname": "allow_zero_qty_in_supplier_quotation",
"fieldtype": "Check",
"label": "Allow Supplier Quotation with Zero Quantity"
},
{
"fieldname": "section_break_xmlt",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_sbwq",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_fcyl",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "bill_for_rejected_quantity_in_purchase_invoice",
"description": "If enabled, the system will generate an accounting entry for materials rejected in the Purchase Receipt.",
"fieldname": "set_valuation_rate_for_rejected_materials",
"fieldtype": "Check",
"label": "Set Valuation Rate for Rejected Materials"
}
],
"grid_page_length": 50,
"icon": "fa fa-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2024-01-31 13:34:18.101256",
"modified": "2025-05-16 15:56:38.321369",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -18,6 +18,9 @@ class BuyingSettings(Document):
from frappe.types import DF
allow_multiple_items: DF.Check
allow_zero_qty_in_purchase_order: DF.Check
allow_zero_qty_in_request_for_quotation: DF.Check
allow_zero_qty_in_supplier_quotation: DF.Check
auto_create_purchase_receipt: DF.Check
auto_create_subcontracting_order: DF.Check
backflush_raw_materials_of_subcontract_based_on: DF.Literal[
@@ -35,6 +38,7 @@ class BuyingSettings(Document):
project_update_frequency: DF.Literal["Each Transaction", "Manual"]
role_to_override_stop_action: DF.Link | None
set_landed_cost_based_on_purchase_invoice_rate: DF.Check
set_valuation_rate_for_rejected_materials: DF.Check
show_pay_button: DF.Check
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
supplier_group: DF.Link | None
@@ -54,6 +58,9 @@ class BuyingSettings(Document):
hide_name_field=False,
)
if not self.bill_for_rejected_quantity_in_purchase_invoice:
self.set_valuation_rate_for_rejected_materials = 0
def before_save(self):
self.check_maintain_same_rate()

View File

@@ -26,7 +26,15 @@ frappe.ui.form.on("Purchase Order", {
}
frm.set_indicator_formatter("item_code", function (doc) {
return doc.qty <= doc.received_qty ? "green" : "orange";
let color;
if (!doc.qty && frm.doc.has_unit_price_items) {
color = "yellow";
} else if (doc.qty <= doc.received_qty) {
color = "green";
} else {
color = "orange";
}
return color;
});
frm.set_query("expense_account", "items", function () {
@@ -63,6 +71,10 @@ frappe.ui.form.on("Purchase Order", {
}
});
}
if (frm.doc.docstatus == 0) {
erpnext.set_unit_price_items_note(frm);
}
},
supplier: function (frm) {

View File

@@ -24,6 +24,7 @@
"apply_tds",
"tax_withholding_category",
"is_subcontracted",
"has_unit_price_items",
"supplier_warehouse",
"amended_from",
"accounting_dimensions_section",
@@ -1285,6 +1286,14 @@
"label": "Dispatch Address Details",
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "has_unit_price_items",
"fieldtype": "Check",
"hidden": 1,
"label": "Has Unit Price Items",
"no_copy": 1
}
],
"grid_page_length": 50,

View File

@@ -97,6 +97,7 @@ class PurchaseOrder(BuyingController):
from_date: DF.Date | None
grand_total: DF.Currency
group_same_items: DF.Check
has_unit_price_items: DF.Check
ignore_pricing_rule: DF.Check
in_words: DF.Data | None
incoterm: DF.Link | None
@@ -191,6 +192,10 @@ class PurchaseOrder(BuyingController):
self.set_onload("supplier_tds", supplier_tds)
self.set_onload("can_update_items", self.can_update_items())
def before_validate(self):
self.set_has_unit_price_items()
self.flags.allow_zero_qty = self.has_unit_price_items
def validate(self):
super().validate()
@@ -223,6 +228,17 @@ class PurchaseOrder(BuyingController):
)
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def set_has_unit_price_items(self):
"""
If permitted in settings and any item has 0 qty, the PO has unit price items.
"""
if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order"):
return
self.has_unit_price_items = any(
not row.qty for row in self.get("items") if (row.item_code and not row.qty)
)
def validate_with_previous_doc(self):
mri_compare_fields = [["project", "="], ["item_code", "="]]
if self.is_subcontracted:
@@ -707,8 +723,13 @@ def set_missing_values(source, target):
@frappe.whitelist()
def make_purchase_receipt(source_name, target_doc=None):
has_unit_price_items = frappe.db.get_value("Purchase Order", source_name, "has_unit_price_items")
def is_unit_price_row(source):
return has_unit_price_items and source.qty == 0
def update_item(obj, target, source_parent):
target.qty = flt(obj.qty) - flt(obj.received_qty)
target.qty = flt(obj.qty) if is_unit_price_row(obj) else flt(obj.qty) - flt(obj.received_qty)
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor)
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
target.base_amount = (
@@ -739,7 +760,9 @@ def make_purchase_receipt(source_name, target_doc=None):
"wip_composite_asset": "wip_composite_asset",
},
"postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
"condition": lambda doc: (
True if is_unit_price_row(doc) else abs(doc.received_qty) < abs(doc.qty)
)
and doc.delivered_by_supplier != 1,
},
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},

View File

@@ -31,6 +31,8 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
class TestPurchaseOrder(FrappeTestCase):
def test_purchase_order_qty(self):
po = create_purchase_order(qty=1, do_not_save=True)
# NonNegativeError with qty=-1
po.append(
"items",
{
@@ -41,9 +43,22 @@ class TestPurchaseOrder(FrappeTestCase):
)
self.assertRaises(frappe.NonNegativeError, po.save)
# InvalidQtyError with qty=0
po.items[1].qty = 0
self.assertRaises(InvalidQtyError, po.save)
# No error with qty=1
po.items[1].qty = 1
po.save()
self.assertEqual(po.items[1].qty, 1)
def test_purchase_order_zero_qty(self):
po = create_purchase_order(qty=0, do_not_save=True)
with change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1}):
po.save()
self.assertEqual(po.items[0].qty, 0)
def test_make_purchase_receipt(self):
po = create_purchase_order(do_not_submit=True)
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)
@@ -793,8 +808,6 @@ class TestPurchaseOrder(FrappeTestCase):
po_doc.reload()
self.assertEqual(po_doc.advance_paid, 5000)
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice
company_doc.book_advance_payments_in_separate_party_account = False
company_doc.save()
@@ -1199,6 +1212,80 @@ class TestPurchaseOrder(FrappeTestCase):
po.reload()
self.assertEqual(po.per_billed, 100)
@change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1})
def test_receive_zero_qty_purchase_order(self):
"""
Test the flow of a Unit Price PO and PR creation against it until completion.
Flow:
PO Qty 0 -> Receive +5 -> Receive +5 -> Update PO Qty +10 -> PO is 100% received
"""
po = create_purchase_order(qty=0)
pr = make_purchase_receipt(po.name)
self.assertEqual(pr.items[0].qty, 0)
pr.items[0].qty = 5
pr.submit()
po.reload()
self.assertEqual(po.items[0].received_qty, 5)
self.assertFalse(po.per_received)
self.assertEqual(po.status, "To Receive and Bill")
# Update PO Item Qty to 10 after receipt of items
first_item_of_po = po.items[0]
trans_item = json.dumps(
[
{
"item_code": first_item_of_po.item_code,
"rate": first_item_of_po.rate,
"qty": 10,
"docname": first_item_of_po.name,
}
]
)
update_child_qty_rate("Purchase Order", trans_item, po.name)
# Test: PR can be made against PO as long PO qty is 0 OR PO qty > received qty
pr2 = make_purchase_receipt(po.name)
po.reload()
self.assertEqual(po.items[0].qty, 10)
self.assertEqual(pr2.items[0].qty, 5)
pr2.submit()
# PO should be updated to 100% received
po.reload()
self.assertEqual(po.items[0].qty, 10)
self.assertEqual(po.items[0].received_qty, 10)
self.assertEqual(po.per_received, 100.0)
self.assertEqual(po.status, "To Bill")
@change_settings("Buying Settings", {"allow_zero_qty_in_purchase_order": 1})
def test_bill_zero_qty_purchase_order(self):
po = create_purchase_order(qty=0)
self.assertEqual(po.grand_total, 0)
self.assertFalse(po.per_billed)
self.assertEqual(po.items[0].qty, 0)
self.assertEqual(po.items[0].rate, 500)
pi = make_pi_from_po(po.name)
self.assertEqual(pi.items[0].qty, 0)
self.assertEqual(pi.items[0].rate, 500)
pi.items[0].qty = 5
pi.submit()
self.assertEqual(pi.grand_total, 2500)
po.reload()
self.assertEqual(po.items[0].amount, 0)
self.assertEqual(po.items[0].billed_amt, 2500)
# PO still has qty 0, so billed % should be unset
self.assertFalse(po.per_billed)
self.assertEqual(po.status, "To Receive and Bill")
def create_po_for_sc_testing():
from erpnext.controllers.tests.test_subcontracting_controller import (
@@ -1341,7 +1428,7 @@ def create_purchase_order(**args):
"item_code": args.item or args.item_code or "_Test Item",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"from_warehouse": args.from_warehouse,
"qty": args.qty or 10,
"qty": args.qty if args.qty is not None else 10,
"rate": args.rate or 500,
"schedule_date": add_days(nowdate(), 1),
"include_exploded_items": args.get("include_exploded_items", 1),

View File

@@ -28,6 +28,10 @@ frappe.ui.form.on("Request for Quotation", {
is_group: 0,
},
}));
frm.set_indicator_formatter("item_code", function (doc) {
return !doc.qty && frm.doc.has_unit_price_items ? "yellow" : "";
});
},
onload: function (frm) {
@@ -163,6 +167,10 @@ frappe.ui.form.on("Request for Quotation", {
__("View")
);
}
if (frm.doc.docstatus === 0) {
erpnext.set_unit_price_items_note(frm);
}
},
show_supplier_quotation_comparison(frm) {

View File

@@ -16,6 +16,7 @@
"transaction_date",
"schedule_date",
"status",
"has_unit_price_items",
"amended_from",
"suppliers_section",
"suppliers",
@@ -306,13 +307,22 @@
"fieldtype": "Small Text",
"label": "Billing Address Details",
"read_only": 1
},
{
"default": "0",
"fieldname": "has_unit_price_items",
"fieldtype": "Check",
"hidden": 1,
"label": "Has Unit Price Items",
"no_copy": 1
}
],
"grid_page_length": 50,
"icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-11-06 12:45:28.898706",
"modified": "2025-03-03 16:48:39.856779",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
@@ -377,6 +387,7 @@
"role": "All"
}
],
"row_format": "Dynamic",
"search_fields": "status, transaction_date",
"show_name_in_global_search": 1,
"sort_field": "modified",

View File

@@ -42,6 +42,7 @@ class RequestforQuotation(BuyingController):
billing_address_display: DF.SmallText | None
company: DF.Link
email_template: DF.Link | None
has_unit_price_items: DF.Check
incoterm: DF.Link | None
items: DF.Table[RequestforQuotationItem]
letter_head: DF.Link | None
@@ -61,9 +62,14 @@ class RequestforQuotation(BuyingController):
vendor: DF.Link | None
# end: auto-generated types
def before_validate(self):
self.set_has_unit_price_items()
self.flags.allow_zero_qty = self.has_unit_price_items
def validate(self):
self.validate_duplicate_supplier()
self.validate_supplier_list()
super().validate_qty_is_not_zero()
validate_for_items(self)
super().set_qty_as_per_stock_uom()
self.update_email_id()
@@ -72,6 +78,17 @@ class RequestforQuotation(BuyingController):
# after amend and save, status still shows as cancelled, until submit
self.db_set("status", "Draft")
def set_has_unit_price_items(self):
"""
If permitted in settings and any item has 0 qty, the RFQ has unit price items.
"""
if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_request_for_quotation"):
return
self.has_unit_price_items = any(
not row.qty for row in self.get("items") if (row.item_code and not row.qty)
)
def validate_duplicate_supplier(self):
supplier_list = [d.supplier for d in self.suppliers]
if len(supplier_list) != len(set(supplier_list)):
@@ -439,11 +456,10 @@ def create_supplier_quotation(doc):
def add_items(sq_doc, supplier, items):
for data in items:
if data.get("qty") > 0:
if isinstance(data, dict):
data = frappe._dict(data)
if isinstance(data, dict):
data = frappe._dict(data)
create_rfq_items(sq_doc, supplier, data)
create_rfq_items(sq_doc, supplier, data)
def create_rfq_items(sq_doc, supplier, data):

View File

@@ -5,7 +5,7 @@
from urllib.parse import urlparse
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import nowdate
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
@@ -14,6 +14,7 @@ from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
get_pdf,
make_supplier_quotation_from_rfq,
)
from erpnext.controllers.accounts_controller import InvalidQtyError
from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq
from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
from erpnext.stock.doctype.item.test_item import make_item
@@ -21,6 +22,26 @@ from erpnext.templates.pages.rfq import check_supplier_has_docname_access
class TestRequestforQuotation(FrappeTestCase):
def test_rfq_qty(self):
rfq = make_request_for_quotation(qty=0, do_not_save=True)
with self.assertRaises(InvalidQtyError):
rfq.save()
# No error with qty=1
rfq.items[0].qty = 1
rfq.save()
self.assertEqual(rfq.items[0].qty, 1)
def test_rfq_zero_qty(self):
"""
Test if RFQ with zero qty (Unit Price Item) is conditionally allowed.
"""
rfq = make_request_for_quotation(qty=0, do_not_save=True)
with change_settings("Buying Settings", {"allow_zero_qty_in_request_for_quotation": 1}):
rfq.save()
self.assertEqual(rfq.items[0].qty, 0)
def test_quote_status(self):
rfq = make_request_for_quotation()
@@ -161,6 +182,32 @@ class TestRequestforQuotation(FrappeTestCase):
supplier_doc.reload()
self.assertTrue(supplier_doc.portal_users[0].user)
@change_settings("Buying Settings", {"allow_zero_qty_in_request_for_quotation": 1})
def test_supplier_quotation_from_zero_qty_rfq(self):
rfq = make_request_for_quotation(qty=0)
sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get("suppliers")[0].supplier)
self.assertEqual(len(sq.items), 1)
self.assertEqual(sq.items[0].qty, 0)
self.assertEqual(sq.items[0].item_code, rfq.items[0].item_code)
@change_settings(
"Buying Settings",
{
"allow_zero_qty_in_request_for_quotation": 1,
"allow_zero_qty_in_supplier_quotation": 1,
},
)
def test_supplier_quotation_from_zero_qty_rfq_in_portal(self):
rfq = make_request_for_quotation(qty=0)
rfq.supplier = rfq.suppliers[0].supplier
sq_name = create_supplier_quotation(rfq)
sq = frappe.get_doc("Supplier Quotation", sq_name)
self.assertEqual(len(sq.items), 1)
self.assertEqual(sq.items[0].qty, 0)
self.assertEqual(sq.items[0].item_code, rfq.items[0].item_code)
def make_request_for_quotation(**args) -> "RequestforQuotation":
"""
@@ -184,14 +231,17 @@ def make_request_for_quotation(**args) -> "RequestforQuotation":
"description": "_Test Item",
"uom": args.uom or "_Test UOM",
"stock_uom": args.stock_uom or "_Test UOM",
"qty": args.qty or 5,
"qty": args.qty if args.qty is not None else 5,
"conversion_factor": args.conversion_factor or 1.0,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"schedule_date": nowdate(),
},
)
rfq.submit()
if not args.do_not_save:
rfq.insert()
if not args.do_not_submit:
rfq.submit()
return rfq

View File

@@ -11,6 +11,11 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
Quotation: "Quotation",
};
const me = this;
this.frm.set_indicator_formatter("item_code", function (doc) {
return !doc.qty && me.frm.doc.has_unit_price_items ? "yellow" : "";
});
super.setup();
}
@@ -26,6 +31,8 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
} else if (this.frm.doc.docstatus === 0) {
erpnext.set_unit_price_items_note(this.frm);
this.frm.add_custom_button(
__("Material Request"),
function () {

View File

@@ -19,6 +19,7 @@
"transaction_date",
"valid_till",
"quotation_number",
"has_unit_price_items",
"amended_from",
"accounting_dimensions_section",
"cost_center",
@@ -921,14 +922,23 @@
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"default": "0",
"fieldname": "has_unit_price_items",
"fieldtype": "Check",
"hidden": 1,
"label": "Has Unit Price Items",
"no_copy": 1
}
],
"grid_page_length": 50,
"icon": "fa fa-shopping-cart",
"idx": 29,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-03-28 10:20:30.231915",
"modified": "2025-03-03 17:39:38.459977",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
@@ -989,6 +999,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "status, transaction_date, supplier,grand_total",
"show_name_in_global_search": 1,
"sort_field": "modified",

View File

@@ -60,6 +60,7 @@ class SupplierQuotation(BuyingController):
discount_amount: DF.Currency
grand_total: DF.Currency
group_same_items: DF.Check
has_unit_price_items: DF.Check
ignore_pricing_rule: DF.Check
in_words: DF.Data | None
incoterm: DF.Link | None
@@ -103,6 +104,10 @@ class SupplierQuotation(BuyingController):
valid_till: DF.Date | None
# end: auto-generated types
def before_validate(self):
self.set_has_unit_price_items()
self.flags.allow_zero_qty = self.has_unit_price_items
def validate(self):
super().validate()
@@ -129,6 +134,17 @@ class SupplierQuotation(BuyingController):
def on_trash(self):
pass
def set_has_unit_price_items(self):
"""
If permitted in settings and any item has 0 qty, the SQ has unit price items.
"""
if not frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_supplier_quotation"):
return
self.has_unit_price_items = any(
not row.qty for row in self.get("items") if (row.item_code and not row.qty)
)
def validate_with_previous_doc(self):
super().validate_with_previous_doc(
{

View File

@@ -3,14 +3,26 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, today
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
from erpnext.controllers.accounts_controller import InvalidQtyError
class TestPurchaseOrder(FrappeTestCase):
def test_make_purchase_order(self):
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
def test_supplier_quotation_qty(self):
sq = frappe.copy_doc(test_records[0])
sq.items[0].qty = 0
with self.assertRaises(InvalidQtyError):
sq.save()
# No error with qty=1
sq.items[0].qty = 1
sq.save()
self.assertEqual(sq.items[0].qty, 1)
def test_make_purchase_order(self):
sq = frappe.copy_doc(test_records[0]).insert()
self.assertRaises(frappe.ValidationError, make_purchase_order, sq.name)
@@ -30,5 +42,16 @@ class TestPurchaseOrder(FrappeTestCase):
po.insert()
@change_settings("Buying Settings", {"allow_zero_qty_in_supplier_quotation": 1})
def test_map_purchase_order_from_zero_qty_supplier_quotation(self):
sq = frappe.copy_doc(test_records[0]).insert()
sq.items[0].qty = 0
sq.submit()
po = make_purchase_order(sq.name)
self.assertEqual(len(po.get("items")), 1)
self.assertEqual(po.get("items")[0].qty, 0)
self.assertEqual(po.get("items")[0].item_code, sq.get("items")[0].item_code)
test_records = frappe.get_test_records("Supplier Quotation")

View File

@@ -10,7 +10,7 @@ frappe.query_reports["Purchase Order Analysis"] = {
width: "80",
options: "Company",
reqd: 1,
default: frappe.defaults.get_default("company"),
default: frappe.defaults.get_user_default("Company"),
},
{
fieldname: "from_date",

View File

@@ -20,6 +20,9 @@ def update_last_purchase_rate(doc, is_submit) -> None:
this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date"))
for d in doc.get("items"):
if d.get("is_free_item"):
continue
# get last purchase details
last_purchase_details = get_last_purchase_details(d.item_code, doc.name)
@@ -46,11 +49,6 @@ def update_last_purchase_rate(doc, is_submit) -> None:
def validate_for_items(doc) -> None:
items = []
for d in doc.get("items"):
if not d.qty:
if doc.doctype == "Purchase Receipt" and d.rejected_qty:
continue
frappe.throw(_("Please enter quantity for Item {0}").format(d.item_code))
set_stock_levels(row=d) # update with latest quantities
item = validate_item_and_get_basic_data(row=d)
validate_stock_item_warehouse(row=d, item=item)

View File

@@ -0,0 +1,11 @@
There was a bug in the **Additional Discount** functionality of ERPNext in **v15.64.0**. This has since been fixed.
**If you've updated from a version older than v15.64.0, no action is needed on your side.**
If you're updating from v15.64.0, the **Additional Discount Amount** in some transactions may differ from the value you entered. This only affects cases where **Additional Discount Amount** is manually entered. If it is computed from **Additional Discount Percentage** entered by you, there shouldn't be any issue.
This report can help identify such transactions: [Calculated Discount Mismatch](/app/query-report/Calculated%20Discount%20Mismatch)
Please review and amend these as necessary.
We apologize for the inconvenience caused.

View File

@@ -230,6 +230,8 @@ class AccountsController(TransactionBase):
self.validate_party_accounts()
self.validate_inter_company_reference()
# validate inter company transaction rate
self.validate_internal_transaction()
self.disable_pricing_rule_on_internal_transfer()
self.disable_tax_included_prices_for_internal_transfer()
@@ -649,6 +651,9 @@ class AccountsController(TransactionBase):
self.base_paid_amount = flt(
self.paid_amount * self.conversion_rate, self.precision("base_paid_amount")
)
else:
self.paid_amount = 0
self.base_paid_amount = 0
def set_missing_values(self, for_validate=False):
if frappe.flags.in_test:
@@ -737,6 +742,91 @@ class AccountsController(TransactionBase):
msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer"
frappe.throw(_(msg), title=_("Internal Transfer Reference Missing"))
def validate_internal_transaction(self):
if not cint(
frappe.db.get_single_value("Accounts Settings", "maintain_same_internal_transaction_rate")
):
return
doctypes_list = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
if self.doctype in doctypes_list and (
self.get("is_internal_customer") or self.get("is_internal_supplier")
):
self.validate_internal_transaction_based_on_voucher_type()
def validate_internal_transaction_based_on_voucher_type(self):
order = ["Sales Order", "Purchase Order"]
invoice = ["Sales Invoice", "Purchase Invoice"]
if self.doctype in order and self.get("inter_company_order_reference"):
# Fetch the linked order
linked_doctype = "Sales Order" if self.doctype == "Purchase Order" else "Purchase Order"
self.validate_line_items(
linked_doctype,
"sales_order" if linked_doctype == "Sales Order" else "purchase_order",
"sales_order_item" if linked_doctype == "Sales Order" else "purchase_order_item",
)
elif self.doctype in invoice and self.get("inter_company_invoice_reference"):
# Fetch the linked invoice
linked_doctype = "Sales Invoice" if self.doctype == "Purchase Invoice" else "Purchase Invoice"
self.validate_line_items(
linked_doctype,
"sales_invoice" if linked_doctype == "Sales Invoice" else "purchase_invoice",
"sales_invoice_item" if linked_doctype == "Sales Invoice" else "purchase_invoice_item",
)
def validate_line_items(self, ref_dt, ref_dn_field, ref_link_field):
action, role_allowed_to_override = frappe.get_cached_value(
"Accounts Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
)
reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
reference_details = self.get_reference_details(reference_names, ref_dt + " Item")
stop_actions = []
for d in self.get("items"):
if d.get(ref_link_field):
ref_rate = reference_details.get(d.get(ref_link_field))
if ref_rate is not None and abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
if action == "Stop":
user_roles = [
r["role"]
for r in frappe.get_all(
"Has Role", filters={"parent": frappe.session.user}, fields=["role"]
)
]
if role_allowed_to_override not in user_roles:
stop_actions.append(
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
d.idx,
ref_dt,
self.inter_company_invoice_reference
if d.parenttype in ("Sales Invoice", "Purchase Invoice")
else d.get(ref_dn_field),
d.rate,
ref_rate,
)
)
else:
frappe.msgprint(
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
d.idx,
ref_dt,
self.inter_company_invoice_reference
if d.parenttype in ("Sales Invoice", "Purchase Invoice")
else d.get(ref_dn_field),
d.rate,
ref_rate,
),
title=_("Warning"),
indicator="orange",
)
if stop_actions:
frappe.throw(stop_actions, as_list=True)
def disable_pricing_rule_on_internal_transfer(self):
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
self.ignore_pricing_rule = 1
@@ -1148,6 +1238,8 @@ class AccountsController(TransactionBase):
with temporary_flag("company", self.company):
update_gl_dict_with_regional_fields(self, gl_dict)
update_gl_dict_with_app_based_fields(self, gl_dict)
accounting_dimensions = get_accounting_dimensions()
dimension_dict = frappe._dict()
@@ -1252,13 +1344,18 @@ class AccountsController(TransactionBase):
)
def validate_qty_is_not_zero(self):
if self.doctype == "Purchase Receipt":
if self.flags.allow_zero_qty:
return
for item in self.items:
if self.doctype == "Purchase Receipt" and item.rejected_qty:
continue
if not flt(item.qty):
frappe.throw(
msg=_("Row #{0}: Item quantity cannot be zero").format(item.idx),
msg=_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
item.idx, frappe.bold(item.item_code)
),
title=_("Invalid Quantity"),
exc=InvalidQtyError,
)
@@ -3589,7 +3686,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
def validate_quantity(child_item, new_data):
if not flt(new_data.get("qty")):
frappe.throw(
_("Row # {0}: Quantity for Item {1} cannot be zero").format(
_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
),
title=_("Invalid Qty"),
@@ -3730,9 +3827,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
)
if amount_below_billed_amt and row_rate > 0.0:
frappe.throw(
_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.").format(
child_item.idx, child_item.item_code
)
_(
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
).format(child_item.idx, child_item.item_code)
)
else:
child_item.rate = row_rate
@@ -3929,3 +4026,8 @@ def validate_einvoice_fields(doc):
@erpnext.allow_regional
def update_gl_dict_with_regional_fields(doc, gl_dict):
pass
def update_gl_dict_with_app_based_fields(doc, gl_dict):
for method in frappe.get_hooks("update_gl_dict_with_app_based_fields", default=[]):
frappe.get_attr(method)(doc, gl_dict)

View File

@@ -70,6 +70,14 @@ class BuyingController(SubcontractingController):
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on"),
)
if self.docstatus == 1 and self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
self.set_onload(
"allow_to_make_qc_after_submission",
frappe.db.get_single_value(
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
),
)
def create_package_for_transfer(self) -> None:
"""Create serial and batch package for Sourece Warehouse in case of inter transfer."""
@@ -650,6 +658,10 @@ class BuyingController(SubcontractingController):
sl_entries.append(from_warehouse_sle)
if flt(d.rejected_qty) != 0:
valuation_rate_for_rejected_item = 0.0
if frappe.db.get_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials"):
valuation_rate_for_rejected_item = d.valuation_rate
sl_entries.append(
self.get_sl_entries(
d,
@@ -658,7 +670,8 @@ class BuyingController(SubcontractingController):
"actual_qty": flt(
flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty")
),
"incoming_rate": 0.0,
"incoming_rate": valuation_rate_for_rejected_item if not self.is_return else 0.0,
"outgoing_rate": valuation_rate_for_rejected_item if self.is_return else 0.0,
"serial_and_batch_bundle": d.rejected_serial_and_batch_bundle,
},
)

View File

@@ -394,7 +394,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
doctype = "Batch"
meta = frappe.get_meta(doctype, cached=True)
searchfields = meta.get_search_fields()
page_len = 30
page_len = 300
batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len)
batches.extend(get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len))

View File

@@ -342,7 +342,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
def make_return_doc(doctype: str, source_name: str, target_doc=None, return_against_rejected_qty=False):
from frappe.model.mapper import get_mapped_doc
company = frappe.db.get_value("Delivery Note", source_name, "company")
company = frappe.db.get_value(doctype, source_name, "company")
default_warehouse_for_sales_return = frappe.get_cached_value(
"Company", company, "default_warehouse_for_sales_return"
)
@@ -431,99 +431,6 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
else:
doc.run_method("calculate_taxes_and_totals")
def update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
returned_serial_nos = []
returned_batches = frappe._dict()
serial_and_batch_field = (
"serial_and_batch_bundle" if qty_field == "stock_qty" else "rejected_serial_and_batch_bundle"
)
old_serial_no_field = "serial_no" if qty_field == "stock_qty" else "rejected_serial_no"
old_batch_no_field = "batch_no"
if (
source_doc.get(serial_and_batch_field)
or source_doc.get(old_serial_no_field)
or source_doc.get(old_batch_no_field)
):
if item_details.has_serial_no:
returned_serial_nos = get_returned_serial_nos(
source_doc, source_parent, serial_no_field=serial_and_batch_field
)
else:
returned_batches = get_returned_batches(
source_doc, source_parent, batch_no_field=serial_and_batch_field
)
type_of_transaction = "Inward"
if source_doc.get(serial_and_batch_field) and (
frappe.db.get_value(
"Serial and Batch Bundle", source_doc.get(serial_and_batch_field), "type_of_transaction"
)
== "Inward"
):
type_of_transaction = "Outward"
elif source_parent.doctype in [
"Purchase Invoice",
"Purchase Receipt",
"Subcontracting Receipt",
]:
type_of_transaction = "Outward"
warehouse = source_doc.warehouse if qty_field == "stock_qty" else source_doc.rejected_warehouse
if source_parent.doctype in [
"Sales Invoice",
"POS Invoice",
"Delivery Note",
] and source_parent.get("is_internal_customer"):
type_of_transaction = "Outward"
warehouse = source_doc.target_warehouse
cls_obj = SerialBatchCreation(
{
"type_of_transaction": type_of_transaction,
"serial_and_batch_bundle": source_doc.get(serial_and_batch_field),
"returned_against": source_doc.name,
"item_code": source_doc.item_code,
"returned_serial_nos": returned_serial_nos,
"voucher_type": source_parent.doctype,
"do_not_submit": True,
"warehouse": warehouse,
"has_serial_no": item_details.has_serial_no,
"has_batch_no": item_details.has_batch_no,
}
)
serial_nos = []
batches = frappe._dict()
if source_doc.get(old_batch_no_field):
batches = frappe._dict({source_doc.batch_no: source_doc.get(qty_field)})
elif source_doc.get(old_serial_no_field):
serial_nos = get_serial_nos(source_doc.get(old_serial_no_field))
elif source_doc.get(serial_and_batch_field):
if item_details.has_serial_no:
serial_nos = get_serial_nos_from_bundle(source_doc.get(serial_and_batch_field))
else:
batches = get_batches_from_bundle(source_doc.get(serial_and_batch_field))
if serial_nos:
cls_obj.serial_nos = sorted(list(set(serial_nos) - set(returned_serial_nos)))
elif batches:
for batch in batches:
if batch in returned_batches:
batches[batch] -= flt(returned_batches.get(batch))
cls_obj.batches = batches
if source_doc.get(serial_and_batch_field):
cls_obj.duplicate_package()
if cls_obj.serial_and_batch_bundle:
target_doc.set(serial_and_batch_field, cls_obj.serial_and_batch_bundle)
else:
target_doc.set(serial_and_batch_field, cls_obj.make_serial_and_batch_bundle().name)
def update_item(source_doc, target_doc, source_parent):
target_doc.qty = -1 * source_doc.qty
target_doc.pricing_rules = None
@@ -866,8 +773,6 @@ def get_returned_serial_nos(child_doc, parent_doc, serial_no_field=None, ignore_
def get_returned_batches(child_doc, parent_doc, batch_no_field=None, ignore_voucher_detail_no=None):
from erpnext.stock.serial_batch_bundle import get_batches_from_bundle
batches = frappe._dict()
old_field = "batch_no"

View File

@@ -31,6 +31,14 @@ class SellingController(StockController):
)
)
if self.docstatus == 1 and self.doctype in ["Delivery Note", "Sales Invoice"]:
self.set_onload(
"allow_to_make_qc_after_submission",
frappe.db.get_single_value(
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
),
)
def validate(self):
super().validate()
self.validate_items()
@@ -316,9 +324,6 @@ class SellingController(StockController):
def get_item_list(self):
il = []
for d in self.get("items"):
if d.qty is None:
frappe.throw(_("Row {0}: Qty is mandatory").format(d.idx))
if self.has_product_bundle(d.item_code):
for p in self.get("packed_items"):
if p.parent_detail_docname == d.name and p.parent_item == d.item_code:

View File

@@ -892,7 +892,7 @@ class StockController(AccountsController):
or sl_dict.actual_qty < 0
and self.get("is_return")
)
and self.doctype in ["Purchase Invoice", "Purchase Receipt"]
and self.doctype in ["Purchase Invoice", "Purchase Receipt", "Stock Entry"]
) or (
(
sl_dict.actual_qty < 0
@@ -902,6 +902,15 @@ class StockController(AccountsController):
)
and self.doctype in ["Sales Invoice", "Delivery Note", "Stock Entry"]
):
if self.doctype == "Stock Entry":
if row.get("t_warehouse") == sl_dict.warehouse and sl_dict.get("actual_qty") > 0:
fieldname = f"to_{dimension.source_fieldname}"
if dimension.source_fieldname.startswith("to_"):
fieldname = f"{dimension.source_fieldname}"
sl_dict[dimension.target_fieldname] = row.get(fieldname)
return
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
else:
fieldname_start_with = "to"

View File

@@ -20,14 +20,14 @@ def get_columns(filters, trans):
columns = (
based_on_details["based_on_cols"]
+ period_cols
+ [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency:120"]
+ [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency/currency:120"]
)
if group_by_cols:
columns = (
based_on_details["based_on_cols"]
+ group_by_cols
+ period_cols
+ [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency:120"]
+ [_("Total(Qty)") + ":Float:120", _("Total(Amt)") + ":Currency/currency:120"]
)
conditions = {
@@ -157,7 +157,7 @@ def get_data(filters, conditions):
# get data for group_by filter
row1 = frappe.db.sql(
""" select {} , {} from `tab{}` t1, `tab{} Item` t2 {}
""" select t1.currency , {} , {} from `tab{}` t1, `tab{} Item` t2 {}
where t2.parent = t1.name and t1.company = {} and {} between {} and {}
and t1.docstatus = 1 and {} = {} and {} = {} {} {}
""".format(
@@ -182,6 +182,7 @@ def get_data(filters, conditions):
)
des[ind] = row[i][0]
des[ind - 1] = row1[0][0]
for j in range(1, len(conditions["columns"]) - inc):
des[j + inc] = row1[0][j]
@@ -236,7 +237,7 @@ def period_wise_columns_query(filters, trans):
else:
pwc = [
_(filters.get("fiscal_year")) + " (" + _("Qty") + "):Float:120",
_(filters.get("fiscal_year")) + " (" + _("Amt") + "):Currency:120",
_(filters.get("fiscal_year")) + " (" + _("Amt") + "):Currency/currency:120",
]
query_details = " SUM(t2.stock_qty), SUM(t2.base_net_amount),"
@@ -248,12 +249,17 @@ def get_period_wise_columns(bet_dates, period, pwc):
if period == "Monthly":
pwc += [
_(get_mon(bet_dates[0])) + " (" + _("Qty") + "):Float:120",
_(get_mon(bet_dates[0])) + " (" + _("Amt") + "):Currency:120",
_(get_mon(bet_dates[0])) + " (" + _("Amt") + "):Currency/currency:120",
]
else:
pwc += [
_(get_mon(bet_dates[0])) + "-" + _(get_mon(bet_dates[1])) + " (" + _("Qty") + "):Float:120",
_(get_mon(bet_dates[0])) + "-" + _(get_mon(bet_dates[1])) + " (" + _("Amt") + "):Currency:120",
_(get_mon(bet_dates[0]))
+ "-"
+ _(get_mon(bet_dates[1]))
+ " ("
+ _("Amt")
+ "):Currency/currency:120",
]
@@ -375,6 +381,9 @@ def based_wise_columns_query(based_on, trans):
else:
frappe.throw(_("Project-wise data is not available for Quotation"))
based_on_details["based_on_select"] += "t1.currency,"
based_on_details["based_on_cols"].append("Currency:Link/Currency:120")
return based_on_details

View File

@@ -29,4 +29,10 @@ frappe.ui.form.on("Contract", {
});
}
},
party_name: function (frm) {
let field = frm.doc.party_type.toLowerCase() + "_name";
frappe.db.get_value(frm.doc.party_type, frm.doc.party_name, field, (r) => {
frm.set_value("party_full_name", r[field]);
});
},
});

View File

@@ -14,6 +14,7 @@
"party_user",
"status",
"fulfilment_status",
"party_full_name",
"sb_terms",
"start_date",
"cb_date",
@@ -244,11 +245,18 @@
"fieldname": "authorised_by_section",
"fieldtype": "Section Break",
"label": "Authorised By"
},
{
"fieldname": "party_full_name",
"fieldtype": "Data",
"label": "Party Full Name",
"read_only": 1
}
],
"grid_page_length": 50,
"is_submittable": 1,
"links": [],
"modified": "2020-12-07 11:15:58.385521",
"modified": "2025-05-23 13:54:03.346537",
"modified_by": "Administrator",
"module": "CRM",
"name": "Contract",
@@ -315,9 +323,10 @@
"write": 1
}
],
"row_format": "Dynamic",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -34,6 +34,7 @@ class Contract(Document):
fulfilment_terms: DF.Table[ContractFulfilmentChecklist]
ip_address: DF.Data | None
is_signed: DF.Check
party_full_name: DF.Data | None
party_name: DF.DynamicLink
party_type: DF.Literal["Customer", "Supplier", "Employee"]
party_user: DF.Link | None
@@ -59,10 +60,17 @@ class Contract(Document):
self.name = _(name)
def validate(self):
self.set_missing_values()
self.validate_dates()
self.update_contract_status()
self.update_fulfilment_status()
def set_missing_values(self):
if not self.party_full_name:
field = self.party_type.lower() + "_name"
if res := frappe.db.get_value(self.party_type, self.party_name, field):
self.party_full_name = res
def before_submit(self):
self.signed_by_company = frappe.session.user

View File

@@ -480,6 +480,10 @@ erpnext.bom.BomController = class BomController extends erpnext.TransactionContr
child.bom_no = "";
}
if (doc.item == child.item_code) {
child.do_not_explode = 1;
}
get_bom_material_detail(doc, cdt, cdn, scrap_items);
}

View File

@@ -131,6 +131,7 @@
"fieldname": "quantity",
"fieldtype": "Float",
"label": "Quantity",
"non_negative": 1,
"oldfieldname": "quantity",
"oldfieldtype": "Currency",
"reqd": 1
@@ -637,7 +638,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2024-06-03 16:24:47.518411",
"modified": "2025-06-16 16:13:22.497695",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
@@ -670,6 +671,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "item, item_name",
"show_name_in_global_search": 1,
"sort_field": "modified",

View File

@@ -7,7 +7,7 @@ from collections import deque
from operator import itemgetter
import frappe
from frappe import _
from frappe import _, bold
from frappe.core.doctype.version.version import get_diff
from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, cstr, flt, today
@@ -655,9 +655,16 @@ class BOM(WebsiteGenerator):
def check_recursion(self, bom_list=None):
"""Check whether recursion occurs in any bom"""
def _throw_error(bom_name):
def _throw_error(bom_name, production_item=None):
msg = _("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name)
if production_item and bom_name != self.name:
msg += "<br><br>"
msg += _(
"Note: If you want to use the finished good {0} as a raw material, then enable the 'Do Not Explode' checkbox in the Items table against the same raw material."
).format(bold(production_item))
frappe.throw(
_("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name),
msg,
exc=BOMRecursionError,
)
@@ -674,7 +681,7 @@ class BOM(WebsiteGenerator):
if self.item == item.item_code and item.bom_no:
# Same item but with different BOM should not be allowed.
# Same item can appear recursively once as long as it doesn't have BOM.
_throw_error(item.bom_no)
_throw_error(item.bom_no, self.item)
if self.name in {d.bom_no for d in self.items}:
_throw_error(self.name)

View File

@@ -343,6 +343,7 @@ def get_children(doctype=None, parent=None, **kwargs):
fields = [
"item_code as value",
"item_name as title",
"is_expandable as expandable",
"parent as parent_id",
"qty",

View File

@@ -705,7 +705,7 @@ class JobCard(Document):
bold("Job Card"), get_link_to_form("Job Card", self.name)
)
)
else:
elif frappe.db.get_single_value("Manufacturing Settings", "enforce_time_logs"):
for row in self.time_logs:
if not row.from_time or not row.to_time:
frappe.throw(

View File

@@ -26,6 +26,7 @@
"overproduction_percentage_for_work_order",
"job_card_section",
"add_corrective_operation_cost_in_finished_good_valuation",
"enforce_time_logs",
"column_break_24",
"job_card_excess_transfer",
"capacity_planning",
@@ -235,13 +236,20 @@
"fieldname": "set_op_cost_and_scrap_from_sub_assemblies",
"fieldtype": "Check",
"label": "Set Operating Cost / Scrap Items From Sub-assemblies"
},
{
"default": "0",
"description": "Enabling this checkbox will force each Job Card Time Log to have From Time and To Time",
"fieldname": "enforce_time_logs",
"fieldtype": "Check",
"label": "Enforce Time Logs"
}
],
"icon": "icon-wrench",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-02-05 16:11:11.639916",
"modified": "2025-05-16 11:23:16.916512",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing Settings",
@@ -259,4 +267,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -26,6 +26,7 @@ class ManufacturingSettings(Document):
default_scrap_warehouse: DF.Link | None
default_wip_warehouse: DF.Link | None
disable_capacity_planning: DF.Check
enforce_time_logs: DF.Check
get_rm_cost_from_consumption_entry: DF.Check
job_card_excess_transfer: DF.Check
make_serial_no_batch_from_work_order: DF.Check

View File

@@ -116,7 +116,9 @@ frappe.ui.form.on("Production Plan", {
);
}
if (frm.doc.po_items && frm.doc.status !== "Closed") {
let items = frm.events.get_items_for_work_order(frm);
if (items?.length && frm.doc.status !== "Closed") {
frm.add_custom_button(
__("Work Order / Subcontract PO"),
() => {
@@ -193,6 +195,24 @@ frappe.ui.form.on("Production Plan", {
set_field_options("projected_qty_formula", projected_qty_formula);
},
get_items_for_work_order(frm) {
let items = frm.doc.po_items;
if (frm.doc.sub_assembly_items?.length) {
items = [...items, ...frm.doc.sub_assembly_items];
}
let has_items =
items.filter((item) => {
if (item.pending_qty) {
return item.pending_qty > item.ordered_qty;
} else {
return item.qty > (item.received_qty || item.ordered_qty);
}
}) || [];
return has_items;
},
close_open_production_plan(frm, close = false) {
frappe.call({
method: "set_status",

View File

@@ -751,7 +751,14 @@ class ProductionPlan(Document):
"company": self.get("company"),
}
if flt(row.qty) <= flt(row.ordered_qty):
continue
self.prepare_data_for_sub_assembly_items(row, work_order_data)
if work_order_data.get("qty") <= 0:
continue
work_order = self.create_work_order(work_order_data)
if work_order:
wo_list.append(work_order)
@@ -771,6 +778,8 @@ class ProductionPlan(Document):
if row.get(field):
wo_data[field] = row.get(field)
wo_data["qty"] = flt(row.get("qty")) - flt(row.get("ordered_qty"))
wo_data.update(
{
"use_multi_level_bom": 0,
@@ -1175,6 +1184,7 @@ def get_exploded_items(item_details, company, bom_no, include_non_stock_items, p
item.purchase_uom,
item_uom.conversion_factor,
item.safety_stock,
bom.item.as_("main_bom_item"),
)
.where(
(bei.docstatus < 2)
@@ -1242,6 +1252,7 @@ def get_subitems(
item_default.default_warehouse,
item.purchase_uom,
item_uom.conversion_factor,
bom.item.as_("main_bom_item"),
)
.where(
(bom.name == bom_no)
@@ -1355,6 +1366,7 @@ def get_material_request_items(
"sales_order": sales_order,
"description": row.get("description"),
"uom": row.get("purchase_uom") or row.get("stock_uom"),
"main_bom_item": row.get("main_bom_item"),
}
@@ -1916,6 +1928,7 @@ def get_raw_materials_of_sub_assembly_items(
item.purchase_uom,
item_uom.conversion_factor,
item.safety_stock,
bom.item.as_("main_bom_item"),
)
.where(
(bei.docstatus == 1)

View File

@@ -1693,6 +1693,63 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(mr_items[0].get("quantity"), 80)
self.assertEqual(mr_items[1].get("quantity"), 70)
def test_production_plan_for_partial_sub_assembly_items(self):
from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.subcontracting.doctype.subcontracting_bom.test_subcontracting_bom import (
create_subcontracting_bom,
)
frappe.flags.test_print = False
fg_wo_item = "Test Motherboard 11"
bom_tree_1 = {"Test Laptop 11": {fg_wo_item: {"Test Motherboard Wires 11": {}}}}
create_nested_bom(bom_tree_1, prefix="")
plan = create_production_plan(
item_code="Test Laptop 11",
planned_qty=10,
use_multi_level_bom=1,
do_not_submit=True,
company="_Test Company",
skip_getting_mr_items=True,
)
plan.get_sub_assembly_items()
plan.submit()
plan.make_work_order()
work_order = frappe.db.get_value("Work Order", {"production_plan": plan.name, "docstatus": 0}, "name")
wo_doc = frappe.get_doc("Work Order", work_order)
wo_doc.qty = 5.0
wo_doc.skip_transfer = 1
wo_doc.from_wip_warehouse = 1
wo_doc.wip_warehouse = "_Test Warehouse - _TC"
wo_doc.fg_warehouse = "_Test Warehouse - _TC"
wo_doc.submit()
plan.reload()
for row in plan.sub_assembly_items:
self.assertEqual(row.ordered_qty, 5.0)
plan.make_work_order()
work_order = frappe.db.get_value("Work Order", {"production_plan": plan.name, "docstatus": 0}, "name")
wo_doc = frappe.get_doc("Work Order", work_order)
self.assertEqual(wo_doc.qty, 5.0)
wo_doc.skip_transfer = 1
wo_doc.from_wip_warehouse = 1
wo_doc.wip_warehouse = "_Test Warehouse - _TC"
wo_doc.fg_warehouse = "_Test Warehouse - _TC"
wo_doc.submit()
plan.reload()
for row in plan.sub_assembly_items:
self.assertEqual(row.ordered_qty, 10.0)
def create_production_plan(**args):
"""

View File

@@ -21,6 +21,7 @@
"purchase_order",
"production_plan_item",
"column_break_7",
"ordered_qty",
"received_qty",
"indent",
"section_break_19",
@@ -204,12 +205,19 @@
"fieldtype": "Float",
"label": "Produced Qty",
"read_only": 1
},
{
"fieldname": "ordered_qty",
"fieldtype": "Float",
"label": "Ordered Qty",
"no_copy": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-02-27 13:45:17.422435",
"modified": "2025-06-10 13:36:24.759101",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan Sub Assembly Item",
@@ -220,4 +228,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -22,6 +22,7 @@ class ProductionPlanSubAssemblyItem(Document):
fg_warehouse: DF.Link | None
indent: DF.Int
item_name: DF.Data | None
ordered_qty: DF.Float
parent: DF.Data
parent_item_code: DF.Link | None
parentfield: DF.Data

View File

@@ -101,6 +101,17 @@ frappe.ui.form.on("Work Order", {
};
});
frm.set_query("sales_order", function () {
if (frm.doc.production_item) {
return {
query: "erpnext.manufacturing.doctype.work_order.work_order.query_sales_order",
filters: {
production_item: frm.doc.production_item,
},
};
}
});
// formatter for work order operation
frm.set_indicator_formatter("operation", function (doc) {
return frm.doc.qty == doc.completed_qty ? "green" : "orange";
@@ -481,7 +492,6 @@ frappe.ui.form.on("Work Order", {
callback: function (r) {
if (r.message) {
frm.set_value("sales_order", "");
frm.trigger("set_sales_order");
erpnext.in_production_item_onchange = true;
$.each(
@@ -543,23 +553,6 @@ frappe.ui.form.on("Work Order", {
frm.toggle_reqd("transfer_material_against", frm.doc.operations && frm.doc.operations.length > 0);
},
set_sales_order: function (frm) {
if (frm.doc.production_item) {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.query_sales_order",
args: { production_item: frm.doc.production_item },
callback: function (r) {
frm.set_query("sales_order", function () {
erpnext.in_production_item_onchange = true;
return {
filters: [["Sales Order", "name", "in", r.message]],
};
});
},
});
}
},
additional_operating_cost: function (frm) {
erpnext.work_order.calculate_cost(frm.doc);
erpnext.work_order.calculate_total_cost(frm);

View File

@@ -762,22 +762,34 @@ class WorkOrder(Document):
)
def update_ordered_qty(self):
if self.production_plan and self.production_plan_item and not self.production_plan_sub_assembly_item:
if self.production_plan and (self.production_plan_item or self.production_plan_sub_assembly_item):
table = frappe.qb.DocType("Work Order")
query = (
frappe.qb.from_(table)
.select(Sum(table.qty))
.where(
(table.production_plan == self.production_plan)
& (table.production_plan_item == self.production_plan_item)
& (table.docstatus == 1)
)
).run()
.where((table.production_plan == self.production_plan) & (table.docstatus == 1))
)
if self.production_plan_item:
query = query.where(table.production_plan_item == self.production_plan_item)
elif self.production_plan_sub_assembly_item:
query = query.where(
table.production_plan_sub_assembly_item == self.production_plan_sub_assembly_item
)
query = query.run()
qty = flt(query[0][0]) if query else 0
frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty)
if self.production_plan_item:
frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty)
elif self.production_plan_sub_assembly_item:
frappe.db.set_value(
"Production Plan Sub Assembly Item",
self.production_plan_sub_assembly_item,
"ordered_qty",
qty,
)
doc = frappe.get_doc("Production Plan", self.production_plan)
doc.set_status()
@@ -1503,20 +1515,22 @@ def stop_unstop(work_order, status):
@frappe.whitelist()
def query_sales_order(production_item):
out = frappe.db.sql_list(
"""
select distinct so.name from `tabSales Order` so, `tabSales Order Item` so_item
where so_item.parent=so.name and so_item.item_code=%s and so.docstatus=1
union
select distinct so.name from `tabSales Order` so, `tabPacked Item` pi_item
where pi_item.parent=so.name and pi_item.item_code=%s and so.docstatus=1
""",
(production_item, production_item),
@frappe.validate_and_sanitize_search_inputs
def query_sales_order(doctype, txt, searchfield, start, page_len, filters) -> list[str]:
return frappe.get_list(
"Sales Order",
fields=["name"],
filters=[
["Sales Order", "docstatus", "=", 1],
],
or_filters=[
["Sales Order Item", "item_code", "=", filters.get("production_item")],
["Packed Item", "item_code", "=", filters.get("production_item")],
],
as_list=True,
distinct=True,
)
return out
@frappe.whitelist()
def make_job_card(work_order, operations):

View File

@@ -23,6 +23,7 @@ def get_columns():
"""return columns"""
columns = [
_("Item") + ":Link/Item:150",
_("Item Name") + "::240",
_("Description") + "::300",
_("BOM Qty") + ":Float:160",
_("BOM UoM") + "::160",
@@ -73,11 +74,12 @@ def get_bom_stock(filters):
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
.select(
BOM_ITEM.item_code,
BOM_ITEM.item_name,
BOM_ITEM.description,
BOM_ITEM.stock_qty,
BOM_ITEM.stock_uom,
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
Sum(BIN.actual_qty).as_("actual_qty"),
BIN.actual_qty.as_("actual_qty"),
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
)
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))

View File

@@ -94,6 +94,7 @@ def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
expected_data.append(
[
item.item_code,
item.item_name,
item.description,
item.stock_qty,
item.stock_uom,

View File

@@ -262,6 +262,7 @@ erpnext.patches.v14_0.clear_reconciliation_values_from_singles
execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True)
erpnext.patches.v14_0.update_proprietorship_to_individual
erpnext.patches.v15_0.rename_subcontracting_fields
erpnext.patches.v15_0.unset_incorrect_additional_discount_percentage
[post_model_sync]
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
@@ -404,3 +405,8 @@ erpnext.patches.v15_0.update_payment_schedule_fields_in_invoices
erpnext.patches.v15_0.rename_group_by_to_categorize_by
execute:frappe.db.set_single_value("Accounts Settings", "receivable_payable_fetch_method", "Buffered Cursor")
erpnext.patches.v14_0.set_update_price_list_based_on
erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice
erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
erpnext.patches.v14_0.update_full_name_in_contract
erpnext.patches.v15_0.drop_sle_indexes
erpnext.patches.v15_0.update_pegged_currencies

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