Compare commits

...

289 Commits

Author SHA1 Message Date
rohitwaghchaure
d749c4aac2 Revert "feat: Add operating cost based on bom quanity without creating job card" 2023-01-09 23:35:11 +05:30
rohitwaghchaure
20fe1875e7 Merge pull request #33347 from vishdha/fb_based_operating_cost
feat: Add operating cost based on bom quanity without creating job card
2023-01-09 23:31:41 +05:30
Gughan Ravikumar
4d5067d6d4 fix(accounts): currency fields no longer read as strings by validation function in Payment Entry (#33535)
explicitly cast paid_amount and received_amount to float in the Payment Entry set_unallocated_amount validation function
2023-01-09 23:26:51 +05:30
ruthra kumar
bbe5e5d9d6 fix: incorrect warehouse and selling amount on bundled products (#33549) 2023-01-09 22:45:43 +05:30
Sagar Sharma
dd3b79b511 Merge pull request #33562 from ruthra-kumar/better_handling_of_duplicate_bundle_items
fix: better handling of duplicate bundle items
2023-01-09 22:22:57 +05:30
Devin Slauenwhite
6bc8bb26b6 fix: customer/supplier quick entry dialog (#33496)
* fix: readonly primary contact fields.

* refactor: supplier and customer quick entry form into common class.
2023-01-09 22:22:19 +05:30
Sagar Sharma
b56c1ed050 Merge branch 'develop' into better_handling_of_duplicate_bundle_items 2023-01-09 21:48:48 +05:30
Deepesh Garg
f056e9e7c5 Merge pull request #33569 from ruthra-kumar/patch_to_update_new_je_type_in_property_setters
chore: patch property setters for JE with new entry type
2023-01-09 21:01:52 +05:30
Deepesh Garg
57a6c37833 Merge pull request #32933 from AnandBaburajan/asset_depreciation_schedule
feat: separating depreciation schedule from assets into a new doc
2023-01-09 20:47:21 +05:30
Ankush Menat
fa4af2acce refactor: convert heatmap queries to QB (#33581)
Uses new `UnixTimestamp` function, don't backport.
2023-01-09 20:00:22 +05:30
Ankush Menat
7ee151880a chore: instructions for running forks
[skip ci]
2023-01-09 18:16:04 +05:30
Raffael Meyer
a4cb0313dd chore: test with wkhtmltopdf 12.6 (#33554)
* chore: test with latest wkhtmltopdf

* chore: check ubuntu version

* ci: check exit code of wk install

Co-authored-by: Ankush Menat <ankush@frappe.io>
2023-01-09 14:27:34 +05:30
Ankush Menat
1ad1fc4c7d ci: bump node in release workflow (#33574)
[skip ci]
2023-01-09 13:25:05 +05:30
rohitwaghchaure
44f32f7521 Merge pull request #33568 from dj12djdjs/fix-manufacture-field-tab
fix: move `include_item_in_manufacturing` into Manufacturing Tab
2023-01-09 12:25:40 +05:30
ruthra kumar
789e448f0e chore: patch property setters for JE with new entry type 2023-01-08 11:45:28 +05:30
Devin Slauenwhite
e35ac4f8c2 fix: move include in manufacturing to manufacturing tab. 2023-01-07 22:47:16 -05:00
ruthra kumar
c717e87c9e fix: better handling of duplicate bundle items 2023-01-06 16:21:20 +05:30
rohitwaghchaure
525f054c45 Merge pull request #33548 from rohitwaghchaure/fixed-incorrect-wo-status
fix: incorrect status in the work order
2023-01-06 16:01:24 +05:30
Rohit Waghchaure
b0baba84a0 fix: incorrect status in the work order 2023-01-06 14:16:18 +05:30
ruthra kumar
51b082b43c Merge pull request #33537 from ruthra-kumar/sales_partner_in_ar_report
refactor: Sales Partner column in AR and AR Summary Report
2023-01-06 12:07:50 +05:30
Vishal
9fd1c3efb4 fix: test case added for FG_BASED OPERTING COST 2023-01-06 11:18:09 +05:30
Vishal
6b0b49ee3f fix: minor changes added 2023-01-06 11:18:09 +05:30
Vishal
bc1bdd58a7 feat: Add operating cost based on bom quanity without creating job card 2023-01-06 11:18:09 +05:30
Sagar Sharma
f4a1218467 Merge pull request #33543 from s-aga-r/fix/github-issue/25162
chore: enable `No Copy` attribute for `route` in Item Group
2023-01-05 13:01:53 +05:30
s-aga-r
348dc32514 chore: enable No Copy attribute for route in Item Group 2023-01-05 12:25:50 +05:30
rohitwaghchaure
f6da6ece0a Merge pull request #33524 from rohitwaghchaure/revamp-process-loss-feature
refactor: revamp process loss feature & added tab breaks
2023-01-04 18:37:32 +05:30
ruthra kumar
ee94127974 refactor: Sales Partner column in AR and AR Summary Report 2023-01-04 12:41:02 +05:30
Anand Baburajan
3ae47ab32c Merge branch 'develop' into asset_depreciation_schedule 2023-01-04 01:34:08 +05:30
Rohit Waghchaure
524c0994e0 test: test cases for process loss 2023-01-03 22:47:52 +05:30
Deepesh Garg
c78399c618 fix: Missing opening entry in general ledger (#33519) 2023-01-03 21:35:03 +05:30
Deepesh Garg
1a83a67d41 fix: Get payment entry button not visible in Bank Clearance doc (#33518) 2023-01-03 21:34:49 +05:30
Rohit Waghchaure
ae039777f9 refactor: revamp process loss feature & added tab breaks 2023-01-03 19:05:41 +05:30
Sagar Sharma
d3b74a868e Merge pull request #33514 from s-aga-r/fix/github-issue/33417
fix: set `supplier` details while mapping SE(Send to Subcontractor)
2023-01-03 18:57:07 +05:30
Sagar Sharma
9f59330fa9 Merge branch 'develop' into fix/github-issue/33417 2023-01-03 18:51:27 +05:30
Deepesh Garg
a3ab8f973a fix: Deferred revenue date comparison (#33515) 2023-01-03 17:51:41 +05:30
s-aga-r
751bdc98ed fix: set supplier details while mapping SE(Send to Subcontractor) 2023-01-03 17:17:23 +05:30
Deepesh Garg
9a3d947e89 fix: Exchange gain and loss booking on multi-currency invoice reconciliation (#32900)
* fix: Exchange gain and loss booking on multi-curreny invoice reconciliation

* test: Update test cases

* chore: Ignore SQL linting rule

* chore: Joural Entry for exchange gainand loss booking

* chore: Journal entry for exchange gain loss booking

* test: Update test case

* chore: Default exchange gain and loss account
2023-01-03 16:55:15 +05:30
Anand Baburajan
d9c91fa072 Merge branch 'develop' into asset_depreciation_schedule 2023-01-03 15:10:07 +05:30
anandbaburajan
312e51e8b2 chore: more refactoring and adding test for duplicate asset_depr_schedule 2023-01-03 15:09:04 +05:30
anandbaburajan
adb5249257 chore: fix patch 2023-01-03 13:03:12 +05:30
anandbaburajan
7b6b5ae581 chore: refactoring patch 2023-01-03 09:21:08 +05:30
anandbaburajan
e501c2db51 chore: just testing patch 2023-01-02 21:11:53 +05:30
anandbaburajan
357c17214d chore: fix patch 2023-01-02 20:32:29 +05:30
rohitwaghchaure
6a4e25c1f9 Merge pull request #33504 from rohitwaghchaure/fixed-incorrect-picked-qty-in-so
fix: [concurrency issue] incorrect picked qty in sales order
2023-01-02 20:12:05 +05:30
Rohit Waghchaure
aba83849a6 fix: [concurrency issue] incorrect picked qty in sales order 2023-01-02 18:46:22 +05:30
Anand Baburajan
72c2e77a5d Merge branch 'develop' into asset_depreciation_schedule 2023-01-02 17:07:58 +05:30
anandbaburajan
7c5168eeac chore: allow manually submitting and cancelling asset depr schedule 2023-01-02 17:07:28 +05:30
ruthra kumar
914b23038c refactor: Exchange rate revaluation to handle accounts with zero account balance (#33165)
* refactor: new type for JE - Exchange Gain or Loss

* refactor: skip few validations for Exchanage Gain Or Loss type Jour

* refactor: ERR create 2 journals for handling zero and non-zero compa

1. Additional check box accounts table to identify accounts with zero balance
2. Accounts with zero balance only in either of the 2 currencies will be handled on separate Journal

* refactor: skips few validation for allowing 0 debit/credit

* fix: General Ledger presentaion currency

* test: fix test case in general ledger

* test: fix failing test case in AR report
2023-01-02 14:33:14 +05:30
Sagar Sharma
a7b011aed4 Merge pull request #33444 from s-aga-r/fix/github-issue/28766
fix: consider child nodes while getting bin details
2023-01-02 11:54:11 +05:30
s-aga-r
b425c2d7ec Merge branch 'develop' into fix/github-issue/28766 2023-01-02 11:25:13 +05:30
Sagar Sharma
332f4dc028 Merge pull request #33495 from dj12djdjs/fix-get-bin-details
fix(stock): missing ordered_qty in get_bin_details
2023-01-02 11:14:25 +05:30
Sagar Sharma
e18b0ebe92 Merge branch 'develop' into fix-get-bin-details 2023-01-02 10:49:29 +05:30
Sagar Sharma
49025db33c Merge branch 'develop' into fix/github-issue/28766 2023-01-02 10:27:42 +05:30
Marc de Lima Lucio
3c393bfdc5 fix: javascript: execution blocked by undefined route options (#33405)
Some implementations of DocField.get_route_options_for_new_doc() returned no value instead of an empty object in some cases, which caused a JavaScript error.
2023-01-02 09:18:25 +05:30
MOHAMMED NIYAS
d054f37602 feat: calculate hours (#33464)
* feat: calculate hours

* chore: Linting Issues

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-01-02 09:17:14 +05:30
Sabu Siyad
010718ffed fix(ecommerce/cart): explicitly set frappe.boot (#33431)
related: https://github.com/frappe/frappe/pull/18323

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

Signed-off-by: Sabu Siyad <hello@ssiyad.com>
2023-01-02 08:58:01 +05:30
Devin Slauenwhite
98c39c4f5f fix: update payment entry references (#33274)
* fix: set_amounts after deductions and losses are set

* test: difference_amount changes after update_references_in_payment_entry

* chore: linter

* fix: use kwargs instad of destructing a dict

[skip ci]

* fix(test): test payment entry difference_amount after payment reconciliation.
2023-01-02 08:55:12 +05:30
Sagar Sharma
7126d71627 Merge branch 'develop' into fix/github-issue/28766 2023-01-01 22:24:47 +05:30
Devin Slauenwhite
239a5f8bf4 test: get_item_details contains bin details 2022-12-31 15:41:07 -05:00
Devin Slauenwhite
8d62cdfd5f fix: add missing 'ordered_qty' to get_bin_details 2022-12-31 14:14:25 -05:00
Deepesh Garg
bb4725e9b5 Merge pull request #33488 from deepeshgarg007/multi_currency_bank_reco
fix: Multi-currency issues in Bank Reconciliation Tool
2022-12-31 13:05:25 +05:30
Deepesh Garg
ad53ecf2b4 fix: Multi-currency issues in Bank Recociliation Tool 2022-12-30 13:37:41 +05:30
Gughan Ravikumar
48a9cd5ef3 fix: allow arbitary items in Quotation ans Sales Order (#33430)
fix: allow arbitary items in Quotation ans Sales Order
2022-12-30 13:16:40 +05:30
rohitwaghchaure
6e1ef4f3d0 Merge pull request #33487 from frappe/revert-33387-audit-to-fix-incorrect-valuation-entry
Revert "fix: daily scheduler to identify and fix stock transfer entries having incorrect valuation"
2022-12-30 12:58:36 +05:30
rohitwaghchaure
728dc1acf4 Revert "fix: daily scheduler to identify and fix stock transfer entries having incorrect valuation" 2022-12-30 11:08:31 +05:30
Sagar Sharma
d072169340 Merge branch 'develop' into fix/github-issue/28766 2022-12-29 16:38:53 +05:30
s-aga-r
c3911a592a chore: use frappe.qb instead of frappe.db.get_value 2022-12-29 16:38:08 +05:30
s-aga-r
c716dcc01e fix: consider child nodes while getting bin details 2022-12-29 14:01:42 +05:30
Deepesh Garg
617518389a fix: Conversion factor error for invoices without item code (petty expenses) (#32714)
* fix: Set default uom conversion factor to 1 for invoices

* chore: set default conversion_factor as 1

* chore: remove print statements
2022-12-29 10:41:36 +05:30
Devin Slauenwhite
123920d0bc feat: add after_refresh hook to item dashboard (#33372)
* fix: return promise

* fix: use after_refresh hook instead of promise

# Because there is already a before_refresh hook. I think it makes sense to do the same for after.
2022-12-29 10:31:26 +05:30
ruthra kumar
e3a0ce5d63 fix: use base_net_amount in case of missing stock qty (#33457) 2022-12-29 09:34:51 +05:30
Dany Robert
cabaed9ed2 fix(pricing rule): consider child tables in condition (#33469) 2022-12-28 18:05:55 +05:30
Sagar Sharma
882f92e732 Merge pull request #33465 from s-aga-r/fix/po-fg_item_qty
fix: `fg_item_qty` in non-subcontracted PO
2022-12-28 15:27:18 +05:30
s-aga-r
6f5824cb21 fix: fg_item_qty in non-subcontracted PO 2022-12-28 14:54:24 +05:30
Sabu Siyad
8e271fd719 feat(exotel): make use of CustomField in API (#33338)
* feat(exotel): pass kwargs for `make_a_call`

https://developer.exotel.com/api/make-a-call-api#call-agent

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

* feat(exotel): map custom field to doctype

Signed-off-by: Sabu Siyad <hello@ssiyad.com>

Signed-off-by: Sabu Siyad <hello@ssiyad.com>
2022-12-28 08:11:28 +05:30
Deepesh Garg
0b75aa5390 fix: Default dimensions on fetching items from BOM (#33439) 2022-12-27 17:53:43 +05:30
Deepesh Garg
d2686ce75b fix: Multiple rows for same warehouse and batches in pick list (#33456) 2022-12-27 17:42:03 +05:30
anandbaburajan
21a09e7431 chore: fix patch 2022-12-27 15:01:42 +05:30
anandbaburajan
97f8588202 chore: fix patch name 2022-12-27 15:01:42 +05:30
anandbaburajan
c916fb0513 chore: fix module name in patch 2022-12-27 15:01:42 +05:30
anandbaburajan
57adcc0b03 chore: move patch to pre_model_sync 2022-12-27 15:01:42 +05:30
Anand Baburajan
4ff70db668 Merge branch 'develop' into asset_depreciation_schedule 2022-12-27 14:59:53 +05:30
Deepesh Garg
8263bf9a9a fix: Random behaviour while picking items using picklist (#33449) 2022-12-27 14:59:35 +05:30
anandbaburajan
482116db9f chore: improve tests 2022-12-27 10:03:32 +05:30
Anand Baburajan
6f4e6fafa9 Merge branch 'develop' into asset_depreciation_schedule 2022-12-27 07:36:32 +05:30
anandbaburajan
6d791cabd1 chore: more refactoring 2022-12-26 22:13:11 +05:30
Sagar Sharma
4e2291f52d Merge pull request #33437 from s-aga-r/fix/purchase-order/shipping_address
fix: `shipping_address` in PO for non-drop ship item
2022-12-26 21:20:40 +05:30
Sagar Sharma
88edc412e3 Merge branch 'develop' into fix/purchase-order/shipping_address 2022-12-26 12:00:39 +05:30
s-aga-r
67a7ccf3ce fix: shipping_address for non-drop shipping item 2022-12-26 11:57:50 +05:30
Deepesh Garg
8a79efab4b feat: Accounting Dimension updation in Payment Request and Entry (#33411) 2022-12-26 10:17:55 +05:30
Solufyin
7d9f3f23dd fix: Customer Primary Contact (#33424)
Co-authored-by: Nihantra C. Patel <n.patel.serpentcs@gmail.com>
2022-12-26 10:15:10 +05:30
Sagar Sharma
9858d9d968 Merge pull request #33434 from s-aga-r/fix/github-issue/32258
fix(ux): `shipping_address` in PO
2022-12-25 19:40:09 +05:30
Sagar Sharma
32f3b64a89 Merge branch 'develop' into fix/github-issue/32258 2022-12-25 18:12:08 +05:30
s-aga-r
7e1b6b3c2a fix: shipping_address in PO 2022-12-25 18:11:05 +05:30
anandbaburajan
03662be150 chore: have different functions for draft. active and cancelled depr schedules 2022-12-25 18:05:19 +05:30
ruthra kumar
a31e9d3afe Merge pull request #33402 from ruthra-kumar/hidden_party_detail_fields_for_better_handling_of_permission
refactor: Customer and Supplier Ledger summary will have hidden fields for better handling of user permission
2022-12-25 16:25:13 +05:30
ruthra kumar
a0484793c3 refactor: remove unrelated filters from Supplier Ledger Summary
Territory, Sales Partner and Sales Person doesn't belong in Supplier Ledger
2022-12-24 11:20:39 +05:30
ruthra kumar
89229d9b58 refactor: additional doctype access for 'Accounts User' role 2022-12-24 11:20:16 +05:30
ruthra kumar
bfa511cb65 refactor: hidden columns to help framework handle user permissions
'territory', 'supplier_group', 'customer_group' have been added as
hidden columns to handle 'user permission' based access control.
2022-12-24 10:30:30 +05:30
rohitwaghchaure
40769998c6 Merge pull request #33422 from rohitwaghchaure/fix-stock-entry-submit-performance
fix: timeout error while submitting stock entry
2022-12-23 13:57:25 +05:30
Rohit Waghchaure
a05c47e499 fix: timeout error while submitting stock entry
Co-authored-by: Ankush Menat <ankush@frappe.io>
2022-12-23 13:27:32 +05:30
Anand Baburajan
b17ef34042 Merge branch 'develop' into asset_depreciation_schedule 2022-12-23 11:02:17 +05:30
anandbaburajan
3e91a56c19 chore: only consider draft and active assets in patch, and allow asset depr schedule to be manually createed 2022-12-23 11:01:18 +05:30
ruthra kumar
8f5d8e0474 Merge pull request #33418 from ruthra-kumar/filter_issue_in_ar_ap_report
fix: payment terms and sales partner filter issue in Accounts Receivable report
2022-12-22 11:38:38 +05:30
ruthra kumar
13c4420f42 fix: payment terms and sales partner filter issue in AR/AP report 2022-12-22 10:55:22 +05:30
ruthra kumar
a26698b88e Merge pull request #33410 from ruthra-kumar/typeerror_on_gp_due_to_incorrect_warehouse_on_bundle_item
fix: TypeError on GP Report due to multiple warehouse on bundled items
2022-12-21 18:26:40 +05:30
ruthra kumar
5918bb03f7 test: type error on bundled products with different warehouses 2022-12-21 17:47:57 +05:30
ruthra kumar
e684eb32d0 fix: typerror on multi warehouse in Packed Items
DN(with bundled item with varying warehouses)-> Sales Invoice.
2022-12-21 15:09:55 +05:30
Deepesh Garg
76be35aaea Merge pull request #33406 from deepeshgarg007/cs_cf_report_fix
fix: Consolidated financial report
2022-12-20 18:22:58 +05:30
Deepesh Garg
ade83cbab3 fix: Consolidated financial report 2022-12-20 16:45:46 +05:30
Deepesh Garg
1da14b7ec3 Merge pull request #33393 from deepeshgarg007/cc_filter_cashflow_report
fix: Cost center filter not working in cash flow report
2022-12-20 13:46:18 +05:30
Deepesh Garg
925387550e Merge pull request #33350 from deepeshgarg007/payment_request_flow
fix: Payment Request flow fixes from Order to Payment Entry
2022-12-20 13:45:15 +05:30
Deepesh Garg
9dc1509cf5 Merge pull request #33358 from resilient-tech/fix-appointment-booking
fix: remove unnecessary permissions from Appointment and Appointment Booking Settings
2022-12-20 11:53:01 +05:30
Deepesh Garg
31c95deb88 chore: More fixes 2022-12-20 11:45:51 +05:30
Deepesh Garg
ff48634cbb Merge branch 'develop' of https://github.com/frappe/erpnext into payment_request_flow 2022-12-20 11:42:15 +05:30
Deepesh Garg
e25b98b620 chore: Update test case 2022-12-20 11:42:05 +05:30
Deepesh Garg
068df9f815 chore: remove print statement 2022-12-20 10:00:53 +05:30
Deepesh Garg
d0dbfec052 fix: Cost center filter not working in cash flow report 2022-12-20 09:59:27 +05:30
rohitwaghchaure
1ed2230e4c Merge pull request #33382 from rohitwaghchaure/fixed-pick-list-issue
fix: unsupported operand type(s) for +=: 'int' and 'NoneType'
2022-12-20 09:47:29 +05:30
rohitwaghchaure
eaa04abe29 Merge pull request #33387 from rohitwaghchaure/audit-to-fix-incorrect-valuation-entry
fix: daily scheduler to identify and fix stock transfer entries having incorrect valuation
2022-12-20 09:46:57 +05:30
Rohit Waghchaure
f31612376a test: added test case to validate audit for incorrect entries 2022-12-20 00:14:41 +05:30
Rohit Waghchaure
b1721b79ce fix: daily scheduler to identify and fix stock transfer entries having incorrect valuation 2022-12-19 23:33:44 +05:30
rohitwaghchaure
7851adb49a Merge branch 'develop' into fixed-pick-list-issue 2022-12-19 23:32:06 +05:30
Ankush Menat
88ce11f03d fix: incorrect type hints (#33381)
[skip ci]
2022-12-19 16:44:19 +05:30
Rohit Waghchaure
2b4eae5f84 fix: unsupported operand type(s) for +=: 'int' and 'NoneType' 2022-12-19 16:24:55 +05:30
ruthra kumar
65db9cea25 Merge pull request #33380 from ruthra-kumar/err_for_invoices_should_reflect_in_ar_ap_report
fix: ERR journals should reported in AR/AP
2022-12-18 16:56:19 +05:30
ruthra kumar
2ed86760d7 test: err for party should be in AR/AP report 2022-12-18 12:40:07 +05:30
ruthra kumar
b09eade3e4 fix: ERR journals reported in AR/AP
Exchange Rate Revaluation on Receivable/Payable will included in AR/AP report
2022-12-18 06:38:23 +05:30
Gokulnath
4ecce242a8 feat: adding warehouse filter for sales order ananlysis report
feat: adding warehouse filter for sales order ananlysis report
2022-12-17 20:06:01 +05:30
Deepesh Garg
67c5cec505 Merge pull request #33339 from barredterra/print-rfq
feat: more control when printing RFQ
2022-12-17 19:24:57 +05:30
Deepesh Garg
6238fd9d9e Merge pull request #33341 from deepeshgarg007/tds_tcs_cost_center
fix: Cost Center for tax withholding invoices
2022-12-17 19:24:34 +05:30
Deepesh Garg
2a4eec245a Merge pull request #33371 from deepeshgarg007/coa_import_srbnb
fix: Unable to import COA through importer
2022-12-17 16:42:37 +05:30
Deepesh Garg
3b66920342 fix: Unable to import COA through importer 2022-12-16 19:22:29 +05:30
Deepesh Garg
0e86cde64b Merge branch 'develop' into print-rfq 2022-12-16 16:24:02 +05:30
Sagar Vora
9989ccbb02 Merge branch 'develop' into fix-appointment-booking 2022-12-16 10:36:48 +00:00
Daizy Modi
4802d719ed fix: removed unused data and minor changes 2022-12-16 15:55:58 +05:30
Deepesh Garg
6bb22c703a Merge pull request #33275 from pps190/fix-pe-exchange-rate-precision
fix: use highest precision for exchange rate.
2022-12-16 15:43:36 +05:30
Deepesh Garg
50421b3d7d Merge pull request #33315 from kunhimohamed/develop
fix: at create_customer_or_supplier on session creation
2022-12-16 15:04:10 +05:30
Deepesh Garg
b76129c1dd Merge pull request #33286 from artykbasar/develop
Subscription Cost center value is fixed to Default value(Bug fix)
2022-12-16 14:44:51 +05:30
Deepesh Garg
5c50a46822 Merge pull request #33362 from deepeshgarg007/exclude_gst_credential_doctype
feat: Ignore company related doctype for other apps via hooks
2022-12-16 11:07:47 +05:30
Daizy Modi
5d0b5c8d2a fix: pass necessary params instead of args 2022-12-16 10:10:29 +05:30
Deepesh Garg
1a40c04b72 feat: Ignore company related doctype for other apps via hooks 2022-12-15 18:51:58 +05:30
rohitwaghchaure
da1d8da355 Merge pull request #33355 from rohitwaghchaure/fixed-do-not-show-disabled-items
fix: disabled items showing in the report Itemwise Recommended Reorder Level
2022-12-15 18:08:05 +05:30
Daizy Modi
56f3ac15d8 fix: removed unncessary changes 2022-12-15 18:02:14 +05:30
Daizy Modi
4bfe2ea572 fix: agent assignment and permissions for appointment 2022-12-15 17:19:28 +05:30
rohitwaghchaure
5cbbb5d59e Merge pull request #33354 from rohitwaghchaure/fixed-trivial-discount-amount
fix: unsupported operand type(s) for +: 'int' and 'NoneType'
2022-12-15 17:13:46 +05:30
Rohit Waghchaure
ae31ff1c48 fix: disabled items showing in the report 'Itemwise Recommended Reorder Level
'
2022-12-15 17:05:53 +05:30
Daizy Modi
ac51c27500 fix: fetch required details from appointment booking settings 2022-12-15 16:53:51 +05:30
Daizy Modi
9a00b3bdbd fix: format html of verify email for book appointment 2022-12-15 16:51:00 +05:30
Daizy Modi
25f0b26a17 fix: remove unused page book-appointment 2022-12-15 16:49:23 +05:30
Daizy Modi
ae8dd2b2bc fix: remove guest role from doctype 2022-12-15 16:47:27 +05:30
Rohit Waghchaure
0f28074e5a fix: unsupported operand type(s) for +: 'int' and 'NoneType' 2022-12-15 16:36:47 +05:30
ruthra kumar
d126c8030f Merge pull request #33335 from ruthra-kumar/cost_center_issue_on_reconciliation_tool
fix: paid invoices shows up as outstanding when 'cost_center' filter is applied
2022-12-15 15:02:18 +05:30
Anand Baburajan
246d3b4075 Merge branch 'develop' into asset_depreciation_schedule 2022-12-15 14:51:16 +05:30
anandbaburajan
a358f77774 chore: move patch to post_model_sync and cancel asset depr schedule properly 2022-12-15 14:50:53 +05:30
ruthra kumar
a998a8a2da test: cost center should not affect outstanding calculation 2022-12-15 14:28:33 +05:30
Deepesh Garg
dc178984ae fix: Payment Request flow fixes from Order to Payment Entry 2022-12-15 14:15:20 +05:30
anandbaburajan
7f5e761c04 chore: add patch 2022-12-15 13:13:04 +05:30
Deepesh Garg
3a8a467edb Merge pull request #33329 from Gokulnath17/Gokulnath_dev
fix: in gross profit report
2022-12-15 12:01:39 +05:30
Deepesh Garg
076527bb88 Merge pull request #33305 from barredterra/incoterm-named-place
feat: incoterm named place
2022-12-15 11:59:54 +05:30
Gokulnath
f73976946e Merge branch 'develop' into Gokulnath_dev 2022-12-15 10:22:49 +05:30
Deepesh Garg
b7e9942681 Merge pull request #33309 from barredterra/refactor-validate_payment_against_negative_invoice
refactor: translatable strings and guard clause
2022-12-15 09:21:16 +05:30
rohitwaghchaure
9bf2682218 Merge pull request #33323 from barredterra/translate-overbilling-overdelivery
fix: translatability of warning on overbilling/-receipt/-delivery
2022-12-14 23:24:11 +05:30
Deepesh Garg
26277cfcf3 chore: resolve errors in test 2022-12-14 21:22:48 +05:30
Deepesh Garg
973ef33eb5 fix: Cost Center for tax withholding invoices 2022-12-14 20:41:00 +05:30
barredterra
8717148d9b feat: improve visibility of default values 2022-12-14 14:14:29 +01:00
barredterra
ce9626fead feat: more control when printing RFQ 2022-12-14 14:03:36 +01:00
Gokulnath
d404a03947 Merge branch 'develop' into Gokulnath_dev 2022-12-14 17:38:16 +05:30
ruthra kumar
8eb93004f7 fix: cost_center filter fix for 'Get Outstanding Invoice' in PE 2022-12-14 16:16:22 +05:30
rohitwaghchaure
21c86bd649 Merge pull request #33332 from rohitwaghchaure/fixed--missing-required-argument
fix: get_serial_nos_for_fg() missing 1 required positional argument: …
2022-12-14 16:06:58 +05:30
ruthra kumar
6d9d730759 fix: cost_center filter gives incorrect output
filtering on cost center gives invoices that are reconciled as having outstanding
2022-12-14 16:05:15 +05:30
Rohit Waghchaure
410a58b3de fix: get_serial_nos_for_fg() missing 1 required positional argument: 'args' 2022-12-14 15:33:36 +05:30
anandbaburajan
c99cd74a15 chore: handle some error cases and add empty patch file 2022-12-14 13:20:37 +05:30
Gokulnath17
41fc3be339 fixes in gross profit report 2022-12-14 10:46:11 +05:30
barredterra
36997d9788 fix: translation for warning on Overbilling/-receipt/-delivery 2022-12-13 18:59:20 +01:00
Anand Baburajan
8534390627 Merge branch 'develop' into asset_depreciation_schedule 2022-12-13 20:49:06 +05:30
anandbaburajan
22ef342ac7 chore: fixing all tests 2022-12-13 20:48:34 +05:30
anandbaburajan
df134c7c5b chore: fixing some tests 2022-12-13 16:26:33 +05:30
Nabin Hait
5ea6080a6e Merge pull request #33300 from ssiyad/fix/pos/var_typo
fix(pos): variable typo: `s_pos` -> `is_pos`
2022-12-13 15:21:10 +05:30
Nabin Hait
c5d21b2461 Merge pull request #33314 from s-aga-r/fix/github-issue/33145
fix: `Enough Parts to Build` in `BOM Stock Report`
2022-12-13 15:19:40 +05:30
ruthra kumar
3db446e35a Merge pull request #33313 from nabinhait/tax_detail_report_perm_issue
fix: Permission issue in Tax Detail report
2022-12-13 14:27:08 +05:30
kunhi
15e3b7f218 fix: at create_customer_or_supplier on session creation 2022-12-13 12:41:56 +04:00
Sagar Sharma
f9be137cf5 Merge branch 'develop' into fix/github-issue/33145 2022-12-13 14:00:51 +05:30
s-aga-r
723c64ba73 fix: Enough Parts to Build in BOM Stock Report 2022-12-13 14:00:30 +05:30
Nabin Hait
aa787e4030 fix: Permission issue in Tax Detail report 2022-12-13 13:16:24 +05:30
ruthra kumar
2175784142 Merge pull request #33303 from ruthra-kumar/incorrect_balance_on_consolidate_balance_sheet
fix: incorrect balance on parent company on consolidate Balance sheet due to key mismatch
2022-12-13 09:04:13 +05:30
barredterra
f20370c5ef refactor: translatable strings and guard clause 2022-12-13 02:36:12 +01:00
Sagar Sharma
cb49055aac Merge pull request #33304 from s-aga-r/fix/so-mr-ref
fix: `Material Request` reference in internal `Sales Order`
2022-12-12 23:48:14 +05:30
barredterra
e057e1dfe7 feat: incoterm named place 2022-12-12 18:49:47 +01:00
s-aga-r
78b438f6cf fix: Material Request reference in internal Sales Order 2022-12-12 22:41:44 +05:30
ruthra kumar
7b3316dc31 fix: incorrect balance on parent company due to key mismatch 2022-12-12 20:15:59 +05:30
rohitwaghchaure
5c0f8ee44f Merge pull request #33281 from tundebabzy/29976
fix: maintain same rate throughout sales cycle doesn't work with quotation
2022-12-12 19:30:50 +05:30
Sabu Siyad
7d64bf78cf fix(pos): variable typo: s_pos -> is_pos
Signed-off-by: Sabu Siyad <hello@ssiyad.com>
2022-12-12 19:09:00 +05:30
anandbaburajan
28d4942d6c chore: fix validation and add make_schedules_editable 2022-12-12 18:19:34 +05:30
anandbaburajan
1ea2ba0dea chore: handle some cases where asset_depr_schedule doesn't exist 2022-12-12 17:31:57 +05:30
anandbaburajan
b997d1eb87 chore: add some validation, shorten some function names 2022-12-12 17:25:38 +05:30
Anand Baburajan
f8cd424559 Merge branch 'develop' into asset_depreciation_schedule 2022-12-12 16:36:59 +05:30
anandbaburajan
ec6505d747 chore: fix some bugs, refactor some functions, add proper notes 2022-12-12 16:35:30 +05:30
Ankush Menat
915e0347b0 chore!: remove activity log feeds (#33294)
- This contains little to no information and practically no one uses this.
- Also causes a lot of problem by adding way too many feeds in activity
  log to the point where activity page doesn't even load.
2022-12-12 15:14:30 +05:30
Ankush Menat
593626f502 perf: add indexes on payment entry reference (#33288)
Adds index on:
1. reference doctype
2. reference name

*Why not composite index?*

There are three type of queries on this doctype

- filtering ref_doctype - doctype index helps here
- filtering ref_name - name index helps here
- filtering both - name index helps here too. Since it has sufficiently
  high cardinality. Composite index wont help in case where ref_doctype
  isn't specfied.


[skip ci]
2022-12-12 12:58:11 +05:30
artykbasar
d512919f5c Subscription Cost center value is fixed to Default value(Bug fix)
self.cost_center 
    field value keeps changing to 
erpnext.get_default_cost_center(self.get("company"))
    after validation of this doctype. This overrides the user input.
simple fix, it will first check if the field is empty, if empty then puts the company's default cost center value if not then user input will be saved.
2022-12-11 08:51:34 +00:00
Tunde Akinyanmi
71aa8c5e1c test: refactor test case 2022-12-09 14:33:54 +01:00
Tunde Akinyanmi
d193a14b8f test: ensure test case sets Selling Settings 2022-12-09 14:09:54 +01:00
anandbaburajan
90e1b9cb3d chore: fix asset depr schedule notes 2022-12-09 16:18:40 +05:30
Tunde Akinyanmi
97ddfcfc7c fix: Maintain Same Rate Throughout Sales Cycle doesn't work
Issue #29976 was partly fixed by #32923 but the problem still persists.
The reason is because an incorrect fieldname was passed to the
`validate_rate_with_reference_doc` method.
This commit fixes it
2022-12-09 10:29:28 +01:00
anandbaburajan
16365bfca3 chore: more refactoring 2022-12-09 14:04:00 +05:30
Devin Slauenwhite
2010b1b6e8 fix: use highest precision for exchange rate. 2022-12-08 16:26:07 -05:00
anandbaburajan
77dc8e7966 fix: bug in posting depr entries 2022-12-09 00:58:19 +05:30
anandbaburajan
187e1a324a chore: refactor get_depreciable_assets 2022-12-09 00:46:56 +05:30
anandbaburajan
1fd73af744 chore: refactor some reports 2022-12-09 00:28:34 +05:30
Deepesh Garg
7d41f63c5d Merge pull request #33200 from deepeshgarg007/buying_selling_pricing_rule
fix: Buying and selling check in pricing rule
2022-12-08 18:41:42 +05:30
Deepesh Garg
6db52930d8 Merge branch 'develop' of https://github.com/frappe/erpnext into buying_selling_pricing_rule 2022-12-08 18:05:05 +05:30
Deepesh Garg
6192af5cf0 chore: Update tests 2022-12-08 18:04:40 +05:30
Deepesh Garg
b179514273 Merge pull request #33269 from ssiyad/fix/ecommerce/breadcrumb_base
fix(ecommerce): remove query parameters from referrer
2022-12-08 17:11:34 +05:30
Sabu Siyad
b6bd408f19 fix(ecommerce): remove query parameters from referer
inclusion of query parameters results in logic failure

example:
- logic check if referrer is `all-products`
- `http://shop.example/all-products` -> `all-products`, valid outcome
- `http://shop.example/all-products?start=1` -> `all-products?start=1`,
  invalid outcome

Signed-off-by: Sabu Siyad <hello@ssiyad.com>
2022-12-08 16:47:50 +05:30
Ankush Menat
0b86b1baca refactor: make payments app a soft dependency (#33245)
refactor: make payment app a soft dependency
2022-12-08 16:40:13 +05:30
Deepesh Garg
07c815f35f Merge pull request #33253 from niyazrazak/patch-9
feat: get lead with WhatsApp number
2022-12-08 15:19:13 +05:30
Deepesh Garg
81397ba3ee Merge pull request #33227 from barredterra/add-translation-variable-order
fix: add translation variable index
2022-12-08 15:17:07 +05:30
Deepesh Garg
f0487451f7 Merge pull request #33248 from QwQuan/develop
fix: bugs in zh.csv
2022-12-08 15:16:16 +05:30
ruthra kumar
1d37c0143f Merge pull request #33254 from ruthra-kumar/index_error_on_customer_master
fix: index error on customer master
2022-12-08 12:52:20 +05:30
rohitwaghchaure
e451639ff6 Merge pull request #33258 from rohitwaghchaure/fixed-total-value-in-report
fix: total value in Warehouse Wise Stock Balance
2022-12-08 10:13:03 +05:30
Rohit Waghchaure
f598da7c81 fix: total value in Warehouse Wise Stock Balance 2022-12-08 00:55:45 +05:30
rohitwaghchaure
5e0f4eb580 Merge pull request #33255 from s-aga-r/fix/github-issue/33212
fix: order status in `Production Planning Report`
2022-12-07 23:36:12 +05:30
s-aga-r
632c08f7e0 fix: order status in Production Planning Report 2022-12-07 22:47:06 +05:30
ruthra kumar
cf1e3dc8ea fix: index error on customer master 2022-12-07 20:48:40 +05:30
MOHAMMED NIYAS
922375c3ba feat: get lead with WhatsApp number 2022-12-07 17:50:35 +05:30
wuzhouquan
a6fe79f29d fix: bugs in zh.csv 2022-12-07 15:04:56 +08:00
anandbaburajan
18fc2b5695 chore: fix bug in unlink_asset_reference 2022-12-06 23:34:56 +05:30
anandbaburajan
2cd42dbb10 chore: refactor get_finance_book_value_map 2022-12-06 23:16:02 +05:30
anandbaburajan
e7d404a30a chore: remove finance_book and finance_book_id from depreciation_schedule and refactor some functions 2022-12-06 21:33:20 +05:30
anandbaburajan
8365d6bdb7 fix: refactor reverse_depreciation_entry_made_after_disposal and fix fb bug 2022-12-06 17:58:38 +05:30
rohitwaghchaure
7cbb6c4de9 Merge pull request #33236 from rohitwaghchaure/feat-warehouse-wise-stock-balance
feat: warehouse wise stock balance
2022-12-06 17:58:31 +05:30
Deepesh Garg
b602a0dcb0 Merge pull request #33146 from barredterra/reload-currency-exchange-settings
fix: reload Currency Exchange Settings in patch
2022-12-06 17:38:13 +05:30
Deepesh Garg
26adbc6282 Merge pull request #33192 from deepeshgarg007/bundle_item_rates
fix: Bundle item rates
2022-12-06 17:37:22 +05:30
Rohit Waghchaure
861aa9e08a feat: warehouse wise stock balance 2022-12-06 17:33:33 +05:30
Deepesh Garg
c1a82dc9e5 Merge pull request #33191 from ruthra-kumar/key_error_in_profit_loss_report
fix: key error while filtering on date range and reporting on foreign currency
2022-12-06 15:42:53 +05:30
Deepesh Garg
b1242bc56c chore: Update tests 2022-12-06 15:14:48 +05:30
Deepesh Garg
af1f98e188 Merge branch 'develop' of https://github.com/frappe/erpnext into bundle_item_rates 2022-12-06 13:59:51 +05:30
Deepesh Garg
e5566b31d5 chore: Consider bundle qty as well 2022-12-06 13:59:45 +05:30
ruthra kumar
a6794c3606 fix: key error on p/l and balance sheet reports on foreign currency 2022-12-06 13:48:46 +05:30
ruthra kumar
19db7e2989 fix: replace sql code with fields list in get_cached_value 2022-12-06 13:48:46 +05:30
Ankush Menat
d23b5d8f2f ci: use mariadb 10.6 (#33220)
https://github.com/frappe/frappe/pull/19116

[skip ci]
2022-12-06 12:58:07 +05:30
Deepesh Garg
01b84a9751 Merge pull request #33194 from barredterra/validate-employee-dates
refactor: validate dates in Employee
2022-12-06 12:34:15 +05:30
Deepesh Garg
3aa6f97420 Merge pull request #33216 from barredterra/validate-accounts-dates
refactor: validate dates in accounts module
2022-12-06 12:33:01 +05:30
Deepesh Garg
63393fa503 Merge pull request #33217 from barredterra/validate-project-dates
refactor: validate dates in project and task
2022-12-06 12:32:27 +05:30
Deepesh Garg
921f8edde8 Merge pull request #33219 from deepeshgarg007/internal_transfer_editable_rate
fix: Allow item rate updates for non-stock invoices
2022-12-06 09:12:24 +05:30
Raffael Meyer
5853b80d25 Merge branch 'develop' into reload-currency-exchange-settings 2022-12-06 02:30:55 +01:00
Raffael Meyer
8301d3b13f Merge branch 'develop' into key_error_in_profit_loss_report 2022-12-06 02:08:47 +01:00
barredterra
a26a29f33b fix: incorrect dates in test records 2022-12-05 19:49:05 +01:00
Raffael Meyer
a074ffa880 Merge branch 'develop' into validate-employee-dates 2022-12-05 19:38:01 +01:00
Raffael Meyer
e526a0e282 Merge branch 'develop' into validate-accounts-dates 2022-12-05 19:37:54 +01:00
Raffael Meyer
a2abc879c9 Merge branch 'develop' into validate-project-dates 2022-12-05 19:37:46 +01:00
barredterra
748c74ba52 fix: add translation variable order 2022-12-05 18:24:22 +01:00
rohitwaghchaure
d0478ec3b8 Merge pull request #33224 from rohitwaghchaure/fixed-partial-work-order-incorrect-batch-picked
fix: non empty FG batch picked while completing work order
2022-12-05 20:29:19 +05:30
Rohit Waghchaure
713330cbf6 fix: non empty FG batch picked while completing work order 2022-12-05 18:15:13 +05:30
Deepesh Garg
0581000990 fix: Remove free items 2022-12-05 17:59:02 +05:30
ruthra kumar
7bd0e977bf Merge pull request #33222 from ruthra-kumar/data_import_errors_for_sales_invoice
fix: data import mandatory account_head, charge_type
2022-12-05 16:56:43 +05:30
ruthra kumar
3814db02eb fix: data import mandatory account_head, charge_type 2022-12-05 16:24:05 +05:30
Deepesh Garg
55c0770280 Merge branch 'develop' of https://github.com/frappe/erpnext into buying_selling_pricing_rule 2022-12-05 15:43:29 +05:30
Deepesh Garg
5f821b93a5 chore: Add POS Invoices 2022-12-05 15:43:03 +05:30
Deepesh Garg
ef9d126254 fix: Allow item rate udpates for non-stock invoices 2022-12-05 10:17:19 +05:30
anandbaburajan
83ed93fbb6 chore: refactor chart and manual set_accumulated_depreciation logic 2022-12-04 22:45:48 +05:30
barredterra
31db0e7c79 refactor: validate parent_expected_end_date in Task 2022-12-04 15:28:38 +01:00
barredterra
2c4eb371a6 refactor: validate dates in project and task 2022-12-04 15:15:07 +01:00
anandbaburajan
ca8c827492 chore: refactor schedules in split_asset functions 2022-12-04 19:22:58 +05:30
barredterra
eb66b749b2 refactor: validate dates in accounts module 2022-12-04 14:41:21 +01:00
Raffael Meyer
083a954b5d Merge branch 'develop' into validate-employee-dates 2022-12-04 14:30:51 +01:00
Deepesh Garg
b741ae143c fix: Reapply pricing rule on qty change 2022-12-02 17:14:06 +05:30
Deepesh Garg
f54838ab56 fix: Buying and selling check in pricing rule 2022-12-01 21:23:17 +05:30
barredterra
03f7bfbbde refactor: validate dates 2022-12-01 12:42:03 +01:00
Deepesh Garg
826f45ad60 fix: Bundle item rates 2022-12-01 16:11:10 +05:30
anandbaburajan
f20238fa3e chore: use make_new_active_asset_depr_schedules_and_cancel_current_ones instead of prepare_depreciation_data 2022-12-01 15:15:32 +05:30
ruthra kumar
9b8d6fe411 fix: key error while filtering on date range and different currency 2022-12-01 13:38:41 +05:30
anandbaburajan
96ede2fcf9 fix: bug in new depr schedule 2022-11-29 21:30:23 +05:30
barredterra
06e094b5fc fix: reload currency exchange settings 2022-11-28 22:58:31 +01:00
anandbaburajan
7d09440579 chore: more refactoring 2022-11-25 16:49:44 +05:30
anandbaburajan
4e63ba3ac6 chore: refactor schedules in journal_entry 2022-11-24 17:39:11 +05:30
anandbaburajan
de17367a36 chore: finish refactoring asset_value_adjustment 2022-11-24 16:16:36 +05:30
anandbaburajan
f35b19eac3 chore: renaming functions and variables again, and continuing refactoring AssetValueAdjustment 2022-11-24 15:33:21 +05:30
anandbaburajan
7db66b0916 chore: renaming some functions and variables, and partially refactoring AssetValueAdjustment 2022-11-24 13:02:48 +05:30
anandbaburajan
76f28de7eb chore: refactor more functions to use new depr schedule 2022-11-23 18:04:54 +05:30
Anand Baburajan
a66d9f8e8e Merge branch 'develop' into asset_depreciation_schedule 2022-11-17 20:17:21 +05:30
anandbaburajan
417180e6ba chore: refactor some functions to use new depr schedule 2022-11-14 17:51:11 +05:30
anandbaburajan
ffd41703de chore: add expected_value_after_useful_life in depr schedule 2022-11-14 14:36:10 +05:30
anandbaburajan
ca3581d055 chore: rename modify_draft_asset_depreciation_schedules 2022-11-14 12:47:48 +05:30
anandbaburajan
ed495dc846 chore: add convert_draft_asset_depreciation_schedules_into_active and fix some bugs 2022-11-14 12:45:49 +05:30
anandbaburajan
97ab7bd36b chore: remove some unwanted functions in asset 2022-11-11 16:27:29 +05:30
anandbaburajan
710d2452b7 feat: moving, refactoring and adding functions to asset_depreciation_schedule 2022-11-11 16:20:17 +05:30
anandbaburajan
5a274176d3 feat: add asset_depreciation_schedule 2022-11-01 11:45:17 +05:30
193 changed files with 4707 additions and 2329 deletions

View File

@@ -24,15 +24,14 @@ fi
if [ "$DB" == "mariadb" ];then
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe"
mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES"
fi
if [ "$DB" == "postgres" ];then
@@ -42,12 +41,17 @@ fi
install_whktml() {
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
sudo chmod o+x /usr/local/bin/wkhtmltopdf
if [ "$(lsb_release -rs)" = "22.04" ]; then
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
sudo apt install /tmp/wkhtmltox.deb
else
echo "Please update this script to support wkhtmltopdf for $(lsb_release -ds)"
exit 1
fi
}
install_whktml &
wkpid=$!
cd ~/frappe-bench || exit
@@ -61,6 +65,8 @@ bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
wait $wkpid
bench start &> bench_run_logs.txt &
CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes

View File

@@ -9,8 +9,8 @@
"mail_password": "test",
"admin_password": "admin",
"root_login": "root",
"root_password": "travis",
"root_password": "root",
"host_name": "http://test_site:8000",
"install_apps": ["erpnext"],
"install_apps": ["payments", "erpnext"],
"throttle_user_limit": 100
}

View File

@@ -25,7 +25,7 @@ jobs:
mysql:
image: mariadb:10.3
env:
MYSQL_ALLOW_EMPTY_PASSWORD: YES
MARIADB_ROOT_PASSWORD: 'root'
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3

View File

@@ -13,10 +13,10 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js v14
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 14
node-version: 18
- name: Setup dependencies
run: |
npm install @semantic-release/git @semantic-release/exec --no-save
@@ -28,4 +28,4 @@ jobs:
GIT_AUTHOR_EMAIL: "developers@frappe.io"
GIT_COMMITTER_NAME: "Frappe PR Bot"
GIT_COMMITTER_EMAIL: "developers@frappe.io"
run: npx semantic-release
run: npx semantic-release

View File

@@ -16,12 +16,12 @@ on:
workflow_dispatch:
inputs:
user:
description: 'user'
description: 'Frappe Framework repository user (add your username for forks)'
required: true
default: 'frappe'
type: string
branch:
description: 'Branch name'
description: 'Frappe Framework branch'
default: 'develop'
required: false
type: string
@@ -45,9 +45,9 @@ jobs:
services:
mysql:
image: mariadb:10.3
image: mariadb:10.6
env:
MYSQL_ALLOW_EMPTY_PASSWORD: YES
MARIADB_ROOT_PASSWORD: 'root'
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3

View File

@@ -378,7 +378,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
return
# check if books nor frozen till endate:
if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto):
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry:

View File

@@ -37,14 +37,11 @@ frappe.ui.form.on("Bank Clearance", {
refresh: function(frm) {
frm.disable_save();
frm.add_custom_button(__('Get Payment Entries'), () =>
frm.trigger("get_payment_entries")
);
if (frm.doc.account && frm.doc.from_date && frm.doc.to_date) {
frm.add_custom_button(__('Get Payment Entries'), () =>
frm.trigger("get_payment_entries")
);
frm.change_custom_button_type('Get Payment Entries', null, 'primary');
}
frm.change_custom_button_type('Get Payment Entries', null, 'primary');
},
update_clearance_date: function(frm) {

View File

@@ -302,7 +302,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
dict(
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
),
["credit", "debit"],
["credit_in_account_currency as credit", "debit_in_account_currency as debit"],
as_dict=1,
)
gl_amount, transaction_amount = (

View File

@@ -137,7 +137,7 @@ def get_paid_amount(payment_entry, currency, bank_account):
)
elif doc.payment_type == "Pay":
paid_amount_field = (
"paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount"
"paid_amount" if doc.paid_from_account_currency == currency else "base_paid_amount"
)
return frappe.db.get_value(

View File

@@ -485,6 +485,10 @@ def set_default_accounts(company):
"default_payable_account": frappe.db.get_value(
"Account", {"company": company.name, "account_type": "Payable", "is_group": 0}
),
"default_provisional_account": frappe.db.get_value(
"Account",
{"company": company.name, "account_type": "Service Received But Not Billed", "is_group": 0},
),
}
)

View File

@@ -26,7 +26,7 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
doc: frm.doc,
callback: function(r) {
if (r.message) {
frm.add_custom_button(__('Journal Entry'), function() {
frm.add_custom_button(__('Journal Entries'), function() {
return frm.events.make_jv(frm);
}, __('Create'));
}
@@ -35,10 +35,11 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
}
},
get_entries: function(frm) {
get_entries: function(frm, account) {
frappe.call({
method: "get_accounts_data",
doc: cur_frm.doc,
account: account,
callback: function(r){
frappe.model.clear_table(frm.doc, "accounts");
if(r.message) {
@@ -57,7 +58,6 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
let total_gain_loss = 0;
frm.doc.accounts.forEach((d) => {
d.gain_loss = flt(d.new_balance_in_base_currency, precision("new_balance_in_base_currency", d)) - flt(d.balance_in_base_currency, precision("balance_in_base_currency", d));
total_gain_loss += flt(d.gain_loss, precision("gain_loss", d));
});
@@ -66,13 +66,19 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
},
make_jv : function(frm) {
let revaluation_journal = null;
let zero_balance_journal = null;
frappe.call({
method: "make_jv_entry",
method: "make_jv_entries",
doc: frm.doc,
freeze: true,
freeze_message: "Making Journal Entries...",
callback: function(r){
if (r.message) {
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
let response = r.message;
if(response['revaluation_jv'] || response['zero_balance_jv']) {
frappe.msgprint(__("Journals have been created"));
}
}
}
});

View File

@@ -14,6 +14,9 @@
"get_entries",
"accounts",
"section_break_6",
"gain_loss_unbooked",
"gain_loss_booked",
"column_break_10",
"total_gain_loss",
"amended_from"
],
@@ -59,13 +62,6 @@
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "total_gain_loss",
"fieldtype": "Currency",
"label": "Total Gain/Loss",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
@@ -74,11 +70,37 @@
"options": "Exchange Rate Revaluation",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "gain_loss_unbooked",
"fieldtype": "Currency",
"label": "Gain/Loss from Revaluation",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"description": "Gain/Loss accumulated in foreign currency account. Accounts with '0' balance in either Base or Account currency",
"fieldname": "gain_loss_booked",
"fieldtype": "Currency",
"label": "Gain/Loss already booked",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "total_gain_loss",
"fieldtype": "Currency",
"label": "Total Gain/Loss",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
}
],
"is_submittable": 1,
"links": [],
"modified": "2022-11-17 10:28:03.911554",
"modified": "2022-12-29 19:38:24.416529",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Exchange Rate Revaluation",

View File

@@ -3,10 +3,12 @@
import frappe
from frappe import _
from frappe import _, qb
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.utils import flt
from frappe.query_builder import Criterion, Order
from frappe.query_builder.functions import NullIf, Sum
from frappe.utils import flt, get_link_to_form
import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on
@@ -19,11 +21,25 @@ class ExchangeRateRevaluation(Document):
def set_total_gain_loss(self):
total_gain_loss = 0
gain_loss_booked = 0
gain_loss_unbooked = 0
for d in self.accounts:
d.gain_loss = flt(
d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency")
) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency"))
if not d.zero_balance:
d.gain_loss = flt(
d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency")
) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency"))
if d.zero_balance:
gain_loss_booked += flt(d.gain_loss, d.precision("gain_loss"))
else:
gain_loss_unbooked += flt(d.gain_loss, d.precision("gain_loss"))
total_gain_loss += flt(d.gain_loss, d.precision("gain_loss"))
self.gain_loss_booked = gain_loss_booked
self.gain_loss_unbooked = gain_loss_unbooked
self.total_gain_loss = flt(total_gain_loss, self.precision("total_gain_loss"))
def validate_mandatory(self):
@@ -35,98 +51,206 @@ class ExchangeRateRevaluation(Document):
@frappe.whitelist()
def check_journal_entry_condition(self):
total_debit = frappe.db.get_value(
"Journal Entry Account",
{"reference_type": "Exchange Rate Revaluation", "reference_name": self.name, "docstatus": 1},
"sum(debit) as sum",
exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
jea = qb.DocType("Journal Entry Account")
journals = (
qb.from_(jea)
.select(jea.parent)
.distinct()
.where(
(jea.reference_type == "Exchange Rate Revaluation")
& (jea.reference_name == self.name)
& (jea.docstatus == 1)
)
.run()
)
total_amt = 0
for d in self.accounts:
total_amt = total_amt + d.new_balance_in_base_currency
if journals:
gle = qb.DocType("GL Entry")
total_amt = (
qb.from_(gle)
.select((Sum(gle.credit) - Sum(gle.debit)).as_("total_amount"))
.where(
(gle.voucher_type == "Journal Entry")
& (gle.voucher_no.isin(journals))
& (gle.account == exchange_gain_loss_account)
& (gle.is_cancelled == 0)
)
.run()
)
if total_amt != total_debit:
return True
if total_amt and total_amt[0][0] != self.total_gain_loss:
return True
else:
return False
return False
return True
@frappe.whitelist()
def get_accounts_data(self, account=None):
accounts = []
def get_accounts_data(self):
self.validate_mandatory()
company_currency = erpnext.get_company_currency(self.company)
account_details = self.get_account_balance_from_gle(
company=self.company, posting_date=self.posting_date, account=None, party_type=None, party=None
)
accounts_with_new_balance = self.calculate_new_account_balance(
self.company, self.posting_date, account_details
)
if not accounts_with_new_balance:
self.throw_invalid_response_message(account_details)
return accounts_with_new_balance
@staticmethod
def get_account_balance_from_gle(company, posting_date, account, party_type, party):
account_details = []
if company and posting_date:
company_currency = erpnext.get_company_currency(company)
acc = qb.DocType("Account")
if account:
accounts = [account]
else:
res = (
qb.from_(acc)
.select(acc.name)
.where(
(acc.is_group == 0)
& (acc.report_type == "Balance Sheet")
& (acc.root_type.isin(["Asset", "Liability", "Equity"]))
& (acc.account_type != "Stock")
& (acc.company == company)
& (acc.account_currency != company_currency)
)
.orderby(acc.name)
.run(as_list=True)
)
accounts = [x[0] for x in res]
if accounts:
having_clause = (qb.Field("balance") != qb.Field("balance_in_account_currency")) & (
(qb.Field("balance_in_account_currency") != 0) | (qb.Field("balance") != 0)
)
gle = qb.DocType("GL Entry")
# conditions
conditions = []
conditions.append(gle.account.isin(accounts))
conditions.append(gle.posting_date.lte(posting_date))
conditions.append(gle.is_cancelled == 0)
if party_type:
conditions.append(gle.party_type == party_type)
if party:
conditions.append(gle.party == party)
account_details = (
qb.from_(gle)
.select(
gle.account,
gle.party_type,
gle.party,
gle.account_currency,
(Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency)).as_(
"balance_in_account_currency"
),
(Sum(gle.debit) - Sum(gle.credit)).as_("balance"),
(Sum(gle.debit) - Sum(gle.credit) == 0)
^ (Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency) == 0).as_(
"zero_balance"
),
)
.where(Criterion.all(conditions))
.groupby(gle.account, NullIf(gle.party_type, ""), NullIf(gle.party, ""))
.having(having_clause)
.orderby(gle.account)
.run(as_dict=True)
)
return account_details
@staticmethod
def calculate_new_account_balance(company, posting_date, account_details):
accounts = []
company_currency = erpnext.get_company_currency(company)
precision = get_field_precision(
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
company_currency,
)
account_details = self.get_accounts_from_gle()
for d in account_details:
current_exchange_rate = (
d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0
)
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, self.posting_date)
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
if gain_loss:
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
}
if account_details:
# Handle Accounts with balance in both Account/Base Currency
for d in [x for x in account_details if not x.zero_balance]:
current_exchange_rate = (
d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0
)
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date)
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
if gain_loss:
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"zero_balance": d.zero_balance,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
"new_balance_in_account_currency": d.balance_in_account_currency,
"gain_loss": gain_loss,
}
)
if not accounts:
self.throw_invalid_response_message(account_details)
# Handle Accounts with '0' balance in Account/Base Currency
for d in [x for x in account_details if x.zero_balance]:
# TODO: Set new balance in Base/Account currency
if d.balance > 0:
current_exchange_rate = new_exchange_rate = 0
new_balance_in_account_currency = 0 # this will be '0'
new_balance_in_base_currency = 0 # this will be '0'
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
else:
new_exchange_rate = 0
new_balance_in_base_currency = 0
new_balance_in_account_currency = 0
current_exchange_rate = calculate_exchange_rate_using_last_gle(
company, d.account, d.party_type, d.party
)
gain_loss = new_balance_in_account_currency - (
current_exchange_rate * d.balance_in_account_currency
)
if gain_loss:
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"zero_balance": d.zero_balance,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
"new_balance_in_account_currency": new_balance_in_account_currency,
"gain_loss": gain_loss,
}
)
return accounts
def get_accounts_from_gle(self):
company_currency = erpnext.get_company_currency(self.company)
accounts = frappe.db.sql_list(
"""
select name
from tabAccount
where is_group = 0
and report_type = 'Balance Sheet'
and root_type in ('Asset', 'Liability', 'Equity')
and account_type != 'Stock'
and company=%s
and account_currency != %s
order by name""",
(self.company, company_currency),
)
account_details = []
if accounts:
account_details = frappe.db.sql(
"""
select
account, party_type, party, account_currency,
sum(debit_in_account_currency) - sum(credit_in_account_currency) as balance_in_account_currency,
sum(debit) - sum(credit) as balance
from `tabGL Entry`
where account in (%s)
and posting_date <= %s
and is_cancelled = 0
group by account, NULLIF(party_type,''), NULLIF(party,'')
having sum(debit) != sum(credit)
order by account
"""
% (", ".join(["%s"] * len(accounts)), "%s"),
tuple(accounts + [self.posting_date]),
as_dict=1,
)
return account_details
def throw_invalid_response_message(self, account_details):
if account_details:
message = _("No outstanding invoices require exchange rate revaluation")
@@ -134,11 +258,7 @@ class ExchangeRateRevaluation(Document):
message = _("No outstanding invoices found")
frappe.msgprint(message)
@frappe.whitelist()
def make_jv_entry(self):
if self.total_gain_loss == 0:
return
def get_for_unrealized_gain_loss_account(self):
unrealized_exchange_gain_loss_account = frappe.get_cached_value(
"Company", self.company, "unrealized_exchange_gain_loss_account"
)
@@ -146,6 +266,130 @@ class ExchangeRateRevaluation(Document):
frappe.throw(
_("Please set Unrealized Exchange Gain/Loss Account in Company {0}").format(self.company)
)
return unrealized_exchange_gain_loss_account
@frappe.whitelist()
def make_jv_entries(self):
zero_balance_jv = self.make_jv_for_zero_balance()
if zero_balance_jv:
frappe.msgprint(
f"Zero Balance Journal: {get_link_to_form('Journal Entry', zero_balance_jv.name)}"
)
revaluation_jv = self.make_jv_for_revaluation()
if revaluation_jv:
frappe.msgprint(
f"Revaluation Journal: {get_link_to_form('Journal Entry', revaluation_jv.name)}"
)
return {
"revaluation_jv": revaluation_jv.name if revaluation_jv else None,
"zero_balance_jv": zero_balance_jv.name if zero_balance_jv else None,
}
def make_jv_for_zero_balance(self):
if self.gain_loss_booked == 0:
return
accounts = [x for x in self.accounts if x.zero_balance]
if not accounts:
return
unrealized_exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
journal_entry.company = self.company
journal_entry.posting_date = self.posting_date
journal_entry.multi_currency = 1
journal_entry_accounts = []
for d in accounts:
journal_account = frappe._dict(
{
"account": d.get("account"),
"party_type": d.get("party_type"),
"party": d.get("party"),
"account_currency": d.get("account_currency"),
"balance": flt(
d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")
),
"exchange_rate": 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
}
)
# Account Currency has balance
if d.get("balance_in_account_currency") and not d.get("new_balance_in_account_currency"):
dr_or_cr = (
"credit_in_account_currency"
if d.get("balance_in_account_currency") > 0
else "debit_in_account_currency"
)
reverse_dr_or_cr = (
"debit_in_account_currency"
if dr_or_cr == "credit_in_account_currency"
else "credit_in_account_currency"
)
journal_account.update(
{
dr_or_cr: flt(
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
),
reverse_dr_or_cr: 0,
"debit": 0,
"credit": 0,
}
)
elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"):
# Base currency has balance
dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit"
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
journal_account.update(
{
dr_or_cr: flt(
abs(d.get("balance_in_base_currency")), d.precision("balance_in_base_currency")
),
reverse_dr_or_cr: 0,
"debit_in_account_currency": 0,
"credit_in_account_currency": 0,
}
)
journal_entry_accounts.append(journal_account)
journal_entry_accounts.append(
{
"account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
"debit": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
"credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0,
"debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
"credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
}
)
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_total_debit_credit()
journal_entry.save()
return journal_entry
def make_jv_for_revaluation(self):
if self.gain_loss_unbooked == 0:
return
accounts = [x for x in self.accounts if not x.zero_balance]
if not accounts:
return
unrealized_exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Rate Revaluation"
@@ -154,7 +398,7 @@ class ExchangeRateRevaluation(Document):
journal_entry.multi_currency = 1
journal_entry_accounts = []
for d in self.accounts:
for d in accounts:
dr_or_cr = (
"debit_in_account_currency"
if d.get("balance_in_account_currency") > 0
@@ -179,6 +423,7 @@ class ExchangeRateRevaluation(Document):
dr_or_cr: flt(
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
),
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
@@ -196,6 +441,7 @@ class ExchangeRateRevaluation(Document):
reverse_dr_or_cr: flt(
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
),
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
@@ -206,8 +452,11 @@ class ExchangeRateRevaluation(Document):
{
"account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
"debit_in_account_currency": abs(self.total_gain_loss) if self.total_gain_loss < 0 else 0,
"credit_in_account_currency": self.total_gain_loss if self.total_gain_loss > 0 else 0,
"debit_in_account_currency": abs(self.gain_loss_unbooked)
if self.gain_loss_unbooked < 0
else 0,
"credit_in_account_currency": self.gain_loss_unbooked if self.gain_loss_unbooked > 0 else 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
@@ -217,42 +466,90 @@ class ExchangeRateRevaluation(Document):
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit()
return journal_entry.as_dict()
journal_entry.save()
return journal_entry
def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
"""
Use last GL entry to calculate exchange rate
"""
last_exchange_rate = None
if company and account:
gl = qb.DocType("GL Entry")
# build conditions
conditions = []
conditions.append(gl.company == company)
conditions.append(gl.account == account)
conditions.append(gl.is_cancelled == 0)
if party_type:
conditions.append(gl.party_type == party_type)
if party:
conditions.append(gl.party == party)
voucher_type, voucher_no = (
qb.from_(gl)
.select(gl.voucher_type, gl.voucher_no)
.where(Criterion.all(conditions))
.orderby(gl.posting_date, order=Order.desc)
.limit(1)
.run()[0]
)
last_exchange_rate = (
qb.from_(gl)
.select((gl.debit - gl.credit) / (gl.debit_in_account_currency - gl.credit_in_account_currency))
.where(
(gl.voucher_type == voucher_type) & (gl.voucher_no == voucher_no) & (gl.account == account)
)
.orderby(gl.posting_date, order=Order.desc)
.limit(1)
.run()[0][0]
)
return last_exchange_rate
@frappe.whitelist()
def get_account_details(account, company, posting_date, party_type=None, party=None):
def get_account_details(company, posting_date, account, party_type=None, party=None):
if not (company and posting_date):
frappe.throw(_("Company and Posting Date is mandatory"))
account_currency, account_type = frappe.get_cached_value(
"Account", account, ["account_currency", "account_type"]
)
if account_type in ["Receivable", "Payable"] and not (party_type and party):
frappe.throw(_("Party Type and Party is mandatory for {0} account").format(account_type))
account_details = {}
company_currency = erpnext.get_company_currency(company)
balance = get_balance_on(
account, date=posting_date, party_type=party_type, party=party, in_account_currency=False
)
account_details = {
"account_currency": account_currency,
}
account_balance = ExchangeRateRevaluation.get_account_balance_from_gle(
company=company, posting_date=posting_date, account=account, party_type=party_type, party=party
)
if balance:
balance_in_account_currency = get_balance_on(
account, date=posting_date, party_type=party_type, party=party
if account_balance and (
account_balance[0].balance or account_balance[0].balance_in_account_currency
):
account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance(
company, posting_date, account_balance
)
current_exchange_rate = (
balance / balance_in_account_currency if balance_in_account_currency else 0
)
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate
account_details = account_details.update(
row = account_with_new_balance[0]
account_details.update(
{
"balance_in_base_currency": balance,
"balance_in_account_currency": balance_in_account_currency,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
"balance_in_base_currency": row["balance_in_base_currency"],
"balance_in_account_currency": row["balance_in_account_currency"],
"current_exchange_rate": row["current_exchange_rate"],
"new_exchange_rate": row["new_exchange_rate"],
"new_balance_in_base_currency": row["new_balance_in_base_currency"],
"new_balance_in_account_currency": row["new_balance_in_account_currency"],
"zero_balance": row["zero_balance"],
"gain_loss": row["gain_loss"],
}
)

View File

@@ -10,14 +10,21 @@
"party",
"column_break_2",
"account_currency",
"account_balances",
"balance_in_account_currency",
"column_break_46yz",
"new_balance_in_account_currency",
"balances",
"current_exchange_rate",
"balance_in_base_currency",
"column_break_9",
"column_break_xown",
"new_exchange_rate",
"column_break_9",
"balance_in_base_currency",
"column_break_ukce",
"new_balance_in_base_currency",
"gain_loss"
"section_break_ngrs",
"gain_loss",
"zero_balance"
],
"fields": [
{
@@ -78,7 +85,7 @@
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
"fieldtype": "Section Break"
},
{
"fieldname": "new_exchange_rate",
@@ -102,11 +109,45 @@
"label": "Gain/Loss",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"default": "0",
"description": "This Account has '0' balance in either Base Currency or Account Currency",
"fieldname": "zero_balance",
"fieldtype": "Check",
"label": "Zero Balance"
},
{
"fieldname": "new_balance_in_account_currency",
"fieldtype": "Currency",
"label": "New Balance In Account Currency",
"options": "account_currency",
"read_only": 1
},
{
"fieldname": "account_balances",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_46yz",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_xown",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_ukce",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_ngrs",
"fieldtype": "Section Break"
}
],
"istable": 1,
"links": [],
"modified": "2022-11-17 10:26:18.302728",
"modified": "2022-12-29 19:38:52.915295",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Exchange Rate Revaluation Account",

View File

@@ -9,10 +9,6 @@ from frappe.model.document import Document
from frappe.utils import add_days, add_years, cstr, getdate
class FiscalYearIncorrectDate(frappe.ValidationError):
pass
class FiscalYear(Document):
@frappe.whitelist()
def set_as_default(self):
@@ -53,23 +49,18 @@ class FiscalYear(Document):
)
def validate_dates(self):
self.validate_from_to_dates("year_start_date", "year_end_date")
if self.is_short_year:
# Fiscal Year can be shorter than one year, in some jurisdictions
# under certain circumstances. For example, in the USA and Germany.
return
if getdate(self.year_start_date) > getdate(self.year_end_date):
frappe.throw(
_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"),
FiscalYearIncorrectDate,
)
date = getdate(self.year_start_date) + relativedelta(years=1) - relativedelta(days=1)
if getdate(self.year_end_date) != date:
frappe.throw(
_("Fiscal Year End Date should be one year after Fiscal Year Start Date"),
FiscalYearIncorrectDate,
frappe.exceptions.InvalidDates,
)
def on_update(self):
@@ -169,5 +160,6 @@ def auto_create_fiscal_year():
def get_from_and_to_date(fiscal_year):
fields = ["year_start_date as from_date", "year_end_date as to_date"]
return frappe.get_cached_value("Fiscal Year", fiscal_year, fields, as_dict=1)
fields = ["year_start_date", "year_end_date"]
cached_results = frappe.get_cached_value("Fiscal Year", fiscal_year, fields, as_dict=1)
return dict(from_date=cached_results.year_start_date, to_date=cached_results.year_end_date)

View File

@@ -7,8 +7,6 @@ import unittest
import frappe
from frappe.utils import now_datetime
from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrectDate
test_ignore = ["Company"]
@@ -26,7 +24,7 @@ class TestFiscalYear(unittest.TestCase):
}
)
self.assertRaises(FiscalYearIncorrectDate, fy.insert)
self.assertRaises(frappe.exceptions.InvalidDates, fy.insert)
def test_record_generator():
@@ -35,8 +33,8 @@ def test_record_generator():
"doctype": "Fiscal Year",
"year": "_Test Short Fiscal Year 2011",
"is_short_year": 1,
"year_end_date": "2011-04-01",
"year_start_date": "2011-12-31",
"year_start_date": "2011-04-01",
"year_end_date": "2011-12-31",
}
]

View File

@@ -95,7 +95,15 @@ class GLEntry(Document):
)
# Zero value transaction is not allowed
if not (flt(self.debit, self.precision("debit")) or flt(self.credit, self.precision("credit"))):
if not (
flt(self.debit, self.precision("debit"))
or flt(self.credit, self.precision("credit"))
or (
self.voucher_type == "Journal Entry"
and frappe.get_cached_value("Journal Entry", self.voucher_no, "voucher_type")
== "Exchange Gain Or Loss"
)
):
frappe.throw(
_("{0} {1}: Either debit or credit amount is required for {2}").format(
self.voucher_type, self.voucher_no, self.account

View File

@@ -88,7 +88,7 @@
"label": "Entry Type",
"oldfieldname": "voucher_type",
"oldfieldtype": "Select",
"options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nDeferred Revenue\nDeferred Expense",
"options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense",
"reqd": 1,
"search_index": 1
},
@@ -539,7 +539,7 @@
"idx": 176,
"is_submittable": 1,
"links": [],
"modified": "2022-06-23 22:01:32.348337",
"modified": "2022-11-28 17:40:01.241908",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -6,7 +6,7 @@ import json
import frappe
from frappe import _, msgprint, scrub
from frappe.utils import cint, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
from frappe.utils import cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
import erpnext
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
@@ -23,6 +23,9 @@ from erpnext.accounts.utils import (
get_stock_accounts,
get_stock_and_account_balance,
)
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
)
from erpnext.controllers.accounts_controller import AccountsController
@@ -34,9 +37,6 @@ class JournalEntry(AccountsController):
def __init__(self, *args, **kwargs):
super(JournalEntry, self).__init__(*args, **kwargs)
def get_feed(self):
return self.voucher_type
def validate(self):
if self.voucher_type == "Opening Entry":
self.is_opening = "Yes"
@@ -286,16 +286,17 @@ class JournalEntry(AccountsController):
for d in self.get("accounts"):
if d.reference_type == "Asset" and d.reference_name:
asset = frappe.get_doc("Asset", d.reference_name)
for s in asset.get("schedules"):
if s.journal_entry == self.name:
s.db_set("journal_entry", None)
for row in asset.get("finance_books"):
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
idx = cint(s.finance_book_id) or 1
finance_books = asset.get("finance_books")[idx - 1]
finance_books.value_after_depreciation += s.depreciation_amount
finance_books.db_update()
for s in depr_schedule or []:
if s.journal_entry == self.name:
s.db_set("journal_entry", None)
asset.set_status()
row.value_after_depreciation += s.depreciation_amount
row.db_update()
asset.set_status()
def unlink_inter_company_jv(self):
if (
@@ -592,28 +593,30 @@ class JournalEntry(AccountsController):
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
else:
for d in self.get("accounts"):
if flt(d.debit > 0):
if flt(d.debit) > 0:
accounts_debited.append(d.party or d.account)
if flt(d.credit) > 0:
accounts_credited.append(d.party or d.account)
for d in self.get("accounts"):
if flt(d.debit > 0):
if flt(d.debit) > 0:
d.against_account = ", ".join(list(set(accounts_credited)))
if flt(d.credit > 0):
if flt(d.credit) > 0:
d.against_account = ", ".join(list(set(accounts_debited)))
def validate_debit_credit_amount(self):
for d in self.get("accounts"):
if not flt(d.debit) and not flt(d.credit):
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
for d in self.get("accounts"):
if not flt(d.debit) and not flt(d.credit):
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
def validate_total_debit_and_credit(self):
self.set_total_debit_credit()
if self.difference:
frappe.throw(
_("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference)
)
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
if self.difference:
frappe.throw(
_("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference)
)
def set_total_debit_credit(self):
self.total_debit, self.total_credit, self.difference = 0, 0, 0
@@ -651,16 +654,17 @@ class JournalEntry(AccountsController):
self.set_exchange_rate()
def set_amounts_in_company_currency(self):
for d in self.get("accounts"):
d.debit_in_account_currency = flt(
d.debit_in_account_currency, d.precision("debit_in_account_currency")
)
d.credit_in_account_currency = flt(
d.credit_in_account_currency, d.precision("credit_in_account_currency")
)
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
for d in self.get("accounts"):
d.debit_in_account_currency = flt(
d.debit_in_account_currency, d.precision("debit_in_account_currency")
)
d.credit_in_account_currency = flt(
d.credit_in_account_currency, d.precision("credit_in_account_currency")
)
d.debit = flt(d.debit_in_account_currency * flt(d.exchange_rate), d.precision("debit"))
d.credit = flt(d.credit_in_account_currency * flt(d.exchange_rate), d.precision("credit"))
d.debit = flt(d.debit_in_account_currency * flt(d.exchange_rate), d.precision("debit"))
d.credit = flt(d.credit_in_account_currency * flt(d.exchange_rate), d.precision("credit"))
def set_exchange_rate(self):
for d in self.get("accounts"):
@@ -759,7 +763,7 @@ class JournalEntry(AccountsController):
pay_to_recd_from = d.party
if pay_to_recd_from and pay_to_recd_from == d.party:
party_amount += d.debit_in_account_currency or d.credit_in_account_currency
party_amount += flt(d.debit_in_account_currency) or flt(d.credit_in_account_currency)
party_account_currency = d.account_currency
elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]:
@@ -789,7 +793,7 @@ class JournalEntry(AccountsController):
def build_gl_map(self):
gl_map = []
for d in self.get("accounts"):
if d.debit or d.credit:
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
r = [d.user_remark, self.remark]
r = [x for x in r if x]
remarks = "\n".join(r)
@@ -837,7 +841,7 @@ class JournalEntry(AccountsController):
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
@frappe.whitelist()
def get_balance(self):
def get_balance(self, difference_account=None):
if not self.get("accounts"):
msgprint(_("'Entries' cannot be empty"), raise_exception=True)
else:
@@ -852,7 +856,13 @@ class JournalEntry(AccountsController):
blank_row = d
if not blank_row:
blank_row = self.append("accounts", {})
blank_row = self.append(
"accounts",
{
"account": difference_account,
"cost_center": erpnext.get_default_cost_center(self.company),
},
)
blank_row.exchange_rate = 1
if diff > 0:

View File

@@ -305,6 +305,7 @@
"fieldname": "source_exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"precision": "9",
"print_hide": 1,
"reqd": 1
},
@@ -334,6 +335,7 @@
"fieldname": "target_exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"precision": "9",
"print_hide": 1,
"reqd": 1
},
@@ -731,7 +733,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-02-23 20:08:39.559814",
"modified": "2022-12-08 16:25:43.824051",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@@ -622,7 +622,7 @@ class PaymentEntry(AccountsController):
self.payment_type == "Receive"
and self.base_total_allocated_amount < self.base_received_amount + total_deductions
and self.total_allocated_amount
< self.paid_amount + (total_deductions / self.source_exchange_rate)
< flt(self.paid_amount) + (total_deductions / self.source_exchange_rate)
):
self.unallocated_amount = (
self.base_received_amount + total_deductions - self.base_total_allocated_amount
@@ -632,7 +632,7 @@ class PaymentEntry(AccountsController):
self.payment_type == "Pay"
and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions)
and self.total_allocated_amount
< self.received_amount + (total_deductions / self.target_exchange_rate)
< flt(self.received_amount) + (total_deductions / self.target_exchange_rate)
):
self.unallocated_amount = (
self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)
@@ -684,35 +684,34 @@ class PaymentEntry(AccountsController):
)
def validate_payment_against_negative_invoice(self):
if (self.payment_type == "Pay" and self.party_type == "Customer") or (
self.payment_type == "Receive" and self.party_type == "Supplier"
if (self.payment_type != "Pay" or self.party_type != "Customer") and (
self.payment_type != "Receive" or self.party_type != "Supplier"
):
return
total_negative_outstanding = sum(
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
total_negative_outstanding = sum(
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
)
paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
additional_charges = sum(flt(d.amount) for d in self.deductions)
if not total_negative_outstanding:
if self.party_type == "Customer":
msg = _("Cannot pay to Customer without any negative outstanding invoice")
else:
msg = _("Cannot receive from Supplier without any negative outstanding invoice")
frappe.throw(msg, InvalidPaymentEntry)
elif paid_amount - additional_charges > total_negative_outstanding:
frappe.throw(
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
total_negative_outstanding
),
InvalidPaymentEntry,
)
paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
additional_charges = sum([flt(d.amount) for d in self.deductions])
if not total_negative_outstanding:
frappe.throw(
_("Cannot {0} {1} {2} without any negative outstanding invoice").format(
_(self.payment_type),
(_("to") if self.party_type == "Customer" else _("from")),
self.party_type,
),
InvalidPaymentEntry,
)
elif paid_amount - additional_charges > total_negative_outstanding:
frappe.throw(
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
total_negative_outstanding
),
InvalidPaymentEntry,
)
def set_title(self):
if frappe.flags.in_import and self.title:
# do not set title dynamically if title exists during data import.
@@ -1188,6 +1187,7 @@ def get_outstanding_reference_documents(args):
ple = qb.DocType("Payment Ledger Entry")
common_filter = []
accounting_dimensions_filter = []
posting_and_due_date = []
# confirm that Supplier is not blocked
@@ -1217,7 +1217,7 @@ def get_outstanding_reference_documents(args):
# Add cost center condition
if args.get("cost_center"):
condition += " and cost_center='%s'" % args.get("cost_center")
common_filter.append(ple.cost_center == args.get("cost_center"))
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
date_fields_dict = {
"posting_date": ["from_posting_date", "to_posting_date"],
@@ -1243,6 +1243,7 @@ def get_outstanding_reference_documents(args):
posting_date=posting_and_due_date,
min_outstanding=args.get("outstanding_amt_greater_than"),
max_outstanding=args.get("outstanding_amt_less_than"),
accounting_dimensions=accounting_dimensions_filter,
)
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
@@ -1639,7 +1640,7 @@ def get_payment_entry(
):
reference_doc = None
doc = frappe.get_doc(dt, dn)
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0:
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99:
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
if not party_type:
@@ -1757,6 +1758,8 @@ def get_payment_entry(
pe.setup_party_account_field()
pe.set_missing_values()
update_accounting_dimensions(pe, doc)
if party_account and bank:
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts()
@@ -1774,6 +1777,18 @@ def get_payment_entry(
return pe
def update_accounting_dimensions(pe, doc):
"""
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document
"""
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
for dimension in get_accounting_dimensions():
pe.set(dimension, doc.get(dimension))
def get_bank_cash_account(doc, bank_account):
bank = get_default_bank_cash_account(
doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account

View File

@@ -25,7 +25,8 @@
"in_list_view": 1,
"label": "Type",
"options": "DocType",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"columns": 2,
@@ -35,7 +36,8 @@
"in_list_view": 1,
"label": "Name",
"options": "reference_doctype",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"fieldname": "due_date",
@@ -104,7 +106,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-09-26 17:06:55.597389",
"modified": "2022-12-12 12:31:44.919895",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",
@@ -113,5 +115,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -3,6 +3,7 @@
frappe.ui.form.on('Payment Gateway Account', {
refresh(frm) {
erpnext.utils.check_payments_app();
if(!frm.doc.__islocal) {
frm.set_df_property('payment_gateway', 'read_only', 1);
}

View File

@@ -170,7 +170,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
}
reconcile() {
var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount);
if (show_dialog && show_dialog.length) {
@@ -179,8 +179,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
title: __("Select Difference Account"),
fields: [
{
fieldname: "allocation", fieldtype: "Table", label: __("Allocation"),
data: this.data, in_place_edit: true,
fieldname: "allocation",
fieldtype: "Table",
label: __("Allocation"),
data: this.data,
in_place_edit: true,
cannot_add_rows: true,
get_data: () => {
return this.data;
},
@@ -218,6 +222,10 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
read_only: 1
}]
},
{
fieldtype: 'HTML',
options: "<b> New Journal Entry will be posted for the difference amount </b>"
}
],
primary_action: () => {
const args = dialog.get_values()["allocation"];
@@ -234,7 +242,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
});
this.frm.doc.allocation.forEach(d => {
if (d.difference_amount && !d.difference_account) {
if (d.difference_amount) {
dialog.fields_dict.allocation.df.data.push({
'docname': d.name,
'reference_name': d.reference_name,

View File

@@ -14,7 +14,6 @@ from erpnext.accounts.utils import (
QueryPaymentLedger,
get_outstanding_invoices,
reconcile_against_document,
update_reference_in_payment_entry,
)
from erpnext.controllers.accounts_controller import get_advance_payment_entries
@@ -23,6 +22,7 @@ class PaymentReconciliation(Document):
def __init__(self, *args, **kwargs):
super(PaymentReconciliation, self).__init__(*args, **kwargs)
self.common_filter_conditions = []
self.accounting_dimension_filter_conditions = []
self.ple_posting_date_filter = []
@frappe.whitelist()
@@ -79,12 +79,13 @@ class PaymentReconciliation(Document):
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
)
# nosemgrep
journal_entries = frappe.db.sql(
"""
select
"Journal Entry" as reference_type, t1.name as reference_name,
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
{dr_or_cr} as amount, t2.is_advance,
{dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
t2.account_currency as currency
from
`tabJournal Entry` t1, `tabJournal Entry Account` t2
@@ -193,6 +194,7 @@ class PaymentReconciliation(Document):
posting_date=self.ple_posting_date_filter,
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
accounting_dimensions=self.accounting_dimension_filter_conditions,
)
if self.invoice_limit:
@@ -213,26 +215,26 @@ class PaymentReconciliation(Document):
inv.currency = entry.get("currency")
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
def get_difference_amount(self, allocated_entry):
if allocated_entry.get("reference_type") != "Payment Entry":
return
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
difference_amount = 0
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
"exchange_rate", 1
):
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency"
)
row = self.get_payment_details(allocated_entry, dr_or_cr)
doc = frappe.get_doc(allocated_entry.reference_type, allocated_entry.reference_name)
update_reference_in_payment_entry(row, doc, do_not_save=True)
return doc.difference_amount
return difference_amount
@frappe.whitelist()
def allocate_entries(self, args):
self.validate_entries()
invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"))
default_exchange_gain_loss_account = frappe.get_cached_value(
"Company", self.company, "exchange_gain_loss_account"
)
entries = []
for pay in args.get("payments"):
pay.update({"unreconciled_amount": pay.get("amount")})
@@ -246,7 +248,10 @@ class PaymentReconciliation(Document):
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
pay["amount"] = 0
res.difference_amount = self.get_difference_amount(res)
inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number"))
res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"])
res.difference_account = default_exchange_gain_loss_account
res.exchange_rate = inv.get("exchange_rate")
if pay.get("amount") == 0:
entries.append(res)
@@ -276,6 +281,7 @@ class PaymentReconciliation(Document):
"amount": pay.get("amount"),
"allocated_amount": allocated_amount,
"difference_amount": pay.get("difference_amount"),
"currency": inv.get("currency"),
}
)
@@ -298,7 +304,11 @@ class PaymentReconciliation(Document):
else:
reconciled_entry = entry_list
reconciled_entry.append(self.get_payment_details(row, dr_or_cr))
payment_details = self.get_payment_details(row, dr_or_cr)
reconciled_entry.append(payment_details)
if payment_details.difference_amount:
self.make_difference_entry(payment_details)
if entry_list:
reconcile_against_document(entry_list)
@@ -309,6 +319,56 @@ class PaymentReconciliation(Document):
msgprint(_("Successfully Reconciled"))
self.get_unreconciled_entries()
def make_difference_entry(self, row):
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
journal_entry.company = self.company
journal_entry.posting_date = nowdate()
journal_entry.multi_currency = 1
party_account_currency = frappe.get_cached_value(
"Account", self.receivable_payable_account, "account_currency"
)
difference_account_currency = frappe.get_cached_value(
"Account", row.difference_account, "account_currency"
)
# Account Currency has balance
dr_or_cr = "debit" if self.party_type == "Customer" else "debit"
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
journal_account = frappe._dict(
{
"account": self.receivable_payable_account,
"party_type": self.party_type,
"party": self.party,
"account_currency": party_account_currency,
"exchange_rate": 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"reference_type": row.against_voucher_type,
"reference_name": row.against_voucher,
dr_or_cr: flt(row.difference_amount),
dr_or_cr + "_in_account_currency": 0,
}
)
journal_entry.append("accounts", journal_account)
journal_account = frappe._dict(
{
"account": row.difference_account,
"account_currency": difference_account_currency,
"exchange_rate": 1,
"cost_center": erpnext.get_default_cost_center(self.company),
reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
}
)
journal_entry.append("accounts", journal_account)
journal_entry.save()
journal_entry.submit()
def get_payment_details(self, row, dr_or_cr):
return frappe._dict(
{
@@ -318,6 +378,7 @@ class PaymentReconciliation(Document):
"against_voucher_type": row.get("invoice_type"),
"against_voucher": row.get("invoice_number"),
"account": self.receivable_payable_account,
"exchange_rate": row.get("exchange_rate"),
"party_type": self.party_type,
"party": self.party,
"is_advance": row.get("is_advance"),
@@ -342,6 +403,41 @@ class PaymentReconciliation(Document):
if not self.get("payments"):
frappe.throw(_("No records found in the Payments table"))
def get_invoice_exchange_map(self, invoices):
sales_invoices = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Sales Invoice"
]
purchase_invoices = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Purchase Invoice"
]
invoice_exchange_map = frappe._dict()
if sales_invoices:
sales_invoice_map = frappe._dict(
frappe.db.get_all(
"Sales Invoice",
filters={"name": ("in", sales_invoices)},
fields=["name", "conversion_rate"],
as_list=1,
)
)
invoice_exchange_map.update(sales_invoice_map)
if purchase_invoices:
purchase_invoice_map = frappe._dict(
frappe.db.get_all(
"Purchase Invoice",
filters={"name": ("in", purchase_invoices)},
fields=["name", "conversion_rate"],
as_list=1,
)
)
invoice_exchange_map.update(purchase_invoice_map)
return invoice_exchange_map
def validate_allocation(self):
unreconciled_invoices = frappe._dict()
@@ -381,7 +477,7 @@ class PaymentReconciliation(Document):
self.common_filter_conditions.append(ple.company == self.company)
if self.get("cost_center") and (get_invoices or get_return_invoices):
self.common_filter_conditions.append(ple.cost_center == self.cost_center)
self.accounting_dimension_filter_conditions.append(ple.cost_center == self.cost_center)
if get_invoices:
if self.from_invoice_date:

View File

@@ -6,8 +6,10 @@ import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, nowdate
from frappe.utils import add_days, flt, nowdate
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
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.party import get_party_account
@@ -20,6 +22,7 @@ class TestPaymentReconciliation(FrappeTestCase):
self.create_item()
self.create_customer()
self.create_account()
self.create_cost_center()
self.clear_old_entries()
def tearDown(self):
@@ -72,33 +75,11 @@ class TestPaymentReconciliation(FrappeTestCase):
self.item = item if isinstance(item, str) else item.item_code
def create_customer(self):
if frappe.db.exists("Customer", "_Test PR Customer"):
self.customer = "_Test PR Customer"
else:
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test PR Customer"
customer.type = "Individual"
customer.save()
self.customer = customer.name
if frappe.db.exists("Customer", "_Test PR Customer 2"):
self.customer2 = "_Test PR Customer 2"
else:
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test PR Customer 2"
customer.type = "Individual"
customer.save()
self.customer2 = customer.name
if frappe.db.exists("Customer", "_Test PR Customer 3"):
self.customer3 = "_Test PR Customer 3"
else:
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test PR Customer 3"
customer.type = "Individual"
customer.default_currency = "EUR"
customer.save()
self.customer3 = customer.name
self.customer = make_customer("_Test PR Customer")
self.customer2 = make_customer("_Test PR Customer 2")
self.customer3 = make_customer("_Test PR Customer 3", "EUR")
self.customer4 = make_customer("_Test PR Customer 4", "EUR")
self.customer5 = make_customer("_Test PR Customer 5", "EUR")
def create_account(self):
account_name = "Debtors EUR"
@@ -216,6 +197,22 @@ class TestPaymentReconciliation(FrappeTestCase):
)
return je
def create_cost_center(self):
# Setup cost center
cc_name = "Sub"
self.main_cc = frappe.get_doc("Cost Center", get_default_cost_center(self.company))
cc_exists = frappe.db.get_list("Cost Center", filters={"cost_center_name": cc_name})
if cc_exists:
self.sub_cc = frappe.get_doc("Cost Center", cc_exists[0].name)
else:
sub_cc = frappe.new_doc("Cost Center")
sub_cc.cost_center_name = "Sub"
sub_cc.parent_cost_center = self.main_cc.parent_cost_center
sub_cc.company = self.main_cc.company
self.sub_cc = sub_cc.save()
def test_filter_min_max(self):
# check filter condition minimum and maximum amount
self.create_sales_invoice(qty=1, rate=300)
@@ -578,3 +575,188 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.payments), 1)
self.assertEqual(pr.payments[0].amount, amount)
self.assertEqual(pr.payments[0].currency, "EUR")
def test_difference_amount_via_journal_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer4
si.currency = "EUR"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.save().submit()
# Make payment using Journal Entry
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
je1.multi_currency = 1
je1.accounts[0].exchange_rate = 1
je1.accounts[0].credit_in_account_currency = 0
je1.accounts[0].credit = 0
je1.accounts[0].debit_in_account_currency = 8000
je1.accounts[0].debit = 8000
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = self.customer4
je1.accounts[1].exchange_rate = 80
je1.accounts[1].credit_in_account_currency = 100
je1.accounts[1].credit = 8000
je1.accounts[1].debit_in_account_currency = 0
je1.accounts[1].debit = 0
je1.save()
je1.submit()
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
je2.multi_currency = 1
je2.accounts[0].exchange_rate = 1
je2.accounts[0].credit_in_account_currency = 0
je2.accounts[0].credit = 0
je2.accounts[0].debit_in_account_currency = 16000
je2.accounts[0].debit = 16000
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer4
je2.accounts[1].exchange_rate = 80
je2.accounts[1].credit_in_account_currency = 200
je1.accounts[1].credit = 16000
je1.accounts[1].debit_in_account_currency = 0
je1.accounts[1].debit = 0
je2.save()
je2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer4
pr.receivable_payable_account = self.debtors_eur
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 2)
# Test exact payment allocation
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[0].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
# Test partial payment allocation (with excess payment entry)
pr.set("allocation", [])
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[1].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
# Check if difference journal entry gets generated for difference amount after reconciliation
pr.reconcile()
total_debit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
"sum(debit) as amount",
group_by="reference_name",
)[0].amount
self.assertEqual(flt(total_debit_amount, 2), -500)
def test_difference_amount_via_payment_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer5
si.currency = "EUR"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.save().submit()
# Make payment using Payment Entry
pe1 = create_payment_entry(
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer5,
paid_from=self.debtors_eur,
paid_to=self.bank,
paid_amount=100,
)
pe1.source_exchange_rate = 80
pe1.received_amount = 8000
pe1.save()
pe1.submit()
pe2 = create_payment_entry(
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer5,
paid_from=self.debtors_eur,
paid_to=self.bank,
paid_amount=200,
)
pe2.source_exchange_rate = 80
pe2.received_amount = 16000
pe2.save()
pe2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer5
pr.receivable_payable_account = self.debtors_eur
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 2)
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[0].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
pr.set("allocation", [])
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[1].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
def test_differing_cost_center_on_invoice_and_payment(self):
"""
Cost Center filter should not affect outstanding amount calculation
"""
si = self.create_sales_invoice(qty=1, rate=100, do_not_submit=True)
si.cost_center = self.main_cc.name
si.submit()
pr = get_payment_entry(si.doctype, si.name)
pr.cost_center = self.sub_cc.name
pr = pr.save().submit()
pr = self.create_payment_reconciliation()
pr.cost_center = self.main_cc.name
pr.get_unreconciled_entries()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
customer.type = "Individual"
if currency:
customer.default_currency = currency
customer.save()
return customer.name
else:
return customer_name

View File

@@ -20,7 +20,9 @@
"section_break_5",
"difference_amount",
"column_break_7",
"difference_account"
"difference_account",
"exchange_rate",
"currency"
],
"fields": [
{
@@ -37,7 +39,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated Amount",
"options": "Currency",
"options": "currency",
"reqd": 1
},
{
@@ -112,7 +114,7 @@
"fieldtype": "Currency",
"hidden": 1,
"label": "Unreconciled Amount",
"options": "Currency",
"options": "currency",
"read_only": 1
},
{
@@ -120,7 +122,7 @@
"fieldtype": "Currency",
"hidden": 1,
"label": "Amount",
"options": "Currency",
"options": "currency",
"read_only": 1
},
{
@@ -129,11 +131,24 @@
"hidden": 1,
"label": "Reference Row",
"read_only": 1
},
{
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2021-10-06 11:48:59.616562",
"modified": "2022-12-24 21:01:14.882747",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Allocation",
@@ -141,5 +156,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -11,7 +11,8 @@
"col_break1",
"amount",
"outstanding_amount",
"currency"
"currency",
"exchange_rate"
],
"fields": [
{
@@ -62,11 +63,17 @@
"hidden": 1,
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "exchange_rate",
"fieldtype": "Float",
"hidden": 1,
"label": "Exchange Rate"
}
],
"istable": 1,
"links": [],
"modified": "2021-08-24 22:42:40.923179",
"modified": "2022-11-08 18:18:02.502149",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Invoice",
@@ -75,5 +82,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -15,7 +15,8 @@
"difference_amount",
"sec_break1",
"remark",
"currency"
"currency",
"exchange_rate"
],
"fields": [
{
@@ -91,11 +92,17 @@
"label": "Difference Amount",
"options": "currency",
"read_only": 1
},
{
"fieldname": "exchange_rate",
"fieldtype": "Float",
"hidden": 1,
"label": "Exchange Rate"
}
],
"istable": 1,
"links": [],
"modified": "2021-08-30 10:51:48.140062",
"modified": "2022-11-08 18:18:36.268760",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Payment",
@@ -103,5 +110,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@@ -42,7 +42,7 @@ frappe.ui.form.on("Payment Request", "refresh", function(frm) {
});
}
if(!frm.doc.payment_gateway_account && frm.doc.status == "Initiated") {
if((!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") && frm.doc.status == "Initiated") {
frm.add_custom_button(__('Create Payment Entry'), function(){
frappe.call({
method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry",

View File

@@ -32,6 +32,10 @@
"iban",
"branch_code",
"swift_number",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"recipient_and_message",
"print_format",
"email_to",
@@ -362,13 +366,35 @@
"label": "Payment Channel",
"options": "\nEmail\nPhone",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-09-30 16:19:43.680025",
"modified": "2022-12-21 16:56:40.115737",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",

View File

@@ -9,8 +9,10 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import flt, get_url, nowdate
from frappe.utils.background_jobs import enqueue
from payments.utils import get_payment_gateway_controller
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_company_defaults,
get_payment_entry,
@@ -19,6 +21,14 @@ from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_pla
from erpnext.accounts.party import get_party_account, get_party_bank_account
from erpnext.accounts.utils import get_account_currency
from erpnext.erpnext_integrations.stripe_integration import create_stripe_subscription
from erpnext.utilities import payment_app_import_guard
def _get_payment_gateway_controller(*args, **kwargs):
with payment_app_import_guard():
from payments.utils import get_payment_gateway_controller
return get_payment_gateway_controller(*args, **kwargs)
class PaymentRequest(Document):
@@ -107,7 +117,7 @@ class PaymentRequest(Document):
self.request_phone_payment()
def request_phone_payment(self):
controller = get_payment_gateway_controller(self.payment_gateway)
controller = _get_payment_gateway_controller(self.payment_gateway)
request_amount = self.get_request_amount()
payment_record = dict(
@@ -156,7 +166,7 @@ class PaymentRequest(Document):
def payment_gateway_validation(self):
try:
controller = get_payment_gateway_controller(self.payment_gateway)
controller = _get_payment_gateway_controller(self.payment_gateway)
if hasattr(controller, "on_payment_request_submission"):
return controller.on_payment_request_submission(self)
else:
@@ -189,7 +199,7 @@ class PaymentRequest(Document):
)
data.update({"company": frappe.defaults.get_defaults().company})
controller = get_payment_gateway_controller(self.payment_gateway)
controller = _get_payment_gateway_controller(self.payment_gateway)
controller.validate_transaction_currency(self.currency)
if hasattr(controller, "validate_minimum_transaction_amount"):
@@ -254,6 +264,7 @@ class PaymentRequest(Document):
payment_entry.update(
{
"mode_of_payment": self.mode_of_payment,
"reference_no": self.name,
"reference_date": nowdate(),
"remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(
@@ -262,6 +273,17 @@ class PaymentRequest(Document):
}
)
# Update dimensions
payment_entry.update(
{
"cost_center": self.get("cost_center"),
"project": self.get("project"),
}
)
for dimension in get_accounting_dimensions():
payment_entry.update({dimension: self.get(dimension)})
if payment_entry.difference_amount:
company_details = get_company_defaults(ref_doc.company)
@@ -403,25 +425,22 @@ def make_payment_request(**args):
else ""
)
existing_payment_request = None
if args.order_type == "Shopping Cart":
existing_payment_request = frappe.db.get_value(
"Payment Request",
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)},
)
draft_payment_request = frappe.db.get_value(
"Payment Request",
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
)
if existing_payment_request:
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
if existing_payment_request_amount:
grand_total -= existing_payment_request_amount
if draft_payment_request:
frappe.db.set_value(
"Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
)
pr = frappe.get_doc("Payment Request", existing_payment_request)
pr = frappe.get_doc("Payment Request", draft_payment_request)
else:
if args.order_type != "Shopping Cart":
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
if existing_payment_request_amount:
grand_total -= existing_payment_request_amount
pr = frappe.new_doc("Payment Request")
pr.update(
{
@@ -444,6 +463,17 @@ def make_payment_request(**args):
}
)
# Update dimensions
pr.update(
{
"cost_center": ref_doc.get("cost_center"),
"project": ref_doc.get("project"),
}
)
for dimension in get_accounting_dimensions():
pr.update({dimension: ref_doc.get(dimension)})
if args.order_type == "Shopping Cart" or args.mute_email:
pr.flags.mute_email = True

View File

@@ -10,7 +10,7 @@ import re
import frappe
from frappe import _, throw
from frappe.model.document import Document
from frappe.utils import cint, flt, getdate
from frappe.utils import cint, flt
apply_on_dict = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}
@@ -184,8 +184,7 @@ class PricingRule(Document):
if self.is_cumulative and not (self.valid_from and self.valid_upto):
frappe.throw(_("Valid from and valid upto fields are mandatory for the cumulative"))
if self.valid_from and self.valid_upto and getdate(self.valid_from) > getdate(self.valid_upto):
frappe.throw(_("Valid from date must be less than valid upto date"))
self.validate_from_to_dates("valid_from", "valid_upto")
def validate_condition(self):
if (
@@ -256,7 +255,7 @@ def apply_pricing_rule(args, doc=None):
for item in item_list:
args_copy = copy.deepcopy(args)
args_copy.update(item)
data = get_pricing_rule_for_item(args_copy, item.get("price_list_rate"), doc=doc)
data = get_pricing_rule_for_item(args_copy, doc=doc)
out.append(data)
if (
@@ -293,7 +292,7 @@ def update_pricing_rule_uom(pricing_rule, args):
pricing_rule.uom = row.uom
def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False):
def get_pricing_rule_for_item(args, doc=None, for_validate=False):
from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules,
get_pricing_rule_items,

View File

@@ -1123,7 +1123,7 @@ def make_pricing_rule(**args):
"apply_on": args.apply_on or "Item Code",
"applicable_for": args.applicable_for,
"selling": args.selling or 0,
"currency": "USD",
"currency": "INR",
"apply_discount_on_rate": args.apply_discount_on_rate or 0,
"buying": args.buying or 0,
"min_qty": args.min_qty or 0.0,

View File

@@ -250,6 +250,22 @@ def get_other_conditions(conditions, values, args):
and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')"""
values["transaction_date"] = args.get("transaction_date")
if args.get("doctype") in [
"Quotation",
"Quotation Item",
"Sales Order",
"Sales Order Item",
"Delivery Note",
"Delivery Note Item",
"Sales Invoice",
"Sales Invoice Item",
"POS Invoice",
"POS Invoice Item",
]:
conditions += """ and ifnull(`tabPricing Rule`.selling, 0) = 1"""
else:
conditions += """ and ifnull(`tabPricing Rule`.buying, 0) = 1"""
return conditions
@@ -669,7 +685,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
item_details.free_item_data.append(free_item_data_args)
def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False):
def apply_pricing_rule_for_free_items(doc, pricing_rule_args):
if pricing_rule_args:
items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item)

View File

@@ -64,12 +64,13 @@
"tax_withholding_net_total",
"base_tax_withholding_net_total",
"taxes_section",
"tax_category",
"taxes_and_charges",
"column_break_58",
"tax_category",
"column_break_49",
"shipping_rule",
"column_break_49",
"incoterm",
"named_place",
"section_break_51",
"taxes",
"totals",
@@ -1541,13 +1542,19 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2022-11-25 12:44:29.935567",
"modified": "2022-12-12 18:37:38.142688",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@@ -231,7 +231,9 @@ class PurchaseInvoice(BuyingController):
)
if (
cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) and not self.is_return
cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate"))
and not self.is_return
and not self.is_internal_supplier
):
self.validate_rate_with_reference_doc(
[

View File

@@ -61,12 +61,13 @@
"total",
"net_total",
"taxes_section",
"tax_category",
"taxes_and_charges",
"column_break_38",
"shipping_rule",
"incoterm",
"column_break_55",
"tax_category",
"incoterm",
"named_place",
"section_break_40",
"taxes",
"section_break_43",
@@ -921,6 +922,7 @@
"fieldtype": "Table",
"hide_days": 1,
"hide_seconds": 1,
"label": "Sales Taxes and Charges",
"oldfieldname": "other_charges",
"oldfieldtype": "Table",
"options": "Sales Taxes and Charges"
@@ -2121,6 +2123,12 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
}
],
"icon": "fa fa-file-text",
@@ -2133,7 +2141,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2022-11-17 17:17:10.883487",
"modified": "2022-12-12 18:34:33.409895",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@@ -1185,11 +1185,24 @@ class SalesInvoice(SellingController):
if asset.calculate_depreciation:
posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
reset_depreciation_schedule(asset, self.posting_date)
notes = _(
"This schedule was created when Asset {0} was returned after being sold through Sales Invoice {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.doctype, self.get("name")),
)
reset_depreciation_schedule(asset, self.posting_date, notes)
asset.reload()
else:
if asset.calculate_depreciation:
depreciate_asset(asset, self.posting_date)
notes = _(
"This schedule was created when Asset {0} was sold through Sales Invoice {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.doctype, self.get("name")),
)
depreciate_asset(asset, self.posting_date, notes)
asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(

View File

@@ -21,6 +21,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp
from erpnext.accounts.utils import PaymentEntryUnlinkError
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
)
from erpnext.controllers.accounts_controller import update_invoice_status
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
@@ -2774,7 +2777,7 @@ class TestSalesInvoice(unittest.TestCase):
["2021-09-30", 5041.1, 26407.22],
]
for i, schedule in enumerate(asset.schedules):
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
@@ -2805,7 +2808,7 @@ class TestSalesInvoice(unittest.TestCase):
expected_values = [["2020-12-31", 30000, 30000], ["2021-12-31", 30000, 60000]]
for i, schedule in enumerate(asset.schedules):
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
@@ -2834,7 +2837,7 @@ class TestSalesInvoice(unittest.TestCase):
["2025-06-06", 18633.88, 100000.0, False],
]
for i, schedule in enumerate(asset.schedules):
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)

View File

@@ -890,7 +890,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2022-11-02 12:53:12.693217",
"modified": "2022-12-28 16:17:33.484531",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -280,7 +280,8 @@ class Subscription(Document):
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
self.validate_end_date()
self.validate_to_follow_calendar_months()
self.cost_center = erpnext.get_default_cost_center(self.get("company"))
if not self.cost_center:
self.cost_center = erpnext.get_default_cost_center(self.get("company"))
def validate_trial_period(self):
"""

View File

@@ -5,5 +5,9 @@ frappe.ui.form.on('Subscription Plan', {
price_determination: function(frm) {
frm.toggle_reqd("cost", frm.doc.price_determination === 'Fixed rate');
frm.toggle_reqd("price_list", frm.doc.price_determination === 'Based on price list');
}
},
subscription_plan: function (frm) {
erpnext.utils.check_payments_app();
},
});

View File

@@ -32,7 +32,7 @@ class TaxRule(Document):
def validate(self):
self.validate_tax_template()
self.validate_date()
self.validate_from_to_dates("from_date", "to_date")
self.validate_filters()
self.validate_use_for_shopping_cart()
@@ -51,10 +51,6 @@ class TaxRule(Document):
if not (self.sales_tax_template or self.purchase_tax_template):
frappe.throw(_("Tax Template is mandatory."))
def validate_date(self):
if self.from_date and self.to_date and self.from_date > self.to_date:
frappe.throw(_("From Date cannot be greater than To Date"))
def validate_filters(self):
filters = {
"tax_type": self.tax_type,

View File

@@ -121,12 +121,24 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
cost_center = get_cost_center(inv)
tax_row.update({"cost_center": cost_center})
if inv.doctype == "Purchase Invoice":
return tax_row, tax_deducted_on_advances, voucher_wise_amount
else:
return tax_row
def get_cost_center(inv):
cost_center = frappe.get_cached_value("Company", inv.company, "cost_center")
if len(inv.get("taxes", [])) > 0:
cost_center = inv.get("taxes")[0].cost_center
return cost_center
def get_tax_withholding_details(tax_withholding_category, posting_date, company):
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)

View File

@@ -199,7 +199,14 @@ def merge_similar_entries(gl_map, precision=None):
# filter zero debit and credit entries
merged_gl_map = filter(
lambda x: flt(x.debit, precision) != 0 or flt(x.credit, precision) != 0, merged_gl_map
lambda x: flt(x.debit, precision) != 0
or flt(x.credit, precision) != 0
or (
x.voucher_type == "Journal Entry"
and frappe.get_cached_value("Journal Entry", x.voucher_no, "voucher_type")
== "Exchange Gain Or Loss"
),
merged_gl_map,
)
merged_gl_map = list(merged_gl_map)
@@ -350,15 +357,26 @@ def process_debit_credit_difference(gl_map):
allowance = get_debit_credit_allowance(voucher_type, precision)
debit_credit_diff = get_debit_credit_difference(gl_map, precision)
if abs(debit_credit_diff) > allowance:
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
if not (
voucher_type == "Journal Entry"
and frappe.get_cached_value("Journal Entry", voucher_no, "voucher_type")
== "Exchange Gain Or Loss"
):
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
elif abs(debit_credit_diff) >= (1.0 / (10**precision)):
make_round_off_gle(gl_map, debit_credit_diff, precision)
debit_credit_diff = get_debit_credit_difference(gl_map, precision)
if abs(debit_credit_diff) > allowance:
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
if not (
voucher_type == "Journal Entry"
and frappe.get_cached_value("Journal Entry", voucher_no, "voucher_type")
== "Exchange Gain Or Loss"
):
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
def get_debit_credit_difference(gl_map, precision):

View File

@@ -99,6 +99,9 @@ class ReceivablePayableReport(object):
# Get return entries
self.get_return_entries()
# Get Exchange Rate Revaluations
self.get_exchange_rate_revaluations()
self.data = []
for ple in self.ple_entries:
@@ -251,7 +254,8 @@ class ReceivablePayableReport(object):
row.invoice_grand_total = row.invoiced
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
or (row.voucher_no in self.err_journals)
):
# non-zero oustanding, we must consider this row
@@ -794,19 +798,19 @@ class ReceivablePayableReport(object):
if self.filters.get("payment_terms_template"):
self.qb_selection_filter.append(
self.ple.party_isin(
qb.from_(self.customer).where(
self.customer.payment_terms == self.filters.get("payment_terms_template")
)
self.ple.party.isin(
qb.from_(self.customer)
.select(self.customer.name)
.where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
)
)
if self.filters.get("sales_partner"):
self.qb_selection_filter.append(
self.ple.party_isin(
qb.from_(self.customer).where(
self.customer.default_sales_partner == self.filters.get("payment_terms_template")
)
self.ple.party.isin(
qb.from_(self.customer)
.select(self.customer.name)
.where(self.customer.default_sales_partner == self.filters.get("sales_partner"))
)
)
@@ -865,10 +869,15 @@ class ReceivablePayableReport(object):
def get_party_details(self, party):
if not party in self.party_details:
if self.party_type == "Customer":
fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"]
if self.filters.get("sales_partner"):
fields.append("default_sales_partner")
self.party_details[party] = frappe.db.get_value(
"Customer",
party,
["customer_name", "territory", "customer_group", "customer_primary_contact"],
fields,
as_dict=True,
)
else:
@@ -969,6 +978,9 @@ class ReceivablePayableReport(object):
if self.filters.show_sales_person:
self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data")
if self.filters.sales_partner:
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
if self.filters.party_type == "Supplier":
self.add_column(
label=_("Supplier Group"),
@@ -1028,3 +1040,17 @@ class ReceivablePayableReport(object):
"data": {"labels": self.ageing_column_labels, "datasets": rows},
"type": "percentage",
}
def get_exchange_rate_revaluations(self):
je = qb.DocType("Journal Entry")
results = (
qb.from_(je)
.select(je.name)
.where(
(je.company == self.filters.company)
& (je.posting_date.lte(self.filters.report_date))
& (je.voucher_type == "Exchange Rate Revaluation")
)
.run()
)
self.err_journals = [x[0] for x in results] if results else []

View File

@@ -1,9 +1,10 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate, today
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, getdate, today
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
@@ -17,10 +18,37 @@ class TestAccountsReceivable(FrappeTestCase):
frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'")
self.create_usd_account()
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 test_accounts_receivable(self):
filters = {
"company": "_Test Company 2",
@@ -33,7 +61,7 @@ class TestAccountsReceivable(FrappeTestCase):
}
# check invoice grand total and invoiced column's value for 3 payment terms
name = make_sales_invoice()
name = make_sales_invoice().name
report = execute(filters)
expected_data = [[100, 30], [100, 50], [100, 20]]
@@ -118,8 +146,72 @@ class TestAccountsReceivable(FrappeTestCase):
],
)
@change_settings(
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
)
def test_exchange_revaluation_for_party(self):
"""
Exchange Revaluation for party on Receivable/Payable shoule be included
"""
def make_sales_invoice():
company = "_Test Company 2"
customer = "_Test Customer 2"
# Using Exchange Gain/Loss account for unrealized as well.
company_doc = frappe.get_doc("Company", company)
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
company_doc.save()
si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.currency = "USD"
si.conversion_rate = 0.90
si.debit_to = self.debtors_usd
si = si.save().submit()
# Exchange Revaluation
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = company
err.posting_date = today()
accounts = err.get_accounts_data()
err.extend("accounts", accounts)
err.accounts[0].new_exchange_rate = 0.95
row = err.accounts[0]
row.new_balance_in_base_currency = flt(
row.new_exchange_rate * flt(row.balance_in_account_currency)
)
row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
err.set_total_gain_loss()
err = err.save().submit()
# Submit JV for ERR
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
je = je.submit()
filters = {
"company": company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
report = execute(filters)
expected_data_for_err = [0, -5, 0, 5]
row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0]
self.assertEqual(
expected_data_for_err,
[
row.invoiced,
row.paid,
row.credit_note,
row.outstanding,
],
)
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
si = create_sales_invoice(
@@ -134,22 +226,26 @@ def make_sales_invoice():
do_not_save=1,
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
)
if not no_payment_schedule:
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
)
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
)
si.submit()
si = si.save()
return si.name
if not do_not_submit:
si = si.submit()
return si
def make_payment(docname):

View File

@@ -121,6 +121,9 @@ class AccountsReceivableSummary(ReceivablePayableReport):
if row.sales_person:
self.party_total[row.party].sales_person.append(row.sales_person)
if self.filters.sales_partner:
self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner")
def get_columns(self):
self.columns = []
self.add_column(
@@ -160,6 +163,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
)
if self.filters.show_sales_person:
self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data")
if self.filters.sales_partner:
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
else:
self.add_column(
label=_("Supplier Group"),

View File

@@ -131,8 +131,8 @@ def get_assets(filters):
else
0
end), 0) as depreciation_amount_during_the_period
from `tabAsset` a, `tabDepreciation Schedule` ds
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent and ifnull(ds.journal_entry, '') != ''
from `tabAsset` a, `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and ads.asset = a.name and ads.docstatus=1 and ads.name = ds.parent and ifnull(ds.journal_entry, '') != ''
group by a.asset_category
union
SELECT a.asset_category,

View File

@@ -8,6 +8,7 @@ from frappe.utils import cint, cstr
from erpnext.accounts.report.financial_statements import (
get_columns,
get_cost_centers_with_children,
get_data,
get_filtered_list_for_consolidated_report,
get_period_list,
@@ -160,10 +161,11 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
total = 0
for period in period_list:
start_date = get_start_date(period, accumulated_values, company)
filters.start_date = start_date
filters.end_date = period["to_date"]
filters.account_type = account_type
amount = get_account_type_based_gl_data(
company, start_date, period["to_date"], account_type, filters
)
amount = get_account_type_based_gl_data(company, filters)
if amount and account_type == "Depreciation":
amount *= -1
@@ -175,7 +177,7 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
return data
def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None):
def get_account_type_based_gl_data(company, filters=None):
cond = ""
filters = frappe._dict(filters or {})
@@ -191,17 +193,21 @@ def get_account_type_based_gl_data(company, start_date, end_date, account_type,
frappe.db.escape(cstr(filters.finance_book))
)
if filters.get("cost_center"):
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
cond += " and cost_center in %(cost_center)s"
gl_sum = frappe.db.sql_list(
"""
select sum(credit) - sum(debit)
from `tabGL Entry`
where company=%s and posting_date >= %s and posting_date <= %s
where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s
and voucher_type != 'Period Closing Voucher'
and account in ( SELECT name FROM tabAccount WHERE account_type = %s) {cond}
and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond}
""".format(
cond=cond
),
(company, start_date, end_date, account_type),
filters,
)
return gl_sum[0] if gl_sum and gl_sum[0] else 0

View File

@@ -268,10 +268,12 @@ def get_cash_flow_data(fiscal_year, companies, filters):
def get_account_type_based_data(account_type, companies, fiscal_year, filters):
data = {}
total = 0
filters.account_type = account_type
filters.start_date = fiscal_year.year_start_date
filters.end_date = fiscal_year.year_end_date
for company in companies:
amount = get_account_type_based_gl_data(
company, fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters
)
amount = get_account_type_based_gl_data(company, filters)
if amount and account_type == "Depreciation":
amount *= -1
@@ -533,12 +535,13 @@ def get_accounts(root_type, companies):
],
filters={"company": company, "root_type": root_type},
):
if account.account_name not in added_accounts:
if account.account_number:
account_key = account.account_number + "-" + account.account_name
else:
account_key = account.account_name
if account_key not in added_accounts:
accounts.append(account)
if account.account_number:
account_key = account.account_number + "-" + account.account_name
else:
account_key = account.account_name
added_accounts.append(account_key)
return accounts

View File

@@ -26,6 +26,7 @@ class PartyLedgerSummaryReport(object):
)
self.get_gl_entries()
self.get_additional_columns()
self.get_return_invoices()
self.get_party_adjustment_amounts()
@@ -33,6 +34,42 @@ class PartyLedgerSummaryReport(object):
data = self.get_data()
return columns, data
def get_additional_columns(self):
"""
Additional Columns for 'User Permission' based access control
"""
from frappe import qb
if self.filters.party_type == "Customer":
self.territories = frappe._dict({})
self.customer_group = frappe._dict({})
customer = qb.DocType("Customer")
result = (
frappe.qb.from_(customer)
.select(
customer.name, customer.territory, customer.customer_group, customer.default_sales_partner
)
.where((customer.disabled == 0))
.run(as_dict=True)
)
for x in result:
self.territories[x.name] = x.territory
self.customer_group[x.name] = x.customer_group
else:
self.supplier_group = frappe._dict({})
supplier = qb.DocType("Supplier")
result = (
frappe.qb.from_(supplier)
.select(supplier.name, supplier.supplier_group)
.where((supplier.disabled == 0))
.run(as_dict=True)
)
for x in result:
self.supplier_group[x.name] = x.supplier_group
def get_columns(self):
columns = [
{
@@ -116,6 +153,35 @@ class PartyLedgerSummaryReport(object):
},
]
# Hidden columns for handling 'User Permissions'
if self.filters.party_type == "Customer":
columns += [
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
"hidden": 1,
},
{
"label": _("Customer Group"),
"fieldname": "customer_group",
"fieldtype": "Link",
"options": "Customer Group",
"hidden": 1,
},
]
else:
columns += [
{
"label": _("Supplier Group"),
"fieldname": "supplier_group",
"fieldtype": "Link",
"options": "Supplier Group",
"hidden": 1,
}
]
return columns
def get_data(self):
@@ -143,6 +209,12 @@ class PartyLedgerSummaryReport(object):
),
)
if self.filters.party_type == "Customer":
self.party_data[gle.party].update({"territory": self.territories.get(gle.party)})
self.party_data[gle.party].update({"customer_group": self.customer_group.get(gle.party)})
else:
self.party_data[gle.party].update({"supplier_group": self.supplier_group.get(gle.party)})
amount = gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr)
self.party_data[gle.party].closing_balance += amount

View File

@@ -239,7 +239,7 @@ def get_conditions(filters):
):
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
conditions.append("(posting_date <=%(to_date)s)")
conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
if filters.get("project"):
conditions.append("project in %(project)s")

View File

@@ -109,8 +109,7 @@ class TestGeneralLedger(FrappeTestCase):
frappe.db.set_value(
"Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
)
revaluation_jv = revaluation.make_jv_entry()
revaluation_jv = frappe.get_doc(revaluation_jv)
revaluation_jv = revaluation.make_jv_for_revaluation()
revaluation_jv.cost_center = "_Test Cost Center - _TC"
for acc in revaluation_jv.get("accounts"):
acc.cost_center = "_Test Cost Center - _TC"

View File

@@ -439,6 +439,18 @@ class GrossProfitGenerator(object):
row.delivery_note, frappe._dict()
)
row.item_row = row.dn_detail
# Update warehouse and base_amount from 'Packed Item' List
if product_bundles and not row.parent:
# For Packed Items, row.parent_invoice will be the Bundle name
product_bundle = product_bundles.get(row.parent_invoice)
if product_bundle:
for packed_item in product_bundle:
if (
packed_item.get("item_code") == row.item_code
and packed_item.get("parent_detail_docname") == row.item_row
):
row.warehouse = packed_item.warehouse
row.base_amount = packed_item.base_amount
# get buying amount
if row.item_code in product_bundles:
@@ -503,7 +515,7 @@ class GrossProfitGenerator(object):
invoice_portion = 100
elif row.invoice_portion:
invoice_portion = row.invoice_portion
else:
elif row.payment_amount:
invoice_portion = row.payment_amount * 100 / row.base_net_amount
if i == 0:
@@ -589,7 +601,9 @@ class GrossProfitGenerator(object):
buying_amount = 0.0
for packed_item in product_bundle:
if packed_item.get("parent_detail_docname") == row.item_row:
buying_amount += self.get_buying_amount(row, packed_item.item_code)
packed_item_row = row.copy()
packed_item_row.warehouse = packed_item.warehouse
buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code)
return flt(buying_amount, self.currency_precision)
@@ -607,6 +621,7 @@ class GrossProfitGenerator(object):
return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
else:
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
return 0.0
def get_buying_amount(self, row, item_code):
# IMP NOTE
@@ -921,12 +936,25 @@ class GrossProfitGenerator(object):
def load_product_bundle(self):
self.product_bundles = {}
for d in frappe.db.sql(
"""select parenttype, parent, parent_item,
item_code, warehouse, -1*qty as total_qty, parent_detail_docname
from `tabPacked Item` where docstatus=1""",
as_dict=True,
):
pki = qb.DocType("Packed Item")
pki_query = (
frappe.qb.from_(pki)
.select(
pki.parenttype,
pki.parent,
pki.parent_item,
pki.item_code,
pki.warehouse,
(-1 * pki.qty).as_("total_qty"),
pki.rate,
(pki.rate * pki.qty).as_("base_amount"),
pki.parent_detail_docname,
)
.where(pki.docstatus == 1)
)
for d in pki_query.run(as_dict=True):
self.product_bundles.setdefault(d.parenttype, frappe._dict()).setdefault(
d.parent, frappe._dict()
).setdefault(d.parent_item, []).append(d)

View File

@@ -6,6 +6,8 @@ from frappe.utils import add_days, flt, nowdate
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.gross_profit.gross_profit import execute
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -14,6 +16,7 @@ class TestGrossProfit(FrappeTestCase):
def setUp(self):
self.create_company()
self.create_item()
self.create_bundle()
self.create_customer()
self.create_sales_invoice()
self.clear_old_entries()
@@ -42,6 +45,7 @@ class TestGrossProfit(FrappeTestCase):
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "Stores - " + abbr
self.finished_warehouse = "Finished Goods - " + abbr
self.income_account = "Sales - " + abbr
self.expense_account = "Cost of Goods Sold - " + abbr
self.debit_to = "Debtors - " + abbr
@@ -53,6 +57,23 @@ class TestGrossProfit(FrappeTestCase):
)
self.item = item if isinstance(item, str) else item.item_code
def create_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
item2 = create_item(
item_code="_Test GP Item 2", is_stock_item=1, company=self.company, warehouse=self.warehouse
)
self.item2 = item2 if isinstance(item2, str) else item2.item_code
# This will be parent item
bundle = create_item(
item_code="_Test GP bundle", is_stock_item=0, company=self.company, warehouse=self.warehouse
)
self.bundle = bundle if isinstance(bundle, str) else bundle.item_code
# Create Product Bundle
self.product_bundle = make_product_bundle(parent=self.bundle, items=[self.item, self.item2])
def create_customer(self):
name = "_Test GP Customer"
if frappe.db.exists("Customer", name):
@@ -93,6 +114,28 @@ class TestGrossProfit(FrappeTestCase):
)
return sinv
def create_delivery_note(
self, item=None, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
):
"""
Helper function to populate default values in Delivery Note
"""
dnote = create_delivery_note(
company=self.company,
customer=self.customer,
currency="INR",
item=item or self.item,
qty=qty,
rate=rate,
cost_center=self.cost_center,
warehouse=self.warehouse,
return_against=None,
expense_account=self.expense_account,
do_not_save=do_not_save,
do_not_submit=do_not_submit,
)
return dnote
def clear_old_entries(self):
doctype_list = [
"Sales Invoice",
@@ -207,3 +250,55 @@ class TestGrossProfit(FrappeTestCase):
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0])
def test_bundled_delivery_note_with_different_warehouses(self):
"""
Test Delivery Note with bundled item. Packed Item from the bundle having different warehouses
"""
se = make_stock_entry(
company=self.company,
item_code=self.item,
target=self.warehouse,
qty=1,
basic_rate=100,
do_not_submit=True,
)
item = se.items[0]
se.append(
"items",
{
"item_code": self.item2,
"s_warehouse": "",
"t_warehouse": self.finished_warehouse,
"qty": 1,
"basic_rate": 100,
"conversion_factor": item.conversion_factor or 1.0,
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
"serial_no": item.serial_no,
"batch_no": item.batch_no,
"cost_center": item.cost_center,
"expense_account": item.expense_account,
},
)
se = se.save().submit()
# Make a Delivery note with Product bundle
# Packed Items will have different warehouses
dnote = self.create_delivery_note(item=self.bundle, qty=1, rate=200, do_not_submit=True)
dnote.packed_items[1].warehouse = self.finished_warehouse
dnote = dnote.submit()
# make Sales Invoice for above delivery note
sinv = make_sales_invoice(dnote.name)
sinv = sinv.save().submit()
filters = frappe._dict(
company=self.company,
from_date=nowdate(),
to_date=nowdate(),
group_by="Invoice",
sales_invoice=sinv.name,
)
columns, data = execute(filters=filters)
self.assertGreater(len(data), 0)

View File

@@ -53,9 +53,6 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
item_details = get_item_details()
for d in item_list:
if not d.stock_qty:
continue
item_record = item_details.get(d.item_code)
purchase_receipt = None
@@ -94,7 +91,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
"expense_account": expense_account,
"stock_qty": d.stock_qty,
"stock_uom": d.stock_uom,
"rate": d.base_net_amount / d.stock_qty,
"rate": d.base_net_amount / d.stock_qty if d.stock_qty else d.base_net_amount,
"amount": d.base_net_amount,
}
)

View File

@@ -63,24 +63,6 @@ frappe.query_reports["Supplier Ledger Summary"] = {
"fieldtype": "Link",
"options": "Payment Terms Template"
},
{
"fieldname":"territory",
"label": __("Territory"),
"fieldtype": "Link",
"options": "Territory"
},
{
"fieldname":"sales_partner",
"label": __("Sales Partner"),
"fieldtype": "Link",
"options": "Sales Partner"
},
{
"fieldname":"sales_person",
"label": __("Sales Person"),
"fieldtype": "Link",
"options": "Sales Person"
},
{
"fieldname":"tax_id",
"label": __("Tax Id"),

View File

@@ -234,8 +234,11 @@ def modify_report_columns(doctype, field, column):
if field in ["item_tax_rate", "base_net_amount"]:
return None
if doctype == "GL Entry" and field in ["debit", "credit"]:
column.update({"label": _("Amount"), "fieldname": "amount"})
if doctype == "GL Entry":
if field in ["debit", "credit"]:
column.update({"label": _("Amount"), "fieldname": "amount"})
elif field == "voucher_type":
column.update({"fieldtype": "Data", "options": ""})
if field == "taxes_and_charges":
column.update({"label": _("Taxes and Charges Template")})

View File

@@ -28,7 +28,7 @@ def get_currency(filters):
filters["presentation_currency"] if filters.get("presentation_currency") else company_currency
)
report_date = filters.get("to_date")
report_date = filters.get("to_date") or filters.get("period_end_date")
if not report_date:
fiscal_year_to_date = get_from_and_to_date(filters.get("to_fiscal_year"))["to_date"]
@@ -101,11 +101,8 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
account_currency = entry["account_currency"]
if len(account_currencies) == 1 and account_currency == presentation_currency:
if debit_in_account_currency:
entry["debit"] = debit_in_account_currency
if credit_in_account_currency:
entry["credit"] = credit_in_account_currency
entry["debit"] = debit_in_account_currency
entry["credit"] = credit_in_account_currency
else:
date = currency_info["report_date"]
converted_debit_value = convert(debit, presentation_currency, company_currency, date)

View File

@@ -3,11 +3,14 @@ import unittest
import frappe
from frappe.test_runner import make_test_objects
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.party import get_party_shipping_address
from erpnext.accounts.utils import (
get_future_stock_vouchers,
get_voucherwise_gl_entries,
sort_stock_vouchers_by_posting_date,
update_reference_in_payment_entry,
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -73,6 +76,47 @@ class TestUtils(unittest.TestCase):
sorted_vouchers = sort_stock_vouchers_by_posting_date(list(reversed(vouchers)))
self.assertEqual(sorted_vouchers, vouchers)
def test_update_reference_in_payment_entry(self):
item = make_item().name
purchase_invoice = make_purchase_invoice(
item=item, supplier="_Test Supplier USD", currency="USD", conversion_rate=82.32
)
purchase_invoice.submit()
payment_entry = get_payment_entry(purchase_invoice.doctype, purchase_invoice.name)
payment_entry.target_exchange_rate = 62.9
payment_entry.paid_amount = 15725
payment_entry.deductions = []
payment_entry.insert()
self.assertEqual(payment_entry.difference_amount, -4855.00)
payment_entry.references = []
payment_entry.submit()
payment_reconciliation = frappe.new_doc("Payment Reconciliation")
payment_reconciliation.company = payment_entry.company
payment_reconciliation.party_type = "Supplier"
payment_reconciliation.party = purchase_invoice.supplier
payment_reconciliation.receivable_payable_account = payment_entry.paid_to
payment_reconciliation.get_unreconciled_entries()
payment_reconciliation.allocate_entries(
{
"payments": [d.__dict__ for d in payment_reconciliation.payments],
"invoices": [d.__dict__ for d in payment_reconciliation.invoices],
}
)
for d in payment_reconciliation.invoices:
# Reset invoice outstanding_amount because allocate_entries will zero this value out.
d.outstanding_amount = d.amount
for d in payment_reconciliation.allocation:
d.difference_account = "Exchange Gain/Loss - _TC"
payment_reconciliation.reconcile()
payment_entry.load_from_db()
self.assertEqual(len(payment_entry.references), 1)
self.assertEqual(payment_entry.difference_amount, 0)
ADDRESS_RECORDS = [
{

View File

@@ -611,11 +611,6 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
new_row.docstatus = 1
new_row.update(reference_details)
payment_entry.flags.ignore_validate_update_after_submit = True
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
payment_entry.set_amounts()
if d.difference_amount and d.difference_account:
account_details = {
"account": d.difference_account,
@@ -627,6 +622,11 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
payment_entry.set_gain_or_loss(account_details=account_details)
payment_entry.flags.ignore_validate_update_after_submit = True
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
payment_entry.set_amounts()
if not do_not_save:
payment_entry.save(ignore_permissions=True)
@@ -836,6 +836,7 @@ def get_outstanding_invoices(
posting_date=None,
min_outstanding=None,
max_outstanding=None,
accounting_dimensions=None,
):
ple = qb.DocType("Payment Ledger Entry")
@@ -866,6 +867,7 @@ def get_outstanding_invoices(
min_outstanding=min_outstanding,
max_outstanding=max_outstanding,
get_invoices=True,
accounting_dimensions=accounting_dimensions or [],
)
for d in invoice_list:
@@ -1615,6 +1617,7 @@ class QueryPaymentLedger(object):
.where(ple.delinked == 0)
.where(Criterion.all(filter_on_voucher_no))
.where(Criterion.all(self.common_filter))
.where(Criterion.all(self.dimensions_filter))
.where(Criterion.all(self.voucher_posting_date))
.groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party)
)
@@ -1702,6 +1705,7 @@ class QueryPaymentLedger(object):
max_outstanding=None,
get_payments=False,
get_invoices=False,
accounting_dimensions=None,
):
"""
Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE
@@ -1717,6 +1721,7 @@ class QueryPaymentLedger(object):
self.reset()
self.vouchers = vouchers
self.common_filter = common_filter or []
self.dimensions_filter = accounting_dimensions or []
self.voucher_posting_date = posting_date or []
self.min_outstanding = min_outstanding
self.max_outstanding = max_outstanding

View File

@@ -76,7 +76,6 @@ frappe.ui.form.on('Asset', {
refresh: function(frm) {
frappe.ui.form.trigger("Asset", "is_existing_asset");
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
frm.events.make_schedules_editable(frm);
if (frm.doc.docstatus==1) {
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
@@ -188,7 +187,11 @@ frappe.ui.form.on('Asset', {
})
},
setup_chart: function(frm) {
setup_chart: async function(frm) {
if(frm.doc.finance_books.length > 1) {
return
}
var x_intervals = [frm.doc.purchase_date];
var asset_values = [frm.doc.gross_purchase_amount];
var last_depreciation_date = frm.doc.purchase_date;
@@ -202,7 +205,20 @@ frappe.ui.form.on('Asset', {
flt(frm.doc.opening_accumulated_depreciation));
}
$.each(frm.doc.schedules || [], function(i, v) {
let depr_schedule = [];
if (frm.doc.finance_books.length == 1) {
depr_schedule = (await frappe.call(
"erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule",
{
asset_name: frm.doc.name,
status: frm.doc.docstatus ? "Active" : "Draft",
finance_book: frm.doc.finance_books[0].finance_book || null
}
)).message;
}
$.each(depr_schedule || [], function(i, v) {
x_intervals.push(v.schedule_date);
var asset_value = flt(frm.doc.gross_purchase_amount) - flt(v.accumulated_depreciation_amount);
if(v.journal_entry) {
@@ -266,21 +282,6 @@ frappe.ui.form.on('Asset', {
// frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation));
},
opening_accumulated_depreciation: function(frm) {
erpnext.asset.set_accumulated_depreciation(frm);
},
make_schedules_editable: function(frm) {
if (frm.doc.finance_books) {
var is_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0
? true : false;
frm.toggle_enable("schedules", is_editable);
frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_editable);
frm.fields_dict["schedules"].grid.toggle_enable("depreciation_amount", is_editable);
}
},
make_sales_invoice: function(frm) {
frappe.call({
args: {
@@ -476,7 +477,6 @@ frappe.ui.form.on('Asset Finance Book', {
depreciation_method: function(frm, cdt, cdn) {
const row = locals[cdt][cdn];
frm.events.set_depreciation_rate(frm, row);
frm.events.make_schedules_editable(frm);
},
expected_value_after_useful_life: function(frm, cdt, cdn) {
@@ -512,41 +512,6 @@ frappe.ui.form.on('Asset Finance Book', {
}
});
frappe.ui.form.on('Depreciation Schedule', {
make_depreciation_entry: function(frm, cdt, cdn) {
var row = locals[cdt][cdn];
if (!row.journal_entry) {
frappe.call({
method: "erpnext.assets.doctype.asset.depreciation.make_depreciation_entry",
args: {
"asset_name": frm.doc.name,
"date": row.schedule_date
},
callback: function(r) {
frappe.model.sync(r.message);
frm.refresh();
}
})
}
},
depreciation_amount: function(frm, cdt, cdn) {
erpnext.asset.set_accumulated_depreciation(frm);
}
})
erpnext.asset.set_accumulated_depreciation = function(frm) {
if(frm.doc.depreciation_method != "Manual") return;
var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation);
$.each(frm.doc.schedules || [], function(i, row) {
accumulated_depreciation += flt(row.depreciation_amount);
frappe.model.set_value(row.doctype, row.name,
"accumulated_depreciation_amount", accumulated_depreciation);
})
};
erpnext.asset.scrap_asset = function(frm) {
frappe.confirm(__("Do you really want to scrap this asset?"), function () {
frappe.call({

View File

@@ -52,8 +52,6 @@
"column_break_24",
"frequency_of_depreciation",
"next_depreciation_date",
"section_break_14",
"schedules",
"insurance_details",
"policy_number",
"insurer",
@@ -307,19 +305,6 @@
"label": "Next Depreciation Date",
"no_copy": 1
},
{
"depends_on": "calculate_depreciation",
"fieldname": "section_break_14",
"fieldtype": "Section Break",
"label": "Depreciation Schedule"
},
{
"fieldname": "schedules",
"fieldtype": "Table",
"label": "Depreciation Schedule",
"no_copy": 1,
"options": "Depreciation Schedule"
},
{
"collapsible": 1,
"fieldname": "insurance_details",
@@ -508,9 +493,14 @@
"group": "Value",
"link_doctype": "Asset Value Adjustment",
"link_fieldname": "asset"
},
{
"group": "Depreciation",
"link_doctype": "Asset Depreciation Schedule",
"link_fieldname": "asset"
}
],
"modified": "2022-07-20 10:15:12.887372",
"modified": "2022-11-25 12:47:19.689702",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -8,14 +8,15 @@ import math
import frappe
from frappe import _
from frappe.utils import (
add_days,
add_months,
cint,
date_diff,
flt,
get_datetime,
get_last_day,
get_link_to_form,
getdate,
is_last_day_of_the_month,
month_diff,
nowdate,
today,
@@ -28,6 +29,16 @@ from erpnext.assets.doctype.asset.depreciation import (
get_disposal_account_and_cost_center,
)
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
cancel_asset_depr_schedules,
convert_draft_asset_depr_schedules_into_active,
get_asset_depr_schedule_doc,
get_depr_schedule,
make_draft_asset_depr_schedules,
make_draft_asset_depr_schedules_if_not_present,
set_draft_asset_depr_schedule_details,
update_draft_asset_depr_schedules,
)
from erpnext.controllers.accounts_controller import AccountsController
@@ -40,9 +51,9 @@ class Asset(AccountsController):
self.set_missing_values()
if not self.split_from:
self.prepare_depreciation_data()
update_draft_asset_depr_schedules(self)
self.validate_gross_and_purchase_amount()
if self.get("schedules"):
self.validate_expected_value_after_useful_life()
self.validate_expected_value_after_useful_life()
self.status = self.get_status()
@@ -52,16 +63,24 @@ class Asset(AccountsController):
self.make_asset_movement()
if not self.booked_fixed_asset and self.validate_make_gl_entry():
self.make_gl_entries()
if not self.split_from:
make_draft_asset_depr_schedules_if_not_present(self)
convert_draft_asset_depr_schedules_into_active(self)
def on_cancel(self):
self.validate_cancellation()
self.cancel_movement_entries()
self.delete_depreciation_entries()
cancel_asset_depr_schedules(self)
self.set_status()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name)
self.db_set("booked_fixed_asset", 0)
def after_insert(self):
if not self.split_from:
make_draft_asset_depr_schedules(self)
def validate_asset_and_reference(self):
if self.purchase_invoice or self.purchase_receipt:
reference_doc = "Purchase Invoice" if self.purchase_invoice else "Purchase Receipt"
@@ -79,12 +98,10 @@ class Asset(AccountsController):
_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)
)
def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None):
def prepare_depreciation_data(self):
if self.calculate_depreciation:
self.value_after_depreciation = 0
self.set_depreciation_rate()
self.make_depreciation_schedule(date_of_disposal)
self.set_accumulated_depreciation(date_of_disposal, date_of_return)
else:
self.finance_books = []
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
@@ -223,148 +240,6 @@ class Asset(AccountsController):
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
)
def make_depreciation_schedule(self, date_of_disposal):
if "Manual" not in [d.depreciation_method for d in self.finance_books] and not self.get(
"schedules"
):
self.schedules = []
if not self.available_for_use_date:
return
start = self.clear_depreciation_schedule()
for finance_book in self.get("finance_books"):
self._make_depreciation_schedule(finance_book, start, date_of_disposal)
def _make_depreciation_schedule(self, finance_book, start, date_of_disposal):
self.validate_asset_finance_books(finance_book)
value_after_depreciation = self._get_value_after_depreciation(finance_book)
finance_book.value_after_depreciation = value_after_depreciation
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - cint(
self.number_of_depreciations_booked
)
has_pro_rata = self.check_is_pro_rata(finance_book)
if has_pro_rata:
number_of_pending_depreciations += 1
skip_row = False
should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date)
for n in range(start[finance_book.idx - 1], number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row:
continue
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
schedule_date = add_months(
finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)
)
if should_get_last_day:
schedule_date = get_last_day(schedule_date)
# schedule date will be a year later from start date
# so monthly schedule date is calculated by removing 11 months from it
monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1)
# if asset is being sold
if date_of_disposal:
from_date = self.get_from_date(finance_book.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(
finance_book, depreciation_amount, from_date, date_of_disposal
)
if depreciation_amount > 0:
self._add_depreciation_row(
date_of_disposal,
depreciation_amount,
finance_book.depreciation_method,
finance_book.finance_book,
finance_book.idx,
)
break
# For first row
if has_pro_rata and not self.opening_accumulated_depreciation and n == 0:
from_date = add_days(
self.available_for_use_date, -1
) # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = self.get_pro_rata_amt(
finance_book, depreciation_amount, from_date, finance_book.depreciation_start_date
)
# For first depr schedule date will be the start date
# so monthly schedule date is calculated by removing month difference between use date and start date
monthly_schedule_date = add_months(finance_book.depreciation_start_date, -months + 1)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
if not self.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months(
self.available_for_use_date,
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation),
)
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = self.get_pro_rata_amt(
finance_book, depreciation_amount, schedule_date, self.to_date
)
depreciation_amount = self.get_adjusted_depreciation_amount(
depreciation_amount_without_pro_rata, depreciation_amount, finance_book.finance_book
)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days)
last_schedule_date = schedule_date
if not depreciation_amount:
continue
value_after_depreciation -= flt(depreciation_amount, self.precision("gross_purchase_amount"))
# Adjust depreciation amount in the last period based on the expected value after useful life
if finance_book.expected_value_after_useful_life and (
(
n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != finance_book.expected_value_after_useful_life
)
or value_after_depreciation < finance_book.expected_value_after_useful_life
):
depreciation_amount += value_after_depreciation - finance_book.expected_value_after_useful_life
skip_row = True
if depreciation_amount > 0:
self._add_depreciation_row(
schedule_date,
depreciation_amount,
finance_book.depreciation_method,
finance_book.finance_book,
finance_book.idx,
)
def _add_depreciation_row(
self, schedule_date, depreciation_amount, depreciation_method, finance_book, finance_book_id
):
self.append(
"schedules",
{
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": depreciation_method,
"finance_book": finance_book,
"finance_book_id": finance_book_id,
},
)
def _get_value_after_depreciation(self, finance_book):
# value_after_depreciation - current Asset value
if self.docstatus == 1 and finance_book.value_after_depreciation:
@@ -376,58 +251,6 @@ class Asset(AccountsController):
return value_after_depreciation
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
# JE: Journal Entry, FB: Finance Book
def clear_depreciation_schedule(self):
start = []
num_of_depreciations_completed = 0
depr_schedule = []
for schedule in self.get("schedules"):
# to update start when there are JEs linked with all the schedule rows corresponding to an FB
if len(start) == (int(schedule.finance_book_id) - 2):
start.append(num_of_depreciations_completed)
num_of_depreciations_completed = 0
# to ensure that start will only be updated once for each FB
if len(start) == (int(schedule.finance_book_id) - 1):
if schedule.journal_entry:
num_of_depreciations_completed += 1
depr_schedule.append(schedule)
else:
start.append(num_of_depreciations_completed)
num_of_depreciations_completed = 0
# to update start when all the schedule rows corresponding to the last FB are linked with JEs
if len(start) == (len(self.finance_books) - 1):
start.append(num_of_depreciations_completed)
# when the Depreciation Schedule is being created for the first time
if start == []:
start = [0] * len(self.finance_books)
else:
self.schedules = depr_schedule
return start
def get_from_date(self, finance_book):
if not self.get("schedules"):
return self.available_for_use_date
if len(self.finance_books) == 1:
return self.schedules[-1].schedule_date
from_date = ""
for schedule in self.get("schedules"):
if schedule.finance_book == finance_book:
from_date = schedule.schedule_date
if from_date:
return from_date
# since depr for available_for_use_date is not yet booked
return add_days(self.available_for_use_date, -1)
# if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row):
has_pro_rata = False
@@ -512,83 +335,15 @@ class Asset(AccountsController):
).format(row.idx)
)
# to ensure that final accumulated depreciation amount is accurate
def get_adjusted_depreciation_amount(
self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book
):
if not self.opening_accumulated_depreciation:
depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book)
if (
depreciation_amount_for_first_row + depreciation_amount_for_last_row
!= depreciation_amount_without_pro_rata
):
depreciation_amount_for_last_row = (
depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
)
return depreciation_amount_for_last_row
def get_depreciation_amount_for_first_row(self, finance_book):
if self.has_only_one_finance_book():
return self.schedules[0].depreciation_amount
else:
for schedule in self.schedules:
if schedule.finance_book == finance_book:
return schedule.depreciation_amount
def has_only_one_finance_book(self):
if len(self.finance_books) == 1:
return True
def set_accumulated_depreciation(
self, date_of_sale=None, date_of_return=None, ignore_booked_entry=False
):
straight_line_idx = [
d.idx for d in self.get("schedules") if d.depreciation_method == "Straight Line"
]
finance_books = []
for i, d in enumerate(self.get("schedules")):
if ignore_booked_entry and d.journal_entry:
continue
if int(d.finance_book_id) not in finance_books:
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id))
finance_books.append(int(d.finance_book_id))
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
value_after_depreciation -= flt(depreciation_amount)
# for the last row, if depreciation method = Straight Line
if (
straight_line_idx
and i == max(straight_line_idx) - 1
and not date_of_sale
and not date_of_return
):
book = self.get("finance_books")[cint(d.finance_book_id) - 1]
depreciation_amount += flt(
value_after_depreciation - flt(book.expected_value_after_useful_life),
d.precision("depreciation_amount"),
)
d.depreciation_amount = depreciation_amount
accumulated_depreciation += d.depreciation_amount
d.accumulated_depreciation_amount = flt(
accumulated_depreciation, d.precision("accumulated_depreciation_amount")
)
def get_value_after_depreciation(self, idx):
return flt(self.get("finance_books")[cint(idx) - 1].value_after_depreciation)
def validate_expected_value_after_useful_life(self):
for row in self.get("finance_books"):
depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book)
if not depr_schedule:
continue
accumulated_depreciation_after_full_schedule = [
d.accumulated_depreciation_amount
for d in self.get("schedules")
if cint(d.finance_book_id) == row.idx
d.accumulated_depreciation_amount for d in depr_schedule
]
if accumulated_depreciation_after_full_schedule:
@@ -637,10 +392,13 @@ class Asset(AccountsController):
movement.cancel()
def delete_depreciation_entries(self):
for d in self.get("schedules"):
if d.journal_entry:
frappe.get_doc("Journal Entry", d.journal_entry).cancel()
d.db_set("journal_entry", None)
for row in self.get("finance_books"):
depr_schedule = get_depr_schedule(self.name, "Active", row.finance_book)
for d in depr_schedule or []:
if d.journal_entry:
frappe.get_doc("Journal Entry", d.journal_entry).cancel()
d.db_set("journal_entry", None)
self.db_set(
"value_after_depreciation",
@@ -1072,32 +830,6 @@ def get_total_days(date, frequency):
return date_diff(date, period_start_date)
def is_last_day_of_the_month(date):
last_day_of_the_month = get_last_day(date)
return getdate(last_day_of_the_month) == getdate(date)
@erpnext.allow_regional
def get_depreciation_amount(asset, depreciable_value, row):
if row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life:
depreciation_amount = (
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair
else:
depreciation_amount = (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
else:
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
return depreciation_amount
@frappe.whitelist()
def split_asset(asset_name, split_qty):
asset = frappe.get_doc("Asset", asset_name)
@@ -1109,12 +841,12 @@ def split_asset(asset_name, split_qty):
remaining_qty = asset.asset_quantity - split_qty
new_asset = create_new_asset_after_split(asset, split_qty)
update_existing_asset(asset, remaining_qty)
update_existing_asset(asset, remaining_qty, new_asset.name)
return new_asset
def update_existing_asset(asset, remaining_qty):
def update_existing_asset(asset, remaining_qty, new_asset_name):
remaining_gross_purchase_amount = flt(
(asset.gross_purchase_amount * remaining_qty) / asset.asset_quantity
)
@@ -1132,34 +864,49 @@ def update_existing_asset(asset, remaining_qty):
},
)
for finance_book in asset.get("finance_books"):
for row in asset.get("finance_books"):
value_after_depreciation = flt(
(finance_book.value_after_depreciation * remaining_qty) / asset.asset_quantity
(row.value_after_depreciation * remaining_qty) / asset.asset_quantity
)
expected_value_after_useful_life = flt(
(finance_book.expected_value_after_useful_life * remaining_qty) / asset.asset_quantity
(row.expected_value_after_useful_life * remaining_qty) / asset.asset_quantity
)
frappe.db.set_value(
"Asset Finance Book", finance_book.name, "value_after_depreciation", value_after_depreciation
"Asset Finance Book", row.name, "value_after_depreciation", value_after_depreciation
)
frappe.db.set_value(
"Asset Finance Book",
finance_book.name,
row.name,
"expected_value_after_useful_life",
expected_value_after_useful_life,
)
accumulated_depreciation = 0
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
asset.name, "Active", row.finance_book
)
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
for term in asset.get("schedules"):
depreciation_amount = flt((term.depreciation_amount * remaining_qty) / asset.asset_quantity)
frappe.db.set_value(
"Depreciation Schedule", term.name, "depreciation_amount", depreciation_amount
)
accumulated_depreciation += depreciation_amount
frappe.db.set_value(
"Depreciation Schedule", term.name, "accumulated_depreciation_amount", accumulated_depreciation
set_draft_asset_depr_schedule_details(new_asset_depr_schedule_doc, asset, row)
accumulated_depreciation = 0
for term in new_asset_depr_schedule_doc.get("depreciation_schedule"):
depreciation_amount = flt((term.depreciation_amount * remaining_qty) / asset.asset_quantity)
term.depreciation_amount = depreciation_amount
accumulated_depreciation += depreciation_amount
term.accumulated_depreciation_amount = accumulated_depreciation
notes = _(
"This schedule was created when Asset {0} was updated after being split into new Asset {1}."
).format(
get_link_to_form(asset.doctype, asset.name), get_link_to_form(asset.doctype, new_asset_name)
)
new_asset_depr_schedule_doc.notes = notes
current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
current_asset_depr_schedule_doc.cancel()
new_asset_depr_schedule_doc.submit()
def create_new_asset_after_split(asset, split_qty):
@@ -1173,31 +920,49 @@ def create_new_asset_after_split(asset, split_qty):
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
new_asset.asset_quantity = split_qty
new_asset.split_from = asset.name
accumulated_depreciation = 0
for finance_book in new_asset.get("finance_books"):
finance_book.value_after_depreciation = flt(
(finance_book.value_after_depreciation * split_qty) / asset.asset_quantity
for row in new_asset.get("finance_books"):
row.value_after_depreciation = flt(
(row.value_after_depreciation * split_qty) / asset.asset_quantity
)
finance_book.expected_value_after_useful_life = flt(
(finance_book.expected_value_after_useful_life * split_qty) / asset.asset_quantity
row.expected_value_after_useful_life = flt(
(row.expected_value_after_useful_life * split_qty) / asset.asset_quantity
)
for term in new_asset.get("schedules"):
depreciation_amount = flt((term.depreciation_amount * split_qty) / asset.asset_quantity)
term.depreciation_amount = depreciation_amount
accumulated_depreciation += depreciation_amount
term.accumulated_depreciation_amount = accumulated_depreciation
new_asset.submit()
new_asset.set_status()
for term in new_asset.get("schedules"):
# Update references in JV
if term.journal_entry:
add_reference_in_jv_on_split(
term.journal_entry, new_asset.name, asset.name, term.depreciation_amount
)
for row in new_asset.get("finance_books"):
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
asset.name, "Active", row.finance_book
)
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
set_draft_asset_depr_schedule_details(new_asset_depr_schedule_doc, new_asset, row)
accumulated_depreciation = 0
for term in new_asset_depr_schedule_doc.get("depreciation_schedule"):
depreciation_amount = flt((term.depreciation_amount * split_qty) / asset.asset_quantity)
term.depreciation_amount = depreciation_amount
accumulated_depreciation += depreciation_amount
term.accumulated_depreciation_amount = accumulated_depreciation
notes = _("This schedule was created when new Asset {0} was split from Asset {1}.").format(
get_link_to_form(new_asset.doctype, new_asset.name), get_link_to_form(asset.doctype, asset.name)
)
new_asset_depr_schedule_doc.notes = notes
new_asset_depr_schedule_doc.submit()
for row in new_asset.get("finance_books"):
depr_schedule = get_depr_schedule(new_asset.name, "Active", row.finance_book)
for term in depr_schedule:
# Update references in JV
if term.journal_entry:
add_reference_in_jv_on_split(
term.journal_entry, new_asset.name, asset.name, term.depreciation_amount
)
return new_asset

View File

@@ -4,12 +4,18 @@
import frappe
from frappe import _
from frappe.utils import add_months, cint, flt, getdate, nowdate, today
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, nowdate, today
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
get_asset_depr_schedule_name,
get_temp_asset_depr_schedule_doc,
make_new_active_asset_depr_schedules_and_cancel_current_ones,
)
def post_depreciation_entries(date=None, commit=True):
@@ -21,8 +27,11 @@ def post_depreciation_entries(date=None, commit=True):
if not date:
date = today()
for asset in get_depreciable_assets(date):
make_depreciation_entry(asset, date)
for asset_name in get_depreciable_assets(date):
asset_doc = frappe.get_doc("Asset", asset_name)
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
if commit:
frappe.db.commit()
@@ -30,21 +39,35 @@ def post_depreciation_entries(date=None, commit=True):
def get_depreciable_assets(date):
return frappe.db.sql_list(
"""select distinct a.name
from tabAsset a, `tabDepreciation Schedule` ds
where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1
from tabAsset a, `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds
where a.name = ads.asset and ads.name = ds.parent and a.docstatus=1 and ads.docstatus=1
and a.status in ('Submitted', 'Partially Depreciated')
and a.calculate_depreciation = 1
and ds.schedule_date<=%s
and ifnull(ds.journal_entry, '')=''""",
date,
)
def make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date=None):
for row in asset_doc.get("finance_books"):
asset_depr_schedule_name = get_asset_depr_schedule_name(
asset_doc.name, "Active", row.finance_book
)
make_depreciation_entry(asset_depr_schedule_name, date)
@frappe.whitelist()
def make_depreciation_entry(asset_name, date=None):
def make_depreciation_entry(asset_depr_schedule_name, date=None):
frappe.has_permission("Journal Entry", throw=True)
if not date:
date = today()
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name)
asset_name = asset_depr_schedule_doc.asset
asset = frappe.get_doc("Asset", asset_name)
(
fixed_asset_account,
@@ -60,14 +83,14 @@ def make_depreciation_entry(asset_name, date=None):
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
for d in asset.get("schedules"):
for d in asset_depr_schedule_doc.get("depreciation_schedule"):
if not d.journal_entry and getdate(d.schedule_date) <= getdate(date):
je = frappe.new_doc("Journal Entry")
je.voucher_type = "Depreciation Entry"
je.naming_series = depreciation_series
je.posting_date = d.schedule_date
je.company = asset.company
je.finance_book = d.finance_book
je.finance_book = asset_depr_schedule_doc.finance_book
je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
credit_account, debit_account = get_credit_and_debit_accounts(
@@ -118,14 +141,14 @@ def make_depreciation_entry(asset_name, date=None):
d.db_set("journal_entry", je.name)
idx = cint(d.finance_book_id)
finance_books = asset.get("finance_books")[idx - 1]
finance_books.value_after_depreciation -= d.depreciation_amount
finance_books.db_update()
idx = cint(asset_depr_schedule_doc.finance_book_id)
row = asset.get("finance_books")[idx - 1]
row.value_after_depreciation -= d.depreciation_amount
row.db_update()
asset.set_status()
return asset
return asset_depr_schedule_doc
def get_depreciation_accounts(asset):
@@ -199,7 +222,11 @@ def scrap_asset(asset_name):
date = today()
depreciate_asset(asset, date)
notes = _("This schedule was created when Asset {0} was scrapped.").format(
get_link_to_form(asset.doctype, asset.name)
)
depreciate_asset(asset, date, notes)
asset.reload()
depreciation_series = frappe.get_cached_value(
@@ -232,10 +259,15 @@ def restore_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name)
reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date)
reset_depreciation_schedule(asset, asset.disposal_date)
je = asset.journal_entry_for_scrap
notes = _("This schedule was created when Asset {0} was restored.").format(
get_link_to_form(asset.doctype, asset.name)
)
reset_depreciation_schedule(asset, asset.disposal_date, notes)
asset.db_set("disposal_date", None)
asset.db_set("journal_entry_for_scrap", None)
@@ -244,22 +276,28 @@ def restore_asset(asset_name):
asset.set_status()
def depreciate_asset(asset, date):
asset.flags.ignore_validate_update_after_submit = True
asset.prepare_depreciation_data(date_of_disposal=date)
asset.save()
def depreciate_asset(asset_doc, date, notes):
asset_doc.flags.ignore_validate_update_after_submit = True
make_depreciation_entry(asset.name, date)
make_new_active_asset_depr_schedules_and_cancel_current_ones(
asset_doc, notes, date_of_disposal=date
)
asset_doc.save()
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
def reset_depreciation_schedule(asset, date):
asset.flags.ignore_validate_update_after_submit = True
def reset_depreciation_schedule(asset_doc, date, notes):
asset_doc.flags.ignore_validate_update_after_submit = True
# recreate original depreciation schedule of the asset
asset.prepare_depreciation_data(date_of_return=date)
make_new_active_asset_depr_schedules_and_cancel_current_ones(
asset_doc, notes, date_of_return=date
)
modify_depreciation_schedule_for_asset_repairs(asset)
asset.save()
modify_depreciation_schedule_for_asset_repairs(asset_doc)
asset_doc.save()
def modify_depreciation_schedule_for_asset_repairs(asset):
@@ -271,35 +309,36 @@ def modify_depreciation_schedule_for_asset_repairs(asset):
if repair.increase_in_asset_life:
asset_repair = frappe.get_doc("Asset Repair", repair.name)
asset_repair.modify_depreciation_schedule()
asset.prepare_depreciation_data()
notes = _("This schedule was created when Asset {0} went through Asset Repair {1}.").format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(asset_repair.doctype, asset_repair.name),
)
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes)
def reverse_depreciation_entry_made_after_disposal(asset, date):
row = -1
finance_book = asset.get("schedules")[0].get("finance_book")
for schedule in asset.get("schedules"):
if schedule.finance_book != finance_book:
row = 0
finance_book = schedule.finance_book
else:
row += 1
for row in asset.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
if schedule.schedule_date == date:
if not disposal_was_made_on_original_schedule_date(
asset, schedule, row, date
) or disposal_happens_in_the_future(date):
for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
if schedule.schedule_date == date:
if not disposal_was_made_on_original_schedule_date(
schedule_idx, row, date
) or disposal_happens_in_the_future(date):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate()
frappe.flags.is_reverse_depr_entry = True
reverse_journal_entry.submit()
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate()
frappe.flags.is_reverse_depr_entry = True
reverse_journal_entry.submit()
frappe.flags.is_reverse_depr_entry = False
asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None
depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry)
asset.finance_books[0].value_after_depreciation += depreciation_amount
asset.save()
frappe.flags.is_reverse_depr_entry = False
asset_depr_schedule_doc.flags.ignore_validate_update_after_submit = True
asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None
depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry)
row.value_after_depreciation += depreciation_amount
asset_depr_schedule_doc.save()
asset.save()
def get_depreciation_amount_in_je(journal_entry):
@@ -310,15 +349,14 @@ def get_depreciation_amount_in_je(journal_entry):
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
def disposal_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_disposal):
for finance_book in asset.get("finance_books"):
if schedule.finance_book == finance_book.finance_book:
orginal_schedule_date = add_months(
finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
)
def disposal_was_made_on_original_schedule_date(schedule_idx, row, posting_date_of_disposal):
orginal_schedule_date = add_months(
row.depreciation_start_date, schedule_idx * cint(row.frequency_of_depreciation)
)
if orginal_schedule_date == posting_date_of_disposal:
return True
if orginal_schedule_date == posting_date_of_disposal:
return True
return False
@@ -499,24 +537,27 @@ def get_disposal_account_and_cost_center(company):
def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None):
asset_doc = frappe.get_doc("Asset", asset)
if asset_doc.calculate_depreciation:
asset_doc.prepare_depreciation_data(getdate(disposal_date))
finance_book_id = 1
if finance_book:
for fb in asset_doc.finance_books:
if fb.finance_book == finance_book:
finance_book_id = fb.idx
break
asset_schedules = [
sch for sch in asset_doc.schedules if cint(sch.finance_book_id) == finance_book_id
]
accumulated_depr_amount = asset_schedules[-1].accumulated_depreciation_amount
return flt(
flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount,
asset_doc.precision("gross_purchase_amount"),
)
else:
if not asset_doc.calculate_depreciation:
return flt(asset_doc.value_after_depreciation)
idx = 1
if finance_book:
for d in asset.finance_books:
if d.finance_book == finance_book:
idx = d.idx
break
row = asset_doc.finance_books[idx - 1]
temp_asset_depreciation_schedule = get_temp_asset_depr_schedule_doc(
asset_doc, row, getdate(disposal_date)
)
accumulated_depr_amount = temp_asset_depreciation_schedule.get("depreciation_schedule")[
-1
].accumulated_depreciation_amount
return flt(
flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount,
asset_doc.precision("gross_purchase_amount"),
)

View File

@@ -27,6 +27,11 @@ from erpnext.assets.doctype.asset.depreciation import (
restore_asset,
scrap_asset,
)
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
clear_depr_schedule,
get_asset_depr_schedule_doc,
get_depr_schedule,
)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_invoice,
)
@@ -205,6 +210,9 @@ class TestAsset(AssetSetup):
submit=1,
)
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
post_depreciation_entries(date=add_months(purchase_date, 2))
asset.load_from_db()
@@ -216,6 +224,11 @@ class TestAsset(AssetSetup):
scrap_asset(asset.name)
asset.load_from_db()
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
accumulated_depr_amount = flt(
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
@@ -256,6 +269,11 @@ class TestAsset(AssetSetup):
self.assertSequenceEqual(gle, expected_gle)
restore_asset(asset.name)
second_asset_depr_schedule.load_from_db()
third_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(third_asset_depr_schedule.status, "Active")
self.assertEquals(second_asset_depr_schedule.status, "Cancelled")
asset.load_from_db()
self.assertFalse(asset.journal_entry_for_scrap)
@@ -283,6 +301,9 @@ class TestAsset(AssetSetup):
submit=1,
)
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
post_depreciation_entries(date=add_months(purchase_date, 2))
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
@@ -294,6 +315,12 @@ class TestAsset(AssetSetup):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
)
@@ -370,6 +397,9 @@ class TestAsset(AssetSetup):
submit=1,
)
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
post_depreciation_entries(date="2021-01-01")
self.assertEqual(asset.asset_quantity, 10)
@@ -378,21 +408,31 @@ class TestAsset(AssetSetup):
new_asset = split_asset(asset.name, 2)
asset.load_from_db()
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
first_asset_depr_schedule_of_new_asset = get_asset_depr_schedule_doc(new_asset.name, "Active")
self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule_of_new_asset.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
depr_schedule_of_asset = second_asset_depr_schedule.get("depreciation_schedule")
depr_schedule_of_new_asset = first_asset_depr_schedule_of_new_asset.get("depreciation_schedule")
self.assertEqual(new_asset.asset_quantity, 2)
self.assertEqual(new_asset.gross_purchase_amount, 24000)
self.assertEqual(new_asset.opening_accumulated_depreciation, 4000)
self.assertEqual(new_asset.split_from, asset.name)
self.assertEqual(new_asset.schedules[0].depreciation_amount, 4000)
self.assertEqual(new_asset.schedules[1].depreciation_amount, 4000)
self.assertEqual(depr_schedule_of_new_asset[0].depreciation_amount, 4000)
self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 4000)
self.assertEqual(asset.asset_quantity, 8)
self.assertEqual(asset.gross_purchase_amount, 96000)
self.assertEqual(asset.opening_accumulated_depreciation, 16000)
self.assertEqual(asset.schedules[0].depreciation_amount, 16000)
self.assertEqual(asset.schedules[1].depreciation_amount, 16000)
self.assertEqual(depr_schedule_of_asset[0].depreciation_amount, 16000)
self.assertEqual(depr_schedule_of_asset[1].depreciation_amount, 16000)
journal_entry = asset.schedules[0].journal_entry
journal_entry = depr_schedule_of_asset[0].journal_entry
jv = frappe.get_doc("Journal Entry", journal_entry)
self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000)
@@ -629,7 +669,7 @@ class TestDepreciationMethods(AssetSetup):
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in asset.get("schedules")
for d in get_depr_schedule(asset.name, "Draft")
]
self.assertEqual(schedules, expected_schedules)
@@ -651,7 +691,7 @@ class TestDepreciationMethods(AssetSetup):
expected_schedules = [["2032-12-31", 30000.0, 77095.89], ["2033-06-06", 12904.11, 90000.0]]
schedules = [
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
for d in asset.get("schedules")
for d in get_depr_schedule(asset.name, "Draft")
]
self.assertEqual(schedules, expected_schedules)
@@ -678,7 +718,7 @@ class TestDepreciationMethods(AssetSetup):
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in asset.get("schedules")
for d in get_depr_schedule(asset.name, "Draft")
]
self.assertEqual(schedules, expected_schedules)
@@ -703,7 +743,7 @@ class TestDepreciationMethods(AssetSetup):
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in asset.get("schedules")
for d in get_depr_schedule(asset.name, "Draft")
]
self.assertEqual(schedules, expected_schedules)
@@ -733,7 +773,7 @@ class TestDepreciationMethods(AssetSetup):
flt(d.depreciation_amount, 2),
flt(d.accumulated_depreciation_amount, 2),
]
for d in asset.get("schedules")
for d in get_depr_schedule(asset.name, "Draft")
]
self.assertEqual(schedules, expected_schedules)
@@ -765,7 +805,7 @@ class TestDepreciationMethods(AssetSetup):
flt(d.depreciation_amount, 2),
flt(d.accumulated_depreciation_amount, 2),
]
for d in asset.get("schedules")
for d in get_depr_schedule(asset.name, "Draft")
]
self.assertEqual(schedules, expected_schedules)
@@ -798,7 +838,7 @@ class TestDepreciationMethods(AssetSetup):
flt(d.depreciation_amount, 2),
flt(d.accumulated_depreciation_amount, 2),
]
for d in asset.get("schedules")
for d in get_depr_schedule(asset.name, "Draft")
]
self.assertEqual(schedules, expected_schedules)
@@ -831,7 +871,7 @@ class TestDepreciationMethods(AssetSetup):
flt(d.depreciation_amount, 2),
flt(d.accumulated_depreciation_amount, 2),
]
for d in asset.get("schedules")
for d in get_depr_schedule(asset.name, "Draft")
]
self.assertEqual(schedules, expected_schedules)
@@ -854,7 +894,7 @@ class TestDepreciationBasics(AssetSetup):
["2022-12-31", 30000, 90000],
]
for i, schedule in enumerate(asset.schedules):
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
@@ -877,7 +917,7 @@ class TestDepreciationBasics(AssetSetup):
["2023-01-01", 15000, 90000],
]
for i, schedule in enumerate(asset.schedules):
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
@@ -885,7 +925,9 @@ class TestDepreciationBasics(AssetSetup):
def test_get_depreciation_amount(self):
"""Tests if get_depreciation_amount() returns the right value."""
from erpnext.assets.doctype.asset.asset import get_depreciation_amount
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depreciation_amount,
)
asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31")
@@ -904,8 +946,8 @@ class TestDepreciationBasics(AssetSetup):
depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0])
self.assertEqual(depreciation_amount, 30000)
def test_make_depreciation_schedule(self):
"""Tests if make_depreciation_schedule() returns the right values."""
def test_make_depr_schedule(self):
"""Tests if make_depr_schedule() returns the right values."""
asset = create_asset(
item_code="Macbook Pro",
@@ -920,7 +962,7 @@ class TestDepreciationBasics(AssetSetup):
expected_values = [["2020-12-31", 30000.0], ["2021-12-31", 30000.0], ["2022-12-31", 30000.0]]
for i, schedule in enumerate(asset.schedules):
for i, schedule in enumerate(get_depr_schedule(asset.name, "Draft")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
@@ -940,7 +982,7 @@ class TestDepreciationBasics(AssetSetup):
expected_values = [30000.0, 60000.0, 90000.0]
for i, schedule in enumerate(asset.schedules):
for i, schedule in enumerate(get_depr_schedule(asset.name, "Draft")):
self.assertEqual(expected_values[i], schedule.accumulated_depreciation_amount)
def test_check_is_pro_rata(self):
@@ -1120,9 +1162,11 @@ class TestDepreciationBasics(AssetSetup):
post_depreciation_entries(date="2021-06-01")
asset.load_from_db()
self.assertTrue(asset.schedules[0].journal_entry)
self.assertFalse(asset.schedules[1].journal_entry)
self.assertFalse(asset.schedules[2].journal_entry)
depr_schedule = get_depr_schedule(asset.name, "Active")
self.assertTrue(depr_schedule[0].journal_entry)
self.assertFalse(depr_schedule[1].journal_entry)
self.assertFalse(depr_schedule[2].journal_entry)
def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self):
"""Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account."""
@@ -1141,7 +1185,7 @@ class TestDepreciationBasics(AssetSetup):
post_depreciation_entries(date="2021-06-01")
asset.load_from_db()
je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry)
je = frappe.get_doc("Journal Entry", get_depr_schedule(asset.name, "Active")[0].journal_entry)
accounting_entries = [
{"account": entry.account, "debit": entry.debit, "credit": entry.credit}
for entry in je.accounts
@@ -1177,7 +1221,7 @@ class TestDepreciationBasics(AssetSetup):
post_depreciation_entries(date="2021-06-01")
asset.load_from_db()
je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry)
je = frappe.get_doc("Journal Entry", get_depr_schedule(asset.name, "Active")[0].journal_entry)
accounting_entries = [
{"account": entry.account, "debit": entry.debit, "credit": entry.credit}
for entry in je.accounts
@@ -1196,8 +1240,8 @@ class TestDepreciationBasics(AssetSetup):
depr_expense_account.parent_account = "Expenses - _TC"
depr_expense_account.save()
def test_clear_depreciation_schedule(self):
"""Tests if clear_depreciation_schedule() works as expected."""
def test_clear_depr_schedule(self):
"""Tests if clear_depr_schedule() works as expected."""
asset = create_asset(
item_code="Macbook Pro",
@@ -1213,17 +1257,20 @@ class TestDepreciationBasics(AssetSetup):
post_depreciation_entries(date="2021-06-01")
asset.load_from_db()
asset.clear_depreciation_schedule()
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEqual(len(asset.schedules), 1)
clear_depr_schedule(asset_depr_schedule_doc)
def test_clear_depreciation_schedule_for_multiple_finance_books(self):
self.assertEqual(len(asset_depr_schedule_doc.get("depreciation_schedule")), 1)
def test_clear_depr_schedule_for_multiple_finance_books(self):
asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31", do_not_save=1)
asset.calculate_depreciation = 1
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 1",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 3,
@@ -1234,6 +1281,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 2",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 6,
@@ -1244,6 +1292,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 3",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
@@ -1256,15 +1305,23 @@ class TestDepreciationBasics(AssetSetup):
post_depreciation_entries(date="2020-04-01")
asset.load_from_db()
asset.clear_depreciation_schedule()
asset_depr_schedule_doc_1 = get_asset_depr_schedule_doc(
asset.name, "Active", "Test Finance Book 1"
)
clear_depr_schedule(asset_depr_schedule_doc_1)
self.assertEqual(len(asset_depr_schedule_doc_1.get("depreciation_schedule")), 3)
self.assertEqual(len(asset.schedules), 6)
asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc(
asset.name, "Active", "Test Finance Book 2"
)
clear_depr_schedule(asset_depr_schedule_doc_2)
self.assertEqual(len(asset_depr_schedule_doc_2.get("depreciation_schedule")), 3)
for schedule in asset.schedules:
if schedule.idx <= 3:
self.assertEqual(schedule.finance_book_id, "1")
else:
self.assertEqual(schedule.finance_book_id, "2")
asset_depr_schedule_doc_3 = get_asset_depr_schedule_doc(
asset.name, "Active", "Test Finance Book 3"
)
clear_depr_schedule(asset_depr_schedule_doc_3)
self.assertEqual(len(asset_depr_schedule_doc_3.get("depreciation_schedule")), 0)
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31", do_not_save=1)
@@ -1273,6 +1330,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 1",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
@@ -1283,6 +1341,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append(
"finance_books",
{
"finance_book": "Test Finance Book 2",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 6,
@@ -1292,13 +1351,15 @@ class TestDepreciationBasics(AssetSetup):
)
asset.save()
self.assertEqual(len(asset.schedules), 9)
asset_depr_schedule_doc_1 = get_asset_depr_schedule_doc(
asset.name, "Draft", "Test Finance Book 1"
)
self.assertEqual(len(asset_depr_schedule_doc_1.get("depreciation_schedule")), 3)
for schedule in asset.schedules:
if schedule.idx <= 3:
self.assertEqual(schedule.finance_book_id, 1)
else:
self.assertEqual(schedule.finance_book_id, 2)
asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc(
asset.name, "Draft", "Test Finance Book 2"
)
self.assertEqual(len(asset_depr_schedule_doc_2.get("depreciation_schedule")), 6)
def test_depreciation_entry_cancellation(self):
asset = create_asset(
@@ -1318,12 +1379,12 @@ class TestDepreciationBasics(AssetSetup):
asset.load_from_db()
# cancel depreciation entry
depr_entry = asset.get("schedules")[0].journal_entry
depr_entry = get_depr_schedule(asset.name, "Active")[0].journal_entry
self.assertTrue(depr_entry)
frappe.get_doc("Journal Entry", depr_entry).cancel()
asset.load_from_db()
depr_entry = asset.get("schedules")[0].journal_entry
depr_entry = get_depr_schedule(asset.name, "Active")[0].journal_entry
self.assertFalse(depr_entry)
def test_asset_expected_value_after_useful_life(self):
@@ -1338,7 +1399,7 @@ class TestDepreciationBasics(AssetSetup):
)
accumulated_depreciation_after_full_schedule = max(
d.accumulated_depreciation_amount for d in asset.get("schedules")
d.accumulated_depreciation_amount for d in get_depr_schedule(asset.name, "Draft")
)
asset_value_after_full_schedule = flt(asset.gross_purchase_amount) - flt(
@@ -1369,7 +1430,7 @@ class TestDepreciationBasics(AssetSetup):
asset.load_from_db()
# check depreciation entry series
self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR")
self.assertEqual(get_depr_schedule(asset.name, "Active")[0].journal_entry[:4], "DEPR")
expected_gle = (
("_Test Accumulated Depreciations - _TC", 0.0, 30000.0),
@@ -1439,7 +1500,7 @@ class TestDepreciationBasics(AssetSetup):
"2020-07-15",
]
for i, schedule in enumerate(asset.schedules):
for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date))
@@ -1453,6 +1514,15 @@ def create_asset_data():
if not frappe.db.exists("Location", "Test Location"):
frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert()
if not frappe.db.exists("Finance Book", "Test Finance Book 1"):
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 1"}).insert()
if not frappe.db.exists("Finance Book", "Test Finance Book 2"):
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 2"}).insert()
if not frappe.db.exists("Finance Book", "Test Finance Book 3"):
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 3"}).insert()
def create_asset(**args):
args = frappe._dict(args)

View File

@@ -7,7 +7,7 @@ import frappe
# import erpnext
from frappe import _
from frappe.utils import cint, flt
from frappe.utils import cint, flt, get_link_to_form
from six import string_types
import erpnext
@@ -19,6 +19,9 @@ from erpnext.assets.doctype.asset.depreciation import (
reverse_depreciation_entry_made_after_disposal,
)
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
make_new_active_asset_depr_schedules_and_cancel_current_ones,
)
from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import (
get_current_asset_value,
)
@@ -427,7 +430,12 @@ class AssetCapitalization(StockController):
asset = self.get_asset(item)
if asset.calculate_depreciation:
depreciate_asset(asset, self.posting_date)
notes = _(
"This schedule was created when Asset {0} was consumed when Asset Capitalization {1} was submitted."
).format(
get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.get("name"))
)
depreciate_asset(asset, self.posting_date, notes)
asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
@@ -513,7 +521,12 @@ class AssetCapitalization(StockController):
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.prepare_depreciation_data()
notes = _(
"This schedule was created when target Asset {0} was updated when Asset Capitalization {1} was submitted."
).format(
get_link_to_form(asset_doc.doctype, asset_doc.name), get_link_to_form(self.doctype, self.name)
)
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset_doc, notes)
asset_doc.flags.ignore_validate_update_after_submit = True
asset_doc.save()
elif self.docstatus == 2:
@@ -524,7 +537,12 @@ class AssetCapitalization(StockController):
if asset.calculate_depreciation:
reverse_depreciation_entry_made_after_disposal(asset, self.posting_date)
reset_depreciation_schedule(asset, self.posting_date)
notes = _(
"This schedule was created when Asset {0} was restored when Asset Capitalization {1} was cancelled."
).format(
get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name)
)
reset_depreciation_schedule(asset, self.posting_date, notes)
def get_asset(self, item):
asset = frappe.get_doc("Asset", item.asset)

