Compare commits

...

452 Commits

Author SHA1 Message Date
Saqib Ansari
aa8b241d5a chore: update CODEOWNERS
[skip ci]
2023-04-11 15:46:47 +05:30
rohitwaghchaure
2f9856436e Merge pull request #34808 from rohitwaghchaure/fixed-reposting-issue-for-stock-reco
fix: reposting record not created for backdated stock reconciliation
2023-04-11 15:02:18 +05:30
Rohit Waghchaure
6851b5ba97 fix: reposting record not created for backdated stock reco 2023-04-11 14:35:33 +05:30
Anand Baburajan
c957a5cd2e fix: provide filter by depreciable assets in fixed asset register (#34803) 2023-04-11 12:16:07 +05:30
Raffael Meyer
934e1b4e6a fix: add german translation of "Partly Paid" (#34776) 2023-04-09 19:04:57 +05:30
Deepesh Garg
6f6928fa7b fix: Item tax validity comparison fixes (#34784)
fix: Item tax validity comparsion fixes
2023-04-09 09:38:13 +05:30
Sagar Sharma
02d5d43eb1 Merge pull request #34632 from s-aga-r/refactor-batch
refactor: rewrite `batch.py` queries in `QB`
2023-04-08 13:11:01 +05:30
Sagar Sharma
299fb2619f Merge branch 'develop' into refactor-batch 2023-04-08 11:15:08 +05:30
s-aga-r
4a9a5f03fc Merge branch 'develop' into refactor-batch 2023-04-08 11:06:27 +05:30
Sagar Sharma
ba553dba62 Merge pull request #34739 from resilient-tech/item-tax-override-remove
chore!: remove override to set item tax based on HSN code
2023-04-07 21:39:08 +05:30
Anand Baburajan
29e025c012 Merge pull request #34778 from AnandBaburajan/asset_value_adjust_get_depr_amt
fix: improper usage of get_depreciation_amount in asset_value_adjustment
2023-04-07 16:48:06 +05:30
Anand Baburajan
90dee8b663 Merge pull request #34735 from ihosseinu/asset_movement_translation_fix
'Make Asset Movement' button translation fix in asset_list.js
2023-04-07 15:30:26 +05:30
Anand Baburajan
edaa0bc3f7 Merge branch 'develop' into asset_value_adjust_get_depr_amt 2023-04-07 15:24:37 +05:30
anandbaburajan
984905109d fix: improper usage of get_depreciation_amount in asset_value_adjustment 2023-04-07 15:23:02 +05:30
Sagar Sharma
45291aaa43 Merge pull request #34760 from s-aga-r/FIX-ISS-22-23-06306
fix: validate `Received Qty` for Internal Purchase Receipt
2023-04-06 16:25:29 +05:30
Sagar Sharma
f7fb001126 Merge branch 'develop' into FIX-ISS-22-23-06306 2023-04-06 15:59:32 +05:30
s-aga-r
a575bd50ef test: add test cases for internal PR received qty 2023-04-06 15:26:19 +05:30
rohitwaghchaure
7d2d2dd0af Merge pull request #34768 from rohitwaghchaure/fixed-ux-issue
fix: UX for stock entry, bom and work order
2023-04-06 14:16:20 +05:30
Sagar Sharma
7934441f3c Merge pull request #34769 from s-aga-r/FIX-ISS-22-23-06397
fix: Subcontracting Receipt incorrect `status`
2023-04-06 13:56:23 +05:30
Sagar Sharma
cfd4da3e8a Merge branch 'develop' into FIX-ISS-22-23-06397 2023-04-06 13:23:39 +05:30
s-aga-r
a55b818119 fix: Subcontracting Receipt incorrect status 2023-04-06 13:20:33 +05:30
Rohit Waghchaure
82a136f991 fix: UX for stock entry, bom and work order 2023-04-06 12:51:32 +05:30
Deepesh Garg
91a26608ee fix: Unable to create payment request against purchase invoice (#34762) 2023-04-06 12:40:58 +05:30
s-aga-r
0d1df26b88 chore: add Delivery Note Item in Purchase Receipt Status Updater 2023-04-06 09:31:46 +05:30
s-aga-r
bc39dfab5d feat: add Received Qty field in Delivery Note Item 2023-04-06 09:29:44 +05:30
rohitwaghchaure
8503d581a5 Merge pull request #34743 from rohitwaghchaure/fix-incorrect-balance-qty
fix: incorrect stock balance quantity for batch item
2023-04-05 23:21:44 +05:30
Lucky-Tsuma
9bf87d708e fix: payment entry is already created on posawesome. (#34712) 2023-04-05 13:52:00 +05:30
Deepesh Garg
56f5078357 fix: Shop by category fixes (#34688)
* fix: Shop by category fixes

* chore: Update tests
2023-04-05 12:43:32 +05:30
Ritwik Puri
f193393f57 fix!: require sender and message for contact us page (#34707)
* fix: require sender and message for contact us page

* refactor: dont override frappe.send_message from client side

used override_whitelisted_method hook for the same
2023-04-05 12:04:36 +05:30
Deepesh Garg
fd3fb64aa3 feat: Auto allocate advance payments only against orders (#34727)
feat: Auto allocate advance payments only againt orders
2023-04-05 12:02:44 +05:30
Anand Baburajan
869289a6e4 Merge pull request #34745 from frappe/mergify/bp/develop/pr-34737
fix: don't include cancelled JVs in assdeprledger report (backport #34737)
2023-04-05 11:59:52 +05:30
Raffael Meyer
e6a9b6ee95 feat: remove deprecated get_customer_list (#34624)
feat: remove deprecated method

get_customer_list
2023-04-05 11:56:15 +05:30
Anand Baburajan
5cc3c08059 fix: asset monthly WDV and DD schedule and refactor [develop] (#34646)
* fix: monthly wdv and dd schedule

* chore: minor rename

* chore: fix tests
2023-04-05 11:46:31 +05:30
anandbaburajan
1101b7bfdf fix: don't include cancelled JVs in assdeprledger report
(cherry picked from commit 3896d41e95)
2023-04-05 06:04:43 +00:00
Rohit Waghchaure
ef4bd77196 fix: incorrect stock balance quantity for batch item 2023-04-05 00:02:45 +05:30
Sagar Vora
9c66ebb108 chore!: remove override to set item tax based on HSN code 2023-04-04 18:02:55 +05:30
Hossein Yousefian
b70615ef18 'Make Asset Movement' button translation fix 2023-04-04 14:49:43 +03:30
rohitwaghchaure
12325cb685 Merge pull request #31271 from dj12djdjs/fix-reserve-qty
fix(stock): don't reserve qty on sales return.
2023-04-04 06:21:30 +05:30
rohitwaghchaure
682730a498 Merge pull request #34715 from rohitwaghchaure/fixed-bom-update-log
fix: bom update log not working for large batch size
2023-04-03 15:49:25 +05:30
Sagar Sharma
5f2874743b Merge pull request #34713 from s-aga-r/FIX-ISS-22-23-06369
fix: consider qty field precision
2023-04-03 15:36:19 +05:30
Rohit Waghchaure
d56070301c fix: bom update log not working for large batch size 2023-04-03 14:47:58 +05:30
Sagar Sharma
4571bd58d2 Merge branch 'develop' into FIX-ISS-22-23-06369 2023-04-03 14:26:09 +05:30
s-aga-r
6ec7590c21 fix: consider qty field precision 2023-04-03 14:08:11 +05:30
ruthra kumar
1eefd517d1 Merge pull request #34694 from ruthra-kumar/recalculate_difference_on_allocation_change
fix(ui): recalculate difference amount on allocation change
2023-04-03 10:03:01 +05:30
Deepesh Garg
4c61ee30bb fix: Multiple issues in purchase invoice submission (#34600)
* fix: Multiple issues in purchase invoice submission

* fix: Base grand total calculation

* chore: Calculate base grand total separately only in multi currency docs

* fix: Add gl entry for round off
2023-04-02 09:35:27 +05:30
Kitti U. @ Ecosoft
74b29eb5e2 fix: Bank clearance for case loan (disburstment/repayment) (#34586) 2023-04-01 18:50:30 +05:30
Deepesh Garg
1e3eb4deed Merge pull request #34287 from frappe/early-payment-loss
fix: Allocate tax loss to tax account head on early payment discount
2023-04-01 18:49:02 +05:30
Sagar Sharma
7c7cfc4373 Merge pull request #34656 from s-aga-r/FIX-33830
fix: BOM Update Cost, when no actual qty
2023-04-01 16:24:01 +05:30
ruthra kumar
32a4ca6b6c fix: recalculate difference amount on allocation change 2023-04-01 09:17:33 +05:30
Sagar Sharma
2c2b5a5b6e Merge branch 'develop' into FIX-33830 2023-03-31 14:14:17 +05:30
ruthra kumar
770640f4e1 Merge pull request #34679 from ruthra-kumar/fix_credit_note_with_positive_total
fix: enclose ternary operator in parentheses
2023-03-31 13:31:16 +05:30
Sagar Sharma
b4ace345f9 Merge branch 'develop' into FIX-33830 2023-03-31 12:35:45 +05:30
Sagar Sharma
e56d40f48a Merge pull request #34677 from s-aga-r/FIX-34240
chore: make `Production Plan Item Reference` table hidden in Production Plan
2023-03-31 12:34:59 +05:30
Sagar Sharma
d0d3961a1e Merge branch 'develop' into FIX-34240 2023-03-31 12:34:27 +05:30
ruthra kumar
986daa6578 fix: enclose ternary operator in parentheses 2023-03-31 12:09:57 +05:30
Deepesh Garg
576575c227 fix: Column value mismatch in COA blank template (#34658) 2023-03-31 11:10:50 +05:30
s-aga-r
706be2a415 chore: make Production Plan Item Reference table hidden in Production Plan 2023-03-31 10:32:49 +05:30
Deepesh Garg
7c42b72ee7 fix: Total debit and credit while importing via Data Import (#34659) 2023-03-30 17:28:19 +05:30
rohitwaghchaure
edb0666b2c Merge branch 'develop' into fix-reserve-qty 2023-03-30 17:26:32 +05:30
rohitwaghchaure
c4a0842a4f Merge pull request #34636 from rohitwaghchaure/posting-time-issue
fix: posting time issue
2023-03-30 17:25:47 +05:30
rohitwaghchaure
06eac67796 Merge pull request #34664 from rohitwaghchaure/fixed-incorrect-balance-qty-stock-ledger
fix: incorrect balance qty in the stock ledger report
2023-03-30 17:25:35 +05:30
Richard Case
ddb17a8880 fix: plaid log_error syntax issue (#34642) 2023-03-30 16:45:56 +05:30
Anand Baburajan
d999dea3e4 chore: improve asset depr posting failure msg (#34661)
* chore: improve asset depr posting error msg

* chore: add period

* chore: improve msg
2023-03-30 16:38:37 +05:30
Rohit Waghchaure
345e6facbe fix: posting time issue 2023-03-30 16:34:34 +05:30
Rohit Waghchaure
cbdaab940d fix: incorrect balance qty in the stock ledger report 2023-03-30 16:20:42 +05:30
Anand Baburajan
a7a45fbe59 Merge pull request #34660 from AnandBaburajan/asset_improvements_30_mar_develop
chore: auto fill asset name and available for use date
2023-03-30 16:07:59 +05:30
anandbaburajan
af3e807607 chore: auto fill asset name and available for use date 2023-03-30 15:28:59 +05:30
Sagar Sharma
b6c0a675f9 Merge branch 'develop' into FIX-33830 2023-03-30 13:49:41 +05:30
s-aga-r
a4112c75c5 fix: BOM Update Cost, when no actual qty 2023-03-30 13:46:50 +05:30
rohitwaghchaure
448afa2b7c Merge pull request #34648 from rohitwaghchaure/fixed-serial-no-qty-issue-in-stock-reco
fix: serial no with zero quantity issue in stock reco
2023-03-30 13:04:48 +05:30
Anand Baburajan
0ac81e3793 Merge pull request #34649 from AnandBaburajan/asset_value_adjustment_asset_name
fix: incorrect arg name in asset value adjustment
2023-03-30 13:03:18 +05:30
Anand Baburajan
33ff1b5a72 Merge branch 'develop' into asset_value_adjustment_asset_name 2023-03-30 12:53:06 +05:30
anandbaburajan
2b0470d1f5 fix: incorrect arg name in asset value adjustment 2023-03-30 12:50:33 +05:30
Rohit Waghchaure
17131e5a02 fix: serial no with zero quantity issue in stock reco 2023-03-30 11:47:32 +05:30
Komal-Saraf0609
ad11934d39 fix: enabling lead even after "Opportunity" created against it (#34627)
* fix: enabling lead even after "Opportunity" created against it

* chore: Linting Issues

---------

Co-authored-by: Komal Saraf <komal@frappe.io>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-03-30 08:03:55 +05:30
Anand Baburajan
7c5720ddb3 Merge pull request #34641 from frappe/mergify/bp/develop/pr-34607
Asset maintenance task add dropdown "3 Yearly" (backport #34607)
2023-03-29 18:15:17 +05:30
Bevan Tony Medrano
ddc3050e99 Asset maintenance task add dropdown "3 Yearly" (#34607)
* feat(asset_maintenance.json):Add 3 yearly in periodicity dropdown

* add server side implications for 3 yearly

(cherry picked from commit 625b8e8005)
2023-03-29 12:16:03 +00:00
Raffael Meyer
f5453adf4d fix: switch to supported update_linked_doctypes (#34625) 2023-03-29 16:59:10 +05:30
Komal-Saraf0609
d0660ad222 fix: lost opportunity report issue (#34626)
* fix: lost opportunity report issue

* chore: Linting Issues

---------

Co-authored-by: Komal Saraf <komal@frappe.io>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-03-29 16:51:18 +05:30
s-aga-r
517b5f8567 refactor: rewrite batch.py queries in QB 2023-03-29 14:02:09 +05:30
Sagar Sharma
867d898304 fix: zero rm-cost for batch rm item in SCR (#34616)
fix: `0` rm-cost for batch rm item in SCR
2023-03-28 18:45:16 +05:30
rohitwaghchaure
738dd57904 Merge pull request #34461 from s-aga-r/FIX-ISS-22-23-05936
fix: incorrect `Opening Value` in `Stock Balance` report
2023-03-28 17:40:25 +05:30
akiratfli
07c9b99072 fix: bad strings format for update-translations (#34592)
Co-authored-by: justin.li <justin.li@lungteh.com>
2023-03-28 17:20:54 +05:30
ruthra kumar
7c13533f30 Merge pull request #34608 from ruthra-kumar/reconciliation_debit_credit_mismatch
chore: removing redundant validation
2023-03-28 16:54:37 +05:30
Vishal Dhayagude
7aafc90d58 fix: Tax Category not able to set hence it calculating zero tax for item whoes tax template set (#34525)
* fix: Tax Category not able to set hence it calculating zero tax for item whoes tax template set

* fix: minor change added
2023-03-28 16:48:03 +05:30
Devin Slauenwhite
393bc25e2d fix: don't get zero value entries for exchange rate calculation (#34475)
* fix: multiply None by float

* chore: remove debug
2023-03-28 16:34:24 +05:30
Deepesh Garg
50c1172f29 fix: Party Name in SOA print when viewed from Customer/Supplier master (#34597)
fix: Party Name in SOA print when viewd from Customer/Supplier master
2023-03-28 15:36:16 +05:30
Deepesh Garg
12ad2aa2e5 fix: Percentage billing in Sales Order (#34606) 2023-03-28 15:33:59 +05:30
Marica
dae40dfbb4 Merge branch 'develop' into early-payment-loss 2023-03-28 15:01:28 +05:30
ruthra kumar
d52f7e2820 fix: removing redundant validation 2023-03-28 13:44:23 +05:30
Devin Slauenwhite
3c553b0938 feat: don't reserve qty on sales return 2023-03-27 10:37:54 -04:00
Devin Slauenwhite
17f8080168 chore: revert get_reserve_qty method 2023-03-27 10:37:04 -04:00
marination
216a46bd66 feat: Make Tax loss booking optional
- Checkbox in Accounts Settings
- Apply checkbox in PE deductions setting logic
- Adjust tests
2023-03-27 16:11:00 +05:30
=
1142e69d1a Merge branch 'develop' into fix-reserve-qty
# Conflicts:
#	erpnext/selling/doctype/selling_settings/selling_settings.json
2023-03-24 23:50:50 -04:00
ruthra kumar
f7780cdb58 refactor: additional filters and columns in Payment Ledger report (#34577)
1. 'Party type' and 'Party' filters have been added
2. checkbox to include Amount in Acccount Currency
3. Grouping vouchers on Party
4. Replaced Company with Posting Date
2023-03-24 12:25:03 +05:30
Raffael Meyer
8c7fa5712b feat: deprecate get_customer_list (#34563) 2023-03-24 12:13:53 +05:30
Shram Kadia
0cd870fc29 fix: get batch_no. for item automatically (#34473)
* fix: default pos conversion factor set to 1

* fix:get-batch-no-of-item-automatically
2023-03-24 12:05:26 +05:30
Nabin Hait
282a0ee2ce Merge pull request #34257 from deepeshgarg007/opening_entry
feat: Closing balance for period closing and reporting
2023-03-24 11:59:00 +05:30
rohitwaghchaure
980cca1772 Merge pull request #34573 from rohitwaghchaure/timer-buttons-not-working
fix: Time buttons not working in the job card
2023-03-23 22:35:43 +05:30
Rohit Waghchaure
34c190b7d6 fix: Time button not working in the job card 2023-03-23 22:32:11 +05:30
Anand Baburajan
e6b0196493 fix: recalculate wdv rate after asset repair [develop] (#34570)
fix: recalculate wdv rate after asset repair
2023-03-23 21:10:22 +05:30
Deepesh Garg
87108be11a fix: Sales person variance report without item group (#34552)
fix: Sales person variance report witout item group
2023-03-23 19:19:46 +05:30
Deepesh Garg
8ce1da111e chore: Improve validation message 2023-03-23 19:14:12 +05:30
Deepesh Garg
f8cff09129 fix: CS financial statement param 2023-03-23 19:12:42 +05:30
Deepesh Garg
b7dcf27b01 fix: Partial trial balance view 2023-03-23 19:06:07 +05:30
Deepesh Garg
30eb6c8512 fix: Validation for cancelation 2023-03-23 19:05:25 +05:30
Deepesh Garg
3fd95200da chore: Fix Typo 2023-03-23 19:04:48 +05:30
Deepesh Garg
528ab503f2 fix: Don't validate if no GL Entry exists 2023-03-23 15:32:24 +05:30
Raffael Meyer
f7bf1b8a0c fix: unset address and contact on trash (#34495)
* fix(Customer): unset address and contact on trash

* fix(Supplier): unset address and contact on trash

---------

Co-authored-by: Sagar Sharma <sagarsharma.s312@gmail.com>
2023-03-23 11:52:32 +05:30
Shariq Ansari
829bc6a51f Merge pull request #34547 from shariquerik/crm-comments-overlap-fix 2023-03-22 12:02:10 +05:30
Shariq Ansari
24a0833980 Merge branch 'develop' into crm-comments-overlap-fix 2023-03-22 11:40:58 +05:30
Shariq Ansari
76cea7dd6a fix: Note username overlapping with note content(CRM) 2023-03-22 11:28:58 +05:30
Anand Baburajan
6d6a7bcb50 fix: incorrect depr schedules after asset repair [develop] (#34544)
* fix: backport missing changes from #30838

* fix: incorrect schedule after repair
2023-03-21 20:07:27 +05:30
Deepesh Garg
fa991b2dcf Merge branch 'develop' of https://github.com/frappe/erpnext into opening_entry 2023-03-21 17:00:08 +05:30
Deepesh Garg
f9397a87ac fix: Ignore opening entries if PCV posted 2023-03-21 16:59:22 +05:30
Richard Case
d8e73b63f3 fix: remove non-maintained Tax Detail report and missing workspace links (#34192)
* fix: remove non-maintained Tax Detail report and missing workspace links

* patch: delete report "Tax Detail"

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-03-21 16:23:27 +05:30
Raffael Meyer
79911734e9 fix: translations and UX in alternative item mapping (#34433)
* fix: disable deletion in alternative item mapping

* feat: german translations

* fix: make string translatable

---------

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-03-21 16:20:11 +05:30
Raffael Meyer
0df3a1a3af fix: remove unused translation (#34519) 2023-03-21 16:18:45 +05:30
Shram Kadia
08fc686513 fix: default pos conversion factor set to 1 (#34437) 2023-03-21 15:51:18 +05:30
Raffael Meyer
59c2e7ec3e fix: german translations (#34312)
fix: some german translations
2023-03-21 15:48:27 +05:30
Vishal Dhayagude
6966fa4d88 fix: POS not picking up pos profile company address instead fetch any random company address (#34521) 2023-03-21 15:43:57 +05:30
Deepesh Garg
ee6c107d58 fix(client): Amount calculation for 0 qty debit notes (#34455)
fix(client): Amount calculaton for 0 qty debit notes

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-03-21 15:38:39 +05:30
ruthra kumar
2d19cee713 Merge pull request #34501 from ruthra-kumar/incorrect_currency_symbol_for_bank_accounts_in_reconciliation_tool
refactor(bank reconciliation tool): currency symbol fix and concurrent usage
2023-03-21 15:30:50 +05:30
rohitwaghchaure
af5f59e856 Merge pull request #34528 from rohitwaghchaure/no-stock-still-option-to-add-in-cart
fix: E-commerce issue with Item Variants
2023-03-21 14:46:20 +05:30
Rohit Waghchaure
aaa4d1eb55 fix: E-commerce issue with Item Variants 2023-03-21 14:44:12 +05:30
Marica
c5da7f5fb1 Merge branch 'develop' into early-payment-loss 2023-03-21 11:40:30 +05:30
Ankush Menat
109a9f1390 perf: index against_sales_invoice field on DN items (#34509)
This is used on Sales invoice dashboard and takes a lot of time to load
as db size increases.

Results:

Before: ~10-20 seconds to load dashboard
After: few milliseconds because of index

[skip ci]
2023-03-20 14:46:26 +05:30
Sagar Sharma
4c35a37eaa Merge branch 'develop' into FIX-ISS-22-23-05936 2023-03-20 10:28:10 +05:30
Deepesh Garg
44053db010 chore: Remove unnecessary list comprehension 2023-03-19 19:46:01 +05:30
Deepesh Garg
fc86a8568f fix: Supplier RFQ email link (#34338) 2023-03-19 18:09:18 +05:30
Raffael Meyer
d791dc11a3 fix: patch depends on Currency Exchange Settings (#34494) 2023-03-19 18:05:12 +05:30
ruthra kumar
1eea585d29 refactor: allow for concurrent use of reconciliation tool
1. set default filter dates a period of one month from current date
2023-03-19 14:20:03 +05:30
ruthra kumar
2d14d92b32 fix: incorrect currency symbol in Bank Reconciliation tool 2023-03-19 12:47:29 +05:30
Deepesh Garg
0aadb680eb chore: Add missing validations 2023-03-19 12:46:42 +05:30
Deepesh Garg
00fe3042b2 chore: Simplify query 2023-03-18 20:05:43 +05:30
Deepesh Garg
7f11373b58 fix: Account sub query 2023-03-17 16:59:23 +05:30
Deepesh Garg
d11a64ddcb Merge branch 'develop' of https://github.com/frappe/erpnext into opening_entry 2023-03-17 16:50:49 +05:30
Sagar Sharma
a07a5572ea Merge branch 'develop' into FIX-ISS-22-23-05936 2023-03-17 16:18:54 +05:30
Danny
ca10e2bb9f fix: bad strings format for command get-untraslated (#34361)
* fix: bad string foramt

* fix: bad string format

* fix: pre-commit format

---------

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-03-17 16:13:32 +05:30
Sagar Sharma
2ac079210d Merge pull request #34440 from s-aga-r/FIX-34354
chore: `Allow Zero Valuation Rate` msg in SE
2023-03-17 16:02:52 +05:30
Sagar Sharma
f2c78163bf Merge branch 'develop' into FIX-34354 2023-03-17 15:58:46 +05:30
Deepesh Garg
d8ece86463 fix: Update account number from parent company (#34474) 2023-03-17 15:55:11 +05:30
Deepesh Garg
7b630217bd fix: Multiple accounting dimension filtering in AR/AP reports (#34464)
Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-03-17 15:51:33 +05:30
Deepesh Garg
be723bb9d4 chore: Update user manual link (#34478) 2023-03-17 15:39:33 +05:30
ruthra kumar
6b0dc62681 Merge pull request #34456 from ruthra-kumar/gp_return_invoice_issue
fix: Gross Profit reports Invoices with -ve qty for Invoices with Cr Notes
2023-03-17 15:26:41 +05:30
Sagar Sharma
0753aa5ab7 Merge branch 'develop' into FIX-34354 2023-03-17 15:16:41 +05:30
Sagar Sharma
3fb64cacfc Merge branch 'develop' into FIX-ISS-22-23-05936 2023-03-17 15:03:44 +05:30
ruthra kumar
6e0f1c6ad6 Merge pull request #34466 from ruthra-kumar/difference_amount_issue_while_reconciling
fix: unwanted difference amt while reconciling vouchers from base currency account
2023-03-17 14:51:41 +05:30
ruthra kumar
cc61daeec4 test: Gross Profit report output for Cr notes
2 New test cases added.
1. Standalone Cr notes will be reported as normal Invoices
2. Cr notes against an Invoice will not overallocate qty if there are
multiple instances of same item
2023-03-17 14:48:57 +05:30
ruthra kumar
d0715a82eb refactor: Ignore linked Cr Notes in Report output 2023-03-17 14:48:57 +05:30
ruthra kumar
e2f19c6a14 fix: Overallocation of 'qty' from Cr Notes to Parent Invoice
Cr Notes 'qty' are overallocated to parent invoice, when there are
mulitple instances of same item in Invoice.
2023-03-17 14:48:57 +05:30
ruthra kumar
ec075122b6 refactor: difference amt validation for same currency accounts 2023-03-17 14:14:17 +05:30
ruthra kumar
861387f164 test: difference amount should not be calculated for base currency 2023-03-17 14:14:17 +05:30
ruthra kumar
48fae0c1ce fix: difference amount calculation for company currency accounts 2023-03-17 14:14:17 +05:30
Sagar Sharma
db1f2d6279 Merge branch 'develop' into FIX-ISS-22-23-05936 2023-03-17 13:54:13 +05:30
s-aga-r
b04a101c11 fix: incorrect Opening Value in Stock Balance report 2023-03-17 13:53:49 +05:30
rohitwaghchaure
1d767d58f2 Merge pull request #34279 from s-aga-r/fix-22-23-05686
fix: `Blanket Order`
2023-03-17 10:14:37 +05:30
rohitwaghchaure
558db13040 Merge branch 'develop' into fix-22-23-05686 2023-03-17 08:57:37 +05:30
Anand Baburajan
563ee92043 Merge branch 'develop' into FIX-34354 2023-03-16 18:22:13 +05:30
Anand Baburajan
b6b8392413 fix: refactor asset depr schedule and remove unnecessary depr method (#34434)
* fix: remove depr method from depr schedule and refactor assetdeprsch

* chore: use assetdeprsch's depr method, not deprschedule's

* fix: use default 0 value for NDB and OAD

* chore: fix rounded numbers

* chore: correct rounding in test_website_item_price_for_logged_in_user
2023-03-16 18:18:47 +05:30
mergify[bot]
bd2dd7dfac ci: use version specific payments repo (backport #34468) (#34470)
ci: use version specific payments repo (#34468)

ci: use version-14 branch of payments repo for v14 erpnext
(cherry picked from commit befd1a0f91)

Co-authored-by: Ritwik Puri <ritwikpuri5678@gmail.com>
2023-03-16 16:20:21 +05:30
Sagar Sharma
357d4994e4 Merge branch 'develop' into fix-22-23-05686 2023-03-14 20:59:38 +05:30
Sagar Sharma
ea269c6a1c Merge branch 'develop' into FIX-34354 2023-03-14 20:59:27 +05:30
Ankush Menat
f1752fcd7a Merge pull request #34116 from prateekkaramchandani/develop
fix: use max function to get default company address
2023-03-14 19:38:26 +05:30
Ankush Menat
e0042972c8 test: add test for primary address sorting 2023-03-14 18:50:19 +05:30
Sagar Sharma
e9f5ea6ede Merge pull request #34415 from s-aga-r/FIX-ISS-22-23-05720
fix: operation time for multi-level BOM in WO
2023-03-14 18:47:40 +05:30
Ankush Menat
55489d0056 Merge branch 'develop' into prateekkaramchandani/develop 2023-03-14 18:39:06 +05:30
Sagar Sharma
e0b288fbf0 Merge branch 'develop' into FIX-34354 2023-03-14 18:19:38 +05:30
Sagar Sharma
258242f8d7 Merge branch 'develop' into FIX-ISS-22-23-05720 2023-03-14 18:17:48 +05:30
Ankush Menat
f95ad039e4 test: add timeout to all BOM related tests (#34446)
* Revert "chore: remove failing test (#34444)"

This reverts commit b89ecd482d.

* test: add timeout to bom tests
2023-03-14 18:10:17 +05:30
Deepesh Garg
b89ecd482d chore: remove failing test (#34444) 2023-03-14 15:53:03 +05:30
Johannes Obermeier
70c78d0d67 fix: incorrect EAN validation, EAN can be an EAN8, EAN12 or EAN13 code (#34250)
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-03-14 15:27:05 +05:30
s-aga-r
22ad9a1903 chore: Allow Zero Valuation Rate msg in SE 2023-03-14 14:33:03 +05:30
marination
d6d0163514 fix: Provision to apply early payment discount if payment is recorded late
- Party could have paid on time but payment is recorded late
- Prompt for reference date so that discount is applied while mapping
- Prompt only if discount in payment schedule of valid doctypes
- test: Reference date and impact on PE
- `make_payment_entry` (JS) must be able to access `this`
2023-03-14 14:22:39 +05:30
Sagar Sharma
0dbf4557db Merge branch 'develop' into FIX-ISS-22-23-05720 2023-03-14 13:19:58 +05:30
Sagar Sharma
185a8910d2 Merge branch 'develop' into fix-22-23-05686 2023-03-14 13:19:46 +05:30
Anand Baburajan
fe672aa9b1 Merge pull request #34392 from AnandBaburajan/patch_assdeprsch_gpa_ndb
fix: add missing patch for new fields added in #34214
2023-03-14 12:18:29 +05:30
Sagar Sharma
6c88fb7a8d Merge branch 'develop' into fix-22-23-05686 2023-03-14 11:44:39 +05:30
Sagar Sharma
48a88b7064 Merge branch 'develop' into FIX-ISS-22-23-05720 2023-03-14 11:44:33 +05:30
Anand Baburajan
72e9c05f46 Merge branch 'develop' into patch_assdeprsch_gpa_ndb 2023-03-13 21:31:57 +05:30
Deepesh Garg
c6999fc687 fix: Total row in trail balance report (#34395)
* fix: Total row in trail balance report

* fix: Calculate total after preparing opening and closing
2023-03-13 21:18:30 +05:30
Deepesh Garg
91f7b0bbd6 Merge branch 'develop' of https://github.com/frappe/erpnext into opening_entry 2023-03-13 20:52:48 +05:30
Deepesh Garg
76775a3e49 fix: Update patch 2023-03-13 20:51:35 +05:30
Deepesh Garg
492e994c22 Merge branch 'develop' into patch_assdeprsch_gpa_ndb 2023-03-13 19:37:55 +05:30
Deepesh Garg
fa776d2987 fix: Use customer name instead of name(id) in PSOA (#34412) 2023-03-13 19:37:09 +05:30
Deepesh Garg
d8e54a21fb test: Update values in Sales Invoice tests (#34419) 2023-03-13 19:34:15 +05:30
Sagar Vora
5c06620f97 fix: set tax category from address before executing get_regional_address_details (#34372) 2023-03-13 19:32:50 +05:30
HENRY Florian
d267111e13 chore: fix french translation (#34381)
chore: update french translation
2023-03-13 19:03:16 +05:30
Ernesto Ruiz
796ad01b87 fix: Add translate function to Delayed Tasks Summary Report (#34411)
fix: Add translate function to Delayed Tasks Summary Report
2023-03-13 19:02:34 +05:30
Deepesh Garg
c8cc3fc65f chore: Move source and campaign to additional info section (#34414) 2023-03-13 18:37:19 +05:30
Deepesh Garg
4416ddc4af fix: Linked invoice cancellation issue via timesheet (#34337) 2023-03-13 18:36:43 +05:30
Sagar Sharma
4a05f3736f Merge branch 'develop' into FIX-ISS-22-23-05720 2023-03-13 17:50:09 +05:30
s-aga-r
66f650061d test: add test cases for Over Order Allowance against Blanket Order 2023-03-13 17:21:07 +05:30
Sagar Sharma
9d49afebde Merge branch 'develop' into fix-22-23-05686 2023-03-13 17:05:27 +05:30
Marica
e1d28063b0 Merge branch 'develop' into early-payment-loss 2023-03-13 16:23:24 +05:30
Anand Baburajan
b8703b0b9b Merge branch 'develop' into patch_assdeprsch_gpa_ndb 2023-03-13 15:35:08 +05:30
Ankush Menat
b8a61be080 test: fix hypothesis tests (#34416) 2023-03-13 15:16:30 +05:30
Anand Baburajan
097be8bdea Merge branch 'develop' into patch_assdeprsch_gpa_ndb 2023-03-13 15:08:08 +05:30
Sagar Sharma
f2e9a67f37 Merge branch 'develop' into FIX-ISS-22-23-05720 2023-03-13 14:32:26 +05:30
s-aga-r
442ee3adba fix: operation time for multi-level BOM in WO 2023-03-13 14:31:38 +05:30
Ankush Menat
5bf6fb43a9 chore: fix broken TR translations 2023-03-13 14:12:12 +05:30
Deepesh Garg
7101e7acc4 fix: Filtering issue in payment entry (#34332) 2023-03-13 14:05:13 +05:30
marination
caa1a3dccf fix: Handle rounding more gracefully
- Round off pending discount loss to avoid miniscule losses rounded to 0.0 that are added in deductions
- Use base amounts to calculate base losses instead of using conversion factor which increases rounding error
- Round of total base loss instead of individual income and tax losses to reduce rounding error
- Use default round off account for pending rounding loss in deductions
2023-03-13 14:01:31 +05:30
ruthra kumar
9f26e3c27a Merge pull request #34408 from ruthra-kumar/delete_remarks_migration_patch
chore: delete remarks migration patch
2023-03-13 09:14:21 +05:30
ruthra kumar
da37573b73 chore: delete remarks migration patch
Versions higher than V14.18.2, 'remarks' will be moved in 'migrate_gl_to_payment_ledger'
2023-03-12 13:41:50 +05:30
ruthra kumar
e371bb6672 Merge pull request #34387 from ruthra-kumar/refactor_gl_migration_patch
refactor(patch): remove inner join to improve SQL performance
2023-03-11 21:00:19 +05:30
Mehmet Demirel
fa6d37542b chore: Improve Turkish language translation
chore: Improve Turkish language translation
2023-03-11 19:18:44 +05:30
Deepesh Garg
aae53bb910 fix: Error in consolidated financial statement (#34330) 2023-03-11 19:06:56 +05:30
ruthra kumar
a9989b12d5 Merge pull request #34370 from ruthra-kumar/fix_flaky_test_in_payment_terms_report
fix(test): flaky test case in Payment terms report
2023-03-11 14:17:45 +05:30
ruthra kumar
9d0a1149d8 chore: remove remarks migrations patch from patches.txt
'Remarks' field is moved in migrate_gl_to_payment_ledger patch itself
from versions highers than v14.18.2. Removing it from patches.txt
2023-03-11 13:17:52 +05:30
ruthra kumar
a43304b01b Merge branch 'develop' into fix_flaky_test_in_payment_terms_report 2023-03-11 13:05:38 +05:30
ruthra kumar
7fcd74ed03 fix(test): flaky test case in Payment terms report 2023-03-10 21:23:12 +05:30
ruthra kumar
1744f1d4e4 refactor: add remarks to column as well 2023-03-10 15:57:11 +05:30
Anand Baburajan
6308b509fc Merge branch 'develop' into patch_assdeprsch_gpa_ndb 2023-03-10 15:25:39 +05:30
Deepesh Garg
0157fa15eb chore: Use account closing balance in set gl entries 2023-03-10 13:02:01 +05:30
Sagar Sharma
430378bb6e Merge pull request #34383 from s-aga-r/FIX-ISS-22-23-05836
fix: filters not getting applied on `Web Form`
2023-03-10 00:24:08 +05:30
s-aga-r
9c1e566394 fix: filters not getting applied on Web Form 2023-03-09 22:12:35 +05:30
ruthra kumar
f9cfabf78e refactor(patch): remove inner join to improve SQL performance 2023-03-09 19:42:28 +05:30
Marica
ae0a8a6529 Merge branch 'develop' into early-payment-loss 2023-03-09 18:36:36 +05:30
anandbaburajan
ebb6953274 fix: add missing patch for new fields added in #34214 2023-03-09 17:55:27 +05:30
marination
9abf0ef615 test: Multi currency SI with multi-currency accounting and single currency accounting + Early payment discount 2023-03-09 16:32:36 +05:30
marination
b09c2381ca fix: Multi-currency SI with base currency PE
- Return total discount loss in base currency
- Allocate payment based on terms: Set allocated amount in references table in base currency if accounting is in that currency
- Allocate payment based on terms: While back updating set paid amount (payment schedule) in transaction currency always
- minor: discount msgprint in correct currency
2023-03-09 16:25:45 +05:30
Deepesh Garg
9a8f8e8b7d Revert "fix: Default sales team not getting set" (#34376)
Revert "fix: Default sales team not getting set (#34284)"

This reverts commit 7d0199d743.
2023-03-09 15:36:52 +05:30
Sagar Sharma
428274df56 Merge pull request #34360 from s-aga-r/FIX-ITEM-ALTERNATIVE-ERR-MSG
chore: `Alternative Item Code` error msg
2023-03-09 10:07:35 +05:30
Sagar Sharma
87b5734f5b Merge branch 'develop' into FIX-ITEM-ALTERNATIVE-ERR-MSG 2023-03-09 10:07:24 +05:30
Sagar Sharma
c312692af5 Merge pull request #34362 from s-aga-r/FIX-ISS-22-23-05697
fix: `required_qty` get reset to `1` for Alternative Item in WO
2023-03-09 10:05:08 +05:30
s-aga-r
046834a97a fix: required_qty get reset to 1 for Alternative Item in WO 2023-03-08 20:40:03 +05:30
s-aga-r
baef5ae1ef chore: Alternative Item Code error msg 2023-03-08 20:07:30 +05:30
Deepesh Garg
719ac5c8f3 Merge branch 'develop' of https://github.com/frappe/erpnext into opening_entry 2023-03-08 18:35:26 +05:30
Deepesh Garg
f0267feca8 test: Add test case for closing balance 2023-03-08 18:34:07 +05:30
marination
761f68d7bf fix: Paid amount must be discounted considering accounting currency
- Accounting is in the same currency if party currency and company currency is the same
- If accounting is in the same currency, paid and recvd amount is in the base currency
- Then, discount amount must also be in the base currency as it is deducted from paid amount
- Received amount must be in base currency if not multi currency
- cleanup: Deductions setting broken into smaller functions
2023-03-08 17:20:48 +05:30
Sagar Sharma
a1ad9587fa Merge pull request #34352 from s-aga-r/FIX-ISS-22-23-05722
fix: `BOM Stock Report`
2023-03-08 16:26:19 +05:30
Sagar Sharma
2d66384cbc Merge branch 'develop' into FIX-ISS-22-23-05722 2023-03-08 16:25:42 +05:30
Ankush Menat
52ab11389b chore: drop hypothesis dependency
pinned in framework now
2023-03-08 15:31:25 +05:30
Deepesh Garg
3249a79f07 chore: rename Closing Balance to Account Closing Balance 2023-03-08 15:15:33 +05:30
s-aga-r
b53dcb04ed test: add test cases for BOM Stock Report 2023-03-08 14:00:06 +05:30
s-aga-r
a65b80911b fix: BOM Stock Report 2023-03-08 12:59:50 +05:30
Sagar Sharma
ba1162ff3f Merge pull request #34313 from s-aga-r/fix-22-23-05690
perf: Stock Entry (Material Transfer)
2023-03-07 18:03:08 +05:30
Sagar Sharma
336e0b9666 Merge branch 'develop' into fix-22-23-05690 2023-03-07 18:02:52 +05:30
Solufyin
71de72bdd0 fix: Set contact filter link in Opportunity (#34325)
Co-authored-by: Nihantra C. Patel <n.patel.serpentcs@gmail.com>
2023-03-07 17:44:31 +05:30
Devin Slauenwhite
6de826b8c4 fix: exchange rate revaluation errors (#33947)
* fix: set new balance for non-positive balances

* fix: don't add debit: 0, credit: 0 entries to journal entry.

* fix: add journal entry difference to unbooked gain/loss of exchange.

* chore: linter

* chore: remove invlaid TODO. [skip-ci]
2023-03-07 17:16:15 +05:30
Sagar Sharma
4d1f620d75 Merge branch 'develop' into fix-22-23-05690 2023-03-07 16:35:17 +05:30
Marica
2feb27e399 fix(minor): Dirty the form after clicking on Get advances button in Invoices (#34323)
fix(minor): Dirty form after clicking on Get advances button
2023-03-07 15:45:18 +05:30
Sagar Sharma
4cd16a839b Merge branch 'develop' into fix-22-23-05690 2023-03-07 14:55:46 +05:30
marination
f02fc8acf0 fix: Don't add to deductions if amount is 0
- misc: better docstring
2023-03-07 12:12:45 +05:30
Deepesh Garg
10632d75b0 fix: Do not calculate commission post submit (#34267)
* fix: Do not calculate commision post submit

* chore: Update condition to match server side logic

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

---------

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-03-07 11:44:09 +05:30
Deepesh Garg
7d0199d743 fix: Default sales team not getting set (#34284) 2023-03-07 11:41:26 +05:30
Deepesh Garg
ea8e23384d fix: Payment Request against sales order with disabled rounded total (#34281)
* fix: Payment Request against sales order with disabled rounded total

* chore: Do not consider advance amount
2023-03-07 11:40:29 +05:30
Sagar Sharma
3952b3373d Merge branch 'develop' into fix-22-23-05690 2023-03-07 10:50:39 +05:30
s-aga-r
8ad9e99cea perf: update_completed_qty() in material_request.py 2023-03-07 10:50:11 +05:30
Raffael Meyer
ee12313dbb Merge branch 'develop' into early-payment-loss 2023-03-06 21:41:54 +01:00
s-aga-r
de18f98c5c perf: Stock Entry (Material Transfer) 2023-03-06 23:41:04 +05:30
rohitwaghchaure
53efc8ff77 Merge pull request #34305 from rohitwaghchaure/fixed-not-able-to-complete-bom-update-log
fix: BOM Update log not completed
2023-03-06 23:23:58 +05:30
Sagar Sharma
04b346865f Merge pull request #34301 from barredterra/rename-timezone-utils
refactor: use renamed timezone utils
2023-03-06 22:22:23 +05:30
marination
7f2e7badff fix: Set deduction amount in company currency on Doctype
- Even via JS, deductions amount is always in company currency
- Since there is nothing dynamic about this field, set it in the doctype spec itself
- fixed: Inconsistency between label currency and field currency formatted value
2023-03-06 17:45:26 +05:30
Deepesh Garg
5dabc98ba5 chore: Add index to period closing voucher column 2023-03-06 16:56:33 +05:30
Marica
b5f80815a1 Merge branch 'develop' into early-payment-loss 2023-03-06 15:50:50 +05:30
marination
c217bb2018 test: PE from SI with early payment discount amount & PE assertions in discount % test 2023-03-06 15:02:32 +05:30
marination
2ae5834290 fix: Back update discounted amount in Invoice based on discount type
- Discount value was always trated as a percentage on back updation
2023-03-06 14:54:46 +05:30
Rohit Waghchaure
2f157fa5d3 fix: BOM Update log not completed 2023-03-06 12:09:50 +05:30
Deepesh Garg
310f71c313 test: Add static posting dates to tests 2023-03-05 18:43:26 +05:30
Deepesh Garg
4a2046dfb6 fix: Aggregation with previous closing balance 2023-03-05 18:18:33 +05:30
barredterra
502a37a864 refactor: use renamed timezone utils
https://github.com/frappe/frappe/pull/20253
2023-03-04 19:31:40 +01:00
Sagar Sharma
ac87fca0d7 Merge pull request #34299 from s-aga-r/fix-22-23-05638-2
fix: Stock Reconciliation `actual_qty`
2023-03-04 17:35:05 +05:30
s-aga-r
70de444b7b fix: Stock Reconciliation actual_qty 2023-03-04 17:06:07 +05:30
Sagar Sharma
0185096e09 Merge pull request #34298 from s-aga-r/fix-22-23-05638-1
fix: update inventory dimensions before returning sle
2023-03-04 16:50:55 +05:30
s-aga-r
e6a02719f7 fix: update inventory dimensions before returning sle 2023-03-04 16:02:50 +05:30
Sagar Sharma
31ed4ef6d6 Merge pull request #34293 from s-aga-r/fix-22-23-05638
fix: `Inventory Dimension` for `Stock Reconciliation`
2023-03-04 02:13:13 +05:30
s-aga-r
0e1b7760a8 fix: Inventory Dimension for Stock Reconciliation 2023-03-04 01:05:55 +05:30
s-aga-r
53701c37b1 feat: consider over_order_allowance while validating sales order qty 2023-03-03 22:03:54 +05:30
Devin Slauenwhite
6190b4cf63 chore: revert unrelated changes. 2023-03-03 09:42:29 -05:00
marination
dc2998f544 fix: Set deductions in base currency
- Use field precision to get more accurate values
2023-03-03 17:24:43 +05:30
marination
75ec0a0a85 fix: Recalculate difference amount after setting deductions 2023-03-03 14:13:27 +05:30
Marica
619824855a Merge branch 'develop' into early-payment-loss 2023-03-03 13:53:57 +05:30
marination
768c3a4927 fix: Taxes aren't discounted on early payment discount
- Deductions in payment entry must be split into income loss and tax loss
- Compute total discount in percentage, makes discounting different amounts proportionately easier
2023-03-03 13:21:38 +05:30
s-aga-r
d7da8928ac feat: add field Over Order Allowance (%) in Selling Settings 2023-03-03 11:19:28 +05:30
s-aga-r
8bcbc45add feat: consider over_order_allowance while validating order qty 2023-03-03 11:11:33 +05:30
s-aga-r
fc1088d9c4 fix: don't map item row having 0 qty 2023-03-03 10:50:17 +05:30
s-aga-r
f3993783a3 refactor: rewrite blanket_order.py queries in QB 2023-03-02 17:18:46 +05:30
s-aga-r
f5937f46cb feat: add field Over Order Allowance (%) in Buying Settings 2023-03-02 17:04:09 +05:30
Deepesh Garg
c3d455653e Merge pull request #34258 from deepeshgarg007/finance_book_read_only
chore: Make finance book read only
2023-03-02 16:47:47 +05:30
Deepesh Garg
c089c4156c chore: Minor fixes 2023-03-02 16:46:56 +05:30
s-aga-r
abf9a28d6a fix: hide + button based on Blanket Order Type 2023-03-02 16:40:37 +05:30
Sagar Sharma
6e43d6882a Merge pull request #34117 from s-aga-r/github-issue-33344
refactor: rewrite `get_item_details.py` queries in `QB`
2023-03-02 14:54:58 +05:30
Sagar Sharma
90e1a6bc16 Merge branch 'develop' into github-issue-33344 2023-03-02 14:24:14 +05:30
Sagar Sharma
310643d215 Merge pull request #34273 from s-aga-r/fix-return-pr-serial-no
fix: `rejected_serial_no` not getting copied from PR to PR(Return)
2023-03-02 12:37:39 +05:30
Sagar Sharma
6e9d001819 Merge branch 'develop' into github-issue-33344 2023-03-02 12:09:21 +05:30
Sagar Sharma
50dc0c18eb Merge branch 'develop' into fix-return-pr-serial-no 2023-03-02 12:06:59 +05:30
Ankush Menat
330ae419be fix!: Parse float as per number format in quality inspection (#34259)
fix: Parse float as per number format in quality inspection
2023-03-02 11:59:18 +05:30
s-aga-r
cb0b6de4b9 fix: Serial No is mandatory even if the qty is 0 2023-03-02 11:27:46 +05:30
s-aga-r
a9f0a11ce6 fix: rejected_serial_no not getting copied from PR to PR(Return) 2023-03-02 11:15:46 +05:30
Deepesh Garg
790f0aac12 Merge branch 'develop' into finance_book_read_only 2023-03-02 10:01:12 +05:30
Deepesh Garg
6a89cb98ce Merge pull request #33874 from frappe/alternative-items-quotation
feat: Support for Alternative Items in Quotation
2023-03-01 17:41:24 +05:30
Suraj Shetty
e0a3869874 Merge pull request #34266 from frappe/mergify/bp/develop/pr-34265 2023-03-01 17:06:20 +05:30
Suraj Shetty
6cd2ba5c66 fix: Resolve conflicts
(cherry picked from commit f6469d8398)
2023-03-01 10:59:21 +00:00
Suraj Shetty
46a722d51c fix: Wrap unexpectedly long text in remark
(cherry picked from commit ba66a6714c)

# Conflicts:
#	erpnext/accounts/report/general_ledger/general_ledger.html
(cherry picked from commit b13bf1ebc5)
2023-03-01 10:59:20 +00:00
rohitwaghchaure
32b64db4d6 Merge pull request #34254 from rohitwaghchaure/fixed-subcontracting-stock-availability-issue
fix: consumed qty validation for subcontracting receipt
2023-03-01 15:15:25 +05:30
Deepesh Garg
28dd1a25cb chore: Make finance book read only 2023-03-01 15:00:24 +05:30
Deepesh Garg
e5f603c9d9 perf: Apply closing balance in Trial Balance report 2023-03-01 14:48:06 +05:30
Deepesh Garg
95c9aafda9 fix: Update patch to generate closing balance entries 2023-03-01 14:45:22 +05:30
Deepesh Garg
436fc03eda fix: Closing balance entries for period closing voucher 2023-03-01 14:43:49 +05:30
Rohit Waghchaure
b38fe24090 fix: consumed qty validation for subcontracting receipt 2023-03-01 11:42:59 +05:30
rohitwaghchaure
d88fe27be6 Merge pull request #34195 from HarryPaulo/fix-performance-on-insert-item
fix: Performance improvement when adding a new item
2023-03-01 10:10:14 +05:30
ruthra kumar
f9c75055bb Merge pull request #34241 from ruthra-kumar/bug_on_credit_note_creation
fix: pos return throwing amount greater than grand total
2023-02-28 18:22:30 +05:30
rohitwaghchaure
9bb8e351a2 Merge pull request #34243 from rohitwaghchaure/fixed-date-filters-in-subcontracting-reports
fix: default date in Subcontracting reports
2023-02-28 18:01:14 +05:30
Rohit Waghchaure
dfddc4efc3 fix: default date in Subcontracting reports 2023-02-28 17:53:51 +05:30
rohitwaghchaure
2942cee31e Merge pull request #34235 from rohitwaghchaure/feat-adjust-valuation-rate
feat: adjust purchase receipt valuation rate as per purchase invoice rate
2023-02-28 17:34:53 +05:30
ruthra kumar
35c70f39fa fix: pos return throwing amount greater than grand total 2023-02-28 16:21:32 +05:30
Rohit Waghchaure
a8445da02a fix: labels name 2023-02-28 15:47:45 +05:30
s-aga-r
58c027d4cc chore: Linters 2023-02-28 15:14:34 +05:30
Sagar Sharma
26ced47ddc Merge branch 'develop' into github-issue-33344 2023-02-28 15:01:40 +05:30
Rohit Waghchaure
8e86553717 test: added test cases 2023-02-28 14:57:04 +05:30
Sagar Sharma
90abb28cb4 Merge pull request #34060 from vishdha/pos_fix
fix: multiple Point of Sale conversion issue resolved
2023-02-28 14:55:14 +05:30
Rohit Waghchaure
eab775ef32 feat: adjust purchase receipt valuation rate as per purchase invoice rate 2023-02-28 14:11:55 +05:30
Vishal
f18ae5856f chore: minor changes in pos_controller 2023-02-28 11:03:26 +05:30
Vishal
a51bec0269 chore: minor change 2023-02-28 11:03:26 +05:30
Vishal
3ebe7d861d chore: minor changes added to code 2023-02-28 11:03:26 +05:30
Vishal
1de531e56e fix: multiple pos conversion issue resolved 2023-02-28 11:03:25 +05:30
Sagar Sharma
ecd15d2a65 Merge branch 'develop' into github-issue-33344 2023-02-27 16:57:29 +05:30
Sagar Sharma
9cd42dd60f Merge pull request #34225 from s-aga-r/fix-mr-warehouse
fix: set `from_warehouse` and `to_warehouse` while mapping SE
2023-02-27 12:52:53 +05:30
s-aga-r
c09a61f360 fix: set from_warehouse and to_warehouse while mapping SE 2023-02-27 12:26:21 +05:30
Sagar Sharma
c22641092a Merge pull request #34212 from brian-pond/patch-2
fix: Remove missing DocField in fetch_from
2023-02-27 11:52:53 +05:30
Sagar Sharma
bda1340760 Merge branch 'develop' into patch-2 2023-02-27 11:52:31 +05:30
Patrick Eissler
bbb6a62a7d chore: add german translations (#34167)
* chore: add german translations

* Apply suggestions from code review

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

---------

Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
2023-02-27 11:49:22 +05:30
Deepesh Garg
81651beaef Merge pull request #34174 from Vishnu7025/coa-import-currency
fix: currency in coa import
2023-02-27 11:48:44 +05:30
Sagar Sharma
5dcb723fc7 Merge branch 'develop' into patch-2 2023-02-27 11:14:14 +05:30
Brian Pond
83f3e317e1 fix: Remove missing DocField in fetch_from 2023-02-27 11:10:51 +05:30
ruthra kumar
d8b9b83b11 Merge pull request #34207 from ruthra-kumar/fix_permisssion_error_on_work_order_creation
fix: permission error while calling get_work_order_items
2023-02-27 09:55:18 +05:30
Anand Baburajan
9e38b61bd9 Merge pull request #34214 from AnandBaburajan/manual_asset_schedule_develop
fix: asset manual depr schedule [develop]
2023-02-26 20:22:34 +05:30
Anand Baburajan
feeebb1262 Merge branch 'develop' into manual_asset_schedule_develop 2023-02-26 17:40:26 +05:30
anandbaburajan
dd74839eba fix: manual depr schedule 2023-02-26 17:36:36 +05:30
Deepesh Garg
a663df376c fix: Add patch to update closing balances 2023-02-26 16:06:04 +05:30
Deepesh Garg
6607c8bd82 fix: Order by issue in aggregation query 2023-02-26 16:05:26 +05:30
Deepesh Garg
f92c63fb10 feat: Add validations against period closing voucher 2023-02-26 15:53:33 +05:30
Deepesh Garg
7fa7d6b5e4 chore: Rewrite query using query builder 2023-02-26 15:52:30 +05:30
Deepesh Garg
e18336ebe7 feat: Add views in standard filter 2023-02-26 15:47:29 +05:30
rohitwaghchaure
6d6e3cb015 Merge pull request #34206 from rohitwaghchaure/fixed-trivial-error-for-stock-reposting
fix: not able to repost gl entries
2023-02-24 21:10:27 +05:30
ruthra kumar
a11d3327df fix(test): use standalone method to fetch work orders from SO 2023-02-24 21:07:49 +05:30
ruthra kumar
b6bad728cd fix: permission error while calling get_work_order_items 2023-02-24 21:07:46 +05:30
Rohit Waghchaure
7d10dd9ea8 fix: not able to repost gl entries 2023-02-24 17:50:00 +05:30
Deepesh Garg
e26c6dc76b Revert "fix: Concurrency issues in Sales and Purchase returns" (#34202)
Revert "fix: Concurrency issues in Sales and Purchase returns (#34019)"

This reverts commit a67284e96d.
2023-02-24 15:28:14 +05:30
rohitwaghchaure
04f00fd141 Merge pull request #34199 from rohitwaghchaure/fixed-converison-factor-issue
fix: conversion factor not set
2023-02-24 14:56:36 +05:30
Rohit Waghchaure
8e46aebc50 fix: conversion factor not set 2023-02-24 14:49:05 +05:30
HarryPaulo
49af5ba434 fix: Performance improvement when adding a new item 2023-02-23 18:11:09 -03:00
rohitwaghchaure
670304fe8e Merge pull request #34189 from rohitwaghchaure/prevent-to-make-item-price-for-template
fix: user shouldn't able to make item price for item template
2023-02-23 20:47:48 +05:30
Rohit Waghchaure
6417ae0ee8 fix: user shouldn't able to make item price for item template 2023-02-23 20:21:50 +05:30
Deepesh Garg
c3f39c3f32 feat: Cascade closing balances on PCV submit 2023-02-23 16:48:54 +05:30
Deepesh Garg
36c08d0835 fix: Add patches to create accounting dimension in Closing Balance 2023-02-23 16:46:37 +05:30
Marica
47ab26dedf Merge branch 'develop' into alternative-items-quotation 2023-02-23 13:20:28 +05:30
ruthra kumar
a9920715ab Merge pull request #34022 from ruthra-kumar/gross_profit_memory_and_performance_issue
perf: Gross Profit report will fetch SLE's on demand and memoize
2023-02-23 11:55:08 +05:30
ruthra kumar
88d888d9d0 refactor: use docstatus from Delivery Note Item 2023-02-23 11:15:56 +05:30
rohitwaghchaure
3692f960d0 Merge pull request #34172 from rohitwaghchaure/zero-rate-adjust-issue-using-lcv
fix: zero division error while making LCV
2023-02-23 10:54:01 +05:30
ruthra kumar
60d3b86cdb Merge pull request #34176 from ruthra-kumar/fix_ui_freeze_on_sales_invoice
fix: ui freeze on item selection in sales invoice
2023-02-23 10:35:50 +05:30
rohitwaghchaure
e3de216e06 Merge pull request #34173 from rohitwaghchaure/incorrect-color-showing-in-report
fix: incorrect color in the BOM Stock Report
2023-02-23 09:54:45 +05:30
ruthra kumar
6412583e98 fix: ui freeze on item selection in sales invoice 2023-02-23 09:36:21 +05:30
vishnu
e3c000d0be chore: change column label 2023-02-22 10:53:11 +00:00
vishnu
19c0b7a523 fix: currency in coa import 2023-02-22 10:24:45 +00:00
Rohit Waghchaure
a8f03ebf7f fix: incorrect color in the BOM Stock Report 2023-02-22 14:24:18 +05:30
Rohit Waghchaure
80e94a08cf fix: zero division error while making LCV 2023-02-22 13:29:06 +05:30
Deepesh Garg
b44a19bd1a feat: Introduce opening entry for reporting 2023-02-21 23:20:28 +05:30
Sankara Subramanian V
a06d24037b refactor(region): Splitting of KSA(Saudi Arabia) Regional logic from ERPNext (#33895)
* feat: remove KSA regional code
2023-02-21 22:52:20 +05:30
ruthra kumar
dde5bbc9a7 Merge pull request #34102 from ruthra-kumar/pos_validate_for_duplicate_pos_invoices
fix: check for duplicate pos invoices in closing entry
2023-02-21 18:45:39 +05:30
ruthra kumar
47add0b751 fix: check for duplicate in pos closing and pos merge log entry 2023-02-21 17:50:57 +05:30
Deepesh Garg
9739d8b52a feat: Introduce opening entry for reporting 2023-02-21 15:23:29 +05:30
ruthra kumar
4a7b1de2d8 refactor: clear records in batches in 'Transaction Deletion Record' (#34109)
refactor: clear records in batches
2023-02-21 14:27:00 +05:30
Deepesh Garg
c88444a6c4 fix: Filters in item-wise sales history report (#34145) 2023-02-21 14:26:33 +05:30
Anand Baburajan
5e3a034395 Merge pull request #34153 from AnandBaburajan/fixed_asset_report_fiscal_year
fix: fiscal year error for existing assets in fixed asset register
2023-02-21 13:54:54 +05:30
Anand Baburajan
b781c57dcb Merge branch 'develop' into fixed_asset_report_fiscal_year 2023-02-21 13:10:07 +05:30
Himanshu Shivhare
3547252217 Telegram Group link updated (#34149) 2023-02-21 13:05:03 +05:30
anandbaburajan
76861eb332 fix: fiscal year error for existing assets in fixed asset register 2023-02-21 13:04:01 +05:30
Deepesh Garg
35cdd996a9 fix: Use normal rounding for Tax Withholding Category (#34114) 2023-02-21 12:57:49 +05:30
Sagar Sharma
18541ef54f Merge pull request #34138 from s-aga-r/dn-create-button
fix(ux): `ReferenceError: me is not defined` Delivery Note
2023-02-21 10:26:52 +05:30
Sagar Sharma
77b88961f6 Merge branch 'develop' into dn-create-button 2023-02-21 10:19:09 +05:30
marination
6b789e2f04 fix: Map only non alternative items from Quotation in Sales Invoice
- Since there's no item selection, only Quotation selection :/
2023-02-20 21:17:43 +05:30
marination
19456127cf fix: Handle Get Items From in Sales Order
- Map all non alternatives from Quotation to SO if no selected items
- Show disclaimer mentioning that Qtns with alternatives must be mapped to SO from the Qtn form
2023-02-20 20:52:14 +05:30
Ankush Menat
3f2a2c96f6 fix: Add company perm check unconditionally (#34142)
fix: Add employee perm check unconditionally
2023-02-20 16:07:27 +05:30
rohitwaghchaure
5e9620b98b Merge pull request #34139 from rohitwaghchaure/fixed-stock-ledger-report-filter-issue
fix: inventory dimension filter not overriding with existing filter for stock ledger report
2023-02-20 13:04:23 +05:30
rohitwaghchaure
3871cf7110 Merge pull request #33722 from rohitwaghchaure/purchase-invoice-performance-issue
fix: purchase invoice performance issue
2023-02-20 12:47:07 +05:30
Rohit Waghchaure
0e388ba872 fix: inventory dimension filter not overriding with existing filter for stock ledger report 2023-02-20 12:28:43 +05:30
Marica
3c96791d52 fix: Use block variable
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-02-20 12:04:14 +05:30
s-aga-r
44ee9f0f19 chore: Linters 2023-02-20 11:43:32 +05:30
s-aga-r
1b010add26 fix(ux): ReferenceError: me is not defined Delivery Note 2023-02-20 11:17:49 +05:30
Sagar Sharma
5bbe8728ad Merge branch 'develop' into github-issue-33344 2023-02-20 10:47:29 +05:30
Ankush Menat
e84e778151 chore(deps): remove stdnum dependency (#34137) 2023-02-20 10:40:07 +05:30
Sagar Sharma
c8fda8a8b5 Merge branch 'develop' into github-issue-33344 2023-02-19 22:17:20 +05:30
Alirio Castro
183e42af1a fix: differency entry journal debit/credit missing (#34104)
* fix: difference entry journal is wrong

* fix: difference entry journal is wrong
2023-02-19 19:40:13 +05:30
Johannes Obermeier
2a90fad710 feat(stock): Support more barcodes in an item by validate the barcode with the barcodenumber module (#33863)
feat(stock): Support more barcodes in an article by validate the barcode with the barcodenumber module
2023-02-19 19:36:05 +05:30
Sagar Sharma
2959285c82 Merge branch 'develop' into fix-reserve-qty 2023-02-19 12:45:39 +05:30
Sagar Sharma
f342e6e765 Merge branch 'develop' into github-issue-33344 2023-02-19 12:17:33 +05:30
ruthra kumar
c722f2819c fix: ignore repost payment ledger on basic documents cancellation (#34054)
fix: ignore repost payment ledger on cancel/delete of Inv/Pay/JE's

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-02-19 12:09:10 +05:30
Sagar Sharma
2c3973156f Merge branch 'develop' into github-issue-33344 2023-02-19 12:07:27 +05:30
Richard Case
5b7d23de15 feat: bank reconciliation and plaid changes (#33986)
fix: plaid link refresh: update account ids
fix: plaid transactions for credit cards & add accounts on link refresh if they don't exist
fix: bank reconciliation amount matching
fix: bank reconciliation dialog usability
feat: rewrite bank transaction reconciliation to allow multiple transactions to reconcile against vouchers before clearance
fix: matching transaction amounts and race condition bug
fix: ensure there is a reference number in plaid transactions and other tweaks
feat: add references to Payroll Entry Bank Journal Entry
feat: only clear Voucher once all Bank GLEs are allocated to Bank Transactions
fix: strange type error
feat: add payment method field to bank and plaid transactions and prepopulate relevant bank reconciliation new voucher fields
feat: bank reconciliation - allow bank transactions to reconcile against themselves for when there are banking amendments
fix: bank transaction self-reconcile bug and tidy
fix: bank reconciliation datatable index update
2023-02-19 11:50:17 +05:30
Shadrak Gurupnor
5e48e61c66 fix: forced delete linked desktop_icons (#34107)
* fix: forced delete linked desktop_icons

* fix: black
2023-02-19 11:42:35 +05:30
ruthra kumar
9c6466f15b fix: consider rounded total amount while making payment request (#34110) 2023-02-19 11:41:41 +05:30
Devin Slauenwhite
c76352340d fix(ux): customer/supplier docfield layout (#34125) 2023-02-19 11:37:32 +05:30
Sagar Sharma
07c6ad6fa7 Merge branch 'develop' into github-issue-33344 2023-02-18 22:24:06 +05:30
s-aga-r
35489fbbf9 refactor: remove method get_serial_no_batchwise from get_item_details.py 2023-02-18 22:23:53 +05:30
s-aga-r
6b144baa69 refactor: rewrite get_item_details.py queries in QB 2023-02-18 22:23:51 +05:30
rohitwaghchaure
dff9655d46 Merge pull request #34115 from rohitwaghchaure/fixed-convert-to-item-warehouse-reposting
feat: provision to convert transaction based reposting to item wareho…
2023-02-18 21:31:20 +05:30
Rohit Waghchaure
f1383b5ef9 feat: provision to convert transaction based reposting to item warehouse based reposting 2023-02-18 19:31:48 +05:30
Sagar Sharma
c292859b59 Merge pull request #34077 from s-aga-r/purchase-order-portal
fix: show Purchase Order Portal `Pay` button based on configuration
2023-02-18 12:20:14 +05:30
Sagar Sharma
91983fb4c6 Merge branch 'develop' into purchase-order-portal 2023-02-17 23:17:00 +05:30
Prateek
b93c18bd4a fix: use max function to get default company address 2023-02-17 20:25:13 +05:30
rohitwaghchaure
47264481ad Merge branch 'develop' into purchase-invoice-performance-issue 2023-02-17 16:41:30 +05:30
Anand Baburajan
6b3028d77b Merge pull request #34111 from AnandBaburajan/asset_fixes_17_feb
fix: repair status after deletion, asset status after manual depr entry and other misc bugs [develop]
2023-02-17 16:12:45 +05:30
anandbaburajan
e50bd52162 fix: asset repair status after deletion and asset status after manual depr entry 2023-02-17 15:32:24 +05:30
Sagar Sharma
ee21850ab8 Merge branch 'develop' into purchase-order-portal 2023-02-17 11:08:30 +05:30
ruthra kumar
067f8c1d70 Merge pull request #34071 from ruthra-kumar/rearrange_patch_order
patch: reorder migration patches
2023-02-17 11:04:36 +05:30
ruthra kumar
68202639f5 chore: reorder migration patches.
'migrate_gl_to_payment_ledger',
'migrate_remarks_from_gl_to_payment_ledger' should always run last.
2023-02-17 10:03:55 +05:30
rohitwaghchaure
4399a93843 Merge pull request #34091 from rohitwaghchaure/fixed-incorrect-consumed-qty-in-scr
fix: incorrect consumed qty in subcontracting receipt
2023-02-16 15:10:14 +05:30
Sagar Sharma
758f47bba6 Merge branch 'develop' into purchase-order-portal 2023-02-16 14:28:05 +05:30
Sagar Sharma
70660191a6 Merge pull request #34090 from frappe/update-codeowners
chore: update `CODEOWNERS`
2023-02-16 14:26:11 +05:30
Rohit Waghchaure
156e45970a fix: incorrect consumed qty in subcontracting receipt 2023-02-16 14:17:27 +05:30
Sagar Sharma
fb5796f18f Merge branch 'develop' into purchase-order-portal 2023-02-16 11:22:19 +05:30
Sagar Sharma
471e77b931 Merge branch 'develop' into purchase-order-portal 2023-02-15 22:46:07 +05:30
Sagar Sharma
421e34a181 Merge branch 'develop' into purchase-order-portal 2023-02-15 21:49:09 +05:30
Sagar Sharma
46afbb594a Merge branch 'develop' into fix-reserve-qty 2023-02-15 21:37:30 +05:30
s-aga-r
20bdc63b03 fix: show Purchase Order Portal Pay button based on configuration 2023-02-15 20:14:39 +05:30
marination
74fab53e28 test: Alternative items in Quotation
- Taxes and totals, mapping, back updation
2023-02-15 15:04:00 +05:30
s-aga-r
d7ef5ad955 chore: add field show_pay_button in Buying Settings 2023-02-15 14:43:10 +05:30
Marica
22010b745c Merge branch 'develop' into alternative-items-quotation 2023-02-15 13:05:33 +05:30
Devin Slauenwhite
4c5147f270 Merge: upstream/develop into pps190/fix-reserve-qty 2023-02-14 10:22:54 -05:00
Deepesh Garg
ff4ae69408 Merge branch 'develop' into gross_profit_memory_and_performance_issue 2023-02-13 15:38:26 +05:30
ruthra kumar
3e5691072a perf: fetch SLE's on demand and memoize 2023-02-12 14:49:17 +05:30
Devin Slauenwhite
5685492995 Merge remote-tracking branch 'frappe/develop' into fix-reserve-qty
# Conflicts:
#	erpnext/stock/doctype/delivery_note/test_delivery_note.py
2023-02-09 13:48:18 -05:00
Marica
bd7e6e264e Merge branch 'develop' into alternative-items-quotation 2023-02-06 16:30:10 +05:30
marination
db2076db69 refactor: Order based alternative items mapping
- Alternatives must be followed by a non-alternative item row
- On submit, store non-alternative rows in hidden checkbox to avoid recomputation
- Check for valid/mappable rows by row name
- UI: Select from table rows.Add single row for original/alternative item in dialog
- UI: Indicator for alternative items in dialog grid
- UI: Indicator legend and description of table
- DB: Added check field 'Has Alternative Item' not to be confused with 'Has Alternative' in Mfg
2023-02-06 16:25:38 +05:30
marination
03321f5f13 chore: Code simplification
- Map is not required, avoid filter multiple times, use single loop instead
- Better variable name
- Reduce LOC
2023-02-02 17:28:03 +05:30
marination
b3fe7c6dad fix: Consider only ordered alternative/original item for Quotation status
- The original and its alternatives make a set of items where one is chosen
- While setting order status of Quotation, check if the chosen item from the set is fully ordered or not
- Filter out unselected items from the set
- Create a map containing the set of items and if they were ordered or not for ease of grouping
- The simple items will work as it used to
2023-02-01 19:10:32 +05:30
marination
ece6358e60 fix: Iterate over list instead of map's output and formatting 2023-01-31 17:13:05 +05:30
marination
fa9b327501 chore: Validate 'alternative_to' field values, must be a valid non-alterntaive item from table 2023-01-30 16:27:01 +05:30
marination
94cacb60de feat: Filter rows to be mapped on server side mapping function
- Pass dialog selections to `make_sales_order`
- Map either original item or its alternative depending on mapping
- Only qty check for simple rows (without alternatives and not an alternative itself)
2023-01-30 13:54:30 +05:30
marination
cef7dfd0b4 feat: Dialog to select alternative item before creating Sales order
- Users can leave the row blank in the dialog if original item is to be used
- Else users can select an alternative item against an original item
- In the document, users must check `Is Alternative Item` if needed and also specify which item it is an altenrative to since there are no documented mappings
2023-01-26 14:36:25 +05:30
marination
f19eadab9a feat: Consider filtered items table in JS for totals computation
- Set `_items` as filtered rows if quotation else the entire table. Set at entry point of JS API
- Use `_items` instead of `items` to compute taxes and charges. Exclude alternative item rows
2023-01-25 13:10:03 +05:30
Rohit Waghchaure
92d857d49c fix: purchase invoice performance issue 2023-01-24 22:32:07 +05:30
marination
91982d1e4f feat: Filter out alternative item rows in taxes and totals for Quotation
- Added a Quotation Item field `is_alternative_item`
- Use filtered rows for taxes and totals computation
2023-01-24 18:03:53 +05:30
Devin Slauenwhite
ef6480803b chore: linter 2022-06-22 14:10:40 -04:00
Devin Slauenwhite
199b8dd0e3 Merge branch 'develop' into fix-reserve-qty 2022-06-22 14:06:35 -04:00
Devin Slauenwhite
6a6c560375 fix: move to transcation settings 2022-06-17 15:51:11 -04:00
Devin Slauenwhite
179e2d2c74 fix: reset selling setting 2022-06-17 15:29:32 -04:00
Devin Slauenwhite
591b5917a9 fix: re-assign after append query 2022-06-17 15:27:17 -04:00
Devin Slauenwhite
0328874018 fix: syntax missing ) 2022-06-17 14:45:33 -04:00
Devin Slauenwhite
7855a59843 Merge branch 'develop' into fix-reserve-qty 2022-06-17 14:39:31 -04:00
Devin Slauenwhite
494bbf0124 feat: add checkbox to reserve qty on sales return 2022-06-17 13:51:33 -04:00
Devin Slauenwhite
f3f26d2c78 refactor: get_reserved_qty query builder 2022-06-17 11:51:21 -04:00
Devin Slauenwhite
d259c2e36a fix: merge conflict 2022-06-17 11:50:54 -04:00
Devin Slauenwhite
f39422afd8 Merge branch 'develop' into fix-reserve-qty
# Conflicts:
#	erpnext/patches.txt
#	erpnext/public/js/utils/barcode_scanner.js
#	erpnext/regional/report/gstr_1/gstr_1.py
#	erpnext/stock/doctype/delivery_note/test_delivery_note.py
2022-06-17 08:04:52 -04:00
Devin Slauenwhite
5a402a5709 fix(test): use unique item in sales order. 2022-05-17 13:32:26 -04:00
Devin Slauenwhite
4d0fa85a75 fix(test): update_prevdoc_status 2022-05-16 19:45:59 -04:00
Devin Slauenwhite
47f867b77a test: don't reserve qty on sales return. 2022-05-16 14:27:45 -04:00
Devin Slauenwhite
1d1c975da1 Merge remote-tracking branch 'upstream/develop' into fix-reserve-qty 2022-05-16 11:10:28 -04:00
Devin Slauenwhite
695e2bcfbc fix: don't reserve qty on sales return. 2022-05-14 11:06:24 -04:00
330 changed files with 10528 additions and 9889 deletions

View File

@@ -8,8 +8,9 @@ sudo apt update && sudo apt install redis-server libcups2-dev
pip install frappe-bench
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"}
frappebranch=${FRAPPE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF##*/}}}
frappebranch=${FRAPPE_BRANCH:-$githubbranch}
git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
@@ -60,7 +61,7 @@ sed -i 's/schedule:/# schedule:/g' Procfile
sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app payments
bench get-app payments --branch ${githubbranch%"-hotfix"}
bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi

View File

@@ -7,7 +7,6 @@ on:
- '**.css'
- '**.md'
- '**.html'
- '**.csv'
push:
branches: [ develop ]
paths-ignore:

View File

@@ -3,13 +3,13 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @anandbaburajan @deepeshgarg007
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/support/ @nextchamp-saqib @deepeshgarg007
pos* @nextchamp-saqib
erpnext/loan_management/ @deepeshgarg007
erpnext/regional @deepeshgarg007 @ruthra-kumar
erpnext/selling @deepeshgarg007 @ruthra-kumar
erpnext/support/ @deepeshgarg007
pos*
erpnext/buying/ @rohitwaghchaure @s-aga-r
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
@@ -18,12 +18,8 @@ erpnext/quality_management/ @rohitwaghchaure @s-aga-r
erpnext/stock/ @rohitwaghchaure @s-aga-r
erpnext/subcontracting @rohitwaghchaure @s-aga-r
erpnext/crm/ @NagariaHussain
erpnext/education/ @rutwikhdev
erpnext/projects/ @ruchamahabal
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
erpnext/patches/ @deepeshgarg007
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib
.github/ @ankush
.github/ @deepeshgarg007
pyproject.toml @ankush

View File

@@ -65,7 +65,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
4. [Telegram Group](https://t.me/erpnexthelp) - Get instant help from huge community of users.
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
## Contributing

View File

@@ -394,7 +394,13 @@ def update_account_number(name, account_name, account_number=None, from_descenda
if ancestors and not allow_independent_account_creation:
for ancestor in ancestors:
if frappe.db.get_value("Account", {"account_name": old_acc_name, "company": ancestor}, "name"):
old_name = frappe.db.get_value(
"Account",
{"account_number": old_acc_number, "account_name": old_acc_name, "company": ancestor},
"name",
)
if old_name:
# same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company")

View File

@@ -29,6 +29,7 @@ def create_charts(
"root_type",
"is_group",
"tax_rate",
"account_currency",
]:
account_number = cstr(child.get("account_number")).strip()
@@ -95,7 +96,17 @@ def identify_is_group(child):
is_group = child.get("is_group")
elif len(
set(child.keys())
- set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])
- set(
[
"account_name",
"account_type",
"root_type",
"is_group",
"tax_rate",
"account_number",
"account_currency",
]
)
):
is_group = 1
else:
@@ -185,6 +196,7 @@ def get_account_tree_from_existing_company(existing_company):
"root_type",
"tax_rate",
"account_number",
"account_currency",
],
order_by="lft, rgt",
)
@@ -267,6 +279,7 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
"root_type",
"is_group",
"tax_rate",
"account_currency",
]:
continue

View File

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

View File

@@ -0,0 +1,164 @@
{
"actions": [],
"creation": "2023-02-21 15:20:59.586811",
"default_view": "List",
"doctype": "DocType",
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
"closing_date",
"account",
"cost_center",
"debit",
"credit",
"account_currency",
"debit_in_account_currency",
"credit_in_account_currency",
"project",
"company",
"finance_book",
"period_closing_voucher",
"is_period_closing_voucher_entry"
],
"fields": [
{
"fieldname": "closing_date",
"fieldtype": "Date",
"in_filter": 1,
"in_list_view": 1,
"label": "Closing Date",
"oldfieldname": "posting_date",
"oldfieldtype": "Date",
"search_index": 1
},
{
"fieldname": "account",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Account",
"oldfieldname": "account",
"oldfieldtype": "Link",
"options": "Account",
"search_index": 1
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"label": "Cost Center",
"oldfieldname": "cost_center",
"oldfieldtype": "Link",
"options": "Cost Center"
},
{
"fieldname": "debit",
"fieldtype": "Currency",
"label": "Debit Amount",
"oldfieldname": "debit",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency"
},
{
"fieldname": "credit",
"fieldtype": "Currency",
"label": "Credit Amount",
"oldfieldname": "credit",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency"
},
{
"fieldname": "account_currency",
"fieldtype": "Link",
"label": "Account Currency",
"options": "Currency"
},
{
"fieldname": "debit_in_account_currency",
"fieldtype": "Currency",
"label": "Debit Amount in Account Currency",
"options": "account_currency"
},
{
"fieldname": "credit_in_account_currency",
"fieldtype": "Currency",
"label": "Credit Amount in Account Currency",
"options": "account_currency"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"oldfieldname": "company",
"oldfieldtype": "Link",
"options": "Company",
"search_index": 1
},
{
"fieldname": "finance_book",
"fieldtype": "Link",
"label": "Finance Book",
"options": "Finance Book"
},
{
"fieldname": "period_closing_voucher",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Period Closing Voucher",
"options": "Period Closing Voucher",
"search_index": 1
},
{
"default": "0",
"fieldname": "is_period_closing_voucher_entry",
"fieldtype": "Check",
"label": "Is Period Closing Voucher Entry"
}
],
"icon": "fa fa-list",
"in_create": 1,
"links": [],
"modified": "2023-03-06 08:56:36.393237",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account Closing Balance",
"owner": "Administrator",
"permissions": [
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User"
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager"
},
{
"export": 1,
"read": 1,
"report": 1,
"role": "Auditor"
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,127 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe.utils import cint, cstr
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
class AccountClosingBalance(Document):
pass
def make_closing_entries(closing_entries, voucher_name):
accounting_dimensions = get_accounting_dimensions()
company = closing_entries[0].get("company")
closing_date = closing_entries[0].get("closing_date")
previous_closing_entries = get_previous_closing_entries(
company, closing_date, accounting_dimensions
)
combined_entries = closing_entries + previous_closing_entries
merged_entries = aggregate_with_last_account_closing_balance(
combined_entries, accounting_dimensions
)
for key, value in merged_entries.items():
cle = frappe.new_doc("Account Closing Balance")
cle.update(value)
cle.update(value["dimensions"])
cle.update(
{
"period_closing_voucher": voucher_name,
"closing_date": closing_date,
}
)
cle.submit()
def aggregate_with_last_account_closing_balance(entries, accounting_dimensions):
merged_entries = {}
for entry in entries:
key, key_values = generate_key(entry, accounting_dimensions)
merged_entries.setdefault(
key,
{
"debit": 0,
"credit": 0,
"debit_in_account_currency": 0,
"credit_in_account_currency": 0,
},
)
merged_entries[key]["dimensions"] = key_values
merged_entries[key]["debit"] += entry.get("debit")
merged_entries[key]["credit"] += entry.get("credit")
merged_entries[key]["debit_in_account_currency"] += entry.get("debit_in_account_currency")
merged_entries[key]["credit_in_account_currency"] += entry.get("credit_in_account_currency")
return merged_entries
def generate_key(entry, accounting_dimensions):
key = [
cstr(entry.get("account")),
cstr(entry.get("account_currency")),
cstr(entry.get("cost_center")),
cstr(entry.get("project")),
cstr(entry.get("finance_book")),
cint(entry.get("is_period_closing_voucher_entry")),
]
key_values = {
"company": cstr(entry.get("company")),
"account": cstr(entry.get("account")),
"account_currency": cstr(entry.get("account_currency")),
"cost_center": cstr(entry.get("cost_center")),
"project": cstr(entry.get("project")),
"finance_book": cstr(entry.get("finance_book")),
"is_period_closing_voucher_entry": cint(entry.get("is_period_closing_voucher_entry")),
}
for dimension in accounting_dimensions:
key.append(cstr(entry.get(dimension)))
key_values[dimension] = cstr(entry.get(dimension))
return tuple(key), key_values
def get_previous_closing_entries(company, closing_date, accounting_dimensions):
entries = []
last_period_closing_voucher = frappe.db.get_all(
"Period Closing Voucher",
filters={"docstatus": 1, "company": company, "posting_date": ("<", closing_date)},
fields=["name"],
order_by="posting_date desc",
limit=1,
)
if last_period_closing_voucher:
account_closing_balance = frappe.qb.DocType("Account Closing Balance")
query = frappe.qb.from_(account_closing_balance).select(
account_closing_balance.company,
account_closing_balance.account,
account_closing_balance.account_currency,
account_closing_balance.debit,
account_closing_balance.credit,
account_closing_balance.debit_in_account_currency,
account_closing_balance.credit_in_account_currency,
account_closing_balance.cost_center,
account_closing_balance.project,
account_closing_balance.finance_book,
account_closing_balance.is_period_closing_voucher_entry,
)
for dimension in accounting_dimensions:
query = query.select(account_closing_balance[dimension])
query = query.where(
account_closing_balance.period_closing_voucher == last_period_closing_voucher[0].name
)
entries = query.run(as_dict=1)
return entries

View File

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

View File

@@ -31,6 +31,7 @@
"determine_address_tax_category_from",
"column_break_19",
"add_taxes_from_item_tax_template",
"book_tax_discount_loss",
"print_settings",
"show_inclusive_tax_in_print",
"column_break_12",
@@ -360,6 +361,13 @@
"fieldname": "show_balance_in_coa",
"fieldtype": "Check",
"label": "Show Balances in Chart Of Accounts"
},
{
"default": "0",
"description": "Split Early Payment Discount Loss into Income and Tax Loss",
"fieldname": "book_tax_discount_loss",
"fieldtype": "Check",
"label": "Book Tax Loss on Early Payment Discount"
}
],
"icon": "icon-cog",
@@ -367,7 +375,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-01-02 12:07:42.434214",
"modified": "2023-03-28 09:50:20.375233",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -118,6 +118,10 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
}
plaid_success(token, response) {
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.update_bank_account_ids', {
response: response,
}).then(() => {
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
});
}
};

View File

@@ -81,7 +81,7 @@ class BankClearance(Document):
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
loan_disbursements = (
query = (
frappe.qb.from_(loan_disbursement)
.select(
ConstantColumn("Loan Disbursement").as_("payment_document"),
@@ -90,17 +90,22 @@ class BankClearance(Document):
ConstantColumn(0).as_("debit"),
loan_disbursement.reference_number.as_("cheque_number"),
loan_disbursement.reference_date.as_("cheque_date"),
loan_disbursement.clearance_date.as_("clearance_date"),
loan_disbursement.disbursement_date.as_("posting_date"),
loan_disbursement.applicant.as_("against_account"),
)
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.disbursement_date >= self.from_date)
.where(loan_disbursement.disbursement_date <= self.to_date)
.where(loan_disbursement.clearance_date.isnull())
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
.orderby(loan_disbursement.disbursement_date)
.orderby(loan_disbursement.name, order=frappe.qb.desc)
).run(as_dict=1)
)
if not self.include_reconciled_entries:
query = query.where(loan_disbursement.clearance_date.isnull())
loan_disbursements = query.run(as_dict=1)
loan_repayment = frappe.qb.DocType("Loan Repayment")
@@ -113,16 +118,19 @@ class BankClearance(Document):
ConstantColumn(0).as_("credit"),
loan_repayment.reference_number.as_("cheque_number"),
loan_repayment.reference_date.as_("cheque_date"),
loan_repayment.clearance_date.as_("clearance_date"),
loan_repayment.applicant.as_("against_account"),
loan_repayment.posting_date,
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
)
if not self.include_reconciled_entries:
query = query.where(loan_repayment.clearance_date.isnull())
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))

View File

@@ -18,6 +18,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
},
onload: function (frm) {
// Set default filter dates
today = frappe.datetime.get_today()
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
frm.doc.bank_statement_to_date = today;
frm.trigger('bank_account');
},
@@ -32,6 +36,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
},
refresh: function (frm) {
frm.disable_save();
frappe.require("bank-reconciliation-tool.bundle.js", () =>
frm.trigger("make_reconciliation_tool")
);
@@ -72,10 +77,12 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
},
})
});
},
after_save: function (frm) {
frm.trigger("make_reconciliation_tool");
frm.add_custom_button(__('Get Unreconciled Entries'), function() {
frm.trigger("make_reconciliation_tool");
});
frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary');
},
bank_account: function (frm) {
@@ -89,7 +96,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
r.account,
"account_currency",
(r) => {
frm.currency = r.account_currency;
frm.doc.account_currency = r.account_currency;
frm.trigger("render_chart");
}
);
@@ -155,19 +162,19 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}
},
render_chart: frappe.utils.debounce((frm) => {
render_chart(frm) {
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
{
$reconciliation_tool_cards: frm.get_field(
"reconciliation_tool_cards"
).$wrapper,
bank_statement_closing_balance:
frm.doc.bank_statement_closing_balance,
frm.doc.bank_statement_closing_balance,
cleared_balance: frm.cleared_balance,
currency: frm.currency,
currency: frm.doc.account_currency,
}
);
}, 500),
},
render(frm) {
if (frm.doc.bank_account) {

View File

@@ -14,6 +14,7 @@
"to_reference_date",
"filter_by_reference_date",
"column_break_2",
"account_currency",
"account_opening_balance",
"bank_statement_closing_balance",
"section_break_1",
@@ -59,7 +60,7 @@
"fieldname": "account_opening_balance",
"fieldtype": "Currency",
"label": "Account Opening Balance",
"options": "Currency",
"options": "account_currency",
"read_only": 1
},
{
@@ -67,7 +68,7 @@
"fieldname": "bank_statement_closing_balance",
"fieldtype": "Currency",
"label": "Closing Balance",
"options": "Currency"
"options": "account_currency"
},
{
"fieldname": "section_break_1",
@@ -104,13 +105,20 @@
"fieldname": "filter_by_reference_date",
"fieldtype": "Check",
"label": "Filter by Reference Date"
},
{
"fieldname": "account_currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Account Currency",
"options": "Currency"
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-01-13 13:00:02.022919",
"modified": "2023-03-07 11:02:24.535714",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Reconciliation Tool",

View File

@@ -10,7 +10,7 @@ from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import cint, flt
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
get_amounts_not_reflected_in_system,
get_entries,
@@ -28,7 +28,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
filters = []
filters.append(["bank_account", "=", bank_account])
filters.append(["docstatus", "=", 1])
filters.append(["unallocated_amount", ">", 0])
filters.append(["unallocated_amount", ">", 0.0])
if to_date:
filters.append(["date", "<=", to_date])
if from_date:
@@ -58,7 +58,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
@frappe.whitelist()
def get_account_balance(bank_account, till_date):
# returns account balance till the specified date
account = frappe.get_cached_value("Bank Account", bank_account, "account")
account = frappe.db.get_value("Bank Account", bank_account, "account")
filters = frappe._dict(
{"account": account, "report_date": till_date, "include_pos_transactions": 1}
)
@@ -66,7 +66,7 @@ def get_account_balance(bank_account, till_date):
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
total_debit, total_credit = 0, 0
total_debit, total_credit = 0.0, 0.0
for d in data:
total_debit += flt(d.debit)
total_credit += flt(d.credit)
@@ -131,10 +131,8 @@ def create_journal_entry_bts(
fieldname=["name", "deposit", "withdrawal", "bank_account"],
as_dict=True,
)[0]
company_account = frappe.get_cached_value(
"Bank Account", bank_transaction.bank_account, "account"
)
account_type = frappe.get_cached_value("Account", second_account, "account_type")
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
account_type = frappe.db.get_value("Account", second_account, "account_type")
if account_type in ["Receivable", "Payable"]:
if not (party_type and party):
frappe.throw(
@@ -147,10 +145,8 @@ def create_journal_entry_bts(
accounts.append(
{
"account": second_account,
"credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
"debit_in_account_currency": bank_transaction.withdrawal
if bank_transaction.withdrawal > 0
else 0,
"credit_in_account_currency": bank_transaction.deposit,
"debit_in_account_currency": bank_transaction.withdrawal,
"party_type": party_type,
"party": party,
}
@@ -160,14 +156,12 @@ def create_journal_entry_bts(
{
"account": company_account,
"bank_account": bank_transaction.bank_account,
"credit_in_account_currency": bank_transaction.withdrawal
if bank_transaction.withdrawal > 0
else 0,
"debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
"credit_in_account_currency": bank_transaction.withdrawal,
"debit_in_account_currency": bank_transaction.deposit,
}
)
company = frappe.get_cached_value("Account", company_account, "company")
company = frappe.get_value("Account", company_account, "company")
journal_entry_dict = {
"voucher_type": entry_type,
@@ -187,16 +181,22 @@ def create_journal_entry_bts(
journal_entry.insert()
journal_entry.submit()
if bank_transaction.deposit > 0:
if bank_transaction.deposit > 0.0:
paid_amount = bank_transaction.deposit
else:
paid_amount = bank_transaction.withdrawal
vouchers = json.dumps(
[{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}]
[
{
"payment_doctype": "Journal Entry",
"payment_name": journal_entry.name,
"amount": paid_amount,
}
]
)
return reconcile_vouchers(bank_transaction.name, vouchers)
return reconcile_vouchers(bank_transaction_name, vouchers)
@frappe.whitelist()
@@ -220,12 +220,10 @@ def create_payment_entry_bts(
as_dict=True,
)[0]
paid_amount = bank_transaction.unallocated_amount
payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
company_account = frappe.get_cached_value(
"Bank Account", bank_transaction.bank_account, "account"
)
company = frappe.get_cached_value("Account", company_account, "company")
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
company = frappe.get_value("Account", company_account, "company")
payment_entry_dict = {
"company": company,
"payment_type": payment_type,
@@ -261,9 +259,15 @@ def create_payment_entry_bts(
payment_entry.submit()
vouchers = json.dumps(
[{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}]
[
{
"payment_doctype": "Payment Entry",
"payment_name": payment_entry.name,
"amount": paid_amount,
}
]
)
return reconcile_vouchers(bank_transaction.name, vouchers)
return reconcile_vouchers(bank_transaction_name, vouchers)
@frappe.whitelist()
@@ -345,59 +349,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
# updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
company_account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account")
if transaction.unallocated_amount == 0:
frappe.throw(_("This bank transaction is already fully reconciled"))
total_amount = 0
for voucher in vouchers:
voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"])
total_amount += get_paid_amount(
frappe._dict(
{
"payment_document": voucher["payment_doctype"],
"payment_entry": voucher["payment_name"],
}
),
transaction.currency,
company_account,
)
if total_amount > transaction.unallocated_amount:
frappe.throw(
_(
"The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"
)
)
account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account")
for voucher in vouchers:
gl_entry = frappe.db.get_value(
"GL Entry",
dict(
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
),
["credit_in_account_currency as credit", "debit_in_account_currency as debit"],
as_dict=1,
)
gl_amount, transaction_amount = (
(gl_entry.credit, transaction.deposit)
if gl_entry.credit > 0
else (gl_entry.debit, transaction.withdrawal)
)
allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
transaction.append(
"payment_entries",
{
"payment_document": voucher["payment_entry"].doctype,
"payment_entry": voucher["payment_entry"].name,
"allocated_amount": allocated_amount,
},
)
transaction.save()
transaction.update_allocations()
transaction.add_payment_entries(vouchers)
return frappe.get_doc("Bank Transaction", bank_transaction_name)
@@ -416,9 +368,9 @@ def get_linked_payments(
bank_account = frappe.db.get_values(
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
)[0]
(account, company) = (bank_account.account, bank_account.company)
(gl_account, company) = (bank_account.account, bank_account.company)
matching = check_matching(
account,
gl_account,
company,
transaction,
document_types,
@@ -428,7 +380,27 @@ def get_linked_payments(
from_reference_date,
to_reference_date,
)
return matching
return subtract_allocations(gl_account, matching)
def subtract_allocations(gl_account, vouchers):
"Look up & subtract any existing Bank Transaction allocations"
copied = []
for voucher in vouchers:
rows = get_total_allocated_amount(voucher[1], voucher[2])
amount = None
for row in rows:
if row["gl_account"] == gl_account:
amount = row["total"]
break
if amount:
l = list(voucher)
l[3] -= amount
copied.append(tuple(l))
else:
copied.append(voucher)
return copied
def check_matching(
@@ -442,6 +414,7 @@ def check_matching(
from_reference_date,
to_reference_date,
):
exact_match = True if "exact_match" in document_types else False
# combine all types of vouchers
subquery = get_queries(
bank_account,
@@ -453,10 +426,11 @@ def check_matching(
filter_by_reference_date,
from_reference_date,
to_reference_date,
exact_match,
)
filters = {
"amount": transaction.unallocated_amount,
"payment_type": "Receive" if transaction.deposit > 0 else "Pay",
"payment_type": "Receive" if transaction.deposit > 0.0 else "Pay",
"reference_no": transaction.reference_number,
"party_type": transaction.party_type,
"party": transaction.party,
@@ -465,7 +439,9 @@ def check_matching(
matching_vouchers = []
matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters))
matching_vouchers.extend(
get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match)
)
for query in subquery:
matching_vouchers.extend(
@@ -487,10 +463,10 @@ def get_queries(
filter_by_reference_date,
from_reference_date,
to_reference_date,
exact_match,
):
# get queries to get matching vouchers
amount_condition = "=" if "exact_match" in document_types else "<="
account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
queries = []
# get matching queries from all the apps
@@ -501,7 +477,7 @@ def get_queries(
company,
transaction,
document_types,
amount_condition,
exact_match,
account_from_to,
from_date,
to_date,
@@ -520,7 +496,7 @@ def get_matching_queries(
company,
transaction,
document_types,
amount_condition,
exact_match,
account_from_to,
from_date,
to_date,
@@ -530,8 +506,8 @@ def get_matching_queries(
):
queries = []
if "payment_entry" in document_types:
pe_amount_matching = get_pe_matching_query(
amount_condition,
query = get_pe_matching_query(
exact_match,
account_from_to,
transaction,
from_date,
@@ -540,11 +516,11 @@ def get_matching_queries(
from_reference_date,
to_reference_date,
)
queries.extend([pe_amount_matching])
queries.append(query)
if "journal_entry" in document_types:
je_amount_matching = get_je_matching_query(
amount_condition,
query = get_je_matching_query(
exact_match,
transaction,
from_date,
to_date,
@@ -552,34 +528,70 @@ def get_matching_queries(
from_reference_date,
to_reference_date,
)
queries.extend([je_amount_matching])
queries.append(query)
if transaction.deposit > 0 and "sales_invoice" in document_types:
si_amount_matching = get_si_matching_query(amount_condition)
queries.extend([si_amount_matching])
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
query = get_si_matching_query(exact_match)
queries.append(query)
if transaction.withdrawal > 0:
if transaction.withdrawal > 0.0:
if "purchase_invoice" in document_types:
pi_amount_matching = get_pi_matching_query(amount_condition)
queries.extend([pi_amount_matching])
query = get_pi_matching_query(exact_match)
queries.append(query)
if "bank_transaction" in document_types:
query = get_bt_matching_query(exact_match, transaction)
queries.append(query)
return queries
def get_loan_vouchers(bank_account, transaction, document_types, filters):
def get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match):
vouchers = []
amount_condition = True if "exact_match" in document_types else False
if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
if transaction.withdrawal > 0.0 and "loan_disbursement" in document_types:
vouchers.extend(get_ld_matching_query(bank_account, exact_match, filters))
if transaction.deposit > 0 and "loan_repayment" in document_types:
vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
if transaction.deposit > 0.0 and "loan_repayment" in document_types:
vouchers.extend(get_lr_matching_query(bank_account, exact_match, filters))
return vouchers
def get_ld_matching_query(bank_account, amount_condition, filters):
def get_bt_matching_query(exact_match, transaction):
# get matching bank transaction query
# find bank transactions in the same bank account with opposite sign
# same bank account must have same company and currency
field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
return f"""
SELECT
(CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END
+ CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END
+ CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END
+ 1) AS rank,
'Bank Transaction' AS doctype,
name,
unallocated_amount AS paid_amount,
reference_number AS reference_no,
date AS reference_date,
party,
party_type,
date AS posting_date,
currency
FROM
`tabBank Transaction`
WHERE
status != 'Reconciled'
AND name != '{transaction.name}'
AND bank_account = '{transaction.bank_account}'
AND {field} {'= %(amount)s' if exact_match else '> 0.0'}
"""
def get_ld_matching_query(bank_account, exact_match, filters):
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
matching_party = loan_disbursement.applicant_type == filters.get(
@@ -607,17 +619,17 @@ def get_ld_matching_query(bank_account, amount_condition, filters):
.where(loan_disbursement.disbursement_account == bank_account)
)
if amount_condition:
if exact_match:
query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
else:
query.where(loan_disbursement.disbursed_amount <= filters.get("amount"))
query.where(loan_disbursement.disbursed_amount > 0.0)
vouchers = query.run(as_list=True)
return vouchers
def get_lr_matching_query(bank_account, amount_condition, filters):
def get_lr_matching_query(bank_account, exact_match, filters):
loan_repayment = frappe.qb.DocType("Loan Repayment")
matching_reference = loan_repayment.reference_number == filters.get("reference_number")
matching_party = loan_repayment.applicant_type == filters.get(
@@ -648,10 +660,10 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))
if amount_condition:
if exact_match:
query.where(loan_repayment.amount_paid == filters.get("amount"))
else:
query.where(loan_repayment.amount_paid <= filters.get("amount"))
query.where(loan_repayment.amount_paid > 0.0)
vouchers = query.run()
@@ -659,7 +671,7 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
def get_pe_matching_query(
amount_condition,
exact_match,
account_from_to,
transaction,
from_date,
@@ -669,7 +681,7 @@ def get_pe_matching_query(
to_reference_date,
):
# get matching payment entries query
if transaction.deposit > 0:
if transaction.deposit > 0.0:
currency_field = "paid_to_account_currency as currency"
else:
currency_field = "paid_from_account_currency as currency"
@@ -684,7 +696,8 @@ def get_pe_matching_query(
return f"""
SELECT
(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
+ 1 ) AS rank,
'Payment Entry' as doctype,
name,
@@ -698,20 +711,19 @@ def get_pe_matching_query(
FROM
`tabPayment Entry`
WHERE
paid_amount {amount_condition} %(amount)s
AND docstatus = 1
docstatus = 1
AND payment_type IN (%(payment_type)s, 'Internal Transfer')
AND ifnull(clearance_date, '') = ""
AND {account_from_to} = %(bank_account)s
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
{filter_by_date}
{filter_by_reference_no}
order by{order_by}
"""
def get_je_matching_query(
amount_condition,
exact_match,
transaction,
from_date,
to_date,
@@ -723,7 +735,7 @@ def get_je_matching_query(
# We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
cr_or_dr = "credit" if transaction.withdrawal > 0.0 else "debit"
filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
order_by = " je.posting_date"
filter_by_reference_no = ""
@@ -735,26 +747,29 @@ def get_je_matching_query(
return f"""
SELECT
(CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
+ CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END
+ 1) AS rank ,
'Journal Entry' as doctype,
'Journal Entry' AS doctype,
je.name,
jea.{cr_or_dr}_in_account_currency as paid_amount,
je.cheque_no as reference_no,
je.cheque_date as reference_date,
je.pay_to_recd_from as party,
jea.{cr_or_dr}_in_account_currency AS paid_amount,
je.cheque_no AS reference_no,
je.cheque_date AS reference_date,
je.pay_to_recd_from AS party,
jea.party_type,
je.posting_date,
jea.account_currency as currency
jea.account_currency AS currency
FROM
`tabJournal Entry Account` as jea
`tabJournal Entry Account` AS jea
JOIN
`tabJournal Entry` as je
`tabJournal Entry` AS je
ON
jea.parent = je.name
WHERE
(je.clearance_date is null or je.clearance_date='0000-00-00')
je.docstatus = 1
AND je.voucher_type NOT IN ('Opening Entry')
AND (je.clearance_date IS NULL OR je.clearance_date='0000-00-00')
AND jea.account = %(bank_account)s
AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s
AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'}
AND je.docstatus = 1
{filter_by_date}
{filter_by_reference_no}
@@ -762,11 +777,12 @@ def get_je_matching_query(
"""
def get_si_matching_query(amount_condition):
# get matchin sales invoice query
def get_si_matching_query(exact_match):
# get matching sales invoice query
return f"""
SELECT
( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
+ CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END
+ 1 ) AS rank,
'Sales Invoice' as doctype,
si.name,
@@ -784,18 +800,20 @@ def get_si_matching_query(amount_condition):
`tabSales Invoice` as si
ON
sip.parent = si.name
WHERE (sip.clearance_date is null or sip.clearance_date='0000-00-00')
WHERE
si.docstatus = 1
AND (sip.clearance_date is null or sip.clearance_date='0000-00-00')
AND sip.account = %(bank_account)s
AND sip.amount {amount_condition} %(amount)s
AND si.docstatus = 1
AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'}
"""
def get_pi_matching_query(amount_condition):
# get matching purchase invoice query
def get_pi_matching_query(exact_match):
# get matching purchase invoice query when they are also used as payment entries (is_paid)
return f"""
SELECT
( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
+ 1 ) AS rank,
'Purchase Invoice' as doctype,
name,
@@ -809,9 +827,9 @@ def get_pi_matching_query(amount_condition):
FROM
`tabPurchase Invoice`
WHERE
paid_amount {amount_condition} %(amount)s
AND docstatus = 1
docstatus = 1
AND is_paid = 1
AND ifnull(clearance_date, '') = ""
AND cash_bank_account = %(bank_account)s
AND cash_bank_account = %(bank_account)s
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
"""

View File

@@ -12,8 +12,13 @@ frappe.ui.form.on("Bank Transaction", {
};
});
},
bank_account: function(frm) {
refresh(frm) {
frm.add_custom_button(__('Unreconcile Transaction'), () => {
frm.call('remove_payment_entries')
.then( () => frm.refresh() );
});
},
bank_account: function (frm) {
set_bank_statement_filter(frm);
},
@@ -34,6 +39,7 @@ frappe.ui.form.on("Bank Transaction", {
"Journal Entry",
"Sales Invoice",
"Purchase Invoice",
"Bank Transaction",
];
}
});
@@ -49,7 +55,7 @@ const update_clearance_date = (frm, cdt, cdn) => {
frappe
.xcall(
"erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment",
{ doctype: cdt, docname: cdn }
{ doctype: cdt, docname: cdn, bt_name: frm.doc.name }
)
.then((e) => {
if (e == "success") {

View File

@@ -20,9 +20,11 @@
"currency",
"section_break_10",
"description",
"section_break_14",
"reference_number",
"column_break_10",
"transaction_id",
"transaction_type",
"section_break_14",
"payment_entries",
"section_break_18",
"allocated_amount",
@@ -190,11 +192,21 @@
"label": "Withdrawal",
"oldfieldname": "credit",
"options": "currency"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "transaction_type",
"fieldtype": "Data",
"label": "Transaction Type",
"length": 50
}
],
"is_submittable": 1,
"links": [],
"modified": "2022-03-21 19:05:04.208222",
"modified": "2022-05-29 18:36:50.475964",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
@@ -248,4 +260,4 @@
"states": [],
"title_field": "bank_account",
"track_changes": 1
}
}

View File

@@ -1,9 +1,6 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from functools import reduce
import frappe
from frappe.utils import flt
@@ -18,72 +15,135 @@ class BankTransaction(StatusUpdater):
self.clear_linked_payment_entries()
self.set_status()
_saving_flag = False
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
def on_update_after_submit(self):
self.update_allocations()
self.clear_linked_payment_entries()
self.set_status(update=True)
"Run on save(). Avoid recursion caused by multiple saves"
if not self._saving_flag:
self._saving_flag = True
self.clear_linked_payment_entries()
self.update_allocations()
self._saving_flag = False
def on_cancel(self):
self.clear_linked_payment_entries(for_cancel=True)
self.set_status(update=True)
def update_allocations(self):
"The doctype does not allow modifications after submission, so write to the db direct"
if self.payment_entries:
allocated_amount = reduce(
lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
)
allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
else:
allocated_amount = 0
allocated_amount = 0.0
if allocated_amount:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount))
frappe.db.set_value(
self.doctype,
self.name,
"unallocated_amount",
abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
)
amount = abs(flt(self.withdrawal) - flt(self.deposit))
self.db_set("allocated_amount", flt(allocated_amount))
self.db_set("unallocated_amount", amount - flt(allocated_amount))
self.reload()
self.set_status(update=True)
else:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0)
frappe.db.set_value(
self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))
)
def add_payment_entries(self, vouchers):
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
if 0.0 >= self.unallocated_amount:
frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
amount = self.deposit or self.withdrawal
if amount == self.allocated_amount:
frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
added = False
for voucher in vouchers:
# Can't add same voucher twice
found = False
for pe in self.payment_entries:
if (
pe.payment_document == voucher["payment_doctype"]
and pe.payment_entry == voucher["payment_name"]
):
found = True
if not found:
pe = {
"payment_document": voucher["payment_doctype"],
"payment_entry": voucher["payment_name"],
"allocated_amount": 0.0, # Temporary
}
child = self.append("payment_entries", pe)
added = True
# runs on_update_after_submit
if added:
self.save()
def allocate_payment_entries(self):
"""Refactored from bank reconciliation tool.
Non-zero allocations must be amended/cleared manually
Get the bank transaction amount (b) and remove as we allocate
For each payment_entry if allocated_amount == 0:
- get the amount already allocated against all transactions (t), need latest date
- get the voucher amount (from gl) (v)
- allocate (a = v - t)
- a = 0: should already be cleared, so clear & remove payment_entry
- 0 < a <= u: allocate a & clear
- 0 < a, a > u: allocate u
- 0 > a: Error: already over-allocated
- clear means: set the latest transaction date as clearance date
"""
gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
remaining_amount = self.unallocated_amount
for payment_entry in self.payment_entries:
if payment_entry.allocated_amount == 0.0:
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
self, payment_entry
)
if 0.0 == unallocated_amount:
if should_clear:
latest_transaction.clear_linked_payment_entry(payment_entry)
self.db_delete_payment_entry(payment_entry)
elif remaining_amount <= 0.0:
self.db_delete_payment_entry(payment_entry)
elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount:
payment_entry.db_set("allocated_amount", unallocated_amount)
remaining_amount -= unallocated_amount
if should_clear:
latest_transaction.clear_linked_payment_entry(payment_entry)
elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount:
payment_entry.db_set("allocated_amount", remaining_amount)
remaining_amount = 0.0
elif 0.0 > unallocated_amount:
self.db_delete_payment_entry(payment_entry)
frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
self.reload()
def clear_linked_payment_entries(self, for_cancel=False):
def db_delete_payment_entry(self, payment_entry):
frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name})
@frappe.whitelist()
def remove_payment_entries(self):
for payment_entry in self.payment_entries:
if payment_entry.payment_document == "Sales Invoice":
self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document in get_doctypes_for_bank_reconciliation():
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
self.remove_payment_entry(payment_entry)
# runs on_update_after_submit
self.save()
def clear_simple_entry(self, payment_entry, for_cancel=False):
if payment_entry.payment_document == "Payment Entry":
if (
frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type")
== "Internal Transfer"
):
if len(get_reconciled_bank_transactions(payment_entry)) < 2:
return
def remove_payment_entry(self, payment_entry):
"Clear payment entry and clearance"
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
self.remove(payment_entry)
clearance_date = self.date if not for_cancel else None
frappe.db.set_value(
payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date
)
def clear_linked_payment_entries(self, for_cancel=False):
if for_cancel:
for payment_entry in self.payment_entries:
self.clear_linked_payment_entry(payment_entry, for_cancel)
else:
self.allocate_payment_entries()
def clear_sales_invoice(self, payment_entry, for_cancel=False):
clearance_date = self.date if not for_cancel else None
frappe.db.set_value(
"Sales Invoice Payment",
dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry),
"clearance_date",
clearance_date,
def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
clearance_date = None if for_cancel else self.date
set_voucher_clearance(
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
)
@@ -93,38 +153,114 @@ def get_doctypes_for_bank_reconciliation():
return frappe.get_hooks("bank_reconciliation_doctypes")
def get_reconciled_bank_transactions(payment_entry):
reconciled_bank_transactions = frappe.get_all(
"Bank Transaction Payments",
filters={"payment_entry": payment_entry.payment_entry},
fields=["parent"],
def get_clearance_details(transaction, payment_entry):
"""
There should only be one bank gle for a voucher.
Could be none for a Bank Transaction.
But if a JE, could affect two banks.
Should only clear the voucher if all bank gles are allocated.
"""
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
bt_allocations = get_total_allocated_amount(
payment_entry.payment_document, payment_entry.payment_entry
)
return reconciled_bank_transactions
unallocated_amount = min(
transaction.unallocated_amount,
get_paid_amount(payment_entry, transaction.currency, gl_bank_account),
)
unmatched_gles = len(gles)
latest_transaction = transaction
for gle in gles:
if gle["gl_account"] == gl_bank_account:
if gle["amount"] <= 0.0:
frappe.throw(
frappe._("Voucher {0} value is broken: {1}").format(
payment_entry.payment_entry, gle["amount"]
)
)
unmatched_gles -= 1
unallocated_amount = gle["amount"]
for a in bt_allocations:
if a["gl_account"] == gle["gl_account"]:
unallocated_amount = gle["amount"] - a["total"]
if frappe.utils.getdate(transaction.date) < a["latest_date"]:
latest_transaction = frappe.get_doc("Bank Transaction", a["latest_name"])
else:
# Must be a Journal Entry affecting more than one bank
for a in bt_allocations:
if a["gl_account"] == gle["gl_account"] and a["total"] == gle["amount"]:
unmatched_gles -= 1
return unallocated_amount, unmatched_gles == 0, latest_transaction
def get_total_allocated_amount(payment_entry):
return frappe.db.sql(
def get_related_bank_gl_entries(doctype, docname):
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
result = frappe.db.sql(
"""
SELECT
SUM(btp.allocated_amount) as allocated_amount,
bt.name
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
gle.account AS gl_account
FROM
`tabBank Transaction Payments` as btp
`tabGL Entry` gle
LEFT JOIN
`tabBank Transaction` bt ON bt.name=btp.parent
`tabAccount` ac ON ac.name=gle.account
WHERE
btp.payment_document = %s
AND
btp.payment_entry = %s
AND
bt.docstatus = 1""",
(payment_entry.payment_document, payment_entry.payment_entry),
ac.account_type = 'Bank'
AND gle.voucher_type = %(doctype)s
AND gle.voucher_no = %(docname)s
AND is_cancelled = 0
""",
dict(doctype=doctype, docname=docname),
as_dict=True,
)
return result
def get_paid_amount(payment_entry, currency, bank_account):
def get_total_allocated_amount(doctype, docname):
"""
Gets the sum of allocations for a voucher on each bank GL account
along with the latest bank transaction name & date
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
"""
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
result = frappe.db.sql(
"""
SELECT total, latest_name, latest_date, gl_account FROM (
SELECT
ROW_NUMBER() OVER w AS rownum,
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
FIRST_VALUE(bt.name) OVER w AS latest_name,
FIRST_VALUE(bt.date) OVER w AS latest_date,
ba.account AS gl_account
FROM
`tabBank Transaction Payments` btp
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
WHERE
btp.payment_document = %(doctype)s
AND btp.payment_entry = %(docname)s
AND bt.docstatus = 1
WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
) temp
WHERE
rownum = 1
""",
dict(doctype=doctype, docname=docname),
as_dict=True,
)
for row in result:
# Why is this *sometimes* a byte string?
if isinstance(row["latest_name"], bytes):
row["latest_name"] = row["latest_name"].decode()
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
return result
def get_paid_amount(payment_entry, currency, gl_bank_account):
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount"
@@ -147,7 +283,7 @@ def get_paid_amount(payment_entry, currency, bank_account):
elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value(
"Journal Entry Account",
{"parent": payment_entry.payment_entry, "account": bank_account},
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
"sum(credit_in_account_currency)",
)
@@ -166,6 +302,12 @@ def get_paid_amount(payment_entry, currency, bank_account):
payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
)
elif payment_entry.payment_document == "Bank Transaction":
dep, wth = frappe.db.get_value(
"Bank Transaction", payment_entry.payment_entry, ("deposit", "withdrawal")
)
return abs(flt(wth) - flt(dep))
else:
frappe.throw(
"Please reconcile {0}: {1} manually".format(
@@ -174,18 +316,55 @@ def get_paid_amount(payment_entry, currency, bank_account):
)
@frappe.whitelist()
def unclear_reference_payment(doctype, docname):
if frappe.db.exists(doctype, docname):
doc = frappe.get_doc(doctype, docname)
if doctype == "Sales Invoice":
frappe.db.set_value(
"Sales Invoice Payment",
dict(parenttype=doc.payment_document, parent=doc.payment_entry),
"clearance_date",
None,
)
else:
frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
def set_voucher_clearance(doctype, docname, clearance_date, self):
if doctype in [
"Payment Entry",
"Journal Entry",
"Purchase Invoice",
"Expense Claim",
"Loan Repayment",
"Loan Disbursement",
]:
if (
doctype == "Payment Entry"
and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer"
and len(get_reconciled_bank_transactions(doctype, docname)) < 2
):
return
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
return doc.payment_entry
elif doctype == "Sales Invoice":
frappe.db.set_value(
"Sales Invoice Payment",
dict(parenttype=doctype, parent=docname),
"clearance_date",
clearance_date,
)
elif doctype == "Bank Transaction":
# For when a second bank transaction has fixed another, e.g. refund
bt = frappe.get_doc(doctype, docname)
if clearance_date:
vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
bt.add_payment_entries(vouchers)
else:
for pe in bt.payment_entries:
if pe.payment_document == self.doctype and pe.payment_entry == self.name:
bt.remove(pe)
bt.save()
break
def get_reconciled_bank_transactions(doctype, docname):
return frappe.get_all(
"Bank Transaction Payments",
filters={"payment_document": doctype, "payment_entry": docname},
pluck="parent",
)
@frappe.whitelist()
def unclear_reference_payment(doctype, docname, bt_name):
bt = frappe.get_doc("Bank Transaction", bt_name)
set_voucher_clearance(doctype, docname, None, bt)
return docname

View File

@@ -36,7 +36,7 @@ def validate_columns(data):
no_of_columns = max([len(d) for d in data])
if no_of_columns > 7:
if no_of_columns > 8:
frappe.throw(
_("More columns found than expected. Please compare the uploaded file with standard template"),
title=(_("Wrong Template")),
@@ -233,6 +233,7 @@ def build_forest(data):
is_group,
account_type,
root_type,
account_currency,
) = i
if not account_name:
@@ -253,6 +254,8 @@ def build_forest(data):
charts_map[account_name]["account_type"] = account_type
if root_type:
charts_map[account_name]["root_type"] = root_type
if account_currency:
charts_map[account_name]["account_currency"] = account_currency
path = return_parent(data, account_name)[::-1]
paths.append(path) # List of path is created
line_no += 1
@@ -315,20 +318,21 @@ def get_template(template_type):
"Is Group",
"Account Type",
"Root Type",
"Account Currency",
]
writer = UnicodeWriter()
writer.writerow(fields)
if template_type == "Blank Template":
for root_type in get_root_types():
writer.writerow(["", "", "", 1, "", root_type])
writer.writerow(["", "", "", "", 1, "", root_type])
for account in get_mandatory_group_accounts():
writer.writerow(["", "", "", 1, account, "Asset"])
writer.writerow(["", "", "", "", 1, account, "Asset"])
for account_type in get_mandatory_account_types():
writer.writerow(
["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
)
else:
writer = get_sample_template(writer)

View File

@@ -211,8 +211,7 @@ class ExchangeRateRevaluation(Document):
# 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:
if d.balance != 0:
current_exchange_rate = new_exchange_rate = 0
new_balance_in_account_currency = 0 # this will be '0'
@@ -399,6 +398,9 @@ class ExchangeRateRevaluation(Document):
journal_entry_accounts = []
for d in accounts:
if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
continue
dr_or_cr = (
"debit_in_account_currency"
if d.get("balance_in_account_currency") > 0
@@ -448,7 +450,13 @@ class ExchangeRateRevaluation(Document):
}
)
journal_entry_accounts.append(
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit()
self.gain_loss_unbooked += journal_entry.difference - self.gain_loss_unbooked
journal_entry.append(
"accounts",
{
"account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
@@ -460,10 +468,9 @@ class ExchangeRateRevaluation(Document):
"exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
}
},
)
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit()
journal_entry.save()
@@ -483,6 +490,8 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
conditions.append(gl.company == company)
conditions.append(gl.account == account)
conditions.append(gl.is_cancelled == 0)
conditions.append((gl.debit > 0) | (gl.credit > 0))
conditions.append((gl.debit_in_account_currency > 0) | (gl.credit_in_account_currency > 0))
if party_type:
conditions.append(gl.party_type == party_type)
if party:

View File

@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry'];
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger"];
},
refresh: function(frm) {

View File

@@ -137,7 +137,8 @@
"fieldname": "finance_book",
"fieldtype": "Link",
"label": "Finance Book",
"options": "Finance Book"
"options": "Finance Book",
"read_only": 1
},
{
"fieldname": "2_add_edit_gl_entries",
@@ -538,7 +539,7 @@
"idx": 176,
"is_submittable": 1,
"links": [],
"modified": "2023-01-17 12:53:53.280620",
"modified": "2023-03-01 14:58:59.286591",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@@ -51,7 +51,7 @@ class JournalEntry(AccountsController):
self.validate_multi_currency()
self.set_amounts_in_company_currency()
self.validate_debit_credit_amount()
self.set_total_debit_credit()
# Do not validate while importing via data import
if not frappe.flags.in_import:
self.validate_total_debit_and_credit()
@@ -89,7 +89,13 @@ class JournalEntry(AccountsController):
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
unlink_ref_doc_from_payment_entries(self)
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Payment Ledger Entry",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
)
self.make_gl_entries(1)
self.update_advance_paid()
self.unlink_advance_entry_reference()
@@ -238,21 +244,16 @@ class JournalEntry(AccountsController):
):
processed_assets.append(d.reference_name)
asset = frappe.db.get_value(
"Asset", d.reference_name, ["calculate_depreciation", "value_after_depreciation"], as_dict=1
)
asset = frappe.get_doc("Asset", d.reference_name)
if asset.calculate_depreciation:
continue
depr_value = d.debit or d.credit
frappe.db.set_value(
"Asset",
d.reference_name,
"value_after_depreciation",
asset.value_after_depreciation - depr_value,
)
asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value)
asset.set_status()
def update_inter_company_jv(self):
if (
@@ -348,12 +349,9 @@ class JournalEntry(AccountsController):
else:
depr_value = d.debit or d.credit
frappe.db.set_value(
"Asset",
d.reference_name,
"value_after_depreciation",
asset.value_after_depreciation + depr_value,
)
asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value)
asset.set_status()
def unlink_inter_company_jv(self):
if (
@@ -668,7 +666,6 @@ class JournalEntry(AccountsController):
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 not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
if self.difference:
frappe.throw(

View File

@@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', {
onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"];
if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
@@ -217,7 +217,6 @@ frappe.ui.form.on('Payment Entry', {
frm.toggle_display("set_exchange_gain_loss",
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
frm.refresh_fields();
},
set_dynamic_labels: function(frm) {
@@ -245,8 +244,6 @@ frappe.ui.form.on('Payment Entry', {
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
party_account_currency, "references");
frm.set_currency_labels(["amount"], company_currency, "deductions");
cur_frm.set_df_property("source_exchange_rate", "description",
("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency));

View File

@@ -92,7 +92,13 @@ class PaymentEntry(AccountsController):
self.set_status()
def on_cancel(self):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Payment Ledger Entry",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
)
self.make_gl_entries(cancel=1)
self.update_outstanding_amounts()
self.update_advance_paid()
@@ -410,7 +416,7 @@ class PaymentEntry(AccountsController):
for ref in self.get("references"):
if ref.payment_term and ref.reference_name:
key = (ref.payment_term, ref.reference_name)
key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
invoice_payment_amount_map.setdefault(key, 0.0)
invoice_payment_amount_map[key] += ref.allocated_amount
@@ -418,20 +424,37 @@ class PaymentEntry(AccountsController):
payment_schedule = frappe.get_all(
"Payment Schedule",
filters={"parent": ref.reference_name},
fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"],
fields=[
"paid_amount",
"payment_amount",
"payment_term",
"discount",
"outstanding",
"discount_type",
],
)
for term in payment_schedule:
invoice_key = (term.payment_term, ref.reference_name)
invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype)
invoice_paid_amount_map.setdefault(invoice_key, {})
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
term.discount / 100
)
if not (term.discount_type and term.discount):
continue
if term.discount_type == "Percentage":
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
term.discount / 100
)
else:
invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount
for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1):
if not invoice_paid_amount_map.get(key):
frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1]))
allocated_amount = self.get_allocated_amount_in_transaction_currency(
allocated_amount, key[2], key[1]
)
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
@@ -466,6 +489,33 @@ class PaymentEntry(AccountsController):
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
)
def get_allocated_amount_in_transaction_currency(
self, allocated_amount, reference_doctype, reference_docname
):
"""
Payment Entry could be in base currency while reference's payment schedule
is always in transaction currency.
E.g.
* SI with base=INR and currency=USD
* SI with payment schedule in USD
* PE in INR (accounting done in base currency)
"""
ref_currency, ref_exchange_rate = frappe.db.get_value(
reference_doctype, reference_docname, ["currency", "conversion_rate"]
)
is_single_currency = self.paid_from_account_currency == self.paid_to_account_currency
# PE in different currency
reference_is_multi_currency = self.paid_from_account_currency != ref_currency
if not (is_single_currency and reference_is_multi_currency):
return allocated_amount
allocated_amount = flt(
allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount")
)
return allocated_amount
def set_status(self):
if self.docstatus == 2:
self.status = "Cancelled"
@@ -1636,7 +1686,14 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
@frappe.whitelist()
def get_payment_entry(
dt, dn, party_amount=None, bank_account=None, bank_amount=None, party_type=None, payment_type=None
dt,
dn,
party_amount=None,
bank_account=None,
bank_amount=None,
party_type=None,
payment_type=None,
reference_date=None,
):
reference_doc = None
doc = frappe.get_doc(dt, dn)
@@ -1663,8 +1720,9 @@ def get_payment_entry(
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
)
paid_amount, received_amount, discount_amount = apply_early_payment_discount(
paid_amount, received_amount, doc
reference_date = getdate(reference_date)
paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount(
paid_amount, received_amount, doc, party_account_currency, reference_date
)
pe = frappe.new_doc("Payment Entry")
@@ -1672,6 +1730,7 @@ def get_payment_entry(
pe.company = doc.company
pe.cost_center = doc.get("cost_center")
pe.posting_date = nowdate()
pe.reference_date = reference_date
pe.mode_of_payment = doc.get("mode_of_payment")
pe.party_type = party_type
pe.party = doc.get(scrub(party_type))
@@ -1712,7 +1771,7 @@ def get_payment_entry(
):
for reference in get_reference_as_per_payment_terms(
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
):
pe.append("references", reference)
else:
@@ -1763,16 +1822,17 @@ def get_payment_entry(
if party_account and bank:
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts()
if discount_amount:
pe.set_gain_or_loss(
account_details={
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
"cost_center": pe.cost_center
or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": discount_amount * (-1 if payment_type == "Pay" else 1),
}
base_total_discount_loss = 0
if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
base_total_discount_loss = split_early_payment_discount_loss(pe, doc, valid_discounts)
set_pending_discount_loss(
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
)
pe.set_difference_amount()
pe.set_difference_amount()
return pe
@@ -1883,20 +1943,28 @@ def set_paid_amount_and_received_amount(
return paid_amount, received_amount
def apply_early_payment_discount(paid_amount, received_amount, doc):
def apply_early_payment_discount(
paid_amount, received_amount, doc, party_account_currency, reference_date
):
total_discount = 0
valid_discounts = []
eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule
is_multi_currency = party_account_currency != doc.company_currency
if doc.doctype in eligible_for_payments and has_payment_schedule:
for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
if not term.discounted_amount and term.discount and reference_date <= term.discount_date:
if term.discount_type == "Percentage":
discount_amount = flt(doc.get("grand_total")) * (term.discount / 100)
grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total")
discount_amount = flt(grand_total) * (term.discount / 100)
else:
discount_amount = term.discount
discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1)
# if accounting is done in the same currency, paid_amount = received_amount
conversion_rate = doc.get("conversion_rate", 1) if is_multi_currency else 1
discount_amount_in_foreign_currency = discount_amount * conversion_rate
if doc.doctype == "Sales Invoice":
paid_amount -= discount_amount
@@ -1905,23 +1973,151 @@ def apply_early_payment_discount(paid_amount, received_amount, doc):
received_amount -= discount_amount
paid_amount -= discount_amount_in_foreign_currency
valid_discounts.append({"type": term.discount_type, "discount": term.discount})
total_discount += discount_amount
if total_discount:
money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency"))
currency = doc.get("currency") if is_multi_currency else doc.company_currency
money = frappe.utils.fmt_money(total_discount, currency=currency)
frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
return paid_amount, received_amount, total_discount
return paid_amount, received_amount, total_discount, valid_discounts
def set_pending_discount_loss(
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
):
# If multi-currency, get base discount amount to adjust with base currency deductions/losses
if party_account_currency != doc.company_currency:
discount_amount = discount_amount * doc.get("conversion_rate", 1)
# Avoid considering miniscule losses
discount_amount = flt(discount_amount - base_total_discount_loss, doc.precision("grand_total"))
# Set base discount amount (discount loss/pending rounding loss) in deductions
if discount_amount > 0.0:
positive_negative = -1 if pe.payment_type == "Pay" else 1
# If tax loss booking is enabled, pending loss will be rounding loss.
# Otherwise it will be the total discount loss.
book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
account_type = "round_off_account" if book_tax_loss else "default_discount_account"
pe.set_gain_or_loss(
account_details={
"account": frappe.get_cached_value("Company", pe.company, account_type),
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": discount_amount * positive_negative,
}
)
def split_early_payment_discount_loss(pe, doc, valid_discounts) -> float:
"""Split early payment discount into Income Loss & Tax Loss."""
total_discount_percent = get_total_discount_percent(doc, valid_discounts)
if not total_discount_percent:
return 0.0
base_loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent)
base_loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent)
# Round off total loss rather than individual losses to reduce rounding error
return flt(base_loss_on_income + base_loss_on_taxes, doc.precision("grand_total"))
def get_total_discount_percent(doc, valid_discounts) -> float:
"""Get total percentage and amount discount applied as a percentage."""
total_discount_percent = (
sum(
discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage"
)
or 0.0
)
# Operate in percentages only as it makes the income & tax split easier
total_discount_amount = (
sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Amount")
or 0.0
)
if total_discount_amount:
discount_percentage = (total_discount_amount / doc.get("grand_total")) * 100
total_discount_percent += discount_percentage
return total_discount_percent
return total_discount_percent
def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
"""Add loss on income discount in base currency."""
precision = doc.precision("total")
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
pe.append(
"deductions",
{
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": flt(base_loss_on_income, precision),
},
)
return base_loss_on_income # Return loss without rounding
def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
"""Add loss on tax discount in base currency."""
tax_discount_loss = {}
base_total_tax_loss = 0
precision = doc.precision("tax_amount_after_discount_amount", "taxes")
# The same account head could be used more than once
for tax in doc.get("taxes", []):
base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * (
total_discount_percentage / 100
)
account = tax.get("account_head")
if not tax_discount_loss.get(account):
tax_discount_loss[account] = base_tax_loss
else:
tax_discount_loss[account] += base_tax_loss
for account, loss in tax_discount_loss.items():
base_total_tax_loss += loss
if loss == 0.0:
continue
pe.append(
"deductions",
{
"account": account,
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
"amount": flt(loss, precision),
},
)
return base_total_tax_loss # Return loss without rounding
def get_reference_as_per_payment_terms(
payment_schedule, dt, dn, doc, grand_total, outstanding_amount
payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
):
references = []
is_multi_currency_acc = (doc.currency != doc.company_currency) and (
party_account_currency != doc.company_currency
)
for payment_term in payment_schedule:
payment_term_outstanding = flt(
payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount")
)
if not is_multi_currency_acc:
# If accounting is done in company currency for multi-currency transaction
payment_term_outstanding = flt(
payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount")
)
if payment_term_outstanding:
references.append(

View File

@@ -5,7 +5,7 @@ import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import (
@@ -256,10 +256,25 @@ class TestPaymentEntry(FrappeTestCase):
},
)
si.save()
si.submit()
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 236.0)
self.assertEqual(pe_with_tax_loss.paid_amount, 212.4)
self.assertEqual(pe_with_tax_loss.deductions[0].amount, 20.0) # Loss on Income
self.assertEqual(pe_with_tax_loss.deductions[1].amount, 3.6) # Loss on Tax
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe.references[0].allocated_amount, 236.0)
self.assertEqual(pe.paid_amount, 212.4)
self.assertEqual(pe.deductions[0].amount, 23.6)
pe.submit()
si.load_from_db()
@@ -269,6 +284,190 @@ class TestPaymentEntry(FrappeTestCase):
self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
def test_payment_entry_against_payment_terms_with_discount_amount(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
si.payment_terms_template = "Test Discount Amount Template"
create_payment_terms_template_with_discount(
name="30 Credit Days with Rs.50 Discount",
discount_type="Amount",
discount=50,
template_name="Test Discount Amount Template",
)
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": "_Test Account Service Tax - _TC",
"cost_center": "_Test Cost Center - _TC",
"description": "Service Tax",
"rate": 18,
},
)
si.save()
si.submit()
# Set reference date past discount cut off date
pe_1 = get_payment_entry(
"Sales Invoice",
si.name,
bank_account="_Test Cash - _TC",
reference_date=frappe.utils.add_days(si.posting_date, 2),
)
self.assertEqual(pe_1.paid_amount, 236.0) # discount not applied
# Test if tax loss is booked on enabling configuration
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe_with_tax_loss.deductions[0].amount, 42.37) # Loss on Income
self.assertEqual(pe_with_tax_loss.deductions[1].amount, 7.63) # Loss on Tax
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
self.assertEqual(pe.references[0].allocated_amount, 236.0)
self.assertEqual(pe.paid_amount, 186)
self.assertEqual(pe.deductions[0].amount, 50.0)
pe.submit()
si.load_from_db()
self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
self.assertEqual(si.payment_schedule[0].paid_amount, 186)
self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 50)
@change_settings(
"Accounts Settings",
{
"allow_multi_currency_invoices_against_single_party_account": 1,
"book_tax_discount_loss": 1,
},
)
def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount(
self,
):
"""
1. Multi-currency SI with single currency accounting (company currency)
2. PE with early payment discount
3. Test if Paid Amount is calculated in company currency
4. Test if deductions are calculated in company currency
SI is in USD to document agreed amounts that are in USD, but the accounting is in base currency.
"""
si = create_sales_invoice(
customer="_Test Customer",
currency="USD",
conversion_rate=50,
do_not_save=1,
)
create_payment_terms_template_with_discount()
si.payment_terms_template = "Test Discount Template"
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
si.save()
si.submit()
pe = get_payment_entry(
"Sales Invoice",
si.name,
bank_account="_Test Bank - _TC",
)
pe.reference_no = si.name
pe.reference_date = nowdate()
# Early payment discount loss on income
self.assertEqual(pe.paid_amount, 4500.0) # Amount in company currency
self.assertEqual(pe.received_amount, 4500.0)
self.assertEqual(pe.deductions[0].amount, 500.0)
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
self.assertEqual(pe.difference_amount, 0.0)
pe.insert()
pe.submit()
expected_gle = dict(
(d[0], d)
for d in [
["Debtors - _TC", 0, 5000, si.name],
["_Test Bank - _TC", 4500, 0, None],
["Write Off - _TC", 500.0, 0, None],
]
)
self.validate_gl_entries(pe.name, expected_gle)
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0)
def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(self):
"""
1. Multi-currency SI with multi-currency accounting
2. PE with early payment discount and also exchange loss
3. Test if Paid Amount is calculated in transaction currency
4. Test if deductions are calculated in base/company currency
5. Test if exchange loss is reflected in difference
"""
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=50,
do_not_save=1,
)
create_payment_terms_template_with_discount()
si.payment_terms_template = "Test Discount Template"
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
si.save()
si.submit()
pe = get_payment_entry(
"Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700
)
pe.reference_no = si.name
pe.reference_date = nowdate()
# Early payment discount loss on income
self.assertEqual(pe.paid_amount, 90.0)
self.assertEqual(pe.received_amount, 4200.0) # 5000 - 500 (discount) - 300 (exchange loss)
self.assertEqual(pe.deductions[0].amount, 500.0)
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
# Exchange loss
self.assertEqual(pe.difference_amount, 300.0)
pe.append(
"deductions",
{
"account": "_Test Exchange Gain/Loss - _TC",
"cost_center": "_Test Cost Center - _TC",
"amount": 300.0,
},
)
pe.insert()
pe.submit()
self.assertEqual(pe.difference_amount, 0.0)
expected_gle = dict(
(d[0], d)
for d in [
["_Test Receivable USD - _TC", 0, 5000, si.name],
["_Test Bank - _TC", 4200, 0, None],
["Write Off - _TC", 500.0, 0, None],
["_Test Exchange Gain/Loss - _TC", 300.0, 0, None],
]
)
self.validate_gl_entries(pe.name, expected_gle)
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0)
def test_payment_against_purchase_invoice_to_check_status(self):
pi = make_purchase_invoice(
supplier="_Test Supplier USD",
@@ -839,24 +1038,27 @@ def create_payment_terms_template():
).insert()
def create_payment_terms_template_with_discount():
def create_payment_terms_template_with_discount(
name=None, discount_type=None, discount=None, template_name=None
):
create_payment_term(name or "30 Credit Days with 10% Discount")
template_name = template_name or "Test Discount Template"
create_payment_term("30 Credit Days with 10% Discount")
if not frappe.db.exists("Payment Terms Template", "Test Discount Template"):
payment_term_template = frappe.get_doc(
if not frappe.db.exists("Payment Terms Template", template_name):
frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": "Test Discount Template",
"template_name": template_name,
"allocate_payment_based_on_payment_terms": 1,
"terms": [
{
"doctype": "Payment Terms Template Detail",
"payment_term": "30 Credit Days with 10% Discount",
"payment_term": name or "30 Credit Days with 10% Discount",
"invoice_portion": 100,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 2,
"discount": 10,
"discount_type": discount_type or "Percentage",
"discount": discount or 10,
"discount_validity_based_on": "Day(s) after invoice date",
"discount_validity": 1,
}

View File

@@ -3,6 +3,7 @@
"creation": "2016-06-15 15:56:30.815503",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"account",
"cost_center",
@@ -17,9 +18,7 @@
"in_list_view": 1,
"label": "Account",
"options": "Account",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fieldname": "cost_center",
@@ -28,37 +27,30 @@
"label": "Cost Center",
"options": "Cost Center",
"print_hide": 1,
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"label": "Amount (Company Currency)",
"options": "Company:company:default_currency",
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
"show_days": 1,
"show_seconds": 1
"label": "Description"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-09-12 20:38:08.110674",
"modified": "2023-03-06 07:11:57.739619",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Deduction",
@@ -66,5 +58,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@@ -272,4 +272,32 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
}
};
frappe.ui.form.on('Payment Reconciliation Allocation', {
allocated_amount: function(frm, cdt, cdn) {
let row = locals[cdt][cdn];
// filter invoice
let invoice = frm.doc.invoices.filter((x) => (x.invoice_number == row.invoice_number));
// filter payment
let payment = frm.doc.payments.filter((x) => (x.reference_name == row.reference_name));
frm.call({
doc: frm.doc,
method: 'calculate_difference_on_allocation_change',
args: {
payment_entry: payment,
invoice: invoice,
allocated_amount: row.allocated_amount
},
callback: (r) => {
if (r.message) {
row.difference_amount = r.message;
frm.refresh();
}
}
});
}
});
extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm}));

View File

@@ -221,15 +221,27 @@ class PaymentReconciliation(Document):
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
if frappe.get_cached_value(
"Account", self.receivable_payable_account, "account_currency"
) != frappe.get_cached_value("Company", self.company, "default_currency"):
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
return difference_amount
@frappe.whitelist()
def calculate_difference_on_allocation_change(self, payment_entry, invoice, allocated_amount):
invoice_exchange_map = self.get_invoice_exchange_map(invoice, payment_entry)
invoice[0]["exchange_rate"] = invoice_exchange_map.get(invoice[0].get("invoice_number"))
new_difference_amount = self.get_difference_amount(
payment_entry[0], invoice[0], allocated_amount
)
return new_difference_amount
@frappe.whitelist()
def allocate_entries(self, args):
self.validate_entries()
@@ -368,6 +380,7 @@ class PaymentReconciliation(Document):
"exchange_rate": 1,
"cost_center": erpnext.get_default_cost_center(self.company),
reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
reverse_dr_or_cr: flt(row.difference_amount),
}
)

View File

@@ -5,7 +5,7 @@ import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, nowdate
from erpnext import get_default_cost_center
@@ -349,6 +349,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
si.reload()
@@ -390,6 +395,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
# check PR tool output
@@ -414,6 +424,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
# assert outstanding
@@ -450,6 +465,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Difference amount should not be calculated for base currency accounts
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
self.assertEqual(pr.get("invoices"), [])
@@ -824,6 +844,52 @@ class TestPaymentReconciliation(FrappeTestCase):
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
@change_settings(
"Accounts Settings",
{
"allow_multi_currency_invoices_against_single_party_account": 1,
},
)
def test_no_difference_amount_for_base_currency_accounts(self):
# Make Sale Invoice
si = self.create_sales_invoice(
qty=1, rate=1, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer
si.currency = "EUR"
si.conversion_rate = 85
si.debit_to = self.debit_to
si.save().submit()
# Make payment using Payment Entry
pe1 = create_payment_entry(
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer,
paid_from=self.debit_to,
paid_to=self.bank,
paid_amount=100,
)
pe1.save()
pe1.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer
pr.receivable_payable_account = self.debit_to
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
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, 85)
self.assertEqual(pr.allocation[0].difference_amount, 0)
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):

View File

@@ -495,26 +495,28 @@ def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype"""
dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]:
grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.outstanding_amount)
else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
if not ref_doc.get("is_pos"):
if ref_doc.party_account_currency == ref_doc.currency:
grand_total = flt(ref_doc.outstanding_amount)
else:
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
elif dt == "Sales Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
grand_total = pay.amount
break
elif dt == "POS Invoice":
for pay in ref_doc.payments:
if pay.type == "Phone" and pay.account == payment_account:
grand_total = pay.amount
break
elif dt == "Fees":
grand_total = ref_doc.outstanding_amount
if grand_total > 0:
return grand_total
else:
frappe.throw(_("Payment Entry is already created"))

View File

@@ -6,6 +6,7 @@ import unittest
import frappe
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.setup.utils import get_exchange_rate
@@ -45,7 +46,10 @@ class TestPaymentRequest(unittest.TestCase):
frappe.get_doc(method).insert(ignore_permissions=True)
def test_payment_request_linkings(self):
so_inr = make_sales_order(currency="INR")
so_inr = make_sales_order(currency="INR", do_not_save=True)
so_inr.disable_rounded_total = 1
so_inr.save()
pr = make_payment_request(
dt="Sales Order",
dn=so_inr.name,
@@ -71,6 +75,29 @@ class TestPaymentRequest(unittest.TestCase):
self.assertEqual(pr.reference_name, si_usd.name)
self.assertEqual(pr.currency, "USD")
def test_payment_entry_against_purchase_invoice(self):
si_usd = make_purchase_invoice(
customer="_Test Supplier USD",
debit_to="_Test Payable USD - _TC",
currency="USD",
conversion_rate=50,
)
pr = make_payment_request(
dt="Purchase Invoice",
dn=si_usd.name,
recipient_id="user@example.com",
mute_email=1,
payment_gateway_account="_Test Gateway - USD",
submit_doc=1,
return_doc=1,
)
pe = pr.create_payment_entry()
pr.load_from_db()
self.assertEqual(pr.status, "Paid")
def test_payment_entry(self):
frappe.db.set_value(
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"

View File

@@ -4,12 +4,13 @@
import frappe
from frappe import _
from frappe.utils import flt
from frappe.query_builder.functions import Sum
from frappe.utils import add_days, flt
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.utils import get_account_currency
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, validate_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
@@ -20,9 +21,17 @@ class PeriodClosingVoucher(AccountsController):
def on_submit(self):
self.db_set("gle_processing_status", "In Progress")
self.make_gl_entries()
get_opening_entries = False
if not frappe.db.exists(
"Period Closing Voucher", {"company": self.company, "docstatus": 1, "name": ("!=", self.name)}
):
get_opening_entries = True
self.make_gl_entries(get_opening_entries=get_opening_entries)
def on_cancel(self):
self.validate_future_closing_vouchers()
self.db_set("gle_processing_status", "In Progress")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
gle_count = frappe.db.count(
@@ -42,6 +51,25 @@ class PeriodClosingVoucher(AccountsController):
else:
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
self.delete_closing_entries()
def validate_future_closing_vouchers(self):
if frappe.db.exists(
"Period Closing Voucher",
{"posting_date": (">", self.posting_date), "docstatus": 1, "company": self.company},
):
frappe.throw(
_(
"You can not cancel this Period Closing Voucher, please cancel the future Period Closing Vouchers first"
)
)
def delete_closing_entries(self):
closing_balance = frappe.qb.DocType("Account Closing Balance")
frappe.qb.from_(closing_balance).delete().where(
closing_balance.period_closing_voucher == self.name
).run()
def validate_account_head(self):
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
@@ -56,8 +84,6 @@ class PeriodClosingVoucher(AccountsController):
frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency))
def validate_posting_date(self):
from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year
validate_fiscal_year(
self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self
)
@@ -66,6 +92,8 @@ class PeriodClosingVoucher(AccountsController):
self.posting_date, self.fiscal_year, company=self.company
)[1]
self.check_if_previous_year_closed()
pce = frappe.db.sql(
"""select name from `tabPeriod Closing Voucher`
where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
@@ -78,28 +106,64 @@ class PeriodClosingVoucher(AccountsController):
)
)
def make_gl_entries(self):
def check_if_previous_year_closed(self):
last_year_closing = add_days(self.year_start_date, -1)
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
if previous_fiscal_year and not frappe.db.exists(
"GL Entry", {"posting_date": ("<=", last_year_closing), "company": self.company}
):
return
if previous_fiscal_year and not frappe.db.exists(
"Period Closing Voucher",
{"posting_date": ("<=", last_year_closing), "docstatus": 1, "company": self.company},
):
frappe.throw(_("Previous Year is not closed, please close it first"))
def make_gl_entries(self, get_opening_entries=False):
gl_entries = self.get_gl_entries()
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
if gl_entries:
if len(gl_entries) > 5000:
frappe.enqueue(process_gl_entries, gl_entries=gl_entries, queue="long")
frappe.enqueue(
process_gl_entries,
gl_entries=gl_entries,
closing_entries=closing_entries,
voucher_name=self.name,
queue="long",
)
frappe.msgprint(
_("The GL Entries will be processed in the background, it can take a few minutes."),
alert=True,
)
else:
process_gl_entries(gl_entries)
process_gl_entries(gl_entries, closing_entries, voucher_name=self.name)
def get_grouped_gl_entries(self, get_opening_entries=False):
closing_entries = []
for acc in self.get_balances_based_on_dimensions(
group_by_account=True, for_aggregation=True, get_opening_entries=get_opening_entries
):
closing_entries.append(self.get_closing_entries(acc))
return closing_entries
def get_gl_entries(self):
gl_entries = []
# pl account
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=True):
for acc in self.get_balances_based_on_dimensions(
group_by_account=True, report_type="Profit and Loss"
):
if flt(acc.bal_in_company_currency):
gl_entries.append(self.get_gle_for_pl_account(acc))
# closing liability account
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=False):
for acc in self.get_balances_based_on_dimensions(
group_by_account=False, report_type="Profit and Loss"
):
if flt(acc.bal_in_company_currency):
gl_entries.append(self.get_gle_for_closing_account(acc))
@@ -108,6 +172,8 @@ class PeriodClosingVoucher(AccountsController):
def get_gle_for_pl_account(self, acc):
gl_entry = self.get_gl_dict(
{
"company": self.company,
"closing_date": self.posting_date,
"account": acc.account,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
@@ -120,6 +186,7 @@ class PeriodClosingVoucher(AccountsController):
if flt(acc.bal_in_account_currency) > 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
"is_period_closing_voucher_entry": 1,
},
item=acc,
)
@@ -129,6 +196,8 @@ class PeriodClosingVoucher(AccountsController):
def get_gle_for_closing_account(self, acc):
gl_entry = self.get_gl_dict(
{
"company": self.company,
"closing_date": self.posting_date,
"account": self.closing_account_head,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
@@ -141,12 +210,36 @@ class PeriodClosingVoucher(AccountsController):
if flt(acc.bal_in_account_currency) < 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
"is_period_closing_voucher_entry": 1,
},
item=acc,
)
self.update_default_dimensions(gl_entry, acc)
return gl_entry
def get_closing_entries(self, acc):
closing_entry = self.get_gl_dict(
{
"company": self.company,
"closing_date": self.posting_date,
"period_closing_voucher": self.name,
"account": acc.account,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": flt(acc.debit_in_account_currency),
"debit": flt(acc.debit),
"credit_in_account_currency": flt(acc.credit_in_account_currency),
"credit": flt(acc.credit),
},
item=acc,
)
for dimension in self.accounting_dimensions:
closing_entry.update({dimension: acc.get(dimension)})
return closing_entry
def update_default_dimensions(self, gl_entry, acc):
if not self.accounting_dimensions:
self.accounting_dimensions = get_accounting_dimensions()
@@ -154,47 +247,88 @@ class PeriodClosingVoucher(AccountsController):
for dimension in self.accounting_dimensions:
gl_entry.update({dimension: acc.get(dimension)})
def get_pl_balances_based_on_dimensions(self, group_by_account=False):
def get_balances_based_on_dimensions(
self, group_by_account=False, report_type=None, for_aggregation=False, get_opening_entries=False
):
"""Get balance for dimension-wise pl accounts"""
dimension_fields = ["t1.cost_center", "t1.finance_book"]
qb_dimension_fields = ["cost_center", "finance_book", "project"]
self.accounting_dimensions = get_accounting_dimensions()
for dimension in self.accounting_dimensions:
dimension_fields.append("t1.{0}".format(dimension))
qb_dimension_fields.append(dimension)
if group_by_account:
dimension_fields.append("t1.account")
qb_dimension_fields.append("account")
return frappe.db.sql(
"""
select
t2.account_currency,
{dimension_fields},
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
from `tabGL Entry` t1, `tabAccount` t2
where
t1.is_cancelled = 0
and t1.account = t2.name
and t2.report_type = 'Profit and Loss'
and t2.docstatus < 2
and t2.company = %s
and t1.posting_date between %s and %s
group by {dimension_fields}
""".format(
dimension_fields=", ".join(dimension_fields)
),
(self.company, self.get("year_start_date"), self.posting_date),
as_dict=1,
account_filters = {
"company": self.company,
"is_group": 0,
}
if report_type:
account_filters.update({"report_type": report_type})
accounts = frappe.get_all("Account", filters=account_filters, pluck="name")
gl_entry = frappe.qb.DocType("GL Entry")
query = frappe.qb.from_(gl_entry).select(gl_entry.account, gl_entry.account_currency)
if not for_aggregation:
query = query.select(
(Sum(gl_entry.debit_in_account_currency) - Sum(gl_entry.credit_in_account_currency)).as_(
"bal_in_account_currency"
),
(Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("bal_in_company_currency"),
)
else:
query = query.select(
(Sum(gl_entry.debit_in_account_currency)).as_("debit_in_account_currency"),
(Sum(gl_entry.credit_in_account_currency)).as_("credit_in_account_currency"),
(Sum(gl_entry.debit)).as_("debit"),
(Sum(gl_entry.credit)).as_("credit"),
)
for dimension in qb_dimension_fields:
query = query.select(gl_entry[dimension])
query = query.where(
(gl_entry.company == self.company)
& (gl_entry.is_cancelled == 0)
& (gl_entry.account.isin(accounts))
)
if get_opening_entries:
query = query.where(
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
| gl_entry.is_opening
== "Yes"
)
else:
query = query.where(
gl_entry.posting_date.between(self.get("year_start_date"), self.posting_date)
& gl_entry.is_opening
== "No"
)
def process_gl_entries(gl_entries):
if for_aggregation:
query = query.where(gl_entry.voucher_type != "Period Closing Voucher")
for dimension in qb_dimension_fields:
query = query.groupby(gl_entry[dimension])
return query.run(as_dict=1)
def process_gl_entries(gl_entries, closing_entries, voucher_name=None):
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries,
)
from erpnext.accounts.general_ledger import make_gl_entries
try:
make_gl_entries(gl_entries, merge_entries=False)
make_closing_entries(gl_entries + closing_entries, voucher_name=voucher_name)
frappe.db.set_value(
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed"
)

View File

@@ -16,16 +16,17 @@ from erpnext.accounts.utils import get_fiscal_year, now
class TestPeriodClosingVoucher(unittest.TestCase):
def test_closing_entry(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
jv1 = make_journal_entry(
posting_date="2021-03-15",
amount=400,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
posting_date=now(),
save=False,
)
jv1.company = company
@@ -33,18 +34,18 @@ class TestPeriodClosingVoucher(unittest.TestCase):
jv1.submit()
jv2 = make_journal_entry(
posting_date="2021-03-15",
amount=600,
account1="Cost of Goods Sold - TPC",
account2="Cash - TPC",
cost_center=cost_center,
posting_date=now(),
save=False,
)
jv2.company = company
jv2.save()
jv2.submit()
pcv = self.make_period_closing_voucher()
pcv = self.make_period_closing_voucher(posting_date="2021-03-31")
surplus_account = pcv.closing_account_head
expected_gle = (
@@ -65,6 +66,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
def test_cost_center_wise_posting(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
surplus_account = create_account()
@@ -81,6 +83,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
debit_to="Debtors - TPC",
currency="USD",
customer="_Test Customer USD",
posting_date="2021-03-15",
)
create_sales_invoice(
company=company,
@@ -91,9 +94,10 @@ class TestPeriodClosingVoucher(unittest.TestCase):
debit_to="Debtors - TPC",
currency="USD",
customer="_Test Customer USD",
posting_date="2021-03-15",
)
pcv = self.make_period_closing_voucher(submit=False)
pcv = self.make_period_closing_voucher(posting_date="2021-03-31", submit=False)
pcv.save()
pcv.submit()
surplus_account = pcv.closing_account_head
@@ -128,12 +132,13 @@ class TestPeriodClosingVoucher(unittest.TestCase):
def test_period_closing_with_finance_book_entries(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
si = create_sales_invoice(
create_sales_invoice(
company=company,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
@@ -142,6 +147,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
debit_to="Debtors - TPC",
currency="USD",
customer="_Test Customer USD",
posting_date="2021-03-15",
)
jv = make_journal_entry(
@@ -149,14 +155,14 @@ class TestPeriodClosingVoucher(unittest.TestCase):
account2="Sales - TPC",
amount=400,
cost_center=cost_center,
posting_date=now(),
posting_date="2021-03-15",
)
jv.company = company
jv.finance_book = create_finance_book().name
jv.save()
jv.submit()
pcv = self.make_period_closing_voucher()
pcv = self.make_period_closing_voucher(posting_date="2021-03-31")
surplus_account = pcv.closing_account_head
expected_gle = (
@@ -177,14 +183,130 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertSequenceEqual(pcv_gle, expected_gle)
def make_period_closing_voucher(self, submit=True):
def test_gl_entries_restrictions(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
company = create_company()
cost_center = create_cost_center("Test Cost Center 1")
self.make_period_closing_voucher(posting_date="2021-03-31")
jv1 = make_journal_entry(
posting_date="2021-03-15",
amount=400,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center,
save=False,
)
jv1.company = company
jv1.save()
self.assertRaises(frappe.ValidationError, jv1.submit)
def test_closing_balance_with_dimensions(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
frappe.db.sql("delete from `tabAccount Closing Balance` where company='Test PCV Company'")
company = create_company()
cost_center1 = create_cost_center("Test Cost Center 1")
cost_center2 = create_cost_center("Test Cost Center 2")
jv1 = make_journal_entry(
posting_date="2021-03-15",
amount=400,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center1,
save=False,
)
jv1.company = company
jv1.save()
jv1.submit()
jv2 = make_journal_entry(
posting_date="2021-03-15",
amount=200,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center2,
save=False,
)
jv2.company = company
jv2.save()
jv2.submit()
pcv1 = self.make_period_closing_voucher(posting_date="2021-03-31")
closing_balance = frappe.db.get_value(
"Account Closing Balance",
{
"account": "Sales - TPC",
"cost_center": cost_center1,
"period_closing_voucher": pcv1.name,
"is_period_closing_voucher_entry": 0,
},
["credit", "credit_in_account_currency"],
as_dict=1,
)
self.assertEqual(closing_balance.credit, 400)
self.assertEqual(closing_balance.credit_in_account_currency, 400)
jv3 = make_journal_entry(
posting_date="2022-03-15",
amount=300,
account1="Cash - TPC",
account2="Sales - TPC",
cost_center=cost_center2,
save=False,
)
jv3.company = company
jv3.save()
jv3.submit()
pcv2 = self.make_period_closing_voucher(posting_date="2022-03-31")
cc1_closing_balance = frappe.db.get_value(
"Account Closing Balance",
{
"account": "Sales - TPC",
"cost_center": cost_center1,
"period_closing_voucher": pcv2.name,
"is_period_closing_voucher_entry": 0,
},
["credit", "credit_in_account_currency"],
as_dict=1,
)
cc2_closing_balance = frappe.db.get_value(
"Account Closing Balance",
{
"account": "Sales - TPC",
"cost_center": cost_center2,
"period_closing_voucher": pcv2.name,
"is_period_closing_voucher_entry": 0,
},
["credit", "credit_in_account_currency"],
as_dict=1,
)
self.assertEqual(cc1_closing_balance.credit, 400)
self.assertEqual(cc1_closing_balance.credit_in_account_currency, 400)
self.assertEqual(cc2_closing_balance.credit, 500)
self.assertEqual(cc2_closing_balance.credit_in_account_currency, 500)
def make_period_closing_voucher(self, posting_date=None, submit=True):
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
pcv = frappe.get_doc(
{
"doctype": "Period Closing Voucher",
"transaction_date": today(),
"posting_date": today(),
"transaction_date": posting_date or today(),
"posting_date": posting_date or today(),
"company": "Test PCV Company",
"fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0],
"cost_center": cost_center,

View File

@@ -21,8 +21,24 @@ class POSClosingEntry(StatusUpdater):
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
self.validate_duplicate_pos_invoices()
self.validate_pos_invoices()
def validate_duplicate_pos_invoices(self):
pos_occurences = {}
for idx, inv in enumerate(self.pos_transactions, 1):
pos_occurences.setdefault(inv.pos_invoice, []).append(idx)
error_list = []
for key, value in pos_occurences.items():
if len(value) > 1:
error_list.append(
_("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
)
if error_list:
frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True)
def validate_pos_invoices(self):
invalid_rows = []
for d in self.pos_transactions:

View File

@@ -112,7 +112,8 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
party_type: "Customer",
account: this.frm.doc.debit_to,
price_list: this.frm.doc.selling_price_list,
pos_profile: pos_profile
pos_profile: pos_profile,
company_address: this.frm.doc.company_address
}, () => {
this.apply_pricing_rule();
});

View File

@@ -161,7 +161,7 @@ class POSInvoice(SalesInvoice):
bold_item_name = frappe.bold(item.item_name)
bold_extra_batch_qty_needed = frappe.bold(
abs(available_batch_qty - reserved_batch_qty - item.qty)
abs(available_batch_qty - reserved_batch_qty - item.stock_qty)
)
bold_invalid_batch_no = frappe.bold(item.batch_no)
@@ -172,7 +172,7 @@ class POSInvoice(SalesInvoice):
).format(item.idx, bold_invalid_batch_no, bold_item_name),
title=_("Item Unavailable"),
)
elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
elif (available_batch_qty - reserved_batch_qty - item.stock_qty) < 0:
frappe.throw(
_(
"Row #{}: Batch No. {} of item {} has less than required stock available, {} more required"
@@ -246,7 +246,7 @@ class POSInvoice(SalesInvoice):
),
title=_("Item Unavailable"),
)
elif is_stock_item and flt(available_stock) < flt(d.qty):
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
frappe.throw(
_(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
@@ -651,7 +651,7 @@ def get_bundle_availability(bundle_item_code, warehouse):
item_pos_reserved_qty = get_pos_reserved_qty(item.item_code, warehouse)
available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.qty
max_available_bundles = available_qty / item.stock_qty
if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item"
):

View File

@@ -17,6 +17,22 @@ class POSInvoiceMergeLog(Document):
def validate(self):
self.validate_customer()
self.validate_pos_invoice_status()
self.validate_duplicate_pos_invoices()
def validate_duplicate_pos_invoices(self):
pos_occurences = {}
for idx, inv in enumerate(self.pos_invoices, 1):
pos_occurences.setdefault(inv.pos_invoice, []).append(idx)
error_list = []
for key, value in pos_occurences.items():
if len(value) > 1:
error_list.append(
_("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
)
if error_list:
frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True)
def validate_customer(self):
if self.merge_invoices_based_on == "Customer Group":
@@ -425,6 +441,8 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
if closing_entry:
closing_entry.set_status(update=True, status="Failed")
if type(error_message) == list:
error_message = frappe.json.dumps(error_message)
closing_entry.db_set("error_message", error_message)
raise

View File

@@ -15,7 +15,7 @@
</div>
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
<div>
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party[0] }}</b></h5>
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party_name[0] }}</b></h5>
<h5 style="float: right;">
{{ _("Date: ") }}
<b>{{ frappe.format(filters.from_date, 'Date')}}

View File

@@ -24,7 +24,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get
class ProcessStatementOfAccounts(Document):
def validate(self):
if not self.subject:
self.subject = "Statement Of Accounts for {{ customer.name }}"
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
if not self.body:
self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}."
@@ -87,6 +87,7 @@ def get_report_pdf(doc, consolidated=True):
"account": [doc.account] if doc.account else None,
"party_type": "Customer",
"party": [entry.customer],
"party_name": [entry.customer_name] if entry.customer_name else None,
"presentation_currency": presentation_currency,
"group_by": doc.group_by,
"currency": doc.currency,
@@ -156,7 +157,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
]
return frappe.get_list(
"Customer",
fields=["name", "email_id"],
fields=["name", "customer_name", "email_id"],
filters=[[fields_dict[customer_collection], "IN", selected]],
)
@@ -179,7 +180,7 @@ def get_customers_based_on_sales_person(sales_person):
if sales_person_records.get("Customer"):
return frappe.get_list(
"Customer",
fields=["name", "email_id"],
fields=["name", "customer_name", "email_id"],
filters=[["name", "in", list(sales_person_records["Customer"])]],
)
else:
@@ -228,7 +229,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
if customer_collection == "Sales Partner":
customers = frappe.get_list(
"Customer",
fields=["name", "email_id"],
fields=["name", "customer_name", "email_id"],
filters=[["default_sales_partner", "=", collection_name]],
)
else:
@@ -245,7 +246,12 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
continue
customer_list.append(
{"name": customer.name, "primary_email": primary_email, "billing_email": billing_email}
{
"name": customer.name,
"customer_name": customer.customer_name,
"primary_email": primary_email,
"billing_email": billing_email,
}
)
return customer_list

View File

@@ -1,12 +1,12 @@
{
"actions": [],
"allow_workflow": 1,
"creation": "2020-08-03 16:35:21.852178",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"customer",
"customer_name",
"billing_email",
"primary_email"
],
@@ -30,11 +30,18 @@
"fieldtype": "Read Only",
"in_list_view": 1,
"label": "Billing Email"
},
{
"fetch_from": "customer.customer_name",
"fieldname": "customer_name",
"fieldtype": "Data",
"label": "Customer Name",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-08-03 22:55:38.875601",
"modified": "2023-03-13 00:12:34.508086",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts Customer",
@@ -43,5 +50,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
super.onload();
// Ignore linked advances
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice'];
this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"];
if(!this.frm.doc.__islocal) {
// show credit_to in print format
@@ -82,7 +82,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
if(doc.docstatus == 1 && doc.outstanding_amount != 0
&& !(doc.is_return && doc.return_against) && !doc.on_hold) {
this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create'));
this.frm.add_custom_button(
__('Payment'),
() => this.make_payment_entry(),
__('Create')
);
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
}

View File

@@ -118,6 +118,7 @@
"paid_amount",
"advances_section",
"allocate_advances_automatically",
"only_include_allocated_payments",
"get_advances",
"advances",
"advance_tax",
@@ -1550,17 +1551,24 @@
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
},
{
"default": "0",
"depends_on": "allocate_advances_automatically",
"description": "Advance payments allocated against orders will only be fetched",
"fieldname": "only_include_allocated_payments",
"fieldtype": "Check",
"label": "Only Include Allocated Payments"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2023-01-28 19:18:56.586321",
"modified": "2023-04-03 22:57:14.074982",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [

View File

@@ -5,6 +5,7 @@
import frappe
from frappe import _, throw
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
import erpnext
@@ -116,7 +117,7 @@ class PurchaseInvoice(BuyingController):
self.validate_expense_account()
self.set_against_expense_account()
self.validate_write_off_account()
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items")
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
self.create_remarks()
self.set_status()
self.validate_purchase_receipt_if_update_stock()
@@ -231,7 +232,7 @@ class PurchaseInvoice(BuyingController):
)
if (
cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate"))
cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))
and not self.is_return
and not self.is_internal_supplier
):
@@ -580,6 +581,7 @@ class PurchaseInvoice(BuyingController):
self.make_supplier_gl_entry(gl_entries)
self.make_item_gl_entries(gl_entries)
self.make_precision_loss_gl_entry(gl_entries)
if self.check_asset_cwip_enabled():
self.get_asset_gl_entry(gl_entries)
@@ -974,6 +976,28 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount, item.precision("item_tax_amount")
)
def make_precision_loss_gl_entry(self, gl_entries):
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
self.company, "Purchase Invoice", self.name
)
precision_loss = self.get("base_net_total") - flt(
self.get("net_total") * self.conversion_rate, self.precision("net_total")
)
if precision_loss:
gl_entries.append(
self.get_gl_dict(
{
"account": round_off_account,
"against": self.supplier,
"credit": precision_loss,
"cost_center": self.cost_center or round_off_cost_center,
"remarks": _("Net total calculation precision loss"),
}
)
)
def get_asset_gl_entry(self, gl_entries):
arbnb_account = self.get_company_default("asset_received_but_not_billed")
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
@@ -1416,6 +1440,8 @@ class PurchaseInvoice(BuyingController):
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
"Payment Ledger Entry",
"Tax Withheld Vouchers",
)
@@ -1463,19 +1489,16 @@ class PurchaseInvoice(BuyingController):
def update_billing_status_in_pr(self, update_modified=True):
updated_pr = []
po_details = []
pr_details_billed_amt = self.get_pr_details_billed_amt()
for d in self.get("items"):
if d.pr_detail:
billed_amt = frappe.db.sql(
"""select sum(amount) from `tabPurchase Invoice Item`
where pr_detail=%s and docstatus=1""",
d.pr_detail,
)
billed_amt = billed_amt and billed_amt[0][0] or 0
frappe.db.set_value(
"Purchase Receipt Item",
d.pr_detail,
"billed_amt",
billed_amt,
flt(pr_details_billed_amt.get(d.pr_detail)),
update_modified=update_modified,
)
updated_pr.append(d.purchase_receipt)
@@ -1485,11 +1508,35 @@ class PurchaseInvoice(BuyingController):
if po_details:
updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
adjust_incoming_rate = frappe.db.get_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
)
for pr in set(updated_pr):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
pr_doc = frappe.get_doc("Purchase Receipt", pr)
update_billing_percentage(pr_doc, update_modified=update_modified)
update_billing_percentage(
pr_doc, update_modified=update_modified, adjust_incoming_rate=adjust_incoming_rate
)
def get_pr_details_billed_amt(self):
# Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice
pr_details_billed_amt = {}
pr_details = [d.get("pr_detail") for d in self.get("items") if d.get("pr_detail")]
if pr_details:
doctype = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(doctype)
.select(doctype.pr_detail, Sum(doctype.amount))
.where(doctype.pr_detail.isin(pr_details) & doctype.docstatus == 1)
.groupby(doctype.pr_detail)
)
pr_details_billed_amt = frappe._dict(query.run(as_list=1))
return pr_details_billed_amt
def on_recurring(self, reference_doc, auto_repeat_doc):
self.due_date = None

View File

@@ -1523,6 +1523,94 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
company.enable_provisional_accounting_for_non_stock_items = 0
company.save()
def test_adjust_incoming_rate(self):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
frappe.db.set_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1
)
# Increase the cost of the item
pr = make_purchase_receipt(qty=1, rate=100)
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 100)
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.rate = 150
pi.save()
pi.submit()
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 150)
# Reduce the cost of the item
pr = make_purchase_receipt(qty=1, rate=100)
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 100)
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.rate = 50
pi.save()
pi.submit()
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 50)
frappe.db.set_single_value(
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 0
)
# Don't adjust incoming rate
pr = make_purchase_receipt(qty=1, rate=100)
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 100)
pi = create_purchase_invoice_from_receipt(pr.name)
for row in pi.items:
row.rate = 50
pi.save()
pi.submit()
stock_value_difference = frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
"stock_value_difference",
)
self.assertEqual(stock_value_difference, 100)
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
def test_item_less_defaults(self):
pi = frappe.new_doc("Purchase Invoice")

View File

@@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
'POS Closing Entry', 'Journal Entry', 'Payment Entry'];
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format
@@ -93,9 +93,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
if (doc.docstatus == 1 && doc.outstanding_amount!=0
&& !(cint(doc.is_return) && doc.return_against)) {
cur_frm.add_custom_button(__('Payment'),
this.make_payment_entry, __('Create'));
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
this.frm.add_custom_button(
__('Payment'),
() => this.make_payment_entry(),
__('Create')
);
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
}
if(doc.docstatus==1 && !doc.is_return) {

View File

@@ -32,9 +32,6 @@
"cost_center",
"dimension_col_break",
"project",
"column_break_27",
"campaign",
"source",
"currency_and_price_list",
"currency",
"conversion_rate",
@@ -123,6 +120,7 @@
"account_for_change_amount",
"advances_section",
"allocate_advances_automatically",
"only_include_allocated_payments",
"get_advances",
"advances",
"write_off_section",
@@ -203,7 +201,9 @@
"more_information",
"status",
"inter_company_invoice_reference",
"campaign",
"represents_company",
"source",
"customer_group",
"col_break23",
"is_internal_customer",
@@ -2083,10 +2083,6 @@
"fieldname": "company_addr_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_52",
"fieldtype": "Column Break"
@@ -2131,6 +2127,14 @@
"fieldname": "named_place",
"fieldtype": "Data",
"label": "Named Place"
},
{
"default": "0",
"depends_on": "allocate_advances_automatically",
"description": "Advance payments allocated against orders will only be fetched",
"fieldname": "only_include_allocated_payments",
"fieldtype": "Check",
"label": "Only Include Allocated Payments"
}
],
"icon": "fa fa-file-text",
@@ -2143,11 +2147,10 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2023-01-28 19:45:47.538163",
"modified": "2023-04-03 22:55:14.206473",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [

View File

@@ -145,7 +145,7 @@ class SalesInvoice(SellingController):
self.set_against_income_account()
self.validate_time_sheets_are_submitted()
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount")
if not self.is_return:
self.validate_serial_numbers()
else:
@@ -397,6 +397,8 @@ class SalesInvoice(SellingController):
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
"Payment Ledger Entry",
)

View File

@@ -266,16 +266,16 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account Education Cess - _TC": [3, 1618, 0.06, 32.36],
"_Test Account S&H Education Cess - _TC": [1.5, 1619.5, 0.03, 32.39],
"_Test Account CST - _TC": [32.5, 1652, 0.65, 33.04],
"_Test Account VAT - _TC": [156.5, 1808.5, 3.13, 36.17],
"_Test Account Discount - _TC": [-181.0, 1627.5, -3.62, 32.55],
"_Test Account VAT - _TC": [156.0, 1808.0, 3.12, 36.16],
"_Test Account Discount - _TC": [-181.0, 1627.0, -3.62, 32.54],
}
for d in si.get("taxes"):
for i, k in enumerate(expected_values["keys"]):
self.assertEqual(d.get(k), expected_values[d.account_head][i])
self.assertEqual(si.base_grand_total, 1627.5)
self.assertEqual(si.grand_total, 32.55)
self.assertEqual(si.base_grand_total, 1627.0)
self.assertEqual(si.grand_total, 32.54)
def test_sales_invoice_with_discount_and_inclusive_tax(self):
si = create_sales_invoice(qty=100, rate=50, do_not_save=True)
@@ -401,10 +401,10 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account S&H Education Cess - _TC": [1.4, 1.30, 1297.67],
"_Test Account CST - _TC": [27.88, 25.95, 1323.62],
"_Test Account VAT - _TC": [156.25, 145.43, 1469.05],
"_Test Account Customs Duty - _TC": [125, 116.35, 1585.40],
"_Test Account Shipping Charges - _TC": [100, 100, 1685.40],
"_Test Account Discount - _TC": [-180.33, -168.54, 1516.86],
"_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.01],
"_Test Account Customs Duty - _TC": [125, 116.34, 1585.39],
"_Test Account Shipping Charges - _TC": [100, 100, 1685.39],
"_Test Account Discount - _TC": [-180.33, -168.54, 1516.85],
"_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.00],
}
for d in si.get("taxes"):
@@ -413,7 +413,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.base_grand_total, 1500)
self.assertEqual(si.grand_total, 1500)
self.assertEqual(si.rounding_adjustment, -0.01)
self.assertEqual(si.rounding_adjustment, 0.0)
def test_discount_amount_gl_entry(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
@@ -454,7 +454,7 @@ class TestSalesInvoice(unittest.TestCase):
[test_records[3]["taxes"][2]["account_head"], 0.0, 1.30],
[test_records[3]["taxes"][3]["account_head"], 0.0, 25.95],
[test_records[3]["taxes"][4]["account_head"], 0.0, 145.43],
[test_records[3]["taxes"][5]["account_head"], 0.0, 116.35],
[test_records[3]["taxes"][5]["account_head"], 0.0, 116.34],
[test_records[3]["taxes"][6]["account_head"], 0.0, 100],
[test_records[3]["taxes"][7]["account_head"], 168.54, 0.0],
["_Test Account Service Tax - _TC", 16.85, 0.0],
@@ -1614,7 +1614,7 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account Education Cess - _TC": [1.4, 1.4, 1.4],
"_Test Account S&H Education Cess - _TC": [0.7, 0.7, 0.7],
"_Test Account CST - _TC": [17.19, 17.19, 17.19],
"_Test Account VAT - _TC": [78.13, 78.13, 78.13],
"_Test Account VAT - _TC": [78.12, 78.12, 78.12],
"_Test Account Discount - _TC": [-95.49, -95.49, -95.49],
}
@@ -1623,9 +1623,9 @@ class TestSalesInvoice(unittest.TestCase):
if expected_values.get(d.account_head):
self.assertEqual(d.get(k), expected_values[d.account_head][i])
self.assertEqual(si.total_taxes_and_charges, 234.43)
self.assertEqual(si.base_grand_total, 859.43)
self.assertEqual(si.grand_total, 859.43)
self.assertEqual(si.total_taxes_and_charges, 234.42)
self.assertEqual(si.base_grand_total, 859.42)
self.assertEqual(si.grand_total, 859.42)
def test_multi_currency_gle(self):
si = create_sales_invoice(
@@ -1985,17 +1985,17 @@ class TestSalesInvoice(unittest.TestCase):
)
si.save()
si.submit()
self.assertEqual(si.net_total, 19453.13)
self.assertEqual(si.net_total, 19453.12)
self.assertEqual(si.grand_total, 24900)
self.assertEqual(si.total_taxes_and_charges, 5446.88)
self.assertEqual(si.rounding_adjustment, -0.01)
self.assertEqual(si.rounding_adjustment, 0.0)
expected_values = dict(
(d[0], d)
for d in [
[si.debit_to, 24900, 0.0],
["_Test Account Service Tax - _TC", 0.0, 5446.88],
["Sales - _TC", 0.0, 19453.13],
["Sales - _TC", 0.0, 19453.12],
["Round Off - _TC", 0.01, 0.0],
]
)

View File

@@ -278,7 +278,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
if cint(tax_details.round_off_tax_amount):
tax_amount = round(tax_amount)
tax_amount = normal_round(tax_amount)
return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
@@ -603,3 +603,20 @@ def is_valid_certificate(
valid = True
return valid
def normal_round(number):
"""
Rounds a number to the nearest integer.
:param number: The number to round.
"""
decimal_part = number - int(number)
if decimal_part >= 0.5:
decimal_part = 1
else:
decimal_part = 0
number = int(number) + decimal_part
return number

View File

@@ -300,6 +300,9 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
if gl_map:
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
if gl_map[0]["voucher_type"] != "Period Closing Voucher":
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
for entry in gl_map:
make_entry(entry, adv_adj, update_outstanding, from_repost)
@@ -519,6 +522,9 @@ def make_reverse_gl_entries(
)
validate_accounting_period(gl_entries)
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"])
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
for entry in gl_entries:
@@ -566,6 +572,28 @@ def check_freezing_date(posting_date, adv_adj=False):
)
def validate_against_pcv(is_opening, posting_date, company):
if is_opening and frappe.db.exists(
"Period Closing Voucher", {"docstatus": 1, "company": company}
):
frappe.throw(
_("Opening Entry can not be created after Period Closing Voucher is created."),
title=_("Invalid Opening Entry"),
)
last_pcv_date = frappe.db.get_value(
"Period Closing Voucher", {"docstatus": 1, "company": company}, "max(posting_date)"
)
if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date):
message = _("Books have been closed till the period ending on {0}").format(
formatdate(last_pcv_date)
)
message += "</br >"
message += _("You cannot create/amend any accounting entries till this date.")
frappe.throw(message, title=_("Period Closed"))
def set_as_cancel(voucher_type, voucher_no):
"""
Set is_cancelled=1 in all original gl entries for the voucher

View File

@@ -32,6 +32,16 @@ from erpnext import get_company_currency
from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"}
SALES_TRANSACTION_TYPES = {
"Quotation",
"Sales Order",
"Delivery Note",
"Sales Invoice",
"POS Invoice",
}
TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES
class DuplicatePartyAccountError(frappe.ValidationError):
pass
@@ -124,12 +134,6 @@ def _get_party_details(
set_other_values(party_details, party, party_type)
set_price_list(party_details, party, party_type, price_list, pos_profile)
party_details["tax_category"] = get_address_tax_category(
party.get("tax_category"),
party_address,
shipping_address if party_type != "Supplier" else party_address,
)
tax_template = set_taxes(
party.name,
party_type,
@@ -170,6 +174,9 @@ def _get_party_details(
party_type, party.name, "tax_withholding_category"
)
if not party_details.get("tax_category") and pos_profile:
party_details["tax_category"] = frappe.get_value("POS Profile", pos_profile, "tax_category")
return party_details
@@ -211,20 +218,10 @@ def set_address_details(
else:
party_details.update(get_company_address(company))
if doctype and doctype in [
"Delivery Note",
"Sales Invoice",
"Sales Order",
"Quotation",
"POS Invoice",
]:
if party_details.company_address:
party_details.update(
get_fetch_values(doctype, "company_address", party_details.company_address)
)
get_regional_address_details(party_details, doctype, company)
if doctype in SALES_TRANSACTION_TYPES and party_details.company_address:
party_details.update(get_fetch_values(doctype, "company_address", party_details.company_address))
elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]:
if doctype in PURCHASE_TRANSACTION_TYPES:
if shipping_address:
party_details.update(
shipping_address=shipping_address,
@@ -250,9 +247,21 @@ def set_address_details(
**get_fetch_values(doctype, "shipping_address", party_details.billing_address)
)
party_address, shipping_address = (
party_details.get(billing_address_field),
party_details.shipping_address_name,
)
party_details["tax_category"] = get_address_tax_category(
party.get("tax_category"),
party_address,
shipping_address if party_type != "Supplier" else party_address,
)
if doctype in TRANSACTION_TYPES:
get_regional_address_details(party_details, doctype, company)
return party_details.get(billing_address_field), party_details.shipping_address_name
return party_address, shipping_address
@erpnext.allow_regional

View File

@@ -859,7 +859,7 @@ class ReceivablePayableReport(object):
)
else:
self.qb_selection_filter.append(
self.ple[dimension.fieldname] == self.filters[dimension.fieldname]
self.ple[dimension.fieldname].isin(self.filters[dimension.fieldname])
)
def is_invoice(self, ple):

View File

@@ -25,6 +25,7 @@ def get_data(filters):
["posting_date", "<=", filters.get("to_date")],
["against_voucher_type", "=", "Asset"],
["account", "in", depreciation_accounts],
["is_cancelled", "=", 0],
]
if filters.get("asset"):

View File

@@ -25,6 +25,8 @@ def execute(filters=None):
company=filters.company,
)
filters.period_start_date = period_list[0]["year_start_date"]
currency = filters.presentation_currency or frappe.get_cached_value(
"Company", filters.company, "default_currency"
)
@@ -96,7 +98,7 @@ def execute(filters=None):
chart = get_chart_data(filters, columns, asset, liability, equity)
report_summary = get_report_summary(
period_list, asset, liability, equity, provisional_profit_loss, total_credit, currency, filters
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
)
return columns, data, message, chart, report_summary
@@ -174,7 +176,6 @@ def get_report_summary(
liability,
equity,
provisional_profit_loss,
total_credit,
currency,
filters,
consolidated=False,

View File

@@ -4,6 +4,7 @@
import frappe
from frappe import _
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import getdate, nowdate
@@ -91,4 +92,65 @@ def get_entries(filters):
as_list=1,
)
return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate()))
# Loan Disbursement
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
query = (
frappe.qb.from_(loan_disbursement)
.select(
ConstantColumn("Loan Disbursement").as_("payment_document_type"),
loan_disbursement.name.as_("payment_entry"),
loan_disbursement.disbursement_date.as_("posting_date"),
loan_disbursement.reference_number.as_("cheque_no"),
loan_disbursement.clearance_date.as_("clearance_date"),
loan_disbursement.applicant.as_("against"),
-loan_disbursement.disbursed_amount.as_("amount"),
)
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.disbursement_date >= filters["from_date"])
.where(loan_disbursement.disbursement_date <= filters["to_date"])
.where(loan_disbursement.disbursement_account == filters["account"])
.orderby(loan_disbursement.disbursement_date, order=frappe.qb.desc)
.orderby(loan_disbursement.name, order=frappe.qb.desc)
)
if filters.get("from_date"):
query = query.where(loan_disbursement.disbursement_date >= filters["from_date"])
if filters.get("to_date"):
query = query.where(loan_disbursement.disbursement_date <= filters["to_date"])
loan_disbursements = query.run(as_list=1)
# Loan Repayment
loan_repayment = frappe.qb.DocType("Loan Repayment")
query = (
frappe.qb.from_(loan_repayment)
.select(
ConstantColumn("Loan Repayment").as_("payment_document_type"),
loan_repayment.name.as_("payment_entry"),
loan_repayment.posting_date.as_("posting_date"),
loan_repayment.reference_number.as_("cheque_no"),
loan_repayment.clearance_date.as_("clearance_date"),
loan_repayment.applicant.as_("against"),
loan_repayment.amount_paid.as_("amount"),
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.posting_date >= filters["from_date"])
.where(loan_repayment.posting_date <= filters["to_date"])
.where(loan_repayment.payment_account == filters["account"])
.orderby(loan_repayment.posting_date, order=frappe.qb.desc)
.orderby(loan_repayment.name, order=frappe.qb.desc)
)
if filters.get("from_date"):
query = query.where(loan_repayment.posting_date >= filters["from_date"])
if filters.get("to_date"):
query = query.where(loan_repayment.posting_date <= filters["to_date"])
loan_repayments = query.run(as_list=1)
return sorted(
journal_entries + payment_entries + loan_disbursements + loan_repayments,
key=lambda k: k[2] or getdate(nowdate()),
)

View File

@@ -118,7 +118,6 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
liability,
equity,
provisional_profit_loss,
total_credit,
company_currency,
filters,
True,
@@ -138,7 +137,8 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data,
for data in [asset_data, liability_data, equity_data]:
if data:
account_name = get_root_account_name(data[0].root_type, company)
opening_value += get_opening_balance(account_name, data, company) or 0.0
if account_name:
opening_value += get_opening_balance(account_name, data, company) or 0.0
opening_balance[company] = opening_value
@@ -155,7 +155,7 @@ def get_opening_balance(account_name, data, company):
def get_root_account_name(root_type, company):
return frappe.get_all(
root_account = frappe.get_all(
"Account",
fields=["account_name"],
filters={
@@ -165,7 +165,10 @@ def get_root_account_name(root_type, company):
"parent_account": ("is", "not set"),
},
as_list=1,
)[0][0]
)
if root_account:
return root_account[0][0]
def get_profit_loss_data(fiscal_year, companies, columns, filters):

View File

@@ -418,46 +418,47 @@ def set_gl_entries_by_account(
ignore_closing_entries=False,
):
"""Returns a dict like { "account": [gl entries], ... }"""
gl_entries = []
additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters)
accounts = frappe.db.sql_list(
"""select name from `tabAccount`
where lft >= %s and rgt <= %s and company = %s""",
(root_lft, root_rgt, company),
accounts_list = frappe.db.get_all(
"Account",
filters={"company": company, "is_group": 0, "lft": (">=", root_lft), "rgt": ("<=", root_rgt)},
pluck="name",
)
if accounts:
additional_conditions += " and account in ({})".format(
", ".join(frappe.db.escape(d) for d in accounts)
)
ignore_opening_entries = False
if accounts_list:
# For balance sheet
if not from_date:
from_date = filters["period_start_date"]
last_period_closing_voucher = frappe.db.get_all(
"Period Closing Voucher",
filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", from_date)},
fields=["posting_date", "name"],
order_by="posting_date desc",
limit=1,
)
if last_period_closing_voucher:
gl_entries += get_accounting_entries(
"Account Closing Balance",
from_date,
to_date,
accounts_list,
filters,
ignore_closing_entries,
last_period_closing_voucher[0].name,
)
from_date = add_days(last_period_closing_voucher[0].posting_date, 1)
ignore_opening_entries = True
gl_filters = {
"company": company,
"from_date": from_date,
"to_date": to_date,
"finance_book": cstr(filters.get("finance_book")),
}
if filters.get("include_default_book_entries"):
gl_filters["company_fb"] = frappe.get_cached_value("Company", company, "default_finance_book")
for key, value in filters.items():
if value:
gl_filters.update({key: value})
gl_entries = frappe.db.sql(
"""
select posting_date, account, debit, credit, is_opening, fiscal_year,
debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
where company=%(company)s
{additional_conditions}
and posting_date <= %(to_date)s
and is_cancelled = 0""".format(
additional_conditions=additional_conditions
),
gl_filters,
as_dict=True,
gl_entries += get_accounting_entries(
"GL Entry",
from_date,
to_date,
accounts_list,
filters,
ignore_closing_entries,
ignore_opening_entries=ignore_opening_entries,
)
if filters and filters.get("presentation_currency"):
@@ -469,34 +470,82 @@ def set_gl_entries_by_account(
return gl_entries_by_account
def get_additional_conditions(from_date, ignore_closing_entries, filters):
additional_conditions = []
def get_accounting_entries(
doctype,
from_date,
to_date,
accounts,
filters,
ignore_closing_entries,
period_closing_voucher=None,
ignore_opening_entries=False,
):
gl_entry = frappe.qb.DocType(doctype)
query = (
frappe.qb.from_(gl_entry)
.select(
gl_entry.account,
gl_entry.debit,
gl_entry.credit,
gl_entry.debit_in_account_currency,
gl_entry.credit_in_account_currency,
gl_entry.account_currency,
)
.where(gl_entry.company == filters.company)
)
if doctype == "GL Entry":
query = query.select(gl_entry.posting_date, gl_entry.is_opening, gl_entry.fiscal_year)
query = query.where(gl_entry.is_cancelled == 0)
query = query.where(gl_entry.posting_date <= to_date)
if ignore_opening_entries:
query = query.where(gl_entry.is_opening == "No")
else:
query = query.select(gl_entry.closing_date.as_("posting_date"))
query = query.where(gl_entry.period_closing_voucher == period_closing_voucher)
query = apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters)
query = query.where(gl_entry.account.isin(accounts))
entries = query.run(as_dict=True)
return entries
def apply_additional_conditions(doctype, query, from_date, ignore_closing_entries, filters):
gl_entry = frappe.qb.DocType(doctype)
accounting_dimensions = get_accounting_dimensions(as_list=False)
if ignore_closing_entries:
additional_conditions.append("ifnull(voucher_type, '')!='Period Closing Voucher'")
if doctype == "GL Entry":
query = query.where(gl_entry.voucher_type != "Period Closing Voucher")
else:
query = query.where(gl_entry.is_period_closing_voucher_entry == 0)
if from_date:
additional_conditions.append("posting_date >= %(from_date)s")
if from_date and doctype == "GL Entry":
query = query.where(gl_entry.posting_date >= from_date)
if filters:
if filters.get("project"):
if not isinstance(filters.get("project"), list):
filters.project = frappe.parse_json(filters.get("project"))
additional_conditions.append("project in %(project)s")
query = query.where(gl_entry.project.isin(filters.project))
if filters.get("cost_center"):
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
additional_conditions.append("cost_center in %(cost_center)s")
query = query.where(gl_entry.cost_center.isin(filters.cost_center))
if filters.get("include_default_book_entries"):
additional_conditions.append(
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
query = query.where(
(gl_entry.finance_book.isin([cstr(filters.finance_book), cstr(filters.company_fb), ""]))
| (gl_entry.finance_book.isnull())
)
else:
additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
query = query.where(
(gl_entry.finance_book.isin([cstr(filters.company_fb), ""])) | (gl_entry.finance_book.isnull())
)
if accounting_dimensions:
for dimension in accounting_dimensions:
@@ -505,11 +554,10 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters):
filters[dimension.fieldname] = get_dimension_with_children(
dimension.document_type, filters.get(dimension.fieldname)
)
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
else:
additional_conditions.append("{0} in %({0})s".format(dimension.fieldname))
return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""
query = query.where(gl_entry[dimension.fieldname].isin(filters[dimension.fieldname]))
return query
def get_cost_centers_with_children(cost_centers):

View File

@@ -38,8 +38,11 @@
{% if(data[i].posting_date) { %}
<td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td>
<td>{%= data[i].voucher_type %}
<br>{%= data[i].voucher_no %}</td>
<td>
<br>{%= data[i].voucher_no %}
</td>
{% var longest_word = cstr(data[i].remarks).split(" ").reduce((longest, word) => word.length > longest.length ? word : longest, ""); %}
<td {% if longest_word.length > 45 %} class="overflow-wrap-anywhere" {% endif %}>
<span>
{% if(!(filters.party || filters.account)) { %}
{%= data[i].party || data[i].account %}
<br>
@@ -49,11 +52,14 @@
{% if(data[i].bill_no) { %}
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
{% } %}
</td>
<td style="text-align: right">
{%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}</td>
<td style="text-align: right">
{%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}</td>
</span>
</td>
<td style="text-align: right">
{%= format_currency(data[i].debit, filters.presentation_currency) %}
</td>
<td style="text-align: right">
{%= format_currency(data[i].credit, filters.presentation_currency) %}
</td>
{% } else { %}
<td></td>
<td></td>

View File

@@ -58,9 +58,8 @@ frappe.query_reports["General Ledger"] = {
{
"fieldname":"party_type",
"label": __("Party Type"),
"fieldtype": "Link",
"options": "Party Type",
"default": "",
"fieldtype": "Autocomplete",
options: Object.keys(frappe.boot.party_account_types),
on_change: function() {
frappe.query_report.set_filter_value('party', "");
}

View File

@@ -395,6 +395,7 @@ def get_column_names():
class GrossProfitGenerator(object):
def __init__(self, filters=None):
self.sle = {}
self.data = []
self.average_buying_rate = {}
self.filters = frappe._dict(filters)
@@ -404,7 +405,6 @@ class GrossProfitGenerator(object):
if filters.group_by == "Invoice":
self.group_items_by_invoice()
self.load_stock_ledger_entries()
self.load_product_bundle()
self.load_non_stock_items()
self.get_returned_invoice_items()
@@ -501,7 +501,14 @@ class GrossProfitGenerator(object):
):
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
for returned_item_row in returned_item_rows:
row.qty += flt(returned_item_row.qty)
# returned_items 'qty' should be stateful
if returned_item_row.qty != 0:
if row.qty >= abs(returned_item_row.qty):
row.qty += returned_item_row.qty
returned_item_row.qty = 0
else:
row.qty = 0
returned_item_row.qty += row.qty
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
if flt(row.qty) or row.base_amount:
@@ -633,7 +640,7 @@ class GrossProfitGenerator(object):
return flt(row.qty) * item_rate
else:
my_sle = self.sle.get((item_code, row.warehouse))
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
if (row.update_stock or row.dn_detail) and my_sle:
parenttype, parent = row.parenttype, row.parent
if row.dn_detail:
@@ -651,7 +658,7 @@ class GrossProfitGenerator(object):
dn["item_row"],
dn["warehouse"],
)
my_sle = self.sle.get((item_code, warehouse))
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)
@@ -667,15 +674,12 @@ class GrossProfitGenerator(object):
def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code):
from frappe.query_builder.functions import Sum
delivery_note = frappe.qb.DocType("Delivery Note")
delivery_note_item = frappe.qb.DocType("Delivery Note Item")
query = (
frappe.qb.from_(delivery_note)
.inner_join(delivery_note_item)
.on(delivery_note.name == delivery_note_item.parent)
frappe.qb.from_(delivery_note_item)
.select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty))
.where(delivery_note.docstatus == 1)
.where(delivery_note_item.docstatus == 1)
.where(delivery_note_item.item_code == item_code)
.where(delivery_note_item.against_sales_order == sales_order)
.where(delivery_note_item.so_detail == so_detail)
@@ -737,6 +741,8 @@ class GrossProfitGenerator(object):
if self.filters.to_date:
conditions += " and posting_date <= %(to_date)s"
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
if self.filters.item_group:
conditions += " and {0}".format(get_item_group_condition(self.filters.item_group))
@@ -947,24 +953,36 @@ class GrossProfitGenerator(object):
"Item", item_code, ["item_name", "description", "item_group", "brand"]
)
def load_stock_ledger_entries(self):
res = frappe.db.sql(
"""select item_code, voucher_type, voucher_no,
voucher_detail_no, stock_value, warehouse, actual_qty as qty
from `tabStock Ledger Entry`
where company=%(company)s and is_cancelled = 0
order by
item_code desc, warehouse desc, posting_date desc,
posting_time desc, creation desc""",
self.filters,
as_dict=True,
)
self.sle = {}
for r in res:
if (r.item_code, r.warehouse) not in self.sle:
self.sle[(r.item_code, r.warehouse)] = []
def get_stock_ledger_entries(self, item_code, warehouse):
if item_code and warehouse:
if (item_code, warehouse) not in self.sle:
sle = qb.DocType("Stock Ledger Entry")
res = (
qb.from_(sle)
.select(
sle.item_code,
sle.voucher_type,
sle.voucher_no,
sle.voucher_detail_no,
sle.stock_value,
sle.warehouse,
sle.actual_qty.as_("qty"),
)
.where(
(sle.company == self.filters.company)
& (sle.item_code == item_code)
& (sle.warehouse == warehouse)
& (sle.is_cancelled == 0)
)
.orderby(sle.item_code)
.orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc)
.run(as_dict=True)
)
self.sle[(r.item_code, r.warehouse)].append(r)
self.sle[(item_code, warehouse)] = res
return self.sle[(item_code, warehouse)]
return []
def load_product_bundle(self):
self.product_bundles = {}

View File

@@ -381,3 +381,82 @@ class TestGrossProfit(FrappeTestCase):
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry, gp_entry[0])
def test_crnote_against_invoice_with_multiple_instances_of_same_item(self):
"""
Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items.
"""
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
# Invoice with an item added twice
sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True)
sinv.append("items", frappe.copy_doc(sinv.items[0], ignore_no_copy=False))
sinv = sinv.save().submit()
# Create Credit Note for Invoice
cr_note = make_sales_return(sinv.name)
cr_note = cr_note.save().submit()
filters = frappe._dict(
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
)
columns, data = execute(filters=filters)
expected_entry = {
"parent_invoice": sinv.name,
"currency": "INR",
"sales_invoice": self.item,
"customer": self.customer,
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": 0.0,
"avg._selling_rate": 0.0,
"valuation_rate": 0.0,
"selling_amount": -100.0,
"buying_amount": 0.0,
"gross_profit": -100.0,
"gross_profit_%": 100.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
# Both items of Invoice should have '0' qty
self.assertEqual(len(gp_entry), 2)
self.assertDictContainsSubset(expected_entry, gp_entry[0])
self.assertDictContainsSubset(expected_entry, gp_entry[1])
def test_standalone_cr_notes(self):
"""
Standalone cr notes will be reported as usual
"""
# Make Cr Note
sinv = self.create_sales_invoice(
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
sinv.is_return = 1
sinv = sinv.save().submit()
filters = frappe._dict(
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
)
columns, data = execute(filters=filters)
expected_entry = {
"parent_invoice": sinv.name,
"currency": "INR",
"sales_invoice": self.item,
"customer": self.customer,
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
"item_code": self.item,
"item_name": self.item,
"warehouse": "Stores - _GP",
"qty": -1.0,
"avg._selling_rate": 100.0,
"valuation_rate": 0.0,
"selling_amount": -100.0,
"buying_amount": 0.0,
"gross_profit": -100.0,
"gross_profit_%": 100.0,
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry, gp_entry[0])

View File

@@ -37,6 +37,29 @@ function get_filters() {
});
}
},
{
"fieldname":"party_type",
"label": __("Party Type"),
"fieldtype": "Link",
"options": "Party Type",
"default": "",
on_change: function() {
frappe.query_report.set_filter_value('party', "");
}
},
{
"fieldname":"party",
"label": __("Party"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
if (!frappe.query_report.filters) return;
let party_type = frappe.query_report.get_filter_value('party_type');
if (!party_type) return;
return frappe.db.get_link_options(party_type, txt);
},
},
{
"fieldname":"voucher_no",
"label": __("Voucher No"),
@@ -49,6 +72,20 @@ function get_filters() {
"fieldtype": "Data",
"width": 100,
},
{
"fieldname":"include_account_currency",
"label": __("Include Account Currency"),
"fieldtype": "Check",
"width": 100,
},
{
"fieldname":"group_party",
"label": __("Group by Party"),
"fieldtype": "Check",
"width": 100,
},
]
return filters;

View File

@@ -17,34 +17,26 @@ class PaymentLedger(object):
self.ple = qb.DocType("Payment Ledger Entry")
def init_voucher_dict(self):
if self.voucher_amount:
s = set()
# build a set of unique vouchers
# for each ple, using group_by_key to create a key and assign it to +/- list
for ple in self.voucher_amount:
key = (ple.voucher_type, ple.voucher_no, ple.party)
s.add(key)
group_by_key = None
if not self.filters.group_party:
group_by_key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
else:
group_by_key = (ple.party_type, ple.party)
# for each unique vouchers, initialize +/- list
for key in s:
self.voucher_dict[key] = frappe._dict(increase=list(), decrease=list())
# for each ple, using against voucher and amount, assign it to +/- list
# group by against voucher
for ple in self.voucher_amount:
against_key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
target = None
if self.voucher_dict.get(against_key):
if ple.amount > 0:
target = self.voucher_dict.get(against_key).increase
else:
target = self.voucher_dict.get(against_key).decrease
if ple.amount > 0:
target = self.voucher_dict.setdefault(group_by_key, {}).setdefault("increase", [])
else:
target = self.voucher_dict.setdefault(group_by_key, {}).setdefault("decrease", [])
# this if condition will lose unassigned ple entries(against_voucher doc doesn't have ple)
# need to somehow include the stray entries as well.
if target is not None:
entry = frappe._dict(
company=ple.company,
posting_date=ple.posting_date,
account=ple.account,
party_type=ple.party_type,
party=ple.party,
@@ -66,10 +58,10 @@ class PaymentLedger(object):
for value in self.voucher_dict.values():
voucher_data = []
if value.increase != []:
voucher_data.extend(value.increase)
if value.decrease != []:
voucher_data.extend(value.decrease)
if value.get("increase"):
voucher_data.extend(value.get("increase"))
if value.get("decrease"):
voucher_data.extend(value.get("decrease"))
if voucher_data:
# balance row
@@ -117,6 +109,12 @@ class PaymentLedger(object):
if self.filters.against_voucher_no:
self.conditions.append(self.ple.against_voucher_no == self.filters.against_voucher_no)
if self.filters.party_type:
self.conditions.append(self.ple.party_type == self.filters.party_type)
if self.filters.party:
self.conditions.append(self.ple.party.isin(self.filters.party))
def get_data(self):
ple = self.ple
@@ -134,7 +132,13 @@ class PaymentLedger(object):
def get_columns(self):
options = None
self.columns.append(
dict(label=_("Company"), fieldname="company", fieldtype="data", options=options, width="100")
dict(
label=_("Posting Date"),
fieldname="posting_date",
fieldtype="Date",
options=options,
width="100",
)
)
self.columns.append(
@@ -160,7 +164,11 @@ class PaymentLedger(object):
)
self.columns.append(
dict(
label=_("Voucher No"), fieldname="voucher_no", fieldtype="data", options=options, width="100"
label=_("Voucher No"),
fieldname="voucher_no",
fieldtype="Dynamic Link",
options="voucher_type",
width="100",
)
)
self.columns.append(
@@ -176,8 +184,8 @@ class PaymentLedger(object):
dict(
label=_("Against Voucher No"),
fieldname="against_voucher_no",
fieldtype="data",
options=options,
fieldtype="Dynamic Link",
options="against_voucher_type",
width="100",
)
)
@@ -209,7 +217,7 @@ class PaymentLedger(object):
self.get_columns()
self.get_data()
# initialize dictionary and group using against voucher
# initialize dictionary and group using key
self.init_voucher_dict()
# convert dictionary to list and add balance rows

View File

@@ -1,451 +0,0 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// Contributed by Case Solved and sponsored by Nulight Studios
/* eslint-disable */
frappe.provide('frappe.query_reports');
frappe.query_reports["Tax Detail"] = {
filters: [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("company"),
reqd: 1
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.datetime.month_start(frappe.datetime.get_today()),
reqd: 1,
width: "60px"
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.datetime.month_end(frappe.datetime.get_today()),
reqd: 1,
width: "60px"
},
{
fieldname: "report_name",
label: __("Report Name"),
fieldtype: "Read Only",
default: frappe.query_report.report_name,
hidden: 1,
reqd: 1
},
{
fieldname: "mode",
label: __("Mode"),
fieldtype: "Read Only",
default: "edit",
hidden: 1,
reqd: 1
}
],
onload: function onload(report) {
// Remove Add Column and Save from menu
report.page.add_inner_button(__("New Report"), () => new_report(), __("Custom Report"));
report.page.add_inner_button(__("Load Report"), () => load_report(), __("Custom Report"));
hide_filters(report);
}
};
function hide_filters(report) {
report.page.page_form[0].querySelectorAll('.form-group.frappe-control').forEach(function setHidden(field) {
if (field.dataset.fieldtype == "Read Only") {
field.classList.add("hidden");
}
});
}
erpnext.TaxDetail = class TaxDetail {
constructor() {
this.patch();
this.load_report();
}
// Monkey patch the QueryReport class
patch() {
this.qr = frappe.query_report;
this.super = {
refresh_report: this.qr.refresh_report,
show_footer_message: this.qr.show_footer_message
}
this.qr.refresh_report = () => this.refresh_report();
this.qr.show_footer_message = () => this.show_footer_message();
}
show_footer_message() {
// The last thing to run after datatable_render in refresh()
this.super.show_footer_message.apply(this.qr);
if (this.qr.report_name !== 'Tax Detail') {
this.show_help();
if (this.loading) {
this.set_section('');
} else {
this.reload_component('');
}
}
this.loading = false;
}
refresh_report() {
// Infrequent report build (onload), load filters & data
// super function runs a refresh() serially
// already run within frappe.run_serially
this.loading = true;
this.super.refresh_report.apply(this.qr);
if (this.qr.report_name !== 'Tax Detail') {
frappe.call({
method: 'erpnext.accounts.report.tax_detail.tax_detail.get_custom_reports',
args: {name: this.qr.report_name}
}).then((r) => {
const data = JSON.parse(r.message[this.qr.report_name]['json']);
this.create_controls();
this.sections = data.sections || {};
this.controls['show_detail'].set_input(data.show_detail);
});
}
}
load_report() {
// One-off report build like titles, menu, etc
// Run when this object is created which happens in qr.load_report
this.qr.menu_items = this.get_menu_items();
}
get_menu_items() {
// Replace Save action
let new_items = [];
const save = __('Save');
for (let item of this.qr.menu_items) {
if (item.label === save) {
new_items.push({
label: save,
action: () => this.save_report(),
standard: false
});
} else {
new_items.push(item);
}
}
return new_items;
}
save_report() {
this.check_datatable();
if (this.qr.report_name !== 'Tax Detail') {
frappe.call({
method:'erpnext.accounts.report.tax_detail.tax_detail.save_custom_report',
args: {
reference_report: 'Tax Detail',
report_name: this.qr.report_name,
data: {
columns: this.qr.get_visible_columns(),
sections: this.sections,
show_detail: this.controls['show_detail'].get_input_value()
}
},
freeze: true
}).then((r) => {
this.set_section('');
});
}
}
check_datatable() {
if (!this.qr.datatable) {
frappe.throw(__('Please change the date range to load data first'));
}
}
set_section(name) {
// Sets the given section name and then reloads the data
if (name && !this.sections[name]) {
this.sections[name] = {};
}
let options = Object.keys(this.sections);
options.unshift('');
this.controls['section_name'].$wrapper.find("select").empty().add_options(options);
const org_mode = this.qr.get_filter_value('mode');
let refresh = false;
if (name) {
this.controls['section_name'].set_input(name);
this.qr.set_filter_value('mode', 'edit');
if (org_mode === 'run') {
refresh = true;
}
} else {
this.controls['section_name'].set_input('');
this.qr.set_filter_value('mode', 'run');
if (org_mode === 'edit') {
refresh = true;
}
}
if (refresh) {
this.qr.refresh();
}
this.reload_component('');
}
reload_component(component_name) {
const section_name = this.controls['section_name'].get_input_value();
if (section_name) {
const section = this.sections[section_name];
const component_names = Object.keys(section);
component_names.unshift('');
this.controls['component'].$wrapper.find("select").empty().add_options(component_names);
this.controls['component'].set_input(component_name);
if (component_name) {
this.controls['component_type'].set_input(section[component_name].type);
}
} else {
this.controls['component'].$wrapper.find("select").empty();
this.controls['component'].set_input('');
}
this.set_table_filters();
}
set_table_filters() {
let filters = {};
const section_name = this.controls['section_name'].get_input_value();
const component_name = this.controls['component'].get_input_value();
if (section_name && component_name) {
const component_type = this.sections[section_name][component_name].type;
if (component_type === 'filter') {
filters = this.sections[section_name][component_name]['filters'];
}
}
this.setAppliedFilters(filters);
}
setAppliedFilters(filters) {
if (this.qr.datatable) {
Array.from(this.qr.datatable.header.querySelectorAll('.dt-filter')).map(function setFilters(input) {
let idx = input.dataset.colIndex;
if (filters[idx]) {
input.value = filters[idx];
} else {
input.value = null;
}
});
this.qr.datatable.columnmanager.applyFilter(filters);
}
}
delete(name, type) {
if (type === 'section') {
delete this.sections[name];
const new_section = Object.keys(this.sections)[0] || '';
this.set_section(new_section);
}
if (type === 'component') {
const cur_section = this.controls['section_name'].get_input_value();
delete this.sections[cur_section][name];
this.reload_component('');
}
}
create_controls() {
let controls = {};
// SELECT in data.js
controls['section_name'] = this.qr.page.add_field({
label: __('Section'),
fieldtype: 'Select',
fieldname: 'section_name',
change: (e) => {
this.set_section(this.controls['section_name'].get_input_value());
}
});
// BUTTON in button.js
controls['new_section'] = this.qr.page.add_field({
label: __('New Section'),
fieldtype: 'Button',
fieldname: 'new_section',
click: () => {
frappe.prompt({
label: __('Section Name'),
fieldname: 'name',
fieldtype: 'Data'
}, (values) => {
this.set_section(values.name);
});
}
});
controls['delete_section'] = this.qr.page.add_field({
label: __('Delete Section'),
fieldtype: 'Button',
fieldname: 'delete_section',
click: () => {
let cur_section = this.controls['section_name'].get_input_value();
if (cur_section) {
frappe.confirm(__('Are you sure you want to delete section') + ' ' + cur_section + '?',
() => {this.delete(cur_section, 'section')});
}
}
});
controls['component'] = this.qr.page.add_field({
label: __('Component'),
fieldtype: 'Select',
fieldname: 'component',
change: (e) => {
this.reload_component(this.controls['component'].get_input_value());
}
});
controls['component_type'] = this.qr.page.add_field({
label: __('Component Type'),
fieldtype: 'Select',
fieldname: 'component_type',
default: 'filter',
options: [
{label: __('Filtered Row Subtotal'), value: 'filter'},
{label: __('Section Subtotal'), value: 'section'}
]
});
controls['add_component'] = this.qr.page.add_field({
label: __('Add Component'),
fieldtype: 'Button',
fieldname: 'add_component',
click: () => {
this.check_datatable();
let section_name = this.controls['section_name'].get_input_value();
if (section_name) {
const component_type = this.controls['component_type'].get_input_value();
let idx = 0;
const names = Object.keys(this.sections[section_name]);
if (names.length > 0) {
const idxs = names.map((key) => parseInt(key.match(/\d+$/)) || 0);
idx = Math.max(...idxs) + 1;
}
const filters = this.qr.datatable.columnmanager.getAppliedFilters();
if (component_type === 'filter') {
const name = 'Filter' + idx.toString();
let data = {
type: component_type,
filters: filters
}
this.sections[section_name][name] = data;
this.reload_component(name);
} else if (component_type === 'section') {
if (filters && Object.keys(filters).length !== 0) {
frappe.show_alert({
message: __('Column filters ignored'),
indicator: 'yellow'
});
}
let data = {
type: component_type
}
frappe.prompt({
label: __('Section'),
fieldname: 'section',
fieldtype: 'Select',
options: Object.keys(this.sections)
}, (values) => {
this.sections[section_name][values.section] = data;
this.reload_component(values.section);
});
} else {
frappe.throw(__('Please select the Component Type first'));
}
} else {
frappe.throw(__('Please select the Section first'));
}
}
});
controls['delete_component'] = this.qr.page.add_field({
label: __('Delete Component'),
fieldtype: 'Button',
fieldname: 'delete_component',
click: () => {
const component = this.controls['component'].get_input_value();
if (component) {
frappe.confirm(__('Are you sure you want to delete component') + ' ' + component + '?',
() => {this.delete(component, 'component')});
}
}
});
controls['save'] = this.qr.page.add_field({
label: __('Save & Run'),
fieldtype: 'Button',
fieldname: 'save',
click: () => {
this.save_report();
}
});
controls['show_detail'] = this.qr.page.add_field({
label: __('Show Detail'),
fieldtype: 'Check',
fieldname: 'show_detail',
default: 1
});
this.controls = controls;
}
show_help() {
const help = __('Your custom report is built from General Ledger Entries within the date range. You can add multiple sections to the report using the New Section button. Each component added to a section adds a subset of the data into the specified section. Beware of duplicated data rows. The Filtered Row component type saves the datatable column filters to specify the added data. The Section component type refers to the data in a previously defined section, but it cannot refer to its parent section. The Amount column is summed to give the section subtotal. Use the Show Detail box to see the data rows included in each section in the final report. Once finished, hit Save & Run. Report contributed by');
this.qr.$report_footer.append('<div class="col-md-12"><strong>' + __('Help') + `: </strong>${help}<a href="https://www.casesolved.co.uk"> Case Solved</a></div>`);
}
}
if (!window.taxdetail) {
window.taxdetail = new erpnext.TaxDetail();
}
function get_reports(cb) {
frappe.call({
method: 'erpnext.accounts.report.tax_detail.tax_detail.get_custom_reports',
freeze: true
}).then((r) => {
cb(r.message);
})
}
function new_report() {
const dialog = new frappe.ui.Dialog({
title: __('New Report'),
fields: [
{
fieldname: 'report_name',
label: __('Report Name'),
fieldtype: 'Data',
default: 'VAT Return'
}
],
primary_action_label: __('Create'),
primary_action: function new_report_pa(values) {
frappe.call({
method:'erpnext.accounts.report.tax_detail.tax_detail.save_custom_report',
args: {
reference_report: 'Tax Detail',
report_name: values.report_name,
data: {
columns: [],
sections: {},
show_detail: 1
}
},
freeze: true
}).then((r) => {
frappe.set_route('query-report', values.report_name);
});
dialog.hide();
}
});
dialog.show();
}
function load_report() {
get_reports(function load_report_cb(reports) {
const dialog = new frappe.ui.Dialog({
title: __('Load Report'),
fields: [
{
fieldname: 'report_name',
label: __('Report Name'),
fieldtype: 'Select',
options: Object.keys(reports)
}
],
primary_action_label: __('Load'),
primary_action: function load_report_pa(values) {
dialog.hide();
frappe.set_route('query-report', values.report_name);
}
});
dialog.show();
});
}

View File

@@ -1,32 +0,0 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-02-19 16:44:21.175113",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-02-19 16:44:21.175113",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Detail",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "Tax Detail",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
}

View File

@@ -1,325 +0,0 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# Contributed by Case Solved and sponsored by Nulight Studios
import json
import frappe
from frappe import _
# NOTE: Payroll is implemented using Journal Entries which are included as GL Entries
# field lists in multiple doctypes will be coalesced
required_sql_fields = {
("GL Entry", 1): ["posting_date"],
("Account",): ["root_type", "account_type"],
("GL Entry", 2): ["account", "voucher_type", "voucher_no", "debit", "credit"],
("Purchase Invoice Item", "Sales Invoice Item"): [
"base_net_amount",
"item_tax_rate",
"item_tax_template",
"item_group",
"item_name",
],
("Purchase Invoice", "Sales Invoice"): ["taxes_and_charges", "tax_category"],
}
def execute(filters=None):
if not filters:
return [], []
fieldlist = required_sql_fields
fieldstr = get_fieldstr(fieldlist)
gl_entries = frappe.db.sql(
"""
select {fieldstr}
from `tabGL Entry` ge
inner join `tabAccount` a on
ge.account=a.name and ge.company=a.company
left join `tabSales Invoice` si on
ge.company=si.company and ge.voucher_type='Sales Invoice' and ge.voucher_no=si.name
left join `tabSales Invoice Item` sii on
a.root_type='Income' and si.name=sii.parent
left join `tabPurchase Invoice` pi on
ge.company=pi.company and ge.voucher_type='Purchase Invoice' and ge.voucher_no=pi.name
left join `tabPurchase Invoice Item` pii on
a.root_type='Expense' and pi.name=pii.parent
where
ge.company=%(company)s and
ge.posting_date>=%(from_date)s and
ge.posting_date<=%(to_date)s
order by ge.posting_date, ge.voucher_no
""".format(
fieldstr=fieldstr
),
filters,
as_dict=1,
)
report_data = modify_report_data(gl_entries)
summary = None
if filters["mode"] == "run" and filters["report_name"] != "Tax Detail":
report_data, summary = run_report(filters["report_name"], report_data)
# return columns, data, message, chart, report_summary
return get_columns(fieldlist), report_data, None, None, summary
def run_report(report_name, data):
"Applies the sections and filters saved in the custom report"
report_config = json.loads(frappe.get_doc("Report", report_name).json)
# Columns indexed from 1 wrt colno
columns = report_config.get("columns")
sections = report_config.get("sections", {})
show_detail = report_config.get("show_detail", 1)
report = {}
new_data = []
summary = []
for section_name, section in sections.items():
report[section_name] = {"rows": [], "subtotal": 0.0}
for component_name, component in section.items():
if component["type"] == "filter":
for row in data:
matched = True
for colno, filter_string in component["filters"].items():
filter_field = columns[int(colno) - 1]["fieldname"]
if not filter_match(row[filter_field], filter_string):
matched = False
break
if matched:
report[section_name]["rows"] += [row]
report[section_name]["subtotal"] += row["amount"]
if component["type"] == "section":
if component_name == section_name:
frappe.throw(_("A report component cannot refer to its parent section") + ": " + section_name)
try:
report[section_name]["rows"] += report[component_name]["rows"]
report[section_name]["subtotal"] += report[component_name]["subtotal"]
except KeyError:
frappe.throw(
_("A report component can only refer to an earlier section") + ": " + section_name
)
if show_detail:
new_data += report[section_name]["rows"]
new_data += [{"voucher_no": section_name, "amount": report[section_name]["subtotal"]}]
summary += [
{"label": section_name, "datatype": "Currency", "value": report[section_name]["subtotal"]}
]
if show_detail:
new_data += [{}]
return new_data or data, summary or None
def filter_match(value, string):
"Approximation to datatable filters"
import datetime
if string == "":
return True
if value is None:
value = -999999999999999
elif isinstance(value, datetime.date):
return True
if isinstance(value, str):
value = value.lower()
string = string.lower()
if string[0] == "<":
return True if string[1:].strip() else False
elif string[0] == ">":
return False if string[1:].strip() else True
elif string[0] == "=":
return string[1:] in value if string[1:] else False
elif string[0:2] == "!=":
return string[2:] not in value
elif len(string.split(":")) == 2:
pre, post = string.split(":")
return True if not pre.strip() and post.strip() in value else False
else:
return string in value
else:
if string[0] in ["<", ">", "="]:
operator = string[0]
if operator == "=":
operator = "=="
string = string[1:].strip()
elif string[0:2] == "!=":
operator = "!="
string = string[2:].strip()
elif len(string.split(":")) == 2:
pre, post = string.split(":")
try:
return True if float(pre) <= value and float(post) >= value else False
except ValueError:
return False if pre.strip() else True
else:
return string in str(value)
try:
num = float(string) if string.strip() else 0
return frappe.safe_eval(f"{value} {operator} {num}")
except ValueError:
if operator == "<":
return True
return False
def abbrev(dt):
return "".join(l[0].lower() for l in dt.split(" ")) + "."
def doclist(dt, dfs):
return [abbrev(dt) + f for f in dfs]
def as_split(fields):
for field in fields:
split = field.split(" as ")
yield (split[0], split[1] if len(split) > 1 else split[0])
def coalesce(doctypes, fields):
coalesce = []
for name, new_name in as_split(fields):
sharedfields = ", ".join(abbrev(dt) + name for dt in doctypes)
coalesce += [f"coalesce({sharedfields}) as {new_name}"]
return coalesce
def get_fieldstr(fieldlist):
fields = []
for doctypes, docfields in fieldlist.items():
if len(doctypes) == 1 or isinstance(doctypes[1], int):
fields += doclist(doctypes[0], docfields)
else:
fields += coalesce(doctypes, docfields)
return ", ".join(fields)
def get_columns(fieldlist):
columns = {}
for doctypes, docfields in fieldlist.items():
fieldmap = {name: new_name for name, new_name in as_split(docfields)}
for doctype in doctypes:
if isinstance(doctype, int):
break
meta = frappe.get_meta(doctype)
# get column field metadata from the db
fieldmeta = {}
for field in meta.get("fields"):
if field.fieldname in fieldmap.keys():
new_name = fieldmap[field.fieldname]
fieldmeta[new_name] = {
"label": _(field.label),
"fieldname": new_name,
"fieldtype": field.fieldtype,
"options": field.options,
}
# edit the columns to match the modified data
for field in fieldmap.values():
col = modify_report_columns(doctype, field, fieldmeta[field])
if col:
columns[col["fieldname"]] = col
# use of a dict ensures duplicate columns are removed
return list(columns.values())
def modify_report_columns(doctype, field, column):
"Because data is rearranged into other columns"
if doctype in ["Sales Invoice Item", "Purchase Invoice Item"]:
if field in ["item_tax_rate", "base_net_amount"]:
return None
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")})
return column
def modify_report_data(data):
import json
new_data = []
for line in data:
if line.debit:
line.amount = -line.debit
else:
line.amount = line.credit
# Remove Invoice GL Tax Entries and generate Tax entries from the invoice lines
if "Invoice" in line.voucher_type:
if line.account_type not in ("Tax", "Round Off"):
new_data += [line]
if line.item_tax_rate:
tax_rates = json.loads(line.item_tax_rate)
for account, rate in tax_rates.items():
tax_line = line.copy()
tax_line.account_type = "Tax"
tax_line.account = account
if line.voucher_type == "Sales Invoice":
line.amount = line.base_net_amount
tax_line.amount = line.base_net_amount * (rate / 100)
if line.voucher_type == "Purchase Invoice":
line.amount = -line.base_net_amount
tax_line.amount = -line.base_net_amount * (rate / 100)
new_data += [tax_line]
else:
new_data += [line]
return new_data
# JS client utilities
custom_report_dict = {
"ref_doctype": "GL Entry",
"report_type": "Custom Report",
"reference_report": "Tax Detail",
}
@frappe.whitelist()
def get_custom_reports(name=None):
filters = custom_report_dict.copy()
if name:
filters["name"] = name
reports = frappe.get_list("Report", filters=filters, fields=["name", "json"], as_list=False)
reports_dict = {rep.pop("name"): rep for rep in reports}
# Prevent custom reports with the same name
reports_dict["Tax Detail"] = {"json": None}
return reports_dict
@frappe.whitelist()
def save_custom_report(reference_report, report_name, data):
if reference_report != "Tax Detail":
frappe.throw(_("The wrong report is referenced."))
if report_name == "Tax Detail":
frappe.throw(_("The parent report cannot be overwritten."))
doc = {
"doctype": "Report",
"report_name": report_name,
"is_standard": "No",
"module": "Accounts",
"json": data,
}
doc.update(custom_report_dict)
try:
newdoc = frappe.get_doc(doc)
newdoc.insert()
frappe.msgprint(_("Report created successfully"))
except frappe.exceptions.DuplicateEntryError:
dbdoc = frappe.get_doc("Report", report_name)
dbdoc.update(doc)
dbdoc.save()
frappe.msgprint(_("Report updated successfully"))
return report_name

View File

@@ -1,840 +0,0 @@
[
{
"account_manager": null,
"accounts": [],
"companies": [],
"credit_limits": [],
"customer_details": null,
"customer_group": "All Customer Groups",
"customer_name": "_Test Customer",
"customer_pos_id": null,
"customer_primary_address": null,
"customer_primary_contact": null,
"customer_type": "Company",
"default_bank_account": null,
"default_commission_rate": 0.0,
"default_currency": null,
"default_price_list": null,
"default_sales_partner": null,
"disabled": 0,
"dn_required": 0,
"docstatus": 0,
"doctype": "Customer",
"email_id": null,
"gender": null,
"image": null,
"industry": null,
"is_frozen": 0,
"is_internal_customer": 0,
"language": "en",
"lead_name": null,
"loyalty_program": null,
"loyalty_program_tier": null,
"market_segment": null,
"mobile_no": null,
"modified": "2021-02-15 05:18:03.624724",
"name": "_Test Customer",
"naming_series": "CUST-.YYYY.-",
"pan": null,
"parent": null,
"parentfield": null,
"parenttype": null,
"payment_terms": null,
"primary_address": null,
"represents_company": "",
"sales_team": [],
"salutation": null,
"so_required": 0,
"tax_category": null,
"tax_id": null,
"tax_withholding_category": null,
"territory": "All Territories",
"website": null
},{
"accounts": [],
"allow_purchase_invoice_creation_without_purchase_order": 0,
"allow_purchase_invoice_creation_without_purchase_receipt": 0,
"companies": [],
"country": "United Kingdom",
"default_bank_account": null,
"default_currency": null,
"default_price_list": null,
"disabled": 0,
"docstatus": 0,
"doctype": "Supplier",
"hold_type": "",
"image": null,
"is_frozen": 0,
"is_internal_supplier": 0,
"is_transporter": 0,
"language": "en",
"modified": "2021-03-31 16:47:10.109316",
"name": "_Test Supplier",
"naming_series": "SUP-.YYYY.-",
"on_hold": 0,
"pan": null,
"parent": null,
"parentfield": null,
"parenttype": null,
"payment_terms": null,
"prevent_pos": 0,
"prevent_rfqs": 0,
"release_date": null,
"represents_company": null,
"supplier_details": null,
"supplier_group": "Raw Material",
"supplier_name": "_Test Supplier",
"supplier_type": "Company",
"tax_category": null,
"tax_id": null,
"tax_withholding_category": null,
"warn_pos": 0,
"warn_rfqs": 0,
"website": null
},{
"account_currency": "GBP",
"account_name": "Debtors",
"account_number": "",
"account_type": "Receivable",
"balance_must_be": "",
"company": "_T",
"disabled": 0,
"docstatus": 0,
"doctype": "Account",
"freeze_account": "No",
"include_in_gross": 0,
"inter_company_account": 0,
"is_group": 0,
"lft": 58,
"modified": "2021-03-26 04:44:19.955468",
"name": "Debtors - _T",
"old_parent": null,
"parent": null,
"parent_account": "Application of Funds (Assets) - _T",
"parentfield": null,
"parenttype": null,
"report_type": "Balance Sheet",
"rgt": 59,
"root_type": "Asset",
"tax_rate": 0.0
},{
"account_currency": "GBP",
"account_name": "Sales",
"account_number": "",
"account_type": "Income Account",
"balance_must_be": "",
"company": "_T",
"disabled": 0,
"docstatus": 0,
"doctype": "Account",
"freeze_account": "No",
"include_in_gross": 0,
"inter_company_account": 0,
"is_group": 0,
"lft": 291,
"modified": "2021-03-26 04:50:21.697703",
"name": "Sales - _T",
"old_parent": null,
"parent": null,
"parent_account": "Income - _T",
"parentfield": null,
"parenttype": null,
"report_type": "Profit and Loss",
"rgt": 292,
"root_type": "Income",
"tax_rate": 0.0
},{
"account_currency": "GBP",
"account_name": "VAT on Sales",
"account_number": "",
"account_type": "Tax",
"balance_must_be": "",
"company": "_T",
"disabled": 0,
"docstatus": 0,
"doctype": "Account",
"freeze_account": "No",
"include_in_gross": 0,
"inter_company_account": 0,
"is_group": 0,
"lft": 317,
"modified": "2021-03-26 04:50:21.697703",
"name": "VAT on Sales - _T",
"old_parent": null,
"parent": null,
"parent_account": "Source of Funds (Liabilities) - _T",
"parentfield": null,
"parenttype": null,
"report_type": "Balance Sheet",
"rgt": 318,
"root_type": "Liability",
"tax_rate": 0.0
},{
"account_currency": "GBP",
"account_name": "Cost of Goods Sold",
"account_number": "",
"account_type": "Cost of Goods Sold",
"balance_must_be": "",
"company": "_T",
"disabled": 0,
"docstatus": 0,
"doctype": "Account",
"freeze_account": "No",
"include_in_gross": 0,
"inter_company_account": 0,
"is_group": 0,
"lft": 171,
"modified": "2021-03-26 04:44:19.994857",
"name": "Cost of Goods Sold - _T",
"old_parent": null,
"parent": null,
"parent_account": "Expenses - _T",
"parentfield": null,
"parenttype": null,
"report_type": "Profit and Loss",
"rgt": 172,
"root_type": "Expense",
"tax_rate": 0.0
},{
"account_currency": "GBP",
"account_name": "VAT on Purchases",
"account_number": "",
"account_type": "Tax",
"balance_must_be": "",
"company": "_T",
"disabled": 0,
"docstatus": 0,
"doctype": "Account",
"freeze_account": "No",
"include_in_gross": 0,
"inter_company_account": 0,
"is_group": 0,
"lft": 80,
"modified": "2021-03-26 04:44:19.961983",
"name": "VAT on Purchases - _T",
"old_parent": null,
"parent": null,
"parent_account": "Application of Funds (Assets) - _T",
"parentfield": null,
"parenttype": null,
"report_type": "Balance Sheet",
"rgt": 81,
"root_type": "Asset",
"tax_rate": 0.0
},{
"account_currency": "GBP",
"account_name": "Creditors",
"account_number": "",
"account_type": "Payable",
"balance_must_be": "",
"company": "_T",
"disabled": 0,
"docstatus": 0,
"doctype": "Account",
"freeze_account": "No",
"include_in_gross": 0,
"inter_company_account": 0,
"is_group": 0,
"lft": 302,
"modified": "2021-03-26 04:50:21.697703",
"name": "Creditors - _T",
"old_parent": null,
"parent": null,
"parent_account": "Source of Funds (Liabilities) - _T",
"parentfield": null,
"parenttype": null,
"report_type": "Balance Sheet",
"rgt": 303,
"root_type": "Liability",
"tax_rate": 0.0
},{
"additional_discount_percentage": 0.0,
"address_display": null,
"adjust_advance_taxes": 0,
"advances": [],
"against_expense_account": "Cost of Goods Sold - _T",
"allocate_advances_automatically": 0,
"amended_from": null,
"apply_discount_on": "Grand Total",
"apply_tds": 0,
"auto_repeat": null,
"base_discount_amount": 0.0,
"base_grand_total": 511.68,
"base_in_words": "GBP Five Hundred And Eleven and Sixty Eight Pence only.",
"base_net_total": 426.4,
"base_paid_amount": 0.0,
"base_rounded_total": 511.68,
"base_rounding_adjustment": 0.0,
"base_taxes_and_charges_added": 85.28,
"base_taxes_and_charges_deducted": 0.0,
"base_total": 426.4,
"base_total_taxes_and_charges": 85.28,
"base_write_off_amount": 0.0,
"bill_date": null,
"bill_no": null,
"billing_address": null,
"billing_address_display": null,
"buying_price_list": "Standard Buying",
"cash_bank_account": null,
"clearance_date": null,
"company": "_T",
"contact_display": null,
"contact_email": null,
"contact_mobile": null,
"contact_person": null,
"conversion_rate": 1.0,
"cost_center": null,
"credit_to": "Creditors - _T",
"currency": "GBP",
"disable_rounded_total": 0,
"discount_amount": 0.0,
"docstatus": 0,
"doctype": "Purchase Invoice",
"due_date": null,
"from_date": null,
"grand_total": 511.68,
"group_same_items": 0,
"hold_comment": null,
"ignore_pricing_rule": 0,
"in_words": "GBP Five Hundred And Eleven and Sixty Eight Pence only.",
"inter_company_invoice_reference": null,
"is_internal_supplier": 0,
"is_opening": "No",
"is_paid": 0,
"is_return": 0,
"is_subcontracted": 0,
"items": [
{
"allow_zero_valuation_rate": 0,
"amount": 426.4,
"asset_category": null,
"asset_location": null,
"base_amount": 426.4,
"base_net_amount": 426.4,
"base_net_rate": 5.33,
"base_price_list_rate": 5.33,
"base_rate": 5.33,
"base_rate_with_margin": 0.0,
"batch_no": null,
"bom": null,
"brand": null,
"conversion_factor": 0.0,
"cost_center": "Main - _T",
"deferred_expense_account": null,
"description": "<div class=\"ql-editor read-mode\"><p>Fluid to make widgets</p></div>",
"discount_amount": 0.0,
"discount_percentage": 0.0,
"enable_deferred_expense": 0,
"expense_account": "Cost of Goods Sold - _T",
"from_warehouse": null,
"image": null,
"include_exploded_items": 0,
"is_fixed_asset": 0,
"is_free_item": 0,
"item_code": null,
"item_group": null,
"item_name": "Widget Fluid 1Litre",
"item_tax_amount": 0.0,
"item_tax_rate": "{\"VAT on Purchases - _T\": 20.0}",
"item_tax_template": null,
"landed_cost_voucher_amount": 0.0,
"manufacturer": null,
"manufacturer_part_no": null,
"margin_rate_or_amount": 0.0,
"margin_type": "",
"net_amount": 426.4,
"net_rate": 5.33,
"page_break": 0,
"parent": null,
"parentfield": "items",
"parenttype": "Purchase Invoice",
"po_detail": null,
"pr_detail": null,
"price_list_rate": 5.33,
"pricing_rules": null,
"project": null,
"purchase_invoice_item": null,
"purchase_order": null,
"purchase_receipt": null,
"qty": 80.0,
"quality_inspection": null,
"rate": 5.33,
"rate_with_margin": 0.0,
"received_qty": 0.0,
"rejected_qty": 0.0,
"rejected_serial_no": null,
"rejected_warehouse": null,
"rm_supp_cost": 0.0,
"sales_invoice_item": null,
"serial_no": null,
"service_end_date": null,
"service_start_date": null,
"service_stop_date": null,
"stock_qty": 0.0,
"stock_uom": "Nos",
"stock_uom_rate": 0.0,
"total_weight": 0.0,
"uom": "Nos",
"valuation_rate": 0.0,
"warehouse": null,
"weight_per_unit": 0.0,
"weight_uom": null
}
],
"language": "en",
"letter_head": null,
"mode_of_payment": null,
"modified": "2021-04-03 03:33:09.180453",
"name": null,
"naming_series": "ACC-PINV-.YYYY.-",
"net_total": 426.4,
"on_hold": 0,
"other_charges_calculation": "<div class=\"tax-break-up\" style=\"overflow-x: auto;\">\n\t<table class=\"table table-bordered table-hover\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-left\">Item</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-right\">Taxable Amount</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-right\">VAT on Purchases</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t</tr>\n\t\t</thead>\n\t\t<tbody>\n\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Widget Fluid 1Litre</td>\n\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 426.40\n\t\t\t\t\t\t\n\t\t\t\t\t</td>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(20.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 85.28\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t</tr>\n\t\t\t\n\t\t</tbody>\n\t</table>\n</div>",
"outstanding_amount": 511.68,
"paid_amount": 0.0,
"parent": null,
"parentfield": null,
"parenttype": null,
"party_account_currency": "GBP",
"payment_schedule": [],
"payment_terms_template": null,
"plc_conversion_rate": 1.0,
"posting_date": null,
"posting_time": "16:59:56.789522",
"price_list_currency": "GBP",
"pricing_rules": [],
"project": null,
"rejected_warehouse": null,
"release_date": null,
"remarks": "No Remarks",
"represents_company": null,
"return_against": null,
"rounded_total": 511.68,
"rounding_adjustment": 0.0,
"scan_barcode": null,
"select_print_heading": null,
"set_from_warehouse": null,
"set_posting_time": 0,
"set_warehouse": null,
"shipping_address": null,
"shipping_address_display": "",
"shipping_rule": null,
"status": "Unpaid",
"supplied_items": [],
"supplier": "_Test Supplier",
"supplier_address": null,
"supplier_name": "_Test Supplier",
"supplier_warehouse": "Stores - _T",
"tax_category": null,
"tax_id": null,
"tax_withholding_category": null,
"taxes": [
{
"account_head": "VAT on Purchases - _T",
"add_deduct_tax": "Add",
"base_tax_amount": 85.28,
"base_tax_amount_after_discount_amount": 85.28,
"base_total": 511.68,
"category": "Total",
"charge_type": "On Net Total",
"cost_center": "Main - _T",
"description": "VAT on Purchases",
"included_in_print_rate": 0,
"item_wise_tax_detail": "{\"Widget Fluid 1Litre\":[20.0,85.28]}",
"parent": null,
"parentfield": "taxes",
"parenttype": "Purchase Invoice",
"rate": 0.0,
"row_id": null,
"tax_amount": 85.28,
"tax_amount_after_discount_amount": 85.28,
"total": 511.68
}
],
"taxes_and_charges": null,
"taxes_and_charges_added": 85.28,
"taxes_and_charges_deducted": 0.0,
"tc_name": null,
"terms": null,
"title": "_Purchase Invoice",
"to_date": null,
"total": 426.4,
"total_advance": 0.0,
"total_net_weight": 0.0,
"total_qty": 80.0,
"total_taxes_and_charges": 85.28,
"unrealized_profit_loss_account": null,
"update_stock": 0,
"write_off_account": null,
"write_off_amount": 0.0,
"write_off_cost_center": null
},{
"account_for_change_amount": null,
"additional_discount_percentage": 0.0,
"address_display": null,
"advances": [],
"against_income_account": "Sales - _T",
"allocate_advances_automatically": 0,
"amended_from": null,
"apply_discount_on": "Grand Total",
"auto_repeat": null,
"base_change_amount": 0.0,
"base_discount_amount": 0.0,
"base_grand_total": 868.25,
"base_in_words": "GBP Eight Hundred And Sixty Eight and Twenty Five Pence only.",
"base_net_total": 825.0,
"base_paid_amount": 0.0,
"base_rounded_total": 868.25,
"base_rounding_adjustment": 0.0,
"base_total": 825.0,
"base_total_taxes_and_charges": 43.25,
"base_write_off_amount": 0.0,
"c_form_applicable": "No",
"c_form_no": null,
"campaign": null,
"cash_bank_account": null,
"change_amount": 0.0,
"commission_rate": 0.0,
"company": "_T",
"company_address": null,
"company_address_display": null,
"company_tax_id": null,
"contact_display": null,
"contact_email": null,
"contact_mobile": null,
"contact_person": null,
"conversion_rate": 1.0,
"cost_center": null,
"currency": "GBP",
"customer": "_Test Customer",
"customer_address": null,
"customer_group": "All Customer Groups",
"customer_name": "_Test Customer",
"debit_to": "Debtors - _T",
"discount_amount": 0.0,
"docstatus": 0,
"doctype": "Sales Invoice",
"due_date": null,
"from_date": null,
"grand_total": 868.25,
"group_same_items": 0,
"ignore_pricing_rule": 0,
"in_words": "GBP Eight Hundred And Sixty Eight and Twenty Five Pence only.",
"inter_company_invoice_reference": null,
"is_consolidated": 0,
"is_discounted": 0,
"is_internal_customer": 0,
"is_opening": "No",
"is_pos": 0,
"is_return": 0,
"items": [
{
"actual_batch_qty": 0.0,
"actual_qty": 0.0,
"allow_zero_valuation_rate": 0,
"amount": 200.0,
"asset": null,
"barcode": null,
"base_amount": 200.0,
"base_net_amount": 200.0,
"base_net_rate": 50.0,
"base_price_list_rate": 0.0,
"base_rate": 50.0,
"base_rate_with_margin": 0.0,
"batch_no": null,
"brand": null,
"conversion_factor": 1.0,
"cost_center": "Main - _T",
"customer_item_code": null,
"deferred_revenue_account": null,
"delivered_by_supplier": 0,
"delivered_qty": 0.0,
"delivery_note": null,
"description": "<div class=\"ql-editor read-mode\"><p>Used</p></div>",
"discount_amount": 0.0,
"discount_percentage": 0.0,
"dn_detail": null,
"enable_deferred_revenue": 0,
"expense_account": null,
"finance_book": null,
"image": null,
"income_account": "Sales - _T",
"incoming_rate": 0.0,
"is_fixed_asset": 0,
"is_free_item": 0,
"item_code": null,
"item_group": null,
"item_name": "Dunlop tyres",
"item_tax_rate": "{\"VAT on Sales - _T\": 20.0}",
"item_tax_template": null,
"margin_rate_or_amount": 0.0,
"margin_type": "",
"net_amount": 200.0,
"net_rate": 50.0,
"page_break": 0,
"parent": null,
"parentfield": "items",
"parenttype": "Sales Invoice",
"price_list_rate": 0.0,
"pricing_rules": null,
"project": null,
"qty": 4.0,
"quality_inspection": null,
"rate": 50.0,
"rate_with_margin": 0.0,
"sales_invoice_item": null,
"sales_order": null,
"serial_no": null,
"service_end_date": null,
"service_start_date": null,
"service_stop_date": null,
"so_detail": null,
"stock_qty": 4.0,
"stock_uom": "Nos",
"stock_uom_rate": 50.0,
"target_warehouse": null,
"total_weight": 0.0,
"uom": "Nos",
"warehouse": null,
"weight_per_unit": 0.0,
"weight_uom": null
},
{
"actual_batch_qty": 0.0,
"actual_qty": 0.0,
"allow_zero_valuation_rate": 0,
"amount": 65.0,
"asset": null,
"barcode": null,
"base_amount": 65.0,
"base_net_amount": 65.0,
"base_net_rate": 65.0,
"base_price_list_rate": 0.0,
"base_rate": 65.0,
"base_rate_with_margin": 0.0,
"batch_no": null,
"brand": null,
"conversion_factor": 1.0,
"cost_center": "Main - _T",
"customer_item_code": null,
"deferred_revenue_account": null,
"delivered_by_supplier": 0,
"delivered_qty": 0.0,
"delivery_note": null,
"description": "<div class=\"ql-editor read-mode\"><p>Used</p></div>",
"discount_amount": 0.0,
"discount_percentage": 0.0,
"dn_detail": null,
"enable_deferred_revenue": 0,
"expense_account": null,
"finance_book": null,
"image": null,
"income_account": "Sales - _T",
"incoming_rate": 0.0,
"is_fixed_asset": 0,
"is_free_item": 0,
"item_code": "",
"item_group": null,
"item_name": "Continental tyres",
"item_tax_rate": "{\"VAT on Sales - _T\": 5.0}",
"item_tax_template": null,
"margin_rate_or_amount": 0.0,
"margin_type": "",
"net_amount": 65.0,
"net_rate": 65.0,
"page_break": 0,
"parent": null,
"parentfield": "items",
"parenttype": "Sales Invoice",
"price_list_rate": 0.0,
"pricing_rules": null,
"project": null,
"qty": 1.0,
"quality_inspection": null,
"rate": 65.0,
"rate_with_margin": 0.0,
"sales_invoice_item": null,
"sales_order": null,
"serial_no": null,
"service_end_date": null,
"service_start_date": null,
"service_stop_date": null,
"so_detail": null,
"stock_qty": 1.0,
"stock_uom": null,
"stock_uom_rate": 65.0,
"target_warehouse": null,
"total_weight": 0.0,
"uom": "Nos",
"warehouse": null,
"weight_per_unit": 0.0,
"weight_uom": null
},
{
"actual_batch_qty": 0.0,
"actual_qty": 0.0,
"allow_zero_valuation_rate": 0,
"amount": 560.0,
"asset": null,
"barcode": null,
"base_amount": 560.0,
"base_net_amount": 560.0,
"base_net_rate": 70.0,
"base_price_list_rate": 0.0,
"base_rate": 70.0,
"base_rate_with_margin": 0.0,
"batch_no": null,
"brand": null,
"conversion_factor": 1.0,
"cost_center": "Main - _T",
"customer_item_code": null,
"deferred_revenue_account": null,
"delivered_by_supplier": 0,
"delivered_qty": 0.0,
"delivery_note": null,
"description": "<div class=\"ql-editor read-mode\"><p>New</p></div>",
"discount_amount": 0.0,
"discount_percentage": 0.0,
"dn_detail": null,
"enable_deferred_revenue": 0,
"expense_account": null,
"finance_book": null,
"image": null,
"income_account": "Sales - _T",
"incoming_rate": 0.0,
"is_fixed_asset": 0,
"is_free_item": 0,
"item_code": null,
"item_group": null,
"item_name": "Toyo tyres",
"item_tax_rate": "{\"VAT on Sales - _T\": 0.0}",
"item_tax_template": null,
"margin_rate_or_amount": 0.0,
"margin_type": "",
"net_amount": 560.0,
"net_rate": 70.0,
"page_break": 0,
"parent": null,
"parentfield": "items",
"parenttype": "Sales Invoice",
"price_list_rate": 0.0,
"pricing_rules": null,
"project": null,
"qty": 8.0,
"quality_inspection": null,
"rate": 70.0,
"rate_with_margin": 0.0,
"sales_invoice_item": null,
"sales_order": null,
"serial_no": null,
"service_end_date": null,
"service_start_date": null,
"service_stop_date": null,
"so_detail": null,
"stock_qty": 8.0,
"stock_uom": null,
"stock_uom_rate": 70.0,
"target_warehouse": null,
"total_weight": 0.0,
"uom": "Nos",
"warehouse": null,
"weight_per_unit": 0.0,
"weight_uom": null
}
],
"language": "en",
"letter_head": null,
"loyalty_amount": 0.0,
"loyalty_points": 0,
"loyalty_program": null,
"loyalty_redemption_account": null,
"loyalty_redemption_cost_center": null,
"modified": "2021-02-16 05:18:59.755144",
"name": null,
"naming_series": "ACC-SINV-.YYYY.-",
"net_total": 825.0,
"other_charges_calculation": "<div class=\"tax-break-up\" style=\"overflow-x: auto;\">\n\t<table class=\"table table-bordered table-hover\">\n\t\t<thead>\n\t\t\t<tr>\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-left\">Item</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-right\">Taxable Amount</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t<th class=\"text-right\">VAT on Sales</th>\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t</tr>\n\t\t</thead>\n\t\t<tbody>\n\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Dunlop tyres</td>\n\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 200.00\n\t\t\t\t\t\t\n\t\t\t\t\t</td>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(20.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 40.00\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t</tr>\n\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Continental tyres</td>\n\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 65.00\n\t\t\t\t\t\t\n\t\t\t\t\t</td>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(5.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 3.25\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t</tr>\n\t\t\t\n\t\t\t\t<tr>\n\t\t\t\t\t<td>Toyo tyres</td>\n\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t\u00a3 560.00\n\t\t\t\t\t\t\n\t\t\t\t\t</td>\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\t<td class='text-right'>\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t(0.0%)\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t\t\t\u00a3 0.00\n\t\t\t\t\t\t\t\t\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t</tr>\n\t\t\t\n\t\t</tbody>\n\t</table>\n</div>",
"outstanding_amount": 868.25,
"packed_items": [],
"paid_amount": 0.0,
"parent": null,
"parentfield": null,
"parenttype": null,
"party_account_currency": "GBP",
"payment_schedule": [],
"payment_terms_template": null,
"payments": [],
"plc_conversion_rate": 1.0,
"po_date": null,
"po_no": "",
"pos_profile": null,
"posting_date": null,
"posting_time": "5:19:02.994077",
"price_list_currency": "GBP",
"pricing_rules": [],
"project": null,
"redeem_loyalty_points": 0,
"remarks": "No Remarks",
"represents_company": "",
"return_against": null,
"rounded_total": 868.25,
"rounding_adjustment": 0.0,
"sales_partner": null,
"sales_team": [],
"scan_barcode": null,
"select_print_heading": null,
"selling_price_list": "Standard Selling",
"set_posting_time": 0,
"set_target_warehouse": null,
"set_warehouse": null,
"shipping_address": null,
"shipping_address_name": "",
"shipping_rule": null,
"source": null,
"status": "Overdue",
"tax_category": "",
"tax_id": null,
"taxes": [
{
"account_head": "VAT on Sales - _T",
"base_tax_amount": 43.25,
"base_tax_amount_after_discount_amount": 43.25,
"base_total": 868.25,
"charge_type": "On Net Total",
"cost_center": "Main - _T",
"description": "VAT on Sales",
"included_in_print_rate": 0,
"item_wise_tax_detail": "{\"Dunlop tyres\":[20.0,40.0],\"Continental tyres\":[5.0,3.25],\"Toyo tyres\":[0.0,0.0]}",
"parent": null,
"parentfield": "taxes",
"parenttype": "Sales Invoice",
"rate": 0.0,
"row_id": null,
"tax_amount": 43.25,
"tax_amount_after_discount_amount": 43.25,
"total": 868.25
}
],
"taxes_and_charges": null,
"tc_name": null,
"terms": null,
"territory": "All Territories",
"timesheets": [],
"title": "_Sales Invoice",
"to_date": null,
"total": 825.0,
"total_advance": 0.0,
"total_billing_amount": 0.0,
"total_commission": 0.0,
"total_net_weight": 0.0,
"total_qty": 13.0,
"total_taxes_and_charges": 43.25,
"unrealized_profit_loss_account": null,
"update_billed_amount_in_sales_order": 0,
"update_stock": 0,
"write_off_account": null,
"write_off_amount": 0.0,
"write_off_cost_center": null,
"write_off_outstanding_amount_automatically": 0
}
]

View File

@@ -1,213 +0,0 @@
import datetime
import json
import os
import unittest
import frappe
from frappe.utils import (
add_to_date,
get_first_day,
get_last_day,
get_year_ending,
get_year_start,
getdate,
)
from .tax_detail import filter_match, save_custom_report
class TestTaxDetail(unittest.TestCase):
def load_testdocs(self):
from erpnext.accounts.utils import FiscalYearError, get_fiscal_year
datapath, _ = os.path.splitext(os.path.realpath(__file__))
with open(datapath + ".json", "r") as fp:
docs = json.load(fp)
now = getdate()
self.from_date = get_first_day(now)
self.to_date = get_last_day(now)
try:
get_fiscal_year(now, company="_T")
except FiscalYearError:
docs = [
{
"companies": [
{
"company": "_T",
"parent": "_Test Fiscal",
"parentfield": "companies",
"parenttype": "Fiscal Year",
}
],
"doctype": "Fiscal Year",
"year": "_Test Fiscal",
"year_end_date": get_year_ending(now),
"year_start_date": get_year_start(now),
}
] + docs
docs = [
{
"abbr": "_T",
"company_name": "_T",
"country": "United Kingdom",
"default_currency": "GBP",
"doctype": "Company",
"name": "_T",
}
] + docs
for doc in docs:
try:
db_doc = frappe.get_doc(doc)
if "Invoice" in db_doc.doctype:
db_doc.due_date = add_to_date(now, days=1)
db_doc.insert()
# Create GL Entries:
db_doc.submit()
else:
db_doc.insert(ignore_if_duplicate=True)
except frappe.exceptions.DuplicateEntryError:
pass
def load_defcols(self):
self.company = frappe.get_doc("Company", "_T")
custom_report = frappe.get_doc("Report", "Tax Detail")
self.default_columns, _ = custom_report.run_query_report(
filters={
"from_date": "2021-03-01",
"to_date": "2021-03-31",
"company": self.company.name,
"mode": "run",
"report_name": "Tax Detail",
},
user=frappe.session.user,
)
def rm_testdocs(self):
"Remove the Company and all data"
from erpnext.setup.doctype.company.company import create_transaction_deletion_request
create_transaction_deletion_request(self.company.name)
def test_report(self):
self.load_testdocs()
self.load_defcols()
report_name = save_custom_report(
"Tax Detail",
"_Test Tax Detail",
json.dumps(
{
"columns": self.default_columns,
"sections": {
"Box1": {"Filter0": {"type": "filter", "filters": {"4": "VAT on Sales"}}},
"Box2": {"Filter0": {"type": "filter", "filters": {"4": "Acquisition"}}},
"Box3": {"Box1": {"type": "section"}, "Box2": {"type": "section"}},
"Box4": {"Filter0": {"type": "filter", "filters": {"4": "VAT on Purchases"}}},
"Box5": {"Box3": {"type": "section"}, "Box4": {"type": "section"}},
"Box6": {"Filter0": {"type": "filter", "filters": {"3": "!=Tax", "4": "Sales"}}},
"Box7": {"Filter0": {"type": "filter", "filters": {"2": "Expense", "3": "!=Tax"}}},
"Box8": {"Filter0": {"type": "filter", "filters": {"3": "!=Tax", "4": "Sales", "12": "EU"}}},
"Box9": {
"Filter0": {"type": "filter", "filters": {"2": "Expense", "3": "!=Tax", "12": "EU"}}
},
},
"show_detail": 1,
}
),
)
data = frappe.desk.query_report.run(
report_name,
filters={
"from_date": self.from_date,
"to_date": self.to_date,
"company": self.company.name,
"mode": "run",
"report_name": report_name,
},
user=frappe.session.user,
)
self.assertListEqual(data.get("columns"), self.default_columns)
expected = (
("Box1", 43.25),
("Box2", 0.0),
("Box3", 43.25),
("Box4", -85.28),
("Box5", -42.03),
("Box6", 825.0),
("Box7", -426.40),
("Box8", 0.0),
("Box9", 0.0),
)
exrow = iter(expected)
for row in data.get("result"):
if row.get("voucher_no") and not row.get("posting_date"):
label, value = next(exrow)
self.assertDictEqual(row, {"voucher_no": label, "amount": value})
self.assertListEqual(
data.get("report_summary"),
[{"label": label, "datatype": "Currency", "value": value} for label, value in expected],
)
self.rm_testdocs()
def test_filter_match(self):
# None - treated as -inf number except range
self.assertTrue(filter_match(None, "!="))
self.assertTrue(filter_match(None, "<"))
self.assertTrue(filter_match(None, "<jjj"))
self.assertTrue(filter_match(None, " : "))
self.assertTrue(filter_match(None, ":56"))
self.assertTrue(filter_match(None, ":de"))
self.assertFalse(filter_match(None, "3.4"))
self.assertFalse(filter_match(None, "="))
self.assertFalse(filter_match(None, "=3.4"))
self.assertFalse(filter_match(None, ">3.4"))
self.assertFalse(filter_match(None, " <"))
self.assertFalse(filter_match(None, "ew"))
self.assertFalse(filter_match(None, " "))
self.assertFalse(filter_match(None, " f :"))
# Numbers
self.assertTrue(filter_match(3.4, "3.4"))
self.assertTrue(filter_match(3.4, ".4"))
self.assertTrue(filter_match(3.4, "3"))
self.assertTrue(filter_match(-3.4, "< -3"))
self.assertTrue(filter_match(-3.4, "> -4"))
self.assertTrue(filter_match(3.4, "= 3.4 "))
self.assertTrue(filter_match(3.4, "!=4.5"))
self.assertTrue(filter_match(3.4, " 3 : 4 "))
self.assertTrue(filter_match(0.0, " : "))
self.assertFalse(filter_match(3.4, "=4.5"))
self.assertFalse(filter_match(3.4, " = 3.4 "))
self.assertFalse(filter_match(3.4, "!=3.4"))
self.assertFalse(filter_match(3.4, ">6"))
self.assertFalse(filter_match(3.4, "<-4.5"))
self.assertFalse(filter_match(3.4, "4.5"))
self.assertFalse(filter_match(3.4, "5:9"))
# Strings
self.assertTrue(filter_match("ACC-SINV-2021-00001", "SINV"))
self.assertTrue(filter_match("ACC-SINV-2021-00001", "sinv"))
self.assertTrue(filter_match("ACC-SINV-2021-00001", "-2021"))
self.assertTrue(filter_match(" ACC-SINV-2021-00001", " acc"))
self.assertTrue(filter_match("ACC-SINV-2021-00001", "=2021"))
self.assertTrue(filter_match("ACC-SINV-2021-00001", "!=zz"))
self.assertTrue(filter_match("ACC-SINV-2021-00001", "< zzz "))
self.assertTrue(filter_match("ACC-SINV-2021-00001", " : sinv "))
self.assertFalse(filter_match("ACC-SINV-2021-00001", " sinv :"))
self.assertFalse(filter_match("ACC-SINV-2021-00001", " acc"))
self.assertFalse(filter_match("ACC-SINV-2021-00001", "= 2021 "))
self.assertFalse(filter_match("ACC-SINV-2021-00001", "!=sinv"))
self.assertFalse(filter_match("ACC-SINV-2021-00001", " >"))
self.assertFalse(filter_match("ACC-SINV-2021-00001", ">aa"))
self.assertFalse(filter_match("ACC-SINV-2021-00001", " <"))
self.assertFalse(filter_match("ACC-SINV-2021-00001", "< "))
self.assertFalse(filter_match("ACC-SINV-2021-00001", " ="))
self.assertFalse(filter_match("ACC-SINV-2021-00001", "="))
# Date - always match
self.assertTrue(filter_match(datetime.date(2021, 3, 19), " kdsjkldfs "))

View File

@@ -4,7 +4,8 @@
import frappe
from frappe import _
from frappe.utils import cstr, flt, formatdate, getdate
from frappe.query_builder.functions import Sum
from frappe.utils import add_days, cstr, flt, formatdate, getdate
import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@@ -78,7 +79,6 @@ def validate_filters(filters):
def get_data(filters):
accounts = frappe.db.sql(
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
@@ -118,12 +118,10 @@ def get_data(filters):
ignore_closing_entries=not flt(filters.with_period_closing_entry),
)
total_row = calculate_values(
accounts, gl_entries_by_account, opening_balances, filters, company_currency
)
calculate_values(accounts, gl_entries_by_account, opening_balances)
accumulate_values_into_parents(accounts, accounts_by_name)
data = prepare_data(accounts, filters, total_row, parent_children_map, company_currency)
data = prepare_data(accounts, filters, parent_children_map, company_currency)
data = filter_out_zero_value_rows(
data, parent_children_map, show_zero_values=filters.get("show_zero_values")
)
@@ -140,45 +138,125 @@ def get_opening_balances(filters):
def get_rootwise_opening_balances(filters, report_type):
additional_conditions = ""
if not filters.show_unclosed_fy_pl_balances:
additional_conditions = (
" and posting_date >= %(year_start_date)s" if report_type == "Profit and Loss" else ""
)
gle = []
if not flt(filters.with_period_closing_entry):
additional_conditions += " and ifnull(voucher_type, '')!='Period Closing Voucher'"
if filters.cost_center:
lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"])
additional_conditions += """ and cost_center in (select name from `tabCost Center`
where lft >= %s and rgt <= %s)""" % (
lft,
rgt,
)
if filters.project:
additional_conditions += " and project = %(project)s"
if filters.get("include_default_book_entries"):
additional_conditions += (
" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
)
else:
additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
last_period_closing_voucher = frappe.db.get_all(
"Period Closing Voucher",
filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)},
fields=["posting_date", "name"],
order_by="posting_date desc",
limit=1,
)
accounting_dimensions = get_accounting_dimensions(as_list=False)
query_filters = {
"company": filters.company,
"from_date": filters.from_date,
"to_date": filters.to_date,
"report_type": report_type,
"year_start_date": filters.year_start_date,
"project": filters.project,
"finance_book": filters.finance_book,
"company_fb": frappe.get_cached_value("Company", filters.company, "default_finance_book"),
}
if last_period_closing_voucher:
gle = get_opening_balance(
"Account Closing Balance",
filters,
report_type,
accounting_dimensions,
period_closing_voucher=last_period_closing_voucher[0].name,
)
if getdate(last_period_closing_voucher[0].posting_date) < getdate(
add_days(filters.from_date, -1)
):
start_date = add_days(last_period_closing_voucher[0].posting_date, 1)
gle += get_opening_balance(
"GL Entry", filters, report_type, accounting_dimensions, start_date=start_date
)
else:
gle = get_opening_balance("GL Entry", filters, report_type, accounting_dimensions)
opening = frappe._dict()
for d in gle:
opening.setdefault(
d.account,
{
"account": d.account,
"opening_debit": 0.0,
"opening_credit": 0.0,
},
)
opening[d.account]["opening_debit"] += flt(d.opening_debit)
opening[d.account]["opening_credit"] += flt(d.opening_credit)
return opening
def get_opening_balance(
doctype, filters, report_type, accounting_dimensions, period_closing_voucher=None, start_date=None
):
closing_balance = frappe.qb.DocType(doctype)
account = frappe.qb.DocType("Account")
opening_balance = (
frappe.qb.from_(closing_balance)
.select(
closing_balance.account,
Sum(closing_balance.debit).as_("opening_debit"),
Sum(closing_balance.credit).as_("opening_credit"),
)
.where(
(closing_balance.company == filters.company)
& (
closing_balance.account.isin(
frappe.qb.from_(account).select("name").where(account.report_type == report_type)
)
)
)
.groupby(closing_balance.account)
)
if period_closing_voucher:
opening_balance = opening_balance.where(
closing_balance.period_closing_voucher == period_closing_voucher
)
else:
if start_date:
opening_balance = opening_balance.where(closing_balance.posting_date >= start_date)
opening_balance = opening_balance.where(closing_balance.is_opening == "No")
opening_balance = opening_balance.where(closing_balance.posting_date < filters.from_date)
if (
not filters.show_unclosed_fy_pl_balances
and report_type == "Profit and Loss"
and doctype == "GL Entry"
):
opening_balance = opening_balance.where(closing_balance.posting_date >= filters.year_start_date)
if not flt(filters.with_period_closing_entry):
if doctype == "Account Closing Balance":
opening_balance = opening_balance.where(closing_balance.is_period_closing_voucher_entry == 0)
else:
opening_balance = opening_balance.where(
closing_balance.voucher_type != "Period Closing Voucher"
)
if filters.cost_center:
lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"])
cost_center = frappe.qb.DocType("Cost Center")
opening_balance = opening_balance.where(
closing_balance.cost_center.in_(
frappe.qb.from_(cost_center)
.select("name")
.where((cost_center.lft >= lft) & (cost_center.rgt <= rgt))
)
)
if filters.project:
opening_balance = opening_balance.where(closing_balance.project == filters.project)
if filters.get("include_default_book_entries"):
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(filters.company_fb), ""]))
| (closing_balance.finance_book.isnull())
)
else:
opening_balance = opening_balance.where(
(closing_balance.finance_book.isin([cstr(filters.finance_book), ""]))
| (closing_balance.finance_book.isnull())
)
if accounting_dimensions:
for dimension in accounting_dimensions:
@@ -187,38 +265,20 @@ def get_rootwise_opening_balances(filters, report_type):
filters[dimension.fieldname] = get_dimension_with_children(
dimension.document_type, filters.get(dimension.fieldname)
)
additional_conditions += " and {0} in %({0})s".format(dimension.fieldname)
opening_balance = opening_balance.where(
closing_balance[dimension.fieldname].isin(filters[dimension.fieldname])
)
else:
additional_conditions += " and {0} in %({0})s".format(dimension.fieldname)
opening_balance = opening_balance.where(
closing_balance[dimension.fieldname].isin(filters[dimension.fieldname])
)
query_filters.update({dimension.fieldname: filters.get(dimension.fieldname)})
gle = opening_balance.run(as_dict=1)
gle = frappe.db.sql(
"""
select
account, sum(debit) as opening_debit, sum(credit) as opening_credit
from `tabGL Entry`
where
company=%(company)s
{additional_conditions}
and (posting_date < %(from_date)s or (ifnull(is_opening, 'No') = 'Yes' and posting_date <= %(to_date)s))
and account in (select name from `tabAccount` where report_type=%(report_type)s)
and is_cancelled = 0
group by account""".format(
additional_conditions=additional_conditions
),
query_filters,
as_dict=True,
)
opening = frappe._dict()
for d in gle:
opening.setdefault(d.account, d)
return opening
return gle
def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency):
def calculate_values(accounts, gl_entries_by_account, opening_balances):
init = {
"opening_debit": 0.0,
"opening_credit": 0.0,
@@ -228,22 +288,6 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
"closing_credit": 0.0,
}
total_row = {
"account": "'" + _("Total") + "'",
"account_name": "'" + _("Total") + "'",
"warn_if_negative": True,
"opening_debit": 0.0,
"opening_credit": 0.0,
"debit": 0.0,
"credit": 0.0,
"closing_debit": 0.0,
"closing_credit": 0.0,
"parent_account": None,
"indent": 0,
"has_value": True,
"currency": company_currency,
}
for d in accounts:
d.update(init.copy())
@@ -261,8 +305,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
prepare_opening_closing(d)
for field in value_fields:
total_row[field] += d[field]
def calculate_total_row(accounts, company_currency):
total_row = {
"account": "'" + _("Total") + "'",
"account_name": "'" + _("Total") + "'",
"warn_if_negative": True,
"opening_debit": 0.0,
"opening_credit": 0.0,
"debit": 0.0,
"credit": 0.0,
"closing_debit": 0.0,
"closing_credit": 0.0,
"parent_account": None,
"indent": 0,
"has_value": True,
"currency": company_currency,
}
for d in accounts:
if not d.parent_account:
for field in value_fields:
total_row[field] += d[field]
return total_row
@@ -274,7 +338,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
accounts_by_name[d.parent_account][key] += d[key]
def prepare_data(accounts, filters, total_row, parent_children_map, company_currency):
def prepare_data(accounts, filters, parent_children_map, company_currency):
data = []
for d in accounts:
@@ -305,6 +369,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr
row["has_value"] = has_value
data.append(row)
total_row = calculate_total_row(accounts, company_currency)
data.extend([{}, total_row])
return data

View File

@@ -30,10 +30,6 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("Sales Register", {}),
("Sales Register", {"item_group": "All Item Groups"}),
("Purchase Register", {}),
(
"Tax Detail",
{"mode": "run", "report_name": "Tax Detail"},
),
]
OPTIONAL_FILTERS = {}

View File

@@ -51,13 +51,25 @@ GL_REPOSTING_CHUNK = 100
@frappe.whitelist()
def get_fiscal_year(
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False, boolean=False
):
return get_fiscal_years(date, fiscal_year, label, verbose, company, as_dict=as_dict)[0]
fiscal_years = get_fiscal_years(
date, fiscal_year, label, verbose, company, as_dict=as_dict, boolean=boolean
)
if boolean:
return fiscal_years
else:
return fiscal_years[0]
def get_fiscal_years(
transaction_date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False
transaction_date=None,
fiscal_year=None,
label="Date",
verbose=1,
company=None,
as_dict=False,
boolean=False,
):
fiscal_years = frappe.cache().hget("fiscal_years", company) or []
@@ -121,8 +133,12 @@ def get_fiscal_years(
if company:
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
if boolean:
return False
if verbose == 1:
frappe.msgprint(error_msg)
raise FiscalYearError(error_msg)
@@ -451,12 +467,6 @@ def reconcile_against_document(args): # nosemgrep
else:
update_reference_in_payment_entry(entry, doc, do_not_save=True)
if doc.doctype == "Journal Entry":
try:
doc.validate_total_debit_and_credit()
except Exception as validation_exception:
raise frappe.ValidationError(_(f"Validation Error for {doc.name}")) from validation_exception
doc.save(ignore_permissions=True)
# re-submit advance entry
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)

View File

@@ -13,6 +13,7 @@
"hide_custom": 0,
"icon": "accounting",
"idx": 0,
"is_hidden": 0,
"label": "Accounting",
"links": [
{
@@ -493,17 +494,6 @@
"onboard": 0,
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "Tax Detail",
"link_count": 0,
"link_to": "Tax Detail",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
@@ -516,18 +506,6 @@
"only_for": "United Arab Emirates",
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "KSA VAT Report",
"link_count": 0,
"link_to": "KSA VAT",
"link_type": "Report",
"onboard": 0,
"only_for": "Saudi Arabia",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@@ -1029,17 +1007,6 @@
"only_for": "India",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "KSA VAT Setting",
"link_count": 0,
"link_to": "KSA VAT Setting",
"link_type": "DocType",
"onboard": 0,
"only_for": "Saudi Arabia",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@@ -1093,7 +1060,7 @@
"type": "Link"
}
],
"modified": "2022-06-24 05:41:09.236458",
"modified": "2023-02-23 15:32:12.135355",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",

View File

@@ -258,7 +258,7 @@ frappe.ui.form.on('Asset', {
$.each(depr_entries || [], function(i, v) {
x_intervals.push(frappe.format(v.posting_date, { fieldtype: 'Date' }));
let last_asset_value = asset_values[asset_values.length - 1]
asset_values.push(last_asset_value - v.value);
asset_values.push(flt(last_asset_value - v.value, precision('gross_purchase_amount')));
});
}
@@ -466,6 +466,9 @@ frappe.ui.form.on('Asset', {
} else {
frm.set_value('purchase_date', purchase_doc.posting_date);
}
if (!frm.doc.is_existing_asset && !frm.doc.available_for_use_date) {
frm.set_value('available_for_use_date', frm.doc.purchase_date);
}
const item = purchase_doc.items.find(item => item.item_code === frm.doc.item_code);
if (!item) {
doctype_field = frappe.scrub(doctype)

View File

@@ -79,6 +79,9 @@
"options": "ACC-ASS-.YYYY.-"
},
{
"depends_on": "item_code",
"fetch_from": "item_code.item_name",
"fetch_if_empty": 1,
"fieldname": "asset_name",
"fieldtype": "Data",
"in_list_view": 1,
@@ -517,7 +520,7 @@
"table_fieldname": "accounts"
}
],
"modified": "2023-02-02 00:03:11.706427",
"modified": "2023-03-30 15:07:41.542374",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -8,16 +8,12 @@ import math
import frappe
from frappe import _
from frappe.utils import (
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,
)
@@ -239,30 +235,6 @@ class Asset(AccountsController):
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
)
# 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
# if not existing asset, from_date = available_for_use_date
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022
from_date = self.get_modified_available_for_use_date(row)
days = date_diff(row.depreciation_start_date, from_date) + 1
# if frequency_of_depreciation is 12 months, total_days = 365
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
if days < total_days:
has_pro_rata = True
return has_pro_rata
def get_modified_available_for_use_date(self, row):
return add_months(
self.available_for_use_date,
(self.number_of_depreciations_booked * row.frequency_of_depreciation),
)
def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw(
@@ -413,11 +385,14 @@ class Asset(AccountsController):
if self.journal_entry_for_scrap:
status = "Scrapped"
elif self.finance_books:
idx = self.get_default_finance_book_idx() or 0
else:
expected_value_after_useful_life = 0
value_after_depreciation = self.value_after_depreciation
expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life
value_after_depreciation = self.finance_books[idx].value_after_depreciation
if self.calculate_depreciation:
idx = self.get_default_finance_book_idx() or 0
expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life
value_after_depreciation = self.finance_books[idx].value_after_depreciation
if flt(value_after_depreciation) <= expected_value_after_useful_life:
status = "Fully Depreciated"
@@ -463,29 +438,11 @@ class Asset(AccountsController):
.where(gle.debit != 0)
.where(gle.is_cancelled == 0)
.orderby(gle.posting_date)
.orderby(gle.creation)
).run(as_dict=True)
return records
@erpnext.allow_regional
def get_depreciation_amount(self, depreciable_value, fb_row):
if fb_row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time
if not self.flags.increase_in_asset_life:
depreciation_amount = (
flt(self.gross_purchase_amount) - flt(fb_row.expected_value_after_useful_life)
) / flt(fb_row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair
else:
depreciation_amount = (
flt(fb_row.value_after_depreciation) - flt(fb_row.expected_value_after_useful_life)
) / (date_diff(self.to_date, self.available_for_use_date) / 365)
else:
depreciation_amount = flt(depreciable_value * (flt(fb_row.rate_of_depreciation) / 100))
return depreciation_amount
def validate_make_gl_entry(self):
purchase_document = self.get_purchase_document()
if not purchase_document:
@@ -610,23 +567,42 @@ class Asset(AccountsController):
float_precision = cint(frappe.db.get_default("float_precision")) or 2
if args.get("depreciation_method") == "Double Declining Balance":
return 200.0 / args.get("total_number_of_depreciations")
return 200.0 / (
(
flt(args.get("total_number_of_depreciations"), 2) * flt(args.get("frequency_of_depreciation"))
)
/ 12
)
if args.get("depreciation_method") == "Written Down Value":
if args.get("rate_of_depreciation") and on_validate:
if (
args.get("rate_of_depreciation")
and on_validate
and not self.flags.increase_in_asset_value_due_to_repair
):
return args.get("rate_of_depreciation")
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2))
if self.flags.increase_in_asset_value_due_to_repair:
value = flt(args.get("expected_value_after_useful_life")) / flt(
args.get("value_after_depreciation")
)
else:
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
depreciation_rate = math.pow(
value,
1.0
/ (
(
flt(args.get("total_number_of_depreciations"), 2)
* flt(args.get("frequency_of_depreciation"))
)
/ 12
),
)
return flt((100 * (1 - depreciation_rate)), float_precision)
def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date):
days = date_diff(to_date, from_date)
months = month_diff(to_date, from_date)
total_days = get_total_days(to_date, row.frequency_of_depreciation)
return (depreciation_amount * flt(days)) / flt(total_days), days, months
def update_maintenance_status():
assets = frappe.get_all(
@@ -870,15 +846,6 @@ def get_asset_value_after_depreciation(asset_name, finance_book=None):
return asset.get_value_after_depreciation(finance_book)
def get_total_days(date, frequency):
period_start_date = add_months(date, cint(frequency) * -1)
if is_last_day_of_the_month(date):
period_start_date = get_last_day(period_start_date)
return date_diff(date, period_start_date)
@frappe.whitelist()
def split_asset(asset_name, split_qty):
asset = frappe.get_doc("Asset", asset_name)

View File

@@ -36,7 +36,7 @@ frappe.listview_settings['Asset'] = {
}
},
onload: function(me) {
me.page.add_action_item('Make Asset Movement', function() {
me.page.add_action_item(__("Make Asset Movement"), function() {
const assets = me.get_checked_items();
frappe.call({
method: "erpnext.assets.doctype.asset.asset.make_asset_movement",

View File

@@ -168,7 +168,7 @@ def make_depreciation_entry(asset_depr_schedule_name, date=None):
row.value_after_depreciation -= d.depreciation_amount
row.db_update()
frappe.db.set_value("Asset", asset_name, "depr_entry_posting_status", "Successful")
asset.db_set("depr_entry_posting_status", "Successful")
asset.set_status()
@@ -249,10 +249,16 @@ def notify_depr_entry_posting_error(failed_asset_names):
asset_links = get_comma_separated_asset_links(failed_asset_names)
message = (
_("Hi,")
+ "<br>"
+ _("The following assets have failed to post depreciation entries: {0}").format(asset_links)
_("Hello,")
+ "<br><br>"
+ _("The following assets have failed to automatically post depreciation entries: {0}").format(
asset_links
)
+ "."
+ "<br><br>"
+ _(
"Please raise a support ticket and share this email, or forward this email to your development team so that they can find the issue in the developer console by manually creating the depreciation entry via the asset's depreciation schedule table."
)
)
frappe.sendmail(recipients=recipients, subject=subject, message=message)

View File

@@ -29,8 +29,11 @@ from erpnext.assets.doctype.asset.depreciation import (
scrap_asset,
)
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
_check_is_pro_rata,
_get_pro_rata_amt,
get_asset_depr_schedule_doc,
get_depr_schedule,
get_depreciation_amount,
)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_invoice,
@@ -234,7 +237,7 @@ class TestAsset(AssetSetup):
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
asset.precision("gross_purchase_amount"),
)
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
pro_rata_amount, _, _ = _get_pro_rata_amt(
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
)
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
@@ -321,7 +324,7 @@ class TestAsset(AssetSetup):
self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
pro_rata_amount, _, _ = _get_pro_rata_amt(
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
)
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
@@ -828,8 +831,8 @@ class TestDepreciationMethods(AssetSetup):
expected_schedules = [
["2030-12-31", 28630.14, 28630.14],
["2031-12-31", 35684.93, 64315.07],
["2032-12-31", 17842.47, 82157.54],
["2033-06-06", 5342.46, 87500.0],
["2032-12-31", 17842.46, 82157.53],
["2033-06-06", 5342.47, 87500.0],
]
schedules = [
@@ -857,12 +860,12 @@ class TestDepreciationMethods(AssetSetup):
)
expected_schedules = [
["2022-02-28", 647.25, 647.25],
["2022-03-31", 1210.71, 1857.96],
["2022-04-30", 1053.99, 2911.95],
["2022-05-31", 917.55, 3829.5],
["2022-06-30", 798.77, 4628.27],
["2022-07-15", 371.73, 5000.0],
["2022-02-28", 310.89, 310.89],
["2022-03-31", 654.45, 965.34],
["2022-04-30", 654.45, 1619.79],
["2022-05-31", 654.45, 2274.24],
["2022-06-30", 654.45, 2928.69],
["2022-07-15", 2071.31, 5000.0],
]
schedules = [
@@ -938,7 +941,7 @@ class TestDepreciationBasics(AssetSetup):
},
)
depreciation_amount = asset.get_depreciation_amount(100000, asset.finance_books[0])
depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0])
self.assertEqual(depreciation_amount, 30000)
def test_make_depr_schedule(self):
@@ -997,7 +1000,7 @@ class TestDepreciationBasics(AssetSetup):
},
)
has_pro_rata = asset.check_is_pro_rata(asset.finance_books[0])
has_pro_rata = _check_is_pro_rata(asset, asset.finance_books[0])
self.assertFalse(has_pro_rata)
asset.finance_books = []
@@ -1012,7 +1015,7 @@ class TestDepreciationBasics(AssetSetup):
},
)
has_pro_rata = asset.check_is_pro_rata(asset.finance_books[0])
has_pro_rata = _check_is_pro_rata(asset, asset.finance_books[0])
self.assertTrue(has_pro_rata)
def test_expected_value_after_useful_life_greater_than_purchase_amount(self):

View File

@@ -43,9 +43,9 @@ 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) {
$.each(frm.doc.depreciation_schedule || [], function(i, row) {
accumulated_depreciation += flt(row.depreciation_amount);
frappe.model.set_value(row.doctype, row.name,
"accumulated_depreciation_amount", accumulated_depreciation);
frappe.model.set_value(row.doctype, row.name, "accumulated_depreciation_amount", accumulated_depreciation);
})
};

View File

@@ -10,7 +10,9 @@
"asset",
"naming_series",
"column_break_2",
"gross_purchase_amount",
"opening_accumulated_depreciation",
"number_of_depreciations_booked",
"finance_book",
"finance_book_id",
"depreciation_details_section",
@@ -148,18 +150,36 @@
"read_only": 1
},
{
"depends_on": "opening_accumulated_depreciation",
"fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency",
"hidden": 1,
"label": "Opening Accumulated Depreciation",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "gross_purchase_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Gross Purchase Amount",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "number_of_depreciations_booked",
"fieldtype": "Int",
"hidden": 1,
"label": "Number of Depreciations Booked",
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-01-16 21:08:21.421260",
"modified": "2023-02-26 16:37:23.734806",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Depreciation Schedule",

View File

@@ -4,7 +4,19 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import add_days, add_months, cint, flt, get_last_day, is_last_day_of_the_month
from frappe.utils import (
add_days,
add_months,
cint,
date_diff,
flt,
get_last_day,
getdate,
is_last_day_of_the_month,
month_diff,
)
import erpnext
class AssetDepreciationSchedule(Document):
@@ -83,15 +95,58 @@ class AssetDepreciationSchedule(Document):
date_of_return=None,
update_asset_finance_book_row=True,
):
have_asset_details_been_modified = self.have_asset_details_been_modified(asset_doc)
not_manual_depr_or_have_manual_depr_details_been_modified = (
self.not_manual_depr_or_have_manual_depr_details_been_modified(row)
)
self.set_draft_asset_depr_schedule_details(asset_doc, row)
self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
self.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
if self.should_prepare_depreciation_schedule(
have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
):
self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
self.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
def have_asset_details_been_modified(self, asset_doc):
return (
asset_doc.gross_purchase_amount != self.gross_purchase_amount
or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
or asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked
)
def not_manual_depr_or_have_manual_depr_details_been_modified(self, row):
return (
self.depreciation_method != "Manual"
or row.total_number_of_depreciations != self.total_number_of_depreciations
or row.frequency_of_depreciation != self.frequency_of_depreciation
or getdate(row.depreciation_start_date) != self.get("depreciation_schedule")[0].schedule_date
or row.expected_value_after_useful_life != self.expected_value_after_useful_life
)
def should_prepare_depreciation_schedule(
self, have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
):
if not self.get("depreciation_schedule"):
return True
old_asset_depr_schedule_doc = self.get_doc_before_save()
if self.docstatus != 0 and not old_asset_depr_schedule_doc:
return True
if have_asset_details_been_modified or not_manual_depr_or_have_manual_depr_details_been_modified:
return True
return False
def set_draft_asset_depr_schedule_details(self, asset_doc, row):
self.asset = asset_doc.name
self.finance_book = row.finance_book
self.finance_book_id = row.idx
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0
self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked or 0
self.gross_purchase_amount = asset_doc.gross_purchase_amount
self.depreciation_method = row.depreciation_method
self.total_number_of_depreciations = row.total_number_of_depreciations
self.frequency_of_depreciation = row.frequency_of_depreciation
@@ -102,7 +157,7 @@ class AssetDepreciationSchedule(Document):
def make_depr_schedule(
self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True
):
if row.depreciation_method != "Manual" and not self.get("depreciation_schedule"):
if not self.get("depreciation_schedule"):
self.depreciation_schedule = []
if not asset_doc.available_for_use_date:
@@ -141,24 +196,49 @@ class AssetDepreciationSchedule(Document):
row.db_update()
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
asset_doc.number_of_depreciations_booked
self.number_of_depreciations_booked
)
has_pro_rata = asset_doc.check_is_pro_rata(row)
has_pro_rata = _check_is_pro_rata(asset_doc, row)
if has_pro_rata:
number_of_pending_depreciations += 1
has_wdv_or_dd_non_yearly_pro_rata = False
if (
row.depreciation_method in ("Written Down Value", "Double Declining Balance")
and cint(row.frequency_of_depreciation) != 12
):
has_wdv_or_dd_non_yearly_pro_rata = _check_is_pro_rata(
asset_doc, row, wdv_or_dd_non_yearly=True
)
skip_row = False
should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date)
depreciation_amount = 0
for n in range(start, number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row:
continue
depreciation_amount = asset_doc.get_depreciation_amount(value_after_depreciation, row)
if n > 0 and len(self.get("depreciation_schedule")) > n - 1:
prev_depreciation_amount = self.get("depreciation_schedule")[n - 1].depreciation_amount
else:
prev_depreciation_amount = 0
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
depreciation_amount = get_depreciation_amount(
asset_doc,
value_after_depreciation,
row,
n,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
)
if not has_pro_rata or (
n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2
):
schedule_date = add_months(
row.depreciation_start_date, n * cint(row.frequency_of_depreciation)
)
@@ -176,26 +256,36 @@ class AssetDepreciationSchedule(Document):
if self.depreciation_schedule:
from_date = self.depreciation_schedule[-1].schedule_date
depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
row, depreciation_amount, from_date, date_of_disposal
depreciation_amount, days, months = _get_pro_rata_amt(
row,
depreciation_amount,
from_date,
date_of_disposal,
)
if depreciation_amount > 0:
self.add_depr_schedule_row(
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:
if (
(has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
and not self.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
depreciation_amount, days, months = _get_pro_rata_amt(
row,
depreciation_amount,
from_date,
row.depreciation_start_date,
has_wdv_or_dd_non_yearly_pro_rata,
)
# For first depr schedule date will be the start date
@@ -209,13 +299,17 @@ class AssetDepreciationSchedule(Document):
# 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),
(n + self.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, days, months = _get_pro_rata_amt(
row,
depreciation_amount,
schedule_date,
asset_doc.to_date,
has_wdv_or_dd_non_yearly_pro_rata,
)
depreciation_amount = self.get_adjusted_depreciation_amount(
@@ -247,7 +341,6 @@ class AssetDepreciationSchedule(Document):
self.add_depr_schedule_row(
schedule_date,
depreciation_amount,
row.depreciation_method,
)
# to ensure that final accumulated depreciation amount is accurate
@@ -274,14 +367,12 @@ class AssetDepreciationSchedule(Document):
self,
schedule_date,
depreciation_amount,
depreciation_method,
):
self.append(
"depreciation_schedule",
{
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": depreciation_method,
},
)
@@ -293,7 +384,9 @@ class AssetDepreciationSchedule(Document):
ignore_booked_entry=False,
):
straight_line_idx = [
d.idx for d in self.get("depreciation_schedule") if d.depreciation_method == "Straight Line"
d.idx
for d in self.get("depreciation_schedule")
if self.depreciation_method == "Straight Line" or self.depreciation_method == "Manual"
]
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
@@ -336,6 +429,132 @@ def _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row):
return value_after_depreciation
# if it returns True, depreciation_amount will not be equal for the first and last rows
def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
has_pro_rata = False
# if not existing asset, from_date = available_for_use_date
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022
from_date = _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly)
days = date_diff(row.depreciation_start_date, from_date) + 1
if wdv_or_dd_non_yearly:
total_days = get_total_days(row.depreciation_start_date, 12)
else:
# if frequency_of_depreciation is 12 months, total_days = 365
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
if days < total_days:
has_pro_rata = True
return has_pro_rata
def _get_modified_available_for_use_date(asset_doc, row, wdv_or_dd_non_yearly=False):
if wdv_or_dd_non_yearly:
return add_months(
asset_doc.available_for_use_date,
(asset_doc.number_of_depreciations_booked * 12),
)
else:
return add_months(
asset_doc.available_for_use_date,
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation),
)
def _get_pro_rata_amt(
row, depreciation_amount, from_date, to_date, has_wdv_or_dd_non_yearly_pro_rata=False
):
days = date_diff(to_date, from_date)
months = month_diff(to_date, from_date)
if has_wdv_or_dd_non_yearly_pro_rata:
total_days = get_total_days(to_date, 12)
else:
total_days = get_total_days(to_date, row.frequency_of_depreciation)
return (depreciation_amount * flt(days)) / flt(total_days), days, months
def get_total_days(date, frequency):
period_start_date = add_months(date, cint(frequency) * -1)
if is_last_day_of_the_month(date):
period_start_date = get_last_day(period_start_date)
return date_diff(date, period_start_date)
@erpnext.allow_regional
def get_depreciation_amount(
asset,
depreciable_value,
row,
schedule_idx=0,
prev_depreciation_amount=0,
has_wdv_or_dd_non_yearly_pro_rata=False,
):
if row.depreciation_method in ("Straight Line", "Manual"):
return get_straight_line_or_manual_depr_amount(asset, row)
else:
return get_wdv_or_dd_depr_amount(
depreciable_value,
row.rate_of_depreciation,
row.frequency_of_depreciation,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
)
def get_straight_line_or_manual_depr_amount(asset, row):
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
if asset.flags.increase_in_asset_life:
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
date_diff(asset.to_date, asset.available_for_use_date) / 365
)
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
elif asset.flags.increase_in_asset_value_due_to_repair:
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
row.total_number_of_depreciations
)
# if the Depreciation Schedule is being prepared for the first time
else:
return (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / flt(
row.total_number_of_depreciations
)
def get_wdv_or_dd_depr_amount(
depreciable_value,
rate_of_depreciation,
frequency_of_depreciation,
schedule_idx,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
):
if cint(frequency_of_depreciation) == 12:
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
else:
if has_wdv_or_dd_non_yearly_pro_rata:
if schedule_idx == 0:
return flt(depreciable_value) * (flt(rate_of_depreciation) / 100)
elif schedule_idx % (12 / cint(frequency_of_depreciation)) == 1:
return (
flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200)
)
else:
return prev_depreciation_amount
else:
if schedule_idx % (12 / cint(frequency_of_depreciation)) == 0:
return (
flt(depreciable_value) * flt(frequency_of_depreciation) * (flt(rate_of_depreciation) / 1200)
)
else:
return prev_depreciation_amount
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(
@@ -412,6 +631,16 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in (
"Written Down Value",
"Double Declining Balance",
):
new_rate_of_depreciation = flt(
asset_doc.get_depreciation_rate(row), row.precision("rate_of_depreciation")
)
row.rate_of_depreciation = new_rate_of_depreciation
new_asset_depr_schedule_doc.rate_of_depreciation = new_rate_of_depreciation
new_asset_depr_schedule_doc.make_depr_schedule(asset_doc, row, date_of_disposal)
new_asset_depr_schedule_doc.set_accumulated_depreciation(row, date_of_disposal, date_of_return)

View File

@@ -84,6 +84,8 @@ def calculate_next_due_date(
next_due_date = add_years(start_date, 1)
if periodicity == "2 Yearly":
next_due_date = add_years(start_date, 2)
if periodicity == "3 Yearly":
next_due_date = add_years(start_date, 3)
if periodicity == "Quarterly":
next_due_date = add_months(start_date, 3)
if end_date and (

View File

@@ -1,664 +1,156 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"creation": "2017-10-20 07:10:55.903571",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2017-10-20 07:10:55.903571",
"doctype": "DocType",
"document_type": "Document",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"maintenance_task",
"maintenance_type",
"column_break_2",
"maintenance_status",
"section_break_2",
"start_date",
"periodicity",
"column_break_4",
"end_date",
"certificate_required",
"section_break_9",
"assign_to",
"column_break_10",
"assign_to_name",
"section_break_10",
"next_due_date",
"column_break_14",
"last_completion_date",
"section_break_7",
"description"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "maintenance_task",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Maintenance Task",
"length": 0,
"no_copy": 0,
"options": "",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "maintenance_task",
"fieldtype": "Data",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Maintenance Task",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "maintenance_type",
"fieldtype": "Select",
"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": "Maintenance Type",
"length": 0,
"no_copy": 0,
"options": "Preventive Maintenance\nCalibration",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "maintenance_type",
"fieldtype": "Select",
"label": "Maintenance Type",
"options": "Preventive Maintenance\nCalibration"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "maintenance_status",
"fieldtype": "Select",
"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": "Maintenance Status",
"length": 0,
"no_copy": 0,
"options": "Planned\nOverdue\nCancelled",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "maintenance_status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Maintenance Status",
"options": "Planned\nOverdue\nCancelled",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_2",
"fieldtype": "Section 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,
"translatable": 0,
"unique": 0
},
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Today",
"fieldname": "start_date",
"fieldtype": "Date",
"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": "Start Date",
"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": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"default": "Today",
"fieldname": "start_date",
"fieldtype": "Date",
"label": "Start Date",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "periodicity",
"fieldtype": "Select",
"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": "Periodicity",
"length": 0,
"no_copy": 0,
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "periodicity",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Periodicity",
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nYearly\n2 Yearly\n3 Yearly",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_4",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "end_date",
"fieldtype": "Date",
"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": "End Date",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "certificate_required",
"fieldtype": "Check",
"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": "Certificate Required",
"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": 1,
"set_only_once": 1,
"translatable": 0,
"unique": 0
},
"default": "0",
"fieldname": "certificate_required",
"fieldtype": "Check",
"label": "Certificate Required",
"search_index": 1,
"set_only_once": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_9",
"fieldtype": "Section 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,
"translatable": 0,
"unique": 0
},
"fieldname": "section_break_9",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "assign_to",
"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": "Assign To",
"length": 0,
"no_copy": 0,
"options": "User",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "assign_to",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Assign To",
"options": "User"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_10",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "assign_to.full_name",
"fieldname": "assign_to_name",
"fieldtype": "Read Only",
"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": "Assign to Name",
"length": 0,
"no_copy": 0,
"options": "",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "assign_to_name",
"fieldtype": "Read Only",
"label": "Assign to Name"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_10",
"fieldtype": "Section 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,
"translatable": 0,
"unique": 0
},
"fieldname": "section_break_10",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "next_due_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": "Next Due Date",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "next_due_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Next Due Date"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_14",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_14",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "last_completion_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": "Last Completion Date",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "last_completion_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Last Completion Date"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_7",
"fieldtype": "Section 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,
"translatable": 0,
"unique": 0
},
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Text Editor",
"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": "Description",
"length": 0,
"no_copy": 0,
"options": "",
"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,
"translatable": 0,
"unique": 0
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Description"
}
],
"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-06-18 16:12:04.330021",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Maintenance Task",
"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": "2023-03-23 07:03:07.113452",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Maintenance Task",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -43,53 +43,60 @@ class AssetRepair(AccountsController):
def before_submit(self):
self.check_repair_status()
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
self.increase_asset_value()
if self.get("stock_consumption"):
self.check_for_stock_items_and_warehouse()
self.decrease_stock_quantity()
if self.get("capitalize_repair_cost"):
self.make_gl_entries()
if (
frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
and self.increase_in_asset_life
):
self.modify_depreciation_schedule()
self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
notes = _(
"This schedule was created when Asset {0} was repaired through Asset Repair {1}."
).format(
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
get_link_to_form(self.doctype, self.name),
)
self.asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
self.asset_doc.save()
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
self.increase_asset_value()
if self.get("stock_consumption"):
self.check_for_stock_items_and_warehouse()
self.decrease_stock_quantity()
if self.get("capitalize_repair_cost"):
self.make_gl_entries()
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
self.modify_depreciation_schedule()
notes = _(
"This schedule was created when Asset {0} was repaired through Asset Repair {1}."
).format(
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
get_link_to_form(self.doctype, self.name),
)
self.asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
self.asset_doc.save()
def before_cancel(self):
self.asset_doc = frappe.get_doc("Asset", self.asset)
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
self.decrease_asset_value()
if self.get("stock_consumption"):
self.increase_stock_quantity()
if self.get("capitalize_repair_cost"):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.make_gl_entries(cancel=True)
self.db_set("stock_entry", None)
if (
frappe.db.get_value("Asset", self.asset, "calculate_depreciation")
and self.increase_in_asset_life
):
self.revert_depreciation_schedule_on_cancellation()
self.asset_doc.flags.increase_in_asset_value_due_to_repair = False
notes = _("This schedule was created when Asset {0}'s Asset Repair {1} was cancelled.").format(
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
get_link_to_form(self.doctype, self.name),
)
self.asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
self.asset_doc.save()
if self.get("stock_consumption") or self.get("capitalize_repair_cost"):
self.asset_doc.flags.increase_in_asset_value_due_to_repair = True
self.decrease_asset_value()
if self.get("stock_consumption"):
self.increase_stock_quantity()
if self.get("capitalize_repair_cost"):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.make_gl_entries(cancel=True)
self.db_set("stock_entry", None)
if self.asset_doc.calculate_depreciation and self.increase_in_asset_life:
self.revert_depreciation_schedule_on_cancellation()
notes = _("This schedule was created when Asset {0}'s Asset Repair {1} was cancelled.").format(
get_link_to_form(self.asset_doc.doctype, self.asset_doc.name),
get_link_to_form(self.doctype, self.name),
)
self.asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
self.asset_doc.save()
def after_delete(self):
frappe.get_doc("Asset", self.asset).set_status()
def check_repair_status(self):
if self.repair_status == "Pending":

View File

@@ -49,7 +49,7 @@ frappe.ui.form.on('Asset Value Adjustment', {
frm.call({
method: "erpnext.assets.doctype.asset.asset.get_asset_value_after_depreciation",
args: {
asset: frm.doc.asset,
asset_name: frm.doc.asset,
finance_book: frm.doc.finance_book
},
callback: function(r) {

View File

@@ -14,6 +14,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciatio
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,
)
@@ -162,7 +163,7 @@ class AssetValueAdjustment(Document):
depreciation_amount = days * rate_per_day
from_date = data.schedule_date
else:
depreciation_amount = asset.get_depreciation_amount(value_after_depreciation, d)
depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d)
if depreciation_amount:
value_after_depreciation -= flt(depreciation_amount)

View File

@@ -12,8 +12,7 @@
"column_break_3",
"accumulated_depreciation_amount",
"journal_entry",
"make_depreciation_entry",
"depreciation_method"
"make_depreciation_entry"
],
"fields": [
{
@@ -58,20 +57,11 @@
"fieldname": "make_depreciation_entry",
"fieldtype": "Button",
"label": "Make Depreciation Entry"
},
{
"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
}
],
"istable": 1,
"links": [],
"modified": "2022-12-06 20:35:50.264281",
"modified": "2023-03-13 23:17:15.849950",
"modified_by": "Administrator",
"module": "Assets",
"name": "Depreciation Schedule",

View File

@@ -24,7 +24,7 @@ frappe.query_reports["Fixed Asset Register"] = {
"label": __("Period Based On"),
"fieldtype": "Select",
"options": ["Fiscal Year", "Date Range"],
"default": ["Fiscal Year"],
"default": "Fiscal Year",
"reqd": 1
},
{
@@ -75,12 +75,6 @@ frappe.query_reports["Fixed Asset Register"] = {
fieldtype: "Link",
options: "Asset Category"
},
{
fieldname:"finance_book",
label: __("Finance Book"),
fieldtype: "Link",
options: "Finance Book"
},
{
fieldname:"cost_center",
label: __("Cost Center"),
@@ -96,8 +90,20 @@ frappe.query_reports["Fixed Asset Register"] = {
reqd: 1
},
{
fieldname:"is_existing_asset",
label: __("Is Existing Asset"),
fieldname:"finance_book",
label: __("Finance Book"),
fieldtype: "Link",
options: "Finance Book",
depends_on: "eval: doc.only_depreciable_assets == 1",
},
{
fieldname:"only_depreciable_assets",
label: __("Only depreciable assets"),
fieldtype: "Check"
},
{
fieldname:"only_existing_assets",
label: __("Only existing assets"),
fieldtype: "Check"
},
]

View File

@@ -45,8 +45,10 @@ def get_conditions(filters):
filters.year_end_date = getdate(fiscal_year.year_end_date)
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
if filters.get("is_existing_asset"):
conditions["is_existing_asset"] = filters.get("is_existing_asset")
if filters.get("only_depreciable_assets"):
conditions["calculate_depreciation"] = filters.get("only_depreciable_assets")
if filters.get("only_existing_assets"):
conditions["is_existing_asset"] = filters.get("only_existing_assets")
if filters.get("asset_category"):
conditions["asset_category"] = filters.get("asset_category")
if filters.get("cost_center"):
@@ -102,19 +104,18 @@ def get_data(filters):
]
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
assets_linked_to_fb = frappe.db.get_all(
doctype="Asset Finance Book",
filters={"finance_book": filters.finance_book or ("is", "not set")},
pluck="parent",
)
assets_linked_to_fb = None
if filters.only_depreciable_assets:
assets_linked_to_fb = frappe.db.get_all(
doctype="Asset Finance Book",
filters={"finance_book": filters.finance_book or ("is", "not set")},
pluck="parent",
)
for asset in assets_record:
if filters.finance_book:
if asset.asset_id not in assets_linked_to_fb:
continue
else:
if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb:
continue
if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb:
continue
asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book)
row = {
@@ -151,6 +152,7 @@ def prepare_chart_data(data, filters):
filters.filter_based_on,
"Monthly",
company=filters.company,
ignore_fiscal_year=True,
)
for d in period_list:
@@ -171,11 +173,11 @@ def prepare_chart_data(data, filters):
"datasets": [
{
"name": _("Asset Value"),
"values": [d.get("asset_value") for d in labels_values_map.values()],
"values": [flt(d.get("asset_value"), 2) for d in labels_values_map.values()],
},
{
"name": _("Depreciatied Amount"),
"values": [d.get("depreciated_amount") for d in labels_values_map.values()],
"values": [flt(d.get("depreciated_amount"), 2) for d in labels_values_map.values()],
},
],
},
@@ -311,7 +313,7 @@ def get_columns(filters):
return [
{
"label": _("Asset Id"),
"label": _("Asset ID"),
"fieldtype": "Link",
"fieldname": "asset_id",
"options": "Asset",

View File

@@ -16,11 +16,14 @@
"transaction_settings_section",
"po_required",
"pr_required",
"over_order_allowance",
"column_break_12",
"maintain_same_rate",
"set_landed_cost_based_on_purchase_invoice_rate",
"allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
"disable_last_purchase_rate",
"show_pay_button",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
@@ -140,6 +143,27 @@
"fieldname": "disable_last_purchase_rate",
"fieldtype": "Check",
"label": "Disable Last Purchase Rate"
},
{
"default": "1",
"fieldname": "show_pay_button",
"fieldtype": "Check",
"label": "Show Pay Button in Purchase Order Portal"
},
{
"default": "0",
"depends_on": "eval: !doc.maintain_same_rate",
"description": "Users can enable the checkbox If they want to adjust the incoming rate (set using purchase receipt) based on the purchase invoice rate.",
"fieldname": "set_landed_cost_based_on_purchase_invoice_rate",
"fieldtype": "Check",
"label": "Set Landed Cost Based on Purchase Invoice Rate"
},
{
"default": "0",
"description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
"fieldname": "over_order_allowance",
"fieldtype": "Float",
"label": "Over Order Allowance (%)"
}
],
"icon": "fa fa-cog",
@@ -147,7 +171,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-01-09 17:08:28.828173",
"modified": "2023-03-02 17:02:14.404622",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@@ -21,3 +21,10 @@ class BuyingSettings(Document):
self.get("supp_master_name") == "Naming Series",
hide_name_field=False,
)
def before_save(self):
self.check_maintain_same_rate()
def check_maintain_same_rate(self):
if self.maintain_same_rate:
self.set_landed_cost_based_on_purchase_invoice_rate = 0

View File

@@ -236,7 +236,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
this.make_purchase_invoice, __('Create'));
if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
this.frm.add_custom_button(
__('Payment'),
() => this.make_payment_entry(),
__('Create')
);
}
if(flt(doc.per_billed) < 100) {

View File

@@ -21,6 +21,9 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
from erpnext.accounts.party import get_party_account, get_party_account_currency
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
from erpnext.controllers.buying_controller import BuyingController
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
validate_against_blanket_order,
)
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
@@ -69,6 +72,7 @@ class PurchaseOrder(BuyingController):
self.validate_with_previous_doc()
self.validate_for_subcontracting()
self.validate_minimum_order_qty()
validate_against_blanket_order(self)
if self.is_old_subcontracting_flow:
self.validate_bom_for_subcontracting_items()

View File

@@ -113,7 +113,10 @@ class RequestforQuotation(BuyingController):
def get_link(self):
# RFQ link for supplier portal
return get_url("/app/request-for-quotation/" + self.name)
route = frappe.db.get_value(
"Portal Menu Item", {"reference_doctype": "Request for Quotation"}, ["route"]
)
return get_url("/app/{0}/".format(route) + self.name)
def update_supplier_part_no(self, supplier):
self.vendor = supplier

View File

@@ -64,7 +64,7 @@ frappe.ui.form.on("Supplier", {
// custom buttons
frm.add_custom_button(__('Accounting Ledger'), function () {
frappe.set_route('query-report', 'General Ledger',
{ party_type: 'Supplier', party: frm.doc.name });
{ party_type: 'Supplier', party: frm.doc.name, party_name: frm.doc.supplier_name });
}, __("View"));
frm.add_custom_button(__('Accounts Payable'), function () {

View File

@@ -23,7 +23,6 @@
"default_bank_account",
"column_break_10",
"default_price_list",
"payment_terms",
"internal_supplier_section",
"is_internal_supplier",
"represents_company",
@@ -53,6 +52,7 @@
"supplier_primary_address",
"primary_address",
"accounting_tab",
"payment_terms",
"accounts",
"settings_tab",
"allow_purchase_invoice_creation_without_purchase_order",
@@ -457,11 +457,10 @@
"link_fieldname": "party"
}
],
"modified": "2022-11-09 18:02:59.075203",
"modified": "2023-02-18 11:05:50.592270",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [

View File

@@ -125,18 +125,9 @@ class Supplier(TransactionBase):
def on_trash(self):
if self.supplier_primary_contact:
frappe.db.sql(
"""
UPDATE `tabSupplier`
SET
supplier_primary_contact=null,
supplier_primary_address=null,
mobile_no=null,
email_id=null,
primary_address=null
WHERE name=%(name)s""",
{"name": self.name},
)
self.db_set("supplier_primary_contact", None)
if self.supplier_primary_address:
self.db_set("supplier_primary_address", None)
delete_contact_and_address("Supplier", self.name)

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