Compare commits

...

248 Commits

Author SHA1 Message Date
Frappe PR Bot
6931db98f1 chore(release): Bumped to Version 14.44.1
## [14.44.1](https://github.com/frappe/erpnext/compare/v14.44.0...v14.44.1) (2023-10-19)

### Bug Fixes

* billed_qty to show a sum of all invoiced qty from the purchase order item. (backport [#37539](https://github.com/frappe/erpnext/issues/37539)) ([#37558](https://github.com/frappe/erpnext/issues/37558)) ([ac7d6d6](ac7d6d6d59))
* consider received qty while creating SO -> MR (backport [#37414](https://github.com/frappe/erpnext/issues/37414)) ([#37514](https://github.com/frappe/erpnext/issues/37514)) ([1b94510](1b94510f08))
* don't set finance books if gross_purchase_amount is not set (backport [#37480](https://github.com/frappe/erpnext/issues/37480)) ([#37482](https://github.com/frappe/erpnext/issues/37482)) ([0590f21](0590f21814))
* e-commerce permissions for address ([#37554](https://github.com/frappe/erpnext/issues/37554)) ([022f85d](022f85dd08))
* german tranlations of "Is Return" ([f9b2355](f9b2355066))
* GL Entries not getting created for PR Return (backport [#37513](https://github.com/frappe/erpnext/issues/37513)) ([#37516](https://github.com/frappe/erpnext/issues/37516)) ([c32258e](c32258e4b6))
* **gp:** wrong `allocated_amount` on multi sales person invoice ([d266423](d266423011))
* Incorrect vat amount in KSA VAT report ([44f7de0](44f7de0f31))
* inflated total amt in TDS report using back calculation ([78e22af](78e22af3ca))
* Issues related to RFQ and Supplier Quotation on Portal (backport [#37565](https://github.com/frappe/erpnext/issues/37565)) ([#37577](https://github.com/frappe/erpnext/issues/37577)) ([e1504ef](e1504efd40))
* keep customer/supplier website role by default ([76ef61c](76ef61c24f))
* keyerror on gl and pl comparision report ([6f143d3](6f143d35aa))
* payment entry count on supplier dashboard (backport [#37571](https://github.com/frappe/erpnext/issues/37571)) ([#37575](https://github.com/frappe/erpnext/issues/37575)) ([95abd79](95abd7908f))
* same Serial No get mapped while creating SO -> DN ([#37527](https://github.com/frappe/erpnext/issues/37527)) ([5025850](5025850258))
* serial and batch no get removed on save of return DN ([#37476](https://github.com/frappe/erpnext/issues/37476)) ([f1814a1](f1814a1a2a))
* Stock Reconciliation Insufficient Stock Error ([#37494](https://github.com/frappe/erpnext/issues/37494)) ([9406ddb](9406ddbff0))
* **test:** project test case (backport [#37541](https://github.com/frappe/erpnext/issues/37541)) ([#37543](https://github.com/frappe/erpnext/issues/37543)) ([e23710b](e23710bf00))
* use `flt` to ignore TypeError ([#37481](https://github.com/frappe/erpnext/issues/37481)) ([d2b22db](d2b22db500))

### Performance Improvements

* index `dn_detail` in `Delivery Note Item` (backport [#37528](https://github.com/frappe/erpnext/issues/37528)) ([#37530](https://github.com/frappe/erpnext/issues/37530)) ([001c230](001c230688))
2023-10-19 11:36:28 +00:00
Deepesh Garg
fdb2e94b5c Merge pull request #37545 from frappe/version-14-hotfix
chore: release v14
2023-10-19 17:04:34 +05:30
mergify[bot]
e1504efd40 fix: Issues related to RFQ and Supplier Quotation on Portal (backport #37565) (#37577)
* fix: Issues related to RFQ and Supplier Quotation on Portal (#37565)

fix: RFQ and Supplier Quotation for Portal
(cherry picked from commit 2851a41310)

* chore: removed backport changes

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-10-19 13:50:55 +05:30
mergify[bot]
95abd7908f fix: payment entry count on supplier dashboard (backport #37571) (#37575)
fix: payment entry count on supplier dashboard (#37571)

(cherry picked from commit 10311ff114)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-10-19 13:38:24 +05:30
mergify[bot]
022f85dd08 fix: e-commerce permissions for address (#37554)
* fix: E-commerce permissions

(cherry picked from commit f4d74990fe)

# Conflicts:
#	erpnext/controllers/selling_controller.py

* chore: conflicts

---------

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-10-18 17:21:46 +05:30
Deepesh Garg
837a6b5f50 Merge pull request #37555 from frappe/mergify/bp/version-14-hotfix/pr-37550
chore: Add accounting dimensions to Sales Order Item table (backport #37550)
2023-10-18 17:09:14 +05:30
ruthra kumar
f6a550faae Merge pull request #37569 from frappe/mergify/bp/version-14-hotfix/pr-37105
refactor: move `unreconcile` button into a drop down (backport #37105)
2023-10-18 16:31:52 +05:30
ruthra kumar
54f672e144 refactor: add unreconcile btn to purchase invoice
(cherry picked from commit 94ce43b0d5)
2023-10-18 10:58:20 +00:00
ruthra kumar
e8d082560a refactor: move unreconcile btn inside a drop down
(cherry picked from commit f2b0ac6868)
2023-10-18 10:58:19 +00:00
mergify[bot]
ac7d6d6d59 fix: billed_qty to show a sum of all invoiced qty from the purchase order item. (backport #37539) (#37558)
fix: billed_qty to show a sum of all invoiced qty from the purchase order item.

(cherry picked from commit 8a72f4f58a)

Co-authored-by: HarryPaulo <paulo_fabris@hotmail.com>
2023-10-18 06:00:29 +00:00
ruthra kumar
e2e89492e0 Merge pull request #37556 from frappe/mergify/bp/version-14-hotfix/pr-37549
refactor: use account in key while grouping voucher in ar/ap report (backport #37549)
2023-10-18 09:41:24 +05:30
ruthra kumar
760eab961d test: report output if party is missing
(cherry picked from commit 244cec64b2)
2023-10-18 03:40:35 +00:00
ruthra kumar
3499089323 refactor: use account in key while grouping voucher in ar/ap report
(cherry picked from commit 601ab4567e)
2023-10-18 03:40:34 +00:00
Deepesh Garg
7db6988364 chore: resolve conflicts 2023-10-18 09:00:49 +05:30
Deepesh Garg
bfa93cd3f6 chore: Add accounting dimensions to Sales Order Item table
(cherry picked from commit e31db18912)

# Conflicts:
#	erpnext/patches.txt
2023-10-17 17:25:57 +00:00
mergify[bot]
e23710bf00 fix(test): project test case (backport #37541) (#37543)
fix(test): project test case

(cherry picked from commit fd6aee15e6)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-10-17 15:03:16 +05:30
s-aga-r
5025850258 fix: same Serial No get mapped while creating SO -> DN (#37527)
* fix: same Serial No get mapped while creating SO -> DN

* test: add test case for DN with repetitive serial item
2023-10-17 12:20:23 +05:30
ruthra kumar
473610506c Merge pull request #37540 from frappe/mergify/bp/version-14-hotfix/pr-37330
refactor: checkbox to toggle exchange rate inheritence in PO->PI (backport #37330)
2023-10-17 10:55:27 +05:30
ruthra kumar
71cb7d37ee refactor: checkbox to toggle exchange rate inheritence in PO->PI
(cherry picked from commit 08315522bb)
2023-10-17 04:16:50 +00:00
Deepesh Garg
5b1016c17d Merge pull request #37524 from deepeshgarg007/ksa_vat
fix: Incorrect vat amount in KSA VAT report
2023-10-16 18:41:25 +05:30
Ankush Menat
d598dad50e Merge pull request #37533 from frappe/mergify/bp/version-14-hotfix/pr-37532
fix: keep customer/supplier website role by default (backport #37532)
2023-10-16 17:33:44 +05:30
Ankush Menat
76ef61c24f fix: keep customer/supplier website role by default
(cherry picked from commit d2096cfdb7)
2023-10-16 12:01:17 +00:00
mergify[bot]
001c230688 perf: index dn_detail in Delivery Note Item (backport #37528) (#37530)
* perf: index `dn_detail` in `Delivery Note Item`

(cherry picked from commit 5b4528e614)

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

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-10-16 16:48:55 +05:30
Deepesh Garg
44f7de0f31 fix: Incorrect vat amount in KSA VAT report 2023-10-16 14:37:16 +05:30
mergify[bot]
c32258e4b6 fix: GL Entries not getting created for PR Return (backport #37513) (#37516)
* fix: GL Entries not getting created for PR Return

(cherry picked from commit 46add06a29)

* test: add test case for PR return with zero rate

(cherry picked from commit 253d4782c6)

# Conflicts:
#	erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-10-16 01:24:10 +05:30
mergify[bot]
1b94510f08 fix: consider received qty while creating SO -> MR (backport #37414) (#37514)
fix: consider received qty while creating SO -> MR

(cherry picked from commit b2cee396ac)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-10-16 01:23:13 +05:30
ruthra kumar
65b7fb1293 Merge pull request #37511 from frappe/mergify/bp/version-14-hotfix/pr-37319
test: use fixtures for sales and purchase invoice (backport #37319)
2023-10-15 12:44:13 +05:30
ruthra kumar
d1f6d62d72 chore: fix flaky test case 2023-10-15 12:19:34 +05:30
ruthra kumar
77e7a6cde7 Merge pull request #37512 from ruthra-kumar/back_calculate_tds
fix: inflated total amt in TDS report using back calculation
2023-10-15 11:54:25 +05:30
ruthra kumar
78e22af3ca fix: inflated total amt in TDS report using back calculation 2023-10-15 11:23:52 +05:30
ruthra kumar
7f903532f3 chore: resovle conflicts 2023-10-15 10:44:37 +05:30
ruthra kumar
8d1eac89e3 refactor(test): make sure TDS Payable is available for testing
(cherry picked from commit fbabf4ac2e)
2023-10-15 04:47:43 +00:00
ruthra kumar
d78316869b refactor(test): make use of @change_settings in PI test cases
(cherry picked from commit 0207d6e7c9)
2023-10-15 04:47:43 +00:00
ruthra kumar
33becb7b32 refactor(test): use test fixture in purchase invoice
(cherry picked from commit a2e064d214)
2023-10-15 04:47:43 +00:00
ruthra kumar
b97fdbe6fc refactor(test): use test fixture in subscription
(cherry picked from commit 3bdf4f628c)

# Conflicts:
#	erpnext/accounts/doctype/subscription/test_subscription.py
2023-10-15 04:47:42 +00:00
ruthra kumar
5699a8daa2 refactor(test): use @change_settings to fix failing test cases
(cherry picked from commit de9baef84a)
2023-10-15 04:47:42 +00:00
ruthra kumar
91a5bd8615 refactor(test): fix broken test cases in Sales Invoice
(cherry picked from commit 8ebe5733ac)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
2023-10-15 04:47:42 +00:00
ruthra kumar
485cb7dd28 refactor(test): use @change_settings in sales invoice
(cherry picked from commit 58065f31b1)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
2023-10-15 04:47:42 +00:00
ruthra kumar
9d6b434d1f refactor(test): unset accounts frozen date
(cherry picked from commit fc50b174eb)
2023-10-15 04:47:41 +00:00
ruthra kumar
405d1528c3 test: use fixtures for sales and purchase invoice
(cherry picked from commit c322e5f381)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
2023-10-15 04:47:41 +00:00
s-aga-r
f1814a1a2a fix: serial and batch no get removed on save of return DN (#37476)
* fix: serial and batch no get removed on save of return DN

* test: add test case for DN return with product bundle
2023-10-15 09:57:39 +05:30
s-aga-r
9406ddbff0 fix: Stock Reconciliation Insufficient Stock Error (#37494)
* fix: Stock Reconciliation Insufficient Stock Error

* fix: linter

* test: add test case for Stock Reco Batch Item
2023-10-14 16:53:29 +05:30
ruthra kumar
bae6c5bf5f Merge pull request #37500 from frappe/mergify/bp/version-14-hotfix/pr-37435
fix(gp): wrong `allocated_amount` when grouped by Sales Person (backport #37435)
2023-10-14 12:54:33 +05:30
ruthra kumar
cf9acd1ff6 Merge pull request #37501 from frappe/mergify/bp/version-14-hotfix/pr-37495
fix: keyerror on gl and pl comparision report (backport #37495)
2023-10-14 12:54:09 +05:30
ruthra kumar
6f143d35aa fix: keyerror on gl and pl comparision report
(cherry picked from commit ad00df0af6)
2023-10-14 06:46:18 +00:00
Dany Robert
d266423011 fix(gp): wrong allocated_amount on multi sales person invoice
(cherry picked from commit bda82bf1e9)
2023-10-14 06:43:15 +00:00
s-aga-r
d2b22db500 fix: use flt to ignore TypeError (#37481) 2023-10-13 10:22:20 +05:30
ruthra kumar
c43d0b81e3 Merge pull request #37488 from frappe/mergify/bp/version-14-hotfix/pr-37484
refactor(patch): ignore links on closing balance patch (backport #37484)
2023-10-13 09:59:08 +05:30
ruthra kumar
cf0ab51348 refactor(patch): ignore links on closing balance patch
(cherry picked from commit 17ca8756a7)
2023-10-13 03:50:02 +00:00
mergify[bot]
0590f21814 fix: don't set finance books if gross_purchase_amount is not set (backport #37480) (#37482)
fix: don't set finance books if gross_purchase_amount is not set (#37480)

(cherry picked from commit 18e3a8907a)

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-10-12 19:44:36 +05:30
Sagar Vora
bbdf26c82e Merge pull request #37468 from frappe/mergify/bp/version-14-hotfix/pr-37418
fix: german tranlations of "Is Return" (backport #37418)
2023-10-12 13:05:49 +05:30
ruthra kumar
020d2e2ca6 Merge pull request #37464 from frappe/mergify/bp/version-14-hotfix/pr-37436
refactor: for non-repost fields, don't validate (backport #37436)
2023-10-12 10:51:42 +05:30
ruthra kumar
f434548204 Merge pull request #37465 from frappe/mergify/bp/version-14-hotfix/pr-37459
refactor: add validation for Advances in SI/PI (backport #37459)
2023-10-12 10:51:18 +05:30
barredterra
f9b2355066 fix: german tranlations of "Is Return"
(cherry picked from commit 38ca164662)
2023-10-12 02:21:05 +00:00
Frappe PR Bot
2815952a03 chore(release): Bumped to Version 14.44.0
# [14.44.0](https://github.com/frappe/erpnext/compare/v14.43.1...v14.44.0) (2023-10-12)

### Bug Fixes

* added validation for the batch on stock reco ([#37174](https://github.com/frappe/erpnext/issues/37174)) ([4c337a6](4c337a6f44))
* ageing summary in AR ([15d2024](15d2024b8e))
* allocate amt for payment term invoices ([b22ac13](b22ac137f5))
* call validate before setting repost flag ([bec3e8e](bec3e8ed96))
* do not run bg job for single doc ([4123e7b](4123e7b244))
* **Employee:** enable `no_copy` for `relieving_date` (backport [#37344](https://github.com/frappe/erpnext/issues/37344)) ([#37358](https://github.com/frappe/erpnext/issues/37358)) ([2b38b78](2b38b780ba))
* exception on exporting errored rows ([e58b3b1](e58b3b11e9))
* fetch company details for Lead based quotation ([c1d40a6](c1d40a6bfa))
* fetch dependent task subject and project (backport [#37401](https://github.com/frappe/erpnext/issues/37401)) ([#37421](https://github.com/frappe/erpnext/issues/37421)) ([0aad942](0aad942312))
* ignore cancelled gle in voucher-wise balance report ([#36417](https://github.com/frappe/erpnext/issues/36417)) ([ee1255a](ee1255a716))
* incorrect status of the returned purchase receipt ([#37300](https://github.com/frappe/erpnext/issues/37300)) ([63f4573](63f45739e0))
* linting issues ([6c8a65e](6c8a65e03b))
* negative valuation rate in PR return ([#37424](https://github.com/frappe/erpnext/issues/37424)) ([26ad688](26ad688584))
* payment request rounding in multi-currency and on status update ([eed5863](eed58634ba))
* production plan reserved qty incorrect calculation (backport [#37400](https://github.com/frappe/erpnext/issues/37400)) ([#37458](https://github.com/frappe/erpnext/issues/37458)) ([573b159](573b159541))
* split inv allocated amt on server side ([06b0477](06b04770fc))
* typo in doctype name and qb ([606c99e](606c99e57c))
* **ux:** allow MR to Stop until fully received (backport [#37452](https://github.com/frappe/erpnext/issues/37452)) ([#37456](https://github.com/frappe/erpnext/issues/37456)) ([fb0b426](fb0b426fe4))
* validation for si ([3dc68e3](3dc68e3b00))

### Features

* add repost btn in invoice ([cde848d](cde848dc7f))
* allow on submit fields ([f5245f6](f5245f6b3f))
* allow repost for pi ([2d13dda](2d13dda49c))
* composite WIP asset ([#37352](https://github.com/frappe/erpnext/issues/37352)) ([0ecd7d2](0ecd7d2bf5))
* disable currency exchange api. ([#33593](https://github.com/frappe/erpnext/issues/33593)) ([1ca0516](1ca0516fe5))
* filter on voucher no ([cb35218](cb35218eec))
* introduce unreconcile doctype ([ae8355c](ae8355c953))
* UI for unreconcile ([9531a45](9531a45b94))
* unreconcile support for journal entry ([cd2d335](cd2d335256))
* validate negative stock for inventory dimension ([#37373](https://github.com/frappe/erpnext/issues/37373)) ([1480aca](1480acabb0))
2023-10-12 02:15:37 +00:00
Deepesh Garg
33f4fae8cd Merge pull request #37430 from frappe/version-14-hotfix
chore: release v14
2023-10-12 07:43:46 +05:30
ruthra kumar
d37a1811db refactor: add validation for Advances in SI/PI
(cherry picked from commit 0cdd6435a5)
2023-10-11 14:34:38 +00:00
ruthra kumar
8dd26949b7 refactor: for non-repost fields, don't validate
(cherry picked from commit c1782c5015)
2023-10-11 14:34:29 +00:00
s-aga-r
26ad688584 fix: negative valuation rate in PR return (#37424)
* fix: negative valuation rate in PR return

* test: add test case for PR return
2023-10-11 18:44:32 +05:30
Frappe PR Bot
48ceead9d0 chore(release): Bumped to Version 14.43.1
## [14.43.1](https://github.com/frappe/erpnext/compare/v14.43.0...v14.43.1) (2023-10-11)

### Bug Fixes

* fetch company details for Lead based quotation ([e4ed1d6](e4ed1d684d))
2023-10-11 11:14:16 +00:00
ruthra kumar
9dd33739f9 Merge pull request #37460 from frappe/mergify/bp/version-14/pr-37371
fix: fetch company details for Lead based quotation (backport #37370) (backport #37371)
2023-10-11 16:42:42 +05:30
ruthra kumar
e4ed1d684d fix: fetch company details for Lead based quotation
(cherry picked from commit f388864fd5)
(cherry picked from commit c1d40a6bfa)
2023-10-11 10:46:00 +00:00
mergify[bot]
573b159541 fix: production plan reserved qty incorrect calculation (backport #37400) (#37458)
fix: production plan reserved qty incorrect calculation (#37400)

(cherry picked from commit f3238f9105)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-10-11 14:41:32 +05:30
mergify[bot]
fb0b426fe4 fix(ux): allow MR to Stop until fully received (backport #37452) (#37456)
fix(ux): allow MR to Stop until fully received

(cherry picked from commit 0d7a0f393d)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-10-11 13:51:29 +05:30
ruthra kumar
63ff1f1eaa Merge pull request #37399 from frappe/mergify/bp/version-14-hotfix/pr-37194
feat: editable purchase invoice  (backport #37194)
2023-10-10 21:05:56 +05:30
ruthra kumar
4d79a4e7b3 Merge pull request #37420 from frappe/mergify/bp/version-14-hotfix/pr-37204
fix: allocate payment amount for split invoices in PE (backport #37204)
2023-10-10 21:04:26 +05:30
ruthra kumar
97f9656460 Merge pull request #37419 from frappe/mergify/bp/version-14-hotfix/pr-37123
fix: payment request rounding in multi-currency and on status update (backport #37123)
2023-10-10 21:03:52 +05:30
ruthra kumar
14ddcd6c24 Merge pull request #37427 from frappe/mergify/bp/version-14-hotfix/pr-36417
fix: ignore cancelled gle in voucher-wise balance report (backport #36417)
2023-10-10 21:03:21 +05:30
Deepesh Garg
9689c1fcec Merge branch 'version-14' into version-14-hotfix 2023-10-10 20:19:26 +05:30
Gursheen Kaur Anand
ee1255a716 fix: ignore cancelled gle in voucher-wise balance report (#36417)
fix: ignore cancelled gle
(cherry picked from commit 1ddfaa7605)
2023-10-10 08:09:32 +00:00
ruthra kumar
b0ac097327 chore: resolve conflicts 2023-10-10 11:31:19 +05:30
ruthra kumar
5d97a69e32 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-37194 2023-10-10 11:10:09 +05:30
mergify[bot]
0aad942312 fix: fetch dependent task subject and project (backport #37401) (#37421)
fix: fetch dependent task subject and project (#37401)

(cherry picked from commit 78eaf5d035)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-10-10 11:06:33 +05:30
ruthra kumar
debfbc4761 refactor: remove references in repost doctypes upon parent doc delet
(cherry picked from commit ed7f67b1a8)
2023-10-10 11:01:05 +05:30
Gursheen Anand
3dc68e3b00 fix: validation for si
(cherry picked from commit 61c6ebbb95)
2023-10-10 10:58:19 +05:30
Gursheen Anand
bec3e8ed96 fix: call validate before setting repost flag
(cherry picked from commit 8ef0d88708)
2023-10-10 10:58:19 +05:30
Gursheen Anand
4123e7b244 fix: do not run bg job for single doc
(cherry picked from commit 1856050ef9)
2023-10-10 10:58:19 +05:30
Gursheen Anand
c9bcf79e83 refactor: remove repeated validation for voucher
(cherry picked from commit a856091ff4)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.py
2023-10-10 10:58:19 +05:30
Gursheen Anand
677525b2cf refactor: use repost accounting legder
(cherry picked from commit 7ebf083683)
2023-10-10 10:58:19 +05:30
Gursheen Anand
8c83bbc096 refactor: remove unused method
(cherry picked from commit ba7212c98b)
2023-10-10 10:58:19 +05:30
Gursheen Anand
a512d27dbb test: reposted acc entries for pi
(cherry picked from commit c66c438575)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
2023-10-10 10:58:19 +05:30
Gursheen Anand
6c8a65e03b fix: linting issues
(cherry picked from commit c88f6d1fa7)
2023-10-10 10:58:19 +05:30
Gursheen Anand
2d13dda49c feat: allow repost for pi
(cherry picked from commit 23470bf52d)
2023-10-10 10:58:19 +05:30
Gursheen Anand
cde848dc7f feat: add repost btn in invoice
(cherry picked from commit e77814fbc0)
2023-10-10 10:58:19 +05:30
Gursheen Anand
79e414cb97 refactor: move reposting logic to common controller
(cherry picked from commit 68effd93bd)

# Conflicts:
#	erpnext/accounts/doctype/sales_invoice/sales_invoice.py
2023-10-10 10:58:19 +05:30
Gursheen Anand
f5245f6b3f feat: allow on submit fields
(cherry picked from commit e922ec60eb)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
2023-10-10 10:58:19 +05:30
ruthra kumar
dc804f216d Merge pull request #37402 from frappe/mergify/bp/version-14-hotfix/pr-36879
feat: Unreconcile Payments (backport #36879)
2023-10-10 10:39:54 +05:30
Gursheen Anand
bc0db696c9 chore: remove unused variable
(cherry picked from commit 545f2ccdf1)
2023-10-10 04:42:15 +00:00
Gursheen Anand
06b04770fc fix: split inv allocated amt on server side
(cherry picked from commit b3aa201eb5)
2023-10-10 04:42:15 +00:00
Gursheen Anand
b22ac137f5 fix: allocate amt for payment term invoices
(cherry picked from commit ac28a5b372)
2023-10-10 04:42:15 +00:00
David Arnold
eed58634ba fix: payment request rounding in multi-currency and on status update
(cherry picked from commit 6e1ad4c5bd)
2023-10-10 04:38:31 +00:00
mergify[bot]
24852e46c1 chore: rewrite query using query builder (backport #37310) (#37415)
* chore: rewrite query using query builder

(cherry picked from commit 25718f5cc7)

* chore: fix shopping cart tests

(cherry picked from commit fb51cae88b)

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-10-09 20:56:43 +05:30
Deepesh Garg
50521d4efe Merge pull request #37322 from frappe/mergify/bp/version-14-hotfix/pr-37119
chore: add regional support for getting payment entries (#37119)
2023-10-09 19:28:07 +05:30
ruthra kumar
8fc705ea6a chore: resolve conflicts 2023-10-09 15:17:47 +05:30
ruthra kumar
63274527d2 test: multi currency invoice unreconciliation
exchange gain/loss associated with the unreconcile invoice should be
cancelled as well

(cherry picked from commit d398775715)
2023-10-09 06:15:09 +00:00
ruthra kumar
aba51ee352 refactor(test): more modularization
(cherry picked from commit 5c09fdf941)
2023-10-09 06:15:09 +00:00
ruthra kumar
4bd83b5058 refactor: cancel gain/loss JE on multi currency transactions
(cherry picked from commit 1d93d66c30)
2023-10-09 06:15:09 +00:00
ruthra kumar
75d3093aea refactor: only cancel specific gain/loss je
(cherry picked from commit 5dbcf7d2b9)
2023-10-09 06:15:09 +00:00
ruthra kumar
669d692844 refactor: display allocated amount in account currency with symbol
(cherry picked from commit 6fd1c1bca2)
2023-10-09 06:15:09 +00:00
ruthra kumar
606c99e57c fix: typo in doctype name and qb
(cherry picked from commit 9a1588f1cc)
2023-10-09 06:15:08 +00:00
ruthra kumar
cf308912a1 test: more granular unreconciliation
(cherry picked from commit 67980188a7)
2023-10-09 06:15:08 +00:00
ruthra kumar
f5718390b7 refactor: unlink individual vouchers from payments
(cherry picked from commit 9b6eac23b6)
2023-10-09 06:15:08 +00:00
ruthra kumar
8954bd7759 chore: type info
(cherry picked from commit b4dc2bdf28)
2023-10-09 06:15:08 +00:00
ruthra kumar
3cbaea389b refactor: convert raw sql to query_builder
(cherry picked from commit 0130aea2aa)
2023-10-09 06:15:07 +00:00
ruthra kumar
335cb5fd28 refactor: single fetch and unlinking logic for JE and PE
(cherry picked from commit de910ab152)
2023-10-09 06:15:07 +00:00
ruthra kumar
cd2d335256 feat: unreconcile support for journal entry
(cherry picked from commit 285963acdb)
2023-10-09 06:15:07 +00:00
ruthra kumar
1a69db0f80 refactor: modularisation and group by voucher_no
(cherry picked from commit cce96669f0)
2023-10-09 06:15:07 +00:00
ruthra kumar
84e4a2509c chore: rename and add trigger in journal entry
(cherry picked from commit 0ccb6d8242)
2023-10-09 06:15:06 +00:00
ruthra kumar
f4e1959cc7 chore: code cleanup
(cherry picked from commit 69683776a5)
2023-10-09 06:15:06 +00:00
ruthra kumar
7651ecbc2b chore: fetch logic for payment entry
(cherry picked from commit 1981f3837a)
2023-10-09 06:15:06 +00:00
ruthra kumar
e464f5e419 chore: move functions to a separate file in utils
(cherry picked from commit 25fe752185)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/payment_entry.js
#	erpnext/public/js/erpnext.bundle.js
2023-10-09 06:15:06 +00:00
ruthra kumar
1e93d0bcc4 chore: move dialog building function to utils.js file
(cherry picked from commit 5981c7e0ad)
2023-10-09 06:15:05 +00:00
ruthra kumar
b886589657 refactor: add UI elements
(cherry picked from commit 58dc0e52e1)
2023-10-09 06:15:05 +00:00
ruthra kumar
3a670264b2 chore: delete unreoncile doc upon parent doc deletion
(cherry picked from commit 6bbe47c671)
2023-10-09 06:15:05 +00:00
ruthra kumar
2fd500ce26 chore: track changes
(cherry picked from commit 489a545bbb)
2023-10-09 06:15:04 +00:00
ruthra kumar
9422422dcc refactor: remove references using framework
(cherry picked from commit 42df0d3d67)
2023-10-09 06:15:04 +00:00
ruthra kumar
37fc82cd11 chore: delete references upon parent deletion
(cherry picked from commit fbdfb8151c)
2023-10-09 06:15:04 +00:00
ruthra kumar
cb35218eec feat: filter on voucher no
(cherry picked from commit 41eb2c9f5a)
2023-10-09 06:15:04 +00:00
ruthra kumar
9531a45b94 feat: UI for unreconcile
(cherry picked from commit fc6be5bfb9)
2023-10-09 06:15:03 +00:00
ruthra kumar
fb41f5f88c test: basic unreconcile function
(cherry picked from commit 0faffaa8db)
2023-10-09 06:15:03 +00:00
ruthra kumar
b9647ac0a4 refactor: adding 'Get Allocations' button
(cherry picked from commit 5114a9580d)
2023-10-09 06:15:03 +00:00
ruthra kumar
77fa0f68df chore: working state on barebones functions
(cherry picked from commit e48a90efe6)
2023-10-09 06:15:03 +00:00
ruthra kumar
ae8355c953 feat: introduce unreconcile doctype
(cherry picked from commit dc71623295)
2023-10-09 06:15:02 +00:00
ruthra kumar
c42ef922d2 Merge pull request #37396 from frappe/mergify/bp/version-14-hotfix/pr-37395
fix: exception on exporting errored rows (backport #37395)
2023-10-08 18:39:49 +05:30
ruthra kumar
e58b3b11e9 fix: exception on exporting errored rows
(cherry picked from commit d3c6000904)
2023-10-08 12:52:16 +00:00
ruthra kumar
8f0e10bfbe Merge pull request #37388 from frappe/mergify/bp/version-14-hotfix/pr-37289
fix: ageing summary in SOA AR (backport #37289)
2023-10-08 12:25:51 +05:30
Gursheen Kaur Anand
77d719af6e chore: linting issues 2023-10-07 15:40:27 +05:30
Gursheen Kaur Anand
3f59518d01 chore: resolve conflicts 2023-10-07 15:21:50 +05:30
Gursheen Anand
24b1100c8f test: process soa for gl and ar
(cherry picked from commit 644e25e587)
2023-10-07 09:40:32 +00:00
Gursheen Anand
c29eab12df refactor: separate function for statement dict
(cherry picked from commit 67f878ff8c)

# Conflicts:
#	erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
2023-10-07 09:40:32 +00:00
Gursheen Anand
15d2024b8e fix: ageing summary in AR
(cherry picked from commit d9eb44e62d)
2023-10-07 09:40:32 +00:00
rohitwaghchaure
1480acabb0 feat: validate negative stock for inventory dimension (#37373)
* feat: validate negative stock for inventory dimension

* test: test case for validate negative stock for inv dimension
2023-10-06 17:55:32 +05:30
rohitwaghchaure
4c337a6f44 fix: added validation for the batch on stock reco (#37174) 2023-10-06 13:06:13 +05:30
rohitwaghchaure
63f45739e0 fix: incorrect status of the returned purchase receipt (#37300) 2023-10-06 11:58:15 +05:30
ruthra kumar
55a9a8fd51 Merge pull request #37371 from frappe/mergify/bp/version-14-hotfix/pr-37370
fix: fetch company details for Lead based quotation (backport #37370)
2023-10-05 14:25:14 +05:30
ruthra kumar
c1d40a6bfa fix: fetch company details for Lead based quotation
(cherry picked from commit f388864fd5)
2023-10-05 08:21:26 +00:00
ruthra kumar
bbc13c719e Merge pull request #37367 from frappe/mergify/bp/version-14-hotfix/pr-37359
refactor: add `access_key` field to facilitate use of exchangerate.host provider (backport #37359)
2023-10-05 12:57:41 +05:30
ruthra kumar
3b49079ad7 Merge branch 'version-14-hotfix' into mergify/bp/version-14-hotfix/pr-37359 2023-10-05 12:17:07 +05:30
ruthra kumar
6fad2bad11 Merge pull request #37369 from frappe/mergify/bp/version-14-hotfix/pr-33593
feat: disable currency exchange api. (backport #33593)
2023-10-05 10:25:40 +05:30
Devin Slauenwhite
1ca0516fe5 feat: disable currency exchange api. (#33593)
(cherry picked from commit 179a31ed5e)
2023-10-05 04:03:15 +00:00
ruthra kumar
04b8527ba8 chore: refactor test case for exchangerate.host provider
(cherry picked from commit c8e3dc6c4c)
2023-10-05 03:50:28 +00:00
ruthra kumar
98a9007e9f refactor: introduce access_key field
(cherry picked from commit 81591a34c2)

# Conflicts:
#	erpnext/accounts/doctype/currency_exchange_settings/currency_exchange_settings.json
2023-10-05 03:50:28 +00:00
ruthra kumar
64a50cfabf Merge pull request #37366 from frappe/mergify/bp/version-14-hotfix/pr-37294
refactor: block Payment Entry as ref in Journals from UI (backport #37294)
2023-10-05 09:20:20 +05:30
ruthra kumar
587a965bdf refactor: block Payment Entry as ref in JE from UI
(cherry picked from commit d391e81505)
2023-10-05 03:25:04 +00:00
ruthra kumar
52c0900576 Merge pull request #37363 from frappe/mergify/copy/version-14-hotfix/pr-37362
test: fixing test_capitalization_with_wip_composite_asset (copy #37362)
2023-10-05 08:38:20 +05:30
anandbaburajan
67a43c353c test: fixing test_capitalization_with_wip_composite_asset
(cherry picked from commit 9468513d7c)
2023-10-04 16:44:31 +00:00
Frappe PR Bot
cf9fc552f0 chore(release): Bumped to Version 14.43.0
# [14.43.0](https://github.com/frappe/erpnext/compare/v14.42.0...v14.43.0) (2023-10-04)

### Features

* composite WIP asset (backport [#37352](https://github.com/frappe/erpnext/issues/37352)) ([#37360](https://github.com/frappe/erpnext/issues/37360)) ([6947686](6947686141))
2023-10-04 14:18:28 +00:00
mergify[bot]
6947686141 feat: composite WIP asset (backport #37352) (#37360)
feat: composite WIP asset (#37352)

feat: wip composite asset
(cherry picked from commit 0ecd7d2bf5)

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-10-04 19:46:30 +05:30
mergify[bot]
2b38b780ba fix(Employee): enable no_copy for relieving_date (backport #37344) (#37358)
Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
Co-authored-by: Jignesh (GreyCube Technologies) <jignesh@greycube.in>
fix(Employee): enable `no_copy` for `relieving_date` (#37344)
2023-10-04 14:29:45 +05:30
Anand Baburajan
0ecd7d2bf5 feat: composite WIP asset (#37352)
feat: wip composite asset
2023-10-04 10:29:14 +05:30
Frappe PR Bot
9f1b9320e9 chore(release): Bumped to Version 14.42.0
# [14.42.0](https://github.com/frappe/erpnext/compare/v14.41.2...v14.42.0) (2023-10-04)

### Bug Fixes

* add only float row values for total ([020aedb](020aedb8b0))
* currency symbol in the Supplier Quotation Comparison report ([#37337](https://github.com/frappe/erpnext/issues/37337)) ([82e8606](82e8606b3c))
* Description field for the 'Ignore Available Stock' ([#37293](https://github.com/frappe/erpnext/issues/37293)) ([7f1483a](7f1483ad70))
* do not consider submitted Work Orders in the Production Plan Res… ([#37343](https://github.com/frappe/erpnext/issues/37343)) ([c3aeb2d](c3aeb2dec5))
* ignore user permissions for `Source Warehouse` (backport [#37313](https://github.com/frappe/erpnext/issues/37313)) ([#37314](https://github.com/frappe/erpnext/issues/37314)) ([04f0dfb](04f0dfb691))
* incorrect qty for material request in Production Plan ([#37270](https://github.com/frappe/erpnext/issues/37270)) ([8fe4a4d](8fe4a4d3aa))
* Not unique table/alias: 'tabTask' (backport [#37285](https://github.com/frappe/erpnext/issues/37285)) ([#37298](https://github.com/frappe/erpnext/issues/37298)) ([95e0bf5](95e0bf5e0e))
* party format in test ([28756bf](28756bf7b6))
* PCV posting issues ([#37029](https://github.com/frappe/erpnext/issues/37029)) ([92eabe3](92eabe3cf5))
* process soa filter for multiselect ([4962b67](4962b67358))
* query for multiselect filter ([6d7aa2a](6d7aa2ae94))
* set route filter values for AP ([49f0f1c](49f0f1ca09))
* set route filter values for AR ([2b30727](2b30727fdc))
* summary report filters ([403ff69](403ff697e9))
* trial balance report freezes when adding filters (backport [#37264](https://github.com/frappe/erpnext/issues/37264)) ([#37265](https://github.com/frappe/erpnext/issues/37265)) ([6a8146b](6a8146ba8a))
* Use default Cost Center of the Company for additional discount ([#37234](https://github.com/frappe/erpnext/issues/37234)) ([e483b4a](e483b4a78a))
* validation message for valuation rate ([#37301](https://github.com/frappe/erpnext/issues/37301)) ([643bb05](643bb0511c))

### Features

* asset salvage_value_percentage (backport [#37302](https://github.com/frappe/erpnext/issues/37302)) ([#37334](https://github.com/frappe/erpnext/issues/37334)) ([6daea6c](6daea6ccb2))
2023-10-04 02:15:16 +00:00
ruthra kumar
12a7cb21b5 Merge pull request #37339 from frappe/version-14-hotfix
chore: release v14
2023-10-04 07:43:39 +05:30
rohitwaghchaure
643bb0511c fix: validation message for valuation rate (#37301) 2023-10-03 22:56:33 +05:30
rohitwaghchaure
e975a10a75 chore: fix linter issue (#37349) 2023-10-03 22:08:37 +05:30
rohitwaghchaure
c3aeb2dec5 fix: do not consider submitted Work Orders in the Production Plan Res… (#37343)
fix: do not consider submitted Work Orders in the Production Plan Reserve qty
2023-10-03 20:34:10 +05:30
ruthra kumar
fe32787e6e Merge pull request #37346 from frappe/mergify/bp/version-14-hotfix/pr-37304
fix: only float row values for total in AP summary (backport #37304)
2023-10-03 20:19:54 +05:30
ruthra kumar
f3b872a8e2 refactor: use isinstance over type
(cherry picked from commit 67440c38ae)
2023-10-03 14:16:27 +00:00
Gursheen Anand
020aedb8b0 fix: add only float row values for total
(cherry picked from commit 1dab195560)
2023-10-03 14:16:27 +00:00
rohitwaghchaure
82e8606b3c fix: currency symbol in the Supplier Quotation Comparison report (#37337)
fix: currency in the Supplier Quotation Comparison report
2023-10-03 18:53:43 +05:30
mergify[bot]
6daea6ccb2 feat: asset salvage_value_percentage (backport #37302) (#37334)
* feat: asset salvage_value_percentage (#37302)

* feat: asset salvage_value_percentage

* chore: add missing parameter in get_item_details

* chore: change asset depr table colors

(cherry picked from commit fed94845ce)

# Conflicts:
#	erpnext/assets/doctype/asset/asset.js
#	erpnext/assets/doctype/asset_activity/asset_activity.json
#	erpnext/assets/doctype/asset_finance_book/asset_finance_book.json

* chore: resolving conflicts

---------

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-10-03 13:35:10 +05:30
Smit Vora
f7de825e89 chore: add regional support for getting payment entries (#37119)
chore: add regional support for get payment entries
(cherry picked from commit 3e282bfbce)
2023-10-02 10:32:40 +00:00
mergify[bot]
04f0dfb691 fix: ignore user permissions for Source Warehouse (backport #37313) (#37314)
* fix: ignore user permissions for `Source Warehouse` (#37313)

(cherry picked from commit e7f4b7b190)

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

* chore: `conflicts`

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-10-01 21:31:17 +05:30
mergify[bot]
95e0bf5e0e fix: Not unique table/alias: 'tabTask' (backport #37285) (#37298)
fix: Not unique table/alias: 'tabTask' (#37285)

(cherry picked from commit 361e555118)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-29 15:07:10 +05:30
rohitwaghchaure
7f1483ad70 fix: Description field for the 'Ignore Available Stock' (#37293) 2023-09-29 14:35:45 +05:30
Frappe PR Bot
7fd4d3c882 chore(release): Bumped to Version 14.41.2
## [14.41.2](https://github.com/frappe/erpnext/compare/v14.41.1...v14.41.2) (2023-09-29)

### Bug Fixes

* incorrect qty for material request in Production Plan (backport [#37270](https://github.com/frappe/erpnext/issues/37270)) ([#37290](https://github.com/frappe/erpnext/issues/37290)) ([cfb3a9e](cfb3a9eabf))
2023-09-29 06:50:09 +00:00
mergify[bot]
cfb3a9eabf fix: incorrect qty for material request in Production Plan (backport #37270) (#37290)
fix: incorrect qty for material request in Production Plan (#37270)

(cherry picked from commit 8fe4a4d3aa)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-09-29 12:18:35 +05:30
HENRY Florian
21c1c7194b refactor: In Quotation Item, discount_and_margin section should have same collapsible_depends_on as other similar DocType (Sales Order Item,Sales Invoice Item,...) (#37272) 2023-09-29 08:29:18 +05:30
Deepesh Garg
81c4279a30 Merge pull request #37282 from frappe/mergify/bp/version-14-hotfix/pr-37268
fix: AP AR filters from Party link (backport #37268)
2023-09-28 19:34:56 +05:30
ruthra kumar
01b54134ae test: multi select party filter in AR report
(cherry picked from commit 2c7d6aec89)
2023-09-28 06:26:05 +00:00
Gursheen Anand
28756bf7b6 fix: party format in test
(cherry picked from commit 59e8abfd57)
2023-09-28 06:26:05 +00:00
Gursheen Anand
4962b67358 fix: process soa filter for multiselect
(cherry picked from commit 4b28154f5e)
2023-09-28 06:26:05 +00:00
Gursheen Anand
403ff697e9 fix: summary report filters
(cherry picked from commit f7cb68a45f)
2023-09-28 06:26:04 +00:00
Gursheen Anand
6d7aa2ae94 fix: query for multiselect filter
(cherry picked from commit e7239e02d4)
2023-09-28 06:26:04 +00:00
Gursheen Anand
2b30727fdc fix: set route filter values for AR
(cherry picked from commit 9d15124a6a)
2023-09-28 06:26:04 +00:00
Gursheen Anand
49f0f1ca09 fix: set route filter values for AP
(cherry picked from commit 888ed36eed)
2023-09-28 06:26:04 +00:00
rohitwaghchaure
8fe4a4d3aa fix: incorrect qty for material request in Production Plan (#37270) 2023-09-27 20:02:05 +05:30
Frappe PR Bot
1b86e7e2f5 chore(release): Bumped to Version 14.41.1
## [14.41.1](https://github.com/frappe/erpnext/compare/v14.41.0...v14.41.1) (2023-09-27)

### Bug Fixes

* trial balance report freezes when adding filters (backport [#37264](https://github.com/frappe/erpnext/issues/37264)) ([#37266](https://github.com/frappe/erpnext/issues/37266)) ([53817e4](53817e463f))
2023-09-27 07:08:57 +00:00
mergify[bot]
53817e463f fix: trial balance report freezes when adding filters (backport #37264) (#37266)
fix: trial balance report freezes when adding filters (#37264)

fix: Only add onclick if correct data is returned

workaround for https://github.com/frappe/datatable/issues/177

(cherry picked from commit 2dc95e5d59)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-09-27 12:37:05 +05:30
mergify[bot]
6a8146ba8a fix: trial balance report freezes when adding filters (backport #37264) (#37265)
fix: trial balance report freezes when adding filters (#37264)

fix: Only add onclick if correct data is returned

workaround for https://github.com/frappe/datatable/issues/177

(cherry picked from commit 2dc95e5d59)

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-09-27 12:36:58 +05:30
Deepesh Garg
b18678df4d Merge pull request #37262 from frappe/mergify/bp/version-14-hotfix/pr-37029
fix: PCV posting issues (backport #37029)
2023-09-27 12:07:54 +05:30
Deepesh Garg
8d813e3256 chore: resolve conflicts 2023-09-27 11:19:39 +05:30
Deepesh Garg
c6966f4e2d Merge pull request #37263 from frappe/mergify/bp/version-14-hotfix/pr-37234
fix: Use default Cost Center of the Company for additional discount (backport #37234)
2023-09-27 11:18:29 +05:30
Frappe PR Bot
abb6044291 chore(release): Bumped to Version 14.41.0
# [14.41.0](https://github.com/frappe/erpnext/compare/v14.40.0...v14.41.0) (2023-09-27)

### Bug Fixes

* allow to select parent warehouse in the website item (backport [#37047](https://github.com/frappe/erpnext/issues/37047)) ([#37173](https://github.com/frappe/erpnext/issues/37173)) ([56657b6](56657b6122))
* AP & AR summary filters to match AR  (backport [#37248](https://github.com/frappe/erpnext/issues/37248)) ([#37261](https://github.com/frappe/erpnext/issues/37261)) ([1dc58b3](1dc58b3660))
* apply gl report filters ([b4bc44d](b4bc44db4a))
* german translation of Sales and Purchase Invoice ([#37122](https://github.com/frappe/erpnext/issues/37122)) ([8236814](8236814270))
* incorrect `Parent Task` getting set for 2nd to nth child Task (backport [#37230](https://github.com/frappe/erpnext/issues/37230)) ([#37240](https://github.com/frappe/erpnext/issues/37240)) ([279f21d](279f21d1e5))
* labels for `Stock Ledger Invariant Check` report (backport [#37150](https://github.com/frappe/erpnext/issues/37150)) ([#37176](https://github.com/frappe/erpnext/issues/37176)) ([f2bcfb5](f2bcfb5f97))
* **Material Request:** consider project for item details (backport [#37215](https://github.com/frappe/erpnext/issues/37215)) ([#37221](https://github.com/frappe/erpnext/issues/37221)) ([25f800d](25f800d3f5))
* reserved qty for production plan ([#37251](https://github.com/frappe/erpnext/issues/37251)) ([0a0d5b3](0a0d5b3e66))
* serial number decimal issue ([#37242](https://github.com/frappe/erpnext/issues/37242)) ([78ab201](78ab2013e5))
* set customer currency in pos_invoice if exists ([e0da8d2](e0da8d261f))
* Update `advance_paid` in SO/PO after unlinking from advance entry ([1181dcf](1181dcf521))
* validate duplicate serial no on submit in DN ([#37243](https://github.com/frappe/erpnext/issues/37243)) ([70e2093](70e2093941))

### Features

* `Stock Ledger Variance` report (backport [#37165](https://github.com/frappe/erpnext/issues/37165)) ([#37183](https://github.com/frappe/erpnext/issues/37183)) ([02fc67c](02fc67c83c))
* Toggle net values in Trial Balance report ([57c82c1](57c82c1800))
2023-09-27 05:40:33 +00:00
Deepesh Garg
e752027709 Merge pull request #37247 from frappe/version-14-hotfix
chore: release v14
2023-09-27 11:09:00 +05:30
vr-greycube
e483b4a78a fix: Use default Cost Center of the Company for additional discount (#37234)
fix: Set cost center as default company cost center

When Discount Accounting in enabled in Selling Settings, use Company default Cost Center while making GL entries for additional_discount_account

(cherry picked from commit 4ada5a488e)
2023-09-27 05:09:46 +00:00
Deepesh Garg
92eabe3cf5 fix: PCV posting issues (#37029)
* fix: PCV posting issues

* fix: process closing entries separately in a background job

* test: Update tests

* chore: fix broken ci

(cherry picked from commit 8c5fcb8257)

# Conflicts:
#	erpnext/accounts/doctype/payment_request/payment_request.json
2023-09-27 04:53:41 +00:00
mergify[bot]
1dc58b3660 fix: AP & AR summary filters to match AR (backport #37248) (#37261)
* fix: set AR filters after rename

(cherry picked from commit 832d7e7d7b)

* fix: change filters for AR summary

(cherry picked from commit 7d96044d8e)

* fix: set new AP summary filters

(cherry picked from commit 76a5d94f37)

---------

Co-authored-by: Gursheen Anand <gursheen@frappe.io>
2023-09-27 10:22:27 +05:30
rohitwaghchaure
0a0d5b3e66 fix: reserved qty for production plan (#37251) 2023-09-26 17:55:33 +05:30
rohitwaghchaure
78ab2013e5 fix: serial number decimal issue (#37242) 2023-09-26 16:46:19 +05:30
s-aga-r
70e2093941 fix: validate duplicate serial no on submit in DN (#37243) 2023-09-26 16:34:19 +05:30
mergify[bot]
279f21d1e5 fix: incorrect Parent Task getting set for 2nd to nth child Task (backport #37230) (#37240)
fix: incorrect `Parent Task` getting set for 2nd to nth child Task (#37230)

(cherry picked from commit 73fc974950)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-26 13:32:05 +05:30
Deepesh Garg
ba45299e0d Merge pull request #37228 from FHenry/14_warning_depreciation_regional_france
refactor(region): Splitting of France Regional logic from ERPNext
2023-09-25 14:55:53 +05:30
Florian HENRY
c9ba777e3c refactor(region): Splitting of France Regional logic from ERPNext 2023-09-25 10:24:32 +02:00
Florian HENRY
ab8a4f7f7b Merge branch 'version-14-hotfix' of https://github.com/frappe/erpnext into 14_warning_depreciation_regional_france 2023-09-25 10:19:55 +02:00
mergify[bot]
25f800d3f5 fix(Material Request): consider project for item details (backport #37215) (#37221)
fix(Material Request): consider project for item details (#37215)

fix(Material Request): project in item details

(cherry picked from commit 7c4ebe2733)

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-09-24 22:24:56 +05:30
Deepesh Garg
2b8118c5e6 Merge pull request #37218 from frappe/mergify/bp/version-14-hotfix/pr-37211
feat: Toggle net values in Trial Balance report (#37211)
2023-09-24 20:19:06 +05:30
Deepesh Garg
4250a7c4d1 Merge pull request #37217 from frappe/mergify/bp/version-14-hotfix/pr-37213
fix: filter gl entries in process soa (#37213)
2023-09-24 20:18:51 +05:30
Deepesh Garg
81d52b16d9 Merge pull request #37220 from frappe/mergify/bp/version-14-hotfix/pr-37167
fix: set customer currency in pos_invoice if exists (#37167)
2023-09-24 20:18:09 +05:30
milanpethani
e0da8d261f fix: set customer currency in pos_invoice if exists
if currency exists in the profile and customer currency doesn't exists still it will update currency to None, so update customer currency only if exists

(cherry picked from commit 041d52e828)
2023-09-24 12:24:02 +00:00
Deepesh Garg
57c82c1800 feat: Toggle net values in Trial Balance report
(cherry picked from commit 06a45897de)
2023-09-24 12:15:57 +00:00
Gursheen Anand
b4bc44db4a fix: apply gl report filters
(cherry picked from commit 5346c67b02)
2023-09-24 12:15:44 +00:00
ruthra kumar
d3c9a6d975 Merge pull request #37206 from frappe/mergify/bp/version-14-hotfix/pr-37202
refactor: ignore PLE's on PCV cancellation (backport #37202)
2023-09-22 13:57:53 +05:30
ruthra kumar
9d5fce9091 refactor: ignore PLE's on PCV cancellation
(cherry picked from commit 301092dad1)
2023-09-22 08:00:36 +00:00
ruthra kumar
139a7ce911 Merge pull request #37196 from frappe/mergify/bp/version-14-hotfix/pr-37069
fix: Recalculate `advance_paid` in SO/PO after unlinking from advance entry (backport #37069)
2023-09-22 10:18:00 +05:30
marination
591c720e51 test: Impact on SO of advance PE submit and unlinking/replacement by SI
(cherry picked from commit 8a4954d713)
2023-09-22 03:06:31 +00:00
marination
1181dcf521 fix: Update advance_paid in SO/PO after unlinking from advance entry
(cherry picked from commit 426350eee6)
2023-09-22 03:06:31 +00:00
mergify[bot]
02fc67c83c feat: Stock Ledger Variance report (backport #37165) (#37183)
feat: `Stock Ledger Variance` report (#37165)

* feat: `Stock Ledger Variance` report

* refactor: `get_data()`

(cherry picked from commit acda72d616)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-21 07:49:02 +00:00
Deepesh Garg
43f3779f10 Merge pull request #37156 from frappe/mergify/bp/version-14-hotfix/pr-37122
fix: german translation of Sales and Purchase Invoice (backport #37122)
2023-09-20 22:15:43 +05:30
mergify[bot]
f2bcfb5f97 fix: labels for Stock Ledger Invariant Check report (backport #37150) (#37176)
fix: labels for `Stock Ledger Invariant Check` report (#37150)

refactor: `Stock Ledger Invariant Check` report
(cherry picked from commit f0859ecc60)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-20 19:21:54 +05:30
Raffael Meyer
03e52d3859 chore: resolve conflicts 2023-09-20 14:38:42 +02:00
mergify[bot]
56657b6122 fix: allow to select parent warehouse in the website item (backport #37047) (#37173)
fix: allow to select parent warehouse in the website item (#37047)

(cherry picked from commit e6199dc802)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-09-20 18:06:30 +05:30
Frappe PR Bot
2713925628 chore(release): Bumped to Version 14.40.0
# [14.40.0](https://github.com/frappe/erpnext/compare/v14.39.0...v14.40.0) (2023-09-20)

### Bug Fixes

* + btn not appearing for delivery note connection (backport [#36980](https://github.com/frappe/erpnext/issues/36980)) ([#37070](https://github.com/frappe/erpnext/issues/37070)) ([c2a0c1e](c2a0c1e989))
* accepted warehouse and rejected warehouse can't be same (backport [#36973](https://github.com/frappe/erpnext/issues/36973)) ([#37071](https://github.com/frappe/erpnext/issues/37071)) ([b56c9b9](b56c9b91f1))
* after applying coupon code, field in_words not updated ([#37133](https://github.com/frappe/erpnext/issues/37133)) ([1cb8c64](1cb8c64c94))
* Apply dimension filter, irrespective of dimesion columns ([9bc44a3](9bc44a3b40))
* asset validation misfire on debit notes ([b33db6c](b33db6c79a))
* company wise deferred accounting fields in item ([#37023](https://github.com/frappe/erpnext/issues/37023)) ([13aaff3](13aaff30a5))
* Don't allow merging accounts with different currency ([#37074](https://github.com/frappe/erpnext/issues/37074)) ([c41cb39](c41cb3930c))
* don't set from warehouse for purchase material request ([#37132](https://github.com/frappe/erpnext/issues/37132)) ([e62b783](e62b783f34))
* Duplicate Serial Nos validation in POS ([#36927](https://github.com/frappe/erpnext/issues/36927)) ([366325c](366325ca3c))
* fetch logic for repay_from_salary in loan_repayment [v14] ([#37135](https://github.com/frappe/erpnext/issues/37135)) ([480a0ca](480a0ca7a8))
* ignore user permissions for `Source Warehouse` in MR (backport [#37102](https://github.com/frappe/erpnext/issues/37102)) ([#37110](https://github.com/frappe/erpnext/issues/37110)) ([727dcc5](727dcc5034))
* incorrect stock ledger entries in DN (backport [#36944](https://github.com/frappe/erpnext/issues/36944)) ([#37066](https://github.com/frappe/erpnext/issues/37066)) ([699ad80](699ad80802))
* packed item using expired price ([413b40f](413b40f5a7))
* POS opening Issue if Product Bundle is available ([#37138](https://github.com/frappe/erpnext/issues/37138)) ([af05864](af05864e6d))
* precision issue and column name ([#37073](https://github.com/frappe/erpnext/issues/37073)) ([f2395a9](f2395a9297))
* Purchase Receipt Provisional Accounting GL Entries (backport [#37046](https://github.com/frappe/erpnext/issues/37046)) ([#37068](https://github.com/frappe/erpnext/issues/37068)) ([8772e40](8772e40bae))
* Remove redundant code ([#37001](https://github.com/frappe/erpnext/issues/37001)) ([3ecdf02](3ecdf028f2))
* **ux:** move `get_route_options_for_new_doc` to `refresh` ([#37092](https://github.com/frappe/erpnext/issues/37092)) ([a563fed](a563fed6dc))
* validate duplicate serial no in DN ([fffa13f](fffa13f22b))

### Features

* provision to create RIV from `Stock Ledger Invariant Check` report (backport [#37115](https://github.com/frappe/erpnext/issues/37115)) ([#37147](https://github.com/frappe/erpnext/issues/37147)) ([29ff0ce](29ff0ce286))
2023-09-20 06:24:47 +00:00
Deepesh Garg
428870a65d Merge pull request #37153 from frappe/version-14-hotfix
chore: release v14
2023-09-20 11:53:07 +05:30
mergify[bot]
c5df164e1c chore: translations dutch (#37042)
chore: translations dutch (#37042)

update: translations dutch
(cherry picked from commit c35dea7177)

Co-authored-by: RJPvT <48353029+RJPvT@users.noreply.github.com>
2023-09-19 20:41:53 +05:30
HENRY Florian
8c14dbd7ac chore: translation manual backport of #35697 (#37053)
chore: translation manual backport of #35697
2023-09-19 20:26:05 +05:30
Raffael Meyer
8236814270 fix: german translation of Sales and Purchase Invoice (#37122)
* fix: german translation of Purchase Invoice

* fix: german translation of Sales Invoice

(cherry picked from commit 84a9000db2)

# Conflicts:
#	erpnext/translations/de.csv
2023-09-19 14:29:56 +00:00
mergify[bot]
1cb8c64c94 fix: after applying coupon code, field in_words not updated (#37133)
fix: after applying coupon code, field in_words not updated (#37133)

* fix: after applying coupon code, field in_words not updated

* fix: changed the order of the function set_total_in_words

(cherry picked from commit 03f0abf6de)

Co-authored-by: RitvikSardana <65544983+RitvikSardana@users.noreply.github.com>
2023-09-19 19:44:29 +05:30
mergify[bot]
699ad80802 fix: incorrect stock ledger entries in DN (backport #36944) (#37066)
fix: incorrect stock ledger entries in DN (#36944)

(cherry picked from commit 0e83190c19)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-09-19 19:30:47 +05:30
Anand Baburajan
480a0ca7a8 fix: fetch logic for repay_from_salary in loan_repayment [v14] (#37135)
* fix: fetch logic for repay_from_salary in loan_repayment

* chore: only fetch repay_from_salary if field exists
2023-09-19 15:18:35 +05:30
mergify[bot]
29ff0ce286 feat: provision to create RIV from Stock Ledger Invariant Check report (backport #37115) (#37147)
feat: provision to create RIV from `Stock Ledger Invariant Check` report (#37115)

* feat: provision to create RIV from `Stock Ledger Invariant Check` report

* fix: `linter`

(cherry picked from commit 9c9d0ecb73)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-09-19 14:01:47 +05:30
RitvikSardana
af05864e6d fix: POS opening Issue if Product Bundle is available (#37138)
fix: POS opening issue because of Product Bundle
2023-09-19 13:09:35 +05:30
mergify[bot]
dfe5f63f59 refactor: more generic filters in Accounts Receivable report (backport #37131) (#37137)
* refactor: replace 'customer' filter with 'party_type' and 'party'

(cherry picked from commit ac650d2e7a)

* chore: remove stale code

(cherry picked from commit 083c82c206)

* refactor(test): AR output filtered on USD customers

(cherry picked from commit 08d91ab831)

---------

Co-authored-by: ruthra kumar <ruthra@erpnext.com>
2023-09-19 13:07:30 +05:30
RitvikSardana
366325ca3c fix: Duplicate Serial Nos validation in POS (#36927)
* fix: added validation for duplicate serial nos in pos

* chore: code cleanup

* chore: code cleanup

* fix: removed duplicate batch number validation

* chore: code cleanup
2023-09-18 16:32:45 +05:30
rohitwaghchaure
e62b783f34 fix: don't set from warehouse for purchase material request (#37132) 2023-09-18 16:10:21 +05:30
ruthra kumar
8ef548f999 Merge pull request #37129 from frappe/mergify/bp/version-14-hotfix/pr-37127
refactor: better date filters in `Get Outstanding Invoices` dialog (backport #37127)
2023-09-18 13:51:48 +05:30
ruthra kumar
4b700b726f refactor: better date filters in Get Outstanding Invoices dialog
(cherry picked from commit 9004721859)
2023-09-18 07:52:18 +00:00
mergify[bot]
c41cb3930c fix: Don't allow merging accounts with different currency (#37074)
* fix: Don't allow merging accounts with different currency (#37074)

* fix: Don't allow merging accounts with different currency

* test: Update conflicting values

* test: Update conflicting values

(cherry picked from commit 5e21e7cd1d)

# Conflicts:
#	erpnext/accounts/doctype/account/account.js
#	erpnext/accounts/doctype/account/account.py

* chore: resolve conflicts

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-09-18 13:03:08 +05:30
Frappe PR Bot
7af8aec879 chore(release): Bumped to Version 14.39.0
# [14.39.0](https://github.com/frappe/erpnext/compare/v14.38.0...v14.39.0) (2023-09-12)

### Bug Fixes

* `company` is ambiguous ([fe69d53](fe69d5364d))
* `Parent Task` link with `Project Task` (backport [#37025](https://github.com/frappe/erpnext/issues/37025)) ([#37033](https://github.com/frappe/erpnext/issues/37033)) ([6602787](66027877d3))
* correct asset daily depr schedule calculation [v14] ([#36991](https://github.com/frappe/erpnext/issues/36991)) ([2ae4463](2ae4463b76))
* generate pdf only when result exists ([53270dd](53270dd933))
* remove report field db set ([284181d](284181d766))
* show letterhead and terms for AR pdf ([2077b2c](2077b2cde4))
* Update party type for payroll payable account ([f251d6c](f251d6cb69))
* use primary key for link lookup (backport [#36919](https://github.com/frappe/erpnext/issues/36919)) ([#36978](https://github.com/frappe/erpnext/issues/36978)) ([4fede56](4fede56d98))
* **ux:** docstatus filter for `Reference Name` in QI (backport [#37024](https://github.com/frappe/erpnext/issues/37024)) ([#37028](https://github.com/frappe/erpnext/issues/37028)) ([21be889](21be889a77))

### Features

* add field for specifying pdf name ([657ca7f](657ca7ff22))
* Add half-yearly asset maintenance periodicity. (backport [#37006](https://github.com/frappe/erpnext/issues/37006)) ([#37014](https://github.com/frappe/erpnext/issues/37014)) ([acd9c69](acd9c69201))
* provision to set required by from Production Plan ([#37039](https://github.com/frappe/erpnext/issues/37039)) ([d278b11](d278b11603))
2023-09-12 12:22:33 +00:00
Deepesh Garg
69454fc804 Merge pull request #37043 from frappe/version-14-hotfix
chore: release v14
2023-09-12 17:49:56 +05:30
Florian HENRY
5c0a232e0f refactor(region): Splitting of France Regional logic from ERPNext 2023-09-10 17:24:01 +02:00
Florian HENRY
7de3d08ce3 refactor(region): Splitting of France Regional logic from ERPNext 2023-09-10 17:23:35 +02:00
Florian HENRY
fb7ca8fca2 refactor(region): Splitting of France Regional logic from ERPNext 2023-09-10 17:19:35 +02:00
Florian HENRY
d9f15e96b5 refactor(region): Splitting of France Regional logic from ERPNext 2023-09-10 17:18:42 +02:00
Frappe PR Bot
a4b80d1ec4 chore(release): Bumped to Version 14.38.0
# [14.38.0](https://github.com/frappe/erpnext/compare/v14.37.1...v14.38.0) (2023-09-06)

### Bug Fixes

* account payable currency and value ([#36859](https://github.com/frappe/erpnext/issues/36859)) ([98c2640](98c26403c1))
* added ignore_user_permissions to parent field of tree doctypes ([451cc7b](451cc7bc12))
* added validation for unique serial numbers in pos invoice ([#36302](https://github.com/frappe/erpnext/issues/36302)) ([1192736](119273639c))
* allow payment_account of loan repayment to be edited ([#36948](https://github.com/frappe/erpnext/issues/36948)) ([09e2f24](09e2f24329))
* ask for asset related accounts only when needed (backport [#36960](https://github.com/frappe/erpnext/issues/36960)) ([#36971](https://github.com/frappe/erpnext/issues/36971)) ([58163d5](58163d5aa8))
* calcuate received/paid amount on rate change in PE ([0a63266](0a632660e0))
* deduplicate gain/loss JE creation for journals as payment ([9168b3b](9168b3b0e8))
* difference amount in UI should not be calculated ([9bc2b41](9bc2b419e3))
* fetch discount amount for gle in base currency ([a8b5880](a8b58800bb))
* ignore mandatory fields while saving WO (backport [#36954](https://github.com/frappe/erpnext/issues/36954)) ([#36970](https://github.com/frappe/erpnext/issues/36970)) ([c125dea](c125dea0f1))
* index error on Receivable report based on payment terms ([#36963](https://github.com/frappe/erpnext/issues/36963)) ([e3d64fc](e3d64fc553))
* invalid gain/loss JE created on base currency Expense Claim ([068f1b5](068f1b5a6b))
* only show "Unreconcile" if reconciled ([61752ac](61752ac2b4))
* prorate factor for subscription plan ([#36953](https://github.com/frappe/erpnext/issues/36953)) ([fc79af5](fc79af5926))
* reduce threshold for bg job fn ([5a226a8](5a226a8395))
* remove withholding category from common fields ([18f8f7f](18f8f7f09c))
* Set the default filter in All Trends Report ([132957f](132957f59e))
* Set the default filter in All Trends Report ([420536c](420536ca52))
* when create doc from item dashboard default uom (buying or selling) is not correctly selected (backport [#36892](https://github.com/frappe/erpnext/issues/36892)) ([#36928](https://github.com/frappe/erpnext/issues/36928)) ([5c8bee0](5c8bee0a95))

### Features

* **RFQ:** optionally send document print ([#36363](https://github.com/frappe/erpnext/issues/36363)) ([345c608](345c6084e5))
2023-09-06 05:57:04 +00:00
ruthra kumar
260567e99e Merge pull request #36969 from frappe/version-14-hotfix
chore: release v14
2023-09-06 11:25:38 +05:30
Frappe PR Bot
09d9263082 chore(release): Bumped to Version 14.37.1
## [14.37.1](https://github.com/frappe/erpnext/compare/v14.37.0...v14.37.1) (2023-08-31)

### Bug Fixes

* calcuate received/paid amount on rate change in PE ([46bea0e](46bea0e56f))
2023-08-31 14:11:41 +00:00
ruthra kumar
38d27a13c3 Merge pull request #36890 from frappe/mergify/bp/version-14/pr-36888
fix: calcuate received/paid amount on exchange rate change in Payment Entry (backport #36888)
2023-08-31 19:39:50 +05:30
ruthra kumar
46bea0e56f fix: calcuate received/paid amount on rate change in PE
(cherry picked from commit 64d835374b)
2023-08-31 13:09:15 +00:00
Frappe PR Bot
b95ab3425a chore(release): Bumped to Version 14.37.0
# [14.37.0](https://github.com/frappe/erpnext/compare/v14.36.0...v14.37.0) (2023-08-30)

### Bug Fixes

* added valuation field type (Float/Currency) in the filter (backport [#36866](https://github.com/frappe/erpnext/issues/36866)) ([#36868](https://github.com/frappe/erpnext/issues/36868)) ([22247cf](22247cfa17))
* Allow to make return against sales invoice which has closed sales order ([0f98cc8](0f98cc85e9))
* Asset Category filter is not working in asset depreciation([#36806](https://github.com/frappe/erpnext/issues/36806)) ([bd41cb2](bd41cb221b))
* create entries for only PR items present in LCV ([#36852](https://github.com/frappe/erpnext/issues/36852)) ([d2091cc](d2091cc22c))
* error in report when data is not available to load chart in report (backport [#36842](https://github.com/frappe/erpnext/issues/36842)) ([#36853](https://github.com/frappe/erpnext/issues/36853)) ([9789b7b](9789b7bdef))
* error listindexoutofrange when save a production plan ([#36807](https://github.com/frappe/erpnext/issues/36807)) ([fd41594](fd4159423d))
* fetch JVs in tax withholding report without party values ([bc6bd81](bc6bd81f87))
* fetch rounded total while pulling reference details on SO ([adc87f1](adc87f16a3))
* missing company flag for regional fn ([#36791](https://github.com/frappe/erpnext/issues/36791)) ([c07548a](c07548a612))
* SCR return status (backport [#36793](https://github.com/frappe/erpnext/issues/36793)) ([#36796](https://github.com/frappe/erpnext/issues/36796)) ([6edfcf4](6edfcf4de8))
* Tax withholding reversal on Debit Notes ([e8dc63c](e8dc63c89c))

### Features

* **MR:** Project and Cost Center in Connections (backport [#36794](https://github.com/frappe/erpnext/issues/36794)) ([#36795](https://github.com/frappe/erpnext/issues/36795)) ([4fa0777](4fa07777e9))
2023-08-30 13:56:17 +00:00
Deepesh Garg
16edf3ac36 Merge pull request #36861 from frappe/version-14-hotfix
chore: release v14
2023-08-30 19:24:48 +05:30
Frappe PR Bot
d1b2ce35b6 chore(release): Bumped to Version 14.36.0
# [14.36.0](https://github.com/frappe/erpnext/compare/v14.35.2...v14.36.0) (2023-08-23)

### Bug Fixes

* Accounts Payable Currency bug ([94612b9](94612b90ef))
* Add company filters for account ([e62ffa9](e62ffa990d))
* add missing items labels back ([#36737](https://github.com/frappe/erpnext/issues/36737)) ([3634e80](3634e80341))
* broken advance field in Accounts Receivable summary rpt ([296a4d7](296a4d7a12))
* broken consolidated report due to finance book filter ([5bd2a09](5bd2a0923f))
* check tax and charges if it is passed ([1f76c69](1f76c6972b))
* clear dimension defaults after test ([2a467a9](2a467a9fbf))
* dict value for dimension for gl entries defined without the dimension ([11ba553](11ba553dbd))
* dimension name in remark ([01ae513](01ae513f70))
* divide offsetting amount for multiple dimensions ([1269f2d](1269f2d301))
* divide offsetting amount only when account exists ([2c8c3a0](2c8c3a022c))
* Document Name link validation in Bank Reconciliation Tool ([#36495](https://github.com/frappe/erpnext/issues/36495)) ([83cbc1b](83cbc1bef6)), closes [#35540](https://github.com/frappe/erpnext/issues/35540) [#35540](https://github.com/frappe/erpnext/issues/35540)
* don't throw if item does not have default BOM (backport [#36709](https://github.com/frappe/erpnext/issues/36709)) ([#36734](https://github.com/frappe/erpnext/issues/36734)) ([e1bd9a7](e1bd9a7e8d))
* duplicate acc dimension in test ([cdb66bf](cdb66bf198))
* fetch acc dimensions correctly when fieldname is different from name ([7ac35b4](7ac35b496a))
* fetch accounting dimension details specific to company ([c1f1a21](c1f1a21714))
* include gain/loss journal in AR/AP reports ([4606079](4606079568))
* incorrect gl balance on multi company setup ([cb9aad3](cb9aad3e87))
* incorrect schedule in asset value adjustment ([#36725](https://github.com/frappe/erpnext/issues/36725)) ([a0575ed](a0575ed2b0))
* make offsetting entry for acc dimensions ([3198f26](3198f2669d))
* make offsetting entry for all doctypes ([f578c32](f578c3219d))
* mode of payment fetched from pos profile company in POS ([c74a414](c74a414313))
* not able to make stock entry ([#36759](https://github.com/frappe/erpnext/issues/36759)) ([873ee38](873ee384a1))
* Procurement Tracker report not showing material request items ([#36768](https://github.com/frappe/erpnext/issues/36768)) ([6a9935c](6a9935c00e))
* reset dimension defaults when company changedin test ([37ef6e9](37ef6e959b))
* **RFQ:** make "update password" and "submit quotation" buttons the same size (backport [#36667](https://github.com/frappe/erpnext/issues/36667)) ([#36686](https://github.com/frappe/erpnext/issues/36686)) ([36147ec](36147ec02c))
* timeout error coming during reposting ([#36715](https://github.com/frappe/erpnext/issues/36715)) ([620b21f](620b21fec5))
* **ux:** change batch selection message to alert (backport [#36500](https://github.com/frappe/erpnext/issues/36500)) ([#36697](https://github.com/frappe/erpnext/issues/36697)) ([0a4947a](0a4947a8f6))
* **UX:** Ignore prepared report ([3e23e1f](3e23e1fb91))

### Features

* **RFQ:** make email message fully configurable (backport [#36353](https://github.com/frappe/erpnext/issues/36353)) ([#36531](https://github.com/frappe/erpnext/issues/36531)) ([c308bd5](c308bd5309))
* Tick on checkbox to include draft timesheets (backport [#36577](https://github.com/frappe/erpnext/issues/36577)) ([#36640](https://github.com/frappe/erpnext/issues/36640)) ([9668615](9668615f7e))
* utility to repost accounting ledgers without cancellation ([#36469](https://github.com/frappe/erpnext/issues/36469)) ([f8d6fe6](f8d6fe6be0))

### Performance Improvements

* pull latest details only for referenced vouchers ([47345e8](47345e81a1))
2023-08-23 02:59:58 +00:00
Deepesh Garg
a0a3519cd3 Merge pull request #36763 from frappe/version-14-hotfix
chore: release v14
2023-08-23 08:28:36 +05:30
Frappe PR Bot
de3b67c327 chore(release): Bumped to Version 14.35.2
## [14.35.2](https://github.com/frappe/erpnext/compare/v14.35.1...v14.35.2) (2023-08-18)

### Bug Fixes

* timeout error coming during reposting (backport [#36715](https://github.com/frappe/erpnext/issues/36715)) ([#36716](https://github.com/frappe/erpnext/issues/36716)) ([d5a596d](d5a596dff1))
2023-08-18 15:30:59 +00:00
mergify[bot]
d5a596dff1 fix: timeout error coming during reposting (backport #36715) (#36716)
fix: timeout error coming during reposting (#36715)

(cherry picked from commit 620b21fec5)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
2023-08-18 20:59:20 +05:30
Frappe PR Bot
2aa3b30b56 chore(release): Bumped to Version 14.35.1
## [14.35.1](https://github.com/frappe/erpnext/compare/v14.35.0...v14.35.1) (2023-08-17)

### Bug Fixes

* **ux:** change batch selection message to alert ([#36500](https://github.com/frappe/erpnext/issues/36500)) ([641fe77](641fe7738c))
2023-08-17 12:32:49 +00:00
Dany Robert
641fe7738c fix(ux): change batch selection message to alert (#36500)
* fix(ux): change batch selection message to alert

* chore: linters
2023-08-17 18:01:15 +05:30
Frappe PR Bot
b773465b41 chore(release): Bumped to Version 14.35.0
# [14.35.0](https://github.com/frappe/erpnext/compare/v14.34.3...v14.35.0) (2023-08-16)

### Bug Fixes

* Allow backdated repayment cancels for term loans ([1377cf4](1377cf4cf1))
* allow negative stock condition for batch item ([#36586](https://github.com/frappe/erpnext/issues/36586)) ([ee04c6d](ee04c6d5c5))
* AR/AP report based on payment terms ([#36574](https://github.com/frappe/erpnext/issues/36574)) ([ac0fff7](ac0fff7e94))
* better remarks on Cr note created by Reconciliation ([5443592](5443592d84))
* Button Alignment center in hero slider ([#36607](https://github.com/frappe/erpnext/issues/36607)) ([b131f70](b131f70ed6)), closes [#36561](https://github.com/frappe/erpnext/issues/36561)
* cr/dr note should be posted for exc gain/loss ([349601b](349601b4b9))
* disallow mulitple SO with same PO No ([b901cfd](b901cfdbe2))
* don't show disabled items in `Item Shortage Report` (backport [#36550](https://github.com/frappe/erpnext/issues/36550)) ([#36571](https://github.com/frappe/erpnext/issues/36571)) ([19cfcea](19cfcea78e))
* fetch `Stock UOM` from Item if not set (backport [#36606](https://github.com/frappe/erpnext/issues/36606)) ([#36617](https://github.com/frappe/erpnext/issues/36617)) ([8b13185](8b13185c25))
* Group Account total not showing in Financial Statements ([2912648](2912648151))
* incorrect available qty for backdated stock reco with batch ([#36581](https://github.com/frappe/erpnext/issues/36581)) ([2800ad3](2800ad39d2))
* incorrect gain/loss on allocation change on reconciliation tool ([39c439d](39c439dc4b))
* Make default sales update frequency as monthly instead of each transaction ([4ca1f3b](4ca1f3b9cf))
* move company rename to long queue ([8083c0b](8083c0b59e))
* precision issue while submitting the stock entry ([#36575](https://github.com/frappe/erpnext/issues/36575)) ([a864e07](a864e07d4f))
* re-add permission that was unintentionally removed ([#36663](https://github.com/frappe/erpnext/issues/36663)) ([99777d3](99777d3fa4))
* **RFQ:** link to supplier portal ([eb2f68e](eb2f68ec98))
* standard formula to calculate the "difference" ([#36612](https://github.com/frappe/erpnext/issues/36612)) ([716d5c0](716d5c0b98))
* Tax withholding post LDC limit consumed ([#36611](https://github.com/frappe/erpnext/issues/36611)) ([1deebe8](1deebe8757))
* **test:** replace hardcoded reference to adv with dynamic one ([2e6bfa3](2e6bfa36de))
* **test:** test case breakage in Github Actions ([3542df7](3542df70f6))
* unhide `uom` and `stock_uom` fields in print view ([b49309c](b49309c160))
* validation blocks partial payment for SO and PO ([ce08f03](ce08f038d2))
* wrap none type rate under flt (backport [#36602](https://github.com/frappe/erpnext/issues/36602)) ([#36604](https://github.com/frappe/erpnext/issues/36604)) ([63c061e](63c061e5e8))
* wrong currency on financial-statement based reports ([#36524](https://github.com/frappe/erpnext/issues/36524)) ([3044f46](3044f46c52))

### Features

* add voucher totals in tds payable report ([#36568](https://github.com/frappe/erpnext/issues/36568)) ([90b390c](90b390c2c5))
* daily asset depreciation method ([#36587](https://github.com/frappe/erpnext/issues/36587)) ([2918127](29181274c8))
* Reallow customizing company abbreviation on setup. ([#36646](https://github.com/frappe/erpnext/issues/36646)) ([ca34b63](ca34b63470))

### Performance Improvements

* **invoice:** Faster return amount query (backport [#36556](https://github.com/frappe/erpnext/issues/36556)) ([#36557](https://github.com/frappe/erpnext/issues/36557)) ([fe41be9](fe41be953d))
2023-08-16 06:34:09 +00:00
Deepesh Garg
787118d00b Merge pull request #36658 from frappe/version-14-hotfix
chore: release v14
2023-08-16 12:02:35 +05:30
155 changed files with 4524 additions and 1127 deletions

View File

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

View File

@@ -117,9 +117,6 @@ frappe.ui.form.on('Account', {
args: {
old: frm.doc.name,
new: data.name,
is_group: frm.doc.is_group,
root_type: frm.doc.root_type,
company: frm.doc.company
},
callback: function(r) {
if(!r.exc) {

View File

@@ -18,6 +18,10 @@ class BalanceMismatchError(frappe.ValidationError):
pass
class InvalidAccountMergeError(frappe.ValidationError):
pass
class Account(NestedSet):
nsm_parent_field = "parent_account"
@@ -444,24 +448,35 @@ def update_account_number(name, account_name, account_number=None, from_descenda
@frappe.whitelist()
def merge_account(old, new, is_group, root_type, company):
def merge_account(old, new):
# Validate properties before merging
if not frappe.db.exists("Account", new):
new_account = frappe.get_cached_doc("Account", new)
old_account = frappe.get_cached_doc("Account", old)
if not new_account:
throw(_("Account {0} does not exist").format(new))
val = list(frappe.db.get_value("Account", new, ["is_group", "root_type", "company"]))
if val != [cint(is_group), root_type, company]:
if (
cint(new_account.is_group),
new_account.root_type,
new_account.company,
cstr(new_account.account_currency),
) != (
cint(old_account.is_group),
old_account.root_type,
old_account.company,
cstr(old_account.account_currency),
):
throw(
_(
"""Merging is only possible if following properties are same in both records. Is Group, Root Type, Company"""
)
msg=_(
"""Merging is only possible if following properties are same in both records. Is Group, Root Type, Company and Account Currency"""
),
title=("Invalid Accounts"),
exc=InvalidAccountMergeError,
)
if is_group and frappe.db.get_value("Account", new, "parent_account") == old:
frappe.db.set_value(
"Account", new, "parent_account", frappe.db.get_value("Account", old, "parent_account")
)
if old_account.is_group and new_account.parent_account == old:
new_account.db_set("parent_account", frappe.get_cached_value("Account", old, "parent_account"))
frappe.rename_doc("Account", old, new, merge=1, force=1)

View File

@@ -7,7 +7,11 @@ import unittest
import frappe
from frappe.test_runner import make_test_records
from erpnext.accounts.doctype.account.account import merge_account, update_account_number
from erpnext.accounts.doctype.account.account import (
InvalidAccountMergeError,
merge_account,
update_account_number,
)
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
test_dependencies = ["Company"]
@@ -47,49 +51,53 @@ class TestAccount(unittest.TestCase):
frappe.delete_doc("Account", "1211-11-4 - 6 - Debtors 1 - Test - - _TC")
def test_merge_account(self):
if not frappe.db.exists("Account", "Current Assets - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Current Assets"
acc.is_group = 1
acc.parent_account = "Application of Funds (Assets) - _TC"
acc.company = "_Test Company"
acc.insert()
if not frappe.db.exists("Account", "Securities and Deposits - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Securities and Deposits"
acc.parent_account = "Current Assets - _TC"
acc.is_group = 1
acc.company = "_Test Company"
acc.insert()
if not frappe.db.exists("Account", "Earnest Money - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Earnest Money"
acc.parent_account = "Securities and Deposits - _TC"
acc.company = "_Test Company"
acc.insert()
if not frappe.db.exists("Account", "Cash In Hand - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Cash In Hand"
acc.is_group = 1
acc.parent_account = "Current Assets - _TC"
acc.company = "_Test Company"
acc.insert()
if not frappe.db.exists("Account", "Accumulated Depreciation - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Accumulated Depreciation"
acc.parent_account = "Fixed Assets - _TC"
acc.company = "_Test Company"
acc.account_type = "Accumulated Depreciation"
acc.insert()
create_account(
account_name="Current Assets",
is_group=1,
parent_account="Application of Funds (Assets) - _TC",
company="_Test Company",
)
create_account(
account_name="Securities and Deposits",
is_group=1,
parent_account="Current Assets - _TC",
company="_Test Company",
)
create_account(
account_name="Earnest Money",
parent_account="Securities and Deposits - _TC",
company="_Test Company",
)
create_account(
account_name="Cash In Hand",
is_group=1,
parent_account="Current Assets - _TC",
company="_Test Company",
)
create_account(
account_name="Receivable INR",
parent_account="Current Assets - _TC",
company="_Test Company",
account_currency="INR",
)
create_account(
account_name="Receivable USD",
parent_account="Current Assets - _TC",
company="_Test Company",
account_currency="USD",
)
doc = frappe.get_doc("Account", "Securities and Deposits - _TC")
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
self.assertEqual(parent, "Securities and Deposits - _TC")
merge_account(
"Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company
)
merge_account("Securities and Deposits - _TC", "Cash In Hand - _TC")
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
# Parent account of the child account changes after merging
@@ -98,30 +106,28 @@ class TestAccount(unittest.TestCase):
# Old account doesn't exist after merging
self.assertFalse(frappe.db.exists("Account", "Securities and Deposits - _TC"))
doc = frappe.get_doc("Account", "Current Assets - _TC")
# Raise error as is_group property doesn't match
self.assertRaises(
frappe.ValidationError,
InvalidAccountMergeError,
merge_account,
"Current Assets - _TC",
"Accumulated Depreciation - _TC",
doc.is_group,
doc.root_type,
doc.company,
)
doc = frappe.get_doc("Account", "Capital Stock - _TC")
# Raise error as root_type property doesn't match
self.assertRaises(
frappe.ValidationError,
InvalidAccountMergeError,
merge_account,
"Capital Stock - _TC",
"Softwares - _TC",
doc.is_group,
doc.root_type,
doc.company,
)
# Raise error as currency doesn't match
self.assertRaises(
InvalidAccountMergeError,
merge_account,
"Receivable INR - _TC",
"Receivable USD - _TC",
)
def test_account_sync(self):
@@ -400,11 +406,20 @@ def create_account(**kwargs):
"Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")}
)
if account:
return account
account = frappe.get_doc("Account", account)
account.update(
dict(
is_group=kwargs.get("is_group", 0),
parent_account=kwargs.get("parent_account"),
)
)
account.save()
return account.name
else:
account = frappe.get_doc(
dict(
doctype="Account",
is_group=kwargs.get("is_group", 0),
account_name=kwargs.get("account_name"),
account_type=kwargs.get("account_type"),
parent_account=kwargs.get("parent_account"),

View File

@@ -37,6 +37,7 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date):
}
)
cle.flags.ignore_permissions = True
cle.flags.ignore_links = True
cle.submit()

View File

@@ -301,3 +301,30 @@ def get_dimensions(with_cost_center_and_project=False):
default_dimensions_map[dimension.company][dimension.fieldname] = dimension.default_dimension
return dimension_filters, default_dimensions_map
def create_accounting_dimensions_for_doctype(doctype):
accounting_dimensions = frappe.db.get_all(
"Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"]
)
if not accounting_dimensions:
return
for d in accounting_dimensions:
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
if field:
continue
df = {
"fieldname": d.fieldname,
"label": d.label,
"fieldtype": "Link",
"options": d.document_type,
"insert_after": "accounting_dimensions_section",
}
create_custom_field(doctype, df, ignore_validate=True)
frappe.clear_cache(doctype=doctype)

View File

@@ -352,10 +352,11 @@ frappe.ui.form.on("Bank Statement Import", {
export_errored_rows(frm) {
open_url_post(
"/api/method/frappe.core.doctype.data_import.data_import.download_errored_template",
"/api/method/erpnext.accounts.doctype.bank_statement_import.bank_statement_import.download_errored_template",
{
data_import_name: frm.doc.name,
}
},
true
);
},

View File

@@ -6,8 +6,10 @@
"engine": "InnoDB",
"field_order": [
"api_details_section",
"disabled",
"service_provider",
"api_endpoint",
"access_key",
"url",
"column_break_3",
"help",
@@ -77,12 +79,24 @@
"label": "Service Provider",
"options": "frankfurter.app\nexchangerate.host\nCustom",
"reqd": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"depends_on": "eval:doc.service_provider == 'exchangerate.host';",
"fieldname": "access_key",
"fieldtype": "Data",
"label": "Access Key"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-01-10 15:51:14.521174",
"modified": "2023-10-04 15:30:25.333860",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",

View File

@@ -18,11 +18,21 @@ class CurrencyExchangeSettings(Document):
def set_parameters_and_result(self):
if self.service_provider == "exchangerate.host":
if not self.access_key:
frappe.throw(
_("Access Key is required for Service Provider: {0}").format(
frappe.bold(self.service_provider)
)
)
self.set("result_key", [])
self.set("req_params", [])
self.api_endpoint = "https://api.exchangerate.host/convert"
self.append("result_key", {"key": "result"})
self.append("req_params", {"key": "access_key", "value": self.access_key})
self.append("req_params", {"key": "amount", "value": "1"})
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
self.append("req_params", {"key": "from", "value": "{from_currency}"})
self.append("req_params", {"key": "to", "value": "{to_currency}"})

View File

@@ -50,8 +50,18 @@ frappe.ui.form.on("Journal Entry", {
frm.trigger("make_inter_company_journal_entry");
}, __('Make'));
}
},
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
},
before_save: function(frm) {
if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) {
let payment_entry_references = frm.doc.accounts.filter(elem => (elem.reference_type == "Payment Entry"));
if (payment_entry_references.length > 0) {
let rows = payment_entry_references.map(x => "#"+x.idx);
frappe.throw(__("Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually.", [frappe.utils.comma_and(rows)]));
}
}
},
make_inter_company_journal_entry: function(frm) {
var d = new frappe.ui.Dialog({
title: __("Select Company"),

View File

@@ -49,9 +49,6 @@ def start_merge(docname):
merge_account(
row.account,
ledger_merge.account,
ledger_merge.is_group,
ledger_merge.root_type,
ledger_merge.company,
)
row.db_set("merged", 1)
frappe.db.commit()

View File

@@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', {
onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger'];
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries'];
if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
@@ -152,6 +152,7 @@ frappe.ui.form.on('Payment Entry', {
frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm);
frm.events.show_general_ledger(frm);
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
},
validate_company: (frm) => {

View File

@@ -107,6 +107,8 @@ class PaymentEntry(AccountsController):
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
"Unreconcile Payments",
"Unreconcile Payment Entries",
)
super(PaymentEntry, self).on_cancel()
self.make_gl_entries(cancel=1)
@@ -227,16 +229,18 @@ class PaymentEntry(AccountsController):
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
latest = latest.get(d.payment_term) or latest.get(None)
# The reference has already been fully paid
if not latest:
frappe.throw(
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
)
# The reference has already been partly paid
elif latest.outstanding_amount < latest.invoice_amount and flt(
d.outstanding_amount, d.precision("outstanding_amount")
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
elif (
latest.outstanding_amount < latest.invoice_amount
and flt(d.outstanding_amount, d.precision("outstanding_amount"))
!= flt(latest.outstanding_amount, d.precision("outstanding_amount"))
and d.payment_term == ""
):
frappe.throw(
_(
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
@@ -1455,6 +1459,14 @@ def get_outstanding_reference_documents(args):
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
)
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
elif args.get(date_fields[0]):
# if only from date is supplied
condition += " and {0} >= '{1}'".format(fieldname, args.get(date_fields[0]))
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
elif args.get(date_fields[1]):
# if only to date is supplied
condition += " and {0} <= '{1}'".format(fieldname, args.get(date_fields[1]))
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
if args.get("company"):
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
@@ -1592,11 +1604,10 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company):
"voucher_type": d.voucher_type,
"posting_date": d.posting_date,
"invoice_amount": flt(d.invoice_amount),
"outstanding_amount": flt(d.outstanding_amount),
"payment_term_outstanding": payment_term_outstanding,
"allocated_amount": payment_term_outstanding
"outstanding_amount": payment_term_outstanding
if payment_term_outstanding
else d.outstanding_amount,
"payment_term_outstanding": payment_term_outstanding,
"payment_amount": payment_term.payment_amount,
"payment_term": payment_term.payment_term,
}

View File

@@ -19,7 +19,7 @@ from erpnext.accounts.utils import (
get_outstanding_invoices,
reconcile_against_document,
)
from erpnext.controllers.accounts_controller import get_advance_payment_entries
from erpnext.controllers.accounts_controller import get_advance_payment_entries_for_regional
class PaymentReconciliation(Document):
@@ -62,7 +62,7 @@ class PaymentReconciliation(Document):
if self.payment_name:
condition += "name like '%%{0}%%'".format(self.payment_name)
payment_entries = get_advance_payment_entries(
payment_entries = get_advance_payment_entries_for_regional(
self.party_type,
self.party,
self.receivable_payable_account,
@@ -350,6 +350,7 @@ class PaymentReconciliation(Document):
)
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
adjust_allocations_for_taxes(self)
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
@@ -650,3 +651,8 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
None,
inv.cost_center,
)
@erpnext.allow_regional
def adjust_allocations_for_taxes(doc):
pass

View File

@@ -249,7 +249,7 @@ class PaymentRequest(Document):
if (
party_account_currency == ref_doc.company_currency and party_account_currency != self.currency
):
party_amount = ref_doc.base_grand_total
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
else:
party_amount = self.grand_total

View File

@@ -8,6 +8,7 @@
"transaction_date",
"posting_date",
"fiscal_year",
"year_start_date",
"amended_from",
"company",
"column_break1",
@@ -100,16 +101,22 @@
"fieldtype": "Text",
"label": "Error Message",
"read_only": 1
},
{
"fieldname": "year_start_date",
"fieldtype": "Date",
"label": "Year Start Date"
}
],
"icon": "fa fa-file-text",
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-07-20 14:51:04.714154",
"modified": "2023-09-11 20:19:11.810533",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Period Closing Voucher",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@@ -144,5 +151,6 @@
"search_fields": "posting_date, fiscal_year",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "closing_account_head"
}

View File

@@ -33,7 +33,7 @@ class PeriodClosingVoucher(AccountsController):
def on_cancel(self):
self.validate_future_closing_vouchers()
self.db_set("gle_processing_status", "In Progress")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
gle_count = frappe.db.count(
"GL Entry",
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
@@ -95,15 +95,23 @@ class PeriodClosingVoucher(AccountsController):
self.check_if_previous_year_closed()
pce = frappe.db.sql(
"""select name from `tabPeriod Closing Voucher`
where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
(self.posting_date, self.fiscal_year, self.company),
pcv = frappe.qb.DocType("Period Closing Voucher")
existing_entry = (
frappe.qb.from_(pcv)
.select(pcv.name)
.where(
(pcv.posting_date >= self.posting_date)
& (pcv.fiscal_year == self.fiscal_year)
& (pcv.docstatus == 1)
& (pcv.company == self.company)
)
.run()
)
if pce and pce[0][0]:
if existing_entry and existing_entry[0][0]:
frappe.throw(
_("Another Period Closing Entry {0} has been made after {1}").format(
pce[0][0], self.posting_date
existing_entry[0][0], self.posting_date
)
)
@@ -130,18 +138,27 @@ class PeriodClosingVoucher(AccountsController):
frappe.enqueue(
process_gl_entries,
gl_entries=gl_entries,
voucher_name=self.name,
timeout=3000,
)
frappe.enqueue(
process_closing_entries,
gl_entries=gl_entries,
closing_entries=closing_entries,
voucher_name=self.name,
company=self.company,
closing_date=self.posting_date,
queue="long",
timeout=3000,
)
frappe.msgprint(
_("The GL Entries will be processed in the background, it can take a few minutes."),
alert=True,
)
else:
process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
process_gl_entries(gl_entries, self.name)
process_closing_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
def get_grouped_gl_entries(self, get_opening_entries=False):
closing_entries = []
@@ -322,17 +339,12 @@ class PeriodClosingVoucher(AccountsController):
return query.run(as_dict=1)
def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
def process_gl_entries(gl_entries, voucher_name):
from erpnext.accounts.general_ledger import make_gl_entries
try:
if gl_entries:
make_gl_entries(gl_entries, merge_entries=False)
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
@@ -340,6 +352,19 @@ def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closi
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
def process_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
try:
if gl_entries + closing_entries:
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
def make_reverse_gl_entries(voucher_type, voucher_no):
from erpnext.accounts.general_ledger import make_reverse_gl_entries

View File

@@ -10,7 +10,7 @@ from frappe.utils import add_months, today
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.utils import get_fiscal_year, now
from erpnext.accounts.utils import get_fiscal_year
class TestPeriodClosingVoucher(unittest.TestCase):

View File

@@ -1,5 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors
# For license information, please see license.txt
import collections
import frappe
@@ -43,7 +45,6 @@ class POSInvoice(SalesInvoice):
self.validate_debit_to_acc()
self.validate_write_off_account()
self.validate_change_amount()
self.validate_duplicate_serial_and_batch_no()
self.validate_change_account()
self.validate_item_cost_centers()
self.validate_warehouse()
@@ -56,6 +57,7 @@ class POSInvoice(SalesInvoice):
self.validate_payment_amount()
self.validate_loyalty_transaction()
self.validate_company_with_pos_company()
self.validate_duplicate_serial_no()
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
@@ -156,27 +158,18 @@ class POSInvoice(SalesInvoice):
title=_("Item Unavailable"),
)
def validate_duplicate_serial_and_batch_no(self):
def validate_duplicate_serial_no(self):
serial_nos = []
batch_nos = []
for row in self.get("items"):
if row.serial_no:
serial_nos = row.serial_no.split("\n")
if row.batch_no and not row.serial_no:
batch_nos.append(row.batch_no)
if serial_nos:
for key, value in collections.Counter(serial_nos).items():
if value > 1:
frappe.throw(_("Duplicate Serial No {0} found").format("key"))
if batch_nos:
for key, value in collections.Counter(batch_nos).items():
if value > 1:
frappe.throw(_("Duplicate Batch No {0} found").format("key"))
def validate_pos_reserved_batch_qty(self, item):
filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no}
@@ -525,7 +518,7 @@ class POSInvoice(SalesInvoice):
selling_price_list = (
customer_price_list or customer_group_price_list or profile.get("selling_price_list")
)
if customer_currency != profile.get("currency"):
if customer_currency and customer_currency != profile.get("currency"):
self.set("currency", customer_currency)
else:
@@ -683,7 +676,7 @@ def get_bundle_availability(bundle_item_code, warehouse):
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.stock_qty
max_available_bundles = available_qty / item.qty
if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item"
):

View File

@@ -464,6 +464,37 @@ class TestPOSInvoice(unittest.TestCase):
pos2.insert()
self.assertRaises(frappe.ValidationError, pos2.submit)
def test_pos_invoice_with_duplicate_serial_no(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
se = make_serialized_item(
company="_Test Company",
target_warehouse="Stores - _TC",
cost_center="Main - _TC",
expense_account="Cost of Goods Sold - _TC",
)
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
pos = create_pos_invoice(
company="_Test Company",
debit_to="Debtors - _TC",
account_for_change_amount="Cash - _TC",
warehouse="Stores - _TC",
income_account="Sales - _TC",
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
item=se.get("items")[0].item_code,
rate=1000,
qty=2,
do_not_save=1,
)
pos.get("items")[0].has_serial_no = 1
pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[0]
self.assertRaises(frappe.ValidationError, pos.submit)
def test_invalid_serial_no_validation(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item

View File

@@ -47,6 +47,20 @@ class ProcessStatementOfAccounts(Document):
def get_report_pdf(doc, consolidated=True):
statement_dict = get_statement_dict(doc)
if not bool(statement_dict):
return False
elif consolidated:
delimiter = '<div style="page-break-before: always;"></div>' if doc.include_break else ""
result = delimiter.join(list(statement_dict.values()))
return get_pdf(result, {"orientation": doc.orientation})
else:
for customer, statement_html in statement_dict.items():
statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
return statement_dict
def get_statement_dict(doc, get_statement_dict=False):
statement_dict = {}
ageing = ""
@@ -64,6 +78,7 @@ def get_report_pdf(doc, consolidated=True):
filters = get_common_filters(doc)
if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
col, res = get_soa(filters)
for x in [0, -2, -1]:
res[x]["account"] = res[x]["account"].replace("'", "")
@@ -76,17 +91,11 @@ def get_report_pdf(doc, consolidated=True):
if not res:
continue
statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing)
statement_dict[entry.customer] = (
[res, ageing] if get_statement_dict else get_html(doc, filters, entry, col, res, ageing)
)
if not bool(statement_dict):
return False
elif consolidated:
result = "".join(list(statement_dict.values()))
return get_pdf(result, {"orientation": doc.orientation})
else:
for customer, statement_html in statement_dict.items():
statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
return statement_dict
return statement_dict
def set_ageing(doc, entry):
@@ -99,7 +108,8 @@ def set_ageing(doc, entry):
"range2": 60,
"range3": 90,
"range4": 120,
"customer": entry.customer,
"party_type": "Customer",
"party": [entry.customer],
}
)
col1, ageing = get_ageing(ageing_filters)
@@ -142,7 +152,8 @@ def get_gl_filters(doc, entry, tax_id, presentation_currency):
def get_ar_filters(doc, entry):
return {
"report_date": doc.posting_date if doc.posting_date else None,
"customer": entry.customer,
"party_type": "Customer",
"party": [entry.customer],
"customer_name": entry.customer_name if entry.customer_name else None,
"payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None,
"sales_partner": doc.sales_partner if doc.sales_partner else None,

View File

@@ -4,39 +4,107 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate, today
from erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts import (
get_statement_dict,
send_emails,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
class TestProcessStatementOfAccounts(unittest.TestCase):
class TestProcessStatementOfAccounts(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.create_customer()
self.create_customer(customer_name="Other Customer")
self.clear_old_entries()
self.si = create_sales_invoice()
self.process_soa = create_process_soa()
create_sales_invoice(customer="Other Customer")
def test_process_soa_for_gl(self):
"""Tests the utils for Statement of Accounts(General Ledger)"""
process_soa = create_process_soa(
name="_Test Process SOA for GL",
customers=[{"customer": "_Test Customer"}, {"customer": "Other Customer"}],
)
statement_dict = get_statement_dict(process_soa, get_statement_dict=True)
# Checks if the statements are filtered based on the Customer
self.assertIn("Other Customer", statement_dict)
self.assertIn("_Test Customer", statement_dict)
# Checks if the correct number of receivable entries exist
# 3 rows for opening and closing and 1 row for SI
receivable_entries = statement_dict["_Test Customer"][0]
self.assertEqual(len(receivable_entries), 4)
# Checks the amount for the receivable entry
self.assertEqual(receivable_entries[1].voucher_no, self.si.name)
self.assertEqual(receivable_entries[1].balance, 100)
def test_process_soa_for_ar(self):
"""Tests the utils for Statement of Accounts(Accounts Receivable)"""
process_soa = create_process_soa(name="_Test Process SOA for AR", report="Accounts Receivable")
statement_dict = get_statement_dict(process_soa, get_statement_dict=True)
# Checks if the statements are filtered based on the Customer
self.assertNotIn("Other Customer", statement_dict)
self.assertIn("_Test Customer", statement_dict)
# Checks if the correct number of receivable entries exist
receivable_entries = statement_dict["_Test Customer"][0]
self.assertEqual(len(receivable_entries), 1)
# Checks the amount for the receivable entry
self.assertEqual(receivable_entries[0].voucher_no, self.si.name)
self.assertEqual(receivable_entries[0].total_due, 100)
# Checks the ageing summary for AR
ageing_summary = statement_dict["_Test Customer"][1][0]
expected_summary = frappe._dict(
range1=100,
range2=0,
range3=0,
range4=0,
range5=0,
)
self.check_ageing_summary(ageing_summary, expected_summary)
def test_auto_email_for_process_soa_ar(self):
send_emails(self.process_soa.name, from_scheduler=True)
self.process_soa.load_from_db()
self.assertEqual(self.process_soa.posting_date, getdate(add_days(today(), 7)))
process_soa = create_process_soa(
name="_Test Process SOA", enable_auto_email=1, report="Accounts Receivable"
)
send_emails(process_soa.name, from_scheduler=True)
process_soa.load_from_db()
self.assertEqual(process_soa.posting_date, getdate(add_days(today(), 7)))
def check_ageing_summary(self, ageing, expected_ageing):
for age_range in expected_ageing:
self.assertEqual(expected_ageing[age_range], ageing.get(age_range))
def tearDown(self):
frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA")
frappe.db.rollback()
def create_process_soa():
frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA")
def create_process_soa(**args):
args = frappe._dict(args)
frappe.delete_doc_if_exists("Process Statement Of Accounts", args.name)
process_soa = frappe.new_doc("Process Statement Of Accounts")
soa_dict = {
"name": "Test Process SOA",
"company": "_Test Company",
}
soa_dict = frappe._dict(
name=args.name,
company=args.company or "_Test Company",
customers=args.customers or [{"customer": "_Test Customer"}],
enable_auto_email=1 if args.enable_auto_email else 0,
frequency=args.frequency or "Weekly",
report=args.report or "General Ledger",
from_date=args.from_date or getdate(today()),
to_date=args.to_date or getdate(today()),
posting_date=args.posting_date or getdate(today()),
include_ageing=1,
)
process_soa.update(soa_dict)
process_soa.set("customers", [{"customer": "_Test Customer"}])
process_soa.enable_auto_email = 1
process_soa.frequency = "Weekly"
process_soa.report = "Accounts Receivable"
process_soa.save()
return process_soa

View File

@@ -59,6 +59,25 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
this.show_stock_ledger();
}
if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) {
this.frm.set_intro(__("Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update."));
this.frm.add_custom_button(__('Repost Accounting Entries'),
() => {
this.frm.call({
doc: this.frm.doc,
method: 'repost_accounting_entries',
freeze: true,
freeze_message: __('Reposting...'),
callback: (r) => {
if (!r.exc) {
frappe.msgprint(__('Accounting Entries are reposted.'));
me.frm.refresh();
}
}
});
}).removeClass('btn-default').addClass('btn-warning');
}
if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){
if(doc.on_hold) {
this.frm.add_custom_button(
@@ -162,6 +181,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
}
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
}
unblock_invoice() {
@@ -460,6 +480,12 @@ cur_frm.set_query("expense_account", "items", function(doc) {
}
});
cur_frm.set_query("wip_composite_asset", "items", function() {
return {
filters: {'is_composite_asset': 1, 'docstatus': 0 }
}
});
cur_frm.cscript.expense_account = function(doc, cdt, cdn){
var d = locals[cdt][cdn];
if(d.idx == 1 && d.expense_account){

View File

@@ -36,6 +36,7 @@
"currency_and_price_list",
"currency",
"conversion_rate",
"use_transaction_date_exchange_rate",
"column_break2",
"buying_price_list",
"price_list_currency",
@@ -166,6 +167,7 @@
"against_expense_account",
"column_break_63",
"unrealized_profit_loss_account",
"repost_required",
"subscription_section",
"auto_repeat",
"update_auto_repeat_reference",
@@ -190,8 +192,7 @@
"inter_company_invoice_reference",
"is_old_subcontracting_flow",
"remarks",
"connections_tab",
"column_break_38"
"connections_tab"
],
"fields": [
{
@@ -987,6 +988,7 @@
"print_hide": 1
},
{
"allow_on_submit": 1,
"fieldname": "cash_bank_account",
"fieldtype": "Link",
"label": "Cash/Bank Account",
@@ -1050,6 +1052,7 @@
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"depends_on": "eval:flt(doc.write_off_amount)!=0",
"fieldname": "write_off_account",
"fieldtype": "Link",
@@ -1213,6 +1216,7 @@
"read_only": 1
},
{
"allow_on_submit": 1,
"default": "No",
"fieldname": "is_opening",
"fieldtype": "Select",
@@ -1345,6 +1349,7 @@
"options": "Project"
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.is_internal_supplier",
"description": "Unrealized Profit/Loss account for intra-company transfers",
"fieldname": "unrealized_profit_loss_account",
@@ -1377,6 +1382,7 @@
"depends_on": "eval:doc.is_subcontracted",
"fieldname": "supplier_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Supplier Warehouse",
"no_copy": 1,
"options": "Warehouse",
@@ -1494,10 +1500,6 @@
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_38",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_50",
"fieldtype": "Column Break"
@@ -1568,13 +1570,29 @@
"fieldname": "use_company_roundoff_cost_center",
"fieldtype": "Check",
"label": "Use Company Default Round Off Cost Center"
},
{
"default": "0",
"fieldname": "repost_required",
"fieldtype": "Check",
"hidden": 1,
"label": "Repost Required",
"options": "Account",
"read_only": 1
},
{
"default": "0",
"fieldname": "use_transaction_date_exchange_rate",
"fieldtype": "Check",
"label": "Use Transaction Date Exchange Rate",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2023-07-04 17:23:59.145031",
"modified": "2023-10-16 16:24:51.886231",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -11,6 +11,9 @@ from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate,
import erpnext
from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
validate_docs_for_deferred_accounting,
)
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
check_if_return_invoice_linked_with_payment_entry,
get_total_in_party_account_currency,
@@ -487,6 +490,11 @@ class PurchaseInvoice(BuyingController):
_("Stock cannot be updated against Purchase Receipt {0}").format(item.purchase_receipt)
)
def validate_for_repost(self):
self.validate_write_off_account()
self.validate_expense_account()
validate_docs_for_deferred_accounting([], [self.name])
def on_submit(self):
super(PurchaseInvoice, self).on_submit()
@@ -529,6 +537,19 @@ class PurchaseInvoice(BuyingController):
self.process_common_party_accounting()
def on_update_after_submit(self):
if hasattr(self, "repost_required"):
fields_to_check = [
"cash_bank_account",
"write_off_account",
"unrealized_profit_loss_account",
]
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
if self.needs_repost:
self.validate_for_repost()
self.db_set("repost_required", self.needs_repost)
def make_gl_entries(self, gl_entries=None, from_repost=False):
if not gl_entries:
gl_entries = self.get_gl_entries()

View File

@@ -5,7 +5,7 @@
import unittest
import frappe
from frappe.tests.utils import change_settings
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cint, flt, getdate, nowdate, today
import erpnext
@@ -33,7 +33,7 @@ test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Templ
test_ignore = ["Serial No"]
class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
@classmethod
def setUpClass(self):
unlink_payment_on_cancel_of_invoice()
@@ -43,6 +43,9 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
def tearDownClass(self):
unlink_payment_on_cancel_of_invoice(0)
def tearDown(self):
frappe.db.rollback()
def test_purchase_invoice_received_qty(self):
"""
1. Test if received qty is validated against accepted + rejected
@@ -417,6 +420,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
self.assertEqual(tax.tax_amount, expected_values[i][1])
self.assertEqual(tax.total, expected_values[i][2])
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_purchase_invoice_with_advance(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
test_records as jv_test_records,
@@ -471,6 +475,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
)
)
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_invoice_with_advance_and_multi_payment_terms(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
test_records as jv_test_records,
@@ -1209,6 +1214,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
acc_settings.submit_journal_entriessubmit_journal_entries = 0
acc_settings.save()
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_gain_loss_with_advance_entry(self):
unlink_enabled = frappe.db.get_value(
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
@@ -1411,6 +1417,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
)
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_purchase_invoice_advance_taxes(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -1796,7 +1803,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
pi = make_purchase_invoice(
company="_Test Company",
customer="_Test Supplier",
do_not_save=True,
do_not_submit=True,
rate=1000,
@@ -1826,6 +1832,32 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
clear_dimension_defaults("Branch")
disable_dimension()
def test_repost_accounting_entries(self):
pi = make_purchase_invoice(
rate=1000,
price_list_rate=1000,
qty=1,
)
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate()],
["Creditors - _TC", 0.0, 1000, nowdate()],
]
check_gl_entries(self, pi.name, expected_gle, nowdate())
pi.items[0].expense_account = "Service - _TC"
pi.save()
pi.load_from_db()
self.assertTrue(pi.repost_required)
pi.repost_accounting_entries()
expected_gle = [
["Creditors - _TC", 0.0, 1000, nowdate()],
["Service - _TC", 1000, 0.0, nowdate()],
]
check_gl_entries(self, pi.name, expected_gle, nowdate())
pi.load_from_db()
self.assertFalse(pi.repost_required)
def check_gl_entries(
doc,

View File

@@ -75,6 +75,7 @@
"manufacturer_part_no",
"accounting",
"expense_account",
"wip_composite_asset",
"col_break5",
"is_fixed_asset",
"asset_location",
@@ -467,6 +468,7 @@
"label": "Accounting"
},
{
"allow_on_submit": 1,
"fieldname": "expense_account",
"fieldtype": "Link",
"label": "Expense Head",
@@ -877,12 +879,18 @@
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply TDS"
},
{
"fieldname": "wip_composite_asset",
"fieldtype": "Link",
"label": "WIP Composite Asset",
"options": "Asset"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2023-07-04 17:22:21.501152",
"modified": "2023-10-03 21:01:01.824892",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
@@ -892,4 +900,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
}

View File

@@ -86,6 +86,7 @@
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"columns": 2,
"fieldname": "account_head",
"fieldtype": "Link",
@@ -97,6 +98,7 @@
"reqd": 1
},
{
"allow_on_submit": 1,
"default": ":Company",
"fieldname": "cost_center",
"fieldtype": "Link",

View File

@@ -55,7 +55,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-07-27 15:47:58.975034",
"modified": "2023-09-26 14:21:27.362567",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Accounting Ledger",
@@ -77,5 +77,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
"states": [],
"track_changes": 1
}

View File

@@ -21,29 +21,8 @@ class RepostAccountingLedger(Document):
def validate_for_deferred_accounting(self):
sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"]
docs_with_deferred_revenue = frappe.db.get_all(
"Sales Invoice Item",
filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
fields=["parent"],
as_list=1,
)
purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"]
docs_with_deferred_expense = frappe.db.get_all(
"Purchase Invoice Item",
filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
fields=["parent"],
as_list=1,
)
if docs_with_deferred_revenue or docs_with_deferred_expense:
frappe.throw(
_("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
frappe.bold(
comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])
)
)
)
validate_docs_for_deferred_accounting(sales_docs, purchase_docs)
def validate_for_closed_fiscal_year(self):
if self.vouchers:
@@ -139,14 +118,17 @@ class RepostAccountingLedger(Document):
return rendered_page
def on_submit(self):
job_name = "repost_accounting_ledger_" + self.name
frappe.enqueue(
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
account_repost_doc=self.name,
is_async=True,
job_name=job_name,
)
frappe.msgprint(_("Repost has started in the background"))
if len(self.vouchers) > 1:
job_name = "repost_accounting_ledger_" + self.name
frappe.enqueue(
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
account_repost_doc=self.name,
is_async=True,
job_name=job_name,
)
frappe.msgprint(_("Repost has started in the background"))
else:
start_repost(self.name)
@frappe.whitelist()
@@ -181,3 +163,26 @@ def start_repost(account_repost_doc=str) -> None:
doc.make_gl_entries()
frappe.db.commit()
def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
docs_with_deferred_revenue = frappe.db.get_all(
"Sales Invoice Item",
filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
fields=["parent"],
as_list=1,
)
docs_with_deferred_expense = frappe.db.get_all(
"Purchase Invoice Item",
filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
fields=["parent"],
as_list=1,
)
if docs_with_deferred_revenue or docs_with_deferred_expense:
frappe.throw(
_("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
frappe.bold(comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue]))
)
)

View File

@@ -99,7 +99,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-11-08 07:38:40.079038",
"modified": "2023-09-26 14:21:35.719727",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Repost Payment Ledger",
@@ -155,5 +155,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
"states": [],
"track_changes": 1
}

View File

@@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"];
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format
@@ -177,8 +177,11 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}, __('Create'));
}
}
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
}
make_maintenance_schedule() {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",

View File

@@ -11,13 +11,13 @@ from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form
import erpnext
from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
get_loyalty_program_details_with_points,
validate_loyalty_points,
)
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
validate_docs_for_deferred_accounting,
)
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
@@ -176,6 +176,12 @@ class SalesInvoice(SellingController):
self.validate_account_for_change_amount()
self.validate_income_account()
def validate_for_repost(self):
self.validate_write_off_account()
self.validate_account_for_change_amount()
self.validate_income_account()
validate_docs_for_deferred_accounting([self.name], [])
def validate_fixed_asset(self):
for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
@@ -401,6 +407,8 @@ class SalesInvoice(SellingController):
"Repost Payment Ledger Items",
"Repost Accounting Ledger",
"Repost Accounting Ledger Items",
"Unreconcile Payments",
"Unreconcile Payment Entries",
"Payment Ledger Entry",
)
@@ -527,89 +535,22 @@ class SalesInvoice(SellingController):
def on_update_after_submit(self):
if hasattr(self, "repost_required"):
needs_repost = 0
# Check if any field affecting accounting entry is altered
doc_before_update = self.get_doc_before_save()
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
# Check if opening entry check updated
if doc_before_update.get("is_opening") != self.is_opening:
needs_repost = 1
if not needs_repost:
# Parent Level Accounts excluding party account
for field in (
"additional_discount_account",
"cash_bank_account",
"account_for_change_amount",
"write_off_account",
"loyalty_redemption_account",
"unrealized_profit_loss_account",
):
if doc_before_update.get(field) != self.get(field):
needs_repost = 1
break
# Check for parent accounting dimensions
for dimension in accounting_dimensions:
if doc_before_update.get(dimension) != self.get(dimension):
needs_repost = 1
break
# Check for child tables
if self.check_if_child_table_updated(
"items",
doc_before_update,
("income_account", "expense_account", "discount_account"),
accounting_dimensions,
):
needs_repost = 1
if self.check_if_child_table_updated(
"taxes", doc_before_update, ("account_head",), accounting_dimensions
):
needs_repost = 1
self.validate_accounts()
# validate if deferred revenue is enabled for any item
# Don't allow to update the invoice if deferred revenue is enabled
for item in self.get("items"):
if item.enable_deferred_revenue:
frappe.throw(
_(
"Deferred Revenue is enabled for item {0}. You cannot update the invoice after submission."
).format(item.item_code)
)
self.db_set("repost_required", needs_repost)
def check_if_child_table_updated(
self, child_table, doc_before_update, fields_to_check, accounting_dimensions
):
# Check if any field affecting accounting entry is altered
for index, item in enumerate(self.get(child_table)):
for field in fields_to_check:
if doc_before_update.get(child_table)[index].get(field) != item.get(field):
return True
for dimension in accounting_dimensions:
if doc_before_update.get(child_table)[index].get(dimension) != item.get(dimension):
return True
return False
@frappe.whitelist()
def repost_accounting_entries(self):
if self.repost_required:
self.docstatus = 2
self.make_gl_entries_on_cancel()
self.docstatus = 1
self.make_gl_entries()
self.db_set("repost_required", 0)
else:
frappe.throw(_("No updates pending for reposting"))
fields_to_check = [
"additional_discount_account",
"cash_bank_account",
"account_for_change_amount",
"write_off_account",
"loyalty_redemption_account",
"unrealized_profit_loss_account",
]
child_tables = {
"items": ("income_account", "expense_account", "discount_account"),
"taxes": ("account_head",),
}
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
if self.needs_repost:
self.validate_for_repost()
self.db_set("repost_required", self.needs_repost)
def set_paid_amount(self):
paid_amount = 0.0

View File

@@ -7,7 +7,7 @@ import unittest
import frappe
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.model.naming import make_autoname
from frappe.tests.utils import change_settings
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, getdate, nowdate, today
import erpnext
@@ -38,13 +38,17 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
class TestSalesInvoice(unittest.TestCase):
class TestSalesInvoice(FrappeTestCase):
def setUp(self):
from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
create_internal_parties()
setup_accounts()
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
def tearDown(self):
frappe.db.rollback()
def make(self):
w = frappe.copy_doc(test_records[0])
@@ -172,6 +176,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertRaises(frappe.LinkExistsError, si.cancel)
unlink_payment_on_cancel_of_invoice()
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_payment_entry_unlink_against_standalone_credit_note(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
@@ -1293,6 +1298,7 @@ class TestSalesInvoice(unittest.TestCase):
dn.submit()
return dn
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
def test_sales_invoice_with_advance(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
test_records as jv_test_records,
@@ -1801,6 +1807,10 @@ class TestSalesInvoice(unittest.TestCase):
)
def test_outstanding_amount_after_advance_payment_entry_cancellation(self):
"""Test impact of advance PE submission/cancellation on SI and SO."""
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
sales_order = make_sales_order(item_code="138-CMS Shoe", qty=1, price_list_rate=500)
pe = frappe.get_doc(
{
"doctype": "Payment Entry",
@@ -1820,10 +1830,25 @@ class TestSalesInvoice(unittest.TestCase):
"paid_to": "_Test Cash - _TC",
}
)
pe.append(
"references",
{
"reference_doctype": "Sales Order",
"reference_name": sales_order.name,
"total_amount": sales_order.grand_total,
"outstanding_amount": sales_order.grand_total,
"allocated_amount": 300,
},
)
pe.insert()
pe.submit()
sales_order.reload()
self.assertEqual(sales_order.advance_paid, 300)
si = frappe.copy_doc(test_records[0])
si.items[0].sales_order = sales_order.name
si.items[0].so_detail = sales_order.get("items")[0].name
si.is_pos = 0
si.append(
"advances",
@@ -1831,6 +1856,7 @@ class TestSalesInvoice(unittest.TestCase):
"doctype": "Sales Invoice Advance",
"reference_type": "Payment Entry",
"reference_name": pe.name,
"reference_row": pe.references[0].name,
"advance_amount": 300,
"allocated_amount": 300,
"remarks": pe.remarks,
@@ -1839,7 +1865,13 @@ class TestSalesInvoice(unittest.TestCase):
si.insert()
si.submit()
si.load_from_db()
si.reload()
pe.reload()
sales_order.reload()
# Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0
self.assertEqual(pe.references[0].reference_name, si.name)
self.assertEqual(sales_order.advance_paid, 0.0)
# check outstanding after advance allocation
self.assertEqual(
@@ -1847,11 +1879,9 @@ class TestSalesInvoice(unittest.TestCase):
flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")),
)
# added to avoid Document has been modified exception
pe = frappe.get_doc("Payment Entry", pe.name)
pe.cancel()
si.reload()
si.load_from_db()
# check outstanding after advance cancellation
self.assertEqual(
flt(si.outstanding_amount),
@@ -2750,6 +2780,13 @@ class TestSalesInvoice(unittest.TestCase):
company="_Test Company",
)
tds_payable_account = create_account(
account_name="TDS Payable",
account_type="Tax",
parent_account="Duties and Taxes - _TC",
company="_Test Company",
)
si = create_sales_invoice(parent_cost_center="Main - _TC", do_not_save=1)
si.apply_discount_on = "Grand Total"
si.additional_discount_account = additional_discount_account
@@ -3048,8 +3085,8 @@ class TestSalesInvoice(unittest.TestCase):
si.commission_rate = commission_rate
self.assertRaises(frappe.ValidationError, si.save)
@change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)})
def test_sales_invoice_submission_post_account_freezing_date(self):
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True)
si.posting_date = add_days(getdate(), 1)
si.save()
@@ -3058,8 +3095,6 @@ class TestSalesInvoice(unittest.TestCase):
si.posting_date = getdate()
si.submit()
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None)
def test_over_billing_case_against_delivery_note(self):
"""
Test a case where duplicating the item with qty = 1 in the invoice
@@ -3088,6 +3123,13 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance)
@change_settings(
"Accounts Settings",
{
"book_deferred_entries_via_journal_entry": 1,
"submit_journal_entries": 1,
},
)
def test_multi_currency_deferred_revenue_via_journal_entry(self):
deferred_account = create_account(
account_name="Deferred Revenue",
@@ -3095,11 +3137,6 @@ class TestSalesInvoice(unittest.TestCase):
company="_Test Company",
)
acc_settings = frappe.get_single("Accounts Settings")
acc_settings.book_deferred_entries_via_journal_entry = 1
acc_settings.submit_journal_entries = 1
acc_settings.save()
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_expense = 1
item.item_defaults[0].deferred_revenue_account = deferred_account
@@ -3165,13 +3202,6 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_gle[i][2], gle.debit)
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
acc_settings = frappe.get_single("Accounts Settings")
acc_settings.book_deferred_entries_via_journal_entry = 0
acc_settings.submit_journal_entries = 0
acc_settings.save()
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None)
def test_standalone_serial_no_return(self):
si = create_sales_invoice(
item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1

View File

@@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils.data import (
add_days,
add_months,
@@ -90,10 +91,14 @@ def create_parties():
customer.insert()
class TestSubscription(unittest.TestCase):
class TestSubscription(FrappeTestCase):
def setUp(self):
create_plan()
create_parties()
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
def tearDown(self):
frappe.db.rollback()
def test_create_subscription_with_trial_with_correct_period(self):
subscription = frappe.new_doc("Subscription")

View File

@@ -0,0 +1,83 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2023-08-22 10:28:10.196712",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"account",
"party_type",
"party",
"reference_doctype",
"reference_name",
"allocated_amount",
"account_currency",
"unlinked"
],
"fields": [
{
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Name",
"options": "reference_doctype"
},
{
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated Amount",
"options": "account_currency"
},
{
"default": "0",
"fieldname": "unlinked",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Unlinked",
"read_only": 1
},
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Reference Type",
"options": "DocType"
},
{
"fieldname": "account",
"fieldtype": "Data",
"label": "Account"
},
{
"fieldname": "party_type",
"fieldtype": "Data",
"label": "Party Type"
},
{
"fieldname": "party",
"fieldtype": "Data",
"label": "Party"
},
{
"fieldname": "account_currency",
"fieldtype": "Link",
"label": "Account Currency",
"options": "Currency",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-09-05 09:33:28.620149",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Unreconcile Payment Entries",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class UnreconcilePaymentEntries(Document):
pass

View File

@@ -0,0 +1,316 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import today
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.test.accounts_mixin import AccountsTestMixin
class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.create_customer()
self.create_usd_receivable_account()
self.create_item()
self.clear_old_entries()
def tearDown(self):
frappe.db.rollback()
def create_sales_invoice(self, do_not_submit=False):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
price_list_rate=100,
do_not_submit=do_not_submit,
)
return si
def create_payment_entry(self):
pe = create_payment_entry(
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer,
paid_from=self.debit_to,
paid_to=self.cash,
paid_amount=200,
save=True,
)
return pe
def test_01_unreconcile_invoice(self):
si1 = self.create_sales_invoice()
si2 = self.create_sales_invoice()
pe = self.create_payment_entry()
pe.append(
"references",
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100},
)
pe.append(
"references",
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100},
)
# Allocation payment against both invoices
pe.save().submit()
# Assert outstanding
[doc.reload() for doc in [si1, si2, pe]]
self.assertEqual(si1.outstanding_amount, 0)
self.assertEqual(si2.outstanding_amount, 0)
self.assertEqual(pe.unallocated_amount, 0)
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payments",
"company": self.company,
"voucher_type": pe.doctype,
"voucher_no": pe.name,
}
)
unreconcile.add_references()
self.assertEqual(len(unreconcile.allocations), 2)
allocations = [x.reference_name for x in unreconcile.allocations]
self.assertEquals([si1.name, si2.name], allocations)
# unreconcile si1
for x in unreconcile.allocations:
if x.reference_name != si1.name:
unreconcile.remove(x)
unreconcile.save().submit()
# Assert outstanding
[doc.reload() for doc in [si1, si2, pe]]
self.assertEqual(si1.outstanding_amount, 100)
self.assertEqual(si2.outstanding_amount, 0)
self.assertEqual(len(pe.references), 1)
self.assertEqual(pe.unallocated_amount, 100)
def test_02_unreconcile_one_payment_from_multi_payments(self):
"""
Scenario: 2 payments, both split against 2 different invoices
Unreconcile only one payment from one invoice
"""
si1 = self.create_sales_invoice()
si2 = self.create_sales_invoice()
pe1 = self.create_payment_entry()
pe1.paid_amount = 100
# Allocate payment against both invoices
pe1.append(
"references",
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
)
pe1.append(
"references",
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
)
pe1.save().submit()
pe2 = self.create_payment_entry()
pe2.paid_amount = 100
# Allocate payment against both invoices
pe2.append(
"references",
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
)
pe2.append(
"references",
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
)
pe2.save().submit()
# Assert outstanding and unallocated
[doc.reload() for doc in [si1, si2, pe1, pe2]]
self.assertEqual(si1.outstanding_amount, 0.0)
self.assertEqual(si2.outstanding_amount, 0.0)
self.assertEqual(pe1.unallocated_amount, 0.0)
self.assertEqual(pe2.unallocated_amount, 0.0)
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payments",
"company": self.company,
"voucher_type": pe2.doctype,
"voucher_no": pe2.name,
}
)
unreconcile.add_references()
self.assertEqual(len(unreconcile.allocations), 2)
allocations = [x.reference_name for x in unreconcile.allocations]
self.assertEquals([si1.name, si2.name], allocations)
# unreconcile si1 from pe2
for x in unreconcile.allocations:
if x.reference_name != si1.name:
unreconcile.remove(x)
unreconcile.save().submit()
# Assert outstanding and unallocated
[doc.reload() for doc in [si1, si2, pe1, pe2]]
self.assertEqual(si1.outstanding_amount, 50)
self.assertEqual(si2.outstanding_amount, 0)
self.assertEqual(len(pe1.references), 2)
self.assertEqual(len(pe2.references), 1)
self.assertEqual(pe1.unallocated_amount, 0)
self.assertEqual(pe2.unallocated_amount, 50)
def test_03_unreconciliation_on_multi_currency_invoice(self):
self.create_customer("_Test MC Customer USD", "USD")
si1 = self.create_sales_invoice(do_not_submit=True)
si1.currency = "USD"
si1.debit_to = self.debtors_usd
si1.conversion_rate = 80
si1.save().submit()
si2 = self.create_sales_invoice(do_not_submit=True)
si2.currency = "USD"
si2.debit_to = self.debtors_usd
si2.conversion_rate = 80
si2.save().submit()
pe = self.create_payment_entry()
pe.paid_from = self.debtors_usd
pe.paid_from_account_currency = "USD"
pe.source_exchange_rate = 75
pe.received_amount = 75 * 200
pe.save()
# Allocate payment against both invoices
pe.append(
"references",
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100},
)
pe.append(
"references",
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100},
)
pe.save().submit()
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payments",
"company": self.company,
"voucher_type": pe.doctype,
"voucher_no": pe.name,
}
)
unreconcile.add_references()
self.assertEqual(len(unreconcile.allocations), 2)
allocations = [x.reference_name for x in unreconcile.allocations]
self.assertEquals([si1.name, si2.name], allocations)
# unreconcile si1 from pe
for x in unreconcile.allocations:
if x.reference_name != si1.name:
unreconcile.remove(x)
unreconcile.save().submit()
# Assert outstanding and unallocated
[doc.reload() for doc in [si1, si2, pe]]
self.assertEqual(si1.outstanding_amount, 100)
self.assertEqual(si2.outstanding_amount, 0)
self.assertEqual(len(pe.references), 1)
self.assertEqual(pe.unallocated_amount, 100)
# Exc gain/loss JE should've been cancelled as well
self.assertEqual(
frappe.db.count(
"Journal Entry Account",
filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1},
),
0,
)
def test_04_unreconciliation_on_multi_currency_invoice(self):
"""
2 payments split against 2 foreign currency invoices
"""
self.create_customer("_Test MC Customer USD", "USD")
si1 = self.create_sales_invoice(do_not_submit=True)
si1.currency = "USD"
si1.debit_to = self.debtors_usd
si1.conversion_rate = 80
si1.save().submit()
si2 = self.create_sales_invoice(do_not_submit=True)
si2.currency = "USD"
si2.debit_to = self.debtors_usd
si2.conversion_rate = 80
si2.save().submit()
pe1 = self.create_payment_entry()
pe1.paid_from = self.debtors_usd
pe1.paid_from_account_currency = "USD"
pe1.source_exchange_rate = 75
pe1.received_amount = 75 * 100
pe1.save()
# Allocate payment against both invoices
pe1.append(
"references",
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
)
pe1.append(
"references",
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
)
pe1.save().submit()
pe2 = self.create_payment_entry()
pe2.paid_from = self.debtors_usd
pe2.paid_from_account_currency = "USD"
pe2.source_exchange_rate = 75
pe2.received_amount = 75 * 100
pe2.save()
# Allocate payment against both invoices
pe2.append(
"references",
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
)
pe2.append(
"references",
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
)
pe2.save().submit()
unreconcile = frappe.get_doc(
{
"doctype": "Unreconcile Payments",
"company": self.company,
"voucher_type": pe2.doctype,
"voucher_no": pe2.name,
}
)
unreconcile.add_references()
self.assertEqual(len(unreconcile.allocations), 2)
allocations = [x.reference_name for x in unreconcile.allocations]
self.assertEquals([si1.name, si2.name], allocations)
# unreconcile si1 from pe2
for x in unreconcile.allocations:
if x.reference_name != si1.name:
unreconcile.remove(x)
unreconcile.save().submit()
# Assert outstanding and unallocated
[doc.reload() for doc in [si1, si2, pe1, pe2]]
self.assertEqual(si1.outstanding_amount, 50)
self.assertEqual(si2.outstanding_amount, 0)
self.assertEqual(len(pe1.references), 2)
self.assertEqual(len(pe2.references), 1)
self.assertEqual(pe1.unallocated_amount, 0)
self.assertEqual(pe2.unallocated_amount, 50)
# Exc gain/loss JE from PE1 should be available
self.assertEqual(
frappe.db.count(
"Journal Entry Account",
filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1},
),
1,
)

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Unreconcile Payments", {
refresh(frm) {
frm.set_query("voucher_type", function() {
return {
filters: {
name: ["in", ["Payment Entry", "Journal Entry"]]
}
}
});
frm.set_query("voucher_no", function(doc) {
return {
filters: {
company: doc.company,
docstatus: 1
}
}
});
},
get_allocations: function(frm) {
frm.clear_table("allocations");
frappe.call({
method: "get_allocations_from_payment",
doc: frm.doc,
callback: function(r) {
if (r.message) {
r.message.forEach(x => {
frm.add_child("allocations", x)
})
frm.refresh_fields();
}
}
})
}
});

View File

@@ -0,0 +1,93 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:UNREC-{#####}",
"creation": "2023-08-22 10:26:34.421423",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"voucher_type",
"voucher_no",
"get_allocations",
"allocations",
"amended_from"
],
"fields": [
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Unreconcile Payments",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"label": "Voucher Type",
"options": "DocType"
},
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"label": "Voucher No",
"options": "voucher_type"
},
{
"fieldname": "get_allocations",
"fieldtype": "Button",
"label": "Get Allocations"
},
{
"fieldname": "allocations",
"fieldtype": "Table",
"label": "Allocations",
"options": "Unreconcile Payment Entries"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-08-28 17:42:50.261377",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Unreconcile Payments",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"read": 1,
"role": "Accounts Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"read": 1,
"role": "Accounts User",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,158 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _, qb
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Abs, Sum
from frappe.utils.data import comma_and
from erpnext.accounts.utils import (
cancel_exchange_gain_loss_journal,
unlink_ref_doc_from_payment_entries,
update_voucher_outstanding,
)
class UnreconcilePayments(Document):
def validate(self):
self.supported_types = ["Payment Entry", "Journal Entry"]
if not self.voucher_type in self.supported_types:
frappe.throw(_("Only {0} are supported").format(comma_and(self.supported_types)))
@frappe.whitelist()
def get_allocations_from_payment(self):
allocated_references = []
ple = qb.DocType("Payment Ledger Entry")
allocated_references = (
qb.from_(ple)
.select(
ple.account,
ple.party_type,
ple.party,
ple.against_voucher_type.as_("reference_doctype"),
ple.against_voucher_no.as_("reference_name"),
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
ple.account_currency,
)
.where(
(ple.docstatus == 1)
& (ple.voucher_type == self.voucher_type)
& (ple.voucher_no == self.voucher_no)
& (ple.voucher_no != ple.against_voucher_no)
)
.groupby(ple.against_voucher_type, ple.against_voucher_no)
.run(as_dict=True)
)
return allocated_references
def add_references(self):
allocations = self.get_allocations_from_payment()
for alloc in allocations:
self.append("allocations", alloc)
def on_submit(self):
# todo: more granular unreconciliation
for alloc in self.allocations:
doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name)
unlink_ref_doc_from_payment_entries(doc, self.voucher_no)
cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no)
update_voucher_outstanding(
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
)
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
@frappe.whitelist()
def doc_has_references(doctype: str = None, docname: str = None):
if doctype in ["Sales Invoice", "Purchase Invoice"]:
return frappe.db.count(
"Payment Ledger Entry",
filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]},
)
else:
return frappe.db.count(
"Payment Ledger Entry",
filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]},
)
@frappe.whitelist()
def get_linked_payments_for_doc(
company: str = None, doctype: str = None, docname: str = None
) -> list:
if company and doctype and docname:
_dt = doctype
_dn = docname
ple = qb.DocType("Payment Ledger Entry")
if _dt in ["Sales Invoice", "Purchase Invoice"]:
criteria = [
(ple.company == company),
(ple.delinked == 0),
(ple.against_voucher_no == _dn),
(ple.amount < 0),
]
res = (
qb.from_(ple)
.select(
ple.company,
ple.voucher_type,
ple.voucher_no,
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
ple.account_currency,
)
.where(Criterion.all(criteria))
.groupby(ple.voucher_no, ple.against_voucher_no)
.having(qb.Field("allocated_amount") > 0)
.run(as_dict=True)
)
return res
else:
criteria = [
(ple.company == company),
(ple.delinked == 0),
(ple.voucher_no == _dn),
(ple.against_voucher_no != _dn),
]
query = (
qb.from_(ple)
.select(
ple.company,
ple.against_voucher_type.as_("voucher_type"),
ple.against_voucher_no.as_("voucher_no"),
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
ple.account_currency,
)
.where(Criterion.all(criteria))
.groupby(ple.against_voucher_no)
)
res = query.run(as_dict=True)
return res
return []
@frappe.whitelist()
def create_unreconcile_doc_for_selection(selections=None):
if selections:
selections = frappe.json.loads(selections)
# assuming each row is a unique voucher
for row in selections:
unrecon = frappe.new_doc("Unreconcile Payments")
unrecon.company = row.get("company")
unrecon.voucher_type = row.get("voucher_type")
unrecon.voucher_no = row.get("voucher_no")
unrecon.add_references()
# remove unselected references
unrecon.allocations = [
x
for x in unrecon.allocations
if x.reference_doctype == row.get("against_voucher_type")
and x.reference_name == row.get("against_voucher_no")
]
unrecon.save().submit()

View File

@@ -6,12 +6,7 @@ from typing import Optional
import frappe
from frappe import _, msgprint, scrub
from frappe.contacts.doctype.address.address import (
get_address_display,
get_company_address,
get_default_address,
)
from frappe.contacts.doctype.contact.contact import get_contact_details
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Abs, Date, Sum
@@ -133,6 +128,7 @@ def _get_party_details(
party_address,
company_address,
shipping_address,
ignore_permissions=ignore_permissions,
)
set_contact_details(party_details, party, party_type)
set_other_values(party_details, party, party_type)
@@ -193,6 +189,8 @@ def set_address_details(
party_address=None,
company_address=None,
shipping_address=None,
*,
ignore_permissions=False
):
billing_address_field = (
"customer_address" if party_type == "Lead" else party_type.lower() + "_address"
@@ -205,13 +203,17 @@ def set_address_details(
get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])
)
# address display
party_details.address_display = get_address_display(party_details[billing_address_field])
party_details.address_display = render_address(
party_details[billing_address_field], check_permissions=not ignore_permissions
)
# shipping address
if party_type in ["Customer", "Lead"]:
party_details.shipping_address_name = shipping_address or get_party_shipping_address(
party_type, party.name
)
party_details.shipping_address = get_address_display(party_details["shipping_address_name"])
party_details.shipping_address = render_address(
party_details["shipping_address_name"], check_permissions=not ignore_permissions
)
if doctype:
party_details.update(
get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name)
@@ -229,7 +231,7 @@ def set_address_details(
if shipping_address:
party_details.update(
shipping_address=shipping_address,
shipping_address_display=get_address_display(shipping_address),
shipping_address_display=render_address(shipping_address),
**get_fetch_values(doctype, "shipping_address", shipping_address)
)
@@ -238,7 +240,8 @@ def set_address_details(
party_details.update(
billing_address=party_details.company_address,
billing_address_display=(
party_details.company_address_display or get_address_display(party_details.company_address)
party_details.company_address_display
or render_address(party_details.company_address, check_permissions=False)
),
**get_fetch_values(doctype, "billing_address", party_details.company_address)
)
@@ -290,7 +293,34 @@ def set_contact_details(party_details, party, party_type):
}
)
else:
party_details.update(get_contact_details(party_details.contact_person))
fields = [
"name as contact_person",
"salutation",
"first_name",
"last_name",
"email_id as contact_email",
"mobile_no as contact_mobile",
"phone as contact_phone",
"designation as contact_designation",
"department as contact_department",
]
contact_details = frappe.db.get_value(
"Contact", party_details.contact_person, fields, as_dict=True
)
contact_details.contact_display = " ".join(
filter(
None,
[
contact_details.get("salutation"),
contact_details.get("first_name"),
contact_details.get("last_name"),
],
)
)
party_details.update(contact_details)
def set_other_values(party_details, party, party_type):
@@ -957,3 +987,13 @@ def add_party_account(party_type, party, company, account):
doc.append("accounts", accounts)
doc.save()
def render_address(address, check_permissions=True):
try:
from frappe.contacts.doctype.address.address import render_address as _render
except ImportError:
# Older frappe versions where this function is not available
from frappe.contacts.doctype.address.address import get_address_display as _render
return frappe.call(_render, address, check_permissions=check_permissions)

View File

@@ -95,30 +95,27 @@ frappe.query_reports["Accounts Payable"] = {
"options": "Payment Terms Template"
},
{
"fieldname": "party_type",
"fieldname":"party_type",
"label": __("Party Type"),
"fieldtype": "Link",
"options": "Party Type",
get_query: () => {
return {
filters: {
'account_type': 'Payable'
}
};
},
on_change: () => {
"fieldtype": "Autocomplete",
options: get_party_type_options(),
on_change: function() {
frappe.query_report.set_filter_value('party', "");
let party_type = frappe.query_report.get_filter_value('party_type');
frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier");
}
},
{
"fieldname":"party",
"label": __("Party"),
"fieldtype": "Dynamic Link",
"options": "party_type",
"fieldtype": "MultiSelectList",
get_data: function(txt) {
if (!frappe.query_report.filters) return;
let party_type = frappe.query_report.get_filter_value('party_type');
if (!party_type) return;
return frappe.db.get_link_options(party_type, txt);
},
},
{
"fieldname": "supplier_group",
@@ -167,3 +164,15 @@ frappe.query_reports["Accounts Payable"] = {
}
erpnext.utils.add_dimensions('Accounts Payable', 9);
function get_party_type_options() {
let options = [];
frappe.db.get_list(
"Party Type", {filters:{"account_type": "Payable"}, fields:['name']}
).then((res) => {
res.forEach((party_type) => {
options.push(party_type.name);
});
});
return options;
}

View File

@@ -24,7 +24,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
def test_accounts_receivable_with_supplier(self):
def test_accounts_payable_for_foreign_currency_supplier(self):
pi = self.create_purchase_invoice(do_not_submit=True)
pi.currency = "USD"
pi.conversion_rate = 80
@@ -34,7 +34,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
filters = {
"company": self.company,
"party_type": "Supplier",
"party": self.supplier,
"party": [self.supplier],
"report_date": today(),
"range1": 30,
"range2": 60,

View File

@@ -72,10 +72,27 @@ frappe.query_reports["Accounts Payable Summary"] = {
}
},
{
"fieldname":"supplier",
"label": __("Supplier"),
"fieldtype": "Link",
"options": "Supplier"
"fieldname":"party_type",
"label": __("Party Type"),
"fieldtype": "Autocomplete",
options: get_party_type_options(),
on_change: function() {
frappe.query_report.set_filter_value('party', "");
frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier");
}
},
{
"fieldname":"party",
"label": __("Party"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
if (!frappe.query_report.filters) return;
let party_type = frappe.query_report.get_filter_value('party_type');
if (!party_type) return;
return frappe.db.get_link_options(party_type, txt);
},
},
{
"fieldname":"payment_terms_template",
@@ -105,3 +122,15 @@ frappe.query_reports["Accounts Payable Summary"] = {
}
erpnext.utils.add_dimensions('Accounts Payable Summary', 9);
function get_party_type_options() {
let options = [];
frappe.db.get_list(
"Party Type", {filters:{"account_type": "Payable"}, fields:['name']}
).then((res) => {
res.forEach((party_type) => {
options.push(party_type.name);
});
});
return options;
}

View File

@@ -1,6 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.utils");
frappe.query_reports["Accounts Receivable"] = {
"filters": [
{
@@ -38,32 +40,28 @@ frappe.query_reports["Accounts Receivable"] = {
}
},
{
"fieldname": "customer",
"label": __("Customer"),
"fieldtype": "Link",
"options": "Customer",
on_change: () => {
var customer = frappe.query_report.get_filter_value('customer');
var company = frappe.query_report.get_filter_value('company');
if (customer) {
frappe.db.get_value('Customer', customer, ["customer_name", "payment_terms"], function(value) {
frappe.query_report.set_filter_value('customer_name', value["customer_name"]);
frappe.query_report.set_filter_value('payment_terms', value["payment_terms"]);
});
frappe.db.get_value('Customer Credit Limit', {'parent': customer, 'company': company},
["credit_limit"], function(value) {
if (value) {
frappe.query_report.set_filter_value('credit_limit', value["credit_limit"]);
}
}, "Customer");
} else {
frappe.query_report.set_filter_value('customer_name', "");
frappe.query_report.set_filter_value('credit_limit', "");
frappe.query_report.set_filter_value('payment_terms', "");
}
"fieldname":"party_type",
"label": __("Party Type"),
"fieldtype": "Autocomplete",
options: get_party_type_options(),
on_change: function() {
frappe.query_report.set_filter_value('party', "");
frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer");
}
},
{
"fieldname":"party",
"label": __("Party"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
if (!frappe.query_report.filters) return;
let party_type = frappe.query_report.get_filter_value('party_type');
if (!party_type) return;
return frappe.db.get_link_options(party_type, txt);
},
},
{
"fieldname": "party_account",
"label": __("Receivable Account"),
@@ -174,24 +172,6 @@ frappe.query_reports["Accounts Receivable"] = {
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check",
},
{
"fieldname": "customer_name",
"label": __("Customer Name"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname": "payment_terms",
"label": __("Payment Tems"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname": "credit_limit",
"label": __("Credit Limit"),
"fieldtype": "Currency",
"hidden": 1
}
],
@@ -213,3 +193,16 @@ frappe.query_reports["Accounts Receivable"] = {
}
erpnext.utils.add_dimensions('Accounts Receivable', 9);
function get_party_type_options() {
let options = [];
frappe.db.get_list(
"Party Type", {filters:{"account_type": "Receivable"}, fields:['name']}
).then((res) => {
res.forEach((party_type) => {
options.push(party_type.name);
});
});
return options;
}

View File

@@ -116,7 +116,7 @@ class ReceivablePayableReport(object):
# build all keys, since we want to exclude vouchers beyond the report date
for ple in self.ple_entries:
# get the balance object for voucher_type
key = (ple.voucher_type, ple.voucher_no, ple.party)
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
if not key in self.voucher_balance:
self.voucher_balance[key] = frappe._dict(
voucher_type=ple.voucher_type,
@@ -183,7 +183,7 @@ class ReceivablePayableReport(object):
):
return
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)
# If payment is made against credit note
# and credit note is made against a Sales Invoice
@@ -192,13 +192,13 @@ class ReceivablePayableReport(object):
if ple.against_voucher_no in self.return_entries:
return_against = self.return_entries.get(ple.against_voucher_no)
if return_against:
key = (ple.against_voucher_type, return_against, ple.party)
key = (ple.account, ple.against_voucher_type, return_against, ple.party)
row = self.voucher_balance.get(key)
if not row:
# no invoice, this is an invoice / stand-alone payment / credit note
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party))
row.party_type = ple.party_type
return row
@@ -769,15 +769,12 @@ class ReceivablePayableReport(object):
self.or_filters = []
for party_type in self.party_type:
party_type_field = scrub(party_type)
self.or_filters.append(self.ple.party_type == party_type)
self.add_common_filters()
self.add_common_filters(party_type_field=party_type_field)
if party_type_field == "customer":
if self.account_type == "Receivable":
self.add_customer_filters()
elif party_type_field == "supplier":
elif self.account_type == "Payable":
self.add_supplier_filters()
if self.filters.cost_center:
@@ -793,21 +790,18 @@ class ReceivablePayableReport(object):
]
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
def add_common_filters(self, party_type_field):
def add_common_filters(self):
if self.filters.company:
self.qb_selection_filter.append(self.ple.company == self.filters.company)
if self.filters.finance_book:
self.qb_selection_filter.append(self.ple.finance_book == self.filters.finance_book)
if self.filters.get(party_type_field):
self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field))
if self.filters.get("party_type"):
self.qb_selection_filter.append(self.filters.party_type == self.ple.party_type)
if self.filters.get("party"):
self.qb_selection_filter.append(self.filters.party == self.ple.party)
self.qb_selection_filter.append(self.ple.party.isin(self.filters.party))
if self.filters.party_account:
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
@@ -969,6 +963,20 @@ class ReceivablePayableReport(object):
fieldtype="Link",
options="Contact",
)
if self.filters.party_type == "Customer":
self.add_column(
_("Customer Name"),
fieldname="customer_name",
fieldtype="Link",
options="Customer",
)
elif self.filters.party_type == "Supplier":
self.add_column(
_("Supplier Name"),
fieldname="supplier_name",
fieldtype="Link",
options="Supplier",
)
self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data")
self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data")

View File

@@ -1,6 +1,7 @@
import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, getdate, today
@@ -23,29 +24,6 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
def create_usd_account(self):
name = "Debtors USD"
exists = frappe.db.get_list(
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"}
)
if exists:
self.debtors_usd = exists[0].name
else:
debtors = frappe.get_doc(
"Account",
frappe.db.get_list(
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors"}
)[0].name,
)
debtors_usd = frappe.new_doc("Account")
debtors_usd.company = debtors.company
debtors_usd.account_name = "Debtors USD"
debtors_usd.account_currency = "USD"
debtors_usd.parent_account = debtors.parent_account
debtors_usd.account_type = debtors.account_type
self.debtors_usd = debtors_usd.save().name
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
si = create_sales_invoice(
@@ -568,3 +546,169 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
row.account_currency,
],
)
def test_usd_customer_filter(self):
filters = {
"company": self.company,
"party_type": "Customer",
"party": [self.customer],
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.currency = "USD"
si.conversion_rate = 80
si.debit_to = self.debtors_usd
si.save().submit()
name = si.name
# check invoice grand total and invoiced column's value for 3 payment terms
report = execute(filters)
expected = {
"voucher_type": si.doctype,
"voucher_no": si.name,
"party_account": self.debtors_usd,
"customer_name": self.customer,
"invoiced": 100.0,
"outstanding": 100.0,
"account_currency": "USD",
}
self.assertEqual(len(report[1]), 1)
report_output = report[1][0]
for field in expected:
with self.subTest(field=field):
self.assertEqual(report_output.get(field), expected.get(field))
def test_multi_select_party_filter(self):
self.customer1 = self.customer
self.create_customer("_Test Customer 2")
self.customer2 = self.customer
self.create_customer("_Test Customer 3")
self.customer3 = self.customer
filters = {
"company": self.company,
"party_type": "Customer",
"party": [self.customer1, self.customer3],
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
si1 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si1.customer = self.customer1
si1.save().submit()
si2 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si2.customer = self.customer2
si2.save().submit()
si3 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si3.customer = self.customer3
si3.save().submit()
# check invoice grand total and invoiced column's value for 3 payment terms
report = execute(filters)
expected_output = {self.customer1, self.customer3}
self.assertEqual(len(report[1]), 2)
output_for = set([x.party for x in report[1]])
self.assertEqual(output_for, expected_output)
def test_report_output_if_party_is_missing(self):
acc_name = "Additional Debtors"
if not frappe.db.get_value(
"Account", filters={"account_name": acc_name, "company": self.company}
):
additional_receivable_acc = frappe.get_doc(
{
"doctype": "Account",
"account_name": acc_name,
"parent_account": "Accounts Receivable - " + self.company_abbr,
"company": self.company,
"account_type": "Receivable",
}
).save()
self.debtors2 = additional_receivable_acc.name
je = frappe.new_doc("Journal Entry")
je.company = self.company
je.posting_date = today()
je.append(
"accounts",
{
"account": self.debit_to,
"party_type": "Customer",
"party": self.customer,
"debit_in_account_currency": 150,
"credit_in_account_currency": 0,
"cost_center": self.cost_center,
},
)
je.append(
"accounts",
{
"account": self.debtors2,
"party_type": "Customer",
"party": self.customer,
"debit_in_account_currency": 200,
"credit_in_account_currency": 0,
"cost_center": self.cost_center,
},
)
je.append(
"accounts",
{
"account": self.cash,
"debit_in_account_currency": 0,
"credit_in_account_currency": 350,
"cost_center": self.cost_center,
},
)
je.save().submit()
# manually remove party from Payment Ledger
ple = qb.DocType("Payment Ledger Entry")
qb.update(ple).set(ple.party, None).where(ple.voucher_no == je.name).run()
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
report_ouput = execute(filters)[1]
expected_data = [
[self.debtors2, je.doctype, je.name, "Customer", self.customer, 200.0, 0.0, 0.0, 200.0],
[self.debit_to, je.doctype, je.name, "Customer", self.customer, 150.0, 0.0, 0.0, 150.0],
]
self.assertEqual(len(report_ouput), 2)
# fetch only required fields
report_output = [
[
x.party_account,
x.voucher_type,
x.voucher_no,
"Customer",
self.customer,
x.invoiced,
x.paid,
x.credit_note,
x.outstanding,
]
for x in report_ouput
]
# use account name to sort
# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
report_output = sorted(report_output, key=lambda x: x[0])
self.assertEqual(expected_data, report_output)

View File

@@ -72,10 +72,27 @@ frappe.query_reports["Accounts Receivable Summary"] = {
}
},
{
"fieldname":"customer",
"label": __("Customer"),
"fieldtype": "Link",
"options": "Customer"
"fieldname":"party_type",
"label": __("Party Type"),
"fieldtype": "Autocomplete",
options: get_party_type_options(),
on_change: function() {
frappe.query_report.set_filter_value('party', "");
frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer");
}
},
{
"fieldname":"party",
"label": __("Party"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
if (!frappe.query_report.filters) return;
let party_type = frappe.query_report.get_filter_value('party_type');
if (!party_type) return;
return frappe.db.get_link_options(party_type, txt);
},
},
{
"fieldname":"customer_group",
@@ -133,3 +150,15 @@ frappe.query_reports["Accounts Receivable Summary"] = {
}
erpnext.utils.add_dimensions('Accounts Receivable Summary', 9);
function get_party_type_options() {
let options = [];
frappe.db.get_list(
"Party Type", {filters:{"account_type": "Receivable"}, fields:['name']}
).then((res) => {
res.forEach((party_type) => {
options.push(party_type.name);
});
});
return options;
}

View File

@@ -99,13 +99,11 @@ class AccountsReceivableSummary(ReceivablePayableReport):
# Add all amount columns
for k in list(self.party_total[d.party]):
if k not in ["currency", "sales_person"]:
self.party_total[d.party][k] += d.get(k, 0.0)
if isinstance(self.party_total[d.party][k], float):
self.party_total[d.party][k] += d.get(k) or 0.0
# set territory, customer_group, sales person etc
self.set_party_details(d)
self.party_total[d.party].update({"party_type": d.party_type})
def init_party_total(self, row):
self.party_total.setdefault(
@@ -124,6 +122,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
"total_due": 0.0,
"future_amount": 0.0,
"sales_person": [],
"party_type": row.party_type,
}
),
)
@@ -133,13 +132,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
for key in ("territory", "customer_group", "supplier_group"):
if row.get(key):
self.party_total[row.party][key] = row.get(key)
self.party_total[row.party][key] = row.get(key, "")
if row.sales_person:
self.party_total[row.party].sales_person.append(row.sales_person)
self.party_total[row.party].sales_person.append(row.get("sales_person", ""))
if self.filters.sales_partner:
self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner")
self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner", "")
def get_columns(self):
self.columns = []

View File

@@ -23,6 +23,7 @@ class TestBankReconciliationStatement(FrappeTestCase):
"Payment Entry",
]:
frappe.db.delete(dt)
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
def test_loan_entries_in_bank_reco_statement(self):
create_loan_accounts()

View File

@@ -133,15 +133,17 @@ class General_Payment_Ledger_Comparison(object):
self.gle_balances = set(val.gle) | self.gle_balances
self.ple_balances = set(val.ple) | self.ple_balances
self.diff1 = self.gle_balances.difference(self.ple_balances)
self.diff2 = self.ple_balances.difference(self.gle_balances)
self.variation_in_payment_ledger = self.gle_balances.difference(self.ple_balances)
self.variation_in_general_ledger = self.ple_balances.difference(self.gle_balances)
self.diff = frappe._dict({})
for x in self.diff1:
for x in self.variation_in_payment_ledger:
self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
for x in self.diff2:
self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]}))
for x in self.variation_in_general_ledger:
self.diff.setdefault((x[0], x[1], x[2], x[3]), frappe._dict({"gl_balance": 0.0})).update(
frappe._dict({"pl_balance": x[4]})
)
def generate_data(self):
self.data = []

View File

@@ -544,6 +544,8 @@ class GrossProfitGenerator(object):
new_row.qty += flt(row.qty)
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
new_row.base_amount += flt(row.base_amount, self.currency_precision)
if self.filters.get("group_by") == "Sales Person":
new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision)
new_row = self.set_average_rate(new_row)
self.grouped_data.append(new_row)

View File

@@ -68,7 +68,11 @@ def get_result(
tax_amount += entry.credit - entry.debit
if net_total_map.get(name):
total_amount, grand_total, base_total = net_total_map.get(name)
if voucher_type == "Journal Entry":
# back calcalute total amount from rate and tax_amount
total_amount = grand_total = base_total = tax_amount / (rate / 100)
else:
total_amount, grand_total, base_total = net_total_map.get(name)
else:
total_amount += entry.credit

View File

@@ -99,6 +99,12 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"label": __("Include Default Book Entries"),
"fieldtype": "Check",
"default": 1
},
{
"fieldname": "show_net_values",
"label": __("Show net values in opening and closing columns"),
"fieldtype": "Check",
"default": 1
}
],
"formatter": erpnext.financial_statements.formatter,

View File

@@ -120,7 +120,9 @@ def get_data(filters):
ignore_opening_entries=True,
)
calculate_values(accounts, gl_entries_by_account, opening_balances)
calculate_values(
accounts, gl_entries_by_account, opening_balances, filters.get("show_net_values")
)
accumulate_values_into_parents(accounts, accounts_by_name)
data = prepare_data(accounts, filters, parent_children_map, company_currency)
@@ -310,7 +312,7 @@ def get_opening_balance(
return gle
def calculate_values(accounts, gl_entries_by_account, opening_balances):
def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net_values):
init = {
"opening_debit": 0.0,
"opening_credit": 0.0,
@@ -335,7 +337,8 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances):
d["closing_debit"] = d["opening_debit"] + d["debit"]
d["closing_credit"] = d["opening_credit"] + d["credit"]
prepare_opening_closing(d)
if show_net_values:
prepare_opening_closing(d)
def calculate_total_row(accounts, company_currency):
@@ -375,7 +378,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
for d in accounts:
# Prepare opening closing for group account
if parent_children_map.get(d.account):
if parent_children_map.get(d.account) and filters.get("show_net_values"):
prepare_opening_closing(d)
has_value = False

View File

@@ -46,6 +46,7 @@ def get_data(filters):
.select(
gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit")
)
.where(gle.is_cancelled == 0)
.groupby(gle.voucher_no)
)
query = apply_filters(query, filters, gle)

View File

@@ -560,6 +560,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
"""
jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they might be getting unlinked
if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"):
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
# adjust the unreconciled balance
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
@@ -625,6 +629,13 @@ def update_reference_in_payment_entry(
if d.voucher_detail_no:
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
# Update Advance Paid in SO/PO since they are getting unlinked
if existing_row.get("reference_doctype") in ("Sales Order", "Purchase Order"):
frappe.get_doc(
existing_row.reference_doctype, existing_row.reference_name
).set_total_advance_paid()
original_row = existing_row.as_dict().copy()
existing_row.update(reference_details)
@@ -652,7 +663,9 @@ def update_reference_in_payment_entry(
payment_entry.save(ignore_permissions=True)
def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None:
def cancel_exchange_gain_loss_journal(
parent_doc: dict | object, referenced_dt: str = None, referenced_dn: str = None
) -> None:
"""
Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any.
"""
@@ -679,76 +692,147 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None:
as_list=1,
)
for doc in gain_loss_journals:
frappe.get_doc("Journal Entry", doc[0]).cancel()
gain_loss_je = frappe.get_doc("Journal Entry", doc[0])
if referenced_dt and referenced_dn:
references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
if (
len(references) == 2
and (referenced_dt, referenced_dn) in references
and (parent_doc.doctype, parent_doc.name) in references
):
# only cancel JE generated against parent_doc and referenced_dn
gain_loss_je.cancel()
else:
gain_loss_je.cancel()
def unlink_ref_doc_from_payment_entries(ref_doc):
remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name)
remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name)
frappe.db.sql(
"""update `tabGL Entry`
set against_voucher_type=null, against_voucher=null,
modified=%s, modified_by=%s
where against_voucher_type=%s and against_voucher=%s
and voucher_no != ifnull(against_voucher, '')""",
(now(), frappe.session.user, ref_doc.doctype, ref_doc.name),
def update_accounting_ledgers_after_reference_removal(
ref_type: str = None, ref_no: str = None, payment_name: str = None
):
# General Ledger
gle = qb.DocType("GL Entry")
gle_update_query = (
qb.update(gle)
.set(gle.against_voucher_type, None)
.set(gle.against_voucher, None)
.set(gle.modified, now())
.set(gle.modified_by, frappe.session.user)
.where((gle.against_voucher_type == ref_type) & (gle.against_voucher == ref_no))
)
if payment_name:
gle_update_query = gle_update_query.where(gle.voucher_no == payment_name)
gle_update_query.run()
# Payment Ledger
ple = qb.DocType("Payment Ledger Entry")
ple_update_query = (
qb.update(ple)
.set(ple.against_voucher_type, ple.voucher_type)
.set(ple.against_voucher_no, ple.voucher_no)
.set(ple.modified, now())
.set(ple.modified_by, frappe.session.user)
.where(
(ple.against_voucher_type == ref_type)
& (ple.against_voucher_no == ref_no)
& (ple.delinked == 0)
)
)
qb.update(ple).set(ple.against_voucher_type, ple.voucher_type).set(
ple.against_voucher_no, ple.voucher_no
).set(ple.modified, now()).set(ple.modified_by, frappe.session.user).where(
(ple.against_voucher_type == ref_doc.doctype)
& (ple.against_voucher_no == ref_doc.name)
& (ple.delinked == 0)
).run()
if payment_name:
ple_update_query = ple_update_query.where(ple.voucher_no == payment_name)
ple_update_query.run()
def remove_ref_from_advance_section(ref_doc: object = None):
# TODO: this might need some testing
if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"):
ref_doc.set("advances", [])
frappe.db.sql(
"""delete from `tab{0} Advance` where parent = %s""".format(ref_doc.doctype), ref_doc.name
)
adv_type = qb.DocType(f"{ref_doc.doctype} Advance")
qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run()
def remove_ref_doc_link_from_jv(ref_type, ref_no):
linked_jv = frappe.db.sql_list(
"""select parent from `tabJournal Entry Account`
where reference_type=%s and reference_name=%s and docstatus < 2""",
(ref_type, ref_no),
def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str = None):
remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name, payment_name)
remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name, payment_name)
update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name, payment_name)
remove_ref_from_advance_section(ref_doc)
def remove_ref_doc_link_from_jv(
ref_type: str = None, ref_no: str = None, payment_name: str = None
):
jea = qb.DocType("Journal Entry Account")
linked_jv = (
qb.from_(jea)
.select(jea.parent)
.where((jea.reference_type == ref_type) & (jea.reference_name == ref_no) & (jea.docstatus.lt(2)))
.run(as_list=1)
)
linked_jv = convert_to_list(linked_jv)
# remove reference only from specified payment
linked_jv = [x for x in linked_jv if x == payment_name] if payment_name else linked_jv
if linked_jv:
frappe.db.sql(
"""update `tabJournal Entry Account`
set reference_type=null, reference_name = null,
modified=%s, modified_by=%s
where reference_type=%s and reference_name=%s
and docstatus < 2""",
(now(), frappe.session.user, ref_type, ref_no),
update_query = (
qb.update(jea)
.set(jea.reference_type, None)
.set(jea.reference_name, None)
.set(jea.modified, now())
.set(jea.modified_by, frappe.session.user)
.where((jea.reference_type == ref_type) & (jea.reference_name == ref_no))
)
if payment_name:
update_query = update_query.where(jea.parent == payment_name)
update_query.run()
frappe.msgprint(_("Journal Entries {0} are un-linked").format("\n".join(linked_jv)))
def remove_ref_doc_link_from_pe(ref_type, ref_no):
linked_pe = frappe.db.sql_list(
"""select parent from `tabPayment Entry Reference`
where reference_doctype=%s and reference_name=%s and docstatus < 2""",
(ref_type, ref_no),
def convert_to_list(result):
"""
Convert tuple to list
"""
return [x[0] for x in result]
def remove_ref_doc_link_from_pe(
ref_type: str = None, ref_no: str = None, payment_name: str = None
):
per = qb.DocType("Payment Entry Reference")
pay = qb.DocType("Payment Entry")
linked_pe = (
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:
frappe.db.sql(
"""update `tabPayment Entry Reference`
set allocated_amount=0, modified=%s, modified_by=%s
where reference_doctype=%s and reference_name=%s
and docstatus < 2""",
(now(), frappe.session.user, ref_type, ref_no),
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))
)
)
if payment_name:
update_query = update_query.where(per.parent == payment_name)
update_query.run()
for pe in linked_pe:
try:
pe_doc = frappe.get_doc("Payment Entry", pe)
@@ -761,19 +845,13 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no):
msg += _("Please cancel payment entry manually first")
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
frappe.db.sql(
"""update `tabPayment Entry` set total_allocated_amount=%s,
base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s
where name=%s""",
(
pe_doc.total_allocated_amount,
pe_doc.base_total_allocated_amount,
pe_doc.unallocated_amount,
now(),
frappe.session.user,
pe,
),
)
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)))

View File

@@ -147,6 +147,15 @@ frappe.ui.form.on('Asset', {
if (frm.doc.docstatus == 0) {
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
if (frm.doc.is_composite_asset && !frm.doc.capitalized_in) {
$('.primary-action').prop('hidden', true);
$('.form-message').text('Capitalize this asset to confirm');
frm.add_custom_button(__("Capitalize Asset"), function() {
frm.trigger("create_asset_capitalization");
});
}
}
},
@@ -168,7 +177,7 @@ frappe.ui.form.on('Asset', {
frm.set_df_property('purchase_invoice', 'read_only', 1);
frm.set_df_property('purchase_receipt', 'read_only', 1);
}
else if (frm.doc.is_existing_asset) {
else if (frm.doc.is_existing_asset || frm.doc.is_composite_asset) {
frm.toggle_reqd('purchase_receipt', 0);
frm.toggle_reqd('purchase_invoice', 0);
}
@@ -275,7 +284,7 @@ frappe.ui.form.on('Asset', {
item_code: function(frm) {
if(frm.doc.item_code && frm.doc.calculate_depreciation) {
if(frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) {
frm.trigger('set_finance_book');
} else {
frm.set_value('finance_books', []);
@@ -287,7 +296,8 @@ frappe.ui.form.on('Asset', {
method: "erpnext.assets.doctype.asset.asset.get_item_details",
args: {
item_code: frm.doc.item_code,
asset_category: frm.doc.asset_category
asset_category: frm.doc.asset_category,
gross_purchase_amount: frm.doc.gross_purchase_amount
},
callback: function(r, rt) {
if(r.message) {
@@ -299,7 +309,17 @@ frappe.ui.form.on('Asset', {
is_existing_asset: function(frm) {
frm.trigger("toggle_reference_doc");
// frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation));
},
is_composite_asset: function(frm) {
if(frm.doc.is_composite_asset) {
frm.set_value('gross_purchase_amount', 0);
frm.set_df_property('gross_purchase_amount', 'read_only', 1);
} else {
frm.set_df_property('gross_purchase_amount', 'read_only', 0);
}
frm.trigger("toggle_reference_doc");
},
make_schedules_editable: function(frm) {
@@ -360,6 +380,19 @@ frappe.ui.form.on('Asset', {
});
},
create_asset_capitalization: function(frm) {
frappe.call({
args: {
"asset": frm.doc.name,
},
method: "erpnext.assets.doctype.asset.asset.create_asset_capitalization",
callback: function(r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
});
},
split_asset: function(frm) {
const title = __('Split Asset');
@@ -415,7 +448,7 @@ frappe.ui.form.on('Asset', {
calculate_depreciation: function(frm) {
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
if (frm.doc.item_code && frm.doc.calculate_depreciation ) {
if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) {
frm.trigger("set_finance_book");
} else {
frm.set_value("finance_books", []);
@@ -423,9 +456,11 @@ frappe.ui.form.on('Asset', {
},
gross_purchase_amount: function(frm) {
frm.doc.finance_books.forEach(d => {
frm.events.set_depreciation_rate(frm, d);
})
if (frm.doc.finance_books) {
frm.doc.finance_books.forEach(d => {
frm.events.set_depreciation_rate(frm, d);
})
}
},
purchase_receipt: (frm) => {
@@ -504,7 +539,21 @@ frappe.ui.form.on('Asset', {
}
});
}
}
},
set_salvage_value_percentage_or_expected_value_after_useful_life: function(frm, row, salvage_value_percentage_changed, expected_value_after_useful_life_changed) {
if (expected_value_after_useful_life_changed) {
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = true;
const new_salvage_value_percentage = flt((row.expected_value_after_useful_life * 100) / frm.doc.gross_purchase_amount, precision("salvage_value_percentage", row));
frappe.model.set_value(row.doctype, row.name, "salvage_value_percentage", new_salvage_value_percentage);
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = false;
} else if (salvage_value_percentage_changed) {
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = true;
const new_expected_value_after_useful_life = flt(frm.doc.gross_purchase_amount * (row.salvage_value_percentage / 100), precision('gross_purchase_amount'));
frappe.model.set_value(row.doctype, row.name, "expected_value_after_useful_life", new_expected_value_after_useful_life);
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = false;
}
},
});
frappe.ui.form.on('Asset Finance Book', {
@@ -516,9 +565,19 @@ frappe.ui.form.on('Asset Finance Book', {
expected_value_after_useful_life: function(frm, cdt, cdn) {
const row = locals[cdt][cdn];
if (!frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life) {
frm.events.set_salvage_value_percentage_or_expected_value_after_useful_life(frm, row, false, true);
}
frm.events.set_depreciation_rate(frm, row);
},
salvage_value_percentage: function(frm, cdt, cdn) {
const row = locals[cdt][cdn];
if (!frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life) {
frm.events.set_salvage_value_percentage_or_expected_value_after_useful_life(frm, row, true, false);
}
},
frequency_of_depreciation: function(frm, cdt, cdn) {
const row = locals[cdt][cdn];
frm.events.set_depreciation_rate(frm, row);

View File

@@ -14,6 +14,7 @@
"asset_owner",
"asset_owner_company",
"is_existing_asset",
"is_composite_asset",
"supplier",
"customer",
"image",
@@ -72,7 +73,8 @@
"purchase_receipt_amount",
"default_finance_book",
"depr_entry_posting_status",
"amended_from"
"amended_from",
"capitalized_in"
],
"fields": [
{
@@ -199,7 +201,7 @@
"fieldtype": "Date",
"label": "Purchase Date",
"read_only": 1,
"read_only_depends_on": "eval:!doc.is_existing_asset",
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
"reqd": 1
},
{
@@ -237,10 +239,12 @@
"default": "0",
"fieldname": "calculate_depreciation",
"fieldtype": "Check",
"label": "Calculate Depreciation"
"label": "Calculate Depreciation",
"read_only_depends_on": "eval:doc.is_composite_asset && !doc.gross_purchase_amount"
},
{
"default": "0",
"depends_on": "eval:!doc.is_composite_asset",
"fieldname": "is_existing_asset",
"fieldtype": "Check",
"label": "Is Existing Asset"
@@ -492,7 +496,7 @@
"fieldname": "asset_quantity",
"fieldtype": "Int",
"label": "Asset Quantity",
"read_only_depends_on": "eval:!doc.is_existing_asset"
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
},
{
"fieldname": "depr_entry_posting_status",
@@ -510,6 +514,21 @@
"fieldname": "is_fully_depreciated",
"fieldtype": "Check",
"label": "Is Fully Depreciated"
},
{
"default": "0",
"depends_on": "eval:!doc.is_existing_asset",
"fieldname": "is_composite_asset",
"fieldtype": "Check",
"label": "Is Composite Asset"
},
{
"fieldname": "capitalized_in",
"fieldtype": "Link",
"hidden": 1,
"label": "Capitalized In",
"options": "Asset Capitalization",
"read_only": 1
}
],
"idx": 72,
@@ -538,7 +557,7 @@
"table_fieldname": "accounts"
}
],
"modified": "2023-08-10 20:25:09.913073",
"modified": "2023-10-03 23:28:26.732269",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -204,7 +204,9 @@ class Asset(AccountsController):
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
if self.item_code and not self.get("finance_books"):
finance_books = get_item_details(self.item_code, self.asset_category)
finance_books = get_item_details(
self.item_code, self.asset_category, self.gross_purchase_amount
)
self.set("finance_books", finance_books)
def validate_finance_books(self):
@@ -232,7 +234,7 @@ class Asset(AccountsController):
if not self.asset_category:
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
if not flt(self.gross_purchase_amount):
if not flt(self.gross_purchase_amount) and not self.is_composite_asset:
frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError)
if is_cwip_accounting_enabled(self.asset_category):
@@ -1164,6 +1166,15 @@ def create_asset_repair(asset, asset_name):
return asset_repair
@frappe.whitelist()
def create_asset_capitalization(asset):
asset_capitalization = frappe.new_doc("Asset Capitalization")
asset_capitalization.update(
{"target_asset": asset, "capitalization_method": "Choose a WIP composite asset"}
)
return asset_capitalization
@frappe.whitelist()
def create_asset_value_adjustment(asset, asset_category, company):
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
@@ -1195,7 +1206,7 @@ def transfer_asset(args):
@frappe.whitelist()
def get_item_details(item_code, asset_category):
def get_item_details(item_code, asset_category, gross_purchase_amount):
asset_category_doc = frappe.get_doc("Asset Category", asset_category)
books = []
for d in asset_category_doc.finance_books:
@@ -1205,7 +1216,11 @@ def get_item_details(item_code, asset_category):
"depreciation_method": d.depreciation_method,
"total_number_of_depreciations": d.total_number_of_depreciations,
"frequency_of_depreciation": d.frequency_of_depreciation,
"start_date": nowdate(),
"daily_depreciation": d.daily_depreciation,
"salvage_value_percentage": d.salvage_value_percentage,
"expected_value_after_useful_life": flt(gross_purchase_amount)
* flt(d.salvage_value_percentage / 100),
"depreciation_start_date": d.depreciation_start_date or nowdate(),
}
)

View File

@@ -1686,6 +1686,7 @@ def create_asset(**args):
"location": args.location or "Test Location",
"asset_owner": args.asset_owner or "Company",
"is_existing_asset": args.is_existing_asset or 1,
"is_composite_asset": args.is_composite_asset or 0,
"asset_quantity": args.get("asset_quantity") or 1,
"depr_entry_posting_status": args.depr_entry_posting_status or "",
}

View File

@@ -15,9 +15,15 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
refresh() {
this.show_general_ledger();
if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) {
this.show_stock_ledger();
}
if (this.frm.doc.stock_items && !this.frm.doc.stock_items.length && this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") {
this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset);
this.get_target_asset_details();
}
}
setup_queries() {
@@ -34,18 +40,9 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
});
me.frm.set_query("target_asset", function() {
var filters = {};
if (me.frm.doc.target_item_code) {
filters['item_code'] = me.frm.doc.target_item_code;
}
filters['status'] = ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]];
filters['docstatus'] = 1;
return {
filters: filters
};
filters: {'is_composite_asset': 1, 'docstatus': 0 }
}
});
me.frm.set_query("asset", "asset_items", function() {
@@ -104,6 +101,39 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
return this.get_target_item_details();
}
target_asset() {
if (this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") {
this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset);
this.get_target_asset_details();
}
}
set_consumed_stock_items_tagged_to_wip_composite_asset(asset) {
var me = this;
if (asset) {
return me.frm.call({
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_items_tagged_to_wip_composite_asset",
args: {
asset: asset,
},
callback: function (r) {
if (!r.exc && r.message) {
me.frm.clear_table("stock_items");
for (let item of r.message) {
me.frm.add_child("stock_items", item);
}
refresh_field("stock_items");
me.calculate_totals();
}
}
});
}
}
item_code(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
if (cdt === "Asset Capitalization Stock Item") {
@@ -218,6 +248,26 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
}
}
get_target_asset_details() {
var me = this;
if (me.frm.doc.target_asset) {
return me.frm.call({
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details",
child: me.frm.doc,
args: {
asset: me.frm.doc.target_asset,
company: me.frm.doc.company,
},
callback: function (r) {
if (!r.exc) {
me.frm.refresh_fields();
}
}
});
}
}
get_consumed_stock_item_details(row) {
var me = this;

View File

@@ -8,24 +8,25 @@
"engine": "InnoDB",
"field_order": [
"title",
"company",
"naming_series",
"entry_type",
"target_item_code",
"target_asset",
"target_item_name",
"target_is_fixed_asset",
"target_has_batch_no",
"target_has_serial_no",
"column_break_9",
"target_asset_name",
"capitalization_method",
"target_item_code",
"target_asset_location",
"target_asset",
"target_asset_name",
"target_warehouse",
"target_qty",
"target_stock_uom",
"target_batch_no",
"target_serial_no",
"column_break_5",
"company",
"finance_book",
"posting_date",
"posting_time",
@@ -57,12 +58,13 @@
"label": "Title"
},
{
"depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || ((doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization')",
"fieldname": "target_item_code",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Target Item Code",
"options": "Item",
"reqd": 1
"mandatory_depends_on": "eval:(doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization'",
"options": "Item"
},
{
"depends_on": "eval:doc.target_item_code && doc.target_item_name != doc.target_item_code",
@@ -86,16 +88,18 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval:(doc.target_asset && !doc.__islocal) || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')",
"fieldname": "target_asset",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Target Asset",
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset'",
"no_copy": 1,
"options": "Asset",
"read_only": 1
"read_only_depends_on": "eval:(doc.entry_type=='Decapitalization') || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset')"
},
{
"depends_on": "eval:doc.entry_type=='Capitalization'",
"depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')",
"fetch_from": "target_asset.asset_name",
"fieldname": "target_asset_name",
"fieldtype": "Data",
@@ -186,12 +190,14 @@
},
{
"default": "1",
"depends_on": "eval:doc.entry_type=='Decapitalization'",
"fieldname": "target_qty",
"fieldtype": "Float",
"label": "Target Qty",
"read_only_depends_on": "eval:doc.entry_type=='Capitalization'"
},
{
"depends_on": "eval:doc.entry_type=='Decapitalization'",
"fetch_from": "target_item_code.stock_uom",
"fieldname": "target_stock_uom",
"fieldtype": "Link",
@@ -331,18 +337,26 @@
"read_only": 1
},
{
"depends_on": "eval:doc.entry_type=='Capitalization'",
"depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'",
"fieldname": "target_asset_location",
"fieldtype": "Link",
"label": "Target Asset Location",
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'",
"options": "Location"
},
{
"depends_on": "eval:doc.entry_type=='Capitalization'",
"fieldname": "capitalization_method",
"fieldtype": "Select",
"label": "Capitalization Method",
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
"options": "\nCreate a new composite asset\nChoose a WIP composite asset"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-06-22 14:17:07.995120",
"modified": "2023-10-03 22:55:59.461456",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Capitalization",

View File

@@ -53,6 +53,7 @@ class AssetCapitalization(StockController):
self.validate_posting_time()
self.set_missing_values(for_validate=True)
self.validate_target_item()
self.validate_target_asset()
self.validate_consumed_stock_item()
self.validate_consumed_asset_item()
self.validate_service_item()
@@ -63,12 +64,12 @@ class AssetCapitalization(StockController):
def before_submit(self):
self.validate_source_mandatory()
if self.entry_type == "Capitalization":
self.create_target_asset()
self.create_target_asset()
def on_submit(self):
self.update_stock_ledger()
self.make_gl_entries()
self.update_target_asset()
def on_cancel(self):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
@@ -85,6 +86,11 @@ class AssetCapitalization(StockController):
if self.meta.has_field(k) and (not self.get(k) or k in force_fields):
self.set(k, v)
target_asset_details = get_target_asset_details(self.target_asset, self.company)
for k, v in target_asset_details.items():
if self.meta.has_field(k) and (not self.get(k) or k in force_fields):
self.set(k, v)
for d in self.stock_items:
args = self.as_dict()
args.update(d.as_dict())
@@ -146,6 +152,33 @@ class AssetCapitalization(StockController):
self.validate_item(target_item)
def validate_target_asset(self):
if self.target_asset:
target_asset = self.get_asset_for_validation(self.target_asset)
if not target_asset.is_composite_asset:
frappe.throw(_("Target Asset {0} needs to be composite asset").format(target_asset.name))
if target_asset.item_code != self.target_item_code:
frappe.throw(
_("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code)
)
if target_asset.status in ("Scrapped", "Sold", "Capitalized", "Decapitalized"):
frappe.throw(
_("Target Asset {0} cannot be {1}").format(target_asset.name, target_asset.status)
)
if target_asset.docstatus == 1:
frappe.throw(_("Target Asset {0} cannot be submitted").format(target_asset.name))
elif target_asset.docstatus == 2:
frappe.throw(_("Target Asset {0} cannot be cancelled").format(target_asset.name))
if target_asset.company != self.company:
frappe.throw(
_("Target Asset {0} does not belong to company {1}").format(target_asset.name, self.company)
)
def validate_consumed_stock_item(self):
for d in self.stock_items:
if d.item_code:
@@ -170,7 +203,23 @@ class AssetCapitalization(StockController):
)
asset = self.get_asset_for_validation(d.asset)
self.validate_asset(asset)
if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"):
frappe.throw(
_("Row #{0}: Consumed Asset {1} cannot be {2}").format(d.idx, asset.name, asset.status)
)
if asset.docstatus == 0:
frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be Draft").format(d.idx, asset.name))
elif asset.docstatus == 2:
frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be cancelled").format(d.idx, asset.name))
if asset.company != self.company:
frappe.throw(
_("Row #{0}: Consumed Asset {1} does not belong to company {2}").format(
d.idx, asset.name, self.company
)
)
def validate_service_item(self):
for d in self.service_items:
@@ -205,21 +254,12 @@ class AssetCapitalization(StockController):
def get_asset_for_validation(self, asset):
return frappe.db.get_value(
"Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1
"Asset",
asset,
["name", "item_code", "company", "status", "docstatus", "is_composite_asset"],
as_dict=1,
)
def validate_asset(self, asset):
if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"):
frappe.throw(_("Asset {0} is {1}").format(asset.name, asset.status))
if asset.docstatus == 0:
frappe.throw(_("Asset {0} is Draft").format(asset.name))
if asset.docstatus == 2:
frappe.throw(_("Asset {0} is cancelled").format(asset.name))
if asset.company != self.company:
frappe.throw(_("Asset {0} does not belong to company {1}").format(asset.name, self.company))
@frappe.whitelist()
def set_warehouse_details(self):
for d in self.get("stock_items"):
@@ -485,16 +525,25 @@ class AssetCapitalization(StockController):
)
def create_target_asset(self):
if (
self.entry_type != "Capitalization"
or self.capitalization_method != "Create a new composite asset"
):
return
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
asset_doc = frappe.new_doc("Asset")
asset_doc.company = self.company
asset_doc.item_code = self.target_item_code
asset_doc.is_existing_asset = 1
asset_doc.is_composite_asset = 1
asset_doc.location = self.target_asset_location
asset_doc.available_for_use_date = self.posting_date
asset_doc.purchase_date = self.posting_date
asset_doc.gross_purchase_amount = total_target_asset_value
asset_doc.purchase_receipt_amount = total_target_asset_value
asset_doc.purchase_receipt_amount = total_target_asset_value
asset_doc.capitalized_in = self.name
asset_doc.flags.ignore_validate = True
asset_doc.insert()
@@ -510,6 +559,28 @@ class AssetCapitalization(StockController):
).format(get_link_to_form("Asset", asset_doc.name))
)
def update_target_asset(self):
if (
self.entry_type != "Capitalization"
or self.capitalization_method != "Choose a WIP composite asset"
):
return
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
asset_doc = frappe.get_doc("Asset", self.target_asset)
asset_doc.gross_purchase_amount = total_target_asset_value
asset_doc.purchase_receipt_amount = total_target_asset_value
asset_doc.capitalized_in = self.name
asset_doc.flags.ignore_validate = True
asset_doc.save()
frappe.msgprint(
_(
"Asset {0} has been updated. Please set the depreciation details if any and submit it."
).format(get_link_to_form("Asset", asset_doc.name))
)
def restore_consumed_asset_items(self):
for item in self.asset_items:
asset = frappe.get_doc("Asset", item.asset)
@@ -568,6 +639,33 @@ def get_target_item_details(item_code=None, company=None):
return out
@frappe.whitelist()
def get_target_asset_details(asset=None, company=None):
out = frappe._dict()
# Get Asset Details
asset_details = frappe._dict()
if asset:
asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1)
if not asset_details:
frappe.throw(_("Asset {0} does not exist").format(asset))
# Re-set item code from Asset
out.target_item_code = asset_details.item_code
# Set Asset Details
out.asset_name = asset_details.asset_name
if asset_details.item_code:
out.target_fixed_asset_account = get_asset_category_account(
"fixed_asset_account", item=asset_details.item_code, company=company
)
else:
out.target_fixed_asset_account = None
return out
@frappe.whitelist()
def get_consumed_stock_item_details(args):
if isinstance(args, string_types):
@@ -716,3 +814,30 @@ def get_service_item_details(args):
)
return out
@frappe.whitelist()
def get_items_tagged_to_wip_composite_asset(asset):
fields = [
"item_code",
"item_name",
"batch_no",
"serial_no",
"stock_qty",
"stock_uom",
"warehouse",
"cost_center",
"qty",
"valuation_rate",
"amount",
]
pi_items = frappe.get_all(
"Purchase Invoice Item", filters={"wip_composite_asset": asset}, fields=fields
)
pr_items = frappe.get_all(
"Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields
)
return pi_items + pr_items

View File

@@ -50,6 +50,7 @@ class TestAssetCapitalization(unittest.TestCase):
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
capitalization_method="Create a new composite asset",
target_item_code="Macbook Pro",
target_asset_location="Test Location",
stock_qty=stock_qty,
@@ -139,6 +140,7 @@ class TestAssetCapitalization(unittest.TestCase):
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
capitalization_method="Create a new composite asset",
target_item_code="Macbook Pro",
target_asset_location="Test Location",
stock_qty=stock_qty,
@@ -203,6 +205,77 @@ class TestAssetCapitalization(unittest.TestCase):
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
def test_capitalization_with_wip_composite_asset(self):
company = "_Test Company with perpetual inventory"
set_depreciation_settings_in_company(company=company)
stock_rate = 1000
stock_qty = 2
stock_amount = 2000
total_amount = 2000
wip_composite_asset = create_asset(
asset_name="Asset Capitalization WIP Composite Asset",
is_composite_asset=1,
warehouse="Stores - TCP1",
company=company,
)
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Capitalization",
capitalization_method="Choose a WIP composite asset",
target_asset=wip_composite_asset.name,
target_asset_location="Test Location",
stock_qty=stock_qty,
stock_rate=stock_rate,
service_expense_account="Expenses Included In Asset Valuation - TCP1",
company=company,
submit=1,
)
# Test Asset Capitalization values
self.assertEqual(asset_capitalization.entry_type, "Capitalization")
self.assertEqual(asset_capitalization.capitalization_method, "Choose a WIP composite asset")
self.assertEqual(asset_capitalization.target_qty, 1)
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
self.assertEqual(asset_capitalization.total_value, total_amount)
self.assertEqual(asset_capitalization.target_incoming_rate, total_amount)
# Test Target Asset values
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
# Test General Ledger Entries
expected_gle = {
"_Test Fixed Asset - TCP1": 2000,
"_Test Warehouse - TCP1": -2000,
}
actual_gle = get_actual_gle_dict(asset_capitalization.name)
self.assertEqual(actual_gle, expected_gle)
# Test Stock Ledger Entries
expected_sle = {
("Capitalization Source Stock Item", "_Test Warehouse - TCP1"): {
"actual_qty": -stock_qty,
"stock_value_difference": -stock_amount,
}
}
actual_sle = get_actual_sle_dict(asset_capitalization.name)
self.assertEqual(actual_sle, expected_sle)
# Cancel Asset Capitalization and make test entries and status are reversed
asset_capitalization.cancel()
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
def test_decapitalization_with_depreciation(self):
# Variables
purchase_date = "2020-01-01"
@@ -326,6 +399,7 @@ def create_asset_capitalization(**args):
asset_capitalization.update(
{
"entry_type": args.entry_type or "Capitalization",
"capitalization_method": args.capitalization_method or None,
"company": company,
"posting_date": args.posting_date or now.strftime("%Y-%m-%d"),
"posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"),

View File

@@ -12,6 +12,7 @@
"column_break_5",
"frequency_of_depreciation",
"depreciation_start_date",
"salvage_value_percentage",
"expected_value_after_useful_life",
"value_after_depreciation",
"rate_of_depreciation"
@@ -87,12 +88,17 @@
"fieldname": "daily_depreciation",
"fieldtype": "Check",
"label": "Daily Depreciation"
},
{
"fieldname": "salvage_value_percentage",
"fieldtype": "Percent",
"label": "Salvage Value Percentage"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-08-10 18:56:09.022246",
"modified": "2023-09-29 15:39:52.740594",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",

View File

@@ -24,6 +24,7 @@
"bill_for_rejected_quantity_in_purchase_invoice",
"disable_last_purchase_rate",
"show_pay_button",
"use_transaction_date_exchange_rate",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
@@ -164,6 +165,13 @@
"fieldname": "over_order_allowance",
"fieldtype": "Float",
"label": "Over Order Allowance (%)"
},
{
"default": "0",
"description": "While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice.",
"fieldname": "use_transaction_date_exchange_rate",
"fieldtype": "Check",
"label": "Use Transaction Date Exchange Rate"
}
],
"icon": "fa fa-cog",
@@ -171,7 +179,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-03-02 17:02:14.404622",
"modified": "2023-10-16 16:22:03.201078",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -475,6 +475,7 @@
"depends_on": "eval:doc.is_subcontracted",
"fieldname": "supplier_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Supplier Warehouse",
"options": "Warehouse"
},
@@ -1272,7 +1273,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2023-09-13 16:21:07.361700",
"modified": "2023-10-01 20:58:07.851037",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -68,7 +68,7 @@ frappe.ui.form.on("Supplier", {
}, __("View"));
frm.add_custom_button(__('Accounts Payable'), function () {
frappe.set_route('query-report', 'Accounts Payable', { supplier: frm.doc.name });
frappe.set_route('query-report', 'Accounts Payable', { party_type: "Supplier", party: frm.doc.name });
}, __("View"));
frm.add_custom_button(__('Bank Account'), function () {

View File

@@ -8,7 +8,7 @@ def get_data():
"This is based on transactions against this Supplier. See timeline below for details"
),
"fieldname": "supplier",
"non_standard_fieldnames": {"Payment Entry": "party_name", "Bank Account": "party"},
"non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"},
"transactions": [
{"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]},
{"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]},

View File

@@ -6,7 +6,7 @@ import copy
import frappe
from frappe import _
from frappe.query_builder.functions import IfNull
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import date_diff, flt, getdate
@@ -57,7 +57,7 @@ def get_data(filters):
po_item.qty,
po_item.received_qty,
(po_item.qty - po_item.received_qty).as_("pending_qty"),
IfNull(pi_item.qty, 0).as_("billed_qty"),
Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"),
po_item.base_amount.as_("amount"),
(po_item.received_qty * po_item.base_rate).as_("received_qty_amount"),
(po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"),

View File

@@ -35,8 +35,12 @@ def get_data(filters):
sq_item.parent,
sq_item.item_code,
sq_item.qty,
sq.currency,
sq_item.stock_qty,
sq_item.amount,
sq_item.base_rate,
sq_item.base_amount,
sq.price_list_currency,
sq_item.uom,
sq_item.stock_uom,
sq_item.request_for_quotation,
@@ -105,7 +109,11 @@ def prepare_data(supplier_quotation_data, filters):
"qty": data.get("qty"),
"price": flt(data.get("amount") * exchange_rate, float_precision),
"uom": data.get("uom"),
"price_list_currency": data.get("price_list_currency"),
"currency": data.get("currency"),
"stock_uom": data.get("stock_uom"),
"base_amount": flt(data.get("base_amount"), float_precision),
"base_rate": flt(data.get("base_rate"), float_precision),
"request_for_quotation": data.get("request_for_quotation"),
"valid_till": data.get("valid_till"),
"lead_time_days": data.get("lead_time_days"),
@@ -183,6 +191,8 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map):
def get_columns(filters):
currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
group_by_columns = [
{
"fieldname": "supplier_name",
@@ -203,11 +213,18 @@ def get_columns(filters):
columns = [
{"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 90},
{"fieldname": "qty", "label": _("Quantity"), "fieldtype": "Float", "width": 80},
{
"fieldname": "currency",
"label": _("Currency"),
"fieldtype": "Link",
"options": "Currency",
"width": 110,
},
{
"fieldname": "price",
"label": _("Price"),
"fieldtype": "Currency",
"options": "Company:company:default_currency",
"options": "currency",
"width": 110,
},
{
@@ -221,9 +238,23 @@ def get_columns(filters):
"fieldname": "price_per_unit",
"label": _("Price per Unit (Stock UOM)"),
"fieldtype": "Currency",
"options": "Company:company:default_currency",
"options": "currency",
"width": 120,
},
{
"fieldname": "base_amount",
"label": _("Price ({0})").format(currency),
"fieldtype": "Currency",
"options": "price_list_currency",
"width": 180,
},
{
"fieldname": "base_rate",
"label": _("Price Per Unit ({0})").format(currency),
"fieldtype": "Currency",
"options": "price_list_currency",
"width": 180,
},
{
"fieldname": "quotation",
"label": _("Supplier Quotation"),

View File

@@ -12,6 +12,7 @@ from frappe.utils import (
add_days,
add_months,
cint,
comma_and,
flt,
fmt_money,
formatdate,
@@ -168,7 +169,6 @@ class AccountsController(TransactionBase):
self.validate_value("base_grand_total", ">=", 0)
validate_return(self)
self.set_total_in_words()
self.validate_all_documents_schedule()
@@ -181,6 +181,17 @@ class AccountsController(TransactionBase):
self.validate_party_account_currency()
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
if invalid_advances := [
x for x in self.advances if not x.reference_type or not x.reference_name
]:
frappe.throw(
_(
"Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry."
).format(
frappe.bold(comma_and([x.idx for x in invalid_advances])), frappe.bold(_("Advance Payments"))
)
)
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
self.set_advances()
@@ -207,15 +218,75 @@ class AccountsController(TransactionBase):
if self.doctype != "Material Request" and not self.ignore_pricing_rule:
apply_pricing_rule_on_transaction(self)
self.set_total_in_words()
def before_cancel(self):
validate_einvoice_fields(self)
def _remove_references_in_unreconcile(self):
upe = frappe.qb.DocType("Unreconcile Payment Entries")
rows = (
frappe.qb.from_(upe)
.select(upe.name, upe.parent)
.where((upe.reference_doctype == self.doctype) & (upe.reference_name == self.name))
.run(as_dict=True)
)
if rows:
references_map = frappe._dict()
for x in rows:
references_map.setdefault(x.parent, []).append(x.name)
for doc, rows in references_map.items():
unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc)
for row in rows:
unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0])
unreconcile_doc.flags.ignore_validate_update_after_submit = True
unreconcile_doc.flags.ignore_links = True
unreconcile_doc.save(ignore_permissions=True)
# delete docs upon parent doc deletion
unreconcile_docs = frappe.db.get_all("Unreconcile Payments", filters={"voucher_no": self.name})
for x in unreconcile_docs:
_doc = frappe.get_doc("Unreconcile Payments", x.name)
if _doc.docstatus == 1:
_doc.cancel()
_doc.delete()
def _remove_references_in_repost_doctypes(self):
repost_doctypes = ["Repost Payment Ledger Items", "Repost Accounting Ledger Items"]
for _doctype in repost_doctypes:
dt = frappe.qb.DocType(_doctype)
rows = (
frappe.qb.from_(dt)
.select(dt.name, dt.parent, dt.parenttype)
.where((dt.voucher_type == self.doctype) & (dt.voucher_no == self.name))
.run(as_dict=True)
)
if rows:
references_map = frappe._dict()
for x in rows:
references_map.setdefault((x.parenttype, x.parent), []).append(x.name)
for doc, rows in references_map.items():
repost_doc = frappe.get_doc(doc[0], doc[1])
for row in rows:
if _doctype == "Repost Payment Ledger Items":
repost_doc.remove(repost_doc.get("repost_vouchers", {"name": row})[0])
else:
repost_doc.remove(repost_doc.get("vouchers", {"name": row})[0])
repost_doc.flags.ignore_validate_update_after_submit = True
repost_doc.flags.ignore_links = True
repost_doc.save(ignore_permissions=True)
def on_trash(self):
# delete references in 'Repost Payment Ledger'
rpi = frappe.qb.DocType("Repost Payment Ledger Items")
frappe.qb.from_(rpi).delete().where(
(rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name)
).run()
self._remove_references_in_repost_doctypes()
self._remove_references_in_unreconcile()
# delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
@@ -512,6 +583,17 @@ class AccountsController(TransactionBase):
self.currency, self.company_currency, transaction_date, args
)
if (
self.currency
and buying_or_selling == "Buying"
and frappe.db.get_single_value("Buying Settings", "use_transaction_date_exchange_rate")
and self.doctype == "Purchase Invoice"
):
self.use_transaction_date_exchange_rate = True
self.conversion_rate = get_exchange_rate(
self.currency, self.company_currency, transaction_date, args
)
def set_missing_item_details(self, for_validate=False):
"""set missing item values"""
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -535,6 +617,7 @@ class AccountsController(TransactionBase):
self.pricing_rules = []
selected_serial_nos_map = {}
for item in self.get("items"):
if item.get("item_code"):
args = parent_dict.copy()
@@ -546,6 +629,7 @@ class AccountsController(TransactionBase):
args["ignore_pricing_rule"] = (
self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0
)
args["ignore_serial_nos"] = selected_serial_nos_map.get(item.get("item_code"))
if not args.get("transaction_date"):
args["transaction_date"] = args.get("posting_date")
@@ -602,6 +686,11 @@ class AccountsController(TransactionBase):
if ret.get("pricing_rules"):
self.apply_pricing_rule_on_items(item, ret)
self.set_pricing_rule_details(item, ret)
if ret.get("serial_no"):
selected_serial_nos_map.setdefault(item.get("item_code"), []).extend(
ret.get("serial_no").split("\n")
)
else:
# Transactions line item without item code
@@ -908,7 +997,7 @@ class AccountsController(TransactionBase):
party_type, party, party_account, amount_field, order_doctype, order_list, include_unallocated
)
payment_entries = get_advance_payment_entries(
payment_entries = get_advance_payment_entries_for_regional(
party_type, party, party_account, order_doctype, order_list, include_unallocated
)
@@ -1405,7 +1494,7 @@ class AccountsController(TransactionBase):
"account": self.additional_discount_account,
"against": supplier_or_customer,
dr_or_cr: self.base_discount_amount,
"cost_center": self.cost_center,
"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company),
},
item=self,
)
@@ -2125,6 +2214,45 @@ class AccountsController(TransactionBase):
_("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx)
)
def check_if_fields_updated(self, fields_to_check, child_tables):
# Check if any field affecting accounting entry is altered
doc_before_update = self.get_doc_before_save()
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
# Check if opening entry check updated
needs_repost = doc_before_update.get("is_opening") != self.is_opening
if not needs_repost:
# Parent Level Accounts excluding party account
fields_to_check += accounting_dimensions
for field in fields_to_check:
if doc_before_update.get(field) != self.get(field):
needs_repost = 1
break
if not needs_repost:
# Check for child tables
for table in child_tables:
needs_repost = check_if_child_table_updated(
doc_before_update.get(table), self.get(table), child_tables[table]
)
if needs_repost:
break
return needs_repost
@frappe.whitelist()
def repost_accounting_entries(self):
if self.repost_required:
repost_ledger = frappe.new_doc("Repost Accounting Ledger")
repost_ledger.company = self.company
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
repost_ledger.insert()
repost_ledger.submit()
self.db_set("repost_required", 0)
else:
frappe.throw(_("No updates pending for reposting"))
@frappe.whitelist()
def get_tax_rate(account_head):
@@ -2348,6 +2476,11 @@ def get_advance_journal_entries(
return list(journal_entries)
@erpnext.allow_regional
def get_advance_payment_entries_for_regional(*args, **kwargs):
return get_advance_payment_entries(*args, **kwargs)
def get_advance_payment_entries(
party_type,
party,
@@ -3005,6 +3138,23 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.set_status()
def check_if_child_table_updated(
child_table_before_update, child_table_after_update, fields_to_check
):
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
# Check if any field affecting accounting entry is altered
for index, item in enumerate(child_table_after_update):
for field in fields_to_check:
if child_table_before_update[index].get(field) != item.get(field):
return True
for dimension in accounting_dimensions:
if child_table_before_update[index].get(dimension) != item.get(dimension):
return True
return False
@erpnext.allow_regional
def validate_regional(doc):
pass

View File

@@ -14,7 +14,8 @@ from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.utils import get_incoming_rate
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
class QtyMismatchError(ValidationError):
@@ -514,9 +515,20 @@ class BuyingController(SubcontractingController):
)
if self.is_return:
outgoing_rate = get_rate_for_return(
self.doctype, self.name, d.item_code, self.return_against, item_row=d
)
if get_valuation_method(d.item_code) == "Moving Average":
previous_sle = get_previous_sle(
{
"item_code": d.item_code,
"warehouse": d.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
}
)
outgoing_rate = flt(previous_sle.get("valuation_rate"))
else:
outgoing_rate = get_rate_for_return(
self.doctype, self.name, d.item_code, self.return_against, item_row=d
)
sle.update({"outgoing_rate": outgoing_rate, "recalculate_rate": 1})
if d.from_warehouse:

View File

@@ -4,9 +4,9 @@
import frappe
from frappe import _, bold, throw
from frappe.contacts.doctype.address.address import get_address_display
from frappe.utils import cint, cstr, flt, get_link_to_form, nowtime
from erpnext.accounts.party import render_address
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.stock_controller import StockController
@@ -282,7 +282,9 @@ class SellingController(StockController):
last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1)
if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
throw_message(item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate")
throw_message(
item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate (Moving Average)"
)
def get_item_list(self):
il = []
@@ -589,7 +591,9 @@ class SellingController(StockController):
for address_field, address_display_field in address_dict.items():
if self.get(address_field):
self.set(address_display_field, get_address_display(self.get(address_field)))
self.set(
address_display_field, render_address(self.get(address_field), check_permissions=False)
)
def validate_for_duplicate_items(self):
check_list, chk_dupl_itm = [], []

View File

@@ -655,7 +655,7 @@ class SubcontractingController(StockController):
{
"item_code": item.rm_item_code,
"warehouse": self.supplier_warehouse,
"actual_qty": -1 * flt(item.consumed_qty),
"actual_qty": -1 * flt(item.consumed_qty, item.precision("consumed_qty")),
"dependant_sle_voucher_detail_no": item.reference_name,
},
)

View File

@@ -190,7 +190,9 @@ class calculate_taxes_and_totals(object):
item.net_rate = item.rate
if not item.qty and self.doc.get("is_return"):
if (
not item.qty and self.doc.get("is_return") and self.doc.get("doctype") != "Purchase Receipt"
):
item.amount = flt(-1 * item.rate, item.precision("amount"))
elif not item.qty and self.doc.get("is_debit_note"):
item.amount = flt(item.rate, item.precision("amount"))

View File

@@ -382,7 +382,7 @@ def get_lead_details(lead, posting_date=None, company=None):
}
)
set_address_details(out, lead, "Lead")
set_address_details(out, lead, "Lead", company=company)
taxes_and_charges = set_taxes(
None,

View File

@@ -4,6 +4,7 @@
import frappe
from frappe import _
from frappe.query_builder.functions import Concat_ws, Date
def execute(filters=None):
@@ -69,53 +70,41 @@ def get_columns():
def get_data(filters):
return frappe.db.sql(
"""
SELECT
`tabLead`.name,
`tabLead`.lead_name,
`tabLead`.status,
`tabLead`.lead_owner,
`tabLead`.territory,
`tabLead`.source,
`tabLead`.email_id,
`tabLead`.mobile_no,
`tabLead`.phone,
`tabLead`.owner,
`tabLead`.company,
concat_ws(', ',
trim(',' from `tabAddress`.address_line1),
trim(',' from tabAddress.address_line2)
) AS address,
`tabAddress`.state,
`tabAddress`.pincode,
`tabAddress`.country
FROM
`tabLead` left join `tabDynamic Link` on (
`tabLead`.name = `tabDynamic Link`.link_name and
`tabDynamic Link`.parenttype = 'Address')
left join `tabAddress` on (
`tabAddress`.name=`tabDynamic Link`.parent)
WHERE
company = %(company)s
AND DATE(`tabLead`.creation) BETWEEN %(from_date)s AND %(to_date)s
{conditions}
ORDER BY
`tabLead`.creation asc """.format(
conditions=get_conditions(filters)
),
filters,
as_dict=1,
lead = frappe.qb.DocType("Lead")
address = frappe.qb.DocType("Address")
dynamic_link = frappe.qb.DocType("Dynamic Link")
query = (
frappe.qb.from_(lead)
.left_join(dynamic_link)
.on((lead.name == dynamic_link.link_name) & (dynamic_link.parenttype == "Address"))
.left_join(address)
.on(address.name == dynamic_link.parent)
.select(
lead.name,
lead.lead_name,
lead.status,
lead.lead_owner,
lead.territory,
lead.source,
lead.email_id,
lead.mobile_no,
lead.phone,
lead.owner,
lead.company,
(Concat_ws(", ", address.address_line1, address.address_line2)).as_("address"),
address.state,
address.pincode,
address.country,
)
.where(lead.company == filters.company)
.where(Date(lead.creation).between(filters.from_date, filters.to_date))
)
def get_conditions(filters):
conditions = []
if filters.get("territory"):
conditions.append(" and `tabLead`.territory=%(territory)s")
query = query.where(lead.territory == filters.get("territory"))
if filters.get("status"):
conditions.append(" and `tabLead`.status=%(status)s")
query = query.where(lead.status == filters.get("status"))
return " ".join(conditions) if conditions else ""
return query.run(as_dict=1)

View File

@@ -312,7 +312,7 @@ class TestWebsiteItem(unittest.TestCase):
# check if stock details are fetched and item not in stock with warehouse set
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
self.assertFalse(bool(data.product_info["in_stock"]))
self.assertEqual(data.product_info["stock_qty"][0][0], 0)
self.assertEqual(data.product_info["stock_qty"], 0)
# disable show stock availability
setup_e_commerce_settings({"show_stock_availability": 0})
@@ -355,7 +355,7 @@ class TestWebsiteItem(unittest.TestCase):
# check if stock details are fetched and item is in stock with warehouse set
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
self.assertTrue(bool(data.product_info["in_stock"]))
self.assertEqual(data.product_info["stock_qty"][0][0], 2)
self.assertEqual(data.product_info["stock_qty"], 2)
# unset warehouse
frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "")
@@ -364,7 +364,7 @@ class TestWebsiteItem(unittest.TestCase):
# (even though it has stock in some warehouse)
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
self.assertFalse(bool(data.product_info["in_stock"]))
self.assertFalse(bool(data.product_info["stock_qty"]))
self.assertFalse(data.product_info["stock_qty"])
# disable show stock availability
setup_e_commerce_settings({"show_stock_availability": 0})

View File

@@ -5,12 +5,6 @@ frappe.ui.form.on('Website Item', {
onload: (frm) => {
// should never check Private
frm.fields_dict["website_image"].df.is_private = 0;
frm.set_query("website_warehouse", () => {
return {
filters: {"is_group": 0}
};
});
},
refresh: (frm) => {

View File

@@ -135,7 +135,7 @@
"fieldtype": "Column Break"
},
{
"description": "Show Stock availability based on this warehouse.",
"description": "Show Stock availability based on this warehouse. If the parent warehouse is selected, then the system will display the consolidated available quantity of all child warehouses.",
"fieldname": "website_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
@@ -348,7 +348,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2022-09-30 04:01:52.090732",
"modified": "2023-09-12 14:19:22.822689",
"modified_by": "Administrator",
"module": "E-commerce",
"name": "Website Item",

View File

@@ -259,6 +259,10 @@ class ProductQuery:
)
def get_stock_availability(self, item):
from erpnext.templates.pages.wishlist import (
get_stock_availability as get_stock_availability_from_template,
)
"""Modify item object and add stock details."""
item.in_stock = False
warehouse = item.get("website_warehouse")
@@ -274,11 +278,7 @@ class ProductQuery:
else:
item.in_stock = True
elif warehouse:
# stock item and has warehouse
actual_qty = frappe.db.get_value(
"Bin", {"item_code": item.item_code, "warehouse": item.get("website_warehouse")}, "actual_qty"
)
item.in_stock = bool(flt(actual_qty))
item.in_stock = get_stock_availability_from_template(item.item_code, warehouse)
def get_cart_items(self):
customer = get_customer(silent=True)

View File

@@ -111,8 +111,8 @@ def place_order():
item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse")
if not cint(item_stock.in_stock):
throw(_("{0} Not in Stock").format(item.item_code))
if item.qty > item_stock.stock_qty[0][0]:
throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
if item.qty > item_stock.stock_qty:
throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty, item.item_code))
sales_order.flags.ignore_permissions = True
sales_order.insert()
@@ -150,6 +150,10 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
empty_card = True
else:
warehouse = frappe.get_cached_value(
"Website Item", {"item_code": item_code}, "website_warehouse"
)
quotation_items = quotation.get("items", {"item_code": item_code})
if not quotation_items:
quotation.append(
@@ -159,11 +163,13 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
"item_code": item_code,
"qty": qty,
"additional_notes": additional_notes,
"warehouse": warehouse,
},
)
else:
quotation_items[0].qty = qty
quotation_items[0].additional_notes = additional_notes
quotation_items[0].warehouse = warehouse
apply_cart_settings(quotation=quotation)
@@ -322,6 +328,10 @@ def decorate_quotation_doc(doc):
fields = fields[2:]
d.update(frappe.db.get_value("Website Item", {"item_code": item_code}, fields, as_dict=True))
website_warehouse = frappe.get_cached_value(
"Website Item", {"item_code": item_code}, "website_warehouse"
)
d.warehouse = website_warehouse
return doc

View File

@@ -17,7 +17,6 @@ from erpnext.e_commerce.shopping_cart.cart import (
request_for_quotation,
update_cart,
)
from erpnext.tests.utils import create_test_contact_and_address
class TestShoppingCart(unittest.TestCase):
@@ -28,7 +27,6 @@ class TestShoppingCart(unittest.TestCase):
def setUp(self):
frappe.set_user("Administrator")
create_test_contact_and_address()
self.enable_shopping_cart()
if not frappe.db.exists("Website Item", {"item_code": "_Test Item"}):
make_website_item(frappe.get_cached_doc("Item", "_Test Item"))
@@ -46,48 +44,57 @@ class TestShoppingCart(unittest.TestCase):
frappe.db.sql("delete from `tabTax Rule`")
def test_get_cart_new_user(self):
self.login_as_new_user()
self.login_as_customer(
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
)
create_address_and_contact(
address_title="_Test Address for Customer 2",
first_name="_Test Contact for Customer 2",
email="test_contact_two_customer@example.com",
customer="_Test Customer 2",
)
# test if lead is created and quotation with new lead is fetched
quotation = _get_cart_quotation()
customer = frappe.get_doc("Customer", "_Test Customer 2")
quotation = _get_cart_quotation(party=customer)
self.assertEqual(quotation.quotation_to, "Customer")
self.assertEqual(
quotation.contact_person,
frappe.db.get_value("Contact", dict(email_id="test_cart_user@example.com")),
frappe.db.get_value("Contact", dict(email_id="test_contact_two_customer@example.com")),
)
self.assertEqual(quotation.contact_email, frappe.session.user)
return quotation
def test_get_cart_customer(self):
def validate_quotation():
def test_get_cart_customer(self, customer="_Test Customer 2"):
def validate_quotation(customer_name):
# test if quotation with customer is fetched
quotation = _get_cart_quotation()
party = frappe.get_doc("Customer", customer_name)
quotation = _get_cart_quotation(party=party)
self.assertEqual(quotation.quotation_to, "Customer")
self.assertEqual(quotation.party_name, "_Test Customer")
self.assertEqual(quotation.party_name, customer_name)
self.assertEqual(quotation.contact_email, frappe.session.user)
return quotation
self.login_as_customer(
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
)
validate_quotation()
self.login_as_customer()
quotation = validate_quotation()
quotation = validate_quotation(customer)
return quotation
def test_add_to_cart(self):
self.login_as_customer()
self.login_as_customer(
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
)
create_address_and_contact(
address_title="_Test Address for Customer 2",
first_name="_Test Contact for Customer 2",
email="test_contact_two_customer@example.com",
customer="_Test Customer 2",
)
# clear existing quotations
self.clear_existing_quotations()
# add first item
update_cart("_Test Item", 1)
quotation = self.test_get_cart_customer()
quotation = self.test_get_cart_customer("_Test Customer 2")
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item")
self.assertEqual(quotation.get("items")[0].qty, 1)
@@ -95,7 +102,7 @@ class TestShoppingCart(unittest.TestCase):
# add second item
update_cart("_Test Item 2", 1)
quotation = self.test_get_cart_customer()
quotation = self.test_get_cart_customer("_Test Customer 2")
self.assertEqual(quotation.get("items")[1].item_code, "_Test Item 2")
self.assertEqual(quotation.get("items")[1].qty, 1)
self.assertEqual(quotation.get("items")[1].amount, 20)
@@ -108,7 +115,7 @@ class TestShoppingCart(unittest.TestCase):
# update first item
update_cart("_Test Item", 5)
quotation = self.test_get_cart_customer()
quotation = self.test_get_cart_customer("_Test Customer 2")
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item")
self.assertEqual(quotation.get("items")[0].qty, 5)
self.assertEqual(quotation.get("items")[0].amount, 50)
@@ -121,7 +128,7 @@ class TestShoppingCart(unittest.TestCase):
# remove first item
update_cart("_Test Item", 0)
quotation = self.test_get_cart_customer()
quotation = self.test_get_cart_customer("_Test Customer 2")
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item 2")
self.assertEqual(quotation.get("items")[0].qty, 1)
@@ -132,7 +139,17 @@ class TestShoppingCart(unittest.TestCase):
@unittest.skip("Flaky in CI")
def test_tax_rule(self):
self.create_tax_rule()
self.login_as_customer()
self.login_as_customer(
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
)
create_address_and_contact(
address_title="_Test Address for Customer 2",
first_name="_Test Contact for Customer 2",
email="test_contact_two_customer@example.com",
customer="_Test Customer 2",
)
quotation = self.create_quotation()
from erpnext.accounts.party import set_taxes
@@ -320,7 +337,7 @@ class TestShoppingCart(unittest.TestCase):
if frappe.db.exists("User", email):
return
frappe.get_doc(
user = frappe.get_doc(
{
"doctype": "User",
"user_type": "Website User",
@@ -330,6 +347,40 @@ class TestShoppingCart(unittest.TestCase):
}
).insert(ignore_permissions=True)
user.add_roles("Customer")
def create_address_and_contact(**kwargs):
if not frappe.db.get_value("Address", {"address_title": kwargs.get("address_title")}):
frappe.get_doc(
{
"doctype": "Address",
"address_title": kwargs.get("address_title"),
"address_type": kwargs.get("address_type") or "Office",
"address_line1": kwargs.get("address_line1") or "Station Road",
"city": kwargs.get("city") or "_Test City",
"state": kwargs.get("state") or "Test State",
"country": kwargs.get("country") or "India",
"links": [
{"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"}
],
}
).insert()
if not frappe.db.get_value("Contact", {"first_name": kwargs.get("first_name")}):
contact = frappe.get_doc(
{
"doctype": "Contact",
"first_name": kwargs.get("first_name"),
"links": [
{"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"}
],
}
)
contact.add_email(kwargs.get("email") or "test_contact_customer@example.com", is_primary=True)
contact.add_phone(kwargs.get("phone") or "+91 0000000000", is_primary_phone=True)
contact.insert()
test_dependencies = [
"Sales Taxes and Charges Template",

View File

@@ -104,6 +104,8 @@ def get_attributes_and_values(item_code):
@frappe.whitelist(allow_guest=True)
def get_next_attribute_and_values(item_code, selected_attributes):
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
"""Find the count of Items that match the selected attributes.
Also, find the attribute values that are not applicable for further searching.
If less than equal to 10 items are found, return item_codes of those items.
@@ -168,7 +170,7 @@ def get_next_attribute_and_values(item_code, selected_attributes):
product_info = None
product_id = ""
website_warehouse = ""
warehouse = ""
if exact_match or filtered_items:
if exact_match and len(exact_match) == 1:
product_id = exact_match[0]
@@ -176,16 +178,19 @@ def get_next_attribute_and_values(item_code, selected_attributes):
product_id = list(filtered_items)[0]
if product_id:
website_warehouse = frappe.get_cached_value(
warehouse = frappe.get_cached_value(
"Website Item", {"item_code": product_id}, "website_warehouse"
)
available_qty = 0.0
if website_warehouse:
available_qty = flt(
frappe.db.get_value(
"Bin", {"item_code": product_id, "warehouse": website_warehouse}, "actual_qty"
)
if warehouse and frappe.get_cached_value("Warehouse", warehouse, "is_group") == 1:
warehouses = get_child_warehouses(warehouse)
else:
warehouses = [warehouse] if warehouse else []
for warehouse in warehouses:
available_qty += flt(
frappe.db.get_value("Bin", {"item_code": product_id, "warehouse": warehouse}, "actual_qty")
)
return {

View File

@@ -517,6 +517,7 @@ accounting_dimension_doctypes = [
"Sales Invoice Item",
"Purchase Invoice Item",
"Purchase Order Item",
"Sales Order Item",
"Journal Entry Account",
"Material Request Item",
"Delivery Note Item",

View File

@@ -6,7 +6,14 @@
frappe.ui.form.on('Loan Repayment', {
// refresh: function(frm) {
// }
// },
setup: function(frm) {
if (frappe.meta.has_field("Loan Repayment", "repay_from_salary")) {
frm.add_fetch("against_loan", "repay_from_salary", "repay_from_salary");
}
},
onload: function(frm) {
frm.set_query('against_loan', function() {
return {

View File

@@ -80,6 +80,12 @@ class LoanRepayment(AccountsController):
if amounts.get("due_date"):
self.due_date = amounts.get("due_date")
if hasattr(self, "repay_from_salary") and hasattr(self, "payroll_payable_account"):
if self.repay_from_salary and not self.payroll_payable_account:
frappe.throw(_("Please set Payroll Payable Account in Loan Repayment"))
elif not self.repay_from_salary and self.payroll_payable_account:
self.repay_from_salary = 1
def check_future_entries(self):
future_repayment_date = frappe.db.get_value(
"Loan Repayment",

View File

@@ -476,6 +476,15 @@ frappe.ui.form.on("Material Request Plan Item", {
}
})
}
},
material_request_type(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.from_warehouse &&
row.material_request_type !== "Material Transfer") {
frappe.model.set_value(cdt, cdn, 'from_warehouse', '');
}
}
});

View File

@@ -228,7 +228,7 @@
},
{
"default": "0",
"description": "If enabled, the system won't create material requests for the available items.",
"description": "If enabled, the system will create material requests even if the stock exists in the 'Raw Materials Warehouse'.",
"fieldname": "ignore_existing_ordered_qty",
"fieldtype": "Check",
"label": "Ignore Available Stock"
@@ -422,7 +422,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-07-28 13:37:43.926686",
"modified": "2023-09-29 11:41:03.246059",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",

View File

@@ -40,6 +40,12 @@ class ProductionPlan(Document):
self._rename_temporary_references()
validate_uom_is_integer(self, "stock_uom", "planned_qty")
self.validate_sales_orders()
self.validate_material_request_type()
def validate_material_request_type(self):
for row in self.get("mr_items"):
if row.from_warehouse and row.material_request_type != "Material Transfer":
row.from_warehouse = ""
@frappe.whitelist()
def validate_sales_orders(self, sales_order=None):
@@ -749,7 +755,9 @@ class ProductionPlan(Document):
"items",
{
"item_code": item.item_code,
"from_warehouse": item.from_warehouse,
"from_warehouse": item.from_warehouse
if material_request_type == "Material Transfer"
else None,
"qty": item.quantity,
"schedule_date": schedule_date,
"warehouse": item.warehouse,
@@ -1500,6 +1508,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
from erpnext.stock.doctype.pick_list.pick_list import get_available_item_locations
stock_uom, purchase_uom = frappe.db.get_value(
"Item", item.get("item_code"), ["stock_uom", "purchase_uom"]
)
locations = get_available_item_locations(
item.get("item_code"), warehouses, item.get("quantity"), company, ignore_validation=True
)
@@ -1510,6 +1522,10 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
if required_qty <= 0:
return
conversion_factor = 1.0
if purchase_uom != stock_uom and purchase_uom == item["uom"]:
conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"])
new_dict = copy.deepcopy(item)
quantity = required_qty if d.get("qty") > required_qty else d.get("qty")
@@ -1522,25 +1538,14 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
}
)
required_qty -= quantity
required_qty -= quantity / conversion_factor
new_mr_items.append(new_dict)
# raise purchase request for remaining qty
if required_qty:
stock_uom, purchase_uom = frappe.db.get_value(
"Item", item["item_code"], ["stock_uom", "purchase_uom"]
)
if purchase_uom != stock_uom and purchase_uom == item["uom"]:
conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"])
if not (conversion_factor or frappe.flags.show_qty_in_stock_uom):
frappe.throw(
_("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format(
purchase_uom, stock_uom, item["item_code"]
)
)
required_qty = required_qty / conversion_factor
precision = frappe.get_precision("Material Request Plan Item", "quantity")
if flt(required_qty, precision) > 0:
required_qty = required_qty
if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"):
required_qty = ceil(required_qty)
@@ -1611,18 +1616,25 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
table = frappe.qb.DocType("Production Plan")
child = frappe.qb.DocType("Material Request Plan Item")
non_completed_production_plans = get_non_completed_production_plans()
query = (
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0)))
.select(Sum(child.required_bom_qty))
.where(
(table.docstatus == 1)
& (child.item_code == item_code)
& (child.warehouse == warehouse)
& (table.status.notin(["Completed", "Closed"]))
)
).run()
)
if non_completed_production_plans:
query = query.where(table.name.isin(non_completed_production_plans))
query = query.run()
if not query:
return 0.0
@@ -1630,7 +1642,9 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
reserved_qty_for_production_plan = flt(query[0][0])
reserved_qty_for_production = flt(
get_reserved_qty_for_production(item_code, warehouse, check_production_plan=True)
get_reserved_qty_for_production(
item_code, warehouse, non_completed_production_plans, check_production_plan=True
)
)
if reserved_qty_for_production > reserved_qty_for_production_plan:
@@ -1639,6 +1653,25 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
return reserved_qty_for_production_plan - reserved_qty_for_production
def get_non_completed_production_plans():
table = frappe.qb.DocType("Production Plan")
child = frappe.qb.DocType("Production Plan Item")
query = (
frappe.qb.from_(table)
.inner_join(child)
.on(table.name == child.parent)
.select(table.name)
.where(
(table.docstatus == 1)
& (table.status.notin(["Completed", "Closed"]))
& (child.planned_qty > child.ordered_qty)
)
).run(as_dict=True)
return list(set([d.name for d in query]))
def get_raw_materials_of_sub_assembly_items(
item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1
):

View File

@@ -7,6 +7,7 @@ from frappe.utils import add_to_date, flt, getdate, now_datetime, nowdate
from erpnext.controllers.item_variant import create_variant
from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_items_for_material_requests,
get_non_completed_production_plans,
get_sales_orders,
get_warehouse_list,
)
@@ -1039,6 +1040,102 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(after_qty, before_qty)
def test_resered_qty_for_production_plan_for_work_order(self):
from erpnext.stock.utils import get_or_make_bin
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
pln = create_production_plan(item_code="Test Production Item 1")
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty - before_qty, 1)
pln.make_work_order()
work_orders = []
for row in frappe.get_all("Work Order", filters={"production_plan": pln.name}, fields=["name"]):
wo_doc = frappe.get_doc("Work Order", row.name)
wo_doc.source_warehouse = "_Test Warehouse - _TC"
wo_doc.wip_warehouse = "_Test Warehouse 1 - _TC"
wo_doc.fg_warehouse = "_Test Warehouse - _TC"
for d in wo_doc.required_items:
d.source_warehouse = "_Test Warehouse - _TC"
make_stock_entry(
item_code=d.item_code,
qty=d.required_qty,
rate=100,
target="_Test Warehouse - _TC",
)
wo_doc.submit()
work_orders.append(wo_doc)
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty, before_qty)
rm_work_order = None
for wo_doc in work_orders:
for d in wo_doc.required_items:
if d.item_code == "Raw Material Item 1":
rm_work_order = wo_doc
break
if rm_work_order:
s = frappe.get_doc(make_se_from_wo(rm_work_order.name, "Material Transfer for Manufacture", 1))
s.submit()
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty, before_qty)
def test_resered_qty_for_production_plan_for_less_rm_qty(self):
from erpnext.stock.utils import get_or_make_bin
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
pln = create_production_plan(item_code="Test Production Item 1", planned_qty=10)
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty - before_qty, 10)
pln.make_work_order()
plans = []
for row in frappe.get_all("Work Order", filters={"production_plan": pln.name}, fields=["name"]):
wo_doc = frappe.get_doc("Work Order", row.name)
wo_doc.source_warehouse = "_Test Warehouse - _TC"
wo_doc.wip_warehouse = "_Test Warehouse 1 - _TC"
wo_doc.fg_warehouse = "_Test Warehouse - _TC"
for d in wo_doc.required_items:
d.source_warehouse = "_Test Warehouse - _TC"
d.required_qty -= 5
make_stock_entry(
item_code=d.item_code,
qty=d.required_qty,
rate=100,
target="_Test Warehouse - _TC",
)
wo_doc.submit()
plans.append(pln.name)
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(after_qty, before_qty)
completed_plans = get_non_completed_production_plans()
for plan in plans:
self.assertFalse(plan in completed_plans)
def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self):
from erpnext.stock.utils import get_or_make_bin
@@ -1087,6 +1184,41 @@ class TestProductionPlan(FrappeTestCase):
)
self.assertEqual(reserved_qty_after_mr, before_qty)
def test_from_warehouse_for_purchase_material_request(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_or_make_bin
create_item("RM-TEST-123 For Purchase", valuation_rate=100)
bin_name = get_or_make_bin("RM-TEST-123 For Purchase", "_Test Warehouse - _TC")
t_warehouse = create_warehouse("_Test Store - _TC")
make_stock_entry(
item_code="Raw Material Item 1",
qty=5,
rate=100,
target=t_warehouse,
)
plan = create_production_plan(item_code="Test Production Item 1", do_not_save=1)
mr_items = get_items_for_material_requests(
plan.as_dict(), warehouses=[{"warehouse": t_warehouse}]
)
for d in mr_items:
plan.append("mr_items", d)
plan.save()
for row in plan.mr_items:
if row.material_request_type == "Material Transfer":
self.assertEqual(row.from_warehouse, t_warehouse)
row.material_request_type = "Purchase"
plan.save()
for row in plan.mr_items:
self.assertFalse(row.from_warehouse)
def test_skip_available_qty_for_sub_assembly_items(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
@@ -1131,6 +1263,64 @@ class TestProductionPlan(FrappeTestCase):
if row.item_code == "SubAssembly2 For SUB Test":
self.assertEqual(row.quantity, 10)
def test_transfer_and_purchase_mrp_for_purchase_uom(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
bom_tree = {
"Test FG Item INK PEN": {
"Test RM Item INK": {},
}
}
parent_bom = create_nested_bom(bom_tree, prefix="")
if not frappe.db.exists("UOM Conversion Detail", {"parent": "Test RM Item INK", "uom": "Kg"}):
doc = frappe.get_doc("Item", "Test RM Item INK")
doc.purchase_uom = "Kg"
doc.append("uoms", {"uom": "Kg", "conversion_factor": 0.5})
doc.save()
wh1 = create_warehouse("PNE Warehouse", company="_Test Company")
wh2 = create_warehouse("MBE Warehouse", company="_Test Company")
mrp_warhouse = create_warehouse("MRPBE Warehouse", company="_Test Company")
make_stock_entry(
item_code="Test RM Item INK",
qty=2,
rate=100,
target=wh1,
)
make_stock_entry(
item_code="Test RM Item INK",
qty=2,
rate=100,
target=wh2,
)
plan = create_production_plan(
item_code=parent_bom.item,
planned_qty=10,
do_not_submit=1,
warehouse="_Test Warehouse - _TC",
)
plan.for_warehouse = mrp_warhouse
items = get_items_for_material_requests(
plan.as_dict(), warehouses=[{"warehouse": wh1}, {"warehouse": wh2}]
)
for row in items:
row = frappe._dict(row)
if row.material_request_type == "Material Transfer":
self.assertTrue(row.from_warehouse in [wh1, wh2])
self.assertEqual(row.quantity, 2)
if row.material_request_type == "Purchase":
self.assertTrue(row.warehouse == mrp_warhouse)
self.assertEqual(row.quantity, 12)
def create_production_plan(**args):
"""

View File

@@ -362,10 +362,10 @@ class WorkOrder(Document):
else:
self.update_work_order_qty_in_so()
self.update_ordered_qty()
self.update_reserved_qty_for_production()
self.update_completed_qty_in_material_request()
self.update_planned_qty()
self.update_ordered_qty()
self.create_job_card()
def on_cancel(self):
@@ -1491,37 +1491,47 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
def get_reserved_qty_for_production(
item_code: str, warehouse: str, check_production_plan: bool = False
item_code: str,
warehouse: str,
non_completed_production_plans: list = None,
check_production_plan: bool = False,
) -> float:
"""Get total reserved quantity for any item in specified warehouse"""
wo = frappe.qb.DocType("Work Order")
wo_item = frappe.qb.DocType("Work Order Item")
if check_production_plan:
qty_field = wo_item.required_qty
else:
qty_field = Case()
qty_field = qty_field.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
qty_field = qty_field.else_(wo_item.required_qty - wo_item.consumed_qty)
query = (
frappe.qb.from_(wo)
.from_(wo_item)
.select(
Sum(
Case()
.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
.else_(wo_item.required_qty - wo_item.consumed_qty)
)
)
.select(Sum(qty_field))
.where(
(wo_item.item_code == item_code)
& (wo_item.parent == wo.name)
& (wo.docstatus == 1)
& (wo_item.source_warehouse == warehouse)
& (wo.status.notin(["Stopped", "Completed", "Closed"]))
& (
(wo_item.required_qty > wo_item.transferred_qty)
| (wo_item.required_qty > wo_item.consumed_qty)
)
)
)
if check_production_plan:
query = query.where(wo.production_plan.isnotnull())
else:
query = query.where(
(wo.status.notin(["Stopped", "Completed", "Closed"]))
& (
(wo_item.required_qty > wo_item.transferred_qty)
| (wo_item.required_qty > wo_item.consumed_qty)
)
)
if non_completed_production_plans:
query = query.where(wo.production_plan.isin(non_completed_production_plans))
return query.run()[0][0] or 0.0

View File

@@ -269,6 +269,7 @@ erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
erpnext.patches.v13_0.reset_corrupt_defaults
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
erpnext.patches.v14_0.update_reference_due_date_in_journal_entry
erpnext.patches.v14_0.france_depreciation_warning
[post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
@@ -333,12 +334,13 @@ erpnext.patches.v14_0.update_company_in_ldc
erpnext.patches.v14_0.set_packed_qty_in_draft_delivery_notes
erpnext.patches.v14_0.cleanup_workspaces
erpnext.patches.v14_0.enable_allow_existing_serial_no
erpnext.patches.v14_0.set_report_in_process_SOA
erpnext.patches.v14_0.set_report_in_process_SOA
erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
erpnext.patches.v14_0.update_closing_balances #15-07-2023
execute:frappe.defaults.clear_default("fiscal_year")
execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0)
erpnext.patches.v14_0.correct_asset_value_if_je_with_workflow
erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults
erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger

View File

@@ -0,0 +1,7 @@
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
create_accounting_dimensions_for_doctype,
)
def execute():
create_accounting_dimensions_for_doctype(doctype="Sales Order Item")

View File

@@ -0,0 +1,12 @@
import click
import frappe
def execute():
if "erpnext_france" in frappe.get_installed_apps():
return
click.secho(
"Feature for region France will be remove in version-15 and moved to a separate app\n"
"Please install the app to continue using the regionnal France features: https://github.com/scopen-coop/erpnext_france.git",
fg="yellow",
)

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