View File

@@ -12,6 +12,9 @@ from erpnext.assets.doctype.asset.test_asset import (
create_asset_data,
set_depreciation_settings_in_company,
)
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
)
from erpnext.stock.doctype.item.test_item import create_item
@@ -253,6 +256,9 @@ class TestAssetCapitalization(unittest.TestCase):
submit=1,
)
first_asset_depr_schedule = get_asset_depr_schedule_doc(consumed_asset.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
# Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization(
entry_type="Decapitalization",
@@ -282,8 +288,18 @@ class TestAssetCapitalization(unittest.TestCase):
consumed_asset.reload()
self.assertEqual(consumed_asset.status, "Decapitalized")
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(consumed_asset.name, "Active")
self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
depr_schedule_of_consumed_asset = second_asset_depr_schedule.get("depreciation_schedule")
consumed_depreciation_schedule = [
d for d in consumed_asset.schedules if getdate(d.schedule_date) == getdate(capitalization_date)
d
for d in depr_schedule_of_consumed_asset
if getdate(d.schedule_date) == getdate(capitalization_date)
]
self.assertTrue(
consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry

View File

@@ -0,0 +1,51 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.provide("erpnext.asset");
frappe.ui.form.on('Asset Depreciation Schedule', {
onload: function(frm) {
frm.events.make_schedules_editable(frm);
},
make_schedules_editable: function(frm) {
var is_editable = frm.doc.depreciation_method == "Manual" ? true : false;
frm.toggle_enable("depreciation_schedule", is_editable);
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_editable);
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_editable);
}
});
frappe.ui.form.on('Depreciation Schedule', {
make_depreciation_entry: function(frm, cdt, cdn) {
var row = locals[cdt][cdn];
if (!row.journal_entry) {
frappe.call({
method: "erpnext.assets.doctype.asset.depreciation.make_depreciation_entry",
args: {
"asset_depr_schedule_name": frm.doc.name,
"date": row.schedule_date
},
callback: function(r) {
frappe.model.sync(r.message);
frm.refresh();
}
})
}
},
depreciation_amount: function(frm, cdt, cdn) {
erpnext.asset.set_accumulated_depreciation(frm);
}
});
erpnext.asset.set_accumulated_depreciation = function(frm) {
if(frm.doc.depreciation_method != "Manual") return;
var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation);
$.each(frm.doc.schedules || [], function(i, row) {
accumulated_depreciation += flt(row.depreciation_amount);
frappe.model.set_value(row.doctype, row.name,
"accumulated_depreciation_amount", accumulated_depreciation);
})
};

