Compare commits

..

124 Commits

Author SHA1 Message Date
Frappe PR Bot
0854333e42 chore(release): Bumped to Version 15.69.0
# [15.69.0](https://github.com/frappe/erpnext/compare/v15.68.0...v15.69.0) (2025-07-09)

### Features

* batch rate (valuation) in Batch-Wise Balance History report ([ba7e0b9](ba7e0b9506))
2025-07-09 15:16:52 +00:00
rohitwaghchaure
48e0ce1132 Merge pull request #48498 from frappe/mergify/bp/version-15/pr-48489
feat: batch rate (valuation) in Batch-Wise Balance History report (backport #48487) (backport #48489)
2025-07-09 20:44:29 +05:30
Rohit Waghchaure
ba7e0b9506 feat: batch rate (valuation) in Batch-Wise Balance History report
(cherry picked from commit 8a2a845a16)
(cherry picked from commit facd2027c3)
2025-07-09 14:16:27 +00:00
Frappe PR Bot
a94a13a7c1 chore(release): Bumped to Version 15.68.0
# [15.68.0](https://github.com/frappe/erpnext/compare/v15.67.0...v15.68.0) (2025-07-08)

### Bug Fixes

* add company field on POS Invoice Merge Log (backport [#48357](https://github.com/frappe/erpnext/issues/48357)) ([#48414](https://github.com/frappe/erpnext/issues/48414)) ([26db582](26db582499))
* Add company validation to company related fields in Process Statement Of Accounts ([5362648](536264896e))
* add not specified key for None respresented customer_group and territory ([8371daf](8371dafb1a))
* add selling price validation on update item ([a10e394](a10e3948b2))
* address not found when creating internal PR from DN ([f1621d1](f1621d15ff))
* consider empty string in previous doc validation ([b2de9cd](b2de9cdef2))
* cost center for payment entry against advance payment doctypes in accounts Payable/Receivable report ([3f004db](3f004db14f))
* duplicate items being created when fetching items from warehouse in stock reco ([818ddc0](818ddc0b8c))
* fetch from parent optional in inventory dimension ([ed77c15](ed77c15ebc))
* get fiscal year based on date ([177b23c](177b23c624))
* incorrect pending qty when creating PI from PO and PI rates differ from PO (backport [#48173](https://github.com/frappe/erpnext/issues/48173)) ([#48340](https://github.com/frappe/erpnext/issues/48340)) ([8eede1d](8eede1d266))
* item list and project not being set in work order when created from material request ([5cd36c3](5cd36c318b))
* job card material request/transfer buttons UI overlap ([09f8660](09f866022b))
* LCV from PR order mismatch ([74948aa](74948aabda))
* make labels in error message translatable (backport [#48327](https://github.com/frappe/erpnext/issues/48327)) ([#48436](https://github.com/frappe/erpnext/issues/48436)) ([6b41dc2](6b41dc2fed))
* multiple fixes related Deferred Accounting ([a4633d6](a4633d6e75))
* pos recent order display customer code and name (backport [#48379](https://github.com/frappe/erpnext/issues/48379)) ([#48388](https://github.com/frappe/erpnext/issues/48388)) ([f25097d](f25097da1d))
* **Quotation:** hide buttons if user cannot use them (backport [#48115](https://github.com/frappe/erpnext/issues/48115)) ([#48405](https://github.com/frappe/erpnext/issues/48405)) ([a2436e4](a2436e4b6e))
* rate not being fetched for product bundles in material request ([cfedaf5](cfedaf5dc1))
* rename journal entry title on update ([b7b5f6a](b7b5f6acf3))
* unnecessary primary button ([b1abcd5](b1abcd5577))
* update condition for blank tree fields in pricing rule ([f2d644b](f2d644ba29))
* update item reference in quality inspection ([65c277f](65c277fd27))
* update payment request outstanding on unreconciliation ([450061c](450061c7db))
* use default buying price list when price list is falsy ([a336e19](a336e19bb8))
* valuation rate of raw materials in subcontracting receipt ([4545213](4545213adc))

### Features

* add price list field to material request (backport [#48425](https://github.com/frappe/erpnext/issues/48425)) ([#48429](https://github.com/frappe/erpnext/issues/48429)) ([d4700e5](d4700e5560))
* add subject field to project ([#48368](https://github.com/frappe/erpnext/issues/48368)) ([9a538c6](9a538c6843))

### Reverts

* do not convert exchange gain/loss amount to foreign currency ([d0d1d63](d0d1d63d31))
* Revert "fix: stock reco qty with inventory dimension ([#47918](https://github.com/frappe/erpnext/issues/47918))" ([9a99ccc](9a99ccc166))
2025-07-08 13:01:14 +00:00
ruthra kumar
e573c6094a Merge pull request #48462 from frappe/version-15-hotfix
chore: release v15
2025-07-08 18:29:42 +05:30
ruthra kumar
524619e0b4 Merge pull request #48467 from frappe/mergify/bp/version-15-hotfix/pr-48259
fix: add not specified key for None respresented customer_group and t… (backport #48259)
2025-07-08 17:48:30 +05:30
ruthra kumar
5f5fa2f8e1 Merge pull request #48466 from frappe/mergify/bp/version-15-hotfix/pr-48378
fix: update item reference in quality inspection (backport #48378)
2025-07-08 17:48:17 +05:30
pugazhendhivelu
65c277fd27 fix: update item reference in quality inspection
(cherry picked from commit 9da5010265)

# Conflicts:
#	erpnext/public/js/controllers/transaction.js
2025-07-08 17:16:50 +05:30
l0gesh29
8371dafb1a fix: add not specified key for None respresented customer_group and territory
(cherry picked from commit 24cc711a70)

# Conflicts:
#	erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
2025-07-08 17:13:30 +05:30
ruthra kumar
f8bc0e47ff Merge pull request #48464 from frappe/mergify/bp/version-15-hotfix/pr-48402
fix: rename journal entry title on update (backport #48402)
2025-07-08 17:11:27 +05:30
ruthra kumar
d3a95f04cc Merge pull request #48470 from frappe/mergify/bp/version-15-hotfix/pr-48469
chore: better label for checkbox (backport #48469)
2025-07-08 17:10:50 +05:30
ruthra kumar
5f63794739 chore: better label for checkbox
(cherry picked from commit 8c2e40e291)
2025-07-08 11:40:08 +00:00
ruthra kumar
41bca34f6a Merge pull request #48468 from frappe/mergify/bp/version-15-hotfix/pr-48427
fix: Add company validation to company related fields in Process Statement Of Accounts (backport #48427)
2025-07-08 16:23:47 +05:30
ruthra kumar
f4b7093838 Merge pull request #48465 from frappe/mergify/bp/version-15-hotfix/pr-48359
fix: get fiscal year based on date (backport #48359)
2025-07-08 16:21:03 +05:30
ljain112
536264896e fix: Add company validation to company related fields in Process Statement Of Accounts
(cherry picked from commit 4e45e69247)
2025-07-08 10:37:17 +00:00
l0gesh29
177b23c624 fix: get fiscal year based on date
(cherry picked from commit efb8e7c0e4)
2025-07-08 10:26:26 +00:00
ravibharathi656
f81dba6380 chore: add none value
(cherry picked from commit 9e633bddef)
2025-07-08 10:25:18 +00:00
ravibharathi656
b7b5f6acf3 fix: rename journal entry title on update
(cherry picked from commit acb9829159)
2025-07-08 10:25:18 +00:00
ruthra kumar
d60d8a2625 Merge pull request #48457 from frappe/mergify/bp/version-15-hotfix/pr-48361
revert: do not convert exchange gain/loss amount to foreign currency (backport #48361)
2025-07-08 15:45:34 +05:30
ruthra kumar
e0efe922a6 Merge pull request #48459 from frappe/mergify/bp/version-15-hotfix/pr-48328
fix: update condition for blank tree fields in pricing rule (backport #48328)
2025-07-08 15:44:42 +05:30
mergify[bot]
500972e8aa refactor: remove do_reposting_for_each_stock_transaction feature (backport #48444) (#48452)
* refactor: remove do_reposting_for_each_stock_transaction feature

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2025-07-08 15:05:35 +05:30
Mihir Kandoi
b833dfab9d Merge pull request #48460 from frappe/mergify/bp/version-15-hotfix/pr-48456
fix: duplicate items being created when fetching items from warehouse in stock reco (backport #48456)
2025-07-08 15:05:11 +05:30
Mihir Kandoi
818ddc0b8c fix: duplicate items being created when fetching items from warehouse in stock reco
(cherry picked from commit 73f6c29559)
2025-07-08 09:01:47 +00:00
venkat102
d0d1d63d31 revert: do not convert exchange gain/loss amount to foreign currency
(cherry picked from commit c17ae703c7)

# Conflicts:
#	erpnext/accounts/report/general_ledger/test_general_ledger.py
2025-07-08 14:30:10 +05:30
ljain112
f2d644ba29 fix: update condition for blank tree fields in pricing rule
(cherry picked from commit 7e0e9db4d2)
2025-07-08 08:53:35 +00:00
ruthra kumar
9e831bbfc6 Merge pull request #48448 from frappe/mergify/bp/version-15-hotfix/pr-48326
fix: cost center for payment entry against advance payment doctypes in accounts payable/rece (backport #48326)
2025-07-08 14:06:28 +05:30
ruthra kumar
7690efa4f7 Merge pull request #48454 from frappe/mergify/bp/version-15-hotfix/pr-48416
refactor: remove duplicate reconciliation date logic (backport #48416)
2025-07-08 13:50:32 +05:30
ljain112
00669661e5 chore: resolve conflicts 2025-07-08 13:19:03 +05:30
ljain112
2ab1b42033 refactor: remove duplicate reconciliation date logic
(cherry picked from commit 398406082a)
2025-07-08 07:34:23 +00:00
ruthra kumar
2c156b58f0 Merge pull request #48453 from frappe/mergify/bp/version-15-hotfix/pr-48343
fix: consider empty string in previous doc validation (backport #48343)
2025-07-08 12:59:52 +05:30
Diptanil Saha
c617b343b3 Merge pull request #48449 from frappe/mergify/bp/version-15-hotfix/pr-48403
fix: add selling price validation on update item (backport #48403)
2025-07-08 12:41:58 +05:30
l0gesh29
b2de9cdef2 fix: consider empty string in previous doc validation
(cherry picked from commit dd43594ad6)
2025-07-08 07:11:51 +00:00
rohitwaghchaure
ae59908f0a Merge pull request #48443 from frappe/mergify/bp/version-15-hotfix/pr-48441
Revert "fix: stock reco qty with inventory dimension" (backport #48441)
2025-07-08 12:30:40 +05:30
l0gesh29
a10e3948b2 fix: add selling price validation on update item
(cherry picked from commit 327d067305)
2025-07-08 06:45:18 +00:00
Mihir Kandoi
fbe1f449d8 Merge pull request #48447 from frappe/mergify/bp/version-15-hotfix/pr-48332
fix: valuation rate of raw materials in subcontracting receipt (backport #48332)
2025-07-08 12:14:53 +05:30
Mihir Kandoi
b3fa0ac596 Merge pull request #48446 from frappe/mergify/bp/version-15-hotfix/pr-48445
fix: use default buying price list when price list is falsy (backport #48445)
2025-07-08 12:14:36 +05:30
ljain112
82d03e2617 refactor: function to fetch advance payment doctypes
(cherry picked from commit 48e8e85617)

# Conflicts:
#	erpnext/accounts/doctype/journal_entry/journal_entry.py
#	erpnext/accounts/doctype/payment_entry/payment_entry.py
#	erpnext/accounts/doctype/payment_request/payment_request.py
#	erpnext/accounts/doctype/unreconcile_payment/unreconcile_payment.py
#	erpnext/accounts/utils.py
#	erpnext/controllers/accounts_controller.py
2025-07-08 06:36:06 +00:00
ljain112
3f004db14f fix: cost center for payment entry against advance payment doctypes in accounts Payable/Receivable report
(cherry picked from commit 8f19f14004)
2025-07-08 06:36:06 +00:00
Mihir Kandoi
4545213adc fix: valuation rate of raw materials in subcontracting receipt
(cherry picked from commit 84ea6afd01)
2025-07-08 06:28:44 +00:00
Mihir Kandoi
a336e19bb8 fix: use default buying price list when price list is falsy
(cherry picked from commit 27c73cf9e9)
2025-07-08 06:26:09 +00:00
rohitwaghchaure
9a99ccc166 Revert "fix: stock reco qty with inventory dimension (#47918)"
This reverts commit 342cebc778.

(cherry picked from commit 8ba66c9833)
2025-07-08 05:08:04 +00:00
ruthra kumar
9935f04bde Merge pull request #48440 from frappe/mergify/bp/version-15-hotfix/pr-48324
fix: update payment request outstanding on unreconciliation (backport #48324)
2025-07-08 10:33:36 +05:30
ruthra kumar
cc5c4a3f9a chore: resolve conflict 2025-07-08 10:17:43 +05:30
rohitwaghchaure
0fefccc128 Merge pull request #48434 from frappe/mergify/bp/version-15-hotfix/pr-48432
fix: fetch from parent optional in inventory dimension (backport #48432)
2025-07-08 09:58:27 +05:30
ljain112
a8448f9e60 chore: fix test case for payment request
(cherry picked from commit 31d12517f0)
2025-07-08 04:16:41 +00:00
ljain112
450061c7db fix: update payment request outstanding on unreconciliation
(cherry picked from commit 8098229b55)

# Conflicts:
#	erpnext/accounts/doctype/payment_request/test_payment_request.py
2025-07-08 04:16:40 +00:00
rohitwaghchaure
5df8ad6ef1 chore: fix conflicts 2025-07-08 08:33:33 +05:30
mergify[bot]
6b41dc2fed fix: make labels in error message translatable (backport #48327) (#48436)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: make labels in error message translatable (#48327)
2025-07-07 22:36:48 +02:00
Rohit Waghchaure
ed77c15ebc fix: fetch from parent optional in inventory dimension
(cherry picked from commit 8aac6a6b18)

# Conflicts:
#	erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
2025-07-07 11:52:53 +00:00
mergify[bot]
d4700e5560 feat: add price list field to material request (backport #48425) (#48429)
* feat: add price list field to material request

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2025-07-07 15:50:04 +05:30
Mihir Kandoi
f4c9a6e59c Merge pull request #48430 from frappe/mergify/bp/version-15-hotfix/pr-48428
fix: address not found when creating internal PR from DN (backport #48428)
2025-07-07 15:43:22 +05:30
Mihir Kandoi
f1621d15ff fix: address not found when creating internal PR from DN
(cherry picked from commit 97c48ed6d2)
2025-07-07 09:51:19 +00:00
Mihir Kandoi
8638654c13 Merge pull request #48426 from frappe/mergify/bp/version-15-hotfix/pr-48424
fix: item list and project not being set in work order when created from material request (backport #48424)
2025-07-07 15:05:15 +05:30
Mihir Kandoi
5cd36c318b fix: item list and project not being set in work order when created from material request
(cherry picked from commit 099a5fbad9)
2025-07-07 08:49:26 +00:00
mergify[bot]
8eede1d266 fix: incorrect pending qty when creating PI from PO and PI rates differ from PO (backport #48173) (#48340)
* fix: incorrect pending qty when creating PI from PO and PI rates differ from PO

Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2025-07-06 21:40:26 +05:30
mergify[bot]
26db582499 fix: add company field on POS Invoice Merge Log (backport #48357) (#48414)
* fix: add company field on POS Invoice Merge Log

(cherry picked from commit 109658731b)

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

* fix: patch for updating company name on existing pos merge log records

(cherry picked from commit d46b68230c)

# Conflicts:
#	erpnext/patches.txt

* fix: pass company on create_merge_logs

(cherry picked from commit b4b473185f)

* test: test company fetching from POS Closing Entry

(cherry picked from commit 9548f341bf)

# Conflicts:
#	erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py

* chore: remove conflicts

---------

Co-authored-by: Kavin <78342682+kavin0411@users.noreply.github.com>
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2025-07-06 15:25:59 +05:30
ruthra kumar
89458f07c9 Merge pull request #48415 from frappe/mergify/bp/version-15-hotfix/pr-47805
fix: multiple fixes related Deferred Accounting (backport #47805)
2025-07-06 15:25:16 +05:30
Lakshit Jain
a4633d6e75 fix: multiple fixes related Deferred Accounting
(cherry picked from commit 277c1101fc)
2025-07-06 14:59:23 +05:30
Mihir Kandoi
dbe107345c Merge pull request #48404 from frappe/mergify/bp/version-15-hotfix/pr-48368
feat: add subject field to project (backport #48368)
2025-07-05 12:07:00 +05:30
mergify[bot]
a2436e4b6e fix(Quotation): hide buttons if user cannot use them (backport #48115) (#48405)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix(Quotation): hide buttons if user cannot use them (#48115)
2025-07-04 20:10:27 +02:00
Mihir Kandoi
9a538c6843 feat: add subject field to project (#48368)
* feat: add subject field to project

(cherry picked from commit 407fdab487)
2025-07-04 16:10:19 +00:00
Mihir Kandoi
d8f5924eb2 Merge pull request #48373 from mihir-kandoi/st42781
fix: LCV from PR order mismatch
2025-07-04 15:30:29 +05:30
Mihir Kandoi
9312a5b762 Merge pull request #48396 from frappe/mergify/bp/version-15-hotfix/pr-48372
fix: rate not being fetched for product bundles in material request (backport #48372)
2025-07-04 15:30:09 +05:30
Mihir Kandoi
edef03ac22 chore: resolve conflict 2025-07-04 12:49:45 +05:30
Mihir Kandoi
74948aabda fix: LCV from PR order mismatch 2025-07-04 12:48:15 +05:30
Mihir Kandoi
cfedaf5dc1 fix: rate not being fetched for product bundles in material request
(cherry picked from commit 45c7bac2d0)

# Conflicts:
#	erpnext/stock/doctype/packed_item/packed_item.py
2025-07-04 07:16:39 +00:00
mergify[bot]
f25097da1d fix: pos recent order display customer code and name (backport #48379) (#48388)
fix: pos recent order display customer code and name (#48379)

(cherry picked from commit 5f721f01d3)

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-07-04 12:40:19 +05:30
mergify[bot]
3952f5d913 chore: fix flaky test in Tax Withholding Details (backport #48375) (#48394)
* chore: fix flaky test in Tax Withholding Details

(cherry picked from commit 14a2f98521)

* fix: sort tax withhodling details report by section code and transaction date

(cherry picked from commit 7ee2418f60)

* fix(test): flaky budget test case

(cherry picked from commit 704223e5d0)

* fix(test): import get_accumulated_monthly_budget

---------

Co-authored-by: ljain112 <ljain112@gmail.com>
Co-authored-by: ruthra kumar <ruthra@erpnext.com>
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2025-07-04 12:05:55 +05:30
Mihir Kandoi
9d23199c25 Merge pull request #48377 from mihir-kandoi/fix-job-card-buttons-d
fix: job card material request/transfer buttons UI overlap
2025-07-04 11:22:46 +05:30
Mihir Kandoi
b1abcd5577 fix: unnecessary primary button 2025-07-04 08:29:29 +05:30
Mihir Kandoi
09f866022b fix: job card material request/transfer buttons UI overlap 2025-07-03 14:05:07 +05:30
Frappe PR Bot
f087da927a chore(release): Bumped to Version 15.67.0
# [15.67.0](https://github.com/frappe/erpnext/compare/v15.66.1...v15.67.0) (2025-07-01)

### Bug Fixes

* accounting entries for standalone credit notes ([cfc8c61](cfc8c610fa))
* better integration of Pick List with Delivery Note (backport [#47831](https://github.com/frappe/erpnext/issues/47831)) ([#48158](https://github.com/frappe/erpnext/issues/48158)) ([8f47505](8f47505604))
* customer section on pos item cart (backport [#48284](https://github.com/frappe/erpnext/issues/48284)) ([#48285](https://github.com/frappe/erpnext/issues/48285)) ([b6e0953](b6e09531d7))
* customer_group import from lead to customer ([#48266](https://github.com/frappe/erpnext/issues/48266)) ([5463a8b](5463a8b6cf))
* default UOMs by new stock Entry created by Stock Level section button ([f1062c6](f1062c61f6))
* disassemble qty calculation & max calculation to be allowed to create it ([bf78f61](bf78f6173c))
* failing test case ([bde63ed](bde63ed0e5))
* func parameters ([c69bb74](c69bb746ce))
* not able to save material request ([0e2bca4](0e2bca4b34))
* option to pick serial / batch for asset repair ([7de15b7](7de15b74d4))
* **pos invoice:** search using customer name (backport [#48279](https://github.com/frappe/erpnext/issues/48279)) ([#48323](https://github.com/frappe/erpnext/issues/48323)) ([ab20b96](ab20b965ca))
* saperated validations for each purpose of validation ([0c07dfa](0c07dfadfe))
* update salvage value after value adjustment (backport [#48228](https://github.com/frappe/erpnext/issues/48228)) ([#48248](https://github.com/frappe/erpnext/issues/48248)) ([ef202d7](ef202d7cd0))
* use company default currency in amount_eligible_for_commission ([9b8fffd](9b8fffd1d4))
* use gain_loss_posting_date instead of today ([ff36284](ff362843cb))
* use label "State/Province" for translatability (backport [#48273](https://github.com/frappe/erpnext/issues/48273)) ([#48286](https://github.com/frappe/erpnext/issues/48286)) ([af55ce0](af55ce0f6c))
* validate asset before repair ([a1eab1d](a1eab1db74))

### Features

* added Transfer and Issue option in purpose ([1f7eccd](1f7eccdac5))

### Performance Improvements

* use set_value for updating bank clearance_date ([a0db227](a0db227a7a))
2025-07-01 12:02:06 +00:00
ruthra kumar
171d86a3ab Merge pull request #48338 from frappe/version-15-hotfix
chore: release v15
2025-07-01 17:30:39 +05:30
ruthra kumar
58c6031c1f Merge pull request #48339 from frappe/mergify/bp/version-15-hotfix/pr-48244
fix: use gain_loss_posting_date instead of today (backport #48244)
2025-07-01 16:20:01 +05:30
ravibharathi656
ff362843cb fix: use gain_loss_posting_date instead of today
(cherry picked from commit 0585bc5aef)
2025-07-01 10:12:35 +00:00
rohitwaghchaure
86b14dd4be Merge pull request #48319 from frappe/mergify/bp/version-15-hotfix/pr-48298
fix: default UOMs by new Stock Entry created by Stock Level section button (when Item is batch or serial) (backport #48298)
2025-07-01 13:24:29 +05:30
ruthra kumar
037ef10fcb Merge pull request #48292 from ljain112/fix-bank-cleareance-v15
perf: use set_value for updating bank clearance_date
2025-07-01 11:58:13 +05:30
mergify[bot]
ab20b965ca fix(pos invoice): search using customer name (backport #48279) (#48323)
* fix(pos invoice): search using customer name

(cherry picked from commit 20fd071c4e)

# Conflicts:
#	erpnext/selling/page/point_of_sale/point_of_sale.py

* refactor: use or_filters for customer and customer_name

(cherry picked from commit 6a401bcfbb)

# Conflicts:
#	erpnext/selling/page/point_of_sale/point_of_sale.py

* refactor: refactored for version 15

---------

Co-authored-by: ravibharathi656 <ravibharathi656@gmail.com>
Co-authored-by: diptanilsaha <diptanil@frappe.io>
2025-06-30 14:51:47 +05:30
rohitwaghchaure
e1a726bdd4 Merge pull request #48294 from frappe/mergify/bp/version-15-hotfix/pr-48293
fix: option to pick serial / batch for asset repair (backport #48293)
2025-06-30 13:27:29 +05:30
rohitwaghchaure
221e5a2190 Merge pull request #48320 from frappe/mergify/bp/version-15-hotfix/pr-48184
fix: Disassembly order items calculation in stock entry & track it in work order (backport #48184)
2025-06-30 13:16:30 +05:30
rohitwaghchaure
f0724f2f04 Merge pull request #48317 from frappe/mergify/bp/version-15-hotfix/pr-48240
refactor: bom stock report (backport #48240)
2025-06-30 13:15:37 +05:30
rohitwaghchaure
c404faaa6d chore: fix issue 2025-06-30 12:46:52 +05:30
rohitwaghchaure
abfe3c8365 chore: fix conflicts 2025-06-30 12:45:04 +05:30
iamkhanraheel
61f4547860 test: added test case for disassembly order
(cherry picked from commit aee26c3550)
2025-06-30 06:57:52 +00:00
iamkhanraheel
c69bb746ce fix: func parameters
(cherry picked from commit ce6ace4b8a)
2025-06-30 06:57:51 +00:00
iamkhanraheel
bf78f6173c fix: disassemble qty calculation & max calculation to be allowed to create it
(cherry picked from commit 3e4d160626)

# Conflicts:
#	erpnext/manufacturing/doctype/work_order/work_order.json
#	erpnext/stock/doctype/stock_entry/stock_entry.py
2025-06-30 06:57:51 +00:00
ruthra kumar
e628a37b99 Merge pull request #48318 from frappe/mergify/bp/version-15-hotfix/pr-48304
chore: fix translation message (backport #48304)
2025-06-30 12:22:58 +05:30
Florian HENRY
f1062c61f6 fix: default UOMs by new stock Entry created by Stock Level section button
(cherry picked from commit e7da4992f3)
2025-06-30 05:40:15 +00:00
Abdallah A. Zaqout
f479675ce6 chore: fix translation message
(cherry picked from commit bc002937ad)
2025-06-30 05:34:05 +00:00
Mihir Kandoi
ce7dbf3090 refactor: bom stock report
(cherry picked from commit ee4e0c646d)
2025-06-30 05:32:04 +00:00
rohitwaghchaure
ac587b9c94 chore: fix conflicts 2025-06-30 10:55:56 +05:30
ruthra kumar
1ea3daeb17 Merge pull request #48267 from frappe/mergify/bp/version-15-hotfix/pr-48266
fix: customer_group import from lead to customer (backport #48266)
2025-06-30 10:48:20 +05:30
ruthra kumar
b2f6fed42c Merge pull request #48315 from frappe/mergify/bp/version-15-hotfix/pr-48271
fix: use company default currency in amount_eligible_for_commission (backport #48271)
2025-06-30 10:34:49 +05:30
rohitwaghchaure
352642096e chore: fix conflicts 2025-06-30 10:12:54 +05:30
rohitwaghchaure
ab61b46a01 chore: fix conflicts 2025-06-30 10:09:59 +05:30
ravibharathi656
9b8fffd1d4 fix: use company default currency in amount_eligible_for_commission
(cherry picked from commit 7c7b392789)
2025-06-30 04:39:08 +00:00
rohitwaghchaure
46243fdb5f Merge pull request #48314 from frappe/mergify/bp/version-15-hotfix/pr-48310
fix: accounting entries for standalone credit notes (backport #48310)
2025-06-30 10:07:36 +05:30
Rohit Waghchaure
cfc8c610fa fix: accounting entries for standalone credit notes
(cherry picked from commit 52177cffcd)
2025-06-30 04:09:56 +00:00
Khushi Rawat
d73ff810d9 Merge pull request #48312 from frappe/mergify/bp/version-15-hotfix/pr-48311
fix: validate asset status for repair (backport #48311)
2025-06-30 01:31:45 +05:30
Khushi Rawat
2b37287b19 chore: resolved conflicts 2025-06-30 01:04:54 +05:30
Khushi Rawat
f31b008502 chore: resolved conflicts 2025-06-30 01:03:49 +05:30
khushi8112
6f24c02121 test: asset status validation
(cherry picked from commit cfe04a2aaf)

# Conflicts:
#	erpnext/assets/doctype/asset_repair/test_asset_repair.py
2025-06-29 19:26:03 +00:00
khushi8112
a1eab1db74 fix: validate asset before repair
(cherry picked from commit c6baa34812)

# Conflicts:
#	erpnext/assets/doctype/asset_repair/asset_repair.json
2025-06-29 19:26:03 +00:00
Frappe PR Bot
fcf9f82092 chore(release): Bumped to Version 15.66.1
## [15.66.1](https://github.com/frappe/erpnext/compare/v15.66.0...v15.66.1) (2025-06-27)

### Bug Fixes

* not able to save material request ([a49026e](a49026e9d2))
2025-06-27 12:28:50 +00:00
rohitwaghchaure
e817561dce Merge pull request #48300 from frappe/mergify/bp/version-15/pr-48297
fix: not able to save material request (backport #48296) (backport #48297)
2025-06-27 17:57:24 +05:30
Rohit Waghchaure
a49026e9d2 fix: not able to save material request
(cherry picked from commit c5e36eb323)
(cherry picked from commit 0e2bca4b34)
2025-06-27 11:18:10 +00:00
rohitwaghchaure
7574069af2 Merge pull request #48297 from frappe/mergify/bp/version-15-hotfix/pr-48296
fix: not able to save material request (backport #48296)
2025-06-27 16:47:17 +05:30
Rohit Waghchaure
0e2bca4b34 fix: not able to save material request
(cherry picked from commit c5e36eb323)
2025-06-27 10:55:48 +00:00
Rohit Waghchaure
7de15b74d4 fix: option to pick serial / batch for asset repair
(cherry picked from commit ae77c609ff)

# Conflicts:
#	erpnext/assets/doctype/asset_repair/asset_repair.py
#	erpnext/assets/doctype/asset_repair_consumed_item/asset_repair_consumed_item.json
2025-06-27 09:59:22 +00:00
ljain112
a0db227a7a perf: use set_value for updating bank clearance_date 2025-06-27 14:12:14 +05:30
mergify[bot]
af55ce0f6c fix: use label "State/Province" for translatability (backport #48273) (#48286)
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
fix: use label "State/Province" for translatability (#48273)
2025-06-26 20:49:06 +02:00
0xD0M1M0
894bb703f6 chore: improve some german translations (#48283) 2025-06-26 20:23:35 +02:00
mergify[bot]
b6e09531d7 fix: customer section on pos item cart (backport #48284) (#48285)
* fix: customer section on pos item cart (#48284)

* fix: customer recent transactions

* fix: pos customer section display customer_name instead of customer name

(cherry picked from commit e1d9f863c6)

# Conflicts:
#	erpnext/public/scss/point-of-sale.scss
#	erpnext/selling/page/point_of_sale/point_of_sale.py
#	erpnext/selling/page/point_of_sale/pos_item_cart.js

* chore: resolve conflict

* chore: resolve conflict

* chore: resolve conflict

---------

Co-authored-by: Diptanil Saha <diptanil@frappe.io>
2025-06-26 23:20:38 +05:30
mergify[bot]
8f47505604 fix: better integration of Pick List with Delivery Note (backport #47831) (#48158)
* fix: better integration of Pick List with Delivery Note (#47831)

Co-authored-by: priyanshshah2442 <priyanshshah2442@gmail.com>
(cherry picked from commit 527cfe9c7d)

# Conflicts:
#	erpnext/patches.txt
#	erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
#	erpnext/stock/doctype/pick_list/pick_list.py
#	erpnext/stock/doctype/pick_list_item/pick_list_item.json

* chore: resolve conflicts

* fix: setting status correctly as per v15 utility

* fix: get items from Pick List to DN even if not linked to Sales Order

---------

Co-authored-by: Smit Vora <smitvora203@gmail.com>
Co-authored-by: Priyansh Shah <108476017+priyanshshah2442@users.noreply.github.com>
2025-06-26 18:25:53 +05:30
0xD0M1M0
5463a8b6cf fix: customer_group import from lead to customer (#48266)
In case customization happens and the lead has the field "customer_group", the get_mapped_doc function would fail and be overwritten by the default value.

(cherry picked from commit 1b18105bce)
2025-06-25 21:26:40 +00:00
Khushi Rawat
b8a773e3e1 Merge pull request #48253 from frappe/mergify/bp/version-15-hotfix/pr-47901
feat: Asset Transfer and Issue in single asset movement record (backport #47901)
2025-06-25 15:23:54 +05:30
khushi8112
dc642fbc41 chore: linters check 2025-06-25 15:04:17 +05:30
Khushi Rawat
ddbdcbb026 chore: resolved conflicts 2025-06-25 14:47:47 +05:30
Khushi Rawat
df938f24d4 chore: resolved conflicts 2025-06-25 14:47:00 +05:30
Khushi Rawat
bde63ed0e5 fix: failing test case
(cherry picked from commit 7d3bec8ef8)
2025-06-25 09:08:24 +00:00
Khushi Rawat
1c3ac9c1fd refactor: split set_latest_location_and_custodian_in_asset into smaller functions
(cherry picked from commit 7e52cb2856)
2025-06-25 09:08:24 +00:00
Khushi Rawat
0c07dfadfe fix: saperated validations for each purpose of validation
(cherry picked from commit 07d1a0ed9c)

# Conflicts:
#	erpnext/assets/doctype/asset_movement/asset_movement.py
2025-06-25 09:08:24 +00:00
Khushi Rawat
1f7eccdac5 feat: added Transfer and Issue option in purpose
(cherry picked from commit f5e5146021)

# Conflicts:
#	erpnext/assets/doctype/asset_movement/asset_movement.json
2025-06-25 09:08:23 +00:00
Khushi Rawat
ef202d7cd0 fix: update salvage value after value adjustment (backport #48228) (#48248)
fix: update salvage value after value adjustment
2025-06-25 11:43:18 +05:30
120 changed files with 1904 additions and 868 deletions

View File

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

View File

@@ -147,8 +147,8 @@ class AccountsSettings(Document):
if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
frappe.throw(
_("You cannot enable both the settings '{0}' and '{1}'.").format(
frappe.bold(self.meta.get_label("add_taxes_from_item_tax_template")),
frappe.bold(self.meta.get_label("add_taxes_from_taxes_and_charges_template")),
frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))),
frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))),
),
title=_("Auto Tax Settings Error"),
)

View File

@@ -118,8 +118,9 @@ class BankClearance(Document):
else:
# using db_set to trigger notification
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
payment_entry.db_set("clearance_date", d.clearance_date)
frappe.db.set_value(
d.payment_document, d.payment_entry, "clearance_date", d.clearance_date
)
clearance_date_updated = True

View File

@@ -6,7 +6,11 @@ import unittest
import frappe
from frappe.utils import now_datetime, nowdate
from erpnext.accounts.doctype.budget.budget import BudgetError, get_actual_expense
from erpnext.accounts.doctype.budget.budget import (
BudgetError,
get_accumulated_monthly_budget,
get_actual_expense,
)
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -96,6 +100,10 @@ class TestBudget(unittest.TestCase):
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
accumulated_limit = get_accumulated_monthly_budget(
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
)
mr = frappe.get_doc(
{
"doctype": "Material Request",
@@ -109,7 +117,7 @@ class TestBudget(unittest.TestCase):
"uom": "_Test UOM",
"warehouse": "_Test Warehouse - _TC",
"schedule_date": nowdate(),
"rate": 100000,
"rate": accumulated_limit + 1,
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
}

View File

@@ -24,6 +24,7 @@ from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
get_advance_payment_doctypes,
get_balance_on,
get_stock_accounts,
get_stock_and_account_balance,
@@ -146,8 +147,8 @@ class JournalEntry(AccountsController):
if self.docstatus == 0:
self.apply_tax_withholding()
self.title = self.get_title()
if self.is_new() or not self.title:
self.title = self.get_title()
def validate_advance_accounts(self):
journal_accounts = set([x.account for x in self.accounts])
@@ -238,9 +239,10 @@ class JournalEntry(AccountsController):
def update_advance_paid(self):
advance_paid = frappe._dict()
advance_payment_doctypes = get_advance_payment_doctypes()
for d in self.get("accounts"):
if d.is_advance:
if d.reference_type in frappe.get_hooks("advance_payment_doctypes"):
if d.reference_type in advance_payment_doctypes:
advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
for voucher_type, order_list in advance_paid.items():
@@ -1042,7 +1044,9 @@ class JournalEntry(AccountsController):
def set_print_format_fields(self):
bank_amount = party_amount = total_amount = 0.0
currency = bank_account_currency = party_account_currency = pay_to_recd_from = None
currency = (
bank_account_currency
) = party_account_currency = pay_to_recd_from = self.pay_to_recd_from = None
party_type = None
for d in self.get("accounts"):
if d.party_type in ["Customer", "Supplier"] and d.party:

View File

@@ -46,8 +46,10 @@ from erpnext.accounts.party import (
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_account_currency,
get_advance_payment_doctypes,
get_balance_on,
get_outstanding_invoices,
get_reconciliation_effect_date,
)
from erpnext.controllers.accounts_controller import (
AccountsController,
@@ -568,7 +570,7 @@ class PaymentEntry(AccountsController):
def validate_mandatory(self):
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
if not self.get(field):
frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field)))
frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label(field))))
def validate_reference_documents(self):
valid_reference_doctypes = self.get_valid_reference_doctypes()
@@ -1028,7 +1030,7 @@ class PaymentEntry(AccountsController):
def calculate_base_allocated_amount_for_reference(self, d) -> float:
base_allocated_amount = 0
if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
if d.reference_doctype in get_advance_payment_doctypes():
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
# This is so there are no Exchange Gain/Loss generated for such doctypes
@@ -1308,8 +1310,7 @@ class PaymentEntry(AccountsController):
if not self.party_account:
return
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
advance_payment_doctypes = get_advance_payment_doctypes()
if self.payment_type == "Receive":
against_account = self.paid_to
else:
@@ -1492,23 +1493,7 @@ class PaymentEntry(AccountsController):
else:
# For backwards compatibility
# Supporting reposting on payment entries reconciled before select field introduction
reconciliation_takes_effect_on = frappe.get_cached_value(
"Company", self.company, "reconciliation_takes_effect_on"
)
if reconciliation_takes_effect_on == "Advance Payment Date":
posting_date = self.posting_date
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
posting_date = frappe.db.get_value(
invoice.reference_doctype, invoice.reference_name, date_field
)
if getdate(posting_date) < getdate(self.posting_date):
posting_date = self.posting_date
elif reconciliation_takes_effect_on == "Reconciliation Date":
posting_date = nowdate()
posting_date = get_reconciliation_effect_date(invoice, self.company, self.posting_date)
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
@@ -1699,12 +1684,15 @@ class PaymentEntry(AccountsController):
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
def update_advance_paid(self):
if self.payment_type in ("Receive", "Pay") and self.party:
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
frappe.get_doc(
d.reference_doctype, d.reference_name, for_update=True
).set_total_advance_paid()
if self.payment_type not in ("Receive", "Pay") or not self.party:
return
advance_payment_doctypes = get_advance_payment_doctypes()
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
frappe.get_doc(
d.reference_doctype, d.reference_name, for_update=True
).set_total_advance_paid()
def on_recurring(self, reference_doc, auto_repeat_doc):
self.reference_no = reference_doc.name

View File

@@ -589,7 +589,7 @@ class PaymentReconciliation(Document):
def check_mandatory_to_fetch(self):
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
if not self.get(fieldname):
frappe.throw(_("Please select {0} first").format(self.meta.get_label(fieldname)))
frappe.throw(_("Please select {0} first").format(_(self.meta.get_label(fieldname))))
def validate_entries(self):
if not self.get("invoices"):
@@ -826,7 +826,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
create_gain_loss_journal(
company,
today(),
inv.difference_posting_date,
inv.party_type,
inv.party,
inv.account,

View File

@@ -829,8 +829,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
if not references:
return
precision = references[0].precision("allocated_amount")
precision = frappe.get_precision("Payment Entry Reference", "allocated_amount")
referenced_payment_requests = frappe.get_all(
"Payment Request",
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},

View File

@@ -630,29 +630,58 @@ class TestPaymentRequest(FrappeTestCase):
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
self.assertEqual(pr.grand_total, si.outstanding_amount)
def test_partial_paid_invoice_with_submitted_payment_entry(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
pi.save()
pi.submit()
def test_partial_paid_invoice_with_submitted_payment_entry(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
pi.save()
pi.submit()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "PURINV0001"
pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500
pe.save()
pe.submit()
pe.cancel()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "PURINV0001"
pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500
pe.save()
pe.submit()
pe.cancel()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "PURINV0002"
pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500
pe.save()
pe.submit()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "PURINV0002"
pe.reference_date = frappe.utils.nowdate()
pe.paid_amount = 2500
pe.references[0].allocated_amount = 2500
pe.save()
pe.submit()
pi.load_from_db()
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
self.assertEqual(pr.grand_total, pi.outstanding_amount)
pi.load_from_db()
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
self.assertEqual(pr.grand_total, pi.outstanding_amount)
def test_payment_request_on_unreconcile(self):
pi = make_purchase_invoice(currency="INR", qty=1, rate=500)
pi.submit()
pr = make_payment_request(
dt=pi.doctype,
dn=pi.name,
mute_email=1,
submit_doc=True,
return_doc=True,
)
self.assertEqual(pr.grand_total, pi.outstanding_amount)
pe = pr.create_payment_entry()
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payment",
"company": pe.company,
"voucher_type": pe.doctype,
"voucher_no": pe.name,
}
)
unreconcile.add_references()
unreconcile.submit()
pi.load_from_db()
pr.load_from_db()
self.assertEqual(pr.grand_total, pi.outstanding_amount)

View File

@@ -5,6 +5,7 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"posting_date",
"posting_time",
"merge_invoices_based_on",
@@ -113,12 +114,22 @@
"label": "Posting Time",
"no_copy": 1,
"reqd": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"print_hide": 1,
"remember_last_selected_value": 1,
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-08-01 11:36:42.456429",
"modified": "2025-07-02 17:08:04.747202",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Merge Log",
@@ -179,8 +190,9 @@
"write": 1
}
],
"sort_field": "modified",
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -28,11 +28,10 @@ class POSInvoiceMergeLog(Document):
if TYPE_CHECKING:
from frappe.types import DF
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import (
POSInvoiceReference,
)
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import POSInvoiceReference
amended_from: DF.Link | None
company: DF.Link
consolidated_credit_note: DF.Link | None
consolidated_invoice: DF.Link | None
customer: DF.Link
@@ -584,6 +583,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
merge_log.posting_time = (
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
)
merge_log.company = closing_entry.get("company") if closing_entry else None
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
merge_log.set("pos_invoices", _invoices)

View File

@@ -169,7 +169,7 @@ class PricingRule(Document):
tocheck = frappe.scrub(self.get("applicable_for", ""))
if tocheck and not self.get(tocheck):
throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError)
throw(_("{0} is required").format(_(self.meta.get_label(tocheck))), frappe.MandatoryError)
if self.apply_rule_on_other:
o_field = "other_" + frappe.scrub(self.apply_rule_on_other)

View File

@@ -205,6 +205,56 @@ class TestPricingRule(FrappeTestCase):
details = get_item_details(args)
self.assertEqual(details.get("discount_percentage"), 10)
def test_unset_group_condition(self):
"""
If args are not set for group condition, then pricing rule should not be applied.
"""
from erpnext.stock.get_item_details import get_item_details
test_record = {
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule",
"apply_on": "Item Code",
"items": [{"item_code": "_Test Item"}],
"currency": "USD",
"selling": 1,
"rate_or_discount": "Discount Percentage",
"rate": 0,
"discount_percentage": 10,
"applicable_for": "Territory",
"territory": "All Territories",
"company": "_Test Company",
}
frappe.get_doc(test_record.copy()).insert()
args = frappe._dict(
{
"item_code": "_Test Item",
"company": "_Test Company",
"price_list": "_Test Price List",
"currency": "_Test Currency",
"doctype": "Sales Order",
"conversion_rate": 1,
"price_list_currency": "_Test Currency",
"plc_conversion_rate": 1,
"order_type": "Sales",
"customer": "_Test Customer",
"name": None,
}
)
# without territory in customer
customer = frappe.get_doc("Customer", "_Test Customer")
territory = customer.territory
customer.territory = None
customer.save()
details = get_item_details(args)
self.assertEqual(details.get("discount_percentage"), 0)
customer.territory = territory
customer.save()
def test_pricing_rule_for_variants(self):
from erpnext.stock.get_item_details import get_item_details

View File

@@ -223,6 +223,10 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
)
frappe.flags.tree_conditions[key] = condition
elif allow_blank:
condition = f"ifnull({table}.{field}, '') = ''"
return condition

View File

@@ -40,6 +40,13 @@ class TestProcessDeferredAccounting(unittest.TestCase):
si.save()
si.submit()
original_gle = [
["Debtors - _TC", 3000.0, 0, "2023-07-01"],
[deferred_account, 0.0, 3000, "2023-07-01"],
]
check_gl_entries(self, si.name, original_gle, "2023-07-01")
process_deferred_accounting = frappe.get_doc(
dict(
doctype="Process Deferred Accounting",
@@ -63,6 +70,12 @@ class TestProcessDeferredAccounting(unittest.TestCase):
]
check_gl_entries(self, si.name, expected_gle, "2023-07-01")
# cancel the process deferred accounting document
process_deferred_accounting.cancel()
# check if gl entries are cancelled
check_gl_entries(self, si.name, original_gle, "2023-07-01")
change_acc_settings()
def test_pda_submission_and_cancellation(self):

View File

@@ -54,6 +54,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
};
});
frm.set_query("account", function () {
if (!frm.doc.company) {
frappe.throw(__("Please set Company"));
}
return {
filters: {
company: frm.doc.company,
@@ -61,6 +64,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
};
});
frm.set_query("cost_center", function () {
if (!frm.doc.company) {
frappe.throw(__("Please set Company"));
}
return {
filters: {
company: frm.doc.company,
@@ -68,6 +74,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
};
});
frm.set_query("project", function () {
if (!frm.doc.company) {
frappe.throw(__("Please set Company"));
}
return {
filters: {
company: frm.doc.company,
@@ -79,6 +88,11 @@ frappe.ui.form.on("Process Statement Of Accounts", {
frm.set_value("to_date", frappe.datetime.get_today());
}
},
company: function (frm) {
frm.set_value("account", "");
frm.set_value("cost_center", "");
frm.set_value("project", "");
},
report: function (frm) {
let filters = {
company: frm.doc.company,

View File

@@ -376,7 +376,7 @@
"default": "0",
"fieldname": "ignore_exchange_rate_revaluation_journals",
"fieldtype": "Check",
"label": "Ignore Exchange Rate Revaluation Journals"
"label": "Ignore Exchange Rate Revaluation and Gain / Loss Journals"
},
{
"default": "0",
@@ -400,7 +400,7 @@
}
],
"links": [],
"modified": "2025-04-30 14:43:23.643006",
"modified": "2025-07-08 16:52:12.602384",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -82,6 +82,10 @@ class ProcessStatementOfAccounts(Document):
# end: auto-generated types
def validate(self):
self.validate_account()
self.validate_company_for_table("Cost Center")
self.validate_company_for_table("Project")
if not self.subject:
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
if not self.body:
@@ -104,6 +108,43 @@ class ProcessStatementOfAccounts(Document):
self.to_date = self.start_date
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
def validate_account(self):
if not self.account:
return
if self.company != frappe.get_cached_value("Account", self.account, "company"):
frappe.throw(
_("Account {0} doesn't belong to Company {1}").format(
frappe.bold(self.account),
frappe.bold(self.company),
)
)
def validate_company_for_table(self, doctype):
field = frappe.scrub(doctype)
if not self.get(field):
return
fieldname = field + "_name"
values = set(d.get(fieldname) for d in self.get(field))
invalid_values = frappe.db.get_all(
doctype, filters={"name": ["in", values], "company": ["!=", self.company]}, pluck="name"
)
if invalid_values:
msg = _("<p>Following {0}s doesn't belong to Company {1} :</p>").format(
doctype, frappe.bold(self.company)
)
msg += (
"<ul>"
+ "".join(_("<li>{}</li>").format(frappe.bold(row)) for row in invalid_values)
+ "</ul>"
)
frappe.throw(_(msg))
def get_report_pdf(doc, consolidated=True):
statement_dict = get_statement_dict(doc)

View File

@@ -1336,17 +1336,12 @@ class PurchaseInvoice(BuyingController):
warehouse_debit_amount = stock_amount
elif self.is_return and self.update_stock and self.is_internal_supplier and warehouse_debit_amount:
elif self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against):
net_rate = item.base_net_amount
if item.sales_incoming_rate: # for internal transfer
net_rate = item.qty * item.sales_incoming_rate
stock_amount = (
net_rate
+ item.item_tax_amount
+ flt(item.landed_cost_voucher_amount)
+ flt(item.get("amount_difference_with_purchase_invoice"))
)
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
cost_of_goods_sold_account = self.get_company_default("default_expense_account")

View File

@@ -2024,6 +2024,7 @@
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"options": "Company:company:default_currency",
"read_only": 1
},
{
@@ -2188,7 +2189,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2025-03-17 19:32:31.809658",
"modified": "2025-06-26 14:06:56.773552",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -2497,6 +2497,10 @@ class TestSalesInvoice(FrappeTestCase):
for gle in gl_entries:
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
@change_settings(
"Accounts Settings",
{"book_deferred_entries_based_on": "Days", "book_deferred_entries_via_journal_entry": 0},
)
def test_deferred_revenue(self):
deferred_account = create_account(
account_name="Deferred Revenue",
@@ -2551,6 +2555,10 @@ class TestSalesInvoice(FrappeTestCase):
self.assertRaises(frappe.ValidationError, si.save)
@change_settings(
"Accounts Settings",
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
)
def test_fixed_deferred_revenue(self):
deferred_account = create_account(
account_name="Deferred Revenue",
@@ -2558,10 +2566,6 @@ class TestSalesInvoice(FrappeTestCase):
company="_Test Company",
)
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.book_deferred_entries_based_on = "Months"
acc_settings.save()
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_account
@@ -2601,10 +2605,6 @@ class TestSalesInvoice(FrappeTestCase):
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
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(
@@ -2853,7 +2853,9 @@ class TestSalesInvoice(FrappeTestCase):
self.assertEqual(si.items[0].rate, rate)
self.assertEqual(target_doc.items[0].rate, rate)
check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
check_gl_entries(
self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1), voucher_type="Purchase Invoice"
)
def test_internal_transfer_gl_precision_issues(self):
# Make a stock queue of an item with two valuations
@@ -4587,6 +4589,8 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type="
)
gl_entries = q.run(as_dict=True)
doc.assertGreater(len(gl_entries), 0)
for i, gle in enumerate(gl_entries):
doc.assertEqual(expected_gle[i][0], gle.account)
doc.assertEqual(expected_gle[i][1], gle.debit)

View File

@@ -12,6 +12,7 @@ from frappe.utils.data import comma_and
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
get_advance_payment_doctypes,
unlink_ref_doc_from_payment_entries,
update_voucher_outstanding,
)
@@ -84,7 +85,7 @@ class UnreconcilePayment(Document):
update_voucher_outstanding(
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
)
if doc.doctype in frappe.get_hooks("advance_payment_doctypes"):
if doc.doctype in get_advance_payment_doctypes():
doc.set_total_advance_paid()
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)

View File

@@ -692,7 +692,18 @@ def make_reverse_gl_entries(
query.run()
else:
if not immutable_ledger_enabled:
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
gle_names = [x.get("name") for x in gl_entries]
# if names are available, cancel only that set of entries
if not all(gle_names):
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
else:
frappe.db.sql(
"""UPDATE `tabGL Entry` SET is_cancelled = 1,
modified=%s, modified_by=%s
where name in %s and is_cancelled = 0""",
(now(), frappe.session.user, tuple(gle_names)),
)
for entry in gl_entries:
new_gle = copy.deepcopy(entry)

View File

@@ -15,7 +15,11 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_dimension_with_children,
)
from erpnext.accounts.utils import get_currency_precision, get_party_types_from_account_type
from erpnext.accounts.utils import (
get_advance_payment_doctypes,
get_currency_precision,
get_party_types_from_account_type,
)
# This report gives a summary of all Outstanding Invoices considering the following
@@ -62,6 +66,7 @@ class ReceivablePayableReport:
frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
or "Buffered Cursor"
) # Fail Safe
self.advance_payment_doctypes = get_advance_payment_doctypes()
def run(self, args):
self.filters.update(args)
@@ -85,6 +90,7 @@ class ReceivablePayableReport:
self.party_details = {}
self.invoices = set()
self.skip_total_row = 0
self.advance_payment_doctypes = get_advance_payment_doctypes()
if self.filters.get("group_by_party"):
self.previous_party = ""
@@ -181,7 +187,10 @@ class ReceivablePayableReport:
if key not in self.voucher_balance:
self.voucher_balance[key] = self.build_voucher_dict(ple)
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
if (ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no) or (
ple.voucher_type in ("Payment Entry", "Journal Entry")
and ple.against_voucher_type in self.advance_payment_doctypes
):
self.voucher_balance[key].cost_center = ple.cost_center
self.get_invoices(ple)

View File

@@ -79,6 +79,14 @@ class Deferred_Item:
return - estimated amount to post for given period
Calculated based on already booked amount and item service period
"""
if self.filters.book_deferred_entries_based_on == "Months":
# if the deferred entries are based on service period, use service start and end date
return self.calculate_monthly_amount(start_date, end_date)
else:
return self.calculate_days_amount(start_date, end_date)
def calculate_monthly_amount(self, start_date, end_date):
total_months = (
(self.service_end_date.year - self.service_start_date.year) * 12
+ (self.service_end_date.month - self.service_start_date.month)
@@ -105,6 +113,19 @@ class Deferred_Item:
return base_amount
def calculate_days_amount(self, start_date, end_date):
base_amount = 0
total_days = date_diff(self.service_end_date, self.service_start_date) + 1
total_booking_days = date_diff(end_date, start_date) + 1
already_booked_amount = self.get_item_total()
base_amount = flt(self.base_net_amount * total_booking_days / flt(total_days))
if base_amount + already_booked_amount > self.base_net_amount:
base_amount = self.base_net_amount - already_booked_amount
return base_amount
def make_dummy_gle(self, name, date, amount):
"""
return - frappe._dict() of a dummy gle entry
@@ -245,6 +266,10 @@ class Deferred_Revenue_and_Expense_Report:
else:
self.filters = frappe._dict(filters)
self.filters.book_deferred_entries_based_on = frappe.db.get_singles_value(
"Accounts Settings", "book_deferred_entries_based_on"
)
self.period_list = None
self.deferred_invoices = []
# holds period wise total for report
@@ -289,7 +314,11 @@ class Deferred_Revenue_and_Expense_Report:
.join(inv)
.on(inv.name == inv_item.parent)
.left_join(gle)
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
.on(
(inv_item.name == gle.voucher_detail_no)
& (deferred_account_field == gle.account)
& (gle.is_cancelled == 0)
)
.select(
inv.name.as_("doc"),
inv.posting_date,

View File

@@ -9,7 +9,7 @@ import re
import frappe
from frappe import _
from frappe.query_builder.functions import Sum
from frappe.query_builder.functions import Max, Min, Sum
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
from pypika.terms import ExistsCriterion
@@ -109,14 +109,19 @@ def get_period_list(
def get_fiscal_year_data(from_fiscal_year, to_fiscal_year):
fiscal_year = frappe.db.sql(
"""select min(year_start_date) as year_start_date,
max(year_end_date) as year_end_date from `tabFiscal Year` where
name between %(from_fiscal_year)s and %(to_fiscal_year)s""",
{"from_fiscal_year": from_fiscal_year, "to_fiscal_year": to_fiscal_year},
as_dict=1,
from_year_start_date = frappe.get_cached_value("Fiscal Year", from_fiscal_year, "year_start_date")
to_year_end_date = frappe.get_cached_value("Fiscal Year", to_fiscal_year, "year_end_date")
fy = frappe.qb.DocType("Fiscal Year")
query = (
frappe.qb.from_(fy)
.select(Min(fy.year_start_date).as_("year_start_date"), Max(fy.year_end_date).as_("year_end_date"))
.where(fy.year_start_date >= from_year_start_date)
.where(fy.year_end_date <= to_year_end_date)
)
fiscal_year = query.run(as_dict=True)
return fiscal_year[0] if fiscal_year else {}

View File

@@ -209,7 +209,7 @@ frappe.query_reports["General Ledger"] = {
},
{
fieldname: "ignore_err",
label: __("Ignore Exchange Rate Revaluation Journals"),
label: __("Ignore Exchange Rate Revaluation and Gain / Loss Journals"),
fieldtype: "Check",
},
{

View File

@@ -6,12 +6,9 @@ from frappe import qb
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):
@@ -171,90 +168,6 @@ 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

@@ -5,13 +5,14 @@
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
from frappe.query_builder import functions as fn
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
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
from erpnext.accounts.report.utils import get_values_for_columns
from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import (
get_customer_details,
)
@@ -433,7 +434,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
si.is_internal_customer,
si.customer,
si.remarks,
si.territory,
fn.IfNull(si.territory, "Not Specified").as_("territory"),
si.company,
si.base_net_total,
sii.project,
@@ -456,7 +457,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
sii.base_net_rate,
sii.base_net_amount,
si.customer_name,
si.customer_group,
fn.IfNull(si.customer_group, "Not Specified").as_("customer_group"),
sii.so_detail,
si.update_stock,
sii.uom,

View File

@@ -121,7 +121,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
)
out.append(row)
out.sort(key=lambda x: x["section_code"])
out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
return out

View File

@@ -67,11 +67,12 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
mid_year = add_to_date(fiscal_year[1], months=6)
tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3")
tds_doc.rates[0].to_date = mid_year
from_date = add_to_date(mid_year, days=1)
tds_doc.append(
"rates",
{
"tax_withholding_rate": 20,
"from_date": add_to_date(mid_year, days=1),
"from_date": from_date,
"to_date": fiscal_year[2],
"single_threshold": 1,
"cumulative_threshold": 1,
@@ -80,18 +81,19 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
tds_doc.save()
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
inv_1 = make_purchase_invoice(
rate=1000, posting_date=add_to_date(fiscal_year[1], days=1), do_not_save=True, do_not_submit=True
)
inv_1.set_posting_time = 1
inv_1.apply_tds = 1
inv_1.tax_withholding_category = "TDS - 3"
inv_1.tax_withholding_category = tds_doc.name
inv_1.save()
inv_1.submit()
inv_2 = make_purchase_invoice(
rate=1000, do_not_submit=True, posting_date=add_to_date(mid_year, days=1), do_not_save=True
)
inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True)
inv_2.set_posting_time = 1
inv_1.apply_tds = 1
inv_2.tax_withholding_category = "TDS - 3"
inv_2.apply_tds = 1
inv_2.tax_withholding_category = tds_doc.name
inv_2.save()
inv_2.submit()

View File

@@ -107,11 +107,7 @@ def convert_to_presentation_currency(gl_entries, currency_info):
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
and (debit_in_account_currency or credit_in_account_currency)
):
if len(account_currencies) == 1 and account_currency == presentation_currency:
entry["debit"] = debit_in_account_currency
entry["credit"] = credit_in_account_currency
else:

View File

@@ -169,7 +169,7 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
if doc:
doc.fiscal_year = years[0]
else:
throw(_("{0} '{1}' not in Fiscal Year {2}").format(label, formatdate(date), fiscal_year))
throw(_("{0} '{1}' not in Fiscal Year {2}").format(_(label), formatdate(date), fiscal_year))
@frappe.whitelist()
@@ -629,6 +629,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
# Update Advance Paid in SO/PO since they might be getting unlinked
update_advance_paid = []
if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]:
update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name))
@@ -713,23 +714,8 @@ def update_reference_in_payment_entry(
update_advance_paid = []
# Update Reconciliation effect date in reference
reconciliation_takes_effect_on = frappe.get_cached_value(
"Company", payment_entry.company, "reconciliation_takes_effect_on"
)
if payment_entry.book_advance_payments_in_separate_party_account:
if reconciliation_takes_effect_on == "Advance Payment Date":
reconcile_on = payment_entry.posting_date
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if d.against_voucher_type in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
reconcile_on = frappe.db.get_value(d.against_voucher_type, d.against_voucher, date_field)
if getdate(reconcile_on) < getdate(payment_entry.posting_date):
reconcile_on = payment_entry.posting_date
elif reconciliation_takes_effect_on == "Reconciliation Date":
reconcile_on = nowdate()
reconcile_on = get_reconciliation_effect_date(d, payment_entry.company, payment_entry.posting_date)
reference_details.update({"reconcile_effect_on": reconcile_on})
if d.voucher_detail_no:
@@ -783,6 +769,28 @@ def update_reference_in_payment_entry(
return row, update_advance_paid
def get_reconciliation_effect_date(reference, company, posting_date):
reconciliation_takes_effect_on = frappe.get_cached_value(
"Company", company, "reconciliation_takes_effect_on"
)
if reconciliation_takes_effect_on == "Advance Payment Date":
reconcile_on = posting_date
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
date_field = "posting_date"
if reference.against_voucher_type in ["Sales Order", "Purchase Order"]:
date_field = "transaction_date"
reconcile_on = frappe.db.get_value(
reference.against_voucher_type, reference.against_voucher, date_field
)
if getdate(reconcile_on) < getdate(posting_date):
reconcile_on = posting_date
elif reconciliation_takes_effect_on == "Reconciliation Date":
reconcile_on = nowdate()
return reconcile_on
def cancel_exchange_gain_loss_journal(
parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None
) -> None:
@@ -997,58 +1005,79 @@ def remove_ref_doc_link_from_pe(
per = qb.DocType("Payment Entry Reference")
pay = qb.DocType("Payment Entry")
linked_pe = (
query = (
qb.from_(per)
.select(per.parent)
.where((per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2)))
.run(as_list=1)
)
linked_pe = convert_to_list(linked_pe)
# remove reference only from specified payment
linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe
if linked_pe:
update_query = (
qb.update(per)
.set(per.allocated_amount, 0)
.set(per.modified, now())
.set(per.modified_by, frappe.session.user)
.where(per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no))
.select("*")
.where(
(per.reference_doctype == ref_type)
& (per.reference_name == ref_no)
& (per.docstatus.lt(2))
& (per.parenttype == "Payment Entry")
)
)
if payment_name:
update_query = update_query.where(per.parent == payment_name)
# update reference only from specified payment
if payment_name:
query = query.where(per.parent == payment_name)
update_query.run()
reference_rows = query.run(as_dict=True)
for pe in linked_pe:
try:
pe_doc = frappe.get_doc("Payment Entry", pe)
pe_doc.set_amounts()
if not reference_rows:
return
# Call cancel on only removed reference
references = [
x
for x in pe_doc.references
if x.reference_doctype == ref_type and x.reference_name == ref_no
]
[pe_doc.make_advance_gl_entries(x, cancel=1) for x in references]
linked_pe = set()
row_names = set()
pe_doc.clear_unallocated_reference_document_rows()
pe_doc.validate_payment_type_with_outstanding()
except Exception:
msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
msg += "<br>"
msg += _("Please cancel payment entry manually first")
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
for row in reference_rows:
linked_pe.add(row.parent)
row_names.add(row.name)
qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set(
pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount
).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set(
pay.modified_by, frappe.session.user
).where(pay.name == pe).run()
from erpnext.accounts.doctype.payment_request.payment_request import (
update_payment_requests_as_per_pe_references,
)
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
# Update payment request amount
update_payment_requests_as_per_pe_references(reference_rows, cancel=True)
# Update allocated amounts and modified fields in one go
(
qb.update(per)
.set(per.allocated_amount, 0)
.set(per.modified, now())
.set(per.modified_by, frappe.session.user)
.where(per.name.isin(row_names))
.where(per.parenttype == "Payment Entry")
.run()
)
for pe in linked_pe:
try:
pe_doc = frappe.get_doc("Payment Entry", pe)
pe_doc.set_amounts()
# Call cancel on only removed reference
references = [x for x in pe_doc.references if x.name in row_names]
[pe_doc.make_advance_gl_entries(x, cancel=1) for x in references]
pe_doc.clear_unallocated_reference_document_rows()
pe_doc.validate_payment_type_with_outstanding()
except Exception:
msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
msg += "<br>"
msg += _("Please cancel payment entry manually first")
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
(
qb.update(pay)
.set(pay.total_allocated_amount, pe_doc.total_allocated_amount)
.set(pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount)
.set(pay.unallocated_amount, pe_doc.unallocated_amount)
.set(pay.modified, now())
.set(pay.modified_by, frappe.session.user)
.where(pay.name == pe)
.run()
)
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
@frappe.whitelist()
@@ -2235,6 +2264,15 @@ def get_party_types_from_account_type(account_type):
return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name")
def get_advance_payment_doctypes():
"""
Get list of advance payment doctypes based on type.
:param type: Optional, can be "receivable" or "payable". If not provided, returns both.
"""
return frappe.get_hooks("advance_payment_doctypes")
def run_ledger_health_checks():
health_monitor_settings = frappe.get_doc("Ledger Health Monitor")
if health_monitor_settings.enable_health_monitor:

View File

@@ -62,8 +62,8 @@ frappe.ui.form.on("Asset Movement", {
fieldnames_to_be_altered = {
target_location: { read_only: 0, reqd: 1 },
source_location: { read_only: 1, reqd: 0 },
from_employee: { read_only: 0, reqd: 0 },
to_employee: { read_only: 1, reqd: 0 },
from_employee: { read_only: 1, reqd: 0 },
to_employee: { read_only: 0, reqd: 0 },
};
} else if (frm.doc.purpose === "Issue") {
fieldnames_to_be_altered = {
@@ -72,6 +72,13 @@ frappe.ui.form.on("Asset Movement", {
from_employee: { read_only: 1, reqd: 0 },
to_employee: { read_only: 0, reqd: 1 },
};
} else if (frm.doc.purpose === "Transfer and Issue") {
fieldnames_to_be_altered = {
target_location: { read_only: 0, reqd: 1 },
source_location: { read_only: 0, reqd: 1 },
from_employee: { read_only: 0, reqd: 1 },
to_employee: { read_only: 0, reqd: 1 },
};
}
if (fieldnames_to_be_altered) {
Object.keys(fieldnames_to_be_altered).forEach((fieldname) => {

View File

@@ -33,7 +33,7 @@
"fieldname": "purpose",
"fieldtype": "Select",
"label": "Purpose",
"options": "\nIssue\nReceipt\nTransfer",
"options": "\nIssue\nReceipt\nTransfer\nTransfer and Issue",
"reqd": 1
},
{
@@ -93,10 +93,11 @@
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-06-28 16:54:26.571083",
"modified": "2025-05-30 17:01:55.864353",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Movement",
@@ -149,7 +150,8 @@
"write": 1
}
],
"sort_field": "modified",
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -24,105 +24,81 @@ class AssetMovement(Document):
amended_from: DF.Link | None
assets: DF.Table[AssetMovementItem]
company: DF.Link
purpose: DF.Literal["", "Issue", "Receipt", "Transfer"]
purpose: DF.Literal["", "Issue", "Receipt", "Transfer", "Transfer and Issue"]
reference_doctype: DF.Link | None
reference_name: DF.DynamicLink | None
transaction_date: DF.Datetime
# end: auto-generated types
def validate(self):
self.validate_asset()
self.validate_location()
self.validate_employee()
def validate_asset(self):
for d in self.assets:
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
if self.purpose == "Transfer" and status in ("Draft", "Scrapped", "Sold"):
frappe.throw(_("{0} asset cannot be transferred").format(status))
self.validate_asset(d)
self.validate_movement(d)
if company != self.company:
frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company))
def validate_asset(self, d):
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
if self.purpose == "Transfer" and status in ("Draft", "Scrapped", "Sold"):
frappe.throw(_("{0} asset cannot be transferred").format(status))
if not (d.source_location or d.target_location or d.from_employee or d.to_employee):
frappe.throw(_("Either location or employee must be required"))
if company != self.company:
frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company))
def validate_location(self):
for d in self.assets:
if self.purpose in ["Transfer", "Issue"]:
current_location = frappe.db.get_value("Asset", d.asset, "location")
if d.source_location:
if current_location != d.source_location:
frappe.throw(
_("Asset {0} does not belongs to the location {1}").format(
d.asset, d.source_location
)
)
else:
d.source_location = current_location
def validate_movement(self, d):
if self.purpose == "Transfer and Issue":
self.validate_location_and_employee(d)
elif self.purpose in ["Receipt", "Transfer"]:
self.validate_location(d)
else:
self.validate_employee(d)
if self.purpose == "Issue":
if d.target_location:
def validate_location_and_employee(self, d):
self.validate_location(d)
self.validate_employee(d)
def validate_location(self, d):
if self.purpose in ["Transfer", "Transfer and Issue"]:
current_location = frappe.db.get_value("Asset", d.asset, "location")
if d.source_location:
if current_location != d.source_location:
frappe.throw(
_(
"Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to"
).format(d.asset),
title=_("Incorrect Movement Purpose"),
)
if not d.to_employee:
frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset))
if self.purpose == "Transfer":
if d.to_employee:
frappe.throw(
_(
"Transferring cannot be done to an Employee. Please enter location where Asset {0} has to be transferred"
).format(d.asset),
title=_("Incorrect Movement Purpose"),
)
if not d.target_location:
frappe.throw(
_("Target Location is required while transferring Asset {0}").format(d.asset)
)
if d.source_location == d.target_location:
frappe.throw(_("Source and Target Location cannot be same"))
if self.purpose == "Receipt":
if not (d.source_location) and not (d.target_location or d.to_employee):
frappe.throw(
_("Target Location or To Employee is required while receiving Asset {0}").format(
d.asset
)
)
elif d.source_location:
if d.from_employee and not d.target_location:
frappe.throw(
_(
"Target Location is required while receiving Asset {0} from an employee"
).format(d.asset)
)
elif d.to_employee and d.target_location:
frappe.throw(
_(
"Asset {0} cannot be received at a location and given to an employee in a single movement"
).format(d.asset)
)
def validate_employee(self):
for d in self.assets:
if d.from_employee:
current_custodian = frappe.db.get_value("Asset", d.asset, "custodian")
if current_custodian != d.from_employee:
frappe.throw(
_("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee)
_("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location)
)
else:
d.source_location = current_location
if not d.target_location:
frappe.throw(_("Target Location is required for transferring Asset {0}").format(d.asset))
if d.source_location == d.target_location:
frappe.throw(_("Source and Target Location cannot be same"))
if self.purpose == "Receipt":
if not d.target_location:
frappe.throw(_("Target Location is required while receiving Asset {0}").format(d.asset))
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
frappe.throw(
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
)
def validate_employee(self, d):
if self.purpose == "Tranfer and Issue":
if not d.from_employee:
frappe.throw(_("From Employee is required while issuing Asset {0}").format(d.asset))
if d.from_employee:
current_custodian = frappe.db.get_value("Asset", d.asset, "custodian")
if current_custodian != d.from_employee:
frappe.throw(
_("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee)
)
if not d.to_employee:
frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset))
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
frappe.throw(
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
)
def on_submit(self):
self.set_latest_location_and_custodian_in_asset()
@@ -130,53 +106,63 @@ class AssetMovement(Document):
self.set_latest_location_and_custodian_in_asset()
def set_latest_location_and_custodian_in_asset(self):
for d in self.assets:
current_location, current_employee = self.get_latest_location_and_custodian(d.asset)
self.update_asset_location_and_custodian(d.asset, current_location, current_employee)
self.log_asset_activity(d.asset, current_location, current_employee)
def get_latest_location_and_custodian(self, asset):
current_location, current_employee = "", ""
cond = "1=1"
for d in self.assets:
args = {"asset": d.asset, "company": self.company}
# latest entry corresponds to current document's location, employee when transaction date > previous dates
# In case of cancellation it corresponds to previous latest document's location, employee
args = {"asset": asset, "company": self.company}
latest_movement_entry = frappe.db.sql(
f"""
SELECT asm_item.target_location, asm_item.to_employee
FROM `tabAsset Movement Item` asm_item
JOIN `tabAsset Movement` asm ON asm_item.parent = asm.name
WHERE
asm_item.asset = %(asset)s AND
asm.company = %(company)s AND
asm.docstatus = 1 AND {cond}
ORDER BY asm.transaction_date DESC
LIMIT 1
""",
args,
)
# latest entry corresponds to current document's location, employee when transaction date > previous dates
# In case of cancellation it corresponds to previous latest document's location, employee
latest_movement_entry = frappe.db.sql(
f"""
SELECT asm_item.target_location, asm_item.to_employee
FROM `tabAsset Movement Item` asm_item, `tabAsset Movement` asm
WHERE
asm_item.parent=asm.name and
asm_item.asset=%(asset)s and
asm.company=%(company)s and
asm.docstatus=1 and {cond}
ORDER BY
asm.transaction_date desc limit 1
""",
args,
if latest_movement_entry:
current_location = latest_movement_entry[0][0]
current_employee = latest_movement_entry[0][1]
return current_location, current_employee
def update_asset_location_and_custodian(self, asset_id, location, employee):
asset = frappe.get_doc("Asset", asset_id)
if employee and employee != asset.custodian:
frappe.db.set_value("Asset", asset_id, "custodian", employee)
if location and location != asset.location:
frappe.db.set_value("Asset", asset_id, "location", location)
def log_asset_activity(self, asset_id, location, employee):
if location and employee:
add_asset_activity(
asset_id,
_("Asset received at Location {0} and issued to Employee {1}").format(
get_link_to_form("Location", location),
get_link_to_form("Employee", employee),
),
)
elif location:
add_asset_activity(
asset_id,
_("Asset transferred to Location {0}").format(get_link_to_form("Location", location)),
)
elif employee:
add_asset_activity(
asset_id,
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", employee)),
)
if latest_movement_entry:
current_location = latest_movement_entry[0][0]
current_employee = latest_movement_entry[0][1]
frappe.db.set_value("Asset", d.asset, "location", current_location, update_modified=False)
frappe.db.set_value("Asset", d.asset, "custodian", current_employee, update_modified=False)
if current_location and current_employee:
add_asset_activity(
d.asset,
_("Asset received at Location {0} and issued to Employee {1}").format(
get_link_to_form("Location", current_location),
get_link_to_form("Employee", current_employee),
),
)
elif current_location:
add_asset_activity(
d.asset,
_("Asset transferred to Location {0}").format(
get_link_to_form("Location", current_location)
),
)
elif current_employee:
add_asset_activity(
d.asset,
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)),
)

View File

@@ -88,7 +88,7 @@ class TestAssetMovement(unittest.TestCase):
)
# after issuing, asset should belong to an employee not at a location
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None)
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee)
create_asset_movement(

View File

@@ -3,6 +3,8 @@
frappe.ui.form.on("Asset Repair", {
setup: function (frm) {
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
frm.fields_dict.cost_center.get_query = function (doc) {
return {
filters: {
@@ -167,4 +169,37 @@ frappe.ui.form.on("Asset Repair Consumed Item", {
var row = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, "total_value", row.consumed_quantity * row.valuation_rate);
},
pick_serial_and_batch(frm, cdt, cdn) {
let item = locals[cdt][cdn];
let doc = frm.doc;
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).then((r) => {
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
item.has_serial_no = r.message.has_serial_no;
item.has_batch_no = r.message.has_batch_no;
item.qty = item.consumed_quantity;
item.type_of_transaction = item.consumed_quantity > 0 ? "Outward" : "Inward";
item.title = item.has_serial_no ? __("Select Serial No") : __("Select Batch No");
if (item.has_serial_no && item.has_batch_no) {
item.title = __("Select Serial and Batch");
}
frm.doc.posting_date = frappe.datetime.get_today();
frm.doc.posting_time = frappe.datetime.now_time();
new erpnext.SerialBatchPackageSelector(frm, item, (r) => {
if (r) {
frappe.model.set_value(item.doctype, item.name, {
serial_and_batch_bundle: r.name,
use_serial_batch_fields: 0,
valuation_rate: r.avg_rate,
consumed_quantity: Math.abs(r.total_qty),
});
}
});
}
});
},
});

View File

@@ -138,6 +138,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Asset",
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",null]]]",
"options": "Asset",
"reqd": 1
},
@@ -248,7 +249,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-06-13 16:14:14.398356",
"modified": "2025-06-29 22:30:00.589597",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",
@@ -292,4 +293,4 @@
"title_field": "asset_name",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -53,6 +53,7 @@ class AssetRepair(AccountsController):
def validate(self):
self.asset_doc = frappe.get_doc("Asset", self.asset)
self.validate_asset()
self.validate_dates()
self.update_status()
@@ -60,6 +61,14 @@ class AssetRepair(AccountsController):
self.set_stock_items_cost()
self.calculate_total_repair_cost()
def validate_asset(self):
if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"):
frappe.throw(
_("Asset {0} is in {1} status and cannot be repaired.").format(
get_link_to_form("Asset", self.asset), self.asset_doc.status
)
)
def validate_dates(self):
if self.completion_date and (self.failure_date > self.completion_date):
frappe.throw(
@@ -131,6 +140,13 @@ class AssetRepair(AccountsController):
),
)
def cancel_sabb(self):
for row in self.stock_items:
if sabb := row.serial_and_batch_bundle:
row.db_set("serial_and_batch_bundle", None)
doc = frappe.get_doc("Serial and Batch Bundle", sabb)
doc.cancel()
def before_cancel(self):
self.asset_doc = frappe.get_doc("Asset", self.asset)
@@ -172,6 +188,8 @@ class AssetRepair(AccountsController):
),
)
self.cancel_sabb()
def after_delete(self):
frappe.get_doc("Asset", self.asset).set_status()

View File

@@ -4,11 +4,12 @@
import unittest
import frappe
from frappe.utils import flt, nowdate, nowtime, today
from frappe.utils import add_months, flt, get_first_day, nowdate, nowtime, today
from erpnext.assets.doctype.asset.asset import (
get_asset_account,
get_asset_value_after_depreciation,
make_sales_invoice,
)
from erpnext.assets.doctype.asset.test_asset import (
create_asset,
@@ -33,6 +34,33 @@ class TestAssetRepair(unittest.TestCase):
create_item("_Test Stock Item")
frappe.db.sql("delete from `tabTax Rule`")
def test_asset_status(self):
date = nowdate()
purchase_date = add_months(get_first_day(date), -2)
asset = create_asset(
calculate_depreciation=1,
available_for_use_date=purchase_date,
purchase_date=purchase_date,
expected_value_after_useful_life=10000,
total_number_of_depreciations=10,
frequency_of_depreciation=1,
submit=1,
)
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
si.customer = "_Test Customer"
si.due_date = date
si.get("items")[0].rate = 25000
si.insert()
si.submit()
asset.reload()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
asset_repair = frappe.new_doc("Asset Repair")
asset_repair.update({"company": "_Test Company", "asset": asset.name, "asset_name": asset.asset_name})
self.assertRaises(frappe.ValidationError, asset_repair.save)
def test_update_status(self):
asset = create_asset(submit=1)
initial_status = asset.status

View File

@@ -11,6 +11,8 @@
"consumed_quantity",
"total_value",
"serial_no",
"column_break_xzfr",
"pick_serial_and_batch",
"serial_and_batch_bundle"
],
"fields": [
@@ -61,12 +63,21 @@
"label": "Warehouse",
"options": "Warehouse",
"reqd": 1
},
{
"fieldname": "pick_serial_and_batch",
"fieldtype": "Button",
"label": "Pick Serial / Batch"
},
{
"fieldname": "column_break_xzfr",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-06-13 12:01:47.147333",
"modified": "2025-06-27 14:52:56.311166",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair Consumed Item",
@@ -76,4 +87,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -192,6 +192,10 @@ class AssetValueAdjustment(Document):
if asset.calculate_depreciation:
for row in asset.finance_books:
if cstr(row.finance_book) == cstr(self.finance_book):
salvage_value_adjustment = (
self.get_adjusted_salvage_value_amount(row, difference_amount) or 0
)
row.expected_value_after_useful_life += salvage_value_adjustment
row.value_after_depreciation += flt(difference_amount)
row.db_update()
@@ -208,6 +212,11 @@ class AssetValueAdjustment(Document):
asset.save()
asset.set_status()
def get_adjusted_salvage_value_amount(self, row, difference_amount):
if row.expected_value_after_useful_life:
salvage_value_adjustment = (difference_amount * row.salvage_value_percentage) / 100
return flt(salvage_value_adjustment if self.docstatus == 1 else -1 * salvage_value_adjustment)
@frappe.whitelist()
def get_value_of_accounting_dimensions(asset_name):

View File

@@ -292,6 +292,43 @@ class TestAssetValueAdjustment(unittest.TestCase):
asset_doc.load_from_db()
self.assertEqual(asset_doc.value_after_depreciation, 50000.0)
def test_expected_value_after_useful_life(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_doc = frappe.get_doc("Asset", asset_name)
asset_doc.calculate_depreciation = 1
asset_doc.available_for_use_date = "2023-01-15"
asset_doc.purchase_date = "2023-01-15"
asset_doc.append(
"finance_books",
{
"expected_value_after_useful_life": 5000,
"salvage_value_percentage": 5,
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 12,
"frequency_of_depreciation": 1,
"depreciation_start_date": "2023-01-31",
},
)
asset_doc.submit()
self.assertEqual(asset_doc.finance_books[0].expected_value_after_useful_life, 5000.0)
current_asset_value = get_asset_value_after_depreciation(asset_doc.name)
adj_doc = make_asset_value_adjustment(
asset=asset_doc.name,
current_asset_value=current_asset_value,
new_asset_value=40000,
date="2023-08-21",
)
adj_doc.submit()
difference_amount = adj_doc.new_asset_value - adj_doc.current_asset_value
self.assertEqual(difference_amount, -60000)
asset_doc.load_from_db()
self.assertEqual(asset_doc.finance_books[0].value_after_depreciation, 40000.0)
self.assertEqual(asset_doc.finance_books[0].expected_value_after_useful_life, 2000.0)
def make_asset_value_adjustment(**args):
args = frappe._dict(args)

View File

@@ -807,11 +807,19 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
def update_item(obj, target, source_parent):
target.amount = flt(obj.amount) - flt(obj.billed_amt)
target.base_amount = target.amount * flt(source_parent.conversion_rate)
target.qty = (
target.amount / flt(obj.rate) if (flt(obj.rate) and flt(obj.billed_amt)) else flt(obj.qty)
)
def get_billed_qty(po_item_name):
from frappe.query_builder.functions import Sum
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(table)
.select(Sum(table.qty).as_("qty"))
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
)
return query.run(pluck="qty")[0] or 0
billed_qty = flt(get_billed_qty(obj.name))
target.qty = flt(obj.qty) - billed_qty
item = get_item_defaults(target.item_code, source_parent.company)
item_group = get_item_group_defaults(target.item_code, source_parent.company)

View File

@@ -1286,6 +1286,25 @@ class TestPurchaseOrder(FrappeTestCase):
self.assertFalse(po.per_billed)
self.assertEqual(po.status, "To Receive and Bill")
@change_settings("Buying Settings", {"maintain_same_rate": 0})
def test_purchase_invoice_creation_with_partial_qty(self):
po = create_purchase_order(qty=100, rate=10)
pi = make_pi_from_po(po.name)
pi.items[0].qty = 42
pi.items[0].rate = 7.5
pi.submit()
pi = make_pi_from_po(po.name)
self.assertEqual(pi.items[0].qty, 58)
self.assertEqual(pi.items[0].rate, 10)
pi.items[0].qty = 8
pi.items[0].rate = 5
pi.submit()
pi = make_pi_from_po(po.name)
self.assertEqual(pi.items[0].qty, 50)
def create_po_for_sc_testing():
from erpnext.controllers.tests.test_subcontracting_controller import (

View File

@@ -51,6 +51,9 @@ from erpnext.accounts.utils import (
get_fiscal_years,
validate_fiscal_year,
)
from erpnext.accounts.utils import (
get_advance_payment_doctypes as _get_advance_payment_doctypes,
)
from erpnext.buying.utils import update_last_purchase_rate
from erpnext.controllers.print_settings import (
set_print_templates_for_item_table,
@@ -386,9 +389,7 @@ class AccountsController(TransactionBase):
adv = qb.DocType("Advance Payment Ledger Entry")
qb.from_(adv).delete().where(adv.voucher_type.eq(self.doctype) & adv.voucher_no.eq(self.name)).run()
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
if self.doctype in advance_payment_doctypes:
if self.doctype in self.get_advance_payment_doctypes():
qb.from_(adv).delete().where(
adv.against_voucher_type.eq(self.doctype) & adv.against_voucher_no.eq(self.name)
).run()
@@ -1134,6 +1135,10 @@ class AccountsController(TransactionBase):
return True
def set_taxes_and_charges(self):
if self.doctype == "Material Request":
# Material Request does not have taxes
return
if self.get("taxes") or self.get("is_pos"):
return
@@ -2908,7 +2913,7 @@ class AccountsController(TransactionBase):
repost_ledger.submit()
def get_advance_payment_doctypes(self) -> list:
return frappe.get_hooks("advance_payment_doctypes")
return _get_advance_payment_doctypes()
def make_advance_payment_ledger_for_journal(self):
advance_payment_doctypes = self.get_advance_payment_doctypes()
@@ -3961,6 +3966,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
).format(frappe.bold(parent.name))
)
else: # Sales Order
parent.validate_selling_price()
parent.validate_for_duplicate_items()
parent.validate_warehouse()
parent.update_reserved_qty()

View File

@@ -156,6 +156,17 @@ status_map = {
["Draft", None],
["Completed", "eval:self.docstatus == 1"],
],
"Pick List": [
["Draft", None],
["Open", "eval:self.docstatus == 1"],
["Completed", "stock_entry_exists"],
[
"Partly Delivered",
"eval:self.purpose == 'Delivery' and self.delivery_status == 'Partly Delivered'",
],
["Completed", "eval:self.purpose == 'Delivery' and self.delivery_status == 'Fully Delivered'"],
["Cancelled", "eval:self.docstatus == 2"],
],
}

View File

@@ -1648,8 +1648,9 @@ def make_quality_inspections(doctype, docname, items):
"sample_size": flt(item.get("sample_size")),
"item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None,
"batch_no": item.get("batch_no"),
"child_row_reference": item.get("child_row_reference"),
}
).insert()
)
quality_inspection.save()
inspections.append(quality_inspection.name)
@@ -1662,14 +1663,9 @@ def is_reposting_pending():
)
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
def future_sle_exists(args, sl_entries=None):
from erpnext.stock.utils import get_combine_datetime
if allow_force_reposting and frappe.db.get_single_value(
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
):
return True
key = (args.voucher_type, args.voucher_no)
if not hasattr(frappe.local, "future_sle"):
frappe.local.future_sle = {}

View File

@@ -601,12 +601,15 @@ class SubcontractingController(StockController):
rm_obj.use_serial_batch_fields = 1
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields:
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
item_row, rm_obj, rm_obj.consumed_qty
)
if self.doctype == "Subcontracting Receipt":
if not use_serial_batch_fields:
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
item_row, rm_obj, rm_obj.consumed_qty
)
self.set_rate_for_supplied_items(rm_obj, item_row)
self.set_rate_for_supplied_items(rm_obj, item_row)
elif self.backflush_based_on == "BOM":
self.update_rate_for_supplied_items()
def update_rate_for_supplied_items(self):
if self.doctype != "Subcontracting Receipt":

View File

@@ -47,7 +47,7 @@ def get_columns(filters, trans):
def validate_filters(filters):
for f in ["Fiscal Year", "Based On", "Period", "Company"]:
if not filters.get(f.lower().replace(" ", "_")):
frappe.throw(_("{0} is mandatory").format(f))
frappe.throw(_("{0} is mandatory").format(_(f)))
if not frappe.db.exists("Fiscal Year", filters.get("fiscal_year")):
frappe.throw(_("Fiscal Year {0} Does Not Exist").format(filters.get("fiscal_year")))

View File

@@ -497,7 +497,7 @@
{
"fieldname": "state",
"fieldtype": "Data",
"label": "State"
"label": "State/Province"
},
{
"fieldname": "country",
@@ -512,11 +512,12 @@
"show_dashboard": 1
}
],
"grid_page_length": 50,
"icon": "fa fa-user",
"idx": 5,
"image_field": "image",
"links": [],
"modified": "2025-01-31 13:40:08.094759",
"modified": "2025-06-26 11:02:01.158901",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
@@ -575,6 +576,7 @@
"role": "Sales User"
}
],
"row_format": "Dynamic",
"search_fields": "lead_name,lead_owner,status",
"sender_field": "email_id",
"sender_name_field": "lead_name",

View File

@@ -323,7 +323,8 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False):
target.customer_type = "Individual"
target.customer_name = source.lead_name
target.customer_group = frappe.db.get_default("Customer Group")
if not target.customer_group:
target.customer_group = frappe.db.get_default("Customer Group")
doclist = get_mapped_doc(
"Lead",

View File

@@ -613,7 +613,7 @@
{
"fieldname": "state",
"fieldtype": "Data",
"label": "State"
"label": "State/Province"
},
{
"fieldname": "country",
@@ -622,10 +622,11 @@
"options": "Country"
}
],
"grid_page_length": 50,
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
"modified": "2024-08-20 04:12:29.095761",
"modified": "2025-06-26 11:16:13.665866",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",
@@ -657,6 +658,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "status,transaction_date,party_name,opportunity_type,territory,company",
"sender_field": "contact_email",
"show_name_in_global_search": 1,

View File

@@ -58,7 +58,7 @@ def get_columns():
{"label": _("Address"), "fieldname": "address", "fieldtype": "Data", "width": 130},
{"label": _("Postal Code"), "fieldname": "pincode", "fieldtype": "Data", "width": 90},
{"label": _("City"), "fieldname": "city", "fieldtype": "Data", "width": 100},
{"label": _("State"), "fieldname": "state", "fieldtype": "Data", "width": 100},
{"label": _("State/Province"), "fieldname": "state", "fieldtype": "Data", "width": 100},
{
"label": _("Country"),
"fieldname": "country",

View File

@@ -987,7 +987,7 @@ class BOM(WebsiteGenerator):
self.transfer_material_against = "Work Order"
if not self.transfer_material_against and not self.is_new():
frappe.throw(
_("Setting {} is required").format(self.meta.get_label("transfer_material_against")),
_("Setting {0} is required").format(_(self.meta.get_label("transfer_material_against"))),
title=_("Missing value"),
)

View File

@@ -51,9 +51,13 @@ frappe.ui.form.on("Job Card", {
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
if (to_request || excess_transfer_allowed) {
frm.add_custom_button(__("Material Request"), () => {
frm.trigger("make_material_request");
});
frm.add_custom_button(
__("Material Request"),
() => {
frm.trigger("make_material_request");
},
__("Create")
);
}
// check if any row has untransferred materials
@@ -61,9 +65,13 @@ frappe.ui.form.on("Job Card", {
let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty);
if (to_transfer || excess_transfer_allowed) {
frm.add_custom_button(__("Material Transfer"), () => {
frm.trigger("make_stock_entry");
}).addClass("btn-primary");
frm.add_custom_button(
__("Material Transfer"),
() => {
frm.trigger("make_stock_entry");
},
__("Create")
);
}
}

View File

@@ -2368,6 +2368,105 @@ class TestWorkOrder(FrappeTestCase):
stock_entry.submit()
def test_disassembly_order_with_qty_behavior(self):
# Create raw material and FG item
raw_item = make_item("Test Raw for Disassembly", {"is_stock_item": 1}).name
fg_item = make_item("Test FG for Disassembly", {"is_stock_item": 1}).name
bom = make_bom(item=fg_item, quantity=10, raw_materials=[raw_item], rm_qty=5)
# Create and submit a Work Order for 10 qty
wo = make_wo_order_test_record(production_item=fg_item, qty=10, bom_no=bom.name, status="Not Started")
# create material receipt stock entry for raw material
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
make_stock_entry as make_stock_entry_test_record,
)
make_stock_entry_test_record(
item_code=raw_item,
purpose="Material Receipt",
target=wo.wip_warehouse,
qty=10,
basic_rate=100,
)
make_stock_entry_test_record(
item_code=raw_item,
purpose="Material Receipt",
target=wo.fg_warehouse,
qty=10,
basic_rate=100,
)
# create material transfer for manufacture stock entry
se_for_material_tranfer_mfr = frappe.get_doc(
make_stock_entry(wo.name, "Material Transfer for Manufacture", wo.qty)
)
se_for_material_tranfer_mfr.items[0].s_warehouse = wo.wip_warehouse
se_for_material_tranfer_mfr.save()
se_for_material_tranfer_mfr.submit()
se_for_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", wo.qty))
se_for_manufacture.submit()
# Simulate a disassembly stock entry
disassemble_qty = 4
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", disassemble_qty))
stock_entry.append(
"items",
{
"item_code": fg_item,
"qty": disassemble_qty,
"s_warehouse": wo.fg_warehouse,
},
)
for bom_item in bom.items:
stock_entry.append(
"items",
{
"item_code": bom_item.item_code,
"qty": (bom_item.qty / bom.quantity) * disassemble_qty,
"t_warehouse": wo.source_warehouse,
},
)
wo.reload()
stock_entry.save()
stock_entry.submit()
# Assert FG item is present with correct qty
finished_good_entry = next((item for item in stock_entry.items if item.item_code == fg_item), None)
self.assertIsNotNone(finished_good_entry, "Finished good item missing from stock entry")
self.assertEqual(
finished_good_entry.qty,
disassemble_qty,
f"Expected FG qty {disassemble_qty}, found {finished_good_entry.qty}",
)
# Assert raw materials
for item in stock_entry.items:
if item.item_code == fg_item:
continue
bom_item = next((i for i in bom.items if i.item_code == item.item_code), None)
if bom_item:
expected_qty = (bom_item.qty / bom.quantity) * disassemble_qty
self.assertAlmostEqual(
item.qty,
expected_qty,
places=3,
msg=f"Raw item {item.item_code} qty mismatch: expected {expected_qty}, got {item.qty}",
)
else:
self.fail(f"Unexpected item {item.item_code} found in stock entry")
wo.reload()
# Assert disassembled_qty field updated in Work Order
self.assertEqual(
wo.disassembled_qty,
disassemble_qty,
f"Work Order disassembled_qty mismatch: expected {disassemble_qty}, got {wo.disassembled_qty}",
)
def test_components_alternate_item_for_bom_based_manufacture_entry(self):
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)
@@ -3118,6 +3217,7 @@ def make_wo_order_test_record(**args):
wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
wo_order.from_wip_warehouse = args.from_wip_warehouse or 0
wo_order.batch_size = args.batch_size or 0
wo_order.status = args.status or "Draft"
if args.source_warehouse:
wo_order.source_warehouse = args.source_warehouse

View File

@@ -803,7 +803,7 @@ erpnext.work_order = {
get_max_transferable_qty: (frm, purpose) => {
let max = 0;
if (purpose === "Disassemble") {
return flt(frm.doc.produced_qty);
return flt(frm.doc.produced_qty - frm.doc.disassembled_qty);
}
if (frm.doc.skip_transfer) {

View File

@@ -20,6 +20,7 @@
"qty",
"material_transferred_for_manufacturing",
"produced_qty",
"disassembled_qty",
"process_loss_qty",
"project",
"section_break_ndpq",
@@ -586,6 +587,14 @@
{
"fieldname": "section_break_ndpq",
"fieldtype": "Section Break"
},
{
"depends_on": "eval:doc.docstatus==1",
"fieldname": "disassembled_qty",
"fieldtype": "Float",
"label": "Disassembled Qty",
"no_copy": 1,
"read_only": 1
}
],
"icon": "fa fa-cogs",
@@ -593,7 +602,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
"modified": "2024-02-11 15:47:13.454422",
"modified": "2025-06-21 00:55:45.916224",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
@@ -629,4 +638,4 @@
"title_field": "production_item",
"track_changes": 1,
"track_seen": 1
}
}

View File

@@ -88,6 +88,7 @@ class WorkOrder(Document):
company: DF.Link
corrective_operation_cost: DF.Currency
description: DF.SmallText | None
disassembled_qty: DF.Float
expected_delivery_date: DF.Date | None
fg_warehouse: DF.Link
from_wip_warehouse: DF.Check
@@ -389,7 +390,7 @@ class WorkOrder(Document):
if qty > completed_qty:
frappe.throw(
_("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format(
self.meta.get_label(fieldname), qty, completed_qty, self.name
_(self.meta.get_label(fieldname)), qty, completed_qty, self.name
),
StockOverProductionError,
)
@@ -406,6 +407,18 @@ class WorkOrder(Document):
self.set_produced_qty_for_sub_assembly_item()
self.update_production_plan_status()
def update_disassembled_qty(self, qty, is_cancel=False):
if is_cancel:
self.disassembled_qty = max(0, self.disassembled_qty - qty)
else:
if self.docstatus == 1:
self.disassembled_qty += qty
if not is_cancel and self.disassembled_qty > self.produced_qty:
frappe.throw(_("Cannot disassemble more than produced quantity."))
self.db_set("disassembled_qty", self.disassembled_qty)
def get_transferred_or_manufactured_qty(self, purpose):
table = frappe.qb.DocType("Stock Entry")
query = frappe.qb.from_(table).where(
@@ -1064,7 +1077,7 @@ class WorkOrder(Document):
self.transfer_material_against = "Work Order"
if not self.transfer_material_against:
frappe.throw(
_("Setting {} is required").format(self.meta.get_label("transfer_material_against")),
_("Setting {0} is required").format(_(self.meta.get_label("transfer_material_against"))),
title=_("Missing value"),
)
@@ -1475,7 +1488,7 @@ def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
stock_entry.set_stock_entry_type()
stock_entry.get_items()
stock_entry.get_items(qty, work_order.production_item)
if purpose != "Disassemble":
stock_entry.set_serial_no_batch_for_finished_good()

View File

@@ -1,12 +1,10 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.query_builder.functions import Floor, Sum
from frappe.utils import cint
from pypika.terms import ExistsCriterion
def execute(filters=None):
@@ -20,8 +18,7 @@ def execute(filters=None):
def get_columns():
"""return columns"""
columns = [
return [
_("Item") + ":Link/Item:150",
_("Item Name") + "::240",
_("Description") + "::300",
@@ -32,55 +29,54 @@ def get_columns():
_("Enough Parts to Build") + ":Float:200",
]
return columns
def get_bom_stock(filters):
qty_to_produce = filters.get("qty_to_produce")
if cint(qty_to_produce) <= 0:
frappe.throw(_("Quantity to Produce should be greater than zero."))
if filters.get("show_exploded_view"):
bom_item_table = "BOM Explosion Item"
else:
bom_item_table = "BOM Item"
bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item"
warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1)
warehouse = filters.get("warehouse")
warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1)
BOM = frappe.qb.DocType("BOM")
BOM_ITEM = frappe.qb.DocType(bom_item_table)
BIN = frappe.qb.DocType("Bin")
WH = frappe.qb.DocType("Warehouse")
CONDITIONS = ()
if warehouse_details:
CONDITIONS = ExistsCriterion(
frappe.qb.from_(WH)
.select(WH.name)
.where(
(WH.lft >= warehouse_details.lft)
& (WH.rgt <= warehouse_details.rgt)
& (BIN.warehouse == WH.name)
)
bin_subquery = (
frappe.qb.from_(BIN)
.join(WH)
.on(BIN.warehouse == WH.name)
.select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty"))
.where((WH.lft >= warehouse_details.lft) & (WH.rgt <= warehouse_details.rgt))
.groupby(BIN.item_code)
)
else:
CONDITIONS = BIN.warehouse == filters.get("warehouse")
bin_subquery = (
frappe.qb.from_(BIN)
.select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty"))
.where(BIN.warehouse == warehouse)
.groupby(BIN.item_code)
)
QUERY = (
frappe.qb.from_(BOM)
.inner_join(BOM_ITEM)
.join(BOM_ITEM)
.on(BOM.name == BOM_ITEM.parent)
.left_join(BIN)
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
.left_join(bin_subquery)
.on(BOM_ITEM.item_code == bin_subquery.item_code)
.select(
BOM_ITEM.item_code,
BOM_ITEM.item_name,
BOM_ITEM.description,
BOM_ITEM.stock_qty,
Sum(BOM_ITEM.stock_qty),
BOM_ITEM.stock_uom,
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
BIN.actual_qty.as_("actual_qty"),
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
(Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity,
bin_subquery.actual_qty,
Floor(bin_subquery.actual_qty / ((Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity)),
)
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
.groupby(BOM_ITEM.item_code)

View File

@@ -409,4 +409,6 @@ 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_pick_list_fields
erpnext.patches.v15_0.update_pegged_currencies
erpnext.patches.v15_0.set_company_on_pos_inv_merge_log

View File

@@ -0,0 +1,12 @@
import frappe
def execute():
pos_invoice_merge_logs = frappe.db.get_all(
"POS Invoice Merge Log", {"docstatus": 1}, ["name", "pos_closing_entry"]
)
for log in pos_invoice_merge_logs:
if log.pos_closing_entry and frappe.db.exists("POS Closing Entry", log.pos_closing_entry):
company = frappe.db.get_value("POS Closing Entry", log.pos_closing_entry, "company")
frappe.db.set_value("POS Invoice Merge Log", log.name, "company", company)

View File

@@ -0,0 +1,28 @@
import frappe
from frappe.query_builder.functions import IfNull
def execute():
update_delivery_note()
update_pick_list_items()
def update_delivery_note():
DN = frappe.qb.DocType("Delivery Note")
DNI = frappe.qb.DocType("Delivery Note Item")
frappe.qb.update(DNI).join(DN).on(DN.name == DNI.parent).set(DNI.against_pick_list, DN.pick_list).where(
IfNull(DN.pick_list, "") != ""
).run()
def update_pick_list_items():
PL = frappe.qb.DocType("Pick List")
PLI = frappe.qb.DocType("Pick List Item")
pick_lists = frappe.qb.from_(PL).select(PL.name).where(PL.status == "Completed").run(pluck="name")
if not pick_lists:
return
frappe.qb.update(PLI).set(PLI.delivered_qty, PLI.picked_qty).where(PLI.parent.isin(pick_lists)).run()

View File

@@ -202,6 +202,12 @@ frappe.ui.form.on("Project", {
});
});
},
collect_progress: function (frm) {
if (frm.doc.collect_progress && !frm.doc.subject) {
frm.set_value("subject", __("For project {0}, update your status", [frm.doc.name]));
}
},
});
function open_form(frm, doctype, child_doctype, parentfield) {

View File

@@ -62,6 +62,7 @@
"day_to_send",
"weekly_time_to_send",
"column_break_45",
"subject",
"message"
],
"fields": [
@@ -447,6 +448,13 @@
"print_hide": 1,
"reqd": 1,
"set_only_once": 1
},
{
"depends_on": "collect_progress",
"fieldname": "subject",
"fieldtype": "Data",
"label": "Subject",
"mandatory_depends_on": "collect_progress"
}
],
"icon": "fa fa-puzzle-piece",
@@ -454,7 +462,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
"modified": "2024-04-24 10:56:16.001032",
"modified": "2025-07-03 10:54:30.444139",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
@@ -501,6 +509,7 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "project_name,customer, status, priority, is_active",
"show_name_in_global_search": 1,
"sort_field": "modified",
@@ -509,4 +518,4 @@
"timeline_field": "customer",
"title_field": "project_name",
"track_seen": 1
}
}

View File

@@ -62,6 +62,7 @@ class Project(Document):
sales_order: DF.Link | None
second_email: DF.Time | None
status: DF.Literal["Open", "Completed", "Cancelled"]
subject: DF.Data | None
to_time: DF.Time | None
total_billable_amount: DF.Currency
total_billed_amount: DF.Currency
@@ -606,8 +607,6 @@ def send_project_update_email_to_users(project):
}
).insert()
subject = "For project %s, update your status" % (project)
incoming_email_account = frappe.db.get_value(
"Email Account", dict(enable_incoming=1, default_incoming=1), "email_id"
)
@@ -615,7 +614,7 @@ def send_project_update_email_to_users(project):
frappe.sendmail(
recipients=get_users_email(doc),
message=doc.message,
subject=_(subject),
subject=doc.subject,
reference_doctype=project_update.doctype,
reference_name=project_update.name,
reply_to=incoming_email_account,

View File

@@ -581,7 +581,8 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
transaction_date: frm.doc.transaction_date || frm.doc.posting_date,
ignore_pricing_rule: frm.doc.ignore_pricing_rule,
doctype: frm.doc.doctype
}
},
price_list: frm.doc.price_list,
},
freeze: true,
callback: function(r) {

View File

@@ -8,7 +8,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let me = this;
this.set_fields_onload_for_line_item();
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
this.frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
frappe.flags.hide_serial_batch_dialog = true;
frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) {
@@ -371,6 +371,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
"inspection_type": inspection_type,
"reference_type": me.frm.doc.doctype,
"reference_name": me.frm.doc.name,
"child_row_reference": row.doc.name,
"item_code": row.doc.item_code,
"description": row.doc.description,
"item_serial_no": row.doc.serial_no ? row.doc.serial_no.split("\n")[0] : null,
@@ -385,7 +386,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
docstatus: ["<", 2],
inspection_type: inspection_type,
reference_name: doc.name,
item_code: d.item_code
item_code: d.item_code,
child_row_reference : d.name
}
}
});
@@ -2427,12 +2429,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
fields: fields,
primary_action: function () {
const data = dialog.get_values();
const selected_data = data.items.filter(item => item?.__checked == 1 );
frappe.call({
method: "erpnext.controllers.stock_controller.make_quality_inspections",
args: {
doctype: me.frm.doc.doctype,
docname: me.frm.doc.name,
items: data.items
items: selected_data,
},
freeze: true,
callback: function (r) {

View File

@@ -1004,7 +1004,7 @@ erpnext.utils.map_current_doc = function (opts) {
if (
opts.allow_child_item_selection ||
["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)
["Purchase Receipt", "Delivery Note", "Pick List"].includes(opts.source_doctype)
) {
// args contains filtered child docnames
opts.args = args;

View File

@@ -81,7 +81,7 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
fieldtype: "Data",
},
{
label: __("State"),
label: __("State/Province"),
fieldname: "state",
fieldtype: "Data",
},

View File

@@ -284,13 +284,24 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
margin-top: var(--margin-md);
column-gap: var(--padding-sm);
row-gap: var(--padding-xs);
row-gap: var(--padding-sm);
}
> .transactions-label {
@extend .label;
> .transactions-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: var(--margin-md);
margin-bottom: var(--margin-sm);
> .recent-transactions {
@extend .label;
}
> .last-transaction {
font-weight: 400;
font-size: var(--text-sm);
}
}
}
@@ -299,8 +310,8 @@
overflow-x: hidden;
overflow-y: scroll;
margin-right: -12px;
padding-right: 12px;
margin-left: -10px;
scrollbar-width: thin;
> .no-transactions-placeholder {
height: 100%;
@@ -611,6 +622,11 @@
background-color: var(--gray-50);
}
&.invoice-selected {
background-color: var(--control-bg);
}
> .invoice-name-customer,
> .invoice-name-date {
display: flex;
flex-direction: column;
@@ -630,6 +646,21 @@
}
}
> .invoice-name-date {
> .invoice-name {
font-size: var(--text-md);
display: flex;
align-items: center;
font-weight: 700;
}
> .invoice-date {
@extend .nowrap;
font-size: var(--text-sm);
}
}
> .invoice-total-date,
> .invoice-total-status {
display: flex;
flex-direction: column;
@@ -650,6 +681,18 @@
justify-content: right;
}
}
> .invoice-total-status {
> .invoice-total {
margin-right: 8px;
}
> .invoice-status {
display: flex;
align-items: center;
justify-content: right;
}
}
}
> .item-details-container {
@@ -1020,21 +1063,31 @@
justify-content: flex-end;
padding-right: var(--padding-sm);
> .customer-name {
font-size: var(--text-2xl);
font-weight: 700;
}
> .customer-section {
margin-bottom: auto;
> .customer-email {
font-size: var(--text-md);
font-weight: 500;
> .customer-name {
font-size: var(--text-2xl);
font-weight: 700;
}
> .customer-code {
font-size: var(--text-xs);
font-weight: 500;
color: var(--text-light);
}
> .customer-email {
font-size: var(--text-md);
font-weight: 500;
}
}
> .cashier {
font-size: var(--text-md);
font-weight: 500;
color: var(--gray-600);
margin-top: auto;
margin-top: var(--margin-md);
}
}
@@ -1042,7 +1095,6 @@
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: space-between;
> .paid-amount {
font-size: var(--text-2xl);

View File

@@ -117,14 +117,15 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) {
if (
frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation ||
!doc.valid_till ||
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0
frappe.model.can_create("Sales Order") &&
(frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation ||
!doc.valid_till ||
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0)
) {
this.frm.add_custom_button(__("Sales Order"), () => this.make_sales_order(), __("Create"));
}
if (doc.status !== "Ordered") {
if (doc.status !== "Ordered" && this.frm.has_perm("write")) {
this.frm.add_custom_button(__("Set as Lost"), () => {
this.frm.trigger("set_as_lost_dialog");
});
@@ -133,7 +134,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
}
if (this.frm.doc.docstatus === 0) {
if (this.frm.doc.docstatus === 0 && frappe.model.can_read("Opportunity")) {
this.frm.add_custom_button(
__("Opportunity"),
function () {

View File

@@ -1741,8 +1741,8 @@ def create_pick_list(source_name, target_doc=None):
"doctype": "Pick List Item",
"field_map": {
"parent": "sales_order",
"name": "sales_order_item",
"parent_detail_docname": "product_bundle_item",
"parent_detail_docname": "sales_order_item",
"name": "product_bundle_item",
},
"field_no_map": ["picked_qty"],
"postprocess": update_packed_item_qty,

View File

@@ -338,13 +338,17 @@ def create_opening_voucher(pos_profile, company, balance_details):
@frappe.whitelist()
def get_past_order_list(search_term, status, limit=20):
fields = ["name", "grand_total", "currency", "customer", "posting_time", "posting_date"]
fields = ["name", "grand_total", "currency", "customer", "customer_name", "posting_time", "posting_date"]
invoice_list = []
if search_term and status:
invoices_by_customer = frappe.db.get_list(
"POS Invoice",
filters={"customer": ["like", f"%{search_term}%"], "status": status},
filters={"status": status},
or_filters={
"customer_name": ["like", f"%{search_term}%"],
"customer": ["like", f"%{search_term}%"],
},
fields=fields,
page_length=limit,
)

View File

@@ -342,7 +342,13 @@ erpnext.PointOfSale.ItemCart = class {
if (customer) {
return new Promise((resolve) => {
frappe.db
.get_value("Customer", customer, ["email_id", "mobile_no", "image", "loyalty_program"])
.get_value("Customer", customer, [
"email_id",
"customer_name",
"mobile_no",
"image",
"loyalty_program",
])
.then(({ message }) => {
const { loyalty_program } = message;
// if loyalty program then fetch loyalty points too
@@ -439,7 +445,7 @@ erpnext.PointOfSale.ItemCart = class {
update_customer_section() {
const me = this;
const { customer, email_id = "", mobile_no = "", image } = this.customer_info || {};
const { customer, customer_name, email_id = "", mobile_no = "", image } = this.customer_info || {};
if (customer) {
this.$customer_section.html(
@@ -447,7 +453,7 @@ erpnext.PointOfSale.ItemCart = class {
<div class="customer-display">
${this.get_customer_image()}
<div class="customer-name-desc">
<div class="customer-name">${customer}</div>
<div class="customer-name">${customer_name}</div>
${get_customer_description()}
</div>
<div class="reset-customer-btn" data-customer="${escape(customer)}">
@@ -867,7 +873,7 @@ erpnext.PointOfSale.ItemCart = class {
toggle_customer_info(show) {
if (show) {
const { customer } = this.customer_info || {};
const { customer, customer_name } = this.customer_info || {};
this.$cart_container.css("display", "none");
this.$customer_section.css({
@@ -886,8 +892,8 @@ erpnext.PointOfSale.ItemCart = class {
<div class="customer-display">
${this.get_customer_image()}
<div class="customer-name-desc">
<div class="customer-name">${customer}</div>
<div class="customer-desc"></div>
<div class="customer-name">${customer_name}</div>
<div class="customer-desc">${customer}</div>
</div>
</div>
<div class="customer-fields-container">
@@ -896,7 +902,10 @@ erpnext.PointOfSale.ItemCart = class {
<div class="loyalty_program-field"></div>
<div class="loyalty_points-field"></div>
</div>
<div class="transactions-label">${__("Recent Transactions")}</div>`
<div class="transactions-section">
<div class="recent-transactions">${__("Recent Transactions")}</div>
<div class="last-transaction"></div>
</div>`
);
// transactions need to be in diff div from sticky elem for scrolling
this.$customer_section.append(`<div class="customer-transactions"></div>`);
@@ -1005,7 +1014,7 @@ erpnext.PointOfSale.ItemCart = class {
const elapsed_time = moment(res[0].posting_date + " " + res[0].posting_time).fromNow();
this.$customer_section
.find(".customer-desc")
.find(".last-transaction")
.html(`${__("Last transacted")} ${__(elapsed_time)}`);
res.forEach((invoice) => {
@@ -1027,7 +1036,7 @@ erpnext.PointOfSale.ItemCart = class {
</div>
<div class="invoice-total-status">
<div class="invoice-total">
${format_currency(invoice.grand_total, invoice.currency, 0) || 0}
${format_currency(invoice.grand_total, invoice.currency, frappe.sys_defaults.currency_precision) || 0}
</div>
<div class="invoice-status">
<span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status]}">

View File

@@ -106,7 +106,7 @@ erpnext.PointOfSale.PastOrderList = class {
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
</svg>
${frappe.ellipsis(invoice.customer, 20)}
${frappe.ellipsis(invoice.customer_name, 20)}
</div>
</div>
<div class="invoice-total-status">

View File

@@ -73,14 +73,18 @@ erpnext.PointOfSale.PastOrderSummary = class {
get_upper_section_html(doc) {
const { status } = doc;
let indicator_color = "";
const is_customer_naming_by_customer_name = frappe.sys_defaults.cust_master_name !== "Customer Name";
["Paid", "Consolidated"].includes(status) && (indicator_color = "green");
status === "Draft" && (indicator_color = "red");
status === "Return" && (indicator_color = "grey");
return `<div class="left-section">
<div class="customer-name">${doc.customer}</div>
<div class="customer-email">${this.customer_email}</div>
<div class="customer-section">
<div class="customer-name">${doc.customer_name}</div>
${is_customer_naming_by_customer_name ? `<div class="customer-code">${doc.customer}</div>` : ""}
<div class="customer-email">${this.customer_email}</div>
</div>
<div class="cashier">${__("Sold by")}: ${doc.owner}</div>
</div>
<div class="right-section">

View File

@@ -395,7 +395,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"General Ledger",
self.meta.get_label("income"),
_(self.meta.get_label("income")),
filters={
"from_date": self.future_from_date,
"to_date": self.future_to_date,
@@ -427,7 +427,7 @@ class EmailDigest(Document):
filters = {"currency": self.currency}
label = get_link_to_report(
"Profit and Loss Statement",
label=self.meta.get_label(root_type + "_year_to_date"),
label=_(self.meta.get_label(root_type + "_year_to_date")),
filters=filters,
)
@@ -435,7 +435,7 @@ class EmailDigest(Document):
filters = {"currency": self.currency}
label = get_link_to_report(
"Profit and Loss Statement",
label=self.meta.get_label(root_type + "_year_to_date"),
label=_(self.meta.get_label(root_type + "_year_to_date")),
filters=filters,
)
@@ -466,7 +466,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"General Ledger",
self.meta.get_label("expenses_booked"),
_(self.meta.get_label("expenses_booked")),
filters={
"company": self.company,
"from_date": self.future_from_date,
@@ -500,7 +500,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Sales Order",
label=self.meta.get_label("sales_orders_to_bill"),
label=_(self.meta.get_label("sales_orders_to_bill")),
report_type="Report Builder",
doctype="Sales Order",
filters={
@@ -526,7 +526,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Sales Order",
label=self.meta.get_label("sales_orders_to_deliver"),
label=_(self.meta.get_label("sales_orders_to_deliver")),
report_type="Report Builder",
doctype="Sales Order",
filters={
@@ -552,7 +552,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Purchase Order",
label=self.meta.get_label("purchase_orders_to_receive"),
label=_(self.meta.get_label("purchase_orders_to_receive")),
report_type="Report Builder",
doctype="Purchase Order",
filters={
@@ -578,7 +578,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Purchase Order",
label=self.meta.get_label("purchase_orders_to_bill"),
label=_(self.meta.get_label("purchase_orders_to_bill")),
report_type="Report Builder",
doctype="Purchase Order",
filters={
@@ -630,7 +630,7 @@ class EmailDigest(Document):
"company": self.company,
}
label = get_link_to_report(
"Account Balance", label=self.meta.get_label(fieldname), filters=filters
"Account Balance", label=_(self.meta.get_label(fieldname)), filters=filters
)
else:
filters = {
@@ -640,7 +640,7 @@ class EmailDigest(Document):
"company": self.company,
}
label = get_link_to_report(
"Account Balance", label=self.meta.get_label(fieldname), filters=filters
"Account Balance", label=_(self.meta.get_label(fieldname)), filters=filters
)
return {"label": label, "value": balance, "last_value": prev_balance}
@@ -648,17 +648,17 @@ class EmailDigest(Document):
if account_type == "Payable":
label = get_link_to_report(
"Accounts Payable",
label=self.meta.get_label(fieldname),
label=_(self.meta.get_label(fieldname)),
filters={"report_date": self.future_to_date, "company": self.company},
)
elif account_type == "Receivable":
label = get_link_to_report(
"Accounts Receivable",
label=self.meta.get_label(fieldname),
label=_(self.meta.get_label(fieldname)),
filters={"report_date": self.future_to_date, "company": self.company},
)
else:
label = self.meta.get_label(fieldname)
label = _(self.meta.get_label(fieldname))
return {"label": label, "value": balance, "last_value": prev_balance, "count": count}
@@ -748,7 +748,7 @@ class EmailDigest(Document):
label = get_link_to_report(
"Quotation",
label=self.meta.get_label(fieldname),
label=_(self.meta.get_label(fieldname)),
report_type="Report Builder",
doctype="Quotation",
filters={
@@ -779,7 +779,7 @@ class EmailDigest(Document):
label = get_link_to_report(
doc_type,
label=self.meta.get_label(fieldname),
label=_(self.meta.get_label(fieldname)),
report_type="Report Builder",
filters=filters,
doctype=doc_type,

View File

@@ -51,7 +51,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
let stock_uom = unescape(element.attr("data-stock-uom"));
if (disable_quick_entry) {
open_stock_entry(item, warehouse, entry_type);
open_stock_entry(item, warehouse, entry_type, stock_uom);
} else {
if (action === "Add") {
let rate = unescape($(this).attr("data-rate"));
@@ -66,7 +66,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
}
}
function open_stock_entry(item, warehouse, entry_type) {
function open_stock_entry(item, warehouse, entry_type, stock_uom) {
frappe.model.with_doctype("Stock Entry", function () {
var doc = frappe.model.get_new_doc("Stock Entry");
if (entry_type) {
@@ -75,6 +75,9 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
var row = frappe.model.add_child(doc, "items");
row.item_code = item;
row.uom = stock_uom;
row.stock_uom = stock_uom;
row.conversion_factor = 1;
if (entry_type === "Material Transfer") {
row.s_warehouse = warehouse;

View File

@@ -264,7 +264,7 @@ def update_qty(bin_name, args):
actual_qty = bin_details.actual_qty or 0.0
# actual qty is not up to date in case of backdated transaction
if future_sle_exists(args, allow_force_reposting=False):
if future_sle_exists(args):
actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse"))
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))

View File

@@ -188,6 +188,55 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
);
}
if (
!doc.is_return &&
doc.status != "Closed" &&
this.frm.has_perm("write") &&
frappe.model.can_read("Pick List") &&
this.frm.doc.docstatus === 0
) {
this.frm.add_custom_button(
__("Pick List"),
function () {
if (!me.frm.doc.customer) {
frappe.throw({
title: __("Mandatory"),
message: __("Please Select a Customer"),
});
}
erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.pick_list.pick_list.create_dn_for_pick_lists",
source_doctype: "Pick List",
target: me.frm,
setters: [
{
fieldname: "customer",
default: me.frm.doc.customer,
label: __("Customer"),
fieldtype: "Link",
options: "Customer",
reqd: 1,
read_only: 1,
},
{
fieldname: "sales_order",
label: __("Sales Order"),
fieldtype: "Link",
options: "Sales Order",
link_filters: `[["Sales Order","customer","=","${me.frm.doc.customer}"],["Sales Order","docstatus","=","1"],["Sales Order","delivery_status","not in",["Closed","Fully Delivered"]]]`,
},
],
get_query_filters: {
company: me.frm.doc.company,
},
get_query_method: "erpnext.stock.doctype.pick_list.pick_list.get_pick_list_query",
size: "extra-large",
});
},
__("Get Items From")
);
}
if (!doc.is_return && doc.status != "Closed") {
if (doc.docstatus == 1 && frappe.model.can_create("Shipment")) {
this.frm.add_custom_button(

View File

@@ -38,7 +38,6 @@
"ignore_pricing_rule",
"items_section",
"scan_barcode",
"pick_list",
"col_break_warehouse",
"set_warehouse",
"set_target_warehouse",
@@ -1218,15 +1217,6 @@
"options": "Sales Team",
"print_hide": 1
},
{
"fieldname": "pick_list",
"fieldtype": "Link",
"hidden": 1,
"label": "Pick List",
"options": "Pick List",
"read_only": 1,
"search_index": 1
},
{
"default": "0",
"fetch_from": "customer.is_internal_customer",

View File

@@ -174,6 +174,19 @@ class DeliveryNote(SellingController):
"overflow_type": "delivery",
"no_allowance": 1,
},
{
"source_dt": "Delivery Note Item",
"target_dt": "Pick List Item",
"join_field": "pick_list_item",
"target_field": "delivered_qty",
"target_parent_dt": "Pick List",
"target_parent_field": "per_delivered",
"target_ref_field": "picked_qty",
"source_field": "stock_qty",
"percent_join_field": "against_pick_list",
"status_field": "delivery_status",
"keyword": "Delivered",
},
]
if cint(self.is_return):
self.status_updater.extend(
@@ -326,18 +339,15 @@ class DeliveryNote(SellingController):
def set_serial_and_batch_bundle_from_pick_list(self):
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
if not self.pick_list:
return
for item in self.items:
if item.use_serial_batch_fields:
if item.use_serial_batch_fields or not item.against_pick_list:
continue
if item.pick_list_item and not item.serial_and_batch_bundle:
filters = {
"item_code": item.item_code,
"voucher_type": "Pick List",
"voucher_no": self.pick_list,
"voucher_no": item.against_pick_list,
"voucher_detail_no": item.pick_list_item,
}
@@ -586,7 +596,9 @@ class DeliveryNote(SellingController):
def update_pick_list_status(self):
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
update_pick_list_status(self.pick_list)
pick_lists = {row.against_pick_list for row in self.items if row.against_pick_list}
for pick_list in pick_lists:
update_pick_list_status(pick_list)
def check_next_docstatus(self):
submit_rv = frappe.db.sql(
@@ -1269,6 +1281,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"doctype": target_doctype,
"postprocess": update_details,
"field_no_map": ["taxes_and_charges", "set_warehouse"],
"field_map": {"shipping_address_name": "shipping_address"},
},
doctype + " Item": {
"doctype": target_doctype + " Item",

View File

@@ -77,6 +77,7 @@
"against_sales_invoice",
"si_detail",
"dn_detail",
"against_pick_list",
"pick_list_item",
"section_break_40",
"pick_serial_and_batch",
@@ -935,13 +936,23 @@
{
"fieldname": "column_break_fguf",
"fieldtype": "Column Break"
},
{
"fieldname": "against_pick_list",
"fieldtype": "Link",
"label": "Against Pick List",
"no_copy": 1,
"options": "Pick List",
"print_hide": 1,
"read_only": 1,
"search_index": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-02-05 14:28:33.322181",
"modified": "2025-05-31 18:51:32.651562",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",

View File

@@ -16,6 +16,7 @@ class DeliveryNoteItem(Document):
actual_batch_qty: DF.Float
actual_qty: DF.Float
against_pick_list: DF.Link | None
against_sales_invoice: DF.Link | None
against_sales_order: DF.Link | None
allow_zero_valuation_rate: DF.Check

View File

@@ -75,7 +75,9 @@ frappe.ui.form.on("Inventory Dimension", {
set_parent_fields(frm) {
if (frm.doc.apply_to_all_doctypes) {
frm.set_df_property("fetch_from_parent", "options", frm.doc.reference_document);
let options = ["\n", frm.doc.reference_document];
frm.set_df_property("fetch_from_parent", "options", options);
} else if (frm.doc.document_type && frm.doc.istable) {
frappe.call({
method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields",
@@ -85,7 +87,7 @@ frappe.ui.form.on("Inventory Dimension", {
},
callback: (r) => {
if (r.message && r.message.length) {
frm.set_df_property("fetch_from_parent", "options", [""].concat(r.message));
frm.set_df_property("fetch_from_parent", "options", ["\n"].concat(r.message));
} else {
frm.set_df_property("fetch_from_parent", "hidden", 1);
}

View File

@@ -143,7 +143,6 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:!doc.apply_to_all_doctypes",
"description": "Set fieldname from which you want to fetch the data from the parent form.",
"fieldname": "fetch_from_parent",
"fieldtype": "Select",
@@ -189,7 +188,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-07-08 08:58:50.228211",
"modified": "2025-07-07 15:51:29.329064",
"modified_by": "Administrator",
"module": "Stock",
"name": "Inventory Dimension",

View File

@@ -64,16 +64,11 @@ class InventoryDimension(Document):
self.reset_value()
self.set_source_and_target_fieldname()
self.set_type_of_transaction()
self.set_fetch_value_from()
def set_type_of_transaction(self):
if self.apply_to_all_doctypes:
self.type_of_transaction = "Both"
def set_fetch_value_from(self):
if self.apply_to_all_doctypes:
self.fetch_from_parent = self.reference_document
def do_not_update_document(self):
if self.is_new() or not self.has_stock_ledger():
return

View File

@@ -155,6 +155,8 @@ class TestInventoryDimension(FrappeTestCase):
reference_document="Rack", dimension_name="Rack", apply_to_all_doctypes=1
)
inv_dimension.db_set("fetch_from_parent", "Rack")
self.assertEqual(inv_dimension.type_of_transaction, "Both")
self.assertEqual(inv_dimension.fetch_from_parent, "Rack")

View File

@@ -634,7 +634,7 @@ class Item(Document):
if new_properties != [cstr(self.get(field)) for field in field_list]:
msg = _("To merge, following properties must be same for both items")
msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
msg += ": \n" + ", ".join([_(self.meta.get_label(fld)) for fld in field_list])
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
@@ -981,7 +981,7 @@ class Item(Document):
return
if linked_doc := self._get_linked_submitted_documents(changed_fields):
changed_field_labels = [frappe.bold(self.meta.get_label(f)) for f in changed_fields]
changed_field_labels = [frappe.bold(_(self.meta.get_label(f))) for f in changed_fields]
msg = _(
"As there are existing submitted transactions against item {0}, you can not change the value of {1}."
).format(self.name, ", ".join(changed_field_labels))

View File

@@ -332,5 +332,6 @@ def get_pr_items(purchase_receipt):
(pr_item.parent == purchase_receipt.receipt_document)
& ((item.is_stock_item == 1) | (item.is_fixed_asset == 1))
)
.orderby(pr_item.idx)
.run(as_dict=True)
)

View File

@@ -42,6 +42,15 @@ frappe.ui.form.on("Material Request", {
},
};
});
frm.set_query("price_list", () => {
return {
filters: {
buying: 1,
enabled: 1,
},
};
});
},
onload: function (frm) {
@@ -70,6 +79,7 @@ frappe.ui.form.on("Material Request", {
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
frm.doc.price_list = frappe.defaults.get_default("buying_price_list");
},
company: function (frm) {
@@ -245,7 +255,9 @@ frappe.ui.form.on("Material Request", {
from_warehouse: item.from_warehouse,
warehouse: item.warehouse,
doctype: frm.doc.doctype,
buying_price_list: frappe.defaults.get_default("buying_price_list"),
buying_price_list: frm.doc.price_list
? frm.doc.price_list
: frappe.defaults.get_default("buying_price_list"),
currency: frappe.defaults.get_default("Currency"),
name: frm.doc.name,
qty: item.qty || 1,

View File

@@ -16,6 +16,7 @@
"column_break_2",
"transaction_date",
"schedule_date",
"price_list",
"amended_from",
"warehouse_section",
"scan_barcode",
@@ -351,13 +352,19 @@
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"fieldname": "price_list",
"fieldtype": "Link",
"label": "Price List",
"options": "Price List"
}
],
"icon": "fa fa-ticket",
"idx": 70,
"is_submittable": 1,
"links": [],
"modified": "2025-04-21 18:36:04.827917",
"modified": "2025-07-07 13:15:28.615984",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",
@@ -425,10 +432,11 @@
}
],
"quick_entry": 1,
"row_format": "Dynamic",
"search_fields": "status,transaction_date",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}
}

View File

@@ -8,6 +8,7 @@
import json
import frappe
import frappe.defaults
from frappe import _, msgprint
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
@@ -45,6 +46,7 @@ class MaterialRequest(BuyingController):
naming_series: DF.Literal["MAT-MR-.YYYY.-"]
per_ordered: DF.Percent
per_received: DF.Percent
price_list: DF.Link | None
scan_barcode: DF.Data | None
schedule_date: DF.Date | None
select_print_heading: DF.Link | None
@@ -151,6 +153,9 @@ class MaterialRequest(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
if not self.price_list:
self.price_list = frappe.defaults.get_defaults().buying_price_list
def before_update_after_submit(self):
self.validate_schedule_date()
@@ -764,10 +769,11 @@ def raise_work_orders(material_request):
"material_request_item": d.name,
"planned_start_date": mr.transaction_date,
"company": mr.company,
"project": d.project,
}
)
wo_order.set_work_order_operations()
wo_order.get_items_and_operations_from_bom()
wo_order.flags.ignore_validate = True
wo_order.flags.ignore_mandatory = True
wo_order.save()

View File

@@ -7,6 +7,7 @@
import json
import frappe
import frappe.defaults
from frappe.model.document import Document
from frappe.utils import flt
@@ -80,6 +81,10 @@ def make_packing_list(doc):
update_packed_item_basic_data(item_row, pi_row, bundle_item, item_data)
update_packed_item_stock_data(item_row, pi_row, bundle_item, item_data, doc)
update_packed_item_price_data(pi_row, item_data, doc)
if item_row.get("against_pick_list"):
update_packed_item_with_pick_list_info(item_row, pi_row)
update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc)
if set_price_from_children: # create/update bundle item wise price dict
@@ -228,6 +233,28 @@ def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data
pi_row.use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields")
def update_packed_item_with_pick_list_info(main_item_row, pi_row):
pl_row = frappe.db.get_value(
"Pick List Item",
{
"item_code": pi_row.item_code,
"sales_order": main_item_row.get("against_sales_order"),
"sales_order_item": main_item_row.get("so_detail"),
"parent": main_item_row.against_pick_list,
},
["warehouse", "batch_no", "serial_no"],
as_dict=True,
order_by="qty desc",
)
if not pl_row:
return
pi_row.warehouse = pl_row.warehouse
pi_row.batch_no = pl_row.batch_no
pi_row.serial_no = pl_row.serial_no
def update_packed_item_price_data(pi_row, item_data, doc):
"Set price as per price list or from the Item master."
if pi_row.rate:
@@ -313,12 +340,20 @@ def on_doctype_update():
@frappe.whitelist()
def get_items_from_product_bundle(row):
def get_items_from_product_bundle(row, price_list):
row, items = json.loads(row), []
bundled_items = get_product_bundle_items(row["item_code"])
for item in bundled_items:
row.update({"item_code": item.item_code, "qty": flt(row["quantity"]) * flt(item.qty)})
row.update(
{
"item_code": item.item_code,
"qty": flt(row["quantity"]) * flt(item.qty),
"conversion_rate": 1,
"price_list": price_list,
"currency": frappe.defaults.get_defaults().currency,
}
)
items.append(get_item_details(row))
return items

View File

@@ -98,34 +98,28 @@ frappe.ui.form.on("Pick List", {
refresh: (frm) => {
frm.trigger("add_get_items_button");
if (frm.doc.docstatus === 1) {
frappe
.xcall("erpnext.stock.doctype.pick_list.pick_list.target_document_exists", {
pick_list_name: frm.doc.name,
purpose: frm.doc.purpose,
})
.then((target_document_exists) => {
frm.set_df_property("locations", "allow_on_submit", target_document_exists ? 0 : 1);
const status_completed = frm.doc.status === "Completed";
frm.set_df_property("locations", "allow_on_submit", status_completed ? 0 : 1);
if (target_document_exists) return;
if (!status_completed) {
frm.add_custom_button(__("Update Current Stock"), () =>
frm.trigger("update_pick_list_stock")
);
frm.add_custom_button(__("Update Current Stock"), () =>
frm.trigger("update_pick_list_stock")
if (frm.doc.purpose === "Delivery") {
frm.add_custom_button(
__("Create Delivery Note"),
() => frm.trigger("create_delivery_note"),
__("Create")
);
if (frm.doc.purpose === "Delivery") {
frm.add_custom_button(
__("Delivery Note"),
() => frm.trigger("create_delivery_note"),
__("Create")
);
} else {
frm.add_custom_button(
__("Stock Entry"),
() => frm.trigger("create_stock_entry"),
__("Create")
);
}
});
} else {
frm.add_custom_button(
__("Create Stock Entry"),
() => frm.trigger("create_stock_entry"),
__("Create")
);
}
}
if (frm.doc.purpose === "Delivery" && frm.doc.status === "Open") {
if (frm.doc.__onload && frm.doc.__onload.has_unreserved_stock) {

View File

@@ -30,7 +30,11 @@
"amended_from",
"print_settings_section",
"group_same_items",
"status"
"status_section",
"status",
"column_break_qyam",
"delivery_status",
"per_delivered"
],
"fields": [
{
@@ -181,7 +185,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Draft\nOpen\nCompleted\nCancelled",
"options": "Draft\nOpen\nPartly Delivered\nCompleted\nCancelled",
"print_hide": 1,
"read_only": 1,
"report_hide": 1,
@@ -208,11 +212,42 @@
"fieldname": "ignore_pricing_rule",
"fieldtype": "Check",
"label": "Ignore Pricing Rule"
},
{
"collapsible": 1,
"fieldname": "status_section",
"fieldtype": "Section Break",
"label": "Status",
"print_hide": 1
},
{
"fieldname": "delivery_status",
"fieldtype": "Select",
"hidden": 1,
"in_standard_filter": 1,
"label": "Delivery Status",
"no_copy": 1,
"options": "Not Delivered\nFully Delivered\nPartly Delivered",
"print_hide": 1
},
{
"depends_on": "eval:!doc.__islocal && doc.purpose === \"Delivery\"",
"description": "% of materials delivered against this Pick List",
"fieldname": "per_delivered",
"fieldtype": "Percent",
"label": "% Delivered",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_qyam",
"fieldtype": "Column Break"
}
],
"is_submittable": 1,
"links": [],
"modified": "2024-08-14 13:20:42.168827",
"modified": "2025-05-31 19:18:30.860044",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",
@@ -280,6 +315,7 @@
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],

View File

@@ -7,8 +7,7 @@ from itertools import groupby
import frappe
from frappe import _, bold
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc
from frappe.model.mapper import get_mapped_doc, map_child_doc
from frappe.query_builder import Case
from frappe.query_builder.custom import GROUP_CONCAT
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
@@ -28,11 +27,12 @@ from erpnext.stock.serial_batch_bundle import (
get_batches_from_bundle,
get_serial_nos_from_bundle,
)
from erpnext.utilities.transaction_base import TransactionBase
# TODO: Prioritize SO or WO group warehouse
class PickList(Document):
class PickList(TransactionBase):
# begin: auto-generated types
# This code is auto-generated. Do not modify anything in this block.
@@ -48,6 +48,7 @@ class PickList(Document):
consider_rejected_warehouses: DF.Check
customer: DF.Link | None
customer_name: DF.Data | None
delivery_status: DF.Literal["Not Delivered", "Fully Delivered", "Partly Delivered"]
for_qty: DF.Float
group_same_items: DF.Check
ignore_pricing_rule: DF.Check
@@ -55,12 +56,13 @@ class PickList(Document):
material_request: DF.Link | None
naming_series: DF.Literal["STO-PICK-.YYYY.-"]
parent_warehouse: DF.Link | None
per_delivered: DF.Percent
pick_manually: DF.Check
prompt_qty: DF.Check
purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"]
scan_barcode: DF.Data | None
scan_mode: DF.Check
status: DF.Literal["Draft", "Open", "Completed", "Cancelled"]
status: DF.Literal["Draft", "Open", "Partly Delivered", "Completed", "Cancelled"]
work_order: DF.Link | None
# end: auto-generated types
@@ -77,6 +79,7 @@ class PickList(Document):
self.validate_for_qty()
self.validate_stock_qty()
self.check_serial_no_status()
self.validate_with_previous_doc()
def before_save(self):
self.update_status()
@@ -150,6 +153,18 @@ class PickList(Document):
title=_("Incorrect Warehouse"),
)
def validate_with_previous_doc(self):
super().validate_with_previous_doc(
{
"Sales Order": {
"ref_dn_field": "sales_order",
"compare_fields": [
["company", "="],
],
},
}
)
def validate_sales_order_percentage(self):
# set percentage picked in SO
for location in self.get("locations"):
@@ -326,19 +341,19 @@ class PickList(Document):
doc.submit()
def update_status(self, status=None, update_modified=True):
if not status:
if self.docstatus == 0:
status = "Draft"
elif self.docstatus == 1:
if target_document_exists(self.name, self.purpose):
status = "Completed"
else:
status = "Open"
elif self.docstatus == 2:
status = "Cancelled"
if status:
self.db_set("status", status)
self.db_set("status", status, update_modified=update_modified)
else:
self.set_status(update=True)
def stock_entry_exists(self):
if self.docstatus != 1:
return False
if self.purpose == "Delivery":
return False
return stock_entry_exists(self.name)
def update_reference_qty(self):
packed_items = []
@@ -346,7 +361,7 @@ class PickList(Document):
for item in self.locations:
if item.product_bundle_item:
packed_items.append(item.sales_order_item)
packed_items.append(item.product_bundle_item)
elif item.sales_order_item:
so_items.append(item.sales_order_item)
@@ -357,12 +372,12 @@ class PickList(Document):
self.update_sales_order_item_qty(so_items)
def update_packed_items_qty(self, packed_items):
picked_items = get_picked_items_qty(packed_items)
picked_items = get_picked_items_qty(packed_items, contains_packed_items=True)
self.validate_picked_qty(picked_items)
picked_qty = frappe._dict()
for d in picked_items:
picked_qty[d.sales_order_item] = d.picked_qty
picked_qty[d.product_bundle_item] = d.picked_qty
for packed_item in packed_items:
frappe.db.set_value(
@@ -575,7 +590,6 @@ class PickList(Document):
# maintain count of each item (useful to limit get query)
self.item_count_map.setdefault(item_code, 0)
self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty"))
return item_map.values()
def validate_for_qty(self):
@@ -739,9 +753,10 @@ class PickList(Document):
for item in self.locations:
if not item.product_bundle_item:
continue
product_bundles[item.product_bundle_item] = frappe.db.get_value(
product_bundles[item.sales_order_item] = frappe.db.get_value(
"Sales Order Item",
item.product_bundle_item,
item.sales_order_item,
"item_code",
)
return product_bundles
@@ -757,17 +772,16 @@ class PickList(Document):
def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int:
"""Compute how many full bundles can be created from picked items."""
precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction")
possible_bundles = []
possible_bundles = {}
for item in self.locations:
if item.product_bundle_item != bundle_row:
if item.sales_order_item != bundle_row:
continue
if qty_in_bundle := bundle_items.get(item.item_code):
possible_bundles.append(item.picked_qty / qty_in_bundle)
else:
possible_bundles.append(0)
return int(flt(min(possible_bundles), precision or 6))
possible_bundles.setdefault(item.product_bundle_item, 0)
possible_bundles[item.product_bundle_item] += item.picked_qty / qty_in_bundle
return int(flt(min(possible_bundles.values()), precision or 6)) if possible_bundles else 0
def has_unreserved_stock(self):
if self.purpose == "Delivery":
@@ -800,24 +814,35 @@ def update_pick_list_status(pick_list):
doc.run_method("update_status")
def get_picked_items_qty(items) -> list[dict]:
def get_picked_items_qty(items, contains_packed_items=False) -> list[dict]:
pi_item = frappe.qb.DocType("Pick List Item")
return (
query = (
frappe.qb.from_(pi_item)
.select(
pi_item.sales_order_item,
pi_item.product_bundle_item,
pi_item.item_code,
pi_item.sales_order,
Sum(pi_item.stock_qty).as_("stock_qty"),
Sum(pi_item.picked_qty).as_("picked_qty"),
)
.where((pi_item.docstatus == 1) & (pi_item.sales_order_item.isin(items)))
.groupby(
.where(pi_item.docstatus == 1)
.for_update()
)
if contains_packed_items:
query = query.groupby(
pi_item.product_bundle_item,
pi_item.sales_order,
).where(pi_item.product_bundle_item.isin(items))
else:
query = query.groupby(
pi_item.sales_order_item,
pi_item.sales_order,
)
.for_update()
).run(as_dict=True)
).where(pi_item.sales_order_item.isin(items))
return query.run(as_dict=True)
def validate_item_locations(pick_list):
@@ -1188,13 +1213,17 @@ def create_delivery_note(source_name, target_doc=None):
if not all(item.sales_order for item in pick_list.locations):
delivery_note = create_dn_wo_so(pick_list)
delivery_note.flags.ignore_mandatory = True
delivery_note.save()
frappe.msgprint(_("Delivery Note(s) created for the Pick List"))
return delivery_note
def create_dn_wo_so(pick_list):
delivery_note = frappe.new_doc("Delivery Note")
def create_dn_wo_so(pick_list, delivery_note=None):
if not delivery_note:
delivery_note = frappe.new_doc("Delivery Note")
delivery_note.company = pick_list.company
item_table_mapper_without_so = {
@@ -1206,14 +1235,61 @@ def create_dn_wo_so(pick_list):
},
}
map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note)
delivery_note.insert(ignore_mandatory=True)
return delivery_note
@frappe.whitelist()
def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None):
"""Get Items from Multiple Pick Lists and create a Delivery Note for filtered customer"""
pick_list = frappe.get_doc("Pick List", source_name)
validate_item_locations(pick_list)
sales_order_arg = kwargs.get("sales_order") if kwargs else None
customer_arg = kwargs.get("customer") if kwargs else None
if sales_order_arg:
sales_orders = {sales_order_arg}
else:
sales_orders = {row.sales_order for row in pick_list.locations if row.sales_order}
if customer_arg:
sales_orders = frappe.get_all(
"Sales Order",
filters={"customer": customer_arg, "name": ["in", list(sales_orders)]},
pluck="name",
)
delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc)
if not sales_order_arg and not all(item.sales_order for item in pick_list.locations):
if isinstance(delivery_note, str):
delivery_note = frappe.get_doc(frappe.parse_json(delivery_note))
delivery_note = create_dn_wo_so(pick_list, delivery_note)
return delivery_note
def create_dn_with_so(sales_dict, pick_list):
"""Create Delivery Note for each customer (based on SO) in a Pick List."""
delivery_note = None
for customer in sales_dict:
delivery_note = create_dn_from_so(pick_list, sales_dict[customer], None)
if delivery_note:
delivery_note.flags.ignore_mandatory = True
# updates packed_items on save
# save as multiple customers are possible
delivery_note.save()
return delivery_note
def create_dn_from_so(pick_list, sales_order_list, delivery_note=None):
if not sales_order_list:
return delivery_note
item_table_mapper = {
"doctype": "Delivery Note Item",
"field_map": {
@@ -1224,20 +1300,17 @@ def create_dn_with_so(sales_dict, pick_list):
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1,
}
for customer in sales_dict:
for so in sales_dict[customer]:
delivery_note = None
kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
delivery_note = create_delivery_note_from_sales_order(so, delivery_note, kwargs=kwargs)
break
if delivery_note:
# map all items of all sales orders of that customer
for so in sales_dict[customer]:
map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
delivery_note.flags.ignore_mandatory = True
delivery_note.insert()
update_packed_item_details(pick_list, delivery_note)
delivery_note.save()
kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
delivery_note = create_delivery_note_from_sales_order(
next(iter(sales_order_list)), delivery_note, kwargs=kwargs
)
if not delivery_note:
return
for so in sales_order_list:
map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
return delivery_note
@@ -1257,24 +1330,29 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
dn_item = map_child_doc(source_doc, delivery_note, item_mapper)
if dn_item:
dn_item.against_pick_list = pick_list.name
dn_item.pick_list_item = location.name
dn_item.warehouse = location.warehouse
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
dn_item.qty = flt(location.picked_qty - location.delivered_qty) / (
flt(dn_item.conversion_factor) or 1
)
dn_item.batch_no = location.batch_no
dn_item.serial_no = location.serial_no
dn_item.use_serial_batch_fields = location.use_serial_batch_fields
update_delivery_note_item(source_doc, dn_item, delivery_note)
add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper)
add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper, sales_order)
set_delivery_note_missing_values(delivery_note)
delivery_note.pick_list = pick_list.name
delivery_note.company = pick_list.company
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
if sales_order:
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
def add_product_bundles_to_delivery_note(pick_list: "PickList", delivery_note, item_mapper) -> None:
def add_product_bundles_to_delivery_note(
pick_list: "PickList", delivery_note, item_mapper, sales_order=None
) -> None:
"""Add product bundles found in pick list to delivery note.
When mapping pick list items, the bundle item itself isn't part of the
@@ -1284,38 +1362,17 @@ def add_product_bundles_to_delivery_note(pick_list: "PickList", delivery_note, i
for so_row, item_code in product_bundles.items():
sales_order_item = frappe.get_doc("Sales Order Item", so_row)
if sales_order and sales_order_item.parent != sales_order:
continue
dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper)
dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
so_row, product_bundle_qty_map[item_code]
)
dn_bundle_item.against_pick_list = pick_list.name
update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)
def update_packed_item_details(pick_list: "PickList", delivery_note) -> None:
"""Update stock details on packed items table of delivery note."""
def _find_so_row(packed_item):
for item in delivery_note.items:
if packed_item.parent_detail_docname == item.name:
return item.so_detail
def _find_pick_list_location(bundle_row, packed_item):
if not bundle_row:
return
for loc in pick_list.locations:
if loc.product_bundle_item == bundle_row and loc.item_code == packed_item.item_code:
return loc
for packed_item in delivery_note.packed_items:
so_row = _find_so_row(packed_item)
location = _find_pick_list_location(so_row, packed_item)
if not location:
continue
packed_item.warehouse = location.warehouse
packed_item.batch_no = location.batch_no
packed_item.serial_no = location.serial_no
@frappe.whitelist()
def create_stock_entry(pick_list):
pick_list = frappe.get_doc(json.loads(pick_list))
@@ -1362,14 +1419,6 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
).run(as_dict=as_dict)
@frappe.whitelist()
def target_document_exists(pick_list_name, purpose):
if purpose == "Delivery":
return frappe.db.exists("Delivery Note", {"pick_list": pick_list_name, "docstatus": 1})
return stock_entry_exists(pick_list_name)
@frappe.whitelist()
def get_item_details(item_code, uom=None):
details = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1)
@@ -1490,3 +1539,50 @@ def get_rejected_warehouses():
)
return frappe.local.rejected_warehouses
@frappe.whitelist()
def get_pick_list_query(doctype, txt, searchfield, start, page_len, filters):
frappe.has_permission("Pick List", throw=True)
if not filters.get("company"):
frappe.throw(_("Please select a Company"))
PICK_LIST = frappe.qb.DocType("Pick List")
PICK_LIST_ITEM = frappe.qb.DocType("Pick List Item")
SALES_ORDER = frappe.qb.DocType("Sales Order")
query = (
frappe.qb.from_(PICK_LIST)
.join(PICK_LIST_ITEM)
.on(PICK_LIST.name == PICK_LIST_ITEM.parent)
.join(SALES_ORDER)
.on(PICK_LIST_ITEM.sales_order == SALES_ORDER.name)
.select(
PICK_LIST.name,
SALES_ORDER.customer,
Replace(GROUP_CONCAT(PICK_LIST_ITEM.sales_order).distinct(), ",", "<br>").as_("sales_order"),
)
.where(PICK_LIST.docstatus == 1)
.where(PICK_LIST.status.isin(["Open", "Partly Delivered"]))
.where(PICK_LIST.company == filters.get("company"))
.where(SALES_ORDER.customer == filters.get("customer"))
.groupby(PICK_LIST.name)
)
if filters.get("sales_order"):
query = query.where(PICK_LIST_ITEM.sales_order == filters.get("sales_order"))
if txt:
meta = frappe.get_meta("Pick List")
search_fields = meta.get_search_fields()
txt = f"%{txt}%"
txt_condition = PICK_LIST[search_fields[-1]].like(txt)
for field in search_fields[:-1]:
txt_condition |= PICK_LIST[field].like(txt)
query = query.where(txt_condition)
return query.run(as_dict=True)

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