View File

@@ -0,0 +1,202 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "naming_series:",
"creation": "2022-10-31 15:03:35.424877",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"asset",
"naming_series",
"column_break_2",
"opening_accumulated_depreciation",
"finance_book",
"finance_book_id",
"depreciation_details_section",
"depreciation_method",
"total_number_of_depreciations",
"rate_of_depreciation",
"column_break_8",
"frequency_of_depreciation",
"expected_value_after_useful_life",
"depreciation_schedule_section",
"depreciation_schedule",
"details_section",
"notes",
"status",
"amended_from"
],
"fields": [
{
"fieldname": "asset",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Asset",
"options": "Asset",
"reqd": 1
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "ACC-ADS-.YYYY.-"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Asset Depreciation Schedule",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "depreciation_details_section",
"fieldtype": "Section Break",
"label": "Depreciation Details"
},
{
"fieldname": "finance_book",
"fieldtype": "Link",
"label": "Finance Book",
"options": "Finance Book"
},
{
"fieldname": "depreciation_method",
"fieldtype": "Select",
"label": "Depreciation Method",
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
"read_only": 1
},
{
"depends_on": "eval:doc.depreciation_method == 'Written Down Value'",
"description": "In Percentage",
"fieldname": "rate_of_depreciation",
"fieldtype": "Percent",
"label": "Rate of Depreciation",
"read_only": 1
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"depends_on": "total_number_of_depreciations",
"fieldname": "total_number_of_depreciations",
"fieldtype": "Int",
"label": "Total Number of Depreciations",
"read_only": 1
},
{
"fieldname": "depreciation_schedule_section",
"fieldtype": "Section Break",
"label": "Depreciation Schedule"
},
{
"fieldname": "depreciation_schedule",
"fieldtype": "Table",
"label": "Depreciation Schedule",
"options": "Depreciation Schedule"
},
{
"collapsible": 1,
"collapsible_depends_on": "notes",
"fieldname": "details_section",
"fieldtype": "Section Break",
"label": "Details"
},
{
"fieldname": "notes",
"fieldtype": "Small Text",
"label": "Notes",
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"label": "Status",
"options": "Draft\nActive\nCancelled",
"read_only": 1
},
{
"depends_on": "frequency_of_depreciation",
"fieldname": "frequency_of_depreciation",
"fieldtype": "Int",
"label": "Frequency of Depreciation (Months)",
"read_only": 1
},
{
"fieldname": "expected_value_after_useful_life",
"fieldtype": "Currency",
"label": "Expected Value After Useful Life",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "finance_book_id",
"fieldtype": "Int",
"hidden": 1,
"label": "Finance Book Id",
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "opening_accumulated_depreciation",
"fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency",
"label": "Opening Accumulated Depreciation",
"options": "Company:company:default_currency",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-01-02 15:38:30.766779",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Depreciation Schedule",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Quality Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,516 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import (
add_days,
add_months,
cint,
date_diff,
flt,
get_last_day,
is_last_day_of_the_month,
)
import erpnext
class AssetDepreciationSchedule(Document):
def before_save(self):
if not self.finance_book_id:
self.prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name(
self.asset, self.finance_book
)
def validate(self):
self.validate_another_asset_depr_schedule_does_not_exist()
def validate_another_asset_depr_schedule_does_not_exist(self):
finance_book_filter = ["finance_book", "is", "not set"]
if self.finance_book:
finance_book_filter = ["finance_book", "=", self.finance_book]
asset_depr_schedule = frappe.db.exists(
"Asset Depreciation Schedule",
[
["asset", "=", self.asset],
finance_book_filter,
["docstatus", "<", 2],
],
)
if asset_depr_schedule and asset_depr_schedule != self.name:
if self.finance_book:
frappe.throw(
_(
"Asset Depreciation Schedule {0} for Asset {1} and Finance Book {2} already exists."
).format(asset_depr_schedule, self.asset, self.finance_book)
)
else:
frappe.throw(
_("Asset Depreciation Schedule {0} for Asset {1} already exists.").format(
asset_depr_schedule, self.asset
)
)
def on_submit(self):
self.db_set("status", "Active")
def before_cancel(self):
if not self.flags.should_not_cancel_depreciation_entries:
self.cancel_depreciation_entries()
def cancel_depreciation_entries(self):
for d in self.get("depreciation_schedule"):
if d.journal_entry:
frappe.get_doc("Journal Entry", d.journal_entry).cancel()
def on_cancel(self):
self.db_set("status", "Cancelled")
def prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name(self, asset_name, fb_name):
asset_doc = frappe.get_doc("Asset", asset_name)
finance_book_filter = ["finance_book", "is", "not set"]
if fb_name:
finance_book_filter = ["finance_book", "=", fb_name]
asset_finance_book_name = frappe.db.get_value(
doctype="Asset Finance Book",
filters=[["parent", "=", asset_name], finance_book_filter],
)
asset_finance_book_doc = frappe.get_doc("Asset Finance Book", asset_finance_book_name)
prepare_draft_asset_depr_schedule_data(self, asset_doc, asset_finance_book_doc)
def make_draft_asset_depr_schedules_if_not_present(asset_doc):
for row in asset_doc.get("finance_books"):
draft_asset_depr_schedule_name = get_asset_depr_schedule_name(
asset_doc.name, "Draft", row.finance_book
)
active_asset_depr_schedule_name = get_asset_depr_schedule_name(
asset_doc.name, "Active", row.finance_book
)
if not draft_asset_depr_schedule_name and not active_asset_depr_schedule_name:
make_draft_asset_depr_schedule(asset_doc, row)
def make_draft_asset_depr_schedules(asset_doc):
for row in asset_doc.get("finance_books"):
make_draft_asset_depr_schedule(asset_doc, row)
def make_draft_asset_depr_schedule(asset_doc, row):
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
prepare_draft_asset_depr_schedule_data(asset_depr_schedule_doc, asset_doc, row)
asset_depr_schedule_doc.insert()
def update_draft_asset_depr_schedules(asset_doc):
for row in asset_doc.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book)
if not asset_depr_schedule_doc:
continue
prepare_draft_asset_depr_schedule_data(asset_depr_schedule_doc, asset_doc, row)
asset_depr_schedule_doc.save()
def prepare_draft_asset_depr_schedule_data(
asset_depr_schedule_doc,
asset_doc,
row,
date_of_disposal=None,
date_of_return=None,
update_asset_finance_book_row=True,
):
set_draft_asset_depr_schedule_details(asset_depr_schedule_doc, asset_doc, row)
make_depr_schedule(
asset_depr_schedule_doc, asset_doc, row, date_of_disposal, update_asset_finance_book_row
)
set_accumulated_depreciation(asset_depr_schedule_doc, row, date_of_disposal, date_of_return)
def set_draft_asset_depr_schedule_details(asset_depr_schedule_doc, asset_doc, row):
asset_depr_schedule_doc.asset = asset_doc.name
asset_depr_schedule_doc.finance_book = row.finance_book
asset_depr_schedule_doc.finance_book_id = row.idx
asset_depr_schedule_doc.opening_accumulated_depreciation = (
asset_doc.opening_accumulated_depreciation
)
asset_depr_schedule_doc.depreciation_method = row.depreciation_method
asset_depr_schedule_doc.total_number_of_depreciations = row.total_number_of_depreciations
asset_depr_schedule_doc.frequency_of_depreciation = row.frequency_of_depreciation
asset_depr_schedule_doc.rate_of_depreciation = row.rate_of_depreciation
asset_depr_schedule_doc.expected_value_after_useful_life = row.expected_value_after_useful_life
asset_depr_schedule_doc.status = "Draft"
def convert_draft_asset_depr_schedules_into_active(asset_doc):
for row in asset_doc.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book)
if not asset_depr_schedule_doc:
continue
asset_depr_schedule_doc.submit()
def cancel_asset_depr_schedules(asset_doc):
for row in asset_doc.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Active", row.finance_book)
if not asset_depr_schedule_doc:
continue
asset_depr_schedule_doc.cancel()
def make_new_active_asset_depr_schedules_and_cancel_current_ones(
asset_doc, notes, date_of_disposal=None, date_of_return=None
):
for row in asset_doc.get("finance_books"):
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
asset_doc.name, "Active", row.finance_book
)
if not current_asset_depr_schedule_doc:
frappe.throw(
_("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
asset_doc.name, row.finance_book
)
)
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
make_depr_schedule(new_asset_depr_schedule_doc, asset_doc, row, date_of_disposal)
set_accumulated_depreciation(new_asset_depr_schedule_doc, row, date_of_disposal, date_of_return)
new_asset_depr_schedule_doc.notes = notes
current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
current_asset_depr_schedule_doc.cancel()
new_asset_depr_schedule_doc.submit()
def get_temp_asset_depr_schedule_doc(
asset_doc, row, date_of_disposal=None, date_of_return=None, update_asset_finance_book_row=False
):
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
prepare_draft_asset_depr_schedule_data(
asset_depr_schedule_doc,
asset_doc,
row,
date_of_disposal,
date_of_return,
update_asset_finance_book_row,
)
return asset_depr_schedule_doc
def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
finance_book_filter = ["finance_book", "is", "not set"]
if finance_book:
finance_book_filter = ["finance_book", "=", finance_book]
return frappe.db.get_value(
doctype="Asset Depreciation Schedule",
filters=[
["asset", "=", asset_name],
finance_book_filter,
["status", "=", status],
],
)
@frappe.whitelist()
def get_depr_schedule(asset_name, status, finance_book=None):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book)
if not asset_depr_schedule_doc:
return
return asset_depr_schedule_doc.get("depreciation_schedule")
def get_asset_depr_schedule_doc(asset_name, status, finance_book=None):
asset_depr_schedule_name = get_asset_depr_schedule_name(asset_name, status, finance_book)
if not asset_depr_schedule_name:
return
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name)
return asset_depr_schedule_doc
def make_depr_schedule(
asset_depr_schedule_doc, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True
):
if row.depreciation_method != "Manual" and not asset_depr_schedule_doc.get(
"depreciation_schedule"
):
asset_depr_schedule_doc.depreciation_schedule = []
if not asset_doc.available_for_use_date:
return
start = clear_depr_schedule(asset_depr_schedule_doc)
_make_depr_schedule(
asset_depr_schedule_doc, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row
)
def clear_depr_schedule(asset_depr_schedule_doc):
start = 0
num_of_depreciations_completed = 0
depr_schedule = []
for schedule in asset_depr_schedule_doc.get("depreciation_schedule"):
if schedule.journal_entry:
num_of_depreciations_completed += 1
depr_schedule.append(schedule)
else:
start = num_of_depreciations_completed
break
asset_depr_schedule_doc.depreciation_schedule = depr_schedule
return start
def _make_depr_schedule(
asset_depr_schedule_doc, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row
):
asset_doc.validate_asset_finance_books(row)
value_after_depreciation = asset_doc._get_value_after_depreciation(row)
row.value_after_depreciation = value_after_depreciation
if update_asset_finance_book_row:
row.db_update()
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
asset_doc.number_of_depreciations_booked
)
has_pro_rata = asset_doc.check_is_pro_rata(row)
if has_pro_rata:
number_of_pending_depreciations += 1
skip_row = False
should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date)
for n in range(start, number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row:
continue
depreciation_amount = get_depreciation_amount(asset_doc, value_after_depreciation, row)
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
schedule_date = add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation))
if should_get_last_day:
schedule_date = get_last_day(schedule_date)
# schedule date will be a year later from start date
# so monthly schedule date is calculated by removing 11 months from it
monthly_schedule_date = add_months(schedule_date, -row.frequency_of_depreciation + 1)
# if asset is being sold or scrapped
if date_of_disposal:
from_date = asset_doc.available_for_use_date
if asset_depr_schedule_doc.depreciation_schedule:
from_date = asset_depr_schedule_doc.depreciation_schedule[-1].schedule_date
depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
row, depreciation_amount, from_date, date_of_disposal
)
if depreciation_amount > 0:
add_depr_schedule_row(
asset_depr_schedule_doc,
date_of_disposal,
depreciation_amount,
row.depreciation_method,
)
break
# For first row
if has_pro_rata and not asset_doc.opening_accumulated_depreciation and n == 0:
from_date = add_days(
asset_doc.available_for_use_date, -1
) # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
row, depreciation_amount, from_date, row.depreciation_start_date
)
# For first depr schedule date will be the start date
# so monthly schedule date is calculated by removing
# month difference between use date and start date
monthly_schedule_date = add_months(row.depreciation_start_date, -months + 1)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
if not asset_doc.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
asset_doc.to_date = add_months(
asset_doc.available_for_use_date,
(n + asset_doc.number_of_depreciations_booked) * cint(row.frequency_of_depreciation),
)
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
row, depreciation_amount, schedule_date, asset_doc.to_date
)
depreciation_amount = get_adjusted_depreciation_amount(
asset_depr_schedule_doc, depreciation_amount_without_pro_rata, depreciation_amount
)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days)
last_schedule_date = schedule_date
if not depreciation_amount:
continue
value_after_depreciation -= flt(
depreciation_amount, asset_doc.precision("gross_purchase_amount")
)
# Adjust depreciation amount in the last period based on the expected value after useful life
if row.expected_value_after_useful_life and (
(
n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != row.expected_value_after_useful_life
)
or value_after_depreciation < row.expected_value_after_useful_life
):
depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life
skip_row = True
if depreciation_amount > 0:
add_depr_schedule_row(
asset_depr_schedule_doc,
schedule_date,
depreciation_amount,
row.depreciation_method,
)
# to ensure that final accumulated depreciation amount is accurate
def get_adjusted_depreciation_amount(
asset_depr_schedule_doc, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row
):
if not asset_depr_schedule_doc.opening_accumulated_depreciation:
depreciation_amount_for_first_row = get_depreciation_amount_for_first_row(
asset_depr_schedule_doc
)
if (
depreciation_amount_for_first_row + depreciation_amount_for_last_row
!= depreciation_amount_without_pro_rata
):
depreciation_amount_for_last_row = (
depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
)
return depreciation_amount_for_last_row
def get_depreciation_amount_for_first_row(asset_depr_schedule_doc):
return asset_depr_schedule_doc.get("depreciation_schedule")[0].depreciation_amount
@erpnext.allow_regional
def get_depreciation_amount(asset_doc, depreciable_value, row):
if row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time
if not asset_doc.flags.increase_in_asset_life:
depreciation_amount = (
flt(asset_doc.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair
else:
depreciation_amount = (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
) / (date_diff(asset_doc.to_date, asset_doc.available_for_use_date) / 365)
else:
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
return depreciation_amount
def add_depr_schedule_row(
asset_depr_schedule_doc,
schedule_date,
depreciation_amount,
depreciation_method,
):
asset_depr_schedule_doc.append(
"depreciation_schedule",
{
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": depreciation_method,
},
)
def set_accumulated_depreciation(
asset_depr_schedule_doc,
row,
date_of_disposal=None,
date_of_return=None,
ignore_booked_entry=False,
):
straight_line_idx = [
d.idx
for d in asset_depr_schedule_doc.get("depreciation_schedule")
if d.depreciation_method == "Straight Line"
]
accumulated_depreciation = flt(asset_depr_schedule_doc.opening_accumulated_depreciation)
value_after_depreciation = flt(row.value_after_depreciation)
for i, d in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
if ignore_booked_entry and d.journal_entry:
continue
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
value_after_depreciation -= flt(depreciation_amount)
# for the last row, if depreciation method = Straight Line
if (
straight_line_idx
and i == max(straight_line_idx) - 1
and not date_of_disposal
and not date_of_return
):
depreciation_amount += flt(
value_after_depreciation - flt(row.expected_value_after_useful_life),
d.precision("depreciation_amount"),
)
d.depreciation_amount = depreciation_amount
accumulated_depreciation += d.depreciation_amount
d.accumulated_depreciation_amount = flt(
accumulated_depreciation, d.precision("accumulated_depreciation_amount")
)

View File

@@ -0,0 +1,27 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
)
class TestAssetDepreciationSchedule(FrappeTestCase):
def setUp(self):
create_asset_data()
def test_throw_error_if_another_asset_depr_schedule_exist(self):
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1)
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
second_asset_depr_schedule = frappe.get_doc(
{"doctype": "Asset Depreciation Schedule", "asset": asset.name, "finance_book": None}
)
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)

View File

@@ -3,11 +3,15 @@
import frappe
from frappe import _
from frappe.utils import add_months, cint, flt, getdate, time_diff_in_hours
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
import erpnext
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
make_new_active_asset_depr_schedules_and_cancel_current_ones,
)
from erpnext.controllers.accounts_controller import AccountsController
@@ -52,8 +56,11 @@ class AssetRepair(AccountsController):
):
self.modify_depreciation_schedule()
notes = _("This schedule was created when Asset Repair {0} was submitted.").format(
get_link_to_form(self.doctype, self.name)
)
self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.prepare_depreciation_data()
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
self.asset_doc.save()
def before_cancel(self):
@@ -73,8 +80,11 @@ class AssetRepair(AccountsController):
):
self.revert_depreciation_schedule_on_cancellation()
notes = _("This schedule was created when Asset Repair {0} was cancelled.").format(
get_link_to_form(self.doctype, self.name)
)
self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.prepare_depreciation_data()
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
self.asset_doc.save()
def check_repair_status(self):
@@ -279,8 +289,10 @@ class AssetRepair(AccountsController):
asset.number_of_depreciations_booked
)
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
# the Schedule Date in the final row of the old Depreciation Schedule
last_schedule_date = asset.schedules[len(asset.schedules) - 1].schedule_date
last_schedule_date = depr_schedule[len(depr_schedule) - 1].schedule_date
# the Schedule Date in the final row of the new Depreciation Schedule
asset.to_date = add_months(last_schedule_date, extra_months)
@@ -310,8 +322,10 @@ class AssetRepair(AccountsController):
asset.number_of_depreciations_booked
)
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
# the Schedule Date in the final row of the modified Depreciation Schedule
last_schedule_date = asset.schedules[len(asset.schedules) - 1].schedule_date
last_schedule_date = depr_schedule[len(depr_schedule) - 1].schedule_date
# the Schedule Date in the final row of the original Depreciation Schedule
asset.to_date = add_months(last_schedule_date, -extra_months)

View File

@@ -12,6 +12,9 @@ from erpnext.assets.doctype.asset.test_asset import (
create_asset_data,
set_depreciation_settings_in_company,
)
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
)
from erpnext.stock.doctype.item.test_item import create_item
@@ -232,13 +235,23 @@ class TestAssetRepair(unittest.TestCase):
def test_increase_in_asset_life(self):
asset = create_asset(calculate_depreciation=1, submit=1)
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
initial_num_of_depreciations = num_of_depreciations(asset)
create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1)
asset.reload()
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset))
self.assertEqual(
asset.schedules[-1].accumulated_depreciation_amount,
second_asset_depr_schedule.get("depreciation_schedule")[-1].accumulated_depreciation_amount,
asset.finance_books[0].value_after_depreciation,
)

View File

@@ -5,13 +5,17 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, date_diff, flt, formatdate, getdate
from frappe.utils import date_diff, flt, formatdate, get_link_to_form, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.assets.doctype.asset.asset import get_depreciation_amount
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
get_depreciation_amount,
set_accumulated_depreciation,
)
class AssetValueAdjustment(Document):
@@ -112,21 +116,40 @@ class AssetValueAdjustment(Document):
for d in asset.finance_books:
d.value_after_depreciation = asset_value
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
asset.name, "Active", d.finance_book
)
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
new_asset_depr_schedule_doc.status = "Draft"
new_asset_depr_schedule_doc.docstatus = 0
current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
current_asset_depr_schedule_doc.cancel()
notes = _(
"This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.get("doctype"), self.get("name")),
)
new_asset_depr_schedule_doc.notes = notes
new_asset_depr_schedule_doc.insert()
depr_schedule = new_asset_depr_schedule_doc.get("depreciation_schedule")
if d.depreciation_method in ("Straight Line", "Manual"):
end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx)
end_date = max(s.schedule_date for s in depr_schedule)
total_days = date_diff(end_date, self.date)
rate_per_day = flt(d.value_after_depreciation) / flt(total_days)
from_date = self.date
else:
no_of_depreciations = len(
[
s.name for s in asset.schedules if (cint(s.finance_book_id) == d.idx and not s.journal_entry)
]
)
no_of_depreciations = len([s.name for s in depr_schedule if not s.journal_entry])
value_after_depreciation = d.value_after_depreciation
for data in asset.schedules:
if cint(data.finance_book_id) == d.idx and not data.journal_entry:
for data in depr_schedule:
if not data.journal_entry:
if d.depreciation_method in ("Straight Line", "Manual"):
days = date_diff(data.schedule_date, from_date)
depreciation_amount = days * rate_per_day
@@ -140,10 +163,12 @@ class AssetValueAdjustment(Document):
d.db_update()
asset.set_accumulated_depreciation(ignore_booked_entry=True)
for asset_data in asset.schedules:
if not asset_data.journal_entry:
asset_data.db_update()
set_accumulated_depreciation(new_asset_depr_schedule_doc, d, ignore_booked_entry=True)
for asset_data in depr_schedule:
if not asset_data.journal_entry:
asset_data.db_update()
new_asset_depr_schedule_doc.submit()
@frappe.whitelist()

View File

@@ -7,6 +7,9 @@ import frappe
from frappe.utils import add_days, get_last_day, nowdate
from erpnext.assets.doctype.asset.test_asset import create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
)
from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import (
get_current_asset_value,
)
@@ -73,12 +76,21 @@ class TestAssetValueAdjustment(unittest.TestCase):
)
asset_doc.submit()
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
current_value = get_current_asset_value(asset_doc.name)
adj_doc = make_asset_value_adjustment(
asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0
)
adj_doc.submit()
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
expected_gle = (
("_Test Accumulated Depreciations - _TC", 0.0, 50000.0),
("_Test Depreciations - _TC", 50000.0, 0.0),

View File

@@ -1,318 +1,84 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 1,
"autoname": "",
"beta": 0,
"creation": "2016-03-02 15:11:01.278862",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"allow_rename": 1,
"creation": "2016-03-02 15:11:01.278862",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"schedule_date",
"depreciation_amount",
"column_break_3",
"accumulated_depreciation_amount",
"journal_entry",
"make_depreciation_entry",
"depreciation_method"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "finance_book",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Finance Book",
"length": 0,
"no_copy": 0,
"options": "Finance Book",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "schedule_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Schedule Date",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "schedule_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Schedule Date",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "depreciation_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Depreciation Amount",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "depreciation_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Depreciation Amount",
"length": 0,
"no_copy": 1,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"fieldname": "accumulated_depreciation_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Accumulated Depreciation Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "accumulated_depreciation_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Accumulated Depreciation Amount",
"length": 0,
"no_copy": 1,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"depends_on": "eval:doc.docstatus==1",
"fieldname": "journal_entry",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Journal Entry",
"options": "Journal Entry",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.docstatus==1",
"fieldname": "journal_entry",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Journal Entry",
"length": 0,
"no_copy": 1,
"options": "Journal Entry",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
"allow_on_submit": 1,
"depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())",
"fieldname": "make_depreciation_entry",
"fieldtype": "Button",
"label": "Make Depreciation Entry"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())",
"fieldname": "make_depreciation_entry",
"fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Make Depreciation Entry",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "finance_book_id",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Finance Book Id",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "depreciation_method",
"fieldtype": "Select",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Depreciation Method",
"length": 0,
"no_copy": 1,
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "depreciation_method",
"fieldtype": "Select",
"hidden": 1,
"label": "Depreciation Method",
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
"print_hide": 1,
"read_only": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-05-10 15:12:41.679436",
"modified_by": "Administrator",
"module": "Assets",
"name": "Depreciation Schedule",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
],
"istable": 1,
"links": [],
"modified": "2022-12-06 20:35:50.264281",
"modified_by": "Administrator",
"module": "Assets",
"name": "Depreciation Schedule",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -176,15 +176,17 @@ def get_finance_book_value_map(filters):
return frappe._dict(
frappe.db.sql(
""" Select
parent, SUM(depreciation_amount)
FROM `tabDepreciation Schedule`
ads.asset, SUM(depreciation_amount)
FROM `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds
WHERE
parentfield='schedules'
AND schedule_date<=%s
AND journal_entry IS NOT NULL
AND ifnull(finance_book, '')=%s
GROUP BY parent""",
(date, cstr(filters.finance_book or "")),
ds.parent = ads.name
AND ifnull(ads.finance_book, '')=%s
AND ads.docstatus=1
AND ds.parentfield='depreciation_schedule'
AND ds.schedule_date<=%s
AND ds.journal_entry IS NOT NULL
GROUP BY ads.asset""",
(cstr(filters.finance_book or ""), date),
)
)

View File

@@ -235,11 +235,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
cur_frm.add_custom_button(__('Purchase Invoice'),
this.make_purchase_invoice, __('Create'));
if(flt(doc.per_billed)==0 && doc.status != "Delivered") {
if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
}
if(flt(doc.per_billed)==0) {
if(flt(doc.per_billed) < 100) {
this.frm.add_custom_button(__('Payment Request'),
function() { me.make_payment_request() }, __('Create'));
}

View File

@@ -62,12 +62,13 @@
"set_reserve_warehouse",
"supplied_items",
"taxes_section",
"tax_category",
"taxes_and_charges",
"column_break_53",
"tax_category",
"column_break_50",
"shipping_rule",
"column_break_50",
"incoterm",
"named_place",
"section_break_52",
"taxes",
"totals",
@@ -107,7 +108,7 @@
"contact_display",
"contact_mobile",
"contact_email",
"company_shipping_address_section",
"shipping_address_section",
"shipping_address",
"column_break_99",
"shipping_address_display",
@@ -384,7 +385,7 @@
{
"fieldname": "shipping_address",
"fieldtype": "Link",
"label": "Company Shipping Address",
"label": "Shipping Address",
"options": "Address",
"print_hide": 1
},
@@ -1206,11 +1207,6 @@
"fieldtype": "Tab Break",
"label": "Address & Contact"
},
{
"fieldname": "company_shipping_address_section",
"fieldtype": "Section Break",
"label": "Company Shipping Address"
},
{
"fieldname": "company_billing_address_section",
"fieldtype": "Section Break",
@@ -1256,13 +1252,24 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
},
{
"fieldname": "shipping_address_section",
"fieldtype": "Section Break",
"label": "Shipping Address"
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2022-11-17 17:28:07.729943",
"modified": "2022-12-25 18:08:59.074182",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@@ -207,31 +207,36 @@ class PurchaseOrder(BuyingController):
)
def validate_fg_item_for_subcontracting(self):
if self.is_subcontracted and not self.is_old_subcontracting_flow:
if self.is_subcontracted:
if not self.is_old_subcontracting_flow:
for item in self.items:
if not item.fg_item:
frappe.throw(
_("Row #{0}: Finished Good Item is not specified for service item {1}").format(
item.idx, item.item_code
)
)
else:
if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"):
frappe.throw(
_(
"Row #{0}: Finished Good Item {1} must be a sub-contracted item for service item {2}"
).format(item.idx, item.fg_item, item.item_code)
)
elif not frappe.get_value("Item", item.fg_item, "default_bom"):
frappe.throw(
_("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item)
)
if not item.fg_item_qty:
frappe.throw(
_("Row #{0}: Finished Good Item Qty is not specified for service item {0}").format(
item.idx, item.item_code
)
)
else:
for item in self.items:
if not item.fg_item:
frappe.throw(
_("Row #{0}: Finished Good Item is not specified for service item {1}").format(
item.idx, item.item_code
)
)
else:
if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"):
frappe.throw(
_(
"Row #{0}: Finished Good Item {1} must be a sub-contracted item for service item {2}"
).format(item.idx, item.fg_item, item.item_code)
)
elif not frappe.get_value("Item", item.fg_item, "default_bom"):
frappe.throw(
_("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item)
)
if not item.fg_item_qty:
frappe.throw(
_("Row #{0}: Finished Good Item Qty is not specified for service item {0}").format(
item.idx, item.item_code
)
)
item.set("fg_item", None)
item.set("fg_item_qty", 0)
def get_schedule_dates(self):
for d in self.get("items"):

View File

@@ -57,44 +57,96 @@ frappe.ui.form.on("Request for Quotation",{
});
}, __("Tools"));
frm.add_custom_button(__('Download PDF'), () => {
var suppliers = [];
const fields = [{
fieldtype: 'Link',
label: __('Select a Supplier'),
fieldname: 'supplier',
options: 'Supplier',
reqd: 1,
get_query: () => {
return {
filters: [
["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})]
]
}
}
}];
frappe.prompt(fields, data => {
var child = locals[cdt][cdn]
var w = window.open(
frappe.urllib.get_full_url("/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?"
+"doctype="+encodeURIComponent(frm.doc.doctype)
+"&name="+encodeURIComponent(frm.doc.name)
+"&supplier="+encodeURIComponent(data.supplier)
+"&no_letterhead=0"));
if(!w) {
frappe.msgprint(__("Please enable pop-ups")); return;
}
frm.add_custom_button(
__("Download PDF"),
() => {
frappe.prompt(
[
{
fieldtype: "Link",
label: "Select a Supplier",
fieldname: "supplier",
options: "Supplier",
reqd: 1,
default: frm.doc.suppliers?.length == 1 ? frm.doc.suppliers[0].supplier : "",
get_query: () => {
return {
filters: [
[
"Supplier",
"name",
"in",
frm.doc.suppliers.map((row) => {
return row.supplier;
}),
],
],
};
},
},
{
fieldtype: "Section Break",
label: "Print Settings",
fieldname: "print_settings",
collapsible: 1,
},
{
fieldtype: "Link",
label: "Print Format",
fieldname: "print_format",
options: "Print Format",
placeholder: "Standard",
get_query: () => {
return {
filters: {
doc_type: "Request for Quotation",
},
};
},
},
{
fieldtype: "Link",
label: "Language",
fieldname: "language",
options: "Language",
default: frappe.boot.lang,
},
{
fieldtype: "Link",
label: "Letter Head",
fieldname: "letter_head",
options: "Letter Head",
default: frm.doc.letter_head,
},
],
(data) => {
var w = window.open(
frappe.urllib.get_full_url(
"/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" +
new URLSearchParams({
doctype: frm.doc.doctype,
name: frm.doc.name,
supplier: data.supplier,
print_format: data.print_format || "Standard",
language: data.language || frappe.boot.lang,
letter_head: data.letter_head || frm.doc.letter_head || "",
}).toString()
)
);
if (!w) {
frappe.msgprint(__("Please enable pop-ups"));
return;
}
},
"Download PDF for Supplier",
"Download"
);
},
'Download PDF for Supplier',
'Download');
},
__("Tools"));
__("Tools")
);
frm.page.set_inner_btn_group_as_primary(__('Create'));
frm.page.set_inner_btn_group_as_primary(__("Create"));
}
},
make_supplier_quotation: function(frm) {

View File

@@ -389,10 +389,17 @@ def create_rfq_items(sq_doc, supplier, data):
@frappe.whitelist()
def get_pdf(doctype, name, supplier):
doc = get_rfq_doc(doctype, name, supplier)
if doc:
download_pdf(doctype, name, doc=doc)
def get_pdf(doctype, name, supplier, print_format=None, language=None, letter_head=None):
# permissions get checked in `download_pdf`
if doc := get_rfq_doc(doctype, name, supplier):
download_pdf(
doctype,
name,
print_format,
doc=doc,
language=language,
letter_head=letter_head or None,
)
def get_rfq_doc(doctype, name, supplier):

View File

@@ -20,9 +20,6 @@ from erpnext.utilities.transaction_base import TransactionBase
class Supplier(TransactionBase):
def get_feed(self):
return self.supplier_name
def onload(self):
"""Load address and contacts in `__onload`"""
load_address_and_contact(self)

View File

@@ -40,12 +40,13 @@
"total",
"net_total",
"taxes_section",
"tax_category",
"taxes_and_charges",
"column_break_34",
"tax_category",
"column_break_36",
"shipping_rule",
"column_break_36",
"incoterm",
"named_place",
"section_break_38",
"taxes",
"totals",
@@ -830,6 +831,12 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
},
{
"depends_on": "incoterm",
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
}
],
"icon": "fa fa-shopping-cart",
@@ -837,7 +844,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-11-17 17:27:32.179686",
"modified": "2022-12-12 18:35:39.740974",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",

View File

@@ -197,7 +197,7 @@ class AccountsController(TransactionBase):
validate_einvoice_fields(self)
if self.doctype != "Material Request":
if self.doctype != "Material Request" and not self.ignore_pricing_rule:
apply_pricing_rule_on_transaction(self)
def before_cancel(self):
@@ -584,7 +584,12 @@ class AccountsController(TransactionBase):
if bool(uom) != bool(stock_uom): # xor
item.stock_uom = item.uom = uom or stock_uom
item.conversion_factor = get_uom_conv_factor(item.get("uom"), item.get("stock_uom"))
# UOM cannot be zero so substitute as 1
item.conversion_factor = (
get_uom_conv_factor(item.get("uom"), item.get("stock_uom"))
or item.get("conversion_factor")
or 1
)
if self.doctype == "Purchase Invoice":
self.set_expense_account(for_validate)

View File

@@ -25,10 +25,6 @@ class BuyingController(SubcontractingController):
def __setup__(self):
self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"]
def get_feed(self):
if self.get("supplier_name"):
return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total)
def validate(self):
super(BuyingController, self).validate()
if getattr(self, "supplier", None) and not self.supplier_name:
@@ -322,17 +318,18 @@ class BuyingController(SubcontractingController):
)
if self.is_internal_transfer():
if rate != d.rate:
d.rate = rate
frappe.msgprint(
_(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
).format(d.idx),
alert=1,
)
d.discount_percentage = 0.0
d.discount_amount = 0.0
d.margin_rate_or_amount = 0.0
if self.doctype == "Purchase Receipt" or self.get("update_stock"):
if rate != d.rate:
d.rate = rate
frappe.msgprint(
_(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
).format(d.idx),
alert=1,
)
d.discount_percentage = 0.0
d.discount_amount = 0.0
d.margin_rate_or_amount = 0.0
def validate_for_subcontracting(self):
if self.is_subcontracted and self.get("is_old_subcontracting_flow"):

View File

@@ -19,14 +19,11 @@ class SellingController(StockController):
def __setup__(self):
self.flags.ignore_permlevel_for_fields = ["selling_price_list", "price_list_currency"]
def get_feed(self):
return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total)
def onload(self):
super(SellingController, self).onload()
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
for item in self.get("items"):
item.update(get_bin_details(item.item_code, item.warehouse))
item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True))
def validate(self):
super(SellingController, self).validate()
@@ -442,30 +439,31 @@ class SellingController(StockController):
# For internal transfers use incoming rate as the valuation rate
if self.is_internal_transfer():
if d.doctype == "Packed Item":
incoming_rate = flt(
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
d.precision("incoming_rate"),
)
if d.incoming_rate != incoming_rate:
d.incoming_rate = incoming_rate
else:
rate = flt(
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
d.precision("rate"),
)
if d.rate != rate:
d.rate = rate
frappe.msgprint(
_(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
).format(d.idx),
alert=1,
if self.doctype == "Delivery Note" or self.get("update_stock"):
if d.doctype == "Packed Item":
incoming_rate = flt(
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
d.precision("incoming_rate"),
)
if d.incoming_rate != incoming_rate:
d.incoming_rate = incoming_rate
else:
rate = flt(
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
d.precision("rate"),
)
if d.rate != rate:
d.rate = rate
frappe.msgprint(
_(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
).format(d.idx),
alert=1,
)
d.discount_percentage = 0.0
d.discount_amount = 0.0
d.margin_rate_or_amount = 0.0
d.discount_percentage = 0.0
d.discount_amount = 0.0
d.margin_rate_or_amount = 0.0
elif self.get("return_against"):
# Get incoming rate of return entry from reference document

View File

@@ -347,16 +347,21 @@ class StatusUpdater(Document):
)
def warn_about_bypassing_with_role(self, item, qty_or_amount, role):
action = _("Over Receipt/Delivery") if qty_or_amount == "qty" else _("Overbilling")
if qty_or_amount == "qty":
msg = _("Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.")
else:
msg = _("Overbilling of {0} {1} ignored for item {2} because you have {3} role.")
msg = _("{} of {} {} ignored for item {} because you have {} role.").format(
action,
_(item["target_ref_field"].title()),
frappe.bold(item["reduce_by"]),
frappe.bold(item.get("item_code")),
role,
frappe.msgprint(
msg.format(
_(item["target_ref_field"].title()),
frappe.bold(item["reduce_by"]),
frappe.bold(item.get("item_code")),
role,
),
indicator="orange",
alert=True,
)
frappe.msgprint(msg, indicator="orange", alert=True)
def update_qty(self, update_modified=True):
"""Updates qty or amount at row level

View File

@@ -829,6 +829,9 @@ def make_rm_stock_entry(
order_doctype: {
"doctype": "Stock Entry",
"field_map": {
"supplier": "supplier",
"supplier_name": "supplier_name",
"supplier_address": "supplier_address",
"to_warehouse": "supplier_warehouse",
},
"field_no_map": [field_no_map],

View File

@@ -102,7 +102,7 @@
}
],
"links": [],
"modified": "2021-06-30 13:09:14.228756",
"modified": "2022-12-15 11:11:02.131986",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment",
@@ -121,16 +121,6 @@
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Guest",
"share": 1
},
{
"create": 1,
"delete": 1,
@@ -170,5 +160,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -6,7 +6,9 @@ from collections import Counter
import frappe
from frappe import _
from frappe.desk.form.assign_to import add as add_assignment
from frappe.model.document import Document
from frappe.share import add_docshare
from frappe.utils import get_url, getdate, now
from frappe.utils.verified_command import get_signed_params
@@ -130,21 +132,18 @@ class Appointment(Document):
self.party = lead.name
def auto_assign(self):
from frappe.desk.form.assign_to import add as add_assignemnt
existing_assignee = self.get_assignee_from_latest_opportunity()
if existing_assignee:
# If the latest opportunity is assigned to someone
# Assign the appointment to the same
add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [existing_assignee]})
self.assign_agent(existing_assignee)
return
if self._assign:
return
available_agents = _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time))
for agent in available_agents:
if _check_agent_availability(agent, self.scheduled_time):
agent = agent[0]
add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
self.assign_agent(agent[0])
break
def get_assignee_from_latest_opportunity(self):
@@ -199,9 +198,15 @@ class Appointment(Document):
params = {"email": self.customer_email, "appointment": self.name}
return get_url(verify_route + "?" + get_signed_params(params))
def assign_agent(self, agent):
if not frappe.has_permission(doc=self, user=agent):
add_docshare(self.doctype, self.name, agent, flags={"ignore_share_permission": True})
add_assignment({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
def _get_agents_sorted_by_asc_workload(date):
appointments = frappe.db.get_list("Appointment", fields="*")
appointments = frappe.get_all("Appointment", fields="*")
agent_list = _get_agent_list_as_strings()
if not appointments:
return agent_list
@@ -226,7 +231,7 @@ def _get_agent_list_as_strings():
def _check_agent_availability(agent_email, scheduled_time):
appointemnts_at_scheduled_time = frappe.get_list(
appointemnts_at_scheduled_time = frappe.get_all(
"Appointment", filters={"scheduled_time": scheduled_time}
)
for appointment in appointemnts_at_scheduled_time:

View File

@@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2019-08-27 10:56:48.309824",
"doctype": "DocType",
"editable_grid": 1,
@@ -101,7 +102,8 @@
}
],
"issingle": 1,
"modified": "2019-11-26 12:14:17.669366",
"links": [],
"modified": "2022-12-15 11:10:13.517742",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment Booking Settings",
@@ -117,13 +119,6 @@
"share": 1,
"write": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Guest",
"share": 1
},
{
"create": 1,
"email": 1,
@@ -147,5 +142,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -14,9 +14,6 @@ from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_
class Lead(SellingController, CRMNote):
def get_feed(self):
return "{0}: {1}".format(_(self.status), self.lead_name)
def onload(self):
customer = frappe.db.get_value("Customer", {"lead_name": self.name})
self.get("__onload").is_customer = customer
@@ -453,6 +450,7 @@ def get_lead_with_phone_number(number):
"Lead",
or_filters={
"phone": ["like", "%{}".format(number)],
"whatsapp_no": ["like", "%{}".format(number)],
"mobile_no": ["like", "%{}".format(number)],
},
limit=1,

View File

@@ -48,5 +48,11 @@ frappe.ui.form.on("E Commerce Settings", {
frm.set_value('default_customer_group', '');
frm.set_value('quotation_series', '');
}
},
enable_checkout: function(frm) {
if (frm.doc.enable_checkout) {
erpnext.utils.check_payments_app();
}
}
});

View File

@@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2019-05-21 07:41:53.536536",
"doctype": "DocType",
"engine": "InnoDB",
@@ -7,10 +8,14 @@
"section_break_2",
"account_sid",
"api_key",
"api_token"
"api_token",
"section_break_6",
"map_custom_field_to_doctype",
"target_doctype"
],
"fields": [
{
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
@@ -18,7 +23,8 @@
{
"depends_on": "enabled",
"fieldname": "section_break_2",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Credentials"
},
{
"fieldname": "account_sid",
@@ -34,10 +40,31 @@
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key"
},
{
"depends_on": "enabled",
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"label": "Custom Field"
},
{
"default": "0",
"fieldname": "map_custom_field_to_doctype",
"fieldtype": "Check",
"label": "Map Custom Field to DocType"
},
{
"depends_on": "map_custom_field_to_doctype",
"fieldname": "target_doctype",
"fieldtype": "Link",
"label": "Target DocType",
"mandatory_depends_on": "map_custom_field_to_doctype",
"options": "DocType"
}
],
"issingle": 1,
"modified": "2019-05-22 06:25:18.026997",
"links": [],
"modified": "2022-12-14 17:24:50.176107",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Exotel Settings",
@@ -57,5 +84,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View File

@@ -2,4 +2,7 @@
// For license information, please see license.txt
frappe.ui.form.on('GoCardless Settings', {
refresh: function(frm) {
erpnext.utils.check_payments_app();
}
});

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